From 589bf05b9653897e34344400295979990756df6b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 04:32:51 +0000 Subject: [PATCH] Stop a call in an attribute value swallowing the tag's body query name="q" cachedWithin=createTimeSpan(0,0,0,5) { ... } failed while cachedWithin=t parsed. Not the attribute grammar: functionCall carried an optional trailing block, functionCall : (identifier | specialWord) LEFTPAREN argumentList RIGHTPAREN body=compoundStatement? ; so createTimeSpan(0,0,0,5) { echo("x"); } was read as a call with a body, taking the block the tag needed and leaving cfmlfunctionStatement at EOF. That label was never read. The ctx.body handling in the expression visitor belongs to visitQualifiedFunctionCall, whose own rule keeps its body, which is why removing this one compiles untouched and why no fixture records it. It only ever consumed input. The forms that do want a body still have one of their own: cfhttp(url="a" method="GET") { } through cfmlfunctionStatement, cfmail(to="a", from="b") { } through tagFunctionStatement. Differential harness 8 disagreements to 7 against master, nothing newly broken. Verified end to end -- 313 cfparser tests, ./gradlew build, CFLint's 675. Closes #45 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GzpZFd4rnE1Yi2sVHAji35 --- .../src/main/antlr4/cfml/CFSCRIPTParser.g4 | 1 - .../tests/tagscript/tag_attribute_call_45.cfc | 10 + .../tag_attribute_call_45.expected.txt | 307 ++++++++++++++++++ 3 files changed, 317 insertions(+), 1 deletion(-) create mode 100644 cfml.parsing/src/test/resources/cfml/tests/tagscript/tag_attribute_call_45.cfc create mode 100644 cfml.parsing/src/test/resources/cfml/tests/tagscript/tag_attribute_call_45.expected.txt diff --git a/cfml.parsing/src/main/antlr4/cfml/CFSCRIPTParser.g4 b/cfml.parsing/src/main/antlr4/cfml/CFSCRIPTParser.g4 index cd5cbe6..98a158d 100644 --- a/cfml.parsing/src/main/antlr4/cfml/CFSCRIPTParser.g4 +++ b/cfml.parsing/src/main/antlr4/cfml/CFSCRIPTParser.g4 @@ -591,7 +591,6 @@ arraySlice functionCall :(identifier | specialWord) LEFTPAREN argumentList RIGHTPAREN - body=compoundStatement? ; simpleFunctionCall :(identifier | specialWord) LEFTPAREN argumentList RIGHTPAREN diff --git a/cfml.parsing/src/test/resources/cfml/tests/tagscript/tag_attribute_call_45.cfc b/cfml.parsing/src/test/resources/cfml/tests/tagscript/tag_attribute_call_45.cfc new file mode 100644 index 0000000..4f4185c --- /dev/null +++ b/cfml.parsing/src/test/resources/cfml/tests/tagscript/tag_attribute_call_45.cfc @@ -0,0 +1,10 @@ +// A function call as an attribute value. The call's own optional block body used to +// swallow the tag's body, leaving the tag with nothing to match. +query name="results" cachedWithin=createTimeSpan(0,0,0,5) { + echo("select 1"); +} +// The same tag with a plain attribute value, and with no body at all. +query name="other" cachedWithin=timeout { + echo("select 2"); +} +cfdirectory directory=expandPath("/tmp") action="list"; diff --git a/cfml.parsing/src/test/resources/cfml/tests/tagscript/tag_attribute_call_45.expected.txt b/cfml.parsing/src/test/resources/cfml/tests/tagscript/tag_attribute_call_45.expected.txt new file mode 100644 index 0000000..5e53176 --- /dev/null +++ b/cfml.parsing/src/test/resources/cfml/tests/tagscript/tag_attribute_call_45.expected.txt @@ -0,0 +1,307 @@ +/*===TOKENS===*/ +Hidden:LINE_COMMENT +Hidden:LINE_COMMENT +QUERY +IDENTIFIER +'=' <=> +OPEN_STRING <"> +STRING_LITERAL +CLOSE_STRING <"> +IDENTIFIER +'=' <=> +IDENTIFIER +'(' <(> +INTEGER_LITERAL <0> +',' <,> +INTEGER_LITERAL <0> +',' <,> +INTEGER_LITERAL <0> +',' <,> +INTEGER_LITERAL <5> +')' <)> +'{' <{> +Hidden:NEWLINE <> +IDENTIFIER +'(' <(> +OPEN_STRING <"> +STRING_LITERAL +CLOSE_STRING <"> +')' <)> +';' <;> +Hidden:NEWLINE <> +'}' <}> +Hidden:NEWLINE <> +DIRECTORY +DIRECTORY +'=' <=> +IDENTIFIER +'(' <(> +OPEN_STRING <"> +STRING_LITERAL +CLOSE_STRING <"> +')' <)> +IDENTIFIER +'=' <=> +OPEN_STRING <"> +STRING_LITERAL +CLOSE_STRING <"> +';' <;> +Hidden:NEWLINE <> +/*===TREE===*/ +(scriptBlock + (element + (statement + (tagOperatorStatement + (cfmlfunctionStatement + (cfmlFunction query) + (paramStatementAttributes + (param + (multipartIdentifier (identifier name)) + = + (startExpression + (baseExpression + (unaryExpression + (primaryExpression + (literalExpression (stringLiteral " (stringLiteralPart results) ")) + ) + ) + ) + ) + ) + (param + (multipartIdentifier (identifier cachedWithin)) + = + (startExpression + (baseExpression + (unaryExpression + (memberExpression + (functionCall + (identifier createTimeSpan) + ( + (argumentList + (argument + (startExpression + (baseExpression (unaryExpression (primaryExpression (literalExpression 0)))) + ) + ) + , + (argument + (startExpression + (baseExpression (unaryExpression (primaryExpression (literalExpression 0)))) + ) + ) + , + (argument + (startExpression + (baseExpression (unaryExpression (primaryExpression (literalExpression 0)))) + ) + ) + , + (argument + (startExpression + (baseExpression (unaryExpression (primaryExpression (literalExpression 5)))) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + (compoundStatement + { + (statement + (startExpression + (baseExpression + (unaryExpression + (memberExpression + (functionCall + (identifier echo) + ( + (argumentList + (argument + (startExpression + (baseExpression + (unaryExpression + (primaryExpression + (literalExpression (stringLiteral " (stringLiteralPart select 1) ")) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ; + ) + } + ) + ) + ) + ) + ) + (element + (statement + (tagOperatorStatement + (cfmlfunctionStatement + (cfmlFunction query) + (paramStatementAttributes + (param + (multipartIdentifier (identifier name)) + = + (startExpression + (baseExpression + (unaryExpression + (primaryExpression + (literalExpression (stringLiteral " (stringLiteralPart other) ")) + ) + ) + ) + ) + ) + (param + (multipartIdentifier (identifier cachedWithin)) + = + (startExpression + (baseExpression (unaryExpression (memberExpression (identifier timeout)))) + ) + ) + ) + (compoundStatement + { + (statement + (startExpression + (baseExpression + (unaryExpression + (memberExpression + (functionCall + (identifier echo) + ( + (argumentList + (argument + (startExpression + (baseExpression + (unaryExpression + (primaryExpression + (literalExpression (stringLiteral " (stringLiteralPart select 2) ")) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ; + ) + } + ) + ) + ) + ) + ) + (element + (statement + (tagOperatorStatement + (cfmlfunctionStatement + (cfmlFunction cfdirectory) + (paramStatementAttributes + (param + (multipartIdentifier (identifier (cfmlFunction directory))) + = + (startExpression + (baseExpression + (unaryExpression + (memberExpression + (functionCall + (identifier expandPath) + ( + (argumentList + (argument + (startExpression + (baseExpression + (unaryExpression + (primaryExpression + (literalExpression (stringLiteral " (stringLiteralPart /tmp) ")) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + (param + (multipartIdentifier (identifier action)) + = + (startExpression + (baseExpression + (unaryExpression + (primaryExpression + (literalExpression (stringLiteral " (stringLiteralPart list) ")) + ) + ) + ) + ) + ) + ) + ; + ) + ) + ) + ) +) +/*======*/ +/*===DECOMPILE===*/ +{ +query cachedWithin=createTimeSpan(0, 0, 0, 5) name='results'{ +echo('select 1'); + +}; +query cachedWithin=timeout name='other'{ +echo('select 2'); + +}; +cfdirectory action='list' directory=expandPath('/tmp');; + +} +/*======*/ \ No newline at end of file