Skip to content

Commit

Permalink
Emulate outer this access
Browse files Browse the repository at this point in the history
  • Loading branch information
I-Al-Istannen committed Nov 16, 2022
1 parent 71034d7 commit 5800b52
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 8 deletions.
23 changes: 20 additions & 3 deletions src/main/java/spoon/support/compiler/jdt/ParentExiter.java
Original file line number Diff line number Diff line change
Expand Up @@ -720,7 +720,7 @@ public <T> void visitCtInvocation(CtInvocation<T> invocation) {
} else if (child instanceof CtExpression) {
if (hasChildEqualsToReceiver(invocation) || hasChildEqualsToQualification(invocation)) {
if (child instanceof CtThisAccess) {
if (!setTargetFromStaticImport(invocation)) {
if (!setTargetFromUnqualifiedAccess(invocation)) {
final CtTypeReference<?> declaringType = invocation.getExecutable().getDeclaringType();
if (declaringType != null && invocation.getExecutable().isStatic() && child.isImplicit()) {
invocation.setTarget(jdtTreeBuilder.getFactory().Code().createTypeAccess(declaringType, true));
Expand All @@ -739,11 +739,14 @@ public <T> void visitCtInvocation(CtInvocation<T> invocation) {
super.visitCtInvocation(invocation);
}

private <T> boolean setTargetFromStaticImport(CtInvocation<T> invocation) {
private <T> boolean setTargetFromUnqualifiedAccess(CtInvocation<T> invocation) {
// A call to a statically imported method (e.g. assertTrue(false)) is modelled as
// "this.assertTrue(false)" by JDT. We need to unscramble that heuristically and replace the
// "this" reference with the correct type (e.g. org.junit.api.Assertions)

// Additionally, references to methods of enclosing classes are also modelled as "this" by JDT.
// Compare with Test "correctlySetsThisTargetForUnqualifiedCalls".

// We need a MessageSend as the parent to resolve the actualType from the receiver
if (!(parentPair.node instanceof MessageSend)) {
return false;
Expand All @@ -762,7 +765,21 @@ private <T> boolean setTargetFromStaticImport(CtInvocation<T> invocation) {
return false;
}

// If not, we probably had a static import here and should use the actual type instead
if (messageSend.binding() == null || !messageSend.binding().isStatic()) {
// Emulate outer this access
while (resolvedReceiverType != null) {
resolvedReceiverType = resolvedReceiverType.getDeclaringType();
if (actualReceiverType.equals(resolvedReceiverType)) {
invocation.setTarget(jdtTreeBuilder.getFactory().Code().createThisAccess(actualReceiverType, true));
return true;
}
}
// I don't think this can happen but let's be conservative and preserve the previous behaviour
return false;
}

// If not, we probably had a static import/static outer method reference here and should use the actual type
// instead
invocation.setTarget(jdtTreeBuilder.getFactory().Code().createTypeAccess(actualReceiverType, true));
return true;
}
Expand Down
6 changes: 3 additions & 3 deletions src/test/java/spoon/reflect/visitor/CtScannerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -271,9 +271,9 @@ public void exit(CtElement o) {
// this is a coarse-grain check to see if the scanner changes
// no more exec ref in paramref
// also takes into account the comments
assertEquals(3655, counter.nElement + countOfCommentsInCompilationUnits);
assertEquals(2435, counter.nEnter + countOfCommentsInCompilationUnits);
assertEquals(2435, counter.nExit + countOfCommentsInCompilationUnits);
assertEquals(3631, counter.nElement + countOfCommentsInCompilationUnits);
assertEquals(2423, counter.nEnter + countOfCommentsInCompilationUnits);
assertEquals(2423, counter.nExit + countOfCommentsInCompilationUnits);

// contract: all AST nodes which are part of Collection or Map are visited first by method "scan(Collection|Map)" and then by method "scan(CtElement)"
Counter counter2 = new Counter();
Expand Down
45 changes: 45 additions & 0 deletions src/test/java/spoon/test/imports/ImportTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@
import java.util.StringTokenizer;

import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.IsCollectionContaining.hasItem;
Expand Down Expand Up @@ -1773,4 +1775,47 @@ public void testImportsForElementsAnnotatedWithTypeUseAnnotations() {
.filter(ctImport -> ctImport.prettyprint().equals("import spoon.test.imports.testclasses.badimportissue3320.source.other.SomeObjectDto;"))
.count());
}

@ModelTest("src/test/resources/imports/UnqualifiedCalls.java")
void correctlySetsThisTargetForUnqualifiedCalls(Factory factory) {
CtType<?> test = factory.Type().get("Test$Inner");
CtMethod<?> method = test.getMethodsByName("entrypoint").get(0);
CtInvocation<?> actualThisInvocation = method.getBody().getStatement(0);
CtInvocation<?> outerThisInvocation = method.getBody().getStatement(1);
CtInvocation<?> staticOuterInvocation = method.getBody().getStatement(2);
CtInvocation<?> staticInvocation = method.getBody().getStatement(3);

assertThat(actualThisInvocation.getTarget().isImplicit(), is(true));
assertThat(actualThisInvocation.getTarget(), is(instanceOf(CtThisAccess.class)));
assertThat(
((CtTypeAccess<?>) ((CtThisAccess<?>) actualThisInvocation.getTarget()).getTarget())
.getAccessedType()
.getQualifiedName(),
is("Test$Inner")
);

assertThat(outerThisInvocation.getTarget().isImplicit(), is(true));
assertThat(outerThisInvocation.getTarget(), is(instanceOf(CtThisAccess.class)));
assertThat(
((CtTypeAccess<?>) ((CtThisAccess<?>) outerThisInvocation.getTarget()).getTarget())
.getAccessedType()
.getQualifiedName(),
is("Test")
);

assertThat(staticOuterInvocation.getTarget().isImplicit(), is(true));
assertThat(staticOuterInvocation.getTarget(), is(instanceOf(CtTypeAccess.class)));
assertThat(
((CtTypeAccess<?>) staticOuterInvocation.getTarget()).getAccessedType().getQualifiedName(),
is("Test")
);

assertThat(staticInvocation.getTarget().isImplicit(), is(true));
assertThat(staticInvocation.getTarget(), is(instanceOf(CtTypeAccess.class)));
assertThat(
((CtTypeAccess<?>) staticInvocation.getTarget()).getAccessedType().getQualifiedName(),
is("java.lang.System")
);

}
}
23 changes: 21 additions & 2 deletions src/test/java/spoon/test/targeted/TargetedExpressionTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@
import spoon.test.targeted.testclasses.Tapas;
import spoon.testing.utils.ModelTest;

import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.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;
Expand Down Expand Up @@ -401,13 +404,29 @@ public void testTargetsOfInvInInnerClass() throws Exception {
final List<CtInvocation<?>> elements = innerInvMethod.getElements(new TypeFilter<>(CtInvocation.class));
assertEquals(8, elements.size());
expectedThisAccess.setType(expectedInnerClass);
assertEqualsInvocation(new ExpectedTargetedExpression().declaringType(expectedType).target(expectedThisAccess).result("spoon.test.targeted.testclasses.Foo.inv()"), elements.get(0));
assertThat(elements.get(0).getTarget().isImplicit(), is(true));
assertThat(elements.get(0).getTarget(), is(instanceOf(CtThisAccess.class)));
assertThat(
((CtTypeAccess<?>) ((CtThisAccess<?>) elements.get(0).getTarget()).getTarget())
.getAccessedType()
.getQualifiedName(),
is("spoon.test.targeted.testclasses.Foo")
);
assertThat(elements.get(0).getExecutable().getSimpleName(), is("inv"));
expectedThisAccess.setType(expectedType);
assertEqualsInvocation(new ExpectedTargetedExpression().declaringType(expectedType).target(expectedThisAccess).result("this.inv()"), elements.get(1));
assertEqualsInvocation(new ExpectedTargetedExpression().declaringType(expectedType).target(fooTypeAccess).result("spoon.test.targeted.testclasses.Foo.staticMethod()"), elements.get(2));
assertEqualsInvocation(new ExpectedTargetedExpression().declaringType(expectedType).target(fooTypeAccess).result("spoon.test.targeted.testclasses.Foo.staticMethod()"), elements.get(3));
expectedSuperThisAccess.setType(expectedInnerClass);
assertEqualsInvocation(new ExpectedTargetedExpression().declaringType(expectedSuperClassType).target(expectedSuperThisAccess).result("spoon.test.targeted.testclasses.Foo.superMethod()"), elements.get(4));
assertThat(elements.get(4).getTarget().isImplicit(), is(true));
assertThat(elements.get(4).getTarget(), is(instanceOf(CtThisAccess.class)));
assertThat(
((CtTypeAccess<?>) ((CtThisAccess<?>) elements.get(4).getTarget()).getTarget())
.getAccessedType()
.getQualifiedName(),
is("spoon.test.targeted.testclasses.Foo")
);
assertThat(elements.get(4).getExecutable().getSimpleName(), is("superMethod"));
assertEqualsInvocation(new ExpectedTargetedExpression().declaringType(expectedSuperClassType).target(expectedThisAccess).result("this.superMethod()"), elements.get(5));
assertEqualsInvocation(new ExpectedTargetedExpression().declaringType(expectedInnerClass).target(expectedInnerClassAccess).result("method()"), elements.get(6));
assertEqualsInvocation(new ExpectedTargetedExpression().declaringType(expectedInnerClass).target(expectedInnerClassAccess).result("this.method()"), elements.get(7));
Expand Down
14 changes: 14 additions & 0 deletions src/test/resources/imports/UnqualifiedCalls.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import static java.lang.System.lineSeparator;
public class Test {
public void outerMethod() {}
public static void staticOuterMethod() {}

public class Inner {
void entrypoint() {
entrypoint();
outerMethod();
staticOuterMethod();
lineSeparator();
}
}
}

0 comments on commit 5800b52

Please sign in to comment.