Added a acorn parser to infer types from string values and also do pretty formatting with escodegen

This commit is contained in:
patrick.lafrance 2019-11-10 23:25:44 -05:00
parent 896ebc8c6e
commit 96cc4c9885
15 changed files with 635 additions and 217 deletions

View File

@ -50,8 +50,12 @@
"@storybook/router": "5.3.0-alpha.40",
"@storybook/source-loader": "5.3.0-alpha.40",
"@storybook/theming": "5.3.0-alpha.40",
"acorn": "^7.1.0",
"acorn-jsx": "^5.1.0",
"acorn-walk": "^7.0.0",
"core-js": "^3.0.1",
"doctrine": "^3.0.0",
"escodegen": "^1.12.0",
"global": "^4.3.2",
"js-string-escape": "^1.0.1",
"lodash": "^4.17.15",

View File

@ -0,0 +1,6 @@
export const CUSTOM_CAPTION = 'custom';
export const OBJECT_CAPTION = 'object';
export const ARRAY_CAPTION = 'array';
export const CLASS_CAPTION = 'class';
export const FUNCTION_CAPTION = 'func';
export const ELEMENT_CAPTION = 'element';

View File

@ -0,0 +1,25 @@
import { generate } from 'escodegen';
const BASIC_OPTIONS = {
format: {
ident: {
style: ' ',
},
semicolons: false,
},
};
const COMPACT_OPTIONS = {
...BASIC_OPTIONS,
format: {
newline: '',
},
};
const PRETTY_OPTIONS = {
...BASIC_OPTIONS,
};
export function generateCode(ast: any, compact = false): string {
return generate(ast, compact ? COMPACT_OPTIONS : PRETTY_OPTIONS);
}

View File

@ -1,34 +0,0 @@
export enum InspectionType {
OBJECT = 'object',
ARRAY = 'array',
FUNCTION = 'func',
ELEMENT = 'element',
STRING = 'string',
}
export interface InspectionResult {
inferedType: InspectionType;
}
export function inspectValue(value: string): InspectionResult {
const trimmedValue = value.trimLeft();
let type = InspectionType.STRING;
if (trimmedValue.startsWith('{')) {
type = InspectionType.OBJECT;
} else if (trimmedValue.startsWith('[')) {
type = InspectionType.ARRAY;
} else if (trimmedValue.startsWith('()') || trimmedValue.startsWith('function')) {
type = mightBeComponent(trimmedValue) ? InspectionType.ELEMENT : InspectionType.FUNCTION;
} else if (trimmedValue.startsWith('class') && mightBeComponent(trimmedValue)) {
type = InspectionType.ELEMENT;
}
return {
inferedType: type,
};
}
function mightBeComponent(value: string): boolean {
return value.includes('React') || value.includes('Component') || value.includes('render');
}

View File

