diff --git a/src/handlers/__tests__/__snapshots__/flowTypeHandler-test.js.snap b/src/handlers/__tests__/__snapshots__/flowTypeHandler-test.js.snap new file mode 100644 index 00000000000..38a6c0e16a0 --- /dev/null +++ b/src/handlers/__tests__/__snapshots__/flowTypeHandler-test.js.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`flowTypeHandler does support utility types inline 1`] = ` +Object { + "foo": Object { + "description": "", + "flowType": Object {}, + "required": true, + }, +} +`; diff --git a/src/handlers/__tests__/flowTypeHandler-test.js b/src/handlers/__tests__/flowTypeHandler-test.js index 08e6b348719..f7fa538ebc8 100644 --- a/src/handlers/__tests__/flowTypeHandler-test.js +++ b/src/handlers/__tests__/flowTypeHandler-test.js @@ -246,6 +246,20 @@ describe('flowTypeHandler', () => { }); }); + it('does support utility types inline', () => { + const definition = statement(` + (props: $ReadOnly) =>
; + + var React = require('React'); + + type Props = { foo: 'fooValue' }; + `).get('expression'); + + expect(() => flowTypeHandler(documentation, definition)).not.toThrow(); + + expect(documentation.descriptors).toMatchSnapshot(); + }); + it('does not support union proptypes', () => { const definition = statement(` (props: Props) =>
; diff --git a/src/handlers/flowTypeHandler.js b/src/handlers/flowTypeHandler.js index d935e206503..cd73311c1bf 100644 --- a/src/handlers/flowTypeHandler.js +++ b/src/handlers/flowTypeHandler.js @@ -17,20 +17,14 @@ import getFlowTypeFromReactComponent, { } from '../utils/getFlowTypeFromReactComponent'; import resolveToValue from '../utils/resolveToValue'; import setPropDescription from '../utils/setPropDescription'; -import { - isSupportedUtilityType, - unwrapUtilityType, -} from '../utils/flowUtilityTypes'; +import { unwrapUtilityType } from '../utils/flowUtilityTypes'; const { types: { namedTypes: types }, } = recast; function setPropDescriptor(documentation: Documentation, path: NodePath): void { if (types.ObjectTypeSpreadProperty.check(path.node)) { - let argument = path.get('argument'); - while (isSupportedUtilityType(argument)) { - argument = unwrapUtilityType(argument); - } + const argument = unwrapUtilityType(path.get('argument')); if (types.ObjectTypeAnnotation.check(argument.node)) { applyToFlowTypeProperties(argument, propertyPath => { diff --git a/src/utils/__tests__/flowUtilityTypes-test.js b/src/utils/__tests__/flowUtilityTypes-test.js new file mode 100644 index 00000000000..8075c21cb63 --- /dev/null +++ b/src/utils/__tests__/flowUtilityTypes-test.js @@ -0,0 +1,84 @@ +/** + * 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 describe, it, expect*/ + +import { unwrapUtilityType, isSupportedUtilityType } from '../flowUtilityTypes'; + +import { statement } from '../../../tests/utils'; + +describe('flowTypeUtilities', () => { + describe('unwrapUtilityType', () => { + it('correctly unwraps', () => { + const def = statement(` + type A = $ReadOnly<{ foo: string }> + `); + + expect(unwrapUtilityType(def.get('right'))).toBe( + def.get('right', 'typeParameters', 'params', 0), + ); + }); + + it('correctly unwraps double', () => { + const def = statement(` + type A = $ReadOnly<$ReadOnly<{ foo: string }>> + `); + + expect(unwrapUtilityType(def.get('right'))).toBe( + def.get( + 'right', + 'typeParameters', + 'params', + 0, + 'typeParameters', + 'params', + 0, + ), + ); + }); + + it('correctly unwraps triple', () => { + const def = statement(` + type A = $ReadOnly<$ReadOnly<$ReadOnly<{ foo: string }>>> + `); + + expect(unwrapUtilityType(def.get('right'))).toBe( + def.get( + 'right', + 'typeParameters', + 'params', + 0, + 'typeParameters', + 'params', + 0, + 'typeParameters', + 'params', + 0, + ), + ); + }); + }); + + describe('isSupportedUtilityType', () => { + it('correctly returns true for valid type', () => { + const def = statement(` + type A = $Exact<{ foo: string }> + `); + + expect(isSupportedUtilityType(def.get('right'))).toBe(true); + }); + + it('correctly returns false for invalid type', () => { + const def = statement(` + type A = $Homer<{ foo: string }> + `); + + expect(isSupportedUtilityType(def.get('right'))).toBe(false); + }); + }); +}); diff --git a/src/utils/flowUtilityTypes.js b/src/utils/flowUtilityTypes.js index f2d14b5ac5a..8934463586c 100644 --- a/src/utils/flowUtilityTypes.js +++ b/src/utils/flowUtilityTypes.js @@ -21,7 +21,7 @@ const supportedUtilityTypes = new Set(['$Exact', '$ReadOnly']); export function isSupportedUtilityType(path: NodePath): boolean { if (types.GenericTypeAnnotation.check(path.node)) { const idPath = path.get('id'); - return Boolean(idPath) && supportedUtilityTypes.has(idPath.node.name); + return !!idPath && supportedUtilityTypes.has(idPath.node.name); } return false; } @@ -32,5 +32,9 @@ export function isSupportedUtilityType(path: NodePath): boolean { * $ReadOnly => T */ export function unwrapUtilityType(path: NodePath): NodePath { - return path.get('typeParameters', 'params', 0); + while (isSupportedUtilityType(path)) { + path = path.get('typeParameters', 'params', 0); + } + + return path; } diff --git a/src/utils/getFlowTypeFromReactComponent.js b/src/utils/getFlowTypeFromReactComponent.js index 55731fca742..04579f3d787 100644 --- a/src/utils/getFlowTypeFromReactComponent.js +++ b/src/utils/getFlowTypeFromReactComponent.js @@ -11,19 +11,8 @@ import getTypeAnnotation from '../utils/getTypeAnnotation'; import getMemberValuePath from '../utils/getMemberValuePath'; import isReactComponentClass from '../utils/isReactComponentClass'; import isStatelessComponent from '../utils/isStatelessComponent'; -import isUnreachableFlowType from '../utils/isUnreachableFlowType'; -import recast from 'recast'; -import resolveToValue from '../utils/resolveToValue'; -import { - isSupportedUtilityType, - unwrapUtilityType, -} from '../utils/flowUtilityTypes'; import resolveGenericTypeAnnotation from '../utils/resolveGenericTypeAnnotation'; -const { - types: { namedTypes: types }, -} = recast; - /** * Given an React component (stateless or class) tries to find the * flow type for the props. If not found or not one of the supported @@ -56,21 +45,6 @@ export default (path: NodePath): ?NodePath => { typePath = getTypeAnnotation(param); } - if (typePath && isSupportedUtilityType(typePath)) { - typePath = unwrapUtilityType(typePath); - } else if (typePath && types.GenericTypeAnnotation.check(typePath.node)) { - typePath = resolveToValue(typePath.get('id')); - if ( - !typePath || - types.Identifier.check(typePath.node) || - isUnreachableFlowType(typePath) - ) { - return; - } - - typePath = typePath.get('right'); - } - return typePath; }; diff --git a/src/utils/resolveGenericTypeAnnotation.js b/src/utils/resolveGenericTypeAnnotation.js index 256cc581d8b..8bcfff6bfd3 100644 --- a/src/utils/resolveGenericTypeAnnotation.js +++ b/src/utils/resolveGenericTypeAnnotation.js @@ -10,32 +10,40 @@ import isUnreachableFlowType from '../utils/isUnreachableFlowType'; import recast from 'recast'; import resolveToValue from '../utils/resolveToValue'; -import { isSupportedUtilityType, unwrapUtilityType } from './flowUtilityTypes'; +import { unwrapUtilityType } from './flowUtilityTypes'; const { types: { namedTypes: types }, } = recast; +function tryResolveGenericTypeAnnotation(path: NodePath): ?NodePath { + let typePath = unwrapUtilityType(path); + + if (types.GenericTypeAnnotation.check(typePath.node)) { + typePath = resolveToValue(typePath.get('id')); + if (isUnreachableFlowType(typePath)) { + return; + } + + return tryResolveGenericTypeAnnotation(typePath.get('right')); + } + + return typePath; +} + /** * Given an React component (stateless or class) tries to find the * flow type for the props. If not found or not one of the supported - * component types returns null. + * component types returns undefined. */ export default function resolveGenericTypeAnnotation( path: NodePath, ): ?NodePath { - // If the node doesn't have types or properties, try to get the type. - let typePath: ?NodePath; - if (path && isSupportedUtilityType(path)) { - typePath = unwrapUtilityType(path); - } else if (path && types.GenericTypeAnnotation.check(path.node)) { - typePath = resolveToValue(path.get('id')); - if (isUnreachableFlowType(typePath)) { - return; - } + if (!path) return; - typePath = typePath.get('right'); - } + const typePath = tryResolveGenericTypeAnnotation(path); + + if (!typePath || typePath === path) return; return typePath; }