Skip to content

Commit 2036c3e

Browse files
committed
Use indentation to judge whether a closing } should be considered part of a code block if { is missing
## Motivating example: ```swift struct Foo { func foo() } ``` We need to decide whether we should use the `}` to close the opening `{` of the `struct` or `func`. ## Solution Do what a human would do: Use to decide how to match the braces. If the opening `{` of the `func` is missing and the `}` has an indentation that’s less than that of the `func` keyword, assume that it was indented to match an outer and don’t eat it for the `func`.
1 parent 9d13a48 commit 2036c3e

File tree

9 files changed

+101
-34
lines changed

9 files changed

+101
-34
lines changed

Sources/SwiftParser/Declarations.swift

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ extension Parser {
298298
} else {
299299
whereClause = nil
300300
}
301-
let members = self.parseMemberDeclList()
301+
let members = self.parseMemberDeclList(introducer: extensionKeyword)
302302
return RawExtensionDeclSyntax(
303303
attributes: attrs.attributes,
304304
modifiers: attrs.modifiers,
@@ -577,8 +577,11 @@ extension Parser {
577577
)
578578
}
579579

580+
/// `introducer` is the `struct`, `class`, ... keyword that is the cause that the member decl block is being parsed.
581+
/// If the left brace is missing, its indentation will be used to judge whether a following `}` was
582+
/// indented to close this code block or a surrounding context. See `expectRightBrace`.
580583
@_spi(RawSyntax)
581-
public mutating func parseMemberDeclList() -> RawMemberDeclBlockSyntax {
584+
public mutating func parseMemberDeclList(introducer: RawTokenSyntax? = nil) -> RawMemberDeclBlockSyntax {
582585
var elements = [RawMemberDeclListItemSyntax]()
583586
let (unexpectedBeforeLBrace, lbrace) = self.expect(.leftBrace)
584587
do {
@@ -594,7 +597,7 @@ extension Parser {
594597
elements.append(newElement)
595598
}
596599
}
597-
let (unexpectedBeforeRBrace, rbrace) = self.expect(.rightBrace)
600+
let (unexpectedBeforeRBrace, rbrace) = self.expectRightBrace(leftBrace: lbrace, introducer: introducer)
598601
let members: RawMemberDeclListSyntax
599602
if elements.isEmpty && (lbrace.isMissing || rbrace.isMissing) {
600603
members = RawMemberDeclListSyntax(elements: [], arena: self.arena)
@@ -676,7 +679,7 @@ extension Parser {
676679
whereClause = nil
677680
}
678681

679-
let members = self.parseMemberDeclList()
682+
let members = self.parseMemberDeclList(introducer: classKeyword)
680683
return RawClassDeclSyntax(
681684
attributes: attrs.attributes,
682685
modifiers: attrs.modifiers,
@@ -798,7 +801,7 @@ extension Parser {
798801
whereClause = nil
799802
}
800803

801-
let members = self.parseMemberDeclList()
804+
let members = self.parseMemberDeclList(introducer: enumKeyword)
802805
return RawEnumDeclSyntax(
803806
attributes: attrs.attributes, modifiers: attrs.modifiers,
804807
unexpectedBeforeEnumKeyword,
@@ -943,7 +946,7 @@ extension Parser {
943946
whereClause = nil
944947
}
945948

946-
let members = self.parseMemberDeclList()
949+
let members = self.parseMemberDeclList(introducer: structKeyword)
947950
return RawStructDeclSyntax(
948951
attributes: attrs.attributes, modifiers: attrs.modifiers,
949952
unexpectedBeforeStructKeyword,
@@ -1055,7 +1058,7 @@ extension Parser {
10551058
whereClause = nil
10561059
}
10571060

1058-
let members = self.parseMemberDeclList()
1061+
let members = self.parseMemberDeclList(introducer: protocolKeyword)
10591062

10601063
return RawProtocolDeclSyntax(
10611064
attributes: attrs.attributes, modifiers: attrs.modifiers,
@@ -1183,7 +1186,7 @@ extension Parser {
11831186
whereClause = nil
11841187
}
11851188

1186-
let members = self.parseMemberDeclList()
1189+
let members = self.parseMemberDeclList(introducer: actorKeyword)
11871190
return RawActorDeclSyntax(
11881191
attributes: attrs.attributes,
11891192
modifiers: attrs.modifiers,

Sources/SwiftParser/Parser.swift

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,58 @@ extension Parser {
424424
self.missingToken(.identifier, text: nil)
425425
)
426426
}
427+
428+
/// Expect a right brace but with a little smart recovery logic:
429+
/// If `leftBrace` is missing, we only recover to a `}` whose indentation is greater or equal to that of `introducer`.
430+
/// That way, if the developer forgot to to type `{`, we won't eat `}` that were most likely intended to close an outer scope.
431+
///
432+
/// If `leftBrace` is present or `introducer` is `nil`, this is equivalent to `self.expect(.rightBrace)`.
433+
mutating func expectRightBrace(leftBrace: RawTokenSyntax, introducer: RawTokenSyntax?) -> (unexpected: RawUnexpectedNodesSyntax?, token: RawTokenSyntax) {
434+
func indentation(_ pieces: [RawTriviaPiece]) -> RawTriviaPiece? {
435+
if pieces.last?.isNewline == true {
436+
return .spaces(0)
437+
}
438+
guard pieces.count >= 2 else {
439+
return nil
440+
}
441+
guard pieces[pieces.count - 2].isNewline else {
442+
return nil
443+
}
444+
switch pieces[pieces.count - 1] {
445+
case .spaces, .tabs:
446+
return pieces[pieces.count - 1]
447+
default:
448+
return nil
449+
}
450+
}
451+
452+
guard leftBrace.isMissing, let introducer = introducer else {
453+
// Fast path for correct parses: If leftBrace is not missing, just `expect`.
454+
return self.expect(.rightBrace)
455+
}
456+
457+
var lookahead = self.lookahead()
458+
guard let recoveryHandle = lookahead.canRecoverTo([.rightBrace]) else {
459+
// We can't recover to '}'. Synthesize it.
460+
return (nil, self.missingToken(.rightBrace, text: nil))
461+
}
462+
463+
// We can recover to a '}'. Decide whether we want to eat it based on its indentation.
464+
let rightBraceTrivia = self.arena.parseTrivia(source: lookahead.currentToken.leadingTriviaText, position: .leading)
465+
switch (indentation(introducer.leadingTriviaPieces), indentation(rightBraceTrivia)) {
466+
// Catch cases where the brace has known indentation that is less than that of `introducer`, in which case we don't want to consume it.
467+
case (.spaces(let introducerSpaces), .spaces(let rightBraceSpaces)) where rightBraceSpaces < introducerSpaces:
468+
return (nil, self.missingToken(.rightBrace, text: nil))
469+
case (.tabs(let introducerTabs), .tabs(let rightBraceTabs)) where rightBraceTabs < introducerTabs:
470+
return (nil, self.missingToken(.rightBrace, text: nil))
471+
case (.spaces, .tabs(0)):
472+
return (nil, self.missingToken(.rightBrace, text: nil))
473+
case (.tabs, .spaces(0)):
474+
return (nil, self.missingToken(.rightBrace, text: nil))
475+
default:
476+
return self.eat(recoveryHandle)
477+
}
478+
}
427479
}
428480

429481
// MARK: Splitting Tokens

Sources/SwiftParser/Statements.swift

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ extension Parser {
132132
// A scope encloses the condition and true branch for any variables bound
133133
// by a conditional binding. The else branch does *not* see these variables.
134134
let conditions = self.parseConditionList()
135-
let body = self.parseCodeBlock()
135+
let body = self.parseCodeBlock(introducer: ifKeyword)
136136

137137
// The else branch, if any, is outside of the scope of the condition.
138138
let elseKeyword = self.consume(if: .elseKeyword)
@@ -141,7 +141,7 @@ extension Parser {
141141
if self.at(.ifKeyword) {
142142
elseBody = RawSyntax(self.parseIfStatement(ifHandle: .constant(.ifKeyword)))
143143
} else {
144-
elseBody = RawSyntax(self.parseCodeBlock())
144+
elseBody = RawSyntax(self.parseCodeBlock(introducer: ifKeyword))
145145
}
146146
} else {
147147
elseBody = nil
@@ -169,7 +169,7 @@ extension Parser {
169169
let (unexpectedBeforeGuardKeyword, guardKeyword) = self.eat(guardHandle)
170170
let conditions = self.parseConditionList()
171171
let (unexpectedBeforeElseKeyword, elseKeyword) = self.expect(.elseKeyword)
172-
let body = self.parseCodeBlock()
172+
let body = self.parseCodeBlock(introducer: guardKeyword)
173173
return RawGuardStmtSyntax(
174174
unexpectedBeforeGuardKeyword,
175175
guardKeyword: guardKeyword,
@@ -421,7 +421,7 @@ extension Parser {
421421
@_spi(RawSyntax)
422422
public mutating func parseDeferStatement(deferHandle: RecoveryConsumptionHandle) -> RawDeferStmtSyntax {
423423
let (unexpectedBeforeDeferKeyword, deferKeyword) = self.eat(deferHandle)
424-
let items = self.parseCodeBlock()
424+
let items = self.parseCodeBlock(introducer: deferKeyword)
425425
return RawDeferStmtSyntax(
426426
unexpectedBeforeDeferKeyword,
427427
deferKeyword: deferKeyword,
@@ -443,7 +443,7 @@ extension Parser {
443443
@_spi(RawSyntax)
444444
public mutating func parseDoStatement(doHandle: RecoveryConsumptionHandle) -> RawDoStmtSyntax {
445445
let (unexpectedBeforeDoKeyword, doKeyword) = self.eat(doHandle)
446-
let body = self.parseCodeBlock()
446+
let body = self.parseCodeBlock(introducer: doKeyword)
447447

448448
// If the next token is 'catch', this is a 'do'/'catch' statement.
449449
var elements = [RawCatchClauseSyntax]()
@@ -487,7 +487,7 @@ extension Parser {
487487
arena: self.arena))
488488
} while keepGoing != nil && loopProgress.evaluate(currentToken)
489489
}
490-
let body = self.parseCodeBlock()
490+
let body = self.parseCodeBlock(introducer: catchKeyword)
491491
return RawCatchClauseSyntax(
492492
unexpectedBeforeCatchKeyword,
493493
catchKeyword: catchKeyword,
@@ -510,7 +510,7 @@ extension Parser {
510510
public mutating func parseWhileStatement(whileHandle: RecoveryConsumptionHandle) -> RawWhileStmtSyntax {
511511
let (unexpectedBeforeWhileKeyword, whileKeyword) = self.eat(whileHandle)
512512
let conditions = self.parseConditionList()
513-
let body = self.parseCodeBlock()
513+
let body = self.parseCodeBlock(introducer: whileKeyword)
514514
return RawWhileStmtSyntax(
515515
unexpectedBeforeWhileKeyword,
516516
whileKeyword: whileKeyword,
@@ -531,7 +531,7 @@ extension Parser {
531531
@_spi(RawSyntax)
532532
public mutating func parseRepeatWhileStatement(repeatHandle: RecoveryConsumptionHandle) -> RawRepeatWhileStmtSyntax {
533533
let (unexpectedBeforeRepeatKeyword, repeatKeyword) = self.eat(repeatHandle)
534-
let body = self.parseCodeBlock()
534+
let body = self.parseCodeBlock(introducer: repeatKeyword)
535535
let (unexpectedBeforeWhileKeyword, whileKeyword) = self.expect(.whileKeyword)
536536
let condition = self.parseExpression()
537537
return RawRepeatWhileStmtSyntax(
@@ -601,7 +601,7 @@ extension Parser {
601601
}
602602

603603
// stmt-brace
604-
let body = self.parseCodeBlock()
604+
let body = self.parseCodeBlock(introducer: forKeyword)
605605
return RawForInStmtSyntax(
606606
unexpectedBeforeForKeyword,
607607
forKeyword: forKeyword,
@@ -639,7 +639,7 @@ extension Parser {
639639

640640
let cases = self.parseSwitchCases()
641641

642-
let (unexpectedBeforeRBrace, rbrace) = self.expect(.rightBrace)
642+
let (unexpectedBeforeRBrace, rbrace) = self.expectRightBrace(leftBrace: lbrace, introducer: switchKeyword)
643643
return RawSwitchStmtSyntax(
644644
unexpectedBeforeSwitchKeyword,
645645
switchKeyword: switchKeyword,

Sources/SwiftParser/TopLevel.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,14 @@ extension Parser {
8181
/// =======
8282
///
8383
/// code-block → '{' statements? '}'
84-
mutating func parseCodeBlock() -> RawCodeBlockSyntax {
84+
///
85+
/// `introducer` is the `while`, `if`, ... keyword that is the cause that the code block is being parsed.
86+
/// If the left brace is missing, its indentation will be used to judge whether a following `}` was
87+
/// indented to close this code block or a surrounding context. See `expectRightBrace`.
88+
mutating func parseCodeBlock(introducer: RawTokenSyntax? = nil) -> RawCodeBlockSyntax {
8589
let (unexpectedBeforeLBrace, lbrace) = self.expect(.leftBrace)
8690
let itemList = parseCodeBlockItemList(isAtTopLevel: false, stopCondition: { $0.at(.rightBrace) })
87-
let (unexpectedBeforeRBrace, rbrace) = self.expect(.rightBrace)
91+
let (unexpectedBeforeRBrace, rbrace) = self.expectRightBrace(leftBrace: lbrace, introducer: introducer)
8892

8993
return .init(
9094
unexpectedBeforeLBrace,

Tests/SwiftParserTest/Declarations.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1109,6 +1109,19 @@ final class DeclarationTests: XCTestCase {
11091109
]
11101110
)
11111111
}
1112+
1113+
func testMatchBracesBasedOnSpaces() {
1114+
AssertParse(
1115+
"""
1116+
struct Foo {
1117+
struct Bar 1️⃣
1118+
}
1119+
""",
1120+
diagnostics: [
1121+
DiagnosticSpec(message: "expected member block in struct")
1122+
]
1123+
)
1124+
}
11121125
}
11131126

11141127
extension Parser.DeclAttributes {

Tests/SwiftParserTest/translated/ForeachAsyncTests.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ final class ForeachAsyncTests: XCTestCase {
6464
// FIXME: Bad diagnostics; should be just 'expected 'in' after for-each patter'.
6565
for await i 1️⃣r {
6666
}
67-
for await i in r 2️⃣sum = sum + i;
68-
}3️⃣
67+
for await i in r 2️⃣sum = sum + i;3️⃣
68+
}
6969
""",
7070
diagnostics: [
7171
// TODO: Old parser expected error on line 21: found an unexpected second identifier in constant declaration
@@ -76,7 +76,7 @@ final class ForeachAsyncTests: XCTestCase {
7676
DiagnosticSpec(locationMarker: "1️⃣", message: "expected 'in' in 'for' statement"),
7777
// TODO: Old parser expected error on line 23: expected '{' to start the body of for-each loop
7878
DiagnosticSpec(locationMarker: "2️⃣", message: "expected '{' in 'for' statement"),
79-
DiagnosticSpec(locationMarker: "3️⃣", message: "expected '}' to end function"),
79+
DiagnosticSpec(locationMarker: "3️⃣", message: "expected '}' to end 'for' statement"),
8080
]
8181
)
8282
}

Tests/SwiftParserTest/translated/ForeachTests.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ final class ForeachTests: XCTestCase {
4141
// FIXME: Bad diagnostics; should be just 'expected 'in' after for-each patter'.
4242
for i 1️⃣r {
4343
}
44-
for i in r 2️⃣sum = sum + i;
45-
}3️⃣
44+
for i in r 2️⃣sum = sum + i;3️⃣
45+
}
4646
""",
4747
diagnostics: [
4848
// TODO: Old parser expected error on line 21: found an unexpected second identifier in constant declaration
@@ -53,7 +53,7 @@ final class ForeachTests: XCTestCase {
5353
DiagnosticSpec(locationMarker: "1️⃣", message: "expected 'in' in 'for' statement"),
5454
// TODO: Old parser expected error on line 23: expected '{' to start the body of for-each loop
5555
DiagnosticSpec(locationMarker: "2️⃣", message: "expected '{' in 'for' statement"),
56-
DiagnosticSpec(locationMarker: "3️⃣", message: "expected '}' to end function"),
56+
DiagnosticSpec(locationMarker: "3️⃣", message: "expected '}' to end 'for' statement"),
5757
]
5858
)
5959
}

Tests/SwiftParserTest/translated/GuardTests.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,9 @@ final class GuardTests: XCTestCase {
1717
// TODO: Old parser expected error on line 2: missing condition in 'guard' statement
1818
// TODO: Old parser expected error on line 2: expected 'else' after 'guard' condition
1919
DiagnosticSpec(locationMarker: "1️⃣", message: "expected 'else' in 'guard' statement"),
20-
DiagnosticSpec(locationMarker: "1️⃣", message: "expected '{' in 'guard' statement"),
20+
DiagnosticSpec(locationMarker: "1️⃣", message: "expected code block in 'guard' statement"),
2121
// TODO: Old parser expected error on line 5: missing condition in 'guard' statement
2222
DiagnosticSpec(locationMarker: "2️⃣", message: "expected expression in 'guard' statement"),
23-
DiagnosticSpec(locationMarker: "3️⃣", message: "expected '}' to end function"),
2423
]
2524
)
2625
}

Tests/SwiftParserTest/translated/SwitchTests.swift

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ final class SwitchTests: XCTestCase {
2323
}
2424
""",
2525
diagnostics: [
26-
// TODO: Old parser expected error on line 2: expected expression in 'switch' statement
27-
// TODO: Old parser expected error on line 2: expected identifier in function declaration
2826
DiagnosticSpec(locationMarker: "1️⃣", message: "expected expression and '{}' to end 'switch' statement"),
2927
DiagnosticSpec(locationMarker: "2️⃣", message: "expected identifier in function"),
3028
DiagnosticSpec(locationMarker: "2️⃣", message: "expected argument list in function declaration")
@@ -37,12 +35,10 @@ final class SwitchTests: XCTestCase {
3735
"""
3836
func parseError2(x: Int) {
3937
switch x 1️⃣
40-
}2️⃣
38+
}
4139
""",
4240
diagnostics: [
43-
// TODO: Old parser expected error on line 2: expected '{' after 'switch' subject expression
44-
DiagnosticSpec(locationMarker: "1️⃣", message: "expected '{' in 'switch' statement"),
45-
DiagnosticSpec(locationMarker: "2️⃣", message: "expected '}' to end function"),
41+
DiagnosticSpec(locationMarker: "1️⃣", message: "expected '{}' in 'switch' statement"),
4642
]
4743
)
4844
}

0 commit comments

Comments
 (0)