Skip to content

fix(flow): Fix cycles in flow type detection #252

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 17, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions src/utils/__tests__/getFlowType-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -304,4 +304,95 @@ describe('getFlowType', () => {
{ name: 'literal', value: "'banana'" },
], raw: '$Keys<CONTENTS>'});
});

it('handles multiple references to one type', () => {
var typePath = statement(`
let action: { a: Action, b: Action };
type Action = {};
`).get('declarations', 0).get('id').get('typeAnnotation').get('typeAnnotation');

expect(getFlowType(typePath)).toEqual({name: 'signature', type: 'object', signature: {
properties: [
{
key: 'a',
value: {
name: 'signature',
type: 'object',
required: true,
raw: '{}',
signature: { properties: [] },
},
},
{
key: 'b',
value: {
name: 'signature',
type: 'object',
required: true,
raw: '{}',
signature: { properties: [] },
},
},
],
}, raw: '{ a: Action, b: Action }'});
});

it('handles self-referencing type cycles', () => {
var typePath = statement(`
let action: Action;
type Action = { subAction: Action };
`).get('declarations', 0).get('id').get('typeAnnotation').get('typeAnnotation');

expect(getFlowType(typePath)).toEqual({name: 'signature', type: 'object', signature: {
properties: [
{ key: 'subAction', value: { name: 'Action', required: true } },
],
}, raw: '{ subAction: Action }'});
});

it('handles long type cycles', () => {
var typePath = statement(`
let action: Action;
type Action = { subAction: SubAction };
type SubAction = { subAction: SubSubAction };
type SubSubAction = { subAction: SubSubSubAction };
type SubSubSubAction = { rootAction: Action };
`).get('declarations', 0).get('id').get('typeAnnotation').get('typeAnnotation');

expect(getFlowType(typePath)).toEqual({name: 'signature', type: 'object', signature: {
properties: [
{
key: 'subAction',
value: {
name: 'signature', type: 'object', required: true, signature: {
properties: [
{
key: 'subAction',
value: {
name: 'signature', type: 'object', required: true, signature: {
properties: [
{
key: 'subAction',
value: {
name: 'signature', type: 'object', required: true, signature: {
properties: [
{
key: 'rootAction',
value: { name: 'Action', required: true },
},
],
}, raw: '{ rootAction: Action }',
},
},
],
}, raw: '{ subAction: SubSubSubAction }',
},
},
],
}, raw: '{ subAction: SubSubAction }',
},
},
],
}, raw: '{ subAction: SubAction }'});
});
});
70 changes: 49 additions & 21 deletions src/utils/getFlowType.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const namedTypes = {
};

