Skip to content

Commit 04eefb3

Browse files
committed
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 documentationjs#515
1 parent b1f288d commit 04eefb3

11 files changed

+1143
-61
lines changed

index.js

Lines changed: 6 additions & 1 deletion
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

Lines changed: 3 additions & 2 deletions
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

Lines changed: 218 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,93 @@
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');
46

57
/**
68
* Iterate through the abstract syntax tree, finding ES6-style exports,
79
* and inserting blank comments into documentation.js's processing stream.
810
* Through inference steps, these comments gain more information and are automatically
911
* documented as well as we can.
12+
* @param {Function} parseToAst funtiont that parses to an ast
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(parseToAst, ast, data, addComment) {
1620
var newResults = [];
21+
var filename = data.file;
22+
23+
function addBlankComment(data, path, node) {
24+
return addComment(data, '', node.loc, path, node.loc, true);
25+
}
26+
27+
function getComments(data, path) {
28+
if (!hasJSDocComment(path)) {
29+
return [addBlankComment(data, path, path.node)];
30+
}
31+
return path.node.leadingComments.filter(isJSDocComment).map(function (comment) {
32+
return addComment(data, comment.value, comment.loc, path, path.node.loc, true);
33+
}).filter(Boolean);
34+
}
1735

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

2246
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));
47+
ExportDeclaration: function (path) {
48+
var declaration = path.get('declaration');
49+
if (t.isDeclaration(declaration)) {
50+
traverseExportedSubtree(declaration, data, addComments);
51+
}
52+
53+
if (path.isExportDefaultDeclaration()) {
54+
if (declaration.isDeclaration()) {
55+
traverseExportedSubtree(declaration, data, addComments);
56+
} else if (declaration.isIdentifier()) {
57+
var binding = declaration.scope.getBinding(declaration.node.name);
58+
traverseExportedSubtree(binding.path, data, addComments);
3159
}
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));
60+
}
61+
62+
if (t.isExportNamedDeclaration(path)) {
63+
var specifiers = path.get('specifiers');
64+
var source = path.node.source;
65+
var exportKind = path.node.exportKind;
66+
specifiers.forEach(function (specifier) {
67+
var specData = data;
68+
var local, exported;
69+
if (t.isExportDefaultSpecifier(specifier)) {
70+
local ='default';
71+
} else { // ExportSpecifier
72+
local = specifier.node.local.name;
73+
}
74+
exported = specifier.node.exported.name;
75+
76+
var bindingPath;
77+
if (source) {
78+
var tmp = findExportDeclaration(parseToAst, local, exportKind, filename, source.value);
79+
bindingPath = tmp.ast;
80+
specData = tmp.data;
81+
} else if (exportKind === 'value') {
82+
bindingPath = path.scope.getBinding(local).path;
83+
} else if (exportKind === 'type') {
84+
bindingPath = findLocalType(path.scope, local);
85+
} else {
86+
throw new Error('Unreachable');
87+
}
88+
89+
traverseExportedSubtree(bindingPath, specData, addComments, exported);
90+
});
3891
}
3992
}
4093
});
@@ -46,18 +99,155 @@ function hasJSDocComment(path) {
4699
return path.node.leadingComments && path.node.leadingComments.some(isJSDocComment);
47100
}
48101

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

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

63253
module.exports = walkExported;

0 commit comments

Comments
 (0)