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.} to complete without a {@link NoClassDefFoundError}. + *

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 utf8Entries = readConstantPoolUtf8Entries(classUrl); + + // The method name "isGenerated" must appear (once, shared by all overloads). + assertThat(utf8Entries).contains("isGenerated"); + + // The two critical ECJ method descriptors that lombok transplants into ECJ bytecode + // must be present so the JVM can link the invokestatic call sites at runtime. + // (The constant pool stores full descriptors, not bare internal class names, when + // a type only appears in method signatures rather than in new/checkcast instructions.) + assertThat(utf8Entries).contains("(Lorg/eclipse/jdt/internal/compiler/ast/ASTNode;)Z"); + assertThat(utf8Entries).contains("(Lorg/eclipse/jdt/core/dom/ASTNode;)Z"); + assertThat(utf8Entries).contains("(Lorg/eclipse/jdt/core/IMember;)Z"); + } + + /** + * Reads all {@code CONSTANT_Utf8} entries from a class file's constant pool. + * Parses only the constant pool (stops before the class declaration), so it + * works on any valid class file without needing any external library. + */ + private static List readConstantPoolUtf8Entries(URL classUrl) throws Exception { + List entries = new ArrayList<>(); + try (InputStream raw = classUrl.openStream(); + DataInputStream in = new DataInputStream(raw)) { + // magic (4) + minor_version (2) + major_version (2) + in.skipBytes(8); + int cpCount = in.readUnsignedShort(); // constant_pool_count + for (int i = 1; i < cpCount; i++) { + int tag = in.readUnsignedByte(); + switch (tag) { + case 1: // CONSTANT_Utf8 + entries.add(in.readUTF()); + break; + case 3: // CONSTANT_Integer + case 4: // CONSTANT_Float + in.skipBytes(4); + break; + case 5: // CONSTANT_Long + case 6: // CONSTANT_Double + in.skipBytes(8); + i++; // long/double take two slots + break; + case 7: // CONSTANT_Class + case 8: // CONSTANT_String + case 16: // CONSTANT_MethodType + case 19: // CONSTANT_Module + case 20: // CONSTANT_Package + in.skipBytes(2); + break; + case 9: // CONSTANT_Fieldref + case 10: // CONSTANT_Methodref + case 11: // CONSTANT_InterfaceMethodref + case 12: // CONSTANT_NameAndType + case 17: // CONSTANT_Dynamic + case 18: // CONSTANT_InvokeDynamic + in.skipBytes(4); + break; + case 15: // CONSTANT_MethodHandle + in.skipBytes(3); + break; + default: + throw new IllegalStateException("Unknown constant pool tag " + tag + " at index " + i); + } + } + } + return entries; + } + + // ------------------------------------------------------------------------- + // lombok.eclipse.agent stubs + // ------------------------------------------------------------------------- + + @Test + void patchDiagnostics_setSourceRangeCheckIsCallable() throws Exception { + Class clazz = loader.loadClass("lombok.eclipse.agent.PatchDiagnostics"); + Method m = clazz.getMethod("setSourceRangeCheck", Object.class, int.class, int.class); + assertThat(Modifier.isStatic(m.getModifiers())).isTrue(); + // Must return false so ECJ applies its normal source-range logic. + assertThat(m.invoke(null, new Object(), 0, 10)).isEqualTo(false); + } + + @Test + void patchValEclipsePortal_copyInitializationMethodsExist() throws Exception { + Class clazz = loader.loadClass("lombok.eclipse.agent.PatchValEclipsePortal"); + Method m = clazz.getMethod("copyInitializationOfForEachIterable", Object.class); + assertThat(Modifier.isStatic(m.getModifiers())).isTrue(); + assertThatNoException().isThrownBy(() -> m.invoke(null, new Object())); + } + + @Test + void patchDelegatePortal_handleDelegateForTypeReturnsFalse() throws Exception { + Class clazz = loader.loadClass("lombok.eclipse.agent.PatchDelegatePortal"); + Method m = clazz.getMethod("handleDelegateForType", Object.class); + assertThat(Modifier.isStatic(m.getModifiers())).isTrue(); + assertThat(m.invoke(null, new Object())).isEqualTo(false); + } + + @Test + void patchFixesShadowLoaded_addLombokNotesReturnsOriginal() throws Exception { + Class clazz = loader.loadClass("lombok.eclipse.agent.PatchFixesShadowLoaded"); + Method m = clazz.getMethod("addLombokNotesToEclipseAboutDialog", String.class, String.class); + assertThat(Modifier.isStatic(m.getModifiers())).isTrue(); + assertThat(m.invoke(null, "original", "key")).isEqualTo("original"); + } + + @Test + void patchJavadoc_getHTMLContentReturnsOriginal() throws Exception { + Class clazz = loader.loadClass("lombok.eclipse.agent.PatchJavadoc"); + Method m = clazz.getMethod("getHTMLContentFromSource", Object.class, String.class, Object.class); + assertThat(Modifier.isStatic(m.getModifiers())).isTrue(); + assertThat(m.invoke(null, null, "original", null)).isEqualTo("original"); + } + + @Test + void eclipseLoaderPatcherTransplants_overrideLoadDecideReturnsFalse() throws Exception { + Class clazz = loader.loadClass("lombok.eclipse.agent.EclipseLoaderPatcherTransplants"); + Method m = clazz.getMethod("overrideLoadDecide", ClassLoader.class, String.class, boolean.class); + assertThat(Modifier.isStatic(m.getModifiers())).isTrue(); + assertThat(m.invoke(null, getClass().getClassLoader(), "some.Class", false)).isEqualTo(false); } // ------------------------------------------------------------------------- @@ -124,10 +256,10 @@ void patchFixesHider_patchFixesCanBeLoaded() throws ClassNotFoundException { @Test void unknownLombokClass_isSynthesisedAsEmptyClass() throws ClassNotFoundException { - // PatchDiagnostics has no hand-written stub; the loader must synthesise one. - Class clazz = loader.loadClass("lombok.eclipse.agent.PatchDiagnostics"); + // EclipsePatcher has no hand-written stub; the loader must synthesise one. + Class clazz = loader.loadClass("lombok.eclipse.agent.EclipsePatcher"); assertThat(clazz).isNotNull(); - assertThat(clazz.getName()).isEqualTo("lombok.eclipse.agent.PatchDiagnostics"); + assertThat(clazz.getName()).isEqualTo("lombok.eclipse.agent.EclipsePatcher"); // Synthesised class extends Object and declares no methods beyond . assertThat(clazz.getSuperclass()).isEqualTo(Object.class); assertThat(clazz.getDeclaredMethods()).isEmpty(); @@ -135,7 +267,7 @@ void unknownLombokClass_isSynthesisedAsEmptyClass() throws ClassNotFoundExceptio @Test void unknownLombokClass_canBeInstantiated() throws Exception { - Class clazz = loader.loadClass("lombok.eclipse.agent.PatchDiagnostics"); + Class clazz = loader.loadClass("lombok.eclipse.agent.EclipsePatcher"); // The synthesised class has a default public ; instantiation must succeed. assertThatNoException().isThrownBy(() -> clazz.getDeclaredConstructor().newInstance()); } diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index 6c8cff0b56..c46f5a4e10 100644 --- a/plugin-gradle/CHANGES.md +++ b/plugin-gradle/CHANGES.md @@ -21,6 +21,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( - `versionCatalog()` 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)) - `versionCatalog()` 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)) - Stop calling deprecated `Configuration.setVisible` from Gradle 9.0.0 ([#3053](https://github.com/diffplug/spotless/pull/3053)) +- 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)) ## [8.10.2] - 2026-09-04 ### Fixed diff --git a/plugin-maven/CHANGES.md b/plugin-maven/CHANGES.md index f2f939dab7..4ac4819193 100644 --- a/plugin-maven/CHANGES.md +++ b/plugin-maven/CHANGES.md @@ -20,6 +20,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( - `` preserves standalone comments at section boundaries and the end of the file. ([#3048](https://github.com/diffplug/spotless/issues/3048)) - `` 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)) - `` 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)) +- 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)) ## [3.10.2] - 2026-09-04 ### Fixed