diff options
author | Nima Karimipour <karimipour.nima@gmail.com> | 2021-12-22 11:02:10 -0600 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-12-22 12:02:10 -0500 |
commit | da577508a4f189cecf761026feddbe4e016a5109 (patch) | |
tree | 151df25d1250c8f837a9110946c73f017df92e8a | |
parent | 283564913161d78c4d129aa27fa1bf46ff0708b2 (diff) | |
download | nullaway-da577508a4f189cecf761026feddbe4e016a5109.tar.gz |
Adds custom Nullable Annotation via Error Prone CLI flags (#522)
This PR adds the ability to configure additional custom `@Nullable/@Nonnull` annotations.
In particular, this adds the following configuration options to NullAway:
```
-XepOpt:NullAway:CustomNullableAnnotations=[...]
-XepOpt:NullAway:CustomNonnullAnnotations=[...]
```
Each of these options takes a comma-separated list of fully qualified annotation names, which NullAway should regard as nullable and non-null annotations (respectively), in *addition* to those it looks for by default. Neither configuration option is required when all relevant nullability annotations follow the conventions NullAway already expects (e.g. simple name `@Nullable` or `@NonNull` or any of our default supported annotations such as `@CheckForNull`).
During auto-annotation testing, we have encountered a few projects which are partially annotated with nullability annotations with different simple names than the conventional `@Nullable` or `@NonNull` (and recognized variations, such as `@NotNull`). For example, [LibGdx](https://github.com/libgdx/libgdx) uses [@com.badlogic.gdx.utils.Null](https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/utils/Null.java). This is a custom nullability annotation which lives within the project itself and doesn't use the `@Nullable` simple name convention. Rather than explicitly encoding every such annotation in existence into our defaults, this PR allows supporting such cases with a simple configuration change.
6 files changed, 107 insertions, 2 deletions
diff --git a/nullaway/src/main/java/com/uber/nullaway/AbstractConfig.java b/nullaway/src/main/java/com/uber/nullaway/AbstractConfig.java index 0510b40..afc6cb1 100644 --- a/nullaway/src/main/java/com/uber/nullaway/AbstractConfig.java +++ b/nullaway/src/main/java/com/uber/nullaway/AbstractConfig.java @@ -110,6 +110,11 @@ public abstract class AbstractConfig implements Config { protected String errorURL; + /** --- Fully qualified names of custom nonnull/nullable annotation --- */ + protected Set<String> customNonnullAnnotations; + + protected Set<String> customNullableAnnotations; + protected static Pattern getPackagePattern(Set<String> packagePrefixes) { // noinspection ConstantConditions String choiceRegexp = @@ -165,6 +170,16 @@ public abstract class AbstractConfig implements Config { } @Override + public boolean isCustomNullableAnnotation(String annotationName) { + return customNullableAnnotations.contains(annotationName); + } + + @Override + public boolean isCustomNonnullAnnotation(String annotationName) { + return customNonnullAnnotations.contains(annotationName); + } + + @Override public boolean exhaustiveOverride() { return isExhaustiveOverride; } diff --git a/nullaway/src/main/java/com/uber/nullaway/Config.java b/nullaway/src/main/java/com/uber/nullaway/Config.java index a82abc5..2d92da2 100644 --- a/nullaway/src/main/java/com/uber/nullaway/Config.java +++ b/nullaway/src/main/java/com/uber/nullaway/Config.java @@ -75,6 +75,22 @@ public interface Config { boolean isInitializerMethodAnnotation(String annotationName); /** + * Checks if the annotation is a custom @Nullable annotation. + * + * @param annotationName fully-qualified annotation name + * @return true if annotation should be considered as a custom nullable annotation. + */ + boolean isCustomNullableAnnotation(String annotationName); + + /** + * Checks if the annotation is a custom @Nonnull annotation. + * + * @param annotationName fully-qualified annotation name + * @return true if annotation should be considered as a custom nonnull annotation. + */ + boolean isCustomNonnullAnnotation(String annotationName); + + /** * Checks if the annotation is an excluded field annotation. * * @param annotationName fully-qualified annotation name diff --git a/nullaway/src/main/java/com/uber/nullaway/DummyOptionsConfig.java b/nullaway/src/main/java/com/uber/nullaway/DummyOptionsConfig.java index 6f20b81..d6cd7e8 100644 --- a/nullaway/src/main/java/com/uber/nullaway/DummyOptionsConfig.java +++ b/nullaway/src/main/java/com/uber/nullaway/DummyOptionsConfig.java @@ -104,6 +104,16 @@ public class DummyOptionsConfig implements Config { } @Override + public boolean isCustomNullableAnnotation(String annotationName) { + return false; + } + + @Override + public boolean isCustomNonnullAnnotation(String annotationName) { + return false; + } + + @Override public boolean suggestSuppressions() { throw new IllegalStateException(error_msg); } diff --git a/nullaway/src/main/java/com/uber/nullaway/ErrorProneCLIFlagsConfig.java b/nullaway/src/main/java/com/uber/nullaway/ErrorProneCLIFlagsConfig.java index 6893860..075743e 100644 --- a/nullaway/src/main/java/com/uber/nullaway/ErrorProneCLIFlagsConfig.java +++ b/nullaway/src/main/java/com/uber/nullaway/ErrorProneCLIFlagsConfig.java @@ -51,6 +51,8 @@ final class ErrorProneCLIFlagsConfig extends AbstractConfig { static final String FL_ACKNOWLEDGE_ANDROID_RECENT = EP_FL_NAMESPACE + ":AcknowledgeAndroidRecent"; static final String FL_EXCLUDED_FIELD_ANNOT = EP_FL_NAMESPACE + ":ExcludedFieldAnnotations"; static final String FL_INITIALIZER_ANNOT = EP_FL_NAMESPACE + ":CustomInitializerAnnotations"; + static final String FL_NULLABLE_ANNOT = EP_FL_NAMESPACE + ":CustomNullableAnnotations"; + static final String FL_NONNULL_ANNOT = EP_FL_NAMESPACE + ":CustomNonnullAnnotations"; static final String FL_CTNN_METHOD = EP_FL_NAMESPACE + ":CastToNonNullMethod"; static final String FL_EXTERNAL_INIT_ANNOT = EP_FL_NAMESPACE + ":ExternalInitAnnotations"; static final String FL_CONTRACT_ANNOT = EP_FL_NAMESPACE + ":CustomContractAnnotations"; @@ -152,6 +154,8 @@ final class ErrorProneCLIFlagsConfig extends AbstractConfig { flags, FL_CLASS_ANNOTATIONS_TO_EXCLUDE, DEFAULT_CLASS_ANNOTATIONS_TO_EXCLUDE); initializerAnnotations = getFlagStringSet(flags, FL_INITIALIZER_ANNOT, DEFAULT_INITIALIZER_ANNOT); + customNullableAnnotations = getFlagStringSet(flags, FL_NULLABLE_ANNOT, ImmutableSet.of()); + customNonnullAnnotations = getFlagStringSet(flags, FL_NONNULL_ANNOT, ImmutableSet.of()); externalInitAnnotations = getFlagStringSet(flags, FL_EXTERNAL_INIT_ANNOT, DEFAULT_EXTERNAL_INIT_ANNOT); contractAnnotations = getFlagStringSet(flags, FL_CONTRACT_ANNOT, DEFAULT_CONTRACT_ANNOT); diff --git a/nullaway/src/main/java/com/uber/nullaway/Nullness.java b/nullaway/src/main/java/com/uber/nullaway/Nullness.java index 41423ee..d8a042a 100644 --- a/nullaway/src/main/java/com/uber/nullaway/Nullness.java +++ b/nullaway/src/main/java/com/uber/nullaway/Nullness.java @@ -157,7 +157,8 @@ public enum Nullness implements AbstractValue<Nullness> { // matches javax.annotation.CheckForNull and edu.umd.cs.findbugs.annotations.CheckForNull || annotName.endsWith(".CheckForNull") || (config.acknowledgeAndroidRecent() - && annotName.equals("androidx.annotation.RecentlyNullable")); + && annotName.equals("androidx.annotation.RecentlyNullable")) + || config.isCustomNullableAnnotation(annotName); } /** @@ -171,7 +172,8 @@ public enum Nullness implements AbstractValue<Nullness> { || annotName.endsWith(".NotNull") || annotName.endsWith(".Nonnull") || (config.acknowledgeAndroidRecent() - && annotName.equals("androidx.annotation.RecentlyNonNull")); + && annotName.equals("androidx.annotation.RecentlyNonNull")) + || config.isCustomNonnullAnnotation(annotName); } /** diff --git a/nullaway/src/test/java/com/uber/nullaway/NullAwayTest.java b/nullaway/src/test/java/com/uber/nullaway/NullAwayTest.java index 9c97620..3521f66 100644 --- a/nullaway/src/test/java/com/uber/nullaway/NullAwayTest.java +++ b/nullaway/src/test/java/com/uber/nullaway/NullAwayTest.java @@ -3170,4 +3170,62 @@ public class NullAwayTest { "}") .doTest(); } + + @Test + public void testCustomNullableAnnotation() { + makeTestHelperWithArgs( + Arrays.asList( + "-d", + temporaryFolder.getRoot().getAbsolutePath(), + "-XepOpt:NullAway:AnnotatedPackages=com.uber", + "-XepOpt:NullAway:CustomNullableAnnotations=qual.Null")) + .addSourceLines("qual/Null.java", "package qual;", "public @interface Null {", "}") + .addSourceLines( + "Test.java", + "package com.uber;", + "import qual.Null;", + "class Test {", + " @Null Object foo;", // No error, should detect @Null + " @Null Object baz(){", + " bar(foo);", + " return null;", // No error, should detect @Null + " }", + " String bar(@Null Object item){", + " // BUG: Diagnostic contains: dereferenced expression item is @Nullable", + " return item.toString();", + " }", + "}") + .doTest(); + } + + @Test + public void testCustomNonnullAnnotation() { + makeTestHelperWithArgs( + Arrays.asList( + "-d", + temporaryFolder.getRoot().getAbsolutePath(), + "-XepOpt:NullAway:AnnotatedPackages=com.uber", + "-XepOpt:NullAway:UnannotatedClasses=com.uber.Other", + "-XepOpt:NullAway:CustomNonnullAnnotations=qual.NoNull", + "-XepOpt:NullAway:AcknowledgeRestrictiveAnnotations=true")) + .addSourceLines("qual/NoNull.java", "package qual;", "public @interface NoNull {", "}") + .addSourceLines( + "Other.java", + "package com.uber;", + "import qual.NoNull;", + "public class Other {", + " void bar(@NoNull Object item) { }", + "}") + .addSourceLines( + "Test.java", + "package com.uber;", + "class Test {", + " Other other = new Other();", + " void foo(){", + " // BUG: Diagnostic contains: passing @Nullable parameter 'null'", + " other.bar(null);", + " }", + "}") + .doTest(); + } } |