1
1
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' ) ;
4
7
5
8
/**
6
9
* Iterate through the abstract syntax tree, finding ES6-style exports,
7
10
* and inserting blank comments into documentation.js's processing stream.
8
11
* Through inference steps, these comments gain more information and are automatically
9
12
* documented as well as we can.
10
13
* @param {Object } ast the babel-parsed syntax tree
14
+ * @param {Object } data the name of the file
11
15
* @param {Function } addComment a method that creates a new comment if necessary
12
16
* @returns {Array<Object> } comments
13
17
* @private
14
18
*/
15
- function walkExported ( ast , addComment ) {
19
+ function walkExported ( ast , data , addComment ) {
16
20
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
+ }
17
36
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 ) ;
20
45
}
21
46
22
47
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 ) ;
31
63
}
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
+ } ) ;
38
95
}
39
96
}
40
97
} ) ;
@@ -46,18 +103,153 @@ function hasJSDocComment(path) {
46
103
return path . node . leadingComments && path . node . leadingComments . some ( isJSDocComment ) ;
47
104
}
48
105
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
+ }
52
133
}
53
134
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 ;
59
148
}
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 ;
61
253
}
62
254
63
255
module . exports = walkExported ;
0 commit comments