Skip to content

Commit

Permalink
Change document-exported to traverse the code (#533)
Browse files Browse the repository at this point in the history
* Change document-exported to traverse the code

Instead of traversing over all input files this changes
documentExported to traverse from the exports in the input files,
loading, parsing and traversing the module specifier as needed.

Fixes #515

* Move parseToAst to its own module

* Skip non export declarations for speed
  • Loading branch information
arv authored and tmcw committed Sep 9, 2016
1 parent 7809669 commit e11c1ab
Show file tree
Hide file tree
Showing 12 changed files with 1,159 additions and 73 deletions.
7 changes: 6 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,12 @@ function pipeline() {
* @returns {undefined}
*/
function expandInputs(indexes, options, callback) {
var inputFn = (options.polyglot || options.shallow) ? shallow : dependency;
var inputFn;
if (options.polyglot || options.shallow || options.documentExported) {
inputFn = shallow;
} else {
inputFn = dependency;
}
inputFn(indexes, options, callback);
}

Expand Down
5 changes: 3 additions & 2 deletions lib/extractors/comments.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ var traverse = require('babel-traverse').default,
* @param {string} type comment type to find
* @param {boolean} includeContext to include context in the nodes
* @param {Object} ast the babel-parsed syntax tree
* @param {Object} data the filename and the source of the file the comment is in
* @param {Function} addComment a method that creates a new comment if necessary
* @returns {Array<Object>} comments
* @private
*/
function walkComments(type, includeContext, ast, addComment) {
function walkComments(type, includeContext, ast, data, addComment) {
var newResults = [];

traverse(ast, {
Expand All @@ -30,7 +31,7 @@ function walkComments(type, includeContext, ast, addComment) {
* @return {undefined} this emits data
*/
function parseComment(comment) {
newResults.push(addComment(comment.value, comment.loc, path, path.node.loc, includeContext));
newResults.push(addComment(data, comment.value, comment.loc, path, path.node.loc, includeContext));
}

(path.node[type] || [])
Expand Down
248 changes: 220 additions & 28 deletions lib/extractors/exported.js
Original file line number Diff line number Diff line change
@@ -1,40 +1,97 @@
var traverse = require('babel-traverse').default,
isJSDocComment = require('../../lib/is_jsdoc_comment');

isJSDocComment = require('../../lib/is_jsdoc_comment'),
t = require('babel-types'),
nodePath = require('path'),
fs = require('fs'),
parseToAst = require('../parsers/parse_to_ast');

/**
* 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, addComment) {
function walkExported(ast, data, addComment) {
var newResults = [];
var filename = data.file;
var dataCache = Object.create(null);

function addBlankComment(data, path, node) {
return addComment(data, '', node.loc, path, node.loc, true);
}

function getComments(data, path) {
if (!hasJSDocComment(path)) {
return [addBlankComment(data, path, path.node)];
}
return path.node.leadingComments.filter(isJSDocComment).map(function (comment) {
return addComment(data, comment.value, comment.loc, path, path.node.loc, true);
}).filter(Boolean);
}

function addBlankComment(path, node) {
return addComment('', node.loc, path, node.loc, true);
function addComments(data, path, overrideName) {
var comments = getComments(data, path);
if (overrideName) {
comments.forEach(function (comment) {
comment.name = overrideName;
});
}
newResults.push.apply(newResults, comments);
}

traverse(ast, {
enter: function (path) {
if (path.isExportDeclaration()) {
if (!hasJSDocComment(path)) {
if (!path.node.declaration) {
return;
}
const node = path.node.declaration;
newResults.push(addBlankComment(path, node));
Statement: function (path) {
path.skip();
},
ExportDeclaration: function (path) {
var declaration = path.get('declaration');
if (t.isDeclaration(declaration)) {
traverseExportedSubtree(declaration, data, addComments);
}

if (path.isExportDefaultDeclaration()) {
if (declaration.isDeclaration()) {
traverseExportedSubtree(declaration, data, addComments);
} else if (declaration.isIdentifier()) {
var binding = declaration.scope.getBinding(declaration.node.name);
traverseExportedSubtree(binding.path, data, addComments);
}
} else if ((path.isClassProperty() || path.isClassMethod()) &&
!hasJSDocComment(path) && inExportedClass(path)) {
newResults.push(addBlankComment(path, path.node));
} else if ((path.isObjectProperty() || path.isObjectMethod()) &&
!hasJSDocComment(path) && inExportedObject(path)) {
newResults.push(addBlankComment(path, path.node));
}

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, exported;
if (t.isExportDefaultSpecifier(specifier)) {
local ='default';
} else { // ExportSpecifier
local = specifier.node.local.name;
}
exported = specifier.node.exported.name;

var bindingPath;
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');
}

traverseExportedSubtree(bindingPath, specData, addComments, exported);
});
}
}
});
Expand All @@ -46,18 +103,153 @@ function hasJSDocComment(path) {
return path.node.leadingComments && path.node.leadingComments.some(isJSDocComment);
}

function inExportedClass(path) {
var c = path.parentPath.parentPath;
return c.isClass() && c.parentPath.isExportDeclaration();
function traverseExportedSubtree(path, data, addComments, overrideName) {
var attachCommentPath = path;
if (path.parentPath && path.parentPath.isExportDeclaration()) {
attachCommentPath = path.parentPath;
}
addComments(data, attachCommentPath, overrideName);

if (path.isVariableDeclaration()) {
// TODO: How does JSDoc handle multiple declarations?
path = path.get('declarations')[0].get('init');
if (!path) {
return;
}
}

if (path.isClass() || path.isObjectExpression()) {
path.traverse({
Property: function (path) {
addComments(data, path);
path.skip();
},
Method: function (path) {
addComments(data, path);
path.skip();
}
});
}
}

function inExportedObject(path) {
// ObjectExpression -> VariableDeclarator -> VariableDeclaration -> ExportNamedDeclaration
var p = path.parentPath.parentPath;
if (!p.isVariableDeclarator()) {
return false;
function getCachedData(dataCache, path) {
var value = dataCache[path];
if (!value) {
var input = fs.readFileSync(path, 'utf-8');
var ast = parseToAst(input, path);
value = {
data: {
file: path,
source: input
},
ast: ast
};
dataCache[path] = value;
}
return p.parentPath.parentPath.isExportDeclaration();
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;
traverse(ast, {
Statement: function (path) {
path.skip();
},
ExportDeclaration: function (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;
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, 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';
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: 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;
scope.path.traverse({
Statement: function (path) {
path.skip();
},
TypeAlias: function (path) {
if (path.node.id.name === local) {
rv = path;
path.stop();
} else {
path.skip();
}
}
});
return rv;
}

module.exports = walkExported;
Loading

0 comments on commit e11c1ab

Please sign in to comment.