diff --git a/src/main/java/spoon/reflect/factory/TypeFactory.java b/src/main/java/spoon/reflect/factory/TypeFactory.java index 489b744e4df..4deb37e139c 100644 --- a/src/main/java/spoon/reflect/factory/TypeFactory.java +++ b/src/main/java/spoon/reflect/factory/TypeFactory.java @@ -93,6 +93,7 @@ public class TypeFactory extends SubFactory { public final CtTypeReference SET = createReference(Set.class); public final CtTypeReference MAP = createReference(Map.class); public final CtTypeReference ENUM = createReference(Enum.class); + public final CtTypeReference OMITTED_TYPE_ARG_TYPE = createReference(CtTypeReference.OMITTED_TYPE_ARG_NAME); private final Map, CtType> shadowCache = new ConcurrentHashMap<>(); diff --git a/src/main/java/spoon/reflect/reference/CtTypeReference.java b/src/main/java/spoon/reflect/reference/CtTypeReference.java index a43134de2c1..88313706d67 100644 --- a/src/main/java/spoon/reflect/reference/CtTypeReference.java +++ b/src/main/java/spoon/reflect/reference/CtTypeReference.java @@ -34,6 +34,11 @@ public interface CtTypeReference extends CtReference, CtActualTypeContainer, */ String NULL_TYPE_NAME = ""; + /** + * Special type used as a type argument when actual type arguments can't be inferred. + */ + String OMITTED_TYPE_ARG_NAME = ""; + /** * Returns the simple (unqualified) name of this element. * Following the compilation convention, if the type is a local type, diff --git a/src/main/java/spoon/support/compiler/jdt/ReferenceBuilder.java b/src/main/java/spoon/support/compiler/jdt/ReferenceBuilder.java index a02bb01bc81..488b44945f3 100644 --- a/src/main/java/spoon/support/compiler/jdt/ReferenceBuilder.java +++ b/src/main/java/spoon/support/compiler/jdt/ReferenceBuilder.java @@ -86,6 +86,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Deque; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -569,6 +570,47 @@ private void insertGenericTypesInNoClasspathFromJDTInSpoon(TypeReference ori } } } + + if (original.isParameterizedTypeReference() && !type.isParameterized()) { + tryRecoverTypeArguments(type); + } + } + + /** + * In noclasspath mode, empty diamonds in constructor calls on generic types can be lost. This happens if any + * of the following apply: + * + *
    + *
  • The generic type is not on the classpath.
  • + *
  • The generic type is used in a context where the type arguments cannot be inferred, such as in an + * unresolved method + *
  • + *
+ * + * See #3360 for details. + */ + private void tryRecoverTypeArguments(CtTypeReference type) { + final Deque stack = jdtTreeBuilder.getContextBuilder().stack; + if (stack.peek() == null || !(stack.peek().node instanceof AllocationExpression)) { + // have thus far only ended up here with a generic array type, + // don't know if we want or need to deal with those + return; + } + + AllocationExpression alloc = (AllocationExpression) stack.peek().node; + if (alloc.expectedType() == null || !(alloc.expectedType() instanceof ParameterizedTypeBinding)) { + // the expected type is not available/parameterized if the constructor call occurred in e.g. an unresolved + // method, or in a method that did not expect a parameterized argument + type.addActualTypeArgument(jdtTreeBuilder.getFactory().Type().OMITTED_TYPE_ARG_TYPE.clone()); + } else { + ParameterizedTypeBinding expectedType = (ParameterizedTypeBinding) alloc.expectedType(); + // type arguments can be recovered from the expected type + for (TypeBinding binding : expectedType.typeArguments()) { + CtTypeReference typeArgRef = getTypeReference(binding); + typeArgRef.setImplicit(true); + type.addActualTypeArgument(typeArgRef); + } + } } /** diff --git a/src/test/java/spoon/test/constructorcallnewclass/ConstructorCallTest.java b/src/test/java/spoon/test/constructorcallnewclass/ConstructorCallTest.java index dc7b32d158e..7ff2f978f64 100644 --- a/src/test/java/spoon/test/constructorcallnewclass/ConstructorCallTest.java +++ b/src/test/java/spoon/test/constructorcallnewclass/ConstructorCallTest.java @@ -19,8 +19,10 @@ import org.junit.Before; import org.junit.Test; import spoon.Launcher; +import spoon.reflect.CtModel; import spoon.reflect.code.CtConstructorCall; import spoon.reflect.declaration.CtClass; +import spoon.reflect.declaration.CtElement; import spoon.reflect.declaration.CtType; import spoon.reflect.factory.Factory; import spoon.reflect.reference.CtArrayTypeReference; @@ -32,8 +34,10 @@ import spoon.test.constructorcallnewclass.testclasses.Panini; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.TreeSet; +import java.util.stream.Collectors; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; @@ -140,4 +144,49 @@ public void testCoreConstructorCall() { assertEquals("new Bar()", call2.toString()); } + @Test + public void testParameterizedConstructorCallOmittedTypeArgsNoClasspath() { + // contract: omitted type arguments to constructors must be properly resolved if the context allows + // the expected type to be known + List expectedTypeArgNames = Arrays.asList("Integer", "String"); + String sourceFile = "./src/test/resources/noclasspath/GenericTypeEmptyDiamond.java"; + + CtTypeReference executableType = getConstructorCallTypeFrom("GenericKnownExpectedType", sourceFile); + + assertTrue(executableType.isParameterized()); + assertEquals(expectedTypeArgNames, + executableType.getActualTypeArguments().stream() + .map(CtTypeReference::getSimpleName).collect(Collectors.toList())); + assertTrue(executableType.getActualTypeArguments().stream().allMatch(CtElement::isImplicit)); + } + + @Test + public void testParameterizedConstructorCallOmittedTypeArgsUnknownExpectedTypeNoClasspath() { + // contract: even if the expected type is not known for omitted type arguments the type access must be + // detected as parameterized + String sourceFile = "./src/test/resources/noclasspath/GenericTypeEmptyDiamond.java"; + CtTypeReference executableType = getConstructorCallTypeFrom("GenericUnknownExpectedType", sourceFile); + assertTrue(executableType.isParameterized()); + assertTrue(executableType.getActualTypeArguments().stream().allMatch(CtElement::isImplicit)); + } + + @Test + public void testParameterizedConstructorCallOmittedTypeArgsResolvedTypeNoClasspath() { + // contract: if a resolved type (here, java.util.ArrayList) is parameterized with empty diamonds in an + // unresolved method, the resolved type reference should still be parameterized. + String sourceFile = "./src/test/resources/noclasspath/GenericTypeEmptyDiamond.java"; + CtTypeReference executableType = getConstructorCallTypeFrom("ArrayList", sourceFile); + assertTrue(executableType.isParameterized()); + } + + private CtTypeReference getConstructorCallTypeFrom(String simpleName, String sourceFile) { + final Launcher launcher = new Launcher(); + launcher.getEnvironment().setNoClasspath(true); + launcher.addInputResource(sourceFile); + CtModel model = launcher.buildModel(); + List> calls = + model.getElements(element -> element.getExecutable().getType().getSimpleName().equals(simpleName)); + assert calls.size() == 1; + return calls.get(0).getExecutable().getType(); + } } diff --git a/src/test/resources/noclasspath/GenericTypeEmptyDiamond.java b/src/test/resources/noclasspath/GenericTypeEmptyDiamond.java new file mode 100644 index 00000000000..8ebe839d65c --- /dev/null +++ b/src/test/resources/noclasspath/GenericTypeEmptyDiamond.java @@ -0,0 +1,13 @@ +// the purpose of this test class is to check that omitted type arguments are properly resolved in noclasspath mode +import java.util.ArrayList; + +class GenericTypeEmptyDiamond { + public static void main(String[] args) { + // the context should allow the type arguments for this constructor call to be recovered + GenericKnownExpectedType someGeneric = new GenericKnownExpectedType<>(); + // meth is an unresolved method, so there is no context to allow for inference of type arguments + meth(new GenericUnknownExpectedType<>()); + // same as the above, but with a generic type that is available on the classpath + meth(new ArrayList<>()); + } +} \ No newline at end of file