rigortype 0.4.3 → 0.4.4

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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/data/core_overlay/ostruct.rbs +40 -0
  4. data/docs/handbook/11-sig-gen.md +16 -0
  5. data/docs/manual/02-cli-reference.md +4 -3
  6. data/docs/manual/03-configuration.md +16 -5
  7. data/docs/manual/04-diagnostics.md +44 -2
  8. data/docs/manual/07-plugins.md +1 -1
  9. data/docs/manual/10-mcp-server.md +32 -3
  10. data/docs/manual/18-removing-dead-code.md +11 -1
  11. data/docs/manual/plugins/rigor-rails-routes.md +2 -2
  12. data/docs/manual/plugins/rigor-shoulda-matchers.md +4 -1
  13. data/exe/rigor +12 -17
  14. data/lib/rigor/analysis/check_rules/rule_ids.rb +57 -0
  15. data/lib/rigor/analysis/check_rules.rb +46 -36
  16. data/lib/rigor/analysis/incremental.rb +12 -0
  17. data/lib/rigor/analysis/incremental_session.rb +121 -21
  18. data/lib/rigor/analysis/reachability/graph.rb +23 -3
  19. data/lib/rigor/analysis/reachability/scan.rb +51 -9
  20. data/lib/rigor/analysis/rule_catalog.rb +6 -1
  21. data/lib/rigor/analysis/run_cache_key.rb +31 -7
  22. data/lib/rigor/analysis/run_cache_probe.rb +5 -3
  23. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +48 -12
  24. data/lib/rigor/analysis/runner/pool_coordinator.rb +110 -15
  25. data/lib/rigor/analysis/runner/project_pre_passes.rb +29 -4
  26. data/lib/rigor/analysis/runner.rb +35 -9
  27. data/lib/rigor/analysis/severity_stamp.rb +34 -1
  28. data/lib/rigor/analysis/worker_session.rb +57 -1
  29. data/lib/rigor/cache/descriptor.rb +40 -6
  30. data/lib/rigor/cache/incremental_snapshot.rb +20 -4
  31. data/lib/rigor/cli/mcp_command.rb +1 -1
  32. data/lib/rigor/config_audit.rb +111 -15
  33. data/lib/rigor/configuration/dependencies.rb +19 -11
  34. data/lib/rigor/environment/rbs_loader.rb +46 -10
  35. data/lib/rigor/inference/block_call_timing.rb +5 -0
  36. data/lib/rigor/inference/captured_locals.rb +26 -3
  37. data/lib/rigor/inference/definer_resolution.rb +16 -7
  38. data/lib/rigor/inference/expression_typer.rb +59 -4
  39. data/lib/rigor/inference/guard_rebinding.rb +7 -2
  40. data/lib/rigor/inference/last_line/self_evidence.rb +1 -1
  41. data/lib/rigor/inference/module_function_state.rb +80 -13
  42. data/lib/rigor/inference/narrowing.rb +184 -68
  43. data/lib/rigor/inference/reflective_ivar_write.rb +116 -0
  44. data/lib/rigor/inference/repeated_or_writes.rb +414 -1
  45. data/lib/rigor/inference/scope_indexer.rb +439 -70
  46. data/lib/rigor/inference/self_call_ivar_writes.rb +341 -0
  47. data/lib/rigor/inference/statement_evaluator.rb +84 -29
  48. data/lib/rigor/lens/anchor.rb +3 -3
  49. data/lib/rigor/lens/contract_reader.rb +15 -0
  50. data/lib/rigor/lens/document.rb +2 -2
  51. data/lib/rigor/lens/file_reader.rb +442 -73
  52. data/lib/rigor/lens/members.rb +65 -15
  53. data/lib/rigor/lens/run.rb +44 -2
  54. data/lib/rigor/lens.rb +2 -1
  55. data/lib/rigor/mcp/server.rb +61 -20
  56. data/lib/rigor/plugin/box_probe.rb +154 -7
  57. data/lib/rigor/project_environment.rb +6 -3
  58. data/lib/rigor/scope.rb +11 -6
  59. data/lib/rigor/sig_gen/layout_index.rb +36 -0
  60. data/lib/rigor/sig_gen/method_candidate.rb +8 -0
  61. data/lib/rigor/sig_gen/path_mapper.rb +18 -2
  62. data/lib/rigor/sig_gen/writer.rb +47 -34
  63. data/lib/rigor/version.rb +1 -1
  64. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +5 -1
  65. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
  66. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +28 -6
  67. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +1 -1
  68. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +5 -1
  69. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +155 -12
  70. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +10 -8
  71. metadata +4 -1
@@ -507,6 +507,7 @@ module Rigor
507
507
  method_assign_effects = build_method_assign_effects(root)
508
508
  walk_class_ivars(root, [], default_scope, accumulator, mutated_ivars,
509
509
  read_before_write, init_writes, method_assign_effects)
510
+ exempt_class_body_writes!(init_writes, accumulator.delete(DEFINE_METHOD_WRITES) || {})
510
511
  merge_held_ivar_writes!(accumulator)
511
512
  record_aliased_ivar_mutations!(root, mutated_ivars)
512
513
  widen_mutated_ivar_entries!(accumulator, mutated_ivars)
@@ -514,6 +515,19 @@ module Rigor
514
515
  accumulator.transform_values(&:freeze).freeze
515
516
  end
516
517
 
518
+ # Folds the class-body writes {#collect_class_body_ivar_writes} held under {CLASS_BODY_WRITES} into the
519
+ # `init_writes` exemption, except for an ivar a `define_method` block of the class writes (#1768). Such an ivar
520
+ # had no census entry before the block's writes were counted, so the exemption never applied to it; a
521
+ # class-body `@x = nil` writes the class object's ivar and says nothing about whether an instance's `@x` is set,
522
+ # and exempting it would seed `@x` non-nil from the block's write alone (`if @x` folding always-truthy on an
523
+ # instance whose `@x` is unset). It keeps the read-before-write `nil` instead, which only widens.
524
+ def exempt_class_body_writes!(init_writes, define_method_writes)
525
+ (init_writes.delete(CLASS_BODY_WRITES) || {}).each do |class_name, ivars|
526
+ kept = ivars - (define_method_writes[class_name] || EMPTY_GUARDED_IVARS)
527
+ (init_writes[class_name] ||= Set.new).merge(kept)
528
+ end
529
+ end
530
+
517
531
  # B2.3 — finalize the read-before-write nil contribution. For each class, for each ivar where SOME method body
