diff --git a/src/core/lombok/ConfigurationKeys.java b/src/core/lombok/ConfigurationKeys.java index ce31bddf7f..2a2b4c2105 100644 --- a/src/core/lombok/ConfigurationKeys.java +++ b/src/core/lombok/ConfigurationKeys.java @@ -728,7 +728,9 @@ private ConfigurationKeys() {} * Copy these annotations to getters, setters, with methods, builder-setters, etc. */ public static final ConfigurationKey> COPYABLE_ANNOTATIONS = new ConfigurationKey>("lombok.copyableAnnotations", "Copy these annotations to getters, setters, with methods, builder-setters, etc.") {}; - + + public static final ConfigurationKey> COPYABLE_ANNOTATIONS_FOR_DELEGATE = new ConfigurationKey>("lombok.copyableAnnotationsForDelegate", "Copy these annotations to methods generated by @Delegate") {}; + /** * lombok configuration: {@code checkerframework} = {@code true} | {@code false} | <String: MajorVer.MinorVer> (Default: false). * diff --git a/src/core/lombok/core/handlers/HandlerUtil.java b/src/core/lombok/core/handlers/HandlerUtil.java index 53ca16f570..a0aa90ff05 100644 --- a/src/core/lombok/core/handlers/HandlerUtil.java +++ b/src/core/lombok/core/handlers/HandlerUtil.java @@ -78,8 +78,14 @@ public static int primeForFalse() { public static int primeForNull() { return 43; } - - public static final List NONNULL_ANNOTATIONS, BASE_COPYABLE_ANNOTATIONS, JACKSON_COPY_TO_GETTER_ANNOTATIONS, JACKSON_COPY_TO_SETTER_ANNOTATIONS, JACKSON_COPY_TO_BUILDER_SINGULAR_SETTER_ANNOTATIONS, JACKSON_COPY_TO_BUILDER_ANNOTATIONS; + + public static final List NONNULL_ANNOTATIONS, + BASE_COPYABLE_ANNOTATIONS, + JACKSON_COPY_TO_GETTER_ANNOTATIONS, + JACKSON_COPY_TO_SETTER_ANNOTATIONS, + JACKSON_COPY_TO_BUILDER_SINGULAR_SETTER_ANNOTATIONS, + JACKSON_COPY_TO_BUILDER_ANNOTATIONS, + COPYABLE_ANNOTATIONS_FOR_DELEGATE; static { // This is a list of annotations with a __highly specific meaning__: All annotations in this list indicate that passing null for the relevant item is __never__ acceptable, regardless of settings or circumstance. // In other words, things like 'this models a database table, and the db table column has a nonnull constraint', or 'this represents a web form, and if this is null, the form is invalid' __do not count__ and should not be in this list; @@ -489,6 +495,73 @@ public static int primeForNull() { "com.fasterxml.jackson.annotation.JsonView", "com.fasterxml.jackson.databind.annotation.JsonNaming", })); + COPYABLE_ANNOTATIONS_FOR_DELEGATE = Collections.unmodifiableList(Arrays.asList(new String[] { + "android.annotation.NonNull", + "android.annotation.Nullable", + "android.support.annotation.NonNull", + "android.support.annotation.Nullable", + "android.support.annotation.RecentlyNonNull", + "android.support.annotation.RecentlyNullable", + "androidx.annotation.NonNull", + "androidx.annotation.Nullable", + "androidx.annotation.RecentlyNonNull", + "androidx.annotation.RecentlyNullable", + "com.android.annotations.NonNull", + "com.android.annotations.Nullable", + "com.google.firebase.database.annotations.NotNull", + "com.google.firebase.database.annotations.Nullable", + "com.mongodb.lang.NonNull", + "com.mongodb.lang.Nullable", + "com.sun.istack.NotNull", + "com.sun.istack.Nullable", + "com.unboundid.util.NotNull", + "com.unboundid.util.Nullable", + "edu.umd.cs.findbugs.annotations.CheckForNull", + "edu.umd.cs.findbugs.annotations.NonNull", + "edu.umd.cs.findbugs.annotations.Nullable", + "edu.umd.cs.findbugs.annotations.PossiblyNull", + "edu.umd.cs.findbugs.annotations.UnknownNullness", + "io.micrometer.core.lang.NonNull", + "io.micrometer.core.lang.Nullable", + "io.reactivex.annotations.NonNull", + "io.reactivex.annotations.Nullable", + "io.reactivex.rxjava3.annotations.NonNull", + "io.reactivex.rxjava3.annotations.Nullable", + "jakarta.annotation.Nonnull", + "jakarta.annotation.Nullable", + "javax.annotation.CheckForNull", + "javax.annotation.Nonnull", + "javax.annotation.Nullable", + "libcore.util.NonNull", + "libcore.util.Nullable", + "lombok.NonNull", + "org.checkerframework.checker.nullness.compatqual.NonNullDecl", + "org.checkerframework.checker.nullness.compatqual.NonNullType", + "org.checkerframework.checker.nullness.compatqual.NullableDecl", + "org.checkerframework.checker.nullness.compatqual.NullableType", + "org.checkerframework.checker.nullness.qual.NonNull", + "org.checkerframework.checker.nullness.qual.Nullable", + "org.codehaus.commons.nullanalysis.NotNull", + "org.codehaus.commons.nullanalysis.Nullable", + "org.eclipse.jdt.annotation.NonNull", + "org.eclipse.jdt.annotation.Nullable", + "org.jetbrains.annotations.NotNull", + "org.jetbrains.annotations.Nullable", + "org.jetbrains.annotations.UnknownNullability", + "org.jmlspecs.annotation.NonNull", + "org.jmlspecs.annotation.Nullable", + "org.jspecify.annotations.NonNull", + "org.jspecify.annotations.NonNull", + "org.jspecify.annotations.Nullable", + "org.netbeans.api.annotations.common.CheckForNull", + "org.netbeans.api.annotations.common.NonNull", + "org.netbeans.api.annotations.common.NullAllowed", + "org.netbeans.api.annotations.common.NullUnknown", + "org.springframework.lang.NonNull", + "org.springframework.lang.Nullable", + "reactor.util.annotation.NonNull", + "reactor.util.annotation.Nullable", + })); } /** Checks if the given name is a valid identifier. diff --git a/src/core/lombok/javac/handlers/HandleDelegate.java b/src/core/lombok/javac/handlers/HandleDelegate.java index 4d72eea32c..6f197fb9b3 100644 --- a/src/core/lombok/javac/handlers/HandleDelegate.java +++ b/src/core/lombok/javac/handlers/HandleDelegate.java @@ -34,11 +34,7 @@ import java.util.List; import java.util.Set; -import javax.lang.model.element.ElementKind; -import javax.lang.model.element.ExecutableElement; -import javax.lang.model.element.Modifier; -import javax.lang.model.element.TypeParameterElement; -import javax.lang.model.element.VariableElement; +import javax.lang.model.element.*; import javax.lang.model.type.ExecutableType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; @@ -71,6 +67,7 @@ import lombok.core.AST.Kind; import lombok.core.AnnotationValues; import lombok.core.HandlerPriority; +import lombok.core.handlers.HandlerUtil; import lombok.experimental.Delegate; import lombok.javac.FindTypeVarScanner; import lombok.javac.JavacAnnotationHandler; @@ -295,16 +292,20 @@ public JCMethodDecl createDelegateMethod(MethodSig sig, JavacNode annotation, Na JavacTreeMaker maker = annotation.getTreeMaker(); - com.sun.tools.javac.util.List annotations; + ArrayList methodAnnotationsToCopy = new ArrayList(); if (sig.isDeprecated) { - annotations = com.sun.tools.javac.util.List.of(maker.Annotation( + methodAnnotationsToCopy.add(maker.Annotation( genJavaLangTypeRef(annotation, "Deprecated"), com.sun.tools.javac.util.List.nil())); - } else { - annotations = com.sun.tools.javac.util.List.nil(); } - - JCModifiers mods = maker.Modifiers(PUBLIC, annotations); + Set copyableAnnotations = JavacHandlerUtil.getCopyableAnnotationsForDelegate(annotation); + for (Compound sigAnnotation : sig.annotations) { + if (copyableAnnotations.contains(sigAnnotation.type.tsym.flatName().toString())) { + methodAnnotationsToCopy.add(maker.Annotation(sigAnnotation)); + } + } + + JCModifiers mods = maker.Modifiers(PUBLIC, com.sun.tools.javac.util.List.from(methodAnnotationsToCopy.toArray(new JCAnnotation[0]))); JCExpression returnType = JavacResolution.typeToJCTree((Type) sig.type.getReturnType(), annotation.getAst(), true); boolean useReturn = sig.type.getReturnType().getKind() != TypeKind.VOID; ListBuffer params = sig.type.getParameterTypes().isEmpty() ? null : new ListBuffer(); @@ -329,18 +330,26 @@ public JCMethodDecl createDelegateMethod(MethodSig sig, JavacNode annotation, Na for (TypeMirror ex : sig.type.getThrownTypes()) { thrown.append(JavacResolution.typeToJCTree((Type) ex, annotation.getAst(), true)); } - - int idx = 0; - String[] paramNames = sig.getParameterNames(); + boolean varargs = sig.elem.isVarArgs(); - for (TypeMirror param : sig.type.getParameterTypes()) { + ParameterSig[] paramSigs = sig.getParameters(); + for (int idx = 0; idx < paramSigs.length; idx++) { + ParameterSig paramSig = paramSigs[idx]; long flags = JavacHandlerUtil.addFinalIfNeeded(Flags.PARAMETER, annotation.getContext()); - JCModifiers paramMods = maker.Modifiers(flags); - Name name = annotation.toName(paramNames[idx++]); - if (varargs && idx == paramNames.length) { + + List paramAnnotationsToCopy = new ArrayList(); + for (AnnotationMirror b : paramSig.annotations) { + String fqn = ((TypeElement) b.getAnnotationType().asElement()).getQualifiedName().toString(); + if (copyableAnnotations.contains(fqn)) { + paramAnnotationsToCopy.add(maker.Annotation((Compound) b)); + } + } + JCModifiers paramMods = maker.Modifiers(flags, com.sun.tools.javac.util.List.from(paramAnnotationsToCopy.toArray(new JCAnnotation[0]))); + Name name = annotation.toName(paramSig.name); + if (varargs && idx == paramSigs.length - 1) { paramMods.flags |= VARARGS; } - params.append(maker.VarDef(paramMods, name, JavacResolution.typeToJCTree((Type) param, annotation.getAst(), true), null)); + params.append(maker.VarDef(paramMods, name, JavacResolution.typeToJCTree((Type) paramSig.type, annotation.getAst(), true), null)); args.append(maker.Ident(name)); } @@ -369,6 +378,7 @@ public void addMethodBindings(List signatures, ClassType ct, JavacTyp if (tsym == null) return; for (Symbol member : tsym.getEnclosedElements()) { + ArrayList annotations = new ArrayList(); for (Compound am : member.getAnnotationMirrors()) { String name = null; try { @@ -378,6 +388,9 @@ public void addMethodBindings(List signatures, ClassType ct, JavacTyp if ("lombok.Delegate".equals(name) || "lombok.experimental.Delegate".equals(name)) { throw new DelegateRecursion(ct.tsym.name.toString(), member.name.toString()); } + + annotations.add(am); + } if (member.getKind() != ElementKind.METHOD) continue; if (member.isStatic()) continue; @@ -388,7 +401,7 @@ public void addMethodBindings(List signatures, ClassType ct, JavacTyp String sig = printSig(methodType, member.name, types); if (!banList.add(sig)) continue; //If add returns false, it was already in there boolean isDeprecated = (member.flags() & DEPRECATED) != 0; - signatures.add(new MethodSig(member.name, methodType, isDeprecated, exElem)); + signatures.add(new MethodSig(member.name, methodType, isDeprecated, exElem, annotations)); } for (Type type : types.directSupertypes(ct)) { @@ -403,28 +416,47 @@ public static class MethodSig { final ExecutableType type; final boolean isDeprecated; final ExecutableElement elem; - - MethodSig(Name name, ExecutableType type, boolean isDeprecated, ExecutableElement elem) { + final List annotations; + + MethodSig(Name name, ExecutableType type, boolean isDeprecated, ExecutableElement elem, List annotations) { this.name = name; this.type = type; this.isDeprecated = isDeprecated; this.elem = elem; + this.annotations = annotations; } - String[] getParameterNames() { - List paramList = elem.getParameters(); - String[] paramNames = new String[paramList.size()]; - for (int i = 0; i < paramNames.length; i++) { - paramNames[i] = paramList.get(i).getSimpleName().toString(); + ParameterSig[] getParameters() { + VariableElement[] params = elem.getParameters().toArray(new VariableElement[0]); + TypeMirror[] paramTypes = type.getParameterTypes().toArray(new TypeMirror[0]); + ParameterSig[] parameterSigs = new ParameterSig[params.length]; + for (int i = 0; i < parameterSigs.length; i++) { + parameterSigs[i] = new ParameterSig( + params[i].getSimpleName().toString(), + paramTypes[i], + params[i].getAnnotationMirrors() + ); } - return paramNames; + return parameterSigs; } @Override public String toString() { return (isDeprecated ? "@Deprecated " : "") + name + " " + type; } } - + + public static class ParameterSig { + final String name; + final TypeMirror type; + final List annotations; + + ParameterSig(String name, TypeMirror type, List annotations) { + this.name = name; + this.type = type; + this.annotations = annotations; + } + } + public static String printSig(ExecutableType method, Name name, JavacTypes types) { StringBuilder sb = new StringBuilder(); sb.append(name.toString()).append("("); diff --git a/src/core/lombok/javac/handlers/JavacHandlerUtil.java b/src/core/lombok/javac/handlers/JavacHandlerUtil.java index f243c56a43..a958a61deb 100644 --- a/src/core/lombok/javac/handlers/JavacHandlerUtil.java +++ b/src/core/lombok/javac/handlers/JavacHandlerUtil.java @@ -32,10 +32,7 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; +import java.util.*; import java.util.regex.Pattern; import com.sun.source.tree.TreeVisitor; @@ -1755,7 +1752,18 @@ public static List findCopyableAnnotations(JavacNode node) { return copyAnnotations(result.toList(), node.getTreeMaker()); } - + + public static Set getCopyableAnnotationsForDelegate(JavacNode node) { + HashSet copyable = new HashSet(); + java.util.List configuredCopyable = node.getAst().readConfiguration(ConfigurationKeys.COPYABLE_ANNOTATIONS_FOR_DELEGATE); + if (configuredCopyable != null) { + for (TypeName cn : configuredCopyable) if (cn != null) copyable.add(cn.toString()); + } + copyable.addAll(COPYABLE_ANNOTATIONS_FOR_DELEGATE); + + return copyable; + } + /** * Searches the given field node for annotations that are specifically intended to be copied to the getter. * diff --git a/test/transform/resource/after-delombok/RecordWithNullable.java b/test/transform/resource/after-delombok/RecordWithNullable.java new file mode 100644 index 0000000000..9aff300ce2 --- /dev/null +++ b/test/transform/resource/after-delombok/RecordWithNullable.java @@ -0,0 +1,19 @@ +//platform !eclipse: Requires a 'full' eclipse with intialized workspace, and we don't (yet) have that set up properly in the test run. +//version 14: +import javax.annotation.Nullable; +import javax.annotation.Tainted; + +record DelegateOnRecord(SomeInterface runnable) { + interface SomeInterface { + @Nullable + @Tainted + String getString(@Nullable @Tainted String input); + } + + @javax.annotation.Nullable + @java.lang.SuppressWarnings("all") + @lombok.Generated + public java.lang.String getString(@javax.annotation.Nullable final java.lang.String input) { + return this.runnable.getString(input); + } +} diff --git a/test/transform/resource/before/RecordWithNullable.java b/test/transform/resource/before/RecordWithNullable.java new file mode 100644 index 0000000000..5d41c87daa --- /dev/null +++ b/test/transform/resource/before/RecordWithNullable.java @@ -0,0 +1,13 @@ +//platform !eclipse: Requires a 'full' eclipse with intialized workspace, and we don't (yet) have that set up properly in the test run. +//version 14: +import lombok.experimental.Delegate; +import javax.annotation.Nullable; +import javax.annotation.Tainted; + +record DelegateOnRecord(@Delegate SomeInterface runnable) { + interface SomeInterface { + @Nullable + @Tainted + String getString(@Nullable @Tainted String input); + } +}