rigortype 0.4.0 → 0.4.2

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 (237) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -3
  3. data/data/builtins/ruby_core/io.yml +1 -0
  4. data/data/builtins/ruby_core/proc.yml +4 -0
  5. data/data/builtins/ruby_core/random.yml +4 -0
  6. data/data/builtins/ruby_core/struct.yml +3 -0
  7. data/data/core_overlay/env.rbs +34 -0
  8. data/data/core_overlay/gc.rbs +50 -0
  9. data/data/core_overlay/hash.rbs +45 -0
  10. data/data/core_overlay/integer.rbs +31 -0
  11. data/data/core_overlay/io_buffer.rbs +27 -0
  12. data/data/core_overlay/kernel.rbs +40 -0
  13. data/data/core_overlay/match_data.rbs +28 -0
  14. data/data/core_overlay/module.rbs +51 -0
  15. data/data/core_overlay/object_space.rbs +29 -0
  16. data/data/core_overlay/proc.rbs +45 -0
  17. data/data/core_overlay/range.rbs +40 -0
  18. data/data/core_overlay/source_range.rbs +78 -0
  19. data/data/core_overlay/string.rbs +86 -0
  20. data/data/vendored_gem_sigs/prime/integer-extension.rbs +41 -0
  21. data/data/vendored_gem_sigs/prime/prime.rbs +372 -0
  22. data/docs/handbook/01-getting-started.md +3 -3
  23. data/docs/handbook/06-classes.md +3 -1
  24. data/docs/handbook/08-understanding-errors.md +3 -2
  25. data/docs/handbook/11-sig-gen.md +6 -0
  26. data/docs/handbook/appendix-liskov.md +5 -0
  27. data/docs/install.md +87 -15
  28. data/docs/manual/02-cli-reference.md +2 -2
  29. data/docs/manual/03-configuration.md +6 -1
  30. data/docs/manual/04-diagnostics.md +2 -1
  31. data/docs/manual/06-baseline.md +2 -0
  32. data/docs/manual/07-plugins.md +1 -1
  33. data/docs/manual/08-skills.md +4 -3
  34. data/docs/manual/12-caching.md +43 -4
  35. data/docs/manual/14-rails-quickstart.md +19 -11
  36. data/docs/manual/18-removing-dead-code.md +32 -2
  37. data/docs/manual/plugins/README.md +6 -1
  38. data/docs/manual/plugins/rigor-activerecord.md +21 -0
  39. data/docs/manual/plugins/rigor-alba.md +60 -0
  40. data/docs/manual/plugins/rigor-minitest.md +22 -4
  41. data/docs/manual/plugins/rigor-typelizer.md +71 -0
  42. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
  43. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
  44. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
  45. data/lib/rigor/analysis/check_rules/rule_ids.rb +4 -0
  46. data/lib/rigor/analysis/check_rules/source_arity.rb +184 -61
  47. data/lib/rigor/analysis/check_rules.rb +407 -144
  48. data/lib/rigor/analysis/dependency_recorder.rb +215 -18
  49. data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
  50. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
  51. data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
  52. data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
  53. data/lib/rigor/analysis/erb_template_detector.rb +2 -1
  54. data/lib/rigor/analysis/incremental.rb +29 -9
  55. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +291 -0
  56. data/lib/rigor/analysis/incremental_run_slot.rb +173 -0
  57. data/lib/rigor/analysis/incremental_session.rb +287 -50
  58. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
  59. data/lib/rigor/analysis/reachability/graph.rb +123 -23
  60. data/lib/rigor/analysis/reachability/scan.rb +290 -36
  61. data/lib/rigor/analysis/reachability/scan_cache.rb +2 -1
  62. data/lib/rigor/analysis/rule_catalog.rb +43 -0
  63. data/lib/rigor/analysis/run_cache_key.rb +43 -9
  64. data/lib/rigor/analysis/run_stats.rb +2 -2
  65. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +88 -6
  66. data/lib/rigor/analysis/runner/pool_coordinator.rb +52 -13
  67. data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -13
  68. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  69. data/lib/rigor/analysis/runner.rb +371 -49
  70. data/lib/rigor/analysis/shadow_harness.rb +27 -0
  71. data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
  72. data/lib/rigor/analysis/template_unit_paths.rb +6 -4
  73. data/lib/rigor/analysis/template_units.rb +15 -5
  74. data/lib/rigor/analysis/worker_session.rb +11 -7
  75. data/lib/rigor/cache/descriptor.rb +40 -1
  76. data/lib/rigor/cache/file_digest.rb +28 -0
  77. data/lib/rigor/cache/incremental_snapshot.rb +92 -8
  78. data/lib/rigor/cache/store.rb +63 -3
  79. data/lib/rigor/cache/value_digest.rb +325 -0
  80. data/lib/rigor/cli/annotate_command.rb +5 -0
  81. data/lib/rigor/cli/baseline_command.rb +7 -18
  82. data/lib/rigor/cli/check_command.rb +85 -14
  83. data/lib/rigor/cli/doctor_command.rb +1 -1
  84. data/lib/rigor/cli/effects_report.rb +2 -2
  85. data/lib/rigor/cli/trace_command.rb +5 -0
  86. data/lib/rigor/cli/type_of_command.rb +5 -3
  87. data/lib/rigor/cli/unused_command.rb +16 -10
  88. data/lib/rigor/cli.rb +11 -3
  89. data/lib/rigor/config_audit.rb +3 -2
  90. data/lib/rigor/config_path.rb +29 -0
  91. data/lib/rigor/configuration/severity_profile.rb +11 -0
  92. data/lib/rigor/configuration.rb +52 -7
  93. data/lib/rigor/effects/plugin_facts.rb +6 -6
  94. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
  95. data/lib/rigor/environment/gated_signature_guard.rb +43 -0
  96. data/lib/rigor/environment/gated_signature_plan.rb +339 -0
  97. data/lib/rigor/environment/lockfile_resolver.rb +10 -1
  98. data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
  99. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  100. data/lib/rigor/environment/rbs_loader.rb +284 -15
  101. data/lib/rigor/environment/required_features.rb +206 -0
  102. data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
  103. data/lib/rigor/environment.rb +34 -73
  104. data/lib/rigor/inference/acceptance.rb +28 -3
  105. data/lib/rigor/inference/block_parameter_binder.rb +29 -13
  106. data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
  107. data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
  108. data/lib/rigor/inference/captured_locals.rb +7 -1
  109. data/lib/rigor/inference/declaration_walk/context.rb +195 -0
  110. data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
  111. data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
  112. data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
  113. data/lib/rigor/inference/declaration_walk.rb +7 -0
  114. data/lib/rigor/inference/def_node_resolver.rb +26 -3
  115. data/lib/rigor/inference/definer_resolution.rb +266 -0
  116. data/lib/rigor/inference/expression_typer.rb +351 -61
  117. data/lib/rigor/inference/external_ancestor_resolution.rb +13 -16
  118. data/lib/rigor/inference/global_write_census.rb +7 -4
  119. data/lib/rigor/inference/guard_rebinding.rb +54 -13
  120. data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
  121. data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
  122. data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +282 -0
  123. data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
  124. data/lib/rigor/inference/in_effect_refinements.rb +256 -0
  125. data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
  126. data/lib/rigor/inference/key_presence_guard.rb +303 -0
  127. data/lib/rigor/inference/macro_block_self_type.rb +73 -14
  128. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  129. data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
  130. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
  131. data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
  132. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
  133. data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
  134. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
  135. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
  136. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +305 -54
  137. data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
  138. data/lib/rigor/inference/method_dispatcher.rb +44 -11
  139. data/lib/rigor/inference/module_function_state.rb +214 -0
  140. data/lib/rigor/inference/multi_target_binder.rb +21 -16
  141. data/lib/rigor/inference/narrowing.rb +166 -22
  142. data/lib/rigor/inference/object_mixins.rb +180 -0
  143. data/lib/rigor/inference/parameter_inference_collector.rb +10 -9
  144. data/lib/rigor/inference/project_patched_methods.rb +10 -5
  145. data/lib/rigor/inference/project_patched_scanner.rb +71 -3
  146. data/lib/rigor/inference/refine_census.rb +239 -0
  147. data/lib/rigor/inference/refine_self.rb +317 -0
  148. data/lib/rigor/inference/refined_dispatch/own_method.rb +403 -0
  149. data/lib/rigor/inference/refined_dispatch.rb +272 -0
  150. data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
  151. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
  152. data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
  153. data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
  154. data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
  155. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +143 -0
  156. data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
  157. data/lib/rigor/inference/scope_indexer.rb +2129 -354
  158. data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
  159. data/lib/rigor/inference/statement_evaluator.rb +149 -32
  160. data/lib/rigor/inference/string_mutation.rb +9 -3
  161. data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
  162. data/lib/rigor/inference/version_guard.rb +19 -10
  163. data/lib/rigor/language_server/project_context.rb +31 -0
  164. data/lib/rigor/language_server/server.rb +18 -0
  165. data/lib/rigor/plugin/base.rb +28 -8
  166. data/lib/rigor/plugin/inflector.rb +1 -1
  167. data/lib/rigor/plugin/io_boundary.rb +124 -7
  168. data/lib/rigor/plugin/isolation.rb +1 -1
  169. data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
  170. data/lib/rigor/plugin/trust_policy.rb +3 -3
  171. data/lib/rigor/project_environment.rb +29 -6
  172. data/lib/rigor/protection/discovery_seed.rb +18 -9
  173. data/lib/rigor/protection/mutation_cache.rb +4 -4
  174. data/lib/rigor/protection/mutator.rb +1 -1
  175. data/lib/rigor/reflection/constant_ancestors.rb +211 -46
  176. data/lib/rigor/reflection/constant_path.rb +33 -7
  177. data/lib/rigor/reflection.rb +41 -10
  178. data/lib/rigor/scope/discovery_index.rb +258 -3
  179. data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
  180. data/lib/rigor/scope/resolution_chain.rb +1032 -0
  181. data/lib/rigor/scope.rb +480 -328
  182. data/lib/rigor/sig_gen/alias_index.rb +1 -1
  183. data/lib/rigor/sig_gen/classification.rb +4 -1
  184. data/lib/rigor/sig_gen/effect_annotation.rb +2 -2
  185. data/lib/rigor/sig_gen/generator.rb +77 -21
  186. data/lib/rigor/sig_gen/method_candidate.rb +2 -2
  187. data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
  188. data/lib/rigor/source/alias_names.rb +52 -0
  189. data/lib/rigor/source/literals.rb +3 -2
  190. data/lib/rigor/source/symbol_name.rb +20 -0
  191. data/lib/rigor/type/combinator.rb +59 -3
  192. data/lib/rigor/type/float_range.rb +4 -1
  193. data/lib/rigor/type/nominal.rb +6 -1
  194. data/lib/rigor/version.rb +1 -1
  195. data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
  196. data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
  197. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
  198. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
  199. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
  200. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
  201. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
  202. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
  203. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
  204. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
  205. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
  206. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
  207. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
  208. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
  209. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
  210. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
  211. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
  212. data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
  213. data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
  214. data/plugins/rigor-alba/sig/alba.rbs +4 -0
  215. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
  216. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
  217. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +13 -2
  218. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
  219. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
  220. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
  221. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
  222. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
  223. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
  224. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
  225. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
  226. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
  227. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
  228. data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
  229. data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
  230. data/sig/rigor/environment.rbs +2 -0
  231. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  232. data/sig/rigor/scope.rbs +69 -4
  233. data/sig/rigor/source.rbs +4 -0
  234. data/sig/rigor.rbs +3 -1
  235. data/skills/rigor-project-init/SKILL.md +37 -11
  236. data/skills/rigor-project-init/references/02-configure.md +7 -2
  237. metadata +90 -1
