diff --git a/src/main/java/spoon/support/compiler/jdt/ReferenceBuilder.java b/src/main/java/spoon/support/compiler/jdt/ReferenceBuilder.java index d09b82f1a63..7b6c9ef9565 100644 --- a/src/main/java/spoon/support/compiler/jdt/ReferenceBuilder.java +++ b/src/main/java/spoon/support/compiler/jdt/ReferenceBuilder.java @@ -58,6 +58,7 @@ import org.eclipse.jdt.internal.compiler.lookup.ReferenceBinding; import org.eclipse.jdt.internal.compiler.lookup.Scope; import org.eclipse.jdt.internal.compiler.lookup.SourceTypeBinding; +import org.eclipse.jdt.internal.compiler.lookup.SyntheticFactoryMethodBinding; import org.eclipse.jdt.internal.compiler.lookup.TypeBinding; import org.eclipse.jdt.internal.compiler.lookup.TypeVariableBinding; import org.eclipse.jdt.internal.compiler.lookup.UnresolvedReferenceBinding; @@ -422,7 +423,11 @@ CtExecutableReference getExecutableReference(MethodBinding exec, int sour if (sourceStart >= 0 && sourceEnd >= 0) { ref.setPosition(jdtTreeBuilder.getPositionBuilder().buildPosition(sourceStart, sourceEnd)); } - if (exec.isConstructor()) { + // JDT creates synthetic methods for inference of diamond constructors (We guess they had that + // lying around). If the type can not be completely resolved, e.g. due to no classpath, this factory is + // not replaced and appears in the AST. We need to fix its name so e.g. `CtExecutableReference#isConstructor` + // works. + if (exec.isConstructor() || exec.original() instanceof SyntheticFactoryMethodBinding) { ref.setSimpleName(CtExecutableReference.CONSTRUCTOR_NAME); // in case of constructor of an array, it's the return type that we want @@ -1023,7 +1028,15 @@ private CtTypeReference getTypeReferenceFromTypeVariableBinding( return getTypeReferenceOfBoundingType(binding).clone(); } else { CtTypeReference ref = this.jdtTreeBuilder.getFactory().Core().createTypeParameterReference(); - ref.setSimpleName(new String(binding.sourceName())); + String name = new String(binding.sourceName()); + if (binding.declaringElement instanceof SyntheticFactoryMethodBinding) { + // JDT uses these factory methods for type inference of diamond constructors. In no classpath mode they + // might be left around. They append a variable number of primes (') to the original name, which is not + // valid in Java. We undo this here and hope for the best. + name = name.replace("'", ""); + } + + ref.setSimpleName(name); return ref; } } diff --git a/src/test/java/spoon/test/api/NoClasspathTest.java b/src/test/java/spoon/test/api/NoClasspathTest.java index 0a86ef91b28..daf0d98eef4 100644 --- a/src/test/java/spoon/test/api/NoClasspathTest.java +++ b/src/test/java/spoon/test/api/NoClasspathTest.java @@ -23,11 +23,14 @@ import org.junit.jupiter.api.Test; import spoon.Launcher; +import spoon.reflect.CtModel; +import spoon.reflect.code.CtConstructorCall; import spoon.reflect.code.CtFieldAccess; import spoon.reflect.code.CtInvocation; import spoon.reflect.code.CtLocalVariable; import spoon.reflect.code.CtReturn; import spoon.reflect.declaration.CtClass; +import spoon.reflect.declaration.CtElement; import spoon.reflect.declaration.CtField; import spoon.reflect.declaration.CtMethod; import spoon.reflect.declaration.CtType; @@ -38,7 +41,11 @@ import spoon.support.SpoonClassNotFoundException; import spoon.support.visitor.SignaturePrinter; import spoon.test.api.testclasses.Bar; +import spoon.testing.assertions.SpoonAssertions; +import spoon.testing.utils.ModelTest; +import static java.util.function.Predicate.not; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -201,4 +208,19 @@ public void testInheritanceInNoClassPathWithClasses() { assertTrue(myClassReference.isSubtypeOf(myInterfaceReference)); assertTrue(field.getType().isSubtypeOf(myInterfaceReference)); } + + @ModelTest("src/test/resources/noclasspath/issue5591/DiamondConstructorCallTypeInference.java") + void testJdtFactoryMethodsForDiamond(CtModel model) { + // contract: Leftover methods from JDT's diamond constructor type inference are handled in + // no classpath mode + var constructorCalls = model.getElements(new TypeFilter<>(CtConstructorCall.class)); + assertThat(constructorCalls).hasSize(1); + + var executableReference = constructorCalls.get(0).getExecutable(); + SpoonAssertions.assertThat(executableReference) + .isNotNull() + .nested(it -> it.getSimpleName().isEqualTo("")) + .nested(it -> it.satisfies(CtExecutableReference::isConstructor)) + .nested(it -> it.getParameters().hasSize(1)); + } } diff --git a/src/test/java/spoon/testing/assertions/SpoonAssert.java b/src/test/java/spoon/testing/assertions/SpoonAssert.java index 849eb8497c8..fe11419be8b 100644 --- a/src/test/java/spoon/testing/assertions/SpoonAssert.java +++ b/src/test/java/spoon/testing/assertions/SpoonAssert.java @@ -1,10 +1,17 @@ package spoon.testing.assertions; +import java.util.function.Consumer; + public interface SpoonAssert { SELF self(); ACTUAL actual(); + default SELF nested(Consumer checks) { + checks.accept(self()); + return self(); + } + void failWithMessage(String errorMessage, Object... arguments); } diff --git a/src/test/resources/noclasspath/issue5591/DiamondConstructorCallTypeInference.java b/src/test/resources/noclasspath/issue5591/DiamondConstructorCallTypeInference.java new file mode 100644 index 00000000000..f6918d5e399 --- /dev/null +++ b/src/test/resources/noclasspath/issue5591/DiamondConstructorCallTypeInference.java @@ -0,0 +1,14 @@ +package com.foo.bar; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public class DiamondConstructorCallTypeInference.java { + // Whatever does not exist. + private final List items; + + public DiamondConstructorCallTypeInference.java(Collection items) { + this.items = new ArrayList<>(items); + } +}