Skip to content

Commit 72a2344

Browse files
motiz88danez
authored andcommitted
feat(methods): support static methods on function components
1 parent d71c3d2 commit 72a2344

File tree

4 files changed

+235
-15
lines changed

4 files changed

+235
-15
lines changed

src/handlers/__tests__/__snapshots__/componentMethodsHandler-test.js.snap

+69
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,74 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3+
exports[`componentMethodsHandler function components finds static methods on a component in a variable declaration 1`] = `
4+
Array [
5+
Object {
6+
"docblock": null,
7+
"modifiers": Array [
8+
"static",
9+
],
10+
"name": "doFoo",
11+
"params": Array [],
12+
"returns": null,
13+
},
14+
Object {
15+
"docblock": null,
16+
"modifiers": Array [
17+
"static",
18+
],
19+
"name": "doBar",
20+
"params": Array [],
21+
"returns": null,
22+
},
23+
]
24+
`;
25+
26+
exports[`componentMethodsHandler function components finds static methods on a component in an assignment 1`] = `
27+
Array [
28+
Object {
29+
"docblock": null,
30+
"modifiers": Array [
31+
"static",
32+
],
33+
"name": "doFoo",
34+
"params": Array [],
35+
"returns": null,
36+
},
37+
Object {
38+
"docblock": null,
39+
"modifiers": Array [
40+
"static",
41+
],
42+
"name": "doBar",
43+
"params": Array [],
44+
"returns": null,
45+
},
46+
]
47+
`;
48+
49+
exports[`componentMethodsHandler function components finds static methods on a function declaration 1`] = `
50+
Array [
51+
Object {
52+
"docblock": null,
53+
"modifiers": Array [
54+
"static",
55+
],
56+
"name": "doFoo",
57+
"params": Array [],
58+
"returns": null,
59+
},
60+
Object {
61+
"docblock": null,
62+
"modifiers": Array [
63+
"static",
64+
],
65+
"name": "doBar",
66+
"params": Array [],
67+
"returns": null,
68+
},
69+
]
70+
`;
71+
372
exports[`componentMethodsHandler should handle and ignore computed methods 1`] = `
473
Array [
574
Object {

src/handlers/__tests__/componentMethodsHandler-test.js

+49-8
Original file line numberDiff line numberDiff line change
@@ -156,13 +156,54 @@ describe('componentMethodsHandler', () => {
156156
expect(documentation.methods).toMatchSnapshot();
157157
});
158158

159-
it('should not find methods for stateless components', () => {
160-
const src = `
161-
(props) => {}
162-
`;
163-
164-
const definition = parse(src).get('body', 0, 'expression');
165-
componentMethodsHandler(documentation, definition);
166-
expect(documentation.methods).toEqual([]);
159+
describe('function components', () => {
160+
it('no methods', () => {
161+
const src = `
162+
(props) => {}
163+
`;
164+
165+
const definition = parse(src).get('body', 0, 'expression');
166+
componentMethodsHandler(documentation, definition);
167+
expect(documentation.methods).toEqual([]);
168+
});
169+
170+
it('finds static methods on a component in a variable declaration', () => {
171+
const src = `
172+
const Test = (props) => {};
173+
Test.doFoo = () => {};
174+
Test.doBar = () => {};
175+
Test.displayName = 'Test'; // Not a method
176+
`;
177+
178+
const definition = parse(src).get('body', 0, 'declarations', 0, 'init');
179+
componentMethodsHandler(documentation, definition);
180+
expect(documentation.methods).toMatchSnapshot();
181+
});
182+
183+
it('finds static methods on a component in an assignment', () => {
184+
const src = `
185+
Test = (props) => {};
186+
Test.doFoo = () => {};
187+
Test.doBar = () => {};
188+
Test.displayName = 'Test'; // Not a method
189+
`;
190+
191+
const definition = parse(src).get('body', 0, 'expression', 'right');
192+
componentMethodsHandler(documentation, definition);
193+
expect(documentation.methods).toMatchSnapshot();
194+
});
195+
196+
it('finds static methods on a function declaration', () => {
197+
const src = `
198+
function Test(props) {}
199+
Test.doFoo = () => {};
200+
Test.doBar = () => {};
201+
Test.displayName = 'Test'; // Not a method
202+
`;
203+
204+
const definition = parse(src).get('body', 0);
205+
componentMethodsHandler(documentation, definition);
206+
expect(documentation.methods).toMatchSnapshot();
207+
});
167208
});
168209
});

src/handlers/componentMethodsHandler.js

+51
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import getMethodDocumentation from '../utils/getMethodDocumentation';
1313
import isReactComponentClass from '../utils/isReactComponentClass';
1414
import isReactComponentMethod from '../utils/isReactComponentMethod';
1515
import type Documentation from '../Documentation';
16+
import match from '../utils/match';
17+
import { traverseShallow } from '../utils/traverse';
18+
import resolveToValue from '../utils/resolveToValue';
1619

1720
/**
1821
* The following values/constructs are considered methods:
@@ -31,6 +34,37 @@ function isMethod(path) {
3134
return isProbablyMethod && !isReactComponentMethod(path);
3235
}
3336

37+
function findAssignedMethods(scope, idPath) {
38+
const results = [];
39+
40+
if (!t.Identifier.check(idPath.node)) {
41+
return results;
42+
}
43+
44+
const name = idPath.node.name;
45+
const idScope = idPath.scope.lookup(idPath.node.name);
46+
47+
traverseShallow((scope: any).path, {
48+
visitAssignmentExpression: function(path) {
49+
const node = path.node;
50+
if (
51+
match(node.left, {
52+
type: 'MemberExpression',
53+
object: { type: 'Identifier', name },
54+
}) &&
55+
path.scope.lookup(name) === idScope &&
56+
t.Function.check(resolveToValue(path.get('right')).node)
57+
) {
58+
results.push(path);
59+
return false;
60+
}
61+
return this.traverse(path);
62+
},
63+
});
64+
65+
return results;
66+
}
67+
3468
/**
3569
* Extract all flow types for the methods of a react component. Doesn't
3670
* return any react specific lifecycle methods.
@@ -56,6 +90,23 @@ export default function componentMethodsHandler(
5690
}
5791
});
5892
}
93+
} else if (
94+
t.VariableDeclarator.check(path.parent.node) &&
95+
path.parent.node.init === path.node &&
96+
t.Identifier.check(path.parent.node.id)
97+
) {
98+
methodPaths = findAssignedMethods(path.parent.scope, path.parent.get('id'));
99+
} else if (
100+
t.AssignmentExpression.check(path.parent.node) &&
101+
path.parent.node.right === path.node &&
102+
t.Identifier.check(path.parent.node.left)
103+
) {
104+
methodPaths = findAssignedMethods(
105+
path.parent.scope,
106+
path.parent.get('left'),
107+
);
108+
} else if (t.FunctionDeclaration.check(path.node)) {
109+
methodPaths = findAssignedMethods(path.parent.scope, path.get('id'));
59110
}
60111

61112
documentation.set(

src/utils/getMethodDocumentation.js

+66-7
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import getParameterName from './getParameterName';
1515
import getPropertyName from './getPropertyName';
1616
import getTypeAnnotation from './getTypeAnnotation';
1717
import type { FlowTypeDescriptor } from '../types';
18+
import resolveToValue from './resolveToValue';
1819

1920
type MethodParameter = {
2021
name: string,
@@ -34,9 +35,17 @@ type MethodDocumentation = {
3435
returns: ?MethodReturn,
3536
};
3637

38+
function getMethodFunctionExpression(methodPath) {
39+
if (t.AssignmentExpression.check(methodPath.node)) {
40+
return resolveToValue(methodPath.get('right'));
41+
}
42+
// Otherwise this is a method/property node
43+
return methodPath.get('value');
44+
}
45+
3746
function getMethodParamsDoc(methodPath) {
3847
const params = [];
39-
const functionExpression = methodPath.get('value');
48+
const functionExpression = getMethodFunctionExpression(methodPath);
4049

4150
// Extract param flow types.
4251
functionExpression.get('params').each(paramPath => {
@@ -68,7 +77,7 @@ function getMethodParamsDoc(methodPath) {
6877

6978
// Extract flow return type.
7079
function getMethodReturnDoc(methodPath) {
71-
const functionExpression = methodPath.get('value');
80+
const functionExpression = getMethodFunctionExpression(methodPath);
7281

7382
if (functionExpression.node.returnType) {
7483
const returnType = getTypeAnnotation(functionExpression.get('returnType'));
@@ -83,6 +92,12 @@ function getMethodReturnDoc(methodPath) {
8392
}
8493

8594
function getMethodModifiers(methodPath) {
95+
if (t.AssignmentExpression.check(methodPath.node)) {
96+
return ['static'];
97+
}
98+
99+
// Otherwise this is a method/property node
100+
86101
const modifiers = [];
87102

88103
if (methodPath.node.static) {
@@ -104,21 +119,65 @@ function getMethodModifiers(methodPath) {
104119
return modifiers;
105120
}
106121

122+
function getMethodName(methodPath) {
123+
if (
124+
t.AssignmentExpression.check(methodPath.node) &&
125+
t.MemberExpression.check(methodPath.node.left)
126+
) {
127+
const left = methodPath.node.left;
128+
const property = left.property;
129+
if (!left.computed) {
130+
return property.name;
131+
}
132+
if (t.Literal.check(property)) {
133+
return String(property.value);
134+
}
135+
return null;
136+
}
137+
return getPropertyName(methodPath);
138+
}
139+
140+
function getMethodAccessibility(methodPath) {
141+
if (t.AssignmentExpression.check(methodPath.node)) {
142+
return null;
143+
}
144+
145+
// Otherwise this is a method/property node
146+
return methodPath.node.accessibility;
147+
}
148+
149+
function getMethodDocblock(methodPath) {
150+
if (t.AssignmentExpression.check(methodPath.node)) {
151+
let path = methodPath;
152+
do {
153+
path = path.parent;
154+
} while (path && !t.ExpressionStatement.check(path.node));
155+
if (path) {
156+
return getDocblock(path);
157+
}
158+
return null;
159+
}
160+
161+
// Otherwise this is a method/property node
162+
return getDocblock(methodPath);
163+
}
164+
165+
// Gets the documentation object for a component method.
166+
// Component methods may be represented as class/object method/property nodes
167+
// or as assignment expresions of the form `Component.foo = function() {}`
107168
export default function getMethodDocumentation(
108169
methodPath: NodePath,
109170
): ?MethodDocumentation {
110-
if (methodPath.node.accessibility === 'private') {
171+
if (getMethodAccessibility(methodPath) === 'private') {
111172
return null;
112173
}
113174

114-
const name = getPropertyName(methodPath);
175+
const name = getMethodName(methodPath);
115176
if (!name) return null;
116177

117-
const docblock = getDocblock(methodPath);
118-
119178
return {
120179
name,
121-
docblock,
180+
docblock: getMethodDocblock(methodPath),
122181
modifiers: getMethodModifiers(methodPath),
123182
params: getMethodParamsDoc(methodPath),
124183
returns: getMethodReturnDoc(methodPath),

0 commit comments

Comments
 (0)