Skip to content

Commit

Permalink
add referenced class objects to JavaClass
Browse files Browse the repository at this point in the history
To detect usages of class objects we need to look which classes are referenced in the constant pool. So far the following usage of `SomeClass` would not have been detected by ArchUnit:

```
class Example {
  private Map<Class<?>, Value> map = Map.of(SomeClass.class, someValue);
}
```

In other words, pure usages of `Foo.class` could not be detected, because there was no "access" to `Foo` in the bytecode. However, for each such occurrence the class would actually be present in the constant pool of the referring class. While ASM does not allow direct access to the constant pool, it does allow us to hook into `ldc` instructions, i.e. load constant instructions in the bytecode, that will in turn reference the respective class. The way to detect this is principally an `instanceof` check for `org.objectweb.asm.Type` in `void visitLdcInsn(Object value)`. As far as I could see, any reference of another class as a constant would cause this method to be called with the respective ASM `Type` object.

It would actually be possible to import a lot more constants here. I have looked a little into adding all supported constant types, so it would e.g. be possible to have assertions on `String` values, but then decided to let it go for now. Strings would still be simple, but as soon as `Integer` comes into play (which could also be imported), there are a lot of internal optimizations by the JVM (e.g. `iconst_1`, ...), which makes it hard to do this in a consistent way. I think the most valuable feature by far is to detect constant loads of classes (since those cause dependencies), so I decided to keep it simple for now.

Signed-off-by: Peter Gafert <[email protected]>
  • Loading branch information
codecholeric committed Jan 30, 2021
1 parent 1b00986 commit 33b1e45
Show file tree
Hide file tree
Showing 13 changed files with 363 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@ public static Source createSource(URI uri, Optional<String> sourceFileName, bool
return new Source(uri, sourceFileName, md5InClassSourcesEnabled);
}

public static ReferencedClassObject createReferencedClassObject(JavaCodeUnit codeUnit, JavaClass javaClass, int lineNumber) {
return ReferencedClassObject.from(codeUnit, javaClass, lineNumber);
}

public static <CODE_UNIT extends JavaCodeUnit> ThrowsClause<CODE_UNIT> createThrowsClause(CODE_UNIT codeUnit, List<JavaClass> types) {
return ThrowsClause.from(codeUnit, types);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,15 @@ public List<JavaTypeVariable<JavaClass>> getTypeParameters() {
return typeParameters;
}

@PublicAPI(usage = ACCESS)
public Set<ReferencedClassObject> getReferencedClassObjects() {
ImmutableSet.Builder<ReferencedClassObject> result = ImmutableSet.builder();
for (JavaCodeUnit codeUnit : codeUnits) {
result.addAll(codeUnit.getReferencedClassObjects());
}
return result.build();
}

@Override
@PublicAPI(usage = ACCESS)
public JavaClass toErasure() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public abstract class JavaCodeUnit extends JavaMember implements HasParameterTyp
private final JavaClass returnType;
private final JavaClassList parameters;
private final String fullName;
private final Set<ReferencedClassObject> referencedClassObjects;
private final Set<InstanceofCheck> instanceofChecks;

private Set<JavaFieldAccess> fieldAccesses = Collections.emptySet();
Expand All @@ -61,7 +62,8 @@ public abstract class JavaCodeUnit extends JavaMember implements HasParameterTyp
this.returnType = builder.getReturnType();
this.parameters = builder.getParameters();
fullName = formatMethod(getOwner().getName(), getName(), getRawParameterTypes());
instanceofChecks = builder.getInstanceofChecks(this);
referencedClassObjects = ImmutableSet.copyOf(builder.getReferencedClassObjects(this));
instanceofChecks = ImmutableSet.copyOf(builder.getInstanceofChecks(this));
}

/**
Expand Down Expand Up @@ -112,6 +114,11 @@ public Set<JavaConstructorCall> getConstructorCallsFromSelf() {
return constructorCalls;
}

@PublicAPI(usage = ACCESS)
public Set<ReferencedClassObject> getReferencedClassObjects() {
return referencedClassObjects;
}

@PublicAPI(usage = ACCESS)
public Set<InstanceofCheck> getInstanceofChecks() {
return instanceofChecks;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* Copyright 2014-2021 TNG Technology Consulting GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tngtech.archunit.core.domain;

import com.tngtech.archunit.PublicAPI;
import com.tngtech.archunit.base.ChainableFunction;
import com.tngtech.archunit.core.domain.properties.HasOwner;
import com.tngtech.archunit.core.domain.properties.HasSourceCodeLocation;
import com.tngtech.archunit.core.domain.properties.HasType;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.tngtech.archunit.PublicAPI.Usage.ACCESS;

@PublicAPI(usage = ACCESS)
public final class ReferencedClassObject implements HasType, HasOwner<JavaCodeUnit>, HasSourceCodeLocation {
private final JavaCodeUnit owner;
private final JavaClass value;
private final int lineNumber;
private final SourceCodeLocation sourceCodeLocation;

private ReferencedClassObject(JavaCodeUnit owner, JavaClass value, int lineNumber) {
this.owner = checkNotNull(owner);
this.value = checkNotNull(value);
this.lineNumber = lineNumber;
sourceCodeLocation = SourceCodeLocation.of(owner.getOwner(), lineNumber);
}

@Override
@PublicAPI(usage = ACCESS)
public JavaType getType() {
return getRawType();
}

@Override
@PublicAPI(usage = ACCESS)
public JavaClass getRawType() {
return value;
}

@Override
@PublicAPI(usage = ACCESS)
public JavaCodeUnit getOwner() {
return owner;
}

@PublicAPI(usage = ACCESS)
public JavaClass getValue() {
return value;
}

@PublicAPI(usage = ACCESS)
public int getLineNumber() {
return lineNumber;
}

@Override
@PublicAPI(usage = ACCESS)
public SourceCodeLocation getSourceCodeLocation() {
return sourceCodeLocation;
}

@Override
public String toString() {
return getClass().getSimpleName() + "{value=" + value + ",lineNumber=" + lineNumber + '}';
}

static ReferencedClassObject from(JavaCodeUnit owner, JavaClass javaClass, int lineNumber) {
return new ReferencedClassObject(owner, javaClass, lineNumber);
}

@PublicAPI(usage = ACCESS)
public static final class Functions {
private Functions() {
}

@PublicAPI(usage = ACCESS)
public static final ChainableFunction<ReferencedClassObject, JavaClass> GET_VALUE = new ChainableFunction<ReferencedClassObject, JavaClass>() {
@Override
public JavaClass apply(ReferencedClassObject input) {
return input.getValue();
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
import com.tngtech.archunit.core.domain.JavaType;
import com.tngtech.archunit.core.domain.JavaTypeVariable;
import com.tngtech.archunit.core.domain.JavaWildcardType;
import com.tngtech.archunit.core.domain.ReferencedClassObject;
import com.tngtech.archunit.core.domain.Source;
import com.tngtech.archunit.core.domain.ThrowsClause;

Expand All @@ -67,6 +68,7 @@
import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.completeTypeVariable;
import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.createInstanceofCheck;
import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.createJavaClassList;
import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.createReferencedClassObject;
import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.createSource;
import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.createThrowsClause;
import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.createTypeVariable;
Expand Down Expand Up @@ -217,6 +219,7 @@ public abstract static class JavaCodeUnitBuilder<OUTPUT, SELF extends JavaCodeUn
private JavaClassDescriptor returnType;
private List<JavaClassDescriptor> parameters;
private List<JavaClassDescriptor> throwsDeclarations;
private final Set<RawReferencedClassObject> rawReferencedClassObjects = new HashSet<>();
private final List<RawInstanceofCheck> instanceOfChecks = new ArrayList<>();

private JavaCodeUnitBuilder() {
Expand All @@ -237,6 +240,11 @@ SELF withThrowsClause(List<JavaClassDescriptor> throwsDeclarations) {
return self();
}

SELF addReferencedClassObject(RawReferencedClassObject rawReferencedClassObject) {
rawReferencedClassObjects.add(rawReferencedClassObject);
return self();
}

SELF addInstanceOfCheck(RawInstanceofCheck rawInstanceOfChecks) {
this.instanceOfChecks.add(rawInstanceOfChecks);
return self();
Expand All @@ -262,6 +270,14 @@ public <CODE_UNIT extends JavaCodeUnit> ThrowsClause<CODE_UNIT> getThrowsClause(
return createThrowsClause(codeUnit, asJavaClasses(this.throwsDeclarations));
}

public Set<ReferencedClassObject> getReferencedClassObjects(JavaCodeUnit codeUnit) {
ImmutableSet.Builder<ReferencedClassObject> result = ImmutableSet.builder();
for (RawReferencedClassObject rawReferencedClassObject : this.rawReferencedClassObjects) {
result.add(createReferencedClassObject(codeUnit, get(rawReferencedClassObject.getClassName()), rawReferencedClassObject.getLineNumber()));
}
return result.build();
}

public Set<InstanceofCheck> getInstanceofChecks(JavaCodeUnit codeUnit) {
ImmutableSet.Builder<InstanceofCheck> result = ImmutableSet.builder();
for (RawInstanceofCheck instanceOfCheck : this.instanceOfChecks) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,14 @@ class JavaClassDescriptorImporter {
* i.e. java/lang/Object (note that this is not a descriptor like Ljava/lang/Object;)
*/
static JavaClassDescriptor createFromAsmObjectTypeName(String objectTypeName) {
return importAsmType(Type.getObjectType(objectTypeName));
return JavaClassDescriptor.From.name(Type.getObjectType(objectTypeName).getClassName());
}