518
532
  # observed a read-before-write AND no `initialize` write exists for that ivar, contribute `Constant[nil]` to the
519
533
  # class-wide accumulator.
@@ -854,6 +868,7 @@ module Rigor
854
868
  owner = def_owner || qualified_prefix
855
869
  collect_initializer_block_ivars(node, owner, default_scope, accumulator, mutated_ivars, init_writes)
856
870
  record_attr_writer_ivars(node, owner, accumulator) if ATTR_WRITER_MACROS.include?(node.name)
871
+ collect_define_method_block_ivars(node, owner, default_scope, accumulator, mutated_ivars)
857
872
  false
858
873
  else
859
874
  walk_ivars_meta_new?(node, qualified_prefix, default_scope, accumulator,
@@ -1065,6 +1080,44 @@ module Rigor
1065
1080
  end
1066
1081
  end
1067
1082
 
1083
+ # The `define_method` arm of {#walk_class_ivars} (#1768). `define_method(:m) { … }` on the implicit
1084
+ # or `self` receiver installs its block as an instance method of `owner`, and Ruby runs the body on
1085
+ # the instance, so its `@x = …` writes are the instance's ivars exactly as a `def m` body's are —
1086
+ # `DefineMethodBlockSelf` narrows the block's `self` the same way when it is evaluated. Without this
1087
+ # arm an ivar `initialize` sets to `nil` and a `define_method` block sets to a String read as `nil`
1088
+ # in every sibling `def`. The block is typed under the instance `self`, as a def body is. Only the
1089
+ # literal-block form counts: a `define_method(:m, callable)` or `&callable` argument is not walked.
1090
+ # `owner` is empty in a `class << self` body (the call defines a class method there) and in an
1091
+ # anonymous factory body, and the block contributes nothing there. A `define_method` inside a `def`
1092
+ # body is already reached by that body's walk ({#gather_ivar_writes} enters blocks).
1093
+ def collect_define_method_block_ivars(node, owner, default_scope, accumulator, mutated_ivars)
1094
+ return if owner.empty?
1095
+ return unless define_method_block_call?(node)
1096
+
1097
+ block = node.block
1098
+ return unless block.body
1099
+
1100
+ class_name = owner.join("::")
1101
+ body_scope = census_body_scope(default_scope, owner, Type::Combinator.nominal_of(class_name))
1102
+ gather_ivar_writes(block.body, body_scope, class_name, accumulator, EMPTY_GUARDED_IVARS, mutated_ivars)
1103
+ written = (accumulator[DEFINE_METHOD_WRITES] ||= {})[class_name] ||= Set.new
1104
+ define_method_written_ivars(block.body, written)
1105
+ end
1106
+
1107
+ # The ivars a `define_method` block body writes, in any write form, outside a nested `def` / `class` / `module`.
1108
+ def define_method_written_ivars(node, acc)
1109
+ return unless node.is_a?(Prism::Node)
1110
+ return if IVAR_BARRIER_NODES.any? { |klass| node.is_a?(klass) }
1111
+
1112
+ case node
1113
+ when Prism::InstanceVariableWriteNode, Prism::InstanceVariableOrWriteNode,
1114
+ Prism::InstanceVariableAndWriteNode, Prism::InstanceVariableOperatorWriteNode,
1115
+ Prism::InstanceVariableTargetNode
1116
+ acc << node.name
1117
+ end
1118
+ node.rigor_each_child { |child| define_method_written_ivars(child, acc) }
1119
+ end
1120
+
1068
1121
  # ADR-38 block-form gate: true when a loaded plugin declares `method_name` a block-form initializer for
1069
1122
  # `class_name` (or an ancestor). Mirrors `additional_initializer?` but queries `covers_block_method?` instead of
1070
1123
  # `covers_method?`.
@@ -1156,15 +1209,21 @@ module Rigor
1156
1209
  # every `@x = …` write target as a class-body init. Consumed by `contribute_read_before_write_nil!` to exempt
1157
1210
  # ivars the author already knows might be nil (the `@x = nil` at class-body level is the canonical nullability
1158
1211
  # acknowledgement; the instance @x is technically a separate store, but the pragmatic intent is unambiguous).
1212
+ #
1213
+ # A literal `define_method` block is a method body, not class-body code (#1768): its writes reach the census as
1214
+ # a `def` body's do, so they must not exempt the ivar from the read-before-write `nil` either. Only its receiver
1215
+ # and arguments are walked.
1159
1216
  def collect_class_body_ivar_writes(node, class_name, init_writes)
1160
1217
  return unless node.is_a?(Prism::Node)
1161
1218
  return if IVAR_BARRIER_NODES.any? { |klass| node.is_a?(klass) }
1162
1219
 
1220
+ return collect_define_method_argument_writes(node, class_name, init_writes) if define_method_block_call?(node)
1221
+
1163
1222
  if node.is_a?(Prism::InstanceVariableWriteNode) ||
1164
1223
  node.is_a?(Prism::InstanceVariableOrWriteNode) ||
1165
1224
  node.is_a?(Prism::InstanceVariableAndWriteNode) ||
1166
1225
  node.is_a?(Prism::InstanceVariableOperatorWriteNode)
1167
- (init_writes[class_name] ||= Set.new) << node.name
1226
+ ((init_writes[CLASS_BODY_WRITES] ||= {})[class_name] ||= Set.new) << node.name
1168
1227
  end
1169
1228
 
1170
1229
  node.rigor_each_child do |child|
@@ -1172,6 +1231,18 @@ module Rigor
1172
1231
  end
1173
1232
  end
1174
1233
 
1234
+ # The arguments of a `define_method` block call, walked by {#collect_class_body_ivar_writes} in place of the
1235
+ # call's children so the block body stays out of the class-body init writes.
1236
+ def collect_define_method_argument_writes(node, class_name, init_writes)
1237
+ node.arguments&.arguments&.each { |arg| collect_class_body_ivar_writes(arg, class_name, init_writes) }
1238
+ end
1239
+
1240
+ # A `define_method` call with a literal block on the implicit or `self` receiver: the block is an instance
1241
+ # method body of the class the walk is in (#1768).
1242
+ def define_method_block_call?(node)
1243
+ DefineMethodBlockSelf.define_method_on_lexical_self?(node) && node.block.is_a?(Prism::BlockNode)
1244
+ end
1245
+
1175
1246
  def detect_read_before_write(node, seen_writes, read_first)
1176
1247
  return unless node.is_a?(Prism::Node)
1177
1248
  return if IVAR_BARRIER_NODES.any? { |klass| node.is_a?(klass) }
@@ -2011,6 +2082,13 @@ module Rigor
2011
2082
  OPERATOR_WRITE_CONTRIBUTIONS = :operator_write_contributions
2012
2083
  private_constant :AND_WRITE_CONTRIBUTIONS, :OPERATOR_WRITE_CONTRIBUTIONS
2013
2084
 
2085
+ # The same Symbol-key convention for two side tables the walk fills and {#build_class_ivar_index} removes
2086
+ # before anything reads class names: the ivars each class's `define_method` blocks write (in the
2087
+ # accumulator), and the class-body writes held out of `init_writes` until {#exempt_class_body_writes!}.
2088
+ DEFINE_METHOD_WRITES = :define_method_writes
2089
+ CLASS_BODY_WRITES = :class_body_writes
2090
+ private_constant :DEFINE_METHOD_WRITES, :CLASS_BODY_WRITES
2091
+
2014
2092
  # Unions `type` into the class-ivar accumulator for `(class_name, ivar_name)`. Shared by the single-write and
2015
2093
  # multi-write (parallel-assignment) collectors.
2016
2094
  def accumulate_ivar_type(accumulator, class_name, ivar_name, type)
@@ -7004,7 +7082,9 @@ module Rigor
7004
7082
  def extends_block_handled?(node, qualified_prefix, current_class, accumulator, in_singleton, singleton_self,
7005
7083
  singleton_cref)
7006
7084
  case extends_block_plan(node, accumulator)
7007
- when :skip then true
7085
+ when :skip
7086
+ taint_skipped_singleton_rewrites(node.block, qualified_prefix, current_class, accumulator)
7087
+ true
7008
7088
  when :rebound
7009
7089
  walk_extends_block_call(node, qualified_prefix, current_class, accumulator,
7010
7090
  in_singleton, singleton_self, singleton_cref)
@@ -7013,6 +7093,31 @@ module Rigor
7013
7093
  end
7014
7094
  end
7015
7095
 
7096
+ # Issue #1906 — a block the extends walk skips may still run, and a `singleton_class` call in it, or in a `def`
7097
+ # it holds (`[1].each { D.singleton_class.alias_method :own, :other }` in a method body,
7098
+ # `to_prepare do def helper = D.singleton_class.alias_method(...) end`), reaches no mark the methods walk records
7099
+ # there. Every such call taints the side it may rewrite, as {#singleton_class_rewrite_tainted?} does for one the
7100
+ # walk visits; a constant receiver taints every name it can denote from here.
7101
+ def taint_skipped_singleton_rewrites(node, qualified_prefix, current_class, accumulator)
7102
+ return unless node.is_a?(Prism::Node)
7103
+
7104
+ if node.is_a?(Prism::CallNode) && singleton_class_of_receiver?(node.receiver) && surface_rewriting_call?(node)
7105
+ skipped_rewrite_owners(node.receiver.receiver, qualified_prefix, current_class).each do |owner|
7106
+ accumulator.taint(owner, :extend)
7107
+ end
7108
+ end
7109
+ node.rigor_each_child do |child|
7110
+ taint_skipped_singleton_rewrites(child, qualified_prefix, current_class, accumulator)
7111
+ end
7112
+ end
7113
+
7114
+ def skipped_rewrite_owners(object, qualified_prefix, current_class)
7115
+ return [current_class].compact if object.nil? || object.is_a?(Prism::SelfNode)
7116
+ return [] unless object.is_a?(Prism::ConstantReadNode) || object.is_a?(Prism::ConstantPathNode)
7117
+
7118
+ constant_receiver_candidates(object, qualified_prefix)
7119
+ end
7120
+
7016
7121
  ARRAY_ITERATORS = %i[each each_with_index map].freeze
7017
7122
  HASH_ITERATORS = %i[each each_pair each_key each_value map].freeze
7018
7123
  private_constant :ARRAY_ITERATORS, :HASH_ITERATORS
@@ -7228,10 +7333,15 @@ module Rigor
7228
7333
  # `Const.singleton_class.send(:include, M)`) puts methods or modules on that singleton class that no edge
7229
7334
  # records, so its singleton side is unpositioned: the owner is the enclosing class for a bare or `self.`
7230
7335
  # receiver, else the constant. True when it tainted.
7336
+ #
7337
+ # Issue #1906 — a naming call with literal names ({#singleton_class_named_call?}) marks only its names instead,
7338
+ # but only where it is a direct statement (a top-level statement, or one of a direct declaration body or of a
7339
+ # block that runs there once): there the methods walk records those marks under the same owner. In a method
7340
+ # body, a hook or a conditional it may run on a receiver or at a time the marks cannot place, so it still taints.
7231
7341
  def singleton_class_rewrite_tainted?(node, qualified_prefix, current_class, accumulator)
7232
7342
  receiver = node.receiver
7233
7343
  return false unless singleton_class_of_receiver?(receiver) && surface_rewriting_call?(node)
7234
- return false if singleton_class_named_call?(node)
7344
+ return false if singleton_class_named_call?(node) && accumulator.direct?(node)
7235
7345
 
7236
7346
  owner = singleton_class_owner(node, receiver.receiver, qualified_prefix, current_class)
7237
7347
  return false if owner.nil?
@@ -7245,15 +7355,37 @@ module Rigor
7245
7355
  receiver.is_a?(Prism::CallNode) && receiver.name == :singleton_class && receiver.arguments.nil?
7246
7356
  end
7247
7357
 
7248
- # An `attr_*` on a singleton class (`singleton_class.attr_accessor :conf`) rewrites only the names it spells, so
7249
- # it makes those names' singleton envelopes opaque ({#record_singleton_class_names}) and leaves the rest of the
7250
- # singleton side readable. An `attr_*` with a name it does not spell as a literal (`attr_accessor(*KEYS)`, a
7251
- # block's variable), and an `alias_method` or `define_method` there, still taint the side: the own-definer read
7252
- # would otherwise answer the body Ruby replaced (`definer_resolution_witness_spec.rb`).
7358
+ # An `attr_*`, `alias_method`, `define_method` or `remove_method` on a singleton class
7359
+ # (`singleton_class.attr_accessor :conf`, `singleton_class.alias_method :o2, :own`) rewrites only the names it
7360
+ # spells, so it makes those names' singleton envelopes opaque and contested ({#record_singleton_class_names}) and
7361
+ # leaves the rest of the singleton side readable. One whose rewritten name is not a literal Symbol or String
7362
+ # (`attr_accessor(*KEYS)`, `alias_method name, :own`, `define_method(sym)`), and any other surface call there
7363
+ # (`class_eval`, `send`, `prepend`), still taints the side: the own-definer read would otherwise answer the body
7364
+ # Ruby replaced (`definer_resolution_witness_spec.rb`).
7253
7365
  def singleton_class_named_call?(node)
7366
+ !singleton_class_rewritten_names(node).nil?
7367
+ end
7368
+
7369
+ # Issue #1906 — the names a singleton-class call rewrites, or nil where it is not one of the naming forms or
7370
+ # does not spell every rewritten name as a literal. `alias_method` rewrites its new name (the aliased one keeps
7371
+ # its body), `define_method` its first argument, `remove_method` and `attr_*` every argument.
7372
+ def singleton_class_rewritten_names(node)
7254
7373
  arguments = node.arguments&.arguments || []
7255
- ATTR_MACROS.include?(node.name) && !arguments.empty? &&
7256
- arguments.all? { |argument| literal_method_name(argument) }
7374
+ rewritten = case node.name
7375
+ when :alias_method, :define_method then arguments.first(1)
7376
+ when :remove_method then arguments
7377
+ else ATTR_MACROS.include?(node.name) && arguments
7378
+ end
7379
+ return nil if !rewritten || arguments.empty? || !arguments.all? { |argument| literal_argument?(node, argument) }
7380
+
7381
+ rewritten.map { |argument| literal_method_name(argument) }
7382
+ end
7383
+
7384
+ # `define_method(:x, body)` takes a body that is not a name; every other argument must be a literal name.
7385
+ def literal_argument?(node, argument)
7386
+ return true if node.name == :define_method && !argument.equal?(node.arguments.arguments.first)
7387
+
7388
+ !literal_method_name(argument).nil?
7257
7389
  end
7258
7390
 
7259
7391
  def singleton_class_owner(node, object, qualified_prefix, current_class)
@@ -7447,19 +7579,15 @@ module Rigor
7447
7579
 
7448
7580
  # v0.1.2 — per-class method-visibility table for the `def.method-visibility-mismatch` CheckRule.
7449
7581
  #
7450
- # Tracks two visibility-changing forms:
7582
+ # Tracks three visibility-changing forms ({#apply_visibility_call}):
7451
7583
  #
7452
- # - **Modifier blocks**: a bare `private` / `protected` /
7453
- # `public` call inside a class body switches the
7454
- # "current default" visibility for every subsequent
7455
- # `def` until another modifier flips it again.
7456
- # - **Named-argument form**: `private :foo, :bar` (or
7457
- # the same with `protected` / `public`) marks specific
7458
- # names already-recorded under the class. Symbol-only
7459
- # args are recognised; `private def foo; end` (the
7460
- # wrap-around form) is not yet — it would need
7461
- # tracking the def-call's return-value visibility,
7462
- # which is a separate slice.
7584
+ # - **Modifier blocks**: a bare `private` / `protected` / `public` call inside a class body (also written
7585
+ # `self.private`, `send(:private)` or with a block) switches the "current default" visibility for every
7586
+ # subsequent `def` and `attr_*` member until another modifier flips it again.
7587
+ # - **Named-argument form**: `private :foo, :bar` or `private %i[foo bar]` (or the same with `protected` /
7588
+ # `public`) marks specific names already recorded under the class.
7589
+ # - **Wrap-around form** (#1911, #1819, #1633): `private def foo` and `private attr_reader :foo` record the
7590
+ # members with the call's visibility and leave the default alone.
7463
7591
  #
7464
7592
  # Top-level (no surrounding class) defs do not contribute — Ruby's top-level visibility nuances (private at
7465
7593
  # top-level marks the method on `Object`) are out of scope for v0.1.2.
@@ -7516,6 +7644,26 @@ module Rigor
7516
7644
  end
7517
7645
  end
7518
7646
 
7647
+ # Contests the `[class_name, name]` slot, recorded or not: a visibility call changed it in a way the walk
7648
+ # cannot read (`private def self.x` reaching an instance `x`).
7649
+ def contest_slot(class_name, name)
7650
+ (@contested ||= Set.new) << [class_name, name].freeze
7651
+ end
7652
+
7653
+ # Contests every slot of `class_name` recorded so far (`private(*names)` may name any of them).
7654
+ def contest_class(class_name)
7655
+ self[class_name]&.each_key { |name| contest_slot(class_name, name) }
7656
+ end
7657
+
7658
+ # Runs the block with the running default uncertain, so a `def` it records is contested.
7659
+ def uncertainly
7660
+ saved = @uncertain
7661
+ @uncertain = true
7662
+ yield
7663
+ ensure
7664
+ @uncertain = saved
7665
+ end
7666
+
7519
7667
  # Runs the block as a fresh body (a class or module, `class << self`, a meta-new or eval block), whose
7520
7668
  # default starts certain and whose toggles do not reach the enclosing body.
7521
7669
  def body
@@ -7537,10 +7685,37 @@ module Rigor
7537
7685
  end
7538
7686
  end
7539
7687
 
7540
- # A bare toggle ({VISIBILITY_TOGGLES}): receiverless, with no arguments.
7688
+ # A bare toggle ({VISIBILITY_TOGGLES}): a visibility call ({#visibility_call_parts}) with no arguments.
7541
7689
  def bare_visibility_toggle?(node)
7542
- node.is_a?(Prism::CallNode) && node.receiver.nil? && VISIBILITY_TOGGLES.include?(node.name) &&
7543
- (node.arguments.nil? || node.arguments.arguments.empty?) && node.block.nil?
7690
+ parts = visibility_call_parts(node)
7691
+ !parts.nil? && parts.last.empty?
7692
+ end
7693
+
7694
+ # Whether a visibility call with these arguments may set the running default: none at all, or a splat that
7695
+ # may expand to none (`private(*[])` is a bare `private`).
7696
+ def visibility_toggle_arguments?(arguments)
7697
+ arguments.empty? || arguments.any?(Prism::SplatNode)
7698
+ end
7699
+
7700
+ VISIBILITY_SEND_CALLS = %i[send __send__].freeze
7701
+ EMPTY_VISIBILITY_ARGUMENTS = [].freeze
7702
+ private_constant :VISIBILITY_SEND_CALLS, :EMPTY_VISIBILITY_ARGUMENTS
7703
+
7704
+ # Issue #1633 — `[name, arguments]` for a call Ruby runs as one of {VISIBILITY_TOGGLES} on the body's self:
7705
+ # written receiverless or on `self` (`self.private`), or sent with a literal name (`send(:private, :x)`,
7706
+ # `__send__(:private)`). A block is ignored, as `Module#private` ignores it (`private {}` is a bare
7707
+ # `private`). nil for any other call.
7708
+ def visibility_call_parts(node)
7709
+ return nil unless node.is_a?(Prism::CallNode)
7710
+ return nil unless node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
7711
+
7712
+ name = node.name
7713
+ arguments = node.arguments&.arguments || EMPTY_VISIBILITY_ARGUMENTS
7714
+ if VISIBILITY_SEND_CALLS.include?(name)
7715
+ name = Source::Literals.symbol_or_string(arguments.first)
7716
+ arguments = arguments.drop(1)
7717
+ end
7718
+ VISIBILITY_TOGGLES.include?(name) ? [name, arguments] : nil
7544
7719
  end
7545
7720
 
7546
7721
  # rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/AbcSize
@@ -7604,16 +7779,31 @@ module Rigor
7604
7779
  defs_singleton: defs_singleton)
