Skip to content

Format the same on every route: formatDiff on the JVM, ranges, and the order of fixing imports - #220

Open
abashev wants to merge 3 commits into
21.X.Xfrom
abashev/Same-output-on-every-route
Open

abashev wants to merge 3 commits into
21.X.Xfrom
abashev/Same-output-on-every-route

Conversation

@abashev

@abashev abashev commented Nov 12, 2026

Copy link
Copy Markdown
Collaborator

Closes #219.

formatDiff on the JVM and with the native binary, IntelliJ IDEA, the Maven plugin and the Spotless step now format a file the way the command line does.

  • Imports. Formatter.formatSourceAndFixImports, behind the Maven plugin and the Spotless step, fixes the imports after formatting, as Main does. The order decided where the comment of a removed import ended up, and whether a blank line stayed under it.
  • Ranges. --output-replacements and FormatterServiceImpl.getFormatReplacements go through one method, Formatter.getFormatAndReflowReplacements, so the in-process service reflows long strings as the bootstrapping and native services do. formatDiff without the native binary left them on one line, and the GitHub Action failed on the result.
  • Strings outside the ranges. StringWrapper.wrap takes the ranges and reflows only the literals and text blocks that overlap them, moving the ranges along with each of its passes. The ranges are those of the code that formatting them rewrote, which is the statement around a changed line where the formatter re-lays it out. Before, one long string in the ranges reflowed every long string in the file.

What changes for users

  • Whole files: nothing. Without ranges StringWrapper runs as before. The patched Main gives byte for byte what the 21.0.2 native binary gives on 1,500 files, so the command line, the GitHub Action and the pre-commit hook are unchanged.
  • The Maven plugin and the Spotless step now write what the command line writes. In the 3,865 files of The routes format the same file differently: formatDiff on the JVM, ranges, and the order of fixing imports #219 that changes one, CharsetEncoder of JDK 21. A file with an extraneous semicolon between imports, which they used to drop, now fails as on the command line: extraneous semicolon.
  • Ranges reflow the strings in the code the formatter rewrites for them, and nothing else. They do that more often than the native binary did: its check of whether to reflow looked at the ranges of the input in the formatted text, and missed a long string the formatter had moved into a rewritten statement. Over every line with a string literal and more than 90 characters in 600 of those files, one line at a time: 28 results reflowed, all inside the rewritten code; 21.0.2's --output-replacements reflowed 11, 2 of them outside it.

Tests

  • FormatterServiceImplTest holds the in-process service to what Main prints: whole files, and each line of each file as a range. On the code before this change 7 of the 11 new cases in it and in FormatterTest fail.
  • StringWrapperTest covers wrapping in ranges and how the ranges move with the replacements.
  • FormatterServicesTest and FormatDiffTest run the in-process formatter too, next to the bootstrapping and native ones, on a file with a long string on a changed line and another on a line that is not.

On JDK 21: ./gradlew check passes, 1,435 tests in :open-java-format, 141 in :gradle-open-java-format and 65 in the IntelliJ plugin; with -PnativeImage=true (macOS aarch64, GraalVM 25) the 11 of FormatterServicesTest and the 7 of FormatDiffTest pass. Each of the three commits passes :open-java-format:check on its own.

Not here

  • open-java-format --lines without --output-replacements still reflows the whole file: it fixes the imports between formatting and reflowing, and the ranges cannot be followed through that.
  • The Eclipse plugin formats with Formatter.getFormatReplacements and does not reflow strings.
  • Spotless' own palantirJavaFormat() step never reflows strings; that is Spotless' code.

formatSourceAndFixImports, which the Maven plugin and the Spotless
step run, ordered the imports and removed the unused ones before
formatting. Main formats first and fixes the imports after, because
the ranges of --lines and --offset are offsets into the input. The
order decides where the comment of a removed import ends up:
ImportOrderer moves a comment along with its import, and
RemoveUnusedImports leaves it behind. Fixing them first also dropped
an extraneous semicolon between imports, where the command line fails
the file (#219).

formatSourceAndFixImports now fixes the imports after formatting.
FormatterServiceImplTest holds the in-process service to what Main
prints for whole files.
--output-replacements, which IntelliJ IDEA and formatDiff with the
native binary run, reflowed the whole file once a line in the ranges
needed it, and long strings on lines nobody touched changed with it
(#219).

StringWrapper.wrap takes the ranges and reflows only the literals and
text blocks that overlap them. Its passes format and replace text, so
the ranges move along with each of them, growing to take in what a
replacement rewrites. The ranges it gets are those of the code that
formatting the given ranges rewrote, and they also decide whether to
reflow at all; that check used to hold the ranges of the input
against the lines of the formatted text. Without ranges nothing
changes: the whole file is reflowed as before.

The method that formats the ranges and reflows them moves to
Formatter, for the in-process service to use next.
The Gradle plugin loads FormatterServiceImpl into its own JVM, and
its getFormatReplacements never reflowed strings, while the
bootstrapping and native services run Main --output-replacements,
which does. formatDiff without the native binary left long strings on
one line, and the GitHub Action failed on the result (#219).

FormatterServiceImpl.getFormatReplacements now returns what
--output-replacements prints. FormatterServiceImplTest compares the
two for each line of its sources, and FormatterServicesTest and
FormatDiffTest run the in-process formatter next to the bootstrapping
and native ones, which they never did.
@abashev abashev added the bug Something isn't working label Nov 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The routes format the same file differently: formatDiff on the JVM, ranges, and the order of fixing imports

1 participant