Skip to content

Commit 9591d7e

Browse files
authored
Resolve interopability with spotless, lombok and VSCode (#3038)
2 parents 4ea1c6c + e7f5b60 commit 9591d7e

10 files changed

Lines changed: 733 additions & 1 deletion

File tree

CHANGES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ This document is intended for Spotless developers.
1010
We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`).
1111

1212
## [Unreleased]
13+
### Fixed
14+
- `FeatureClassLoader` now provides stub implementations of `lombok.*` classes (and synthesises empty classes for any others) so that the Eclipse JDT formatter step no longer fails with `NoClassDefFoundError` 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))
1315

1416
## [4.10.1] - 2026-08-27
1517
### Fixed

lib/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ def NEEDS_GLUE = [
2121
'javaParser',
2222
'ktfmt',
2323
'ktlint',
24+
'lombokStubs',
2425
'palantirJavaFormat',
2526
'princeOfSpace',
2627
'scalafmt',
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* Copyright 2026 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package lombok.core;
17+
18+
/**
19+
* Stub implementation of {@code lombok.core.FieldAugment} used only within the
20+
* {@code FeatureClassLoader} isolation boundary. The real FieldAugment is
21+
* loaded by the lombok java-agent and is never reachable from Spotless's
22+
* feature class-loader; this stub satisfies the static references that ECJ's
23+
* patched classes make when lombok is active as a JVM agent.
24+
*
25+
* <p>{@link #augment} returns a no-op instance rather than {@code null} so that
26+
* callers such as {@code EcjAugments} can safely call {@code .get()},
27+
* {@code .set()}, etc. on the returned object without a {@link NullPointerException}.
28+
*/
29+
@SuppressWarnings("unused")
30+
public abstract class FieldAugment<T, F> {
31+
32+
/** Returns a non-null no-op augment so callers can safely invoke instance methods on it. */
33+
@SuppressWarnings("unchecked")
34+
public static <T, F> FieldAugment<T, F> augment(Class<T> type, Class<? super F> fieldType, String name) {
35+
return (FieldAugment<T, F>) NoopFieldAugment.INSTANCE;
36+
}
37+
38+
/** Returns a non-null no-op augment so callers can safely invoke instance methods on it. */
39+
@SuppressWarnings("unchecked")
40+
public static <T, F> FieldAugment<T, F> circularSafeAugment(Class<T> type, Class<? super F> fieldType, String name) {
41+
return (FieldAugment<T, F>) NoopFieldAugment.INSTANCE;
42+
}
43+
44+
public abstract F get(T object);
45+
46+
public abstract void set(T object, F value);
47+
48+
public abstract F getAndSet(T object, F value);
49+
50+
public abstract F clear(T object);
51+
52+
public abstract F compareAndClear(T object, F expected);
53+
54+
public abstract F setIfAbsent(T object, F value);
55+
56+
public abstract F compareAndSet(T object, F expected, F value);
57+
58+
/** Singleton no-op implementation returned by {@link #augment} and {@link #circularSafeAugment}. */
59+
@SuppressWarnings("rawtypes")
60+
private static final class NoopFieldAugment extends FieldAugment {
61+
static final NoopFieldAugment INSTANCE = new NoopFieldAugment();
62+
63+
@Override
64+
public Object get(Object object) {
65+
return null;
66+
}
67+
68+
@Override
69+
public void set(Object object, Object value) {}
70+
71+
@Override
72+
public Object getAndSet(Object object, Object value) {
73+
return null;
74+
}
75+
76+
@Override
77+
public Object clear(Object object) {
78+
return null;
79+
}
80+
81+
@Override
82+
public Object compareAndClear(Object object, Object expected) {
83+
return null;
84+
}
85+
86+
@Override
87+
public Object setIfAbsent(Object object, Object value) {
88+
return null;
89+
}
90+
91+
@Override
92+
public Object compareAndSet(Object object, Object expected, Object value) {
93+
return null;
94+
}
95+
}
96+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright 2026 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package lombok.eclipse;
17+
18+
import lombok.core.FieldAugment;
19+
20+
/**
21+
* Stub implementation of {@code lombok.eclipse.EcjAugments} used only within
22+
* the {@code FeatureClassLoader} isolation boundary.
23+
*
24+
* <p>Each field is initialised via {@link FieldAugment#augment} so that callers
25+
* (such as ECJ's patched {@code ASTConverter}) can safely invoke instance
26+
* methods like {@code .get()} and {@code .set()} on them without a
27+
* {@link NullPointerException}. The augment instances are no-ops that always
28+
* return {@code null}.
29+
*/
30+
@SuppressWarnings({"unused", "rawtypes"})
31+
public final class EcjAugments {
32+
33+
private EcjAugments() {
34+
// prevent instantiation
35+
}
36+
37+
public static final FieldAugment ASTNode_generatedBy = FieldAugment.augment(Object.class, Object.class, "$generatedBy");
38+
public static final FieldAugment ASTNode_handled = FieldAugment.augment(Object.class, boolean.class, "lombok$handled");
39+
public static final FieldAugment ASTNode_tokens = FieldAugment.augment(Object.class, Object.class, "lombok$tokens");
40+
public static final FieldAugment FieldDeclaration_booleanLazyGetter = FieldAugment.augment(Object.class, boolean.class, "lombok$booleanLazyGetter");
41+
public static final FieldAugment Annotation_applied = FieldAugment.augment(Object.class, boolean.class, "lombok$applied");
42+
public static final FieldAugment CompilationUnit_javadoc = FieldAugment.augment(Object.class, Object.class, "$javadoc");
43+
public static final FieldAugment CompilationUnitDeclaration_transformationState = FieldAugment.augment(Object.class, Object.class, "$transformationState");
44+
45+
/** Stub inner class mirroring {@code EcjAugments.EclipseAugments}. */
46+
public static final class EclipseAugments {
47+
private EclipseAugments() {}
48+
49+
public static final FieldAugment CompilationUnit_delegateMethods = FieldAugment.augment(Object.class, Object.class, "$delegateMethods");
50+
}
51+
}
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
/*
2+
* Copyright 2026 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package lombok.launch;
17+
18+
/**
19+
* Stub implementation of {@code lombok.launch.PatchFixesHider} used only
20+
* within the {@code FeatureClassLoader} isolation boundary.
21+
*
22+
* <p>When lombok is loaded as a JVM agent (e.g. {@code -javaagent:lombok.jar}),
23+
* it patches ECJ's {@code Parser} class so that its static initializer
24+
* references inner classes of {@code PatchFixesHider} such as
25+
* {@code ModuleClassLoading} and {@code Transform}. Spotless's
26+
* {@code FeatureClassLoader} isolates formatter JARs from the build-tool
27+
* class-loader, so it cannot see the real {@code PatchFixesHider} that was
28+
* injected by the agent. Loading this stub instead allows ECJ's
29+
* {@code Parser.<clinit>} to complete without a {@link NoClassDefFoundError}.
30+
*
31+
* <p>Every method in every inner class is a no-op stub. No real formatting
32+
* logic lives here.
33+
*/
34+
@SuppressWarnings("unused")
35+
final class PatchFixesHider {
36+
37+
private PatchFixesHider() {}
38+
39+
/** Stub for {@code PatchFixesHider.ModuleClassLoading}. */
40+
public static final class ModuleClassLoading {
41+
private ModuleClassLoading() {}
42+
43+
/** Stub – performs no class-loader manipulation. */
44+
public static void parserClinit() {
45+
// no-op stub
46+
}
47+
}
48+
49+
/** Stub for {@code PatchFixesHider.Transform}. */
50+
public static final class Transform {
51+
private Transform() {}
52+
53+
/** Stub – performs no AST transformation. */
54+
public static void transform(Object parser, Object ast) {
55+
// no-op stub
56+
}
57+
58+
/** Stub – performs no AST transformation. */
59+
public static void transform_swapped(Object ast, Object parser) {
60+
// no-op stub
61+
}
62+
}
63+
64+
/** Stub for {@code PatchFixesHider.PatchFixes}. */
65+
public static final class PatchFixes {
66+
private PatchFixes() {}
67+
68+
/** Stub – always returns {@code false}. */
69+
public static boolean isGenerated(Object node) {
70+
return false;
71+
}
72+
73+
/** Stub – always returns {@code false}. */
74+
public static boolean returnFalse(Object object) {
75+
return false;
76+
}
77+
78+
/** Stub – always returns {@code true}. */
79+
public static boolean returnTrue(Object object) {
80+
return true;
81+
}
82+
83+
/** Stub – always returns {@code false}. */
84+
public static boolean isBlockedVisitorAndGenerated(Object node, Object visitor) {
85+
return false;
86+
}
87+
88+
/** Stub – returns 0-length array. */
89+
public static Object[] listRewriteHandleGeneratedMethods(Object rewriteEvent) {
90+
return new Object[0];
91+
}
92+
93+
/** Stub – returns {@code sourceEnd} unchanged. */
94+
public static int getSourceEndFixed(int sourceEnd, Object node) {
95+
return sourceEnd;
96+
}
97+
98+
/** Stub – returns {@code original} unchanged. */
99+
public static int fixRetrieveStartingCatchPosition(int original, int start) {
100+
return original == -1 ? start : original;
101+
}
102+
103+
/** Stub – returns {@code original} unchanged. */
104+
public static int fixRetrieveRightBraceOrSemiColonPosition(int original, int end) {
105+
return original == -1 ? end : original;
106+
}
107+
}
108+
109+
/** Stub for {@code PatchFixesHider.ValPortal}. */
110+
public static final class ValPortal {
111+
private ValPortal() {}
112+
113+
/** Stub – no-op. */
114+
public static void copyInitializationOfForEachIterable(Object parser) {}
115+
116+
/** Stub – no-op. */
117+
public static void copyInitializationOfLocalDeclaration(Object parser) {}
118+
119+
/** Stub – no-op. */
120+
public static void addFinalAndValAnnotationToVariableDeclarationStatement(Object converter, Object out, Object in) {}
121+
122+
/** Stub – no-op. */
123+
public static void addFinalAndValAnnotationToSingleVariableDeclaration(Object converter, Object out, Object in) {}
124+
}
125+
126+
/** Stub for {@code PatchFixesHider.Val}. */
127+
public static final class Val {
128+
private Val() {}
129+
130+
/** Stub – always returns {@code false}. */
131+
public static boolean handleValForLocalDeclaration(Object local, Object scope) {
132+
return false;
133+
}
134+
135+
/** Stub – always returns {@code false}. */
136+
public static boolean handleValForForEach(Object forEach, Object scope) {
137+
return false;
138+
}
139+
}
140+
141+
/** Stub for {@code PatchFixesHider.ExtensionMethod}. */
142+
public static final class ExtensionMethod {
143+
private ExtensionMethod() {}
144+
145+
/** Stub – returns {@code resolvedType} unchanged. */
146+
public static Object resolveType(Object resolvedType, Object methodCall, Object scope) {
147+
return resolvedType;
148+
}
149+
150+
/** Stub – no-op. */
151+
public static void errorNoMethodFor(Object problemReporter, Object messageSend, Object recType, Object params) {}
152+
153+
/** Stub – no-op. */
154+
public static void invalidMethod(Object problemReporter, Object messageSend, Object method) {}
155+
156+
/** Stub – no-op. */
157+
public static void invalidMethod(Object problemReporter, Object messageSend, Object method, Object scope) {}
158+
159+
/** Stub – no-op. */
160+
public static void nonStaticAccessToStaticMethod(Object problemReporter, Object location, Object method, Object messageSend) {}
161+
162+
/** Stub – returns {@code original} unchanged. */
163+
public static Object modifyMethodPattern(Object original) {
164+
return original;
165+
}
166+
}
167+
168+
/** Stub for {@code PatchFixesHider.Delegate}. */
169+
public static final class Delegate {
170+
private Delegate() {}
171+
172+
/** Stub – always returns {@code false}. */
173+
public static boolean handleDelegateForType(Object classScope) {
174+
return false;
175+
}
176+
177+
/** Stub – returns an empty array. */
178+
public static Object[] addGeneratedDelegateMethods(Object returnValue, Object javaElement) {
179+
return new Object[0];
180+
}
181+
182+
/** Stub – always returns {@code false}. */
183+
public static boolean isDelegateSourceMethod(Object sourceMethod) {
184+
return false;
185+
}
186+
187+
/** Stub – always returns {@code null}. */
188+
public static Object returnElementInfo(Object delegateSourceMethod) {
189+
return null;
190+
}
191+
}
192+
193+
/** Stub for {@code PatchFixesHider.Util}. */
194+
public static final class Util {
195+
private Util() {}
196+
}
197+
198+
/** Stub for {@code PatchFixesHider.LombokDeps}. */
199+
public static final class LombokDeps {
200+
private LombokDeps() {}
201+
}
202+
203+
/** Stub for {@code PatchFixesHider.Javadoc}. */
204+
public static final class Javadoc {
205+
private Javadoc() {}
206+
207+
/** Stub – returns {@code original} unchanged. */
208+
public static String getHTMLContentFromSource(String original, Object member) {
209+
return original;
210+
}
211+
}
212+
}

0 commit comments

Comments
 (0)