Skip to content

Commit bc86b59

Browse files
author
Max Grebenets
committed
(#1257) Combined module link issue - public extension of dependent module causes resolution failure
The basic setup is this: ```swift // ImportedModule struct ExtendedType {} struct BaseType {} ``` ```swift // MainModule import ImportedModule extension ExtendedType { func extensionMethod() } ``` With the above we have the following issues: - `/ImportedModule/ExtendedType` **always** resolves to the type extension link - `/ImportModule/BaseType` links are broken Prioritize absolute links resolution to point to the module referenced in the link, so that `/ImportedModule/BaseType` finds the base type because the path starts with `/`. To refer to the extended type or method, the non-absolute paths work in Xcode and with docc: `ImportedModule/ExtendedType` resolves to the type extension page, `ImportedModule/ExtendedType/extensionMethod()` resolves to the extension method page. > ‼️🤖 Disclaimer: this is an AI-assisted change. While AI was utilized to identify and apply the initial fix and create initial version of the tests, the code has been reviewed and modified where needed. More importantly, I have tested the changes both with the sample project attached to the original issue and with a real work project where I'm using `xcodebuild docbuild`. In the real world project I'm dealing with similar setup. `ParentFramework` imports `ChildFramework` and extends `ChildType` and then uses paths like `/ChildFramework/ChildType` in the documentation. By passing `DOCC_EXEC` build setting to `xcodebuild docbuild` invocation along with `--enable-experimental-combined-documentation` and other required flags I was able to produce doc archives for the 2 modules, then `docc merge` them into one and in the resulting doc archive all cross-linking works as expected. I can also ue `ChildFramework/ChildType/extensionMethod()` links to create reference to the extension method doc page.
1 parent b27288d commit bc86b59

File tree

2 files changed

+100
-2
lines changed

2 files changed

+100
-2
lines changed

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,9 @@ extension PathHierarchy {
8484
if let moduleMatch = modules.first(where: { $0.matches(firstComponent) }) {
8585
return try searchForNode(descendingFrom: moduleMatch, pathComponents: remaining.dropFirst(), onlyFindSymbols: onlyFindSymbols, rawPathForError: rawPath)
8686
}
87-
if modules.count == 1 {
87+
// For absolute links, only use the single-module fallback if the first component doesn't match
88+
// any module name
89+
if modules.count == 1 && !isAbsolute {
8890
do {
8991
return try searchForNode(descendingFrom: modules.first!, pathComponents: remaining, onlyFindSymbols: onlyFindSymbols, rawPathForError: rawPath)
9092
} catch {

Tests/SwiftDocCTests/Infrastructure/PathHierarchyTests.swift

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3126,7 +3126,103 @@ class PathHierarchyTests: XCTestCase {
31263126
try assertFindsPath("/MainModule/TopLevelProtocol/extensionMember(_:)", in: tree, asSymbolID: "extensionMember1")
31273127
try assertFindsPath("/MainModule/TopLevelProtocol/InnerStruct/extensionMember(_:)", in: tree, asSymbolID: "extensionMember2")
31283128
}
3129-
3129+
3130+
func testAbsoluteLinksToOtherModuleWithExtensions() async throws {
3131+
enableFeatureFlag(\.isExperimentalLinkHierarchySerializationEnabled)
3132+
3133+
let importedProtocolID = "s:14ImportedModule12BaseProtocolP"
3134+
let importedTypeID = "s:14ImportedModule12ExtendedTypeV"
3135+
let extensionSymbolID = "s:e:s:14ImportedModule12ExtendedTypeV04MainC0E15extensionMethodyyF"
3136+
let extensionMethodID = "s:14ImportedModule12ExtendedTypeV04MainC0E15extensionMethodyyF"
3137+
let mainModuleTypeID = "s:10MainModule0A4TypeV"
3138+
3139+
let extensionMixin = SymbolGraph.Symbol.Swift.Extension(
3140+
extendedModule: "ImportedModule",
3141+
typeKind: .struct,
3142+
constraints: []
3143+
)
3144+
3145+
let catalog = Folder(name: "TestCatalog.docc", content: [
3146+
JSONFile(name: "MainModule.symbols.json", content: makeSymbolGraph(
3147+
moduleName: "MainModule",
3148+
symbols: [
3149+
makeSymbol(id: mainModuleTypeID, kind: .struct, pathComponents: ["MainType"])
3150+
]
3151+
)),
3152+
JSONFile(name: "[email protected]", content: makeSymbolGraph(
3153+
moduleName: "MainModule",
3154+
symbols: [
3155+
makeSymbol(id: importedProtocolID, kind: .protocol, pathComponents: ["BaseProtocol"]),
3156+
makeSymbol(id: importedTypeID, kind: .struct, pathComponents: ["ExtendedType"]),
3157+
makeSymbol(
3158+
id: extensionSymbolID,
3159+
kind: .extension,
3160+
pathComponents: ["ExtendedType"],
3161+
otherMixins: [extensionMixin]
3162+
),
3163+
makeSymbol(
3164+
id: extensionMethodID,
3165+
kind: .method,
3166+
pathComponents: ["ExtendedType", "extensionMethod()"],
3167+
otherMixins: [extensionMixin]
3168+
)
3169+
],
3170+
relationships: [
3171+
.init(
3172+
source: extensionMethodID,
3173+
target: extensionSymbolID,
3174+
kind: .memberOf,
3175+
targetFallback: "ImportedModule.ExtendedType"
3176+
),
3177+
.init(
3178+
source: extensionSymbolID,
3179+
target: importedTypeID,
3180+
kind: .extensionTo,
3181+
targetFallback: "ImportedModule.ExtendedType"
3182+
)
3183+
]
3184+
))
3185+
])
3186+
3187+
let (_, context) = try await loadBundle(catalog: catalog)
3188+
let tree = context.linkResolver.localResolver.pathHierarchy
3189+
3190+
XCTAssertEqual(tree.modules.count, 1)
3191+
XCTAssertEqual(tree.modules.first?.name, "MainModule")
3192+
3193+
let paths = tree.caseInsensitiveDisambiguatedPaths()
3194+
XCTAssertEqual(paths[importedProtocolID], "/MainModule/ImportedModule/BaseProtocol")
3195+
XCTAssertEqual(paths[importedTypeID], "/MainModule/ImportedModule/ExtendedType-struct")
3196+
XCTAssertEqual(
3197+
paths[extensionMethodID],
3198+
"/MainModule/ImportedModule/ExtendedType/extensionMethod()"
3199+
)
3200+
3201+
// Verify that symbols can be found at their correct paths
3202+
try assertFindsPath("/MainModule/ImportedModule/BaseProtocol", in: tree, asSymbolID: importedProtocolID)
3203+
try assertFindsPath("/MainModule/ImportedModule/ExtendedType-struct", in: tree, asSymbolID: importedTypeID)
3204+
try assertFindsPath(
3205+
"/MainModule/ImportedModule/ExtendedType/extensionMethod()",
3206+
in: tree,
3207+
asSymbolID: extensionMethodID
3208+
)
3209+
3210+
// Verify that absolute paths to non-existent modules throw moduleNotFound error
3211+
// This is the fix being tested: without it, single-module fallback would trigger incorrectly
3212+
try assertPathRaisesErrorMessage(
3213+
"/ImportedModule/BaseProtocol",
3214+
in: tree,
3215+
context: context,
3216+
expectedErrorMessage: "No module named 'ImportedModule'"
3217+
)
3218+
try assertPathRaisesErrorMessage(
3219+
"/ImportedModule/ExtendedType",
3220+
in: tree,
3221+
context: context,
3222+
expectedErrorMessage: "No module named 'ImportedModule'"
3223+
)
3224+
}
3225+
31303226
func testMissingRequiredMemberOfSymbolGraphRelationshipInOneLanguageAcrossManyPlatforms() async throws {
31313227
// We make a best-effort attempt to create a valid path hierarchy, even if the symbol graph inputs are not valid.
31323228

0 commit comments

Comments
 (0)