rigortype 0.4.4 → 0.4.5

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 (147) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -5
  3. data/docs/handbook/11-sig-gen.md +90 -17
  4. data/docs/manual/02-cli-reference.md +39 -9
  5. data/docs/manual/03-configuration.md +10 -8
  6. data/docs/manual/04-diagnostics.md +16 -5
  7. data/docs/manual/05-inspecting-types.md +2 -2
  8. data/docs/manual/06-baseline.md +1 -1
  9. data/docs/manual/07-plugins.md +2 -1
  10. data/docs/manual/08-skills.md +7 -0
  11. data/docs/manual/10-mcp-server.md +8 -1
  12. data/docs/manual/18-removing-dead-code.md +18 -7
  13. data/docs/manual/plugins/rigor-rspec.md +101 -1
  14. data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
  15. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +111 -7
  16. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +190 -0
  17. data/lib/rigor/analysis/check_rules/rule_ids.rb +3 -1
  18. data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
  19. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +13 -4
  20. data/lib/rigor/analysis/check_rules.rb +228 -15
  21. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
  22. data/lib/rigor/analysis/incremental.rb +49 -0
  23. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
  24. data/lib/rigor/analysis/incremental_session.rb +166 -24
  25. data/lib/rigor/analysis/path_expansion.rb +3 -1
  26. data/lib/rigor/analysis/reachability/graph.rb +90 -13
  27. data/lib/rigor/analysis/reachability/scan.rb +279 -39
  28. data/lib/rigor/analysis/rule_catalog.rb +17 -4
  29. data/lib/rigor/analysis/runner/pool_coordinator.rb +69 -16
  30. data/lib/rigor/analysis/runner.rb +69 -65
  31. data/lib/rigor/analysis/severity_stamp.rb +11 -0
  32. data/lib/rigor/analysis/worker_session.rb +12 -1
  33. data/lib/rigor/cache/descriptor.rb +40 -7
  34. data/lib/rigor/cache/engine_source.rb +3 -2
  35. data/lib/rigor/cache/incremental_snapshot.rb +26 -8
  36. data/lib/rigor/cache/rbs_descriptor.rb +2 -1
  37. data/lib/rigor/cache/store.rb +4 -3
  38. data/lib/rigor/cli/command.rb +3 -1
  39. data/lib/rigor/cli/docs_command.rb +3 -2
  40. data/lib/rigor/cli/doctor_command.rb +1 -1
  41. data/lib/rigor/cli/explain_command.rb +3 -2
  42. data/lib/rigor/cli/plugin_command.rb +2 -1
  43. data/lib/rigor/cli/plugins_command.rb +2 -1
  44. data/lib/rigor/cli/sig_gen_command.rb +32 -11
  45. data/lib/rigor/cli/skill_command.rb +2 -1
  46. data/lib/rigor/cli/skill_describe.rb +3 -2
  47. data/lib/rigor/cli/unused_command.rb +10 -7
  48. data/lib/rigor/cli.rb +3 -2
  49. data/lib/rigor/config_audit.rb +60 -15
  50. data/lib/rigor/configuration/severity_profile.rb +20 -2
  51. data/lib/rigor/configuration.rb +29 -3
  52. data/lib/rigor/effects/signature_sources.rb +3 -1
  53. data/lib/rigor/environment/bundle_sig_discovery.rb +3 -2
  54. data/lib/rigor/environment/gated_signature_guard.rb +2 -1
  55. data/lib/rigor/environment/gated_signature_plan.rb +2 -1
  56. data/lib/rigor/environment/installed_gem_set.rb +2 -1
  57. data/lib/rigor/environment/missing_gem_constant_index.rb +4 -3
  58. data/lib/rigor/environment/rbs_loader.rb +21 -1
  59. data/lib/rigor/inference/block_call_timing.rb +7 -1
  60. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  61. data/lib/rigor/inference/captured_locals.rb +44 -13
  62. data/lib/rigor/inference/clause_widening.rb +90 -0
  63. data/lib/rigor/inference/definer_resolution.rb +87 -20
  64. data/lib/rigor/inference/expression_typer.rb +35 -5
  65. data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
  66. data/lib/rigor/inference/macro_block_self_type.rb +20 -6
  67. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +170 -35
  68. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
  69. data/lib/rigor/inference/method_dispatcher.rb +67 -3
  70. data/lib/rigor/inference/module_function_state.rb +137 -42
  71. data/lib/rigor/inference/narrowing.rb +54 -10
  72. data/lib/rigor/inference/parameter_inference_collector.rb +4 -26
  73. data/lib/rigor/inference/project_patched_methods.rb +168 -4
  74. data/lib/rigor/inference/project_patched_mixins.rb +162 -0
  75. data/lib/rigor/inference/project_patched_scanner.rb +51 -12
  76. data/lib/rigor/inference/project_patched_visibility.rb +214 -0
  77. data/lib/rigor/inference/project_seed_tables.rb +120 -0
  78. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +113 -0
  79. data/lib/rigor/inference/refined_dispatch/own_method.rb +41 -12
  80. data/lib/rigor/inference/reflective_ivar_write.rb +6 -6
  81. data/lib/rigor/inference/return_barrier.rb +16 -0
  82. data/lib/rigor/inference/scope_indexer.rb +675 -105
  83. data/lib/rigor/inference/self_call_ivar_writes.rb +11 -6
  84. data/lib/rigor/inference/singleton_external_ahead.rb +64 -0
  85. data/lib/rigor/inference/singleton_hook_decline.rb +7 -0
  86. data/lib/rigor/inference/statement_evaluator.rb +446 -139
  87. data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
  88. data/lib/rigor/lens/contract_reader.rb +45 -2
  89. data/lib/rigor/lens/document.rb +2 -1
  90. data/lib/rigor/lens/file_reader.rb +12 -2
  91. data/lib/rigor/lens/members.rb +16 -2
  92. data/lib/rigor/lens/run.rb +34 -7
  93. data/lib/rigor/mcp/server.rb +2 -2
  94. data/lib/rigor/plugin/base.rb +3 -2
  95. data/lib/rigor/plugin/io_boundary.rb +42 -6
  96. data/lib/rigor/plugin/isolation.rb +4 -3
  97. data/lib/rigor/protection/discovery_seed.rb +9 -36
  98. data/lib/rigor/protection/mutation_cache.rb +8 -2
  99. data/lib/rigor/rooted_glob.rb +27 -0
  100. data/lib/rigor/scope/discovery_index.rb +5 -0
  101. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -0
  102. data/lib/rigor/scope/resolution_chain.rb +40 -5
  103. data/lib/rigor/scope.rb +45 -2
  104. data/lib/rigor/sig_gen/alias_index.rb +2 -1
  105. data/lib/rigor/sig_gen/classification.rb +4 -1
  106. data/lib/rigor/sig_gen/generator.rb +167 -22
  107. data/lib/rigor/sig_gen/layout_index.rb +5 -2
  108. data/lib/rigor/sig_gen/observation_collector.rb +137 -11
  109. data/lib/rigor/sig_gen/renderer.rb +13 -155
  110. data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
  111. data/lib/rigor/sig_gen/write_reporter.rb +203 -0
  112. data/lib/rigor/sig_gen/write_result.rb +3 -1
  113. data/lib/rigor/sig_gen/writer.rb +5 -3
  114. data/lib/rigor/signature_path_audit.rb +4 -2
  115. data/lib/rigor/type/constant.rb +10 -1
  116. data/lib/rigor/version.rb +1 -1
  117. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
  118. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +1 -1
  119. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +1 -1
  120. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +6 -1
  121. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  122. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +1 -1
  123. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +4 -3
  124. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +1 -1
  125. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +1 -1
  126. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +1 -1
  127. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -1
  128. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
  129. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  130. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +38 -9
  131. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
  132. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +1 -1
  133. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +1 -1
  134. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +7 -2
  135. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -5
  136. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +18 -17
  137. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  138. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
  139. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +4 -2
  140. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +2 -2
  141. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +2 -2
  142. data/sig/rigor/plugin/io_boundary.rbs +5 -2
  143. data/sig/rigor/rooted_glob.rbs +7 -0
  144. data/sig/rigor/scope.rbs +4 -3
  145. data/skills/rigor-doctor/references/01-checks.md +23 -11
  146. data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
  147. metadata +13 -1
@@ -62,9 +62,14 @@ module Rigor
62
62
  # may be one of its descendants (#1878): a `def self.x = const_get(:X)` runs on whichever subclass it
63
63
  # is called on, and a module's method on whichever class includes, prepends or extends it. {Graph}
64
64
  # then looks the literal name up in those descendants as well.
65
+ #
66
+ # `anywhere` is true when the receiver is a `self` the walk cannot name at all — the block of an eval call
67
+ # on a local or another computed receiver (`k.class_eval { const_get(:E) }`, #1946). {Graph} then keeps
68
+ # every declaration the literal could name.
65
69
  DynamicUse = Data.define(:name, :prefix, :reason, :site, :path, :line, :scope, :within, :inherit, :role,
66
- :receiver_open) do
67
- def initialize(scope: :namespace, within: nil, inherit: true, role: :production, receiver_open: false, **)
70
+ :receiver_open, :anywhere) do
71
+ def initialize(scope: :namespace, within: nil, inherit: true, role: :production, receiver_open: false,
72
+ anywhere: false, **)
68
73
  super
69
74
  end
70
75
 
@@ -102,7 +107,18 @@ module Rigor
102
107
  end
103
108
  end
104
109
 
105
- Result = Data.define(:declarations, :references, :dynamic_uses)
110
+ # An ancestry edge written outside a declaration's header and body statements (#1946): `K.include(Mix)`,
111
+ # `K.send(:include, Mix)` and the `prepend` / `extend` / `public_send` / `__send__` forms, an `include`
112
+ # in an eval block on `K` or nested in a conditional, and the superclass of `K = Class.new(Base)`. `target`
113
+ # is the class the module joins, `ancestor` the module (or superclass); each is a {Reference} resolved in
114
+ # the call's nesting, or nil when it is not a constant the walk can name — a local receiver, an
115
+ # `include` whose `self` is unknown, or a computed argument (`K.include(mods.first)`). {Graph} reads
116
+ # these only to widen a `const_get` on `self`'s reach, never for ordinary constant resolution.
117
+ Ancestry = Data.define(:target, :ancestor, :path, :line)
118
+
119
+ Result = Data.define(:declarations, :references, :dynamic_uses, :ancestries) do
120
+ def initialize(ancestries: [].freeze, **) = super
121
+ end
106
122
 
