Skip to content

Commit

Permalink
add support for accesses from lambdas TNG#847
Browse files Browse the repository at this point in the history
For lambdas in Java code (e.g. `() -> "some value"`) the compiler adds a synthetic static method to the Java class that follows the naming pattern `lambda$declaringMethod$42`. It also adds an `invokeDynamic` instruction that creates the link from the method that declares the lambda to this static method.

So far we ignore those `invokeDynamic` instructions for lambda cases. On the other hand we treat the synthetic lambda method like an ordinary method. This can lead to confusing behavior, e.g.

```
Supplier<SomeObject> call() {
  return () -> new SomeObject();
}
```

will now create a `JavaConstructorCall` that originates from some method `lambda$call$0()`, while there will be no trace about any constructor call from within the method `call()`.

We decided that for a typical user the simplest and likely most appropriate behavior will be to treat this constructor call from within the lambda as a `JavaConstructorCall` from `call()` to `SomeObject.<init>()` (even though technically it is something different, since the call to `new SomeObject()` will in this case not happen on invocation time of the method, but on invocation time of the return value). From an architecture test point of view the relevant information will likely be, that there is a dependency from `call()` to `new SomeObject()`, which is exactly what this way of treating the call from the lambda will provide.

Thus, to improve this we will track all `invokeDynamic` instructions and then "merge" these with the accesses from synthetic lambda methods into more appropriate accesses from the code unit declaring the lambda to the target that the synthetic lambda method targets.
  • Loading branch information
codecholeric authored Jun 3, 2022
2 parents 6862363 + 0330c74 commit 347dc45
Show file tree
Hide file tree
Showing 46 changed files with 1,206 additions and 606 deletions.
1 change: 1 addition & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ jobs:
uses: gradle/gradle-build-action@v2
with:
arguments: test -PallTests -PtestJavaVersion=${{ matrix.test_java_version }}
cache-disabled: true

integration-test:
strategy:
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.tngtech.archunit.core.importer;

import java.util.function.Supplier;

import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.java.junit.dataprovider.DataProviderRunner;
import org.junit.Test;
import org.junit.runner.RunWith;

import static org.assertj.core.api.Assertions.assertThat;

@RunWith(DataProviderRunner.class)
public class ClassFileImporterCodeUnitReferencesNewerJavaVersionTest {
/**
* A local class constructor obtains extra parameters from the outer scope that the compiler transparently adds
* to the byte code. A reference to this local constructor will then always be translated to a lambda call.
* Thus, in this case we do not expect a constructor reference.
*
* Note that this actually does not compile with JDK 8
*/
@Test
public void does_not_import_local_constructor_references() {
@SuppressWarnings("unused")
class ReferencedTarget {
ReferencedTarget() {
}
}
@SuppressWarnings("unused")
class Origin {
void referencesConstructor() {
Supplier<ReferencedTarget> a = ReferencedTarget::new;
}
}

JavaClasses javaClasses = new ClassFileImporter().importClasses(Origin.class, ReferencedTarget.class);

assertThat(javaClasses.get(Origin.class).getMethod("referencesConstructor").getConstructorReferencesFromSelf()).isEmpty();
assertThat(javaClasses.get(ReferencedTarget.class).getConstructor(ClassFileImporterCodeUnitReferencesNewerJavaVersionTest.class).getReferencesToSelf()).isEmpty();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.tngtech.archunit.core.importer;

import java.util.Set;
import java.util.function.Supplier;

import com.tngtech.archunit.core.domain.JavaAccess;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.domain.JavaConstructorCall;
import com.tngtech.java.junit.dataprovider.DataProviderRunner;
import org.junit.Test;
import org.junit.runner.RunWith;

import static com.google.common.collect.Iterables.getOnlyElement;
import static com.tngtech.archunit.core.domain.JavaConstructor.CONSTRUCTOR_NAME;
import static com.tngtech.archunit.testutil.Assertions.assertThatAccess;
import static java.util.stream.Collectors.toSet;

@RunWith(DataProviderRunner.class)
public class ClassFileImporterLambdaAccessesNewerJavaVersionTest {
/**
* This is a special case: For local constructors the Java compiler actually adds a lambda calling the constructor
* for a constructor reference. Since we do not distinguish between calls from within a lambda and outside it,
* this will lead to such a constructor reference being reported as a constructor call.
*
* Note that this actually does not compile with JDK 8
*/
@Test
public void imports_constructor_reference_to_local_class_from_lambda_without_parameter_as_direct_call() {
class Target {
}

@SuppressWarnings("unused")
class Caller {
Supplier<Supplier<Target>> call() {
return () -> Target::new;
}
}

JavaClasses classes = new ClassFileImporter().importClasses(Target.class, Caller.class);
JavaConstructorCall call = getOnlyElement(
filterOriginByName(classes.get(Caller.class).getConstructorCallsFromSelf(), "call"));

assertThatAccess(call).isFrom("call").isTo(Target.class, CONSTRUCTOR_NAME, getClass());
}

private <ACCESS extends JavaAccess<?>> Set<ACCESS> filterOriginByName(Set<ACCESS> calls, String methodName) {
return calls.stream()
.filter(call -> call.getOrigin().getName().equals(methodName))
.collect(toSet());
}
}
Loading

0 comments on commit 347dc45

Please sign in to comment.