Skip to content

Commit 8ea19b6

Browse files
authored
[SwiftParser] Improve statement level recovery (#3156)
Consider the following code: ```swift } @ATTR1 func foo() {} ``` Previously, this was parsed as a `FunctionDeclSyntax` with the unexpected code `} @attr` attached as `unexpectedBeforeFuncKeyword`. This was problematic because `@attr` was effectively ignored. The `TokenPrecedence`-based recovery strategy couldn't handle this well, since `@` is not a declaration keyword and doesn't by itself signal the start of a declaration. One option would be to check `atStartOfDeclaration()` after each `skipSingle()` and recognize `@` as the start of a declaration. However, then the preceding `}` would need to be attached to the first attribute in the attribute list, which would require threading recovery logic into `parseAttributeList()` and only applying it to the first `parseAttribute()`. This would make the code more complex and harder to maintain. This PR introduces a new syntax node `UnexpectedCodeDeclSyntax` * It represents unexpected code at the statement/declaration level. * All statement-level recovery is now handled by `parseUnexpectedCodeDeclaration()`. This simplifies recovery handling significantly. --- Also: * Improve `#if` related recovery: * orphan `#endif` et al at top-level is now `UnexpectedCodeDecl` instead of making everything after it "unexpected" ```swift #endif func foo() {} ``` * Unbalanced `#endif` et al closes code/member block with missing `}`. ```swift struct S { #if FOO func foo() { #endif } ``` * Simplified `parsePoundDirective()`. It now receives only one closure instead of 3. * Don't attach ';' to compiler directives. For example: ```swift #sourceLocation(file: "<file>", line: 100) ; ``` `;` is now diagnosed as `;`-only statement, or unexpected `;` separator depending on the position. * Adjust TokenPrecedence for decl/statement keyword so. Decl keyword recovery won't go beyond `}` or statements. E.g. ```swift func foo() { @attr } struct S {} ``` ```swift func foo() { @attr if true {} struct S {} } ```
1 parent baef1ff commit 8ea19b6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+1148
-478
lines changed

CodeGeneration/Sources/SyntaxSupport/CommonNodes.swift

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ public let COMMON_NODES: [Node] = [
177177
kind: .decl,
178178
base: .syntax,
179179
nameForDiagnostics: "declaration",
180-
parserFunction: "parseDeclaration"
180+
parserFunction: "parseDeclarationOrIfConfig"
181181
),
182182

183183
Node(
@@ -401,4 +401,18 @@ public let COMMON_NODES: [Node] = [
401401
elementChoices: [.syntax]
402402
),
403403

404+
Node(
405+
kind: .unexpectedCodeDecl,
406+
base: .decl,
407+
nameForDiagnostics: nil,
408+
documentation: "Unexpected code at declaration position",
409+
children: [
410+
Child(
411+
name: "unexpectedCode",
412+
// NOTE: This is not .collection() on purpose. We don't need collection related functions for this.
413+
kind: .node(kind: .unexpectedNodes)
414+
)
415+
],
416+
noInterleaveUnexpected: true
417+
),
404418
]

CodeGeneration/Sources/SyntaxSupport/Node.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,8 @@ public class Node: NodeChoiceConvertible {
127127
parserFunction: TokenSyntax? = nil,
128128
traits: [String] = [],
129129
children: [Child] = [],
130-
childHistory: Child.History = []
130+
childHistory: Child.History = [],
131+
noInterleaveUnexpected: Bool = false
131132
) {
132133
precondition(base != .syntaxCollection)
133134
precondition(base.isBase, "unknown base kind '\(base)' for node '\(kind)'")
@@ -140,7 +141,8 @@ public class Node: NodeChoiceConvertible {
140141
self.documentation = SwiftSyntax.Trivia.docCommentTrivia(from: documentation)
141142
self.parserFunction = parserFunction
142143

143-
let childrenWithUnexpected = kind.isBase ? children : interleaveUnexpectedChildren(children)
144+
let childrenWithUnexpected =
145+
(kind.isBase || noInterleaveUnexpected) ? children : interleaveUnexpectedChildren(children)
144146

145147
self.data = .layout(children: childrenWithUnexpected, childHistory: childHistory, traits: traits)
146148
}

CodeGeneration/Sources/SyntaxSupport/SyntaxNodeKind.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,7 @@ public enum SyntaxNodeKind: String, CaseIterable, IdentifierConvertible, TypeCon
301301
case typeSpecifier
302302
case lifetimeSpecifierArguments
303303
case typeSpecifierList
304+
case unexpectedCodeDecl
304305
case unexpectedNodes
305306
case unresolvedAsExpr
306307
case unresolvedIsExpr

CodeGeneration/Sources/generate-swift-syntax/templates/swiftparser/LayoutNodesParsableFile.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ let layoutNodesParsableFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
6868
DeclSyntax(
6969
"""
7070
mutating func parseNonOptionalCodeBlockItem() -> RawCodeBlockItemSyntax {
71-
guard let node = self.parseCodeBlockItem(isAtTopLevel: false, allowInitDecl: true) else {
71+
guard let node = self.parseCodeBlockItem(allowInitDecl: true, until: { _ in false }) else {
7272
// The missing item is not necessary to be a declaration,
7373
// which is just a placeholder here
7474
return RawCodeBlockItemSyntax(

CodeGeneration/Tests/ValidateSyntaxNodes/ValidateSyntaxNodes.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -641,7 +641,12 @@ class ValidateSyntaxNodes: XCTestCase {
641641

642642
assertFailuresMatchXFails(
643643
failures,
644-
expectedFailures: []
644+
expectedFailures: [
645+
ValidationFailure(
646+
node: .unexpectedCodeDecl,
647+
message: "child 'unexpectedCode' is a SyntaxCollection but child is not marked as a collection"
648+
)
649+
]
645650
)
646651
}
647652

Sources/SwiftParser/Attributes.swift

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,7 @@ extension Parser {
3333
//
3434
// In such cases, the second `#if` is not `consumeIfConfigOfAttributes()`.
3535
return .ifConfigDecl(
36-
self.parsePoundIfDirective { (parser, _) -> RawAttributeListSyntax.Element in
37-
return parser.parseAttributeListElement()
38-
} syntax: { parser, attributes in
39-
return .attributes(RawAttributeListSyntax(elements: attributes, arena: parser.arena))
40-
}
36+
self.parsePoundIfDirective({ .attributes($0.parseAttributeList()) })
4137
)
4238
} else {
4339
return .attribute(self.parseAttribute())
@@ -896,7 +892,32 @@ extension Parser {
896892
return makeMissingProviderArguments(unexpectedBefore: [])
897893
}
898894

899-
let decl = parseDeclaration(in: .argumentList)
895+
let decl: RawDeclSyntax
896+
if self.at(.poundIf) {
897+
// '#if' is not accepted in '@abi' attribute, but for recovery, parse it
898+
// and wrap the first decl in it with unexpected nodes.
899+
let ifConfig = self.parsePoundIfDirective({ parser in
900+
let decl = parser.parseDeclaration(in: .argumentList)
901+
let member = RawMemberBlockItemSyntax(decl: decl, semicolon: nil, arena: parser.arena)
902+
return .decls(RawMemberBlockItemListSyntax(elements: [member], arena: parser.arena))
903+
})
904+
decl = ifConfig.makeUnexpectedKeepingFirstNode(
905+
of: RawDeclSyntax.self,
906+
arena: self.arena,
907+
where: { !$0.is(RawIfConfigDeclSyntax.self) },
908+
makeMissing: {
909+
RawDeclSyntax(
910+
RawMissingDeclSyntax(
911+
attributes: self.emptyCollection(RawAttributeListSyntax.self),
912+
modifiers: self.emptyCollection(RawDeclModifierListSyntax.self),
913+
arena: self.arena
914+
)
915+
)
916+
}
917+
)
918+
} else {
919+
decl = self.parseDeclaration(in: .argumentList)
920+
}
900921

901922
guard let provider = RawABIAttributeArgumentsSyntax.Provider(decl) else {
902923
return makeMissingProviderArguments(unexpectedBefore: [decl.raw])

Sources/SwiftParser/CollectionNodes+Parsable.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ extension CodeBlockItemListSyntax: SyntaxParseable {
116116
extension MemberBlockItemListSyntax: SyntaxParseable {
117117
public static func parse(from parser: inout Parser) -> Self {
118118
return parse(from: &parser) { parser in
119-
return parser.parseMemberDeclList()
119+
return parser.parseMemberDeclList(until: { _ in false })
120120
} makeMissing: { remainingTokens, arena in
121121
let missingDecl = RawMissingDeclSyntax(
122122
attributes: RawAttributeListSyntax(elements: [], arena: arena),

Sources/SwiftParser/CompilerFiles.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ extension Parser {
8989

9090
/// Parse a declaration macro expansions in type contexts.
9191
mutating func parseMemberBlockItemListFile() -> RawMemberBlockItemListFileSyntax {
92-
let members = self.parseMemberDeclList()
92+
let members = self.parseMemberDeclList(until: { _ in false })
9393
let (unexpectedBeforeEndOfFileToken, endOfFile) = self.expectEndOfFile()
9494

9595
return RawMemberBlockItemListFileSyntax(

0 commit comments

Comments
 (0)