@@ -28,6 +28,7 @@ require_relative "fallback"
28
28
  require_relative "flow_tracer"
29
29
  require_relative "index_write_widening"
30
30
  require_relative "indexed_narrowing"
31
+ require_relative "key_presence_guard"
31
32
  require_relative "jump_targets"
32
33
  require_relative "define_method_block_self"
33
34
  require_relative "macro_block_self_type"
@@ -39,6 +40,7 @@ require_relative "optimistic_origin"
39
40
  require_relative "receiver_alias"
40
41
  require_relative "repeated_or_writes"
41
42
  require_relative "singleton_object_constant"
43
+ require_relative "refined_dispatch"
42
44
  require_relative "stored_block_call"
43
45
  require_relative "struct_fold_safety"
44
46
  require_relative "unknown_store_widening"
@@ -648,7 +650,12 @@ module Rigor
648
650
  # `StatementEvaluator` at class-body and method-body boundaries) or `Dynamic[Top]` at the top level.
649
651
  # Class-body `self` is `Singleton[<class>]`; instance-method `self` is `Nominal[<class>]`;
650
652
  # singleton-method `self` is `Singleton[<class>]`.
653
+ #
654
+ # Issue #1763 — directly in a `refine X` block body `self` is the refinement module, which Rigor does not
655
+ # model; its `Singleton[X]` stand-in serves implicit-self calls only, so a value read of `self` is untyped.
651
656
  def type_of_self_node(_node)
657
+ return dynamic_top if scope.refinement_block_self?
658
+
652
659
  scope.self_type || dynamic_top
653
660
  end
654
661
 
@@ -1811,7 +1818,8 @@ module Rigor
1811
1818
  # that normal return is `untyped` — the result's declared type — so `loop { e.next }` is not `bot` and
1812
1819
  # `loop { x = e.next; break x if x }` keeps its arm beside it.
1813
1820
  def call_dispatch_type_for(node, receiver_override: nil)
1814
- result = loop_completion_type(node, call_result_type_for(node, receiver_override: receiver_override))
1821
+ result = key_guarded_type(node, call_result_type_for(node, receiver_override: receiver_override))
1822
+ result = loop_completion_type(node, result)
1815
1823
  arms = call_break_arm_types(node, receiver_override: receiver_override)
1816
1824
  if exactly_once_block_never_completes?(node, receiver_override)
1817
1825
  return arms.empty? ? Type::Combinator.bot : Type::Combinator.union(*arms)
@@ -1821,6 +1829,15 @@ module Rigor
1821
1829
  widen_optimistic_predicate_constant(node, combined)
1822
1830
  end
1823
1831
 
1832
+ # Issue #1703 — `H[k]` under a true `H.key?(k)` with a non-literal key drops the miss `nil`
1833
+ # ({KeyPresenceGuard.guarded_read}). Only inside the guarded re-walk `call.possible-nil-receiver` runs to
1834
+ # decide whether to withhold a report; the analysis every other rule reads never records a guard.
1835
+ def key_guarded_type(node, type)
1836
+ return type unless node.name == :[]
1837
+
1838
+ KeyPresenceGuard.guarded_read(node, type, scope) || type
1839
+ end
1840
+
1824
1841
  # Issue #1172 — the nil-collapsing predicates (`nil?`, `!`, `x == nil`, …) answer a `Constant` that
1825
1842
  # encodes the receiver's nil-freeness. When that nil-freeness is only *optimistic* — an
1826
1843
  # `%a{implicitly-returns-nil}` read `RbsDispatch` deliberately reads past — the constant is a bet,
@@ -1896,6 +1913,9 @@ module Rigor
1896
1913
  # asks nothing further of dispatch. Off (the default) this is one integer read.
1897
1914
  Effects::Collector.record_call(node, receiver, scope) if Effects::Collector.active?
1898
1915
 
1916
+ refined = try_refined_dispatch(node, receiver, arg_types, block_type)
1917
+ return refined if refined
1918
+
1899
1919
  literal_send = try_literal_send(node, receiver)
1900
1920
  return literal_send if literal_send
1901
1921
 
@@ -1927,6 +1947,155 @@ module Rigor
1927
1947
  dispatch_miss_result(node, receiver, arg_types, block_type)
1928
1948
  end
1929
1949
 
1950
+ # ADR-121 WD2 (issue #1664) — a call through an in-effect Ruby refinement, typed from the winning refine body
1951
+ # with the call's receiver as `self`, ahead of every tier below: each answers from the method the refinement
1952
+ # replaces (a refined `String#upcase` must not fold, a refined `map` must not fold per element, a top-level
1953
+ # `def` must not bind ahead of a refinement of `Object`, a plugin models the class's own method). An unreadable
1954
+ # body, or a list that may hold any refinement, answers `Dynamic[top]`, never the replaced method's signature.
1955
+ # A union receiver is decided per member; a `Dynamic` receiver, a class object and a call no refinement wins
1956
+ # fall through. A call site no refinement is in effect at answers nil at once and records nothing: its answer
1957
+ # cannot change without an edit to its own file.
1958
+ #
1959
+ # ADR-121 WD7 (A2) — inside the span of an activation that puts an opaque module in effect
1960
+ # ({InEffectRefinements.opaque_module?}), any method of any instance may be replaced, so every instance-receiver
1961
+ # call answers `Dynamic[top]`; a class object falls through, as it does below.
1962
+ def try_refined_dispatch(node, receiver, arg_types, block_type)
1963
+ list = scope.in_effect_refinements(node)
1964
+ return nil if list.empty?
1965
+
1966
+ InEffectRefinements.record_refinement_names(node.name)
1967
+ return dynamic_top if refined_instance_receiver?(receiver) && opaque_refinement_span?(node, list)
1968
+ return nil if scope.discovered_refinements.empty?
1969
+
1970
+ refined_indirect_result(node, receiver, list) ||
1971
+ refined_receiver_result(node, receiver, list, arg_types, block_type)
1972
+ end
1973
+
1974
+ # ADR-121 WD4 — `send` / `__send__` / `public_send` with a literal name, `respond_to?` and `method` /
1975
+ # `public_method` with one, and a `&:name` block argument honour refinements too (CRuby `test_refinement.rb`;
1976
+ # probed on Ruby 4.0.5). When the name they reach is refined in effect for the receiver they answer
1977
+ # `Dynamic[top]`: typing them through the arm is deferred, and `respond_to?` must never fold to `false`. A
1978
+ # `&:name` block's receiver is the yielded element, which is not known here, so any in-effect refinement of the
1979
+ # name answers. Only `methods` ignores refinements and keeps its answer.
1980
+ def refined_indirect_result(node, receiver, list)
1981
+ name = REFINED_NAMING_CALLS.include?(node.name) ? literal_method_name_argument(node) : nil
1982
+ return dynamic_top if name && refined_name_for?(receiver, name, list)
1983
+
1984
+ block_name = symbol_block_argument(node)
1985
+ dynamic_top if block_name && refined_anywhere?(block_name, list)
1986
+ end
1987
+
1988
+ REFINED_NAMING_CALLS = Set[:send, :__send__, :public_send, :respond_to?, :method, :public_method].freeze
1989
+ private_constant :REFINED_NAMING_CALLS
1990
+
1991
+ def literal_method_name_argument(node)
1992
+ argument = node.arguments&.arguments&.first
1993
+ name = argument.unescaped if argument.is_a?(Prism::SymbolNode) || argument.is_a?(Prism::StringNode)
1994
+ return nil if name.nil?
1995
+
1996
+ name = name.to_sym
1997
+ InEffectRefinements.record_refinement_names(name)
1998
+ name
1999
+ end
2000
+
2001
+ def symbol_block_argument(node)
2002
+ block = node.block
2003
+ return nil unless block.is_a?(Prism::BlockArgumentNode) && block.expression.is_a?(Prism::SymbolNode)
2004
+
2005
+ name = block.expression.unescaped.to_sym
2006
+ InEffectRefinements.record_refinement_names(name)
2007
+ name
2008
+ end
2009
+
2010
+ def refined_name_for?(receiver, name, list)
2011
+ members = receiver.is_a?(Type::Union) ? receiver.members : [receiver]
2012
+ members.any? do |member|
2013
+ class_name = refined_receiver_class_name(member)
2014
+ !class_name.nil? && !RefinedDispatch.winner(scope, class_name, name, list).nil?
2015
+ end
2016
+ end
2017
+
2018
+ # ADR-121 WD7 — a names-wildcard row counts as a refinement of every name.
2019
+ def refined_anywhere?(name, list)
2020
+ refinements = scope.discovered_refinements
2021
+ wildcard = Scope::DiscoveryIndex::REFINEMENT_WILDCARD
2022
+ return true if list.include?(InEffectRefinements::UNKNOWN) &&
2023
+ refinements.any? { |_refined, methods| methods.key?(name) || methods.key?(wildcard) }
2024
+
2025
+ !RefinedDispatch.targets(refinements, name, list).nil? ||
2026
+ !RefinedDispatch.targets(refinements, wildcard, list).nil?
2027
+ end
2028
+
2029
+ # ADR-121 WD7 — does the call's lexical list (a plugin-declared module is the plugin's declaration and never
2030
+ # opaque) hold an opaque module? `list` is the full list, which is the lexical one when nothing is declared.
2031
+ def opaque_refinement_span?(node, list)
2032
+ lexical = scope.declared_refinements.empty? ? list : scope.in_effect_refinements(node, InEffectRefinements::EMPTY)
2033
+ InEffectRefinements.opaque_in?(scope, lexical)
2034
+ end
2035
+
2036
+ # Does `receiver` (or a member of a union) have a class the refined arm answers for: an instance, not a class
2037
+ # object or `Dynamic`?
2038
+ def refined_instance_receiver?(receiver)
2039
+ members = receiver.is_a?(Type::Union) ? receiver.members : [receiver]
2040
+ members.any? { |member| !refined_receiver_class_name(member).nil? }
2041
+ end
2042
+
2043
+ def refined_receiver_result(node, receiver, list, arg_types, block_type)
2044
+ return refined_member_result(node, receiver, list, arg_types, block_type) unless receiver.is_a?(Type::Union)
2045
+
2046
+ answers = receiver.members.map { |member| refined_member_result(node, member, list, arg_types, block_type) }
2047
+ return nil if answers.none?
2048
+
2049
+ members = receiver.members.each_with_index.map do |member, index|
2050
+ answers[index] || call_result_type_for(node, receiver_override: member)
2051
+ end
2052
+ Type::Combinator.union(*members)
2053
+ end
2054
+
2055
+ def refined_member_result(node, receiver, list, arg_types, block_type)
2056
+ class_name = refined_receiver_class_name(receiver)
2057
+ return nil if class_name.nil?
2058
+
2059
+ winner = RefinedDispatch.winner(scope, class_name, node.name, list)
2060
+ return nil if winner.nil?
2061
+ return dynamic_top if winner == RefinedDispatch::UNKNOWN
2062
+
2063
+ refined_body_return(winner, node.name, Type::Combinator.nominal_of(class_name), arg_types, block_type) ||
2064
+ dynamic_top
2065
+ end
2066
+
2067
+ # The winning refine body's inferred return, re-typed with the receiver's class as `self` and the call's
2068
+ # argument types bound as an undeclared method's are; nil when the body is not readable here.
2069
+ #
2070
+ # A body another file wrote is re-typed with that file's in-effect refinements at the `def` (its own module
2071
+ # among them), so a sibling override it calls answers from its refine body too.
2072
+ def refined_body_return(winner, method_name, self_type, arg_types, block_type)
2073
+ def_node, foreign =
2074
+ InEffectRefinements.refinement_def_with_query(scope, winner.module_name, winner.refined_class, method_name)
2075
+ return nil if def_node.nil?
2076
+
2077
+ refinements = foreign&.at(def_node.location.start_offset) do |name|
2078
+ InEffectRefinements.activated_modules(scope, name)
2079
+ end
2080
+ infer_user_method_return(def_node, self_type, arg_types, yield_type: block_type, refinements: refinements)
2081
+ rescue StandardError
2082
+ nil
2083
+ end
2084
+
2085
+ # The class an instance-side receiver is an instance of, or nil for one a refinement does not answer here: a
2086
+ # class object (refinements of singleton classes are not recorded), `Dynamic`, `top`, or a shape with no class.
2087
+ def refined_receiver_class_name(receiver)
2088
+ case receiver
2089
+ when Type::Nominal, Type::StructInstance, Type::DataInstance then receiver.class_name
2090
+ when Type::Constant then receiver.value.class.name
2091
+ when Type::Tuple then "Array"
2092
+ when Type::HashShape then "Hash"
2093
+ when Type::IntegerRange then "Integer"
2094
+ when Type::FloatRange then "Float"
2095
+ when Type::Refined, Type::Difference then refined_receiver_class_name(receiver.base)
2096
+ end
2097
+ end
2098
+
1930
2099
  # The post-dispatch tiers for a call `MethodDispatcher` could not answer, in their historical
1931
2100
  # order; extracted from {#call_dispatch_type_for} whole.
1932
2101
  def dispatch_miss_result(node, receiver, arg_types, block_type = nil)
@@ -2280,8 +2449,8 @@ module Rigor
2280
2449
  def try_user_method_inference(receiver, call_node, arg_types, method_name: call_node.name, block_type: nil)
2281
2450
  return nil unless user_inference_receiver?(receiver)
2282
2451
 
2283
- def_node, owner = resolve_user_def_with_owner(receiver.class_name, method_name)
2284
- return nil if def_node.nil?
2452
+ def_node, owner, raising = resolve_typing_def_with_owner(receiver.class_name, method_name)
2453
+ return every_definer_raises_type(raising, receiver, call_node, arg_types, block_type) if def_node.nil?
2285
2454
 
2286
2455
  result = infer_user_method_return(def_node, receiver, arg_types,
2287
2456
  self_fold_safe: fold_safe_call_receiver?(call_node, receiver),
@@ -2383,8 +2552,7 @@ module Rigor
2383
2552
  key = method_name.to_sym
2384
2553
  return table[key] if table.key?(key)
2385
2554
 
2386
- table[key] = scope.singleton_def_through_ancestors(class_name, method_name,
2387
- name_memo: class_graph_buckets[:name])
2555
+ table[key] = scope.singleton_def_through_ancestors(class_name, method_name)
2388
2556
  end
2389
2557
 
2390
2558
  # The project-side singleton-method band: a `Foo.bar` call resolved against a `class << …` / `def self.…`
@@ -2415,13 +2583,12 @@ module Rigor
2415
2583
  nil
2416
2584
  end
2417
2585
 
2418
- # ADR-24 slice 2 — resolves `method_name` against `class_name`'s own `def`s, then walks the user-class
2419
- # ancestor chain: included / prepended modules (transitive) and the superclass chain. RBS-known
2420
- # ancestors are NOT walked here — the `MethodDispatcher` RBS tier runs before
2421
- # `try_user_method_inference` and already covers them; an ancestor name that resolves to no
2422
- # project-discovered class/module ends that branch. Cross-file: the chain is followed through
2423
- # `Scope#discovered_superclasses` / `#discovered_includes` / `#discovered_def_nodes`, which the runner
2424
- # seeds from the project-wide pre-pass. The walk is breadth-first, cycle-guarded, and node-count-capped.
2586
+ # ADR-24 slice 2 — resolves `method_name` against the project definers on `class_name`'s
2587
+ # {Scope::ResolutionChain}, in Ruby's order: prepended modules, the class, its included modules, then the
2588
+ # superclass's chain likewise. RBS-known ancestors are NOT answered here — the `MethodDispatcher` RBS
2589
+ # tier runs before `try_user_method_inference` and already covers them; they sit on the chain as
2590
+ # external entries this lookup passes over. Cross-file: the chain is built from the tables the runner
2591
+ # seeds from the project-wide pre-pass, cycle-guarded and capped at `Scope::ANCESTOR_WALK_LIMIT`.
2425
2592
 
2426
2593
  CLASS_GRAPH_CACHE_KEY = :__rigor_class_graph_cache__
2427
2594
  private_constant :CLASS_GRAPH_CACHE_KEY
@@ -2464,13 +2631,114 @@ module Rigor
2464
2631
  # is a pure function of the same frozen index — the sibling resolver it walks reads nothing else.
2465
2632
  # `singleton_def` is added lazily by {#singleton_def_through_ancestors}'s caller.
2466
2633
  slot = [discovery,
2467
- { name: {}, user_def: {}, self_pure: {}.compare_by_identity,
2468
- yields: {}.compare_by_identity }]
2634
+ { user_def: {}, typing_def: {}, self_pure: {}.compare_by_identity, yields: {}.compare_by_identity }]
2469
2635
  Thread.current[CLASS_GRAPH_CACHE_KEY] = slot
2470
2636
  end
2471
2637
  slot[1]
2472
2638
  end
2473
2639
 
