Skip to content

Commit dd9c44c

Browse files
authored
Better support for relative links when multiple symbols in the hierarchy have the same name (#578)
* Walk up the path hierarchy if links fail to resolve in a specific scope rdar://108672152 Also, check the module's scope if the link couldn't otherwise resolve rdar://76252171 * Fix test linking to heading that doesn't exist * Update expression that was very slow to type check * Fix warning about mutating a captured sendable value * Remove outdated comment about adding more test assertions * Update test for old link resolver implementation
1 parent de843c4 commit dd9c44c

File tree

7 files changed

+1033
-163
lines changed

7 files changed

+1033
-163
lines changed

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

Lines changed: 153 additions & 143 deletions
Large diffs are not rendered by default.

Sources/SwiftDocC/Model/Rendering/Symbol/ConformanceSection.swift

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,16 @@ public struct ConformanceSection: Codable, Equatable {
8787
}
8888

8989
// Adds "," or ", and" to the requirements wherever necessary.
90-
let merged = zip(rendered, separators).flatMap({ $0 + [$1] })
91-
+ rendered[separators.count...].flatMap({ $0 })
90+
var merged: [RenderInlineContent] = []
91+
merged.reserveCapacity(rendered.count * 4) // 3 for each constraint and 1 for each separator
92+
for (constraint, separator) in zip(rendered, separators) {
93+
merged.append(contentsOf: constraint)
94+
merged.append(separator)
95+
}
96+
merged.append(contentsOf: rendered.last!)
97+
merged.append(.text("."))
9298

93-
self.constraints = merged + [RenderInlineContent.text(".")]
99+
self.constraints = merged
94100
}
95101

