Skip to content

Commit 7778544

Browse files
committed
Produce meaningful diagnostic for "#elif" typo
1 parent 3c73cc8 commit 7778544

File tree

3 files changed

+133
-17
lines changed

3 files changed

+133
-17
lines changed

Sources/SwiftParser/Directives.swift

Lines changed: 62 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,27 @@
1313
@_spi(RawSyntax) import SwiftSyntax
1414

1515
extension Parser {
16-
private enum IfConfigContinuationClauseStartKeyword: TokenSpecSet {
16+
private enum PoundIfDirectiveKeywords: TokenSpecSet {
17+
case poundIfKeyword
1718
case poundElseifKeyword
1819
case poundElseKeyword
20+
case pound
1921

2022
var spec: TokenSpec {
2123
switch self {
24+
case .poundIfKeyword: return .poundIfKeyword
2225
case .poundElseifKeyword: return .poundElseifKeyword
2326
case .poundElseKeyword: return .poundElseKeyword
27+
case .pound: return TokenSpec(.pound, recoveryPrecedence: .openingPoundIf)
2428
}
2529
}
2630

2731
init?(lexeme: Lexer.Lexeme) {
2832
switch PrepareForKeywordMatch(lexeme) {
33+
case TokenSpec(.poundIfKeyword): self = .poundIfKeyword
2934
case TokenSpec(.poundElseifKeyword): self = .poundElseifKeyword
3035
case TokenSpec(.poundElseKeyword): self = .poundElseKeyword
36+
case TokenSpec(.pound): self = .pound
3137
default: return nil
3238
}
3339
}
@@ -103,32 +109,72 @@ extension Parser {
103109
do {
104110
var firstIteration = true
105111
var loopProgress = LoopProgressCondition()
106-
while let poundIfHandle = firstIteration ? self.canRecoverTo(.poundIfKeyword) : self.canRecoverTo(anyIn: IfConfigContinuationClauseStartKeyword.self)?.handle,
107-
loopProgress.evaluate(self.currentToken)
108-
{
109-
var (unexpectedBeforePoundIf, poundIf) = self.eat(poundIfHandle)
110-
firstIteration = false
111-
// Parse the condition.
112+
LOOP: while let (match, handle) = self.canRecoverTo(anyIn: PoundIfDirectiveKeywords.self), loopProgress.evaluate(self.currentToken) {
113+
var unexpectedBeforePoundKeyword: RawUnexpectedNodesSyntax?
114+
var poundKeyword: RawTokenSyntax
112115
let condition: RawExprSyntax?
113-
switch poundIf.tokenKind {
114-
case .poundIfKeyword, .poundElseifKeyword:
116+
var atElifTypo: Bool {
117+
guard self.at(TokenSpec(.pound)), self.currentToken.trailingTriviaText.isEmpty else {
118+
return false
119+
}
120+
var lookahead = self.lookahead()
121+
lookahead.consumeAnyToken() // consume `#`
122+
guard lookahead.at(TokenSpec(.identifier, allowAtStartOfLine: false)), lookahead.currentToken.tokenText == "elif", lookahead.currentToken.leadingTriviaText.isEmpty else {
123+
return false // `#` and `elif` must not be separated by trivia
124+
}
125+
lookahead.consumeAnyToken() // consume `elif`
126+
// We are only at a `elif` typo if it’s followed by an identifier for the condition.
127+
// `#elif` or `#elif(…)` could be macro invocations.
128+
return lookahead.at(TokenSpec(.identifier, allowAtStartOfLine: false))
129+
}
130+
131+
switch match {
132+
case .poundIfKeyword:
133+
if !firstIteration {
134+
break LOOP
135+
}
136+
firstIteration = false
137+
(unexpectedBeforePoundKeyword, poundKeyword) = self.eat(handle)
138+
condition = RawExprSyntax(self.parseSequenceExpression(.basic, forDirective: true))
139+
case .poundElseifKeyword:
140+
if firstIteration {
141+
break LOOP
142+
}
143+
(unexpectedBeforePoundKeyword, poundKeyword) = self.eat(handle)
115144
condition = RawExprSyntax(self.parseSequenceExpression(.basic, forDirective: true))
116145
case .poundElseKeyword:
146+
if firstIteration {
147+
break LOOP
148+
}
149+
(unexpectedBeforePoundKeyword, poundKeyword) = self.eat(handle)
117150
if let ifToken = self.consume(if: .init(.if, allowAtStartOfLine: false)) {
118-
unexpectedBeforePoundIf = RawUnexpectedNodesSyntax(combining: unexpectedBeforePoundIf, poundIf, ifToken, arena: self.arena)
119-
poundIf = self.missingToken(.poundElseifKeyword)
151+
unexpectedBeforePoundKeyword = RawUnexpectedNodesSyntax(combining: unexpectedBeforePoundKeyword, poundKeyword, ifToken, arena: self.arena)
152+
poundKeyword = self.missingToken(.poundElseifKeyword)
120153
condition = RawExprSyntax(self.parseSequenceExpression(.basic, forDirective: true))
121154
} else {
122155
condition = nil
123156
}
124-
default:
125-
preconditionFailure("The loop condition should guarantee that we are at one of these tokens")
157+
case .pound:
158+
if firstIteration {
159+
break LOOP
160+
}
161+
if atElifTypo {
162+
(unexpectedBeforePoundKeyword, poundKeyword) = self.eat(handle)
163+
guard let elif = self.consume(if: TokenSpec(.identifier, allowAtStartOfLine: false)) else {
164+
preconditionFailure("The current token should be an identifier, guaranteed by the `atElifTypo` check.")
165+
}
166+
unexpectedBeforePoundKeyword = RawUnexpectedNodesSyntax(combining: unexpectedBeforePoundKeyword, poundKeyword, elif, arena: self.arena)
167+
poundKeyword = self.missingToken(.poundElseifKeyword)
168+
condition = RawExprSyntax(self.parseSequenceExpression(.basic, forDirective: true))
169+
} else {
170+
break LOOP
171+
}
126172
}
127173

128174
var elements = [Element]()
129175
do {
130176
var elementsProgress = LoopProgressCondition()
131-
while !self.at(.eof) && !self.at(.poundElseKeyword, .poundElseifKeyword, .poundEndifKeyword) && elementsProgress.evaluate(currentToken) {
177+
while !self.at(.eof) && !self.at(.poundElseKeyword, .poundElseifKeyword, .poundEndifKeyword) && !atElifTypo && elementsProgress.evaluate(currentToken) {
132178
let newItemAtStartOfLine = self.currentToken.isAtStartOfLine
133179
guard let element = parseElement(&self, elements.isEmpty), !element.isEmpty else {
134180
break
@@ -142,8 +188,8 @@ extension Parser {
142188

143189
clauses.append(
144190
RawIfConfigClauseSyntax(
145-
unexpectedBeforePoundIf,
146-
poundKeyword: poundIf,
191+
unexpectedBeforePoundKeyword,
192+
poundKeyword: poundKeyword,
147193
condition: condition,
148194
elements: syntax(&self, elements),
149195
arena: self.arena

Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -839,10 +839,18 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
839839
unexpectedBeforePoundKeyword
840840
.suffix(2)
841841
.compactMap { $0.as(TokenSyntax.self) }
842+
var diagnosticMessage: DiagnosticMessage?
843+
842844
if unexpectedTokens.map(\.tokenKind) == [.poundElseKeyword, .keyword(.if)] {
845+
diagnosticMessage = StaticParserError.unexpectedPoundElseSpaceIf
846+
} else if unexpectedTokens.first?.tokenKind == .pound, unexpectedTokens.last?.text == "elif" {
847+
diagnosticMessage = UnknownDirectiveError(unexpected: unexpectedBeforePoundKeyword)
848+
}
849+
850+
if let diagnosticMessage = diagnosticMessage {
843851
addDiagnostic(
844852
unexpectedBeforePoundKeyword,
845-
StaticParserError.unexpectedPoundElseSpaceIf,
853+
diagnosticMessage,
846854
fixIts: [
847855
FixIt(
848856
message: ReplaceTokensFixIt(replaceTokens: unexpectedTokens, replacements: [clause.poundKeyword]),

Tests/SwiftParserTest/translated/IfconfigExprTests.swift

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,68 @@ final class IfconfigExprTests: XCTestCase {
465465
)
466466
}
467467

468+
func testIfConfigExpr32() {
469+
assertParse(
470+
"""
471+
#if arch(x86_64)
472+
debugPrint("x86_64")
473+
1️⃣#elif arch(arm64)
474+
debugPrint("arm64")
475+
#else
476+
debugPrint("Some other architecture.")
477+
#endif
478+
""",
479+
diagnostics: [
480+
DiagnosticSpec(
481+
message: "use of unknown directive '#elif'",
482+
fixIts: ["replace '#elif' with '#elseif'"]
483+
)
484+
],
485+
fixedSource: """
486+
#if arch(x86_64)
487+
debugPrint("x86_64")
488+
#elseif arch(arm64)
489+
debugPrint("arm64")
490+
#else
491+
debugPrint("Some other architecture.")
492+
#endif
493+
"""
494+
)
495+
}
496+
497+
func testIfConfigExpr33() {
498+
assertParse(
499+
"""
500+
#if arch(x86_64)
501+
#line
502+
#endif
503+
"""
504+
)
505+
}
506+
507+
// FIXME: Parsing should generate diagnostics - https://github.com/apple/swift-syntax/issues/1395
508+
func testIfConfigExpr34() {
509+
assertParse(
510+
"""
511+
#if MY_FLAG
512+
#
513+
elif
514+
#endif
515+
"""
516+
)
517+
}
518+
519+
// FIXME: Parsing should generate diagnostics - https://github.com/apple/swift-syntax/issues/1395
520+
func testIfConfigExpr35() {
521+
assertParse(
522+
"""
523+
#if MY_FLAG
524+
# elif
525+
#endif
526+
"""
527+
)
528+
}
529+
468530
func testUnknownPlatform1() {
469531
assertParse(
470532
"""

0 commit comments

Comments
 (0)