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