Groovy: keep escape sequences decoded in J.Literal.value - #8872
Merged
jkschneider merged 1 commit intoSep 19, 2026
Merged
Conversation
`GroovyParserVisitor` stored the raw source spelling in `J.Literal.value` for
String literals, so `"a\nb"` carried a backslash and an `n` rather than a
newline. `value` is the decoded value; `valueSource` is the spelling that gets
printed.
Both sites now keep `ConstantExpression.getValue()` and take only `valueSource`
from the source: `visitConstantExpression`'s `ClassHelper.STRING_TYPE` branch,
and the constant segments of `visitGStringExpression`. No unescaper is needed —
the compiler already decodes per delimiter, so `'a\nb'` and `"""a\nb"""` yield
a newline while `/a\nb/` and `$/a\nb/$` keep the backslash. Printing is
unaffected, as `JavaPrinter.visitLiteral` emits `getValueSource()`.
Callers that derived a literal's delimiter by searching its spelling for its
value could no longer find it. `ChangeExtraProperty` on `ext { foo = "a\nb" }`
threw:
StringIndexOutOfBoundsException: Range [0, -1) out of bounds for length 6
`ChangeStringLiteral.withStringValue` recognizes the delimiters instead,
matching a known open/close pair at both ends of the spelling. A pair only fits
when the spelling encloses at least as many characters as the value has, since
an escape sequence is longer than the character it spells. That is what
separates a delimited literal from a `G.GString` or `K.StringTemplate`
fragment, whose value includes any quotes or slashes it starts and ends with:
a fragment matches no pair and is re-emitted bare.
`ChangeDependencyClassifier` and `ChangeDependencyExtension` repeated the
search in six places and route through that helper now, as does
`UpgradePluginVersion`, which patched a spelling with `String.replace` and left
it on the old version whenever the search failed.
Two more callers read a value where they needed a spelling.
`DependencyUseStringNotation` rebuilds map notation as a fresh double quoted
literal, which cannot spell a quote, backslash or dollar, so a component
holding one is left alone. `FindRepository` builds a GString's spelling to
compare against its `url` option, and takes a template's own fragments from
`valueSource` so that both of its GString paths agree.
Recognizing the pair also repairs delimiters the search got wrong: `$/a/$`
became `$/baz$/`, and an empty value lost its quotes entirely.
jkschneider
deleted the
groovy-j.literal-values-keep-escapes-unexpanded
branch
September 19, 2026 00:25
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
GroovyParserVisitorput the raw source spelling intoJ.Literal.valuefor every String literal, so a recipe readinggetValue()ondef s = "a\nb"got a backslash and annwhere the newline should be.J.Literaldocumentsvalueas the decoded value andvalueSourceas the spelling that gets printed.The Groovy compiler has already decoded the literal, per delimiter, by the time the visitor sees it:
valuevalueSource"a\nb"a⏎b"a\nb"'a\nb'a⏎b'a\nb''''a\nb'''a⏎b'''a\nb'''/a\nb/a\nb/a\nb/$/a\nb/$a\nb$/a\nb/$So both sites — the
ClassHelper.STRING_TYPEbranch ofvisitConstantExpression, and the constant segments ofvisitGStringExpression— keepConstantExpression.getValue()and take onlyvalueSourcefrom the source. There is no unescaper here, and none is needed. Printing is unaffected:JavaPrinter.visitLiteralemitsgetValueSource().The callers that searched a spelling for its value
Several recipes derived a literal's delimiter with
valueSource.substring(0, valueSource.indexOf(value)). A decoded value is not a substring of its own spelling, soindexOfreturns-1:ChangeStringLiteral.withStringValuerecognises the delimiters instead, matching a known open/close pair against both ends of the spelling. A pair only fits when the spelling encloses at least as many characters as the value has, since an escape sequence is longer than the character it spells.That length test is what separates a delimited literal from a
G.GStringorK.StringTemplatefragment: a fragment's value includes the quotes or slashes it starts and ends with, so no pair fits and it is re-emitted bare. Recognising the pair also repairs delimiters the old search got wrong:$/a/$$/baz$/$/baz/$""baz"baz"ChangeDependencyClassifierandChangeDependencyExtensionrepeated that search in six places and call the helper now.UpgradePluginVersionpatched a spelling withString.replaceand left it on the old version whenever the search failed; it calls the helper too.Two callers that needed a spelling, not a value
DependencyUseStringNotationrebuilds map notation as a fresh double-quoted literal, which has no way to spell a quote, backslash or dollar that the value happens to contain —version: '$x'became a live interpolation. Such a component is now left alone.FindRepositorycompares a GString's reconstructed spelling against itsurloption. ItsTemplateAsStringappends the template's delimiters itself, so a fragment of that template contributes itsvalueSource, while a literal nested inside an interpolation keeps contributing its value — that one is spelled by its own delimiters. Its two GString paths agree again.Tests
LiteralTestpins the decoded value and the spelling at both parser sites, with a slashy string as the control that decoding stays delimiter-sensitive.ChangeStringLiteralTestcovers the escaped-delimiter, fragment and dollar-slashy cases, and each affected recipe has a regression test for the input that broke it.