Skip to content

UseTextBlocks: Added support for converting string concatenations with variables using .formatted(...) - #1301

Open
jbessels wants to merge 2 commits into
openrewrite:mainfrom
jbessels:feature/use-text-blocks-format-variables
Open

jbessels wants to merge 2 commits into
openrewrite:mainfrom
jbessels:feature/use-text-blocks-format-variables

Conversation

@jbessels

@jbessels jbessels commented Oct 17, 2026 •

Copy link
Copy Markdown

Finally!! After all these years, I finally got to scratch that itch! 😄
UseTextBlocks: Support converting string concatenations with variables using .formatted(...)

Tbh its AI generated. I checked out all relevant OpenRewrite repo's, fed the AI with existing recipe name and a very careful description. Antigravity with Vertex / Gemini was then tasked to create it. I ordered it to test it all and create edge case tests - tests first, implementation later. Also to check and follow the OpenRewrite naming convention, test convertion etc. The result is shown beneath.

I'm very curious if this is decent code or AI slop. I used AI cause not knowledgable enough to code this OpenRewrite stuff my self (limited free time), the honest truth.

Summary

Extends the UseTextBlocks recipe with an opt-in formatVariables configuration option. When enabled, multi-line string concatenations that contain variable references or expressions (e.g. "SELECT * FROM\n" + table + " WHERE id = " + id) are migrated into a Java 15+ text block combined with .formatted(...).

Motivation

Developers migrating legacy Java code to modern Java often encounter multi-line SQL queries, JSON/XML templates, and messages that mix multi-line string literals with variables. Currently, UseTextBlocks only migrates pure string literals. Converting concatenated variables into text blocks with .formatted(...) by hand is tedious and error-prone.

Before & After

Before:

String query = "SELECT * FROM\n" +
        table + "\n" +
        "WHERE id = " + id;

After (formatVariables = true):

String query = """
        SELECT * FROM
        %s
        WHERE id = %d""".formatted(table, id);

Implementation Details

  1. Strict Backward Compatibility:

    • Added @Option boolean formatVariables (defaults to false).
    • When formatVariables = false, the recipe strictly follows existing behavior with zero changes to existing migrations.
  2. Format Specifier Resolution:

    • Integers, longs, shorts, and bytes (primitive and boxed) -> %d
    • Booleans (primitive and boxed) -> %b
    • Characters (primitive and boxed) -> %c
    • Floats and doubles intentionally map to %s (rather than %f) to prevent locale-dependent comma vs. dot decimal formatting issues.
    • Strings and general objects -> %s
  3. Escaping & Formatting:

    • Literal % characters inside string segments are safely escaped to %%.
    • Preserves trailing whitespace escapes (\s) and line continuations (\) consistent with existing text block formatting rules.
  4. AST Type Attribution:

    • Attaches fully resolved JavaType.Method for java.lang.String.formatted(Object... args) to both J.Identifier and J.MethodInvocation to satisfy FindMissingTypes static analysis.
  5. Runtime Compatibility:

    • Built and tested against Java 8 bytecode targets (-source 8 -target 8).

Verification

  • Ran ./gradlew check -x test (all license checks, jar manifests, and recipe CSV validations passed).
  • Ran ./gradlew test --tests org.openrewrite.java.migrate.lang.UseTextBlocksTest (all 52 tests passed, including edge cases for adjacent variables, escaped quotes, trailing spaces, and method/field arguments).

@github-project-automation github-project-automation Bot moved this to In Progress in OpenRewrite Oct 17, 2026
@jbessels jbessels changed the title feat(UseTextBlocks): support converting string concatenations with variables using .formatted(...) UseTextBlocks: Added support for converting string concatenations with variables using .formatted(...) Oct 17, 2026
@jbessels

Copy link
Copy Markdown
Author

Related ticket by me: #261

@greg-at-moderne

Copy link
Copy Markdown
Contributor

I've asked the AI to review it and this is what it gave me among others:

Correctness issues

  1. Constant expressions turn into method calls, so the result can fail to compile. "..." + CONST + "..." is a compile-time constant, but """...""".formatted(CONST) is not. Nothing guards against this today.

    • In an annotation value, @Query("SELECT *\n" + "FROM " + TABLE + "\n" + "WHERE id = 1") becomes @Query("""...""".formatted(TABLE)), which javac rejects with expression not allowed as annotation value.
    • In a switch, static final String Q = "x\n" + A + "\n" + "y"; used as case Q: is rewritten too, so the case label stops compiling.
  2. %d depends on the locale. The same reasoning that sends float/double to %s applies to integers:

    "id=" + 123              // id=123
    "id=%d".formatted(123)   // fa-IR: id=۱۲۳, ar-EG: id=١٢٣, th-TH-u-nu-thai: id=๑๒๓
  3. A null Boolean with %b prints false instead of null:

    Boolean b = null;
    "x " + b              // "x null"
    "x %b".formatted(b)   // "x false"
  4. An array operand gets spread into the varargs:

    String[] arr = {"p", "q"};
    "x " + arr              // "x [Ljava.lang.String;@..."
    "x %s".formatted(arr)   // "x p"

    javac only gives a warning here. The null literal has the same problem: it becomes .formatted(null).

  5. Partial conversions are back (Do not partially use text blocks #273 removed them). When collectOperands fails, for example because one operand is already a text block, the code falls back to super.visitBinary and converts a subexpression:

    return """
           a
           %s
           """.formatted(x) +
           """
           tb""";
  6. Formattable objects. %s calls Formattable.formatTo(...) instead of toString(), so the output can differ from concatenation. This is rare.

Suggestion:

  • Always use %s.
  • Skip constant-expression contexts (annotation values, case labels, constant fields).
  • Skip array-typed and null operands.
  • Keep the original behaviour of not visiting children when the whole concatenation can't be converted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

2 participants