Skip to content

Commit d17c6b8

Browse files
committed
feat: support forwardRef for all resolvers
1 parent 55695c3 commit d17c6b8

7 files changed

+205
-18
lines changed

bin/__tests__/react-docgen-test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ describe('react-docgen CLI', () => {
247247
);
248248

249249
describe('--resolver', () => {
250-
it(
250+
it.only(
251251
'accepts the names of built in resolvers',
252252
() => {
253253
return Promise.all([

src/resolver/__tests__/findAllComponentDefinitions-test.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,4 +189,44 @@ describe('findAllComponentDefinitions', () => {
189189
expect(result.length).toBe(0);
190190
});
191191
});
192+
193+
describe('forwardRef components', () => {
194+
it('finds forwardRef components', () => {
195+
const source = `
196+
import React from 'react';
197+
import PropTypes from 'prop-types';
198+
import extendStyles from 'enhancers/extendStyles';
199+
200+
const ColoredView = React.forwardRef((props, ref) => (
201+
<div ref={ref} style={{backgroundColor: props.color}} />
202+
));
203+
204+
extendStyles(ColoredView);
205+
`;
206+
207+
const result = parse(source);
208+
expect(Array.isArray(result)).toBe(true);
209+
expect(result.length).toBe(1);
210+
expect(result[0].value.type).toEqual('CallExpression');
211+
});
212+
213+
it('finds none inline forwardRef components', () => {
214+
const source = `
215+
import React from 'react';
216+
import PropTypes from 'prop-types';
217+
import extendStyles from 'enhancers/extendStyles';
218+
219+
function ColoredView(props, ref) {
220+
return <div ref={ref} style={{backgroundColor: props.color}} />
221+
}
222+
223+
const ForwardedColoredView = React.forwardRef(ColoredView);
224+
`;
225+
226+
const result = parse(source);
227+
expect(Array.isArray(result)).toBe(true);
228+
expect(result.length).toBe(1);
229+
expect(result[0].value.type).toEqual('CallExpression');
230+
});
231+
});
192232
});

src/resolver/__tests__/findAllExportedComponentDefinitions-test.js

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,50 @@ describe('findAllExportedComponentDefinitions', () => {
143143
});
144144
});
145145

146+
describe('forwardRef components', () => {
147+
it('finds forwardRef components', () => {
148+
const source = `
149+
import React from 'react';
150+
import PropTypes from 'prop-types';
151+
import extendStyles from 'enhancers/extendStyles';
152+
153+
const ColoredView = React.forwardRef((props, ref) => (
154+
<div ref={ref} style={{backgroundColor: props.color}} />
155+
));
156+
157+
module.exports = extendStyles(ColoredView);
158+
`;
159+
160+
const parsed = parse(source);
161+
const actual = findComponents(parsed);
162+
163+
expect(actual.length).toBe(1);
164+
expect(actual[0].value.type).toEqual('CallExpression');
165+
});
166+
167+
it('finds none inline forwardRef components', () => {
168+
const source = `
169+
import React from 'react';
170+
import PropTypes from 'prop-types';
171+
import extendStyles from 'enhancers/extendStyles';
172+
173+
function ColoredView(props, ref) {
174+
return <div ref={ref} style={{backgroundColor: props.color}} />
175+
}
176+
177+
const ForwardedColoredView = React.forwardRef(ColoredView);
178+
179+
module.exports = ForwardedColoredView
180+
`;
181+
182+
const parsed = parse(source);
183+
const actual = findComponents(parsed);
184+
185+
expect(actual.length).toBe(1);
186+
expect(actual[0].value.type).toEqual('CallExpression');
187+
});
188+
});
189+
146190
describe('module.exports = <C>; / exports.foo = <C>;', () => {
147191
describe('React.createClass', () => {
148192
it('finds assignments to exports', () => {
@@ -486,6 +530,50 @@ describe('findAllExportedComponentDefinitions', () => {
486530
expect(actual.length).toBe(1);
487531
});
488532
});
533+
534+
describe('forwardRef components', () => {
535+
it('finds forwardRef components', () => {
536+
const source = `
537+
import React from 'react';
538+
import PropTypes from 'prop-types';
539+
import extendStyles from 'enhancers/extendStyles';
540+
541+
const ColoredView = React.forwardRef((props, ref) => (
542+
<div ref={ref} style={{backgroundColor: props.color}} />
543+
));
544+
545+
export default extendStyles(ColoredView);
546+
`;
547+
548+
const parsed = parse(source);
549+
const actual = findComponents(parsed);
550+
551+
expect(actual.length).toBe(1);
552+
expect(actual[0].value.type).toEqual('CallExpression');
553+
});
554+
555+
it('finds none inline forwardRef components', () => {
556+
const source = `
557+
import React from 'react';
558+
import PropTypes from 'prop-types';
559+
import extendStyles from 'enhancers/extendStyles';
560+
561+
function ColoredView(props, ref) {
562+
return <div ref={ref} style={{backgroundColor: props.color}} />
563+
}
564+
565+
const ForwardedColoredView = React.forwardRef(ColoredView);
566+
567+
export default ForwardedColoredView
568+
`;
569+
570+
const parsed = parse(source);
571+
const actual = findComponents(parsed);
572+
573+
expect(actual.length).toBe(1);
574+
expect(actual[0].value.type).toEqual('CallExpression');
575+
});
576+
});
489577
});
490578

491579
describe('export var foo = <C>, ...;', () => {
@@ -734,6 +822,26 @@ describe('findAllExportedComponentDefinitions', () => {
734822
expect(actual[0].node.type).toBe('FunctionExpression');
735823
});
736824
});
825+
826+
describe('forwardRef components', () => {
827+
it('finds forwardRef components', () => {
828+
const source = `
829+
import React from 'react';
830+
import PropTypes from 'prop-types';
831+
import extendStyles from 'enhancers/extendStyles';
832+
833+
export const ColoredView = extendStyles(React.forwardRef((props, ref) => (
834+
<div ref={ref} style={{backgroundColor: props.color}} />
835+
)));
836+
`;
837+
838+
const parsed = parse(source);
839+
const actual = findComponents(parsed);
840+
841+
expect(actual.length).toBe(1);
842+
expect(actual[0].value.type).toEqual('CallExpression');
843+
});
844+
});
737845
});
738846

739847
describe('export {<C>};', () => {
@@ -994,6 +1102,28 @@ describe('findAllExportedComponentDefinitions', () => {
9941102
expect(actual[0].node.type).toBe('ArrowFunctionExpression');
9951103
});
9961104
});
1105+
1106+
describe('forwardRef components', () => {
1107+
it('finds forwardRef components', () => {
1108+
const source = `
1109+
import React from 'react';
1110+
import PropTypes from 'prop-types';
1111+
import extendStyles from 'enhancers/extendStyles';
1112+
1113+
const ColoredView = extendStyles(React.forwardRef((props, ref) => (
1114+
<div ref={ref} style={{backgroundColor: props.color}} />
1115+
)));
1116+
1117+
export { ColoredView }
1118+
`;
1119+
1120+
const parsed = parse(source);
1121+
const actual = findComponents(parsed);
1122+
1123+
expect(actual.length).toBe(1);
1124+
expect(actual[0].value.type).toEqual('CallExpression');
1125+
});
1126+
});
9971127
});
9981128

