jsduck 3.11.2 → 4.0.beta

Sign up to get free protection for your applications and to get access to all the features.
@@ -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