Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Properly check generic method overriding in explicitly-typed anonymous classes #808

Merged
merged 94 commits into from
Sep 29, 2023
Merged
Show file tree
Hide file tree
Changes from 93 commits
Commits
Show all changes
94 commits
Select commit Hold shift + click to select a range
362ee2f
all changes for handling method overrides
NikitaAware Mar 31, 2023
4a690c2
Merge branch 'master' into try-method-match
msridhar Apr 5, 2023
ac1e7f8
code for pretty printing generic types
msridhar Apr 26, 2023
241793d
better printing of types in errors related to generic types
msridhar Apr 27, 2023
7c86f30
fix compile error on JDK 8
msridhar Apr 27, 2023
899f615
Merge branch 'generic-type-pretty' into try-method-match
msridhar Apr 27, 2023
90b0c69
fix
msridhar Apr 27, 2023
74837a1
cleanup
msridhar Apr 27, 2023
6e3f2d0
better error message
msridhar Apr 27, 2023
56cddbe
WIP
msridhar Apr 27, 2023
b9d698c
Merge branch 'master' into check-toolchains
msridhar Apr 27, 2023
56fb488
test
msridhar Apr 27, 2023
304e567
reorder
msridhar Apr 27, 2023
7381c6e
just windows
msridhar Apr 27, 2023
2689a62
Merge branch 'master' into generic-type-pretty
msridhar Apr 27, 2023
5d7e5af
Merge branch 'check-toolchains' into generic-type-pretty
msridhar Apr 27, 2023
570a03a
Merge branch 'generic-type-pretty' into try-method-match
msridhar Apr 27, 2023
7cdb1b8
Merge branch 'master' into generic-type-pretty
msridhar Apr 27, 2023
a2dbade
Merge branch 'generic-type-pretty' into try-method-match
msridhar Apr 27, 2023
608f3db
tweaks
msridhar Apr 28, 2023
39c6e7d
cleanup
msridhar Apr 28, 2023
ac24db5
more cleanup
msridhar Apr 28, 2023
eb1ecae
cleanup
msridhar Apr 28, 2023
3bbd1d6
docs
msridhar Apr 30, 2023
ee73b04
more
msridhar May 6, 2023
e56cb12
more
msridhar May 6, 2023
05782eb
more
msridhar May 6, 2023
90a4530
more
msridhar May 6, 2023
a769e30
cleanup tests
msridhar May 6, 2023
6630bbf
another rename
msridhar May 6, 2023
b33ff48
WIP
msridhar May 6, 2023
8500957
more tests
msridhar May 7, 2023
250fb68
more
msridhar May 7, 2023
0593341
Make all methods in GenericsChecks static
msridhar May 7, 2023
68adca8
Merge branch 'make-generics-checks-methods-static' into override-anon…
msridhar May 7, 2023
1298081
more, still need tests for conditional expressions as receivers
msridhar May 7, 2023
fb3f418
Merge branch 'master' into try-method-match
msridhar May 8, 2023
e2bd583
Merge branch 'try-method-match' into make-generics-checks-methods-static
msridhar May 8, 2023
40affd0
Merge branch 'make-generics-checks-methods-static' into override-anon…
msridhar May 8, 2023
564c8dc
Merge branch 'master' into try-method-match
msridhar May 14, 2023
662606a
fix nullaway error
msridhar May 14, 2023
3de870c
Merge branch 'master' into try-method-match
msridhar Jun 14, 2023
f7d8dc9
Merge branch 'master' into try-method-match
msridhar Jun 14, 2023
08596f0
fix class ordering in test
msridhar Jun 14, 2023
97ec62a
bug fix: did not handle direct dereference of method call expression …
msridhar Jun 15, 2023
8bca16e
test interactions with @Contract
msridhar Jun 15, 2023
242b2e0
simplify using new API
msridhar Jun 15, 2023
631c417
better variable names
msridhar Jun 15, 2023
9024a67
rename variables and add a TODO
msridhar Jun 15, 2023
be06ff1
add a comment
msridhar Jun 15, 2023
87d194a
Merge branch 'try-method-match' into make-generics-checks-methods-static
msridhar Jun 20, 2023
b364573
Merge branch 'make-generics-checks-methods-static' into override-anon…
msridhar Jun 20, 2023
bb848fa
Merge branch 'master' into try-method-match
msridhar Jun 21, 2023
55ac129
Merge branch 'try-method-match' into make-generics-checks-methods-static
msridhar Jun 21, 2023
58dcd9f
Merge branch 'make-generics-checks-methods-static' into override-anon…
msridhar Jun 21, 2023
1ad0952
bug fix
msridhar Jun 21, 2023
7333529
delete unused method
msridhar Jun 21, 2023
09a2679
failing test
msridhar Jun 21, 2023
d0aa308
fixes
msridhar Jun 22, 2023
12e78ea
big simplification
msridhar Jun 22, 2023
2e36bde
comments
msridhar Jun 22, 2023
83b82fb
Merge branch 'master' into try-method-match
msridhar Jun 23, 2023
bc3b181
Merge branch 'master' into try-method-match
msridhar Jul 8, 2023
254c99a
Merge branch 'try-method-match' into make-generics-checks-methods-static
msridhar Jul 8, 2023
fee899b
Merge branch 'make-generics-checks-methods-static' into override-anon…
msridhar Jul 8, 2023
df25816
Merge branch 'master' into try-method-match
msridhar Jul 15, 2023
9582cff
Merge branch 'master' into try-method-match
msridhar Jul 18, 2023
4afd3d9
Merge branch 'master' into try-method-match
msridhar Aug 11, 2023
2e4a0e7
Merge branch 'try-method-match' into make-generics-checks-methods-static
msridhar Aug 11, 2023
bb94132
Merge branch 'make-generics-checks-methods-static' into override-anon…
msridhar Aug 11, 2023
aa2bc8c
Merge branch 'master' into try-method-match
msridhar Aug 11, 2023
dc1c5fa
Merge branch 'try-method-match' into make-generics-checks-methods-static
msridhar Aug 11, 2023
4f6c05b
Merge branch 'try-method-match' into override-anonymous-class-checking
msridhar Aug 11, 2023
2521dac
Merge branch 'make-generics-checks-methods-static' into override-anon…
msridhar Aug 11, 2023
eefe6e2
Merge branch 'master' into make-generics-checks-methods-static
msridhar Aug 12, 2023
549146c
Merge branch 'make-generics-checks-methods-static' into override-anon…
msridhar Aug 12, 2023
033b081
add test case
msridhar Aug 13, 2023
f7e0a87
Merge branch 'master' into override-anonymous-class-checking
msridhar Aug 18, 2023
f266006
fix comment
msridhar Aug 18, 2023
9a981dc
exclude test
msridhar Aug 18, 2023
3633f7b
Merge branch 'master' into override-anonymous-class-checking
msridhar Aug 21, 2023
d1a50bf
Merge branch 'master' into override-anonymous-class-checking
msridhar Aug 24, 2023
cfdda5f
Merge branch 'master' into override-anonymous-class-checking
msridhar Aug 25, 2023
ef854c4
Merge branch 'master' into override-anonymous-class-checking
msridhar Aug 30, 2023
ed5a7bc
Merge branch 'master' into override-anonymous-class-checking
msridhar Aug 30, 2023
b6c7081
Merge branch 'override-anonymous-class-checking' of https://github.co…
msridhar Aug 30, 2023
df4d0f7
Merge branch 'master' into override-anonymous-class-checking
msridhar Sep 1, 2023
bcc16c4
Merge branch 'master' into override-anonymous-class-checking
msridhar Sep 1, 2023
2721787
Merge branch 'master' into override-anonymous-class-checking
msridhar Sep 7, 2023
73255af
Merge branch 'master' into override-anonymous-class-checking
msridhar Sep 28, 2023
778447e
test more cases
msridhar Sep 28, 2023
af6ca1f
check that type is in fact a supertype
msridhar Sep 28, 2023
16eb9c2
add tests
msridhar Sep 29, 2023
26085c4
link issue and add static import
msridhar Sep 29, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions nullaway/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ def jdk8Test = tasks.register("testJdk8", Test) {
testClassesDirs = testTask.testClassesDirs
jvmArgs "-Xbootclasspath/p:${configurations.errorproneJavac.asPath}"
filter {
// JDK 8 does not support diamonds on anonymous classes
excludeTestsMatching "com.uber.nullaway.NullAwayJSpecifyGenericsTests.overrideDiamondAnonymousClass"
// tests cannot run on JDK 8 since Mockito version no longer supports it
excludeTestsMatching "com.uber.nullaway.NullAwaySerializationTest.initializationError"
excludeTestsMatching "com.uber.nullaway.handlers.contract.ContractUtilsTest.getEmptyAntecedent"
Expand Down
82 changes: 76 additions & 6 deletions nullaway/src/main/java/com/uber/nullaway/GenericsChecks.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import static java.util.stream.Collectors.joining;

import com.google.common.base.Preconditions;
import com.google.common.base.Verify;
import com.google.errorprone.VisitorState;
import com.google.errorprone.suppliers.Supplier;
import com.google.errorprone.suppliers.Suppliers;
Expand All @@ -22,6 +23,7 @@
import com.sun.source.tree.Tree;
import com.sun.source.tree.VariableTree;
import com.sun.source.util.SimpleTreeVisitor;
import com.sun.source.util.TreePath;
import com.sun.tools.javac.code.Attribute;
import com.sun.tools.javac.code.BoundKind;
import com.sun.tools.javac.code.Symbol;
Expand Down Expand Up @@ -686,14 +688,53 @@
* String}.
*
* @param method the generic method
* @param enclosingType the enclosing type in which we want to know {@code method}'s return type
* nullability
* @param enclosingSymbol the enclosing class in which we want to know {@code method}'s return
* type nullability
* @param state Visitor state
* @param config The analysis config
* @return nullability of the return type of {@code method} in the context of {@code
* enclosingType}
*/
public static Nullness getGenericMethodReturnTypeNullness(
Symbol.MethodSymbol method, Symbol enclosingSymbol, VisitorState state, Config config) {
Type enclosingType = getTypeForSymbol(enclosingSymbol, state);
if (enclosingType == null) {
// we have no additional information from generics, so return NONNULL (presence of a @Nullable
// annotation should have been handled by the caller)
return Nullness.NONNULL;
}
return getGenericMethodReturnTypeNullness(method, enclosingType, state, config);
}

/**
* Get the type for the symbol, accounting for anonymous classes
*
* @param symbol the symbol
* @param state the visitor state
* @return the type for {@code symbol}
*/
@Nullable
private static Type getTypeForSymbol(Symbol symbol, VisitorState state) {
if (symbol.isAnonymous()) {
// For anonymous classes, symbol.type does not contain annotations on generic type parameters.
// So, we get a correct type from the enclosing NewClassTree.
TreePath path = state.getPath();
NewClassTree newClassTree = ASTHelpers.findEnclosingNode(path, NewClassTree.class);
if (newClassTree == null) {
throw new RuntimeException(
"method should be inside a NewClassTree " + state.getSourceForNode(path.getLeaf()));

Check warning on line 725 in nullaway/src/main/java/com/uber/nullaway/GenericsChecks.java

View check run for this annotation

Codecov / codecov/patch

nullaway/src/main/java/com/uber/nullaway/GenericsChecks.java#L724-L725

Added lines #L724 - L725 were not covered by tests
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want a check that the NewClassTree is for the same type as symbol['s supertype]? Or can that be safely assumed to never break?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a test case for when the class name is given as something other than a simple name? new Foo.Builder<@Nullable String>() { ... } or even new Foo<@Nullable String>.Builder() {...}?

Copy link
Collaborator Author

@msridhar msridhar Sep 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want a check that the NewClassTree is for the same type as symbol['s supertype]? Or can that be safely assumed to never break?

I added a check in af6ca1f; can't hurt :-)

Can we add a test case for when the class name is given as something other than a simple name? new Foo.Builder<@Nullable String>() { ... } or even new Foo<@Nullable String>.Builder() {...}?

I added some tests in 16eb9c2 but one of them is failing (see overrideAnonymousNestedClass()) 😐 I propose I fix that issue in a follow-up; does that sound ok?

Type typeFromTree = getTreeType(newClassTree, state);
if (typeFromTree != null) {
Verify.verify(state.getTypes().isAssignable(symbol.type, typeFromTree));
}
return typeFromTree;
} else {
return symbol.type;
}
}

private static Nullness getGenericMethodReturnTypeNullness(
Symbol.MethodSymbol method, Type enclosingType, VisitorState state, Config config) {
Type overriddenMethodType = state.getTypes().memberType(enclosingType, method);
if (!(overriddenMethodType instanceof Type.MethodType)) {
Expand Down Expand Up @@ -743,7 +784,7 @@
}
Type methodReceiverType =
castToNonNull(
ASTHelpers.getType(((MemberSelectTree) tree.getMethodSelect()).getExpression()));
getTreeType(((MemberSelectTree) tree.getMethodSelect()).getExpression(), state));
return getGenericMethodReturnTypeNullness(
invokedMethodSymbol, methodReceiverType, state, config);
}
Expand Down Expand Up @@ -791,11 +832,11 @@
if (!(tree.getMethodSelect() instanceof MemberSelectTree)) {
return Nullness.NONNULL;
}
Type methodReceiverType =
Type enclosingType =
castToNonNull(
ASTHelpers.getType(((MemberSelectTree) tree.getMethodSelect()).getExpression()));
getTreeType(((MemberSelectTree) tree.getMethodSelect()).getExpression(), state));
return getGenericMethodParameterNullness(
paramIndex, invokedMethodSymbol, methodReceiverType, state, config);
paramIndex, invokedMethodSymbol, enclosingType, state, config);
}

