zard 0.0.1

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,1781 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ module Zard
6
+ module Parsing
7
+ class Parser
8
+ def self.call(source, path:)
9
+ new(source, path).call
10
+ end
11
+
12
+ def initialize(source, path)
13
+ @source = source
14
+ @path = path
15
+ end
16
+
17
+ def call
18
+ result = Prism.parse(@source)
19
+ diagnostics = result.errors.map do |error|
20
+ diagnostic("ruby.syntax", :error, error.message, span(error.location))
21
+ end
22
+
23
+ declarations = DeclarationCollector.new(
24
+ @source,
25
+ @path,
26
+ result.comments,
27
+ diagnostics,
28
+ result.encoding,
29
+ result.source
30
+ ).call(result.value)
31
+
32
+ Model::V1::Document.new(
33
+ path: @path,
34
+ declarations: declarations.freeze,
35
+ diagnostics: diagnostics.freeze
36
+ )
37
+ end
38
+
39
+ private
40
+
41
+ def diagnostic(code, severity, message, location)
42
+ Model::V1::Diagnostic.new(code: code, severity: severity, message: message, span: location)
43
+ end
44
+
45
+ def span(location)
46
+ Model::V1::SourceSpan.new(
47
+ path: @path,
48
+ start_line: location.start_line,
49
+ start_column: location.start_column,
50
+ end_line: location.end_line,
51
+ end_column: location.end_column,
52
+ start_offset: location.start_offset,
53
+ end_offset: location.end_offset
54
+ )
55
+ end
56
+ end
57
+
58
+ class DeclarationCollector < Prism::Visitor
59
+ ATTRIBUTE_KINDS = {
60
+ attr: :attribute_reader,
61
+ attr_reader: :attribute_reader,
62
+ attr_writer: :attribute_writer,
63
+ attr_accessor: :attribute_accessor
64
+ }.freeze
65
+ VISIBILITY_NAMES = %i[public protected private].freeze
66
+ CLASS_METHOD_VISIBILITY = {
67
+ private_class_method: :private,
68
+ public_class_method: :public
69
+ }.freeze
70
+ CONSTANT_VISIBILITY = {
71
+ private_constant: :private,
72
+ public_constant: :public
73
+ }.freeze
74
+ MIXIN_NAMES = %i[include prepend extend].freeze
75
+ METHOD_DEFINITION_NAMES = %i[define_method define_singleton_method].freeze
76
+ METHOD_REMOVAL_NAMES = %i[remove_method undef_method].freeze
77
+ EVALUATION_NAMES = %i[class_eval module_eval class_exec module_exec].freeze
78
+ INSTANCE_EVALUATION_NAMES = %i[instance_eval instance_exec].freeze
79
+ CONTAINER_BUILDERS = {
80
+ ["Data", :define] => {kind: :class, attribute_kind: :attribute_reader}.freeze,
81
+ ["Struct", :new] => {kind: :class, attribute_kind: :attribute_accessor}.freeze,
82
+ ["Class", :new] => {kind: :class, attribute_kind: nil}.freeze,
83
+ ["Module", :new] => {kind: :module, attribute_kind: nil}.freeze,
84
+ [nil, :DelegateClass] => {kind: :class, attribute_kind: nil}.freeze
85
+ }.freeze
86
+
87
+ def initialize(source, path, comments, diagnostics, encoding, prism_source)
88
+ @source = source
89
+ @path = path
90
+ @comments = comments.select { |comment| standalone?(comment) && !magic_comment?(comment) }
91
+ @diagnostics = diagnostics
92
+ @encoding = encoding
93
+ @prism_source = prism_source
94
+ @declarations = []
95
+ @namespace = []
96
+ @singleton_depth = 0
97
+ @singleton_receiver = nil
98
+ @singleton_receiver_span = nil
99
+ @refinement = nil
100
+ @refinement_span = nil
101
+ @instance_visibility = :public
102
+ @singleton_visibility = :public
103
+ @container_kind = nil
104
+ @module_function_mode = false
105
+ @method_depth = 0
106
+ @container_declaration_index = nil
107
+ @instance_evaluation_receiver = nil
108
+ @instance_evaluation_receiver_span = nil
109
+ end
110
+
111
+ def call(program)
112
+ program.accept(self)
113
+ @declarations.freeze
114
+ end
115
+
116
+ def visit_module_node(node)
117
+ declaration_index = collect_path_declaration(:module, node.constant_path.location.slice, node)
118
+ within_namespace(node.constant_path.location.slice, :module, declaration_index) { node.body&.accept(self) }
119
+ end
120
+
121
+ def visit_class_node(node)
122
+ builder_call = node.superclass if class_builder?(node.superclass)
123
+ declaration_index = collect_path_declaration(
124
+ :class,
125
+ node.constant_path.location.slice,
126
+ node,
127
+ superclass: node.superclass&.location&.slice,
128
+ superclass_span: node.superclass ? span(node.superclass.location) : nil
129
+ )
130
+ within_namespace(node.constant_path.location.slice, :class, declaration_index) do
131
+ collect_container_builder_attributes(builder_call) if builder_call
132
+ node.body&.accept(self)
133
+ end
134
+ end
135
+
136
+ def visit_constant_write_node(node)
137
+ path = node.name.to_s
138
+ visibility = constant_visibility_for(path)
139
+ replace_constant_declarations(path) unless self_referential_constant_guard?(node.value, path)
140
+ call = container_builder_call(node.value, guarded_name: path)
141
+ return visit_container_builder_write(path, node, call, visibility: visibility) if call
142
+
143
+ collect_path_declaration(:constant, path, node, visibility: visibility)
144
+ super
145
+ end
146
+
147
+ def visit_constant_path_write_node(node)
148
+ path = node.target.location.slice
149
+ visibility = constant_visibility_for(path)
150
+ guarded_name = constant_reference_name(node.target)
151
+ replace_constant_declarations(path) unless self_referential_constant_guard?(node.value, guarded_name)
152
+ call = container_builder_call(node.value, guarded_name: guarded_name)
153
+ return visit_container_builder_write(path, node, call, visibility: visibility) if call
154
+
155
+ collect_path_declaration(:constant, path, node, visibility: visibility)
156
+ super
157
+ end
158
+
159
+ def visit_constant_or_write_node(node)
160
+ call = container_builder_call(node.value)
161
+ return visit_container_builder_write(node.name.to_s, node, call) if call
162
+
163
+ super
164
+ end
165
+
166
+ def visit_constant_path_or_write_node(node)
167
+ call = container_builder_call(node.value)
168
+ return visit_container_builder_write(node.target.location.slice, node, call) if call
169
+
170
+ super
171
+ end
172
+
173
+ def visit_singleton_class_node(node)
174
+ previous_visibility = @singleton_visibility
175
+ previous_receiver = @singleton_receiver
176
+ previous_receiver_span = @singleton_receiver_span
177
+ @singleton_visibility = :public
178
+ @singleton_receiver = node.expression.location.slice
179
+ @singleton_receiver_span = span(node.expression.location)
180
+ @singleton_depth += 1
181
+ node.body&.accept(self)
182
+ ensure
183
+ @singleton_depth -= 1
184
+ @singleton_visibility = previous_visibility
185
+ @singleton_receiver = previous_receiver
186
+ @singleton_receiver_span = previous_receiver_span
187
+ end
188
+
189
+ def visit_def_node(node)
190
+ collect_method_declarations(node)
191
+
192
+ within_method_body { node.body&.accept(self) }
193
+ end
194
+
195
+ def visit_alias_method_node(node)
196
+ collect_method_alias(node.new_name.unescaped, node.old_name.unescaped, node)
197
+ end
198
+
199
+ def visit_undef_node(node)
200
+ return super if @method_depth.positive?
201
+
202
+ remove_method_declarations(node.names.filter_map { |name| attribute_name(name) })
203
+ end
204
+
205
+ def visit_call_node(node)
206
+ return super if @method_depth.positive?
207
+ return visit_current_instance_evaluation_call(node) if current_instance_evaluation_call?(node)
208
+ return if foreign_instance_evaluation_call?(node)
209
+ return if foreign_evaluation_call?(node)
210
+
211
+ return visit_refinement_call(node) if refinement_call?(node)
212
+ return visit_method_definition_call(node) if method_definition_call?(node)
213
+ return visit_const_set_call(node) if const_set_call?(node)
214
+ return visit_remove_const_call(node) if remove_const_call?(node)
215
+
216
+ collect_autoload_declaration(node) if autoload_call?(node)
217
+ collect_mixin_references(node) if mixin_call?(node)
218
+
219
+ if node.name == :module_function &&
220
+ (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)) &&
221
+ module_function_context?
222
+ return visit_module_function_call(node) { super }
223
+ end
224
+
225
+ if (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)) &&
226
+ (class_visibility = CLASS_METHOD_VISIBILITY[node.name])
227
+ return visit_class_method_visibility_call(node, class_visibility) { super }
228
+ end
229
+
230
+ if (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)) &&
231
+ (constant_visibility = CONSTANT_VISIBILITY[node.name])
232
+ return visit_constant_visibility_call(node, constant_visibility) { super }
233
+ end
234
+
235
+ visibility = VISIBILITY_NAMES.find do |name|
236
+ node.name == name && (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
237
+ end
238
+ return visit_visibility_call(node, visibility) { super } if visibility
239
+
240
+ if node.name == :alias_method && (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
241
+ collect_alias_method_call(node)
242
+ return super
243
+ end
244
+
245
+ if method_removal_call?(node)
246
+ names = node.arguments.arguments.filter_map { |argument| attribute_name(argument) }
247
+ remove_method_declarations(names)
248
+ return super
249
+ end
250
+
251
+ collect_attribute_declarations(node)
252
+ super
253
+ end
254
+
255
+ private
256
+
257
+ def current_instance_evaluation_call?(node)
258
+ instance_evaluation_call?(node) &&
259
+ (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
260
+ end
261
+
262
+ def foreign_instance_evaluation_call?(node)
263
+ instance_evaluation_call?(node) && node.receiver &&
264
+ !node.receiver.is_a?(Prism::SelfNode)
265
+ end
266
+
267
+ def instance_evaluation_call?(node)
268
+ INSTANCE_EVALUATION_NAMES.include?(node.name) && node.block
269
+ end
270
+
271
+ def visit_current_instance_evaluation_call(node)
272
+ previous_receiver = @instance_evaluation_receiver
273
+ previous_receiver_span = @instance_evaluation_receiver_span
274
+ @instance_evaluation_receiver = node.receiver&.location&.slice || "self"
275
+ @instance_evaluation_receiver_span = span(node.receiver.location) if node.receiver
276
+ node.block.body&.accept(self)
277
+ ensure
278
+ @instance_evaluation_receiver = previous_receiver
279
+ @instance_evaluation_receiver_span = previous_receiver_span
280
+ end
281
+
282
+ def foreign_evaluation_call?(node)
283
+ EVALUATION_NAMES.include?(node.name) && node.block && node.receiver &&
284
+ !node.receiver.is_a?(Prism::SelfNode)
285
+ end
286
+
287
+ def autoload_call?(node)
288
+ return false unless node.name == :autoload && node.block.nil? && node.arguments&.arguments&.length == 2
289
+ return false if @singleton_depth.positive? || @refinement
290
+
291
+ node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
292
+ end
293
+
294
+ def collect_autoload_declaration(node)
295
+ name = attribute_name(node.arguments.arguments.first)
296
+ collect_path_declaration(:constant, name, node) if name
297
+ end
298
+
299
+ def const_set_call?(node)
300
+ return false unless node.name == :const_set && node.block.nil? && node.arguments&.arguments&.length == 2
301
+ return false if @namespace.empty? || @singleton_depth.positive? || @refinement
302
+
303
+ node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
304
+ end
305
+
306
+ def visit_const_set_call(node)
307
+ name_node, value = node.arguments.arguments
308
+ name = attribute_name(name_node)
309
+ return unless name
310
+
311
+ visibility = constant_visibility_for(name)
312
+ replace_constant_declarations(name)
313
+ call = container_builder_call(value)
314
+ return visit_container_builder_write(name, node, call, visibility: visibility) if call
315
+
316
+ collect_path_declaration(:constant, name, node, visibility: visibility)
317
+ end
318
+
319
+ def remove_const_call?(node)
320
+ return false unless node.name == :remove_const && node.block.nil? && node.arguments&.arguments&.one?
321
+ return false if @namespace.empty? || @singleton_depth.positive? || @refinement
322
+
323
+ (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)) &&
324
+ attribute_name(node.arguments.arguments.first)
325
+ end
326
+
327
+ def visit_remove_const_call(node)
328
+ remove_constant_declarations(attribute_name(node.arguments.arguments.first))
329
+ end
330
+
331
+ def remove_constant_declarations(name)
332
+ remove_constant_path("#{current_namespace}::#{name}")
333
+ end
334
+
335
+ def replace_constant_declarations(path)
336
+ remove_constant_path(namespace_parts(path).join("::"))
337
+ end
338
+
339
+ def constant_visibility_for(path)
340
+ full_path = namespace_parts(path).join("::")
341
+ declaration = @declarations.reverse_each.find { |item| constant_declaration_path(item) == full_path }
342
+ declaration ? declaration.visibility : :public
343
+ end
344
+
345
+ def remove_constant_path(path)
346
+ container_declaration = @declarations[@container_declaration_index] if @container_declaration_index
347
+ @declarations.reject! do |declaration|
348
+ constant_declaration_path(declaration) == path ||
349
+ declaration.namespace == path ||
350
+ declaration.namespace&.start_with?("#{path}::")
351
+ end
352
+ @container_declaration_index = @declarations.index(container_declaration) if container_declaration
353
+ end
354
+
355
+ def self_referential_constant_guard?(node, name)
356
+ node.is_a?(Prism::OrNode) && constant_reference_name(node.left) == name
357
+ end
358
+
359
+ def constant_declaration_path(declaration)
360
+ return unless %i[class module constant].include?(declaration.kind)
361
+
362
+ [declaration.namespace, declaration.name].compact.join("::")
363
+ end
364
+
365
+ def class_builder?(node)
366
+ descriptor = container_builder_descriptor(node)
367
+ descriptor && descriptor.fetch(:kind) == :class
368
+ end
369
+
370
+ def container_builder_descriptor(node)
371
+ call = container_builder_call(node)
372
+ return unless call
373
+
374
+ CONTAINER_BUILDERS[container_builder_key(call)]
375
+ end
376
+
377
+ def visit_container_builder_write(path, node, call, visibility: constant_visibility_for(path))
378
+ descriptor = CONTAINER_BUILDERS.fetch(container_builder_key(call))
379
+ builder, builder_span = container_builder_reference(call)
380
+ declaration_index = collect_path_declaration(
381
+ descriptor.fetch(:kind),
382
+ path,
383
+ node,
384
+ visibility: visibility,
385
+ container_builder: builder,
386
+ container_builder_span: builder_span
387
+ )
388
+ within_namespace(path, descriptor.fetch(:kind), declaration_index) do
389
+ collect_container_builder_attributes(call)
390
+ call.block&.body&.accept(self)
391
+ end
392
+ end
393
+
394
+ def container_builder_reference(call)
395
+ return [call.location.slice, span(call.location)] unless call.block
396
+
397
+ length = call.block.location.start_offset - call.location.start_offset
398
+ source = @source.byteslice(call.location.start_offset, length).rstrip
399
+ location = Prism::Location.new(@prism_source, call.location.start_offset, source.bytesize)
400
+ [source, span(location)]
401
+ end
402
+
403
+ def collect_container_builder_attributes(call)
404
+ call = attribute_builder_call(call)
405
+ return unless call
406
+
407
+ descriptor = CONTAINER_BUILDERS.fetch(container_builder_key(call))
408
+ attribute_kind = descriptor.fetch(:attribute_kind)
409
+ return unless attribute_kind
410
+
411
+ builder_attribute_arguments(call).each do |argument|
412
+ replace_method_declarations(
413
+ kind: :instance_method,
414
+ names: attribute_generated_method_names(argument.unescaped, attribute_kind),
415
+ namespace: current_namespace,
416
+ receiver: nil,
417
+ refinement: @refinement
418
+ )
419
+ append_declaration(
420
+ kind: :"instance_#{attribute_kind}",
421
+ name: argument.unescaped,
422
+ namespace: current_namespace,
423
+ visibility: :public,
424
+ parameters: [].freeze,
425
+ node: argument,
426
+ comments: [].freeze,
427
+ parsed: {documentation: [].freeze, contracts: [].freeze}
428
+ )
429
+ end
430
+ end
431
+
432
+ def builder_attribute_arguments(call)
433
+ arguments = call.arguments&.arguments || []
434
+ arguments = arguments.drop(1) if container_builder_key(call) == ["Struct", :new] && arguments.first.is_a?(Prism::StringNode)
435
+ arguments.select { |argument| argument.is_a?(Prism::SymbolNode) || argument.is_a?(Prism::StringNode) }
436
+ end
437
+
438
+ def attribute_builder_call(call)
439
+ wrapped = false
440
+ while class_new_builder?(call)
441
+ return if wrapped && call.block
442
+
443
+ arguments = call.arguments&.arguments
444
+ return unless arguments&.one?
445
+
446
+ call = arguments.first
447
+ wrapped = true
448
+ end
449
+ return unless call.is_a?(Prism::CallNode)
450
+ return if wrapped && call.block
451
+
452
+ descriptor = CONTAINER_BUILDERS[container_builder_key(call)]
453
+ call if descriptor&.fetch(:attribute_kind)
454
+ end
455
+
456
+ def class_new_builder?(call)
457
+ container_builder_key(call) == ["Class", :new]
458
+ end
459
+
460
+ def container_builder_key(node)
461
+ return unless node.is_a?(Prism::CallNode)
462
+
463
+ receiver = node.receiver&.location&.slice&.sub(/\A::/, "")
464
+ [receiver, node.name]
465
+ end
466
+
467
+ def container_builder_call(node, guarded_name: nil)
468
+ node = unwrap_builder_guard(node, guarded_name) if guarded_name
469
+ node = node.receiver while freeze_tail?(node)
470
+ return unless node.is_a?(Prism::CallNode)
471
+
472
+ node if CONTAINER_BUILDERS.key?(container_builder_key(node))
473
+ end
474
+
475
+ def unwrap_builder_guard(node, guarded_name)
476
+ return node unless node.is_a?(Prism::OrNode)
477
+
478
+ if constant_reference_name(node.left) == guarded_name
479
+ node.right
480
+ else
481
+ node
482
+ end
483
+ end
484
+
485
+ def constant_reference_name(node)
486
+ return node.name.to_s if node.is_a?(Prism::ConstantReadNode)
487
+ return unless node.is_a?(Prism::ConstantPathNode)
488
+
489
+ parent = node.parent
490
+ return node.name.to_s unless parent
491
+
492
+ parent_name = constant_reference_name(parent)
493
+ "#{parent_name}::#{node.name}" if parent_name
494
+ end
495
+
496
+ def freeze_tail?(node)
497
+ node.is_a?(Prism::CallNode) && node.name == :freeze && !node.safe_navigation? &&
498
+ node.receiver && node.arguments.nil? && node.block.nil?
499
+ end
500
+
501
+ def method_definition_call?(node)
502
+ return false unless METHOD_DEFINITION_NAMES.include?(node.name)
503
+ return true if node.name == :define_singleton_method
504
+
505
+ (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)) &&
506
+ (!@namespace.empty? || @refinement)
507
+ end
508
+
509
+ def visit_method_definition_call(node)
510
+ arguments = node.arguments&.arguments || []
511
+ name = attribute_name(arguments.first) if arguments.first
512
+ collect_call_method_declaration(node, name) if name
513
+ within_method_body { node.block&.body&.accept(self) }
514
+ end
515
+
516
+ def collect_call_method_declaration(node, name)
517
+ parameters = parameter_names(node.block&.parameters&.parameters).freeze
518
+ singleton = node.name == :define_singleton_method
519
+ kind = singleton ? :singleton_method : current_method_kind
520
+ receiver = if singleton
521
+ node.receiver&.location&.slice || "self"
522
+ else
523
+ current_singleton_receiver
524
+ end
525
+ receiver_span = if singleton
526
+ span(node.receiver.location) if node.receiver
527
+ else
528
+ current_singleton_receiver_span
529
+ end
530
+ visibility = singleton ? @singleton_visibility : current_visibility
531
+ collect_method_entries(
532
+ kind: kind,
533
+ name: name,
534
+ namespace: current_namespace,
535
+ visibility: visibility,
536
+ parameters: parameters,
537
+ receiver: receiver,
538
+ receiver_span: receiver_span,
539
+ node: node
540
+ )
541
+ end
542
+
543
+ def refinement_call?(node)
544
+ node.name == :refine &&
545
+ (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)) &&
546
+ node.block &&
547
+ @container_kind == :module &&
548
+ @singleton_depth.zero? &&
549
+ node.arguments&.arguments&.length == 1
550
+ end
551
+
552
+ def visit_refinement_call(node)
553
+ target_node = node.arguments.arguments.fetch(0)
554
+ target = target_node.location.slice
555
+ collect_declaration(
556
+ kind: :refinement,
557
+ name: target,
558
+ namespace: current_namespace,
559
+ visibility: :public,
560
+ parameters: [].freeze,
561
+ node: node
562
+ )
563
+ within_refinement(target, span(target_node.location)) do
564
+ node.block.body&.accept(self)
565
+ end
566
+ end
567
+
568
+ def mixin_call?(node)
569
+ MIXIN_NAMES.include?(node.name) &&
570
+ (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)) &&
571
+ %i[class module].include?(@container_kind) &&
572
+ @container_declaration_index &&
573
+ @singleton_depth.zero?
574
+ end
575
+
576
+ def collect_mixin_references(node)
577
+ arguments = node.arguments&.arguments || []
578
+ return if arguments.empty?
579
+
580
+ declaration = @declarations.fetch(@container_declaration_index)
581
+ mixins = arguments.map do |argument|
582
+ Model::V1::MixinReference.new(
583
+ kind: node.name,
584
+ target: argument.location.slice,
585
+ span: span(argument.location)
586
+ )
587
+ end
588
+ @declarations[@container_declaration_index] = declaration_with_mixins(declaration, declaration.mixins + mixins)
589
+ end
590
+
591
+ def within_method_body
592
+ @method_depth += 1
593
+ yield
594
+ ensure
595
+ @method_depth -= 1
596
+ end
597
+
598
+ def collect_alias_method_call(node)
599
+ arguments = node.arguments&.arguments || []
600
+ return unless arguments.length == 2
601
+
602
+ new_name, old_name = arguments.map { |argument| attribute_name(argument) }
603
+ collect_method_alias(new_name, old_name, node) if new_name && old_name
604
+ end
605
+
606
+ def method_removal_call?(node)
607
+ METHOD_REMOVAL_NAMES.include?(node.name) &&
608
+ (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)) &&
609
+ node.block.nil? &&
610
+ node.arguments
611
+ end
612
+
613
+ def remove_method_declarations(names)
614
+ return if names.empty?
615
+
616
+ transform_method_declarations do |declaration|
617
+ if removable_method_declaration?(declaration)
618
+ declaration_after_method_removal(declaration, names)
619
+ else
620
+ declaration
621
+ end
622
+ end
623
+ end
624
+
625
+ def removable_method_declaration?(declaration)
626
+ declaration.namespace == current_namespace &&
627
+ declaration_scope_matches?(declaration, current_singleton_receiver) &&
628
+ (declaration.kind == current_method_kind || attribute_in_scope?(declaration, current_attribute_scope))
629
+ end
630
+
631
+ def declaration_after_method_removal(declaration, names)
632
+ unless declaration.kind.to_s.include?("attribute")
633
+ return if names.include?(declaration.name)
634
+
635
+ return declaration
636
+ end
637
+
638
+ remaining = attribute_method_names(declaration) - names
639
+ return if remaining.empty?
640
+ return declaration unless declaration.kind.to_s.end_with?("_attribute_accessor")
641
+
642
+ attribute_scope = declaration.kind.to_s.delete_suffix("accessor")
643
+ kind = if remaining == [declaration.name]
644
+ :"#{attribute_scope}reader"
645
+ elsif remaining == ["#{declaration.name}="]
646
+ :"#{attribute_scope}writer"
647
+ else
648
+ declaration.kind
649
+ end
650
+ declaration_with_kind_and_visibility(declaration, kind, declaration.visibility)
651
+ end
652
+
653
+ def replace_method_declarations(kind:, names:, namespace:, receiver:, refinement:)
654
+ attribute_scope = (kind == :singleton_method) ? "singleton_attribute_" : "instance_attribute_"
655
+ transform_method_declarations do |declaration|
656
+ matches = declaration.namespace == namespace &&
657
+ declaration_scope_matches_values?(declaration, receiver, refinement) &&
658
+ (declaration.kind == kind || attribute_in_scope?(declaration, attribute_scope))
659
+ matches ? declaration_after_method_removal(declaration, names) : declaration
660
+ end
661
+ end
662
+
663
+ def transform_method_declarations
664
+ container_declaration = @declarations[@container_declaration_index] if @container_declaration_index
665
+ @declarations = @declarations.filter_map { |declaration| yield declaration }
666
+ @container_declaration_index = @declarations.index(container_declaration) if container_declaration
667
+ end
668
+
669
+ def inline_alias_method_name(node)
670
+ return unless node.is_a?(Prism::CallNode) && node.name == :alias_method
671
+ return unless node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
672
+
673
+ arguments = node.arguments&.arguments || []
674
+ attribute_name(arguments.first) if arguments.length == 2
675
+ end
676
+
677
+ def collect_method_alias(name, target, node)
678
+ kind = current_method_kind
679
+ original = alias_target_declaration(kind, target)
680
+ unless original
681
+ @diagnostics << Model::V1::Diagnostic.new(
682
+ code: "alias.unresolved-target",
683
+ severity: :warning,
684
+ message: "The alias target is not declared in this source scope.",
685
+ span: span(node.location)
686
+ )
687
+ end
688
+
689
+ replace_method_declarations(
690
+ kind: kind,
691
+ names: [name],
692
+ namespace: current_namespace,
693
+ receiver: current_singleton_receiver,
694
+ refinement: @refinement
695
+ )
696
+ comments, parsed = parse_comments(node)
697
+ append_declaration(
698
+ kind: kind,
699
+ name: name,
700
+ namespace: current_namespace,
701
+ visibility: original&.visibility || current_visibility,
702
+ parameters: alias_target_parameters(original, target),
703
+ receiver: current_singleton_receiver,
704
+ receiver_span: current_singleton_receiver_span,
705
+ refinement: @refinement,
706
+ refinement_span: @refinement_span,
707
+ alias_target: target,
708
+ node: node,
709
+ comments: comments,
710
+ parsed: parsed
711
+ )
712
+ end
713
+
714
+ def alias_target_declaration(kind, target)
715
+ attribute_scope = (kind == :singleton_method) ? "singleton_attribute_" : "instance_attribute_"
716
+ @declarations.reverse_each.find do |declaration|
717
+ next unless declaration.namespace == current_namespace
718
+ next unless declaration_scope_matches?(declaration, current_singleton_receiver)
719
+
720
+ (declaration.kind == kind && declaration.name == target) ||
721
+ (attribute_in_scope?(declaration, attribute_scope) && attribute_method_names(declaration).include?(target))
722
+ end
723
+ end
724
+
725
+ def alias_target_parameters(declaration, target)
726
+ return [].freeze unless declaration
727
+ return declaration.parameters unless declaration.kind.to_s.include?("attribute")
728
+
729
+ target.end_with?("=") ? ["value"].freeze : [].freeze
730
+ end
731
+
732
+ def collect_method_declarations(node)
733
+ instance_evaluation_method = instance_evaluation_method?(node)
734
+ kind = (singleton_method?(node) || instance_evaluation_method) ? :singleton_method : :instance_method
735
+ name = node.name.to_s
736
+ namespace = current_namespace
737
+ parameters = parameter_names(node.parameters).freeze
738
+ receiver = node.receiver&.location&.slice || current_singleton_receiver || @instance_evaluation_receiver
739
+ receiver_span = if node.receiver
740
+ span(node.receiver.location)
741
+ else
742
+ current_singleton_receiver_span || @instance_evaluation_receiver_span
743
+ end
744
+ collect_method_entries(
745
+ kind: kind,
746
+ name: name,
747
+ namespace: namespace,
748
+ visibility: instance_evaluation_method ? :public : method_visibility(node),
749
+ parameters: parameters,
750
+ receiver: receiver,
751
+ receiver_span: receiver_span,
752
+ node: node
753
+ )
754
+ end
755
+
756
+ def instance_evaluation_method?(node)
757
+ @instance_evaluation_receiver && @singleton_depth.zero? && node.receiver.nil?
758
+ end
759
+
760
+ def collect_method_entries(kind:, name:, namespace:, visibility:, parameters:, receiver:, receiver_span:, node:)
761
+ module_function = kind == :instance_method && module_function_definition?(node)
762
+ replace_method_declarations(
763
+ kind: kind,
764
+ names: [name],
765
+ namespace: namespace,
766
+ receiver: receiver,
767
+ refinement: @refinement
768
+ )
769
+ unless module_function
770
+ return collect_declaration(
771
+ kind: kind,
772
+ name: name,
773
+ namespace: namespace,
774
+ visibility: visibility,
775
+ parameters: parameters,
776
+ receiver: receiver,
777
+ receiver_span: receiver_span,
778
+ refinement: @refinement,
779
+ refinement_span: @refinement_span,
780
+ node: node
781
+ )
782
+ end
783
+
784
+ replace_method_declarations(
785
+ kind: :singleton_method,
786
+ names: [name],
787
+ namespace: namespace,
788
+ receiver: receiver,
789
+ refinement: @refinement
790
+ )
791
+ comments, parsed = parse_comments(node)
792
+ append_declaration(
793
+ kind: :instance_method,
794
+ name: name,
795
+ namespace: namespace,
796
+ visibility: :private,
797
+ parameters: parameters,
798
+ receiver: receiver,
799
+ receiver_span: receiver_span,
800
+ refinement: @refinement,
801
+ refinement_span: @refinement_span,
802
+ node: node,
803
+ comments: comments,
804
+ parsed: parsed
805
+ )
806
+ append_declaration(
807
+ kind: :singleton_method,
808
+ name: name,
809
+ namespace: namespace,
810
+ visibility: :public,
811
+ parameters: parameters,
812
+ receiver: receiver,
813
+ receiver_span: receiver_span,
814
+ refinement: @refinement,
815
+ refinement_span: @refinement_span,
816
+ node: node,
817
+ comments: comments,
818
+ parsed: parsed
819
+ )
820
+ end
821
+
822
+ def collect_attribute_declarations(node)
823
+ attribute_kind = attribute_kind(node)
824
+ current_owner = node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
825
+ return unless attribute_kind && current_owner && !@namespace.empty?
826
+
827
+ arguments = node.arguments&.arguments || []
828
+ names = arguments.filter_map { |argument| attribute_name(argument) }
829
+ return if names.empty?
830
+
831
+ comments, parsed = parse_comments(node)
832
+ scope = @singleton_depth.positive? ? :singleton : :instance
833
+ kind = :"#{scope}_#{attribute_kind}"
834
+ names.each do |name|
835
+ replace_method_declarations(
836
+ kind: :"#{scope}_method",
837
+ names: attribute_generated_method_names(name, attribute_kind),
838
+ namespace: current_namespace,
839
+ receiver: current_singleton_receiver,
840
+ refinement: @refinement
841
+ )
842
+ append_declaration(
843
+ kind: kind,
844
+ name: name,
845
+ namespace: @namespace.join("::"),
846
+ visibility: current_visibility,
847
+ parameters: [].freeze,
848
+ receiver: current_singleton_receiver,
849
+ receiver_span: current_singleton_receiver_span,
850
+ refinement: @refinement,
851
+ refinement_span: @refinement_span,
852
+ node: node,
853
+ comments: comments,
854
+ parsed: parsed
855
+ )
856
+ end
857
+ end
858
+
859
+ def attribute_name(argument)
860
+ argument.unescaped if argument.is_a?(Prism::SymbolNode) || argument.is_a?(Prism::StringNode)
861
+ end
862
+
863
+ def attribute_kind(node)
864
+ kind = ATTRIBUTE_KINDS[node.name]
865
+ arguments = node.arguments&.arguments || []
866
+ return :attribute_accessor if node.name == :attr && arguments.length == 2 && arguments.last.is_a?(Prism::TrueNode)
867
+
868
+ kind
869
+ end
870
+
871
+ def attribute_generated_method_names(name, attribute_kind)
872
+ case attribute_kind
873
+ when :attribute_reader
874
+ [name]
875
+ when :attribute_writer
876
+ ["#{name}="]
877
+ else
878
+ [name, "#{name}="]
879
+ end
880
+ end
881
+
882
+ def collect_path_declaration(kind, path, node, visibility: constant_visibility_for(path), superclass: nil, superclass_span: nil, container_builder: nil, container_builder_span: nil)
883
+ parts = namespace_parts(path)
884
+ collect_declaration(
885
+ kind: kind,
886
+ name: parts.last,
887
+ namespace: (parts.length > 1) ? parts[0...-1].join("::") : nil,
888
+ visibility: visibility,
889
+ parameters: [].freeze,
890
+ superclass: superclass,
891
+ superclass_span: superclass_span,
892
+ container_builder: container_builder,
893
+ container_builder_span: container_builder_span,
894
+ node: node
895
+ )
896
+ end
897
+
898
+ def collect_declaration(kind:, name:, namespace:, visibility:, parameters:, node:, receiver: nil, receiver_span: nil, refinement: nil, refinement_span: nil, superclass: nil, superclass_span: nil, container_builder: nil, container_builder_span: nil)
899
+ comments, parsed = parse_comments(node)
900
+ append_declaration(
901
+ kind: kind,
902
+ name: name,
903
+ namespace: namespace,
904
+ visibility: visibility,
905
+ parameters: parameters,
906
+ receiver: receiver,
907
+ receiver_span: receiver_span,
908
+ refinement: refinement,
909
+ refinement_span: refinement_span,
910
+ superclass: superclass,
911
+ superclass_span: superclass_span,
912
+ container_builder: container_builder,
913
+ container_builder_span: container_builder_span,
914
+ node: node,
915
+ comments: comments,
916
+ parsed: parsed
917
+ )
918
+ end
919
+
920
+ def append_declaration(kind:, name:, namespace:, visibility:, parameters:, node:, comments:, parsed:, receiver: nil, receiver_span: nil, refinement: nil, refinement_span: nil, alias_target: nil, superclass: nil, superclass_span: nil, container_builder: nil, container_builder_span: nil)
921
+ @declarations << Model::V1::Declaration.new(
922
+ kind: kind,
923
+ name: name,
924
+ namespace: namespace,
925
+ visibility: visibility,
926
+ parameters: parameters,
927
+ receiver: receiver,
928
+ receiver_span: receiver_span,
929
+ refinement: refinement,
930
+ refinement_span: refinement_span,
931
+ alias_target: alias_target,
932
+ superclass: superclass,
933
+ superclass_span: superclass_span,
934
+ container_builder: container_builder,
935
+ container_builder_span: container_builder_span,
936
+ span: span(node.location),
937
+ comment_span: comment_span(comments),
938
+ documentation: parsed.fetch(:documentation).freeze,
939
+ contracts: parsed.fetch(:contracts).freeze
940
+ )
941
+ @declarations.length - 1
942
+ end
943
+
944
+ def parse_comments(node)
945
+ comments = comment_block_for(node.location.start_line)
946
+ parsed = CommentBlockParser.new(@source, @path, comments, @diagnostics, @encoding).call
947
+ [comments, parsed]
948
+ end
949
+
950
+ def within_namespace(name, kind, declaration_index)
951
+ previous_namespace = @namespace
952
+ previous_instance_visibility = @instance_visibility
953
+ previous_singleton_visibility = @singleton_visibility
954
+ previous_container_kind = @container_kind
955
+ previous_module_function_mode = @module_function_mode
956
+ previous_container_span = @declarations.fetch(@container_declaration_index).span if @container_declaration_index
957
+ previous_refinement = @refinement
958
+ previous_refinement_span = @refinement_span
959
+ previous_instance_evaluation_receiver = @instance_evaluation_receiver
960
+ previous_instance_evaluation_receiver_span = @instance_evaluation_receiver_span
961
+ @namespace = namespace_parts(name)
962
+ @instance_visibility = :public
963
+ @singleton_visibility = :public
964
+ @container_kind = kind
965
+ @module_function_mode = false
966
+ @container_declaration_index = declaration_index
967
+ @refinement = nil
968
+ @refinement_span = nil
969
+ @instance_evaluation_receiver = nil
970
+ @instance_evaluation_receiver_span = nil
971
+ yield
972
+ ensure
973
+ @namespace = previous_namespace
974
+ @instance_visibility = previous_instance_visibility
975
+ @singleton_visibility = previous_singleton_visibility
976
+ @container_kind = previous_container_kind
977
+ @module_function_mode = previous_module_function_mode
978
+ @container_declaration_index = @declarations.index { |declaration| declaration.span.equal?(previous_container_span) }
979
+ @refinement = previous_refinement
980
+ @refinement_span = previous_refinement_span
981
+ @instance_evaluation_receiver = previous_instance_evaluation_receiver
982
+ @instance_evaluation_receiver_span = previous_instance_evaluation_receiver_span
983
+ end
984
+
985
+ def within_refinement(target, target_span)
986
+ previous_instance_visibility = @instance_visibility
987
+ previous_singleton_visibility = @singleton_visibility
988
+ previous_container_kind = @container_kind
989
+ previous_module_function_mode = @module_function_mode
990
+ previous_container_span = @declarations.fetch(@container_declaration_index).span if @container_declaration_index
991
+ previous_refinement = @refinement
992
+ previous_refinement_span = @refinement_span
993
+ previous_instance_evaluation_receiver = @instance_evaluation_receiver
994
+ previous_instance_evaluation_receiver_span = @instance_evaluation_receiver_span
995
+ @instance_visibility = :public
996
+ @singleton_visibility = :public
997
+ @container_kind = :refinement
998
+ @module_function_mode = false
999
+ @container_declaration_index = nil
1000
+ @refinement = target
1001
+ @refinement_span = target_span
1002
+ @instance_evaluation_receiver = nil
1003
+ @instance_evaluation_receiver_span = nil
1004
+ yield
1005
+ ensure
1006
+ @instance_visibility = previous_instance_visibility
1007
+ @singleton_visibility = previous_singleton_visibility
1008
+ @container_kind = previous_container_kind
1009
+ @module_function_mode = previous_module_function_mode
1010
+ @container_declaration_index = @declarations.index { |declaration| declaration.span.equal?(previous_container_span) }
1011
+ @refinement = previous_refinement
1012
+ @refinement_span = previous_refinement_span
1013
+ @instance_evaluation_receiver = previous_instance_evaluation_receiver
1014
+ @instance_evaluation_receiver_span = previous_instance_evaluation_receiver_span
1015
+ end
1016
+
1017
+ def namespace_parts(name)
1018
+ prefix = name.start_with?("::") ? [] : @namespace
1019
+ prefix + name.sub(/\A::/, "").split("::")
1020
+ end
1021
+
1022
+ def singleton_method?(node)
1023
+ !node.receiver.nil? || @singleton_depth.positive?
1024
+ end
1025
+
1026
+ def method_visibility(node)
1027
+ return :public if node.receiver && @singleton_depth.zero?
1028
+
1029
+ current_visibility
1030
+ end
1031
+
1032
+ def current_visibility
1033
+ @singleton_depth.positive? ? @singleton_visibility : @instance_visibility
1034
+ end
1035
+
1036
+ def visit_visibility_call(node, visibility)
1037
+ arguments = node.arguments&.arguments
1038
+ if arguments.nil? || arguments.empty?
1039
+ @module_function_mode = false if module_function_context?
1040
+ set_current_visibility(visibility)
1041
+ return yield
1042
+ end
1043
+
1044
+ if arguments.any? { |argument| visibility_declaration?(argument) }
1045
+ return with_visibility(visibility) { yield }
1046
+ end
1047
+
1048
+ names = arguments.filter_map { |argument| attribute_name(argument) || inline_alias_method_name(argument) }
1049
+ yield
1050
+ unless names.empty?
1051
+ apply_named_visibility(
1052
+ names,
1053
+ visibility,
1054
+ node,
1055
+ method_kind: current_method_kind,
1056
+ attribute_scope: current_attribute_scope,
1057
+ receiver: current_singleton_receiver
1058
+ )
1059
+ end
1060
+ end
1061
+
1062
+ def visit_module_function_call(node)
1063
+ arguments = node.arguments&.arguments
1064
+ if arguments.nil? || arguments.empty?
1065
+ @instance_visibility = :private
1066
+ @module_function_mode = true
1067
+ return yield
1068
+ end
1069
+
1070
+ if arguments.any? { |argument| module_function_declaration?(argument) }
1071
+ return with_module_function_mode { yield }
1072
+ end
1073
+
1074
+ yield
1075
+ names = arguments.flat_map { |argument| module_function_argument_names(argument) }
1076
+ names.each { |name| apply_named_module_function(name) }
1077
+ end
1078
+
1079
+ def module_function_argument_names(argument)
1080
+ name = attribute_name(argument) || inline_alias_method_name(argument)
1081
+ return [name] if name
1082
+ return [] unless argument.is_a?(Prism::SplatNode)
1083
+
1084
+ call = argument.expression
1085
+ return [] unless call.is_a?(Prism::CallNode) && ATTRIBUTE_KINDS.key?(call.name)
1086
+ return [] unless call.receiver.nil? || call.receiver.is_a?(Prism::SelfNode)
1087
+
1088
+ kind = attribute_kind(call)
1089
+ (call.arguments&.arguments || []).filter_map { |item| attribute_name(item) }
1090
+ .flat_map { |item| attribute_generated_method_names(item, kind) }
1091
+ end
1092
+
1093
+ def module_function_context?
1094
+ @container_kind == :module && @singleton_depth.zero?
1095
+ end
1096
+
1097
+ def module_function_definition?(node)
1098
+ @module_function_mode &&
1099
+ module_function_context? &&
1100
+ (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
1101
+ end
1102
+
1103
+ def module_function_declaration?(node)
1104
+ node.is_a?(Prism::DefNode) ||
1105
+ (node.is_a?(Prism::CallNode) && node.name == :define_method &&
1106
+ (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)))
1107
+ end
1108
+
1109
+ def with_module_function_mode
1110
+ previous_visibility = @instance_visibility
1111
+ previous_mode = @module_function_mode
1112
+ @instance_visibility = :private
1113
+ @module_function_mode = true
1114
+ yield
1115
+ ensure
1116
+ @instance_visibility = previous_visibility
1117
+ @module_function_mode = previous_mode
1118
+ end
1119
+
1120
+ def apply_named_module_function(name)
1121
+ declaration = alias_target_declaration(:instance_method, name)
1122
+ return unless declaration
1123
+
1124
+ apply_module_function_instance_visibility(declaration, name)
1125
+ replace_method_declarations(
1126
+ kind: :singleton_method,
1127
+ names: [name],
1128
+ namespace: declaration.namespace,
1129
+ receiver: declaration.receiver,
1130
+ refinement: declaration.refinement
1131
+ )
1132
+ @declarations << declaration_with_kind_and_visibility(
1133
+ declaration,
1134
+ :singleton_method,
1135
+ :public,
1136
+ name: name,
1137
+ parameters: alias_target_parameters(declaration, name)
1138
+ )
1139
+ end
1140
+
1141
+ def apply_module_function_instance_visibility(declaration, name)
1142
+ index = @declarations.index(declaration)
1143
+ return unless index
1144
+
1145
+ replacements = if declaration.kind.to_s.end_with?("_attribute_accessor")
1146
+ target_kind, retained_kind = if name.end_with?("=")
1147
+ %i[instance_attribute_writer instance_attribute_reader]
1148
+ else
1149
+ %i[instance_attribute_reader instance_attribute_writer]
1150
+ end
1151
+ [
1152
+ declaration_with_kind_and_visibility(declaration, target_kind, :private),
1153
+ declaration_with_kind_and_visibility(declaration, retained_kind, declaration.visibility)
1154
+ ]
1155
+ else
1156
+ [declaration_with_visibility(declaration, :private)]
1157
+ end
1158
+
1159
+ container_declaration = @declarations[@container_declaration_index] if @container_declaration_index
1160
+ @declarations[index, 1] = replacements
1161
+ @container_declaration_index = @declarations.index(container_declaration) if container_declaration
1162
+ end
1163
+
1164
+ def visit_class_method_visibility_call(node, visibility)
1165
+ arguments = node.arguments&.arguments || []
1166
+ yield
1167
+ names = arguments.filter_map do |argument|
1168
+ if argument.is_a?(Prism::DefNode) && argument.receiver
1169
+ argument.name.to_s
1170
+ elsif argument.is_a?(Prism::CallNode) && argument.name == :define_singleton_method &&
1171
+ (argument.receiver.nil? || argument.receiver.is_a?(Prism::SelfNode))
1172
+ attribute_name(argument.arguments&.arguments&.first)
1173
+ else
1174
+ attribute_name(argument)
1175
+ end
1176
+ end
1177
+ unless names.empty?
1178
+ apply_named_visibility(
1179
+ names,
1180
+ visibility,
1181
+ node,
1182
+ method_kind: :singleton_method,
1183
+ attribute_scope: "singleton_attribute_",
1184
+ receiver: nil
1185
+ )
1186
+ end
1187
+ end
1188
+
1189
+ def visit_constant_visibility_call(node, visibility)
1190
+ arguments = node.arguments&.arguments || []
1191
+ yield
1192
+ names = arguments.filter_map { |argument| attribute_name(argument) }
1193
+ return if names.empty?
1194
+
1195
+ @declarations.map! do |declaration|
1196
+ if %i[class module constant].include?(declaration.kind) &&
1197
+ declaration.namespace == current_namespace &&
1198
+ names.include?(declaration.name)
1199
+ declaration_with_visibility(declaration, visibility)
1200
+ else
1201
+ declaration
1202
+ end
1203
+ end
1204
+ end
1205
+
1206
+ def visibility_declaration?(node)
1207
+ node.is_a?(Prism::DefNode) ||
1208
+ (node.is_a?(Prism::CallNode) && (ATTRIBUTE_KINDS.key?(node.name) ||
1209
+ (node.name == :define_method &&
1210
+ (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)))))
1211
+ end
1212
+
1213
+ def set_current_visibility(visibility)
1214
+ if @singleton_depth.positive?
1215
+ @singleton_visibility = visibility
1216
+ else
1217
+ @instance_visibility = visibility
1218
+ end
1219
+ end
1220
+
1221
+ def with_visibility(visibility)
1222
+ previous_visibility = current_visibility
1223
+ set_current_visibility(visibility)
1224
+ yield
1225
+ ensure
1226
+ set_current_visibility(previous_visibility)
1227
+ end
1228
+
1229
+ def apply_named_visibility(names, visibility, node, method_kind:, attribute_scope:, receiver:)
1230
+ attribute_match = false
1231
+ @declarations.map! do |declaration|
1232
+ next declaration unless declaration.namespace == current_namespace
1233
+ next declaration unless declaration_scope_matches?(declaration, receiver)
1234
+
1235
+ if declaration.kind == method_kind && names.include?(declaration.name)
1236
+ declaration_with_visibility(declaration, visibility)
1237
+ elsif attribute_in_scope?(declaration, attribute_scope) && (attribute_method_names(declaration) & names).any?
1238
+ attribute_match = true
1239
+ (visibility == :public) ? declaration : declaration_with_visibility(declaration, visibility)
1240
+ else
1241
+ declaration
1242
+ end
1243
+ end
1244
+
1245
+ return unless attribute_match
1246
+
1247
+ @diagnostics << Model::V1::Diagnostic.new(
1248
+ code: "visibility.named-attribute",
1249
+ severity: :warning,
1250
+ message: "Use a lexical or inline visibility modifier for an attribute declaration.",
1251
+ span: span(node.location)
1252
+ )
1253
+ end
1254
+
1255
+ def declaration_scope_matches?(declaration, receiver)
1256
+ declaration_scope_matches_values?(declaration, receiver, @refinement)
1257
+ end
1258
+
1259
+ def declaration_scope_matches_values?(declaration, receiver, refinement)
1260
+ receiver_matches = if receiver.nil? || receiver == "self"
1261
+ declaration.receiver.nil? || declaration.receiver == "self"
1262
+ else
1263
+ declaration.receiver == receiver
1264
+ end
1265
+ receiver_matches && declaration.refinement == refinement
1266
+ end
1267
+
1268
+ def current_namespace
1269
+ @namespace.empty? ? nil : @namespace.join("::")
1270
+ end
1271
+
1272
+ def current_method_kind
1273
+ @singleton_depth.positive? ? :singleton_method : :instance_method
1274
+ end
1275
+
1276
+ def current_singleton_receiver
1277
+ @singleton_receiver if @singleton_depth.positive?
1278
+ end
1279
+
1280
+ def current_singleton_receiver_span
1281
+ @singleton_receiver_span if @singleton_depth.positive?
1282
+ end
1283
+
1284
+ def current_attribute_scope
1285
+ @singleton_depth.positive? ? "singleton_attribute_" : "instance_attribute_"
1286
+ end
1287
+
1288
+ def attribute_in_scope?(declaration, scope)
1289
+ declaration.kind.to_s.start_with?(scope)
1290
+ end
1291
+
1292
+ def attribute_method_names(declaration)
1293
+ case declaration.kind.to_s
1294
+ when /_attribute_reader\z/
1295
+ [declaration.name]
1296
+ when /_attribute_writer\z/
1297
+ ["#{declaration.name}="]
1298
+ else
1299
+ [declaration.name, "#{declaration.name}="]
1300
+ end
1301
+ end
1302
+
1303
+ def declaration_with_visibility(declaration, visibility)
1304
+ declaration_with_kind_and_visibility(declaration, declaration.kind, visibility)
1305
+ end
1306
+
1307
+ def declaration_with_mixins(declaration, mixins)
1308
+ declaration_with_kind_and_visibility(declaration, declaration.kind, declaration.visibility, mixins: mixins.freeze)
1309
+ end
1310
+
1311
+ def declaration_with_kind_and_visibility(declaration, kind, visibility, name: declaration.name, parameters: declaration.parameters, mixins: declaration.mixins)
1312
+ Model::V1::Declaration.new(
1313
+ kind: kind,
1314
+ name: name,
1315
+ namespace: declaration.namespace,
1316
+ visibility: visibility,
1317
+ parameters: parameters,
1318
+ receiver: declaration.receiver,
1319
+ receiver_span: declaration.receiver_span,
1320
+ refinement: declaration.refinement,
1321
+ refinement_span: declaration.refinement_span,
1322
+ alias_target: declaration.alias_target,
1323
+ superclass: declaration.superclass,
1324
+ superclass_span: declaration.superclass_span,
1325
+ container_builder: declaration.container_builder,
1326
+ container_builder_span: declaration.container_builder_span,
1327
+ mixins: mixins,
1328
+ span: declaration.span,
1329
+ comment_span: declaration.comment_span,
1330
+ documentation: declaration.documentation,
1331
+ contracts: declaration.contracts
1332
+ )
1333
+ end
1334
+
1335
+ def parameter_names(parameters)
1336
+ return [] unless parameters
1337
+
1338
+ names = []
1339
+ names.concat(parameters.requireds.map { |parameter| parameter_name(parameter) })
1340
+ names.concat(parameters.optionals.map { |parameter| parameter_name(parameter) })
1341
+ names << prefixed_name("*", parameters.rest) if parameters.rest
1342
+ names.concat(parameters.posts.map { |parameter| parameter_name(parameter) })
1343
+ names.concat(parameters.keywords.map { |parameter| "#{parameter.name}:" })
1344
+ names << prefixed_name("**", parameters.keyword_rest) if parameters.keyword_rest
1345
+ names << prefixed_name("&", parameters.block) if parameters.block
1346
+ names
1347
+ end
1348
+
1349
+ def parameter_name(parameter)
1350
+ return parameter.location.slice unless parameter.respond_to?(:name)
1351
+
1352
+ parameter.name.to_s
1353
+ end
1354
+
1355
+ def prefixed_name(prefix, parameter)
1356
+ return parameter.location.slice unless parameter.respond_to?(:name)
1357
+
1358
+ parameter.name ? "#{prefix}#{parameter.name}" : prefix
1359
+ end
1360
+
1361
+ def standalone?(comment)
1362
+ offset = comment.location.start_offset
1363
+ line_start = offset.zero? ? 0 : (@source.b.rindex("\n", offset - 1) || -1) + 1
1364
+ prefix = @source.byteslice(line_start, offset - line_start)
1365
+ prefix.match?(/\A[\t ]*\z/)
1366
+ end
1367
+
1368
+ def magic_comment?(comment)
1369
+ return false if comment.location.start_line > 2
1370
+
1371
+ comment.location.slice.match?(/\A#\s*(?:en)?coding\s*[:=]|\A#\s*frozen_string_literal\s*:/i)
1372
+ end
1373
+
1374
+ def comment_block_for(declaration_line)
1375
+ expected_line = declaration_line - 1
1376
+ block = []
1377
+
1378
+ @comments.reverse_each do |comment|
1379
+ next if comment.location.end_line > expected_line
1380
+ break if comment.location.end_line < expected_line
1381
+
1382
+ block << comment
1383
+ expected_line = comment.location.start_line - 1
1384
+ end
1385
+
1386
+ block.reverse
1387
+ end
1388
+
1389
+ def comment_span(comments)
1390
+ return nil if comments.empty?
1391
+
1392
+ first = comments.first.location
1393
+ last = comments.last.location
1394
+ Model::V1::SourceSpan.new(
1395
+ path: @path,
1396
+ start_line: first.start_line,
1397
+ start_column: first.start_column,
1398
+ end_line: last.end_line,
1399
+ end_column: last.end_column,
1400
+ start_offset: first.start_offset,
1401
+ end_offset: last.end_offset
1402
+ )
1403
+ end
1404
+
1405
+ def span(location)
1406
+ Model::V1::SourceSpan.new(
1407
+ path: @path,
1408
+ start_line: location.start_line,
1409
+ start_column: location.start_column,
1410
+ end_line: location.end_line,
1411
+ end_column: location.end_column,
1412
+ start_offset: location.start_offset,
1413
+ end_offset: location.end_offset
1414
+ )
1415
+ end
1416
+ end
1417
+
1418
+ class CommentBlockParser
1419
+ def initialize(source, path, comments, diagnostics, encoding)
1420
+ @source = source
1421
+ @path = path
1422
+ @comments = comments
1423
+ @diagnostics = diagnostics
1424
+ @encoding = encoding
1425
+ end
1426
+
1427
+ def call
1428
+ documentation = []
1429
+ contracts = []
1430
+ seen_non_extrbs_annotation = false
1431
+ pending = nil
1432
+
1433
+ @comments.each do |comment|
1434
+ raw = comment.location.slice
1435
+ body = comment_body(raw)
1436
+
1437
+ if annotation_boundary?(body, raw)
1438
+ flush_pending(documentation, pending)
1439
+ pending = nil
1440
+ elsif pending
1441
+ append_continuation(pending, body, comment)
1442
+ next
1443
+ end
1444
+
1445
+ if (tag = noncanonical_known_tag(body, raw))
1446
+ code = (tag == "@extrbs") ? "extrbs.noncanonical-spacing" : "documentation.noncanonical-spacing"
1447
+ message = (tag == "@extrbs") ? "Write @extrbs as '# @extrbs' with one space." : "Write ZARD annotations immediately after '# '."
1448
+ add_diagnostic(code, :warning, message, comment.location)
1449
+ pending = pending_documentation(raw_tag(body, raw, comment.location), comment)
1450
+ seen_non_extrbs_annotation = true
1451
+ next
1452
+ end
1453
+
1454
+ if non_utf8_documentation?(body, raw)
1455
+ add_diagnostic(
1456
+ "documentation.non-utf8",
1457
+ :error,
1458
+ "ZARD documentation requires UTF-8 source text.",
1459
+ comment.location
1460
+ )
1461
+ pending = pending_documentation(raw_tag(body, raw, comment.location), comment)
1462
+ seen_non_extrbs_annotation = true
1463
+ next
1464
+ end
1465
+
1466
+ if (payload = contract_payload(body, "@extrbs"))
1467
+ if seen_non_extrbs_annotation
1468
+ add_diagnostic(
1469
+ "extrbs.not-first",
1470
+ :warning,
1471
+ "Place @extrbs before the other annotations in this comment block.",
1472
+ comment.location
1473
+ )
1474
+ end
1475
+ contracts << contract(:extrbs, payload, raw, comment.location)
1476
+ elsif (payload = contract_payload(body, "@rbs"))
1477
+ contracts << contract(:rbs, payload, raw, comment.location)
1478
+ seen_non_extrbs_annotation = true
1479
+ elsif raw.start_with?("#:")
1480
+ contracts << contract(:rbs, raw.delete_prefix("#:").strip, raw, comment.location)
1481
+ seen_non_extrbs_annotation = true
1482
+ elsif body.match?(/\A@param(?:\s|\z)/)
1483
+ pending = pending_documentation(parse_named_tag(body, raw, comment.location, :param, "@param"), comment)
1484
+ seen_non_extrbs_annotation = true
1485
+ elsif body.match?(/\A@yieldparam(?:\s|\z)/)
1486
+ pending = pending_documentation(parse_named_tag(body, raw, comment.location, :yieldparam, "@yieldparam"), comment)
1487
+ seen_non_extrbs_annotation = true
1488
+ elsif body.match?(/\A@option(?:\s|\z)/)
1489
+ pending = pending_documentation(parse_option(body, raw, comment.location), comment)
1490
+ seen_non_extrbs_annotation = true
1491
+ elsif body.match?(/\A@raise(?:\s|\z)/)
1492
+ pending = pending_documentation(parse_named_tag(body, raw, comment.location, :raise, "@raise"), comment)
1493
+ seen_non_extrbs_annotation = true
1494
+ elsif body.match?(/\A@return(?:\s|\z)/)
1495
+ pending = pending_documentation(parse_nameless_tag(body, raw, comment.location, :return, "@return"), comment)
1496
+ seen_non_extrbs_annotation = true
1497
+ elsif body.match?(/\A@yieldreturn(?:\s|\z)/)
1498
+ pending = pending_documentation(parse_nameless_tag(body, raw, comment.location, :yieldreturn, "@yieldreturn"), comment)
1499
+ seen_non_extrbs_annotation = true
1500
+ elsif body.match?(/\A@note(?:\s|\z)/)
1501
+ pending = pending_documentation(parse_description_tag(body, raw, comment.location, :note, "@note"), comment)
1502
+ seen_non_extrbs_annotation = true
1503
+ elsif body.match?(/\A@see(?:\s|\z)/)
1504
+ pending = pending_documentation(parse_description_tag(body, raw, comment.location, :see, "@see"), comment)
1505
+ seen_non_extrbs_annotation = true
1506
+ elsif body.match?(/\A@deprecated(?:\s|\z)/)
1507
+ pending = pending_documentation(parse_description_tag(body, raw, comment.location, :deprecated, "@deprecated"), comment)
1508
+ seen_non_extrbs_annotation = true
1509
+ elsif body.match?(/\A@example(?:\s|\z)/)
1510
+ pending = pending_documentation(parse_description_tag(body, raw, comment.location, :example, "@example"), comment)
1511
+ seen_non_extrbs_annotation = true
1512
+ elsif body.start_with?("@")
1513
+ pending = pending_documentation(raw_tag(body, raw, comment.location), comment)
1514
+ seen_non_extrbs_annotation = true
1515
+ elsif !body.empty?
1516
+ documentation << documentation_tag(:text, nil, nil, body, raw, comment.location)
1517
+ end
1518
+ end
1519
+
1520
+ flush_pending(documentation, pending)
1521
+
1522
+ {documentation: documentation, contracts: contracts}
1523
+ end
1524
+
1525
+ private
1526
+
1527
+ def comment_body(raw)
1528
+ raw.sub(/\A# ?/, "").delete_suffix("\r")
1529
+ end
1530
+
1531
+ def annotation_boundary?(body, raw)
1532
+ raw.start_with?("#:", "# @") || !noncanonical_known_tag(body, raw).nil?
1533
+ end
1534
+
1535
+ def noncanonical_known_tag(body, raw)
1536
+ return if raw.start_with?("# @")
1537
+
1538
+ candidate = body.lstrip[/\A@[^\s]*/]
1539
+ return unless %w[@extrbs @rbs @param @yieldparam @option @raise @return @yieldreturn @note @see @deprecated @example].include?(candidate)
1540
+
1541
+ candidate
1542
+ end
1543
+
1544
+ def pending_documentation(tag, comment)
1545
+ lines = tag.description.strip.empty? ? [] : [tag.description]
1546
+ {tag: tag, comments: [comment], lines: lines}
1547
+ end
1548
+
1549
+ def append_continuation(pending, body, comment)
1550
+ pending.fetch(:lines) << (body.strip.empty? ? "" : body)
1551
+ pending.fetch(:comments) << comment
1552
+ end
1553
+
1554
+ def flush_pending(documentation, pending)
1555
+ return unless pending
1556
+
1557
+ lines = pending.fetch(:lines)
1558
+ lines.shift while lines.first == ""
1559
+ lines.pop while lines.last == ""
1560
+ tag = pending.fetch(:tag)
1561
+ comments = pending.fetch(:comments)
1562
+ description = lines.join("\n")
1563
+ location = comments.first.location
1564
+
1565
+ if tag.name != :raw && description.empty?
1566
+ add_diagnostic(
1567
+ "documentation.empty-description",
1568
+ :warning,
1569
+ "Add a description to this ZARD documentation tag.",
1570
+ location
1571
+ )
1572
+ end
1573
+
1574
+ documentation << Model::V1::DocumentationTag.new(
1575
+ name: tag.name,
1576
+ owner: tag.owner,
1577
+ subject: tag.subject,
1578
+ claim: tag.claim,
1579
+ description: description,
1580
+ span: combined_span(comments),
1581
+ raw: combined_raw(comments)
1582
+ )
1583
+ end
1584
+
1585
+ def combined_span(comments)
1586
+ first = comments.first.location
1587
+ last = comments.last.location
1588
+ Model::V1::SourceSpan.new(
1589
+ path: @path,
1590
+ start_line: first.start_line,
1591
+ start_column: first.start_column,
1592
+ end_line: last.end_line,
1593
+ end_column: last.end_column,
1594
+ start_offset: first.start_offset,
1595
+ end_offset: last.end_offset
1596
+ )
1597
+ end
1598
+
1599
+ def combined_raw(comments)
1600
+ first = comments.first.location.start_offset
1601
+ last = comments.last.location.end_offset
1602
+ @source.byteslice(first, last - first)
1603
+ end
1604
+
1605
+ def contract_payload(body, tag)
1606
+ match = body.match(/\A#{Regexp.escape(tag)}(?:\s+(.*)|\z)/)
1607
+ match && match[1].to_s
1608
+ end
1609
+
1610
+ def non_utf8_documentation?(body, raw)
1611
+ return false if @encoding == Encoding::UTF_8
1612
+ return false if body.empty?
1613
+ return false if contract_payload(body, "@extrbs") || contract_payload(body, "@rbs")
1614
+ return false if raw.start_with?("#:")
1615
+
1616
+ true
1617
+ end
1618
+
1619
+ def contract(channel, payload, raw, location)
1620
+ contract_payload, marker, note = payload.partition(" -- ")
1621
+ Model::V1::Contract.new(
1622
+ channel: channel,
1623
+ payload: contract_payload,
1624
+ note: marker.empty? ? nil : note,
1625
+ span: span(location),
1626
+ raw: raw
1627
+ )
1628
+ end
1629
+
1630
+ def parse_named_tag(body, raw, location, name, prefix)
1631
+ rest = body.delete_prefix(prefix).sub(/\A[\t ]/, "")
1632
+ head, description = split_named_description(rest)
1633
+
1634
+ unless head
1635
+ yard_like(raw, location)
1636
+ return raw_tag(body, raw, location)
1637
+ end
1638
+
1639
+ subject, claim, valid = parse_named_head(head)
1640
+ unless valid
1641
+ yard_like(raw, location)
1642
+ return raw_tag(body, raw, location)
1643
+ end
1644
+
1645
+ documentation_tag(name, subject, claim, description, raw, location)
1646
+ end
1647
+
1648
+ def parse_option(body, raw, location)
1649
+ rest = body.delete_prefix("@option").sub(/\A[\t ]/, "")
1650
+ head, description = split_named_description(rest)
1651
+ unless head
1652
+ yard_like(raw, location)
1653
+ return raw_tag(body, raw, location)
1654
+ end
1655
+
1656
+ owner, option_head = head.split(/\s+/, 2)
1657
+ subject, claim, valid = parse_named_head(option_head.to_s)
1658
+
1659
+ if !owner || !valid
1660
+ yard_like(raw, location)
1661
+ return raw_tag(body, raw, location)
1662
+ end
1663
+
1664
+ documentation_tag(:option, subject, claim, description, raw, location, owner: owner)
1665
+ end
1666
+
1667
+ def parse_nameless_tag(body, raw, location, name, prefix)
1668
+ rest = body.delete_prefix(prefix).sub(/\A[\t ]/, "")
1669
+
1670
+ if rest == "—" || rest.start_with?("— ")
1671
+ add_diagnostic(
1672
+ "documentation.redundant-marker",
1673
+ :warning,
1674
+ "Remove the redundant em dash from #{prefix} without a type claim.",
1675
+ location
1676
+ )
1677
+ description = rest.delete_prefix("—").sub(/\A /, "")
1678
+ return documentation_tag(name, nil, nil, description, raw, location)
1679
+ end
1680
+
1681
+ if rest.start_with?("[")
1682
+ claim, remainder = bracketed_claim(rest)
1683
+ valid_claim = claim && (remainder == " —" || remainder.start_with?(" — "))
1684
+ if !valid_claim
1685
+ yard_like(raw, location)
1686
+ return raw_tag(body, raw, location)
1687
+ end
1688
+
1689
+ description = remainder.delete_prefix(" —").sub(/\A /, "")
1690
+ return documentation_tag(name, nil, claim, description, raw, location)
1691
+ end
1692
+
1693
+ documentation_tag(name, nil, nil, rest, raw, location)
1694
+ end
1695
+
1696
+ def parse_description_tag(body, raw, location, name, prefix)
1697
+ description = body.delete_prefix(prefix).sub(/\A[\t ]/, "")
1698
+ documentation_tag(name, nil, nil, description, raw, location)
1699
+ end
1700
+
1701
+ def split_named_description(rest)
1702
+ match = rest.match(/\A(.+?) —(?: (.*))?\z/)
1703
+ return [nil, nil] unless match
1704
+
1705
+ [match[1], match[2].to_s]
1706
+ end
1707
+
1708
+ def parse_named_head(head)
1709
+ subject, remainder = head.split(/\s+/, 2)
1710
+ return [nil, nil, false] unless subject
1711
+ return [subject, nil, true] unless remainder
1712
+
1713
+ claim, trailing = bracketed_claim(remainder)
1714
+ return [subject, claim, true] if claim && trailing.empty?
1715
+
1716
+ [subject, nil, false]
1717
+ end
1718
+
1719
+ def bracketed_claim(text)
1720
+ return [nil, text] unless text.start_with?("[")
1721
+
1722
+ depth = 0
1723
+ text.each_char.with_index do |character, index|
1724
+ depth += 1 if character == "["
1725
+ depth -= 1 if character == "]"
1726
+ next unless depth.zero?
1727
+
1728
+ return [text[1...index], text[(index + 1)..]]
1729
+ end
1730
+
1731
+ [nil, text]
1732
+ end
1733
+
1734
+ def yard_like(raw, location)
1735
+ add_diagnostic(
1736
+ "documentation.yard-like",
1737
+ :warning,
1738
+ "This tag is preserved as raw text because it does not use the ZARD em dash marker.",
1739
+ location
1740
+ )
1741
+ end
1742
+
1743
+ def raw_tag(body, raw, location)
1744
+ documentation_tag(:raw, nil, nil, body, raw, location)
1745
+ end
1746
+
1747
+ def documentation_tag(name, subject, claim, description, raw, location, owner: nil)
1748
+ Model::V1::DocumentationTag.new(
1749
+ name: name,
1750
+ owner: owner,
1751
+ subject: subject,
1752
+ claim: claim,
1753
+ description: description,
1754
+ span: span(location),
1755
+ raw: raw
1756
+ )
1757
+ end
1758
+
1759
+ def add_diagnostic(code, severity, message, location)
1760
+ @diagnostics << Model::V1::Diagnostic.new(
1761
+ code: code,
1762
+ severity: severity,
1763
+ message: message,
1764
+ span: span(location)
1765
+ )
1766
+ end
1767
+
1768
+ def span(location)
1769
+ Model::V1::SourceSpan.new(
1770
+ path: @path,
1771
+ start_line: location.start_line,
1772
+ start_column: location.start_column,
1773
+ end_line: location.end_line,
1774
+ end_column: location.end_column,
1775
+ start_offset: location.start_offset,
1776
+ end_offset: location.end_offset
1777
+ )
1778
+ end
1779
+ end
1780
+ end
1781
+ end