Skip to content

Commit

Permalink
feat(methods): support static methods on function components
Browse files Browse the repository at this point in the history
  • Loading branch information
motiz88 authored and danez committed Feb 13, 2020
1 parent d71c3d2 commit 72a2344
Show file tree
Hide file tree
Showing 4 changed files with 235 additions and 15 deletions.
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
57 changes: 49 additions & 8 deletions src/handlers/__tests__/componentMethodsHandler-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
});
51 changes: 51 additions & 0 deletions src/handlers/componentMethodsHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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.
Expand All @@ -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(
Expand Down
73 changes: 66 additions & 7 deletions src/utils/getMethodDocumentation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 => {
Expand Down Expand Up @@ -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'));
Expand All @@ -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) {
Expand All @@ -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),
Expand Down

0 comments on commit 72a2344

Please sign in to comment.