Skip to content

JavaTemplate: add failing tests for literal #{ in template source - #8470

Closed
martinfrancois wants to merge 1 commit into
openrewrite:mainfrom
martinfrancois:repro/javatemplate-hash-brace-escape
Closed

JavaTemplate: add failing tests for literal #{ in template source#8470
martinfrancois wants to merge 1 commit into
openrewrite:mainfrom
martinfrancois:repro/javatemplate-hash-brace-escape

Conversation

@martinfrancois

@martinfrancois martinfrancois commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

What's changed?

Adds 2 known-failing tests to JavaTemplateSubstitutionsTest that reproduce two shapes of the same problem: a raw literal #{ in template source makes JavaTemplate throw before any parsing (hashBraceInStringLiteral), and the backslash escape \#{ that PropertyPlaceholderHelper documents is consumed but then re-parsed as a placeholder (escapedHashBraceInStringLiteral). No framework code changes. The tests are marked @ExpectedToFail so the suite stays green; removing the annotation shows the failure.

What's your motivation?

There is no way to splice the two characters #{ into code through a JavaTemplate. A visitor replaces the string literal "placeholder" using JavaTemplate.builder("\"#{foo}\"") with zero template parameters, on this source:

public class Test {
    String s = "placeholder";
}

The run fails with:

java.lang.IllegalArgumentException: The parameter foo must be defined before it is referenced.
    at org.openrewrite.java.internal.template.Substitutions.lambda$substitute$0(Substitutions.java:70)
    at org.openrewrite.internal.PropertyPlaceholderHelper.parseStringValue(PropertyPlaceholderHelper.java:138)
    at org.openrewrite.internal.PropertyPlaceholderHelper.replacePlaceholders(PropertyPlaceholderHelper.java:107)
    at org.openrewrite.java.internal.template.Substitutions.substitute(Substitutions.java:61)
    at org.openrewrite.java.JavaTemplate.doApply(JavaTemplate.java:124)

After the recipe runs, the code should read String s = "#{foo}";, but no output is produced at all: the #{foo} inside the Java string literal is treated as a template placeholder before template parsing even starts.

The second test uses JavaTemplate.builder("\"\\#{foo}\""), the backslash escape that PropertyPlaceholderHelper documents in its javadoc since #6817. It throws the same exception. The mechanism, measured with a standalone program against the compiled rewrite-core classes: pass 1 of replacePlaceholders consumes the escape, calls the resolver zero times, and returns text containing a bare #{foo}. Because that output differs from the input, the fixed-point loop in Substitutions.substitute() runs a second pass, which re-parses the now unescaped #{foo} and resolves key foo, one iteration later. So the documented helper-level escape does not survive the loop above it. It was never a documented JavaTemplate-level feature.

Found while preparing openrewrite/rewrite-static-analysis#976, which works around this defect in a recipe.

Anything in particular you'd like reviewers to focus on?

The first test pins literal pass-through, which is one of at least two reasonable designs: the exception could be intentional validation, and a documented escape syntax would be the other option. Direction welcome. Also, a fix cannot live in Substitutions alone: JavaTemplateParser builds its own PropertyPlaceholderHelper("#{", "}", null), which must stay consistent with any escape handling.

We think the second shape is a genuine bug: the escape is documented on the helper, and the loop silently undoes it. If you agree this should change, I would gladly prepare the fix. If this behavior is intended, feel free to close this and we know it is settled.

Have you considered any alternatives or workarounds?

openrewrite/rewrite-static-analysis#976 emits \u0023 instead of #, so tokens containing #{ survive substitution. That works, but every affected recipe would need the same trick.

Any additional context

On main, JavaTemplateSubstitutionsTest has 15 tests; with these additions 17 run. The 2 new ones abort as expected under @ExpectedToFail, and all 15 pre-existing tests pass.

This reproduction was prepared with AI assistance (Claude Code). I reviewed the tests and this description.

Checklist

  • I've added unit tests to cover both positive and negative cases
  • I've read and applied the recipe conventions and best practices
  • I've used the IntelliJ IDEA auto-formatter on affected files

This draft adds reproduction tests only, so the first box stays unticked on purpose; I will complete it together with the fix if you want one. The formatter run was calibrated per file; I declined reindentation of untouched lines.

`hashBraceInStringLiteral` pins that a raw `#{` spliced into template
source throws "The parameter foo must be defined before it is referenced".
`escapedHashBraceInStringLiteral` pins that the `\#{` escape supported by
PropertyPlaceholderHelper is undone by the fixed-point loop in
Substitutions. Both are marked @ExpectedToFail until an escape survives.
@github-project-automation github-project-automation Bot moved this to In Progress in OpenRewrite Aug 11, 2026
@github-project-automation github-project-automation Bot moved this from In Progress to Done in OpenRewrite Aug 15, 2026
@martinfrancois
martinfrancois deleted the repro/javatemplate-hash-brace-escape branch August 16, 2026 15:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant