From ea424ae9714a01c0c11fc867ce0f0276686e0a38 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Sun, 21 Apr 2019 22:38:37 -0700 Subject: [PATCH] Process TypeScript annotations --- src/__tests__/__snapshots__/main-test.js.snap | 128 ++++ src/__tests__/fixtures/component_21.tsx | 32 + src/__tests__/fixtures/component_22.tsx | 16 + src/__tests__/main-test.js | 8 +- src/handlers/flowTypeHandler.js | 15 + src/utils/__tests__/getTSType-test.js | 599 ++++++++++++++++++ src/utils/getFlowTypeFromReactComponent.js | 7 +- src/utils/getTSType.js | 300 +++++++++ src/utils/getTypeAnnotation.js | 3 +- src/utils/resolveGenericTypeAnnotation.js | 7 + src/utils/resolveObjectKeysToArray.js | 18 +- src/utils/resolveObjectValuesToArray.js | 18 +- src/utils/resolveToValue.js | 3 +- tests/utils.js | 8 +- 14 files changed, 1145 insertions(+), 17 deletions(-) create mode 100644 src/__tests__/fixtures/component_21.tsx create mode 100644 src/__tests__/fixtures/component_22.tsx create mode 100644 src/utils/__tests__/getTSType-test.js create mode 100644 src/utils/getTSType.js diff --git a/src/__tests__/__snapshots__/main-test.js.snap b/src/__tests__/__snapshots__/main-test.js.snap index 6f7551200d6..9a90c78330c 100644 --- a/src/__tests__/__snapshots__/main-test.js.snap +++ b/src/__tests__/__snapshots__/main-test.js.snap @@ -1048,3 +1048,131 @@ Object { }, } `; + +exports[`main fixtures processes component "component_21.tsx" without errors 1`] = ` +Object { + "description": "This is a typescript class component", + "displayName": "TSComponent", + "methods": Array [], + "props": Object { + "bar": Object { + "description": "Required prop", + "flowType": Object { + "name": "number", + }, + "required": true, + }, + "baz": Object { + "description": "Complex union prop", + "flowType": Object { + "elements": Array [ + Object { + "name": "number", + }, + Object { + "name": "signature", + "raw": "{ enter?: number, exit?: number }", + "signature": Object { + "properties": Array [ + Object { + "key": "enter", + "value": Object { + "name": "number", + "required": false, + }, + }, + Object { + "key": "exit", + "value": Object { + "name": "number", + "required": false, + }, + }, + ], + }, + "type": "object", + }, + Object { + "name": "literal", + "value": "'auto'", + }, + ], + "name": "union", + "raw": "number | { enter?: number, exit?: number } | 'auto'", + }, + "required": true, + }, + "foo": Object { + "description": "Optional prop", + "flowType": Object { + "name": "string", + }, + "required": false, + }, + }, +} +`; + +exports[`main fixtures processes component "component_22.tsx" without errors 1`] = ` +Object { + "description": "This is a TypeScript function component", + "displayName": "TSFunctionComponent", + "methods": Array [], + "props": Object { + "align": Object { + "description": "", + "flowType": Object { + "elements": Array [ + Object { + "name": "literal", + "value": "\\"left\\"", + }, + Object { + "name": "literal", + "value": "\\"center\\"", + }, + Object { + "name": "literal", + "value": "\\"right\\"", + }, + Object { + "name": "literal", + "value": "\\"justify\\"", + }, + ], + "name": "union", + "raw": "\\"left\\" | \\"center\\" | \\"right\\" | \\"justify\\"", + }, + "required": false, + }, + "center": Object { + "description": "", + "flowType": Object { + "name": "boolean", + }, + "required": false, + }, + "justify": Object { + "description": "", + "flowType": Object { + "name": "boolean", + }, + "required": false, + }, + "left": Object { + "description": "", + "flowType": Object { + "name": "boolean", + }, + "required": false, + }, + "right": Object { + "description": "", + "flowType": Object { + "name": "boolean", + }, + "required": false, + }, + }, +} +`; diff --git a/src/__tests__/fixtures/component_21.tsx b/src/__tests__/fixtures/component_21.tsx new file mode 100644 index 00000000000..df327820e98 --- /dev/null +++ b/src/__tests__/fixtures/component_21.tsx @@ -0,0 +1,32 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import React, { Component } from 'react'; + +type BaseProps = { + /** Optional prop */ + foo?: string, + /** Required prop */ + bar: number +}; + +type TransitionDuration = number | { enter?: number, exit?: number } | 'auto'; + +type Props = BaseProps & { + /** Complex union prop */ + baz: TransitionDuration +} + +/** + * This is a typescript class component + */ +export default class TSComponent extends Component { + render() { + return

Hello world

; + } +} diff --git a/src/__tests__/fixtures/component_22.tsx b/src/__tests__/fixtures/component_22.tsx new file mode 100644 index 00000000000..35c1892b271 --- /dev/null +++ b/src/__tests__/fixtures/component_22.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +type Props = { + align?: "left" | "center" | "right" | "justify", + left?: boolean, + center?: boolean, + right?: boolean, + justify?: boolean, +}; + +/** + * This is a TypeScript function component + */ +export function TSFunctionComponent(props: Props) { + return

Hello world

; +} diff --git a/src/__tests__/main-test.js b/src/__tests__/main-test.js index 4289b20cad1..77b34754549 100644 --- a/src/__tests__/main-test.js +++ b/src/__tests__/main-test.js @@ -225,12 +225,16 @@ describe('main', () => { const fixturePath = path.join(__dirname, 'fixtures'); const fileNames = fs.readdirSync(fixturePath); for (let i = 0; i < fileNames.length; i++) { - const fileContent = fs.readFileSync(path.join(fixturePath, fileNames[i])); + const filePath = path.join(fixturePath, fileNames[i]); + const fileContent = fs.readFileSync(filePath); it(`processes component "${fileNames[i]}" without errors`, () => { let result; expect(() => { - result = docgen.parse(fileContent); + result = docgen.parse(fileContent, null, null, { + filename: filePath, + babelrc: false, + }); }).not.toThrowError(); expect(result).toMatchSnapshot(); }); diff --git a/src/handlers/flowTypeHandler.js b/src/handlers/flowTypeHandler.js index 24776329934..327dbb19299 100644 --- a/src/handlers/flowTypeHandler.js +++ b/src/handlers/flowTypeHandler.js @@ -11,6 +11,7 @@ import recast from 'recast'; import type Documentation from '../Documentation'; import getFlowType from '../utils/getFlowType'; +import getTSType from '../utils/getTSType'; import getPropertyName from '../utils/getPropertyName'; import getFlowTypeFromReactComponent, { applyToFlowTypeProperties, @@ -53,6 +54,20 @@ function setPropDescriptor(documentation: Documentation, path: NodePath): void { propDescriptor.required = !path.node.optional; propDescriptor.flowType = type; + // We are doing this here instead of in a different handler + // to not need to duplicate the logic for checking for + // imported types that are spread in to props. + setPropDescription(documentation, path); + } else if (types.TSPropertySignature.check(path.node)) { + const type = getTSType(path.get('typeAnnotation')); + + const propName = getPropertyName(path); + if (!propName) return; + + const propDescriptor = documentation.getPropDescriptor(propName); + propDescriptor.required = !path.node.optional; + propDescriptor.flowType = type; + // We are doing this here instead of in a different handler // to not need to duplicate the logic for checking for // imported types that are spread in to props. diff --git a/src/utils/__tests__/getTSType-test.js b/src/utils/__tests__/getTSType-test.js new file mode 100644 index 00000000000..ea491446d4c --- /dev/null +++ b/src/utils/__tests__/getTSType-test.js @@ -0,0 +1,599 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +/* global jest, describe, beforeEach, it, expect */ + +jest.disableAutomock(); + +describe('getTSType', () => { + let expression, statement; + let getTSType; + + beforeEach(() => { + getTSType = require('../getTSType').default; + const { + expression: expr, + statement: stmt, + } = require('../../../tests/utils'); + expression = code => + expr(code, undefined, { + filename: 'test.ts', + babelrc: false, + }); + statement = code => + stmt(code, undefined, { + filename: 'test.ts', + babelrc: false, + }); + }); + + it('detects simple types', () => { + const simplePropTypes = [ + 'string', + 'number', + 'boolean', + 'symbol', + 'object', + 'any', + 'unknown', + 'null', + 'undefined', + 'void', + 'Object', + 'Function', + 'Boolean', + 'String', + 'Number', + ]; + + simplePropTypes.forEach(type => { + const typePath = expression('x: ' + type) + .get('typeAnnotation') + .get('typeAnnotation'); + expect(getTSType(typePath)).toEqual({ name: type }); + }); + }); + + it('detects literal types', () => { + const literalTypes = ['"foo"', 1234, true]; + + literalTypes.forEach(value => { + const typePath = expression(`x: ${value}`) + .get('typeAnnotation') + .get('typeAnnotation'); + expect(getTSType(typePath)).toEqual({ + name: 'literal', + value: `${value}`, + }); + }); + }); + + it('detects external type', () => { + const typePath = expression('x: xyz') + .get('typeAnnotation') + .get('typeAnnotation'); + expect(getTSType(typePath)).toEqual({ name: 'xyz' }); + }); + + it('detects array type shorthand', () => { + const typePath = expression('x: number[]') + .get('typeAnnotation') + .get('typeAnnotation'); + expect(getTSType(typePath)).toEqual({ + name: 'Array', + elements: [{ name: 'number' }], + raw: 'number[]', + }); + }); + + it('detects array type', () => { + const typePath = expression('x: Array') + .get('typeAnnotation') + .get('typeAnnotation'); + expect(getTSType(typePath)).toEqual({ + name: 'Array', + elements: [{ name: 'number' }], + raw: 'Array', + }); + }); + + it('detects array type with multiple types', () => { + const typePath = expression('x: Array') + .get('typeAnnotation') + .get('typeAnnotation'); + expect(getTSType(typePath)).toEqual({ + name: 'Array', + elements: [{ name: 'number' }, { name: 'xyz' }], + raw: 'Array', + }); + }); + + it('detects class type', () => { + const typePath = expression('x: Class') + .get('typeAnnotation') + .get('typeAnnotation'); + expect(getTSType(typePath)).toEqual({ + name: 'Class', + elements: [{ name: 'Boolean' }], + raw: 'Class', + }); + }); + + it('detects function type with subtype', () => { + const typePath = expression('x: Function') + .get('typeAnnotation') + .get('typeAnnotation'); + expect(getTSType(typePath)).toEqual({ + name: 'Function', + elements: [{ name: 'xyz' }], + raw: 'Function', + }); + }); + + it('detects object types', () => { + const typePath = expression('x: { a: string, b?: xyz }') + .get('typeAnnotation') + .get('typeAnnotation'); + expect(getTSType(typePath)).toEqual({ + name: 'signature', + type: 'object', + signature: { + properties: [ + { key: 'a', value: { name: 'string', required: true } }, + { key: 'b', value: { name: 'xyz', required: false } }, + ], + }, + raw: '{ a: string, b?: xyz }', + }); + }); + + it('detects union type', () => { + const typePath = expression('x: string | xyz | "foo" | void') + .get('typeAnnotation') + .get('typeAnnotation'); + expect(getTSType(typePath)).toEqual({ + name: 'union', + elements: [ + { name: 'string' }, + { name: 'xyz' }, + { name: 'literal', value: '"foo"' }, + { name: 'void' }, + ], + raw: 'string | xyz | "foo" | void', + }); + }); + + it('detects intersection type', () => { + const typePath = expression('x: string & xyz & "foo" & void') + .get('typeAnnotation') + .get('typeAnnotation'); + expect(getTSType(typePath)).toEqual({ + name: 'intersection', + elements: [ + { name: 'string' }, + { name: 'xyz' }, + { name: 'literal', value: '"foo"' }, + { name: 'void' }, + ], + raw: 'string & xyz & "foo" & void', + }); + }); + + it('detects function signature type', () => { + const typePath = expression('x: (p1: number, p2: string) => boolean') + .get('typeAnnotation') + .get('typeAnnotation'); + expect(getTSType(typePath)).toEqual({ + name: 'signature', + type: 'function', + signature: { + arguments: [ + { name: 'p1', type: { name: 'number' } }, + { name: 'p2', type: { name: 'string' } }, + ], + return: { name: 'boolean' }, + }, + raw: '(p1: number, p2: string) => boolean', + }); + }); + + it('detects callable signature type', () => { + const typePath = expression('x: { (str: string): string, token: string }') + .get('typeAnnotation') + .get('typeAnnotation'); + expect(getTSType(typePath)).toEqual({ + name: 'signature', + type: 'object', + signature: { + constructor: { + name: 'signature', + type: 'function', + signature: { + arguments: [{ name: 'str', type: { name: 'string' } }], + return: { name: 'string' }, + }, + raw: '(str: string): string,', // TODO: why does it print a comma? + }, + properties: [ + { key: 'token', value: { name: 'string', required: true } }, + ], + }, + raw: '{ (str: string): string, token: string }', + }); + }); + + it('detects map signature', () => { + const typePath = expression( + 'x: { [key: string]: number, [key: "xl"]: string, token: "a" | "b" }', + ) + .get('typeAnnotation') + .get('typeAnnotation'); + expect(getTSType(typePath)).toEqual({ + name: 'signature', + type: 'object', + signature: { + properties: [ + { + key: { name: 'string' }, + value: { name: 'number', required: true }, + }, + { + key: { name: 'literal', value: '"xl"' }, + value: { name: 'string', required: true }, + }, + { + key: 'token', + value: { + name: 'union', + required: true, + raw: '"a" | "b"', + elements: [ + { name: 'literal', value: '"a"' }, + { name: 'literal', value: '"b"' }, + ], + }, + }, + ], + }, + raw: '{ [key: string]: number, [key: "xl"]: string, token: "a" | "b" }', + }); + }); + + it('detects tuple signature', () => { + const typePath = expression('x: [string, number]') + .get('typeAnnotation') + .get('typeAnnotation'); + expect(getTSType(typePath)).toEqual({ + name: 'tuple', + elements: [{ name: 'string' }, { name: 'number' }], + raw: '[string, number]', + }); + }); + + it('detects tuple in union signature', () => { + const typePath = expression('x: [string, number] | [number, string]') + .get('typeAnnotation') + .get('typeAnnotation'); + expect(getTSType(typePath)).toEqual({ + name: 'union', + elements: [ + { + name: 'tuple', + elements: [{ name: 'string' }, { name: 'number' }], + raw: '[string, number]', + }, + { + name: 'tuple', + elements: [{ name: 'number' }, { name: 'string' }], + raw: '[number, string]', + }, + ], + raw: '[string, number] | [number, string]', + }); + }); + + it('resolves types in scope', () => { + const typePath = statement(` + var x: MyType = 2; + + type MyType = string; + `) + .get('declarations', 0) + .get('id') + .get('typeAnnotation') + .get('typeAnnotation'); + + expect(getTSType(typePath)).toEqual({ name: 'string' }); + }); + + it('handles typeof types', () => { + const typePath = statement(` + var x: typeof MyType = {}; + + type MyType = { a: string, b: xyz }; + `) + .get('declarations', 0) + .get('id') + .get('typeAnnotation') + .get('typeAnnotation'); + + expect(getTSType(typePath)).toEqual({ + name: 'signature', + type: 'object', + signature: { + properties: [ + { key: 'a', value: { name: 'string', required: true } }, + { key: 'b', value: { name: 'xyz', required: true } }, + ], + }, + raw: '{ a: string, b: xyz }', + }); + }); + + it('handles qualified type identifiers', () => { + const typePath = statement(` + var x: MyType.x = {}; + + type MyType = { a: string, b: xyz }; + `) + .get('declarations', 0) + .get('id') + .get('typeAnnotation') + .get('typeAnnotation'); + + expect(getTSType(typePath)).toEqual({ + name: 'MyType.x', + }); + }); + + it('handles qualified type identifiers with params', () => { + const typePath = statement(` + var x: MyType.x = {}; + + type MyType = { a: string, b: xyz }; + `) + .get('declarations', 0) + .get('id') + .get('typeAnnotation') + .get('typeAnnotation'); + + expect(getTSType(typePath)).toEqual({ + name: 'MyType.x', + raw: 'MyType.x', + elements: [ + { + name: 'any', + }, + ], + }); + }); + + describe('React types', () => { + function test(type, expected) { + const typePath = statement(` + var x: ${type} = 2; + + type Props = { x: string }; + `) + .get('declarations', 0) + .get('id') + .get('typeAnnotation') + .get('typeAnnotation'); + + expect(getTSType(typePath)).toEqual({ + ...expected, + name: type.replace('.', '').replace(/<.+>/, ''), + raw: type, + }); + } + + const types = { + 'React.Node': {}, + 'React.Key': {}, + 'React.ElementType': {}, + 'React.ChildrenArray': { elements: [{ name: 'string' }] }, + 'React.Element': { elements: [{ name: 'any' }] }, + 'React.Ref': { elements: [{ name: 'Component' }] }, + 'React.ElementProps': { elements: [{ name: 'Component' }] }, + 'React.ElementRef': { elements: [{ name: 'Component' }] }, + 'React.ComponentType': { + elements: [ + { + name: 'signature', + raw: '{ x: string }', + signature: { + properties: [ + { key: 'x', value: { name: 'string', required: true } }, + ], + }, + type: 'object', + }, + ], + }, + 'React.StatelessFunctionalComponent': { + elements: [{ name: 'Props2' }], + }, + }; + + Object.keys(types).forEach(type => { + it(type, () => test(type, types[type])); + }); + }); + + it('resolves keyof to union', () => { + const typePath = statement(` + var x: keyof typeof CONTENTS = 2; + const CONTENTS = { + 'apple': '🍎', + 'banana': '🍌', + }; + `) + .get('declarations', 0) + .get('id') + .get('typeAnnotation') + .get('typeAnnotation'); + + expect(getTSType(typePath)).toEqual({ + name: 'union', + elements: [ + { name: 'literal', value: "'apple'" }, + { name: 'literal', value: "'banana'" }, + ], + raw: 'keyof typeof CONTENTS', + }); + }); + + it('resolves keyof with inline object to union', () => { + const typePath = statement(` + var x: keyof { apple: string, banana: string } = 2; + `) + .get('declarations', 0) + .get('id') + .get('typeAnnotation') + .get('typeAnnotation'); + + expect(getTSType(typePath)).toEqual({ + name: 'union', + elements: [ + { name: 'literal', value: 'apple' }, + { name: 'literal', value: 'banana' }, + ], + raw: 'keyof { apple: string, banana: string }', + }); + }); + + it('handles multiple references to one type', () => { + const typePath = statement(` + let action: { a: Action, b: Action }; + type Action = {}; + `) + .get('declarations', 0) + .get('id') + .get('typeAnnotation') + .get('typeAnnotation'); + + expect(getTSType(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', () => { + const typePath = statement(` + let action: Action; + type Action = { subAction: Action }; + `) + .get('declarations', 0) + .get('id') + .get('typeAnnotation') + .get('typeAnnotation'); + + expect(getTSType(typePath)).toEqual({ + name: 'signature', + type: 'object', + signature: { + properties: [ + { key: 'subAction', value: { name: 'Action', required: true } }, + ], + }, + raw: '{ subAction: Action }', + }); + }); + + it('handles long type cycles', () => { + const 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(getTSType(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 }', + }); + }); +}); diff --git a/src/utils/getFlowTypeFromReactComponent.js b/src/utils/getFlowTypeFromReactComponent.js index 04579f3d787..0b6e49ca1bf 100644 --- a/src/utils/getFlowTypeFromReactComponent.js +++ b/src/utils/getFlowTypeFromReactComponent.js @@ -54,7 +54,12 @@ export function applyToFlowTypeProperties( ) { if (path.node.properties) { path.get('properties').each(propertyPath => callback(propertyPath)); - } else if (path.node.type === 'IntersectionTypeAnnotation') { + } else if (path.node.members) { + path.get('members').each(propertyPath => callback(propertyPath)); + } else if ( + path.node.type === 'IntersectionTypeAnnotation' || + path.node.type === 'TSIntersectionType' + ) { path .get('types') .each(typesPath => applyToFlowTypeProperties(typesPath, callback)); diff --git a/src/utils/getTSType.js b/src/utils/getTSType.js new file mode 100644 index 00000000000..8869a879f59 --- /dev/null +++ b/src/utils/getTSType.js @@ -0,0 +1,300 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ +import getPropertyName from './getPropertyName'; +import printValue from './printValue'; +import recast from 'recast'; +import getTypeAnnotation from '../utils/getTypeAnnotation'; +import resolveToValue from '../utils/resolveToValue'; +import { resolveObjectToNameArray } from '../utils/resolveObjectKeysToArray'; +import type { + FlowTypeDescriptor, + FlowElementsType, + FlowFunctionSignatureType, + FlowObjectSignatureType, +} from '../types'; + +const { + types: { namedTypes: types }, +} = recast; + +const tsTypes = { + TSAnyKeyword: 'any', + TSBooleanKeyword: 'boolean', + TSUnknownKeyword: 'unknown', + TSNullKeyword: 'null', + TSUndefinedKeyword: 'undefined', + TSNumberKeyword: 'number', + TSStringKeyword: 'string', + TSSymbolKeyword: 'symbol', + TSThisType: 'this', + TSObjectKeyword: 'object', + TSVoidKeyword: 'void', +}; + +const namedTypes = { + TSArrayType: handleTSArrayType, + TSTypeReference: handleTSTypeReference, + TSTypeLiteral: handleTSTypeLiteral, + TSUnionType: handleTSUnionType, + TSFunctionType: handleTSFunctionType, + TSIntersectionType: handleTSIntersectionType, + TSTupleType: handleTSTupleType, + TSTypeQuery: handleTSTypeQuery, + TSTypeOperator: handleTSTypeOperator, +}; + +function handleTSArrayType(path: NodePath): FlowElementsType { + return { + name: 'Array', + elements: [getTSTypeWithResolvedTypes(path.get('elementType'))], + raw: printValue(path), + }; +} + +function handleTSTypeReference(path: NodePath): ?FlowTypeDescriptor { + let type: FlowTypeDescriptor; + if (types.TSQualifiedName.check(path.node.typeName)) { + const typeName = path.get('typeName'); + + if (typeName.node.left.name === 'React') { + type = { + name: `${typeName.node.left.name}${typeName.node.right.name}`, + raw: printValue(typeName), + }; + } else { + type = { name: printValue(typeName).replace(/<.*>$/, '') }; + } + } else { + type = { name: path.node.typeName.name }; + } + + if (path.node.typeParameters) { + const params = path.get('typeParameters').get('params'); + + type = { + ...type, + elements: params.map(param => getTSTypeWithResolvedTypes(param)), + raw: printValue(path), + }; + } else { + const resolvedPath = resolveToValue(path.get('typeName')); + if (resolvedPath && resolvedPath.node.typeAnnotation) { + type = getTSTypeWithResolvedTypes(resolvedPath.get('typeAnnotation')); + } + } + + return type; +} + +function getTSTypeWithRequirements(path: NodePath): FlowTypeDescriptor { + const type = getTSTypeWithResolvedTypes(path); + type.required = !path.parentPath.node.optional; + return type; +} + +function handleTSTypeLiteral(path: NodePath): FlowTypeDescriptor { + const type: FlowObjectSignatureType = { + name: 'signature', + type: 'object', + raw: printValue(path), + signature: { properties: [] }, + }; + + path.get('members').each(param => { + if ( + types.TSPropertySignature.check(param.node) || + types.TSMethodSignature.check(param.node) + ) { + type.signature.properties.push({ + key: getPropertyName(param), + value: getTSTypeWithRequirements(param.get('typeAnnotation')), + }); + } else if (types.TSCallSignatureDeclaration.check(param.node)) { + type.signature.constructor = handleTSFunctionType(param); + } else if (types.TSIndexSignature.check(param.node)) { + type.signature.properties.push({ + key: getTSTypeWithResolvedTypes( + param + .get('parameters') + .get(0) + .get('typeAnnotation'), + ), + value: getTSTypeWithRequirements(param.get('typeAnnotation')), + }); + } + }); + + return type; +} + +function handleTSUnionType(path: NodePath): FlowElementsType { + return { + name: 'union', + raw: printValue(path), + elements: path + .get('types') + .map(subType => getTSTypeWithResolvedTypes(subType)), + }; +} + +function handleTSIntersectionType(path: NodePath): FlowElementsType { + return { + name: 'intersection', + raw: printValue(path), + elements: path + .get('types') + .map(subType => getTSTypeWithResolvedTypes(subType)), + }; +} + +function handleTSFunctionType(path: NodePath): FlowFunctionSignatureType { + const type: FlowFunctionSignatureType = { + name: 'signature', + type: 'function', + raw: printValue(path), + signature: { + arguments: [], + return: getTSTypeWithResolvedTypes(path.get('typeAnnotation')), + }, + }; + + path.get('parameters').each(param => { + const typeAnnotation = getTypeAnnotation(param); + if (!typeAnnotation) return; + + type.signature.arguments.push({ + name: param.node.name || '', + type: getTSTypeWithResolvedTypes(typeAnnotation), + }); + }); + + return type; +} + +function handleTSTupleType(path: NodePath): FlowElementsType { + const type: FlowElementsType = { + name: 'tuple', + raw: printValue(path), + elements: [], + }; + + path.get('elementTypes').each(param => { + type.elements.push(getTSTypeWithResolvedTypes(param)); + }); + + return type; +} + +function handleTSTypeQuery(path: NodePath): FlowTypeDescriptor { + const resolvedPath = resolveToValue(path.get('exprName')); + if (resolvedPath && resolvedPath.node.typeAnnotation) { + return getTSTypeWithResolvedTypes(resolvedPath.get('typeAnnotation')); + } + + return { name: path.node.exprName.name }; +} + +function handleTSTypeOperator(path: NodePath): FlowTypeDescriptor { + if (path.node.operator !== 'keyof') { + return null; + } + + let value = path.get('typeAnnotation'); + if (types.TSTypeQuery.check(value.node)) { + value = value.get('exprName'); + } else if (value.node.id) { + value = value.get('id'); + } + + const resolvedPath = resolveToValue(value); + if ( + resolvedPath && + (types.ObjectExpression.check(resolvedPath.node) || + types.TSTypeLiteral.check(resolvedPath.node)) + ) { + const keys = resolveObjectToNameArray(resolvedPath, true); + + if (keys) { + return { + name: 'union', + raw: printValue(path), + elements: keys.map(key => ({ name: 'literal', value: key })), + }; + } + } +} + +let visitedTypes = {}; + +function getTSTypeWithResolvedTypes(path: NodePath): FlowTypeDescriptor { + if (types.TSTypeAnnotation.check(path.node)) { + path = path.get('typeAnnotation'); + } + + const node = path.node; + let type: ?FlowTypeDescriptor; + const isTypeAlias = types.TSTypeAliasDeclaration.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.TSType.check(node)) { + if (node.type in tsTypes) { + type = { name: tsTypes[node.type] }; + } else if (types.TSLiteralType.check(node)) { + type = { + name: 'literal', + value: node.literal.raw || `${node.literal.value}`, + }; + } else if (node.type in namedTypes) { + type = namedTypes[node.type](path); + } + } + + if (!type) { + type = { name: 'unknown' }; + } + + if (isTypeAlias) { + // mark the type as unvisited so that further calls can resolve the type again + visitedTypes[path.parentPath.node.id.name] = type; + } + + 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 getTSType(path: NodePath): FlowTypeDescriptor { + // Empty visited types before an after run + // Before: in case the detection threw and we rerun again + // After: cleanup memory after we are done here + visitedTypes = {}; + const type = getTSTypeWithResolvedTypes(path); + visitedTypes = {}; + + return type; +} diff --git a/src/utils/getTypeAnnotation.js b/src/utils/getTypeAnnotation.js index 79d82f721c6..3baa4d610c5 100644 --- a/src/utils/getTypeAnnotation.js +++ b/src/utils/getTypeAnnotation.js @@ -28,7 +28,8 @@ export default function getTypeAnnotation(path: NodePath): ?NodePath { resultPath = resultPath.get('typeAnnotation'); } while ( hasTypeAnnotation(resultPath) && - !types.FlowType.check(resultPath.node) + !types.FlowType.check(resultPath.node) && + !types.TSType.check(resultPath.node) ); return resultPath; diff --git a/src/utils/resolveGenericTypeAnnotation.js b/src/utils/resolveGenericTypeAnnotation.js index 8bcfff6bfd3..b93997348cc 100644 --- a/src/utils/resolveGenericTypeAnnotation.js +++ b/src/utils/resolveGenericTypeAnnotation.js @@ -26,6 +26,13 @@ function tryResolveGenericTypeAnnotation(path: NodePath): ?NodePath { } return tryResolveGenericTypeAnnotation(typePath.get('right')); + } else if (types.TSTypeReference.check(typePath.node)) { + typePath = resolveToValue(typePath.get('typeName')); + if (isUnreachableFlowType(typePath)) { + return; + } + + return tryResolveGenericTypeAnnotation(typePath.get('typeAnnotation')); } return typePath; diff --git a/src/utils/resolveObjectKeysToArray.js b/src/utils/resolveObjectKeysToArray.js index 640542ccee6..a4a348f9384 100644 --- a/src/utils/resolveObjectKeysToArray.js +++ b/src/utils/resolveObjectKeysToArray.js @@ -38,7 +38,8 @@ function isWhitelistedObjectProperty(prop) { function isWhiteListedObjectTypeProperty(prop) { return ( types.ObjectTypeProperty.check(prop) || - types.ObjectTypeSpreadProperty.check(prop) + types.ObjectTypeSpreadProperty.check(prop) || + types.TSPropertySignature.check(prop) ); } @@ -51,15 +52,24 @@ export function resolveObjectToNameArray( (types.ObjectExpression.check(object.value) && object.value.properties.every(isWhitelistedObjectProperty)) || (types.ObjectTypeAnnotation.check(object.value) && - object.value.properties.every(isWhiteListedObjectTypeProperty)) + object.value.properties.every(isWhiteListedObjectTypeProperty)) || + (types.TSTypeLiteral.check(object.value) && + object.value.members.every(isWhiteListedObjectTypeProperty)) ) { let values = []; let error = false; - object.get('properties').each(propPath => { + const properties = types.TSTypeLiteral.check(object.value) + ? object.get('members') + : object.get('properties'); + properties.each(propPath => { if (error) return; const prop = propPath.value; - if (types.Property.check(prop) || types.ObjectTypeProperty.check(prop)) { + if ( + types.Property.check(prop) || + types.ObjectTypeProperty.check(prop) || + types.TSPropertySignature.check(prop) + ) { // Key is either Identifier or Literal const name = prop.key.name || (raw ? prop.key.raw : prop.key.value); diff --git a/src/utils/resolveObjectValuesToArray.js b/src/utils/resolveObjectValuesToArray.js index 79d5c5f5b99..865e2bf7734 100644 --- a/src/utils/resolveObjectValuesToArray.js +++ b/src/utils/resolveObjectValuesToArray.js @@ -43,7 +43,8 @@ function isWhitelistedObjectProperty(prop) { function isWhiteListedObjectTypeProperty(prop) { return ( types.ObjectTypeProperty.check(prop) || - types.ObjectTypeSpreadProperty.check(prop) + types.ObjectTypeSpreadProperty.check(prop) || + types.TSPropertySignature.check(prop) ); } @@ -56,18 +57,27 @@ export function resolveObjectToPropMap( (types.ObjectExpression.check(object.value) && object.value.properties.every(isWhitelistedObjectProperty)) || (types.ObjectTypeAnnotation.check(object.value) && - object.value.properties.every(isWhiteListedObjectTypeProperty)) + object.value.properties.every(isWhiteListedObjectTypeProperty)) || + (types.TSTypeLiteral.check(object.value) && + object.value.members.every(isWhiteListedObjectTypeProperty)) ) { const properties = []; let values = {}; let error = false; - object.get('properties').each(propPath => { + const members = types.TSTypeLiteral.check(object.value) + ? object.get('members') + : object.get('properties'); + members.each(propPath => { if (error) return; const prop = propPath.value; if (prop.kind === 'get' || prop.kind === 'set') return; - if (types.Property.check(prop) || types.ObjectTypeProperty.check(prop)) { + if ( + types.Property.check(prop) || + types.ObjectTypeProperty.check(prop) || + types.TSPropertySignature.check(prop) + ) { // Key is either Identifier or Literal const name = prop.key.name || (raw ? prop.key.raw : prop.key.value); const propValue = propPath.get(name).parentPath.value; diff --git a/src/utils/resolveToValue.js b/src/utils/resolveToValue.js index 0028026e48d..398797df103 100644 --- a/src/utils/resolveToValue.js +++ b/src/utils/resolveToValue.js @@ -51,7 +51,8 @@ function findScopePath(paths: Array, path: NodePath): ?NodePath { types.ImportSpecifier.check(parentPath.node) || types.ImportNamespaceSpecifier.check(parentPath.node) || types.VariableDeclarator.check(parentPath.node) || - types.TypeAlias.check(parentPath.node) + types.TypeAlias.check(parentPath.node) || + types.TSTypeAliasDeclaration.check(parentPath.node) ) { resultPath = parentPath; } else if (types.Property.check(parentPath.node)) { diff --git a/tests/utils.js b/tests/utils.js index 088c50af48d..216261c146f 100644 --- a/tests/utils.js +++ b/tests/utils.js @@ -21,12 +21,12 @@ export function parse(src, recast = _recast, options = {}) { ); } -export function statement(src, recast = _recast) { - return parse(src, recast).get('body', 0); +export function statement(src, recast = _recast, options) { + return parse(src, recast, options).get('body', 0); } -export function expression(src, recast = _recast) { - return statement('(' + src + ')', recast).get('expression'); +export function expression(src, recast = _recast, options) { + return statement('(' + src + ')', recast, options).get('expression'); } /**