Repository navigation
type_pattern doesn't accept a final modifier in switch case labels, even though instanceof_expression does #229
Description
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");
}
}
}Works — instanceof 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-javaversion 0.23.5 (via thetree-sitter-javacrate on crates.io)- Confirmed against current
mastergrammar.js as of this report
Activity