Files
react-test/packages/api-docs-builder/utils/generatePropDescription.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

212 lines
6.0 KiB
TypeScript

import * as doctrine from 'doctrine';
import * as recast from 'recast';
import { PropTypeDescriptor } from 'react-docgen';
import { escapeCell, removeNewLines } from '../buildApi';
import {
isElementTypeAcceptingRefProp,
isElementAcceptingRefProp,
} from './generatePropTypeDescription';
import { DescribeablePropDescriptor } from './createDescribeableProp';
import { SeeMore } from '../types/utils.types';
function resolveType(type: NonNullable<doctrine.Tag['type']>): string {
if (type.type === 'AllLiteral') {
return 'any';
}
if (type.type === 'VoidLiteral') {
return 'void';
}
if (type.type === 'NullLiteral') {
return 'null';
}
if (type.type === 'UndefinedLiteral') {
return 'undefined';
}
if (type.type === 'TypeApplication') {
return `${resolveType(type.expression)}<${type.applications
.map((typeApplication) => {
return resolveType(typeApplication);
})
.join(', ')}>`;
}
if (type.type === 'UnionType') {
return type.elements.map((t) => resolveType(t)).join(' | ');
}
if (type.type === 'RecordType') {
if (type.fields.length === 0) {
return '{}';
}
return `{ ${type.fields.map((field) => resolveType(field)).join(', ')} }`;
}
if (type.type === 'FieldType') {
return `${type.key}: ${type.value ? resolveType(type.value) : 'any'}`;
}
if ('name' in type) {
return type.name;
}
throw new TypeError(`resolveType for '${type.type}' not implemented`);
}
function getDeprecatedInfo(type: PropTypeDescriptor) {
const marker = /deprecatedPropType\((\r*\n)*\s*PropTypes\./g;
const match = type.raw.match(marker);
const startIndex = type.raw.search(marker);
if (match) {
const offset = match[0].length;
return {
propTypes: type.raw.substring(startIndex + offset, type.raw.indexOf(',')),
explanation: recast.parse(type.raw).program.body[0].expression.arguments[1].value,
};
}
return false;
}
interface PropTemplateDescriptor {
key: string;
description: string;
}
export default function generatePropDescription(
prop: DescribeablePropDescriptor,
propName: string,
): {
deprecated: string;
seeMore?: SeeMore;
jsDocText: string;
signature?: string;
generics?: { name: string; description: string }[];
signatureArgs?: {
name: string;
description: string;
argType?: string;
argTypeDescription?: string;
}[];
signatureReturn?: { name: string; description: string };
requiresRef?: boolean;
} {
const { annotation } = prop;
const type = prop.type;
let deprecated = '';
if (type.name === 'custom') {
const deprecatedInfo = getDeprecatedInfo(type);
if (deprecatedInfo) {
deprecated = `*Deprecated*. ${deprecatedInfo.explanation}<br><br>`;
}
}
const seeTag = annotation.tags.find((tag) => tag.title === 'see');
let seeMore;
if (seeTag && seeTag.description) {
const params = seeTag.description.match(/{@link ([^|| ]*)[|| ]([^}]*)}/);
if (params?.length === 3) {
seeMore = {
description: seeTag.description.replace(/{@link ([^|| ]*)[|| ]([^}]*)}/, '{{link}}'),
link: {
url: params[1],
text: params[2],
},
};
}
}
const jsDocText = escapeCell(annotation.description);
// Split up the parsed tags into 'arguments' and 'returns' parsed objects. If there's no
// 'returns' parsed object (i.e., one with title being 'returns'), make one of type 'void'.
const parsedArgs: readonly doctrine.Tag[] = annotation.tags.filter(
(tag) => tag.title === 'param',
);
let parsedReturns: { description?: string | null; type?: doctrine.Type | null } | undefined =
annotation.tags.find((tag) => tag.title === 'returns');
let signature;
let signatureArgs;
let signatureReturn;
const generics = annotation.tags
.filter((tag) => tag.title === 'template')
.map((template) => {
const [key, description] = template.description?.split(/(?<=^\S+)\s/) || [];
if (!description) {
return null;
}
return { key, description };
})
.filter(Boolean) as PropTemplateDescriptor[];
if (type.name === 'func' && (parsedArgs.length > 0 || parsedReturns !== undefined)) {
parsedReturns = parsedReturns ?? { type: { type: 'VoidLiteral' } };
// Remove new lines from tag descriptions to avoid markdown errors.
annotation.tags.forEach((tag) => {
if (tag.description) {
tag.description = removeNewLines(tag.description);
}
});
const returnType = parsedReturns.type;
if (returnType == null) {
throw new TypeError(
`Function signature for prop '${propName}' has no return type. Try \`@returns void\`. Otherwise it might be a bug with doctrine.`,
);
}
const returnTypeName = resolveType(returnType);
signature = `function(${parsedArgs
.map((tag, index) => {
if (tag.type != null && tag.type.type === 'OptionalType') {
return `${tag.name}?: ${(tag.type.expression as any).name}`;
}
if (tag.type === undefined) {
throw new TypeError(
`In function signature for prop '${propName}' Argument #${index} has no type.`,
);
}
return `${tag.name}: ${resolveType(tag.type!)}`;
})
.join(', ')}) => ${returnTypeName}`;
signatureArgs = parsedArgs
.filter((tag) => tag.description && tag.name)
.map((tag) => {
const generic = generics.find(
(g) => tag.type?.type === 'NameExpression' && tag.type?.name === g.key,
);
return {
name: tag.name!,
description: tag.description!,
argType: generic?.key,
argTypeDescription: generic?.description,
};
});
if (parsedReturns.description) {
signatureReturn = { name: returnTypeName, description: parsedReturns.description };
}
}
const requiresRef =
isElementAcceptingRefProp(type) || isElementTypeAcceptingRefProp(type) || undefined;
return {
deprecated,
seeMore,
jsDocText,
signature,
signatureArgs,
signatureReturn,
requiresRef,
};
}