documentation
Version:
a documentation generator
325 lines (298 loc) • 8.99 kB
JavaScript
/* @flow */
import traverse from 'babel-traverse';
const isJSDocComment = require('../is_jsdoc_comment');
const t = require('babel-types');
const nodePath = require('path');
const fs = require('fs');
import { parseToAst } from '../parsers/parse_to_ast';
const 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: Object,
data /*: {
file: string
} */,
addComment: Function
) {
const newResults = [];
const filename = data.file;
const dataCache = new Map();
function addBlankComment(data, path, node) {
return addComment(data, '', node.loc, path, node.loc, true);
}
function getComments(data, path) {
const 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);
}
const 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) {
const comments = getComments(data, path);
if (overrideName) {
comments.forEach(function(comment) {
comment.name = overrideName;
});
}
newResults.push.apply(newResults, comments);
}
traverse(ast, {
Statement(path) {
path.skip();
},
ExportDeclaration(path) {
const declaration = path.get('declaration');
if (t.isDeclaration(declaration)) {
traverseExportedSubtree(declaration, data, addComments);
return path.skip();
}
if (path.isExportDefaultDeclaration()) {
if (declaration.isIdentifier()) {
const 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)) {
const specifiers = path.get('specifiers');
const source = path.node.source;
const exportKind = path.node.exportKind;
specifiers.forEach(specifier => {
let specData = data;
let local;
if (t.isExportDefaultSpecifier(specifier)) {
local = 'default';
} else {
// ExportSpecifier
local = specifier.node.local.name;
}
const exported = specifier.node.exported.name;
let bindingPath;
if (source) {
const 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) {
let attachCommentPath = path;
if (path.parentPath && path.parentPath.isExportDeclaration()) {
attachCommentPath = path.parentPath;
}
addComments(data, attachCommentPath, overrideName);
let 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) {
let path = filePath;
if (!nodePath.extname(path)) {
path = require.resolve(path);
}
let value = dataCache.get(path);
if (!value) {
const input = fs.readFileSync(path, 'utf-8');
const 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
) {
const depPath = nodePath.resolve(nodePath.dirname(referrer), filename);
const tmp = getCachedData(dataCache, depPath);
const ast = tmp.ast;
let data = tmp.data;
let rv;
traverse(ast, {
Statement(path) {
path.skip();
},
ExportDeclaration(path) {
if (name === 'default' && path.isExportDefaultDeclaration()) {
rv = path.get('declaration');
path.stop();
} else if (path.isExportNamedDeclaration()) {
const declaration = path.get('declaration');
if (t.isDeclaration(declaration)) {
let bindingName;
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'
const specifiers = path.get('specifiers');
const source = path.node.source;
for (let i = 0; i < specifiers.length; i++) {
const specifier = specifiers[i];
let local, exported;
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';
const 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) {
let rv;
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;