UNPKG

documentation

Version:
295 lines (262 loc) 8.92 kB
'use strict'; var _babelTraverse = require('babel-traverse'); var _babelTraverse2 = _interopRequireDefault(_babelTraverse); var _parse_to_ast = require('../parsers/parse_to_ast'); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } var isJSDocComment = require('../is_jsdoc_comment'); var t = require('babel-types'); var nodePath = require('path'); var fs = require('fs'); var findTarget = require('../infer/finders').findTarget; /** * Iterate through the abstract syntax tree, finding ES6-style exports, * and inserting blank comments into documentation.js's processing stream. * Through inference steps, these comments gain more information and are automatically * documented as well as we can. * @param {Object} ast the babel-parsed syntax tree * @param {Object} data the name of the file * @param {Function} addComment a method that creates a new comment if necessary * @returns {Array<Object>} comments * @private */ function walkExported(ast, data /*: { file: string } */ , addComment) { var newResults = []; var filename = data.file; var dataCache = new Map(); function addBlankComment(data, path, node) { return addComment(data, '', node.loc, path, node.loc, true); } function getComments(data, path) { var comments = (path.node.leadingComments || []).filter(isJSDocComment); if (!comments.length) { // If this is the first declarator we check for comments on the VariableDeclaration. if (t.isVariableDeclarator(path) && path.parentPath.get('declarations')[0] === path) { return getComments(data, path.parentPath); } var added = addBlankComment(data, path, path.node); return added ? [added] : []; } return comments.map(function (comment) { return addComment(data, comment.value, comment.loc, path, path.node.loc, true); }).filter(Boolean); } function addComments(data, path, overrideName) { var comments = getComments(data, path); if (overrideName) { comments.forEach(function (comment) { comment.name = overrideName; }); } newResults.push.apply(newResults, comments); } (0, _babelTraverse2.default)(ast, { Statement(path) { path.skip(); }, ExportDeclaration(path) { var declaration = path.get('declaration'); if (t.isDeclaration(declaration)) { traverseExportedSubtree(declaration, data, addComments); return path.skip(); } if (path.isExportDefaultDeclaration()) { if (declaration.isIdentifier()) { var binding = declaration.scope.getBinding(declaration.node.name); traverseExportedSubtree(binding.path, data, addComments); return path.skip(); } traverseExportedSubtree(declaration, data, addComments); return path.skip(); } if (t.isExportNamedDeclaration(path)) { var specifiers = path.get('specifiers'); var source = path.node.source; var exportKind = path.node.exportKind; specifiers.forEach(function (specifier) { var specData = data; var local = void 0; if (t.isExportDefaultSpecifier(specifier)) { local = 'default'; } else { // ExportSpecifier local = specifier.node.local.name; } var exported = specifier.node.exported.name; var bindingPath = void 0; if (source) { var tmp = findExportDeclaration(dataCache, local, exportKind, filename, source.value); bindingPath = tmp.ast; specData = tmp.data; } else if (exportKind === 'value') { bindingPath = path.scope.getBinding(local).path; } else if (exportKind === 'type') { bindingPath = findLocalType(path.scope, local); } else { throw new Error('Unreachable'); } if (bindingPath === undefined) { throw new Error(`Unable to find the value ${exported} in ${specData.file}`); } traverseExportedSubtree(bindingPath, specData, addComments, exported); }); return path.skip(); } } }); return newResults; } function traverseExportedSubtree(path, data, addComments, overrideName) { var attachCommentPath = path; if (path.parentPath && path.parentPath.isExportDeclaration()) { attachCommentPath = path.parentPath; } addComments(data, attachCommentPath, overrideName); var target = findTarget(path); if (!target) { return; } if (t.isVariableDeclarator(target) && target.has('init')) { target = target.get('init'); } if (target.isClass() || target.isObjectExpression()) { target.traverse({ Property(path) { addComments(data, path); path.skip(); }, Method(path) { // Don't explicitly document constructor methods: their // parameters are output as part of the class itself. if (path.node.kind !== 'constructor') { addComments(data, path); } path.skip(); } }); } } function getCachedData(dataCache, filePath) { var path = filePath; if (!nodePath.extname(path)) { path = require.resolve(path); } var value = dataCache.get(path); if (!value) { var input = fs.readFileSync(path, 'utf-8'); var ast = (0, _parse_to_ast.parseToAst)(input); value = { data: { file: path, source: input }, ast }; dataCache.set(path, value); } return value; } // Loads a module and finds the exported declaration. function findExportDeclaration(dataCache, name, exportKind, referrer, filename) { var depPath = nodePath.resolve(nodePath.dirname(referrer), filename); var tmp = getCachedData(dataCache, depPath); var ast = tmp.ast; var data = tmp.data; var rv = void 0; (0, _babelTraverse2.default)(ast, { Statement(path) { path.skip(); }, ExportDeclaration(path) { if (name === 'default' && path.isExportDefaultDeclaration()) { rv = path.get('declaration'); path.stop(); } else if (path.isExportNamedDeclaration()) { var declaration = path.get('declaration'); if (t.isDeclaration(declaration)) { var bindingName = void 0; if (declaration.isFunctionDeclaration() || declaration.isClassDeclaration() || declaration.isTypeAlias()) { bindingName = declaration.node.id.name; } else if (declaration.isVariableDeclaration()) { // TODO: Multiple declarations. bindingName = declaration.node.declarations[0].id.name; } if (name === bindingName) { rv = declaration; path.stop(); } else { path.skip(); } return; } // export {x as y} // export {x as y} from './file.js' var specifiers = path.get('specifiers'); var source = path.node.source; for (var i = 0; i < specifiers.length; i++) { var specifier = specifiers[i]; var local = void 0, exported = void 0; if (t.isExportDefaultSpecifier(specifier)) { // export x from ... local = 'default'; exported = specifier.node.exported.name; } else { // ExportSpecifier local = specifier.node.local.name; exported = specifier.node.exported.name; } if (exported === name) { if (source) { // export {local as exported} from './file.js'; var _tmp = findExportDeclaration(dataCache, local, exportKind, depPath, source.value); rv = _tmp.ast; data = _tmp.data; if (!rv) { throw new Error(`${name} is not exported by ${depPath}`); } } else { // export {local as exported} if (exportKind === 'value') { rv = path.scope.getBinding(local).path; } else { rv = findLocalType(path.scope, local); } if (!rv) { throw new Error(`${depPath} has no binding for ${name}`); } } path.stop(); return; } } } } }); return { ast: rv, data }; } // Since we cannot use scope.getBinding for types this walks the current scope looking for a // top-level type alias. function findLocalType(scope, local) { var rv = void 0; scope.path.traverse({ Statement(path) { path.skip(); }, TypeAlias(path) { if (path.node.id.name === local) { rv = path; path.stop(); } else { path.skip(); } } }); return rv; } module.exports = walkExported;