docrb 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,630 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Docrb
4
- # RubyParser parses comments and source from a given file path and provides
5
- # its digested contents in standardised structures.
6
- class RubyParser
7
- COMMENT_PREFIX = /^\s*#\s?/
8
-
9
- attr_reader :modules, :classes, :methods, :ast
10
-
11
- # Initializes a new parser for a given path
12
- #
13
- # path - Path of the file to be parsed
14
- def initialize(path)
15
- @ast, @comments = ::Parser::CurrentRuby.parse_file_with_comments(path)
16
- @modules = []
17
- @classes = []
18
- @methods = []
19
- end
20
-
21
- # Kicks off the parsing process
22
- #
23
- # After this method is called, #modules, #classes, and #methods can be
24
- # used to enumerate items contained within the parsed file.
25
- def parse
26
- parse!(@ast)
27
- cleanup!
28
- end
29
-
30
- private
31
-
32
- # Recursivelly cleans modules and classes found in the parsed file by
33
- # removing uneeded internal fields used by the parser.
34
- def cleanup!
35
- clean_modules!
36
- clean_classes!
37
- end
38
-
39
- # Parses a given node and parent. This method is a dispatch source for
40
- # other parsing methods.
41
- #
42
- # node - Node to be parsed
43
- # parent - Parent node of the node being parsed
44
- def parse!(node = nil, parent = nil)
45
- return if node.nil?
46
-
47
- case node.type
48
- when :def
49
- parse_def(node, parent)
50
-
51
- when :defs
52
- parse_singleton_method(node, parent)
53
-
54
- when :class
55
- parse_class(node, parent)
56
-
57
- when :sclass
58
- parse_singleton_class(node, parent)
59
-
60
- when :module
61
- parse_module(node, parent)
62
-
63
- when :begin
64
- parse_begin(node, parent)
65
-
66
- when :send
67
- parse_send(node, parent)
68
-
69
- end
70
- end
71
-
72
- # Attempts to find a comment associated with a given line.
73
- #
74
- # line - Integer representing the line number to be checked for comments
75
- #
76
- # Returns a comment preceding the provided line, or nil, in case there's
77
- # none.
78
- def comment_for_line(line)
79
- return nil if line.zero? || line.negative?
80
-
81
- @comments.find { |c| c.loc.line == line }
82
- end
83
-
84
- # Finds and parses a comment block preceding a given line number.
85
- #
86
- # line - Integer representing the line being processed.
87
- # type: - Type of node depicted in the provided line number. This is used
88
- # to provide better utilities to handle the node being commented.
89
- #
90
- # Returns a hash containing the parsed comment.
91
- def comments_from(line, type:)
92
- # First, find a comment for the line before this
93
- comments = []
94
- comment = comment_for_line(line)
95
-
96
- while comment
97
- comments << comment
98
- comment = comment_for_line(line - 1)
99
- line -= 1
100
- end
101
-
102
- return nil if comments.empty?
103
-
104
- comments = comments
105
- .collect(&:text)
106
- .collect { |l| l.gsub(COMMENT_PREFIX, "") }
107
- .reverse
108
- .join("\n")
109
- CommentParser.parse(type:, comment: comments)
110
- end
111
-
112
- # Parses a `begin` keyword
113
- #
114
- # node - The node being parsed
115
- # parent - The node's parent object
116
- def parse_begin(node, parent)
117
- node.children.each { |n| parse!(n, parent) }
118
- end
119
-
120
- ATTRIBUTES = %i[attr_reader attr_writer attr_accessor].freeze
121
- CLASS_MODIFIERS = %i[extend include].freeze
122
- VISIBILITY_MODIFIERS = %i[private protected public].freeze
123
- MODULE_MODIFIERS = [:module_function].freeze
124
- SEND_DISPATCH = {
125
- ATTRIBUTES => :parse_attribute,
126
- CLASS_MODIFIERS => :parse_class_modifier,
127
- VISIBILITY_MODIFIERS => :parse_visibility_modifier,
128
- MODULE_MODIFIERS => :parse_module_modifier
129
- }.freeze
130
-
131
- PARSEABLE_SENDS = [*ATTRIBUTES, *CLASS_MODIFIERS, *VISIBILITY_MODIFIERS, *MODULE_MODIFIERS].freeze
132
-
133
- # Parses a `send` instruction (a method call)
134
- #
135
- # node - The node being parsed
136
- # parent - The node's parent object
137
- def parse_send(node, parent)
138
- arr = node.to_a
139
- target, name = arr.slice(0..1)
140
- args = arr.slice(2..)
141
-
142
- # TODO: Parse when target is not nil?
143
- return if target || !PARSEABLE_SENDS.include?(name)
144
-
145
- delegate = SEND_DISPATCH.find { |inner, _del| inner.include? name }&.last
146
- send(delegate, node, parent, target, name, args) if delegate
147
- end
148
-
149
- # Parses a module modifier. Currently this only handles a `module_function`
150
- # keyword.
151
- #
152
- # _node - Unused.
153
- # parent - The parent object of this modifier
154
- # _target - Unused.
155
- # name - Name of the modifier being invoked
156
- # args - Arguments passed to the modifier method
157
- def parse_module_modifier(_node, parent, _target, name, args)
158
- # FIXME: `module_function` does a lot:
159
- # Creates module functions for the named methods. These functions may
160
- # be called with the module as a receiver, and also become available as
161
- # instance methods to classes that mix in the module. Module functions
162
- # are copies of the original, and so may be changed independently. The
163
- # instance-method versions are made private. If used with no arguments,
164
- # subsequently defined methods become module functions. String arguments
165
- # are converted to symbols.
166
- # ...but we will only treat it as defs, for now.
167
- return if args.empty? # So when nothing is provided, nothing is done.
168
-
169
- # That's a poor decision by itself, but we may
170
- # address this in the future.
171
-
172
- args.each do |a|
173
- handle_module_function(parent, name, a)
174
- end
175
- end
176
-
177
- # Handles a `module_function` call and either registers and modifies a
178
- # function definition following it, or attempts to change previously-defined
179
- # functions visibility based on the call arguments.
180
- #
181
- # parent - The object on which the function has as its receiver
182
- # _name - Unused.
183
- # target - Target object being passed to the function
184
- def handle_module_function(parent, _name, target)
185
- case target.try?(:type)
186
- when :def
187
- # This will not be ideal, but let's parse the def as a def (duh), then
188
- # apply the same logic of :sym here. This is as not ideal as the impl.
189
- # of parse_module_modifier itself.
190
- parse_def(target, parent)
191
- change_method_kind(of: target.children.first, to: :sdef, on: parent)
192
- when :sym
193
- # This moves the target from `def` to `sdef`...
194
- change_method_kind(of: target.children.first, to: :sdef, on: parent)
195
- when :string
196
- change_method_kind(of: target.children.first.to_sym, to: :sdef, on: parent)
197
- end
198
- end
199
-
200
- # Parses an attribute definition
201
- #
202
- # node - The node being parsed
203
- # parent - The parent containing the node being parsed
204
- # _target - Unused.
205
- # name - Name of the attribute definer being called
206
- # args - List of attribute names being defined
207
- def parse_attribute(node, parent, _target, name, args)
208
- parent[name] ||= []
209
- docs = nil
210
- if args.length == 1
211
- # When we have a single accessor, we may check for docs before it.
212
- docs = comments_from(node.loc.expression.first_line - 1, type: :attribute)
213
- end
214
-
215
- args.map(&:to_a).flatten.each do |n|
216
- parent[name].append({
217
- docs:,
218
- name: n,
219
- writer_visibility: parent[:_visibility],
220
- reader_visibility: parent[:_visibility]
221
- })
222
- end
223
- end
224
-
225
- # Parses a given class modifier (`include`, `extend`).
226
- #
227
- # _node - Unused.
228
- # parent - Parent object on which the modified is being called against.
229
- # _target - Unused.
230
- # name - Name of the modifier being called.
231
- # args - List of objects passed to the modifier.
232
- def parse_class_modifier(_node, parent, _target, name, args)
233
- parent[name] ||= []
234
- args.each do |n|
235
- path, base_name = parse_class_path(n)
236
- parent[name].append({
237
- name: base_name,
238
- class_path: path
239
- })
240
- end
241
- end
242
-
243
- # Parses a given visibility modifier (`private`, `public`, `protected`) and
244
- # sets the current class visibility option based on whether the call
245
- # contains extra parameters or not. When passing names or a method
246
- # definition after the keyword, only listed objects are changed. Otherwise,
247
- # marks all subsequent objects following the keyword with its access level
248
- # until another modifier is found.
249
- #
250
- # _node - Unused.
251
- # parent - Parent on which the keyword is being invoked against.
252
- # _target - Unused.
253
- # name - Name of the modifier being invoked.
254
- # args - List of arguments passed to the modifier.
255
- def parse_visibility_modifier(_node, parent, _target, name, args)
256
- # Empty args changes all items defined after that point to the
257
- # visibility level `name`
258
- return parent[:_visibility] = name if args.empty?
259
-
260
- args.each do |a|
261
- handle_visibility_keyword(parent, name, a)
262
- end
263
- end
264
-
265
- # Handles a visibility keyword for a specific target.
266
- #
267
- # parent - Parent containing the method call.
268
- # name - Name of the visibility keyword being applied
269
- # target - Target object on which the visibility keyword is being invoked
270
- # against.
271
- def handle_visibility_keyword(parent, name, target)
272
- case target.try?(:type)
273
- when :def
274
- old_visibility = parent[:_visibility]
275
- parent[:_visibility] = name
276
- parse_def(target, parent)
277
- parent[:_visibility] = old_visibility
278
-
279
- when :defs
280
- old_visibility = parent[:_visibility]
281
- parent[:_visibility] = name
282
- parse_singleton_method(target, parent)
283
- parent[:_visibility] = old_visibility
284
-
285
- when :send
286
- # This one will be tricky... The called method can return anything,
287
- # so we will support at least the attr_* family.
288
- old_visibility = parent[:_visibility]
289
- parent[:_visibility] = name
290
- parse_send(target, parent)
291
- parent[:_visibility] = old_visibility
292
-
293
- when :sym
294
- # Oh no.
295
- change_visibility(of: target.children.first, to: name, on: parent)
296
-
297
- else
298
- puts "BUG: Unexpected element on handle_visibility_keyword. Please file an issue."
299
- exit(1)
300
- end
301
- end
302
-
303
- # Changes the visibility of a single object on a given parent to a provided
304
- # value.
305
- #
306
- # of: - Name of the object having its visibility changed.
307
- # to: - New visibility value for the object.
308
- # on: - The object's parent container.
309
- def change_visibility(of:, to:, on:)
310
- # Method?
311
- if on.key?(:methods) && (method = on[:methods].find { |m| m[:name] == of })
312
- return method[:visibility] = to
313
- end
314
-
315
- # Accessor?
316
- writer = of.end_with? "="
317
- normalized = of.to_s.gsub(/=$/, "").to_sym
318
- if on.key?(:attr_accessor) && (acc = on[:attr_accessor].find { |a| a[:name] == normalized })
319
- return acc[writer ? :writer_visibility : :reader_visibility] = to
320
- end
321
-
322
- # Reader or writer?
323
- type = writer ? :attr_writer : :attr_reader
324
- return unless on.key?(type) && (acc = on[type].find { |a| a[:name] == normalized })
325
-
326
- acc[writer ? :writer_visibility : :reader_visibility] = to
327
- end
328
-
329
- # Changes the kind of a method object (E.g.: Between sdef and def)
330
- #
331
- # of: - Name of the method having its kind changed.
332
- # to: - New kind value for the method.
333
- # on: - The methods's parent container.
334
- def change_method_kind(of:, to:, on:)
335
- return unless on.key?(:methods) && (method = on[:methods].find { |m| m[:name] == of })
336
-
337
- method[:type] = to
338
- end
339
-
340
- # Parses an instance method definition
341
- #
342
- # node - Node representing the method being defined
343
- # parent - The node's parent container (e.g. class on which it is being
344
- # defined on)
345
- def parse_def(node, parent)
346
- loc = node.loc.keyword
347
- comments = comments_from(loc.line - 1, type: :def)
348
- def_meta = method_to_meta(node)
349
- def_meta[:doc] = comments
350
- def_meta[:visibility] = parent ? parent[:_visibility] : :public
351
- if parent.nil?
352
- @methods << def_meta
353
- else
354
- parent[:methods] ||= []
355
- parent[:methods] << def_meta
356
- end
357
- end
358
-
359
- # Parses a class method definition
360
- #
361
- # node - Node representing the method being defined
362
- # parent - The node's parent container (e.g. class on which it is being
363
- # defined on)
364
- def parse_singleton_method(node, parent)
365
- loc = node.loc.keyword
366
- comments = comments_from(loc.line - 1, type: :defs)
367
- def_meta = singleton_method_to_meta(node)
368
- def_meta[:doc] = comments
369
- def_meta[:visibility] = parent ? parent[:_visibility] : :public
370
- if parent.nil?
371
- @methods << def_meta
372
- else
373
- parent[:methods] ||= []
374
- parent[:methods] << def_meta
375
- end
376
- end
377
-
378
- # Parses a class definition
379
- #
380
- # node - Node representing the class being defined
381
- # parent - The node's parent container (e.g. class/module on which it is
382
- # being defined on)
383
- def parse_class(node, parent)
384
- loc = node.loc.keyword
385
- comments = comments_from(loc.line - 1, type: :class)
386
- class_meta = class_to_meta(node)
387
- class_meta[:doc] = comments
388
- if parent.nil?
389
- @classes << class_meta
390
- else
391
- parent[:classes] ||= []
392
- parent[:classes] << class_meta
393
- end
394
- node.to_a.slice(2...).each do |n|
395
- parse!(n, class_meta)
396
- end
397
- end
398
-
399
- # Parses a singleton class definition
400
- #
401
- # node - Node representing the singleton class being defined
402
- # parent - The node's parent container (e.g. class/module on which it is
403
- # being defined on)
404
- def parse_singleton_class(node, parent)
405
- loc = node.loc.keyword
406
- comments = comments_from(loc.line - 1, type: :sclass)
407
- class_meta = singleton_class_to_meta(node)
408
- class_meta[:doc] = comments
409
- if parent.nil?
410
- @classes << class_meta
411
- else
412
- parent[:classes] ||= []
413
- parent[:classes] << class_meta
414
- end
415
-
416
- node.to_a.slice(1...).each do |n|
417
- parse!(n, class_meta)
418
- end
419
- end
420
-
421
- # Parses a module definition
422
- #
423
- # node - Node representing the module being defined
424
- # parent - The node's parent container (e.g. class/module on which it is
425
- # being defined on)
426
- def parse_module(node, parent)
427
- loc = node.loc.keyword
428
- comments = comments_from(loc.line - 1, type: :module)
429
- module_meta = module_to_meta(node)
430
- module_meta[:doc] = comments
431
- if parent.nil?
432
- @modules << module_meta
433
- else
434
- parent[:modules] ||= []
435
- parent[:modules] << module_meta
436
- end
437
-
438
- node.to_a.slice(1...).each do |n|
439
- parse!(n, module_meta)
440
- end
441
- end
442
-
443
- # Generates metadata for a given method, such as its name, arguments,
444
- # and boundaries.
445
- #
446
- # node - Method node being processed
447
- #
448
- # Returns a Hash containing metadata about the provided method.
449
- def method_to_meta(node)
450
- {
451
- type: :def,
452
- name: node.children.first,
453
- args: parse_method_args(node),
454
- start_at: node.loc.keyword.line,
455
- end_at: node.loc&.end&.line || node.loc.keyword.line
456
- }
457
- end
458
-
459
- # Generates metadata for a given singleton method, such as its name,
460
- # arguments, and boundaries.
461
- #
462
- # node - Method node being processed
463
- #
464
- # Returns a Hash containing metadata about the provided singleton method.
465
- def singleton_method_to_meta(node)
466
- class_path, name = parse_class_path(node.children.first)
467
- {
468
- type: :defs,
469
- target: name,
470
- class_path:,
471
- name: node.children[1],
472
- args: parse_method_args(node),
473
- start_at: node.loc.keyword.line,
474
- end_at: node.loc&.end&.line || node.loc.keyword.line
475
- }
476
- end
477
-
478
- # Generates metadata for a given class, such as its name, arguments,
479
- # and boundaries.
480
- #
481
- # node - Class node being processed
482
- #
483
- # Returns a Hash containing metadata about the provided class.
484
- def class_to_meta(node)
485
- class_path, name = parse_class_path(node.children.first)
486
- {
487
- type: :class,
488
- name:,
489
- class_path:,
490
- start_at: node.loc.keyword.line,
491
- end_at: node.loc.end.line,
492
- _visibility: :public
493
- }.tap do |h|
494
- if (inherits = node.children[1])
495
- h[:inherits] = inherits.children.last
496
- end
497
- end
498
- end
499
-
500
- # Generates metadata for a given singleton class, such as its name,
501
- # arguments, and boundaries.
502
- #
503
- # node - Singleton class node being processed
504
- #
505
- # Returns a Hash containing metadata about the provided singleton class.
506
- def singleton_class_to_meta(node)
507
- target = node.children.first.type
508
- target = node.children[0].children.last if target == :const
509
- {
510
- type: :sclass,
511
- target:,
512
- _visibility: :public,
513
- start_at: node.loc.keyword.line,
514
- end_at: node.loc.end.line
515
- }
516
- end
517
-
518
- # Generates metadata for a given module, such as its name, arguments,
519
- # and boundaries.
520
- #
521
- # node - Class node being processed
522
- #
523
- # Returns a Hash containing metadata about the provided module.
524
- def module_to_meta(node)
525
- class_path, name = parse_class_path(node.children.first)
526
- {
527
- type: :module,
528
- name:,
529
- _visibility: :public,
530
- class_path:,
531
- start_at: node.loc.keyword.line,
532
- end_at: node.loc.end.line
533
- }
534
- end
535
-
536
- # Parses an argument list of a method defintion
537
- #
538
- # node - The method's node definition
539
- #
540
- # Returns a list of standardised hashes representing all method's arguments
541
- def parse_method_args(node)
542
- node.children
543
- .find { |n| n.try?(:type) == :args }
544
- &.to_a
545
- &.map do |n|
546
- {
547
- type: n.type,
548
- name: n.children.first
549
- }.tap do |m|
550
- if n.children.length > 1
551
- val = n.children[1]
552
- represented_type = nil
553
- represented_value = nil
554
- case val.type
555
- when true, :true
556
- represented_type = :bool
557
- represented_value = true
558
- when false, :false
559
- represented_type = :bool
560
- represented_value = false
561
- when :nil
562
- represented_type = :nil
563
- represented_value = :nil
564
- when :send
565
- represented_type = :send
566
- represented_value = {
567
- target: parse_class_path(val.children.first).flatten.compact,
568
- name: val.children.last
569
- }
570
- when :const
571
- represented_type = :const
572
- represented_value = {
573
- target: parse_class_path(val.children.first).flatten.compact,
574
- name: val.children.last
575
- }
576
- else
577
- represented_type = val.type
578
- represented_value = val.children.first
579
- end
580
- m[:value_type] = represented_type
581
- m[:value] = represented_value
582
- end
583
- end
584
- end
585
- end
586
-
587
- # Parses a given class path into an interable list of objects representing
588
- # its full path
589
- #
590
- # path - Path to be processed
591
- #
592
- # Returns a multidimensional array structure representing the path to the
593
- # provided value.
594
- def parse_class_path(path)
595
- return [[], :self] if path.try?(:type) == :self
596
-
597
- path = path.to_a
598
- name = path[1]
599
- parents = []
600
- parent = path.first.try?(:to_a) || []
601
- until parent&.empty?
602
- parents << parent.last
603
- parent = parent.first.to_a
604
- end
605
- [parents.reverse, name]
606
- end
607
-
608
- # Strips all contained module's structures of its internal keys used by
609
- # the parser.
610
- def clean_modules!
611
- @modules.each { |m| m.delete(:_visibility) }
612
- end
613
-
614
- # Recursivelly calls #clean_class for all classes contained within the
615
- # parser.
616
- def clean_classes!
617
- @classes.map! { |c| clean_class(c) }
618
- end
619
-
620
- # Strps the provided class structure of its internal keys used by the
621
- # parser.
622
- #
623
- # cls - Class structure to be cleaned.
624
- def clean_class(cls)
625
- cls.delete(:_visibility)
626
- cls[:classes].map! { |c| clean_class(c) } if cls.key? :classes
627
- cls
628
- end
629
- end
630
- end