Skip to content

Commit cf573ed

Browse files
authored
Fix a few mismatches between the two link resolver implementations (#375)
* Avoid reporting mismatches for symbols that won't have pages * Avoid traversing symbols that are not in the symbol index * Handle collisions that only occur when symbol names are mapped to file names * Don't add default implementation symbols to the target's parent path * Fix a data race when gathering link resolution mismatches. Also, avoid percent encoding in the gathered info. * Fix incorrect behaviors when both link resolvers run to gather mismatches * Remove real file paths in test data * Combine two consecutive if-statements with same condition. rdar://99891308
1 parent 0324af2 commit cf573ed

File tree

7 files changed

+5384
-34
lines changed

7 files changed

+5384
-34
lines changed

Sources/SwiftDocC/Infrastructure/DocumentationContext.swift

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1137,7 +1137,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
11371137
// Build references for all symbols in all of this module's symbol graphs.
11381138
let symbolReferences: [SymbolGraph.Symbol.Identifier : [ResolvedTopicReference]]
11391139
if LinkResolutionMigrationConfiguration.shouldUseHierarchyBasedLinkResolver {
1140-
symbolReferences = hierarchyBasedLinkResolver!.referencesForSymbols(in:symbolGraphLoader.unifiedGraphs, bundle: bundle, context: self)
1140+
symbolReferences = hierarchyBasedLinkResolver!.referencesForSymbols(in: symbolGraphLoader.unifiedGraphs, bundle: bundle, context: self)
11411141
.mapValues({ [$0] }) // The documentation cache implementation uses an array of values to handle multi languages
11421142
} else {
11431143
symbolReferences = documentationCacheBasedLinkResolver.referencesForSymbols(in: symbolGraphLoader.unifiedGraphs, bundle: bundle, context: self)
@@ -1275,9 +1275,11 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
12751275

12761276
try shouldContinueRegistration()
12771277

1278-
if let hierarchyBasedLinkResolver = hierarchyBasedLinkResolver {
1279-
// Map the resolved references with their identifiers
1280-
hierarchyBasedLinkResolver.addMappingForSymbols(symbolIndex: symbolIndex)
1278+
// Only add the symbol mapping now if the path hierarchy based resolver is the main implementation.
1279+
// If it is only used for mismatch checking then we must wait until the documentation cache code path has traversed and updated all the colliding nodes.
1280+
// Otherwise the mappings will save the unmodified references and the hierarchy based resolver won't find the expected parent nodes when resolving links.
1281+
if LinkResolutionMigrationConfiguration.shouldUseHierarchyBasedLinkResolver {
1282+
hierarchyBasedLinkResolver!.addMappingForSymbols(symbolIndex: symbolIndex)
12811283
}
12821284

12831285
if hierarchyBasedLinkResolver == nil || LinkResolutionMigrationConfiguration.shouldReportLinkResolutionMismatches {
@@ -1311,20 +1313,22 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
13111313
})
13121314
}
13131315

1314-
13151316
// Only update the nodes and symbol index when using the documentation cache-based link resolver otherwise the context will end up in an inconsistent state.
1316-
if hierarchyBasedLinkResolver == nil {
1317+
if !LinkResolutionMigrationConfiguration.shouldUseHierarchyBasedLinkResolver {
13171318
// Update the children of collision URLs. Walk the tree and update any dependents of updated URLs
13181319
for moduleReference in moduleReferences.values {
13191320
try symbolsURLHierarchy.traversePreOrder(from: moduleReference) { reference in
13201321
try self.documentationCacheBasedLinkResolver.updateNodeWithReferenceIfCollisionChild(reference, symbolsURLHierarchy: &symbolsURLHierarchy, symbolIndex: &symbolIndex, context: self)
13211322
}
13221323
}
1324+
1325+
// Now that the colliding references have been updated, the hierarchy based resolver can save the reference to identifier mapping.
1326+
hierarchyBasedLinkResolver?.addMappingForSymbols(symbolIndex: symbolIndex)
13231327
}
13241328

13251329
if LinkResolutionMigrationConfiguration.shouldReportLinkResolutionPathMismatches {
13261330
// The way that symbol path mismatches are gathered depend on which link resolution implementation is used to resolve links.
1327-
if let hierarchyBasedLinkResolver = hierarchyBasedLinkResolver {
1331+
if LinkResolutionMigrationConfiguration.shouldUseHierarchyBasedLinkResolver {
13281332
// Attempting to use the The documentation cache based link resolver to compute the disambiguated symbol paths when it is not in full control of the symbol index,
13291333
// topic graph, and documentation cache will result in the wrong behavior. The issues range from incorrectly computed paths to precondition failures.
13301334
//
@@ -1337,7 +1341,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
13371341
// Luckily the path hierarchy describe the hierarchical relationships between the symbols in a way where it's possible to get the `Symbol.Identifier` for each symbol.
13381342
// By sorting the pairs of `(symbol, parent symbol)` by the symbol's number of path components it's possible to traverse the hierarchy breath first and update the initial disambiguated
13391343
// paths from the documentation cache based link resolver so that each child path matches the disambiguation from the parent path.
1340-
let sortedSymbolAndParentPairs = hierarchyBasedLinkResolver.pathHierarchy.lookup.values.lazy
1344+
let sortedSymbolAndParentPairs = hierarchyBasedLinkResolver!.pathHierarchy.lookup.values.lazy
13411345
.filter { $0.symbol != nil && $0.parent?.symbol != nil }
13421346
.sorted(by: \.symbol!.pathComponents.count)
13431347
.map { ($0.symbol!.identifier, $0.parent!.symbol!.identifier) }
@@ -1362,7 +1366,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
13621366
linkResolutionMismatches.pathsWithMismatchedDisambiguation[hierarchyBasedReference.path] = cacheBasedMainReference.path
13631367
}
13641368
}
1365-
for (id, cacheBasedReference) in cacheBasedReferences where !hierarchyBasedReferences.keys.contains(id) {
1369+
for (id, cacheBasedReference) in cacheBasedReferences where !hierarchyBasedReferences.keys.contains(id) && symbolGraphLoader.hasPrimaryURL(moduleName: cacheBasedReference.pathComponents[2]) {
13661370
linkResolutionMismatches.missingPathsInHierarchyBasedLinkResolver.append(cacheBasedReference.path)
13671371
}
13681372
} else {
@@ -2553,15 +2557,15 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
25532557
let cacheBasedResult = documentationCacheBasedLinkResolver.resolve(unresolvedReference, in: parent, fromSymbolLink: isCurrentlyResolvingSymbolLink, context: self)
25542558

25552559
let inputInfo = LinkResolutionMismatches.FailedLinkInfo(
2556-
path: unresolvedReference.topicURL.url.withoutHostAndPortAndScheme().absoluteString,
2557-
parent: parent.url.withoutHostAndPortAndScheme().absoluteString,
2560+
path: unresolvedReference.topicURL.url.path + (unresolvedReference.topicURL.url.fragment.map { "#" + $0 } ?? ""),
2561+
parent: parent.url.path,
25582562
asSymbolLink: isCurrentlyResolvingSymbolLink
25592563
)
25602564
switch (hierarchyBasedResult, cacheBasedResult) {
25612565
case (.success, .failure):
2562-
linkResolutionMismatches.mismatchedLinksThatCacheBasedLinkResolverFailedToResolve.insert(inputInfo)
2566+
linkResolutionMismatches.mismatchedLinksThatCacheBasedLinkResolverFailedToResolve.sync { $0.insert(inputInfo) }
25632567
case (.failure, .success):
2564-
linkResolutionMismatches.mismatchedLinksThatHierarchyBasedLinkResolverFailedToResolve.insert(inputInfo)
2568+
linkResolutionMismatches.mismatchedLinksThatHierarchyBasedLinkResolverFailedToResolve.sync { $0.insert(inputInfo) }
25652569
default:
25662570
break // No difference to report
25672571
}

Sources/SwiftDocC/Infrastructure/Link Resolution/LinkResolutionMigration.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,10 @@ final class LinkResolutionMismatches {
8080
}
8181

8282
/// Links that resolved in the cache-based implementation but not the path hierarchy-based implementation
83-
var mismatchedLinksThatHierarchyBasedLinkResolverFailedToResolve = Set<FailedLinkInfo>()
83+
var mismatchedLinksThatHierarchyBasedLinkResolverFailedToResolve: Synchronized<Set<FailedLinkInfo>> = .init([])
8484

8585
/// Links that resolved in the path hierarchy-based implementation but not the cache-based implementation.
86-
var mismatchedLinksThatCacheBasedLinkResolverFailedToResolve = Set<FailedLinkInfo>()
86+
var mismatchedLinksThatCacheBasedLinkResolverFailedToResolve: Synchronized<Set<FailedLinkInfo>> = .init([])
8787
}
8888

8989
// MARK: Reporting mismatches
@@ -122,8 +122,8 @@ extension LinkResolutionMismatches {
122122
return
123123
}
124124

125-
let mismatchedFailedCacheResults = mismatchedLinksThatCacheBasedLinkResolverFailedToResolve
126-
let mismatchedFailedHierarchyResults = mismatchedLinksThatHierarchyBasedLinkResolverFailedToResolve
125+
let mismatchedFailedCacheResults = mismatchedLinksThatCacheBasedLinkResolverFailedToResolve.sync({ $0 })
126+
let mismatchedFailedHierarchyResults = mismatchedLinksThatHierarchyBasedLinkResolverFailedToResolve.sync({ $0 })
127127

128128
if mismatchedFailedCacheResults.isEmpty && mismatchedFailedHierarchyResults.isEmpty {
129129
print("\(prefix) Both link resolver implementations succeeded and failed to resolve the same links.")

Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy.swift

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -133,18 +133,6 @@ struct PathHierarchy {
133133
continue
134134
}
135135
}
136-
for relationship in graph.relationships where [.defaultImplementationOf].contains(relationship.kind) {
137-
guard let sourceNode = nodes[relationship.source], sourceNode.parent == nil else {
138-
continue
139-
}
140-
topLevelCandidates.removeValue(forKey: relationship.source)
141-
guard let targetParent = nodes[relationship.target]?.parent else {
142-
continue
143-
}
144-
if targetParent.add(symbolChild: sourceNode) {
145-
nodes[relationship.source] = nil
146-
}
147-
}
148136

149137
// The hierarchy doesn't contain any non-symbol nodes yet. It's OK to unwrap the `symbol` property.
150138
for topLevelNode in topLevelCandidates.values where topLevelNode.symbol!.pathComponents.count == 1 {
@@ -688,6 +676,12 @@ extension PathHierarchy {
688676

689677
// MARK: Determining disambiguated paths
690678

679+
private let nonAllowedPathCharacters = CharacterSet.urlPathAllowed.inverted
680+
681+
private func symbolFileName(_ symbolName: String) -> String {
682+
return symbolName.components(separatedBy: nonAllowedPathCharacters).joined(separator: "_")
683+
}
684+
691685
extension PathHierarchy {
692686
/// Determines the least disambiguated paths for all symbols in the path hierarchy.
693687
///
@@ -701,7 +695,7 @@ extension PathHierarchy {
701695
) -> [String: String] {
702696
func descend(_ node: Node, accumulatedPath: String) -> [(String, (String, Bool))] {
703697
var results: [(String, (String, Bool))] = []
704-
let caseInsensitiveChildren = [String: DisambiguationTree](node.children.map { ($0.key.lowercased(), $0.value) }, uniquingKeysWith: { $0.merge(with: $1) })
698+
let caseInsensitiveChildren = [String: DisambiguationTree](node.children.map { (symbolFileName($0.key.lowercased()), $0.value) }, uniquingKeysWith: { $0.merge(with: $1) })
705699

706700
for (_, tree) in caseInsensitiveChildren {
707701
let disambiguatedChildren = tree.disambiguatedValuesWithCollapsedUniqueSymbols(includeLanguage: includeLanguage)
@@ -720,9 +714,9 @@ extension PathHierarchy {
720714
if hash != "_" {
721715
knownDisambiguation += "-\(hash)"
722716
}
723-
path = accumulatedPath + "/" + node.name + knownDisambiguation
717+
path = accumulatedPath + "/" + symbolFileName(node.name) + knownDisambiguation
724718
} else {
725-
path = accumulatedPath + "/" + node.name
719+
path = accumulatedPath + "/" + symbolFileName(node.name)
726720
}
727721
if let symbol = node.symbol {
728722
results.append(
@@ -749,7 +743,23 @@ extension PathHierarchy {
749743
}
750744

751745
// If a symbol node exist in multiple languages, prioritize the Swift variant.
752-
return [String: (String, Bool)](gathered, uniquingKeysWith: { lhs, rhs in lhs.1 ? lhs : rhs }).mapValues({ $0.0 })
746+
let result = [String: (String, Bool)](gathered, uniquingKeysWith: { lhs, rhs in lhs.1 ? lhs : rhs }).mapValues({ $0.0 })
747+
748+
assert(
749+
Set(result.values).count == result.keys.count,
750+
{
751+
let collisionDescriptions = result
752+
.reduce(into: [String: [String]](), { $0[$1.value, default: []].append($1.key) })
753+
.filter({ $0.value.count > 1 })
754+
.map { "\($0.key)\n\($0.value.map({ " " + $0 }).joined(separator: "\n"))" }
755+
return """
756+
Disambiguated paths contain \(collisionDescriptions.count) collision(s):
757+
\(collisionDescriptions.joined(separator: "\n"))
758+
"""
759+
}()
760+
)
761+
762+
return result
753763
}
754764
}
755765

Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchyBasedLinkResolver.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,10 @@ final class PathHierarchyBasedLinkResolver {
5151
guard node.symbol != nil else { continue }
5252

5353
guard let parentID = node.parent?.identifier else { continue }
54-
observe(resolvedReferenceMap[id]!, resolvedReferenceMap[parentID]!)
54+
55+
// Only symbols in the symbol index are added to the reference map.
56+
guard let reference = resolvedReferenceMap[id], let parentReference = resolvedReferenceMap[parentID] else { continue }
57+
observe(reference, parentReference)
5558
}
5659
}
5760

0 commit comments

Comments
 (0)