From 344adc5f0887ad4a99603d0607f8ecb59e6c8426 Mon Sep 17 00:00:00 2001 From: treenwang Date: Tue, 8 Oct 2024 16:47:09 +0800 Subject: [PATCH] fix: Improve noclasspath handling of mixed (un)qualified references (#5981) Co-authored-by: I-Al-Istannen --- .../compiler/jdt/ReferenceBuilder.java | 18 +++++- src/test/java/spoon/test/api/APITest.java | 62 +++++++++++++++++++ .../CtTypeInformationAssertInterface.java | 5 ++ .../assertions/codegen/AssertJCodegen.java | 2 + 4 files changed, 86 insertions(+), 1 deletion(-) diff --git a/src/main/java/spoon/support/compiler/jdt/ReferenceBuilder.java b/src/main/java/spoon/support/compiler/jdt/ReferenceBuilder.java index 7b6c9ef9565..842f8c4115c 100644 --- a/src/main/java/spoon/support/compiler/jdt/ReferenceBuilder.java +++ b/src/main/java/spoon/support/compiler/jdt/ReferenceBuilder.java @@ -97,6 +97,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -1188,7 +1189,11 @@ private CtTypeReference getTypeReferenceFromProblemReferenceBinding(ProblemRe CtTypeReference ref = this.jdtTreeBuilder.getFactory().Core().createTypeReference(); ref.setSimpleName(stripPackageName(readableName)); - final CtReference declaring = this.getDeclaringReferenceFromImports(binding.sourceName()); + CtReference declaring = this.getDeclaringReferenceFromImports(binding.sourceName()); + Optional packageName = getPackageName(readableName); + if (declaring == null && packageName.isPresent()) { + declaring = this.jdtTreeBuilder.getFactory().Package().createReference(packageName.get()); + } setPackageOrDeclaringType(ref, declaring); return ref; @@ -1207,6 +1212,17 @@ private static String stripPackageName(String fullyQualifiedName) { return fullyQualifiedName.substring(s); } + private static Optional getPackageName(String fullyQualifiedName) { + if (!fullyQualifiedName.contains(".")) { + return Optional.empty(); + } + String className = stripPackageName(fullyQualifiedName); + if (fullyQualifiedName.equals(className)) { + return Optional.empty(); + } + return Optional.of(fullyQualifiedName.substring(0, fullyQualifiedName.length() - className.length())); + } + private CtTypeReference getTypeReferenceFromIntersectionTypeBinding(IntersectionTypeBinding18 binding) { List> boundingTypes = new ArrayList<>(); for (ReferenceBinding superInterface : binding.getIntersectingTypes()) { diff --git a/src/test/java/spoon/test/api/APITest.java b/src/test/java/spoon/test/api/APITest.java index 131ee260972..ff3016f970d 100644 --- a/src/test/java/spoon/test/api/APITest.java +++ b/src/test/java/spoon/test/api/APITest.java @@ -69,7 +69,10 @@ import spoon.template.TemplateParameter; import spoon.test.api.processors.AwesomeProcessor; import spoon.test.api.testclasses.Bar; +import spoon.testing.utils.GitHubIssue; + +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -77,6 +80,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import static spoon.testing.assertions.SpoonAssertions.assertThat; public class APITest { @@ -639,4 +643,62 @@ public void testProcessModelsTwice() { } } + @Test + public void testRespectPackageOfQualifiedUnknownClass() { + // contract: Classes appearing with qualified names in the source should be put in an appropriate package, + // even if we do not have them in our classpath + CtClass theClass = Launcher.parseClass(""" + package io.example.pack1.pack2; + public class Example { + void add(io.example.other.Class1 value){ + } + } + """); + List> addParameters = theClass.getMethodsByName("add").get(0).getParameters(); + assertThat(addParameters).hasSize(1); + + CtParameter parameter = addParameters.get(0); + assertThat(parameter).getSimpleName().isEqualTo("value"); + + CtTypeReference parameterType = parameter.getType(); + assertThat(parameterType).getQualifiedName().isEqualTo("io.example.other.Class1"); + assertThat(parameterType).getActualTypeArguments().hasSize(1); + assertThat(parameterType.getActualTypeArguments().get(0)).getQualifiedName() + .isEqualTo("io.example.other.Class2"); + } + + @Test + @GitHubIssue(issueNumber = 4783, fixed = true) + public void testRespectPackageOfQualifiedUnknownClassPreserveUnqualified() { + // contract: Classes appearing with qualified names in the source should be put in an appropriate package, + // even if we do not have them in our classpath. Unqualified classes should be re-homed, however. + CtClass theClass = Launcher.parseClass(""" + package io.example.pack1.pack2; + public class Example { + void add(io.example.other.Class1 value1, Class1 value2) { + } + } + """); + List> addParameters = theClass.getMethodsByName("add").get(0).getParameters(); + assertThat(addParameters).hasSize(2); + + CtParameter firstParameter = addParameters.get(0); + assertThat(firstParameter).getSimpleName().isEqualTo("value1"); + + CtParameter secondParameter = addParameters.get(1); + assertThat(secondParameter).getSimpleName().isEqualTo("value2"); + + CtTypeReference firstParameterType = firstParameter.getType(); + assertThat(firstParameterType).getQualifiedName().isEqualTo("io.example.other.Class1"); + assertThat(firstParameterType).getActualTypeArguments().hasSize(1); + assertThat(firstParameterType.getActualTypeArguments().get(0)).getQualifiedName() + .isEqualTo("io.example.other.Class2"); + + CtTypeReference secondParameterType = secondParameter.getType(); + assertThat(secondParameterType).getQualifiedName().isEqualTo("io.example.pack1.pack2.Class1"); + assertThat(secondParameterType).getActualTypeArguments().hasSize(1); + assertThat(secondParameterType.getActualTypeArguments().get(0)).getQualifiedName() + .isEqualTo("io.example.pack1.pack2.Class2"); + } + } diff --git a/src/test/java/spoon/testing/assertions/CtTypeInformationAssertInterface.java b/src/test/java/spoon/testing/assertions/CtTypeInformationAssertInterface.java index 22bbdddc3d3..78e622a8143 100644 --- a/src/test/java/spoon/testing/assertions/CtTypeInformationAssertInterface.java +++ b/src/test/java/spoon/testing/assertions/CtTypeInformationAssertInterface.java @@ -2,6 +2,7 @@ import java.util.Collection; import org.assertj.core.api.AbstractCollectionAssert; import org.assertj.core.api.AbstractObjectAssert; +import org.assertj.core.api.AbstractStringAssert; import org.assertj.core.api.Assertions; import spoon.reflect.declaration.CtTypeInformation; import spoon.reflect.declaration.ModifierKind; @@ -11,6 +12,10 @@ public interface CtTypeInformationAssertInterface getQualifiedName() { + return Assertions.assertThat(actual().getQualifiedName()); + } + default AbstractCollectionAssert>, CtTypeReference, ?> getSuperInterfaces() { return Assertions.assertThat(actual().getSuperInterfaces()); } diff --git a/src/test/java/spoon/testing/assertions/codegen/AssertJCodegen.java b/src/test/java/spoon/testing/assertions/codegen/AssertJCodegen.java index f1b29a1ce42..9c64d68c348 100644 --- a/src/test/java/spoon/testing/assertions/codegen/AssertJCodegen.java +++ b/src/test/java/spoon/testing/assertions/codegen/AssertJCodegen.java @@ -308,6 +308,8 @@ private void createExtractingMethods(CtType type, CtInterface assertInterf Metamodel instance = Metamodel.getInstance(); MetamodelConcept concept = instance.getConcept((Class) type.getActualClass()); TreeSet> methods = new TreeSet<>(Comparator.comparing(CtMethod::getSimpleName)); + methods.addAll(assertInterface.getMethods()); + for (var entry : concept.getRoleToProperty().entrySet()) { List> declaredMethods = entry.getValue().getMethod(MMMethodKind.GET).getDeclaredMethods(); CtMethod method = declaredMethods.stream().reduce((l, r) -> l.getType().isSubtypeOf(r.getType()) ? l : r).orElseThrow();