Skip to content

Commit a6855ca

Browse files
authored
Omit Swift tuple labels in type signature disambiguation (#1139)
* Omit Swift tuple labels in type signature disambiguation rdar://142822416 * Add debug assertion about balanced open and close brackets * Fix a logic bug where "->" incorrectly popped bracket scopes
1 parent b1d4c6c commit a6855ca

File tree

2 files changed

+137
-11
lines changed

2 files changed

+137
-11
lines changed

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

+66-10
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ extension PathHierarchy {
5454

5555
// Iterating over the declaration fragments to accumulate their spelling and to identify places that need to apply syntactic sugar.
5656
var markers = ContiguousArray<Int>()
57+
// Track the current [], (), and <> scopes to identify when ":" is a part of the type name.
58+
var swiftBracketsStack = SwiftBracketsStack()
59+
5760
for fragment in fragments {
5861
let preciseIdentifier = fragment.preciseIdentifier
5962
if isSwift {
@@ -84,17 +87,38 @@ extension PathHierarchy {
8487
// Accumulate all of the identifier tokens' spelling.
8588
accumulated.append(contentsOf: fragment.spelling.utf8)
8689

87-
case .text: // In Swift, only `text` tokens contains whitespace so they're handled separately.
88-
// Text tokens like "[", "?", "<", "...", ",", "(", "->" etc. contribute to the type spellings like
90+
case .text: // In Swift, we're only want some `text` tokens characters in the type disambiguation.
91+
// For example: "[", "?", "<", "...", ",", "(", "->" etc. contribute to the type spellings like
8992
// `[Name]`, `Name?`, "Name<T>", "Name...", "()", "(Name, Name)", "(Name)->Name" and more.
90-
var spelling = fragment.spelling.utf8[...]
91-
// If the type spelling is a parameter, if often starts with a leading ":" that's not part of the type name.
92-
if accumulated.isEmpty, spelling.first == colon {
93-
_ = spelling.removeFirst()
94-
}
95-
96-
// Ignore whitespace in text tokens. Here we use a loop instead of `filter` to avoid a potential temporary allocation.
97-
for char in spelling where char != space {
93+
let utf8Spelling = fragment.spelling.utf8
94+
for index in utf8Spelling.indices {
95+
let char = utf8Spelling[index]
96+
switch char {
97+
case openAngle:
98+
swiftBracketsStack.push(.angle)
99+
case openParen:
100+
swiftBracketsStack.push(.paren)
101+
case openSquare:
102+
swiftBracketsStack.push(.square)
103+
104+
case closeAngle:
105+
guard utf8Spelling.startIndex < index, utf8Spelling[utf8Spelling.index(before: index)] != hyphen else {
106+
break // "->" shouldn't count when balancing brackets but should still be included in the type spelling.
107+
}
108+
fallthrough
109+
case closeSquare, closeParen:
110+
assert(!swiftBracketsStack.isEmpty, "Unexpectedly found more closing brackets than open brackets in \(fragments.map(\.spelling).joined())")
111+
swiftBracketsStack.pop()
112+
113+
case colon where swiftBracketsStack.isCurrentScopeSquareBracket,
114+
comma, fullStop, question, hyphen:
115+
break // Include this character
116+
117+
default:
118+
continue // Skip this character
119+
}
120+
121+
// Unless the switch-statement (above) continued the next iteration, add this character to the accumulated type spelling.
98122
accumulated.append(char)
99123
}
100124

@@ -158,6 +182,37 @@ extension PathHierarchy {
158182
String(cString: pointer.baseAddress!)
159183
}
160184
}
185+
186+
/// A small helper type that tracks the scope of nested brackets; `()`, `[]`, or `<>`.
187+
private struct SwiftBracketsStack {
188+
enum Bracket {
189+
case angle // <>
190+
case square // []
191+
case paren // ()
192+
}
193+
private var stack: ContiguousArray<Bracket>
194+
init() {
195+
stack = []
196+
stack.reserveCapacity(32) // Some temporary space to work with.
197+
}
198+
199+
/// Push a new bracket scope to the stack.
200+
mutating func push(_ scope: Bracket) {
201+
stack.append(scope)
202+
}
203+
/// Pop the current bracket scope from the stack.
204+
mutating func pop() {
205+
_ = stack.popLast()
206+
}
207+
/// A Boolean value that indicates whether the current scope is square brackets.
208+
var isCurrentScopeSquareBracket: Bool {
209+
stack.last == .square
210+
}
211+
212+
var isEmpty: Bool {
213+
stack.isEmpty
214+
}
215+
}
161216
}
162217

163218
// A collection of UInt8 raw values for various UTF-8 characters that this implementation frequently checks for
@@ -176,6 +231,7 @@ private let openParen = UTF8.CodeUnit(ascii: "(")
176231
private let closeParen = UTF8.CodeUnit(ascii: ")")
177232

178233
private let comma = UTF8.CodeUnit(ascii: ",")
234+
private let fullStop = UTF8.CodeUnit(ascii: ".")
179235
private let question = UTF8.CodeUnit(ascii: "?")
180236
private let colon = UTF8.CodeUnit(ascii: ":")
181237
private let hyphen = UTF8.CodeUnit(ascii: "-")

Tests/SwiftDocCTests/Infrastructure/PathHierarchyTests.swift

+71-1
Original file line numberDiff line numberDiff line change
@@ -1368,7 +1368,7 @@ class PathHierarchyTests: XCTestCase {
13681368
return PathHierarchy.functionSignatureTypeNames(for: SymbolGraph.Symbol(
13691369
identifier: SymbolGraph.Symbol.Identifier(precise: "some-symbol-id", interfaceLanguage: SourceLanguage.swift.id),
13701370
names: .init(title: "SymbolName", navigator: nil, subHeading: nil, prose: nil),
1371-
pathComponents: ["SymbolName"], docComment: nil, accessLevel: .public, kind: .init(parsedIdentifier: .class, displayName: "Kind Display NAme"), mixins: [
1371+
pathComponents: ["SymbolName"], docComment: nil, accessLevel: .public, kind: .init(parsedIdentifier: .class, displayName: "Kind Display Name"), mixins: [
13721372
SymbolGraph.Symbol.FunctionSignature.mixinKey: SymbolGraph.Symbol.FunctionSignature(
13731373
parameters: [
13741374
.init(name: "someName", externalName: "with", declarationFragments: [
@@ -1418,6 +1418,30 @@ class PathHierarchyTests: XCTestCase {
14181418
.init(kind: .text, spelling: ">", preciseIdentifier: nil),
14191419
]))
14201420

1421+
// any Sequence<Int>
1422+
// The Swift symbol graph extractor emits `any` differently than `some` (rdar://142814138).
1423+
XCTAssertEqual("Sequence<Int>", functionSignatureParameterTypeName([
1424+
.init(kind: .text, spelling: "any ", preciseIdentifier: nil),
1425+
.init(kind: .typeIdentifier, spelling: "Sequence", preciseIdentifier: "s:ST"),
1426+
.init(kind: .text, spelling: "<", preciseIdentifier: nil),
1427+
.init(kind: .typeIdentifier, spelling: "Int", preciseIdentifier: "s:Si"),
1428+
.init(kind: .text, spelling: ">", preciseIdentifier: nil),
1429+
]))
1430+
1431+
// (Int, String)
1432+
// Swift _does_ support overloading by tuple labels but we don't include tuple labels in the type disambiguation because it would be
1433+
// longer and harder to read/write in the common case when the other overloads aren't tuples with the same types but different labels.
1434+
// In the rare case of actual overloads only distinguishable by tuple labels they would all require hash disambiguation instead.
1435+
XCTAssertEqual("(Int,String)", functionSignatureParameterTypeName([
1436+
.init(kind: .text, spelling: "(number ", preciseIdentifier: nil),
1437+
.init(kind: .text, spelling: ": ", preciseIdentifier: nil),
1438+
.init(kind: .typeIdentifier, spelling: "Int", preciseIdentifier: "s:Si"),
1439+
.init(kind: .text, spelling: ", text", preciseIdentifier: nil),
1440+
.init(kind: .text, spelling: ": ", preciseIdentifier: nil),
1441+
.init(kind: .typeIdentifier, spelling: "String", preciseIdentifier: "s:SS"),
1442+
.init(kind: .text, spelling: ")", preciseIdentifier: nil),
1443+
]))
1444+
14211445
// Array<(Int,Double)>
14221446
XCTAssertEqual("[(Int,Double)]", functionSignatureParameterTypeName([
14231447
.init(kind: .typeIdentifier, spelling: "Array", preciseIdentifier: "s:Sa"),
@@ -1472,6 +1496,52 @@ class PathHierarchyTests: XCTestCase {
14721496
.init(kind: .text, spelling: ">", preciseIdentifier: nil),
14731497
]))
14741498

1499+
// [[Double: Int]]
1500+
XCTAssertEqual("[[Double:Int]]", functionSignatureParameterTypeName([
1501+
.init(kind: .text, spelling: "[[", preciseIdentifier: nil),
1502+
.init(kind: .typeIdentifier, spelling: "Double", preciseIdentifier: "s:Sd"),
1503+
.init(kind: .text, spelling: " : ", preciseIdentifier: nil),
1504+
.init(kind: .typeIdentifier, spelling: "Int", preciseIdentifier: "s:Si"),
1505+
.init(kind: .text, spelling: "]]", preciseIdentifier: nil),
1506+
]))
1507+
1508+
// [ ([Int]?) : Int]
1509+
XCTAssertEqual("[([Int]?):Int]", functionSignatureParameterTypeName([
1510+
.init(kind: .text, spelling: "[ ([", preciseIdentifier: nil),
1511+
.init(kind: .typeIdentifier, spelling: "Int", preciseIdentifier: "s:Si"),
1512+
.init(kind: .text, spelling: "]?) : ", preciseIdentifier: nil),
1513+
.init(kind: .typeIdentifier, spelling: "Int", preciseIdentifier: "s:Si"),
1514+
.init(kind: .text, spelling: "]", preciseIdentifier: nil),
1515+
]))
1516+
1517+
// [Array<(_: Int)>: (number: Int, text: String)]
1518+
XCTAssertEqual("[[(Int)]:(Int,String)]", functionSignatureParameterTypeName([
1519+
.init(kind: .text, spelling: "[", preciseIdentifier: nil),
1520+
.init(kind: .typeIdentifier, spelling: "Array", preciseIdentifier: "s:Sa"),
1521+
.init(kind: .text, spelling: "<(_:", preciseIdentifier: nil),
1522+
.init(kind: .typeIdentifier, spelling: "Int", preciseIdentifier: "s:Si"),
1523+
.init(kind: .text, spelling: ")>: (number", preciseIdentifier: nil),
1524+
.init(kind: .text, spelling: ": ", preciseIdentifier: nil),
1525+
.init(kind: .typeIdentifier, spelling: "Int", preciseIdentifier: "s:Si"),
1526+
.init(kind: .text, spelling: ", text", preciseIdentifier: nil),
1527+
.init(kind: .text, spelling: ": ", preciseIdentifier: nil),
1528+
.init(kind: .typeIdentifier, spelling: "String", preciseIdentifier: "s:SS"),
1529+
.init(kind: .text, spelling: ")]", preciseIdentifier: nil),
1530+
]))
1531+
1532+
// [[Int: Int] : [Int: Int]]
1533+
XCTAssertEqual("[[Int:Int]:[Int:Int]]", functionSignatureParameterTypeName([
1534+
.init(kind: .text, spelling: "[[", preciseIdentifier: nil),
1535+
.init(kind: .typeIdentifier, spelling: "Int", preciseIdentifier: "s:Si"),
1536+
.init(kind: .text, spelling: ": ", preciseIdentifier: nil),
1537+
.init(kind: .typeIdentifier, spelling: "Int", preciseIdentifier: "s:Si"),
1538+
.init(kind: .text, spelling: "] : [", preciseIdentifier: nil),
1539+
.init(kind: .typeIdentifier, spelling: "Int", preciseIdentifier: "s:Si"),
1540+
.init(kind: .text, spelling: ": ", preciseIdentifier: nil),
1541+
.init(kind: .typeIdentifier, spelling: "Int", preciseIdentifier: "s:Si"),
1542+
.init(kind: .text, spelling: "]]", preciseIdentifier: nil),
1543+
]))
1544+
14751545
// (Dictionary<Double,Int>)->Array<String>
14761546
XCTAssertEqual("([Double:Int])->[String]", functionSignatureParameterTypeName([
14771547
.init(kind: .text, spelling: "(", preciseIdentifier: nil),

0 commit comments

Comments
 (0)