7605
7780
  return current_visibility
7606
7781
  end
7607
- accumulator.toggles += 1 if bare_visibility_toggle?(node)
7608
- # The visibility table is instance-side only — `private :x` inside `class <<`
7609
- # (or a receiver-eval body that re-evaluates a singleton self) marks the
7610
- # SINGLETON method, which this table cannot express.
7611
- updated = if in_singleton_class
7612
- current_visibility
7613
- else
7614
- apply_visibility_call(node, owner_prefix, current_visibility, accumulator)
7615
- end
7616
- return updated unless updated.equal?(current_visibility)
7782
+ if AnonymousMetaClass.block_form_receiver(node)
7783
+ walk_anonymous_meta_visibilities(node, qualified_prefix, in_singleton_class, current_visibility,
7784
+ accumulator, def_owner_prefix, singleton_cref: singleton_cref,
7785
+ defs_singleton: defs_singleton)
7786
+ return current_visibility
7787
+ end
7788
+ if (parts = visibility_call_parts(node))
7789
+ accumulator.toggles += 1 if visibility_toggle_arguments?(parts.last)
7790
+ # The visibility table is instance-side only — `private :x` inside `class <<`
7791
+ # (or a receiver-eval body that re-evaluates a singleton self) marks the
7792
+ # SINGLETON method, which this table cannot express. The `owner_prefix` check is a
7793
+ # guard: an ownerless body (the top level, an anonymous factory block) records nothing,
7794
+ # and its calls walk as any other call.
7795
+ if !in_singleton_class && !owner_prefix.empty? && VISIBILITY_MODIFIERS.include?(parts.first)
7796
+ return apply_visibility_call(node, *parts, owner_prefix, current_visibility, accumulator,
7797
+ defs_singleton: defs_singleton) do |argument|
7798
+ walk_method_visibilities(argument, qualified_prefix, in_singleton_class, current_visibility,
7799
+ accumulator, def_owner_prefix, singleton_cref: singleton_cref,
7800
+ defs_singleton: defs_singleton)
7801
+ end
7802
+ end
7803
+ end
7804
+ unless in_singleton_class || defs_singleton
7805
+ record_attr_visibility(node, owner_prefix, current_visibility, accumulator, toggled: true)
7806
+ end
7617
7807
  end
