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
@@ -526,12 +526,60 @@ module Rigor
526
526
  registry = environment&.plugin_registry
527
527
  each_source_ancestor_candidate(scope, class_name) do |candidate|
528
528
  return nil if scope.discovered_method?(candidate, method_name, kind)
529
- return candidate if ALLOWED_RBS_COMPLETE_ANCESTORS.include?(candidate) ||
530
- registry&.rbs_complete_ancestor?(candidate)
529
+ next unless ALLOWED_RBS_COMPLETE_ANCESTORS.include?(candidate) ||
530
+ registry&.rbs_complete_ancestor?(candidate)
531
+
532
+ return shadowed_in_ruby_order?(scope, class_name, kind, method_name, candidate) ? nil : candidate
531
533
  end
532
534
  nil
533
535
  end
534
536
 
537
+ # Issue #1572 — the walk above is breadth-first, so it reaches the superclass before a module an included
538
+ # module includes, which Ruby's order puts ahead of it (`class P < Rigor::Plugin::Base; include A` with `A`
539
+ # including `M` calls `M#prepare`, not `Base#prepare`), and it passes over RBS-known modules. True when the
540
+ # bridged `ancestor` is not what Ruby calls: on `class_name`'s {Scope::ResolutionChain} in Ruby's order, a
541
+ # project entry ahead of it records the name ({#project_records_name?}), an external entry ahead of it is a
542
+ # module RBS knows and declares the name in, or the chain does not hold it. An RBS-unknown external ahead is
543
+ # passed over, as the breadth-first walk always did. An unsettled chain withholds too: a mixin edge the
544
+ # tables cannot place (a receiver-form `Demo.include(M)` after the body, an edge in a conditional, a block or
545
+ # a hook) may put a definer ahead of the ancestor.
546
+ def shadowed_in_ruby_order?(scope, class_name, kind, method_name, ancestor)
547
+ chain = Scope::ResolutionChain.for(scope, class_name.to_s, kind, :methods)
548
+ return true if chain.unsettled?
549
+
550
+ entries = chain.entries
551
+ stop = entries.index do |entry|
552
+ entry.external? ? entry.candidates.include?(ancestor) : entry.name == ancestor
553
+ end
554
+ return true if stop.nil?
555
+
556
+ entries.first(stop).any? do |entry|
557
+ if entry.external?
558
+ rbs_declares_ahead?(scope, entry, method_name)
559
+ else
560
+ project_records_name?(scope, entry.name, method_name, entry.side)
561
+ end
562
+ end
563
+ end
564
+
565
+ # Whether the project entry `owner` holds the name on `kind`'s side. `discovered_methods` carries the
566
+ # `attr_*` and `define_method` rows project-wide, but at dispatch it has no row for a `def` in a module
567
+ # declared in another file, so a `def` is also asked of the project-wide def tables. ADR-46:
568
+ # `user_def_for` / `singleton_def_for` file the method edge, positive or negative, so a file that adds or
569
+ # removes the `def` on an entry ahead of the ancestor re-checks the call.
570
+ def project_records_name?(scope, owner, method_name, kind)
571
+ return true if scope.discovered_method?(owner, method_name, kind)
572
+
573
+ return !scope.singleton_def_for(owner, method_name).nil? if kind == :singleton
574
+
575
+ !scope.user_def_for(owner, method_name).nil?
576
+ end
577
+
578
+ def rbs_declares_ahead?(scope, entry, method_name)
579
+ entry.candidates.any? { |candidate| Rigor::Reflection.rbs_class_known?(candidate, scope: scope) } &&
580
+ !Scope::ResolutionChain::Relevance.external_lacks_on?(scope, entry.candidates, entry.side, method_name)
581
+ end
582
+
535
583
  # Issue #527 slice 1 — the RBS instance definition a Ruby-source class inherits from a CORE or
536
584
  # STDLIB ancestor, or nil. `Oj::EasyHash < Hash` answering `Dynamic[top]` to `has_key?` while
537
585
  # `{}.has_key?` folds was the largest single opacity family in the 2026-09-01 corpus sweep.
@@ -220,7 +220,8 @@ module Rigor
220
220
  # `call.undefined-method`.
221
221
  patched_result =
222
222
  unless skip_project_patched
223
- try_project_patched_method(receiver_type, method_name, arg_types, block_type, environment)
223
+ try_project_patched_method(receiver_type, method_name, arg_types, block_type, environment,
224
+ call_node: call_node, scope: scope)
224
225
  end
225
226
  if patched_result
226
227
  scope&.record_dynamic_origin(call_node, DynamicOrigin::EXTERNAL_GEM_WITHOUT_RBS)
@@ -258,6 +259,22 @@ module Rigor
258
259
  return discovered_result
259
260
  end
260
261
 
