Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions core/src/main/java/lucee/runtime/ComponentImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -2442,9 +2442,12 @@ else if (propOwnerPS == null) {
}

top.properties.properties.put(propNameLower, propImpl);
if (propImpl.getDefaultAsObject() != null) {
Object defaultObj = propImpl.getDefaultAsObject();
if (defaultObj != null && !(defaultObj instanceof lucee.runtime.component.ExpressionDefault)) {
// Expression-form defaults are seeded by the pseudo-constructor's inline emission
// (see TagProperty.emitExpressionEvalAndSet); only literal defaults are eager-set here.
Key propKey = propImpl.getNameAsKey();
scope.setEL(propKey, propImpl.getDefaultAsObject());
scope.setEL(propKey, defaultObj);
if (ownPropertyDefaults == null) ownPropertyDefaults = new HashSet<>();
ownPropertyDefaults.add(propKey);
}
Expand Down
66 changes: 66 additions & 0 deletions core/src/main/java/lucee/runtime/component/ExpressionDefault.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package lucee.runtime.component;

import java.util.Set;

import lucee.commons.lang.StringUtil;
import lucee.runtime.PageContext;
import lucee.runtime.converter.ScriptConvertable;
import lucee.runtime.dump.DumpData;
import lucee.runtime.dump.DumpProperties;
import lucee.runtime.dump.DumpTable;
import lucee.runtime.dump.Dumpable;
import lucee.runtime.dump.SimpleDumpData;

/** Immutable wrapper for cfproperty expression-form defaults; surfaces in metadata.default. */
public final class ExpressionDefault implements Dumpable, ScriptConvertable {

// Required by Serializable (inherited via Dumpable) but never consulted: components rehydrate
// via ComponentImpl.readExternal which re-instantiates from the compiled class, not byte-level.
private static final long serialVersionUID = 1L;

private final String source;

public ExpressionDefault(String source) {
this.source = source == null ? "" : source;
}

public String getSource() {
return source;
}

@Override
public String toString() {
return source;
}

@Override
public DumpData toDumpData(PageContext pageContext, int maxlevel, DumpProperties properties) {
DumpTable table = new DumpTable("Expression", "#ff6600", "#ffcc99", "#000000");
table.appendRow(1, new SimpleDumpData("Expression"), new SimpleDumpData(source));
return table;
}

@Override
public String serialize() {
return serialize(null);
}

@Override
public String serialize(Set<Object> done) {
// escapeJS produces JSON-compatible string output (already wraps in quotes); same pattern
// JSONConverter uses for date serialisation.
return "{\"expression\":" + StringUtil.escapeJS(source, '"') + "}";
}

@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof ExpressionDefault)) return false;
return source.equals(((ExpressionDefault) obj).source);
}

