diff options
44 files changed, 915 insertions, 128 deletions
diff --git a/doc/changelog.markdown b/doc/changelog.markdown index 876c810a..8c1b463d 100644 --- a/doc/changelog.markdown +++ b/doc/changelog.markdown @@ -5,7 +5,8 @@ Lombok Changelog * PLATFORM: Support for JDK13 (including `yield` in switch expressions, as well as delombok having a nicer style for arrow-style switch blocks, and text blocks). * FEATURE: In [`lombok.config`](https://projectlombok.org/features/configuration) it is possible to import other config files, even from a `.zip` or `.jar`. * FEATURE: You can now configure a builder's 'setter' prefixes via `@Builder(setterPrefix = "set")` for example. We discourage doing this, but if some library you use requires them, have at it. [Pull Request #2174](https://github.com/rzwitserloot/lombok/pull/2174], [Issue #1805](https://github.com/rzwitserloot/lombok/issues/1805). -* FEATURE: Tired of being unable to use `@javax.annotation.ParametersAreNonnullByDefault` or `@org.eclipse.jdt.annotation.NonNullByDefault` because then the equals method that lombok generates isn't valid? Fret no more; lombok will now add the appropriate `@Nullable` annotation to the parameter to avoid any issues. (package-level nonnullByDefault not supported, it must appear on the class for this feature to work). [Issue #788](https://github.com/rzwitserloot/lombok/issues/788) +* FEATURE: If you use `@Builder`'s `@Singular`, a plural form is also generated, which has the effect of adding all elements in the passed collection. If you pass a null reference, this would result in a message-less `NullPointerException`. Now, it results in that exception but with a useful message attached, and you can choose other behaviors as well via a parameter on the `@Singular` annotation and via `lombok.config`; you can even choose treat them as empty collections; this can be useful when deserializing (e.g. Jackson JSON) and JPA/Hibernate code. [Issue #2221](https://github.com/rzwitserloot/lombok/issues/2221]. [singular documentation](https://projectlombok.org/features/Builder). +* FEATURE: Tired of being unable to use `@javax.annotation.ParametersAreNonnullByDefault` or `@org.eclipse.jdt.annotation.NonNullByDefault` because then the equals method that lombok generates isn't valid? Fret no more; lombok can now add nullity annotations where relevant. Set the flavour of nullity annotation you prefer in your `lombok.config`. Applies to the return value of `toString` and `withX` methods, and the parameter of `equals`, `canEqual`, and the plural form of `@Singular` marked fields for builder classes. [Issue #788](https://github.com/rzwitserloot/lombok/issues/788) * BUGFIX: Referring to an inner class inside the generics on a class marked with `@SuperBuilder` would cause the error `wrong number of type arguments; required 3` [Issue #2262](https://github.com/rzwitserloot/lombok/issues/2262); fixed by github user [`@Lekanich`](https://github.com/rzwitserloot/lombok/issues/2262) - thank you! * BUGFIX: Some of the code generated by `@Builder` did not include `this.` prefixes when accessing fields. While semantically it didn't matter, if you use the 'add this prefix for field accesses' save action in eclipse, the save action would break. [Issue #2327](https://github.com/rzwitserloot/lombok/issues/2327) * BUGFIX: When lombok copies javadoc from fields to relevant methods, it should generate an appropriate `@return this` line if lombok copies the javadoc to a generated setter that is chainable (returns itself). It didn't do that when generating the 'setters' in a `@Builder`. Lombok also didn't generate an appropriate `@return` item for `@With` methods. The javadoc has also been updated slightly (the `this` reference in the javadoc is now rendered in a code tag).[Issue #2323](https://github.com/rzwitserloot/lombok/issues/2323) diff --git a/src/core/lombok/ConfigurationKeys.java b/src/core/lombok/ConfigurationKeys.java index 691346cd..559fff92 100644 --- a/src/core/lombok/ConfigurationKeys.java +++ b/src/core/lombok/ConfigurationKeys.java @@ -30,6 +30,7 @@ import lombok.core.configuration.ConfigurationKey; import lombok.core.configuration.FlagUsageType; import lombok.core.configuration.IdentifierName; import lombok.core.configuration.LogDeclaration; +import lombok.core.configuration.NullAnnotationLibrary; import lombok.core.configuration.NullCheckExceptionType; import lombok.core.configuration.TypeName; @@ -89,6 +90,32 @@ public class ConfigurationKeys { */ public static final ConfigurationKey<Boolean> ADD_FINDBUGS_SUPPRESSWARNINGS_ANNOTATIONS = new ConfigurationKey<Boolean>("lombok.extern.findbugs.addSuppressFBWarnings", "Generate @edu.umd.cs.findbugs.annotations.SuppressFBWarnings on all generated code (default: false).") {}; + /** + * lombok configuration: {@code lombok.addNullAnnotations = one of: [{@code none}, {@code javax}, {@code eclipse}, {@code jetbrains}, {@code netbeans}, {@code androidx}, {@code android.support}, {@code checkerframework}, {@code findbugs}, {@code spring}, {@code JML}, or a custom set of fully qualified annotation types]. + * + * Lombok generally copies relevant nullity annotations from your source code to the right places. However, sometimes lombok generates code where the nullability of some node is not dependent on something in your source code. You can configure lombok to add an appropriate nullity annotation in this case.<ul> + * <li>{@code none} (the default) - no annotations are added.</li> + * <li>{@code javax} - The annotations {@code javax.annotation.NonNull} and {@code javax.annotation.Nullable} are used.</li> + * <li>{@code eclipse} - The annotations {@code org.eclipse.jdt.annotation.NonNull} and {@code org.eclipse.jdt.annotation.Nullable} are used.</li> + * <li>{@code jetbrains} - The annotations {@code org.jetbrains.annotations.NotNull} and {@code org.jetbrains.annotations.Nullable} are used.</li> + * <li>{@code netbeans} - The annotations {@code org.netbeans.api.annotations.common.NonNull} and {@code org.netbeans.api.annotations.common.NullAllowed} are used.</li> + * <li>{@code androidx} - The annotations {@code androidx.annotation.NonNull} and {@code androidx.annotation.Nullable} are used.</li> + * <li>{@code android.support} - The annotations {@code android.support.annotation.NonNull} and {@code android.support.annotation.Nullable} are used.</li> + * <li>{@code checkerframework} - The annotations {@code org.checkerframework.checker.nullness.qual.NonNull} and {@code org.checkerframework.checker.nullness.qual.Nullable} are used.</li> + * <li>{@code findbugs} - The annotations {@code edu.umd.cs.findbugs.annotations.NonNull} and {@code edu.umd.cs.findbugs.annotations.Nullable} are used.</li> + * <li>{@code spring} - The annotations {@code org.springframework.lang.NonNull} and {@code org.springframework.lang.Nullable} are used.</li> + * <li>{@code jml} - The annotations {@code org.jmlspecs.annotation.NonNull} and {@code org.jmlspecs.annotation.Nullable} are used.</li> + * <li><code>CUSTOM:<em>fully.qualified.nonnull.annotation</em>:<em>fully.qualified.nullable.annotation</em></code> to configure your own types; the nullable annotation (and the colon) are optional.</li> + * </ul> + * <p> + * Lombok will not put these annotations on the classpath for you; your project must be set up such that these annotations are available. + * <p> + * Current features which use this configuration:<ul> + * <li>{@code @Builder.Singular} makes methods that accept a collection, all of which must be added. The parameter to this 'plural form' method is annotated.</li> + * </ul> + */ + public static final ConfigurationKey<NullAnnotationLibrary> ADD_NULL_ANNOTATIONS = new ConfigurationKey<NullAnnotationLibrary>("lombok.addNullAnnotations", "Generate some style of null annotation for generated code where this is relevant. (default: none).") {}; + // ----- *ArgsConstructor ----- /** diff --git a/src/core/lombok/core/configuration/NullAnnotationLibrary.java b/src/core/lombok/core/configuration/NullAnnotationLibrary.java new file mode 100644 index 00000000..68760e6f --- /dev/null +++ b/src/core/lombok/core/configuration/NullAnnotationLibrary.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2020 The Project Lombok Authors. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package lombok.core.configuration; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public final class NullAnnotationLibrary implements ConfigurationValueType { + private final String key; + private final String nonNullAnnotation; + private final String nullableAnnotation; + private final boolean typeUse; + + private NullAnnotationLibrary(String key, String nonNullAnnotation, String nullableAnnotation, boolean typeUse) { + this.key = key; + this.nonNullAnnotation = nonNullAnnotation; + this.nullableAnnotation = nullableAnnotation; + this.typeUse = typeUse; + } + + /** + * Returns the fully qualified annotation name to apply to non-null elements. If {@code null} is returned, apply no annotation. + */ + public String getNonNullAnnotation() { + return nonNullAnnotation; + } + + /** + * Returns the fully qualified annotation name to apply to nullable elements. If {@code null} is returned, apply no annotation. + */ + public String getNullableAnnotation() { + return nullableAnnotation; + } + + /** + * If {@code true}, the annotation can only be used in TYPE_USE form, otherwise, prefer to annotate the parameter, not the type of the parameter (or the method, not the return type, etc). + */ + public boolean isTypeUse() { + return typeUse; + } + + public static final NullAnnotationLibrary NONE = new NullAnnotationLibrary("none", null, null, false); + public static final NullAnnotationLibrary JAVAX = new NullAnnotationLibrary("javax", "javax.annotation.Nonnull", "javax.annotation.Nullable", false); + public static final NullAnnotationLibrary ECLIPSE = new NullAnnotationLibrary("eclipse", "org.eclipse.jdt.annotation.NonNull", "org.eclipse.jdt.annotation.Nullable", true); + public static final NullAnnotationLibrary JETBRAINS = new NullAnnotationLibrary("jetbrains", "org.jetbrains.annotations.NotNull", "org.jetbrains.annotations.Nullable", false); + public static final NullAnnotationLibrary NETBEANS = new NullAnnotationLibrary("netbeans", "org.netbeans.api.annotations.common.NonNull", "org.netbeans.api.annotations.common.NullAllowed", false); + public static final NullAnnotationLibrary ANDROIDX = new NullAnnotationLibrary("androidx", "androidx.annotation.NonNull", "androidx.annotation.Nullable", false); + public static final NullAnnotationLibrary ANDROID_SUPPORT = new NullAnnotationLibrary("android.support", "android.support.annotation.NonNull", "android.support.annotation.Nullable", false); + public static final NullAnnotationLibrary CHECKERFRAMEWORK = new NullAnnotationLibrary("checkerframework", "org.checkerframework.checker.nullness.qual.NonNull", "org.checkerframework.checker.nullness.qual.Nullable", true); + public static final NullAnnotationLibrary FINDBUGS = new NullAnnotationLibrary("findbugs", "edu.umd.cs.findbugs.annotations.NonNull", "edu.umd.cs.findbugs.annotations.Nullable", false); + public static final NullAnnotationLibrary SPRING = new NullAnnotationLibrary("spring", "org.springframework.lang.NonNull", "org.springframework.lang.Nullable", false); + public static final NullAnnotationLibrary JML = new NullAnnotationLibrary("jml", "org.jmlspecs.annotation.NonNull", "org.jmlspecs.annotation.Nullable", false); + + private static final List<NullAnnotationLibrary> ALL_AVAILABLE; + private static final String EXAMPLE_VALUE; + + static { + ArrayList<NullAnnotationLibrary> out = new ArrayList<NullAnnotationLibrary>(); + StringBuilder example = new StringBuilder(); + for (Field f : NullAnnotationLibrary.class.getDeclaredFields()) { + if (f.getType() != NullAnnotationLibrary.class || !Modifier.isStatic(f.getModifiers()) || !Modifier.isPublic(f.getModifiers())) continue; + try { + NullAnnotationLibrary nal = (NullAnnotationLibrary) f.get(null); + out.add(nal); + example.append(nal.key).append(" | "); + } catch (IllegalAccessException e) { + continue; + } + } + out.trimToSize(); + example.append("CUSTOM:com.foo.my.nonnull.annotation:com.foo.my.nullable.annotation"); + ALL_AVAILABLE = Collections.unmodifiableList(out); + EXAMPLE_VALUE = example.toString(); + } + + public static NullAnnotationLibrary custom(String nonNullAnnotation, String nullableAnnotation, boolean typeUse) { + if (nonNullAnnotation == null && nullableAnnotation == null) return NONE; + String typeUseStr = typeUse ? "TYPE_USE:" : ""; + if (nullableAnnotation == null) return new NullAnnotationLibrary("custom:" + typeUseStr + nonNullAnnotation, nonNullAnnotation, null, typeUse); + if (nonNullAnnotation == null) return new NullAnnotationLibrary("custom::" + typeUseStr + nullableAnnotation, null, nullableAnnotation, typeUse); + return new NullAnnotationLibrary("custom:" + typeUseStr + nonNullAnnotation + ":" + nullableAnnotation, nonNullAnnotation, nullableAnnotation, typeUse); + } + + public static String description() { + return "nullity-annotation-library"; + } + + public static String exampleValue() { + return EXAMPLE_VALUE; + } + + public static NullAnnotationLibrary valueOf(String in) { + String ci = in == null ? "" : in.toLowerCase(); + if (ci.length() == 0) return NONE; + for (NullAnnotationLibrary nal : ALL_AVAILABLE) if (nal.key.equals(ci)) return nal; + if (!ci.startsWith("custom:")) { + StringBuilder out = new StringBuilder("Invalid null annotation library. Valid options: "); + for (NullAnnotationLibrary nal : ALL_AVAILABLE) out.append(nal.key).append(", "); + out.setLength(out.length() - 2); + out.append(" or CUSTOM:[TYPE_USE:]nonnull.annotation.type:nullable.annotation.type"); + throw new IllegalArgumentException(out.toString()); + } + boolean typeUse = ci.startsWith("custom:type_use:"); + int start = typeUse ? 16 : 7; + int split = ci.indexOf(':', start); + if (split == -1) { + String nonNullAnnotation = ci.substring(start); + return custom(verifyTypeName(nonNullAnnotation), null, typeUse); + } + String nonNullAnnotation = ci.substring(start, split); + String nullableAnnotation = ci.substring(split + 1); + return custom(verifyTypeName(nonNullAnnotation), verifyTypeName(nullableAnnotation), typeUse); + } + + private static final String MSG = "Not an identifier (provide a fully qualified type for custom: nullity annotations): "; + private static String verifyTypeName(String fqn) { + boolean atStart = true; + for (int i = 0; i < fqn.length(); i++) { + char c = fqn.charAt(i); + if (Character.isJavaIdentifierStart(c)) { + atStart = false; + continue; + } + if (atStart) throw new IllegalArgumentException(MSG + fqn); + if (c == '.') { + atStart = true; + continue; + } + if (Character.isJavaIdentifierPart(c)) continue; + throw new IllegalArgumentException(MSG + fqn); + } + if (atStart) throw new IllegalArgumentException(MSG + fqn); + return fqn; + } +} diff --git a/src/core/lombok/core/handlers/HandlerUtil.java b/src/core/lombok/core/handlers/HandlerUtil.java index 883b1a29..bfc7b690 100644 --- a/src/core/lombok/core/handlers/HandlerUtil.java +++ b/src/core/lombok/core/handlers/HandlerUtil.java @@ -79,7 +79,7 @@ public class HandlerUtil { public static final List<String> NONNULL_ANNOTATIONS, BASE_COPYABLE_ANNOTATIONS, COPY_TO_SETTER_ANNOTATIONS; static { NONNULL_ANNOTATIONS = Collections.unmodifiableList(Arrays.asList(new String[] { - "android.annotation.NonNull", + "androidx.annotation.NonNull", "android.support.annotation.NonNull", "com.sun.istack.internal.NotNull", "edu.umd.cs.findbugs.annotations.NonNull", @@ -95,6 +95,8 @@ public class HandlerUtil { "org.springframework.lang.NonNull", })); BASE_COPYABLE_ANNOTATIONS = Collections.unmodifiableList(Arrays.asList(new String[] { + "androidx.annotation.NonNull", + "androidx.annotation.Nullable", "android.support.annotation.NonNull", "android.support.annotation.Nullable", "edu.umd.cs.findbugs.annotations.NonNull", @@ -104,6 +106,8 @@ public class HandlerUtil { "javax.annotation.Nonnull", "javax.annotation.Nullable", "lombok.NonNull", + "org.jmlspecs.annotation.NonNull", + "org.jmlspecs.annotation.Nullable", // To update Checker Framework annotations, run: // grep --recursive --files-with-matches -e '^@Target\b.*TYPE_USE' $CHECKERFRAMEWORK/checker/src/main/java $CHECKERFRAMEWORK/framework/src/main/java | grep '\.java$' | sed 's/.*\/java\//\t\t\t"/' | sed 's/\.java$/",/' | sed 's/\//./g' | sort // Only add new annotations, do not remove annotations that have been removed from the lastest version of the Checker Framework. @@ -304,11 +308,13 @@ public class HandlerUtil { "org.jetbrains.annotations.Nullable", "org.springframework.lang.NonNull", "org.springframework.lang.Nullable", + "org.netbeans.api.annotations.common.NonNull", + "org.netbeans.api.annotations.common.NullAllowed", })); COPY_TO_SETTER_ANNOTATIONS = Collections.unmodifiableList(Arrays.asList(new String[] { - "com.fasterxml.jackson.annotation.JsonProperty", - "com.fasterxml.jackson.annotation.JsonSetter", - })); + "com.fasterxml.jackson.annotation.JsonProperty", + "com.fasterxml.jackson.annotation.JsonSetter", + })); } /** Checks if the given name is a valid identifier. diff --git a/src/core/lombok/eclipse/handlers/EclipseHandlerUtil.java b/src/core/lombok/eclipse/handlers/EclipseHandlerUtil.java index aa48e000..bd0ad23b 100644 --- a/src/core/lombok/eclipse/handlers/EclipseHandlerUtil.java +++ b/src/core/lombok/eclipse/handlers/EclipseHandlerUtil.java @@ -43,6 +43,7 @@ import org.eclipse.jdt.internal.compiler.ast.AbstractMethodDeclaration; import org.eclipse.jdt.internal.compiler.ast.AbstractVariableDeclaration; import org.eclipse.jdt.internal.compiler.ast.AllocationExpression; import org.eclipse.jdt.internal.compiler.ast.Annotation; +import org.eclipse.jdt.internal.compiler.ast.Argument; import org.eclipse.jdt.internal.compiler.ast.ArrayInitializer; import org.eclipse.jdt.internal.compiler.ast.ArrayQualifiedTypeReference; import org.eclipse.jdt.internal.compiler.ast.ArrayTypeReference; @@ -112,6 +113,7 @@ import lombok.core.AnnotationValues.AnnotationValue; import lombok.core.LombokImmutableList; import lombok.core.TypeResolver; import lombok.core.configuration.CheckerFrameworkVersion; +import lombok.core.configuration.NullAnnotationLibrary; import lombok.core.configuration.NullCheckExceptionType; import lombok.core.configuration.TypeName; import lombok.core.debug.ProblemReporter; @@ -2381,6 +2383,101 @@ public class EclipseHandlerUtil { return p.equals("Object") || p.equals("java.lang.Object"); } + public static void createRelevantNullableAnnotation(EclipseNode typeNode, MethodDeclaration mth) { + NullAnnotationLibrary lib = typeNode.getAst().readConfiguration(ConfigurationKeys.ADD_NULL_ANNOTATIONS); + if (lib == null) return; + + applyAnnotationToMethodDecl(typeNode, mth, lib.getNullableAnnotation(), lib.isTypeUse()); + } + + public static void createRelevantNonNullAnnotation(EclipseNode typeNode, MethodDeclaration mth) { + NullAnnotationLibrary lib = typeNode.getAst().readConfiguration(ConfigurationKeys.ADD_NULL_ANNOTATIONS); + if (lib == null) return; + + applyAnnotationToMethodDecl(typeNode, mth, lib.getNonNullAnnotation(), lib.isTypeUse()); + } + + public static void createRelevantNullableAnnotation(EclipseNode typeNode, Argument arg) { + NullAnnotationLibrary lib = typeNode.getAst().readConfiguration(ConfigurationKeys.ADD_NULL_ANNOTATIONS); + if (lib == null) return; + + applyAnnotationToVarDecl(typeNode, arg, lib.getNullableAnnotation(), lib.isTypeUse()); + } + + public static void createRelevantNonNullAnnotation(EclipseNode typeNode, Argument arg) { + NullAnnotationLibrary lib = typeNode.getAst().readConfiguration(ConfigurationKeys.ADD_NULL_ANNOTATIONS); + if (lib == null) return; + + applyAnnotationToVarDecl(typeNode, arg, lib.getNonNullAnnotation(), lib.isTypeUse()); + } + + private static void applyAnnotationToMethodDecl(EclipseNode typeNode, MethodDeclaration mth, String annType, boolean typeUse) { + if (annType == null) return; + + int partCount = 1; + for (int i = 0; i < annType.length(); i++) if (annType.charAt(i) == '.') partCount++; + long[] ps = new long[partCount]; + Arrays.fill(ps, 0L); + Annotation ann = new MarkerAnnotation(new QualifiedTypeReference(Eclipse.fromQualifiedName(annType), ps), 0); + + if (!typeUse || mth.returnType == null || mth.returnType.getTypeName().length < 2) { + Annotation[] a = mth.annotations; + if (a == null) a = new Annotation[1]; + else { + Annotation[] b = new Annotation[a.length + 1]; + System.arraycopy(a, 0, b, 0, a.length); + a = b; + } + a[a.length - 1] = ann; + mth.annotations = a; + } else { + int len = mth.returnType.getTypeName().length; + if (mth.returnType.annotations == null) mth.returnType.annotations = new Annotation[len][]; + Annotation[] a = mth.returnType.annotations[len - 1]; + if (a == null) a = new Annotation[1]; + else { + Annotation[] b = new Annotation[a.length + 1]; + System.arraycopy(a, 0, b, 1, a.length); + a = b; + } + a[0] = ann; + mth.returnType.annotations[len - 1] = a; + } + } + private static void applyAnnotationToVarDecl(EclipseNode typeNode, Argument arg, String annType, boolean typeUse) { + if (annType == null) return; + + int partCount = 1; + for (int i = 0; i < annType.length(); i++) if (annType.charAt(i) == '.') partCount++; + long[] ps = new long[partCount]; + Arrays.fill(ps, 0L); + Annotation ann = new MarkerAnnotation(new QualifiedTypeReference(Eclipse.fromQualifiedName(annType), ps), 0); + + if (!typeUse || arg.type.getTypeName().length < 2) { + Annotation[] a = arg.annotations; + if (a == null) a = new Annotation[1]; + else { + Annotation[] b = new Annotation[a.length + 1]; + System.arraycopy(a, 0, b, 0, a.length); + a = b; + } + a[a.length - 1] = ann; + arg.annotations = a; + } else { + int len = arg.type.getTypeName().length; + if (arg.type.annotations == null) arg.type.annotations = new Annotation[len][]; + Annotation[] a = arg.type.annotations[len - 1]; + if (a == null) a = new Annotation[1]; + else { + Annotation[] b = new Annotation[a.length + 1]; + System.arraycopy(a, 0, b, 1, a.length); + a = b; + } + a[0] = ann; + arg.type.annotations[len - 1] = a; + } + } + public static NameReference generateQualifiedNameRef(ASTNode source, char[]... varNames) { int pS = source.sourceStart, pE = source.sourceEnd; long p = (long)pS << 32 | pE; diff --git a/src/core/lombok/eclipse/handlers/EclipseSingularsRecipes.java b/src/core/lombok/eclipse/handlers/EclipseSingularsRecipes.java index 5fe4b958..cbbd4cc8 100755 --- a/src/core/lombok/eclipse/handlers/EclipseSingularsRecipes.java +++ b/src/core/lombok/eclipse/handlers/EclipseSingularsRecipes.java @@ -35,6 +35,7 @@ import org.eclipse.jdt.internal.compiler.ast.ASTNode; import org.eclipse.jdt.internal.compiler.ast.AbstractMethodDeclaration; import org.eclipse.jdt.internal.compiler.ast.AllocationExpression; import org.eclipse.jdt.internal.compiler.ast.Annotation; +import org.eclipse.jdt.internal.compiler.ast.Argument; import org.eclipse.jdt.internal.compiler.ast.Block; import org.eclipse.jdt.internal.compiler.ast.ConditionalExpression; import org.eclipse.jdt.internal.compiler.ast.EqualExpression; @@ -437,7 +438,7 @@ public class EclipseSingularsRecipes { } } - protected void nullBehaviorize(SingularData data, List<Statement> statements) { + protected void nullBehaviorize(EclipseNode typeNode, SingularData data, List<Statement> statements, Argument arg) { NullCollectionBehavior behavior = data.getNullCollectionBehavior(); if (behavior == NullCollectionBehavior.IGNORE) { @@ -446,9 +447,12 @@ public class EclipseSingularsRecipes { b.statements = statements.toArray(new Statement[statements.size()]); statements.clear(); statements.add(new IfStatement(isNotNull, b, 0, 0)); + EclipseHandlerUtil.createRelevantNullableAnnotation(typeNode, arg); return; } + EclipseHandlerUtil.createRelevantNonNullAnnotation(typeNode, arg); + String exceptionTypeStr = behavior.getExceptionType(); StringLiteral message = new StringLiteral(behavior.toExceptionMessage(new String(data.getPluralName())).toCharArray(), 0, 0, 0); if (exceptionTypeStr != null) { diff --git a/src/core/lombok/eclipse/handlers/HandleEqualsAndHashCode.java b/src/core/lombok/eclipse/handlers/HandleEqualsAndHashCode.java index 46474b07..959b2cae 100755 --- a/src/core/lombok/eclipse/handlers/HandleEqualsAndHashCode.java +++ b/src/core/lombok/eclipse/handlers/HandleEqualsAndHashCode.java @@ -557,6 +557,7 @@ public class HandleEqualsAndHashCode extends EclipseAnnotationHandler<EqualsAndH method.arguments = new Argument[] {new Argument(new char[] { 'o' }, 0, objectRef, Modifier.FINAL)}; method.arguments[0].sourceStart = pS; method.arguments[0].sourceEnd = pE; if (!onParam.isEmpty()) method.arguments[0].annotations = onParam.toArray(new Annotation[0]); + EclipseHandlerUtil.createRelevantNullableAnnotation(type, method.arguments[0]); setGeneratedBy(method.arguments[0], source); List<Statement> statements = new ArrayList<Statement>(); @@ -806,6 +807,7 @@ public class HandleEqualsAndHashCode extends EclipseAnnotationHandler<EqualsAndH method.arguments = new Argument[] {new Argument(otherName, 0, objectRef, Modifier.FINAL)}; method.arguments[0].sourceStart = pS; method.arguments[0].sourceEnd = pE; if (!onParam.isEmpty()) method.arguments[0].annotations = onParam.toArray(new Annotation[0]); + EclipseHandlerUtil.createRelevantNullableAnnotation(type, method.arguments[0]); setGeneratedBy(method.arguments[0], source); SingleNameReference otherRef = new SingleNameReference(otherName, p); diff --git a/src/core/lombok/eclipse/handlers/HandleToString.java b/src/core/lombok/eclipse/handlers/HandleToString.java index 39fd5937..a6bcb24f 100644 --- a/src/core/lombok/eclipse/handlers/HandleToString.java +++ b/src/core/lombok/eclipse/handlers/HandleToString.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2019 The Project Lombok Authors. + * Copyright (C) 2009-2020 The Project Lombok Authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -315,6 +315,7 @@ public class HandleToString extends EclipseAnnotationHandler<ToString> { method.bodyStart = method.declarationSourceStart = method.sourceStart = source.sourceStart; method.bodyEnd = method.declarationSourceEnd = method.sourceEnd = source.sourceEnd; method.statements = new Statement[] { returnStatement }; + EclipseHandlerUtil.createRelevantNonNullAnnotation(type, method); return method; } diff --git a/src/core/lombok/eclipse/handlers/HandleWith.java b/src/core/lombok/eclipse/handlers/HandleWith.java index 8c8c3712..4771818d 100644 --- a/src/core/lombok/eclipse/handlers/HandleWith.java +++ b/src/core/lombok/eclipse/handlers/HandleWith.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2019 The Project Lombok Authors. + * Copyright (C) 2012-2020 The Project Lombok Authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -287,6 +287,8 @@ public class HandleWith extends EclipseAnnotationHandler<With> { } param.annotations = copyAnnotations(source, copyableAnnotations, onParam.toArray(new Annotation[0])); + EclipseHandlerUtil.createRelevantNonNullAnnotation(fieldNode, method); + method.traverse(new SetGeneratedByVisitor(source), parent.scope); return method; } diff --git a/src/core/lombok/eclipse/handlers/singulars/EclipseGuavaSingularizer.java b/src/core/lombok/eclipse/handlers/singulars/EclipseGuavaSingularizer.java index b067ad80..5d656c91 100755 --- a/src/core/lombok/eclipse/handlers/singulars/EclipseGuavaSingularizer.java +++ b/src/core/lombok/eclipse/handlers/singulars/EclipseGuavaSingularizer.java @@ -196,16 +196,18 @@ abstract class EclipseGuavaSingularizer extends EclipseSingularizer { thisDotFieldDotAddAll.selector = (getAddMethodName() + "All").toCharArray(); statements.add(thisDotFieldDotAddAll); - nullBehaviorize(data, statements); + TypeReference paramType; + paramType = new QualifiedTypeReference(fromQualifiedName(getAddAllTypeName()), NULL_POSS); + paramType = addTypeArgs(getTypeArgumentsCount(), true, builderType, paramType, data.getTypeArgs()); + Argument param = new Argument(data.getPluralName(), 0, paramType, ClassFileConstants.AccFinal); + + nullBehaviorize(builderType, data, statements, param); if (returnStatement != null) statements.add(returnStatement); md.statements = statements.toArray(new Statement[0]); - TypeReference paramType; - paramType = new QualifiedTypeReference(fromQualifiedName(getAddAllTypeName()), NULL_POSS); - paramType = addTypeArgs(getTypeArgumentsCount(), true, builderType, paramType, data.getTypeArgs()); - Argument param = new Argument(data.getPluralName(), 0, paramType, ClassFileConstants.AccFinal); + md.arguments = new Argument[] {param}; md.returnType = returnType; char[] prefixedSelector = data.getSetterPrefix().length == 0 ? data.getPluralName() : HandlerUtil.buildAccessorName(new String(data.getSetterPrefix()), new String(data.getPluralName())).toCharArray(); diff --git a/src/core/lombok/eclipse/handlers/singulars/EclipseJavaUtilListSetSingularizer.java b/src/core/lombok/eclipse/handlers/singulars/EclipseJavaUtilListSetSingularizer.java index ba447397..024f5880 100755 --- a/src/core/lombok/eclipse/handlers/singulars/EclipseJavaUtilListSetSingularizer.java +++ b/src/core/lombok/eclipse/handlers/singulars/EclipseJavaUtilListSetSingularizer.java @@ -174,14 +174,15 @@ abstract class EclipseJavaUtilListSetSingularizer extends EclipseJavaUtilSingula thisDotFieldDotAddAll.selector = "addAll".toCharArray(); statements.add(thisDotFieldDotAddAll); - nullBehaviorize(data, statements); + TypeReference paramType = new QualifiedTypeReference(TypeConstants.JAVA_UTIL_COLLECTION, NULL_POSS); + paramType = addTypeArgs(1, true, builderType, paramType, data.getTypeArgs()); + Argument param = new Argument(data.getPluralName(), 0, paramType, ClassFileConstants.AccFinal); + + nullBehaviorize(builderType, data, statements, param); if (returnStatement != null) statements.add(returnStatement); md.statements = statements.toArray(new Statement[0]); - TypeReference paramType = new QualifiedTypeReference(TypeConstants.JAVA_UTIL_COLLECTION, NULL_POSS); - paramType = addTypeArgs(1, true, builderType, paramType, data.getTypeArgs()); - Argument param = new Argument(data.getPluralName(), 0, paramType, ClassFileConstants.AccFinal); md.arguments = new Argument[] {param}; md.returnType = returnType; char[] prefixedSelector = data.getSetterPrefix().length == 0 ? data.getPluralName() : HandlerUtil.buildAccessorName(new String(data.getSetterPrefix()), new String(data.getPluralName())).toCharArray(); diff --git a/src/core/lombok/eclipse/handlers/singulars/EclipseJavaUtilMapSingularizer.java b/src/core/lombok/eclipse/handlers/singulars/EclipseJavaUtilMapSingularizer.java index e91c6616..843fd073 100755 --- a/src/core/lombok/eclipse/handlers/singulars/EclipseJavaUtilMapSingularizer.java +++ b/src/core/lombok/eclipse/handlers/singulars/EclipseJavaUtilMapSingularizer.java @@ -306,15 +306,16 @@ public class EclipseJavaUtilMapSingularizer extends EclipseJavaUtilSingularizer forEach.action = forEachContent; statements.add(forEach); - nullBehaviorize(data, statements); + TypeReference paramType = new QualifiedTypeReference(JAVA_UTIL_MAP, NULL_POSS); + paramType = addTypeArgs(2, true, builderType, paramType, data.getTypeArgs()); + Argument param = new Argument(data.getPluralName(), 0, paramType, ClassFileConstants.AccFinal); + + nullBehaviorize(builderType, data, statements, param); if (returnStatement != null) statements.add(returnStatement); md.statements = statements.toArray(new Statement[0]); - TypeReference paramType = new QualifiedTypeReference(JAVA_UTIL_MAP, NULL_POSS); - paramType = addTypeArgs(2, true, builderType, paramType, data.getTypeArgs()); - Argument param = new Argument(data.getPluralName(), 0, paramType, ClassFileConstants.AccFinal); md.arguments = new Argument[] {param}; md.returnType = returnType; diff --git a/src/core/lombok/javac/handlers/HandleEqualsAndHashCode.java b/src/core/lombok/javac/handlers/HandleEqualsAndHashCode.java index 0d0369b9..c4cf28da 100644 --- a/src/core/lombok/javac/handlers/HandleEqualsAndHashCode.java +++ b/src/core/lombok/javac/handlers/HandleEqualsAndHashCode.java @@ -383,18 +383,6 @@ public class HandleEqualsAndHashCode extends JavacAnnotationHandler<EqualsAndHas List<JCAnnotation> annsOnParamOnMethod = List.nil(); - String nearest = scanForNearestAnnotation(typeNode, "org.eclipse.jdt.annotation.NonNullByDefault"); - if (nearest != null) { - JCAnnotation m = maker.Annotation(genTypeRef(typeNode, "org.eclipse.jdt.annotation.Nullable"), List.<JCExpression>nil()); - annsOnParamOnMethod = annsOnParamOnMethod.prepend(m); - } - - nearest = scanForNearestAnnotation(typeNode, "javax.annotation.ParametersAreNullableByDefault", "javax.annotation.ParametersAreNonnullByDefault"); - if ("javax.annotation.ParametersAreNonnullByDefault".equals(nearest)) { - JCAnnotation m = maker.Annotation(genTypeRef(typeNode, "javax.annotation.Nullable"), List.<JCExpression>nil()); - annsOnParamOnMethod = annsOnParamOnMethod.prepend(m); - } - JCAnnotation overrideAnnotation = maker.Annotation(genJavaLangTypeRef(typeNode, "Override"), List.<JCExpression>nil()); List<JCAnnotation> annsOnMethod = List.of(overrideAnnotation); CheckerFrameworkVersion checkerFramework = getCheckerFrameworkVersion(typeNode); @@ -415,7 +403,10 @@ public class HandleEqualsAndHashCode extends JavacAnnotationHandler<EqualsAndHas long finalFlag = JavacHandlerUtil.addFinalIfNeeded(0L, typeNode.getContext()); ListBuffer<JCStatement> statements = new ListBuffer<JCStatement>(); - final List<JCVariableDecl> params = List.of(maker.VarDef(maker.Modifiers(finalFlag | Flags.PARAMETER, onParam), oName, objectType, null)); + JCVariableDecl param = maker.VarDef(maker.Modifiers(finalFlag | Flags.PARAMETER, onParam), oName, objectType, null); + JavacHandlerUtil.createRelevantNullableAnnotation(typeNode, param); + + final List<JCVariableDecl> params = List.of(param); /* if (o == this) return true; */ { statements.append(maker.If(maker.Binary(CTC_EQUAL, maker.Ident(oName), @@ -538,7 +529,9 @@ public class HandleEqualsAndHashCode extends JavacAnnotationHandler<EqualsAndHas JCExpression objectType = genJavaLangTypeRef(typeNode, "Object"); Name otherName = typeNode.toName("other"); long flags = JavacHandlerUtil.addFinalIfNeeded(Flags.PARAMETER, typeNode.getContext()); - List<JCVariableDecl> params = List.of(maker.VarDef(maker.Modifiers(flags, onParam), otherName, objectType, null)); + JCVariableDecl param = maker.VarDef(maker.Modifiers(flags, onParam), otherName, objectType, null); + createRelevantNullableAnnotation(typeNode, param); + List<JCVariableDecl> params = List.of(param); JCBlock body = maker.Block(0, List.<JCStatement>of( maker.Return(maker.TypeTest(maker.Ident(otherName), createTypeReference(typeNode, false))))); diff --git a/src/core/lombok/javac/handlers/HandleToString.java b/src/core/lombok/javac/handlers/HandleToString.java index d0d36e06..0a950f7c 100644 --- a/src/core/lombok/javac/handlers/HandleToString.java +++ b/src/core/lombok/javac/handlers/HandleToString.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2019 The Project Lombok Authors. + * Copyright (C) 2009-2020 The Project Lombok Authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -253,8 +253,10 @@ public class HandleToString extends JavacAnnotationHandler<ToString> { JCBlock body = maker.Block(0, List.of(returnStatement)); - return recursiveSetGeneratedBy(maker.MethodDef(mods, typeNode.toName("toString"), returnType, - List.<JCTypeParameter>nil(), List.<JCVariableDecl>nil(), List.<JCExpression>nil(), body, null), source, typeNode.getContext()); + JCMethodDecl methodDef = maker.MethodDef(mods, typeNode.toName("toString"), returnType, + List.<JCTypeParameter>nil(), List.<JCVariableDecl>nil(), List.<JCExpression>nil(), body, null); + createRelevantNonNullAnnotation(typeNode, methodDef); + return recursiveSetGeneratedBy(methodDef, source, typeNode.getContext()); } public static String getTypeName(JavacNode typeNode) { diff --git a/src/core/lombok/javac/handlers/HandleWith.java b/src/core/lombok/javac/handlers/HandleWith.java index 7b2417da..6977b10e 100644 --- a/src/core/lombok/javac/handlers/HandleWith.java +++ b/src/core/lombok/javac/handlers/HandleWith.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2019 The Project Lombok Authors. + * Copyright (C) 2012-2020 The Project Lombok Authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -211,6 +211,7 @@ public class HandleWith extends JavacAnnotationHandler<With> { long access = toJavacModifier(level); JCMethodDecl createdWith = createWith(access, fieldNode, fieldNode.getTreeMaker(), source, onMethod, onParam, makeAbstract); + createRelevantNonNullAnnotation(fieldNode, createdWith); ClassSymbol sym = ((JCClassDecl) fieldNode.up().get()).sym; Type returnType = sym == null ? null : sym.type; diff --git a/src/core/lombok/javac/handlers/JavacHandlerUtil.java b/src/core/lombok/javac/handlers/JavacHandlerUtil.java index 9359b1ae..0ef8c359 100644 --- a/src/core/lombok/javac/handlers/JavacHandlerUtil.java +++ b/src/core/lombok/javac/handlers/JavacHandlerUtil.java @@ -94,6 +94,7 @@ import lombok.core.CleanupTask; import lombok.core.LombokImmutableList; import lombok.core.TypeResolver; import lombok.core.configuration.CheckerFrameworkVersion; +import lombok.core.configuration.NullAnnotationLibrary; import lombok.core.configuration.NullCheckExceptionType; import lombok.core.configuration.TypeName; import lombok.core.handlers.HandlerUtil; @@ -1085,6 +1086,13 @@ public class JavacHandlerUtil { } } + static void setAnnotations(JCTree obj, List<JCAnnotation> anns) { + init(obj.getClass()); + try { + ANNOTATIONS.set(obj, anns); + } catch (Exception e) {} + } + static JCExpression getUnderlyingType(JCTree obj) { init(obj.getClass()); try { @@ -2054,4 +2062,95 @@ public class JavacHandlerUtil { String p = extending.toString(); return p.equals("Object") || p.equals("java.lang.Object"); } + + public static void createRelevantNullableAnnotation(JavacNode typeNode, JCMethodDecl mth) { + NullAnnotationLibrary lib = typeNode.getAst().readConfiguration(ConfigurationKeys.ADD_NULL_ANNOTATIONS); + if (lib == null) return; + applyAnnotationToMethodDecl(typeNode, mth, lib.getNullableAnnotation(), lib.isTypeUse()); + } + + public static void createRelevantNonNullAnnotation(JavacNode typeNode, JCMethodDecl mth) { + NullAnnotationLibrary lib = typeNode.getAst().readConfiguration(ConfigurationKeys.ADD_NULL_ANNOTATIONS); + if (lib == null) return; + applyAnnotationToMethodDecl(typeNode, mth, lib.getNonNullAnnotation(), lib.isTypeUse()); + } + + public static void createRelevantNonNullAnnotation(JavacNode typeNode, JCVariableDecl arg) { + NullAnnotationLibrary lib = typeNode.getAst().readConfiguration(ConfigurationKeys.ADD_NULL_ANNOTATIONS); + if (lib == null) return; + + applyAnnotationToVarDecl(typeNode, arg, lib.getNonNullAnnotation(), lib.isTypeUse()); + } + + public static void createRelevantNullableAnnotation(JavacNode typeNode, JCVariableDecl arg) { + NullAnnotationLibrary lib = typeNode.getAst().readConfiguration(ConfigurationKeys.ADD_NULL_ANNOTATIONS); + if (lib == null) return; + + applyAnnotationToVarDecl(typeNode, arg, lib.getNullableAnnotation(), lib.isTypeUse()); + } + + private static void applyAnnotationToMethodDecl(JavacNode typeNode, JCMethodDecl mth, String annType, boolean typeUse) { + if (annType == null) return; + JavacTreeMaker maker = typeNode.getTreeMaker(); + + JCAnnotation m = maker.Annotation(genTypeRef(typeNode, annType), List.<JCExpression>nil()); + if (typeUse) { + JCExpression resType = mth.restype; + if (resType instanceof JCTypeApply) { + JCTypeApply ta = (JCTypeApply) resType; + resType = ta.clazz; + } + + if (resType instanceof JCFieldAccess || resType instanceof JCArrayTypeTree) { + mth.restype = maker.AnnotatedType(List.of(m), resType); + return; + } + + if (JCAnnotatedTypeReflect.is(resType)) { + List<JCAnnotation> annotations = JCAnnotatedTypeReflect.getAnnotations(resType); + JCAnnotatedTypeReflect.setAnnotations(resType, annotations.prepend(m)); + return; + } + + if (resType instanceof JCPrimitiveTypeTree || resType instanceof JCIdent) { + mth.mods.annotations = mth.mods.annotations == null ? List.of(m) : mth.mods.annotations.prepend(m); + } + } else { + mth.mods.annotations = mth.mods.annotations == null ? List.of(m) : mth.mods.annotations.prepend(m); + } + } + + private static void applyAnnotationToVarDecl(JavacNode typeNode, JCVariableDecl arg, String annType, boolean typeUse) { + if (annType == null) return; + JavacTreeMaker maker = typeNode.getTreeMaker(); + + JCAnnotation m = maker.Annotation(genTypeRef(typeNode, annType), List.<JCExpression>nil()); + if (typeUse) { + JCExpression varType = arg.vartype; + JCTypeApply ta = null; + if (varType instanceof JCTypeApply) { + ta = (JCTypeApply) varType; + varType = ta.clazz; + } + + if (varType instanceof JCFieldAccess || varType instanceof JCArrayTypeTree) { + varType = maker.AnnotatedType(List.of(m), varType); + if (ta != null) ta.clazz = varType; + else arg.vartype = varType; + return; + } + + if (JCAnnotatedTypeReflect.is(varType)) { + List<JCAnnotation> annotations = JCAnnotatedTypeReflect.getAnnotations(varType); + JCAnnotatedTypeReflect.setAnnotations(varType, annotations.prepend(m)); + return; + } + + if (varType instanceof JCPrimitiveTypeTree || varType instanceof JCIdent) { + arg.mods.annotations = arg.mods.annotations == null ? List.of(m) : arg.mods.annotations.prepend(m); + } + } else { + arg.mods.annotations = arg.mods.annotations == null ? List.of(m) : arg.mods.annotations.prepend(m); + } + } } diff --git a/src/core/lombok/javac/handlers/JavacSingularsRecipes.java b/src/core/lombok/javac/handlers/JavacSingularsRecipes.java index a5d4a295..9dab3da5 100644 --- a/src/core/lombok/javac/handlers/JavacSingularsRecipes.java +++ b/src/core/lombok/javac/handlers/JavacSingularsRecipes.java @@ -273,12 +273,19 @@ public class JavacSingularsRecipes { generateClearMethod(cfv, deprecate, maker, returnTypeMaker.make(), returnStatementMaker.make(), data, builderType, source, access); } - private void finishAndInjectMethod(CheckerFrameworkVersion cfv, JavacTreeMaker maker, JCExpression returnType, JCStatement returnStatement, SingularData data, JavacNode builderType, JCTree source, boolean deprecate, ListBuffer<JCStatement> statements, Name methodName, List<JCVariableDecl> jcVariableDecls, AccessLevel access) { + private void finishAndInjectMethod(CheckerFrameworkVersion cfv, JavacTreeMaker maker, JCExpression returnType, JCStatement returnStatement, SingularData data, JavacNode builderType, JCTree source, boolean deprecate, ListBuffer<JCStatement> statements, Name methodName, List<JCVariableDecl> jcVariableDecls, AccessLevel access, NullCollectionBehavior nullBehavior) { if (returnStatement != null) statements.append(returnStatement); JCBlock body = maker.Block(0, statements.toList()); JCModifiers mods = makeMods(maker, cfv, builderType, deprecate, access); List<JCTypeParameter> typeParams = List.nil(); List<JCExpression> thrown = List.nil(); + + if (nullBehavior == NullCollectionBehavior.IGNORE) { + for (JCVariableDecl d : jcVariableDecls) createRelevantNullableAnnotation(builderType, d); + } else if (nullBehavior != null) { + for (JCVariableDecl d : jcVariableDecls) createRelevantNonNullAnnotation(builderType, d); + } + JCMethodDecl method = maker.MethodDef(mods, methodName, returnType, typeParams, jcVariableDecls, thrown, body, null); recursiveSetGeneratedBy(method, source, builderType.getContext()); injectMethod(builderType, method); @@ -290,7 +297,7 @@ public class JavacSingularsRecipes { statements.add(clearStatement); Name methodName = builderType.toName(HandlerUtil.buildAccessorName("clear", data.getPluralName().toString())); - finishAndInjectMethod(cfv, maker, returnType, returnStatement, data, builderType, source, deprecate, statements, methodName, List.<JCVariableDecl>nil(), access); + finishAndInjectMethod(cfv, maker, returnType, returnStatement, data, builderType, source, deprecate, statements, methodName, List.<JCVariableDecl>nil(), access, null); } protected abstract JCStatement generateClearStatements(JavacTreeMaker maker, SingularData data, JavacNode builderType); @@ -304,7 +311,7 @@ public class JavacSingularsRecipes { if (!setterPrefix.isEmpty()) name = builderType.toName(HandlerUtil.buildAccessorName(setterPrefix, name.toString())); statements.prepend(createConstructBuilderVarIfNeeded(maker, data, builderType, source)); - finishAndInjectMethod(cfv, maker, returnType, returnStatement, data, builderType, source, deprecate, statements, name, params, access); + finishAndInjectMethod(cfv, maker, returnType, returnStatement, data, builderType, source, deprecate, statements, name, params, access, null); } protected JCVariableDecl generateSingularMethodParameter(int typeIndex, JavacTreeMaker maker, SingularData data, JavacNode builderType, JCTree source, Name name) { @@ -336,10 +343,11 @@ public class JavacSingularsRecipes { JCExpression paramType = getPluralMethodParamType(builderType); paramType = addTypeArgs(getTypeArgumentsCount(), true, builderType, paramType, data.getTypeArgs(), source); long paramFlags = JavacHandlerUtil.addFinalIfNeeded(Flags.PARAMETER, builderType.getContext()); - JCVariableDecl param = maker.VarDef(maker.Modifiers(paramFlags), data.getPluralName(), paramType, null); - statements.prepend(createConstructBuilderVarIfNeeded(maker, data, builderType, source)); - NullCollectionBehavior behavior = data.getNullCollectionBehavior(); + if (behavior == null) behavior = NullCollectionBehavior.IGNORE; + JCModifiers paramMods = maker.Modifiers(paramFlags); + JCVariableDecl param = maker.VarDef(paramMods, data.getPluralName(), paramType, null); + statements.prepend(createConstructBuilderVarIfNeeded(maker, data, builderType, source)); if (behavior == NullCollectionBehavior.IGNORE) { JCExpression incomingIsNotNull = maker.Binary(CTC_NOT_EQUAL, maker.Ident(data.getPluralName()), maker.Literal(CTC_BOT, null)); @@ -361,7 +369,7 @@ public class JavacSingularsRecipes { } } - finishAndInjectMethod(cfv, maker, returnType, returnStatement, data, builderType, source, deprecate, statements, name, List.of(param), access); + finishAndInjectMethod(cfv, maker, returnType, returnStatement, data, builderType, source, deprecate, statements, name, List.of(param), access, behavior); } protected ListBuffer<JCStatement> generatePluralMethodStatements(JavacTreeMaker maker, SingularData data, JavacNode builderType, JCTree source) { diff --git a/src/core/lombok/javac/handlers/singulars/JavacGuavaSingularizer.java b/src/core/lombok/javac/handlers/singulars/JavacGuavaSingularizer.java index 546dc66e..7cd676c0 100644 --- a/src/core/lombok/javac/handlers/singulars/JavacGuavaSingularizer.java +++ b/src/core/lombok/javac/handlers/singulars/JavacGuavaSingularizer.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015-2018 The Project Lombok Authors. + * Copyright (C) 2015-2020 The Project Lombok Authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -121,14 +121,7 @@ abstract class JavacGuavaSingularizer extends JavacSingularizer { @Override protected JCExpression getPluralMethodParamType(JavacNode builderType) { - JCExpression paramType; - String aaTypeName = getAddAllTypeName(); - if (aaTypeName.startsWith("java.lang.") && aaTypeName.indexOf('.', 11) == -1) { - paramType = genJavaLangTypeRef(builderType, aaTypeName.substring(10)); - } else { - paramType = chainDotsString(builderType, aaTypeName); - } - return paramType; + return genTypeRef(builderType, getAddAllTypeName()); } @Override public void appendBuildCode(SingularData data, JavacNode builderType, JCTree source, ListBuffer<JCStatement> statements, Name targetVariableName, String builderVariable) { diff --git a/test/stubs/org/checkerframework/checker/nullness/qual/NonNull.java b/test/stubs/org/checkerframework/checker/nullness/qual/NonNull.java new file mode 100644 index 00000000..d552f8c7 --- /dev/null +++ b/test/stubs/org/checkerframework/checker/nullness/qual/NonNull.java @@ -0,0 +1,12 @@ +package org.checkerframework.checker.nullness.qual; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@Documented +public @interface NonNull {} diff --git a/test/stubs/org/checkerframework/checker/nullness/qual/Nullable.java b/test/stubs/org/checkerframework/checker/nullness/qual/Nullable.java new file mode 100644 index 00000000..d81e3abb --- /dev/null +++ b/test/stubs/org/checkerframework/checker/nullness/qual/Nullable.java @@ -0,0 +1,12 @@ +package org.checkerframework.checker.nullness.qual; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@Documented +public @interface Nullable {} diff --git a/test/stubs/org/eclipse/jdt/annotation/NonNull.java b/test/stubs/org/eclipse/jdt/annotation/NonNull.java new file mode 100644 index 00000000..be9ad6eb --- /dev/null +++ b/test/stubs/org/eclipse/jdt/annotation/NonNull.java @@ -0,0 +1,12 @@ +package org.eclipse.jdt.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Documented +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.TYPE_USE) +public @interface NonNull {} diff --git a/test/stubs/org/eclipse/jdt/annotation/Nullable.java b/test/stubs/org/eclipse/jdt/annotation/Nullable.java new file mode 100644 index 00000000..d19c54f1 --- /dev/null +++ b/test/stubs/org/eclipse/jdt/annotation/Nullable.java @@ -0,0 +1,12 @@ +package org.eclipse.jdt.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Documented +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.TYPE_USE) +public @interface Nullable {} diff --git a/test/stubs/org/springframework/lang/NonNull.java b/test/stubs/org/springframework/lang/NonNull.java new file mode 100644 index 00000000..b6b16680 --- /dev/null +++ b/test/stubs/org/springframework/lang/NonNull.java @@ -0,0 +1,12 @@ +package org.springframework.lang; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface NonNull {} diff --git a/test/stubs/org/springframework/lang/Nullable.java b/test/stubs/org/springframework/lang/Nullable.java new file mode 100644 index 00000000..94317005 --- /dev/null +++ b/test/stubs/org/springframework/lang/Nullable.java @@ -0,0 +1,12 @@ +package org.springframework.lang; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Nullable {} diff --git a/test/transform/resource/after-delombok/BuilderSingularNullBehavior2.java b/test/transform/resource/after-delombok/BuilderSingularNullBehavior2.java new file mode 100644 index 00000000..f62ebd10 --- /dev/null +++ b/test/transform/resource/after-delombok/BuilderSingularNullBehavior2.java @@ -0,0 +1,99 @@ +import java.util.List; +import lombok.Singular.NullCollectionBehavior; +class BuilderSingularNullBehavior2 { + private List<String> locations; + private List<String> doohickeys; + @java.lang.SuppressWarnings("all") + BuilderSingularNullBehavior2(final List<String> locations, final List<String> doohickeys) { + this.locations = locations; + this.doohickeys = doohickeys; + } + @java.lang.SuppressWarnings("all") + public static class BuilderSingularNullBehavior2Builder { + @java.lang.SuppressWarnings("all") + private java.util.ArrayList<String> locations; + @java.lang.SuppressWarnings("all") + private java.util.ArrayList<String> doohickeys; + @java.lang.SuppressWarnings("all") + BuilderSingularNullBehavior2Builder() { + } + @java.lang.SuppressWarnings("all") + public BuilderSingularNullBehavior2.BuilderSingularNullBehavior2Builder location(final String location) { + if (this.locations == null) this.locations = new java.util.ArrayList<String>(); + this.locations.add(location); + return this; + } + @java.lang.SuppressWarnings("all") + public BuilderSingularNullBehavior2.BuilderSingularNullBehavior2Builder locations(final java.util.@org.checkerframework.checker.nullness.qual.NonNull Collection<? extends String> locations) { + java.util.Objects.requireNonNull(locations, "locations cannot be null"); + if (this.locations == null) this.locations = new java.util.ArrayList<String>(); + this.locations.addAll(locations); + return this; + } + @java.lang.SuppressWarnings("all") + public BuilderSingularNullBehavior2.BuilderSingularNullBehavior2Builder clearLocations() { + if (this.locations != null) this.locations.clear(); + return this; + } + + @java.lang.SuppressWarnings("all") + public BuilderSingularNullBehavior2.BuilderSingularNullBehavior2Builder doohickey(final String doohickey) { + if (this.doohickeys == null) this.doohickeys = new java.util.ArrayList<String>(); + this.doohickeys.add(doohickey); + return this; + } + + @java.lang.SuppressWarnings("all") + public BuilderSingularNullBehavior2.BuilderSingularNullBehavior2Builder doohickeys(final java.util.@org.checkerframework.checker.nullness.qual.Nullable Collection<? extends String> doohickeys) { + if (doohickeys != null) { + if (this.doohickeys == null) this.doohickeys = new java.util.ArrayList<String>(); + this.doohickeys.addAll(doohickeys); + } + return this; + } + + @java.lang.SuppressWarnings("all") + public BuilderSingularNullBehavior2.BuilderSingularNullBehavior2Builder clearDoohickeys() { + if (this.doohickeys != null) this.doohickeys.clear(); + return this; + } + + @java.lang.SuppressWarnings("all") + public BuilderSingularNullBehavior2 build() { + java.util.List<String> locations; + switch (this.locations == null ? 0 : this.locations.size()) { + case 0: + locations = java.util.Collections.emptyList(); + break; + case 1: + locations = java.util.Collections.singletonList(this.locations.get(0)); + break; + default: + locations = java.util.Collections.unmodifiableList(new java.util.ArrayList<String>(this.locations)); + } + java.util.List<String> doohickeys; + switch (this.doohickeys == null ? 0 : this.doohickeys.size()) { + case 0: + doohickeys = java.util.Collections.emptyList(); + break; + case 1: + doohickeys = java.util.Collections.singletonList(this.doohickeys.get(0)); + break; + default: + doohickeys = java.util.Collections.unmodifiableList(new java.util.ArrayList<String>(this.doohickeys)); + } + return new BuilderSingularNullBehavior2(locations, doohickeys); + } + + @java.lang.Override + @java.lang.SuppressWarnings("all") + public java.lang.@org.checkerframework.checker.nullness.qual.NonNull String toString() { + return "BuilderSingularNullBehavior2.BuilderSingularNullBehavior2Builder(locations=" + this.locations + ", doohickeys=" + this.doohickeys + ")"; + } + } + + @java.lang.SuppressWarnings("all") + public static BuilderSingularNullBehavior2.BuilderSingularNullBehavior2Builder builder() { + return new BuilderSingularNullBehavior2.BuilderSingularNullBehavior2Builder(); + } +}
\ No newline at end of file diff --git a/test/transform/resource/after-delombok/EqualsAndHashCodeWithNNBD.java b/test/transform/resource/after-delombok/EqualsAndHashCodeWithNNBD.java deleted file mode 100644 index 6eaf9f36..00000000 --- a/test/transform/resource/after-delombok/EqualsAndHashCodeWithNNBD.java +++ /dev/null @@ -1,30 +0,0 @@ -import javax.annotation.ParametersAreNonnullByDefault; - -@ParametersAreNonnullByDefault -class EqualsAndHashCodeWithNNBD { - - @org.eclipse.jdt.annotation.NonNullByDefault - static class Inner { - @java.lang.Override - @java.lang.SuppressWarnings("all") - public boolean equals(final java.lang.@javax.annotation.Nullable @org.eclipse.jdt.annotation.Nullable Object o) { - if (o == this) return true; - if (!(o instanceof EqualsAndHashCodeWithNNBD.Inner)) return false; - final EqualsAndHashCodeWithNNBD.Inner other = (EqualsAndHashCodeWithNNBD.Inner) o; - if (!other.canEqual((java.lang.Object) this)) return false; - return true; - } - - @java.lang.SuppressWarnings("all") - protected boolean canEqual(final java.lang.Object other) { - return other instanceof EqualsAndHashCodeWithNNBD.Inner; - } - - @java.lang.Override - @java.lang.SuppressWarnings("all") - public int hashCode() { - final int result = 1; - return result; - } - } -}
\ No newline at end of file diff --git a/test/transform/resource/after-delombok/NullLibrary1.java b/test/transform/resource/after-delombok/NullLibrary1.java new file mode 100644 index 00000000..e426e3d3 --- /dev/null +++ b/test/transform/resource/after-delombok/NullLibrary1.java @@ -0,0 +1,42 @@ +public class NullLibrary1 { + String foo; + @java.lang.Override + @java.lang.SuppressWarnings("all") + public boolean equals(final java.lang.@org.eclipse.jdt.annotation.Nullable Object o) { + if (o == this) return true; + if (!(o instanceof NullLibrary1)) return false; + final NullLibrary1 other = (NullLibrary1) o; + if (!other.canEqual((java.lang.Object) this)) return false; + final java.lang.Object this$foo = this.foo; + final java.lang.Object other$foo = other.foo; + if (this$foo == null ? other$foo != null : !this$foo.equals(other$foo)) return false; + return true; + } + @java.lang.SuppressWarnings("all") + protected boolean canEqual(final java.lang.@org.eclipse.jdt.annotation.Nullable Object other) { + return other instanceof NullLibrary1; + } + @java.lang.Override + @java.lang.SuppressWarnings("all") + public int hashCode() { + final int PRIME = 59; + int result = 1; + final java.lang.Object $foo = this.foo; + result = result * PRIME + ($foo == null ? 43 : $foo.hashCode()); + return result; + } + @java.lang.Override + @java.lang.SuppressWarnings("all") + public java.lang.@org.eclipse.jdt.annotation.NonNull String toString() { + return "NullLibrary1(foo=" + this.foo + ")"; + } + @java.lang.SuppressWarnings("all") + public NullLibrary1(final String foo) { + this.foo = foo; + } + @org.eclipse.jdt.annotation.NonNull + @java.lang.SuppressWarnings("all") + public NullLibrary1 withFoo(final String foo) { + return this.foo == foo ? this : new NullLibrary1(foo); + } +} diff --git a/test/transform/resource/after-delombok/NullLibrary2.java b/test/transform/resource/after-delombok/NullLibrary2.java new file mode 100644 index 00000000..8f48c8a5 --- /dev/null +++ b/test/transform/resource/after-delombok/NullLibrary2.java @@ -0,0 +1,43 @@ +public class NullLibrary2 { + String foo; + @java.lang.Override + @java.lang.SuppressWarnings("all") + public boolean equals(@org.springframework.lang.Nullable final java.lang.Object o) { + if (o == this) return true; + if (!(o instanceof NullLibrary2)) return false; + final NullLibrary2 other = (NullLibrary2) o; + if (!other.canEqual((java.lang.Object) this)) return false; + final java.lang.Object this$foo = this.foo; + final java.lang.Object other$foo = other.foo; + if (this$foo == null ? other$foo != null : !this$foo.equals(other$foo)) return false; + return true; + } + @java.lang.SuppressWarnings("all") + protected boolean canEqual(@org.springframework.lang.Nullable final java.lang.Object other) { + return other instanceof NullLibrary2; + } + @java.lang.Override + @java.lang.SuppressWarnings("all") + public int hashCode() { + final int PRIME = 59; + int result = 1; + final java.lang.Object $foo = this.foo; + result = result * PRIME + ($foo == null ? 43 : $foo.hashCode()); + return result; + } + @org.springframework.lang.NonNull + @java.lang.Override + @java.lang.SuppressWarnings("all") + public java.lang.String toString() { + return "NullLibrary2(foo=" + this.foo + ")"; + } + @java.lang.SuppressWarnings("all") + public NullLibrary2(final String foo) { + this.foo = foo; + } + @org.springframework.lang.NonNull + @java.lang.SuppressWarnings("all") + public NullLibrary2 withFoo(final String foo) { + return this.foo == foo ? this : new NullLibrary2(foo); + } +} diff --git a/test/transform/resource/after-ecj/BuilderSingularNullBehavior2.java b/test/transform/resource/after-ecj/BuilderSingularNullBehavior2.java new file mode 100644 index 00000000..308a5174 --- /dev/null +++ b/test/transform/resource/after-ecj/BuilderSingularNullBehavior2.java @@ -0,0 +1,88 @@ +import java.util.List; +import lombok.Singular; +import lombok.Singular.NullCollectionBehavior; +@lombok.Builder class BuilderSingularNullBehavior2 { + public static @java.lang.SuppressWarnings("all") class BuilderSingularNullBehavior2Builder { + private @java.lang.SuppressWarnings("all") java.util.ArrayList<String> locations; + private @java.lang.SuppressWarnings("all") java.util.ArrayList<String> doohickeys; + @java.lang.SuppressWarnings("all") BuilderSingularNullBehavior2Builder() { + super(); + } + public @java.lang.SuppressWarnings("all") BuilderSingularNullBehavior2.BuilderSingularNullBehavior2Builder location(final String location) { + if ((this.locations == null)) + this.locations = new java.util.ArrayList<String>(); + this.locations.add(location); + return this; + } + public @java.lang.SuppressWarnings("all") BuilderSingularNullBehavior2.BuilderSingularNullBehavior2Builder locations(final java.util. @org.checkerframework.checker.nullness.qual.NonNull Collection<? extends String> locations) { + java.util.Objects.requireNonNull(locations, "locations cannot be null"); + if ((this.locations == null)) + this.locations = new java.util.ArrayList<String>(); + this.locations.addAll(locations); + return this; + } + public @java.lang.SuppressWarnings("all") BuilderSingularNullBehavior2.BuilderSingularNullBehavior2Builder clearLocations() { + if ((this.locations != null)) + this.locations.clear(); + return this; + } + public @java.lang.SuppressWarnings("all") BuilderSingularNullBehavior2.BuilderSingularNullBehavior2Builder doohickey(final String doohickey) { + if ((this.doohickeys == null)) + this.doohickeys = new java.util.ArrayList<String>(); + this.doohickeys.add(doohickey); + return this; + } + public @java.lang.SuppressWarnings("all") BuilderSingularNullBehavior2.BuilderSingularNullBehavior2Builder doohickeys(final java.util. @org.checkerframework.checker.nullness.qual.Nullable Collection<? extends String> doohickeys) { + if ((doohickeys != null)) + { + if ((this.doohickeys == null)) + this.doohickeys = new java.util.ArrayList<String>(); + this.doohickeys.addAll(doohickeys); + } + return this; + } + public @java.lang.SuppressWarnings("all") BuilderSingularNullBehavior2.BuilderSingularNullBehavior2Builder clearDoohickeys() { + if ((this.doohickeys != null)) + this.doohickeys.clear(); + return this; + } + public @java.lang.SuppressWarnings("all") BuilderSingularNullBehavior2 build() { + java.util.List<String> locations; + switch (((this.locations == null) ? 0 : this.locations.size())) { + case 0 : + locations = java.util.Collections.emptyList(); + break; + case 1 : + locations = java.util.Collections.singletonList(this.locations.get(0)); + break; + default : + locations = java.util.Collections.unmodifiableList(new java.util.ArrayList<String>(this.locations)); + } + java.util.List<String> doohickeys; + switch (((this.doohickeys == null) ? 0 : this.doohickeys.size())) { + case 0 : + doohickeys = java.util.Collections.emptyList(); + break; + case 1 : + doohickeys = java.util.Collections.singletonList(this.doohickeys.get(0)); + break; + default : + doohickeys = java.util.Collections.unmodifiableList(new java.util.ArrayList<String>(this.doohickeys)); + } + return new BuilderSingularNullBehavior2(locations, doohickeys); + } + public @java.lang.Override @java.lang.SuppressWarnings("all") java.lang.@org.checkerframework.checker.nullness.qual.NonNull String toString() { + return (((("BuilderSingularNullBehavior2.BuilderSingularNullBehavior2Builder(locations=" + this.locations) + ", doohickeys=") + this.doohickeys) + ")"); + } + } + private @Singular(nullBehavior = Singular.NullCollectionBehavior.JDK) List<String> locations; + private @Singular(nullBehavior = lombok.Singular.NullCollectionBehavior.IGNORE) List<String> doohickeys; + @java.lang.SuppressWarnings("all") BuilderSingularNullBehavior2(final List<String> locations, final List<String> doohickeys) { + super(); + this.locations = locations; + this.doohickeys = doohickeys; + } + public static @java.lang.SuppressWarnings("all") BuilderSingularNullBehavior2.BuilderSingularNullBehavior2Builder builder() { + return new BuilderSingularNullBehavior2.BuilderSingularNullBehavior2Builder(); + } +} diff --git a/test/transform/resource/after-ecj/EqualsAndHashCodeWithNNBD.java b/test/transform/resource/after-ecj/EqualsAndHashCodeWithNNBD.java deleted file mode 100644 index 4781839d..00000000 --- a/test/transform/resource/after-ecj/EqualsAndHashCodeWithNNBD.java +++ /dev/null @@ -1,28 +0,0 @@ -import javax.annotation.ParametersAreNonnullByDefault; -@ParametersAreNonnullByDefault class EqualsAndHashCodeWithNNBD { - static @lombok.EqualsAndHashCode @org.eclipse.jdt.annotation.NonNullByDefault class Inner { - Inner() { - super(); - } - public @java.lang.Override @java.lang.SuppressWarnings("all") boolean equals(final java.lang.@javax.annotation.Nullable @org.eclipse.jdt.annotation.Nullable Object o) { - if ((o == this)) - return true; - if ((! (o instanceof EqualsAndHashCodeWithNNBD.Inner))) - return false; - final EqualsAndHashCodeWithNNBD.Inner other = (EqualsAndHashCodeWithNNBD.Inner) o; - if ((! other.canEqual((java.lang.Object) this))) - return false; - return true; - } - protected @java.lang.SuppressWarnings("all") boolean canEqual(final java.lang.Object other) { - return (other instanceof EqualsAndHashCodeWithNNBD.Inner); - } - public @java.lang.Override @java.lang.SuppressWarnings("all") int hashCode() { - final int result = 1; - return result; - } - } - EqualsAndHashCodeWithNNBD() { - super(); - } -}
\ No newline at end of file diff --git a/test/transform/resource/after-ecj/NullLibrary1.java b/test/transform/resource/after-ecj/NullLibrary1.java new file mode 100644 index 00000000..baea9e7a --- /dev/null +++ b/test/transform/resource/after-ecj/NullLibrary1.java @@ -0,0 +1,37 @@ +public @lombok.EqualsAndHashCode @lombok.ToString @lombok.AllArgsConstructor class NullLibrary1 { + @lombok.With String foo; + public @org.eclipse.jdt.annotation.NonNull @java.lang.SuppressWarnings("all") NullLibrary1 withFoo(final String foo) { + return ((this.foo == foo) ? this : new NullLibrary1(foo)); + } + public @java.lang.Override @java.lang.SuppressWarnings("all") boolean equals(final java.lang.@org.eclipse.jdt.annotation.Nullable Object o) { + if ((o == this)) + return true; + if ((! (o instanceof NullLibrary1))) + return false; + final NullLibrary1 other = (NullLibrary1) o; + if ((! other.canEqual((java.lang.Object) this))) + return false; + final java.lang.Object this$foo = this.foo; + final java.lang.Object other$foo = other.foo; + if (((this$foo == null) ? (other$foo != null) : (! this$foo.equals(other$foo)))) + return false; + return true; + } + protected @java.lang.SuppressWarnings("all") boolean canEqual(final java.lang.@org.eclipse.jdt.annotation.Nullable Object other) { + return (other instanceof NullLibrary1); + } + public @java.lang.Override @java.lang.SuppressWarnings("all") int hashCode() { + final int PRIME = 59; + int result = 1; + final java.lang.Object $foo = this.foo; + result = ((result * PRIME) + (($foo == null) ? 43 : $foo.hashCode())); + return result; + } + public @java.lang.Override @java.lang.SuppressWarnings("all") java.lang.@org.eclipse.jdt.annotation.NonNull String toString() { + return (("NullLibrary1(foo=" + this.foo) + ")"); + } + public @java.lang.SuppressWarnings("all") NullLibrary1(final String foo) { + super(); + this.foo = foo; + } +} diff --git a/test/transform/resource/after-ecj/NullLibrary2.java b/test/transform/resource/after-ecj/NullLibrary2.java new file mode 100644 index 00000000..60ad103f --- /dev/null +++ b/test/transform/resource/after-ecj/NullLibrary2.java @@ -0,0 +1,37 @@ +public @lombok.EqualsAndHashCode @lombok.ToString @lombok.AllArgsConstructor class NullLibrary2 { + @lombok.With String foo; + public @org.springframework.lang.NonNull @java.lang.SuppressWarnings("all") NullLibrary2 withFoo(final String foo) { + return ((this.foo == foo) ? this : new NullLibrary2(foo)); + } + public @java.lang.Override @java.lang.SuppressWarnings("all") boolean equals(final @org.springframework.lang.Nullable java.lang.Object o) { + if ((o == this)) + return true; + if ((! (o instanceof NullLibrary2))) + return false; + final NullLibrary2 other = (NullLibrary2) o; + if ((! other.canEqual((java.lang.Object) this))) + return false; + final java.lang.Object this$foo = this.foo; + final java.lang.Object other$foo = other.foo; + if (((this$foo == null) ? (other$foo != null) : (! this$foo.equals(other$foo)))) + return false; + return true; + } + protected @java.lang.SuppressWarnings("all") boolean canEqual(final @org.springframework.lang.Nullable java.lang.Object other) { + return (other instanceof NullLibrary2); + } + public @java.lang.Override @java.lang.SuppressWarnings("all") int hashCode() { + final int PRIME = 59; + int result = 1; + final java.lang.Object $foo = this.foo; + result = ((result * PRIME) + (($foo == null) ? 43 : $foo.hashCode())); + return result; + } + public @java.lang.Override @org.springframework.lang.NonNull @java.lang.SuppressWarnings("all") java.lang.String toString() { + return (("NullLibrary2(foo=" + this.foo) + ")"); + } + public @java.lang.SuppressWarnings("all") NullLibrary2(final String foo) { + super(); + this.foo = foo; + } +} diff --git a/test/transform/resource/before/BuilderSingularNullBehavior2.java b/test/transform/resource/before/BuilderSingularNullBehavior2.java new file mode 100644 index 00000000..720cf5a8 --- /dev/null +++ b/test/transform/resource/before/BuilderSingularNullBehavior2.java @@ -0,0 +1,11 @@ +//CONF: lombok.addNullAnnotations = checkerframework +import java.util.List; + +import lombok.Singular; +import lombok.Singular.NullCollectionBehavior; + +@lombok.Builder +class BuilderSingularNullBehavior2 { + @Singular(nullBehavior = Singular.NullCollectionBehavior.JDK) private List<String> locations; + @Singular(nullBehavior = lombok.Singular.NullCollectionBehavior.IGNORE) private List<String> doohickeys; +} diff --git a/test/transform/resource/before/EqualsAndHashCodeWithNNBD.java b/test/transform/resource/before/EqualsAndHashCodeWithNNBD.java deleted file mode 100644 index dc86cb55..00000000 --- a/test/transform/resource/before/EqualsAndHashCodeWithNNBD.java +++ /dev/null @@ -1,8 +0,0 @@ -// version 8: -import javax.annotation.ParametersAreNonnullByDefault; -@ParametersAreNonnullByDefault -class EqualsAndHashCodeWithNNBD { - @lombok.EqualsAndHashCode @org.eclipse.jdt.annotation.NonNullByDefault - static class Inner { - } -} diff --git a/test/transform/resource/before/NullLibrary1.java b/test/transform/resource/before/NullLibrary1.java new file mode 100644 index 00000000..95433270 --- /dev/null +++ b/test/transform/resource/before/NullLibrary1.java @@ -0,0 +1,7 @@ +//CONF: lombok.addNullAnnotations = eclipse +@lombok.EqualsAndHashCode +@lombok.ToString +@lombok.AllArgsConstructor +public class NullLibrary1 { + @lombok.With String foo; +} diff --git a/test/transform/resource/before/NullLibrary2.java b/test/transform/resource/before/NullLibrary2.java new file mode 100644 index 00000000..17b1e6e0 --- /dev/null +++ b/test/transform/resource/before/NullLibrary2.java @@ -0,0 +1,7 @@ +//CONF: lombok.addNullAnnotations = spring +@lombok.EqualsAndHashCode +@lombok.ToString +@lombok.AllArgsConstructor +public class NullLibrary2 { + @lombok.With String foo; +} diff --git a/test/transform/resource/messages-delombok/EqualsAndHashCodeWithNNBD.java.messages b/test/transform/resource/messages-delombok/EqualsAndHashCodeWithNNBD.java.messages deleted file mode 100644 index 31259a51..00000000 --- a/test/transform/resource/messages-delombok/EqualsAndHashCodeWithNNBD.java.messages +++ /dev/null @@ -1 +0,0 @@ -5 package org.eclipse.jdt.annotation does not exist diff --git a/test/transform/resource/messages-ecj/EqualsAndHashCodeWithNNBD.java.messages b/test/transform/resource/messages-ecj/EqualsAndHashCodeWithNNBD.java.messages deleted file mode 100644 index 8fa7c194..00000000 --- a/test/transform/resource/messages-ecj/EqualsAndHashCodeWithNNBD.java.messages +++ /dev/null @@ -1 +0,0 @@ -2 The import javax.annotation.ParametersAreNonnullByDefault cannot be resolved diff --git a/test/transform/resource/messages-idempotent/EqualsAndHashCodeWithNNBD.java.messages b/test/transform/resource/messages-idempotent/EqualsAndHashCodeWithNNBD.java.messages deleted file mode 100644 index 15b05f9c..00000000 --- a/test/transform/resource/messages-idempotent/EqualsAndHashCodeWithNNBD.java.messages +++ /dev/null @@ -1,3 +0,0 @@ -6 package org.eclipse.jdt.annotation does not exist -10 package org.eclipse.jdt.annotation does not exist -10 annotation not applicabl diff --git a/website/templates/features/Builder.html b/website/templates/features/Builder.html index 08ff1ec8..9e5b34c8 100644 --- a/website/templates/features/Builder.html +++ b/website/templates/features/Builder.html @@ -136,6 +136,8 @@ The snippet below does not show what lombok generates for a <code>@Singular</code> field/parameter because it is rather complicated. You can view a snippet <a href="builderSingular">here</a>. </p><p> If also using <code>setterPrefix = "with"</code>, the generated names are, for example, <code>withName</code> (add 1 name), <code>withNames</code> (add many names), and <code>clearNames</code> (reset all names). + </p><p> + Ordinarily, the generated 'plural form' method (which takes in a collection, and adds each element in this collection) will check if a <code>null</code> is passed and throws a <code>NullPointerException</code> with an appropriate message. However, you can configure alternative behaviour. For example, for deserialization classes it can be useful to just do nothing (as if an empty collection was passed) instead: <code>@Singular(nullBehavior = NullCollectionBehavior.IGNORE)</code>. If you want to change the kind of nullcheck lombok generates (for example, if you prefer the non-recommended <code>IllegalArgumentException</code> instead, we suggest you configure this in one central place by making a <code>lombok.config</code> entry instead; the intended use for specifying the behavior on the <code>@Singular</code> annotation directly is to explicitly request the <code>IGNORE</code> behaviour. </p> </@f.featureSection> @@ -146,7 +148,7 @@ @Value @Builder @JsonDeserialize(builder = JacksonExample.JacksonExampleBuilder.class) public class JacksonExample { - @Singular private List<Foo> foos; + @Singular(nullBehavior = NullCollectionBehavior.IGNORE) private List<Foo> foos; @JsonPOJOBuilder(withPrefix = "") public static class JacksonExampleBuilder implements JacksonExampleBuilderMeta { @@ -179,6 +181,10 @@ public class JacksonExample { <code>lombok.singular.auto</code> = [<code>true</code> | <code>false</code>] (default: true) </dt><dd> If <code>true</code> (which is the default), lombok automatically tries to singularize your identifier name by assuming that it is a common english plural. If <code>false</code>, you must always explicitly specify the singular name, and lombok will generate an error if you don't (useful if you write your code in a language other than english). + </dd><dt> + <code>lombok.singular.nullCollections</code> = [<code>NullPointerException</code> | <code>IllegalArgumentException</code> | <code>Guava</code> | <code>JDK</code> | <code>ignore</code>] (default: <code>NullPointerException</code>) + </dt><dd> + What should lombok do when a generated 'plural form' (for singular properties) method is called with a <code>null</code> argument? Normally, lombok does an explicit null check with an appropriate message, but you can use this configuration to pick another flavour of nullcheck. The <code>ignore</code> option makes lombok treat null as an empty collection: Do nothing. An appropriate nullity annotation will be placed on the generated plural form method's parameter if you configured the flavour of nullity annotations you want via <a href="configuration"><code>lombok.config</code></a> key <code>lombok.addNullAnnotations</code>. </dd> </@f.confKeys> @@ -211,6 +217,8 @@ public class JacksonExample { </p><p> If setting the access level to <code>PROTECTED</code>, all methods generated inside the builder class are actually generated as <code>public</code>; the meaning of the <code>protected</code> keyword is different inside the inner class, and the precise behaviour that <code>PROTECTED</code> would indicate (access by any source in the same package is allowed, as well as any subclasses <em>from the outer class, marked with <code>@Builder</code></em> is not possible, and marking the inner members <code>public</code> is as close as we can get. + </p><p> + If you have configured a nullity annotation flavour via <a href="configuration"><code>lombok.config</code></a> key <code>lombok.addNullAnnotations</code>, any plural-form generated builder methods for <code>@Singular</code> marked properties (these plural form methods take a collection of some sort and add all elements) get a nullity annotation on the parameter. You get a non-null one normally, but if you have configured the behaviour on <code>null</code> being passed in as collection to <code>IGNORE</code>, a nullable annotation is generated instead. </p> </@f.smallPrint> </@f.scaffold> diff --git a/website/templates/features/EqualsAndHashCode.html b/website/templates/features/EqualsAndHashCode.html index 5b54e027..2ea2954a 100644 --- a/website/templates/features/EqualsAndHashCode.html +++ b/website/templates/features/EqualsAndHashCode.html @@ -54,7 +54,7 @@ If a getter exists for a field to be included, it is called instead of using a direct field reference. This behaviour can be suppressed:<br /> <code>@EqualsAndHashCode(doNotUseGetters = true)</code> </p><p> - If the class (or an enclosing class) has either the <code>@org.eclipse.jdt.annotation.NonNullByDefault</code> or the <code>@javax.annotation.ParametersAreNonnullByDefault</code> annotation, the parameter of the generated <code>equals</code> method will have the appropriate <code>@Nullable</code> annotation; JDK8+ is required when you do this. <span class="since">(since 1.18.12)</span>. + If you have configured a nullity annotation flavour via <a href="configuration"><code>lombok.config</code></a> key <code>lombok.addNullAnnotations</code>, the parameter of both the generated <code>equals</code> method as well as any <code>canEqual</code> method is annotated with a nullable annotation. This is required if you use a <code>@NonNullByDefault</code> style annotation in combination with strict nullity checking. </p> </@f.smallPrint> </@f.scaffold> diff --git a/website/templates/features/ToString.html b/website/templates/features/ToString.html index d2644629..456092d5 100644 --- a/website/templates/features/ToString.html +++ b/website/templates/features/ToString.html @@ -59,6 +59,8 @@ <code>@ToString(doNotUseGetters = true)</code> </p><p> <code>@ToString</code> can also be used on an enum definition. + </p><p> + If you have configured a nullity annotation flavour via <a href="configuration"><code>lombok.config</code></a> key <code>lombok.addNullAnnotations</code>, the method or return type (as appropriate for the chosen flavour) is annotated with a non-null annotation. </p> </@f.smallPrint> </@f.scaffold> diff --git a/website/templates/features/With.html b/website/templates/features/With.html index 425a1640..8b34f038 100644 --- a/website/templates/features/With.html +++ b/website/templates/features/With.html @@ -47,6 +47,8 @@ No method is generated if any method already exists with the same name (case insensitive) and same parameter count. For example, <code>withX(int x)</code> will not be generated if there's already a method <code>withX(String... x)</code> even though it is technically possible to make the method. This caveat exists to prevent confusion. If the generation of a method is skipped for this reason, a warning is emitted instead. Varargs count as 0 to N parameters. </p><p> Various well known annotations about nullity cause null checks to be inserted and will be copied to the parameter. See <a href="/features/GetterSetter">Getter/Setter</a> documentation's small print for more information. + </p><p> + If you have configured a nullity annotation flavour via <a href="configuration"><code>lombok.config</code></a> key <code>lombok.addNullAnnotations</code>, the method or return type (as appropriate for the chosen flavour) is annotated with a non-null annotation. </p> </@f.smallPrint> </@f.scaffold> diff --git a/website/templates/features/configuration.html b/website/templates/features/configuration.html index 4d7b1547..26d9af4f 100644 --- a/website/templates/features/configuration.html +++ b/website/templates/features/configuration.html @@ -80,6 +80,12 @@ </ol> can be included. We suggest you put this in the root of your workspace directory. </p><p> + Lombok can add nullity annotations (usually called <code>@NonNull</code> and <code>@Nullable</code>) whenever it makes sense to do so; think of generated <a href="ToString"><code>toString</code></a> and <a href="with"><code>withX</code> methods (these never return null), or the parameter of a generated <a href="EqualsAndHashCode"><code>equals</code></a> method, which is allowed to be null, and requires such an annotation if you've set up your IDE for strict null checks as well as 'parameters are non-null by default'. There are many such libraries; you must tell lombok which one to use. By default, no such annotations are added. Enable this feature with: + <div class="snippet example"> + <code>lombok.addNullAnnotations = <em>flavour</em></code> (flavours: <code>javax</code> (=JSR305; not recommended), <code>eclipse</code>, <code>jetbrains</code>, <code>netbeans</code>, <code>androidx</code>, <code>android.support</code> (deprecated within android), <code>checkerframework</code> (recommended), <code>findbugs</code>, <code>spring</code>, <code>jml</code>, or define your own via <code>CUSTOM:fully.qualified.NonNullAnnotation:fully.qualified.NullableAnnotation</code>. + </div> + <em>This feature was introduced in lombok v1.20.0</em>. + </p><p> Lombok can add <code>@javax.annotation.Generated</code> annotations to all generated nodes where possible. You can enable this with: <ol class="snippet example oneliner"> <li><code>lombok.addJavaxGeneratedAnnotation = true</code></li> |