jsduck 3.11.2 → 4.0.beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +1 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +45 -0
- data/README.md +0 -3
- data/Rakefile +5 -2
- data/js-classes/String.js +1 -1
- data/jsduck.gemspec +5 -2
- data/lib/jsduck/accessors.rb +1 -0
- data/lib/jsduck/aggregator.rb +3 -0
- data/lib/jsduck/app.rb +3 -6
- data/lib/jsduck/ast.rb +446 -0
- data/lib/jsduck/class_doc_expander.rb +135 -0
- data/lib/jsduck/css_lexer.rb +1 -1
- data/lib/jsduck/css_parser.rb +8 -11
- data/lib/jsduck/doc_ast.rb +305 -0
- data/lib/jsduck/doc_parser.rb +33 -28
- data/lib/jsduck/doc_type.rb +58 -0
- data/lib/jsduck/esprima.rb +32 -0
- data/lib/jsduck/evaluator.rb +69 -0
- data/lib/jsduck/guides.rb +12 -11
- data/lib/jsduck/inherit_doc.rb +80 -14
- data/lib/jsduck/js_parser.rb +162 -373
- data/lib/jsduck/lexer.rb +1 -1
- data/lib/jsduck/logger.rb +0 -2
- data/lib/jsduck/merger.rb +89 -435
- data/lib/jsduck/options.rb +4 -14
- data/lib/jsduck/serializer.rb +262 -0
- data/lib/jsduck/source_file.rb +5 -18
- data/lib/jsduck/source_file_parser.rb +72 -0
- metadata +33 -9
- data/lib/jsduck/js_literal_builder.rb +0 -21
- data/lib/jsduck/js_literal_parser.rb +0 -106
- data/lib/jsduck/tag/chainable.rb +0 -14
data/lib/jsduck/js_parser.rb
CHANGED
@@ -1,430 +1,219 @@
|
|
1
|
-
require 'jsduck/
|
2
|
-
require 'jsduck/doc_parser'
|
3
|
-
require 'jsduck/js_literal_parser'
|
4
|
-
require 'jsduck/js_literal_builder'
|
1
|
+
require 'jsduck/esprima'
|
5
2
|
|
6
3
|
module JsDuck
|
7
4
|
|
8
|
-
|
5
|
+
# JavaScript parser that internally uses Esprima.js
|
6
|
+
class JsParser
|
7
|
+
|
8
|
+
# Initializes the parser with JavaScript source code to be parsed.
|
9
9
|
def initialize(input, options = {})
|
10
|
-
|
11
|
-
@doc_parser = DocParser.new
|
12
|
-
@docs = []
|
13
|
-
@ext_namespaces = (options[:ext_namespaces] || ["Ext"]).map {|ns| tokenize_ns(ns) }
|
14
|
-
end
|
10
|
+
@input = input
|
15
11
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
#
|
20
|
-
def tokenize_ns(ns)
|
21
|
-
ns.split(".").reduce([]) do |res, x|
|
22
|
-
res << "." unless res.length == 0
|
23
|
-
res << x
|
24
|
-
end
|
12
|
+
# Initialize line number counting
|
13
|
+
@start_index = 0
|
14
|
+
@start_linenr = 1
|
25
15
|
end
|
26
16
|
|
27
|
-
# Parses
|
28
|
-
# each doc-comment there is a hash of three values: the comment
|
29
|
-
# structure created by DocParser, number of the line where the
|
30
|
-
# comment starts, and parsed structure of the code that
|
31
|
-
# immediately follows the comment.
|
17
|
+
# Parses JavaScript source code and returns array of hashes like this:
|
32
18
|
#
|
33
|
-
#
|
34
|
-
#
|
35
|
-
#
|
36
|
-
#
|
37
|
-
#
|
38
|
-
# MyClass.doIt = function(foo, bar) {
|
39
|
-
# }
|
40
|
-
#
|
41
|
-
# The return value of this function will be:
|
42
|
-
#
|
43
|
-
# [
|
44
|
-
# {
|
45
|
-
# :comment => [
|
46
|
-
# {:tagname => :default, :doc => "Method description"},
|
47
|
-
# {:tagname => :return, :type => "Number", :doc => ""},
|
48
|
-
# ],
|
49
|
-
# :linenr => 1,
|
50
|
-
# :code => {
|
51
|
-
# :type => :assignment,
|
52
|
-
# :left => ["MyClass", "doIt"],
|
53
|
-
# :right => {
|
54
|
-
# :type => :function,
|
55
|
-
# :name => nil,
|
56
|
-
# :params => [
|
57
|
-
# {:name => "foo"},
|
58
|
-
# {:name => "bar"}
|
59
|
-
# ]
|
60
|
-
# }
|
19
|
+
# {
|
20
|
+
# :comment => "The contents of the comment",
|
21
|
+
# :code => {...AST data structure for code following the comment...},
|
22
|
+
# :linenr => 12, // Beginning with 1
|
23
|
+
# :type => :doc_comment, // or :plain_comment
|
61
24
|
# }
|
62
|
-
# }
|
63
|
-
# ]
|
64
25
|
#
|
65
26
|
def parse
|
66
|
-
|
67
|
-
if look(:doc_comment)
|
68
|
-
comment = @lex.next(true)
|
69
|
-
@docs << {
|
70
|
-
:comment => @doc_parser.parse(comment[:value]),
|
71
|
-
:linenr => comment[:linenr],
|
72
|
-
:code => code_block
|
73
|
-
}
|
74
|
-
else
|
75
|
-
@lex.next
|
76
|
-
end
|
77
|
-
end
|
78
|
-
@docs
|
79
|
-
end
|
27
|
+
@ast = Esprima.instance.parse(@input)
|
80
28
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
# <code-block> := <function> | <var-declaration> | <ext-define> |
|
85
|
-
# <assignment> | <property-literal>
|
86
|
-
def code_block
|
87
|
-
if look(:function)
|
88
|
-
function
|
89
|
-
elsif look(:var)
|
90
|
-
var_declaration
|
91
|
-
elsif ext_look(:ns, ".", "define", "(", :string)
|
92
|
-
ext_define(:ns, ".", "define", "(", :string)
|
93
|
-
elsif ext_look(:ns, ".", "ClassManager", ".", "create", "(", :string)
|
94
|
-
ext_define(:ns, ".", "ClassManager", ".", "create", "(", :string)
|
95
|
-
elsif look(:ident, ":") || look(:string, ":")
|
96
|
-
property_literal
|
97
|
-
elsif look(",", :ident, ":") || look(",", :string, ":")
|
98
|
-
match(",")
|
99
|
-
property_literal
|
100
|
-
elsif look(:ident) || look(:this)
|
101
|
-
maybe_assignment
|
102
|
-
elsif look(:string)
|
103
|
-
{:type => :assignment, :left => [match(:string)[:value]]}
|
104
|
-
else
|
105
|
-
{:type => :nop}
|
106
|
-
end
|
29
|
+
@ast["comments"] = merge_comments(@ast["comments"])
|
30
|
+
locate_comments
|
107
31
|
end
|
108
32
|
|
109
|
-
|
110
|
-
def function
|
111
|
-
match(:function)
|
112
|
-
return {
|
113
|
-
:type => :function,
|
114
|
-
:name => look(:ident) ? match(:ident)[:value] : "",
|
115
|
-
:params => function_parameters,
|
116
|
-
:body => function_body,
|
117
|
-
}
|
118
|
-
end
|
119
|
-
|
120
|
-
# <ext-emptyfn> := "Ext" "." "emptyFn"
|
121
|
-
def ext_emptyfn
|
122
|
-
match(:ident, ".", "emptyFn")
|
123
|
-
return {
|
124
|
-
:type => :function,
|
125
|
-
:name => "",
|
126
|
-
:params => [],
|
127
|
-
}
|
128
|
-
end
|
33
|
+
private
|
129
34
|
|
130
|
-
#
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
params << {:name => match(",", :ident)[:value]}
|
136
|
-
end
|
137
|
-
match(")")
|
138
|
-
return params
|
139
|
-
end
|
35
|
+
# Merges consecutive line-comments and Establishes links between
|
36
|
+
# comments, so we can easily use comment["next"] to get to the
|
37
|
+
# next comment.
|
38
|
+
def merge_comments(original_comments)
|
39
|
+
result = []
|
140
40
|
|
141
|
-
|
142
|
-
|
143
|
-
match("{")
|
144
|
-
end
|
41
|
+
comment = original_comments[0]
|
42
|
+
i = 0
|
145
43
|
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
maybe_assignment
|
150
|
-
end
|
44
|
+
while comment
|
45
|
+
i += 1
|
46
|
+
next_comment = original_comments[i]
|
151
47
|
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
match(",")
|
163
|
-
right = nil
|
164
|
-
else
|
165
|
-
return {:type => :nop}
|
48
|
+
if next_comment && mergeable?(comment, next_comment)
|
49
|
+
# Merge next comment to current one
|
50
|
+
comment["value"] += "\n" + next_comment["value"]
|
51
|
+
comment["range"][1] = next_comment["range"][1]
|
52
|
+
else
|
53
|
+
# Create a link and continue with next comment
|
54
|
+
comment["next"] = next_comment
|
55
|
+
result << comment
|
56
|
+
comment = next_comment
|
57
|
+
end
|
166
58
|
end
|
167
59
|
|
168
|
-
|
169
|
-
:type => :assignment,
|
170
|
-
:left => left,
|
171
|
-
:right => right,
|
172
|
-
}
|
60
|
+
result
|
173
61
|
end
|
174
62
|
|
175
|
-
#
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
63
|
+
# Two comments can be merged if they are both line-comments and
|
64
|
+
# they are separated only by whitespace (but no newlines)
|
65
|
+
def mergeable?(c1, c2)
|
66
|
+
if c1["type"] == "Line" && c2["type"] == "Line"
|
67
|
+
/\A[ \t]*\Z/ =~ @input.slice((c1["range"][1])..(c2["range"][0]-1))
|
180
68
|
else
|
181
|
-
|
182
|
-
end
|
183
|
-
|
184
|
-
while look(".", :ident) do
|
185
|
-
chain << match(".", :ident)[:value]
|
69
|
+
false
|
186
70
|
end
|
187
|
-
return chain
|
188
71
|
end
|
189
72
|
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
else
|
201
|
-
my_literal
|
202
|
-
end
|
203
|
-
end
|
73
|
+
def locate_comments
|
74
|
+
@ast["comments"].map do |comment|
|
75
|
+
# Detect comment type and strip * at the beginning of doc-comment
|
76
|
+
value = comment["value"]
|
77
|
+
if comment["type"] == "Block" && value =~ /\A\*/
|
78
|
+
type = :doc_comment
|
79
|
+
value = value.slice(1, value.length-1)
|
80
|
+
else
|
81
|
+
type = :plain_comment
|
82
|
+
end
|
204
83
|
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
:string => "String",
|
212
|
-
:number => "Number",
|
213
|
-
:regex => "RegExp",
|
214
|
-
:array => "Array",
|
215
|
-
:object => "Object",
|
216
|
-
}
|
217
|
-
|
218
|
-
if cls_map[lit[:type]]
|
219
|
-
cls = cls_map[lit[:type]]
|
220
|
-
elsif lit[:type] == :ident && (lit[:value] == "true" || lit[:value] == "false")
|
221
|
-
cls = "Boolean"
|
222
|
-
else
|
223
|
-
cls = nil
|
84
|
+
{
|
85
|
+
:comment => value,
|
86
|
+
:code => stuff_after(comment),
|
87
|
+
:linenr => line_number(comment["range"][0]),
|
88
|
+
:type => type,
|
89
|
+
}
|
224
90
|
end
|
225
|
-
|
226
|
-
value = JsLiteralBuilder.new.to_s(lit)
|
227
|
-
|
228
|
-
{:type => :literal, :class => cls, :value => value}
|
229
|
-
end
|
230
|
-
|
231
|
-
# True when we're at the end of literal expression.
|
232
|
-
# ",", ";" and "}" are the normal closing symbols, but for
|
233
|
-
# our docs purposes doc-comment and file end work too.
|
234
|
-
def literal_expression_end?
|
235
|
-
look(",") || look(";") || look("}") || look(:doc_comment) || @lex.empty?
|
236
91
|
end
|
237
92
|
|
238
|
-
#
|
239
|
-
def
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
93
|
+
# Given index inside input string, returns the corresponding line number
|
94
|
+
def line_number(index)
|
95
|
+
# To speed things up, remember the index until which we counted,
|
96
|
+
# then next time just begin counting from there. This way we
|
97
|
+
# only count each line once.
|
98
|
+
@start_linenr = @input[@start_index...index].count("\n") + @start_linenr
|
99
|
+
@start_index = index
|
100
|
+
return @start_linenr
|
245
101
|
end
|
246
102
|
|
247
|
-
#
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
}
|
255
|
-
end
|
256
|
-
|
257
|
-
# <ext-define> := "Ext" "." ["define" | "ClassManager" "." "create" ] "(" <string> "," <ext-define-cfg>
|
258
|
-
def ext_define(*pattern)
|
259
|
-
name = ext_match(*pattern)[:value]
|
260
|
-
|
261
|
-
if look(",", "{")
|
262
|
-
match(",")
|
263
|
-
cfg = ext_define_cfg
|
103
|
+
# Sees if there is some code following the comment.
|
104
|
+
# Returns the code found. But if the comment is instead
|
105
|
+
# followed by another comment, returns nil.
|
106
|
+
def stuff_after(comment)
|
107
|
+
code = code_after(comment["range"], @ast)
|
108
|
+
if code && comment["next"]
|
109
|
+
return code["range"][0] < comment["next"]["range"][0] ? code : nil
|
264
110
|
else
|
265
|
-
|
111
|
+
code
|
266
112
|
end
|
267
|
-
|
268
|
-
cfg[:type] = :ext_define
|
269
|
-
cfg[:name] = name
|
270
|
-
|
271
|
-
cfg
|
272
113
|
end
|
273
114
|
|
274
|
-
#
|
275
|
-
#
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
elsif
|
287
|
-
|
288
|
-
|
289
|
-
cfg[:alias] = found
|
290
|
-
elsif found = ext_define_xtype
|
291
|
-
cfg[:xtype] = found
|
292
|
-
elsif found = ext_define_requires
|
293
|
-
cfg[:requires] = found
|
294
|
-
elsif found = ext_define_uses
|
295
|
-
cfg[:uses] = found
|
296
|
-
elsif found = ext_define_singleton
|
297
|
-
cfg[:singleton] = found
|
298
|
-
elsif found = ext_define_whatever
|
299
|
-
# ignore this
|
115
|
+
# Looks for code following the given range.
|
116
|
+
#
|
117
|
+
# The second argument is the parent node within which we perform
|
118
|
+
# our search.
|
119
|
+
def code_after(range, parent)
|
120
|
+
# Look through all child nodes of parent...
|
121
|
+
child_nodes(parent).each do |node|
|
122
|
+
if less(range, node["range"])
|
123
|
+
# If node is after our range, then that's it. There could
|
124
|
+
# be comments in our way, but that's taken care of in
|
125
|
+
# #stuff_after method.
|
126
|
+
return node
|
127
|
+
elsif within(range, node["range"])
|
128
|
+
# Our range is within the node --> recurse
|
129
|
+
return code_after(range, node)
|
300
130
|
end
|
301
|
-
match(",") if look(",")
|
302
131
|
end
|
303
|
-
cfg
|
304
|
-
end
|
305
132
|
|
306
|
-
|
307
|
-
def ext_define_extend
|
308
|
-
if look("extend", ":", :string)
|
309
|
-
match("extend", ":", :string)[:value]
|
310
|
-
end
|
133
|
+
return nil
|
311
134
|
end
|
312
135
|
|
313
|
-
# <mixins> := "mixins" ":" [ <object-literal> | <array-literal> ]
|
314
|
-
def ext_define_mixins
|
315
|
-
if look("mixins", ":")
|
316
|
-
match("mixins", ":")
|
317
|
-
lit = literal
|
318
|
-
if lit && lit[:type] == :object
|
319
|
-
lit[:value].map {|x| x[:value][:value] }
|
320
|
-
elsif lit && lit[:type] == :array
|
321
|
-
lit[:value].map {|x| x[:value] }
|
322
|
-
else
|
323
|
-
nil
|
324
|
-
end
|
325
|
-
end
|
326
|
-
end
|
327
136
|
|
328
|
-
#
|
329
|
-
def
|
330
|
-
|
331
|
-
match("alternateClassName", ":")
|
332
|
-
string_or_list
|
333
|
-
end
|
137
|
+
# True if range A is less than range B
|
138
|
+
def less(a, b)
|
139
|
+
return a[1] < b[0]
|
334
140
|
end
|
335
141
|
|
336
|
-
#
|
337
|
-
def
|
338
|
-
|
339
|
-
match("alias", ":")
|
340
|
-
string_or_list
|
341
|
-
end
|
142
|
+
# True if range A is greater than range B
|
143
|
+
def greater(a, b)
|
144
|
+
return a[0] > b[1]
|
342
145
|
end
|
343
146
|
|
344
|
-
#
|
345
|
-
def
|
346
|
-
|
347
|
-
match("xtype", ":")
|
348
|
-
string_or_list
|
349
|
-
end
|
147
|
+
# True if range A is within range B
|
148
|
+
def within(a, b)
|
149
|
+
return b[0] < a[0] && a[1] < b[1]
|
350
150
|
end
|
351
151
|
|
352
|
-
# <requires> := "requires" ":" <string-or-list>
|
353
|
-
def ext_define_requires
|
354
|
-
if look("requires", ":")
|
355
|
-
match("requires", ":")
|
356
|
-
string_or_list
|
357
|
-
end
|
358
|
-
end
|
359
152
|
|
360
|
-
#
|
361
|
-
def
|
362
|
-
|
363
|
-
match("uses", ":")
|
364
|
-
string_or_list
|
365
|
-
end
|
366
|
-
end
|
153
|
+
# Returns array of child nodes of given node
|
154
|
+
def child_nodes(node)
|
155
|
+
properties = NODE_TYPES[node["type"]]
|
367
156
|
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
match("singleton", ":", "true")
|
372
|
-
true
|
157
|
+
unless properties
|
158
|
+
puts "Unknown node type: "+node["type"]
|
159
|
+
exit(1)
|
373
160
|
end
|
374
|
-
end
|
375
|
-
|
376
|
-
# <?> := <ident> ":" <literal>
|
377
|
-
def ext_define_whatever
|
378
|
-
if look(:ident, ":")
|
379
|
-
match(:ident, ":")
|
380
|
-
literal
|
381
|
-
end
|
382
|
-
end
|
383
|
-
|
384
|
-
# <string-or-list> := ( <string> | <array-literal> )
|
385
|
-
def string_or_list
|
386
|
-
lit = literal
|
387
|
-
if lit && lit[:type] == :string
|
388
|
-
[ lit[:value] ]
|
389
|
-
elsif lit && lit[:type] == :array
|
390
|
-
lit[:value].map {|x| x[:value] }
|
391
|
-
else
|
392
|
-
[]
|
393
|
-
end
|
394
|
-
end
|
395
161
|
|
396
|
-
|
397
|
-
def property_literal
|
398
|
-
left = look(:ident) ? match(:ident)[:value] : match(:string)[:value]
|
399
|
-
match(":")
|
400
|
-
right = expression
|
401
|
-
return {
|
402
|
-
:type => :assignment,
|
403
|
-
:left => [left],
|
404
|
-
:right => right,
|
405
|
-
}
|
162
|
+
properties.map {|p| node[p] }.compact.flatten
|
406
163
|
end
|
407
164
|
|
408
|
-
#
|
409
|
-
#
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
165
|
+
# All possible node types in Esprima-created abstract syntax tree
|
166
|
+
#
|
167
|
+
# Each node type maps to list of properties of that node into
|
168
|
+
# which we can recurse for further parsing.
|
169
|
+
NODE_TYPES = {
|
170
|
+
"Program" => ["body"],
|
171
|
+
|
172
|
+
"BlockStatement" => ["body"],
|
173
|
+
"BreakStatement" => [],
|
174
|
+
"ContinueStatement" => [],
|
175
|
+
"DoWhileStatement" => ["body", "test"],
|
176
|
+
"DebuggerStatement" => [],
|
177
|
+
"EmptyStatement" => [],
|
178
|
+
"ExpressionStatement" => ["expression"],
|
179
|
+
"ForStatement" => ["init", "test", "update", "body"],
|
180
|
+
"ForInStatement" => ["left", "right", "body"],
|
181
|
+
"IfStatement" => ["test", "consequent", "alternate"],
|
182
|
+
"LabeledStatement" => ["body"],
|
183
|
+
"ReturnStatement" => ["argument"],
|
184
|
+
"SwitchStatement" => ["discriminant", "cases"],
|
185
|
+
"SwitchCase" => ["test", "consequent"],
|
186
|
+
"ThrowStatement" => ["argument"],
|
187
|
+
"TryStatement" => ["block", "handlers", "finalizer"],
|
188
|
+
"CatchClause" => ["param", "body"],
|
189
|
+
"WhileStatement" => ["test", "body"],
|
190
|
+
"WithStatement" => ["object", "body"],
|
191
|
+
|
192
|
+
"FunctionDeclaration" => ["id", "params", "body"],
|
193
|
+
"VariableDeclaration" => ["declarations"],
|
194
|
+
"VariableDeclarator" => ["id", "init"],
|
195
|
+
|
196
|
+
"AssignmentExpression" => ["left", "right"],
|
197
|
+
"ArrayExpression" => ["elements"],
|
198
|
+
"BinaryExpression" => ["left", "right"],
|
199
|
+
"CallExpression" => ["callee", "arguments"],
|
200
|
+
"ConditionalExpression" => ["test", "consequent", "alternate"],
|
201
|
+
"FunctionExpression" => ["body"],
|
202
|
+
|
203
|
+
"LogicalExpression" => ["left", "right"],
|
204
|
+
"MemberExpression" => ["object", "property"],
|
205
|
+
"NewExpression" => ["callee", "arguments"],
|
206
|
+
"ObjectExpression" => ["properties"],
|
207
|
+
"Property" => ["key", "value"],
|
208
|
+
|
209
|
+
"SequenceExpression" => ["expressions"],
|
210
|
+
"ThisExpression" => [],
|
211
|
+
"UnaryExpression" => ["argument"],
|
212
|
+
"UpdateExpression" => ["argument"],
|
213
|
+
|
214
|
+
"Identifier" => [],
|
215
|
+
"Literal" => [],
|
216
|
+
}
|
427
217
|
|
428
218
|
end
|
429
|
-
|
430
219
|
end
|