262
+ # Issue #1708 — a `pre_eval:` patch on one of the receiver's ancestors (`class Object; def zz`,
263
+ # `module Enumerable; def sum_by`). Below the RBS tier, whose decline means no nearer RBS declares the
264
+ # method, and below the tiers that answer the receiver's own patch or a gem catalogue entry. A nearer
265
+ # `def` in the project's source (on an RBS-known receiver, which the discovered-method tier leaves to
266
+ # user-method inference) stops the walk: see {#project_source_defines?}. That probe reads the scope's def
267
+ # index, so a single-file scope (`type-of`, `annotate`, an LSP buffer) cannot see a `def` in another file.
268
+ ancestor_patched =
269
+ if !skip_project_patched && scope
270
+ try_project_patched_method(receiver_type, method_name, arg_types, block_type, environment,
271
+ call_node: call_node, scope: scope, through_ancestors: true)
272
+ end
273
+ if ancestor_patched
274
+ scope&.record_dynamic_origin(call_node, DynamicOrigin::EXTERNAL_GEM_WITHOUT_RBS)
275
+ return ancestor_patched
276
+ end
277
+
261
278
  # ADR-5 robustness — synthesized-stub-type tier. When the receiver is a type Rigor
262
279
  # invented to make an otherwise-unbuildable project signature resolve (a
263
280
  # missing-namespace module, or a stub for a referenced-but-undeclared type like an
@@ -644,7 +661,13 @@ module Rigor
644
661
  # `def to_url; "hello"; end` patched onto `String` now resolves `s.to_url` to
645
662
  # `Dynamic[Nominal[String]]` instead of the pre-3a `Dynamic[Top]`. Falls back to
646
663
  # `Dynamic[Top]` when the heuristic declined (non-literal tail expression).
647
- def try_project_patched_method(receiver_type, method_name, arg_types, block_type, environment)
664
+ #
665
+ # `through_ancestors:` (issue #1708), with a scope, searches the receiver's ancestors as well, through
666
+ # {ProjectPatchedMethods#lookup_through_ancestors}, stopping at a nearer project-source `def`. A private or
667
+ # protected patch Ruby would not let the call reach ({ProjectPatchedMethods#callable?}) answers nothing, so
668
+ # `1.kp` on `module Kernel; private def kp` reads as the undefined call it is.
669
+ def try_project_patched_method(receiver_type, method_name, arg_types, block_type, environment, # rubocop:disable Metrics/ParameterLists
670
+ call_node: nil, scope: nil, through_ancestors: false)
648
671
  registry = environment&.project_patched_methods
649
672
  return nil if registry.nil? || registry.empty?
650
673
 
@@ -652,14 +675,55 @@ module Rigor
652
675
  return nil if class_name.nil?
653
676
 
654
677
  kind = receiver_type.is_a?(Type::Singleton) ? :singleton : :instance
655
- entry = registry.lookup(class_name: class_name, method_name: method_name, kind: kind)
678
+ entry = project_patched_entry(registry, class_name, method_name, kind, environment,
679
+ scope: scope, through_ancestors: through_ancestors)
656
680
  return nil if entry.nil?
681
+
682
+ verdict = registry.callable?(entry, explicit: explicit_non_self_receiver?(call_node),
683
+ self_type: scope ? scope.self_type : :unknown,
684
+ rbs_loader: environment.rbs_loader)
685
+ return nil if verdict == false
686
+ return Type::Combinator.untyped if verdict.nil?
657
687
  return Type::Combinator.dynamic(entry.return_type) if entry.return_type
658
688
 
659
689
  aliased = project_patched_alias_target(entry, receiver_type, arg_types, block_type, environment)
660
690
  aliased ? Type::Combinator.dynamic(aliased) : Type::Combinator.untyped
661
691
  end
662
692
 
693
+ def project_patched_entry(registry, class_name, method_name, kind, environment, scope:, through_ancestors:)
694
+ return registry.lookup(class_name: class_name, method_name: method_name, kind: kind) unless through_ancestors
695
+
696
+ registry.lookup_through_ancestors(
697
+ class_name: class_name, method_name: method_name, kind: kind, rbs_loader: environment.rbs_loader,
698
+ source_defines: ->(owner, side) { project_source_defines?(scope, owner, method_name, side) }
699
+ )
700
+ end
701
+
702
+ # Issue #1708 — does the project's own source define `method_name` on `owner` (`side` as the ancestor walk
703
+ # reaches it), or on a module the project prepends to or includes into it? Ruby runs that `def` ahead of a
704
+ # patch on a farther ancestor: `class Integer; def nn = "s"` beats `class Object; def nn = nil` in a
705
+ # `pre_eval:` file. The owner's level of its {Scope::ResolutionChain} (its prepends, itself, its includes
706
+ # and their own mixins, or a class object's singleton and its extends) holds those definers; whether one
707
+ # of them defines the name does not depend on their order, so the chain is searched without settling it.
708
+ def project_source_defines?(scope, owner, method_name, side)
709
+ return false unless scope.known_user_class?(owner)
710
+
711
+ chain = Scope::ResolutionChain.for(scope, owner, side, :methods)
712
+ own_level_end = chain.level_starts[1] || chain.entries.size
713
+ found = chain.search(scope, 0, own_level_end) do |entry|
714
+ !entry.external? && source_def?(scope, entry.name, method_name, entry.side)
715
+ end
716
+ !found.nil?
717
+ end
718
+
719
+ def source_def?(scope, owner, method_name, side)
720
+ if side == :singleton
721
+ scope.discovered_method?(owner, method_name, :singleton) || !scope.singleton_def_for(owner, method_name).nil?
722
+ else
723
+ scope.discovered_method?(owner, method_name, :instance) || !scope.user_def_for(owner, method_name).nil?
724
+ end
725
+ end
726
+
663
727
  # Issue #1702 — an alias in a `pre_eval:` patch whose old name no earlier patch binding defines
