Skip to content

type_pattern doesn't accept a final modifier in switch case labels, even though instanceof_expression does #229

Description

@DragonAxe

Disclaimer: This issue was found and documented by Claude while I was working on an auto-formatter for java. I apologize for any inconvenience this might cause. I, the author, am willing to make any adjustments or clarifications if needed.

Java allows an optional final modifier on a pattern variable, for both instanceof patterns (JEP 394/405) and switch pattern matching (JEP 441). For example:

    return switch (element) {
      case final Element.Server server -> makeServerNode(server);
      case final Element.Markup markup -> makeMarkupNode(markup);
    };

instanceof final Type x parses correctly, but the equivalent case final Type x -> in a switch does not — it produces an ERROR node.

Looking at grammar.js, instanceof_expression explicitly includes optional('final'), but type_pattern (used by switch_label) does not:

// instanceof_expression -- handles `final` correctly
seq($.expression, 'instanceof', optional('final'), $._unannotated_type, $.identifier)

// type_pattern -- used inside switch_label's `case` patterns, no `final` support
type_pattern: $ => seq($._unannotated_type, choice($.identifier, $._reserved_identifier)),

It looks like final support was added to instanceof_expression in #124 ("Fix issues with records and support instanceof final") but never carried over to type_pattern when switch pattern matching was completed in #158.

Reproduction

Works — plain switch pattern, no modifier:

class Foo {
  void m(Object o) {
    switch (o) {
      case String s -> System.out.println(s);
      default -> System.out.println("other");
    }
  }
}

Worksinstanceof with final:

class Foo {
  void m(Object o) {
    if (o instanceof final String s) {
      System.out.println(s);
    }
  }
}

Fails — switch pattern with final:

class Foo {
  void m(Object o) {
    switch (o) {
      case final String s -> System.out.println(s);
      default -> System.out.println("other");
    }
  }
}

Actual behavior

Parsing the failing example produces (relevant excerpt, tree-sitter parse):

switch_label
  case (anon)
  pattern
    type_pattern
      type_identifier "final"      <- "final" is consumed as the pattern's TYPE
      identifier "String"          <- "String" is consumed as the pattern's VARIABLE NAME
ERROR
  identifier "s"                   <- the real variable name is left over as an ERROR node
-> (anon)

Since type_pattern has no rule for a leading final keyword, the parser treats final itself as _unannotated_type (a bare identifier) and String as the pattern's binding identifier, leaving the real variable name s dangling as an ERROR node.

Expected behavior

case final String s -> should parse the same shape as case String s ->, just with a final flag on the pattern/type_pattern node (mirroring how instanceof_expression represents it).

Suggested fix

Add optional('final') to type_pattern, e.g.:

type_pattern: $ => seq(optional('final'), $._unannotated_type, choice($.identifier, $._reserved_identifier)),

(possibly capturing it as a field, e.g. field('modifier', optional('final')), for consistency with how instanceof_expression exposes it — happy to defer to maintainer preference on the exact node shape.)

Environment

  • tree-sitter-java version 0.23.5 (via the tree-sitter-java crate on crates.io)
  • Confirmed against current master grammar.js as of this report

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions