Skip to content

Commit 0d8ccb7

Browse files
authored
Support either language representation's relative links in extension files (#858)
rdar://120380627
1 parent 8a52dcf commit 0d8ccb7

File tree

3 files changed

+174
-11
lines changed

3 files changed

+174
-11
lines changed

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

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,8 +180,32 @@ extension PathHierarchy {
180180
}
181181

182182
if !isAbsolute, let parentID {
183-
// If this is a relative link with a known starting point, search from that node up the hierarchy.
184-
return try searchForNodeUpTheHierarchy(from: lookup[parentID]!, path: remaining)
183+
// If this is a relative link with a known starting point, search from that node (or its language counterpoint) up the hierarchy.
184+
var startingPoint = lookup[parentID]!
185+
186+
// If the known starting point has multiple language representations, check which language representation to search from.
187+
if let firstComponent = remaining.first, let counterpoint = startingPoint.counterpart {
188+
switch (startingPoint.anyChildMatches(firstComponent), counterpoint.anyChildMatches(firstComponent)) {
189+
// If only one of the language representations match the first path components, use that as the starting point
190+
case (true, false):
191+
break
192+
case (false, true):
193+
startingPoint = counterpoint
194+
195+
// Otherwise, there isn't a clear starting point. Pick one based on the languages to get stable behavior across builds.
196+
case _ where startingPoint.languages.contains(.swift):
197+
break
198+
case _ where counterpoint.languages.contains(.swift):
199+
startingPoint = counterpoint
200+
default:
201+
// Only symbols have counterpoints which means that each node should always have at least one language
202+
if counterpoint.languages.map(\.id).min()! < startingPoint.languages.map(\.id).min()! {
203+
startingPoint = counterpoint
204+
}
205+
}
206+
}
207+
208+
return try searchForNodeUpTheHierarchy(from: startingPoint, path: remaining)
185209
}
186210
return try searchForNodeInModules()
187211
}

Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContextTests.swift

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3192,6 +3192,126 @@ let expected = """
31923192
])
31933193
}
31943194

3195+
func testExtensionCanUseLanguageSpecificRelativeLinks() throws {
3196+
// This test uses a symbol with different names in Swift and Objective-C, each with a member that's only available in that language.
3197+
let symbolID = "some-symbol-id"
3198+
let fileSystem = try TestFileSystem(folders: [
3199+
Folder(name: "unit-test.docc", content: [
3200+
Folder(name: "swift", content: [
3201+
JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph(
3202+
moduleName: "ModuleName",
3203+
symbols: [
3204+
.init(
3205+
identifier: .init(precise: symbolID, interfaceLanguage: SourceLanguage.swift.id),
3206+
names: .init(title: "SwiftName", navigator: nil, subHeading: nil, prose: nil),
3207+
pathComponents: ["SwiftName"],
3208+
docComment: nil,
3209+
accessLevel: .public,
3210+
kind: .init(parsedIdentifier: .class, displayName: "Kind Display Name"),
3211+
mixins: [:]
3212+
),
3213+
.init(
3214+
identifier: .init(precise: "swift-only-member-id", interfaceLanguage: SourceLanguage.swift.id),
3215+
names: .init(title: "swiftOnlyMemberName", navigator: nil, subHeading: nil, prose: nil),
3216+
pathComponents: ["SwiftName", "swiftOnlyMemberName"],
3217+
docComment: nil,
3218+
accessLevel: .public,
3219+
kind: .init(parsedIdentifier: .property, displayName: "Kind Display Name"),
3220+
mixins: [:]
3221+
),
3222+
], relationships: [
3223+
.init(source: "swift-only-member-id", target: symbolID, kind: .memberOf, targetFallback: nil)
3224+
])
3225+
),
3226+
]),
3227+
3228+
Folder(name: "clang", content: [
3229+
JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph(
3230+
moduleName: "ModuleName",
3231+
symbols: [
3232+
.init(
3233+
identifier: .init(precise: symbolID, interfaceLanguage: SourceLanguage.objectiveC.id),
3234+
names: .init(title: "ObjectiveCName", navigator: nil, subHeading: nil, prose: nil),
3235+
pathComponents: ["ObjectiveCName"],
3236+
docComment: nil,
3237+
accessLevel: .public,
3238+
kind: .init(parsedIdentifier: .class, displayName: "Kind Display Name"),
3239+
mixins: [:]
3240+
),
3241+
.init(
3242+
identifier: .init(precise: "objc-only-member-id", interfaceLanguage: SourceLanguage.objectiveC.id),
3243+
names: .init(title: "objectiveCOnlyMemberName", navigator: nil, subHeading: nil, prose: nil),
3244+
pathComponents: ["ObjectiveCName", "objectiveCOnlyMemberName"],
3245+
docComment: nil,
3246+
accessLevel: .public,
3247+
kind: .init(parsedIdentifier: .property, displayName: "Kind Display Name"),
3248+
mixins: [:]
3249+
),
3250+
], relationships: [
3251+
.init(source: "objc-only-member-id", target: symbolID, kind: .memberOf, targetFallback: nil)
3252+
])
3253+
),
3254+
]),
3255+
3256+
TextFile(name: "Extension.md", utf8Content: """
3257+
# ``SwiftName``
3258+
3259+
A documentation extension that uses both language's language specific links to curate the same symbol 6 times (2 that fail with warnings)
3260+
3261+
## Topics
3262+
3263+
### Relative links
3264+
3265+
- ``swiftOnlyMemberName``
3266+
- ``objectiveCOnlyMemberName``
3267+
3268+
### Correct absolute links
3269+
3270+
- ``SwiftName/swiftOnlyMemberName``
3271+
- ``ObjectiveCName/objectiveCOnlyMemberName``
3272+
3273+
### Incorrect absolute links
3274+
3275+
- ``ObjectiveCName/swiftOnlyMemberName``
3276+
- ``SwiftName/objectiveCOnlyMemberName``
3277+
"""),
3278+
])
3279+
])
3280+
3281+
let workspace = DocumentationWorkspace()
3282+
let context = try DocumentationContext(dataProvider: workspace)
3283+
try workspace.registerProvider(fileSystem)
3284+
3285+
XCTAssertEqual(context.problems.map(\.diagnostic.summary).sorted(), [
3286+
"'objectiveCOnlyMemberName' doesn't exist at '/ModuleName/SwiftName'",
3287+
"'swiftOnlyMemberName' doesn't exist at '/ModuleName/ObjectiveCName'",
3288+
])
3289+
3290+
let reference = ResolvedTopicReference(bundleIdentifier: "unit-test", path: "/documentation/ModuleName/SwiftName", sourceLanguage: .swift)
3291+
let entity = try context.entity(with: reference)
3292+
let symbol = try XCTUnwrap(entity.semantic as? Symbol)
3293+
let taskGroups = try XCTUnwrap(symbol.topics).taskGroups
3294+
3295+
XCTAssertEqual(taskGroups.map { $0.links.map(\.destination) }, [
3296+
// Relative links
3297+
[
3298+
"doc://unit-test/documentation/ModuleName/SwiftName/swiftOnlyMemberName",
3299+
"doc://unit-test/documentation/ModuleName/ObjectiveCName/objectiveCOnlyMemberName",
3300+
],
3301+
// Correct absolute links
3302+
[
3303+
"doc://unit-test/documentation/ModuleName/SwiftName/swiftOnlyMemberName",
3304+
"doc://unit-test/documentation/ModuleName/ObjectiveCName/objectiveCOnlyMemberName",
3305+
],
3306+
// Incorrect absolute links
3307+
[
3308+
// This links remain as they were authored because they didn't resolve
3309+
"ObjectiveCName/swiftOnlyMemberName",
3310+
"SwiftName/objectiveCOnlyMemberName",
3311+
]
3312+
])
3313+
}
3314+
31953315
func testWarnOnMultipleMarkdownExtensions() throws {
31963316
let fileContent = """
31973317
# ``MyKit/MyClass/myFunction()``

