Skip to content

Commit c5647cc

Browse files
committed
support generic array types in generic type signature
We can now handle generic array types like `T[][]` in `Foo<T, A extends List<T[][]>>`. Note that a generic array type cannot be a toplevel bound of a generic type, but can only appear as part of the nested type signature. Signed-off-by: Peter Gafert <peter.gafert@tngtech.com>
1 parent c10a8a9 commit c5647cc

File tree

8 files changed

+257
-13
lines changed

8 files changed

+257
-13
lines changed

archunit/src/main/java/com/tngtech/archunit/core/domain/DomainObjectCreationContext.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@
4848
import com.tngtech.archunit.core.importer.DomainBuilders.JavaWildcardTypeBuilder;
4949
import com.tngtech.archunit.core.importer.DomainBuilders.MethodCallTargetBuilder;
5050

51+
import static com.google.common.base.Preconditions.checkArgument;
52+
5153
/**
5254
* Together with {@link DomainBuilders}, this class is the link to create domain objects from the import
5355
* context. To make the API clear, we try to keep only those methods public, which are really meant to be used.
@@ -160,6 +162,12 @@ public static void completeTypeVariable(JavaTypeVariable variable, List<JavaType
160162
variable.setUpperBounds(upperBounds);
161163
}
162164

165+
public static JavaGenericArrayType createGenericArrayType(JavaType componentType, JavaClass erasure) {
166+
checkArgument(componentType instanceof JavaTypeVariable || componentType instanceof JavaGenericArrayType,
167+
"Component type of a generic array type can only be a type variable or a generic array type. This is most likely a bug.");
168+
return new JavaGenericArrayType(componentType.getName() + "[]", componentType, erasure);
169+
}
170+
163171
public static JavaWildcardType createWildcardType(JavaWildcardTypeBuilder builder) {
164172
return new JavaWildcardType(builder);
165173
}

archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClassDescriptor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ private static String createObjectComponentType(String componentTypeName) {
134134
return "L" + componentTypeName + ";";
135135
}
136136

137-
static JavaClassDescriptor javaClass(JavaClass javaClass) {
137+
public static JavaClassDescriptor javaClass(JavaClass javaClass) {
138138
return name(javaClass.getName());
139139
}
140140

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright 2014-2020 TNG Technology Consulting GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.tngtech.archunit.core.domain;
17+
18+
import com.tngtech.archunit.PublicAPI;
19+
20+
import static com.google.common.base.Preconditions.checkNotNull;
21+
import static com.tngtech.archunit.PublicAPI.Usage.ACCESS;
22+
23+
/**
24+
* Represents a generic array type used in signatures of parameterized types.<br>
25+
* E.g. for {@code MyClass<A, T extends List<A[]>>} the upper bound {@code List<A[]>}
26+
* would have one {@link JavaGenericArrayType} {@code A[]} as its type parameter.<br>
27+
* Like its concrete counterpart a {@link JavaGenericArrayType} can be queried for its
28+
* {@link #getComponentType() component type}, which will by definition be a
29+
* {@link JavaTypeVariable} or a {@link JavaGenericArrayType} corresponding to a lower dimensional array.
30+
*/
31+
@PublicAPI(usage = ACCESS)
32+
public final class JavaGenericArrayType implements JavaType {
33+
private final String name;
34+
private final JavaType componentType;
35+
private final JavaClass erasure;
36+
37+
JavaGenericArrayType(String name, JavaType componentType, JavaClass erasure) {
38+
this.name = checkNotNull(name);
39+
this.componentType = checkNotNull(componentType);
40+
this.erasure = checkNotNull(erasure);
41+
}
42+
43+
/**
44+
* @return The name of this {@link JavaGenericArrayType}, e.g. for {@code A[]} within
45+
* signature {@code MyClass<A, T extends List<A[]>>} the name would be "A[]"
46+
*/
47+
@Override
48+
@PublicAPI(usage = ACCESS)
49+
public String getName() {
50+
return name;
51+
}
52+
53+
/**
54+
* @return The component type of this {@link JavaGenericArrayType}, e.g. for {@code A[]} within
55+
* signature {@code MyClass<A, T extends List<A[]>>} the component type would be {@code A},
56+
* while for {@code A[][]} within {@code MyClass<A, T extends List<A[][]>>} the component
57+
* type would be {@code A[]}.
58+
*/
59+
@PublicAPI(usage = ACCESS)
60+
public JavaType getComponentType() {
61+
return componentType;
62+
}
63+
64+
@Override
65+
@PublicAPI(usage = ACCESS)
66+
public JavaClass toErasure() {
67+
return erasure;
68+
}
69+
70+
@Override
71+
public String toString() {
72+
return getClass().getSimpleName() + '{' + getName() + '}';
73+
}
74+
}

