From b0fdbcb747a0adc089351f39b2a9ed85e360e825 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 22:12:51 +0000 Subject: [PATCH] Remove the unreachable VAR (FINAL | STATIC) alternative localAssignmentExpression declared a trailing modifier form that no input could reach. FINAL and STATIC are both in the identifier rule, put there by #46 so code naming a variable final keeps working, so "var final" matches the plain VAR alternative with final as the identifier before the longer one is considered. Prediction commits there and never reaches the third alternative. Removed rather than made reachable, because "var final" already means something and cfparser gets it right: var final = getValue(); -> var final = getValue() tree-sitter-cfml's corpus tests exactly that under a case named "keywords as variable names", with the expected tree showing an identifier. Making "var final x = 1" parse would resolve that ambiguity against a documented and tested reading in favour of one with no evidence behind it. I could find no support for "var final x = 1", "var static x = 1" or "static final x = 1" in the tree-sitter corpus or grammar, the shipped dictionaries, CFLint's real-world test corpus, or cfdocs -- and cfdocs cannot answer it either way, since it documents 806 functions and 170 tags but no language keywords at all, not even var. Adobe and Lucee documentation was unreachable from here. "final x = 1" is not affected and stays. It is Lucee's immutable local and the reason FINAL is in this rule. The change is behaviour-neutral by construction, and that is verified rather than asserted: parse and decompile output across thirteen forms is byte-identical before and after, including the three that fail. The new fixture therefore passes with cfml.parsing/src/main stashed, by design -- it exists to pin the keyword-as-name readings against a future change to this rule, not to demonstrate a fix. VAR STATIC was added in #65, mirroring the existing VAR FINAL, so half of what this removes is mine. 326 tests, ./gradlew build, differential harness unchanged at 1 with nothing newly broken, CFLint's 675 against a clean build. Closes #68 --- .../src/main/antlr4/cfml/CFSCRIPTParser.g4 | 13 +- .../tests/expressions/var_modifiers_68.cfc | 9 + .../expressions/var_modifiers_68.expected.txt | 232 ++++++++++++++++++ 3 files changed, 251 insertions(+), 3 deletions(-) create mode 100644 cfml.parsing/src/test/resources/cfml/tests/expressions/var_modifiers_68.cfc create mode 100644 cfml.parsing/src/test/resources/cfml/tests/expressions/var_modifiers_68.expected.txt diff --git a/cfml.parsing/src/main/antlr4/cfml/CFSCRIPTParser.g4 b/cfml.parsing/src/main/antlr4/cfml/CFSCRIPTParser.g4 index fc6e813..065f17a 100644 --- a/cfml.parsing/src/main/antlr4/cfml/CFSCRIPTParser.g4 +++ b/cfml.parsing/src/main/antlr4/cfml/CFSCRIPTParser.g4 @@ -509,13 +509,20 @@ cfmlExpression | importStatement EOF ; -// FINAL is Lucee's immutable local; it takes the same shape as VAR and may combine -// with it, as in `final var x = 1;`. +// FINAL is Lucee's immutable local; it takes the same shape as VAR and may lead it, as in +// `final var x = 1;`. // STATIC sits here alongside FINAL because `static x = 1;` is a declaration, not a function. // Without it, STATIC is only reachable as a functionModifier, so functionDeclaration wins // prediction and then finds no FUNCTION token -- see #63. +// +// The modifier only ever leads. A trailing `VAR (FINAL | STATIC)` alternative used to sit here +// and could not match anything: both words are in the identifier rule, put there by #46 so code +// naming a variable `final` keeps working, so `var final` matches the plain VAR alternative with +// `final` as the identifier before the longer one is considered. That reading is the correct one +// -- tree-sitter-cfml's corpus tests `var final = getValue();` as a local named final -- so the +// alternative was removed rather than made reachable. See #68. localAssignmentExpression - : (VAR | (FINAL | STATIC) VAR? | VAR (FINAL | STATIC)) left=startExpression ( (EQUALSOP otherIdentifiers)* EQUALSOP right=startExpression )? //-> ^( VARLOCAL identifier ( EQUALSOP baseExpression )? ) + : (VAR | (FINAL | STATIC) VAR?) left=startExpression ( (EQUALSOP otherIdentifiers)* EQUALSOP right=startExpression )? //-> ^( VARLOCAL identifier ( EQUALSOP baseExpression )? ) ; otherIdentifiers: diff --git a/cfml.parsing/src/test/resources/cfml/tests/expressions/var_modifiers_68.cfc b/cfml.parsing/src/test/resources/cfml/tests/expressions/var_modifiers_68.cfc new file mode 100644 index 0000000..efb34da --- /dev/null +++ b/cfml.parsing/src/test/resources/cfml/tests/expressions/var_modifiers_68.cfc @@ -0,0 +1,9 @@ +var plain = 1; +final immutable = 2; +static shared = 3; +final var finalLocal = 4; +static var staticLocal = 5; +final = getValue(); +var final = getValue(); +static = getValue(); +var static = getValue(); diff --git a/cfml.parsing/src/test/resources/cfml/tests/expressions/var_modifiers_68.expected.txt b/cfml.parsing/src/test/resources/cfml/tests/expressions/var_modifiers_68.expected.txt new file mode 100644 index 0000000..15ad922 --- /dev/null +++ b/cfml.parsing/src/test/resources/cfml/tests/expressions/var_modifiers_68.expected.txt @@ -0,0 +1,232 @@ +/*===TOKENS===*/ +VAR +IDENTIFIER +'=' <=> +INTEGER_LITERAL <1> +';' <;> +Hidden:NEWLINE <> +FINAL +IDENTIFIER +'=' <=> +INTEGER_LITERAL <2> +';' <;> +Hidden:NEWLINE <> +STATIC +IDENTIFIER +'=' <=> +INTEGER_LITERAL <3> +';' <;> +Hidden:NEWLINE <> +FINAL +VAR +IDENTIFIER +'=' <=> +INTEGER_LITERAL <4> +';' <;> +Hidden:NEWLINE <> +STATIC +VAR +IDENTIFIER +'=' <=> +INTEGER_LITERAL <5> +';' <;> +Hidden:NEWLINE <> +FINAL +'=' <=> +IDENTIFIER +'(' <(> +')' <)> +';' <;> +Hidden:NEWLINE <> +VAR +FINAL +'=' <=> +IDENTIFIER +'(' <(> +')' <)> +';' <;> +Hidden:NEWLINE <> +STATIC +'=' <=> +IDENTIFIER +'(' <(> +')' <)> +';' <;> +Hidden:NEWLINE <> +VAR +STATIC +'=' <=> +IDENTIFIER +'(' <(> +')' <)> +';' <;> +Hidden:NEWLINE <> +/*===TREE===*/ +(scriptBlock + (element + (statement + (localAssignmentExpression + var + (startExpression + (baseExpression (unaryExpression (memberExpression (identifier plain)))) + ) + = + (startExpression + (baseExpression (unaryExpression (primaryExpression (literalExpression 1)))) + ) + ) + (endOfStatement ;) + ) + ) + (element + (statement + (localAssignmentExpression + final + (startExpression + (baseExpression (unaryExpression (memberExpression (identifier immutable)))) + ) + = + (startExpression + (baseExpression (unaryExpression (primaryExpression (literalExpression 2)))) + ) + ) + (endOfStatement ;) + ) + ) + (element + (statement + (localAssignmentExpression + static + (startExpression + (baseExpression (unaryExpression (memberExpression (identifier shared)))) + ) + = + (startExpression + (baseExpression (unaryExpression (primaryExpression (literalExpression 3)))) + ) + ) + (endOfStatement ;) + ) + ) + (element + (statement + (localAssignmentExpression + final + var + (startExpression + (baseExpression (unaryExpression (memberExpression (identifier finalLocal)))) + ) + = + (startExpression + (baseExpression (unaryExpression (primaryExpression (literalExpression 4)))) + ) + ) + (endOfStatement ;) + ) + ) + (element + (statement + (localAssignmentExpression + static + var + (startExpression + (baseExpression (unaryExpression (memberExpression (identifier staticLocal)))) + ) + = + (startExpression + (baseExpression (unaryExpression (primaryExpression (literalExpression 5)))) + ) + ) + (endOfStatement ;) + ) + ) + (element + (statement + (assignmentExpression + (startExpression + (baseExpression (unaryExpression (memberExpression (identifier final)))) + ) + = + (startExpression + (baseExpression + (unaryExpression + (memberExpression (functionCall (identifier getValue) ( argumentList ))) + ) + ) + ) + ) + (endOfStatement ;) + ) + ) + (element + (statement + (localAssignmentExpression + var + (startExpression + (baseExpression (unaryExpression (memberExpression (identifier final)))) + ) + = + (startExpression + (baseExpression + (unaryExpression + (memberExpression (functionCall (identifier getValue) ( argumentList ))) + ) + ) + ) + ) + (endOfStatement ;) + ) + ) + (element + (statement + (assignmentExpression + (startExpression + (baseExpression (unaryExpression (memberExpression (identifier static)))) + ) + = + (startExpression + (baseExpression + (unaryExpression + (memberExpression (functionCall (identifier getValue) ( argumentList ))) + ) + ) + ) + ) + (endOfStatement ;) + ) + ) + (element + (statement + (localAssignmentExpression + var + (startExpression + (baseExpression (unaryExpression (memberExpression (identifier static)))) + ) + = + (startExpression + (baseExpression + (unaryExpression + (memberExpression (functionCall (identifier getValue) ( argumentList ))) + ) + ) + ) + ) + (endOfStatement ;) + ) + ) +) +/*======*/ +/*===DECOMPILE===*/ +{ + var plain = 1; + final immutable = 2; + static shared = 3; + final finalLocal = 4; + static staticLocal = 5; +final = getValue(); + var final = getValue(); +static = getValue(); + var static = getValue(); + +} +/*======*/ \ No newline at end of file