Skip to content

Commit db0088f

Browse files
committed
fix inconsistent behavior with respect to primitive array types
When we tried to resolve the component type of primitive array types (or other types missing from the import), we used `org.objectweb.asm.Type.getType(descriptor)` in some places, which unfortunately uses the canonical name as type name, instead of the class name. Then when the imported classes were checked for the type, e.g. only the type `[B` was present, but not the canonical name `byte[]` and consequently a new type `byte[]` was created that behaved inconsistently and could not return its component type. The correct handling of this case was actually already handled within `JavaType`, thus I have encapsulated all type parsing into this class and its facade `JavaTypeImporter` and written an architecture test to ensure that in the future only `JavaType` and `JavaTypeImporter` will be used to parse types from type names. On the way I noticed that `AnnotationTypeConversion` was actually misleading, it looked like a generic class to handle many different cases when in fact for each caller only one specific case was relevant. Thus I have inlined most of the cases where they are actually relevant (e.g. some places only cared to handle a type `Class[]` but did not care about `org.objectweb.asm.Type`, while the concrete conversion only cared about that case and not e.g. `Class`, etc.). Usually only one specific case was relevant and most of the time there was only one caller. The only thing left is the generic `convert(Object value)` method, since it is called from different places and encapsulates that we need to convert `org.objectweb.asm.Type` transparently, since we do not want to deal with this type anymore outside of `JavaType(Importer)`. Signed-off-by: Peter Gafert <[email protected]>
1 parent 34fa36d commit db0088f

File tree

6 files changed

+112
-94
lines changed

6 files changed

+112
-94
lines changed

