Add rewrite-java-next module for non-LTS JDK support#7719
Conversation
Introduces a new `rewrite-java-next` module copied from `rewrite-java-25`, intended as a moving target for the latest non-LTS JDK releases (initially 26; will advance to 27/28 as those ship, then be renamed to `rewrite-java-29` once 29 LTS is released). Bundles the JDK 26 parser fixes from #7716 and #7717 into this new module rather than backporting them into `rewrite-java-25`: - Implicit lambda parameter type: handle the zero-width `JCErroneous` vartype that JDK 26 synthesizes at the parameter name's position (previously detected only via `NOPOS`). - Annotation `value=` shorthand: handle the synthesized `JCAssign` carrying a zero-width range at the rhs's start position. - Enum-constant identifier: skip the synthesized class identifier on JDK 26 using `endPos > startPos` instead of `endPos >= 0`. `JavaParser` now dispatches to `JavaNextParser` for JDK 26+, falling through to `Java25Parser` for 25, etc. Three new test classes cover the JDK 26 regressions referenced in #7554.
|
This finally adopts the style discussed with Shannon when we started rewrite-java-25 and dropped the backports that made the parsers hard to reason about and maintain. rewrite-java-next would be used for projects running on Java 26 or newer, and continuously updated as new versions come out. We still need the matching changes downstream in the Maven and Gradle plugins and the CLI. |
The annotation and lambda tests duplicated existing TCK coverage (AnnotationTest.annotationWithDefaultArgument / LambdaTest.untypedLambdaParameter) — those already exercise the JDK 26 fix once rewrite-java-next:compatibilityTest runs on JDK 26. The two enum cases with non-literal arguments (method call, string concat) are genuinely new ground, so move them to EnumTest in the TCK where they run against every parser module.
The default Gradle test logging only writes failure detail to the HTML report, which isn't preserved as a CI artifact. Since this module is the only one exercising the TCK on the latest non-LTS JDK, individual failures need to be visible directly in the CI log to diagnose JDK-version-specific parser issues.
CI surfaced two classes of follow-up failures from JDK 26's switch from
NOPOS to zero-width source ranges for synthesized AST nodes:
1. `var` / `val` local declarations (and record patterns,
try-with-resources, unnamed variables) were rendering the inferred
type instead of the source's `var` keyword. The existing
`endPos(vartype) < 0` guard in visitVariables didn't fire on JDK 26.
Factor the dual-shape (NOPOS or zero-width) check into a shared
`isSynthesizedVarType` helper and apply it to both the lambda-parameter
and the local-declaration branches.
2. Single-element annotation shorthand on record components (e.g.
`@Select("native") String value`) was producing J.Erroneous nodes.
On JDK 26 the JCAssign sometimes carries the rhs's full range rather
than a zero-width one, so checking the JCAssign's own positions
missed those cases. Switch `isSyntheticValueAssignment` to inspect
the lhs (the `value=` identifier) instead — the lhs is always
synthesized regardless of how javac chose to position the assignment.
Position-based detection of the synthesized `value=` JCAssign worked for
annotations on classes/methods (zero-width range on JDK 26, NOPOS on JDK
≤ 25) but not for annotations on record components, where JDK 26
apparently encodes the synthesized assignment to overlap with the rhs's
range. Switch to source-text detection: a real `value = "x"` assignment
shows an `=` in source between the annotation's `(` and the rhs;
shorthand `("x")` does not. Falls back to the prior position-based
check if the rhs's start sits behind the cursor (unrecoverable position
data).
Fixes the four remaining RecordTest annotation failures on JDK 26
(annotationsAndRecords, typeParameterAnnotation,
recordWithMultipleAnnotations, differentLiteralTypesAnnotation).
Parser visitor diff:
|
| Affected TCK tests on JDK 26 (before fixes) | Cause | Fix |
|---|---|---|
AnnotationTest.annotationWithDefaultArgument |
Synthesized value= JCAssign no longer has NOPOS; old endPos(arg) < 0 guard missed it, so the parser rendered both the synthesized value identifier and the rhs (producing valueecked") |
isSyntheticValueAssignment — source-text based |
RecordTest.{annotationsAndRecords, typeParameterAnnotation, recordWithMultipleAnnotations, differentLiteralTypesAnnotation} |
Propagated JCLiteral values inside record-component annotations carry zero-width ranges at startPos on JDK 26 (not NOPOS, not their real range); cursor(endPos) set cursor to its own position, leaving literal text unconsumed in source, and convertAll wrapped the otherwise-correct J.Literal in J.Erroneous whose printed text was the literal itself (hence the misleading "native", "A", "a", "b" in failure output) |
extended synthesized-literal detection in visitLiteral: endPos == NOPOS || endPos <= startPos |
EnumTest.enumWithParameters (plus the new enumConstantWith{MethodCall,StringConcat}Argument cases added to the TCK) |
Synthesized class identifier on enum constants is now zero-width instead of NOPOS; old endPos >= 0 guard let it through and visitIdentifier advanced the cursor past the args |
endPos > startPos instead of endPos >= 0 |
LambdaTest.untypedLambdaParameter and any other implicit lambda parameter |
Synthesized vartype for an implicit lambda parameter is now a zero-width JCErroneous at the parameter name; old endPos(vartype) < 0 didn't fire and convert(vartype) failed the cast to TypeTree |
isImplicitLambdaParameterType via isSynthesizedVarType |
VariableDeclarationsTest.{typeOnVarKeyword, string, finalVar, unknownVar, implicitlyDeclaredLocalVariable}, RecordPatternMatchingTest.shouldParseRecordPatternWithVarKeyword, TypeCastTest.intersectionCastAssignedToVar, TypeUtilsTest.{typeToString2, intersectionTypes}, LombokTest.{val, var}, UnnamedVariableTest.{unnamedVariableInTryWithResources, unnamedVariableInLocalDeclaration, unnamedVariableInForLoop, unnamedVariableWithSideEffects} |
Same dual-shape issue as the lambda parameter case, but for var/val local declarations (the original PR fix only covered the PARAMETER-flagged branch) |
extract isSynthesizedVarType and apply it to both branches |
Build config
rewrite-java-next/build.gradle.kts differs from rewrite-java-25/build.gradle.kts only in the toolchain bump (JDK 25 → 26) and a testLogging { events("failed") } block that surfaces TCK failure detail in CI logs — added because this module is the only one exercising the TCK on the latest non-LTS JDK and the HTML report isn't preserved as a CI artifact.
The 4 remaining JDK 26 RecordTest failures had a different root cause from the synthesized-position issues fixed earlier: the canonical constructor parameter annotations (synthesized copies of the record-component annotations) on JDK 26 now carry real source positions identical to the originals. In the record-annotation merging logic these copies were put into the same per-component map keyed by .pos AFTER the originals from rc.getOriginalAnnos() — silently overwriting the authoritative versions with copies that have a structurally different (often JCErroneous) rhs. On JDK ≤ 25 the copies carried NOPOS (-1) so they collided harmlessly at a single sentinel key and the originals survived. Switch to putIfAbsent semantics for the canonical-constructor merge so the originals win at any shared position.
The 4 remaining RecordTest annotation failures had a different root cause from what the prior commits hypothesized. On JDK 26, javac wraps the argument expressions of record-component annotations in JCErroneous nodes — but the wrapping is purely structural; the single child is the real expression with correct positions, types, and value. The previous `putIfAbsent` experiment (reverted in this commit) showed that the JCErroneous wrapping is inherent to the annotation as javac stores it, not a consequence of which merge source wins (canonical constructor parameter vs. `rc.getOriginalAnnos()`). Both views share the same JCAnnotation references, so the JCErroneous is observable regardless of which one we pick. Have `visitErroneous` transparently delegate to the wrapped expression when there's exactly one child that's a JCExpression — the wrapper has no semantic meaning, and producing a J.Erroneous for it would trip the LST validation "erroneous nodes" check.
The previous visitErroneous-unwrap fix didn't help — the same RecordTest annotation failures persist. Encode the JCErroneous's children count, child class names, child positions, and source content into the J.Erroneous text so the next CI failure output surfaces the actual shape javac is producing on JDK 26. To be reverted once the root cause is understood.
Root cause of the four remaining RecordTest annotation failures: when
javac propagates a record-component annotation to the canonical
constructor parameter on JDK 26, the propagated JCLiteral (the value
inside e.g. `@Select("native")`) is reported with a zero-width
source range at its start position — not the literal's real range and
not NOPOS. The existing `endPos == Position.NOPOS` guard in
visitLiteral missed this, so cursor(endPos) set the cursor to its own
current position, leaving the literal text unconsumed in source. The
outer convertAll then saw non-whitespace in the suffix and wrapped the
resulting J.Literal in J.Erroneous (whose printed text was the literal
itself — hence the misleading `"native"`, "A", "a", "b"
contents in the test failures).
Fix: extend the synthesized-literal detection to also fire when
`endPos <= startPos` (matching the pattern used elsewhere for
synthesized vartypes, value= assignments, and enum-constant
identifiers), recompute the end position via cursor + value length +
quotes, and use the (now-correct) cursor as the start for valueSource.
Revert the two unhelpful changes from prior iterations: the
visitErroneous unwrap (never reached — the JCErroneous wasn't actually
present in the AST) and the diagnostic instrumentation in
getErroneous.
Verified locally on JDK 26: `:rewrite-java-next:compatibilityTest`
passes the full TCK; `:rewrite-java-25:compatibilityTest` still
passes on JDK 25.
Same root cause as the EOL Docker images workflow: rewrite-java-next requires a JDK 26 toolchain at Gradle configuration time. The last scheduled run on 2026-05-19 succeeded because rewrite-java-next was added on 2026-05-20 (#7719); the next Tuesday run would fail.
* Provide JDK 26 toolchain for EOL Docker images workflow The rewrite-java-next module requires a JDK 26 toolchain at Gradle configuration time. Without it, the syncEolImages task fails before it can run. * Provide JDK 26 toolchain for Gradle wrapper update workflow Same root cause as the EOL Docker images workflow: rewrite-java-next requires a JDK 26 toolchain at Gradle configuration time. The last scheduled run on 2026-05-19 succeeded because rewrite-java-next was added on 2026-05-20 (#7719); the next Tuesday run would fail.
Summary
Adds a new
rewrite-java-nextmodule copied fromrewrite-java-25, intended as a moving target for the latest non-LTS JDK releases — initially targeting JDK 26, will advance to 27/28 as those ship, then be renamed torewrite-java-29once 29 LTS lands.Bundles the JDK 26 parser fixes from Java parser: handle JDK 26 implicit lambda parameter type #7716 and Java parser: handle JDK 26 synthesized annotation
value=and enum-constant identifiers #7717 into this new module instead ofrewrite-java-25: implicit lambda parameter type (zero-widthJCErroneousvartype), single-element annotationvalue=shorthand (zero-widthJCAssign), and enum-constant synthesized class identifier.JavaParsernow dispatches toJavaNextParserfor JDK 26+, falling through toJava25Parserfor 25 etc.Three new test classes (
Java26ImplicitLambdaParameterTest,Java26AnnotationValueTest,Java26EnumConstructorTest) cover the JDK 26 regressions referenced in JDK26 behaves diffently than JDK25 - "There were problems parsing" #7554.Closes Java parser: handle JDK 26 implicit lambda parameter type #7716
Closes Java parser: handle JDK 26 synthesized annotation
value=and enum-constant identifiers #7717Test plan
./gradlew :rewrite-java-next:test :rewrite-java-next:compatibilityTeston a JDK 26 toolchain./gradlew :rewrite-java-25:test :rewrite-java-25:compatibilityTeststill green on JDK 25 (no regression in the LTS module)./gradlew :rewrite-java:testvalidates the dispatcher change inJavaParser