From 0cb3aa81d0d204f923a2ad1957fc2be90ae524c3 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 04:55:50 +0000 Subject: [PATCH] Accept inline component definitions `new component { ... }` declares and instantiates in one expression. newComponentExpression required a path and a parenthesised argument list, and the inline form has neither. The new alternative is `NEW componentDeclaration`, reusing the existing rule rather than restating its shape, so attributes, directives and members behave exactly as they do in a named component. CFAnonymousComponentExpression follows CFAnonymousFunctionExpression: a declaration statement hanging off an expression, which is the same problem this repo already solved once for function(){}. A walk reaches the inline component's members through decomposeScript -- CFAnonymousComponentExpression CFCompDeclStatement CFFuncDeclStatement name=buildApp which is the part that matters; a green parse would have proved nothing. Adding the alternative regressed parsing/cached_cf573.cfc, whose function carries `cachedwithin=createTimeSpan(1,0,0,0)` before its body. That is the latent bug in #45 one rule over: simpleFunctionCall also had an optional trailing block, so the call swallowed the function's body. The new alternative only changed which way prediction fell. Removing that body restores the fixture's recorded tree untouched, which is the evidence the removal is right rather than convenient -- as with functionCall, no visitor reads it; the ctx.body handling belongs to visitQualifiedFunctionCall, whose own rule keeps its own. Differential harness 8 disagreements to 7, nothing newly broken. Verified end to end -- 313 cfparser tests, ./gradlew build, CFLint's 675. Closes #49 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GzpZFd4rnE1Yi2sVHAji35 --- .../src/main/antlr4/cfml/CFSCRIPTParser.g4 | 5 +- .../CFAnonymousComponentExpression.java | 56 +++ .../cfscript/walker/CFExpressionVisitor.java | 10 + .../tests/components/inline_component_49.cfc | 16 + .../inline_component_49.expected.txt | 345 ++++++++++++++++++ 5 files changed, 431 insertions(+), 1 deletion(-) create mode 100644 cfml.parsing/src/main/java/cfml/parsing/cfscript/CFAnonymousComponentExpression.java create mode 100644 cfml.parsing/src/test/resources/cfml/tests/components/inline_component_49.cfc create mode 100644 cfml.parsing/src/test/resources/cfml/tests/components/inline_component_49.expected.txt 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