7618
7808
 
7619
7809
  # Statement-position StatementsNode preserves left-to-right visibility flow; everything else recurses with the
@@ -7667,6 +7857,23 @@ module Rigor
7667
7857
  end
7668
7858
  end
7669
7859
 
7860
+ # A `Class.new` / `Module.new` / `Struct.new` / `Data.define` block no constant write names
7861
+ # ({#meta_new_block_split} takes the named ones): `klass = Class.new do … end`, `@k = Struct.new(:a) do … end`,
7862
+ # `register(Class.new do … end)` or a bare `Data.define(:a) do … end`. Its body is the anonymous class's, so
7863
+ # it walks as a fresh body with no owner and records nothing for the enclosing class; the receiver and the
7864
+ # arguments are walked where they stand.
7865
+ def walk_anonymous_meta_visibilities(node, qualified_prefix, in_singleton_class, current_visibility,
7866
+ accumulator, def_owner_prefix, singleton_cref:, defs_singleton:) # rubocop:disable Metrics/ParameterLists
7867
+ [node.receiver, *node.arguments&.arguments].each do |part|
7868
+ walk_method_visibilities(part, qualified_prefix, in_singleton_class, current_visibility, accumulator,
7869
+ def_owner_prefix, singleton_cref: singleton_cref, defs_singleton: defs_singleton)
7870
+ end
7871
+ accumulator.body do
7872
+ walk_method_visibilities(node.block.body, qualified_prefix, false, :public, accumulator,
7873
+ EMPTY_PREFIX, singleton_cref: singleton_cref)
7874
+ end
7875
+ end
7876
+
7670
7877
  # The eval-block arm of {#walk_method_visibilities}: the block is a fresh class body under