664
728
  # (`alias old_plus +` on Integer, or `alias_method :orig_succ, :succ` ahead of the patch's own `def succ`)
665
729
  # names the method the class had before the patch, so it answers what that method answers for the same
@@ -9,24 +9,36 @@ module Rigor
9
9
  # The one home for what a receiverless `module_function` call does to the methods around it
10
10
  # ([#1507](https://github.com/rigortype/rigor/issues/1507),
11
11
  # [#1197](https://github.com/rigortype/rigor/issues/1197)). Four readers used to compute it in four
12
- # places. They disagree with each other and, in places, with Ruby. Each entry point below reproduces
13
- # one reader's answer exactly: this module gives the readings one place to live, and does not yet make
14
- # them agree. A change to one reading lands here, beside the other three. An example is the reset a bare
15
- # `public`, `private` or `protected` makes.
12
+ # places, and the method-visibility table reads it too. They disagree with each other and, in places, with
13
+ # Ruby. Each entry point below reproduces one reader's answer exactly: this module gives the readings one
14
+ # place to live, and does not yet make them agree. A change to one reading lands here, beside the others.
15
+ #
16
+ # In Ruby a bare `module_function` call turns the body's module_function mode on, and a later bare
17
+ # `public`, `private` or `protected` ({.visibility_reset?}) turns it off again (#1968). A named call
18
+ # (`public :x`, `private def y`) leaves the mode alone, and each `module` body starts with it off. A reset
19
+ # written in the body's own statements, or in a plain `begin` or `ensure` there, runs. One nested anywhere
20
+ # else (control flow, a block, a `rescue` clause, an argument) may not, so the mode after it is uncertain.
21
+ # A reset in a nested `class`, `module`, `class <<` or `def` body, a `*_eval` / `*_exec` block or a
22
+ # class-creating block (`Class.new do`, `Struct.new do`, ...) belongs to another scope and does not count.
16
23
  #
17
24
  # - The singleton def-node table (`ScopeIndexer#walk_singleton_body`) reads
18
25
  # {.each_singleton_sibling} and {.each_singleton_copy}. A bare call covers the later direct
19
- # statements of its own body and is never reset.
26
+ # statements of its own body, up to a reset; after an uncertain one the copy is possible.
20
27
  # - The deferred ranges (`ScopeIndexer#walk_deferred_body`) read {.prescan_deferred} and
21
- # {.deferred_module_function?}. A bare call covers every later def by offset, and a call inside
22
- # control flow or a block counts.
28
+ # {.deferred_module_function?}. A bare call covers every later def by offset, up to a later reset,
29
+ # and a call inside control flow or a block counts. An uncertain reset changes nothing here.
23
30
  # - The extends table (`ScopeIndexer#record_extend_call`) reads {.extends_self?}. A bare call covers
24
- # every instance def of the module, in any order.
31
+ # every instance def of the module, in any order, and is never reset.
25
32
  # - sig-gen (`SigGen::Generator#walk_statements`) reads {.each_sig_gen_statement} and
26
33
  # {.sig_gen_module_function?}. A bare call covers the later statements of its own statement list and
27
- # what nests in them.
34
+ # what nests in them, up to a reset; after an uncertain one {.sig_gen_uncertain?} says so.
35
+ # - The method-visibility table (`ScopeIndexer#walk_method_visibilities`) reads {.instance_visibility}. It
36
+ # applies a `module_function` call as it applies a `private` one, bare or named: the call makes the instance
37
+ # copy private (#1569), and a later bare `public`, `private` or `protected` sets the running default as it
38
+ # would anyway, so the reset needs no reading of its own. A call or reset that may not run leaves the slots
39
+ # after it contested, as any visibility call there does.
28
40
  #
29
- # A fifth reader does not come here yet: `rigor-activerecord`'s `ModelDiscoverer#table_name_decorators`
41
+ # Another reader does not come here yet: `rigor-activerecord`'s `ModelDiscoverer#table_name_decorators`
30
42
  # (`plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb`) reads a module body's
31
43
  # direct statements for its table-name readers. It also accepts `self.module_function`, and there a bare
