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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions