Skip to content

Commit b517f88

Browse files
committed
Handle cycle in PropTypes shapeish validators
1 parent bca73e9 commit b517f88

File tree

5 files changed

+189
-61
lines changed

5 files changed

+189
-61
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: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,3 +369,31 @@ 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+
"computed": true,
393+
"name": "shape",
394+
"required": false,
395+
"value": "",
396+
},
397+
},
398+
}
399+
`;

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 { NodePath } from '@babel/traverse';
56

67
describe('getPropType', () => {
78
test('detects simple prop types', () => {
@@ -532,4 +533,50 @@ describe('getPropType', () => {
532533
),
533534
).toMatchSnapshot();
534535
});
536+
537+
test('works with cyclic references in shape', () => {
538+
expect(
539+
getPropType(
540+
parse
541+
.statementLast<ExpressionStatement>(
542+
`const Component = () => {}
543+
Component.propTypes = {
544+
foo: shape(Component.propTypes)
545+
}`,
546+
)
547+
.get('expression.right.properties.0.value') as NodePath,
548+
),
549+
).toMatchSnapshot();
550+
});
551+
552+
test('works with cyclic references in shape and required', () => {
553+
expect(
554+
getPropType(
555+
parse
556+
.statementLast<ExpressionStatement>(
557+
`const Component = () => {}
558+
Component.propTypes = {
559+
foo: shape(Component.propTypes).isRequired
560+
}`,
561+
)
562+
.get('expression.right.properties.0.value') as NodePath,
563+
),
564+
).toMatchSnapshot();
565+
});
566+
567+
test('works with missing argument', () => {
568+
expect(
569+
getPropType(
570+
parse
571+
.statementLast<ExpressionStatement>(
572+
`const Component = () => {}
573+
const MyShape = { foo: shape() }
574+
Component.propTypes = {
575+
foo: shape(MyShape)
576+
}`,
577+
)
578+
.get('expression.right.properties.0.value') as NodePath,
579+
),
580+
).toMatchSnapshot();
581+
});
535582
});

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

Lines changed: 108 additions & 61 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';
@@ -51,11 +50,15 @@ function getEnumValuesFromArrayExpression(
5150
return values;
5251
}
5352

54-
function getPropTypeOneOf(argumentPath: NodePath): PropTypeDescriptor {
55-
const type: PropTypeDescriptor = { name: 'enum' };
56-
const value: NodePath = resolveToValue(argumentPath);
53+
function getPropTypeOneOf(
54+
type: PropTypeDescriptor,
55+
argumentPath: NodePath,
56+
): PropTypeDescriptor {
57+
const value = resolveToValue(argumentPath);
5758

58-
if (!value.isArrayExpression()) {
59+
if (value.isArrayExpression()) {
60+
type.value = getEnumValuesFromArrayExpression(value);
61+
} else {
5962
const objectValues =
6063
resolveObjectKeysToArray(value) || resolveObjectValuesToArray(value);
6164

@@ -69,20 +72,16 @@ function getPropTypeOneOf(argumentPath: NodePath): PropTypeDescriptor {
6972
type.computed = true;
7073
type.value = printValue(argumentPath);
7174
}
72-
} else {
73-
type.value = getEnumValuesFromArrayExpression(value);
7475
}
7576

7677
return type;
7778
}
7879

79-
function getPropTypeOneOfType(argumentPath: NodePath): PropTypeDescriptor {
80-
const type: PropTypeDescriptor = { name: 'union' };
81-
82-
if (!argumentPath.isArrayExpression()) {
83-
type.computed = true;
84-
type.value = printValue(argumentPath);
85-
} else {
80+
function getPropTypeOneOfType(
81+
type: PropTypeDescriptor,
82+
argumentPath: NodePath,
83+
): PropTypeDescriptor {
84+
if (argumentPath.isArrayExpression()) {
8685
type.value = argumentPath.get('elements').map((elementPath) => {
8786
if (!elementPath.hasNode()) return;
8887
const descriptor: PropTypeDescriptor = getPropType(elementPath);
@@ -101,9 +100,7 @@ function getPropTypeOneOfType(argumentPath: NodePath): PropTypeDescriptor {
101100
return type;
102101
}
103102

104-
function getPropTypeArrayOf(argumentPath: NodePath) {
105-
const type: PropTypeDescriptor = { name: 'arrayOf' };
106-
103+
function getPropTypeArrayOf(type: PropTypeDescriptor, argumentPath: NodePath) {
107104
const docs = getDocblock(argumentPath);
108105

109106
if (docs) {
@@ -113,19 +110,14 @@ function getPropTypeArrayOf(argumentPath: NodePath) {
113110
const subType = getPropType(argumentPath);
114111

115112
// @ts-ignore
116-
if (subType.name === 'unknown') {
117-
type.value = printValue(argumentPath);
118-
type.computed = true;
119-
} else {
113+
if (subType.name !== 'unknown') {
120114
type.value = subType;
121115
}
122116

123117
return type;
124118
}
125119

126-
function getPropTypeObjectOf(argumentPath: NodePath) {
127-
const type: PropTypeDescriptor = { name: 'objectOf' };
128-
120+
function getPropTypeObjectOf(type: PropTypeDescriptor, argumentPath: NodePath) {
129121
const docs = getDocblock(argumentPath);
130122

131123
if (docs) {
@@ -135,63 +127,87 @@ function getPropTypeObjectOf(argumentPath: NodePath) {
135127
const subType = getPropType(argumentPath);
136128

137129
// @ts-ignore
138-
if (subType.name === 'unknown') {
139-
type.value = printValue(argumentPath);
140-
type.computed = true;
141-
} else {
130+
if (subType.name !== 'unknown') {
142131
type.value = subType;
143132
}
144133

145134
return type;
146135
}
147136

137+
function getFirstArgument(path: NodePath): NodePath | undefined {
138+
let argument: NodePath | undefined;
139+
140+
if (path.isCallExpression()) {
141+
argument = path.get('arguments')[0];
142+
} else {
143+
const members = getMembers(path, true);
144+
145+
if (members[0] && members[0].argumentPaths[0]) {
146+
argument = members[0].argumentPaths[0];
147+
}
148+
}
149+
150+
return argument;
151+
}
152+
153+
function isCyclicReference(
154+
argument: NodePath,
155+
argumentPath: NodePath,
156+
): boolean {
157+
return Boolean(argument && resolveToValue(argument) === argumentPath);
158+
}
159+
148160
/**
149161
* Handles shape and exact prop types
150162
*/
151-
function getPropTypeShapish(name: 'exact' | 'shape', argumentPath: NodePath) {
152-
const type: PropTypeDescriptor = { name };
153-
163+
function getPropTypeShapish(type: PropTypeDescriptor, argumentPath: NodePath) {
154164
if (!argumentPath.isObjectExpression()) {
155165
argumentPath = resolveToValue(argumentPath);
156166
}
157167

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

161171
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-
}
172+
// We only handle ObjectProperty as there is nothing to handle for
173+
// SpreadElements and ObjectMethods
174+
if (propertyPath.isObjectProperty()) {
175+
const propertyName = getPropertyName(propertyPath);
166176

167-
const propertyName = getPropertyName(propertyPath);
177+
if (!propertyName) return;
168178

169-
if (!propertyName) return;
179+
const valuePath = propertyPath.get('value');
180+
const argument = getFirstArgument(valuePath);
170181

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

173-
const descriptor: PropDescriptor | PropTypeDescriptor =
174-
getPropType(valuePath);
175-
const docs = getDocblock(propertyPath);
187+
return;
188+
}
176189

177-
if (docs) {
178-
descriptor.description = docs;
190+
const descriptor = getPropType(valuePath);
191+
const docs = getDocblock(propertyPath);
192+
193+
if (docs) {
194+
descriptor.description = docs;
195+
}
196+
descriptor.required = isRequiredPropType(valuePath);
197+
value[propertyName] = descriptor;
179198
}
180-
descriptor.required = isRequiredPropType(valuePath);
181-
value[propertyName] = descriptor;
182199
});
183-
type.value = value;
184-
}
185200

186-
if (!type.value) {
187-
type.value = printValue(argumentPath);
188-
type.computed = true;
201+
type.value = value;
189202
}
190203

191204
return type;
192205
}
193206

194-
function getPropTypeInstanceOf(argumentPath: NodePath): PropTypeDescriptor {
207+
function getPropTypeInstanceOf(
208+
_type: PropTypeDescriptor,
209+
argumentPath: NodePath,
210+
): PropTypeDescriptor {
195211
return {
196212
name: 'instanceOf',
197213
value: printValue(argumentPath),
@@ -218,16 +234,47 @@ function isSimplePropType(
218234
return simplePropTypes.includes(name as (typeof simplePropTypes)[number]);
219235
}
220236

221-
const propTypes = new Map<string, (path: NodePath) => PropTypeDescriptor>([
222-
['oneOf', getPropTypeOneOf],
223-
['oneOfType', getPropTypeOneOfType],
224-
['instanceOf', getPropTypeInstanceOf],
225-
['arrayOf', getPropTypeArrayOf],
226-
['objectOf', getPropTypeObjectOf],
227-
['shape', getPropTypeShapish.bind(null, 'shape')],
228-
['exact', getPropTypeShapish.bind(null, 'exact')],
237+
type PropTypeHandler = (
238+
type: PropTypeDescriptor,
239+
argumentPath: NodePath,
240+
) => PropTypeDescriptor;
241+
242+
const propTypes = new Map<
243+
string,
244+
(argumentPath: NodePath | undefined) => PropTypeDescriptor
245+
>([
246+
['oneOf', callPropTypeHandler.bind(null, 'enum', getPropTypeOneOf)],
247+
['oneOfType', callPropTypeHandler.bind(null, 'union', getPropTypeOneOfType)],
248+
[
249+
'instanceOf',
250+
callPropTypeHandler.bind(null, 'instanceOf', getPropTypeInstanceOf),
251+
],
252+
['arrayOf', callPropTypeHandler.bind(null, 'arrayOf', getPropTypeArrayOf)],
253+
['objectOf', callPropTypeHandler.bind(null, 'objectOf', getPropTypeObjectOf)],
254+
['shape', callPropTypeHandler.bind(null, 'shape', getPropTypeShapish)],
255+
['exact', callPropTypeHandler.bind(null, 'exact', getPropTypeShapish)],
229256
]);
230257

258+
function callPropTypeHandler(
259+
name: PropTypeDescriptor['name'],
260+
handler: PropTypeHandler,
261+
argumentPath: NodePath | undefined,
262+
) {
263+
let type: PropTypeDescriptor = { name };
264+
265+
if (argumentPath) {
266+
type = handler(type, argumentPath);
267+
}
268+
269+
if (!type.value) {
270+
// If there is no argument then leave the value an empty string
271+
type.value = argumentPath ? printValue(argumentPath) : '';
272+
type.computed = true;
273+
}
274+
275+
return type;
276+
}
277+
231278
/**
232279
* Tries to identify the prop type by inspecting the path for known
233280
* prop type names. This method doesn't check whether the found type is actually
@@ -256,7 +303,7 @@ export default function getPropType(path: NodePath): PropTypeDescriptor {
256303
}
257304
const propTypeHandler = propTypes.get(name);
258305

259-
if (propTypeHandler && member.argumentPaths.length) {
306+
if (propTypeHandler) {
260307
descriptor = propTypeHandler(member.argumentPaths[0]);
261308

262309
return true;

tsconfig.base.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"strict": true,
99
"noImplicitAny": false,
1010
"noImplicitReturns": true,
11+
"noUncheckedIndexedAccess": false,
1112
"noUnusedLocals": true,
1213
"noUnusedParameters": true,
1314
"moduleResolution": "node16",

0 commit comments

Comments
 (0)