Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -553,19 +553,35 @@ private void reportInvalidOverridingMethodParamTypeError(
} else {
result = ASTHelpers.getType(tree);
if (result != null) {
// for method invocations and field reads, there may be annotations on type variables in
// the return / field type that need to be restored
if (tree instanceof MethodInvocationTree invocationTree) {
// for a call to an instance method, we may need to run inference on a nested call

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is nested call here just meaning a chained method call as the receiver? Or are there more complex cases? I know is in the test, but a brief example here would be great :)

// inside the receiver in order to figure out the proper nullability of the receiver's
// type arguments. E.g., for a call `foo(x,y).bar().baz()`, where `foo` is a generic
// method, we need to run inference on the call to `foo` to determine the receiver type
// of the `bar()` call, which could in turn impact the receiver type of the `baz()`
// call. We invoke getEnclosingTypeForCallExpression, which will run
// inference if needed, and then recompute the type as a member of the returned
// enclosing type
Symbol.MethodSymbol symbol = castToNonNull(ASTHelpers.getSymbol(invocationTree));
Type invokedMethodType = symbol.type;
Type enclosingType =
getEnclosingTypeForCallExpression(
symbol, invocationTree, state.getPath(), state, false);
if (enclosingType != null) {
invokedMethodType =
TypeSubstitutionUtils.memberType(state.getTypes(), enclosingType, symbol, config);
}
Type.MethodType methodType =
handler.onOverrideMethodType(symbol, symbol.type.asMethodType(), state);
handler.onOverrideMethodType(symbol, invokedMethodType.asMethodType(), state);
// restore explicit annotations from the return type
Type returnType = methodType.getReturnType();
result =
TypeSubstitutionUtils.restoreExplicitNullabilityAnnotations(
returnType, result, config, Collections.emptyMap());
} else if (tree instanceof MemberSelectTree memberSelectTree) {
Symbol memberSelectSymbol = ASTHelpers.getSymbol(memberSelectTree);
if (memberSelectSymbol != null && memberSelectSymbol.getKind().isField()) {
// restore explicit annotations from the field's declared type
Type fieldType = memberSelectSymbol.type;
result =
TypeSubstitutionUtils.restoreExplicitNullabilityAnnotations(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1648,6 +1648,43 @@ String nonNullElse(@Nullable String nullableString, String defaultValue) {
.doTest();
}

@Test
public void nestedReceivers() {
makeHelperWithInferenceFailureWarning()
.addSourceLines(
"Test.java",
"""
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
class Test {
static class Foo<T extends @Nullable Object> {
static <U extends @Nullable Object> Foo<U> of(Foo<U> other) {
throw new RuntimeException();
}
Foo<T> or(Foo<T> other) { return this; }
}
// infer Foo<@Nullable String> as the type of the Foo.of() call via
// its parameter, and use that type to determine the return type of
// the or() call is Foo<@Nullable String>
static Foo<@Nullable String> FOO =
Foo.of(new Foo<@Nullable String>()).or(new Foo<@Nullable String>());

// like the case above, but more deeply nested
static Foo<@Nullable String> FOO2 =
Foo.of(new Foo<@Nullable String>())
.or(new Foo<@Nullable String>())
.or(new Foo<@Nullable String>());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

A bit surprised we don't have an example here reporting an issue (e.g. where one of the .or(...) methods is passing an incompatible type. We have a few true negative cases, but not the true positive, right?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good point, will add a true positive test


// a true positive case
// BUG: Diagnostic contains: incompatible types: Foo<@Nullable String> cannot be converted to Foo<String>
static Foo<String> WRONG =
Foo.of(new Foo<@Nullable String>()).or(new Foo<@Nullable String>());
}
""")
.doTest();
}

private CompilationTestHelper makeHelper() {
return makeTestHelperWithArgs(
JSpecifyJavacConfig.withJSpecifyModeArgs(
Expand Down
Loading