Skip to content

Commit 2067dda

Browse files
Resolving subclasses of generic "known" classes (like Map) #556
1 parent 52b6bc1 commit 2067dda

File tree

7 files changed

+189
-68
lines changed

7 files changed

+189
-68
lines changed

typescript-generator-core/src/main/java/cz/habarta/typescript/generator/CustomMappingTypeProcessor.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
package cz.habarta.typescript.generator;
33

44
import cz.habarta.typescript.generator.util.GenericsResolver;
5-
import cz.habarta.typescript.generator.util.Pair;
65
import cz.habarta.typescript.generator.util.Utils;
76
import java.lang.reflect.Type;
87
import java.util.ArrayList;
@@ -20,11 +19,10 @@ public CustomMappingTypeProcessor(List<Settings.CustomTypeMapping> customMapping
2019

2120
@Override
2221
public Result processType(Type javaType, Context context) {
23-
final Pair<Class<?>, List<Type>> rawClassAndTypeArguments = Utils.getRawClassAndTypeArguments(javaType);
24-
if (rawClassAndTypeArguments == null) {
22+
final Class<?> rawClass = Utils.getRawClassOrNull(javaType);
23+
if (rawClass == null) {
2524
return null;
2625
}
27-
final Class<?> rawClass = rawClassAndTypeArguments.getValue1();
2826
final Settings.CustomTypeMapping mapping = customMappings.stream()
2927
.filter(m -> m.matchSubclasses
3028
? m.rawClass.isAssignableFrom(rawClass)

typescript-generator-core/src/main/java/cz/habarta/typescript/generator/DefaultTypeProcessor.java

Lines changed: 56 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import cz.habarta.typescript.generator.compiler.Symbol;
55
import cz.habarta.typescript.generator.type.JTypeWithNullability;
66
import cz.habarta.typescript.generator.type.JUnionType;
7+
import cz.habarta.typescript.generator.util.GenericsResolver;
78
import cz.habarta.typescript.generator.util.Utils;
89
import java.lang.reflect.GenericArrayType;
910
import java.lang.reflect.Method;
@@ -42,7 +43,11 @@ public DefaultTypeProcessor(LoadedDataLibraries dataLibraries) {
4243
}
4344

4445
private static boolean isAssignableFrom(List<Class<?>> classes, Class<?> cls) {
45-
return classes.stream().anyMatch(c -> c.isAssignableFrom(cls));
46+
return assignableFrom(classes, cls).isPresent();
47+
}
48+
49+
private static Optional<Class<?>> assignableFrom(List<Class<?>> classes, Class<?> cls) {
50+
return classes.stream().filter(c -> c.isAssignableFrom(cls)).findFirst();
4651
}
4752

4853
@Override
@@ -87,21 +92,16 @@ public Result processType(Type javaType, Context context) {
8792
if (javaClass.isEnum()) {
8893
return new Result(new TsType.EnumReferenceType(context.getSymbol(javaClass)), javaClass);
8994
}
90-
if (isAssignableFrom(known.listClasses, javaClass)) {
91-
final Result result = context.processTypeInsideCollection(Object.class);
92-
return new Result(new TsType.BasicArrayType(result.getTsType()), result.getDiscoveredClasses());
93-
}
94-
if (isAssignableFrom(known.mapClasses, javaClass)) {
95-
return processMapType(String.class, Object.class, context);
95+
// list, map, optional, wrapper
96+
final Result knownGenericTypeResult = processKnownGenericType(javaClass, javaClass, context);
97+
if (knownGenericTypeResult != null) {
98+
return knownGenericTypeResult;
9699
}
97100
if (OptionalInt.class.isAssignableFrom(javaClass) ||
98101
OptionalLong.class.isAssignableFrom(javaClass) ||
99102
OptionalDouble.class.isAssignableFrom(javaClass)) {
100103
return new Result(TsType.Number.optional());
101104
}
102-
if (isAssignableFrom(known.wrapperClasses, javaClass)) {
103-
return new Result(TsType.Any);
104-
}
105105
// generic structural type used without type arguments
106106
if (javaClass.getTypeParameters().length > 0) {
107107
final List<TsType> tsTypeArguments = new ArrayList<>();
@@ -117,20 +117,10 @@ public Result processType(Type javaType, Context context) {
117117
final ParameterizedType parameterizedType = (ParameterizedType) javaType;
118118
if (parameterizedType.getRawType() instanceof Class) {
119119
final Class<?> javaClass = (Class<?>) parameterizedType.getRawType();
120-
if (isAssignableFrom(known.listClasses, javaClass)) {
121-
final Result result = context.processTypeInsideCollection(parameterizedType.getActualTypeArguments()[0]);
122-
return new Result(new TsType.BasicArrayType(result.getTsType()), result.getDiscoveredClasses());
123-
}
124-
if (isAssignableFrom(known.mapClasses, javaClass)) {
125-
return processMapType(parameterizedType.getActualTypeArguments()[0], parameterizedType.getActualTypeArguments()[1], context);
126-
}
127-
if (isAssignableFrom(known.optionalClasses, javaClass)) {
128-
final Result result = context.processType(parameterizedType.getActualTypeArguments()[0]);
129-
return new Result(result.getTsType().optional(), result.getDiscoveredClasses());
130-
}
131-
if (isAssignableFrom(known.wrapperClasses, javaClass)) {
132-
final Result result = context.processType(parameterizedType.getActualTypeArguments()[0]);
133-
return new Result(result.getTsType(), result.getDiscoveredClasses());
120+
// list, map, optional, wrapper
121+
final Result knownGenericTypeResult = processKnownGenericType(javaType, javaClass, context);
122+
if (knownGenericTypeResult != null) {
123+
return knownGenericTypeResult;
134124
}
135125
// generic structural type
136126
final List<Class<?>> discoveredClasses = new ArrayList<>();
@@ -189,21 +179,49 @@ public Result processType(Type javaType, Context context) {
189179
return null;
190180
}
191181

192-
private Result processMapType(Type keyType, Type valueType, Context context) {
193-
final Result keyResult = context.processType(keyType);
194-
final Result valueResult = context.processTypeInsideCollection(valueType);
195-
final TsType valueTsType = valueResult.getTsType();
196-
if (keyResult.getTsType() instanceof TsType.EnumReferenceType) {
197-
return new Result(
198-
new TsType.MappedType(keyResult.getTsType(), TsType.MappedType.QuestionToken.Question, valueTsType),
199-
Utils.concat(keyResult.getDiscoveredClasses(), valueResult.getDiscoveredClasses())
200-
);
201-
} else {
202-
return new Result(
203-
new TsType.IndexedArrayType(TsType.String, valueTsType),
204-
valueResult.getDiscoveredClasses()
205-
);
182+
private Result processKnownGenericType(Type javaType, Class<?> rawClass, Context context) {
183+
184+
final Optional<Class<?>> listBaseClass = assignableFrom(known.listClasses, rawClass);
185+
if (listBaseClass.isPresent()) {
186+
final List<Type> resolvedGenericVariables = GenericsResolver.resolveBaseGenericVariables(listBaseClass.get(), javaType);
187+
final Result result = context.processTypeInsideCollection(resolvedGenericVariables.get(0));
188+
return new Result(new TsType.BasicArrayType(result.getTsType()), result.getDiscoveredClasses());
189+
}
190+
191+
final Optional<Class<?>> mapBaseClass = assignableFrom(known.mapClasses, rawClass);
192+
if (mapBaseClass.isPresent()) {
193+
final List<Type> resolvedGenericVariables = GenericsResolver.resolveBaseGenericVariables(mapBaseClass.get(), javaType);
194+
final Result keyResult = context.processType(resolvedGenericVariables.get(0));
195+
final Result valueResult = context.processTypeInsideCollection(resolvedGenericVariables.get(1));
196+
final TsType valueTsType = valueResult.getTsType();
197+
if (keyResult.getTsType() instanceof TsType.EnumReferenceType) {
198+
return new Result(
199+
new TsType.MappedType(keyResult.getTsType(), TsType.MappedType.QuestionToken.Question, valueTsType),
200+
Utils.concat(keyResult.getDiscoveredClasses(), valueResult.getDiscoveredClasses())
201+
);
202+
} else {
203+
return new Result(
204+
new TsType.IndexedArrayType(TsType.String, valueTsType),
205+
valueResult.getDiscoveredClasses()
206+
);
207+
}
206208
}
209+
210+
final Optional<Class<?>> optionalBaseClass = assignableFrom(known.optionalClasses, rawClass);
211+
if (optionalBaseClass.isPresent()) {
212+
final List<Type> resolvedGenericVariables = GenericsResolver.resolveBaseGenericVariables(optionalBaseClass.get(), javaType);
213+
final Result result = context.processType(resolvedGenericVariables.get(0));
214+
return new Result(result.getTsType().optional(), result.getDiscoveredClasses());
215+
}
216+
217+
final Optional<Class<?>> wrapperBaseClass = assignableFrom(known.wrapperClasses, rawClass);
218+
if (wrapperBaseClass.isPresent()) {
219+
final List<Type> resolvedGenericVariables = GenericsResolver.resolveBaseGenericVariables(wrapperBaseClass.get(), javaType);
220+
final Result result = context.processType(resolvedGenericVariables.get(0));
221+
return new Result(result.getTsType(), result.getDiscoveredClasses());
222+
}
223+
224+
return null;
207225
}
208226

209227
private static LoadedDataLibraries getKnownClasses() {

typescript-generator-core/src/main/java/cz/habarta/typescript/generator/LoadedDataLibraries.java

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,25 @@ public LoadedDataLibraries(
5353
this.dateClasses = dateClasses;
5454
this.anyClasses = anyClasses;
5555
this.voidClasses = voidClasses;
56-
this.listClasses = listClasses;
57-
this.mapClasses = mapClasses;
58-
this.optionalClasses = optionalClasses;
59-
this.wrapperClasses = wrapperClasses;
56+
this.listClasses = validateNumberOfGenericParameters(listClasses, 1);
57+
this.mapClasses = validateNumberOfGenericParameters(mapClasses, 2);
58+
this.optionalClasses = validateNumberOfGenericParameters(optionalClasses, 1);
59+
this.wrapperClasses = validateNumberOfGenericParameters(wrapperClasses, 1);
6060
this.typeMappings = typeMappings;
6161
this.typeAliases = typeAliases;
6262
}
6363

64+
private static List<Class<?>> validateNumberOfGenericParameters(List<Class<?>> classes, int required) {
65+
for (Class<?> cls : classes) {
66+
if (cls.getTypeParameters().length != required) {
67+
throw new RuntimeException(String.format(
68+
"Data library class '%s' is required to have %d generic type parameters but it has %d",
69+
cls.getName(), required, cls.getTypeParameters().length));
70+
}
71+
}
72+
return classes;
73+
}
74+
6475
public static LoadedDataLibraries join(LoadedDataLibraries... jsons) {
6576
return join(Arrays.asList(jsons));
6677
}

typescript-generator-core/src/main/java/cz/habarta/typescript/generator/util/GenericsResolver.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import java.util.List;
1212
import java.util.Map;
1313
import java.util.Objects;
14+
import java.util.Optional;
1415
import java.util.stream.Collectors;
1516

1617

@@ -45,8 +46,11 @@ public static List<String> mapGenericVariablesToBase(Class<?> derivedClass, Clas
4546
}
4647

4748
public static List<Type> resolveBaseGenericVariables(Class<?> baseClass, Type contextType) {
48-
final Pair<Class<?>, List<Type>> rawClassAndTypeArguments = Utils.getRawClassAndTypeArguments(contextType);
49+
final Pair<Class<?>, Optional<List<Type>>> rawClassAndTypeArguments = Utils.getRawClassAndTypeArguments(contextType);
4950
if (rawClassAndTypeArguments != null) {
51+
if (rawClassAndTypeArguments.getValue2().isEmpty()) {
52+
return Collections.nCopies(baseClass.getTypeParameters().length, Object.class);
53+
}
5054
final ResolvedClass resolvedContextType = new ResolvedClass(null, null, null).resolveAncestor(contextType);
5155
final List<ResolvedClass> path = traverseSomeInheritancePath(resolvedContextType, baseClass);
5256
final ResolvedClass resolvedClass = path != null && !path.isEmpty() ? path.get(0) : resolvedContextType;
@@ -114,10 +118,13 @@ public List<ResolvedClass> getDirectAncestors() {
114118
}
115119

116120
public ResolvedClass resolveAncestor(Type ancestor) {
117-
final Pair<Class<?>, List<Type>> rawClassAndTypeArguments = Utils.getRawClassAndTypeArguments(ancestor);
121+
final Pair<Class<?>, Optional<List<Type>>> rawClassAndTypeArguments = Utils.getRawClassAndTypeArguments(ancestor);
122+
if (rawClassAndTypeArguments == null || rawClassAndTypeArguments.getValue2().isEmpty()) {
123+
return null;
124+
}
118125
final Class<?> cls = rawClassAndTypeArguments.getValue1();
119126
final List<TypeVariable<?>> typeVariables = Arrays.asList(cls.getTypeParameters());
120-
final List<Type> arguments = rawClassAndTypeArguments.getValue2();
127+
final List<Type> arguments = rawClassAndTypeArguments.getValue2().get();
121128
final Map<String, Type> typeParameters = new LinkedHashMap<>();
122129
final int count = Math.min(typeVariables.size(), arguments.size());
123130
for (int i = 0; i < count; i++) {

typescript-generator-core/src/main/java/cz/habarta/typescript/generator/util/Utils.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import java.util.List;
3434
import java.util.Map;
3535
import java.util.Objects;
36+
import java.util.Optional;
3637
import java.util.Scanner;
3738
import java.util.Spliterator;
3839
import java.util.Spliterators;
@@ -69,20 +70,22 @@ private static String trimRightSlash(String path) {
6970
}
7071

7172
public static Class<?> getRawClassOrNull(Type type) {
72-
final Pair<Class<?>, List<Type>> rawClassAndTypeArguments = getRawClassAndTypeArguments(type);
73+
final Pair<Class<?>, Optional<List<Type>>> rawClassAndTypeArguments = getRawClassAndTypeArguments(type);
7374
return rawClassAndTypeArguments != null ? rawClassAndTypeArguments.getValue1() : null;
7475
}
7576

76-
public static Pair<Class<?>, List<Type>> getRawClassAndTypeArguments(Type type) {
77+
public static Pair<Class<?>, Optional<List<Type>>> getRawClassAndTypeArguments(Type type) {
7778
if (type instanceof Class) {
7879
final Class<?> javaClass = (Class<?>) type;
79-
return Pair.of(javaClass, Arrays.asList(javaClass.getTypeParameters()));
80+
return javaClass.getTypeParameters().length != 0
81+
? Pair.of(javaClass, Optional.empty()) // raw usage of generic class
82+
: Pair.of(javaClass, Optional.of(Collections.emptyList())); // non-generic class
8083
}
8184
if (type instanceof ParameterizedType) {
8285
final ParameterizedType parameterizedType = (ParameterizedType) type;
8386
if (parameterizedType.getRawType() instanceof Class) {
8487
final Class<?> javaClass = (Class<?>) parameterizedType.getRawType();
85-
return Pair.of(javaClass, Arrays.asList(parameterizedType.getActualTypeArguments()));
88+
return Pair.of(javaClass, Optional.of(Arrays.asList(parameterizedType.getActualTypeArguments())));
8689
}
8790
}
8891
return null;

typescript-generator-core/src/test/java/cz/habarta/typescript/generator/GenericsResolverTest.java

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,21 +108,21 @@ static class R123<A, B, C> extends R12<C, List<B>> {
108108
public void testResolvingGenericVariablesInContextType1() throws NoSuchFieldException {
109109
final Type contextType = MyClass.class.getField("property1").getGenericType();
110110
final List<Type> resolvedTypeParameters = GenericsResolver.resolveBaseGenericVariables(BaseClass.class, contextType);
111-
Assert.assertEquals(Arrays.asList("java.lang.String", "java.lang.Integer"), resolvedTypeParameters.stream().map(Type::getTypeName).collect(Collectors.toList()));
111+
Assert.assertEquals(Arrays.asList("java.lang.String", "java.lang.Integer"), getTypeNames(resolvedTypeParameters));
112112
}
113113

114114
@Test
115115
public void testResolvingGenericVariablesInContextType3() throws NoSuchFieldException {
116116
final Type contextType = MyClass.class.getField("property3").getGenericType();
117117
final List<Type> resolvedTypeParameters = GenericsResolver.resolveBaseGenericVariables(BaseClass.class, contextType);
118-
Assert.assertEquals(Arrays.asList("java.lang.Integer", "java.lang.Boolean"), resolvedTypeParameters.stream().map(Type::getTypeName).collect(Collectors.toList()));
118+
Assert.assertEquals(Arrays.asList("java.lang.Integer", "java.lang.Boolean"), getTypeNames(resolvedTypeParameters));
119119
}
120120

121121
@Test
122122
public void testResolvingGenericVariablesInContextTypeBase() throws NoSuchFieldException {
123123
final Type contextType = MyClass.class.getField("propertyBase").getGenericType();
124124
final List<Type> resolvedTypeParameters = GenericsResolver.resolveBaseGenericVariables(BaseClass.class, contextType);
125-
Assert.assertEquals(Arrays.asList("java.lang.Integer", "java.lang.String"), resolvedTypeParameters.stream().map(Type::getTypeName).collect(Collectors.toList()));
125+
Assert.assertEquals(Arrays.asList("java.lang.Integer", "java.lang.String"), getTypeNames(resolvedTypeParameters));
126126
}
127127

128128
static class BaseClass<A, B> {}
@@ -137,4 +137,45 @@ static class MyClass {
137137
public BaseClass<Integer, String> propertyBase;
138138
}
139139

140+
@Test
141+
public void testResolvingRawUsage1() throws NoSuchFieldException {
142+
final Type contextType = RawUsage.class.getField("rawMap").getGenericType();
143+
final List<Type> resolvedTypeParameters = GenericsResolver.resolveBaseGenericVariables(Map.class, contextType);
144+
Assert.assertEquals(Arrays.asList("java.lang.Object", "java.lang.Object"), getTypeNames(resolvedTypeParameters));
145+
}
146+
147+
@Test
148+
public void testResolvingRawUsage2() throws NoSuchFieldException {
149+
final Type contextType = RawUsage.class.getField("rawStringKeyMap").getGenericType();
150+
final List<Type> resolvedTypeParameters = GenericsResolver.resolveBaseGenericVariables(Map.class, contextType);
151+
Assert.assertEquals(Arrays.asList("java.lang.Object", "java.lang.Object"), getTypeNames(resolvedTypeParameters));
152+
}
153+
154+
static class RawUsage {
155+
public Map rawMap;
156+
public StringKeyMap rawStringKeyMap;
157+
}
158+
159+
static interface StringKeyMap<T> extends Map<String, T> {}
160+
161+
162+
@Test
163+
public void testResolvingFixedDescendant() throws NoSuchFieldException {
164+
final Type contextType = StringMapDescendantUsage.class.getField("stringMapDescendant").getGenericType();
165+
final List<Type> resolvedTypeParameters = GenericsResolver.resolveBaseGenericVariables(Map.class, contextType);
166+
Assert.assertEquals(Arrays.asList("java.lang.String", "java.lang.String"), getTypeNames(resolvedTypeParameters));
167+
}
168+
169+
static class StringMapDescendantUsage {
170+
public StringMapDescendant stringMapDescendant;
171+
}
172+
173+
static interface StringMapDescendant extends StringMap {}
174+
175+
static interface StringMap extends Map<String, String> {}
176+
177+
private static List<String> getTypeNames(List<Type> types) {
178+
return types.stream().map(Type::getTypeName).collect(Collectors.toList());
179+
}
180+
140181
}

0 commit comments

Comments
 (0)