diff --git a/src/handlers/__tests__/__snapshots__/componentMethodsHandler-test.js.snap b/src/handlers/__tests__/__snapshots__/componentMethodsHandler-test.js.snap index f31d43fa66f..1a7e29fd661 100644 --- a/src/handlers/__tests__/__snapshots__/componentMethodsHandler-test.js.snap +++ b/src/handlers/__tests__/__snapshots__/componentMethodsHandler-test.js.snap @@ -1,5 +1,74 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`componentMethodsHandler function components finds static methods on a component in a variable declaration 1`] = ` +Array [ + Object { + "docblock": null, + "modifiers": Array [ + "static", + ], + "name": "doFoo", + "params": Array [], + "returns": null, + }, + Object { + "docblock": null, + "modifiers": Array [ + "static", + ], + "name": "doBar", + "params": Array [], + "returns": null, + }, +] +`; + +exports[`componentMethodsHandler function components finds static methods on a component in an assignment 1`] = ` +Array [ + Object { + "docblock": null, + "modifiers": Array [ + "static", + ], + "name": "doFoo", + "params": Array [], + "returns": null, + }, + Object { + "docblock": null, + "modifiers": Array [ + "static", + ], + "name": "doBar", + "params": Array [], + "returns": null, + }, +] +`; + +exports[`componentMethodsHandler function components finds static methods on a function declaration 1`] = ` +Array [ + Object { + "docblock": null, + "modifiers": Array [ + "static", + ], + "name": "doFoo", + "params": Array [], + "returns": null, + }, + Object { + "docblock": null, + "modifiers": Array [ + "static", + ], + "name": "doBar", + "params": Array [], + "returns": null, + }, +] +`; + exports[`componentMethodsHandler should handle and ignore computed methods 1`] = ` Array [ Object { diff --git a/src/handlers/__tests__/componentMethodsHandler-test.js b/src/handlers/__tests__/componentMethodsHandler-test.js index 3e913aa3863..da27ef77ab0 100644 --- a/src/handlers/__tests__/componentMethodsHandler-test.js +++ b/src/handlers/__tests__/componentMethodsHandler-test.js @@ -156,13 +156,54 @@ describe('componentMethodsHandler', () => { expect(documentation.methods).toMatchSnapshot(); }); - it('should not find methods for stateless components', () => { - const src = ` - (props) => {} - `; - - const definition = parse(src).get('body', 0, 'expression'); - componentMethodsHandler(documentation, definition); - expect(documentation.methods).toEqual([]); + describe('function components', () => { + it('no methods', () => { + const src = ` + (props) => {} + `; + + const definition = parse(src).get('body', 0, 'expression'); + componentMethodsHandler(documentation, definition); + expect(documentation.methods).toEqual([]); + }); + + it('finds static methods on a component in a variable declaration', () => { + const src = ` + const Test = (props) => {}; + Test.doFoo = () => {}; + Test.doBar = () => {}; + Test.displayName = 'Test'; // Not a method + `; + + const definition = parse(src).get('body', 0, 'declarations', 0, 'init'); + componentMethodsHandler(documentation, definition); + expect(documentation.methods).toMatchSnapshot(); + }); + + it('finds static methods on a component in an assignment', () => { + const src = ` + Test = (props) => {}; + Test.doFoo = () => {}; + Test.doBar = () => {}; + Test.displayName = 'Test'; // Not a method + `; + + const definition = parse(src).get('body', 0, 'expression', 'right'); + componentMethodsHandler(documentation, definition); + expect(documentation.methods).toMatchSnapshot(); + }); + + it('finds static methods on a function declaration', () => { + const src = ` + function Test(props) {} + Test.doFoo = () => {}; + Test.doBar = () => {}; + Test.displayName = 'Test'; // Not a method + `; + + const definition = parse(src).get('body', 0); + componentMethodsHandler(documentation, definition); + expect(documentation.methods).toMatchSnapshot(); + }); }); }); diff --git a/src/handlers/componentMethodsHandler.js b/src/handlers/componentMethodsHandler.js index d711e0722e8..31f3a11b4bb 100644 --- a/src/handlers/componentMethodsHandler.js +++ b/src/handlers/componentMethodsHandler.js @@ -13,6 +13,9 @@ import getMethodDocumentation from '../utils/getMethodDocumentation'; import isReactComponentClass from '../utils/isReactComponentClass'; import isReactComponentMethod from '../utils/isReactComponentMethod'; import type Documentation from '../Documentation'; +import match from '../utils/match'; +import { traverseShallow } from '../utils/traverse'; +import resolveToValue from '../utils/resolveToValue'; /** * The following values/constructs are considered methods: @@ -31,6 +34,37 @@ function isMethod(path) { return isProbablyMethod && !isReactComponentMethod(path); } +function findAssignedMethods(scope, idPath) { + const results = []; + + if (!t.Identifier.check(idPath.node)) { + return results; + } + + const name = idPath.node.name; + const idScope = idPath.scope.lookup(idPath.node.name); + + traverseShallow((scope: any).path, { + visitAssignmentExpression: function(path) { + const node = path.node; + if ( + match(node.left, { + type: 'MemberExpression', + object: { type: 'Identifier', name }, + }) && + path.scope.lookup(name) === idScope && + t.Function.check(resolveToValue(path.get('right')).node) + ) { + results.push(path); + return false; + } + return this.traverse(path); + }, + }); + + return results; +} + /** * Extract all flow types for the methods of a react component. Doesn't * return any react specific lifecycle methods. @@ -56,6 +90,23 @@ export default function componentMethodsHandler( } }); } + } else if ( + t.VariableDeclarator.check(path.parent.node) && + path.parent.node.init === path.node && + t.Identifier.check(path.parent.node.id) + ) { + methodPaths = findAssignedMethods(path.parent.scope, path.parent.get('id')); + } else if ( + t.AssignmentExpression.check(path.parent.node) && + path.parent.node.right === path.node && + t.Identifier.check(path.parent.node.left) + ) { + methodPaths = findAssignedMethods( + path.parent.scope, + path.parent.get('left'), + ); + } else if (t.FunctionDeclaration.check(path.node)) { + methodPaths = findAssignedMethods(path.parent.scope, path.get('id')); } documentation.set( diff --git a/src/utils/getMethodDocumentation.js b/src/utils/getMethodDocumentation.js index c8982e75fb0..517003477cc 100644 --- a/src/utils/getMethodDocumentation.js +++ b/src/utils/getMethodDocumentation.js @@ -15,6 +15,7 @@ import getParameterName from './getParameterName'; import getPropertyName from './getPropertyName'; import getTypeAnnotation from './getTypeAnnotation'; import type { FlowTypeDescriptor } from '../types'; +import resolveToValue from './resolveToValue'; type MethodParameter = { name: string, @@ -34,9 +35,17 @@ type MethodDocumentation = { returns: ?MethodReturn, }; +function getMethodFunctionExpression(methodPath) { + if (t.AssignmentExpression.check(methodPath.node)) { + return resolveToValue(methodPath.get('right')); + } + // Otherwise this is a method/property node + return methodPath.get('value'); +} + function getMethodParamsDoc(methodPath) { const params = []; - const functionExpression = methodPath.get('value'); + const functionExpression = getMethodFunctionExpression(methodPath); // Extract param flow types. functionExpression.get('params').each(paramPath => { @@ -68,7 +77,7 @@ function getMethodParamsDoc(methodPath) { // Extract flow return type. function getMethodReturnDoc(methodPath) { - const functionExpression = methodPath.get('value'); + const functionExpression = getMethodFunctionExpression(methodPath); if (functionExpression.node.returnType) { const returnType = getTypeAnnotation(functionExpression.get('returnType')); @@ -83,6 +92,12 @@ function getMethodReturnDoc(methodPath) { } function getMethodModifiers(methodPath) { + if (t.AssignmentExpression.check(methodPath.node)) { + return ['static']; + } + + // Otherwise this is a method/property node + const modifiers = []; if (methodPath.node.static) { @@ -104,21 +119,65 @@ function getMethodModifiers(methodPath) { return modifiers; } +function getMethodName(methodPath) { + if ( + t.AssignmentExpression.check(methodPath.node) && + t.MemberExpression.check(methodPath.node.left) + ) { + const left = methodPath.node.left; + const property = left.property; + if (!left.computed) { + return property.name; + } + if (t.Literal.check(property)) { + return String(property.value); + } + return null; + } + return getPropertyName(methodPath); +} + +function getMethodAccessibility(methodPath) { + if (t.AssignmentExpression.check(methodPath.node)) { + return null; + } + + // Otherwise this is a method/property node + return methodPath.node.accessibility; +} + +function getMethodDocblock(methodPath) { + if (t.AssignmentExpression.check(methodPath.node)) { + let path = methodPath; + do { + path = path.parent; + } while (path && !t.ExpressionStatement.check(path.node)); + if (path) { + return getDocblock(path); + } + return null; + } + + // Otherwise this is a method/property node + return getDocblock(methodPath); +} + +// Gets the documentation object for a component method. +// Component methods may be represented as class/object method/property nodes +// or as assignment expresions of the form `Component.foo = function() {}` export default function getMethodDocumentation( methodPath: NodePath, ): ?MethodDocumentation { - if (methodPath.node.accessibility === 'private') { + if (getMethodAccessibility(methodPath) === 'private') { return null; } - const name = getPropertyName(methodPath); + const name = getMethodName(methodPath); if (!name) return null; - const docblock = getDocblock(methodPath); - return { name, - docblock, + docblock: getMethodDocblock(methodPath), modifiers: getMethodModifiers(methodPath), params: getMethodParamsDoc(methodPath), returns: getMethodReturnDoc(methodPath),