Skip to content

Keep //$NON-NLS-n$ markers valid: no inserted space, each marker on the line of its string - #150

Merged
abashev merged 9 commits into
21.X.Xfrom
fix/non-nls-marker-space
Oct 25, 2026
Merged

abashev merged 9 commits into
21.X.Xfrom
fix/non-nls-marker-space

Conversation

@abashev

@abashev abashev commented Oct 25, 2026 •

Copy link
Copy Markdown
Collaborator

Eclipse marks a string literal that is deliberately not externalised with a trailing //$NON-NLS-n$ comment, where n is the position of the literal on its line. Eclipse recognises a marker only with that exact spelling and only on the line of its literal. The formatter broke this in three ways (#23); this fixes all three, in two commits.

Case 1: a space was inserted into a trailing marker

String a = "x"; //$NON-NLS-1$ came out as String a = "x"; // $NON-NLS-1$. A trailing line comment lands in the level of the statement's last break, and that level is written from its flat text when it fits on one line, so Comment.computeFlat() added the space itself, without the exemption that JavaCommentsHelper.rewrite() has for //noinspection and //$NON-NLS-n$. The missing-space rule is now one method, JavaCommentsHelper.addMissingSpace, applied by both paths, and the comment's width follows its flat text. A trailing /// comment that already had its space is no longer turned into // / either.

Cases 2 and 3: markers left behind when a statement is wrapped or reflowed

Before:

