Skip to content

Commit

Permalink
fix: Subtyping relationships for arrays (#5466)
Browse files Browse the repository at this point in the history
  • Loading branch information
I-Al-Istannen authored Sep 27, 2023
1 parent 412529b commit e777ea7
Show file tree
Hide file tree
Showing 3 changed files with 214 additions and 32 deletions.
72 changes: 61 additions & 11 deletions src/main/java/spoon/support/adaption/TypeAdaptor.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,7 @@ public TypeAdaptor(CtType<?> hierarchyStart) {
* @param hierarchyStart the start of the hierarchy
*/
public TypeAdaptor(CtTypeReference<?> hierarchyStart) {
CtTypeReference<?> usedHierarchyStart = hierarchyStart;
if (hierarchyStart instanceof CtArrayTypeReference) {
usedHierarchyStart = ((CtArrayTypeReference<?>) hierarchyStart).getArrayType();
}
this.hierarchyStartReference = usedHierarchyStart;
this.hierarchyStartReference = hierarchyStart;
this.hierarchyStart = hierarchyStartReference.getTypeDeclaration();
this.initializedWithReference = true;
}
Expand Down Expand Up @@ -103,6 +99,12 @@ public boolean isSubtypeOf(CtTypeReference<?> superRef) {
if (useLegacyTypeAdaption(superRef)) {
return getOldClassTypingContext().isSubtypeOf(superRef);
}
// This check is above the hierarchy start, as we can not build CtTypes for arrays of source-only types.
// Therefore, the hierarchyStart might be null for them.
if (hierarchyStartReference instanceof CtArrayTypeReference<?>) {
return handleArraySubtyping((CtArrayTypeReference<?>) hierarchyStartReference, superRef);
}

if (hierarchyStart == null) {
// We have no declaration, so we can't really do any subtype queries. This happens when the constructor was
// called with a type reference to a class not on the classpath. Any subtype relationships of that class are
Expand All @@ -114,12 +116,37 @@ public boolean isSubtypeOf(CtTypeReference<?> superRef) {
if (!subtype) {
return false;
}
// No generics -> All good, no further analysis needed, we can say they are subtypes
if (hierarchyStartReference.getActualTypeArguments().isEmpty() && superRef.getActualTypeArguments().isEmpty()) {
return true;
}
// Generics? We need to check for subtyping relationships between wildcard and other parameters
// (co/contra variance). Delegate.
return new ClassTypingContext(hierarchyStartReference).isSubtypeOf(superRef);
}

/**
* We need to special case array references, as we can not build a type declaration for them.
* Array types do not exist in the source and no CtType can be built for them (unless they are shadow types).
*
* @param start the start reference
* @param superRef the potential supertype
* @return true if start is a subtype of superRef
*/
private static boolean handleArraySubtyping(CtArrayTypeReference<?> start, CtTypeReference<?> superRef) {
// array-array subtyping
if (superRef instanceof CtArrayTypeReference) {
CtTypeReference<?> superInner = ((CtArrayTypeReference<?>) superRef).getComponentType();
return new TypeAdaptor(start.getComponentType()).isSubtypeOf(superInner);
}
// array-normal subtyping
// https://docs.oracle.com/javase/specs/jls/se21/html/jls-4.html#jls-4.10.3
String superRefQualName = superRef.getQualifiedName();
return superRefQualName.equals("java.lang.Object")
|| superRefQualName.equals("java.io.Serializable")
|| superRefQualName.equals("java.lang.Cloneable");
}

/**
* @return the context of this type adaptor
*/
Expand Down Expand Up @@ -150,11 +177,31 @@ public static boolean isSubtype(CtType<?> base, CtTypeReference<?> superRef) {
if (useLegacyTypeAdaption(base)) {
return new TypeAdaptor(base).isSubtypeOf(superRef);
}

// Handle shadow array types, as we can build a CtType for any Class, which includes arrays. We need to lower
// them to their innermost component type and then decide subtyping based on their relationship.
if (base.isArray() && superRef instanceof CtArrayTypeReference<?>) {
if (!base.isShadow()) {
throw new SpoonException("There are no source level array type declarations");
}
// Peel off one layer at a time. Slow, but easy to maintain. Can be optimized to directly peel of
// min(a.dim, b.dim) when necessary.
Class<?> actualClass = base.getActualClass();
return isSubtype(
base.getFactory().Type().get(actualClass.getComponentType()),
((CtArrayTypeReference<?>) superRef).getComponentType()
);
}
// Note that we have handle T[] < Object/Serializable/Cloneable by using a shadow type as `base`, which will
// implement the correct interfaces as read by reflection.

String superRefFqn = superRef.getTypeErasure().getQualifiedName();

return superRef.getQualifiedName().equals("java.lang.Object")
|| base.getQualifiedName().equals(superRefFqn)
|| supertypeReachableInInheritanceTree(base, superRefFqn);
if (superRef.getQualifiedName().equals("java.lang.Object") || base.getQualifiedName().equals(superRefFqn)) {
return true;
}

return supertypeReachableInInheritanceTree(base, superRefFqn);
}

/**
Expand Down Expand Up @@ -560,8 +607,11 @@ private Optional<CtExecutable<?>> getDeclaringMethodOrConstructor(CtTypeReferenc
}

@SuppressWarnings("AssignmentToMethodParameter")
private DeclarationNode buildHierarchyFrom(CtTypeReference<?> startReference, CtType<?> startType,
CtTypeReference<?> end) {
private DeclarationNode buildHierarchyFrom(
CtTypeReference<?> startReference,
CtType<?> startType,
CtTypeReference<?> end
) {
CtType<?> endType = findDeclaringType(end);
Map<CtTypeReference<?>, DeclarationNode> declarationNodes = new HashMap<>();

Expand Down Expand Up @@ -610,7 +660,7 @@ private CtType<?> moveStartTypeToEnclosingClass(CtType<?> start, CtTypeReference
current = current.getDeclaringType();
}
throw new SpoonException(
"Did not find a suitable enclosing type to start parameter type adaption from"
"Did not find a suitable enclosing type to start parameter type adaption from"
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,17 +97,12 @@ public <T, R extends CtType<T>> R scan(Class<T> clazz) {
// The shadow factory should not be modified in other places and nobody should be directly calling
// the visit methods.
synchronized (factory) {
CtPackage ctPackage;
CtType<?> ctEnclosingClass;
if (clazz.getEnclosingClass() != null && !clazz.isAnonymousClass()) {
ctEnclosingClass = factory.Type().get(clazz.getEnclosingClass());
return ctEnclosingClass.getNestedType(clazz.getSimpleName());
} else {
if (clazz.getPackage() == null) {
ctPackage = factory.Package().getRootPackage();
} else {
ctPackage = factory.Package().getOrCreate(clazz.getPackage().getName());
}
CtPackage ctPackage = getCtPackage(clazz);
if (contexts.isEmpty()) {
enter(new PackageRuntimeBuilderContext(ctPackage));
}
Expand Down Expand Up @@ -141,6 +136,24 @@ public <T, R extends CtType<T>> R scan(Class<T> clazz) {
}
}

private <T> CtPackage getCtPackage(Class<T> clazz) {
Package javaPackage = clazz.getPackage();
if (javaPackage == null && clazz.isArray()) {
javaPackage = getArrayType(clazz).getPackage();
}
if (javaPackage == null) {
return factory.Package().getRootPackage();
}
return factory.Package().getOrCreate(javaPackage.getName());
}

private static Class<?> getArrayType(Class<?> array) {
if (array.isArray()) {
return getArrayType(array.getComponentType());
}
return array;
}

@Override
public void visitPackage(Package aPackage) {
CtPackage ctPackage = factory.Package().get(aPackage.getName());
Expand Down
149 changes: 134 additions & 15 deletions src/test/java/spoon/support/TypeAdaptorTest.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package spoon.support;

import java.util.stream.Collectors;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
Expand All @@ -12,13 +11,17 @@
import spoon.reflect.declaration.CtType;
import spoon.reflect.declaration.CtTypeParameter;
import spoon.reflect.factory.Factory;
import spoon.reflect.reference.CtArrayTypeReference;
import spoon.reflect.reference.CtTypeReference;
import spoon.reflect.visitor.filter.TypeFilter;
import spoon.support.adaption.TypeAdaptor;
import spoon.support.compiler.VirtualFile;
import spoon.testing.utils.GitHubIssue;
import spoon.testing.utils.ModelTest;

import java.io.Serializable;
import java.util.List;
import java.util.stream.Collectors;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
Expand Down Expand Up @@ -428,7 +431,8 @@ void testOverridenOverloadingWithMethodDeclaredParameter(Factory factory) {
private static abstract class GenericThrowsParent {
public abstract <E extends Throwable> void orElseThrow(E throwable) throws E;

public <T extends String> void overloaded(T t) {}
public <T extends String> void overloaded(T t) {
}

public abstract <T extends String> void overriden(T t);
}
Expand All @@ -439,9 +443,11 @@ public <E extends Throwable> void orElseThrow(E throwable) throws E {
throw throwable;
}

public <T extends CharSequence> void overloaded(T t) {}
public <T extends CharSequence> void overloaded(T t) {
}

public <T extends String> void overriden(T t) {}
public <T extends String> void overriden(T t) {
}
}

@Test
Expand All @@ -451,21 +457,21 @@ void testAdaptingTypeFromEnclosingClass() {
launcher.getEnvironment().setComplianceLevel(11);
launcher.addInputResource("src/test/java/spoon/support/TypeAdaptorTest.java");
CtType<?> type = launcher.getFactory()
.Type()
.get(UseGenericFromEnclosingType.class);
.Type()
.get(UseGenericFromEnclosingType.class);
@SuppressWarnings("rawtypes")
List<CtMethod> methods = type.getElements(new TypeFilter<>(CtMethod.class))
.stream()
.filter(it -> it.getSimpleName().equals("someMethod"))
.collect(Collectors.toList());
.stream()
.filter(it -> it.getSimpleName().equals("someMethod"))
.collect(Collectors.toList());
CtMethod<?> test1Method = methods.stream()
.filter(it -> !it.getDeclaringType().getSimpleName().startsWith("Extends"))
.findAny()
.orElseThrow();
.filter(it -> !it.getDeclaringType().getSimpleName().startsWith("Extends"))
.findAny()
.orElseThrow();
CtMethod<?> test2Method = methods.stream()
.filter(it -> it.getDeclaringType().getSimpleName().startsWith("Extends"))
.findAny()
.orElseThrow();
.filter(it -> it.getDeclaringType().getSimpleName().startsWith("Extends"))
.findAny()
.orElseThrow();

assertTrue(test2Method.isOverriding(test1Method));
assertFalse(test1Method.isOverriding(test2Method));
Expand Down Expand Up @@ -493,4 +499,117 @@ void someMethod(Integer s, String t) {
}
}
}

@Test
@GitHubIssue(issueNumber = 5462, fixed = true)
void testArraySubtypingShadow() {
// contract: Array subtyping should hold for shadow types
Factory factory = new Launcher().getFactory();
CtTypeReference<Integer> intType = factory.Type().INTEGER;
CtArrayTypeReference<?> intArr = factory.createArrayReference(intType, 1);
CtArrayTypeReference<?> intArr2 = factory.createArrayReference(intType, 2);

CtTypeReference<Number> numType = factory.createCtTypeReference(Number.class);
CtArrayTypeReference<?> numArr = factory.createArrayReference(numType, 1);
CtArrayTypeReference<?> numArr2 = factory.createArrayReference(numType, 2);

CtArrayTypeReference<?> objArr = factory.createArrayReference(factory.Type().objectType(), 1);

verifySubtype(intArr, numArr, true);
verifySubtype(intArr2, numArr2, true);

verifySubtype(intArr, numArr2, false);
verifySubtype(intArr, numType, false);
verifySubtype(intArr, intType, false);
verifySubtype(numArr, numType, false);
verifySubtype(numArr, intType, false);
verifySubtype(intArr2, numType, false);
verifySubtype(intArr2, intType, false);
verifySubtype(numArr2, numType, false);
verifySubtype(numArr2, intType, false);

// int[][] < Object[], as int[] < Object
verifySubtype(intArr2, objArr, true);
verifySubtype(numArr2, objArr, true);
}

@Test
@GitHubIssue(issueNumber = 5462, fixed = true)
void testArraySubtypingNoShadow() {
// contract: Array subtyping should hold for non-shadow types
Launcher launcher = new Launcher();
launcher.getEnvironment().setNoClasspath(false);
launcher.getEnvironment().setShouldCompile(true);
launcher.addInputResource(new VirtualFile(
"public class Foo {}",
"Foo.java"
));
launcher.addInputResource(new VirtualFile(
"public class Bar extends Foo {}",
"Bar.java"
));
launcher.buildModel();
Factory factory = launcher.getFactory();

CtTypeReference<?> barType = factory.Type().get("Bar").getReference();
CtArrayTypeReference<?> barArr = factory.createArrayReference(barType, 1);
CtArrayTypeReference<?> barArr2 = factory.createArrayReference(barType, 2);

CtTypeReference<?> fooType = factory.Type().get("Foo").getReference();
CtArrayTypeReference<?> fooArr = factory.createArrayReference(fooType, 1);
CtArrayTypeReference<?> fooArr2 = factory.createArrayReference(fooType, 2);

verifySubtype(barArr, fooArr, true);
verifySubtype(barArr2, fooArr2, true);

verifySubtype(barArr, fooArr2, false);
verifySubtype(barArr, fooType, false);
verifySubtype(barArr, barType, false);
}

@Test
@GitHubIssue(issueNumber = 5462, fixed = true)
void testArraySubtypingInbuiltTypes() {
// contract: Array subtyping should hold for inbuilt types (Cloneable, Serializable, Object)
Launcher launcher = new Launcher();
launcher.getEnvironment().setNoClasspath(false);
launcher.getEnvironment().setShouldCompile(true);
launcher.addInputResource(new VirtualFile(
"public class Foo {}",
"Foo.java"
));
CtTypeReference<?> fooType = launcher.buildModel().getAllTypes().iterator().next().getReference();
Factory factory = launcher.getFactory();

CtArrayTypeReference<?> fooArray = factory.createArrayReference(fooType, 1);
CtArrayTypeReference<?> intArray = factory.createArrayReference(factory.Type().INTEGER_PRIMITIVE, 1);
CtArrayTypeReference<?> objArray = factory.createArrayReference(factory.Type().objectType(), 1);

verifySubtype(fooArray, factory.createCtTypeReference(Object.class), true);
verifySubtype(fooArray, factory.createCtTypeReference(Cloneable.class), true);
verifySubtype(fooArray, factory.createCtTypeReference(Serializable.class), true);

verifySubtype(intArray, factory.createCtTypeReference(Object.class), true);
verifySubtype(intArray, factory.createCtTypeReference(Cloneable.class), true);
verifySubtype(intArray, factory.createCtTypeReference(Serializable.class), true);

verifySubtype(objArray, factory.createCtTypeReference(Object.class), true);
verifySubtype(objArray, factory.createCtTypeReference(Cloneable.class), true);
verifySubtype(objArray, factory.createCtTypeReference(Serializable.class), true);
}

private static void verifySubtype(CtTypeReference<?> bottom, CtTypeReference<?> top, boolean shouldSubtype) {
String message = bottom + (shouldSubtype ? " < " : " !< ") + top;
boolean resultReference = bottom.isSubtypeOf(top);
boolean resultAdaptor = new TypeAdaptor(bottom).isSubtypeOf(top);
assertEquals(shouldSubtype, resultReference, message);
assertEquals(shouldSubtype, resultAdaptor, message);

// There are no type declarations for source level arrays
if (bottom.getTypeDeclaration() != null) {
boolean resultAdaptorTwo = TypeAdaptor.isSubtype(bottom.getTypeDeclaration(), top);
assertEquals(shouldSubtype, resultAdaptorTwo, message);
}

}
}

0 comments on commit e777ea7

Please sign in to comment.