Skip to content

Accept static as a declaration modifier, not only a function modifier - #65

Merged
ghedwards merged 1 commit into
masterfrom
claude/fix-static-assignment-63
Aug 13, 2026
Merged

Accept static as a declaration modifier, not only a function modifier#65
ghedwards merged 1 commit into
masterfrom
claude/fix-static-assignment-63

Conversation

@ghedwards

Copy link
Copy Markdown

Closes #63.

component { static myStatic = "v"; }

threw:

java.lang.NullPointerException: Cannot invoke "org.antlr.v4.runtime.tree.TerminalNode.getSymbol()"
because the return value of "cfml.CFSCRIPTParser$FunctionDeclarationContext.FUNCTION()" is null

It threw rather than reporting an error, so one such line took the whole file with it. Through CFLint that surfaces as PARSE_ERROR: Unable to parse and no lint results at all for the file.

Cause

STATIC was reachable only as a functionModifier. functionDeclaration starts functionModifier* accessType? functionModifier* typeSpec? FUNCTION identifier, so prediction committed on seeing static, no FUNCTION token arrived, and the visitor dereferenced ctx.FUNCTION() unconditionally.

localAssignmentExpression already carried FINAL for exactly this reason — which is why final x = 1 worked and static x = 1 did not, despite the two sitting side by side in both componentModifier and functionModifier. STATIC now sits beside it.

The visitor is taught the shape

Per CLAUDE.md, a green parse proves nothing here — this is the rule where visitChildren silently folds an unhandled shape into whatever is nearby. So CFVarDeclExpression gains staticDecl alongside finalDecl, and Decompile emits static rather than falling through to var .

Checked by reading the output rather than trusting the parse:

component { static myStatic = "v"; final myConst = "c"; }
  ->  static myStatic = 'v';
      final myConst = 'c';

Both markers survive the round trip.

Pre-existing faults left alone

Confirmed against master before this change, so none is a regression introduced here:

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 and splits into two statements. static var produces outright garbage. Separate fault in the same rule; the fixture deliberately stays off these so it tests #63 and nothing else. Worth its own issue — say the word and I'll file it.

Also unrelated and unchanged: any multi-element component decompiles with an extra { } around the body, including component { x = 1; y = 2; } with no static in sight.

Verification

  • 323 tests, ./gradlew build
  • Differential harness unchanged at 1, nothing newly broken case-by-case
  • CFLint 675 against a clean build
  • components/static_assignment_63.cfc fails with cfml.parsing/src/main stashed — the expected tree does not match

Not covered by this PR

#64, the access modifier inside a static block, is the other half of cfmleditor/CFLint#50 and is a separate fault in staticBlock. Coming next.


Generated by Claude Code

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Component-level static x = 1; throws NullPointerException

2 participants