Skip to content

Commit e41b861

Browse files
committed
Produce meaningful diagnostic for "#elif" typo
1 parent 9a4c5e4 commit e41b861

File tree

3 files changed

+194
-45
lines changed

3 files changed

+194
-45
lines changed

Sources/SwiftParser/Directives.swift

Lines changed: 89 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,21 @@ extension Parser {
1616
private enum IfConfigContinuationClauseStartKeyword: TokenSpecSet {
1717
case poundElseifKeyword
1818
case poundElseKeyword
19+
case pound
1920

2021
var spec: TokenSpec {
2122
switch self {
2223
case .poundElseifKeyword: return .poundElseifKeyword
2324
case .poundElseKeyword: return .poundElseKeyword
25+
case .pound: return TokenSpec(.pound, recoveryPrecedence: .openingPoundIf)
2426
}
2527
}
2628

2729
init?(lexeme: Lexer.Lexeme) {
2830
switch PrepareForKeywordMatch(lexeme) {
2931
case TokenSpec(.poundElseifKeyword): self = .poundElseifKeyword
3032
case TokenSpec(.poundElseKeyword): self = .poundElseKeyword
33+
case TokenSpec(.pound): self = .pound
3134
default: return nil
3235
}
3336
}
@@ -100,56 +103,64 @@ extension Parser {
100103
}
101104

102105
var clauses = [RawIfConfigClauseSyntax]()
103-
do {
104-
var firstIteration = true
105-
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-
let condition: RawExprSyntax?
113-
switch poundIf.tokenKind {
114-
case .poundIfKeyword, .poundElseifKeyword:
106+
107+
// Parse #if
108+
let (unexpectedBeforePoundIfKeyword, poundIfKeyword) = self.expect(.poundIfKeyword)
109+
let condition = RawExprSyntax(self.parseSequenceExpression(.basic, forDirective: true))
110+
111+
clauses.append(
112+
RawIfConfigClauseSyntax(
113+
unexpectedBeforePoundIfKeyword,
114+
poundKeyword: poundIfKeyword,
115+
condition: condition,
116+
elements: syntax(&self, parseIfConfigClauseElements(parseElement, addSemicolonIfNeeded: addSemicolonIfNeeded)),
117+
arena: self.arena
118+
)
119+
)
120+
121+
// Proceed to parse #if continuation clauses (#elseif, #else, check #elif typo, #endif)
122+
var loopProgress = LoopProgressCondition()
123+
LOOP: while let (match, handle) = self.canRecoverTo(anyIn: IfConfigContinuationClauseStartKeyword.self), loopProgress.evaluate(self.currentToken) {
124+
var unexpectedBeforePoundKeyword: RawUnexpectedNodesSyntax?
125+
var poundKeyword: RawTokenSyntax
126+
let condition: RawExprSyntax?
127+
128+
switch match {
129+
case .poundElseifKeyword:
130+
(unexpectedBeforePoundKeyword, poundKeyword) = self.eat(handle)
131+
condition = RawExprSyntax(self.parseSequenceExpression(.basic, forDirective: true))
132+
case .poundElseKeyword:
133+
(unexpectedBeforePoundKeyword, poundKeyword) = self.eat(handle)
134+
if let ifToken = self.consume(if: .init(.if, allowAtStartOfLine: false)) {
135+
unexpectedBeforePoundKeyword = RawUnexpectedNodesSyntax(combining: unexpectedBeforePoundKeyword, poundKeyword, ifToken, arena: self.arena)
136+
poundKeyword = self.missingToken(.poundElseifKeyword)
115137
condition = RawExprSyntax(self.parseSequenceExpression(.basic, forDirective: true))
116-
case .poundElseKeyword:
117-
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)
120-
condition = RawExprSyntax(self.parseSequenceExpression(.basic, forDirective: true))
121-
} else {
122-
condition = nil
123-
}
124-
default:
125-
preconditionFailure("The loop condition should guarantee that we are at one of these tokens")
138+
} else {
139+
condition = nil
126140
}
127-
128-
var elements = [Element]()
129-
do {
130-
var elementsProgress = LoopProgressCondition()
131-
while !self.at(.eof) && !self.at(.poundElseKeyword, .poundElseifKeyword, .poundEndifKeyword) && elementsProgress.evaluate(currentToken) {
132-
let newItemAtStartOfLine = self.currentToken.isAtStartOfLine
133-
guard let element = parseElement(&self, elements.isEmpty), !element.isEmpty else {
134-
break
135-
}
136-
if let lastElement = elements.last, let fixedUpLastItem = addSemicolonIfNeeded(lastElement, newItemAtStartOfLine, &self) {
137-
elements[elements.count - 1] = fixedUpLastItem
138-
}
139-
elements.append(element)
141+
case .pound:
142+
if self.atElifTypo() {
143+
(unexpectedBeforePoundKeyword, poundKeyword) = self.eat(handle)
144+
guard let elif = self.consume(if: TokenSpec(.identifier, allowAtStartOfLine: false)) else {
145+
preconditionFailure("The current token should be an identifier, guaranteed by the `atElifTypo` check.")
140146
}
147+
unexpectedBeforePoundKeyword = RawUnexpectedNodesSyntax(combining: unexpectedBeforePoundKeyword, poundKeyword, elif, arena: self.arena)
148+
poundKeyword = self.missingToken(.poundElseifKeyword)
149+
condition = RawExprSyntax(self.parseSequenceExpression(.basic, forDirective: true))
150+
} else {
151+
break LOOP
141152
}
153+
}
142154

143-
clauses.append(
144-
RawIfConfigClauseSyntax(
145-
unexpectedBeforePoundIf,
146-
poundKeyword: poundIf,
147-
condition: condition,
148-
elements: syntax(&self, elements),
149-
arena: self.arena
150-
)
155+
clauses.append(
156+
RawIfConfigClauseSyntax(
157+
unexpectedBeforePoundKeyword,
158+
poundKeyword: poundKeyword,
159+
condition: condition,
160+
elements: syntax(&self, parseIfConfigClauseElements(parseElement, addSemicolonIfNeeded: addSemicolonIfNeeded)),
161+
arena: self.arena
151162
)
152-
}
163+
)
153164
}
154165

155166
let (unexpectedBeforePoundEndIf, poundEndIf) = self.expect(.poundEndifKeyword)
@@ -160,6 +171,40 @@ extension Parser {
160171
arena: self.arena
161172
)
162173
}
174+
175+
private mutating func atElifTypo() -> Bool {
176+
guard self.at(TokenSpec(.pound)), self.currentToken.trailingTriviaText.isEmpty else {
177+
return false
178+
}
179+
var lookahead = self.lookahead()
180+
lookahead.consumeAnyToken() // consume `#`
181+
guard lookahead.at(TokenSpec(.identifier, allowAtStartOfLine: false)), lookahead.currentToken.tokenText == "elif", lookahead.currentToken.leadingTriviaText.isEmpty else {
182+
return false // `#` and `elif` must not be separated by trivia
183+
}
184+
lookahead.consumeAnyToken() // consume `elif`
185+
// We are only at a `elif` typo if it’s followed by an identifier for the condition.
186+
// `#elif` or `#elif(…)` could be macro invocations.
187+
return lookahead.at(TokenSpec(.identifier, allowAtStartOfLine: false))
188+
}
189+
190+
private mutating func parseIfConfigClauseElements<Element: RawSyntaxNodeProtocol>(
191+
_ parseElement: (_ parser: inout Parser, _ isFirstElement: Bool) -> Element?,
192+
addSemicolonIfNeeded: (_ lastElement: Element, _ newItemAtStartOfLine: Bool, _ parser: inout Parser) -> Element?
193+
) -> [Element] {
194+
var elements = [Element]()
195+
var elementsProgress = LoopProgressCondition()
196+
while !self.at(.eof) && !self.at(.poundElseKeyword, .poundElseifKeyword, .poundEndifKeyword) && !self.atElifTypo() && elementsProgress.evaluate(currentToken) {
197+
let newItemAtStartOfLine = self.currentToken.isAtStartOfLine
198+
guard let element = parseElement(&self, elements.isEmpty), !element.isEmpty else {
199+
break
200+
}
201+
if let lastElement = elements.last, let fixedUpLastItem = addSemicolonIfNeeded(lastElement, newItemAtStartOfLine, &self) {
202+
elements[elements.count - 1] = fixedUpLastItem
203+
}
204+
elements.append(element)
205+
}
206+
return elements
207+
}
163208
}
164209

165210
extension Parser {

Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -865,10 +865,18 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
865865
unexpectedBeforePoundKeyword
866866
.suffix(2)
867867
.compactMap { $0.as(TokenSyntax.self) }
868+
var diagnosticMessage: DiagnosticMessage?
869+
868870
if unexpectedTokens.map(\.tokenKind) == [.poundElseKeyword, .keyword(.if)] {
871+
diagnosticMessage = StaticParserError.unexpectedPoundElseSpaceIf
872+
} else if unexpectedTokens.first?.tokenKind == .pound, unexpectedTokens.last?.text == "elif" {
873+
diagnosticMessage = UnknownDirectiveError(unexpected: unexpectedBeforePoundKeyword)
874+
}
875+
876+
if let diagnosticMessage = diagnosticMessage {
869877
addDiagnostic(
870878
unexpectedBeforePoundKeyword,
871-
StaticParserError.unexpectedPoundElseSpaceIf,
879+
diagnosticMessage,
872880
fixIts: [
873881
FixIt(
874882
message: ReplaceTokensFixIt(replaceTokens: unexpectedTokens, replacements: [clause.poundKeyword]),

Tests/SwiftParserTest/translated/IfconfigExprTests.swift

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,102 @@ 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+
530+
func testIfConfigExpr36() {
531+
assertParse(
532+
"""
533+
switch x ℹ️{1️⃣
534+
2️⃣#else()
535+
#if true
536+
bar()
537+
#endif
538+
case .A, .B:
539+
break
540+
}
541+
""",
542+
diagnostics: [
543+
DiagnosticSpec(
544+
message: "expected '}' to end 'switch' statement",
545+
notes: [NoteSpec(message: "to match this opening '{'")],
546+
fixIts: ["insert '}'"]
547+
),
548+
DiagnosticSpec(locationMarker: "2️⃣", message: "extraneous code at top level"),
549+
],
550+
fixedSource: """
551+
switch x {
552+
}
553+
#else()
554+
#if true
555+
bar()
556+
#endif
557+
case .A, .B:
558+
break
559+
}
560+
"""
561+
)
562+
}
563+
468564
func testUnknownPlatform1() {
469565
assertParse(
470566
"""

0 commit comments

Comments
 (0)