From bda7f10735b6fc85b07f29d781b53dc35ae6da75 Mon Sep 17 00:00:00 2001 From: Rintaro Ishizaki Date: Wed, 24 Sep 2025 16:41:47 -0700 Subject: [PATCH 01/17] [SwiftParser] Improve statement level recovery --- .../Sources/SyntaxSupport/CommonNodes.swift | 12 + .../SyntaxSupport/SyntaxNodeKind.swift | 1 + Sources/SwiftParser/Declarations.swift | 166 ++++++++-- Sources/SwiftParser/Directives.swift | 13 +- Sources/SwiftParser/Expressions.swift | 19 +- Sources/SwiftParser/Statements.swift | 11 +- Sources/SwiftParser/TokenPrecedence.swift | 6 +- Sources/SwiftParser/TopLevel.swift | 29 +- .../generated/SwiftSyntax.md | 1 + .../generated/ChildNameForKeyPath.swift | 6 + .../generated/SyntaxAnyVisitor.swift | 8 + .../generated/SyntaxBaseNodes.swift | 5 +- .../SwiftSyntax/generated/SyntaxEnum.swift | 6 + .../SwiftSyntax/generated/SyntaxKind.swift | 3 + .../generated/SyntaxRewriter.swift | 16 + .../SwiftSyntax/generated/SyntaxVisitor.swift | 24 ++ .../generated/raw/RawSyntaxNodesD.swift | 2 +- .../generated/raw/RawSyntaxNodesTUVWXYZ.swift | 58 ++++ .../generated/raw/RawSyntaxValidation.swift | 8 + .../syntaxNodes/SyntaxNodesTUVWXYZ.swift | 75 +++++ ...DiagnosticsFormatterIntegrationTests.swift | 2 +- Tests/SwiftParserTest/Assertions.swift | 10 + Tests/SwiftParserTest/AttributeTests.swift | 22 +- Tests/SwiftParserTest/DeclarationTests.swift | 66 ++-- Tests/SwiftParserTest/DirectiveTests.swift | 4 +- Tests/SwiftParserTest/DoExpressionTests.swift | 2 +- Tests/SwiftParserTest/ExpressionTests.swift | 27 +- Tests/SwiftParserTest/RegexLiteralTests.swift | 2 +- Tests/SwiftParserTest/StatementTests.swift | 40 ++- .../SwiftParserTest/ValueGenericsTests.swift | 2 +- .../translated/EnumTests.swift | 26 +- .../translated/ErrorsTests.swift | 16 +- .../translated/EscapedIdentifiersTests.swift | 16 +- ...orwardSlashRegexSkippingInvalidTests.swift | 14 +- .../translated/ForwardSlashRegexTests.swift | 2 +- .../translated/IfconfigExprTests.swift | 2 +- .../translated/ModuleSelectorTests.swift | 8 +- .../translated/MultilineErrorsTests.swift | 4 +- .../translated/OperatorDeclTests.swift | 2 +- .../translated/OptionalTests.swift | 2 +- .../translated/PoundAssertTests.swift | 4 +- .../translated/RecoveryLibraryTests.swift | 6 +- .../translated/RecoveryTests.swift | 309 ++++++++++++++---- .../translated/TypealiasTests.swift | 4 +- 44 files changed, 831 insertions(+), 230 deletions(-) diff --git a/CodeGeneration/Sources/SyntaxSupport/CommonNodes.swift b/CodeGeneration/Sources/SyntaxSupport/CommonNodes.swift index 5e1b4f0a0b9..96f3b0b9624 100644 --- a/CodeGeneration/Sources/SyntaxSupport/CommonNodes.swift +++ b/CodeGeneration/Sources/SyntaxSupport/CommonNodes.swift @@ -401,4 +401,16 @@ public let COMMON_NODES: [Node] = [ elementChoices: [.syntax] ), + Node( + kind: .unexpectedCodeDecl, + base: .decl, + nameForDiagnostics: nil, + documentation: "Unexpected code at declaration position", + children: [ + Child( + name: "unexpectedCode", + kind: .node(kind: .unexpectedNodes) + ) + ] + ), ] diff --git a/CodeGeneration/Sources/SyntaxSupport/SyntaxNodeKind.swift b/CodeGeneration/Sources/SyntaxSupport/SyntaxNodeKind.swift index 85e93c701f1..70eae620e70 100644 --- a/CodeGeneration/Sources/SyntaxSupport/SyntaxNodeKind.swift +++ b/CodeGeneration/Sources/SyntaxSupport/SyntaxNodeKind.swift @@ -301,6 +301,7 @@ public enum SyntaxNodeKind: String, CaseIterable, IdentifierConvertible, TypeCon case typeSpecifier case lifetimeSpecifierArguments case typeSpecifierList + case unexpectedCodeDecl case unexpectedNodes case unresolvedAsExpr case unresolvedIsExpr diff --git a/Sources/SwiftParser/Declarations.swift b/Sources/SwiftParser/Declarations.swift index 97e2c3e2c8b..325bc07bdd9 100644 --- a/Sources/SwiftParser/Declarations.swift +++ b/Sources/SwiftParser/Declarations.swift @@ -55,7 +55,8 @@ extension TokenConsumer { mutating func atStartOfDeclaration( isAtTopLevel: Bool = false, allowInitDecl: Bool = true, - allowRecovery: Bool = false + allowRecovery: Bool = false, + requiresDecl: Bool = false, ) -> Bool { if self.at(.poundIf) { return true @@ -132,7 +133,7 @@ extension TokenConsumer { case .lhs(.case): // When 'case' appears inside a function, it's probably a switch // case, not an enum case declaration. - return false + return requiresDecl case .lhs(.`init`): return allowInitDecl case .lhs(.macro): @@ -140,7 +141,7 @@ extension TokenConsumer { return subparser.peek().rawTokenKind == .identifier case .lhs(.pound): // Force parsing '#' after attributes as a macro expansion decl. - if hasAttribute || hasModifier { + if hasAttribute || hasModifier || requiresDecl { return true } @@ -181,8 +182,25 @@ extension TokenConsumer { allowRecovery: allowRecovery ) } - if allowRecovery && (hasAttribute || hasModifier) { + if requiresDecl { // If we found any attributes or modifiers, consider it's a missing decl. + if hasAttribute || hasModifier { + return true + } + if subparser.atFunctionDeclarationWithoutFuncKeyword() { + return true + } + if subparser.atBindingDeclarationWithoutVarKeyword() { + return true + } + if subparser.currentToken.isEditorPlaceholder { + return true + } + } + // Special recovery 'try let/var'. + if subparser.at(.keyword(.try)), + subparser.peek(isAtAnyIn: VariableDeclSyntax.BindingSpecifierOptions.self) != nil + { return true } return false @@ -199,6 +217,10 @@ extension Parser { self.attributes = attributes self.modifiers = modifiers } + + var isEmpty: Bool { + attributes.isEmpty && modifiers.isEmpty + } } /// Describes the context around a declaration in order to modify how it is parsed. @@ -267,8 +289,8 @@ extension Parser { semicolon: semicolon, arena: parser.arena ) - } addSemicolonIfNeeded: { lastElement, newItemAtStartOfLine, parser in - if lastElement.semicolon == nil && !newItemAtStartOfLine { + } addSemicolonIfNeeded: { lastElement, newItemAtStartOfLine, newItem, parser in + if lastElement.semicolon == nil && !newItemAtStartOfLine && !newItem.decl.is(RawUnexpectedCodeDeclSyntax.self) { return RawMemberBlockItemSyntax( lastElement.unexpectedBeforeDecl, decl: lastElement.decl, @@ -313,11 +335,11 @@ extension Parser { // We aren't at a declaration keyword and it looks like we are at a function // declaration. Parse a function declaration. recoveryResult = (.lhs(.func), .missing(.keyword(.func))) + } else if atBindingDeclarationWithoutVarKeyword() { + recoveryResult = (.rhs(.var), .missing(.keyword(.var))) } else { // In all other cases, use standard token recovery to find the declaration // to parse. - // If we are inside a memberDecl list, we don't want to eat closing braces (which most likely close the outer context) - // while recovering to the declaration start. recoveryResult = self.canRecoverTo( anyIn: DeclarationKeyword.self, overrideRecoveryPrecedence: context.recoveryPrecedence @@ -380,13 +402,6 @@ extension Parser { } if context.requiresDecl { - let isProbablyVarDecl = self.at(.identifier, .wildcard) && self.peek(isAt: .colon, .equal, .comma) - let isProbablyTupleDecl = self.at(.leftParen) && self.peek(isAt: .identifier, .wildcard) - - if isProbablyVarDecl || isProbablyTupleDecl { - return RawDeclSyntax(self.parseBindingDeclaration(attrs, .missing(.keyword(.var)), in: context)) - } - if self.currentToken.isEditorPlaceholder { let placeholder = self.parseAnyIdentifier() return RawDeclSyntax( @@ -398,10 +413,6 @@ extension Parser { ) ) } - - if atFunctionDeclarationWithoutFuncKeyword() { - return RawDeclSyntax(self.parseFuncDeclaration(attrs, .missing(.keyword(.func)))) - } } return RawDeclSyntax( RawMissingDeclSyntax( @@ -411,6 +422,27 @@ extension Parser { ) ) } +} + +extension TokenConsumer { + /// Returns `true` if it looks like the parser is positioned at a variable declaration that’s missing the `var` keyword. + fileprivate mutating func atBindingDeclarationWithoutVarKeyword() -> Bool { + if self.at(.identifier, .wildcard), + self.peek(isAt: .colon, .equal, .comma) + { + return true + } + if self.at(.leftParen), + self.peek(isAt: .identifier, .wildcard), + self.withLookahead({ + $0.skipSingle(); return $0.at(.colon, .equal) + }) + { + return true + } + + return false + } /// Returns `true` if it looks like the parser is positioned at a function declaration that’s missing the `func` keyword. fileprivate mutating func atFunctionDeclarationWithoutFuncKeyword() -> Bool { @@ -983,20 +1015,35 @@ extension Parser { let decl: RawDeclSyntax if self.at(.poundSourceLocation) { decl = RawDeclSyntax(self.parsePoundSourceLocationDirective()) - } else { + } else if self.atStartOfDeclaration(isAtTopLevel: false, allowInitDecl: true, requiresDecl: true) { decl = self.parseDeclaration(in: .memberDeclList) + } else { + decl = RawDeclSyntax( + self.parseUnexpectedCodeDeclaration( + isAtTopLevel: false, + allowInitDecl: true, + requiresDecl: true, + skipToDeclOnly: true + ) + ) } - let semi = self.consume(if: .semicolon) + if decl.isEmpty && !self.at(.semicolon) { + return nil + } + + let semi: RawTokenSyntax? + if !decl.isEmpty { + semi = self.consume(if: .semicolon) + } else { + // orphan ';' case. Put it to "unexpected" nodes. + semi = nil + } var trailingSemis: [RawTokenSyntax] = [] while let trailingSemi = self.consume(if: .semicolon) { trailingSemis.append(trailingSemi) } - if decl.isEmpty && semi == nil && trailingSemis.isEmpty { - return nil - } - let result = RawMemberBlockItemSyntax( decl: decl, semicolon: semi, @@ -1013,12 +1060,14 @@ extension Parser { var elements = [RawMemberBlockItemSyntax]() do { var loopProgress = LoopProgressCondition() - while !self.at(.endOfFile, .rightBrace) && self.hasProgressed(&loopProgress) { + while !self.at(.endOfFile, .rightBrace, .poundEndif) && self.hasProgressed(&loopProgress) { let newItemAtStartOfLine = self.atStartOfLine guard let newElement = self.parseMemberBlockItem() else { break } - if let lastItem = elements.last, lastItem.semicolon == nil && !newItemAtStartOfLine { + if let lastItem = elements.last, + lastItem.semicolon == nil && !newItemAtStartOfLine && !newElement.decl.is(RawUnexpectedCodeDeclSyntax.self) + { elements[elements.count - 1] = RawMemberBlockItemSyntax( lastItem.unexpectedBeforeDecl, decl: lastItem.decl, @@ -1039,7 +1088,15 @@ extension Parser { /// If the left brace is missing, its indentation will be used to judge whether a following `}` was /// indented to close this code block or a surrounding context. See `expectRightBrace`. mutating func parseMemberBlock(introducer: RawTokenSyntax? = nil) -> RawMemberBlockSyntax { - let (unexpectedBeforeLBrace, lbrace) = self.expect(.leftBrace) + guard let lBraceHandle = canRecoverTo(.leftBrace) else { + return RawMemberBlockSyntax( + leftBrace: self.missingToken(.leftBrace), + members: RawMemberBlockItemListSyntax(elements: [], arena: self.arena), + rightBrace: consume(if: TokenSpec(.rightBrace, allowAtStartOfLine: false)) ?? self.missingToken(.rightBrace), + arena: arena + ) + } + let (unexpectedBeforeLBrace, lbrace) = self.eat(lBraceHandle) let members = parseMemberDeclList() let (unexpectedBeforeRBrace, rbrace) = self.expectRightBrace(leftBrace: lbrace, introducer: introducer) @@ -2322,4 +2379,57 @@ extension Parser { arena: self.arena ) } + + mutating func parseUnexpectedCodeDeclaration( + isAtTopLevel: Bool, + allowInitDecl: Bool, + requiresDecl: Bool, + skipToDeclOnly: Bool, + ) -> RawUnexpectedCodeDeclSyntax { + let numTokensToSkip = withLookahead { (lookahead) -> Int in + while !lookahead.at(.endOfFile, .semicolon) { + if lookahead.at(.poundElse, .poundElseif, .poundEndif) { + break; + } + if !isAtTopLevel && lookahead.at(.rightBrace) { + break; + } + lookahead.skipSingle() + + if lookahead.at(.poundIf) { + break + } + if lookahead.at(.poundSourceLocation) { + break + } + if lookahead.atStartOfDeclaration( + isAtTopLevel: isAtTopLevel, + allowInitDecl: allowInitDecl, + requiresDecl: requiresDecl + ) { + break + } + + if skipToDeclOnly { + continue + } + if lookahead.atStartOfStatement(preferExpr: false) { + break + } + // Recover to an expression only if it's on the next line. + if lookahead.currentToken.isAtStartOfLine && lookahead.atStartOfExpression() { + break + } + } + return lookahead.tokensConsumed + } + var unexpectedTokens = [RawSyntax]() + for _ in 0..( _ parseElement: (_ parser: inout Parser, _ isFirstElement: Bool) -> Element?, addSemicolonIfNeeded: - (_ lastElement: Element, _ newItemAtStartOfLine: Bool, _ parser: inout Parser) -> Element? = { _, _, _ in nil }, + (_ lastElement: Element, _ newItemAtStartOfLine: Bool, _ newItem: Element, _ parser: inout Parser) -> Element? = { + _, + _, + _, + _ in nil + }, syntax: (inout Parser, [Element]) -> RawIfConfigClauseSyntax.Elements? ) -> RawIfConfigDeclSyntax { if let remainingTokens = remainingTokensIfMaximumNestingLevelReached() { @@ -185,7 +190,9 @@ extension Parser { private mutating func parseIfConfigClauseElements( _ parseElement: (_ parser: inout Parser, _ isFirstElement: Bool) -> Element?, - addSemicolonIfNeeded: (_ lastElement: Element, _ newItemAtStartOfLine: Bool, _ parser: inout Parser) -> Element? + addSemicolonIfNeeded: ( + _ lastElement: Element, _ newItemAtStartOfLine: Bool, _ newItem: Element, _ parser: inout Parser + ) -> Element? ) -> [Element] { var elements = [Element]() var elementsProgress = LoopProgressCondition() @@ -199,7 +206,7 @@ extension Parser { break } if let lastElement = elements.last, - let fixedUpLastItem = addSemicolonIfNeeded(lastElement, newItemAtStartOfLine, &self) + let fixedUpLastItem = addSemicolonIfNeeded(lastElement, newItemAtStartOfLine, element, &self) { elements[elements.count - 1] = fixedUpLastItem } diff --git a/Sources/SwiftParser/Expressions.swift b/Sources/SwiftParser/Expressions.swift index 09a9b0e3ce6..58748166af9 100644 --- a/Sources/SwiftParser/Expressions.swift +++ b/Sources/SwiftParser/Expressions.swift @@ -2414,8 +2414,23 @@ extension Parser { mutating func parseSwitchCaseBody() -> RawCodeBlockItemListSyntax { parseCodeBlockItemList(until: { - $0.at(.rightBrace) || $0.at(.poundEndif, .poundElseif, .poundElse) - || $0.withLookahead({ $0.atStartOfConditionalSwitchCases() }) + if $0.at(.rightBrace) || $0.at(.poundEndif, .poundElseif, .poundElse) { + return true + } + if $0.at(.keyword(.case), .keyword(.default)) { + return true + } + if $0.at(.atSign) + && $0.withLookahead({ + $0.consumeAnyAttribute(); return $0.at(.keyword(.case), .keyword(.default)) + }) + { + return true + } + if $0.withLookahead({ $0.atStartOfConditionalSwitchCases() }) { + return true + } + return false }) } diff --git a/Sources/SwiftParser/Statements.swift b/Sources/SwiftParser/Statements.swift index 619794f19e7..16affb4ac11 100644 --- a/Sources/SwiftParser/Statements.swift +++ b/Sources/SwiftParser/Statements.swift @@ -141,7 +141,9 @@ extension Parser { mutating func parseGuardStatement(guardHandle: RecoveryConsumptionHandle) -> RawGuardStmtSyntax { let (unexpectedBeforeGuardKeyword, guardKeyword) = self.eat(guardHandle) let conditions = self.parseConditionList(isGuardStatement: true) - let (unexpectedBeforeElseKeyword, elseKeyword) = self.expect(.keyword(.else)) + let (unexpectedBeforeElseKeyword, elseKeyword) = self.expect( + TokenSpec(.else, recoveryPrecedence: .openingBrace(closingDelimiter: .rightBrace)) + ) let body = self.parseCodeBlock(introducer: guardKeyword) return RawGuardStmtSyntax( unexpectedBeforeGuardKeyword, @@ -1087,6 +1089,13 @@ extension Parser.Lookahead { ) case nil: + // Special recovery 'try return' etc.. + if !preferExpr, + consume(if: .keyword(.try)) != nil, + self.at(anyIn: SingleValueStatementExpression.self) == nil + { + return atStartOfStatement(allowRecovery: allowRecovery, preferExpr: preferExpr) + } return false } } diff --git a/Sources/SwiftParser/TokenPrecedence.swift b/Sources/SwiftParser/TokenPrecedence.swift index 55dd9ffb769..061e019882d 100644 --- a/Sources/SwiftParser/TokenPrecedence.swift +++ b/Sources/SwiftParser/TokenPrecedence.swift @@ -84,15 +84,13 @@ enum TokenPrecedence: Comparable { return 5 case .weakBracketClose: return 6 - case .stmtKeyword: - return 7 case .strongPunctuator: return 8 case .openingBrace: return 9 - case .closingBrace: + case .declKeyword, .stmtKeyword: return 10 - case .declKeyword: + case .closingBrace: return 11 case .openingPoundIf: return 12 diff --git a/Sources/SwiftParser/TopLevel.swift b/Sources/SwiftParser/TopLevel.swift index 5bdc10fddf1..bf746a323e2 100644 --- a/Sources/SwiftParser/TopLevel.swift +++ b/Sources/SwiftParser/TopLevel.swift @@ -78,7 +78,9 @@ extension Parser { guard let newElement = self.parseCodeBlockItem(isAtTopLevel: isAtTopLevel, allowInitDecl: allowInitDecl) else { break } - if let lastItem = elements.last, lastItem.semicolon == nil && !newItemAtStartOfLine { + if let lastItem = elements.last, + lastItem.semicolon == nil && !newItemAtStartOfLine && !newElement.item.is(RawUnexpectedCodeDeclSyntax.self) + { elements[elements.count - 1] = RawCodeBlockItemSyntax( lastItem.unexpectedBeforeItem, item: .init(lastItem.item)!, @@ -226,8 +228,8 @@ extension Parser { // doesn't constitute its own code block item. let directive = self.parsePoundIfDirective { (parser, _) in parser.parseCodeBlockItem(isAtTopLevel: isAtTopLevel, allowInitDecl: allowInitDecl) - } addSemicolonIfNeeded: { lastElement, newItemAtStartOfLine, parser in - if lastElement.semicolon == nil && !newItemAtStartOfLine { + } addSemicolonIfNeeded: { lastElement, newItemAtStartOfLine, newItem, parser in + if lastElement.semicolon == nil && !newItemAtStartOfLine && !newItem.item.is(RawUnexpectedCodeDeclSyntax.self) { return RawCodeBlockItemSyntax( lastElement.unexpectedBeforeItem, item: .init(lastElement.item)!, @@ -251,16 +253,25 @@ extension Parser { return self.parseStatementItem() } else if self.atStartOfExpression() { return .expr(self.parseExpression(flavor: .basic, pattern: .none)) - } else if self.atStartOfStatement(allowRecovery: true, preferExpr: false) { - return self.parseStatementItem() - } else if self.atStartOfDeclaration(isAtTopLevel: isAtTopLevel, allowInitDecl: allowInitDecl, allowRecovery: true) { - return .decl(self.parseDeclaration()) - } else if self.at(.atSign), peek(isAt: .identifier) { + // } else if self.atStartOfStatement(allowRecovery: true, preferExpr: false) { + // return self.parseStatementItem() + // } else if self.atStartOfDeclaration(isAtTopLevel: isAtTopLevel, allowInitDecl: allowInitDecl, allowRecovery: true) { + // return .decl(self.parseDeclaration()) + } else if (self.at(.atSign) && peek(isAt: .identifier)) || self.at(anyIn: DeclarationModifier.self) != nil { // Force parsing '@' as a declaration, as there's no valid // expression or statement starting with an attribute. return .decl(self.parseDeclaration()) } else { - return .init(expr: RawMissingExprSyntax(arena: self.arena)) + return .decl( + RawDeclSyntax( + self.parseUnexpectedCodeDeclaration( + isAtTopLevel: isAtTopLevel, + allowInitDecl: allowInitDecl, + requiresDecl: false, + skipToDeclOnly: false + ) + ) + ) } } } diff --git a/Sources/SwiftSyntax/Documentation.docc/generated/SwiftSyntax.md b/Sources/SwiftSyntax/Documentation.docc/generated/SwiftSyntax.md index 843dbcfbbe0..d155f0bf117 100644 --- a/Sources/SwiftSyntax/Documentation.docc/generated/SwiftSyntax.md +++ b/Sources/SwiftSyntax/Documentation.docc/generated/SwiftSyntax.md @@ -79,6 +79,7 @@ allows Swift tools to parse, inspect, generate, and transform Swift source code. - - - +- - ### Expressions diff --git a/Sources/SwiftSyntax/generated/ChildNameForKeyPath.swift b/Sources/SwiftSyntax/generated/ChildNameForKeyPath.swift index 7f199475e8b..b627cd56075 100644 --- a/Sources/SwiftSyntax/generated/ChildNameForKeyPath.swift +++ b/Sources/SwiftSyntax/generated/ChildNameForKeyPath.swift @@ -3444,6 +3444,12 @@ public func childName(_ keyPath: AnyKeyPath) -> String? { return "value" case \TypeInitializerClauseSyntax.unexpectedAfterValue: return "unexpectedAfterValue" + case \UnexpectedCodeDeclSyntax.unexpectedBeforeUnexpectedCode: + return "unexpectedBeforeUnexpectedCode" + case \UnexpectedCodeDeclSyntax.unexpectedCode: + return "unexpectedCode" + case \UnexpectedCodeDeclSyntax.unexpectedAfterUnexpectedCode: + return "unexpectedAfterUnexpectedCode" case \UnresolvedAsExprSyntax.unexpectedBeforeAsKeyword: return "unexpectedBeforeAsKeyword" case \UnresolvedAsExprSyntax.asKeyword: diff --git a/Sources/SwiftSyntax/generated/SyntaxAnyVisitor.swift b/Sources/SwiftSyntax/generated/SyntaxAnyVisitor.swift index 093287a440e..1bff5115f67 100644 --- a/Sources/SwiftSyntax/generated/SyntaxAnyVisitor.swift +++ b/Sources/SwiftSyntax/generated/SyntaxAnyVisitor.swift @@ -2265,6 +2265,14 @@ open class SyntaxAnyVisitor: SyntaxVisitor { visitAnyPost(node._syntaxNode) } + override open func visit(_ node: UnexpectedCodeDeclSyntax) -> SyntaxVisitorContinueKind { + return visitAny(node._syntaxNode) + } + + override open func visitPost(_ node: UnexpectedCodeDeclSyntax) { + visitAnyPost(node._syntaxNode) + } + override open func visit(_ node: UnexpectedNodesSyntax) -> SyntaxVisitorContinueKind { return visitAny(node._syntaxNode) } diff --git a/Sources/SwiftSyntax/generated/SyntaxBaseNodes.swift b/Sources/SwiftSyntax/generated/SyntaxBaseNodes.swift index 4aede428e8c..971e335ecd5 100644 --- a/Sources/SwiftSyntax/generated/SyntaxBaseNodes.swift +++ b/Sources/SwiftSyntax/generated/SyntaxBaseNodes.swift @@ -179,6 +179,7 @@ extension Syntax { /// - ``StructDeclSyntax`` /// - ``SubscriptDeclSyntax`` /// - ``TypeAliasDeclSyntax`` +/// - ``UnexpectedCodeDeclSyntax`` /// - ``VariableDeclSyntax`` public struct DeclSyntax: DeclSyntaxProtocol, SyntaxHashable { public let _syntaxNode: Syntax @@ -214,7 +215,7 @@ public struct DeclSyntax: DeclSyntaxProtocol, SyntaxHashable { public init?(_ node: __shared some SyntaxProtocol) { switch node.raw.kind { - case .accessorDecl, .actorDecl, .associatedTypeDecl, .classDecl, .deinitializerDecl, .editorPlaceholderDecl, .enumCaseDecl, .enumDecl, .extensionDecl, .functionDecl, .ifConfigDecl, .importDecl, .initializerDecl, .macroDecl, .macroExpansionDecl, .missingDecl, .operatorDecl, .poundSourceLocation, .precedenceGroupDecl, .protocolDecl, .structDecl, .subscriptDecl, .typeAliasDecl, .usingDecl, .variableDecl: + case .accessorDecl, .actorDecl, .associatedTypeDecl, .classDecl, .deinitializerDecl, .editorPlaceholderDecl, .enumCaseDecl, .enumDecl, .extensionDecl, .functionDecl, .ifConfigDecl, .importDecl, .initializerDecl, .macroDecl, .macroExpansionDecl, .missingDecl, .operatorDecl, .poundSourceLocation, .precedenceGroupDecl, .protocolDecl, .structDecl, .subscriptDecl, .typeAliasDecl, .unexpectedCodeDecl, .usingDecl, .variableDecl: self._syntaxNode = node._syntaxNode default: return nil @@ -262,6 +263,7 @@ public struct DeclSyntax: DeclSyntaxProtocol, SyntaxHashable { .node(StructDeclSyntax.self), .node(SubscriptDeclSyntax.self), .node(TypeAliasDeclSyntax.self), + .node(UnexpectedCodeDeclSyntax.self), .node(UsingDeclSyntax.self), .node(VariableDeclSyntax.self) ]) @@ -1786,6 +1788,7 @@ extension Syntax { .node(TypeExprSyntax.self), .node(TypeInitializerClauseSyntax.self), .node(TypeSpecifierListSyntax.self), + .node(UnexpectedCodeDeclSyntax.self), .node(UnexpectedNodesSyntax.self), .node(UnresolvedAsExprSyntax.self), .node(UnresolvedIsExprSyntax.self), diff --git a/Sources/SwiftSyntax/generated/SyntaxEnum.swift b/Sources/SwiftSyntax/generated/SyntaxEnum.swift index 7dfaad0a54f..9c7a96068ff 100644 --- a/Sources/SwiftSyntax/generated/SyntaxEnum.swift +++ b/Sources/SwiftSyntax/generated/SyntaxEnum.swift @@ -301,6 +301,7 @@ public enum SyntaxEnum: Sendable { case typeExpr(TypeExprSyntax) case typeInitializerClause(TypeInitializerClauseSyntax) case typeSpecifierList(TypeSpecifierListSyntax) + case unexpectedCodeDecl(UnexpectedCodeDeclSyntax) case unexpectedNodes(UnexpectedNodesSyntax) case unresolvedAsExpr(UnresolvedAsExprSyntax) case unresolvedIsExpr(UnresolvedIsExprSyntax) @@ -874,6 +875,8 @@ extension Syntax { return .typeInitializerClause(TypeInitializerClauseSyntax(self)!) case .typeSpecifierList: return .typeSpecifierList(TypeSpecifierListSyntax(self)!) + case .unexpectedCodeDecl: + return .unexpectedCodeDecl(UnexpectedCodeDeclSyntax(self)!) case .unexpectedNodes: return .unexpectedNodes(UnexpectedNodesSyntax(self)!) case .unresolvedAsExpr: @@ -939,6 +942,7 @@ public enum DeclSyntaxEnum { case structDecl(StructDeclSyntax) case subscriptDecl(SubscriptDeclSyntax) case typeAliasDecl(TypeAliasDeclSyntax) + case unexpectedCodeDecl(UnexpectedCodeDeclSyntax) @_spi(ExperimentalLanguageFeatures) case usingDecl(UsingDeclSyntax) case variableDecl(VariableDeclSyntax) @@ -994,6 +998,8 @@ extension DeclSyntax { return .subscriptDecl(SubscriptDeclSyntax(self)!) case .typeAliasDecl: return .typeAliasDecl(TypeAliasDeclSyntax(self)!) + case .unexpectedCodeDecl: + return .unexpectedCodeDecl(UnexpectedCodeDeclSyntax(self)!) case .usingDecl: return .usingDecl(UsingDeclSyntax(self)!) case .variableDecl: diff --git a/Sources/SwiftSyntax/generated/SyntaxKind.swift b/Sources/SwiftSyntax/generated/SyntaxKind.swift index 48111e23c7c..b4bfaad0313 100644 --- a/Sources/SwiftSyntax/generated/SyntaxKind.swift +++ b/Sources/SwiftSyntax/generated/SyntaxKind.swift @@ -301,6 +301,7 @@ public enum SyntaxKind: Sendable { case typeExpr case typeInitializerClause case typeSpecifierList + case unexpectedCodeDecl case unexpectedNodes case unresolvedAsExpr case unresolvedIsExpr @@ -999,6 +1000,8 @@ public enum SyntaxKind: Sendable { return TypeInitializerClauseSyntax.self case .typeSpecifierList: return TypeSpecifierListSyntax.self + case .unexpectedCodeDecl: + return UnexpectedCodeDeclSyntax.self case .unexpectedNodes: return UnexpectedNodesSyntax.self case .unresolvedAsExpr: diff --git a/Sources/SwiftSyntax/generated/SyntaxRewriter.swift b/Sources/SwiftSyntax/generated/SyntaxRewriter.swift index 598f0786e17..4edf4a1a1f9 100644 --- a/Sources/SwiftSyntax/generated/SyntaxRewriter.swift +++ b/Sources/SwiftSyntax/generated/SyntaxRewriter.swift @@ -2022,6 +2022,13 @@ open class SyntaxRewriter { return TypeSpecifierListSyntax(unsafeCasting: visitChildren(node._syntaxNode)) } + /// Visit a ``UnexpectedCodeDeclSyntax``. + /// - Parameter node: the node that is being visited + /// - Returns: the rewritten node + open func visit(_ node: UnexpectedCodeDeclSyntax) -> DeclSyntax { + return DeclSyntax(UnexpectedCodeDeclSyntax(unsafeCasting: visitChildren(node._syntaxNode))) + } + /// Visit a ``UnexpectedNodesSyntax``. /// - Parameter node: the node that is being visited /// - Returns: the rewritten node @@ -3554,6 +3561,11 @@ open class SyntaxRewriter { Syntax(visit(TypeSpecifierListSyntax(unsafeCasting: node))) } + @inline(never) + private func visitUnexpectedCodeDeclSyntaxImpl(_ node: Syntax) -> Syntax { + Syntax(visit(UnexpectedCodeDeclSyntax(unsafeCasting: node))) + } + @inline(never) private func visitUnexpectedNodesSyntaxImpl(_ node: Syntax) -> Syntax { Syntax(visit(UnexpectedNodesSyntax(unsafeCasting: node))) @@ -4218,6 +4230,8 @@ open class SyntaxRewriter { return self.visitTypeInitializerClauseSyntaxImpl(_:) case .typeSpecifierList: return self.visitTypeSpecifierListSyntaxImpl(_:) + case .unexpectedCodeDecl: + return self.visitUnexpectedCodeDeclSyntaxImpl(_:) case .unexpectedNodes: return self.visitUnexpectedNodesSyntaxImpl(_:) case .unresolvedAsExpr: @@ -4810,6 +4824,8 @@ open class SyntaxRewriter { return visitTypeInitializerClauseSyntaxImpl(node) case .typeSpecifierList: return visitTypeSpecifierListSyntaxImpl(node) + case .unexpectedCodeDecl: + return visitUnexpectedCodeDeclSyntaxImpl(node) case .unexpectedNodes: return visitUnexpectedNodesSyntaxImpl(node) case .unresolvedAsExpr: diff --git a/Sources/SwiftSyntax/generated/SyntaxVisitor.swift b/Sources/SwiftSyntax/generated/SyntaxVisitor.swift index b6800a6db74..55d8361eeb8 100644 --- a/Sources/SwiftSyntax/generated/SyntaxVisitor.swift +++ b/Sources/SwiftSyntax/generated/SyntaxVisitor.swift @@ -3335,6 +3335,18 @@ open class SyntaxVisitor { open func visitPost(_ node: TypeSpecifierListSyntax) { } + /// Visiting ``UnexpectedCodeDeclSyntax`` specifically. + /// - Parameter node: the node we are visiting. + /// - Returns: how should we continue visiting. + open func visit(_ node: UnexpectedCodeDeclSyntax) -> SyntaxVisitorContinueKind { + return .visitChildren + } + + /// The function called after visiting ``UnexpectedCodeDeclSyntax`` and its descendants. + /// - node: the node we just finished visiting. + open func visitPost(_ node: UnexpectedCodeDeclSyntax) { + } + /// Visiting ``UnexpectedNodesSyntax`` specifically. /// - Parameter node: the node we are visiting. /// - Returns: how should we continue visiting. @@ -5756,6 +5768,14 @@ open class SyntaxVisitor { visitPost(TypeSpecifierListSyntax(unsafeCasting: node)) } + @inline(never) + private func visitUnexpectedCodeDeclSyntaxImpl(_ node: Syntax) { + if visit(UnexpectedCodeDeclSyntax(unsafeCasting: node)) == .visitChildren { + visitChildren(node) + } + visitPost(UnexpectedCodeDeclSyntax(unsafeCasting: node)) + } + @inline(never) private func visitUnexpectedNodesSyntaxImpl(_ node: Syntax) { if visit(UnexpectedNodesSyntax(unsafeCasting: node)) == .visitChildren { @@ -6474,6 +6494,8 @@ open class SyntaxVisitor { return self.visitTypeInitializerClauseSyntaxImpl(_:) case .typeSpecifierList: return self.visitTypeSpecifierListSyntaxImpl(_:) + case .unexpectedCodeDecl: + return self.visitUnexpectedCodeDeclSyntaxImpl(_:) case .unexpectedNodes: return self.visitUnexpectedNodesSyntaxImpl(_:) case .unresolvedAsExpr: @@ -7066,6 +7088,8 @@ open class SyntaxVisitor { self.visitTypeInitializerClauseSyntaxImpl(node) case .typeSpecifierList: self.visitTypeSpecifierListSyntaxImpl(node) + case .unexpectedCodeDecl: + self.visitUnexpectedCodeDeclSyntaxImpl(node) case .unexpectedNodes: self.visitUnexpectedNodesSyntaxImpl(node) case .unresolvedAsExpr: diff --git a/Sources/SwiftSyntax/generated/raw/RawSyntaxNodesD.swift b/Sources/SwiftSyntax/generated/raw/RawSyntaxNodesD.swift index 53e55832a34..f27b8eaff23 100644 --- a/Sources/SwiftSyntax/generated/raw/RawSyntaxNodesD.swift +++ b/Sources/SwiftSyntax/generated/raw/RawSyntaxNodesD.swift @@ -511,7 +511,7 @@ public struct RawDeclSyntax: RawDeclSyntaxNodeProtocol { public static func isKindOf(_ raw: RawSyntax) -> Bool { switch raw.kind { - case .accessorDecl, .actorDecl, .associatedTypeDecl, .classDecl, .deinitializerDecl, .editorPlaceholderDecl, .enumCaseDecl, .enumDecl, .extensionDecl, .functionDecl, .ifConfigDecl, .importDecl, .initializerDecl, .macroDecl, .macroExpansionDecl, .missingDecl, .operatorDecl, .poundSourceLocation, .precedenceGroupDecl, .protocolDecl, .structDecl, .subscriptDecl, .typeAliasDecl, .usingDecl, .variableDecl: + case .accessorDecl, .actorDecl, .associatedTypeDecl, .classDecl, .deinitializerDecl, .editorPlaceholderDecl, .enumCaseDecl, .enumDecl, .extensionDecl, .functionDecl, .ifConfigDecl, .importDecl, .initializerDecl, .macroDecl, .macroExpansionDecl, .missingDecl, .operatorDecl, .poundSourceLocation, .precedenceGroupDecl, .protocolDecl, .structDecl, .subscriptDecl, .typeAliasDecl, .unexpectedCodeDecl, .usingDecl, .variableDecl: return true default: return false diff --git a/Sources/SwiftSyntax/generated/raw/RawSyntaxNodesTUVWXYZ.swift b/Sources/SwiftSyntax/generated/raw/RawSyntaxNodesTUVWXYZ.swift index 704219505b9..72034364441 100644 --- a/Sources/SwiftSyntax/generated/raw/RawSyntaxNodesTUVWXYZ.swift +++ b/Sources/SwiftSyntax/generated/raw/RawSyntaxNodesTUVWXYZ.swift @@ -1533,6 +1533,64 @@ public struct RawTypeSyntax: RawTypeSyntaxNodeProtocol { } } +@_spi(RawSyntax) +public struct RawUnexpectedCodeDeclSyntax: RawDeclSyntaxNodeProtocol { + @_spi(RawSyntax) + public var layoutView: RawSyntaxLayoutView { + return raw.layoutView! + } + + public static func isKindOf(_ raw: RawSyntax) -> Bool { + return raw.kind == .unexpectedCodeDecl + } + + public var raw: RawSyntax + + init(raw: RawSyntax) { + precondition(Self.isKindOf(raw)) + self.raw = raw + } + + private init(unchecked raw: RawSyntax) { + self.raw = raw + } + + public init?(_ other: some RawSyntaxNodeProtocol) { + guard Self.isKindOf(other.raw) else { + return nil + } + self.init(unchecked: other.raw) + } + + public init( + _ unexpectedBeforeUnexpectedCode: RawUnexpectedNodesSyntax? = nil, + unexpectedCode: RawUnexpectedNodesSyntax, + _ unexpectedAfterUnexpectedCode: RawUnexpectedNodesSyntax? = nil, + arena: __shared RawSyntaxArena + ) { + let raw = RawSyntax.makeLayout( + kind: .unexpectedCodeDecl, uninitializedCount: 3, arena: arena) { layout in + layout.initialize(repeating: nil) + layout[0] = unexpectedBeforeUnexpectedCode?.raw + layout[1] = unexpectedCode.raw + layout[2] = unexpectedAfterUnexpectedCode?.raw + } + self.init(unchecked: raw) + } + + public var unexpectedBeforeUnexpectedCode: RawUnexpectedNodesSyntax? { + layoutView.children[0].map(RawUnexpectedNodesSyntax.init(raw:)) + } + + public var unexpectedCode: RawUnexpectedNodesSyntax { + layoutView.children[1].map(RawUnexpectedNodesSyntax.init(raw:))! + } + + public var unexpectedAfterUnexpectedCode: RawUnexpectedNodesSyntax? { + layoutView.children[2].map(RawUnexpectedNodesSyntax.init(raw:)) + } +} + @_spi(RawSyntax) public struct RawUnexpectedNodesSyntax: RawSyntaxNodeProtocol { @_spi(RawSyntax) diff --git a/Sources/SwiftSyntax/generated/raw/RawSyntaxValidation.swift b/Sources/SwiftSyntax/generated/raw/RawSyntaxValidation.swift index 437ec0f5d05..feca969e1bb 100644 --- a/Sources/SwiftSyntax/generated/raw/RawSyntaxValidation.swift +++ b/Sources/SwiftSyntax/generated/raw/RawSyntaxValidation.swift @@ -3006,6 +3006,12 @@ func validateLayout(layout: RawSyntaxBuffer, as kind: SyntaxKind) { verify(element, as: RawNonisolatedTypeSpecifierSyntax.self)]) } } + func validateUnexpectedCodeDeclSyntax(kind: SyntaxKind, layout: RawSyntaxBuffer) { + assert(layout.count == 3) + assertNoError(kind, 0, verify(layout[0], as: RawUnexpectedNodesSyntax?.self)) + assertNoError(kind, 1, verify(layout[1], as: RawUnexpectedNodesSyntax.self)) + assertNoError(kind, 2, verify(layout[2], as: RawUnexpectedNodesSyntax?.self)) + } func validateUnexpectedNodesSyntax(kind: SyntaxKind, layout: RawSyntaxBuffer) { for (index, element) in layout.enumerated() { assertNoError(kind, index, verify(element, as: RawSyntax.self)) @@ -3713,6 +3719,8 @@ func validateLayout(layout: RawSyntaxBuffer, as kind: SyntaxKind) { validateTypeInitializerClauseSyntax(kind: kind, layout: layout) case .typeSpecifierList: validateTypeSpecifierListSyntax(kind: kind, layout: layout) + case .unexpectedCodeDecl: + validateUnexpectedCodeDeclSyntax(kind: kind, layout: layout) case .unexpectedNodes: validateUnexpectedNodesSyntax(kind: kind, layout: layout) case .unresolvedAsExpr: diff --git a/Sources/SwiftSyntax/generated/syntaxNodes/SyntaxNodesTUVWXYZ.swift b/Sources/SwiftSyntax/generated/syntaxNodes/SyntaxNodesTUVWXYZ.swift index bf5973151ba..38fdc27c2b6 100644 --- a/Sources/SwiftSyntax/generated/syntaxNodes/SyntaxNodesTUVWXYZ.swift +++ b/Sources/SwiftSyntax/generated/syntaxNodes/SyntaxNodesTUVWXYZ.swift @@ -2606,6 +2606,81 @@ public struct TypeInitializerClauseSyntax: SyntaxProtocol, SyntaxHashable, _Leaf ]) } +// MARK: - UnexpectedCodeDeclSyntax + +/// ### Children +/// +/// - `unexpectedCode`: ``UnexpectedNodesSyntax`` +public struct UnexpectedCodeDeclSyntax: DeclSyntaxProtocol, SyntaxHashable, _LeafDeclSyntaxNodeProtocol { + public let _syntaxNode: Syntax + + public init?(_ node: __shared some SyntaxProtocol) { + guard node.raw.kind == .unexpectedCodeDecl else { + return nil + } + self._syntaxNode = node._syntaxNode + } + + @_transparent + init(unsafeCasting node: Syntax) { + self._syntaxNode = node + } + + /// - Parameters: + /// - leadingTrivia: Trivia to be prepended to the leading trivia of the node’s first token. If the node is empty, there is no token to attach the trivia to and the parameter is ignored. + /// - trailingTrivia: Trivia to be appended to the trailing trivia of the node’s last token. If the node is empty, there is no token to attach the trivia to and the parameter is ignored. + public init( + leadingTrivia: Trivia? = nil, + _ unexpectedBeforeUnexpectedCode: UnexpectedNodesSyntax? = nil, + unexpectedCode: UnexpectedNodesSyntax, + _ unexpectedAfterUnexpectedCode: UnexpectedNodesSyntax? = nil, + trailingTrivia: Trivia? = nil + ) { + // Extend the lifetime of all parameters so their arenas don't get destroyed + // before they can be added as children of the new arena. + self = withExtendedLifetime((RawSyntaxArena(), (unexpectedBeforeUnexpectedCode, unexpectedCode, unexpectedAfterUnexpectedCode))) { (arena, _) in + let layout: [RawSyntax?] = [unexpectedBeforeUnexpectedCode?.raw, unexpectedCode.raw, unexpectedAfterUnexpectedCode?.raw] + let raw = RawSyntax.makeLayout( + kind: SyntaxKind.unexpectedCodeDecl, + from: layout, + arena: arena, + leadingTrivia: leadingTrivia, + trailingTrivia: trailingTrivia + ) + return Syntax.forRoot(raw, rawNodeArena: arena).cast(Self.self) + } + } + + public var unexpectedBeforeUnexpectedCode: UnexpectedNodesSyntax? { + get { + return Syntax(self).child(at: 0)?.cast(UnexpectedNodesSyntax.self) + } + set(value) { + self = Syntax(self).replacingChild(at: 0, with: Syntax(value), rawAllocationArena: RawSyntaxArena()).cast(UnexpectedCodeDeclSyntax.self) + } + } + + public var unexpectedCode: UnexpectedNodesSyntax { + get { + return Syntax(self).child(at: 1)!.cast(UnexpectedNodesSyntax.self) + } + set(value) { + self = Syntax(self).replacingChild(at: 1, with: Syntax(value), rawAllocationArena: RawSyntaxArena()).cast(UnexpectedCodeDeclSyntax.self) + } + } + + public var unexpectedAfterUnexpectedCode: UnexpectedNodesSyntax? { + get { + return Syntax(self).child(at: 2)?.cast(UnexpectedNodesSyntax.self) + } + set(value) { + self = Syntax(self).replacingChild(at: 2, with: Syntax(value), rawAllocationArena: RawSyntaxArena()).cast(UnexpectedCodeDeclSyntax.self) + } + } + + public static let structure: SyntaxNodeStructure = .layout([\Self.unexpectedBeforeUnexpectedCode, \Self.unexpectedCode, \Self.unexpectedAfterUnexpectedCode]) +} + // MARK: - UnresolvedAsExprSyntax /// The `as` keyword without any operands. diff --git a/Tests/SwiftDiagnosticsTest/ParserDiagnosticsFormatterIntegrationTests.swift b/Tests/SwiftDiagnosticsTest/ParserDiagnosticsFormatterIntegrationTests.swift index 5f717422309..d40353270c0 100644 --- a/Tests/SwiftDiagnosticsTest/ParserDiagnosticsFormatterIntegrationTests.swift +++ b/Tests/SwiftDiagnosticsTest/ParserDiagnosticsFormatterIntegrationTests.swift @@ -161,7 +161,7 @@ final class ParserDiagnosticsFormatterIntegrationTests: XCTestCase { let expectedOutput = """ 1 | func o() { 2 | }👨‍👩‍👧‍👦} - | |`- error: extraneous braces at top level + | |`- error: unexpected braces in source file | `- error: consecutive statements on a line must be separated by newline or ';' 3 | } diff --git a/Tests/SwiftParserTest/Assertions.swift b/Tests/SwiftParserTest/Assertions.swift index da6f7008056..2c1c6f03a08 100644 --- a/Tests/SwiftParserTest/Assertions.swift +++ b/Tests/SwiftParserTest/Assertions.swift @@ -292,6 +292,16 @@ struct DiagnosticSpec { self.file = file self.line = line } + + static func consecutiveStatementsOnALineDiagnosticSpec( + locationMarker: String = "1️⃣" + ) -> Self { + DiagnosticSpec( + locationMarker: locationMarker, + message: "consecutive statements on a line must be separated by newline or ';'", + fixIts: ["insert newline", "insert ';'"] + ) + } } /// Assert that `location` is the same as that of `locationMarker` in `tree`. diff --git a/Tests/SwiftParserTest/AttributeTests.swift b/Tests/SwiftParserTest/AttributeTests.swift index b5eb6ae1193..0da6dffc640 100644 --- a/Tests/SwiftParserTest/AttributeTests.swift +++ b/Tests/SwiftParserTest/AttributeTests.swift @@ -885,28 +885,18 @@ final class AttributeTests: ParserTestCase { ), DiagnosticSpec( locationMarker: "2️⃣", - message: "expected identifier in class", - fixIts: ["insert identifier"] + message: "expected identifier and member block in class", + fixIts: ["insert identifier and member block"] ), DiagnosticSpec( locationMarker: "2️⃣", - message: "expected '{' in class", - fixIts: ["insert '{'"] - ), - DiagnosticSpec( - locationMarker: "2️⃣", - message: "unexpected code '))' before macro" - ), - DiagnosticSpec( - locationMarker: "3️⃣", - message: "expected '}' to end class", - fixIts: ["insert '}'"] + message: "unexpected code '))' in source file" ), ], fixedSource: """ - @attached(member, names: named(<#expression#>)) class <#identifier#> {)) + @attached(member, names: named(<#expression#>)) class <#identifier#> { + })) macro m() - } """ ) } @@ -1276,7 +1266,7 @@ final class AttributeTests: ParserTestCase { ), DiagnosticSpec( locationMarker: "7️⃣", - message: "unexpected code ')' before function" + message: "unexpected code ')' in source file" ), ], fixedSource: """ diff --git a/Tests/SwiftParserTest/DeclarationTests.swift b/Tests/SwiftParserTest/DeclarationTests.swift index dfce19135e3..af214ffae1d 100644 --- a/Tests/SwiftParserTest/DeclarationTests.swift +++ b/Tests/SwiftParserTest/DeclarationTests.swift @@ -163,7 +163,7 @@ final class DeclarationTests: ParserTestCase { func foo() {} """, diagnostics: [ - DiagnosticSpec(message: "unexpected brace before function") + DiagnosticSpec(message: "unexpected brace in source file") ] ) } @@ -243,7 +243,7 @@ final class DeclarationTests: ParserTestCase { actor Foo {} """, diagnostics: [ - DiagnosticSpec(message: "unexpected brace before actor") + DiagnosticSpec(message: "unexpected brace in source file") ] ) } @@ -327,7 +327,7 @@ final class DeclarationTests: ParserTestCase { diagnostics: [ DiagnosticSpec( locationMarker: "1️⃣", - message: "unexpected code '{}' before enum case" + message: "unexpected code '{}' in protocol" ), DiagnosticSpec( locationMarker: "2️⃣", @@ -611,10 +611,10 @@ final class DeclarationTests: ParserTestCase { """, diagnostics: [ DiagnosticSpec(message: "expected 'set)' to end modifier", fixIts: ["insert 'set)'"]), - DiagnosticSpec(message: "unexpected code 'get, didSet' in variable"), + DiagnosticSpec(message: "expected 'var' in variable", fixIts: ["insert 'var'"]), ], fixedSource: """ - private(set) get, didSet var a = 0 + private(set) var get, didSet var a = 0 """ ) } @@ -1245,7 +1245,7 @@ final class DeclarationTests: ParserTestCase { 1️⃣} """, diagnostics: [ - DiagnosticSpec(message: "extraneous brace at top level") + DiagnosticSpec(message: "unexpected brace in source file") ] ) } @@ -1432,7 +1432,7 @@ final class DeclarationTests: ParserTestCase { message: "consecutive statements on a line must be separated by newline or ';'", fixIts: ["insert newline", "insert ';'"] ), - DiagnosticSpec(locationMarker: "5️⃣", message: "extraneous code ', consectetur adipiscing elit' at top level"), + DiagnosticSpec(locationMarker: "5️⃣", message: "unexpected code ', consectetur adipiscing elit' in source file"), ], applyFixIts: ["insert newline"], fixedSource: """ @@ -1471,7 +1471,7 @@ final class DeclarationTests: ParserTestCase { message: "consecutive statements on a line must be separated by newline or ';'", fixIts: ["insert newline", "insert ';'"] ), - DiagnosticSpec(locationMarker: "5️⃣", message: "extraneous code ', consectetur adipiscing elit' at top level"), + DiagnosticSpec(locationMarker: "5️⃣", message: "unexpected code ', consectetur adipiscing elit' in source file"), ], applyFixIts: ["insert ';'"], fixedSource: """ @@ -1649,7 +1649,11 @@ final class DeclarationTests: ParserTestCase { @3️⃣ """, diagnostics: [ - DiagnosticSpec(locationMarker: "1️⃣", message: "expected '{' in struct", fixIts: ["insert '{'"]), + DiagnosticSpec( + locationMarker: "1️⃣", + message: "expected member block in struct", + fixIts: ["insert member block"] + ), DiagnosticSpec( locationMarker: "2️⃣", message: "expected condition in conditional compilation clause", @@ -1666,14 +1670,13 @@ final class DeclarationTests: ParserTestCase { message: "expected '#endif' in conditional compilation block", fixIts: ["insert '#endif'"] ), - DiagnosticSpec(locationMarker: "3️⃣", message: "expected '}' to end struct", fixIts: ["insert '}'"]), ], fixedSource: """ struct n { + } #if <#expression#> @<#type#> <#declaration#> #endif - } """ ) } @@ -1738,7 +1741,7 @@ final class DeclarationTests: ParserTestCase { assertParse( "1️⃣}class C2️⃣", diagnostics: [ - DiagnosticSpec(locationMarker: "1️⃣", message: "unexpected brace before class"), + DiagnosticSpec(locationMarker: "1️⃣", message: "unexpected brace in source file"), DiagnosticSpec(locationMarker: "2️⃣", message: "expected member block in class", fixIts: ["insert member block"]), ], fixedSource: """ @@ -1749,7 +1752,7 @@ final class DeclarationTests: ParserTestCase { assertParse( "1️⃣}enum C2️⃣", diagnostics: [ - DiagnosticSpec(locationMarker: "1️⃣", message: "unexpected brace before enum"), + DiagnosticSpec(locationMarker: "1️⃣", message: "unexpected brace in source file"), DiagnosticSpec(locationMarker: "2️⃣", message: "expected member block in enum", fixIts: ["insert member block"]), ], fixedSource: """ @@ -1760,7 +1763,7 @@ final class DeclarationTests: ParserTestCase { assertParse( "1️⃣}protocol C2️⃣", diagnostics: [ - DiagnosticSpec(locationMarker: "1️⃣", message: "unexpected brace before protocol"), + DiagnosticSpec(locationMarker: "1️⃣", message: "unexpected brace in source file"), DiagnosticSpec( locationMarker: "2️⃣", message: "expected member block in protocol", @@ -1775,7 +1778,7 @@ final class DeclarationTests: ParserTestCase { assertParse( "1️⃣}actor C2️⃣", diagnostics: [ - DiagnosticSpec(locationMarker: "1️⃣", message: "unexpected brace before actor"), + DiagnosticSpec(locationMarker: "1️⃣", message: "unexpected brace in source file"), DiagnosticSpec(locationMarker: "2️⃣", message: "expected member block in actor", fixIts: ["insert member block"]), ], fixedSource: """ @@ -1786,7 +1789,7 @@ final class DeclarationTests: ParserTestCase { assertParse( "1️⃣}struct C2️⃣", diagnostics: [ - DiagnosticSpec(locationMarker: "1️⃣", message: "unexpected brace before struct"), + DiagnosticSpec(locationMarker: "1️⃣", message: "unexpected brace in source file"), DiagnosticSpec( locationMarker: "2️⃣", message: "expected member block in struct", @@ -1801,7 +1804,7 @@ final class DeclarationTests: ParserTestCase { assertParse( "1️⃣}func C2️⃣", diagnostics: [ - DiagnosticSpec(locationMarker: "1️⃣", message: "unexpected brace before function"), + DiagnosticSpec(locationMarker: "1️⃣", message: "unexpected brace in source file"), DiagnosticSpec( locationMarker: "2️⃣", message: "expected parameter clause in function signature", @@ -1815,7 +1818,7 @@ final class DeclarationTests: ParserTestCase { assertParse( "1️⃣}init2️⃣", diagnostics: [ - DiagnosticSpec(locationMarker: "1️⃣", message: "unexpected brace before initializer"), + DiagnosticSpec(locationMarker: "1️⃣", message: "unexpected brace in source file"), DiagnosticSpec( locationMarker: "2️⃣", message: "expected parameter clause in function signature", @@ -1829,7 +1832,7 @@ final class DeclarationTests: ParserTestCase { assertParse( "1️⃣}subscript2️⃣", diagnostics: [ - DiagnosticSpec(locationMarker: "1️⃣", message: "unexpected brace before subscript"), + DiagnosticSpec(locationMarker: "1️⃣", message: "unexpected brace in source file"), DiagnosticSpec( locationMarker: "2️⃣", message: "expected parameter clause in subscript", @@ -2411,8 +2414,13 @@ final class DeclarationTests: ParserTestCase { name: .identifier("A"), memberBlock: MemberBlockSyntax( leftBrace: .leftBraceToken(), - members: MemberBlockItemListSyntax(), - UnexpectedNodesSyntax([TokenSyntax.binaryOperator("^")]), + members: MemberBlockItemListSyntax([ + MemberBlockItemSyntax( + decl: DeclSyntax( + UnexpectedCodeDeclSyntax(unexpectedCode: UnexpectedNodesSyntax([TokenSyntax.binaryOperator("^")])) + ) + ) + ]), rightBrace: .rightBraceToken() ) ) @@ -2467,13 +2475,13 @@ final class DeclarationTests: ParserTestCase { message: "expected parameter clause in function signature", fixIts: ["insert parameter clause"] ), - DiagnosticSpec(locationMarker: "1️⃣", message: "unexpected code ': Int = A.M1' before macro"), + DiagnosticSpec(locationMarker: "1️⃣", message: "unexpected code ': Int = A.M1' in source file"), DiagnosticSpec( locationMarker: "2️⃣", message: "expected parameter clause in function signature", fixIts: ["insert parameter clause"] ), - DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected code ': T = A.M4 where T.Assoc: P' before macro"), + DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected code ': T = A.M4 where T.Assoc: P' in source file"), ], fixedSource: """ macro m1(): Int = A.M1 @@ -2904,15 +2912,15 @@ final class DeclarationTests: ParserTestCase { class A ℹ️{ 1️⃣^ } - unowned 2️⃣B { + unowned2️⃣ B { } """, diagnostics: [ - DiagnosticSpec(locationMarker: "1️⃣", message: "unexpected code before modifier"), + DiagnosticSpec(locationMarker: "1️⃣", message: "unexpected code '^' in class"), DiagnosticSpec( locationMarker: "2️⃣", - message: "expected declaration and '}' after 'unowned' modifier", - fixIts: ["insert declaration and '}'"] + message: "consecutive statements on a line must be separated by newline or ';'", + fixIts: ["insert newline", "insert ';'"] ), ], fixedSource: @@ -2920,8 +2928,8 @@ final class DeclarationTests: ParserTestCase { class A { ^ } - unowned <#declaration#> - }B { + unowned + B { } """ ) diff --git a/Tests/SwiftParserTest/DirectiveTests.swift b/Tests/SwiftParserTest/DirectiveTests.swift index 60ee4cd239a..a41e75125c5 100644 --- a/Tests/SwiftParserTest/DirectiveTests.swift +++ b/Tests/SwiftParserTest/DirectiveTests.swift @@ -151,8 +151,8 @@ final class DirectiveTests: ParserTestCase { #endif """, diagnostics: [ - DiagnosticSpec(locationMarker: "1️⃣", message: "unexpected brace before conditional compilation clause"), - DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected brace in conditional compilation block"), + DiagnosticSpec(locationMarker: "1️⃣", message: "unexpected brace in conditional compilation clause"), + DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected brace in conditional compilation clause"), ] ) } diff --git a/Tests/SwiftParserTest/DoExpressionTests.swift b/Tests/SwiftParserTest/DoExpressionTests.swift index 3dc5b4efd83..b34e85ba79e 100644 --- a/Tests/SwiftParserTest/DoExpressionTests.swift +++ b/Tests/SwiftParserTest/DoExpressionTests.swift @@ -332,7 +332,7 @@ final class DoExpressionTests: ParserTestCase { } ), diagnostics: [ - DiagnosticSpec(message: "extraneous code 'as Int' at top level") + DiagnosticSpec(message: "unexpected code 'as Int' in source file") ], experimentalFeatures: [] ) diff --git a/Tests/SwiftParserTest/ExpressionTests.swift b/Tests/SwiftParserTest/ExpressionTests.swift index 9533e6b4f56..54db7b14efa 100644 --- a/Tests/SwiftParserTest/ExpressionTests.swift +++ b/Tests/SwiftParserTest/ExpressionTests.swift @@ -233,7 +233,7 @@ final class ExpressionTests: ParserTestCase { #""" \String?.!.count1️⃣.? """#, - diagnostics: [DiagnosticSpec(message: "extraneous code '.?' at top level")] + diagnostics: [DiagnosticSpec(message: "unexpected code '.?' in source file")] ) assertParse( @@ -246,7 +246,7 @@ final class ExpressionTests: ParserTestCase { #""" \Optional.?!?!?!?1️⃣.??! """#, - diagnostics: [DiagnosticSpec(message: "extraneous code '.??!' at top level")] + diagnostics: [DiagnosticSpec(message: "unexpected code '.??!' in source file")] ) assertParse( @@ -678,14 +678,14 @@ final class ExpressionTests: ParserTestCase { func testChainedOptionalUnwrapsWithDot() { assertParse( #"\T.?1️⃣.!"#, - diagnostics: [DiagnosticSpec(message: "extraneous code '.!' at top level")] + diagnostics: [DiagnosticSpec(message: "unexpected code '.!' in source file")] ) } func testChainedOptionalUnwrapsAfterSubscript() { assertParse( #"\T.abc[2]1️⃣.?"#, - diagnostics: [DiagnosticSpec(message: "extraneous code '.?' at top level")] + diagnostics: [DiagnosticSpec(message: "unexpected code '.?' in source file")] ) } @@ -1764,7 +1764,7 @@ final class ExpressionTests: ParserTestCase { ), DiagnosticSpec( locationMarker: "4️⃣", - message: #"extraneous code ')"' at top level"# + message: #"unexpected code ')"' in source file"# ), ], fixedSource: #""" @@ -2277,19 +2277,20 @@ final class ExpressionTests: ParserTestCase { func testSecondaryArgumentLabelDollarIdentifierInClosure() { assertParse( """ - ℹ️{ a1️⃣: (a $ + ℹ️{ a1️⃣: (a $2️⃣ """, diagnostics: [ + DiagnosticSpec(locationMarker: "1️⃣", message: "unexpected code ': (a $' in closure"), DiagnosticSpec( + locationMarker: "2️⃣", message: "expected '}' to end closure", notes: [NoteSpec(message: "to match this opening '{'")], fixIts: ["insert '}'"] ), - DiagnosticSpec(message: "extraneous code ': (a $' at top level"), ], fixedSource: """ - { a - }: (a $ + { a: (a $ + } """ ) } @@ -2699,7 +2700,7 @@ final class StatementExpressionTests: ParserTestCase { } """, diagnostics: [ - DiagnosticSpec(message: "unexpected 'is' keyword in 'switch' statement") + DiagnosticSpec(message: "unexpected 'is' keyword in switch case") ] ) } @@ -3049,7 +3050,7 @@ final class StatementExpressionTests: ParserTestCase { ), DiagnosticSpec( locationMarker: "4️⃣", - message: #"extraneous code ')"' at top level"# + message: #"unexpected code ')"' in source file"# ), ], fixedSource: #""" @@ -3079,7 +3080,7 @@ final class StatementExpressionTests: ParserTestCase { ), DiagnosticSpec( locationMarker: "3️⃣", - message: #"extraneous code ')"' at top level"# + message: #"unexpected code ')"' in source file"# ), ], fixedSource: #""" @@ -3573,7 +3574,7 @@ final class StatementExpressionTests: ParserTestCase { #endif """, diagnostics: [ - DiagnosticSpec(message: "unexpected brace before class") + DiagnosticSpec(message: "unexpected brace in conditional compilation clause") ] ) } diff --git a/Tests/SwiftParserTest/RegexLiteralTests.swift b/Tests/SwiftParserTest/RegexLiteralTests.swift index 56cf4d85dd7..3c84cb49c28 100644 --- a/Tests/SwiftParserTest/RegexLiteralTests.swift +++ b/Tests/SwiftParserTest/RegexLiteralTests.swift @@ -1388,7 +1388,7 @@ final class RegexLiteralTests: ParserTestCase { """, substructure: BinaryOperatorExprSyntax(operator: .binaryOperator("/")), diagnostics: [ - DiagnosticSpec(message: "extraneous code ':/def/' at top level") + DiagnosticSpec(message: "unexpected code ':/def/' in source file") ] ) } diff --git a/Tests/SwiftParserTest/StatementTests.swift b/Tests/SwiftParserTest/StatementTests.swift index d56274bb449..52418fffb10 100644 --- a/Tests/SwiftParserTest/StatementTests.swift +++ b/Tests/SwiftParserTest/StatementTests.swift @@ -265,16 +265,32 @@ final class StatementTests: ParserTestCase { assertParse( """ func test1() { - 1️⃣@s return + @s 1️⃣return } func test2() { - 2️⃣@unknown return + @unknown 2️⃣return } """, diagnostics: [ - DiagnosticSpec(locationMarker: "1️⃣", message: "unexpected code '@s' before 'return' statement"), - DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected code '@unknown' before 'return' statement"), - ] + DiagnosticSpec( + locationMarker: "1️⃣", + message: "expected declaration and ';' after attribute", + fixIts: ["insert declaration and ';'"] + ), + DiagnosticSpec( + locationMarker: "2️⃣", + message: "expected declaration and ';' after attribute", + fixIts: ["insert declaration and ';'"] + ), + ], + fixedSource: """ + func test1() { + @s <#declaration#>; return + } + func test2() { + @unknown <#declaration#>; return + } + """ ) } @@ -335,7 +351,7 @@ final class StatementTests: ParserTestCase { assertParse( "LABEL1️⃣:", diagnostics: [ - DiagnosticSpec(message: "extraneous code ':' at top level") + DiagnosticSpec(message: "unexpected code ':' in source file") ] ) } @@ -826,7 +842,7 @@ final class StatementTests: ParserTestCase { """, diagnostics: [ DiagnosticSpec(locationMarker: "1️⃣", message: "missing condition in 'if' statement"), - DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected code in 'if' statement"), + DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected 'in' keyword in 'if' statement"), ] ) } @@ -863,7 +879,7 @@ final class StatementTests: ParserTestCase { "guard test 1️⃣{ $0 } 2️⃣else {}", diagnostics: [ DiagnosticSpec(locationMarker: "1️⃣", message: "expected 'else' in 'guard' statement", fixIts: ["insert 'else'"]), - DiagnosticSpec(locationMarker: "2️⃣", message: "extraneous code 'else {}' at top level"), + DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected code 'else {}' in source file"), ], fixedSource: "guard test else { $0 } else {}" ) @@ -876,7 +892,7 @@ final class StatementTests: ParserTestCase { """, diagnostics: [ DiagnosticSpec(locationMarker: "1️⃣", message: "expected 'else' in 'guard' statement", fixIts: ["insert 'else'"]), - DiagnosticSpec(locationMarker: "2️⃣", message: "extraneous code 'else {}' at top level"), + DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected code 'else {}' in source file"), ], fixedSource: """ @@ -893,7 +909,7 @@ final class StatementTests: ParserTestCase { """, diagnostics: [ DiagnosticSpec(locationMarker: "1️⃣", message: "expected 'else' in 'guard' statement", fixIts: ["insert 'else'"]), - DiagnosticSpec(locationMarker: "2️⃣", message: "extraneous code 'else {}' at top level"), + DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected code 'else {}' in source file"), ], fixedSource: """ guard test else { $0 @@ -909,8 +925,8 @@ final class StatementTests: ParserTestCase { """, diagnostics: [ DiagnosticSpec(locationMarker: "1️⃣", message: "expected 'else' in 'guard' statement", fixIts: ["insert 'else'"]), - DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected code in 'guard' statement"), - DiagnosticSpec(locationMarker: "3️⃣", message: "extraneous code 'else {}' at top level"), + DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected 'in' keyword in 'guard' statement"), + DiagnosticSpec(locationMarker: "3️⃣", message: "unexpected code 'else {}' in source file"), ], fixedSource: """ guard test else { x in diff --git a/Tests/SwiftParserTest/ValueGenericsTests.swift b/Tests/SwiftParserTest/ValueGenericsTests.swift index 7350ca0b749..0075a3b0a64 100644 --- a/Tests/SwiftParserTest/ValueGenericsTests.swift +++ b/Tests/SwiftParserTest/ValueGenericsTests.swift @@ -71,7 +71,7 @@ final class ValueGenericsTests: ParserTestCase { ), DiagnosticSpec( locationMarker: "2️⃣", - message: "extraneous code '>() {}' at top level" + message: "unexpected code '>() {}' in source file" ), ], fixedSource: """ diff --git a/Tests/SwiftParserTest/translated/EnumTests.swift b/Tests/SwiftParserTest/translated/EnumTests.swift index 946169ae67c..d199ed13f1e 100644 --- a/Tests/SwiftParserTest/translated/EnumTests.swift +++ b/Tests/SwiftParserTest/translated/EnumTests.swift @@ -1393,7 +1393,7 @@ final class EnumTests: ParserTestCase { assertParse( """ enum E_53662_PatternMatching { - case 1️⃣let 2️⃣.foo(x, y): + case 1️⃣let 2️⃣.3️⃣foo(x4️⃣, y5️⃣)6️⃣: } """, diagnostics: [ @@ -1404,12 +1404,32 @@ final class EnumTests: ParserTestCase { ), DiagnosticSpec( locationMarker: "2️⃣", - message: "unexpected code '.foo(x, y):' in enum" + message: "unexpected code '.' in enum" ), + DiagnosticSpec( + locationMarker: "3️⃣", + message: "expected 'func' in function", + fixIts: ["insert 'func'"] + ), + DiagnosticSpec( + locationMarker: "4️⃣", + message: "expected ':' and type in parameter", + fixIts: ["insert ':' and type"] + ), + DiagnosticSpec( + locationMarker: "5️⃣", + message: "expected ':' and type in parameter", + fixIts: ["insert ':' and type"] + ), + DiagnosticSpec( + locationMarker: "6️⃣", + message: "unexpected code ':' in enum" + ), + ], fixedSource: """ enum E_53662_PatternMatching { - case `let` .foo(x, y): + case `let` .func foo(x: <#type#>, y: <#type#>): } """ ) diff --git a/Tests/SwiftParserTest/translated/ErrorsTests.swift b/Tests/SwiftParserTest/translated/ErrorsTests.swift index 0f86ff82a38..351779ca78d 100644 --- a/Tests/SwiftParserTest/translated/ErrorsTests.swift +++ b/Tests/SwiftParserTest/translated/ErrorsTests.swift @@ -279,7 +279,7 @@ final class ErrorsTests: ParserTestCase { assertParse( """ func incompleteThrowType() { - let _: () 1️⃣throws + let _: ()1️⃣ 2️⃣throws } """, substructure: CodeBlockSyntax( @@ -300,12 +300,18 @@ final class ErrorsTests: ParserTestCase { ) ) ) - ) - ]), - UnexpectedNodesSyntax([TokenSyntax.keyword(.throws)]) + ), + CodeBlockItemSyntax( + item: .decl( + DeclSyntax( + UnexpectedCodeDeclSyntax(unexpectedCode: UnexpectedNodesSyntax([TokenSyntax.keyword(.throws)])) + ) + ) + ), + ]) ), diagnostics: [ - DiagnosticSpec(message: "unexpected 'throws' keyword in function") + DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected 'throws' keyword in function") ] ) } diff --git a/Tests/SwiftParserTest/translated/EscapedIdentifiersTests.swift b/Tests/SwiftParserTest/translated/EscapedIdentifiersTests.swift index 939beb8b972..0427a76177c 100644 --- a/Tests/SwiftParserTest/translated/EscapedIdentifiersTests.swift +++ b/Tests/SwiftParserTest/translated/EscapedIdentifiersTests.swift @@ -205,14 +205,18 @@ final class EscapedIdentifiersTests: ParserTestCase { assertParse( """ 1️⃣`multiline is - not allowed` = 5 + not2️⃣ allowed3️⃣` = 5 """, diagnostics: [ - DiagnosticSpec( - locationMarker: "1️⃣", - message: "extraneous code at top level" - ) - ] + DiagnosticSpec(locationMarker: "1️⃣", message: "unexpected code '`multiline is' in source file"), + .consecutiveStatementsOnALineDiagnosticSpec(locationMarker: "2️⃣"), + DiagnosticSpec(locationMarker: "3️⃣", message: "unexpected code '` = 5' in source file"), + ], + fixedSource: """ + `multiline is + not + allowed` = 5 + """ ) } diff --git a/Tests/SwiftParserTest/translated/ForwardSlashRegexSkippingInvalidTests.swift b/Tests/SwiftParserTest/translated/ForwardSlashRegexSkippingInvalidTests.swift index 3959dc9d2bf..1c25a07b872 100644 --- a/Tests/SwiftParserTest/translated/ForwardSlashRegexSkippingInvalidTests.swift +++ b/Tests/SwiftParserTest/translated/ForwardSlashRegexSkippingInvalidTests.swift @@ -156,7 +156,7 @@ final class ForwardSlashRegexSkippingInvalidTests: ParserTestCase { 1️⃣} """, diagnostics: [ - DiagnosticSpec(message: "extraneous brace at top level") + DiagnosticSpec(message: "unexpected brace in source file") ] ) } @@ -322,7 +322,7 @@ final class ForwardSlashRegexSkippingInvalidTests: ParserTestCase { ), DiagnosticSpec( locationMarker: "3️⃣", - message: "extraneous brace at top level" + message: "unexpected brace in source file" ), ], applyFixIts: ["insert '/'"], @@ -365,11 +365,12 @@ final class ForwardSlashRegexSkippingInvalidTests: ParserTestCase { 0 /x}}1️⃣} / 2 - } + 2️⃣} } """, diagnostics: [ - DiagnosticSpec(message: "extraneous code at top level") + DiagnosticSpec(locationMarker: "1️⃣", message: "unexpected code '} /' in source file"), + DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected braces in source file"), ] ) } @@ -381,10 +382,11 @@ final class ForwardSlashRegexSkippingInvalidTests: ParserTestCase { _ = 2 /x} 1️⃣/ .bitWidth - } + 2️⃣} """, diagnostics: [ - DiagnosticSpec(message: "extraneous code at top level") + DiagnosticSpec(locationMarker: "1️⃣", message: "unexpected code '/' in source file"), + DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected brace in source file"), ] ) } diff --git a/Tests/SwiftParserTest/translated/ForwardSlashRegexTests.swift b/Tests/SwiftParserTest/translated/ForwardSlashRegexTests.swift index 25e94d39f4a..b916ad09c02 100644 --- a/Tests/SwiftParserTest/translated/ForwardSlashRegexTests.swift +++ b/Tests/SwiftParserTest/translated/ForwardSlashRegexTests.swift @@ -1423,7 +1423,7 @@ final class ForwardSlashRegexTests: ParserTestCase { _ = /\()1️⃣/ """#, diagnostics: [ - DiagnosticSpec(message: "extraneous code '/' at top level") + DiagnosticSpec(message: "unexpected code '/' in source file") ] ) } diff --git a/Tests/SwiftParserTest/translated/IfconfigExprTests.swift b/Tests/SwiftParserTest/translated/IfconfigExprTests.swift index 9307e79271e..423b586003e 100644 --- a/Tests/SwiftParserTest/translated/IfconfigExprTests.swift +++ b/Tests/SwiftParserTest/translated/IfconfigExprTests.swift @@ -105,7 +105,7 @@ final class IfconfigExprTests: ParserTestCase { } """#, diagnostics: [ - DiagnosticSpec(locationMarker: "1️⃣", message: "unexpected code '+ otherExpr' in conditional compilation block"), + DiagnosticSpec(locationMarker: "1️⃣", message: "unexpected code '+ otherExpr' in conditional compilation clause"), DiagnosticSpec( locationMarker: "2️⃣", message: #"unexpected code 'print("debug")' in conditional compilation block"# diff --git a/Tests/SwiftParserTest/translated/ModuleSelectorTests.swift b/Tests/SwiftParserTest/translated/ModuleSelectorTests.swift index 4e586e58953..5479ed1ea06 100644 --- a/Tests/SwiftParserTest/translated/ModuleSelectorTests.swift +++ b/Tests/SwiftParserTest/translated/ModuleSelectorTests.swift @@ -1116,7 +1116,7 @@ final class ModuleSelectorTests: ParserTestCase { """, diagnostics: [ DiagnosticSpec(message: "expected type in type annotation", fixIts: ["insert type"]), - DiagnosticSpec(message: "extraneous code '*::Int' at top level"), + DiagnosticSpec(message: "unexpected code '*::Int' in source file"), ], fixedSource: """ var c: <#type#>*::Int @@ -1303,7 +1303,7 @@ final class ModuleSelectorTests: ParserTestCase { ), DiagnosticSpec( locationMarker: "3️⃣", - message: "extraneous code ']' at top level" + message: "unexpected code ']' in source file" ), ], fixedSource: """ @@ -1344,7 +1344,7 @@ final class ModuleSelectorTests: ParserTestCase { var cIndex: String1️⃣.*::Index """, diagnostics: [ - DiagnosticSpec(message: "extraneous code '.*::Index' at top level") + DiagnosticSpec(message: "unexpected code '.*::Index' in source file") ] ) assertParse( @@ -1823,7 +1823,7 @@ final class ModuleSelectorTests: ParserTestCase { ), DiagnosticSpec( locationMarker: "2️⃣", - message: "extraneous code 'else { 0 }' at top level" + message: "unexpected code 'else { 0 }' in source file" ), ], fixedSource: """ diff --git a/Tests/SwiftParserTest/translated/MultilineErrorsTests.swift b/Tests/SwiftParserTest/translated/MultilineErrorsTests.swift index e9f57e14bac..56e6ffea3d7 100644 --- a/Tests/SwiftParserTest/translated/MultilineErrorsTests.swift +++ b/Tests/SwiftParserTest/translated/MultilineErrorsTests.swift @@ -563,7 +563,7 @@ final class MultilineErrorsTests: ParserTestCase { ), DiagnosticSpec( locationMarker: "4️⃣", - message: #"extraneous code ')!"' at top level"# + message: #"unexpected code ')!"' in source file"# ), ], fixedSource: ##""" @@ -597,7 +597,7 @@ final class MultilineErrorsTests: ParserTestCase { ), DiagnosticSpec( locationMarker: "2️⃣", - message: #"extraneous code ')!"' at top level"# + message: #"unexpected code ')!"' in source file"# ), ], fixedSource: ##""" diff --git a/Tests/SwiftParserTest/translated/OperatorDeclTests.swift b/Tests/SwiftParserTest/translated/OperatorDeclTests.swift index a8e79ba960f..2ddb23d46b1 100644 --- a/Tests/SwiftParserTest/translated/OperatorDeclTests.swift +++ b/Tests/SwiftParserTest/translated/OperatorDeclTests.swift @@ -214,7 +214,7 @@ final class OperatorDeclTests: ParserTestCase { prefix operator %%+ """, diagnostics: [ - DiagnosticSpec(message: "unexpected ';' separator", fixIts: ["remove ';'"]) + DiagnosticSpec(message: "standalone ';' statements are not allowed", fixIts: ["remove ';'"]) ], fixedSource: """ diff --git a/Tests/SwiftParserTest/translated/OptionalTests.swift b/Tests/SwiftParserTest/translated/OptionalTests.swift index f29ee9511b3..542021fb26d 100644 --- a/Tests/SwiftParserTest/translated/OptionalTests.swift +++ b/Tests/SwiftParserTest/translated/OptionalTests.swift @@ -33,7 +33,7 @@ final class OptionalTests: ParserTestCase { var b : A 1️⃣? """, diagnostics: [ - DiagnosticSpec(message: "extraneous code '?' at top level") + DiagnosticSpec(message: "unexpected code '?' in source file") ] ) } diff --git a/Tests/SwiftParserTest/translated/PoundAssertTests.swift b/Tests/SwiftParserTest/translated/PoundAssertTests.swift index f2b57c8b173..4ee9fa6a2fc 100644 --- a/Tests/SwiftParserTest/translated/PoundAssertTests.swift +++ b/Tests/SwiftParserTest/translated/PoundAssertTests.swift @@ -41,7 +41,7 @@ final class PoundAssertTests: ParserTestCase { message: "consecutive statements on a line must be separated by newline or ';'", fixIts: ["insert newline", "insert ';'"] ), - DiagnosticSpec(locationMarker: "2️⃣", message: #"extraneous code ', "error message")' at top level"#), + DiagnosticSpec(locationMarker: "2️⃣", message: #"unexpected code ', "error message")' in source file"#), ], applyFixIts: ["insert newline"], fixedSource: #""" @@ -61,7 +61,7 @@ final class PoundAssertTests: ParserTestCase { message: "consecutive statements on a line must be separated by newline or ';'", fixIts: ["insert newline", "insert ';'"] ), - DiagnosticSpec(locationMarker: "2️⃣", message: #"extraneous code ', "error message")' at top level"#), + DiagnosticSpec(locationMarker: "2️⃣", message: #"unexpected code ', "error message")' in source file"#), ], applyFixIts: ["insert ';'"], fixedSource: #""" diff --git a/Tests/SwiftParserTest/translated/RecoveryLibraryTests.swift b/Tests/SwiftParserTest/translated/RecoveryLibraryTests.swift index a6f511902ab..e9311734db4 100644 --- a/Tests/SwiftParserTest/translated/RecoveryLibraryTests.swift +++ b/Tests/SwiftParserTest/translated/RecoveryLibraryTests.swift @@ -35,15 +35,15 @@ final class RecoveryLibraryTests: ParserTestCase { diagnostics: [ DiagnosticSpec( locationMarker: "1️⃣", - message: "unexpected braces before function", + message: "unexpected braces in source file", highlight: """ // Check that we handle multiple consecutive right braces. } } """ ), - DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected braces before function"), - DiagnosticSpec(locationMarker: "3️⃣", message: "extraneous braces at top level"), + DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected braces in source file"), + DiagnosticSpec(locationMarker: "3️⃣", message: "unexpected braces in source file"), ] ) } diff --git a/Tests/SwiftParserTest/translated/RecoveryTests.swift b/Tests/SwiftParserTest/translated/RecoveryTests.swift index aba6d85f9ea..821fc58ff97 100644 --- a/Tests/SwiftParserTest/translated/RecoveryTests.swift +++ b/Tests/SwiftParserTest/translated/RecoveryTests.swift @@ -28,7 +28,7 @@ final class RecoveryTests: ParserTestCase { diagnostics: [ DiagnosticSpec( message: - "unexpected code ') this line is invalid, but we will stop at the keyword below...' before 'return' statement" + "unexpected code ') this line is invalid, but we will stop at the keyword below...' in function" ) ] ) @@ -45,7 +45,7 @@ final class RecoveryTests: ParserTestCase { """#, diagnostics: [ DiagnosticSpec( - message: "unexpected code ') this line is invalid, but we will stop at the declaration...' before function" + message: "unexpected code ') this line is invalid, but we will stop at the declaration...' in function" ) ] ) @@ -93,7 +93,7 @@ final class RecoveryTests: ParserTestCase { ), DiagnosticSpec( locationMarker: "4️⃣", - message: "unexpected code in function" + message: "unexpected code ', b : Int' in function" ), ], applyFixIts: ["insert '>'", "insert expression"], @@ -140,7 +140,7 @@ final class RecoveryTests: ParserTestCase { diagnostics: [ DiagnosticSpec( locationMarker: "1️⃣", - message: "unexpected brace before function" + message: "unexpected brace in source file" ), DiagnosticSpec( locationMarker: "2️⃣", @@ -174,7 +174,7 @@ final class RecoveryTests: ParserTestCase { diagnostics: [ DiagnosticSpec( locationMarker: "1️⃣", - message: "unexpected brace before function" + message: "unexpected brace in source file" ), DiagnosticSpec( locationMarker: "2️⃣", @@ -996,29 +996,27 @@ final class RecoveryTests: ParserTestCase { extension NoBracesStruct14️⃣() """, diagnostics: [ - DiagnosticSpec(locationMarker: "1️⃣", message: "expected '{' in enum", fixIts: ["insert '{'"]), - DiagnosticSpec(locationMarker: "1️⃣", message: "unexpected code '()' before class"), - DiagnosticSpec(locationMarker: "2️⃣", message: "expected '{' in class", fixIts: ["insert '{'"]), - DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected code '()' before protocol"), - DiagnosticSpec(locationMarker: "3️⃣", message: "expected '{' in protocol", fixIts: ["insert '{'"]), - DiagnosticSpec(locationMarker: "3️⃣", message: "unexpected code '()' before extension"), + DiagnosticSpec(locationMarker: "1️⃣", message: "expected member block in enum", fixIts: ["insert member block"]), + DiagnosticSpec(locationMarker: "2️⃣", message: "expected member block in class", fixIts: ["insert member block"]), + DiagnosticSpec( + locationMarker: "3️⃣", + message: "expected member block in protocol", + fixIts: ["insert member block"] + ), DiagnosticSpec( locationMarker: "4️⃣", message: "expected member block in extension", fixIts: ["insert member block"] ), - DiagnosticSpec(locationMarker: "4️⃣", message: "expected '}' to end protocol", fixIts: ["insert '}'"]), - DiagnosticSpec(locationMarker: "4️⃣", message: "expected '}' to end class", fixIts: ["insert '}'"]), - DiagnosticSpec(locationMarker: "4️⃣", message: "expected '}' to end enum", fixIts: ["insert '}'"]), ], fixedSource: """ - enum NoBracesUnion1 {() - class NoBracesClass1 {() - protocol NoBracesProtocol1 {() + enum NoBracesUnion1 { + }() + class NoBracesClass1 { + }() + protocol NoBracesProtocol1 { + }() extension NoBracesStruct1 { - } - } - } }() """ ) @@ -1034,30 +1032,34 @@ final class RecoveryTests: ParserTestCase { extension NoBracesStruct25️⃣ """, diagnostics: [ - DiagnosticSpec(locationMarker: "1️⃣", message: "expected '{' in struct", fixIts: ["insert '{'"]), - DiagnosticSpec(locationMarker: "2️⃣", message: "expected '{' in enum", fixIts: ["insert '{'"]), - DiagnosticSpec(locationMarker: "3️⃣", message: "expected '{' in class", fixIts: ["insert '{'"]), - DiagnosticSpec(locationMarker: "4️⃣", message: "expected '{' in protocol", fixIts: ["insert '{'"]), + DiagnosticSpec( + locationMarker: "1️⃣", + message: "expected member block in struct", + fixIts: ["insert member block"] + ), + DiagnosticSpec(locationMarker: "2️⃣", message: "expected member block in enum", fixIts: ["insert member block"]), + DiagnosticSpec(locationMarker: "3️⃣", message: "expected member block in class", fixIts: ["insert member block"]), + DiagnosticSpec( + locationMarker: "4️⃣", + message: "expected member block in protocol", + fixIts: ["insert member block"] + ), DiagnosticSpec( locationMarker: "5️⃣", message: "expected member block in extension", fixIts: ["insert member block"] ), - DiagnosticSpec(locationMarker: "5️⃣", message: "expected '}' to end protocol", fixIts: ["insert '}'"]), - DiagnosticSpec(locationMarker: "5️⃣", message: "expected '}' to end class", fixIts: ["insert '}'"]), - DiagnosticSpec(locationMarker: "5️⃣", message: "expected '}' to end enum", fixIts: ["insert '}'"]), - DiagnosticSpec(locationMarker: "5️⃣", message: "expected '}' to end struct", fixIts: ["insert '}'"]), ], fixedSource: """ struct NoBracesStruct2 { - enum NoBracesUnion2 { - class NoBracesClass2 { - protocol NoBracesProtocol2 { - extension NoBracesStruct2 { } + enum NoBracesUnion2 { } + class NoBracesClass2 { } + protocol NoBracesProtocol2 { } + extension NoBracesStruct2 { } """ ) @@ -1112,7 +1114,7 @@ final class RecoveryTests: ParserTestCase { assertParse( """ enum EE 1️⃣EE where T : Multi { - case a2️⃣ 3️⃣a + case a 2️⃣a case b } """, @@ -1122,18 +1124,12 @@ final class RecoveryTests: ParserTestCase { message: "found an unexpected second identifier in enum; is there an accidental break?", fixIts: ["join the identifiers together"] ), - DiagnosticSpec( - locationMarker: "2️⃣", - message: "consecutive declarations on a line must be separated by newline or ';'", - fixIts: ["insert newline", "insert ';'"] - ), - DiagnosticSpec(locationMarker: "3️⃣", message: "unexpected code 'a' before enum case"), + DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected code 'a' in enum"), ], applyFixIts: ["join the identifiers together", "insert newline"], fixedSource: """ enum EEEE where T : Multi { - case a - a + case a a case b } """ @@ -1144,7 +1140,7 @@ final class RecoveryTests: ParserTestCase { assertParse( """ enum EE 1️⃣EE where T : Multi { - case a2️⃣ 3️⃣a + case a 2️⃣a case b } """, @@ -1154,17 +1150,11 @@ final class RecoveryTests: ParserTestCase { message: "found an unexpected second identifier in enum; is there an accidental break?", fixIts: ["join the identifiers together"] ), - DiagnosticSpec( - locationMarker: "2️⃣", - message: "consecutive declarations on a line must be separated by newline or ';'", - fixIts: ["insert newline", "insert ';'"] - ), - DiagnosticSpec(locationMarker: "3️⃣", message: "unexpected code 'a' before enum case"), + DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected code 'a' in enum"), ], - applyFixIts: ["join the identifiers together", "insert ';'"], fixedSource: """ enum EEEE where T : Multi { - case a;a + case a a case b } """ @@ -1733,7 +1723,7 @@ final class RecoveryTests: ParserTestCase { """, diagnostics: [ DiagnosticSpec( - message: "extraneous code ']' at top level" + message: "unexpected code ']' in source file" ) ] ) @@ -1750,7 +1740,7 @@ final class RecoveryTests: ParserTestCase { notes: [NoteSpec(message: "to match this opening '<'")], fixIts: ["insert '>'"] ), - DiagnosticSpec(message: "extraneous code ']>' at top level"), + DiagnosticSpec(message: "unexpected code ']>' in source file"), ], fixedSource: """ let a2: Set]> @@ -1772,12 +1762,7 @@ final class RecoveryTests: ParserTestCase { diagnostics: [ DiagnosticSpec( locationMarker: "1️⃣", - message: "consecutive declarations on a line must be separated by newline or ';'", - fixIts: ["insert newline", "insert ';'"] - ), - DiagnosticSpec( - locationMarker: "1️⃣", - message: "unexpected code ':' before variable" + message: "unexpected code ':' in struct" ), DiagnosticSpec( locationMarker: "2️⃣", @@ -1812,8 +1797,7 @@ final class RecoveryTests: ParserTestCase { ], fixedSource: """ struct ErrorTypeInVarDeclDictionaryType { - let a1: String - : + let a1: String: let a2: [String: Int] let a3: [String: [Int]] let a4: [String: Int] @@ -1829,7 +1813,7 @@ final class RecoveryTests: ParserTestCase { """, diagnostics: [ DiagnosticSpec( - message: "extraneous code ']' at top level" + message: "unexpected code ']' in source file" ) ] ) @@ -2076,7 +2060,7 @@ final class RecoveryTests: ParserTestCase { } """, diagnostics: [ - DiagnosticSpec(message: "unexpected code before typealias declaration") + DiagnosticSpec(message: "unexpected code in struct") ] ) } @@ -2430,7 +2414,7 @@ final class RecoveryTests: ParserTestCase { ), DiagnosticSpec( locationMarker: "2️⃣", - message: "unexpected code '> {}' in 'switch' statement" + message: "unexpected code '> {}' in switch case" ), ], fixedSource: """ @@ -2502,7 +2486,7 @@ final class RecoveryTests: ParserTestCase { ), DiagnosticSpec( locationMarker: "4️⃣", - message: "unexpected code ')}' before struct" + message: "unexpected code ')}' in source file" ), DiagnosticSpec( locationMarker: "5️⃣", @@ -2597,7 +2581,7 @@ final class RecoveryTests: ParserTestCase { } """, diagnostics: [ - DiagnosticSpec(message: "extraneous code at top level") + DiagnosticSpec(message: "unexpected code in source file") ] ) } @@ -2932,7 +2916,7 @@ final class RecoveryTests: ParserTestCase { ), DiagnosticSpec( locationMarker: "4️⃣", - message: "extraneous brace at top level" + message: "unexpected brace in source file" ), ], applyFixIts: ["remove operator body", "insert newline"], @@ -2969,7 +2953,7 @@ final class RecoveryTests: ParserTestCase { ), DiagnosticSpec( locationMarker: "4️⃣", - message: "extraneous brace at top level" + message: "unexpected brace in source file" ), ], applyFixIts: ["remove operator body", "insert ';'"], @@ -3251,8 +3235,197 @@ final class RecoveryTests: ParserTestCase { assertParse( "func foo() -> Int1️⃣:", diagnostics: [ - DiagnosticSpec(message: "extraneous code ':' at top level") + DiagnosticSpec(message: "unexpected code ':' in source file") ] ) } + + func testStatementAfterAttribute() { + assertParse( + """ + @attr1️⃣ + guard foo else {} + struct S {} + """, + substructure: CodeBlockItemListSyntax([ + CodeBlockItemSyntax( + item: .decl( + DeclSyntax( + MissingDeclSyntax( + attributes: [ + .attribute( + AttributeSyntax( + atSign: .atSignToken(), + attributeName: IdentifierTypeSyntax(name: .identifier("attr")) + ) + ) + ], + modifiers: [], + placeholder: .identifier("<#declaration#>", presence: .missing) + ) + ) + ) + ), + CodeBlockItemSyntax( + item: .init( + GuardStmtSyntax( + guardKeyword: .keyword(.guard), + conditions: ConditionElementListSyntax([ + ConditionElementSyntax( + condition: ConditionElementSyntax.Condition(DeclReferenceExprSyntax(baseName: .identifier("foo"))) + ) + ]), + elseKeyword: .keyword(.else), + body: CodeBlockSyntax( + leftBrace: .leftBraceToken(), + statements: CodeBlockItemListSyntax([]), + rightBrace: .rightBraceToken() + ) + ) + ) + ), + CodeBlockItemSyntax( + item: .init( + StructDeclSyntax( + attributes: AttributeListSyntax([]), + modifiers: DeclModifierListSyntax([]), + structKeyword: .keyword(.struct), + name: .identifier("S"), + memberBlock: MemberBlockSyntax( + leftBrace: .leftBraceToken(), + members: MemberBlockItemListSyntax([]), + rightBrace: .rightBraceToken() + ) + ) + ) + ), + ]), + diagnostics: [ + DiagnosticSpec(message: "expected declaration after attribute", fixIts: ["insert declaration"]) + ], + fixedSource: """ + @attr <#declaration#> + guard foo else {} + struct S {} + """ + ) + } + + func testUnexpectedBeforeAttributeInMemberBlock() { + assertParse( + """ + struct S { + 1️⃣do {} + @attr func foo() + } + """, + substructure: StructDeclSyntax( + name: .identifier("S"), + memberBlock: MemberBlockSyntax( + members: [ + MemberBlockItemSyntax( + decl: UnexpectedCodeDeclSyntax( + unexpectedCode: UnexpectedNodesSyntax([ + TokenSyntax.keyword(.do), + TokenSyntax.leftBraceToken(), + TokenSyntax.rightBraceToken(), + ]) + ) + ), + MemberBlockItemSyntax( + decl: FunctionDeclSyntax( + attributes: [ + .attribute(AttributeSyntax(attributeName: IdentifierTypeSyntax(name: .identifier("attr")))) + ], + name: .identifier("foo"), + signature: FunctionSignatureSyntax( + parameterClause: FunctionParameterClauseSyntax( + leftParen: .leftParenToken(), + parameters: [], + rightParen: .rightParenToken() + ) + ) + ) + ), + ] + ) + ), + diagnostics: [ + DiagnosticSpec(message: "unexpected code 'do {}' in struct") + ] + ) + } + + func testAttrInCodeBlock() { + assertParse( + """ + func foo() { + @attr1️⃣ + } + struct S {} + """, + substructure: CodeBlockItemListSyntax([ + CodeBlockItemSyntax( + item: .init( + FunctionDeclSyntax( + funcKeyword: .keyword(.func), + name: .identifier("foo"), + signature: FunctionSignatureSyntax( + parameterClause: FunctionParameterClauseSyntax( + leftParen: .leftParenToken(), + parameters: FunctionParameterListSyntax([]), + rightParen: .rightParenToken() + ) + ), + body: CodeBlockSyntax( + leftBrace: .leftBraceToken(), + statements: CodeBlockItemListSyntax([ + CodeBlockItemSyntax( + item: .init( + MissingDeclSyntax( + attributes: AttributeListSyntax([ + .attribute( + AttributeSyntax( + atSign: .atSignToken(), + attributeName: TypeSyntax(IdentifierTypeSyntax(name: .identifier("attr"))) + ) + ) + ]), + placeholder: .identifier("<#declaration#>", presence: .missing) + ) + ) + ) + ]), + rightBrace: .rightBraceToken() + ) + ) + ) + ), + CodeBlockItemSyntax( + item: CodeBlockItemSyntax.Item( + StructDeclSyntax( + structKeyword: .keyword(.struct), + name: .identifier("S"), + memberBlock: MemberBlockSyntax( + leftBrace: .leftBraceToken(), + members: MemberBlockItemListSyntax([]), + rightBrace: .rightBraceToken() + ) + ) + ) + ), + ]), + diagnostics: [ + // FIXME: expected *declaration* after attribute + DiagnosticSpec(message: "expected statements after attribute", fixIts: ["insert statements"]) + ], + fixedSource: """ + func foo() { + @attr <#declaration#> + } + struct S {} + """ + ) + } + } diff --git a/Tests/SwiftParserTest/translated/TypealiasTests.swift b/Tests/SwiftParserTest/translated/TypealiasTests.swift index 05e769667ac..32ebbb88e5a 100644 --- a/Tests/SwiftParserTest/translated/TypealiasTests.swift +++ b/Tests/SwiftParserTest/translated/TypealiasTests.swift @@ -168,7 +168,7 @@ final class TypealiasTests: ParserTestCase { message: "expected '=' in typealias declaration", fixIts: ["replace ':' with '='"] ), - DiagnosticSpec(locationMarker: "2️⃣", message: "extraneous code ', Float' at top level"), + DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected code ', Float' in source file"), ], fixedSource: """ typealias Recovery5 = Int, Float @@ -183,7 +183,7 @@ final class TypealiasTests: ParserTestCase { """, diagnostics: [ DiagnosticSpec(message: "expected type in typealias declaration", fixIts: ["insert type"]), - DiagnosticSpec(message: "extraneous code '=' at top level"), + DiagnosticSpec(message: "unexpected code '=' in source file"), ], fixedSource: """ typealias Recovery6 = <#type#>= From 41286de40279fd0e3bcd7db00f6f89ea88cbb74a Mon Sep 17 00:00:00 2001 From: Rintaro Ishizaki Date: Thu, 25 Sep 2025 16:34:17 -0700 Subject: [PATCH 02/17] [SwiftParser] Improve '#if' recovery --- Sources/SwiftParser/Attributes.swift | 27 ++- Sources/SwiftParser/CompilerFiles.swift | 2 +- Sources/SwiftParser/Declarations.swift | 182 ++++++------------ Sources/SwiftParser/Directives.swift | 23 +-- Sources/SwiftParser/Expressions.swift | 28 +-- Sources/SwiftParser/Statements.swift | 21 +- Sources/SwiftParser/TopLevel.swift | 155 ++++++++------- .../generated/LayoutNodes+Parsable.swift | 2 +- Tests/SwiftParserTest/DirectiveTests.swift | 34 ++++ 9 files changed, 219 insertions(+), 255 deletions(-) diff --git a/Sources/SwiftParser/Attributes.swift b/Sources/SwiftParser/Attributes.swift index 27cc1b80363..944ae2fd7b1 100644 --- a/Sources/SwiftParser/Attributes.swift +++ b/Sources/SwiftParser/Attributes.swift @@ -33,10 +33,8 @@ extension Parser { // // In such cases, the second `#if` is not `consumeIfConfigOfAttributes()`. return .ifConfigDecl( - self.parsePoundIfDirective { (parser, _) -> RawAttributeListSyntax.Element in - return parser.parseAttributeListElement() - } syntax: { parser, attributes in - return .attributes(RawAttributeListSyntax(elements: attributes, arena: parser.arena)) + self.parsePoundIfDirective { parser in + return .attributes(parser.parseAttributeList()) } ) } else { @@ -896,7 +894,26 @@ extension Parser { return makeMissingProviderArguments(unexpectedBefore: []) } - let decl = parseDeclaration(in: .argumentList) + let decl: RawDeclSyntax + if self.at(.poundIf) { + let ifConfig = self.parsePoundIfDirective({ parser in + let decl = parser.parseDeclaration(in: .argumentList) + let member = RawMemberBlockItemSyntax(decl: decl, semicolon: nil, arena: parser.arena) + return .decls(RawMemberBlockItemListSyntax(elements: [member], arena: parser.arena)) + }) + decl = ifConfig.makeUnexpectedKeepingFirstNode( + of: RawDeclSyntax.self, arena: self.arena, + where: { !$0.is(RawIfConfigDeclSyntax.self) }, + makeMissing: { + RawDeclSyntax(RawMissingDeclSyntax( + attributes: self.emptyCollection(RawAttributeListSyntax.self), + modifiers: self.emptyCollection(RawDeclModifierListSyntax.self), + arena: self.arena) + ) + }) + } else { + decl = self.parseDeclaration(in: .argumentList) + } guard let provider = RawABIAttributeArgumentsSyntax.Provider(decl) else { return makeMissingProviderArguments(unexpectedBefore: [decl.raw]) diff --git a/Sources/SwiftParser/CompilerFiles.swift b/Sources/SwiftParser/CompilerFiles.swift index dbaea1a1902..2424c7964a6 100644 --- a/Sources/SwiftParser/CompilerFiles.swift +++ b/Sources/SwiftParser/CompilerFiles.swift @@ -89,7 +89,7 @@ extension Parser { /// Parse a declaration macro expansions in type contexts. mutating func parseMemberBlockItemListFile() -> RawMemberBlockItemListFileSyntax { - let members = self.parseMemberDeclList() + let members = self.parseMemberDeclList(until: { _ in false }) let (unexpectedBeforeEndOfFileToken, endOfFile) = self.expectEndOfFile() return RawMemberBlockItemListFileSyntax( diff --git a/Sources/SwiftParser/Declarations.swift b/Sources/SwiftParser/Declarations.swift index 325bc07bdd9..f16357708b7 100644 --- a/Sources/SwiftParser/Declarations.swift +++ b/Sources/SwiftParser/Declarations.swift @@ -53,15 +53,9 @@ extension TokenConsumer { } mutating func atStartOfDeclaration( - isAtTopLevel: Bool = false, allowInitDecl: Bool = true, - allowRecovery: Bool = false, - requiresDecl: Bool = false, + requiresDecl: Bool = false ) -> Bool { - if self.at(.poundIf) { - return true - } - var subparser = self.lookahead() var hasAttribute = false @@ -71,7 +65,6 @@ extension TokenConsumer { _ = subparser.consumeAttributeList() hasAttribute = true } else if subparser.at(.poundIf) && subparser.consumeIfConfigOfAttributes() { - subparser.skipSingle() hasAttribute = true } else { break @@ -106,17 +99,7 @@ extension TokenConsumer { } } - let declStartKeyword: DeclarationKeyword? - if allowRecovery { - declStartKeyword = - subparser.canRecoverTo( - anyIn: DeclarationKeyword.self, - overrideRecoveryPrecedence: isAtTopLevel ? nil : .closingBrace - )?.0 - } else { - declStartKeyword = subparser.at(anyIn: DeclarationKeyword.self)?.0 - } - switch declStartKeyword { + switch subparser.at(anyIn: DeclarationKeyword.self)?.0 { case .lhs(.actor): // actor Foo {} if subparser.peek().rawTokenKind == .identifier { @@ -128,7 +111,7 @@ extension TokenConsumer { var lookahead = subparser.lookahead() repeat { lookahead.consumeAnyToken() - } while lookahead.atStartOfDeclaration(isAtTopLevel: isAtTopLevel, allowInitDecl: allowInitDecl) + } while lookahead.atStartOfDeclaration(allowInitDecl: allowInitDecl) return lookahead.at(.identifier) case .lhs(.case): // When 'case' appears inside a function, it's probably a switch @@ -177,9 +160,8 @@ extension TokenConsumer { if subparser.at(anyIn: ContextualDeclKeyword.self)?.0 != nil { subparser.consumeAnyToken() return subparser.atStartOfDeclaration( - isAtTopLevel: isAtTopLevel, allowInitDecl: allowInitDecl, - allowRecovery: allowRecovery + requiresDecl: requiresDecl ) } if requiresDecl { @@ -278,50 +260,6 @@ extension Parser { /// - Parameter context: Describes the code around the declaration being parsed. This affects how the parser tries /// to recover from malformed syntax in the declaration. mutating func parseDeclaration(in context: DeclarationParseContext = .topLevelOrCodeBlock) -> RawDeclSyntax { - // If we are at a `#if` of attributes, the `#if` directive should be - // parsed when we're parsing the attributes. - if self.at(.poundIf) && !self.withLookahead({ $0.consumeIfConfigOfAttributes() }) { - let directive = self.parsePoundIfDirective { (parser, _) in - let parsedDecl = parser.parseDeclaration() - let semicolon = parser.consume(if: .semicolon) - return RawMemberBlockItemSyntax( - decl: parsedDecl, - semicolon: semicolon, - arena: parser.arena - ) - } addSemicolonIfNeeded: { lastElement, newItemAtStartOfLine, newItem, parser in - if lastElement.semicolon == nil && !newItemAtStartOfLine && !newItem.decl.is(RawUnexpectedCodeDeclSyntax.self) { - return RawMemberBlockItemSyntax( - lastElement.unexpectedBeforeDecl, - decl: lastElement.decl, - lastElement.unexpectedBetweenDeclAndSemicolon, - semicolon: parser.missingToken(.semicolon), - lastElement.unexpectedAfterSemicolon, - arena: parser.arena - ) - } else { - return nil - } - } syntax: { parser, elements in - return .decls(RawMemberBlockItemListSyntax(elements: elements, arena: parser.arena)) - } - if !context.allowsIfConfigDecl { - // Convert the IfConfig to unexpected syntax around the first decl inside it, if any. - return directive.makeUnexpectedKeepingFirstNode(of: RawDeclSyntax.self, arena: self.arena) { node in - return !node.is(RawIfConfigDeclSyntax.self) - } makeMissing: { - return RawDeclSyntax( - RawMissingDeclSyntax( - attributes: self.emptyCollection(RawAttributeListSyntax.self), - modifiers: self.emptyCollection(RawDeclModifierListSyntax.self), - arena: self.arena - ) - ) - } - } - return RawDeclSyntax(directive) - } - let attrs = DeclAttributes( attributes: self.parseAttributeList(), modifiers: self.parseDeclModifierList() @@ -992,7 +930,7 @@ extension Parser { } extension Parser { - mutating func parseMemberBlockItem() -> RawMemberBlockItemSyntax? { + mutating func parseMemberBlockItem(until stopCondition: (inout Parser) -> Bool) -> RawMemberBlockItemSyntax? { let startToken = self.currentToken if let syntax = self.loadCurrentSyntaxNodeFromCache(for: .memberBlockItem) { self.registerNodeForIncrementalParse(node: syntax.raw, startToken: startToken) @@ -1013,35 +951,47 @@ extension Parser { } let decl: RawDeclSyntax + let attachSemi: Bool if self.at(.poundSourceLocation) { decl = RawDeclSyntax(self.parsePoundSourceLocationDirective()) - } else if self.atStartOfDeclaration(isAtTopLevel: false, allowInitDecl: true, requiresDecl: true) { + attachSemi = false + } else if self.at(.poundIf) && !self.withLookahead({ $0.consumeIfConfigOfAttributes() }) { + decl = RawDeclSyntax(self.parsePoundIfDirective { parser in + return .decls(parser.parseMemberDeclList(until: {$0.atEndOfIfConfigClauseBody()})) + }) + attachSemi = false + } else if self.atStartOfDeclaration(allowInitDecl: true, requiresDecl: true) { decl = self.parseDeclaration(in: .memberDeclList) + attachSemi = true } else { decl = RawDeclSyntax( self.parseUnexpectedCodeDeclaration( - isAtTopLevel: false, allowInitDecl: true, requiresDecl: true, - skipToDeclOnly: true + skipToDeclOnly: true, + until: stopCondition ) ) - } - - if decl.isEmpty && !self.at(.semicolon) { - return nil + attachSemi = true } let semi: RawTokenSyntax? - if !decl.isEmpty { - semi = self.consume(if: .semicolon) + var trailingSemis: [RawTokenSyntax] = [] + if attachSemi { + if !decl.isEmpty { + semi = self.consume(if: .semicolon) + } else { + semi = nil + } + while let trailingSemi = self.consume(if: .semicolon) { + trailingSemis.append(trailingSemi) + } } else { - // orphan ';' case. Put it to "unexpected" nodes. semi = nil } - var trailingSemis: [RawTokenSyntax] = [] - while let trailingSemi = self.consume(if: .semicolon) { - trailingSemis.append(trailingSemi) + + if decl.isEmpty && semi == nil && trailingSemis.isEmpty { + return nil } let result = RawMemberBlockItemSyntax( @@ -1056,13 +1006,13 @@ extension Parser { return result } - mutating func parseMemberDeclList() -> RawMemberBlockItemListSyntax { + mutating func parseMemberDeclList(until stopCondition: (inout Parser) -> Bool = { $0.at(.rightBrace) }) -> RawMemberBlockItemListSyntax { var elements = [RawMemberBlockItemSyntax]() do { var loopProgress = LoopProgressCondition() - while !self.at(.endOfFile, .rightBrace, .poundEndif) && self.hasProgressed(&loopProgress) { + while !self.at(.endOfFile) && !stopCondition(&self) && self.hasProgressed(&loopProgress) { let newItemAtStartOfLine = self.atStartOfLine - guard let newElement = self.parseMemberBlockItem() else { + guard let newElement = self.parseMemberBlockItem(until: stopCondition) else { break } if let lastItem = elements.last, @@ -2380,52 +2330,42 @@ extension Parser { ) } + /// Eat tokens until a start of decl, or if `skipToDeclOnly` is not set until + /// a start of statement or expression. + /// Returns consumed tokens as a `RawUnexpectedCodeDeclSyntax` declaration. mutating func parseUnexpectedCodeDeclaration( - isAtTopLevel: Bool, allowInitDecl: Bool, requiresDecl: Bool, skipToDeclOnly: Bool, + until stopCondition: (inout Parser) -> Bool ) -> RawUnexpectedCodeDeclSyntax { - let numTokensToSkip = withLookahead { (lookahead) -> Int in - while !lookahead.at(.endOfFile, .semicolon) { - if lookahead.at(.poundElse, .poundElseif, .poundEndif) { - break; - } - if !isAtTopLevel && lookahead.at(.rightBrace) { - break; - } - lookahead.skipSingle() + var unexpectedTokens = [RawSyntax]() + while !self.at(.endOfFile, .semicolon) && !stopCondition(&self) { + let numTokensToSkip = self.withLookahead({ $0.skipSingle(); return $0.tokensConsumed }) + for _ in 0.. Bool { + return self.at(.poundElseif, .poundElse, .poundEndif) || self.atElifTypo() + } + private enum IfConfigContinuationClauseStartKeyword: TokenSpecSet { case poundElseif case poundElse @@ -56,16 +60,8 @@ extension Parser { /// previous element. /// - syntax: A function that aggregates the parsed conditional elements /// into a syntax collection. - mutating func parsePoundIfDirective( - _ parseElement: (_ parser: inout Parser, _ isFirstElement: Bool) -> Element?, - addSemicolonIfNeeded: - (_ lastElement: Element, _ newItemAtStartOfLine: Bool, _ newItem: Element, _ parser: inout Parser) -> Element? = { - _, - _, - _, - _ in nil - }, - syntax: (inout Parser, [Element]) -> RawIfConfigClauseSyntax.Elements? + mutating func parsePoundIfDirective( + _ parseBody: (_ parser: inout Parser) -> RawIfConfigClauseSyntax.Elements? ) -> RawIfConfigDeclSyntax { if let remainingTokens = remainingTokensIfMaximumNestingLevelReached() { return RawIfConfigDeclSyntax( @@ -89,7 +85,7 @@ extension Parser { poundKeyword: poundIf, condition: condition, unexpectedBetweenConditionAndElements, - elements: syntax(&self, parseIfConfigClauseElements(parseElement, addSemicolonIfNeeded: addSemicolonIfNeeded)), + elements: parseBody(&self), arena: self.arena ) ) @@ -150,10 +146,7 @@ extension Parser { poundKeyword: pound, condition: condition, unexpectedBetweenConditionAndElements, - elements: syntax( - &self, - parseIfConfigClauseElements(parseElement, addSemicolonIfNeeded: addSemicolonIfNeeded) - ), + elements: parseBody(&self), arena: self.arena ) ) diff --git a/Sources/SwiftParser/Expressions.swift b/Sources/SwiftParser/Expressions.swift index 58748166af9..bef812d1906 100644 --- a/Sources/SwiftParser/Expressions.swift +++ b/Sources/SwiftParser/Expressions.swift @@ -720,10 +720,7 @@ extension Parser { ) -> RawExprSyntax { precondition(self.at(.poundIf)) - let config = self.parsePoundIfDirective { (parser, isFirstElement) -> RawExprSyntax? in - if !isFirstElement { - return nil - } + let config = self.parsePoundIfDirective { parser in let head: RawExprSyntax if parser.at(.period) { head = parser.parseDottedExpressionSuffix(nil) @@ -738,15 +735,7 @@ extension Parser { flavor: flavor, pattern: .none ) - - // TODO: diagnose and skip the remaining token in the current clause. - return result - } syntax: { (parser, elements) -> RawIfConfigClauseSyntax.Elements? in - switch elements.count { - case 0: return nil - case 1: return .postfixExpression(elements.first!) - default: fatalError("Postfix #if should only have one element") - } + return .postfixExpression(result) } return RawExprSyntax( @@ -2352,16 +2341,9 @@ extension Parser { // clauses. elements.append( .ifConfigDecl( - self.parsePoundIfDirective( - { (parser, _) in parser.parseSwitchCases(allowStandaloneStmtRecovery: allowStandaloneStmtRecovery) }, - syntax: { parser, cases in - guard cases.count == 1, let firstCase = cases.first else { - precondition(cases.isEmpty) - return .switchCases(RawSwitchCaseListSyntax(elements: [], arena: parser.arena)) - } - return .switchCases(firstCase) - } - ) + self.parsePoundIfDirective({ parser in + .switchCases(parser.parseSwitchCases(allowStandaloneStmtRecovery: allowStandaloneStmtRecovery)) + }) ) ) } else if allowStandaloneStmtRecovery diff --git a/Sources/SwiftParser/Statements.swift b/Sources/SwiftParser/Statements.swift index 16affb4ac11..8426150eb87 100644 --- a/Sources/SwiftParser/Statements.swift +++ b/Sources/SwiftParser/Statements.swift @@ -28,14 +28,9 @@ extension TokenConsumer { /// /// - Note: This function must be kept in sync with `parseStatement()`. /// - Seealso: ``Parser/parseStatement()`` - func atStartOfStatement(allowRecovery: Bool = false, preferExpr: Bool) -> Bool { + func atStartOfStatement(preferExpr: Bool) -> Bool { var lookahead = self.lookahead() - if allowRecovery { - // Attributes are not allowed on statements. But for recovery, skip over - // misplaced attributes. - _ = lookahead.consumeAttributeList() - } - return lookahead.atStartOfStatement(allowRecovery: allowRecovery, preferExpr: preferExpr) + return lookahead.atStartOfStatement(preferExpr: preferExpr) } } @@ -1041,7 +1036,7 @@ extension Parser.Lookahead { /// /// - Note: This function must be kept in sync with `parseStatement()`. /// - Seealso: ``Parser/parseStatement()`` - mutating func atStartOfStatement(allowRecovery: Bool = false, preferExpr: Bool) -> Bool { + mutating func atStartOfStatement(preferExpr: Bool) -> Bool { if (self.at(anyIn: SwitchCaseStart.self) != nil || self.at(.atSign)) && withLookahead({ $0.atStartOfSwitchCaseItem() }) { @@ -1050,13 +1045,7 @@ extension Parser.Lookahead { } _ = self.consume(if: .identifier, followedBy: .colon) - let switchSubject: CanBeStatementStart? - if allowRecovery { - switchSubject = self.canRecoverTo(anyIn: CanBeStatementStart.self)?.0 - } else { - switchSubject = self.at(anyIn: CanBeStatementStart.self)?.0 - } - switch switchSubject { + switch self.at(anyIn: CanBeStatementStart.self)?.0 { case .return?, .throw?, .defer?, @@ -1094,7 +1083,7 @@ extension Parser.Lookahead { consume(if: .keyword(.try)) != nil, self.at(anyIn: SingleValueStatementExpression.self) == nil { - return atStartOfStatement(allowRecovery: allowRecovery, preferExpr: preferExpr) + return atStartOfStatement(preferExpr: preferExpr) } return false } diff --git a/Sources/SwiftParser/TopLevel.swift b/Sources/SwiftParser/TopLevel.swift index bf746a323e2..678a5e87a6f 100644 --- a/Sources/SwiftParser/TopLevel.swift +++ b/Sources/SwiftParser/TopLevel.swift @@ -67,7 +67,6 @@ extension Parser { extension Parser { mutating func parseCodeBlockItemList( - isAtTopLevel: Bool = false, allowInitDecl: Bool = true, until stopCondition: (inout Parser) -> Bool ) -> RawCodeBlockItemListSyntax { @@ -75,11 +74,11 @@ extension Parser { var loopProgress = LoopProgressCondition() while !stopCondition(&self), self.hasProgressed(&loopProgress) { let newItemAtStartOfLine = self.atStartOfLine - guard let newElement = self.parseCodeBlockItem(isAtTopLevel: isAtTopLevel, allowInitDecl: allowInitDecl) else { + guard let newElement = self.parseCodeBlockItem(allowInitDecl: allowInitDecl, until: stopCondition) else { break } if let lastItem = elements.last, - lastItem.semicolon == nil && !newItemAtStartOfLine && !newElement.item.is(RawUnexpectedCodeDeclSyntax.self) + lastItem.semicolon == nil && !newItemAtStartOfLine && !newElement.item.is(RawUnexpectedCodeDeclSyntax.self) { elements[elements.count - 1] = RawCodeBlockItemSyntax( lastItem.unexpectedBeforeItem, @@ -97,7 +96,7 @@ extension Parser { /// Parse the top level items in a source file. mutating func parseTopLevelCodeBlockItems() -> RawCodeBlockItemListSyntax { - return parseCodeBlockItemList(isAtTopLevel: true, until: { _ in false }) + return parseCodeBlockItemList(until: { _ in false }) } /// The optional form of `parseCodeBlock` that checks to see if the parser has @@ -132,11 +131,35 @@ extension Parser { ) } + mutating func fixWithSemi(codeBlockItem item: RawCodeBlockItemSyntax.Item) -> RawCodeBlockItemSyntax? { + let semi = self.consume(if: .semicolon) + var trailingSemis: [RawTokenSyntax] = [] + while let trailingSemi = self.consume(if: .semicolon) { + trailingSemis.append(trailingSemi) + } + + if item.raw.isEmpty && semi == nil && trailingSemis.isEmpty { + return nil + } + return RawCodeBlockItemSyntax( + item: item, + semicolon: semi, + RawUnexpectedNodesSyntax(trailingSemis, arena: self.arena), + arena: self.arena + ) + } + /// Parse an individual item - either in a code block or at the top level. /// /// Returns `nil` if the parser did not consume any tokens while trying to /// parse the code block item. - mutating func parseCodeBlockItem(isAtTopLevel: Bool, allowInitDecl: Bool) -> RawCodeBlockItemSyntax? { + /// + /// `isAtTopLevel` determines whether this is trying to parse an item that's at + /// the top level of the source file. If this is the case, we allow skipping + /// closing braces while trying to recover to the next item. + /// If we are not at the top level, such a closing brace should close the + /// wrapping declaration instead of being consumed by lookahead. + mutating func parseCodeBlockItem(allowInitDecl: Bool, until stopCondition: (inout Parser) -> Bool) -> RawCodeBlockItemSyntax? { let startToken = self.currentToken if let syntax = self.loadCurrentSyntaxNodeFromCache(for: .codeBlockItem) { self.registerNodeForIncrementalParse(node: syntax.raw, startToken: startToken) @@ -151,6 +174,7 @@ extension Parser { arena: self.arena ) } + if self.at(.keyword(.case), .keyword(.default)) { // 'case' and 'default' are invalid in code block items. // Parse them and put them in their own CodeBlockItem but as an unexpected node. @@ -163,14 +187,61 @@ extension Parser { ) } - let item = self.parseItem(isAtTopLevel: isAtTopLevel, allowInitDecl: allowInitDecl) - let semi = self.consume(if: .semicolon) + let item: RawCodeBlockItemSyntax.Item + let attachSemi: Bool + if self.at(.poundIf) && !self.withLookahead({ $0.consumeIfConfigOfAttributes() }) { + // If config of attributes is parsed as part of declaration parsing as it + // doesn't constitute its own code block item. + let directive = self.parsePoundIfDirective { parser in + let items = parser.parseCodeBlockItemList(allowInitDecl: allowInitDecl, until: { $0.atEndOfIfConfigClauseBody() }) + return .statements(items) + } + item = .init(decl: directive) + attachSemi = false + } else if self.at(.poundSourceLocation) { + item = .init(decl: self.parsePoundSourceLocationDirective()) + attachSemi = false + } else if self.atStartOfDeclaration(allowInitDecl: allowInitDecl) { + item = .decl(self.parseDeclaration()) + attachSemi = true + } else if self.atStartOfStatement(preferExpr: false) { + item = self.parseStatementItem() + attachSemi = true + } else if self.atStartOfExpression() { + item = .expr(self.parseExpression(flavor: .basic, pattern: .none)) + attachSemi = true + } else if (self.at(.atSign) && peek(isAt: .identifier)) || self.at(anyIn: DeclarationModifier.self) != nil { + // Force parsing '@' as a declaration, as there's no valid + // expression or statement starting with an attribute. + item = .decl(self.parseDeclaration()) + attachSemi = true + + } else { + item = .decl( + RawDeclSyntax( + self.parseUnexpectedCodeDeclaration( + allowInitDecl: allowInitDecl, + requiresDecl: false, + skipToDeclOnly: false, + until: stopCondition + ) + ) + ) + attachSemi = true + } + + let semi: RawTokenSyntax? var trailingSemis: [RawTokenSyntax] = [] - while let trailingSemi = self.consume(if: .semicolon) { - trailingSemis.append(trailingSemi) + if attachSemi { + semi = self.consume(if: .semicolon) + while let trailingSemi = self.consume(if: .semicolon) { + trailingSemis.append(trailingSemi) + } + } else { + semi = nil } - if item.raw.isEmpty && semi == nil && trailingSemis.isEmpty { + if item.isEmpty && semi == nil && trailingSemis.isEmpty { return nil } @@ -182,7 +253,6 @@ extension Parser { ) self.registerNodeForIncrementalParse(node: result.raw, startToken: startToken) - return result } @@ -195,7 +265,7 @@ extension Parser { // or 'switch' as an expression when in statement position, but that // could result in less useful recovery behavior. if at(.keyword(.as)), - let expr = stmt.as(RawExpressionStmtSyntax.self)?.expression + let expr = stmt.as(RawExpressionStmtSyntax.self)?.expression { if expr.is(RawDoExprSyntax.self) || expr.is(RawIfExprSyntax.self) || expr.is(RawSwitchExprSyntax.self) { let (op, rhs) = parseUnresolvedAsExpr( @@ -213,65 +283,4 @@ extension Parser { } return .stmt(stmt) } - - /// `isAtTopLevel` determines whether this is trying to parse an item that's at - /// the top level of the source file. If this is the case, we allow skipping - /// closing braces while trying to recover to the next item. - /// If we are not at the top level, such a closing brace should close the - /// wrapping declaration instead of being consumed by lookahead. - private mutating func parseItem( - isAtTopLevel: Bool = false, - allowInitDecl: Bool = true - ) -> RawCodeBlockItemSyntax.Item { - if self.at(.poundIf) && !self.withLookahead({ $0.consumeIfConfigOfAttributes() }) { - // If config of attributes is parsed as part of declaration parsing as it - // doesn't constitute its own code block item. - let directive = self.parsePoundIfDirective { (parser, _) in - parser.parseCodeBlockItem(isAtTopLevel: isAtTopLevel, allowInitDecl: allowInitDecl) - } addSemicolonIfNeeded: { lastElement, newItemAtStartOfLine, newItem, parser in - if lastElement.semicolon == nil && !newItemAtStartOfLine && !newItem.item.is(RawUnexpectedCodeDeclSyntax.self) { - return RawCodeBlockItemSyntax( - lastElement.unexpectedBeforeItem, - item: .init(lastElement.item)!, - lastElement.unexpectedBetweenItemAndSemicolon, - semicolon: parser.missingToken(.semicolon), - lastElement.unexpectedAfterSemicolon, - arena: parser.arena - ) - } else { - return nil - } - } syntax: { parser, items in - return .statements(RawCodeBlockItemListSyntax(elements: items, arena: parser.arena)) - } - return .init(decl: directive) - } else if self.at(.poundSourceLocation) { - return .init(decl: self.parsePoundSourceLocationDirective()) - } else if self.atStartOfDeclaration(isAtTopLevel: isAtTopLevel, allowInitDecl: allowInitDecl) { - return .decl(self.parseDeclaration()) - } else if self.atStartOfStatement(preferExpr: false) { - return self.parseStatementItem() - } else if self.atStartOfExpression() { - return .expr(self.parseExpression(flavor: .basic, pattern: .none)) - // } else if self.atStartOfStatement(allowRecovery: true, preferExpr: false) { - // return self.parseStatementItem() - // } else if self.atStartOfDeclaration(isAtTopLevel: isAtTopLevel, allowInitDecl: allowInitDecl, allowRecovery: true) { - // return .decl(self.parseDeclaration()) - } else if (self.at(.atSign) && peek(isAt: .identifier)) || self.at(anyIn: DeclarationModifier.self) != nil { - // Force parsing '@' as a declaration, as there's no valid - // expression or statement starting with an attribute. - return .decl(self.parseDeclaration()) - } else { - return .decl( - RawDeclSyntax( - self.parseUnexpectedCodeDeclaration( - isAtTopLevel: isAtTopLevel, - allowInitDecl: allowInitDecl, - requiresDecl: false, - skipToDeclOnly: false - ) - ) - ) - } - } } diff --git a/Sources/SwiftParser/generated/LayoutNodes+Parsable.swift b/Sources/SwiftParser/generated/LayoutNodes+Parsable.swift index c880b12f8e5..521a585374d 100644 --- a/Sources/SwiftParser/generated/LayoutNodes+Parsable.swift +++ b/Sources/SwiftParser/generated/LayoutNodes+Parsable.swift @@ -238,7 +238,7 @@ extension VersionTupleSyntax: SyntaxParseable { fileprivate extension Parser { mutating func parseNonOptionalCodeBlockItem() -> RawCodeBlockItemSyntax { - guard let node = self.parseCodeBlockItem(isAtTopLevel: false, allowInitDecl: true) else { + guard let node = self.parseCodeBlockItem(allowInitDecl: true, until: { _ in false }) else { // The missing item is not necessary to be a declaration, // which is just a placeholder here return RawCodeBlockItemSyntax( diff --git a/Tests/SwiftParserTest/DirectiveTests.swift b/Tests/SwiftParserTest/DirectiveTests.swift index a41e75125c5..d47f5b6146a 100644 --- a/Tests/SwiftParserTest/DirectiveTests.swift +++ b/Tests/SwiftParserTest/DirectiveTests.swift @@ -458,4 +458,38 @@ final class DirectiveTests: ParserTestCase { """ ) } + + func testOrphanEndifInMember() { + assertParse( + """ + struct S { + 1️⃣#endif + } + """, + diagnostics: [ + DiagnosticSpec(message: "unexpected code '#endif' in struct") + ] + ) + } + + func testRightBraceInIfConfig() { + assertParse( + """ + struct S { + #if true + 1️⃣} + #endif + } + func foo() { + #if true + 2️⃣} + #endif + } + """, + diagnostics: [ + DiagnosticSpec(locationMarker: "1️⃣", message: "unexpected brace in conditional compilation clause"), + DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected brace in conditional compilation clause"), + ] + ) + } } From 6026c787940d9efac469557417bd8e4de4a20d96 Mon Sep 17 00:00:00 2001 From: Rintaro Ishizaki Date: Thu, 25 Sep 2025 20:08:38 -0700 Subject: [PATCH 03/17] tbs --- Sources/SwiftParser/Attributes.swift | 16 ++-- Sources/SwiftParser/Declarations.swift | 26 +++--- Sources/SwiftParser/TopLevel.swift | 18 ++-- Tests/SwiftParserTest/AttributeTests.swift | 20 +++-- Tests/SwiftParserTest/DeclarationTests.swift | 12 ++- .../translated/RecoveryTests.swift | 89 ++++++++++--------- 6 files changed, 106 insertions(+), 75 deletions(-) diff --git a/Sources/SwiftParser/Attributes.swift b/Sources/SwiftParser/Attributes.swift index 944ae2fd7b1..b6763dad82a 100644 --- a/Sources/SwiftParser/Attributes.swift +++ b/Sources/SwiftParser/Attributes.swift @@ -902,15 +902,19 @@ extension Parser { return .decls(RawMemberBlockItemListSyntax(elements: [member], arena: parser.arena)) }) decl = ifConfig.makeUnexpectedKeepingFirstNode( - of: RawDeclSyntax.self, arena: self.arena, + of: RawDeclSyntax.self, + arena: self.arena, where: { !$0.is(RawIfConfigDeclSyntax.self) }, makeMissing: { - RawDeclSyntax(RawMissingDeclSyntax( - attributes: self.emptyCollection(RawAttributeListSyntax.self), - modifiers: self.emptyCollection(RawDeclModifierListSyntax.self), - arena: self.arena) + RawDeclSyntax( + RawMissingDeclSyntax( + attributes: self.emptyCollection(RawAttributeListSyntax.self), + modifiers: self.emptyCollection(RawDeclModifierListSyntax.self), + arena: self.arena + ) ) - }) + } + ) } else { decl = self.parseDeclaration(in: .argumentList) } diff --git a/Sources/SwiftParser/Declarations.swift b/Sources/SwiftParser/Declarations.swift index f16357708b7..59bf1cb7b94 100644 --- a/Sources/SwiftParser/Declarations.swift +++ b/Sources/SwiftParser/Declarations.swift @@ -956,9 +956,11 @@ extension Parser { decl = RawDeclSyntax(self.parsePoundSourceLocationDirective()) attachSemi = false } else if self.at(.poundIf) && !self.withLookahead({ $0.consumeIfConfigOfAttributes() }) { - decl = RawDeclSyntax(self.parsePoundIfDirective { parser in - return .decls(parser.parseMemberDeclList(until: {$0.atEndOfIfConfigClauseBody()})) - }) + decl = RawDeclSyntax( + self.parsePoundIfDirective { parser in + return .decls(parser.parseMemberDeclList(until: { $0.atEndOfIfConfigClauseBody() })) + } + ) attachSemi = false } else if self.atStartOfDeclaration(allowInitDecl: true, requiresDecl: true) { decl = self.parseDeclaration(in: .memberDeclList) @@ -1006,7 +1008,9 @@ extension Parser { return result } - mutating func parseMemberDeclList(until stopCondition: (inout Parser) -> Bool = { $0.at(.rightBrace) }) -> RawMemberBlockItemListSyntax { + mutating func parseMemberDeclList( + until stopCondition: (inout Parser) -> Bool = { $0.at(.rightBrace) } + ) -> RawMemberBlockItemListSyntax { var elements = [RawMemberBlockItemSyntax]() do { var loopProgress = LoopProgressCondition() @@ -1038,15 +1042,7 @@ extension Parser { /// If the left brace is missing, its indentation will be used to judge whether a following `}` was /// indented to close this code block or a surrounding context. See `expectRightBrace`. mutating func parseMemberBlock(introducer: RawTokenSyntax? = nil) -> RawMemberBlockSyntax { - guard let lBraceHandle = canRecoverTo(.leftBrace) else { - return RawMemberBlockSyntax( - leftBrace: self.missingToken(.leftBrace), - members: RawMemberBlockItemListSyntax(elements: [], arena: self.arena), - rightBrace: consume(if: TokenSpec(.rightBrace, allowAtStartOfLine: false)) ?? self.missingToken(.rightBrace), - arena: arena - ) - } - let (unexpectedBeforeLBrace, lbrace) = self.eat(lBraceHandle) + let (unexpectedBeforeLBrace, lbrace) = self.expect(.leftBrace) let members = parseMemberDeclList() let (unexpectedBeforeRBrace, rbrace) = self.expectRightBrace(leftBrace: lbrace, introducer: introducer) @@ -2341,7 +2337,9 @@ extension Parser { ) -> RawUnexpectedCodeDeclSyntax { var unexpectedTokens = [RawSyntax]() while !self.at(.endOfFile, .semicolon) && !stopCondition(&self) { - let numTokensToSkip = self.withLookahead({ $0.skipSingle(); return $0.tokensConsumed }) + let numTokensToSkip = self.withLookahead({ + $0.skipSingle(); return $0.tokensConsumed + }) for _ in 0.. Bool) -> RawCodeBlockItemSyntax? { + mutating func parseCodeBlockItem( + allowInitDecl: Bool, + until stopCondition: (inout Parser) -> Bool + ) -> RawCodeBlockItemSyntax? { let startToken = self.currentToken if let syntax = self.loadCurrentSyntaxNodeFromCache(for: .codeBlockItem) { self.registerNodeForIncrementalParse(node: syntax.raw, startToken: startToken) @@ -174,7 +177,7 @@ extension Parser { arena: self.arena ) } - + if self.at(.keyword(.case), .keyword(.default)) { // 'case' and 'default' are invalid in code block items. // Parse them and put them in their own CodeBlockItem but as an unexpected node. @@ -193,7 +196,10 @@ extension Parser { // If config of attributes is parsed as part of declaration parsing as it // doesn't constitute its own code block item. let directive = self.parsePoundIfDirective { parser in - let items = parser.parseCodeBlockItemList(allowInitDecl: allowInitDecl, until: { $0.atEndOfIfConfigClauseBody() }) + let items = parser.parseCodeBlockItemList( + allowInitDecl: allowInitDecl, + until: { $0.atEndOfIfConfigClauseBody() } + ) return .statements(items) } item = .init(decl: directive) @@ -215,7 +221,7 @@ extension Parser { // expression or statement starting with an attribute. item = .decl(self.parseDeclaration()) attachSemi = true - + } else { item = .decl( RawDeclSyntax( @@ -265,7 +271,7 @@ extension Parser { // or 'switch' as an expression when in statement position, but that // could result in less useful recovery behavior. if at(.keyword(.as)), - let expr = stmt.as(RawExpressionStmtSyntax.self)?.expression + let expr = stmt.as(RawExpressionStmtSyntax.self)?.expression { if expr.is(RawDoExprSyntax.self) || expr.is(RawIfExprSyntax.self) || expr.is(RawSwitchExprSyntax.self) { let (op, rhs) = parseUnresolvedAsExpr( diff --git a/Tests/SwiftParserTest/AttributeTests.swift b/Tests/SwiftParserTest/AttributeTests.swift index 0da6dffc640..1a9f72712b4 100644 --- a/Tests/SwiftParserTest/AttributeTests.swift +++ b/Tests/SwiftParserTest/AttributeTests.swift @@ -885,18 +885,28 @@ final class AttributeTests: ParserTestCase { ), DiagnosticSpec( locationMarker: "2️⃣", - message: "expected identifier and member block in class", - fixIts: ["insert identifier and member block"] + message: "expected identifier in class", + fixIts: ["insert identifier"] ), DiagnosticSpec( locationMarker: "2️⃣", - message: "unexpected code '))' in source file" + message: "expected '{' in class", + fixIts: ["insert '{'"] + ), + DiagnosticSpec( + locationMarker: "2️⃣", + message: "unexpected code '))' in class" + ), + DiagnosticSpec( + locationMarker: "3️⃣", + message: "expected '}' to end class", + fixIts: ["insert '}'"] ), ], fixedSource: """ - @attached(member, names: named(<#expression#>)) class <#identifier#> { - })) + @attached(member, names: named(<#expression#>)) class <#identifier#> {)) macro m() + } """ ) } diff --git a/Tests/SwiftParserTest/DeclarationTests.swift b/Tests/SwiftParserTest/DeclarationTests.swift index af214ffae1d..5abae62a131 100644 --- a/Tests/SwiftParserTest/DeclarationTests.swift +++ b/Tests/SwiftParserTest/DeclarationTests.swift @@ -1651,8 +1651,8 @@ final class DeclarationTests: ParserTestCase { diagnostics: [ DiagnosticSpec( locationMarker: "1️⃣", - message: "expected member block in struct", - fixIts: ["insert member block"] + message: "expected '{' in struct", + fixIts: ["insert '{'"] ), DiagnosticSpec( locationMarker: "2️⃣", @@ -1670,13 +1670,19 @@ final class DeclarationTests: ParserTestCase { message: "expected '#endif' in conditional compilation block", fixIts: ["insert '#endif'"] ), + DiagnosticSpec( + locationMarker: "3️⃣", + message: "expected '}' to end struct", + fixIts: ["insert '}'"] + ), + ], fixedSource: """ struct n { - } #if <#expression#> @<#type#> <#declaration#> #endif + } """ ) } diff --git a/Tests/SwiftParserTest/translated/RecoveryTests.swift b/Tests/SwiftParserTest/translated/RecoveryTests.swift index 821fc58ff97..61b52080edc 100644 --- a/Tests/SwiftParserTest/translated/RecoveryTests.swift +++ b/Tests/SwiftParserTest/translated/RecoveryTests.swift @@ -975,14 +975,16 @@ final class RecoveryTests: ParserTestCase { func testRecovery54() { assertParse( """ - struct NoBracesStruct11️⃣() + struct NoBracesStruct11️⃣()2️⃣ """, diagnostics: [ - DiagnosticSpec(message: "expected member block in struct", fixIts: ["insert member block"]) + DiagnosticSpec(locationMarker: "1️⃣", message: "expected '{' in struct", fixIts: ["insert '{'"]), + DiagnosticSpec(locationMarker: "1️⃣", message: "unexpected code '()' in struct"), + DiagnosticSpec(locationMarker: "2️⃣", message: "expected '}' to end struct", fixIts: ["insert '}'"]), ], fixedSource: """ - struct NoBracesStruct1 { - }() + struct NoBracesStruct1 {() + } """ ) } @@ -993,31 +995,31 @@ final class RecoveryTests: ParserTestCase { enum NoBracesUnion11️⃣() class NoBracesClass12️⃣() protocol NoBracesProtocol13️⃣() - extension NoBracesStruct14️⃣() + extension NoBracesStruct14️⃣()5️⃣ """, diagnostics: [ - DiagnosticSpec(locationMarker: "1️⃣", message: "expected member block in enum", fixIts: ["insert member block"]), - DiagnosticSpec(locationMarker: "2️⃣", message: "expected member block in class", fixIts: ["insert member block"]), - DiagnosticSpec( - locationMarker: "3️⃣", - message: "expected member block in protocol", - fixIts: ["insert member block"] - ), - DiagnosticSpec( - locationMarker: "4️⃣", - message: "expected member block in extension", - fixIts: ["insert member block"] - ), + DiagnosticSpec(locationMarker: "1️⃣", message: "expected '{' in enum", fixIts: ["insert '{'"]), + DiagnosticSpec(locationMarker: "1️⃣", message: "unexpected code '()' in enum"), + DiagnosticSpec(locationMarker: "2️⃣", message: "expected '{' in class", fixIts: ["insert '{'"]), + DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected code '()' in class"), + DiagnosticSpec(locationMarker: "3️⃣", message: "expected '{' in protocol", fixIts: ["insert '{'"]), + DiagnosticSpec(locationMarker: "3️⃣", message: "unexpected code '()' in protocol"), + DiagnosticSpec(locationMarker: "4️⃣", message: "expected '{' in extension", fixIts: ["insert '{'"]), + DiagnosticSpec(locationMarker: "4️⃣", message: "unexpected code '()' in extension"), + DiagnosticSpec(locationMarker: "5️⃣", message: "expected '}' to end extension", fixIts: ["insert '}'"]), + DiagnosticSpec(locationMarker: "5️⃣", message: "expected '}' to end protocol", fixIts: ["insert '}'"]), + DiagnosticSpec(locationMarker: "5️⃣", message: "expected '}' to end class", fixIts: ["insert '}'"]), + DiagnosticSpec(locationMarker: "5️⃣", message: "expected '}' to end enum", fixIts: ["insert '}'"]), ], fixedSource: """ - enum NoBracesUnion1 { - }() - class NoBracesClass1 { - }() - protocol NoBracesProtocol1 { - }() - extension NoBracesStruct1 { - }() + enum NoBracesUnion1 {() + class NoBracesClass1 {() + protocol NoBracesProtocol1 {() + extension NoBracesStruct1 {() + } + } + } + } """ ) } @@ -1032,35 +1034,31 @@ final class RecoveryTests: ParserTestCase { extension NoBracesStruct25️⃣ """, diagnostics: [ - DiagnosticSpec( - locationMarker: "1️⃣", - message: "expected member block in struct", - fixIts: ["insert member block"] - ), - DiagnosticSpec(locationMarker: "2️⃣", message: "expected member block in enum", fixIts: ["insert member block"]), - DiagnosticSpec(locationMarker: "3️⃣", message: "expected member block in class", fixIts: ["insert member block"]), - DiagnosticSpec( - locationMarker: "4️⃣", - message: "expected member block in protocol", - fixIts: ["insert member block"] - ), + DiagnosticSpec(locationMarker: "1️⃣", message: "expected '{' in struct", fixIts: ["insert '{'"]), + DiagnosticSpec(locationMarker: "2️⃣", message: "expected '{' in enum", fixIts: ["insert '{'"]), + DiagnosticSpec(locationMarker: "3️⃣", message: "expected '{' in class", fixIts: ["insert '{'"]), + DiagnosticSpec(locationMarker: "4️⃣", message: "expected '{' in protocol", fixIts: ["insert '{'"]), DiagnosticSpec( locationMarker: "5️⃣", message: "expected member block in extension", fixIts: ["insert member block"] ), + DiagnosticSpec(locationMarker: "5️⃣", message: "expected '}' to end protocol", fixIts: ["insert '}'"]), + DiagnosticSpec(locationMarker: "5️⃣", message: "expected '}' to end class", fixIts: ["insert '}'"]), + DiagnosticSpec(locationMarker: "5️⃣", message: "expected '}' to end enum", fixIts: ["insert '}'"]), + DiagnosticSpec(locationMarker: "5️⃣", message: "expected '}' to end struct", fixIts: ["insert '}'"]), ], fixedSource: """ struct NoBracesStruct2 { - } enum NoBracesUnion2 { - } class NoBracesClass2 { - } protocol NoBracesProtocol2 { - } extension NoBracesStruct2 { } + } + } + } + } """ ) } @@ -3428,4 +3426,13 @@ final class RecoveryTests: ParserTestCase { ) } + func testTTT() { + assertParse( + """ + struct S { + : + } + """ + ) + } } From 91f9d63dcfd413a2a9069ecd8768e4efa960544a Mon Sep 17 00:00:00 2001 From: Rintaro Ishizaki Date: Fri, 26 Sep 2025 15:06:30 -0700 Subject: [PATCH 04/17] tbs --- .../CollectionNodes+Parsable.swift | 2 +- Sources/SwiftParser/Declarations.swift | 31 ++--- Sources/SwiftParser/Expressions.swift | 22 +-- Sources/SwiftParser/Statements.swift | 14 +- Sources/SwiftParser/TopLevel.swift | 14 +- Tests/SwiftParserTest/DirectiveTests.swift | 127 +++++++++++++++++- Tests/SwiftParserTest/ExpressionTests.swift | 20 ++- .../translated/RecoveryTests.swift | 4 +- 8 files changed, 171 insertions(+), 63 deletions(-) diff --git a/Sources/SwiftParser/CollectionNodes+Parsable.swift b/Sources/SwiftParser/CollectionNodes+Parsable.swift index da31a9424ec..3424de77418 100644 --- a/Sources/SwiftParser/CollectionNodes+Parsable.swift +++ b/Sources/SwiftParser/CollectionNodes+Parsable.swift @@ -116,7 +116,7 @@ extension CodeBlockItemListSyntax: SyntaxParseable { extension MemberBlockItemListSyntax: SyntaxParseable { public static func parse(from parser: inout Parser) -> Self { return parse(from: &parser) { parser in - return parser.parseMemberDeclList() + return parser.parseMemberDeclList(until: { _ in false }) } makeMissing: { remainingTokens, arena in let missingDecl = RawMissingDeclSyntax( attributes: RawAttributeListSyntax(elements: [], arena: arena), diff --git a/Sources/SwiftParser/Declarations.swift b/Sources/SwiftParser/Declarations.swift index 59bf1cb7b94..43edf0763f8 100644 --- a/Sources/SwiftParser/Declarations.swift +++ b/Sources/SwiftParser/Declarations.swift @@ -966,13 +966,9 @@ extension Parser { decl = self.parseDeclaration(in: .memberDeclList) attachSemi = true } else { + // Otherwise, eat the unexpected tokens into an "decl". decl = RawDeclSyntax( - self.parseUnexpectedCodeDeclaration( - allowInitDecl: true, - requiresDecl: true, - skipToDeclOnly: true, - until: stopCondition - ) + self.parseUnexpectedCodeDeclaration(allowInitDecl: true, requiresDecl: true, until: stopCondition) ) attachSemi = true } @@ -1009,7 +1005,7 @@ extension Parser { } mutating func parseMemberDeclList( - until stopCondition: (inout Parser) -> Bool = { $0.at(.rightBrace) } + until stopCondition: (inout Parser) -> Bool = { $0.at(.rightBrace) || $0.atEndOfIfConfigClauseBody() } ) -> RawMemberBlockItemListSyntax { var elements = [RawMemberBlockItemSyntax]() do { @@ -1765,7 +1761,7 @@ extension Parser { // There can only be an implicit getter if no other accessors were // seen before this one. guard let accessorList else { - let body = parseCodeBlockItemList(until: { $0.at(.rightBrace) }) + let body = parseCodeBlockItemList() let (unexpectedBeforeRBrace, rbrace) = self.expect(.rightBrace) return RawAccessorBlockSyntax( @@ -2326,20 +2322,20 @@ extension Parser { ) } - /// Eat tokens until a start of decl, or if `skipToDeclOnly` is not set until - /// a start of statement or expression. + /// Eats tokens until a start of decl, statement, or expression. /// Returns consumed tokens as a `RawUnexpectedCodeDeclSyntax` declaration. mutating func parseUnexpectedCodeDeclaration( allowInitDecl: Bool, requiresDecl: Bool, - skipToDeclOnly: Bool, until stopCondition: (inout Parser) -> Bool ) -> RawUnexpectedCodeDeclSyntax { var unexpectedTokens = [RawSyntax]() - while !self.at(.endOfFile, .semicolon) && !stopCondition(&self) { - let numTokensToSkip = self.withLookahead({ - $0.skipSingle(); return $0.tokensConsumed - }) + var loopProgress = LoopProgressCondition() + while !self.at(.endOfFile, .semicolon), !stopCondition(&self), self.hasProgressed(&loopProgress) { + let numTokensToSkip = self.withLookahead { + $0.skipSingle() + return $0.tokensConsumed + } for _ in 0.. RawSwitchCaseListSyntax { var elements = [RawSwitchCaseListSyntax.Element]() var elementsProgress = LoopProgressCondition() - while !self.at(.endOfFile, .rightBrace) && !self.at(.poundEndif, .poundElseif, .poundElse) - && self.hasProgressed(&elementsProgress) - { + while !self.at(.endOfFile, .rightBrace), !self.atEndOfIfConfigClauseBody(), self.hasProgressed(&elementsProgress) { if self.withLookahead({ $0.atStartOfSwitchCase(allowRecovery: false) }) { elements.append(.switchCase(self.parseSwitchCase())) } else if self.canRecoverTo(.poundIf) != nil { @@ -2346,10 +2344,7 @@ extension Parser { }) ) ) - } else if allowStandaloneStmtRecovery - && (self.atStartOfExpression() || self.atStartOfStatement(preferExpr: false) - || self.atStartOfDeclaration()) - { + } else if allowStandaloneStmtRecovery { // Synthesize a label for the statement or declaration that isn't covered by a case right now. let statements = parseSwitchCaseBody() if statements.isEmpty { @@ -2385,8 +2380,6 @@ extension Parser { ) ) ) - } else if self.withLookahead({ $0.atStartOfSwitchCase(allowRecovery: true) }) { - elements.append(.switchCase(self.parseSwitchCase())) } else { break } @@ -2396,14 +2389,11 @@ extension Parser { mutating func parseSwitchCaseBody() -> RawCodeBlockItemListSyntax { parseCodeBlockItemList(until: { - if $0.at(.rightBrace) || $0.at(.poundEndif, .poundElseif, .poundElse) { - return true - } - if $0.at(.keyword(.case), .keyword(.default)) { + if $0.at(.rightBrace, .keyword(.case), .keyword(.default)) || $0.atEndOfIfConfigClauseBody() { return true } - if $0.at(.atSign) - && $0.withLookahead({ + if $0.at(.atSign), + $0.withLookahead({ $0.consumeAnyAttribute(); return $0.at(.keyword(.case), .keyword(.default)) }) { diff --git a/Sources/SwiftParser/Statements.swift b/Sources/SwiftParser/Statements.swift index 8426150eb87..ebdf9af0b25 100644 --- a/Sources/SwiftParser/Statements.swift +++ b/Sources/SwiftParser/Statements.swift @@ -1095,18 +1095,8 @@ extension Parser.Lookahead { // Check for and consume attributes. The only valid attribute is `@unknown` // but that's a semantic restriction. var lookahead = self.lookahead() - var loopProgress = LoopProgressCondition() - var hasAttribute = false - while lookahead.at(.atSign) && lookahead.hasProgressed(&loopProgress) { - guard lookahead.peek().rawTokenKind == .identifier else { - return false - } - - lookahead.eat(.atSign) - lookahead.eat(.identifier) - hasAttribute = true - } - + + let hasAttribute = lookahead.consumeAttributeList() if hasAttribute && lookahead.at(.rightBrace) { // If we are at an attribute that's the last token in the SwitchCase, parse // that as an attribute to a missing 'case'. That way, if the developer writes diff --git a/Sources/SwiftParser/TopLevel.swift b/Sources/SwiftParser/TopLevel.swift index 2a105142a24..1f76d139c00 100644 --- a/Sources/SwiftParser/TopLevel.swift +++ b/Sources/SwiftParser/TopLevel.swift @@ -68,11 +68,11 @@ extension Parser { extension Parser { mutating func parseCodeBlockItemList( allowInitDecl: Bool = true, - until stopCondition: (inout Parser) -> Bool + until stopCondition: (inout Parser) -> Bool = { $0.at(.rightBrace) || $0.atEndOfIfConfigClauseBody() } ) -> RawCodeBlockItemListSyntax { var elements = [RawCodeBlockItemSyntax]() var loopProgress = LoopProgressCondition() - while !stopCondition(&self), self.hasProgressed(&loopProgress) { + while !stopCondition(&self), !self.at(.endOfFile), self.hasProgressed(&loopProgress) { let newItemAtStartOfLine = self.atStartOfLine guard let newElement = self.parseCodeBlockItem(allowInitDecl: allowInitDecl, until: stopCondition) else { break @@ -118,7 +118,7 @@ extension Parser { /// indented to close this code block or a surrounding context. See `expectRightBrace`. mutating func parseCodeBlock(introducer: RawTokenSyntax? = nil, allowInitDecl: Bool = true) -> RawCodeBlockSyntax { let (unexpectedBeforeLBrace, lbrace) = self.expect(.leftBrace) - let itemList = parseCodeBlockItemList(allowInitDecl: allowInitDecl, until: { $0.at(.rightBrace) }) + let itemList = parseCodeBlockItemList(allowInitDecl: allowInitDecl) let (unexpectedBeforeRBrace, rbrace) = self.expectRightBrace(leftBrace: lbrace, introducer: introducer) return .init( @@ -223,14 +223,10 @@ extension Parser { attachSemi = true } else { + // Otherwise, eat the unexpected tokens into an "decl". item = .decl( RawDeclSyntax( - self.parseUnexpectedCodeDeclaration( - allowInitDecl: allowInitDecl, - requiresDecl: false, - skipToDeclOnly: false, - until: stopCondition - ) + self.parseUnexpectedCodeDeclaration(allowInitDecl: allowInitDecl, requiresDecl: false, until: stopCondition) ) ) attachSemi = true diff --git a/Tests/SwiftParserTest/DirectiveTests.swift b/Tests/SwiftParserTest/DirectiveTests.swift index d47f5b6146a..17f50a7004f 100644 --- a/Tests/SwiftParserTest/DirectiveTests.swift +++ b/Tests/SwiftParserTest/DirectiveTests.swift @@ -462,13 +462,104 @@ final class DirectiveTests: ParserTestCase { func testOrphanEndifInMember() { assertParse( """ - struct S { - 1️⃣#endif + struct S ℹ️{1️⃣ + 2️⃣#endif } """, diagnostics: [ - DiagnosticSpec(message: "unexpected code '#endif' in struct") - ] + DiagnosticSpec( + locationMarker: "1️⃣", + message: "expected '}' to end struct", + notes: [NoteSpec(message: "to match this opening '{'")], + fixIts: ["insert '}'"], + ), + DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected code in source file"), + ], + fixedSource: """ + struct S { + } + #endif + } + """ + ) + } + + func testOrphanEndifInCodeBlock() { + assertParse( + """ + func foo() ℹ️{1️⃣ + 2️⃣#endif + } + """, + diagnostics: [ + DiagnosticSpec( + locationMarker: "1️⃣", + message: "expected '}' to end function", + notes: [NoteSpec(message: "to match this opening '{'")], + fixIts: ["insert '}'"], + ), + DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected code in source file"), + ], + fixedSource: """ + func foo() { + } + #endif + } + """ + ) + } + + func testOrphanEndifInSwitch() { + assertParse( + """ + switch subject ℹ️{1️⃣ + 2️⃣#endif + } + """, + diagnostics: [ + DiagnosticSpec( + locationMarker: "1️⃣", + message: "expected '}' to end 'switch' statement", + notes: [NoteSpec(message: "to match this opening '{'")], + fixIts: ["insert '}'"], + ), + DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected code in source file"), + ], + fixedSource: """ + switch subject { + } + #endif + } + """ + ) + } + + func testOrphanEndifInSwitchCase() { + assertParse( + """ + switch subject ℹ️{ + case foo: + print()1️⃣ + 2️⃣#endif + } + """, + diagnostics: [ + DiagnosticSpec( + locationMarker: "1️⃣", + message: "expected '}' to end 'switch' statement", + notes: [NoteSpec(message: "to match this opening '{'")], + fixIts: ["insert '}'"], + ), + DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected code in source file"), + ], + fixedSource: """ + switch subject { + case foo: + print() + } + #endif + } + """ ) } @@ -492,4 +583,32 @@ final class DirectiveTests: ParserTestCase { ] ) } + + func testMismatchedPoundIfAndCodeBlock() { + assertParse( + """ + #if FOO + func foo() ℹ️{1️⃣ + #endif + 2️⃣} + """, + diagnostics: [ + DiagnosticSpec( + locationMarker: "1️⃣", + message: "expected '}' to end function", + notes: [NoteSpec(message: "to match this opening '{'")], + fixIts: ["insert '}'"], + ), + DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected brace in source file"), + ], + fixedSource: """ + #if FOO + func foo() { + } + #endif + } + """ + ) + } + } diff --git a/Tests/SwiftParserTest/ExpressionTests.swift b/Tests/SwiftParserTest/ExpressionTests.swift index 54db7b14efa..a461d1c4dc7 100644 --- a/Tests/SwiftParserTest/ExpressionTests.swift +++ b/Tests/SwiftParserTest/ExpressionTests.swift @@ -2709,10 +2709,26 @@ final class StatementExpressionTests: ParserTestCase { assertParse( """ switch x { - 1️⃣@case + @1️⃣case2️⃣ } """, - diagnostics: [DiagnosticSpec(message: "unexpected code '@case' in 'switch' statement")] + diagnostics: [ + DiagnosticSpec( + locationMarker: "1️⃣", + // FIXME: "expected attribute name after '@'". + message: "expected type in attribute", fixIts: ["insert type"] + ), + DiagnosticSpec( + locationMarker: "2️⃣", + // FIXME: "expected pattern and ':' in switch case" + message: "expected expression and ':' in switch case", fixIts: ["insert expression and ':'"] + ), + ], + fixedSource: """ + switch x { + @<#identifier#> case <#expression#>: + } + """ ) } diff --git a/Tests/SwiftParserTest/translated/RecoveryTests.swift b/Tests/SwiftParserTest/translated/RecoveryTests.swift index 61b52080edc..4fdda5e6848 100644 --- a/Tests/SwiftParserTest/translated/RecoveryTests.swift +++ b/Tests/SwiftParserTest/translated/RecoveryTests.swift @@ -3429,8 +3429,8 @@ final class RecoveryTests: ParserTestCase { func testTTT() { assertParse( """ - struct S { - : + switch s { + @ } """ ) From ad00841907143bec99bb839817b3b1a9f7f08e4c Mon Sep 17 00:00:00 2001 From: Rintaro Ishizaki Date: Fri, 26 Sep 2025 15:54:15 -0700 Subject: [PATCH 05/17] tbs --- Sources/SwiftParser/Statements.swift | 25 +++++---------- Sources/SwiftParser/TopLevel.swift | 31 +++++++++---------- Tests/SwiftParserTest/ExpressionTests.swift | 6 ++-- .../translated/RecoveryTests.swift | 10 ------ 4 files changed, 27 insertions(+), 45 deletions(-) diff --git a/Sources/SwiftParser/Statements.swift b/Sources/SwiftParser/Statements.swift index ebdf9af0b25..1165c11780e 100644 --- a/Sources/SwiftParser/Statements.swift +++ b/Sources/SwiftParser/Statements.swift @@ -34,16 +34,6 @@ extension TokenConsumer { } } -extension Parser.Lookahead { - mutating func atStartOfSwitchCaseItem() -> Bool { - while self.consume(if: .atSign) != nil { - self.consume(if: .identifier) - } - - return self.at(anyIn: SwitchCaseStart.self) != nil - } -} - extension Parser { /// Parse a statement. /// @@ -700,6 +690,9 @@ extension Parser { if self.atStartOfStatement(preferExpr: true) || self.atStartOfDeclaration() { return false } + if self.atStartOfLine && self.withLookahead({ $0.atStartOfSwitchCase() }) { + return false + } return true } @@ -1037,12 +1030,10 @@ extension Parser.Lookahead { /// - Note: This function must be kept in sync with `parseStatement()`. /// - Seealso: ``Parser/parseStatement()`` mutating func atStartOfStatement(preferExpr: Bool) -> Bool { - if (self.at(anyIn: SwitchCaseStart.self) != nil || self.at(.atSign)) - && withLookahead({ $0.atStartOfSwitchCaseItem() }) - { - // We consider SwitchCaseItems statements so we don't parse the start of a new case item as trailing parts of an expression. - return true - } +// if self.atStartOfSwitchCase() { +// // We consider 'case' statements so we don't parse the start of a new case item as trailing parts of an expression. +// return true +// } _ = self.consume(if: .identifier, followedBy: .colon) switch self.at(anyIn: CanBeStatementStart.self)?.0 { @@ -1095,7 +1086,7 @@ extension Parser.Lookahead { // Check for and consume attributes. The only valid attribute is `@unknown` // but that's a semantic restriction. var lookahead = self.lookahead() - + let hasAttribute = lookahead.consumeAttributeList() if hasAttribute && lookahead.at(.rightBrace) { // If we are at an attribute that's the last token in the SwitchCase, parse diff --git a/Sources/SwiftParser/TopLevel.swift b/Sources/SwiftParser/TopLevel.swift index 1f76d139c00..4a2c0a97cca 100644 --- a/Sources/SwiftParser/TopLevel.swift +++ b/Sources/SwiftParser/TopLevel.swift @@ -74,11 +74,13 @@ extension Parser { var loopProgress = LoopProgressCondition() while !stopCondition(&self), !self.at(.endOfFile), self.hasProgressed(&loopProgress) { let newItemAtStartOfLine = self.atStartOfLine - guard let newElement = self.parseCodeBlockItem(allowInitDecl: allowInitDecl, until: stopCondition) else { + guard let newItem = self.parseCodeBlockItem(allowInitDecl: allowInitDecl, until: stopCondition) else { break } if let lastItem = elements.last, - lastItem.semicolon == nil && !newItemAtStartOfLine && !newElement.item.is(RawUnexpectedCodeDeclSyntax.self) + lastItem.semicolon == nil, + !newItemAtStartOfLine, + !newItem.item.is(RawUnexpectedCodeDeclSyntax.self) { elements[elements.count - 1] = RawCodeBlockItemSyntax( lastItem.unexpectedBeforeItem, @@ -89,7 +91,7 @@ extension Parser { arena: self.arena ) } - elements.append(newElement) + elements.append(newItem) } return .init(elements: elements, arena: self.arena) } @@ -178,18 +180,6 @@ extension Parser { ) } - if self.at(.keyword(.case), .keyword(.default)) { - // 'case' and 'default' are invalid in code block items. - // Parse them and put them in their own CodeBlockItem but as an unexpected node. - let switchCase = self.parseSwitchCase() - return RawCodeBlockItemSyntax( - RawUnexpectedNodesSyntax([switchCase], arena: self.arena), - item: .init(expr: RawMissingExprSyntax(arena: self.arena)), - semicolon: nil, - arena: self.arena - ) - } - let item: RawCodeBlockItemSyntax.Item let attachSemi: Bool if self.at(.poundIf) && !self.withLookahead({ $0.consumeIfConfigOfAttributes() }) { @@ -221,7 +211,16 @@ extension Parser { // expression or statement starting with an attribute. item = .decl(self.parseDeclaration()) attachSemi = true - + } else if self.withLookahead({ $0.atStartOfSwitchCase() }) { + // 'case' and 'default' are invalid in code block items. + // Parse them and put them in their own CodeBlockItem but as an unexpected node. + let switchCase = self.parseSwitchCase() + return RawCodeBlockItemSyntax( + RawUnexpectedNodesSyntax([switchCase], arena: self.arena), + item: .init(expr: RawMissingExprSyntax(arena: self.arena)), + semicolon: nil, + arena: self.arena + ) } else { // Otherwise, eat the unexpected tokens into an "decl". item = .decl( diff --git a/Tests/SwiftParserTest/ExpressionTests.swift b/Tests/SwiftParserTest/ExpressionTests.swift index a461d1c4dc7..6ef894c90d8 100644 --- a/Tests/SwiftParserTest/ExpressionTests.swift +++ b/Tests/SwiftParserTest/ExpressionTests.swift @@ -2716,12 +2716,14 @@ final class StatementExpressionTests: ParserTestCase { DiagnosticSpec( locationMarker: "1️⃣", // FIXME: "expected attribute name after '@'". - message: "expected type in attribute", fixIts: ["insert type"] + message: "expected type in attribute", + fixIts: ["insert type"] ), DiagnosticSpec( locationMarker: "2️⃣", // FIXME: "expected pattern and ':' in switch case" - message: "expected expression and ':' in switch case", fixIts: ["insert expression and ':'"] + message: "expected expression and ':' in switch case", + fixIts: ["insert expression and ':'"] ), ], fixedSource: """ diff --git a/Tests/SwiftParserTest/translated/RecoveryTests.swift b/Tests/SwiftParserTest/translated/RecoveryTests.swift index 4fdda5e6848..dcc01fdcfef 100644 --- a/Tests/SwiftParserTest/translated/RecoveryTests.swift +++ b/Tests/SwiftParserTest/translated/RecoveryTests.swift @@ -3425,14 +3425,4 @@ final class RecoveryTests: ParserTestCase { """ ) } - - func testTTT() { - assertParse( - """ - switch s { - @ - } - """ - ) - } } From 130a51bd0f6f9a84ab05f052446bfd87188d5173 Mon Sep 17 00:00:00 2001 From: Rintaro Ishizaki Date: Fri, 26 Sep 2025 16:12:25 -0700 Subject: [PATCH 06/17] tbs --- .../Sources/SyntaxSupport/CommonNodes.swift | 2 +- .../swiftparser/LayoutNodesParsableFile.swift | 2 +- Sources/SwiftParser/Declarations.swift | 14 ++++++++ Sources/SwiftParser/Statements.swift | 5 --- .../generated/LayoutNodes+Parsable.swift | 6 ++-- .../syntaxNodes/SyntaxNodesTUVWXYZ.swift | 2 ++ Tests/SwiftParserTest/DirectiveTests.swift | 10 +++--- Tests/SwiftParserTest/Parser+EntryTests.swift | 33 +++++++++++++++++++ 8 files changed, 60 insertions(+), 14 deletions(-) diff --git a/CodeGeneration/Sources/SyntaxSupport/CommonNodes.swift b/CodeGeneration/Sources/SyntaxSupport/CommonNodes.swift index 96f3b0b9624..bacff39d38a 100644 --- a/CodeGeneration/Sources/SyntaxSupport/CommonNodes.swift +++ b/CodeGeneration/Sources/SyntaxSupport/CommonNodes.swift @@ -177,7 +177,7 @@ public let COMMON_NODES: [Node] = [ kind: .decl, base: .syntax, nameForDiagnostics: "declaration", - parserFunction: "parseDeclaration" + parserFunction: "parseDeclarationOrIfConfig" ), Node( diff --git a/CodeGeneration/Sources/generate-swift-syntax/templates/swiftparser/LayoutNodesParsableFile.swift b/CodeGeneration/Sources/generate-swift-syntax/templates/swiftparser/LayoutNodesParsableFile.swift index bb6006711f6..6403657f4cc 100644 --- a/CodeGeneration/Sources/generate-swift-syntax/templates/swiftparser/LayoutNodesParsableFile.swift +++ b/CodeGeneration/Sources/generate-swift-syntax/templates/swiftparser/LayoutNodesParsableFile.swift @@ -68,7 +68,7 @@ let layoutNodesParsableFile = SourceFileSyntax(leadingTrivia: copyrightHeader) { DeclSyntax( """ mutating func parseNonOptionalCodeBlockItem() -> RawCodeBlockItemSyntax { - guard let node = self.parseCodeBlockItem(isAtTopLevel: false, allowInitDecl: true) else { + guard let node = self.self.parseCodeBlockItem(allowInitDecl: true, until: { _ in false }) else { // The missing item is not necessary to be a declaration, // which is just a placeholder here return RawCodeBlockItemSyntax( diff --git a/Sources/SwiftParser/Declarations.swift b/Sources/SwiftParser/Declarations.swift index 43edf0763f8..f69feb3fa73 100644 --- a/Sources/SwiftParser/Declarations.swift +++ b/Sources/SwiftParser/Declarations.swift @@ -2368,3 +2368,17 @@ extension Parser { ) } } + +extension Parser { + mutating func parseDeclarationOrIfConfig() -> RawDeclSyntax { + if self.at(.poundIf) { + return RawDeclSyntax( + self.parsePoundIfDirective({ + .decls($0.parseMemberDeclList(until: { $0.atEndOfIfConfigClauseBody() })) + }) + ) + } else { + return parseDeclaration(in: .memberDeclList) + } + } +} diff --git a/Sources/SwiftParser/Statements.swift b/Sources/SwiftParser/Statements.swift index 1165c11780e..3127d710cd8 100644 --- a/Sources/SwiftParser/Statements.swift +++ b/Sources/SwiftParser/Statements.swift @@ -1030,11 +1030,6 @@ extension Parser.Lookahead { /// - Note: This function must be kept in sync with `parseStatement()`. /// - Seealso: ``Parser/parseStatement()`` mutating func atStartOfStatement(preferExpr: Bool) -> Bool { -// if self.atStartOfSwitchCase() { -// // We consider 'case' statements so we don't parse the start of a new case item as trailing parts of an expression. -// return true -// } - _ = self.consume(if: .identifier, followedBy: .colon) switch self.at(anyIn: CanBeStatementStart.self)?.0 { case .return?, diff --git a/Sources/SwiftParser/generated/LayoutNodes+Parsable.swift b/Sources/SwiftParser/generated/LayoutNodes+Parsable.swift index 521a585374d..32ce354cd48 100644 --- a/Sources/SwiftParser/generated/LayoutNodes+Parsable.swift +++ b/Sources/SwiftParser/generated/LayoutNodes+Parsable.swift @@ -135,7 +135,7 @@ extension CodeBlockSyntax: SyntaxParseable { extension DeclSyntax: SyntaxParseable { public static func parse(from parser: inout Parser) -> Self { parse(from: &parser) { - $0.parseDeclaration() + $0.parseDeclarationOrIfConfig() } } } @@ -238,7 +238,9 @@ extension VersionTupleSyntax: SyntaxParseable { fileprivate extension Parser { mutating func parseNonOptionalCodeBlockItem() -> RawCodeBlockItemSyntax { - guard let node = self.parseCodeBlockItem(allowInitDecl: true, until: { _ in false }) else { + guard let node = self.self.parseCodeBlockItem(allowInitDecl: true, until: { _ in + false + }) else { // The missing item is not necessary to be a declaration, // which is just a placeholder here return RawCodeBlockItemSyntax( diff --git a/Sources/SwiftSyntax/generated/syntaxNodes/SyntaxNodesTUVWXYZ.swift b/Sources/SwiftSyntax/generated/syntaxNodes/SyntaxNodesTUVWXYZ.swift index 38fdc27c2b6..86f5a7b9e89 100644 --- a/Sources/SwiftSyntax/generated/syntaxNodes/SyntaxNodesTUVWXYZ.swift +++ b/Sources/SwiftSyntax/generated/syntaxNodes/SyntaxNodesTUVWXYZ.swift @@ -2608,6 +2608,8 @@ public struct TypeInitializerClauseSyntax: SyntaxProtocol, SyntaxHashable, _Leaf // MARK: - UnexpectedCodeDeclSyntax +/// Unexpected code at declaration position +/// /// ### Children /// /// - `unexpectedCode`: ``UnexpectedNodesSyntax`` diff --git a/Tests/SwiftParserTest/DirectiveTests.swift b/Tests/SwiftParserTest/DirectiveTests.swift index 17f50a7004f..afe89089962 100644 --- a/Tests/SwiftParserTest/DirectiveTests.swift +++ b/Tests/SwiftParserTest/DirectiveTests.swift @@ -471,7 +471,7 @@ final class DirectiveTests: ParserTestCase { locationMarker: "1️⃣", message: "expected '}' to end struct", notes: [NoteSpec(message: "to match this opening '{'")], - fixIts: ["insert '}'"], + fixIts: ["insert '}'"] ), DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected code in source file"), ], @@ -496,7 +496,7 @@ final class DirectiveTests: ParserTestCase { locationMarker: "1️⃣", message: "expected '}' to end function", notes: [NoteSpec(message: "to match this opening '{'")], - fixIts: ["insert '}'"], + fixIts: ["insert '}'"] ), DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected code in source file"), ], @@ -521,7 +521,7 @@ final class DirectiveTests: ParserTestCase { locationMarker: "1️⃣", message: "expected '}' to end 'switch' statement", notes: [NoteSpec(message: "to match this opening '{'")], - fixIts: ["insert '}'"], + fixIts: ["insert '}'"] ), DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected code in source file"), ], @@ -548,7 +548,7 @@ final class DirectiveTests: ParserTestCase { locationMarker: "1️⃣", message: "expected '}' to end 'switch' statement", notes: [NoteSpec(message: "to match this opening '{'")], - fixIts: ["insert '}'"], + fixIts: ["insert '}'"] ), DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected code in source file"), ], @@ -597,7 +597,7 @@ final class DirectiveTests: ParserTestCase { locationMarker: "1️⃣", message: "expected '}' to end function", notes: [NoteSpec(message: "to match this opening '{'")], - fixIts: ["insert '}'"], + fixIts: ["insert '}'"] ), DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected brace in source file"), ], diff --git a/Tests/SwiftParserTest/Parser+EntryTests.swift b/Tests/SwiftParserTest/Parser+EntryTests.swift index c0b84809f86..73ad877866a 100644 --- a/Tests/SwiftParserTest/Parser+EntryTests.swift +++ b/Tests/SwiftParserTest/Parser+EntryTests.swift @@ -37,6 +37,39 @@ class EntryTests: ParserTestCase { ) } + func testDeclSyntaxParseIfConfig() throws { + assertParse( + """ + #if FLAG + func test() {} + #endif + """, + { DeclSyntax.parse(from: &$0) }, + substructure: IfConfigDeclSyntax( + clauses: IfConfigClauseListSyntax([ + IfConfigClauseSyntax( + poundKeyword: .poundIfToken(), + condition: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier("FLAG"))), + elements: .init([ + MemberBlockItemSyntax( + decl: DeclSyntax( + FunctionDeclSyntax( + funcKeyword: .keyword(.func), + name: .identifier("test"), + signature: FunctionSignatureSyntax( + parameterClause: FunctionParameterClauseSyntax(parameters: []) + ), + body: CodeBlockSyntax(statements: []) + ) + ) + ) + ]) + ) + ]) + ) + ) + } + func testRemainderUnexpected() throws { assertParse( "func test() {} 1️⃣other tokens", From bfddfd615544d652717692587c3a81c4272bf75c Mon Sep 17 00:00:00 2001 From: Rintaro Ishizaki Date: Fri, 26 Sep 2025 19:36:53 -0700 Subject: [PATCH 07/17] cleanup --- .../Sources/SyntaxSupport/CommonNodes.swift | 1 + .../swiftparser/LayoutNodesParsableFile.swift | 2 +- .../ValidateSyntaxNodes.swift | 7 ++++- Sources/SwiftParser/Attributes.swift | 6 ++--- Sources/SwiftParser/Declarations.swift | 20 +++++++++----- Sources/SwiftParser/Directives.swift | 27 ------------------- Sources/SwiftParser/Expressions.swift | 9 +------ Sources/SwiftParser/Statements.swift | 8 ++---- .../generated/LayoutNodes+Parsable.swift | 2 +- 9 files changed, 28 insertions(+), 54 deletions(-) diff --git a/CodeGeneration/Sources/SyntaxSupport/CommonNodes.swift b/CodeGeneration/Sources/SyntaxSupport/CommonNodes.swift index bacff39d38a..39d226abe4e 100644 --- a/CodeGeneration/Sources/SyntaxSupport/CommonNodes.swift +++ b/CodeGeneration/Sources/SyntaxSupport/CommonNodes.swift @@ -409,6 +409,7 @@ public let COMMON_NODES: [Node] = [ children: [ Child( name: "unexpectedCode", + // NOTE: This is not .collection() on purpose. We don't need collection related functions for this. kind: .node(kind: .unexpectedNodes) ) ] diff --git a/CodeGeneration/Sources/generate-swift-syntax/templates/swiftparser/LayoutNodesParsableFile.swift b/CodeGeneration/Sources/generate-swift-syntax/templates/swiftparser/LayoutNodesParsableFile.swift index 6403657f4cc..4c383adeee7 100644 --- a/CodeGeneration/Sources/generate-swift-syntax/templates/swiftparser/LayoutNodesParsableFile.swift +++ b/CodeGeneration/Sources/generate-swift-syntax/templates/swiftparser/LayoutNodesParsableFile.swift @@ -68,7 +68,7 @@ let layoutNodesParsableFile = SourceFileSyntax(leadingTrivia: copyrightHeader) { DeclSyntax( """ mutating func parseNonOptionalCodeBlockItem() -> RawCodeBlockItemSyntax { - guard let node = self.self.parseCodeBlockItem(allowInitDecl: true, until: { _ in false }) else { + guard let node = self.parseCodeBlockItem(allowInitDecl: true, until: { _ in false }) else { // The missing item is not necessary to be a declaration, // which is just a placeholder here return RawCodeBlockItemSyntax( diff --git a/CodeGeneration/Tests/ValidateSyntaxNodes/ValidateSyntaxNodes.swift b/CodeGeneration/Tests/ValidateSyntaxNodes/ValidateSyntaxNodes.swift index b61533bad21..274208775bb 100644 --- a/CodeGeneration/Tests/ValidateSyntaxNodes/ValidateSyntaxNodes.swift +++ b/CodeGeneration/Tests/ValidateSyntaxNodes/ValidateSyntaxNodes.swift @@ -641,7 +641,12 @@ class ValidateSyntaxNodes: XCTestCase { assertFailuresMatchXFails( failures, - expectedFailures: [] + expectedFailures: [ + ValidationFailure( + node: .unexpectedCodeDecl, + message: "child 'unexpectedCode' is a SyntaxCollection but child is not marked as a collection" + ), + ] ) } diff --git a/Sources/SwiftParser/Attributes.swift b/Sources/SwiftParser/Attributes.swift index b6763dad82a..44aae5967f1 100644 --- a/Sources/SwiftParser/Attributes.swift +++ b/Sources/SwiftParser/Attributes.swift @@ -33,9 +33,7 @@ extension Parser { // // In such cases, the second `#if` is not `consumeIfConfigOfAttributes()`. return .ifConfigDecl( - self.parsePoundIfDirective { parser in - return .attributes(parser.parseAttributeList()) - } + self.parsePoundIfDirective({ .attributes($0.parseAttributeList()) }) ) } else { return .attribute(self.parseAttribute()) @@ -896,6 +894,8 @@ extension Parser { let decl: RawDeclSyntax if self.at(.poundIf) { + // '#if' is not accepted in '@abi' attribute, but for recovery, parse it + // parse it and wrap the first decl init with unexpected nodes. let ifConfig = self.parsePoundIfDirective({ parser in let decl = parser.parseDeclaration(in: .argumentList) let member = RawMemberBlockItemSyntax(decl: decl, semicolon: nil, arena: parser.arena) diff --git a/Sources/SwiftParser/Declarations.swift b/Sources/SwiftParser/Declarations.swift index f69feb3fa73..bdec7faeef1 100644 --- a/Sources/SwiftParser/Declarations.swift +++ b/Sources/SwiftParser/Declarations.swift @@ -52,6 +52,16 @@ extension TokenConsumer { } } + /// Check if the current token is at a start of any declaration. + /// + /// + /// - Parameters + /// - allowInitDecl: whether to consider 'init' a declaration in the context. + /// Only initializer bodies should use `false` for this. + /// - requiresDecl: Whether only declarations are expected in the context. + /// For example, in member blocks. + /// + /// - Note: this returns `false` for `#if` unless it's an attribute list. mutating func atStartOfDeclaration( allowInitDecl: Bool = true, requiresDecl: Bool = false @@ -111,7 +121,7 @@ extension TokenConsumer { var lookahead = subparser.lookahead() repeat { lookahead.consumeAnyToken() - } while lookahead.atStartOfDeclaration(allowInitDecl: allowInitDecl) + } while lookahead.atStartOfDeclaration(allowInitDecl: allowInitDecl, requiresDecl: requiresDecl) return lookahead.at(.identifier) case .lhs(.case): // When 'case' appears inside a function, it's probably a switch @@ -179,7 +189,7 @@ extension TokenConsumer { return true } } - // Special recovery 'try let/var'. + // Special recovery for 'try let/var'. if subparser.at(.keyword(.try)), subparser.peek(isAtAnyIn: VariableDeclSyntax.BindingSpecifierOptions.self) != nil { @@ -199,10 +209,6 @@ extension Parser { self.attributes = attributes self.modifiers = modifiers } - - var isEmpty: Bool { - attributes.isEmpty && modifiers.isEmpty - } } /// Describes the context around a declaration in order to modify how it is parsed. @@ -1010,7 +1016,7 @@ extension Parser { var elements = [RawMemberBlockItemSyntax]() do { var loopProgress = LoopProgressCondition() - while !self.at(.endOfFile) && !stopCondition(&self) && self.hasProgressed(&loopProgress) { + while !stopCondition(&self), !self.at(.endOfFile), self.hasProgressed(&loopProgress) { let newItemAtStartOfLine = self.atStartOfLine guard let newElement = self.parseMemberBlockItem(until: stopCondition) else { break diff --git a/Sources/SwiftParser/Directives.swift b/Sources/SwiftParser/Directives.swift index a4087ddc98e..3025a423b45 100644 --- a/Sources/SwiftParser/Directives.swift +++ b/Sources/SwiftParser/Directives.swift @@ -180,33 +180,6 @@ extension Parser { // `#elif` or `#elif(…)` could be macro invocations. return lookahead.at(TokenSpec(.identifier, allowAtStartOfLine: false)) } - - private mutating func parseIfConfigClauseElements( - _ parseElement: (_ parser: inout Parser, _ isFirstElement: Bool) -> Element?, - addSemicolonIfNeeded: ( - _ lastElement: Element, _ newItemAtStartOfLine: Bool, _ newItem: Element, _ parser: inout Parser - ) -> Element? - ) -> [Element] { - var elements = [Element]() - var elementsProgress = LoopProgressCondition() - while !self.at(.endOfFile) - && !self.at(.poundElse, .poundElseif, .poundEndif) - && !self.atElifTypo() - && self.hasProgressed(&elementsProgress) - { - let newItemAtStartOfLine = self.atStartOfLine - guard let element = parseElement(&self, elements.isEmpty), !element.isEmpty else { - break - } - if let lastElement = elements.last, - let fixedUpLastItem = addSemicolonIfNeeded(lastElement, newItemAtStartOfLine, element, &self) - { - elements[elements.count - 1] = fixedUpLastItem - } - elements.append(element) - } - return elements - } } extension Parser { diff --git a/Sources/SwiftParser/Expressions.swift b/Sources/SwiftParser/Expressions.swift index 354d2184d65..b768e586462 100644 --- a/Sources/SwiftParser/Expressions.swift +++ b/Sources/SwiftParser/Expressions.swift @@ -2332,7 +2332,7 @@ extension Parser { var elements = [RawSwitchCaseListSyntax.Element]() var elementsProgress = LoopProgressCondition() while !self.at(.endOfFile, .rightBrace), !self.atEndOfIfConfigClauseBody(), self.hasProgressed(&elementsProgress) { - if self.withLookahead({ $0.atStartOfSwitchCase(allowRecovery: false) }) { + if self.withLookahead({ $0.atStartOfSwitchCase() }) { elements.append(.switchCase(self.parseSwitchCase())) } else if self.canRecoverTo(.poundIf) != nil { // '#if' in 'case' position can enclose zero or more 'case' or 'default' @@ -2392,13 +2392,6 @@ extension Parser { if $0.at(.rightBrace, .keyword(.case), .keyword(.default)) || $0.atEndOfIfConfigClauseBody() { return true } - if $0.at(.atSign), - $0.withLookahead({ - $0.consumeAnyAttribute(); return $0.at(.keyword(.case), .keyword(.default)) - }) - { - return true - } if $0.withLookahead({ $0.atStartOfConditionalSwitchCases() }) { return true } diff --git a/Sources/SwiftParser/Statements.swift b/Sources/SwiftParser/Statements.swift index 3127d710cd8..a480b059ae3 100644 --- a/Sources/SwiftParser/Statements.swift +++ b/Sources/SwiftParser/Statements.swift @@ -1077,7 +1077,7 @@ extension Parser.Lookahead { /// Returns whether the parser's current position is the start of a switch case, /// given that we're in the middle of a switch already. - mutating func atStartOfSwitchCase(allowRecovery: Bool = false) -> Bool { + mutating func atStartOfSwitchCase() -> Bool { // Check for and consume attributes. The only valid attribute is `@unknown` // but that's a semantic restriction. var lookahead = self.lookahead() @@ -1092,11 +1092,7 @@ extension Parser.Lookahead { return true } - if allowRecovery { - return lookahead.canRecoverTo(anyIn: SwitchCaseStart.self) != nil - } else { - return lookahead.at(anyIn: SwitchCaseStart.self) != nil - } + return lookahead.at(anyIn: SwitchCaseStart.self) != nil } mutating func atStartOfConditionalSwitchCases() -> Bool { diff --git a/Sources/SwiftParser/generated/LayoutNodes+Parsable.swift b/Sources/SwiftParser/generated/LayoutNodes+Parsable.swift index 32ce354cd48..7dd4f5f89ab 100644 --- a/Sources/SwiftParser/generated/LayoutNodes+Parsable.swift +++ b/Sources/SwiftParser/generated/LayoutNodes+Parsable.swift @@ -238,7 +238,7 @@ extension VersionTupleSyntax: SyntaxParseable { fileprivate extension Parser { mutating func parseNonOptionalCodeBlockItem() -> RawCodeBlockItemSyntax { - guard let node = self.self.parseCodeBlockItem(allowInitDecl: true, until: { _ in + guard let node = self.parseCodeBlockItem(allowInitDecl: true, until: { _ in false }) else { // The missing item is not necessary to be a declaration, From 0eccca193535afef8b0cf1cf20e7d0e4a9921f37 Mon Sep 17 00:00:00 2001 From: Rintaro Ishizaki Date: Fri, 26 Sep 2025 20:52:25 -0700 Subject: [PATCH 08/17] tbs --- Sources/SwiftParser/Declarations.swift | 4 ++-- Sources/SwiftParser/Statements.swift | 2 -- Sources/SwiftParser/TokenPrecedence.swift | 18 +++++++++--------- Sources/SwiftParser/TopLevel.swift | 18 ------------------ 4 files changed, 11 insertions(+), 31 deletions(-) diff --git a/Sources/SwiftParser/Declarations.swift b/Sources/SwiftParser/Declarations.swift index bdec7faeef1..8b3c99315ac 100644 --- a/Sources/SwiftParser/Declarations.swift +++ b/Sources/SwiftParser/Declarations.swift @@ -52,8 +52,8 @@ extension TokenConsumer { } } - /// Check if the current token is at a start of any declaration. - /// + /// Returns `true` if the current token represents the start of a declaration + /// item. /// /// - Parameters /// - allowInitDecl: whether to consider 'init' a declaration in the context. diff --git a/Sources/SwiftParser/Statements.swift b/Sources/SwiftParser/Statements.swift index a480b059ae3..53ee258ce81 100644 --- a/Sources/SwiftParser/Statements.swift +++ b/Sources/SwiftParser/Statements.swift @@ -21,7 +21,6 @@ extension TokenConsumer { /// item. /// /// - Parameters: - /// - allowRecovery: Whether to attempt to perform recovery. /// - preferExpr: If either an expression or statement could be /// parsed and this parameter is `true`, the function returns `false` /// such that an expression can be parsed. @@ -1022,7 +1021,6 @@ extension Parser.Lookahead { /// item. /// /// - Parameters: - /// - allowRecovery: Whether to attempt to perform recovery. /// - preferExpr: If either an expression or statement could be /// parsed and this parameter is `true`, the function returns `false` /// such that an expression can be parsed. diff --git a/Sources/SwiftParser/TokenPrecedence.swift b/Sources/SwiftParser/TokenPrecedence.swift index 061e019882d..212d6dfbcfa 100644 --- a/Sources/SwiftParser/TokenPrecedence.swift +++ b/Sources/SwiftParser/TokenPrecedence.swift @@ -38,8 +38,6 @@ enum TokenPrecedence: Comparable { case mediumPunctuator /// The closing delimiter of `weakBracketed` case weakBracketClose - /// Keywords that start a new statement. - case stmtKeyword /// The '{' token because it typically marks the body of a declaration. /// `closingDelimiter` must have type `strongPunctuator` case openingBrace(closingDelimiter: RawTokenKind) @@ -47,7 +45,9 @@ enum TokenPrecedence: Comparable { case strongPunctuator /// The closing delimiter of `strongBracketed` case closingBrace - /// Tokens that start a new declaration + /// Keywords that start a new statement. + case stmtKeyword + /// Keywords that start a new declaration case declKeyword case openingPoundIf case closingPoundIf @@ -85,17 +85,17 @@ enum TokenPrecedence: Comparable { case .weakBracketClose: return 6 case .strongPunctuator: - return 8 + return 7 case .openingBrace: - return 9 + return 8 case .declKeyword, .stmtKeyword: - return 10 + return 9 case .closingBrace: - return 11 + return 10 case .openingPoundIf: - return 12 + return 11 case .closingPoundIf: - return 13 + return 12 } } diff --git a/Sources/SwiftParser/TopLevel.swift b/Sources/SwiftParser/TopLevel.swift index 4a2c0a97cca..90a26fa7635 100644 --- a/Sources/SwiftParser/TopLevel.swift +++ b/Sources/SwiftParser/TopLevel.swift @@ -133,24 +133,6 @@ extension Parser { ) } - mutating func fixWithSemi(codeBlockItem item: RawCodeBlockItemSyntax.Item) -> RawCodeBlockItemSyntax? { - let semi = self.consume(if: .semicolon) - var trailingSemis: [RawTokenSyntax] = [] - while let trailingSemi = self.consume(if: .semicolon) { - trailingSemis.append(trailingSemi) - } - - if item.raw.isEmpty && semi == nil && trailingSemis.isEmpty { - return nil - } - return RawCodeBlockItemSyntax( - item: item, - semicolon: semi, - RawUnexpectedNodesSyntax(trailingSemis, arena: self.arena), - arena: self.arena - ) - } - /// Parse an individual item - either in a code block or at the top level. /// /// Returns `nil` if the parser did not consume any tokens while trying to From 6d342162b39d0c44e06546d8cff09eb620a16691 Mon Sep 17 00:00:00 2001 From: Rintaro Ishizaki Date: Fri, 26 Sep 2025 21:25:42 -0700 Subject: [PATCH 09/17] comment --- Sources/SwiftParser/Directives.swift | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/Sources/SwiftParser/Directives.swift b/Sources/SwiftParser/Directives.swift index 3025a423b45..cd26429f3ca 100644 --- a/Sources/SwiftParser/Directives.swift +++ b/Sources/SwiftParser/Directives.swift @@ -46,20 +46,8 @@ extension Parser { /// Parse a conditional compilation block. /// - /// This function should be used to parse conditional compilation statements, - /// declarations, and expressions. It is generic over the particular kind of - /// parse that must occur for these elements, and allows a context-specific - /// syntax kind to be emitted to collect the results. For example, declaration - /// parsing parses items and collects the items into a ``MemberDeclListSyntax`` - /// node. - /// /// - Parameters: - /// - parseElement: Parse an element of the conditional compilation block. - /// - addSemicolonIfNeeded: If elements need to be separated by a newline, this - /// allows the insertion of missing semicolons to the - /// previous element. - /// - syntax: A function that aggregates the parsed conditional elements - /// into a syntax collection. + /// - parseBody: Parse a body of single conditional compilation clause. mutating func parsePoundIfDirective( _ parseBody: (_ parser: inout Parser) -> RawIfConfigClauseSyntax.Elements? ) -> RawIfConfigDeclSyntax { From 7d97ad94e988fc3182d58f0dca6ccea83bb13d7c Mon Sep 17 00:00:00 2001 From: Rintaro Ishizaki Date: Fri, 26 Sep 2025 21:59:21 -0700 Subject: [PATCH 10/17] cleanup --- Tests/SwiftParserTest/Assertions.swift | 10 ---------- .../translated/EscapedIdentifiersTests.swift | 6 +++++- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/Tests/SwiftParserTest/Assertions.swift b/Tests/SwiftParserTest/Assertions.swift index 2c1c6f03a08..da6f7008056 100644 --- a/Tests/SwiftParserTest/Assertions.swift +++ b/Tests/SwiftParserTest/Assertions.swift @@ -292,16 +292,6 @@ struct DiagnosticSpec { self.file = file self.line = line } - - static func consecutiveStatementsOnALineDiagnosticSpec( - locationMarker: String = "1️⃣" - ) -> Self { - DiagnosticSpec( - locationMarker: locationMarker, - message: "consecutive statements on a line must be separated by newline or ';'", - fixIts: ["insert newline", "insert ';'"] - ) - } } /// Assert that `location` is the same as that of `locationMarker` in `tree`. diff --git a/Tests/SwiftParserTest/translated/EscapedIdentifiersTests.swift b/Tests/SwiftParserTest/translated/EscapedIdentifiersTests.swift index 0427a76177c..c47d921ec53 100644 --- a/Tests/SwiftParserTest/translated/EscapedIdentifiersTests.swift +++ b/Tests/SwiftParserTest/translated/EscapedIdentifiersTests.swift @@ -209,7 +209,11 @@ final class EscapedIdentifiersTests: ParserTestCase { """, diagnostics: [ DiagnosticSpec(locationMarker: "1️⃣", message: "unexpected code '`multiline is' in source file"), - .consecutiveStatementsOnALineDiagnosticSpec(locationMarker: "2️⃣"), + DiagnosticSpec( + locationMarker: "2️⃣", + message: "consecutive statements on a line must be separated by newline or ';'", + fixIts: ["insert newline", "insert ';'"] + ), DiagnosticSpec(locationMarker: "3️⃣", message: "unexpected code '` = 5' in source file"), ], fixedSource: """ From a61a1a79a067bf5904093ffe21b64b82e9d4d5a4 Mon Sep 17 00:00:00 2001 From: Rintaro Ishizaki Date: Fri, 26 Sep 2025 22:47:03 -0700 Subject: [PATCH 11/17] format --- .../Tests/ValidateSyntaxNodes/ValidateSyntaxNodes.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CodeGeneration/Tests/ValidateSyntaxNodes/ValidateSyntaxNodes.swift b/CodeGeneration/Tests/ValidateSyntaxNodes/ValidateSyntaxNodes.swift index 274208775bb..a686edb059d 100644 --- a/CodeGeneration/Tests/ValidateSyntaxNodes/ValidateSyntaxNodes.swift +++ b/CodeGeneration/Tests/ValidateSyntaxNodes/ValidateSyntaxNodes.swift @@ -645,7 +645,7 @@ class ValidateSyntaxNodes: XCTestCase { ValidationFailure( node: .unexpectedCodeDecl, message: "child 'unexpectedCode' is a SyntaxCollection but child is not marked as a collection" - ), + ) ] ) } From f9a97cf14def1e70ba3aeb28cedcd5553896ef5e Mon Sep 17 00:00:00 2001 From: Rintaro Ishizaki Date: Sat, 27 Sep 2025 20:08:01 -0700 Subject: [PATCH 12/17] fix --- Sources/SwiftParser/Declarations.swift | 2 +- Tests/SwiftParserTest/Parser+EntryTests.swift | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftParser/Declarations.swift b/Sources/SwiftParser/Declarations.swift index 8b3c99315ac..8a8b7481a43 100644 --- a/Sources/SwiftParser/Declarations.swift +++ b/Sources/SwiftParser/Declarations.swift @@ -2377,7 +2377,7 @@ extension Parser { extension Parser { mutating func parseDeclarationOrIfConfig() -> RawDeclSyntax { - if self.at(.poundIf) { + if self.at(.poundIf) && !self.withLookahead({ $0.consumeIfConfigOfAttributes() }) { return RawDeclSyntax( self.parsePoundIfDirective({ .decls($0.parseMemberDeclList(until: { $0.atEndOfIfConfigClauseBody() })) diff --git a/Tests/SwiftParserTest/Parser+EntryTests.swift b/Tests/SwiftParserTest/Parser+EntryTests.swift index 73ad877866a..223f872ff21 100644 --- a/Tests/SwiftParserTest/Parser+EntryTests.swift +++ b/Tests/SwiftParserTest/Parser+EntryTests.swift @@ -70,6 +70,36 @@ class EntryTests: ParserTestCase { ) } + func testDeclSyntaxParseIfConfigAttr() throws { + assertParse( + """ + #if FLAG + @attr + #endif + func test() {} + """, + { DeclSyntax.parse(from: &$0) }, + substructure: FunctionDeclSyntax( + attributes: [ + .ifConfigDecl(IfConfigDeclSyntax(clauses: [ + IfConfigClauseSyntax( + poundKeyword: .poundIfToken(), + condition: DeclReferenceExprSyntax(baseName: .identifier("FLAG")), + elements: .attributes([ + .attribute(AttributeSyntax(TypeSyntax(IdentifierTypeSyntax(name: .identifier("attr"))))) + ])) + ])) + ], + funcKeyword: .keyword(.func), + name: .identifier("test"), + signature: FunctionSignatureSyntax( + parameterClause: FunctionParameterClauseSyntax(parameters: []) + ), + body: CodeBlockSyntax(statements: []) + ) + ) + } + func testRemainderUnexpected() throws { assertParse( "func test() {} 1️⃣other tokens", From 229d5dce9860df8b848cada5a283866484ce7066 Mon Sep 17 00:00:00 2001 From: Rintaro Ishizaki Date: Sat, 27 Sep 2025 21:07:49 -0700 Subject: [PATCH 13/17] format --- Tests/SwiftParserTest/Parser+EntryTests.swift | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/Tests/SwiftParserTest/Parser+EntryTests.swift b/Tests/SwiftParserTest/Parser+EntryTests.swift index 223f872ff21..91ca7a12e2c 100644 --- a/Tests/SwiftParserTest/Parser+EntryTests.swift +++ b/Tests/SwiftParserTest/Parser+EntryTests.swift @@ -81,14 +81,17 @@ class EntryTests: ParserTestCase { { DeclSyntax.parse(from: &$0) }, substructure: FunctionDeclSyntax( attributes: [ - .ifConfigDecl(IfConfigDeclSyntax(clauses: [ - IfConfigClauseSyntax( - poundKeyword: .poundIfToken(), - condition: DeclReferenceExprSyntax(baseName: .identifier("FLAG")), - elements: .attributes([ - .attribute(AttributeSyntax(TypeSyntax(IdentifierTypeSyntax(name: .identifier("attr"))))) - ])) - ])) + .ifConfigDecl( + IfConfigDeclSyntax(clauses: [ + IfConfigClauseSyntax( + poundKeyword: .poundIfToken(), + condition: DeclReferenceExprSyntax(baseName: .identifier("FLAG")), + elements: .attributes([ + .attribute(AttributeSyntax(TypeSyntax(IdentifierTypeSyntax(name: .identifier("attr"))))) + ]) + ) + ]) + ) ], funcKeyword: .keyword(.func), name: .identifier("test"), From dfab212320364b45cc1add2bcfc89ee10dc8ac48 Mon Sep 17 00:00:00 2001 From: Rintaro Ishizaki Date: Mon, 29 Sep 2025 10:30:56 -0700 Subject: [PATCH 14/17] [Parser] @unknown default at statement level --- Sources/SwiftParser/TopLevel.swift | 10 +++++----- Tests/SwiftParserTest/StatementTests.swift | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/Sources/SwiftParser/TopLevel.swift b/Sources/SwiftParser/TopLevel.swift index 90a26fa7635..f9c4e1f53db 100644 --- a/Sources/SwiftParser/TopLevel.swift +++ b/Sources/SwiftParser/TopLevel.swift @@ -188,11 +188,6 @@ extension Parser { } else if self.atStartOfExpression() { item = .expr(self.parseExpression(flavor: .basic, pattern: .none)) attachSemi = true - } else if (self.at(.atSign) && peek(isAt: .identifier)) || self.at(anyIn: DeclarationModifier.self) != nil { - // Force parsing '@' as a declaration, as there's no valid - // expression or statement starting with an attribute. - item = .decl(self.parseDeclaration()) - attachSemi = true } else if self.withLookahead({ $0.atStartOfSwitchCase() }) { // 'case' and 'default' are invalid in code block items. // Parse them and put them in their own CodeBlockItem but as an unexpected node. @@ -203,6 +198,11 @@ extension Parser { semicolon: nil, arena: self.arena ) + } else if (self.at(.atSign) && peek(isAt: .identifier)) || self.at(anyIn: DeclarationModifier.self) != nil { + // Force parsing '@' as a declaration, as there's no valid + // expression or statement starting with an attribute. + item = .decl(self.parseDeclaration()) + attachSemi = true } else { // Otherwise, eat the unexpected tokens into an "decl". item = .decl( diff --git a/Tests/SwiftParserTest/StatementTests.swift b/Tests/SwiftParserTest/StatementTests.swift index 52418fffb10..228aec6d6ee 100644 --- a/Tests/SwiftParserTest/StatementTests.swift +++ b/Tests/SwiftParserTest/StatementTests.swift @@ -257,6 +257,20 @@ final class StatementTests: ParserTestCase { ) } + func testUnknownDefaultAtStatement() { + assertParse( + """ + func test() { + 1️⃣@unknown default: + return + } + """, + diagnostics: [ + DiagnosticSpec(message: "'default' label can only appear inside a 'switch' statement") + ] + ) + } + func testMissingIfClauseIntroducer() { assertParse("if _ = 42 {}") } From 161643bd6ec4eca8b181ca4e259e310c7bd5925a Mon Sep 17 00:00:00 2001 From: Rintaro Ishizaki Date: Mon, 29 Sep 2025 10:31:28 -0700 Subject: [PATCH 15/17] comment --- Sources/SwiftParser/Attributes.swift | 2 +- Sources/SwiftParser/TopLevel.swift | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/Sources/SwiftParser/Attributes.swift b/Sources/SwiftParser/Attributes.swift index 44aae5967f1..9add4a654ae 100644 --- a/Sources/SwiftParser/Attributes.swift +++ b/Sources/SwiftParser/Attributes.swift @@ -895,7 +895,7 @@ extension Parser { let decl: RawDeclSyntax if self.at(.poundIf) { // '#if' is not accepted in '@abi' attribute, but for recovery, parse it - // parse it and wrap the first decl init with unexpected nodes. + // and wrap the first decl in it with unexpected nodes. let ifConfig = self.parsePoundIfDirective({ parser in let decl = parser.parseDeclaration(in: .argumentList) let member = RawMemberBlockItemSyntax(decl: decl, semicolon: nil, arena: parser.arena) diff --git a/Sources/SwiftParser/TopLevel.swift b/Sources/SwiftParser/TopLevel.swift index f9c4e1f53db..9ceaeed59d0 100644 --- a/Sources/SwiftParser/TopLevel.swift +++ b/Sources/SwiftParser/TopLevel.swift @@ -137,12 +137,6 @@ extension Parser { /// /// Returns `nil` if the parser did not consume any tokens while trying to /// parse the code block item. - /// - /// `isAtTopLevel` determines whether this is trying to parse an item that's at - /// the top level of the source file. If this is the case, we allow skipping - /// closing braces while trying to recover to the next item. - /// If we are not at the top level, such a closing brace should close the - /// wrapping declaration instead of being consumed by lookahead. mutating func parseCodeBlockItem( allowInitDecl: Bool, until stopCondition: (inout Parser) -> Bool From e8bbace5df93319fe68e38b049a858323bdcc37d Mon Sep 17 00:00:00 2001 From: Rintaro Ishizaki Date: Wed, 1 Oct 2025 10:52:47 -0700 Subject: [PATCH 16/17] Don't interleave UexpectedNodes in UnexpectedCodeDeclSyntax --- .../Sources/SyntaxSupport/CommonNodes.swift | 3 +- .../Sources/SyntaxSupport/Node.swift | 5 ++-- .../generated/ChildNameForKeyPath.swift | 4 --- .../generated/raw/RawSyntaxNodesTUVWXYZ.swift | 23 +++----------- .../generated/raw/RawSyntaxValidation.swift | 6 ++-- .../syntaxNodes/SyntaxNodesTUVWXYZ.swift | 30 ++++--------------- 6 files changed, 16 insertions(+), 55 deletions(-) diff --git a/CodeGeneration/Sources/SyntaxSupport/CommonNodes.swift b/CodeGeneration/Sources/SyntaxSupport/CommonNodes.swift index 39d226abe4e..d3875576887 100644 --- a/CodeGeneration/Sources/SyntaxSupport/CommonNodes.swift +++ b/CodeGeneration/Sources/SyntaxSupport/CommonNodes.swift @@ -412,6 +412,7 @@ public let COMMON_NODES: [Node] = [ // NOTE: This is not .collection() on purpose. We don't need collection related functions for this. kind: .node(kind: .unexpectedNodes) ) - ] + ], + noInterleaveUnexpected: true ), ] diff --git a/CodeGeneration/Sources/SyntaxSupport/Node.swift b/CodeGeneration/Sources/SyntaxSupport/Node.swift index 7a4eb9631b3..cfa56edad03 100644 --- a/CodeGeneration/Sources/SyntaxSupport/Node.swift +++ b/CodeGeneration/Sources/SyntaxSupport/Node.swift @@ -127,7 +127,8 @@ public class Node: NodeChoiceConvertible { parserFunction: TokenSyntax? = nil, traits: [String] = [], children: [Child] = [], - childHistory: Child.History = [] + childHistory: Child.History = [], + noInterleaveUnexpected: Bool = false ) { precondition(base != .syntaxCollection) precondition(base.isBase, "unknown base kind '\(base)' for node '\(kind)'") @@ -140,7 +141,7 @@ public class Node: NodeChoiceConvertible { self.documentation = SwiftSyntax.Trivia.docCommentTrivia(from: documentation) self.parserFunction = parserFunction - let childrenWithUnexpected = kind.isBase ? children : interleaveUnexpectedChildren(children) + let childrenWithUnexpected = (kind.isBase || noInterleaveUnexpected) ? children : interleaveUnexpectedChildren(children) self.data = .layout(children: childrenWithUnexpected, childHistory: childHistory, traits: traits) } diff --git a/Sources/SwiftSyntax/generated/ChildNameForKeyPath.swift b/Sources/SwiftSyntax/generated/ChildNameForKeyPath.swift index b627cd56075..db537fb970b 100644 --- a/Sources/SwiftSyntax/generated/ChildNameForKeyPath.swift +++ b/Sources/SwiftSyntax/generated/ChildNameForKeyPath.swift @@ -3444,12 +3444,8 @@ public func childName(_ keyPath: AnyKeyPath) -> String? { return "value" case \TypeInitializerClauseSyntax.unexpectedAfterValue: return "unexpectedAfterValue" - case \UnexpectedCodeDeclSyntax.unexpectedBeforeUnexpectedCode: - return "unexpectedBeforeUnexpectedCode" case \UnexpectedCodeDeclSyntax.unexpectedCode: return "unexpectedCode" - case \UnexpectedCodeDeclSyntax.unexpectedAfterUnexpectedCode: - return "unexpectedAfterUnexpectedCode" case \UnresolvedAsExprSyntax.unexpectedBeforeAsKeyword: return "unexpectedBeforeAsKeyword" case \UnresolvedAsExprSyntax.asKeyword: diff --git a/Sources/SwiftSyntax/generated/raw/RawSyntaxNodesTUVWXYZ.swift b/Sources/SwiftSyntax/generated/raw/RawSyntaxNodesTUVWXYZ.swift index 72034364441..b61e8335b43 100644 --- a/Sources/SwiftSyntax/generated/raw/RawSyntaxNodesTUVWXYZ.swift +++ b/Sources/SwiftSyntax/generated/raw/RawSyntaxNodesTUVWXYZ.swift @@ -1562,32 +1562,17 @@ public struct RawUnexpectedCodeDeclSyntax: RawDeclSyntaxNodeProtocol { self.init(unchecked: other.raw) } - public init( - _ unexpectedBeforeUnexpectedCode: RawUnexpectedNodesSyntax? = nil, - unexpectedCode: RawUnexpectedNodesSyntax, - _ unexpectedAfterUnexpectedCode: RawUnexpectedNodesSyntax? = nil, - arena: __shared RawSyntaxArena - ) { + public init(unexpectedCode: RawUnexpectedNodesSyntax, arena: __shared RawSyntaxArena) { let raw = RawSyntax.makeLayout( - kind: .unexpectedCodeDecl, uninitializedCount: 3, arena: arena) { layout in + kind: .unexpectedCodeDecl, uninitializedCount: 1, arena: arena) { layout in layout.initialize(repeating: nil) - layout[0] = unexpectedBeforeUnexpectedCode?.raw - layout[1] = unexpectedCode.raw - layout[2] = unexpectedAfterUnexpectedCode?.raw + layout[0] = unexpectedCode.raw } self.init(unchecked: raw) } - public var unexpectedBeforeUnexpectedCode: RawUnexpectedNodesSyntax? { - layoutView.children[0].map(RawUnexpectedNodesSyntax.init(raw:)) - } - public var unexpectedCode: RawUnexpectedNodesSyntax { - layoutView.children[1].map(RawUnexpectedNodesSyntax.init(raw:))! - } - - public var unexpectedAfterUnexpectedCode: RawUnexpectedNodesSyntax? { - layoutView.children[2].map(RawUnexpectedNodesSyntax.init(raw:)) + layoutView.children[0].map(RawUnexpectedNodesSyntax.init(raw:))! } } diff --git a/Sources/SwiftSyntax/generated/raw/RawSyntaxValidation.swift b/Sources/SwiftSyntax/generated/raw/RawSyntaxValidation.swift index feca969e1bb..2592e549bbd 100644 --- a/Sources/SwiftSyntax/generated/raw/RawSyntaxValidation.swift +++ b/Sources/SwiftSyntax/generated/raw/RawSyntaxValidation.swift @@ -3007,10 +3007,8 @@ func validateLayout(layout: RawSyntaxBuffer, as kind: SyntaxKind) { } } func validateUnexpectedCodeDeclSyntax(kind: SyntaxKind, layout: RawSyntaxBuffer) { - assert(layout.count == 3) - assertNoError(kind, 0, verify(layout[0], as: RawUnexpectedNodesSyntax?.self)) - assertNoError(kind, 1, verify(layout[1], as: RawUnexpectedNodesSyntax.self)) - assertNoError(kind, 2, verify(layout[2], as: RawUnexpectedNodesSyntax?.self)) + assert(layout.count == 1) + assertNoError(kind, 0, verify(layout[0], as: RawUnexpectedNodesSyntax.self)) } func validateUnexpectedNodesSyntax(kind: SyntaxKind, layout: RawSyntaxBuffer) { for (index, element) in layout.enumerated() { diff --git a/Sources/SwiftSyntax/generated/syntaxNodes/SyntaxNodesTUVWXYZ.swift b/Sources/SwiftSyntax/generated/syntaxNodes/SyntaxNodesTUVWXYZ.swift index 86f5a7b9e89..40a3bc8890c 100644 --- a/Sources/SwiftSyntax/generated/syntaxNodes/SyntaxNodesTUVWXYZ.swift +++ b/Sources/SwiftSyntax/generated/syntaxNodes/SyntaxNodesTUVWXYZ.swift @@ -2633,15 +2633,13 @@ public struct UnexpectedCodeDeclSyntax: DeclSyntaxProtocol, SyntaxHashable, _Lea /// - trailingTrivia: Trivia to be appended to the trailing trivia of the node’s last token. If the node is empty, there is no token to attach the trivia to and the parameter is ignored. public init( leadingTrivia: Trivia? = nil, - _ unexpectedBeforeUnexpectedCode: UnexpectedNodesSyntax? = nil, unexpectedCode: UnexpectedNodesSyntax, - _ unexpectedAfterUnexpectedCode: UnexpectedNodesSyntax? = nil, trailingTrivia: Trivia? = nil ) { // Extend the lifetime of all parameters so their arenas don't get destroyed // before they can be added as children of the new arena. - self = withExtendedLifetime((RawSyntaxArena(), (unexpectedBeforeUnexpectedCode, unexpectedCode, unexpectedAfterUnexpectedCode))) { (arena, _) in - let layout: [RawSyntax?] = [unexpectedBeforeUnexpectedCode?.raw, unexpectedCode.raw, unexpectedAfterUnexpectedCode?.raw] + self = withExtendedLifetime((RawSyntaxArena(), (unexpectedCode))) { (arena, _) in + let layout: [RawSyntax?] = [unexpectedCode.raw] let raw = RawSyntax.makeLayout( kind: SyntaxKind.unexpectedCodeDecl, from: layout, @@ -2653,34 +2651,16 @@ public struct UnexpectedCodeDeclSyntax: DeclSyntaxProtocol, SyntaxHashable, _Lea } } - public var unexpectedBeforeUnexpectedCode: UnexpectedNodesSyntax? { - get { - return Syntax(self).child(at: 0)?.cast(UnexpectedNodesSyntax.self) - } - set(value) { - self = Syntax(self).replacingChild(at: 0, with: Syntax(value), rawAllocationArena: RawSyntaxArena()).cast(UnexpectedCodeDeclSyntax.self) - } - } - public var unexpectedCode: UnexpectedNodesSyntax { get { - return Syntax(self).child(at: 1)!.cast(UnexpectedNodesSyntax.self) + return Syntax(self).child(at: 0)!.cast(UnexpectedNodesSyntax.self) } set(value) { - self = Syntax(self).replacingChild(at: 1, with: Syntax(value), rawAllocationArena: RawSyntaxArena()).cast(UnexpectedCodeDeclSyntax.self) - } - } - - public var unexpectedAfterUnexpectedCode: UnexpectedNodesSyntax? { - get { - return Syntax(self).child(at: 2)?.cast(UnexpectedNodesSyntax.self) - } - set(value) { - self = Syntax(self).replacingChild(at: 2, with: Syntax(value), rawAllocationArena: RawSyntaxArena()).cast(UnexpectedCodeDeclSyntax.self) + self = Syntax(self).replacingChild(at: 0, with: Syntax(value), rawAllocationArena: RawSyntaxArena()).cast(UnexpectedCodeDeclSyntax.self) } } - public static let structure: SyntaxNodeStructure = .layout([\Self.unexpectedBeforeUnexpectedCode, \Self.unexpectedCode, \Self.unexpectedAfterUnexpectedCode]) + public static let structure: SyntaxNodeStructure = .layout([\Self.unexpectedCode]) } // MARK: - UnresolvedAsExprSyntax From ce47be24b6f25413c6610da920831d60aee42ddf Mon Sep 17 00:00:00 2001 From: Rintaro Ishizaki Date: Thu, 2 Oct 2025 21:05:08 -0700 Subject: [PATCH 17/17] tweak --- CodeGeneration/Sources/SyntaxSupport/Node.swift | 3 ++- Tests/SwiftParserTest/DirectiveTests.swift | 1 - Tests/SwiftParserTest/ExpressionTests.swift | 4 ++-- Tests/SwiftParserTest/translated/ErrorsTests.swift | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CodeGeneration/Sources/SyntaxSupport/Node.swift b/CodeGeneration/Sources/SyntaxSupport/Node.swift index cfa56edad03..73fa9404835 100644 --- a/CodeGeneration/Sources/SyntaxSupport/Node.swift +++ b/CodeGeneration/Sources/SyntaxSupport/Node.swift @@ -141,7 +141,8 @@ public class Node: NodeChoiceConvertible { self.documentation = SwiftSyntax.Trivia.docCommentTrivia(from: documentation) self.parserFunction = parserFunction - let childrenWithUnexpected = (kind.isBase || noInterleaveUnexpected) ? children : interleaveUnexpectedChildren(children) + let childrenWithUnexpected = + (kind.isBase || noInterleaveUnexpected) ? children : interleaveUnexpectedChildren(children) self.data = .layout(children: childrenWithUnexpected, childHistory: childHistory, traits: traits) } diff --git a/Tests/SwiftParserTest/DirectiveTests.swift b/Tests/SwiftParserTest/DirectiveTests.swift index afe89089962..35848f2db25 100644 --- a/Tests/SwiftParserTest/DirectiveTests.swift +++ b/Tests/SwiftParserTest/DirectiveTests.swift @@ -610,5 +610,4 @@ final class DirectiveTests: ParserTestCase { """ ) } - } diff --git a/Tests/SwiftParserTest/ExpressionTests.swift b/Tests/SwiftParserTest/ExpressionTests.swift index 6ef894c90d8..41ac9978167 100644 --- a/Tests/SwiftParserTest/ExpressionTests.swift +++ b/Tests/SwiftParserTest/ExpressionTests.swift @@ -2715,13 +2715,13 @@ final class StatementExpressionTests: ParserTestCase { diagnostics: [ DiagnosticSpec( locationMarker: "1️⃣", - // FIXME: "expected attribute name after '@'". + // FIXME: "expected attribute name after '@'". https://github.com/swiftlang/swift-syntax/issues/3159 message: "expected type in attribute", fixIts: ["insert type"] ), DiagnosticSpec( locationMarker: "2️⃣", - // FIXME: "expected pattern and ':' in switch case" + // FIXME: "expected pattern and ':' in switch case". https://github.com/swiftlang/swift-syntax/issues/3158 message: "expected expression and ':' in switch case", fixIts: ["insert expression and ':'"] ), diff --git a/Tests/SwiftParserTest/translated/ErrorsTests.swift b/Tests/SwiftParserTest/translated/ErrorsTests.swift index 351779ca78d..cb79adc7483 100644 --- a/Tests/SwiftParserTest/translated/ErrorsTests.swift +++ b/Tests/SwiftParserTest/translated/ErrorsTests.swift @@ -279,7 +279,7 @@ final class ErrorsTests: ParserTestCase { assertParse( """ func incompleteThrowType() { - let _: ()1️⃣ 2️⃣throws + let _: () 1️⃣throws } """, substructure: CodeBlockSyntax( @@ -311,7 +311,7 @@ final class ErrorsTests: ParserTestCase { ]) ), diagnostics: [ - DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected 'throws' keyword in function") + DiagnosticSpec(message: "unexpected 'throws' keyword in function") ] ) }