Skip to content

Commit 7e362ab

Browse files
committed
Improve parsing of links with type disambiguation that include generics
rdar://160232871
1 parent b816349 commit 7e362ab

File tree

2 files changed

+117
-7
lines changed

2 files changed

+117
-7
lines changed

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

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,15 @@ private struct StringScanner: ~Copyable {
555555
return remaining[..<index]
556556
}
557557

558+
mutating func scan(past predicate: (Character) -> Bool) -> Substring? {
559+
guard let beforeIndex = remaining.firstIndex(where: predicate) else {
560+
return nil
561+
}
562+
let index = remaining.index(after: beforeIndex)
563+
defer { remaining = remaining[index...] }
564+
return remaining[..<index]
565+
}
566+
558567
var isAtEnd: Bool {
559568
remaining.isEmpty
560569
}
@@ -595,7 +604,7 @@ private struct StringScanner: ~Copyable {
595604
guard peek() == "(" else {
596605
// If the argument doesn't start with "(" it can't be neither a tuple nor a closure type.
597606
// In this case, scan until the next argument (",") or the end of the arguments (")")
598-
return scan(until: { $0 == "," || $0 == ")" }) ?? takeAll()
607+
return scanValue() ?? takeAll()
599608
}
600609

601610
guard var argumentString = scanTuple() else {
@@ -611,7 +620,7 @@ private struct StringScanner: ~Copyable {
611620

612621
guard peek() == "(" else {
613622
// This closure type has a simple return type.
614-
guard let returnValue = scan(until: { $0 == "," || $0 == ")" }) else {
623+
guard let returnValue = scanValue() else {
615624
return nil
616625
}
617626
return argumentString + returnValue
@@ -632,13 +641,29 @@ private struct StringScanner: ~Copyable {
632641
depth += 1
633642
return false // keep scanning
634643
}
635-
if depth > 0 {
636-
if $0 == ")" {
637-
depth -= 1
638-
}
644+
else if $0 == ")" {
645+
depth -= 1
646+
return depth == 0 // stop only if we've reached a balanced number of parenthesis
647+
}
648+
return false // keep scanning
649+
}
650+
651+
return scan(past: predicate)
652+
}
653+
654+
mutating func scanValue() -> Substring? {
655+
// The value may contain any number of nested generics. Keep track of the open and close angle brackets while scanning.
656+
var depth = 0
657+
let predicate: (Character) -> Bool = {
658+
if $0 == "<" {
659+
depth += 1
660+
return false // keep scanning
661+
}
662+
else if $0 == ">" {
663+
depth -= 1
639664
return false // keep scanning
640665
}
641-
return $0 == "," || $0 == ")"
666+
return depth == 0 && ($0 == "," || $0 == ")")
642667
}
643668
return scan(until: predicate)
644669
}

Tests/SwiftDocCTests/Infrastructure/PathHierarchyTests.swift

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2037,6 +2037,71 @@ class PathHierarchyTests: XCTestCase {
20372037
try assertFindsPath("doSomething()-9kd0v", in: tree, asSymbolID: "some-function-id-AnyObject")
20382038
}
20392039

2040+
func testParameterDisambiguationWithKeyPathType() async throws {
2041+
// Create two overloads with different key path parameter types
2042+
let parameterTypes: [SymbolGraph.Symbol.DeclarationFragments.Fragment] = [
2043+
// Swift.Int
2044+
.init(kind: .typeIdentifier, spelling: "Int", preciseIdentifier: "s:Si"),
2045+
// Swift.Bool
2046+
.init(kind: .typeIdentifier, spelling: "Bool", preciseIdentifier: "s:Sb"),
2047+
]
2048+
2049+
let catalog = Folder(name: "CatalogName.docc", content: [
2050+
JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph(moduleName: "ModuleName", symbols: parameterTypes.map { parameterTypeFragment in
2051+
makeSymbol(id: "some-function-id-\(parameterTypeFragment.spelling)-KeyPath", kind: .func, pathComponents: ["doSomething(keyPath:)"], signature: .init(
2052+
parameters: [
2053+
// "keyPath: KeyPath<String, Int>" or "keyPath: KeyPath<String, Bool>"
2054+
.init(name: "keyPath", externalName: nil, declarationFragments: [
2055+
.init(kind: .identifier, spelling: "keyPath", preciseIdentifier: nil),
2056+
.init(kind: .text, spelling: ": ", preciseIdentifier: nil),
2057+
.init(kind: .typeIdentifier, spelling: "KeyPath", preciseIdentifier: "s:s7KeyPathC"),
2058+
.init(kind: .text, spelling: "<", preciseIdentifier: nil),
2059+
.init(kind: .typeIdentifier, spelling: "String", preciseIdentifier: "s:SS"),
2060+
.init(kind: .text, spelling: ", ", preciseIdentifier: nil),
2061+
parameterTypeFragment,
2062+
.init(kind: .text, spelling: ">", preciseIdentifier: nil)
2063+
], children: [])
2064+
],
2065+
returns: [
2066+
.init(kind: .text, spelling: "()", preciseIdentifier: nil) // 'Void' in text representation
2067+
]
2068+
))
2069+
})),
2070+
])
2071+
let (_, context) = try await loadBundle(catalog: catalog)
2072+
let tree = context.linkResolver.localResolver.pathHierarchy
2073+
2074+
XCTAssert(context.problems.isEmpty, "Unexpected problems \(context.problems.map(\.diagnostic.summary))")
2075+
2076+
let paths = tree.caseInsensitiveDisambiguatedPaths()
2077+
2078+
XCTAssertEqual(paths["some-function-id-Int-KeyPath"], "/ModuleName/doSomething(keyPath:)-(KeyPath<String,Int>)")
2079+
XCTAssertEqual(paths["some-function-id-Bool-KeyPath"], "/ModuleName/doSomething(keyPath:)-(KeyPath<String,Bool>)")
2080+
2081+
try assertPathCollision("doSomething(keyPath:)", in: tree, collisions: [
2082+
("some-function-id-Int-KeyPath", "-(KeyPath<String,Int>)"),
2083+
("some-function-id-Bool-KeyPath", "-(KeyPath<String,Bool>)"),
2084+
])
2085+
2086+
try assertPathRaisesErrorMessage("doSomething(keyPath:)", in: tree, context: context, expectedErrorMessage: "'doSomething(keyPath:)' is ambiguous at '/ModuleName'") { error in
2087+
XCTAssertEqual(error.solutions.count, 2)
2088+
2089+
// These test symbols don't have full declarations. A real solution would display enough information to distinguish these.
2090+
XCTAssertEqual(error.solutions.dropFirst(0).first, .init(summary: "Insert '-(KeyPath<String,Bool>)' for \n'doSomething(keyPath:)'" , replacements: [("-(KeyPath<String,Bool>)", 21, 21)]))
2091+
XCTAssertEqual(error.solutions.dropFirst(1).first, .init(summary: "Insert '-(KeyPath<String,Int>)' for \n'doSomething(keyPath:)'" /* the test symbols don't have full declarations */, replacements: [("-(KeyPath<String,Int>)", 21, 21)]))
2092+
}
2093+
2094+
assertParsedPathComponents("doSomething(keyPath:)-(KeyPath<String,Int>)", [("doSomething(keyPath:)", .typeSignature(parameterTypes: ["KeyPath<String,Int>"], returnTypes: nil))])
2095+
try assertFindsPath("doSomething(keyPath:)-(KeyPath<String,Int>)", in: tree, asSymbolID: "some-function-id-Int-KeyPath")
2096+
try assertFindsPath("doSomething(keyPath:)-(KeyPath<String,Int>)->()", in: tree, asSymbolID: "some-function-id-Int-KeyPath")
2097+
try assertFindsPath("doSomething(keyPath:)-2zg7h", in: tree, asSymbolID: "some-function-id-Int-KeyPath")
2098+
2099+
assertParsedPathComponents("doSomething(keyPath:)-(KeyPath<String,Bool>)", [("doSomething(keyPath:)", .typeSignature(parameterTypes: ["KeyPath<String,Bool>"], returnTypes: nil))])
2100+
try assertFindsPath("doSomething(keyPath:)-(KeyPath<String,Bool>)", in: tree, asSymbolID: "some-function-id-Bool-KeyPath")
2101+
try assertFindsPath("doSomething(keyPath:)-(KeyPath<String,Bool>)->()", in: tree, asSymbolID: "some-function-id-Bool-KeyPath")
2102+
try assertFindsPath("doSomething(keyPath:)-2frrn", in: tree, asSymbolID: "some-function-id-Bool-KeyPath")
2103+
}
2104+
20402105
func testOverloadGroupSymbolsResolveLinksWithoutHash() async throws {
20412106
enableFeatureFlag(\.isExperimentalOverloadedSymbolPresentationEnabled)
20422107

@@ -4404,6 +4469,26 @@ class PathHierarchyTests: XCTestCase {
44044469
}
44054470

44064471
assertParsedPathComponents("operator[]-(std::string&)->std::string&", [("operator[]", .typeSignature(parameterTypes: ["std::string&"], returnTypes: ["std::string&"]))])
4472+
4473+
// Nested generic types
4474+
assertParsedPathComponents("functionName-(KeyPath<String,Int>)", [("functionName", .typeSignature(parameterTypes: ["KeyPath<String,Int>"], returnTypes: nil))])
4475+
assertParsedPathComponents("functionName->KeyPath<String,Int>", [("functionName", .typeSignature(parameterTypes: nil, returnTypes: ["KeyPath<String,Int>"]))])
4476+
4477+
assertParsedPathComponents("functionName-(KeyPath<String,Int>,Dictionary<Int,Int>)", [("functionName", .typeSignature(parameterTypes: ["KeyPath<String,Int>", "Dictionary<Int,Int>"], returnTypes: nil))])
4478+
assertParsedPathComponents("functionName->(KeyPath<String,Int>,Dictionary<Int,Int>)", [("functionName", .typeSignature(parameterTypes: nil, returnTypes: ["KeyPath<String,Int>", "Dictionary<Int,Int>"]))])
4479+
4480+
assertParsedPathComponents("functionName-(KeyPath<String,Dictionary<Int,Int>>)", [("functionName", .typeSignature(parameterTypes: ["KeyPath<String,Dictionary<Int,Int>>"], returnTypes: nil))])
4481+
assertParsedPathComponents("functionName->KeyPath<String,Dictionary<Int,Int>>", [("functionName", .typeSignature(parameterTypes: nil, returnTypes: ["KeyPath<String,Dictionary<Int,Int>>"]))])
4482+
4483+
assertParsedPathComponents("functionName-(KeyPath<Array<Bool>,Dictionary<Int,(Bool,Bool))>>)", [("functionName", .typeSignature(parameterTypes: ["KeyPath<Array<Bool>,Dictionary<Int,(Bool,Bool))>>"], returnTypes: nil))])
4484+
assertParsedPathComponents("functionName->KeyPath<Array<Bool>,Dictionary<Int,(Bool,Bool))>>", [("functionName", .typeSignature(parameterTypes: nil, returnTypes: ["KeyPath<Array<Bool>,Dictionary<Int,(Bool,Bool))>>"]))])
4485+
4486+
// Nested generics and tuple types
4487+
assertParsedPathComponents( "functionName-(A<B,C>,(D<E,F,G>,H<(I,J),(K,L)>),M<N,(O<P,Q>,R),S>)", [("functionName", .typeSignature(parameterTypes: ["A<B,C>", "(D<E,F,G>,H<(I,J),(K,L)>)", "M<N,(O<P,Q>,R),S>"], returnTypes: nil))])
4488+
assertParsedPathComponents("functionName->(A<B,C>,(D<E,F,G>,H<(I,J),(K,L)>),M<N,(O<P,Q>,R),S>)", [("functionName", .typeSignature(parameterTypes: nil, returnTypes: ["A<B,C>", "(D<E,F,G>,H<(I,J),(K,L)>)", "M<N,(O<P,Q>,R),S>"]))])
4489+
// With special characters
4490+
assertParsedPathComponents( "functionName-(Å<𝔹,©>,(Δ<∃,⨍,𝄞>,ℌ<(𝓲,ⅉ),(🄺,ƛ)>),𝔐<𝚗,(Ω<π,Ⓠ>,℟),𝔖>)", [("functionName", .typeSignature(parameterTypes: ["Å<𝔹,©>", "(Δ<∃,⨍,𝄞>,ℌ<(𝓲,ⅉ),(🄺,ƛ)>)", "𝔐<𝚗,(Ω<π,Ⓠ>,℟),𝔖>"], returnTypes: nil))])
4491+
assertParsedPathComponents("functionName->(Å<𝔹,©>,(Δ<∃,⨍,𝄞>,ℌ<(𝓲,ⅉ),(🄺,ƛ)>),𝔐<𝚗,(Ω<π,Ⓠ>,℟),𝔖>)", [("functionName", .typeSignature(parameterTypes: nil, returnTypes: ["Å<𝔹,©>", "(Δ<∃,⨍,𝄞>,ℌ<(𝓲,ⅉ),(🄺,ƛ)>)", "𝔐<𝚗,(Ω<π,Ⓠ>,℟),𝔖>"]))])
44074492
}
44084493

44094494
func testResolveExternalLinkFromTechnologyRoot() async throws {

0 commit comments

Comments
 (0)