Skip to content

Commit 78ee088

Browse files
committed
LDEV-6303 factor sliceSource helper, comment cleanup, lock contracts via tests
1 parent 1f9b955 commit 78ee088

6 files changed

Lines changed: 140 additions & 31 deletions

File tree

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@
1111
import lucee.runtime.dump.Dumpable;
1212
import lucee.runtime.dump.SimpleDumpData;
1313

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). */
14+
/** Immutable wrapper for cfproperty expression-form defaults; surfaces in metadata.default. */
1715
public final class ExpressionDefault implements Dumpable, ScriptConvertable {
1816

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

2121
private final String source;
@@ -47,6 +47,8 @@ public String serialize() {
4747

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

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1265,8 +1265,6 @@ else if (defaultExpr instanceof LitBooleanImpl) {
12651265
ga.invokeVirtual(Types.PROPERTY_IMPL, new Method("setDefault", Type.VOID_TYPE, new Type[] { Types.OBJECT }));
12661266
}
12671267
else {
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.
12701268
String source = propDefaultAttr.getRawValue();
12711269
if (source == null) source = "";
12721270
Type EXPRESSION_DEFAULT = Type.getType("Llucee/runtime/component/ExpressionDefault;");

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

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2541,18 +2541,7 @@ private final Attribute attribute(TagLibTag tlt, Data data, ArrayList<String> ar
25412541
if (hasValue) {
25422542
comments(data);
25432543
value = attributeValue(data, allowExpression);
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-
}
2544+
rawValue = Attribute.sliceSource(data.srcCode, value);
25562545
}
25572546
else {
25582547
value = defaultValue;

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

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1131,18 +1131,7 @@ private static Attribute attribute(Data data, TagLibTag tag, ArrayList<String> a
11311131
comment(data.srcCode, true);
11321132
// Value
11331133
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-
}
1134+
rawValue = Attribute.sliceSource(data.srcCode, value);
11461135
}
11471136
// default value boolean true
11481137
else {

core/src/main/java/lucee/transformer/statement/tag/Attribute.java

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
package lucee.transformer.statement.tag;
2020

2121
import lucee.transformer.expression.Expression;
22+
import lucee.transformer.util.SourceCode;
2223

2324
public final class Attribute {
2425

@@ -45,8 +46,10 @@ public Attribute(boolean dynamicType, String name, Expression value, String type
4546
this.isDefaultValue = isDefaultValue;
4647
}
4748

48-
/** Raw source text of the attribute value (between the `=` and the next attribute boundary).
49-
* Captured at parse time when available; null for synthesised or default-injected attributes. */
49+
/** Raw source text of the attribute value, post-unwrap (i.e. <code>now()</code> for both
50+
* <code>default="#now()#"</code> and <code>default=#now()#</code>). Null for synthesised
51+
* or default-injected attributes. Public so PageImpl bytecode emission can read it
52+
* cross-package — only loaded for cfproperty expression-form defaults today. */
5053
public String getRawValue() {
5154
return rawValue;
5255
}
@@ -55,6 +58,22 @@ public void setRawValue(String rawValue) {
5558
this.rawValue = rawValue;
5659
}
5760

61+
/** Capture the post-unwrap source for an Expression by slicing from the parse-time SourceCode.
62+
* Uses Expression.getStart()/getEnd() — these point at the inner expression already, so quotes
63+
* and hash markers fall outside the slice. The peek-back handles json()'s post-delimiter
64+
* position-recording for { and [ literals. Returns null when positions are unavailable. */
65+
public static String sliceSource(SourceCode srcCode, Expression value) {
66+
if (value == null || value.getStart() == null || value.getEnd() == null) return null;
67+
int start = value.getStart().pos;
68+
int end = value.getEnd().pos;
69+
if (start > 0) {
70+
String peek = srcCode.subCFMLString(start - 1, 1).toString();
71+
if (peek.length() == 1 && (peek.charAt(0) == '{' || peek.charAt(0) == '[')) start--;
72+
}
73+
if (end <= start) return null;
74+
return srcCode.subCFMLString(start, end - start).toString();
75+
}
76+
5877
public boolean isDefaultValue() {
5978
return isDefaultValue;
6079
}

test/tickets/LDEV6303.cfc

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,118 @@ component extends="org.lucee.cfml.test.LuceeTestCase" labels="orm" {
468468

469469
});
470470

471+
describe( "serializeJson shape — programmatic discriminability for consumers", function(){
472+
473+
it( "literal-form metadata.default serialises as a bare string in JSON", function(){
474+
var inst = new LDEV6303.ExpressionDefaultsCfc();
475+
var json = serializeJson( getMetadata( inst ) );
476+
var parsed = deserializeJson( json );
477+
var meta = findProperty( parsed.properties, "literalDef" );
478+
expect( meta.default ).toBe( "hello" );
479+
expect( isSimpleValue( meta.default ) ).toBeTrue(
480+
"literal-form must round-trip through JSON as a simple string"
481+
);
482+
});
483+
484+
it( "expression-form metadata.default serialises as a struct with key 'expression' in JSON", function(){
485+
var inst = new LDEV6303.ExpressionDefaultsCfc();
486+
var json = serializeJson( getMetadata( inst ) );
487+
var parsed = deserializeJson( json );
488+
var meta = findProperty( parsed.properties, "expressionDef" );
489+
expect( isStruct( meta.default ) ).toBeTrue(
490+
"expression-form must round-trip through JSON as a struct (programmatically discriminable from a literal default)"
491+
);
492+
expect( structKeyExists( meta.default, "expression" ) ).toBeTrue(
493+
"the JSON struct must use the lowercase key 'expression'"
494+
);
495+
expect( meta.default.expression ).toBe( "repeatString( 'x', 5 )" );
496+
});
497+
498+
it( "JSON for #now()# expression-form is a struct with the source CFML, not an evaluated DateTime", function(){
499+
var inst = new LDEV6303.ExpressionDefaultsCfc();
500+
var json = serializeJson( getMetadata( inst ) );
501+
var parsed = deserializeJson( json );
502+
var meta = findProperty( parsed.properties, "nowDef" );
503+
expect( isStruct( meta.default ) ).toBeTrue();
504+
expect( meta.default.expression ).toBe( "now()",
505+
"JSON for #now()# must carry the source string, not a frozen evaluated DateTime"
506+
);
507+
});
508+
509+
it( "JSON for struct-literal expression-form preserves the source verbatim with embedded quotes escaped", function(){
510+
var inst = new LDEV6303.ExpressionDefaultsCfc();
511+
var json = serializeJson( getMetadata( inst ) );
512+
var parsed = deserializeJson( json );
513+
var meta = findProperty( parsed.properties, "nowStructDef" );
514+
expect( isStruct( meta.default ) ).toBeTrue();
515+
// The CFC declares default='#{"ts":now(),"n":1}#' so source is {"ts":now(),"n":1}
516+
expect( meta.default.expression ).toInclude( "now()" );
517+
expect( meta.default.expression ).toInclude( "ts" );
518+
expect( meta.default.expression ).toInclude( "n" );
519+
});
520+
521+
});
522+
523+
describe( "Castable behaviour — wrapper coerces to source string in CFML coercion paths", function(){
524+
525+
it( "string concat coerces metadata.default to the source CFML", function(){
526+
var inst = new LDEV6303.ExpressionDefaultsCfc();
527+
var meta = findProperty( getMetadata( inst ).properties, "expressionDef" );
528+
var concat = meta.default & "";
529+
expect( concat ).toBe( "repeatString( 'x', 5 )",
530+
"string-concat with empty string must coerce wrapper to source via toString()"
531+
);
532+
});
533+
534+
it( "len() of the wrapper returns the source string length", function(){
535+
var inst = new LDEV6303.ExpressionDefaultsCfc();
536+
var meta = findProperty( getMetadata( inst ).properties, "nowDef" );
537+
expect( len( meta.default ) ).toBe( len( "now()" ) );
538+
});
539+
540+
it( "ucase()/lcase() coerce the wrapper through string casting", function(){
541+
var inst = new LDEV6303.ExpressionDefaultsCfc();
542+
var meta = findProperty( getMetadata( inst ).properties, "expressionDef" );
543+
expect( ucase( meta.default ) ).toBe( "REPEATSTRING( 'X', 5 )" );
544+
});
545+
546+
});
547+
548+
describe( "external serialisation — ExpressionDefault survives objectSave/objectLoad rehydration", function(){
549+
550+
it( "objectSave + objectLoad round-trips an instance and metadata.default for expression-form is still an Expression wrapper", function(){
551+
var src = new LDEV6303.ExpressionDefaultsCfc();
552+
var bin = objectSave( src );
553+
var dup = objectLoad( bin );
554+
555+
// instance scope values must round-trip (variables-scope per-instance evaluation)
556+
expect( dup.getExpressionDef() ).toBe( "xxxxx",
557+
"variables-scope evaluated value must survive objectSave/objectLoad"
558+
);
559+
560+
// metadata.default for expression-form must still be the wrapper after rehydration
561+
var meta = findProperty( getMetadata( dup ).properties, "expressionDef" );
562+
expect( structKeyExists( meta, "default" ) ).toBeTrue();
563+
expect( isObject( meta.default ) ).toBeTrue(
564+
"rehydrated metadata.default for expression-form must still be an Expression wrapper, not flattened to a string"
565+
);
566+
expect( "" & meta.default ).toBe( "repeatString( 'x', 5 )",
567+
"rehydrated wrapper must stringify to the same source CFML"
568+
);
569+
});
570+
571+
it( "objectSave + objectLoad preserves literal-form metadata.default as a simple string", function(){
572+
var src = new LDEV6303.ExpressionDefaultsCfc();
573+
var bin = objectSave( src );
574+
var dup = objectLoad( bin );
575+
576+
var meta = findProperty( getMetadata( dup ).properties, "literalDef" );
577+
expect( meta.default ).toBe( "hello" );
578+
expect( isSimpleValue( meta.default ) ).toBeTrue();
579+
});
580+
581+
});
582+
471583
});
472584
}
473585

0 commit comments

Comments
 (0)