@@ -13,6 +13,9 @@ export function isSuperType(a: reflect.TypeReference, b: reflect.TypeReference,
13
13
if ( a . void || b . void ) { throw new Error ( 'isSuperType() does not handle voids' ) ; }
14
14
if ( a . isAny ) { return { success : true } ; }
15
15
16
+ // Assume true if we're currently already checking this.
17
+ if ( CURRENTLY_CHECKING . has ( a , b ) ) { return { success : true } ; }
18
+
16
19
if ( a . primitive !== undefined ) {
17
20
if ( a . primitive === b . primitive ) { return { success : true } ; }
18
21
return failure ( `${ b } is not assignable to ${ a } ` ) ;
@@ -52,32 +55,39 @@ export function isSuperType(a: reflect.TypeReference, b: reflect.TypeReference,
52
55
) ;
53
56
}
54
57
55
- // For named types, we'll always do a nominal typing relationship.
56
- // That is, if in the updated typesystem someone were to use the type name
57
- // from the old assembly, do they have a typing relationship that's accepted
58
- // by a nominal type system. (That check also rules out enums)
59
- const nominalCheck = isNominalSuperType ( a , b , updatedSystem ) ;
60
- if ( nominalCheck . success === false ) { return nominalCheck ; }
61
-
62
- // At this point, the types are legal in the updated assembly's type system.
63
- // However, for structs we also structurally check the fields between the OLD
64
- // and the NEW type system.
65
- // We could do more complex analysis on typing of methods, but it doesn't seem
66
- // worth it.
58
+ // We have two named types, recursion might happen so protect against it.
59
+ CURRENTLY_CHECKING . add ( a , b ) ;
67
60
try {
68
- const A = a . type ! ; // Note: lookup in old type system!
69
- const B = b . type ! ;
70
- if ( A . isInterfaceType ( ) && A . isDataType ( ) && B . isInterfaceType ( ) && B . datatype ) {
71
- return isStructuralSuperType ( A , B , updatedSystem ) ;
61
+
62
+ // For named types, we'll always do a nominal typing relationship.
63
+ // That is, if in the updated typesystem someone were to use the type name
64
+ // from the old assembly, do they have a typing relationship that's accepted
65
+ // by a nominal type system. (That check also rules out enums)
66
+ const nominalCheck = isNominalSuperType ( a , b , updatedSystem ) ;
67
+ if ( nominalCheck . success === false ) { return nominalCheck ; }
68
+
69
+ // At this point, the types are legal in the updated assembly's type system.
70
+ // However, for structs we also structurally check the fields between the OLD
71
+ // and the NEW type system.
72
+ // We could do more complex analysis on typing of methods, but it doesn't seem
73
+ // worth it.
74
+ try {
75
+ const A = a . type ! ; // Note: lookup in old type system!
76
+ const B = b . type ! ;
77
+ if ( A . isInterfaceType ( ) && A . isDataType ( ) && B . isInterfaceType ( ) && B . datatype ) {
78
+ return isStructuralSuperType ( A , B , updatedSystem ) ;
79
+ }
80
+ } catch ( e ) {
81
+ // We might get an exception if the type is supposed to come from a different
82
+ // assembly and the lookup fails.
83
+ return failure ( e . message ) ;
72
84
}
73
- } catch ( e ) {
74
- // We might get an exception if the type is supposed to come from a different
75
- // assembly and the lookup fails.
76
- return { success : false , reasons : [ e . message ] } ;
77
- }
78
85
79
- // All seems good
80
- return { success : true } ;
86
+ // All seems good
87
+ return { success : true } ;
88
+ } finally {
89
+ CURRENTLY_CHECKING . remove ( a , b ) ;
90
+ }
81
91
}
82
92
83
93
/**
@@ -147,7 +157,7 @@ function isStructuralSuperType(a: reflect.InterfaceType, b: reflect.InterfaceTyp
147
157
}
148
158
149
159
const ana = isSuperType ( aProp . type , bProp . type , updatedSystem ) ;
150
- if ( ! ana . success ) { return ana ; }
160
+ if ( ! ana . success ) { return failure ( `property ${ name } ` , ... ana . reasons ) ; }
151
161
}
152
162
153
163
return { success : true } ;
@@ -166,3 +176,49 @@ export function prependReason(analysis: Analysis, message: string): Analysis {
166
176
if ( analysis . success ) { return analysis ; }
167
177
return failure ( message , ...analysis . reasons ) ;
168
178
}
179
+
180
+ /**
181
+ * Keep a set of type pairs.
182
+ *
183
+ * Needs more code than it should because JavaScript has an anemic runtime.
184
+ *
185
+ * (The ideal type would have been Set<[string, string]> but different
186
+ * instances of those pairs wouldn't count as "the same")
187
+ */
188
+ class TypePairs {
189
+ private readonly pairs = new Map < string , Set < string > > ( ) ;
190
+
191
+ public add ( a : reflect . TypeReference , b : reflect . TypeReference ) : void {
192
+ if ( ! a . fqn || ! b . fqn ) { return ; } // Only for user-defined types
193
+
194
+ let image = this . pairs . get ( a . fqn ) ;
195
+ if ( ! image ) {
196
+ this . pairs . set ( a . fqn , image = new Set < string > ( ) ) ;
197
+ }
198
+ image . add ( b . fqn ) ;
199
+ }
200
+
201
+ public remove ( a : reflect . TypeReference , b : reflect . TypeReference ) : void {
202
+ if ( ! a . fqn || ! b . fqn ) { return ; } // Only for user-defined types
203
+
204
+ const image = this . pairs . get ( a . fqn ) ;
205
+ if ( image ) {
206
+ image . delete ( b . fqn ) ;
207
+ }
208
+ }
209
+
210
+ public has ( a : reflect . TypeReference , b : reflect . TypeReference ) : boolean {
211
+ if ( ! a . fqn || ! b . fqn ) { return false ; } // Only for user-defined types
212
+
213
+ const image = this . pairs . get ( a . fqn ) ;
214
+ return ! ! image && image . has ( b . fqn ) ;
215
+ }
216
+ }
217
+
218
+ /**
219
+ * To avoid recursion, we keep a set of relationships we're *currently*
220
+ * checking, so that if we're recursing we can just assume the subtyping
221
+ * relationship holds (and let the other struct members falsify that
222
+ * statement).
223
+ */
224
+ const CURRENTLY_CHECKING = new TypePairs ( ) ;
0 commit comments