Skip to content

Commit 16fcaa0

Browse files
janicduplessisfkling
authored andcommitted
Extract documentation from Component methods
This adds documentation for methods of react components. It works with both React.createClass and classes. It reads from jsdoc blocks for descriptions and flow types for type information. It supports: - name - description - docblock (the raw docblock for further parsing) - parameters (name, type, description) - modifiers ('static', 'generator', 'async') - return (type, description) It only removes react component methods and leaves removing private methods to the users if they choose so. Tested on the react-native codebase. Closes #17
1 parent dca8ec9 commit 16fcaa0

17 files changed

+1006
-3
lines changed

.eslintrc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@
1111
},
1212
"globals": {
1313
"ASTNode": true,
14+
"FlowTypeDescriptor": true,
1415
"Handler": true,
1516
"NodePath": true,
16-
"PropTypeDescriptor": true,
1717
"PropDescriptor": true,
18+
"PropTypeDescriptor": true,
1819
"Resolver": true
1920
}
2021
}

flow/recast.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ declare class NodePath {
3535
each(f: (p: NodePath) => any): any;
3636
map<T>(f: (p: NodePath) => T): Array<T>;
3737
filter(f: (p: NodePath) => bool): Array<NodePath>;
38+
push(node: ASTNode): void;
3839
}
3940