archunit/src/main/java/com/tngtech/archunit/core/domain/JavaType.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ public interface JavaType extends HasName {
2929
* <li>the class itself, if this type is a {@link JavaClass}</li>
3030
* <li>the {@link JavaClass} equivalent to {@link Object}, if this type is an unbound {@link JavaTypeVariable}</li>
3131
* <li>the {@link JavaClass} equivalent to the erasure of the left most bound, if this type is a bound {@link JavaTypeVariable}</li>
32+
* <li>if this type is a {@link JavaGenericArrayType}, the erasure will be the {@link JavaClass}
33+
* equivalent to the array type that has the erasure of the generic component type of this type as its component type;
34+
* e.g. take the generic array type {@code T[][]} where {@code T} is unbound, then the erasure will be the array type {@code Object[][]}</li>
3235
* </ul>
3336
*/
3437
@PublicAPI(usage = ACCESS)

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

Lines changed: 78 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import com.google.common.base.Function;
2222
import com.google.common.base.Functions;
23+
import com.tngtech.archunit.core.domain.JavaClass;
2324
import com.tngtech.archunit.core.domain.JavaClassDescriptor;
2425
import com.tngtech.archunit.core.domain.JavaType;
2526
import com.tngtech.archunit.core.domain.JavaTypeVariable;
@@ -36,6 +37,7 @@
3637
import org.slf4j.LoggerFactory;
3738

3839
import static com.google.common.base.Functions.compose;
40+
import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.createGenericArrayType;
3941
import static com.tngtech.archunit.core.importer.ClassFileProcessor.ASM_API_VERSION;
4042

4143
class JavaGenericTypeImporter {
@@ -109,12 +111,12 @@ public void visitTypeArgument() {
109111
@Override
110112
public void visitTypeVariable(String name) {
111113
log.trace("Encountered upper bound for {}: Type variable {}", currentType.getName(), name);
112-
currentType.addBound(new ReferenceCreationProcess(name));
114+
currentType.addBound(new ReferenceCreationProcess(name, ReferenceCreationProcess.JavaTypeVariableFinisher.IDENTITY));
113115
}
114116

115117
@Override
116118
public SignatureVisitor visitTypeArgument(char wildcard) {
117-
return TypeArgumentProcessor.create(wildcard, currentBound, Functions.<JavaClassDescriptor>identity());
119+
return TypeArgumentProcessor.create(wildcard, currentBound, Functions.<JavaClassDescriptor>identity(), ReferenceCreationProcess.JavaTypeVariableFinisher.IDENTITY);
118120
}
119121
}
120122
}
@@ -134,13 +136,19 @@ public JavaType finish(Iterable<JavaTypeVariable> allTypeParametersInContext, Cl
134136

135137
private static class ReferenceCreationProcess implements JavaTypeCreationProcess {
136138
private final String typeVariableName;
139+
private final JavaTypeVariableFinisher finisher;
137140

138-
ReferenceCreationProcess(String typeVariableName) {
141+
ReferenceCreationProcess(String typeVariableName, JavaTypeVariableFinisher finisher) {
139142
this.typeVariableName = typeVariableName;
143+
this.finisher = finisher;
140144
}
141145

142146
@Override
143147
public JavaType finish(Iterable<JavaTypeVariable> allTypeParametersInContext, ClassesByTypeName classes) {
148+
return finisher.finish(createTypeVariable(allTypeParametersInContext, classes), classes);
149+
}
150+
151+
private JavaType createTypeVariable(Iterable<JavaTypeVariable> allTypeParametersInContext, ClassesByTypeName classes) {
144152
for (JavaTypeVariable existingTypeVariable : allTypeParametersInContext) {
145153
if (existingTypeVariable.getName().equals(typeVariableName)) {
146154
return existingTypeVariable;
@@ -149,6 +157,38 @@ public JavaType finish(Iterable<JavaTypeVariable> allTypeParametersInContext, Cl
149157
// type variables can be missing from the import context -> create a simple unbound type variable since we have no more information
150158
return new JavaTypeParameterBuilder(typeVariableName).build(classes);
151159
}
160+
161+
abstract static class JavaTypeVariableFinisher {
162+
abstract JavaType finish(JavaType input, ClassesByTypeName classes);
163+
164+
abstract String getFinishedName(String name);
165+
166+
JavaTypeVariableFinisher after(final JavaTypeVariableFinisher other) {
167+
return new JavaTypeVariableFinisher() {
168+
@Override
169+
JavaType finish(JavaType input, ClassesByTypeName classes) {
170+
return JavaTypeVariableFinisher.this.finish(other.finish(input, classes), classes);
171+
}
172+
173+
@Override
174+
String getFinishedName(String name) {
175+
return JavaTypeVariableFinisher.this.getFinishedName(other.getFinishedName(name));
176+
}
177+
};
178+
}
179+
180+
static JavaTypeVariableFinisher IDENTITY = new JavaTypeVariableFinisher() {
181+
@Override
182+
JavaType finish(JavaType input, ClassesByTypeName classes) {
183+
return input;
184+
}
185+
186+
@Override
187+
String getFinishedName(String name) {
188+
return name;
189+
}
190+
};
191+
}
152192
}
153193

154194
private static class TypeArgumentProcessor extends SignatureVisitor {
@@ -158,18 +198,37 @@ public JavaClassDescriptor apply(JavaClassDescriptor input) {
158198
return input.toArrayDescriptor();
159199
}
160200
};
201+
private static final ReferenceCreationProcess.JavaTypeVariableFinisher GENERIC_ARRAY_CREATOR = new ReferenceCreationProcess.JavaTypeVariableFinisher() {
202+
@Override
203+
public JavaType finish(JavaType componentType, ClassesByTypeName classes) {
204+
JavaClassDescriptor erasureType = JavaClassDescriptor.From.javaClass(componentType.toErasure()).toArrayDescriptor();
205+
JavaClass erasure = classes.get(erasureType.getFullyQualifiedClassName());
206+
return createGenericArrayType(componentType, erasure);
207+
}
208+
209+
@Override
210+
String getFinishedName(String name) {
211+
return name + "[]";
212+
}
213+
};
161214

162215
private final TypeArgumentType typeArgumentType;
163216
private final JavaParameterizedTypeBuilder parameterizedType;
164217
private final Function<JavaClassDescriptor, JavaClassDescriptor> typeMapping;
218+
private final ReferenceCreationProcess.JavaTypeVariableFinisher typeVariableFinisher;
165219

166220
private JavaParameterizedTypeBuilder currentTypeArgument;
167221

168-
TypeArgumentProcessor(TypeArgumentType typeArgumentType, JavaParameterizedTypeBuilder parameterizedType, Function<JavaClassDescriptor, JavaClassDescriptor> typeMapping) {
222+
TypeArgumentProcessor(
223+
TypeArgumentType typeArgumentType,
224+
JavaParameterizedTypeBuilder parameterizedType,
225+
Function<JavaClassDescriptor, JavaClassDescriptor> typeMapping,
226+
ReferenceCreationProcess.JavaTypeVariableFinisher typeVariableFinisher) {
169227
super(ASM_API_VERSION);
170228
this.typeArgumentType = typeArgumentType;
171229
this.parameterizedType = parameterizedType;
172230
this.typeMapping = typeMapping;
231+
this.typeVariableFinisher = typeVariableFinisher;
173232
}
174233

175234
@Override
@@ -188,28 +247,35 @@ public void visitTypeArgument() {
188247

189248
@Override
190249
public void visitTypeVariable(String name) {
191-
log.trace("Encountered {} for {}: Type variable {}", typeArgumentType.description, parameterizedType.getTypeName(), name);
192-
typeArgumentType.addTypeArgumentToBuilder(parameterizedType, new ReferenceCreationProcess(name));
250+
if (log.isTraceEnabled()) {
251+
log.trace("Encountered {} for {}: Type variable {}", typeArgumentType.description, parameterizedType.getTypeName(), typeVariableFinisher.getFinishedName(name));
252+
}
253+
typeArgumentType.addTypeArgumentToBuilder(parameterizedType, new ReferenceCreationProcess(name, typeVariableFinisher));
193254
}
194255

195256
@Override
196257
public SignatureVisitor visitTypeArgument(char wildcard) {
197-
return TypeArgumentProcessor.create(wildcard, currentTypeArgument, typeMapping);
258+
return TypeArgumentProcessor.create(wildcard, currentTypeArgument, typeMapping, typeVariableFinisher);
198259
}
199260

200261
@Override
201262
public SignatureVisitor visitArrayType() {
202-
return new TypeArgumentProcessor(typeArgumentType, parameterizedType, compose(typeMapping, TO_ARRAY_TYPE));
263+
return new TypeArgumentProcessor(typeArgumentType, parameterizedType, compose(typeMapping, TO_ARRAY_TYPE), typeVariableFinisher.after(GENERIC_ARRAY_CREATOR));
203264
}
204265

205-
static TypeArgumentProcessor create(char identifier, JavaParameterizedTypeBuilder parameterizedType, Function<JavaClassDescriptor, JavaClassDescriptor> typeMapping) {
266+
static TypeArgumentProcessor create(
267+
char identifier,
268+
JavaParameterizedTypeBuilder parameterizedType,
269+
Function<JavaClassDescriptor, JavaClassDescriptor> typeMapping,
270+
ReferenceCreationProcess.JavaTypeVariableFinisher typeVariableFinisher) {
271+
206272
switch (identifier) {
207273
case '=':
208-
return new TypeArgumentProcessor(PARAMETERIZED_TYPE, parameterizedType, typeMapping);
274+
return new TypeArgumentProcessor(PARAMETERIZED_TYPE, parameterizedType, typeMapping, typeVariableFinisher);
209275
case '+':
210-
return new TypeArgumentProcessor(WILDCARD_WITH_UPPER_BOUND, parameterizedType, typeMapping);
276+
return new TypeArgumentProcessor(WILDCARD_WITH_UPPER_BOUND, parameterizedType, typeMapping, typeVariableFinisher);
211277
case '-':
212-
return new TypeArgumentProcessor(WILDCARD_WITH_LOWER_BOUND, parameterizedType, typeMapping);
278+
return new TypeArgumentProcessor(WILDCARD_WITH_LOWER_BOUND, parameterizedType, typeMapping, typeVariableFinisher);
213279
default:
214280
throw new IllegalStateException(String.format("Cannot handle asm type argument identifier '%s'", identifier));
215281
}

archunit/src/test/java/com/tngtech/archunit/core/domain/JavaTypeTest.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,15 @@ public void erased_type_parameter_of_concrete_array_type_is_array_type() {
5656
assertThatType(getTypeArgumentOfFirstBound(typeParameters.get(2)).toErasure()).matches(List[][][].class);
5757
}
5858

59+
@Test
60+
public void erased_type_parameter_of_generic_array_type_is_array_type() {
61+
List<JavaTypeVariable> typeParameters = new ClassFileImporter().importClass(ClassWithBoundTypeParameterWithGenericArrayBounds.class).getTypeParameters();
62+
63+
assertThatType(getTypeArgumentOfFirstBound(typeParameters.get(3)).toErasure()).matches(Object[].class);
64+
assertThatType(getTypeArgumentOfFirstBound(typeParameters.get(4)).toErasure()).matches(String[][].class);
65+
assertThatType(getTypeArgumentOfFirstBound(typeParameters.get(5)).toErasure()).matches(List[][][].class);
66+
}
67+
5968
private static JavaType getTypeArgumentOfFirstBound(JavaTypeVariable typeParameter) {
6069
JavaParameterizedType firstBound = (JavaParameterizedType) typeParameter.getBounds().get(0);
6170
return firstBound.getActualTypeArguments().get(0);
@@ -80,6 +89,10 @@ private static class ClassWithBoundTypeParameterWithSingleGenericClassBound<T ex
8089
private static class ClassWithBoundTypeParameterWithSingleGenericArrayBound<T extends List<Object[]>, U extends List<String[][]>, V extends List<List<?>[][][]>> {
8190
}
8291

92+
@SuppressWarnings("unused")
93+
private static class ClassWithBoundTypeParameterWithGenericArrayBounds<A, B extends String, C extends List<?>, T extends List<A[]>, U extends List<B[][]>, V extends List<C[][][]>> {
94+
}
95+
8396
@SuppressWarnings("unused")
8497
private static class ClassWithBoundTypeParameterWithMultipleGenericClassAndInterfaceBounds<T extends HashMap<String, String> & Iterable<String> & Serializable> {
8598
}

0 commit comments

Comments
 (0)