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

Archived in project

Development

Successfully merging this pull request may close these issues.

1 participant