107
123
  # Roles a referring file can have (ADR-102 WD8). A reference edge carries its referrer's role so
108
124
  # "used only by its own test" is a reportable category rather than a bucket boundary.
@@ -163,7 +179,7 @@ module Rigor
163
179
  walker = Walker.new(path: path, role: role || role_for(path, root: root))
164
180
  walker.walk(parsed.value, [])
165
181
  Result.new(declarations: walker.declarations.freeze, references: walker.references.freeze,
166
- dynamic_uses: walker.dynamic_uses.freeze)
182
+ dynamic_uses: walker.dynamic_uses.freeze, ancestries: walker.ancestries.freeze)
167
183
  end
168
184
 
169
185
  # The values a local variable can hold, read off its assignments in one local scope, for a
@@ -263,17 +279,47 @@ module Rigor
263
279
  EVAL_FAMILY = %i[class_eval module_eval class_exec module_exec instance_eval instance_exec].freeze
264
280
  private_constant :EVAL_FAMILY
265
281
 
282
+ # Calls that define a method from their block: `define_method`'s block is an instance method's body,
283
+ # `define_singleton_method`'s a singleton method's (#1946).
284
+ METHOD_DEFINERS = { define_method: :instance, define_singleton_method: :singleton }.freeze
285
+ private_constant :METHOD_DEFINERS
286
+
266
287
  # A call is a dynamic-resolution site, and its block may run with another `self` ({#rebinds_self?}).
267
288
  def record_call(node, nesting)
268
289
  record_dynamic_use(node, nesting)
