Skip to content

Commit 9591054

Browse files
committed
Add diagnostic for missing condition binding
1 parent 7e099ed commit 9591054

File tree

4 files changed

+57
-8
lines changed

4 files changed

+57
-8
lines changed

Sources/SwiftParser/Statements.swift

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ extension Parser {
186186
var keepGoing: RawTokenSyntax? = nil
187187
var loopProgress = LoopProgressCondition()
188188
repeat {
189-
let condition = self.parseConditionElement()
189+
let condition = self.parseConditionElement(lastBindingKind: elements.last?.condition.as(RawOptionalBindingConditionSyntax.self)?.bindingKeyword)
190190
let unexpectedBeforeKeepGoing: RawUnexpectedNodesSyntax?
191191
keepGoing = self.consume(if: .comma)
192192
if keepGoing == nil, let andOperator = self.consumeIfContextualPunctuator("&&") {
@@ -219,15 +219,15 @@ extension Parser {
219219
/// optional-binding-condition → 'let' pattern initializer? | 'var' pattern initializer? |
220220
/// 'inout' pattern initializer?
221221
@_spi(RawSyntax)
222-
public mutating func parseConditionElement() -> RawConditionElementSyntax.Condition {
222+
public mutating func parseConditionElement(lastBindingKind: RawTokenSyntax?) -> RawConditionElementSyntax.Condition {
223223
// Parse a leading #available/#unavailable condition if present.
224224
if self.at(.poundAvailableKeyword, .poundUnavailableKeyword) {
225225
return self.parsePoundAvailableConditionElement()
226226
}
227227

228228
// Parse the basic expression case. If we have a leading let, var, inout,
229229
// borrow, case keyword or an assignment, then we know this is a binding.
230-
guard self.at(.keyword(.let), .keyword(.var), .keyword(.case)) || self.at(.keyword(.inout)) else {
230+
guard self.at(.keyword(.let), .keyword(.var), .keyword(.case)) || self.at(.keyword(.inout)) || (lastBindingKind != nil && self.peek().rawTokenKind == .equal) else {
231231
// If we lack it, then this is theoretically a boolean condition.
232232
// However, we also need to handle migrating from Swift 2 syntax, in
233233
// which a comma followed by an expression could actually be a pattern
@@ -244,7 +244,6 @@ extension Parser {
244244
}
245245

246246
// We're parsing a conditional binding.
247-
precondition(self.at(.keyword(.let), .keyword(.var)) || self.at(.keyword(.inout), .keyword(.case)))
248247
enum BindingKind {
249248
case pattern(RawTokenSyntax, RawPatternSyntax)
250249
case optional(RawTokenSyntax, RawPatternSyntax)
@@ -255,7 +254,14 @@ extension Parser {
255254
let pattern = self.parseMatchingPattern(context: .matching)
256255
kind = .pattern(caseKeyword, pattern)
257256
} else {
258-
let letOrVar = self.consumeAnyToken()
257+
let letOrVar: RawTokenSyntax
258+
259+
if self.at(.identifier) {
260+
letOrVar = RawTokenSyntax(missing: lastBindingKind!.tokenKind, text: lastBindingKind!.tokenText, arena: self.arena)
261+
} else {
262+
letOrVar = self.consumeAnyToken()
263+
}
264+
259265
let pattern = self.parseMatchingPattern(context: .bindingIntroducer)
260266
kind = .optional(letOrVar, pattern)
261267
}

Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -913,6 +913,28 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
913913
return .visitChildren
914914
}
915915

916+
public override func visit(_ node: OptionalBindingConditionSyntax) -> SyntaxVisitorContinueKind {
917+
if shouldSkip(node) {
918+
return .skipChildren
919+
}
920+
921+
if node.bindingKeyword.presence == .missing {
922+
addDiagnostic(
923+
node.bindingKeyword,
924+
ExpectedBindingKeyword(token: node.bindingKeyword),
925+
fixIts: [
926+
FixIt(
927+
message: InsertFixIt(tokenToBeInserted: node.bindingKeyword),
928+
changes: [.makePresent(node.bindingKeyword)]
929+
)
930+
],
931+
handledNodes: [node.bindingKeyword.id]
932+
)
933+
}
934+
935+
return .visitChildren
936+
}
937+
916938
public override func visit(_ node: PrecedenceGroupAssignmentSyntax) -> SyntaxVisitorContinueKind {
917939
if shouldSkip(node) {
918940
return .skipChildren

Sources/SwiftParserDiagnostics/ParserDiagnosticMessages.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,14 @@ public struct EffectsSpecifierAfterArrow: ParserError {
260260
}
261261
}
262262

263+
public struct ExpectedBindingKeyword: ParserError {
264+
let token: TokenSyntax
265+
266+
public var message: String {
267+
return "expected '\(token.text)' in conditional"
268+
}
269+
}
270+
263271
public struct ExtaneousCodeAtTopLevel: ParserError {
264272
public let extraneousCode: UnexpectedNodesSyntax
265273

@@ -527,6 +535,14 @@ extension FixItMessage where Self == StaticParserFixIt {
527535
}
528536
}
529537

538+
public struct InsertFixIt: ParserFixIt {
539+
public let tokenToBeInserted: TokenSyntax
540+
541+
public var message: String {
542+
"insert '\(tokenToBeInserted.text)'"
543+
}
544+
}
545+
530546
public struct MoveTokensAfterFixIt: ParserFixIt {
531547
/// The token that should be moved
532548
public let movedTokens: [TokenSyntax]

Tests/SwiftParserTest/translated/RecoveryTests.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1839,13 +1839,18 @@ final class RecoveryTests: XCTestCase {
18391839
func testRecovery153() {
18401840
assertParse(
18411841
"""
1842-
if var y = x, z = x {
1842+
if var y = x, 1️⃣z = x {
18431843
z = y; y = z
18441844
}
18451845
""",
18461846
diagnostics: [
1847-
// TODO: Old parser expected error on line 1: expected 'var' in conditional, Fix-It replacements: 17 - 17 = 'var '
1848-
]
1847+
DiagnosticSpec(message: "expected 'var' in conditional", fixIts: ["insert 'var'"])
1848+
],
1849+
fixedSource: """
1850+
if var y = x, var z = x {
1851+
z = y; y = z
1852+
}
1853+
"""
18491854
)
18501855
}
18511856

0 commit comments

Comments
 (0)