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-java version 0.23.5 (via the tree-sitter-java crate on crates.io)
- Confirmed against current
master grammar.js as of this report
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
finalmodifier on a pattern variable, for bothinstanceofpatterns (JEP 394/405) andswitchpattern matching (JEP 441). For example:instanceof final Type xparses correctly, but the equivalentcase final Type x ->in a switch does not — it produces anERRORnode.Looking at
grammar.js,instanceof_expressionexplicitly includesoptional('final'), buttype_pattern(used byswitch_label) does not:It looks like
finalsupport was added toinstanceof_expressionin #124 ("Fix issues with records and support instanceof final") but never carried over totype_patternwhen switch pattern matching was completed in #158.Reproduction
Works — plain switch pattern, no modifier:
Works —
instanceofwithfinal:Fails — switch pattern with
final:Actual behavior
Parsing the failing example produces (relevant excerpt,
tree-sitter parse):Since
type_patternhas no rule for a leadingfinalkeyword, the parser treatsfinalitself as_unannotated_type(a bare identifier) andStringas the pattern's binding identifier, leaving the real variable namesdangling as anERRORnode.Expected behavior
case final String s ->should parse the same shape ascase String s ->, just with afinalflag on the pattern/type_pattern node (mirroring howinstanceof_expressionrepresents it).Suggested fix
Add
optional('final')totype_pattern, e.g.:(possibly capturing it as a field, e.g.
field('modifier', optional('final')), for consistency with howinstanceof_expressionexposes 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)mastergrammar.js as of this report