docrb-parser 0.1.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.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +75 -0
  4. data/Rakefile +12 -0
  5. data/docrb-parser.gemspec +38 -0
  6. data/lib/docrb/core_extensions.rb +60 -0
  7. data/lib/docrb/parser/attribute.rb +25 -0
  8. data/lib/docrb/parser/call.rb +27 -0
  9. data/lib/docrb/parser/class.rb +94 -0
  10. data/lib/docrb/parser/comment.rb +40 -0
  11. data/lib/docrb/parser/comment_parser.rb +290 -0
  12. data/lib/docrb/parser/computations.rb +471 -0
  13. data/lib/docrb/parser/constant.rb +19 -0
  14. data/lib/docrb/parser/container.rb +305 -0
  15. data/lib/docrb/parser/deferred_singleton_class.rb +17 -0
  16. data/lib/docrb/parser/location.rb +43 -0
  17. data/lib/docrb/parser/method.rb +62 -0
  18. data/lib/docrb/parser/method_parameters.rb +85 -0
  19. data/lib/docrb/parser/module.rb +50 -0
  20. data/lib/docrb/parser/node_array.rb +24 -0
  21. data/lib/docrb/parser/reference.rb +25 -0
  22. data/lib/docrb/parser/reloader.rb +19 -0
  23. data/lib/docrb/parser/resolved_reference.rb +26 -0
  24. data/lib/docrb/parser/version.rb +7 -0
  25. data/lib/docrb/parser/virtual_container.rb +21 -0
  26. data/lib/docrb/parser/virtual_location.rb +9 -0
  27. data/lib/docrb/parser/virtual_method.rb +19 -0
  28. data/lib/docrb/parser.rb +139 -0
  29. data/lib/docrb-parser.rb +3 -0
  30. data/sig/docrb/core_extensions.rbs +24 -0
  31. data/sig/docrb/parser/attribute.rbs +18 -0
  32. data/sig/docrb/parser/call.rbs +17 -0
  33. data/sig/docrb/parser/class.rbs +34 -0
  34. data/sig/docrb/parser/comment.rbs +14 -0
  35. data/sig/docrb/parser/comment_parser.rbs +79 -0
  36. data/sig/docrb/parser/constant.rbs +15 -0
  37. data/sig/docrb/parser/container.rbs +91 -0
  38. data/sig/docrb/parser/deferred_singleton_class.rbs +12 -0
  39. data/sig/docrb/parser/location.rbs +24 -0
  40. data/sig/docrb/parser/method.rbs +34 -0
  41. data/sig/docrb/parser/method_parameters.rbs +34 -0
  42. data/sig/docrb/parser/module.rbs +14 -0
  43. data/sig/docrb/parser/node_array.rbs +12 -0
  44. data/sig/docrb/parser/reference.rbs +19 -0
  45. data/sig/docrb/parser/reloader.rbs +7 -0
  46. data/sig/docrb/parser/resolved_reference.rbs +22 -0
  47. data/sig/docrb/parser/virtual_method.rbs +17 -0
  48. data/sig/docrb/parser.rbs +5 -0
  49. metadata +109 -0
