Skip to content

Commit 36f57a4

Browse files
authored
Merge pull request #2891 from MAJKFL/sanitize-dollar-identifier
Sanitize dollar identifiers with leading zeros in `Identifier`.
2 parents df28b99 + c0f0ddd commit 36f57a4

File tree

5 files changed

+71
-34
lines changed

5 files changed

+71
-34
lines changed

Sources/SwiftLexicalLookup/LookupName.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ import SwiftSyntax
8686
/// `self` and `Self` identifers override implicit `self` and `Self` introduced by
8787
/// the `Foo` class declaration.
8888
var identifier: Identifier {
89-
Identifier(name)
89+
Identifier(canonicalName: name)
9090
}
9191

9292
/// Position of this implicit name.

Sources/SwiftLexicalLookup/Scopes/ScopeImplementations.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,10 +188,10 @@ import SwiftSyntax
188188
checkIdentifier(identifier, refersTo: name, at: lookUpPosition)
189189
}
190190

191-
if let dollarIdentifierStr = identifier?.dollarIdentifierStr {
191+
if let identifier, identifier.isDollarIdentifier {
192192
signatureResults = LookupResult.getResultArray(
193193
for: self,
194-
withNames: filteredCaptureNames + [LookupName.dollarIdentifier(self, strRepresentation: dollarIdentifierStr)]
194+
withNames: filteredCaptureNames + [LookupName.dollarIdentifier(self, strRepresentation: identifier.name)]
195195
)
196196
} else {
197197
signatureResults =

Sources/SwiftSyntax/Identifier.swift

Lines changed: 58 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ public struct Identifier: Equatable, Hashable, Sendable {
1717
String(syntaxText: raw.name)
1818
}
1919

20-
public let dollarIdentifierStr: String?
20+
/// `true` if the identifier is a dollar identifier.
21+
public var isDollarIdentifier: Bool {
22+
raw.original.hasPrefix(SyntaxText("$")) && Int(String(syntaxText: raw.original).dropFirst()) != nil
23+
}
2124

2225
@_spi(RawSyntax)
2326
public let raw: RawIdentifier
@@ -26,61 +29,86 @@ public struct Identifier: Equatable, Hashable, Sendable {
2629
public init?(_ token: TokenSyntax) {
2730
switch token.tokenKind {
2831
case .identifier, .keyword(.self), .keyword(.Self):
29-
self.raw = RawIdentifier(token.tokenView)
32+
self.raw = RawIdentifier(token.tokenView.rawText)
3033
self.arena = token.raw.arenaReference
31-
32-
self.dollarIdentifierStr = nil
3334
case .dollarIdentifier(let dollarIdentifierStr):
34-
self.raw = RawIdentifier(token.tokenView)
3535
self.arena = token.raw.arenaReference
3636

37-
self.dollarIdentifierStr = dollarIdentifierStr
37+
if Self.isPaddedDollarIdentifier(dollarIdentfierStr: dollarIdentifierStr),
38+
let newDollarIdentifierNumber = Int(dollarIdentifierStr.dropFirst())
39+
{
40+
let newDollarIdentifierStr = "$\(newDollarIdentifierNumber)"
41+
let sanitizedDollarIdentifierSyntaxText = token.raw.arenaReference.intern(newDollarIdentifierStr)
42+
43+
self.raw = RawIdentifier(sanitizedDollarIdentifierSyntaxText)
44+
} else {
45+
self.raw = RawIdentifier(token.tokenView.rawText)
46+
}
3847
default:
3948
return nil
4049
}
4150
}
4251

43-
public init(_ staticString: StaticString) {
44-
self.raw = RawIdentifier(staticString)
45-
self.arena = nil
46-
47-
let name = String(syntaxText: raw.name)
52+
/// Create a new `Identifier` from given `canonicalName`.
53+
///
54+
/// - Precondition: `canonicalName` is a canonical identifier i.e. doesn't
55+
/// use backticks and is not a dollar identifier with leading zeros.
56+
public init(canonicalName: StaticString) {
57+
precondition(
58+
Self.isCanonicalRepresentation(canonicalName),
59+
"\(canonicalName) is not a canonical identifier."
60+
)
4861

49-
if name.first == "$" && Int(name.dropFirst()) != nil {
50-
self.dollarIdentifierStr = name
51-
} else {
52-
self.dollarIdentifierStr = nil
53-
}
62+
self.raw = RawIdentifier(SyntaxText(canonicalName))
63+
self.arena = nil
5464
}
5565

5666
public static func == (lhs: Self, rhs: Self) -> Bool {
5767
lhs.name == rhs.name
5868
}
5969

60-
private static func getDollarIdentifierNumber(str: String) -> Int? {
61-
guard str.first == "$" else { return nil }
70+
/// Returns `true` if `staticString` is a canonical identifier i.e. doesn't
71+
/// use backticks and is not a dollar identifier with leading zeros.
72+
private static func isCanonicalRepresentation(_ staticString: StaticString) -> Bool {
73+
let text = SyntaxText(staticString)
74+
75+
guard !Self.hasBackticks(text) else { return false }
76+
77+
let str = String(syntaxText: text)
78+
let isDollarIdentifier = str.first == "$" && Int(str.dropFirst()) != nil
6279

63-
return Int(str.dropFirst())
80+
return !(isDollarIdentifier && Self.isPaddedDollarIdentifier(dollarIdentfierStr: str))
81+
}
82+
83+
/// Returns `true` if `rawText` doesn't use backticks.
84+
fileprivate static func hasBackticks(_ rawText: SyntaxText) -> Bool {
85+
let backtick = SyntaxText("`")
86+
return rawText.count > 2 && rawText.hasPrefix(backtick) && rawText.hasSuffix(backtick)
87+
}
88+
89+
/// Returns `true` if `dollarIdentfierStr` is not a
90+
/// dollar identifier with leading zeros.
91+
fileprivate static func isPaddedDollarIdentifier(dollarIdentfierStr: String) -> Bool {
92+
dollarIdentfierStr.count > 2 && dollarIdentfierStr.hasPrefix("$0")
6493
}
6594
}
6695

6796
@_spi(RawSyntax)
6897
public struct RawIdentifier: Equatable, Hashable, Sendable {
98+
fileprivate let original: SyntaxText
6999
public let name: SyntaxText
70100

71101
@_spi(RawSyntax)
72-
fileprivate init(_ raw: RawSyntaxTokenView) {
73-
let backtick = SyntaxText("`")
74-
if raw.rawText.count > 2 && raw.rawText.hasPrefix(backtick) && raw.rawText.hasSuffix(backtick) {
75-
let startIndex = raw.rawText.index(after: raw.rawText.startIndex)
76-
let endIndex = raw.rawText.index(before: raw.rawText.endIndex)
77-
self.name = SyntaxText(rebasing: raw.rawText[startIndex..<endIndex])
78-
} else {
79-
self.name = raw.rawText
102+
fileprivate init(_ rawText: SyntaxText) {
103+
self.original = rawText
104+
105+
guard Identifier.hasBackticks(rawText) else {
106+
self.name = rawText
107+
return
80108
}
81-
}
82109

83-
fileprivate init(_ staticString: StaticString) {
84-
name = SyntaxText(staticString)
110+
let startIndex = rawText.index(after: rawText.startIndex)
111+
let endIndex = rawText.index(before: rawText.endIndex)
112+
self.name = SyntaxText(rebasing: rawText[startIndex..<endIndex])
85113
}
86114
}

Sources/SwiftSyntax/SyntaxArena.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,12 @@ struct SyntaxArenaRef: Hashable, @unchecked Sendable {
291291
return RetainedSyntaxArena(value)
292292
}
293293

294+
/// Copies a UTF8 sequence of `String` to the memory the referenced arena manages, and
295+
/// returns the copied string as a ``SyntaxText``
296+
func intern(_ value: String) -> SyntaxText {
297+
self.value.intern(value)
298+
}
299+
294300
#if DEBUG || SWIFTSYNTAX_ENABLE_ASSERTIONS
295301
/// Accessor for the underlying's `SyntaxArena.hasParent`
296302
var hasParent: Bool {

Tests/SwiftLexicalLookupTest/NameLookupTests.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ final class testNameLookup: XCTestCase {
142142
func foo() {
143143
let 0️⃣a = 1
144144
let x = 5️⃣{
145-
print(2️⃣a, 3️⃣$0, 4️⃣$123)
145+
print(2️⃣a, 3️⃣$0, 4️⃣$123, 6️⃣$00000001)
146146
}
147147
}
148148
""",
@@ -157,6 +157,9 @@ final class testNameLookup: XCTestCase {
157157
"4️⃣": [
158158
.fromScope(ClosureExprSyntax.self, expectedNames: [NameExpectation.dollarIdentifier("5️⃣", "$123")])
159159
],
160+
"6️⃣": [
161+
.fromScope(ClosureExprSyntax.self, expectedNames: [NameExpectation.dollarIdentifier("5️⃣", "$1")])
162+
],
160163
]
161164
)
162165
}

0 commit comments

Comments
 (0)