Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
ed8e47f
Don't erase the type on the receiver
aosen-xiong Jun 25, 2024
ab59a90
Merge branch 'master' into type-argument
aosen-xiong Jun 25, 2024
0df34f6
Add jtreg test case
aosen-xiong Jun 26, 2024
997dc5d
Merge branch 'master' into type-argument
aosen-xiong Jun 26, 2024
82c9faa
Empty commit for CI
aosen-xiong Jun 27, 2024
e160b61
Delete extra folder and rename the file
aosen-xiong Jun 27, 2024
a5d1fad
Rename the test file and refine the summary
aosen-xiong Jul 3, 2024
a4387a1
Merge branch 'master' into type-argument
aosen-xiong Jul 3, 2024
053340d
Merge branch 'master' into type-argument
aosen-xiong Aug 2, 2024
91dd74f
Merge branch 'master' into type-argument
aosen-xiong Aug 17, 2024
5768f2c
Merge branch 'master' into type-argument
aosen-xiong Aug 19, 2024
c6afb70
Merge branch 'master' into type-argument
aosen-xiong Dec 17, 2024
b7f5767
Subclass receiver test
aosen-xiong Jul 4, 2024
f66d93d
Also fixes 104
aosen-xiong Dec 18, 2024
714b75a
Update expected output
aosen-xiong Dec 18, 2024
0c4519a
Merge branch 'master' into type-argument
wmdietl Dec 27, 2024
f806c1d
Merge branch 'master' into type-argument
wmdietl Jan 5, 2025
c529f0d
Merge branch 'master' into type-argument
aosen-xiong Jan 29, 2025
badbfd3
Merge branch 'master' into type-argument
aosen-xiong Mar 9, 2025
8a67c54
Try the original change
aosen-xiong Mar 9, 2025
c876775
This might not be the most elegant solution
aosen-xiong Mar 9, 2025
bf0e385
Merge branch 'master' into type-argument
aosen-xiong May 19, 2025
64711f7
Merge branch 'master' into type-argument
aosen-xiong May 20, 2025
ce6e91d
Empty commit for CI
aosen-xiong May 20, 2025
287347d
Try not to copy the method receiver
aosen-xiong May 21, 2025
e6e92d4
Remove code no longer need
aosen-xiong May 21, 2025
b554427
Try change from eisop#1078
aosen-xiong May 30, 2025
c63ae6c
Merge branch 'master' into type-argument
aosen-xiong Jun 16, 2025
f22dccf
Revert change
aosen-xiong Jul 17, 2025
b0674e9
Add test case
aosen-xiong Jul 17, 2025
fbaf351
Add avoiding check logic
aosen-xiong Jul 17, 2025
c494a8b
Merge branch 'master' into type-argument
aosen-xiong Jul 17, 2025
9e63567
Fix the logic
aosen-xiong Jul 19, 2025
0fe838e
Add null check
aosen-xiong Jul 20, 2025
86fbbde
Changelog
aosen-xiong Jul 20, 2025
be8c65e
Merge branch 'master' into type-argument
aosen-xiong Jul 20, 2025
c2a2ecf
Merge branch 'master' into type-argument
aosen-xiong Aug 19, 2025
201481b
Merge branch 'master' into type-argument
aosen-xiong Aug 26, 2025
7519918
Merge branch 'master' into type-argument
aosen-xiong Oct 4, 2025
1e3f9ae
Merge branch 'master' into type-argument
aosen-xiong Oct 20, 2025
229bd9b
Merge branch 'master' into type-argument
aosen-xiong Mar 20, 2026
c6f38f7
Merge branch 'master' into type-argument
aosen-xiong Mar 20, 2026
1b33ca6
Merge branch 'master' into type-argument
wmdietl Mar 20, 2026
c020864
Merge branch 'master' into type-argument
aosen-xiong Mar 25, 2026
9a2ef29
Redo comment
aosen-xiong Mar 25, 2026
85137e6
Redo comment
aosen-xiong Mar 25, 2026
1d7655a
Redo the logic and comment
aosen-xiong Mar 25, 2026
6127c1d
Redo comment on the test case
aosen-xiong Mar 25, 2026
d05ccf3
Apply suggestion from @wmdietl
aosen-xiong Mar 25, 2026
bceb0b6
Move the logic to `checkMethodInvocability`
aosen-xiong Mar 25, 2026
5fbc0d7
Wording
aosen-xiong Mar 25, 2026
c88795d
Wording
aosen-xiong Mar 25, 2026
8145d7d
Move the checks to `checkMethodInvocability`
aosen-xiong Mar 25, 2026
307ee12
Update docs/CHANGELOG.md
aosen-xiong Mar 26, 2026
dbcff2d
Add link to the issue
aosen-xiong Mar 26, 2026
70493a1
Rename the variable
aosen-xiong Mar 26, 2026
a37741d
Expand the test cases
aosen-xiong Mar 26, 2026
6d7cb16
Fix closed issues in CHANGELOG.md
aosen-xiong Mar 26, 2026
e0e4461
Merge branch 'master' into type-argument
aosen-xiong Apr 2, 2026
50f67da
Apply suggestions from code review
aosen-xiong Apr 2, 2026
5c0ee2a
Merge branch 'master' into type-argument
wmdietl Apr 5, 2026
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
@@ -0,0 +1,41 @@
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.checker.nullness.qual.PolyNull;
Comment thread
aosen-xiong marked this conversation as resolved.