String message = Messages.format(
        "first argument that is long enough to matter",
        "second argument that is also rather long",
        "third one"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
String reflowed =
        "a single string literal that is far too long to fit into one hundred and twenty columns, so the"
                + " formatter reflows it"; //$NON-NLS-1$

After:

String message = Messages.format(
        "first argument that is long enough to matter", //$NON-NLS-1$
        "second argument that is also rather long", //$NON-NLS-1$
        "third one"); //$NON-NLS-1$
String reflowed =
        "a single string literal that is far too long to fit into one hundred and twenty" //$NON-NLS-1$
                + " columns, so the formatter reflows it"; //$NON-NLS-1$

OpsBuilder.build pairs each marker of a markers-only trailing comment with the n-th string literal of the comment's input line (within the same partial-format region) and puts a zero-text ReservedWidth after that literal, so the layout already counts the marker on the literal's line and a second run changes nothing. JavaOutput.appendNonNlsMarkers writes each paired marker at the end of its literal's output line, numbered by the literal's position among the string literals written there; a marker whose number has no literal stays where it was. A comment of markers is never wrapped. StringWrapper recomputes the markers of a reflowed chain's line from the new line structure: one marker after every piece, the markers of literals before and after the chain renumbered for their lines, with their room reserved per line.

Left alone by design: a mixed comment such as // note //$NON-NLS-1$, a line on which two reflowed chains change (only reachable with a wrap limit below the formatter's), and a chain that ends in a text block.

Also in this PR

  • The marker code hands its lists around as plain List (List.of, List.copyOf, Stream.toList), not Guava's ImmutableList.
  • The line-comment wrapper no longer keeps // MOE: comments on one line: google/MOE has been dead for years, so such a comment wraps like any other.
  • The conventions of the review, applied here and written down in CLAUDE.md: var for locals, no Error Prone @Immutable on the marker nodes, and the Apache header without a copyright line on the four new files (Audit the license headers of all source files #154 covers the rest of the tree).

Tests

Written first, and failing before each change: trailingLineCommentGetsItsMissingSpaceExceptIdeMarkers with the fixture ojf-issue-23-non-nls-trailing-marker (case 1); nonNlsMarkersStayOnTheLineOfTheirLiteral, aRunOfNonNlsMarkersIsNeverWrapped and the fixtures ojf-issue-23-wrapped-arguments, ojf-issue-23-reflowed-literal and ojf-issue-23-marked-literals (cases 2 and 3). The fixtures also run the idempotency checks. Unit tests cover the utility classes: NonNlsMarkersTest, ReservedWidthTest, and two JavaCommentsHelperTest cases for addMissingSpace.

Verification

  • ./gradlew test over all modules is green on the rebased branch; ./gradlew formatDiff changes nothing.
  • JDK 21 corpus, 15,747 files: case 1 changes 4 files, all a trailing /// ... comment that the flat path used to turn into // / ...; cases 2 and 3 change none, as the JDK carries no Eclipse markers.

Fixes #23. The same defect upstream: google/google-java-format#221, google/google-java-format#361.

…t that fits on one line

A trailing line comment lands in the level of the statement's last break, while
its forced line break lands in the enclosing level. When that level fits on one
line it is written from its flat text, and Comment.computeFlat added the space
after the slashes itself, bypassing the exemption that JavaCommentsHelper.rewrite
has for the IDE markers: `String a = "x"; //$NON-NLS-1$` came out as
`// $NON-NLS-1$`, which Eclipse no longer recognises.

The missing-space rule is now one method, JavaCommentsHelper.addMissingSpace,
applied by both paths, and the comment's width follows its flat text instead of
a hand-maintained + 1. A trailing `///` comment that already had its space is no
longer turned into `// /` either: 4 of the 15,747 JDK 21 sources change, all
that way.

The tests were written first: trailingLineCommentGetsItsMissingSpaceExceptIdeMarkers
and the ojf-issue-23-non-nls-trailing-marker fixture failed before the change.

The first of the three cases of #23. Same defect upstream:
google/google-java-format#221, google/google-java-format#361.
… statement is wrapped or reflowed

Eclipse numbers a marker by the position of its literal on the line and reads
it only there. The formatter left every marker of a wrapped call on the call's
last line, and gave a reflowed literal one marker, on its last piece.

OpsBuilder.build now pairs each marker of a markers-only trailing comment with
the n-th string literal of the comment's input line, within the same
partial-format region, and puts a zero-text ReservedWidth after that literal,
so the layout counts the marker on the literal's line and a second run changes
nothing. JavaOutput.appendNonNlsMarkers writes each paired marker at the end of
its literal's output line, numbered by the literal's position among the string
literals written there; a marker whose number has no literal stays where it
was. JavaCommentsHelper never wraps a comment of markers. StringWrapper
recomputes the markers of a reflowed chain's line from the new line structure,
one marker after every piece and the markers of the literals before and after
the chain renumbered for their lines, and reserves their room per line; a mixed
comment, a second changing chain on the line, or a chain that ends in a text
block leaves the markers alone.

The tests were written first: the fixtures ojf-issue-23-wrapped-arguments,
ojf-issue-23-reflowed-literal and ojf-issue-23-marked-literals, with
nonNlsMarkersStayOnTheLineOfTheirLiteral and aRunOfNonNlsMarkersIsNeverWrapped,
failed before the change. None of the 15,747 JDK 21 sources changes: the JDK
carries no Eclipse markers.

Fixes #23. Same defect upstream: google/google-java-format#221,
google/google-java-format#361.
The marker code used Guava's ImmutableList for its lists of marker numbers
and literal indices. List.of, List.copyOf and Stream.toList give unmodifiable
lists without it, so the signatures take and return List. The field in
Comment carries the Immutable suppression the other doc nodes use for their
effectively immutable state.
The fixtures exercise the marker code end to end only. NonNlsMarkersTest pins
which comments count as markers only, the order and the places in which the
numbers are read, and the spelling of a marker; ReservedWidthTest pins that a
reservation takes its width on the line and nothing else; the two
JavaCommentsHelperTest cases pin the missing-space rule and its exemptions.
google/MOE, the tool whose markers the line-comment wrapper kept on one line,
has been dead for years. Its special case goes, with the test that pinned it;
only the NON-NLS marker runs stay unwrapped.
The fork's own files carry the Apache 2.0 header without a copyright line.
The four files this change adds were created with a Palantir one; CLAUDE.md
records the rule, and #154 covers the rest of the tree.
Comment carried the annotation and needed a suppression for its List field,
and ReservedWidth had taken the annotation from its sibling nodes. Doc itself
is not annotated, so the check adds nothing here: the nodes are immutable by
construction.
The locals this change added declare their type with var where the
initializer gives it unchanged. The loop variables over a List<Integer> keep
int, so that they are not boxed, and the lambda keeps its target type.
CLAUDE.md: plain java.util collections instead of Guava's immutable ones,
javax.annotation.Nullable as the one nullability annotation, var for locals,
no Error Prone @immutable, and the Apache header without a copyright line on
a new file.
@abashev
abashev merged commit 8f3a269 into 21.X.X Oct 25, 2026
17 checks passed
@abashev
abashev deleted the fix/non-nls-marker-space branch October 25, 2026 19:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Keep //$NON-NLS-n$ markers valid: no inserted space, each marker on the line of its string

1 participant