Skip to content

Commit c6e32f6

Browse files
authored
Fix inconsistent assertions with some language variant symbols (#894)
* Add more information in debug assert message * Fix a bug where symbol with only capitalization differences in their language counterparts (same ID and kind) sometimes wouldn't get assigned a path. rdar://125948615 * Fix a bug where extensions to symbols with different kinds in each language representation hit a debug assertion about inconsistent path hierarchies rdar://125948615 * Minor implementation comment refinements
1 parent 93a2982 commit c6e32f6

File tree

2 files changed

+135
-25
lines changed

2 files changed

+135
-25
lines changed

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

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -157,9 +157,23 @@ struct PathHierarchy {
157157
if let targetNode = nodes[relationship.target], targetNode.name == expectedContainerName {
158158
targetNode.add(symbolChild: sourceNode)
159159
topLevelCandidates.removeValue(forKey: relationship.source)
160-
} else if let targetNodes = allNodes[relationship.target] {
161-
for targetNode in targetNodes where targetNode.name == expectedContainerName {
160+
} else if var targetNodes = allNodes[relationship.target] {
161+
// If the source was added in an extension symbol graph file, then its target won't be found in the same symbol graph file (in `nodes`).
162+
163+
// We may have encountered multiple language representations of the target symbol. Try to find the best matching representation of the target to add the source to.
164+
// Remove any targets that don't match the source symbol's path components (see comment above for more details).
165+
targetNodes.removeAll(where: { $0.name != expectedContainerName })
166+
167+
// Prefer the symbol that matches the relationship's language.
168+
if let targetNode = targetNodes.first(where: { $0.symbol!.identifier.interfaceLanguage == language?.id }) {
162169
targetNode.add(symbolChild: sourceNode)
170+
} else {
171+
// It's not clear which target to add the source to, so we add it to all of them.
172+
// This will likely hit a _debug_ assertion (later in this initializer) about inconsistent traversal through the hierarchy,
173+
// but in release builds DocC will "repair" the inconsistent hierarchy.
174+
for targetNode in targetNodes {
175+
targetNode.add(symbolChild: sourceNode)
176+
}
163177
}
164178
topLevelCandidates.removeValue(forKey: relationship.source)
165179
} else {
@@ -294,8 +308,9 @@ struct PathHierarchy {
294308
assert(element.node.parent === node, {
295309
func describe(_ node: Node?) -> String {
296310
guard let node else { return "<nil>" }
297-
guard let identifier = node.symbol?.identifier else { return node.name }
298-
return "\(identifier.precise) (\(identifier.interfaceLanguage))"
311+
guard let symbol = node.symbol else { return node.name }
312+
let id = symbol.identifier
313+
return "\(id.precise) (\(id.interfaceLanguage).\(symbol.kind.identifier.identifier)) [\(symbol.pathComponents.joined(separator: "/"))]"
299314
}
300315
return """
301316
Every child node should point back to its parent so that the tree can be traversed both up and down without any dead-ends. \
@@ -613,8 +628,19 @@ extension PathHierarchy.DisambiguationContainer {
613628
/// Combines the data from this tree with another tree to form a new, merged disambiguation tree.
614629
func merge(with other: Self) -> Self {
615630
var newStorage = storage
616-
for element in other.storage where !storage.contains(where: { $0.matches(kind: element.kind, hash: element.hash )}) {
617-
newStorage.append(element)
631+
for element in other.storage {
632+
if let existingIndex = storage.firstIndex(where: { $0.matches(kind: element.kind, hash: element.hash )}) {
633+
let existing = storage[existingIndex]
634+
// If the same element exist in both containers, keep it unless the "other" element is the Swift counterpart of this symbol.
635+
if existing.node.counterpart === element.node,
636+
element.node.symbol?.identifier.interfaceLanguage == "swift"
637+
{
638+
// The "other" element is the Swift counterpart. Replace the existing element with it.
639+
newStorage[existingIndex] = element
640+
}
641+
} else {
642+
newStorage.append(element)
643+
}
618644
}
619645
return .init(storage: newStorage)
620646
}

Tests/SwiftDocCTests/Infrastructure/PathHierarchyTests.swift

Lines changed: 103 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1716,7 +1716,7 @@ class PathHierarchyTests: XCTestCase {
17161716
let exampleDocumentation = Folder(name: "unit-test.docc", content: [
17171717
JSONFile(name: "Module.symbols.json", content: makeSymbolGraph(
17181718
moduleName: "Module",
1719-
symbols: symbolPaths.map { ($0.joined(separator: "."), .swift, $0) }
1719+
symbols: symbolPaths.map { ($0.joined(separator: "."), .swift, $0, .class) }
17201720
)),
17211721
])
17221722
let tempURL = try createTemporaryDirectory()
@@ -1744,7 +1744,7 @@ class PathHierarchyTests: XCTestCase {
17441744
XCTAssertEqual(paths["X.Y2.Z.W"], "/Module/X/Y2/Z/W")
17451745
}
17461746

1747-
func testMixedLanguageSymbolAndItsExtendingModule() throws {
1747+
func testMixedLanguageSymbolWithSameKindAndAddedMemberFromExtendingModule() throws {
17481748
let containerID = "some-container-symbol-id"
17491749
let memberID = "some-member-symbol-id"
17501750

@@ -1753,7 +1753,7 @@ class PathHierarchyTests: XCTestCase {
17531753
JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph(
17541754
moduleName: "ModuleName",
17551755
symbols: [
1756-
(containerID, .objectiveC, ["ContainerName"])
1756+
(containerID, .objectiveC, ["ContainerName"], .class)
17571757
]
17581758
)),
17591759
]),
@@ -1762,14 +1762,14 @@ class PathHierarchyTests: XCTestCase {
17621762
JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph(
17631763
moduleName: "ModuleName",
17641764
symbols: [
1765-
(containerID, .swift, ["ContainerName"])
1765+
(containerID, .swift, ["ContainerName"], .class)
17661766
]
17671767
)),
17681768

17691769
JSONFile(name: "[email protected]", content: makeSymbolGraph(
17701770
moduleName: "ExtendingModule",
17711771
symbols: [
1772-
(memberID, .swift, ["ContainerName", "MemberName"])
1772+
(memberID, .swift, ["ContainerName", "MemberName"], .property)
17731773
],
17741774
relationships: [
17751775
.init(source: memberID, target: containerID, kind: .memberOf, targetFallback: nil)
@@ -1787,6 +1787,90 @@ class PathHierarchyTests: XCTestCase {
17871787
XCTAssertEqual(paths[memberID], "/ModuleName/ContainerName/MemberName")
17881788
}
17891789

1790+
func testMixedLanguageSymbolWithDifferentKindsAndAddedMemberFromExtendingModule() throws {
1791+
let containerID = "some-container-symbol-id"
1792+
let memberID = "some-member-symbol-id"
1793+
1794+
let exampleDocumentation = Folder(name: "unit-test.docc", content: [
1795+
Folder(name: "clang", content: [
1796+
JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph(
1797+
moduleName: "ModuleName",
1798+
symbols: [
1799+
(containerID, .objectiveC, ["ContainerName"], .typealias)
1800+
]
1801+
)),
1802+
]),
1803+
1804+
Folder(name: "swift", content: [
1805+
JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph(
1806+
moduleName: "ModuleName",
1807+
symbols: [
1808+
(containerID, .swift, ["ContainerName"], .struct)
1809+
]
1810+
)),
1811+
1812+
JSONFile(name: "[email protected]", content: makeSymbolGraph(
1813+
moduleName: "ExtendingModule",
1814+
symbols: [
1815+
(memberID, .swift, ["ContainerName", "MemberName"], .property)
1816+
],
1817+
relationships: [
1818+
.init(source: memberID, target: containerID, kind: .memberOf, targetFallback: nil)
1819+
]
1820+
)),
1821+
])
1822+
])
1823+
1824+
let tempURL = try createTempFolder(content: [exampleDocumentation])
1825+
let (_, _, context) = try loadBundle(from: tempURL)
1826+
let tree = context.linkResolver.localResolver.pathHierarchy
1827+
1828+
let paths = tree.caseInsensitiveDisambiguatedPaths()
1829+
XCTAssertEqual(paths[containerID], "/ModuleName/ContainerName")
1830+
XCTAssertEqual(paths[memberID], "/ModuleName/ContainerName/MemberName")
1831+
}
1832+
1833+
func testLanguageRepresentationsWithDifferentCapitalization() throws {
1834+
let containerID = "some-container-symbol-id"
1835+
let memberID = "some-member-symbol-id"
1836+
1837+
let exampleDocumentation = Folder(name: "unit-test.docc", content: [
1838+
Folder(name: "clang", content: [
1839+
JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph(
1840+
moduleName: "ModuleName",
1841+
symbols: [
1842+
(containerID, .objectiveC, ["ContainerName"], .class),
1843+
(memberID, .objectiveC, ["ContainerName", "MemberName"], .property), // member starts with uppercase "M"
1844+
],
1845+
relationships: [
1846+
.init(source: memberID, target: containerID, kind: .memberOf, targetFallback: nil)
1847+
]
1848+
)),
1849+
]),
1850+
1851+
Folder(name: "swift", content: [
1852+
JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph(
1853+
moduleName: "ModuleName",
1854+
symbols: [
1855+
(containerID, .swift, ["ContainerName"], .class),
1856+
(memberID, .swift, ["ContainerName", "memberName"], .property), // member starts with lowercase "m"
1857+
],
1858+
relationships: [
1859+
.init(source: memberID, target: containerID, kind: .memberOf, targetFallback: nil)
1860+
]
1861+
)),
1862+
])
1863+
])
1864+
1865+
let tempURL = try createTempFolder(content: [exampleDocumentation])
1866+
let (_, _, context) = try loadBundle(from: tempURL)
1867+
let tree = context.linkResolver.localResolver.pathHierarchy
1868+
1869+
let paths = tree.caseInsensitiveDisambiguatedPaths()
1870+
XCTAssertEqual(paths[containerID], "/ModuleName/ContainerName")
1871+
XCTAssertEqual(paths[memberID], "/ModuleName/ContainerName/memberName") // The Swift spelling is preferred
1872+
}
1873+
17901874
func testMixedLanguageSymbolAndItsExtendingModuleWithDifferentContainerNames() throws {
17911875
let containerID = "some-container-symbol-id"
17921876
let memberID = "some-member-symbol-id"
@@ -1796,7 +1880,7 @@ class PathHierarchyTests: XCTestCase {
17961880
JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph(
17971881
moduleName: "ModuleName",
17981882
symbols: [
1799-
(containerID, .objectiveC, ["ObjectiveCContainerName"])
1883+
(containerID, .objectiveC, ["ObjectiveCContainerName"], .class)
18001884
]
18011885
)),
18021886
]),
@@ -1805,14 +1889,14 @@ class PathHierarchyTests: XCTestCase {
18051889
JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph(
18061890
moduleName: "ModuleName",
18071891
symbols: [
1808-
(containerID, .swift, ["SwiftContainerName"])
1892+
(containerID, .swift, ["SwiftContainerName"], .class)
18091893
]
18101894
)),
18111895

18121896
JSONFile(name: "[email protected]", content: makeSymbolGraph(
18131897
moduleName: "ExtendingModule",
18141898
symbols: [
1815-
(memberID, .swift, ["SwiftContainerName", "MemberName"])
1899+
(memberID, .swift, ["SwiftContainerName", "MemberName"], .property)
18161900
],
18171901
relationships: [
18181902
.init(source: memberID, target: containerID, kind: .memberOf, targetFallback: nil)
@@ -1839,9 +1923,9 @@ class PathHierarchyTests: XCTestCase {
18391923
JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph(
18401924
moduleName: "ModuleName",
18411925
symbols: [
1842-
(containerID, .swift, ["ContainerName"]),
1843-
(otherID, .swift, ["ContainerName"]),
1844-
(memberID, .swift, ["ContainerName", "MemberName1"]),
1926+
(containerID, .swift, ["ContainerName"], .class),
1927+
(otherID, .swift, ["ContainerName"], .class),
1928+
(memberID, .swift, ["ContainerName", "MemberName1"], .property),
18451929
],
18461930
relationships: [
18471931
.init(source: memberID, target: containerID, kind: .optionalMemberOf, targetFallback: nil),
@@ -1864,7 +1948,7 @@ class PathHierarchyTests: XCTestCase {
18641948
JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph(
18651949
moduleName: "ModuleName",
18661950
symbols: [
1867-
("some-symbol-id", .swift, ["SymbolName"]),
1951+
("some-symbol-id", .swift, ["SymbolName"], .class),
18681952
],
18691953
relationships: []
18701954
)),
@@ -1963,7 +2047,7 @@ class PathHierarchyTests: XCTestCase {
19632047
JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph(
19642048
moduleName: "ModuleName",
19652049
symbols: [
1966-
(symbolID, .swift, ["SymbolName"]),
2050+
(symbolID, .swift, ["SymbolName"], .class),
19672051
],
19682052
relationships: []
19692053
)),
@@ -1997,9 +2081,9 @@ class PathHierarchyTests: XCTestCase {
19972081
moduleName: "ModuleName",
19982082
platformName: platformName,
19992083
symbols: [
2000-
(protocolID, .swift, ["SomeProtocolName"]),
2001-
(protocolRequirementID, .swift, ["SomeProtocolName", "someProtocolRequirement()"]),
2002-
(defaultImplementationID, .swift, ["SomeConformingType", "someProtocolRequirement()"]),
2084+
(protocolID, .swift, ["SomeProtocolName"], .class),
2085+
(protocolRequirementID, .swift, ["SomeProtocolName", "someProtocolRequirement()"], .class),
2086+
(defaultImplementationID, .swift, ["SomeConformingType", "someProtocolRequirement()"], .class),
20032087
],
20042088
relationships: [
20052089
.init(source: protocolRequirementID, target: protocolID, kind: .requirementOf, targetFallback: nil),
@@ -2089,20 +2173,20 @@ class PathHierarchyTests: XCTestCase {
20892173
private func makeSymbolGraph(
20902174
moduleName: String,
20912175
platformName: String? = nil,
2092-
symbols: [(identifier: String, language: SourceLanguage, pathComponents: [String])],
2176+
symbols: [(identifier: String, language: SourceLanguage, pathComponents: [String], kindID: SymbolGraph.Symbol.KindIdentifier)],
20932177
relationships: [SymbolGraph.Relationship] = []
20942178
) -> SymbolGraph {
20952179
return SymbolGraph(
20962180
metadata: SymbolGraph.Metadata(formatVersion: .init(major: 0, minor: 5, patch: 3), generator: "unit-test"),
20972181
module: SymbolGraph.Module(name: moduleName, platform: .init(operatingSystem: platformName.map { .init(name: $0) })),
2098-
symbols: symbols.map { identifier, language, pathComponents in
2182+
symbols: symbols.map { identifier, language, pathComponents, kindID in
20992183
SymbolGraph.Symbol(
21002184
identifier: .init(precise: identifier, interfaceLanguage: language.id),
21012185
names: .init(title: "SymbolName", navigator: nil, subHeading: nil, prose: nil), // names doesn't matter for path disambiguation
21022186
pathComponents: pathComponents,
21032187
docComment: nil,
21042188
accessLevel: .public,
2105-
kind: .init(parsedIdentifier: .class, displayName: "Kind Display Name"), // kind display names doesn't matter for path disambiguation
2189+
kind: .init(parsedIdentifier: kindID, displayName: "Kind Display Name"), // kind display names doesn't matter for path disambiguation
21062190
mixins: [:]
21072191
)
21082192
},

0 commit comments

Comments
 (0)