Skip to content

Commit 3c21ffe

Browse files
committed
Recover if func or var is missing for a member decl item
1 parent be959d1 commit 3c21ffe

File tree

10 files changed

+299
-77
lines changed

10 files changed

+299
-77
lines changed

Sources/SwiftParser/Declarations.swift

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,14 +99,16 @@ extension TokenConsumer {
9999
// When 'case' appears inside a function, it's probably a switch
100100
// case, not an enum case declaration.
101101
return false
102+
case .some(_):
103+
// All other decl start keywords unconditonally start a decl.
104+
return true
102105
case nil:
103106
if subparser.at(anyIn: ContextualDeclKeyword.self)?.0 != nil {
104107
subparser.consumeAnyToken()
105108
return subparser.atStartOfDeclaration(
106109
isAtTopLevel: isAtTopLevel, allowRecovery: allowRecovery)
107110
}
108111
return false
109-
default: return true
110112
}
111113
}
112114
}
@@ -146,8 +148,11 @@ extension Parser {
146148
/// declaration → precedence-group-declaration
147149
///
148150
/// declarations → declaration declarations?
151+
///
152+
/// If `allowMissingFuncOrVarKeywordRecovery` is `true`, this methods tries to
153+
/// synthesize `func` or `var` keywords where necessary.
149154
@_spi(RawSyntax)
150-
public mutating func parseDeclaration() -> RawDeclSyntax {
155+
public mutating func parseDeclaration(allowMissingFuncOrVarKeywordRecovery: Bool = false) -> RawDeclSyntax {
151156
switch self.at(anyIn: PoundDeclarationStart.self) {
152157
case (.poundIfKeyword, _)?:
153158
let directive = self.parsePoundIfDirective { parser in
@@ -212,10 +217,25 @@ extension Parser {
212217
case (.actorContextualKeyword, let handle)?:
213218
return RawDeclSyntax(self.parseActorDeclaration(attrs, handle))
214219
case nil:
220+
if allowMissingFuncOrVarKeywordRecovery {
221+
let isProbablyVarDecl = self.at(any: [.identifier, .wildcardKeyword]) && self.peek().tokenKind.is(any: [.colon, .equal, .comma])
222+
let isProbablyTupleDecl = self.at(.leftParen) && self.peek().tokenKind.is(any: [.identifier, .wildcardKeyword])
223+
224+
if isProbablyVarDecl || isProbablyTupleDecl {
225+
return RawDeclSyntax(self.parseLetOrVarDeclaration(attrs, .missing(.varKeyword)))
226+
}
227+
228+
let isProbablyFuncDecl = self.at(any: [.identifier, .wildcardKeyword]) || self.at(anyIn: Operator.self) != nil
229+
230+
if isProbablyFuncDecl {
231+
return RawDeclSyntax(self.parseFuncDeclaration(attrs, .missing(.funcKeyword)))
232+
}
233+
}
215234
return RawDeclSyntax(RawMissingDeclSyntax(
216235
attributes: attrs.attributes,
217236
modifiers: attrs.modifiers,
218-
arena: self.arena))
237+
arena: self.arena
238+
))
219239
}
220240
}
221241
}
@@ -586,7 +606,7 @@ extension Parser {
586606
if self.at(.poundSourceLocationKeyword) {
587607
decl = RawDeclSyntax(self.parsePoundSourceLocationDirective())
588608
} else {
589-
decl = self.parseDeclaration()
609+
decl = self.parseDeclaration(allowMissingFuncOrVarKeywordRecovery: true)
590610
}
591611

592612
let semi = self.consume(if: .semicolon)

Sources/SwiftParser/Diagnostics/DiagnosticExtensions.swift

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,22 @@ extension FixIt.Changes {
7676
if let trailingTrivia = trailingTrivia {
7777
presentNode = presentNode.withTrailingTrivia(trailingTrivia)
7878
}
79-
return [.replace(
80-
oldNode: Syntax(node),
81-
newNode: Syntax(presentNode)
82-
)]
79+
if node.shouldBeInsertedAfterNextTokenTrivia,
80+
let nextToken = node.nextToken(viewMode: .sourceAccurate),
81+
leadingTrivia == nil {
82+
return [
83+
.replace(
84+
oldNode: Syntax(node),
85+
newNode: Syntax(presentNode).withLeadingTrivia(nextToken.leadingTrivia)
86+
),
87+
.replaceLeadingTrivia(token: nextToken, newTrivia: [])
88+
]
89+
} else {
90+
return [.replace(
91+
oldNode: Syntax(node),
92+
newNode: Syntax(presentNode)
93+
)]
94+
}
8395
}
8496

8597
/// Makes the `token` present, moving it in front of the previous token's trivia.

Sources/SwiftParser/Diagnostics/MissingNodesError.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,9 +343,16 @@ extension ParseDiagnosticsGenerator {
343343
notes.append(Note(node: Syntax(startToken), message: MatchingOpeningTokenNote(openingToken: startToken)))
344344
}
345345

346+
let position: AbsolutePosition
347+
if node.shouldBeInsertedAfterNextTokenTrivia, let nextToken = node.nextToken(viewMode: .sourceAccurate) {
348+
position = nextToken.positionAfterSkippingLeadingTrivia
349+
} else {
350+
position = node.endPosition
351+
}
352+
346353
addDiagnostic(
347354
node,
348-
position: node.endPosition,
355+
position: position,
349356
MissingNodesError(missingNodes: missingNodes),
350357
notes: notes,
351358
fixIts: [fixIt],

Sources/SwiftParser/Diagnostics/SyntaxExtensions.swift

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,15 +92,32 @@ extension SyntaxProtocol {
9292

9393
/// Returns this node or the first ancestor that satisfies `condition`.
9494
func ancestorOrSelf(where condition: (Syntax) -> Bool) -> Syntax? {
95+
return ancestorOrSelf(mapping: { condition($0) ? $0 : nil })
96+
}
97+
98+
/// Returns this node or the first ancestor that satisfies `condition`.
99+
func ancestorOrSelf<T>(mapping map: (Syntax) -> T?) -> T? {
95100
var walk: Syntax? = Syntax(self)
96101
while let unwrappedParent = walk {
97-
if condition(unwrappedParent) {
98-
return walk
102+
if let mapped = map(unwrappedParent) {
103+
return mapped
99104
}
100105
walk = unwrappedParent.parent
101106
}
102107
return nil
103108
}
109+
110+
/// Returns `true` if the next token's leading trivia should be made leading trivia
111+
/// of this mode, when it is switched from being missing to present.
112+
var shouldBeInsertedAfterNextTokenTrivia: Bool {
113+
if !self.raw.kind.isMissing,
114+
let memberDeclItem = self.ancestorOrSelf(mapping: { $0.as(MemberDeclListItemSyntax.self) }),
115+
memberDeclItem.firstToken(viewMode: .all) == self.firstToken(viewMode: .all) {
116+
return true
117+
} else {
118+
return false
119+
}
120+
}
104121
}
105122

106123
extension TokenKind {

Sources/SwiftParser/Recovery.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,15 @@ public struct RecoveryConsumptionHandle {
2929
unexpectedTokens: 0,
3030
tokenConsumptionHandle: TokenConsumptionHandle(tokenKind: token))
3131
}
32+
33+
/// A `RecoveryConsumptionHandle` that will not eat any tokens but instead
34+
/// synthesize a missing token of kind `token`.
35+
@_spi(RawSyntax)
36+
public static func missing(_ token: RawTokenKind) -> RecoveryConsumptionHandle {
37+
return RecoveryConsumptionHandle(
38+
unexpectedTokens: 0,
39+
tokenConsumptionHandle: TokenConsumptionHandle(tokenKind: token, missing: true))
40+
}
3241
}
3342

3443
extension Parser.Lookahead {

Sources/SwiftParser/SyntaxUtils.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,9 @@ extension SyntaxText {
4747
self.hasSuffix(SyntaxText("#>"))
4848
}
4949
}
50+
51+
extension RawTokenKind {
52+
func `is`(any kinds: [RawTokenKind]) -> Bool {
53+
return kinds.contains(self)
54+
}
55+
}

Sources/SwiftParser/TokenConsumer.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ struct TokenConsumptionHandle {
4545
var tokenKind: RawTokenKind
4646
/// When not `nil`, the token's kind will be remapped to this kind when consumed.
4747
var remappedKind: RawTokenKind?
48+
/// If `true`, the token we should consume should be synthesized as a missing token
49+
/// and no tokens should be consumed.
50+
var missing: Bool = false
4851
}
4952

5053
extension TokenConsumer {
@@ -116,10 +119,13 @@ extension TokenConsumer {
116119

117120
/// Eat a token that we know we are currently positioned at, based on `at(anyIn:)`.
118121
mutating func eat(_ handle: TokenConsumptionHandle) -> Token {
119-
assert(self.at(handle.tokenKind))
120-
if let remappedKind = handle.remappedKind {
122+
if handle.missing {
123+
return missingToken(handle.remappedKind ?? handle.tokenKind, text: nil)
124+
} else if let remappedKind = handle.remappedKind {
125+
assert(self.at(handle.tokenKind))
121126
return consumeAnyToken(remapping: remappedKind)
122127
} else {
128+
assert(self.at(handle.tokenKind))
123129
return consumeAnyToken()
124130
}
125131
}

Tests/SwiftParserTest/Declarations.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -717,11 +717,13 @@ final class DeclarationTests: XCTestCase {
717717
AssertParse(
718718
"""
719719
struct S {
720-
1️⃣/ ###line 25 "line-directive.swift"
720+
1️⃣/ 2️⃣###line 25 "line-directive.swift"
721721
}
722722
""",
723723
diagnostics: [
724-
DiagnosticSpec(locationMarker: "1️⃣", message: #"unexpected code '/ ###line 25 "line-directive.swift"' in struct"#)
724+
DiagnosticSpec(locationMarker: "1️⃣", message: "expected 'func' in function"),
725+
DiagnosticSpec(locationMarker: "2️⃣", message: "expected parameter clause in function signature"),
726+
DiagnosticSpec(locationMarker: "2️⃣", message: #"unexpected code '###line 25 "line-directive.swift"' in struct"#)
725727
]
726728
)
727729
}

0 commit comments

Comments
 (0)