Skip to content

Commit

Permalink
fix: Fix dropping diamonds on constructor calls (#3362)
Browse files Browse the repository at this point in the history
  • Loading branch information
slarse committed May 8, 2020
1 parent 5c7ec2b commit 92ee4ba
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/main/java/spoon/reflect/factory/TypeFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ public class TypeFactory extends SubFactory {
public final CtTypeReference<Set> SET = createReference(Set.class);
public final CtTypeReference<Map> MAP = createReference(Map.class);
public final CtTypeReference<Enum> ENUM = createReference(Enum.class);
public final CtTypeReference<?> OMITTED_TYPE_ARG_TYPE = createReference(CtTypeReference.OMITTED_TYPE_ARG_NAME);

private final Map<Class<?>, CtType<?>> shadowCache = new ConcurrentHashMap<>();

Expand Down
5 changes: 5 additions & 0 deletions src/main/java/spoon/reflect/reference/CtTypeReference.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ public interface CtTypeReference<T> extends CtReference, CtActualTypeContainer,
*/
String NULL_TYPE_NAME = "<nulltype>";

/**
* Special type used as a type argument when actual type arguments can't be inferred.
*/
String OMITTED_TYPE_ARG_NAME = "<omitted>";

/**
* Returns the simple (unqualified) name of this element.
* Following the compilation convention, if the type is a local type,
Expand Down
42 changes: 42 additions & 0 deletions src/main/java/spoon/support/compiler/jdt/ReferenceBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -569,6 +570,47 @@ private <T> 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:
*
* <ul>
* <li>The generic type is not on the classpath.</li>
* <li>The generic type is used in a context where the type arguments cannot be inferred, such as in an
* unresolved method
* </li>
* </ul>
*
* See #3360 for details.
*/
private void tryRecoverTypeArguments(CtTypeReference<?> type) {
final Deque<ASTPair> 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);
}
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<String> 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<CtConstructorCall<?>> calls =
model.getElements(element -> element.getExecutable().getType().getSimpleName().equals(simpleName));
assert calls.size() == 1;
return calls.get(0).getExecutable().getType();
}
}
13 changes: 13 additions & 0 deletions src/test/resources/noclasspath/GenericTypeEmptyDiamond.java
Original file line number Diff line number Diff line change
@@ -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<Integer, String> 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<>());
}
}

0 comments on commit 92ee4ba

Please sign in to comment.