diff --git a/src/__tests__/__snapshots__/main-test.js.snap b/src/__tests__/__snapshots__/main-test.js.snap index 40a39071292..35b8932cedb 100644 --- a/src/__tests__/__snapshots__/main-test.js.snap +++ b/src/__tests__/__snapshots__/main-test.js.snap @@ -933,3 +933,33 @@ Object { }, } `; + +exports[`main fixtures processes component "component_18.js" without errors 1`] = ` +Object { + "description": "", + "displayName": "UncoloredView", + "methods": Array [], + "props": Object { + "color": Object { + "description": "", + "required": false, + "type": Object { + "name": "custom", + "raw": "PropTypes.string.isRequired", + }, + }, + "id": Object { + "defaultValue": Object { + "computed": false, + "value": "'test-forward-ref-default'", + }, + "description": "", + "required": false, + "type": Object { + "name": "custom", + "raw": "PropTypes.string", + }, + }, + }, +} +`; diff --git a/src/__tests__/fixtures/component_18.js b/src/__tests__/fixtures/component_18.js new file mode 100644 index 00000000000..7ab76472e47 --- /dev/null +++ b/src/__tests__/fixtures/component_18.js @@ -0,0 +1,22 @@ + +import React from 'react'; +import extendStyles from 'enhancers/extendStyles'; + +type Props = $ReadOnly<{| + color?: ?string, +|}>; + +const ColoredView = React.forwardRef((props: Props, ref) => ( +
+)); + +ColoredView.displayName = 'UncoloredView'; +ColoredView.propTypes = { + color: PropTypes.string.isRequired, + id: PropTypes.string +} +ColoredView.defaultProps = { + id: 'test-forward-ref-default' +} + +module.exports = extendStyles(ColoredView); \ No newline at end of file diff --git a/src/resolver/findExportedComponentDefinition.js b/src/resolver/findExportedComponentDefinition.js index f72a210794f..c48594f4f6e 100644 --- a/src/resolver/findExportedComponentDefinition.js +++ b/src/resolver/findExportedComponentDefinition.js @@ -10,6 +10,7 @@ */ import isExportsOrModuleAssignment from '../utils/isExportsOrModuleAssignment'; +import isReactForwardRefCall from '../utils/isReactForwardRefCall'; import isReactComponentClass from '../utils/isReactComponentClass'; import isReactCreateClassCall from '../utils/isReactCreateClassCall'; import isStatelessComponent from '../utils/isStatelessComponent'; @@ -29,7 +30,8 @@ function isComponentDefinition(path) { return ( isReactCreateClassCall(path) || isReactComponentClass(path) || - isStatelessComponent(path) + isStatelessComponent(path) || + isReactForwardRefCall(path) ); } @@ -45,6 +47,8 @@ function resolveDefinition(definition, types) { return definition; } else if (isStatelessComponent(definition)) { return definition; + } else if (isReactForwardRefCall(definition)) { + return definition; } return null; } diff --git a/src/utils/getMemberExpressionValuePath.js b/src/utils/getMemberExpressionValuePath.js index 83470d290a9..437967da241 100644 --- a/src/utils/getMemberExpressionValuePath.js +++ b/src/utils/getMemberExpressionValuePath.js @@ -12,6 +12,7 @@ import getNameOrValue from './getNameOrValue'; import { String as toString } from './expressionTo'; +import isReactForwardRefCall from './isReactForwardRefCall'; import recast from 'recast'; const { @@ -40,7 +41,8 @@ function resolveName(path) { if ( types.FunctionExpression.check(path.node) || types.ArrowFunctionExpression.check(path.node) || - types.TaggedTemplateExpression.check(path.node) + types.TaggedTemplateExpression.check(path.node) || + isReactForwardRefCall(path) ) { let currentPath = path; while (currentPath.parent) { @@ -56,7 +58,7 @@ function resolveName(path) { throw new TypeError( 'Attempted to resolveName for an unsupported path. resolveName accepts a ' + - 'VariableDeclaration, FunctionDeclaration, or FunctionExpression. Got "' + + 'VariableDeclaration, FunctionDeclaration, FunctionExpression or CallExpression. Got "' + path.node.type + '".', ); diff --git a/src/utils/getMemberValuePath.js b/src/utils/getMemberValuePath.js index c783ecbd105..e276b33af6e 100644 --- a/src/utils/getMemberValuePath.js +++ b/src/utils/getMemberValuePath.js @@ -34,6 +34,7 @@ const POSTPROCESS_MEMBERS = { const LOOKUP_METHOD = { [types.ArrowFunctionExpression.name]: getMemberExpressionValuePath, + [types.CallExpression.name]: getMemberExpressionValuePath, [types.FunctionExpression.name]: getMemberExpressionValuePath, [types.FunctionDeclaration.name]: getMemberExpressionValuePath, [types.VariableDeclaration.name]: getMemberExpressionValuePath, @@ -62,7 +63,8 @@ function isSupportedDefinitionType({ node }) { types.VariableDeclaration.check(node) || types.ArrowFunctionExpression.check(node) || types.FunctionDeclaration.check(node) || - types.FunctionExpression.check(node) + types.FunctionExpression.check(node) || + types.CallExpression.check(node) ); } @@ -88,7 +90,7 @@ export default function getMemberValuePath( 'Got unsupported definition type. Definition must be one of ' + 'ObjectExpression, ClassDeclaration, ClassExpression,' + 'VariableDeclaration, ArrowFunctionExpression, FunctionExpression, ' + - 'TaggedTemplateExpression or FunctionDeclaration. Got "' + + 'TaggedTemplateExpression, FunctionDeclaration or CallExpression. Got "' + componentDefinition.node.type + '"' + 'instead.', diff --git a/src/utils/isReactForwardRefCall.js b/src/utils/isReactForwardRefCall.js new file mode 100644 index 00000000000..d2d35b761a9 --- /dev/null +++ b/src/utils/isReactForwardRefCall.js @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @flow + */ +import isReactModuleName from './isReactModuleName'; +import match from './match'; +import recast from 'recast'; +import resolveToModule from './resolveToModule'; + +const { + types: { namedTypes: types }, +} = recast; + +/** + * Returns true if the expression is a function call of the form + * `React.forwardRef(...)`. + */ +export default function isReactForwardRefCall(path: NodePath): boolean { + if (types.ExpressionStatement.check(path.node)) { + path = path.get('expression'); + } + + if (!match(path.node, { callee: { property: { name: 'forwardRef' } } })) { + return false; + } + const module = resolveToModule(path.get('callee', 'object')); + return Boolean(module && isReactModuleName(module)); +}