Skip to content

Commit 6a3eefe

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

File tree

3 files changed

+123
-17
lines changed

3 files changed

+123
-17
lines changed

Sources/SwiftParser/Directives.swift

Lines changed: 52 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,62 @@ 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 unexpectedBeforePound: RawUnexpectedNodesSyntax?
114+
var pound: 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+
let identifierSpec = TokenSpec(.identifier, allowAtStartOfLine: false)
121+
var lookahead = self.lookahead()
122+
lookahead.consumeAnyToken()
123+
guard lookahead.at(identifierSpec), lookahead.currentToken.tokenText == "elif", lookahead.currentToken.leadingTriviaText.isEmpty else {
124+
return false
125+
}
126+
lookahead.consumeAnyToken()
127+
return lookahead.at(identifierSpec)
128+
}
129+
130+
switch match {
131+
case .poundIfKeyword:
132+
if !firstIteration {
133+
break LOOP
134+
}
135+
firstIteration = false
136+
(unexpectedBeforePound, pound) = self.eat(handle)
137+
condition = RawExprSyntax(self.parseSequenceExpression(.basic, forDirective: true))
138+
case .poundElseifKeyword:
139+
(unexpectedBeforePound, pound) = self.eat(handle)
115140
condition = RawExprSyntax(self.parseSequenceExpression(.basic, forDirective: true))
116141
case .poundElseKeyword:
142+
(unexpectedBeforePound, pound) = self.eat(handle)
117143
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)
144+
unexpectedBeforePound = RawUnexpectedNodesSyntax(combining: unexpectedBeforePound, pound, ifToken, arena: self.arena)
145+
pound = self.missingToken(.poundElseifKeyword)
120146
condition = RawExprSyntax(self.parseSequenceExpression(.basic, forDirective: true))
121147
} else {
122148
condition = nil
123149
}
124-
default:
125-
preconditionFailure("The loop condition should guarantee that we are at one of these tokens")
150+
case .pound:
151+
if atElifTypo {
152+
(unexpectedBeforePound, pound) = self.eat(handle)
153+
guard let identifier = self.consume(if: TokenSpec(.identifier, allowAtStartOfLine: false)) else {
154+
preconditionFailure("The current token should be an identifier, guaranteed by the `atElifTypo` check.")
155+
}
156+
unexpectedBeforePound = RawUnexpectedNodesSyntax(combining: unexpectedBeforePound, pound, identifier, arena: self.arena)
157+
pound = self.missingToken(.poundElseifKeyword)
158+
condition = RawExprSyntax(self.parseSequenceExpression(.basic, forDirective: true))
159+
} else {
160+
break LOOP
161+
}
126162
}
127163

128164
var elements = [Element]()
129165
do {
130166
var elementsProgress = LoopProgressCondition()
131-
while !self.at(.eof) && !self.at(.poundElseKeyword, .poundElseifKeyword, .poundEndifKeyword) && elementsProgress.evaluate(currentToken) {
167+
while !self.at(.eof) && !self.at(.poundElseKeyword, .poundElseifKeyword, .poundEndifKeyword) && !atElifTypo && elementsProgress.evaluate(currentToken) {
132168
let newItemAtStartOfLine = self.currentToken.isAtStartOfLine
133169
guard let element = parseElement(&self, elements.isEmpty), !element.isEmpty else {
134170
break
@@ -142,8 +178,8 @@ extension Parser {
142178

143179
clauses.append(
144180
RawIfConfigClauseSyntax(
145-
unexpectedBeforePoundIf,
146-
poundKeyword: poundIf,
181+
unexpectedBeforePound,
182+
poundKeyword: pound,
147183
condition: condition,
148184
elements: syntax(&self, elements),
149185
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)