7671
7878
  # the receiver's def-owner — visibility starts at `:public`, and nothing inside it leaks
7672
7879
  # back to this body's modifier state. An unnameable receiver walks the body ownerless;
@@ -7704,47 +7911,80 @@ module Rigor
7704
7911
  end
7705
7912
  end
7706
7913
 
7707
- def record_def_visibility(def_node, qualified_prefix, in_singleton_class, current_visibility, accumulator)
7914
+ # `toggled` when the def reads the running default (a statement), false for one a visibility call wraps.
7915
+ def record_def_visibility(def_node, qualified_prefix, in_singleton_class, current_visibility, accumulator,
7916
+ toggled: true)
7708
7917
  return if def_node.receiver.is_a?(Prism::SelfNode) || in_singleton_class
7709
7918
  return if qualified_prefix.empty?
7710
7919
 
7711
7920
  class_name = qualified_prefix.join("::")
7712
7921
  accumulator[class_name] ||= {}
7713
7922
  accumulator[class_name][def_node.name] = current_visibility
7714
- accumulator.wrote(class_name, def_node.name, def_node, toggled: true)
7715
- end
7923
+ accumulator.wrote(class_name, def_node.name, def_node, toggled: toggled)
7924
+ end
7925
+
7926
+ # Applies one `private` / `protected` / `public` call ({#visibility_call_parts}) written on the body's self
7927
+ # and returns the running default after it:
7928
+ #
7929
+ # - no arguments — the call's visibility becomes the default for subsequent defs.
7930
+ # - `private :foo, :bar`, `private %i[foo bar]` — back-patch the named methods in the accumulator; the
7931
+ # default does NOT change for this form.
7932
+ # - Issues #1911, #1819, #1633 — `private def foo` (endless or not) and `private attr_reader :foo` define
7933
+ # the members with the call's visibility, whatever the running default is (`private; public def foo` is
7934
+ # public); `private def self.foo` changes the instance method `foo`, if one exists, so its slot is
7935
+ # contested.
7936
+ # - a splat (`private(*names)`) may name any member or none (a bare toggle), so the class's slots this file
7937
+ # wrote so far are contested, and the statement loop reads the default after it as uncertain
7938
+ # ({#visibility_toggle_arguments?}).
7939
+ # - any other argument (`private memoize def foo`, `private delegate(...)`) is walked with the default
7940
+ # uncertain, so a `def` under it is contested: the call's visibility reaches it through a return value
7941
+ # the walk does not read.
7942
+ #
7943
+ # `walk` walks an argument the call does not consume itself.
7944
+ def apply_visibility_call(call_node, name, args, qualified_prefix, current_visibility, accumulator,
7945
+ defs_singleton: false, &walk)
7946
+ return name if args.empty?
7716
7947
 
