Skip to content

Format module imports, compact source files, and unnamed patterns - #1707

Open
asm0dey wants to merge 15 commits into
palantir:developfrom
asm0dey:feature/java-26-language-support
Open

asm0dey wants to merge 15 commits into
palantir:developfrom
asm0dey:feature/java-26-language-support

Conversation

@asm0dey

@asm0dey asm0dey commented Jul 3, 2026 •

Copy link
Copy Markdown

Before this PR

palantir-java-format supports Java language features through Java 21. Three kinds of source file that javac --release 25 compiles crash the formatter:

syntax error
import module java.base; (JEP 511) expected token: 'module'; generated java instead, Expected ; after import from --fix-imports-only --skip-removing-unused-imports, and class com.sun.tools.javac.tree.JCTree$JCModuleImport cannot be cast to class com.sun.tools.javac.tree.JCTree$JCImport from --fix-imports-only
compact source file / instance main (JEP 512) expected token: 'void' — the implicit top-level class emits a class token that is not in the source
unnamed pattern, e.g. case Box(_, _) (JEP 456) expected token: '_'; generated , instead

After this PR

All three format, and the fixes key off the tree javac built rather than the runtime version, so each applies on every JDK whose parser produces that tree.

flowchart TD
    A["--fix-imports-only"] --> IO
    B["full format / spotlessApply"] --> IO
    IO["ImportOrderer<br/>renders each declaration; comments keep their slot"]
    IO --> RU["RemoveUnusedImports<br/>a module import is never 'unused', and is not cast to JCImport"]
    RU --> PARSE["javac parse, preview enabled"]
    PARSE --> V["JavaInputAstVisitor<br/>ImportTree.isModule() reflectively · IMPLICIT_CLASS members at ZERO indent"]
    V --> V21["Java21InputAstVisitor<br/>ANY_PATTERN emits a single '_' token"]
    V21 --> SW["StringWrapper"]
    SW --> OUT["output"]
Loading
  • Module import declarations — sorted between static and non-static imports, matching google-java-format's ImportType { STATIC, MODULE, NORMAL }, so projects running both formatters do not get their imports reordered back and forth. ImportTree#isModule() (JDK 23) is reached reflectively, because this module compiles with a JDK 21 compiler; where the method does not exist the lookup returns null and imports format exactly as before.
  • Compact source files and instance main methods — the implicit class's members are emitted as a bare top-level list, with no braces and no indent.
  • Unnamed patterns — ANY_PATTERN emits one _ token, without sync(), because javac records that node's start position one past the _.

ImportTree#isModule is registered in the native image's reachability-metadata.json. Native-image also folds the lookup on its own — the registration is belt and braces, not the reason it works — and the image built from this branch formats module imports in both full and --fix-imports-only modes.

ImportOrderer no longer rebuilds declarations

Getting module imports through the import-fixing path exposed the cause of a family of failures: the orderer discarded each declaration's text and re-synthesized it from the kind and the name, so a comment had to be relocated to wherever that synthesized string could hold it, and anything that did not reduce to those two pieces was rejected. It now renders the declaration once while scanning it, normalizing whitespace — one import per line is the style — and leaving each comment where it was written.

-import module /* the base module */ java.base;   error: Unexpected token after import
-import module                                    error: Expected ; after import
-    java.base;
-import module java . base;                       error: Expected ; after import
-import module java./* why */base;                error: Could not parse imported name
-import java.util.List; /* x */                   error: Imports not contiguous
+import module /* the base module */ java.base;   comment stays in its slot
+import module java.base;                         line break normalized away
+import module java.base;                         whitespace in the name normalized
+import module java./* why */ base;               comment stays between the parts
+import java.util.List; /* x */                   trailing block comment kept

Every one of those already compiled, and formatSource already handled them; only the import-fixing entry points failed — including formatSourceAndFixImports, which the Gradle plugin's Spotless step reaches through FormatterService. Reordering is now a fixed point for all of them, which the suites assert (see Tests).

Which JDK formats what

The formatter parses with preview features enabled, so the syntax it accepts depends on the JVM that runs it: the Gradle daemon for the Gradle plugin and Spotless, the Project SDK for IntelliJ, the Eclipse JVM for the Eclipse plugin, GraalVM 23 for the native image.

17 21 22 23 25 26
compact source files, unnamed patterns — ✅ ✅ ✅ ✅ ✅
module import declarations — — — ✅ ✅ ✅
/// markdown docs understood as documentation — — — ✅ ✅ ✅

The last row is pre-existing and not something this PR changes: before 23 a /// comment is an ordinary line comment to javac, so RemoveUnusedImports cannot see its references and an import used only by /// see [List] is deleted. That tier is now in the README.

Java 26 needs nothing further: its Source$Feature set adds one entry over 25 (CAPTURE_MREF_RETURN_TYPE, a method-reference inference change with no syntax) and its preview features are API-only, so there is no new syntax to format. Module imports, compact source files and unnamed patterns were checked by hand on 26, and all 231 goldens are fixed points there.

Behaviour changes for code that uses no new syntax

These follow from the ImportOrderer rewrite and are worth an explicit look in review:

  • A comment or a line break inside an import declaration used to be an error and now formats, for ordinary and static imports as well as module imports.
  • import com . foo . Second ;, which an existing test row recorded as "syntactically valid, but we don't support it", now formats to import com.foo.Second;.
  • Two imports of the same name that are written differently — one carrying a comment, say — are both kept. Before, the second was deduplicated away and its comment went with it. Identical declarations still collapse into one.
  • For the input import followed by a line break, the error names the zero-width EOF tok rather than the newline, because the line break is now skipped before the error is reported.
  • A javadoc comment inside or trailing an import is still rejected: formatSource moves javadoc onto a line of its own, which would separate the imports.

The changelog entry records the first and the third.

Tests

task JDK what it runs
test 21 everything; 1443 tests, 10 skipped — all of them module-import-gated
testJdk23 23 the file-based suites, RemoveUnusedImportsTest, ModuleImportTest, FormatterVersionTest, both import-ordering suites; module imports as a preview feature
testJdk25 25 the same, with module imports final

check depends on both extra legs. Each asserts the JDK it resolved to twice: FormatterVersionTest compares Runtime.version().feature() with a system property, and the task itself checks javaLauncher.metadata.languageVersion in a doFirst, which cannot be skipped if the property's name is ever typo'd.

