Skip to content

Commit db725e2

Browse files
committed
LDEV-5989 preserve original source in AST raw field for SimpleExprTransformer
1 parent a0e2970 commit db725e2

7 files changed

Lines changed: 120 additions & 4 deletions

File tree

core/src/main/java/lucee/transformer/bytecode/literal/LitStringImpl.java

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ public class LitStringImpl extends ExpressionBase implements LitString, ExprStri
5050
public static final int TYPE_LOWER = 2;
5151

5252
private String str;
53+
private String rawSource; // Original source representation for AST dump
5354
private boolean fromBracket;
5455

5556
/*
@@ -195,13 +196,32 @@ public boolean fromBracket() {
195196
return fromBracket;
196197
}
197198

199+
/**
200+
* Set the original source representation for AST dump.
201+
* This preserves the exact source text including quotes.
202+
*/
203+
public void setRawSource(String rawSource) {
204+
this.rawSource = rawSource;
205+
}
206+
207+
/**
208+
* Get the original source representation.
209+
*/
210+
public String getRawSource() {
211+
return rawSource;
212+
}
213+
198214
@Override
199215
public void dump(Struct sct) {
200216
super.dump(sct);
201217
sct.setEL(KeyConstants._type, "StringLiteral");
202218
sct.setEL(KeyConstants._value, str);
203-
// Raw must be valid CFML source: escape # to ## and " to ""
204-
if (str != null) {
219+
// Use rawSource if available (preserves original source representation)
220+
// Otherwise compute from value: escape # to ## and " to ""
221+
if (rawSource != null) {
222+
sct.setEL(KeyConstants._raw, rawSource);
223+
}
224+
else if (str != null) {
205225
String escaped = str.replace("#", "##").replace("\"", "\"\"");
206226
sct.setEL(KeyConstants._raw, "\"" + escaped + "\"");
207227
}

core/src/main/java/lucee/transformer/cfml/expression/SimpleExprTransformer.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import lucee.transformer.cfml.Data;
2525
import lucee.transformer.cfml.ExprTransformer;
2626
import lucee.transformer.expression.Expression;
27+
import lucee.transformer.bytecode.literal.LitStringImpl;
2728
import lucee.transformer.expression.literal.LitString;
2829
import lucee.transformer.util.SourceCode;
2930

@@ -56,7 +57,7 @@ public Expression transform(Data data) throws TemplateException {
5657

5758
/**
5859
* Liest den String ein
59-
*
60+
*
6061
* @return Element
6162
* @throws TemplateException
6263
*/
@@ -65,6 +66,8 @@ public Expression string(Factory f, SourceCode cfml) throws TemplateException {
6566
char quoter = cfml.getCurrentLower();
6667
if (quoter != '"' && quoter != '\'') return null;
6768
StringBuilder str = new StringBuilder();
69+
StringBuilder rawSource = new StringBuilder();
70+
rawSource.append(quoter); // Start with opening quote
6871
boolean insideSpecial = false;
6972

7073
Position line = cfml.getPosition();
@@ -74,14 +77,16 @@ public Expression string(Factory f, SourceCode cfml) throws TemplateException {
7477
if (cfml.isCurrent(specialChar)) {
7578
insideSpecial = !insideSpecial;
7679
str.append(specialChar);
77-
80+
rawSource.append(specialChar);
7881
}
7982
// check quoter
8083
else if (!insideSpecial && cfml.isCurrent(quoter)) {
8184
// Ecaped sharp
8285
if (cfml.isNext(quoter)) {
8386
cfml.next();
8487
str.append(quoter);
88+
rawSource.append(quoter);
89+
rawSource.append(quoter); // escaped quote in raw
8590
}
8691
// finish
8792
else {
@@ -91,12 +96,18 @@ else if (!insideSpecial && cfml.isCurrent(quoter)) {
9196
// all other character
9297
else {
9398
str.append(cfml.getCurrent());
99+
rawSource.append(cfml.getCurrent());
94100
}
95101
}
96102

97103
if (!cfml.forwardIfCurrent(quoter)) throw new TemplateException(cfml, "Invalid Syntax Closing [" + quoter + "] not found");
98104

105+
rawSource.append(quoter); // End with closing quote
99106
LitString rtn = f.createLitString(str.toString(), line, cfml.getPosition());
107+
// Set raw source to preserve original representation for AST dump
108+
if (rtn instanceof LitStringImpl) {
109+
((LitStringImpl) rtn).setRawSource(rawSource.toString());
110+
}
100111
cfml.removeSpace();
101112
return rtn;
102113
}

test/tickets/LDEV5989.cfc

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
component extends="org.lucee.cfml.test.LuceeTestCase" labels="ast" {
2+
3+
variables.testDir = getDirectoryFromPath( getCurrentTemplatePath() ) & "LDEV5989/";
4+
5+
function run( testResults, testBox ) {
6+
7+
describe( "LDEV-5989: Interpolated attribute values have escaped hashes in raw field", function() {
8+
9+
it( "raw should contain original single hashes for interpolated attributes", function() {
10+
var code = fileRead( variables.testDir & "interpolatedAttr.cfm" );
11+
var ast = astFromString( code );
12+
13+
// Find the condition attribute
14+
var attr = findAttribute( ast, "condition" );
15+
expect( attr ).notToBeNull( "condition attribute should be present in AST" );
16+
17+
// The raw field should contain single hashes like the original source
18+
// NOT escaped hashes like "##it.hasNext()##"
19+
var rawValue = attr.value.raw;
20+
21+
// Check for doubled hashes (##) in raw - use chr(35) to avoid CFML escaping issues
22+
// If raw has ##, find() will locate chr(35)&chr(35) sequence
23+
var doubleHash = chr( 35 ) & chr( 35 );
24+
var hasDoubleHash = find( doubleHash, rawValue ) > 0;
25+
26+
// If hasDoubleHash is true, raw contains ## which is the bug
27+
expect( hasDoubleHash ).toBeFalse( "Raw should contain single hashes, not escaped ## - got: " & rawValue );
28+
});
29+
30+
it( "round-trip should preserve interpolated attribute values", function() {
31+
// Parse file with interpolated attribute
32+
var code1 = fileRead( variables.testDir & "roundtrip1.cfm" );
33+
var ast1 = astFromString( code1 );
34+
35+
var attr1 = findAttribute( ast1, "condition" );
36+
expect( attr1 ).notToBeNull( "First parse should find condition attribute" );
37+
var raw1 = attr1.value.raw;
38+
39+
// Build round-trip file using template
40+
var template = fileRead( variables.testDir & "loopTemplate.txt" );
41+
var code2 = replace( template, "%%CONDITION%%", raw1 );
42+
fileWrite( variables.testDir & "roundtrip2.cfm", code2 );
43+
44+
// Parse the round-trip file
45+
var ast2 = astFromString( code2 );
46+
var attr2 = findAttribute( ast2, "condition" );
47+
expect( attr2 ).notToBeNull( "Second parse should find condition attribute" );
48+
49+
// Raw should be stable - not doubling hashes each round
50+
expect( attr2.value.raw ).toBe( raw1, "Raw should be stable after round-trip, not doubling hashes" );
51+
});
52+
53+
});
54+
55+
}
56+
57+
/**
58+
* Recursively find an Attribute node by name
59+
*/
60+
private function findAttribute( required struct node, required string name ) {
61+
if ( ( node.type ?: "" ) == "Attribute" && ( node.name ?: "" ) == name ) {
62+
return node;
63+
}
64+
for ( var key in node ) {
65+
var val = node[ key ];
66+
if ( isStruct( val ) ) {
67+
var result = findAttribute( val, name );
68+
if ( !isNull( result ) ) return result;
69+
} else if ( isArray( val ) ) {
70+
for ( var item in val ) {
71+
if ( isStruct( item ) ) {
72+
var result = findAttribute( item, name );
73+
if ( !isNull( result ) ) return result;
74+
}
75+
}
76+
}
77+
}
78+
return;
79+
}
80+
81+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<cfloop condition="#it.hasNext()#"></cfloop>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<cfloop condition=%%CONDITION%%></cfloop>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<cfloop condition="#x.hasNext()#"></cfloop>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<cfloop condition="#x.hasNext()#"></cfloop>

0 commit comments

Comments
 (0)