diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 16baa251edca..c70107b8025f 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -50,6 +50,7 @@ Optional Checker. **Closed issues:** +eisop#433 Version 3.49.5-eisop1 (April 26, 2026) -------------------------------------- diff --git a/framework/src/main/java/org/checkerframework/framework/type/AbstractViewpointAdapter.java b/framework/src/main/java/org/checkerframework/framework/type/AbstractViewpointAdapter.java index 631f35acf096..e26c28cff8aa 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/AbstractViewpointAdapter.java +++ b/framework/src/main/java/org/checkerframework/framework/type/AbstractViewpointAdapter.java @@ -4,6 +4,7 @@ import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedArrayType; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedIntersectionType; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedNullType; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedPrimitiveType; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedTypeVariable; @@ -12,8 +13,10 @@ import org.checkerframework.javacutil.ElementUtils; import org.plumelib.util.IPair; +import java.util.ArrayList; import java.util.IdentityHashMap; import java.util.List; +import java.util.function.Function; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; @@ -363,6 +366,10 @@ protected AnnotatedTypeMirror combineAnnotationWithType( receiverAnnotation, extractAnnotationMirror(ant)); ant.replaceAnnotation(resultAnnotation); return ant; + } else if (declared.getKind() == TypeKind.INTERSECTION) { + return adaptIntersectionBounds( + (AnnotatedIntersectionType) declared, + bound -> combineAnnotationWithType(receiverAnnotation, bound)); } else { throw new BugInCF( "ViewpointAdapter::combineAnnotationWithType: Unknown decl: " @@ -447,6 +454,10 @@ private AnnotatedTypeMirror substituteTVars(AnnotatedTypeMirror lhs, AnnotatedTy rhs = AnnotatedTypeCopierWithReplacement.replace(aat, mappings); } else if (rhs.getKind().isPrimitive() || rhs.getKind() == TypeKind.NULL) { // nothing to do for primitive types and the null type + } else if (rhs.getKind() == TypeKind.INTERSECTION) { + rhs = + adaptIntersectionBounds( + (AnnotatedIntersectionType) rhs, bound -> substituteTVars(lhs, bound)); } else { throw new BugInCF( "ViewpointAdapter::substituteTVars: Cannot handle rhs: " @@ -458,6 +469,30 @@ private AnnotatedTypeMirror substituteTVars(AnnotatedTypeMirror lhs, AnnotatedTy return rhs; } + /** + * Returns a copy of {@code source} whose bounds have been adapted by {@code adaptBound}. + * + * @param source intersection type whose bounds should be adapted + * @param adaptBound function that adapts one bound + * @return a copy of {@code source} with adapted bounds + */ + private AnnotatedIntersectionType adaptIntersectionBounds( + AnnotatedIntersectionType source, + Function adaptBound) { + AnnotatedIntersectionType intersection = source.shallowCopy(/* copyAnnotations= */ false); + List bounds = source.getBounds(); + List adaptedBounds = new ArrayList<>(bounds.size()); + for (AnnotatedTypeMirror bound : bounds) { + adaptedBounds.add(adaptBound.apply(bound)); + } + // First replace the bounds copied by shallowCopy with the adapted bounds. Then clear the + // shallow copy's stale primary annotations and recompute them from the adapted bounds. + intersection.setBounds(adaptedBounds); + intersection.clearAnnotations(); + intersection.copyIntersectionBoundAnnotations(); + return intersection; + } + /** * Return actual type argument for formal type parameter "var" from "type" * diff --git a/framework/tests/all-systems/IntersectionTypeNoCrash.java b/framework/tests/all-systems/IntersectionTypeNoCrash.java new file mode 100644 index 000000000000..051213799c86 --- /dev/null +++ b/framework/tests/all-systems/IntersectionTypeNoCrash.java @@ -0,0 +1,21 @@ +// Test case for EISOP Issue 433: +// https://github.com/eisop/checker-framework/issues/433 + +public class IntersectionTypeNoCrash { + + interface Foo {} + + interface Bar {} + + class Baz implements Foo, Bar {} + + void call(T p) {} + + void test() { + call(new Baz()); + } + + void testCast(Object obj) { + Foo fooAndBar = (Foo & Bar) obj; + } +} diff --git a/framework/tests/viewpointtest/IntersectionTypes.java b/framework/tests/viewpointtest/IntersectionTypes.java new file mode 100644 index 000000000000..f95391477a1e --- /dev/null +++ b/framework/tests/viewpointtest/IntersectionTypes.java @@ -0,0 +1,86 @@ +// Test case for EISOP Issue 433: +// https://github.com/eisop/checker-framework/issues/433 + +import viewpointtest.quals.*; + +public class IntersectionTypes { + + interface Foo {} + + interface Bar {} + + interface Accessor { + @ReceiverDependentQual Object get(); + + void set(@ReceiverDependentQual Object o); + } + + class Baz implements Foo, Bar, Accessor { + @Override + public @ReceiverDependentQual Object get() { + return null; + } + + @Override + public void set(@ReceiverDependentQual Object o) {} + } + + void call(T p) {} + + void callAccessor(T p) {} + + class ViewpointAdaptedIntersectionBound { + void viewpointAdaptedIntersectionBound(@A T p, @A Object aObj, @B Object bObj) { + @A Object adapted = p.get(); + // :: error: (assignment.type.incompatible) + @B Object notAdaptedToB = p.get(); + + p.set(aObj); + // :: error: (argument.type.incompatible) + p.set(bObj); + } + } + + void foo(@A Object aObj, @B Object bObj) { + // :: warning: (cast.unsafe.constructor.invocation) + Baz baz = new @A Baz(); + call(baz); + callAccessor(baz); + + @A Object adapted = baz.get(); + // :: error: (assignment.type.incompatible) + @B Object notAdaptedToB = baz.get(); + + baz.set(aObj); + // :: error: (argument.type.incompatible) + baz.set(bObj); + } + + void intersectionCasts(Object obj) { + Foo fooAndBar = (Foo & Bar) obj; + Accessor fooAndAccessor = (Foo & Accessor) obj; + } + + void annotatedIntersectionCast(@B Object obj) { + // :: warning: (cast.unsafe) + Accessor fooAndAccessor = (@A Foo & Accessor) obj; + } + + interface BType {} + + interface CType {} + + abstract class D & CType> {} + + class BC implements BType, CType {} + + class E extends D {} + + & CType> void callBC(T p) {} + + // Documents the current decision for https://github.com/eisop/checker-framework/issues/1735: + // when multiple bounds in an intersection type have explicit qualifiers in the same hierarchy, + // the later qualifier is ignored. + // :: warning: (explicit.annotation.ignored) + void callAnnotatedBounds(T p) {} +}