From 469eae0770785c95cfee0ed331cf487655b4f421 Mon Sep 17 00:00:00 2001 From: screret <68943070+screret@users.noreply.github.com> Date: Thu, 12 Feb 2026 10:41:57 +0200 Subject: [PATCH 1/3] Implement a system for "default" `@ExtensionMethod` extensions --- src/core/lombok/ConfigurationKeys.java | 16 ++- .../handlers/HandleExtensionMethod.java | 82 ++++++++++----- .../javac/handlers/HandleExtensionMethod.java | 99 +++++++++++++------ .../javac/handlers/JavacHandlerUtil.java | 4 +- 4 files changed, 143 insertions(+), 58 deletions(-) diff --git a/src/core/lombok/ConfigurationKeys.java b/src/core/lombok/ConfigurationKeys.java index 997c550bed..043e912608 100644 --- a/src/core/lombok/ConfigurationKeys.java +++ b/src/core/lombok/ConfigurationKeys.java @@ -601,7 +601,21 @@ private ConfigurationKeys() {} * If set, any usage of {@code @ExtensionMethod} results in a warning / error. */ public static final ConfigurationKey EXTENSION_METHOD_FLAG_USAGE = new ConfigurationKey("lombok.extensionMethod.flagUsage", "Emit a warning or error if @ExtensionMethod is used.") {}; - + + /** + * lombok configuration: {@code lombok.extensionMethod.defaultSuppressBaseMethods} = {@code true} | {@code false}. + * + * For any class without an {@code @ExtensionMethod} that explicitly defines the {@code suppressBaseMethods} option, this value is used (default = true). + */ + public static final ConfigurationKey EXTENSION_METHOD_SUPPRESS_BASE_METHODS = new ConfigurationKey("lombok.extensionMethod.suppressBaseMethods", "If true, an applicable extension method is used (if found) even if the method call already was compilable (this is the default). If false, an extension method is only used if the method call is not also defined by the type itself..") {}; + + /** + * lombok configuration: {@code lombok.extensionMethod.defaultExtensions} += <TypeName: fully-qualified annotation class name>. + * + * All types whose static methods will be exposed as extension methods. + */ + public static final ConfigurationKey> EXTENSION_METHOD_DEFAULT_EXTENSIONS = new ConfigurationKey>("lombok.extensionMethod.defaultExtensions", "All types whose static methods will be exposed as extension methods.") {}; + // ----- FieldDefaults ----- /** diff --git a/src/core/lombok/eclipse/handlers/HandleExtensionMethod.java b/src/core/lombok/eclipse/handlers/HandleExtensionMethod.java index b84018c66d..a917fa5e59 100644 --- a/src/core/lombok/eclipse/handlers/HandleExtensionMethod.java +++ b/src/core/lombok/eclipse/handlers/HandleExtensionMethod.java @@ -22,45 +22,79 @@ package lombok.eclipse.handlers; import static lombok.core.handlers.HandlerUtil.*; +import static lombok.eclipse.handlers.EclipseHandlerUtil.*; +import java.util.Arrays; import java.util.List; +import lombok.core.AST; +import lombok.eclipse.*; import org.eclipse.jdt.internal.compiler.ast.Annotation; +import org.eclipse.jdt.internal.compiler.ast.QualifiedTypeReference; +import org.eclipse.jdt.internal.compiler.ast.SingleTypeReference; import org.eclipse.jdt.internal.compiler.ast.TypeDeclaration; +import org.eclipse.jdt.internal.compiler.ast.TypeReference; import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants; import lombok.ConfigurationKeys; import lombok.core.AnnotationValues; import lombok.core.HandlerPriority; -import lombok.eclipse.EclipseAnnotationHandler; -import lombok.eclipse.EclipseNode; import lombok.experimental.ExtensionMethod; import lombok.spi.Provides; // This handler just does some additional error checking; the real work is done in the agent. -@Provides +@Provides(EclipseASTVisitor.class) @HandlerPriority(66560) // 2^16 + 2^10; we must run AFTER HandleVal which is at 2^16 -public class HandleExtensionMethod extends EclipseAnnotationHandler { - @Override public void handle(AnnotationValues annotation, Annotation ast, EclipseNode annotationNode) { - handleExperimentalFlagUsage(annotationNode, ConfigurationKeys.EXTENSION_METHOD_FLAG_USAGE, "@ExtensionMethod"); - - TypeDeclaration typeDecl = null; - EclipseNode owner = annotationNode.up(); - if (owner.get() instanceof TypeDeclaration) typeDecl = (TypeDeclaration) owner.get(); - int modifiers = typeDecl == null ? 0 : typeDecl.modifiers; - - boolean notAClass = (modifiers & - (ClassFileConstants.AccAnnotation)) != 0; - - if (typeDecl == null || notAClass) { - annotationNode.addError("@ExtensionMethod is legal only on classes and enums and interfaces."); - return; - } - - List listenerInterfaces = annotation.getActualExpressions("value"); - if (listenerInterfaces.isEmpty()) { - annotationNode.addWarning(String.format("@ExtensionMethod has no effect since no extension types were specified.")); - return; +public class HandleExtensionMethod extends EclipseASTAdapter { + + private static final char[] EXTENSION_METHOD = "ExtensionMethod".toCharArray(); + + @Override public void visitType(EclipseNode typeNode, TypeDeclaration typeDecl) { + int modifiers = typeDecl.modifiers; + boolean notAClass = (modifiers & (ClassFileConstants.AccAnnotation)) != 0; + + AnnotationValues extensionMethod = null; + EclipseNode source = typeNode; + + List listenerInterfaces = null; + boolean suppressBaseMethodsIsExplicit = false; + ExtensionMethod em = null; + for (EclipseNode jn : typeNode.down()) { + if (jn.getKind() != AST.Kind.ANNOTATION) continue; + Annotation ann = (Annotation) jn.get(); + TypeReference typeTree = ann.type; + if (typeTree == null) continue; + if (typeTree instanceof SingleTypeReference) { + char[] t = ((SingleTypeReference) typeTree).token; + if (!Arrays.equals(t, EXTENSION_METHOD)) continue; + } else if (typeTree instanceof QualifiedTypeReference) { + char[][] t = ((QualifiedTypeReference) typeTree).tokens; + if (!Eclipse.nameEquals(t, "lombok.experimental.ExtensionMethod")) continue; + } else { + continue; + } + + if (!typeMatches(ExtensionMethod.class, jn, typeTree)) continue; + + source = jn; + extensionMethod = createAnnotation(ExtensionMethod.class, jn); + suppressBaseMethodsIsExplicit = extensionMethod.isExplicit("suppressBaseMethods"); + + handleExperimentalFlagUsage(jn, ConfigurationKeys.EXTENSION_METHOD_FLAG_USAGE, "@ExtensionMethod"); + + em = extensionMethod.getInstance(); + if (notAClass) { + jn.addError("@ExtensionMethod is legal only on classes and enums and interfaces."); + return; + } + + listenerInterfaces = extensionMethod.getActualExpressions("value"); + if (listenerInterfaces.isEmpty()) { + jn.addWarning("@ExtensionMethod has no effect since no extension types were specified."); + return; + } + break; } + } } diff --git a/src/core/lombok/javac/handlers/HandleExtensionMethod.java b/src/core/lombok/javac/handlers/HandleExtensionMethod.java index 6e2f6a30d0..298f1698fc 100644 --- a/src/core/lombok/javac/handlers/HandleExtensionMethod.java +++ b/src/core/lombok/javac/handlers/HandleExtensionMethod.java @@ -26,19 +26,18 @@ import static lombok.javac.handlers.JavacHandlerUtil.*; import static lombok.javac.handlers.JavacResolver.*; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import javax.lang.model.element.ElementKind; import lombok.ConfigurationKeys; +import lombok.core.AST; import lombok.core.AnnotationValues; import lombok.core.HandlerPriority; +import lombok.core.configuration.TypeName; import lombok.experimental.ExtensionMethod; -import lombok.javac.JavacAnnotationHandler; +import lombok.javac.JavacASTAdapter; +import lombok.javac.JavacASTVisitor; import lombok.javac.JavacNode; import lombok.javac.JavacResolution; import lombok.spi.Provides; @@ -66,37 +65,60 @@ /** * Handles the {@link ExtensionMethod} annotation for javac. */ -@Provides +@Provides(JavacASTVisitor.class) @HandlerPriority(66560) // 2^16 + 2^10; we must run AFTER HandleVal which is at 2^16 -public class HandleExtensionMethod extends JavacAnnotationHandler { - @Override - public void handle(final AnnotationValues annotation, final JCAnnotation source, final JavacNode annotationNode) { - handleExperimentalFlagUsage(annotationNode, ConfigurationKeys.EXTENSION_METHOD_FLAG_USAGE, "@ExtensionMethod"); - - deleteAnnotationIfNeccessary(annotationNode, ExtensionMethod.class); - JavacNode typeNode = annotationNode.up(); +public class HandleExtensionMethod extends JavacASTAdapter { + @Override public void visitType(JavacNode typeNode, JCClassDecl type) { boolean isClassEnumInterfaceOrRecord = isClassEnumInterfaceOrRecord(typeNode); - - if (!isClassEnumInterfaceOrRecord) { - annotationNode.addError("@ExtensionMethod can only be used on a class, an enum, an interface or a record"); - return; + + AnnotationValues extensionMethod = null; + JavacNode source = typeNode; + + boolean suppressBaseMethodsIsExplicit = false; + ExtensionMethod em = null; + for (JavacNode jn : typeNode.down()) { + if (jn.getKind() != AST.Kind.ANNOTATION) continue; + JCAnnotation ann = (JCAnnotation) jn.get(); + JCTree typeTree = ann.annotationType; + if (typeTree == null) continue; + String typeTreeToString = typeTree.toString(); + if (!typeTreeToString.equals("ExtensionMethod") && !typeTreeToString.equals("lombok.experimental.ExtensionMethod")) continue; + if (!typeMatches(ExtensionMethod.class, jn, typeTree)) continue; + + source = jn; + extensionMethod = createAnnotation(ExtensionMethod.class, jn); + suppressBaseMethodsIsExplicit = extensionMethod.isExplicit("suppressBaseMethods"); + + handleExperimentalFlagUsage(jn, ConfigurationKeys.EXTENSION_METHOD_FLAG_USAGE, "@ExtensionMethod"); + + em = extensionMethod.getInstance(); + if (!isClassEnumInterfaceOrRecord) { + jn.addError("@ExtensionMethod can only be used on a class, an enum, an interface or a record"); + return; + } + break; } - - boolean suppressBaseMethods = annotation.getInstance().suppressBaseMethods(); - - List extensionProviders = annotation.getActualExpressions("value"); - if (extensionProviders.isEmpty()) { - annotationNode.addError(String.format("@%s has no effect since no extension types were specified.", ExtensionMethod.class.getName())); + + boolean defaultSuppressBaseMethods = suppressBaseMethodsIsExplicit ? true : !Boolean.FALSE.equals(typeNode.getAst().readConfiguration(ConfigurationKeys.EXTENSION_METHOD_SUPPRESS_BASE_METHODS)); + List defaultExtensions = findDefaultExtensions(typeNode); + + List extensionProviders = extensionMethod != null ? extensionMethod.getActualExpressions("value") : Collections.emptyList(); + if (extensionProviders.isEmpty() && defaultExtensions.isEmpty()) { + source.addWarning("@ExtensionMethod has no effect since no extension types were specified."); return; } - final List extensions = getExtensions(annotationNode, extensionProviders); - if (extensions.isEmpty()) return; - - new ExtensionMethodReplaceVisitor(annotationNode, extensions, suppressBaseMethods).replace(); - - annotationNode.rebuild(); + + final List extensions = getExtensions(source, extensionProviders); + extensions.addAll(defaultExtensions); + if (extensions.isEmpty()) return; + + boolean emSuppressBaseMethods = (extensionMethod != null && suppressBaseMethodsIsExplicit) ? em.suppressBaseMethods() : defaultSuppressBaseMethods; + + new ExtensionMethodReplaceVisitor(source, extensions, emSuppressBaseMethods).replace(); + + source.rebuild(); } - + public List getExtensions(final JavacNode typeNode, final List extensionProviders) { List extensions = new ArrayList(); @@ -126,6 +148,21 @@ public Extension getExtension(final JavacNode typeNode, final ClassType extensio } return new Extension(extensionMethods, tsym); } + + public List findDefaultExtensions(JavacNode typeNode) { + java.util.List configuredDefaults = typeNode.getAst().readConfiguration(ConfigurationKeys.EXTENSION_METHOD_DEFAULT_EXTENSIONS); + + List extensions = new ArrayList(); + for (TypeName cn : configuredDefaults) { + JCExpression extensionType = chainDotsString(typeNode, cn.getName()); + Type providerType = CLASS.resolveMember(typeNode, extensionType); + if (providerType == null) continue; + if ((providerType.tsym.flags() & (INTERFACE | ANNOTATION)) != 0) continue; + + extensions.add(getExtension(typeNode, (ClassType) providerType)); + } + return extensions; + } private static class Extension { final List extensionMethods; diff --git a/src/core/lombok/javac/handlers/JavacHandlerUtil.java b/src/core/lombok/javac/handlers/JavacHandlerUtil.java index 0c075295b3..e8d22aa98b 100644 --- a/src/core/lombok/javac/handlers/JavacHandlerUtil.java +++ b/src/core/lombok/javac/handlers/JavacHandlerUtil.java @@ -322,7 +322,7 @@ public static boolean typeMatches(String type, JavacNode node, JCTree typeNode) return typeMatches(type, node, typeName); } - private static boolean typeMatches(String type, JavacNode node, String typeName) { + static boolean typeMatches(String type, JavacNode node, String typeName) { if (typeName == null || typeName.length() == 0) return false; int lastIndexA = typeName.lastIndexOf('.') + 1; int lastIndexB = Math.max(type.lastIndexOf('.'), type.lastIndexOf('$')) + 1; @@ -333,7 +333,7 @@ private static boolean typeMatches(String type, JavacNode node, String typeName) return resolver.typeMatches(node, type, typeName); } - private static String getTypeName(JCTree typeNode) { + static String getTypeName(JCTree typeNode) { return typeNode == null ? null : typeNode.toString(); } From e352920bf53a8513d53c08741aeb96a52b2a1d61 Mon Sep 17 00:00:00 2001 From: screret <68943070+screret@users.noreply.github.com> Date: Thu, 12 Feb 2026 16:56:31 +0200 Subject: [PATCH 2/3] fix default extension methods not actually applying --- .../javac/handlers/HandleExtensionMethod.java | 28 ++++++++++++++----- .../com/sun/tools/javac/code/Symtab.java | 2 ++ .../sun/tools/javac/main/JavaCompiler.java | 1 + src/utils/lombok/javac/Javac.java | 24 ++++++++++++++-- 4 files changed, 46 insertions(+), 9 deletions(-) diff --git a/src/core/lombok/javac/handlers/HandleExtensionMethod.java b/src/core/lombok/javac/handlers/HandleExtensionMethod.java index 298f1698fc..e5e3fac49f 100644 --- a/src/core/lombok/javac/handlers/HandleExtensionMethod.java +++ b/src/core/lombok/javac/handlers/HandleExtensionMethod.java @@ -36,6 +36,7 @@ import lombok.core.HandlerPriority; import lombok.core.configuration.TypeName; import lombok.experimental.ExtensionMethod; +import lombok.javac.Javac; import lombok.javac.JavacASTAdapter; import lombok.javac.JavacASTVisitor; import lombok.javac.JavacNode; @@ -54,6 +55,7 @@ import com.sun.tools.javac.code.Type.ForAll; import com.sun.tools.javac.code.Type.MethodType; import com.sun.tools.javac.code.Types; +import com.sun.tools.javac.main.JavaCompiler; import com.sun.tools.javac.tree.JCTree; import com.sun.tools.javac.tree.JCTree.JCAnnotation; import com.sun.tools.javac.tree.JCTree.JCClassDecl; @@ -61,6 +63,8 @@ import com.sun.tools.javac.tree.JCTree.JCFieldAccess; import com.sun.tools.javac.tree.JCTree.JCIdent; import com.sun.tools.javac.tree.JCTree.JCMethodInvocation; +import com.sun.tools.javac.util.Convert; +import com.sun.tools.javac.util.Name; /** * Handles the {@link ExtensionMethod} annotation for javac. @@ -87,6 +91,8 @@ public class HandleExtensionMethod extends JavacASTAdapter { source = jn; extensionMethod = createAnnotation(ExtensionMethod.class, jn); + deleteAnnotationIfNeccessary(jn, ExtensionMethod.class); + suppressBaseMethodsIsExplicit = extensionMethod.isExplicit("suppressBaseMethods"); handleExperimentalFlagUsage(jn, ConfigurationKeys.EXTENSION_METHOD_FLAG_USAGE, "@ExtensionMethod"); @@ -103,14 +109,14 @@ public class HandleExtensionMethod extends JavacASTAdapter { List defaultExtensions = findDefaultExtensions(typeNode); List extensionProviders = extensionMethod != null ? extensionMethod.getActualExpressions("value") : Collections.emptyList(); - if (extensionProviders.isEmpty() && defaultExtensions.isEmpty()) { + if (extensionMethod != null && extensionProviders.isEmpty() && !defaultExtensions.isEmpty()) { source.addWarning("@ExtensionMethod has no effect since no extension types were specified."); return; } final List extensions = getExtensions(source, extensionProviders); + if (extensions.isEmpty() && defaultExtensions.isEmpty()) return; extensions.addAll(defaultExtensions); - if (extensions.isEmpty()) return; boolean emSuppressBaseMethods = (extensionMethod != null && suppressBaseMethodsIsExplicit) ? em.suppressBaseMethods() : defaultSuppressBaseMethods; @@ -151,15 +157,23 @@ public Extension getExtension(final JavacNode typeNode, final ClassType extensio public List findDefaultExtensions(JavacNode typeNode) { java.util.List configuredDefaults = typeNode.getAst().readConfiguration(ConfigurationKeys.EXTENSION_METHOD_DEFAULT_EXTENSIONS); + if (configuredDefaults.isEmpty()) return Collections.emptyList(); List extensions = new ArrayList(); for (TypeName cn : configuredDefaults) { - JCExpression extensionType = chainDotsString(typeNode, cn.getName()); - Type providerType = CLASS.resolveMember(typeNode, extensionType); - if (providerType == null) continue; - if ((providerType.tsym.flags() & (INTERFACE | ANNOTATION)) != 0) continue; + Name name = typeNode.toName(cn.getName()); - extensions.add(getExtension(typeNode, (ClassType) providerType)); + Object module = null; + if (Javac.getJavaCompilerVersion() >= 9) { + module = typeNode.getSymbolTable().inferModule(Convert.packagePart(name)); + if (module == null) { + module = typeNode.getSymbolTable().unnamedModule; + } + } + ClassSymbol classSymbol = Javac.resolveIdent(JavaCompiler.instance(typeNode.getContext()), module, cn.getName()); + if ((classSymbol.flags() & (INTERFACE | ANNOTATION)) != 0) continue; + + extensions.add(getExtension(typeNode, (ClassType) classSymbol.type)); } return extensions; } diff --git a/src/stubs/com/sun/tools/javac/code/Symtab.java b/src/stubs/com/sun/tools/javac/code/Symtab.java index 89ed54788d..54024253f1 100644 --- a/src/stubs/com/sun/tools/javac/code/Symtab.java +++ b/src/stubs/com/sun/tools/javac/code/Symtab.java @@ -7,6 +7,7 @@ import com.sun.tools.javac.code.Symbol.ModuleSymbol; import com.sun.tools.javac.code.Symbol.TypeSymbol; import com.sun.tools.javac.util.Context; +import com.sun.tools.javac.util.Name; public class Symtab { // Shared by JDK6-9 @@ -21,4 +22,5 @@ public class Symtab { // JDK 9 public ModuleSymbol unnamedModule; + public ModuleSymbol inferModule(Name packageName) {return null;} } diff --git a/src/stubs/com/sun/tools/javac/main/JavaCompiler.java b/src/stubs/com/sun/tools/javac/main/JavaCompiler.java index d0e7b38f41..3f0e203437 100644 --- a/src/stubs/com/sun/tools/javac/main/JavaCompiler.java +++ b/src/stubs/com/sun/tools/javac/main/JavaCompiler.java @@ -21,6 +21,7 @@ public class JavaCompiler { public JavaCompiler(Context context) {} public int errorCount() { return 0; } + public static JavaCompiler instance(Context context) {return null;} public static String version() { return ""; } public JCCompilationUnit parse(String fileName) throws IOException { return null; } public List enterTrees(List roots) {return null;} diff --git a/src/utils/lombok/javac/Javac.java b/src/utils/lombok/javac/Javac.java index 840c32ee82..624d52cdff 100644 --- a/src/utils/lombok/javac/Javac.java +++ b/src/utils/lombok/javac/Javac.java @@ -39,6 +39,7 @@ import com.sun.tools.javac.code.Flags; import com.sun.tools.javac.code.Source; +import com.sun.tools.javac.code.Symbol; import com.sun.tools.javac.code.Symtab; import com.sun.tools.javac.code.Type; import com.sun.tools.javac.main.JavaCompiler; @@ -211,7 +212,7 @@ public static Object calculateGuess(JCExpression expr) { public static final TreeTag CTC_POSTINC = treeTag("POSTINC"); public static final TreeTag CTC_POSTDEC = treeTag("POSTDEC"); - private static final Method getExtendsClause, getEndPosition, storeEnd; + private static final Method getExtendsClause, getEndPosition, storeEnd, resolveIdent; static { getExtendsClause = getMethod(JCClassDecl.class, "getExtendsClause", new Class[0]); @@ -242,7 +243,12 @@ public static Object calculateGuess(JCExpression expr) { } storeEnd = storeEndMethodTemp; } - Permit.setAccessible(getEndPosition); + if (getJavaCompilerVersion() >= 9) { + resolveIdent = getMethod(JavaCompiler.class, "resolveIdent", Symbol.ModuleSymbol.class, String.class); + } else { + resolveIdent = getMethod(JavaCompiler.class, "resolveIdent", String.class); + } + Permit.setAccessible(getEndPosition); Permit.setAccessible(storeEnd); } @@ -444,6 +450,20 @@ public static void storeEnd(JCTree tree, int pos, JCCompilationUnit top) { } } + public static Symbol.ClassSymbol resolveIdent(JavaCompiler javaCompiler, Object module, String name) { + try { + if (getJavaCompilerVersion() >= 9) { + return (Symbol.ClassSymbol) resolveIdent.invoke(javaCompiler, module, name); + } else { + return (Symbol.ClassSymbol) resolveIdent.invoke(javaCompiler, name); + } + } catch (IllegalAccessException e) { + throw sneakyThrow(e); + } catch (InvocationTargetException e) { + throw sneakyThrow(e.getCause()); + } + } + private static final Class JC_VOID_TYPE, JC_NO_TYPE; static { From eafc00ec984fa1cbe6b384f28c2ed41cbf68f793 Mon Sep 17 00:00:00 2001 From: screret <68943070+screret@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:24:22 +0200 Subject: [PATCH 3/3] add myself to AUTHORS --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 380ed790cf..8bc1cd99e0 100755 --- a/AUTHORS +++ b/AUTHORS @@ -65,5 +65,6 @@ Till Brychcy Victor Williams Stafusa da Silva Yonatan Sherwin Yun Zhi Lin +Minttu Stenberg By adding your name to this list, you grant full and irrevocable copyright and patent indemnity to Project Lombok and all use of Project Lombok in relation to all commits you add to Project Lombok, and you certify that you have the right to do so.