UNPKG

@enterthenamehere/esdoc

Version:

Good Documentation Generator For JavaScript, updated for new decade

771 lines (670 loc) 26.1 kB
import CommentParser from '../Parser/CommentParser.js'; import FileDoc from '../Doc/FileDoc.js'; import ClassDoc from '../Doc/ClassDoc.js'; import MethodDoc from '../Doc/MethodDoc.js'; import ClassProperty from '../Doc/ClassPropertyDoc'; import MemberDoc from '../Doc/MemberDoc.js'; import FunctionDoc from '../Doc/FunctionDoc.js'; import VariableDoc from '../Doc/VariableDoc.js'; import AssignmentDoc from '../Doc/AssignmentDoc.js'; import TypedefDoc from '../Doc/TypedefDoc.js'; import ExternalDoc from '../Doc/ExternalDoc.js'; import ASTUtil from '../Util/ASTUtil.js'; const already = Symbol('already'); const debug = require('debug')('ESDoc:DocFactory'); /** * Doc factory class. * * @example * let factory = new DocFactory(ast, pathResolver); * factory.push(node, parentNode); * let results = factory.results; */ export default class DocFactory { /** * @type {DocObject[]} */ get results() { return [...this._results]; } /** * create instance. * @param {AST} ast - AST of source code. * @param {PathResolver} pathResolver - path resolver of source code. */ constructor(ast, pathResolver) { this._ast = ast; this._pathResolver = pathResolver; this._results = []; this._processedClassNodes = []; this._inspectExportDefaultDeclaration(); this._inspectExportNamedDeclaration(); // file doc const doc = new FileDoc(ast, ast, pathResolver, []); this._results.push(doc.value); // ast does not child, so only comment. if (ast.program.body.length === 0 && ast.program.innerComments) { const results = this._traverseComments(ast, null, ast.program.innerComments); this._results.push(...results); } } /** * inspect ExportDefaultDeclaration. * * case1: separated export * * ```javascript * class Foo {} * export default Foo; * ``` * * case2: export instance(directly). * * ```javascript * class Foo {} * export default new Foo(); * ``` * * case3: export instance(indirectly). * * ```javascript * class Foo {} * let foo = new Foo(); * export default foo; * ``` * * @private * @todo support function export. */ _inspectExportDefaultDeclaration() { const pseudoExportNodes = []; for (const exportNode of this._ast.program.body) { if (exportNode.type !== 'ExportDefaultDeclaration') continue; let targetClassName = null; let targetVariableName = null; let pseudoClassExport = false; switch (exportNode.declaration.type) { case 'NewExpression': if (exportNode.declaration.callee.type === 'Identifier') { targetClassName = exportNode.declaration.callee.name; } else if (exportNode.declaration.callee.type === 'MemberExpression') { targetClassName = exportNode.declaration.callee.property.name; } else { targetClassName = ''; } targetVariableName = targetClassName.replace(/^./u, (c) => { return c.toLowerCase(); }); pseudoClassExport = true; break; case 'Identifier': { const varNode = ASTUtil.findVariableDeclarationAndNewExpressionNode(exportNode.declaration.name, this._ast); if (varNode) { targetClassName = varNode.declarations[0].init.callee.name; targetVariableName = exportNode.declaration.name; pseudoClassExport = true; ASTUtil.sanitize(varNode); } else { targetClassName = exportNode.declaration.name; pseudoClassExport = false; } break; } default: debug(`Unknown export declaration type. type = "${exportNode.declaration.type}"`); break; } const {classNode, exported} = ASTUtil.findClassDeclarationNode(targetClassName, this._ast); if (classNode) { if (!exported) { const pseudoExportNode1 = this._copy(exportNode); pseudoExportNode1.declaration = this._copy(classNode); pseudoExportNode1.leadingComments = null; pseudoExportNode1.declaration.__PseudoExport__ = pseudoClassExport; pseudoExportNodes.push(pseudoExportNode1); ASTUtil.sanitize(classNode); } if (targetVariableName) { const pseudoExportNode2 = this._copy(exportNode); pseudoExportNode2.declaration = ASTUtil.createVariableDeclarationAndNewExpressionNode(targetVariableName, targetClassName, exportNode.loc); pseudoExportNodes.push(pseudoExportNode2); } ASTUtil.sanitize(exportNode); } const functionNode = ASTUtil.findFunctionDeclarationNode(exportNode.declaration.name, this._ast); if (functionNode) { const pseudoExportNode = this._copy(exportNode); pseudoExportNode.declaration = this._copy(functionNode); ASTUtil.sanitize(exportNode); ASTUtil.sanitize(functionNode); pseudoExportNodes.push(pseudoExportNode); } const variableNode = ASTUtil.findVariableDeclarationNode(exportNode.declaration.name, this._ast); if (variableNode) { const pseudoExportNode = this._copy(exportNode); pseudoExportNode.declaration = this._copy(variableNode); ASTUtil.sanitize(exportNode); ASTUtil.sanitize(variableNode); pseudoExportNodes.push(pseudoExportNode); } } this._ast.program.body.push(...pseudoExportNodes); } /* eslint-disable max-statements */ /** * inspect ExportNamedDeclaration. * * case1: separated export * * ```javascript * class Foo {} * export {Foo}; * ``` * * case2: export instance(indirectly). * * ```javascript * class Foo {} * let foo = new Foo(); * export {foo}; * ``` * * @private * @todo support function export. */ _inspectExportNamedDeclaration() { const pseudoExportNodes = []; for (const exportNode of this._ast.program.body) { if (exportNode.type !== 'ExportNamedDeclaration') continue; if (exportNode.declaration && exportNode.declaration.type === 'VariableDeclaration') { for (const declaration of exportNode.declaration.declarations) { if (!declaration.init || declaration.init.type !== 'NewExpression') continue; const {classNode, exported} = ASTUtil.findClassDeclarationNode(declaration.init.callee.name, this._ast); if (classNode && !exported) { const pseudoExportNode = this._copy(exportNode); pseudoExportNode.declaration = this._copy(classNode); pseudoExportNode.leadingComments = null; pseudoExportNodes.push(pseudoExportNode); pseudoExportNode.declaration.__PseudoExport__ = true; ASTUtil.sanitize(classNode); } } continue; } for (const specifier of exportNode.specifiers) { if (specifier.type !== 'ExportSpecifier') continue; let targetClassName = null; let pseudoClassExport = false; const varNode = ASTUtil.findVariableDeclarationAndNewExpressionNode(specifier.exported.name, this._ast); if (varNode) { targetClassName = varNode.declarations[0].init.callee.name; pseudoClassExport = true; const pseudoExportNode = this._copy(exportNode); pseudoExportNode.declaration = this._copy(varNode); pseudoExportNode.specifiers = null; pseudoExportNodes.push(pseudoExportNode); ASTUtil.sanitize(varNode); } else { targetClassName = specifier.exported.name; pseudoClassExport = false; } const {classNode, exported} = ASTUtil.findClassDeclarationNode(targetClassName, this._ast); if (classNode && !exported) { const pseudoExportNode = this._copy(exportNode); pseudoExportNode.declaration = this._copy(classNode); pseudoExportNode.leadingComments = null; pseudoExportNode.specifiers = null; pseudoExportNode.declaration.__PseudoExport__ = pseudoClassExport; pseudoExportNodes.push(pseudoExportNode); ASTUtil.sanitize(classNode); } const functionNode = ASTUtil.findFunctionDeclarationNode(specifier.exported.name, this._ast); if (functionNode) { const pseudoExportNode = this._copy(exportNode); pseudoExportNode.declaration = this._copy(functionNode); pseudoExportNode.leadingComments = null; pseudoExportNode.specifiers = null; ASTUtil.sanitize(functionNode); pseudoExportNodes.push(pseudoExportNode); } const variableNode = ASTUtil.findVariableDeclarationNode(specifier.exported.name, this._ast); if (variableNode) { const pseudoExportNode = this._copy(exportNode); pseudoExportNode.declaration = this._copy(variableNode); pseudoExportNode.leadingComments = null; pseudoExportNode.specifiers = null; ASTUtil.sanitize(variableNode); pseudoExportNodes.push(pseudoExportNode); } } } this._ast.program.body.push(...pseudoExportNodes); } /** * push node, and factory processes node. * @param {ASTNode} node - target node. * @param {ASTNode} parentNode - parent node of target node. */ push(node, parentNode) { if (node === this._ast) return; if (node[already]) return; const isLastNodeInParent = this._isLastNodeInParent(node, parentNode); node[already] = true; Reflect.defineProperty(node, 'parent', {value: parentNode}); // unwrap export declaration if (['ExportDefaultDeclaration', 'ExportNamedDeclaration'].includes(node.type)) { parentNode = node; node = this._unwrapExportDeclaration(node); if (!node) return; node[already] = true; Reflect.defineProperty(node, 'parent', {value: parentNode}); } // if node has decorators, leading comments is attached to decorators. if (node.decorators && node.decorators[0].leadingComments) { if (!node.leadingComments || !node.leadingComments.length) { node.leadingComments = node.decorators[0].leadingComments; } } let results = []; results = this._traverseComments(parentNode, node, node.leadingComments); this._results.push(...results); // for trailing comments. // traverse with only last node, because prevent duplication of trailing comments. if (node.trailingComments && isLastNodeInParent) { results = this._traverseComments(parentNode, null, node.trailingComments); this._results.push(...results); } } /** * traverse comments of node, and create doc object. * @param {ASTNode|AST} parentNode - parent of target node. * @param {?ASTNode} node - target node. * @param {ASTNode[]} comments - comment nodes. * @returns {DocObject[]} created doc objects. * @private */ _traverseComments(parentNode, node, comments) { if (!node) { const virtualNode = {}; Reflect.defineProperty(virtualNode, 'parent', {value: parentNode}); node = virtualNode; } if (comments && comments.length) { const temp = []; for (const comment of comments) { if (CommentParser.isESDoc(comment)) temp.push(comment); } comments = temp; } else { comments = []; } if (comments.length === 0) { comments = [{type: 'CommentBlock', value: '* @undocument'}]; } const results = []; const lastComment = comments[comments.length - 1]; for (const comment of comments) { const tags = CommentParser.parse(comment); let doc = null; if (comment === lastComment) { if (node.declarations && node.declarations[0].id.type === "ArrayPattern") { // HACK implementing multiple variables from array pattern. e.g. export cont [a, b] = arr // Uses elementIndex which we add to node so VariableDoc knows which element to pick. const length = node.declarations[0].id.elements.length; for (let ii = 0; ii < length; ii = ii + 1) { if (typeof(node.elementIndex) === 'undefined') { Reflect.defineProperty(node, 'elementIndex', {writable: true}); } node.elementIndex = ii; doc = this._createDoc(node, tags); if (doc) results.push(doc.value); } } else if (node.declarations && node.declarations[0].id.type === 'ObjectPattern') { // HACK implementing multiple variables from object pattern. e.g. export const {a, b} = obj // Uses propertyIndex which we add to node so VariableDoc knows which element to pick. const length = node.declarations[0].id.properties.length; for (let ii = 0; ii < length; ii = ii + 1) { if (typeof(node.propertyIndex) === 'undefined') { Reflect.defineProperty(node, 'propertyIndex', {writable: true}); } node.propertyIndex = ii; doc = this._createDoc( node, tags ); if (doc) results.push(doc.value); } } else { doc = this._createDoc(node, tags); if (doc) results.push(doc.value); } } else { const virtualNode = {}; Reflect.defineProperty(virtualNode, 'parent', {value: parentNode}); doc = this._createDoc(virtualNode, tags); if (doc) results.push(doc.value); } } return results; } /** * create Doc. * @param {ASTNode} node - target node. * @param {Tag[]} tags - tags of target node. * @returns {AbstractDoc} created Doc. * @private */ _createDoc(node, tags) { const result = this._decideType(tags, node); const type = result.type; node = result.node; if (!type) return null; if (type === 'Class') { this._processedClassNodes.push(node); } let Clazz = null; /* eslint-disable max-statements-per-line */ switch (type) { case 'Class': Clazz = ClassDoc; break; case 'Method': Clazz = MethodDoc; break; case 'ClassProperty': Clazz = ClassProperty; break; case 'Member': Clazz = MemberDoc; break; case 'Function': Clazz = FunctionDoc; break; case 'Variable': Clazz = VariableDoc; break; case 'Assignment': Clazz = AssignmentDoc; break; case 'Typedef': Clazz = TypedefDoc; break; case 'External': Clazz = ExternalDoc; break; default: throw new Error(`unexpected type: ${type}`); } if (!Clazz) return null; if (!node.type) node.type = type; return new Clazz(this._ast, node, this._pathResolver, tags); } /** * decide Doc type by using tags and node. * @param {Tag[]} tags - tags of node. * @param {ASTNode} node - target node. * @returns {{type: ?string, node: ?ASTNode}} decided type. * @private */ _decideType(tags, node) { let type = null; for (const tag of tags) { const tagName = tag.tagName; /* eslint-disable default-case */ switch (tagName) { case '@typedef': type = 'Typedef'; break; case '@external': type = 'External'; break; } } if (type) return {type, node}; if (!node) return {type, node}; /* eslint-disable default-case */ switch (node.type) { case 'ClassDeclaration': return this._decideClassDeclarationType(node); case 'ClassMethod': // intended fallthrough case 'ClassPrivateMethod': return this._decideMethodDefinitionType(node); case 'ClassProperty': // intended fallthrough case 'ClassPrivateProperty': return this._decideClassPropertyType(node); case 'ExpressionStatement': return this._decideExpressionStatementType(node); case 'FunctionDeclaration': return this._decideFunctionDeclarationType(node); case 'FunctionExpression': return this._decideFunctionExpressionType(node); case 'VariableDeclaration': return this._decideVariableType(node); case 'AssignmentExpression': return this._decideAssignmentType(node); case 'ArrowFunctionExpression': return this._decideArrowFunctionExpressionType(node); } return {type: null, node: null}; } /** * decide Doc type from class declaration node. * @param {ASTNode} node - target node that is class declaration node. * @returns {{type: string, node: ASTNode}} decided type. * @private */ _decideClassDeclarationType(node) { if (!this._isTopDepthInBody(node, this._ast.program.body)) return {type: null, node: null}; return {type: 'Class', node: node}; } /** * decide Doc type from method definition node. * @param {ASTNode} node - target node that is method definition node. * @returns {{type: ?string, node: ?ASTNode}} decided type. * @private */ _decideMethodDefinitionType(node) { const classNode = this._findUp(node, ['ClassDeclaration', 'ClassExpression']); if (this._processedClassNodes.includes(classNode)) { return {type: 'Method', node: node}; } debug('This method is not in class', node); return {type: null, node: null}; } /** * decide Doc type from class property node. * @param {ASTNode} node - target node that is class property node. * @returns {{type: ?string, node: ?ASTNode}} decided type. * @private */ _decideClassPropertyType(node) { const classNode = this._findUp(node, ['ClassDeclaration', 'ClassExpression']); if (this._processedClassNodes.includes(classNode)) { return {type: 'ClassProperty', node: node}; } debug('This class property is not in class', node); return {type: null, node: null}; } /** * decide Doc type from function declaration node. * @param {ASTNode} node - target node that is function declaration node. * @returns {{type: string, node: ASTNode}} decided type. * @private */ _decideFunctionDeclarationType(node) { if (!this._isTopDepthInBody(node, this._ast.program.body)) return {type: null, node: null}; return {type: 'Function', node: node}; } /** * decide Doc type from function expression node. * babylon 6.11.2 judges`export default async function foo(){}` to be `FunctionExpression`. * I expect `FunctionDeclaration`. this behavior may be bug of babylon. * for now, workaround for it with this method. * @param {ASTNode} node - target node that is function expression node. * @returns {{type: string, node: ASTNode}} decided type. * @private * @todo inspect with newer babylon. */ _decideFunctionExpressionType(node) { if (!node.async) return {type: null, node: null}; if (!this._isTopDepthInBody(node, this._ast.program.body)) return {type: null, node: null}; return {type: 'Function', node: node}; } /** * decide Doc type from arrow function expression node. * @param {ASTNode} node - target node that is arrow function expression node. * @returns {{type: string, node: ASTNode}} decided type. * @private */ _decideArrowFunctionExpressionType(node) { if (!this._isTopDepthInBody(node, this._ast.program.body)) return {type: null, node: null}; return {type: 'Function', node: node}; } /** * decide Doc type from expression statement node. * @param {ASTNode} node - target node that is expression statement node. * @returns {{type: ?string, node: ?ASTNode}} decided type. * @private */ _decideExpressionStatementType(node) { const isTop = this._isTopDepthInBody(node, this._ast.program.body); Reflect.defineProperty(node.expression, 'parent', {value: node}); node = node.expression; node[already] = true; let innerType = ''; let innerNode = []; if (!node.right) return {type: null, node: null}; switch (node.right.type) { case 'FunctionExpression': innerType = 'Function'; break; case 'ClassExpression': innerType = 'Class'; break; default: if (node.left.type === 'MemberExpression' && node.left.object.type === 'ThisExpression') { const classNode = this._findUp(node, ['ClassExpression', 'ClassDeclaration']); if (!this._processedClassNodes.includes(classNode)) { debug('This member is not in class.', this._pathResolver.filePath, node); return {type: null, node: null}; } return {type: 'Member', node: node}; } return {type: null, node: null}; } if (!isTop) return {type: null, node: null}; if( !node.left.id || !node.left.property ) { // TODO: AssignmentExpression's left doesn't have id or property. Check why and what are the consequences. return {type: null, node: null}; } /* eslint-disable prefer-const */ innerNode = node.right; innerNode.id = this._copy(node.left.id || node.left.property); Reflect.defineProperty(innerNode, 'parent', {value: node}); innerNode[already] = true; return {type: innerType, node: innerNode}; } /** * decide Doc type from variable node. * @param {ASTNode} node - target node that is variable node. * @returns {{type: string, node: ASTNode}} decided type. * @private */ _decideVariableType(node) { if (!this._isTopDepthInBody(node, this._ast.program.body)) return {type: null, node: null}; let innerType = null; let innerNode = null; if (!node.declarations[0].init) return {type: innerType, node: innerNode}; switch (node.declarations[0].init.type) { case 'FunctionExpression': innerType = 'Function'; break; case 'ClassExpression': innerType = 'Class'; break; case 'ArrowFunctionExpression': innerType = 'Function'; break; default: return {type: 'Variable', node: node}; } innerNode = node.declarations[0].init; innerNode.id = this._copy(node.declarations[0].id); Reflect.defineProperty(innerNode, 'parent', {value: node}); innerNode[already] = true; return {type: innerType, node: innerNode}; } /** * decide Doc type from assignment node. * @param {ASTNode} node - target node that is assignment node. * @returns {{type: string, node: ASTNode}} decided type. * @private */ _decideAssignmentType(node) { if (!this._isTopDepthInBody(node, this._ast.program.body)) return {type: null, node: null}; let innerType = ''; let innerNode = []; switch (node.right.type) { case 'FunctionExpression': innerType = 'Function'; break; case 'ClassExpression': innerType = 'Class'; break; default: return {type: 'Assignment', node: node}; } /* eslint-disable prefer-const */ innerNode = node.right; innerNode.id = this._copy(node.left.id || node.left.property); Reflect.defineProperty(innerNode, 'parent', {value: node}); innerNode[already] = true; return {type: innerType, node: innerNode}; } /** * unwrap exported node. * @param {ASTNode} node - target node that is export declaration node. * @returns {ASTNode|null} unwrapped child node of exported node. * @private */ _unwrapExportDeclaration(node) { // e.g. `export A from './A.js'` has not declaration if (!node.declaration) return null; const exportedASTNode = node.declaration; if (!exportedASTNode.leadingComments) exportedASTNode.leadingComments = []; exportedASTNode.leadingComments.push(...node.leadingComments || []); if (!exportedASTNode.trailingComments) exportedASTNode.trailingComments = []; exportedASTNode.trailingComments.push(...node.trailingComments || []); return exportedASTNode; } /** * judge node is last in parent. * @param {ASTNode} node - target node. * @param {ASTNode} parentNode - target parent node. * @returns {boolean} if true, the node is last in parent. * @private */ _isLastNodeInParent(node, parentNode) { if (parentNode && parentNode.body) { const lastNode = parentNode.body[parentNode.body.length - 1]; return node === lastNode; } return false; } /** * judge node is top in body. * @param {ASTNode} node - target node. * @param {ASTNode[]} body - target body node. * @returns {boolean} if true, the node is top in body. * @private */ _isTopDepthInBody(node, body) { if (!body) return false; if (!Array.isArray(body)) return false; const parentNode = node.parent; if (['ExportDefaultDeclaration', 'ExportNamedDeclaration'].includes(parentNode.type)) { node = parentNode; } for (const _node of body) { if (node === _node) return true; } return false; } /** * deep copy object. * @param {Object} obj - target object. * @return {Object} copied object. * @private */ _copy(obj) { return JSON.parse(JSON.stringify(obj)); } /** * find node while goes up. * @param {ASTNode} node - start node. * @param {string[]} types - ASTNode types. * @returns {ASTNode|null} found first node. * @private */ _findUp(node, types) { let parent = node.parent; while (parent) { if (types.includes(parent.type)) return parent; parent = parent.parent; } return null; } }