Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ This document is intended for Spotless developers.
We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`).

## [Unreleased]
### Fixed
- `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))

## [4.10.1] - 2026-08-27
### Fixed
Expand Down
1 change: 1 addition & 0 deletions lib/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ def NEEDS_GLUE = [
'javaParser',
'ktfmt',
'ktlint',
'lombokStubs',
'palantirJavaFormat',
'princeOfSpace',
'scalafmt',
Expand Down
96 changes: 96 additions & 0 deletions lib/src/lombokStubs/java/lombok/core/FieldAugment.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* 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.core;

/**
* Stub implementation of {@code lombok.core.FieldAugment} used only within the
* {@code FeatureClassLoader} isolation boundary. The real FieldAugment is
* loaded by the lombok java-agent and is never reachable from Spotless's
* feature class-loader; this stub satisfies the static references that ECJ's
* patched classes make when lombok is active as a JVM agent.
*
* <p>{@link #augment} returns a no-op instance rather than {@code null} so that
* callers such as {@code EcjAugments} can safely call {@code .get()},
* {@code .set()}, etc. on the returned object without a {@link NullPointerException}.
*/
@SuppressWarnings("unused")
public abstract class FieldAugment<T, F> {

/** Returns a non-null no-op augment so callers can safely invoke instance methods on it. */
@SuppressWarnings("unchecked")
public static <T, F> FieldAugment<T, F> augment(Class<T> type, Class<? super F> fieldType, String name) {
return (FieldAugment<T, F>) NoopFieldAugment.INSTANCE;
}

/** Returns a non-null no-op augment so callers can safely invoke instance methods on it. */
@SuppressWarnings("unchecked")
public static <T, F> FieldAugment<T, F> circularSafeAugment(Class<T> type, Class<? super F> fieldType, String name) {
return (FieldAugment<T, F>) NoopFieldAugment.INSTANCE;
}

public abstract F get(T object);

public abstract void set(T object, F value);

public abstract F getAndSet(T object, F value);

public abstract F clear(T object);

public abstract F compareAndClear(T object, F expected);

public abstract F setIfAbsent(T object, F value);

public abstract F compareAndSet(T object, F expected, F value);

/** Singleton no-op implementation returned by {@link #augment} and {@link #circularSafeAugment}. */
@SuppressWarnings("rawtypes")
private static final class NoopFieldAugment extends FieldAugment {
static final NoopFieldAugment INSTANCE = new NoopFieldAugment();

@Override
public Object get(Object object) {
return null;
}

@Override
public void set(Object object, Object value) {}

@Override
public Object getAndSet(Object object, Object value) {
return null;
}

@Override
public Object clear(Object object) {
return null;
}

@Override
public Object compareAndClear(Object object, Object expected) {
return null;
}

@Override
public Object setIfAbsent(Object object, Object value) {
return null;
}

@Override
public Object compareAndSet(Object object, Object expected, Object value) {
return null;
}
}
}
51 changes: 51 additions & 0 deletions lib/src/lombokStubs/java/lombok/eclipse/EcjAugments.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* 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;

import lombok.core.FieldAugment;

/**
* Stub implementation of {@code lombok.eclipse.EcjAugments} used only within
* the {@code FeatureClassLoader} isolation boundary.
*
* <p>Each field is initialised via {@link FieldAugment#augment} so that callers
* (such as ECJ's patched {@code ASTConverter}) can safely invoke instance
* methods like {@code .get()} and {@code .set()} on them without a
* {@link NullPointerException}. The augment instances are no-ops that always
* return {@code null}.
*/
@SuppressWarnings({"unused", "rawtypes"})
public final class EcjAugments {

private EcjAugments() {
// prevent instantiation
}

public static final FieldAugment ASTNode_generatedBy = FieldAugment.augment(Object.class, Object.class, "$generatedBy");
public static final FieldAugment ASTNode_handled = FieldAugment.augment(Object.class, boolean.class, "lombok$handled");
public static final FieldAugment ASTNode_tokens = FieldAugment.augment(Object.class, Object.class, "lombok$tokens");
public static final FieldAugment FieldDeclaration_booleanLazyGetter = FieldAugment.augment(Object.class, boolean.class, "lombok$booleanLazyGetter");
public static final FieldAugment Annotation_applied = FieldAugment.augment(Object.class, boolean.class, "lombok$applied");
public static final FieldAugment CompilationUnit_javadoc = FieldAugment.augment(Object.class, Object.class, "$javadoc");
public static final FieldAugment CompilationUnitDeclaration_transformationState = FieldAugment.augment(Object.class, Object.class, "$transformationState");

/** Stub inner class mirroring {@code EcjAugments.EclipseAugments}. */
public static final class EclipseAugments {
private EclipseAugments() {}

public static final FieldAugment CompilationUnit_delegateMethods = FieldAugment.augment(Object.class, Object.class, "$delegateMethods");
}
}
212 changes: 212 additions & 0 deletions lib/src/lombokStubs/java/lombok/launch/PatchFixesHider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
/*
* 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.launch;

/**
* Stub implementation of {@code lombok.launch.PatchFixesHider} used only
* within the {@code FeatureClassLoader} isolation boundary.
*
* <p>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.<clinit>} to complete without a {@link NoClassDefFoundError}.
*
* <p>Every method in every inner class is a no-op stub. No real formatting
* logic lives here.
*/
@SuppressWarnings("unused")
final class PatchFixesHider {

private PatchFixesHider() {}

/** Stub for {@code PatchFixesHider.ModuleClassLoading}. */
public static final class ModuleClassLoading {
private ModuleClassLoading() {}

/** Stub – performs no class-loader manipulation. */
public static void parserClinit() {
// no-op stub
}
}

/** 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 – performs no AST transformation. */
public static void transform_swapped(Object ast, Object parser) {
// no-op stub
}
}

/** Stub for {@code PatchFixesHider.PatchFixes}. */
public static final class PatchFixes {
private PatchFixes() {}

/** Stub – always returns {@code false}. */
public static boolean isGenerated(Object node) {
return false;
}

/** Stub – always returns {@code false}. */
public static boolean returnFalse(Object object) {
return false;
}

/** Stub – always returns {@code true}. */
public static boolean returnTrue(Object object) {
return true;
}

/** Stub – always returns {@code false}. */
public static boolean isBlockedVisitorAndGenerated(Object node, Object visitor) {
return false;
}

/** Stub – returns 0-length array. */
public static Object[] listRewriteHandleGeneratedMethods(Object rewriteEvent) {
return new Object[0];
}

/** Stub – returns {@code sourceEnd} unchanged. */
public static int getSourceEndFixed(int sourceEnd, Object node) {
return sourceEnd;
}

/** Stub – returns {@code original} unchanged. */
public static int fixRetrieveStartingCatchPosition(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 for {@code PatchFixesHider.ValPortal}. */
public static final class ValPortal {
private ValPortal() {}

/** Stub – no-op. */
public static void copyInitializationOfForEachIterable(Object parser) {}

/** Stub – no-op. */
public static void copyInitializationOfLocalDeclaration(Object parser) {}

/** Stub – no-op. */
public static void addFinalAndValAnnotationToVariableDeclarationStatement(Object converter, Object out, Object in) {}

/** Stub – no-op. */
public static void addFinalAndValAnnotationToSingleVariableDeclaration(Object converter, Object out, Object in) {}
}

/** Stub for {@code PatchFixesHider.Val}. */
public static final class Val {
private Val() {}

/** Stub – always returns {@code false}. */
public static boolean handleValForLocalDeclaration(Object local, Object scope) {
return false;
}

/** Stub – always returns {@code false}. */
public static boolean handleValForForEach(Object forEach, Object scope) {
return false;
}
}

/** Stub for {@code PatchFixesHider.ExtensionMethod}. */
public static final class ExtensionMethod {
private ExtensionMethod() {}

/** Stub – returns {@code resolvedType} unchanged. */
public static Object resolveType(Object resolvedType, Object methodCall, Object scope) {
return resolvedType;
}

/** Stub – no-op. */
public static void errorNoMethodFor(Object problemReporter, Object messageSend, Object recType, Object params) {}

/** Stub – no-op. */
public static void invalidMethod(Object problemReporter, Object messageSend, Object method) {}

/** Stub – no-op. */
public static void invalidMethod(Object problemReporter, Object messageSend, Object method, Object scope) {}

/** Stub – no-op. */
public static void nonStaticAccessToStaticMethod(Object problemReporter, Object location, Object method, Object messageSend) {}

/** Stub – returns {@code original} unchanged. */
public static Object modifyMethodPattern(Object original) {
return original;
}
}

/** Stub for {@code PatchFixesHider.Delegate}. */
public static final class Delegate {
private Delegate() {}

/** 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];
}

/** Stub – always returns {@code false}. */
public static boolean isDelegateSourceMethod(Object sourceMethod) {
return false;
}

/** Stub – always returns {@code null}. */
public static Object returnElementInfo(Object delegateSourceMethod) {
return null;
}
}

/** Stub for {@code PatchFixesHider.Util}. */
public static final class Util {
private Util() {}
}

/** Stub for {@code PatchFixesHider.LombokDeps}. */
public static final class LombokDeps {
private LombokDeps() {}
}

/** Stub for {@code PatchFixesHider.Javadoc}. */
public static final class Javadoc {
private Javadoc() {}

/** Stub – returns {@code original} unchanged. */
public static String getHTMLContentFromSource(String original, Object member) {
return original;
}
}
}
Loading
Loading