From d1b07c456ba4d4152bfba5d0fa4ed9aa8d3a5615 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Fri, 3 May 2019 16:52:43 -0700 Subject: [PATCH] Added support for TypeScript, Flow interfaces and Flow generic types (#348) * Support parsing typescript with babel * Process TypeScript annotations * Support typescript and flow interfaces * Add composes for unresolved interface extensions * Move TypeScript output to tsType instead of flowType * Add support for flow and typescript generic type parameters * Support method parameters in TypeScript * Support rest parameters in function signatures * Bump recast * Separate TS `this` parameters from arguments * Code review comments * Fix tests for latests babel parser and yarn lock --- README.md | 9 +- package.json | 5 +- src/__tests__/__snapshots__/main-test.js.snap | 373 ++++++++++ src/__tests__/fixtures/component_21.tsx | 32 + src/__tests__/fixtures/component_22.tsx | 16 + src/__tests__/fixtures/component_23.tsx | 32 + src/__tests__/fixtures/component_24.js | 32 + src/__tests__/fixtures/component_25.tsx | 33 + src/__tests__/fixtures/component_26.js | 33 + src/__tests__/fixtures/component_27.tsx | 29 + src/__tests__/main-test.js | 8 +- src/babelParser.js | 62 +- src/handlers/flowTypeHandler.js | 56 +- ...indAllExportedComponentDefinitions-test.js | 2 +- .../findExportedComponentDefinition-test.js | 1 + src/types.js | 8 +- src/utils/__tests__/getFlowType-test.js | 54 +- src/utils/__tests__/getTSType-test.js | 664 ++++++++++++++++++ .../resolveExportDeclaration-test.js | 2 +- src/utils/getFlowType.js | 155 ++-- src/utils/getFlowTypeFromReactComponent.js | 77 +- src/utils/getMethodDocumentation.js | 12 +- src/utils/getTSType.js | 388 ++++++++++ src/utils/getTypeAnnotation.js | 3 +- src/utils/getTypeParameters.js | 48 ++ src/utils/resolveGenericTypeAnnotation.js | 21 +- src/utils/resolveObjectKeysToArray.js | 18 +- src/utils/resolveObjectValuesToArray.js | 18 +- src/utils/resolveToValue.js | 5 +- tests/utils.js | 8 +- yarn.lock | 105 ++- 31 files changed, 2185 insertions(+), 124 deletions(-) create mode 100644 src/__tests__/fixtures/component_21.tsx create mode 100644 src/__tests__/fixtures/component_22.tsx create mode 100644 src/__tests__/fixtures/component_23.tsx create mode 100644 src/__tests__/fixtures/component_24.js create mode 100644 src/__tests__/fixtures/component_25.tsx create mode 100644 src/__tests__/fixtures/component_26.js create mode 100644 src/__tests__/fixtures/component_27.tsx create mode 100644 src/utils/__tests__/getTSType-test.js create mode 100644 src/utils/getTSType.js create mode 100644 src/utils/getTypeParameters.js diff --git a/README.md b/README.md index a05d9a1f3ad..30bf287c867 100644 --- a/README.md +++ b/README.md @@ -239,15 +239,15 @@ we are getting this output: } ``` -## Flow Type support +## Flow and TypeScript support -If you are using [flow][flow] then react-docgen can also extract the flow type annotations. As flow has a way more advanced and fine granular type system, the returned types from react-docgen are different in comparison when using `React.PropTypes`. +If you are using [flow][flow] or [typescript][typescript] then react-docgen can also extract the type annotations. As flow and typescript have way more advanced and fine granular type systems, the returned types from react-docgen are different in comparison when using `React.PropTypes`. > **Note**: react-docgen will not be able to grab the type definition if the type is imported or declared in a different file. ### Example -For the following component +For the following component with Flow types ```js import React, { Component } from 'react'; @@ -387,6 +387,7 @@ The structure of the JSON blob / JavaScript object is as follows: ["raw": string] }, "flowType": , + "tsType": , "required": boolean, "description": string, ["defaultValue": { @@ -406,9 +407,11 @@ The structure of the JSON blob / JavaScript object is as follows: - ``: The name of the type, which is usually corresponds to the function name in `React.PropTypes`. However, for types define with `oneOf`, we use `"enum"` and for `oneOfType` we use `"union"`. If a custom function is provided or the type cannot be resolved to anything of `React.PropTypes`, we use `"custom"`. - ``: Some types accept parameters which define the type in more detail (such as `arrayOf`, `instanceOf`, `oneOf`, etc). Those are stored in ``. The data type of `` depends on the type definition. - ``: If using flow type this property contains the parsed flow type as can be seen in the table above. +- ``: If using TypeScript type this property contains the parsed TypeScript type as can be seen in the table above. [react]: http://facebook.github.io/react/ [flow]: http://flowtype.org/ +[typescript]: http://typescriptlang.org/ [recast]: https://github.com/benjamn/recast [@babel/parser]: https://github.com/babel/babel/tree/master/packages/babel-parser [classes]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes diff --git a/package.json b/package.json index 1d80bd2fc4c..65b22b5c663 100644 --- a/package.json +++ b/package.json @@ -37,17 +37,16 @@ "author": "Felix Kling", "license": "MIT", "dependencies": { - "@babel/core": "^7.0.0", + "@babel/core": "^7.4.4", "@babel/runtime": "^7.0.0", "async": "^2.1.4", "commander": "^2.19.0", "doctrine": "^3.0.0", "node-dir": "^0.1.10", - "recast": "^0.17.3" + "recast": "^0.17.6" }, "devDependencies": { "@babel/cli": "^7.0.0", - "@babel/core": "^7.0.0", "@babel/plugin-proposal-object-rest-spread": "^7.0.0", "@babel/plugin-transform-runtime": "^7.0.0", "@babel/preset-env": "^7.0.0", diff --git a/src/__tests__/__snapshots__/main-test.js.snap b/src/__tests__/__snapshots__/main-test.js.snap index 6f7551200d6..3652dd82b4f 100644 --- a/src/__tests__/__snapshots__/main-test.js.snap +++ b/src/__tests__/__snapshots__/main-test.js.snap @@ -1048,3 +1048,376 @@ 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", + "required": true, + "tsType": Object { + "name": "number", + }, + }, + "baz": Object { + "description": "Complex union prop", + "required": true, + "tsType": 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'", + }, + }, + "foo": Object { + "description": "Optional prop", + "required": false, + "tsType": Object { + "name": "string", + }, + }, + }, +} +`; + +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": "", + "required": false, + "tsType": 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\\"", + }, + }, + "center": Object { + "description": "", + "required": false, + "tsType": Object { + "name": "boolean", + }, + }, + "justify": Object { + "description": "", + "required": false, + "tsType": Object { + "name": "boolean", + }, + }, + "left": Object { + "description": "", + "required": false, + "tsType": Object { + "name": "boolean", + }, + }, + "right": Object { + "description": "", + "required": false, + "tsType": Object { + "name": "boolean", + }, + }, + }, +} +`; + +exports[`main fixtures processes component "component_23.tsx" without errors 1`] = ` +Object { + "composes": Array [ + "OtherProps", + ], + "description": "This is a typescript class component", + "displayName": "TSComponent", + "methods": Array [], + "props": Object { + "bar": Object { + "description": "Required prop", + "required": true, + "tsType": Object { + "name": "number", + }, + }, + "baz": Object { + "description": "Complex union prop", + "required": true, + "tsType": 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'", + }, + }, + "foo": Object { + "description": "Optional prop", + "required": false, + "tsType": Object { + "name": "string", + }, + }, + }, +} +`; + +exports[`main fixtures processes component "component_24.js" without errors 1`] = ` +Object { + "composes": Array [ + "OtherProps", + ], + "description": "This is a flow class component with an interface as props", + "displayName": "FlowComponent", + "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_25.tsx" without errors 1`] = ` +Object { + "description": "This is a typescript class component", + "displayName": "TSComponent", + "methods": Array [], + "props": Object { + "bar": Object { + "description": "Required prop", + "required": true, + "tsType": Object { + "elements": Array [ + Object { + "name": "Child", + }, + ], + "name": "Array", + "raw": "Array", + }, + }, + "baz": Object { + "description": "Complex union prop", + "required": true, + "tsType": Object { + "name": "number", + }, + }, + "foo": Object { + "description": "Optional prop", + "required": false, + "tsType": Object { + "name": "Child", + }, + }, + }, +} +`; + +exports[`main fixtures processes component "component_26.js" without errors 1`] = ` +Object { + "description": "This is a typescript class component", + "displayName": "FlowComponent", + "methods": Array [], + "props": Object { + "bar": Object { + "description": "Required prop", + "flowType": Object { + "elements": Array [ + Object { + "name": "Child", + }, + ], + "name": "Array", + "raw": "Array", + }, + "required": true, + }, + "baz": Object { + "description": "Complex union prop", + "flowType": Object { + "name": "number", + }, + "required": true, + }, + "foo": Object { + "description": "Optional prop", + "flowType": Object { + "name": "Child", + }, + "required": false, + }, + }, +} +`; + +exports[`main fixtures processes component "component_27.tsx" without errors 1`] = ` +Object { + "description": "This is a typescript class component", + "displayName": "TSComponent", + "methods": Array [ + Object { + "description": "This is a method", + "docblock": "This is a method", + "modifiers": Array [], + "name": "method", + "params": Array [ + Object { + "name": "a", + "type": Object { + "name": "string", + }, + }, + ], + "returns": Object { + "type": Object { + "name": "string", + }, + }, + }, + ], + "props": Object { + "foo": Object { + "description": "", + "required": true, + "tsType": Object { + "name": "string", + }, + }, + }, +} +`; 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__/fixtures/component_23.tsx b/src/__tests__/fixtures/component_23.tsx new file mode 100644 index 00000000000..45f6e25be08 --- /dev/null +++ b/src/__tests__/fixtures/component_23.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'; + +interface BaseProps { + /** Optional prop */ + foo?: string, + /** Required prop */ + bar: number +} + +type TransitionDuration = number | { enter?: number, exit?: number } | 'auto'; + +interface Props extends BaseProps, OtherProps { + /** 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_24.js b/src/__tests__/fixtures/component_24.js new file mode 100644 index 00000000000..09eb6b7ca79 --- /dev/null +++ b/src/__tests__/fixtures/component_24.js @@ -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'; + +interface BaseProps { + /** Optional prop */ + foo?: string, + /** Required prop */ + bar: number +} + +type TransitionDuration = number | { enter?: number, exit?: number } | 'auto'; + +interface Props extends BaseProps, OtherProps { + /** Complex union prop */ + baz: TransitionDuration +} + +/** + * This is a flow class component with an interface as props + */ +export default class FlowComponent extends Component { + render() { + return

Hello world

; + } +} diff --git a/src/__tests__/fixtures/component_25.tsx b/src/__tests__/fixtures/component_25.tsx new file mode 100644 index 00000000000..d8e00c27654 --- /dev/null +++ b/src/__tests__/fixtures/component_25.tsx @@ -0,0 +1,33 @@ +/** + * 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 Test = Array; +interface BaseProps { + /** Optional prop */ + foo?: T, + /** Required prop */ + bar: Test +} + +interface Child {} + +interface Props extends BaseProps { + /** Complex union prop */ + baz: number +} + +/** + * This is a typescript class component + */ +export default class TSComponent extends Component { + render() { + return

Hello world

; + } +} diff --git a/src/__tests__/fixtures/component_26.js b/src/__tests__/fixtures/component_26.js new file mode 100644 index 00000000000..6a6084aebfa --- /dev/null +++ b/src/__tests__/fixtures/component_26.js @@ -0,0 +1,33 @@ +/** + * 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 Test = Array; +interface BaseProps { + /** Optional prop */ + foo?: T, + /** Required prop */ + bar: Test +} + +interface Child {} + +interface Props extends BaseProps { + /** Complex union prop */ + baz: number +} + +/** + * This is a typescript class component + */ +export default class FlowComponent extends Component { + render() { + return

Hello world

; + } +} diff --git a/src/__tests__/fixtures/component_27.tsx b/src/__tests__/fixtures/component_27.tsx new file mode 100644 index 00000000000..9cd7c4187ae --- /dev/null +++ b/src/__tests__/fixtures/component_27.tsx @@ -0,0 +1,29 @@ +/** + * 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'; + +interface Props { + foo: string +} + +/** + * This is a typescript class component + */ +export default class TSComponent extends Component { + render() { + return

Hello world

; + } + + /** + * This is a method + */ + method(a: string): string { + return a; + } +} 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/babelParser.js b/src/babelParser.js index 993fc2712e4..7ef995e445a 100644 --- a/src/babelParser.js +++ b/src/babelParser.js @@ -8,32 +8,42 @@ */ const babel = require('@babel/core'); +const path = require('path'); -const defaultPlugins = [ - 'jsx', - 'flow', - 'asyncGenerators', - 'bigInt', - 'classProperties', - 'classPrivateProperties', - 'classPrivateMethods', - ['decorators', { decoratorsBeforeExport: false }], - 'doExpressions', - 'dynamicImport', - 'exportDefaultFrom', - 'exportNamespaceFrom', - 'functionBind', - 'functionSent', - 'importMeta', - 'logicalAssignment', - 'nullishCoalescingOperator', - 'numericSeparator', - 'objectRestSpread', - 'optionalCatchBinding', - 'optionalChaining', - ['pipelineOperator', { proposal: 'minimal' }], - 'throwExpressions', -]; +const TYPESCRIPT_EXTS = { + '.ts': true, + '.tsx': true, +}; + +function getDefaultPlugins(options: BabelOptions) { + return [ + 'jsx', + TYPESCRIPT_EXTS[path.extname(options.filename || '')] + ? 'typescript' + : 'flow', + 'asyncGenerators', + 'bigInt', + 'classProperties', + 'classPrivateProperties', + 'classPrivateMethods', + ['decorators', { decoratorsBeforeExport: false }], + 'doExpressions', + 'dynamicImport', + 'exportDefaultFrom', + 'exportNamespaceFrom', + 'functionBind', + 'functionSent', + 'importMeta', + 'logicalAssignment', + 'nullishCoalescingOperator', + 'numericSeparator', + 'objectRestSpread', + 'optionalCatchBinding', + 'optionalChaining', + ['pipelineOperator', { proposal: 'minimal' }], + 'throwExpressions', + ]; +} type ParserOptions = { plugins?: Array, @@ -73,7 +83,7 @@ function buildOptions( const partialConfig = babel.loadPartialConfig(babelOptions); if (!partialConfig.hasFilesystemConfig() && parserOpts.plugins.length === 0) { - parserOpts.plugins = [...defaultPlugins]; + parserOpts.plugins = getDefaultPlugins(babelOptions); } // Recast needs tokens to be in the tree diff --git a/src/handlers/flowTypeHandler.js b/src/handlers/flowTypeHandler.js index 24776329934..2512dd805ae 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, @@ -18,18 +19,28 @@ import getFlowTypeFromReactComponent, { import resolveToValue from '../utils/resolveToValue'; import setPropDescription from '../utils/setPropDescription'; import { unwrapUtilityType } from '../utils/flowUtilityTypes'; +import { type TypeParameters } from '../utils/getTypeParameters'; const { types: { namedTypes: types }, } = recast; -function setPropDescriptor(documentation: Documentation, path: NodePath): void { +function setPropDescriptor( + documentation: Documentation, + path: NodePath, + typeParams: ?TypeParameters, +): void { if (types.ObjectTypeSpreadProperty.check(path.node)) { const argument = unwrapUtilityType(path.get('argument')); if (types.ObjectTypeAnnotation.check(argument.node)) { - applyToFlowTypeProperties(argument, propertyPath => { - setPropDescriptor(documentation, propertyPath); - }); + applyToFlowTypeProperties( + documentation, + argument, + (propertyPath, innerTypeParams) => { + setPropDescriptor(documentation, propertyPath, innerTypeParams); + }, + typeParams, + ); return; } @@ -38,14 +49,19 @@ function setPropDescriptor(documentation: Documentation, path: NodePath): void { if (resolvedPath && types.TypeAlias.check(resolvedPath.node)) { const right = resolvedPath.get('right'); - applyToFlowTypeProperties(right, propertyPath => { - setPropDescriptor(documentation, propertyPath); - }); + applyToFlowTypeProperties( + documentation, + right, + (propertyPath, innerTypeParams) => { + setPropDescriptor(documentation, propertyPath, innerTypeParams); + }, + typeParams, + ); } else { documentation.addComposes(name.node.name); } } else if (types.ObjectTypeProperty.check(path.node)) { - const type = getFlowType(path.get('value')); + const type = getFlowType(path.get('value'), typeParams); const propName = getPropertyName(path); if (!propName) return; @@ -53,6 +69,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'), typeParams); + + const propName = getPropertyName(path); + if (!propName) return; + + const propDescriptor = documentation.getPropDescriptor(propName); + propDescriptor.required = !path.node.optional; + propDescriptor.tsType = 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. @@ -75,7 +105,11 @@ export default function flowTypeHandler( return; } - applyToFlowTypeProperties(flowTypesPath, propertyPath => { - setPropDescriptor(documentation, propertyPath); - }); + applyToFlowTypeProperties( + documentation, + flowTypesPath, + (propertyPath, typeParams) => { + setPropDescriptor(documentation, propertyPath, typeParams); + }, + ); } diff --git a/src/resolver/__tests__/findAllExportedComponentDefinitions-test.js b/src/resolver/__tests__/findAllExportedComponentDefinitions-test.js index b6c54ba12d5..cb40555ea0a 100644 --- a/src/resolver/__tests__/findAllExportedComponentDefinitions-test.js +++ b/src/resolver/__tests__/findAllExportedComponentDefinitions-test.js @@ -867,7 +867,7 @@ describe('findAllExportedComponentDefinitions', () => { parsed = parse(` import React from "React" - + var foo = 42; var Component = React.createClass({}); export {Component, foo} `); diff --git a/src/resolver/__tests__/findExportedComponentDefinition-test.js b/src/resolver/__tests__/findExportedComponentDefinition-test.js index ecc1283f61d..849ca43fe0a 100644 --- a/src/resolver/__tests__/findExportedComponentDefinition-test.js +++ b/src/resolver/__tests__/findExportedComponentDefinition-test.js @@ -617,6 +617,7 @@ describe('findExportedComponentDefinition', () => { source = ` import React from "React" + var foo = 42; var Component = React.createClass({}); export {Component, foo} `; diff --git a/src/types.js b/src/types.js index 59b3c9c95f3..ff504122091 100644 --- a/src/types.js +++ b/src/types.js @@ -59,12 +59,18 @@ export type FlowElementsType = FlowBaseType & { elements: Array, }; +export type FlowFunctionArgumentType = { + name: string, + type: FlowTypeDescriptor, + rest?: boolean, +}; + export type FlowFunctionSignatureType = FlowBaseType & { name: 'signature', type: 'function', raw: string, signature: { - arguments: Array<{ name: string, type: FlowTypeDescriptor }>, + arguments: Array, return: FlowTypeDescriptor, }, }; diff --git a/src/utils/__tests__/getFlowType-test.js b/src/utils/__tests__/getFlowType-test.js index 1e2b129cd91..5a7e11318bd 100644 --- a/src/utils/__tests__/getFlowType-test.js +++ b/src/utils/__tests__/getFlowType-test.js @@ -216,7 +216,9 @@ describe('getFlowType', () => { }); it('detects function signature type', () => { - const typePath = expression('x: (p1: number, p2: ?string) => boolean') + const typePath = expression( + 'x: (p1: number, p2: ?string, ...rest: Array) => boolean', + ) .get('typeAnnotation') .get('typeAnnotation'); expect(getFlowType(typePath)).toEqual({ @@ -226,10 +228,19 @@ describe('getFlowType', () => { arguments: [ { name: 'p1', type: { name: 'number' } }, { name: 'p2', type: { name: 'string', nullable: true } }, + { + name: 'rest', + rest: true, + type: { + name: 'Array', + elements: [{ name: 'string' }], + raw: 'Array', + }, + }, ], return: { name: 'boolean' }, }, - raw: '(p1: number, p2: ?string) => boolean', + raw: '(p1: number, p2: ?string, ...rest: Array) => boolean', }); }); @@ -265,6 +276,7 @@ describe('getFlowType', () => { raw: 'string => boolean', }); }); + it('detects callable signature type', () => { const typePath = expression('x: { (str: string): string, token: string }') .get('typeAnnotation') @@ -436,6 +448,44 @@ describe('getFlowType', () => { }); }); + it('handles generic types', () => { + const typePath = statement(` + var x: MyType = {}; + + type MyType = { a: T, b: Array }; + `) + .get('declarations', 0) + .get('id') + .get('typeAnnotation') + .get('typeAnnotation'); + + expect(getFlowType(typePath)).toEqual({ + name: 'signature', + type: 'object', + raw: '{ a: T, b: Array }', + signature: { + properties: [ + { + key: 'a', + value: { + name: 'string', + required: true, + }, + }, + { + key: 'b', + value: { + name: 'Array', + raw: 'Array', + required: true, + elements: [{ name: 'string' }], + }, + }, + ], + }, + }); + }); + describe('React types', () => { function test(type, expected) { const typePath = statement(` diff --git a/src/utils/__tests__/getTSType-test.js b/src/utils/__tests__/getTSType-test.js new file mode 100644 index 00000000000..8ff2f35e276 --- /dev/null +++ b/src/utils/__tests__/getTSType-test.js @@ -0,0 +1,664 @@ +/** + * 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, ...rest: Array) => 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' } }, + { + name: 'rest', + rest: true, + type: { + name: 'Array', + elements: [{ name: 'string' }], + raw: 'Array', + }, + }, + ], + return: { name: 'boolean' }, + }, + raw: '(p1: number, p2: string, ...rest: Array) => boolean', + }); + }); + + it('detects function signature type with `this` parameter', () => { + const typePath = expression('x: (this: Foo, p1: number) => boolean') + .get('typeAnnotation') + .get('typeAnnotation'); + expect(getTSType(typePath)).toEqual({ + name: 'signature', + type: 'function', + signature: { + arguments: [{ name: 'p1', type: { name: 'number' } }], + this: { name: 'Foo' }, + return: { name: 'boolean' }, + }, + raw: '(this: Foo, p1: number) => 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', + }, + ], + }); + }); + + it('handles generic types', () => { + const typePath = statement(` + var x: MyType = {}; + + type MyType = { a: T, b: Array }; + `) + .get('declarations', 0) + .get('id') + .get('typeAnnotation') + .get('typeAnnotation'); + + expect(getTSType(typePath)).toEqual({ + name: 'signature', + type: 'object', + raw: '{ a: T, b: Array }', + signature: { + properties: [ + { + key: 'a', + value: { + name: 'string', + required: true, + }, + }, + { + key: 'b', + value: { + name: 'Array', + raw: 'Array', + required: true, + elements: [{ name: 'string' }], + }, + }, + ], + }, + }); + }); + + 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/__tests__/resolveExportDeclaration-test.js b/src/utils/__tests__/resolveExportDeclaration-test.js index 779266b7f55..4a010eb74a4 100644 --- a/src/utils/__tests__/resolveExportDeclaration-test.js +++ b/src/utils/__tests__/resolveExportDeclaration-test.js @@ -53,7 +53,7 @@ describe('resolveExportDeclaration', () => { }); it('resolves named exports', () => { - const exp = statement('export {foo, bar, baz}'); + const exp = statement('export {foo, bar, baz}; var foo, bar, baz;'); const resolved = resolveExportDeclaration(exp); const specifiers = exp.get('specifiers'); diff --git a/src/utils/getFlowType.js b/src/utils/getFlowType.js index 8d2de09fccb..1aca5dc6d65 100644 --- a/src/utils/getFlowType.js +++ b/src/utils/getFlowType.js @@ -15,6 +15,9 @@ import recast from 'recast'; import getTypeAnnotation from '../utils/getTypeAnnotation'; import resolveToValue from '../utils/resolveToValue'; import { resolveObjectToNameArray } from '../utils/resolveObjectKeysToArray'; +import getTypeParameters, { + type TypeParameters, +} from '../utils/getTypeParameters'; import type { FlowTypeDescriptor, FlowElementsType, @@ -46,6 +49,7 @@ const namedTypes = { ArrayTypeAnnotation: handleArrayTypeAnnotation, GenericTypeAnnotation: handleGenericTypeAnnotation, ObjectTypeAnnotation: handleObjectTypeAnnotation, + InterfaceDeclaration: handleInterfaceDeclaration, UnionTypeAnnotation: handleUnionTypeAnnotation, NullableTypeAnnotation: handleNullableTypeAnnotation, FunctionTypeAnnotation: handleFunctionTypeAnnotation, @@ -54,8 +58,11 @@ const namedTypes = { TypeofTypeAnnotation: handleTypeofTypeAnnotation, }; -function getFlowTypeWithRequirements(path: NodePath): FlowTypeDescriptor { - const type = getFlowTypeWithResolvedTypes(path); +function getFlowTypeWithRequirements( + path: NodePath, + typeParams: ?TypeParameters, +): FlowTypeDescriptor { + const type = getFlowTypeWithResolvedTypes(path, typeParams); type.required = !path.parentPath.node.optional; @@ -89,15 +96,23 @@ function handleKeysHelper(path: NodePath): ?FlowElementsType { return null; } -function handleArrayTypeAnnotation(path: NodePath): FlowElementsType { +function handleArrayTypeAnnotation( + path: NodePath, + typeParams: ?TypeParameters, +): FlowElementsType { return { name: 'Array', - elements: [getFlowTypeWithResolvedTypes(path.get('elementType'))], + elements: [ + getFlowTypeWithResolvedTypes(path.get('elementType'), typeParams), + ], raw: printValue(path), }; } -function handleGenericTypeAnnotation(path: NodePath): ?FlowTypeDescriptor { +function handleGenericTypeAnnotation( + path: NodePath, + typeParams: ?TypeParameters, +): ?FlowTypeDescriptor { if (path.node.id.name === '$Keys' && path.node.typeParameters) { return handleKeysHelper(path); } @@ -118,25 +133,42 @@ function handleGenericTypeAnnotation(path: NodePath): ?FlowTypeDescriptor { type = { name: path.node.id.name }; } - if (path.node.typeParameters) { + const resolvedPath = + (typeParams && typeParams[type.name]) || resolveToValue(path.get('id')); + + if (path.node.typeParameters && resolvedPath.node.typeParameters) { + typeParams = getTypeParameters( + resolvedPath.get('typeParameters'), + path.get('typeParameters'), + typeParams, + ); + } + + if (typeParams && typeParams[type.name]) { + type = getFlowTypeWithResolvedTypes(resolvedPath, typeParams); + } + + if (resolvedPath && resolvedPath.node.right) { + type = getFlowTypeWithResolvedTypes(resolvedPath.get('right'), typeParams); + } else if (path.node.typeParameters) { const params = path.get('typeParameters').get('params'); type = { ...type, - elements: params.map(param => getFlowTypeWithResolvedTypes(param)), + elements: params.map(param => + getFlowTypeWithResolvedTypes(param, typeParams), + ), raw: printValue(path), }; - } else { - const resolvedPath = resolveToValue(path.get('id')); - if (resolvedPath && resolvedPath.node.right) { - type = getFlowTypeWithResolvedTypes(resolvedPath.get('right')); - } } return type; } -function handleObjectTypeAnnotation(path: NodePath): FlowTypeDescriptor { +function handleObjectTypeAnnotation( + path: NodePath, + typeParams: ?TypeParameters, +): FlowTypeDescriptor { const type: FlowObjectSignatureType = { name: 'signature', type: 'object', @@ -147,13 +179,14 @@ function handleObjectTypeAnnotation(path: NodePath): FlowTypeDescriptor { path.get('callProperties').each(param => { type.signature.constructor = getFlowTypeWithResolvedTypes( param.get('value'), + typeParams, ); }); path.get('indexers').each(param => { type.signature.properties.push({ - key: getFlowTypeWithResolvedTypes(param.get('key')), - value: getFlowTypeWithRequirements(param.get('value')), + key: getFlowTypeWithResolvedTypes(param.get('key'), typeParams), + value: getFlowTypeWithRequirements(param.get('value'), typeParams), }); }); @@ -161,7 +194,7 @@ function handleObjectTypeAnnotation(path: NodePath): FlowTypeDescriptor { if (types.ObjectTypeProperty.check(param.node)) { type.signature.properties.push({ key: getPropertyName(param), - value: getFlowTypeWithRequirements(param.get('value')), + value: getFlowTypeWithRequirements(param.get('value'), typeParams), }); } }); @@ -169,32 +202,49 @@ function handleObjectTypeAnnotation(path: NodePath): FlowTypeDescriptor { return type; } -function handleUnionTypeAnnotation(path: NodePath): FlowElementsType { +function handleInterfaceDeclaration(path: NodePath): FlowElementsType { + // Interfaces are handled like references which would be documented separately, + // rather than inlined like type aliases. + return { + name: path.node.id.name, + }; +} + +function handleUnionTypeAnnotation( + path: NodePath, + typeParams: ?TypeParameters, +): FlowElementsType { return { name: 'union', raw: printValue(path), elements: path .get('types') - .map(subType => getFlowTypeWithResolvedTypes(subType)), + .map(subType => getFlowTypeWithResolvedTypes(subType, typeParams)), }; } -function handleIntersectionTypeAnnotation(path: NodePath): FlowElementsType { +function handleIntersectionTypeAnnotation( + path: NodePath, + typeParams: ?TypeParameters, +): FlowElementsType { return { name: 'intersection', raw: printValue(path), elements: path .get('types') - .map(subType => getFlowTypeWithResolvedTypes(subType)), + .map(subType => getFlowTypeWithResolvedTypes(subType, typeParams)), }; } -function handleNullableTypeAnnotation(path: NodePath): ?FlowTypeDescriptor { +function handleNullableTypeAnnotation( + path: NodePath, + typeParams: ?TypeParameters, +): ?FlowTypeDescriptor { const typeAnnotation = getTypeAnnotation(path); if (!typeAnnotation) return null; - const type = getFlowTypeWithResolvedTypes(typeAnnotation); + const type = getFlowTypeWithResolvedTypes(typeAnnotation, typeParams); type.nullable = true; return type; @@ -202,6 +252,7 @@ function handleNullableTypeAnnotation(path: NodePath): ?FlowTypeDescriptor { function handleFunctionTypeAnnotation( path: NodePath, + typeParams: ?TypeParameters, ): FlowFunctionSignatureType { const type: FlowFunctionSignatureType = { name: 'signature', @@ -209,24 +260,41 @@ function handleFunctionTypeAnnotation( raw: printValue(path), signature: { arguments: [], - return: getFlowTypeWithResolvedTypes(path.get('returnType')), + return: getFlowTypeWithResolvedTypes(path.get('returnType'), typeParams), }, }; path.get('params').each(param => { const typeAnnotation = getTypeAnnotation(param); - if (!typeAnnotation) return; type.signature.arguments.push({ name: param.node.name ? param.node.name.name : '', - type: getFlowTypeWithResolvedTypes(typeAnnotation), + type: typeAnnotation + ? getFlowTypeWithResolvedTypes(typeAnnotation, typeParams) + : null, }); }); + if (path.node.rest) { + const rest = path.get('rest'); + const typeAnnotation = getTypeAnnotation(rest); + + type.signature.arguments.push({ + name: rest.node.name ? rest.node.name.name : '', + type: typeAnnotation + ? getFlowTypeWithResolvedTypes(typeAnnotation, typeParams) + : null, + rest: true, + }); + } + return type; } -function handleTupleTypeAnnotation(path: NodePath): FlowElementsType { +function handleTupleTypeAnnotation( + path: NodePath, + typeParams: ?TypeParameters, +): FlowElementsType { const type: FlowElementsType = { name: 'tuple', raw: printValue(path), @@ -234,19 +302,25 @@ function handleTupleTypeAnnotation(path: NodePath): FlowElementsType { }; path.get('types').each(param => { - type.elements.push(getFlowTypeWithResolvedTypes(param)); + type.elements.push(getFlowTypeWithResolvedTypes(param, typeParams)); }); return type; } -function handleTypeofTypeAnnotation(path: NodePath): FlowTypeDescriptor { - return getFlowTypeWithResolvedTypes(path.get('argument')); +function handleTypeofTypeAnnotation( + path: NodePath, + typeParams: ?TypeParameters, +): FlowTypeDescriptor { + return getFlowTypeWithResolvedTypes(path.get('argument'), typeParams); } let visitedTypes = {}; -function getFlowTypeWithResolvedTypes(path: NodePath): FlowTypeDescriptor { +function getFlowTypeWithResolvedTypes( + path: NodePath, + typeParams: ?TypeParameters, +): FlowTypeDescriptor { const node = path.node; let type: ?FlowTypeDescriptor; @@ -266,14 +340,12 @@ function getFlowTypeWithResolvedTypes(path: NodePath): FlowTypeDescriptor { visitedTypes[path.parentPath.node.id.name] = true; } - if (types.FlowType.check(node)) { - if (node.type in flowTypes) { - type = { name: flowTypes[node.type] }; - } else if (node.type in flowLiteralTypes) { - type = { name: 'literal', value: node.raw || `${node.value}` }; - } else if (node.type in namedTypes) { - type = namedTypes[node.type](path); - } + if (node.type in flowTypes) { + type = { name: flowTypes[node.type] }; + } else if (node.type in flowLiteralTypes) { + type = { name: 'literal', value: node.raw || `${node.value}` }; + } else if (node.type in namedTypes) { + type = namedTypes[node.type](path, typeParams); } if (!type) { @@ -295,12 +367,15 @@ function getFlowTypeWithResolvedTypes(path: NodePath): FlowTypeDescriptor { * * If there is no match, "unknown" is returned. */ -export default function getFlowType(path: NodePath): FlowTypeDescriptor { +export default function getFlowType( + path: NodePath, + typeParams: ?TypeParameters, +): 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 = getFlowTypeWithResolvedTypes(path); + const type = getFlowTypeWithResolvedTypes(path, typeParams); visitedTypes = {}; return type; diff --git a/src/utils/getFlowTypeFromReactComponent.js b/src/utils/getFlowTypeFromReactComponent.js index 04579f3d787..a8f3ba46863 100644 --- a/src/utils/getFlowTypeFromReactComponent.js +++ b/src/utils/getFlowTypeFromReactComponent.js @@ -7,11 +7,15 @@ * @flow */ +import type Documentation from '../Documentation'; import getTypeAnnotation from '../utils/getTypeAnnotation'; import getMemberValuePath from '../utils/getMemberValuePath'; import isReactComponentClass from '../utils/isReactComponentClass'; import isStatelessComponent from '../utils/isStatelessComponent'; import resolveGenericTypeAnnotation from '../utils/resolveGenericTypeAnnotation'; +import getTypeParameters, { + type TypeParameters, +} from '../utils/getTypeParameters'; /** * Given an React component (stateless or class) tries to find the @@ -49,21 +53,84 @@ export default (path: NodePath): ?NodePath => { }; export function applyToFlowTypeProperties( + documentation: Documentation, path: NodePath, - callback: (propertyPath: NodePath) => void, + callback: (propertyPath: NodePath, typeParams: ?TypeParameters) => void, + typeParams?: ?TypeParameters, ) { if (path.node.properties) { - path.get('properties').each(propertyPath => callback(propertyPath)); - } else if (path.node.type === 'IntersectionTypeAnnotation') { + path + .get('properties') + .each(propertyPath => callback(propertyPath, typeParams)); + } else if (path.node.members) { + path + .get('members') + .each(propertyPath => callback(propertyPath, typeParams)); + } else if (path.node.type === 'InterfaceDeclaration') { + if (path.node.extends) { + applyExtends(documentation, path, callback, typeParams); + } + + path + .get('body', 'properties') + .each(propertyPath => callback(propertyPath, typeParams)); + } else if (path.node.type === 'TSInterfaceDeclaration') { + if (path.node.extends) { + applyExtends(documentation, path, callback, typeParams); + } + + path + .get('body', 'body') + .each(propertyPath => callback(propertyPath, typeParams)); + } else if ( + path.node.type === 'IntersectionTypeAnnotation' || + path.node.type === 'TSIntersectionType' + ) { path .get('types') - .each(typesPath => applyToFlowTypeProperties(typesPath, callback)); + .each(typesPath => + applyToFlowTypeProperties( + documentation, + typesPath, + callback, + typeParams, + ), + ); } else if (path.node.type !== 'UnionTypeAnnotation') { // The react-docgen output format does not currently allow // for the expression of union types const typePath = resolveGenericTypeAnnotation(path); if (typePath) { - applyToFlowTypeProperties(typePath, callback); + applyToFlowTypeProperties(documentation, typePath, callback, typeParams); } } } + +function applyExtends(documentation, path, callback, typeParams) { + path.get('extends').each((extendsPath: NodePath) => { + const resolvedPath = resolveGenericTypeAnnotation(extendsPath); + if (resolvedPath) { + if (resolvedPath.node.typeParameters && extendsPath.node.typeParameters) { + typeParams = getTypeParameters( + resolvedPath.get('typeParameters'), + extendsPath.get('typeParameters'), + typeParams, + ); + } + applyToFlowTypeProperties( + documentation, + resolvedPath, + callback, + typeParams, + ); + } else { + const id = + extendsPath.node.id || + extendsPath.node.typeName || + extendsPath.node.expression; + if (id && id.type === 'Identifier') { + documentation.addComposes(id.name); + } + } + }); +} diff --git a/src/utils/getMethodDocumentation.js b/src/utils/getMethodDocumentation.js index 1b901ed7932..163a74b73a1 100644 --- a/src/utils/getMethodDocumentation.js +++ b/src/utils/getMethodDocumentation.js @@ -9,6 +9,7 @@ import { getDocblock } from './docblock'; import getFlowType from './getFlowType'; +import getTSType from './getTSType'; import getParameterName from './getParameterName'; import getPropertyName from './getPropertyName'; import getTypeAnnotation from './getTypeAnnotation'; @@ -45,11 +46,16 @@ function getMethodParamsDoc(methodPath) { functionExpression.get('params').each(paramPath => { let type = null; const typePath = getTypeAnnotation(paramPath); - if (typePath) { + if (typePath && types.Flow.check(typePath.node)) { type = getFlowType(typePath); if (types.GenericTypeAnnotation.check(typePath.node)) { type.alias = typePath.node.id.name; } + } else if (typePath) { + type = getTSType(typePath); + if (types.TSTypeReference.check(typePath.node)) { + type.alias = typePath.node.typeName.name; + } } const param = { @@ -70,8 +76,10 @@ function getMethodReturnDoc(methodPath) { if (functionExpression.node.returnType) { const returnType = getTypeAnnotation(functionExpression.get('returnType')); - if (returnType) { + if (returnType && types.Flow.check(returnType.node)) { return { type: getFlowType(returnType) }; + } else if (returnType) { + return { type: getTSType(returnType) }; } } diff --git a/src/utils/getTSType.js b/src/utils/getTSType.js new file mode 100644 index 00000000000..9c41b4f1e8a --- /dev/null +++ b/src/utils/getTSType.js @@ -0,0 +1,388 @@ +/** + * 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 getTypeParameters, { + type TypeParameters, +} from '../utils/getTypeParameters'; +import type { + FlowTypeDescriptor, + FlowElementsType, + FlowFunctionSignatureType, + FlowFunctionArgumentType, + FlowObjectSignatureType, +} from '../types'; + +const { + types: { namedTypes: types }, +} = recast; + +const tsTypes = { + TSAnyKeyword: 'any', + TSBooleanKeyword: 'boolean', + TSUnknownKeyword: 'unknown', + TSNeverKeyword: 'never', + TSNullKeyword: 'null', + TSUndefinedKeyword: 'undefined', + TSNumberKeyword: 'number', + TSStringKeyword: 'string', + TSSymbolKeyword: 'symbol', + TSThisType: 'this', + TSObjectKeyword: 'object', + TSVoidKeyword: 'void', +}; + +const namedTypes = { + TSArrayType: handleTSArrayType, + TSTypeReference: handleTSTypeReference, + TSTypeLiteral: handleTSTypeLiteral, + TSInterfaceDeclaration: handleTSInterfaceDeclaration, + TSUnionType: handleTSUnionType, + TSFunctionType: handleTSFunctionType, + TSIntersectionType: handleTSIntersectionType, + TSTupleType: handleTSTupleType, + TSTypeQuery: handleTSTypeQuery, + TSTypeOperator: handleTSTypeOperator, +}; + +function handleTSArrayType( + path: NodePath, + typeParams: ?TypeParameters, +): FlowElementsType { + return { + name: 'Array', + elements: [getTSTypeWithResolvedTypes(path.get('elementType'), typeParams)], + raw: printValue(path), + }; +} + +function handleTSTypeReference( + path: NodePath, + typeParams: ?TypeParameters, +): ?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 }; + } + + const resolvedPath = + (typeParams && typeParams[type.name]) || + resolveToValue(path.get('typeName')); + + if (path.node.typeParameters && resolvedPath.node.typeParameters) { + typeParams = getTypeParameters( + resolvedPath.get('typeParameters'), + path.get('typeParameters'), + typeParams, + ); + } + + if (typeParams && typeParams[type.name]) { + type = getTSTypeWithResolvedTypes(resolvedPath, typeParams); + } + + if (resolvedPath && resolvedPath.node.typeAnnotation) { + type = getTSTypeWithResolvedTypes( + resolvedPath.get('typeAnnotation'), + typeParams, + ); + } else if (path.node.typeParameters) { + const params = path.get('typeParameters').get('params'); + + type = { + ...type, + elements: params.map(param => + getTSTypeWithResolvedTypes(param, typeParams), + ), + raw: printValue(path), + }; + } + + return type; +} + +function getTSTypeWithRequirements( + path: NodePath, + typeParams: ?TypeParameters, +): FlowTypeDescriptor { + const type = getTSTypeWithResolvedTypes(path, typeParams); + type.required = !path.parentPath.node.optional; + return type; +} + +function handleTSTypeLiteral( + path: NodePath, + typeParams: ?TypeParameters, +): 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'), + typeParams, + ), + }); + } else if (types.TSCallSignatureDeclaration.check(param.node)) { + type.signature.constructor = handleTSFunctionType(param, typeParams); + } else if (types.TSIndexSignature.check(param.node)) { + type.signature.properties.push({ + key: getTSTypeWithResolvedTypes( + param + .get('parameters') + .get(0) + .get('typeAnnotation'), + typeParams, + ), + value: getTSTypeWithRequirements( + param.get('typeAnnotation'), + typeParams, + ), + }); + } + }); + + return type; +} + +function handleTSInterfaceDeclaration(path: NodePath): FlowElementsType { + // Interfaces are handled like references which would be documented separately, + // rather than inlined like type aliases. + return { + name: path.node.id.name, + }; +} + +function handleTSUnionType( + path: NodePath, + typeParams: ?TypeParameters, +): FlowElementsType { + return { + name: 'union', + raw: printValue(path), + elements: path + .get('types') + .map(subType => getTSTypeWithResolvedTypes(subType, typeParams)), + }; +} + +function handleTSIntersectionType( + path: NodePath, + typeParams: ?TypeParameters, +): FlowElementsType { + return { + name: 'intersection', + raw: printValue(path), + elements: path + .get('types') + .map(subType => getTSTypeWithResolvedTypes(subType, typeParams)), + }; +} + +function handleTSFunctionType( + path: NodePath, + typeParams: ?TypeParameters, +): FlowFunctionSignatureType { + const type: FlowFunctionSignatureType = { + name: 'signature', + type: 'function', + raw: printValue(path), + signature: { + arguments: [], + return: getTSTypeWithResolvedTypes( + path.get('typeAnnotation'), + typeParams, + ), + }, + }; + + path.get('parameters').each(param => { + const typeAnnotation = getTypeAnnotation(param); + const arg: FlowFunctionArgumentType = { + name: param.node.name || '', + type: typeAnnotation + ? getTSTypeWithResolvedTypes(typeAnnotation, typeParams) + : null, + }; + + if (param.node.name === 'this') { + type.signature.this = arg.type; + return; + } + + if (param.node.type === 'RestElement') { + arg.name = param.node.argument.name; + arg.rest = true; + } + + type.signature.arguments.push(arg); + }); + + return type; +} + +function handleTSTupleType( + path: NodePath, + typeParams: ?TypeParameters, +): FlowElementsType { + const type: FlowElementsType = { + name: 'tuple', + raw: printValue(path), + elements: [], + }; + + path.get('elementTypes').each(param => { + type.elements.push(getTSTypeWithResolvedTypes(param, typeParams)); + }); + + return type; +} + +function handleTSTypeQuery( + path: NodePath, + typeParams: ?TypeParameters, +): FlowTypeDescriptor { + const resolvedPath = resolveToValue(path.get('exprName')); + if (resolvedPath && resolvedPath.node.typeAnnotation) { + return getTSTypeWithResolvedTypes( + resolvedPath.get('typeAnnotation'), + typeParams, + ); + } + + 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, + typeParams: ?TypeParameters, +): 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 (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, typeParams); + } + + 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 typescript type by inspecting the path for known + * typescript 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, + typeParamMap: ?TypeParameters, +): 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, typeParamMap); + 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/getTypeParameters.js b/src/utils/getTypeParameters.js new file mode 100644 index 00000000000..601df07c02e --- /dev/null +++ b/src/utils/getTypeParameters.js @@ -0,0 +1,48 @@ +/** + * 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 resolveGenericTypeAnnotation from '../utils/resolveGenericTypeAnnotation'; + +export type TypeParameters = { + [string]: NodePath, +}; + +export default function getTypeParameters( + declaration: NodePath, + instantiation: NodePath, + inputParams: ?TypeParameters, +): TypeParameters { + const params = {}; + const numInstantiationParams = instantiation.node.params.length; + + let i = 0; + declaration.get('params').each((paramPath: NodePath) => { + const key = paramPath.node.name; + const defaultTypePath = paramPath.node.default + ? paramPath.get('default') + : null; + const typePath = + i < numInstantiationParams + ? instantiation.get('params', i++) + : defaultTypePath; + + if (typePath) { + let resolvedTypePath = resolveGenericTypeAnnotation(typePath) || typePath; + const typeName = + resolvedTypePath.node.typeName || resolvedTypePath.node.id; + if (typeName && inputParams && inputParams[typeName.name]) { + resolvedTypePath = inputParams[typeName.name]; + } + + params[key] = resolvedTypePath; + } + }); + + return params; +} diff --git a/src/utils/resolveGenericTypeAnnotation.js b/src/utils/resolveGenericTypeAnnotation.js index 8bcfff6bfd3..9d7934c79fc 100644 --- a/src/utils/resolveGenericTypeAnnotation.js +++ b/src/utils/resolveGenericTypeAnnotation.js @@ -18,14 +18,29 @@ const { function tryResolveGenericTypeAnnotation(path: NodePath): ?NodePath { let typePath = unwrapUtilityType(path); + let idPath; - if (types.GenericTypeAnnotation.check(typePath.node)) { - typePath = resolveToValue(typePath.get('id')); + if (typePath.node.id) { + idPath = typePath.get('id'); + } else if (types.TSTypeReference.check(typePath.node)) { + idPath = typePath.get('typeName'); + } else if (types.TSExpressionWithTypeArguments.check(typePath.node)) { + idPath = typePath.get('expression'); + } + + if (idPath) { + typePath = resolveToValue(idPath); if (isUnreachableFlowType(typePath)) { return; } - return tryResolveGenericTypeAnnotation(typePath.get('right')); + if (types.TypeAlias.check(typePath.node)) { + return tryResolveGenericTypeAnnotation(typePath.get('right')); + } else if (types.TSTypeAliasDeclaration.check(typePath.node)) { + return tryResolveGenericTypeAnnotation(typePath.get('typeAnnotation')); + } + + return typePath; } 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..4dc3f290b93 100644 --- a/src/utils/resolveToValue.js +++ b/src/utils/resolveToValue.js @@ -51,7 +51,10 @@ 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.InterfaceDeclaration.check(parentPath.node) || + types.TSTypeAliasDeclaration.check(parentPath.node) || + types.TSInterfaceDeclaration.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'); } /** diff --git a/yarn.lock b/yarn.lock index 82f4d146f62..043c346c98b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -26,7 +26,7 @@ dependencies: "@babel/highlight" "^7.0.0" -"@babel/core@^7.0.0", "@babel/core@^7.1.0": +"@babel/core@^7.1.0": version "7.2.2" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.2.2.tgz#07adba6dde27bb5ad8d8672f15fde3e08184a687" integrity sha512-59vB0RWt09cAct5EIe58+NzGP4TFSD3Bz//2/ELy3ZeTeKF6VTD1AXlH8BGGbCX0PuobZBsIzO7IAI9PH67eKw== @@ -46,6 +46,26 @@ semver "^5.4.1" source-map "^0.5.0" +"@babel/core@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.4.4.tgz#84055750b05fcd50f9915a826b44fa347a825250" + integrity sha512-lQgGX3FPRgbz2SKmhMtYgJvVzGZrmjaF4apZ2bLwofAKiSjxU0drPh4S/VasyYXwaTs+A1gvQ45BN8SQJzHsQQ== + dependencies: + "@babel/code-frame" "^7.0.0" + "@babel/generator" "^7.4.4" + "@babel/helpers" "^7.4.4" + "@babel/parser" "^7.4.4" + "@babel/template" "^7.4.4" + "@babel/traverse" "^7.4.4" + "@babel/types" "^7.4.4" + convert-source-map "^1.1.0" + debug "^4.1.0" + json5 "^2.1.0" + lodash "^4.17.11" + resolve "^1.3.2" + semver "^5.4.1" + source-map "^0.5.0" + "@babel/generator@^7.0.0", "@babel/generator@^7.2.2": version "7.3.2" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.3.2.tgz#fff31a7b2f2f3dad23ef8e01be45b0d5c2fc0132" @@ -57,6 +77,17 @@ source-map "^0.5.0" trim-right "^1.0.1" +"@babel/generator@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.4.4.tgz#174a215eb843fc392c7edcaabeaa873de6e8f041" + integrity sha512-53UOLK6TVNqKxf7RUh8NE851EHRxOOeVXKbK2bivdb+iziMyk03Sr4eaE9OELCbyZAAafAKPDwF2TPUES5QbxQ== + dependencies: + "@babel/types" "^7.4.4" + jsesc "^2.5.1" + lodash "^4.17.11" + source-map "^0.5.0" + trim-right "^1.0.1" + "@babel/helper-annotate-as-pure@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0.tgz#323d39dd0b50e10c7c06ca7d7638e6864d8c5c32" @@ -202,6 +233,13 @@ dependencies: "@babel/types" "^7.0.0" +"@babel/helper-split-export-declaration@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz#ff94894a340be78f53f06af038b205c49d993677" + integrity sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q== + dependencies: + "@babel/types" "^7.4.4" + "@babel/helper-wrap-function@^7.1.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.2.0.tgz#c4e0012445769e2815b55296ead43a958549f6fa" @@ -221,6 +259,15 @@ "@babel/traverse" "^7.1.5" "@babel/types" "^7.3.0" +"@babel/helpers@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.4.4.tgz#868b0ef59c1dd4e78744562d5ce1b59c89f2f2a5" + integrity sha512-igczbR/0SeuPR8RFfC7tGrbdTbFL3QTvH6D+Z6zNxnTe//GyqmtHmDkzrqDmyZ3eSwPqB/LhyKoU5DXsp+Vp2A== + dependencies: + "@babel/template" "^7.4.4" + "@babel/traverse" "^7.4.4" + "@babel/types" "^7.4.4" + "@babel/highlight@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0.tgz#f710c38c8d458e6dd9a201afb637fcb781ce99e4" @@ -235,6 +282,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.3.2.tgz#95cdeddfc3992a6ca2a1315191c1679ca32c55cd" integrity sha512-QzNUC2RO1gadg+fs21fi0Uu0OuGNzRKEmgCxoLNzbCdoprLwjfmZwzUrpUNfJPaVRwBpDY47A17yYEGWyRelnQ== +"@babel/parser@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.4.4.tgz#5977129431b8fe33471730d255ce8654ae1250b6" + integrity sha512-5pCS4mOsL+ANsFZGdvNLybx4wtqAZJ0MJjMHxvzI3bvIsz6sQvzW8XX92EYIkiPtIvcfG3Aj+Ir5VNyjnZhP7w== + "@babel/plugin-proposal-async-generator-functions@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.2.0.tgz#b289b306669dce4ad20b0252889a15768c9d417e" @@ -625,6 +677,15 @@ "@babel/parser" "^7.2.2" "@babel/types" "^7.2.2" +"@babel/template@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.4.4.tgz#f4b88d1225689a08f5bc3a17483545be9e4ed237" + integrity sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw== + dependencies: + "@babel/code-frame" "^7.0.0" + "@babel/parser" "^7.4.4" + "@babel/types" "^7.4.4" + "@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.1.5", "@babel/traverse@^7.2.2", "@babel/traverse@^7.2.3": version "7.2.3" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.2.3.tgz#7ff50cefa9c7c0bd2d81231fdac122f3957748d8" @@ -640,6 +701,21 @@ globals "^11.1.0" lodash "^4.17.10" +"@babel/traverse@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.4.4.tgz#0776f038f6d78361860b6823887d4f3937133fe8" + integrity sha512-Gw6qqkw/e6AGzlyj9KnkabJX7VcubqPtkUQVAwkc0wUMldr3A/hezNB3Rc5eIvId95iSGkGIOe5hh1kMKf951A== + dependencies: + "@babel/code-frame" "^7.0.0" + "@babel/generator" "^7.4.4" + "@babel/helper-function-name" "^7.1.0" + "@babel/helper-split-export-declaration" "^7.4.4" + "@babel/parser" "^7.4.4" + "@babel/types" "^7.4.4" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.11" + "@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.2.2", "@babel/types@^7.3.0", "@babel/types@^7.3.2": version "7.3.2" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.3.2.tgz#424f5be4be633fff33fb83ab8d67e4a8290f5a2f" @@ -649,6 +725,15 @@ lodash "^4.17.10" to-fast-properties "^2.0.0" +"@babel/types@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.4.4.tgz#8db9e9a629bb7c29370009b4b779ed93fe57d5f0" + integrity sha512-dOllgYdnEFOebhkKCjzSVFqw/PmmB8pH6RGOWkY4GsboQNd47b1fBThBSwlHAq9alF9vc1M3+6oqR47R50L0tQ== + dependencies: + esutils "^2.0.2" + lodash "^4.17.11" + to-fast-properties "^2.0.0" + abab@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.0.tgz#aba0ab4c5eee2d4c79d3487d85450fb2376ebb0f" @@ -806,10 +891,10 @@ assign-symbols@^1.0.0: resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= -ast-types@0.12.2: - version "0.12.2" - resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.12.2.tgz#341656049ee328ac03fc805c156b49ebab1e4462" - integrity sha512-8c83xDLJM/dLDyXNLiR6afRRm4dPKN6KAnKqytRK3DBJul9lA+atxdQkNDkSVPdTqea5HiRq3lnnOIZ0MBpvdg== +ast-types@0.12.4: + version "0.12.4" + resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.12.4.tgz#71ce6383800f24efc9a1a3308f3a6e420a0974d1" + integrity sha512-ky/YVYCbtVAS8TdMIaTiPFHwEpRB5z1hctepJplTr3UW5q8TDrpIMCILyk8pmLxGtn2KCtC/lSn7zOsaI7nzDw== astral-regex@^1.0.0: version "1.0.0" @@ -3582,12 +3667,12 @@ realpath-native@^1.0.0, realpath-native@^1.0.2: dependencies: util.promisify "^1.0.0" -recast@^0.17.3: - version "0.17.3" - resolved "https://registry.yarnpkg.com/recast/-/recast-0.17.3.tgz#f49a9c9a64c59b55f6c93b5a53e3cffd7a13354d" - integrity sha512-NwQguXPwHqaVb6M7tsY11+8RDoAKHGRdymPGDxHJrsxOlNADQh0b08uz/MgYp1R1wmHuSBK4A4I5Oq+cE1J40g== +recast@^0.17.6: + version "0.17.6" + resolved "https://registry.yarnpkg.com/recast/-/recast-0.17.6.tgz#64ae98d0d2dfb10ff92ff5fb9ffb7371823b69fa" + integrity sha512-yoQRMRrK1lszNtbkGyM4kN45AwylV5hMiuEveUBlxytUViWevjvX6w+tzJt1LH4cfUhWt4NZvy3ThIhu6+m5wQ== dependencies: - ast-types "0.12.2" + ast-types "0.12.4" esprima "~4.0.0" private "^0.1.8" source-map "~0.6.1"