Skip to content

Commit

Permalink
add support for accesses from lambdas
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. For cases where users actually want to distinguish a "direct" access and one that is declared in the context of a lambda we add the boolean flag `declaredInLambda` to `JavaAccess`. This way the information can still be retrieved if it should be relevant in a certain context.

Signed-off-by: Hannes Oberprantacher <h.oberprantacher@gmail.com>
Signed-off-by: Peter Gafert <peter.gafert@tngtech.com>
  • Loading branch information
oberprah authored and codecholeric committed May 29, 2022
1 parent e5bb469 commit a404fb4
Show file tree
Hide file tree
Showing 18 changed files with 854 additions and 53 deletions.
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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
import com.tngtech.archunit.core.domain.properties.HasOwner;
import com.tngtech.archunit.core.domain.properties.HasOwner.Functions.Get;
import com.tngtech.archunit.core.domain.properties.HasSourceCodeLocation;
import com.tngtech.archunit.core.importer.DomainBuilders;
import com.tngtech.archunit.core.importer.DomainBuilders.JavaAccessBuilder;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
Expand All @@ -38,12 +38,14 @@ public abstract class JavaAccess<TARGET extends AccessTarget>
private final TARGET target;
private final int lineNumber;
private final SourceCodeLocation sourceCodeLocation;
private final boolean declaredInLambda;

