docrb 0.2.0

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.
@@ -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