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' ) ;
4
6
5
7
/**
6
8
* Iterate through the abstract syntax tree, finding ES6-style exports,
7
9
* and inserting blank comments into documentation.js's processing stream.
8
10
* Through inference steps, these comments gain more information and are automatically
9
11
* documented as well as we can.
12
+ * @param {Function } parseToAst funtiont that parses to an ast
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 ( parseToAst , ast , data , addComment ) {
16
20
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
+ }
17
35
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 ) ;
20
44
}
21
45
22
46
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 ) ;
31
59
}
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
+ } ) ;
38
91
}
39
92
}
40
93
} ) ;
@@ -46,18 +99,155 @@ function hasJSDocComment(path) {
46
99
return path . node . leadingComments && path . node . leadingComments . some ( isJSDocComment ) ;
47
100
}
48
101
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
+ }
52
129
}
53
130
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 ;
59
146
}
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 ;
61
251
}
62
252
63
253
module . exports = walkExported ;
0 commit comments