/**
Expand All @@ -822,6 +863,35 @@
*
* @param parameterIndex index of the parameter
* @param method the generic method
* @param enclosingSymbol the enclosing symbol in which we want to know {@code method}'s parameter
* type nullability
* @param state the visitor state
* @param config the config
* @return nullability of the relevant parameter type of {@code method} in the context of {@code
* enclosingSymbol}
*/
public static Nullness getGenericMethodParameterNullness(
int parameterIndex,
Symbol.MethodSymbol method,
Symbol enclosingSymbol,
VisitorState state,
Config config) {
Type enclosingType = getTypeForSymbol(enclosingSymbol, state);
if (enclosingType == null) {
// we have no additional information from generics, so return NONNULL (presence of a @Nullable
// annotation should have been handled by the caller)
return Nullness.NONNULL;
}
return getGenericMethodParameterNullness(parameterIndex, method, enclosingType, state, config);
}

/**
* Just like {@link #getGenericMethodParameterNullness(int, Symbol.MethodSymbol, Symbol,
* VisitorState, Config)}, but takes the enclosing {@code Type} rather than the enclosing {@code
* Symbol}.
*
* @param parameterIndex index of the parameter
* @param method the generic method
* @param enclosingType the enclosing type in which we want to know {@code method}'s parameter
* type nullability
* @param state the visitor state
Expand Down
8 changes: 4 additions & 4 deletions nullaway/src/main/java/com/uber/nullaway/NullAway.java
Original file line number Diff line number Diff line change
Expand Up @@ -738,7 +738,7 @@ private Description checkParamOverriding(
? GenericsChecks.getGenericMethodParameterNullness(
i,
overriddenMethod,
overridingParamSymbols.get(i).owner.owner.type,
overridingParamSymbols.get(i).owner.owner,
state,
config)
: Nullness.NONNULL);
Expand Down Expand Up @@ -944,7 +944,7 @@ private Description checkOverriding(
// if the super method returns nonnull, overriding method better not return nullable
// Note that, for the overriding method, the permissive default is non-null,
// but it's nullable for the overridden one.
if (overriddenMethodReturnsNonNull(overriddenMethod, overridingMethod.owner.type, state)
if (overriddenMethodReturnsNonNull(overriddenMethod, overridingMethod.owner, state)
&& getMethodReturnNullness(overridingMethod, state, Nullness.NONNULL)
.equals(Nullness.NULLABLE)
&& (memberReferenceTree == null
Expand Down Expand Up @@ -983,7 +983,7 @@ && getMethodReturnNullness(overridingMethod, state, Nullness.NONNULL)
}

private boolean overriddenMethodReturnsNonNull(
Symbol.MethodSymbol overriddenMethod, Type overridingMethodType, VisitorState state) {
Symbol.MethodSymbol overriddenMethod, Symbol enclosingSymbol, VisitorState state) {
Nullness methodReturnNullness =
getMethodReturnNullness(overriddenMethod, state, Nullness.NULLABLE);
if (!methodReturnNullness.equals(Nullness.NONNULL)) {
Expand All @@ -993,7 +993,7 @@ private boolean overriddenMethodReturnsNonNull(
// using the type parameters from the type enclosing the overriding method
if (config.isJSpecifyMode()) {
return GenericsChecks.getGenericMethodReturnTypeNullness(
overriddenMethod, overridingMethodType, state, config)
overriddenMethod, enclosingSymbol, state, config)
.equals(Nullness.NONNULL);
}
return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.google.errorprone.CompilationTestHelper;
import java.util.Arrays;
import org.junit.Ignore;
import org.junit.Test;

public class NullAwayJSpecifyGenericsTests extends NullAwayTestsBase {
Expand Down Expand Up @@ -974,6 +975,163 @@ public void overrideParameterType() {
.doTest();
}

@Test
public void overrideExplicitlyTypedAnonymousClass() {
makeHelper()
.addSourceLines(
"Test.java",
"package com.uber;",
"import org.jspecify.annotations.Nullable;",
"class Test {",
" interface Fn<P extends @Nullable Object, R extends @Nullable Object> {",
" R apply(P p);",
" }",
" static abstract class FnClass<P extends @Nullable Object, R extends @Nullable Object> {",
" abstract R apply(P p);",
" }",
" static void anonymousClasses() {",
" Fn<@Nullable String, String> fn1 = new Fn<@Nullable String, String>() {",
" // BUG: Diagnostic contains: parameter s is @NonNull, but parameter in superclass method",
" public String apply(String s) { return s; }",
" };",
" FnClass<String, String> fn2 = new FnClass<String, String>() {",
" // BUG: Diagnostic contains: method returns @Nullable, but superclass method",
" public @Nullable String apply(String s) { return null; }",
" };",
" Fn<String, @Nullable String> fn3 = new Fn<String, @Nullable String>() {",
" public @Nullable String apply(String s) { return null; }",
" };",
" FnClass<@Nullable String, String> fn4 = new FnClass<@Nullable String, String>() {",
" public String apply(@Nullable String s) { return \"hello\"; }",
" };",
" }",
" static void anonymousClassesFullName() {",
" Test.Fn<@Nullable String, String> fn1 = new Test.Fn<@Nullable String, String>() {",
" // BUG: Diagnostic contains: parameter s is @NonNull, but parameter in superclass method",
" public String apply(String s) { return s; }",
" };",
" Test.FnClass<String, String> fn2 = new Test.FnClass<String, String>() {",
" // BUG: Diagnostic contains: method returns @Nullable, but superclass method",
" public @Nullable String apply(String s) { return null; }",
" };",
" Test.Fn<String, @Nullable String> fn3 = new Test.Fn<String, @Nullable String>() {",
" public @Nullable String apply(String s) { return null; }",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are both true positive cases on the parameter and both true negatives on the return value? Seems like it might test more combinations to do one each?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

" };",
" Test.FnClass<@Nullable String, String> fn4 = new Test.FnClass<@Nullable String, String>() {",
" public String apply(@Nullable String s) { return \"hello\"; }",
" };",
" }",
"}")
.doTest();
}

@Ignore("Need to add support for this case")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's create and link an issue!

@Test
public void overrideAnonymousNestedClass() {
makeHelper()
.addSourceLines(
"Test.java",
"package com.uber;",
"import org.jspecify.annotations.Nullable;",
"class Test {",
" class Wrapper<P extends @Nullable Object> {",
" abstract class Fn<R extends @Nullable Object> {",
" abstract R apply(P p);",
" }",
" }",
" void anonymousNestedClasses() {",
" Wrapper<@Nullable String>.Fn<String> fn1 = (this.new Wrapper<@Nullable String>()).new Fn<String>() {",
" // BUG: Diagnostic contains: parameter s is @NonNull, but parameter in superclass method",
" public String apply(String s) { return s; }",
" };",
" }",
"}")
.doTest();
}

@Test
public void explicitlyTypedAnonymousClassAsReceiver() {
makeHelper()
.addSourceLines(
"Test.java",
"package com.uber;",
"import org.jspecify.annotations.Nullable;",
"class Test {",
" interface Fn<P extends @Nullable Object, R extends @Nullable Object> {",
" R apply(P p);",
" }",
" static abstract class FnClass<P extends @Nullable Object, R extends @Nullable Object> {",
" abstract R apply(P p);",
" }",
" static void anonymousClasses() {",
" String s1 = (new Fn<String, @Nullable String>() {",
" public @Nullable String apply(String s) { return null; }",
" }).apply(\"hi\");",
" // BUG: Diagnostic contains: dereferenced expression s1",
" s1.hashCode();",
" String s2 = (new FnClass<String, @Nullable String>() {",
" public @Nullable String apply(String s) { return null; }",
" }).apply(\"hi\");",
" // BUG: Diagnostic contains: dereferenced expression s2",
" s2.hashCode();",
" (new Fn<String, String>() {",
" public String apply(String s) { return \"hi\"; }",
" // BUG: Diagnostic contains: passing @Nullable parameter",
" }).apply(null);",
" (new FnClass<String, String>() {",
" public String apply(String s) { return \"hi\"; }",
" // BUG: Diagnostic contains: passing @Nullable parameter",
" }).apply(null);",
" (new Fn<@Nullable String, String>() {",
" public String apply(@Nullable String s) { return \"hi\"; }",
" }).apply(null);",
" (new FnClass<@Nullable String, String>() {",
" public String apply(@Nullable String s) { return \"hi\"; }",
" }).apply(null);",
" }",
"}")
.doTest();
}

/** Diamond anonymous classes are not supported yet; tests are for future reference */
@Test
public void overrideDiamondAnonymousClass() {
makeHelper()
.addSourceLines(
"Test.java",
"package com.uber;",
"import org.jspecify.annotations.Nullable;",
"class Test {",
" interface Fn<P extends @Nullable Object, R extends @Nullable Object> {",
" R apply(P p);",
" }",
" static abstract class FnClass<P extends @Nullable Object, R extends @Nullable Object> {",
" abstract R apply(P p);",
" }",
" static void anonymousClasses() {",
" Fn<@Nullable String, String> fn1 = new Fn<>() {",
" // TODO: should report a bug here",
" public String apply(String s) { return s; }",
" };",
" FnClass<@Nullable String, String> fn2 = new FnClass<>() {",
" // TODO: should report a bug here",
" public String apply(String s) { return s; }",
" };",
" Fn<String, @Nullable String> fn3 = new Fn<>() {",
" // TODO: this is a false positive",
" // BUG: Diagnostic contains: method returns @Nullable, but superclass method",
" public @Nullable String apply(String s) { return null; }",
" };",
" FnClass<String, @Nullable String> fn4 = new FnClass<>() {",
" // TODO: this is a false positive",
" // BUG: Diagnostic contains: method returns @Nullable, but superclass method",
" public @Nullable String apply(String s) { return null; }",
" };",
" }",
"}")
.doTest();
}

@Test
public void nullableGenericTypeVariableReturnType() {
makeHelper()
Expand Down