@Override
public int hashCode() {
return source.hashCode();
}
}
2 changes: 2 additions & 0 deletions core/src/main/java/lucee/runtime/component/PropertyImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ protected final int hash() {
@Override
public String getDefault() {
if (_default == null) return null;
// expression-form: source string would corrupt callers piping it into scope.set
if (_default instanceof ExpressionDefault) return null;
try {
return Caster.toString(_default);
}
Expand Down
15 changes: 12 additions & 3 deletions core/src/main/java/lucee/transformer/bytecode/PageImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -1229,10 +1229,9 @@ private void writeOutStatic(PageSource optionalPS, ConstrBytecodeContext constr,
// prop.setDefault(value) if it's a simple literal
// Only handle simple literals (Literal interface) - complex expressions like now()
// or #myVar# need PageContext and must be evaluated at runtime, not class-load time
if (propDefaultAttr != null && propDefaultAttr.getValue() instanceof Literal) {
if (propDefaultAttr != null && propDefaultAttr.getValue() != null) {
Expression defaultExpr = propDefaultAttr.getValue();

// Handle simple literals only - complex expressions handled at runtime
if (defaultExpr instanceof LitStringImpl) {
String value = ((LitStringImpl) defaultExpr).getString();
ga.loadLocal(propLocal);
Expand All @@ -1253,7 +1252,17 @@ else if (defaultExpr instanceof LitBooleanImpl) {
ga.box(Type.BOOLEAN_TYPE);
ga.invokeVirtual(Types.PROPERTY_IMPL, new Method("setDefault", Type.VOID_TYPE, new Type[] { Types.OBJECT }));
}
// else: complex expression - will be handled at runtime in TagProperty
else {
String source = propDefaultAttr.getRawValue();
if (source == null) source = "";
Type EXPRESSION_DEFAULT = Type.getType("Llucee/runtime/component/ExpressionDefault;");
ga.loadLocal(propLocal);
ga.newInstance(EXPRESSION_DEFAULT);
ga.dup();
ga.push(source);
ga.invokeConstructor(EXPRESSION_DEFAULT, new Method("<init>", Type.VOID_TYPE, new Type[] { Types.STRING }));
ga.invokeVirtual(Types.PROPERTY_IMPL, new Method("setDefault", Type.VOID_TYPE, new Type[] { Types.OBJECT }));
}
}

// Collect dynamic attributes (non-standard attributes)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,37 +63,36 @@ public void _writeOut(BytecodeContext bc) throws TransformerException {
Attribute defaultAttr = tag.getAttribute("default");
Attribute nameAttr = tag.getAttribute("name");

// Only handle complex defaults - simple literals are already handled in CLINIT
if (defaultAttr != null && nameAttr != null && defaultAttr.getValue() != null) {
Expression defaultExpr = defaultAttr.getValue();
String propName = getLiteralString(nameAttr);

// Check if it's a complex expression (not a simple literal)
boolean isComplex = !isSimpleLiteral(defaultExpr);

if (isComplex && propName != null) {
final GeneratorAdapter adapter = bc.getAdapter();

// Evaluate the default expression with the current PageContext
defaultExpr.writeOut(bc, Expression.MODE_REF);
int defaultLocal = adapter.newLocal(Types.OBJECT);
adapter.storeLocal(defaultLocal);

// Get PageContext from arg0 and set the value in variables scope
// pc.variablesScope().setEL(KeyImpl.init(propName), defaultValue)
adapter.loadArg(0); // Load PageContext pc
adapter.invokeVirtual(Types.PAGE_CONTEXT, new Method("variablesScope", Types.VARIABLES, new Type[] {}));
adapter.push(propName);
adapter.invokeStatic(Type.getType("Llucee/runtime/type/KeyImpl;"), new Method("init", Types.COLLECTION_KEY, new Type[] { Types.STRING }));
adapter.loadLocal(defaultLocal);
adapter.invokeInterface(Types.SCOPE, new Method("setEL", Types.OBJECT, new Type[] { Types.COLLECTION_KEY, Types.OBJECT }));
adapter.pop(); // Pop return value
if (!isSimpleLiteral(defaultExpr) && propName != null) {
emitExpressionEvalAndSet(bc, propName, defaultExpr);
}
}

bc.visitLine(tag.getEnd());
}

/** Emits eval + variablesScope.setEL bytecode for one cfproperty expression-form default,
* inline in the pseudo-constructor. Slot is empty at this point — ComponentImpl skips
* eager-setting the wrapper into scope at init for expression-form defaults. */
public static void emitExpressionEvalAndSet(BytecodeContext bc, String propName, Expression defaultExpr) throws TransformerException {
final GeneratorAdapter adapter = bc.getAdapter();
defaultExpr.writeOut(bc, Expression.MODE_REF);
int defaultLocal = adapter.newLocal(Types.OBJECT);
adapter.storeLocal(defaultLocal);

adapter.loadArg(0);
adapter.invokeVirtual(Types.PAGE_CONTEXT, new Method("variablesScope", Types.VARIABLES, new Type[] {}));
adapter.push(propName);
adapter.invokeStatic(Type.getType("Llucee/runtime/type/KeyImpl;"), new Method("init", Types.COLLECTION_KEY, new Type[] { Types.STRING }));
adapter.loadLocal(defaultLocal);
adapter.invokeInterface(Types.SCOPE, new Method("setEL", Types.OBJECT, new Type[] { Types.COLLECTION_KEY, Types.OBJECT }));
adapter.pop();
}

private boolean isSimpleLiteral(Expression expr) {
return expr instanceof LitString || expr instanceof LitNumber ||
expr instanceof LitBoolean || expr instanceof LitInteger ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2532,6 +2532,7 @@ private final Attribute attribute(TagLibTag tlt, Data data, ArrayList<String> ar
else if (oAllowExpression instanceof String) allowExpression = ((String) oAllowExpression).equalsIgnoreCase(nameLC);

Expression value = null;
String rawValue = null;

comments(data);

Expand All @@ -2540,7 +2541,7 @@ private final Attribute attribute(TagLibTag tlt, Data data, ArrayList<String> ar
if (hasValue) {
comments(data);
value = attributeValue(data, allowExpression);

rawValue = Attribute.sliceSource(data.srcCode, value);
}
else {
value = defaultValue;
Expand All @@ -2553,7 +2554,9 @@ private final Attribute attribute(TagLibTag tlt, Data data, ArrayList<String> 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);
Attribute attr = new Attribute(dynamic.toBooleanValue(), name, tlta != null ? data.factory.toExpression(value, tlta.getType()) : value, sbType.toString(), !hasValue);
if (rawValue != null) attr.setRawValue(rawValue);
return attr;
}

private final String attributeName(SourceCode cfml, ArrayList<String> args, TagLibTag tag, RefBoolean dynamic, StringBuilder sbType, boolean allowTwiceAttr, boolean allowColon)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1126,10 +1126,12 @@ private static Attribute attribute(Data data, TagLibTag tag, ArrayList<String> a

comment(data.srcCode, true);

String rawValue = null;
if (isDefaultValue || data.srcCode.forwardIfCurrent('=')) {
comment(data.srcCode, true);
// Value
value = attributeValue(data, tag, sbType.toString(), parseExpression[0], false, data.factory.createLitString(""));
rawValue = Attribute.sliceSource(data.srcCode, value);
}
// default value boolean true
else {
Expand All @@ -1143,7 +1145,9 @@ private static Attribute attribute(Data data, TagLibTag tag, ArrayList<String> a
}
comment(data.srcCode, true);

return new Attribute(dynamic.toBooleanValue(), name, value, sbType.toString());
Attribute attr = new Attribute(dynamic.toBooleanValue(), name, value, sbType.toString());
if (rawValue != null) attr.setRawValue(rawValue);
return attr;
}

/**
Expand Down
30 changes: 30 additions & 0 deletions core/src/main/java/lucee/transformer/statement/tag/Attribute.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
package lucee.transformer.statement.tag;

import lucee.transformer.expression.Expression;
import lucee.transformer.util.SourceCode;

public final class Attribute {

Expand All @@ -30,6 +31,7 @@ public final class Attribute {
private boolean defaultAttribute;
private String setterName;
private final boolean isDefaultValue;
private String rawValue;

public Attribute(boolean dynamicType, String name, Expression value, String type) {
this(dynamicType, name, value, type, false);
Expand All @@ -44,6 +46,34 @@ public Attribute(boolean dynamicType, String name, Expression value, String type
this.isDefaultValue = isDefaultValue;
}

/** Raw source text of the attribute value, post-unwrap (i.e. <code>now()</code> for both
* <code>default="#now()#"</code> and <code>default=#now()#</code>). Null for synthesised
* or default-injected attributes. Public so PageImpl bytecode emission can read it
* cross-package — only loaded for cfproperty expression-form defaults today. */
public String getRawValue() {
return rawValue;
}

public void setRawValue(String rawValue) {
this.rawValue = rawValue;
}

/** Capture the post-unwrap source for an Expression by slicing from the parse-time SourceCode.
* Uses Expression.getStart()/getEnd() — these point at the inner expression already, so quotes
* and hash markers fall outside the slice. The peek-back handles json()'s post-delimiter
* position-recording for { and [ literals. Returns null when positions are unavailable. */
public static String sliceSource(SourceCode srcCode, Expression value) {
if (value == null || value.getStart() == null || value.getEnd() == null) return null;
int start = value.getStart().pos;
int end = value.getEnd().pos;
if (start > 0) {
String peek = srcCode.subCFMLString(start - 1, 1).toString();
if (peek.length() == 1 && (peek.charAt(0) == '{' || peek.charAt(0) == '[')) start--;
}
if (end <= start) return null;
return srcCode.subCFMLString(start, end - start).toString();
}

public boolean isDefaultValue() {
return isDefaultValue;
}
Expand Down
Loading
Loading