static JavaClassDescriptor importAsmType(Type type) {
static JavaClassDescriptor importAsmType(Object type) {
return importAsmType((Type) type);
}

private static JavaClassDescriptor importAsmType(Type type) {
return JavaClassDescriptor.From.name(type.getClassName());
}

Expand All @@ -39,7 +43,7 @@ static boolean isAsmType(Object value) {
}

static Object importAsmTypeIfPossible(Object value) {
return isAsmType(value) ? importAsmType((Type) value) : value;
return isAsmType(value) ? importAsmType(value) : value;
}

static JavaClassDescriptor importAsmTypeFromDescriptor(String typeDescriptor) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,14 @@ public void visitLineNumber(int line, Label start) {
accessHandler.setLineNumber(actualLineNumber);
}

@Override
public void visitLdcInsn(Object value) {
if (JavaClassDescriptorImporter.isAsmType(value)) {
codeUnitBuilder.addReferencedClassObject(
RawReferencedClassObject.from(JavaClassDescriptorImporter.importAsmType(value), actualLineNumber));
}
}

@Override
public void visitFieldInsn(int opcode, String owner, String name, String desc) {
accessHandler.handleFieldInstruction(opcode, owner, name, desc);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright 2014-2021 TNG Technology Consulting GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tngtech.archunit.core.importer;

import com.tngtech.archunit.core.domain.JavaClassDescriptor;

import static com.google.common.base.MoreObjects.toStringHelper;
import static com.google.common.base.Preconditions.checkNotNull;

class RawReferencedClassObject {
private final JavaClassDescriptor type;
private final int lineNumber;

private RawReferencedClassObject(JavaClassDescriptor type, int lineNumber) {
this.type = checkNotNull(type);
this.lineNumber = lineNumber;
}

static RawReferencedClassObject from(JavaClassDescriptor target, int lineNumber) {
return new RawReferencedClassObject(target, lineNumber);
}

String getClassName() {
return type.getFullyQualifiedClassName();
}

int getLineNumber() {
return lineNumber;
}

@Override
public String toString() {
return toStringHelper(this)
.add("type", type)
.add("lineNumber", lineNumber)
.toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.tngtech.archunit.core.domain;

import java.io.File;

import com.tngtech.archunit.core.importer.ClassFileImporter;
import org.junit.Test;

import static com.google.common.collect.Iterables.getOnlyElement;
import static com.tngtech.archunit.core.domain.ReferencedClassObject.Functions.GET_VALUE;
import static org.assertj.core.api.Assertions.assertThat;

public class ReferencedClassObjectTest {

@Test
public void function_getValue() {
class SomeClass {
@SuppressWarnings("unused")
Class<?> call() {
return File.class;
}
}

JavaClasses classes = new ClassFileImporter().importClasses(SomeClass.class, File.class);
JavaMethod owner = classes.get(SomeClass.class).getMethod("call");

ReferencedClassObject referencedClassObject = getOnlyElement(owner.getReferencedClassObjects());

assertThat(GET_VALUE.apply(referencedClassObject)).isEqualTo(classes.get(File.class));
}
}
Loading

0 comments on commit 33b1e45

Please sign in to comment.