Skip to content
Open
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
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ Optional Checker.

**Closed issues:**

eisop#433

Version 3.49.5-eisop1 (April 26, 2026)
--------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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: "
Expand Down Expand Up @@ -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) {
Comment thread
aosen-xiong marked this conversation as resolved.
rhs =
adaptIntersectionBounds(
(AnnotatedIntersectionType) rhs, bound -> substituteTVars(lhs, bound));
} else {
throw new BugInCF(
"ViewpointAdapter::substituteTVars: Cannot handle rhs: "
Expand All @@ -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<AnnotatedTypeMirror, AnnotatedTypeMirror> adaptBound) {
AnnotatedIntersectionType intersection = source.shallowCopy(/* copyAnnotations= */ false);
List<AnnotatedTypeMirror> bounds = source.getBounds();
List<AnnotatedTypeMirror> 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"
*
Expand Down
21 changes: 21 additions & 0 deletions framework/tests/all-systems/IntersectionTypeNoCrash.java
Original file line number Diff line number Diff line change
@@ -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 {}

<T extends Foo & Bar> void call(T p) {}

void test() {
call(new Baz());
}

void testCast(Object obj) {
Foo fooAndBar = (Foo & Bar) obj;
}
}
86 changes: 86 additions & 0 deletions framework/tests/viewpointtest/IntersectionTypes.java
Original file line number Diff line number Diff line change
@@ -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) {}
}

<T extends Foo & Bar> void call(T p) {}

<T extends Foo & Accessor> void callAccessor(T p) {}

class ViewpointAdaptedIntersectionBound<T extends @ReceiverDependentQual Accessor & Foo> {
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<X> {}

interface CType<X> {}

abstract class D<X extends BType<X> & CType<X>> {}

class BC implements BType<BC>, CType<BC> {}

class E extends D<BC> {}

<T extends BType<T> & CType<T>> 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)
<T extends @A Foo & @B Bar> void callAnnotatedBounds(T p) {}
}
Loading