Skip to content

Commit e11c1ab

Browse files
arvtmcw
authored andcommitted
Change document-exported to traverse the code (#533)
* 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
1 parent 7809669 commit e11c1ab

12 files changed

+1159
-73
lines changed

index.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,12 @@ function pipeline() {
5555
* @returns {undefined}
5656
*/
5757
function expandInputs(indexes, options, callback) {
58-
var inputFn = (options.polyglot || options.shallow) ? shallow : dependency;
58+
var inputFn;
59+
if (options.polyglot || options.shallow || options.documentExported) {
60+
inputFn = shallow;
61+
} else {
62+
inputFn = dependency;
63+
}
5964
inputFn(indexes, options, callback);
6065
}
6166

lib/extractors/comments.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@ var traverse = require('babel-traverse').default,
88
* @param {string} type comment type to find
99
* @param {boolean} includeContext to include context in the nodes
1010
* @param {Object} ast the babel-parsed syntax tree
11+
* @param {Object} data the filename and the source of the file the comment is in
1112
* @param {Function} addComment a method that creates a new comment if necessary
1213
* @returns {Array<Object>} comments
1314
* @private
1415
*/
15-
function walkComments(type, includeContext, ast, addComment) {
16+
function walkComments(type, includeContext, ast, data, addComment) {
1617
var newResults = [];
1718

1819
traverse(ast, {
@@ -30,7 +31,7 @@ function walkComments(type, includeContext, ast, addComment) {
3031
* @return {undefined} this emits data
3132
*/
3233
function parseComment(comment) {
33-
newResults.push(addComment(comment.value, comment.loc, path, path.node.loc, includeContext));
34+
newResults.push(addComment(data, comment.value, comment.loc, path, path.node.loc, includeContext));
3435
}
3536

3637
(path.node[type] || [])

lib/extractors/exported.js

+220-28
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,97 @@
11
var traverse = require('babel-traverse').default,
2-
isJSDocComment = require('../../lib/is_jsdoc_comment');
3-
2+
isJSDocComment = require('../../lib/is_jsdoc_comment'),
3+
t = require('babel-types'),
4+
nodePath = require('path'),
5+
fs = require('fs'),
6+
parseToAst = require('../parsers/parse_to_ast');
47

58
/**
69
* Iterate through the abstract syntax tree, finding ES6-style exports,
710
* and inserting blank comments into documentation.js's processing stream.
811
* Through inference steps, these comments gain more information and are automatically
912
* documented as well as we can.
1013
* @param {Object} ast the babel-parsed syntax tree
14+
* @param {Object} data the name of the file
1115
* @param {Function} addComment a method that creates a new comment if necessary
1216
* @returns {Array<Object>} comments
1317
* @private
1418
*/
15-
function walkExported(ast, addComment) {
19+
function walkExported(ast, data, addComment) {
1620
var newResults = [];
21+
var filename = data.file;
22+
var dataCache = Object.create(null);
23+
24+
function addBlankComment(data, path, node) {
25+
return addComment(data, '', node.loc, path, node.loc, true);
26+
}
27+
28+
function getComments(data, path) {
29+
if (!hasJSDocComment(path)) {
30+
return [addBlankComment(data, path, path.node)];
31+
}
32+
return path.node.leadingComments.filter(isJSDocComment).map(function (comment) {
33+
return addComment(data, comment.value, comment.loc, path, path.node.loc, true);
34+
}).filter(Boolean);
35+
}
1736

18-
function addBlankComment(path, node) {
19-
return addComment('', node.loc, path, node.loc, true);
37+
function addComments(data, path, overrideName) {
38+
var comments = getComments(data, path);
39+
if (overrideName) {
40+
comments.forEach(function (comment) {
41+
comment.name = overrideName;
42+
});
43+
}
44+
newResults.push.apply(newResults, comments);
2045
}
2146

2247
traverse(ast, {
23-
enter: function (path) {
24-
if (path.isExportDeclaration()) {
25-
if (!hasJSDocComment(path)) {
26-
if (!path.node.declaration) {
27-
return;
28-
}
29-
const node = path.node.declaration;
30-
newResults.push(addBlankComment(path, node));
48+
Statement: function (path) {
49+
path.skip();
50+
},
51+
ExportDeclaration: function (path) {
52+
var declaration = path.get('declaration');
53+
if (t.isDeclaration(declaration)) {
54+
traverseExportedSubtree(declaration, data, addComments);
55+
}
56+
57+
if (path.isExportDefaultDeclaration()) {
58+
if (declaration.isDeclaration()) {
59+
traverseExportedSubtree(declaration, data, addComments);
60+
} else if (declaration.isIdentifier()) {
61+
var binding = declaration.scope.getBinding(declaration.node.name);
62+
traverseExportedSubtree(binding.path, data, addComments);
3163
}
32-
} else if ((path.isClassProperty() || path.isClassMethod()) &&
33-
!hasJSDocComment(path) && inExportedClass(path)) {
34-
newResults.push(addBlankComment(path, path.node));
35-
} else if ((path.isObjectProperty() || path.isObjectMethod()) &&
36-
!hasJSDocComment(path) && inExportedObject(path)) {
37-
newResults.push(addBlankComment(path, path.node));
64+
}
65+
66+
if (t.isExportNamedDeclaration(path)) {
67+
var specifiers = path.get('specifiers');
68+
var source = path.node.source;
69+
var exportKind = path.node.exportKind;
70+
specifiers.forEach(function (specifier) {
71+
var specData = data;
72+
var local, exported;
73+
if (t.isExportDefaultSpecifier(specifier)) {
74+
local ='default';
75+
} else { // ExportSpecifier
76+
local = specifier.node.local.name;
77+
}
78+
exported = specifier.node.exported.name;
79+
80+
var bindingPath;
81+
if (source) {
82+
var tmp = findExportDeclaration(dataCache, local, exportKind, filename, source.value);
83+
bindingPath = tmp.ast;
84+
specData = tmp.data;
85+
} else if (exportKind === 'value') {
86+
bindingPath = path.scope.getBinding(local).path;
87+
} else if (exportKind === 'type') {
88+
bindingPath = findLocalType(path.scope, local);
89+
} else {
90+
throw new Error('Unreachable');
91+
}
92+
93+
traverseExportedSubtree(bindingPath, specData, addComments, exported);
94+
});
3895
}
3996
}
4097
});
@@ -46,18 +103,153 @@ function hasJSDocComment(path) {
46103
return path.node.leadingComments && path.node.leadingComments.some(isJSDocComment);
47104
}
48105

49-
function inExportedClass(path) {
50-
var c = path.parentPath.parentPath;
51-
return c.isClass() && c.parentPath.isExportDeclaration();
106+
function traverseExportedSubtree(path, data, addComments, overrideName) {
107+
var attachCommentPath = path;
108+
if (path.parentPath && path.parentPath.isExportDeclaration()) {
109+
attachCommentPath = path.parentPath;
110+
}
111+
addComments(data, attachCommentPath, overrideName);
112+
113+
if (path.isVariableDeclaration()) {
114+
// TODO: How does JSDoc handle multiple declarations?
115+
path = path.get('declarations')[0].get('init');
116+
if (!path) {
117+
return;
118+
}
119+
}
120+
121+
if (path.isClass() || path.isObjectExpression()) {
122+
path.traverse({
123+
Property: function (path) {
124+
addComments(data, path);
125+
path.skip();
126+
},
127+
Method: function (path) {
128+
addComments(data, path);
129+
path.skip();
130+
}
131+
});
132+
}
52133
}
53134

54-
function inExportedObject(path) {
55-
// ObjectExpression -> VariableDeclarator -> VariableDeclaration -> ExportNamedDeclaration
56-
var p = path.parentPath.parentPath;
57-
if (!p.isVariableDeclarator()) {
58-
return false;
135+
function getCachedData(dataCache, path) {
136+
var value = dataCache[path];
137+
if (!value) {
138+
var input = fs.readFileSync(path, 'utf-8');
139+
var ast = parseToAst(input, path);
140+
value = {
141+
data: {
142+
file: path,
143+
source: input
144+
},
145+
ast: ast
146+
};
147+
dataCache[path] = value;
59148
}
60-
return p.parentPath.parentPath.isExportDeclaration();
149+
return value;
150+
}
151+
152+
// Loads a module and finds the exported declaration.
153+
function findExportDeclaration(dataCache, name, exportKind, referrer, filename) {
154+
var depPath = nodePath.resolve(nodePath.dirname(referrer), filename);
155+
var tmp = getCachedData(dataCache, depPath);
156+
var ast = tmp.ast;
157+
var data = tmp.data;
158+
159+
var rv;
160+
traverse(ast, {
161+
Statement: function (path) {
162+
path.skip();
163+
},
164+
ExportDeclaration: function (path) {
165+
if (name === 'default' && path.isExportDefaultDeclaration()) {
166+
rv = path.get('declaration');
167+
path.stop();
168+
} else if (path.isExportNamedDeclaration()) {
169+
var declaration = path.get('declaration');
170+
if (t.isDeclaration(declaration)) {
171+
var bindingName;
172+
if (declaration.isFunctionDeclaration() || declaration.isClassDeclaration() ||
173+
declaration.isTypeAlias()) {
174+
bindingName = declaration.node.id.name;
175+
} else if (declaration.isVariableDeclaration()) {
176+
// TODO: Multiple declarations.
177+
bindingName = declaration.node.declarations[0].id.name;
178+
}
179+
if (name === bindingName) {
180+
rv = declaration;
181+
path.stop();
182+
} else {
183+
path.skip();
184+
}
185+
return;
186+
}
187+
188+
// export {x as y}
189+
// export {x as y} from './file.js'
190+
var specifiers = path.get('specifiers');
191+
var source = path.node.source;
192+
for (var i = 0; i < specifiers.length; i++) {
193+
var specifier = specifiers[i];
194+
var local, exported;
195+
if (t.isExportDefaultSpecifier(specifier)) {
196+
// export x from ...
197+
local = 'default';
198+
exported = specifier.node.exported.name;
199+
} else {
200+
// ExportSpecifier
201+
local = specifier.node.local.name;
202+
exported = specifier.node.exported.name;
203+
}
204+
if (exported === name) {
205+
if (source) {
206+
// export {local as exported} from './file.js';
207+
var tmp = findExportDeclaration(dataCache, local, exportKind, depPath, source.value);
208+
rv = tmp.ast;
209+
data = tmp.data;
210+
if (!rv) {
211+
throw new Error(`${name} is not exported by ${depPath}`);
212+
}
213+
} else {
214+
// export {local as exported}
215+
if (exportKind === 'value') {
216+
rv = path.scope.getBinding(local).path;
217+
} else {
218+
rv = findLocalType(path.scope, local);
219+
}
220+
if (!rv) {
221+
throw new Error(`${depPath} has no binding for ${name}`);
222+
}
223+
}
224+
path.stop();
225+
return;
226+
}
227+
}
228+
}
229+
}
230+
});
231+
232+
return {ast: rv, data: data};
233+
}
234+
235+
// Since we cannot use scope.getBinding for types this walks the current scope looking for a
236+
// top-level type alias.
237+
function findLocalType(scope, local) {
238+
var rv;
239+
scope.path.traverse({
240+
Statement: function (path) {
241+
path.skip();
242+
},
243+
TypeAlias: function (path) {
244+
if (path.node.id.name === local) {
245+
rv = path;
246+
path.stop();
247+
} else {
248+
path.skip();
249+
}
250+
}
251+
});
252+
return rv;
61253
}
62254

63255
module.exports = walkExported;

0 commit comments

Comments
 (0)