JavaAccess(DomainBuilders.JavaAccessBuilder<TARGET, ?> builder) {
JavaAccess(JavaAccessBuilder<TARGET, ?> builder) {
this.origin = checkNotNull(builder.getOrigin());
this.target = checkNotNull(builder.getTarget());
this.lineNumber = builder.getLineNumber();
this.sourceCodeLocation = SourceCodeLocation.of(getOriginOwner(), lineNumber);
this.declaredInLambda = builder.isDeclaredInLambda();
}

@Override
Expand Down Expand Up @@ -89,6 +91,11 @@ public SourceCodeLocation getSourceCodeLocation() {
return sourceCodeLocation;
}

@PublicAPI(usage = ACCESS)
public boolean isDeclaredInLambda() {
return declaredInLambda;
}

@Override
public String toString() {
return getClass().getSimpleName() +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ interface AccessRecord<TARGET extends AccessTarget> {

int getLineNumber();

boolean isDeclaredInLambda();

RawAccessRecord getRaw();

@Internal
Expand Down Expand Up @@ -263,6 +265,11 @@ public int getLineNumber() {
return record.lineNumber;
}

@Override
public boolean isDeclaredInLambda() {
return record.declaredInLambda;
}

@Override
public RawAccessRecord getRaw() {
return record;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.stream.Stream;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.HashMultimap;
Expand All @@ -40,12 +42,18 @@
import com.tngtech.archunit.core.importer.DomainBuilders.JavaStaticInitializerBuilder;
import com.tngtech.archunit.core.importer.DomainBuilders.TryCatchBlockBuilder;
import com.tngtech.archunit.core.importer.RawAccessRecord.CodeUnit;
import com.tngtech.archunit.core.importer.RawAccessRecord.MemberSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.tngtech.archunit.core.importer.JavaClassDescriptorImporter.isLambdaMethodName;
import static java.util.Collections.emptyList;

class ClassFileImportRecord {
private static final Logger log = LoggerFactory.getLogger(ClassFileImportRecord.class);

private static final JavaClassTypeParametersBuilder NO_TYPE_PARAMETERS =
new JavaClassTypeParametersBuilder(emptyList());

Expand All @@ -70,6 +78,7 @@ class ClassFileImportRecord {
private final Set<RawAccessRecord> rawConstructorCallRecords = new HashSet<>();
private final Set<RawAccessRecord> rawMethodReferenceRecords = new HashSet<>();
private final Set<RawAccessRecord> rawConstructorReferenceRecords = new HashSet<>();
private final Map<String, RawAccessRecord> rawSyntheticLambdaMethodInvocationRecordsByTarget = new HashMap<>();

void setSuperclass(String ownerName, String superclassName) {
checkState(!superclassNamesByOwner.containsKey(ownerName),
Expand Down Expand Up @@ -220,24 +229,61 @@ void registerConstructorReference(RawAccessRecord record) {
rawConstructorReferenceRecords.add(record);
}

void registerLambdaInvocation(RawAccessRecord record) {
rawSyntheticLambdaMethodInvocationRecordsByTarget.put(getMemberKey(record.target), record);
}

void forEachRawFieldAccessRecord(Consumer<RawAccessRecord.ForField> doWithRecord) {
rawFieldAccessRecords.forEach(doWithRecord);
fixLambdaOrigins(rawFieldAccessRecords, createLambdaFieldAccessWithNewOrigin).forEach(doWithRecord);
}

void forEachRawMethodCallRecord(Consumer<RawAccessRecord> doWithRecord) {
rawMethodCallRecords.forEach(doWithRecord);
fixLambdaOrigins(rawMethodCallRecords, createLambdaAccessWithNewOrigin).forEach(doWithRecord);
}

void forEachRawConstructorCallRecord(Consumer<RawAccessRecord> doWithRecord) {
rawConstructorCallRecords.forEach(doWithRecord);
fixLambdaOrigins(rawConstructorCallRecords, createLambdaAccessWithNewOrigin).forEach(doWithRecord);
}

void forEachRawMethodReferenceRecord(Consumer<RawAccessRecord> doWithRecord) {
rawMethodReferenceRecords.forEach(doWithRecord);
fixLambdaOrigins(rawMethodReferenceRecords, createLambdaAccessWithNewOrigin).forEach(doWithRecord);
}

void forEachRawConstructorReferenceRecord(Consumer<RawAccessRecord> doWithRecord) {
rawConstructorReferenceRecords.forEach(doWithRecord);
fixLambdaOrigins(rawConstructorReferenceRecords, createLambdaAccessWithNewOrigin).forEach(doWithRecord);
}

private <ACCESS extends RawAccessRecord> Stream<ACCESS> fixLambdaOrigins(
Set<ACCESS> rawAccessRecordsIncludingSyntheticLambda,
BiFunction<ACCESS, CodeUnit, ACCESS> createAccessWithNewOrigin
) {
return rawAccessRecordsIncludingSyntheticLambda.stream()
.flatMap(access -> isLambdaMethodName(access.caller.getName())
? replaceOriginByLambdaOrigin(access, createAccessWithNewOrigin)
: Stream.of(access));
}

private <ACCESS extends RawAccessRecord> Stream<ACCESS> replaceOriginByLambdaOrigin(ACCESS accessFromSyntheticLambda, BiFunction<ACCESS, CodeUnit, ACCESS> createAccessWithNewOrigin) {
RawAccessRecord lambdaInvocationRecord = findNonSyntheticOriginOf(accessFromSyntheticLambda);

if (lambdaInvocationRecord != null) {
return Stream.of(createAccessWithNewOrigin.apply(accessFromSyntheticLambda, lambdaInvocationRecord.caller));
} else {
log.warn("Could not find matching dynamic invocation for synthetic lambda method {}.{}|{}",
accessFromSyntheticLambda.target.getDeclaringClassName(),
accessFromSyntheticLambda.target.name,
accessFromSyntheticLambda.target.getDescriptor());
return Stream.empty();
}
}

private <ACCESS extends RawAccessRecord> RawAccessRecord findNonSyntheticOriginOf(ACCESS accessFromSyntheticLambda) {
RawAccessRecord result = accessFromSyntheticLambda;
do {
result = rawSyntheticLambdaMethodInvocationRecordsByTarget.get(getMemberKey(result.caller));
} while (result != null && isLambdaMethodName(result.caller.getName()));

return result;
}

void add(JavaClass javaClass) {
Expand All @@ -248,6 +294,10 @@ Map<String, JavaClass> getClasses() {
return classes;
}

private static String getMemberKey(MemberSignature member) {
return getMemberKey(member.getDeclaringClassName(), member.getName(), member.getDescriptor());
}

private static String getMemberKey(JavaMember member) {
return getMemberKey(member.getOwner().getName(), member.getName(), member.getDescriptor());
}
Expand All @@ -256,6 +306,23 @@ private static String getMemberKey(String declaringClassName, String methodName,
return declaringClassName + "|" + methodName + "|" + descriptor;
}

private static final BiFunction<RawAccessRecord, CodeUnit, RawAccessRecord> createLambdaAccessWithNewOrigin =
(access, newOrigin) -> fillWithNewOriginDeclaredInLambda(new RawAccessRecord.Builder(), access, newOrigin)
.build();

private static final BiFunction<RawAccessRecord.ForField, CodeUnit, RawAccessRecord.ForField> createLambdaFieldAccessWithNewOrigin =
(access, newOrigin) -> fillWithNewOriginDeclaredInLambda(new RawAccessRecord.ForField.Builder(), access, newOrigin)
.withAccessType(access.accessType)
.build();

private static <T extends RawAccessRecord.BaseBuilder<T>> T fillWithNewOriginDeclaredInLambda(T builder, RawAccessRecord originalAccess, CodeUnit newOrigin) {
return builder
.withCaller(newOrigin)
.withTarget(originalAccess.target)
.withLineNumber(originalAccess.lineNumber)
.withDeclaredInLambda();
}

private static class EnclosingDeclarationsByInnerClasses {
private final Map<String, String> innerClassNameToEnclosingClassName = new HashMap<>();
private final Map<String, CodeUnit> innerClassNameToEnclosingCodeUnit = new HashMap<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,12 @@ public void handleMethodReferenceInstruction(String owner, String name, String d
dependencyResolutionProcess.registerAccessToType(target.owner.getFullyQualifiedClassName());
}

@Override
public void handleLambdaInstruction(String owner, String name, String desc) {
TargetInfo target = new TargetInfo(owner, name, desc);
importRecord.registerLambdaInvocation(filled(new RawAccessRecord.Builder(), target).build());
}

@Override
public void handleTryCatchBlock(Label start, Label end, Label handler, JavaClassDescriptor throwableType) {
LOG.trace("Found try/catch block between {} and {} for throwable {}", start, end, throwableType);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
Expand Down Expand Up @@ -55,6 +56,7 @@
import com.tngtech.archunit.core.importer.DomainBuilders.JavaConstructorCallBuilder;
import com.tngtech.archunit.core.importer.DomainBuilders.JavaConstructorReferenceBuilder;
import com.tngtech.archunit.core.importer.DomainBuilders.JavaFieldAccessBuilder;
import com.tngtech.archunit.core.importer.DomainBuilders.JavaMethodBuilder;
import com.tngtech.archunit.core.importer.DomainBuilders.JavaMethodCallBuilder;
import com.tngtech.archunit.core.importer.DomainBuilders.JavaMethodReferenceBuilder;
import com.tngtech.archunit.core.importer.DomainBuilders.JavaParameterizedTypeBuilder;
Expand All @@ -72,6 +74,7 @@
import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.createJavaClasses;
import static com.tngtech.archunit.core.importer.DomainBuilders.BuilderWithBuildParameter.BuildFinisher.build;
import static com.tngtech.archunit.core.importer.DomainBuilders.buildAnnotations;
import static com.tngtech.archunit.core.importer.JavaClassDescriptorImporter.isLambdaMethodName;

class ClassGraphCreator implements ImportContext {
private final ImportedClasses classes;
Expand Down Expand Up @@ -198,7 +201,8 @@ B accessBuilderFrom(B builder, AccessRecord<T> record) {
return builder
.withOrigin(record.getOrigin())
.withTarget(record.getTarget())
.withLineNumber(record.getLineNumber());
.withLineNumber(record.getLineNumber())
.withDeclaredInLambda(record.isDeclaredInLambda());
}

@Override
Expand Down Expand Up @@ -259,17 +263,22 @@ public Set<JavaField> createFields(JavaClass owner) {

@Override
public Set<JavaMethod> createMethods(JavaClass owner) {
Set<DomainBuilders.JavaMethodBuilder> methodBuilders = importRecord.getMethodBuildersFor(owner.getName());
Stream<JavaMethodBuilder> methodBuilders = getNonSyntheticLambdaMethodBuildersFor(owner);
if (owner.isAnnotation()) {
for (DomainBuilders.JavaMethodBuilder methodBuilder : methodBuilders) {
methodBuilder.withAnnotationDefaultValue(method ->
importRecord.getAnnotationDefaultValueBuilderFor(method).flatMap(builder -> builder.build(method, classes))
);
}
methodBuilders = methodBuilders.map(methodBuilder -> methodBuilder
.withAnnotationDefaultValue(method ->
importRecord.getAnnotationDefaultValueBuilderFor(method)
.flatMap(builder -> builder.build(method, classes))
));
}
return build(methodBuilders, owner, classes);
}

private Stream<JavaMethodBuilder> getNonSyntheticLambdaMethodBuildersFor(JavaClass owner) {
return importRecord.getMethodBuildersFor(owner.getName()).stream()
.filter(methodBuilder -> !isLambdaMethodName(methodBuilder.getName()));
}

@Override
public Set<JavaConstructor> createConstructors(JavaClass owner) {
return build(importRecord.getConstructorBuildersFor(owner.getName()), owner, classes);
Expand Down Expand Up @@ -328,7 +337,7 @@ public JavaClass resolveClass(String fullyQualifiedClassName) {
}

private Optional<JavaClass> getMethodReturnType(String declaringClassName, String methodName) {
for (DomainBuilders.JavaMethodBuilder methodBuilder : importRecord.getMethodBuildersFor(declaringClassName)) {
for (JavaMethodBuilder methodBuilder : importRecord.getMethodBuildersFor(declaringClassName)) {
if (methodBuilder.getName().equals(methodName) && methodBuilder.hasNoParameters()) {
return Optional.of(classes.getOrResolve(methodBuilder.getReturnTypeName()));
}
Expand Down
Loading

0 comments on commit a404fb4

Please sign in to comment.