mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 08:07:49 +00:00
fix(java): support Lombok builders on compact record constructors (#950)
@Builder on a compact record constructor (record R(...) { @Builder R {} }) failed in the
editor: ECJ has not filled in a compact constructor's implicit parameters when Lombok
runs, so the builder got no fields and builder() was undefined. A patch to Lombok's
Eclipse HandleBuilder takes the parameters from the record components in that case.
The bundled agent is now lombok-1.18.48-openidea.3, built from v1.18.48 with #4059,
#4089 and java/lombok/patches/builder-compact-constructor.patch through the documented
Ant build; only HandleBuilder differs from openidea.2. The new agent path gives JDT LS a
fresh workspace. java/lombok/tests/verify.py compiles and runs the regression cases with
javac and with ECJ plus the agent.
Co-authored-by: egasa21 <44431625+egasa21@users.noreply.github.com>
This commit is contained in:
+34
-13
@@ -1,6 +1,6 @@
|
||||
# Lombok agent for JDT LS
|
||||
|
||||
`lombok-1.18.48-openidea.2.jar` is the app-provided Java agent for JDT LS. It includes:
|
||||
`lombok-1.18.48-openidea.3.jar` is the app-provided Java agent for JDT LS. It includes:
|
||||
|
||||
- [projectlombok/lombok#4059](https://github.com/projectlombok/lombok/pull/4059), which restores
|
||||
`lombok.config` lookup after Equinox removed `getModuleClassLoader(boolean)`.
|
||||
@@ -10,6 +10,11 @@
|
||||
that call in the `@SneakyThrows` try block (ECJ then fails with "Internal Error compiling …
|
||||
BlockScope cannot be cast to MethodScope" and the whole workspace build fails) and put `@NonNull`
|
||||
checks before it; constructors Lombok generates lacked the statement.
|
||||
- [`patches/builder-compact-constructor.patch`](patches/builder-compact-constructor.patch), not yet
|
||||
proposed upstream. `@Builder` on a compact record constructor (`record R(...) { @Builder R {} }`)
|
||||
failed under ECJ ("The method builder() is undefined", missing builder setters), because ECJ has
|
||||
not filled in a compact constructor's implicit parameters when Lombok runs. The builder now takes
|
||||
its parameters from the record components in that case; javac was unaffected.
|
||||
|
||||
The jar is embedded by `ide-tools`, so every platform receives the same bytes without a download.
|
||||
Both Java server startup and launch diagnostics obtain it through `managed_bundle`. Tool detection
|
||||
@@ -19,7 +24,7 @@ The filename includes the patch version because JDT LS workspace keys include th
|
||||
changing agents must rebuild classes produced by the old agent. Old jars remain for running windows.
|
||||
|
||||
Replace this temporary bundle with an official Maven Central release once that release includes
|
||||
#4059 and #4089. Restore the registry's Maven Central install method and remove the embedded jar. Keep the
|
||||
#4059, #4089 and the compact-constructor builder fix. Restore the registry's Maven Central install method and remove the embedded jar. Keep the
|
||||
workspace migration test when changing its path.
|
||||
|
||||
## Provenance and build
|
||||
@@ -33,10 +38,12 @@ workspace migration test when changing its path.
|
||||
- PR #4089 (`Rawi01/lombok` `eclipse-constructor-call-statement`):
|
||||
- `7832faa49b35c839088866daf865384c82cd555f`
|
||||
- `3f3a80b857a969839ab1baba12767aec4965ac32`
|
||||
- Upstream version: `1.18.48`; `openidea.2` identifies this distribution of the patched agent
|
||||
(`openidea.1` had #4059 only).
|
||||
- Then `git apply` [`patches/builder-compact-constructor.patch`](patches/builder-compact-constructor.patch)
|
||||
(changes `src/core/lombok/eclipse/handlers/HandleBuilder.java` only).
|
||||
- Upstream version: `1.18.48`; `openidea.3` identifies this distribution of the patched agent
|
||||
(`openidea.1` had #4059 only; `openidea.2` added #4089).
|
||||
Upstream's own `java -jar ... --version` still reports `1.18.48`.
|
||||
- SHA-256: `2978d8b2d55dac1bb4936ba44562bc4549931d4e71a7ebfeaebccd60bcdf7379`
|
||||
- SHA-256: `26069a8138680f5e1100c83cd064d6aa7314385a1afcef74269cdcc41beb0442`
|
||||
- License: [MIT, including bundled component notices](LICENSE), also retained inside the jar.
|
||||
- Build environment: macOS arm64, Temurin 25.0.5, Apache Ant 1.10.15, upstream Ivy setup
|
||||
(`openidea.1` was built with OpenJDK 23.0.1).
|
||||
@@ -51,8 +58,9 @@ git fetch https://github.com/chagong/lombok.git fix-equinox-bundle-wiring
|
||||
git fetch https://github.com/Rawi01/lombok.git eclipse-constructor-call-statement
|
||||
git cherry-pick 2d535482a913de46c27d06c5b0b294918ede3f9c 3098f4dd78b5349c2f382e7310e64db810dd380b
|
||||
git cherry-pick 7832faa49b35c839088866daf865384c82cd555f 3f3a80b857a969839ab1baba12767aec4965ac32
|
||||
git apply /path/to/openidea/java/lombok/patches/builder-compact-constructor.patch
|
||||
ant dist
|
||||
cp dist/lombok.jar /path/to/openidea/java/lombok/lombok-1.18.48-openidea.2.jar
|
||||
cp dist/lombok.jar /path/to/openidea/java/lombok/lombok-1.18.48-openidea.3.jar
|
||||
```
|
||||
|
||||
The changelog context conflicts because the first commit adds a release-note line under the
|
||||
@@ -62,13 +70,15 @@ skipped). The #4089 commits apply cleanly. The final source diff from v1.18.48 c
|
||||
`src/eclipseAgent/lombok/launch/PatchFixesHider.java`, `test/core/src/lombok/TestEclipse.java`,
|
||||
`test/core/src/lombok/launch/PatchFixesHiderTest.java` (#4059) and the Eclipse handlers
|
||||
`EclipseHandlerUtil`, `HandleConstructor`, `HandleFieldNameConstants`, `HandleNonNull`,
|
||||
`HandleSneakyThrows`, `HandleStandardException`, `HandleSuperBuilder`, `HandleUtilityClass` (#4089);
|
||||
no other Java source changes were made.
|
||||
In the build checkout, the two remote resolvers in
|
||||
`buildScripts/ivysettings.xml` were reordered to try Maven Central before Eclipse staging; the
|
||||
local resolver and all dependency coordinates were retained. The JVM was configured to use the
|
||||
local HTTP/HTTPS proxy for downloads. Ant's timestamps mean a rebuild need not be byte-identical;
|
||||
`SHA256SUMS` records the checked-in artifact and is verified by the Rust regression tests.
|
||||
`HandleSneakyThrows`, `HandleStandardException`, `HandleSuperBuilder`, `HandleUtilityClass` (#4089)
|
||||
and `HandleBuilder` (the compact-constructor patch); no other Java source changes were made.
|
||||
`openidea.3` was built with the unmodified upstream build scripts. (`openidea.2`'s build checkout
|
||||
reordered the two remote resolvers in `buildScripts/ivysettings.xml` to try Maven Central before
|
||||
Eclipse staging and used a local HTTP/HTTPS proxy for downloads.) Compared with `openidea.2`, the
|
||||
only changed class is `HandleBuilder`; other differences are build timestamps in the generated
|
||||
`META-INF/services` files and the nested `mavenEcjBootstrapAgent.jar`. Ant's timestamps mean a
|
||||
rebuild need not be byte-identical; `SHA256SUMS` records the checked-in artifact and is verified by
|
||||
the Rust regression tests.
|
||||
|
||||
## Validation
|
||||
|
||||
@@ -98,6 +108,17 @@ On macOS arm64 with JDT LS 1.63.0 / Equinox 3.25.0, server OpenJDK 25 and projec
|
||||
Failed with three internal errors and wrote no classes; `openidea.2` Succeeded with all classes.
|
||||
- `java/java-usages/tests/verify.py` and `read-write.py` passed with `openidea.2`.
|
||||
|
||||
`openidea.3` (compact-constructor builders) on macOS arm64, JDT LS ECJ
|
||||
`compiler.batch_3.46.200.v20261008-1115`, Temurin 25:
|
||||
|
||||
- [`tests/verify.py`](tests/verify.py) compiles and runs
|
||||
[`tests/BuilderCompactConstructor.java`](tests/BuilderCompactConstructor.java) with javac and with
|
||||
ECJ plus the agent: builders on compact constructors (with validation, `toBuilder`, `setterPrefix`,
|
||||
generics, `@Singular`, `@NonNull`, empty records), a long-form canonical constructor, a
|
||||
non-canonical record constructor, type-level record builders, and the #4089 constructor cases.
|
||||
Both passed with `openidea.3`; with `openidea.2`, ECJ failed on every compact-constructor builder.
|
||||
- `java/java-usages/tests/verify.py` and `read-write.py` passed with `openidea.3`.
|
||||
|
||||
Windows and Linux runtime validation has not been performed. Rust tests cover source selection
|
||||
for both supported architectures on all three OS variants, offline provisioning, corruption
|
||||
repair, concurrent windows, explicit user overrides, and JDT LS workspace migration.
|
||||
|
||||
@@ -1 +1 @@
|
||||
2978d8b2d55dac1bb4936ba44562bc4549931d4e71a7ebfeaebccd60bcdf7379 lombok-1.18.48-openidea.2.jar
|
||||
26069a8138680f5e1100c83cd064d6aa7314385a1afcef74269cdcc41beb0442 lombok-1.18.48-openidea.3.jar
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,21 @@
|
||||
diff --git a/src/core/lombok/eclipse/handlers/HandleBuilder.java b/src/core/lombok/eclipse/handlers/HandleBuilder.java
|
||||
index 64a3fa9..6ebaaaf 100755
|
||||
--- a/src/core/lombok/eclipse/handlers/HandleBuilder.java
|
||||
+++ b/src/core/lombok/eclipse/handlers/HandleBuilder.java
|
||||
@@ -493,10 +493,13 @@ public class HandleBuilder extends EclipseAnnotationHandler<Builder> {
|
||||
if (!checkBuilderClassNameAnnotationConflict("Builder", "lombok.Builder", job.builderClassName, ast, annotationNode)) return;
|
||||
|
||||
if (fillParametersFrom != null) {
|
||||
- for (EclipseNode param : fillParametersFrom.down()) {
|
||||
- if (param.getKind() != Kind.ARGUMENT) continue;
|
||||
+ // ECJ has not populated the implicit arguments of a compact record constructor yet.
|
||||
+ boolean compactConstructor = parent.get() instanceof ConstructorDeclaration
|
||||
+ && isRecord(job.parentType) && (parent.get().bits & IsCanonicalConstructor) != 0;
|
||||
+ for (EclipseNode param : (compactConstructor ? job.parentType : fillParametersFrom).down()) {
|
||||
+ if (compactConstructor ? !isRecordField(param) : param.getKind() != Kind.ARGUMENT) continue;
|
||||
BuilderFieldData bfd = new BuilderFieldData();
|
||||
- Argument arg = (Argument) param.get();
|
||||
+ AbstractVariableDeclaration arg = (AbstractVariableDeclaration) param.get();
|
||||
|
||||
Annotation[] copyableAnnotations = findCopyableAnnotations(param);
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import lombok.Builder;
|
||||
import lombok.NonNull;
|
||||
import lombok.Singular;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
public class BuilderCompactConstructor {
|
||||
@Builder(toBuilder = true)
|
||||
record Payment(String number, String phone, BigDecimal amount,
|
||||
BigDecimal serviceCharge, BigDecimal total, Status status) {
|
||||
@Builder
|
||||
Payment {
|
||||
if (serviceCharge == null) serviceCharge = BigDecimal.ZERO;
|
||||
}
|
||||
}
|
||||
|
||||
enum Status { PENDING, SUCCESS, EXPIRED }
|
||||
|
||||
record ConstructorOnly(String name, int count) {
|
||||
@Builder(toBuilder = true, setterPrefix = "with")
|
||||
ConstructorOnly {
|
||||
if (count < 0) throw new IllegalArgumentException("count");
|
||||
}
|
||||
}
|
||||
|
||||
@Builder(toBuilder = true)
|
||||
record TypeOnly(int count) {
|
||||
TypeOnly { count++; }
|
||||
}
|
||||
|
||||
record Generic<T>(@NonNull T value, @Singular List<String> tags) {
|
||||
static final String IGNORED = "not a component";
|
||||
|
||||
@Builder(toBuilder = true)
|
||||
Generic {}
|
||||
}
|
||||
|
||||
record Empty() {
|
||||
@Builder
|
||||
Empty {}
|
||||
}
|
||||
|
||||
record Canonical(String name, int count) {
|
||||
@Builder
|
||||
Canonical(String name, int count) {
|
||||
this.name = name.trim();
|
||||
this.count = count;
|
||||
}
|
||||
}
|
||||
|
||||
record Explicit(String name, int count) {
|
||||
@Builder
|
||||
Explicit(String name) { this(name, 7); }
|
||||
}
|
||||
|
||||
static class Ordinary {
|
||||
final String name;
|
||||
|
||||
@Builder
|
||||
@SneakyThrows
|
||||
Ordinary(String name) { this.name = name; }
|
||||
}
|
||||
|
||||
static class Parent {
|
||||
Parent(String name) {}
|
||||
}
|
||||
|
||||
static class ConstructorCalls extends Parent {
|
||||
final String name;
|
||||
|
||||
@SneakyThrows
|
||||
ConstructorCalls() {
|
||||
this("default");
|
||||
assert name.equals("default");
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
ConstructorCalls(@NonNull String name) {
|
||||
super(name);
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
static class GeneratedConstructor {
|
||||
final int initialized = 7;
|
||||
@NonNull final String name;
|
||||
}
|
||||
|
||||
@Builder(builderMethodName = "methodBuilder", builderClassName = "MethodBuilder")
|
||||
static String named(String name) { return name; }
|
||||
|
||||
public static void main(String[] args) {
|
||||
var payment = Payment.builder().number("inquiry").phone("123")
|
||||
.amount(BigDecimal.TEN).total(BigDecimal.TEN).status(Status.PENDING).build();
|
||||
assert payment.number().equals("inquiry");
|
||||
assert payment.phone().equals("123");
|
||||
assert payment.amount().equals(BigDecimal.TEN);
|
||||
assert payment.serviceCharge().equals(BigDecimal.ZERO);
|
||||
assert payment.total().equals(BigDecimal.TEN);
|
||||
assert payment.status() == Status.PENDING;
|
||||
assert payment.toBuilder().phone("456").build().phone().equals("456");
|
||||
assert Payment.builder().serviceCharge(BigDecimal.ONE).build().serviceCharge().equals(BigDecimal.ONE);
|
||||
|
||||
var constructor = ConstructorOnly.builder().withName("test").withCount(2).build();
|
||||
assert constructor.toBuilder().withCount(3).build().count() == 3;
|
||||
assert constructor.name().equals("test");
|
||||
try {
|
||||
ConstructorOnly.builder().withCount(-1).build();
|
||||
throw new AssertionError("compact constructor validation was bypassed");
|
||||
} catch (IllegalArgumentException expected) {}
|
||||
|
||||
assert TypeOnly.builder().count(2).build().count() == 3;
|
||||
assert Empty.builder().build().equals(new Empty());
|
||||
var generic = Generic.<String>builder().value("value").tag("one").build();
|
||||
assert generic.value().equals("value");
|
||||
assert generic.tags().equals(List.of("one"));
|
||||
assert generic.toBuilder().tag("two").build().tags().equals(List.of("one", "two"));
|
||||
try {
|
||||
Generic.<String>builder().build();
|
||||
throw new AssertionError("non-null check was bypassed");
|
||||
} catch (NullPointerException expected) {}
|
||||
|
||||
assert Canonical.builder().name(" test ").count(2).build().equals(new Canonical("test", 2));
|
||||
assert Explicit.builder().name("test").build().equals(new Explicit("test", 7));
|
||||
assert Ordinary.builder().name("test").build().name.equals("test");
|
||||
assert methodBuilder().name("test").build().equals("test");
|
||||
assert new ConstructorCalls().name.equals("default");
|
||||
assert new ConstructorCalls("explicit").name.equals("explicit");
|
||||
assert new GeneratedConstructor("generated").initialized == 7;
|
||||
assert new GeneratedConstructor("generated").name.equals("generated");
|
||||
try {
|
||||
new ConstructorCalls(null);
|
||||
throw new AssertionError("constructor non-null check was bypassed");
|
||||
} catch (NullPointerException expected) {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Compile and run builder regressions with javac and the compiler shipped by JDT LS.
|
||||
|
||||
Usage: python3 java/lombok/tests/verify.py /path/to/jdtls /path/to/lombok.jar
|
||||
Requires a JDK 21+ on PATH. All generated files stay in a temporary directory.
|
||||
"""
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
def main():
|
||||
home, lombok = (pathlib.Path(p).resolve() for p in sys.argv[1:3])
|
||||
ecj, = (home / "plugins").glob("org.eclipse.jdt.core.compiler.batch_*.jar")
|
||||
source = pathlib.Path(__file__).with_name("BuilderCompactConstructor.java")
|
||||
with tempfile.TemporaryDirectory(prefix="openidea-lombok-builders-") as tmp:
|
||||
for compiler in ("javac", "ecj"):
|
||||
output = pathlib.Path(tmp) / compiler
|
||||
output.mkdir()
|
||||
if compiler == "javac":
|
||||
command = ["javac", "--release", "21", "-processorpath", str(lombok)]
|
||||
else:
|
||||
command = ["java", f"-javaagent:{lombok}", "-jar", str(ecj), "-21"]
|
||||
subprocess.run(command + ["-cp", str(lombok), "-d", str(output), str(source)], check=True)
|
||||
subprocess.run(["java", "-ea", "-cp", os.pathsep.join((str(output), str(lombok))),
|
||||
"BuilderCompactConstructor"], check=True)
|
||||
print(f"{compiler}: builder regression assertions passed", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user