From 5a36880a61770d8e7629e7fd50abfc56fa1f9c0f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 21:15:17 +0000 Subject: [PATCH] Accept static as a declaration modifier, not only a function modifier component { static x = 1; } threw NullPointerException. STATIC was reachable only as a functionModifier, so prediction committed to functionDeclaration, found no FUNCTION token, and the visitor dereferenced ctx.FUNCTION() null. It threw rather than reporting, so one such line took the whole file with it -- through CFLint that is PARSE_ERROR and no lint results for the file at all. localAssignmentExpression already carried FINAL for the same reason, which is why final x = 1 worked and static x = 1 did not. STATIC now sits beside it. The visitor is taught the shape rather than left to visitChildren: CFVarDeclExpression gains staticDecl alongside finalDecl, and Decompile emits static rather than falling through to var. Verified by reading the output, not by a green parse -- component { static x = "v"; final y = "c"; } decompiles with both markers intact. Deliberately not fixed here, all pre-existing and confirmed against master before this change so none of it is a regression: var final x = 1; -> { var final; x = 1; } var static x = 1; -> { var static; x = 1; } static var x = 1; -> { static public var function x() ;; 1; } FINAL and STATIC are both valid identifiers since #46, so "var final" parses as VAR plus an identifier named final. That is a separate fault and the fixture stays off it. 323 tests, ./gradlew build, differential harness unchanged at 1 with nothing newly broken, CFLint's 675 against a clean build. The new fixture fails with cfml.parsing/src/main stashed. Closes #63 --- .../src/main/antlr4/cfml/CFSCRIPTParser.g4 | 7 +- .../parsing/cfscript/CFVarDeclExpression.java | 12 +- .../cfscript/walker/CFExpressionVisitor.java | 1 + .../tests/components/static_assignment_63.cfc | 9 ++ .../static_assignment_63.expected.txt | 144 ++++++++++++++++++ 5 files changed, 170 insertions(+), 3 deletions(-) create mode 100644 cfml.parsing/src/test/resources/cfml/tests/components/static_assignment_63.cfc create mode 100644 cfml.parsing/src/test/resources/cfml/tests/components/static_assignment_63.expected.txt diff --git a/cfml.parsing/src/main/antlr4/cfml/CFSCRIPTParser.g4 b/cfml.parsing/src/main/antlr4/cfml/CFSCRIPTParser.g4 index 0e4c672..3b72c01 100644 --- a/cfml.parsing/src/main/antlr4/cfml/CFSCRIPTParser.g4 +++ b/cfml.parsing/src/main/antlr4/cfml/CFSCRIPTParser.g4 @@ -505,8 +505,11 @@ cfmlExpression // FINAL is Lucee's immutable local; it takes the same shape as VAR and may combine // with it, as in `final var x = 1;`. -localAssignmentExpression - : (VAR | FINAL VAR? | VAR FINAL) left=startExpression ( (EQUALSOP otherIdentifiers)* EQUALSOP right=startExpression )? //-> ^( VARLOCAL identifier ( EQUALSOP baseExpression )? ) +// 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. +localAssignmentExpression + : (VAR | (FINAL | STATIC) VAR? | VAR (FINAL | STATIC)) left=startExpression ( (EQUALSOP otherIdentifiers)* EQUALSOP right=startExpression )? //-> ^( VARLOCAL identifier ( EQUALSOP baseExpression )? ) ; otherIdentifiers: diff --git a/cfml.parsing/src/main/java/cfml/parsing/cfscript/CFVarDeclExpression.java b/cfml.parsing/src/main/java/cfml/parsing/cfscript/CFVarDeclExpression.java index 658ec6b..254aee1 100644 --- a/cfml.parsing/src/main/java/cfml/parsing/cfscript/CFVarDeclExpression.java +++ b/cfml.parsing/src/main/java/cfml/parsing/cfscript/CFVarDeclExpression.java @@ -16,6 +16,8 @@ public class CFVarDeclExpression extends CFExpression { private CFExpression init; // null if none /** True for Lucee's `final x = 1;`, which is a different declaration from `var x = 1;`. */ private boolean finalDecl = false; + /** True for `static x = 1;`, a component-level static member rather than a local. */ + private boolean staticDecl = false; List otherVars = new ArrayList(); List otherIds = new ArrayList(); @@ -42,7 +44,7 @@ public CFVarDeclExpression(Token _t, CFExpression _var, CFExpression _init) { @Override public String Decompile(int indent) { StringBuilder s = new StringBuilder(Indent(indent)); - s.append(finalDecl ? "final " : "var "); + s.append(staticDecl ? "static " : finalDecl ? "final " : "var "); s.append(var.Decompile(0)); for (CFIdentifier id : otherVars) { s.append(" = var "); @@ -66,6 +68,14 @@ public boolean isFinal() { public void setFinal(boolean b) { finalDecl = b; } + + public boolean isStatic() { + return staticDecl; + } + + public void setStatic(boolean b) { + staticDecl = b; + } public CFExpression getInit() { return init; diff --git a/cfml.parsing/src/main/java/cfml/parsing/cfscript/walker/CFExpressionVisitor.java b/cfml.parsing/src/main/java/cfml/parsing/cfscript/walker/CFExpressionVisitor.java index 835d820..20d9520 100644 --- a/cfml.parsing/src/main/java/cfml/parsing/cfscript/walker/CFExpressionVisitor.java +++ b/cfml.parsing/src/main/java/cfml/parsing/cfscript/walker/CFExpressionVisitor.java @@ -185,6 +185,7 @@ public CFExpression visitLocalAssignmentExpression(LocalAssignmentExpressionCont final CFExpression initExpression = ctx.right == null ? null : visit(ctx.right); CFVarDeclExpression retval = new CFVarDeclExpression(ctx.start, visit(ctx.left), initExpression); retval.setFinal(ctx.FINAL() != null); + retval.setStatic(ctx.STATIC() != null); if (ctx.otherIdentifiers().size() > 0) { for (OtherIdentifiersContext oi : ctx.otherIdentifiers()) { CFIdentifier otherid = (CFIdentifier) visit(oi.identifier()); diff --git a/cfml.parsing/src/test/resources/cfml/tests/components/static_assignment_63.cfc b/cfml.parsing/src/test/resources/cfml/tests/components/static_assignment_63.cfc new file mode 100644 index 0000000..056a4d4 --- /dev/null +++ b/cfml.parsing/src/test/resources/cfml/tests/components/static_assignment_63.cfc @@ -0,0 +1,9 @@ +component { + static myStatic = "v"; + final myConst = "c"; + + function f() { + var local1 = myStatic; + return local1; + } +} diff --git a/cfml.parsing/src/test/resources/cfml/tests/components/static_assignment_63.expected.txt b/cfml.parsing/src/test/resources/cfml/tests/components/static_assignment_63.expected.txt new file mode 100644 index 0000000..d426b7e --- /dev/null +++ b/cfml.parsing/src/test/resources/cfml/tests/components/static_assignment_63.expected.txt @@ -0,0 +1,144 @@ +/*===TOKENS===*/ +COMPONENT +'{' <{> +Hidden:NEWLINE <> +STATIC +IDENTIFIER +'=' <=> +OPEN_STRING <"> +STRING_LITERAL +CLOSE_STRING <"> +';' <;> +Hidden:NEWLINE <> +FINAL +IDENTIFIER +'=' <=> +OPEN_STRING <"> +STRING_LITERAL +CLOSE_STRING <"> +';' <;> +Hidden:NEWLINE <> +FUNCTION +IDENTIFIER +'(' <(> +')' <)> +'{' <{> +Hidden:NEWLINE <> +VAR +IDENTIFIER +'=' <=> +IDENTIFIER +';' <;> +Hidden:NEWLINE <> +RETURN +IDENTIFIER +';' <;> +Hidden:NEWLINE <> +'}' <}> +Hidden:NEWLINE <> +'}' <}> +Hidden:NEWLINE <> +/*===TREE===*/ +(scriptBlock + (componentDeclaration + component + (componentGuts + { + (element + (statement + (localAssignmentExpression + static + (startExpression + (baseExpression (unaryExpression (memberExpression (identifier myStatic)))) + ) + = + (startExpression + (baseExpression + (unaryExpression + (primaryExpression + (literalExpression (stringLiteral " (stringLiteralPart v) ")) + ) + ) + ) + ) + ) + (endOfStatement ;) + ) + ) + (element + (statement + (localAssignmentExpression + final + (startExpression + (baseExpression (unaryExpression (memberExpression (identifier myConst)))) + ) + = + (startExpression + (baseExpression + (unaryExpression + (primaryExpression + (literalExpression (stringLiteral " (stringLiteralPart c) ")) + ) + ) + ) + ) + ) + (endOfStatement ;) + ) + ) + (element + (functionDeclaration + function + (identifier f) + ( + parameterList + ) + (compoundStatement + { + (statement + (localAssignmentExpression + var + (startExpression + (baseExpression (unaryExpression (memberExpression (identifier local1)))) + ) + = + (startExpression + (baseExpression (unaryExpression (memberExpression (identifier myStatic)))) + ) + ) + (endOfStatement ;) + ) + (statement + (returnStatement + return + (anExpression + (startExpression + (baseExpression (unaryExpression (memberExpression (identifier local1)))) + ) + ) + ) + (endOfStatement ;) + ) + } + ) + ) + ) + } + ) + ) +) +/*======*/ +/*===DECOMPILE===*/ +component { + { + static myStatic = 'v'; + final myConst = 'c'; + public function f() { + var local1 = myStatic; + return local1; + + }; + + } +} +/*======*/ \ No newline at end of file