Skip to content

Commit c0873cd

Browse files
authored
Don't remove leading and trailing parentheses for closure types in type signature disambiguation (#1231) (#1243)
* Fix issue where suggested link disambiguation removed leading and trailing parentheses for some closure types rdar://151311221 * Minor unrelated correction of whitespace in symbol declaration test data * Include more information in test failure messages about unknown disambiguation
1 parent 13bb4ba commit c0873cd

File tree

2 files changed

+152
-13
lines changed

2 files changed

+152
-13
lines changed

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

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ extension PathHierarchy {
4545
}
4646
let spelling = utf8TypeSpelling(for: fragments, isSwift: isSwift)
4747

48-
guard isSwift, spelling[...].isTuple() else {
48+
guard isSwift, spelling[...].shapeOfSwiftTypeSpelling() == .tuple else {
4949
return [String(decoding: spelling, as: UTF8.self)]
5050
}
5151

@@ -194,14 +194,14 @@ extension PathHierarchy {
194194
}
195195

196196
// Check if the type names are wrapped in redundant parenthesis and remove them
197-
if accumulated.first == openParen, accumulated.last == closeParen, !accumulated[...].isTuple() {
197+
if accumulated.first == openParen, accumulated.last == closeParen, accumulated[...].shapeOfSwiftTypeSpelling() == .scalar {
198198
// In case there are multiple
199199
// Use a temporary slice until all the layers of redundant parenthesis have been removed.
200200
var temp = accumulated[...]
201201

202202
repeat {
203203
temp = temp.dropFirst().dropLast()
204-
} while temp.first == openParen && temp.last == closeParen && !temp.isTuple()
204+
} while temp.first == openParen && temp.last == closeParen && temp.shapeOfSwiftTypeSpelling() == .scalar
205205

206206
// Adjust the markers so that they align with the expected characters
207207
let difference = (accumulated.count - temp.count) / 2
@@ -282,26 +282,48 @@ private let question = UTF8.CodeUnit(ascii: "?")
282282
private let colon = UTF8.CodeUnit(ascii: ":")
283283
private let hyphen = UTF8.CodeUnit(ascii: "-")
284284

285+
/// A guesstimate of the "shape" of a Swift type based on its spelling.
286+
private enum ShapeOfSwiftTypeSpelling {
287+
/// This type spelling looks like a scalar.
288+
///
289+
/// For example `Name` or `(Name)`.
290+
/// - Note: We treat `(Name)` as a non-tuple so that we can remove the redundant leading and trailing parenthesis.
291+
case scalar
292+
/// This type spelling looks like a tuple.
293+
///
294+
/// For example `(First, Second)`.
295+
case tuple
296+
/// This type spelling looks like a closure.
297+
///
298+
/// For example `(First)->Second` or `(First, Second)->()` or `()->()`.
299+
case closure
300+
}
301+
285302
private extension ContiguousArray<UTF8.CodeUnit>.SubSequence {
286-
/// Checks if the UTF-8 string looks like a tuple with comma separated values.
303+
/// Checks if the UTF-8 string looks like a tuple, scalar, or closure.
287304
///
288305
/// This is used to remove redundant parenthesis around expressions.
289-
func isTuple() -> Bool {
290-
guard first == openParen, last == closeParen else { return false }
306+
func shapeOfSwiftTypeSpelling() -> ShapeOfSwiftTypeSpelling {
307+
guard first == openParen, last == closeParen else { return .scalar }
291308
var depth = 0
292-
for char in self {
293-
switch char {
309+
for index in indices {
310+
switch self[index] {
294311
case openParen:
295312
depth += 1
296313
case closeParen:
297314
depth -= 1
298315
case comma where depth == 1:
299-
return true
316+
// If we find "," in one level of parenthesis, we've found a tuple.
317+
return .tuple
318+
case closeAngle where depth == 0 && index > startIndex && self[index - 1] == hyphen:
319+
// If we find "->" outside any parentheses, we've found a closure.
320+
return .closure
300321
default:
301322
continue
302323
}
303324
}
304-
return false
325+
// If we traversed the entire type name without finding a tuple or a closure we treat the type name as a scalar.
326+
return .scalar
305327
}
306328
}
307329

Tests/SwiftDocCTests/Infrastructure/PathHierarchyTests.swift

Lines changed: 120 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3328,6 +3328,73 @@ class PathHierarchyTests: XCTestCase {
33283328
try assertFindsPath("/ModuleName/ContainerName", in: tree, asSymbolID: containerID)
33293329
}
33303330

3331+
func testMinimalTypeDisambiguationForClosureParameterWithVoidReturnType() throws {
3332+
// Create a `doSomething(with:and:)` function with a `String` parameter (same in every overload) and a `(TYPE)->()` closure parameter.
3333+
func makeSymbolOverload(closureParameterType: SymbolGraph.Symbol.DeclarationFragments.Fragment) -> SymbolGraph.Symbol {
3334+
makeSymbol(
3335+
id: "some-function-overload-\(closureParameterType.spelling.lowercased())",
3336+
kind: .method,
3337+
pathComponents: ["doSomething(with:and:)"],
3338+
signature: .init(
3339+
parameters: [
3340+
.init(name: "first", externalName: "with", declarationFragments: [
3341+
.init(kind: .externalParameter, spelling: "with", preciseIdentifier: nil),
3342+
.init(kind: .text, spelling: " ", preciseIdentifier: nil),
3343+
.init(kind: .internalParameter, spelling: "first", preciseIdentifier: nil),
3344+
.init(kind: .text, spelling: " ", preciseIdentifier: nil),
3345+
.init(kind: .typeIdentifier, spelling: "String", preciseIdentifier: "s:SS")
3346+
], children: []),
3347+
3348+
.init(name: "second", externalName: "and", declarationFragments: [
3349+
.init(kind: .externalParameter, spelling: "and", preciseIdentifier: nil),
3350+
.init(kind: .text, spelling: " ", preciseIdentifier: nil),
3351+
.init(kind: .internalParameter, spelling: "second", preciseIdentifier: nil),
3352+
.init(kind: .text, spelling: " (", preciseIdentifier: nil),
3353+
closureParameterType,
3354+
.init(kind: .text, spelling: ") -> ()", preciseIdentifier: nil),
3355+
], children: [])
3356+
],
3357+
returns: [.init(kind: .typeIdentifier, spelling: "Void", preciseIdentifier: "s:s4Voida")]
3358+
)
3359+
)
3360+
}
3361+
3362+
let catalog = Folder(name: "unit-test.docc", content: [
3363+
JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph(
3364+
moduleName: "ModuleName",
3365+
symbols: [
3366+
makeSymbolOverload(closureParameterType: .init(kind: .typeIdentifier, spelling: "Int", preciseIdentifier: "s:Si")), // (String, (Int)->()) -> Void
3367+
makeSymbolOverload(closureParameterType: .init(kind: .typeIdentifier, spelling: "Double", preciseIdentifier: "s:Sd")), // (String, (Double)->()) -> Void
3368+
makeSymbolOverload(closureParameterType: .init(kind: .typeIdentifier, spelling: "Float", preciseIdentifier: "s:Sf")), // (String, (Float)->()) -> Void
3369+
],
3370+
relationships: []
3371+
))
3372+
])
3373+
3374+
let (_, context) = try loadBundle(catalog: catalog)
3375+
let tree = context.linkResolver.localResolver.pathHierarchy
3376+
3377+
let link = "/ModuleName/doSomething(with:and:)"
3378+
try assertPathRaisesErrorMessage(link, in: tree, context: context, expectedErrorMessage: "'doSomething(with:and:)' is ambiguous at '/ModuleName'") { errorInfo in
3379+
XCTAssertEqual(errorInfo.solutions.count, 3, "There should be one suggestion per overload")
3380+
for solution in errorInfo.solutions {
3381+
// Apply the suggested replacements for each solution and verify that _that_ link resolves to a single symbol.
3382+
var linkWithSuggestion = link
3383+
XCTAssertFalse(solution.replacements.isEmpty, "Diagnostics about ambiguous links should have some replacements for each solution.")
3384+
for (replacementText, start, end) in solution.replacements {
3385+
let range = linkWithSuggestion.index(linkWithSuggestion.startIndex, offsetBy: start) ..< linkWithSuggestion.index(linkWithSuggestion.startIndex, offsetBy: end)
3386+
linkWithSuggestion.replaceSubrange(range, with: replacementText)
3387+
}
3388+
3389+
XCTAssertNotNil(try? tree.findSymbol(path: linkWithSuggestion), """
3390+
Failed to resolve \(linkWithSuggestion) after applying replacements \(solution.replacements.map { "'\($0.0)'@\($0.start)-\($0.end)" }.joined(separator: ",")) to '\(link)'.
3391+
3392+
The replacement that DocC suggests in its warnings should unambiguously refer to a single symbol match.
3393+
""")
3394+
}
3395+
}
3396+
}
3397+
33313398
func testMissingMemberOfAnonymousStructInsideUnion() throws {
33323399
let outerContainerID = "some-outer-container-symbol-id"
33333400
let innerContainerID = "some-inner-container-symbol-id"
@@ -3665,7 +3732,7 @@ class PathHierarchyTests: XCTestCase {
36653732
let voidType = DeclToken.typeIdentifier("Void", precise: "s:s4Voida")
36663733

36673734
func makeParameter(_ name: String, decl: [DeclToken]) -> SymbolGraph.Symbol.FunctionSignature.FunctionParameter {
3668-
.init(name: name, externalName: nil, declarationFragments: makeFragments([.internalParameter(name), .text("")] + decl), children: [])
3735+
.init(name: name, externalName: nil, declarationFragments: makeFragments([.internalParameter(name), .text(" ")] + decl), children: [])
36693736
}
36703737

36713738
func makeSignature(first: DeclToken..., second: DeclToken..., third: DeclToken...) -> SymbolGraph.Symbol.FunctionSignature {
@@ -3772,6 +3839,56 @@ class PathHierarchyTests: XCTestCase {
37723839
])
37733840
}
37743841

3842+
// Each overload has a unique closure parameter with a "()" literal closure return type
3843+
do {
3844+
func makeSignature(first: DeclToken..., second: DeclToken...) -> SymbolGraph.Symbol.FunctionSignature {
3845+
.init(
3846+
parameters: [
3847+
.init(name: "first", externalName: nil, declarationFragments: makeFragments(first), children: []),
3848+
.init(name: "second", externalName: nil, declarationFragments: makeFragments(second), children: [])
3849+
],
3850+
returns: makeFragments([voidType])
3851+
)
3852+
}
3853+
3854+
// String (Int)->()
3855+
// String (Double)->()
3856+
// String (Float)->()
3857+
let catalog = Folder(name: "unit-test.docc", content: [
3858+
JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph(
3859+
moduleName: "ModuleName",
3860+
symbols: [
3861+
// String (Int)->Void
3862+
makeSymbol(id: "function-overload-1", kind: .func, pathComponents: ["doSomething(first:second:)"], signature: makeSignature(
3863+
first: stringType, // String
3864+
second: "(", intType, ") -> ()" // (Int)->()
3865+
)),
3866+
3867+
// String (Double)->Void
3868+
makeSymbol(id: "function-overload-2", kind: .func, pathComponents: ["doSomething(first:second:)"], signature: makeSignature(
3869+
first: stringType, // String
3870+
second: "(", doubleType, ") -> ()" // (Double)->()
3871+
)),
3872+
3873+
// String (Float)->Void
3874+
makeSymbol(id: "function-overload-3", kind: .func, pathComponents: ["doSomething(first:second:)"], signature: makeSignature(
3875+
first: stringType, // String
3876+
second: "(", floatType, ") -> ()" // (Double)->()
3877+
)),
3878+
]
3879+
))
3880+
])
3881+
3882+
let (_, context) = try loadBundle(catalog: catalog)
3883+
let tree = context.linkResolver.localResolver.pathHierarchy
3884+
3885+
try assertPathCollision("ModuleName/doSomething(first:second:)", in: tree, collisions: [
3886+
(symbolID: "function-overload-1", disambiguation: "-(_,(Int)->())"), // _ (Int)->()
3887+
(symbolID: "function-overload-2", disambiguation: "-(_,(Double)->())"), // _ (Double)->()
3888+
(symbolID: "function-overload-3", disambiguation: "-(_,(Float)->())"), // _ (Float)->()
3889+
])
3890+
}
3891+
37753892
// The second overload refers to the metatype of the parameter
37763893
do {
37773894
func makeSignature(first: DeclToken...) -> SymbolGraph.Symbol.FunctionSignature {
@@ -4350,8 +4467,8 @@ class PathHierarchyTests: XCTestCase {
43504467
XCTFail("Symbol for \(path.singleQuoted) not found in tree", file: file, line: line)
43514468
} catch PathHierarchy.Error.unknownName {
43524469
XCTFail("Symbol for \(path.singleQuoted) not found in tree. Only part of path is found.", file: file, line: line)
4353-
} catch PathHierarchy.Error.unknownDisambiguation {
4354-
XCTFail("Symbol for \(path.singleQuoted) not found in tree. Unknown disambiguation.", file: file, line: line)
4470+
} catch PathHierarchy.Error.unknownDisambiguation(_, _, let candidates) {
4471+
XCTFail("Symbol for \(path.singleQuoted) not found in tree. Unknown disambiguation. Suggested disambiguations: \(candidates.map(\.disambiguation.singleQuoted).sorted().joined(separator: ", "))", file: file, line: line)
43554472
} catch PathHierarchy.Error.lookupCollision(_, _, let collisions) {
43564473
let symbols = collisions.map { $0.node.symbol! }
43574474
XCTFail("Unexpected collision for \(path.singleQuoted); \(symbols.map { return "\($0.names.title) - \($0.kind.identifier.identifier) - \($0.identifier.precise.stableHashString)"})", file: file, line: line)

0 commit comments

Comments
 (0)