Tests/SwiftDocCTests/Infrastructure/PathHierarchyTests.swift

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -999,14 +999,22 @@ class PathHierarchyTests: XCTestCase {
999999
let mySwiftClassSwiftID = try tree.find(path: "MySwiftClassSwiftName", parent: moduleID, onlyFindSymbols: true)
10001000
XCTAssertEqual(try tree.findSymbol(path: "myPropertySwiftName", parent: mySwiftClassSwiftID).identifier.precise, "c:@M@MixedFramework@objc(cs)MySwiftClassObjectiveCName(py)myPropertyObjectiveCName")
10011001
XCTAssertEqual(try tree.findSymbol(path: "myMethodSwiftName()", parent: mySwiftClassSwiftID).identifier.precise, "c:@M@MixedFramework@objc(cs)MySwiftClassObjectiveCName(im)myMethodObjectiveCName")
1002-
XCTAssertThrowsError(try tree.findSymbol(path: "myPropertyObjectiveCName", parent: mySwiftClassSwiftID))
1003-
XCTAssertThrowsError(try tree.findSymbol(path: "myMethodObjectiveCName", parent: mySwiftClassSwiftID))
1002+
// Relative links can start with either language representation. This enabled documentation extension files to use relative links.
1003+
XCTAssertEqual(try tree.findSymbol(path: "myPropertyObjectiveCName", parent: mySwiftClassSwiftID).identifier.precise, "c:@M@MixedFramework@objc(cs)MySwiftClassObjectiveCName(py)myPropertyObjectiveCName")
1004+
XCTAssertEqual(try tree.findSymbol(path: "myMethodObjectiveCName", parent: mySwiftClassSwiftID).identifier.precise, "c:@M@MixedFramework@objc(cs)MySwiftClassObjectiveCName(im)myMethodObjectiveCName")
1005+
// Links can't mix languages
1006+
XCTAssertThrowsError(try tree.findSymbol(path: "MySwiftClassSwiftName/myPropertyObjectiveCName", parent: moduleID))
1007+
XCTAssertThrowsError(try tree.findSymbol(path: "MySwiftClassSwiftName/myMethodObjectiveCName", parent: moduleID))
10041008

10051009
let mySwiftClassObjCID = try tree.find(path: "MySwiftClassObjectiveCName", parent: moduleID, onlyFindSymbols: true)
10061010
XCTAssertEqual(try tree.findSymbol(path: "myPropertyObjectiveCName", parent: mySwiftClassObjCID).identifier.precise, "c:@M@MixedFramework@objc(cs)MySwiftClassObjectiveCName(py)myPropertyObjectiveCName")
10071011
XCTAssertEqual(try tree.findSymbol(path: "myMethodObjectiveCName", parent: mySwiftClassObjCID).identifier.precise, "c:@M@MixedFramework@objc(cs)MySwiftClassObjectiveCName(im)myMethodObjectiveCName")
1008-
XCTAssertThrowsError(try tree.findSymbol(path: "myPropertySwiftName", parent: mySwiftClassObjCID))
1009-
XCTAssertThrowsError(try tree.findSymbol(path: "myMethodSwiftName()", parent: mySwiftClassObjCID))
1012+
// Relative links can use either language representation. This enabled documentation extension files to use relative links.
1013+
XCTAssertEqual(try tree.findSymbol(path: "myPropertySwiftName", parent: mySwiftClassObjCID).identifier.precise, "c:@M@MixedFramework@objc(cs)MySwiftClassObjectiveCName(py)myPropertyObjectiveCName")
1014+
XCTAssertEqual(try tree.findSymbol(path: "myMethodSwiftName()", parent: mySwiftClassObjCID).identifier.precise, "c:@M@MixedFramework@objc(cs)MySwiftClassObjectiveCName(im)myMethodObjectiveCName")
1015+
// Absolute links can't mix languages
1016+
XCTAssertThrowsError(try tree.findSymbol(path: "myPropertySwiftName", parent: moduleID))
1017+
XCTAssertThrowsError(try tree.findSymbol(path: "myMethodSwiftName()", parent: moduleID))
10101018

10111019
// typedef NS_OPTIONS(NSInteger, MyObjectiveCOption) {
10121020
// MyObjectiveCOptionNone = 0,
@@ -1017,19 +1025,30 @@ class PathHierarchyTests: XCTestCase {
10171025
XCTAssertEqual(try tree.findSymbol(path: "MyObjectiveCOptionNone", parent: myOptionAsEnumID).identifier.precise, "c:@E@MyObjectiveCOption@MyObjectiveCOptionNone")
10181026
XCTAssertEqual(try tree.findSymbol(path: "MyObjectiveCOptionFirst", parent: myOptionAsEnumID).identifier.precise, "c:@E@MyObjectiveCOption@MyObjectiveCOptionFirst")
10191027
XCTAssertEqual(try tree.findSymbol(path: "MyObjectiveCOptionSecond", parent: myOptionAsEnumID).identifier.precise, "c:@E@MyObjectiveCOption@MyObjectiveCOptionSecond")
1028+
// These names don't exist in either language representation
10201029
XCTAssertThrowsError(try tree.findSymbol(path: "none", parent: myOptionAsEnumID))
1021-
XCTAssertThrowsError(try tree.findSymbol(path: "first", parent: myOptionAsEnumID))
10221030
XCTAssertThrowsError(try tree.findSymbol(path: "second", parent: myOptionAsEnumID))
1023-
XCTAssertThrowsError(try tree.findSymbol(path: "secondCaseSwiftName", parent: myOptionAsEnumID))
1031+
// Relative links can start with either language representation. This enabled documentation extension files to use relative links.
1032+
XCTAssertEqual(try tree.findSymbol(path: "first", parent: myOptionAsEnumID).identifier.precise, "c:@E@MyObjectiveCOption@MyObjectiveCOptionFirst")
1033+
XCTAssertEqual(try tree.findSymbol(path: "secondCaseSwiftName", parent: myOptionAsEnumID).identifier.precise, "c:@E@MyObjectiveCOption@MyObjectiveCOptionSecond")
1034+
// Links can't mix languages
1035+
XCTAssertThrowsError(try tree.findSymbol(path: "MyObjectiveCOption-enum/first", parent: myOptionAsEnumID))
1036+
XCTAssertThrowsError(try tree.findSymbol(path: "MyObjectiveCOption-enum/secondCaseSwiftName", parent: myOptionAsEnumID))
10241037

10251038
let myOptionAsStructID = try tree.find(path: "MyObjectiveCOption-struct", parent: moduleID, onlyFindSymbols: true)
10261039
XCTAssertEqual(try tree.findSymbol(path: "first", parent: myOptionAsStructID).identifier.precise, "c:@E@MyObjectiveCOption@MyObjectiveCOptionFirst")
10271040
XCTAssertEqual(try tree.findSymbol(path: "secondCaseSwiftName", parent: myOptionAsStructID).identifier.precise, "c:@E@MyObjectiveCOption@MyObjectiveCOptionSecond")
1041+
// These names don't exist in either language representation
10281042
XCTAssertThrowsError(try tree.findSymbol(path: "none", parent: myOptionAsStructID))
10291043
XCTAssertThrowsError(try tree.findSymbol(path: "second", parent: myOptionAsStructID))
1030-
XCTAssertThrowsError(try tree.findSymbol(path: "MyObjectiveCOptionNone", parent: myOptionAsStructID))
1031-
XCTAssertThrowsError(try tree.findSymbol(path: "MyObjectiveCOptionFirst", parent: myOptionAsStructID))
1032-
XCTAssertThrowsError(try tree.findSymbol(path: "MyObjectiveCOptionSecond", parent: myOptionAsStructID))
1044+
// Relative links can start with either language representation. This enabled documentation extension files to use relative links.
1045+
XCTAssertEqual(try tree.findSymbol(path: "MyObjectiveCOptionNone", parent: myOptionAsStructID).identifier.precise, "c:@E@MyObjectiveCOption@MyObjectiveCOptionNone")
1046+
XCTAssertEqual(try tree.findSymbol(path: "MyObjectiveCOptionFirst", parent: myOptionAsStructID).identifier.precise, "c:@E@MyObjectiveCOption@MyObjectiveCOptionFirst")
1047+
XCTAssertEqual(try tree.findSymbol(path: "MyObjectiveCOptionSecond", parent: myOptionAsStructID).identifier.precise, "c:@E@MyObjectiveCOption@MyObjectiveCOptionSecond")
1048+
// Links can't mix languages
1049+
XCTAssertThrowsError(try tree.findSymbol(path: "MyObjectiveCOption-struct/MyObjectiveCOptionNone", parent: moduleID))
1050+
XCTAssertThrowsError(try tree.findSymbol(path: "MyObjectiveCOption-struct/MyObjectiveCOptionFirst", parent: moduleID))
1051+
XCTAssertThrowsError(try tree.findSymbol(path: "MyObjectiveCOption-struct/MyObjectiveCOptionSecond", parent: moduleID))
10331052

10341053
// typedef NSInteger MyTypedObjectiveCExtensibleEnum NS_TYPED_EXTENSIBLE_ENUM;
10351054
//

0 commit comments

Comments
 (0)