Skip to content

Commit baf3a32

Browse files
committed
Make identifier cache key creation deterministic
We shouldn't rely on deterministic behavior when calling `.map` on a Set. It seems possible we shouldn't be including langauge strings when creating identifiers for the reference cache but we can look at that in a future PR.
1 parent 57d2955 commit baf3a32

File tree

2 files changed

+32
-2
lines changed

2 files changed

+32
-2
lines changed

Sources/SwiftDocC/Model/Identifier.swift

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ public struct ResolvedTopicReference: Hashable, Codable, Equatable, CustomString
126126

127127
/// The reference cache key
128128
var cacheKey: String {
129-
return "\(path):\(fragment ?? ""):\(sourceLanguages.map(\.id))"
129+
return Self.cacheKey(path: path, fragment: fragment, sourceLanguages: sourceLanguages)
130130
}
131131

132132
/// - Note: The `path` parameter is escaped to a path readable string.
@@ -137,7 +137,7 @@ public struct ResolvedTopicReference: Hashable, Codable, Equatable, CustomString
137137
public init(bundleIdentifier: String, path: String, fragment: String? = nil, sourceLanguages: Set<SourceLanguage>) {
138138
precondition(!sourceLanguages.isEmpty, "ResolvedTopicReference.sourceLanguages cannot be empty")
139139
// Check for a cached instance of the reference
140-
let key = "\(path):\(fragment ?? ""):\(sourceLanguages.map(\.id))"
140+
let key = Self.cacheKey(path: path, fragment: fragment, sourceLanguages: sourceLanguages)
141141
let cached = Self.sharedPool.sync { $0[bundleIdentifier]?[key] }
142142
if let resolved = cached {
143143
self = resolved
@@ -155,6 +155,20 @@ public struct ResolvedTopicReference: Hashable, Codable, Equatable, CustomString
155155
Self.sharedPool.sync { $0[bundleIdentifier, default: [:]][cacheKey] = self }
156156
}
157157

158+
private static func cacheKey(
159+
path: String,
160+
fragment: String?,
161+
sourceLanguages: Set<SourceLanguage>
162+
) -> String {
163+
let sourceLanguagesString = sourceLanguages.map(\.id).sorted().joined(separator: "-")
164+
165+
if let fragment = fragment {
166+
return "\(path):\(fragment):\(sourceLanguagesString)"
167+
} else {
168+
return "\(path):\(sourceLanguagesString)"
169+
}
170+
}
171+
158172
/// The topic URL as you would write in a link.
159173
private (set) public var url: URL! = nil
160174

Tests/SwiftDocCTests/Model/IdentifierTests.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,4 +121,20 @@ class IdentifierTests: XCTestCase {
121121
ref1 = ref1.removingLastPathComponent()
122122
XCTAssertEqual(ref1.absoluteString, "doc://bundle/MyClass")
123123
}
124+
125+
func testMixedLanguageCacheKeyCreationIsDeterministic() {
126+
for _ in 0..<20 {
127+
let firstReference = ResolvedTopicReference(
128+
bundleIdentifier: "bundle",
129+
path: "/MyClass",
130+
sourceLanguages: [.swift, .objectiveC]
131+
)
132+
133+
XCTAssertEqual(
134+
firstReference.cacheKey,
135+
"/MyClass:occ-swift",
136+
"The reference's cache key is not deterministic."
137+
)
138+
}
139+
}
124140
}

0 commit comments

Comments
 (0)