Skip to content

Commit 3c43be1

Browse files
authored
fix(jsii-diff): handle recursive types (#558)
Structural type checking would recurse endlessly if a struct had a member with the same type as itself, or the same type as a struct that contained itself. Add a cache to prevent infinite recursion. Also in this commit: `tryFindFqn` no longer explodes if the referenced assembly couldn't be found, but simply returns `undefined` as it should.
1 parent 99adf19 commit 3c43be1

File tree

3 files changed

+160
-27
lines changed

3 files changed

+160
-27
lines changed

packages/jsii-diff/lib/type-analysis.ts

Lines changed: 80 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ export function isSuperType(a: reflect.TypeReference, b: reflect.TypeReference,
1313
if (a.void || b.void) { throw new Error('isSuperType() does not handle voids'); }
1414
if (a.isAny) { return { success: true }; }
1515

16+
// Assume true if we're currently already checking this.
17+
if (CURRENTLY_CHECKING.has(a, b)) { return { success: true }; }
18+
1619
if (a.primitive !== undefined) {
1720
if (a.primitive === b.primitive) { return { success: true }; }
1821
return failure(`${b} is not assignable to ${a}`);
@@ -52,32 +55,39 @@ export function isSuperType(a: reflect.TypeReference, b: reflect.TypeReference,
5255
);
5356
}
5457

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);
6760
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);
7284
}
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-
}
7885

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+
}
8191
}
8292

8393
/**
@@ -147,7 +157,7 @@ function isStructuralSuperType(a: reflect.InterfaceType, b: reflect.InterfaceTyp
147157
}
148158

149159
const ana = isSuperType(aProp.type, bProp.type, updatedSystem);
150-
if (!ana.success) { return ana; }
160+
if (!ana.success) { return failure(`property ${name}`, ...ana.reasons); }
151161
}
152162

153163
return { success: true };
@@ -166,3 +176,49 @@ export function prependReason(analysis: Analysis, message: string): Analysis {
166176
if (analysis.success) { return analysis; }
167177
return failure(message, ...analysis.reasons);
168178
}
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();

packages/jsii-diff/test/test.structs.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,4 +249,76 @@ export = {
249249

250250
test.done();
251251
},
252+
253+
// ----------------------------------------------------------------------
254+
255+
async 'can handle recursive type references'(test: Test) {
256+
await expectNoError(test,
257+
`
258+
export interface LinkedList {
259+
readonly name: string;
260+
readonly next?: LinkedList;
261+
}
262+
263+
export class UseIt {
264+
public main(list: LinkedList) {
265+
Array.isArray(list);
266+
}
267+
}
268+
`, `
269+
export interface LinkedList {
270+
readonly name: string;
271+
readonly next?: LinkedList;
272+
}
273+
274+
export class UseIt {
275+
public main(list: LinkedList) {
276+
Array.isArray(list);
277+
}
278+
}
279+
`);
280+
281+
test.done();
282+
},
283+
284+
// ----------------------------------------------------------------------
285+
286+
async 'can handle mutually recursive type references'(test: Test) {
287+
await expectNoError(test,
288+
`
289+
export interface A {
290+
readonly name: string;
291+
readonly next?: B;
292+
}
293+
294+
export interface B {
295+
readonly name: string;
296+
readonly next?: A;
297+
}
298+
299+
export class UseIt {
300+
public main(list: A) {
301+
Array.isArray(list);
302+
}
303+
}
304+
`, `
305+
export interface A {
306+
readonly name: string;
307+
readonly next?: B;
308+
}
309+
310+
export interface B {
311+
readonly name: string;
312+
readonly next?: A;
313+
}
314+
315+
export class UseIt {
316+
public main(list: A) {
317+
Array.isArray(list);
318+
}
319+
}
320+
`);
321+
322+
test.done();
323+
},
252324
};

packages/jsii-reflect/lib/type-system.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,14 @@ export class TypeSystem {
142142
}
143143

144144
public findAssembly(name: string) {
145-
if (!(name in this._assemblyLookup)) {
145+
const ret = this.tryFindAssembly(name);
146+
if (!ret) {
146147
throw new Error(`Assembly "${name}" not found`);
147148
}
149+
return ret;
150+
}
151+
152+
public tryFindAssembly(name: string): Assembly | undefined {
148153
return this._assemblyLookup[name];
149154
}
150155

@@ -156,8 +161,8 @@ export class TypeSystem {
156161

157162
public tryFindFqn(fqn: string): Type | undefined {
158163
const [ assembly ] = fqn.split('.');
159-
const asm = this.findAssembly(assembly);
160-
return asm.tryFindType(fqn);
164+
const asm = this.tryFindAssembly(assembly);
165+
return asm && asm.tryFindType(fqn);
161166
}
162167

163168
public findClass(fqn: string): ClassType {

0 commit comments

Comments
 (0)