UNPKG

esdoc

Version:

Good Documentation Generator For JavaScript

795 lines (671 loc) 25.1 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); var _colorLogger = require('color-logger'); var _colorLogger2 = _interopRequireDefault(_colorLogger); var _CommentParser = require('../Parser/CommentParser.js'); var _CommentParser2 = _interopRequireDefault(_CommentParser); var _FileDoc = require('../Doc/FileDoc.js'); var _FileDoc2 = _interopRequireDefault(_FileDoc); var _ClassDoc = require('../Doc/ClassDoc.js'); var _ClassDoc2 = _interopRequireDefault(_ClassDoc); var _MethodDoc = require('../Doc/MethodDoc.js'); var _MethodDoc2 = _interopRequireDefault(_MethodDoc); var _ClassPropertyDoc = require('../Doc/ClassPropertyDoc'); var _ClassPropertyDoc2 = _interopRequireDefault(_ClassPropertyDoc); var _MemberDoc = require('../Doc/MemberDoc.js'); var _MemberDoc2 = _interopRequireDefault(_MemberDoc); var _FunctionDoc = require('../Doc/FunctionDoc.js'); var _FunctionDoc2 = _interopRequireDefault(_FunctionDoc); var _VariableDoc = require('../Doc/VariableDoc.js'); var _VariableDoc2 = _interopRequireDefault(_VariableDoc); var _AssignmentDoc = require('../Doc/AssignmentDoc.js'); var _AssignmentDoc2 = _interopRequireDefault(_AssignmentDoc); var _TypedefDoc = require('../Doc/TypedefDoc.js'); var _TypedefDoc2 = _interopRequireDefault(_TypedefDoc); var _ExternalDoc = require('../Doc/ExternalDoc.js'); var _ExternalDoc2 = _interopRequireDefault(_ExternalDoc); var _ASTUtil = require('../Util/ASTUtil.js'); var _ASTUtil2 = _interopRequireDefault(_ASTUtil); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } const already = Symbol('already'); /** * Doc factory class. * * @example * let factory = new DocFactory(ast, pathResolver); * factory.push(node, parentNode); * let results = factory.results; */ 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 _FileDoc2.default(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; 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(/^./, c => c.toLowerCase()); pseudoClassExport = true; break; case 'Identifier': { const varNode = _ASTUtil2.default.findVariableDeclarationAndNewExpressionNode(exportNode.declaration.name, this._ast); if (varNode) { targetClassName = varNode.declarations[0].init.callee.name; targetVariableName = exportNode.declaration.name; pseudoClassExport = true; _ASTUtil2.default.sanitize(varNode); } else { targetClassName = exportNode.declaration.name; pseudoClassExport = false; } break; } default: _colorLogger2.default.w(`unknown export declaration type. type = "${exportNode.declaration.type}"`); break; } const { classNode, exported } = _ASTUtil2.default.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); _ASTUtil2.default.sanitize(classNode); } if (targetVariableName) { const pseudoExportNode2 = this._copy(exportNode); pseudoExportNode2.declaration = _ASTUtil2.default.createVariableDeclarationAndNewExpressionNode(targetVariableName, targetClassName, exportNode.loc); pseudoExportNodes.push(pseudoExportNode2); } _ASTUtil2.default.sanitize(exportNode); } const functionNode = _ASTUtil2.default.findFunctionDeclarationNode(exportNode.declaration.name, this._ast); if (functionNode) { const pseudoExportNode = this._copy(exportNode); pseudoExportNode.declaration = this._copy(functionNode); _ASTUtil2.default.sanitize(exportNode); _ASTUtil2.default.sanitize(functionNode); pseudoExportNodes.push(pseudoExportNode); } const variableNode = _ASTUtil2.default.findVariableDeclarationNode(exportNode.declaration.name, this._ast); if (variableNode) { const pseudoExportNode = this._copy(exportNode); pseudoExportNode.declaration = this._copy(variableNode); _ASTUtil2.default.sanitize(exportNode); _ASTUtil2.default.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 } = _ASTUtil2.default.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; _ASTUtil2.default.sanitize(classNode); } } continue; } for (const specifier of exportNode.specifiers) { if (specifier.type !== 'ExportSpecifier') continue; let targetClassName = null; let pseudoClassExport; const varNode = _ASTUtil2.default.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); _ASTUtil2.default.sanitize(varNode); } else { targetClassName = specifier.exported.name; pseudoClassExport = false; } const { classNode, exported } = _ASTUtil2.default.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); _ASTUtil2.default.sanitize(classNode); } const functionNode = _ASTUtil2.default.findFunctionDeclarationNode(specifier.exported.name, this._ast); if (functionNode) { const pseudoExportNode = this._copy(exportNode); pseudoExportNode.declaration = this._copy(functionNode); pseudoExportNode.leadingComments = null; pseudoExportNode.specifiers = null; _ASTUtil2.default.sanitize(functionNode); pseudoExportNodes.push(pseudoExportNode); } const variableNode = _ASTUtil2.default.findVariableDeclarationNode(specifier.exported.name, this._ast); if (variableNode) { const pseudoExportNode = this._copy(exportNode); pseudoExportNode.declaration = this._copy(variableNode); pseudoExportNode.leadingComments = null; pseudoExportNode.specifiers = null; _ASTUtil2.default.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 (_CommentParser2.default.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 = _CommentParser2.default.parse(comment); let doc; if (comment === lastComment) { doc = this._createDoc(node, tags); } 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; /* eslint-disable max-statements-per-line */ switch (type) { case 'Class': Clazz = _ClassDoc2.default;break; case 'Method': Clazz = _MethodDoc2.default;break; case 'ClassProperty': Clazz = _ClassPropertyDoc2.default;break; case 'Member': Clazz = _MemberDoc2.default;break; case 'Function': Clazz = _FunctionDoc2.default;break; case 'Variable': Clazz = _VariableDoc2.default;break; case 'Assignment': Clazz = _AssignmentDoc2.default;break; case 'Typedef': Clazz = _TypedefDoc2.default;break; case 'External': Clazz = _ExternalDoc2.default;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': return this._decideMethodDefinitionType(node); case 'ClassProperty': 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 }; } else { _colorLogger2.default.w('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 classs 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 }; } else { _colorLogger2.default.w('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)) { _colorLogger2.default.w('this member is not in class.', this._pathResolver.filePath, node); return { type: null, node: null }; } return { type: 'Member', node: node }; } else { return { type: null, node: null }; } } if (!isTop) 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; } } exports.default = DocFactory;