From 1322f7002b67141c8adb748019669ce5bbdf6ce3 Mon Sep 17 00:00:00 2001 From: Aosen Xiong Date: Sat, 4 Jul 2026 17:37:11 -0400 Subject: [PATCH 1/3] Use adapted bounds for method type arguments --- .../common/basetype/BaseTypeVisitor.java | 3 +- .../framework/type/AnnotatedTypeFactory.java | 20 ++++++ .../MethodTypeVariableBounds.java | 72 +++++++++++++++++++ 3 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 framework/tests/viewpointtest/MethodTypeVariableBounds.java diff --git a/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java b/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java index ac667319c2f1..482e6cda0f5f 100644 --- a/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java +++ b/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java @@ -2236,8 +2236,7 @@ public Void visitMethodInvocation(MethodInvocationTree tree, Void p) { List typeargs = mType.typeArgs; List paramBounds = - CollectionsPlume.mapList( - AnnotatedTypeVariable::getBounds, invokedMethod.getTypeVariables()); + atypeFactory.methodTypeVariablesFromUse(tree, invokedMethod); ExecutableElement method = invokedMethod.getElement(); CharSequence methodName = ElementUtils.getSimpleDescription(method); diff --git a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java index 350c59635874..ea38aa9b15e9 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java @@ -2398,6 +2398,26 @@ public List typeVariablesFromUse( return res; } + /** + * Returns the method type parameter bounds adapted to the viewpoint of a method invocation. + * + * @param tree a method invocation + * @param invokedMethod the type of the invoked method + * @return the adapted method type parameter bounds + */ + public List methodTypeVariablesFromUse( + MethodInvocationTree tree, AnnotatedExecutableType invokedMethod) { + List bounds = + CollectionsPlume.mapList( + AnnotatedTypeVariable::getBounds, invokedMethod.getTypeVariables()); + + AnnotatedTypeMirror receiverType = getReceiverType(tree); + if (viewpointAdapter != null && receiverType != null) { + viewpointAdapter.viewpointAdaptTypeParameterBounds(receiverType, bounds); + } + return bounds; + } + /** * Creates and returns an AnnotatedNullType qualified with {@code annotations}. * diff --git a/framework/tests/viewpointtest/MethodTypeVariableBounds.java b/framework/tests/viewpointtest/MethodTypeVariableBounds.java new file mode 100644 index 000000000000..e7fe4c948242 --- /dev/null +++ b/framework/tests/viewpointtest/MethodTypeVariableBounds.java @@ -0,0 +1,72 @@ +import viewpointtest.quals.*; + +public class MethodTypeVariableBounds { + static class Methods { + void noArg() {} + + void withArg(T t) {} + } + + void topReceiver( + @Top Methods methods, + @Top Object top, + @A Object a, + @B Object b, + @Bottom Object bottom) { + // @Top viewpoint-adapts @ReceiverDependentQual to @Lost, so only @Bottom is within the + // adapted method type parameter bound. + // :: error: (type.argument.type.incompatible) + methods.noArg(); + + // :: error: (type.argument.type.incompatible) + methods.<@Top Object>withArg(top); + + // :: error: (type.argument.type.incompatible) + methods.<@A Object>withArg(a); + + // :: error: (type.argument.type.incompatible) + methods.<@B Object>withArg(b); + + methods.<@Bottom Object>withArg(bottom); + + // :: error: (type.arguments.not.inferred) + methods.withArg(top); + + // :: error: (type.arguments.not.inferred) + methods.withArg(a); + + // :: error: (type.arguments.not.inferred) + methods.withArg(b); + + methods.withArg(bottom); + } + + void aReceiver( + @A Methods methods, @Top Object top, @A Object a, @B Object b, @Bottom Object bottom) { + // @A viewpoint-adapts @ReceiverDependentQual to @A, so @A and @Bottom are within the + // adapted method type parameter bound. + // :: error: (type.argument.type.incompatible) + methods.noArg(); + + // :: error: (type.argument.type.incompatible) + methods.<@Top Object>withArg(top); + + methods.<@A Object>withArg(a); + + // :: error: (type.argument.type.incompatible) + methods.<@B Object>withArg(b); + + methods.<@Bottom Object>withArg(bottom); + + // :: error: (type.arguments.not.inferred) + methods.withArg(top); + + // :: error: (type.arguments.not.inferred) + methods.withArg(a); + + // :: error: (type.arguments.not.inferred) + methods.withArg(b); + + methods.withArg(bottom); + } +} From f7cbccc3fd3b3098daefb1aa54d929f877c91e2d Mon Sep 17 00:00:00 2001 From: Aosen Xiong Date: Sat, 4 Jul 2026 18:12:51 -0400 Subject: [PATCH 2/3] Reuse method receiver handling for type-variable bounds --- .../common/basetype/BaseTypeVisitor.java | 2 +- .../framework/type/AnnotatedTypeFactory.java | 44 +++++++++++++------ 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java b/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java index 482e6cda0f5f..94e06aa0b048 100644 --- a/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java +++ b/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java @@ -2236,7 +2236,7 @@ public Void visitMethodInvocation(MethodInvocationTree tree, Void p) { List typeargs = mType.typeArgs; List paramBounds = - atypeFactory.methodTypeVariablesFromUse(tree, invokedMethod); + atypeFactory.methodTypeVariableBoundsFromUse(tree, invokedMethod); ExecutableElement method = invokedMethod.getElement(); CharSequence methodName = ElementUtils.getSimpleDescription(method); diff --git a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java index 5ed12f9be703..b42ef74287a3 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java @@ -2399,25 +2399,51 @@ public List typeVariablesFromUse( } /** - * Returns the method type parameter bounds adapted to the viewpoint of a method invocation. + * Returns the method type-variable bounds adapted to the viewpoint of a method invocation. * * @param tree a method invocation * @param invokedMethod the type of the invoked method * @return the adapted method type parameter bounds */ - public List methodTypeVariablesFromUse( + public List methodTypeVariableBoundsFromUse( MethodInvocationTree tree, AnnotatedExecutableType invokedMethod) { List bounds = CollectionsPlume.mapList( AnnotatedTypeVariable::getBounds, invokedMethod.getTypeVariables()); - AnnotatedTypeMirror receiverType = getReceiverType(tree); + AnnotatedTypeMirror receiverType = getMethodReceiverType(tree); if (viewpointAdapter != null && receiverType != null) { viewpointAdapter.viewpointAdaptTypeParameterBounds(receiverType, bounds); } return bounds; } + /** + * Returns the receiver type used to viewpoint-adapt a method invocation. + * + * @param tree a method invocation tree + * @return the receiver type, or null if the invocation has no receiver + */ + private @Nullable AnnotatedTypeMirror getMethodReceiverType(MethodInvocationTree tree) { + ExecutableElement methodElt = TreeUtils.elementFromUse(tree); + if (ElementUtils.isStatic(methodElt)) { + return null; + } + + AnnotatedTypeMirror receiverType = getReceiverType(tree); + if (receiverType == null + && (TreeUtils.isSuperConstructorCall(tree) + || TreeUtils.isThisConstructorCall(tree))) { + // super() and this() calls don't have a receiver, but they should be view-point adapted + // as if "this" is the receiver. + receiverType = getSelfType(tree); + } + if (receiverType != null && receiverType.getKind() == TypeKind.DECLARED) { + receiverType = applyCaptureConversion(receiverType); + } + return receiverType; + } + /** * Creates and returns an AnnotatedNullType qualified with {@code annotations}. * @@ -2753,17 +2779,7 @@ public ParameterizedExecutableType methodFromUseWithoutTypeArgInference( protected ParameterizedExecutableType methodFromUse( MethodInvocationTree tree, boolean inferTypeArgs) { ExecutableElement methodElt = TreeUtils.elementFromUse(tree); - AnnotatedTypeMirror receiverType = getReceiverType(tree); - if (receiverType == null - && (TreeUtils.isSuperConstructorCall(tree) - || TreeUtils.isThisConstructorCall(tree))) { - // super() and this() calls don't have a receiver, but they should be view-point adapted - // as if "this" is the receiver. - receiverType = getSelfType(tree); - } - if (receiverType != null && receiverType.getKind() == TypeKind.DECLARED) { - receiverType = applyCaptureConversion(receiverType); - } + AnnotatedTypeMirror receiverType = getMethodReceiverType(tree); ParameterizedExecutableType result = methodFromUse(tree, methodElt, receiverType, inferTypeArgs); From c2323bca87351c93daf94c1f6618d65455285e5f Mon Sep 17 00:00:00 2001 From: Werner Dietl Date: Thu, 16 Jul 2026 15:14:05 -0400 Subject: [PATCH 3/3] Viewpoint adapt constructor type variable bounds and extract receiver utility methods This commit fixes an issue where constructor type variable bounds were not being viewpoint-adapted correctly, mirroring the fix that was made for method invocations. It extracts 'getConstructorReceiverType' into a dedicated helper method, adds 'constructorTypeVariableBoundsFromUse', and utilizes it in 'BaseTypeVisitor.visitNewClass'. It also improves API usability by making both 'getMethodReceiverType' and 'getConstructorReceiverType' public and cross-referencing them in Javadocs. --- .../common/basetype/BaseTypeVisitor.java | 3 +- .../framework/type/AnnotatedTypeFactory.java | 101 ++++++++++++------ .../ConstructorTypeVariableBounds.java | 19 ++++ 3 files changed, 90 insertions(+), 33 deletions(-) create mode 100644 framework/tests/viewpointtest/ConstructorTypeVariableBounds.java diff --git a/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java b/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java index 94e06aa0b048..bffb22cf5c1a 100644 --- a/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java +++ b/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java @@ -2637,8 +2637,7 @@ public Void visitNewClass(NewClassTree tree, Void p) { checkVarargs(constructorType, tree); List paramBounds = - CollectionsPlume.mapList( - AnnotatedTypeVariable::getBounds, constructorType.getTypeVariables()); + atypeFactory.constructorTypeVariableBoundsFromUse(tree, constructorType); checkTypeArguments( tree, diff --git a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java index 1ddad79b22c3..e2a1b423f29c 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java @@ -2444,13 +2444,77 @@ public List methodTypeVariableBoundsFromUse( return bounds; } + /** + * Returns the constructor type-variable bounds adapted to the viewpoint of a constructor + * invocation. + * + * @param tree a constructor invocation + * @param invokedConstructor the type of the invoked constructor + * @return the adapted constructor type parameter bounds + */ + public List constructorTypeVariableBoundsFromUse( + NewClassTree tree, AnnotatedExecutableType invokedConstructor) { + List bounds = + CollectionsPlume.mapList( + AnnotatedTypeVariable::getBounds, invokedConstructor.getTypeVariables()); + + if (viewpointAdapter != null) { + AnnotatedTypeMirror receiverType = getConstructorReceiverType(tree); + viewpointAdapter.viewpointAdaptTypeParameterBounds(receiverType, bounds); + } + return bounds; + } + + /** + * Returns the receiver type used to viewpoint-adapt a constructor invocation. + * + * @param tree a constructor invocation tree + * @return the receiver type + * @see #getReceiverType(ExpressionTree) + * @see #getMethodReceiverType(MethodInvocationTree) + */ + public AnnotatedDeclaredType getConstructorReceiverType(NewClassTree tree) { + // Get the annotations written on the new class tree. + AnnotatedDeclaredType type = + (AnnotatedDeclaredType) toAnnotatedType(TreeUtils.typeOf(tree), false); + if (!TreeUtils.isDiamondTree(tree)) { + if (tree.getClassBody() == null) { + type.setTypeArguments(getExplicitNewClassClassTypeArgs(tree)); + } + } else { + type = getAnnotatedType(TypesUtils.getTypeElement(type.underlyingType)); + // Add explicit annotations below. + type.clearAnnotations(); + } + + AnnotationMirrorSet explicitAnnos = getExplicitNewClassAnnos(tree); + type.addAnnotations(explicitAnnos); + + // Get the enclosing type of the constructor, if one exists. + // this.new InnerClass() + AnnotatedDeclaredType enclosingType = (AnnotatedDeclaredType) getReceiverType(tree); + if (enclosingType != null && enclosingType.isFrozen()) { + // getReceiverType may return a shared frozen cache value; it is embedded in `type` and + // mutated by addComputedTypeAnnotations below, so copy it first. + enclosingType = enclosingType.deepCopy(); + } + type.setEnclosingType(enclosingType); + + // Add computed annotations to the type. + addComputedTypeAnnotations(tree, type); + + return type; + } + /** * Returns the receiver type used to viewpoint-adapt a method invocation. * * @param tree a method invocation tree * @return the receiver type, or null if the invocation has no receiver + * @see #getReceiverType(ExpressionTree) + * @see #getConstructorReceiverType(NewClassTree) */ - private @Nullable AnnotatedTypeMirror getMethodReceiverType(MethodInvocationTree tree) { + public @Nullable AnnotatedTypeMirror getMethodReceiverType(MethodInvocationTree tree) { ExecutableElement methodElt = TreeUtils.elementFromUse(tree); if (ElementUtils.isStatic(methodElt)) { return null; @@ -2684,6 +2748,8 @@ private boolean isSameType(TypeMirror type1, TypeMirror type2) { * * @param expression the expression for which to determine the receiver type * @return the type of the receiver of expression + * @see #getMethodReceiverType(MethodInvocationTree) + * @see #getConstructorReceiverType(NewClassTree) */ public final @Nullable AnnotatedTypeMirror getReceiverType(ExpressionTree expression) { AnnotatedTypeMirror receiverType; @@ -3533,34 +3599,7 @@ public AnnotatedTypeMirror getResultingTypeOfConstructorMemberReference( */ protected ParameterizedExecutableType constructorFromUse( NewClassTree tree, boolean inferTypeArgs) { - // Get the annotations written on the new class tree. - AnnotatedDeclaredType type = - (AnnotatedDeclaredType) toAnnotatedType(TreeUtils.typeOf(tree), false); - if (!TreeUtils.isDiamondTree(tree)) { - if (tree.getClassBody() == null) { - type.setTypeArguments(getExplicitNewClassClassTypeArgs(tree)); - } - } else { - type = getAnnotatedType(TypesUtils.getTypeElement(type.underlyingType)); - // Add explicit annotations below. - type.clearAnnotations(); - } - - AnnotationMirrorSet explicitAnnos = getExplicitNewClassAnnos(tree); - type.addAnnotations(explicitAnnos); - - // Get the enclosing type of the constructor, if one exists. - // this.new InnerClass() - AnnotatedDeclaredType enclosingType = (AnnotatedDeclaredType) getReceiverType(tree); - if (enclosingType != null && enclosingType.isFrozen()) { - // getReceiverType may return a shared frozen cache value; it is embedded in `type` and - // mutated by addComputedTypeAnnotations below, so copy it first. - enclosingType = enclosingType.deepCopy(); - } - type.setEnclosingType(enclosingType); - - // Add computed annotations to the type. - addComputedTypeAnnotations(tree, type); + AnnotatedDeclaredType type = getConstructorReceiverType(tree); ExecutableElement ctor = TreeUtils.elementFromUse(tree); AnnotatedExecutableType con = getAnnotatedType(ctor); // get unsubstituted type @@ -3659,9 +3698,9 @@ protected ParameterizedExecutableType constructorFromUse( addDefaultAnnotations(returnType); con.setReturnType(returnType); } - if (enclosingType != null) { + if (type.getEnclosingType() != null) { // Reset the enclosing type because it can be substituted incorrectly. - ((AnnotatedDeclaredType) con.getReturnType()).setEnclosingType(enclosingType); + ((AnnotatedDeclaredType) con.getReturnType()).setEnclosingType(type.getEnclosingType()); } if (type.isUnderlyingTypeRaw() || TypesUtils.isRaw(TreeUtils.typeOf(tree))) { ((AnnotatedDeclaredType) con.getReturnType()).setIsUnderlyingTypeRaw(); diff --git a/framework/tests/viewpointtest/ConstructorTypeVariableBounds.java b/framework/tests/viewpointtest/ConstructorTypeVariableBounds.java new file mode 100644 index 000000000000..84c059db0c23 --- /dev/null +++ b/framework/tests/viewpointtest/ConstructorTypeVariableBounds.java @@ -0,0 +1,19 @@ +import viewpointtest.quals.*; + +public class ConstructorTypeVariableBounds { + static class MyClass { + MyClass(T t) {} + } + + void test(@Top Object top, @A Object a, @B Object b, @Bottom Object bottom) { + + // :: error: (type.argument.type.incompatible) :: error: (new.class.type.invalid) + new <@Top Object>@Top MyClass(top); + + // :: warning: (cast.unsafe.constructor.invocation) + new <@A Object>@A MyClass(a); + + // :: warning: (cast.unsafe.constructor.invocation) + new <@B Object>@B MyClass(b); + } +}