32
44
  # call covers the instance methods written before it as well as after it, while a named call takes only a
@@ -47,23 +59,60 @@ module Rigor
47
59
  node.name == :module_function && node.receiver.nil?
48
60
  end
49
61
 
62
+ # The visibility a `module_function` call gives the instance method of each name it covers, bare or named:
63
+ # Ruby makes the instance copy private and adds a public singleton copy, which the instance-side
64
+ # method-visibility table does not hold (#1569).
65
+ def instance_visibility = :private
66
+
50
67
  # The toggle form: a `module_function` call with no arguments.
51
68
  def bare?(node)
52
69
  node.arguments.nil? || node.arguments.arguments.empty?
53
70
  end
54
71
 
72
+ # A bare `public`, `private` or `protected` call, receiverless or on `self`: Ruby ends the module_function
73
+ # mode there (#1968). A call with arguments sets the named methods' visibility and leaves the mode alone.
74
+ def visibility_reset?(node)
75
+ %i[public private protected].include?(node.name) && bare?(node) &&
76
+ (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
77
+ end
78
+
79
+ # The mode after `stmt`, a direct statement of a body, for the readers that walk the statements: `mode`
80
+ # is `true`, `false` or `:uncertain` before it. A {.visibility_reset?} the statement runs ends the mode,
81
+ # and one it may not run (`:maybe_off`, below {.prescan_deferred}) makes an on mode uncertain. A
82
+ # `module_function` nested in the statement is not read here, as before. `nested` says the statement
83
+ # itself may not run, so every reset in it may not either.
84
+ def mode_after(mode, stmt, nested: false)
85
+ return mode unless mode
86
+
87
+ events = []
88
+ prescan_deferred(stmt, EMPTY_PREFIX, false, events, [], nested: nested)
89
+ events.sort_by(&:first).each do |_offset, event|
90
+ case event
91
+ when :off then mode = false
92
+ when :maybe_off then mode = :uncertain if mode == true
93
+ end
94
+ end
95
+ mode
96
+ end
97
+
98
+ # The owner prefix {.mode_after} hands the prescan, which records no row a reset needs.
99
+ EMPTY_PREFIX = [].freeze
100
+
55
101
  # The singleton def-node table's reading, over one class-ish body's direct statements in source
56
102
  # order. `ScopeIndexer#statements_of` builds that list, folding a body-level `rescue`, `else` or
57
103
  # `ensure` clause into it. Yields `stmt, module_function_on, named_call` for every statement except a
58
104
  # bare call:
59
105
  #
60
- # - `module_function_on` says whether a bare call precedes the statement. The toggle never switches
61
- # off, although a later bare `public`, `private` or `protected` resets it in Ruby.
106
+ # - `module_function_on` is `true` when a bare call precedes the statement with no reset between them,
107
+ # `:uncertain` when a reset that may not run is between them ({.mode_after}), and `false` otherwise.
62
108
  # - `named_call` is true for a named call (`module_function :a` or `module_function def x`), which
63
109
  # {.each_singleton_copy} resolves; the caller records nothing else for it.
64
110
  #
65
- # A call nested in control flow or a block is not seen.
66
- def each_singleton_sibling(statements)
111
+ # A `module_function` call nested in control flow or a block is not seen. `conditional` lists the
112
+ # statements of a body-level `rescue` or `else` clause ({.conditional_clause_statements}), which may not
113
+ # run, so a reset there makes the mode uncertain rather than ending it.
114
+ def each_singleton_sibling(statements, conditional = [])
115
+ conditional = Set.new(conditional).compare_by_identity
67
116
  module_function_on = false
68
117
  statements.each do |stmt|
69
118
  if stmt.is_a?(Prism::CallNode) && call?(stmt)
@@ -75,9 +124,26 @@ module Rigor
75
124
  next
76
125
  end
77
126
  yield stmt, module_function_on, false
127
+ module_function_on = mode_after(module_function_on, stmt, nested: conditional.include?(stmt))
78
128
  end
79
129
  end
80
130
 
131
+ # The statements of a body-level `rescue` or `else` clause, which `ScopeIndexer#statements_of` folds into
132
+ # the body's direct statements although they run only on an exception, or only without one. An `ensure`
133
+ # clause runs either way and is not listed.
134
+ def conditional_clause_statements(body)
135
+ return [] unless body.is_a?(Prism::BeginNode)
136
+
137
+ statements = []
138
+ clause = body.rescue_clause
139
+ while clause
140
+ statements.concat(ScopeIndexer.statements_of(clause.statements))
141
+ clause = clause.subsequent
142
+ end
143
+ statements.concat(ScopeIndexer.statements_of(body.else_clause.statements)) if body.else_clause
144
+ statements
145
+ end
146
+
81
147
  # The defs a named call copies onto the module's singleton, for the singleton def-node table. Ruby
82
148
  # copies the method in effect at the call (#1550), so a later def of the name redefines only the
83
149
  # instance method. This reading approximates that from the body's direct `statements`: it yields
@@ -121,10 +187,11 @@ module Rigor
121
187
  alias_method define_method attr attr_reader attr_writer attr_accessor remove_method undef_method
122
188
  ].freeze