96102
private static let selfPrefix = "Self."

Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContextTests.swift

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3040,6 +3040,83 @@ let expected = """
30403040
}
30413041
}
30423042

3043+
func testMatchesDocumentationExtensionsRelativeToModule() throws {
3044+
try XCTSkipUnless(LinkResolutionMigrationConfiguration.shouldUseHierarchyBasedLinkResolver)
3045+
3046+
let (_, bundle, context) = try testBundleAndContext(copying: "MixedLanguageFrameworkWithLanguageRefinements") { url in
3047+
// Top level symbols, omitting the module name
3048+
try """
3049+
# ``MyStruct/myStructProperty``
3050+
3051+
@Metadata {
3052+
@DocumentationExtension(mergeBehavior: override)
3053+
}
3054+
3055+
my struct property
3056+
""".write(to: url.appendingPathComponent("struct-property.md"), atomically: true, encoding: .utf8)
3057+
3058+
try """
3059+
# ``MyTypeAlias``
3060+
3061+
@Metadata {
3062+
@DocumentationExtension(mergeBehavior: override)
3063+
}
3064+
3065+
my type alias
3066+
""".write(to: url.appendingPathComponent("alias.md"), atomically: true, encoding: .utf8)
3067+
}
3068+
3069+
do {
3070+
// The resolved reference needs more disambiguation than the documentation extension link did.
3071+
let reference = ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/documentation/MixedFramework/MyStruct/myStructProperty", sourceLanguage: .swift)
3072+
3073+
let node = try context.entity(with: reference)
3074+
let symbol = try XCTUnwrap(node.semantic as? Symbol)
3075+
XCTAssertEqual(symbol.abstract?.plainText, "my struct property", "The abstract should be from the overriding documentation extension.")
3076+
}
3077+
3078+
do {
3079+
// The resolved reference needs more disambiguation than the documentation extension link did.
3080+
let reference = ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/documentation/MixedFramework/MyTypeAlias", sourceLanguage: .swift)
3081+
3082+
let node = try context.entity(with: reference)
3083+
let symbol = try XCTUnwrap(node.semantic as? Symbol)
3084+
XCTAssertEqual(symbol.abstract?.plainText, "my type alias", "The abstract should be from the overriding documentation extension.")
3085+
}
3086+
}
3087+
3088+
func testCurationOfSymbolsWithSameNameAsModule() throws {
3089+
try XCTSkipUnless(LinkResolutionMigrationConfiguration.shouldUseHierarchyBasedLinkResolver)
3090+
3091+
let (_, bundle, context) = try testBundleAndContext(copying: "SymbolsWithSameNameAsModule") { url in
3092+
// Top level symbols, omitting the module name
3093+
try """
3094+
# ``Something``
3095+
3096+
This documentation extension covers the module symbol
3097+
3098+
## Topics
3099+
3100+
This link curates the top-level struct
3101+
3102+
- ``Something``
3103+
""".write(to: url.appendingPathComponent("something.md"), atomically: true, encoding: .utf8)
3104+
}
3105+
3106+
do {
3107+
// The resolved reference needs more disambiguation than the documentation extension link did.
3108+
let reference = ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/documentation/Something", sourceLanguage: .swift)
3109+
3110+
let node = try context.entity(with: reference)
3111+
let symbol = try XCTUnwrap(node.semantic as? Symbol)
3112+
XCTAssertEqual(symbol.abstract?.plainText, "This documentation extension covers the module symbol", "The abstract should be from the overriding documentation extension.")
3113+
3114+
let topics = try XCTUnwrap(symbol.topics?.taskGroups.first)
3115+
XCTAssertEqual(topics.abstract?.paragraph.plainText, "This link curates the top-level struct")
3116+
XCTAssertEqual(topics.links.first?.destination, "doc://SymbolsWithSameNameAsModule/documentation/Something/Something")
3117+
}
3118+
}
3119+
30433120
func testMultipleDocumentationExtensionMatchDiagnostic() throws {
30443121
try XCTSkipUnless(LinkResolutionMigrationConfiguration.shouldUseHierarchyBasedLinkResolver)
30453122

Tests/SwiftDocCTests/Infrastructure/PathHierarchyTests.swift

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1149,6 +1149,54 @@ class PathHierarchyTests: XCTestCase {
11491149
"/ShapeKit/OverloadedEnum/firstTestMemberName(_:)-88rbf")
11501150
}
11511151

1152+
func testSymbolsWithSameNameAsModule() throws {
1153+
try XCTSkipUnless(LinkResolutionMigrationConfiguration.shouldUseHierarchyBasedLinkResolver)
1154+
let (_, context) = try testBundleAndContext(named: "SymbolsWithSameNameAsModule")
1155+
let tree = try XCTUnwrap(context.hierarchyBasedLinkResolver?.pathHierarchy)
1156+
1157+
// /* in a module named "Something "*/
1158+
// public struct Something {
1159+
// public enum Something {
1160+
// case first
1161+
// }
1162+
// public var second = 0
1163+
// }
1164+
// public struct Wrapper {
1165+
// public struct Something {
1166+
// public var third = 0
1167+
// }
1168+
// }
1169+
try assertFindsPath("Something", in: tree, asSymbolID: "Something")
1170+
try assertFindsPath("/Something", in: tree, asSymbolID: "Something")
1171+
1172+
let moduleID = try tree.find(path: "/Something", onlyFindSymbols: true)
1173+
XCTAssertEqual(try tree.findSymbol(path: "/Something", parent: moduleID).identifier.precise, "Something")
1174+
XCTAssertEqual(try tree.findSymbol(path: "Something-module", parent: moduleID).identifier.precise, "Something")
1175+
XCTAssertEqual(try tree.findSymbol(path: "Something", parent: moduleID).identifier.precise, "s:9SomethingAAV")
1176+
XCTAssertEqual(try tree.findSymbol(path: "/Something/Something", parent: moduleID).identifier.precise, "s:9SomethingAAV")
1177+
XCTAssertEqual(try tree.findSymbol(path: "Something/Something", parent: moduleID).identifier.precise, "s:9SomethingAAVAAO")
1178+
XCTAssertEqual(try tree.findSymbol(path: "Something/Something/Something", parent: moduleID).identifier.precise, "s:9SomethingAAVAAO")
1179+
XCTAssertEqual(try tree.findSymbol(path: "/Something/Something/Something", parent: moduleID).identifier.precise, "s:9SomethingAAVAAO")
1180+
XCTAssertEqual(try tree.findSymbol(path: "/Something/Something", parent: moduleID).identifier.precise, "s:9SomethingAAV")
1181+
XCTAssertEqual(try tree.findSymbol(path: "Something/second", parent: moduleID).identifier.precise, "s:9SomethingAAV6secondSivp")
1182+
1183+
let topLevelSymbolID = try tree.find(path: "/Something/Something", onlyFindSymbols: true)
1184+
XCTAssertEqual(try tree.findSymbol(path: "Something", parent: topLevelSymbolID).identifier.precise, "s:9SomethingAAVAAO")
1185+
XCTAssertEqual(try tree.findSymbol(path: "Something/Something", parent: topLevelSymbolID).identifier.precise, "s:9SomethingAAVAAO")
1186+
XCTAssertEqual(try tree.findSymbol(path: "Something/second", parent: topLevelSymbolID).identifier.precise, "s:9SomethingAAV6secondSivp")
1187+
1188+
let wrapperID = try tree.find(path: "/Something/Wrapper", onlyFindSymbols: true)
1189+
XCTAssertEqual(try tree.findSymbol(path: "Something/second", parent: wrapperID).identifier.precise, "s:9SomethingAAV6secondSivp")
1190+
XCTAssertEqual(try tree.findSymbol(path: "Something/third", parent: wrapperID).identifier.precise, "s:9Something7WrapperVAAV5thirdSivp")
1191+
1192+
let wrappedID = try tree.find(path: "/Something/Wrapper/Something", onlyFindSymbols: true)
1193+
XCTAssertEqual(try tree.findSymbol(path: "Something/second", parent: wrappedID).identifier.precise, "s:9SomethingAAV6secondSivp")
1194+
XCTAssertEqual(try tree.findSymbol(path: "Something/third", parent: wrappedID).identifier.precise, "s:9Something7WrapperVAAV5thirdSivp")
1195+
1196+
XCTAssertEqual(try tree.findSymbol(path: "Something/first", parent: topLevelSymbolID).identifier.precise, "s:9SomethingAAVAAO5firstyA2CmF")
1197+
XCTAssertEqual(try tree.findSymbol(path: "Something/second", parent: topLevelSymbolID).identifier.precise, "s:9SomethingAAV6secondSivp")
1198+
}
1199+
11521200
func testSnippets() throws {
11531201
try XCTSkipUnless(LinkResolutionMigrationConfiguration.shouldUseHierarchyBasedLinkResolver)
11541202
let (_, context) = try testBundleAndContext(named: "Snippets")

Tests/SwiftDocCTests/Infrastructure/ReferenceResolverTests.swift

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -471,21 +471,17 @@ class ReferenceResolverTests: XCTestCase {
471471
try """
472472
# ``ModuleWithSingleExtension``
473473
474-
This is a test module with an extension to ``Swift/Array#Array``.
474+
This is a test module with an extension to ``Swift/Array``.
475475
""".write(to: topLevelArticle, atomically: true, encoding: .utf8)
476476
}
477477

478478
// Make sure that linking to `Swift/Array` raises a diagnostic about the page having been removed
479-
if LinkResolutionMigrationConfiguration.shouldUseHierarchyBasedLinkResolver {
480-
let diagnostic = try XCTUnwrap(context.problems.first(where: { $0.diagnostic.identifier == "org.swift.docc.removedExtensionLinkDestination" }))
481-
XCTAssertEqual(diagnostic.possibleSolutions.count, 1)
482-
let solution = try XCTUnwrap(diagnostic.possibleSolutions.first)
483-
XCTAssertEqual(solution.replacements.count, 1)
484-
let replacement = try XCTUnwrap(solution.replacements.first)
485-
XCTAssertEqual(replacement.replacement, "`Swift/Array`")
486-
} else {
487-
XCTAssert(context.problems.contains(where: { $0.diagnostic.identifier == "org.swift.docc.unresolvedTopicReference" }))
488-
}
479+
let diagnostic = try XCTUnwrap(context.problems.first(where: { $0.diagnostic.identifier == "org.swift.docc.removedExtensionLinkDestination" }))
480+
XCTAssertEqual(diagnostic.possibleSolutions.count, 1)
481+
let solution = try XCTUnwrap(diagnostic.possibleSolutions.first)
482+
XCTAssertEqual(solution.replacements.count, 1)
483+
let replacement = try XCTUnwrap(solution.replacements.first)
484+
XCTAssertEqual(replacement.replacement, "`Swift/Array`")
489485

490486
// Also make sure that the extension pages are still gone
491487
let extendedModule = ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/documentation/ModuleWithSingleExtension/Swift", sourceLanguage: .swift)

0 commit comments

Comments
 (0)