docrb 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,630 @@
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
data/lib/docrb/spec.rb ADDED
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docrb
4
+ # Spec provides a small wrapper around Gem::Specification to provide metadata
5
+ # about a given gem being documented.
6
+ class Spec
7
+ # Public: Finds a .gemspec file within the provided input folder and
8
+ # processes it.
9
+ #
10
+ # input - Folder to search for a .gemspec file
11
+ #
12
+ # Returns a hash containing extracted information from the found gemspec
13
+ # file. The following keys are returned: :summary, :name, :license,
14
+ # :git_url, :authors, :host_url
15
+ def self.parse_folder(input)
16
+ spec = Dir["#{input}/*"].find { |f| f.end_with? ".gemspec" }
17
+ return if spec.nil?
18
+
19
+ data = Gem::Specification.load(spec)
20
+ is_private = data.metadata.key? "allowed_push_host"
21
+ {
22
+ summary: data.summary,
23
+ name: data.name,
24
+ license: data.license,
25
+ git_url: data.metadata["source_code_uri"],
26
+ authors: data.authors,
27
+ host_url: is_private ? nil : "https://rubygems.org/gems/#{data.name}"
28
+ }
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docrb
4
+ VERSION = "0.2.0"
5
+ end