Skip to content

Commit a7e5586

Browse files
committed
fix(flow): Fix cycles in flow type detection
1 parent 42a200f commit a7e5586

File tree

2 files changed

+140
-21
lines changed

2 files changed

+140
-21
lines changed

src/utils/__tests__/getFlowType-test.js

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,4 +304,95 @@ describe('getFlowType', () => {
304304
{ name: 'literal', value: "'banana'" },
305305
], raw: '$Keys<CONTENTS>'});
306306
});
307+
308+
it('handles multiple references to one type', () => {
309+
var typePath = statement(`
310+
let action: { a: Action, b: Action };
311+
type Action = {};
312+
`).get('declarations', 0).get('id').get('typeAnnotation').get('typeAnnotation');
313+
314+
expect(getFlowType(typePath)).toEqual({name: 'signature', type: 'object', signature: {
315+
properties: [
316+
{
317+
key: 'a',
318+
value: {
319+
name: 'signature',
320+
type: 'object',
321+
required: true,
322+
raw: '{}',
323+
signature: { properties: [] },
324+
},
325+
},
326+
{
327+
key: 'b',
328+
value: {
329+
name: 'signature',
330+
type: 'object',
331+
required: true,
332+
raw: '{}',
333+
signature: { properties: [] },
334+
},
335+
},
336+
],
337+
}, raw: '{ a: Action, b: Action }'});
338+
});
339+
340+
it('handles self-referencing type cycles', () => {
341+
var typePath = statement(`
342+
let action: Action;
343+
type Action = { subAction: Action };
344+
`).get('declarations', 0).get('id').get('typeAnnotation').get('typeAnnotation');
345+
346+
expect(getFlowType(typePath)).toEqual({name: 'signature', type: 'object', signature: {
347+
properties: [
348+
{ key: 'subAction', value: { name: 'Action', required: true } },
349+
],
350+
}, raw: '{ subAction: Action }'});
351+
});
352+
353+
it('handles long type cycles', () => {
354+
var typePath = statement(`
355+
let action: Action;
356+
type Action = { subAction: SubAction };
357+
type SubAction = { subAction: SubSubAction };
358+
type SubSubAction = { subAction: SubSubSubAction };
359+
type SubSubSubAction = { rootAction: Action };
360+
`).get('declarations', 0).get('id').get('typeAnnotation').get('typeAnnotation');
361+
362+
expect(getFlowType(typePath)).toEqual({name: 'signature', type: 'object', signature: {
363+
properties: [
364+
{
365+
key: 'subAction',
366+
value: {
367+
name: 'signature', type: 'object', "required": true, signature: {
368+
properties: [
369+
{
370+
key: 'subAction',
371+
value: {
372+
name: 'signature', type: 'object', "required": true, signature: {
373+
properties: [
374+
{
375+
key: 'subAction',
376+
value: {
377+
name: 'signature', type: 'object', "required": true, signature: {
378+
properties: [
379+
{
380+
key: 'rootAction',
381+
value: { name: 'Action', "required": true },
382+
},
383+
],
384+
}, raw: '{ rootAction: Action }'
385+
},
386+
},
387+
],
388+
}, raw: '{ subAction: SubSubSubAction }'
389+
},
390+
},
391+
],
392+
}, raw: '{ subAction: SubSubAction }'
393+
},
394+
},
395+
],
396+
}, raw: '{ subAction: SubAction }'});
397+
});
307398
});

src/utils/getFlowType.js

Lines changed: 49 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ const namedTypes = {
4949
};
5050

