Skip to content

Commit 1f9b955

Browse files
committed
LDEV-6303 metadata.default naked expression source + cfdump/JSON DX
1 parent 7583c01 commit 1f9b955

8 files changed

Lines changed: 146 additions & 108 deletions

File tree

core/src/main/java/lucee/runtime/ComponentImpl.java

Lines changed: 9 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
import lucee.runtime.component.ComponentLoader;
5858
import lucee.runtime.component.ComponentPageRef;
5959
import lucee.runtime.component.DataMember;
60+
import lucee.runtime.component.ExpressionDefaultSeeder;
6061
import lucee.runtime.component.ImportDefintion;
6162
import lucee.runtime.component.Member;
6263
import lucee.runtime.component.MetaDataSoftReference;
@@ -115,9 +116,6 @@
115116
import lucee.runtime.type.scope.ArgumentImpl;
116117
import lucee.runtime.type.scope.ArgumentIntKey;
117118
import lucee.runtime.type.scope.Variables;
118-
119-
import java.lang.reflect.Method;
120-
import java.util.concurrent.ConcurrentHashMap;
121119
import lucee.runtime.type.util.ArrayUtil;
122120
import lucee.runtime.type.util.ComponentUtil;
123121
import lucee.runtime.type.util.KeyConstants;
@@ -381,32 +379,6 @@ public ComponentImpl _duplicate(boolean deepCopy, boolean isTop) {
381379
return trg;
382380
}
383381

