diff --git a/cfml.parsing/src/main/antlr4/cfml/CFSCRIPTParser.g4 b/cfml.parsing/src/main/antlr4/cfml/CFSCRIPTParser.g4 index cd5cbe6..6f6139a 100644 --- a/cfml.parsing/src/main/antlr4/cfml/CFSCRIPTParser.g4 +++ b/cfml.parsing/src/main/antlr4/cfml/CFSCRIPTParser.g4 @@ -595,7 +595,6 @@ functionCall ; simpleFunctionCall :(identifier | specialWord) LEFTPAREN argumentList RIGHTPAREN - body=compoundStatement? ; qualifiedFunctionCall :(identifier | reservedWord) LEFTPAREN argumentList RIGHTPAREN @@ -783,8 +782,12 @@ implicitStructKeyExpression | reservedWord ; +// `new component { ... }` defines and instantiates in one expression. The body is an +// ordinary componentDeclaration, reused rather than restated so attributes, directives +// and members all behave the same as in a named component. newComponentExpression : NEW componentPath LEFTPAREN argumentList RIGHTPAREN + | NEW componentDeclaration ; // Lucee types the path being instantiated: new java:java.io.File(p), new cfml:foo.Bar(). diff --git a/cfml.parsing/src/main/java/cfml/parsing/cfscript/CFAnonymousComponentExpression.java b/cfml.parsing/src/main/java/cfml/parsing/cfscript/CFAnonymousComponentExpression.java new file mode 100644 index 0000000..e446cc1 --- /dev/null +++ b/cfml.parsing/src/main/java/cfml/parsing/cfscript/CFAnonymousComponentExpression.java @@ -0,0 +1,56 @@ +package cfml.parsing.cfscript; + +import java.util.List; + +import org.antlr.v4.runtime.Token; + +import cfml.parsing.cfscript.script.CFCompDeclStatement; +import cfml.parsing.cfscript.script.CFScriptStatement; +import cfml.parsing.util.ArrayBuilder; + +/** + * An inline component: new component javaSettings='...' { ... }, which declares and + * instantiates in one expression. + * + * Modelled the way {@link CFAnonymousFunctionExpression} models function(){} -- a + * declaration statement hanging off an expression -- so a walk that already descends into an + * anonymous function reaches an inline component's members the same way. + */ +public class CFAnonymousComponentExpression extends CFExpression { + + private static final long serialVersionUID = 1L; + + private CFCompDeclStatement componentDeclaration; + + public CFAnonymousComponentExpression(Token _t, CFCompDeclStatement _componentDeclaration) { + super(_t); + componentDeclaration = _componentDeclaration; + if (componentDeclaration != null) { + componentDeclaration.setParent(this); + } + } + + @Override + public byte getType() { + return CFExpression.NESTED; + } + + public CFCompDeclStatement getComponentDeclaration() { + return componentDeclaration; + } + + @Override + public String Decompile(int indent) { + return "new " + componentDeclaration.Decompile(0); + } + + @Override + public List decomposeExpression() { + return ArrayBuilder.createCFExpression(); + } + + @Override + public List decomposeScript() { + return ArrayBuilder.createCFScriptStatement(componentDeclaration); + } +} 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 657cf7c..835d820 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 @@ -17,6 +17,7 @@ import cfml.CFSCRIPTParser.CfmlFunctionContext; import cfml.CFSCRIPTParser.CompareExpressionContext; import cfml.CFSCRIPTParser.ComponentAttributeContext; +import cfml.CFSCRIPTParser.ComponentDeclarationContext; import cfml.CFSCRIPTParser.ComponentGutsContext; import cfml.CFSCRIPTParser.ComponentPathContext; import cfml.CFSCRIPTParser.ConditionContext; @@ -57,6 +58,7 @@ import cfml.CFSCRIPTParser.UnaryExpressionContext; import cfml.CFSCRIPTParserBaseVisitor; import cfml.parsing.cfscript.ArgumentsVector; +import cfml.parsing.cfscript.CFAnonymousComponentExpression; import cfml.parsing.cfscript.CFAnonymousFunctionExpression; import cfml.parsing.cfscript.CFArrayExpression; import cfml.parsing.cfscript.CFAssignmentExpression; @@ -78,6 +80,7 @@ import cfml.parsing.cfscript.CFTernaryExpression; import cfml.parsing.cfscript.CFUnaryExpression; import cfml.parsing.cfscript.CFVarDeclExpression; +import cfml.parsing.cfscript.script.CFCompDeclStatement; import cfml.parsing.cfscript.script.CFFuncDeclStatement; import cfml.parsing.cfscript.script.CFReturnStatement; @@ -429,6 +432,13 @@ private CFIdentifier makeIdentifier(CFExpression visit) { @Override public CFExpression visitNewComponentExpression(NewComponentExpressionContext ctx) { + if (ctx.componentDeclaration() != null) { + // `new component { ... }` -- the declaration is built by the statement visitor, the way + // an anonymous function's is, and wrapped so it can sit in an expression. + CFCompDeclStatement declaration = (CFCompDeclStatement) getCFScriptStatementVisitor() + .visitComponentDeclaration(ctx.componentDeclaration()); + return new CFAnonymousComponentExpression(ctx.NEW().getSymbol(), declaration); + } ArgumentsVector args = new ArgumentsVector(); if (ctx.getChildCount() > 4) { for (ArgumentContext argCtx : ctx.argumentList().argument()) { diff --git a/cfml.parsing/src/test/resources/cfml/tests/components/inline_component_49.cfc b/cfml.parsing/src/test/resources/cfml/tests/components/inline_component_49.cfc new file mode 100644 index 0000000..559dfa6 --- /dev/null +++ b/cfml.parsing/src/test/resources/cfml/tests/components/inline_component_49.cfc @@ -0,0 +1,16 @@ +component { + function connect() { + // Declares and instantiates in one expression -- the shape Lucee uses to pull a + // Maven dependency in via javaSettings. + var msal = new component javaSettings='{"maven":["com.microsoft.azure:msal4j:1.23.1"]}' { + function buildApp() { + return 1; + } + }; + // Without attributes, and the ordinary path forms alongside it. + var bare = new component { function f() { return 2; } }; + var named = new models.User(); + var javaFile = new java:java.io.File(path); + return msal; + } +} diff --git a/cfml.parsing/src/test/resources/cfml/tests/components/inline_component_49.expected.txt b/cfml.parsing/src/test/resources/cfml/tests/components/inline_component_49.expected.txt new file mode 100644 index 0000000..be6f621 --- /dev/null +++ b/cfml.parsing/src/test/resources/cfml/tests/components/inline_component_49.expected.txt @@ -0,0 +1,345 @@ +/*===TOKENS===*/ +COMPONENT +'{' <{> +Hidden:NEWLINE <> +FUNCTION +IDENTIFIER +'(' <(> +')' <)> +'{' <{> +Hidden:NEWLINE <> +Hidden:LINE_COMMENT +Hidden:LINE_COMMENT +VAR +IDENTIFIER +'=' <=> +NEW +COMPONENT +IDENTIFIER +'=' <=> +OPEN_STRING <'> +STRING_LITERAL <{"maven":["com.microsoft.azure:msal4j:1.23.1"]}> +CLOSE_STRING <'> +'{' <{> +Hidden:NEWLINE <> +FUNCTION +IDENTIFIER +'(' <(> +')' <)> +'{' <{> +Hidden:NEWLINE <> +RETURN +INTEGER_LITERAL <1> +';' <;> +Hidden:NEWLINE <> +'}' <}> +Hidden:NEWLINE <> +'}' <}> +';' <;> +Hidden:NEWLINE <> +Hidden:LINE_COMMENT +VAR +IDENTIFIER +'=' <=> +NEW +COMPONENT +'{' <{> +FUNCTION +IDENTIFIER +'(' <(> +')' <)> +'{' <{> +RETURN +INTEGER_LITERAL <2> +';' <;> +'}' <}> +'}' <}> +';' <;> +Hidden:NEWLINE <> +VAR +IDENTIFIER +'=' <=> +NEW +IDENTIFIER +'.' <.> +IDENTIFIER +'(' <(> +')' <)> +';' <;> +Hidden:NEWLINE <> +VAR +IDENTIFIER +'=' <=> +NEW +IDENTIFIER +':' <:> +IDENTIFIER +'.' <.> +IDENTIFIER +'.' <.> +FILE +'(' <(> +IDENTIFIER +')' <)> +';' <;> +Hidden:NEWLINE <> +RETURN +IDENTIFIER +';' <;> +Hidden:NEWLINE <> +'}' <}> +Hidden:NEWLINE <> +'}' <}> +Hidden:NEWLINE <> +/*===TREE===*/ +(scriptBlock + (componentDeclaration + component + (componentGuts + { + (element + (functionDeclaration + function + (identifier connect) + ( + parameterList + ) + (compoundStatement + { + (statement + (localAssignmentExpression + var + (startExpression + (baseExpression (unaryExpression (memberExpression (identifier msal)))) + ) + = + (startExpression + (baseExpression + (unaryExpression + (memberExpression + (newComponentExpression + new + (componentDeclaration + component + (componentAttribute + (identifier javaSettings) + = + (startExpression + (baseExpression + (unaryExpression + (primaryExpression + (literalExpression + (stringLiteral + ' + (stringLiteralPart {"maven":["com.microsoft.azure:msal4j:1.23.1"]}) + ' + ) + ) + ) + ) + ) + ) + ) + (componentGuts + { + (element + (functionDeclaration + function + (identifier buildApp) + ( + parameterList + ) + (compoundStatement + { + (statement + (returnStatement + return + (anExpression + (startExpression + (baseExpression (unaryExpression (primaryExpression (literalExpression 1)))) + ) + ) + ) + (endOfStatement ;) + ) + } + ) + ) + ) + } + ) + ) + ) + ) + ) + ) + ) + ) + (endOfStatement ;) + ) + (statement + (localAssignmentExpression + var + (startExpression + (baseExpression (unaryExpression (memberExpression (identifier bare)))) + ) + = + (startExpression + (baseExpression + (unaryExpression + (memberExpression + (newComponentExpression + new + (componentDeclaration + component + (componentGuts + { + (element + (functionDeclaration + function + (identifier f) + ( + parameterList + ) + (compoundStatement + { + (statement + (returnStatement + return + (anExpression + (startExpression + (baseExpression (unaryExpression (primaryExpression (literalExpression 2)))) + ) + ) + ) + (endOfStatement ;) + ) + } + ) + ) + ) + } + ) + ) + ) + ) + ) + ) + ) + ) + (endOfStatement ;) + ) + (statement + (localAssignmentExpression + var + (startExpression + (baseExpression (unaryExpression (memberExpression (identifier named)))) + ) + = + (startExpression + (baseExpression + (unaryExpression + (memberExpression + (newComponentExpression + new + (componentPath + (multipartIdentifier + (identifier models) + . + (identifierOrReservedWord (identifier User)) + ) + ) + ( + argumentList + ) + ) + ) + ) + ) + ) + ) + (endOfStatement ;) + ) + (statement + (localAssignmentExpression + var + (startExpression + (baseExpression (unaryExpression (memberExpression (identifier javaFile)))) + ) + = + (startExpression + (baseExpression + (unaryExpression + (memberExpression + (newComponentExpression + new + (componentPath + (identifier java) + : + (multipartIdentifier + (identifier java) + . + (identifierOrReservedWord (identifier io)) + . + (identifierOrReservedWord (identifier (cfmlFunction File))) + ) + ) + ( + (argumentList + (argument + (startExpression + (baseExpression (unaryExpression (memberExpression (identifier path)))) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + (endOfStatement ;) + ) + (statement + (returnStatement + return + (anExpression + (startExpression + (baseExpression (unaryExpression (memberExpression (identifier msal)))) + ) + ) + ) + (endOfStatement ;) + ) + } + ) + ) + ) + } + ) + ) +) +/*======*/ +/*===DECOMPILE===*/ +component { + public function connect() { + var msal = new component javaSettings='{"maven":["com.microsoft.azure:msal4j:1.23.1"]}' { + public function buildApp() { + return 1; + + } +}; + var bare = new component { + public function f() { + return 2; + + } +}; + var named = new models.User(); + var javaFile = new java:java.io.File(path); + return msal; + + } +} +/*======*/ \ No newline at end of file