public class PolyQualifierOnTypeArgument<T> {
void toArray(PolyQualifierOnTypeArgument<@PolyNull T> this) {
// This method has @PolyNull on receiver's type argument.
}

void test(PolyQualifierOnTypeArgument<?> p) {
// When calling toArray on a receiver with an unbounded wildcard type argument, the LUB of
// the annotations on the wildcard's bounds is @Nullable. As a result, the receiver type is
// substituted as PolyQualifierOnTypeArgument<@Nullable T>, which is not compatible with an
// unbounded wildcard type argument.
// If the method receiver type argument subtyping check is enabled, the error is:
// found : @NonNull PolyQualifierOnTypeArgument<? [ extends @Nullable Object super
// @NonNull NullType ]>
// required: @NonNull PolyQualifierOnTypeArgument<capture#01 [ extends @Nullable Object
// super @Nullable NullType ]>
p.toArray();
Comment thread
aosen-xiong marked this conversation as resolved.
}

void test2(PolyQualifierOnTypeArgument<T> p) {
p.toArray(); // same bound mismatch as unbounded wildcard case
}

void test3(PolyQualifierOnTypeArgument<@NonNull T> p) {
p.toArray(); // ok, the LUB is @NonNull, both bounds are @NonNull is compatible with
// @PolyNull T after substitution
}

void test4(PolyQualifierOnTypeArgument<@Nullable Object> p) {
p.toArray(); // ok, the LUB is @Nullable, both bounds are @Nullable is compatible
// with @PolyNull T after substitution
}

void test5(PolyQualifierOnTypeArgument<Object> p) {
p.toArray(); // ok, the LUB is @NonNull, both bounds are @NonNull is compatible with
// @PolyNull T after substitution
}
}
21 changes: 21 additions & 0 deletions checker/tests/nullness/ReceiverTypeArgs.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Test case for issue 104: https://github.com/eisop/checker-framework/issues/104

import org.checkerframework.checker.nullness.qual.NonNull;
Comment thread
aosen-xiong marked this conversation as resolved.
import org.checkerframework.checker.nullness.qual.Nullable;

public class ReceiverTypeArgs {
static class Box<T> {
T item;

public Box(T item) {
this.item = item;
}

void test(Box<@NonNull T> this) {}
}

private static void foo(Box<@Nullable String> box) {
// :: error: (method.invocation.invalid)
box.test();
}
}
5 changes: 4 additions & 1 deletion docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ Version 3.49.5-eisop1 (July ??, 2025)

**User-visible changes:**

The EISOP Checker Framework checks subtyping for receiver's type arguments when invoking a method.
The annotations on type arguments of a method receiver (e.g., `void test(Box<@NonNull T> this)`) were previously ignored during type-checking.