7717
- # Recognises modifier calls on the implicit-self receiver inside a class body. Returns the (possibly updated)
7718
- # current visibility:
7719
- #
7720
- # - `private` / `public` / `protected` (no args) —
7721
- # switch the running default for subsequent defs.
7722
- # - `private :foo, :bar` — back-patch the named methods
7723
- # in the accumulator. Returns `current_visibility`
7724
- # unchanged because the running default does NOT
7725
- # change for this form.
7726
- def apply_visibility_call(call_node, qualified_prefix, current_visibility, accumulator)
7727
- return current_visibility unless call_node.receiver.nil?
7728
- return current_visibility unless VISIBILITY_MODIFIERS.include?(call_node.name)
7729
- return current_visibility if qualified_prefix.empty?
7948
+ class_name = qualified_prefix.join("::")
7949
+ accumulator.contest_class(class_name) if args.any?(Prism::SplatNode)
7950
+ args.each do |arg|
7951
+ apply_visibility_argument(arg, name, call_node, qualified_prefix, accumulator,
7952
+ defs_singleton: defs_singleton, &walk)
7953
+ end
7954
+ current_visibility
7955
+ end
7730
7956
 
7731
- args = call_node.arguments&.arguments || []
7732
- if args.empty?
7733
- call_node.name
7957
+ def apply_visibility_argument(arg, name, call_node, qualified_prefix, accumulator, defs_singleton:, &walk)
7958
+ case arg
7959
+ when Prism::DefNode
7960
+ if arg.receiver.nil?
7961
+ record_def_visibility(arg, qualified_prefix, defs_singleton, name, accumulator, toggled: false)
7962
+ else
7963
+ accumulator.contest_slot(qualified_prefix.join("::"), arg.name)
7964
+ end
7965
+ when Prism::SymbolNode, Prism::StringNode, Prism::ArrayNode
7966
+ apply_named_visibility(arg, name, qualified_prefix, call_node, accumulator, &walk)
7734
7967
  else
7735
- apply_named_visibility(args, qualified_prefix, call_node, accumulator)
7736
- current_visibility
7968
+ # An `instance_eval` body's wrapped attr is walked like any other argument and records nothing.
7969
+ if !defs_singleton && attr_visibility_call?(arg)
7970
+ record_attr_visibility(arg, qualified_prefix, name, accumulator, toggled: false)
7971
+ else
7972
+ accumulator.uncertainly { walk.call(arg) }
7973
+ end
7737
7974
  end
7738
7975
  end
7739
7976
 
7740
- def apply_named_visibility(args, qualified_prefix, call_node, accumulator)
7977
+ # `private :foo` / `private "foo"` / `private %i[foo bar]`: each literal name the call's visibility. An array
7978
+ # element that is not a literal name is walked like any other argument.
7979
+ def apply_named_visibility(arg, visibility, qualified_prefix, call_node, accumulator, &walk)
7980
+ elements = arg.is_a?(Prism::ArrayNode) ? arg.elements : [arg]
7741
7981
  class_name = qualified_prefix.join("::")
7742
- args.each do |arg|
7743
- name = visibility_target_name(arg)
7744
- next if name.nil?
7982
+ elements.each do |element|
7983
+ name = visibility_target_name(element)
7984
+ next accumulator.uncertainly { walk.call(element) } if name.nil?
7745
7985
 
7746
7986
  accumulator[class_name] ||= {}
7747
- accumulator[class_name][name] = call_node.name
7987
+ accumulator[class_name][name] = visibility
7748
7988
  accumulator.wrote(class_name, name, call_node, toggled: false)
7749
7989
  end
7750
7990
  end
@@ -7753,6 +7993,32 @@ module Rigor
7753
7993
  Source::Literals.symbol_or_string(arg)
7754
7994
  end
7755
7995
 
