Conversation
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.
|
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. |
Generate changelog in
|
|
+1 |
1 similar comment
|
+1 |
|
+1 Any plans when this will be merged? |
|
+1 |
1 similar comment
|
+1 |
|
+1 hoping this gets merged soon! |
vlsi
left a comment
There was a problem hiding this comment.
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.createVisitorstill throwsLinkageErrorif a visitor class fails to load. The only fallback is the versionifchain. - "the library still compiles at
libraryTarget = 17":compileJavauses a JDK 21 toolchain withsourceCompatibility/targetCompatibility11, and the classes are major version 55. - "Formatter tests run on a JDK 21/22/23/25/26 matrix": the extra legs run only
FormatterIntegrationTestandStringWrapperIntegrationTest, plusRemoveUnusedImportsTeston 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.
…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>
|
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 Item 2. Changed to static → module → normal, matching 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 Item 4. Item 5. Changelog entry added, quoting all four error strings. The description and Item 6. Down to Item 7. All four. The Item 8. All corrected or cut, and 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 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
left a comment
There was a problem hiding this comment.
Thanks for the quick and thorough turnaround. I rebuilt e436451 and checked each reply, and almost all of them hold:
import modulenow formats on JDK 23, 25, and 26, in full and--fix-imports-onlymodes. 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 fourModuleImportcases and the module row inRemoveUnusedImportsTest), andtestJdk23andtestJdk25run 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/changelogstatus on e436451 is still "Changelog required". Develop removed the top-levelchangelog/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 frompr-1707.v2.ymlinto 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
buildfails as above. Fixes #1506, #1701, #1614closes 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 | |||
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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."
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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()) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.Baris 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.
| description: |- | ||
| Format Java 22-25 language features that previously crashed the formatter: | ||
|
|
||
| * module import declarations (JEP 511) - `expected token: 'module'; generated java instead`, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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>
|
Thanks again — the build break and the
Comment or line break after 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 ( Smaller inline items. Changelog, title, commit message. The entry now quotes Local state: AI Disclosure: This change and this comment were prepared with the assistance of Claude Code, and reviewed by me before posting. |
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>
|
Before asking for another round, I had this branch attacked rather than reviewed — independent passes over A bug I introduced, now fixed (50d151f)Moving an interior comment past the semicolon was not a fixed point. import /* x */ java.util.List;
import java.util.Set;formatted once to Note Fixed: a block comment on the same line as the Both style suites now assert that every expected output reorders to itself, and Coverage holes closed (78aca54, 8f6e15b)
Two claims of mine that were wrong
Also narrowed: Held up under attackStated because the negative result is also information. The reflective On Java 26 specifically: its Pre-existing bugs found, each reproduced on a develop buildNone are regressions here; listing them so they are not lost, and happy to open issues or PRs for any.
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. |
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>
|
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 |
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.
Bring over upstream palantir#1707 (Java 25 syntax) and palantir#1786 (JDK 27 end positions)
Before this PR
palantir-java-format supports Java language features through Java 21. Three kinds of source file that
javac --release 25compiles crash the formatter:import module java.base;(JEP 511)expected token: 'module'; generated java instead,Expected ; after importfrom--fix-imports-only --skip-removing-unused-imports, andclass com.sun.tools.javac.tree.JCTree$JCModuleImport cannot be cast to class com.sun.tools.javac.tree.JCTree$JCImportfrom--fix-imports-onlymain(JEP 512)expected token: 'void'— the implicit top-level class emits aclasstoken that is not in the sourcecase Box(_, _)(JEP 456)expected token: '_'; generated , insteadAfter 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"]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 returnsnulland imports format exactly as before.ANY_PATTERNemits one_token, withoutsync(), because javac records that node's start position one past the_.ImportTree#isModuleis registered in the native image'sreachability-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-onlymodes.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.
Every one of those already compiled, and
formatSourcealready handled them; only the import-fixing entry points failed — includingformatSourceAndFixImports, which the Gradle plugin's Spotless step reaches throughFormatterService. 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.
///markdown docs understood as documentationThe last row is pre-existing and not something this PR changes: before 23 a
///comment is an ordinary line comment to javac, soRemoveUnusedImportscannot 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$Featureset 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
ImportOrdererrewrite and are worth an explicit look in review:import com . foo . Second ;, which an existing test row recorded as "syntactically valid, but we don't support it", now formats toimport com.foo.Second;.importfollowed 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.formatSourcemoves javadoc onto a line of its own, which would separate the imports.The changelog entry records the first and the third.
Tests
testtestJdk23RemoveUnusedImportsTest,ModuleImportTest,FormatterVersionTest, both import-ordering suites; module imports as a preview featuretestJdk25checkdepends on both extra legs. Each asserts the JDK it resolved to twice:FormatterVersionTestcomparesRuntime.version().feature()with a system property, and the task itself checksjavaLauncher.metadata.languageVersionin adoFirst, 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 _, awhenguard,instanceof,catch).MarkdownDocandFlexibleConstructorare regression pins for behaviour that already worked: this PR has nosrc/mainchange for JEP 467 or JEP 513, and javac parsessuper(...)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
ModuleImportTestasserts the same throughformatSourceAndFixImportsandfixImports— 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
JCModuleImportClassCastException, in a different way, and the two conflict inRemoveUnusedImports.java,FileBasedTests.javaand theUnnamedPatterngoldens. Maintainers: merge one and close the other — happy for that to be #1579 if you prefer its approach.Possible downsides
./gradlew buildprovisions 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.Fixes
palantir-java-formatcannot parse JEP 512 compact source files / instancemainon JDK 25 (expected token: 'void') #1701 — compact source files / instancemain.(One
fixesper 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 buildpasses locally, which runs the default JDK 21testtask plus thetestJdk23andtestJdk25legs andnativeCompile.