2640
+ # ADR-119 C2-b1 — the TYPING read of {#resolve_user_def_with_owner}: the same `[def_node, owner]` pair, but
2641
+ # answered through {DefinerResolution} so a chain that does not stand for the name (a conditional definer,
2642
+ # a conditional include, a fork, an external ancestor that may answer first) is `[nil, nil]` and the call
2643
+ # types `Dynamic` instead of from a definer Ruby may not reach. ABSENT is the same `[nil, nil]`: at a typing
2644
+ # site "no definer" and "cannot say" both fall through to `dispatch_miss_result`. Existence reads
2645
+ # ({#resolve_user_def_through_ancestors}, the self-purity scan) stay on the union memo above: an existence
2646
+ # read turning false there could ADD a `call.undefined-method` firing.
2647
+ #
2648
+ # UNKNOWN carries a third element, the `bot` exception's definers ({#every_project_definer}): the def nodes
2649
+ # of every project entry on the chain that defines the name, or nil where the exception cannot apply.
2650
+ def resolve_typing_def_with_owner(class_name, method_name)
2651
+ cache = class_graph_buckets[:typing_def]
2652
+ table = (cache[class_name.to_s] ||= {})
2653
+ key = method_name.to_sym
2654
+ return table[key] if table.key?(key)
2655
+
2656
+ table[key] =
2657
+ case DefinerResolution.resolve(scope, class_name, method_name, :instance, question: :definer)
2658
+ in DefinerResolution::Known(answer: [node, owner])
2659
+ [node, owner]
2660
+ in DefinerResolution::UNKNOWN
2661
+ [nil, nil, every_project_definer(class_name, method_name)]
2662
+ in DefinerResolution::ABSENT
2663
+ [nil, nil]
2664
+ end
2665
+ end
2666
+
2667
+ # ADR-119 C2-b1 errata (the `bot` exception) — an UNKNOWN chain does not say WHICH definer Ruby calls, but
2668
+ # when every project definer of the name on the chain types `bot` (each raises or exits), the call cannot
2669
+ # return whichever of them runs, so it types `bot` and a guard such as `fail_with 'no' unless count` still
2670
+ # narrows. `defs` is {#every_project_definer}'s list; each is inferred with this call's receiver and
2671
+ # arguments, stopping at the first that returns (the answer is then `Dynamic` whatever the rest say).
2672
+ # Accepted cost: an RBS-unknown external ahead of them that really defines the name and returns is a
2673
+ # narrowing false negative; `bot` reports nothing about a value, and that exposure is master's.
2674
+ def every_definer_raises_type(defs, receiver, call_node, arg_types, block_type)
2675
+ return nil if defs.nil?
2676
+
2677
+ self_fold_safe = fold_safe_call_receiver?(call_node, receiver)
2678
+ defs.each do |def_node|
2679
+ result = infer_user_method_return(def_node, receiver, arg_types,
2680
+ self_fold_safe: self_fold_safe, yield_type: block_type)
2681
+ return nil unless result.is_a?(Type::Bot)
2682
+ end
2683
+ Type::Combinator.bot
2684
+ end
2685
+
2686
+ # The def node of every project entry on `class_name`'s instance chain that defines `method_name`, in any
2687
+ # position (the order is what UNKNOWN does not know), or nil where the `bot` exception cannot apply: a cut
2688
+ # chain, no project definer, an entry whose only record of the name has no body (an `attr_*` or a
2689
+ # `define_method` with no `def`), a definer resting on a `possible` or contested fact (another body may be the
2690
+ # one Ruby holds), or an external RBS knows that declares the name. An external RBS does not know is passed
2691
+ # over. Only what the tables hold is read: a `define_method` beside an entry's own `def` of the name, and a
2692
+ # receiver-form `X.include(M)` outside `X`'s body, are not seen (as on the Known path).
2693
+ #
2694
+ # ADR-46: the answer depends on every entry, so each files its class edge and its negative class edge (a new
2695
+ # file reopening it may add a definer or a mixin), and each project entry's {Scope#user_def_for} files the
2696
+ # method edge, positive for a definer and `read_missing(:method, "Owner#name")` for one that lacks the
2697
+ # `def`. `DefinerResolution`'s decline filed none of this past the external that stopped it.
2698
+ def every_project_definer(class_name, method_name)
2699
+ chain = Scope::ResolutionChain.for(scope, class_name.to_s, :instance, :methods)
2700
+ chain.record(scope)
2701
+ return nil if chain.truncated?
2702
+
2703
+ defs = []
2704
+ declined = false
2705
+ chain.entries.uniq.each do |entry|
2706
+ if entry.external?
2707
+ note_missing_class(entry.raw.to_s.split("::").last)
2708
+ declined = true if external_declares?(entry.candidates, method_name)
2709
+ next
2710
+ end
2711
+ note_missing_class(entry.last_segment)
2712
+ node = scope.user_def_for(entry.name, method_name)
2713
+ defs << node if node
2714
+ declined = true if definer_without_single_body?(entry.name, method_name, node)
2715
+ end
2716
+ declined || defs.empty? ? nil : defs.freeze
2717
+ end
2718
+
2719
+ def note_missing_class(segment)
2720
+ Analysis::DependencyRecorder.read_missing(:class, segment) if Analysis::DependencyRecorder.active?
2721
+ end
2722
+
2723
+ def definer_without_single_body?(owner, method_name, node)
2724
+ discovery = scope.discovery
2725
+ key = [owner, method_name.to_sym]
2726
+ (node.nil? && scope.discovered_method?(owner, method_name, :instance)) ||
2727
+ discovery.possible_method?(owner, method_name, :instance) ||
2728
+ discovery.contested?(:discovered_def_nodes, key)
2729
+ end
2730
+
2731
+ # True where RBS knows one of the names the external's spelling can denote and declares the instance method
2732
+ # in it; a lookup that fails counts as declaring (the exception then does not apply).
2733
+ def external_declares?(candidates, method_name)
2734
+ known = candidates.find { |candidate| Rigor::Reflection.rbs_class_known?(candidate, scope: scope) }
2735
+ return false if known.nil?
2736
+
2737
+ !Rigor::Reflection.instance_method_definition(known, method_name, scope: scope).nil?
2738
+ rescue StandardError
2739
+ true
2740
+ end
2741
+
2474
2742
  def resolve_user_def_through_ancestors(class_name, method_name)
2475
2743
  resolve_user_def_with_owner(class_name, method_name).first
2476
2744
  end
@@ -2493,11 +2761,10 @@ module Rigor
2493
2761
  # nothing but the frozen discovery index, and the `.with` guard in
2494
2762
  # {MethodDispatcher::StructMaterialization} needs the SAME answer without threading dispatcher state
2495
2763
  # through the dispatcher (#598 review). What stays here is the caching: the run-scoped
2496
- # `(class_name, method_name)` memo above, plus the per-edge name bucket handed to the walk so a class
2497
- # whose many methods are resolved pays each ancestor edge once.
2764
+ # `(class_name, method_name)` memo above. The chain itself is memoised per class by
2765
+ # {Scope::ResolutionChain}, so a class whose many methods are resolved builds it once.
2498
2766
  def compute_user_def_with_owner(class_name, method_name)
2499
- scope.user_def_through_ancestors(class_name, method_name,
2500
- name_memo: class_graph_buckets[:name])
2767
+ scope.user_def_through_ancestors(class_name, method_name)
2501
2768
  end
2502
2769
 
2503
2770
  # ADR-57 N5 — overridable-method adoption gate. A self-call resolved to a project `def` whose owner has
@@ -2671,34 +2938,14 @@ module Rigor
2671
2938
  index
2672
2939
  end
2673
2940
 
2674
- # True when `candidate`'s transitive ancestor chain (superclasses + included/prepended modules) reaches
2675
- # `owner` — i.e. `candidate` is a subclass of an owner class or an includer of an owner module. Reuses
2676
- # the same BFS resolver the method-resolution ancestor walk uses, so name resolution (lexical nesting,
2677
- # RBS-known-ancestor pruning) is identical.
2678
- # Delegates to the shared walk's edge step so this BFS and method resolution resolve ancestor names
2679
- # identically, sharing the per-edge memo bucket.
2680
- def enqueue_ancestors(current, queue)
2681
- scope.enqueue_ancestors(current, queue, class_graph_buckets[:name])
2682
- end
2683
-
2941
+ # True when `candidate`'s ancestor chain (superclasses + included/prepended modules) reaches `owner` —
2942
+ # i.e. `candidate` is a subclass of an owner class or an includer of an owner module. Asked of the same
2943
+ # {Scope::ResolutionChain} method resolution reads, so name resolution (lexical nesting, RBS-known-ancestor
2944
+ # pruning) is identical; a chain cut at the budget that does not reach `owner` answers false, as the walk
2945
+ # it replaced did. Reachability is the same in both of the chain's worlds.
2684
2946
  def related_to_owner?(candidate, owner)
2685
- queue = []
2686
- enqueue_ancestors(candidate, queue)
2687
- seen = {}
2688
- visited = 0
2689
- until queue.empty?
2690
- current = queue.shift
2691
- next if current.nil? || seen[current]
2692
-
2693
- return true if current == owner
2694
-
2695
- seen[current] = true
2696
- visited += 1
2697
- return false if visited > Scope::ANCESTOR_WALK_LIMIT
2698
-
2699
- enqueue_ancestors(current, queue)
2700
- end
2701
- false
2947
+ chain = Scope::ResolutionChain.for(scope, candidate.to_s, :instance, :methods)
2948
+ !chain.search(scope) { |entry| entry.name == owner }.nil?
2702
2949
  end
2703
2950
 
2704
2951
  INFERENCE_GUARD_KEY = :__rigor_user_method_inference_stack__
@@ -2881,14 +3128,37 @@ module Rigor
2881
3128
  # not observable on the body scope, so it does need its own memo-key slot. It is dropped for a def
2882
3129
  # that cannot reach a `yield` — nearly all of them — which keeps the key shape constant for the
2883
3130
  # methods the memo actually carries and confines the extra dimension to yielding callees.
2884
- def infer_user_method_return(def_node, receiver, arg_types, self_fold_safe: false, yield_type: nil)
3131
+ # Issue #1703 — inside the guarded re-walk a callee's return summary is still computed with `key?` guards off
3132
+ # ({KeyPresenceGuard.without_guards}): summaries are memoised for the whole run, and one computed with a guard
3133
+ # would reach the analysis every rule reads.
3134
+ #
3135
+ # Issue #1664 — `refinements` are the modules in effect in a refine body another file wrote, which the body scope
3136
+ # carries as declared refinements: its nodes are not this file's, so this file's lexical list cannot answer them.
3137
+ # The return memo and the recursion guard do not key on them, which holds because such a node comes only from
3138
+ # `DefNodeResolver.refinement_query`'s own parse and always carries that file's one list; a caller that types
3139
+ # the same node under another list must add it to the key.
3140
+ def infer_user_method_return(def_node, receiver, arg_types, self_fold_safe: false, yield_type: nil,
3141
+ refinements: nil)
2885
3142
  return nil if def_node.body.nil?
3143
+ unless KeyPresenceGuard.active?
3144
+ return infer_user_method_return_unguarded(def_node, receiver, arg_types, self_fold_safe, yield_type,
3145
+ refinements)
3146
+ end
2886
3147
 
3148
+ KeyPresenceGuard.without_guards do
3149
+ infer_user_method_return_unguarded(def_node, receiver, arg_types, self_fold_safe, yield_type, refinements)
3150
+ end
3151
+ end
3152
+
3153
+ def infer_user_method_return_unguarded(def_node, receiver, arg_types, self_fold_safe, yield_type,
3154
+ refinements = nil)
2887
3155
  yield_type = nil unless yield_type && body_yields?(def_node)
2888
3156
  body_scope = build_user_method_body_scope(def_node, receiver, arg_types,
2889
3157
  self_fold_safe: self_fold_safe)
2890
3158
  return nil if body_scope.nil?
2891
3159
 
3160
+ body_scope = body_scope.with_declared_refinements(refinements) if refinements
3161
+
2892
3162
  # Recursion-guard signature. Keyed on `(receiver, method)` only — NOT the argument types. ADR-24 WD5:
2893
3163
  # a method whose summary is still being computed resolves to `Dynamic[top]` for that cycle. Keying on
2894
3164
  # arg types would let mutual recursion through a `module_function` module (`Acceptance#accepts` →
@@ -3860,15 +4130,28 @@ module Rigor
3860
4130
  # Both are precision-only. A `...` outside a forwarding frame (unreachable in valid Ruby) and a double
3861
4131
  # splat of anything but a Symbol-keyed closed `HashShape` — a shapeless `Hash[Symbol, V]`, an opaque
3862
4132
  # value, a mixed hash — fall back to exactly the pre-#1125 answer.
4133
+ #
4134
+ # Only a list holding a `...` takes the flat_map. Every call is typed through here, and the flat_map's
4135
+ # one-element Array per argument cost `rigor check` about 315K allocations over the v0.3.9 `lib`
4136
+ # ([#1503](https://github.com/rigortype/rigor/issues/1503)).
3863
4137
  def call_arg_types(node)
3864
4138
  arguments_node = node.arguments
3865
4139
  return [] if arguments_node.nil?
3866
4140
 
3867
- arguments_node.arguments.flat_map do |argument|
4141
+ arguments = arguments_node.arguments
4142
+ return arguments.map { |argument| call_arg_type(argument) } unless forwards_arguments?(arguments)
4143
+
4144
+ arguments.flat_map do |argument|
3868
4145
  forwarded_argument_types(argument) || [call_arg_type(argument)]
3869
4146
  end
3870
4147
  end
3871
4148
 
4149
+ # Whether a call's argument list holds `...`. Ruby accepts it only last; the scan checks every argument
4150
+ # rather than rely on that.
4151
+ def forwards_arguments?(arguments)
4152
+ arguments.any?(Prism::ForwardingArgumentsNode)
4153
+ end
4154
+
3872
4155
  # The frame's argument list when `argument` is `...`, else nil so the caller types it normally.