5151
function getFlowTypeWithRequirements(path: NodePath): FlowTypeDescriptor {
52-
const type = getFlowType(path);
52+
const type = getFlowTypeWithResolvedTypes(path);
5353

5454
type.required = !path.parentPath.node.optional;
5555

@@ -80,7 +80,7 @@ function handleKeysHelper(path: NodePath) {
8080
function handleArrayTypeAnnotation(path: NodePath) {
8181
return {
8282
name: 'Array',
83-
elements: [getFlowType(path.get('elementType'))],
83+
elements: [getFlowTypeWithResolvedTypes(path.get('elementType'))],
8484
raw: printValue(path),
8585
};
8686
}
@@ -102,13 +102,13 @@ function handleGenericTypeAnnotation(path: NodePath) {
102102

103103
type = {
104104
...type,
105-
elements: params.map(param => getFlowType(param)),
105+
elements: params.map(param => getFlowTypeWithResolvedTypes(param)),
106106
raw: printValue(path),
107107
};
108108
} else {
109109
let resolvedPath = resolveToValue(path.get('id'));
110110
if (resolvedPath && resolvedPath.node.right) {
111-
type = getFlowType(resolvedPath.get('right'));
111+
type = getFlowTypeWithResolvedTypes(resolvedPath.get('right'));
112112
}
113113
}
114114

@@ -124,12 +124,12 @@ function handleObjectTypeAnnotation(path: NodePath) {
124124
};
125125

126126
path.get('callProperties').each(param => {
127-
type.signature.constructor = getFlowType(param.get('value'));
127+
type.signature.constructor = getFlowTypeWithResolvedTypes(param.get('value'));
128128
});
129129

130130
path.get('indexers').each(param => {
131131
type.signature.properties.push({
132-
key: getFlowType(param.get('key')),
132+
key: getFlowTypeWithResolvedTypes(param.get('key')),
133133
value: getFlowTypeWithRequirements(param.get('value')),
134134
});
135135
});
@@ -148,15 +148,15 @@ function handleUnionTypeAnnotation(path: NodePath) {
148148
return {
149149
name: 'union',
150150
raw: printValue(path),
151-
elements: path.get('types').map(subType => getFlowType(subType)),
151+
elements: path.get('types').map(subType => getFlowTypeWithResolvedTypes(subType)),
152152
};
153153
}
154154

155155
function handleIntersectionTypeAnnotation(path: NodePath) {
156156
return {
157157
name: 'intersection',
158158
raw: printValue(path),
159-
elements: path.get('types').map(subType => getFlowType(subType)),
159+
elements: path.get('types').map(subType => getFlowTypeWithResolvedTypes(subType)),
160160
};
161161
}
162162

@@ -165,7 +165,7 @@ function handleNullableTypeAnnotation(path: NodePath) {
165165

166166
if (!typeAnnotation) return null;
167167

168-
const type = getFlowType(typeAnnotation);
168+
const type = getFlowTypeWithResolvedTypes(typeAnnotation);
169169
type.nullable = true;
170170

171171
return type;
@@ -178,7 +178,7 @@ function handleFunctionTypeAnnotation(path: NodePath) {
178178
raw: printValue(path),
179179
signature: {
180180
arguments: [],
181-
return: getFlowType(path.get('returnType')),
181+
return: getFlowTypeWithResolvedTypes(path.get('returnType')),
182182
},
183183
};
184184

@@ -188,7 +188,7 @@ function handleFunctionTypeAnnotation(path: NodePath) {
188188

189189
type.signature.arguments.push({
190190
name: param.node.name ? param.node.name.name : '',
191-
type: getFlowType(typeAnnotation),
191+
type: getFlowTypeWithResolvedTypes(typeAnnotation),
192192
});
193193
});
194194

@@ -199,14 +199,14 @@ function handleTupleTypeAnnotation(path: NodePath) {
199199
const type = { name: 'tuple', raw: printValue(path), elements: [] };
200200

201201
path.get('types').each(param => {
202-
type.elements.push(getFlowType(param));
202+
type.elements.push(getFlowTypeWithResolvedTypes(param));
203203
});
204204

205205
return type;
206206
}
207207

208208
function handleTypeofTypeAnnotation(path: NodePath) {
209-
return getFlowType(path.get('argument'));
209+
return getFlowTypeWithResolvedTypes(path.get('argument'));
210210
}
211211

212212
function handleQualifiedTypeIdentifier(path: NodePath) {
@@ -215,17 +215,28 @@ function handleQualifiedTypeIdentifier(path: NodePath) {
215215
return { name: `React${path.node.id.name}`, raw: printValue(path) };
216216
}
217217

218-
/**
219-
* Tries to identify the flow type by inspecting the path for known
220-
* flow type names. This method doesn't check whether the found type is actually
221-
* existing. It simply assumes that a match is always valid.
222-
*
223-
* If there is no match, "unknown" is returned.
224-
*/
225-
export default function getFlowType(path: NodePath): FlowTypeDescriptor {
218+
let visitedTypes = {};
219+
220+
function getFlowTypeWithResolvedTypes(path: NodePath): FlowTypeDescriptor {
226221
const node = path.node;
227222
let type: ?FlowTypeDescriptor;
228223

224+
const isTypeAlias = types.TypeAlias.check(path.parentPath.node);
225+
// When we see a typealias mark it as visited so that the next
226+
// call of this function does not run into an endless loop
227+
if (isTypeAlias) {
228+
if (visitedTypes[path.parentPath.node.id.name] === true) {
229+
// if we are currently visiting this node then just return the name
230+
// as we are starting to endless loop
231+
return { name: path.parentPath.node.id.name };
232+
} else if (typeof visitedTypes[path.parentPath.node.id.name] === 'object') {
233+
// if we already resolved the type simple return it
234+
return visitedTypes[path.parentPath.node.id.name];
235+
}
236+
// mark the type as visited
237+
visitedTypes[path.parentPath.node.id.name] = true;
238+
}
239+
229240
if (types.Type.check(node)) {
230241
if (node.type in flowTypes) {
231242
type = { name: flowTypes[node.type] };
@@ -236,9 +247,26 @@ export default function getFlowType(path: NodePath): FlowTypeDescriptor {
236247
}
237248
}
238249

250+
if (isTypeAlias) {
251+
// mark the type as unvisited so that further calls can resolve the type again
252+
visitedTypes[path.parentPath.node.id.name] = type;
253+
}
254+
239255
if (!type) {
240256
type = { name: 'unknown' };
241257
}
242258

243259
return type;
244260
}
261+
262+
/**
263+
* Tries to identify the flow type by inspecting the path for known
264+
* flow type names. This method doesn't check whether the found type is actually
265+
* existing. It simply assumes that a match is always valid.
266+
*
267+
* If there is no match, "unknown" is returned.
268+
*/
269+
export default function getFlowType(path: NodePath): FlowTypeDescriptor {
270+
visitedTypes = {};
271+
return getFlowTypeWithResolvedTypes(path);
272+
}

0 commit comments

Comments
 (0)