Fix Serialization: Split field initialization region into smaller regions (#658)
This PR resolves #657 by splitting the field initialization region into smaller regions. We currently only consider the value "null", for all `errors/fixes` reported in this regions. In large classes which they have many number of field, this can easily create a lot builds as they all have the value "null" for enclosing method causing conflicts in regions.
This PR generalizes the concept of enclosing method to enclosing member, and breaks a single regions of field initialization regions into a separate region for each field declaration and reduce the conflicts in regions dramatically.
The equivalent for enclosing member is described:
The enclosing member for `fix` or `error` is:
- If it is triggered inside a __method__, the enclosing member will be the __enclosing method__.
- If it is enclosed by a __field declaration statement__, the enclosing member will be the __declared field__.
- If __none__ of the above, the enclosing member will be `"null"` (used for __static initialization region__).
With the current approach the following errors will have the values below:
```java
class C {
Field foo1 = Field(null); // Passing nullable, enclMethod = "null"
Field foo2 = null; // assigning nullable, enclMethod = "null"
static Field foo3 3 = null; // assigning nullable, enclMethod = "null"
{
foo3 = null; // assigning nullable, enclMethod = "null"
}
void bar(){
this.foo1 = null; // assigning nullable, enclMethod = "bar()"
}
}
```
From the point of view of Annotator, that is __4__ conflicts in regions as they all have `enclMethod = "null"`.
This can be greatly improved with the new approach suggested and changed to below:
```java
class C {
Field foo1 = Field(null); // Passing nullable, enclMember = "foo1"
Field foo2 = null; // assigning nullable, enclMember = "foo2"
static Field foo3 = null; // assigning nullable, enclMember = "foo3"
{
Field3 = null; // assigning nullable, enclMember = "null"
}
void bar(){
this.foo1 = null; // assigning nullable, enclMember = "bar()"
}
}
```
None of the error reported above have any conflicts and can be allocated to separate regions.
diff --git a/nullaway/src/main/java/com/uber/nullaway/fixserialization/out/ClassAndMemberInfo.java b/nullaway/src/main/java/com/uber/nullaway/fixserialization/out/ClassAndMemberInfo.java
new file mode 100644
index 0000000..ebbd407
--- /dev/null
+++ b/nullaway/src/main/java/com/uber/nullaway/fixserialization/out/ClassAndMemberInfo.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (c) 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.fixserialization.out;
+
+import com.google.errorprone.util.ASTHelpers;
+import com.sun.source.tree.ClassTree;
+import com.sun.source.tree.MethodTree;
+import com.sun.source.tree.VariableTree;
+import com.sun.source.util.TreePath;
+import com.sun.tools.javac.code.Symbol;
+import javax.annotation.Nullable;
+
+/** Class and member corresponding to a program point at which an error / fix was reported. */
+public class ClassAndMemberInfo {
+ /** Path to the program point of the reported error / fix */
+ public final TreePath path;
+
+ // Finding values for these properties is costly and are not needed by default, hence, they are
+ // not final and are only initialized at request.
+ @Nullable private Symbol member;
+
+ @Nullable private ClassTree clazz;
+
+ public ClassAndMemberInfo(TreePath path) {
+ this.path = path;
+ }
+
+ /** Finds the class and member where the error / fix is reported according to {@code path}. */
+ public void findValues() {
+ MethodTree enclosingMethod;
+ // If the error is reported on a method, that method itself is the relevant program point.
+ // Otherwise, use the enclosing method (if present).
+ enclosingMethod =
+ path.getLeaf() instanceof MethodTree
+ ? (MethodTree) path.getLeaf()
+ : ASTHelpers.findEnclosingNode(path, MethodTree.class);
+ // If the error is reported on a class, that class itself is the relevant program point.
+ // Otherwise, use the enclosing class.
+ clazz =
+ path.getLeaf() instanceof ClassTree
+ ? (ClassTree) path.getLeaf()
+ : ASTHelpers.findEnclosingNode(path, ClassTree.class);
+ if (clazz != null) {
+ Symbol.ClassSymbol classSymbol = ASTHelpers.getSymbol(clazz);
+ if (enclosingMethod != null) {
+ // It is possible that the computed method is not enclosed by the computed class, e.g., for
+ // the following case:
+ // class C {
+ // void foo() {
+ // class Local {
+ // Object f = null; // error
+ // }
+ // }
+ // }
+ // Here the above code will compute clazz to be Local and method as foo(). In such cases,
+ // set method to null, we always want the corresponding method to be nested in the
+ // corresponding class.
+ Symbol.MethodSymbol methodSymbol = ASTHelpers.getSymbol(enclosingMethod);
+ if (!methodSymbol.isEnclosedBy(classSymbol)) {
+ enclosingMethod = null;
+ }
+ }
+ if (enclosingMethod != null) {
+ member = ASTHelpers.getSymbol(enclosingMethod);
+ } else {
+ // Node is not enclosed by any method, can be a field declaration or enclosed by it.
+ Symbol sym = ASTHelpers.getSymbol(path.getLeaf());
+ Symbol.VarSymbol fieldSymbol = null;
+ if (sym != null && sym.getKind().isField()) {
+ // Directly on a field declaration.
+ fieldSymbol = (Symbol.VarSymbol) sym;
+ } else {
+ // Can be enclosed by a field declaration tree.
+ VariableTree fieldDeclTree = ASTHelpers.findEnclosingNode(path, VariableTree.class);
+ if (fieldDeclTree != null) {
+ fieldSymbol = ASTHelpers.getSymbol(fieldDeclTree);
+ }
+ }
+ if (fieldSymbol != null && fieldSymbol.isEnclosedBy(classSymbol)) {
+ member = fieldSymbol;
+ }
+ }
+ }
+ }
+
+ @Nullable
+ public Symbol getMember() {
+ return member;
+ }
+
+ @Nullable
+ public ClassTree getClazz() {
+ return clazz;
+ }
+}
diff --git a/nullaway/src/main/java/com/uber/nullaway/fixserialization/out/ClassAndMethodInfo.java b/nullaway/src/main/java/com/uber/nullaway/fixserialization/out/ClassAndMethodInfo.java
deleted file mode 100644
index 2f671db..0000000
--- a/nullaway/src/main/java/com/uber/nullaway/fixserialization/out/ClassAndMethodInfo.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Copyright (c) 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.fixserialization.out;
-
-import com.google.errorprone.util.ASTHelpers;
-import com.sun.source.tree.ClassTree;
-import com.sun.source.tree.MethodTree;
-import com.sun.source.util.TreePath;
-import com.sun.tools.javac.code.Symbol;
-import javax.annotation.Nullable;
-
-/** Class and method corresponding to a program point at which an error was reported. */
-public class ClassAndMethodInfo {
- /** Path to the program point of the reported error */
- public final TreePath path;
-
- /**
- * Finding values for these properties is costly and are not needed by default, hence, they are
- * not {@code final} and are only initialized at request.
- */
- @Nullable private MethodTree method;
-
- @Nullable private ClassTree clazz;
-
- public ClassAndMethodInfo(TreePath path) {
- this.path = path;
- }
-
- /** Finds the class and method where the error is reported according to {@code path}. */
- public void findValues() {
- // If the error is reported on a method, that method itself is the relevant program point.
- // Otherwise, use the enclosing method (if present).
- method =
- path.getLeaf() instanceof MethodTree
- ? (MethodTree) path.getLeaf()
- : ASTHelpers.findEnclosingNode(path, MethodTree.class);
- // If the error is reported on a class, that class itself is the relevant program point.
- // Otherwise, use the enclosing class.
- clazz =
- path.getLeaf() instanceof ClassTree
- ? (ClassTree) path.getLeaf()
- : ASTHelpers.findEnclosingNode(path, ClassTree.class);
- if (clazz != null && method != null) {
- // It is possible that the computed method is not enclosed by the computed class, e.g., for
- // the following case:
- // class C {
- // void foo() {
- // class Local {
- // Object f = null; // error
- // }
- // }
- // }
- // Here the above code will compute clazz to be Local and method as foo(). In such cases, set
- // method to null, we always want the corresponding method to be nested in the corresponding
- // class.
- Symbol.ClassSymbol classSymbol = ASTHelpers.getSymbol(clazz);
- Symbol.MethodSymbol methodSymbol = ASTHelpers.getSymbol(method);
- if (!methodSymbol.isEnclosedBy(classSymbol)) {
- method = null;
- }
- }
- }
-
- @Nullable
- public MethodTree getMethod() {
- return method;
- }
-
- @Nullable
- public ClassTree getClazz() {
- return clazz;
- }
-}
diff --git a/nullaway/src/main/java/com/uber/nullaway/fixserialization/out/ErrorInfo.java b/nullaway/src/main/java/com/uber/nullaway/fixserialization/out/ErrorInfo.java
index 07b00e3..7127b82 100644
--- a/nullaway/src/main/java/com/uber/nullaway/fixserialization/out/ErrorInfo.java
+++ b/nullaway/src/main/java/com/uber/nullaway/fixserialization/out/ErrorInfo.java
@@ -36,7 +36,7 @@
public class ErrorInfo {
private final ErrorMessage errorMessage;
- private final ClassAndMethodInfo classAndMethodInfo;
+ private final ClassAndMemberInfo classAndMemberInfo;
/**
* if non-null, this error involved a pseudo-assignment of a @Nullable expression into a @NonNull
@@ -60,7 +60,7 @@
'\r', 'r');
public ErrorInfo(TreePath path, ErrorMessage errorMessage, @Nullable Symbol nonnullTarget) {
- this.classAndMethodInfo = new ClassAndMethodInfo(path);
+ this.classAndMemberInfo = new ClassAndMemberInfo(path);
this.errorMessage = errorMessage;
this.nonnullTarget = nonnullTarget;
}
@@ -97,20 +97,20 @@
"\t",
errorMessage.getMessageType().toString(),
escapeSpecialCharacters(errorMessage.getMessage()),
- (classAndMethodInfo.getClazz() != null
- ? ASTHelpers.getSymbol(classAndMethodInfo.getClazz()).flatName()
+ (classAndMemberInfo.getClazz() != null
+ ? ASTHelpers.getSymbol(classAndMemberInfo.getClazz()).flatName()
: "null"),
- (classAndMethodInfo.getMethod() != null
- ? ASTHelpers.getSymbol(classAndMethodInfo.getMethod()).toString()
+ (classAndMemberInfo.getMember() != null
+ ? classAndMemberInfo.getMember().toString()
: "null"),
(nonnullTarget != null
? SymbolLocation.createLocationFromSymbol(nonnullTarget).tabSeparatedToString()
: EMPTY_NONNULL_TARGET_LOCATION_STRING));
}
- /** Finds the class and method of program point where the error is reported. */
+ /** Finds the class and member of program point where the error is reported. */
public void initEnclosing() {
- classAndMethodInfo.findValues();
+ classAndMemberInfo.findValues();
}
/**
@@ -125,7 +125,7 @@
"message_type",
"message",
"enc_class",
- "enc_method",
+ "enc_member",
"target_kind",
"target_class",
"target_method",
diff --git a/nullaway/src/main/java/com/uber/nullaway/fixserialization/out/SuggestedNullableFixInfo.java b/nullaway/src/main/java/com/uber/nullaway/fixserialization/out/SuggestedNullableFixInfo.java
index c00a0a7..8232ced 100644
--- a/nullaway/src/main/java/com/uber/nullaway/fixserialization/out/SuggestedNullableFixInfo.java
+++ b/nullaway/src/main/java/com/uber/nullaway/fixserialization/out/SuggestedNullableFixInfo.java
@@ -36,13 +36,13 @@
/** Error which will be resolved by this type change. */
private final ErrorMessage errorMessage;
- private final ClassAndMethodInfo classAndMethodInfo;
+ private final ClassAndMemberInfo classAndMemberInfo;
public SuggestedNullableFixInfo(
TreePath path, SymbolLocation symbolLocation, ErrorMessage errorMessage) {
- this.classAndMethodInfo = new ClassAndMethodInfo(path);
this.symbolLocation = symbolLocation;
this.errorMessage = errorMessage;
+ this.classAndMemberInfo = new ClassAndMemberInfo(path);
}
@Override
@@ -76,17 +76,17 @@
symbolLocation.tabSeparatedToString(),
errorMessage.getMessageType().toString(),
"nullable",
- (classAndMethodInfo.getClazz() == null
+ (classAndMemberInfo.getClazz() == null
? "null"
- : ASTHelpers.getSymbol(classAndMethodInfo.getClazz()).flatName()),
- (classAndMethodInfo.getMethod() == null
+ : ASTHelpers.getSymbol(classAndMemberInfo.getClazz()).flatName()),
+ (classAndMemberInfo.getMember() == null
? "null"
- : ASTHelpers.getSymbol(classAndMethodInfo.getMethod()).toString()));
+ : classAndMemberInfo.getMember().toString()));
}
- /** Finds the class and method of program point where triggered this type change. */
+ /** Finds the class and member of program point where triggered this type change. */
public void initEnclosing() {
- classAndMethodInfo.findValues();
+ classAndMemberInfo.findValues();
}
/**
diff --git a/nullaway/src/test/java/com/uber/nullaway/NullAwaySerializationTest.java b/nullaway/src/test/java/com/uber/nullaway/NullAwaySerializationTest.java
index 019726c..2e49f87 100644
--- a/nullaway/src/test/java/com/uber/nullaway/NullAwaySerializationTest.java
+++ b/nullaway/src/test/java/com/uber/nullaway/NullAwaySerializationTest.java
@@ -1370,6 +1370,105 @@
}
@Test
+ public void errorSerializationTestEnclosedByFieldDeclaration() {
+ SerializationTestHelper<ErrorDisplay> tester = new SerializationTestHelper<>(root);
+ tester
+ .setArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:SerializeFixMetadata=true",
+ "-XepOpt:NullAway:FixSerializationConfigPath=" + configPath))
+ .addSourceLines(
+ "com/uber/Main.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "public class Main {",
+ " // BUG: Diagnostic contains: passing @Nullable parameter",
+ " Foo f = new Foo(null);", // Member should be "f"
+ " // BUG: Diagnostic contains: assigning @Nullable expression",
+ " Foo f1 = null;", // Member should be "f1"
+ " // BUG: Diagnostic contains: assigning @Nullable expression",
+ " static Foo f2 = null;", // Member should be "f2"
+ " static {",
+ " // BUG: Diagnostic contains: assigning @Nullable expression",
+ " f2 = null;", // Member should be "null" (not field or method)
+ " }",
+ " class Inner {",
+ " // BUG: Diagnostic contains: passing @Nullable parameter",
+ " Foo f = new Foo(null);", // Member should be "f" but class is Main$Inner
+ " }",
+ "}")
+ .addSourceLines(
+ "com/uber/Foo.java",
+ "package com.uber;",
+ "public class Foo {",
+ " public Foo(Object foo){",
+ " }",
+ "}")
+ .setExpectedOutputs(
+ new ErrorDisplay(
+ "PASS_NULLABLE",
+ "passing @Nullable parameter",
+ "com.uber.Main",
+ "f",
+ "PARAMETER",
+ "com.uber.Foo",
+ "Foo(java.lang.Object)",
+ "foo",
+ "0",
+ "com/uber/Foo.java"),
+ new ErrorDisplay(
+ "ASSIGN_FIELD_NULLABLE",
+ "assigning @Nullable expression to @NonNull field",
+ "com.uber.Main",
+ "f1",
+ "FIELD",
+ "com.uber.Main",
+ "null",
+ "f1",
+ "null",
+ "com/uber/Main.java"),
+ new ErrorDisplay(
+ "ASSIGN_FIELD_NULLABLE",
+ "assigning @Nullable expression to @NonNull field",
+ "com.uber.Main",
+ "f2",
+ "FIELD",
+ "com.uber.Main",
+ "null",
+ "f2",
+ "null",
+ "com/uber/Main.java"),
+ new ErrorDisplay(
+ "ASSIGN_FIELD_NULLABLE",
+ "assigning @Nullable expression to @NonNull field",
+ "com.uber.Main",
+ "null",
+ "FIELD",
+ "com.uber.Main",
+ "null",
+ "f2",
+ "null",
+ "com/uber/Main.java"),
+ new ErrorDisplay(
+ "PASS_NULLABLE",
+ "passing @Nullable parameter",
+ "com.uber.Main$Inner",
+ "f",
+ "PARAMETER",
+ "com.uber.Foo",
+ "Foo(java.lang.Object)",
+ "foo",
+ "0",
+ "com/uber/Foo.java"))
+ .setFactory(errorDisplayFactory)
+ .setOutputFileNameAndHeader(ERROR_FILE_NAME, ERROR_FILE_HEADER)
+ .doTest();
+ }
+
+ @Test
public void suggestNullableArgumentOnBytecode() {
SerializationTestHelper<FixDisplay> tester = new SerializationTestHelper<>(root);
tester
diff --git a/nullaway/src/test/java/com/uber/nullaway/tools/ErrorDisplay.java b/nullaway/src/test/java/com/uber/nullaway/tools/ErrorDisplay.java
index 0531746..1064823 100644
--- a/nullaway/src/test/java/com/uber/nullaway/tools/ErrorDisplay.java
+++ b/nullaway/src/test/java/com/uber/nullaway/tools/ErrorDisplay.java
@@ -30,7 +30,7 @@
public class ErrorDisplay implements Display {
public final String type;
public final String message;
- public final String encMethod;
+ public final String encMember;
public final String encClass;
public final String kind;
public final String clazz;
@@ -43,7 +43,7 @@
String type,
String message,
String encClass,
- String encMethod,
+ String encMember,
String kind,
String clazz,
String method,
@@ -52,7 +52,7 @@
String uri) {
this.type = type;
this.message = message;
- this.encMethod = encMethod;
+ this.encMember = encMember;
this.encClass = encClass;
this.kind = kind;
this.clazz = clazz;
@@ -63,8 +63,8 @@
this.uri = uri.contains("com/uber/") ? uri.substring(uri.indexOf("com/uber/")) : uri;
}
- public ErrorDisplay(String type, String message, String encClass, String encMethod) {
- this(type, message, encClass, encMethod, "null", "null", "null", "null", "null", "null");
+ public ErrorDisplay(String type, String message, String encClass, String encMember) {
+ this(type, message, encClass, encMember, "null", "null", "null", "null", "null", "null");
}
@Override
@@ -80,10 +80,10 @@
// To increase readability, a shorter version of the actual message might be present in the
// expected output of tests.
&& (message.contains(that.message) || that.message.contains(message))
- && encMethod.equals(that.encMethod)
+ && encMember.equals(that.encMember)
+ && clazz.equals(that.clazz)
&& encClass.equals(that.encClass)
&& kind.equals(that.kind)
- && clazz.equals(that.clazz)
&& method.equals(that.method)
&& variable.equals(that.variable)
&& index.equals(that.index)
@@ -93,7 +93,7 @@
@Override
public int hashCode() {
return Objects.hash(
- type, message, encMethod, encClass, kind, clazz, method, variable, index, uri);
+ type, message, encMember, encClass, kind, clazz, method, variable, index, uri);
}
@Override
@@ -105,8 +105,8 @@
+ "\n\tmessage='"
+ message
+ '\''
- + "\n\tencMethod='"
- + encMethod
+ + "\n\tencMember='"
+ + encMember
+ '\''
+ "\n\tencClass='"
+ encClass