3873
4156
  def forwarded_argument_types(argument)
3874
4157
  return nil unless argument.is_a?(Prism::ForwardingArgumentsNode)
@@ -3930,12 +4213,13 @@ module Rigor
3930
4213
  method_name: call_node.name,
3931
4214
  arg_types: arg_types,
3932
4215
  environment: scope.environment,
3933
- scope: scope
4216
+ scope: scope,
4217
+ call_node: call_node
3934
4218
  )
3935
4219
  block_return_for(
3936
4220
  block_arg, expected,
3937
4221
  call_node: call_node,
3938
- narrowed_self_type: block_body_self_narrowing(call_node, receiver_type),
4222
+ narrowing: block_body_self_narrowing(call_node, receiver_type),
3939
4223
  repeats: !BLOCK_VALUE_DISCARDING.include?(call_node.name) && block_may_repeat?(call_node, receiver_type)
3940
4224
  )
3941
4225
  rescue StandardError
@@ -4002,7 +4286,7 @@ module Rigor
4002
4286
  block_scope = block_entry_scope(
4003
4287
  block_node, param_types,
4004
4288
  call_node: call_node,
4005
- narrowed_self_type: block_body_self_narrowing(call_node, receiver),
4289
+ narrowing: block_body_self_narrowing(call_node, receiver),
4006
4290
  captured: repeating_captured_bindings(block_node, param_types, block_may_repeat?(call_node, receiver))
4007
4291
  )
4008
4292
  _result, collected = StatementEvaluator.with_break_value_sink do
@@ -4011,10 +4295,12 @@ module Rigor
4011
4295
  collected.filter_map { |jump, type| type if targets.key?(jump) }
4012
4296
  end
4013
4297
 
4014
- # The block body's narrowed `self_type`, or `nil` to leave the scope contract unchanged.
4298
+ # The block body's narrowing — a {MacroBlockSelfType::Match} carrying the narrowed `self_type` and the declared
4299
+ # refinements — or `nil` to leave the scope contract unchanged.
4015
4300
  #
4016
4301
  # ADR-16 Tier A: a registered plugin's `block_as_methods` entry matching `(receiver_type,
4017
- # call_node.name)` narrows to the receiver class's instance type.
4302
+ # call_node.name)` narrows to the receiver class's instance type, and puts its `refinements:` in effect in the
4303
+ # body (issue #1667).
4018
4304
  #
4019
4305
  # Issue #963: `define_method(:name) { ... }` installs its block as an instance method and runs it with
4020
4306
  # `self` bound to the receiving instance, so the block body's `self` is the INSTANCE side of a class body's
@@ -4024,9 +4310,11 @@ module Rigor
4024
4310
  # matters wherever the block's value is observable, e.g. a project-declared generic `define_method`
4025
4311
  # signature that returns the block's own type.
4026
4312
  def block_body_self_narrowing(call_node, receiver_type)
4027
- MacroBlockSelfType.narrow_self_type_for(
4028
- scope: scope, call_node: call_node, receiver_type: receiver_type
4029
- ) || DefineMethodBlockSelf.narrow_self_type_for(scope: scope, call_node: call_node)
4313
+ match = MacroBlockSelfType.match_for(scope: scope, call_node: call_node, receiver_type: receiver_type)
4314
+ return match if match
4315
+
4316
+ defined = DefineMethodBlockSelf.narrow_self_type_for(scope: scope, call_node: call_node)
4317
+ defined && MacroBlockSelfType::Match.new(self_type: defined, refinements: InEffectRefinements::EMPTY)
4030
4318
  end
4031
4319
 
4032
4320
  def break_arm_param_types(call_node, receiver)
@@ -4035,16 +4323,17 @@ module Rigor
4035
4323
  method_name: call_node.name,
4036
4324
  arg_types: call_arg_types(call_node),
4037
4325
  environment: scope.environment,
4038
- scope: scope
4326
+ scope: scope,
4327
+ call_node: call_node
4039
4328
  )
4040
4329
  end
4041
4330
 
4042
- def block_return_for(block_arg, expected, call_node: nil, narrowed_self_type: nil, repeats: false)
4331
+ def block_return_for(block_arg, expected, call_node: nil, narrowing: nil, repeats: false)
4043
4332
  case block_arg
4044
4333
  when Prism::BlockNode
4045
4334
  captured = repeating_captured_bindings(block_arg, expected, repeats)
4046
4335
  entry = block_entry_scope(
4047
- block_arg, expected, call_node: call_node, narrowed_self_type: narrowed_self_type, captured: captured
4336
+ block_arg, expected, call_node: call_node, narrowing: narrowing, captured: captured
4048
4337
  )
4049
4338
  type_block_body(block_arg, entry, captured: captured)
4050
4339
  when Prism::BlockArgumentNode
@@ -4060,13 +4349,14 @@ module Rigor
4060
4349
  # Issue #316 — mirrors `StatementEvaluator#build_block_entry_scope`: the block body's `self` is the
4061
4350
  # yielding method's business, so the return-typing pass must see the same unmodelled-self mark. Issue #1358
4062
4351
  # — and the same match-global view ({MatchRebinding.block_entry}), which reads the owning `call_node`.
4063
- def block_entry_scope(block_node, expected, call_node: nil, narrowed_self_type: nil, captured: nil)
4352
+ def block_entry_scope(block_node, expected, call_node: nil, narrowing: nil, captured: nil)
4064
4353
  entry = MatchRebinding.block_entry(scope.entering_opaque_block, block_node, call_node)
4065
4354
  entry = captured.lay(entry) if captured
4066
4355
  block_scope = BlockParameterBinder.new(expected_param_types: expected).bind_onto(block_node, entry)
4067
- return block_scope unless narrowed_self_type
4356
+ return block_scope unless narrowing
4068
4357
 
4069
- block_scope.with_self_type(narrowed_self_type)
4358
+ keeps_unknown = call_node ? scope.block_self_narrowing_unknown?(call_node) : false
4359
+ MacroBlockSelfType.apply(block_scope, narrowing, keeps_unknown: keeps_unknown)
4070
4360
  end
4071
4361
 
4072
4362
  # The #587 (b) captured binding when the call may run the block more than once ({#block_may_repeat?}), and