4041
type Recast = {

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"async": "^1.4.2",
2828
"babel-runtime": "~5.8.25",
2929
"babylon": "~5.8.3",
30+
"doctrine": "^1.2.0",
3031
"node-dir": "^0.1.10",
3132
"nomnom": "^1.8.1",
3233
"recast": "^0.10.41"

src/__tests__/main-test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ describe('main', () => {
2626
expect(docs).toEqual({
2727
displayName: 'ABC',
2828
description: 'Example component description',
29+
methods: [],
2930
props: {
3031
foo: {
3132
type: {
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
* Copyright (c) 2015, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*
9+
*/
10+
11+
/*global jest, describe, beforeEach, it, expect*/
12+
13+
jest.autoMockOff();
14+
jest.mock('../../Documentation');
15+
16+
describe('componentMethodsHandler', () => {
17+
let documentation;
18+
let componentMethodsHandler;
19+
let parse;
20+
21+
beforeEach(() => {
22+
({parse} = require('../../../tests/utils'));
23+
documentation = new (require('../../Documentation'));
24+
componentMethodsHandler = require('../componentMethodsHandler');
25+
});
26+
27+
function test(definition) {
28+
componentMethodsHandler(documentation, definition);
29+
expect(documentation.methods).toEqual([{
30+
name: 'foo',
31+
docblock: 'The foo method',
32+
modifiers: [],
33+
returns: {
34+
type: {name: 'number'},
35+
},
36+
params: [{
37+
name: 'bar',
38+
type: {name: 'number'},
39+
}],
40+
}, {
41+
name: 'bar',
42+
docblock: 'Static function',
43+
modifiers: ['static'],
44+
returns: null,
45+
params: [],
46+
}]);
47+
}
48+
49+
it('extracts the documentation for an ObjectExpression', () => {
50+
const src = `
51+
({
52+
/**
53+
* The foo method
54+
*/
55+
foo(bar: number): number {
56+
return bar;
57+
},
58+
statics: {
59+
/**
60+
* Static function
61+
*/
62+
bar() {}
63+
},
64+
state: {
65+
foo: 'foo',
66+
},
67+
componentDidMount() {},
68+
render() {
69+
return null;
70+
},
71+
})
72+
`;
73+
74+
test(parse(src).get('body', 0, 'expression'));
75+
});
76+
77+
it('extracts the documentation for a ClassDeclaration', () => {
78+
const src = `
79+
class Test {
80+
/**
81+
* The foo method
82+
*/
83+
foo(bar: number): number {
84+
return bar;
85+
}
86+
87+
/**
88+
* Static function
89+
*/
90+
static bar() {}
91+
92+
state = {
93+
foo: 'foo',
94+
};
95+
96+
componentDidMount() {}
97+
98+
render() {
99+
return null;
100+
}
101+
}
102+
`;
103+
104+
test(parse(src).get('body', 0));
105+
});
106+
107+
it('should not find methods for stateless components', () => {
108+
const src = `
109+
(props) => {}
110+
`;
111+
112+
const definition = parse(src).get('body', 0, 'expression');
113+
componentMethodsHandler(documentation, definition);
114+
expect(documentation.methods).toEqual([]);
115+
});
116+
});
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/*
2+
* Copyright (c) 2015, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*
9+
*/
10+
11+
/*global jest, describe, beforeEach, it, expect*/
12+
13+
jest.autoMockOff();
14+
15+
describe('componentMethodsHandler', () => {
16+
let documentation;
17+
let componentMethodsJsDocHandler;
18+
19+
beforeEach(() => {
20+
documentation = new (require('../../Documentation'));
21+
componentMethodsJsDocHandler = require('../componentMethodsJsDocHandler');
22+
});
23+
24+
it('stays the same when no docblock is present', () => {
25+
const methods = [{
26+
name: 'foo',
27+
docblock: null,
28+
modifiers: [],
29+
returns: null,
30+
params: [{
31+
name: 'test',
32+
type: null,
33+
}],
34+
}];
35+
documentation.set('methods', methods);
36+
componentMethodsJsDocHandler(documentation, null);
37+
expect(documentation.get('methods')).toEqual(methods);
38+
});
39+
40+
it('adds js doc types when no flow types', () => {
41+
documentation.set('methods', [{
42+
name: 'foo',
43+
docblock: `
44+
@param {string} test
45+
@returns {string}
46+
`,
47+
modifiers: [],
48+
returns: null,
49+
params: [{
50+
name: 'test',
51+
type: null,
52+
}],
53+
}]);
54+
componentMethodsJsDocHandler(documentation, null);
55+
expect(documentation.get('methods')).toEqual([{
56+
name: 'foo',
57+
description: null,
58+
docblock: `
59+
@param {string} test
60+
@returns {string}
61+
`,
62+
modifiers: [],
63+
returns: {
64+
type: {name: 'string'},
65+
description: null,
66+
},
67+
params: [{
68+
name: 'test',
69+
description: null,
70+
type: {name: 'string'},
71+
}],
72+
}]);
73+
});
74+
75+
it('keeps flow types over js doc types', () => {
76+
documentation.set('methods', [{
77+
name: 'foo',
78+
docblock: `
79+
@param {string} test
80+
@returns {string}
81+
`,
82+
modifiers: [],
83+
returns: {
84+
type: {name: 'number'},
85+
},
86+
params: [{
87+
name: 'test',
88+
type: {name: 'number'},
89+
}],
90+
}]);
91+
componentMethodsJsDocHandler(documentation, null);
92+
expect(documentation.get('methods')).toEqual([{
93+
name: 'foo',
94+
description: null,
95+
docblock: `
96+
@param {string} test
97+
@returns {string}
98+
`,
99+
modifiers: [],
100+
returns: {
101+
type: {name: 'number'},
102+
description: null,
103+
},
104+
params: [{
105+
name: 'test',
106+
description: null,
107+
type: {name: 'number'},
108+
}],
109+
}]);
110+
});
111+
112+
it('adds descriptions', () => {
113+
documentation.set('methods', [{
114+
name: 'foo',
115+
docblock: `
116+
The foo method.
117+
@param test The test
118+
@returns The number
119+
`,
120+
modifiers: [],
121+
returns: null,
122+
params: [{
123+
name: 'test',
124+
type: null,
125+
}],
126+
}]);
127+
componentMethodsJsDocHandler(documentation, null);
128+
expect(documentation.get('methods')).toEqual([{
129+
name: 'foo',
130+
description: 'The foo method.',
131+
docblock: `
132+
The foo method.
133+
@param test The test
134+
@returns The number
135+
`,
136+
modifiers: [],
137+
returns: {
138+
description: 'The number',
139+
type: null,
140+
},
141+
params: [{
142+
name: 'test',
143+
description: 'The test',
144+
type: null,
145+
}],
146+
}]);
147+
});
148+
});
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright (c) 2015, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*
9+
* @flow
10+
*/
11+
12+
import recast from 'recast';
13+
14+
import getMemberValuePath from '../utils/getMemberValuePath';
15+
import getMethodDocumentation from '../utils/getMethodDocumentation';
16+
import isReactComponentClass from '../utils/isReactComponentClass';
17+
import isReactComponentMethod from '../utils/isReactComponentMethod';
18+
import isReactCreateClassCall from '../utils/isReactCreateClassCall';
19+
20+
import type Documentation from '../Documentation';
21+
22+
const {types: {namedTypes: types}} = recast;
23+
24+
function getMethodsDoc(methodPaths) {
25+
const methods = [];
26+
27+
methodPaths.forEach((methodPath) => {
28+
if (isReactComponentMethod(methodPath)) {
29+
return;
30+
}
31+
32+
methods.push(getMethodDocumentation(methodPath));
33+
});
34+
35+
return methods;
36+
}
37+
38+
function isFunctionExpression(path) {
39+
return types.FunctionExpression.check(path.get('value').node)
40+
}
41+
42+
/**
43+
* Extract all flow types for the methods of a react component. Doesn't
44+
* return any react specific lifecycle methods.
45+
*/
46+
export default function componentMethodsHandler(
47+
documentation: Documentation,
48+
path: NodePath
49+
) {
50+
// Extract all methods from the class or object.
51+
let methodPaths = [];
52+
if (isReactComponentClass(path)) {
53+
methodPaths = path
54+
.get('body', 'body')
55+
.filter(p => types.MethodDefinition.check(p.node) && p.node.kind !== 'constructor');
56+
} else if (types.ObjectExpression.check(path.node)) {
57+
methodPaths = path.get('properties').filter(isFunctionExpression);
58+
59+
// Add the statics object properties.
60+
const statics = getMemberValuePath(path, 'statics');
61+
if (statics) {
62+
statics.get('properties').each(p => {
63+
if (isFunctionExpression(p)) {
64+
p.node.static = true;
65+
methodPaths.push(p);
66+
}
67+
});
68+
}
69+
}
70+
71+
const methods = getMethodsDoc(methodPaths);
72+
documentation.set('methods', methods);
73+
}

0 commit comments

Comments
 (0)