7996
+ # Issue #1911 — an `attr_reader` / `attr_writer` / `attr_accessor` call written on the body's self records
7997
+ # each member it defines (`x`, `x=`) with `visibility`: the running default for a statement (`toggled`), the
7998
+ # wrapping call's for `private attr_accessor :x`, which Ruby applies to the `[:x, :x=]` the macro returns.
7999
+ # A non-literal name (`attr_reader(*FIELDS)`) records nothing; a call that is no such macro records nothing.
8000
+ def record_attr_visibility(call_node, qualified_prefix, visibility, accumulator, toggled:)
8001
+ return unless attr_visibility_call?(call_node)
8002
+ return if qualified_prefix.empty?
8003
+
8004
+ class_name = qualified_prefix.join("::")
8005
+ each_attr_macro_name(call_node) do |base|
8006
+ members = []
8007
+ members << base unless call_node.name == :attr_writer
8008
+ members << :"#{base}=" unless call_node.name == :attr_reader
8009
+ members.each do |member|
8010
+ accumulator[class_name] ||= {}
8011
+ accumulator[class_name][member] = visibility
8012
+ accumulator.wrote(class_name, member, call_node, toggled: toggled)
8013
+ end
8014
+ end
8015
+ end
8016
+
8017
+ # A receiverless `attr_reader` / `attr_writer` / `attr_accessor` call ({#record_attr_visibility}).
8018
+ def attr_visibility_call?(node)
8019
+ node.is_a?(Prism::CallNode) && node.receiver.nil? && ATTR_MACROS.include?(node.name)
8020
+ end
8021
+
7756
8022
  # Registers the alias name in the `discovered_methods` table so `undefined-method` diagnostics are not emitted for
7757
8023
  # calls to the aliased name. The kind mirrors the surrounding class context (instance inside a regular class body,
7758
8024
  # singleton inside `class << self`).
@@ -7858,21 +8124,20 @@ module Rigor
7858
8124
  end
7859
8125
 
7860
8126
  # Issue #1752 — the names a {#singleton_class_named_call?} rewrites on its singleton class's owner, each
7861
- # singleton envelope opaque: the evidence the refinement typer reads as a rewritten singleton name.
8127
+ # singleton envelope opaque and contested: the evidence the refinement typer reads as a rewritten singleton name.
7862
8128
  def record_singleton_class_names(node, object, qualified_prefix, tables)
7863
8129
  current_class = qualified_prefix.empty? ? nil : qualified_prefix.join("::")
7864
8130
  owner = singleton_class_owner(node, object, qualified_prefix, current_class)
7865
8131
  return if owner.nil?
7866
8132
 
7867
- (node.arguments&.arguments || []).each do |argument|
7868
- method_name = literal_method_name(argument)
7869
- next unless method_name
7870
-
8133
+ singleton_class_rewritten_names(node).each do |method_name|
7871
8134
  names = [method_name]
7872
8135
  names << :"#{method_name}=" if %i[attr_writer attr_accessor].include?(node.name)
7873
8136
  names.each do |name|
7874
8137
  record_envelope(tables.envelopes, owner, [:singleton, name], Source::ParameterEnvelope::OPAQUE)
7875
- contest_envelope(tables, owner, [:singleton, name]) unless tables.certain
8138
+ # Contested even where the call is certain: the slot's `def` is not the body Ruby runs, so the
8139
+ # own-definer read declines on the name rather than answering it.
8140
+ contest_envelope(tables, owner, [:singleton, name])
7876
8141
  end
7877
8142
  end
7878
8143
  end
@@ -8351,12 +8616,14 @@ module Rigor
8351
8616
  # {#discovered_project_index_incremental}'s changed-file branch) yields the live def nodes the signature
8352
8617
  # reads their parameter structure from. Issue #1120 — and each file's own refinement table, which the merged
8353
8618
  # def-index cannot attribute back to a file, so the session can diff it against the file's seed bundle.
8354
- # @return `{ def_index:, code_fingerprints:, declaration_signatures:, refinements: }`.
8619
+ # Issue #1459 — and each file's {#defined_method_names}, diffed the same way.
8620
+ # @return `{ def_index:, code_fingerprints:, declaration_signatures:, refinements:, defined_method_names: }`.
8355
8621
  def scan_summary_for_paths(paths, buffer: nil)
8356
8622
  acc = new_def_index_accumulator
8357
8623
  code_fingerprints = {}
8358
8624
  declaration_signatures = {}
8359
8625
  refinements = {}
8626
+ defined_method_names = {}
8360
8627
  paths.each do |path|
8361
8628
  physical = buffer ? buffer.resolve(path) : path
8362
8629
  source = File.read(physical)
@@ -8366,13 +8633,29 @@ module Rigor
8366
8633
  code_fingerprints[path] = code_fingerprint(source, parsed.comments)
8367
8634
  declaration_signatures[path] = declaration_signature(file_index)
8368
8635
  refinements[path] = file_index[:refinements] if file_index[:refinements]
8636
+ defined_method_names[path] = defined_method_names(file_index)
8369
8637
  rescue DeclarationWalk::ContractError
8370
8638
  raise # a broken walk contract, not an unreadable file: skipping it would hide the failure
8371
8639
  rescue StandardError
8372
8640
  next
8373
8641
  end
8374
8642
  { def_index: finalize_def_index(acc), code_fingerprints: code_fingerprints,
8375
- declaration_signatures: declaration_signatures, refinements: refinements }
8643
+ declaration_signatures: declaration_signatures, refinements: refinements,
8644
+ defined_method_names: defined_method_names }
8645
+ end
8646
+
8647
+ # Issue #1459 — every method name one file's index defines, on any class or module and either side: the keys of
8648
+ # `methods` (which also carries an `attr_*`, `alias` or `define_method` name) and of the instance and singleton
8649
+ # def-node tables. A single file's `methods` already holds its `def` names, since `subtract_def_methods` runs only
8650
+ # on the merged accumulator; the def-node tables are read as well so the census reads exactly the tables
8651
+ # `BlockCallTiming.project_defines_anywhere?` reads. Takes a live file index or a seed bundle, which hold the
8652
+ # same three keys.
8653
+ def defined_method_names(file_index)
8654
+ names = Set.new
8655
+ %i[methods def_nodes singleton_def_nodes].each do |key|
8656
+ file_index[key]&.each_value { |table| names.merge(table.keys) }
8657
+ end
8658
+ names.freeze
8376
8659
  end
8377
8660
 
8378
8661
  # ADR-89 WD1 — a per-file digest of every cross-file DECLARATION surface an ancestry / file-level
@@ -10589,8 +10872,15 @@ module Rigor
10589
10872
 
10590
10873
  # Issue #1717 — a `def` / `class` / `module` body opens its own `self`, so an enclosing unentered block's
10591
10874
  # unknown-`self` mark ({Scope#block_self_unknown?}) stops at it, as the evaluator's fresh body scope does.
10592
- # Every other node hands its children the scope as is.
10875
+ # A `case` narrows its clauses ({#propagate_subjectless_case}, {#propagate_subject_case}). Every other node
10876
+ # hands its children the scope as is.
10593
10877
  def propagate_children(node, table, current_scope)