123
189
 
124
- # The deferred-ranges reading, over one class-ish body's subtree. Appends to `offsets` the start of
125
- # every bare call; each def that starts after one is a module function
126
- # ({.deferred_module_function?}). Appends to `ranges` one row per named call, in the table's
127
- # `[start, end, name, kind, owner]` shape:
190
+ # The deferred-ranges reading, over one class-ish body's subtree. Appends to `offsets` an
191
+ # `[offset, :on]` event for the start of every bare call, and for every {.visibility_reset?} an
192
+ # `[offset, :off]` event where it runs or a `[offset, :maybe_off]` one where it may not: `nested` is true
193
+ # once the walk has left the body's statements, a plain `begin` and an `ensure`. Appends to `ranges` one
194
+ # row per named call, in the table's `[start, end, name, kind, owner]` shape:
128
195
  #
129
196
  # - a `:name` argument gives a `:singleton` row over the call itself, because the copy happens at
130
197
  # the call and not at the def;
@@ -134,33 +201,36 @@ module Rigor
134
201
  # The prescan looks through control flow and blocks, because a call that ran there still flips the
135
202
  # later defs. It stops at a nested class, module, `class <<` or def body, where the call targets
136
203
  # another module. It skips an `END` body, which runs after every def, and a `*_eval` / `*_exec`
137
- # block, whose self is the receiver's module; that block gets its own prescan as a body. Nothing
138
- # resets it.
204
+ # block, whose self is the receiver's module, and a class-creating block (`Class.new do`, `Struct.new do`,
205
+ # `Data.define do`, `Module.new do`), whose self is the new class; such a block gets its own prescan as a
206
+ # body where the walk opens one.
139
207
  #
140
208
  # `qualified_prefix` names the owner, and `in_singleton_class` says whether the body is a
141
209
  # `class <<` body; both feed `ScopeIndexer.def_singleton?`.
142
- def prescan_deferred(node, qualified_prefix, in_singleton_class, offsets, ranges)
210
+ def prescan_deferred(node, qualified_prefix, in_singleton_class, offsets, ranges, nested: false)
143
211
  return unless node.is_a?(Prism::Node)
144
212
  return if node.is_a?(Prism::ClassNode) || node.is_a?(Prism::ModuleNode) ||
145
213
  node.is_a?(Prism::SingletonClassNode) || node.is_a?(Prism::DefNode) ||
146
214
  node.is_a?(Prism::PostExecutionNode)
215
+ return if node.is_a?(Prism::CallNode) &&
216
+ prescanned_call?(node, qualified_prefix, in_singleton_class, offsets, ranges, nested)
147
217
 
148
- if node.is_a?(Prism::CallNode)
149
- return prescan_call(node, qualified_prefix, in_singleton_class, offsets, ranges) if call?(node)
150
- if ScopeIndexer.receiver_eval_call?(node)
151
- return prescan_eval_call(node, qualified_prefix, in_singleton_class, offsets, ranges)
152
- end
153
- end
154
-
218
+ nested ||= !RUN_IN_PLACE.include?(node.class)
155
219
  node.rigor_each_child do |child|
156
- prescan_deferred(child, qualified_prefix, in_singleton_class, offsets, ranges)
220
+ prescan_deferred(child, qualified_prefix, in_singleton_class, offsets, ranges, nested: nested)
157
221
  end
158
222
  end
159
223
 
160
- # Whether the deferred-ranges reading makes a def starting at `start` a module function: a bare call
161
- # starts before it, at any depth the prescan entered.
224
+ # The nodes whose statements run where they are written, so a reset there runs ({.prescan_deferred}).
225
+ RUN_IN_PLACE = [Prism::StatementsNode, Prism::BeginNode, Prism::EnsureNode].freeze
226
+
227
+ # Whether the deferred-ranges reading makes a def starting at `start` a module function: the last
228
+ # `:on` or `:off` event {.prescan_deferred} recorded before it, at any depth the prescan entered, is
229
+ # `:on`. A `:maybe_off` leaves the answer as it was: the row stays `:both`, the reading that counts the
230
+ # def as run on the singleton side too, since the table has no possible row for it.
162
231
  def deferred_module_function?(offsets, start)
163
- offsets.any? { |offset| offset < start }
232
+ last = offsets.select { |offset, event| offset < start && event != :maybe_off }.max_by(&:first)
233
+ last ? last[1] == :on : false
164
234
  end
165
235
 
166
236
  # The extends table's reading. A bare call makes the module extend itself, so every instance def the
@@ -174,10 +244,12 @@ module Rigor
174
244
  end
175
245
 
176
246
  # sig-gen's reading, over one statement list. Yields every statement except a bare call, with