The new command-line option `-AonlyAnnotatedFor` suppresses all type-checking errors and warnings outside the scope of
a corresponding `@AnnotatedFor` annotation.
Note that the `@AnnotatedFor` annotation must include the checker's name to enable warnings from that checker.
Expand Down Expand Up @@ -57,7 +60,7 @@ Removed method `InitializationParentAnnotatedTypeFactory.createUnderInitializati

**Closed issues:**

eisop#1099, eisop#1247, eisop#1263, eisop#1310, eisop#1326, typetools#7096, eisop#1448, eisop#1543.
eisop#104, eisop#1099, eisop#1247, eisop#1263, eisop#1310, eisop#1326, eisop#1448, eisop#1543, typetools#3203, typetools#7096.


Version 3.49.5 (June 30, 2025)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2178,12 +2178,9 @@ public Void visitMethodInvocation(MethodInvocationTree tree, Void p) {
typeCheckVectorCopyIntoArgument(tree, params);
}

ExecutableElement invokedMethodElement = invokedMethod.getElement();
if (!ElementUtils.isStatic(invokedMethodElement)
&& !TreeUtils.isSuperConstructorCall(tree)) {
checkMethodInvocability(invokedMethod, tree);
}
checkMethodInvocability(invokedMethod, tree);

ExecutableElement invokedMethodElement = invokedMethod.getElement();
// check precondition annotations
checkPreconditions(
tree, atypeFactory.getContractsFromMethod().getPreconditions(invokedMethodElement));
Expand Down Expand Up @@ -3970,11 +3967,12 @@ protected boolean skipReceiverSubtypeCheck(
*/
protected void checkMethodInvocability(
AnnotatedExecutableType method, MethodInvocationTree tree) {
if (method.getReceiverType() == null) {
ExecutableElement invokedMethodElement = method.getElement();
if (ElementUtils.isStatic(invokedMethodElement)) {
// Static methods don't have a receiver to check.
return;
}
if (method.getElement().getKind() == ElementKind.CONSTRUCTOR) {
if (invokedMethodElement.getKind() == ElementKind.CONSTRUCTOR) {
// TODO: Explicit "this()" calls of constructors have an implicit passed
// from the enclosing constructor. We must not use the self type, but
// instead should find a way to determine the receiver of the enclosing constructor.
Expand All @@ -3983,20 +3981,35 @@ protected void checkMethodInvocability(
return;
}

AnnotatedTypeMirror methodReceiver = method.getReceiverType();
AnnotatedTypeMirror erasedMethodReceiver = methodReceiver.getErased();
AnnotatedTypeMirror erasedTreeReceiver = erasedMethodReceiver.shallowCopy(false);
AnnotatedTypeMirror treeReceiver = atypeFactory.getReceiverType(tree);
ParameterizedExecutableType methodDefPreSubstitution =
atypeFactory.methodFromUseWithoutTypeArgInference(tree);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why can't this not simply use methodReceiver or treeReceiver that we calculate below? What is different about this receiver? Or maybe you can use invokedMethodElement?
In any case, the variable names are rather non-descriptive, making the distinction invisible.

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.

I renamed the variable to methodDefPreSubstitution.

methodReceiver or method's poly annotation already got substituted. I need the receiver before substitution.

List<AnnotatedTypeMirror> declaredTypeArgs =
methodDefPreSubstitution.executableType.getReceiverType().getTypeArguments();
Comment on lines +3984 to +3987
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

visitMethodInvocation already computes preInference = atypeFactory.methodFromUseWithoutTypeArgInference(tree); checkMethodInvocability recomputes the same value, which duplicates non-trivial work per method call and risks a noticeable performance regression. Consider reusing the already-computed preInference (e.g., pass it in via an overload or refactor the poly-receiver check to avoid a second factory call).

Copilot uses AI. Check for mistakes.
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. But we haven't decide we want to exclude the check when poly is present. Let's leave it for now.

// Don't check when method receiver's type argument has a poly annotation.
Comment thread
aosen-xiong marked this conversation as resolved.
// See checker/tests/nullness-genericwildcard/PolyQualifierOnTypeArgument.java.
for (AnnotationMirror top : qualHierarchy.getTopAnnotations()) {
AnnotationMirror poly = qualHierarchy.getPolymorphicAnnotation(top);
// If this hierarchy does not have a poly annotation, then continue to next hierarchy.
if (poly == null) {
continue;
}
for (AnnotatedTypeMirror typeArg : declaredTypeArgs) {
// If one of the type arguments has a poly annotation, then skip the check.
if (typeArg.hasAnnotation(poly)) {
return;
}
}
}

erasedTreeReceiver.addAnnotations(treeReceiver.getEffectiveAnnotations());
AnnotatedDeclaredType methodReceiver = method.getReceiverType();
AnnotatedTypeMirror treeReceiver = atypeFactory.getReceiverType(tree);

if (!skipReceiverSubtypeCheck(tree, erasedMethodReceiver, treeReceiver)) {
if (!skipReceiverSubtypeCheck(tree, methodReceiver, treeReceiver)) {
// The diagnostic can be a bit misleading because the check is of the receiver but
// `tree` is the entire method invocation (where the receiver might be implicit).
commonAssignmentCheckStartDiagnostic(methodReceiver, erasedTreeReceiver, tree);
boolean success = typeHierarchy.isSubtype(erasedTreeReceiver, erasedMethodReceiver);
commonAssignmentCheckEndDiagnostic(
success, null, methodReceiver, erasedTreeReceiver, tree);
commonAssignmentCheckStartDiagnostic(methodReceiver, treeReceiver, tree);
boolean success = typeHierarchy.isSubtype(treeReceiver, methodReceiver);
commonAssignmentCheckEndDiagnostic(success, null, methodReceiver, treeReceiver, tree);
if (!success) {
// Don't report the erased types because they show up with '</*RAW*/>' as type args.
reportMethodInvocabilityError(tree, treeReceiver, methodReceiver);
Expand Down