UNPKG

jest-test-gen

Version:

Generator of jest unit test with all imports mocked and tests stub for every class and function exported

469 lines (468 loc) 22.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.parseSourceFile = void 0; const ts = require("typescript"); const debug_1 = require("debug"); const debug = (0, debug_1.default)('jest-test-gen/parse-source-file'); const isNodeJSX = (node) => [ ts.SyntaxKind.JsxElement, ts.SyntaxKind.JsxFragment, ts.SyntaxKind.JsxExpression, ts.SyntaxKind.JsxSelfClosingElement ].includes(node.kind); function parseSourceFile(file) { const result = { imports: [], exportFunctions: [], exportPojos: [], exportClass: undefined, exportComponents: [], components: [], classes: [], functions: [], pojos: [], typeDefinitions: [], interfaceDefinitions: [], propTypesPojo: [], }; walker(file); return result; function walker(node) { switch (node.kind) { case ts.SyntaxKind.ImportDeclaration: debug('walker found import declaration'); importsWalker(node); break; case ts.SyntaxKind.ClassDeclaration: debug('walker found class declaration'); classWalker(node); break; case ts.SyntaxKind.FunctionDeclaration: debug('walker found function declaration'); functionDeclarationWalker(node); break; case ts.SyntaxKind.VariableStatement: debug('walker found variable statement'); variableStatementWalker(node); break; case ts.SyntaxKind.ExportDeclaration: debug('walker found export declaration'); exportDeclarationWalker(node); break; case ts.SyntaxKind.ExportAssignment: debug('walker found export assignment'); exportAssignementWalker(node); break; case ts.SyntaxKind.ExpressionStatement: debug('walker found expression statement'); expressionStatementWalker(node); break; case ts.SyntaxKind.TypeAliasDeclaration: debug('walker found Type Alias Declaration statement'); typeDeclarationWalker(node); break; case ts.SyntaxKind.InterfaceDeclaration: interfaceDeclarationWalker(node); break; default: ts.forEachChild(node, walker); } } function hasAsyncModifier(node) { return node.modifiers ? node.modifiers.some(mod => mod.kind === ts.SyntaxKind.AsyncKeyword) : false; } function hasStaticModifier(node) { return node.modifiers ? node.modifiers.some(mod => mod.kind === ts.SyntaxKind.StaticKeyword) : false; } function hasExportModifier(node) { return node.modifiers ? node.modifiers.some(mod => mod.kind === ts.SyntaxKind.ExportKeyword) : false; } function hasDefaultModifier(node) { return node.modifiers ? node.modifiers.some(mode => mode.kind === ts.SyntaxKind.DefaultKeyword) : false; } function getReactInheritance(node) { let hasReactTypeExpression = (type) => { const outerExpression = type.expression; const outerExpressionText = outerExpression.getText(); return ['PureComponent', 'Component', 'React.PureComponent', 'React.Component'].includes(outerExpressionText); }; if (!node.heritageClauses) { return; } return node.heritageClauses.find(clause => clause.types.some(hasReactTypeExpression)); } function hasReactInheritance(node) { return !!getReactInheritance(node); } function hasJSXChildElement(node) { let hasJSX = false; ts.forEachChild(node, function visitor(child) { if (isNodeJSX(child)) { hasJSX = true; } if (child.getChildren()) { ts.forEachChild(child, visitor); } }); return hasJSX; } function startsWithCapitalOrNoName(name) { if (!name) return true; return !!name.match(/^[A-Z]{1}/); } function parseArgumentTypeIntoComponentPropsMap(fnParamNode) { let compProps = []; if (fnParamNode) { const firstArgType = fnParamNode.type; if (firstArgType) { const tsPropTypeName = firstArgType.typeName.getText(); const maybeTypeDef = findMatchigTypeByName(tsPropTypeName); if (maybeTypeDef) { compProps = parseReactPropsFromTypeDefinition(maybeTypeDef); } } } return compProps; } function parseVariableGenericTypeIntoComponentPropsMap(varChild) { var _a; let compProps = []; if (varChild.type && varChild.type.kind == ts.SyntaxKind.TypeReference) { const typeNode = varChild.type; if (['FunctionComponent', 'React.FunctionComponent', 'FC', 'React.FC'].includes(typeNode.typeName.getText())) { const tsPropType = (_a = typeNode.typeArguments) === null || _a === void 0 ? void 0 : _a[0]; if (tsPropType) { const tsPropTypeName = tsPropType.typeName.getText(); const maybeMatchingTypeDef = findMatchigTypeByName(tsPropTypeName); if (maybeMatchingTypeDef) { compProps = parseReactPropsFromTypeDefinition(maybeMatchingTypeDef); } } } } return compProps; } function classWalker(node) { var _a, _b; const klass = { name: node.name && node.name.escapedText, methods: [], isDefaultExport: hasDefaultModifier(node), }; if (startsWithCapitalOrNoName(klass.name) && hasReactInheritance(node)) { const currComp = { name: klass.name, isFunctional: false, isDefaultExport: hasDefaultModifier(node), props: [], }; //parse type argument to the generic Component interface to extract propTypes const inheritanceClause = getReactInheritance(node); const inheritanceFirstType = inheritanceClause === null || inheritanceClause === void 0 ? void 0 : inheritanceClause.types[0]; if (inheritanceFirstType && ((_a = inheritanceFirstType === null || inheritanceFirstType === void 0 ? void 0 : inheritanceFirstType.typeArguments) === null || _a === void 0 ? void 0 : _a.length)) { const tsPropType = (_b = inheritanceFirstType.typeArguments) === null || _b === void 0 ? void 0 : _b[0]; if (tsPropType) { currComp.tsPropTypeName = tsPropType.typeName.getText(); const maybeMatchingTypeDef = findMatchigTypeByName(currComp.tsPropTypeName); if (maybeMatchingTypeDef) { currComp.props = parseReactPropsFromTypeDefinition(maybeMatchingTypeDef); } } } hasExportModifier(node) ? result.exportComponents.push(currComp) : result.components.push(currComp); return; } ts.forEachChild(node, (child) => { if (child.kind === ts.SyntaxKind.MethodDeclaration) { const methodChild = child; const methodName = methodChild.name ? methodChild.name.escapedText : ''; klass.methods.push({ methodName, params: methodChild.parameters.map(param => param.name.escapedText), isAsync: hasAsyncModifier(methodChild), isStatic: hasStaticModifier(methodChild), }); } }); result.classes.push(klass); if (hasExportModifier(node)) { result.exportClass = klass; } } function importsWalker(node) { const names = []; let importText = ''; if (node.importClause) { importText = node.getText(); ts.forEachChild(node.importClause, (child) => { ts.forEachChild(child, (element) => { names.push(element.getText()); }); }); } result.imports.push({ path: node.moduleSpecifier.getText(), names, importText, }); } function functionDeclarationWalker(node) { const parsedFunction = { name: node.name ? node.name.escapedText : '', params: node.parameters.map(param => param.name.escapedText), isAsync: hasAsyncModifier(node), isDefaultExport: hasDefaultModifier(node) }; debug('function: ', parsedFunction.name, 'isJsx', hasJSXChildElement(node)); if (startsWithCapitalOrNoName(parsedFunction.name) && hasJSXChildElement(node)) { const currComp = { name: parsedFunction.name, isFunctional: true, isDefaultExport: parsedFunction.isDefaultExport, props: [], }; hasExportModifier(node) ? result.exportComponents.push(currComp) : result.components.push(currComp); const firstArg = node.parameters[0]; currComp.props = parseArgumentTypeIntoComponentPropsMap(firstArg); return; } if (hasExportModifier(node)) { result.exportFunctions.push(parsedFunction); } else { result.functions.push(parsedFunction); } } function variableStatementWalker(node) { // check only exported variable statements. if (node.declarationList) { node.declarationList.forEachChild((child) => { //handle arrow function declaration const varChild = child; if (varChild.initializer && (varChild.initializer.kind === ts.SyntaxKind.ArrowFunction || varChild.initializer.kind === ts.SyntaxKind.FunctionExpression)) { const parsedFunction = { name: varChild.name.escapedText, params: varChild.initializer.parameters.map(param => param.name.escapedText), isAsync: hasAsyncModifier(varChild.initializer), isDefaultExport: hasDefaultModifier(varChild.initializer), }; if (startsWithCapitalOrNoName(parsedFunction.name) && hasJSXChildElement(node)) { const currComp = { name: parsedFunction.name, isFunctional: true, isDefaultExport: parsedFunction.isDefaultExport, props: [], }; const firstArg = varChild.initializer.parameters[0]; currComp.props = parseArgumentTypeIntoComponentPropsMap(firstArg); // handle component propTypes definition using FunctionComponent generic type, parse it from first typeArgument if (!currComp.props.length) { currComp.props = parseVariableGenericTypeIntoComponentPropsMap(varChild); } hasExportModifier(node) ? result.exportComponents.push(currComp) : result.components.push(currComp); return; } if (hasExportModifier(node)) { result.exportFunctions.push(parsedFunction); } else { result.functions.push(parsedFunction); } } //handle exported pojo with callable methods if (varChild.initializer && varChild.initializer.kind === ts.SyntaxKind.ObjectLiteralExpression) { const parsedPojo = { name: varChild.name && varChild.name.escapedText, isDefaultExport: hasDefaultModifier(varChild.initializer), methods: [], }; const parsedPropTypePojo = { name: parsedPojo.name, props: [] }; let isPropTypePojo = false; const currLiteralExp = varChild.initializer; currLiteralExp.properties.forEach((propNode) => { if (propNode.kind === ts.SyntaxKind.MethodDeclaration) { const methodNode = propNode; const methodName = methodNode.name ? methodNode.name.escapedText : ''; parsedPojo.methods.push({ methodName, params: methodNode.parameters.map(param => param.name.escapedText), isAsync: hasAsyncModifier(methodNode), isStatic: false, }); } if (propNode.kind === ts.SyntaxKind.PropertyAssignment && propNode.initializer.getText().trim().startsWith('PropTypes')) { isPropTypePojo = true; } }); if (isPropTypePojo) { parsedPropTypePojo.props = parseReactPropTypesFromLiteral(currLiteralExp); result.propTypesPojo.push(parsedPropTypePojo); return; } if (hasExportModifier(node)) { result.exportPojos.push(parsedPojo); } else { result.pojos.push(parsedPojo); } } if (varChild.initializer && varChild.initializer.kind === ts.SyntaxKind.ClassExpression) { const klassExp = { name: varChild.name && varChild.name.escapedText, methods: [], isDefaultExport: false, }; ts.forEachChild(varChild.initializer, (child) => { const methodChild = child; if (child.kind === ts.SyntaxKind.MethodDeclaration) { const methodName = methodChild.name ? methodChild.name.escapedText : ''; klassExp.methods.push({ methodName, params: child.parameters.map(param => param.name.escapedText), isAsync: hasAsyncModifier(child), isStatic: hasStaticModifier(child), }); } }); result.classes.push(klassExp); result.exportClass = klassExp; } }); } } function exportDeclarationWalker(node) { var _a; debug('exportDeclarationWalker', (_a = node.exportClause) === null || _a === void 0 ? void 0 : _a.getFullText()); node.exportClause && node.exportClause.elements.forEach(identifier => { var _a; const asName = identifier.name.escapedText; const idName = ((_a = identifier.propertyName) === null || _a === void 0 ? void 0 : _a.escapedText) || asName; debug('exportDeclarationWalker', idName); const foundClassByIdentifier = result.classes.find(klass => klass.name === idName); if (foundClassByIdentifier) { result.exportClass = Object.assign(Object.assign({}, foundClassByIdentifier), { name: asName }); } const foundFunctionByIdentifier = result.functions.find(func => func.name === idName); if (foundFunctionByIdentifier) { result.exportFunctions.push(Object.assign(Object.assign({}, foundFunctionByIdentifier), { name: asName })); } const foundPojoByIdentifier = result.pojos.find(pojo => pojo.name === idName); if (foundPojoByIdentifier) { result.exportPojos.push(Object.assign(Object.assign({}, foundPojoByIdentifier), { name: asName })); } const foundComponentByIdentifier = result.components.find(component => component.name === idName); if (foundComponentByIdentifier) { result.exportComponents.push(Object.assign(Object.assign({}, foundComponentByIdentifier), { name: asName })); } }); } function exportAssignementWalker(node) { let idName = node.expression.escapedText; if (node.expression.kind === ts.SyntaxKind.CallExpression && node.expression.getFullText().trim().startsWith('React.memo')) { const callExpr = node.expression; if (callExpr.arguments.length && callExpr.arguments[0].kind === ts.SyntaxKind.Identifier) { idName = callExpr.arguments[0].getFullText(); } } const foundClassByIdentifier = result.classes.find(klass => klass.name === idName); if (foundClassByIdentifier) { result.exportClass = Object.assign(Object.assign({}, foundClassByIdentifier), { isDefaultExport: true }); } const foundFunctionByIdentifier = result.functions.find(func => func.name === idName); if (foundFunctionByIdentifier) { result.exportFunctions.push(Object.assign(Object.assign({}, foundFunctionByIdentifier), { isDefaultExport: true })); } const foundPojoByIdentifier = result.pojos.find(pojo => pojo.name === idName); if (foundPojoByIdentifier) { result.exportPojos.push(Object.assign(Object.assign({}, foundPojoByIdentifier), { isDefaultExport: true })); } const foundComponentByIdentifier = result.components.find(component => component.name === idName); if (foundComponentByIdentifier) { result.exportComponents.push(Object.assign(Object.assign({}, foundComponentByIdentifier), { isDefaultExport: true })); } } function expressionStatementWalker(node) { var _a, _b; //look for binary expressions if (((_a = node.expression) === null || _a === void 0 ? void 0 : _a.kind) === ts.SyntaxKind.BinaryExpression) { const binaryExpression = node.expression; const leftExp = binaryExpression.left; const rightExp = binaryExpression.right; const findMatchingComponent = (idName) => { return result.exportComponents.find(component => component.name === idName) || result.components.find(component => component.name === idName); }; if (leftExp.name.escapedText === 'propTypes') { const expText = (_b = leftExp.expression) === null || _b === void 0 ? void 0 : _b.escapedText; const currComponent = findMatchingComponent(expText); if (currComponent) { currComponent.props = parseReactPropTypesFromLiteral(rightExp); } } } } function interfaceDeclarationWalker(node) { result.interfaceDefinitions.push(node); } function typeDeclarationWalker(node) { result.typeDefinitions.push(node); } function findMatchigTypeByName(tsTypeName) { return result.typeDefinitions.find(node => node.name.escapedText === tsTypeName) || result.interfaceDefinitions.find(node => node.name.escapedText === tsTypeName); } function parseReactPropsFromTypeDefinition(node) { var _a, _b; if (node.kind === ts.SyntaxKind.InterfaceDeclaration) { return node.members.map(prop => { var _a; const propDesc = prop; return { name: propDesc.name.escapedText, type: ((_a = propDesc.type) === null || _a === void 0 ? void 0 : _a.getFullText().trim()) || '', isOptional: !!propDesc.questionToken }; }); } if (node.kind === ts.SyntaxKind.TypeAliasDeclaration) { if (node.type.kind === ts.SyntaxKind.TypeLiteral) { return node.type.members.map(prop => { var _a; const propDesc = prop; return { name: propDesc.name.escapedText, type: ((_a = propDesc.type) === null || _a === void 0 ? void 0 : _a.getFullText().trim()) || '', isOptional: !!propDesc.questionToken }; }); } if (node.type.kind === ts.SyntaxKind.TypeReference) { const refNode = node.type; if (refNode.typeName.getFullText().trim() === 'PropTypes.InferProps') { const propTypePojoName = ((_a = refNode.typeArguments) === null || _a === void 0 ? void 0 : _a[0]).exprName.getText(); if (propTypePojoName) { return ((_b = result.propTypesPojo.find(pojo => pojo.name === propTypePojoName)) === null || _b === void 0 ? void 0 : _b.props) || []; } } } } return []; } function parseReactPropTypesFromLiteral(literalObj) { return literalObj.properties.filter(prop => prop.name).map((prop) => { var _a; const fullPropText = prop.initializer.getFullText().trim(); return { name: (_a = prop.name) === null || _a === void 0 ? void 0 : _a.escapedText, type: fullPropText, isOptional: fullPropText.indexOf('isRequired') === -1 }; }); } } exports.parseSourceFile = parseSourceFile;