Skip to content

Commit 214a4bb

Browse files
committed
Diagnose if a number is used in place of an identifier
1 parent 1237f87 commit 214a4bb

File tree

8 files changed

+106
-81
lines changed

8 files changed

+106
-81
lines changed

Sources/SwiftParser/Diagnostics/MissingNodesError.swift

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,12 @@ fileprivate enum NodesDescriptionPart {
3737
}
3838
return "'\(tokenContents.trimmingWhitespace())'"
3939
case .tokenWithoutDefaultText(let token):
40-
if let parent = token.parent,
41-
let childName = parent.childNameForDiagnostics(token.index) {
40+
if let childName = token.childNameInParent {
4241
return childName
4342
}
4443
return token.tokenKind.decomposeToRaw().rawKind.nameForDiagnostics
4544
case .node(let node):
46-
if let parent = node.parent,
47-
let childName = parent.childNameForDiagnostics(node.index) {
45+
if let childName = node.childNameInParent {
4846
return childName
4947
} else {
5048
return node.nodeTypeNameForDiagnostics(allowBlockNames: true)

Sources/SwiftParser/Diagnostics/ParseDiagnosticsGenerator.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,12 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
194194
fixIts = []
195195
}
196196

197-
addDiagnostic(invalidIdentifier, InvalidIdentifierError(invalidIdentifier: invalidIdentifier), fixIts: fixIts, handledNodes: [previousParent.id])
197+
addDiagnostic(
198+
invalidIdentifier,
199+
InvalidIdentifierError(invalidIdentifier: invalidIdentifier, missingIdentifier: node),
200+
fixIts: fixIts,
201+
handledNodes: [previousParent.id]
202+
)
198203
} else {
199204
return handleMissingSyntax(node)
200205
}

Sources/SwiftParser/Diagnostics/ParserDiagnosticMessages.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,11 +122,15 @@ public struct IdentifierNotAllowedInOperatorName: ParserError {
122122

123123
public struct InvalidIdentifierError: ParserError {
124124
public let invalidIdentifier: TokenSyntax
125+
public let missingIdentifier: TokenSyntax
125126

126127
public var message: String {
127128
switch invalidIdentifier.tokenKind {
129+
case .floatingLiteral(let text), .integerLiteral(let text):
130+
fallthrough
128131
case .unknown(let text) where text.first?.isNumber == true:
129-
return "identifier can only start with a letter or underscore, not a number"
132+
let name = missingIdentifier.childNameInParent ?? "identifier"
133+
return "\(name) can only start with a letter or underscore, not a number"
130134
case .wildcardKeyword:
131135
return "'\(invalidIdentifier.text)' cannot be used as an identifier here"
132136
case let tokenKind where tokenKind.isKeyword:

Sources/SwiftParser/Parser.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,12 @@ extension Parser {
387387
self.missingToken(.identifier, text: nil)
388388
)
389389
}
390-
if keywordRecovery, self.currentToken.tokenKind.isKeyword, !self.currentToken.isAtStartOfLine {
390+
if let number = self.consume(ifAny: [.integerLiteral, .floatingLiteral]) {
391+
return (
392+
RawUnexpectedNodesSyntax(elements: [RawSyntax(number)], arena: self.arena),
393+
self.missingToken(.identifier, text: nil)
394+
)
395+
} else if keywordRecovery, self.currentToken.tokenKind.isKeyword, !self.currentToken.isAtStartOfLine {
391396
let keyword = self.consumeAnyToken()
392397
return (
393398
RawUnexpectedNodesSyntax(elements: [RawSyntax(keyword)], arena: self.arena),

Sources/SwiftSyntax/Syntax.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,9 +134,23 @@ public protocol SyntaxProtocol: CustomStringConvertible,
134134

135135
/// Return a name with which the child at the given `index` can be referred to
136136
/// in diagnostics.
137+
/// Typically, you want to use `childNameInParent` on the child instead of
138+
/// calling this method on the parent.
137139
func childNameForDiagnostics(_ index: SyntaxChildrenIndex) -> String?
138140
}
139141

142+
public extension SyntaxProtocol {
143+
/// If the parent has a dedicated "name for diagnostics" for this node, return it.
144+
/// Otherwise, return `nil`.
145+
var childNameInParent: String? {
146+
if let parent = self.parent, let childName = parent.childNameForDiagnostics(self.index) {
147+
return childName
148+
} else {
149+
return nil
150+
}
151+
}
152+
}
153+
140154
extension SyntaxProtocol {
141155
var data: SyntaxData {
142156
return _syntaxNode.data

Tests/SwiftParserTest/Declarations.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -690,8 +690,8 @@ final class DeclarationTests: XCTestCase {
690690
}
691691
""",
692692
diagnostics: [
693-
DiagnosticSpec(locationMarker: "1️⃣", message: "identifier can only start with a letter or underscore, not a number"),
694-
DiagnosticSpec(locationMarker: "2️⃣", message: "identifier can only start with a letter or underscore, not a number"),
693+
DiagnosticSpec(locationMarker: "1️⃣", message: "name can only start with a letter or underscore, not a number"),
694+
DiagnosticSpec(locationMarker: "2️⃣", message: "name can only start with a letter or underscore, not a number"),
695695
]
696696
)
697697
}
@@ -1023,7 +1023,7 @@ final class DeclarationTests: XCTestCase {
10231023
AssertParse(
10241024
"associatedtype 1️⃣5s",
10251025
diagnostics: [
1026-
DiagnosticSpec(message: "identifier can only start with a letter or underscore, not a number"),
1026+
DiagnosticSpec(message: "name can only start with a letter or underscore, not a number"),
10271027
]
10281028
)
10291029
}

Tests/SwiftParserTest/translated/EnumTests.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -259,13 +259,13 @@ final class EnumTests: XCTestCase {
259259
AssertParse(
260260
"""
261261
enum SwitchEnvy {
262-
case 1️⃣0:
262+
case 1️⃣02️⃣:
263263
}
264264
""",
265265
diagnostics: [
266266
// TODO: Old parser expected error on line 2: 'case' label can only appear inside a 'switch' statement
267-
DiagnosticSpec(message: "expected name in enum case"),
268-
DiagnosticSpec(message: "unexpected code '0:' in enum"),
267+
DiagnosticSpec(locationMarker: "1️⃣", message: "name can only start with a letter or underscore, not a number"),
268+
DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected code ':' in enum"),
269269
]
270270
)
271271
}

Tests/SwiftParserTest/translated/NumberIdentifierErrorsTests.swift

Lines changed: 67 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -13,27 +13,40 @@ final class NumberIdentifierErrorsTests: XCTestCase {
1313
)
1414
}
1515

16-
func testNumberIdentifierErrors2() {
16+
func testNumberIdentifierErrors2a() {
1717
AssertParse(
1818
"""
1919
func 1️⃣1() {}
20-
func 2️⃣2.0() {}
21-
func 3️⃣3func() {}
2220
""",
2321
diagnostics: [
24-
// TODO: Old parser expected error on line 1: function name can only start with a letter or underscore, not a number
25-
DiagnosticSpec(locationMarker: "1️⃣", message: "expected name in function"),
26-
DiagnosticSpec(locationMarker: "1️⃣", message: "unexpected code '1' before parameter clause"),
27-
// TODO: Old parser expected error on line 2: function name can only start with a letter or underscore, not a number
28-
DiagnosticSpec(locationMarker: "2️⃣", message: "expected name in function"),
29-
DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected code '2.0' before parameter clause"),
30-
// TODO: Old parser expected error on line 3: function name can only start with a letter or underscore, not a number
31-
// TODO: Old parser expected error on line 3: 'f' is not a valid digit in integer literal
32-
DiagnosticSpec(locationMarker: "3️⃣", message: "identifier can only start with a letter or underscore, not a number"),
22+
DiagnosticSpec(message: "name can only start with a letter or underscore, not a number"),
3323
]
3424
)
3525
}
3626

27+
func testNumberIdentifierErrors2b() {
28+
AssertParse(
29+
"""
30+
func 1️⃣2.0() {}
31+
""",
32+
diagnostics: [
33+
DiagnosticSpec(message: "name can only start with a letter or underscore, not a number"),
34+
]
35+
)
36+
}
37+
38+
func testNumberIdentifierErrors2c() {
39+
AssertParse(
40+
"""
41+
func 1️⃣3func() {}
42+
""",
43+
diagnostics: [
44+
DiagnosticSpec(message: "name can only start with a letter or underscore, not a number"),
45+
]
46+
)
47+
}
48+
49+
3750
func testNumberIdentifierErrors3a() {
3851
AssertParse(
3952
"""
@@ -42,10 +55,8 @@ final class NumberIdentifierErrorsTests: XCTestCase {
4255
}
4356
""",
4457
diagnostics: [
45-
// TODO: Old parser expected error on line 1: protocol name can only start with a letter or underscore, not a number
46-
DiagnosticSpec(locationMarker: "1️⃣", message: "expected name and member block in protocol"),
47-
// TODO: Old parser expected error on line 2: associatedtype name can only start with a letter or underscore, not a number
48-
DiagnosticSpec(locationMarker: "2️⃣", message: "expected name in associatedtype declaration"),
58+
DiagnosticSpec(locationMarker: "1️⃣", message: "name can only start with a letter or underscore, not a number"),
59+
DiagnosticSpec(locationMarker: "2️⃣", message: "name can only start with a letter or underscore, not a number"),
4960
]
5061
)
5162
}
@@ -58,10 +69,8 @@ final class NumberIdentifierErrorsTests: XCTestCase {
5869
}
5970
""",
6071
diagnostics: [
61-
// TODO: Old parser expected error on line 1: protocol name can only start with a letter or underscore, not a number
62-
DiagnosticSpec(locationMarker: "1️⃣", message: "expected name and member block in protocol"),
63-
// TODO: Old parser expected error on line 2: associatedtype name can only start with a letter or underscore, not a number
64-
DiagnosticSpec(locationMarker: "2️⃣", message: "expected name in associatedtype declaration"),
72+
DiagnosticSpec(locationMarker: "1️⃣", message: "name can only start with a letter or underscore, not a number"),
73+
DiagnosticSpec(locationMarker: "2️⃣", message: "name can only start with a letter or underscore, not a number"),
6574
]
6675
)
6776
}
@@ -74,34 +83,42 @@ final class NumberIdentifierErrorsTests: XCTestCase {
7483
}
7584
""",
7685
diagnostics: [
77-
// TODO: Old parser expected error on line 1: protocol name can only start with a letter or underscore, not a number
78-
// TODO: Old parser expected error on line 1: 'p' is not a valid digit in integer literal
79-
DiagnosticSpec(locationMarker: "1️⃣", message: "identifier can only start with a letter or underscore, not a number"),
80-
// TODO: Old parser expected error on line 2: associatedtype name can only start with a letter or underscore, not a number
81-
// TODO: Old parser expected error on line 2: 'a' is not a valid digit in integer literal
82-
DiagnosticSpec(locationMarker: "2️⃣", message: "identifier can only start with a letter or underscore, not a number"),
86+
DiagnosticSpec(locationMarker: "1️⃣", message: "name can only start with a letter or underscore, not a number"),
87+
DiagnosticSpec(locationMarker: "2️⃣", message: "name can only start with a letter or underscore, not a number"),
8388
]
8489
)
8590
}
8691

8792

88-
func testNumberIdentifierErrors4() {
93+
func testNumberIdentifierErrors4a() {
8994
AssertParse(
9095
"""
9196
typealias 1️⃣10 = Int
92-
typealias 2️⃣11.0 = Int
93-
typealias 3️⃣12typealias = Int
9497
""",
9598
diagnostics: [
96-
// TODO: Old parser expected error on line 1: typealias name can only start with a letter or underscore, not a number
97-
DiagnosticSpec(locationMarker: "1️⃣", message: "expected name in typealias declaration"),
98-
DiagnosticSpec(locationMarker: "1️⃣", message: "unexpected code '10' in typealias declaration"),
99-
// TODO: Old parser expected error on line 2: typealias name can only start with a letter or underscore, not a number
100-
DiagnosticSpec(locationMarker: "2️⃣", message: "expected name in typealias declaration"),
101-
DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected code '11.0' in typealias declaration"),
102-
// TODO: Old parser expected error on line 3: typealias name can only start with a letter or underscore, not a number
103-
// TODO: Old parser expected error on line 3: 't' is not a valid digit in integer literal
104-
DiagnosticSpec(locationMarker: "3️⃣", message: "identifier can only start with a letter or underscore, not a number"),
99+
DiagnosticSpec(message: "name can only start with a letter or underscore, not a number"),
100+
]
101+
)
102+
}
103+
104+
func testNumberIdentifierErrors4b() {
105+
AssertParse(
106+
"""
107+
typealias 1️⃣11.0 = Int
108+
""",
109+
diagnostics: [
110+
DiagnosticSpec(message: "name can only start with a letter or underscore, not a number"),
111+
]
112+
)
113+
}
114+
115+
func testNumberIdentifierErrors4c() {
116+
AssertParse(
117+
"""
118+
typealias 1️⃣12typealias = Int
119+
""",
120+
diagnostics: [
121+
DiagnosticSpec(message: "name can only start with a letter or underscore, not a number"),
105122
]
106123
)
107124
}
@@ -112,8 +129,7 @@ final class NumberIdentifierErrorsTests: XCTestCase {
112129
struct 1️⃣13 {}
113130
""",
114131
diagnostics: [
115-
// TODO: Old parser expected error on line 1: struct name can only start with a letter or underscore, not a number
116-
DiagnosticSpec(message: "expected name and member block in struct"),
132+
DiagnosticSpec(message: "name can only start with a letter or underscore, not a number"),
117133
]
118134
)
119135
}
@@ -124,8 +140,7 @@ final class NumberIdentifierErrorsTests: XCTestCase {
124140
struct 1️⃣14.0 {}
125141
""",
126142
diagnostics: [
127-
// TODO: Old parser expected error on line 1: struct name can only start with a letter or underscore, not a number
128-
DiagnosticSpec(message: "expected name and member block in struct"),
143+
DiagnosticSpec(message: "name can only start with a letter or underscore, not a number"),
129144
]
130145
)
131146
}
@@ -136,9 +151,7 @@ final class NumberIdentifierErrorsTests: XCTestCase {
136151
struct 1️⃣15struct {}
137152
""",
138153
diagnostics: [
139-
// TODO: Old parser expected error on line 1: struct name can only start with a letter or underscore, not a number
140-
// TODO: Old parser expected error on line 1: 's' is not a valid digit in integer literal
141-
DiagnosticSpec(message: "identifier can only start with a letter or underscore, not a number"),
154+
DiagnosticSpec(message: "name can only start with a letter or underscore, not a number"),
142155
]
143156
)
144157
}
@@ -149,8 +162,7 @@ final class NumberIdentifierErrorsTests: XCTestCase {
149162
enum 1️⃣16 {}
150163
""",
151164
diagnostics: [
152-
// TODO: Old parser expected error on line 1: enum name can only start with a letter or underscore, not a number
153-
DiagnosticSpec(message: "expected name and member block in enum"),
165+
DiagnosticSpec(message: "name can only start with a letter or underscore, not a number"),
154166
]
155167
)
156168
}
@@ -161,8 +173,7 @@ final class NumberIdentifierErrorsTests: XCTestCase {
161173
enum 1️⃣17.0 {}
162174
""",
163175
diagnostics: [
164-
// TODO: Old parser expected error on line 1: enum name can only start with a letter or underscore, not a number
165-
DiagnosticSpec(message: "expected name and member block in enum"),
176+
DiagnosticSpec(message: "name can only start with a letter or underscore, not a number"),
166177
]
167178
)
168179
}
@@ -173,9 +184,7 @@ final class NumberIdentifierErrorsTests: XCTestCase {
173184
enum 1️⃣18enum {}
174185
""",
175186
diagnostics: [
176-
// TODO: Old parser expected error on line 1: enum name can only start with a letter or underscore, not a number
177-
// TODO: Old parser expected error on line 1: 'n' is not a valid digit in floating point exponent
178-
DiagnosticSpec(message: "identifier can only start with a letter or underscore, not a number"),
187+
DiagnosticSpec(message: "name can only start with a letter or underscore, not a number"),
179188
]
180189
)
181190
}
@@ -188,11 +197,8 @@ final class NumberIdentifierErrorsTests: XCTestCase {
188197
}
189198
""",
190199
diagnostics: [
191-
// TODO: Old parser expected error on line 1: class name can only start with a letter or underscore, not a number
192-
DiagnosticSpec(locationMarker: "1️⃣", message: "expected name and member block in class"),
193-
// TODO: Old parser expected error on line 2: function name can only start with a letter or underscore, not a number
194-
DiagnosticSpec(locationMarker: "2️⃣", message: "expected name in function"),
195-
DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected code '20' before parameter clause"),
200+
DiagnosticSpec(locationMarker: "1️⃣", message: "name can only start with a letter or underscore, not a number"),
201+
DiagnosticSpec(locationMarker: "2️⃣", message: "name can only start with a letter or underscore, not a number"),
196202
]
197203
)
198204
}
@@ -205,11 +211,8 @@ final class NumberIdentifierErrorsTests: XCTestCase {
205211
}
206212
""",
207213
diagnostics: [
208-
// TODO: Old parser expected error on line 1: class name can only start with a letter or underscore, not a number
209-
DiagnosticSpec(locationMarker: "1️⃣", message: "expected name and member block in class"),
210-
// TODO: Old parser expected error on line 2: function name can only start with a letter or underscore, not a number
211-
DiagnosticSpec(locationMarker: "2️⃣", message: "expected name in function"),
212-
DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected code '22.0' before parameter clause"),
214+
DiagnosticSpec(locationMarker: "1️⃣", message: "name can only start with a letter or underscore, not a number"),
215+
DiagnosticSpec(locationMarker: "2️⃣", message: "name can only start with a letter or underscore, not a number"),
213216
]
214217
)
215218
}
@@ -223,12 +226,8 @@ final class NumberIdentifierErrorsTests: XCTestCase {
223226
}
224227
""",
225228
diagnostics: [
226-
// TODO: Old parser expected error on line 1: class name can only start with a letter or underscore, not a number
227-
// TODO: Old parser expected error on line 1: 'c' is not a valid digit in integer literal
228-
DiagnosticSpec(locationMarker: "1️⃣", message: "identifier can only start with a letter or underscore, not a number"),
229-
// TODO: Old parser expected error on line 2: function name can only start with a letter or underscore, not a number
230-
// TODO: Old parser expected error on line 2: 'm' is not a valid digit in integer literal
231-
DiagnosticSpec(locationMarker: "2️⃣", message: "identifier can only start with a letter or underscore, not a number"),
229+
DiagnosticSpec(locationMarker: "1️⃣", message: "name can only start with a letter or underscore, not a number"),
230+
DiagnosticSpec(locationMarker: "2️⃣", message: "name can only start with a letter or underscore, not a number"),
232231
]
233232
)
234233
}

0 commit comments

Comments
 (0)