@@ -0,0 +1,471 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docrb
4
+ class Parser
5
+ class Computations
6
+ attr_reader :parser
7
+
8
+ def initialize(parser)
9
+ @parser = parser
10
+ end
11
+
12
+ def run
13
+ unfurl_locations
14
+ merge_containers(parser.nodes)
15
+ 2.times { resolve_references }
16
+ unfurl_flat_paths
17
+ resolve_overrides
18
+ resolve_deferred_singletons
19
+ attach_documentation
20
+ resolve_documentation_references
21
+ merge_inline_documentation_blocks
22
+ associate_container_sources
23
+ end
24
+
25
+ def unfurl_locations = parser.locations.each(&:load_source!)
26
+
27
+ def unfurl_flat_paths
28
+ parser.all_modules
29
+ .reject { _1.path_segments.nil? || _1.path_segments.empty? }
30
+ .each { unfurl_flat_path(_1) }
31
+
32
+ parser.all_classes
33
+ .reject { _1.path_segments.nil? || _1.path_segments.empty? }
34
+ .each { unfurl_flat_path(_1) }
35
+ end
36
+
37
+ def unfurl_flat_path(node)
38
+ resolve = resolve_path_partial(node.path_segments, node.parent || parser, container_only: true)
39
+ last_found_node = resolve[:last_found_node] || parser
40
+ if resolve[:missing_segments].empty?
41
+ remove_from_parent!(node, parent: node.parent)
42
+ node.parent = resolve[:last_found_node]
43
+ if node.is_a? Class
44
+ last_found_node.classes << node
45
+ elsif node.is_a? Module
46
+ last_found_node.modules << node
47
+ end
48
+
49
+ return
50
+ end
51
+
52
+ # Synthesize missing segments as a Module, inject node into it.
53
+ root = VirtualContainer.new(:module_node, resolve[:missing_segments].shift)
54
+ parent = root
55
+ until resolve[:missing_segments].empty?
56
+ obj = VirtualContainer.new(:module_node, resolve[:missing_segments].shift)
57
+ parent.body.body = [obj]
58
+ parent = obj
59
+ end
60
+
61
+ if last_found_node.is_a? Parser
62
+ obj = last_found_node.handle_node(root)
63
+ last_found_node.nodes << obj
64
+ else
65
+ last_found_node.handle_node(root)
66
+ end
67
+
68
+ unfurl_flat_path(node)
69
+ end
70
+
71
+ def resolve_documentation_references
72
+ @parser.all_objects
73
+ .filter { _1.respond_to? :doc }
74
+ .reject { _1.doc.nil? }
75
+ .each do |node|
76
+ node.doc[:value].each { resolve_documentation_reference(node, _1) }
77
+ end
78
+ end
79
+
80
+ def associate_container_sources
81
+ @parser.all_objects
82
+ .filter { _1.is_a? Container }
83
+ .filter { _1.defined_by.empty? }
84
+ .each { _1.defined_by << _1.location }
85
+ end
86
+
87
+ def resolve_documentation_reference(node, obj)
88
+ case obj[:type]
89
+ when :block
90
+ obj[:value].map! { resolve_documentation_reference node, _1 }
91
+ obj
92
+ when :fields
93
+ obj[:value].transform_values! { resolve_documentation_reference(node, _1) }
94
+ obj
95
+ when :method_ref
96
+ resolve_documentation_pure_reference(node, obj)
97
+ when :span, :symbol
98
+ obj
99
+ else
100
+ puts "Unhandled documentation node #{obj[:type]}"
101
+ obj
102
+ end
103
+
104
+ # obj[:value].each do |v|
105
+ # case v[:type]
106
+ # when :identifier
107
+ # if v[:value].to_sym == node.try(:name)&.to_sym
108
+ # v[:type] = :neutral_identifier
109
+ # else
110
+ # resolved = resolve_path(node, [v[:value].to_sym])
111
+ # if resolved.respond_to?(:id)
112
+ # v[:type] = :reference
113
+ # v[:id] = resolved.id
114
+ # v[:path] = path_of(resolved)
115
+ # else
116
+ # v[:type] = :unresolved_identifier
117
+ # end
118
+ # end
119
+ # when :reference
120
+ # resolve_documentation_pure_reference(v, node)
121
+ # when :block
122
+ # v[:value]
123
+ # .reject { [:span, :symbol].include? _1[:type] }
124
+ # .each { resolve_documentation_reference(node, _1) }
125
+ # when :fields
126
+ # val = v[:value].values.flatten
127
+ # resolve_documentation_reference(node, { value: val })
128
+ # else
129
+ # puts "Unhandled documentation node #{v[:type]}"
130
+ # end
131
+ # end
132
+ end
133
+
134
+ def resolve_documentation_pure_reference(node, ref)
135
+ return ref if ref.key? :object
136
+
137
+ node = find_next_container(node) unless node.is_a? Container
138
+ name = ref[:name].to_sym
139
+ obj = if ref[:class_path]
140
+ resolve_path(ref[:class_path].split("::").map(&:to_sym), node)
141
+ else
142
+ node
143
+ end
144
+ result = if ref[:type] == :method
145
+ obj.all_instance_methods.named(name).first || obj.all_class_methods.named(name).first
146
+ else
147
+ obj.all_objects.named(name).first
148
+ end
149
+
150
+ if result.nil?
151
+ ref[:type] = :unresolved_identifier
152
+ else
153
+ ref[:object] = result
154
+ end
155
+ ref
156
+ end
157
+
158
+ def merge_inline_documentation_blocks
159
+ @parser.all_objects
160
+ .filter { _1.respond_to? :doc }
161
+ .reject { _1.doc.nil? }
162
+ .each { merge_inline_documentation_block(_1.doc) }
163
+ end
164
+
165
+ def merge_inline_documentation_block(block)
166
+ return if !block.key?(:value) || !block[:value].is_a?(Array)
167
+
168
+ continue = true
169
+ while continue
170
+ continue = false
171
+ block[:value].each.with_index do |current, i|
172
+ next if i.zero?
173
+
174
+ last_i = i - 1
175
+ last = block[:value][i - 1]
176
+ mergeable_types = %i[netural_identifier unresolved_identifier reference]
177
+ if mergeable_types.include?(last[:type]) && current[:type] == :text_block
178
+ case current[:value]
179
+ when String
180
+ current[:value] = [last, { type: :span, value: current[:value] }]
181
+ block[:value].delete_at(last_i)
182
+ continue = true
183
+ break
184
+ when Array
185
+ current[:value].unshift(last)
186
+ block[:value].delete_at(last_i)
187
+ continue = true
188
+ break
189
+ end
190
+ elsif mergeable_types.include?(current[:type]) && last[:type] == :text_block
191
+ case last[:value]
192
+ when String
193
+ last[:value] = [{ type: :span, value: last[:value] }, current]
194
+ block[:value].delete_at(i)
195
+ continue = true
196
+ break
197
+ when Array
198
+ last[:value].append(current)
199
+ block[:value].delete_at(i)
200
+ continue = true
201
+ break
202
+ end
203
+ elsif current[:type] == :text_block && last[:type] == :text_block && !current[:paragraph]
204
+ last[:value] = [{ type: :span, value: last[:value] }] if last[:value].is_a? String
205
+ current[:value] = [{ type: :span, value: current[:value] }] if current[:value].is_a? String
206
+ last[:value].append(*current[:value])
207
+ block[:value].delete_at(i)
208
+ continue = true
209
+ break
210
+ end
211
+ end
212
+ end
213
+ end
214
+
215
+ def path_of(obj)
216
+ return [] if obj.nil?
217
+
218
+ [obj.name] + path_of(obj.parent)
219
+ end
220
+
221
+ def resolve_overrides
222
+ parser.all_modules.each do |mod|
223
+ unowned_class_methods = mod.unowned_class_methods
224
+ mod.class_methods.each do |meth|
225
+ next unless (other = unowned_class_methods.named(meth.name).first)
226
+
227
+ meth.overriding = other.id
228
+ other.overridden_by << meth.id
229
+ end
230
+
231
+ unowned_instance_methods = mod.unowned_instance_methods
232
+ mod.instance_methods.each do |meth|
233
+ next unless (other = unowned_instance_methods.named(meth.name).first)
234
+
235
+ meth.overriding = other.id
236
+ other.overridden_by << meth.id
237
+ end
238
+ end
239
+
240
+ parser.all_classes.each do |cls|
241
+ unowned_class_methods = cls.unowned_class_methods
242
+ cls.class_methods.each do |meth|
243
+ next unless (other = unowned_class_methods.named(meth.name).first)
244
+
245
+ meth.overriding = other.id
246
+ other.overridden_by << meth.id
247
+ end
248
+
249
+ unowned_instance_methods = cls.unowned_instance_methods
250
+ cls.instance_methods.each do |meth|
251
+ next unless (other = unowned_instance_methods.named(meth.name).first)
252
+
253
+ meth.overriding = other.id
254
+ other.overridden_by << meth.id
255
+ end
256
+ end
257
+ end
258
+
259
+ CONTAINER_APPENDABLE = %i[
260
+ constants classes modules includes extends instance_methods class_methods
261
+ instance_attributes class_attributes
262
+ ].freeze
263
+
264
+ def merge_containers(from)
265
+ from.typed(Container).map(&:name).uniq.each do |n|
266
+ list = from.named(n)
267
+ next if list.length <= 1
268
+
269
+ final = merge_container(list)
270
+ from.delete_if { _1.name == n && _1 != final }
271
+ end
272
+
273
+ from.typed(Container).each do |container|
274
+ CONTAINER_APPENDABLE.each do |attr|
275
+ merge_containers(container.send(attr))
276
+ end
277
+ end
278
+
279
+ true
280
+ end
281
+
282
+ def merge_container(list)
283
+ list.shift.tap do |first|
284
+ list.each do |item|
285
+ first.defined_by << item.location if item.all_objects.any? { !_1.is_a?(Container) }
286
+ CONTAINER_APPENDABLE.each do |attr|
287
+ items = item.send(attr).each { _1.parent = first }
288
+ first.send(attr).append(*items)
289
+ end
290
+ end
291
+ first.defined_by << first.location if first.defined_by.empty?
292
+ end
293
+ end
294
+
295
+ def remove_from_parent!(item, parent:)
296
+ return parser.nodes.delete(item) if parent.nil?
297
+
298
+ CONTAINER_APPENDABLE.each { parent.send(_1).delete(item) }
299
+ end
300
+
301
+ def resolve_references
302
+ parser.references.reject(&:fulfilled?).each do |ref|
303
+ target = resolve_path(ref.parent, ref.path)
304
+ ref.resolved = if target.nil?
305
+ ResolvedReference.new(parser, :broken, nil)
306
+ else
307
+ ResolvedReference.new(parser, :valid, target.id)
308
+ end
309
+ end
310
+ end
311
+
312
+ def attach_documentation
313
+ parser.all_objects.each do |node|
314
+ comment = case node.try(:kind)
315
+ when :module, :class
316
+ ([node.defined_by] + [node.location])
317
+ .flatten
318
+ .compact
319
+ .uniq
320
+ .lazy
321
+ .map { Comment.new(parser, _1) }
322
+ .reject { _1.comments.nil? }
323
+ .first
324
+ when :method
325
+ Comment.new(parser, node.location)
326
+ end
327
+
328
+ next if comment.nil? || comment.comments.nil?
329
+
330
+ node.doc = CommentParser.parse(comment.comments.join("\n"))
331
+ end
332
+ end
333
+
334
+ def resolve_path(obj, path)
335
+ p = path.dup
336
+ obj = find_object(obj, p.shift) until p.empty? || obj.nil?
337
+ obj
338
+ end
339
+
340
+ def resolve_path_partial(path, parent, container_only: false)
341
+ path = path.dup
342
+ if path.first == :root!
343
+ parent = parser
344
+ path.shift
345
+ end
346
+
347
+ c_filter = -> { container_only ? _1.is_a?(Container) : true }
348
+
349
+ found_segments = []
350
+ until path.empty?
351
+ last_parent = parent
352
+ if parent.is_a? Parser
353
+ parent = parent.nodes.named(path.first).filter(&c_filter).first
354
+ if parent.nil?
355
+ parent = last_parent
356
+ break
357
+ end
358
+
359
+ found_segments << path.shift
360
+ next
361
+ end
362
+
363
+ parent = find_object(parent, path.first, container_only:)
364
+ if parent.nil?
365
+ parent = last_parent
366
+ break
367
+ end
368
+
369
+ found_segments << path.shift
370
+ end
371
+
372
+ {
373
+ last_found_node: parent,
374
+ found_segments:,
375
+ missing_segments: path
376
+ }
377
+ end
378
+
379
+ def resolve_deferred_singletons
380
+ parser.nodes.typed(DeferredSingletonClass).each do |sing|
381
+ resolve_deferred_singleton(sing)
382
+ end
383
+ end
384
+
385
+ def resolve_deferred_singleton(sing)
386
+ object = find_object(sing.parent, sing.target, container_only: true)
387
+ object ||= sing.parser.nodes
388
+ .named(sing.target)
389
+ .typed(Container)
390
+ .reject { _1.is_a?(DeferredSingletonClass) }
391
+ .first
392
+ return if !object.nil? && object.kind != :class
393
+
394
+ if object&.kind == :class
395
+ object.merge_singleton_class(sing)
396
+ remove_from_parent!(sing, parent: sing.parent)
397
+ return
398
+ end
399
+
400
+ resolve = resolve_path_partial(sing.target, sing.parent, container_only: true)
401
+ last_found_node = resolve[:last_found_node]
402
+ missing_segments = resolve[:missing_segments]
403
+
404
+ # At this point we will have to synthesize the class. Prepare its path as modules.
405
+ class_name = missing_segments.pop
406
+ root = if missing_segments.empty?
407
+ parser
408
+ else
409
+ VirtualContainer.new(:module_node, missing_segments.shift).tap do |root|
410
+ parent = root
411
+ until missing_segments.empty?
412
+ obj = VirtualContainer.new(:module_node, missing_segments.shift)
413
+ parent.body.body = [obj]
414
+ parent = obj
415
+ end
416
+
417
+ if last_found_node.is_a? Parser
418
+ obj = last_found_node.handle_node(root)
419
+ last_found_node.nodes << obj
420
+ else
421
+ last_found_node.handle_node(root)
422
+ end
423
+ end
424
+ end
425
+
426
+ cls = VirtualContainer.new(:class_node, class_name)
427
+ cls.location = sing.location
428
+ if root.is_a? Parser
429
+ obj = root.handle_node(cls)
430
+ root.nodes << obj
431
+ else
432
+ root.handle_node(cls)
433
+ end
434
+
435
+ resolve_deferred_singleton(sing)
436
+ end
437
+
438
+ def find_object(container, name, container_only: false)
439
+ if container_only
440
+ return container&.all_modules&.named(name)&.first ||
441
+ container&.all_classes&.named(name)&.first ||
442
+ (find_object(container.parent, name, container_only:) if container&.parent) ||
443
+ parser.nodes.lazy
444
+ .filter { _1.is_a?(Container) && !_1.is_a?(DeferredSingletonClass) && _1.name == name }
445
+ .first ||
446
+ parser.nodes.lazy
447
+ .filter { _1.is_a?(Container) && !_1.is_a?(DeferredSingletonClass) }
448
+ .first { find_object(_1, name, container_only:) }
449
+ end
450
+
451
+ container&.all_modules&.named(name)&.first ||
452
+ container&.all_classes&.named(name)&.first ||
453
+ container&.all_instance_methods&.named(name)&.first ||
454
+ container&.all_instance_attributes&.named(name)&.first ||
455
+ container&.all_class_methods&.named(name)&.first ||
456
+ container&.all_class_attributes&.named(name)&.first ||
457
+ (find_object(container.parent, name, container_only:) if container&.parent) ||
458
+ parser.nodes.lazy.filter { _1.is_a?(Container) && _1.name == name }.first ||
459
+ parser.nodes.lazy.filter { _1.is_a? Container }.first { find_object(_1, name) }
460
+ end
461
+
462
+ def find_next_container(obj)
463
+ return obj.parent if obj.parent.is_a? Container
464
+
465
+ parent = obj.parent
466
+ parent = parent.parent while parent && !parent.is_a?(Container)
467
+ parent
468
+ end
469
+ end
470
+ end
471
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docrb
4
+ class Parser
5
+ class Constant
6
+ visible_attr_reader :name, :location
7
+ attr_accessor :parent, :doc
8
+
9
+ def initialize(parser, parent, node)
10
+ @object_id = parser.make_id(self)
11
+ @parent = parent
12
+ @name = node.name
13
+ @location = parser.location(node.location)
14
+ end
15
+
16
+ def id = @object_id
17
+ end
18
+ end
19
+ end