diff --git a/src/main/java/spoon/support/adaption/TypeAdaptor.java b/src/main/java/spoon/support/adaption/TypeAdaptor.java index 911d4a29d06..7caf4d372bd 100644 --- a/src/main/java/spoon/support/adaption/TypeAdaptor.java +++ b/src/main/java/spoon/support/adaption/TypeAdaptor.java @@ -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; } @@ -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 @@ -114,12 +116,55 @@ 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) { + CtTypeReference startInner = start.getArrayType(); + + // array-array subtyping + if (superRef instanceof CtArrayTypeReference) { + int startArrayDim = getArrayDimension(start.getSimpleName()); + int superArrayDim = getArrayDimension(superRef.getSimpleName()); + // Arrays of different shapes are not subtypes + if (startArrayDim != superArrayDim) { + return false; + } + CtTypeReference superInner = ((CtArrayTypeReference) superRef).getArrayType(); + + if (!isSubtype(startInner.getTypeDeclaration(), superInner)) { + return false; + } + // No generics -> All good, no further analysis needed, we can say they are subtypes + if (startInner.getActualTypeArguments().isEmpty() && superInner.getActualTypeArguments().isEmpty()) { + return true; + } + // Generics? We need to check for subtyping relationships between wildcard and other parameters + // (co/contra variance). Delegate. + return new ClassTypingContext(start).isSubtypeOf(superRef); + } + // 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 */ @@ -150,11 +195,49 @@ 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"); + } + Class actualClass = base.getActualClass(); + // Arrays of different shapes are not subtypes + if (getArrayDimension(actualClass.getSimpleName()) != getArrayDimension(superRef.getSimpleName())) { + return false; + } + superRef = ((CtArrayTypeReference) superRef).getArrayType(); + base = base.getFactory().Type().get(getArrayType(actualClass)); + // We can just carry on here, as T[] > S[] reduces to T > S. + } + // 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); + } + + private static int getArrayDimension(String name) { + int dimension = 0; + for (int i = 0; i < name.length(); i++) { + if (name.codePointAt(i) == '[') { + dimension++; + } + } + return dimension; + } + + private static Class getArrayType(Class array) { + if (array.isArray()) { + return getArrayType(array.getComponentType()); + } + return array; } /** @@ -560,8 +643,11 @@ private Optional> 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, DeclarationNode> declarationNodes = new HashMap<>(); @@ -610,7 +696,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" ); } diff --git a/src/main/java/spoon/support/visitor/java/JavaReflectionTreeBuilder.java b/src/main/java/spoon/support/visitor/java/JavaReflectionTreeBuilder.java index 0d9c32c4233..198966ac8a5 100644 --- a/src/main/java/spoon/support/visitor/java/JavaReflectionTreeBuilder.java +++ b/src/main/java/spoon/support/visitor/java/JavaReflectionTreeBuilder.java @@ -97,17 +97,12 @@ public > R scan(Class 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)); } @@ -141,6 +136,24 @@ public > R scan(Class clazz) { } } + private CtPackage getCtPackage(Class 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()); diff --git a/src/test/java/spoon/support/TypeAdaptorTest.java b/src/test/java/spoon/support/TypeAdaptorTest.java index bab12ea8af5..bd44cedea4a 100644 --- a/src/test/java/spoon/support/TypeAdaptorTest.java +++ b/src/test/java/spoon/support/TypeAdaptorTest.java @@ -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; @@ -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; @@ -428,7 +431,8 @@ void testOverridenOverloadingWithMethodDeclaredParameter(Factory factory) { private static abstract class GenericThrowsParent { public abstract void orElseThrow(E throwable) throws E; - public void overloaded(T t) {} + public void overloaded(T t) { + } public abstract void overriden(T t); } @@ -439,9 +443,11 @@ public void orElseThrow(E throwable) throws E { throw throwable; } - public void overloaded(T t) {} + public void overloaded(T t) { + } - public void overriden(T t) {} + public void overriden(T t) { + } } @Test @@ -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 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)); @@ -493,4 +499,111 @@ 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 intType = factory.Type().INTEGER; + CtArrayTypeReference intArr = factory.createArrayReference(intType, 1); + CtArrayTypeReference intArr2 = factory.createArrayReference(intType, 2); + + CtTypeReference numType = factory.createCtTypeReference(Number.class); + CtArrayTypeReference numArr = factory.createArrayReference(numType, 1); + CtArrayTypeReference numArr2 = factory.createArrayReference(numType, 2); + + 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); + } + + @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); + } + + } }