@ -0,0 +1,214 @@
import { Parser } from 'acorn';
// @ts-ignore
import jsx from 'acorn-jsx';
import { isNil } from 'lodash';
import estree from 'estree';
// @ts-ignore
import * as acornWalk from 'acorn-walk';
import {
InspectionType,
InspectionInferedType,
InspectionLiteral,
InspectionElement,
InspectionFunction,
InspectionClass,
InspectionObject,
InspectionUnknown,
InspectionIdentifier,
InspectionArray,
} from './types';
interface ParsingResult<T> {
inferedType: InspectionInferedType;
ast: any;
}
const ACORN_WALK_VISITORS = {
...acornWalk.base,
JSXElement: () => {},
};
const acornParser = Parser.extend(jsx());
// Cannot use "estree.Identifier" type because this function also support "JSXIdentifier".
function extractIdentifierName(identifierNode: any) {
return !isNil(identifierNode) ? identifierNode.name : null;
}
function parseIdentifier(identifierNode: estree.Identifier): ParsingResult<InspectionIdentifier> {
return {
inferedType: {
type: InspectionType.IDENTIFIER,
identifier: extractIdentifierName(identifierNode),
},
ast: identifierNode,
};
}
function parseLiteral(literalNode: estree.Literal): ParsingResult<InspectionLiteral> {
return {
inferedType: { type: InspectionType.LITERAL },
ast: literalNode,
};
}
function parseFunction(
funcNode: estree.FunctionExpression | estree.ArrowFunctionExpression
): ParsingResult<InspectionFunction | InspectionElement> {
let innerJsxElementNode;
// If there is at least a JSXElement in the body of the function, then it's a React component.
acornWalk.simple(
funcNode.body,
{
JSXElement(node: any) {
innerJsxElementNode = node;
},
},
ACORN_WALK_VISITORS
);
const inferedType: InspectionFunction | InspectionElement = {
type: !isNil(innerJsxElementNode) ? InspectionType.ELEMENT : InspectionType.FUNCTION,
isDefinition: true,
isJsx: !isNil(innerJsxElementNode),
};
const identifierName = extractIdentifierName((funcNode as estree.FunctionExpression).id);
if (!isNil(identifierName)) {
inferedType.identifier = identifierName;
}
return {
inferedType,
ast: funcNode,
};
}
function parseClass(
classNode: estree.ClassExpression
): ParsingResult<InspectionClass | InspectionElement> {
let innerJsxElementNode;
// If there is at least a JSXElement in the body of the class, then it's a React component.
acornWalk.simple(
classNode.body,
{
JSXElement(node: any) {
innerJsxElementNode = node;
},
},
ACORN_WALK_VISITORS
);
const inferedType: any = {
type: !isNil(innerJsxElementNode) ? InspectionType.ELEMENT : InspectionType.CLASS,
identifier: extractIdentifierName(classNode.id),
isDefinition: true,
isJsx: !isNil(innerJsxElementNode),
};
return {
inferedType,
ast: classNode,
};
}
function parseJsxElement(jsxElementNode: any): ParsingResult<InspectionElement> {
const inferedType: InspectionElement = {
type: InspectionType.ELEMENT,
isDefinition: false,
isJsx: true,
};
const identifierName = extractIdentifierName(jsxElementNode.openingElement.name);
if (!isNil(identifierName)) {
inferedType.identifier = identifierName;
}
return {
inferedType,
ast: jsxElementNode,
};
}
function parseCall(callNode: estree.CallExpression): ParsingResult<InspectionObject> {
const identifierNode =
callNode.callee.type === 'MemberExpression' ? callNode.callee.property : callNode.callee;
const identifierName = extractIdentifierName(identifierNode);
if (identifierName === 'shape') {
return {
inferedType: { type: InspectionType.OBJECT },
ast: callNode.arguments[0],
};
}
return null;
}
function parseObject(objectNode: estree.ObjectExpression): ParsingResult<InspectionObject> {
return {
inferedType: { type: InspectionType.OBJECT },
ast: objectNode,
};
}
function parseArray(arrayNode: estree.ArrayExpression): ParsingResult<InspectionArray> {
return {
inferedType: { type: InspectionType.ARRAY },
ast: arrayNode,
};
}
// Cannot set "expression" type to "estree.Expression" because the type doesn't include JSX.
function parseExpression(expression: any): ParsingResult<InspectionInferedType> {
switch (expression.type) {
case 'Identifier':
return parseIdentifier(expression);
case 'Literal':
return parseLiteral(expression);
case 'FunctionExpression':
case 'ArrowFunctionExpression':
return parseFunction(expression);
case 'ClassExpression':
return parseClass(expression);
case 'JSXElement':
return parseJsxElement(expression);
case 'CallExpression':
return parseCall(expression);
case 'ObjectExpression':
return parseObject(expression);
case 'ArrayExpression':
return parseArray(expression);
default:
return null;
}
}
export function parse(value: string): ParsingResult<InspectionInferedType> {
const ast = (acornParser.parse(`(${value})`) as unknown) as estree.Program;
let parsingResult: ParsingResult<InspectionUnknown> = {
inferedType: { type: InspectionType.UNKNOWN },
ast,
};
if (!isNil(ast.body[0])) {
const rootNode = ast.body[0];
switch (rootNode.type) {
case 'ExpressionStatement': {
const expressionResult = parseExpression(rootNode.expression);
if (!isNil(expressionResult)) {
parsingResult = expressionResult;
}
break;
}
default:
break;
}
}
return parsingResult;
}

View File

@ -0,0 +1,16 @@
import { parse } from './acornParser';
import { InspectionResult, InspectionType } from './types';
export function inspectValue(value: string): InspectionResult {
try {
const parsingResult = parse(value);
return { ...parsingResult };
} catch (e) {
// do nothing.
}
return { inferedType: { type: InspectionType.UNKNOWN } };
}
export { InspectionType };

View File