9991129
describe('export <C>;', () => {

src/resolver/findAllComponentDefinitions.js

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import isReactComponentClass from '../utils/isReactComponentClass';
1414
import isReactCreateClassCall from '../utils/isReactCreateClassCall';
15+
import isReactForwardRefCall from '../utils/isReactForwardRefCall';
1516
import isStatelessComponent from '../utils/isStatelessComponent';
1617
import normalizeClassDefinition from '../utils/normalizeClassDefinition';
1718
import resolveToValue from '../utils/resolveToValue';
@@ -25,19 +26,19 @@ export default function findAllReactCreateClassCalls(
2526
recast: Object,
2627
): Array<NodePath> {
2728
const types = recast.types.namedTypes;
28-
const definitions = [];
29+
const definitions = new Set();
2930

3031
function classVisitor(path) {
3132
if (isReactComponentClass(path)) {
3233
normalizeClassDefinition(path);
33-
definitions.push(path);
34+
definitions.add(path);
3435
}
3536
return false;
3637
}
3738

3839
function statelessVisitor(path) {
3940
if (isStatelessComponent(path)) {
40-
definitions.push(path);
41+
definitions.add(path);
4142
}
4243
return false;
4344
}
@@ -49,16 +50,21 @@ export default function findAllReactCreateClassCalls(
4950
visitClassExpression: classVisitor,
5051
visitClassDeclaration: classVisitor,
5152
visitCallExpression: function(path) {
52-
if (!isReactCreateClassCall(path)) {
53-
return false;
54-
}
55-
const resolvedPath = resolveToValue(path.get('arguments', 0));
56-
if (types.ObjectExpression.check(resolvedPath.node)) {
57-
definitions.push(resolvedPath);
53+
if (isReactForwardRefCall(path)) {
54+
// If the the inner function was previously identified as a component
55+
// replace it with the parent node
56+
const inner = resolveToValue(path.get('arguments', 0));
57+
definitions.delete(inner);
58+
definitions.add(path);
59+
} else if (isReactCreateClassCall(path)) {
60+
const resolvedPath = resolveToValue(path.get('arguments', 0));
61+
if (types.ObjectExpression.check(resolvedPath.node)) {
62+
definitions.add(resolvedPath);
63+
}
5864
}
5965
return false;
6066
},
6167
});
6268

63-
return definitions;
69+
return Array.from(definitions);
6470
}

src/resolver/findAllExportedComponentDefinitions.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import isExportsOrModuleAssignment from '../utils/isExportsOrModuleAssignment';
1313
import isReactComponentClass from '../utils/isReactComponentClass';
1414
import isReactCreateClassCall from '../utils/isReactCreateClassCall';
15+
import isReactForwardRefCall from '../utils/isReactForwardRefCall';
1516
import isStatelessComponent from '../utils/isStatelessComponent';
1617
import normalizeClassDefinition from '../utils/normalizeClassDefinition';
1718
import resolveExportDeclaration from '../utils/resolveExportDeclaration';
@@ -26,7 +27,8 @@ function isComponentDefinition(path) {
2627
return (
2728
isReactCreateClassCall(path) ||
2829
isReactComponentClass(path) ||
29-
isStatelessComponent(path)
30+
isStatelessComponent(path) ||
31+
isReactForwardRefCall(path)
3032
);
3133
}
3234

@@ -40,7 +42,10 @@ function resolveDefinition(definition, types): ?NodePath {
4042
} else if (isReactComponentClass(definition)) {
4143
normalizeClassDefinition(definition);
4244
return definition;
43-
} else if (isStatelessComponent(definition)) {
45+
} else if (
46+
isStatelessComponent(definition) ||
47+
isReactForwardRefCall(definition)
48+
) {
4449
return definition;
4550
}
4651
return null;

src/resolver/findExportedComponentDefinition.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
*/
1111

1212
import isExportsOrModuleAssignment from '../utils/isExportsOrModuleAssignment';
13-
import isReactForwardRefCall from '../utils/isReactForwardRefCall';
1413
import isReactComponentClass from '../utils/isReactComponentClass';
1514
import isReactCreateClassCall from '../utils/isReactCreateClassCall';
15+
import isReactForwardRefCall from '../utils/isReactForwardRefCall';
1616
import isStatelessComponent from '../utils/isStatelessComponent';
1717
import normalizeClassDefinition from '../utils/normalizeClassDefinition';
1818
import resolveExportDeclaration from '../utils/resolveExportDeclaration';
@@ -45,9 +45,10 @@ function resolveDefinition(definition, types) {
4545
} else if (isReactComponentClass(definition)) {
4646
normalizeClassDefinition(definition);
4747
return definition;
48-
} else if (isStatelessComponent(definition)) {
49-
return definition;
50-
} else if (isReactForwardRefCall(definition)) {
48+
} else if (
49+
isStatelessComponent(definition) ||
50+
isReactForwardRefCall(definition)
51+
) {
5152
return definition;
5253
}
5354
return null;

src/utils/resolveHOC.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import recast from 'recast';
1414
import isReactCreateClassCall from './isReactCreateClassCall';
15+
import isReactForwardRefCall from './isReactForwardRefCall';
1516

1617
const {
1718
types: { NodePath, namedTypes: types },
@@ -25,7 +26,11 @@ const {
2526
*/
2627
export default function resolveHOC(path: NodePath): NodePath {
2728
const node = path.node;
28-
if (types.CallExpression.check(node) && !isReactCreateClassCall(path)) {
29+
if (
30+
types.CallExpression.check(node) &&
31+
!isReactCreateClassCall(path) &&
32+
!isReactForwardRefCall(path)
33+
) {
2934
if (node.arguments.length) {
3035
return resolveHOC(path.get('arguments', node.arguments.length - 1));
3136
}

0 commit comments

Comments
 (0)