Skip to content

Commit a7488d5

Browse files
authored
implement TypeInfoFactory.getConsolidationMapping(...)
This PR implements the TypeInfoFactory.getConsolidationMapping(...),, which can create a mapping that maps type variables declared by superclasses/interfaces to concrete type, or type variables declared by input class. It is one step in the path of improving support for reflection-based access to code that uses generics.
1 parent 0bf64cc commit a7488d5

File tree

8 files changed

+290
-1
lines changed

8 files changed

+290
-1
lines changed

rhino/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ decycle {
8383
ignoring from: "org.mozilla.javascript.Native*", to: "org.mozilla.javascript.lc.type.TypeInfo*"
8484
// CHECKME: Can we move JavaMembers + MemberBox also to o.m.j.lc
8585
ignoring from: "org.mozilla.javascript.JavaMembers", to: "org.mozilla.javascript.lc.type.TypeInfo*"
86-
ignoring from: "org.mozilla.javascript.MemberBox", to: "org.mozilla.javascript.lc.type.TypeInfo*"
86+
ignoring from: "org.mozilla.javascript.MemberBox", to: "org.mozilla.javascript.lc.type.*"
8787
ignoring from: "org.mozilla.javascript.AccessorSlot\$MemberBoxSetter", to: "org.mozilla.javascript.lc.type.TypeInfo"
8888
// CHECKME: Can we remove dependency to typeinfo from these objects?
8989
ignoring from: "org.mozilla.javascript.ScriptableObject", to: "org.mozilla.javascript.lc.type.TypeInfoFactory"

rhino/src/main/java/org/mozilla/javascript/lc/type/TypeInfoFactory.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,23 @@ default TypeInfo attachParam(TypeInfo base, TypeInfo... params) {
110110
}
111111

112112
/**
113+
* Get consolidation mapping from the input class.
114+
*
115+
* <p>Example (for factory implemented by Rhino):
116+
*
117+
* <pre>
118+
* class {@code A<Ta>} {}
119+
* class {@code B<Tb>} extends {@code A<Tb>} {}
120+
*
121+
* interface {@code C<Tc>} {}
122+
* interface {@code D<Td>} extends {@code C<Td>} {}
123+
*
124+
* class {@code E<Te>} extends {@code B<Te>} implements {@code D<String>} {}
125+
* </pre>
126+
*
127+
* and input class is {@code E.class}. The result mapping will then be: {@code Ta -> Te}, {@code
128+
* Tb -> Te}, {@code Tc -> String}, {@code Td -> String}
129+
*
113130
* @see TypeInfo#consolidate(Map)
114131
*/
115132
default Map<VariableTypeInfo, TypeInfo> getConsolidationMapping(Class<?> from) {

rhino/src/main/java/org/mozilla/javascript/lc/type/impl/factory/ConcurrentFactory.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,9 @@ public class ConcurrentFactory extends WithCacheFactory {
3333
protected final <K, V> Map<K, V> createTypeCache() {
3434
return new ConcurrentHashMap<>();
3535
}
36+
37+
@Override
38+
protected <K, V> Map<K, V> createConsolidationMappingCache() {
39+
return new ConcurrentHashMap<>();
40+
}
3641
}

rhino/src/main/java/org/mozilla/javascript/lc/type/impl/factory/FactoryBase.java

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,16 @@
22

33
import java.lang.reflect.GenericArrayType;
44
import java.lang.reflect.ParameterizedType;
5+
import java.lang.reflect.Type;
56
import java.lang.reflect.WildcardType;
7+
import java.util.ArrayList;
8+
import java.util.HashMap;
69
import java.util.List;
10+
import java.util.Map;
711
import org.mozilla.javascript.lc.type.ParameterizedTypeInfo;
812
import org.mozilla.javascript.lc.type.TypeInfo;
913
import org.mozilla.javascript.lc.type.TypeInfoFactory;
14+
import org.mozilla.javascript.lc.type.VariableTypeInfo;
1015
import org.mozilla.javascript.lc.type.impl.ArrayTypeInfo;
1116
import org.mozilla.javascript.lc.type.impl.ParameterizedTypeInfoImpl;
1217

@@ -53,4 +58,87 @@ default TypeInfo attachParam(TypeInfo base, List<TypeInfo> params) {
5358
}
5459
return new ParameterizedTypeInfoImpl(base, params);
5560
}
61+
62+
private static Map<VariableTypeInfo, TypeInfo> transformMapping(
63+
Map<VariableTypeInfo, TypeInfo> mapping, Map<VariableTypeInfo, TypeInfo> transformer) {
64+
if (mapping.isEmpty()) {
65+
return Map.of();
66+
} else if (mapping.size() == 1) {
67+
var entry = mapping.entrySet().iterator().next();
68+
return Map.of(entry.getKey(), entry.getValue().consolidate(transformer));
69+
}
70+
var transformed = new HashMap<>(mapping);
71+
for (var entry : transformed.entrySet()) {
72+
entry.setValue(entry.getValue().consolidate(transformer));
73+
}
74+
return transformed;
75+
}
76+
77+
/** Used by {@link #getConsolidationMapping(java.lang.Class)} */
78+
default Map<VariableTypeInfo, TypeInfo> computeConsolidationMapping(Class<?> type) {
79+
var mapping = new HashMap<VariableTypeInfo, TypeInfo>();
80+
81+
// in our E.class example, this will collect mapping from B<Te>, forming Tb -> Te
82+
extractSuperMapping(type.getGenericSuperclass(), mapping);
83+
84+
// in our E.class example, this will collect mapping from D<String>, forming Td -> String
85+
for (var genericInterface : type.getGenericInterfaces()) {
86+
extractSuperMapping(genericInterface, mapping);
87+
}
88+
89+
// extract mappings for superclasses/interfaces
90+
// in our E.class example, super mapping will include Ta -> Tb
91+
var superMapping = getConsolidationMapping(type.getSuperclass());
92+
93+
// in our E.class example, interface mapping will include Tc -> Td
94+
var interfaces = type.getInterfaces();
95+
var interfaceMappings = new ArrayList<Map<VariableTypeInfo, TypeInfo>>(interfaces.length);
96+
for (var interface_ : interfaces) {
97+
interfaceMappings.add(getConsolidationMapping(interface_));
98+
}
99+
100+
if (superMapping.isEmpty() && interfaceMappings.stream().allMatch(Map::isEmpty)) {
101+
return Map.copyOf(mapping);
102+
}
103+
104+
// transform super mapping to make it able to directly map a type to types used by E.class,
105+
// then merge them together
106+
// Example: Ta -> Tb (from `superMapping`) will be transformed by Tb -> Te (from `mapping`),
107+
// forming Ta -> Te
108+
var merged = new HashMap<>(transformMapping(superMapping, mapping));
109+
for (var interfaceMapping : interfaceMappings) {
110+
merged.putAll(transformMapping(interfaceMapping, mapping));
111+
}
112+
merged.putAll(mapping);
113+
114+
// Result: `Ta -> Te`, `Tb -> Te`, `Tc -> String`, `Td -> String`
115+
// This means that all type variables from superclass / interface (Ta, Tb, Tc, Td) can be
116+
// eliminated by applying the mapping ONCE, which will be important for performance
117+
return Map.copyOf(merged);
118+
}
119+
120+
private void extractSuperMapping(Type superType, Map<VariableTypeInfo, TypeInfo> pushTo) {
121+
if (!(superType instanceof ParameterizedType)) {
122+
return;
123+
}
124+
var parameterized = (ParameterizedType) superType;
125+
if (!(parameterized.getRawType() instanceof Class<?>)) {
126+
return;
127+
}
128+
var parent = (Class<?>) parameterized.getRawType();
129+
130+
final var params = parent.getTypeParameters(); // T
131+
final var args = parameterized.getActualTypeArguments(); // T is mapped to
132+
133+
if (params.length != args.length) {
134+
throw new IllegalArgumentException(
135+
String.format(
136+
"typeParameters.length != actualTypeArguments.length (%s != %s)",
137+
params.length, args.length));
138+
}
139+
140+
for (int i = 0; i < args.length; i++) {
141+
pushTo.put((VariableTypeInfo) create(params[i]), create(args[i]));
142+
}
143+
}
56144
}

