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.
@@ -1,430 +1,219 @@
1
- require 'jsduck/lexer'
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
- class JsParser < JsLiteralParser
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
- super(input)
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
- # Splits namespace string into array like so:
17
- #
18
- # "Foo.Bar.Baz" --> ["Foo", ".", "Bar", ".", "Baz"]
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 the whole JavaScript block and returns array where for
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
- # For example with the following JavaScript input:
34
- #
35
- # /**
36
- # * @param {String} foo
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
- while !@lex.empty? do
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
- # The following is a recursive-descent parser for JavaScript that
82
- # can possibly follow a doc-comment
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
- # <function> := "function" [ <ident> ] <function-parameters> <function-body>
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
- # <function-parameters> := "(" [ <ident> [ "," <ident> ]* ] ")"
131
- def function_parameters
132
- match("(")
133
- params = look(:ident) ? [{:name => match(:ident)[:value]}] : []
134
- while look(",", :ident) do
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
- # <function-body> := "{" ...
142
- def function_body
143
- match("{")
144
- end
41
+ comment = original_comments[0]
42
+ i = 0
145
43
 
146
- # <var-declaration> := "var" <assignment>
147
- def var_declaration
148
- match(:var)
149
- maybe_assignment
150
- end
44
+ while comment
45
+ i += 1
46
+ next_comment = original_comments[i]
151
47
 
152
- # <maybe-assignment> := <ident-chain> ( "=" <expression> | ";" | "," )
153
- def maybe_assignment
154
- left = ident_chain
155
- if look("=")
156
- match("=")
157
- right = expression
158
- elsif look(";")
159
- match(";")
160
- right = nil
161
- elsif look(",")
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
- return {
169
- :type => :assignment,
170
- :left => left,
171
- :right => right,
172
- }
60
+ result
173
61
  end
174
62
 
175
- # <ident-chain> := [ "this" | <ident> ] [ "." <ident> ]*
176
- def ident_chain
177
- if look(:this)
178
- match(:this)
179
- chain = ["this"]
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
- chain = [match(:ident)[:value]]
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
- # <expression> := <function> | <ext-extend> | <ext-emptyfn> | <ext-base-css-prefix> | <literal>
191
- def expression
192
- if look(:function)
193
- function
194
- elsif ext_look(:ns, ".", "extend")
195
- ext_extend
196
- elsif ext_look(:ns, ".", "emptyFn")
197
- ext_emptyfn
198
- elsif ext_look(:ns, ".", "baseCSSPrefix", "+", :string)
199
- ext_base_css_prefix
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
- # <literal> := ...see JsLiteralParser...
206
- def my_literal
207
- lit = literal
208
- return unless lit && literal_expression_end?
209
-
210
- cls_map = {
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
- # <ext-extend> := "Ext" "." "extend" "(" <ident-chain> "," ...
239
- def ext_extend
240
- match(:ident, ".", "extend", "(")
241
- return {
242
- :type => :ext_extend,
243
- :extend => ident_chain,
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
- # <ext-base-css-prefix> := "Ext" "." "baseCSSPrefix" "+" <string>
248
- def ext_base_css_prefix
249
- match(:ident, ".", "baseCSSPrefix", "+")
250
- return {
251
- :type => :literal,
252
- :class => "String",
253
- :value => '"x-' + match(:string)[:value] + '"',
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
- cfg = {}
111
+ code
266
112
  end
267
-
268
- cfg[:type] = :ext_define
269
- cfg[:name] = name
270
-
271
- cfg
272
113
  end
273
114
 
274
- # <ext-define-cfg> := "{" ( <extend> | <mixins> | <alternate-class-name> | <alias> |
275
- # <xtype> | <requires> | <uses> | <singleton> | <?> )*
276
- def ext_define_cfg
277
- match("{")
278
- cfg = {}
279
- found = true
280
- while found
281
- found = false
282
- if found = ext_define_extend
283
- cfg[:extend] = found
284
- elsif found = ext_define_mixins
285
- cfg[:mixins] = found
286
- elsif found = ext_define_alternate_class_name
287
- cfg[:alternateClassNames] = found
288
- elsif found = ext_define_alias
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
- # <extend> := "extend" ":" <string>
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
- # <alternate-class-name> := "alternateClassName" ":" <string-or-list>
329
- def ext_define_alternate_class_name
330
- if look("alternateClassName", ":")
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
- # <alias> := "alias" ":" <string-or-list>
337
- def ext_define_alias
338
- if look("alias", ":")
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
- # <xtype> := "xtype" ":" <string-or-list>
345
- def ext_define_xtype
346
- if look("xtype", ":")
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
- # <uses> := "uses" ":" <string-or-list>
361
- def ext_define_uses
362
- if look("uses", ":")
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
- # <singleton> := "singleton" ":" "true"
369
- def ext_define_singleton
370
- if look("singleton", ":", "true")
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
- # <property-literal> := ( <ident> | <string> ) ":" <expression>
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
- # Like look() but tries to match as the first argument all the
409
- # names listed in @ext_namespaces
410
- def ext_look(placeholder, *args)
411
- @ext_namespaces.each do |ns|
412
- return true if look(*(ns + args))
413
- end
414
- return false
415
- end
416
-
417
- # Like match() but tries as the first argument all the names
418
- # listed in @ext_namespaces
419
- def ext_match(placeholder, *args)
420
- @ext_namespaces.each do |ns|
421
- pattern = ns + args
422
- if look(*pattern)
423
- return match(*pattern)
424
- end
425
- end
426
- end
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