Skip to content

Commit 15ea6b1

Browse files
committed
Handle cycle in PropTypes shapeish validators
1 parent 1aa0249 commit 15ea6b1

File tree

4 files changed

+129
-25
lines changed

4 files changed

+129
-25
lines changed

.changeset/seven-camels-flash.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'react-docgen': patch
3+
---
4+
5+
Handle cyclic references in PropTypes `shape()` and `exact()` methods.

packages/react-docgen/src/utils/__tests__/__snapshots__/getPropType-test.ts.snap

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,3 +369,30 @@ exports[`getPropType > resolve identifier to their values > resolves variables t
369369
},
370370
}
371371
`;
372+
373+
exports[`getPropType > works with cyclic references in shape 1`] = `
374+
{
375+
"name": "shape",
376+
"value": "Component.propTypes",
377+
}
378+
`;
379+
380+
exports[`getPropType > works with cyclic references in shape and required 1`] = `
381+
{
382+
"name": "shape",
383+
"value": "Component.propTypes",
384+
}
385+
`;
386+
387+
exports[`getPropType > works with missing argument 1`] = `
388+
{
389+
"name": "shape",
390+
"value": {
391+
"foo": {
392+
"name": "custom",
393+
"raw": "shape()",
394+
"required": false,
395+
},
396+
},
397+
}
398+
`;

packages/react-docgen/src/utils/__tests__/getPropType-test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { ExpressionStatement } from '@babel/types';
22
import { parse, makeMockImporter } from '../../../tests/utils';
33
import getPropType from '../getPropType.js';
44
import { describe, expect, test } from 'vitest';
5+
import type { NodePath } from '@babel/traverse';
56

67
describe('getPropType', () => {
78
test('detects simple prop types', () => {
@@ -483,6 +484,52 @@ describe('getPropType', () => {
483484
).toMatchSnapshot();
484485
});
485486

487+
test('works with cyclic references in shape', () => {
488+
expect(
489+
getPropType(
490+
parse
491+
.statementLast<ExpressionStatement>(
492+
`const Component = () => {}
493+
Component.propTypes = {
494+
foo: shape(Component.propTypes)
495+
}`,
496+
)
497+
.get('expression.right.properties.0.value') as NodePath,
498+
),
499+
).toMatchSnapshot();
500+
});
501+
502+
test('works with cyclic references in shape and required', () => {
503+
expect(
504+
getPropType(
505+
parse
506+
.statementLast<ExpressionStatement>(
507+
`const Component = () => {}
508+
Component.propTypes = {
509+
foo: shape(Component.propTypes).isRequired
510+
}`,
511+
)
512+
.get('expression.right.properties.0.value') as NodePath,
513+
),
514+
).toMatchSnapshot();
515+
});
516+
517+
test('works with missing argument', () => {
518+
expect(
519+
getPropType(
520+
parse
521+
.statementLast<ExpressionStatement>(
522+
`const Component = () => {}
523+
const MyShape = { foo: shape() }
524+
Component.propTypes = {
525+
foo: shape(MyShape)
526+
}`,
527+
)
528+
.get('expression.right.properties.0.value') as NodePath,
529+
),
530+
).toMatchSnapshot();
531+
});
532+
486533
test('detects descriptions on nested types in exacts', () => {
487534
expect(
488535
getPropType(

packages/react-docgen/src/utils/getPropType.ts

Lines changed: 50 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/*eslint no-use-before-define: 0*/
21
import type { NodePath } from '@babel/traverse';
32
import { getDocblock } from '../utils/docblock.js';
43
import getMembers from './getMembers.js';
@@ -8,13 +7,8 @@ import printValue from './printValue.js';
87
import resolveToValue from './resolveToValue.js';
98
import resolveObjectKeysToArray from './resolveObjectKeysToArray.js';
109
import resolveObjectValuesToArray from './resolveObjectValuesToArray.js';
11-
import type { PropTypeDescriptor, PropDescriptor } from '../Documentation.js';
12-
import type {
13-
ArrayExpression,
14-
Expression,
15-
ObjectProperty,
16-
SpreadElement,
17-
} from '@babel/types';
10+
import type { PropTypeDescriptor } from '../Documentation.js';
11+
import type { ArrayExpression, Expression, SpreadElement } from '@babel/types';
1812

1913
function getEnumValuesFromArrayExpression(
2014
path: NodePath<ArrayExpression>,
@@ -145,6 +139,29 @@ function getPropTypeObjectOf(argumentPath: NodePath) {
145139
return type;
146140
}
147141

142+
function getFirstArgument(path: NodePath): NodePath | undefined {
143+
let argument: NodePath | undefined;
144+
145+
if (path.isCallExpression()) {
146+
argument = path.get('arguments')[0];
147+
} else {
148+
const members = getMembers(path, true);
149+
150+
if (members[0] && members[0].argumentPaths[0]) {
151+
argument = members[0].argumentPaths[0];
152+
}
153+
}
154+
155+
return argument;
156+
}
157+
158+
function isCyclicReference(
159+
argument: NodePath,
160+
argumentPath: NodePath,
161+
): boolean {
162+
return Boolean(argument && resolveToValue(argument) === argumentPath);
163+
}
164+
148165
/**
149166
* Handles shape and exact prop types
150167
*/
@@ -156,29 +173,36 @@ function getPropTypeShapish(name: 'exact' | 'shape', argumentPath: NodePath) {
156173
}
157174

158175
if (argumentPath.isObjectExpression()) {
159-
const value = {};
176+
let value: Record<string, PropTypeDescriptor> | string = {};
160177

161178
argumentPath.get('properties').forEach((propertyPath) => {
162-
if (propertyPath.isSpreadElement() || propertyPath.isObjectMethod()) {
163-
// It is impossible to resolve a name for a spread element
164-
return;
165-
}
179+
// We only handle ObjectProperty as there is nothing to handle for
180+
// SpreadElements and ObjectMethods
181+
if (propertyPath.isObjectProperty()) {
182+
const propertyName = getPropertyName(propertyPath);
166183

167-
const propertyName = getPropertyName(propertyPath);
184+
if (!propertyName) return;
168185

169-
if (!propertyName) return;
186+
const valuePath = propertyPath.get('value');
187+
const argument = getFirstArgument(valuePath);
170188

171-
const valuePath = (propertyPath as NodePath<ObjectProperty>).get('value');
189+
// This indicates we have a cyclic reference in the shape
190+
// In this case we simply print the argument to shape and bail
191+
if (argument && isCyclicReference(argument, argumentPath)) {
192+
value = printValue(argument);
172193

173-
const descriptor: PropDescriptor | PropTypeDescriptor =
174-
getPropType(valuePath);
175-
const docs = getDocblock(propertyPath);
194+
return;
195+
}
176196

177-
if (docs) {
178-
descriptor.description = docs;
197+
const descriptor = getPropType(valuePath);
198+
const docs = getDocblock(propertyPath);
199+
200+
if (docs) {
201+
descriptor.description = docs;
202+
}
203+
descriptor.required = isRequiredPropType(valuePath);
204+
value[propertyName] = descriptor;
179205
}
180-
descriptor.required = isRequiredPropType(valuePath);
181-
value[propertyName] = descriptor;
182206
});
183207
type.value = value;
184208
}
@@ -279,9 +303,10 @@ export default function getPropType(path: NodePath): PropTypeDescriptor {
279303

280304
if (callee.isIdentifier()) {
281305
const propTypeHandler = propTypes.get(callee.node.name);
306+
const argument = path.get('arguments')[0];
282307

283-
if (propTypeHandler) {
284-
return propTypeHandler(path.get('arguments')[0]);
308+
if (propTypeHandler && argument) {
309+
return propTypeHandler(argument);
285310
}
286311
}
287312
}

0 commit comments

Comments
 (0)