269
- return unless node.block.is_a?(Prism::BlockNode) && rebinds_self?(node)
270
-
271
- @rebinding_blocks << node.block
272
- @eval_receivers[node.block] = receiver_reference(node.receiver, nesting) if eval_on_constant?(node)
290
+ record_mixin(node, nesting)
291
+ return unless node.block.is_a?(Prism::BlockNode)
292
+
293
+ classify_block(node, nesting)
294
+ end
295
+
296
+ def classify_block(node, nesting)
297
+ block = node.block
298
+ if rebinds_self?(node)
299
+ @rebinding_blocks << block
300
+ frame = eval_frame(node, nesting)
301
+ @eval_frames[block] = frame if frame
302
+ elsif meta_new_call?(node)
303
+ @meta_new_blocks << block
304
+ elsif METHOD_DEFINERS.key?(node.name) && (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
305
+ @definer_blocks[block] = METHOD_DEFINERS.fetch(node.name)
306
+ end
273
307
  end
274
308
 
275
- def eval_on_constant?(call)
276
- EVAL_FAMILY.include?(call.name) && constant_receiver?(call.receiver)
309
+ # The `self` of an eval call's block (#1878, #1946): the receiver when it is a constant, `:unknown` when
310
+ # it is a local or another computed value, and nil — the reading the walk had before — on `self`,
311
+ # `self.class`, an implicit receiver or `Object`. `singleton_defs` is whether a plain `def` in the block
312
+ # defines a singleton method of the receiver (`instance_eval`) rather than an instance method.
313
+ def eval_frame(call, nesting)
314
+ return anonymous_subclass_frame(call, nesting) if call.name == :new
315
+ return nil unless EVAL_FAMILY.include?(call.name)
316
+
317
+ singleton_defs = %i[instance_eval instance_exec].include?(call.name)
318
+ receiver = call.receiver
319
+ return EvalFrame.new(receiver_reference(receiver, nesting), singleton_defs) if constant_receiver?(receiver)
320
+ return nil if receiver.nil? || self_receiver?(receiver) || constant_path?(receiver)
321
+
322
+ EvalFrame.new(:unknown, singleton_defs)
277
323
  end
278
324
 
279
325
  # Rigor knows the argument's shape, which is the whole reason this can be tiered rather than treated
@@ -306,6 +352,7 @@ module Rigor
306
352
  else []
307
353
  end
308
354
  bounds = [[nil, :namespace, nil]] if bounds.empty?
355
+ record_anywhere(node, name, reason)
309
356
  bounds.each do |prefix, scope, within|
310
357
  @dynamic_uses << DynamicUse.new(name: name, prefix: prefix, scope: scope, within: within,
311
358
  inherit: anchors.include?(TOP_LEVEL), role: @role,
@@ -338,21 +385,55 @@ module Rigor
338
385
  end
339
386
  end
340
387
 
388
+ # A literal `const_get` on a `self` the walk cannot name at all reaches any declaration of that name.
389
+ def record_anywhere(node, name, reason)
390
+ return unless name && !name.start_with?("::") && unknown_self?(node)
391
+
392
+ @dynamic_uses << DynamicUse.new(name: name, prefix: nil, anywhere: true, role: @role, site: site(node),
393
+ path: @path, line: node.location.start_line, reason: reason)
394
+ end
395
+
341
396
  # Whether a `const_get` on `self` can run on a descendant of the enclosing declaration (#1878): in a
342
397
  # method, or a block that rebinds `self`, `self` is whichever subclass or includer the method was
343
398
  # called on. Directly in a class body, or inside `Other.class_eval { }`, it is the declaration itself.
344
399
  def runtime_self?(node)
345
- node.name == :const_get && self_receiver?(node.receiver) && @eval_receiver.nil? &&
400
+ node.name == :const_get && self_receiver?(node.receiver) && @eval_frame.nil? &&
346
401
  !(@self_kind == :declaration && !@in_block && !@self_rebound)
347
402
  end
348
403
 
404
+ # Whether a `const_get` on `self` runs on a receiver the walk cannot name at all (#1946): directly in the
405
+ # block of an eval call on a computed receiver, or in a method that block defines when the receiver form
406
+ # names the eval receiver ({#method_receiver?}).
407
+ def unknown_self?(node)
408
+ return false unless node.name == :const_get && self_receiver?(node.receiver)
409
+ return @eval_frame.receiver == :unknown && !node.receiver.is_a?(Prism::CallNode) if @eval_frame
410
+
411
+ !@eval_method.nil? && @eval_method.receiver == :unknown && method_receiver?(node.receiver)
412
+ end
413
+
414
+ # Whether `receiver` names the eval receiver in a method an eval block defines: `self.class` in an
415
+ # instance method (`self` is an instance of the receiver or of a descendant), an implicit receiver or
416
+ # `self` in a singleton method. An implicit `const_get` in an instance method raises `NoMethodError`.
417
+ def method_receiver?(receiver)
418
+ class_of_self = receiver.is_a?(Prism::CallNode)
419
+ @eval_method.kind == :instance ? class_of_self : !class_of_self
420
+ end
421
+
422
+ # Inside a method an eval block on a constant defines, the receiver form that names that constant reads
423
+ # it as a runtime `self` that may be any of its descendants (#1946).
424
+ def eval_method_references(receiver)
425
+ return [] unless @eval_method&.receiver.is_a?(Reference) && method_receiver?(receiver)
426
+
427
+ [@eval_method.receiver]
428
+ end
429
+
349
430
  # Where a `const_get` on `self` (implicit, `self`, or `self.class`) looks its name up: `:declaration`
350
431
  # when `self` is the enclosing class or module, `:top_level` when it is `Class` or a singleton class,
351
432
  # `:unknown` otherwise. A block's `self` is unknown — `included do`, `class_eval` and an instance-
352
433
  # evaluated DSL block (`configure { }`, an RSpec `describe` body) all rebind it, and the walk cannot
353
434
  # tell those from an `each` block. So is the `self` of a `def Other.x` or of a `class << Other` body.
354
435
  def self_lookup(receiver)
355
- return :declaration if @eval_receiver && !receiver.is_a?(Prism::CallNode)
436
+ return :declaration if @eval_frame&.receiver.is_a?(Reference) && !receiver.is_a?(Prism::CallNode)
356
437
  return :unknown if @in_block || @self_rebound
357
438
 
358
439
  class_of_self = receiver.is_a?(Prism::CallNode)
@@ -374,7 +455,7 @@ module Rigor
374
455
  receiver = node.receiver
375
456
  namespaces =
376
457
  if self_receiver?(receiver)
377
- eval_self_references(receiver) || self_references(node, nesting)
458
+ eval_self_references(receiver) || (self_references(node, nesting) + eval_method_references(receiver))
378
459
  elsif constant_receiver?(receiver)
379
460
  [receiver_reference(receiver, nesting)]
380
461
  else
@@ -388,7 +469,8 @@ module Rigor
388
469
 
389
470
  # Inside `Other.class_eval { }` an implicit or `self` receiver is `Other` (#1878).
390
471
  def eval_self_references(receiver)
391
- [@eval_receiver] if @eval_receiver && !receiver.is_a?(Prism::CallNode)
472
+ frame = @eval_frame
473
+ [frame.receiver] if frame&.receiver.is_a?(Reference) && !receiver.is_a?(Prism::CallNode)
392
474
  end
393
475
 
394
476
  # The enclosing declaration as a rooted {Reference}, so {Graph} anchors it as it anchors a constant
@@ -407,9 +489,13 @@ module Rigor
407
489
  def rebinds_self?(call)
408
490
  return true if SELF_REBINDING.include?(call.name)
409
491
 
492
+ @owner.nil? && meta_new_call?(call)
493
+ end
494
+
495
+ def meta_new_call?(call)
410
496
  recv = call.receiver
411
497
  name = recv && Source::ConstantPath.qualified_name_or_nil(recv)&.delete_prefix("::")
412
- @owner.nil? && !name.nil? && META_NEW[name] == call.name
498
+ !name.nil? && META_NEW[name] == call.name
413
499
  end
414
500
 
415
501
  def self_receiver?(receiver)
@@ -490,11 +576,127 @@ module Rigor
490
576
  end
491
577
  end
492
578
 
579
+ # #1946 — the {Walker}'s reading of ancestry written outside a declaration's own statements, as
580
+ # {Ancestry} edges: receiver-form and `send`-spelled mixin calls, a mixin call whose `self` is not the
581
+ # enclosing declaration body, and the superclass of `K = Class.new(Base)`.
582
+ module AncestrySites
583
+ private
584
+
585
+ # An anonymous `Class.new(Base) { }` block's `self` is a subclass of `Base` that declares no constant of
586
+ # its own, so a `const_get` on it is read at `Base` (#1946).
587
+ def anonymous_subclass_frame(call, nesting)
588
+ superclass = call.arguments&.arguments&.first
589
+ return nil unless call.receiver.is_a?(Prism::ConstantReadNode) && call.receiver.name == :Class &&
590
+ constant_receiver?(superclass)
591
+
592
+ EvalFrame.new(receiver_reference(superclass, nesting), false)
593
+ end
594
+
595
+ def constant_path?(node)
596
+ node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
597
+ end
598
+
599
+ # Mixin calls: the ones a class body writes, and their `send` spellings.
600
+ MIXINS = %i[include prepend extend].freeze
601
+ private_constant :MIXINS
602
+
603
+ SENDS = %i[send public_send __send__].freeze
604
+ private_constant :SENDS
605
+
606
+ # Records an {Ancestry} for a mixin call the declaration's own statements do not carry (#1946). A
607
+ # receiver-less `include` directly in a class body is already one of its `includes` when every argument
608
+ # is a constant; a computed one (`include Mods.pick`) is an ancestor the walk cannot name. A `self`
609
+ # argument is the `self` the call is written in (`Foo.include(self)` in `module Mix`), not the receiver.
610
+ def record_mixin(node, nesting)
611
+ modules = mixin_arguments(node)
612
+ return if modules.nil? || modules.empty?
613
+ return if node.receiver.nil? && !SENDS.include?(node.name) && @body_statements.include?(node) &&
614
+ modules.all? { |arg| mixin_reference(arg, nesting) }
615
+
616
+ target = mixin_target(node, nesting)
617
+ modules.each do |arg|
618
+ ancestor = arg.is_a?(Prism::SelfNode) ? self_target(node, nesting) : mixin_reference(arg, nesting)
619
+ next if target.nil? && ancestor.nil?
620
+
621
+ @ancestries << Ancestry.new(target: target, ancestor: ancestor, path: @path,
622
+ line: node.location.start_line)
623
+ end
624
+ end
625
+
626
+ # The module arguments of a mixin call, or nil when it is not one.
627
+ def mixin_arguments(node)
628
+ args = node.arguments&.arguments || []
629
+ return args if MIXINS.include?(node.name)
630
+ return nil unless SENDS.include?(node.name)
631
+
632
+ verb = args.first
633
+ name = verb.unescaped if verb.is_a?(Prism::SymbolNode) || verb.is_a?(Prism::StringNode)
634
+ # Compared as strings: `to_sym` raises on a literal that is not valid in its encoding (`send("\xFF")`).
635
+ MIXINS.any? { |mixin| mixin.name == name } ? args.drop(1) : nil
636
+ end
637
+
638
+ def mixin_reference(arg, nesting)
639
+ return nil unless constant_path?(arg) && Source::ConstantPath.qualified_name_or_nil(arg)
640
+
641
+ receiver_reference(arg, nesting)
642
+ end
643
+
644
+ # The class a mixin call adds to: a constant receiver, or for `self` and an implicit receiver whatever
645
+ # `self` is — the eval receiver in an eval block, the enclosing declaration in its body. Anything else,
646
+ # the top level (`Object`, whose descendants are every class) included, is unknown (nil).
647
+ def mixin_target(node, nesting)
648
+ receiver = node.receiver
649
+ return mixin_reference(receiver, nesting) unless receiver.nil? || receiver.is_a?(Prism::SelfNode)
650
+
651
+ self_target(node, nesting)
652
+ end
653
+
654
+ # What `self` is at `node` as a {Reference}, where the walk can name it; nil otherwise.
655
+ def self_target(node, nesting)
656
+ frame = @eval_frame
657
+ return frame&.receiver.is_a?(Reference) ? frame.receiver : nil if frame || @eval_method
658
+ return nil if @in_block || @self_rebound || @self_kind != :declaration || nesting.empty?
659
+
660
+ rooted_reference(nesting.join("::"), node)
661
+ end
662
+
663
+ def rooted_reference(fqn, node)
664
+ Reference.new(as_written: fqn, nesting: [].freeze, from: nil, role: @role, path: @path,
665
+ line: node.location.start_line, rooted: true)
666
+ end
667
+
668
+ # `K = Class.new(Base)` makes `Base` an ancestor of `K` (#1946). It stays out of the declaration's
669
+ # `superclass`, which ordinary constant resolution reads: the engine does not resolve a constant
670
+ # through a `Class.new` superclass either, and the two are pinned to agree.
671
+ def record_class_new_superclass(call, fqn, nesting)
672
+ superclass = call.arguments&.arguments&.first
673
+ return unless call.receiver.name == :Class && superclass
674
+
675
+ ancestor = if superclass.is_a?(Prism::SelfNode) then self_target(call, nesting)
676
+ else mixin_reference(superclass, nesting)
677
+ end
678
+ @ancestries << Ancestry.new(target: rooted_reference(fqn, call), ancestor: ancestor,
679
+ path: @path, line: call.location.start_line)
680
+ end
681
+
682
+ # The receiver-less statements directly in a declaration body.
683
+ def body_statements(body)
684
+ body.child_nodes.select { |stmt| stmt.is_a?(Prism::CallNode) && stmt.receiver.nil? }
685
+ .to_set.compare_by_identity
686
+ end
687
+ end
688
+
689
+ # The `self` of an eval block ({DynamicSites#eval_frame}): a {Reference}, or `:unknown`.
690
+ EvalFrame = Data.define(:receiver, :singleton_defs)
691
+ # A method an eval block defines on its receiver, and whether it is an `:instance` or `:singleton` method.
692
+ EvalMethod = Data.define(:receiver, :kind)
693
+
493
694
  # Single-pass walker. Tracks `nesting` as the stack of enclosing declaration names.
494
695
  class Walker
495
696
  include DynamicSites
697
+ include AncestrySites
496
698
 
497
- attr_reader :declarations, :references, :dynamic_uses
699
+ attr_reader :declarations, :references, :dynamic_uses, :ancestries
498
700
 
499
701
  def initialize(path:, role:)
500
702
  @path = path
@@ -502,6 +704,10 @@ module Rigor
502
704
  @declarations = []
503
705
  @references = []
504
706
  @dynamic_uses = []
707
+ @ancestries = []
708
+ # The receiver-less statements directly in the declaration body being walked, which its own
709
+ # `includes` already carry. See {#record_mixin}.
710
+ @body_statements = Set.new.compare_by_identity
505
711
  # The declaration a meta-new rvalue is being walked for, or nil. See {#walk_constant_write}.
506
712
  @owner = nil
507
713
  # The node whose subtree holds the local variables in scope: the file, a method, or a class body.
@@ -512,10 +718,15 @@ module Rigor
512
718
  # `class_eval` or anonymous `Class.new` block. See {#rebinds_self?}.
513
719
  @self_rebound = false
514
720
  @rebinding_blocks = Set.new.compare_by_identity
515
- # The constant receiver of each `*_eval` block, and the one the walk is inside (nil when `self` is not
516
- # such a constant). See {#eval_on_constant?}.
517
- @eval_receivers = {}.compare_by_identity
518
- @eval_receiver = nil
721
+ # The {EvalFrame} of each `*_eval` block, and the one whose `self` the walk is directly in (nil when
722
+ # `self` is not an eval receiver). See {#eval_frame}.
723
+ @eval_frames = {}.compare_by_identity
724
+ @eval_frame = nil
725
+ # Inside a method an eval block defines (a `def`, or a `define_method` / `define_singleton_method`
726
+ # block), the eval receiver and the method's kind; nil elsewhere. See {#eval_method_references}.
727
+ @eval_method = nil
728
+ @meta_new_blocks = Set.new.compare_by_identity
729
+ @definer_blocks = {}.compare_by_identity
519
730
  # What `self` is here, for {#self_lookup}: `:declaration` in a class or module body (and at the top
520
731
  # level), `:singleton_method` in a `def self.x` or a def in a `class << self` body,
521
732
  # `:instance_method` in any other def, `:singleton_class` in a `class << self` body outside its
@@ -583,14 +794,18 @@ module Rigor
583
794
 
584
795
  # The body's nesting names the declaration itself, so a credit inherited from a meta-new rvalue
585
796
  # this declaration sits in must not leak into it.
586
- owner = @owner
587
- saved = [@self_rebound, @in_block, @module_body]
797
+ walk_body(node.body, nesting + [name], module_body)
798
+ end
799
+
800
+ def walk_body(body, nesting, module_body)
801
+ saved = [@owner, @self_rebound, @in_block, @module_body, @body_statements]
588
802
  @owner = nil
589
803
  @self_rebound = @in_block = false
590
804
  @module_body = module_body
591
- in_local_scope(node.body, :declaration) { walk(node.body, nesting + [name]) }
592
- @owner = owner
593
- @self_rebound, @in_block, @module_body = saved
805
+ @body_statements = body_statements(body)
806
+ in_local_scope(body, :declaration) { walk(body, nesting) }
807
+ ensure
808
+ @owner, @self_rebound, @in_block, @module_body, @body_statements = saved
594
809
  end
595
810
 
596
811
  # A meta-new rvalue — `Class.new(Base) { ... }`, `Struct.new(:a) do ... end` — is the declaration's
@@ -623,6 +838,7 @@ module Rigor
623
838
  fqn = (nesting + [node.name.to_s]).join("::")
624
839
  @declarations << Declaration.new(fqn: fqn, path: @path,
625
840
  line: node.location.start_line, superclass: nil, includes: [].freeze)
841
+ record_class_new_superclass(call, fqn, nesting)
626
842
  fqn
627
843
  end
628
844
 
@@ -630,24 +846,35 @@ module Rigor
630
846
  # top level of the body is inspected: a mixin applied inside a conditional or a nested def is not a
631
847
  # static ancestor edge.
632
848
  def mixin_names(body)
633
- body.child_nodes.filter_map do |stmt|
634
- next unless stmt.is_a?(Prism::CallNode) && %i[include prepend extend].include?(stmt.name)
635
- next if stmt.receiver
849
+ body.child_nodes.flat_map do |stmt|
850
+ next [] unless stmt.is_a?(Prism::CallNode) && %i[include prepend extend].include?(stmt.name)
851
+ next [] if stmt.receiver
636
852
 
637
- arg = stmt.arguments&.arguments&.first
638
- arg && Source::ConstantPath.qualified_name_or_nil(arg)
853
+ (stmt.arguments&.arguments || []).filter_map { |arg| Source::ConstantPath.qualified_name_or_nil(arg) }
639
854
  end
640
855
  end
641
856
 
642
857
  def in_local_scope(scope, self_kind = self_kind_in(scope))
643
- saved = [@local_scope, @block_params, @self_kind, @eval_receiver]
644
- @eval_receiver = nil
858
+ saved = [@local_scope, @block_params, @self_kind, @eval_frame, @eval_method]
859
+ @eval_method = eval_def(scope)
860
+ @eval_frame = nil
645
861
  @local_scope = scope
646
862
  @block_params = Set.new.freeze
647
863
  @self_kind = self_kind
648
864
  yield
649
865
  ensure
650
- @local_scope, @block_params, @self_kind, @eval_receiver = saved
866
+ @local_scope, @block_params, @self_kind, @eval_frame, @eval_method = saved
867
+ end
868
+
869
+ # The method a `def` directly in an eval block defines on the eval receiver (#1946): a plain `def` is an
870
+ # instance method, or a singleton one under `instance_eval`; a `def self.x` is a singleton method.
871
+ def eval_def(scope)
872
+ frame = @eval_frame
873
+ return nil unless frame && scope.is_a?(Prism::DefNode)
874
+
875
+ if scope.receiver.nil? then EvalMethod.new(frame.receiver, frame.singleton_defs ? :singleton : :instance)
876
+ elsif scope.receiver.is_a?(Prism::SelfNode) then EvalMethod.new(frame.receiver, :singleton)
877
+ end
651
878
  end
652
879
 
653
880
  # The {#self_lookup} kind inside `scope`, from the kind around it. Only a receiver-less def or a `def
@@ -681,17 +908,30 @@ module Rigor
681
908
  end
682
909
 
683
910
  def in_block(block)
684
- saved = [@block_params, @self_rebound, @in_block, @eval_receiver]
911
+ saved = [@block_params, @self_rebound, @in_block, @eval_frame, @eval_method]
685
912
  names = block.parameters ? LocalValues.bindings(block.parameters) : []
686
913
  @block_params = (@block_params | names).freeze unless names.empty?
687
- if @rebinding_blocks.include?(block)
688
- @self_rebound = true
689
- @eval_receiver = @eval_receivers[block]
690
- end
914
+ enter_block_self(block)
691
915
  @in_block = true
692
916
  yield
693
917
  ensure
694
- @block_params, @self_rebound, @in_block, @eval_receiver = saved
918
+ @block_params, @self_rebound, @in_block, @eval_frame, @eval_method = saved
919
+ end
920
+
921
+ # A block whose `self` is another object drops the eval reading around it: an eval block sets its own
922
+ # (#1878), and a `Class.new` / `Struct.new` block's `self` is the class it builds. A `define_method` /
923
+ # `define_singleton_method` block directly in an eval block is a method of the eval receiver (#1946).
924
+ def enter_block_self(block)
925
+ if @rebinding_blocks.include?(block)
926
+ @self_rebound = true
927
+ @eval_frame = @eval_frames[block]
928
+ @eval_method = nil
929
+ elsif @meta_new_blocks.include?(block)
930
+ @eval_frame = @eval_method = nil
931
+ elsif @eval_frame && @definer_blocks.key?(block)
932
+ @eval_method = EvalMethod.new(@eval_frame.receiver, @definer_blocks.fetch(block))
933
+ @eval_frame = nil
934
+ end
695
935
  end
696
936
 
697
937
  def site(node)
@@ -99,7 +99,12 @@ module Rigor
99
99
  "Rigor's bundled overlays name (`OpenStruct`, whose fields are per-instance singleton methods; " \
100
100
  "`ActiveSupport::Duration` and `ActiveSupport::Concern` when their gem overlay loads). Both sides " \
101
101
  "of the class are open, and an inherited Object / Kernel signature no longer decides " \
102
- "`call.wrong-arity` or `call.argument-type-mismatch` on it."
102
+ "`call.wrong-arity` or `call.argument-type-mismatch` on it.",
103
+ "The program may answer the call another way (#1722): a `method_missing` on the receiver's class or an " \
104
+ "ancestor; a module mixed into either that defines the method (`Integer.include(Ext)` counts wherever " \
105
+ "it is written); singleton methods given to the receiver local (`s.extend(M)`, " \
106
+ "`define_singleton_method`, `class << s`); or, for a receiver typed by its class's name rather than a " \
107
+ "literal, a project subclass that defines the method."
103
108
  ],
104
109
  suppression: "`# rigor:disable call.undefined-method` on the call line, " \
105
110
  "or `disable: [\"call.undefined-method\"]` in `.rigor.yml`.",
@@ -561,14 +566,22 @@ module Rigor
561
566
  summary: "Explicit-receiver call to a method declared `private` in source.",
562
567
  fires_when: [
563
568
  "Call is `receiver.method(...)` with explicit non-self receiver.",
564
- "Receiver type resolves to `Type::Nominal[X]`.",
565
- "X is a user-defined class whose source carries the method under `private`."
569
+ "Receiver type resolves to `Type::Nominal[X]`, or to the class object `singleton(X)`.",
570
+ "X is a user-defined class whose source carries the method under `private`; for a class object, " \
571
+ "the singleton method the call reaches is private (`private_class_method`, `class << self` with " \
572
+ "`private`, or a private method of a module X extends)."
566
573
  ],
567
574
  does_not_fire_when: [
568
575
  "Implicit-self call (no receiver) — always allowed for private.",
569
576
  "Receiver is `self` (Ruby 2.7+ permits `self.private_method`).",
570
577
  "Receiver class is RBS-known but not user-source-defined (RBS-side visibility is deferred).",
571
- "Method is `:protected` (subclass tracking is deferred)."
578
+ "Method is `:protected` (subclass tracking is deferred).",
579
+ "A `method_missing` or `respond_to_missing?` on the receiver's chain, on the side the call reaches, " \
580
+ "may answer the call (Ruby sends a private call to `method_missing`).",
581
+ "The visibility rests on a statement that may not run, or on files that disagree about it.",
582
+ "On a class object: the receiver is not a constant (`self.class.x` may be a subclass that " \
583
+ "re-publicises the name), or the call is not inside a `def` (load-time code may run before the " \
584
+ "method is made private)."
572
585
  ],
573
586
  suppression: "`# rigor:disable method-visibility-mismatch`.",
574
587
  severity_authored: :error,