diff --git a/CHANGES.md b/CHANGES.md index 1053725a3e..82855c1726 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -28,6 +28,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( - `VersionCatalogStep` preserves standalone comments at section boundaries and the end of the file. ([#3048](https://github.com/diffplug/spotless/issues/3048)) - `VersionCatalogStep` preserves entries when comments contain unmatched brackets, preserves commas inside quoted strings, and keeps significant line boundaries in multiline entries. ([#3042](https://github.com/diffplug/spotless/pull/3042)) - `VersionCatalogStep` now reports unfinished entries as lints at their starting line. These fail formatting by default, so upgrading may expose catalog errors that previously caused silent data loss. ([#3042](https://github.com/diffplug/spotless/pull/3042)) +- `FeatureClassLoader` now provides stub implementations of `lombok.*` classes (and synthesises empty classes for any unknown ones) so that the Eclipse JDT formatter step no longer fails with `NoClassDefFoundError` or `NoSuchMethodError` when lombok is active as a JVM agent (e.g. `-javaagent:lombok.jar` in Eclipse/VS Code/Cursor). ([#2795](https://github.com/diffplug/spotless/issues/2795)) ## [4.10.2] - 2026-09-04 ### Fixed diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index 2992c4a33f..1f9d601d9b 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -182,6 +182,9 @@ dependencies { "tableTestFormatterCompileOnly"(libs.tabletest.formatter.core) // zjsonPatch "zjsonPatchCompileOnly"(libs.zjsonpatch) + // lombokStubs – needs ECJ types to match the exact method descriptors that lombok + // transplants into ECJ's ASTConverter, ASTNode, etc. + "lombokStubsCompileOnly"("org.eclipse.jdt:org.eclipse.jdt.core:3.46.0") } // we'll hold the core lib to a high standard diff --git a/lib/src/lombokStubs/java/lombok/eclipse/agent/EclipseLoaderPatcherTransplants.java b/lib/src/lombokStubs/java/lombok/eclipse/agent/EclipseLoaderPatcherTransplants.java new file mode 100644 index 0000000000..2e88cb3e4d --- /dev/null +++ b/lib/src/lombokStubs/java/lombok/eclipse/agent/EclipseLoaderPatcherTransplants.java @@ -0,0 +1,34 @@ +/* + * Copyright 2026 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package lombok.eclipse.agent; + +/** + * Stub implementation of {@code lombok.eclipse.agent.EclipseLoaderPatcherTransplants} + * used only within the {@code FeatureClassLoader} isolation boundary. + */ +@SuppressWarnings("unused") +public class EclipseLoaderPatcherTransplants { + + /** Stub – returns {@code false} so the normal class-loading path is used. */ + public static boolean overrideLoadDecide(ClassLoader classLoader, String name, boolean resolve) { + return false; + } + + /** Stub – returns {@code null} so the caller falls through to normal loading. */ + public static Class> overrideLoadResult(ClassLoader classLoader, String name, boolean resolve) throws ClassNotFoundException { + return null; + } +} diff --git a/lib/src/lombokStubs/java/lombok/eclipse/agent/PatchDelegatePortal.java b/lib/src/lombokStubs/java/lombok/eclipse/agent/PatchDelegatePortal.java new file mode 100644 index 0000000000..81c2830c2d --- /dev/null +++ b/lib/src/lombokStubs/java/lombok/eclipse/agent/PatchDelegatePortal.java @@ -0,0 +1,34 @@ +/* + * Copyright 2026 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package lombok.eclipse.agent; + +/** + * Stub implementation of {@code lombok.eclipse.agent.PatchDelegatePortal} + * used only within the {@code FeatureClassLoader} isolation boundary. + */ +@SuppressWarnings("unused") +public class PatchDelegatePortal { + + /** Stub – always returns {@code false}. */ + public static boolean handleDelegateForType(Object classScope) { + return false; + } + + /** Stub – returns an empty array. */ + public static Object[] addGeneratedDelegateMethods(Object returnValue, Object javaElement) { + return new Object[0]; + } +} diff --git a/lib/src/lombokStubs/java/lombok/eclipse/agent/PatchDiagnostics.java b/lib/src/lombokStubs/java/lombok/eclipse/agent/PatchDiagnostics.java new file mode 100644 index 0000000000..eace19397b --- /dev/null +++ b/lib/src/lombokStubs/java/lombok/eclipse/agent/PatchDiagnostics.java @@ -0,0 +1,38 @@ +/* + * Copyright 2026 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package lombok.eclipse.agent; + +/** + * Stub implementation of {@code lombok.eclipse.agent.PatchDiagnostics} used + * only within the {@code FeatureClassLoader} isolation boundary. + * + *
When lombok is active as a JVM agent it patches ECJ's + * {@code ASTNode.setSourceRange} to call + * {@code PatchDiagnostics.setSourceRangeCheck}. The real class lives in + * lombok's shadow class-loader and is never reachable from + * {@code FeatureClassLoader}. This stub satisfies the call site so that + * source-range setting proceeds without a {@link NoSuchMethodError}. + * Returning {@code false} means "no override" — ECJ proceeds with its + * normal source-range assignment. + */ +@SuppressWarnings("unused") +public class PatchDiagnostics { + + /** Stub – always returns {@code false} so ECJ applies its normal source-range logic. */ + public static boolean setSourceRangeCheck(Object node, int start, int end) { + return false; + } +} diff --git a/lib/src/lombokStubs/java/lombok/eclipse/agent/PatchFixesShadowLoaded.java b/lib/src/lombokStubs/java/lombok/eclipse/agent/PatchFixesShadowLoaded.java new file mode 100644 index 0000000000..2256b76ba9 --- /dev/null +++ b/lib/src/lombokStubs/java/lombok/eclipse/agent/PatchFixesShadowLoaded.java @@ -0,0 +1,49 @@ +/* + * Copyright 2026 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package lombok.eclipse.agent; + +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * Stub implementation of {@code lombok.eclipse.agent.PatchFixesShadowLoaded} + * used only within the {@code FeatureClassLoader} isolation boundary. + * Methods return their input unchanged; post-compiler steps are no-ops. + */ +@SuppressWarnings("unused") +public class PatchFixesShadowLoaded { + + /** Stub – returns {@code origReturnValue} unchanged. */ + public static String addLombokNotesToEclipseAboutDialog(String origReturnValue, String key) { + return origReturnValue; + } + + /** Stub – returns {@code bytes} unchanged. */ + public static byte[] runPostCompiler(byte[] bytes, String fileName) { + return bytes; + } + + /** Stub – returns {@code out} unchanged. */ + public static OutputStream runPostCompiler(OutputStream out) throws IOException { + return out; + } + + /** Stub – returns {@code out} unchanged. */ + public static BufferedOutputStream runPostCompiler(BufferedOutputStream out, String path, String name) throws IOException { + return out; + } +} diff --git a/lib/src/lombokStubs/java/lombok/eclipse/agent/PatchJavadoc.java b/lib/src/lombokStubs/java/lombok/eclipse/agent/PatchJavadoc.java new file mode 100644 index 0000000000..2179e22abe --- /dev/null +++ b/lib/src/lombokStubs/java/lombok/eclipse/agent/PatchJavadoc.java @@ -0,0 +1,29 @@ +/* + * Copyright 2026 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package lombok.eclipse.agent; + +/** + * Stub implementation of {@code lombok.eclipse.agent.PatchJavadoc} + * used only within the {@code FeatureClassLoader} isolation boundary. + */ +@SuppressWarnings("unused") +public class PatchJavadoc { + + /** Stub – returns {@code original} unchanged. */ + public static String getHTMLContentFromSource(Object instance, String original, Object member) { + return original; + } +} diff --git a/lib/src/lombokStubs/java/lombok/eclipse/agent/PatchValEclipsePortal.java b/lib/src/lombokStubs/java/lombok/eclipse/agent/PatchValEclipsePortal.java new file mode 100644 index 0000000000..c82d0b3749 --- /dev/null +++ b/lib/src/lombokStubs/java/lombok/eclipse/agent/PatchValEclipsePortal.java @@ -0,0 +1,33 @@ +/* + * Copyright 2026 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package lombok.eclipse.agent; + +/** + * Stub implementation of {@code lombok.eclipse.agent.PatchValEclipsePortal} + * used only within the {@code FeatureClassLoader} isolation boundary. + * All methods are no-ops. + */ +@SuppressWarnings("unused") +public class PatchValEclipsePortal { + + public static void copyInitializationOfForEachIterable(Object parser) {} + + public static void copyInitializationOfLocalDeclaration(Object parser) {} + + public static void addFinalAndValAnnotationToVariableDeclarationStatement(Object converter, Object out, Object in) {} + + public static void addFinalAndValAnnotationToSingleVariableDeclaration(Object converter, Object out, Object in) {} +} diff --git a/lib/src/lombokStubs/java/lombok/launch/PatchFixesHider.java b/lib/src/lombokStubs/java/lombok/launch/PatchFixesHider.java index 074be53f0e..00f63bb912 100644 --- a/lib/src/lombokStubs/java/lombok/launch/PatchFixesHider.java +++ b/lib/src/lombokStubs/java/lombok/launch/PatchFixesHider.java @@ -15,21 +15,27 @@ */ package lombok.launch; +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +import org.eclipse.jdt.core.dom.ASTVisitor; +import org.eclipse.jdt.internal.compiler.ast.ASTNode; + /** * Stub implementation of {@code lombok.launch.PatchFixesHider} used only * within the {@code FeatureClassLoader} isolation boundary. * - *
When lombok is loaded as a JVM agent (e.g. {@code -javaagent:lombok.jar}),
- * it patches ECJ's {@code Parser} class so that its static initializer
- * references inner classes of {@code PatchFixesHider} such as
- * {@code ModuleClassLoading} and {@code Transform}. Spotless's
- * {@code FeatureClassLoader} isolates formatter JARs from the build-tool
- * class-loader, so it cannot see the real {@code PatchFixesHider} that was
- * injected by the agent. Loading this stub instead allows ECJ's
- * {@code Parser. When lombok is active as a JVM agent it patches ECJ classes so that their
+ * methods call into inner classes of {@code PatchFixesHider}. The real class
+ * lives in lombok's shadow class-loader and is never reachable from
+ * {@code FeatureClassLoader}. This stub satisfies those call sites so that
+ * ECJ's patched initialisers can complete without a {@link NoClassDefFoundError}
+ * or {@link NoSuchMethodError}.
*
- * Every method in every inner class is a no-op stub. No real formatting
- * logic lives here.
+ * Method signatures must exactly match the transplanted descriptors that
+ * lombok injects into ECJ bytecode; hence several methods use concrete ECJ
+ * types rather than {@code Object}.
*/
@SuppressWarnings("unused")
final class PatchFixesHider {
@@ -40,58 +46,69 @@ private PatchFixesHider() {}
public static final class ModuleClassLoading {
private ModuleClassLoading() {}
- /** Stub – performs no class-loader manipulation. */
- public static void parserClinit() {
- // no-op stub
- }
+ /** Stub – no-op. */
+ public static void parserClinit() {}
}
/** Stub for {@code PatchFixesHider.Transform}. */
public static final class Transform {
private Transform() {}
- /** Stub – performs no AST transformation. */
- public static void transform(Object parser, Object ast) {
- // no-op stub
- }
+ /** Stub – no-op. */
+ public static void transform(Object parser, Object ast) throws IOException {}
- /** Stub – performs no AST transformation. */
- public static void transform_swapped(Object ast, Object parser) {
- // no-op stub
- }
+ /** Stub – no-op. */
+ public static void transform_swapped(Object ast, Object parser) throws IOException {}
}
/** Stub for {@code PatchFixesHider.PatchFixes}. */
public static final class PatchFixes {
private PatchFixes() {}
- /** Stub – always returns {@code false}. */
- public static boolean isGenerated(Object node) {
+ /**
+ * Stub – always returns {@code false}.
+ * Matches transplanted descriptor: {@code (Lorg/eclipse/jdt/core/dom/ASTNode;)Z}
+ */
+ public static boolean isGenerated(org.eclipse.jdt.core.dom.ASTNode node) {
return false;
}
- /** Stub – always returns {@code false}. */
- public static boolean returnFalse(Object object) {
+ /**
+ * Stub – always returns {@code false}.
+ * Matches transplanted descriptor: {@code (Lorg/eclipse/jdt/internal/compiler/ast/ASTNode;)Z}
+ */
+ public static boolean isGenerated(ASTNode node) {
return false;
}
- /** Stub – always returns {@code true}. */
- public static boolean returnTrue(Object object) {
- return true;
+ /**
+ * Stub – always returns {@code false}.
+ * Matches transplanted descriptor: {@code (Lorg/eclipse/jdt/core/IMember;)Z}
+ */
+ public static boolean isGenerated(org.eclipse.jdt.core.IMember member) {
+ return false;
+ }
+
+ /**
+ * Stub – always returns {@code false}.
+ * Matches transplanted descriptor for isBlockedVisitorAndGenerated.
+ */
+ public static boolean isBlockedVisitorAndGenerated(org.eclipse.jdt.core.dom.ASTNode node, ASTVisitor visitor) {
+ return false;
}
/** Stub – always returns {@code false}. */
- public static boolean isBlockedVisitorAndGenerated(Object node, Object visitor) {
+ public static boolean returnFalse(Object object) {
return false;
}
- /** Stub – returns 0-length array. */
- public static Object[] listRewriteHandleGeneratedMethods(Object rewriteEvent) {
- return new Object[0];
+ /** Stub – always returns {@code true}. */
+ public static boolean returnTrue(Object object) {
+ return true;
}
/** Stub – returns {@code sourceEnd} unchanged. */
- public static int getSourceEndFixed(int sourceEnd, Object node) {
+ public static int getSourceEndFixed(int sourceEnd, ASTNode node) throws Exception {
return sourceEnd;
}
@@ -100,10 +117,44 @@ public static int fixRetrieveStartingCatchPosition(int original, int start) {
return original == -1 ? start : original;
}
+ /** Stub – returns {@code original} unchanged. */
+ public static int fixRetrieveIdentifierEndPosition(int original, int start, int end) {
+ if (original == -1)
+ return end;
+ if (original < start)
+ return end;
+ return original;
+ }
+
+ /** Stub – returns {@code original} unchanged. */
+ public static int fixRetrieveEllipsisStartPosition(int original, int end) {
+ return original == -1 ? end : original;
+ }
+
+ /** Stub – returns {@code original} unchanged. */
+ public static int fixRetrieveStartBlockPosition(int original, int start) {
+ return original == -1 ? start : original;
+ }
+
/** Stub – returns {@code original} unchanged. */
public static int fixRetrieveRightBraceOrSemiColonPosition(int original, int end) {
return original == -1 ? end : original;
}
+
+ /** Stub – returns 0-length array. */
+ public static Object[] listRewriteHandleGeneratedMethods(Object rewriteEvent) {
+ return new Object[0];
+ }
+
+ /** Stub – returns {@code original} unchanged. */
+ public static String getRealNodeSource(String original, ASTNode node) {
+ return original;
+ }
+
+ /** Stub – returns {@code original} unchanged. */
+ public static String getRealNodeSource(String original, org.eclipse.jdt.core.dom.ASTNode node) throws Exception {
+ return original;
+ }
}
/** Stub for {@code PatchFixesHider.ValPortal}. */
@@ -198,6 +249,26 @@ private Util() {}
/** Stub for {@code PatchFixesHider.LombokDeps}. */
public static final class LombokDeps {
private LombokDeps() {}
+
+ /** Stub – returns {@code origReturnValue} unchanged. */
+ public static String addLombokNotesToEclipseAboutDialog(String origReturnValue, String key) {
+ return origReturnValue;
+ }
+
+ /** Stub – returns {@code bytes} unchanged. */
+ public static byte[] runPostCompiler(byte[] bytes, String fileName) {
+ return bytes;
+ }
+
+ /** Stub – returns {@code out} unchanged. */
+ public static OutputStream runPostCompiler(OutputStream out) throws IOException {
+ return out;
+ }
+
+ /** Stub – returns {@code out} unchanged. */
+ public static BufferedOutputStream runPostCompiler(BufferedOutputStream out, String path, String name) throws IOException {
+ return out;
+ }
}
/** Stub for {@code PatchFixesHider.Javadoc}. */
diff --git a/lib/src/test/java/com/diffplug/spotless/FeatureClassLoaderLombokStubsTest.java b/lib/src/test/java/com/diffplug/spotless/FeatureClassLoaderLombokStubsTest.java
index 3195be6905..ef28072886 100644
--- a/lib/src/test/java/com/diffplug/spotless/FeatureClassLoaderLombokStubsTest.java
+++ b/lib/src/test/java/com/diffplug/spotless/FeatureClassLoaderLombokStubsTest.java
@@ -18,23 +18,27 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatNoException;
+import java.io.DataInputStream;
+import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/**
* Verifies that {@link FeatureClassLoader} resolves the bundled lombok stubs
- * for the three class families that ECJ references when lombok is active as a
- * JVM agent.
+ * for the class families that ECJ references when lombok is active as a JVM
+ * agent, and that unknown {@code lombok.*} classes are synthesised on the fly.
*
* The tests instantiate a {@code FeatureClassLoader} with an empty URL array
* (no feature JARs) and confirm that each stubbed class can be loaded and has
- * the expected members so that ECJ's static initializers can complete without
- * a {@link NoClassDefFoundError}.
+ * the expected members so that ECJ's static initialisers can complete without
+ * a {@link NoClassDefFoundError} or {@link NoSuchMethodError}.
*/
class FeatureClassLoaderLombokStubsTest {
@@ -114,8 +118,136 @@ void patchFixesHider_transformCanBeLoaded() throws ClassNotFoundException {
}
@Test
- void patchFixesHider_patchFixesCanBeLoaded() throws ClassNotFoundException {
- assertThatNoException().isThrownBy(() -> loader.loadClass("lombok.launch.PatchFixesHider$PatchFixes"));
+ void patchFixesHider_patchFixes_isGeneratedOverloadsExist() throws Exception {
+ // We cannot use getDeclaredMethods() / getParameterTypes() here because the
+ // test JVM would try to resolve ECJ types (IMember, ASTNode, etc.) that are
+ // not on the test classpath, causing NoClassDefFoundError.
+ //
+ // Instead we read the raw .class bytes and parse the constant pool directly.
+ // The lombokStubs output directory is on the test classpath, so the resource
+ // is reachable via the test classloader — no FeatureClassLoader indirection needed.
+ String resourcePath = "lombok/launch/PatchFixesHider$PatchFixes.class";
+ URL classUrl = FeatureClassLoaderLombokStubsTest.class.getClassLoader().getResource(resourcePath);
+ assertThat(classUrl).as("PatchFixesHider$PatchFixes.class on test classpath").isNotNull();
+
+ List