10878
+ if node.is_a?(Prism::CaseNode) || node.is_a?(Prism::CaseMatchNode)
10879
+ return propagate_subjectless_case(node, table, current_scope) if node.predicate.nil?
10880
+
10881
+ return propagate_subject_case(node, table, current_scope)
10882
+ end
10883
+
10594
10884
  child_scope =
10595
10885
  SELF_OPENING_BODIES.include?(node.class) ? current_scope.with_block_self_unknown(false) : current_scope
10596
10886
  node.rigor_each_child { |child| propagate(child, table, child_scope) }
@@ -10796,6 +11086,85 @@ module Rigor
10796
11086
  propagate(node.statements, table, falsey_scope) if node.statements
10797
11087
  propagate(node.else_clause, table, truthy_scope) if node.else_clause
10798
11088
  end
11089
+
11090
+ # Issue #1495 — a `case` without a subject the evaluator did not enter (`[case; when x.nil? then 0; else x.size
11091
+ # end]`) narrows by its conditions' edges, as {StatementEvaluator#subjectless_when_scopes} does: each condition
11092
+ # under the falsey edges of the conditions before it, each clause under the join of its conditions' truthy edges,
11093
+ # and the `else` under the last falsey edge. Like the walk of an `if`, it applies no write a condition makes, so
11094
+ # the edges are taken from a scope where every bound local the condition writes reads `Dynamic[top]`
11095
+ # ({#widen_condition_writes}): neither the local's old type nor an earlier condition's narrowing of it reaches
11096
+ # them, as `when (x = s ? String(s) : nil) then x.upcase` after `x = 1` would.
11097
+ def propagate_subjectless_case(node, table, current_scope)
11098
+ falsey = current_scope
11099
+ node.conditions.each do |when_node|
11100
+ truthy_edges = when_node.conditions.map do |condition|
11101
+ propagate(condition, table, falsey)
11102
+ truthy, falsey = Narrowing.predicate_scopes(condition, widen_condition_writes(condition, falsey))
11103
+ truthy
11104
+ end
11105
+ propagate(when_node, table, truthy_edges.reduce(:join))
11106
+ end
11107
+ propagate(node.else_clause, table, falsey) if node.else_clause
11108
+ end
11109
+
11110
+ # Issue #1484 — a `case` with a subject the evaluator did not enter (`[case io when StringIO then io.string
11111
+ # end]`, `[case io in StringIO then io.string else nil end]`) narrows each clause as the evaluator does
11112
+ # ({Narrowing.case_clause_scopes}): the body under the clause's narrowed edge, the next clause and the `else`
11113
+ # under its falsey edge. The walk applies no write the subject, a `when` condition or an `in` pattern makes, so
11114
+ # every bound local one of them writes reads `Dynamic[top]` from that point on ({#widen_condition_writes}).
11115
+ # Nor does it run the evaluator's per-clause `LastLine.forget_if_set`: the statement holding the `case`
11116
+ # already forgets a `$_` any of its tests may set. A clause's tests are recorded under the scope the earlier
11117
+ # clauses' falsey edges left, where the evaluator's `when` tests read the post-subject scope: a test reading
11118
+ # the subject sees what the earlier clauses ruled out (`when NilClass then 0 when v.succ`).
11119
+ #
11120
+ # A test that calls a method may rebind an instance-variable or global subject before the body reads it
11121
+ # (`when String then 1 when reset! then @x.upcase`, where `reset!` writes `@x`), and the walk cannot tell. From
11122
+ # the first such test on, the subject reads `Dynamic[top]` on both edges ({#forget_called_subject}). The
11123
+ # evaluator keeps the narrowing there (a separate defect); the walk declines it rather than add a report.
11124
+ def propagate_subject_case(node, table, current_scope)
11125
+ subject = node.predicate
11126
+ propagate(subject, table, current_scope)
11127
+ falsey = widen_condition_writes(subject, current_scope)
11128
+ called = false
11129
+ node.conditions.each do |clause|
11130
+ tests = clause.is_a?(Prism::WhenNode) ? clause.conditions : [clause.pattern]
11131
+ tests.each { |test| propagate(test, table, falsey) }
11132
+ falsey = tests.reduce(falsey) { |acc, test| widen_condition_writes(test, acc) }
11133
+ called ||= tests.any? { |test| runs_call?(test) }
11134
+ body, falsey = Narrowing.case_clause_scopes(subject, clause, falsey)
11135
+ body, falsey = [body, falsey].map { |scope| forget_called_subject(subject, scope) } if called
11136
+ propagate(clause, table, body)
11137
+ end
11138
+ propagate(node.else_clause, table, falsey) if node.else_clause
11139
+ end
11140
+
11141
+ CODE_RUNNING_NODES = [Prism::CallNode, Prism::SuperNode, Prism::ForwardingSuperNode, Prism::YieldNode].freeze
11142
+ private_constant :CODE_RUNNING_NODES
11143
+
11144
+ # Whether `node` holds a method call, `super` or `yield` anywhere, a lambda body's included.
11145
+ def runs_call?(node)
11146
+ return false unless node.is_a?(Prism::Node)
11147
+ return true if CODE_RUNNING_NODES.include?(node.class)
11148
+
11149
+ node.rigor_each_child { |child| return true if runs_call?(child) }
11150
+ false
11151
+ end
11152
+
11153
+ # `scope` with an instance-variable or global `subject` rebound to `Dynamic[top]`; any other subject as is.
11154
+ def forget_called_subject(subject, scope)
11155
+ case subject
11156
+ when Prism::InstanceVariableReadNode then scope.with_ivar(subject.name, Type::Combinator.untyped)
11157
+ when Prism::GlobalVariableReadNode then scope.with_global(subject.name, Type::Combinator.untyped)
11158
+ else scope
11159
+ end
11160
+ end
11161
+
11162
+ # `scope` with every bound local `condition` writes rebound to `Dynamic[top]`.
11163
+ def widen_condition_writes(condition, scope)
11164
+ OperandEffects.written_variables(condition).reduce(scope) do |acc, name|
11165
+ acc.local(name) ? acc.with_local(name, Type::Combinator.untyped) : acc
11166
+ end
11167
+ end
10799
11168
  end
10800
11169
  end
10801
11170
  end