Skip to content

Change document-exported to traverse the code #533

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Sep 9, 2016
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
246 changes: 218 additions & 28 deletions lib/extractors/exported.js
Original file line number Diff line number Diff line change
@@ -1,40 +1,93 @@
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');

/**
* 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 {Function} parseToAst funtiont that parses to an ast
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

functiont -> function

* @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(parseToAst, ast, data, addComment) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason parseToAst is passed in is due to CommonJS's lack of cyclic dependencies. However, now that I think of it, I can jsut extract parseToAst to its own module.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is questionable if passing in addComment is worth the complexity here. I'll look at that too.

var newResults = [];
var filename = data.file;

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));
ExportDeclaration: function (path) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could probably have an enter+skip to reduce the amount of traversal done here.

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(parseToAst, 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 +99,155 @@ 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;
var dataCache = Object.create(null);

function getCachedData(parseToAst, 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(parseToAst, name, exportKind, referrer, filename) {
var depPath = nodePath.resolve(nodePath.dirname(referrer), filename);
var tmp = getCachedData(parseToAst, 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(parseToAst, 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