Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
This source file is part of the Swift.org open source project

Copyright (c) 2023-2024 Apple Inc. and the Swift project authors
Copyright (c) 2023-2025 Apple Inc. and the Swift project authors
Licensed under Apache License v2.0 with Runtime Library Exception

See https://swift.org/LICENSE.txt for license information
Expand Down Expand Up @@ -555,6 +555,15 @@ private struct StringScanner: ~Copyable {
return remaining[..<index]
}

mutating func scan(past predicate: (Character) -> Bool) -> Substring? {
guard let beforeIndex = remaining.firstIndex(where: predicate) else {
return nil
}
let index = remaining.index(after: beforeIndex)
defer { remaining = remaining[index...] }
return remaining[..<index]
}

var isAtEnd: Bool {
remaining.isEmpty
}
Expand Down Expand Up @@ -595,7 +604,7 @@ private struct StringScanner: ~Copyable {
guard peek() == "(" else {
// If the argument doesn't start with "(" it can't be neither a tuple nor a closure type.
// In this case, scan until the next argument (",") or the end of the arguments (")")
return scan(until: { $0 == "," || $0 == ")" }) ?? takeAll()
return scanValue() ?? takeAll()
}

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

guard peek() == "(" else {
// This closure type has a simple return type.
guard let returnValue = scan(until: { $0 == "," || $0 == ")" }) else {
guard let returnValue = scanValue() else {
return nil
}
return argumentString + returnValue
Expand All @@ -632,13 +641,29 @@ private struct StringScanner: ~Copyable {
depth += 1
return false // keep scanning
}
if depth > 0 {
if $0 == ")" {
depth -= 1
}
else if $0 == ")" {
depth -= 1
return depth == 0 // stop only if we've reached a balanced number of parenthesis
}
return false // keep scanning
}

return scan(past: predicate)
}

mutating func scanValue() -> Substring? {
// The value may contain any number of nested generics. Keep track of the open and close angle brackets while scanning.
var depth = 0
let predicate: (Character) -> Bool = {
if $0 == "<" {
depth += 1
return false // keep scanning
}
else if $0 == ">" {
depth -= 1
return false // keep scanning
}
return $0 == "," || $0 == ")"
return depth == 0 && ($0 == "," || $0 == ")")
}
return scan(until: predicate)
}
Expand Down
85 changes: 85 additions & 0 deletions Tests/SwiftDocCTests/Infrastructure/PathHierarchyTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2037,6 +2037,71 @@ class PathHierarchyTests: XCTestCase {
try assertFindsPath("doSomething()-9kd0v", in: tree, asSymbolID: "some-function-id-AnyObject")
}

func testParameterDisambiguationWithKeyPathType() async throws {
// Create two overloads with different key path parameter types
let parameterTypes: [SymbolGraph.Symbol.DeclarationFragments.Fragment] = [
// Swift.Int
.init(kind: .typeIdentifier, spelling: "Int", preciseIdentifier: "s:Si"),
// Swift.Bool
.init(kind: .typeIdentifier, spelling: "Bool", preciseIdentifier: "s:Sb"),
]

let catalog = Folder(name: "CatalogName.docc", content: [
JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph(moduleName: "ModuleName", symbols: parameterTypes.map { parameterTypeFragment in
makeSymbol(id: "some-function-id-\(parameterTypeFragment.spelling)-KeyPath", kind: .func, pathComponents: ["doSomething(keyPath:)"], signature: .init(
parameters: [
// "keyPath: KeyPath<String, Int>" or "keyPath: KeyPath<String, Bool>"
.init(name: "keyPath", externalName: nil, declarationFragments: [
.init(kind: .identifier, spelling: "keyPath", preciseIdentifier: nil),
.init(kind: .text, spelling: ": ", preciseIdentifier: nil),
.init(kind: .typeIdentifier, spelling: "KeyPath", preciseIdentifier: "s:s7KeyPathC"),
.init(kind: .text, spelling: "<", preciseIdentifier: nil),
.init(kind: .typeIdentifier, spelling: "String", preciseIdentifier: "s:SS"),
.init(kind: .text, spelling: ", ", preciseIdentifier: nil),
parameterTypeFragment,
.init(kind: .text, spelling: ">", preciseIdentifier: nil)
], children: [])
],
returns: [
.init(kind: .text, spelling: "()", preciseIdentifier: nil) // 'Void' in text representation
]
))
})),
])
let (_, context) = try await loadBundle(catalog: catalog)
let tree = context.linkResolver.localResolver.pathHierarchy

XCTAssert(context.problems.isEmpty, "Unexpected problems \(context.problems.map(\.diagnostic.summary))")

let paths = tree.caseInsensitiveDisambiguatedPaths()

XCTAssertEqual(paths["some-function-id-Int-KeyPath"], "/ModuleName/doSomething(keyPath:)-(KeyPath<String,Int>)")
XCTAssertEqual(paths["some-function-id-Bool-KeyPath"], "/ModuleName/doSomething(keyPath:)-(KeyPath<String,Bool>)")

try assertPathCollision("doSomething(keyPath:)", in: tree, collisions: [
("some-function-id-Int-KeyPath", "-(KeyPath<String,Int>)"),
("some-function-id-Bool-KeyPath", "-(KeyPath<String,Bool>)"),
])

try assertPathRaisesErrorMessage("doSomething(keyPath:)", in: tree, context: context, expectedErrorMessage: "'doSomething(keyPath:)' is ambiguous at '/ModuleName'") { error in
XCTAssertEqual(error.solutions.count, 2)

// These test symbols don't have full declarations. A real solution would display enough information to distinguish these.
XCTAssertEqual(error.solutions.dropFirst(0).first, .init(summary: "Insert '-(KeyPath<String,Bool>)' for \n'doSomething(keyPath:)'" , replacements: [("-(KeyPath<String,Bool>)", 21, 21)]))
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)]))
}

