From 96cc4c988508030f43632da9eed474e63d72e8b3 Mon Sep 17 00:00:00 2001 From: "patrick.lafrance" Date: Sun, 10 Nov 2019 23:25:44 -0500 Subject: [PATCH] Added a acorn parser to infer types from string values and also do pretty formatting with escodegen --- addons/docs/package.json | 4 + .../frameworks/react/propTypes/captions.ts | 6 + .../react/propTypes/generateCode.ts | 25 ++ .../react/propTypes/inspectValue.ts | 34 --- .../react/propTypes/inspection/acornParser.ts | 214 ++++++++++++++++++ .../propTypes/inspection/inspectValue.ts | 16 ++ .../react/propTypes/inspection/types.ts | 69 ++++++ .../react/propTypes/renderDefaultValue.ts | 99 ++++---- .../frameworks/react/propTypes/renderType.ts | 191 +++++++++++----- .../src/frameworks/react/propTypes/types.ts | 4 + addons/docs/src/lib2/formatUtils.ts | 8 - addons/docs/src/lib2/jsdocParser.ts | 155 ++++++++----- addons/docs/src/lib2/types.ts | 1 + .../stories/docgen-tests/types/prop-types.js | 19 +- yarn.lock | 7 + 15 files changed, 635 insertions(+), 217 deletions(-) create mode 100644 addons/docs/src/frameworks/react/propTypes/captions.ts create mode 100644 addons/docs/src/frameworks/react/propTypes/generateCode.ts delete mode 100644 addons/docs/src/frameworks/react/propTypes/inspectValue.ts create mode 100644 addons/docs/src/frameworks/react/propTypes/inspection/acornParser.ts create mode 100644 addons/docs/src/frameworks/react/propTypes/inspection/inspectValue.ts create mode 100644 addons/docs/src/frameworks/react/propTypes/inspection/types.ts delete mode 100644 addons/docs/src/lib2/formatUtils.ts diff --git a/addons/docs/package.json b/addons/docs/package.json index a1da273f81d..400412f4854 100644 --- a/addons/docs/package.json +++ b/addons/docs/package.json @@ -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", diff --git a/addons/docs/src/frameworks/react/propTypes/captions.ts b/addons/docs/src/frameworks/react/propTypes/captions.ts new file mode 100644 index 00000000000..83f968d2300 --- /dev/null +++ b/addons/docs/src/frameworks/react/propTypes/captions.ts @@ -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'; diff --git a/addons/docs/src/frameworks/react/propTypes/generateCode.ts b/addons/docs/src/frameworks/react/propTypes/generateCode.ts new file mode 100644 index 00000000000..39e155f4875 --- /dev/null +++ b/addons/docs/src/frameworks/react/propTypes/generateCode.ts @@ -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); +} diff --git a/addons/docs/src/frameworks/react/propTypes/inspectValue.ts b/addons/docs/src/frameworks/react/propTypes/inspectValue.ts deleted file mode 100644 index a7c7d5e0efb..00000000000 --- a/addons/docs/src/frameworks/react/propTypes/inspectValue.ts +++ /dev/null @@ -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'); -} diff --git a/addons/docs/src/frameworks/react/propTypes/inspection/acornParser.ts b/addons/docs/src/frameworks/react/propTypes/inspection/acornParser.ts new file mode 100644 index 00000000000..cf59fd4234e --- /dev/null +++ b/addons/docs/src/frameworks/react/propTypes/inspection/acornParser.ts @@ -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 { + 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 { + return { + inferedType: { + type: InspectionType.IDENTIFIER, + identifier: extractIdentifierName(identifierNode), + }, + ast: identifierNode, + }; +} + +function parseLiteral(literalNode: estree.Literal): ParsingResult { + return { + inferedType: { type: InspectionType.LITERAL }, + ast: literalNode, + }; +} + +function parseFunction( + funcNode: estree.FunctionExpression | estree.ArrowFunctionExpression +): ParsingResult { + 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 { + 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 { + 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 { + 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 { + return { + inferedType: { type: InspectionType.OBJECT }, + ast: objectNode, + }; +} + +function parseArray(arrayNode: estree.ArrayExpression): ParsingResult { + 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 { + 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 { + const ast = (acornParser.parse(`(${value})`) as unknown) as estree.Program; + + let parsingResult: ParsingResult = { + 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; +} diff --git a/addons/docs/src/frameworks/react/propTypes/inspection/inspectValue.ts b/addons/docs/src/frameworks/react/propTypes/inspection/inspectValue.ts new file mode 100644 index 00000000000..be7e1318ddb --- /dev/null +++ b/addons/docs/src/frameworks/react/propTypes/inspection/inspectValue.ts @@ -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 }; diff --git a/addons/docs/src/frameworks/react/propTypes/inspection/types.ts b/addons/docs/src/frameworks/react/propTypes/inspection/types.ts new file mode 100644 index 00000000000..f5caeb17cca --- /dev/null +++ b/addons/docs/src/frameworks/react/propTypes/inspection/types.ts @@ -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; +} diff --git a/addons/docs/src/frameworks/react/propTypes/renderDefaultValue.ts b/addons/docs/src/frameworks/react/propTypes/renderDefaultValue.ts index eed7e76926b..791367ba3c1 100644 --- a/addons/docs/src/frameworks/react/propTypes/renderDefaultValue.ts +++ b/addons/docs/src/frameworks/react/propTypes/renderDefaultValue.ts @@ -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; } diff --git a/addons/docs/src/frameworks/react/propTypes/renderType.ts b/addons/docs/src/frameworks/react/propTypes/renderType.ts index 35bb1e8bd4e..672dc217ead 100644 --- a/addons/docs/src/frameworks/react/propTypes/renderType.ts +++ b/addons/docs/src/frameworks/react/propTypes/renderType.ts @@ -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
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) }); diff --git a/addons/docs/src/frameworks/react/propTypes/types.ts b/addons/docs/src/frameworks/react/propTypes/types.ts index bf15b454f8a..ec7f5aeda6c 100644 --- a/addons/docs/src/frameworks/react/propTypes/types.ts +++ b/addons/docs/src/frameworks/react/propTypes/types.ts @@ -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', } diff --git a/addons/docs/src/lib2/formatUtils.ts b/addons/docs/src/lib2/formatUtils.ts deleted file mode 100644 index 7d3dd9b3b79..00000000000 --- a/addons/docs/src/lib2/formatUtils.ts +++ /dev/null @@ -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, '␣'); -} diff --git a/addons/docs/src/lib2/jsdocParser.ts b/addons/docs/src/lib2/jsdocParser.ts index 60539ff5a57..2cad883f826 100644 --- a/addons/docs/src/lib2/jsdocParser.ts +++ b/addons/docs/src/lib2/jsdocParser.ts @@ -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') { diff --git a/addons/docs/src/lib2/types.ts b/addons/docs/src/lib2/types.ts index f42244cec9f..0aa87fd584f 100644 --- a/addons/docs/src/lib2/types.ts +++ b/addons/docs/src/lib2/types.ts @@ -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; diff --git a/examples/cra-ts-kitchen-sink/src/stories/docgen-tests/types/prop-types.js b/examples/cra-ts-kitchen-sink/src/stories/docgen-tests/types/prop-types.js index 017f736c736..bcd6246ad96 100644 --- a/examples/cra-ts-kitchen-sink/src/stories/docgen-tests/types/prop-types.js +++ b/examples/cra-ts-kitchen-sink/src/stories/docgen-tests/types/prop-types.js @@ -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:
Hello!
, functionalElement: , + functionalElementInline: () => { + return
Inlined FunctionnalComponent!
; + }, + functionalElementNamedInline: function InlinedFunctionalComponent() { + return
Inlined FunctionnalComponent!
; + }, classElement: , + classElementInline: class InlinedClassComponent extends React.PureComponent { + render() { + return
Inlined ClassComponent!
; + } + }, functionalElementType: FunctionalComponent, classElementType: ClassComponent, instanceOf: new Set(), diff --git a/yarn.lock b/yarn.lock index 94edb2c5200..708ac7a7916 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"