function getFlowTypeWithRequirements(path: NodePath): FlowTypeDescriptor {
const type = getFlowType(path);
const type = getFlowTypeWithResolvedTypes(path);

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

Expand Down Expand Up @@ -80,7 +80,7 @@ function handleKeysHelper(path: NodePath) {
function handleArrayTypeAnnotation(path: NodePath) {
return {
name: 'Array',
elements: [getFlowType(path.get('elementType'))],
elements: [getFlowTypeWithResolvedTypes(path.get('elementType'))],
raw: printValue(path),
};
}
Expand All @@ -102,13 +102,13 @@ function handleGenericTypeAnnotation(path: NodePath) {

type = {
...type,
elements: params.map(param => getFlowType(param)),
elements: params.map(param => getFlowTypeWithResolvedTypes(param)),
raw: printValue(path),
};
} else {
let resolvedPath = resolveToValue(path.get('id'));
if (resolvedPath && resolvedPath.node.right) {
type = getFlowType(resolvedPath.get('right'));
type = getFlowTypeWithResolvedTypes(resolvedPath.get('right'));
}
}

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

path.get('callProperties').each(param => {
type.signature.constructor = getFlowType(param.get('value'));
type.signature.constructor = getFlowTypeWithResolvedTypes(param.get('value'));
});

path.get('indexers').each(param => {
type.signature.properties.push({
key: getFlowType(param.get('key')),
key: getFlowTypeWithResolvedTypes(param.get('key')),
value: getFlowTypeWithRequirements(param.get('value')),
});
});
Expand All @@ -148,15 +148,15 @@ function handleUnionTypeAnnotation(path: NodePath) {
return {
name: 'union',
raw: printValue(path),
elements: path.get('types').map(subType => getFlowType(subType)),
elements: path.get('types').map(subType => getFlowTypeWithResolvedTypes(subType)),
};
}

function handleIntersectionTypeAnnotation(path: NodePath) {
return {
name: 'intersection',
raw: printValue(path),
elements: path.get('types').map(subType => getFlowType(subType)),
elements: path.get('types').map(subType => getFlowTypeWithResolvedTypes(subType)),
};
}

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

if (!typeAnnotation) return null;

const type = getFlowType(typeAnnotation);
const type = getFlowTypeWithResolvedTypes(typeAnnotation);
type.nullable = true;

return type;
Expand All @@ -178,7 +178,7 @@ function handleFunctionTypeAnnotation(path: NodePath) {
raw: printValue(path),
signature: {
arguments: [],
return: getFlowType(path.get('returnType')),
return: getFlowTypeWithResolvedTypes(path.get('returnType')),
},
};

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

type.signature.arguments.push({
name: param.node.name ? param.node.name.name : '',
type: getFlowType(typeAnnotation),
type: getFlowTypeWithResolvedTypes(typeAnnotation),
});
});

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

path.get('types').each(param => {
type.elements.push(getFlowType(param));
type.elements.push(getFlowTypeWithResolvedTypes(param));
});

return type;
}

function handleTypeofTypeAnnotation(path: NodePath) {
return getFlowType(path.get('argument'));
return getFlowTypeWithResolvedTypes(path.get('argument'));
}

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

/**
* Tries to identify the flow type by inspecting the path for known
* flow type names. This method doesn't check whether the found type is actually
* existing. It simply assumes that a match is always valid.
*
* If there is no match, "unknown" is returned.
*/
export default function getFlowType(path: NodePath): FlowTypeDescriptor {
let visitedTypes = {};

function getFlowTypeWithResolvedTypes(path: NodePath): FlowTypeDescriptor {
const node = path.node;
let type: ?FlowTypeDescriptor;

const isTypeAlias = types.TypeAlias.check(path.parentPath.node);
// When we see a typealias mark it as visited so that the next
// call of this function does not run into an endless loop
if (isTypeAlias) {
if (visitedTypes[path.parentPath.node.id.name] === true) {
// if we are currently visiting this node then just return the name
// as we are starting to endless loop
return { name: path.parentPath.node.id.name };
} else if (typeof visitedTypes[path.parentPath.node.id.name] === 'object') {
// if we already resolved the type simple return it
return visitedTypes[path.parentPath.node.id.name];
}
// mark the type as visited
visitedTypes[path.parentPath.node.id.name] = true;
}

if (types.Type.check(node)) {
if (node.type in flowTypes) {
type = { name: flowTypes[node.type] };
Expand All @@ -236,9 +247,26 @@ export default function getFlowType(path: NodePath): FlowTypeDescriptor {
}
}

if (isTypeAlias) {
// mark the type as unvisited so that further calls can resolve the type again
visitedTypes[path.parentPath.node.id.name] = type;
}

if (!type) {
type = { name: 'unknown' };
}

return type;
}

/**
* Tries to identify the flow type by inspecting the path for known
* flow type names. This method doesn't check whether the found type is actually
* existing. It simply assumes that a match is always valid.
*
* If there is no match, "unknown" is returned.
*/
export default function getFlowType(path: NodePath): FlowTypeDescriptor {
visitedTypes = {};
return getFlowTypeWithResolvedTypes(path);
}