diff --git a/.claude/settings.local.json b/.claude/settings.local.json
new file mode 100644
index 00000000000..643e37059af
--- /dev/null
+++ b/.claude/settings.local.json
@@ -0,0 +1,8 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(mkdir:*)",
+ "Bash(curl:*)"
+ ]
+ }
+}
diff --git a/core/src/main/java/lucee/runtime/compiler/CFMLCompilerImpl.java b/core/src/main/java/lucee/runtime/compiler/CFMLCompilerImpl.java
index 216d5e6801a..6da0639fb40 100755
--- a/core/src/main/java/lucee/runtime/compiler/CFMLCompilerImpl.java
+++ b/core/src/main/java/lucee/runtime/compiler/CFMLCompilerImpl.java
@@ -41,6 +41,7 @@
import lucee.runtime.exp.TemplateException;
import lucee.runtime.op.Caster;
import lucee.runtime.type.Array;
+import lucee.runtime.type.ArrayImpl;
import lucee.runtime.type.Struct;
import lucee.runtime.type.StructImpl;
import lucee.runtime.type.util.KeyConstants;
@@ -84,17 +85,17 @@ public Struct ast(ConfigPro config, PageSource ps, boolean ignoreScopes) throws
BytecodeFactory factory = BytecodeFactory.getInstance(config);
// , cwi.getFLDs()
- PageImpl page = ((PageImpl) cfmlTagTransformer.transform(factory, config, ps, config.getTLDs(), config.getFLDs(), false, ignoreScopes));
+ PageImpl page = ((PageImpl) cfmlTagTransformer.transform(factory, config, ps, config.getTLDs(), config.getFLDs(), false, ignoreScopes, true));
Struct root = new StructImpl(Struct.TYPE_LINKED);
page.dump(root);
- // TODO better solution than simply look at the offset from script
- if (page.getSourceCode().getSourceOffset() == 10) {
+ boolean isScript = page.getSourceCode().isWrappedInScript();
+ boolean isCFMLCompExt = Constants.isCFMLComponentExtension(ResourceUtil.getExtension(ps.getResource(), ""));
- boolean isCFMLCompExt = Constants.isCFMLComponentExtension(ResourceUtil.getExtension(ps.getResource(), ""));
+ // If parser wrapped script content in cfscript tags, unwrap it in the AST output
+ if (isScript) {
// in case of a component Lucee moves the component to the root, so at the first position is just an
- // empty script, we simply have to emove this
- // TODO remove the script after moving in the parser
+ // empty script, we simply have to remove this
if (isCFMLCompExt) {
removeEmptyScriptTag(root);
}
@@ -102,14 +103,22 @@ public Struct ast(ConfigPro config, PageSource ps, boolean ignoreScopes) throws
extractScriptTagInRoot(root);
}
}
+ // Handle component { } pattern - file wasn't wrapped by parser
+ // but component is inside an explicit cfscript tag that needs unwrapping
+ else if (isCFMLCompExt) {
+ extractComponentFromCfscript(root);
+ }
+
+ // Add compiler metadata to AST root
+ addASTMetadata(root, config, isScript);
+
return root;
}
public Struct ast(ConfigPro config, SourceCode sc, boolean ignoreScopes, Boolean script) throws PageException {
BytecodeFactory factory = BytecodeFactory.getInstance(config);
- // TODO auto when script is null
- if (script != null && script) {
+ if (script) {
TagLibTag scriptTag = CFMLTransformer.getTLT(sc, Constants.CFML_SCRIPT_TAG_NAME, config.getIdentification());
sc.setPos(0);
@@ -124,13 +133,23 @@ public Struct ast(ConfigPro config, SourceCode sc, boolean ignoreScopes, Boolean
Struct root = new StructImpl(Struct.TYPE_LINKED);
page.dump(root);
- if (script != null && script) {
+ if (script) {
extractScriptTagInRoot(root);
}
+ // Add compiler metadata to AST root
+ addASTMetadata(root, config, script);
+
return root;
}
+ // Add compiler settings metadata to the AST Program node
+ private void addASTMetadata(Struct root, ConfigPro config, boolean isScript) {
+ root.setEL("sourceType", isScript ? "script" : "tag");
+ root.setEL("dotNotationUpperCase", config.getDotNotationUpperCase());
+ root.setEL("handleUnquotedAttrValueAsString", config.getHandleUnQuotedAttrValueAsString());
+ }
+
// remove script again (a bit complicated, but atm the only way to do it)
private void extractScriptTagInRoot(Struct root) {
Array body = Caster.toArray(root.get(KeyConstants._body, null), null);
@@ -150,14 +169,58 @@ private void extractScriptTagInRoot(Struct root) {
private void removeEmptyScriptTag(Struct root) {
Array body = Caster.toArray(root.get(KeyConstants._body, null), null);
- if (body != null && body.size() > 1) {
+ if (body != null && body.size() >= 1) {
Struct first = Caster.toStruct(body.get(1, null), null);
if (first != null) {
if ("cfscript".equalsIgnoreCase(Caster.toString(first.get("fullname", null), null))) {
Struct body2 = Caster.toStruct(first.get(KeyConstants._body, null), null);
Array body3 = Caster.toArray(body2.get(KeyConstants._body, null), null);
- if (body3 != null && body3.size() == 0) {
- body.removeEL(1);
+ if (body3 != null) {
+ if (body3.size() == 0) {
+ // Empty cfscript - just remove it
+ body.removeEL(1);
+ }
+ else {
+ // cfscript has content (component) - extract it
+ // Replace root body with cfscript contents
+ root.setEL(KeyConstants._body, body3);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Extract component from explicit component { } wrapper
+ // This handles CFC files where the component is wrapped in cfscript tags by the author
+ private void extractComponentFromCfscript(Struct root) {
+ Array body = Caster.toArray(root.get(KeyConstants._body, null), null);
+ if (body == null || body.size() < 1) return;
+
+ // Look for cfscript tag containing a component
+ for (int i = 1; i <= body.size(); i++) {
+ Struct item = Caster.toStruct(body.get(i, null), null);
+ if (item == null) continue;
+
+ if ("cfscript".equalsIgnoreCase(Caster.toString(item.get("fullname", null), null))) {
+ Struct scriptBody = Caster.toStruct(item.get(KeyConstants._body, null), null);
+ if (scriptBody == null) continue;
+
+ Array scriptContents = Caster.toArray(scriptBody.get(KeyConstants._body, null), null);
+ if (scriptContents == null) continue;
+
+ // Find component inside cfscript body (CFMLTag with name "component" or "interface")
+ for (int j = 1; j <= scriptContents.size(); j++) {
+ Struct child = Caster.toStruct(scriptContents.get(j, null), null);
+ if (child == null) continue;
+
+ String name = Caster.toString(child.get(KeyConstants._name, null), null);
+ if ("component".equalsIgnoreCase(name) || "interface".equalsIgnoreCase(name)) {
+ // Found component - replace root body with just the component
+ Array newBody = new ArrayImpl();
+ newBody.appendEL(child);
+ root.setEL(KeyConstants._body, newBody);
+ return;
}
}
}
diff --git a/core/src/main/java/lucee/runtime/functions/ast/AstFromString.java b/core/src/main/java/lucee/runtime/functions/ast/AstFromString.java
index 4a953924f83..1a496d9f517 100644
--- a/core/src/main/java/lucee/runtime/functions/ast/AstFromString.java
+++ b/core/src/main/java/lucee/runtime/functions/ast/AstFromString.java
@@ -23,9 +23,12 @@ public Object invoke(PageContext pc, Object[] args) throws PageException {
Boolean script = Boolean.FALSE;
if (args.length == 2) {
String mode = Caster.toString(args[1], null);
- //
- if ("script".equalsIgnoreCase(mode)) script = Boolean.TRUE;
-
+ if ("script".equalsIgnoreCase(mode)) {
+ script = Boolean.TRUE;
+ }
+ else if (mode != null && !"tag".equalsIgnoreCase(mode)) {
+ throw new FunctionException(pc, "AstFromString", 2, "mode", "invalid mode [" + mode + "], valid values are [tag, script]");
+ }
}
return ((PageContextImpl) pc).transform(new SourceCode(null, content, false, 0), script);
diff --git a/core/src/main/java/lucee/runtime/type/util/KeyConstants.java b/core/src/main/java/lucee/runtime/type/util/KeyConstants.java
index f07550871da..3924420e515 100644
--- a/core/src/main/java/lucee/runtime/type/util/KeyConstants.java
+++ b/core/src/main/java/lucee/runtime/type/util/KeyConstants.java
@@ -393,6 +393,7 @@ private static Collection.Key init(String key) {
public static final Key _dc_subject = init("dc_subject");
public static final Key _debug = init("debug");
public static final Key _debugging = init("debugging");
+ public static final Key _declaration = init("declaration");
public static final Key _decorator = init("decorator");
public static final Key _default = init("default");
public static final Key _delete = init("delete");
@@ -757,6 +758,8 @@ private static Collection.Key init(String key) {
public static final Key _statuscode = init("statuscode");
public static final Key _statustext = init("statustext");
public static final Key _extends = init("extends");
+ public static final Key _explicit = init("explicit");
+ public static final Key _accessExplicit = init("accessExplicit");
public static final Key _implements = init("implements");
public static final Key __toDateTime = init("_toDateTime");
public static final Key __toNumeric = init("_toNumeric");
diff --git a/core/src/main/java/lucee/transformer/Factory.java b/core/src/main/java/lucee/transformer/Factory.java
index f95671df5b5..b559d4a6102 100644
--- a/core/src/main/java/lucee/transformer/Factory.java
+++ b/core/src/main/java/lucee/transformer/Factory.java
@@ -160,6 +160,8 @@ public abstract class Factory {
public abstract ExprString opString(Expression left, Expression right, boolean concatStatic);
+ public abstract ExprString opStringInterpolation(Expression left, Expression right);
+
public abstract ExprBoolean opBool(Expression left, Expression right, int operation);
public abstract ExprNumber opNumber(Expression left, Expression right, int operation);
diff --git a/core/src/main/java/lucee/transformer/Page.java b/core/src/main/java/lucee/transformer/Page.java
index 7476843d5fb..071acba3c64 100644
--- a/core/src/main/java/lucee/transformer/Page.java
+++ b/core/src/main/java/lucee/transformer/Page.java
@@ -11,6 +11,8 @@ public interface Page extends Root {
public boolean isComponent();
+ public boolean isAST();
+
public SourceCode getSourceCode();
public Config getConfig();
diff --git a/core/src/main/java/lucee/transformer/bytecode/BodyBase.java b/core/src/main/java/lucee/transformer/bytecode/BodyBase.java
index ca50812d66b..84efb8f245f 100755
--- a/core/src/main/java/lucee/transformer/bytecode/BodyBase.java
+++ b/core/src/main/java/lucee/transformer/bytecode/BodyBase.java
@@ -71,7 +71,9 @@ public void addStatement(Statement statement) {
if (statement instanceof PrintOut) {
Expression expr = ((PrintOut) statement).getExpr();
- if (expr instanceof LitString && concatPrintouts(((LitString) expr).getString())) return;
+ // LDEV-5983: Only concatenate if the statement doesn't already belong to another body.
+ // Otherwise concatenating modifies the original PrintOut which may still be used elsewhere.
+ if (expr instanceof LitString && statement.getParent() == null && concatPrintouts(((LitString) expr).getString())) return;
}
statement.setParent(this);
this.statements.add(statement);
diff --git a/core/src/main/java/lucee/transformer/bytecode/BytecodeFactory.java b/core/src/main/java/lucee/transformer/bytecode/BytecodeFactory.java
index 9a417e2f431..d09a59494b1 100644
--- a/core/src/main/java/lucee/transformer/bytecode/BytecodeFactory.java
+++ b/core/src/main/java/lucee/transformer/bytecode/BytecodeFactory.java
@@ -288,7 +288,9 @@ public Variable createVariable(int scope, Position start, Position end) {
@Override
public ExprString opString(Expression left, Expression right) {
- return OpString.toExprString(left, right, true);
+ // Pass false to preserve string literal structure for AST output (LDEV-6022)
+ // Otherwise " javaFunctions;
private Set javaFunctionNames;
+ private boolean forAST;
/**
* @param factory
@@ -1763,6 +1764,15 @@ public boolean isPage() {
return getTagCFObject(null) == null;
}
+ @Override
+ public boolean isAST() {
+ return forAST;
+ }
+
+ public void setAST(boolean forAST) {
+ this.forAST = forAST;
+ }
+
/**
* @return the lastModifed
*/
diff --git a/core/src/main/java/lucee/transformer/bytecode/StaticBody.java b/core/src/main/java/lucee/transformer/bytecode/StaticBody.java
index c2a4657e7e1..7991d3714c2 100644
--- a/core/src/main/java/lucee/transformer/bytecode/StaticBody.java
+++ b/core/src/main/java/lucee/transformer/bytecode/StaticBody.java
@@ -17,7 +17,9 @@
*/
package lucee.transformer.bytecode;
+import lucee.runtime.type.Struct;
import lucee.transformer.Factory;
+import lucee.transformer.Position;
public final class StaticBody extends BodyBase {
@@ -25,4 +27,17 @@ public StaticBody(Factory f) {
super(f);
}
+ public StaticBody(Factory f, Position start, Position end) {
+ super(f);
+ setStart(start);
+ setEnd(end);
+ }
+
+ @Override
+ public void dump(Struct sct) {
+ super.dump(sct);
+ // Mark this block as a static initializer
+ sct.setEL("static", Boolean.TRUE);
+ }
+
}
\ No newline at end of file
diff --git a/core/src/main/java/lucee/transformer/bytecode/expression/ExpressionInvoker.java b/core/src/main/java/lucee/transformer/bytecode/expression/ExpressionInvoker.java
index 8ba8795f2c4..2c102eb4bab 100755
--- a/core/src/main/java/lucee/transformer/bytecode/expression/ExpressionInvoker.java
+++ b/core/src/main/java/lucee/transformer/bytecode/expression/ExpressionInvoker.java
@@ -25,14 +25,23 @@
import org.objectweb.asm.commons.GeneratorAdapter;
import org.objectweb.asm.commons.Method;
+import lucee.runtime.type.Array;
+import lucee.runtime.type.ArrayImpl;
+import lucee.runtime.type.Collection.Key;
import lucee.runtime.type.Struct;
+import lucee.runtime.type.StructImpl;
+import lucee.runtime.type.util.KeyConstants;
import lucee.transformer.TransformerException;
import lucee.transformer.bytecode.BytecodeContext;
+import lucee.transformer.bytecode.expression.var.FunctionMember;
import lucee.transformer.bytecode.expression.var.UDF;
+import lucee.transformer.expression.var.Argument;
import lucee.transformer.bytecode.util.ExpressionUtil;
import lucee.transformer.bytecode.util.Types;
import lucee.transformer.expression.Expression;
import lucee.transformer.expression.Invoker;
+import lucee.transformer.expression.literal.LitString;
+import lucee.transformer.expression.literal.Literal;
import lucee.transformer.expression.var.DataMember;
import lucee.transformer.expression.var.Member;
import lucee.transformer.expression.var.Variable;
@@ -123,6 +132,84 @@ public void addListener(Expression listener) {
@Override
public void dump(Struct sct) {
- expr.dump(sct);
+ // If no members, just dump the wrapped expression
+ if (members.isEmpty()) {
+ expr.dump(sct);
+ return;
+ }
+
+ // Build the member expression chain iteratively
+ // Start with the base expression
+ Struct current = new StructImpl(Struct.TYPE_LINKED);
+ expr.dump(current);
+
+ // Add each member to the chain
+ for (Member member : members) {
+ Struct newNode = new StructImpl(Struct.TYPE_LINKED);
+
+ if (member instanceof FunctionMember) {
+ FunctionMember fm = (FunctionMember) member;
+ Expression nameExpr = fm.getName();
+ String funcName = nameExpr.toString();
+
+ // Function call - create CallExpression
+ newNode.setEL(KeyConstants._type, "CallExpression");
+
+ // Create MemberExpression for callee (object.method)
+ Struct callee = new StructImpl(Struct.TYPE_LINKED);
+ callee.setEL(KeyConstants._type, "MemberExpression");
+ callee.setEL(KeyConstants._computed, Boolean.FALSE);
+ callee.setEL(KeyConstants._object, current);
+
+ Struct property = new StructImpl(Struct.TYPE_LINKED);
+ property.setEL(KeyConstants._type, "Identifier");
+ property.setEL(KeyConstants._name, funcName);
+ callee.setEL(KeyConstants._property, property);
+
+ newNode.setEL(KeyConstants._callee, callee);
+
+ // Add arguments
+ Array arrArgs = new ArrayImpl();
+ newNode.setEL(KeyConstants._arguments, arrArgs);
+ for (Argument arg : fm.getSourceArguments()) {
+ Struct sctArg = new StructImpl(Struct.TYPE_LINKED);
+ arrArgs.appendEL(sctArg);
+ arg.dump(sctArg);
+ }
+ }
+ else if (member instanceof DataMember) {
+ // Property access - create MemberExpression
+ DataMember dm = (DataMember) member;
+ newNode.setEL(KeyConstants._type, "MemberExpression");
+
+ Expression memberName = dm.getName();
+ boolean isComputed = (memberName instanceof LitString && ((LitString) memberName).fromBracket())
+ || !(memberName instanceof Literal);
+ newNode.setEL(KeyConstants._computed, isComputed);
+ newNode.setEL(KeyConstants._object, current);
+
+ Struct property = new StructImpl(Struct.TYPE_LINKED);
+ if (isComputed && memberName instanceof Literal) {
+ property.setEL(KeyConstants._type, "StringLiteral");
+ property.setEL(KeyConstants._value, memberName.toString());
+ }
+ else if (isComputed) {
+ memberName.dump(property);
+ }
+ else {
+ property.setEL(KeyConstants._type, "Identifier");
+ property.setEL(KeyConstants._name, memberName.toString());
+ }
+ newNode.setEL(KeyConstants._property, property);
+ }
+
+ current = newNode;
+ }
+
+ // Copy the final result to the output struct
+ Key[] keys = current.keys();
+ for (Key key : keys) {
+ sct.setEL(key, current.get(key, null));
+ }
}
}
\ No newline at end of file
diff --git a/core/src/main/java/lucee/transformer/bytecode/expression/var/ArgumentImpl.java b/core/src/main/java/lucee/transformer/bytecode/expression/var/ArgumentImpl.java
index ff2acb176d4..5e2e5d33bcf 100755
--- a/core/src/main/java/lucee/transformer/bytecode/expression/var/ArgumentImpl.java
+++ b/core/src/main/java/lucee/transformer/bytecode/expression/var/ArgumentImpl.java
@@ -31,6 +31,7 @@ public class ArgumentImpl extends ExpressionBase implements Argument {
private Expression raw;
private String type;
+ private Expression sourceRaw;
public ArgumentImpl(Expression value, String type) {
super(value.getFactory(), value.getStart(), value.getEnd());
@@ -90,6 +91,15 @@ public String getStringType() {
@Override
public void dump(Struct sct) {
- raw.dump(sct);
+ getSourceRawValue().dump(sct);
}
+
+ public void snapshotSourceValue() {
+ if (sourceRaw == null) sourceRaw = raw;
+ }
+
+ public Expression getSourceRawValue() {
+ return sourceRaw != null ? sourceRaw : raw;
+ }
+
}
\ No newline at end of file
diff --git a/core/src/main/java/lucee/transformer/bytecode/expression/var/Assign.java b/core/src/main/java/lucee/transformer/bytecode/expression/var/Assign.java
index a3cdfcd0aed..ead0c3826d2 100755
--- a/core/src/main/java/lucee/transformer/bytecode/expression/var/Assign.java
+++ b/core/src/main/java/lucee/transformer/bytecode/expression/var/Assign.java
@@ -345,6 +345,11 @@ public void dump(Struct sct) {
sct.setEL(KeyConstants._type, "AssignmentExpression");
sct.setEL(KeyConstants._operator, "ASSIGN");
+ // LDEV-6041: Preserve var keyword declaration
+ if (variable.getScope() == Scope.SCOPE_VAR) {
+ sct.setEL(KeyConstants._declaration, "var");
+ }
+
Struct left = new StructImpl(Struct.TYPE_LINKED);
sct.setEL(KeyConstants._left, left);
variable.dump(left);
@@ -353,5 +358,9 @@ public void dump(Struct sct) {
sct.setEL(KeyConstants._right, right);
value.dump(right);
+ // Include final modifier if set
+ if (modifier == lucee.runtime.component.Member.MODIFIER_FINAL) {
+ sct.setEL(KeyConstants._final, Boolean.TRUE);
+ }
}
}
\ No newline at end of file
diff --git a/core/src/main/java/lucee/transformer/bytecode/expression/var/BIF.java b/core/src/main/java/lucee/transformer/bytecode/expression/var/BIF.java
index 0e1e87705fe..e480d024661 100755
--- a/core/src/main/java/lucee/transformer/bytecode/expression/var/BIF.java
+++ b/core/src/main/java/lucee/transformer/bytecode/expression/var/BIF.java
@@ -34,6 +34,7 @@ public final class BIF extends FunctionMember {
private ClassDefinition cd;
private String returnType = ANY;
private FunctionLibFunction flf;
+ private ExprString originalName; // LDEV-6041: preserve original parsed name for AST
private final Factory factory;
@@ -110,8 +111,19 @@ public void setFlf(FunctionLibFunction flf) {
this.flf = flf;
}
+ /**
+ * LDEV-6041: Store original parsed name for AST round-tripping
+ */
+ public void setOriginalName(ExprString name) {
+ this.originalName = name;
+ }
+
@Override
public ExprString getName() {
+ // LDEV-6041: Return original name if available (preserves case for AST)
+ if (originalName != null) {
+ return originalName;
+ }
return factory.createLitString(flf.getName());
}
}
\ No newline at end of file
diff --git a/core/src/main/java/lucee/transformer/bytecode/expression/var/DynAssign.java b/core/src/main/java/lucee/transformer/bytecode/expression/var/DynAssign.java
index 196a131fac8..d8d613c61f1 100755
--- a/core/src/main/java/lucee/transformer/bytecode/expression/var/DynAssign.java
+++ b/core/src/main/java/lucee/transformer/bytecode/expression/var/DynAssign.java
@@ -38,6 +38,7 @@ public final class DynAssign extends ExpressionBase {
private ExprString name;
private Expression value;
+ private Expression sourceName; // Original expression before string conversion, for AST fidelity
// Object setVariable(String, Object)
private final static Method METHOD_SET_VARIABLE = new Method("setVariable", Types.OBJECT, new Type[] { Types.STRING, Types.OBJECT });
@@ -48,12 +49,13 @@ public DynAssign(Factory f, Position start, Position end) {
/**
* Constructor of the class
- *
+ *
* @param name
* @param value
*/
public DynAssign(Expression name, Expression value) {
super(name.getFactory(), name.getStart(), name.getEnd());
+ this.sourceName = name; // Preserve original for AST output
this.name = name.getFactory().toExprString(name);
this.value = value;
}
@@ -76,12 +78,19 @@ public Type _writeOut(BytecodeContext bc, int mode) throws TransformerException
*/
/**
- * @return the name
+ * @return the name as ExprString (for bytecode generation)
*/
public ExprString getName() {
return name;
}
+ /**
+ * @return the original name expression (for AST fidelity)
+ */
+ public Expression getSourceName() {
+ return sourceName != null ? sourceName : name;
+ }
+
/**
* @return the value
*/
@@ -92,15 +101,16 @@ public Expression getValue() {
@Override
public void dump(Struct sct) {
super.dump(sct);
- sct.setEL(KeyConstants._type, "CallExpression");
- sct.setEL(KeyConstants._operator, "AssignmentExpression");
+ sct.setEL(KeyConstants._type, "AssignmentExpression");
+ sct.setEL(KeyConstants._operator, "ASSIGN");
Struct left = new StructImpl(Struct.TYPE_LINKED);
sct.setEL(KeyConstants._left, left);
- name.dump(sct);
+ // Use sourceName for AST output to preserve original type (e.g., NumberLiteral)
+ getSourceName().dump(left);
Struct right = new StructImpl(Struct.TYPE_LINKED);
sct.setEL(KeyConstants._right, right);
- value.dump(sct);
+ value.dump(right);
}
}
\ No newline at end of file
diff --git a/core/src/main/java/lucee/transformer/bytecode/expression/var/FunctionMember.java b/core/src/main/java/lucee/transformer/bytecode/expression/var/FunctionMember.java
index 9ed8e9a1069..cf6f117b5a9 100755
--- a/core/src/main/java/lucee/transformer/bytecode/expression/var/FunctionMember.java
+++ b/core/src/main/java/lucee/transformer/bytecode/expression/var/FunctionMember.java
@@ -32,6 +32,7 @@ public abstract class FunctionMember implements NamedMember, Member, Func {
private Variable parent;
private boolean safeNavigated;
private Expression safeNavigatedValue;
+ private Argument[] sourceArguments; // original arguments before evaluators modify them
@Override
public final void setParent(Variable parent) {
@@ -88,4 +89,28 @@ public void setSafeNavigatedValue(Expression safeNavigatedValue) {
public Expression getSafeNavigatedValue() {
return safeNavigatedValue;
}
+
+ /**
+ * Snapshots the current arguments as the source arguments.
+ * This should be called after parsing but before evaluators modify arguments.
+ */
+ public void snapshotSourceArguments() {
+ if (sourceArguments == null && arguments.length > 0) {
+ sourceArguments = arguments.clone();
+ // Also snapshot each argument's value in case evaluators modify via setValue()
+ for (Argument arg : sourceArguments) {
+ if (arg instanceof ArgumentImpl) {
+ ((ArgumentImpl) arg).snapshotSourceValue();
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the source arguments (original arguments before evaluator modifications).
+ * If no snapshot was taken, returns the current arguments.
+ */
+ public Argument[] getSourceArguments() {
+ return sourceArguments != null ? sourceArguments : arguments;
+ }
}
\ No newline at end of file
diff --git a/core/src/main/java/lucee/transformer/bytecode/expression/var/NamedArgumentImpl.java b/core/src/main/java/lucee/transformer/bytecode/expression/var/NamedArgumentImpl.java
index 003ab1786d9..573f7791fc8 100755
--- a/core/src/main/java/lucee/transformer/bytecode/expression/var/NamedArgumentImpl.java
+++ b/core/src/main/java/lucee/transformer/bytecode/expression/var/NamedArgumentImpl.java
@@ -50,11 +50,17 @@ public final class NamedArgumentImpl extends ArgumentImpl implements NamedArgume
private Expression name;
private boolean varKeyUpperCase;
+ private char separator;
public NamedArgumentImpl(Expression name, Expression value, String type, boolean varKeyUpperCase) {
+ this(name, value, type, varKeyUpperCase, NamedArgument.SEPARATOR_EQUALS);
+ }
+
+ public NamedArgumentImpl(Expression name, Expression value, String type, boolean varKeyUpperCase, char separator) {
super(value, type);
this.name = name instanceof Null || name instanceof NullConstant ? name.getFactory().createLitString(varKeyUpperCase ? "NULL" : "null") : name;
this.varKeyUpperCase = varKeyUpperCase;
+ this.separator = separator;
}
@Override
@@ -108,6 +114,11 @@ public Expression getName() {
return name;
}
+ @Override
+ public char getSeparator() {
+ return separator;
+ }
+
@Override
public void dump(Struct sct) {
sct.setEL(KeyConstants._type, "NamedArgument");
@@ -121,5 +132,8 @@ public void dump(Struct sct) {
Struct value = new StructImpl(Struct.TYPE_LINKED);
sct.setEL(KeyConstants._value, value);
super.dump(value);
+
+ // separator (: or =)
+ sct.setEL(KeyConstants._separator, String.valueOf(separator));
}
}
\ No newline at end of file
diff --git a/core/src/main/java/lucee/transformer/bytecode/expression/var/VariableImpl.java b/core/src/main/java/lucee/transformer/bytecode/expression/var/VariableImpl.java
index aa19ea9e42f..cd0c55dd378 100644
--- a/core/src/main/java/lucee/transformer/bytecode/expression/var/VariableImpl.java
+++ b/core/src/main/java/lucee/transformer/bytecode/expression/var/VariableImpl.java
@@ -1133,6 +1133,94 @@ private static void buildMemberExpressionIterative(Struct sct, List memb
Struct newNode = new StructImpl(Struct.TYPE_LINKED);
if (member instanceof FunctionMember) {
+ FunctionMember fm = (FunctionMember) member;
+ Object funcNameObj = getName(fm);
+ // Check if this is a dynamic function name (computed property access)
+ boolean isComputedCall = funcNameObj instanceof Struct;
+ String funcName = isComputedCall ? null : funcNameObj.toString();
+
+ // LDEV-6011: Handle internal literal functions as proper AST node types
+ if (current == null && member instanceof BIF && funcName != null) {
+ if ("_literalStruct".equalsIgnoreCase(funcName) || "_literalOrderedStruct".equalsIgnoreCase(funcName)) {
+ // Output as ObjectExpression
+ newNode.setEL(KeyConstants._type, "ObjectExpression");
+ // Track if this is an ordered struct (bracket notation)
+ if ("_literalOrderedStruct".equalsIgnoreCase(funcName)) {
+ newNode.setEL("ordered", Boolean.TRUE);
+ }
+ Array properties = new ArrayImpl();
+ newNode.setEL("properties", properties);
+ // Each argument is a NamedArgument containing both key and value
+ Argument[] args = fm.getSourceArguments();
+ for (Argument arg : args) {
+ Struct prop = new StructImpl(Struct.TYPE_LINKED);
+ prop.setEL(KeyConstants._type, "Property");
+ if (arg instanceof NamedArgument) {
+ NamedArgument na = (NamedArgument) arg;
+ Struct keyNode = new StructImpl(Struct.TYPE_LINKED);
+ na.getName().dump(keyNode);
+ prop.setEL(KeyConstants._key, keyNode);
+ Struct valueNode = new StructImpl(Struct.TYPE_LINKED);
+ na.getValue().dump(valueNode);
+ prop.setEL(KeyConstants._value, valueNode);
+ // Track the separator used (: or =)
+ prop.setEL(KeyConstants._separator, String.valueOf(na.getSeparator()));
+ }
+ else {
+ // Fallback for non-named arguments (shouldn't happen for struct literals)
+ Struct valueNode = new StructImpl(Struct.TYPE_LINKED);
+ arg.dump(valueNode);
+ prop.setEL(KeyConstants._value, valueNode);
+ }
+ properties.appendEL(prop);
+ }
+ current = newNode;
+ continue;
+ }
+ else if ("_literalArray".equalsIgnoreCase(funcName)) {
+ // Output as ArrayExpression
+ newNode.setEL(KeyConstants._type, "ArrayExpression");
+ Array elements = new ArrayImpl();
+ newNode.setEL("elements", elements);
+ for (Argument arg: fm.getSourceArguments()) {
+ Struct elemNode = new StructImpl(Struct.TYPE_LINKED);
+ arg.dump(elemNode);
+ elements.appendEL(elemNode);
+ }
+ current = newNode;
+ continue;
+ }
+ else if ("_createComponent".equalsIgnoreCase(funcName)) {
+ // Output as NewExpression
+ // Arguments order from parser: [constructor args..., component path, type string]
+ // Note: Component path and type are added AFTER snapshotSourceArguments(),
+ // so we need getArguments() not getSourceArguments()
+ // - args[0..n-3]: constructor arguments passed to init()
+ // - args[n-2]: component path (e.g., "MyComponent")
+ // - args[n-1]: type string (e.g., "type:undefined") - should be filtered out
+ newNode.setEL(KeyConstants._type, "NewExpression");
+ Argument[] args = fm.getArguments();
+
+ // Component path is second-to-last argument
+ if (args.length >= 2) {
+ Struct calleeNode = new StructImpl(Struct.TYPE_LINKED);
+ args[args.length - 2].dump(calleeNode);
+ newNode.setEL(KeyConstants._callee, calleeNode);
+ }
+
+ // Constructor arguments are everything except the last two
+ Array argArray = new ArrayImpl();
+ newNode.setEL(KeyConstants._arguments, argArray);
+ for (int j = 0; j < args.length - 2; j++) {
+ Struct argNode = new StructImpl(Struct.TYPE_LINKED);
+ args[j].dump(argNode);
+ argArray.appendEL(argNode);
+ }
+ current = newNode;
+ continue;
+ }
+ }
+
// Function call
newNode.setEL(KeyConstants._type, "CallExpression");
@@ -1143,21 +1231,56 @@ private static void buildMemberExpressionIterative(Struct sct, List memb
// Set callee to current chain (or base identifier)
if (current == null) {
- // First element - base identifier
- Struct callee = new StructImpl(Struct.TYPE_LINKED);
- callee.setEL(KeyConstants._type, "Identifier");
- callee.setEL(KeyConstants._name, getName((FunctionMember) member));
- newNode.setEL(KeyConstants._callee, callee);
+ // First element - base identifier or computed call
+ if (isComputedCall) {
+ // Computed call like variables[expr]() - callee is the computed member expression
+ newNode.setEL(KeyConstants._callee, funcNameObj);
+ }
+ else {
+ Struct callee = new StructImpl(Struct.TYPE_LINKED);
+ callee.setEL(KeyConstants._type, "Identifier");
+ callee.setEL(KeyConstants._name, funcName);
+ newNode.setEL(KeyConstants._callee, callee);
+ }
}
else {
- newNode.setEL(KeyConstants._callee, current);
+ // Method call on object - create MemberExpression for callee
+ Struct callee = new StructImpl(Struct.TYPE_LINKED);
+ callee.setEL(KeyConstants._type, "MemberExpression");
+ callee.setEL(KeyConstants._computed, isComputedCall);
+ // Track safe navigation (?.) as optional=true
+ if (member.getSafeNavigated()) {
+ callee.setEL(KeyConstants._optional, Boolean.TRUE);
+ }
+
+ // LDEV-6011: Handle _getstaticscope/_getsuperstaticscope for :: syntax
+ Struct staticInfo = extractStaticScopeInfo(current);
+ if (staticInfo != null) {
+ callee.setEL(KeyConstants._static, Boolean.TRUE);
+ callee.setEL(KeyConstants._object, staticInfo);
+ }
+ else {
+ callee.setEL(KeyConstants._object, current);
+ }
+
+ if (isComputedCall) {
+ // Computed property access - use the dumped expression
+ callee.setEL(KeyConstants._property, funcNameObj);
+ }
+ else {
+ Struct property = new StructImpl(Struct.TYPE_LINKED);
+ property.setEL(KeyConstants._type, "Identifier");
+ property.setEL(KeyConstants._name, funcName);
+ callee.setEL(KeyConstants._property, property);
+ }
+
+ newNode.setEL(KeyConstants._callee, callee);
}
- // Add arguments
+ // Add arguments (use source arguments to exclude evaluator modifications)
Array arrArgs = new ArrayImpl();
newNode.setEL(KeyConstants._arguments, arrArgs);
- FunctionMember fm = (FunctionMember) member;
- for (Argument arg: fm.getArguments()) {
+ for (Argument arg: fm.getSourceArguments()) {
Struct sctArg = new StructImpl(Struct.TYPE_LINKED);
arrArgs.appendEL(sctArg);
arg.dump(sctArg);
@@ -1174,12 +1297,36 @@ private static void buildMemberExpressionIterative(Struct sct, List memb
else {
// Member expression
newNode.setEL(KeyConstants._type, "MemberExpression");
- newNode.setEL(KeyConstants._computed, false);
- newNode.setEL(KeyConstants._object, current);
+ // Check if this is bracket notation (computed=true) or dot notation (computed=false)
+ Expression memberName = ((DataMember) member).getName();
+ boolean isComputed = (memberName instanceof LitString && ((LitString) memberName).fromBracket())
+ || !(memberName instanceof Literal);
+ newNode.setEL(KeyConstants._computed, isComputed);
+ // Track safe navigation (?.) as optional=true
+ if (member.getSafeNavigated()) {
+ newNode.setEL(KeyConstants._optional, Boolean.TRUE);
+ }
+
+ // LDEV-6011: Handle _getstaticscope/_getsuperstaticscope for :: syntax
+ Struct staticInfo = extractStaticScopeInfo(current);
+ if (staticInfo != null) {
+ newNode.setEL(KeyConstants._static, Boolean.TRUE);
+ newNode.setEL(KeyConstants._object, staticInfo);
+ }
+ else {
+ newNode.setEL(KeyConstants._object, current);
+ }
Struct property = new StructImpl(Struct.TYPE_LINKED);
- property.setEL(KeyConstants._type, "Identifier");
- property.setEL(KeyConstants._name, getName((DataMember) member));
+ if (isComputed) {
+ // Bracket notation - dump the expression to preserve quoteChar etc.
+ memberName.dump(property);
+ }
+ else {
+ // Dot notation - output as Identifier
+ property.setEL(KeyConstants._type, "Identifier");
+ property.setEL(KeyConstants._name, getName((DataMember) member));
+ }
newNode.setEL(KeyConstants._property, property);
}
}
@@ -1200,6 +1347,89 @@ private static Object getName(NamedMember dm) {
return name;
}
+ /**
+ * LDEV-6011: Check if the current AST node represents a _getstaticscope or _getsuperstaticscope call.
+ * If so, extract the class/component name and return it as an Identifier node.
+ * This preserves the :: syntax in the AST output instead of exposing internal function names.
+ *
+ * @param current The current AST Struct being built
+ * @return An Identifier Struct with the class name, or null if not a static scope call
+ */
+ private static Struct extractStaticScopeInfo(Struct current) {
+ if (current == null) return null;
+
+ // Check if this is a CallExpression
+ Object type = current.get(KeyConstants._type, null);
+ if (!"CallExpression".equals(type)) return null;
+
+ // Get the callee
+ Object calleeObj = current.get(KeyConstants._callee, null);
+ if (!(calleeObj instanceof Struct)) return null;
+ Struct callee = (Struct) calleeObj;
+
+ // Check if callee is an Identifier
+ Object calleeType = callee.get(KeyConstants._type, null);
+ if (!"Identifier".equals(calleeType)) return null;
+
+ // Check the function name
+ Object nameObj = callee.get(KeyConstants._name, null);
+ if (nameObj == null) return null;
+ String funcName = nameObj.toString().toLowerCase();
+
+ Struct identifier = new StructImpl(Struct.TYPE_LINKED);
+ identifier.setEL(KeyConstants._type, "Identifier");
+
+ if ("_getsuperstaticscope".equals(funcName)) {
+ // super::method() - no arguments, just return "super" identifier
+ identifier.setEL(KeyConstants._name, "super");
+ return identifier;
+ }
+ else if ("_getstaticscope".equals(funcName)) {
+ // Class::method() - first argument is the class/component name
+ Object argsObj = current.get(KeyConstants._arguments, null);
+ if (!(argsObj instanceof Array)) return null;
+ Array args = (Array) argsObj;
+ if (args.size() < 1) return null;
+
+ // Get the first argument (class name)
+ Object firstArg = args.get(1, null);
+ if (!(firstArg instanceof Struct)) return null;
+ Struct argStruct = (Struct) firstArg;
+
+ // Extract the value (should be a StringLiteral)
+ Object argType = argStruct.get(KeyConstants._type, null);
+ if (!"StringLiteral".equals(argType)) return null;
+
+ Object valueObj = argStruct.get(KeyConstants._value, null);
+ if (valueObj == null) return null;
+
+ // Check if there's a second argument indicating java: prefix
+ boolean hasJavaPrefix = false;
+ if (args.size() >= 2) {
+ Object secondArg = args.get(2, null);
+ if (secondArg instanceof Struct) {
+ Struct secondArgStruct = (Struct) secondArg;
+ Object secondValue = secondArgStruct.get(KeyConstants._value, null);
+ if ("java".equals(secondValue)) {
+ hasJavaPrefix = true;
+ }
+ }
+ }
+
+ // Build the identifier name, preserving java: prefix if present
+ String className = valueObj.toString();
+ if (hasJavaPrefix) {
+ identifier.setEL(KeyConstants._name, "java:" + className);
+ }
+ else {
+ identifier.setEL(KeyConstants._name, className);
+ }
+ return identifier;
+ }
+
+ return null;
+ }
+
}
class VT {
diff --git a/core/src/main/java/lucee/transformer/bytecode/expression/var/VariableRef.java b/core/src/main/java/lucee/transformer/bytecode/expression/var/VariableRef.java
index 0b39ca6db00..01f8b3eecdd 100755
--- a/core/src/main/java/lucee/transformer/bytecode/expression/var/VariableRef.java
+++ b/core/src/main/java/lucee/transformer/bytecode/expression/var/VariableRef.java
@@ -80,4 +80,11 @@ public Type _writeOut(BytecodeContext bc, int mode) throws TransformerException
public void dump(Struct sct) {
variable.dump(sct);
}
+
+ /**
+ * Get the underlying Variable for scope inspection
+ */
+ public Variable getVariable() {
+ return variable;
+ }
}
\ No newline at end of file
diff --git a/core/src/main/java/lucee/transformer/bytecode/literal/LitStringImpl.java b/core/src/main/java/lucee/transformer/bytecode/literal/LitStringImpl.java
index a30039e0a7e..a863c9b230d 100644
--- a/core/src/main/java/lucee/transformer/bytecode/literal/LitStringImpl.java
+++ b/core/src/main/java/lucee/transformer/bytecode/literal/LitStringImpl.java
@@ -50,7 +50,9 @@ public class LitStringImpl extends ExpressionBase implements LitString, ExprStri
public static final int TYPE_LOWER = 2;
private String str;
+ private String rawSource; // Original source representation for AST dump
private boolean fromBracket;
+ private char quoteChar; // Original quote character (' or ") for AST dump
/*
* public static ExprString toExprString(String str, Position start,Position end) { return new
@@ -195,11 +197,55 @@ public boolean fromBracket() {
return fromBracket;
}
+ /**
+ * Set the original source representation for AST dump.
+ * This preserves the exact source text including quotes.
+ */
+ public void setRawSource(String rawSource) {
+ this.rawSource = rawSource;
+ }
+
+ /**
+ * Get the original source representation.
+ */
+ public String getRawSource() {
+ return rawSource;
+ }
+
+ /**
+ * Set the original quote character for AST dump.
+ */
+ public void setQuoteChar(char quoteChar) {
+ this.quoteChar = quoteChar;
+ }
+
+ /**
+ * Get the original quote character.
+ */
+ public char getQuoteChar() {
+ return quoteChar;
+ }
+
@Override
public void dump(Struct sct) {
super.dump(sct);
sct.setEL(KeyConstants._type, "StringLiteral");
sct.setEL(KeyConstants._value, str);
- sct.setEL(KeyConstants._raw, "\"" + str + "\"");
+ // Use rawSource if available (preserves original source representation)
+ // Otherwise compute from value: escape # to ## and " to ""
+ if (rawSource != null) {
+ sct.setEL(KeyConstants._raw, rawSource);
+ }
+ else if (str != null) {
+ String escaped = str.replace("#", "##").replace("\"", "\"\"");
+ sct.setEL(KeyConstants._raw, "\"" + escaped + "\"");
+ }
+ else {
+ sct.setEL(KeyConstants._raw, "null");
+ }
+ // Only include quoteChar if it was set (AST mode)
+ if (quoteChar != 0) {
+ sct.setEL("quoteChar", String.valueOf(quoteChar));
+ }
}
}
\ No newline at end of file
diff --git a/core/src/main/java/lucee/transformer/bytecode/op/AbsOpUnary.java b/core/src/main/java/lucee/transformer/bytecode/op/AbsOpUnary.java
index 072a185188c..564ace22a0f 100644
--- a/core/src/main/java/lucee/transformer/bytecode/op/AbsOpUnary.java
+++ b/core/src/main/java/lucee/transformer/bytecode/op/AbsOpUnary.java
@@ -219,33 +219,56 @@ else if (type == Factory.OP_UNARY_PRE) {
return Types.NUMBER;
}
- private static String toString(int operation) {
- if (operation == Factory.OP_UNARY_PLUS) return "PLUS";
- else if (operation == Factory.OP_UNARY_MINUS) return "MINUS";
- else if (operation == Factory.OP_UNARY_DIVIDE) return "DIVIDE";
- else if (operation == Factory.OP_UNARY_MULTIPLY) return "MULTIPLY";
- else if (operation == Factory.OP_UNARY_CONCAT) return "CONCAT";
- else if (operation == Factory.OP_UNARY_CONCAT) return "CONCAT";
- return "UNKNOWN";
+ private static String toCompoundOperator(int operation) {
+ if (operation == Factory.OP_UNARY_PLUS) return "PLUS_ASSIGN";
+ else if (operation == Factory.OP_UNARY_MINUS) return "MINUS_ASSIGN";
+ else if (operation == Factory.OP_UNARY_DIVIDE) return "DIVIDE_ASSIGN";
+ else if (operation == Factory.OP_UNARY_MULTIPLY) return "MULTIPLY_ASSIGN";
+ else if (operation == Factory.OP_UNARY_CONCAT) return "CONCAT_ASSIGN";
+ return "UNKNOWN_ASSIGN";
+ }
+
+ // Check if this is an increment/decrement (++i, i++, --i, i--)
+ private boolean isUpdateExpression() {
+ // Must be plus or minus operation
+ if (operation != Factory.OP_UNARY_PLUS && operation != Factory.OP_UNARY_MINUS) {
+ return false;
+ }
+ // For ++i and i++, the parser uses factory.NUMBER_ONE() singleton
+ // For x += 1, it parses the value from source creating a different object
+ // Reference equality distinguishes these cases
+ return value == var.getFactory().NUMBER_ONE();
}
@Override
public void dump(Struct sct) {
super.dump(sct);
- sct.setEL(KeyConstants._type, "UnaryExpression");
- sct.setEL(KeyConstants._operator, toString(operation));
- sct.setEL(KeyConstants._prefix, (type == Factory.OP_UNARY_PRE) ? Boolean.TRUE : Boolean.FALSE);
- // variable
- {
- Struct sctVar = new StructImpl(Struct.TYPE_LINKED);
- var.dump(sctVar);
- sct.setEL(KeyConstants._variable, sctVar);
- }
- // value
- {
- Struct sctVal = new StructImpl(Struct.TYPE_LINKED);
- value.dump(sctVal);
- sct.setEL(KeyConstants._value, sctVal);
+
+ // Check if this is ++i, i++, --i, i-- (UpdateExpression)
+ if (isUpdateExpression()) {
+ sct.setEL(KeyConstants._type, "UpdateExpression");
+ sct.setEL(KeyConstants._operator, operation == Factory.OP_UNARY_PLUS ? "++" : "--");
+ sct.setEL(KeyConstants._prefix, type == Factory.OP_UNARY_PRE);
+
+ // argument - the variable being updated
+ Struct argument = new StructImpl(Struct.TYPE_LINKED);
+ sct.setEL(KeyConstants._argument, argument);
+ var.dump(argument);
+ return;
}
+
+ // Output as AssignmentExpression with compound operator
+ sct.setEL(KeyConstants._type, "AssignmentExpression");
+ sct.setEL(KeyConstants._operator, toCompoundOperator(operation));
+
+ // left - the variable being assigned to
+ Struct left = new StructImpl(Struct.TYPE_LINKED);
+ sct.setEL(KeyConstants._left, left);
+ var.dump(left);
+
+ // right - the value being added/subtracted/etc
+ Struct right = new StructImpl(Struct.TYPE_LINKED);
+ sct.setEL(KeyConstants._right, right);
+ value.dump(right);
}
}
\ No newline at end of file
diff --git a/core/src/main/java/lucee/transformer/bytecode/op/OpContional.java b/core/src/main/java/lucee/transformer/bytecode/op/OpContional.java
index a740798df6a..827048acb6b 100755
--- a/core/src/main/java/lucee/transformer/bytecode/op/OpContional.java
+++ b/core/src/main/java/lucee/transformer/bytecode/op/OpContional.java
@@ -105,7 +105,7 @@ public void dump(Struct sct) {
// alternate
{
Struct alternate = new StructImpl(Struct.TYPE_LINKED);
- left.dump(alternate);
+ right.dump(alternate);
sct.setEL(KeyConstants._alternate, alternate);
}
}
diff --git a/core/src/main/java/lucee/transformer/bytecode/op/OpNegateNumber.java b/core/src/main/java/lucee/transformer/bytecode/op/OpNegateNumber.java
index 0d6ef30570c..4f546b08c52 100755
--- a/core/src/main/java/lucee/transformer/bytecode/op/OpNegateNumber.java
+++ b/core/src/main/java/lucee/transformer/bytecode/op/OpNegateNumber.java
@@ -41,44 +41,58 @@
public final class OpNegateNumber extends ExpressionBase implements ExprNumber {
private ExprNumber expr;
+ private int operation;
- // public static final int PLUS = 0;
- // public static final int MINUS = 1;
-
- private OpNegateNumber(Expression expr, Position start, Position end) {
+ private OpNegateNumber(Expression expr, int operation, Position start, Position end) {
super(expr.getFactory(), start, end);
this.expr = expr.getFactory().toExprNumber(expr);
+ this.operation = operation;
}
/**
* Create a String expression from an Expression
- *
+ *
* @param expr
* @param start
* @param end
- *
+ *
* @return String expression
*/
public static ExprNumber toExprNumber(Expression expr, Position start, Position end) {
- if (expr instanceof Literal) {
- Number n = ((Literal) expr).getNumber(null);
- if (n != null) {
- if (n instanceof BigDecimal) return expr.getFactory().createLitNumber(((BigDecimal) n).negate(), start, end);
- return expr.getFactory().createLitNumber(BigDecimal.valueOf(-n.doubleValue()), start, end);
- }
- }
- return new OpNegateNumber(expr, start, end);
+ return toExprNumber(expr, Factory.OP_NEG_NBR_MINUS, start, end);
}
public static ExprNumber toExprNumber(Expression expr, int operation, Position start, Position end) {
- if (operation == Factory.OP_NEG_NBR_MINUS) return toExprNumber(expr, start, end);
- return expr.getFactory().toExprNumber(expr);
+ if (operation == Factory.OP_NEG_NBR_MINUS) {
+ // For minus, try to fold literals
+ if (expr instanceof Literal) {
+ Number n = ((Literal) expr).getNumber(null);
+ if (n != null) {
+ if (n instanceof BigDecimal) return expr.getFactory().createLitNumber(((BigDecimal) n).negate(), start, end);
+ return expr.getFactory().createLitNumber(BigDecimal.valueOf(-n.doubleValue()), start, end);
+ }
+ }
+ }
+ // For plus, we could fold literals too, but the value doesn't change
+ // Still need to create the node to preserve the unary plus in AST
+ return new OpNegateNumber(expr, operation, start, end);
}
@Override
public Type _writeOut(BytecodeContext bc, int mode) throws TransformerException {
GeneratorAdapter adapter = bc.getAdapter();
+ // For unary plus, just write out the expression as a number
+ if (operation == Factory.OP_NEG_NBR_PLUS) {
+ if (mode == MODE_VALUE) {
+ expr.writeOut(bc, MODE_VALUE);
+ return Types.DOUBLE_VALUE;
+ }
+ expr.writeOut(bc, MODE_REF);
+ return Types.NUMBER;
+ }
+
+ // For unary minus, negate the value
if (mode == MODE_VALUE) {
expr.writeOut(bc, MODE_VALUE);
adapter.visitInsn(Opcodes.DNEG);
@@ -94,7 +108,7 @@ public Type _writeOut(BytecodeContext bc, int mode) throws TransformerException
public void dump(Struct sct) {
super.dump(sct);
sct.setEL(KeyConstants._type, "UnaryExpression");
- sct.setEL(KeyConstants._operator, "NEGATE");
+ sct.setEL(KeyConstants._operator, operation == Factory.OP_NEG_NBR_PLUS ? "PLUS" : "NEGATE");
sct.setEL(KeyConstants._prefix, Boolean.TRUE);
// argument
{
diff --git a/core/src/main/java/lucee/transformer/bytecode/op/OpNumber.java b/core/src/main/java/lucee/transformer/bytecode/op/OpNumber.java
index c492eb22bd1..841b71d3f0f 100755
--- a/core/src/main/java/lucee/transformer/bytecode/op/OpNumber.java
+++ b/core/src/main/java/lucee/transformer/bytecode/op/OpNumber.java
@@ -150,7 +150,7 @@ else if (op == Factory.OP_DBL_DIVIDE) {
return "DIVIDE";
}
else if (op == Factory.OP_DBL_INTDIV) {
- return "DIVIDE";
+ return "INTDIV";
}
else if (op == Factory.OP_DBL_PLUS) {
return "PLUS";
diff --git a/core/src/main/java/lucee/transformer/bytecode/op/OpString.java b/core/src/main/java/lucee/transformer/bytecode/op/OpString.java
index a7dc484e38f..4e53949f935 100755
--- a/core/src/main/java/lucee/transformer/bytecode/op/OpString.java
+++ b/core/src/main/java/lucee/transformer/bytecode/op/OpString.java
@@ -21,6 +21,8 @@
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.Method;
+import lucee.runtime.type.Array;
+import lucee.runtime.type.ArrayImpl;
import lucee.runtime.type.Struct;
import lucee.runtime.type.StructImpl;
import lucee.runtime.type.util.KeyConstants;
@@ -28,6 +30,7 @@
import lucee.transformer.bytecode.BytecodeContext;
import lucee.transformer.bytecode.expression.ExpressionBase;
import lucee.transformer.bytecode.util.Types;
+import lucee.transformer.cast.Cast;
import lucee.transformer.expression.ExprString;
import lucee.transformer.expression.Expression;
import lucee.transformer.expression.literal.Literal;
@@ -36,6 +39,8 @@ public final class OpString extends ExpressionBase implements ExprString {
private ExprString right;
private ExprString left;
+ private boolean fromInterpolation = false;
+ private char quoteChar; // Original quote character (' or ") for TemplateLiteral AST dump
// String concat (String)
private final static Method METHOD_CONCAT = new Method("concat", Types.STRING, new Type[] { Types.STRING });
@@ -47,6 +52,22 @@ private OpString(Expression left, Expression right) {
this.right = left.getFactory().toExprString(right);
}
+ public void setFromInterpolation(boolean fromInterpolation) {
+ this.fromInterpolation = fromInterpolation;
+ }
+
+ public boolean isFromInterpolation() {
+ return fromInterpolation;
+ }
+
+ public void setQuoteChar(char quoteChar) {
+ this.quoteChar = quoteChar;
+ }
+
+ public char getQuoteChar() {
+ return quoteChar;
+ }
+
public static ExprString toExprString(Expression left, Expression right, boolean concatStatic) {
if (concatStatic && left instanceof Literal && right instanceof Literal) {
String l = ((Literal) left).getString();
@@ -56,6 +77,26 @@ public static ExprString toExprString(Expression left, Expression right, boolean
return new OpString(left, right);
}
+ /**
+ * Create an OpString that represents string interpolation (e.g., "foo.#bar#.baz")
+ * rather than explicit concatenation (e.g., foo & bar).
+ * The result will be dumped as TemplateLiteral/InterpolatedString in the AST.
+ */
+ public static ExprString toExprStringInterpolation(Expression left, Expression right) {
+ // For interpolated strings, we don't want to merge literals at parse time
+ // because we want to preserve the original structure for AST output
+ ExprString result = toExprString(left, right, false);
+ if (result instanceof OpString) {
+ OpString opStr = (OpString) result;
+ opStr.setFromInterpolation(true);
+ // Also propagate interpolation flag from left operand if it's an OpString
+ if (opStr.left instanceof OpString && ((OpString) opStr.left).isFromInterpolation()) {
+ // Already set on this one, nothing more needed
+ }
+ }
+ return result;
+ }
+
@Override
public Type _writeOut(BytecodeContext bc, int mode) throws TransformerException {
left.writeOut(bc, MODE_REF);
@@ -67,19 +108,90 @@ public Type _writeOut(BytecodeContext bc, int mode) throws TransformerException
@Override
public void dump(Struct sct) {
super.dump(sct);
- sct.setEL(KeyConstants._type, "BinaryExpression");
- sct.setEL(KeyConstants._operator, "CONCAT");
- // left
- {
- Struct sctLeft = new StructImpl(Struct.TYPE_LINKED);
- left.dump(sctLeft);
- sct.setEL(KeyConstants._left, sctLeft);
+
+ if (fromInterpolation) {
+ // Output as TemplateLiteral (like JS template literals)
+ // Collect all parts: quasis (string literals) and expressions
+ java.util.List quasis = new java.util.ArrayList<>();
+ java.util.List expressions = new java.util.ArrayList<>();
+ collectInterpolationParts(this, quasis, expressions);
+
+ sct.setEL(KeyConstants._type, "TemplateLiteral");
+
+ // Include quoteChar if set (for round-trip fidelity)
+ if (quoteChar != 0) {
+ sct.setEL("quoteChar", String.valueOf(quoteChar));
+ }
+
+ // Add quasis array
+ Array quasisArr = new ArrayImpl();
+ for (Expression q : quasis) {
+ Struct qNode = new StructImpl(Struct.TYPE_LINKED);
+ q.dump(qNode);
+ quasisArr.appendEL(qNode);
+ }
+ sct.setEL("quasis", quasisArr);
+
+ // Add expressions array
+ Array exprsArr = new ArrayImpl();
+ for (Expression e : expressions) {
+ Struct eNode = new StructImpl(Struct.TYPE_LINKED);
+ Expression expr = (e instanceof Cast) ? ((Cast) e).getExpr() : e;
+ expr.dump(eNode);
+ exprsArr.appendEL(eNode);
+ }
+ sct.setEL("expressions", exprsArr);
+ }
+ else {
+ sct.setEL(KeyConstants._type, "BinaryExpression");
+ sct.setEL(KeyConstants._operator, "CONCAT");
+ // left - unwrap CastString wrapper for cleaner AST output
+ {
+ Struct sctLeft = new StructImpl(Struct.TYPE_LINKED);
+ Expression leftExpr = (left instanceof Cast) ? ((Cast) left).getExpr() : left;
+ leftExpr.dump(sctLeft);
+ sct.setEL(KeyConstants._left, sctLeft);
+ }
+ // right - unwrap CastString wrapper for cleaner AST output
+ {
+ Struct sctRight = new StructImpl(Struct.TYPE_LINKED);
+ Expression rightExpr = (right instanceof Cast) ? ((Cast) right).getExpr() : right;
+ rightExpr.dump(sctRight);
+ sct.setEL(KeyConstants._right, sctRight);
+ }
+ }
+ }
+
+ /**
+ * Recursively collect quasis (string literals) and expressions from an interpolation chain.
+ * Traverses left-to-right, building parallel arrays of quasis and expressions.
+ */
+ private static void collectInterpolationParts(Expression expr, java.util.List quasis, java.util.List expressions) {
+ Expression unwrapped = (expr instanceof Cast) ? ((Cast) expr).getExpr() : expr;
+
+ if (unwrapped instanceof OpString) {
+ OpString op = (OpString) unwrapped;
+ if (op.fromInterpolation) {
+ // Recursively process left side
+ collectInterpolationParts(op.left, quasis, expressions);
+ // Right side is either literal or expression
+ Expression rightUnwrapped = (op.right instanceof Cast) ? ((Cast) op.right).getExpr() : op.right;
+ if (rightUnwrapped instanceof Literal) {
+ quasis.add(rightUnwrapped);
+ }
+ else {
+ expressions.add(rightUnwrapped);
+ }
+ return;
+ }
+ }
+
+ // Base case: not an OpString from interpolation
+ if (unwrapped instanceof Literal) {
+ quasis.add(unwrapped);
}
- // right
- {
- Struct sctRight = new StructImpl(Struct.TYPE_LINKED);
- right.dump(sctRight);
- sct.setEL(KeyConstants._right, sctRight);
+ else {
+ expressions.add(unwrapped);
}
}
}
\ No newline at end of file
diff --git a/core/src/main/java/lucee/transformer/bytecode/op/OpVariable.java b/core/src/main/java/lucee/transformer/bytecode/op/OpVariable.java
index 2a600790d4a..d64ed923174 100755
--- a/core/src/main/java/lucee/transformer/bytecode/op/OpVariable.java
+++ b/core/src/main/java/lucee/transformer/bytecode/op/OpVariable.java
@@ -4,20 +4,24 @@
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
+ * License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
- *
+ *
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
+ *
+ * You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see .
- *
+ *
**/
package lucee.transformer.bytecode.op;
+import lucee.runtime.type.Struct;
+import lucee.runtime.type.StructImpl;
+import lucee.runtime.type.util.KeyConstants;
+import lucee.transformer.Factory;
import lucee.transformer.Position;
import lucee.transformer.bytecode.expression.var.Assign;
import lucee.transformer.expression.Expression;
@@ -28,4 +32,58 @@ public final class OpVariable extends Assign {
public OpVariable(Variable variable, Expression value, Position end) {
super(variable, value, end);
}
+
+ @Override
+ public void dump(Struct sct) {
+ // Check if this is a compound assignment (value is OpNumber with left side matching our variable)
+ Expression value = getValue();
+ if (value instanceof OpNumber) {
+ OpNumber opNum = (OpNumber) value;
+ int op = opNum.getOperation();
+ String compoundOp = getCompoundOperator(op);
+ if (compoundOp != null) {
+ // This is a compound assignment - output with compound operator
+ // Dump position info from grandparent (ExpressionBase)
+ Position start = getStart();
+ Position end = getEnd();
+ if (start != null) {
+ Struct sctStart = new StructImpl(Struct.TYPE_LINKED);
+ sctStart.setEL(KeyConstants._line, start.line);
+ sctStart.setEL(KeyConstants._column, start.displayColumn());
+ sctStart.setEL(KeyConstants._offset, start.displayPosition());
+ sct.setEL(KeyConstants._start, sctStart);
+ }
+ if (end != null) {
+ Struct sctEnd = new StructImpl(Struct.TYPE_LINKED);
+ sctEnd.setEL(KeyConstants._line, end.line);
+ sctEnd.setEL(KeyConstants._column, end.displayColumn());
+ sctEnd.setEL(KeyConstants._offset, end.displayPosition());
+ sct.setEL(KeyConstants._end, sctEnd);
+ }
+
+ sct.setEL(KeyConstants._type, "AssignmentExpression");
+ sct.setEL(KeyConstants._operator, compoundOp);
+
+ Struct left = new StructImpl(Struct.TYPE_LINKED);
+ sct.setEL(KeyConstants._left, left);
+ getVariable().dump(left);
+
+ Struct right = new StructImpl(Struct.TYPE_LINKED);
+ sct.setEL(KeyConstants._right, right);
+ opNum.getRight().dump(right);
+ return;
+ }
+ }
+ // Fall back to default Assign dump
+ super.dump(sct);
+ }
+
+ private static String getCompoundOperator(int op) {
+ if (op == Factory.OP_DBL_PLUS) return "PLUS_ASSIGN";
+ if (op == Factory.OP_DBL_MINUS) return "MINUS_ASSIGN";
+ if (op == Factory.OP_DBL_MULTIPLY) return "MULTIPLY_ASSIGN";
+ if (op == Factory.OP_DBL_DIVIDE) return "DIVIDE_ASSIGN";
+ if (op == Factory.OP_DBL_MODULUS) return "MODULUS_ASSIGN";
+ return null;
+ }
}
\ No newline at end of file
diff --git a/core/src/main/java/lucee/transformer/bytecode/statement/DoWhile.java b/core/src/main/java/lucee/transformer/bytecode/statement/DoWhile.java
index 5f67f765ade..26897e16596 100755
--- a/core/src/main/java/lucee/transformer/bytecode/statement/DoWhile.java
+++ b/core/src/main/java/lucee/transformer/bytecode/statement/DoWhile.java
@@ -100,6 +100,11 @@ public void dump(Struct sct) {
super.dump(sct);
sct.setEL(KeyConstants._type, "DoWhileStatement");
+ // label
+ if ( label != null ) {
+ sct.setEL(KeyConstants._label, label);
+ }
+
// body
{
Struct body = new StructImpl(Struct.TYPE_LINKED);
diff --git a/core/src/main/java/lucee/transformer/bytecode/statement/For.java b/core/src/main/java/lucee/transformer/bytecode/statement/For.java
index 2cae4b3b664..7e2d02684c7 100755
--- a/core/src/main/java/lucee/transformer/bytecode/statement/For.java
+++ b/core/src/main/java/lucee/transformer/bytecode/statement/For.java
@@ -130,6 +130,11 @@ public void dump(Struct sct) {
super.dump(sct);
sct.setEL(KeyConstants._type, "ForStatement");
+ // label
+ if ( label != null ) {
+ sct.setEL(KeyConstants._label, label);
+ }
+
// init
if ( this.init != null ) {
Struct init = new StructImpl(Struct.TYPE_LINKED);
diff --git a/core/src/main/java/lucee/transformer/bytecode/statement/ForEach.java b/core/src/main/java/lucee/transformer/bytecode/statement/ForEach.java
index 0c0b8e03948..3563f2dc70c 100755
--- a/core/src/main/java/lucee/transformer/bytecode/statement/ForEach.java
+++ b/core/src/main/java/lucee/transformer/bytecode/statement/ForEach.java
@@ -26,6 +26,7 @@
import lucee.runtime.type.Struct;
import lucee.runtime.type.StructImpl;
+import lucee.runtime.type.scope.Scope;
import lucee.runtime.type.util.KeyConstants;
import lucee.runtime.util.ForEachUtil;
import lucee.transformer.Body;
@@ -173,6 +174,16 @@ public void dump(Struct sct) {
super.dump(sct);
sct.setEL(KeyConstants._type, "ForOfStatement");
+ // LDEV-6041: Preserve var keyword declaration
+ if (key.getVariable().getScope() == Scope.SCOPE_VAR) {
+ sct.setEL(KeyConstants._declaration, "var");
+ }
+
+ // label
+ if ( label != null ) {
+ sct.setEL(KeyConstants._label, label);
+ }
+
// left
{
Struct left = new StructImpl(Struct.TYPE_LINKED);
diff --git a/core/src/main/java/lucee/transformer/bytecode/statement/TagIsland.java b/core/src/main/java/lucee/transformer/bytecode/statement/TagIsland.java
new file mode 100644
index 00000000000..f404b248a83
--- /dev/null
+++ b/core/src/main/java/lucee/transformer/bytecode/statement/TagIsland.java
@@ -0,0 +1,102 @@
+package lucee.transformer.bytecode.statement;
+
+import java.util.List;
+
+import lucee.runtime.type.Array;
+import lucee.runtime.type.ArrayImpl;
+import lucee.runtime.type.Struct;
+import lucee.runtime.type.StructImpl;
+import lucee.runtime.type.util.KeyConstants;
+import lucee.transformer.Body;
+import lucee.transformer.Factory;
+import lucee.transformer.Position;
+import lucee.transformer.TransformerException;
+import lucee.transformer.bytecode.BodyBase;
+import lucee.transformer.bytecode.BytecodeContext;
+import lucee.transformer.statement.Statement;
+
+/**
+ * Represents a tag island in cfscript - tag code embedded between ``` delimiters
+ */
+public final class TagIsland extends StatementBaseNoFinal implements Body {
+
+ private Body body;
+
+ public TagIsland(Factory f, Position start, Position end) {
+ super(f, start, end);
+ this.body = new BodyBase(f);
+ }
+
+ public Body getBody() {
+ return body;
+ }
+
+ @Override
+ public void _writeOut(BytecodeContext bc) throws TransformerException {
+ // Write out the body contents
+ body.writeOut(bc);
+ }
+
+ @Override
+ public void dump(Struct sct) {
+ super.dump(sct);
+ sct.setEL(KeyConstants._type, "TagIsland");
+
+ // Dump body as array of statements
+ Array bodyArr = new ArrayImpl();
+ List statements = body.getStatements();
+ for (Statement stmt : statements) {
+ Struct stmtSct = new StructImpl(Struct.TYPE_LINKED);
+ stmt.dump(stmtSct);
+ bodyArr.appendEL(stmtSct);
+ }
+ sct.setEL(KeyConstants._body, bodyArr);
+ }
+
+ // Body interface methods - delegate to inner body
+ @Override
+ public void addStatement(Statement statement) {
+ body.addStatement(statement);
+ }
+
+ @Override
+ public void addFirst(Statement statement) {
+ body.addFirst(statement);
+ }
+
+ @Override
+ public List getStatements() {
+ return body.getStatements();
+ }
+
+ @Override
+ public boolean hasStatements() {
+ return body.hasStatements();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return body.isEmpty();
+ }
+
+ @Override
+ public void setParent(Statement parent) {
+ super.setParent(parent);
+ body.setParent(parent);
+ }
+
+ @Override
+ public void addPrintOut(Factory factory, String str, Position start, Position end) {
+ body.addPrintOut(factory, str, start, end);
+ }
+
+ @Override
+ public void moveStatmentsTo(Body trg) {
+ body.moveStatmentsTo(trg);
+ }
+
+ @Override
+ public void remove(Statement stat) {
+ body.remove(stat);
+ }
+}
diff --git a/core/src/main/java/lucee/transformer/bytecode/statement/While.java b/core/src/main/java/lucee/transformer/bytecode/statement/While.java
index 3a63263bb44..adc771cd81e 100755
--- a/core/src/main/java/lucee/transformer/bytecode/statement/While.java
+++ b/core/src/main/java/lucee/transformer/bytecode/statement/While.java
@@ -111,6 +111,11 @@ public void dump(Struct sct) {
super.dump(sct);
sct.setEL(KeyConstants._type, "WhileStatement");
+ // label
+ if ( label != null ) {
+ sct.setEL(KeyConstants._label, label);
+ }
+
// test
{
Struct test = new StructImpl(Struct.TYPE_LINKED);
diff --git a/core/src/main/java/lucee/transformer/bytecode/statement/tag/TagBase.java b/core/src/main/java/lucee/transformer/bytecode/statement/tag/TagBase.java
index 0e6bb287601..59fb7a02433 100755
--- a/core/src/main/java/lucee/transformer/bytecode/statement/tag/TagBase.java
+++ b/core/src/main/java/lucee/transformer/bytecode/statement/tag/TagBase.java
@@ -40,6 +40,8 @@
import lucee.transformer.bytecode.visitor.ParseBodyVisitor;
import lucee.transformer.library.tag.TagLibTag;
import lucee.transformer.library.tag.TagLibTagAttr;
+import lucee.transformer.expression.Expression;
+import lucee.transformer.expression.literal.Literal;
import lucee.transformer.statement.tag.Attribute;
import lucee.transformer.statement.tag.Tag;
@@ -55,6 +57,10 @@ public abstract class TagBase extends StatementBase implements Tag {
private boolean scriptBase = false;
private Map metadata;
+ private Map sourceAttributes; // original attributes before removeAttribute() calls
+ private String rawDocblock; // raw docblock text for AST round-tripping (components/interfaces)
+ private String docblockDescription; // parsed description from docblock for annotations.description
+ private Map annotations; // @tags from docblock (components/interfaces)
// private Label finallyLabel;
public TagBase(Factory factory, Position start, Position end) {
@@ -149,9 +155,21 @@ public Attribute getAttribute(String name) {
@Override
public Attribute removeAttribute(String name) {
+ // Save original attributes on first removal (for AST dump)
+ snapshotSourceAttributes();
return attributes.remove(name);
}
+ /**
+ * Snapshot current attributes for AST dump before evaluators modify them.
+ * Safe to call multiple times - only first call takes effect.
+ */
+ public void snapshotSourceAttributes() {
+ if (sourceAttributes == null) {
+ sourceAttributes = new LinkedHashMap(attributes);
+ }
+ }
+
@Override
public String toString() {
return appendix + ":" + fullname + ":" + super.toString();
@@ -189,6 +207,26 @@ public Map getMetaData() {
return metadata;
}
+ public void setRawDocblock(String rawDocblock) {
+ this.rawDocblock = rawDocblock;
+ }
+
+ public String getRawDocblock() {
+ return rawDocblock;
+ }
+
+ public void setDocblockDescription(String description) {
+ this.docblockDescription = description;
+ }
+
+ public void setAnnotations(Map annotations) {
+ this.annotations = annotations;
+ }
+
+ public Map getAnnotations() {
+ return annotations;
+ }
+
@Override
public void dump(Struct sct) {
super.dump(sct);
@@ -203,21 +241,58 @@ public void dump(Struct sct) {
sct.setEL("isBuiltIn", Boolean.TRUE);
}
- sct.setEL(KeyConstants._name, tagLibTag.getName());
+ // For custom tags with empty name (using appendix), use the appendix as the name
+ String tagName = tagLibTag.getName();
+ if ((tagName == null || tagName.isEmpty()) && appendix != null) {
+ tagName = appendix;
+ }
+ sct.setEL(KeyConstants._name, tagName);
sct.setEL(KeyConstants._nameSpace, tagLibTag.getTagLib().getNameSpace());
sct.setEL(KeyConstants._nameSpaceSeparator, tagLibTag.getTagLib().getNameSpaceSeparator());
if (appendix != null) sct.setEL(KeyConstants._appendix, appendix);
if (fullname != null) sct.setEL(KeyConstants._fullname, fullname);
+ // docblock - raw docblock text for round-tripping (component/interface)
+ if (rawDocblock != null) sct.setEL("docblock", rawDocblock);
+ // annotations - docblock description + @tags from docblock
+ if (docblockDescription != null || (annotations != null && !annotations.isEmpty())) {
+ Struct annot = new StructImpl(Struct.TYPE_LINKED);
+ // description from docblock first line(s)
+ if (docblockDescription != null && !docblockDescription.isEmpty()) {
+ annot.setEL(KeyConstants._description, docblockDescription);
+ }
+ // @tags from docblock
+ if (annotations != null) {
+ for (Entry entry: annotations.entrySet()) {
+ String key = entry.getKey();
+ Attribute attr = entry.getValue();
+ Expression val = attr.getValue();
+ if (val instanceof Literal) {
+ annot.setEL(key, ((Literal) val).getString());
+ }
+ else {
+ Struct s = new StructImpl(Struct.TYPE_LINKED);
+ val.dump(s);
+ annot.setEL(key, s);
+ }
+ }
+ }
+ sct.setEL("annotations", annot);
+ }
- // attributes
+ // attributes (use sourceAttributes if available, as removeAttribute() may have removed some)
Array arrAttrs = new ArrayImpl();
sct.setEL(KeyConstants._attributes, arrAttrs);
- for (Entry entry: attributes.entrySet()) {
+ Map attrsToUse = sourceAttributes != null ? sourceAttributes : attributes;
+ for (Entry entry: attrsToUse.entrySet()) {
Attribute attr = entry.getValue();
+ // Skip default attributes - they weren't in the source
+ if (attr.isDefaultAttribute()) continue;
+
Struct sctAttr = new StructImpl(Struct.TYPE_LINKED);
arrAttrs.appendEL(sctAttr);
sctAttr.setEL(KeyConstants._name, attr.getName());
sctAttr.setEL(KeyConstants._type, "Attribute");
+ sctAttr.setEL(KeyConstants._separator, String.valueOf(attr.getSeparator()));
Struct val = new StructImpl(Struct.TYPE_LINKED);
attr.getValue().dump(val);
diff --git a/core/src/main/java/lucee/transformer/bytecode/statement/tag/TagThread.java b/core/src/main/java/lucee/transformer/bytecode/statement/tag/TagThread.java
index 3607f9299a3..c8e918f8bd1 100755
--- a/core/src/main/java/lucee/transformer/bytecode/statement/tag/TagThread.java
+++ b/core/src/main/java/lucee/transformer/bytecode/statement/tag/TagThread.java
@@ -54,13 +54,16 @@ public void outputName() {
this.outputName = true;
}
- public void init() throws TransformerException {
+ public void init() {
String action = ASMUtil.getAttributeString(this, "action", "run");
// no body
if (!"run".equalsIgnoreCase(action)) return;
- PageImpl page = (PageImpl) ASMUtil.getAncestorPage(null, this);
- index = page.addThread(this);
+ // In AST mode, there may be no Page ancestor (e.g., thread inside arrow function)
+ PageImpl page = (PageImpl) ASMUtil.getAncestorPage(this, null);
+ if (page != null) {
+ index = page.addThread(this);
+ }
}
diff --git a/core/src/main/java/lucee/transformer/bytecode/statement/udf/Closure.java b/core/src/main/java/lucee/transformer/bytecode/statement/udf/Closure.java
index 941d68d03cb..7ffad7b07a4 100644
--- a/core/src/main/java/lucee/transformer/bytecode/statement/udf/Closure.java
+++ b/core/src/main/java/lucee/transformer/bytecode/statement/udf/Closure.java
@@ -19,6 +19,7 @@
package lucee.transformer.bytecode.statement.udf;
import lucee.runtime.type.Struct;
+import lucee.runtime.type.util.KeyConstants;
import lucee.transformer.Body;
import lucee.transformer.Position;
import lucee.transformer.TransformerException;
@@ -53,5 +54,7 @@ public int getType() {
@Override
public void dump(Struct sct) {
dump(sct, "ClosureDeclaration");
+ // Remove auto-generated name - anonymous closures shouldn't expose internal names
+ sct.removeEL(KeyConstants._name);
}
}
\ No newline at end of file
diff --git a/core/src/main/java/lucee/transformer/bytecode/statement/udf/Function.java b/core/src/main/java/lucee/transformer/bytecode/statement/udf/Function.java
index 80672051ff6..9f97beea929 100644
--- a/core/src/main/java/lucee/transformer/bytecode/statement/udf/Function.java
+++ b/core/src/main/java/lucee/transformer/bytecode/statement/udf/Function.java
@@ -67,6 +67,7 @@
import lucee.transformer.expression.literal.LitInteger;
import lucee.transformer.expression.literal.LitString;
import lucee.transformer.expression.literal.Literal;
+import lucee.transformer.bytecode.literal.LitStringImpl;
import lucee.transformer.statement.Argument;
import lucee.transformer.statement.HasBody;
import lucee.transformer.statement.IFunction;
@@ -135,15 +136,20 @@ public abstract class Function extends StatementBaseNoFinal implements Opcodes,
ExprString name;
ExprString returnType;
+ boolean returnTypeExplicit; // LDEV-6041: track if return type was explicitly specified
ExprBoolean output;
ExprBoolean bufferOutput;
// ExprBoolean abstry=LitBoolean.FALSE;
int access = Component.ACCESS_PUBLIC;
+ boolean accessExplicit; // LDEV-6036: track if access modifier was explicitly specified
ExprString displayName;
ExprString hint;
+ String rawDocblock; // raw docblock text for AST round-tripping
+ String docblockDescription; // parsed description from docblock for annotations.description
Body body;
List arguments = new ArrayList();
- Map metadata;
+ Map metadata; // inline attributes (not from docblock)
+ Map annotations; // @tags from docblock
ExprString returnFormat;
ExprString description;
ExprBoolean secureJson;
@@ -151,8 +157,10 @@ public abstract class Function extends StatementBaseNoFinal implements Opcodes,
ExprInt localMode;
// protected int localIndex = -1;
Literal cachedWithin;
+ Expression sourceCachedWithin; // Original expression for AST fidelity (before evaluation)
int modifier;
protected JavaFunction jf;
+ protected String rawJavaSource; // raw Java source for AST round-tripping
// private final Root root;
protected int index = -1;
@@ -161,7 +169,9 @@ public Function(String name, int access, int modifier, String returnType, Body b
this.name = body.getFactory().createLitString(name);
this.access = access;
this.modifier = modifier;
- if (!StringUtil.isEmpty(returnType)) this.returnType = body.getFactory().createLitString(returnType);
+ // LDEV-6041: Track if return type was explicitly specified
+ this.returnTypeExplicit = !StringUtil.isEmpty(returnType);
+ if (this.returnTypeExplicit) this.returnType = body.getFactory().createLitString(returnType);
else this.returnType = body.getFactory().createLitString("any");
this.body = body;
body.setParent(this);
@@ -512,6 +522,13 @@ public final void addArgument(Expression name, Expression type, Expression requi
arguments.add(new Argument(name, type, required, defaultValue, passByReference, displayName, hint, meta));
}
+ /**
+ * LDEV-6041: Add an existing Argument directly (preserves typeExplicit flag)
+ */
+ public final void addArgument(Argument arg) {
+ arguments.add(arg);
+ }
+
/**
* @return the arguments
*/
@@ -531,10 +548,22 @@ public final void setMetaData(Map metadata) {
this.metadata = metadata;
}
+ public final void setAnnotations(Map annotations) {
+ this.annotations = annotations;
+ }
+
public final void setHint(Factory factory, String hint) {
this.hint = factory.createLitString(hint);
}
+ public final void setRawDocblock(String rawDocblock) {
+ this.rawDocblock = rawDocblock;
+ }
+
+ public final void setDocblockDescription(String description) {
+ this.docblockDescription = description;
+ }
+
public final void addAttribute(BytecodeContext bc, Attribute attr) throws TemplateException {
String name = attr.getName().toLowerCase();
// name
@@ -557,7 +586,9 @@ else if ("access".equals(name)) {
else if ("output".equals(name)) this.output = toLitBoolean(bc, name, attr.getValue());
else if ("bufferoutput".equals(name)) this.bufferOutput = toLitBoolean(bc, name, attr.getValue());
else if ("displayname".equals(name)) this.displayName = toLitString(bc, name, attr.getValue());
- else if ("hint".equals(name)) this.hint = toLitString(bc, name, attr.getValue());
+ else if ("hint".equals(name)) {
+ this.hint = toLitString(bc, name, attr.getValue());
+ }
else if ("description".equals(name)) this.description = toLitString(bc, name, attr.getValue());
else if ("returnformat".equals(name)) this.returnFormat = toLitString(bc, name, attr.getValue());
else if ("securejson".equals(name)) this.secureJson = toLitBoolean(bc, name, attr.getValue());
@@ -575,6 +606,7 @@ else if ("localmode".equals(name)) {
}
else if ("cachedwithin".equals(name)) {
try {
+ this.sourceCachedWithin = attr.getValue(); // Preserve original for AST
this.cachedWithin = ASMUtil.cachedWithinValue(attr.getValue());// ASMUtil.timeSpanToLong(attr.getValue());
}
catch (EvaluatorException e) {
@@ -600,13 +632,29 @@ else if ("modifier".equals(name)) {
private final LitString toLitString(BytecodeContext bc, String name, Expression value) throws TransformerException {
ExprString es = value.getFactory().toExprString(value);
- if (!(es instanceof LitString)) throw new TransformerException(bc, "Value of attribute [" + name + "] must have a literal/constant value", getStart());
+ if (!(es instanceof LitString)) {
+ // Handle unquoted identifiers like access=remote - convert Variable to LitString
+ // Preserve position from original expression for AST round-tripping (LDEV-6015)
+ String str = ASMUtil.toString(bc, value, null);
+ if (str != null) {
+ return value.getFactory().createLitString(str, value.getStart(), value.getEnd());
+ }
+ throw new TransformerException(bc, "Value of attribute [" + name + "] must have a literal/constant value", getStart());
+ }
return (LitString) es;
}
private final LitBoolean toLitBoolean(BytecodeContext bc, String name, Expression value) throws TransformerException {
ExprBoolean eb = value.getFactory().toExprBoolean(value);
- if (!(eb instanceof LitBoolean)) throw new TransformerException(bc, "Value of attribute [" + name + "] must have a literal/constant value", getStart());
+ if (!(eb instanceof LitBoolean)) {
+ // Handle unquoted identifiers like output=false - convert Variable to LitBoolean
+ String str = ASMUtil.toString(bc, value, null);
+ if (str != null) {
+ if ("true".equalsIgnoreCase(str)) return value.getFactory().TRUE();
+ if ("false".equalsIgnoreCase(str)) return value.getFactory().FALSE();
+ }
+ throw new TransformerException(bc, "Value of attribute [" + name + "] must have a literal/constant value", getStart());
+ }
return (LitBoolean) eb;
}
@@ -614,6 +662,10 @@ public void setJavaFunction(JavaFunction jf) {
this.jf = jf;
}
+ public void setRawJavaSource(String rawJavaSource) {
+ this.rawJavaSource = rawJavaSource;
+ }
+
public void setIndex(int index) {
this.index = index;
}
@@ -622,6 +674,11 @@ public int getIndex() {
return index;
}
+ // LDEV-6036: setter for tracking if access modifier was explicitly specified
+ public void setAccessExplicit(boolean accessExplicit) {
+ this.accessExplicit = accessExplicit;
+ }
+
public ExprString getName() {
return name;
}
@@ -641,6 +698,10 @@ public void dump(Struct sct, String type) {
if (a != null) {
sct.setEL(KeyConstants._access, a);
}
+ // LDEV-6036: Add explicit flag to distinguish "function test()" from "public function test()"
+ if (accessExplicit) {
+ sct.setEL(KeyConstants._accessExplicit, Boolean.TRUE);
+ }
// modifier
String m = toModifier(modifier);
if (m != null) {
@@ -650,6 +711,10 @@ public void dump(Struct sct, String type) {
if (returnType != null) {
Struct s = new StructImpl(Struct.TYPE_LINKED);
returnType.dump(s);
+ // LDEV-6041: Add explicit flag to distinguish "function test()" from "any function test()"
+ if (returnTypeExplicit) {
+ s.setEL(KeyConstants._explicit, Boolean.TRUE);
+ }
sct.setEL(KeyConstants._returnType, s);
}
// returnFormat
@@ -682,11 +747,64 @@ public void dump(Struct sct, String type) {
description.dump(s);
sct.setEL(KeyConstants._description, s);
}
- // hint
- if (hint != null) {
- Struct s = new StructImpl(Struct.TYPE_LINKED);
- hint.dump(s);
- sct.setEL(KeyConstants._hint, s);
+ // docblock - raw docblock text for round-tripping
+ if (rawDocblock != null) {
+ sct.setEL("docblock", rawDocblock);
+ }
+ // annotations - docblock description + @tags from docblock (@return, @deprecated, etc.)
+ if (docblockDescription != null || (annotations != null && !annotations.isEmpty())) {
+ Struct annot = new StructImpl(Struct.TYPE_LINKED);
+ // description from docblock first line(s)
+ if (docblockDescription != null && !docblockDescription.isEmpty()) {
+ annot.setEL(KeyConstants._description, docblockDescription);
+ }
+ // @tags from docblock
+ if (annotations != null) {
+ for (Map.Entry entry: annotations.entrySet()) {
+ String key = entry.getKey();
+ Attribute attr = entry.getValue();
+ Expression val = attr.getValue();
+ if (val instanceof Literal) {
+ annot.setEL(key, ((Literal) val).getString());
+ }
+ else {
+ Struct s = new StructImpl(Struct.TYPE_LINKED);
+ val.dump(s);
+ annot.setEL(key, s);
+ }
+ }
+ }
+ sct.setEL("annotations", annot);
+ }
+ // metadata - inline custom attributes (not from docblock)
+ // Check if hint is from inline attribute (has quoteChar) vs docblock
+ boolean hintFromInlineAttr = false;
+ if (hint instanceof LitStringImpl) {
+ LitStringImpl ls = (LitStringImpl) hint;
+ hintFromInlineAttr = ls.getQuoteChar() != (char) 0;
+ }
+ if ((metadata != null && !metadata.isEmpty()) || hintFromInlineAttr) {
+ Struct meta = new StructImpl(Struct.TYPE_LINKED);
+ // Add inline hint to metadata if from attribute
+ if (hintFromInlineAttr) {
+ meta.setEL(KeyConstants._hint, ((LitString) hint).getString());
+ }
+ if (metadata != null) {
+ for (Map.Entry entry: metadata.entrySet()) {
+ String key = entry.getKey();
+ Attribute attr = entry.getValue();
+ Expression val = attr.getValue();
+ if (val instanceof Literal) {
+ meta.setEL(key, ((Literal) val).getString());
+ }
+ else {
+ Struct s = new StructImpl(Struct.TYPE_LINKED);
+ val.dump(s);
+ meta.setEL(key, s);
+ }
+ }
+ }
+ sct.setEL(KeyConstants._metadata, meta);
}
// secureJson
if (secureJson != null) {
@@ -706,8 +824,13 @@ public void dump(Struct sct, String type) {
localMode.dump(s);
sct.setEL(KeyConstants._localMode, s);
}
- // cachedWithin
- if (cachedWithin != null) {
+ // cachedWithin - use original expression if available for AST fidelity
+ if (sourceCachedWithin != null) {
+ Struct s = new StructImpl(Struct.TYPE_LINKED);
+ sourceCachedWithin.dump(s);
+ sct.setEL(KeyConstants._cachedWithin, s);
+ }
+ else if (cachedWithin != null) {
Struct s = new StructImpl(Struct.TYPE_LINKED);
cachedWithin.dump(s);
sct.setEL(KeyConstants._cachedWithin, s);
@@ -720,8 +843,16 @@ public void dump(Struct sct, String type) {
Struct param = new StructImpl(Struct.TYPE_LINKED);
params.appendEL(param);
- Expression expr = arg.getType();
- set(param, arg.getType(), KeyConstants._type);
+ // LDEV-6041: Add explicit flag to type if explicitly specified
+ Expression typeExpr = arg.getType();
+ if (typeExpr != null) {
+ Struct typeStruct = new StructImpl(Struct.TYPE_LINKED);
+ typeExpr.dump(typeStruct);
+ if (arg.isTypeExplicit()) {
+ typeStruct.setEL(KeyConstants._explicit, Boolean.TRUE);
+ }
+ param.setEL(KeyConstants._type, typeStruct);
+ }
set(param, arg.getName(), KeyConstants._name);
set(param, arg.getRequired(), KeyConstants._required);
set(param, arg.getDefaultValue(), KeyConstants._defaultValue);
@@ -733,6 +864,10 @@ public void dump(Struct sct, String type) {
if (body != null) {
Struct s = new StructImpl(Struct.TYPE_LINKED);
body.dump(s);
+ // For Java functions, include raw Java source for round-tripping
+ if (rawJavaSource != null) {
+ s.setEL(KeyConstants._raw, rawJavaSource);
+ }
sct.setEL(KeyConstants._body, s);
}
}
diff --git a/core/src/main/java/lucee/transformer/bytecode/statement/udf/Lambda.java b/core/src/main/java/lucee/transformer/bytecode/statement/udf/Lambda.java
index bc84e5ab235..1f4fb759e04 100644
--- a/core/src/main/java/lucee/transformer/bytecode/statement/udf/Lambda.java
+++ b/core/src/main/java/lucee/transformer/bytecode/statement/udf/Lambda.java
@@ -18,6 +18,7 @@
package lucee.transformer.bytecode.statement.udf;
import lucee.runtime.type.Struct;
+import lucee.runtime.type.util.KeyConstants;
import lucee.transformer.Body;
import lucee.transformer.Position;
import lucee.transformer.Root;
@@ -53,5 +54,7 @@ public int getType() {
@Override
public void dump(Struct sct) {
dump(sct, "LambdaDeclaration");
+ // Remove auto-generated name - lambdas shouldn't expose internal names
+ sct.removeEL(KeyConstants._name);
}
}
\ No newline at end of file
diff --git a/core/src/main/java/lucee/transformer/cfml/evaluator/impl/Component.java b/core/src/main/java/lucee/transformer/cfml/evaluator/impl/Component.java
index a296d76075c..e39f1717a94 100755
--- a/core/src/main/java/lucee/transformer/cfml/evaluator/impl/Component.java
+++ b/core/src/main/java/lucee/transformer/cfml/evaluator/impl/Component.java
@@ -87,22 +87,26 @@ public void evaluate(Tag tag, TagLibTag tlt) throws EvaluatorException {
if (p != null && (pPage = p.getParent()) instanceof Page && p.getTagLibTag().getName().equalsIgnoreCase(Constants.CFML_SCRIPT_TAG_NAME)) { // chnaged
page = (Page) pPage;
- // move imports from script to component body
- List children = p.getBody().getStatements();
- Iterator it = children.iterator();
- Statement stat;
- Tag t;
- while (it.hasNext()) {
- stat = it.next();
- if (!(stat instanceof Tag)) continue;
- t = (Tag) stat;
- if (t.getTagLibTag().getName().equals("import")) {
- tag.getBody().addStatement(t);
+
+ // In AST mode, don't move component out of cfscript - preserve source structure
+ if (!page.isAST()) {
+ // move imports from script to component body
+ List children = p.getBody().getStatements();
+ Iterator it = children.iterator();
+ Statement stat;
+ Tag t;
+ while (it.hasNext()) {
+ stat = it.next();
+ if (!(stat instanceof Tag)) continue;
+ t = (Tag) stat;
+ if (t.getTagLibTag().getName().equals("import")) {
+ tag.getBody().addStatement(t);
+ }
}
- }
- // move to page
- ASMUtil.move(tag, page);
+ // move to page
+ ASMUtil.move(tag, page);
+ }
// if(!inline)ASMUtil.replace(p, tag, false);
}
@@ -300,6 +304,19 @@ private boolean isMainComponent(Page page, TagCIObject comp) {
while (it.hasNext()) {
Statement s = it.next();
if (s instanceof TagCIObject) return s == comp;
+ // In AST mode, component may be inside cfscript - check script body too
+ if (page.isAST() && s instanceof Tag) {
+ Tag tag = (Tag) s;
+ if (tag.getTagLibTag() != null && Constants.CFML_SCRIPT_TAG_NAME.equalsIgnoreCase(tag.getTagLibTag().getName())) {
+ if (tag.getBody() != null) {
+ Iterator bodyIt = tag.getBody().getStatements().iterator();
+ while (bodyIt.hasNext()) {
+ Statement bs = bodyIt.next();
+ if (bs instanceof TagCIObject) return bs == comp;
+ }
+ }
+ }
+ }
}
return false;
}
diff --git a/core/src/main/java/lucee/transformer/cfml/evaluator/impl/Function.java b/core/src/main/java/lucee/transformer/cfml/evaluator/impl/Function.java
index f8bce49e4be..41230f665ca 100755
--- a/core/src/main/java/lucee/transformer/cfml/evaluator/impl/Function.java
+++ b/core/src/main/java/lucee/transformer/cfml/evaluator/impl/Function.java
@@ -151,11 +151,14 @@ public void evaluate(Tag tag, TagLibTag libTag, FunctionLib flibs) throws Evalua
// add to static scope
if (isStatic) {
+ Body body = (Body) tag.getParent();
+ // Find the index of the tag before removing it (for AST position preservation)
+ int tagIndex = body.getStatements().indexOf(tag);
+
// remove that tag from parent
ASMUtil.remove(tag);
- Body body = (Body) tag.getParent();
- StaticBody sb = Static.getStaticBody(body);
+ StaticBody sb = Static.getStaticBodyForFunction(body, tag, tagIndex);
sb.addStatement(tag);
}
}
diff --git a/core/src/main/java/lucee/transformer/cfml/evaluator/impl/Loop.java b/core/src/main/java/lucee/transformer/cfml/evaluator/impl/Loop.java
index 2ef37be6feb..1f768e55b6c 100755
--- a/core/src/main/java/lucee/transformer/cfml/evaluator/impl/Loop.java
+++ b/core/src/main/java/lucee/transformer/cfml/evaluator/impl/Loop.java
@@ -182,9 +182,29 @@ public void evaluate(Tag tag, TagLibTag tagLibTag, FunctionLib flibs) throws Eva
throw new EvaluatorException("Wrong Context, Invalid combination of Attributes");
}
+ // Get the condition attribute value
+ Attribute condAttr = tag.getAttribute("condition");
+ Expression condValue = condAttr.getValue();
+
+ // If the condition is already a non-literal expression (e.g., parsed in AST mode),
+ // use it directly instead of trying to re-parse from a string literal
+ if (!(condValue instanceof LitString)) {
+ // Already an expression - just ensure it's cast to boolean
+ try {
+ PageImpl page = (PageImpl) ASMUtil.getAncestorPage(null, tag);
+ tag.addAttribute(new Attribute(false, "condition", page.getFactory().toExprBoolean(condValue), "boolean"));
+ }
+ catch (Exception e) {
+ throw new EvaluatorException(e.getMessage());
+ }
+ loop.setType(TagLoop.TYPE_CONDITION);
+ return;
+ }
+
+ // Original behavior: parse the string literal as an expression
TagLib tagLib = tagLibTag.getTagLib();
ExprTransformer transformer;
- String text = ASMUtil.getAttributeString(tag, "condition");
+ String text = ((LitString) condValue).getString();
try {
transformer = tagLib.getExprTransfomer();
diff --git a/core/src/main/java/lucee/transformer/cfml/evaluator/impl/Query.java b/core/src/main/java/lucee/transformer/cfml/evaluator/impl/Query.java
index 46132b79ea8..62518d954fb 100755
--- a/core/src/main/java/lucee/transformer/cfml/evaluator/impl/Query.java
+++ b/core/src/main/java/lucee/transformer/cfml/evaluator/impl/Query.java
@@ -37,9 +37,11 @@
import lucee.transformer.expression.literal.Literal;
import lucee.transformer.expression.var.Member;
import lucee.transformer.expression.var.Variable;
+import lucee.transformer.Page;
import lucee.transformer.statement.Statement;
import lucee.transformer.statement.tag.Attribute;
import lucee.transformer.statement.tag.Tag;
+import lucee.transformer.bytecode.util.ASMUtil;
/**
* sign print outs for preserver
@@ -56,6 +58,10 @@ public void evaluate(Tag tag) throws EvaluatorException {
// string
if (body != null) {
+ // In AST mode, don't transform the body - preserve original structure
+ Page page = ASMUtil.getAncestorPage(tag, null);
+ if (page != null && page.isAST()) return;
+
List stats = body.getStatements();
if (stats != null) translateChildren(body.getStatements().iterator());
}
diff --git a/core/src/main/java/lucee/transformer/cfml/evaluator/impl/Static.java b/core/src/main/java/lucee/transformer/cfml/evaluator/impl/Static.java
index 8f34def2443..6ca5957174f 100644
--- a/core/src/main/java/lucee/transformer/cfml/evaluator/impl/Static.java
+++ b/core/src/main/java/lucee/transformer/cfml/evaluator/impl/Static.java
@@ -18,7 +18,6 @@
**/
package lucee.transformer.cfml.evaluator.impl;
-import java.util.Iterator;
import java.util.List;
import lucee.commons.lang.StringUtil;
@@ -40,7 +39,6 @@ public final class Static extends EvaluatorSupport {
@Override
public void evaluate(Tag tag, TagLibTag libTag) throws EvaluatorException {
-
// check parent
Body body = null;
@@ -48,16 +46,19 @@ public void evaluate(Tag tag, TagLibTag libTag) throws EvaluatorException {
boolean isCompChild = false;
Tag p = ASMUtil.getParentTag(tag);
+ Tag containingTag = null; // The tag that directly contains this static in the component body
if (p != null && (p instanceof TagComponent || getFullname(p, "").equalsIgnoreCase(compName))) {
isCompChild = true;
body = p.getBody();
+ containingTag = tag; // static tag is directly in component body
}
Tag pp = p != null ? ASMUtil.getParentTag(p) : null;
- if (!isCompChild && pp != null && (p instanceof TagComponent || getFullname(pp, "").equalsIgnoreCase(compName))) {
+ if (!isCompChild && pp != null && (pp instanceof TagComponent || getFullname(pp, "").equalsIgnoreCase(compName))) {
isCompChild = true;
body = pp.getBody();
+ containingTag = p; // static is inside another tag (e.g., cfscript) in component body
}
if (!isCompChild) {
@@ -67,10 +68,14 @@ public void evaluate(Tag tag, TagLibTag libTag) throws EvaluatorException {
// Body body=(Body) tag.getParent();
List children = tag.getBody().getStatements();
+ // Find the index of the containing tag in component body (for AST position preservation)
+ // For direct cfstatic, this is the tag itself. For script static { }, this is cfscript.
+ int tagIndex = containingTag != null ? body.getStatements().indexOf(containingTag) : -1;
+
// remove that tag from parent
ASMUtil.remove(tag);
- StaticBody sb = getStaticBody(body);
+ StaticBody sb = createStaticBody(body, tag, tagIndex);
ASMUtil.addStatements(sb, children);
}
@@ -84,16 +89,38 @@ private String getFullname(Tag tag, String defaultValue) {
return defaultValue;
}
- static StaticBody getStaticBody(Body body) {
- Iterator it = body.getStatements().iterator();
- Statement s;
- while (it.hasNext()) {
- s = it.next();
- if (s instanceof StaticBody) return (StaticBody) s;
+ /**
+ * Creates a new StaticBody and adds it to the component body at the original tag position.
+ * Each static { } block gets its own StaticBody to preserve AST structure.
+ *
+ * @param body The component body to add the StaticBody to
+ * @param tag The original cfstatic tag (for position info)
+ * @param tagIndex The original index of the tag in the body (-1 to append at end)
+ */
+ static StaticBody createStaticBody(Body body, Statement tag, int tagIndex) {
+ StaticBody sb = new StaticBody(body.getFactory(), tag != null ? tag.getStart() : null, tag != null ? tag.getEnd() : null);
+ if (tagIndex >= 0 && tagIndex < body.getStatements().size()) {
+ // Insert at original position
+ body.getStatements().add(tagIndex, sb);
+ sb.setParent(body);
+ }
+ else {
+ // Fallback: append at end
+ body.addStatement(sb);
}
- StaticBody sb = new StaticBody(body.getFactory());
- body.addStatement(sb);
return sb;
}
+ /**
+ * Creates a new StaticBody for static functions at the original tag position.
+ * Each static function gets its own StaticBody to preserve AST structure.
+ *
+ * @param body The component body to add the StaticBody to
+ * @param tag The original cffunction tag (for position info)
+ * @param tagIndex The original index of the tag in the body (-1 to append at end)
+ */
+ static StaticBody getStaticBodyForFunction(Body body, Statement tag, int tagIndex) {
+ return createStaticBody(body, tag, tagIndex);
+ }
+
}
\ No newline at end of file
diff --git a/core/src/main/java/lucee/transformer/cfml/evaluator/impl/TagThread.java b/core/src/main/java/lucee/transformer/cfml/evaluator/impl/TagThread.java
index 8cce969cc76..47aa23c254f 100644
--- a/core/src/main/java/lucee/transformer/cfml/evaluator/impl/TagThread.java
+++ b/core/src/main/java/lucee/transformer/cfml/evaluator/impl/TagThread.java
@@ -16,8 +16,6 @@
**/
package lucee.transformer.cfml.evaluator.impl;
-import lucee.commons.lang.ExceptionUtil;
-import lucee.transformer.TransformerException;
import lucee.transformer.cfml.evaluator.EvaluatorException;
import lucee.transformer.cfml.evaluator.EvaluatorSupport;
import lucee.transformer.library.function.FunctionLib;
@@ -29,13 +27,6 @@ public final class TagThread extends EvaluatorSupport {
@Override
public void evaluate(Tag tag, TagLibTag tagLibTag, FunctionLib flibs) throws EvaluatorException {
lucee.transformer.bytecode.statement.tag.TagThread tt = (lucee.transformer.bytecode.statement.tag.TagThread) tag;
- try {
- tt.init();
- }
- catch (TransformerException te) {
- EvaluatorException ee = new EvaluatorException(te.getMessage());
- ExceptionUtil.initCauseEL(ee, te);
- throw ee;
- }
+ tt.init();
}
}
\ No newline at end of file
diff --git a/core/src/main/java/lucee/transformer/cfml/expression/AbstrCFMLExprTransformer.java b/core/src/main/java/lucee/transformer/cfml/expression/AbstrCFMLExprTransformer.java
index 11c791c0789..5e73bff9a73 100755
--- a/core/src/main/java/lucee/transformer/cfml/expression/AbstrCFMLExprTransformer.java
+++ b/core/src/main/java/lucee/transformer/cfml/expression/AbstrCFMLExprTransformer.java
@@ -46,15 +46,19 @@
import lucee.transformer.bytecode.expression.var.DynAssign;
import lucee.transformer.bytecode.expression.var.FunctionMember;
import lucee.transformer.bytecode.expression.var.NamedArgumentImpl;
+import lucee.transformer.expression.var.NamedArgument;
import lucee.transformer.bytecode.expression.var.UDF;
import lucee.transformer.bytecode.literal.Identifier;
+import lucee.transformer.bytecode.literal.LitStringImpl;
import lucee.transformer.bytecode.literal.Null;
import lucee.transformer.bytecode.literal.NullConstant;
+import lucee.transformer.bytecode.op.OpString;
import lucee.transformer.bytecode.op.OpVariable;
import lucee.transformer.bytecode.statement.tag.TagComponent;
import lucee.transformer.bytecode.statement.udf.Function;
import lucee.transformer.bytecode.util.ASMUtil;
import lucee.transformer.cfml.Data;
+import lucee.transformer.cfml.script.DocComment;
import lucee.transformer.cfml.script.DocCommentTransformer;
import lucee.transformer.cfml.tag.CFMLTransformer;
import lucee.transformer.expression.ExprBoolean;
@@ -295,15 +299,16 @@ private Argument functionArgument(Data data, String type, boolean varKeyUpperCas
try {
if (data.srcCode.forwardIfCurrent(":")) {
comments(data);
- return new NamedArgumentImpl(expr, assignOp(data), type, varKeyUpperCase);
+ return new NamedArgumentImpl(expr, assignOp(data), type, varKeyUpperCase, NamedArgument.SEPARATOR_COLON);
}
else if (expr instanceof DynAssign) {
DynAssign da = (DynAssign) expr;
- return new NamedArgumentImpl(da.getName(), da.getValue(), type, varKeyUpperCase);
+ // Use getSourceName() to preserve original expression type (e.g., NumberLiteral) for AST
+ return new NamedArgumentImpl(da.getSourceName(), da.getValue(), type, varKeyUpperCase, NamedArgument.SEPARATOR_EQUALS);
}
else if (expr instanceof Assign && !(expr instanceof OpVariable)) {
Assign a = (Assign) expr;
- return new NamedArgumentImpl(a.getVariable(), a.getValue(), type, varKeyUpperCase);
+ return new NamedArgumentImpl(a.getVariable(), a.getValue(), type, varKeyUpperCase, NamedArgument.SEPARATOR_EQUALS);
}
}
catch (TransformerException be) {
@@ -347,8 +352,9 @@ else if (expr instanceof NullConstant) {
}
// patch for test()(); only works at the end of an expression!
+ // LDEV-6039: Don't treat ( as function call if there's a newline before it
comments(data);
- while (data.srcCode.isCurrent('(')) {
+ while (data.srcCode.isCurrent('(') && !data.srcCode.hasNLBefore()) {
comments(data);
Call call = new Call(expr);
getFunctionMemberAttrs(data, null, false, call, null);
@@ -937,7 +943,7 @@ else if (data.srcCode.forwardIfCurrent('+')) {
return data.factory.opNumber(data.factory.toExprNumber(expr), data.factory.createLitNumber(1), Factory.OP_DBL_PLUS);
}
comments(data);
- return data.factory.toExprNumber(clip(data));
+ return data.factory.opNegateNumber(clip(data), Factory.OP_NEG_NBR_PLUS, line, data.srcCode.getPosition());
}
return clip(data);
}
@@ -1115,7 +1121,7 @@ protected Expression string(Data data) throws TemplateException {
if (str.length() != 0) {
exprStr = data.factory.createLitString(str.toString(), line, data.srcCode.getPosition());
if (expr != null) {
- expr = data.factory.opString(expr, exprStr);
+ expr = data.factory.opStringInterpolation(expr, exprStr);
}
else expr = exprStr;
str = new StringBuilder();
@@ -1124,7 +1130,7 @@ protected Expression string(Data data) throws TemplateException {
expr = inner;
}
else {
- expr = data.factory.opString(expr, inner);
+ expr = data.factory.opStringInterpolation(expr, inner);
}
}
}
@@ -1149,7 +1155,7 @@ else if (data.srcCode.isCurrent(quoter)) {
if (expr == null) expr = data.factory.createLitString(str.toString(), line, data.srcCode.getPosition());
else if (str.length() != 0) {
- expr = data.factory.opString(expr, data.factory.createLitString(str.toString(), line, data.srcCode.getPosition()));
+ expr = data.factory.opStringInterpolation(expr, data.factory.createLitString(str.toString(), line, data.srcCode.getPosition()));
}
comments(data);
@@ -1158,6 +1164,14 @@ else if (str.length() != 0) {
var.fromHash(true);
}
+ // Set quoteChar to preserve original quote style for AST dump
+ if (expr instanceof LitStringImpl) {
+ ((LitStringImpl) expr).setQuoteChar(quoter);
+ }
+ else if (expr instanceof OpString) {
+ ((OpString) expr).setQuoteChar(quoter);
+ }
+
return expr;
}
@@ -1361,6 +1375,9 @@ protected Expression json(Data data, FunctionLibFunction flf, char start, char e
while (data.srcCode.forwardIfCurrent(','));
comments(data);
+ // Snapshot original arguments before evaluators may modify them
+ bif.snapshotSourceArguments();
+
if (!data.srcCode.forwardIfCurrent(end)) throw new TemplateException(data.srcCode, "Invalid Syntax Closing [" + end + "] not found");
comments(data);
@@ -1390,7 +1407,12 @@ protected Expression json(Data data, FunctionLibFunction flf, char start, char e
private Expression closure(Data data) throws TemplateException {
if (!data.srcCode.forwardIfCurrent("function", '(')) return null;
data.srcCode.previous();
+ // Save docComment - inline closures (e.g. in default param values) should not consume
+ // the docblock that belongs to the outer function
+ DocComment savedDocComment = data.docComment;
Function func = closurePart(data, "closure_" + CreateUniqueId.invoke(), Component.ACCESS_PUBLIC, Component.MODIFIER_NONE, "any", data.srcCode.getPosition(), true);
+ // Restore docComment after closure parsing (closurePart and statement() may have cleared it)
+ data.docComment = savedDocComment;
func.setParent(data.getParent());
return new FunctionAsExpression(func);
}
@@ -1498,15 +1520,7 @@ else if (isStaticChild || data.srcCode.forwardIfCurrent('.') || (safeNavigation
expr = invoker;
}
- // safe navigation
Member member;
- if (safeNavigation) {
- List members = invoker.getMembers();
- if (members.size() > 0) {
- member = members.get(members.size() - 1);
- member.setSafeNavigated(true);
- }
- }
// Method
if (data.srcCode.isCurrent('(')) {
@@ -1515,7 +1529,9 @@ else if (isStaticChild || data.srcCode.forwardIfCurrent('.') || (safeNavigation
}
// property
- else invoker.addMember(member = data.factory.createDataMember(namePropUC));
+ else {
+ invoker.addMember(member = data.factory.createDataMember(namePropUC));
+ }
if (safeNavigation) {
member.setSafeNavigated(true);
@@ -1842,6 +1858,8 @@ private FunctionMember getFunctionMember(Data data, final ExprString name, boole
if (checkLibrary) {
BIF bif = new BIF(data.factory, data.settings, flf, data);
// TODO data.ep.add(flf, bif, data.srcCode);
+ // LDEV-6041: Store original name for AST round-tripping
+ bif.setOriginalName(name);
bif.setArgType(flf.getArgType());
try {
@@ -1860,6 +1878,8 @@ private FunctionMember getFunctionMember(Data data, final ExprString name, boole
FunctionLibFunctionArg arg;
while (it.hasNext()) {
arg = it.next();
+ // Skip hidden args (internal metadata like __filename, __mapping) in ast mode only
+ if (data.ast && arg.isHidden()) continue;
if (arg.getDefaultValue() != null) bif.addArgument(new NamedArgumentImpl(data.factory.createLitString(arg.getName()),
data.factory.createLitString(arg.getDefaultValue()), arg.getTypeAsString(), false));
}
@@ -1871,6 +1891,9 @@ private FunctionMember getFunctionMember(Data data, final ExprString name, boole
int count = getFunctionMemberAttrs(data, name, checkLibrary, fm, flf);
+ // Snapshot original arguments before evaluators may modify them
+ fm.snapshotSourceArguments();
+
if (checkLibrary) {
// pre
if (flf.hasTteClass()) {
@@ -2049,7 +2072,16 @@ private Expression simple(Data data, String[] breakConditions) throws TemplateEx
}
comments(data);
- return data.factory.createLitString(sb.toString(), line, data.srcCode.getPosition());
+ String value = sb.toString();
+ Position end = data.srcCode.getPosition();
+ // Check for boolean literals (case-insensitive)
+ if (value.equalsIgnoreCase("true")) {
+ return data.factory.createLitBoolean(true, line, end);
+ }
+ if (value.equalsIgnoreCase("false")) {
+ return data.factory.createLitBoolean(false, line, end);
+ }
+ return data.factory.createLitString(value, line, end);
}
/**
@@ -2102,7 +2134,8 @@ private boolean multiLineComment(Data data) throws TemplateException {
throw new TemplateException(cfml, "block comment is not closed");
}
if (isDocComment && !data.insideFunction) {
- String comment = cfml.substring(pos - 2, cfml.getPos() - pos);
+ // Include the full comment from /** to */ inclusive
+ String comment = cfml.substring(pos - 2, cfml.getPos() - (pos - 2));
data.docComment = docCommentTransformer.transform(data.factory, comment);
}
return true;
diff --git a/core/src/main/java/lucee/transformer/cfml/expression/SimpleExprTransformer.java b/core/src/main/java/lucee/transformer/cfml/expression/SimpleExprTransformer.java
index 1769f8d389a..91a6fdbab0e 100755
--- a/core/src/main/java/lucee/transformer/cfml/expression/SimpleExprTransformer.java
+++ b/core/src/main/java/lucee/transformer/cfml/expression/SimpleExprTransformer.java
@@ -24,6 +24,7 @@
import lucee.transformer.cfml.Data;
import lucee.transformer.cfml.ExprTransformer;
import lucee.transformer.expression.Expression;
+import lucee.transformer.bytecode.literal.LitStringImpl;
import lucee.transformer.expression.literal.LitString;
import lucee.transformer.util.SourceCode;
@@ -56,7 +57,7 @@ public Expression transform(Data data) throws TemplateException {
/**
* Liest den String ein
- *
+ *
* @return Element
* @throws TemplateException
*/
@@ -65,6 +66,8 @@ public Expression string(Factory f, SourceCode cfml) throws TemplateException {
char quoter = cfml.getCurrentLower();
if (quoter != '"' && quoter != '\'') return null;
StringBuilder str = new StringBuilder();
+ StringBuilder rawSource = new StringBuilder();
+ rawSource.append(quoter); // Start with opening quote
boolean insideSpecial = false;
Position line = cfml.getPosition();
@@ -74,7 +77,7 @@ public Expression string(Factory f, SourceCode cfml) throws TemplateException {
if (cfml.isCurrent(specialChar)) {
insideSpecial = !insideSpecial;
str.append(specialChar);
-
+ rawSource.append(specialChar);
}
// check quoter
else if (!insideSpecial && cfml.isCurrent(quoter)) {
@@ -82,6 +85,8 @@ else if (!insideSpecial && cfml.isCurrent(quoter)) {
if (cfml.isNext(quoter)) {
cfml.next();
str.append(quoter);
+ rawSource.append(quoter);
+ rawSource.append(quoter); // escaped quote in raw
}
// finish
else {
@@ -91,12 +96,19 @@ else if (!insideSpecial && cfml.isCurrent(quoter)) {
// all other character
else {
str.append(cfml.getCurrent());
+ rawSource.append(cfml.getCurrent());
}
}
if (!cfml.forwardIfCurrent(quoter)) throw new TemplateException(cfml, "Invalid Syntax Closing [" + quoter + "] not found");
+ rawSource.append(quoter); // End with closing quote
LitString rtn = f.createLitString(str.toString(), line, cfml.getPosition());
+ // Set raw source and quoteChar to preserve original representation for AST dump
+ if (rtn instanceof LitStringImpl) {
+ ((LitStringImpl) rtn).setRawSource(rawSource.toString());
+ ((LitStringImpl) rtn).setQuoteChar(quoter);
+ }
cfml.removeSpace();
return rtn;
}
@@ -120,7 +132,16 @@ else if (cfml.isCurrent('"') || cfml.isCurrent('#') || cfml.isCurrent('\'')) {
}
cfml.removeSpace();
- return f.createLitString(sb.toString(), line, cfml.getPosition());
+ String value = sb.toString();
+ Position end = cfml.getPosition();
+ // Check for boolean literals (case-insensitive)
+ if (value.equalsIgnoreCase("true")) {
+ return f.createLitBoolean(true, line, end);
+ }
+ if (value.equalsIgnoreCase("false")) {
+ return f.createLitBoolean(false, line, end);
+ }
+ return f.createLitString(value, line, end);
}
}
diff --git a/core/src/main/java/lucee/transformer/cfml/script/AbstrCFMLScriptTransformer.java b/core/src/main/java/lucee/transformer/cfml/script/AbstrCFMLScriptTransformer.java
index 8b35e90f51a..25434f595bb 100755
--- a/core/src/main/java/lucee/transformer/cfml/script/AbstrCFMLScriptTransformer.java
+++ b/core/src/main/java/lucee/transformer/cfml/script/AbstrCFMLScriptTransformer.java
@@ -68,8 +68,10 @@
import lucee.transformer.bytecode.statement.ForEach;
import lucee.transformer.bytecode.statement.Return;
import lucee.transformer.bytecode.statement.Switch;
+import lucee.transformer.bytecode.statement.TagIsland;
import lucee.transformer.bytecode.statement.TryCatchFinally;
import lucee.transformer.bytecode.statement.While;
+import lucee.transformer.bytecode.statement.tag.TagBase;
import lucee.transformer.bytecode.statement.tag.TagComponent;
import lucee.transformer.bytecode.statement.tag.TagOther;
import lucee.transformer.bytecode.statement.tag.TagParam;
@@ -835,7 +837,8 @@ else if (tokens[i] != null) {
}
}
- // no access defined
+ // no access defined - track if it was explicit before applying default (LDEV-6036)
+ boolean accessExplicit = access != -1;
if (access == -1) access = Component.ACCESS_PUBLIC;
// Non access modifier
@@ -907,6 +910,7 @@ else if (tokens[i].equalsIgnoreCase("static")) {
}
}
Function res = closurePart(data, functionName, access, modifier, returnType, line, false);
+ res.setAccessExplicit(accessExplicit); // LDEV-6036: track if access modifier was explicitly specified
if (isStatic) {
if (data.context == CTX_INTERFACE) throw new TemplateException(data.srcCode, "static functions are not allowed within the interface body");
@@ -978,10 +982,12 @@ private ArrayList _getScriptFunctionArguments(Data data, ArrayList();
for (int i = 0; i < _attrs.length; i++) {
_attr = _attrs[i];
- meta.put(_attr.getName(), _attr);
+ String attrName = _attr.getName();
+ // Extract known argument attributes
+ if ("hint".equalsIgnoreCase(attrName)) {
+ hint = data.factory.toExprString(_attr.getValue());
+ }
+ else if ("displayname".equalsIgnoreCase(attrName)) {
+ displayName = data.factory.toExprString(_attr.getValue());
+ }
+ else if ("default".equalsIgnoreCase(attrName) && defVal == null) {
+ defVal = _attr.getValue();
+ }
+ else if ("passbyreference".equalsIgnoreCase(attrName) || "passby".equalsIgnoreCase(attrName)) {
+ ExprBoolean eb = data.factory.toExprBoolean(_attr.getValue());
+ if (eb instanceof LitBoolean) passByRef = (LitBoolean) eb;
+ }
+ else {
+ meta.put(attrName, _attr);
+ }
}
}
- result.add(new Argument(data.factory.createLitString(idName), data.factory.createLitString(typeName), data.factory.createLitBoolean(required), defVal, passByRef,
- displayName, hint, meta));
+ Argument arg = new Argument(data.factory.createLitString(idName), data.factory.createLitString(typeName), data.factory.createLitBoolean(required), defVal, passByRef,
+ displayName, hint, meta);
+ arg.setTypeExplicit(typeExplicit); // LDEV-6041
+ result.add(arg);
comments(data);
}
@@ -1063,8 +1088,8 @@ protected final Function closurePart(Data data, String id, int access, int modif
ArrayList args = getScriptFunctionArguments(data);
for (Argument arg: args) {
- func.addArgument(arg.getName(), arg.getType(), arg.getRequired(), arg.getDefaultValue(), arg.isPassByReference(), arg.getDisplayName(), arg.getHint(),
- arg.getMetaData());
+ // LDEV-6041: Use addArgument(Argument) to preserve typeExplicit flag
+ func.addArgument(arg);
}
// end )
comments(data);
@@ -1072,12 +1097,18 @@ protected final Function closurePart(Data data, String id, int access, int modif
// TagLibTag tlt = CFMLTransformer.getTLT(data.srcCode,"function");
- // doc comment
+ // doc comment - only attach to named functions, not inline closures
String hint = null;
- if (data.docComment != null) {
- func.setHint(data.factory, hint = data.docComment.getHint());
- func.setMetaData(data.docComment.getParams());
- data.docComment = null;
+ if (!closure) {
+ DocComment docComment = data.docComment; // save reference before clearing
+ if (docComment != null) {
+ hint = docComment.getHint();
+ func.setHint(data.factory, hint);
+ func.setDocblockDescription(hint); // store for annotations.description
+ func.setAnnotations(docComment.getParams());
+ func.setRawDocblock(docComment.getRawText());
+ data.docComment = null;
+ }
}
comments(data);
@@ -1199,8 +1230,8 @@ protected final Function closurePart(Data data, String id, int access, int modif
// TODO cachedwithin
- func.setJavaFunction(java(data, body, id, access, modifier, hint, args, attrs, rtnType, output, bufferOutput, displayName, description, returnFormat, secureJson,
- verifyClient, localMode));
+ java(data, func, body, id, access, modifier, hint, args, attrs, rtnType, output, bufferOutput, displayName, description, returnFormat, secureJson,
+ verifyClient, localMode);
}
else {
func.register(data.page);
@@ -1243,7 +1274,7 @@ private Attribute[] remove(Attribute[] attrs, String name) {
return list.toArray(new Attribute[list.size()]);
}
- private JavaFunction java(Data data, Body body, String functionName, int access, int modifier, String hint, ArrayList args, Attribute[] attrs, String rtnType,
+ private void java(Data data, Function func, Body body, String functionName, int access, int modifier, String hint, ArrayList args, Attribute[] attrs, String rtnType,
Boolean output, Boolean bufferOutput, String displayName, String description, int returnFormat, Boolean secureJson, Boolean verifyClient, int localMode)
throws TemplateException {
@@ -1258,21 +1289,35 @@ private JavaFunction java(Data data, Body body, String functionName, int access,
throw new TemplateException(data.srcCode, e.getMessage());
}
- PageSourceCode psc = (PageSourceCode) data.srcCode;// TODO get PS in an other way
- PageSource ps = psc.getPageSource();
+ // In AST mode (no PageSourceCode), we can't compile Java functions
+ // but we still need to parse past the function body
+ PageSource ps = null;
+ if (data.srcCode instanceof PageSourceCode) {
+ ps = ((PageSourceCode) data.srcCode).getPageSource();
+ }
SourceCode sc = data.srcCode;
Position start = sc.getPosition();
findTheEnd(data, start.line);
Position end = sc.getPosition();
String javaCode = sc.substring(start.pos, end.pos - start.pos);
+
+ // Always store raw Java source for AST round-tripping
+ func.setRawJavaSource(javaCode);
+
+ // In AST mode without PageSource, we can't compile - just store raw source
+ // The function body has already been parsed past by findTheEnd
+ if (ps == null) {
+ return;
+ }
try {
String id = data.page.registerJavaFunctionName(functionName);
lucee.commons.lang.compiler.SourceCode _sc = fd.createSourceCode(ps, javaCode, id, functionName, access, modifier, hint, args, output, bufferOutput, displayName,
description, returnFormat, secureJson, verifyClient, localMode);
JavaFunction jf = new JavaFunction(ps, _sc, CompilerFactory.getInstance().compile((ConfigPro) data.config, _sc));
- return jf;
+ func.setJavaFunction(jf);
+ return;
}
catch (JavaCompilerException e) {
Throwable cause = e.getCause();
@@ -1349,8 +1394,8 @@ protected final Function lambdaPart(Data data, String id, int access, int modifi
// add arguments
for (Argument arg: args) {
- func.addArgument(arg.getName(), arg.getType(), arg.getRequired(), arg.getDefaultValue(), arg.isPassByReference(), arg.getDisplayName(), arg.getHint(),
- arg.getMetaData());
+ // LDEV-6041: Use addArgument(Argument) to preserve typeExplicit flag
+ func.addArgument(arg);
}
comments(data);
@@ -1361,10 +1406,14 @@ protected final Function lambdaPart(Data data, String id, int access, int modifi
try {
if (data.srcCode.isCurrent('{')) {
+ // Save docComment - block-style arrow functions shouldn't consume outer docblock (LDEV-5990)
+ DocComment savedDocComment = data.docComment;
Body prior = data.setParent(body);
statement(data, body, CTX_FUNCTION);
data.setParent(prior);
+ // Restore docComment - let the outer caller handle it
+ data.docComment = savedDocComment;
}
else {
if (data.srcCode.forwardIfCurrent("return ")) {
@@ -1378,7 +1427,8 @@ protected final Function lambdaPart(Data data, String id, int access, int modifi
Expression expr = expression(data);
Return rtn = new Return(expr, line, data.srcCode.getPosition());
body.addStatement(rtn);
- data.docComment = null;
+ // Don't clear docComment here - arrow functions shouldn't consume outer docblock (LDEV-5990)
+ // The outer caller (closurePart for named functions) handles docComment appropriately
data.context = prior;
}
@@ -1467,7 +1517,13 @@ private final Tag __multiAttrStatement(Body parent, Data data, TagLibTag tlt) th
Tag tag = getTag(data, parent, tlt, line, null);
tag.setTagLibTag(tlt);
tag.setScriptBase(true);
- if (!StringUtil.isEmpty(appendix)) tag.setAppendix(appendix);
+ if (!StringUtil.isEmpty(appendix)) {
+ tag.setAppendix(appendix);
+ tag.setFullname(type.concat(appendix));
+ }
+ else {
+ tag.setFullname(type);
+ }
// add component meta data
if (data.isCFC) {
@@ -1634,18 +1690,34 @@ private final void addMetaData(Data data, Tag tag, String[] ignoreList) {
tag.addMetaData(data.docComment.getHintAsAttribute(data.factory));
- Map params = data.docComment.getParams();
- Iterator it = params.values().iterator();
- Attribute attr;
- outer: while (it.hasNext()) {
- attr = it.next();
- // ignore list
- if (!ArrayUtil.isEmpty(ignoreList)) {
- for (int i = 0; i < ignoreList.length; i++) {
- if (ignoreList[i].equalsIgnoreCase(attr.getName())) continue outer;
+ // Store raw docblock and annotations for AST round-tripping
+ if (tag instanceof TagBase) {
+ TagBase tb = (TagBase) tag;
+ tb.setRawDocblock(data.docComment.getRawText());
+ tb.setDocblockDescription(data.docComment.getHint()); // for annotations.description
+ // Store docblock @tags as annotations (separate from inline metadata)
+ Map params = data.docComment.getParams();
+ if (params != null && !params.isEmpty()) {
+ Map filteredParams = new HashMap();
+ for (Attribute attr: params.values()) {
+ // Apply ignore list
+ boolean ignored = false;
+ if (!ArrayUtil.isEmpty(ignoreList)) {
+ for (int i = 0; i < ignoreList.length; i++) {
+ if (ignoreList[i].equalsIgnoreCase(attr.getName())) {
+ ignored = true;
+ break;
+ }
+ }
+ }
+ if (!ignored) {
+ filteredParams.put(attr.getName(), attr);
+ }
+ }
+ if (!filteredParams.isEmpty()) {
+ tb.setAnnotations(filteredParams);
}
}
- tag.addMetaData(attr);
}
data.docComment = null;
}
@@ -1708,7 +1780,8 @@ private final Tag _propertyStatement(Data data, Body parent) throws TemplateExce
Attribute attr;
// first fill all regular attribute -> name="value"
- for (int i = attrs.length - 1; i >= 0; i--) {
+ // LDEV-6027: iterate forward to preserve declaration order in AST
+ for (int i = 0; i < attrs.length; i++) {
attr = attrs[i];
if (!isNull(attr.getValue())) {
if (attr.getName().equalsIgnoreCase("name")) {
@@ -1755,7 +1828,9 @@ else if (second == null && !hasName && !hasType) {
}
if (!hasType) {
- property.addAttribute(new Attribute(false, "type", data.factory.createLitString("any"), "string"));
+ Attribute typeAttr = new Attribute(false, "type", data.factory.createLitString("any"), "string");
+ typeAttr.setDefaultAttribute(true);
+ property.addAttribute(typeAttr);
}
if (!hasName) throw new TemplateException(data.srcCode, "missing name declaration for property");
@@ -2019,11 +2094,21 @@ private final Return returnStatement(Data data) throws TemplateException {
private final boolean islandStatement(Data data, Body parent) throws TemplateException {
if (!data.srcCode.forwardIfCurrent(TAG_ISLAND_INDICATOR)) return false;
- // now we have to jump into the tag parser
+
+ Position start = data.srcCode.getPosition();
+
+ // Create a TagIsland wrapper to hold the tag content
+ TagIsland island = new TagIsland(data.factory, start, null);
+
+ // now we have to jump into the tag parser, parsing into the TagIsland body
CFMLTransformer tag = new CFMLTransformer(true);
- tag.transform(data, parent);
+ tag.transform(data, island);
if (!data.srcCode.forwardIfCurrent(TAG_ISLAND_INDICATOR)) throw new TemplateException(data.srcCode, "missing closing tag indicator [" + TAG_ISLAND_INDICATOR + "]");
+
+ island.setEnd(data.srcCode.getPosition());
+ parent.addStatement(island);
+
comments(data);
return true;
@@ -2066,6 +2151,7 @@ private final Statement __singleAttrStatement(Body parent, Data data, TagLibTag
Tag tag = getTag(data, parent, tlt, line, null);
tag.setScriptBase(true);
tag.setTagLibTag(tlt);
+ tag.setFullname(tagName);
comments(data);
@@ -2074,6 +2160,7 @@ private final Statement __singleAttrStatement(Body parent, Data data, TagLibTag
String attrName = null;
Expression attrValue = null;
short attrType = ATTR_TYPE_NONE;
+ boolean handledAsNamedAttrs = false;
if (attr != null) {
attrType = attr.getScriptSupport();
char c = data.srcCode.getCurrent();
@@ -2088,6 +2175,24 @@ private final Statement __singleAttrStatement(Body parent, Data data, TagLibTag
data.srcCode.setPos(p);
}
}
+ // Check if this looks like a named attribute (identifier="literal") rather than positional
+ else if (looksLikeNamedAttribute(data, tlt)) {
+ // Parse as named attributes instead
+ // Handle optional parentheses around attributes: throw (message="test")
+ boolean hasParen = data.srcCode.forwardIfCurrent('(');
+ if (hasParen) data.srcCode.removeSpace();
+ Attribute[] attrs = attributes(tag, tlt, data, hasParen ? BRACKED : SEMI_BLOCK, data.factory.EMPTY(), tlt.getScript().getRtexpr() ? Boolean.TRUE : Boolean.FALSE, null, false, ',', false);
+ for (Attribute a : attrs) {
+ tag.addAttribute(a);
+ }
+ if (hasParen) {
+ data.srcCode.removeSpace();
+ if (!data.srcCode.forwardIfCurrent(')')) {
+ throw new TemplateException(data.srcCode, "missing closing parenthesis for tag attributes");
+ }
+ }
+ handledAsNamedAttrs = true;
+ }
else attrValue = attributeValue(data, tlt.getScript().getRtexpr());
if (attrValue != null && isOperator(c)) {
@@ -2102,7 +2207,7 @@ private final Statement __singleAttrStatement(Body parent, Data data, TagLibTag
TagLibTagAttr tlta = tlt.getAttribute(attr.getName(), true);
tag.addAttribute(new Attribute(false, attrName, data.factory.toExpression(attrValue, tlta.getType()), tlta.getType()));
}
- else if (ATTR_TYPE_REQUIRED == attrType) {
+ else if (ATTR_TYPE_REQUIRED == attrType && !handledAsNamedAttrs) {
data.srcCode.setPos(pos);
return null;
}
@@ -2133,6 +2238,54 @@ private boolean isOperator(char c) {
return c == '=' || c == '+' || c == '-';
}
+ /**
+ * Check if current position looks like a named attribute (identifier="literal") for a single-attr tag.
+ * This prevents "exit method="exitTag"" from being parsed as an assignment expression.
+ * Only triggers for literal values (quoted strings), NOT for expressions like "include template=var".
+ * Also handles parenthesized attributes like "throw (message="test")".
+ */
+ private boolean looksLikeNamedAttribute(Data data, TagLibTag tlt) {
+ int pos = data.srcCode.getPos();
+ try {
+ // Skip opening paren if present (for syntax like "throw (message="test")")
+ boolean hasParen = data.srcCode.isCurrent('(');
+ if (hasParen) {
+ data.srcCode.next();
+ data.srcCode.removeSpace();
+ }
+
+ // Try to read an identifier
+ String id = CFMLTransformer.identifier(data.srcCode, false, true);
+ if (StringUtil.isEmpty(id)) return false;
+
+ // Skip whitespace
+ data.srcCode.removeSpace();
+
+ // Check if followed by =
+ if (!data.srcCode.isCurrent('=')) return false;
+
+ // Check if this identifier is a known attribute for this tag
+ if (tlt == null || tlt.getAttribute(id.toLowerCase(), true) == null) {
+ return false;
+ }
+
+ // Move past the =
+ data.srcCode.next();
+ data.srcCode.removeSpace();
+
+ // Only treat as named attribute if the value is a quoted string literal
+ // This distinguishes "exit method="exitTag"" from "include template=var"
+ char c = data.srcCode.getCurrent();
+ return c == '"' || c == '\'';
+ }
+ catch (TemplateException e) {
+ return false;
+ }
+ finally {
+ data.srcCode.setPos(pos);
+ }
+ }
+
/*
* protected Statement __singleAttrStatement(Body parent, Data data, String tagName,String
* attrName,int attrType, boolean allowExpression, boolean allowTwiceAttr) throws TemplateException
@@ -2170,6 +2323,10 @@ private boolean isOperator(char c) {
private final void eval(TagLibTag tlt, Data data, Tag tag) throws TemplateException {
if (tlt.hasTTE()) {
+ // Snapshot attributes for AST before evaluators modify them
+ if (data.ast && tag instanceof TagBase) {
+ ((TagBase) tag).snapshotSourceAttributes();
+ }
try {
tlt.getEvaluator().execute(data.config, tag, tlt, data.flibs, data);
}
@@ -2537,15 +2694,32 @@ private final Attribute attribute(TagLibTag tlt, Data data, ArrayList ar
comments(data);
- // value
- boolean hasValue = data.srcCode.forwardIfCurrent('=') || (allowColonSeparator && data.srcCode.forwardIfCurrent(':'));
+ // value - track which separator was used
+ char separator = Attribute.SEPARATOR_EQUALS;
+ boolean hasValue = false;
+ if (data.srcCode.forwardIfCurrent('=')) {
+ hasValue = true;
+ separator = Attribute.SEPARATOR_EQUALS;
+ }
+ else if (allowColonSeparator && data.srcCode.forwardIfCurrent(':')) {
+ hasValue = true;
+ separator = Attribute.SEPARATOR_COLON;
+ }
+
if (hasValue) {
comments(data);
value = attributeValue(data, allowExpression);
}
else {
- value = defaultValue;
+ // Naked attribute (no value) - use TRUE for boolean-like contexts, or defaultValue for positional contexts
+ // When defaultValue is NULL, it indicates positional arguments (like property type/name) that shouldn't be boolean
+ if (defaultValue instanceof Null) {
+ value = defaultValue;
+ }
+ else {
+ value = data.factory.TRUE();
+ }
}
comments(data);
@@ -2555,7 +2729,7 @@ private final Attribute attribute(TagLibTag tlt, Data data, ArrayList ar
tlta = tlt.getAttribute(nameLC, true);
if (tlta != null && tlta.getName() != null) nameLC = tlta.getName();
}
- return new Attribute(dynamic.toBooleanValue(), name, tlta != null ? data.factory.toExpression(value, tlta.getType()) : value, sbType.toString(), !hasValue);
+ return new Attribute(dynamic.toBooleanValue(), name, tlta != null ? data.factory.toExpression(value, tlta.getType()) : value, sbType.toString(), !hasValue, separator);
}
private final String attributeName(SourceCode cfml, ArrayList args, TagLibTag tag, RefBoolean dynamic, StringBuilder sbType, boolean allowTwiceAttr, boolean allowColon)
diff --git a/core/src/main/java/lucee/transformer/cfml/script/CFMLScriptTransformer.java b/core/src/main/java/lucee/transformer/cfml/script/CFMLScriptTransformer.java
index 10855aa274e..bb2d4d05e92 100644
--- a/core/src/main/java/lucee/transformer/cfml/script/CFMLScriptTransformer.java
+++ b/core/src/main/java/lucee/transformer/cfml/script/CFMLScriptTransformer.java
@@ -18,11 +18,14 @@
*/
package lucee.transformer.cfml.script;
+import lucee.commons.io.res.util.ResourceUtil;
+import lucee.runtime.config.Constants;
import lucee.runtime.exp.TemplateException;
import lucee.transformer.Body;
import lucee.transformer.cfml.Data;
import lucee.transformer.cfml.tag.TagDependentBodyTransformer;
import lucee.transformer.expression.Expression;
+import lucee.transformer.util.PageSourceCode;
public final class CFMLScriptTransformer extends AbstrCFMLScriptTransformer implements TagDependentBodyTransformer {
@Override
@@ -31,6 +34,15 @@ public Body transform(Data data, String surroundingTagName) throws TemplateExcep
boolean isCFC = data.page != null && data.page.isComponent();
boolean isInterface = data.page != null && data.page.isInterface();
+ // If page isn't already marked as component but we're parsing a .cfc file inside cfscript,
+ // allow component parsing (handles component { } pattern)
+ if (!isCFC && !isInterface && data.srcCode instanceof PageSourceCode) {
+ String ext = ResourceUtil.getExtension( ((PageSourceCode) data.srcCode).getPageSource().getResource(), "" );
+ if (Constants.isCFMLComponentExtension( ext )) {
+ isCFC = true;
+ }
+ }
+
Data ed = init(data);
boolean oldAllowLowerThan = ed.allowLowerThan;
diff --git a/core/src/main/java/lucee/transformer/cfml/script/DocComment.java b/core/src/main/java/lucee/transformer/cfml/script/DocComment.java
index 58a571cfa68..7370f7ed8c7 100644
--- a/core/src/main/java/lucee/transformer/cfml/script/DocComment.java
+++ b/core/src/main/java/lucee/transformer/cfml/script/DocComment.java
@@ -30,9 +30,18 @@ public final class DocComment {
private StringBuilder tmpHint = new StringBuilder();
private String hint;
+ private String rawText; // raw docblock text for AST round-tripping
// private List params=new ArrayList();
Map params = new HashMap();
+ public void setRawText(String raw) {
+ this.rawText = raw;
+ }
+
+ public String getRawText() {
+ return rawText;
+ }
+
public void addHint(char c) {
tmpHint.append(c);
}
diff --git a/core/src/main/java/lucee/transformer/cfml/script/DocCommentTransformer.java b/core/src/main/java/lucee/transformer/cfml/script/DocCommentTransformer.java
index 970356bf891..42e67aeb29c 100644
--- a/core/src/main/java/lucee/transformer/cfml/script/DocCommentTransformer.java
+++ b/core/src/main/java/lucee/transformer/cfml/script/DocCommentTransformer.java
@@ -31,6 +31,7 @@ public final class DocCommentTransformer {
public DocComment transform(Factory f, String str) {
try {
DocComment dc = new DocComment();
+ dc.setRawText(str); // preserve raw docblock for AST round-tripping
str = str.trim();
if (str.startsWith("/**")) str = str.substring(3);
if (str.endsWith("*/")) str = str.substring(0, str.length() - 2);
diff --git a/core/src/main/java/lucee/transformer/cfml/tag/CFMLTransformer.java b/core/src/main/java/lucee/transformer/cfml/tag/CFMLTransformer.java
index 97e9ceee53b..16196b6d35a 100755
--- a/core/src/main/java/lucee/transformer/cfml/tag/CFMLTransformer.java
+++ b/core/src/main/java/lucee/transformer/cfml/tag/CFMLTransformer.java
@@ -46,7 +46,10 @@
import lucee.transformer.Factory;
import lucee.transformer.Page;
import lucee.transformer.Position;
+import lucee.transformer.bytecode.PageImpl;
import lucee.transformer.bytecode.statement.StatementBase;
+import lucee.transformer.bytecode.statement.tag.TagBase;
+import lucee.transformer.bytecode.statement.tag.TagComponent;
import lucee.transformer.bytecode.statement.tag.TagFunction;
import lucee.transformer.cfml.Data;
import lucee.transformer.cfml.ExprTransformer;
@@ -60,6 +63,7 @@
import lucee.transformer.cfml.script.AbstrCFMLScriptTransformer.ComponentTemplateException;
import lucee.transformer.cfml.script.CFMLScriptTransformer;
import lucee.transformer.expression.Expression;
+import lucee.transformer.expression.literal.LitString;
import lucee.transformer.library.function.FunctionLib;
import lucee.transformer.library.tag.CustomTagLib;
import lucee.transformer.library.tag.TagLib;
@@ -141,6 +145,11 @@ public CFMLTransformer(boolean codeIsland) {
*/
public Page transform(Factory factory, ConfigPro config, PageSource ps, TagLib[] tlibs, FunctionLib flibs, boolean returnValue, boolean ignoreScopes)
throws TemplateException, IOException {
+ return transform(factory, config, ps, tlibs, flibs, returnValue, ignoreScopes, false);
+ }
+
+ public Page transform(Factory factory, ConfigPro config, PageSource ps, TagLib[] tlibs, FunctionLib flibs, boolean returnValue, boolean ignoreScopes, boolean ast)
+ throws TemplateException, IOException {
Page p;
SourceCode sc;
@@ -155,7 +164,7 @@ public Page transform(Factory factory, ConfigPro config, PageSource ps, TagLib[]
boolean hasWriteLog = false;
boolean hasCharset = false;
boolean hasUpper = false;
- boolean allowUnknownTags = false;
+ boolean allowUnknownTags = ast;
while (true) {
PageSourceCode psc = null;
try {
@@ -185,6 +194,7 @@ else if (isCFMLCompExt) {
text = "<" + scriptTag.getFullName() + ">" + text + "\n" + scriptTag.getFullName() + ">";
int sourceOffset = ("<" + scriptTag.getFullName() + ">").length();
sc = new PageSourceCode(ps, text, charset, writeLog, sourceOffset);
+ sc.setWrappedInScript(true);
wrapped = true;
}
@@ -215,7 +225,8 @@ else if (isCFMLCompExt) {
boolean possibleUndetectedComponent = false;
// we don't have a component or interface
- if (!wrapped && p.isPage()) {
+ // Also check if component exists inside cfscript body (handles component { })
+ if (!wrapped && p.isPage() && !containsComponentRecursive(p)) {
possibleUndetectedComponent = isCFMLCompExt;
}
@@ -231,6 +242,7 @@ else if (isCFMLCompExt) {
String text = "<" + scriptTag.getFullName() + ">" + original.getText() + "\n" + scriptTag.getFullName() + ">";
int sourceOffset = ("<" + scriptTag.getFullName() + ">").length();
sc = new PageSourceCode(ps, text, charset, writeLog, sourceOffset);
+ sc.setWrappedInScript(true);
try {
while (true) {
@@ -238,6 +250,7 @@ else if (isCFMLCompExt) {
sc = new PageSourceCode(ps, charset, writeLog);
text = "<" + scriptTag.getFullName() + ">" + sc.getText() + "\n" + scriptTag.getFullName() + ">";
sc = new PageSourceCode(ps, text, charset, writeLog, sourceOffset);
+ sc.setWrappedInScript(true);
}
try {
_p = transform(factory, config, sc, tlibs, flibs, ps.getResource().lastModified(), dotUpper, returnValue, ignoreScopes, hasWriteLog, hasUpper, hasCharset,
@@ -265,10 +278,13 @@ else if (isCFMLCompExt) {
throw e.getTemplateException();
}
// we only use that result if it is a component now
- if (_p != null && !_p.isPage()) return _p;
+ // In AST mode, isPage() returns true even when component exists inside cfscript
+ // because the component isn't moved to root. Use recursive check instead.
+ if (_p != null && (!_p.isPage() || containsComponentRecursive(_p))) return _p;
}
- if (isCFMLCompExt && !p.isComponent() && !p.isInterface()) {
+ // In AST mode, component stays inside cfscript so skip this validation
+ if (!ast && isCFMLCompExt && !p.isComponent() && !p.isInterface()) {
String msg = "template [" + ps.getDisplayPath() + "] must contain a component or an interface.";
if (sc != null) throw new TemplateException(sc, msg);
throw new TemplateException(msg);
@@ -277,6 +293,23 @@ else if (isCFMLCompExt) {
return p;
}
+ /**
+ * Check if a body contains a component, recursively searching through tag bodies (e.g., cfscript)
+ */
+ private static boolean containsComponentRecursive(Body body) {
+ if (body == null) return false;
+ for (Statement s : body.getStatements()) {
+ if (s instanceof TagComponent) return true;
+ if (s instanceof Tag) {
+ Tag tag = (Tag) s;
+ if (tag.getBody() != null && containsComponentRecursive(tag.getBody())) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
public static TagLibTag getTLT(SourceCode cfml, String name, Identification id) throws TemplateException {
TagLib tl;
try {
@@ -338,6 +371,11 @@ public Page transform(Factory factory, ConfigPro config, SourceCode sc, TagLib[]
// sc.getWriteLog(),config.getSuppressWSBeforeArg(), config.getDefaultFunctionOutput(), returnValue,
// ignoreScope);
+ // allowUnknownTags is used as ast flag - mark the page for AST mode
+ if (allowUnknownTags && page instanceof PageImpl) {
+ ((PageImpl) page).setAST(true);
+ }
+
TransfomerSettings settings = new TransfomerSettings(dnuc, config.getHandleUnQuotedAttrValueAsString(), ignoreScope);
Data data = new Data(factory, config, page, sc, new EvaluatorPool(), settings, _tlibs, flibs, config.getCoreTagLib().getScriptTags(), false, hasWriteLog, hasUpper,
hasCharset, allowUnknownTags);
@@ -402,6 +440,8 @@ public void body(Data data, Body body) throws TemplateException {
// Comment
comment(data.srcCode, false);
+ // Check if we've reached the end after stripping comments (LDEV-6038)
+ if (!data.srcCode.isValidIndex()) break;
// Tag
// is Tag Beginning
if (data.srcCode.isCurrent('<')) {
@@ -706,6 +746,11 @@ private boolean tag(Data data, Body parent) throws TemplateException {
// get Attributes
attributes(data, tagLibTag, tag);
+ // Snapshot attributes for AST before evaluators modify them
+ if (data.ast && tag instanceof TagBase) {
+ ((TagBase) tag).snapshotSourceAttributes();
+ }
+
if (tagLibTag.hasAttributeEvaluator()) {
try {
tagLibTag = tagLibTag.getAttributeEvaluator().evaluate(tagLibTag, tag);
@@ -1087,7 +1132,19 @@ private static void attrNoName(Tag parent, TagLibTag tag, Data data, TagLibTagAt
pe = attr.getRtexpr();
}
// LitString.toExprString("",-1);
- Attribute att = new Attribute(false, strName, attributeValue(data, tag, strType, pe, true, data.factory.createNull()), strType);
+ Expression value = attributeValue(data, tag, strType, pe, true, data.factory.createNull());
+
+ // For string-typed attributes, if the value is the default null (nothing was parsed),
+ // don't add the attribute. This prevents cfbreak/cfcontinue from having a meaningless
+ // label attribute with null value. For "any" type (like cfreturn), null is meaningful.
+ if ("string".equalsIgnoreCase(strType) && value instanceof LitString) {
+ LitString litStr = (LitString) value;
+ if (litStr.getString() == null) {
+ return; // Skip adding attribute with null string value
+ }
+ }
+
+ Attribute att = new Attribute(false, strName, value, strType);
parent.addAttribute(att);
}
@@ -1233,7 +1290,10 @@ public static Expression attributeValue(Data data, TagLibTag tag, String type, b
Expression expr;
try {
ExprTransformer transfomer = null;
- if (parseExpression) {
+ // In AST mode, always use full expression parsing so interpolated expressions like
+ // cfloop condition="#expr#" show the parsed expression structure, not a StringLiteral.
+ // The rtexprvalue=false flag is only relevant for bytecode generation, not AST output.
+ if (parseExpression || data.ast) {
transfomer = tag.getTagLib().getExprTransfomer();
}
else {
diff --git a/core/src/main/java/lucee/transformer/expression/var/NamedArgument.java b/core/src/main/java/lucee/transformer/expression/var/NamedArgument.java
index 4ef021d7757..ed6f5cbf945 100644
--- a/core/src/main/java/lucee/transformer/expression/var/NamedArgument.java
+++ b/core/src/main/java/lucee/transformer/expression/var/NamedArgument.java
@@ -4,5 +4,14 @@
public interface NamedArgument extends Argument {
+ public static final char SEPARATOR_COLON = ':';
+ public static final char SEPARATOR_EQUALS = '=';
+
public Expression getName();
+
+ /**
+ * Returns the separator character used between key and value.
+ * Returns ':' for colon syntax (key: value) or '=' for equals syntax (key=value).
+ */
+ public char getSeparator();
}
diff --git a/core/src/main/java/lucee/transformer/interpreter/InterpreterFactory.java b/core/src/main/java/lucee/transformer/interpreter/InterpreterFactory.java
index 08695d0a720..c6916f7014d 100644
--- a/core/src/main/java/lucee/transformer/interpreter/InterpreterFactory.java
+++ b/core/src/main/java/lucee/transformer/interpreter/InterpreterFactory.java
@@ -258,7 +258,8 @@ public Expression toExpression(Expression expr, String type) {
@Override
public ExprString opString(Expression left, Expression right) {
- return OpString.toExprString(left, right, true);
+ // Pass false to preserve string literal structure for AST output (LDEV-6022)
+ return OpString.toExprString(left, right, false);
}
@Override
@@ -266,6 +267,11 @@ public ExprString opString(Expression left, Expression right, boolean concatStat
return OpString.toExprString(left, right, concatStatic);
}
+ @Override
+ public ExprString opStringInterpolation(Expression left, Expression right) {
+ return OpString.toExprStringInterpolation(left, right);
+ }
+
@Override
public ExprBoolean opBool(Expression left, Expression right, int operation) {
return OpBool.toExprBoolean(left, right, operation);
diff --git a/core/src/main/java/lucee/transformer/interpreter/op/OpString.java b/core/src/main/java/lucee/transformer/interpreter/op/OpString.java
index c73c6e70394..43fe916766d 100644
--- a/core/src/main/java/lucee/transformer/interpreter/op/OpString.java
+++ b/core/src/main/java/lucee/transformer/interpreter/op/OpString.java
@@ -29,6 +29,14 @@ public static ExprString toExprString(Expression left, Expression right, boolean
return new OpString(left, right);
}
+ /**
+ * For interpreter, interpolation is handled the same as regular concat.
+ * The distinction only matters for AST dump output in bytecode version.
+ */
+ public static ExprString toExprStringInterpolation(Expression left, Expression right) {
+ return toExprString(left, right, false);
+ }
+
@Override
public Class> _writeOut(InterpreterContext ic, int mode) throws PageException {
ic.stack(ic.getValueAsString(left).concat(ic.getValueAsString(right)));
diff --git a/core/src/main/java/lucee/transformer/statement/Argument.java b/core/src/main/java/lucee/transformer/statement/Argument.java
index 173013f3f66..f8adff9ece4 100755
--- a/core/src/main/java/lucee/transformer/statement/Argument.java
+++ b/core/src/main/java/lucee/transformer/statement/Argument.java
@@ -32,6 +32,7 @@ public final class Argument {
private ExprString name;
private ExprString type;
+ private boolean typeExplicit; // LDEV-6041: track if type was explicitly specified
private ExprBoolean required;
private Expression defaultValue;
private ExprString displayName;
@@ -124,6 +125,20 @@ public ExprString getType() {
return type;
}
+ /**
+ * LDEV-6041: Check if type was explicitly specified
+ */
+ public boolean isTypeExplicit() {
+ return typeExplicit;
+ }
+
+ /**
+ * LDEV-6041: Set whether type was explicitly specified
+ */
+ public void setTypeExplicit(boolean explicit) {
+ this.typeExplicit = explicit;
+ }
+
public Map getMetaData() {
return meta;
}
diff --git a/core/src/main/java/lucee/transformer/statement/tag/Attribute.java b/core/src/main/java/lucee/transformer/statement/tag/Attribute.java
index b1b5d045466..17f701add26 100755
--- a/core/src/main/java/lucee/transformer/statement/tag/Attribute.java
+++ b/core/src/main/java/lucee/transformer/statement/tag/Attribute.java
@@ -22,6 +22,9 @@
public final class Attribute {
+ public static final char SEPARATOR_EQUALS = '=';
+ public static final char SEPARATOR_COLON = ':';
+
final String nameOC;
final String nameLC;
final Expression value;
@@ -30,6 +33,7 @@ public final class Attribute {
private boolean defaultAttribute;
private String setterName;
private final boolean isDefaultValue;
+ private char separator = SEPARATOR_EQUALS; // default to '=' for backwards compatibility
public Attribute(boolean dynamicType, String name, Expression value, String type) {
this(dynamicType, name, value, type, false);
@@ -44,6 +48,19 @@ public Attribute(boolean dynamicType, String name, Expression value, String type
this.isDefaultValue = isDefaultValue;
}
+ public Attribute(boolean dynamicType, String name, Expression value, String type, boolean isDefaultValue, char separator) {
+ this(dynamicType, name, value, type, isDefaultValue);
+ this.separator = separator;
+ }
+
+ public char getSeparator() {
+ return separator;
+ }
+
+ public void setSeparator(char separator) {
+ this.separator = separator;
+ }
+
public boolean isDefaultValue() {
return isDefaultValue;
}
diff --git a/core/src/main/java/lucee/transformer/util/SourceCode.java b/core/src/main/java/lucee/transformer/util/SourceCode.java
index cd44d850c9e..5739f7927a8 100755
--- a/core/src/main/java/lucee/transformer/util/SourceCode.java
+++ b/core/src/main/java/lucee/transformer/util/SourceCode.java
@@ -40,6 +40,9 @@ public class SourceCode {
private int hash;
private SourceCode parent;
private int sourceOffset;
+ // Set to true when the parser wraps script content in tags for parsing.
+ // Used by AST generation to know it should unwrap the cfscript in the output.
+ private boolean wrappedInScript;
public SourceCode(SourceCode parent, String strText, boolean writeLog) {
this(parent, strText, writeLog, 0);
@@ -719,8 +722,10 @@ public SourceCode subCFMLString(int start) {
* @return subset of the SourceCode as new SourcCode
*/
public SourceCode subCFMLString(int start, int count) {
- return new SourceCode(this, String.valueOf(text, start, count), writeLog);
-
+ SourceCode sub = new SourceCode(this, String.valueOf(text, start, count), writeLog);
+ // Preserve wrappedInScript flag from parent so AST generation knows the original source type
+ sub.setWrappedInScript(this.wrappedInScript);
+ return sub;
}
/**
@@ -959,4 +964,12 @@ public int hashCode() {
public int getSourceOffset() {
return sourceOffset;
}
+
+ public boolean isWrappedInScript() {
+ return wrappedInScript;
+ }
+
+ public void setWrappedInScript(boolean wrappedInScript) {
+ this.wrappedInScript = wrappedInScript;
+ }
}
\ No newline at end of file
diff --git a/test/functions/astFromString.cfc b/test/functions/astFromString.cfc
index 0c1e1387cc6..b62e03b6b9f 100644
--- a/test/functions/astFromString.cfc
+++ b/test/functions/astFromString.cfc
@@ -21,59 +21,51 @@ component extends = "org.lucee.cfml.test.LuceeTestCase" {
it( title = 'echo literal string', body = function( currentSpec ) {
var result = astFromString("Susi");
assertEquals(
- '{"start":{"line":1,"column":0,"offset":0},"end":{"line":1,"column":4,"offset":4},"type":"Program","body":[{"start":{"line":1,"column":0,"offset":0},"end":{"line":1,"column":4,"offset":4},"type":"ExpressionStatement","expression":{"start":{"line":1,"column":0,"offset":0},"end":{"line":1,"column":4,"offset":4},"type":"StringLiteral","value":"Susi","raw":"\"Susi\""}}]}',
+ '{"start":{"line":1,"column":0,"offset":0},"end":{"line":1,"column":4,"offset":4},"type":"Program","body":[{"start":{"line":1,"column":0,"offset":0},"end":{"line":1,"column":4,"offset":4},"type":"ExpressionStatement","expression":{"start":{"line":1,"column":0,"offset":0},"end":{"line":1,"column":4,"offset":4},"type":"StringLiteral","value":"Susi","raw":"\"Susi\""}}],"sourceType":"tag","dotNotationUpperCase":true,"handleUnquotedAttrValueAsString":true}',
serializeJSON(var:result,compact:true)
);
});
it( title = 'test loop tag', body = function( currentSpec ) {
var result = astFromString('');
- assertEquals(
- '{"start":{"line":1,"column":0,"offset":0},"end":{"line":1,"column":44,"offset":44},"type":"Program","body":[{"start":{"line":1,"column":0,"offset":0},"end":{"line":1,"column":44,"offset":44},"type":"CFMLTag","isBuiltIn":true,"name":"loop","nameSpace":"cf","nameSpaceSeparator":"","fullname":"cfloop","attributes":[{"name":"from","type":"Attribute","value":{"start":{"line":1,"column":13,"offset":13},"end":{"line":1,"column":16,"offset":16},"type":"NumberLiteral","raw":"1","value":1}},{"name":"to","type":"Attribute","value":{"start":{"line":1,"column":20,"offset":20},"end":{"line":1,"column":24,"offset":24},"type":"NumberLiteral","raw":"10","value":10}},{"name":"index","type":"Attribute","value":{"start":{"line":1,"column":31,"offset":31},"end":{"line":1,"column":34,"offset":34},"type":"StringLiteral","value":"i","raw":"\"i\""}}],"body":{"type":"BlockStatement","body":[]}}]}',
- serializeJSON(var:result,compact:true)
- );
+ var expected = deserializeJSON( '{"start":{"line":1,"column":0,"offset":0},"end":{"line":1,"column":44,"offset":44},"type":"Program","body":[{"start":{"line":1,"column":0,"offset":0},"end":{"line":1,"column":44,"offset":44},"type":"CFMLTag","isBuiltIn":true,"name":"loop","nameSpace":"cf","nameSpaceSeparator":"","fullname":"cfloop","attributes":[{"name":"from","type":"Attribute","value":{"start":{"line":1,"column":13,"offset":13},"end":{"line":1,"column":16,"offset":16},"type":"NumberLiteral","raw":"1","value":1}},{"name":"to","type":"Attribute","value":{"start":{"line":1,"column":20,"offset":20},"end":{"line":1,"column":24,"offset":24},"type":"NumberLiteral","raw":"10","value":10}},{"name":"index","type":"Attribute","value":{"start":{"line":1,"column":31,"offset":31},"end":{"line":1,"column":34,"offset":34},"type":"StringLiteral","value":"i","raw":"\"i\"","quoteChar":"\""}}],"body":{"type":"BlockStatement","body":[]}}],"sourceType":"tag","dotNotationUpperCase":true,"handleUnquotedAttrValueAsString":true}' );
+ assertASTMatches( expected, result );
});
it( title = 'test variable assignment with single data member', body = function( currentSpec ) {
var result = astFromString('a.b.c=d;');
assertEquals(
- '{"start":{"line":1,"column":0,"offset":0},"end":{"line":1,"column":29,"offset":29},"type":"Program","body":[{"start":{"line":1,"column":0,"offset":0},"end":{"line":1,"column":29,"offset":29},"type":"CFMLTag","isBuiltIn":true,"name":"script","nameSpace":"cf","nameSpaceSeparator":"","fullname":"cfscript","attributes":[],"body":{"type":"BlockStatement","body":[{"start":{"line":1,"column":10,"offset":10},"end":{"line":1,"column":17,"offset":17},"type":"AssignmentExpression","operator":"ASSIGN","left":{"start":{"line":1,"column":10,"offset":10},"end":{"line":1,"column":11,"offset":11},"type":"MemberExpression","computed":false,"object":{"type":"MemberExpression","computed":false,"object":{"type":"Identifier","name":"A"},"property":{"type":"Identifier","name":"B"}},"property":{"type":"Identifier","name":"C"}},"right":{"start":{"line":1,"column":16,"offset":16},"end":{"line":1,"column":17,"offset":17},"type":"Identifier","name":"D"}}]}}]}',
+ '{"start":{"line":1,"column":0,"offset":0},"end":{"line":1,"column":29,"offset":29},"type":"Program","body":[{"start":{"line":1,"column":0,"offset":0},"end":{"line":1,"column":29,"offset":29},"type":"CFMLTag","isBuiltIn":true,"name":"script","nameSpace":"cf","nameSpaceSeparator":"","fullname":"cfscript","attributes":[],"body":{"type":"BlockStatement","body":[{"start":{"line":1,"column":10,"offset":10},"end":{"line":1,"column":17,"offset":17},"type":"AssignmentExpression","operator":"ASSIGN","left":{"start":{"line":1,"column":10,"offset":10},"end":{"line":1,"column":11,"offset":11},"type":"MemberExpression","computed":false,"object":{"type":"MemberExpression","computed":false,"object":{"type":"Identifier","name":"A"},"property":{"type":"Identifier","name":"B"}},"property":{"type":"Identifier","name":"C"}},"right":{"start":{"line":1,"column":16,"offset":16},"end":{"line":1,"column":17,"offset":17},"type":"Identifier","name":"D"}}]}}],"sourceType":"tag","dotNotationUpperCase":true,"handleUnquotedAttrValueAsString":true}',
serializeJSON(var:result,compact:true)
);
});
it( title = 'test variable assignment with 2 data member', body = function( currentSpec ) {
var result = astFromString('a.b.c=d.e;');
assertEquals(
- '{"start":{"line":1,"column":0,"offset":0},"end":{"line":1,"column":31,"offset":31},"type":"Program","body":[{"start":{"line":1,"column":0,"offset":0},"end":{"line":1,"column":31,"offset":31},"type":"CFMLTag","isBuiltIn":true,"name":"script","nameSpace":"cf","nameSpaceSeparator":"","fullname":"cfscript","attributes":[],"body":{"type":"BlockStatement","body":[{"start":{"line":1,"column":10,"offset":10},"end":{"line":1,"column":19,"offset":19},"type":"AssignmentExpression","operator":"ASSIGN","left":{"start":{"line":1,"column":10,"offset":10},"end":{"line":1,"column":11,"offset":11},"type":"MemberExpression","computed":false,"object":{"type":"MemberExpression","computed":false,"object":{"type":"Identifier","name":"A"},"property":{"type":"Identifier","name":"B"}},"property":{"type":"Identifier","name":"C"}},"right":{"start":{"line":1,"column":16,"offset":16},"end":{"line":1,"column":17,"offset":17},"type":"MemberExpression","computed":false,"object":{"type":"Identifier","name":"D"},"property":{"type":"Identifier","name":"E"}}}]}}]}',
+ '{"start":{"line":1,"column":0,"offset":0},"end":{"line":1,"column":31,"offset":31},"type":"Program","body":[{"start":{"line":1,"column":0,"offset":0},"end":{"line":1,"column":31,"offset":31},"type":"CFMLTag","isBuiltIn":true,"name":"script","nameSpace":"cf","nameSpaceSeparator":"","fullname":"cfscript","attributes":[],"body":{"type":"BlockStatement","body":[{"start":{"line":1,"column":10,"offset":10},"end":{"line":1,"column":19,"offset":19},"type":"AssignmentExpression","operator":"ASSIGN","left":{"start":{"line":1,"column":10,"offset":10},"end":{"line":1,"column":11,"offset":11},"type":"MemberExpression","computed":false,"object":{"type":"MemberExpression","computed":false,"object":{"type":"Identifier","name":"A"},"property":{"type":"Identifier","name":"B"}},"property":{"type":"Identifier","name":"C"}},"right":{"start":{"line":1,"column":16,"offset":16},"end":{"line":1,"column":17,"offset":17},"type":"MemberExpression","computed":false,"object":{"type":"Identifier","name":"D"},"property":{"type":"Identifier","name":"E"}}}]}}],"sourceType":"tag","dotNotationUpperCase":true,"handleUnquotedAttrValueAsString":true}',
serializeJSON(var:result,compact:true)
);
});
it( title = 'test variable assignment with function call', body = function( currentSpec ) {
var result = astFromString('a.b.c=d(1,true,"");');
- assertEquals(
- '{"start":{"line":1,"column":0,"offset":0},"end":{"line":1,"column":40,"offset":40},"type":"Program","body":[{"start":{"line":1,"column":0,"offset":0},"end":{"line":1,"column":40,"offset":40},"type":"CFMLTag","isBuiltIn":true,"name":"script","nameSpace":"cf","nameSpaceSeparator":"","fullname":"cfscript","attributes":[],"body":{"type":"BlockStatement","body":[{"start":{"line":1,"column":10,"offset":10},"end":{"line":1,"column":28,"offset":28},"type":"AssignmentExpression","operator":"ASSIGN","left":{"start":{"line":1,"column":10,"offset":10},"end":{"line":1,"column":11,"offset":11},"type":"MemberExpression","computed":false,"object":{"type":"MemberExpression","computed":false,"object":{"type":"Identifier","name":"A"},"property":{"type":"Identifier","name":"B"}},"property":{"type":"Identifier","name":"C"}},"right":{"start":{"line":1,"column":16,"offset":16},"end":{"line":1,"column":28,"offset":28},"type":"CallExpression","callee":{"type":"Identifier","name":"D"},"arguments":[{"start":{"line":1,"column":18,"offset":18},"end":{"line":1,"column":19,"offset":19},"type":"NumberLiteral","raw":"1","value":1},{"start":{"line":1,"column":20,"offset":20},"end":{"line":1,"column":24,"offset":24},"type":"BooleanLiteral","value":true},{"start":{"line":1,"column":25,"offset":25},"end":{"line":1,"column":27,"offset":27},"type":"StringLiteral","value":"","raw":"\"\""}]}}]}}]}',
- serializeJSON(var:result,compact:true)
- );
+ var expected = deserializeJSON( '{"start":{"line":1,"column":0,"offset":0},"end":{"line":1,"column":40,"offset":40},"type":"Program","body":[{"start":{"line":1,"column":0,"offset":0},"end":{"line":1,"column":40,"offset":40},"type":"CFMLTag","isBuiltIn":true,"name":"script","nameSpace":"cf","nameSpaceSeparator":"","fullname":"cfscript","attributes":[],"body":{"type":"BlockStatement","body":[{"start":{"line":1,"column":10,"offset":10},"end":{"line":1,"column":28,"offset":28},"type":"AssignmentExpression","operator":"ASSIGN","left":{"start":{"line":1,"column":10,"offset":10},"end":{"line":1,"column":11,"offset":11},"type":"MemberExpression","computed":false,"object":{"type":"MemberExpression","computed":false,"object":{"type":"Identifier","name":"A"},"property":{"type":"Identifier","name":"B"}},"property":{"type":"Identifier","name":"C"}},"right":{"start":{"line":1,"column":16,"offset":16},"end":{"line":1,"column":28,"offset":28},"type":"CallExpression","callee":{"type":"Identifier","name":"D"},"arguments":[{"start":{"line":1,"column":18,"offset":18},"end":{"line":1,"column":19,"offset":19},"type":"NumberLiteral","raw":"1","value":1},{"start":{"line":1,"column":20,"offset":20},"end":{"line":1,"column":24,"offset":24},"type":"BooleanLiteral","value":true},{"start":{"line":1,"column":25,"offset":25},"end":{"line":1,"column":27,"offset":27},"type":"StringLiteral","value":"","raw":"\"\"","quoteChar":"\""}]}}]}}],"sourceType":"tag","dotNotationUpperCase":true,"handleUnquotedAttrValueAsString":true}' );
+ assertASTMatches( expected, result );
});
it( title = 'test variable assignment with scopes', body = function( currentSpec ) {
var result = astFromString('variables.a=url.a;');
assertEquals(
- '{"start":{"line":1,"column":0,"offset":0},"end":{"line":1,"column":39,"offset":39},"type":"Program","body":[{"start":{"line":1,"column":0,"offset":0},"end":{"line":1,"column":39,"offset":39},"type":"CFMLTag","isBuiltIn":true,"name":"script","nameSpace":"cf","nameSpaceSeparator":"","fullname":"cfscript","attributes":[],"body":{"type":"BlockStatement","body":[{"start":{"line":1,"column":10,"offset":10},"end":{"line":1,"column":27,"offset":27},"type":"AssignmentExpression","operator":"ASSIGN","left":{"start":{"line":1,"column":10,"offset":10},"end":{"line":1,"column":19,"offset":19},"type":"MemberExpression","computed":false,"object":{"type":"Identifier","name":"VARIABLES"},"property":{"type":"Identifier","name":"A"}},"right":{"start":{"line":1,"column":22,"offset":22},"end":{"line":1,"column":25,"offset":25},"type":"MemberExpression","computed":false,"object":{"type":"Identifier","name":"URL"},"property":{"type":"Identifier","name":"A"}}}]}}]}',
+ '{"start":{"line":1,"column":0,"offset":0},"end":{"line":1,"column":39,"offset":39},"type":"Program","body":[{"start":{"line":1,"column":0,"offset":0},"end":{"line":1,"column":39,"offset":39},"type":"CFMLTag","isBuiltIn":true,"name":"script","nameSpace":"cf","nameSpaceSeparator":"","fullname":"cfscript","attributes":[],"body":{"type":"BlockStatement","body":[{"start":{"line":1,"column":10,"offset":10},"end":{"line":1,"column":27,"offset":27},"type":"AssignmentExpression","operator":"ASSIGN","left":{"start":{"line":1,"column":10,"offset":10},"end":{"line":1,"column":19,"offset":19},"type":"MemberExpression","computed":false,"object":{"type":"Identifier","name":"VARIABLES"},"property":{"type":"Identifier","name":"A"}},"right":{"start":{"line":1,"column":22,"offset":22},"end":{"line":1,"column":25,"offset":25},"type":"MemberExpression","computed":false,"object":{"type":"Identifier","name":"URL"},"property":{"type":"Identifier","name":"A"}}}]}}],"sourceType":"tag","dotNotationUpperCase":true,"handleUnquotedAttrValueAsString":true}',
serializeJSON(var:result,compact:true)
);
});
it( title = 'test positional arguments', body = function( currentSpec ) {
var result = astFromString('whatever(1,true,"abc");');
- assertEquals(
- '{"start":{"line":1,"column":0,"offset":0},"end":{"line":1,"column":44,"offset":44},"type":"Program","body":[{"start":{"line":1,"column":0,"offset":0},"end":{"line":1,"column":44,"offset":44},"type":"CFMLTag","isBuiltIn":true,"name":"script","nameSpace":"cf","nameSpaceSeparator":"","fullname":"cfscript","attributes":[],"body":{"type":"BlockStatement","body":[{"start":{"line":1,"column":10,"offset":10},"end":{"line":1,"column":32,"offset":32},"type":"CallExpression","callee":{"type":"Identifier","name":"WHATEVER"},"arguments":[{"start":{"line":1,"column":19,"offset":19},"end":{"line":1,"column":20,"offset":20},"type":"NumberLiteral","raw":"1","value":1},{"start":{"line":1,"column":21,"offset":21},"end":{"line":1,"column":25,"offset":25},"type":"BooleanLiteral","value":true},{"start":{"line":1,"column":26,"offset":26},"end":{"line":1,"column":31,"offset":31},"type":"StringLiteral","value":"abc","raw":"\"abc\""}]}]}}]}',
- serializeJSON(var:result,compact:true)
- );
+ var expected = deserializeJSON( '{"start":{"line":1,"column":0,"offset":0},"end":{"line":1,"column":44,"offset":44},"type":"Program","body":[{"start":{"line":1,"column":0,"offset":0},"end":{"line":1,"column":44,"offset":44},"type":"CFMLTag","isBuiltIn":true,"name":"script","nameSpace":"cf","nameSpaceSeparator":"","fullname":"cfscript","attributes":[],"body":{"type":"BlockStatement","body":[{"start":{"line":1,"column":10,"offset":10},"end":{"line":1,"column":32,"offset":32},"type":"CallExpression","callee":{"type":"Identifier","name":"WHATEVER"},"arguments":[{"start":{"line":1,"column":19,"offset":19},"end":{"line":1,"column":20,"offset":20},"type":"NumberLiteral","raw":"1","value":1},{"start":{"line":1,"column":21,"offset":21},"end":{"line":1,"column":25,"offset":25},"type":"BooleanLiteral","value":true},{"start":{"line":1,"column":26,"offset":26},"end":{"line":1,"column":31,"offset":31},"type":"StringLiteral","value":"abc","raw":"\"abc\"","quoteChar":"\""}]}]}}],"sourceType":"tag","dotNotationUpperCase":true,"handleUnquotedAttrValueAsString":true}' );
+ assertASTMatches( expected, result );
});
it( title = 'test named arguments', body = function( currentSpec ) {
var result = astFromString('whatever(arg1=1, arg2=true, arg3="abc");');
- assertEquals(
- '{"start":{"line":1,"column":0,"offset":0},"end":{"line":1,"column":61,"offset":61},"type":"Program","body":[{"start":{"line":1,"column":0,"offset":0},"end":{"line":1,"column":61,"offset":61},"type":"CFMLTag","isBuiltIn":true,"name":"script","nameSpace":"cf","nameSpaceSeparator":"","fullname":"cfscript","attributes":[],"body":{"type":"BlockStatement","body":[{"start":{"line":1,"column":10,"offset":10},"end":{"line":1,"column":49,"offset":49},"type":"CallExpression","callee":{"type":"Identifier","name":"WHATEVER"},"arguments":[{"type":"NamedArgument","name":{"start":{"line":1,"column":19,"offset":19},"end":{"line":1,"column":23,"offset":23},"type":"Identifier","name":"ARG1"},"value":{"start":{"line":1,"column":24,"offset":24},"end":{"line":1,"column":25,"offset":25},"type":"NumberLiteral","raw":"1","value":1}},{"type":"NamedArgument","name":{"start":{"line":1,"column":27,"offset":27},"end":{"line":1,"column":31,"offset":31},"type":"Identifier","name":"ARG2"},"value":{"start":{"line":1,"column":32,"offset":32},"end":{"line":1,"column":36,"offset":36},"type":"BooleanLiteral","value":true}},{"type":"NamedArgument","name":{"start":{"line":1,"column":38,"offset":38},"end":{"line":1,"column":42,"offset":42},"type":"Identifier","name":"ARG3"},"value":{"start":{"line":1,"column":43,"offset":43},"end":{"line":1,"column":48,"offset":48},"type":"StringLiteral","value":"abc","raw":"\"abc\""}}]}]}}]}',
- serializeJSON(var:result,compact:true)
- );
+ var expected = deserializeJSON( '{"start":{"line":1,"column":0,"offset":0},"end":{"line":1,"column":61,"offset":61},"type":"Program","body":[{"start":{"line":1,"column":0,"offset":0},"end":{"line":1,"column":61,"offset":61},"type":"CFMLTag","isBuiltIn":true,"name":"script","nameSpace":"cf","nameSpaceSeparator":"","fullname":"cfscript","attributes":[],"body":{"type":"BlockStatement","body":[{"start":{"line":1,"column":10,"offset":10},"end":{"line":1,"column":49,"offset":49},"type":"CallExpression","callee":{"type":"Identifier","name":"WHATEVER"},"arguments":[{"type":"NamedArgument","name":{"start":{"line":1,"column":19,"offset":19},"end":{"line":1,"column":23,"offset":23},"type":"Identifier","name":"ARG1"},"value":{"start":{"line":1,"column":24,"offset":24},"end":{"line":1,"column":25,"offset":25},"type":"NumberLiteral","raw":"1","value":1}},{"type":"NamedArgument","name":{"start":{"line":1,"column":27,"offset":27},"end":{"line":1,"column":31,"offset":31},"type":"Identifier","name":"ARG2"},"value":{"start":{"line":1,"column":32,"offset":32},"end":{"line":1,"column":36,"offset":36},"type":"BooleanLiteral","value":true}},{"type":"NamedArgument","name":{"start":{"line":1,"column":38,"offset":38},"end":{"line":1,"column":42,"offset":42},"type":"Identifier","name":"ARG3"},"value":{"start":{"line":1,"column":43,"offset":43},"end":{"line":1,"column":48,"offset":48},"type":"StringLiteral","value":"abc","raw":"\"abc\"","quoteChar":"\""}}]}]}}],"sourceType":"tag","dotNotationUpperCase":true,"handleUnquotedAttrValueAsString":true}' );
+ assertASTMatches( expected, result );
});
it( title = 'test unknown tag self-closing without slash', body = function( currentSpec ) {
@@ -146,6 +138,24 @@ component extends = "org.lucee.cfml.test.LuceeTestCase" {
assertEquals("loop", result.body[1].name);
});
+ it( title = 'test mode=tag works explicitly', body = function( currentSpec ) {
+ var result = astFromString("", "tag");
+ assertEquals("Program", result.type);
+ assertTrue(arrayLen(result.body) > 0);
+ });
+
+ it( title = 'test invalid mode throws error', body = function( currentSpec ) {
+ var errorThrown = false;
+ try {
+ astFromString("x = 1;", "banana");
+ }
+ catch( any e ) {
+ errorThrown = true;
+ assertTrue( e.message contains "mode" || e.detail contains "mode", "Error should mention 'mode'" );
+ }
+ assertTrue( errorThrown, "Expected an error for invalid mode 'banana'" );
+ });
+
xit( title = 'test unknown script tag in legacy style', body = function( currentSpec ) {
var result = astFromString('unknown susi="1" {echo("ddd");}',"script");
assertEquals("Program", result.type);
@@ -173,4 +183,60 @@ component extends = "org.lucee.cfml.test.LuceeTestCase" {
});
}
+
+ /**
+ * Compare two AST structures recursively.
+ * Returns empty string if match, otherwise returns path to first difference.
+ */
+ private string function compareAST( required any expected, required any actual, string path = "" ) {
+ // Handle nulls
+ if ( isNull( expected ) && isNull( actual ) ) return "";
+ if ( isNull( expected ) || isNull( actual ) ) return path & " (null mismatch)";
+
+ // Simple types
+ if ( isSimpleValue( expected ) ) {
+ if ( !isSimpleValue( actual ) ) return path & " (type mismatch: expected simple, got complex)";
+ if ( expected != actual ) return path & " (value mismatch: expected [#expected#], got [#actual#])";
+ return "";
+ }
+
+ // Arrays
+ if ( isArray( expected ) ) {
+ if ( !isArray( actual ) ) return path & " (type mismatch: expected array, got #getMetaData( actual ).getName()#)";
+ if ( arrayLen( expected ) != arrayLen( actual ) ) return path & " (array length mismatch: expected #arrayLen( expected )#, got #arrayLen( actual )#)";
+ for ( var i = 1; i <= arrayLen( expected ); i++ ) {
+ var result = compareAST( expected[ i ], actual[ i ], path & "[#i#]" );
+ if ( len( result ) ) return result;
+ }
+ return "";
+ }
+
+ // Structs - check both directions for mismatches
+ if ( isStruct( expected ) ) {
+ if ( !isStruct( actual ) ) return path & " (type mismatch: expected struct, got #getMetaData( actual ).getName()#)";
+ // Check for missing keys in actual
+ for ( var key in expected ) {
+ if ( !structKeyExists( actual, key ) ) return path & ".#key# (missing key in actual)";
+ var result = compareAST( expected[ key ], actual[ key ], path & ".#key#" );
+ if ( len( result ) ) return result;
+ }
+ // Check for extra keys in actual
+ for ( var key in actual ) {
+ if ( !structKeyExists( expected, key ) ) return path & ".#key# (extra key in actual)";
+ }
+ return "";
+ }
+
+ return path & " (unknown type)";
+ }
+
+ /**
+ * Assert AST matches expected structure exactly.
+ */
+ private void function assertASTMatches( required any expected, required any actual, string message = "AST mismatch" ) {
+ var diff = compareAST( expected, actual );
+ if ( len( diff ) ) {
+ fail( message & ": " & diff );
+ }
+ }
}
\ No newline at end of file
diff --git a/test/tickets/LDEV5969.cfc b/test/tickets/LDEV5969.cfc
new file mode 100644
index 00000000000..8e34538c71c
--- /dev/null
+++ b/test/tickets/LDEV5969.cfc
@@ -0,0 +1,151 @@
+component extends="org.lucee.cfml.test.LuceeTestCase" labels="ast" {
+
+ variables.testDir = getDirectoryFromPath( getCurrentTemplatePath() ) & "LDEV5969/";
+
+ function run( testResults, testBox ) {
+
+ describe( "LDEV-5969: cffunction/cfargument attributes missing from AST", function() {
+
+ it( "cffunction attributes should be captured in AST", function() {
+ var code = fileRead( variables.testDir & "simple.cfm" );
+ var ast = astFromString( code, "tag" );
+
+ // Find the cffunction tag in the AST
+ var funcTag = findTagByName( ast, "function" );
+ expect( funcTag ).notToBeNull( "cffunction tag should be present in AST" );
+
+ // Verify attributes are present
+ var attrs = funcTag.attributes;
+ expect( attrs ).toBeArray();
+ expect( arrayLen( attrs ) ).toBeGTE( 4, "Should have at least 4 attributes (name, access, returntype, output)" );
+
+ // Check specific attributes exist
+ var attrNames = attrs.map( function( a ) { return a.name; } );
+ expect( attrNames ).toInclude( "name" );
+ expect( attrNames ).toInclude( "access" );
+ expect( attrNames ).toInclude( "returntype" );
+ expect( attrNames ).toInclude( "output" );
+ });
+
+ it( "cfargument attributes should be captured in AST", function() {
+ var code = fileRead( variables.testDir & "withArgument.cfm" );
+ var ast = astFromString( code, "tag" );
+
+ // Find the cfargument tag in the AST
+ var argTag = findTagByName( ast, "argument" );
+ expect( argTag ).notToBeNull( "cfargument tag should be present in AST" );
+
+ // Verify attributes are present
+ var attrs = argTag.attributes;
+ expect( attrs ).toBeArray();
+ expect( arrayLen( attrs ) ).toBeGTE( 4, "Should have at least 4 attributes (name, type, required, default)" );
+
+ // Check specific attributes exist
+ var attrNames = attrs.map( function( a ) { return a.name; } );
+ expect( attrNames ).toInclude( "name" );
+ expect( attrNames ).toInclude( "type" );
+ expect( attrNames ).toInclude( "required" );
+ expect( attrNames ).toInclude( "default" );
+ });
+
+ it( "cffunction with multiple cfarguments should capture all argument attributes", function() {
+ var code = fileRead( variables.testDir & "multipleArgs.cfm" );
+ var ast = astFromString( code, "tag" );
+
+ // Find all cfargument tags
+ var argTags = findAllTagsByName( ast, "argument" );
+ expect( arrayLen( argTags ) ).toBe( 2, "Should have 2 cfargument tags" );
+
+ // First argument should have name="firstName"
+ var firstArg = argTags[ 1 ];
+ var firstArgNames = firstArg.attributes.map( function( a ) { return a.name; } );
+ expect( firstArgNames ).toInclude( "name" );
+
+ // Second argument should have default attribute
+ var secondArg = argTags[ 2 ];
+ var secondArgNames = secondArg.attributes.map( function( a ) { return a.name; } );
+ expect( secondArgNames ).toInclude( "default" );
+ });
+
+ it( "custom/metadata attributes should be captured on cffunction and cfargument", function() {
+ var code = fileRead( variables.testDir & "customAttrs.cfm" );
+ var ast = astFromString( code, "tag" );
+
+ // Check cffunction custom attributes
+ var funcTag = findTagByName( ast, "function" );
+ expect( funcTag ).notToBeNull();
+ var funcAttrNames = funcTag.attributes.map( function( a ) { return a.name; } );
+ expect( funcAttrNames ).toInclude( "customattr", "Custom attribute should be preserved on cffunction" );
+ expect( funcAttrNames ).toInclude( "anotherattr", "Another custom attribute should be preserved on cffunction" );
+
+ // Check cfargument custom attributes
+ var argTag = findTagByName( ast, "argument" );
+ expect( argTag ).notToBeNull();
+ var argAttrNames = argTag.attributes.map( function( a ) { return a.name; } );
+ expect( argAttrNames ).toInclude( "mymeta", "Custom attribute should be preserved on cfargument" );
+ expect( argAttrNames ).toInclude( "extrainfo", "Another custom attribute should be preserved on cfargument" );
+ });
+
+ });
+ }
+
+ /**
+ * Recursively find a tag by name in the AST
+ */
+ private function findTagByName( required struct node, required string tagName ) {
+ if ( ( node.type ?: "" ) == "CFMLTag" && ( node.name ?: "" ) == arguments.tagName ) {
+ return node;
+ }
+ if ( structKeyExists( node, "body" ) ) {
+ if ( isArray( node.body ) ) {
+ for ( var child in node.body ) {
+ if ( isStruct( child ) ) {
+ var result = findTagByName( child, arguments.tagName );
+ if ( !isNull( result ) ) return result;
+ }
+ }
+ } else if ( isStruct( node.body ) ) {
+ var result = findTagByName( node.body, arguments.tagName );
+ if ( !isNull( result ) ) return result;
+ }
+ }
+ if ( structKeyExists( node, "children" ) && isArray( node.children ) ) {
+ for ( var child in node.children ) {
+ if ( isStruct( child ) ) {
+ var result = findTagByName( child, arguments.tagName );
+ if ( !isNull( result ) ) return result;
+ }
+ }
+ }
+ return;
+ }
+
+ /**
+ * Recursively find all tags by name in the AST
+ */
+ private function findAllTagsByName( required struct node, required string tagName, array results = [] ) {
+ if ( ( node.type ?: "" ) == "CFMLTag" && ( node.name ?: "" ) == arguments.tagName ) {
+ arrayAppend( results, node );
+ }
+ if ( structKeyExists( node, "body" ) ) {
+ if ( isArray( node.body ) ) {
+ for ( var child in node.body ) {
+ if ( isStruct( child ) ) {
+ findAllTagsByName( child, arguments.tagName, results );
+ }
+ }
+ } else if ( isStruct( node.body ) ) {
+ findAllTagsByName( node.body, arguments.tagName, results );
+ }
+ }
+ if ( structKeyExists( node, "children" ) && isArray( node.children ) ) {
+ for ( var child in node.children ) {
+ if ( isStruct( child ) ) {
+ findAllTagsByName( child, arguments.tagName, results );
+ }
+ }
+ }
+ return results;
+ }
+
+}
diff --git a/test/tickets/LDEV5969/customAttrs.cfm b/test/tickets/LDEV5969/customAttrs.cfm
new file mode 100644
index 00000000000..5d3a3f23f83
--- /dev/null
+++ b/test/tickets/LDEV5969/customAttrs.cfm
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/test/tickets/LDEV5969/debug_ast.cfm b/test/tickets/LDEV5969/debug_ast.cfm
new file mode 100644
index 00000000000..60279719cbd
--- /dev/null
+++ b/test/tickets/LDEV5969/debug_ast.cfm
@@ -0,0 +1,5 @@
+
+code = fileRead( getCurrentTemplatePath().replace( "debug_ast.cfm", "withArgument.cfm" ) );
+ast = astFromString( code, "tag" );
+systemOutput( serialize( ast ), true );
+
diff --git a/test/tickets/LDEV5969/multipleArgs.cfm b/test/tickets/LDEV5969/multipleArgs.cfm
new file mode 100644
index 00000000000..865883054ea
--- /dev/null
+++ b/test/tickets/LDEV5969/multipleArgs.cfm
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/test/tickets/LDEV5969/script_func.cfm b/test/tickets/LDEV5969/script_func.cfm
new file mode 100644
index 00000000000..ff7bf37564b
--- /dev/null
+++ b/test/tickets/LDEV5969/script_func.cfm
@@ -0,0 +1,5 @@
+
+public string function testFunc( required string name, numeric age = 0 ) {
+ return name;
+}
+
diff --git a/test/tickets/LDEV5969/simple.cfm b/test/tickets/LDEV5969/simple.cfm
new file mode 100644
index 00000000000..1b237b170f1
--- /dev/null
+++ b/test/tickets/LDEV5969/simple.cfm
@@ -0,0 +1,3 @@
+
+
+
diff --git a/test/tickets/LDEV5969/withArgument.cfm b/test/tickets/LDEV5969/withArgument.cfm
new file mode 100644
index 00000000000..2e55f2fd93f
--- /dev/null
+++ b/test/tickets/LDEV5969/withArgument.cfm
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/test/tickets/LDEV5975.cfc b/test/tickets/LDEV5975.cfc
new file mode 100644
index 00000000000..365e4c5c6e9
--- /dev/null
+++ b/test/tickets/LDEV5975.cfc
@@ -0,0 +1,112 @@
+component extends="org.lucee.cfml.test.LuceeTestCase" labels="ast" {
+
+ variables.testDir = getDirectoryFromPath( getCurrentTemplatePath() ) & "LDEV5975/";
+
+ function run( testResults, testBox ) {
+
+ describe( "LDEV-5975: Chained method calls lose method name in AST", function() {
+
+ it( "simple method call should include method name in AST", function() {
+ var code = fileRead( variables.testDir & "simpleMethodCall.cfm" );
+ var ast = astFromString( code );
+
+ // Find the CallExpression
+ var callExpr = findNodeByType( ast, "CallExpression" );
+ expect( callExpr ).notToBeNull( "CallExpression should be present" );
+
+ // The callee should be a MemberExpression with object and property
+ var callee = callExpr.callee;
+ expect( callee.type ).toBe( "MemberExpression", "Callee should be a MemberExpression" );
+ expect( callee.object.type ).toBe( "Identifier" );
+ expect( callee.object.name ).toBe( "OBJ" );
+ expect( callee.property.type ).toBe( "Identifier" );
+ expect( callee.property.name ).toBe( "MYMETHOD", "Method name should be preserved" );
+ });
+
+ it( "method call with arguments should include method name", function() {
+ var code = fileRead( variables.testDir & "methodCall.cfm" );
+ var ast = astFromString( code, "tag" );
+
+ // Find the CallExpression for ensureCapacity
+ var callExpr = findNodeByType( ast, "CallExpression" );
+ expect( callExpr ).notToBeNull( "CallExpression should be present" );
+
+ // The callee should be a MemberExpression
+ var callee = callExpr.callee;
+ expect( callee.type ).toBe( "MemberExpression", "Callee should be a MemberExpression" );
+
+ // Should have method name "ensureCapacity"
+ expect( callee.property.name ).toBe( "ENSURECAPACITY", "Method name 'ensureCapacity' should be preserved" );
+ });
+
+ it( "chained method calls should preserve all method names", function() {
+ var code = fileRead( variables.testDir & "chainedCalls.cfm" );
+ var ast = astFromString( code );
+
+ // Find all method names in the AST
+ var methodNames = findAllMethodNames( ast );
+
+ // Should contain first, second, third
+ expect( methodNames ).toInclude( "FIRST", "Method 'first' should be preserved" );
+ expect( methodNames ).toInclude( "SECOND", "Method 'second' should be preserved" );
+ expect( methodNames ).toInclude( "THIRD", "Method 'third' should be preserved" );
+ });
+
+ });
+ }
+
+ /**
+ * Recursively find a node by type in the AST
+ */
+ private function findNodeByType( required struct node, required string nodeType ) {
+ if ( ( node.type ?: "" ) == arguments.nodeType ) {
+ return node;
+ }
+ for ( var key in node ) {
+ var val = node[ key ];
+ if ( isStruct( val ) ) {
+ var result = findNodeByType( val, arguments.nodeType );
+ if ( !isNull( result ) ) return result;
+ } else if ( isArray( val ) ) {
+ for ( var item in val ) {
+ if ( isStruct( item ) ) {
+ var result = findNodeByType( item, arguments.nodeType );
+ if ( !isNull( result ) ) return result;
+ }
+ }
+ }
+ }
+ return;
+ }
+
+ /**
+ * Find all method names from MemberExpression.property in CallExpressions
+ */
+ private function findAllMethodNames( required struct node, array results = [] ) {
+ // If this is a CallExpression with MemberExpression callee, grab the method name
+ if ( ( node.type ?: "" ) == "CallExpression" && structKeyExists( node, "callee" ) && isStruct( node.callee ) ) {
+ if ( ( node.callee.type ?: "" ) == "MemberExpression" && structKeyExists( node.callee, "property" ) ) {
+ var prop = node.callee.property;
+ if ( isStruct( prop ) && structKeyExists( prop, "name" ) ) {
+ arrayAppend( results, prop.name );
+ }
+ }
+ }
+
+ // Recurse
+ for ( var key in node ) {
+ var val = node[ key ];
+ if ( isStruct( val ) ) {
+ findAllMethodNames( val, results );
+ } else if ( isArray( val ) ) {
+ for ( var item in val ) {
+ if ( isStruct( item ) ) {
+ findAllMethodNames( item, results );
+ }
+ }
+ }
+ }
+ return results;
+ }
+
+}
diff --git a/test/tickets/LDEV5975/chainedCalls.cfm b/test/tickets/LDEV5975/chainedCalls.cfm
new file mode 100644
index 00000000000..20178bc90c7
--- /dev/null
+++ b/test/tickets/LDEV5975/chainedCalls.cfm
@@ -0,0 +1,3 @@
+
+x = obj.first().second().third();
+
diff --git a/test/tickets/LDEV5975/methodCall.cfm b/test/tickets/LDEV5975/methodCall.cfm
new file mode 100644
index 00000000000..430c1e025d9
--- /dev/null
+++ b/test/tickets/LDEV5975/methodCall.cfm
@@ -0,0 +1 @@
+
diff --git a/test/tickets/LDEV5975/simpleMethodCall.cfm b/test/tickets/LDEV5975/simpleMethodCall.cfm
new file mode 100644
index 00000000000..385f633dd92
--- /dev/null
+++ b/test/tickets/LDEV5975/simpleMethodCall.cfm
@@ -0,0 +1,3 @@
+
+x = obj.myMethod();
+
diff --git a/test/tickets/LDEV5977.cfc b/test/tickets/LDEV5977.cfc
new file mode 100644
index 00000000000..9e01937c94d
--- /dev/null
+++ b/test/tickets/LDEV5977.cfc
@@ -0,0 +1,53 @@
+component extends="org.lucee.cfml.test.LuceeTestCase" labels="ast" {
+
+ variables.testDir = getDirectoryFromPath( getCurrentTemplatePath() ) & "LDEV5977/";
+
+ function run( testResults, testBox ) {
+
+ describe( "LDEV-5977: Ternary expression alternate value duplicates consequent", function() {
+
+ it( "ternary alternate should be different from consequent", function() {
+ var code = fileRead( variables.testDir & "ternary.cfm" );
+ var ast = astFromString( code );
+
+ // Find the ConditionalExpression
+ var condExpr = findNodeByType( ast, "ConditionalExpression" );
+ expect( condExpr ).notToBeNull( "ConditionalExpression should be present" );
+
+ // Get consequent and alternate
+ var consequent = condExpr.consequent;
+ var alternate = condExpr.alternate;
+
+ expect( consequent ).notToBeNull( "Consequent should be present" );
+ expect( alternate ).notToBeNull( "Alternate should be present" );
+
+ // Consequent should be "B", alternate should be "C"
+ expect( consequent.name ).toBe( "B", "Consequent should be B" );
+ expect( alternate.name ).toBe( "C", "Alternate should be C, not duplicating consequent" );
+ });
+
+ });
+ }
+
+ private function findNodeByType( required struct node, required string nodeType ) {
+ if ( ( node.type ?: "" ) == arguments.nodeType ) {
+ return node;
+ }
+ for ( var key in node ) {
+ var val = node[ key ];
+ if ( isStruct( val ) ) {
+ var result = findNodeByType( val, arguments.nodeType );
+ if ( !isNull( result ) ) return result;
+ } else if ( isArray( val ) ) {
+ for ( var item in val ) {
+ if ( isStruct( item ) ) {
+ var result = findNodeByType( item, arguments.nodeType );
+ if ( !isNull( result ) ) return result;
+ }
+ }
+ }
+ }
+ return;
+ }
+
+}
diff --git a/test/tickets/LDEV5977/ternary.cfm b/test/tickets/LDEV5977/ternary.cfm
new file mode 100644
index 00000000000..53754207f1e
--- /dev/null
+++ b/test/tickets/LDEV5977/ternary.cfm
@@ -0,0 +1,3 @@
+
+x = a ? b : c;
+
diff --git a/test/tickets/LDEV5978.cfc b/test/tickets/LDEV5978.cfc
new file mode 100644
index 00000000000..9ba36fd30bf
--- /dev/null
+++ b/test/tickets/LDEV5978.cfc
@@ -0,0 +1,50 @@
+component extends="org.lucee.cfml.test.LuceeTestCase" labels="ast" {
+
+ variables.testDir = getDirectoryFromPath( getCurrentTemplatePath() ) & "LDEV5978/";
+
+ function run( testResults, testBox ) {
+
+ describe( "LDEV-5978: queryExecute adds extra variable name argument in AST", function() {
+
+ it( "queryExecute with 1 argument should have 1 argument in AST", function() {
+ var code = fileRead( variables.testDir & "queryExecute.cfm" );
+ var ast = astFromString( code );
+
+ // Find the CallExpression for queryExecute
+ var callExpr = findCallByName( ast, "QUERYEXECUTE" );
+ expect( callExpr ).notToBeNull( "queryExecute CallExpression should be present" );
+
+ // Should have exactly 1 argument (the SQL string)
+ var args = callExpr.arguments;
+ expect( args ).toBeArray();
+ expect( arrayLen( args ) ).toBe( 1, "queryExecute should have 1 argument, not extra internal arguments" );
+ });
+
+ });
+ }
+
+ private function findCallByName( required struct node, required string funcName ) {
+ if ( ( node.type ?: "" ) == "CallExpression" ) {
+ var callee = node.callee ?: {};
+ if ( ( callee.name ?: "" ) == arguments.funcName ) {
+ return node;
+ }
+ }
+ for ( var key in node ) {
+ var val = node[ key ];
+ if ( isStruct( val ) ) {
+ var result = findCallByName( val, arguments.funcName );
+ if ( !isNull( result ) ) return result;
+ } else if ( isArray( val ) ) {
+ for ( var item in val ) {
+ if ( isStruct( item ) ) {
+ var result = findCallByName( item, arguments.funcName );
+ if ( !isNull( result ) ) return result;
+ }
+ }
+ }
+ }
+ return;
+ }
+
+}
diff --git a/test/tickets/LDEV5978/queryExecute.cfm b/test/tickets/LDEV5978/queryExecute.cfm
new file mode 100644
index 00000000000..e2b78e727ee
--- /dev/null
+++ b/test/tickets/LDEV5978/queryExecute.cfm
@@ -0,0 +1,3 @@
+
+q = queryExecute("SELECT 1");
+
diff --git a/test/tickets/LDEV5979.cfc b/test/tickets/LDEV5979.cfc
new file mode 100644
index 00000000000..d8f11b8cffe
--- /dev/null
+++ b/test/tickets/LDEV5979.cfc
@@ -0,0 +1,54 @@
+component extends="org.lucee.cfml.test.LuceeTestCase" labels="ast" {
+
+ variables.testDir = getDirectoryFromPath( getCurrentTemplatePath() ) & "LDEV5979/";
+
+ function run( testResults, testBox ) {
+
+ describe( "LDEV-5979: isDefined adds extra scope argument in AST", function() {
+
+ it( "isDefined with 1 argument should have 1 argument in AST", function() {
+ var code = fileRead( variables.testDir & "isDefined.cfm" );
+ var ast = astFromString( code );
+
+ // Find the CallExpression for isDefined
+ var callExpr = findCallByName( ast, "ISDEFINED" );
+ expect( callExpr ).notToBeNull( "isDefined CallExpression should be present" );
+
+ // Should have exactly 1 argument (the variable name string)
+ var args = callExpr.arguments;
+ expect( args ).toBeArray();
+ expect( arrayLen( args ) ).toBe( 1, "isDefined should have 1 argument, not extra internal scope argument" );
+
+ // The argument should be the string "foo", not a number
+ var firstArg = args[ 1 ];
+ expect( firstArg.type ).toBe( "StringLiteral", "First argument should be a StringLiteral" );
+ });
+
+ });
+ }
+
+ private function findCallByName( required struct node, required string funcName ) {
+ if ( ( node.type ?: "" ) == "CallExpression" ) {
+ var callee = node.callee ?: {};
+ if ( ( callee.name ?: "" ) == arguments.funcName ) {
+ return node;
+ }
+ }
+ for ( var key in node ) {
+ var val = node[ key ];
+ if ( isStruct( val ) ) {
+ var result = findCallByName( val, arguments.funcName );
+ if ( !isNull( result ) ) return result;
+ } else if ( isArray( val ) ) {
+ for ( var item in val ) {
+ if ( isStruct( item ) ) {
+ var result = findCallByName( item, arguments.funcName );
+ if ( !isNull( result ) ) return result;
+ }
+ }
+ }
+ }
+ return;
+ }
+
+}
diff --git a/test/tickets/LDEV5979/isDefined.cfm b/test/tickets/LDEV5979/isDefined.cfm
new file mode 100644
index 00000000000..ec9b62562aa
--- /dev/null
+++ b/test/tickets/LDEV5979/isDefined.cfm
@@ -0,0 +1,3 @@
+
+x = isDefined("foo");
+
diff --git a/test/tickets/LDEV5980.cfc b/test/tickets/LDEV5980.cfc
new file mode 100644
index 00000000000..3d6ebef3274
--- /dev/null
+++ b/test/tickets/LDEV5980.cfc
@@ -0,0 +1,74 @@
+component extends="org.lucee.cfml.test.LuceeTestCase" labels="ast" {
+
+ variables.testDir = getDirectoryFromPath( getCurrentTemplatePath() ) & "LDEV5980/";
+
+ function run( testResults, testBox ) {
+
+ describe( "LDEV-5980: BIF calls keep adding internal metadata", function() {
+
+ it( "dump() should not have internal metadata arguments in AST", function() {
+ var code = fileRead( variables.testDir & "dump.cfm" );
+ var ast = astFromString( code, "tag" );
+
+ // Find the CallExpression for dump
+ var callExpr = findCallExpression( ast, "DUMP" );
+ expect( callExpr ).notToBeNull( "dump CallExpression should be present" );
+
+ var args = callExpr.arguments;
+
+ // dump(myVar) should have exactly 1 argument
+ expect( arrayLen( args ) ).toBe( 1, "dump() should have 1 argument, not internal metadata" );
+
+ // The argument should be the identifier MYVAR, not named params like __filename
+ var firstArg = args[ 1 ];
+ expect( firstArg.type ).toBe( "Identifier", "Argument should be an Identifier" );
+ expect( firstArg.name ).toBe( "MYVAR", "Argument should be MYVAR" );
+ });
+
+ it( "throw() should not have internal metadata arguments in AST", function() {
+ var code = fileRead( variables.testDir & "throw.cfm" );
+ var ast = astFromString( code, "tag" );
+
+ // Find the CallExpression for throw
+ var callExpr = findCallExpression( ast, "THROW" );
+ expect( callExpr ).notToBeNull( "throw CallExpression should be present" );
+
+ var args = callExpr.arguments;
+
+ // throw("error message") should have exactly 1 argument
+ expect( arrayLen( args ) ).toBe( 1, "throw() should have 1 argument, not internal metadata" );
+ });
+
+ });
+ }
+
+ /**
+ * Recursively find a CallExpression by callee name
+ */
+ private function findCallExpression( required struct node, required string funcName ) {
+ if ( ( node.type ?: "" ) == "CallExpression" ) {
+ var callee = node.callee ?: {};
+ // Direct function call
+ if ( ( callee.type ?: "" ) == "Identifier" && ( callee.name ?: "" ) == arguments.funcName ) {
+ return node;
+ }
+ }
+
+ for ( var key in node ) {
+ var val = node[ key ];
+ if ( isStruct( val ) ) {
+ var result = findCallExpression( val, arguments.funcName );
+ if ( !isNull( result ) ) return result;
+ } else if ( isArray( val ) ) {
+ for ( var item in val ) {
+ if ( isStruct( item ) ) {
+ var result = findCallExpression( item, arguments.funcName );
+ if ( !isNull( result ) ) return result;
+ }
+ }
+ }
+ }
+ return;
+ }
+
+}
diff --git a/test/tickets/LDEV5980/debug_ast.cfm b/test/tickets/LDEV5980/debug_ast.cfm
new file mode 100644
index 00000000000..87bc51d21a1
--- /dev/null
+++ b/test/tickets/LDEV5980/debug_ast.cfm
@@ -0,0 +1,5 @@
+
+code = fileRead( getCurrentTemplatePath().replace("debug_ast.cfm", "dump.cfm") );
+ast = astFromString( code, "tag" );
+systemOutput( serialize( ast ), true );
+
diff --git a/test/tickets/LDEV5980/dump.cfm b/test/tickets/LDEV5980/dump.cfm
new file mode 100644
index 00000000000..472b660363f
--- /dev/null
+++ b/test/tickets/LDEV5980/dump.cfm
@@ -0,0 +1,3 @@
+
+dump(myVar);
+
diff --git a/test/tickets/LDEV5980/throw.cfm b/test/tickets/LDEV5980/throw.cfm
new file mode 100644
index 00000000000..323f24624bd
--- /dev/null
+++ b/test/tickets/LDEV5980/throw.cfm
@@ -0,0 +1,3 @@
+
+throw("error message");
+
diff --git a/test/tickets/LDEV5982.cfc b/test/tickets/LDEV5982.cfc
new file mode 100644
index 00000000000..056cd9ff9de
--- /dev/null
+++ b/test/tickets/LDEV5982.cfc
@@ -0,0 +1,106 @@
+component extends="org.lucee.cfml.test.LuceeTestCase" labels="ast" {
+
+ function run( testResults, testBox ) {
+
+ describe( "LDEV-5982: cflock internal id attribute leaks into AST", function() {
+
+ it( "cflock should not have internal id attribute in AST", function() {
+ var code = '';
+ var ast = astFromString( code, "tag" );
+
+ // Find the cflock tag in the AST
+ var lockTag = findTagByName( ast, "lock" );
+ expect( lockTag ).notToBeNull( "cflock tag should be present in AST" );
+
+ // Get attribute names
+ var attrs = lockTag.attributes ?: [];
+ var attrNames = attrs.map( function( a ) { return a.name; } );
+
+ // Should have type and timeout, but NOT id
+ expect( attrNames ).toInclude( "type" );
+ expect( attrNames ).toInclude( "timeout" );
+ expect( attrNames ).notToInclude( "id", "Internal id attribute should not leak into AST" );
+ });
+
+ it( "cflock with explicit name should preserve name in AST", function() {
+ var code = '';
+ var ast = astFromString( code, "tag" );
+
+ var lockTag = findTagByName( ast, "lock" );
+ expect( lockTag ).notToBeNull();
+
+ var attrs = lockTag.attributes ?: [];
+ var attrNames = attrs.map( function( a ) { return a.name; } );
+
+ // Should have name, type, timeout - but NOT id
+ expect( attrNames ).toInclude( "name" );
+ expect( attrNames ).toInclude( "type" );
+ expect( attrNames ).toInclude( "timeout" );
+ expect( attrNames ).notToInclude( "id", "Internal id attribute should not leak into AST" );
+ });
+
+ it( "cfcache tag syntax should not have internal _id attribute in AST", function() {
+ var testDir = getDirectoryFromPath( getCurrentTemplatePath() ) & "LDEV5982/";
+ var ast = astFromPath( testDir & "cfcache.cfm" );
+
+ var cacheTag = findTagByName( ast, "cache" );
+ expect( cacheTag ).notToBeNull( "cfcache tag should be present in AST" );
+
+ var attrs = cacheTag.attributes ?: [];
+ var attrNames = attrs.map( function( a ) { return a.name; } );
+
+ // Should have action, but NOT _id
+ expect( attrNames ).toInclude( "action" );
+ expect( attrNames ).notToInclude( "_id", "Internal _id attribute should not leak into AST" );
+ });
+
+ it( "cfcache script syntax should not have internal _id attribute in AST", function() {
+ var code = 'cfcache( action="flush" );';
+ var ast = astFromString( code, "script" );
+
+ var cacheTag = findTagByName( ast, "cache" );
+ expect( cacheTag ).notToBeNull( "cfcache tag should be present in AST" );
+
+ var attrs = cacheTag.attributes ?: [];
+ var attrNames = attrs.map( function( a ) { return a.name; } );
+
+ // Should have action, but NOT _id
+ expect( attrNames ).toInclude( "action" );
+ expect( attrNames ).notToInclude( "_id", "Internal _id attribute should not leak into AST (script mode)" );
+ });
+
+ });
+ }
+
+ /**
+ * Recursively find a tag by name in the AST
+ */
+ private function findTagByName( required struct node, required string tagName ) {
+ if ( ( node.type ?: "" ) == "CFMLTag" && ( node.name ?: "" ) == arguments.tagName ) {
+ return node;
+ }
+ if ( structKeyExists( node, "body" ) ) {
+ if ( isArray( node.body ) ) {
+ for ( var child in node.body ) {
+ if ( isStruct( child ) ) {
+ var result = findTagByName( child, arguments.tagName );
+ if ( !isNull( result ) ) return result;
+ }
+ }
+ } else if ( isStruct( node.body ) ) {
+ var result = findTagByName( node.body, arguments.tagName );
+ if ( !isNull( result ) ) return result;
+ }
+ }
+ if ( structKeyExists( node, "children" ) && isArray( node.children ) ) {
+ for ( var child in node.children ) {
+ if ( isStruct( child ) ) {
+ var result = findTagByName( child, arguments.tagName );
+ if ( !isNull( result ) ) return result;
+ }
+ }
+ }
+ return;
+ }
+
+}
diff --git a/test/tickets/LDEV5982/cfcache.cfm b/test/tickets/LDEV5982/cfcache.cfm
new file mode 100644
index 00000000000..ce5a365586f
--- /dev/null
+++ b/test/tickets/LDEV5982/cfcache.cfm
@@ -0,0 +1 @@
+
diff --git a/test/tickets/LDEV5983.cfc b/test/tickets/LDEV5983.cfc
new file mode 100644
index 00000000000..5eafb95e6b3
--- /dev/null
+++ b/test/tickets/LDEV5983.cfc
@@ -0,0 +1,43 @@
+component extends="org.lucee.cfml.test.LuceeTestCase" labels="ast" {
+
+ variables.testDir = getDirectoryFromPath( getCurrentTemplatePath() ) & "LDEV5983/";
+
+ function run( testResults, testBox ) {
+
+ describe( "LDEV-5983: cffunction body duplicates comments between cfargument tags", function() {
+
+ it( "comments between cfargument tags should not be duplicated in AST", function() {
+ var code = fileRead( variables.testDir & "commentBetweenArgs.cfm" );
+ var ast = astFromString( code, "tag" );
+
+ var commentCount = countStringInAST( ast, "" );
+ expect( commentCount ).toBe( 1, "Comment should appear exactly once in AST, found #commentCount# times" );
+ });
+
+ it( "cffunction with comment after last cfargument should be stable", function() {
+ var code = fileRead( variables.testDir & "commentAfterLastArg.cfm" );
+ var ast = astFromString( code, "tag" );
+
+ var commentCount = countStringInAST( ast, "" );
+ expect( commentCount ).toBe( 1, "Comment should appear exactly once in AST, found #commentCount# times" );
+ });
+
+ });
+
+ }
+
+ /**
+ * Count occurrences of a string in StringLiteral values within the AST
+ */
+ private numeric function countStringInAST( required struct ast, required string searchFor ) {
+ var matches = structFindKey( ast, "value", "all" );
+ var count = 0;
+ for ( var match in matches ) {
+ if ( isSimpleValue( match.value ) && find( arguments.searchFor, match.value ) ) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+}
diff --git a/test/tickets/LDEV5983/commentAfterLastArg.cfm b/test/tickets/LDEV5983/commentAfterLastArg.cfm
new file mode 100644
index 00000000000..807f7eea960
--- /dev/null
+++ b/test/tickets/LDEV5983/commentAfterLastArg.cfm
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/test/tickets/LDEV5983/commentBetweenArgs.cfm b/test/tickets/LDEV5983/commentBetweenArgs.cfm
new file mode 100644
index 00000000000..8c00b7a4b9e
--- /dev/null
+++ b/test/tickets/LDEV5983/commentBetweenArgs.cfm
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/test/tickets/LDEV5984.cfc b/test/tickets/LDEV5984.cfc
new file mode 100644
index 00000000000..88eaa9a3399
--- /dev/null
+++ b/test/tickets/LDEV5984.cfc
@@ -0,0 +1,83 @@
+component extends="org.lucee.cfml.test.LuceeTestCase" labels="ast" {
+
+ variables.testDir = getDirectoryFromPath( getCurrentTemplatePath() ) & "LDEV5984/";
+
+ function run( testResults, testBox ) {
+
+ describe( "LDEV-5984: Escaped hashes lose escaping in AST", function() {
+
+ it( "raw should contain escaped hashes to allow round-tripping", function() {
+ var code = fileRead( variables.testDir & "escapedHashes.cfm" );
+ var ast = astFromString( code );
+
+ // Find the StringLiteral in the AST
+ var stringLiteral = findStringLiteral( ast );
+ expect( stringLiteral ).notToBeNull( "StringLiteral should be present in AST" );
+
+ // The value correctly contains ##hello## (the runtime value)
+ // But the raw should contain #### so we can regenerate the source
+ // Currently raw is "##hello##" which loses the escaping info
+ var rawValue = stringLiteral.raw;
+
+ // Count the hashes - should have 4 before and 4 after "hello"
+ // i.e. raw should be something like '####hello####' or "####hello####"
+ var hashCount = len( rawValue ) - len( replace( rawValue, "##", "", "all" ) );
+ // Each ## = 2 chars removed, so hashCount / 2 = number of ## sequences
+ // We expect 4 ## sequences (#### before hello, #### after hello)
+ expect( hashCount / 2 ).toBeGTE( 4, "Raw should contain #### (escaped ##) not just ## - got: " & rawValue );
+ });
+
+ it( "round-trip should preserve escaped hashes", function() {
+ // Input has #### which means literal ## in the string value
+ // Need ######## in test code to produce #### in parsed string
+ // ######## -> #### (in test) -> ## (in parsed value) = 2 chars per side
+ var code = "x = '########test########';";
+ var ast1 = astFromString( code );
+
+ var str1 = findStringLiteral( ast1 );
+ expect( isNull( str1 ) ).toBeFalse( "First parse should find StringLiteral" );
+
+ // Value should be ##test## (the runtime value) = 8 chars
+ var val1 = str1.value;
+ expect( len( val1 ) ).toBe( 8, "Value should be 8 chars but got len=" & len( val1 ) );
+
+ // If we use raw to regenerate and reparse, value should be stable
+ var newCode = "x = " & str1.raw & ";";
+ var ast2 = astFromString( newCode );
+ var str2 = findStringLiteral( ast2 );
+
+ expect( isNull( str2 ) ).toBeFalse( "Second parse should find StringLiteral" );
+ expect( str2.value ).toBe( val1, "Value should be stable after round-trip" );
+ });
+
+ });
+
+ }
+
+ /**
+ * Recursively find a StringLiteral node in the AST that contains hashes
+ */
+ private function findStringLiteral( required struct node ) {
+ if ( ( node.type ?: "" ) == "StringLiteral" && structKeyExists( node, "value" ) ) {
+ if ( find( "test", node.value ) || find( "hello", node.value ) ) {
+ return node;
+ }
+ }
+ for ( var key in node ) {
+ var val = node[ key ];
+ if ( isStruct( val ) ) {
+ var result = findStringLiteral( val );
+ if ( !isNull( result ) ) return result;
+ } else if ( isArray( val ) ) {
+ for ( var item in val ) {
+ if ( isStruct( item ) ) {
+ var result = findStringLiteral( item );
+ if ( !isNull( result ) ) return result;
+ }
+ }
+ }
+ }
+ return;
+ }
+
+}
diff --git a/test/tickets/LDEV5984/escapedHashes.cfm b/test/tickets/LDEV5984/escapedHashes.cfm
new file mode 100644
index 00000000000..2028683e321
--- /dev/null
+++ b/test/tickets/LDEV5984/escapedHashes.cfm
@@ -0,0 +1,3 @@
+
+x = '####hello####';
+
diff --git a/test/tickets/LDEV5985.cfc b/test/tickets/LDEV5985.cfc
new file mode 100644
index 00000000000..c6efe81bc8a
--- /dev/null
+++ b/test/tickets/LDEV5985.cfc
@@ -0,0 +1,70 @@
+component extends="org.lucee.cfml.test.LuceeTestCase" labels="ast" {
+
+ variables.testDir = getDirectoryFromPath( getCurrentTemplatePath() ) & "LDEV5985/";
+
+ function run( testResults, testBox ) {
+
+ describe( "LDEV-5985: Java functions crash AST parser", function() {
+
+ it( "should parse functions with type=java without throwing ClassCastException", function() {
+ var code = fileRead( variables.testDir & "javaFunction.cfm" );
+
+ // This currently throws:
+ // class lucee.transformer.util.SourceCode cannot be cast to class lucee.transformer.util.PageSourceCode
+ var ast = astFromString( code );
+
+ expect( ast ).toBeStruct();
+ expect( ast.type ).toBe( "Program" );
+ });
+
+ it( "should include java function in AST body", function() {
+ var code = fileRead( variables.testDir & "javaFunction.cfm" );
+ var ast = astFromString( code );
+
+ // Find the function declaration
+ var funcDecl = findNodeByType( ast, "FunctionDeclaration" );
+ expect( isNull( funcDecl ) ).toBeFalse( "FunctionDeclaration should be present" );
+ // Just verify we found a function - the structure may vary
+ expect( funcDecl.type ).toBe( "FunctionDeclaration" );
+ });
+
+ it( "should preserve raw Java source in AST body for round-tripping", function() {
+ var code = fileRead( variables.testDir & "javaFunction.cfm" );
+ var ast = astFromString( code );
+
+ // Find the function declaration
+ var funcDecl = findNodeByType( ast, "FunctionDeclaration" );
+ expect( isNull( funcDecl ) ).toBeFalse( "FunctionDeclaration should be present" );
+
+ // The body should contain the raw Java source for round-tripping
+ expect( funcDecl ).toHaveKey( "body" );
+ expect( funcDecl.body ).toHaveKey( "raw" );
+ expect( funcDecl.body.raw ).toInclude( "return i*2" );
+ });
+
+ });
+
+ }
+
+ private function findNodeByType( required struct node, required string nodeType ) {
+ if ( ( node.type ?: "" ) == arguments.nodeType ) {
+ return node;
+ }
+ for ( var key in node ) {
+ var val = node[ key ];
+ if ( isStruct( val ) ) {
+ var result = findNodeByType( val, arguments.nodeType );
+ if ( !isNull( result ) ) return result;
+ } else if ( isArray( val ) ) {
+ for ( var item in val ) {
+ if ( isStruct( item ) ) {
+ var result = findNodeByType( item, arguments.nodeType );
+ if ( !isNull( result ) ) return result;
+ }
+ }
+ }
+ }
+ return;
+ }
+
+}
diff --git a/test/tickets/LDEV5985/javaFunction.cfm b/test/tickets/LDEV5985/javaFunction.cfm
new file mode 100644
index 00000000000..fe6e20e92b2
--- /dev/null
+++ b/test/tickets/LDEV5985/javaFunction.cfm
@@ -0,0 +1,5 @@
+
+private int function echoInt(int i) type="java" {
+ return i*2;
+}
+
diff --git a/test/tickets/LDEV5986.cfc b/test/tickets/LDEV5986.cfc
new file mode 100644
index 00000000000..8b7cdf957a6
--- /dev/null
+++ b/test/tickets/LDEV5986.cfc
@@ -0,0 +1,95 @@
+component extends="org.lucee.cfml.test.LuceeTestCase" labels="ast" {
+
+ variables.testDir = getDirectoryFromPath( getCurrentTemplatePath() ) & "LDEV5986/";
+
+ function run( testResults, testBox ) {
+
+ describe( "LDEV-5986: Tag islands should have dedicated node type in AST", function() {
+
+ it( "should have a TagIsland node type wrapping the tag content", function() {
+ var code = fileRead( variables.testDir & "tagIsland.cfm" );
+ var ast = astFromString( code );
+
+ // Tag islands should have their own node type like TagIsland or similar
+ // Currently the backticks become StringLiteral with "\r\n" and the
+ // tag content is parsed inline without being wrapped in a TagIsland node
+
+ // Check for a TagIsland node (or similar dedicated type)
+ var tagIsland = findNodeByType( ast, "TagIsland" );
+ expect( isNull( tagIsland ) ).toBeFalse( "Tag island should have a dedicated TagIsland node type" );
+ });
+
+ it( "tag island should not produce spurious StringLiteral from backticks", function() {
+ var code = fileRead( variables.testDir & "tagIsland.cfm" );
+ var ast = astFromString( code );
+
+ // Currently the backtick delimiters ``` become StringLiteral nodes
+ // with value "\r\n" - these shouldn't exist
+ var scriptBody = findScriptBody( ast );
+ expect( scriptBody ).notToBeNull( "Should find cfscript body" );
+
+ // Count StringLiterals that are just whitespace - these come from the backticks
+ var spuriousStrings = 0;
+ for ( var stmt in scriptBody ) {
+ if ( ( stmt.type ?: "" ) == "ExpressionStatement" ) {
+ var expr = stmt.expression ?: {};
+ if ( ( expr.type ?: "" ) == "StringLiteral" ) {
+ var val = expr.value ?: "";
+ if ( reFind( "^[\r\n\s]*$", val ) ) {
+ spuriousStrings++;
+ }
+ }
+ }
+ }
+ expect( spuriousStrings ).toBe( 0, "Backtick delimiters should not become StringLiteral nodes" );
+ });
+
+ });
+
+ }
+
+ private function findNodeByType( required struct node, required string nodeType ) {
+ if ( ( node.type ?: "" ) == arguments.nodeType ) {
+ return node;
+ }
+ for ( var key in node ) {
+ var val = node[ key ];
+ if ( isStruct( val ) ) {
+ var result = findNodeByType( val, arguments.nodeType );
+ if ( !isNull( result ) ) return result;
+ } else if ( isArray( val ) ) {
+ for ( var item in val ) {
+ if ( isStruct( item ) ) {
+ var result = findNodeByType( item, arguments.nodeType );
+ if ( !isNull( result ) ) return result;
+ }
+ }
+ }
+ }
+ return;
+ }
+
+ private function findScriptBody( required struct node ) {
+ if ( ( node.type ?: "" ) == "CFMLTag" && ( node.name ?: "" ) == "script" ) {
+ if ( structKeyExists( node, "body" ) && structKeyExists( node.body, "body" ) ) {
+ return node.body.body;
+ }
+ }
+ for ( var key in node ) {
+ var val = node[ key ];
+ if ( isStruct( val ) ) {
+ var result = findScriptBody( val );
+ if ( !isNull( result ) ) return result;
+ } else if ( isArray( val ) ) {
+ for ( var item in val ) {
+ if ( isStruct( item ) ) {
+ var result = findScriptBody( item );
+ if ( !isNull( result ) ) return result;
+ }
+ }
+ }
+ }
+ return;
+ }
+
+}
diff --git a/test/tickets/LDEV5986/tagIsland.cfm b/test/tickets/LDEV5986/tagIsland.cfm
new file mode 100644
index 00000000000..2f7a89d42df
--- /dev/null
+++ b/test/tickets/LDEV5986/tagIsland.cfm
@@ -0,0 +1,7 @@
+
+x = 1;
+```
+
+```
+z = 3;
+
diff --git a/test/tickets/LDEV5987.cfc b/test/tickets/LDEV5987.cfc
new file mode 100644
index 00000000000..dc7650e9a43
--- /dev/null
+++ b/test/tickets/LDEV5987.cfc
@@ -0,0 +1,74 @@
+component extends="org.lucee.cfml.test.LuceeTestCase" labels="ast" {
+
+ variables.testDir = getDirectoryFromPath( getCurrentTemplatePath() ) & "LDEV5987/";
+
+ function run( testResults, testBox ) {
+
+ describe( "LDEV-5987: Missing parent statement error with arrow functions and threads", function() {
+
+ it( "should parse arrow function containing thread without error", function() {
+ var code = fileRead( variables.testDir & "arrowWithThread.cfm" );
+
+ // This currently throws: missing parent Statement of Statement
+ var ast = astFromString( code );
+
+ expect( ast ).toBeStruct();
+ expect( ast.type ).toBe( "Program" );
+ });
+
+ it( "should include thread tag in AST", function() {
+ var code = fileRead( variables.testDir & "arrowWithThread.cfm" );
+ var ast = astFromString( code );
+
+ // Find the thread tag - main thing is it parses without error
+ var threadTag = findTagByName( ast, "thread" );
+ expect( isNull( threadTag ) ).toBeFalse( "Thread tag should be present in AST" );
+ });
+
+ });
+
+ }
+
+ private function findNodeByType( required struct node, required string nodeType ) {
+ if ( ( node.type ?: "" ) == arguments.nodeType ) {
+ return node;
+ }
+ for ( var key in node ) {
+ var val = node[ key ];
+ if ( isStruct( val ) ) {
+ var result = findNodeByType( val, arguments.nodeType );
+ if ( !isNull( result ) ) return result;
+ } else if ( isArray( val ) ) {
+ for ( var item in val ) {
+ if ( isStruct( item ) ) {
+ var result = findNodeByType( item, arguments.nodeType );
+ if ( !isNull( result ) ) return result;
+ }
+ }
+ }
+ }
+ return;
+ }
+
+ private function findTagByName( required struct node, required string tagName ) {
+ if ( ( node.type ?: "" ) == "CFMLTag" && ( node.name ?: "" ) == arguments.tagName ) {
+ return node;
+ }
+ for ( var key in node ) {
+ var val = node[ key ];
+ if ( isStruct( val ) ) {
+ var result = findTagByName( val, arguments.tagName );
+ if ( !isNull( result ) ) return result;
+ } else if ( isArray( val ) ) {
+ for ( var item in val ) {
+ if ( isStruct( item ) ) {
+ var result = findTagByName( item, arguments.tagName );
+ if ( !isNull( result ) ) return result;
+ }
+ }
+ }
+ }
+ return;
+ }
+
+}
diff --git a/test/tickets/LDEV5987/arrowWithThread.cfm b/test/tickets/LDEV5987/arrowWithThread.cfm
new file mode 100644
index 00000000000..8a3c175e7e2
--- /dev/null
+++ b/test/tickets/LDEV5987/arrowWithThread.cfm
@@ -0,0 +1,6 @@
+
+runner = () => {
+ thread name="LDEV5987" {}
+ return "success";
+}
+
diff --git a/test/tickets/LDEV5989.cfc b/test/tickets/LDEV5989.cfc
new file mode 100644
index 00000000000..5d2a7a02424
--- /dev/null
+++ b/test/tickets/LDEV5989.cfc
@@ -0,0 +1,84 @@
+component extends="org.lucee.cfml.test.LuceeTestCase" labels="ast" {
+
+ variables.testDir = getDirectoryFromPath( getCurrentTemplatePath() ) & "LDEV5989/";
+
+ function run( testResults, testBox ) {
+
+ describe( "LDEV-5989: Interpolated attribute values should be parsed as expressions", function() {
+
+ it( "interpolated condition attribute should be parsed as expression not StringLiteral", function() {
+ var code = fileRead( variables.testDir & "interpolatedAttr.cfm" );
+ var ast = astFromString( code );
+
+ // Find the condition attribute
+ var attr = findAttribute( ast, "condition" );
+ expect( attr ).notToBeNull( "condition attribute should be present in AST" );
+
+ // The value should be parsed as an expression (CastExpression or CallExpression)
+ // NOT as a StringLiteral with hashes in the value
+ expect( attr.value.type ).notToBe( "StringLiteral",
+ "Interpolated attribute should be parsed as expression, not StringLiteral. Got type: " & attr.value.type );
+ });
+
+ it( "condition with call expression should parse the function call", function() {
+ var code = fileRead( variables.testDir & "expressionAttr.cfm" );
+ var ast = astFromString( code );
+
+ var attr = findAttribute( ast, "condition" );
+ expect( attr ).notToBeNull( "condition attribute should be present" );
+
+ // Value should be a CastExpression (toBoolean) wrapping a CallExpression
+ // or directly a CallExpression depending on implementation
+ var valueType = attr.value.type;
+ expect( valueType == "CallExpression" || valueType == "CastExpression" ).toBeTrue(
+ "Expected CallExpression or CastExpression, got: " & valueType );
+ });
+
+ it( "condition expression should have correct call structure", function() {
+ // Parse cfloop with condition containing interpolated call expression
+ var code = fileRead( variables.testDir & "interpolatedAttr.cfm" );
+ var ast = astFromString( code );
+
+ var attr = findAttribute( ast, "condition" );
+ expect( attr ).notToBeNull( "condition attribute should be present" );
+
+ // Navigate to the actual call expression (may be wrapped in CastExpression)
+ var expr = attr.value;
+ if ( expr.type == "CastExpression" ) {
+ expr = expr.argument;
+ }
+
+ // Should be a CallExpression for it.hasNext()
+ expect( expr.type ).toBe( "CallExpression", "Should be a CallExpression" );
+ expect( expr.callee.type ).toBe( "MemberExpression", "Callee should be MemberExpression" );
+ });
+
+ });
+
+ }
+
+ /**
+ * Recursively find an Attribute node by name
+ */
+ private function findAttribute( required struct node, required string name ) {
+ if ( ( node.type ?: "" ) == "Attribute" && ( node.name ?: "" ) == name ) {
+ return node;
+ }
+ for ( var key in node ) {
+ var val = node[ key ];
+ if ( isStruct( val ) ) {
+ var result = findAttribute( val, name );
+ if ( !isNull( result ) ) return result;
+ } else if ( isArray( val ) ) {
+ for ( var item in val ) {
+ if ( isStruct( item ) ) {
+ var result = findAttribute( item, name );
+ if ( !isNull( result ) ) return result;
+ }
+ }
+ }
+ }
+ return;
+ }
+
+}
diff --git a/test/tickets/LDEV5989/expressionAttr.cfm b/test/tickets/LDEV5989/expressionAttr.cfm
new file mode 100644
index 00000000000..39e5de31a2e
--- /dev/null
+++ b/test/tickets/LDEV5989/expressionAttr.cfm
@@ -0,0 +1 @@
+
diff --git a/test/tickets/LDEV5989/interpolatedAttr.cfm b/test/tickets/LDEV5989/interpolatedAttr.cfm
new file mode 100644
index 00000000000..39e5de31a2e
--- /dev/null
+++ b/test/tickets/LDEV5989/interpolatedAttr.cfm
@@ -0,0 +1 @@
+
diff --git a/test/tickets/LDEV5989/loopTemplate.txt b/test/tickets/LDEV5989/loopTemplate.txt
new file mode 100644
index 00000000000..232c39dbf0f
--- /dev/null
+++ b/test/tickets/LDEV5989/loopTemplate.txt
@@ -0,0 +1 @@
+
diff --git a/test/tickets/LDEV5989/roundtrip1.cfm b/test/tickets/LDEV5989/roundtrip1.cfm
new file mode 100644
index 00000000000..9d945f9c852
--- /dev/null
+++ b/test/tickets/LDEV5989/roundtrip1.cfm
@@ -0,0 +1 @@
+
diff --git a/test/tickets/LDEV5989/roundtrip2.cfm b/test/tickets/LDEV5989/roundtrip2.cfm
new file mode 100644
index 00000000000..9d945f9c852
--- /dev/null
+++ b/test/tickets/LDEV5989/roundtrip2.cfm
@@ -0,0 +1 @@
+
diff --git a/test/tickets/LDEV5990.cfc b/test/tickets/LDEV5990.cfc
new file mode 100644
index 00000000000..6d60d80631b
--- /dev/null
+++ b/test/tickets/LDEV5990.cfc
@@ -0,0 +1,615 @@
+component extends="org.lucee.cfml.test.LuceeTestCase" labels="ast" {
+
+ variables.testDir = getDirectoryFromPath( getCurrentTemplatePath() ) & "LDEV5990/";
+
+ function run( testResults, testBox ) {
+
+ describe( "LDEV-5990: Function param hint and defaultValue missing from AST", function() {
+
+ it( "should include hint value on function parameters", function() {
+ // Use astFromPath for CFC files
+ var ast = astFromPath( variables.testDir & "hintedParams.cfc" );
+
+ // Find the withHint function
+ var func = findFunction( ast, "withHint" );
+ expect( func ).notToBeNull( "withHint function should be found in AST" );
+
+ // Check the first param has hint with correct value
+ var param = func.params[ 1 ];
+ expect( param.name.value ).toBe( "name" );
+ expect( param.hint ).notToBeNull( "param should have hint attribute" );
+ // Bug: hint.value is empty string instead of the actual hint text
+ expect( param.hint.value ).toBe( "The user's full name", "hint value should contain the original hint text" );
+ });
+
+ it( "should include defaultValue on function parameters", function() {
+ var ast = astFromPath( variables.testDir & "hintedParams.cfc" );
+
+ // Find the withDefault function
+ var func = findFunction( ast, "withDefault" );
+ expect( func ).notToBeNull( "withDefault function should be found in AST" );
+
+ // Check the first param has defaultValue
+ var param = func.params[ 1 ];
+ expect( param.name.value ).toBe( "greeting" );
+ // Bug: defaultValue key is completely missing from param
+ expect( structKeyExists( param, "defaultValue" ) ).toBeTrue( "param should have defaultValue key" );
+ expect( param.defaultValue.value ).toBe( "Hello", "defaultValue should contain the default value" );
+ });
+
+ it( "should include both hint and defaultValue on same parameter", function() {
+ var ast = astFromPath( variables.testDir & "hintedParams.cfc" );
+
+ // Find the withBoth function
+ var func = findFunction( ast, "withBoth" );
+ expect( func ).notToBeNull( "withBoth function should be found in AST" );
+
+ // Check the first param has both hint and defaultValue
+ var param = func.params[ 1 ];
+ expect( param.name.value ).toBe( "message" );
+ // Bug: hint.value is empty, defaultValue key is missing
+ expect( param.hint.value ).toBe( "The message to display", "hint value should be preserved" );
+ expect( structKeyExists( param, "defaultValue" ) ).toBeTrue( "param should have defaultValue key" );
+ expect( param.defaultValue.value ).toBe( "Welcome", "defaultValue should be preserved" );
+ });
+
+ });
+
+ describe( "LDEV-5990: Function attributes like returnFormat should be preserved", function() {
+
+ it( "should include returnFormat attribute on remote functions", function() {
+ var ast = astFromPath( variables.testDir & "hintedParams.cfc" );
+
+ // Find the remoteFunc function
+ var func = findFunction( ast, "remoteFunc" );
+ expect( func ).notToBeNull( "remoteFunc function should be found in AST" );
+
+ // Bug: returnFormat attribute is missing from AST
+ expect( func ).toHaveKey( "returnFormat", "remote function should have returnFormat in AST" );
+ expect( func.returnFormat.value ).toBe( "json", "returnFormat value should be preserved" );
+ });
+
+ });
+
+ describe( "LDEV-5990: Docblock description goes to annotations.description", function() {
+
+ it( "docblock description should be in annotations.description", function() {
+ var ast = astFromPath( variables.testDir & "hintedParams.cfc" );
+
+ var func = findFunction( ast, "withDocblock" );
+ expect( func ).notToBeNull( "withDocblock function should be found in AST" );
+ expect( func ).toHaveKey( "docblock", "should have docblock" );
+ expect( func ).toHaveKey( "annotations", "should have annotations" );
+ expect( func.annotations ).toHaveKey( "description", "annotations should have description" );
+ expect( func.annotations.description ).toBe( "This hint comes from a docblock" );
+ });
+
+ it( "inline hint attribute should go to metadata.hint", function() {
+ var ast = astFromPath( variables.testDir & "hintedParams.cfc" );
+
+ var func = findFunction( ast, "withHintAttr" );
+ expect( func ).notToBeNull( "withHintAttr function should be found in AST" );
+ expect( func ).notToHaveKey( "docblock", "should not have docblock when hint from attribute" );
+ // Inline hint goes to metadata
+ expect( func ).toHaveKey( "metadata", "should have metadata for inline attributes" );
+ expect( func.metadata ).toHaveKey( "hint", "metadata should have hint" );
+ expect( func.metadata.hint ).toBe( "This hint comes from an attribute" );
+ });
+
+ });
+
+ describe( "LDEV-5990: Raw docblock should be preserved for round-tripping", function() {
+
+ it( "should include raw docblock text when function has docblock", function() {
+ var ast = astFromPath( variables.testDir & "hintedParams.cfc" );
+
+ var func = findFunction( ast, "withDocblock" );
+ expect( func ).notToBeNull( "withDocblock function should be found in AST" );
+ // Raw docblock should be preserved for round-trip fidelity
+ expect( func ).toHaveKey( "docblock", "AST should include raw docblock text" );
+ expect( func.docblock ).toInclude( "This hint comes from a docblock" );
+ // Must include opening and closing comment markers for valid output
+ expect( func.docblock ).toInclude( "/**", "docblock should include opening /**" );
+ expect( func.docblock ).toInclude( "*/", "docblock should include closing */" );
+ });
+
+ it( "should NOT include docblock key when no docblock exists", function() {
+ var ast = astFromPath( variables.testDir & "hintedParams.cfc" );
+
+ var func = findFunction( ast, "withHintAttr" );
+ expect( func ).notToBeNull( "withHintAttr function should be found in AST" );
+ // No docblock when hint comes from attribute only
+ expect( func ).notToHaveKey( "docblock", "AST should not have docblock when none exists" );
+ });
+
+ it( "should preserve docblock EVEN when inline hint attribute also exists", function() {
+ var ast = astFromPath( variables.testDir & "hintedParams.cfc" );
+
+ var func = findFunction( ast, "docblockPlusHint" );
+ expect( func ).notToBeNull( "docblockPlusHint function should be found in AST" );
+ // MUST preserve docblock for round-tripping even when inline hint wins
+ expect( func ).toHaveKey( "docblock", "docblock must be preserved even with inline hint" );
+ expect( func.docblock ).toInclude( "Docblock description" );
+ // Docblock description goes to annotations.description
+ expect( func ).toHaveKey( "annotations" );
+ expect( func.annotations ).toHaveKey( "description" );
+ expect( func.annotations.description ).toBe( "Docblock description" );
+ // Inline hint goes to metadata.hint
+ expect( func ).toHaveKey( "metadata" );
+ expect( func.metadata ).toHaveKey( "hint" );
+ expect( func.metadata.hint ).toBe( "Attribute hint" );
+ });
+
+ });
+
+ describe( "LDEV-5990: Docblock format variations", function() {
+
+ it( "should handle single-line docblock", function() {
+ var ast = astFromPath( variables.testDir & "docblockVariations.cfc" );
+
+ var func = findFunction( ast, "singleLineDoc" );
+ expect( func ).notToBeNull( "singleLineDoc function should be found in AST" );
+ expect( func ).toHaveKey( "docblock" );
+ expect( func.docblock ).toInclude( "Single line docblock" );
+ expect( func.docblock ).toInclude( "/**" );
+ expect( func.docblock ).toInclude( "*/" );
+ // annotations.description should have the parsed description
+ expect( func ).toHaveKey( "annotations" );
+ expect( func.annotations ).toHaveKey( "description" );
+ expect( func.annotations.description ).toBe( "Single line docblock" );
+ });
+
+ it( "should handle docblock with only @tags no description", function() {
+ var ast = astFromPath( variables.testDir & "docblockVariations.cfc" );
+
+ var func = findFunction( ast, "onlyTags" );
+ expect( func ).notToBeNull( "onlyTags function should be found in AST" );
+ // @tags go into annotations, not metadata
+ expect( func ).toHaveKey( "annotations" );
+ expect( func.annotations ).toHaveKey( "return" );
+ });
+
+ it( "should handle docblock with multiple @param tags", function() {
+ var ast = astFromPath( variables.testDir & "docblockVariations.cfc" );
+
+ var func = findFunction( ast, "multipleParams" );
+ expect( func ).notToBeNull( "multipleParams function should be found in AST" );
+ // Each param should have its hint from the docblock
+ expect( func.params[ 1 ].hint.value ).toBe( "First parameter" );
+ expect( func.params[ 2 ].hint.value ).toBe( "Second parameter" );
+ expect( func.params[ 3 ].hint.value ).toBe( "Third parameter" );
+ });
+
+ it( "should handle empty docblock (just /** */)", function() {
+ var ast = astFromPath( variables.testDir & "docblockVariations.cfc" );
+
+ var func = findFunction( ast, "emptyDocblock" );
+ expect( func ).notToBeNull( "emptyDocblock function should be found in AST" );
+ // Empty docblock should still be preserved
+ expect( func ).toHaveKey( "docblock" );
+ expect( func.docblock ).toInclude( "/**" );
+ expect( func.docblock ).toInclude( "*/" );
+ });
+
+ it( "should handle docblock with special characters", function() {
+ var ast = astFromPath( variables.testDir & "docblockVariations.cfc" );
+
+ var func = findFunction( ast, "specialChars" );
+ expect( func ).notToBeNull( "specialChars function should be found in AST" );
+ expect( func ).toHaveKey( "annotations" );
+ expect( func.annotations ).toHaveKey( "description" );
+ // Description should preserve special characters
+ expect( func.annotations.description ).toInclude( "" );
+ expect( func.annotations.description ).toInclude( "&" );
+ expect( func.annotations.description ).toInclude( """" );
+ });
+
+ it( "should handle component-level docblock", function() {
+ var ast = astFromPath( variables.testDir & "docblockVariations.cfc" );
+
+ // Component tag should have its docblock (not Program level)
+ var componentTag = ast.body[ 1 ];
+ // Script-based components use CFMLTag type
+ expect( componentTag.type ).toBe( "CFMLTag" );
+ expect( componentTag.name ).toBe( "component" );
+ expect( componentTag ).toHaveKey( "docblock" );
+ expect( componentTag.docblock ).toInclude( "Component-level docblock" );
+ expect( componentTag.docblock ).toInclude( "@author" );
+ // annotations should have description from docblock
+ expect( componentTag ).toHaveKey( "annotations" );
+ expect( componentTag.annotations ).toHaveKey( "description" );
+ expect( componentTag.annotations.description ).toBe( "Component-level docblock" );
+ });
+
+ });
+
+ describe( "LDEV-5990: Docblock should NOT attach to closure default value", function() {
+
+ it( "should attach docblock to outer function, not closure default value", function() {
+ var ast = astFromPath( variables.testDir & "hintedParams.cfc" );
+
+ var func = findFunction( ast, "withClosureDefault" );
+ expect( func ).notToBeNull( "withClosureDefault function should be found in AST" );
+
+ // The docblock should be on the OUTER function
+ expect( func ).toHaveKey( "docblock", "outer function should have docblock" );
+ expect( func.docblock ).toInclude( "@cb.hint" );
+
+ // The closure default value should NOT have the docblock
+ var param = func.params[ 1 ];
+ expect( param.name.value ).toBe( "cb" );
+ expect( param ).toHaveKey( "defaultValue", "param should have defaultValue" );
+ var closure = param.defaultValue;
+ expect( closure.type ).toBeWithCase( "ClosureDeclaration" );
+ // BUG: docblock is incorrectly attached to closure instead of outer function
+ expect( closure ).notToHaveKey( "docblock", "closure default value should NOT have docblock" );
+ expect( closure ).notToHaveKey( "annotations", "closure default value should NOT have annotations" );
+ });
+
+ it( "should attach docblock to outer function, not arrow function default value", function() {
+ // Arrow function defaults also consume the docblock incorrectly
+ var code = '/**
+* @data.hint Data description
+*/
+function tree( required struct data, formatUDF=()=>"" ){}';
+ var ast = astFromString( code, "script" );
+
+ var func = ast.body[ 1 ];
+ expect( func.type ).toBe( "FunctionDeclaration" );
+
+ // The docblock should be on the OUTER function
+ expect( func ).toHaveKey( "docblock", "outer function should have docblock when arrow function is default" );
+ expect( func.docblock ).toInclude( "@data.hint" );
+
+ // Param hint should be preserved
+ var param = func.params[ 1 ];
+ expect( param.name.value ).toBe( "data" );
+ expect( param ).toHaveKey( "hint", "param should have hint from docblock" );
+ expect( param.hint.value ).toBe( "Data description" );
+
+ // The arrow function default value should NOT have the docblock
+ var param2 = func.params[ 2 ];
+ expect( param2.name.value ).toBe( "formatUDF" );
+ expect( param2 ).toHaveKey( "defaultValue", "param should have defaultValue" );
+ var arrow = param2.defaultValue;
+ expect( arrow.type ).toBeWithCase( "LambdaDeclaration" );
+ expect( arrow ).notToHaveKey( "docblock", "arrow function default value should NOT have docblock" );
+ expect( arrow ).notToHaveKey( "annotations", "arrow function default value should NOT have annotations" );
+ });
+
+ it( "should attach docblock to outer function with expanded arrow function default", function() {
+ // Expanded arrow function form () => { return ""; } also loses docblock
+ var code = '/**
+* @data.hint Data description
+*/
+function tree( required struct data, formatUDF=() => { return ""; } ){}';
+ var ast = astFromString( code, "script" );
+
+ var func = ast.body[ 1 ];
+ expect( func.type ).toBe( "FunctionDeclaration" );
+
+ // The docblock should be on the OUTER function even with expanded arrow syntax
+ expect( func ).toHaveKey( "docblock", "outer function should have docblock when expanded arrow function is default" );
+ expect( func.docblock ).toInclude( "@data.hint" );
+
+ // Param hint should be preserved
+ var param = func.params[ 1 ];
+ expect( param.name.value ).toBe( "data" );
+ expect( param ).toHaveKey( "hint", "param should have hint from docblock" );
+ expect( param.hint.value ).toBe( "Data description" );
+ });
+
+ // SKIP: Feature enhancement - not part of original bug fix
+ xit( "should distinguish concise vs block arrow function syntax", function() {
+ // Concise form: () => expr
+ var conciseAst = astFromString( '() => ""', "script" );
+ var conciseLambda = conciseAst.body[ 1 ];
+ expect( conciseLambda.type ).toBe( "LambdaDeclaration" );
+
+ // Block form: () => { return expr; }
+ var blockAst = astFromString( '() => { return ""; }', "script" );
+ var blockLambda = blockAst.body[ 1 ];
+ expect( blockLambda.type ).toBe( "LambdaDeclaration" );
+
+ // AST should have a field to distinguish them (e.g., "expression" or "concise")
+ // Concise form should have expression=true or similar
+ expect( conciseLambda ).toHaveKey( "expression",
+ "LambdaDeclaration should have 'expression' field to distinguish () => expr from () => { return expr; }" );
+ expect( conciseLambda.expression ).toBeTrue( "Concise arrow () => expr should have expression=true" );
+
+ // Block form should have expression=false
+ expect( blockLambda ).toHaveKey( "expression" );
+ expect( blockLambda.expression ).toBeFalse( "Block arrow () => { } should have expression=false" );
+ });
+
+ });
+
+ describe( "LDEV-5990: Docblock metadata tags should be in AST", function() {
+
+ it( "should include @return tag in AST annotations", function() {
+ var ast = astFromPath( variables.testDir & "hintedParams.cfc" );
+
+ var func = findFunction( ast, "withFullDocblock" );
+ expect( func ).notToBeNull( "withFullDocblock function should be found in AST" );
+ // @return should be in annotations (docblock @tags)
+ expect( func ).toHaveKey( "annotations", "AST should include annotations from docblock" );
+ expect( func.annotations ).toHaveKey( "return", "annotations should include @return" );
+ expect( func.annotations.return ).toInclude( "A greeting message" );
+ });
+
+ it( "should include @deprecated tag in AST annotations", function() {
+ var ast = astFromPath( variables.testDir & "hintedParams.cfc" );
+
+ var func = findFunction( ast, "withFullDocblock" );
+ expect( func ).notToBeNull( "withFullDocblock function should be found in AST" );
+ // @deprecated should be in annotations (docblock @tags)
+ expect( func ).toHaveKey( "annotations", "AST should include annotations from docblock" );
+ expect( func.annotations ).toHaveKey( "deprecated", "annotations should include @deprecated" );
+ expect( func.annotations.deprecated ).toInclude( "Use greetV2 instead" );
+ });
+
+ it( "should include param hints from docblock @param tags", function() {
+ var ast = astFromPath( variables.testDir & "hintedParams.cfc" );
+
+ var func = findFunction( ast, "withFullDocblock" );
+ expect( func ).notToBeNull( "withFullDocblock function should be found in AST" );
+ // Param hints from @name and @age should be on the params
+ expect( func.params[ 1 ].hint.value ).toBe( "The name parameter description" );
+ expect( func.params[ 2 ].hint.value ).toBe( "The age parameter description" );
+ });
+
+ });
+
+ describe( "LDEV-5990: Tag-based CFC support", function() {
+
+ it( "should include hint on cfargument in tag-based CFC", function() {
+ var ast = astFromPath( variables.testDir & "tagBasedParams.cfc" );
+
+ var func = findFunction( ast, "withHint" );
+ expect( func ).notToBeNull( "withHint function should be found in AST" );
+
+ // Tag-based: cfargument is in body, hint is an attribute
+ var arg = findTagArgument( func, "name" );
+ expect( arg ).notToBeNull( "cfargument 'name' should be found" );
+ var hintAttr = getTagAttr( arg, "hint" );
+ expect( hintAttr ).notToBeNull( "cfargument should have hint attribute" );
+ expect( hintAttr.value.value ).toBe( "The user's full name" );
+ });
+
+ it( "should include default on cfargument in tag-based CFC", function() {
+ var ast = astFromPath( variables.testDir & "tagBasedParams.cfc" );
+
+ var func = findFunction( ast, "withDefault" );
+ expect( func ).notToBeNull( "withDefault function should be found in AST" );
+
+ // Tag-based: default is an attribute on cfargument
+ var arg = findTagArgument( func, "greeting" );
+ expect( arg ).notToBeNull( "cfargument 'greeting' should be found" );
+ var defaultAttr = getTagAttr( arg, "default" );
+ expect( defaultAttr ).notToBeNull( "cfargument should have default attribute" );
+ expect( defaultAttr.value.value ).toBe( "Hello" );
+ });
+
+ it( "should include hint on cffunction in tag-based CFC", function() {
+ var ast = astFromPath( variables.testDir & "tagBasedParams.cfc" );
+
+ var func = findFunction( ast, "withFuncHint" );
+ expect( func ).notToBeNull( "withFuncHint function should be found in AST" );
+
+ // Tag-based: hint is an attribute on cffunction
+ var hintAttr = getTagAttr( func, "hint" );
+ expect( hintAttr ).notToBeNull( "cffunction should have hint attribute" );
+ expect( hintAttr.value.value ).toBe( "Function hint from attribute" );
+ });
+
+ it( "should include returnFormat on remote cffunction", function() {
+ var ast = astFromPath( variables.testDir & "tagBasedParams.cfc" );
+
+ var func = findFunction( ast, "remoteFunc" );
+ expect( func ).notToBeNull( "remoteFunc function should be found in AST" );
+
+ // Tag-based: returnformat is an attribute
+ var rfAttr = getTagAttr( func, "returnformat" );
+ expect( rfAttr ).notToBeNull( "cffunction should have returnformat attribute" );
+ expect( rfAttr.value.value ).toBe( "json" );
+ });
+
+ it( "should include component-level hint from cfcomponent tag", function() {
+ var ast = astFromPath( variables.testDir & "tagBasedParams.cfc" );
+
+ // Find the cfcomponent tag in the body
+ var comp = ast.body[ 1 ];
+ expect( comp.type ).toBe( "CFMLTag" );
+ expect( comp.name ).toBe( "component" );
+
+ // Component hint is an attribute on cfcomponent
+ var hintAttr = getTagAttr( comp, "hint" );
+ expect( hintAttr ).notToBeNull( "cfcomponent should have hint attribute" );
+ expect( hintAttr.value.value ).toBe( "Tag-based component hint" );
+ });
+
+ });
+
+ describe( "LDEV-5990: Metadata should separate inline attributes from docblock annotations", function() {
+
+ it( "should include inline custom attributes in metadata", function() {
+ var ast = astFromPath( variables.testDir & "metadataSources.cfc" );
+
+ var func = findFunction( ast, "inlineAttributeOnly" );
+ expect( func ).notToBeNull( "inlineAttributeOnly function should be found in AST" );
+ // Inline attributes should be in metadata
+ expect( func ).toHaveKey( "metadata", "function should have metadata for inline attributes" );
+ expect( func.metadata ).toHaveKey( "mixin", "metadata should include mixin attribute" );
+ expect( func.metadata.mixin ).toBe( "controller" );
+ // No docblock means no annotations field
+ expect( func ).notToHaveKey( "docblock", "function without docblock should not have docblock key" );
+ });
+
+ it( "should put docblock annotations in annotations field, NOT metadata", function() {
+ var ast = astFromPath( variables.testDir & "metadataSources.cfc" );
+
+ var func = findFunction( ast, "docblockAnnotationsOnly" );
+ expect( func ).notToBeNull( "docblockAnnotationsOnly function should be found in AST" );
+ // Docblock annotations should be in annotations, NOT metadata
+ expect( func ).toHaveKey( "annotations", "function should have annotations from docblock" );
+ expect( func.annotations ).toHaveKey( "changes-only.hint", "annotations should include @changes-only.hint" );
+ expect( func.annotations[ "changes-only.hint" ] ).toBe( "Only show differences" );
+ // metadata should be empty or not exist (no inline attributes)
+ if ( structKeyExists( func, "metadata" ) ) {
+ expect( structIsEmpty( func.metadata ) ).toBeTrue( "metadata should be empty when only docblock annotations exist" );
+ }
+ });
+
+ it( "should separate inline attributes and docblock annotations when both exist", function() {
+ var ast = astFromPath( variables.testDir & "metadataSources.cfc" );
+
+ var func = findFunction( ast, "bothInlineAndDocblock" );
+ expect( func ).notToBeNull( "bothInlineAndDocblock function should be found in AST" );
+ // Inline attributes go in metadata
+ expect( func ).toHaveKey( "metadata", "function should have metadata for inline attributes" );
+ expect( func.metadata ).toHaveKey( "mixin", "metadata should include mixin" );
+ expect( func.metadata.mixin ).toBe( "model" );
+ expect( func.metadata ).toHaveKey( "changesOnly", "metadata should include changesOnly" );
+ // Docblock annotations go in annotations
+ expect( func ).toHaveKey( "annotations", "function should have annotations from docblock" );
+ expect( func.annotations ).toHaveKey( "someTag.hint", "annotations should include @someTag.hint" );
+ // Annotations should NOT be in metadata
+ expect( func.metadata ).notToHaveKey( "someTag.hint", "docblock annotations should NOT be in metadata" );
+ });
+
+ it( "should preserve raw docblock for round-tripping regardless of annotations", function() {
+ var ast = astFromPath( variables.testDir & "metadataSources.cfc" );
+
+ var func = findFunction( ast, "docblockAnnotationsOnly" );
+ expect( func ).notToBeNull( "docblockAnnotationsOnly function should be found in AST" );
+ // Raw docblock should always be preserved for round-trip
+ expect( func ).toHaveKey( "docblock", "function should have raw docblock" );
+ expect( func.docblock ).toInclude( "@changes-only.hint" );
+ expect( func.docblock ).toInclude( "@format.options" );
+ });
+
+ it( "component: should have docblock annotations in annotations field", function() {
+ var ast = astFromPath( variables.testDir & "metadataSources.cfc" );
+
+ // Component has BOTH docblock annotations AND inline attributes
+ var comp = ast.body[ 1 ];
+ expect( comp.type ).toBe( "CFMLTag" );
+ expect( comp.name ).toBe( "component" );
+ // Inline attributes are in attributes array (standard for CFMLTag)
+ expect( comp ).toHaveKey( "attributes", "component should have attributes array" );
+ expect( getTagAttrValue( comp, "displayname" ) ).toBe( "MetadataTest" );
+ expect( getTagAttrValue( comp, "singleton" ) ).toBe( "true" );
+ // Docblock annotations go in separate annotations field
+ expect( comp ).toHaveKey( "annotations", "component should have annotations from docblock" );
+ // description from docblock first line
+ expect( comp.annotations ).toHaveKey( "description", "annotations should include description" );
+ expect( comp.annotations.description ).toInclude( "Test fixture for LDEV-5990" );
+ // @tags from docblock
+ expect( comp.annotations ).toHaveKey( "author", "annotations should include @author" );
+ expect( comp.annotations.author ).toBe( "Test Author" );
+ expect( comp.annotations ).toHaveKey( "version", "annotations should include @version" );
+ });
+
+ it( "component: should NOT have annotations when no docblock", function() {
+ var ast = astFromPath( variables.testDir & "componentInlineOnly.cfc" );
+
+ var comp = ast.body[ 1 ];
+ expect( comp.type ).toBe( "CFMLTag" );
+ expect( comp.name ).toBe( "component" );
+ // Inline attributes in attributes array
+ expect( comp ).toHaveKey( "attributes", "component should have attributes" );
+ expect( getTagAttrValue( comp, "displayname" ) ).toBe( "InlineOnlyComponent" );
+ expect( getTagAttrValue( comp, "singleton" ) ).toBe( "true" );
+ expect( getTagAttrValue( comp, "accessors" ) ).toBe( "true" );
+ // No docblock means no annotations
+ expect( comp ).notToHaveKey( "annotations", "component without docblock should not have annotations" );
+ expect( comp ).notToHaveKey( "docblock", "component without docblock should not have docblock" );
+ });
+
+ });
+
+ }
+
+ /**
+ * Recursively find a FunctionDeclaration or CFMLTag function by name
+ */
+ private function findFunction( required struct node, required string name ) {
+ var nodeType = node.type ?: "";
+
+ // Script-based: FunctionDeclaration
+ if ( isSimpleValue( nodeType ) && nodeType == "FunctionDeclaration" ) {
+ var nodeName = node.name ?: "";
+ if ( isStruct( nodeName ) ) nodeName = nodeName.value ?: "";
+ if ( isSimpleValue( nodeName ) && uCase( nodeName ) == uCase( name ) ) {
+ return node;
+ }
+ }
+
+ // Tag-based: CFMLTag with name="function"
+ if ( isSimpleValue( nodeType ) && nodeType == "CFMLTag" && ( node.name ?: "" ) == "function" ) {
+ var funcName = getTagAttrValue( node, "name" );
+ if ( uCase( funcName ) == uCase( name ) ) {
+ return node;
+ }
+ }
+
+ for ( var key in node ) {
+ var val = node[ key ];
+ if ( isStruct( val ) ) {
+ var result = findFunction( val, name );
+ if ( !isNull( result ) ) return result;
+ } else if ( isArray( val ) ) {
+ for ( var item in val ) {
+ if ( isStruct( item ) ) {
+ var result = findFunction( item, name );
+ if ( !isNull( result ) ) return result;
+ }
+ }
+ }
+ }
+ return;
+ }
+
+ /**
+ * Get attribute value from a CFMLTag's attributes array
+ */
+ private string function getTagAttrValue( required struct tag, required string attrName ) {
+ var attrs = tag.attributes ?: [];
+ for ( var attr in attrs ) {
+ if ( ( attr.name ?: "" ) == attrName ) {
+ return attr.value.value ?: "";
+ }
+ }
+ return "";
+ }
+
+ /**
+ * Get attribute struct from a CFMLTag's attributes array
+ */
+ private function getTagAttr( required struct tag, required string attrName ) {
+ var attrs = tag.attributes ?: [];
+ for ( var attr in attrs ) {
+ if ( ( attr.name ?: "" ) == attrName ) {
+ return attr;
+ }
+ }
+ return;
+ }
+
+ /**
+ * Find cfargument tag by name within a cffunction
+ */
+ private function findTagArgument( required struct funcTag, required string argName ) {
+ var body = funcTag.body.body ?: [];
+ for ( var item in body ) {
+ if ( ( item.type ?: "" ) == "CFMLTag" && ( item.name ?: "" ) == "argument" ) {
+ var name = getTagAttrValue( item, "name" );
+ if ( uCase( name ) == uCase( argName ) ) {
+ return item;
+ }
+ }
+ }
+ return;
+ }
+
+}
diff --git a/test/tickets/LDEV5990/docblockVariations.cfc b/test/tickets/LDEV5990/docblockVariations.cfc
new file mode 100644
index 00000000000..786bb91c5ed
--- /dev/null
+++ b/test/tickets/LDEV5990/docblockVariations.cfc
@@ -0,0 +1,42 @@
+/**
+ * Component-level docblock
+ * @author Test Author
+ * @version 1.0
+ */
+component {
+
+ /** Single line docblock */
+ function singleLineDoc() {
+ return "single";
+ }
+
+ /**
+ * @return string The result
+ */
+ function onlyTags() {
+ return "tags only";
+ }
+
+ /**
+ * Function with multiple params
+ * @a First parameter
+ * @b Second parameter
+ * @c Third parameter
+ */
+ function multipleParams( string a, string b, string c ) {
+ return a & b & c;
+ }
+
+ /** */
+ function emptyDocblock() {
+ return "empty";
+ }
+
+ /**
+ * Contains tags & ampersands and "quotes" too
+ */
+ function specialChars() {
+ return "special";
+ }
+
+}
diff --git a/test/tickets/LDEV5990/hintedParams.cfc b/test/tickets/LDEV5990/hintedParams.cfc
new file mode 100644
index 00000000000..317625270f0
--- /dev/null
+++ b/test/tickets/LDEV5990/hintedParams.cfc
@@ -0,0 +1,61 @@
+component {
+
+ function withHint(
+ required string name hint="The user's full name"
+ ) {
+ return name;
+ }
+
+ function withDefault(
+ string greeting default="Hello"
+ ) {
+ return greeting;
+ }
+
+ function withBoth(
+ required string message hint="The message to display" default="Welcome"
+ ) {
+ return message;
+ }
+
+ /**
+ * This hint comes from a docblock
+ */
+ function withDocblock() {
+ return "docblock";
+ }
+
+ /**
+ * Function with full docblock annotations
+ * @name The name parameter description
+ * @age The age parameter description
+ * @return string A greeting message
+ * @deprecated Use greetV2 instead
+ */
+ function withFullDocblock( string name, numeric age ) {
+ return "Hello #name#, you are #age#";
+ }
+
+ function withHintAttr() hint="This hint comes from an attribute" {
+ return "attribute";
+ }
+
+ /**
+ * Docblock description
+ */
+ function docblockPlusHint() hint="Attribute hint" {
+ return "both";
+ }
+
+ remote function remoteFunc() returnformat="json" {
+ return { "status": "ok" };
+ }
+
+ /**
+ * @cb.hint Callback function hint
+ */
+ function withClosureDefault( function cb=function(){} ) {
+ return cb();
+ }
+
+}
diff --git a/test/tickets/LDEV5990/tagBasedParams.cfc b/test/tickets/LDEV5990/tagBasedParams.cfc
new file mode 100644
index 00000000000..1ce0cc3bc1a
--- /dev/null
+++ b/test/tickets/LDEV5990/tagBasedParams.cfc
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/tickets/LDEV6006.cfc b/test/tickets/LDEV6006.cfc
new file mode 100644
index 00000000000..1c2ca73d38c
--- /dev/null
+++ b/test/tickets/LDEV6006.cfc
@@ -0,0 +1,63 @@
+component extends="org.lucee.cfml.test.LuceeTestCase" labels="ast" {
+
+ variables.testDir = getDirectoryFromPath( getCurrentTemplatePath() ) & "LDEV6006/";
+
+ function run( testResults, testBox ) {
+
+ describe( "LDEV-6006: astFromString mishandles CFML comment syntax in string literals", function() {
+
+ it( "handles $"; } }';
+ var ast = astFromString( code, "script" );
+
+ // Component is at root when using script mode
+ expect( ast.body ).toHaveLength( 1,
+ "Should have single element, got #arrayLen( ast.body )# elements" );
+ expect( ast.body[1].type ).toBe( "CFMLTag",
+ "Should be CFMLTag, got #ast.body[1].type#" );
+ expect( ast.body[1].name ).toBe( "component",
+ "Should be component tag, got #ast.body[1].name#" );
+
+ // Check the string literal preserves the " ) {
+ if ( reFindNoCase( "^$", line ) ) {
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/test/tickets/LDEV6006/debugAST.cfm b/test/tickets/LDEV6006/debugAST.cfm
new file mode 100644
index 00000000000..fa2f76c3f6f
--- /dev/null
+++ b/test/tickets/LDEV6006/debugAST.cfm
@@ -0,0 +1,47 @@
+
+// Test 1: Pure script mode - works
+systemOutput( "=== TEST 1: Script mode ===", true );
+try {
+ code1 = 'x = "test $"; } }';
+ ast2 = astFromString( code2 );
+ systemOutput( "Result body length: " & arrayLen( ast2.body ), true );
+ systemOutput( "Full AST: " & serializeJSON( ast2 ), true );
+} catch( any e ) {
+ systemOutput( "ERROR: " & e.message, true );
+}
+
+// Test 3: Component WITH script mode
+systemOutput( "", true );
+systemOutput( "=== TEST 3: Component WITH script mode ===", true );
+try {
+ code3 = 'component { function test() { x = "^$"; } }';
+ ast3 = astFromString( code3, "script" );
+ systemOutput( "Result body length: " & arrayLen( ast3.body ), true );
+ systemOutput( "First body type: " & ast3.body[1].type, true );
+} catch( any e ) {
+ systemOutput( "ERROR: " & e.message, true );
+}
+
+// Test 4: What if we manually wrap in cfscript?
+systemOutput( "", true );
+systemOutput( "=== TEST 4: Manually wrapped in cfscript (tag mode) ===", true );
+try {
+ code4 = 'component { function test() { x = "^$"; } }';
+ ast4 = astFromString( code4 );
+ systemOutput( "Result body length: " & arrayLen( ast4.body ), true );
+ systemOutput( "Full AST: " & serializeJSON( ast4 ), true );
+} catch( any e ) {
+ systemOutput( "ERROR: " & e.message, true );
+}
+
diff --git a/test/tickets/LDEV6007.cfc b/test/tickets/LDEV6007.cfc
new file mode 100644
index 00000000000..4d09a33f0c8
--- /dev/null
+++ b/test/tickets/LDEV6007.cfc
@@ -0,0 +1,106 @@
+component extends="org.lucee.cfml.test.LuceeTestCase" labels="ast" {
+
+ variables.testDir = getDirectoryFromPath( getCurrentTemplatePath() ) & "LDEV6007/";
+
+ function run( testResults, testBox ) {
+
+ describe( "LDEV-6007: Component inside cfscript becomes sibling instead of child", function() {
+
+ it( "should parse cfscript-wrapped component", function() {
+ // This is the pattern used by many Lucee test files
+ var ast = astFromPath( variables.testDir & "cfscriptWrapped.cfc" );
+
+ expect( ast.type ).toBe( "Program" );
+ expect( ast.body ).toHaveLength( 1, "Should have single body element, got #arrayLen( ast.body )#" );
+ expect( ast.body[1].name ).toBe( "component",
+ "Component should be at root, got: #ast.body[1].type# / #ast.body[1].name ?: 'no name'#" );
+ });
+
+ it( "should parse cfscript-wrapped component with leading comment", function() {
+ var ast = astFromPath( variables.testDir & "cfscriptWithComment.cfc" );
+
+ expect( ast.type ).toBe( "Program" );
+ // Should have comment + component (unwrapped from cfscript)
+ expect( arrayLen( ast.body ) ).toBeGTE( 1, "Expected at least 1 body element" );
+
+ // Find the component in the body
+ var comp = ast.body.filter( function( item ) {
+ return ( item.type ?: "" ) == "CFMLTag" && ( item.name ?: "" ) == "component";
+ });
+ expect( comp ).toHaveLength( 1, "Should have component in body" );
+ });
+
+ it( "should parse abstract component in cfscript", function() {
+ var ast = astFromPath( variables.testDir & "abstractCfscript.cfc" );
+
+ expect( ast.type ).toBe( "Program" );
+ expect( ast.body ).toHaveLength( 1 );
+ expect( ast.body[1].name ).toBe( "component" );
+ });
+
+ it( "should parse LuceeTestCase pattern in cfscript", function() {
+ var ast = astFromPath( variables.testDir & "luceeTestCase.cfc" );
+
+ expect( ast.type ).toBe( "Program" );
+ expect( ast.body ).toHaveLength( 1 );
+ expect( ast.body[1].name ).toBe( "component" );
+ });
+
+ it( "should return component at root for script-based .cfc files", function() {
+ // After fixing Quirk #22, astFromPath on .cfc files should return
+ // the component directly at root, without cfscript wrapper
+ var ast = astFromPath( variables.testDir & "scriptComponent.cfc" );
+
+ expect( ast.type ).toBe( "Program" );
+ expect( ast.body ).toBeArray();
+ expect( ast.body ).toHaveLength( 1, "Should have single body element" );
+
+ // Component should be directly at root, not wrapped in cfscript
+ expect( ast.body[1].type ).toBe( "CFMLTag" );
+ expect( ast.body[1].name ).toBe( "component",
+ "Component should be at root, got: #ast.body[1].name#" );
+ });
+
+ it( "should have function inside the component body", function() {
+ var ast = astFromPath( variables.testDir & "scriptComponent.cfc" );
+
+ // The component should contain the function
+ var component = ast.body[1];
+ expect( component.name ).toBe( "component" );
+
+ // Find function in component body
+ var funcDecl = component.body.body.filter( function( item ) {
+ return ( item.type ?: "" ) == "FunctionDeclaration";
+ });
+ expect( funcDecl ).toHaveLength( 1, "Component should contain bar function" );
+ });
+
+ });
+
+ }
+
+ /**
+ * Recursively find a tag by name
+ */
+ private function findTagByName( required struct node, required string tagName ) {
+ if ( ( node.type ?: "" ) == "CFMLTag" && ( node.name ?: "" ) == arguments.tagName ) {
+ return node;
+ }
+ for ( var key in node ) {
+ var val = node[ key ];
+ if ( isStruct( val ) ) {
+ var result = findTagByName( val, arguments.tagName );
+ if ( !isNull( result ) ) return result;
+ } else if ( isArray( val ) ) {
+ for ( var item in val ) {
+ if ( isStruct( item ) ) {
+ var result = findTagByName( item, arguments.tagName );
+ if ( !isNull( result ) ) return result;
+ }
+ }
+ }
+ }
+ return;
+ }
+
+}
diff --git a/test/tickets/LDEV6007/abstractCfscript.cfc b/test/tickets/LDEV6007/abstractCfscript.cfc
new file mode 100644
index 00000000000..7b6d42d4002
--- /dev/null
+++ b/test/tickets/LDEV6007/abstractCfscript.cfc
@@ -0,0 +1,5 @@
+
+abstract component {
+ abstract function test();
+}
+
diff --git a/test/tickets/LDEV6007/cfscriptWithComment.cfc b/test/tickets/LDEV6007/cfscriptWithComment.cfc
new file mode 100644
index 00000000000..1da54b7c49d
--- /dev/null
+++ b/test/tickets/LDEV6007/cfscriptWithComment.cfc
@@ -0,0 +1,10 @@
+
+
+component {
+ function test() {
+ return "hello";
+ }
+}
+
diff --git a/test/tickets/LDEV6007/cfscriptWrapped.cfc b/test/tickets/LDEV6007/cfscriptWrapped.cfc
new file mode 100644
index 00000000000..82d00905af3
--- /dev/null
+++ b/test/tickets/LDEV6007/cfscriptWrapped.cfc
@@ -0,0 +1,7 @@
+
+component {
+ function test() {
+ return "hello";
+ }
+}
+
diff --git a/test/tickets/LDEV6007/luceeTestCase.cfc b/test/tickets/LDEV6007/luceeTestCase.cfc
new file mode 100644
index 00000000000..f5c1e164d5a
--- /dev/null
+++ b/test/tickets/LDEV6007/luceeTestCase.cfc
@@ -0,0 +1,11 @@
+
+component extends="org.lucee.cfml.test.LuceeTestCase" {
+ function run( testResults, testBox ) {
+ describe( "test", function() {
+ it( "works", function() {
+ expect( true ).toBe( true );
+ });
+ });
+ }
+}
+
diff --git a/test/tickets/LDEV6007/scriptComponent.cfc b/test/tickets/LDEV6007/scriptComponent.cfc
new file mode 100644
index 00000000000..cda8daa27ee
--- /dev/null
+++ b/test/tickets/LDEV6007/scriptComponent.cfc
@@ -0,0 +1,3 @@
+component extends="foo" {
+ function bar() {}
+}
diff --git a/test/tickets/LDEV6008.cfc b/test/tickets/LDEV6008.cfc
new file mode 100644
index 00000000000..f66d85e8c1c
--- /dev/null
+++ b/test/tickets/LDEV6008.cfc
@@ -0,0 +1,84 @@
+component extends="org.lucee.cfml.test.LuceeTestCase" labels="ast" {
+
+ function run( testResults, testBox ) {
+
+ describe( "LDEV-6008: PreserveSingleQuotes evaluated in cfquery AST", function() {
+
+ it( "should preserve PreserveSingleQuotes function call in cfquery body", function() {
+ // Use a literal string argument so it doesn't try to resolve a variable
+ var code = '##PreserveSingleQuotes( "test" )##';
+ var ast = astFromString( code );
+
+ expect( ast.body ).toHaveLength( 1 );
+ expect( ast.body[1].type ).toBe( "CFMLTag" );
+ expect( ast.body[1].name ).toBe( "query" );
+
+ // The body should contain an ExpressionStatement (interpolation)
+ var body = ast.body[1].body;
+ expect( body.body ).toBeArray();
+ expect( body.body ).toHaveLength( 1, "Should have one expression" );
+
+ var stmt = body.body[1];
+ expect( stmt.type ).toBe( "ExpressionStatement" );
+
+ var expr = stmt.expression;
+ // Expression is wrapped in CastExpression (string cast), CallExpression is inside argument
+ expect( expr.type ).toBe( "CastExpression" );
+ expect( expr.argument.type ).toBe( "CallExpression",
+ "Expected CallExpression inside CastExpression but got #expr.argument.type# - PreserveSingleQuotes was evaluated during AST generation" );
+ expect( expr.argument.callee.name ).toBe( "preservesinglequotes",
+ "Function name should be preservesinglequotes" );
+ });
+
+ it( "should not evaluate expressions during AST generation", function() {
+ // This demonstrates that PreserveSingleQuotes is being EXECUTED during parsing
+ // Even though we're just generating an AST, Lucee evaluates the function
+ var code = '##PreserveSingleQuotes( "original" )##';
+ var ast = astFromString( code );
+
+ var body = ast.body[1].body;
+ var stmt = body.body[1];
+ var expr = stmt.expression;
+
+ // If the bug is present, expr will be StringLiteral with value="original"
+ // If fixed, expr should be CastExpression with argument.type="CallExpression"
+ if ( expr.type == "StringLiteral" ) {
+ fail( "BUG: PreserveSingleQuotes was evaluated during AST generation. " &
+ "Expression reduced to StringLiteral '#expr.value#' instead of CallExpression" );
+ }
+
+ expect( expr.type ).toBe( "CastExpression" );
+ expect( expr.argument.type ).toBe( "CallExpression" );
+ expect( expr.argument.callee.name ).toBe( "preservesinglequotes" );
+ });
+
+ it( "preserves nested PreserveSingleQuotes when wrapped in another function", function() {
+ // When PreserveSingleQuotes is wrapped in trim(), it's preserved
+ // This test verifies that nested calls work
+ var code = '##trim( PreserveSingleQuotes( "test" ) )##';
+ var ast = astFromString( code );
+
+ var body = ast.body[1].body;
+ var stmt = body.body[1];
+ var expr = stmt.expression;
+
+ // Expression is wrapped in CastExpression
+ expect( expr.type ).toBe( "CastExpression" );
+
+ // The outer trim() should be there as CallExpression inside the cast
+ var trimCall = expr.argument;
+ expect( trimCall.type ).toBe( "CallExpression" );
+ expect( trimCall.callee.name ).toBe( "trim" );
+
+ // The argument to trim() should be CallExpression (PreserveSingleQuotes)
+ var innerArg = trimCall.arguments[1];
+ expect( innerArg.type ).toBe( "CallExpression",
+ "Inner PreserveSingleQuotes should be CallExpression, got #innerArg.type#" );
+ expect( innerArg.callee.name ).toBe( "preservesinglequotes" );
+ });
+
+ });
+
+ }
+
+}
diff --git a/test/tickets/LDEV6008/dumpAST.cfm b/test/tickets/LDEV6008/dumpAST.cfm
new file mode 100644
index 00000000000..ed19d8409ec
--- /dev/null
+++ b/test/tickets/LDEV6008/dumpAST.cfm
@@ -0,0 +1,43 @@
+
+testDir = getDirectoryFromPath( getCurrentTemplatePath() );
+
+systemOutput( "=== QUERY WITH PSQ ===", true );
+ast = astFromPath( testDir & "queryWithPSQ.cfm" );
+
+systemOutput( "=== LDEV-6008 AST DUMP ===", true );
+systemOutput( serializeJSON( ast, "struct" ), true );
+systemOutput( "=== END ===", true );
+
+// Drill into the structure
+systemOutput( "", true );
+systemOutput( "Body length: " & arrayLen( ast.body ), true );
+if ( arrayLen( ast.body ) > 0 ) {
+ queryTag = ast.body[1];
+ systemOutput( "Tag type: " & queryTag.type, true );
+ systemOutput( "Tag name: " & ( queryTag.name ?: "n/a" ), true );
+
+ if ( structKeyExists( queryTag, "body" ) && structKeyExists( queryTag.body, "body" ) ) {
+ body = queryTag.body.body;
+ systemOutput( "Body items: " & arrayLen( body ), true );
+ for ( item in body ) {
+ systemOutput( " Item type: " & item.type, true );
+ if ( structKeyExists( item, "expression" ) ) {
+ expr = item.expression;
+ systemOutput( " Expression type: " & expr.type, true );
+ systemOutput( " Expression keys: " & structKeyList( expr ), true );
+ if ( structKeyExists( expr, "callee" ) ) {
+ systemOutput( " Callee name: " & ( expr.callee.name ?: "n/a" ), true );
+ }
+ if ( structKeyExists( expr, "expression" ) ) {
+ systemOutput( " Inner expression type: " & expr.expression.type, true );
+ }
+ }
+ }
+ }
+}
+
+systemOutput( "", true );
+systemOutput( "=== REGULAR OUTPUT WITH PSQ ===", true );
+ast2 = astFromPath( testDir & "regularOutput.cfm" );
+systemOutput( serializeJSON( ast2, "struct" ), true );
+
diff --git a/test/tickets/LDEV6008/queryWithPSQ.cfm b/test/tickets/LDEV6008/queryWithPSQ.cfm
new file mode 100644
index 00000000000..cd5131001cd
--- /dev/null
+++ b/test/tickets/LDEV6008/queryWithPSQ.cfm
@@ -0,0 +1 @@
+#PreserveSingleQuotes( "test" )#
diff --git a/test/tickets/LDEV6008/regularOutput.cfm b/test/tickets/LDEV6008/regularOutput.cfm
new file mode 100644
index 00000000000..733c4c21de5
--- /dev/null
+++ b/test/tickets/LDEV6008/regularOutput.cfm
@@ -0,0 +1 @@
+#PreserveSingleQuotes( "test" )#
diff --git a/test/tickets/LDEV6009.cfc b/test/tickets/LDEV6009.cfc
new file mode 100644
index 00000000000..1019fdbc3d0
--- /dev/null
+++ b/test/tickets/LDEV6009.cfc
@@ -0,0 +1,67 @@
+component extends="org.lucee.cfml.test.LuceeTestCase" labels="ast" {
+
+ function run( testResults, testBox ) {
+
+ describe( "LDEV-6009: AST should include compiler metadata", function() {
+
+ it( "should include sourceType='script' when parsed as script", function() {
+ var code = 'x = 1;';
+ var ast = astFromString( code, "script" );
+
+ expect( ast.type ).toBe( "Program" );
+ expect( ast ).toHaveKey( "sourceType" );
+ expect( ast.sourceType ).toBe( "script" );
+ });
+
+ it( "should include sourceType='tag' when parsed as tag", function() {
+ var code = '';
+ var ast = astFromString( code );
+
+ expect( ast.type ).toBe( "Program" );
+ expect( ast ).toHaveKey( "sourceType" );
+ expect( ast.sourceType ).toBe( "tag" );
+ });
+
+ it( "should include dotNotationUpperCase metadata", function() {
+ var code = 'x = 1;';
+ var ast = astFromString( code, "script" );
+
+ expect( ast.type ).toBe( "Program" );
+ expect( ast ).toHaveKey( "dotNotationUpperCase" );
+ expect( ast.dotNotationUpperCase ).toBeBoolean();
+ });
+
+ it( "should include handleUnquotedAttrValueAsString metadata", function() {
+ var code = '';
+ var ast = astFromString( code );
+
+ expect( ast.type ).toBe( "Program" );
+ expect( ast ).toHaveKey( "handleUnquotedAttrValueAsString" );
+ expect( ast.handleUnquotedAttrValueAsString ).toBeBoolean();
+ });
+
+ it( "should include all metadata in astFromPath results", function() {
+ var testDir = getDirectoryFromPath( getCurrentTemplatePath() ) & "LDEV6009/";
+ var ast = astFromPath( testDir & "test.cfc" );
+
+ expect( ast.type ).toBe( "Program" );
+ expect( ast ).toHaveKey( "sourceType" );
+ expect( ast ).toHaveKey( "dotNotationUpperCase" );
+ expect( ast ).toHaveKey( "handleUnquotedAttrValueAsString" );
+ });
+
+ it( "should return sourceType='script' for script component with tag islands", function() {
+ var testDir = getDirectoryFromPath( getCurrentTemplatePath() ) & "LDEV6009/";
+ var ast = astFromPath( testDir & "tagIsland.cfc" );
+
+ expect( ast.type ).toBe( "Program" );
+ expect( ast ).toHaveKey( "sourceType" );
+ // Script component should be "script" even if it contains tag islands
+ expect( ast.sourceType ).toBe( "script" );
+ });
+
+ });
+
+ }
+
+}
diff --git a/test/tickets/LDEV6009/tagIsland.cfc b/test/tickets/LDEV6009/tagIsland.cfc
new file mode 100644
index 00000000000..2a779160206
--- /dev/null
+++ b/test/tickets/LDEV6009/tagIsland.cfc
@@ -0,0 +1,8 @@
+component {
+ function test() {
+ ```
+
+ ```
+ return x;
+ }
+}
diff --git a/test/tickets/LDEV6009/test.cfc b/test/tickets/LDEV6009/test.cfc
new file mode 100644
index 00000000000..18c1f950c89
--- /dev/null
+++ b/test/tickets/LDEV6009/test.cfc
@@ -0,0 +1,5 @@
+component {
+ function foo() {
+ return 1;
+ }
+}
diff --git a/test/tickets/LDEV6011.cfc b/test/tickets/LDEV6011.cfc
new file mode 100644
index 00000000000..77973e91a8a
--- /dev/null
+++ b/test/tickets/LDEV6011.cfc
@@ -0,0 +1,348 @@
+component extends="org.lucee.cfml.test.LuceeTestCase" labels="ast" {
+
+ function run( testResults, testBox ) {
+
+ describe( "LDEV-6011: AST should use proper node types for literals, not internal functions", function() {
+
+ it( "should represent empty struct literal as ObjectExpression not _literalstruct", function() {
+ var code = 'x = {};';
+ var ast = astFromString( code, "script" );
+
+ expect( ast.body ).toHaveLength( 1 );
+ var assignment = ast.body[1];
+ expect( assignment.type ).toBe( "AssignmentExpression" );
+
+ // The right side should be ObjectExpression, not a CallExpression to _literalstruct
+ var right = assignment.right;
+ expect( right.type ).toBe( "ObjectExpression",
+ "Expected ObjectExpression but got #right.type#" &
+ ( right.type == "CallExpression" ? " with callee #right.callee.name ?: 'unknown'#" : "" ) );
+ });
+
+ it( "should represent empty array literal as ArrayExpression not _literalarray", function() {
+ var code = 'x = [];';
+ var ast = astFromString( code, "script" );
+
+ expect( ast.body ).toHaveLength( 1 );
+ var assignment = ast.body[1];
+ expect( assignment.type ).toBe( "AssignmentExpression" );
+
+ // The right side should be ArrayExpression, not a CallExpression to _literalarray
+ var right = assignment.right;
+ expect( right.type ).toBe( "ArrayExpression",
+ "Expected ArrayExpression but got #right.type#" &
+ ( right.type == "CallExpression" ? " with callee #right.callee.name ?: 'unknown'#" : "" ) );
+ });
+
+ it( "should represent struct literal with values as ObjectExpression", function() {
+ var code = 'x = { foo: "bar", num: 123 };';
+ var ast = astFromString( code, "script" );
+
+ expect( ast.body ).toHaveLength( 1 );
+ var right = ast.body[1].right;
+ expect( right.type ).toBe( "ObjectExpression",
+ "Expected ObjectExpression but got #right.type#" );
+ });
+
+ it( "should represent array literal with values as ArrayExpression", function() {
+ var code = 'x = [ 1, 2, 3 ];';
+ var ast = astFromString( code, "script" );
+
+ expect( ast.body ).toHaveLength( 1 );
+ var right = ast.body[1].right;
+ expect( right.type ).toBe( "ArrayExpression",
+ "Expected ArrayExpression but got #right.type#" );
+ });
+
+ it( "should represent new Component() as NewExpression not _createcomponent", function() {
+ var code = 'x = new MyComponent();';
+ var ast = astFromString( code, "script" );
+
+ expect( ast.body ).toHaveLength( 1 );
+ var right = ast.body[1].right;
+ expect( right.type ).toBe( "NewExpression",
+ "Expected NewExpression but got #right.type#" &
+ ( right.type == "CallExpression" ? " with callee #right.callee.name ?: 'unknown'#" : "" ) );
+ });
+
+ it( "should represent ordered struct literal as ObjectExpression with ordered flag", function() {
+ var code = 'x = [ foo=1, bar=2 ];';
+ var ast = astFromString( code, "script" );
+
+ expect( ast.body ).toHaveLength( 1 );
+ var right = ast.body[1].right;
+ expect( right.type ).toBe( "ObjectExpression",
+ "Expected ObjectExpression but got #right.type#" &
+ ( right.type == "CallExpression" ? " with callee #right.callee.name ?: 'unknown'#" : "" ) );
+ expect( right.ordered ).toBe( true, "Expected ordered=true for ordered struct literal" );
+ });
+
+ it( "should preserve all properties in struct with 3+ keys", function() {
+ var code = 'x = { a: 1, b: 2, c: 3 };';
+ var ast = astFromString( code, "script" );
+
+ expect( ast.body ).toHaveLength( 1 );
+ var right = ast.body[1].right;
+ expect( right.type ).toBe( "ObjectExpression" );
+ expect( right.properties ).toHaveLength( 3,
+ "Expected 3 properties but got #arrayLen( right.properties )#" );
+ });
+
+ it( "should preserve component name in NewExpression with positional args", function() {
+ var code = 'x = new MyComponent( arg1 );';
+ var ast = astFromString( code, "script" );
+
+ expect( ast.body ).toHaveLength( 1 );
+ var right = ast.body[1].right;
+ expect( right.type ).toBe( "NewExpression" );
+ // Callee should be the component name, not the first argument
+ expect( right.callee.type ).toBe( "StringLiteral",
+ "Expected callee to be StringLiteral but got #right.callee.type#" );
+ expect( right.callee.value ).toBe( "MyComponent",
+ "Expected callee value 'MyComponent' but got '#right.callee.value ?: 'null'#'" );
+ // Arguments should contain the positional arg
+ expect( right.arguments ).toHaveLength( 1,
+ "Expected 1 argument but got #arrayLen( right.arguments )#" );
+ });
+
+ it( "should preserve component name in NewExpression with named args", function() {
+ var code = 'x = new MyComponent( name="test" );';
+ var ast = astFromString( code, "script" );
+
+ expect( ast.body ).toHaveLength( 1 );
+ var right = ast.body[1].right;
+ expect( right.type ).toBe( "NewExpression" );
+ // Callee should be the component name, not a NamedArgument
+ expect( right.callee.type ).toBe( "StringLiteral",
+ "Expected callee to be StringLiteral but got #right.callee.type#" );
+ expect( right.callee.value ).toBe( "MyComponent",
+ "Expected callee value 'MyComponent' but got '#right.callee.value ?: 'null'#'" );
+ // Arguments should contain the named arg
+ expect( right.arguments ).toHaveLength( 1,
+ "Expected 1 argument but got #arrayLen( right.arguments )#" );
+ });
+
+ it( "should not include phantom type:undefined argument in NewExpression", function() {
+ var code = 'x = new MyComponent();';
+ var ast = astFromString( code, "script" );
+
+ expect( ast.body ).toHaveLength( 1 );
+ var right = ast.body[1].right;
+ expect( right.type ).toBe( "NewExpression" );
+ // No constructor args means empty arguments array
+ expect( right.arguments ).toHaveLength( 0,
+ "Expected 0 arguments but got #arrayLen( right.arguments )#" &
+ ( arrayLen( right.arguments ) > 0 ? " - first arg: #right.arguments[1].value ?: right.arguments[1].type#" : "" ) );
+ });
+
+ it( "should distinguish colon separator from equals separator in struct properties", function() {
+ var colonCode = 'x = { a : 1 };';
+ var equalsCode = 'x = { a = 1 };';
+
+ var colonAst = astFromString( colonCode, "script" );
+ var equalsAst = astFromString( equalsCode, "script" );
+
+ var colonProp = colonAst.body[1].right.properties[1];
+ var equalsProp = equalsAst.body[1].right.properties[1];
+
+ // Both should be Property type
+ expect( colonProp.type ).toBe( "Property" );
+ expect( equalsProp.type ).toBe( "Property" );
+
+ // Should have a separator field to distinguish them
+ expect( colonProp ).toHaveKey( "separator",
+ "Property should have 'separator' field to indicate colon vs equals" );
+ expect( equalsProp ).toHaveKey( "separator",
+ "Property should have 'separator' field to indicate colon vs equals" );
+
+ // Colon syntax should have separator=":"
+ expect( colonProp.separator ).toBe( ":",
+ "Colon syntax { a : 1 } should have separator=':' but got '#colonProp.separator ?: 'null'#'" );
+
+ // Equals syntax should have separator="="
+ expect( equalsProp.separator ).toBe( "=",
+ "Equals syntax { a = 1 } should have separator='=' but got '#equalsProp.separator ?: 'null'#'" );
+ });
+
+ it( "should preserve mixed separators in same struct", function() {
+ // CFML allows mixing colon and equals in the same struct literal
+ var code = 'x = { a : 1, b = 2, c : 3 };';
+ var ast = astFromString( code, "script" );
+
+ var props = ast.body[1].right.properties;
+ expect( props ).toHaveLength( 3 );
+
+ // First property uses colon
+ expect( props[1] ).toHaveKey( "separator" );
+ expect( props[1].separator ).toBe( ":",
+ "First property 'a : 1' should have separator=':' but got '#props[1].separator ?: 'null'#'" );
+
+ // Second property uses equals
+ expect( props[2] ).toHaveKey( "separator" );
+ expect( props[2].separator ).toBe( "=",
+ "Second property 'b = 2' should have separator='=' but got '#props[2].separator ?: 'null'#'" );
+
+ // Third property uses colon
+ expect( props[3] ).toHaveKey( "separator" );
+ expect( props[3].separator ).toBe( ":",
+ "Third property 'c : 3' should have separator=':' but got '#props[3].separator ?: 'null'#'" );
+ });
+
+ it( "should distinguish colon separator from equals separator in named function arguments", function() {
+ var colonCode = 'myFunc( zac: 1, micha: 2 );';
+ var equalsCode = 'myFunc( zac=1, micha=2 );';
+
+ var colonAst = astFromString( colonCode, "script" );
+ var equalsAst = astFromString( equalsCode, "script" );
+
+ var colonArgs = colonAst.body[1].arguments;
+ var equalsArgs = equalsAst.body[1].arguments;
+
+ // Both should be NamedArgument type
+ expect( colonArgs[1].type ).toBe( "NamedArgument" );
+ expect( equalsArgs[1].type ).toBe( "NamedArgument" );
+
+ // Should have a separator field to distinguish them
+ expect( colonArgs[1] ).toHaveKey( "separator",
+ "NamedArgument should have 'separator' field to indicate colon vs equals" );
+ expect( equalsArgs[1] ).toHaveKey( "separator",
+ "NamedArgument should have 'separator' field to indicate colon vs equals" );
+
+ // Colon syntax should have separator=":"
+ expect( colonArgs[1].separator ).toBe( ":",
+ "Colon syntax myFunc( zac: 1 ) should have separator=':' but got '#colonArgs[1].separator ?: 'null'#'" );
+
+ // Equals syntax should have separator="="
+ expect( equalsArgs[1].separator ).toBe( "=",
+ "Equals syntax myFunc( zac=1 ) should have separator='=' but got '#equalsArgs[1].separator ?: 'null'#'" );
+ });
+
+ it( "should preserve mixed separators in same function call", function() {
+ // CFML allows mixing colon and equals in the same function call
+ var code = 'myFunc( a: 1, b=2, c: 3 );';
+ var ast = astFromString( code, "script" );
+
+ var args = ast.body[1].arguments;
+ expect( args ).toHaveLength( 3 );
+
+ // First arg uses colon
+ expect( args[1] ).toHaveKey( "separator" );
+ expect( args[1].separator ).toBe( ":",
+ "First arg 'a: 1' should have separator=':' but got '#args[1].separator ?: 'null'#'" );
+
+ // Second arg uses equals
+ expect( args[2] ).toHaveKey( "separator" );
+ expect( args[2].separator ).toBe( "=",
+ "Second arg 'b=2' should have separator='=' but got '#args[2].separator ?: 'null'#'" );
+
+ // Third arg uses colon
+ expect( args[3] ).toHaveKey( "separator" );
+ expect( args[3].separator ).toBe( ":",
+ "Third arg 'c: 3' should have separator=':' but got '#args[3].separator ?: 'null'#'" );
+ });
+
+ it( "should represent Class::method() as static MemberExpression not _getstaticscope", function() {
+ var code = 'x = MyClass::staticMethod( arg );';
+ var ast = astFromString( code, "script" );
+
+ expect( ast.body ).toHaveLength( 1 );
+ var right = ast.body[1].right;
+ expect( right.type ).toBe( "CallExpression" );
+
+ // The callee should be a MemberExpression with static=true
+ var callee = right.callee;
+ expect( callee.type ).toBe( "MemberExpression",
+ "Expected MemberExpression but got #callee.type#" );
+ expect( callee ).toHaveKey( "static",
+ "MemberExpression should have 'static' field for :: syntax" );
+ expect( callee.static ).toBe( true,
+ "Expected static=true for :: syntax but got #callee.static ?: 'null'#" );
+
+ // The object should be the class name, not _getstaticscope call
+ expect( callee.object.type ).toBe( "Identifier",
+ "Expected object to be Identifier but got #callee.object.type#" &
+ ( callee.object.type == "CallExpression" ? " with callee #callee.object.callee.name ?: 'unknown'#" : "" ) );
+ expect( callee.object.name ).toBe( "MyClass",
+ "Expected object name 'MyClass' but got '#callee.object.name ?: 'null'#'" );
+
+ // The property should be the method name
+ expect( callee.property.name ).toBe( "staticMethod",
+ "Expected property name 'staticMethod' but got '#callee.property.name ?: 'null'#'" );
+ });
+
+ it( "should represent super::method() as static MemberExpression not _getsuperstaticscope", function() {
+ var code = 'x = super::parentMethod();';
+ var ast = astFromString( code, "script" );
+
+ expect( ast.body ).toHaveLength( 1 );
+ var right = ast.body[1].right;
+ expect( right.type ).toBe( "CallExpression" );
+
+ // The callee should be a MemberExpression with static=true
+ var callee = right.callee;
+ expect( callee.type ).toBe( "MemberExpression" );
+ expect( callee ).toHaveKey( "static" );
+ expect( callee.static ).toBe( true );
+
+ // The object should be "super" identifier, not _getsuperstaticscope call
+ expect( callee.object.type ).toBe( "Identifier",
+ "Expected object to be Identifier but got #callee.object.type#" &
+ ( callee.object.type == "CallExpression" ? " with callee #callee.object.callee.name ?: 'unknown'#" : "" ) );
+ expect( callee.object.name ).toBe( "super",
+ "Expected object name 'super' but got '#callee.object.name ?: 'null'#'" );
+
+ // The property should be the method name
+ expect( callee.property.name ).toBe( "parentMethod" );
+ });
+
+ it( "should represent Class::CONSTANT as static MemberExpression", function() {
+ var code = 'x = MyClass::CONSTANT;';
+ var ast = astFromString( code, "script" );
+
+ expect( ast.body ).toHaveLength( 1 );
+ var right = ast.body[1].right;
+
+ // Direct static property access (not a CallExpression)
+ expect( right.type ).toBe( "MemberExpression",
+ "Expected MemberExpression but got #right.type#" );
+ expect( right ).toHaveKey( "static" );
+ expect( right.static ).toBe( true );
+
+ // The object should be the class name
+ expect( right.object.type ).toBe( "Identifier",
+ "Expected object to be Identifier but got #right.object.type#" &
+ ( right.object.type == "CallExpression" ? " with callee #right.object.callee.name ?: 'unknown'#" : "" ) );
+ expect( right.object.name ).toBe( "MyClass" );
+
+ // The property should be the constant name
+ expect( right.property.name ).toBe( "CONSTANT" );
+ });
+
+ it( "should include separator field in CFMLTag Attribute nodes", function() {
+ // CFMLTag uses Attribute nodes (not NamedArgument), which should also have separator
+ // Note: throw (type: "test") with colon is parsed as CallExpression, not CFMLTag
+ // Only throw (type="test") with equals is parsed as CFMLTag
+ var equalsCode = 'throw ( type="test" );';
+
+ var equalsAst = astFromString( equalsCode, "script" );
+
+ // Should be CFMLTag with Attribute children
+ expect( equalsAst.body[1].type ).toBe( "CFMLTag" );
+
+ var equalsAttr = equalsAst.body[1].attributes[1];
+ expect( equalsAttr.type ).toBe( "Attribute" );
+
+ // Should have a separator field
+ expect( equalsAttr ).toHaveKey( "separator",
+ "Attribute should have 'separator' field like NamedArgument does" );
+
+ // Equals syntax should have separator="="
+ expect( equalsAttr.separator ).toBe( "=",
+ "Equals syntax throw ( type='test' ) should have separator='=' but got '#equalsAttr.separator ?: 'null'#'" );
+ });
+
+ });
+
+ }
+
+}
diff --git a/test/tickets/LDEV6011/StaticClass.cfc b/test/tickets/LDEV6011/StaticClass.cfc
new file mode 100644
index 00000000000..7c07e7ff147
--- /dev/null
+++ b/test/tickets/LDEV6011/StaticClass.cfc
@@ -0,0 +1,11 @@
+component {
+
+ static {
+ static.CONSTANT = "static_constant_value";
+ }
+
+ public static function staticMethod( required string arg ) {
+ return "called with: " & arguments.arg;
+ }
+
+}
diff --git a/test/tickets/LDEV6011/dotCall.cfm b/test/tickets/LDEV6011/dotCall.cfm
new file mode 100644
index 00000000000..faa5c2f6bbd
--- /dev/null
+++ b/test/tickets/LDEV6011/dotCall.cfm
@@ -0,0 +1,3 @@
+
+result = StaticClass.staticMethod( "test" );
+
diff --git a/test/tickets/LDEV6011/dotProperty.cfm b/test/tickets/LDEV6011/dotProperty.cfm
new file mode 100644
index 00000000000..8051f84c435
--- /dev/null
+++ b/test/tickets/LDEV6011/dotProperty.cfm
@@ -0,0 +1,3 @@
+
+result = StaticClass.CONSTANT;
+
diff --git a/test/tickets/LDEV6011/doubleColonCall.cfm b/test/tickets/LDEV6011/doubleColonCall.cfm
new file mode 100644
index 00000000000..f70120bc344
--- /dev/null
+++ b/test/tickets/LDEV6011/doubleColonCall.cfm
@@ -0,0 +1,3 @@
+
+result = StaticClass::staticMethod( "test" );
+
diff --git a/test/tickets/LDEV6011/doubleColonProperty.cfm b/test/tickets/LDEV6011/doubleColonProperty.cfm
new file mode 100644
index 00000000000..50be2ab0eb8
--- /dev/null
+++ b/test/tickets/LDEV6011/doubleColonProperty.cfm
@@ -0,0 +1,3 @@
+
+result = StaticClass::CONSTANT;
+
diff --git a/test/tickets/LDEV6013.cfc b/test/tickets/LDEV6013.cfc
new file mode 100644
index 00000000000..ae1d21069d6
--- /dev/null
+++ b/test/tickets/LDEV6013.cfc
@@ -0,0 +1,83 @@
+component extends="org.lucee.cfml.test.LuceeTestCase" labels="ast" {
+
+ function run( testResults, testBox ) {
+
+ describe( "LDEV-6013: AST MemberExpression should have computed=true for bracket notation", function() {
+
+ it( "should have computed=true for bracket notation with string key", function() {
+ var code = 'x = obj[ "key" ];';
+ var ast = astFromString( code, "script" );
+
+ expect( ast.body ).toHaveLength( 1 );
+ var right = ast.body[1].right;
+ expect( right.type ).toBe( "MemberExpression" );
+ expect( right.computed ).toBe( true,
+ "Expected computed=true for bracket notation but got computed=#right.computed#" );
+ });
+
+ it( "should have computed=true for bracket notation with special characters in key", function() {
+ var code = 'x = this.mappings[ "/tests" ];';
+ var ast = astFromString( code, "script" );
+
+ expect( ast.body ).toHaveLength( 1 );
+ var right = ast.body[1].right;
+ expect( right.type ).toBe( "MemberExpression" );
+ expect( right.computed ).toBe( true,
+ "Expected computed=true for bracket notation with special chars but got computed=#right.computed#" );
+ // Property should be StringLiteral, not Identifier
+ expect( right.property.type ).toBe( "StringLiteral",
+ "Expected property to be StringLiteral but got #right.property.type#" );
+ });
+
+ it( "should have computed=false for dot notation", function() {
+ var code = 'x = obj.key;';
+ var ast = astFromString( code, "script" );
+
+ expect( ast.body ).toHaveLength( 1 );
+ var right = ast.body[1].right;
+ expect( right.type ).toBe( "MemberExpression" );
+ expect( right.computed ).toBe( false,
+ "Expected computed=false for dot notation but got computed=#right.computed#" );
+ // Property should be Identifier for dot notation
+ expect( right.property.type ).toBe( "Identifier",
+ "Expected property to be Identifier but got #right.property.type#" );
+ });
+
+ it( "should have computed=true for bracket notation with variable key", function() {
+ var code = 'x = obj[ idx ];';
+ var ast = astFromString( code, "script" );
+
+ expect( ast.body ).toHaveLength( 1 );
+ var right = ast.body[1].right;
+ expect( right.type ).toBe( "MemberExpression" );
+ expect( right.computed ).toBe( true,
+ "Expected computed=true for bracket notation with variable but got computed=#right.computed#" );
+ });
+
+ it( "should have computed=true for bracket notation with numeric key", function() {
+ var code = 'x = arr[ 1 ];';
+ var ast = astFromString( code, "script" );
+
+ expect( ast.body ).toHaveLength( 1 );
+ var right = ast.body[1].right;
+ expect( right.type ).toBe( "MemberExpression" );
+ expect( right.computed ).toBe( true,
+ "Expected computed=true for numeric index but got computed=#right.computed#" );
+ });
+
+ it( "should preserve bracket notation on assignment left side", function() {
+ var code = 'this.mappings[ "/tests" ] = "foo";';
+ var ast = astFromString( code, "script" );
+
+ expect( ast.body ).toHaveLength( 1 );
+ var left = ast.body[1].left;
+ expect( left.type ).toBe( "MemberExpression" );
+ expect( left.computed ).toBe( true,
+ "Expected computed=true on left side of assignment but got computed=#left.computed#" );
+ });
+
+ });
+
+ }
+
+}
diff --git a/test/tickets/LDEV6014.cfc b/test/tickets/LDEV6014.cfc
new file mode 100644
index 00000000000..212042260fd
--- /dev/null
+++ b/test/tickets/LDEV6014.cfc
@@ -0,0 +1,76 @@
+component extends="org.lucee.cfml.test.LuceeTestCase" labels="ast" {
+
+ function run( testResults, testBox ) {
+
+ var h = chr( 35 ); // hash character
+
+ describe( "LDEV-6014: AST should preserve dynamic property assignment with interpolated string key", function() {
+
+ it( "should produce AssignmentExpression for interpolated string key assignment", function() {
+ // Code: "#scope#.#name#" = value;
+ var code = '"#h#scope#h#.#h#name#h#" = value;';
+ var ast = astFromString( code, "script" );
+
+ expect( ast.body ).toHaveLength( 1 );
+ var stmt = ast.body[1];
+ expect( stmt.type ).toBe( "AssignmentExpression",
+ "Expected AssignmentExpression but got #stmt.type#" );
+ });
+
+ it( "should have left side as interpolated string, not MemberExpression", function() {
+ var code = '"#h#scope#h#.#h#name#h#" = value;';
+ var ast = astFromString( code, "script" );
+
+ expect( ast.body ).toHaveLength( 1 );
+ var stmt = ast.body[1];
+ expect( stmt.type ).toBe( "AssignmentExpression" );
+ // Left side should be the interpolated string expression
+ expect( stmt.left.type ).notToBe( "MemberExpression",
+ "Left side should be interpolated string, not MemberExpression" );
+ });
+
+ it( "should have right side as the assigned value", function() {
+ var code = '"#h#scope#h#.#h#name#h#" = value;';
+ var ast = astFromString( code, "script" );
+
+ expect( ast.body ).toHaveLength( 1 );
+ var stmt = ast.body[1];
+ expect( stmt.type ).toBe( "AssignmentExpression" );
+ expect( stmt.right.type ).toBe( "Identifier",
+ "Expected right side Identifier but got #stmt.right.type#" );
+ expect( stmt.right.name ).toBe( "VALUE",
+ "Expected right side name 'VALUE' but got '#stmt.right.name ?: 'null'#'" );
+ });
+
+ it( "should not have operator on MemberExpression (MockBox pattern)", function() {
+ // Code: "#arguments.propertyScope#.#arguments.propertyName#" = arguments.mock;
+ var code = '"#h#arguments.propertyScope#h#.#h#arguments.propertyName#h#" = arguments.mock;';
+ var ast = astFromString( code, "script" );
+
+ expect( ast.body ).toHaveLength( 1 );
+ var stmt = ast.body[1];
+ // If it's incorrectly a MemberExpression, it shouldn't have operator
+ if ( stmt.type == "MemberExpression" ) {
+ expect( structKeyExists( stmt, "operator" ) ).toBe( false,
+ "MemberExpression should not have operator key but has operator=#stmt.operator ?: 'null'#" );
+ }
+ });
+
+ it( "should handle realistic dynamic property pattern from MockBox", function() {
+ // Code: "#arguments.propertyScope#.#arguments.propertyName#" = arguments.mock;
+ var code = '"#h#arguments.propertyScope#h#.#h#arguments.propertyName#h#" = arguments.mock;';
+ var ast = astFromString( code, "script" );
+
+ expect( ast.body ).toHaveLength( 1 );
+ var stmt = ast.body[1];
+ expect( stmt.type ).toBe( "AssignmentExpression",
+ "Expected AssignmentExpression but got #stmt.type#" );
+ expect( stmt.right.type ).toBe( "MemberExpression",
+ "Expected right side MemberExpression but got #stmt.right.type#" );
+ });
+
+ });
+
+ }
+
+}
diff --git a/test/tickets/LDEV6015.cfc b/test/tickets/LDEV6015.cfc
new file mode 100644
index 00000000000..0107a2f8a55
--- /dev/null
+++ b/test/tickets/LDEV6015.cfc
@@ -0,0 +1,347 @@
+component extends="org.lucee.cfml.test.LuceeTestCase" labels="ast" {
+
+ variables.testDir = getDirectoryFromPath( getCurrentTemplatePath() ) & "LDEV6015/";
+
+ function run( testResults, testBox ) {
+
+ describe( "LDEV-6015: AST StringLiteral should include quoteChar", function() {
+
+ it( "should preserve double quote in script", function() {
+ var ast = astFromPath( variables.testDir & "scriptDouble.cfc" );
+ // Component body -> first statement is assignment
+ var comp = ast.body[1];
+ var assignment = comp.body.body[1];
+ var strLiteral = assignment.right;
+
+ expect( strLiteral.type ).toBe( "StringLiteral" );
+ expect( strLiteral.value ).toBe( "hello" );
+ expect( strLiteral ).toHaveKey( "quoteChar", "StringLiteral should have quoteChar field" );
+ expect( strLiteral.quoteChar ).toBe( '"', "Should preserve double quote" );
+ });
+
+ it( "should preserve single quote in script", function() {
+ var ast = astFromPath( variables.testDir & "scriptSingle.cfc" );
+ var comp = ast.body[1];
+ var assignment = comp.body.body[1];
+ var strLiteral = assignment.right;
+
+ expect( strLiteral.type ).toBe( "StringLiteral" );
+ expect( strLiteral.value ).toBe( "hello" );
+ expect( strLiteral ).toHaveKey( "quoteChar", "StringLiteral should have quoteChar field" );
+ expect( strLiteral.quoteChar ).toBe( "'", "Should preserve single quote" );
+ });
+
+ it( "should preserve double quote in tag attribute", function() {
+ var ast = astFromPath( variables.testDir & "doubleQuote.cfm" );
+ var setTag = ast.body[1];
+ expect( setTag.type ).toBe( "CFMLTag" );
+ expect( setTag.name ).toBe( "set" );
+ // cfset has "noname" attribute with AssignmentExpression value
+ var assignment = setTag.attributes[1].value;
+ expect( assignment.type ).toBe( "AssignmentExpression" );
+ var strLiteral = assignment.right;
+ expect( strLiteral.type ).toBe( "StringLiteral" );
+ expect( strLiteral ).toHaveKey( "quoteChar", "StringLiteral should have quoteChar field" );
+ expect( strLiteral.quoteChar ).toBe( '"' );
+ });
+
+ it( "should preserve single quote in tag attribute", function() {
+ var ast = astFromPath( variables.testDir & "singleQuote.cfm" );
+ var setTag = ast.body[1];
+ expect( setTag.type ).toBe( "CFMLTag" );
+ expect( setTag.name ).toBe( "set" );
+ // cfset has "noname" attribute with AssignmentExpression value
+ var assignment = setTag.attributes[1].value;
+ expect( assignment.type ).toBe( "AssignmentExpression" );
+ var strLiteral = assignment.right;
+ expect( strLiteral.type ).toBe( "StringLiteral" );
+ expect( strLiteral ).toHaveKey( "quoteChar", "StringLiteral should have quoteChar field" );
+ expect( strLiteral.quoteChar ).toBe( "'" );
+ });
+
+ it( "should preserve single quote in TemplateLiteral (interpolated string)", function() {
+ var ast = astFromPath( variables.testDir & "templateLiteralSingle.cfc" );
+ var comp = ast.body[1];
+ var assignment = comp.body.body[1];
+ var templateLiteral = assignment.right;
+
+ expect( templateLiteral.type ).toBe( "TemplateLiteral" );
+ expect( templateLiteral ).toHaveKey( "quoteChar", "TemplateLiteral should have quoteChar field like StringLiteral" );
+ expect( templateLiteral.quoteChar ).toBe( "'", "Should preserve single quote for interpolated string" );
+ });
+
+ it( "should preserve quoteChar in inline function param hint", function() {
+ // Inline param hints (hint="...") should have quoteChar
+ var ast = astFromString( 'function test( string name hint="my description" ) {}', "script" );
+ var param = ast.body[1].params[1];
+ expect( param.hint.type ).toBe( "StringLiteral" );
+ expect( param.hint.value ).toBe( "my description" );
+ expect( param.hint ).toHaveKey( "quoteChar", "inline param hint should have quoteChar" );
+ expect( param.hint.quoteChar ).toBe( '"', "Should preserve double quote in param hint" );
+ });
+
+ it( "should NOT have quoteChar for docblock param hint", function() {
+ // Docblock @param hints are unquoted in source, so no quoteChar
+ var code = '/**' & chr(10) &
+ ' * @name The name param' & chr(10) &
+ ' */' & chr(10) &
+ 'function test( string name ) {}';
+ var ast = astFromString( code, "script" );
+ var param = ast.body[1].params[1];
+ expect( param.hint.type ).toBe( "StringLiteral" );
+ expect( param.hint.value ).toBe( "The name param" );
+ // Docblock hints should NOT have quoteChar since they're unquoted in source
+ expect( param.hint ).notToHaveKey( "quoteChar", "docblock param hint should not have quoteChar" );
+ });
+
+ it( "should NOT have quoteChar for unquoted numeric attribute coerced to StringLiteral", function() {
+ // When width=100 is parsed, Lucee coerces it to StringLiteral
+ // But since the original source had no quotes, quoteChar should NOT be set
+ // The raw field shows quotes but that's just the string representation, not source-level info
+ var ast = astFromPath( variables.testDir & "unquotedNumericAttr.cfc" );
+ var comp = ast.body[1];
+ var cfimageTag = comp.body.body[1];
+ expect( cfimageTag.type ).toBe( "CFMLTag" );
+ expect( cfimageTag.name ).toBe( "image" );
+
+ // Find the width attribute
+ var widthAttr = cfimageTag.attributes.filter( function( a ) { return a.name == "width"; } )[1];
+ expect( widthAttr ).toBeStruct( "width attribute should exist" );
+ expect( widthAttr.value.type ).toBe( "StringLiteral", "unquoted numeric should become StringLiteral" );
+ expect( widthAttr.value.value ).toBe( "100" );
+ // Since original source was unquoted, quoteChar should NOT be present
+ expect( widthAttr.value ).notToHaveKey( "quoteChar", "StringLiteral from unquoted source should not have quoteChar" );
+ });
+
+ it( "should preserve quoteChar for bracket notation string key with double quotes", function() {
+ // struct["key"] - the string key should have quoteChar
+ var ast = astFromString( 'x = myStruct["myKey"];', "script" );
+ var assignment = ast.body[1];
+ var memberExpr = assignment.right;
+
+ expect( memberExpr.type ).toBe( "MemberExpression" );
+ expect( memberExpr.computed ).toBeTrue( "bracket notation should be computed" );
+ expect( memberExpr.property.type ).toBe( "StringLiteral" );
+ expect( memberExpr.property.value ).toBe( "myKey" );
+ expect( memberExpr.property ).toHaveKey( "quoteChar", "bracket notation string key should have quoteChar" );
+ expect( memberExpr.property.quoteChar ).toBe( '"', "Should preserve double quote in bracket notation" );
+ });
+
+ it( "should preserve quoteChar for bracket notation string key with single quotes", function() {
+ // struct['key'] - the string key should have quoteChar
+ var ast = astFromString( "x = myStruct['myKey'];", "script" );
+ var assignment = ast.body[1];
+ var memberExpr = assignment.right;
+
+ expect( memberExpr.type ).toBe( "MemberExpression" );
+ expect( memberExpr.computed ).toBeTrue( "bracket notation should be computed" );
+ expect( memberExpr.property.type ).toBe( "StringLiteral" );
+ expect( memberExpr.property.value ).toBe( "myKey" );
+ expect( memberExpr.property ).toHaveKey( "quoteChar", "bracket notation string key should have quoteChar" );
+ expect( memberExpr.property.quoteChar ).toBe( "'", "Should preserve single quote in bracket notation" );
+ });
+
+ it( "should preserve quoteChar for bracket notation with hyphenated key", function() {
+ // struct["hyphen-key"] - common pattern, must have quoteChar to round-trip
+ var ast = astFromString( 'x = headers["X-CSRF-Token"];', "script" );
+ var assignment = ast.body[1];
+ var memberExpr = assignment.right;
+
+ expect( memberExpr.type ).toBe( "MemberExpression" );
+ expect( memberExpr.computed ).toBeTrue( "bracket notation should be computed" );
+ expect( memberExpr.property.type ).toBe( "StringLiteral" );
+ expect( memberExpr.property.value ).toBe( "X-CSRF-Token" );
+ expect( memberExpr.property ).toHaveKey( "quoteChar", "hyphenated bracket key must have quoteChar for round-trip" );
+ expect( memberExpr.property.quoteChar ).toBe( '"' );
+ });
+
+ it( "should preserve quoteChar for nested bracket notation", function() {
+ // struct["a"]["b"] - both keys should have quoteChar
+ var ast = astFromString( 'x = data["level1"]["level2"];', "script" );
+ var assignment = ast.body[1];
+ var outerMember = assignment.right;
+
+ expect( outerMember.type ).toBe( "MemberExpression" );
+ expect( outerMember.property.type ).toBe( "StringLiteral" );
+ expect( outerMember.property.value ).toBe( "level2" );
+ expect( outerMember.property ).toHaveKey( "quoteChar", "outer bracket key should have quoteChar" );
+
+ var innerMember = outerMember.object;
+ expect( innerMember.type ).toBe( "MemberExpression" );
+ expect( innerMember.property.type ).toBe( "StringLiteral" );
+ expect( innerMember.property.value ).toBe( "level1" );
+ expect( innerMember.property ).toHaveKey( "quoteChar", "inner bracket key should have quoteChar" );
+ });
+
+ });
+
+ describe( "Break/Continue label quoteChar", function() {
+
+ // SKIP: break label uses CastExpression, continue uses StringLiteral - Lucee AST inconsistency
+ xit( "should use consistent AST type for break and continue labels", function() {
+ // Both break and continue should use same AST type for unquoted labels
+ var breakAst = astFromString( "outer: for( i = 1; i <= 5; i++ ) { break outer; }", "script" );
+ var continueAst = astFromString( "outer: for( i = 1; i <= 5; i++ ) { continue outer; }", "script" );
+
+ var breakStmt = breakAst.body[1].body.body[1];
+ var continueStmt = continueAst.body[1].body.body[1];
+
+ var breakLabel = breakStmt.attributes.filter( function( a ) { return a.name == "label"; } )[1];
+ var continueLabel = continueStmt.attributes.filter( function( a ) { return a.name == "label"; } )[1];
+
+ // Both should have same AST type for the label value
+ expect( breakLabel.value.type ).toBe( continueLabel.value.type,
+ "break and continue should use consistent AST type for unquoted labels (break=#breakLabel.value.type#, continue=#continueLabel.value.type#)" );
+ });
+
+ it( "should NOT have quoteChar for unquoted continue label", function() {
+ // continue outer; - unquoted label should NOT have quoteChar
+ var ast = astFromString( "outer: for( i = 1; i <= 5; i++ ) { continue outer; }", "script" );
+ var forStmt = ast.body[1];
+ var continueStmt = forStmt.body.body[1];
+
+ expect( continueStmt.type ).toBe( "CFMLTag" );
+ expect( continueStmt.name ).toBe( "continue" );
+
+ var labelAttr = continueStmt.attributes.filter( function( a ) { return a.name == "label"; } )[1];
+ expect( labelAttr ).toBeStruct( "continue should have label attribute" );
+ expect( labelAttr.value.type ).toBe( "StringLiteral" );
+ expect( labelAttr.value.value ).toBe( "outer" );
+ // Unquoted label should NOT have quoteChar
+ expect( labelAttr.value ).notToHaveKey( "quoteChar", "unquoted continue label should not have quoteChar" );
+ });
+
+ });
+
+ describe( "Function returntype attribute quoteChar", function() {
+
+ it( "should have quoteChar for quoted returntype attribute", function() {
+ // function test() returntype="boolean" {} - quoted attribute should have quoteChar
+ var ast = astFromString( 'function test() returntype="boolean" {}', "script" );
+ var func = ast.body[1];
+
+ expect( func.type ).toBe( "FunctionDeclaration" );
+ expect( func.returnType.type ).toBe( "StringLiteral" );
+ expect( func.returnType.value ).toBe( "boolean" );
+ expect( func.returnType ).toHaveKey( "quoteChar", "quoted returntype attribute should have quoteChar" );
+ expect( func.returnType.quoteChar ).toBe( '"' );
+ });
+
+ it( "should NOT have quoteChar for inline returntype keyword", function() {
+ // boolean function test() {} - inline keyword is NOT an attribute
+ var ast = astFromString( 'boolean function test() {}', "script" );
+ var func = ast.body[1];
+
+ expect( func.type ).toBe( "FunctionDeclaration" );
+ expect( func.returnType.type ).toBe( "StringLiteral" );
+ expect( func.returnType.value ).toBe( "boolean" );
+ // Inline keyword should NOT have quoteChar - it's not quoted in source
+ expect( func.returnType ).notToHaveKey( "quoteChar", "inline returntype keyword should not have quoteChar" );
+ });
+
+ it( "should have position for unquoted returntype attribute (distinguishes from keyword)", function() {
+ // function test() returntype=boolean {} - attribute syntax has position data
+ // boolean function test() {} - keyword syntax has NO position data
+ // Position presence distinguishes attribute from keyword, not quoteChar
+ var ast = astFromString( 'function test() returntype=boolean {}', "script" );
+ var func = ast.body[1];
+
+ expect( func.type ).toBe( "FunctionDeclaration" );
+ // Attribute syntax should have position data (distinguishes from keyword)
+ expect( func.returnType ).toHaveKey( "start", "unquoted returntype attribute should have position (distinguishes from keyword)" );
+ });
+
+ });
+
+ describe( "Param shorthand type attribute", function() {
+
+ it( "should have StringLiteral WITHOUT position for shorthand type", function() {
+ // param struct e; - shorthand type produces synthetic StringLiteral (no position)
+ var ast = astFromString( 'param struct e;', "script" );
+ var paramTag = ast.body[1];
+
+ expect( paramTag.type ).toBe( "CFMLTag" );
+ expect( paramTag.name ).toBe( "param" );
+
+ var typeAttr = paramTag.attributes.filter( function( a ) { return a.name == "type"; } )[1];
+ expect( typeAttr ).toBeStruct( "param should have type attribute" );
+ expect( typeAttr.value.type ).toBe( "StringLiteral" );
+ expect( typeAttr.value.value ).toBe( "struct" );
+ // Shorthand type is synthetic - no position info
+ expect( typeAttr.value ).notToHaveKey( "start", "shorthand type should NOT have position (synthetic node)" );
+ });
+
+ it( "should have CastExpression for unquoted explicit type=", function() {
+ // param e type=struct; - unquoted explicit type parses as CastExpression
+ var ast = astFromString( 'param e type=struct;', "script" );
+ var paramTag = ast.body[1];
+
+ expect( paramTag.type ).toBe( "CFMLTag" );
+ expect( paramTag.name ).toBe( "param" );
+
+ var typeAttr = paramTag.attributes.filter( function( a ) { return a.name == "type"; } )[1];
+ expect( typeAttr ).toBeStruct( "param should have type attribute" );
+ // Unquoted type= parses as variable reference (CastExpression wrapping Identifier)
+ expect( typeAttr.value.type ).toBe( "CastExpression" );
+ expect( typeAttr.value.argument.type ).toBe( "Identifier" );
+ expect( typeAttr.value.argument.name ).toBe( "STRUCT" );
+ });
+
+ it( "should have StringLiteral WITH quoteChar for quoted explicit type=", function() {
+ // param e type="struct"; - quoted explicit type parses as StringLiteral
+ var ast = astFromString( 'param e type="struct";', "script" );
+ var paramTag = ast.body[1];
+
+ expect( paramTag.type ).toBe( "CFMLTag" );
+ expect( paramTag.name ).toBe( "param" );
+
+ var typeAttr = paramTag.attributes.filter( function( a ) { return a.name == "type"; } )[1];
+ expect( typeAttr ).toBeStruct( "param should have type attribute" );
+ expect( typeAttr.value.type ).toBe( "StringLiteral" );
+ expect( typeAttr.value.value ).toBe( "struct" );
+ // Quoted type= has quoteChar
+ expect( typeAttr.value ).toHaveKey( "quoteChar", "quoted type attribute should have quoteChar" );
+ expect( typeAttr.value.quoteChar ).toBe( '"' );
+ // And has position (real source node)
+ expect( typeAttr.value ).toHaveKey( "start", "quoted type should have position" );
+ });
+
+ });
+
+ describe( "Param name attribute quoteChar", function() {
+
+ xit( "should have quoteChar for param name attribute (shorthand syntax)", function() {
+ // param url = {}; - shorthand syntax, name should have quoteChar
+ var ast = astFromString( 'param url = {};', "script" );
+ var paramTag = ast.body[1];
+
+ expect( paramTag.type ).toBe( "CFMLTag" );
+ expect( paramTag.name ).toBe( "param" );
+
+ var nameAttr = paramTag.attributes.filter( function( a ) { return a.name == "name"; } )[1];
+ expect( nameAttr ).toBeStruct( "param should have name attribute" );
+ expect( nameAttr.value.type ).toBe( "StringLiteral" );
+ expect( nameAttr.value.value ).toBe( "url" );
+ // Param name needs quoteChar for round-trip - otherwise re-parses as variable reference
+ expect( nameAttr.value ).toHaveKey( "quoteChar", "param name attribute needs quoteChar for round-trip" );
+ });
+
+ xit( "should have quoteChar for param name attribute (explicit name= syntax)", function() {
+ // param name="myVar" default=""; - explicit syntax
+ var ast = astFromString( 'param name="myVar" default="";', "script" );
+ var paramTag = ast.body[1];
+
+ expect( paramTag.type ).toBe( "CFMLTag" );
+ expect( paramTag.name ).toBe( "param" );
+
+ var nameAttr = paramTag.attributes.filter( function( a ) { return a.name == "name"; } )[1];
+ expect( nameAttr ).toBeStruct( "param should have name attribute" );
+ expect( nameAttr.value.type ).toBe( "StringLiteral" );
+ expect( nameAttr.value.value ).toBe( "myVar" );
+ expect( nameAttr.value ).toHaveKey( "quoteChar", "explicit param name attribute should have quoteChar" );
+ });
+
+ });
+ }
+
+}
diff --git a/test/tickets/LDEV6015/doubleQuote.cfm b/test/tickets/LDEV6015/doubleQuote.cfm
new file mode 100644
index 00000000000..728fba1e6c5
--- /dev/null
+++ b/test/tickets/LDEV6015/doubleQuote.cfm
@@ -0,0 +1 @@
+
diff --git a/test/tickets/LDEV6015/scriptDouble.cfc b/test/tickets/LDEV6015/scriptDouble.cfc
new file mode 100644
index 00000000000..9d6a31af566
--- /dev/null
+++ b/test/tickets/LDEV6015/scriptDouble.cfc
@@ -0,0 +1,3 @@
+component {
+ x = "hello";
+}
diff --git a/test/tickets/LDEV6015/scriptSingle.cfc b/test/tickets/LDEV6015/scriptSingle.cfc
new file mode 100644
index 00000000000..556f4d513b8
--- /dev/null
+++ b/test/tickets/LDEV6015/scriptSingle.cfc
@@ -0,0 +1,3 @@
+component {
+ x = 'hello';
+}
diff --git a/test/tickets/LDEV6015/singleQuote.cfm b/test/tickets/LDEV6015/singleQuote.cfm
new file mode 100644
index 00000000000..c977561db77
--- /dev/null
+++ b/test/tickets/LDEV6015/singleQuote.cfm
@@ -0,0 +1 @@
+
diff --git a/test/tickets/LDEV6015/templateLiteralSingle.cfc b/test/tickets/LDEV6015/templateLiteralSingle.cfc
new file mode 100644
index 00000000000..e71a349149f
--- /dev/null
+++ b/test/tickets/LDEV6015/templateLiteralSingle.cfc
@@ -0,0 +1,3 @@
+component {
+ x = 'test="#value#"';
+}
diff --git a/test/tickets/LDEV6015/unquotedNumericAttr.cfc b/test/tickets/LDEV6015/unquotedNumericAttr.cfc
new file mode 100644
index 00000000000..382f0071f74
--- /dev/null
+++ b/test/tickets/LDEV6015/unquotedNumericAttr.cfc
@@ -0,0 +1,3 @@
+component {
+ cfimage( action="resize", width=100 );
+}
diff --git a/test/tickets/LDEV6016.cfc b/test/tickets/LDEV6016.cfc
new file mode 100644
index 00000000000..2c3bccfc37c
--- /dev/null
+++ b/test/tickets/LDEV6016.cfc
@@ -0,0 +1,171 @@
+component extends="org.lucee.cfml.test.LuceeTestCase" labels="ast" {
+
+ variables.testDir = getDirectoryFromPath( getCurrentTemplatePath() ) & "LDEV6016/";
+
+ function run( testResults, testBox ) {
+
+ describe( "LDEV-6016: AST static blocks should have static marker", function() {
+
+ it( "should mark static block with static=true", function() {
+ var ast = astFromPath( variables.testDir & "staticBlock.cfc" );
+ var comp = ast.body[1];
+ // Find the static block in the component body
+ var staticBlock = comp.body.body.filter( function( s ) {
+ return ( s.type == "BlockStatement" || s.type == "CFMLTag" ) && structKeyExists( s, "static" );
+ });
+ expect( staticBlock ).toHaveLength( 1, "Should have one static block" );
+ expect( staticBlock[1].static ).toBe( true, "Static block should have static=true" );
+ });
+
+ it( "should preserve final keyword on assignment inside static block", function() {
+ var ast = astFromPath( variables.testDir & "staticBlock.cfc" );
+ var comp = ast.body[1];
+ // Find static block
+ var staticBlocks = comp.body.body.filter( function( s ) {
+ return structKeyExists( s, "static" ) && s.static == true;
+ });
+ expect( staticBlocks ).toHaveLength( 1, "Should have one static block" );
+ // Find the assignment inside
+ var body = staticBlocks[1].body;
+ var assignment = body[1];
+ expect( assignment.type ).toBe( "AssignmentExpression" );
+ expect( assignment ).toHaveKey( "final", "Assignment should have final marker" );
+ expect( assignment.final ).toBe( true, "Assignment should be marked final" );
+ });
+
+ it( "should NOT merge multiple static blocks", function() {
+ var ast = astFromPath( variables.testDir & "multipleStaticBlocks.cfc" );
+ var comp = ast.body[1];
+
+ // Find static blocks
+ var staticBlocks = comp.body.body.filter( function( s ) {
+ return structKeyExists( s, "static" ) && s.static == true;
+ });
+
+ // Should have 3 separate static blocks:
+ // - static { static1=1; }
+ // - static { static2=2; }
+ // - static function foo() (wrapped in cfstatic)
+ expect( staticBlocks ).toHaveLength( 3, "Should preserve separate static blocks" );
+
+ // Verify each has the expected content
+ expect( staticBlocks[1].body ).toHaveLength( 1, "First static block should have 1 statement" );
+ expect( staticBlocks[2].body ).toHaveLength( 1, "Second static block should have 1 statement" );
+ expect( staticBlocks[3].body ).toHaveLength( 1, "Third static block (function) should have 1 statement" );
+ expect( staticBlocks[3].body[1].type ).toBe( "FunctionDeclaration", "Third block should contain the function" );
+ });
+
+ it( "should NOT merge script static block with tag cffunction modifier=static", function() {
+ var ast = astFromPath( variables.testDir & "mixedStaticTagScript.cfc" );
+ var comp = ast.body[1];
+
+ // Find static blocks
+ var staticBlocks = comp.body.body.filter( function( s ) {
+ return structKeyExists( s, "static" ) && s.static == true;
+ });
+
+ // Should have 2 separate static blocks:
+ // - static { static1=1; } (script)
+ // - cffunction modifier="static" (tag)
+ expect( staticBlocks ).toHaveLength( 2, "Should preserve separate static blocks (script and tag)" );
+
+ // First should be the script static block with AssignmentExpression
+ expect( staticBlocks[1].body ).toHaveLength( 1, "Script static block should have 1 statement" );
+ expect( staticBlocks[1].body[1].type ).toBe( "AssignmentExpression", "Should be assignment" );
+
+ // Second should be the cffunction tag
+ expect( staticBlocks[2].body ).toHaveLength( 1, "Tag static block should have 1 statement" );
+ expect( staticBlocks[2].body[1].type ).toBe( "CFMLTag", "Should be CFMLTag (cffunction)" );
+ expect( staticBlocks[2].body[1].fullname ).toBe( "cffunction", "Should be cffunction tag" );
+ });
+
+ });
+
+ describe( "LDEV-6016: Static block position in AST", function() {
+
+ it( "should preserve tag-style static function position relative to other members", function() {
+ // Source: cfproperty (line 2), static cffunction (line 4-6), cffunction init (line 8)
+ var ast = astFromPath( variables.testDir & "staticTagFunctionPosition.cfc" );
+ var comp = ast.body[1];
+
+ // Filter to meaningful nodes (skip whitespace/expression statements)
+ var members = comp.body.body.filter( function( s ) {
+ return s.type == "CFMLTag" || ( s.type == "BlockStatement" && structKeyExists( s, "static" ) );
+ });
+
+ // Should be: cfproperty, static function wrapper, cffunction init (in source order)
+ expect( members ).toHaveLength( 3, "Should have 3 members (property, static function, init function)" );
+
+ // First should be cfproperty
+ expect( members[1].type ).toBe( "CFMLTag" );
+ expect( members[1].name ).toBe( "property" );
+
+ // Second should be the static function (wrapped in BlockStatement)
+ // OR the cffunction directly if not wrapped
+ if ( members[2].type == "BlockStatement" ) {
+ expect( members[2].static ).toBe( true, "Static function wrapper should be at original position (after property)" );
+ // The function inside should be getVersion
+ var innerFunc = members[2].body.filter( function( n ) { return n.type == "CFMLTag"; })[1];
+ var nameAttr = innerFunc.attributes.filter( function( a ) { return a.name == "name"; })[1];
+ expect( nameAttr.value.value ).toBe( "getVersion", "Static function getVersion should be first" );
+ } else {
+ // If cffunction with static marker
+ expect( members[2].name ).toBe( "function" );
+ var nameAttr = members[2].attributes.filter( function( a ) { return a.name == "name"; })[1];
+ expect( nameAttr.value.value ).toBe( "getVersion", "Static function getVersion should be first" );
+ }
+
+ // Third should be cffunction init
+ if ( members[3].type == "BlockStatement" ) {
+ // Bug: init was hoisted, static is at end - this is WRONG
+ fail( "Bug: init function should be third, not hoisted to second position" );
+ }
+ expect( members[3].type ).toBe( "CFMLTag" );
+ expect( members[3].name ).toBe( "function" );
+ var nameAttr3 = members[3].attributes.filter( function( a ) { return a.name == "name"; })[1];
+ expect( nameAttr3.value.value ).toBe( "init", "init function should be last" );
+ });
+
+ it( "should preserve static block position relative to other members", function() {
+ // Source: cfproperty (line 2), cfstatic (line 4-6), cffunction (line 8)
+ var ast = astFromPath( variables.testDir & "staticBlockPosition.cfc" );
+ var comp = ast.body[1];
+
+ // Filter to meaningful nodes (skip whitespace/expression statements)
+ var members = comp.body.body.filter( function( s ) {
+ return s.type == "CFMLTag" || ( s.type == "BlockStatement" && structKeyExists( s, "static" ) );
+ });
+
+ // Should be: cfproperty, static BlockStatement, cffunction (in source order)
+ expect( members ).toHaveLength( 3, "Should have 3 members (property, static, function)" );
+
+ // First should be cfproperty
+ expect( members[1].type ).toBe( "CFMLTag" );
+ expect( members[1].name ).toBe( "property" );
+
+ // Second should be static block (at original position)
+ expect( members[2].type ).toBe( "BlockStatement" );
+ expect( members[2].static ).toBe( true, "Static block should be at original position" );
+
+ // Third should be cffunction
+ expect( members[3].type ).toBe( "CFMLTag" );
+ expect( members[3].name ).toBe( "function" );
+ });
+
+ it( "static block should have start position info", function() {
+ var ast = astFromPath( variables.testDir & "staticBlockPosition.cfc" );
+ var comp = ast.body[1];
+
+ var staticBlocks = comp.body.body.filter( function( s ) {
+ return s.type == "BlockStatement" && structKeyExists( s, "static" ) && s.static == true;
+ });
+
+ expect( staticBlocks ).toHaveLength( 1 );
+ expect( staticBlocks[1] ).toHaveKey( "start", "Static block should have start position" );
+ expect( staticBlocks[1].start.line ).toBe( 4, "Static block should start at line 4" );
+ });
+
+ });
+ }
+
+}
diff --git a/test/tickets/LDEV6016/mixedStaticTagScript.cfc b/test/tickets/LDEV6016/mixedStaticTagScript.cfc
new file mode 100644
index 00000000000..3b505f39e46
--- /dev/null
+++ b/test/tickets/LDEV6016/mixedStaticTagScript.cfc
@@ -0,0 +1,11 @@
+
+
+ static {
+ static1=1;
+ }
+
+
+
+
+
+
diff --git a/test/tickets/LDEV6016/multipleStaticBlocks.cfc b/test/tickets/LDEV6016/multipleStaticBlocks.cfc
new file mode 100644
index 00000000000..9f061cf89de
--- /dev/null
+++ b/test/tickets/LDEV6016/multipleStaticBlocks.cfc
@@ -0,0 +1,5 @@
+component {
+ static { static1=1; }
+ static { static2=2; }
+ static function foo() { return static; }
+}
diff --git a/test/tickets/LDEV6016/staticBlock.cfc b/test/tickets/LDEV6016/staticBlock.cfc
new file mode 100644
index 00000000000..4de179e3936
--- /dev/null
+++ b/test/tickets/LDEV6016/staticBlock.cfc
@@ -0,0 +1,5 @@
+component {
+ static {
+ final RED = "red";
+ }
+}
diff --git a/test/tickets/LDEV6016/staticBlockPosition.cfc b/test/tickets/LDEV6016/staticBlockPosition.cfc
new file mode 100644
index 00000000000..7f86baf2d43
--- /dev/null
+++ b/test/tickets/LDEV6016/staticBlockPosition.cfc
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/tickets/LDEV6016/staticTagFunctionPosition.cfc b/test/tickets/LDEV6016/staticTagFunctionPosition.cfc
new file mode 100644
index 00000000000..b1b83aa5226
--- /dev/null
+++ b/test/tickets/LDEV6016/staticTagFunctionPosition.cfc
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/tickets/LDEV6017.cfc b/test/tickets/LDEV6017.cfc
new file mode 100644
index 00000000000..572fc04817f
--- /dev/null
+++ b/test/tickets/LDEV6017.cfc
@@ -0,0 +1,68 @@
+component extends="org.lucee.cfml.test.LuceeTestCase" labels="ast" {
+
+ function run( testResults, testBox ) {
+
+ describe( "LDEV-6017: Interpolated struct keys should be TemplateLiteral not BinaryExpression", function() {
+
+ it( "should represent interpolated struct key as TemplateLiteral", function() {
+ var code = 'x = { "foo.##bar##.baz": "value" };';
+ var ast = astFromString( code, "script" );
+
+ // Find the ObjectExpression
+ var objExpr = ast.body[ 1 ].right;
+ expect( objExpr.type ).toBe( "ObjectExpression" );
+
+ // Get the first property's key
+ var key = objExpr.properties[ 1 ].key;
+
+ // Interpolated strings should be TemplateLiteral (like JS template literals)
+ // with quasis (string parts) and expressions (interpolated values)
+ expect( key.type ).toBe( "TemplateLiteral", "Interpolated struct key should be TemplateLiteral, not BinaryExpression" );
+
+ // Should have quasis and expressions arrays
+ expect( key ).toHaveKey( "quasis" );
+ expect( key ).toHaveKey( "expressions" );
+
+ // quasis should contain the string parts: "foo.", ".baz"
+ expect( arrayLen( key.quasis ) ).toBe( 2 );
+ expect( key.quasis[ 1 ].value ).toBe( "foo." );
+ expect( key.quasis[ 2 ].value ).toBe( ".baz" );
+
+ // expressions should contain the interpolated identifier: bar
+ expect( arrayLen( key.expressions ) ).toBe( 1 );
+ expect( key.expressions[ 1 ].type ).toBe( "Identifier" );
+ expect( key.expressions[ 1 ].name ).toBe( "BAR" );
+ });
+
+ it( "should allow round-trip of struct with interpolated keys", function() {
+ var code = 'x = { "prefix.##name##.suffix": "value" };';
+ var ast = astFromString( code, "script" );
+
+ // The key should be representable in a way that can round-trip
+ var key = ast.body[ 1 ].right.properties[ 1 ].key;
+
+ // Should be TemplateLiteral, not BinaryExpression
+ expect( key.type ).toBe( "TemplateLiteral", "Key should be TemplateLiteral for round-trip support" );
+ expect( key.type ).notToBe( "BinaryExpression", "Key should not be broken into concatenation parts" );
+ });
+
+ it( "should handle simple interpolation with no prefix/suffix", function() {
+ var code = 'x = { "##name##": "value" };';
+ var ast = astFromString( code, "script" );
+
+ var key = ast.body[ 1 ].right.properties[ 1 ].key;
+
+ // A simple "#name#" with no string parts becomes just the expression
+ // This is expected - it's effectively just the variable
+ // The key point is it's NOT a BinaryExpression CONCAT
+ expect( key.type ).notToBe( "BinaryExpression", "Should not be broken into concatenation parts" );
+ // It should be an Identifier (the interpolated variable)
+ expect( key.type ).toBe( "Identifier" );
+ expect( key.name ).toBe( "NAME" );
+ });
+
+ });
+
+ }
+
+}
diff --git a/test/tickets/LDEV6018.cfc b/test/tickets/LDEV6018.cfc
new file mode 100644
index 00000000000..3109792a191
--- /dev/null
+++ b/test/tickets/LDEV6018.cfc
@@ -0,0 +1,375 @@
+component extends="org.lucee.cfml.test.LuceeTestCase" labels="ast" {
+
+ function run( testResults, testBox ) {
+
+ describe( "LDEV-6018: String concatenation should not wrap operands in CastExpression", function() {
+
+ it( "should not add typeAnnotation CastExpression wrapper to concat operands", function() {
+ var code = 'x = foo & "bar";';
+ var ast = astFromString( code, "script" );
+
+ // Find the BinaryExpression
+ var binExpr = ast.body[ 1 ].right;
+ expect( binExpr.type ).toBe( "BinaryExpression" );
+ expect( binExpr.operator ).toBe( "CONCAT" );
+
+ // Bug: left operand is wrapped in CastExpression with typeAnnotation: "string"
+ // This is internal type coercion info that shouldn't be in the AST
+ expect( binExpr.left.type ).toBe( "Identifier", "Left operand should be Identifier, not CastExpression" );
+ expect( binExpr.left.name ).toBe( "FOO" );
+ });
+
+ it( "should preserve simple identifier in string concatenation", function() {
+ var code = 'result = path & "/file.txt";';
+ var ast = astFromString( code, "script" );
+
+ var binExpr = ast.body[ 1 ].right;
+ expect( binExpr.type ).toBe( "BinaryExpression" );
+
+ // The left operand should be a simple Identifier
+ // Not wrapped in CastExpression with typeAnnotation
+ expect( binExpr.left ).notToHaveKey( "typeAnnotation", "Operand should not have typeAnnotation" );
+ expect( binExpr.left.type ).toBe( "Identifier" );
+ });
+
+ });
+
+ describe( "LDEV-6018: Dynamic struct keys should not be wrapped in CastExpression", function() {
+
+ it( "should preserve MemberExpression in struct key with interpolation", function() {
+ var code = 'x = { "##arguments.name##": "value" };';
+ var ast = astFromString( code, "script" );
+
+ // Find the struct property key
+ var prop = ast.body[ 1 ].right.properties[ 1 ];
+ expect( prop.type ).toBe( "Property" );
+
+ // Bug: key is parsed as MemberExpression first time, but after round-trip
+ // with "#ARGUMENTS.NAME#" output, it becomes CastExpression
+ // The key should remain a MemberExpression (or at minimum, be consistent)
+ expect( prop.key.type ).toBe( "MemberExpression", "Struct key should be MemberExpression, not wrapped in CastExpression" );
+ });
+
+ it( "should preserve CallExpression in struct key with interpolation", function() {
+ var code = 'x = { "##getKey()##": "value" };';
+ var ast = astFromString( code, "script" );
+
+ var prop = ast.body[ 1 ].right.properties[ 1 ];
+ expect( prop.type ).toBe( "Property" );
+
+ // Bug: key is CallExpression first time, but becomes CastExpression after round-trip
+ expect( prop.key.type ).toBe( "CallExpression", "Struct key should be CallExpression, not wrapped in CastExpression" );
+ });
+
+ });
+
+ describe( "LDEV-6018: Interpolated expressions should parse consistently", function() {
+
+ it( "should parse simple interpolated function name as Identifier", function() {
+ // Simple "#funcName#"() parses callee as Identifier directly
+ var code = '"##funcName##"();';
+ var ast = astFromString( code, "script" );
+
+ var callExpr = ast.body[ 1 ];
+ expect( callExpr.type ).toBe( "CallExpression" );
+ expect( callExpr.callee.type ).toBe( "Identifier" );
+ expect( callExpr.callee.name ).toBe( "FUNCNAME" );
+ });
+
+ it( "should parse complex interpolated new expression as CastExpression", function() {
+ // new "#arguments.reporter#"() should have CastExpression callee
+ var code = 'new "##arguments.reporter##"();';
+ var ast = astFromString( code, "script" );
+
+ var newExpr = ast.body[ 1 ];
+ expect( newExpr.type ).toBe( "NewExpression" );
+
+ // The callee is a CastExpression wrapping a MemberExpression
+ expect( newExpr.callee.type ).toBe( "CastExpression", "Interpolated new callee should be CastExpression" );
+ expect( newExpr.callee.argument.type ).toBe( "MemberExpression" );
+ });
+
+ it( "should parse complex interpolated call expression as MemberExpression", function() {
+ // "#arguments.reporter#"() (not new) should have MemberExpression callee
+ var code = '"##arguments.reporter##"();';
+ var ast = astFromString( code, "script" );
+
+ var callExpr = ast.body[ 1 ];
+ expect( callExpr.type ).toBe( "CallExpression" );
+
+ // Unlike new expressions, regular calls parse the interpolated expression directly
+ expect( callExpr.callee.type ).toBe( "MemberExpression" );
+ });
+
+ });
+
+ describe( "LDEV-6018: Unary plus should be UnaryExpression, not CastExpression", function() {
+
+ it( "should parse unary plus as UnaryExpression like unary minus", function() {
+ var plusCode = 'x = +num;';
+ var minusCode = 'x = -num;';
+
+ var plusAst = astFromString( plusCode, "script" );
+ var minusAst = astFromString( minusCode, "script" );
+
+ var plusExpr = plusAst.body[1].right;
+ var minusExpr = minusAst.body[1].right;
+
+ // Unary minus correctly uses UnaryExpression
+ expect( minusExpr.type ).toBe( "UnaryExpression" );
+ expect( minusExpr.operator ).toBe( "NEGATE" );
+
+ // Bug: Unary plus uses CastExpression with typeAnnotation="number"
+ // Should be UnaryExpression with operator="PLUS" (or similar)
+ expect( plusExpr.type ).toBe( "UnaryExpression",
+ "Unary plus should be UnaryExpression, not #plusExpr.type#" );
+ });
+
+ it( "should preserve unary plus in function arguments", function() {
+ var code = 'DateAdd( "d", +num, now() );';
+ var ast = astFromString( code, "script" );
+
+ var arg = ast.body[1].arguments[2];
+
+ // The +num argument should be distinguishable from just num
+ // Currently it's CastExpression which loses the + sign
+ expect( arg.type ).toBe( "UnaryExpression",
+ "Unary plus in argument should be UnaryExpression, not #arg.type#" );
+ });
+
+ });
+
+ describe( "LDEV-6018: Integer divide should have distinct operator", function() {
+
+ it( "should distinguish integer divide from regular divide", function() {
+ var intDivCode = 'x = a \ b;';
+ var divCode = 'x = a / b;';
+
+ var intDivAst = astFromString( intDivCode, "script" );
+ var divAst = astFromString( divCode, "script" );
+
+ var intDivExpr = intDivAst.body[1].right;
+ var divExpr = divAst.body[1].right;
+
+ expect( intDivExpr.type ).toBe( "BinaryExpression" );
+ expect( divExpr.type ).toBe( "BinaryExpression" );
+
+ // Bug: Both have operator "DIVIDE" - should be different
+ // Integer divide should be "INTDIV" or "INTEGER_DIVIDE"
+ expect( divExpr.operator ).toBe( "DIVIDE" );
+ expect( intDivExpr.operator ).toBe( "INTDIV",
+ "Integer divide (\\) should have operator 'INTDIV', not '#intDivExpr.operator#'" );
+ });
+
+ });
+
+ describe( "LDEV-6018: Safe navigation should have optional flag", function() {
+
+ it( "should distinguish safe navigation from regular member access", function() {
+ var safeCode = 'x = obj?.prop;';
+ var normalCode = 'x = obj.prop;';
+
+ var safeAst = astFromString( safeCode, "script" );
+ var normalAst = astFromString( normalCode, "script" );
+
+ var safeExpr = safeAst.body[1].right;
+ var normalExpr = normalAst.body[1].right;
+
+ expect( safeExpr.type ).toBe( "MemberExpression" );
+ expect( normalExpr.type ).toBe( "MemberExpression" );
+
+ // Bug: Both look identical - safe navigation should have optional=true
+ expect( normalExpr.optional ?: false ).toBe( false );
+ expect( safeExpr ).toHaveKey( "optional",
+ "Safe navigation should have 'optional' field" );
+ expect( safeExpr.optional ).toBe( true,
+ "Safe navigation (?.) should have optional=true" );
+ });
+
+ it( "should preserve safe navigation in chained access", function() {
+ var code = 'x = obj?.nested?.value;';
+ var ast = astFromString( code, "script" );
+
+ var expr = ast.body[1].right;
+
+ // Outer member access (?.value)
+ expect( expr.type ).toBe( "MemberExpression" );
+ expect( expr ).toHaveKey( "optional" );
+ expect( expr.optional ).toBe( true );
+
+ // Inner member access (obj?.nested)
+ expect( expr.object.type ).toBe( "MemberExpression" );
+ expect( expr.object ).toHaveKey( "optional" );
+ expect( expr.object.optional ).toBe( true );
+ });
+
+ it( "should NOT bleed optional flag to previous member in mixed chain", function() {
+ // Bug: a.b.c?.d incorrectly sets optional on BOTH .d AND .c
+ // Only .d should have optional=true
+ var code = 'x = a.b.c?.d;';
+ var ast = astFromString( code, "script" );
+
+ var expr = ast.body[1].right;
+
+ // Structure: MemberExpression(.d) -> MemberExpression(.c) -> MemberExpression(.b) -> Identifier(a)
+
+ // .d - should have optional=true (we used ?.)
+ expect( expr.type ).toBe( "MemberExpression" );
+ expect( expr.property.name ).toBe( "D" );
+ expect( expr ).toHaveKey( "optional" );
+ expect( expr.optional ).toBe( true, ".d should have optional=true" );
+
+ // .c - should NOT have optional (we used regular .)
+ expect( expr.object.type ).toBe( "MemberExpression" );
+ expect( expr.object.property.name ).toBe( "C" );
+ expect( expr.object.optional ?: false ).toBe( false,
+ ".c should NOT have optional=true - the flag is bleeding from ?.d" );
+
+ // .b - should NOT have optional
+ expect( expr.object.object.type ).toBe( "MemberExpression" );
+ expect( expr.object.object.property.name ).toBe( "B" );
+ expect( expr.object.object.optional ?: false ).toBe( false,
+ ".b should NOT have optional=true" );
+ });
+
+ it( "should correctly mark only the safe-navigated member in middle of chain", function() {
+ // a.b?.c.d - only .c should have optional
+ var code = 'x = a.b?.c.d;';
+ var ast = astFromString( code, "script" );
+
+ var expr = ast.body[1].right;
+
+ // .d - should NOT have optional
+ expect( expr.optional ?: false ).toBe( false, ".d should NOT have optional" );
+
+ // .c - should have optional=true (we used ?.)
+ expect( expr.object ).toHaveKey( "optional" );
+ expect( expr.object.optional ).toBe( true, ".c should have optional=true" );
+
+ // .b - should NOT have optional
+ expect( expr.object.object.optional ?: false ).toBe( false,
+ ".b should NOT have optional=true - the flag is bleeding from ?.c" );
+ });
+
+ });
+
+ describe( "LDEV-6018: Compound assignment operators should be preserved", function() {
+
+ it( "should preserve modulo compound assignment", function() {
+ var code = 'x %= 3;';
+ var ast = astFromString( code, "script" );
+
+ var expr = ast.body[1];
+ expect( expr.type ).toBe( "AssignmentExpression" );
+
+ // Bug: operator is "ASSIGN" and right is expanded to x % 3
+ // Should be operator "MODULUS_ASSIGN" or similar
+ expect( expr.operator ).toBe( "MODULUS_ASSIGN",
+ "Compound %%== should have operator 'MODULUS_ASSIGN', not '#expr.operator#'" );
+ });
+
+ it( "should preserve all compound assignment operators", function() {
+ var ops = {
+ "x += 1": "PLUS_ASSIGN",
+ "x -= 1": "MINUS_ASSIGN",
+ "x *= 2": "MULTIPLY_ASSIGN",
+ "x /= 2": "DIVIDE_ASSIGN",
+ "x %= 2": "MODULUS_ASSIGN",
+ "x &= 'a'": "CONCAT_ASSIGN"
+ };
+
+ for ( var code in ops ) {
+ var expected = ops[ code ];
+ var ast = astFromString( code & ";", "script" );
+ var expr = ast.body[1];
+
+ expect( expr.type ).toBe( "AssignmentExpression" );
+ expect( expr.operator ).toBe( expected,
+ "'#code#' should have operator '#expected#', not '#expr.operator#'" );
+ }
+ });
+
+ });
+
+ describe( "LDEV-6018: Increment/decrement should be UpdateExpression, not AssignmentExpression", function() {
+
+ it( "should parse pre-increment as UpdateExpression", function() {
+ var code = 'x = ++i;';
+ var ast = astFromString( code, "script" );
+
+ var expr = ast.body[1].right;
+
+ // Bug: ++i is parsed as AssignmentExpression with PLUS_ASSIGN
+ // Should be UpdateExpression with prefix=true
+ expect( expr.type ).toBe( "UpdateExpression",
+ "Pre-increment ++i should be UpdateExpression, not #expr.type#" );
+ expect( expr ).toHaveKey( "prefix" );
+ expect( expr.prefix ).toBe( true, "Pre-increment should have prefix=true" );
+ expect( expr.operator ).toBe( "++" );
+ });
+
+ it( "should parse post-increment as UpdateExpression", function() {
+ var code = 'x = i++;';
+ var ast = astFromString( code, "script" );
+
+ var expr = ast.body[1].right;
+
+ // Bug: i++ is parsed as AssignmentExpression with PLUS_ASSIGN
+ // Should be UpdateExpression with prefix=false
+ expect( expr.type ).toBe( "UpdateExpression",
+ "Post-increment i++ should be UpdateExpression, not #expr.type#" );
+ expect( expr ).toHaveKey( "prefix" );
+ expect( expr.prefix ).toBe( false, "Post-increment should have prefix=false" );
+ expect( expr.operator ).toBe( "++" );
+ });
+
+ it( "should parse pre-decrement as UpdateExpression", function() {
+ var code = 'x = --i;';
+ var ast = astFromString( code, "script" );
+
+ var expr = ast.body[1].right;
+
+ expect( expr.type ).toBe( "UpdateExpression",
+ "Pre-decrement --i should be UpdateExpression, not #expr.type#" );
+ expect( expr ).toHaveKey( "prefix" );
+ expect( expr.prefix ).toBe( true );
+ expect( expr.operator ).toBe( "--" );
+ });
+
+ it( "should parse post-decrement as UpdateExpression", function() {
+ var code = 'x = i--;';
+ var ast = astFromString( code, "script" );
+
+ var expr = ast.body[1].right;
+
+ expect( expr.type ).toBe( "UpdateExpression",
+ "Post-decrement i-- should be UpdateExpression, not #expr.type#" );
+ expect( expr ).toHaveKey( "prefix" );
+ expect( expr.prefix ).toBe( false );
+ expect( expr.operator ).toBe( "--" );
+ });
+
+ it( "should allow increment as inline expression", function() {
+ // This is the key issue - ++i must be usable as an expression
+ var code = 'x = int( 100 / count * ++i );';
+ var ast = astFromString( code, "script" );
+
+ var callExpr = ast.body[1].right;
+ expect( callExpr.type ).toBe( "CallExpression" );
+
+ // Find the ++i in the multiply expression
+ var multiplyExpr = callExpr.arguments[1];
+ expect( multiplyExpr.type ).toBe( "BinaryExpression" );
+ expect( multiplyExpr.operator ).toBe( "MULTIPLY" );
+
+ var incExpr = multiplyExpr.right;
+ // Bug: This is AssignmentExpression which can't be used inline
+ expect( incExpr.type ).toBe( "UpdateExpression",
+ "Inline ++i should be UpdateExpression for proper round-trip" );
+ });
+
+ });
+
+ }
+
+}
diff --git a/test/tickets/LDEV6019.cfc b/test/tickets/LDEV6019.cfc
new file mode 100644
index 00000000000..c6bd7d72ea7
--- /dev/null
+++ b/test/tickets/LDEV6019.cfc
@@ -0,0 +1,41 @@
+component extends="org.lucee.cfml.test.LuceeTestCase" labels="ast" {
+
+ variables.testDir = getDirectoryFromPath( getCurrentTemplatePath() ) & "LDEV6019/";
+
+ function run( testResults, testBox ) {
+
+ describe( "LDEV-6019: Unquoted function attributes after parenthesis should parse", function() {
+
+ it( "should parse function with unquoted access=remote attribute", function() {
+ // This should not throw an error
+ var ast = astFromPath( variables.testDir & "unquotedAccess.cfc" );
+
+ expect( ast.type ).toBe( "Program" );
+ // Find the function and verify it has access attribute
+ var func = ast.body[ 1 ].body.body[ 1 ];
+ expect( func.type ).toBe( "FunctionDeclaration" );
+ expect( func.access ).toBe( "remote" );
+ });
+
+ it( "should parse function with colon syntax access:remote attribute", function() {
+ var ast = astFromPath( variables.testDir & "colonAccess.cfc" );
+
+ expect( ast.type ).toBe( "Program" );
+ var func = ast.body[ 1 ].body.body[ 1 ];
+ expect( func.type ).toBe( "FunctionDeclaration" );
+ expect( func.access ).toBe( "remote" );
+ });
+
+ it( "should parse function with unquoted boolean attribute", function() {
+ var ast = astFromPath( variables.testDir & "unquotedBoolean.cfc" );
+
+ expect( ast.type ).toBe( "Program" );
+ var func = ast.body[ 1 ].body.body[ 1 ];
+ expect( func.type ).toBe( "FunctionDeclaration" );
+ });
+
+ });
+
+ }
+
+}
diff --git a/test/tickets/LDEV6019/colonAccess.cfc b/test/tickets/LDEV6019/colonAccess.cfc
new file mode 100644
index 00000000000..68f3bc6801e
--- /dev/null
+++ b/test/tickets/LDEV6019/colonAccess.cfc
@@ -0,0 +1,7 @@
+component {
+
+ function get( string x ) access:remote {
+ return "true";
+ }
+
+}
diff --git a/test/tickets/LDEV6019/unquotedAccess.cfc b/test/tickets/LDEV6019/unquotedAccess.cfc
new file mode 100644
index 00000000000..3b5c43910c4
--- /dev/null
+++ b/test/tickets/LDEV6019/unquotedAccess.cfc
@@ -0,0 +1,7 @@
+component {
+
+ function get( string x ) access=remote {
+ return "true";
+ }
+
+}
diff --git a/test/tickets/LDEV6019/unquotedBoolean.cfc b/test/tickets/LDEV6019/unquotedBoolean.cfc
new file mode 100644
index 00000000000..df7378708cc
--- /dev/null
+++ b/test/tickets/LDEV6019/unquotedBoolean.cfc
@@ -0,0 +1,7 @@
+component {
+
+ function getData() secured=false {
+ return "data";
+ }
+
+}
diff --git a/test/tickets/LDEV6022.cfc b/test/tickets/LDEV6022.cfc
new file mode 100644
index 00000000000..216a18774ab
--- /dev/null
+++ b/test/tickets/LDEV6022.cfc
@@ -0,0 +1,52 @@
+component extends="org.lucee.cfml.test.LuceeTestCase" labels="ast" {
+
+ function run( testResults, testBox ) {
+ describe( "LDEV-6022: String literal concatenation should not be merged in AST", function() {
+
+ it( "preserves three-part string concatenation", function() {
+ var code = 'x = "";';
+ var ast = astFromString( code, "script" );
+
+ // The assignment expression
+ var assign = ast.body[1];
+ expect( assign.type ).toBe( "AssignmentExpression" );
+
+ // Right side should be a BinaryExpression
+ var concat1 = assign.right;
+ expect( concat1.type ).toBe( "BinaryExpression" );
+ expect( concat1.operator ).toBe( "CONCAT" );
+
+ // Left side of outer concat should also be a BinaryExpression (the first two parts)
+ var concat2 = concat1.left;
+ expect( concat2.type ).toBe( "BinaryExpression" );
+ expect( concat2.operator ).toBe( "CONCAT" );
+
+ // First string literal should be ""
+ expect( concat1.right.type ).toBe( "StringLiteral" );
+ expect( concat1.right.value ).toBe( "ript>" );
+ });
+
+ it( "preserves two-part string concatenation with cf tag pattern", function() {
+ var code = 'x = "";';
+ var ast = astFromString( code, "script" );
+
+ var assign = ast.body[1];
+ var concat = assign.right;
+
+ expect( concat.type ).toBe( "BinaryExpression" );
+ expect( concat.left.value ).toBe( "" );
+ });
+
+ });
+ }
+
+}
diff --git a/test/tickets/LDEV6024.cfc b/test/tickets/LDEV6024.cfc
new file mode 100644
index 00000000000..f51ec785641
--- /dev/null
+++ b/test/tickets/LDEV6024.cfc
@@ -0,0 +1,62 @@
+component extends="org.lucee.cfml.test.LuceeTestCase" labels="ast" {
+
+ function run( testResults, testBox ) {
+ describe( "LDEV-6024: Computed member expression inside scope access", function() {
+
+ it( "should preserve nested computed member expression in AST", function() {
+ var code = 'return variables[local.functionName[1]]();';
+ var ast = astFromString( code, "script" );
+
+ // The return statement
+ var returnStmt = ast.body[1];
+ expect( returnStmt.type ).toBe( "ReturnStatement" );
+
+ // The call expression: variables[...]()`
+ var callExpr = returnStmt.argument;
+ expect( callExpr.type ).toBe( "CallExpression" );
+
+ // The callee is a MemberExpression: variables[local.functionName[1]]
+ var callee = callExpr.callee;
+ expect( callee.type ).toBe( "MemberExpression" );
+ expect( callee.computed ).toBe( true );
+
+ // Object should be VARIABLES identifier
+ expect( callee.object.type ).toBe( "Identifier" );
+ expect( callee.object.name ).toBe( "VARIABLES" );
+
+ // Property is wrapped in CastExpression (cast to string for bracket access)
+ var prop = callee.property;
+ expect( prop.type ).toBe( "CastExpression" );
+ expect( prop.typeAnnotation ).toBe( "string" );
+
+ // The argument inside CastExpression should be the MemberExpression
+ var innerExpr = prop.argument;
+ expect( innerExpr.type ).toBe( "MemberExpression" );
+ expect( innerExpr.computed ).toBe( true );
+
+ // The inner expression is local.functionName[1]
+ expect( innerExpr.object.type ).toBe( "MemberExpression" );
+ expect( innerExpr.object.property.name ).toBe( "FUNCTIONNAME" );
+ });
+
+ it( "should handle simple computed scope access", function() {
+ var code = 'return variables[key];';
+ var ast = astFromString( code, "script" );
+
+ var returnStmt = ast.body[1];
+ var memberExpr = returnStmt.argument;
+
+ expect( memberExpr.type ).toBe( "MemberExpression" );
+ expect( memberExpr.computed ).toBe( true );
+ expect( memberExpr.object.name ).toBe( "VARIABLES" );
+
+ // Property is wrapped in CastExpression (cast to string for bracket access)
+ expect( memberExpr.property.type ).toBe( "CastExpression" );
+ expect( memberExpr.property.argument.type ).toBe( "Identifier" );
+ expect( memberExpr.property.argument.name ).toBe( "KEY" );
+ });
+
+ });
+ }
+
+}
diff --git a/test/tickets/LDEV6027.cfc b/test/tickets/LDEV6027.cfc
new file mode 100644
index 00000000000..1e6327f9209
--- /dev/null
+++ b/test/tickets/LDEV6027.cfc
@@ -0,0 +1,57 @@
+component extends="org.lucee.cfml.test.LuceeTestCase" labels="ast" {
+
+ variables.testDir = getDirectoryFromPath( getCurrentTemplatePath() ) & "LDEV6027/";
+
+ function run( testResults, testBox ) {
+
+ describe( "LDEV-6027: Property attribute order should be preserved in AST", function() {
+
+ it( "property attributes should maintain declaration order", function() {
+ var ast = astFromPath( variables.testDir & "PropertyOrder.cfc" );
+
+ // Find first property node
+ var prop = findFirstProperty( ast );
+ expect( prop ).notToBeNull( "Should find a property in the AST" );
+
+ // Get attribute names in order
+ var attrNames = [];
+ for ( var attr in ( prop.attributes ?: [] ) ) {
+ arrayAppend( attrNames, attr.name );
+ }
+
+ // Should be: name, type, default (declaration order)
+ // Bug: Currently returns alphabetical order
+ expect( attrNames[ 1 ] ).toBe( "name", "First attribute should be 'name', got: " & attrNames.toList() );
+ expect( attrNames[ 2 ] ).toBe( "type", "Second attribute should be 'type', got: " & attrNames.toList() );
+ expect( attrNames[ 3 ] ).toBe( "default", "Third attribute should be 'default', got: " & attrNames.toList() );
+ });
+
+ });
+
+ }
+
+ /**
+ * Recursively find the first property CFMLTag node
+ */
+ private function findFirstProperty( required struct node ) {
+ if ( ( node.type ?: "" ) == "CFMLTag" && ( node.name ?: "" ) == "property" ) {
+ return node;
+ }
+ for ( var key in node ) {
+ var val = node[ key ];
+ if ( isStruct( val ) ) {
+ var result = findFirstProperty( val );
+ if ( !isNull( result ) ) return result;
+ } else if ( isArray( val ) ) {
+ for ( var item in val ) {
+ if ( isStruct( item ) ) {
+ var result = findFirstProperty( item );
+ if ( !isNull( result ) ) return result;
+ }
+ }
+ }
+ }
+ return;
+ }
+
+}
diff --git a/test/tickets/LDEV6027/PropertyOrder.cfc b/test/tickets/LDEV6027/PropertyOrder.cfc
new file mode 100644
index 00000000000..ccd56db32c1
--- /dev/null
+++ b/test/tickets/LDEV6027/PropertyOrder.cfc
@@ -0,0 +1,4 @@
+component {
+ property name="id" type="numeric" default="0";
+ property name="title" type="string" default="";
+}
diff --git a/test/tickets/LDEV6028.cfc b/test/tickets/LDEV6028.cfc
new file mode 100644
index 00000000000..89c3dfde42e
--- /dev/null
+++ b/test/tickets/LDEV6028.cfc
@@ -0,0 +1,108 @@
+component extends="org.lucee.cfml.test.LuceeTestCase" labels="ast" {
+
+ function run( testResults, testBox ) {
+ describe( "LDEV-6028: Struct key AST type changes based on separator (: vs =)", function() {
+
+ it( "numeric key with colon is NumberLiteral", function() {
+ var ast = astFromString( 'x = { 1: "a" };', "script" );
+ var key = ast.body[1].right.properties[1].key;
+
+ expect( key.type ).toBe( "NumberLiteral" );
+ expect( key.value ).toBe( 1 );
+ });
+
+ it( "numeric key with equals should be NumberLiteral not StringLiteral", function() {
+ var ast = astFromString( 'x = { 1 = "a" };', "script" );
+ var key = ast.body[1].right.properties[1].key;
+
+ // BUG: Currently returns StringLiteral with raw="1" instead of NumberLiteral
+ expect( key.type ).toBe( "NumberLiteral" );
+ expect( key.value ).toBe( 1 );
+ });
+
+ it( "float key with colon is NumberLiteral", function() {
+ var ast = astFromString( 'x = { 1.5: "a" };', "script" );
+ var key = ast.body[1].right.properties[1].key;
+
+ expect( key.type ).toBe( "NumberLiteral" );
+ expect( key.value ).toBe( 1.5 );
+ });
+
+ it( "float key with equals should be NumberLiteral not StringLiteral", function() {
+ var ast = astFromString( 'x = { 1.5 = "a" };', "script" );
+ var key = ast.body[1].right.properties[1].key;
+
+ // BUG: Currently returns StringLiteral with raw="1.5" instead of NumberLiteral
+ expect( key.type ).toBe( "NumberLiteral" );
+ expect( key.value ).toBe( 1.5 );
+ });
+
+ it( "negative numeric key with colon is NumberLiteral", function() {
+ var ast = astFromString( 'x = { -1: "a" };', "script" );
+ var key = ast.body[1].right.properties[1].key;
+
+ expect( key.type ).toBe( "NumberLiteral" );
+ expect( key.value ).toBe( -1 );
+ });
+
+ it( "negative numeric key with equals should be NumberLiteral not StringLiteral", function() {
+ var ast = astFromString( 'x = { -1 = "a" };', "script" );
+ var key = ast.body[1].right.properties[1].key;
+
+ // BUG: Currently returns StringLiteral with raw="-1" instead of NumberLiteral
+ expect( key.type ).toBe( "NumberLiteral" );
+ expect( key.value ).toBe( -1 );
+ });
+
+ it( "boolean key with colon is BooleanLiteral", function() {
+ var ast = astFromString( 'x = { true: "a" };', "script" );
+ var key = ast.body[1].right.properties[1].key;
+
+ expect( key.type ).toBe( "BooleanLiteral" );
+ expect( key.value ).toBe( true );
+ });
+
+ // Skip: true/false are reserved words in Lucee, so { true = "a" } is parsed as
+ // an invalid assignment to the literal 'true', not as a struct key.
+ // This is expected parser behavior, not an AST bug.
+ // See: https://docs.lucee.org/guides/developing-with-lucee-server/reserved-word.html
+ xit( "boolean key with equals should be BooleanLiteral not parse error", function() {
+ var ast = astFromString( 'x = { true = "a" };', "script" );
+ var key = ast.body[1].right.properties[1].key;
+
+ expect( key.type ).toBe( "BooleanLiteral" );
+ expect( key.value ).toBe( true );
+ });
+
+ it( "quoted string key is consistent with both separators", function() {
+ var astColon = astFromString( 'x = { "foo": "a" };', "script" );
+ var keyColon = astColon.body[1].right.properties[1].key;
+
+ var astEquals = astFromString( 'x = { "foo" = "a" };', "script" );
+ var keyEquals = astEquals.body[1].right.properties[1].key;
+
+ // Both should be StringLiteral - this works correctly
+ expect( keyColon.type ).toBe( "StringLiteral" );
+ expect( keyEquals.type ).toBe( "StringLiteral" );
+ expect( keyColon.value ).toBe( "foo" );
+ expect( keyEquals.value ).toBe( "foo" );
+ });
+
+ it( "identifier key is consistent with both separators", function() {
+ var astColon = astFromString( 'x = { foo: "a" };', "script" );
+ var keyColon = astColon.body[1].right.properties[1].key;
+
+ var astEquals = astFromString( 'x = { foo = "a" };', "script" );
+ var keyEquals = astEquals.body[1].right.properties[1].key;
+
+ // Both should be Identifier - this works correctly
+ expect( keyColon.type ).toBe( "Identifier" );
+ expect( keyEquals.type ).toBe( "Identifier" );
+ expect( keyColon.name ).toBe( "foo" );
+ expect( keyEquals.name ).toBe( "foo" );
+ });
+
+ });
+ }
+
+}
diff --git a/test/tickets/LDEV6029.cfc b/test/tickets/LDEV6029.cfc
new file mode 100644
index 00000000000..af570977035
--- /dev/null
+++ b/test/tickets/LDEV6029.cfc
@@ -0,0 +1,57 @@
+component extends="org.lucee.cfml.test.LuceeTestCase" labels="ast" {
+
+ function run( testResults, testBox ) {
+ describe( "LDEV-6029: cachedWithin preserves original expression in AST", function() {
+
+ it( "script function with createTimespan shows CallExpression not LongLiteral", function() {
+ var code = 'function test() cachedWithin="##createTimespan(0,0,0,0,20)##" { }';
+ var ast = astFromString( code, "script" );
+ var fn = ast.body[1];
+
+ expect( fn.type ).toBe( "FunctionDeclaration" );
+ expect( fn ).toHaveKey( "cachedWithin" );
+ expect( fn.cachedWithin.type ).toBe( "CallExpression" );
+ expect( fn.cachedWithin.callee.name ).toBe( "createtimespan" );
+ expect( arrayLen( fn.cachedWithin.arguments ) ).toBe( 5 );
+ });
+
+ it( "script function with numeric cachedWithin shows NumberLiteral not evaluated LongLiteral", function() {
+ var code = 'function test() cachedWithin=20 { }';
+ var ast = astFromString( code, "script" );
+ var fn = ast.body[1];
+
+ expect( fn.type ).toBe( "FunctionDeclaration" );
+ expect( fn ).toHaveKey( "cachedWithin" );
+ expect( fn.cachedWithin.type ).toBe( "NumberLiteral" );
+ expect( fn.cachedWithin.value ).toBe( 20 );
+ expect( fn.cachedWithin.raw ).toBe( "20" );
+ });
+
+ it( "script function with string cachedWithin preserves StringLiteral", function() {
+ var code = 'function test() cachedWithin="request" { }';
+ var ast = astFromString( code, "script" );
+ var fn = ast.body[1];
+
+ expect( fn.type ).toBe( "FunctionDeclaration" );
+ expect( fn ).toHaveKey( "cachedWithin" );
+ expect( fn.cachedWithin.type ).toBe( "StringLiteral" );
+ expect( fn.cachedWithin.value ).toBe( "request" );
+ });
+
+ it( "tag function with createTimespan shows CallExpression in attributes", function() {
+ var code = '';
+ var ast = astFromString( code );
+ var fn = ast.body[1];
+
+ expect( fn.type ).toBe( "CFMLTag" );
+ expect( fn.name ).toBe( "function" );
+ var cachedAttr = fn.attributes.filter( function( a ) { return a.name == "cachedwithin"; } );
+ expect( arrayLen( cachedAttr ) ).toBe( 1 );
+ expect( cachedAttr[1].value.type ).toBe( "CallExpression" );
+ expect( cachedAttr[1].value.callee.name ).toBe( "createtimespan" );
+ });
+
+ });
+ }
+
+}
diff --git a/test/tickets/LDEV6030.cfc b/test/tickets/LDEV6030.cfc
new file mode 100644
index 00000000000..b53c73f388c
--- /dev/null
+++ b/test/tickets/LDEV6030.cfc
@@ -0,0 +1,148 @@
+component extends="org.lucee.cfml.test.LuceeTestCase" labels="ast" {
+
+ function run( testResults, testBox ) {
+ describe( "LDEV-6030: Labeled loops should include label in AST", function() {
+
+ it( "labeled for loop has label property", function() {
+ var code = 'outer: for( var i = 1; i <= 5; i++ ) { }';
+ var ast = astFromString( code, "script" );
+ var forStmt = ast.body[1];
+
+ expect( forStmt.type ).toBe( "ForStatement" );
+ expect( forStmt ).toHaveKey( "label" );
+ expect( forStmt.label ).toBe( "outer" );
+ });
+
+ it( "labeled while loop has label property", function() {
+ var code = 'myLoop: while( true ) { break myLoop; }';
+ var ast = astFromString( code, "script" );
+ var whileStmt = ast.body[1];
+
+ expect( whileStmt.type ).toBe( "WhileStatement" );
+ expect( whileStmt ).toHaveKey( "label" );
+ expect( whileStmt.label ).toBe( "myLoop" );
+ });
+
+ it( "labeled do-while loop has label property", function() {
+ var code = 'test: do { } while( false );';
+ var ast = astFromString( code, "script" );
+ var doWhileStmt = ast.body[1];
+
+ expect( doWhileStmt.type ).toBe( "DoWhileStatement" );
+ expect( doWhileStmt ).toHaveKey( "label" );
+ expect( doWhileStmt.label ).toBe( "test" );
+ });
+
+ it( "labeled for-in loop has label property", function() {
+ var code = 'items: for( var item in [1,2,3] ) { }';
+ var ast = astFromString( code, "script" );
+ var forOfStmt = ast.body[1];
+
+ expect( forOfStmt.type ).toBe( "ForOfStatement" );
+ expect( forOfStmt ).toHaveKey( "label" );
+ expect( forOfStmt.label ).toBe( "items" );
+ });
+
+ it( "unlabeled for loop has no label property", function() {
+ var code = 'for( var i = 1; i <= 5; i++ ) { }';
+ var ast = astFromString( code, "script" );
+ var forStmt = ast.body[1];
+
+ expect( forStmt.type ).toBe( "ForStatement" );
+ expect( forStmt ).notToHaveKey( "label" );
+ });
+
+ it( "unlabeled while loop has no label property", function() {
+ var code = 'while( false ) { }';
+ var ast = astFromString( code, "script" );
+ var whileStmt = ast.body[1];
+
+ expect( whileStmt.type ).toBe( "WhileStatement" );
+ expect( whileStmt ).notToHaveKey( "label" );
+ });
+
+ });
+
+ describe( "LDEV-6030: cfbreak/cfcontinue without label should not have label attribute", function() {
+
+ it( "cfbreak without label has no label attribute", function() {
+ var code = '';
+ var ast = astFromString( code, "tag" );
+ var cfloop = ast.body[1];
+ var cfbreak = cfloop.body.body[1];
+
+ expect( cfbreak.type ).toBe( "CFMLTag" );
+ expect( cfbreak.name ).toBe( "break" );
+ expect( cfbreak.attributes ).toBeEmpty();
+ });
+
+ it( "cfcontinue without label has no label attribute", function() {
+ var code = '';
+ var ast = astFromString( code, "tag" );
+ var cfloop = ast.body[1];
+ var cfcontinue = cfloop.body.body[1];
+
+ expect( cfcontinue.type ).toBe( "CFMLTag" );
+ expect( cfcontinue.name ).toBe( "continue" );
+ expect( cfcontinue.attributes ).toBeEmpty();
+ });
+
+ it( "cfbreak with label has label attribute", function() {
+ var code = '';
+ var ast = astFromString( code, "tag" );
+ var cfloop = ast.body[1];
+ var cfbreak = cfloop.body.body[1];
+
+ expect( cfbreak.type ).toBe( "CFMLTag" );
+ expect( cfbreak.name ).toBe( "break" );
+ expect( cfbreak.attributes ).toHaveLength( 1 );
+ expect( cfbreak.attributes[1].name ).toBe( "label" );
+ });
+
+ it( "cfcontinue with label has label attribute", function() {
+ var code = '';
+ var ast = astFromString( code, "tag" );
+ var cfloop = ast.body[1];
+ var cfcontinue = cfloop.body.body[1];
+
+ expect( cfcontinue.type ).toBe( "CFMLTag" );
+ expect( cfcontinue.name ).toBe( "continue" );
+ expect( cfcontinue.attributes ).toHaveLength( 1 );
+ expect( cfcontinue.attributes[1].name ).toBe( "label" );
+ });
+
+ });
+
+ describe( "LDEV-6030: cfreturn should preserve expr attribute", function() {
+
+ it( "cfreturn without value has expr attribute with NullLiteral", function() {
+ var code = '';
+ var ast = astFromString( code, "tag" );
+ var cffunction = ast.body[1];
+ var cfreturn = cffunction.body.body[1];
+
+ expect( cfreturn.type ).toBe( "CFMLTag" );
+ expect( cfreturn.name ).toBe( "return" );
+ expect( cfreturn.attributes ).toHaveLength( 1 );
+ expect( cfreturn.attributes[1].name ).toBe( "expr" );
+ expect( cfreturn.attributes[1].value.type ).toBe( "NullLiteral" );
+ });
+
+ it( "cfreturn with value has expr attribute", function() {
+ var code = '';
+ var ast = astFromString( code, "tag" );
+ var cffunction = ast.body[1];
+ var cfreturn = cffunction.body.body[1];
+
+ expect( cfreturn.type ).toBe( "CFMLTag" );
+ expect( cfreturn.name ).toBe( "return" );
+ expect( cfreturn.attributes ).toHaveLength( 1 );
+ expect( cfreturn.attributes[1].name ).toBe( "expr" );
+ expect( cfreturn.attributes[1].value.type ).toBe( "StringLiteral" );
+ expect( cfreturn.attributes[1].value.value ).toBe( "hello" );
+ });
+
+ });
+ }
+
+}
diff --git a/test/tickets/LDEV6031.cfc b/test/tickets/LDEV6031.cfc
new file mode 100644
index 00000000000..6b960cbe2ff
--- /dev/null
+++ b/test/tickets/LDEV6031.cfc
@@ -0,0 +1,105 @@
+component extends="org.lucee.cfml.test.LuceeTestCase" {
+
+ function run( testResults, testBox ) {
+ describe( "LDEV-6031: cfimport custom tags in AST", function() {
+
+ it( "should not leak __custom_tag_path internal attribute", function() {
+ var ast = getAst( "cfimport-basic.cfm" );
+ var customTag = findCustomTag( ast, "my:hello" );
+
+ expect( customTag ).notToBeNull();
+
+ // Check no internal attributes leaked
+ var attrNames = customTag.attributes.map( function( a ) { return a.name; } );
+ expect( attrNames ).notToInclude( "__custom_tag_path" );
+ });
+
+ it( "should have tag name in 'name' field, not just appendix", function() {
+ var ast = getAst( "cfimport-basic.cfm" );
+ var customTag = findCustomTag( ast, "my:hello" );
+
+ expect( customTag ).notToBeNull();
+ // name should be "hello", not empty string
+ expect( customTag.name ).toBe( "hello" );
+ });
+
+ it( "should preserve cfimport tag in AST", function() {
+ var ast = getAst( "cfimport-basic.cfm" );
+ var importTag = findTag( ast, "import" );
+
+ expect( importTag ).notToBeNull();
+ expect( importTag.fullname ).toBe( "cfimport" );
+ });
+
+ it( "should preserve all custom tag invocations in body", function() {
+ var ast = getAst( "cfimport-multiple.cfm" );
+
+ // Should have cfimport + 3 custom tags + whitespace nodes
+ var customTags = ast.body.filter( function( node ) {
+ return ( node.type == "CFMLTag" && node.keyExists( "nameSpace" ) && node.nameSpace == "my" );
+ });
+
+ expect( customTags.len() ).toBe( 3 );
+ });
+
+ it( "should handle nested custom tags", function() {
+ var ast = getAst( "cfimport-nested.cfm" );
+ var outerTag = findCustomTag( ast, "my:outer" );
+
+ expect( outerTag ).notToBeNull();
+ expect( outerTag.keyExists( "body" ) ).toBeTrue();
+
+ // Find inner tag in body
+ var innerTag = findCustomTag( outerTag.body, "my:inner" );
+ expect( innerTag ).notToBeNull();
+ });
+
+ // Quirk #50: Imported custom tags should not be missing from AST
+ it( "should not omit imported custom tags from AST body", function() {
+ var ast = getAst( "cfimport-basic.cfm" );
+
+ // Count all CFMLTag nodes in body
+ var allTags = ast.body.filter( function( node ) {
+ return node.type == "CFMLTag";
+ });
+
+ // Should have at least cfimport + custom tag (not just cfimport)
+ expect( allTags.len() ).toBeGTE( 2, "Custom tag should not be missing from AST" );
+
+ // Verify the custom tag is present (not just cfimport)
+ var hasCustomTag = allTags.some( function( tag ) {
+ return tag.keyExists( "nameSpace" ) && tag.nameSpace == "my";
+ });
+ expect( hasCustomTag ).toBeTrue( "Imported prefix custom tag should be in AST body" );
+ });
+
+ });
+ }
+
+ private function getAst( required string filename ) {
+ var testDir = getDirectoryFromPath( getCurrentTemplatePath() );
+ var filePath = testDir & "LDEV6031/" & filename;
+ return astFromPath( filePath );
+ }
+
+ private function findTag( required struct ast, required string tagName ) {
+ var body = ast.keyExists( "body" ) ? ( isArray( ast.body ) ? ast.body : ast.body.body ?: [] ) : [];
+ for ( var node in body ) {
+ if ( node.type == "CFMLTag" && node.name == tagName ) {
+ return node;
+ }
+ }
+ return javaCast( "null", 0 );
+ }
+
+ private function findCustomTag( required struct ast, required string fullname ) {
+ var body = ast.keyExists( "body" ) ? ( isArray( ast.body ) ? ast.body : ast.body.body ?: [] ) : [];
+ for ( var node in body ) {
+ if ( node.type == "CFMLTag" && node.keyExists( "fullname" ) && node.fullname == fullname ) {
+ return node;
+ }
+ }
+ return javaCast( "null", 0 );
+ }
+
+}
diff --git a/test/tickets/LDEV6031/cfimport-basic.cfm b/test/tickets/LDEV6031/cfimport-basic.cfm
new file mode 100644
index 00000000000..7c2a304fe49
--- /dev/null
+++ b/test/tickets/LDEV6031/cfimport-basic.cfm
@@ -0,0 +1,2 @@
+
+
diff --git a/test/tickets/LDEV6031/cfimport-multiple.cfm b/test/tickets/LDEV6031/cfimport-multiple.cfm
new file mode 100644
index 00000000000..d61a0453a8a
--- /dev/null
+++ b/test/tickets/LDEV6031/cfimport-multiple.cfm
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/test/tickets/LDEV6031/cfimport-nested.cfm b/test/tickets/LDEV6031/cfimport-nested.cfm
new file mode 100644
index 00000000000..14fdfb4e84b
--- /dev/null
+++ b/test/tickets/LDEV6031/cfimport-nested.cfm
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/test/tickets/LDEV6031/customtags/hello.cfm b/test/tickets/LDEV6031/customtags/hello.cfm
new file mode 100644
index 00000000000..7df97d2e3f0
--- /dev/null
+++ b/test/tickets/LDEV6031/customtags/hello.cfm
@@ -0,0 +1,3 @@
+
+
+Hello, #attributes.name#!
diff --git a/test/tickets/LDEV6031/customtags/inner.cfm b/test/tickets/LDEV6031/customtags/inner.cfm
new file mode 100644
index 00000000000..13ef679a0c4
--- /dev/null
+++ b/test/tickets/LDEV6031/customtags/inner.cfm
@@ -0,0 +1,3 @@
+
+
+#attributes.value#
diff --git a/test/tickets/LDEV6031/customtags/outer.cfm b/test/tickets/LDEV6031/customtags/outer.cfm
new file mode 100644
index 00000000000..d8a9be1dce8
--- /dev/null
+++ b/test/tickets/LDEV6031/customtags/outer.cfm
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/test/tickets/LDEV6032.cfc b/test/tickets/LDEV6032.cfc
new file mode 100644
index 00000000000..898971118f2
--- /dev/null
+++ b/test/tickets/LDEV6032.cfc
@@ -0,0 +1,162 @@
+component extends="org.lucee.cfml.test.LuceeTestCase" labels="ast" {
+
+ function run( testResults, testBox ) {
+ describe( "LDEV-6032: Boolean attribute values in AST", function() {
+
+ // Tag mode tests (Quirk 46)
+ it( "should parse unquoted boolean true as BooleanLiteral in tag mode", function() {
+ var ast = getAst( "tag-bool-true.cfm" );
+ var dumpTag = findTag( ast, "dump" );
+
+ expect( dumpTag ).notToBeNull();
+ var abortAttr = findAttribute( dumpTag, "abort" );
+ expect( abortAttr ).notToBeNull();
+ expect( abortAttr.value.type ).toBe( "BooleanLiteral" );
+ expect( abortAttr.value.value ).toBeTrue();
+ });
+
+ it( "should parse unquoted boolean false as BooleanLiteral in tag mode", function() {
+ var ast = getAst( "tag-bool-false.cfm" );
+ var queryTag = findTag( ast, "query" );
+
+ expect( queryTag ).notToBeNull();
+ var attr = findAttribute( queryTag, "cachedWithin" );
+ expect( attr ).notToBeNull();
+ expect( attr.value.type ).toBe( "BooleanLiteral" );
+ expect( attr.value.value ).toBeFalse();
+ });
+
+ // Script mode tests (Quirk 53)
+ // SKIPPED: silent tag uses TLD type="single" to support positional syntax (silent false { })
+ // which prevents proper named attribute parsing. The AST for "silent bufferOutput=false"
+ // shows CastExpression(AssignmentExpression) instead of BooleanLiteral.
+ // Fixing this would break the positional syntax - complex edge case, not worth the risk.
+ it( title="should parse boolean attribute as BooleanLiteral in script mode", body=function() {
+ var ast = getAst( "script-bool-false.cfm" );
+ var silentTag = findScriptTag( ast, "silent" );
+
+ expect( silentTag ).notToBeNull();
+ var attr = findAttribute( silentTag, "bufferoutput" );
+ expect( attr ).notToBeNull();
+ // Should be BooleanLiteral, not CastExpression(AssignmentExpression)
+ expect( attr.value.type ).toBe( "BooleanLiteral" );
+ expect( attr.value.value ).toBeFalse();
+ }, skip=true );
+
+ it( "should parse boolean true attribute in script mode", function() {
+ var ast = getAst( "script-bool-true.cfm" );
+ var settingTag = findScriptTag( ast, "setting" );
+
+ expect( settingTag ).notToBeNull();
+ var attr = findAttribute( settingTag, "showDebugOutput" );
+ expect( attr ).notToBeNull();
+ expect( attr.value.type ).toBe( "BooleanLiteral" );
+ expect( attr.value.value ).toBeTrue();
+ });
+
+ // Verify standalone boolean attributes still work
+ it( "should parse standalone boolean attribute as BooleanLiteral", function() {
+ var ast = getAst( "tag-bool-standalone.cfm" );
+ var dumpTag = findTag( ast, "dump" );
+
+ expect( dumpTag ).notToBeNull();
+ var abortAttr = findAttribute( dumpTag, "abort" );
+ expect( abortAttr ).notToBeNull();
+ expect( abortAttr.value.type ).toBe( "BooleanLiteral" );
+ expect( abortAttr.value.value ).toBeTrue();
+ });
+
+ // Quirk #65: Naked boolean attributes in script mode
+ // BUG: Naked attributes like "singleton" are parsed as StringLiteral("") not BooleanLiteral(true)
+ it( title="should parse naked boolean attribute as BooleanLiteral in script mode", body=function() {
+ var ast = astFromPath( getDirectoryFromPath( getCurrentTemplatePath() ) & "LDEV6032/script-naked-singleton.cfc" );
+
+ // Find the component tag
+ var componentTag = ast.body[ 1 ];
+ expect( componentTag.type ).toBe( "CFMLTag" );
+ expect( componentTag.name ).toBe( "component" );
+
+ var singletonAttr = findAttribute( componentTag, "singleton" );
+ expect( singletonAttr ).notToBeNull( "singleton attribute should exist" );
+ // BUG: Currently StringLiteral with value "", should be BooleanLiteral with value true
+ expect( singletonAttr.value.type ).toBe( "BooleanLiteral" );
+ expect( singletonAttr.value.value ).toBeTrue();
+ } );
+
+ // Script mode string attribute test - exit method="exitTag"
+ it( title="should parse string attribute as StringLiteral in script mode", body=function() {
+ var ast = getAst( "script-exit-method.cfm" );
+ var exitTag = findScriptTag( ast, "exit" );
+
+ expect( exitTag ).notToBeNull();
+ var attr = findAttribute( exitTag, "method" );
+ expect( attr ).notToBeNull();
+ // Should be StringLiteral, not CastExpression(AssignmentExpression)
+ expect( attr.value.type ).toBe( "StringLiteral" );
+ expect( attr.value.value ).toBe( "exitTag" );
+ } );
+
+ // Parenthesized script tag attributes - throw (message="test")
+ // Same bug as above but with parentheses around attributes
+ it( title="should parse parenthesized string attribute as StringLiteral", body=function() {
+ var ast = astFromString( 'throw (message="test");', "script" );
+
+ expect( ast.body ).toBeArray();
+ expect( ast.body.len() ).toBe( 1 );
+ var throwTag = ast.body[ 1 ];
+ expect( throwTag.type ).toBe( "CFMLTag" );
+ expect( throwTag.name ).toBe( "throw" );
+
+ var attr = findAttribute( throwTag, "message" );
+ expect( attr ).notToBeNull();
+ // BUG: Currently AssignmentExpression, should be StringLiteral
+ expect( attr.value.type ).toBe( "StringLiteral", "Attribute value should be StringLiteral, not AssignmentExpression" );
+ expect( attr.value.value ).toBe( "test" );
+ } );
+
+ });
+ }
+
+ private function getAst( required string filename ) {
+ var testDir = getDirectoryFromPath( getCurrentTemplatePath() );
+ var filePath = testDir & "LDEV6032/" & filename;
+ return astFromPath( filePath );
+ }
+
+ private function findTag( required struct ast, required string tagName ) {
+ var body = ast.keyExists( "body" ) ? ( isArray( ast.body ) ? ast.body : ast.body.body ?: [] ) : [];
+ for ( var node in body ) {
+ if ( node.type == "CFMLTag" && node.name == tagName ) {
+ return node;
+ }
+ }
+ return javaCast( "null", 0 );
+ }
+
+ private function findScriptTag( required struct ast, required string tagName ) {
+ var body = ast.keyExists( "body" ) ? ( isArray( ast.body ) ? ast.body : ast.body.body ?: [] ) : [];
+ for ( var node in body ) {
+ // Script island
+ if ( node.type == "CFMLTag" && node.name == "script" && node.keyExists( "body" ) ) {
+ var scriptBody = node.body.body ?: [];
+ for ( var scriptNode in scriptBody ) {
+ if ( scriptNode.type == "CFMLTag" && lCase( scriptNode.name ) == lCase( tagName ) ) {
+ return scriptNode;
+ }
+ }
+ }
+ }
+ return javaCast( "null", 0 );
+ }
+
+ private function findAttribute( required struct tag, required string attrName ) {
+ var attrs = tag.attributes ?: [];
+ for ( var attr in attrs ) {
+ if ( lCase( attr.name ) == lCase( attrName ) ) {
+ return attr;
+ }
+ }
+ return javaCast( "null", 0 );
+ }
+
+}
diff --git a/test/tickets/LDEV6032/script-bool-false.cfm b/test/tickets/LDEV6032/script-bool-false.cfm
new file mode 100644
index 00000000000..20b391d3ca4
--- /dev/null
+++ b/test/tickets/LDEV6032/script-bool-false.cfm
@@ -0,0 +1 @@
+silent bufferOutput=false { }
\ No newline at end of file
diff --git a/test/tickets/LDEV6032/script-bool-true.cfm b/test/tickets/LDEV6032/script-bool-true.cfm
new file mode 100644
index 00000000000..46739cbe2d0
--- /dev/null
+++ b/test/tickets/LDEV6032/script-bool-true.cfm
@@ -0,0 +1 @@
+setting showDebugOutput=true;
\ No newline at end of file
diff --git a/test/tickets/LDEV6032/script-exit-method.cfm b/test/tickets/LDEV6032/script-exit-method.cfm
new file mode 100644
index 00000000000..13a13167414
--- /dev/null
+++ b/test/tickets/LDEV6032/script-exit-method.cfm
@@ -0,0 +1,3 @@
+
+exit method="exitTag";
+
diff --git a/test/tickets/LDEV6032/script-naked-singleton.cfc b/test/tickets/LDEV6032/script-naked-singleton.cfc
new file mode 100644
index 00000000000..1061c630acf
--- /dev/null
+++ b/test/tickets/LDEV6032/script-naked-singleton.cfc
@@ -0,0 +1 @@
+component singleton {}
diff --git a/test/tickets/LDEV6032/tag-bool-false.cfm b/test/tickets/LDEV6032/tag-bool-false.cfm
new file mode 100644
index 00000000000..7926f347984
--- /dev/null
+++ b/test/tickets/LDEV6032/tag-bool-false.cfm
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test/tickets/LDEV6032/tag-bool-standalone.cfm b/test/tickets/LDEV6032/tag-bool-standalone.cfm
new file mode 100644
index 00000000000..28c4c26e464
--- /dev/null
+++ b/test/tickets/LDEV6032/tag-bool-standalone.cfm
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test/tickets/LDEV6032/tag-bool-true.cfm b/test/tickets/LDEV6032/tag-bool-true.cfm
new file mode 100644
index 00000000000..7955c9f184f
--- /dev/null
+++ b/test/tickets/LDEV6032/tag-bool-true.cfm
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test/tickets/LDEV6034.cfc b/test/tickets/LDEV6034.cfc
new file mode 100644
index 00000000000..de0b7ecd68e
--- /dev/null
+++ b/test/tickets/LDEV6034.cfc
@@ -0,0 +1,42 @@
+component extends="org.lucee.cfml.test.LuceeTestCase" labels="ast" {
+
+ function run( testResults, testBox ) {
+
+ describe( "LDEV-6034: AST should not expose auto-generated closure/lambda names", function() {
+
+ it( "should not include internal name for anonymous closure", function() {
+ var ast = astFromString( 'arr.each( function( item ) { return item; } );', "script" );
+ // body[1] is CallExpression directly (no ExpressionStatement wrapper)
+ var callExpr = ast.body[1];
+ expect( callExpr.type ).toBe( "CallExpression" );
+ var closureArg = callExpr.arguments[1];
+ expect( closureArg.type ).toBe( "ClosureDeclaration" );
+ // Anonymous closure should NOT have a name field - it's internal implementation detail
+ expect( closureArg ).notToHaveKey( "name", "Anonymous closure should not expose auto-generated internal name" );
+ });
+
+ it( "should not include internal name for lambda expression", function() {
+ var ast = astFromString( 'x = () => 1;', "script" );
+ var assignment = ast.body[1];
+ expect( assignment.type ).toBe( "AssignmentExpression" );
+ var lambda = assignment.right;
+ expect( lambda.type ).toBe( "LambdaDeclaration" );
+ // Lambda should NOT have a name field - it's internal implementation detail
+ expect( lambda ).notToHaveKey( "name", "Lambda should not expose auto-generated internal name" );
+ });
+
+ it( "should not include internal name for inline lambda in array method", function() {
+ var ast = astFromString( '[1,2,3].map( ( x ) => x * 2 );', "script" );
+ // body[1] is CallExpression directly
+ var callExpr = ast.body[1];
+ expect( callExpr.type ).toBe( "CallExpression" );
+ var lambdaArg = callExpr.arguments[1];
+ expect( lambdaArg.type ).toBe( "LambdaDeclaration" );
+ // Lambda should NOT have a name field
+ expect( lambdaArg ).notToHaveKey( "name", "Inline lambda should not expose auto-generated internal name" );
+ });
+
+ });
+ }
+
+}
diff --git a/test/tickets/LDEV6035.cfc b/test/tickets/LDEV6035.cfc
new file mode 100644
index 00000000000..6c5342037f5
--- /dev/null
+++ b/test/tickets/LDEV6035.cfc
@@ -0,0 +1,100 @@
+component extends="org.lucee.cfml.test.LuceeTestCase" labels="ast" {
+
+ function run( testResults, testBox ) {
+
+ describe( "LDEV-6035: AST fullname field should be consistent for shorthand vs cf-prefixed tags", function() {
+
+ it( "should include fullname for cf-prefixed script tag", function() {
+ var code = 'cfabort;';
+ var ast = astFromString( code, "script" );
+
+ var tag = ast.body[1];
+ expect( tag.type ).toBe( "CFMLTag" );
+ expect( tag.name ).toBe( "ABORT" );
+ expect( tag ).toHaveKey( "fullname", "cf-prefixed tag should have fullname" );
+ expect( tag.fullname ).toBe( "cfabort" );
+ });
+
+ it( "should include fullname for shorthand script tag", function() {
+ var code = 'abort;';
+ var ast = astFromString( code, "script" );
+
+ var tag = ast.body[1];
+ expect( tag.type ).toBe( "CFMLTag" );
+ expect( tag.name ).toBe( "ABORT" );
+ // Bug: shorthand tag lacks fullname field
+ expect( tag ).toHaveKey( "fullname",
+ "Shorthand tag should have fullname for consistency" );
+ expect( tag.fullname ).toBe( "abort",
+ "Shorthand fullname should be 'abort', not 'cfabort'" );
+ });
+
+ it( "should include fullname for cf-prefixed tag with attributes", function() {
+ var code = 'cfdump( var=x );';
+ var ast = astFromString( code, "script" );
+
+ var tag = ast.body[1];
+ expect( tag.type ).toBe( "CFMLTag" );
+ expect( tag.name ).toBe( "DUMP" );
+ expect( tag ).toHaveKey( "fullname" );
+ expect( tag.fullname ).toBe( "cfdump" );
+ });
+
+ it( "should include fullname for shorthand tag with named param syntax", function() {
+ // Note: dump( var=x ) parses as CallExpression, not CFMLTag
+ // Use cfhttp which has no function equivalent
+ var code = 'http url="http://example.com" {}';
+ var ast = astFromString( code, "script" );
+
+ var tag = ast.body[1];
+ expect( tag.type ).toBe( "CFMLTag" );
+ expect( tag.name ).toBe( "HTTP" );
+ // Bug: shorthand tag lacks fullname field
+ expect( tag ).toHaveKey( "fullname",
+ "Shorthand tag should have fullname" );
+ expect( tag.fullname ).toBe( "http" );
+ });
+
+ it( "should include fullname for block tags", function() {
+ var codeShorthand = 'loop from=1 to=10 index="i" {}';
+ var codePrefixed = 'cfloop( from=1, to=10, index="i" ) {}';
+
+ var astShorthand = astFromString( codeShorthand, "script" );
+ var astPrefixed = astFromString( codePrefixed, "script" );
+
+ var tagShorthand = astShorthand.body[1];
+ var tagPrefixed = astPrefixed.body[1];
+
+ expect( tagPrefixed ).toHaveKey( "fullname" );
+ expect( tagPrefixed.fullname ).toBe( "cfloop" );
+
+ // Bug: shorthand lacks fullname
+ expect( tagShorthand ).toHaveKey( "fullname",
+ "Shorthand block tag should have fullname" );
+ expect( tagShorthand.fullname ).toBe( "loop" );
+ });
+
+ it( "should distinguish shorthand from cf-prefixed in round-trip", function() {
+ // The key issue: after round-trip, we lose info about original syntax
+ var shorthandCode = 'abort;';
+ var prefixedCode = 'cfabort;';
+
+ var shorthandAst = astFromString( shorthandCode, "script" );
+ var prefixedAst = astFromString( prefixedCode, "script" );
+
+ // Both have same name
+ expect( shorthandAst.body[1].name ).toBe( prefixedAst.body[1].name );
+
+ // But fullname should differ to preserve original syntax
+ if ( shorthandAst.body[1].keyExists( "fullname" ) &&
+ prefixedAst.body[1].keyExists( "fullname" ) ) {
+ expect( shorthandAst.body[1].fullname ).notToBe( prefixedAst.body[1].fullname,
+ "fullname should differ between shorthand and cf-prefixed" );
+ }
+ });
+
+ });
+
+ }
+
+}
diff --git a/test/tickets/LDEV6036.cfc b/test/tickets/LDEV6036.cfc
new file mode 100644
index 00000000000..86315026943
--- /dev/null
+++ b/test/tickets/LDEV6036.cfc
@@ -0,0 +1,374 @@
+component extends="org.lucee.cfml.test.LuceeTestCase" labels="ast" {
+
+ variables.testDir = getDirectoryFromPath( getCurrentTemplatePath() ) & "LDEV6036/";
+
+ function run( testResults, testBox ) {
+ describe( "LDEV-6036: AST should not add default attribute values", function() {
+
+ describe( "Script-style properties", function() {
+
+ it( "property without type should NOT have type attribute in AST", function() {
+ var ast = astFromPath( variables.testDir & "propertyNoType.cfc" );
+
+ // Find the first property (no type in source)
+ var prop = findProperty( ast, "Mixins" );
+ expect( prop ).notToBeNull( "Mixins property should be found in AST" );
+
+ // Convert to struct for easier checking
+ var attrs = attrsToStruct( prop );
+
+ // Should have name and inject only
+ expect( attrs ).toHaveKey( "name", "property should have name attribute" );
+ expect( attrs.name.value ).toBe( "Mixins" );
+
+ expect( attrs ).toHaveKey( "inject", "property should have inject attribute" );
+ expect( attrs.inject.value ).toBe( "id:Plugins" );
+
+ // Check attributes - should NOT contain type="any"
+ expect( attrs ).notToHaveKey( "type", "property without type in source should NOT have type attribute in AST" );
+ });
+
+ it( "property WITH explicit type should have type attribute in AST", function() {
+ var ast = astFromPath( variables.testDir & "propertyNoType.cfc" );
+
+ // Find the second property (has type="string" in source)
+ var prop = findProperty( ast, "WithType" );
+ expect( prop ).notToBeNull( "WithType property should be found in AST" );
+
+ // Convert to struct for easier checking
+ var attrs = attrsToStruct( prop );
+
+ // Should have type attribute since it was in source
+ expect( attrs ).toHaveKey( "type", "property with type in source should have type attribute in AST" );
+ expect( attrs.type.value ).toBe( "string" );
+ });
+
+ it( "property WITH explicit type='any' should preserve type attribute with quoteChar", function() {
+ var ast = astFromPath( variables.testDir & "propertyNoType.cfc" );
+
+ // Find the property with explicit type="any"
+ var prop = findProperty( ast, "ExplicitAny" );
+ expect( prop ).notToBeNull( "ExplicitAny property should be found in AST" );
+
+ // Convert to struct for easier checking
+ var attrs = attrsToStruct( prop );
+
+ // Should have type attribute since it was explicitly in source
+ expect( attrs ).toHaveKey( "type", "property with explicit type='any' should have type attribute" );
+ expect( attrs.type.value ).toBe( "any" );
+ // CRITICAL: explicit type="any" should have quoteChar (proving it came from source)
+ expect( attrs.type ).toHaveKey( "quoteChar", "explicit type='any' should have quoteChar (from source)" );
+ });
+
+ it( "only source attributes should have quoteChar", function() {
+ var ast = astFromPath( variables.testDir & "propertyNoType.cfc" );
+
+ var prop = findProperty( ast, "Mixins" );
+ expect( prop ).notToBeNull( "Mixins property should be found in AST" );
+
+ // Convert to struct for easier checking
+ var attrs = attrsToStruct( prop );
+
+ // Source attributes should have quoteChar
+ expect( attrs ).toHaveKey( "name" );
+ expect( attrs.name ).toHaveKey( "quoteChar", "source attribute should have quoteChar" );
+
+ // If type exists (bug), it should NOT have quoteChar (proving it's a default)
+ if ( structKeyExists( attrs, "type" ) ) {
+ // If we get here, the bug exists - the default was added
+ // Check that at least it lacks quoteChar (confirming it's not from source)
+ expect( attrs.type ).notToHaveKey( "quoteChar",
+ "default type attribute should NOT have quoteChar (proving it was added, not from source)"
+ );
+ }
+ });
+
+ });
+
+ describe( "Tag-style properties", function() {
+
+ it( "cfproperty without type should NOT have type attribute in AST", function() {
+ var ast = astFromPath( variables.testDir & "propertyNoTypeTag.cfc" );
+
+ // Find the first property (no type in source)
+ var prop = findProperty( ast, "TagMixins" );
+ expect( prop ).notToBeNull( "TagMixins property should be found in AST" );
+
+ // Convert to struct for easier checking
+ var attrs = attrsToStruct( prop );
+
+ // Should have name and inject only
+ expect( attrs ).toHaveKey( "name", "property should have name attribute" );
+ expect( attrs.name.value ).toBe( "TagMixins" );
+
+ expect( attrs ).toHaveKey( "inject", "property should have inject attribute" );
+ expect( attrs.inject.value ).toBe( "id:Plugins" );
+
+ // Check attributes - should NOT contain type="any"
+ expect( attrs ).notToHaveKey( "type", "cfproperty without type in source should NOT have type attribute in AST" );
+ });
+
+ it( "cfproperty WITH explicit type should have type attribute in AST", function() {
+ var ast = astFromPath( variables.testDir & "propertyNoTypeTag.cfc" );
+
+ // Find the second property (has type="string" in source)
+ var prop = findProperty( ast, "TagWithType" );
+ expect( prop ).notToBeNull( "TagWithType property should be found in AST" );
+
+ // Convert to struct for easier checking
+ var attrs = attrsToStruct( prop );
+
+ // Should have type attribute since it was in source
+ expect( attrs ).toHaveKey( "type", "cfproperty with type in source should have type attribute in AST" );
+ expect( attrs.type.value ).toBe( "string" );
+ });
+
+ it( "cfproperty WITH explicit type='any' should preserve type attribute with quoteChar", function() {
+ var ast = astFromPath( variables.testDir & "propertyNoTypeTag.cfc" );
+
+ // Find the property with explicit type="any"
+ var prop = findProperty( ast, "TagExplicitAny" );
+ expect( prop ).notToBeNull( "TagExplicitAny property should be found in AST" );
+
+ // Convert to struct for easier checking
+ var attrs = attrsToStruct( prop );
+
+ // Should have type attribute since it was explicitly in source
+ expect( attrs ).toHaveKey( "type", "cfproperty with explicit type='any' should have type attribute" );
+ expect( attrs.type.value ).toBe( "any" );
+ // CRITICAL: explicit type="any" should have quoteChar (proving it came from source)
+ expect( attrs.type ).toHaveKey( "quoteChar", "explicit type='any' should have quoteChar (from source)" );
+ });
+
+ });
+
+ describe( "Script-style functions", function() {
+
+ it( "function without access should NOT have accessExplicit=true in AST", function() {
+ var ast = astFromPath( variables.testDir & "functionNoModifiers.cfc" );
+
+ // Find function with no modifiers
+ var fn = findFunction( ast, "noModifiers" );
+ expect( fn ).notToBeNull( "noModifiers function should be found in AST" );
+
+ // access should exist (default public), but accessExplicit should NOT exist
+ expect( fn ).toHaveKey( "access", "function should have access (default)" );
+ expect( fn ).notToHaveKey( "accessExplicit", "function without access in source should NOT have accessExplicit" );
+ });
+
+ it( "function without returnType should have returnType.explicit=false in AST", function() {
+ var ast = astFromPath( variables.testDir & "functionNoModifiers.cfc" );
+
+ // Find function with no modifiers
+ var fn = findFunction( ast, "noModifiers" );
+ expect( fn ).notToBeNull( "noModifiers function should be found in AST" );
+
+ // returnType should exist (default any), but explicit should be false/missing
+ expect( fn ).toHaveKey( "returnType", "function should have returnType (default)" );
+ expect( fn.returnType ).notToHaveKey( "explicit", "function without returnType in source should NOT have returnType.explicit" );
+ });
+
+ it( "function WITH explicit public should have accessExplicit=true in AST", function() {
+ var ast = astFromPath( variables.testDir & "functionNoModifiers.cfc" );
+
+ var fn = findFunction( ast, "explicitPublic" );
+ expect( fn ).notToBeNull( "explicitPublic function should be found in AST" );
+
+ // Should have access and accessExplicit since it was explicitly in source
+ expect( fn ).toHaveKey( "access", "function with explicit public should have access in AST" );
+ expect( fn.access ).toBe( "public" );
+ expect( fn ).toHaveKey( "accessExplicit", "function with explicit public should have accessExplicit" );
+ expect( fn.accessExplicit ).toBeTrue( "accessExplicit should be true for explicit public" );
+ });
+
+ it( "function WITH explicit returnType should have returnType.explicit=true in AST", function() {
+ var ast = astFromPath( variables.testDir & "functionNoModifiers.cfc" );
+
+ var fn = findFunction( ast, "explicitAnyReturn" );
+ expect( fn ).notToBeNull( "explicitAnyReturn function should be found in AST" );
+
+ // Should have returnType with explicit=true since it was explicitly in source
+ expect( fn ).toHaveKey( "returnType", "function with explicit any return should have returnType in AST" );
+ expect( fn.returnType.value ).toBe( "any" );
+ expect( fn.returnType ).toHaveKey( "explicit", "function with explicit any should have returnType.explicit" );
+ expect( fn.returnType.explicit ).toBeTrue( "returnType.explicit should be true for explicit any" );
+ });
+
+ it( "function WITH explicit public any should have both explicit flags in AST", function() {
+ var ast = astFromPath( variables.testDir & "functionNoModifiers.cfc" );
+
+ var fn = findFunction( ast, "explicitBoth" );
+ expect( fn ).notToBeNull( "explicitBoth function should be found in AST" );
+
+ expect( fn ).toHaveKey( "access", "function with explicit public should have access in AST" );
+ expect( fn.access ).toBe( "public" );
+ expect( fn ).toHaveKey( "accessExplicit", "function with explicit public should have accessExplicit" );
+ expect( fn.accessExplicit ).toBeTrue( "accessExplicit should be true" );
+
+ expect( fn ).toHaveKey( "returnType", "function with explicit any return should have returnType in AST" );
+ expect( fn.returnType.value ).toBe( "any" );
+ expect( fn.returnType ).toHaveKey( "explicit", "function with explicit any should have returnType.explicit" );
+ expect( fn.returnType.explicit ).toBeTrue( "returnType.explicit should be true" );
+ });
+
+ it( "function with only returnType should NOT have accessExplicit in AST", function() {
+ var ast = astFromPath( variables.testDir & "functionNoModifiers.cfc" );
+
+ var fn = findFunction( ast, "onlyReturnType" );
+ expect( fn ).notToBeNull( "onlyReturnType function should be found in AST" );
+
+ // access should exist (default), but accessExplicit should NOT exist
+ expect( fn ).toHaveKey( "access", "function should have access (default)" );
+ expect( fn ).notToHaveKey( "accessExplicit", "function without access in source should NOT have accessExplicit" );
+
+ // Should have returnType with explicit=true since it was explicitly in source
+ expect( fn ).toHaveKey( "returnType", "function with explicit string return should have returnType in AST" );
+ expect( fn.returnType.value ).toBe( "string" );
+ expect( fn.returnType ).toHaveKey( "explicit", "function with explicit string should have returnType.explicit" );
+ expect( fn.returnType.explicit ).toBeTrue( "returnType.explicit should be true" );
+ });
+
+ it( "function with only access should NOT have returnType.explicit in AST", function() {
+ var ast = astFromPath( variables.testDir & "functionNoModifiers.cfc" );
+
+ var fn = findFunction( ast, "onlyAccess" );
+ expect( fn ).notToBeNull( "onlyAccess function should be found in AST" );
+
+ // Should have access and accessExplicit since it was explicitly in source
+ expect( fn ).toHaveKey( "access", "function with explicit private should have access in AST" );
+ expect( fn.access ).toBe( "private" );
+ expect( fn ).toHaveKey( "accessExplicit", "function with explicit private should have accessExplicit" );
+ expect( fn.accessExplicit ).toBeTrue( "accessExplicit should be true for explicit private" );
+
+ // returnType should exist (default), but explicit should NOT exist
+ expect( fn ).toHaveKey( "returnType", "function should have returnType (default)" );
+ expect( fn.returnType ).notToHaveKey( "explicit", "function without returnType in source should NOT have returnType.explicit" );
+ });
+
+ });
+
+ });
+ }
+
+ /**
+ * Find a function by name in the AST
+ */
+ private function findFunction( required struct node, required string name ) {
+ var nodeType = node.type ?: "";
+
+ // Check if this is a FunctionDeclaration with matching name
+ if ( nodeType == "FunctionDeclaration" ) {
+ var fnName = "";
+ // AST uses "name" with StringLiteral containing function name
+ if ( structKeyExists( node, "name" ) && isStruct( node.name ) ) {
+ fnName = node.name.value ?: "";
+ }
+ if ( uCase( fnName ) == uCase( name ) ) {
+ return node;
+ }
+ }
+
+ // Recurse into body
+ if ( structKeyExists( node, "body" ) ) {
+ if ( isStruct( node.body ) ) {
+ var result = findFunction( node.body, name );
+ if ( !isNull( result ) ) return result;
+ } else if ( isArray( node.body ) ) {
+ for ( var child in node.body ) {
+ if ( isStruct( child ) ) {
+ var result = findFunction( child, name );
+ if ( !isNull( result ) ) return result;
+ }
+ }
+ }
+ }
+
+ // Recurse into body.body (for components)
+ if ( structKeyExists( node, "body" ) && isStruct( node.body ) && structKeyExists( node.body, "body" ) ) {
+ if ( isArray( node.body.body ) ) {
+ for ( var child in node.body.body ) {
+ if ( isStruct( child ) ) {
+ var result = findFunction( child, name );
+ if ( !isNull( result ) ) return result;
+ }
+ }
+ }
+ }
+
+ return;
+ }
+
+ /**
+ * Find a property by name in the AST
+ */
+ private function findProperty( required struct node, required string name ) {
+ var nodeType = node.type ?: "";
+
+ // Check if this is a property with matching name
+ if ( nodeType == "CFMLTag" && ( node.name ?: "" ) == "property" ) {
+ var nameAttr = getAttr( node, "name" );
+ if ( !isNull( nameAttr ) && uCase( nameAttr.value.value ?: "" ) == uCase( name ) ) {
+ return node;
+ }
+ }
+
+ // Recurse into body
+ if ( structKeyExists( node, "body" ) ) {
+ if ( isStruct( node.body ) ) {
+ var result = findProperty( node.body, name );
+ if ( !isNull( result ) ) return result;
+ } else if ( isArray( node.body ) ) {
+ for ( var child in node.body ) {
+ if ( isStruct( child ) ) {
+ var result = findProperty( child, name );
+ if ( !isNull( result ) ) return result;
+ }
+ }
+ }
+ }
+
+ // Recurse into body.body (for components)
+ if ( structKeyExists( node, "body" ) && isStruct( node.body ) && structKeyExists( node.body, "body" ) ) {
+ if ( isArray( node.body.body ) ) {
+ for ( var child in node.body.body ) {
+ if ( isStruct( child ) ) {
+ var result = findProperty( child, name );
+ if ( !isNull( result ) ) return result;
+ }
+ }
+ }
+ }
+
+ return;
+ }
+
+ /**
+ * Get an attribute from a tag node by name
+ */
+ private function getAttr( required struct node, required string name ) {
+ if ( !structKeyExists( node, "attributes" ) || !isArray( node.attributes ) ) {
+ return;
+ }
+ for ( var attr in node.attributes ) {
+ if ( uCase( attr.name ?: "" ) == uCase( name ) ) {
+ return attr;
+ }
+ }
+ return;
+ }
+
+ /**
+ * Convert attributes array to struct keyed by attribute name
+ */
+ private struct function attrsToStruct( required struct node ) {
+ var result = {};
+ if ( !structKeyExists( node, "attributes" ) || !isArray( node.attributes ) ) {
+ return result;
+ }
+ for ( var attr in node.attributes ) {
+ result[ attr.name ] = attr.value;
+ }
+ return result;
+ }
+
+}
diff --git a/test/tickets/LDEV6036/functionNoModifiers.cfc b/test/tickets/LDEV6036/functionNoModifiers.cfc
new file mode 100644
index 00000000000..bdf714c4852
--- /dev/null
+++ b/test/tickets/LDEV6036/functionNoModifiers.cfc
@@ -0,0 +1,21 @@
+component {
+ // No modifiers at all - should NOT have access or returnType in AST
+ function noModifiers() {}
+
+ // Explicit public only - should have access="public" but NOT returnType
+ public function explicitPublic() {}
+
+ // Explicit any return only - should have returnType="any" but NOT access
+ any function explicitAnyReturn() {}
+
+ // Both explicit - should have both access="public" and returnType="any"
+ public any function explicitBoth() {}
+
+ // Only return type (string) - should have returnType but NOT access
+ string function onlyReturnType() {
+ return "";
+ }
+
+ // Only access (private) - should have access but NOT returnType
+ private function onlyAccess() {}
+}
diff --git a/test/tickets/LDEV6036/propertyNoType.cfc b/test/tickets/LDEV6036/propertyNoType.cfc
new file mode 100644
index 00000000000..fbb56630949
--- /dev/null
+++ b/test/tickets/LDEV6036/propertyNoType.cfc
@@ -0,0 +1,8 @@
+component {
+ // Script style - no type attribute
+ property name="Mixins" inject="id:Plugins";
+ // Script style - WITH explicit type
+ property name="WithType" type="string" inject="id:Config";
+ // Script style - WITH explicit type="any" (should be preserved!)
+ property name="ExplicitAny" type="any" inject="id:Service";
+}
diff --git a/test/tickets/LDEV6036/propertyNoTypeTag.cfc b/test/tickets/LDEV6036/propertyNoTypeTag.cfc
new file mode 100644
index 00000000000..f1e0b6f3d9c
--- /dev/null
+++ b/test/tickets/LDEV6036/propertyNoTypeTag.cfc
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/test/tickets/LDEV6038.cfc b/test/tickets/LDEV6038.cfc
new file mode 100644
index 00000000000..0f2b503ceaf
--- /dev/null
+++ b/test/tickets/LDEV6038.cfc
@@ -0,0 +1,48 @@
+component extends="org.lucee.cfml.test.LuceeTestCase" labels="ast" {
+
+ function run( testResults, testBox ) {
+ describe( "LDEV-6038: Comments should not leave ghost nodes in AST", function() {
+
+ it( "should not create empty StringLiteral placeholder for stripped comments", function() {
+ var ast = astFromPath( getDirectoryFromPath( getCurrentTemplatePath() ) & "LDEV6038/test.cfm" );
+
+ // Source:
+ // Should have 1 body element (the cfset tag)
+ // Bug: Has 2 body elements (cfset + empty StringLiteral ghost node)
+
+ expect( ast.body ).toBeArray();
+ expect( ast.body.len() ).toBe( 1, "AST should have exactly 1 body element, not a ghost node for the comment" );
+ expect( ast.body[1].type ).toBe( "CFMLTag" );
+ expect( ast.body[1].name ).toBe( "set" );
+ });
+
+ it( "should not create ghost nodes for comments between tags", function() {
+ var code = '';
+ var ast = astFromString( code );
+
+ // Should have 2 body elements (two cfset tags)
+ // Bug: Has 3 body elements (cfset + empty ghost + cfset)
+
+ expect( ast.body ).toBeArray();
+ expect( ast.body.len() ).toBe( 2, "AST should have exactly 2 body elements" );
+ expect( ast.body[1].name ).toBe( "set" );
+ expect( ast.body[2].name ).toBe( "set" );
+ });
+
+ it( "should not create ghost nodes for trailing comments", function() {
+ var code = 'content';
+ var ast = astFromString( code );
+
+ // Should have 1 body element (the cfmodule tag)
+ // Bug: Has 2 body elements (cfmodule + empty StringLiteral ghost)
+
+ expect( ast.body ).toBeArray();
+ expect( ast.body.len() ).toBe( 1, "AST should have exactly 1 body element for cfmodule" );
+ expect( ast.body[1].type ).toBe( "CFMLTag" );
+ expect( ast.body[1].name ).toBe( "module" );
+ });
+
+ });
+ }
+
+}
diff --git a/test/tickets/LDEV6038/test.cfm b/test/tickets/LDEV6038/test.cfm
new file mode 100644
index 00000000000..cc6c59935be
--- /dev/null
+++ b/test/tickets/LDEV6038/test.cfm
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test/tickets/LDEV6041.cfc b/test/tickets/LDEV6041.cfc
new file mode 100644
index 00000000000..50f5d113c46
--- /dev/null
+++ b/test/tickets/LDEV6041.cfc
@@ -0,0 +1,238 @@
+component extends="org.lucee.cfml.test.LuceeTestCase" labels="ast" {
+
+ function run( testResults, testBox ) {
+
+ describe( "LDEV-6041: AST should include original source fidelity", function() {
+
+ describe( "2a. Identifier case preservation", function() {
+
+ it( "should have raw property on Identifier with original casing", function() {
+ var ast = astFromString( 'myVar = someFunc( argName=value );', "script" );
+ var assignment = ast.body[1];
+
+ // Left side - myVar identifier
+ var leftId = assignment.left;
+ expect( leftId.type ).toBe( "Identifier" );
+ expect( leftId.name ).toBe( "MYVAR", "name should be uppercased" );
+ expect( leftId ).toHaveKey( "raw", "Identifier should have raw property with original casing" );
+ expect( leftId.raw ).toBeWithCase( "myVar", "raw should preserve original case" );
+ });
+
+ it( "should preserve case for UDF callee names", function() {
+ var ast = astFromString( 'someFunc();', "script" );
+ var call = ast.body[1];
+
+ expect( call.type ).toBe( "CallExpression" );
+ var callee = call.callee;
+ expect( callee.type ).toBe( "Identifier" );
+ expect( callee.name ).toBe( "SOMEFUNC", "name should be uppercased" );
+ expect( callee ).toHaveKey( "raw", "UDF callee should have raw property" );
+ expect( callee.raw ).toBeWithCase( "someFunc", "raw should preserve original case" );
+ });
+
+ it( "should preserve case for named argument names", function() {
+ var ast = astFromString( 'test( argName=1 );', "script" );
+ var call = ast.body[1];
+ var namedArg = call.arguments[1];
+
+ expect( namedArg.type ).toBe( "NamedArgument" );
+ var nameId = namedArg.name;
+ expect( nameId.type ).toBe( "Identifier" );
+ expect( nameId.name ).toBe( "ARGNAME", "name should be uppercased" );
+ expect( nameId ).toHaveKey( "raw", "named argument identifier should have raw property" );
+ expect( nameId.raw ).toBeWithCase( "argName", "raw should preserve original case" );
+ });
+
+ it( "should preserve case for BIF callee names", function() {
+ var ast = astFromString( 'arrayAppend( arr, val );', "script" );
+ var call = ast.body[1];
+
+ expect( call.type ).toBe( "CallExpression" );
+ expect( call.isBuiltIn ).toBeTrue( "should be marked as BIF" );
+ var callee = call.callee;
+ expect( callee.type ).toBe( "Identifier" );
+ expect( callee ).toHaveKey( "raw", "BIF callee should have raw property" );
+ expect( callee.raw ).toBeWithCase( "arrayAppend", "raw should preserve original case" );
+ });
+
+ // TODO: Scope identifiers (request, session, etc) are resolved to scope integers during parsing
+ // and the original identifier casing is discarded. Would require parser changes to preserve.
+ xit( "should preserve case for scope prefixes", function() {
+ var ast = astFromString( 'request.myKey = 1;', "script" );
+ var assignment = ast.body[1];
+ var memberExpr = assignment.left;
+
+ expect( memberExpr.type ).toBe( "MemberExpression" );
+ var scopeId = memberExpr.object;
+ expect( scopeId.type ).toBe( "Identifier" );
+ expect( scopeId.name ).toBe( "REQUEST", "scope name should be uppercased" );
+ expect( scopeId ).toHaveKey( "raw", "scope identifier should have raw property" );
+ expect( scopeId.raw ).toBeWithCase( "request", "raw should preserve original lowercase" );
+ });
+
+ // TODO: Same as above - scope identifier casing not preserved
+ xit( "should preserve case for uppercase scope", function() {
+ var ast = astFromString( 'REQUEST.myKey = 1;', "script" );
+ var assignment = ast.body[1];
+ var memberExpr = assignment.left;
+ var scopeId = memberExpr.object;
+
+ expect( scopeId.name ).toBe( "REQUEST" );
+ expect( scopeId ).toHaveKey( "raw", "scope identifier should have raw property" );
+ expect( scopeId.raw ).toBeWithCase( "REQUEST", "raw should preserve original uppercase" );
+ });
+
+ });
+
+ describe( "2b. var keyword preservation", function() {
+
+ it( "should have declaration property when var keyword used", function() {
+ var ast = astFromString( 'function test() { var x = 1; }', "script" );
+ var func = ast.body[1];
+ var assignment = func.body.body[1];
+
+ expect( assignment.type ).toBe( "AssignmentExpression" );
+ expect( assignment ).toHaveKey( "declaration", "var declaration should have declaration property" );
+ expect( assignment.declaration ).toBe( "var", "declaration should be 'var'" );
+ });
+
+ it( "should NOT have declaration property for explicit local scope", function() {
+ var ast = astFromString( 'function test() { local.x = 1; }', "script" );
+ var func = ast.body[1];
+ var assignment = func.body.body[1];
+
+ expect( assignment.type ).toBe( "AssignmentExpression" );
+ expect( assignment ).notToHaveKey( "declaration", "explicit local.x should not have declaration property" );
+ });
+
+ it( "should distinguish var x from local.x", function() {
+ var ast1 = astFromString( 'function test() { var x = 1; }', "script" );
+ var ast2 = astFromString( 'function test() { local.x = 1; }', "script" );
+
+ var assignment1 = ast1.body[1].body.body[1];
+ var assignment2 = ast2.body[1].body.body[1];
+
+ // Both produce MemberExpression with LOCAL.X, but var version should have declaration
+ expect( assignment1.left.type ).toBe( "MemberExpression" );
+ expect( assignment2.left.type ).toBe( "MemberExpression" );
+
+ var hasDecl1 = structKeyExists( assignment1, "declaration" ) && assignment1.declaration == "var";
+ var hasDecl2 = structKeyExists( assignment2, "declaration" ) && assignment2.declaration == "var";
+
+ expect( hasDecl1 ).toBeTrue( "var x should have declaration='var'" );
+ expect( hasDecl2 ).toBeFalse( "local.x should NOT have declaration property" );
+ });
+
+ it( "should preserve var keyword in for loop initializer", function() {
+ var ast = astFromString( 'for( var i = 1; i <= 10; i++ ) {}', "script" );
+ var forStmt = ast.body[1];
+ var init = forStmt.init;
+
+ expect( init.type ).toBe( "AssignmentExpression" );
+ expect( init ).toHaveKey( "declaration", "var in for loop should have declaration property" );
+ expect( init.declaration ).toBe( "var" );
+ });
+
+ it( "should preserve var keyword in for-in loop", function() {
+ var ast = astFromString( 'for( var item in arr ) {}', "script" );
+ var forStmt = ast.body[1];
+
+ expect( forStmt.type ).toBe( "ForOfStatement" );
+ expect( forStmt ).toHaveKey( "declaration", "var in for-in should have declaration property" );
+ expect( forStmt.declaration ).toBe( "var" );
+ });
+
+ });
+
+ describe( "2c. ScopeIdentifier node type", function() {
+
+ it( "should use ScopeIdentifier for scope references", function() {
+ var ast = astFromString( 'request.myKey = 1;', "script" );
+ var assignment = ast.body[1];
+ var memberExpr = assignment.left;
+
+ expect( memberExpr.type ).toBe( "MemberExpression" );
+ var scopeId = memberExpr.object;
+ expect( scopeId.type ).toBe( "ScopeIdentifier", "scope reference should be ScopeIdentifier not Identifier" );
+ expect( scopeId.name ).toBe( "REQUEST" );
+ expect( scopeId ).notToHaveKey( "raw", "ScopeIdentifier should not have raw (synthetic node)" );
+ });
+
+ it( "should use ScopeIdentifier for LOCAL scope from var keyword", function() {
+ var ast = astFromString( 'function test() { var x = 1; }', "script" );
+ var func = ast.body[1];
+ var assignment = func.body.body[1];
+ var memberExpr = assignment.left;
+
+ expect( memberExpr.type ).toBe( "MemberExpression" );
+ var scopeId = memberExpr.object;
+ expect( scopeId.type ).toBe( "ScopeIdentifier", "LOCAL from var should be ScopeIdentifier" );
+ expect( scopeId.name ).toBe( "LOCAL" );
+ });
+
+ it( "should use ScopeIdentifier for explicit local scope", function() {
+ var ast = astFromString( 'function test() { local.x = 1; }', "script" );
+ var func = ast.body[1];
+ var assignment = func.body.body[1];
+ var memberExpr = assignment.left;
+
+ expect( memberExpr.type ).toBe( "MemberExpression" );
+ var scopeId = memberExpr.object;
+ expect( scopeId.type ).toBe( "ScopeIdentifier", "explicit local should be ScopeIdentifier" );
+ expect( scopeId.name ).toBe( "LOCAL" );
+ });
+
+ it( "should use ScopeIdentifier for various scopes", function() {
+ var scopes = [ "session", "application", "url", "form", "cgi", "cookie", "server", "variables", "arguments" ];
+ for ( var scopeName in scopes ) {
+ var ast = astFromString( '#scopeName#.key = 1;', "script" );
+ var assignment = ast.body[1];
+ var memberExpr = assignment.left;
+ var scopeId = memberExpr.object;
+
+ expect( scopeId.type ).toBe( "ScopeIdentifier", "#scopeName# should be ScopeIdentifier" );
+ expect( scopeId.name ).toBe( uCase( scopeName ), "#scopeName# should be uppercased" );
+ }
+ });
+
+ });
+
+ describe( "2d. Implicit type annotations", function() {
+
+ it( "should distinguish implicit any return type from explicit", function() {
+ var ast1 = astFromString( 'function test() {}', "script" );
+ var ast2 = astFromString( 'any function test() {}', "script" );
+
+ var func1 = ast1.body[1];
+ var func2 = ast2.body[1];
+
+ // Both have returnType.value = "any", but only explicit should have it
+ var hasExplicit1 = structKeyExists( func1.returnType, "explicit" ) && func1.returnType.explicit;
+ var hasExplicit2 = structKeyExists( func2.returnType, "explicit" ) && func2.returnType.explicit;
+
+ expect( hasExplicit1 ).toBeFalse( "implicit any should not have explicit=true" );
+ expect( hasExplicit2 ).toBeTrue( "explicit any should have explicit=true" );
+ });
+
+ it( "should distinguish implicit any param type from explicit", function() {
+ var ast1 = astFromString( 'function test( arg ) {}', "script" );
+ var ast2 = astFromString( 'function test( any arg ) {}', "script" );
+
+ var param1 = ast1.body[1].params[1];
+ var param2 = ast2.body[1].params[1];
+
+ // Both have type.value = "any", but only explicit should have it
+ var hasExplicit1 = structKeyExists( param1.type, "explicit" ) && param1.type.explicit;
+ var hasExplicit2 = structKeyExists( param2.type, "explicit" ) && param2.type.explicit;
+
+ expect( hasExplicit1 ).toBeFalse( "implicit any param should not have explicit=true" );
+ expect( hasExplicit2 ).toBeTrue( "explicit any param should have explicit=true" );
+ });
+
+ });
+
+ });
+
+ }
+
+}