archunit-integration-test/src/test/java/com/tngtech/archunit/ArchUnitArchitectureTest.java

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@
1313
import com.tngtech.archunit.core.domain.JavaClass;
1414
import com.tngtech.archunit.core.domain.properties.HasOwner;
1515
import com.tngtech.archunit.core.domain.properties.HasOwner.Predicates.With;
16-
import com.tngtech.archunit.core.importer.ClassFileImporter;
17-
import com.tngtech.archunit.core.importer.DomainBuilders;
1816
import com.tngtech.archunit.core.importer.ImportOption;
1917
import com.tngtech.archunit.core.importer.Location;
2018
import com.tngtech.archunit.junit.AnalyzeClasses;
@@ -59,9 +57,7 @@ public class ArchUnitArchitectureTest {
5957
.whereLayer("Base").mayOnlyBeAccessedByLayers("Root", "Core", "Lang", "Library", "JUnit");
6058

6159
@ArchTest
62-
public static final ArchRule domain_does_not_access_importer =
63-
noClasses().that().resideInAPackage("..core.domain..")
64-
.should().accessClassesThat(belong_to_the_import_context());
60+
public static final ArchRules importer_rules = ArchRules.in(ImporterRules.class);
6561

6662
@ArchTest
6763
public static final ArchRule types_are_only_resolved_via_reflection_in_allowed_places =
@@ -70,18 +66,7 @@ public class ArchUnitArchitectureTest {
7066
.as("no classes should illegally resolve classes via reflection");
7167

7268
@ArchTest
73-
public static final ArchRules public_API_rules =
74-
ArchRules.in(PublicAPIRules.class);
75-
76-
private static DescribedPredicate<JavaClass> belong_to_the_import_context() {
77-
return new DescribedPredicate<JavaClass>("belong to the import context") {
78-
@Override
79-
public boolean apply(JavaClass input) {
80-
return input.getPackageName().startsWith(ClassFileImporter.class.getPackage().getName())
81-
&& !input.getName().contains(DomainBuilders.class.getSimpleName());
82-
}
83-
};
84-
}
69+
public static final ArchRules public_API_rules = ArchRules.in(PublicAPIRules.class);
8570

8671
private static DescribedPredicate<JavaCall<?>> typeIsIllegallyResolvedViaReflection() {
8772
DescribedPredicate<JavaCall<?>> explicitlyAllowedUsage =
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.tngtech.archunit;
2+
3+
import com.tngtech.archunit.base.DescribedPredicate;
4+
import com.tngtech.archunit.core.domain.JavaClass;
5+
import com.tngtech.archunit.core.domain.JavaType;
6+
import com.tngtech.archunit.core.importer.ClassFileImporter;
7+
import com.tngtech.archunit.core.importer.DomainBuilders;
8+
import com.tngtech.archunit.junit.ArchTest;
9+
import com.tngtech.archunit.lang.ArchRule;
10+
11+
import static com.tngtech.archunit.ArchUnitArchitectureTest.THIRDPARTY_PACKAGE_IDENTIFIER;
12+
import static com.tngtech.archunit.base.DescribedPredicate.not;
13+
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.belongToAnyOf;
14+
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
15+
16+
public class ImporterRules {
17+
18+
@ArchTest
19+
public static final ArchRule domain_does_not_access_importer =
20+
noClasses().that().resideInAPackage("..core.domain..")
21+
.should().accessClassesThat(belong_to_the_import_context());
22+
23+
@ArchTest
24+
public static final ArchRule ASM_type_is_only_accessed_within_JavaType_or_JavaTypeImporter =
25+
noClasses()
26+
.that().resideOutsideOfPackage(THIRDPARTY_PACKAGE_IDENTIFIER)
27+
.and(not(belongToAnyOf(JavaType.class)))
28+
.and().doNotHaveFullyQualifiedName("com.tngtech.archunit.core.importer.JavaTypeImporter")
29+
.should().dependOnClassesThat().haveNameMatching(".*\\.asm\\..*Type")
30+
.as("org.objectweb.asm.Type should only be accessed within JavaType(Importer)")
31+
.because("org.objectweb.asm.Type handles array types inconsistently (uses the canonical name instead of the class name), "
32+
+ "so the correct behavior is implemented only within JavaType");
33+
34+
private static DescribedPredicate<JavaClass> belong_to_the_import_context() {
35+
return new DescribedPredicate<JavaClass>("belong to the import context") {
36+
@Override
37+
public boolean apply(JavaClass input) {
38+
return input.getPackageName().startsWith(ClassFileImporter.class.getPackage().getName())
39+
&& !input.getName().contains(DomainBuilders.class.getSimpleName());
40+
}
41+
};
42+
}
43+
}

archunit/src/main/java/com/tngtech/archunit/core/importer/AccessRecord.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import com.tngtech.archunit.core.domain.JavaField;
3737
import com.tngtech.archunit.core.domain.JavaFieldAccess.AccessType;
3838
import com.tngtech.archunit.core.domain.JavaMethod;
39+
import com.tngtech.archunit.core.domain.JavaType;
3940
import com.tngtech.archunit.core.domain.properties.HasDescriptor;
4041
import com.tngtech.archunit.core.domain.properties.HasName;
4142
import com.tngtech.archunit.core.domain.properties.HasOwner;
@@ -44,7 +45,6 @@
4445
import com.tngtech.archunit.core.importer.DomainBuilders.MethodCallTargetBuilder;
4546
import com.tngtech.archunit.core.importer.RawAccessRecord.CodeUnit;
4647
import com.tngtech.archunit.core.importer.RawAccessRecord.TargetInfo;
47-
import org.objectweb.asm.Type;
4848

4949
import static com.google.common.collect.Iterables.getOnlyElement;
5050
import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.createJavaClassList;
@@ -167,7 +167,7 @@ public JavaCodeUnit getCaller() {
167167
public MethodCallTarget getTarget() {
168168
Supplier<Set<JavaMethod>> methodsSupplier = new MethodTargetSupplier(targetOwner.getAllMethods(), record.target);
169169
JavaClassList parameters = getArgumentTypesFrom(record.target.desc, classes);
170-
JavaClass returnType = classes.getOrResolve(Type.getReturnType(record.target.desc).getClassName());
170+
JavaClass returnType = classes.getOrResolve(JavaTypeImporter.importAsmMethodReturnType(record.target.desc).getName());
171171
return new MethodCallTargetBuilder()
172172
.withOwner(targetOwner)
173173
.withName(record.target.name)
@@ -224,7 +224,7 @@ public JavaCodeUnit getCaller() {
224224
@Override
225225
public FieldAccessTarget getTarget() {
226226
Supplier<Optional<JavaField>> fieldSupplier = new FieldTargetSupplier(targetOwner.getAllFields(), record.target);
227-
JavaClass fieldType = classes.getOrResolve(Type.getType(record.target.desc).getClassName());
227+
JavaClass fieldType = classes.getOrResolve(JavaTypeImporter.importAsmType(record.target.desc).getName());
228228
return new FieldAccessTargetBuilder()
229229
.withOwner(targetOwner)
230230
.withName(record.target.name)
@@ -290,8 +290,8 @@ private static <T> Optional<T> uniqueTargetIn(Collection<T> collection) {
290290

291291
private static JavaClassList getArgumentTypesFrom(String descriptor, ImportedClasses classes) {
292292
List<JavaClass> paramTypes = new ArrayList<>();
293-
for (Type type : Type.getArgumentTypes(descriptor)) {
294-
paramTypes.add(classes.getOrResolve(type.getClassName()));
293+
for (JavaType type : JavaTypeImporter.importAsmMethodArgumentTypes(descriptor)) {
294+
paramTypes.add(classes.getOrResolve(type.getName()));
295295
}
296296
return createJavaClassList(paramTypes);
297297
}

archunit/src/main/java/com/tngtech/archunit/core/importer/JavaClassProcessor.java

Lines changed: 34 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,9 @@
2121
import java.util.Collection;
2222
import java.util.HashSet;
2323
import java.util.List;
24-
import java.util.Map;
2524
import java.util.Set;
2625

2726
import com.google.common.collect.ImmutableList;
28-
import com.google.common.collect.ImmutableMap;
2927
import com.google.common.collect.ImmutableSet;
3028
import com.google.common.primitives.Booleans;
3129
import com.google.common.primitives.Bytes;
@@ -36,7 +34,6 @@
3634
import com.google.common.primitives.Longs;
3735
import com.google.common.primitives.Shorts;
3836
import com.tngtech.archunit.Internal;
39-
import com.tngtech.archunit.base.Function;
4037
import com.tngtech.archunit.base.HasDescription;
4138
import com.tngtech.archunit.base.Optional;
4239
import com.tngtech.archunit.core.MayResolveTypesViaReflection;
@@ -207,7 +204,7 @@ public FieldVisitor visitField(int access, String name, String desc, String sign
207204

208205
DomainBuilders.JavaFieldBuilder fieldBuilder = new DomainBuilders.JavaFieldBuilder()
209206
.withName(name)
210-
.withType(JavaTypeImporter.importAsmType(Type.getType(desc)))
207+
.withType(JavaTypeImporter.importAsmType(desc))
211208
.withModifiers(JavaModifier.getModifiersForField(access))
212209
.withDescriptor(desc);
213210
declarationHandler.onDeclaredField(fieldBuilder);
@@ -221,29 +218,21 @@ public MethodVisitor visitMethod(int access, String name, String desc, String si
221218
}
222219

223220
LOG.trace("Analyzing method {}.{}:{}", className, name, desc);
224-
accessHandler.setContext(new CodeUnit(name, namesOf(Type.getArgumentTypes(desc)), className));
221+
List<JavaType> parameters = JavaTypeImporter.importAsmMethodArgumentTypes(desc);
222+
accessHandler.setContext(new CodeUnit(name, namesOf(parameters), className));
225223

226224
DomainBuilders.JavaCodeUnitBuilder<?, ?> codeUnitBuilder = addCodeUnitBuilder(name);
227-
Type methodType = Type.getMethodType(desc);
228225
codeUnitBuilder
229226
.withName(name)
230227
.withModifiers(JavaModifier.getModifiersForMethod(access))
231-
.withParameters(typesFrom(methodType.getArgumentTypes()))
232-
.withReturnType(JavaTypeImporter.importAsmType(methodType.getReturnType()))
228+
.withParameters(parameters)
229+
.withReturnType(JavaTypeImporter.importAsmMethodReturnType(desc))
233230
.withDescriptor(desc)
234231
.withThrowsClause(typesFrom(exceptions));
235232

236233
return new MethodProcessor(className, accessHandler, codeUnitBuilder);
237234
}
238235

239-
private List<JavaType> typesFrom(Type[] asmTypes) {
240-
List<JavaType> result = new ArrayList<>();
241-
for (Type asmType : asmTypes) {
242-
result.add(JavaTypeImporter.importAsmType(asmType));
243-
}
244-
return result;
245-
}
246-
247236
private List<JavaType> typesFrom(String[] throwsDeclarations) {
248237
List<JavaType> result = new ArrayList<>();
249238
if (throwsDeclarations != null) {
@@ -289,10 +278,10 @@ public void visitEnd() {
289278
LOG.trace("Done analyzing {}", className);
290279
}
291280

292-
private static List<String> namesOf(Type[] types) {
281+
private static List<String> namesOf(Iterable<JavaType> types) {
293282
ImmutableList.Builder<String> result = ImmutableList.builder();
294-
for (Type type : types) {
295-
result.add(JavaTypeImporter.importAsmType(type).getName());
283+
for (JavaType type : types) {
284+
result.add(type.getName());
296285
}
297286
return result.build();
298287
}
@@ -487,7 +476,7 @@ public void visitEnd() {
487476
}
488477

489478
private static DomainBuilders.JavaAnnotationBuilder annotationBuilderFor(String desc) {
490-
return new DomainBuilders.JavaAnnotationBuilder().withType(JavaTypeImporter.importAsmType(Type.getType(desc)));
479+
return new DomainBuilders.JavaAnnotationBuilder().withType(JavaTypeImporter.importAsmType(desc));
491480
}
492481

493482
private static class AnnotationProcessor extends AnnotationVisitor {
@@ -575,7 +564,11 @@ private AnnotationArrayProcessor(AnnotationArrayContext annotationArrayContext)
575564

576565
@Override
577566
public void visit(String name, Object value) {
578-
setDerivedComponentType(value);
567+
if (value instanceof Type) {
568+
setDerivedComponentType(JavaClass.class);
569+
} else {
570+
setDerivedComponentType(value.getClass());
571+
}
579572
values.add(AnnotationTypeConversion.convert(value));
580573
}
581574

@@ -596,16 +589,11 @@ public void visitEnum(String name, final String desc, final String value) {
596589
values.add(javaEnumBuilder(desc, value));
597590
}
598591

599-
private void setDerivedComponentType(Object value) {
600-
setDerivedComponentType(value.getClass());
601-
}
602-
603592
// NOTE: If the declared annotation is not imported itself, we can still use this heuristic,
604593
// to determine additional information about the respective array.
605594
// (It the annotation is imported itself, we could easily determine this from the respective
606595
// JavaClass methods)
607596
private void setDerivedComponentType(Class<?> type) {
608-
type = AnnotationTypeConversion.convert(type);
609597
checkState(derivedComponentType == null || derivedComponentType.equals(type),
610598
"Found mixed component types while importing array, this is most likely a bug");
611599

@@ -668,22 +656,21 @@ private Optional<Class<?>> determineComponentType(ClassesByTypeName importedClas
668656
.tryGetMethod(annotationArrayContext.getDeclaringAnnotationMemberName());
669657

670658
return method.isPresent() ?
671-
determineComponentTypeFromReturnValue(method) :
659+
determineComponentTypeFromReturnValue(method.get()) :
672660
Optional.<Class<?>>absent();
673661
}
674662

675-
private Optional<Class<?>> determineComponentTypeFromReturnValue(Optional<JavaMethod> method) {
676-
String name = method.get().getRawReturnType().getName();
677-
Optional<Class<?>> result = AnnotationTypeConversion.tryConvert(name);
678-
if (result.isPresent()) {
679-
return Optional.<Class<?>>of(result.get().getComponentType());
663+
private Optional<Class<?>> determineComponentTypeFromReturnValue(JavaMethod method) {
664+
if (method.getRawReturnType().isEquivalentTo(Class[].class)) {
665+
return Optional.<Class<?>>of(JavaClass.class);
680666
}
681-
return resolveComponentTypeFrom(name);
667+
668+
return resolveComponentTypeFrom(method.getRawReturnType().getName());
682669
}
683670

684671
@MayResolveTypesViaReflection(reason = "Resolving primitives does not really use reflection")
685-
private Optional<Class<?>> resolveComponentTypeFrom(String name) {
686-
JavaType type = JavaType.From.name(name);
672+
private Optional<Class<?>> resolveComponentTypeFrom(String arrayTypeName) {
673+
JavaType type = JavaType.From.name(arrayTypeName);
687674
JavaType componentType = getComponentType(type);
688675

689676
if (componentType.isPrimitive()) {
@@ -725,50 +712,25 @@ private static ValueBuilder javaEnumBuilder(final String desc, final String valu
725712
public <T extends HasDescription> Optional<Object> build(T owner, ClassesByTypeName importedClasses) {
726713
return Optional.<Object>of(
727714
new DomainBuilders.JavaEnumConstantBuilder()
728-
.withDeclaringClass(importedClasses.get(Type.getType(desc).getClassName()))
715+
.withDeclaringClass(importedClasses.get(JavaTypeImporter.importAsmType(desc).getName()))
729716
.withName(value)
730717
.build());
731718
}
732719
};
733720
}
734721

735722
private static class AnnotationTypeConversion {
736-
private static final Map<String, Class<?>> externalTypeToInternalType = ImmutableMap.of(
737-
Type.class.getName(), JavaClass.class,
738-
Class.class.getName(), JavaClass.class,
739-
Class[].class.getName(), JavaClass[].class
740-
);
741-
private static final Map<Class<?>, Function<Object, ValueBuilder>> importedValueToInternalValue =
742-
ImmutableMap.<Class<?>, Function<Object, ValueBuilder>>of(
743-
Type.class, new Function<Object, ValueBuilder>() {
744-
@Override
745-
public ValueBuilder apply(final Object input) {
746-
return new ValueBuilder() {
747-
@Override
748-
public <T extends HasDescription> Optional<Object> build(T owner, ClassesByTypeName importedClasses) {
749-
return Optional.<Object>of(importedClasses.get(((Type) input).getClassName()));
750-
}
751-
};
752-
}
753-
}
754-
);
755-
756-
static Class<?> convert(Class<?> type) {
757-
return externalTypeToInternalType.containsKey(type.getName()) ?
758-
externalTypeToInternalType.get(type.getName()) :
759-
type;
760-
}
761-
762-
static Optional<Class<?>> tryConvert(String typeName) {
763-
return externalTypeToInternalType.containsKey(typeName) ?
764-
Optional.<Class<?>>of(externalTypeToInternalType.get(typeName)) :
765-
Optional.<Class<?>>absent();
766-
}
767-
768-
static ValueBuilder convert(Object value) {
769-
return importedValueToInternalValue.containsKey(value.getClass()) ?
770-
importedValueToInternalValue.get(value.getClass()).apply(value) :
771-
ValueBuilder.ofFinished(value);
723+
static ValueBuilder convert(Object input) {
724+
final Object value = JavaTypeImporter.importAsmTypeIfPossible(input);
725+
if (value instanceof JavaType) {
726+
return new ValueBuilder() {
727+
@Override
728+
public <T extends HasDescription> Optional<Object> build(T owner, ClassesByTypeName importedClasses) {
729+
return Optional.<Object>of(importedClasses.get(((JavaType) value).getName()));
730+
}
731+
};
732+
}
733+
return ValueBuilder.ofFinished(value);
772734
}
773735
}
774736
}

archunit/src/main/java/com/tngtech/archunit/core/importer/JavaTypeImporter.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
*/
1616
package com.tngtech.archunit.core.importer;
1717

18+
import java.util.List;
19+
20+
import com.google.common.collect.ImmutableList;
1821
import com.tngtech.archunit.core.domain.JavaType;
1922
import org.objectweb.asm.Type;
2023

@@ -30,4 +33,24 @@ static JavaType createFromAsmObjectTypeName(String objectTypeName) {
3033
static JavaType importAsmType(Type type) {
3134
return JavaType.From.name(type.getClassName());
3235
}
36+
37+
static Object importAsmTypeIfPossible(Object value) {
38+
return value instanceof Type ? importAsmType((Type) value) : value;
39+
}
40+
41+
static JavaType importAsmType(String typeDescriptor) {
42+
return importAsmType(Type.getType(typeDescriptor));
43+
}
44+
45+
static List<JavaType> importAsmMethodArgumentTypes(String methodDescriptor) {
46+
ImmutableList.Builder<JavaType> result = ImmutableList.builder();
47+
for (Type type : Type.getArgumentTypes(methodDescriptor)) {
48+
result.add(importAsmType(type));
49+
}
50+
return result.build();
51+
}
52+
53+
static JavaType importAsmMethodReturnType(String methodDescriptor) {
54+
return importAsmType(Type.getReturnType(methodDescriptor));
55+
}
3356
}

archunit/src/test/java/com/tngtech/archunit/testutil/Assertions.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,8 +326,13 @@ public void matches(Class<?> clazz) {
326326
.isEqualTo(getExpectedPackageName(clazz));
327327
assertThat(actual.getModifiers()).as("Modifiers of " + actual)
328328
.isEqualTo(JavaModifier.getModifiersForClass(clazz.getModifiers()));
329+
assertThat(actual.isArray()).as(actual + " is array").isEqualTo(clazz.isArray());
329330
assertThat(propertiesOf(actual.getAnnotations())).as("Annotations of " + actual)
330331
.isEqualTo(propertiesOf(clazz.getAnnotations()));
332+
333+
if (clazz.isArray()) {
334+
new JavaClassAssertion(actual.getComponentType()).matches(clazz.getComponentType());
335+
}
331336
}
332337

333338
private String ensureArrayName(String name) {

0 commit comments

Comments
 (0)