rhino/src/main/java/org/mozilla/javascript/lc/type/impl/factory/NoCacheFactory.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package org.mozilla.javascript.lc.type.impl.factory;
22

33
import java.lang.reflect.TypeVariable;
4+
import java.util.Map;
45
import org.mozilla.javascript.lc.type.TypeInfo;
56
import org.mozilla.javascript.lc.type.TypeInfoFactory;
7+
import org.mozilla.javascript.lc.type.VariableTypeInfo;
68
import org.mozilla.javascript.lc.type.impl.BasicClassTypeInfo;
79
import org.mozilla.javascript.lc.type.impl.EnumTypeInfo;
810
import org.mozilla.javascript.lc.type.impl.InterfaceTypeInfo;
@@ -44,4 +46,12 @@ public TypeInfo create(Class<?> clazz) {
4446
public TypeInfo create(TypeVariable<?> typeVariable) {
4547
return new VariableTypeInfoImpl(typeVariable, this);
4648
}
49+
50+
@Override
51+
public Map<VariableTypeInfo, TypeInfo> getConsolidationMapping(Class<?> from) {
52+
if (from == null || from == Object.class || from.isPrimitive()) {
53+
return Map.of();
54+
}
55+
return computeConsolidationMapping(from);
56+
}
4757
}

