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
Changes from all commits
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
@@ -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);
}

5 changes: 3 additions & 2 deletions lib/extractors/comments.js
Original file line number Diff line number Diff line change
@@ -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, {
@@ -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] || [])
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) {
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(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);
});
}
}
});
@@ -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;
58 changes: 20 additions & 38 deletions lib/parsers/javascript.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
'use strict';

var babylon = require('babylon'),
extend = require('extend'),
_ = require('lodash'),
var _ = require('lodash'),
parse = require('../../lib/parse'),
walkComments = require('../extractors/comments'),
walkExported = require('../extractors/exported');
walkExported = require('../extractors/exported'),
parseToAst = require('./parse_to_ast');

/**
* Left-pad a string so that it can be sorted lexicographically. We sort
@@ -33,52 +32,35 @@ function leftPad(str, width) {
*/
function parseJavaScript(data, options) {
options = options || {};
var visited = {};
var visited = Object.create(null);

var ast = babylon.parse(data.source, {
allowImportExportEverywhere: true,
sourceType: 'module',
plugins: [
'jsx',
'flow',
'asyncFunctions',
'classConstructorCall',
'doExpressions',
'trailingFunctionCommas',
'objectRestSpread',
'decorators',
'classProperties',
'exportExtensions',
'exponentiationOperator',
'asyncGenerators',
'functionBind',
'functionSent'
]
});
var ast = parseToAst(data.source, data.file);
var addComment = _addComment.bind(null, visited);

var addComment = _addComment.bind(null, visited, data);

return _.flatMap([
return _.flatMap(options.documentExported ? [
walkExported
] : [
walkComments.bind(null, 'leadingComments', true),
walkComments.bind(null, 'innerComments', false),
walkComments.bind(null, 'trailingComments', false),
options.documentExported && walkExported
].filter(Boolean), function (fn) {
return fn(ast, addComment);
walkComments.bind(null, 'trailingComments', false)
], function (fn) {
return fn(ast, data, addComment);
}).filter(Boolean);
}

function _addComment(visited, data, commentValue, commentLoc, path, nodeLoc, includeContext) {
var context = {
loc: extend({}, JSON.parse(JSON.stringify(nodeLoc))),
file: data.file,
sortKey: data.sortKey + ' ' + leftPad(nodeLoc.start.line, 8)
};
// Avoid visiting the same comment twice as a leading
// and trailing node
var key = JSON.stringify(commentLoc);
var key = data.file + ':' + commentLoc.start.line + ':' + commentLoc.start.column;
if (!visited[key]) {
visited[key] = true;

var context = {
loc: nodeLoc,
file: data.file,
sortKey: data.sortKey + ' ' + leftPad(nodeLoc.start.line, 8)
};

if (includeContext) {
// This is non-enumerable so that it doesn't get stringified in
// output; e.g. by the documentation binary.
30 changes: 30 additions & 0 deletions lib/parsers/parse_to_ast.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use strict';

var babylon = require('babylon');

var opts = {
allowImportExportEverywhere: true,
sourceType: 'module',
plugins: [
'jsx',
'flow',
'asyncFunctions',
'classConstructorCall',
'doExpressions',
'trailingFunctionCommas',
'objectRestSpread',
'decorators',
'classProperties',
'exportExtensions',
'exponentiationOperator',
'asyncGenerators',
'functionBind',
'functionSent'
]
};

function parseToAst(source) {
return babylon.parse(source, opts);
}

module.exports = parseToAst;
30 changes: 30 additions & 0 deletions test/fixture/document-exported.input.js
Original file line number Diff line number Diff line change
@@ -17,19 +17,49 @@ export var object = {
func: function() {},
};

/** Should not document this */
class NotExportedClass {
/** Should not document this */
classMethod() {}
/** Should not document this */
get classGetter() {}
/** Should not document this */
set classSetter(v) {}
/** Should not document this */
static staticMethod() {}
/** Should not document this */
static get staticGetter() {}
/** Should not document this */
static set staticSetter(v) {}
}

/** Should not document this */
var notExportedObject = {
/** Should not document this */
method() {},
/** Should not document this */
get getter() {},
/** Should not document this */
set setter(v) {},
/** Should not document this */
prop: 42,
/** Should not document this */
func: function() {},
};

export {x, y3 as y4} from './document-exported/x.js';
export z from './document-exported/z.js';
export y2Default from './document-exported/y.js';

function f1() {}
function f2() {}

export {f1, f2 as f3};

export type T = number;
type T2 = string;
type T3 = string;

export type {T2, T3 as T4};

export type {T5} from './document-exported/x.js';
547 changes: 543 additions & 4 deletions test/fixture/document-exported.output.json

Large diffs are not rendered by default.

34 changes: 34 additions & 0 deletions test/fixture/document-exported.output.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
<!-- Generated by documentation.js. Update this documentation by updating the source code. -->

# z

## zMethod

# x

**Parameters**

- `yparam`

# Class

## classMethod
@@ -22,6 +32,20 @@

- `v`

# T5

# y2Default

# y4

Description of y3

**Parameters**

- `p` **[number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)**

Returns **void**

# object

## prop
@@ -37,3 +61,13 @@
**Parameters**

- `v`

# f1

# f3

# T

# T2

# T4
257 changes: 257 additions & 0 deletions test/fixture/document-exported.output.md.json
Original file line number Diff line number Diff line change
@@ -5,6 +5,73 @@
"type": "html",
"value": "<!-- Generated by documentation.js. Update this documentation by updating the source code. -->"
},
{
"depth": 1,
"type": "heading",
"children": [
{
"type": "text",
"value": "z"
}
]
},
{
"depth": 2,
"type": "heading",
"children": [
{
"type": "text",
"value": "zMethod"
}
]
},
{
"depth": 1,
"type": "heading",
"children": [
{
"type": "text",
"value": "x"
}
]
},
{
"type": "strong",
"children": [
{
"type": "text",
"value": "Parameters"
}
]
},
{
"ordered": false,
"type": "list",
"children": [
{
"type": "listItem",
"children": [
{
"type": "paragraph",
"children": [
{
"type": "inlineCode",
"value": "yparam"
},
{
"type": "text",
"value": " "
},
{
"type": "text",
"value": " "
}
]
}
]
}
]
},
{
"depth": 1,
"type": "heading",
@@ -149,6 +216,146 @@
}
]
},
{
"depth": 1,
"type": "heading",
"children": [
{
"type": "text",
"value": "T5"
}
]
},
{
"depth": 1,
"type": "heading",
"children": [
{
"type": "text",
"value": "y2Default"
}
]
},
{
"depth": 1,
"type": "heading",
"children": [
{
"type": "text",
"value": "y4"
}
]
},
{
"type": "paragraph",
"children": [
{
"type": "text",
"value": "Description of y3",
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 18,
"offset": 17
},
"indent": []
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 18,
"offset": 17
},
"indent": []
}
},
{
"type": "strong",
"children": [
{
"type": "text",
"value": "Parameters"
}
]
},
{
"ordered": false,
"type": "list",
"children": [
{
"type": "listItem",
"children": [
{
"type": "paragraph",
"children": [
{
"type": "inlineCode",
"value": "p"
},
{
"type": "text",
"value": " "
},
{
"type": "strong",
"children": [
{
"href": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number",
"url": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number",
"type": "link",
"children": [
{
"type": "text",
"value": "number"
}
]
}
]
},
{
"type": "text",
"value": " "
}
]
}
]
}
]
},
{
"type": "paragraph",
"children": [
{
"type": "text",
"value": "Returns "
},
{
"type": "strong",
"children": [
{
"type": "text",
"value": "void"
}
]
},
{
"type": "text",
"value": " "
}
]
},
{
"depth": 1,
"type": "heading",
@@ -245,6 +452,56 @@
]
}
]
},
{
"depth": 1,
"type": "heading",
"children": [
{
"type": "text",
"value": "f1"
}
]
},
{
"depth": 1,
"type": "heading",
"children": [
{
"type": "text",
"value": "f3"
}
]
},
{
"depth": 1,
"type": "heading",
"children": [
{
"type": "text",
"value": "T"
}
]
},
{
"depth": 1,
"type": "heading",
"children": [
{
"type": "text",
"value": "T2"
}
]
},
{
"depth": 1,
"type": "heading",
"children": [
{
"type": "text",
"value": "T4"
}
]
}
]
}
3 changes: 3 additions & 0 deletions test/fixture/document-exported/x.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export {y as x, y3} from './y.js';

export type T5 = boolean;
10 changes: 10 additions & 0 deletions test/fixture/document-exported/y.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export function y(yparam) {}

function y2() {}

export default y2;

/** Description of y3 */
function y3(p: number): void {}

export {y3};
3 changes: 3 additions & 0 deletions test/fixture/document-exported/z.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default class z {
zMethod() {}
}