177
- # whether a bare call came before it in this list or `active` was already true on entry. The
247
+ # whether a bare call came before it in this list or `active` was already on at entry: `true`, `false`
248
+ # or `:uncertain` after a reset that may not run ({.mode_after}). The
178
249
  # generator passes the enclosing list's answer into an `if`, a block or a `begin`, but not into a
179
250
  # nested class, module or `class << self`. A bare call in a nested list covers only that list's
180
- # later statements. Nothing resets it, and a named call is not a directive at all.
251
+ # later statements. A reset ends it for the statements after it, and a named call is not a directive at
252
+ # all.
181
253
  def each_sig_gen_statement(statements, active)
182
254
  statements.each do |stmt|
183
255
  if sig_gen_directive?(stmt)
@@ -185,6 +257,7 @@ module Rigor
185
257
  next
186
258
  end
187
259
  yield stmt, active
260
+ active = mode_after(active, stmt)
188
261
  end
189
262
  end
190
263
 
@@ -196,7 +269,13 @@ module Rigor
196
269
  # Whether sig-gen renders a def as `def self?.name`: an instance-side def under an active directive,
197
270
  # in a class or a module alike. `kind` is `:singleton` for `def self.x` and a `class << self` body.
198
271
  def sig_gen_module_function?(active, kind)
199
- active && kind == :instance
272
+ active == true && kind == :instance
273
+ end
274
+
275
+ # Whether sig-gen cannot tell a def's kind: an instance-side def after a reset that may not run, which
276
+ # is a module function only if the reset did not run.
277
+ def sig_gen_uncertain?(active, kind)
278
+ active == :uncertain && kind == :instance
200
279
  end
201
280
 
202
281
  # Yields each instance-method name `node` may rebind on the module whose body holds it, or
@@ -239,10 +318,24 @@ module Rigor
239
318
  end
240
319
  end
241
320
 
321
+ # The prescan's arms for a call; true when the arm took the call and its subtree.
322
+ def prescanned_call?(node, qualified_prefix, in_singleton_class, offsets, ranges, nested)
323
+ if call?(node)
324
+ prescan_call(node, qualified_prefix, in_singleton_class, offsets, ranges)
325
+ elsif visibility_reset?(node)
326
+ offsets << [node.location.start_offset, nested ? :maybe_off : :off]
327
+ elsif ScopeIndexer.receiver_eval_call?(node) || (node.block && ScopeIndexer.meta_new_constant_rvalue?(node))
328
+ prescan_eval_call(node, qualified_prefix, in_singleton_class, offsets, ranges)
329
+ else
330
+ return false
331
+ end
332
+ true
333
+ end
334
+
242
335
  # The prescan's call arm.
243
336
  def prescan_call(node, qualified_prefix, in_singleton_class, offsets, ranges)
244
337
  if bare?(node)
245
- offsets << node.location.start_offset
338
+ offsets << [node.location.start_offset, :on]
246
339
  else
247
340
  owner = qualified_prefix.empty? ? nil : qualified_prefix.join("::")
248
341
  node.arguments&.arguments&.each do |arg|
@@ -252,20 +345,22 @@ module Rigor
252
345
  elsif (name = symbol_argument_name(arg))
253
346
  ranges << [node.location.start_offset, node.location.end_offset, name, :singleton, owner]
254
347
  else
255
- prescan_deferred(arg, qualified_prefix, in_singleton_class, offsets, ranges)
348
+ prescan_deferred(arg, qualified_prefix, in_singleton_class, offsets, ranges, nested: true)
256
349
  end
257
350
  end
258
351
  end
259
352
  return unless node.block
260
353
 
261
- prescan_deferred(node.block, qualified_prefix, in_singleton_class, offsets, ranges)
354
+ prescan_deferred(node.block, qualified_prefix, in_singleton_class, offsets, ranges, nested: true)
262
355
  end
263
356
 
264
357
  # The prescan's eval arm: only the call's receiver and arguments keep this module's context.
265
358
  def prescan_eval_call(node, qualified_prefix, in_singleton_class, offsets, ranges)
266
- prescan_deferred(node.receiver, qualified_prefix, in_singleton_class, offsets, ranges) if node.receiver
359
+ if node.receiver
360
+ prescan_deferred(node.receiver, qualified_prefix, in_singleton_class, offsets, ranges, nested: true)
361
+ end
267
362
  node.arguments&.arguments&.each do |arg|
268
- prescan_deferred(arg, qualified_prefix, in_singleton_class, offsets, ranges)
363
+ prescan_deferred(arg, qualified_prefix, in_singleton_class, offsets, ranges, nested: true)
269
364
  end
270
365
  end
271
366
 
@@ -274,8 +369,8 @@ module Rigor
274
369
  Source::Literals.symbol_or_string(arg)
275
370
  end
276
371
 
