Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/seven-camels-flash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'react-docgen': patch
---

Handle cyclic references in PropTypes `shape()` and `exact()` methods.
Original file line number Diff line number Diff line change
Expand Up @@ -369,3 +369,31 @@ exports[`getPropType > resolve identifier to their values > resolves variables t
},
}
`;

exports[`getPropType > works with cyclic references in shape 1`] = `
{
"name": "shape",
"value": "Component.propTypes",
}
`;

exports[`getPropType > works with cyclic references in shape and required 1`] = `
{
"name": "shape",
"value": "Component.propTypes",
}
`;

exports[`getPropType > works with missing argument 1`] = `
{
"name": "shape",
"value": {
"foo": {
"computed": true,
"name": "shape",
"required": false,
"value": "",
},
},
}
`;
47 changes: 47 additions & 0 deletions packages/react-docgen/src/utils/__tests__/getPropType-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { ExpressionStatement } from '@babel/types';
import { parse, makeMockImporter } from '../../../tests/utils';
import getPropType from '../getPropType.js';
import { describe, expect, test } from 'vitest';
import type { NodePath } from '@babel/traverse';

describe('getPropType', () => {
test('detects simple prop types', () => {
Expand Down Expand Up @@ -532,4 +533,50 @@ describe('getPropType', () => {
),
).toMatchSnapshot();
});

test('works with cyclic references in shape', () => {
expect(
getPropType(
parse
.statementLast<ExpressionStatement>(
`const Component = () => {}
Component.propTypes = {
foo: shape(Component.propTypes)
}`,
)
.get('expression.right.properties.0.value') as NodePath,
),
).toMatchSnapshot();
});

test('works with cyclic references in shape and required', () => {
expect(
getPropType(
parse
.statementLast<ExpressionStatement>(
`const Component = () => {}
Component.propTypes = {
foo: shape(Component.propTypes).isRequired
}`,
)
.get('expression.right.properties.0.value') as NodePath,
),
).toMatchSnapshot();
});

test('works with missing argument', () => {
expect(
getPropType(
parse
.statementLast<ExpressionStatement>(
`const Component = () => {}
const MyShape = { foo: shape() }
Component.propTypes = {
foo: shape(MyShape)
}`,
)
.get('expression.right.properties.0.value') as NodePath,
),
).toMatchSnapshot();
});
});
178 changes: 110 additions & 68 deletions packages/react-docgen/src/utils/getPropType.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/*eslint no-use-before-define: 0*/
import type { NodePath } from '@babel/traverse';
import { getDocblock } from '../utils/docblock.js';
import getMembers from './getMembers.js';
Expand All @@ -8,13 +7,8 @@ import printValue from './printValue.js';
import resolveToValue from './resolveToValue.js';
import resolveObjectKeysToArray from './resolveObjectKeysToArray.js';
import resolveObjectValuesToArray from './resolveObjectValuesToArray.js';
import type { PropTypeDescriptor, PropDescriptor } from '../Documentation.js';
import type {
ArrayExpression,
Expression,
ObjectProperty,
SpreadElement,
} from '@babel/types';
import type { PropTypeDescriptor } from '../Documentation.js';
import type { ArrayExpression, Expression, SpreadElement } from '@babel/types';

function getEnumValuesFromArrayExpression(
path: NodePath<ArrayExpression>,
Expand Down Expand Up @@ -51,11 +45,15 @@ function getEnumValuesFromArrayExpression(
return values;
}

function getPropTypeOneOf(argumentPath: NodePath): PropTypeDescriptor {
const type: PropTypeDescriptor = { name: 'enum' };
const value: NodePath = resolveToValue(argumentPath);
function getPropTypeOneOf(
type: PropTypeDescriptor,
argumentPath: NodePath,
): PropTypeDescriptor {
const value = resolveToValue(argumentPath);

if (!value.isArrayExpression()) {
if (value.isArrayExpression()) {
type.value = getEnumValuesFromArrayExpression(value);
} else {
const objectValues =
resolveObjectKeysToArray(value) || resolveObjectValuesToArray(value);

Expand All @@ -69,20 +67,16 @@ function getPropTypeOneOf(argumentPath: NodePath): PropTypeDescriptor {
type.computed = true;
type.value = printValue(argumentPath);
}
} else {
type.value = getEnumValuesFromArrayExpression(value);
}

return type;
}

function getPropTypeOneOfType(argumentPath: NodePath): PropTypeDescriptor {
const type: PropTypeDescriptor = { name: 'union' };

if (!argumentPath.isArrayExpression()) {
type.computed = true;
type.value = printValue(argumentPath);
} else {
function getPropTypeOneOfType(
type: PropTypeDescriptor,
argumentPath: NodePath,
): PropTypeDescriptor {
if (argumentPath.isArrayExpression()) {
type.value = argumentPath.get('elements').map((elementPath) => {
if (!elementPath.hasNode()) return;
const descriptor: PropTypeDescriptor = getPropType(elementPath);
Expand All @@ -101,9 +95,7 @@ function getPropTypeOneOfType(argumentPath: NodePath): PropTypeDescriptor {
return type;
}

function getPropTypeArrayOf(argumentPath: NodePath) {
const type: PropTypeDescriptor = { name: 'arrayOf' };

function getPropTypeArrayOf(type: PropTypeDescriptor, argumentPath: NodePath) {
const docs = getDocblock(argumentPath);

if (docs) {
Expand All @@ -113,19 +105,14 @@ function getPropTypeArrayOf(argumentPath: NodePath) {
const subType = getPropType(argumentPath);

// @ts-ignore
if (subType.name === 'unknown') {
type.value = printValue(argumentPath);
type.computed = true;
} else {
if (subType.name !== 'unknown') {
type.value = subType;
}

return type;
}

function getPropTypeObjectOf(argumentPath: NodePath) {
const type: PropTypeDescriptor = { name: 'objectOf' };

function getPropTypeObjectOf(type: PropTypeDescriptor, argumentPath: NodePath) {
const docs = getDocblock(argumentPath);

if (docs) {
Expand All @@ -135,63 +122,87 @@ function getPropTypeObjectOf(argumentPath: NodePath) {
const subType = getPropType(argumentPath);

// @ts-ignore
if (subType.name === 'unknown') {
type.value = printValue(argumentPath);
type.computed = true;
} else {
if (subType.name !== 'unknown') {
type.value = subType;
}

return type;
}

function getFirstArgument(path: NodePath): NodePath | undefined {
let argument: NodePath | undefined;

if (path.isCallExpression()) {
argument = path.get('arguments')[0];
} else {
const members = getMembers(path, true);

if (members[0] && members[0].argumentPaths[0]) {
argument = members[0].argumentPaths[0];
}
}

return argument;
}

function isCyclicReference(
argument: NodePath,
argumentPath: NodePath,
): boolean {
return Boolean(argument && resolveToValue(argument) === argumentPath);
}

/**
* Handles shape and exact prop types
*/
function getPropTypeShapish(name: 'exact' | 'shape', argumentPath: NodePath) {
const type: PropTypeDescriptor = { name };

function getPropTypeShapish(type: PropTypeDescriptor, argumentPath: NodePath) {
if (!argumentPath.isObjectExpression()) {
argumentPath = resolveToValue(argumentPath);
}

if (argumentPath.isObjectExpression()) {
const value = {};
let value: Record<string, PropTypeDescriptor> | string = {};

argumentPath.get('properties').forEach((propertyPath) => {
if (propertyPath.isSpreadElement() || propertyPath.isObjectMethod()) {
// It is impossible to resolve a name for a spread element
return;
}
// We only handle ObjectProperty as there is nothing to handle for
// SpreadElements and ObjectMethods
if (propertyPath.isObjectProperty()) {
const propertyName = getPropertyName(propertyPath);

const propertyName = getPropertyName(propertyPath);
if (!propertyName) return;

if (!propertyName) return;
const valuePath = propertyPath.get('value');
const argument = getFirstArgument(valuePath);

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

const descriptor: PropDescriptor | PropTypeDescriptor =
getPropType(valuePath);
const docs = getDocblock(propertyPath);
return;
}

if (docs) {
descriptor.description = docs;
const descriptor = getPropType(valuePath);
const docs = getDocblock(propertyPath);

if (docs) {
descriptor.description = docs;
}
descriptor.required = isRequiredPropType(valuePath);
value[propertyName] = descriptor;
}
descriptor.required = isRequiredPropType(valuePath);
value[propertyName] = descriptor;
});
type.value = value;
}

if (!type.value) {
type.value = printValue(argumentPath);
type.computed = true;
type.value = value;
}

return type;
}

function getPropTypeInstanceOf(argumentPath: NodePath): PropTypeDescriptor {
function getPropTypeInstanceOf(
_type: PropTypeDescriptor,
argumentPath: NodePath,
): PropTypeDescriptor {
return {
name: 'instanceOf',
value: printValue(argumentPath),
Expand All @@ -218,16 +229,47 @@ function isSimplePropType(
return simplePropTypes.includes(name as (typeof simplePropTypes)[number]);
}

const propTypes = new Map<string, (path: NodePath) => PropTypeDescriptor>([
['oneOf', getPropTypeOneOf],
['oneOfType', getPropTypeOneOfType],
['instanceOf', getPropTypeInstanceOf],
['arrayOf', getPropTypeArrayOf],
['objectOf', getPropTypeObjectOf],
['shape', getPropTypeShapish.bind(null, 'shape')],
['exact', getPropTypeShapish.bind(null, 'exact')],
type PropTypeHandler = (
type: PropTypeDescriptor,
argumentPath: NodePath,
) => PropTypeDescriptor;

const propTypes = new Map<
string,
(argumentPath: NodePath | undefined) => PropTypeDescriptor
>([
['oneOf', callPropTypeHandler.bind(null, 'enum', getPropTypeOneOf)],
['oneOfType', callPropTypeHandler.bind(null, 'union', getPropTypeOneOfType)],
[
'instanceOf',
callPropTypeHandler.bind(null, 'instanceOf', getPropTypeInstanceOf),
],
['arrayOf', callPropTypeHandler.bind(null, 'arrayOf', getPropTypeArrayOf)],
['objectOf', callPropTypeHandler.bind(null, 'objectOf', getPropTypeObjectOf)],
['shape', callPropTypeHandler.bind(null, 'shape', getPropTypeShapish)],
['exact', callPropTypeHandler.bind(null, 'exact', getPropTypeShapish)],
]);

function callPropTypeHandler(
name: PropTypeDescriptor['name'],
handler: PropTypeHandler,
argumentPath: NodePath | undefined,
) {
let type: PropTypeDescriptor = { name };

if (argumentPath) {
type = handler(type, argumentPath);
}

if (!type.value) {
// If there is no argument then leave the value an empty string
type.value = argumentPath ? printValue(argumentPath) : '';
type.computed = true;
}

return type;
}

/**
* Tries to identify the prop type by inspecting the path for known
* prop type names. This method doesn't check whether the found type is actually
Expand Down Expand Up @@ -256,7 +298,7 @@ export default function getPropType(path: NodePath): PropTypeDescriptor {
}
const propTypeHandler = propTypes.get(name);

if (propTypeHandler && member.argumentPaths.length) {
if (propTypeHandler) {
descriptor = propTypeHandler(member.argumentPaths[0]);

return true;
Expand Down
1 change: 1 addition & 0 deletions tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"strict": true,
"noImplicitAny": false,
"noImplicitReturns": true,
"noUncheckedIndexedAccess": false,
"noUnusedLocals": true,
"noUnusedParameters": true,
"moduleResolution": "node16",
Expand Down