From 0591efad7ac4bb7f64efb69033f346eb9e0ff236 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 21:23:19 +0000 Subject: [PATCH] Accept an access modifier on a static block member, and keep the block component { static { public myVar = "v"; } } failed to parse: no viable alternative at input 'publicmyVar=' staticBlock took a bare statement, and an accessType is not part of one. The block itself was fine -- static { myVar = "v"; } parsed -- so only members carrying public, private, remote or package were rejected. All four are legal there in ACF. A staticMember rule now carries the optional accessType, which puts it on the member rather than the block, where CFML puts it. ## The block was being thrown away Fixed here because adding the modifier without it would have made things worse. staticBlock had no visitor at all, so ANTLR's default visitChildren flattened it: before: component { static { myVar = "v"; } } -> component { myVar = 'v' } after: component { static { myVar = "v"; } } -> component { static { myVar = 'v'; } } Adding accessType to a rule the visitor ignores would have folded the modifier into whatever was nearby -- the exact failure CLAUDE.md describes for arrow functions in #16 and array slicing in #18. So CFStaticBlockStatement models the block, holds its members in source order for decomposeScript, and records each member's access type positionally. No fixture covered a CFML static block before this, which is how a construct that parsed and then vanished went unnoticed. 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 #64 --- .../src/main/antlr4/cfml/CFSCRIPTParser.g4 | 8 +- .../script/CFStaticBlockStatement.java | 80 +++++++++ .../walker/CFScriptStatementVisitor.java | 19 ++ .../components/static_block_modifier_64.cfc | 11 ++ .../static_block_modifier_64.expected.txt | 166 ++++++++++++++++++ 5 files changed, 283 insertions(+), 1 deletion(-) create mode 100644 cfml.parsing/src/main/java/cfml/parsing/cfscript/script/CFStaticBlockStatement.java create mode 100644 cfml.parsing/src/test/resources/cfml/tests/components/static_block_modifier_64.cfc create mode 100644 cfml.parsing/src/test/resources/cfml/tests/components/static_block_modifier_64.expected.txt diff --git a/cfml.parsing/src/main/antlr4/cfml/CFSCRIPTParser.g4 b/cfml.parsing/src/main/antlr4/cfml/CFSCRIPTParser.g4 index 0e4c672..c3f2c5d 100644 --- a/cfml.parsing/src/main/antlr4/cfml/CFSCRIPTParser.g4 +++ b/cfml.parsing/src/main/antlr4/cfml/CFSCRIPTParser.g4 @@ -35,8 +35,14 @@ interfaceDeclaration : INTERFACE componentAttribute* componentGuts //-> ( COMPDECL componentAttribute* componentGuts) ; +// A member inside a static block may carry an access type: static { public myVar = "v"; }. +// accessType sits on the member rather than the block, so it is optional per statement. staticBlock - : STATIC LEFTCURLYBRACKET ( statement )* RIGHTCURLYBRACKET + : STATIC LEFTCURLYBRACKET ( staticMember )* RIGHTCURLYBRACKET + ; + +staticMember + : accessType? statement ; element diff --git a/cfml.parsing/src/main/java/cfml/parsing/cfscript/script/CFStaticBlockStatement.java b/cfml.parsing/src/main/java/cfml/parsing/cfscript/script/CFStaticBlockStatement.java new file mode 100644 index 0000000..daf9b95 --- /dev/null +++ b/cfml.parsing/src/main/java/cfml/parsing/cfscript/script/CFStaticBlockStatement.java @@ -0,0 +1,80 @@ +package cfml.parsing.cfscript.script; + +import java.util.ArrayList; +import java.util.List; + +import org.antlr.v4.runtime.Token; + +import cfml.parsing.cfscript.CFExpression; +import cfml.parsing.util.ArrayBuilder; + +/** + * A static { ... } block inside a component. + * + * Without this the block had no visitor at all, so ANTLR's default visitChildren + * flattened it: component { static { myVar = "v"; } } decompiled to + * component { myVar = 'v' }, losing the fact that the member was static. Members + * are held in source order and reachable through {@link #decomposeScript()}. + * + * A member may carry its own access type -- static { public myVar = "v"; } -- which + * is recorded per member rather than on the block, since that is where CFML puts it. + */ +public class CFStaticBlockStatement extends CFParsedStatement { + + private static final long serialVersionUID = 1L; + + private final List members; + private final List accessTypes; + + public CFStaticBlockStatement(Token _t, List _members, List _accessTypes) { + super(_t); + members = _members == null ? new ArrayList() : _members; + accessTypes = _accessTypes == null ? new ArrayList() : _accessTypes; + for (CFScriptStatement member : members) { + if (member != null) { + member.setParent(this); + } + } + } + + /** The block's members, in source order. */ + public List getMembers() { + return members; + } + + /** + * The access type written on each member, positionally aligned with {@link #getMembers()}. + * Null where a member carried none, which is the common case. + */ + public List getAccessTypes() { + return accessTypes; + } + + @Override + public String Decompile(int indent) { + StringBuilder sb = new StringBuilder(); + sb.append(Indent(indent)); + sb.append("static {\n"); + for (int i = 0; i < members.size(); i++) { + sb.append(Indent(indent + 2)); + String accessType = i < accessTypes.size() ? accessTypes.get(i) : null; + if (accessType != null) { + sb.append(accessType).append(" "); + } + sb.append(members.get(i).Decompile(0)); + sb.append(";\n"); + } + sb.append(Indent(indent)).append("}"); + return sb.toString(); + } + + @Override + public List decomposeExpression() { + return ArrayBuilder.createCFExpression(); + } + + @Override + public List decomposeScript() { + return members; + } +} diff --git a/cfml.parsing/src/main/java/cfml/parsing/cfscript/walker/CFScriptStatementVisitor.java b/cfml.parsing/src/main/java/cfml/parsing/cfscript/walker/CFScriptStatementVisitor.java index 653b841..9dce43c 100644 --- a/cfml.parsing/src/main/java/cfml/parsing/cfscript/walker/CFScriptStatementVisitor.java +++ b/cfml.parsing/src/main/java/cfml/parsing/cfscript/walker/CFScriptStatementVisitor.java @@ -55,6 +55,8 @@ import cfml.CFSCRIPTParser.StartExpressionContext; import cfml.CFSCRIPTParser.StatementContext; import cfml.CFSCRIPTParser.SwitchStatementContext; +import cfml.CFSCRIPTParser.StaticBlockContext; +import cfml.CFSCRIPTParser.StaticMemberContext; import cfml.CFSCRIPTParser.TemplateBlockContext; import cfml.CFSCRIPTParser.TagFunctionStatementContext; import cfml.CFSCRIPTParser.TagStatementContext; @@ -99,6 +101,7 @@ import cfml.parsing.cfscript.script.CFScriptStatement; import cfml.parsing.cfscript.script.CFSwitchStatement; import cfml.parsing.cfscript.script.CFTagThrowStatement; +import cfml.parsing.cfscript.script.CFStaticBlockStatement; import cfml.parsing.cfscript.script.CFTemplateBlockStatement; import cfml.parsing.cfscript.script.CFThreadStatement; import cfml.parsing.cfscript.script.CFThrowStatement; @@ -590,6 +593,22 @@ public CFScriptStatement visitExitStatement(ExitStatementContext ctx) { return exitStatement; } + @Override + public CFScriptStatement visitStaticBlock(StaticBlockContext ctx) { + // Without a visitor here the block was flattened by visitChildren: static { myVar = "v"; } + // decompiled to plain myVar = 'v', losing the fact that the member was static at all. + List members = new ArrayList(); + List accessTypes = new ArrayList(); + for (StaticMemberContext member : ctx.staticMember()) { + CFScriptStatement statement = visit(member.statement()); + if (statement != null) { + members.add(statement); + accessTypes.add(member.accessType() == null ? null : member.accessType().getText()); + } + } + return new CFStaticBlockStatement(ctx.STATIC().getSymbol(), members, accessTypes); + } + @Override public CFScriptStatement visitTemplateBlock(TemplateBlockContext ctx) { // The body is markup, not cfscript, so it is kept verbatim rather than modelled. Only the diff --git a/cfml.parsing/src/test/resources/cfml/tests/components/static_block_modifier_64.cfc b/cfml.parsing/src/test/resources/cfml/tests/components/static_block_modifier_64.cfc new file mode 100644 index 0000000..95db215 --- /dev/null +++ b/cfml.parsing/src/test/resources/cfml/tests/components/static_block_modifier_64.cfc @@ -0,0 +1,11 @@ +component { + static { + public myPublic = "p"; + private myPrivate = "q"; + myPlain = "r"; + } + + function f() { + return myPlain; + } +} diff --git a/cfml.parsing/src/test/resources/cfml/tests/components/static_block_modifier_64.expected.txt b/cfml.parsing/src/test/resources/cfml/tests/components/static_block_modifier_64.expected.txt new file mode 100644 index 0000000..2c36fd5 --- /dev/null +++ b/cfml.parsing/src/test/resources/cfml/tests/components/static_block_modifier_64.expected.txt @@ -0,0 +1,166 @@ +/*===TOKENS===*/ +COMPONENT +'{' <{> +Hidden:NEWLINE <> +STATIC +'{' <{> +Hidden:NEWLINE <> +PUBLIC +IDENTIFIER +'=' <=> +OPEN_STRING <"> +STRING_LITERAL