277
- private_class_method :prescan_call, :prescan_eval_call, :symbol_argument_name, :each_rebound_name,
278
- :each_own_rebound_name, :each_rebinding_call_name
372
+ private_class_method :prescan_call, :prescanned_call?, :prescan_eval_call, :symbol_argument_name,
373
+ :each_rebound_name, :each_own_rebound_name, :each_rebinding_call_name
279
374
  end
280
375
  end
281
376
  end
@@ -10,6 +10,7 @@ require_relative "../rbs_extended"
10
10
  require_relative "../analysis/fact_store"
11
11
  require_relative "../builtins/regex_refinement"
12
12
  require_relative "guard_rebinding"
13
+ require_relative "intervening_ivar_reset"
13
14
  require_relative "key_presence_guard"
14
15
  require_relative "last_line"
15
16
  require_relative "operand_effects"
@@ -511,8 +512,9 @@ module Rigor
511
512
  # none of the condition shapes are recognised, both returned scopes equal the input
512
513
  # scope. The catalogue mirrors {.case_equality_target_class}: static class/module
513
514
  # constants narrow as `is_a?`; integer/float-endpoint ranges narrow to `Numeric`;
514
- # string-endpoint ranges and regexp literals narrow to `String`; a `nil`, `true` or
515
- # `false` literal narrows as its class does ({.when_condition_class}).
515
+ # string-endpoint ranges narrow to `String`; regexp literals narrow as
516
+ # {.regexp_case_equality_type} says; a `nil`, `true` or `false` literal narrows as its
517
+ # class does ({.when_condition_class}).
516
518
  def case_when_scopes(subject, conditions, scope)
517
519
  # C1 — `case x when /re/` runs `/re/ === x`, which sets the regex match-data globals
518
520
  # exactly as a successful `=~` does. Narrow `$~`/`$&`/`$1..$N` on the clause body (the
@@ -1797,9 +1799,13 @@ module Rigor
1797
1799
  # passes through unchanged (failing one pattern proves nothing about the string's shape).
1798
1800
  # A non-literal / unanchored / `\Z` / line-anchored / `//x` pattern falls through to no
1799
1801
  # narrowing.
1802
+ #
1803
+ # The refinement names a String, so it shares `=~`'s gate (#1955): a receiver that may be a Symbol, or an
1804
+ # untyped one, is left as it is ({#refinable_operand?}).
1800
1805
  def analyse_whole_regex_match_predicate(node, scope)
1801
1806
  return nil unless node.receiver.is_a?(Prism::LocalVariableReadNode)
1802
1807
  return nil if node.arguments.nil? || node.arguments.arguments.size != 1
1808
+ return nil unless refinable_operand?(node, scope)
1803
1809
 
1804
1810
  pattern = regex_operand_pattern(node.arguments.arguments.first, scope)
1805
1811
  return nil unless pattern.is_a?(RegexMatchPattern)
@@ -2636,10 +2642,10 @@ module Rigor
2636
2642
  # iff the endpoints compare it. We conservatively narrow `x` to `Numeric` for
2637
2643
  # integer-endpoint ranges and to `String` for string-endpoint ranges; other endpoint
2638
2644
  # types fall through.
2639
- # - **Regexp literal receiver** (`/foo/ === x`): the match operator coerces `x` to a
2640
- # String, so the truthy edge narrows `x` to `String`. The falsey edge keeps the entry
2641
- # type unchanged because `Regexp#===` returns false for non-Strings AND for Strings
2642
- # that simply do not match.
2645
+ # - **Regexp literal receiver** (`/foo/ === x`): the truthy edge narrows `x` as
2646
+ # {#regexp_case_equality_type} says (its String and Symbol parts, #1955). The falsey edge
2647
+ # keeps the entry type unchanged because `Regexp#===` returns false for non-Strings AND
2648
+ # for Strings that simply do not match.
2643
2649
  #
2644
2650
  # Anything else — non-local LHS argument, dynamic receiver, custom `===` method — falls
2645
2651
  # through to the no-narrowing branch (nil), preserving the entry scope on both edges.
@@ -2703,6 +2709,9 @@ module Rigor
2703
2709
  if (class_name = lexical_class_name(receiver, scope))
2704
2710
  return class_predicate_scopes(scope, local_name, current, class_name, exact: false)
2705
2711
  end
2712
+ if regexp_literal?(receiver)
2713
+ return [scope.with_local(local_name, regexp_case_equality_type(current, scope)), scope]
2714
+ end
2706
2715
 
2707
2716
  target_class = case_equality_target_class(receiver)
2708
2717
  return nil if target_class.nil?
@@ -2716,7 +2725,7 @@ module Rigor
2716
2725
  end
2717
2726
 
2718
2727
  # The class a `when` condition narrows the subject to, as `is_a?` would, or nil when the condition is no class
2719
- # test Rigor reads: a class or module constant, a range or regexp literal ({.case_equality_target_class}),
2728
+ # test Rigor reads: a class or module constant, a range literal ({.case_equality_target_class}),
2720
2729
  # or (#1925) a `nil` / `true` / `false` literal. `nil === x` runs `NilClass#===`, which holds only for `nil`