@ -0,0 +1,69 @@
export enum InspectionType {
IDENTIFIER = 'Identifier',
LITERAL = 'Literal',
OBJECT = 'Object',
ARRAY = 'Array',
FUNCTION = 'Function',
CLASS = 'Class',
ELEMENT = 'Element',
UNKNOWN = 'Unknown',
}
export interface BaseInspectionInferedType {
type: InspectionType;
}
export interface InspectionIdentifier extends BaseInspectionInferedType {
type: InspectionType.IDENTIFIER;
identifier: string;
}
export interface InspectionLiteral extends BaseInspectionInferedType {
type: InspectionType.LITERAL;
}
export interface InspectionObject extends BaseInspectionInferedType {
type: InspectionType.OBJECT;
}
export interface InspectionArray extends BaseInspectionInferedType {
type: InspectionType.ARRAY;
}
export interface InspectionClass extends BaseInspectionInferedType {
type: InspectionType.CLASS;
identifier: string;
isDefinition: boolean;
}
export interface InspectionFunction extends BaseInspectionInferedType {
type: InspectionType.FUNCTION;
identifier?: string;
isDefinition: boolean;
}
export interface InspectionElement extends BaseInspectionInferedType {
type: InspectionType.ELEMENT;
identifier?: string;
isDefinition: boolean;
isJsx: boolean;
}
export interface InspectionUnknown extends BaseInspectionInferedType {
type: InspectionType.UNKNOWN;
}
export type InspectionInferedType =
| InspectionIdentifier
| InspectionLiteral
| InspectionObject
| InspectionArray
| InspectionClass
| InspectionFunction
| InspectionElement
| InspectionUnknown;
export interface InspectionResult {
inferedType: InspectionInferedType;
ast?: any;
}

View File

