Skip to content

Commit 198d526

Browse files
committed
Improve recovery in #if blocks
1 parent cb3425e commit 198d526

File tree

8 files changed

+78
-50
lines changed

8 files changed

+78
-50
lines changed

Sources/SwiftParser/Declarations.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ extension TokenConsumer {
4848
if allowRecovery {
4949
declStartKeyword = subparser.canRecoverTo(
5050
anyIn: DeclarationStart.self,
51-
recoveryPrecedence: isAtTopLevel ? nil : .strongBracketedClose
51+
recoveryPrecedence: isAtTopLevel ? nil : .closingBrace
5252
)?.0
5353
} else {
5454
declStartKeyword = subparser.at(anyIn: DeclarationStart.self)?.0

Sources/SwiftParser/Directives.swift

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,26 +62,24 @@ extension Parser {
6262
/// - syntax: A function that aggregates the parsed conditional elements
6363
/// into a syntax collection.
6464
@_spi(RawSyntax)
65-
public mutating func parsePoundIfDirective<Element>(
65+
public mutating func parsePoundIfDirective<Element: RawSyntaxNodeProtocol>(
6666
_ parseElement: (inout Parser) -> Element?,
6767
syntax: (inout Parser, [Element]) -> RawSyntax
6868
) -> RawIfConfigDeclSyntax {
6969
var clauses = [RawIfConfigClauseSyntax]()
7070
do {
7171
var firstIteration = true
7272
var loopProgress = LoopProgressCondition()
73-
while let poundIf = self.consume(ifAny: firstIteration ? [.poundIfKeyword] : [.poundIfKeyword, .poundElseifKeyword, .poundElseKeyword]),
73+
while let poundIfHandle = self.canRecoverTo(any: firstIteration ? [.poundIfKeyword] : [.poundIfKeyword, .poundElseifKeyword, .poundElseKeyword]),
7474
loopProgress.evaluate(self.currentToken) {
75+
let (unexpectedBeforePoundIf, poundIf) = self.eat(poundIfHandle)
7576
firstIteration = false
7677
// Parse the condition.
77-
let unexpectedBeforePoundIf: RawUnexpectedNodesSyntax?
7878
let condition: RawExprSyntax?
7979
switch poundIf.tokenKind {
8080
case .poundIfKeyword, .poundElseifKeyword:
81-
unexpectedBeforePoundIf = nil
8281
condition = RawExprSyntax(self.parseSequenceExpression(.basic, forDirective: true))
8382
case .poundElseKeyword:
84-
unexpectedBeforePoundIf = nil
8583
condition = nil
8684
default:
8785
preconditionFailure("The loop condition should guarantee that we are at one of these tokens")
@@ -90,9 +88,8 @@ extension Parser {
9088
var elements = [Element]()
9189
do {
9290
var elementsProgress = LoopProgressCondition()
93-
while !self.at(any: [.eof, .poundElseKeyword, .poundElseifKeyword, .poundEndifKeyword])
94-
&& elementsProgress.evaluate(currentToken) {
95-
guard let element = parseElement(&self) else {
91+
while !self.at(any: [.eof, .poundElseKeyword, .poundElseifKeyword, .poundEndifKeyword]) && elementsProgress.evaluate(currentToken) {
92+
guard let element = parseElement(&self), element.raw.byteLength > 0 else {
9693
break
9794
}
9895
elements.append(element)

Sources/SwiftParser/Parser.swift

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -275,13 +275,19 @@ extension Parser {
275275
}
276276

277277
/// Eat a token that we know we are currently positioned at, based on `canRecoverTo(anyIn:)`.
278-
mutating func eat(_ handle: RecoveryConsumptionHandle) -> (RawUnexpectedNodesSyntax, Token) {
279-
var unexpectedTokens = [RawSyntax]()
280-
for _ in 0..<handle.unexpectedTokens {
281-
unexpectedTokens.append(RawSyntax(self.consumeAnyToken()))
278+
mutating func eat(_ handle: RecoveryConsumptionHandle) -> (RawUnexpectedNodesSyntax?, Token) {
279+
let unexpectedNodes: RawUnexpectedNodesSyntax?
280+
if handle.unexpectedTokens > 0 {
281+
var unexpectedTokens = [RawSyntax]()
282+
for _ in 0..<handle.unexpectedTokens {
283+
unexpectedTokens.append(RawSyntax(self.consumeAnyToken()))
284+
}
285+
unexpectedNodes = RawUnexpectedNodesSyntax(elements: unexpectedTokens, arena: self.arena)
286+
} else {
287+
unexpectedNodes = nil
282288
}
283289
let token = self.eat(handle.tokenConsumptionHandle)
284-
return (RawUnexpectedNodesSyntax(elements: unexpectedTokens, arena: self.arena), token)
290+
return (unexpectedNodes, token)
285291
}
286292
}
287293

Sources/SwiftParser/RawTokenKindSubset.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,18 @@ enum PoundDeclarationStart: RawTokenKindSubset {
499499
}
500500
}
501501

502+
enum SwitchCaseStart: RawTokenKindSubset {
503+
case caseKeyword
504+
case defaultKeyword
505+
506+
var rawTokenKind: RawTokenKind {
507+
switch self {
508+
case .caseKeyword: return .caseKeyword
509+
case .defaultKeyword: return .defaultKeyword
510+
}
511+
}
512+
}
513+
502514
// MARK: Expression start
503515

504516
enum AwaitTry: RawTokenKindSubset {

Sources/SwiftParser/Statements.swift

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -631,21 +631,20 @@ extension Parser {
631631
var elementsProgress = LoopProgressCondition()
632632
while !self.at(any: [.eof, .rightBrace, .poundEndifKeyword, .poundElseifKeyword, .poundElseKeyword])
633633
&& elementsProgress.evaluate(currentToken) {
634-
if self.lookahead().isAtStartOfSwitchCase() {
634+
if self.lookahead().isAtStartOfSwitchCase(allowRecovery: true) {
635635
elements.append(RawSyntax(self.parseSwitchCase()))
636-
} else if self.at(.poundIfKeyword) {
636+
} else if self.canRecoverTo(.poundIfKeyword) != nil {
637637
// '#if' in 'case' position can enclose zero or more 'case' or 'default'
638638
// clauses.
639-
elements.append(RawSyntax(self.parsePoundIfDirective {
640-
$0.parseSwitchCases()
641-
}
642-
syntax: { parser, cases in
643-
guard cases.count == 1, let firstCase = cases.first else {
644-
assert(cases.isEmpty)
645-
return RawSyntax(RawSwitchCaseListSyntax(elements: [], arena: parser.arena))
646-
}
647-
return RawSyntax(firstCase)
648-
}))
639+
elements.append(RawSyntax(self.parsePoundIfDirective(
640+
{ $0.parseSwitchCases() },
641+
syntax: { parser, cases in
642+
guard cases.count == 1, let firstCase = cases.first else {
643+
assert(cases.isEmpty)
644+
return RawSyntax(RawSwitchCaseListSyntax(elements: [], arena: parser.arena))
645+
}
646+
return RawSyntax(firstCase)
647+
})))
649648
} else {
650649
break
651650
}
@@ -688,9 +687,10 @@ extension Parser {
688687
}
689688

690689
let label: RawSyntax
691-
if self.at(.caseKeyword) {
690+
switch self.canRecoverTo(anyIn: SwitchCaseStart.self) {
691+
case (.caseKeyword, _)?:
692692
label = RawSyntax(self.parseSwitchCaseLabel())
693-
} else {
693+
case (.defaultKeyword, _)?, nil:
694694
label = RawSyntax(self.parseSwitchDefaultLabel())
695695
}
696696

@@ -1039,7 +1039,7 @@ extension Parser.Lookahead {
10391039

10401040
/// Returns whether the parser's current position is the start of a switch case,
10411041
/// given that we're in the middle of a switch already.
1042-
func isAtStartOfSwitchCase() -> Bool {
1042+
func isAtStartOfSwitchCase(allowRecovery: Bool = false) -> Bool {
10431043
// Check for and consume attributes. The only valid attribute is `@unknown`
10441044
// but that's a semantic restriction.
10451045
var lookahead = self.lookahead()
@@ -1053,7 +1053,11 @@ extension Parser.Lookahead {
10531053
lookahead.expectIdentifierWithoutRecovery()
10541054
}
10551055

1056-
return lookahead.at(any: [.caseKeyword, .defaultKeyword])
1056+
if allowRecovery {
1057+
return lookahead.canRecoverTo(anyIn: SwitchCaseStart.self) != nil
1058+
} else {
1059+
return lookahead.at(anyIn: SwitchCaseStart.self) != nil
1060+
}
10571061
}
10581062

10591063
func isStartOfConditionalSwitchCases() -> Bool {

Sources/SwiftParser/TokenPrecedence.swift

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,21 +31,25 @@ public enum TokenPrecedence: Comparable {
3131
case stmtKeyword
3232
/// The '{' token because it typically marks the body of a declaration.
3333
/// `closingDelimiter` must have type `strongPunctuator`
34-
case strongBracketed(closingDelimiter: RawTokenKind)
34+
case openingBrace(closingDelimiter: RawTokenKind)
3535
/// A punctuator that is a strong indicator that it separates two distinct parts of the source code, like two statements
3636
case strongPunctuator
3737
/// The closing delimiter of `strongBracketed`
38-
case strongBracketedClose
38+
case closingBrace
3939
/// Tokens that start a new declaration
4040
case declKeyword
41+
case openingPoundIf
42+
case closingPoundIf
4143

4244
/// If the precedence is `weakBracketed` or `strongBracketed`, the closing delimeter of the bracketed group.
4345
var closingTokenKind: RawTokenKind? {
4446
switch self {
4547
case .weakBracketed(closingDelimiter: let closingDelimiter):
4648
return closingDelimiter
47-
case .strongBracketed(closingDelimiter: let closingDelimiter):
49+
case .openingBrace(closingDelimiter: let closingDelimiter):
4850
return closingDelimiter
51+
case .openingPoundIf:
52+
return .poundEndifKeyword
4953
default:
5054
return nil
5155
}
@@ -69,12 +73,16 @@ public enum TokenPrecedence: Comparable {
6973
return 5
7074
case .strongPunctuator:
7175
return 6
72-
case .strongBracketed:
76+
case .openingBrace:
7377
return 7
74-
case .strongBracketedClose:
78+
case .closingBrace:
7579
return 8
7680
case .declKeyword:
7781
return 9
82+
case .openingPoundIf:
83+
return 10
84+
case .closingPoundIf:
85+
return 11
7886
}
7987
}
8088

@@ -162,9 +170,9 @@ public enum TokenPrecedence: Comparable {
162170

163171
// MARK: Strong bracketet
164172
case .leftBrace:
165-
self = .strongBracketed(closingDelimiter: .rightBrace)
173+
self = .openingBrace(closingDelimiter: .rightBrace)
166174
case .poundElseifKeyword, .poundElseKeyword, .poundIfKeyword:
167-
self = .strongBracketed(closingDelimiter: .poundEndifKeyword)
175+
self = .openingPoundIf
168176

169177
// MARK: Strong punctuator
170178
case
@@ -179,8 +187,10 @@ public enum TokenPrecedence: Comparable {
179187
self = .strongPunctuator
180188

181189
// MARK: Strong bracket close
182-
case .poundEndifKeyword, .rightBrace:
183-
self = .strongBracketedClose
190+
case .rightBrace:
191+
self = .closingBrace
192+
case .poundEndifKeyword:
193+
self = .closingPoundIf
184194

185195
// MARK: Decl keywords
186196
case

Tests/SwiftParserTest/Directives.swift

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,17 +84,16 @@ final class DirectiveTests: XCTestCase {
8484
AssertParse(
8585
"""
8686
#if os(iOS)
87-
func foo() {}#^DIAG_1^#
88-
#^DIAG_2^#}
87+
func foo() {}
88+
#^DIAG_1^#}
8989
#else
90-
func bar() {}
9190
func baz() {}
92-
} // expected-error{{unexpected '}' in conditional compilation block}}
91+
#^DIAG_2^#}
9392
#endif
9493
""",
9594
diagnostics: [
96-
DiagnosticSpec(locationMarker: "DIAG_1", message: "Expected '#endif' in conditional compilation block"),
97-
DiagnosticSpec(locationMarker: "DIAG_2", message: "Extraneous code at top level"),
95+
DiagnosticSpec(locationMarker: "DIAG_1", message: "Unexpected text '}' found in conditional compilation clause"),
96+
DiagnosticSpec(locationMarker: "DIAG_2", message: "Unexpected text '}' found in conditional compilation block"),
9897
]
9998
)
10099
}

Tests/SwiftParserTest/Statements.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -184,17 +184,17 @@ final class StatementTests: XCTestCase {
184184
AssertParse(
185185
"""
186186
switch x {
187-
#^DIAG^#print()
187+
#^FOO^#foo()
188188
#if true
189-
print()
189+
#^BAR^#bar()
190190
#endif
191191
case .A, .B:
192192
break
193193
}
194194
""",
195195
diagnostics: [
196-
DiagnosticSpec(message: "Unexpected text found in 'switch' statement"),
197-
196+
DiagnosticSpec(locationMarker: "FOO", message: "Unexpected text 'foo()' found in conditional compilation clause"),
197+
DiagnosticSpec(locationMarker: "BAR", message: "Unexpected text 'bar()' found in conditional compilation block"),
198198
]
199199
)
200200

@@ -213,7 +213,7 @@ final class StatementTests: XCTestCase {
213213
}
214214
""",
215215
diagnostics: [
216-
DiagnosticSpec(message: "Unexpected text found in 'switch' statement")
216+
DiagnosticSpec(message: "Unexpected text 'print()' found in conditional compilation clause")
217217
]
218218
)
219219
}

0 commit comments

Comments
 (0)