Skip to content

Describe a missing decl with attributes or modifiers as "declaration"#3338

Open
broken-circle wants to merge 1 commit into
swiftlang:mainfrom
broken-circle:missing-decl-description
Open

Describe a missing decl with attributes or modifiers as "declaration"#3338
broken-circle wants to merge 1 commit into
swiftlang:mainfrom
broken-circle:missing-decl-description

Conversation

@broken-circle

Copy link
Copy Markdown
Member

MissingNodesError already recognizes a MissingDeclSyntax carrying attributes or modifiers as a declaration, and afterClause uses that signal to produce tailored diagnostics. This PR extends the same recognition to the description half of the message, which is currently computed independently and can disagree.

For example:

func foo() {
  @attr1️⃣ // error: expected statements after attribute
          // fix-it: insert statements
}
struct S {}

The misleading description comes from the climb in the .node case of NodesDescriptionPart.description(format:), which walks up through single-child parents looking for a childNameInParent. For a MissingDeclSyntax alone in a function body's code block, the chain MissingDecl → CodeBlockItem → CodeBlockItemList consists entirely of single-child parents, so the climb reaches CodeBlockItemList and returns its slot name "statements". For the same MissingDeclSyntax at the top level alongside other statements, the surrounding CodeBlockItemList has more than one child, the climb terminates early, and the fallback nodeTypeNameForDiagnostics(allowBlockNames:) returns "declaration". The diagnostic wording therefore depends on whether other items happen to sit alongside the missing one, which is structural noise unrelated to what the user wrote.

After this PR, the code above produces an expected declaration diagnostic and an insert declaration fix-it.

Alternatives considered

Fix the climb logic

A general fix to the climb logic may involve annotating each collection-typed child's nameForDiagnostics with information regarding whether it names the aggregate (e.g. "statements", "members") or an element (e.g. "condition" for ConditionElementListSyntax in a guard). This could be something like enum NamingKind, and would be plumbed into childNameForDiagnostics(_:) to be consulted during the climb. This is probably the principled solution, but it crosses into code-generation and substantially expands the PR's scope for currently just one FIXME.

Never take a collection name

The childNameInParent of a SyntaxCollection names the collection slot (e.g. "statements", "elements", "arguments"), not any individual element inside it. When describing a single missing element that happens to be the only thing in such a collection, that name is sometimes misleading, since it would describe the missing thing as if it were the whole list. This change would treat collections as transparent in the climb, so we either find a more specific named slot above, or fall through to the node's own type name.

This is implemented with a one-line change in the .node case of NodesDescriptionPart.description(format:):

      var walk: Syntax = node
      while true {
+       if !walk.kind.isSyntaxCollection, let childName = walk.childNameInParent {
-       if let childName = walk.childNameInParent {
          return childName
        }
        if let parent = walk.parent, parent.children(viewMode: .all).count == 1 {
          walk = parent
        } else {
          break
        }
      }

However, the childNameInParent of a SyntaxCollection isn't always an aggregate name. Some collections (e.g. ConditionElementListSyntax in a guard statement) carry an element-naming slot name like "condition", which is the correct description for a missing element inside them. The climb has no way to tell these two kinds apart: both are simply nameForDiagnostics: String with nothing further to discriminate on, so a general "skip-collections" rule regresses the element-naming cases. Concretely, with this change applied, GuardTests.testGuard2() regresses from expected condition to expected expression, which is accurate but less useful, and inconsistent with the corresponding guard/if/while/repeat tests.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant