Files
react-test/packages/api-docs-builder/utils/defaultPropsHandler.ts
how2ice 005cf56baf
Some checks failed
No response / noResponse (push) Has been cancelled
CI / Continuous releases (push) Has been cancelled
CI / test-dev (macos-latest) (push) Has been cancelled
CI / test-dev (ubuntu-latest) (push) Has been cancelled
CI / test-dev (windows-latest) (push) Has been cancelled
Maintenance / main (push) Has been cancelled
Scorecards supply-chain security / Scorecards analysis (push) Has been cancelled
CodeQL / Analyze (push) Has been cancelled
init project
2025-12-12 14:26:25 +09:00

177 lines
5.7 KiB
TypeScript

import { namedTypes as types } from 'ast-types';
import { parse as parseDoctrine, Annotation } from 'doctrine';
import { utils as docgenUtils, NodePath, Documentation, Importer, Handler } from 'react-docgen';
const { getPropertyName, isReactForwardRefCall, printValue, resolveToValue } = docgenUtils;
// based on https://github.com/reactjs/react-docgen/blob/735f39ef784312f4c0e740d4bfb812f0a7acd3d5/src/handlers/defaultPropsHandler.js#L1-L112
// adjusted for material-ui getThemedProps
function getDefaultValue(propertyPath: NodePath, importer: Importer) {
if (!types.AssignmentPattern.check(propertyPath.get('value').node)) {
return null;
}
let path: NodePath = propertyPath.get('value', 'right');
let node = path.node;
let defaultValue: string | undefined;
if (types.Literal.check(path.node)) {
// @ts-expect-error TODO upstream fix
defaultValue = node.raw;
} else {
if (types.AssignmentPattern.check(path.node)) {
path = resolveToValue(path.get('right'), importer);
} else {
path = resolveToValue(path, importer);
}
if (types.ImportDeclaration.check(path.node)) {
if (types.TSAsExpression.check(node)) {
node = node.expression;
}
if (!types.Identifier.check(node)) {
const locationHint =
node.loc != null ? `${node.loc.start.line}:${node.loc.start.column}` : 'unknown location';
throw new TypeError(
`Unable to follow data flow. Expected an 'Identifier' resolve to an 'ImportDeclaration'. Instead attempted to resolve a '${node.type}' at ${locationHint}.`,
);
}
defaultValue = node.name;
} else {
node = path.node;
defaultValue = printValue(path);
}
}
if (defaultValue !== undefined) {
return {
value: defaultValue,
computed:
types.CallExpression.check(node) ||
types.MemberExpression.check(node) ||
types.Identifier.check(node),
};
}
return null;
}
function getJsdocDefaultValue(jsdoc: Annotation): { value: string } | undefined {
const defaultTag = jsdoc.tags.find((tag) => tag.title === 'default');
if (defaultTag === undefined) {
return undefined;
}
return { value: defaultTag.description || '' };
}
function getDefaultValuesFromProps(
properties: NodePath,
documentation: Documentation,
importer: Importer,
) {
const { props: documentedProps } = documentation.toObject();
const implementedProps: Record<string, NodePath> = {};
properties
.filter((propertyPath: NodePath) => types.Property.check(propertyPath.node), undefined)
.forEach((propertyPath: NodePath) => {
const propName = getPropertyName(propertyPath);
if (propName) {
implementedProps[propName] = propertyPath;
}
});
// Sometimes we list props in .propTypes even though they're implemented by another component
// These props are spread so they won't appear in the component implementation.
Object.entries(documentedProps || []).forEach(([propName, propDescriptor]) => {
if (propDescriptor.description === undefined) {
// private props have no propsType validator and therefore
// not description.
// They are either not subject to eslint react/prop-types
// or are and then we catch these issues during linting.
return;
}
const jsdocDefaultValue = getJsdocDefaultValue(
parseDoctrine(propDescriptor.description, {
sloppy: true,
}),
);
if (jsdocDefaultValue) {
propDescriptor.jsdocDefaultValue = jsdocDefaultValue;
}
const propertyPath = implementedProps[propName];
if (propertyPath !== undefined) {
const defaultValue = getDefaultValue(propertyPath, importer);
if (defaultValue) {
propDescriptor.defaultValue = defaultValue;
}
}
});
}
function getRenderBody(componentDefinition: NodePath, importer: Importer): NodePath {
const value = resolveToValue(componentDefinition, importer);
if (isReactForwardRefCall(value, importer)) {
return value.get('arguments', 0, 'body', 'body');
}
return value.get('body', 'body');
}
/**
* Handle the case where `props` is explicitly declared with/without `React.forwardRef(…)`:
*
* @example
* const Component = React.forwardRef((props, ref) => {
* const { className, ...other } = props;
* })
*/
function getExplicitPropsDeclaration(
componentDefinition: NodePath,
importer: Importer,
): NodePath | undefined {
const functionNode = getRenderBody(componentDefinition, importer);
// No function body available to inspect.
if (!functionNode.value) {
return undefined;
}
let propsPath: NodePath | undefined;
// visitVariableDeclarator, can't use visit body.node since it looses scope information
functionNode
.filter((path: NodePath) => {
return types.VariableDeclaration.check(path.node);
}, undefined)
.forEach((path: NodePath) => {
const declaratorPath = path.get('declarations', 0);
// find `const {} = props`
// but not `const ownerState = props`
if (
declaratorPath.get('init', 'name').value === 'props' &&
declaratorPath.get('id', 'type').value === 'ObjectPattern'
) {
propsPath = declaratorPath.get('id');
}
});
if (!propsPath) {
console.error(`${functionNode.parent.value.id.name}: could not find props declaration to generate jsdoc table. The component declaration should be in this format:
function Component(props: ComponentProps) {
const { ...spreadAsUsual } = props;
...
}
`);
}
return propsPath;
}
const defaultPropsHandler: Handler = (documentation, componentDefinition, importer) => {
const props = getExplicitPropsDeclaration(componentDefinition, importer);
getDefaultValuesFromProps(props?.get('properties') ?? [], documentation, importer);
};
export default defaultPropsHandler;