2721
2730
  # itself whatever the subject's class defines, and `nil` is `NilClass`'s only instance, so `when nil` is
2722
2731
  # `when NilClass`; `true` and `false` likewise.
@@ -2733,8 +2742,9 @@ module Rigor
2733
2742
  # Maps a case-equality literal receiver to the class whose membership is implied by the
2734
2743
  # truthy edge. `Prism::ParenthesesNode` wrappers are transparently unwrapped (`(1..10)
2735
2744
  # === x` is parsed with the range inside parentheses). Range literals: integer
2736
- # endpoints → `Numeric`; string endpoints → `String`. Regexp literals → `String`. Other
2737
- # shapes return nil so the caller falls through.
2745
+ # endpoints → `Numeric`; string endpoints → `String`. Regexp literals → `String`, read only
2746
+ # as the class a dead `when` arm names: their narrowing is {#regexp_case_equality_type}'s,
2747
+ # which the callers consult first. Other shapes return nil so the caller falls through.
2738
2748
  def case_equality_target_class(receiver)
2739
2749
  receiver = unwrap_parens(receiver)
2740
2750
  case receiver
@@ -2743,6 +2753,33 @@ module Rigor
2743
2753
  end
2744
2754
  end
2745
2755
 
2756
+ def regexp_literal?(node)
2757
+ case unwrap_parens(node)
2758
+ when Prism::RegularExpressionNode, Prism::InterpolatedRegularExpressionNode then true
2759
+ else false
2760
+ end
2761
+ end
2762
+
2763
+ # Issue #1955 — the subject on the match edge of `/re/ === x` (and of `case x when /re/`). `Regexp#===`
2764
+ # matches a String, a Symbol by its name, and an object that converts with `to_str`, and answers false for
2765
+ # anything else. Each member narrows to its String and Symbol parts: a `String | nil` subject reads `String`, a
2766
+ # Symbol one stays `Symbol`. The `to_str` conversion is not modelled, so a member that defines `to_str` and is
2767
+ # neither drops out of the edge. A `Dynamic` member is left as it is, since it may be a Symbol at run time. The
2768
+ # no-match edge keeps the entry type: a String that does not match reaches it too.
2769
+ def regexp_case_equality_type(current, scope)
2770
+ members = current.is_a?(Type::Union) ? current.members : [current]
2771
+ Type::Combinator.union(*members.map { |member| regexp_case_equality_member(member, scope) })
2772
+ end
2773
+
2774
+ def regexp_case_equality_member(member, scope)
2775
+ return member if member.is_a?(Type::Dynamic)
2776
+
2777
+ parts = %w[String Symbol].map do |class_name|
2778
+ narrow_class(member, class_name, exact: false, environment: scope.environment, scope: scope)
2779
+ end
2780
+ Type::Combinator.union(*parts)
2781
+ end
2782
+
2746
2783
  def unwrap_parens(node)
2747
2784
  while node.is_a?(Prism::ParenthesesNode) && node.body.is_a?(Prism::StatementsNode) &&
2748
2785
  node.body.body.size == 1
@@ -2971,6 +3008,10 @@ module Rigor
2971
3008
  return integer_literal_when_result(current, int_literal, falsey_acc)
2972
3009
  end
2973
3010
 
3011
+ if regexp_literal?(condition)
3012
+ return { truthy: regexp_case_equality_type(current, scope), falsey: falsey_acc, fully_narrowable: true }
3013
+ end
3014
+
2974
3015
  target = when_condition_class(condition, scope)
2975
3016
  return class_when_result(scope, current, target, falsey_acc) if target
2976
3017
 
@@ -3793,9 +3834,12 @@ module Rigor
3793
3834
  # that edge, so a `$_` the earlier operand narrowed is forgotten when the operand may set it
3794
3835
  # (`gets && log(items.each { gets })`), before the operand's own edges narrow it again.
3795
3836
  # Issue #1429 — likewise a guard's narrowing of a global or constant the operand may rebind
3796
- # (`$sep && reset_sep && $sep.length`).
3837
+ # (`$sep && reset_sep && $sep.length`). Issue #1963 — and an instance variable's narrowing an implicit-self
3838
+ # or `self.` call in the operand may write ({InterveningIvarReset.after_operand}): `@x.nil? && reset!` does
3839
+ # not leave `@x` `nil` when `reset!` assigns it.
3797
3840
  def operand_edges(node, edge_scope)
3798
3841
  edge_scope = LastLine.forget_if_set(edge_scope, node)
3842
+ edge_scope = InterveningIvarReset.after_operand(node, edge_scope)
3799
3843
  if edge_scope.guard_narrowed? && GuardRebinding.may_rebind?(node, edge_scope)
3800
3844
  edge_scope = edge_scope.forget_guard_narrowings
3801
3845
  end