Skip to content

Commit c08e171

Browse files
authored
Split tuple return types into smaller type disambiguation elements (#1142)
1 parent 2bd30b1 commit c08e171

File tree

2 files changed

+140
-22
lines changed

2 files changed

+140
-22
lines changed

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

+58-20
Original file line numberDiff line numberDiff line change
@@ -22,26 +22,68 @@ extension PathHierarchy {
2222

2323
let isSwift = symbol.identifier.interfaceLanguage == SourceLanguage.swift.id
2424
return (
25-
signature.parameters.map { parameterTypeSpellings(for: $0.declarationFragments, isSwift: isSwift) },
26-
returnTypeSpellings(for: signature.returns, isSwift: isSwift).map { [$0] } ?? []
25+
signature.parameters.map { parameterTypeSpelling(for: $0.declarationFragments, isSwift: isSwift) },
26+
returnTypeSpellings(for: signature.returns, isSwift: isSwift)
2727
)
2828
}
2929

30-
private static func parameterTypeSpellings(for fragments: [SymbolGraph.Symbol.DeclarationFragments.Fragment], isSwift: Bool) -> String {
31-
typeSpellings(for: fragments, isSwift: isSwift)
30+
/// Creates a type disambiguation string from the given function parameter declaration fragments.
31+
private static func parameterTypeSpelling(for fragments: [SymbolGraph.Symbol.DeclarationFragments.Fragment], isSwift: Bool) -> String {
32+
let accumulated = utf8TypeSpelling(for: fragments, isSwift: isSwift)
33+
34+
return String(decoding: accumulated, as: UTF8.self)
3235
}
3336

34-
private static func returnTypeSpellings(for fragments: [SymbolGraph.Symbol.DeclarationFragments.Fragment], isSwift: Bool) -> String? {
37+
/// Creates a list of type disambiguation strings for the function return declaration fragments.
38+
///
39+
/// Unlike ``parameterTypeSpelling(for:isSwift:)``, this function splits Swift tuple return values is split into smaller disambiguation elements.
40+
/// This makes it possible to disambiguate a `(Int, String)` return value using either `->(Int,_)`, `->(_,String)`, or `->(_,_)` (depending on the other overloads).
41+
private static func returnTypeSpellings(for fragments: [SymbolGraph.Symbol.DeclarationFragments.Fragment], isSwift: Bool) -> [String] {
3542
if fragments.count == 1, knownVoidReturnValues.contains(fragments.first!) {
3643
// We don't want to list "void" return values as type disambiguation
37-
return nil
44+
return []
45+
}
46+
let spelling = utf8TypeSpelling(for: fragments, isSwift: isSwift)
47+
48+
guard isSwift, spelling[...].isTuple() else {
49+
return [String(decoding: spelling, as: UTF8.self)]
3850
}
39-
return typeSpellings(for: fragments, isSwift: isSwift)
51+
52+
// This return value is a tuple that should be split into smaller type spellings
53+
var returnSpellings: [String] = []
54+
55+
var depth = 0
56+
let endIndex = spelling.count - 1 // before the trailing ")"
57+
var substringStartIndex = 1 // skip the leading "("
58+
for index in 1 /* after the leading "(" */ ..< endIndex {
59+
switch spelling[index] {
60+
case openParen:
61+
depth += 1
62+
case closeParen:
63+
depth -= 1
64+
case comma where depth == 0:
65+
// Split here without including the comma in the return value spelling.
66+
returnSpellings.append(
67+
String(decoding: spelling[substringStartIndex ..< index], as: UTF8.self)
68+
)
69+
// Also, skip past the comma for the next return value spelling.
70+
substringStartIndex = index + 1
71+
72+
default:
73+
continue
74+
}
75+
}
76+
returnSpellings.append(
77+
String(decoding: spelling[substringStartIndex ..< endIndex], as: UTF8.self)
78+
)
79+
80+
return returnSpellings
4081
}
4182

4283
private static let knownVoidReturnValues = ParametersAndReturnValidator.knownVoidReturnValuesByLanguage.flatMap { $0.value }
4384

44-
private static func typeSpellings(for fragments: [SymbolGraph.Symbol.DeclarationFragments.Fragment], isSwift: Bool) -> String {
85+
/// Returns the type name spelling as sequence of UTF-8 code units _without_ null-termination.
86+
private static func utf8TypeSpelling(for fragments: [SymbolGraph.Symbol.DeclarationFragments.Fragment], isSwift: Bool) -> ContiguousArray<UTF8.CodeUnit> {
4587
// This function joins the spelling of the text and identifier declaration fragments and applies Swift syntactic sugar;
4688
// `Array<Element>` -> `[Element]`, `Optional<Wrapped>` -> `Wrapped?`, and `Dictionary<Key,Value>` -> `[Key:Value]`
4789

@@ -162,25 +204,21 @@ extension PathHierarchy {
162204
markers[index] -= difference
163205

164206
assert(accumulated[markers[index]] == uppercaseA || accumulated[markers[index]] == uppercaseD || accumulated[markers[index]] == uppercaseO, """
165-
Unexpectedly found '\(String(cString: [accumulated[index], 0]))' at \(index) which should be either an Array, Optional, or Dictionary marker in \(String(cString: accumulated + [0]))
207+
Unexpectedly found '\(String(Unicode.Scalar(accumulated[index])))' at \(index) which should be either an Array, Optional, or Dictionary marker in \(String(decoding: accumulated, as: UTF8.self)))
166208
""")
167209
}
168210
}
169211

170212
assert(markers.allSatisfy { [uppercaseA, uppercaseD, uppercaseO].contains(accumulated[$0]) }, """
171-
Unexpectedly found misaligned markers: \(markers.map { "(index: \($0), char: \(String(cString: [accumulated[$0], 0])))" })
213+
Unexpectedly found misaligned markers: \(markers.map { "(index: \($0), char: \(String(Unicode.Scalar(accumulated[$0])))" })
172214
""")
173215

174216
// Check if we need to apply syntactic sugar to the accumulated declaration fragment spellings.
175217
if !markers.isEmpty {
176218
accumulated.applySwiftSyntacticSugar(markers: markers)
177219
}
178220

179-
// Add a null-terminator to create a String from the accumulated UTF-8 code units.
180-
accumulated.append(0)
181-
return accumulated.withUnsafeBufferPointer { pointer in
182-
String(cString: pointer.baseAddress!)
183-
}
221+
return accumulated
184222
}
185223

186224
/// A small helper type that tracks the scope of nested brackets; `()`, `[]`, or `<>`.
@@ -279,7 +317,7 @@ private extension ContiguousArray<UTF8.CodeUnit> {
279317
angleBracketStack.append(index)
280318
case closeAngle where self[index - 1] != hyphen: // "->" isn't the closing bracket of a generic
281319
guard let open = angleBracketStack.popLast() else {
282-
assertionFailure("Encountered unexpected generic scope brackets in \(String(cString: self + [0]))")
320+
assertionFailure("Encountered unexpected generic scope brackets in \(String(decoding: self, as: UTF8.self))")
283321
return
284322
}
285323

@@ -307,8 +345,8 @@ private extension ContiguousArray<UTF8.CodeUnit> {
307345
// Iterate over all the marked angle bracket pairs (from end to start) and replace the marked text with the syntactic sugar alternative.
308346
while !markedAngleBracketPairs.isEmpty {
309347
let (open, close) = markedAngleBracketPairs.removeLast()
310-
assert(self[open] == openAngle, "Start marker at \(open) is '\(String(cString: [self[open], 0]))' instead of '<' in \(String(cString: self + [0]))")
311-
assert(self[close] == closeAngle, "End marker at \(close) is '\(String(cString: [self[close], 0]))' instead of '>' in \(String(cString: self + [0]))")
348+
assert(self[open] == openAngle, "Start marker at \(open) is '\(String(Unicode.Scalar(self[open])))' instead of '<' in \(String(decoding: self, as: UTF8.self))")
349+
assert(self[close] == closeAngle, "End marker at \(close) is '\(String(Unicode.Scalar(self[close])))' instead of '>' in \(String(decoding: self, as: UTF8.self))")
312350

313351
// The caller accumulated a single character for each marker that indicated the type of syntactic sugar to apply.
314352
let marker = open - 1
@@ -349,12 +387,12 @@ private extension ContiguousArray<UTF8.CodeUnit> {
349387
}
350388
else if $0 == closeAngle || $0 == closeParen {
351389
depth -= 1
352-
assert(depth >= 0, "Unexpectedly found more closing brackets than open brackets in \(String(cString: self[open + 1 ..< close] + [0]))")
390+
assert(depth >= 0, "Unexpectedly found more closing brackets than open brackets in \(String(decoding: self[open + 1 ..< close], as: UTF8.self))")
353391
}
354392
return false // keep scanning
355393
}
356394
guard let commaIndex = self[open + 1 /* skip the known opening bracket */ ..< close /* skip the known closing bracket */].firstIndex(where: predicate) else {
357-
assertionFailure("Didn't find ',' in \(String(cString: self[open + 1 ..< close] + [0]))")
395+
assertionFailure("Didn't find ',' in \(String(decoding: self[open + 1 ..< close], as: UTF8.self))")
358396
return
359397
}
360398

Tests/SwiftDocCTests/Infrastructure/PathHierarchyTests.swift

+82-2
Original file line numberDiff line numberDiff line change
@@ -1767,7 +1767,7 @@ class PathHierarchyTests: XCTestCase {
17671767
_functionSignatureTypeNames(signature, language: .swift)
17681768
}
17691769

1770-
// func doSomething(someName: ((Int, String), Date)) -> ([Int, String?])
1770+
// func doSomething(someName: ((Int, String), Date)) -> ([Int], String?)
17711771
let tupleArgument = functionSignatureTypeNames(.init(
17721772
parameters: [
17731773
.init(name: "someName", externalName: nil, declarationFragments: [
@@ -1790,7 +1790,37 @@ class PathHierarchyTests: XCTestCase {
17901790
])
17911791
)
17921792
XCTAssertEqual(tupleArgument?.parameterTypeNames, ["((Int,String),Date)"])
1793-
XCTAssertEqual(tupleArgument?.returnTypeNames, ["([Int],String?)"])
1793+
XCTAssertEqual(tupleArgument?.returnTypeNames, ["[Int]", "String?"])
1794+
1795+
// func doSomething() -> ((Double, Double) -> Double, [Int: (Int, Int)], (Bool, Bool), String?)
1796+
let bigTupleReturnType = functionSignatureTypeNames(.init(
1797+
parameters: [],
1798+
returns: [
1799+
.init(kind: .text, spelling: "((", preciseIdentifier: nil),
1800+
.init(kind: .typeIdentifier, spelling: "Double", preciseIdentifier: "s:Sd"),
1801+
.init(kind: .text, spelling: ", ", preciseIdentifier: nil),
1802+
.init(kind: .typeIdentifier, spelling: "Double", preciseIdentifier: "s:Sd"),
1803+
.init(kind: .text, spelling: ") -> ", preciseIdentifier: nil),
1804+
.init(kind: .typeIdentifier, spelling: "Double", preciseIdentifier: "s:Sd"),
1805+
.init(kind: .text, spelling: ", [", preciseIdentifier: nil),
1806+
.init(kind: .typeIdentifier, spelling: "Int", preciseIdentifier: "s:Si"),
1807+
.init(kind: .text, spelling: ": (", preciseIdentifier: nil),
1808+
.init(kind: .typeIdentifier, spelling: "Int", preciseIdentifier: "s:Si"),
1809+
.init(kind: .text, spelling: ", ", preciseIdentifier: nil),
1810+
.init(kind: .typeIdentifier, spelling: "Int", preciseIdentifier: "s:Si"),
1811+
.init(kind: .text, spelling: ")], (", preciseIdentifier: nil),
1812+
.init(kind: .typeIdentifier, spelling: "Bool", preciseIdentifier: "s:Si"),
1813+
.init(kind: .text, spelling: ", ", preciseIdentifier: nil),
1814+
.init(kind: .typeIdentifier, spelling: "Bool", preciseIdentifier: "s:Si"),
1815+
.init(kind: .text, spelling: "), ", preciseIdentifier: nil),
1816+
.init(kind: .typeIdentifier, spelling: "Optional", preciseIdentifier: "s:Sq"),
1817+
.init(kind: .text, spelling: "<", preciseIdentifier: nil),
1818+
.init(kind: .typeIdentifier, spelling: "String", preciseIdentifier: "s:SS"),
1819+
.init(kind: .text, spelling: ">)", preciseIdentifier: nil),
1820+
])
1821+
)
1822+
XCTAssertEqual(bigTupleReturnType?.parameterTypeNames, [])
1823+
XCTAssertEqual(bigTupleReturnType?.returnTypeNames, ["(Double,Double)->Double", "[Int:(Int,Int)]", "(Bool,Bool)", "String?"])
17941824

17951825
// func doSomething(with someName: [Int?: String??])
17961826
let dictionaryWithOptionalsArgument = functionSignatureTypeNames(.init(
@@ -3171,6 +3201,56 @@ class PathHierarchyTests: XCTestCase {
31713201
])
31723202
}
31733203

3204+
// Each overload has one unique element in the tuple _return_ type
3205+
do {
3206+
func makeSignature(first: DeclToken..., second: DeclToken..., third: DeclToken...) -> SymbolGraph.Symbol.FunctionSignature {
3207+
.init(
3208+
parameters: [],
3209+
returns: makeFragments([.text("(")] + [first, second, third].joined(separator: [.text(", ")]) + [.text(")")])
3210+
)
3211+
}
3212+
3213+
// String [Int] (Double)->Void
3214+
// String? [Bool] (Double)->Void
3215+
// String? [Int] (Float)->Void
3216+
let catalog = Folder(name: "unit-test.docc", content: [
3217+
JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph(
3218+
moduleName: "ModuleName",
3219+
symbols: [
3220+
// String [Int] (Double)->Void
3221+
makeSymbol(id: "function-overload-1", kind: .func, pathComponents: ["doSomething(first:second:third:)"], signature: makeSignature(
3222+
first: stringType, // String
3223+
second: arrayType, "<", intType, ">", // [Int]
3224+
third: "(", doubleType, ") -> ", voidType // (Double)->Void
3225+
)),
3226+
3227+
// String? [Bool] (Double)->Void
3228+
makeSymbol(id: "function-overload-2", kind: .func, pathComponents: ["doSomething(first:second:third:)"], signature: makeSignature(
3229+
first: optionalType, "<", stringType, ">", // String?
3230+
second: arrayType, "<", boolType, ">", // [Bool]
3231+
third: "(", doubleType, ") -> ", voidType // (Double)->Void
3232+
)),
3233+
3234+
// String? [Int] (Float)->Void
3235+
makeSymbol(id: "function-overload-3", kind: .func, pathComponents: ["doSomething(first:second:third:)"], signature: makeSignature(
3236+
first: optionalType, "<", stringType, ">", // String?
3237+
second: arrayType, "<", intType, ">", // [Int]
3238+
third: "(", floatType, ") -> ", voidType // (Float)->Void
3239+
)),
3240+
]
3241+
))
3242+
])
3243+
3244+
let (_, context) = try loadBundle(catalog: catalog)
3245+
let tree = context.linkResolver.localResolver.pathHierarchy
3246+
3247+
try assertPathCollision("ModuleName/doSomething(first:second:third:)", in: tree, collisions: [
3248+
(symbolID: "function-overload-1", disambiguation: "->(String,_,_)"), // String _ _
3249+
(symbolID: "function-overload-2", disambiguation: "->(_,[Bool],_)"), // _ [Bool] _
3250+
(symbolID: "function-overload-3", disambiguation: "->(_,_,(Float)->Void)"), // _ _ (Float)->Void
3251+
])
3252+
}
3253+
31743254
// Second overload requires combination of two non-unique types to disambiguate
31753255
do {
31763256
// String Set<Int> (Double)->Void

0 commit comments

Comments
 (0)