From 50f9ea854328e213c3134f851bcb4edc02d0cdaa Mon Sep 17 00:00:00 2001 From: broken-circle <252359939+broken-circle@users.noreply.github.com> Date: Wed, 20 May 2026 07:21:59 -0700 Subject: [PATCH] Describe a missing decl with attributes or modifiers as "declaration" When producing a description for a single `MissingDeclSyntax` that already carries attributes or modifiers, the parser has committed to it being a declaration, so describe it that way directly. The general climb logic in `nodesDescriptionAndCommonParent(_:)` would otherwise pick up the surrounding container's slot name (e.g. `"statements"` for a code block), which is accurate to the grammar but misleading as a description of the missing element. --- .../MissingNodesError.swift | 28 +++++++++++++++++-- .../translated/RecoveryTests.swift | 3 +- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/Sources/SwiftParserDiagnostics/MissingNodesError.swift b/Sources/SwiftParserDiagnostics/MissingNodesError.swift index b416ee3ab15..744c2ec6b29 100644 --- a/Sources/SwiftParserDiagnostics/MissingNodesError.swift +++ b/Sources/SwiftParserDiagnostics/MissingNodesError.swift @@ -178,6 +178,28 @@ func nodesDescriptionAndCommonParent( return (nil, formatDescriptions(partDescriptions)) } +/// Like `nodesDescriptionAndCommonParent`, but applies special-case overrides +/// for missing-node arrangements whose surrounding context would otherwise +/// produce a misleading description. +private func missingNodesDescriptionAndCommonParent( + _ missingNodes: [Syntax], + format: Bool +) -> (commonAncestor: Syntax?, description: String) { + // When producing a description for a single `MissingDeclSyntax` that already + // carries attributes or modifiers, the parser has committed to "this should + // be a declaration"; describe it that way directly. The general climb logic + // in `nodesDescriptionAndCommonParent` would otherwise pick up the surrounding + // container's slot name (e.g. "statements" for a code block), which is accurate + // to the grammar but misleading as a description of the missing element. + if missingNodes.count == 1, + let missingDecl = missingNodes.first?.as(MissingDeclSyntax.self), + !missingDecl.attributes.isEmpty || !missingDecl.modifiers.isEmpty + { + return (nil, "declaration") + } + return nodesDescriptionAndCommonParent(missingNodes, format: format) +} + /// Formats an array of descriptions into a single string. /// /// This function takes an array of descriptions and formats them into a single string. Depending on the number @@ -337,7 +359,7 @@ public struct MissingNodesError: ParserError { } public var message: String { - let (anchor, description) = nodesDescriptionAndCommonParent(missingNodes, format: true) + let (anchor, description) = missingNodesDescriptionAndCommonParent(missingNodes, format: true) var message = "expected \(description)" if let afterClause { message += " \(afterClause)" @@ -367,7 +389,9 @@ public struct InsertTokenFixIt: ParserFixIt { self.missingNodes = missingNodes } - public var message: String { "insert \(nodesDescription(missingNodes, format: true))" } + public var message: String { + "insert \(missingNodesDescriptionAndCommonParent(missingNodes, format: true).description)" + } } // MARK: - Generate Error diff --git a/Tests/SwiftParserTest/translated/RecoveryTests.swift b/Tests/SwiftParserTest/translated/RecoveryTests.swift index 55595d9de29..88711c31dbd 100644 --- a/Tests/SwiftParserTest/translated/RecoveryTests.swift +++ b/Tests/SwiftParserTest/translated/RecoveryTests.swift @@ -3419,8 +3419,7 @@ final class RecoveryTests: ParserTestCase { ), ]), diagnostics: [ - // FIXME: expected *declaration* after attribute - DiagnosticSpec(message: "expected statements after attribute", fixIts: ["insert statements"]) + DiagnosticSpec(message: "expected declaration after attribute", fixIts: ["insert declaration"]) ], fixedSource: """ func foo() {