Skip to content

Commit a0b5580

Browse files
committed
Improve recovery of dollar identifiers
1 parent 587d3ad commit a0b5580

File tree

10 files changed

+115
-79
lines changed

10 files changed

+115
-79
lines changed

Sources/SwiftParser/Declarations.swift

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1402,7 +1402,7 @@ extension Parser {
14021402
var elements = [RawFunctionParameterSyntax]()
14031403
// If we are missing the left parenthesis and the next token doesn't appear
14041404
// to be an argument label, don't parse any parameters.
1405-
let shouldSkipParameterParsing = lparen.isMissing && (!currentToken.canBeArgumentLabel || currentToken.isKeyword)
1405+
let shouldSkipParameterParsing = lparen.isMissing && (!currentToken.canBeArgumentLabel(allowDollarIdentifier: true) || currentToken.isKeyword)
14061406
if !shouldSkipParameterParsing {
14071407
var keepGoing = true
14081408
var loopProgress = LoopProgressCondition()
@@ -1426,22 +1426,26 @@ extension Parser {
14261426
misplacedSpecifiers.append(specifier)
14271427
}
14281428

1429+
let unexpectedBeforeFirstName: RawUnexpectedNodesSyntax?
14291430
let firstName: RawTokenSyntax?
1431+
let unexpectedBeforeSecondName: RawUnexpectedNodesSyntax?
14301432
let secondName: RawTokenSyntax?
14311433
let unexpectedBeforeColon: RawUnexpectedNodesSyntax?
14321434
let colon: RawTokenSyntax?
14331435
let shouldParseType: Bool
14341436

14351437
if self.withLookahead({ $0.startsParameterName(isClosure: subject.isClosure, allowMisplacedSpecifierRecovery: false) }) {
1436-
if self.currentToken.canBeArgumentLabel {
1437-
firstName = self.parseArgumentLabel()
1438+
if self.currentToken.canBeArgumentLabel(allowDollarIdentifier: true) {
1439+
(unexpectedBeforeFirstName, firstName) = self.parseArgumentLabel()
14381440
} else {
1441+
unexpectedBeforeFirstName = nil
14391442
firstName = nil
14401443
}
14411444

1442-
if self.currentToken.canBeArgumentLabel {
1443-
secondName = self.parseArgumentLabel()
1445+
if self.currentToken.canBeArgumentLabel(allowDollarIdentifier: true) {
1446+
(unexpectedBeforeSecondName, secondName) = self.parseArgumentLabel()
14441447
} else {
1448+
unexpectedBeforeSecondName = nil
14451449
secondName = nil
14461450
}
14471451
if subject.isClosure {
@@ -1453,7 +1457,9 @@ extension Parser {
14531457
shouldParseType = true
14541458
}
14551459
} else {
1460+
unexpectedBeforeFirstName = nil
14561461
firstName = nil
1462+
unexpectedBeforeSecondName = nil
14571463
secondName = nil
14581464
unexpectedBeforeColon = nil
14591465
colon = nil
@@ -1486,8 +1492,9 @@ extension Parser {
14861492
elements.append(RawFunctionParameterSyntax(
14871493
attributes: attrs,
14881494
modifiers: modifiers,
1489-
RawUnexpectedNodesSyntax(misplacedSpecifiers, arena: self.arena),
1495+
RawUnexpectedNodesSyntax(misplacedSpecifiers.map(RawSyntax.init) + (unexpectedBeforeFirstName?.elements ?? []), arena: self.arena),
14901496
firstName: firstName,
1497+
unexpectedBeforeSecondName,
14911498
secondName: secondName,
14921499
unexpectedBeforeColon,
14931500
colon: colon,

Sources/SwiftParser/Diagnostics/ParseDiagnosticsGenerator.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,8 +212,8 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
212212
let invalidIdentifier = node.previousToken(viewMode: .all),
213213
let previousParent = invalidIdentifier.parent?.as(UnexpectedNodesSyntax.self) {
214214
let fixIts: [FixIt]
215-
if invalidIdentifier.tokenKind.isKeyword {
216-
fixIts = [FixIt(message: .wrapKeywordInBackticks, changes: [
215+
if invalidIdentifier.tokenKind.isKeyword || invalidIdentifier.tokenKind.isDollarIdentifier {
216+
fixIts = [FixIt(message: .wrapInBackticks, changes: [
217217
.replace(
218218
oldNode: Syntax(invalidIdentifier),
219219
newNode: Syntax(TokenSyntax.identifier("`\(invalidIdentifier.text)`", leadingTrivia: invalidIdentifier.leadingTrivia, trailingTrivia: invalidIdentifier.trailingTrivia))

Sources/SwiftParser/Diagnostics/ParserDiagnosticMessages.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ extension FixItMessage where Self == StaticParserFixIt {
319319
public static var removeOperatorBody: Self {
320320
.init("remove operator body")
321321
}
322-
public static var wrapKeywordInBackticks: Self {
322+
public static var wrapInBackticks: Self {
323323
.init("if this name is unavoidable, use backticks to escape it")
324324
}
325325
}

Sources/SwiftParser/Diagnostics/SyntaxExtensions.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,4 +133,13 @@ extension TokenKind {
133133
return false
134134
}
135135
}
136+
137+
var isDollarIdentifier: Bool {
138+
switch self {
139+
case .dollarIdentifier:
140+
return true
141+
default:
142+
return false
143+
}
144+
}
136145
}

Sources/SwiftParser/Expressions.swift

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2185,8 +2185,21 @@ extension Parser {
21852185
var keepGoing: RawTokenSyntax? = nil
21862186
var loopProgress = LoopProgressCondition()
21872187
repeat {
2188-
let labelAndColon = self.consume(if: { $0.canBeArgumentLabel }, followedBy: { $0.tokenKind == .colon })
2189-
let (label, colon) = (labelAndColon?.0, labelAndColon?.1)
2188+
let unexpectedBeforeLabel: RawUnexpectedNodesSyntax?
2189+
let label: RawTokenSyntax?
2190+
let colon: RawTokenSyntax?
2191+
if let labelAndColon = self.consume(if: { $0.canBeArgumentLabel() }, followedBy: { $0.tokenKind == .colon }) {
2192+
unexpectedBeforeLabel = nil
2193+
(label, colon) = labelAndColon
2194+
} else if let dollarLabelAndColon = self.consume(if: .dollarIdentifier, followedBy: .colon) {
2195+
unexpectedBeforeLabel = RawUnexpectedNodesSyntax([dollarLabelAndColon.0], arena: self.arena)
2196+
label = missingToken(.identifier)
2197+
colon = dollarLabelAndColon.1
2198+
} else {
2199+
unexpectedBeforeLabel = nil
2200+
label = nil
2201+
colon = nil
2202+
}
21902203

21912204
// See if we have an operator decl ref '(<op>)'. The operator token in
21922205
// this case lexes as a binary operator because it neither leads nor
@@ -2202,6 +2215,7 @@ extension Parser {
22022215
}
22032216
keepGoing = self.consume(if: .comma)
22042217
result.append(RawTupleExprElementSyntax(
2218+
unexpectedBeforeLabel,
22052219
label: label,
22062220
colon: colon,
22072221
expression: expr,
@@ -2231,10 +2245,11 @@ extension Parser {
22312245
var elements = [RawMultipleTrailingClosureElementSyntax]()
22322246
var loopProgress = LoopProgressCondition()
22332247
while self.lookahead().isStartOfLabelledTrailingClosure() && loopProgress.evaluate(currentToken) {
2234-
let label = self.parseArgumentLabel()
2248+
let (unexpectedBeforeLabel, label) = self.parseArgumentLabel()
22352249
let (unexpectedBeforeColon, colon) = self.expect(.colon)
22362250
let closure = self.parseClosureExpression()
22372251
elements.append(RawMultipleTrailingClosureElementSyntax(
2252+
unexpectedBeforeLabel,
22382253
label: label,
22392254
unexpectedBeforeColon,
22402255
colon: colon,
@@ -2253,7 +2268,7 @@ extension Parser.Lookahead {
22532268
// Fast path: the next two tokens must be a label and a colon.
22542269
// But 'default:' is ambiguous with switch cases and we disallow it
22552270
// (unless escaped) even outside of switches.
2256-
if !self.currentToken.canBeArgumentLabel
2271+
if !self.currentToken.canBeArgumentLabel()
22572272
|| self.at(.defaultKeyword)
22582273
|| self.peek().tokenKind != .colon {
22592274
return false

Sources/SwiftParser/Names.swift

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,16 @@ extension Parser {
2323
}
2424
}
2525

26-
mutating func parseArgumentLabel() -> RawTokenSyntax {
27-
assert(self.currentToken.canBeArgumentLabel)
28-
return self.consumeAnyToken()
26+
mutating func parseArgumentLabel() -> (RawUnexpectedNodesSyntax?, RawTokenSyntax) {
27+
assert(self.currentToken.canBeArgumentLabel(allowDollarIdentifier: true))
28+
if let dollarIdent = self.consume(if: .dollarIdentifier) {
29+
return (
30+
RawUnexpectedNodesSyntax(elements: [RawSyntax(dollarIdent)], arena: self.arena),
31+
self.missingToken(.identifier, text: nil)
32+
)
33+
} else {
34+
return (nil, self.consumeAnyToken())
35+
}
2936
}
3037
}
3138

@@ -86,7 +93,7 @@ extension Parser {
8693
// A close parenthesis, if empty lists are allowed.
8794
let nextIsRParen = flags.contains(.zeroArgCompoundNames) && next.tokenKind == .rightParen
8895
// An argument label.
89-
let nextIsArgLabel = next.canBeArgumentLabel || next.tokenKind == .colon
96+
let nextIsArgLabel = next.canBeArgumentLabel() || next.tokenKind == .colon
9097

9198
guard nextIsRParen || nextIsArgLabel else {
9299
return nil
@@ -105,7 +112,7 @@ extension Parser {
105112
var loopProgress = LoopProgressCondition()
106113
while !self.at(any: [.eof, .rightParen]) && loopProgress.evaluate(currentToken) {
107114
// Check to see if there is an argument label.
108-
assert(self.currentToken.canBeArgumentLabel && self.peek().tokenKind == .colon)
115+
assert(self.currentToken.canBeArgumentLabel() && self.peek().tokenKind == .colon)
109116
let name = self.consumeAnyToken()
110117
let (unexpectedBeforeColon, colon) = self.expect(.colon)
111118
elements.append(RawDeclNameArgumentSyntax(
@@ -217,7 +224,7 @@ extension Parser.Lookahead {
217224
var loopProgress = LoopProgressCondition()
218225
while !lookahead.at(any: [.eof, .rightParen]) && loopProgress.evaluate(lookahead.currentToken) {
219226
// Check to see if there is an argument label.
220-
guard lookahead.currentToken.canBeArgumentLabel && lookahead.peek().tokenKind == .colon else {
227+
guard lookahead.currentToken.canBeArgumentLabel() && lookahead.peek().tokenKind == .colon else {
221228
return false
222229
}
223230

@@ -236,14 +243,16 @@ extension Parser.Lookahead {
236243
}
237244

238245
extension Lexer.Lexeme {
239-
var canBeArgumentLabel: Bool {
246+
func canBeArgumentLabel(allowDollarIdentifier: Bool = false) -> Bool {
240247
if TypeSpecifier(lexeme: self) != nil {
241248
return false
242249
}
243250
switch self.tokenKind {
244251
case .identifier, .wildcardKeyword:
245252
// Identifiers, escaped identifiers, and '_' can be argument labels.
246253
return true
254+
case .dollarIdentifier:
255+
return allowDollarIdentifier
247256
default:
248257
// All other keywords can be argument labels.
249258
return self.isKeyword

Sources/SwiftParser/Parser.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,7 @@ extension Parser {
393393
self.missingToken(.identifier)
394394
)
395395
}
396-
if let number = self.consume(ifAny: [.integerLiteral, .floatingLiteral]) {
396+
if let number = self.consume(ifAny: [.integerLiteral, .floatingLiteral, .dollarIdentifier]) {
397397
return (
398398
RawUnexpectedNodesSyntax(elements: [RawSyntax(number)], arena: self.arena),
399399
self.missingToken(.identifier, text: nil)

Sources/SwiftParser/Patterns.swift

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ extension Parser {
5050
case leftParen
5151
case wildcardKeyword
5252
case identifier
53+
case dollarIdentifier // For recovery
5354
case letKeyword
5455
case varKeyword
5556

@@ -58,6 +59,7 @@ extension Parser {
5859
case .leftParen: self = .leftParen
5960
case .wildcardKeyword: self = .wildcardKeyword
6061
case .identifier: self = .identifier
62+
case .dollarIdentifier: self = .dollarIdentifier
6163
case .letKeyword: self = .letKeyword
6264
case .varKeyword: self = .varKeyword
6365
default: return nil
@@ -69,6 +71,7 @@ extension Parser {
6971
case .leftParen: return .leftParen
7072
case .wildcardKeyword: return .wildcardKeyword
7173
case .identifier: return .identifier
74+
case .dollarIdentifier: return .dollarIdentifier
7275
case .letKeyword: return .letKeyword
7376
case .varKeyword: return .varKeyword
7477
}
@@ -100,6 +103,14 @@ extension Parser {
100103
identifier: identifier,
101104
arena: self.arena
102105
))
106+
case (.dollarIdentifier, let handle)?:
107+
let dollarIdent = self.eat(handle)
108+
let unexpectedBeforeIdentifier = RawUnexpectedNodesSyntax(elements: [RawSyntax(dollarIdent)], arena: self.arena)
109+
return RawPatternSyntax(RawIdentifierPatternSyntax(
110+
unexpectedBeforeIdentifier,
111+
identifier: missingToken(.identifier),
112+
arena: self.arena
113+
))
103114
case (.letKeyword, let handle)?,
104115
(.varKeyword, let handle)?:
105116
let letOrVar = self.eat(handle)
@@ -324,7 +335,7 @@ extension Parser.Lookahead {
324335
}
325336

326337
// To have a parameter name here, we need a name.
327-
guard self.currentToken.canBeArgumentLabel else {
338+
guard self.currentToken.canBeArgumentLabel(allowDollarIdentifier: true) else {
328339
return false
329340
}
330341

@@ -335,7 +346,7 @@ extension Parser.Lookahead {
335346
}
336347

337348
// If the next token can be an argument label, we might have a name.
338-
if nextTok.canBeArgumentLabel {
349+
if nextTok.canBeArgumentLabel(allowDollarIdentifier: true) {
339350
// If the first name wasn't "isolated", we're done.
340351
if !self.atContextualKeyword("isolated") &&
341352
!self.atContextualKeyword("some") &&
@@ -351,7 +362,7 @@ extension Parser.Lookahead {
351362
return true // isolated :
352363
}
353364
self.consumeAnyToken()
354-
return self.currentToken.canBeArgumentLabel && self.peek().tokenKind == .colon
365+
return self.currentToken.canBeArgumentLabel(allowDollarIdentifier: true) && self.peek().tokenKind == .colon
355366
}
356367
}
357368

Sources/SwiftParser/Types.swift

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,9 @@ extension Parser {
398398
var keepGoing = true
399399
var loopProgress = LoopProgressCondition()
400400
while !self.at(any: [.eof, .rightParen]) && keepGoing && loopProgress.evaluate(currentToken) {
401+
let unexpectedBeforeFirst: RawUnexpectedNodesSyntax?
401402
let first: RawTokenSyntax?
403+
let unexpectedBeforeSecond: RawUnexpectedNodesSyntax?
402404
let second: RawTokenSyntax?
403405
let unexpectedBeforeColon: RawUnexpectedNodesSyntax?
404406
let colon: RawTokenSyntax?
@@ -407,21 +409,25 @@ extension Parser {
407409
while let specifier = self.consume(ifAnyIn: TypeSpecifier.self) {
408410
misplacedSpecifiers.append(specifier)
409411
}
410-
first = self.parseArgumentLabel()
412+
(unexpectedBeforeFirst, first) = self.parseArgumentLabel()
411413
if let parsedColon = self.consume(if: .colon) {
414+
unexpectedBeforeSecond = nil
412415
second = nil
413416
unexpectedBeforeColon = nil
414417
colon = parsedColon
415-
} else if self.currentToken.canBeArgumentLabel && self.peek().tokenKind == .colon {
416-
second = self.parseArgumentLabel()
418+
} else if self.currentToken.canBeArgumentLabel(allowDollarIdentifier: true) && self.peek().tokenKind == .colon {
419+
(unexpectedBeforeSecond, second) = self.parseArgumentLabel()
417420
(unexpectedBeforeColon, colon) = self.expect(.colon)
418421
} else {
422+
unexpectedBeforeSecond = nil
419423
second = nil
420424
unexpectedBeforeColon = nil
421425
colon = RawTokenSyntax(missing: .colon, arena: self.arena)
422426
}
423427
} else {
428+
unexpectedBeforeFirst = nil
424429
first = nil
430+
unexpectedBeforeSecond = nil
425431
second = nil
426432
unexpectedBeforeColon = nil
427433
colon = nil
@@ -459,8 +465,9 @@ extension Parser {
459465
keepGoing = trailingComma != nil
460466
elements.append(RawTupleTypeElementSyntax(
461467
inOut: nil,
462-
RawUnexpectedNodesSyntax(misplacedSpecifiers, arena: self.arena),
468+
RawUnexpectedNodesSyntax(misplacedSpecifiers.map(RawSyntax.init) + (unexpectedBeforeFirst?.elements ?? []), arena: self.arena),
463469
name: first,
470+
unexpectedBeforeSecond,
464471
secondName: second,
465472
unexpectedBeforeColon,
466473
colon: colon,
@@ -626,7 +633,7 @@ extension Parser.Lookahead {
626633
// by a type annotation.
627634
if self.startsParameterName(isClosure: false, allowMisplacedSpecifierRecovery: false) {
628635
self.consumeAnyToken()
629-
if self.currentToken.canBeArgumentLabel {
636+
if self.currentToken.canBeArgumentLabel() {
630637
self.consumeAnyToken()
631638
guard self.at(.colon) else {
632639
return false

0 commit comments

Comments
 (0)