rhino/src/main/java/org/mozilla/javascript/lc/type/impl/factory/WeakReferenceFactory.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,9 @@ public class WeakReferenceFactory extends WithCacheFactory {
3333
protected final <K, V> Map<K, V> createTypeCache() {
3434
return Collections.synchronizedMap(new WeakHashMap<>());
3535
}
36+
37+
@Override
38+
protected <K, V> Map<K, V> createConsolidationMappingCache() {
39+
return Collections.synchronizedMap(new WeakHashMap<>());
40+
}
3641
}

rhino/src/main/java/org/mozilla/javascript/lc/type/impl/factory/WithCacheFactory.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import java.util.Map;
88
import org.mozilla.javascript.lc.type.TypeInfo;
99
import org.mozilla.javascript.lc.type.TypeInfoFactory;
10+
import org.mozilla.javascript.lc.type.VariableTypeInfo;
1011
import org.mozilla.javascript.lc.type.impl.BasicClassTypeInfo;
1112
import org.mozilla.javascript.lc.type.impl.EnumTypeInfo;
1213
import org.mozilla.javascript.lc.type.impl.InterfaceTypeInfo;
@@ -29,8 +30,13 @@ public abstract class WithCacheFactory implements FactoryBase {
2930
private transient Map<Class<?>, InterfaceTypeInfo> interfaceCache = createTypeCache();
3031
private transient Map<Class<?>, EnumTypeInfo> enumCache = createTypeCache();
3132

33+
private transient Map<Class<?>, Map<VariableTypeInfo, TypeInfo>> consolidationMappingCache =
34+
createConsolidationMappingCache();
35+
3236
protected abstract <K, V> Map<K, V> createTypeCache();
3337

38+
protected abstract <K, V> Map<K, V> createConsolidationMappingCache();
39+
3440
@Override
3541
public TypeInfo create(Class<?> clazz) {
3642
final var predefined = TypeInfoFactory.matchPredefined(clazz);
@@ -52,6 +58,21 @@ public TypeInfo create(TypeVariable<?> typeVariable) {
5258
typeVariable, raw -> new VariableTypeInfoImpl(raw, this));
5359
}
5460

61+
@Override
62+
public Map<VariableTypeInfo, TypeInfo> getConsolidationMapping(Class<?> from) {
63+
if (from == null || from == Object.class || from.isPrimitive()) {
64+
return Map.of();
65+
}
66+
// no computeIfAbsent because `computeTypeReplacement(...)` will recursively call this
67+
// method again
68+
var got = consolidationMappingCache.get(from);
69+
if (got == null) {
70+
got = computeConsolidationMapping(from);
71+
consolidationMappingCache.put(from, got);
72+
}
73+
return got;
74+
}
75+
5576
private void writeObject(ObjectOutputStream out) throws IOException {
5677
out.defaultWriteObject();
5778
}
@@ -62,5 +83,6 @@ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundE
6283
basicClassCache = createTypeCache();
6384
interfaceCache = createTypeCache();
6485
enumCache = createTypeCache();
86+
consolidationMappingCache = createConsolidationMappingCache();
6587
}
6688
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package org.mozilla.javascript.tests.type_info;
2+
3+
import java.util.ArrayList;
4+
import java.util.Arrays;
5+
import java.util.Collection;
6+
import java.util.Iterator;
7+
import java.util.Map;
8+
import java.util.Set;
9+
import java.util.stream.BaseStream;
10+
import java.util.stream.Stream;
11+
import org.junit.jupiter.api.Assertions;
12+
import org.junit.jupiter.api.Test;
13+
import org.junit.jupiter.params.ParameterizedTest;
14+
import org.junit.jupiter.params.provider.ValueSource;
15+
import org.mozilla.javascript.lc.type.TypeFormatContext;
16+
import org.mozilla.javascript.lc.type.TypeInfo;
17+
import org.mozilla.javascript.lc.type.TypeInfoFactory;
18+
import org.mozilla.javascript.lc.type.impl.NoTypeInfo;
19+
20+
/**
21+
* @author ZZZank
22+
*/
23+
public class TypeConsolidationMappingTest {
24+
25+
@Test
26+
public void testGeneric() {
27+
assertMappingMatch(E.class, "Ta -> Te", "Tb -> Te", "Tc -> String", "Td -> String");
28+
assertMappingMatch(Collection.class, "T -> E");
29+
30+
// T from BaseStream -> T from Stream
31+
assertMappingMatch(Stream.class, "S -> Stream<T>", "T -> T");
32+
33+
// TwoGenericInterfaces<A, B, C> implements Iterator<E -> B>, Map<K -> C, V -> A>
34+
assertMappingMatch(TwoGenericInterfaces.class, "V -> A", "E -> B", "K -> C");
35+
36+
// E from ArrayList & List & Collection & ..., T from Iterable
37+
// due to different class inheritance in Java11/17/21, we can't use 'equals' to match
38+
// mapping
39+
assertMappingInclude(TestListA.class, "E -> N", "T -> N");
40+
}
41+
42+
@Test
43+
public void testGenericParent() {
44+
assertMappingMatch(
45+
GenericSuperClass.class,
46+
"Ta -> Integer",
47+
"Tb -> Integer",
48+
"Tc -> String",
49+
"Td -> String",
50+
"Te -> Integer");
51+
assertMappingMatch(GenericSuperInterface.class, "Tc -> Number", "Td -> Number");
52+
53+
// E from Enum, T from Comparable
54+
assertMappingMatch(NoTypeInfo.class, "T -> NoTypeInfo", "E -> NoTypeInfo");
55+
56+
// TwoInterfaces implements Iterator<E -> Integer>, Map<K -> String, V -> Double>
57+
assertMappingMatch(TwoInterfaces.class, "V -> Double", "K -> String", "E -> Integer");
58+
59+
// E from ArrayList and List and Collection and ..., T from Iterable
60+
// M and N from TestListA
61+
// due to different class inheritance in Java11/17/21, we can't use 'equals' to match
62+
// mapping
63+
assertMappingInclude(
64+
TestListB.class, "E -> String", "M -> Integer", "N -> String", "T -> String");
65+
}
66+
67+
@ParameterizedTest
68+
@ValueSource(classes = {int.class, boolean.class, float.class, void.class})
69+
public void testPrimitive(Class<?> type) {
70+
assertMappingMatch(type /* empty mapping */);
71+
}
72+
73+
@ParameterizedTest
74+
@ValueSource(classes = {Number.class, Object.class, TypeInfo.class})
75+
public void testNonGeneric(Class<?> type) {
76+
assertMappingMatch(type /* empty mapping */);
77+
}
78+
79+
@ParameterizedTest
80+
@ValueSource(classes = {Iterable.class, Iterator.class, BaseStream.class, Map.class})
81+
public void testGenericWithNoGenericParent(Class<?> type) {
82+
assertMappingMatch(type /* empty mapping */);
83+
}
84+
85+
private static void assertMappingMatch(Class<?> clazz, String... expected) {
86+
var formatted = getAndFormatMapping(clazz);
87+
88+
formatted.sort(null);
89+
Arrays.sort(expected);
90+
91+
Assertions.assertEquals(Arrays.asList(expected), formatted);
92+
}
93+
94+
private static void assertMappingInclude(Class<?> clazz, String... expected) {
95+
var formatted = getAndFormatMapping(clazz);
96+
Assertions.assertTrue(
97+
Set.copyOf(formatted).containsAll(Arrays.asList(expected)),
98+
() ->
99+
String.format(
100+
"Found mapping '%s' does not include all elements in '%s'",
101+
Set.copyOf(formatted), Arrays.asList(expected)));
102+
}
103+
104+
private static ArrayList<String> getAndFormatMapping(Class<?> clazz) {
105+
var mapping = TypeInfoFactory.GLOBAL.getConsolidationMapping(clazz);
106+
107+
var formatted = new ArrayList<String>();
108+
mapping.forEach(
109+
(k, v) -> {
110+
var builder = new StringBuilder();
111+
112+
k.append(TypeFormatContext.SIMPLE, builder);
113+
builder.append(" -> ");
114+
v.append(TypeFormatContext.SIMPLE, builder);
115+
116+
formatted.add(builder.toString());
117+
});
118+
return formatted;
119+
}
120+
121+
static class A<Ta> {}
122+
123+
static class B<Tb> extends A<Tb> {}
124+
125+
interface C<Tc> {}
126+
127+
interface D<Td> extends C<Td> {}
128+
129+
static class E<Te> extends B<Te> implements D<String> {}
130+
131+
static class GenericSuperClass extends E<Integer> {}
132+
133+
static class GenericSuperInterface implements D<Number> {}
134+
135+
abstract static class TwoInterfaces implements Iterator<Integer>, Map<String, Double> {}
136+
137+
abstract static class TwoGenericInterfaces<A, B, C> implements Iterator<B>, Map<C, A> {}
138+
139+
abstract static class TestListA<M, N> extends ArrayList<N> {}
140+
141+
abstract static class TestListB extends TestListA<Integer, String> {}
142+
}

0 commit comments

Comments
 (0)