@ -3,98 +3,87 @@ import { isNil } from 'lodash';
import { ExtractedProp } from '../../../lib2/extractDocgenProps';
import { PropTypesType } from './types';
import { createPropText } from '../../../lib2/createComponents';
import { InspectionType, inspectValue } from './inspectValue';
import { InspectionType, inspectValue } from './inspection/inspectValue';
import { CUSTOM_CAPTION, OBJECT_CAPTION, FUNCTION_CAPTION } from './captions';
import { generateCode } from './generateCode';
import { InspectionFunction } from './inspection/types';
const MAX_DEFAULT_VALUE_LENGTH = 50;
interface EvaluationResult {
succeeded: boolean;
object?: any;
}
function evaluate(value: string): EvaluationResult {
try {
// Trying to re-build the value to provide a better formatting.
// This is considered safe since this is the code generated by docgen that is evaluated.
// eslint-disable-next-line no-eval
const object = eval(`(${value})`);
return {
succeeded: true,
object,
};
} catch (e) {
// do nothing.
}
return { succeeded: false };
}
function prettifyValue(value: string, { indent = true } = {}): string {
const { succeeded, object } = evaluate(value);
if (succeeded) {
// TODO: Use import objectToString from 'javascript-stringify'.
return indent ? JSON.stringify(object, null, 2) : JSON.stringify(object);
}
return value;
function isTooLongForDefaultValue(value: string): boolean {
return value.length > MAX_DEFAULT_VALUE_LENGTH;
}
function renderCustom(defaultValue: string): ReactNode {
const { inferedType } = inspectValue(defaultValue);
const { inferedType, ast } = inspectValue(defaultValue);
switch (inferedType) {
switch (inferedType.type) {
case InspectionType.OBJECT:
return renderObject(defaultValue);
default:
return defaultValue.length <= MAX_DEFAULT_VALUE_LENGTH
? createPropText(defaultValue)
: createPropText('custom', { title: defaultValue });
return createPropText(CUSTOM_CAPTION, { title: defaultValue });
}
}
function renderAny(defaultValue: string): ReactNode {
const { inferedType } = inspectValue(defaultValue);
return createPropText(defaultValue);
}
function renderObject(defaultValue: string): ReactNode {
if (defaultValue.length <= MAX_DEFAULT_VALUE_LENGTH) {
return createPropText(prettifyValue(defaultValue, { indent: false }));
}
const { ast } = inspectValue(defaultValue);
return createPropText('Object', { title: prettifyValue(defaultValue) });
const prettyCaption = generateCode(ast, true);
return !isTooLongForDefaultValue(prettyCaption)
? createPropText(prettyCaption)
: createPropText(OBJECT_CAPTION, { title: generateCode(ast) });
}
function renderFunc(defaultValue: string): ReactNode {
const { succeeded, object } = evaluate(defaultValue);
const { inferedType, ast } = inspectValue(defaultValue);
const { identifier } = inferedType as InspectionFunction;
if (succeeded) {
const { name } = object;
if (!isNil(name)) {
if (name !== 'anonymous' && name !== '') {
return createPropText(name, { title: defaultValue });
}
}
if (!isNil(identifier)) {
return createPropText(identifier, { title: generateCode(ast) });
}
return defaultValue.length <= MAX_DEFAULT_VALUE_LENGTH
? createPropText(defaultValue)
: createPropText('Function', { title: defaultValue });
const prettyCaption = generateCode(ast, true);
return !isTooLongForDefaultValue(prettyCaption)
? createPropText(prettyCaption)
: createPropText(FUNCTION_CAPTION, { title: generateCode(ast) });
}
function renderElementOrNode(defaultValue: string, defaultCaption: string): ReactNode {
const { inferedType } = inspectValue(defaultValue);
// if ()
return createPropText(defaultValue);
}
export function renderDefaultValue({ docgenInfo }: ExtractedProp): ReactNode {
const { type, defaultValue } = docgenInfo;
if (!isNil(defaultValue)) {
// TODO: Need a .toString() ?
const { value } = defaultValue;
switch (type.name) {
case PropTypesType.CUSTOM:
return renderCustom(value);
case PropTypesType.ANY:
return renderAny(value);
case PropTypesType.SHAPE:
case PropTypesType.OBJECT:
return renderObject(value);
case PropTypesType.FUNC:
return renderFunc(value);
case PropTypesType.ELEMENT:
return renderElementOrNode(value, 'element');
case PropTypesType.NODE:
return renderElementOrNode(value, 'node');
default:
return null;
}

View File

@ -4,9 +4,16 @@ import { ExtractedProp } from '../../../lib2/extractDocgenProps';
import { ExtractedJsDocParam } from '../../../lib2/jsdocParser';
import { createPropText } from '../../../lib2/createComponents';
import { PropTypesType } from './types';
import { InspectionType, inspectValue } from './inspectValue';
// TODO: For shapes, need to somehow add <br /> between values. Does br works in title?
import { InspectionType, inspectValue } from './inspection/inspectValue';
import { generateCode } from './generateCode';
import {
OBJECT_CAPTION,
ARRAY_CAPTION,
CLASS_CAPTION,
FUNCTION_CAPTION,
ELEMENT_CAPTION,
CUSTOM_CAPTION,
} from './captions';
const MAX_CAPTION_LENGTH = 35;
@ -30,10 +37,6 @@ interface TypeDef {
inferedType?: InspectionType;
}
function shortifyPropTypes(value: string): string {
return value.replace(/PropTypes./g, '').replace(/.isRequired/g, '');
}
function createTypeDef({
name,
caption,
@ -53,33 +56,87 @@ function createTypeDef({
};
}
// TODO: Fix "oneOfComplexShapes"
function generateComputedValue(typeName: string, value: string): TypeDef {
const { inferedType } = inspectValue(value);
function shortifyPropTypes(value: string): string {
return value.replace(/PropTypes./g, '').replace(/.isRequired/g, '');
}
return createTypeDef({
name: typeName,
caption: inferedType.toString(),
value: inferedType === InspectionType.OBJECT ? shortifyPropTypes(value) : value,
inferedType,
});
function prettyObject(ast: any, compact = false): string {
return shortifyPropTypes(generateCode(ast, compact));
}
function getCaptionFromInspectionType(type: InspectionType): string {
switch (type) {
case InspectionType.OBJECT:
return OBJECT_CAPTION;
case InspectionType.ARRAY:
return ARRAY_CAPTION;
case InspectionType.CLASS:
return CLASS_CAPTION;
case InspectionType.FUNCTION:
return FUNCTION_CAPTION;
case InspectionType.ELEMENT:
return ELEMENT_CAPTION;
default:
return CUSTOM_CAPTION;
}
}
function isTooLongForCaption(value: string): boolean {
return value.length > MAX_CAPTION_LENGTH;
}
function generateValuesForObjectAst(ast: any): [string, string] {
let caption = prettyObject(ast, true);
let value;
if (!isTooLongForCaption(caption)) {
value = caption;
} else {
caption = OBJECT_CAPTION;
value = prettyObject(ast);
}
return [caption, value];
}
function generateCustom({ raw }: PropType): TypeDef {
if (!isNil(raw)) {
const { inferedType } = inspectValue(raw);
const { inferedType, ast } = inspectValue(raw);
const { type, identifier } = inferedType as any;
const value = inferedType === InspectionType.OBJECT ? shortifyPropTypes(raw) : raw;
let caption;
let value;
switch (type) {
case InspectionType.IDENTIFIER:
case InspectionType.LITERAL:
caption = raw;
break;
case InspectionType.OBJECT: {
const [objectCaption, objectValue] = generateValuesForObjectAst(ast);
caption = objectCaption;
value = objectValue;
break;
}
case InspectionType.ELEMENT:
caption = !isNil(identifier) ? identifier : ELEMENT_CAPTION;
value = raw;
break;
default:
caption = getCaptionFromInspectionType(type);
value = raw;
break;
}
return createTypeDef({
name: PropTypesType.CUSTOM,
caption: value.length <= MAX_CAPTION_LENGTH ? value : 'custom',
caption,
value,
inferedType,
inferedType: type,
});
}
return createTypeDef({ name: PropTypesType.CUSTOM, caption: 'custom' });
return createTypeDef({ name: PropTypesType.CUSTOM, caption: CUSTOM_CAPTION });
}
function generateFuncSignature(
@ -123,13 +180,13 @@ function generateFunc(extractedProp: ExtractedProp): TypeDef {
if (hasParams || hasReturns) {
return createTypeDef({
name: PropTypesType.FUNC,
caption: 'func',
caption: FUNCTION_CAPTION,
value: generateFuncSignature(extractedProp, hasParams, hasReturns),
});
}
}
return createTypeDef({ name: PropTypesType.FUNC, caption: 'func' });
return createTypeDef({ name: PropTypesType.FUNC, caption: FUNCTION_CAPTION });
}
function generateShape(type: PropType, extractedProp: ExtractedProp): TypeDef {
@ -137,12 +194,13 @@ function generateShape(type: PropType, extractedProp: ExtractedProp): TypeDef {
.map((key: string) => `${key}: ${generateType(type.value[key], extractedProp).value}`)
.join(', ');
const shape = `{ ${fields} }`;
const { ast } = inspectValue(`{ ${fields} }`);
const [caption, value] = generateValuesForObjectAst(ast);
return createTypeDef({
name: PropTypesType.SHAPE,
caption: shape.length <= MAX_CAPTION_LENGTH ? shape : 'object',
value: shape,
caption,
value,
});
}
@ -150,16 +208,10 @@ function generateObjectOf(type: PropType, extractedProp: ExtractedProp): TypeDef
const format = (of: string) => `objectOf(${of})`;
// eslint-disable-next-line prefer-const
let { name, caption, value, inferedType } = generateType(type.value, extractedProp);
let { name, caption, value } = generateType(type.value, extractedProp);
if (name === PropTypesType.CUSTOM) {
if (!isNil(inferedType)) {
if (inferedType !== InspectionType.STRING && inferedType !== InspectionType.OBJECT) {
caption = inferedType.toString();
}
}
} else if (name === PropTypesType.SHAPE) {
if (value.length <= MAX_CAPTION_LENGTH) {
if (name === PropTypesType.SHAPE) {
if (!isTooLongForCaption(value)) {
caption = value;
}
}
@ -196,9 +248,31 @@ function generateUnion(type: PropType, extractedProp: ExtractedProp): TypeDef {
}
function generateEnumValue({ value, computed }: EnumValue): TypeDef {
return computed
? generateComputedValue('enumvalue', value)
: createTypeDef({ name: 'enumvalue', caption: value });
if (computed) {
const { inferedType, ast } = inspectValue(value) as any;
const { type } = inferedType;
let caption = getCaptionFromInspectionType(type);
if (
type === InspectionType.FUNCTION ||
type === InspectionType.CLASS ||
type === InspectionType.ELEMENT
) {
if (!isNil(inferedType.identifier)) {
caption = inferedType.identifier;
}
}
return createTypeDef({
name: 'enumvalue',
caption,
value: type === InspectionType.OBJECT ? prettyObject(ast) : value,
inferedType: type,
});
}
return createTypeDef({ name: 'enumvalue', caption: value });
}
function generateEnum(type: PropType): TypeDef {
@ -225,35 +299,32 @@ function generateEnum(type: PropType): TypeDef {
return createTypeDef({ name: PropTypesType.ENUM, caption: type.value });
}
function generateArray(type: PropType, extractedProp: ExtractedProp): TypeDef {
const braceAfter = (of: string) => `${of}[]`;
const braceAround = (of: string) => `[${of}]`;
function braceAfter(of: string): string {
return `${of}[]`;
}
function braceAround(of: string): string {
return `[${of}]`;
}
function createArrayOfObjectTypeDef(caption: string, value: string): TypeDef {
return createTypeDef({
name: PropTypesType.ARRAYOF,
caption: caption === OBJECT_CAPTION ? braceAfter(caption) : braceAround(caption),
value: braceAround(value),
});
}
function generateArray(type: PropType, extractedProp: ExtractedProp): TypeDef {
// eslint-disable-next-line prefer-const
let { name, caption, value, inferedType } = generateType(type.value, extractedProp);
if (name === PropTypesType.CUSTOM) {
if (!isNil(inferedType)) {
if (inferedType !== InspectionType.STRING && inferedType !== InspectionType.OBJECT) {
caption = inferedType.toString();
} else if (inferedType === InspectionType.OBJECT) {
// Brace around inlined objects.
// Show the inlined object if it's short.
caption =
value.length <= MAX_CAPTION_LENGTH
? braceAround(value)
: braceAfter(inferedType.toString());
value = braceAround(value);
return createTypeDef({ name: PropTypesType.ARRAYOF, caption, value });
}
if (inferedType === InspectionType.OBJECT) {
return createArrayOfObjectTypeDef(caption, value);
}
} else if (name === PropTypesType.SHAPE) {
// Brace around objects.
caption = value.length <= MAX_CAPTION_LENGTH ? braceAround(value) : braceAfter(caption);
value = braceAround(value);
return createTypeDef({ name: PropTypesType.ARRAYOF, caption, value });
return createArrayOfObjectTypeDef(caption, value);
}
return createTypeDef({ name: PropTypesType.ARRAYOF, caption: braceAfter(value) });

View File

@ -1,5 +1,6 @@
export enum PropTypesType {
CUSTOM = 'custom',
ANY = 'any',
FUNC = 'func',
SHAPE = 'shape',
OBJECT = 'object',
@ -8,4 +9,7 @@ export enum PropTypesType {
UNION = 'union',
ENUM = 'enum',
ARRAYOF = 'arrayOf',
ELEMENT = 'element',
ELEMENTTYPE = 'elementType',
NODE = 'node',
}

View File

@ -1,8 +0,0 @@
export function unquote(text: string) {
return text && text.replace(/^['"]|['"]$/g, '');
}
// TODO: Might not need this.
export function showSpaces(text: string) {
return text && text.replace(/^\s|\s$/g, '␣');
}

View File

@ -1,15 +1,6 @@
import doctrine, { Annotation } from 'doctrine';
import { isNil } from 'lodash';
export type ParseJsDoc = (value?: string) => JsDocParsingResult;
export interface JsDocParsingResult {
propHasJsDoc: boolean;
ignore: boolean;
description?: string;
extractedTags?: ExtractedJsDoc;
}
export interface ExtractedJsDocParam {
name: string;
type?: any;
@ -30,16 +21,29 @@ export interface ExtractedJsDoc {
ignore: boolean;
}
export interface JsDocParsingOptions {
tags?: string[];
}
export interface JsDocParsingResult {
propHasJsDoc: boolean;
ignore: boolean;
description?: string;
extractedTags?: ExtractedJsDoc;
}
export type ParseJsDoc = (value?: string, options?: JsDocParsingOptions) => JsDocParsingResult;
function containsJsDoc(value?: string): boolean {
return !isNil(value) && value.includes('@');
}
function parse(content: string): Annotation {
function parse(content: string, tags: string[]): Annotation {
let ast;
try {
ast = doctrine.parse(content, {
tags: ['param', 'arg', 'argument', 'returns', 'ignore'],
tags,
sloppy: true,
});
} catch (e) {
@ -52,7 +56,14 @@ function parse(content: string): Annotation {
return ast;
}
export const parseJsDoc: ParseJsDoc = (value?: string) => {
const DEFAULT_OPTIONS = {
tags: ['param', 'arg', 'argument', 'returns', 'ignore'],
};
export const parseJsDoc: ParseJsDoc = (
value?: string,
options: JsDocParsingOptions = DEFAULT_OPTIONS
) => {
if (!containsJsDoc(value)) {
return {
propHasJsDoc: false,
@ -60,7 +71,7 @@ export const parseJsDoc: ParseJsDoc = (value?: string) => {
};
}
const jsDocAst = parse(value);
const jsDocAst = parse(value, options.tags);
const extractedTags = extractJsDocTags(jsDocAst);
if (extractedTags.ignore) {
@ -90,56 +101,84 @@ function extractJsDocTags(ast: doctrine.Annotation): ExtractedJsDoc {
for (let i = 0; i < ast.tags.length; i += 1) {
const tag = ast.tags[i];
// arg & argument are aliases for param.
if (tag.title === 'param' || tag.title === 'arg' || tag.title === 'argument') {
const paramName = tag.name;
// When the @param doesn't have a name but have a type and a description, "null-null" is returned.
if (!isNil(paramName) && paramName !== 'null-null') {
if (isNil(extractedTags.params)) {
extractedTags.params = [];
}
extractedTags.params.push({
name: tag.name,
type: tag.type,
description: tag.description,
getPrettyName: () => {
if (paramName.includes('null')) {
// There is a few cases in which the returned param name contains "null".
// - @param {SyntheticEvent} event- Original SyntheticEvent
// - @param {SyntheticEvent} event.\n@returns {string}
return paramName.replace('-null', '').replace('.null', '');
}
return tag.name;
},
getTypeName: () => {
return !isNil(tag.type) ? extractJsDocTypeName(tag.type) : null;
},
});
}
} else if (tag.title === 'returns') {
if (!isNil(tag.type)) {
extractedTags.returns = {
type: tag.type,
description: tag.description,
getTypeName: () => {
return extractJsDocTypeName(tag.type);
},
};
}
} else if (tag.title === 'ignore') {
if (tag.title === 'ignore') {
extractedTags.ignore = true;
// Once we reach an @ignore tag, there is no point in parsing the other tags since we will not render the prop.
break;
} else {
switch (tag.title) {
// arg & argument are aliases for param.
case 'param':
case 'arg':
case 'argument': {
const paramTag = extractParam(tag);
if (!isNil(paramTag)) {
if (isNil(extractedTags.params)) {
extractedTags.params = [];
}
extractedTags.params.push(paramTag);
}
break;
}
case 'returns': {
const returnsTag = extractReturns(tag);
if (!isNil(returnsTag)) {
extractedTags.returns = returnsTag;
}
break;
}
default:
break;
}
}
}
return extractedTags;
}
function extractJsDocTypeName(type: doctrine.Type): string {
function extractParam(tag: doctrine.Tag): ExtractedJsDocParam {
const paramName = tag.name;
// When the @param doesn't have a name but have a type and a description, "null-null" is returned.
if (!isNil(paramName) && paramName !== 'null-null') {
return {
name: tag.name,
type: tag.type,
description: tag.description,
getPrettyName: () => {
if (paramName.includes('null')) {
// There is a few cases in which the returned param name contains "null".
// - @param {SyntheticEvent} event- Original SyntheticEvent
// - @param {SyntheticEvent} event.\n@returns {string}
return paramName.replace('-null', '').replace('.null', '');
}
return tag.name;
},
getTypeName: () => {
return !isNil(tag.type) ? extractTypeName(tag.type) : null;
},
};
}
return null;
}
function extractReturns(tag: doctrine.Tag): ExtractedJsDocReturns {
if (!isNil(tag.type)) {
return {
type: tag.type,
description: tag.description,
getTypeName: () => {
return extractTypeName(tag.type);
},
};
}
return null;
}
function extractTypeName(type: doctrine.Type): string {
if (type.type === 'NameExpression') {
return type.name;
}
@ -147,7 +186,7 @@ function extractJsDocTypeName(type: doctrine.Type): string {
if (type.type === 'RecordType') {
const recordFields = type.fields.map((field: doctrine.type.FieldType) => {
if (!isNil(field.value)) {
const valueTypeName = extractJsDocTypeName(field.value);
const valueTypeName = extractTypeName(field.value);
return `${field.key}: ${valueTypeName}`;
}
@ -159,7 +198,7 @@ function extractJsDocTypeName(type: doctrine.Type): string {
}
if (type.type === 'UnionType') {
const unionElements = type.elements.map(extractJsDocTypeName);
const unionElements = type.elements.map(extractTypeName);
return `(${unionElements.join('|')})`;
}
@ -172,7 +211,7 @@ function extractJsDocTypeName(type: doctrine.Type): string {
if (type.type === 'TypeApplication') {
if (!isNil(type.expression)) {
if ((type.expression as doctrine.type.NameExpression).name === 'Array') {
const arrayType = extractJsDocTypeName(type.applications[0]);
const arrayType = extractTypeName(type.applications[0]);
return `${arrayType}[]`;
}
@ -184,7 +223,7 @@ function extractJsDocTypeName(type: doctrine.Type): string {
type.type === 'NonNullableType' ||
type.type === 'OptionalType'
) {
return extractJsDocTypeName(type.expression);
return extractTypeName(type.expression);
}
if (type.type === 'AllLiteral') {

View File

@ -3,6 +3,7 @@ import { Component } from '../blocks/shared';
export type PropsExtractor = (component: Component) => PropsTableProps | null;
// TODO: Define proper docgen types and use them all around in addons-doc.
export interface DocgenInfo {
type?: {
name: string;

View File

@ -1,6 +1,6 @@
/* eslint-disable react/no-unused-prop-types */
import React from 'react';
import PropTypes, { string } from 'prop-types';
import PropTypes, { string, shape } from 'prop-types';
const NAMED_OBJECT = {
text: PropTypes.string.isRequired,
@ -57,7 +57,10 @@ PropTypesProps.propTypes = {
symbol: PropTypes.symbol,
node: PropTypes.node,
functionalElement: PropTypes.element,
functionalElementInline: PropTypes.element,
functionalElementNamedInline: PropTypes.element,
classElement: PropTypes.element,
classElementInline: PropTypes.element,
functionalElementType: PropTypes.elementType,
classElementType: PropTypes.elementType,
/**
@ -96,7 +99,7 @@ PropTypesProps.propTypes = {
})
),
}),
PropTypes.shape({ bar: PropTypes.number }),
shape({ bar: PropTypes.number }),
]),
oneOfComplexType: PropTypes.oneOf([NAMED_OBJECT, ANOTHER_OBJECT]),
oneOfComponents: PropTypes.oneOf([FunctionalComponent, ClassComponent]),
@ -166,6 +169,7 @@ PropTypesProps.propTypes = {
objectOfInlineObject: PropTypes.objectOf({
foo: PropTypes.string,
bar: PropTypes.string,
barry: PropTypes.string,
}),
objectOfShortShape: PropTypes.objectOf(
PropTypes.shape({
@ -276,7 +280,18 @@ PropTypesProps.defaultProps = {
symbol: Symbol('Default symbol'),
node: <div>Hello!</div>,
functionalElement: <FunctionalComponent />,
functionalElementInline: () => {
return <div>Inlined FunctionnalComponent!</div>;
},
functionalElementNamedInline: function InlinedFunctionalComponent() {
return <div>Inlined FunctionnalComponent!</div>;
},
classElement: <ClassComponent />,
classElementInline: class InlinedClassComponent extends React.PureComponent {
render() {
return <div>Inlined ClassComponent!</div>;
}
},
functionalElementType: FunctionalComponent,
classElementType: ClassComponent,
instanceOf: new Set(),

View File

@ -4740,6 +4740,13 @@ acorn-jsx@^5.0.0, acorn-jsx@^5.1.0:
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.1.0.tgz#294adb71b57398b0680015f0a38c563ee1db5384"
integrity sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw==
acorn-loose@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/acorn-loose/-/acorn-loose-7.0.0.tgz#a4a6e8d2ae51dd5a8bdbc274b7ce3dd84964d13a"
integrity sha512-TIqpAWkqpdBXfj1XDVBQ/jNbAb6ByGfoqkcz2Pwd8mEHUndxOCw9FR6TqkMCMAr5XV8zYx0+m9GcGjxZzQuA2w==
dependencies:
acorn "^7.0.0"
acorn-node@^1.2.0, acorn-node@^1.3.0, acorn-node@^1.5.2, acorn-node@^1.6.1:
version "1.8.2"
resolved "https://registry.yarnpkg.com/acorn-node/-/acorn-node-1.8.2.tgz#114c95d64539e53dede23de8b9d96df7c7ae2af8"