Skip to content

Commit 90ec374

Browse files
authored
Fixed groovy parser issue to handle slashy string delimters as escape characters. (#6422)
1 parent 6bcda4d commit 90ec374

3 files changed

Lines changed: 117 additions & 16 deletions

File tree

rewrite-groovy/src/main/java/org/openrewrite/groovy/GroovyParserVisitor.java

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1748,7 +1748,7 @@ public void visitGStringExpression(GStringExpression gstring) {
17481748
String value = sourceSubstring(cursor, delimiter.close);
17491749
// There could be a closer GString before the end of the closing delimiter, so shorten the string if needs be
17501750
int indexNextSign = source.indexOf("$", cursor);
1751-
while (isEscaped(indexNextSign)) {
1751+
while (isEscaped(indexNextSign, delimiter)) {
17521752
indexNextSign = source.indexOf("$", indexNextSign + 1);
17531753
}
17541754
if (indexNextSign != -1 && indexNextSign < (cursor + value.length())) {
@@ -2758,6 +2758,32 @@ private boolean isEscaped(int index) {
27582758
return backslashCount % 2 != 0;
27592759
}
27602760

2761+
/**
2762+
* Determines if a $ character in a GString is escaped based on the delimiter type.
2763+
* For slashy strings, $$ escapes a dollar sign and $ followed by the closing delimiter is treated as literal.
2764+
* For other string types, backslash escaping is used.
2765+
*/
2766+
private boolean isEscaped(int index, Delimiter delimiter) {
2767+
if (index < 0) {
2768+
return false;
2769+
}
2770+
2771+
// Slashy-type strings use different escaping: $$ for literal $ and $ before closing delimiter
2772+
if (delimiter.isSlashyStringDelimiter() || delimiter.isDollarSlashyStringDelimiter()) {
2773+
if (index + 1 < source.length()) {
2774+
if (source.charAt(index + 1) == '$') {
2775+
return true; // $$ escapes a dollar sign
2776+
}
2777+
// For slashy strings (not dollar-slashy), $ before closing delimiter is also literal
2778+
return delimiter.isSlashyStringDelimiter() && source.startsWith(delimiter.close, index + 1);
2779+
}
2780+
return false;
2781+
}
2782+
2783+
// Regular strings use backslash escaping
2784+
return isEscaped(index);
2785+
}
2786+
27612787
/**
27622788
* Returns a string that is a part of this source. The substring begins at the specified beginIndex and extends until delimiter.
27632789
* The cursor will not be moved.

rewrite-groovy/src/main/java/org/openrewrite/groovy/internal/Delimiter.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,12 @@ public enum Delimiter {
4848
}
4949
return null;
5050
}
51+
52+
public boolean isSlashyStringDelimiter() {
53+
return this == SLASHY_STRING || this == PATTERN_SLASHY_STRING;
54+
}
55+
56+
public boolean isDollarSlashyStringDelimiter() {
57+
return this == DOLLAR_SLASHY_STRING || this == PATTERN_DOLLAR_SLASHY_STRING;
58+
}
5159
}

rewrite-groovy/src/test/java/org/openrewrite/groovy/tree/LiteralTest.java

Lines changed: 82 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -195,9 +195,9 @@ void gStringNoCurlyBraces() {
195195
void gStringMultiPropertyAccess() {
196196
rewriteRun(
197197
groovy(
198-
"""
199-
"$System.env.BAR_BAZ"
200198
"""
199+
"$System.env.BAR_BAZ"
200+
"""
201201
)
202202
);
203203
}
@@ -206,9 +206,9 @@ void gStringMultiPropertyAccess() {
206206
void emptyGString() {
207207
rewriteRun(
208208
groovy(
209-
"""
210-
"${}"
211209
"""
210+
"${}"
211+
"""
212212
)
213213
);
214214
}
@@ -217,9 +217,9 @@ void emptyGString() {
217217
void nestedGString() {
218218
rewriteRun(
219219
groovy(
220-
"""
221-
" ${ " ${ " " } " } "
222220
"""
221+
" ${ " ${ " " } " } "
222+
"""
223223
)
224224
);
225225
}
@@ -228,9 +228,9 @@ void nestedGString() {
228228
void gStringInterpolateString() {
229229
rewriteRun(
230230
groovy(
231-
"""
232-
" ${""}\\n${" "} "
233231
"""
232+
" ${""}\\n${" "} "
233+
"""
234234
)
235235
);
236236
}
@@ -401,10 +401,10 @@ void escapeCharacters() {
401401
rewriteRun(
402402
groovy(
403403
"""
404-
"\\\\\\\\n\\\\t"
405-
'\\\\n\\t'
406-
///\\\\n\\t///
407-
"""
404+
"\\\\\\\\n\\\\t"
405+
'\\\\n\\t'
406+
///\\\\n\\t///
407+
"""
408408
)
409409
);
410410
}
@@ -414,9 +414,9 @@ void differentiateEscapeFromLiteral() {
414414
rewriteRun(
415415
groovy(
416416
"""
417-
'\t'
418-
' '
419-
"""
417+
'\t'
418+
' '
419+
"""
420420
)
421421
);
422422
}
@@ -455,4 +455,71 @@ void stringWithMultipleBackslashes() {
455455
)
456456
);
457457
}
458+
459+
@Test
460+
void slashyStrings() {
461+
rewriteRun(
462+
groovy(
463+
"""
464+
def text = "irrelevant"
465+
def glassfishVersion = "1"
466+
println text =~ /^.*ClassPath Element.*glassfish-embedded-all-${glassfishVersion}.jar.*$/
467+
"""
468+
),
469+
groovy(
470+
"""
471+
def pattern = /Price: $$\\d+/
472+
"""
473+
),
474+
groovy(
475+
"""
476+
def pattern = /test$/
477+
"""
478+
),
479+
groovy(
480+
"""
481+
def pattern = /$$foo $${bar} $$$$/
482+
"""
483+
)
484+
);
485+
}
486+
487+
@Test
488+
void dollarSlashyStrings() {
489+
rewriteRun(
490+
groovy(
491+
"""
492+
def version = "1.0"
493+
def path = $/C:\\Program Files\\App-${version}\\bin/$
494+
"""
495+
),
496+
groovy(
497+
"""
498+
def text = $/Price: $$100/$
499+
"""
500+
),
501+
groovy(
502+
"""
503+
def path = $/C:/path/to/file/$
504+
"""
505+
)
506+
);
507+
}
508+
509+
@Test
510+
void patternSlashyStrings() {
511+
rewriteRun(
512+
groovy(
513+
"""
514+
def version = "3.0"
515+
def pattern = ~/version-${version}\\.jar/
516+
"""
517+
),
518+
groovy(
519+
"""
520+
def pattern = ~/$$\\d+\\.\\d+/
521+
"""
522+
)
523+
);
524+
}
458525
}

0 commit comments

Comments
 (0)