From 84bae752c1ec222080f685926d10a9dd3d97a276 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 04:26:36 +0000 Subject: [PATCH] Stop the tag scanner ending a tag inside an arrow operator x + 1> was scanned as the tag ``, because CFMLStartTag.getEnd returns at the first unquoted '>' and the second character of => is one. The truncated text `f = (x) =` then parsed to `f = (x)` -- an expression the visitor accepted, with the body silently gone. Nothing to do with the thin arrow or with lambdaDeclaration, both of which work: `=>` failed identically, and had since long before either was touched. The grammar was never reached with the whole expression. getEnd already special-cases quotes and the ---> of a comment; an arrow joins them. A '-' only counts when it does not follow another '-', so the decrement in still ends the tag, which the fixture pins alongside the arrows. A TestTagFiles fixture rather than a JUnit case: this is the tag path, and nothing under src/test/resources/tag exercised an arrow function, which is why the suite was green. Verified end to end -- 313 cfparser tests, ./gradlew build, CFLint's 675. Differential harness 8 disagreements to 7. Closes #44 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GzpZFd4rnE1Yi2sVHAji35 --- .../parsing/cfmentat/tag/CFMLStartTag.java | 21 ++ .../tag/tests/parsing/cfset_lambda_44.cfm | 8 + .../parsing/cfset_lambda_44.expected.txt | 279 ++++++++++++++++++ 3 files changed, 308 insertions(+) create mode 100644 cfml.parsing/src/test/resources/tag/tests/parsing/cfset_lambda_44.cfm create mode 100644 cfml.parsing/src/test/resources/tag/tests/parsing/cfset_lambda_44.expected.txt diff --git a/cfml.parsing/src/main/java/cfml/parsing/cfmentat/tag/CFMLStartTag.java b/cfml.parsing/src/main/java/cfml/parsing/cfmentat/tag/CFMLStartTag.java index 7ab72e8..9c838f7 100644 --- a/cfml.parsing/src/main/java/cfml/parsing/cfmentat/tag/CFMLStartTag.java +++ b/cfml.parsing/src/main/java/cfml/parsing/cfmentat/tag/CFMLStartTag.java @@ -69,6 +69,8 @@ protected int getEnd(final Source source, final int pos) { if (!isInQuotes && !isInApos) { if (x > 2 && text.subSequence(x - 3, x).equals("---")) { // do nothing, this is a comment + } else if (isArrowOperator(text, x)) { + // do nothing, this '>' is the tail of => or -> inside an expression } else { return x + 1; } @@ -95,6 +97,25 @@ protected int getEnd(final Source source, final int pos) { return endStartTagEnd; } + /** + * True when the '>' at pos is the second character of an arrow operator, as in + * <cfset f = (x) => x + 1>. Without this the tag ends inside the operator + * and the expression is silently truncated to f = (x) =. + * + * A '-' is only an arrow when it does not follow another '-', so the decrement in + * <cfset a = i--> still ends the tag. + */ + private static boolean isArrowOperator(final ParseText text, final int pos) { + if (pos < 1) { + return false; + } + final char previous = text.charAt(pos - 1); + if (previous == '=') { + return true; + } + return previous == '-' && (pos < 2 || text.charAt(pos - 2) != '-'); + } + protected ArrayList getAttributes(String inData) { ArrayList attributes = new ArrayList(); Matcher matcher; diff --git a/cfml.parsing/src/test/resources/tag/tests/parsing/cfset_lambda_44.cfm b/cfml.parsing/src/test/resources/tag/tests/parsing/cfset_lambda_44.cfm new file mode 100644 index 0000000..786f3dd --- /dev/null +++ b/cfml.parsing/src/test/resources/tag/tests/parsing/cfset_lambda_44.cfm @@ -0,0 +1,8 @@ +prefix + t.b()> + t.b()> + x + y> + target.toString() )> + + + diff --git a/cfml.parsing/src/test/resources/tag/tests/parsing/cfset_lambda_44.expected.txt b/cfml.parsing/src/test/resources/tag/tests/parsing/cfset_lambda_44.expected.txt new file mode 100644 index 0000000..55c6c74 --- /dev/null +++ b/cfml.parsing/src/test/resources/tag/tests/parsing/cfset_lambda_44.expected.txt @@ -0,0 +1,279 @@ +START:cfoutput + =========TAG================= + prefix +END:cfoutput +START:cfset + =========TAG================= + t.b()> + TAG + ==========Tokens============== + IDENTIFIER + '=' <=> + IDENTIFIER + '=>' <=>> + IDENTIFIER + '.' <.> + IDENTIFIER + '(' <(> + ')' <)> + ===========Tree================ + (expression + (assignmentExpression + (startExpression + (baseExpression (unaryExpression (memberExpression (identifier closure)))) + ) + = + (startExpression + (baseExpression + (lambdaDeclaration + (identifier t) + => + (startExpression + (baseExpression + (unaryExpression + (memberExpression + (identifier t) + . + (qualifiedFunctionCall (identifier b) ( argumentList )) + ) + ) + ) + ) + ) + ) + ) + ) + + ) + ===========Expression=============== + closure = (t) => t.b() + ================================== +END:cfset +START:cfset + =========TAG================= + t.b()> + TAG + ==========Tokens============== + IDENTIFIER + '=' <=> + IDENTIFIER + '->' <->> + IDENTIFIER + '.' <.> + IDENTIFIER + '(' <(> + ')' <)> + ===========Tree================ + (expression + (assignmentExpression + (startExpression + (baseExpression (unaryExpression (memberExpression (identifier lambda)))) + ) + = + (startExpression + (baseExpression + (lambdaDeclaration + (identifier t) + -> + (startExpression + (baseExpression + (unaryExpression + (memberExpression + (identifier t) + . + (qualifiedFunctionCall (identifier b) ( argumentList )) + ) + ) + ) + ) + ) + ) + ) + ) + + ) + ===========Expression=============== + lambda = (t) -> t.b() + ================================== +END:cfset +START:cfset + =========TAG================= + x + y> + TAG + ==========Tokens============== + IDENTIFIER + '=' <=> + '(' <(> + IDENTIFIER + ',' <,> + IDENTIFIER + ')' <)> + '=>' <=>> + IDENTIFIER + '+' <+> + IDENTIFIER + ===========Tree================ + (expression + (assignmentExpression + (startExpression + (baseExpression + (unaryExpression (memberExpression (identifier parenthesised))) + ) + ) + = + (startExpression + (baseExpression + (lambdaDeclaration + ( + (parameterList (parameter (identifier x)) , (parameter (identifier y))) + ) + => + (startExpression + (baseExpression + (baseExpression (unaryExpression (memberExpression (identifier x)))) + + + (baseExpression (unaryExpression (memberExpression (identifier y)))) + ) + ) + ) + ) + ) + ) + + ) + ===========Expression=============== + parenthesised = (x, y) => x + y + ================================== +END:cfset +START:cfset + =========TAG================= + target.toString() )> + TAG + ==========Tokens============== + IDENTIFIER + '=' <=> + IDENTIFIER + '(' <(> + ')' <)> + '.' <.> + IDENTIFIER + '(' <(> + IDENTIFIER + '->' <->> + IDENTIFIER + '.' <.> + IDENTIFIER + '(' <(> + ')' <)> + ')' <)> + ===========Tree================ + (expression + (assignmentExpression + (startExpression + (baseExpression (unaryExpression (memberExpression (identifier nested)))) + ) + = + (startExpression + (baseExpression + (unaryExpression + (memberExpression + (functionCall (identifier getURLs) ( argumentList )) + . + (qualifiedFunctionCall + (identifier map) + ( + (argumentList + (argument + (lambdaDeclaration + (identifier target) + -> + (startExpression + (baseExpression + (unaryExpression + (memberExpression + (identifier target) + . + (qualifiedFunctionCall (identifier toString) ( argumentList )) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + + ) + ===========Expression=============== + nested = getURLs().map((target) -> target.toString()) + ================================== +END:cfset +START:!--- + =========TAG================= + +END:!--- +START:cfset + =========TAG================= + + TAG + ==========Tokens============== + IDENTIFIER + '=' <=> + IDENTIFIER + '--' <--> + ===========Tree================ + (expression + (assignmentExpression + (startExpression + (baseExpression (unaryExpression (memberExpression (identifier countdown)))) + ) + = + (startExpression + (baseExpression + (unaryExpression (unaryExpression (memberExpression (identifier i))) --) + ) + ) + ) + + ) + ===========Expression=============== + countdown = i-- + ================================== +END:cfset +START:cfset + =========TAG================= + + TAG + ==========Tokens============== + IDENTIFIER + '=' <=> + IDENTIFIER + GT + INTEGER_LITERAL <1> + ===========Tree================ + (expression + (assignmentExpression + (startExpression + (baseExpression (unaryExpression (memberExpression (identifier comparison)))) + ) + = + (startExpression + (baseExpression + (baseExpression (unaryExpression (memberExpression (identifier total)))) + (compareExpressionOperator gt) + (baseExpression (unaryExpression (primaryExpression (literalExpression 1)))) + ) + ) + ) + + ) + ===========Expression=============== + comparison = total gt 1 + ================================== +END:cfset