Skip to content

Commit

Permalink
Initial support for JSpecify's @NullMarked annotation. (#493)
Browse files Browse the repository at this point in the history
Supports `@NullMarked` on top-level or inner classes as defined in the current JSpecify specification, either in source code or bytecode

Co-authored-by: Manu Sridharan <msridhar@gmail.com>
  • Loading branch information
lazaroclapp and msridhar authored Feb 22, 2022
1 parent 243f512 commit 85d93c2
Show file tree
Hide file tree
Showing 18 changed files with 763 additions and 108 deletions.
1 change: 1 addition & 0 deletions gradle/dependencies.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ def build = [
checkerDataflow : "org.checkerframework:dataflow-nullaway:${versions.checkerFramework}",
guava : "com.google.guava:guava:24.1.1-jre",
javaxValidation : "javax.validation:validation-api:2.0.1.Final",
jspecify : "org.jspecify:jspecify:0.2.0",
jsr305Annotations : "com.google.code.findbugs:jsr305:3.0.2",
commonsIO : "commons-io:commons-io:2.4",
wala : ["com.ibm.wala:com.ibm.wala.util:${versions.wala}",
Expand Down
1 change: 1 addition & 0 deletions nullaway/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ dependencies {
testImplementation deps.test.junit5Jupiter
testImplementation deps.test.cfQual
testImplementation deps.test.cfCompatQual
testImplementation deps.build.jspecify
testImplementation project(":test-java-lib")
testImplementation deps.test.inferAnnotations
testImplementation deps.apt.jakartaInject
Expand Down
18 changes: 12 additions & 6 deletions nullaway/src/main/java/com/uber/nullaway/AbstractConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,18 @@ protected static Pattern getPackagePattern(Set<String> packagePrefixes) {
}

@Override
public boolean fromAnnotatedPackage(Symbol.ClassSymbol symbol) {
String className = symbol.getQualifiedName().toString();
return annotatedPackages.matcher(className).matches()
&& !unannotatedSubPackages.matcher(className).matches()
&& (!treatGeneratedAsUnannotated
|| !ASTHelpers.hasDirectAnnotationWithSimpleName(symbol, "Generated"));
public boolean fromExplicitlyAnnotatedPackage(String className) {
return annotatedPackages.matcher(className).matches();
}

@Override
public boolean fromExplicitlyUnannotatedPackage(String className) {
return unannotatedSubPackages.matcher(className).matches();
}

@Override
public boolean shouldTreatGeneratedAsUnannotated() {
return treatGeneratedAsUnannotated;
}

@Override
Expand Down
199 changes: 199 additions & 0 deletions nullaway/src/main/java/com/uber/nullaway/ClassAnnotationInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
/*
* Copyright (c) 2017-2022 Uber Technologies, Inc.
*
* 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 com.uber.nullaway;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.errorprone.util.ASTHelpers;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.util.Context;

/**
* Provides APIs for querying whether code is annotated for nullness checking, and for related
* queries on what annotations are present on a class and/or enclosing classes. Makes use of caching
* internally for performance.
*/
public final class ClassAnnotationInfo {

private static final Context.Key<ClassAnnotationInfo> ANNOTATION_INFO_KEY = new Context.Key<>();

private static final int MAX_CACHE_SIZE = 200;

private final Cache<Symbol.ClassSymbol, CacheRecord> cache =
CacheBuilder.newBuilder().maximumSize(MAX_CACHE_SIZE).build();

private ClassAnnotationInfo() {}

/**
* Get the ClassAnnotationInfo for the given javac context. We ensure there is one instance per
* context (as opposed to using static fields) to avoid memory leaks.
*/
public static ClassAnnotationInfo instance(Context context) {
ClassAnnotationInfo annotationInfo = context.get(ANNOTATION_INFO_KEY);
if (annotationInfo == null) {
annotationInfo = new ClassAnnotationInfo();
context.put(ANNOTATION_INFO_KEY, annotationInfo);
}
return annotationInfo;
}

/**
* Checks if a symbol comes from an annotated package, as determined by either configuration flags
* (e.g. {@code -XepOpt:NullAway::AnnotatedPackages}) or package level annotations (e.g. {@code
* org.jspecify.nullness.NullMarked}).
*
* @param outermostClassSymbol symbol for class (must be an outermost class)
* @param config NullAway config
* @return true if the class is from a package that should be treated as properly annotated
* according to our convention (every possibly null parameter / return / field
* annotated @Nullable), false otherwise
*/
private static boolean fromAnnotatedPackage(
Symbol.ClassSymbol outermostClassSymbol, Config config) {
final String className = outermostClassSymbol.getQualifiedName().toString();
if (!config.fromExplicitlyAnnotatedPackage(className)
&& !ASTHelpers.hasDirectAnnotationWithSimpleName(
outermostClassSymbol.packge(), NullabilityUtil.NULLMARKED_SIMPLE_NAME)) {
// By default, unknown code is unannotated unless @NullMarked or configured as annotated by
// package name
return false;
}
if (config.fromExplicitlyUnannotatedPackage(className)) {
// Any code explicitly marked as unannotated in our configuration is unannotated, no matter
// what.
return false;
}
// Finally, if we are here, the code was marked as annotated (either by configuration or
// @NullMarked) and nothing overrides it.
return true;
}

/**
* Check if a symbol comes from generated code.
*
* @param symbol symbol for entity
* @return true if symbol represents an entity contained in a class annotated with
* {@code @Generated}; false otherwise
*/
public boolean isGenerated(Symbol symbol, Config config) {
Symbol.ClassSymbol outermostClassSymbol =
get(ASTHelpers.enclosingClass(symbol), config).outermostClassSymbol;
return ASTHelpers.hasDirectAnnotationWithSimpleName(outermostClassSymbol, "Generated");
}

/**
* Check if a symbol comes from unannotated code.
*
* @param symbol symbol for entity
* @param config NullAway config
* @return true if symbol represents an entity contained in a class that is unannotated; false
* otherwise
*/
public boolean isSymbolUnannotated(Symbol symbol, Config config) {
return !get(ASTHelpers.enclosingClass(symbol), config).isNullnessAnnotated;
}

/**
* Check whether a class should be treated as nullness-annotated.
*
* @param classSymbol The symbol for the class to be checked
* @return Whether this class should be treated as null-annotated, taking into account annotations
* on enclosing classes, the containing package, and other NullAway configuration like
* annotated packages
*/
public boolean isClassNullAnnotated(Symbol.ClassSymbol classSymbol, Config config) {
return get(classSymbol, config).isNullnessAnnotated;
}

/**
* Retrieve the (outermostClass, isNullMarked) record for a given class symbol.
*
* <p>This method is recursive, using the cache on the way up and populating it on the way down.
*
* @param classSymbol The class to query, possibly an inner class
* @return A record including the outermost class in which the given class is nested, as well as
* boolean flag noting whether it should be treated as nullness-annotated, taking into account
* annotations on enclosing classes, the containing package, and other NullAway configuration
* like annotated packages
*/
private CacheRecord get(Symbol.ClassSymbol classSymbol, Config config) {
CacheRecord record = cache.getIfPresent(classSymbol);
if (record != null) {
return record;
}
if (classSymbol.getNestingKind().isNested()) {
Symbol.ClassSymbol enclosingClass = ASTHelpers.enclosingClass(classSymbol);
// enclosingSymbol can be null in weird cases like for array methods
if (enclosingClass != null) {
CacheRecord recordForEnclosing = get(enclosingClass, config);
record =
new CacheRecord(
recordForEnclosing.outermostClassSymbol,
recordForEnclosing.isNullnessAnnotated
|| ASTHelpers.hasDirectAnnotationWithSimpleName(
classSymbol, NullabilityUtil.NULLMARKED_SIMPLE_NAME));
}
}
if (record == null) {
// We are already at the outermost class (we can find), so let's create a record for it
record = new CacheRecord(classSymbol, isAnnotatedTopLevelClass(classSymbol, config));
}
cache.put(classSymbol, record);
return record;
}

private boolean isAnnotatedTopLevelClass(Symbol.ClassSymbol classSymbol, Config config) {
// first, check if the class has a @NullMarked annotation or comes from an annotated package
if ((ASTHelpers.hasDirectAnnotationWithSimpleName(
classSymbol, NullabilityUtil.NULLMARKED_SIMPLE_NAME)
|| fromAnnotatedPackage(classSymbol, config))) {
if (config.shouldTreatGeneratedAsUnannotated()
&& ASTHelpers.hasDirectAnnotationWithSimpleName(classSymbol, "Generated")) {
// Generated code is or isn't excluded, depending on configuration
// Note: In the future, we might want finer grain controls to distinguish code that is
// generated with nullability info and without.
return false;
}
// make sure it's not explicitly configured as unannotated
return !config.isUnannotatedClass(classSymbol);
}
return false;
}

/**
* Immutable record holding the outermost class symbol and the nullness-annotated state for a
* given (possibly inner) class.
*
* <p>The class being referenced by the record is not represented by this object, but rather the
* key used to retrieve it.
*/
private static final class CacheRecord {
public final Symbol.ClassSymbol outermostClassSymbol;
public final boolean isNullnessAnnotated;

public CacheRecord(Symbol.ClassSymbol outermostClassSymbol, boolean isAnnotated) {
this.outermostClassSymbol = outermostClassSymbol;
this.isNullnessAnnotated = isAnnotated;
}
}
}
30 changes: 24 additions & 6 deletions nullaway/src/main/java/com/uber/nullaway/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,32 @@ public interface Config {
FixSerializationConfig getSerializationConfig();

/**
* Checks if a symbol comes from an annotated package.
* Checks if a class comes from an explicitly annotated package.
*
* @param symbol symbol for class
* @return true if the class is from a package that should be treated as properly annotated
* according to our convention (every possibly null parameter / return / field
* annotated @Nullable), false otherwise
* @param className fully qualified name for class
* @return true if the class is from a package that is explicitly configured to be treated as
* properly annotated according to our convention (every possibly null parameter / return /
* field annotated @Nullable), false otherwise
*/
boolean fromExplicitlyAnnotatedPackage(String className);

/**
* Checks if a class comes from an explicitly unannotated (sub-)package.
*
* @param className fully qualified name for class
* @return true if the class is from a package that is explicitly configured to be treated as
* unannotated (even if it is a subpackage of a package configured to be explicitly annotated
* or if it's marked @NullMarked), false otherwise
*/
boolean fromExplicitlyUnannotatedPackage(String className);

/**
* Checks if generated code should be considered always unannoatated.
*
* @return true if code marked as generated code should be treated as unannotated, even if it
* comes from a package otherwise configured as annotated.
*/
boolean fromAnnotatedPackage(Symbol.ClassSymbol symbol);
boolean shouldTreatGeneratedAsUnannotated();

/**
* Checks if a class should be excluded.
Expand Down
16 changes: 13 additions & 3 deletions nullaway/src/main/java/com/uber/nullaway/DummyOptionsConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,17 +55,27 @@ public class DummyOptionsConfig implements Config {
public DummyOptionsConfig() {}

@Override
public boolean serializationIsActive() {
public boolean fromExplicitlyAnnotatedPackage(String className) {
throw new IllegalStateException(ERROR_MESSAGE);
}

@Override
public FixSerializationConfig getSerializationConfig() {
public boolean fromExplicitlyUnannotatedPackage(String className) {
throw new IllegalStateException(ERROR_MESSAGE);
}

@Override
public boolean shouldTreatGeneratedAsUnannotated() {
throw new IllegalStateException(ERROR_MESSAGE);
}

@Override
public boolean fromAnnotatedPackage(Symbol.ClassSymbol symbol) {
public boolean serializationIsActive() {
throw new IllegalStateException(ERROR_MESSAGE);
}

@Override
public FixSerializationConfig getSerializationConfig() {
throw new IllegalStateException(ERROR_MESSAGE);
}

Expand Down
Loading

0 comments on commit 85d93c2

Please sign in to comment.