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
2 changes: 1 addition & 1 deletion docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ Removed method `InitializationParentAnnotatedTypeFactory.createUnderInitializati

**Closed issues:**

eisop#1247, eisop#1263, eisop#1310, eisop#1326, typetools#7096, eisop#1448, eisop#1543.
eisop#433, eisop#1247, eisop#1263, eisop#1310, eisop#1326, typetools#7096, eisop#1448, eisop#1543.


Version 3.49.5 (June 30, 2025)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,19 @@ protected AnnotatedTypeMirror combineAnnotationWithType(
receiverAnnotation, extractAnnotationMirror(ant));
ant.replaceAnnotation(resultAnnotation);
return ant;
} else if (declared.getKind() == TypeKind.INTERSECTION) {
AnnotatedTypeMirror.AnnotatedIntersectionType intersection =
(AnnotatedTypeMirror.AnnotatedIntersectionType) declared.shallowCopy(true);
List<AnnotatedTypeMirror> listBounds = intersection.getBounds();
List<AnnotatedTypeMirror> listBoundsCopy = new ArrayList<>(listBounds);
for (int i = 0; i < listBoundsCopy.size(); i++) {
AnnotatedTypeMirror bound = listBoundsCopy.get(i);
AnnotatedTypeMirror combinedBound =
combineAnnotationWithType(receiverAnnotation, bound);
listBoundsCopy.set(i, combinedBound);
}
intersection.setBounds(listBoundsCopy);
Comment on lines +371 to +381
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

AnnotatedIntersectionType.shallowCopy(true) copies primary annotations via addAnnotations, and AnnotatedIntersectionType.addAnnotation propagates them to the (shared) bounds list. Because shallowCopy also shares bounds (type.bounds = this.bounds), this can mutate the original declared bounds, and the returned intersection's primary annotations can become inconsistent after you replace bounds with viewpoint-adapted ones. To keep this method side-effect free and consistent, construct the copy without copying annotations (e.g., shallowCopy(false) or deepCopy(false)), adapt bounds, set them, then recompute primary annotations from the new bounds (e.g., clearAnnotations() + copyIntersectionBoundAnnotations()).

Suggested change
AnnotatedTypeMirror.AnnotatedIntersectionType intersection =
(AnnotatedTypeMirror.AnnotatedIntersectionType) declared.shallowCopy(true);
List<AnnotatedTypeMirror> listBounds = intersection.getBounds();
List<AnnotatedTypeMirror> listBoundsCopy = new ArrayList<>(listBounds);
for (int i = 0; i < listBoundsCopy.size(); i++) {
AnnotatedTypeMirror bound = listBoundsCopy.get(i);
AnnotatedTypeMirror combinedBound =
combineAnnotationWithType(receiverAnnotation, bound);
listBoundsCopy.set(i, combinedBound);
}
intersection.setBounds(listBoundsCopy);
AnnotatedTypeMirror.AnnotatedIntersectionType declaredIntersection =
(AnnotatedTypeMirror.AnnotatedIntersectionType) declared;
AnnotatedTypeMirror.AnnotatedIntersectionType intersection =
(AnnotatedTypeMirror.AnnotatedIntersectionType)
declaredIntersection.shallowCopy(/* copyAnnotations= */ false);
List<AnnotatedTypeMirror> listBounds = declaredIntersection.getBounds();
List<AnnotatedTypeMirror> listBoundsCopy = new ArrayList<>(listBounds.size());
for (AnnotatedTypeMirror bound : listBounds) {
AnnotatedTypeMirror combinedBound =
combineAnnotationWithType(receiverAnnotation, bound);
listBoundsCopy.add(combinedBound);
}
intersection.setBounds(listBoundsCopy);
intersection.clearAnnotations();
intersection.copyIntersectionBoundAnnotations();

Copilot uses AI. Check for mistakes.
return intersection;
} else {
throw new BugInCF(
"ViewpointAdapter::combineAnnotationWithType: Unknown decl: "
Expand Down Expand Up @@ -451,6 +464,18 @@ 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) {
AnnotatedTypeMirror.AnnotatedIntersectionType intersection =
(AnnotatedTypeMirror.AnnotatedIntersectionType) rhs.shallowCopy(true);
List<AnnotatedTypeMirror> listBounds = intersection.getBounds();
List<AnnotatedTypeMirror> listBoundsCopy = new ArrayList<>(listBounds);
for (int i = 0; i < listBoundsCopy.size(); i++) {
AnnotatedTypeMirror bound = listBoundsCopy.get(i);
AnnotatedTypeMirror substBound = substituteTVars(lhs, bound);
listBoundsCopy.set(i, substBound);
}
intersection.setBounds(listBoundsCopy);
rhs = intersection;
Comment on lines +468 to +478
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

substituteTVars is documented as side-effect free, but rhs.shallowCopy(true) on an intersection shares its bounds list and copies annotations in a way that propagates to those shared bounds, potentially mutating the original rhs. Prefer creating the intersection copy without copying annotations (or use deepCopy(true)), then substitute bounds and (if needed) ensure the intersection's primary annotations remain consistent with its bounds.

Copilot uses AI. Check for mistakes.
} else {
throw new BugInCF(
"ViewpointAdapter::substituteTVars: Cannot handle rhs: "
Expand Down
33 changes: 33 additions & 0 deletions framework/tests/viewpointtest/IntersectionTypes.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Test case for EISOP Issue 433:
// https://github.com/eisop/checker-framework/issues/433

import viewpointtest.quals.*;

public class IntersectionTypes {

interface Foo {}

interface Bar {}

class Baz implements Foo, Bar {}

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

void foo() {
// :: warning: (cast.unsafe.constructor.invocation)
Baz baz = new @A Baz();
call(baz);
}

interface B<X> {}

interface C<X> {}

abstract class D<X extends B<X> & C<X>> {}

class BC implements B<BC>, C<BC> {}

class E extends D<BC> {}

<T extends B<T> & C<T>> void call(T p) {}
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

This file defines call(T p) twice (line 14 and again at line 32) with identical erasure/signature; Java will reject this as a duplicate method declaration. Rename one of the methods (or change its parameter list) so the test compiles and can exercise both intersection-type scenarios.

Suggested change
<T extends B<T> & C<T>> void call(T p) {}
<T extends B<T> & C<T>> void callBC(T p) {}

Copilot uses AI. Check for mistakes.
}
Loading