+CLOSE_STRING <"> +';' <;> +Hidden:NEWLINE <> +PRIVATE +IDENTIFIER +'=' <=> +OPEN_STRING <"> +STRING_LITERAL +CLOSE_STRING <"> +';' <;> +Hidden:NEWLINE <> +IDENTIFIER +'=' <=> +OPEN_STRING <"> +STRING_LITERAL +CLOSE_STRING <"> +';' <;> +Hidden:NEWLINE <> +'}' <}> +Hidden:NEWLINE <> +FUNCTION +IDENTIFIER +'(' <(> +')' <)> +'{' <{> +Hidden:NEWLINE <> +RETURN +IDENTIFIER +';' <;> +Hidden:NEWLINE <> +'}' <}> +Hidden:NEWLINE <> +'}' <}> +Hidden:NEWLINE <> +/*===TREE===*/ +(scriptBlock + (componentDeclaration + component + (componentGuts + { + (element + (staticBlock + static + { + (staticMember + (accessType public) + (statement + (assignmentExpression + (startExpression + (baseExpression (unaryExpression (memberExpression (identifier myPublic)))) + ) + = + (startExpression + (baseExpression + (unaryExpression + (primaryExpression + (literalExpression (stringLiteral " (stringLiteralPart p) ")) + ) + ) + ) + ) + ) + (endOfStatement ;) + ) + ) + (staticMember + (accessType private) + (statement + (assignmentExpression + (startExpression + (baseExpression (unaryExpression (memberExpression (identifier myPrivate)))) + ) + = + (startExpression + (baseExpression + (unaryExpression + (primaryExpression + (literalExpression (stringLiteral " (stringLiteralPart q) ")) + ) + ) + ) + ) + ) + (endOfStatement ;) + ) + ) + (staticMember + (statement + (assignmentExpression + (startExpression + (baseExpression (unaryExpression (memberExpression (identifier myPlain)))) + ) + = + (startExpression + (baseExpression + (unaryExpression + (primaryExpression + (literalExpression (stringLiteral " (stringLiteralPart r) ")) + ) + ) + ) + ) + ) + (endOfStatement ;) + ) + ) + } + ) + ) + (element + (functionDeclaration + function + (identifier f) + ( + parameterList + ) + (compoundStatement + { + (statement + (returnStatement + return + (anExpression + (startExpression + (baseExpression (unaryExpression (memberExpression (identifier myPlain)))) + ) + ) + ) + (endOfStatement ;) + ) + } + ) + ) + ) + } + ) + ) +) +/*======*/ +/*===DECOMPILE===*/ +component { + { + static { + public myPublic = 'p'; + private myPrivate = 'q'; + myPlain = 'r'; + }; + public function f() { + return myPlain; + + }; + + } +} +/*======*/ \ No newline at end of file