Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

analyze local variable instantiations #1334

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ public final class ArchConfiguration {
static final String CLASS_RESOLVER_ARGS = "classResolver.args";
@Internal
public static final String ENABLE_MD5_IN_CLASS_SOURCES = "enableMd5InClassSources";
@Internal
public static final String ANALYZE_LOCAL_VARIABLE_INSTANTIATIONS = "analyzeLocalVariableInstantiations";
private static final String EXTENSION_PREFIX = "extension";

private static final Logger LOG = LoggerFactory.getLogger(ArchConfiguration.class);
Expand Down Expand Up @@ -124,6 +126,16 @@ public void setMd5InClassSourcesEnabled(boolean enabled) {
properties.setProperty(ENABLE_MD5_IN_CLASS_SOURCES, String.valueOf(enabled));
}

@PublicAPI(usage = ACCESS)
public boolean analyzeLocalVariableInstantiations() {
return Boolean.parseBoolean(properties.getProperty(ANALYZE_LOCAL_VARIABLE_INSTANTIATIONS));
}

@PublicAPI(usage = ACCESS)
public void setAnalyzeLocalVariableInstantiations(boolean enabled) {
properties.setProperty(ANALYZE_LOCAL_VARIABLE_INSTANTIATIONS, String.valueOf(enabled));
}

@PublicAPI(usage = ACCESS)
public Optional<String> getClassResolver() {
return Optional.ofNullable(properties.getProperty(CLASS_RESOLVER));
Expand Down Expand Up @@ -328,7 +340,8 @@ private ArchConfiguration copy() {
private static class PropertiesOverwritableBySystemProperties {
private static final Properties PROPERTY_DEFAULTS = createProperties(ImmutableMap.of(
RESOLVE_MISSING_DEPENDENCIES_FROM_CLASS_PATH, Boolean.TRUE.toString(),
ENABLE_MD5_IN_CLASS_SOURCES, Boolean.FALSE.toString()
ENABLE_MD5_IN_CLASS_SOURCES, Boolean.FALSE.toString(),
ANALYZE_LOCAL_VARIABLE_INSTANTIATIONS, Boolean.FALSE.toString()
));

private final Properties baseProperties;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;
Expand All @@ -36,6 +38,7 @@
import com.google.common.primitives.Ints;
import com.google.common.primitives.Longs;
import com.google.common.primitives.Shorts;
import com.tngtech.archunit.ArchConfiguration;
import com.tngtech.archunit.Internal;
import com.tngtech.archunit.base.HasDescription;
import com.tngtech.archunit.base.MayResolveTypesViaReflection;
Expand Down Expand Up @@ -74,9 +77,9 @@

class JavaClassProcessor extends ClassVisitor {
private static final Logger LOG = LoggerFactory.getLogger(JavaClassProcessor.class);

private static final AccessHandler NO_OP = new AccessHandler.NoOp();

private final boolean analyzeLocalVariableInstantiations = ArchConfiguration.get().analyzeLocalVariableInstantiations();
private DomainBuilders.JavaClassBuilder javaClassBuilder;
private final Set<JavaAnnotationBuilder> annotations = new HashSet<>();
private final SourceDescriptor sourceDescriptor;
Expand Down Expand Up @@ -258,7 +261,7 @@ public MethodVisitor visitMethod(int access, String name, String desc, String si
.withThrowsClause(throwsDeclarations);
declarationHandler.onDeclaredThrowsClause(fullyQualifiedClassNamesOf(throwsDeclarations));

return new MethodProcessor(className, accessHandler, codeUnitBuilder, declarationHandler);
return new MethodProcessor(className, accessHandler, codeUnitBuilder, declarationHandler, analyzeLocalVariableInstantiations);
}

private Collection<String> fullyQualifiedClassNamesOf(List<JavaClassDescriptor> classDescriptors) {
Expand Down Expand Up @@ -318,14 +321,18 @@ private static class MethodProcessor extends MethodVisitor {
private final Set<JavaAnnotationBuilder> annotations = new HashSet<>();
private final SetMultimap<Integer, JavaAnnotationBuilder> parameterAnnotationsByIndex = HashMultimap.create();
private int actualLineNumber;
private final Map<Label, Integer> labelToLineNumber;
private final boolean analyzeLocalVariableInstantiations;

MethodProcessor(String declaringClassName, AccessHandler accessHandler, DomainBuilders.JavaCodeUnitBuilder<?, ?> codeUnitBuilder, DeclarationHandler declarationHandler) {
MethodProcessor(String declaringClassName, AccessHandler accessHandler, DomainBuilders.JavaCodeUnitBuilder<?, ?> codeUnitBuilder, DeclarationHandler declarationHandler, boolean analyzeLocalVariableInstantiations) {
super(ASM_API_VERSION);
this.declaringClassName = declaringClassName;
this.accessHandler = accessHandler;
this.codeUnitBuilder = codeUnitBuilder;
this.declarationHandler = declarationHandler;
codeUnitBuilder.withParameterAnnotations(parameterAnnotationsByIndex);
this.analyzeLocalVariableInstantiations = analyzeLocalVariableInstantiations;
labelToLineNumber = analyzeLocalVariableInstantiations ? new HashMap<>() : null;
}

@Override
Expand All @@ -351,6 +358,9 @@ public void visitLineNumber(int line, Label label) {
public void visitLabel(Label label) {
LOG.trace("Examining label {}", label);
accessHandler.onLabel(label);
if (analyzeLocalVariableInstantiations) {
labelToLineNumber.put(label, actualLineNumber);
}
}

@Override
Expand Down Expand Up @@ -381,6 +391,117 @@ public void visitMethodInsn(int opcode, String owner, String name, String desc,
accessHandler.handleMethodInstruction(owner, name, desc);
}

@Override
public void visitLocalVariable(String name, String desc, String signature, Label start, Label end, int index) {
if (!analyzeLocalVariableInstantiations) {
return;
}
if (name.equals("this") ||
this.codeUnitBuilder.getModifiers().contains(JavaModifier.SYNTHETIC) ||
this.codeUnitBuilder.getName().equals("<init>")) {
return;
}
int lineNumber = start != null ? labelToLineNumber.getOrDefault(start, actualLineNumber) : actualLineNumber;
JavaClassDescriptor type = JavaClassDescriptorImporter.importAsmTypeFromDescriptor(desc);
accessHandler.handleReferencedClassObject(type, lineNumber);
JavaFieldTypeSignatureImporter.parseAsmFieldTypeSignature(signature, new DeclarationHandler() {
@Override
public boolean isNew(String className) {
return false;
}

@Override
public void onNewClass(String className, Optional<String> superclassName, List<String> interfaceNames) {

}

@Override
public void onDeclaredTypeParameters(DomainBuilders.JavaClassTypeParametersBuilder typeParametersBuilder) {

}

@Override
public void onGenericSuperclass(DomainBuilders.JavaParameterizedTypeBuilder<JavaClass> genericSuperclassBuilder) {

}

@Override
public void onGenericInterfaces(List<DomainBuilders.JavaParameterizedTypeBuilder<JavaClass>> genericInterfaceBuilders) {

}

@Override
public void onDeclaredField(DomainBuilders.JavaFieldBuilder fieldBuilder, String fieldTypeName) {

}

@Override
public void onDeclaredConstructor(DomainBuilders.JavaConstructorBuilder constructorBuilder, Collection<String> rawParameterTypeNames) {

}

@Override
public void onDeclaredMethod(DomainBuilders.JavaMethodBuilder methodBuilder, Collection<String> rawParameterTypeNames, String rawReturnTypeName) {

}

@Override
public void onDeclaredStaticInitializer(DomainBuilders.JavaStaticInitializerBuilder staticInitializerBuilder) {

}

@Override
public void onDeclaredClassAnnotations(Set<JavaAnnotationBuilder> annotationBuilders) {

}

@Override
public void onDeclaredMemberAnnotations(String memberName, String descriptor, Set<JavaAnnotationBuilder> annotations) {

}

@Override
public void onDeclaredAnnotationValueType(String valueTypeName) {

}

@Override
public void onDeclaredAnnotationDefaultValue(String methodName, String methodDescriptor, ValueBuilder valueBuilder) {

}

@Override
public void registerEnclosingClass(String ownerName, String enclosingClassName) {

}

@Override
public void registerEnclosingCodeUnit(String ownerName, CodeUnit enclosingCodeUnit) {

}

@Override
public void onDeclaredClassObject(String typeName) {

}

@Override
public void onDeclaredInstanceofCheck(String typeName) {

}

@Override
public void onDeclaredThrowsClause(Collection<String> exceptionTypeNames) {

}

@Override
public void onDeclaredGenericSignatureType(String typeName) {
accessHandler.handleReferencedClassObject(JavaClassDescriptor.From.name(typeName), lineNumber);
}
});
}

@Override
public void visitTypeInsn(int opcode, String type) {
if (opcode == Opcodes.INSTANCEOF) {
Expand Down Expand Up @@ -578,6 +699,7 @@ public void handleTryFinallyBlock(Label start, Label end, Label handler) {
@Override
public void onMethodEnd() {
}

}
}

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

import com.tngtech.archunit.ArchConfiguration;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.domain.ReferencedClassObject;
import com.tngtech.archunit.core.importer.testexamples.referencedclassobjects.ReferencingClassObjectsFromLocalVariable;
import com.tngtech.java.junit.dataprovider.DataProviderRunner;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.io.FilterInputStream;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
import java.util.Set;


import static com.tngtech.archunit.testutil.Assertions.assertThatReferencedClassObjects;
import static com.tngtech.archunit.testutil.assertion.ReferencedClassObjectsAssertion.referencedClassObject;

@RunWith(DataProviderRunner.class)
public class ClassFileImporterLocalVariableDependenciesTest {

@Test
public void imports_referenced_class_object_in_LocalVariable() {
ArchConfiguration.get().setAnalyzeLocalVariableInstantiations(true);

JavaClasses classes = new ClassFileImporter().importClasses(ReferencingClassObjectsFromLocalVariable.class);
Set<ReferencedClassObject> referencedClassObjects = classes.get(ReferencingClassObjectsFromLocalVariable.class).getReferencedClassObjects();

assertThatReferencedClassObjects(referencedClassObjects).containReferencedClassObjects(
referencedClassObject(FilterInputStream.class, 14),
referencedClassObject(List.class, 15),
referencedClassObject(Double.class, 15),
referencedClassObject(FilterInputStream.class, 21),
referencedClassObject(InputStream.class, 22),
referencedClassObject(FilterInputStream.class, 26),
referencedClassObject(InputStream.class, 27),
referencedClassObject(FilterInputStream.class, 32),
referencedClassObject(InputStream.class, 33),
referencedClassObject(FilterInputStream.class, 40),
referencedClassObject(InputStream.class, 41),
referencedClassObject(Map.class, 43),
referencedClassObject(FilterInputStream.class, 43),
referencedClassObject(InputStream.class, 43),
referencedClassObject(Map.class, 45),
referencedClassObject(Set.class, 45),
referencedClassObject(FilterInputStream.class, 45),
referencedClassObject(InputStream.class, 45),
referencedClassObject(Number.class, 53)
);
}

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

import java.io.FilterInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;

@SuppressWarnings("unused")
public class ReferencingClassObjectsFromLocalVariable<T extends Number> {

static {
FilterInputStream streamStatic = null;
@SuppressWarnings("MismatchedQueryAndUpdateOfCollection") List<Double> listStatic = new ArrayList<>();
//noinspection ResultOfMethodCallIgnored
listStatic.size(); // if listStatic is not used it is optimized away. Surprisingly this is not the case for streamStatic above
}

void reference() {
FilterInputStream stream = null;
InputStream stream2 = null;
System.out.println(stream);
System.out.println(stream2);
// after statement and comment
FilterInputStream stream3 = null;
InputStream stream4 = null;
System.out.println(stream3);
System.out.println(stream4);
{
// in block
FilterInputStream stream5 = null;
InputStream stream6 = null;
System.out.println(stream5);
System.out.println(stream6);
}
}

void referenceByGeneric() {
List<FilterInputStream> list = null;
List<InputStream> list2 = null;
// multiple generic parameters
Map<FilterInputStream, InputStream> map = null;
// nested generic parameters
Map<Set<FilterInputStream>, InputStream> map2 = null;
System.out.println(list);
System.out.println(list2);
System.out.println(map);
System.out.println(map2);
}

void referenceToOwnGenericType() {
T myType = null;
System.out.println(myType);
}
}
37 changes: 37 additions & 0 deletions docs/userguide/010_Advanced_Configuration.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -223,3 +223,40 @@ in particular by custom predicates and conditions,
there is at the moment no more sophisticated way than plain text parsing.
Users can tailor this to their specific environments where they know
which sorts of failure formats can appear in practice.

=== Analysis of Local Variable Instantiations

ArchUnit does not analyze local variables by default. There is limited support for it by adding class dependencies
for every local variable instantiation: from the surrounding method to the type of the variable and, if that type has
generic type parameters, also from the method to any concrete generic type. Since this has
a performance impact, it is disabled by default, but it can be activated the following way:

[source,options="nowrap"]
.archunit.properties
----
analyzeLocalVariableInstantiations=true
----

If this feature is enabled, the following code would add class dependencies

* A.m() -> B
* A.m() -> C
* A.m() -> D
* A.m() -> E

which otherwise wouldn't be the case:

[source,java,options="nowrap"]
----
public class A<T extends E> {

void m() {
B var1 = null;
D<C> var2 = null;
T var3 = null;
}

}
----

Note that there are no other uses (method calls, field accesses) of types B, C and D in the code.
Loading