Goldens: ModuleImport, CompactSource (an import above the first member, a nested record among the members, javadoc and /// docs on members), UnnamedPattern (switch, nested, var _, a when guard, instanceof, catch). MarkdownDoc and FlexibleConstructor are regression pins for behaviour that already worked: this PR has no src/main change for JEP 467 or JEP 513, and javac parses super(...) as an ordinary invocation, so flexible constructor bodies need no visitor work.

Both import-ordering suites assert that every expected output reorders to itself, and ModuleImportTest asserts the same through formatSourceAndFixImports and fixImports — including a CLI test for --fix-imports-only --skip-removing-unused-imports, the invocation from the #1506 report.

I also had the branch attacked rather than reviewed, which found a non-idempotent case in my own earlier commit; details and the pre-existing bugs that turned up are in this comment.

Overlap with #1579

#1579 fixes the same unnamed-pattern crash, and the JCModuleImport ClassCastException, in a different way, and the two conflict in RemoveUnusedImports.java, FileBasedTests.java and the UnnamedPattern goldens. Maintainers: merge one and close the other — happy for that to be #1579 if you prefer its approach.

Possible downsides

  • Module imports need the formatter to run on JDK 23 or later — inherent to parsing with javac internals. Earlier syntax, and consumers on older JDKs, are unaffected.
  • ./gradlew build provisions a JDK 25 toolchain in addition to the JDK 23 one already used, and runs the file-based suites twice more. The 25 leg asserts nothing the 23 leg does not: it is insurance against a future preview-vs-final tree difference, not extra coverage today. There is no JDK 26 leg.
  • The behaviour changes listed above touch code that uses no new syntax.

Fixes

(One fixes per number: GitHub needs the keyword before each one.)

==COMMIT_MSG==
Format module imports (JEP 511), compact source files and instance main methods (JEP 512), and unnamed patterns in deconstruction (JEP 456), each of which crashed the formatter.

Every fix keys off the tree javac built rather than the runtime version, so it applies on every JDK whose parser produces that tree. ImportTree#isModule() is reached reflectively because this module compiles with a JDK 21 compiler; where it is absent, imports format as before. Module imports sort between static and non-static imports, matching google-java-format.

ImportOrderer no longer discards an import's text and rebuilds it from the kind and the name: it renders the declaration while scanning it, so whitespace is normalized, comments stay in the slot they were written in, and a qualified name may contain whitespace or a comment. Imports that are written differently are no longer deduplicated into one, which used to discard the comment of the copy that went.

Formatter tests re-run the file-based suites, RemoveUnusedImportsTest, ModuleImportTest and the import-ordering suites on JDK 23 and 25 in addition to the default JDK 21 test task, and each leg asserts the JDK it resolved to.

Fixes #1506, fixes #1701, fixes #1614
==COMMIT_MSG==


AI Disclosure: This pull request was prepared with the assistance of Claude Code. The code, tests, and this description were AI-assisted and reviewed by the human contributor before submission. ./gradlew build passes locally, which runs the default JDK 21 test task plus the testJdk23 and testJdk25 legs and nativeCompile.

asm0dey added 3 commits July 3, 2026 18:33
The formatter parses with javac internals, so Java 25/26 syntax can only
be exercised when the tests run on a matching JDK. Register azul-zulu
JDK 22/25/26 toolchains and add per-tier testJdkNN tasks so each
visitor-dispatch branch (Java21/25/26) and each feature's GA boundary is
exercised natively, while every other module stays on JDK 21. The
published library still targets Java 17 (libraryTarget unchanged).
Add formatting support for language features finalized through Java 26:

- Module import declarations (import module M;) — JEP 511
- Compact source files and instance main methods — JEP 512
- Unnamed patterns in deconstruction (case Box(_, _)) — JEP 456, fixes a
  formatter crash on the AnyPatternTree node
- Markdown documentation comments (///) — JEP 467
- Flexible constructor bodies — JEP 513

Formatter selects Java25InputAstVisitor / Java26InputAstVisitor by runtime
major version, with a graceful fallback to the existing Java21 visitor so
older runtimes are unaffected. All javac APIs newer than Java 17 are
reached via reflection / flag-bit detection, so the library keeps
compiling at libraryTarget 17.
@palantirtech

Copy link
Copy Markdown
Member

Thanks for your interest in palantir/palantir-java-format, @asm0dey! Before we can accept your pull request, you need to sign our contributor license agreement - just visit https://cla.palantir.com/ and follow the instructions. Once you sign, I'll automatically update this pull request.

@changelog-app

changelog-app Bot commented Jul 3, 2026

Copy link
Copy Markdown

Generate changelog in changelog/@unreleased

Type (Select exactly one)

  • Feature (Adding new functionality)
  • Improvement (Improving existing functionality)
  • Fix (Fixing an issue with existing functionality)
  • Break (Creating a new major version by breaking public APIs)
  • Deprecation (Removing functionality in a non-breaking way)
  • Migration (Automatically moving data/functionality to a new system)

Description

Support Java 22–26 language features: module import declarations (JEP 511), compact source files and instance main methods (JEP 512), unnamed patterns in deconstruction (JEP 456), markdown documentation comments (JEP 467), and flexible constructor bodies (JEP 513). Visitors are selected by runtime major version with graceful fallback to Java 21, and post-17 javac APIs are accessed reflectively so the library still targets Java 17. Formatter tests run on a JDK 21/22/23/25/26 matrix.

Check the box to generate changelog(s)

  • Generate changelog entry

@asm0dey
asm0dey marked this pull request as ready for review July 3, 2026 16:42
@abashev

abashev commented Jul 4, 2026

Copy link
Copy Markdown

+1

1 similar comment
@smurf667

Copy link
Copy Markdown

+1

@gphilos

gphilos commented Jul 16, 2026

Copy link
Copy Markdown

+1 Any plans when this will be merged?

@oualidbklee

Copy link
Copy Markdown

+1

1 similar comment
@jmaycon

jmaycon commented Aug 9, 2026

Copy link
Copy Markdown

+1

@andrewbelling

Copy link
Copy Markdown

+1 hoping this gets merged soon!

@vlsi vlsi left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for picking this up. The three crashes in #1506, #1701, and #1614 are real, and this PR fixes all three. I built the PR and develop (a263f0a) and ran both on JDK 17, 21, 22, 23, 25, and 26. Reverting any of the three fixes turns a golden test red. I also formatted about 6700 files with both builds: pgjdbc, NullAway, and parts of Spring Framework, Guava, and java.base, plus the JDK 26 sources and javac's pattern, record, and implicit-class tests. The output was identical on JDK 21, 25, and 26, in Palantir, Google, and --fix-imports-only modes. No file that develop formats fails with the PR, and the 11 files the PR newly formats are idempotent. Run time is the same within noise. The PR merges cleanly into the current develop.

The description claims more than the code does, though, and the module-import fix stops one JDK tier short. Most findings are inline comments. Below are the ones that don't map to a changed line. Each repro is a file that javac --release 25 compiles.

Approach: the new version tiers don't pay for themselves

Java21InputAstVisitor is a separate class because it references JDK 21 APIs directly (DefaultCaseLabelTree, CaseTree.getGuard()), and a class with those references can't load on JDK 17. Java25InputAstVisitor has no such references. The module compiles with a JDK 21 toolchain, so it reaches ImportTree#isModule() through reflection, and reflection works on any runtime. While the toolchain stays at 21, a Java 25 tier can't reference JDK 25 APIs directly at all, so it can't do the one job a tier exists for.

Dispatching on the runtime version is also the wrong condition. What matters is which tree javac built, and with preview enabled, javac 23 and 24 already build JCModuleImport, so import module still crashes there (see the inline comment on Java25InputAstVisitor.visitImport). The other two fixes in this PR already follow the tree: the IMPLICIT_CLASS check sits in the base visitor, and the unnamed-pattern check is in Java21InputAstVisitor. Both work on every JDK whose parser produces those trees.

I suggest moving the reflective isModule() check into JavaInputAstVisitor.visitImport, as google-java-format does, and dropping java25/, java26/, and the >= 25 and >= 26 branches in Formatter. That changes nothing on older runtimes: where ImportTree#isModule() doesn't exist, the lookup returns null and imports format as they do today. If a later change needs JDK 25 APIs directly, a Java 25 tier can arrive with the toolchain bump that makes those references compile.

Markdown comments and flexible constructor bodies: the PR has no code for them

The PR changes nothing in src/main for JEP 467 or JEP 513. MarkdownDoc and FlexibleConstructor produce the expected output on develop, on JDK 21 through 26, so neither test can fail because of this PR.

The description says /// comments are "preserved verbatim, including fenced code blocks and {@snippet} content". That is not true, and on JDK 23+ it can change what the code means. From JDK 23, javac returns a run of /// lines as one comment token. JavaCommentsHelper.wrapLineComments takes the comment prefix from the untrimmed continuation line, gets "", and wraps the overflow without ///:

class K {
    /// Returns the value.
    /// Callers that still rely on the legacy behaviour described in the old migration guides must now use @Deprecated
    int value() {
        return 1;
    }
}

On JDK 25 with --palantir, @Deprecated moves out of the comment and deprecates the method:

class K {
    /// Returns the value.
    /// Callers that still rely on the legacy behaviour described in the old migration guides must now use
    @Deprecated
    int value() {
        return 1;
    }
}

On JDK 21 the file is unchanged. Other cases I found on JDK 23+:

  • a long line inside a fenced block or a {@snippet} is wrapped, which changes the sample code, and sometimes the output does not compile (error: ';' expected);
  • a /// that trails code (int x = 1; /// note) merges with the /// block below it, and the result differs between JDK 22 and 23 and is not idempotent;
  • ///Summary. becomes /// Summary., which changes the common indentation JEP 467 strips, so an indented code block renders as a paragraph.

Develop has all of these, so none is a regression. I'd either fix wrapLineComments for multi-line /// tokens in this PR and add goldens with a long line, a fence, and a snippet, or drop JEP 467 and JEP 513 from the description, the commit message, and the README, and describe the two goldens as regression tests for behavior that already worked. For reference, google-java-format treats /// on JDK 23+ as a Javadoc token in JavaInput.isJavadocComment, so its line-comment wrapping never touches it.

The commit message will carry inaccurate claims

Bulldozer uses the ==COMMIT_MSG== block as the squash commit body, so the following ends up in git log:

  • "graceful fallback to Java21InputAstVisitor": Formatter.createVisitor still throws LinkageError if a visitor class fails to load. The only fallback is the version if chain.
  • "the library still compiles at libraryTarget = 17": compileJava uses a JDK 21 toolchain with sourceCompatibility/targetCompatibility 11, and the classes are major version 55.
  • "Formatter tests run on a JDK 21/22/23/25/26 matrix": the extra legs run only FormatterIntegrationTest and StringWrapperIntegrationTest, plus RemoveUnusedImportsTest on 25 and 26.

The block also has no Fixes #1506, #1701, #1614 line, so the squash commit won't link the issues. There is no changelog entry either: the changelog-app box is unchecked. This is an Improvement, and the entry should quote the error texts users search for (expected token: 'module', JCModuleImport cannot be cast to class ... JCImport, expected token: 'void', expected token: '_').

Overlap with #1579

#1579 fixes the same unnamed-pattern crash and the JCModuleImport ClassCastException in a different way. The two PRs conflict in RemoveUnusedImports.java and FileBasedTests.java, and both add UnnamedPattern.input/.output with different content. Maintainers: please merge one of the two and close the other.


The fixes for #1506, #1701, and #1614 are worth having, and many users are waiting for them. Before merging, I'd want the Approach change (which also fixes the JDK 23/24 crash), the markdown and flexible-constructor claims, the README native-image bullet, and the commit message addressed. The rest can follow.

Comment thread palantir-java-format/src/main/java/com/palantir/javaformat/java/Formatter.java Outdated
Comment thread palantir-java-format/build.gradle Outdated
Comment thread README.md Outdated
Comment thread README.md Outdated
…rsion

Address review feedback on palantir#1707.

Move the reflective ImportTree#isModule() check into JavaInputAstVisitor.visitImport
and delete the Java25/Java26 visitor tiers along with the >= 25 and >= 26 dispatch
branches. Neither tier referenced a JDK 25 or 26 API directly -- this module compiles
with a JDK 21 toolchain, so it cannot -- while dispatching on the runtime version made
`import module` crash on JDK 23 and 24, whose parsers already build a JCModuleImport
because the formatter enables preview features:

    M.java:1:2: error: expected token: 'module'; generated java instead

Sort module imports between static and non-static imports, matching google-java-format's
ImportType { STATIC, MODULE, NORMAL }, so projects running both formatters do not get
their imports reordered back and forth.

Lower the version gates to what the behaviour needs: ModuleImport to 23, and
CompactSource, UnnamedPattern, MarkdownDoc and FlexibleConstructor to 21. Replace the
JDK 22/23/25/26 test matrix with testJdk23 and testJdk25 (23 was already provisioned on
develop), and drop the per-OS/arch JDK 25 re-pin that fought the com.palantir.jdks.latest
defaults. FormatterVersionTest asserts each leg resolved to the JDK it asked for, so a
mis-provisioned leg fails instead of silently skipping every version-gated golden.

Register ImportTree#isModule in the native image's reachability-metadata.json so the
GraalVM 23 native image formats module imports too.

Add a GoogleImportStyleTest row sorting `import module.Foo;` as an ordinary import, which
covers the isModuleKeyword lookahead; add the blank line the CLI's full pipeline produces
to the ModuleImport golden; add a changelog entry quoting the error texts users search for;
and correct comments and Javadoc that did not match the code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@asm0dey

asm0dey commented Sep 11, 2026

Copy link
Copy Markdown
Author

Thank you for this review — building both branches on six JDKs, formatting ~6700 files, and mutation-testing the lookahead is far more than I had any right to expect, and it caught things I would not have found. Pushed 2841e38 addressing most of it.

Approach + item 1. You were right, and I reproduced the JDK 23 crash before changing anything: lowering the ModuleImport gate to 23 and running the golden suite on JDK 23 gives exactly 1:2: error: expected token: 'module'; generated java instead. The reflective isModule() check now lives in JavaInputAstVisitor.visitImport, and java25/, java26/, and the >= 25 / >= 26 branches are gone. testJdk23 now runs ModuleImport with zero skips. The gates in RemoveUnusedImportsTest and build.gradle are 23.

Item 2. Changed to static → module → normal, matching ImportType { STATIC, MODULE, NORMAL }. The Javadoc now says Google Style is silent on module imports rather than presenting the order as Google Style. Both style tests updated.

Item 3. I took the second option: JEP 467 and JEP 513 are out of the description, the commit message, and the README, and the two goldens are described as regression tests for behaviour that already worked. Their gates are 21, where they also pass. The wrapLineComments bug is real and I reproduced your @Deprecated case, but it is on develop and unrelated to these three crashes, so I would rather fix it in its own PR than widen this one — happy to open that next, with goldens for a long line, a fence, and a snippet.

Item 4. ImportTree#isModule is registered in reachability-metadata.json. The README section is now "Java language support" with an explicit <a id="java-21-support"></a>, states that compact source files and unnamed patterns format from 21 and module imports from 23, and names the JVM that runs the formatter in each integration.

Item 5. Changelog entry added, quoting all four error strings. The description and ==COMMIT_MSG== block are rewritten with the corrected claims and a Fixes #1506, #1701, #1614 line.

Item 6. Down to testJdk23 and testJdk25; JDK 22 and 26 and their 48 gradle/jdks/ files are gone, as is the per-OS/arch JDK 25 re-pin. Since 23 was already provisioned on develop, only 25 is new. I kept 23 rather than 25 alone because that is the tier where the bug actually lived, and it costs nothing to provision.

Item 7. All four. The import module.Foo; row kills the return true; mutant — I checked. ModuleImport.output has the blank line. FormatterVersionTest compares Runtime.version().feature() with an expectedJavaVersion system property set by each leg, so a mis-provisioned leg fails instead of skipping everything. CompactSource and UnnamedPattern gates are 21.

Item 8. All corrected or cut, and RemoveUnusedImports now reuses the shared maybeGetMethod/invoke instead of duplicating them.

Item 9. Maintainers' call. Happy to close this in favour of #1579 if that is the one you would rather take.

Green on the default JDK 21 test, testJdk23, testJdk25, and spotlessCheck.

AI Disclosure: This change and this comment were prepared with the assistance of Claude Code, and reviewed by me before posting.

Per review: the codebase uses one-line comments, and the first sentence carries
the point.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@vlsi vlsi left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the quick and thorough turnaround. I rebuilt e436451 and checked each reply, and almost all of them hold:

  • import module now formats on JDK 23, 25, and 26, in full and --fix-imports-only modes. The native image built from this head on GraalVM 23 formats it too, with output byte-identical to the JVM on 23.
  • test (JDK 21) runs 1430 tests with 5 skipped (the four ModuleImport cases and the module row in RemoveUnusedImportsTest), and testJdk23 and testJdk25 run 1003 tests each with none skipped.
  • The import module.Foo; row kills the lookahead mutant; I reran it.
  • The new order and blank lines match google-java-format: 200 random import blocks in Google and AOSP style came out byte-identical to google-java-format 1.36.1.
  • On the round-1 corpus (5609 files), develop and this PR produce identical output on JDK 17, 21, and 25, and the PR's output on 23 matches 25.

One thing blocks the build, and one import-fixing bug is worth fixing while the lookahead is fresh. The smaller ones are inline.

./gradlew build fails on the JDK 25 config files

2841e38 removed the jdk(25) block but kept the 24 files under gradle/jdks/25/, which still pin Zulu 25.0.2. With only jdkMajorVersionsToUse().add(JavaLanguageVersion.of("25")) left, the plugin expects its default Corretto build, and checkGradleJdkConfigs, which check depends on, fails:

Execution failed for task ':checkGradleJdkConfigs'.
> Gradle JDK configuration file `gradle/jdks/25/macos/x86-64/download-url` is out of date, please run `./gradlew setupJdks`

Running ./gradlew setupJdks and committing the result fixes it: it rewrites 10 files to amazon-corretto-25.0.3.9.1 and deletes 14 that Corretto doesn't ship (x86, and Windows aarch64). It also means testJdk25 ran locally on the stale Zulu 25.0.2 rather than the JDK CI would provision. CircleCI didn't report on this head ("Could not find a usable config.yml"), so there is no CI signal yet.

A comment or line break after module breaks import fixing

isModuleKeyword skips one space token and then expects an identifier, so a comment or a line break after module makes it treat module as a package name. Both files below compile with javac --release 25:

import module /* comment */ java.base;
class Example {}
import module
    java.base;
class Example {}

On JDK 25, the first fails with Expected ; after import in full format and in --fix-imports-only. The second passes in the CLI's full format but fails in --fix-imports-only, and in Formatter.formatSourceAndFixImports, which reorders imports before formatting. The Gradle plugin's Spotless step reaches that method through FormatterService.formatSourceReflowStringsAndFixImports, so the second file fails for Spotless users too. JDK 23 and the native image behave the same.

import static has the same limitation on develop (Unexpected token after import: /* comment */), so this isn't a regression. But the module lookahead is new code: skipping whitespace and comments in both the lookahead and the parsing that follows it, with the comments kept in the output, would cover both files. The ModuleImport golden bypasses ImportOrderer, so an end-to-end test through formatSourceAndFixImports with these two inputs would also be the first test to run the full pipeline on a module import.

A correction to my round-1 review

I wrote that google-java-format's Javadoc "notes that Google Style says nothing about module imports". That was wrong: its Javadoc says "Module imports are not allowed by Google Style". The Javadoc you wrote from my comment now carries that error. Details inline, sorry for the churn.

Changelog, title, and commit message

  • The docs/changelog status on e436451 is still "Changelog required". Develop removed the top-level changelog/ directory in #1756, so the changelog-app comment looks like the route now, and its text is still the July version with the JEP 467/513, "graceful fallback", and 21/22/23/25/26 matrix claims. If a maintainer ticks that box as is, those claims ship. I'd move the corrected text from pr-1707.v2.yml into the bot comment and drop the file.
  • Bulldozer uses the PR title as the squash commit subject, and the title still says "Support Java 22-26 language features". Something like "Format module imports, compact source files, and unnamed patterns" matches the final code.
  • The description's disclosure says "the full test suite passes on JDK 21, 23, and 25". The 23 and 25 legs run four test classes, and build fails as above.
  • Fixes #1506, #1701, #1614 closes only #1506: GitHub needs the keyword before each number (Fixes #1506, fixes #1701, fixes #1614).

The /// wrapping bugs in a separate PR sound right to me.

@@ -0,0 +1 @@
https://cdn.azul.com/zulu/bin/zulu25.32.21-ca-jdk25.0.2-macosx_x64.zip

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still points at Zulu 25.0.2, but with the jdk(25) block gone the plugin resolves 25 to Corretto 25.0.3.9.1, and checkGradleJdkConfigs fails the build. ./gradlew setupJdks regenerates this directory: 10 files change and 14 are deleted.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. ./gradlew setupJdks rewrote 14 files under gradle/jdks/25/ to amazon-corretto-25.0.3.9.1 and deleted 10 (x86 on every OS, plus Windows aarch64), and ./gradlew build now gets past checkGradleJdkConfigs. Thanks for catching that — the stale Zulu pin also means my earlier testJdk25 numbers were from Zulu 25.0.2, not the Corretto build CI provisions; the re-run on Corretto is green.

* A {@link Comparator} that orders {@link Import}s by Google Style, defined at
* https://google.github.io/styleguide/javaguide.html#s3.3.3-import-ordering-and-spacing.
*
* <p>Google Style says nothing about module imports ({@code import module foo.bar;}, JEP 511); they sort

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My round-1 comment misquoted google-java-format here, sorry. Its Javadoc says "Module imports are not allowed by Google Style", so "says nothing" is wrong. Suggested: "Google Style does not use module imports ({@code import module foo.bar;}, JEP 511); when present, they sort between static and non-static imports, matching google-java-format."

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected, thanks — no churn cost on my side. The Javadoc now reads: "Google Style does not use module imports (import module foo.bar;, JEP 511); when present, they sort between static and non-static type imports, matching google-java-format."

*/
private static final Comparator<Import> GOOGLE_IMPORT_COMPARATOR =
Comparator.comparing(Import::isStatic, trueFirst()).thenComparing(Import::imported);
private static final Comparator<Import> GOOGLE_IMPORT_COMPARATOR = Comparator.comparing(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No test fails if .thenComparing(Import::isModule, trueFirst()) is removed from this comparator. Every Google row uses java.base or java.desktop as the module, and those sort before java.util.List alphabetically anyway. A row with import module org.foo; and import java.util.List; would catch it.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a row with import module org.example.api; and import java.util.List;. org.example.api sorts after java.util.List, so dropping .thenComparing(Import::isModule, trueFirst()) reorders the output and the row fails — I reran it as a mutant to check. The same row also kills the || prev.isModule() != curr.isModule() half of shouldInsertBlankLineGoogle, since without it there is no blank line between the two groups.

*/
private static boolean shouldInsertBlankLineAosp(Import prev, Import curr) {
if (prev.isStatic() && !curr.isStatic()) {
if (prev.isStatic() != curr.isStatic() || prev.isModule() != curr.isModule()) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same for the || prev.isModule() != curr.isModule() half here: every module-to-normal boundary in the AOSP row also changes the top-level package, so the top-level rule inserts the blank line anyway, and removing this half leaves the tests green. A row with only import module java.base; and import java.util.List; would cover it.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added two AOSP rows, because one row can't cover both halves:

  • import module java.base; + import java.util.List; — same top-level package, so the blank line can only come from the module boundary. Removing the || prev.isModule() != curr.isModule() half fails it.
  • import module java.base; + import org.example.Bar; — org.example.Bar is third-party and would sort first, so removing .thenComparing(Import::isModule, trueFirst()) fails it.

I ran all four mutants (both comparators, both blank-line halves) and each one is now killed.

Comment thread README.md Outdated
description: |-
Format Java 22-25 language features that previously crashed the formatter:

* module import declarations (JEP 511) - `expected token: 'module'; generated java instead`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two additions, wherever the entry ends up: the error from the #1506 report itself, Expected ; after import (from --fix-imports-only --skip-removing-unused-imports), and a note that module imports need the formatter to run on JDK 23 or later. Without it, a user on a JDK 21 daemon reads this entry and still gets '.' expected.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both added: the entry now quotes Expected ; after import from --fix-imports-only --skip-removing-unused-imports, and says formatting module imports needs the JVM that runs the formatter to be JDK 23 or later (Gradle daemon for the plugin and Spotless, Project SDK for IntelliJ).

On moving it into the bot comment: I can't edit that comment — it belongs to changelog-app and I don't have write access on the repo, so GitHub won't let me change its text. The corrected text therefore lives in changelog/@unreleased/pr-1707.v2.yml. If a maintainer would rather take the bot route, please use the file's text rather than the comment's (the comment is still the July version with the JEP 467/513, "graceful fallback" and 21/22/23/25/26 claims) — happy to drop the file in the same breath.

asm0dey and others added 4 commits September 11, 2026 19:38
Address review feedback on palantir#1707.

The jdk(25) block that pinned Zulu 25.0.2 is gone, so the jdks plugin resolves 25 to
its default Corretto build and checkGradleJdkConfigs, which `check` depends on, failed:

    Gradle JDK configuration file `gradle/jdks/25/macos/x86-64/download-url` is out of
    date, please run `./gradlew setupJdks`

This is the output of `./gradlew setupJdks`: 14 files now point at
amazon-corretto-25.0.3.9.1, and the 10 platforms Corretto does not ship (x86 on every
OS, and Windows aarch64) are deleted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Address review feedback on palantir#1707.

The module lookahead, and the import parsing that follows it, skipped a single space
token, so a comment or a line break after `module` made ImportOrderer read `module` as
a package name. Both files below compile with `javac --release 25`, and both failed
with `Expected ; after import` -- in `--fix-imports-only` and in
formatSourceAndFixImports, which the Gradle plugin's Spotless step reaches through
FormatterService:

    import module /* comment */ java.base;
    import module
        java.base;

skipIgnored() now skips whitespace, line terminators and comments, both in the
lookahead and at every point where the parser previously skipped one space token.
Comments found inside a declaration are re-emitted after the semicolon rather than
dropped, and a // comment gets a line terminator after it so it cannot swallow the
next import. `import static`, which had the same limitation, is covered by the same
change.

ModuleImportTest takes both inputs through formatSourceAndFixImports and fixImports --
the first end-to-end test of a module import -- and runs on the testJdk23 and testJdk25
legs. GoogleImportStyleTest covers the reordering on JDK 21, since ImportOrderer only
lexes, and gains a module name that sorts after a non-module import; AospImportStyleTest
gains a module import sharing a top-level package with a non-module import, and one
against a third-party import. Each of those rows fails if the module clause is removed
from the comparator or from the blank-line rule.

For the input `import` followed by a line break, the error now names the zero-width EOF
tok instead of the newline, because the line break is skipped before we report it.

FormatterVersionTest uses Assumptions.assumeTrue, so the default `test` task skips it
instead of reporting a pass, and visitName is private again now that no subclass calls it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Address review feedback on palantir#1707.

The README bullet the Java language support section now leans on was wrong for
Spotless: SpotlessInterop uses the native image only when the Gradle daemon runs on
Java 20 or older, so on a newer daemon Spotless formats in the daemon and the daemon's
JDK decides which syntax can be formatted. The native image is also built only for
Linux (glibc) and macOS aarch64, per NativeImageSupport. The stray bullet that
continued the previous one is folded back into it.

The changelog entry now quotes `Expected ; after import`, the error from the palantir#1506
report itself, and says module imports need the JVM that runs the formatter to be
JDK 23 or later.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
skipIgnored() applies to every import, not just module imports, so add a row for
`import /* a type */ com.foo.Second;`, `import static /* a member */ com.foo.First.first;`
and an ordinary import split across lines.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@asm0dey asm0dey changed the title Support Java 22-26 language features Format module imports, compact source files, and unnamed patterns Sep 11, 2026
@asm0dey

asm0dey commented Sep 11, 2026

Copy link
Copy Markdown
Author

Thanks again — the build break and the module lookahead were both real, and the corpus/mutant numbers are a gift. Everything in round 2 is addressed.

./gradlew build on the JDK 25 config files. ./gradlew setupJdks committed: 14 files under gradle/jdks/25/ rewritten to amazon-corretto-25.0.3.9.1, 10 deleted (x86 everywhere, Windows aarch64). checkGradleJdkConfigs passes, and build is green. You were also right that my earlier testJdk25 run used the stale Zulu 25.0.2; the re-run on Corretto 25.0.3 is green.

Comment or line break after module. Fixed in both places. skipIgnored skips whitespace, line terminators and comments, and the parser uses it in the lookahead and everywhere it previously skipped a single space token. Comments inside an import are re-emitted after the semicolon, so import module /* c */ java.base; becomes import module java.base; /* c */ and import module\n java.base; becomes import module java.base;. A // comment gets a line terminator after it so it can't swallow the next import. Ordinary and static imports pick up the same fix (import /* a type */ com.foo.Second;, import static /* a member */ com.foo.First.first;, an import split across lines), with a GoogleImportStyleTest row for them. New ModuleImportTest takes both of your inputs through formatSourceAndFixImports and fixImports — the first end-to-end test on a module import — and runs on the 23 and 25 legs; the reordering rows are also in GoogleImportStyleTest, which runs on 21 because ImportOrderer only lexes.

Your round-1 correction. Taken verbatim: the Javadoc now says Google Style does not use module imports, and that when present they sort between static and non-static type imports, matching google-java-format.

Test coverage you flagged. Google gets a row whose module name sorts after the non-module import (module org.example.api vs java.util.List); AOSP gets two rows, one with a shared top-level package for the blank line and one against a third-party import for the comparator. I ran all four mutants — both comparator clauses and both blank-line halves — and each is killed now.

Smaller inline items. FormatterVersionTest uses Assumptions.assumeTrue, so the default test task skips instead of passing, and both comments now name what would actually be skipped. visitName is private again. The README bullet is rewritten: stray continuation folded in, native image is Linux (glibc) and macOS aarch64 only, and Spotless uses it only on a daemon running Java 20 or older.

Changelog, title, commit message. The entry now quotes Expected ; after import and states the JDK 23 requirement. I can't edit the changelog-app comment (not its author, no write access), so the corrected text lives in changelog/@unreleased/pr-1707.v2.yml — if a maintainer prefers the bot route, please take the file's text, not the comment's. PR title is now "Format module imports, compact source files, and unnamed patterns". The Fixes line reads Fixes #1506, fixes #1701, fixes #1614. The disclosure no longer claims a full suite on 23 and 25; it says what the legs actually run.

Local state: ./gradlew build green, which covers the default JDK 21 test plus testJdk23 and testJdk25.

AI Disclosure: This change and this comment were prepared with the assistance of Claude Code, and reviewed by me before posting.

asm0dey and others added 4 commits September 11, 2026 20:30
Reordering has to be a fixed point: a formatted file must survive being formatted
again, or `format` followed by `formatDiagnose`/`spotlessCheck` fails. It wasn't.

scanImports only absorbed // comments into an import's trailing text, so a block
comment after the `;` ended the import and left `afterLastImport` pointing at the
comment, which the contiguity check then rejected:

    import java.util.List; /* x */   ->   error: Imports not contiguous (perhaps a
    import java.util.Set;                 comment separates them?)

That input is writable by hand and failed before module imports existed, and the
previous commit made the orderer produce it, so a file with a comment inside an
import formatted once and then failed. It now absorbs a block comment on the same
line as the `;`, and leaves one on a later line to whatever follows it. Emission no
longer puts a space before a comment that starts a line, and no longer doubles the
line terminator after a // comment.

Javadoc is excluded, both trailing an import and inside one: the formatter moves a
javadoc comment onto a line of its own, which would separate the imports, so such an
import is rejected exactly as it was before.

Both style suites now assert that every expected output reorders to itself, which is
what catches this class of bug, and ModuleImportTest asserts the same for
formatSourceAndFixImports and fixImports.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Assert the resolved toolchain in Gradle as well as in FormatterVersionTest: the test
reads a system property and skips when it is absent, so a typo in the property name
made a mis-provisioned leg pass. A `doFirst` check on the launcher's language version
cannot skip.

Run the import-ordering suites on the 23 and 25 legs too. They hold every
module-import ordering row, and ImportOrderer only lexes, so JDK 21 covers them
today -- but that is where a new module-import ordering test would land.

Cover two paths nothing reached: `instanceof Box(Integer _, _)`, the unnamed-pattern
form the JEP and palantir#1579 use, which the golden only exercised through `switch`; and
`--fix-imports-only --skip-removing-unused-imports` on a module import, the exact
invocation reported in palantir#1506.

Narrow the new API surface on JavaInputAstVisitor, a published class this module does
not gate with revapi: IMPLICIT_CLASS and the four-argument addBodyDeclarations are
private, since only the reflection helpers need to be reachable from the java14 and
java21 subclasses in other packages.

Drop the claim that the native image handles module imports *because* ImportTree#isModule
is registered in reachability-metadata.json. Removing the registration and rebuilding
the image still formats module imports, in full and --fix-imports-only modes:
native-image folds the lookup because its arguments are compile-time constants. The
entry stays as belt and braces, in alphabetical order with its neighbours.

Also: the README lists the Eclipse plugin's JVM, since it runs the formatter in
process, and the changelog records that a comment inside an ordinary or static import
now formats instead of failing, which is a behaviour change for code using no new
syntax.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`case Box(var _, _)` fails differently on develop -- `expected token: '_'; generated )
instead` at the `var _`, not the `generated , instead` the golden reproduced -- so a
regression that re-broke only that shape would have passed. Add it, along with a
nested pattern (`Pair(Box(var _, Integer i), _)`) and an unnamed pattern beside a
`when` guard, which is the idiomatic pairing.

The golden's output compiles under `javac --release 25` and `--release 26`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Markdown documentation comments are plain line comments to a javac older than 23, so
`trees.getDocCommentTree` returns nothing for them and `RemoveUnusedImports` cannot see
their references. Verified: with the formatter on JDK 21, `import java.util.List;`
referenced only by `/// see [List]` is deleted; on JDK 25 it is kept.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@asm0dey

asm0dey commented Sep 11, 2026

Copy link
Copy Markdown
Author

Before asking for another round, I had this branch attacked rather than reviewed — independent passes over ImportOrderer, the approach and build wiring, pattern matching, compact source files, and /// comments, each required to back a finding with a run. It found a bug in my own last commit, so here is everything, including what I got wrong.

A bug I introduced, now fixed (50d151f)

Moving an interior comment past the semicolon was not a fixed point. scanImports only absorbed // comments into an import's trailing text, so a block comment after the ; ended the import, left afterLastImport pointing at the comment, and the contiguity check rejected it on the next pass:

import /* x */ java.util.List;
import java.util.Set;

formatted once to import java.util.List; /* x */, and formatting that again threw Imports not contiguous (perhaps a comment separates them?). So format followed by formatDiagnose/spotlessCheck would have failed on a file the formatter had just written. Worse, the two goldens I added last round were themselves non-round-trippable, and the suite stayed green because the harness ran a single pass.

Note import java.util.List; /* x */ is hand-writable and fails on develop today, so the absorption gap predates module imports.

Fixed: a block comment on the same line as the ; is absorbed into trailing text; one on a later line still belongs to whatever follows. Emission no longer puts a space before a comment that starts a line, nor doubles the line terminator after a // comment. Javadoc stays rejected, inside or trailing an import, because formatSource moves a javadoc comment onto a line of its own, which would separate the imports — I verified that rather than assuming it.

Both style suites now assert that every expected output reorders to itself, and ModuleImportTest asserts the same through formatSourceAndFixImports and fixImports. That invariant is what catches this class of bug.

Coverage holes closed (78aca54, 8f6e15b)

  • FormatterVersionTest could not catch a typo in its own property name: with expectedJavaVersionTYPO and a mis-provisioned launcher, testJdk25 ran entirely on JDK 23 and the build was green. The leg now also asserts javaLauncher.metadata.languageVersion in Gradle, where it cannot skip.
  • case Box(var _, _) fails differently on develop — expected token: '_'; generated ) instead at the var _, not the generated , instead the golden reproduced — so a regression that re-broke only that shape would have passed. Added, with a nested pattern and an unnamed pattern beside a when guard. The golden's output compiles under javac --release 25 and --release 26.
  • instanceof Box(Integer _, _), the form the JEP and Fix parsing of unnamed patterns #1579 use, was untested; the golden only went through switch.
  • --fix-imports-only --skip-removing-unused-imports on a module import — the exact invocation in the Support for module import #1506 report, and the one the changelog names — had no end-to-end test. It has one now.
  • The import-ordering suites, which hold every module-import row, now also run on the 23 and 25 legs.

Two claims of mine that were wrong

  • I wrote that ImportTree#isModule is registered in reachability-metadata.json so the native image handles module imports. Deleting the registration and rebuilding still formats module imports, in both full and --fix-imports-only modes: native-image folds the lookup because its arguments are compile-time constants. The entry stays as belt and braces; the description no longer claims it is the reason.
  • The changelog described only the three JEPs, but a comment inside an ordinary or static import used to be an error and now formats, with the comment moved past the semicolon. That is a behaviour change for code using no new syntax, and it is in the entry now.

Also narrowed: IMPLICIT_CLASS and the four-argument addBodyDeclarations are private again. This module is not gated by revapi, so protected on a published class is a permanent commitment, and only the reflection helpers need to be reachable from the java14/java21 subclasses.

Held up under attack

Stated because the negative result is also information. The reflective isModule lookup was probed on 17, 21, 22, 23, 25 and 26: it is public abstract on the exported ImportTree interface from 23, absent before, so no setAccessible and no --add-exports. IMPLICIT_CLASS = 1L << 19 is UNNAMED_CLASS on 21, IMPLICIT_CLASS on 22+, and unassigned on 17, so the compact-source branch cannot misfire on the oldest runtime the major-55 artifact supports. No JCImport cast remains; restoring one reproduces the original ClassCastException. The per-node scan override costs nothing measurable (58.8 vs 60.7 ms/iter formatting a 4000-line file — noise). setupJdks output is byte-exact. A 260-file randomised pattern corpus, a width sweep, and per-line partial formatting of four _-heavy files produced no crash and no non-idempotency on 21/23/25/26, and develop-vs-this-branch over all ~500 testdata inputs differs in exactly the three files this PR fixes.

On Java 26 specifically: its Source$Feature set adds one entry over 25 (CAPTURE_MREF_RETURN_TYPE, semantics only) and its previews are API-only, so there is no new syntax to format. All 231 goldens are fixed points on 26 and the whole repo formats byte-identically on 21 and 26.

Pre-existing bugs found, each reproduced on a develop build

None are regressions here; listing them so they are not lost, and happy to open issues or PRs for any.

  1. /// wrapping can silently add an annotation, and the result compiles. In Palantir style a 117-column /// continuation line ending in the word @Deprecated comes out as the comment, then @Deprecated on its own line, then the method — javap -v goes from 0 to 4 Deprecated markers with javac exit 0. The root cause is two lines in JavaCommentsHelper.wrapLineComments: on JDK 23+ javac returns a /// run as one token, lineCommentPrefix reads the prefix from index 0 of a continuation line that still carries its source indentation and returns "", and the width test adds column0 on top of indentation that is about to be stripped. It needs the block to be indented, which is why it survived: Fix markdown docstring wrapping #1672's test uses a single /// line and runs on JDK 21, where the multi-line path is unreachable. Before Fix markdown docstring wrapping #1672 the same input produced // limit @Deprecated — wrong docs, valid Java. I am preparing that fix as its own PR, with the repro, the threshold case, the unbreakable-token case, module-info.java, and a JDK-independent unit test on the string transformation so it is covered on a JDK 21 CI.
  2. Same family: an unbreakable token emits a lone /// with the content on a bare line; ///Summary. gains a space, which raises the run's common indentation and turns an indented code block into a paragraph (confirmed in the rendered HTML); two trailing spaces, a CommonMark hard line break, are trimmed away; doc/Comment.java:75-77 holds a second, unguarded copy of the missing-space rule that also breaks trailing //noinspection and //$NON-NLS-1$; and the same /// input formats differently on 21/22 than on 23+.
  3. Colon-form guards. case R r when o.hashCode() > 0: always breaks before when and puts it at the case column, at ~50 characters. Java14InputAstVisitor.java:290-295 — the guard's breakToFill sits outside any open(plusFour). This PR makes it far more visible, since _ with a guard was previously a crash.
  4. Removing the only import is not idempotent: pass 1 leaves a leading blank line, pass 2 removes it. Same on 21/22/23/25/26 and in module-info.java.
  5. Smaller ones: case Nothing(/* no components */) gains a space before ); a wrapping record pattern strands instanceof alone on a line; unicode-escaped identifiers (abc, not just _) fail with expected token: '\'; ~500-deep nested patterns overflow the stack, with or without _.
  6. On JDK 21/22 an import referenced only from a markdown link (/// see [List]) is removed as unused, because getDocCommentTree returns nothing for /// there. That tier difference is now in the README.

./gradlew build is green on this head, including nativeCompile, with 1441 tests on the JDK 21 leg and 1006 on each of 23 and 25.

AI Disclosure: This round of review was run with the assistance of Claude Code — several independent adversarial passes, each required to reproduce its findings — and the fixes and this comment were prepared the same way and reviewed by me before posting.

asm0dey and others added 2 commits September 11, 2026 21:09
The golden was a field and a `void main()`, so the two things this PR actually adds for
JEP 512 -- the `Indent.Const.ZERO` member indent and the `FirstDeclarationsOrNot.YES`
blank-line policy -- were pinned only in their simplest shape. Add an import directly
above the first member, where the import-driven blank line meets `first0 = YES`; a
nested record among the top-level members, which takes the ClassTree member branch; and
javadoc and a markdown doc comment on top-level members.

The golden's output compiles under `javac --release 25`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ImportOrderer threw away the text of each import and re-synthesized it from the kind
and the name, so every comment in the declaration had to be relocated to wherever that
synthesized string could hold it -- after the semicolon -- and anything that did not
reduce to those two pieces was rejected:

    import module java . base;          error: Expected ; after import
    import module java./* why */base;   error: Could not parse imported name, at: /* why */

Both compile with `javac --release 25`, and `formatSource` formats both; only the
import-fixing entry points failed, which the Gradle plugin's Spotless step reaches
through FormatterService. The same cause made the earlier comment handling a patch on a
symptom: a relocated comment is a comment the author has to find again.

The declaration is now rendered once while it is scanned, normalizing the whitespace --
one import per line is the style -- and leaving each comment in the slot it was written
in. That removes the comment list, the line-terminator juggling around it, and the
special case for a `//` comment inside a declaration.

Imports that compare equal still collapse into one, but the comparator now breaks ties
on the rendered declaration, so two imports of the same name that are written
differently are both kept. Before, the second one vanished, and any comment it carried
vanished with it.

Consequences worth noting in review: `import com . foo . Second ;`, which an existing
row recorded as "syntactically valid, but we don't support it", now formats; and a
duplicate that differs only by a comment is no longer deduplicated. A duplicate that
differs only by a trailing comment after the semicolon still loses it, as on develop.

Javadoc inside or trailing an import is still rejected: the formatter moves a javadoc
comment onto a line of its own, which would separate the imports.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@abashev

abashev commented Sep 22, 2026

Copy link
Copy Markdown

Hi @asm0dey @vlsi if you don't mind I took your draft as a base for implementing new Java feature till 27th release. It also has CI configuration for different JDKs' but it is not so much changes compare to original PR openjavaformat#30

abashev added a commit to openjavaformat/open-java-format that referenced this pull request Sep 23, 2026
Conflicts with #43 and #44 in ImportOrderer, RemoveUnusedImports and
their tests.

ImportOrderer: an import now carries both the comments on the lines
before it, from #44, and its declaration rendered from its toks, from
this branch (palantir#1707). This branch's loop
over same-line block comments already covers #44's single block comment
after the `;`, so #44's copy of that step goes. Two interactions are
decided here:

- A javadoc comment right after an import's `;` is no longer an error.
  This branch rejected it because the formatter moves it onto a line of
  its own, which used to separate the imports. Since #44 a comment
  between imports goes with the import after it, and so does this one.
- Two copies of an import that differ only in the comment on the lines
  before them no longer collapse into one, so that comment stays. This
  branch already kept copies that differ in a comment inside the
  declaration.

RemoveUnusedImports: this branch's loop over JCTree with
Trees.getEndPosition, and #43's coalesced ranges and blank-line cleanup.

Tests: every case from both sides. This branch's case for a same-line
block comment repeated #44's and is folded into it. Its javadoc case now
expects the comment to move with the next import. A new case covers the
duplicate.

Checked: all 1493 formatter tests pass. On the JDK 21 sources (15,747
files, Temurin 21) and the JDK 25 sources (15,368 files, Corretto 25),
the merge formats every file exactly as main does, with no errors.
abashev added a commit to openjavaformat/open-java-format that referenced this pull request Sep 23, 2026
Bring over upstream palantir#1707 (Java 25 syntax) and palantir#1786 (JDK 27 end positions)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

9 participants