384-
// SEED_METHOD_NONE flags "Page class compiled by pre-LDEV-6303 Lucee" (cross-version compat).
385-
private static final java.util.Map<Class<?>, Method> SEED_METHOD_CACHE = new ConcurrentHashMap<Class<?>, Method>();
386-
private static final Method SEED_METHOD_NONE;
387-
static {
388-
Method tmp = null;
389-
try {
390-
tmp = Object.class.getDeclaredMethod("hashCode");
391-
}
392-
catch (NoSuchMethodException e) {}
393-
SEED_METHOD_NONE = tmp;
394-
}
395-
396-
private static Method lookupSeedMethod(Class<?> pageClass) {
397-
Method m = SEED_METHOD_CACHE.get(pageClass);
398-
if (m != null) return m == SEED_METHOD_NONE ? null : m;
399-
try {
400-
m = pageClass.getDeclaredMethod("_seedExpressionDefaults", PageContext.class);
401-
SEED_METHOD_CACHE.put(pageClass, m);
402-
return m;
403-
}
404-
catch (NoSuchMethodException e) {
405-
SEED_METHOD_CACHE.put(pageClass, SEED_METHOD_NONE);
406-
return null;
407-
}
408-
}
409-
410382
private static void reFireExpressionDefaults(ComponentImpl trg) {
411383
PageContext pc = ThreadLocalPageContext.get();
412384
if (pc == null) return;
@@ -420,20 +392,18 @@ private static void reFireExpressionDefaults(ComponentImpl trg) {
420392
if (walker.pageSource != null) {
421393
try {
422394
lucee.runtime.Page page = walker.pageSource.loadPage(pc, false);
423-
if (page != null) {
424-
Method seed = lookupSeedMethod(page.getClass());
425-
if (seed != null) {
426-
try {
427-
seed.invoke(null, pc);
428-
}
429-
catch (Throwable t) {
430-
ExceptionUtil.rethrowIfNecessary(t);
431-
}
395+
if (page instanceof ExpressionDefaultSeeder) {
396+
try {
397+
((ExpressionDefaultSeeder) page)._seedExpressionDefaults(pc);
398+
}
399+
catch (Throwable t) {
400+
ExceptionUtil.rethrowIfNecessary(t);
401+
throw new lucee.runtime.exp.PageRuntimeException(Caster.toPageException(t));
432402
}
433403
}
434404
}
435405
catch (PageException pe) {
436-
break;
406+
throw new lucee.runtime.exp.PageRuntimeException(pe);
437407
}
438408
}
439409
walker = (ComponentImpl) walker.base;

core/src/main/java/lucee/runtime/component/ExpressionDefault.java

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,22 @@
11
package lucee.runtime.component;
22

3-
/** Immutable source-string wrapper for non-foldable cfproperty expression-form defaults. */
4-
public final class ExpressionDefault {
3+
import java.util.Set;
4+
5+
import lucee.commons.lang.StringUtil;
6+
import lucee.runtime.PageContext;
7+
import lucee.runtime.converter.ScriptConvertable;
8+
import lucee.runtime.dump.DumpData;
9+
import lucee.runtime.dump.DumpProperties;
10+
import lucee.runtime.dump.DumpTable;
11+
import lucee.runtime.dump.Dumpable;
12+
import lucee.runtime.dump.SimpleDumpData;
13+
14+
/** Immutable source-string wrapper for non-foldable cfproperty expression-form defaults.
15+
* Serialises to JSON as { "expression": "<source>" } so consumers can programmatically
16+
* distinguish expression-form metadata.default from literal-form (which serialises as a bare string). */
17+
public final class ExpressionDefault implements Dumpable, ScriptConvertable {
18+
19+
private static final long serialVersionUID = 1L;
520

621
private final String source;
722

@@ -18,6 +33,23 @@ public String toString() {
1833
return source;
1934
}
2035

36+
@Override
37+
public DumpData toDumpData(PageContext pageContext, int maxlevel, DumpProperties properties) {
38+
DumpTable table = new DumpTable("Expression", "#ff6600", "#ffcc99", "#000000");
39+
table.appendRow(1, new SimpleDumpData("Expression"), new SimpleDumpData(source));
40+
return table;
41+
}
42+
43+
@Override
44+
public String serialize() {
45+
return serialize(null);
46+
}
47+
48+
@Override
49+
public String serialize(Set<Object> done) {
50+
return "{\"expression\":" + StringUtil.escapeJS(source, '"') + "}";
51+
}
52+
2153
@Override
2254
public boolean equals(Object obj) {
2355
if (this == obj) return true;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package lucee.runtime.component;
2+
3+
import lucee.runtime.PageContext;
4+
5+
/** Implemented by emitted CFC classes that declare cfproperty expression-form defaults. */
6+
public interface ExpressionDefaultSeeder {
7+
8+
void _seedExpressionDefaults(PageContext pc) throws Throwable;
9+
}

core/src/main/java/lucee/transformer/bytecode/PageImpl.java

Lines changed: 29 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,17 @@ public byte[] execute(String className) throws TransformerException {
380380
String[] interfaces = null;
381381
if (isComponent(comp)) {
382382
parent = ComponentPageImpl.class.getName();// "lucee/runtime/ComponentPage";
383-
if (isSub) interfaces = new String[] { SubPage.class.getName().replace('.', '/') };
383+
List<TagProperty> exprDefProps = collectExpressionFormProperties(comp);
384+
boolean hasExprDefaults = !exprDefProps.isEmpty();
385+
if (isSub && hasExprDefaults) {
386+
interfaces = new String[] { SubPage.class.getName().replace('.', '/'), "lucee/runtime/component/ExpressionDefaultSeeder" };
387+
}
388+
else if (isSub) {
389+
interfaces = new String[] { SubPage.class.getName().replace('.', '/') };
390+
}
391+
else if (hasExprDefaults) {
392+
interfaces = new String[] { "lucee/runtime/component/ExpressionDefaultSeeder" };
393+
}
384394
}
385395
else if (isInterface(comp)) parent = InterfacePageImpl.class.getName();// "lucee/runtime/InterfacePage";
386396
parent = parent.replace('.', '/');
@@ -1255,22 +1265,10 @@ else if (defaultExpr instanceof LitBooleanImpl) {
12551265
ga.invokeVirtual(Types.PROPERTY_IMPL, new Method("setDefault", Type.VOID_TYPE, new Type[] { Types.OBJECT }));
12561266
}
12571267
else {
1258-
String source;
1259-
try {
1260-
int start = defaultExpr.getStart().pos;
1261-
int end = defaultExpr.getEnd().pos;
1262-
// json() consumes opening { or [ before recording position; back up
1263-
if (start > 0) {
1264-
String peek = sourceCode.subCFMLString(start - 1, 1).toString();
1265-
if (peek.length() == 1 && (peek.charAt(0) == '{' || peek.charAt(0) == '[')) {
1266-
start--;
1267-
}
1268-
}
1269-
source = sourceCode.subCFMLString(start, end - start).toString();
1270-
}
1271-
catch (Exception e) {
1272-
source = "";
1273-
}
1268+
// WIP: raw value includes attribute wrapping syntax (quotes + hashes); contract for
1269+
// metadata.default source TBD. See LDEV-6303-code-review.md for state.
1270+
String source = propDefaultAttr.getRawValue();
1271+
if (source == null) source = "";
12741272
Type EXPRESSION_DEFAULT = Type.getType("Llucee/runtime/component/ExpressionDefault;");
12751273
ga.loadLocal(propLocal);
12761274
ga.newInstance(EXPRESSION_DEFAULT);
@@ -1565,13 +1563,11 @@ else if (defaultExpr instanceof LitBooleanImpl) {
15651563

15661564
}
15671565

1568-
private void writeOutSeedExpressionDefaults(PageSource optionalPS, ConstrBytecodeContext constr, Map<LitString, Integer> keys, ClassWriter cw, TagCIObject component,
1569-
String name) throws TransformerException {
1570-
if (component == null || component.getBody() == null) return;
1566+
private static List<TagProperty> collectExpressionFormProperties(TagCIObject component) {
1567+
List<TagProperty> result = new ArrayList<TagProperty>();
1568+
if (component == null || component.getBody() == null) return result;
15711569
List<Statement> statements = component.getBody().getStatements();
1572-
if (statements == null) return;
1573-
1574-
List<TagProperty> exprDefProps = new ArrayList<TagProperty>();
1570+
if (statements == null) return result;
15751571
for (Statement stmt: statements) {
15761572
if (!(stmt instanceof TagProperty)) continue;
15771573
TagProperty tagProp = (TagProperty) stmt;
@@ -1581,37 +1577,27 @@ private void writeOutSeedExpressionDefaults(PageSource optionalPS, ConstrBytecod
15811577
if (defaultExpr instanceof LitStringImpl || defaultExpr instanceof LitNumberImpl || defaultExpr instanceof LitBooleanImpl) continue;
15821578
Attribute nameAttr = tagProp.getAttribute("name");
15831579
if (nameAttr == null || nameAttr.getValue() == null) continue;
1584-
exprDefProps.add(tagProp);
1580+
result.add(tagProp);
15851581
}
1582+
return result;
1583+
}
1584+
1585+
private void writeOutSeedExpressionDefaults(PageSource optionalPS, ConstrBytecodeContext constr, Map<LitString, Integer> keys, ClassWriter cw, TagCIObject component,
1586+
String name) throws TransformerException {
1587+
List<TagProperty> exprDefProps = collectExpressionFormProperties(component);
1588+
if (exprDefProps.isEmpty()) return;
15861589

1587-
// Method shell always emitted on component classes — empty body is JIT no-op; ensures
1588-
// presence-of-method is stable for ComponentImpl._duplicate's reflection lookup.
15891590
Method seedMethod = new Method("_seedExpressionDefaults", Types.VOID, new Type[] { Types.PAGE_CONTEXT });
1590-
GeneratorAdapter ga = new GeneratorAdapter(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, seedMethod, null, new Type[] { Types.THROWABLE }, cw);
1591+
GeneratorAdapter ga = new GeneratorAdapter(Opcodes.ACC_PUBLIC, seedMethod, null, new Type[] { Types.THROWABLE }, cw);
15911592
BytecodeContext bc = new BytecodeContext(config, optionalPS, constr, this, keys, cw, name, ga, seedMethod, writeLog(), suppressWSbeforeArg, output, returnValue,
15921593
sourceCode.getSourceOffset());
15931594

1594-
Method METHOD_VARIABLES_SCOPE = new Method("variablesScope", Types.VARIABLES, new Type[] {});
1595-
Method METHOD_SET_EL = new Method("setEL", Types.OBJECT, new Type[] { Types.COLLECTION_KEY, Types.OBJECT });
1596-
15971595
for (TagProperty tagProp: exprDefProps) {
15981596
Attribute nameAttr = tagProp.getAttribute("name");
15991597
Attribute defaultAttr = tagProp.getAttribute("default");
16001598
String propName = nameAttr.getValue() instanceof Literal ? ((Literal) nameAttr.getValue()).getString() : null;
16011599
if (propName == null) continue;
1602-
Expression defaultExpr = defaultAttr.getValue();
1603-
1604-
defaultExpr.writeOut(bc, Expression.MODE_REF);
1605-
int defaultLocal = ga.newLocal(Types.OBJECT);
1606-
ga.storeLocal(defaultLocal);
1607-
1608-
ga.loadArg(0);
1609-
ga.invokeVirtual(Types.PAGE_CONTEXT, METHOD_VARIABLES_SCOPE);
1610-
ga.push(propName);
1611-
ga.invokeStatic(KEY_IMPL, KEY_INIT);
1612-
ga.loadLocal(defaultLocal);
1613-
ga.invokeInterface(Types.SCOPE, METHOD_SET_EL);
1614-
ga.pop();
1600+
TagProperty.emitExpressionEvalAndSet(bc, propName, defaultAttr.getValue());
16151601
}
16161602

16171603
ga.returnValue();

core/src/main/java/lucee/transformer/bytecode/statement/tag/TagProperty.java

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -63,37 +63,38 @@ public void _writeOut(BytecodeContext bc) throws TransformerException {
6363
Attribute defaultAttr = tag.getAttribute("default");
6464
Attribute nameAttr = tag.getAttribute("name");
6565

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

71-
// Check if it's a complex expression (not a simple literal)
72-
boolean isComplex = !isSimpleLiteral(defaultExpr);
73-
74-
if (isComplex && propName != null) {
75-
final GeneratorAdapter adapter = bc.getAdapter();
76-
77-
// Evaluate the default expression with the current PageContext
78-
defaultExpr.writeOut(bc, Expression.MODE_REF);
79-
int defaultLocal = adapter.newLocal(Types.OBJECT);
80-
adapter.storeLocal(defaultLocal);
81-
82-
// Get PageContext from arg0 and set the value in variables scope
83-
// pc.variablesScope().setEL(KeyImpl.init(propName), defaultValue)
84-
adapter.loadArg(0); // Load PageContext pc
85-
adapter.invokeVirtual(Types.PAGE_CONTEXT, new Method("variablesScope", Types.VARIABLES, new Type[] {}));
86-
adapter.push(propName);
87-
adapter.invokeStatic(Type.getType("Llucee/runtime/type/KeyImpl;"), new Method("init", Types.COLLECTION_KEY, new Type[] { Types.STRING }));
88-
adapter.loadLocal(defaultLocal);
89-
adapter.invokeInterface(Types.SCOPE, new Method("setEL", Types.OBJECT, new Type[] { Types.COLLECTION_KEY, Types.OBJECT }));
90-
adapter.pop(); // Pop return value
70+
if (!isSimpleLiteral(defaultExpr) && propName != null) {
71+
emitExpressionEvalAndSet(bc, propName, defaultExpr);
9172
}
9273
}
9374

9475
bc.visitLine(tag.getEnd());
9576
}
9677

78+
/**
79+
* Emits eval + variablesScope.setEL bytecode for a single cfproperty expression-form default.
80+
* Shared by construction-time pseudo-constructor and the duplicate-time _seedExpressionDefaults
81+
* method so both paths emit identical eval bytecode and stay in lockstep.
82+
*/
83+
public static void emitExpressionEvalAndSet(BytecodeContext bc, String propName, Expression defaultExpr) throws TransformerException {
84+
final GeneratorAdapter adapter = bc.getAdapter();
85+
defaultExpr.writeOut(bc, Expression.MODE_REF);
86+
int defaultLocal = adapter.newLocal(Types.OBJECT);
87+
adapter.storeLocal(defaultLocal);
88+
89+
adapter.loadArg(0);
90+
adapter.invokeVirtual(Types.PAGE_CONTEXT, new Method("variablesScope", Types.VARIABLES, new Type[] {}));
91+
adapter.push(propName);
92+
adapter.invokeStatic(Type.getType("Llucee/runtime/type/KeyImpl;"), new Method("init", Types.COLLECTION_KEY, new Type[] { Types.STRING }));
93+
adapter.loadLocal(defaultLocal);
94+
adapter.invokeInterface(Types.SCOPE, new Method("setEL", Types.OBJECT, new Type[] { Types.COLLECTION_KEY, Types.OBJECT }));
95+
adapter.pop();
96+
}
97+
9798
private boolean isSimpleLiteral(Expression expr) {
9899
return expr instanceof LitString || expr instanceof LitNumber ||
99100
expr instanceof LitBoolean || expr instanceof LitInteger ||

core/src/main/java/lucee/transformer/cfml/script/AbstrCFMLScriptTransformer.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2532,6 +2532,7 @@ private final Attribute attribute(TagLibTag tlt, Data data, ArrayList<String> ar
25322532
else if (oAllowExpression instanceof String) allowExpression = ((String) oAllowExpression).equalsIgnoreCase(nameLC);
25332533

25342534
Expression value = null;
2535+
String rawValue = null;
25352536

25362537
comments(data);
25372538

@@ -2540,7 +2541,18 @@ private final Attribute attribute(TagLibTag tlt, Data data, ArrayList<String> ar
25402541
if (hasValue) {
25412542
comments(data);
25422543
value = attributeValue(data, allowExpression);
2543-
2544+
if (value != null && value.getStart() != null && value.getEnd() != null) {
2545+
int start = value.getStart().pos;
2546+
int end = value.getEnd().pos;
2547+
// json() records position after consuming opening { or [; back up so the slice
2548+
// includes the opening delimiter (the post-delimiter behaviour is correct for
2549+
// json()'s 40 stack-trace callers — only the source-slice case wants pre-delimiter)
2550+
if (start > 0) {
2551+
String peek = data.srcCode.subCFMLString(start - 1, 1).toString();
2552+
if (peek.length() == 1 && (peek.charAt(0) == '{' || peek.charAt(0) == '[')) start--;
2553+
}
2554+
if (end > start) rawValue = data.srcCode.subCFMLString(start, end - start).toString();
2555+
}
25442556
}
25452557
else {
25462558
value = defaultValue;
@@ -2553,7 +2565,9 @@ private final Attribute attribute(TagLibTag tlt, Data data, ArrayList<String> ar
25532565
tlta = tlt.getAttribute(nameLC, true);
25542566
if (tlta != null && tlta.getName() != null) nameLC = tlta.getName();
25552567
}
2556-
return new Attribute(dynamic.toBooleanValue(), name, tlta != null ? data.factory.toExpression(value, tlta.getType()) : value, sbType.toString(), !hasValue);
2568+
Attribute attr = new Attribute(dynamic.toBooleanValue(), name, tlta != null ? data.factory.toExpression(value, tlta.getType()) : value, sbType.toString(), !hasValue);
2569+
if (rawValue != null) attr.setRawValue(rawValue);
2570+
return attr;
25572571
}
25582572

25592573
private final String attributeName(SourceCode cfml, ArrayList<String> args, TagLibTag tag, RefBoolean dynamic, StringBuilder sbType, boolean allowTwiceAttr, boolean allowColon)

core/src/main/java/lucee/transformer/cfml/tag/CFMLTransformer.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1126,10 +1126,23 @@ private static Attribute attribute(Data data, TagLibTag tag, ArrayList<String> a
11261126

11271127
comment(data.srcCode, true);
11281128

1129+
String rawValue = null;
11291130
if (isDefaultValue || data.srcCode.forwardIfCurrent('=')) {
11301131
comment(data.srcCode, true);
11311132
// Value
11321133
value = attributeValue(data, tag, sbType.toString(), parseExpression[0], false, data.factory.createLitString(""));
1134+
if (value != null && value.getStart() != null && value.getEnd() != null) {
1135+
int start = value.getStart().pos;
1136+
int end = value.getEnd().pos;
1137+
// json() records position after consuming opening { or [; back up so the slice
1138+
// includes the opening delimiter (the post-delimiter behaviour is correct for
1139+
// json()'s 40 stack-trace callers — only the source-slice case wants pre-delimiter)
1140+
if (start > 0) {
1141+
String peek = data.srcCode.subCFMLString(start - 1, 1).toString();
1142+
if (peek.length() == 1 && (peek.charAt(0) == '{' || peek.charAt(0) == '[')) start--;
1143+
}
1144+
if (end > start) rawValue = data.srcCode.subCFMLString(start, end - start).toString();
1145+
}
11331146
}
11341147
// default value boolean true
11351148
else {
@@ -1143,7 +1156,9 @@ private static Attribute attribute(Data data, TagLibTag tag, ArrayList<String> a
11431156
}
11441157
comment(data.srcCode, true);
11451158

1146-
return new Attribute(dynamic.toBooleanValue(), name, value, sbType.toString());
1159+
Attribute attr = new Attribute(dynamic.toBooleanValue(), name, value, sbType.toString());
1160+
if (rawValue != null) attr.setRawValue(rawValue);
1161+
return attr;
11471162
}
11481163

11491164
/**

0 commit comments

Comments
 (0)