assertParsedPathComponents("doSomething(keyPath:)-(KeyPath<String,Int>)", [("doSomething(keyPath:)", .typeSignature(parameterTypes: ["KeyPath<String,Int>"], returnTypes: nil))])
try assertFindsPath("doSomething(keyPath:)-(KeyPath<String,Int>)", in: tree, asSymbolID: "some-function-id-Int-KeyPath")
try assertFindsPath("doSomething(keyPath:)-(KeyPath<String,Int>)->()", in: tree, asSymbolID: "some-function-id-Int-KeyPath")
try assertFindsPath("doSomething(keyPath:)-2zg7h", in: tree, asSymbolID: "some-function-id-Int-KeyPath")

assertParsedPathComponents("doSomething(keyPath:)-(KeyPath<String,Bool>)", [("doSomething(keyPath:)", .typeSignature(parameterTypes: ["KeyPath<String,Bool>"], returnTypes: nil))])
try assertFindsPath("doSomething(keyPath:)-(KeyPath<String,Bool>)", in: tree, asSymbolID: "some-function-id-Bool-KeyPath")
try assertFindsPath("doSomething(keyPath:)-(KeyPath<String,Bool>)->()", in: tree, asSymbolID: "some-function-id-Bool-KeyPath")
try assertFindsPath("doSomething(keyPath:)-2frrn", in: tree, asSymbolID: "some-function-id-Bool-KeyPath")
}

func testOverloadGroupSymbolsResolveLinksWithoutHash() async throws {
enableFeatureFlag(\.isExperimentalOverloadedSymbolPresentationEnabled)

Expand Down Expand Up @@ -4404,6 +4469,26 @@ class PathHierarchyTests: XCTestCase {
}

assertParsedPathComponents("operator[]-(std::string&)->std::string&", [("operator[]", .typeSignature(parameterTypes: ["std::string&"], returnTypes: ["std::string&"]))])

// Nested generic types
assertParsedPathComponents("functionName-(KeyPath<String,Int>)", [("functionName", .typeSignature(parameterTypes: ["KeyPath<String,Int>"], returnTypes: nil))])
assertParsedPathComponents("functionName->KeyPath<String,Int>", [("functionName", .typeSignature(parameterTypes: nil, returnTypes: ["KeyPath<String,Int>"]))])

assertParsedPathComponents("functionName-(KeyPath<String,Int>,Dictionary<Int,Int>)", [("functionName", .typeSignature(parameterTypes: ["KeyPath<String,Int>", "Dictionary<Int,Int>"], returnTypes: nil))])
assertParsedPathComponents("functionName->(KeyPath<String,Int>,Dictionary<Int,Int>)", [("functionName", .typeSignature(parameterTypes: nil, returnTypes: ["KeyPath<String,Int>", "Dictionary<Int,Int>"]))])

assertParsedPathComponents("functionName-(KeyPath<String,Dictionary<Int,Int>>)", [("functionName", .typeSignature(parameterTypes: ["KeyPath<String,Dictionary<Int,Int>>"], returnTypes: nil))])
assertParsedPathComponents("functionName->KeyPath<String,Dictionary<Int,Int>>", [("functionName", .typeSignature(parameterTypes: nil, returnTypes: ["KeyPath<String,Dictionary<Int,Int>>"]))])

assertParsedPathComponents("functionName-(KeyPath<Array<Bool>,Dictionary<Int,(Bool,Bool))>>)", [("functionName", .typeSignature(parameterTypes: ["KeyPath<Array<Bool>,Dictionary<Int,(Bool,Bool))>>"], returnTypes: nil))])
assertParsedPathComponents("functionName->KeyPath<Array<Bool>,Dictionary<Int,(Bool,Bool))>>", [("functionName", .typeSignature(parameterTypes: nil, returnTypes: ["KeyPath<Array<Bool>,Dictionary<Int,(Bool,Bool))>>"]))])

// Nested generics and tuple types
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))])
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>"]))])
// With special characters
assertParsedPathComponents( "functionName-(Å<𝔹,©>,(Δ<∃,⨍,𝄞>,ℌ<(𝓲,ⅉ),(🄺,ƛ)>),𝔐<𝚗,(Ω<π,Ⓠ>,℟),𝔖>)", [("functionName", .typeSignature(parameterTypes: ["Å<𝔹,©>", "(Δ<∃,⨍,𝄞>,ℌ<(𝓲,ⅉ),(🄺,ƛ)>)", "𝔐<𝚗,(Ω<π,Ⓠ>,℟),𝔖>"], returnTypes: nil))])
assertParsedPathComponents("functionName->(Å<𝔹,©>,(Δ<∃,⨍,𝄞>,ℌ<(𝓲,ⅉ),(🄺,ƛ)>),𝔐<𝚗,(Ω<π,Ⓠ>,℟),𝔖>)", [("functionName", .typeSignature(parameterTypes: nil, returnTypes: ["Å<𝔹,©>", "(Δ<∃,⨍,𝄞>,ℌ<(𝓲,ⅉ),(🄺,ƛ)>)", "𝔐<𝚗,(Ω<π,Ⓠ>,℟),𝔖>"]))])
}

func testResolveExternalLinkFromTechnologyRoot() async throws {
Expand Down