rigortype 0.4.1 → 0.4.3

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 (230) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  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/11-sig-gen.md +18 -5
  25. data/docs/handbook/appendix-liskov.md +5 -0
  26. data/docs/manual/02-cli-reference.md +19 -1
  27. data/docs/manual/03-configuration.md +7 -5
  28. data/docs/manual/04-diagnostics.md +2 -1
  29. data/docs/manual/05-inspecting-types.md +50 -0
  30. data/docs/manual/06-baseline.md +2 -0
  31. data/docs/manual/07-plugins.md +1 -1
  32. data/docs/manual/12-caching.md +18 -1
  33. data/docs/manual/14-rails-quickstart.md +2 -2
  34. data/docs/manual/18-removing-dead-code.md +41 -4
  35. data/docs/manual/plugins/README.md +6 -1
  36. data/docs/manual/plugins/rigor-activerecord.md +21 -0
  37. data/docs/manual/plugins/rigor-alba.md +60 -0
  38. data/docs/manual/plugins/rigor-minitest.md +22 -4
  39. data/docs/manual/plugins/rigor-typelizer.md +71 -0
  40. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
  41. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
  42. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
  43. data/lib/rigor/analysis/check_rules/rule_ids.rb +11 -0
  44. data/lib/rigor/analysis/check_rules/source_arity.rb +21 -2
  45. data/lib/rigor/analysis/check_rules.rb +292 -59
  46. data/lib/rigor/analysis/dependency_recorder.rb +15 -0
  47. data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
  48. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
  49. data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
  50. data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
  51. data/lib/rigor/analysis/effects_cache_probe.rb +1 -1
  52. data/lib/rigor/analysis/erb_template_detector.rb +2 -1
  53. data/lib/rigor/analysis/incremental.rb +29 -9
  54. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +30 -5
  55. data/lib/rigor/analysis/incremental_run_slot.rb +6 -4
  56. data/lib/rigor/analysis/incremental_session.rb +103 -23
  57. data/lib/rigor/analysis/path_expansion.rb +62 -6
  58. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +124 -37
  59. data/lib/rigor/analysis/reachability/graph.rb +241 -37
  60. data/lib/rigor/analysis/reachability/scan.rb +490 -77
  61. data/lib/rigor/analysis/reachability/scan_cache.rb +4 -2
  62. data/lib/rigor/analysis/rule_catalog.rb +43 -0
  63. data/lib/rigor/analysis/run_cache_key.rb +16 -1
  64. data/lib/rigor/analysis/run_cache_probe.rb +1 -1
  65. data/lib/rigor/analysis/run_stats.rb +28 -6
  66. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +113 -6
  67. data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -19
  68. data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -1
  69. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  70. data/lib/rigor/analysis/runner.rb +62 -13
  71. data/lib/rigor/analysis/worker_session.rb +8 -5
  72. data/lib/rigor/cache/engine_source.rb +21 -7
  73. data/lib/rigor/cache/incremental_snapshot.rb +55 -5
  74. data/lib/rigor/cli/annotate_command.rb +5 -0
  75. data/lib/rigor/cli/baseline_command.rb +7 -18
  76. data/lib/rigor/cli/check_command.rb +140 -40
  77. data/lib/rigor/cli/docs_command.rb +5 -4
  78. data/lib/rigor/cli/lens_command.rb +144 -0
  79. data/lib/rigor/cli/lsp_command.rb +5 -2
  80. data/lib/rigor/cli/skill_command.rb +3 -3
  81. data/lib/rigor/cli/skill_describe.rb +1 -1
  82. data/lib/rigor/cli/trace_command.rb +5 -0
  83. data/lib/rigor/cli/type_of_command.rb +6 -3
  84. data/lib/rigor/cli/unused_command.rb +166 -27
  85. data/lib/rigor/cli.rb +39 -5
  86. data/lib/rigor/configuration/severity_profile.rb +6 -0
  87. data/lib/rigor/configuration.rb +61 -2
  88. data/lib/rigor/environment/bundle_sig_discovery.rb +11 -7
  89. data/lib/rigor/environment/gated_signature_guard.rb +43 -0
  90. data/lib/rigor/environment/gated_signature_plan.rb +339 -0
  91. data/lib/rigor/environment/installed_gem_set.rb +38 -11
  92. data/lib/rigor/environment/lockfile_resolver.rb +37 -6
  93. data/lib/rigor/environment/missing_gem_constant_index.rb +22 -7
  94. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  95. data/lib/rigor/environment/rbs_loader.rb +316 -14
  96. data/lib/rigor/environment/required_features.rb +208 -0
  97. data/lib/rigor/environment.rb +33 -8
  98. data/lib/rigor/inference/acceptance.rb +28 -3
  99. data/lib/rigor/inference/builtins/cruby_definers.rb +78 -0
  100. data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
  101. data/lib/rigor/inference/captured_locals.rb +43 -3
  102. data/lib/rigor/inference/def_node_resolver.rb +23 -1
  103. data/lib/rigor/inference/expression_typer.rb +261 -29
  104. data/lib/rigor/inference/global_write_census.rb +7 -4
  105. data/lib/rigor/inference/guard_rebinding.rb +1 -1
  106. data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
  107. data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
  108. data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +286 -0
  109. data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
  110. data/lib/rigor/inference/in_effect_refinements.rb +256 -0
  111. data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
  112. data/lib/rigor/inference/key_presence_guard.rb +303 -0
  113. data/lib/rigor/inference/macro_block_self_type.rb +59 -12
  114. data/lib/rigor/inference/match_rebinding.rb +1 -1
  115. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  116. data/lib/rigor/inference/method_dispatcher/argument_nodes.rb +25 -0
  117. data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
  118. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
  119. data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
  120. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
  121. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +3 -2
  122. data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
  123. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +2 -2
  124. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +3 -3
  125. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
  126. data/lib/rigor/inference/method_dispatcher/optional_count.rb +63 -0
  127. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
  128. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +247 -28
  129. data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
  130. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -7
  131. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +8 -26
  132. data/lib/rigor/inference/method_dispatcher/unit_denominator_rational.rb +67 -0
  133. data/lib/rigor/inference/method_dispatcher.rb +96 -14
  134. data/lib/rigor/inference/module_function_state.rb +2 -1
  135. data/lib/rigor/inference/narrowing.rb +20 -6
  136. data/lib/rigor/inference/object_mixins.rb +180 -0
  137. data/lib/rigor/inference/project_patched_definedness_guard.rb +86 -0
  138. data/lib/rigor/inference/project_patched_methods.rb +12 -7
  139. data/lib/rigor/inference/project_patched_scanner.rb +108 -26
  140. data/lib/rigor/inference/refine_census.rb +239 -0
  141. data/lib/rigor/inference/refine_self.rb +317 -0
  142. data/lib/rigor/inference/refined_dispatch/class_object.rb +350 -0
  143. data/lib/rigor/inference/refined_dispatch/own_method.rb +475 -0
  144. data/lib/rigor/inference/refined_dispatch.rb +320 -0
  145. data/lib/rigor/inference/repeated_or_writes.rb +2 -0
  146. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +13 -0
  147. data/lib/rigor/inference/scope_indexer.rb +827 -65
  148. data/lib/rigor/inference/statement_evaluator.rb +294 -51
  149. data/lib/rigor/inference/string_mutation.rb +9 -3
  150. data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
  151. data/lib/rigor/inference/untyped_rejoin.rb +75 -0
  152. data/lib/rigor/inference/version_guard.rb +19 -10
  153. data/lib/rigor/language_server/project_context.rb +87 -5
  154. data/lib/rigor/language_server/server.rb +18 -0
  155. data/lib/rigor/lens/anchor.rb +115 -0
  156. data/lib/rigor/lens/contract_reader.rb +118 -0
  157. data/lib/rigor/lens/document.rb +113 -0
  158. data/lib/rigor/lens/file_reader.rb +433 -0
  159. data/lib/rigor/lens/members.rb +153 -0
  160. data/lib/rigor/lens/parameters.rb +91 -0
  161. data/lib/rigor/lens/plugins.rb +40 -0
  162. data/lib/rigor/lens/run.rb +280 -0
  163. data/lib/rigor/lens/slot.rb +79 -0
  164. data/lib/rigor/lens.rb +40 -0
  165. data/lib/rigor/plugin/inflector.rb +1 -1
  166. data/lib/rigor/plugin/io_boundary.rb +42 -3
  167. data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
  168. data/lib/rigor/project_environment.rb +48 -6
  169. data/lib/rigor/protection/mutation_cache.rb +3 -2
  170. data/lib/rigor/protection/mutator.rb +1 -1
  171. data/lib/rigor/reflection/constant_ancestors.rb +127 -11
  172. data/lib/rigor/reflection/constant_path.rb +13 -2
  173. data/lib/rigor/reflection.rb +40 -8
  174. data/lib/rigor/scope/discovery_index.rb +40 -1
  175. data/lib/rigor/scope/resolution_chain.rb +159 -11
  176. data/lib/rigor/scope.rb +211 -21
  177. data/lib/rigor/sig_gen/classification.rb +4 -1
  178. data/lib/rigor/sig_gen/generator.rb +100 -10
  179. data/lib/rigor/sig_gen/method_candidate.rb +2 -2
  180. data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
  181. data/lib/rigor/source/alias_names.rb +52 -0
  182. data/lib/rigor/source/literals.rb +3 -2
  183. data/lib/rigor/source/symbol_name.rb +20 -0
  184. data/lib/rigor/type/combinator.rb +34 -2
  185. data/lib/rigor/type/float_range.rb +4 -1
  186. data/lib/rigor/type/nominal.rb +6 -1
  187. data/lib/rigor/version.rb +1 -1
  188. data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
  189. data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
  190. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
  191. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
  192. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
  193. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
  194. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
  195. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
  196. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
  197. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
  198. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +29 -3
  199. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +5 -1
  200. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
  201. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
  202. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
  203. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
  204. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
  205. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
  206. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
  207. data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
  208. data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
  209. data/plugins/rigor-alba/sig/alba.rbs +4 -0
  210. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
  211. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
  212. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +7 -6
  213. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
  214. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
  215. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
  216. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
  217. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
  218. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
  219. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
  220. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
  221. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
  222. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
  223. data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
  224. data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
  225. data/sig/rigor/plugin/io_boundary.rbs +3 -0
  226. data/sig/rigor/scope.rbs +30 -4
  227. data/sig/rigor/source.rbs +4 -0
  228. data/sig/rigor.rbs +9 -4
  229. data/skills/rigor-project-init/references/02-configure.md +2 -2
  230. metadata +83 -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,173 @@ 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 and a call no refinement wins fall through. A
1956
+ # class object walks its own lookup (issue #1752): under `refine(Object) { def oo = :o }` a refinement that wins
1957
+ # `Integer.oo` answers `Dynamic[top]` ({#refined_class_object_result}). A call site no refinement is in effect
1958
+ # at answers nil at once and records nothing: its answer cannot change without an edit to its own file.
1959
+ #
1960
+ # ADR-121 WD7 (A2) — inside the span of an activation that puts an opaque module in effect
1961
+ # ({InEffectRefinements.opaque_module?}), any method of any object may be replaced, so every call on an instance
1962
+ # or a class object answers `Dynamic[top]`.
1963
+ def try_refined_dispatch(node, receiver, arg_types, block_type)
1964
+ list = scope.in_effect_refinements(node)
1965
+ return nil if list.empty?
1966
+
1967
+ InEffectRefinements.record_refinement_names(node.name)
1968
+ return dynamic_top if refined_receiver?(receiver) && opaque_refinement_span?(node, list)
1969
+ return nil if scope.discovered_refinements.empty?
1970
+
1971
+ refined_indirect_result(node, receiver, list) ||
1972
+ refined_receiver_result(node, receiver, list, arg_types, block_type)
1973
+ end
1974
+
1975
+ # ADR-121 WD4 — `send` / `__send__` / `public_send` with a literal name, `respond_to?` and `method` /
1976
+ # `public_method` with one, and a `&:name` block argument honour refinements too (CRuby `test_refinement.rb`;
1977
+ # probed on Ruby 4.0.5). When the name they reach is refined in effect for the receiver they answer
1978
+ # `Dynamic[top]`: typing them through the arm is deferred, and `respond_to?` must never fold to `false`. A
1979
+ # `&:name` block's receiver is the yielded element, which is not known here, so any in-effect refinement of the
1980
+ # name answers. Only `methods` ignores refinements and keeps its answer.
1981
+ def refined_indirect_result(node, receiver, list)
1982
+ name = REFINED_NAMING_CALLS.include?(node.name) ? literal_method_name_argument(node) : nil
1983
+ return dynamic_top if name && refined_name_for?(receiver, name, list)
1984
+
1985
+ block_name = symbol_block_argument(node)
1986
+ dynamic_top if block_name && refined_anywhere?(block_name, list)
1987
+ end
1988
+
1989
+ REFINED_NAMING_CALLS = Set[:send, :__send__, :public_send, :respond_to?, :method, :public_method].freeze
1990
+ private_constant :REFINED_NAMING_CALLS
1991
+
1992
+ def literal_method_name_argument(node)
1993
+ argument = node.arguments&.arguments&.first
1994
+ name = argument.unescaped if argument.is_a?(Prism::SymbolNode) || argument.is_a?(Prism::StringNode)
1995
+ return nil if name.nil?
1996
+
1997
+ name = name.to_sym
1998
+ InEffectRefinements.record_refinement_names(name)
1999
+ name
2000
+ end
2001
+
2002
+ def symbol_block_argument(node)
2003
+ block = node.block
2004
+ return nil unless block.is_a?(Prism::BlockArgumentNode) && block.expression.is_a?(Prism::SymbolNode)
2005
+
2006
+ name = block.expression.unescaped.to_sym
2007
+ InEffectRefinements.record_refinement_names(name)
2008
+ name
2009
+ end
2010
+
2011
+ def refined_name_for?(receiver, name, list)
2012
+ members = receiver.is_a?(Type::Union) ? receiver.members : [receiver]
2013
+ members.any? do |member|
2014
+ next !RefinedDispatch.winner(scope, member.class_name, name, list, kind: :singleton).nil? if
2015
+ member.is_a?(Type::Singleton)
2016
+
2017
+ class_name = refined_receiver_class_name(member)
2018
+ !class_name.nil? && !RefinedDispatch.winner(scope, class_name, name, list).nil?
2019
+ end
2020
+ end
2021
+
2022
+ # ADR-121 WD7 — a names-wildcard row counts as a refinement of every name.
2023
+ def refined_anywhere?(name, list)
2024
+ refinements = scope.discovered_refinements
2025
+ wildcard = Scope::DiscoveryIndex::REFINEMENT_WILDCARD
2026
+ return true if list.include?(InEffectRefinements::UNKNOWN) &&
2027
+ refinements.any? { |_refined, methods| methods.key?(name) || methods.key?(wildcard) }
2028
+
2029
+ !RefinedDispatch.targets(refinements, name, list).nil? ||
2030
+ !RefinedDispatch.targets(refinements, wildcard, list).nil?
2031
+ end
2032
+
2033
+ # ADR-121 WD7 — does the call's lexical list (a plugin-declared module is the plugin's declaration and never
2034
+ # opaque) hold an opaque module? `list` is the full list, which is the lexical one when nothing is declared.
2035
+ def opaque_refinement_span?(node, list)
2036
+ lexical = scope.declared_refinements.empty? ? list : scope.in_effect_refinements(node, InEffectRefinements::EMPTY)
2037
+ InEffectRefinements.opaque_in?(scope, lexical)
2038
+ end
2039
+
2040
+ # Does `receiver` (or a member of a union) have a lookup the refined arm answers for: an instance of a class,
2041
+ # or a class object (issue #1752), not `Dynamic`?
2042
+ def refined_receiver?(receiver)
2043
+ members = receiver.is_a?(Type::Union) ? receiver.members : [receiver]
2044
+ members.any? { |member| member.is_a?(Type::Singleton) || !refined_receiver_class_name(member).nil? }
2045
+ end
2046
+
2047
+ def refined_receiver_result(node, receiver, list, arg_types, block_type)
2048
+ return refined_member_result(node, receiver, list, arg_types, block_type) unless receiver.is_a?(Type::Union)
2049
+
2050
+ answers = receiver.members.map { |member| refined_member_result(node, member, list, arg_types, block_type) }
2051
+ return nil if answers.none?
2052
+
2053
+ members = receiver.members.each_with_index.map do |member, index|
2054
+ answers[index] || call_result_type_for(node, receiver_override: member)
2055
+ end
2056
+ Type::Combinator.union(*members)
2057
+ end
2058
+
2059
+ def refined_member_result(node, receiver, list, arg_types, block_type)
2060
+ return refined_class_object_result(node, receiver, list, arg_types, block_type) if
2061
+ receiver.is_a?(Type::Singleton)
2062
+
2063
+ class_name = refined_receiver_class_name(receiver)
2064
+ return nil if class_name.nil?
2065
+
2066
+ winner = RefinedDispatch.winner(scope, class_name, node.name, list)
2067
+ return nil if winner.nil?
2068
+ return dynamic_top if winner == RefinedDispatch::UNKNOWN
2069
+
2070
+ refined_body_return(winner, node.name, Type::Combinator.nominal_of(class_name), arg_types, block_type) ||
2071
+ dynamic_top
2072
+ end
2073
+
2074
+ # Issue #1752 — a call on a class object, walked through its singleton levels. A refinement that wins answers
2075
+ # `Dynamic[top]`, never its refine body's type: a hook on any ancestor (an RBS-declared `include Singleton`, a
2076
+ # gem concern, `define_singleton_method(:included)`, a `Module#included` patch) can install the class object's
2077
+ # own method where no table sees it, and the body's type then fires downstream on correct code. A walk that finds
2078
+ # no winner keeps the class object's own answer (`Integer.sqrt(16)` stays `Integer`).
2079
+ def refined_class_object_result(node, receiver, list, _arg_types, _block_type)
2080
+ winner = RefinedDispatch.winner(scope, receiver.class_name, node.name, list, kind: :singleton)
2081
+ dynamic_top unless winner.nil?
2082
+ end
2083
+
2084
+ # The winning refine body's inferred return, re-typed with the receiver's class as `self` and the call's
2085
+ # argument types bound as an undeclared method's are; nil when the body is not readable here.
2086
+ #
2087
+ # A body another file wrote is re-typed with that file's in-effect refinements at the `def` (its own module
2088
+ # among them), so a sibling override it calls answers from its refine body too.
2089
+ def refined_body_return(winner, method_name, self_type, arg_types, block_type)
2090
+ def_node, foreign =
2091
+ InEffectRefinements.refinement_def_with_query(scope, winner.module_name, winner.refined_class, method_name)
2092
+ return nil if def_node.nil?
2093
+
2094
+ refinements = foreign&.at(def_node.location.start_offset) do |name|
2095
+ InEffectRefinements.activated_modules(scope, name)
2096
+ end
2097
+ infer_user_method_return(def_node, self_type, arg_types, yield_type: block_type, refinements: refinements)
2098
+ rescue StandardError
2099
+ nil
2100
+ end
2101
+
2102
+ # The class an instance-side receiver is an instance of, or nil for one this instance walk does not answer: a
2103
+ # class object (it walks its own lookup, {#refined_class_object_result}), `Dynamic`, `top`, or a shape with no
2104
+ # class.
2105
+ def refined_receiver_class_name(receiver)
2106
+ case receiver
2107
+ when Type::Nominal, Type::StructInstance, Type::DataInstance then receiver.class_name
2108
+ when Type::Constant then receiver.value.class.name
2109
+ when Type::Tuple then "Array"
2110
+ when Type::HashShape then "Hash"
2111
+ when Type::IntegerRange then "Integer"
2112
+ when Type::FloatRange then "Float"
2113
+ when Type::Refined, Type::Difference then refined_receiver_class_name(receiver.base)
2114
+ end
2115
+ end
2116
+
1930
2117
  # The post-dispatch tiers for a call `MethodDispatcher` could not answer, in their historical
1931
2118
  # order; extracted from {#call_dispatch_type_for} whole.
1932
2119
  def dispatch_miss_result(node, receiver, arg_types, block_type = nil)
@@ -2284,7 +2471,7 @@ module Rigor
2284
2471
  return every_definer_raises_type(raising, receiver, call_node, arg_types, block_type) if def_node.nil?
2285
2472
 
2286
2473
  result = infer_user_method_return(def_node, receiver, arg_types,
2287
- self_fold_safe: fold_safe_call_receiver?(call_node, receiver),
2474
+ self_fold_safe: fold_safe_call_receiver?(call_node),
2288
2475
  yield_type: block_type)
2289
2476
  return result if result.nil?
2290
2477
 
@@ -2319,11 +2506,11 @@ module Rigor
2319
2506
  # This is only a property of the receiver expression; whether the grant is actually issued also needs
2320
2507
  # a `StructInstance` carrier and a body that survives the self-use scan, both decided in
2321
2508
  # {#build_user_method_body_scope}.
2322
- def fold_safe_call_receiver?(call_node, receiver)
2509
+ def fold_safe_call_receiver?(call_node)
2323
2510
  return false if call_node.nil?
2324
2511
 
2325
2512
  case call_node.receiver
2326
- when Prism::CallNode then materialization_call?(call_node.receiver, receiver)
2513
+ when Prism::CallNode then materialization_call?(call_node.receiver)
2327
2514
  when Prism::LocalVariableReadNode then scope.struct_fold_safe?(call_node.receiver.name)
2328
2515
  when nil, Prism::SelfNode then scope.struct_fold_safe?(:self)
2329
2516
  else false
@@ -2333,10 +2520,10 @@ module Rigor
2333
2520
  # Issue #595 — the materialisation test lives ONCE, in
2334
2521
  # [`struct_materialization.rb`](method_dispatcher/struct_materialization.rb), because the direct
2335
2522
  # member-read gate ({MethodDispatcher::StructFolding}`#fresh_receiver?`) needs the identical answer;
2336
- # see {MethodDispatcher::StructMaterialization.materialization_call?}. It resolves its own `.with`
2337
- # guard through `Scope`, so neither consumer supplies a lookup and neither can answer differently.
2338
- def materialization_call?(node, receiver)
2339
- MethodDispatcher::StructMaterialization.materialization_call?(node, receiver, scope)
2523
+ # see {MethodDispatcher::StructMaterialization.materialization_call?}. A `.with` call is never one
2524
+ # (`Struct` has no `#with`, #1335), so the test needs no lookup from either consumer.
2525
+ def materialization_call?(node)
2526
+ MethodDispatcher::StructMaterialization.materialization_call?(node, scope)
2340
2527
  end
2341
2528
 
2342
2529
  # Module-singleton call resolution (ADR-57 follow-up) — resolves `Foo.<name>` on a `Singleton[Foo]`
@@ -2505,7 +2692,7 @@ module Rigor
2505
2692
  def every_definer_raises_type(defs, receiver, call_node, arg_types, block_type)
2506
2693
  return nil if defs.nil?
2507
2694
 
2508
- self_fold_safe = fold_safe_call_receiver?(call_node, receiver)
2695
+ self_fold_safe = fold_safe_call_receiver?(call_node)
2509
2696
  defs.each do |def_node|
2510
2697
  result = infer_user_method_return(def_node, receiver, arg_types,
2511
2698
  self_fold_safe: self_fold_safe, yield_type: block_type)
@@ -2959,14 +3146,37 @@ module Rigor
2959
3146
  # not observable on the body scope, so it does need its own memo-key slot. It is dropped for a def
2960
3147
  # that cannot reach a `yield` — nearly all of them — which keeps the key shape constant for the
2961
3148
  # methods the memo actually carries and confines the extra dimension to yielding callees.
2962
- def infer_user_method_return(def_node, receiver, arg_types, self_fold_safe: false, yield_type: nil)
3149
+ # Issue #1703 — inside the guarded re-walk a callee's return summary is still computed with `key?` guards off
3150
+ # ({KeyPresenceGuard.without_guards}): summaries are memoised for the whole run, and one computed with a guard
3151
+ # would reach the analysis every rule reads.
3152
+ #
3153
+ # Issue #1664 — `refinements` are the modules in effect in a refine body another file wrote, which the body scope
3154
+ # carries as declared refinements: its nodes are not this file's, so this file's lexical list cannot answer them.
3155
+ # The return memo and the recursion guard do not key on them, which holds because such a node comes only from
3156
+ # `DefNodeResolver.refinement_query`'s own parse and always carries that file's one list; a caller that types
3157
+ # the same node under another list must add it to the key.
3158
+ def infer_user_method_return(def_node, receiver, arg_types, self_fold_safe: false, yield_type: nil,
3159
+ refinements: nil)
2963
3160
  return nil if def_node.body.nil?
3161
+ unless KeyPresenceGuard.active?
3162
+ return infer_user_method_return_unguarded(def_node, receiver, arg_types, self_fold_safe, yield_type,
3163
+ refinements)
3164
+ end
3165
+
3166
+ KeyPresenceGuard.without_guards do
3167
+ infer_user_method_return_unguarded(def_node, receiver, arg_types, self_fold_safe, yield_type, refinements)
3168
+ end
3169
+ end
2964
3170
 
3171
+ def infer_user_method_return_unguarded(def_node, receiver, arg_types, self_fold_safe, yield_type,
3172
+ refinements = nil)
2965
3173
  yield_type = nil unless yield_type && body_yields?(def_node)
2966
3174
  body_scope = build_user_method_body_scope(def_node, receiver, arg_types,
2967
3175
  self_fold_safe: self_fold_safe)
2968
3176
  return nil if body_scope.nil?
2969
3177
 
3178
+ body_scope = body_scope.with_declared_refinements(refinements) if refinements
3179
+
2970
3180
  # Recursion-guard signature. Keyed on `(receiver, method)` only — NOT the argument types. ADR-24 WD5:
2971
3181
  # a method whose summary is still being computed resolves to `Dynamic[top]` for that cycle. Keying on
2972
3182
  # arg types would let mutual recursion through a `module_function` module (`Acceptance#accepts` →
@@ -4021,12 +4231,13 @@ module Rigor
4021
4231
  method_name: call_node.name,
4022
4232
  arg_types: arg_types,
4023
4233
  environment: scope.environment,
4024
- scope: scope
4234
+ scope: scope,
4235
+ call_node: call_node
4025
4236
  )
4026
4237
  block_return_for(
4027
4238
  block_arg, expected,
4028
4239
  call_node: call_node,
4029
- narrowed_self_type: block_body_self_narrowing(call_node, receiver_type),
4240
+ narrowing: block_body_self_narrowing(call_node, receiver_type),
4030
4241
  repeats: !BLOCK_VALUE_DISCARDING.include?(call_node.name) && block_may_repeat?(call_node, receiver_type)
4031
4242
  )
4032
4243
  rescue StandardError
@@ -4093,7 +4304,7 @@ module Rigor
4093
4304
  block_scope = block_entry_scope(
4094
4305
  block_node, param_types,
4095
4306
  call_node: call_node,
4096
- narrowed_self_type: block_body_self_narrowing(call_node, receiver),
4307
+ narrowing: block_body_self_narrowing(call_node, receiver),
4097
4308
  captured: repeating_captured_bindings(block_node, param_types, block_may_repeat?(call_node, receiver))
4098
4309
  )
4099
4310
  _result, collected = StatementEvaluator.with_break_value_sink do
@@ -4102,10 +4313,12 @@ module Rigor
4102
4313
  collected.filter_map { |jump, type| type if targets.key?(jump) }
4103
4314
  end
4104
4315
 
4105
- # The block body's narrowed `self_type`, or `nil` to leave the scope contract unchanged.
4316
+ # The block body's narrowing — a {MacroBlockSelfType::Match} carrying the narrowed `self_type` and the declared
4317
+ # refinements — or `nil` to leave the scope contract unchanged.
4106
4318
  #
4107
4319
  # ADR-16 Tier A: a registered plugin's `block_as_methods` entry matching `(receiver_type,
4108
- # call_node.name)` narrows to the receiver class's instance type.
4320
+ # call_node.name)` narrows to the receiver class's instance type, and puts its `refinements:` in effect in the
4321
+ # body (issue #1667).
4109
4322
  #
4110
4323
  # Issue #963: `define_method(:name) { ... }` installs its block as an instance method and runs it with
4111
4324
  # `self` bound to the receiving instance, so the block body's `self` is the INSTANCE side of a class body's
@@ -4115,9 +4328,11 @@ module Rigor
4115
4328
  # matters wherever the block's value is observable, e.g. a project-declared generic `define_method`
4116
4329
  # signature that returns the block's own type.
4117
4330
  def block_body_self_narrowing(call_node, receiver_type)
4118
- MacroBlockSelfType.narrow_self_type_for(
4119
- scope: scope, call_node: call_node, receiver_type: receiver_type
4120
- ) || DefineMethodBlockSelf.narrow_self_type_for(scope: scope, call_node: call_node)
4331
+ match = MacroBlockSelfType.match_for(scope: scope, call_node: call_node, receiver_type: receiver_type)
4332
+ return match if match
4333
+
4334
+ defined = DefineMethodBlockSelf.narrow_self_type_for(scope: scope, call_node: call_node)
4335
+ defined && MacroBlockSelfType::Match.new(self_type: defined, refinements: InEffectRefinements::EMPTY)
4121
4336
  end
4122
4337
 
4123
4338
  def break_arm_param_types(call_node, receiver)
@@ -4126,16 +4341,17 @@ module Rigor
4126
4341
  method_name: call_node.name,
4127
4342
  arg_types: call_arg_types(call_node),
4128
4343
  environment: scope.environment,
4129
- scope: scope
4344
+ scope: scope,
4345
+ call_node: call_node
4130
4346
  )
4131
4347
  end
4132
4348
 
4133
- def block_return_for(block_arg, expected, call_node: nil, narrowed_self_type: nil, repeats: false)
4349
+ def block_return_for(block_arg, expected, call_node: nil, narrowing: nil, repeats: false)
4134
4350
  case block_arg
4135
4351
  when Prism::BlockNode
4136
4352
  captured = repeating_captured_bindings(block_arg, expected, repeats)
4137
4353
  entry = block_entry_scope(
4138
- block_arg, expected, call_node: call_node, narrowed_self_type: narrowed_self_type, captured: captured
4354
+ block_arg, expected, call_node: call_node, narrowing: narrowing, captured: captured
4139
4355
  )
4140
4356
  type_block_body(block_arg, entry, captured: captured)
4141
4357
  when Prism::BlockArgumentNode
@@ -4151,13 +4367,14 @@ module Rigor
4151
4367
  # Issue #316 — mirrors `StatementEvaluator#build_block_entry_scope`: the block body's `self` is the
4152
4368
  # yielding method's business, so the return-typing pass must see the same unmodelled-self mark. Issue #1358
4153
4369
  # — and the same match-global view ({MatchRebinding.block_entry}), which reads the owning `call_node`.
4154
- def block_entry_scope(block_node, expected, call_node: nil, narrowed_self_type: nil, captured: nil)
4370
+ def block_entry_scope(block_node, expected, call_node: nil, narrowing: nil, captured: nil)
4155
4371
  entry = MatchRebinding.block_entry(scope.entering_opaque_block, block_node, call_node)
4156
4372
  entry = captured.lay(entry) if captured
4157
4373
  block_scope = BlockParameterBinder.new(expected_param_types: expected).bind_onto(block_node, entry)
4158
- return block_scope unless narrowed_self_type
4374
+ return block_scope unless narrowing
4159
4375
 
4160
- block_scope.with_self_type(narrowed_self_type)
4376
+ keeps_unknown = call_node ? scope.block_self_narrowing_unknown?(call_node) : false
4377
+ MacroBlockSelfType.apply(block_scope, narrowing, keeps_unknown: keeps_unknown)
4161
4378
  end
4162
4379
 
4163
4380
  # The #587 (b) captured binding when the call may run the block more than once ({#block_may_repeat?}), and
@@ -5218,13 +5435,27 @@ module Rigor
5218
5435
  acc, rest = inject_constant_start(members, seed, has_seed)
5219
5436
  return nil if acc.nil?
5220
5437
 
5221
- rest.each do |element_value|
5222
- acc = inject_constant_step(block, acc, element_value)
5438
+ captured = inject_captured_bindings(block, rest.map { |value| Type::Combinator.constant_of(value) })
5439
+ rest.each_with_index do |element_value, position|
5440
+ acc = inject_constant_step(block, acc, element_value, captured, position)
5223
5441
  return nil if acc.nil?
5224
5442
  end
5225
5443
  acc
5226
5444
  end
5227
5445
 
5446
+ # Issue #1332 — the #587 (b) captured-block binding ({#captured_block_bindings}) both inject arms lay under
5447
+ # every step, as the Tuple / Range per-element folds and the per-pair `HashShape` fold do. Without it each
5448
+ # step reads a captured variable at its entry binding, so a body that rebinds or mutates one (`n += 1`,
5449
+ # `log << e`, `cache[:k] ||= e`) pinned the result: `n = 0; [1, 2].inject(0) { |a, e| r = a + n; n += 1;
5450
+ # r }` folded to `0` where Ruby answers `1`. The fixpoint binds the accumulator parameter to `Dynamic[top]`
5451
+ # rather than to a type the inject fixpoint has not converged yet: a captured name that reads the
5452
+ # accumulator widens rather than pins. `element_types` is the constant arm's steps, one position per block
5453
+ # run, which {RepeatedOrWrites} reads; nil for the nominal arm, which marks every repeated site.
5454
+ def inject_captured_bindings(block, element_types, element: nil)
5455
+ element ||= Type::Combinator.union(*element_types)
5456
+ captured_block_bindings(block, [Type::Combinator.untyped, element], element_types: element_types)
5457
+ end
5458
+
5228
5459
  # Extracts the receiver's foldable constant values, size-capped before enumeration, or nil to decline.
5229
5460
  def inject_constant_members(receiver)
5230
5461
  case receiver
@@ -5276,9 +5507,9 @@ module Rigor
5276
5507
  # Evaluates the block body once with the running constant accumulator + the next constant element
5277
5508
  # bound to the block params, returning the result when it is a foldable `Constant` within the
5278
5509
  # magnitude cap, else nil to decline the whole fold.
5279
- def inject_constant_step(block, acc, element_value)
5510
+ def inject_constant_step(block, acc, element_value, captured, position)
5280
5511
  element = Type::Combinator.constant_of(element_value)
5281
- result = type_block_body_with_param(block, [acc, element])
5512
+ result = type_block_body_with_param(block, [acc, element], captured: captured, position: position)
5282
5513
  return nil unless result.is_a?(Type::Constant)
5283
5514
  return nil unless inject_foldable?(result.value)
5284
5515
  return nil if inject_magnitude_too_large?(result.value)
@@ -5308,13 +5539,14 @@ module Rigor
5308
5539
  seed_acc = has_seed ? seed : element
5309
5540
  return nil if seed_acc.nil?
5310
5541
 
5542
+ captured = inject_captured_bindings(block, nil, element: element)
5311
5543
  converged = BodyFixpoint.converge(
5312
5544
  names: [:__inject_acc__],
5313
5545
  seed_bindings: { __inject_acc__: seed_acc },
5314
5546
  widen: method(:widen_value_pinned),
5315
5547
  evaluate_body: lambda do |bindings|
5316
5548
  acc = bindings[:__inject_acc__]
5317
- result = type_block_body_with_param(block, [acc, element])
5549
+ result = type_block_body_with_param(block, [acc, element], captured: captured)
5318
5550
  result.nil? ? {} : { __inject_acc__: result }
5319
5551
  end
5320
5552
  )
@@ -82,7 +82,7 @@ module Rigor
82
82
 
83
83
  # A standalone walk, for a file no existing descent visits (a `pre_eval:` entry).
84
84
  def scan(root)
85
- collector = Collector.new
85
+ collector = Collector.new(InEffectRefinements.new(root))
86
86
  walk(root, collector, true)
87
87
  collector.census.freeze
88
88
  end
@@ -98,13 +98,16 @@ module Rigor
98
98
 
99
99
  # Accumulates one file's census, fed each node of a pre-order descent. `top_level` says whether the node sits
100
100
  # outside every `class` / `module` body; it is the host walk's to track. `refine` blocks are remembered by
101
- # offset as they are met, so a node inside one is known without the host walk tracking it.
101
+ # offset as they are met, so a node inside one is known without the host walk tracking it. `in_effect` is the
102
+ # file's {InEffectRefinements}: a `refine` it places where `self` is a class is the class's own method, so its
103
+ # block is not a refinement (issue #1689).
102
104
  class Collector
103
105
  attr_reader :census
104
106
 
105
- def initialize
107
+ def initialize(in_effect)
106
108
  @census = Set.new
107
109
  @refine_ranges = []
110
+ @in_effect = in_effect
108
111
  end
109
112
 
110
113
  # A node the collector cannot read must not fail the analysis of its file, nor of every file the pre-pass
@@ -223,7 +226,7 @@ module Rigor
223
226
  def self_receiver?(node) = node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
224
227
 
225
228
  def remember_refine(node)
226
- return unless node.name == :refine && self_receiver?(node) && node.block.is_a?(Prism::BlockNode)
229
+ return unless ScopeIndexer.refine_call?(node) && !@in_effect.class_body_refine?(node)
227
230
 
228
231
  location = node.block.location
229
232
  @refine_ranges << (location.start_offset...location.end_offset)
@@ -406,7 +406,7 @@ module Rigor
406
406
  arguments = call_node.arguments&.arguments || []
407
407
  MethodDispatcher.expected_block_param_types(
408
408
  receiver_type: receiver, method_name: call_node.name, environment: scope.environment, scope: scope,
409
- arg_types: arguments.map { |argument| scope.type_of(argument) }
409
+ arg_types: arguments.map { |argument| scope.type_of(argument) }, call_node: call_node
410
410
  )
411
411
  rescue StandardError
412
412
  []
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ module Rigor
6
+ module Inference
7
+ class InEffectRefinements
8
+ # Issue #1666 — the Proc literals whose body a `.refined` receiver call applies to, recognised from syntax.
9
+ module ProcLiterals
10
+ # The `Kernel` calls whose literal block is the Proc they return.
11
+ BARE_PROC_CALLS = %i[proc lambda].freeze
12
+ private_constant :BARE_PROC_CALLS
13
+
14
+ # Copies that keep a refined Proc's refinements (`test_refined_preserved_by_dup`): a chain reads through them.
15
+ COPY_CALLS = %i[dup clone].freeze
16
+ private_constant :COPY_CALLS
17
+
18
+ module_function
19
+
20
+ # `[literal, refined calls]` for the outermost `.refined` call `node`: the Proc literal the chain starts from
21
+ # ({.refinable}, or nil), and every `.refined` call of the chain, innermost (first called) first. The chain
22
+ # reads through parentheses and through `dup` / `clone`.
23
+ def refined_chain(node)
24
+ calls = [node]
25
+ receiver = unwrap(node.receiver)
26
+ loop do
27
+ if refined_call?(receiver)
28
+ calls << receiver
29
+ elsif !copy_call?(receiver)
30
+ break
31
+ end
32
+ receiver = unwrap(receiver.receiver)
33
+ end
34
+ [refinable(receiver), calls.reverse]
35
+ end
36
+
37
+ def refined_call?(node) = node.is_a?(Prism::CallNode) && node.name == :refined
38
+
39
+ def copy_call?(node)
40
+ node.is_a?(Prism::CallNode) && COPY_CALLS.include?(node.name) && node.arguments.nil? && node.block.nil? &&
41
+ !node.receiver.nil?
42
+ end
43
+
44
+ # `( expr )` with one statement reads as `expr`.
45
+ def unwrap(node)
46
+ while node.is_a?(Prism::ParenthesesNode) && node.body.is_a?(Prism::StatementsNode) &&
47
+ node.body.body.size == 1
48
+ node = node.body.body.first
49
+ end
50
+ node
51
+ end
52
+
53
+ # The node whose span is the body `node.refined(…)` applies to: `node` itself for a lambda literal, or the
54
+ # literal block of a `proc` / `lambda` call on no receiver or on `Kernel`, or of `Proc.new`; nil for anything
55
+ # else. A Proc held in a variable, or one passed as `&blk`, is not one: which block it holds is not syntax.
56
+ def refinable(node)
57
+ case node
58
+ when Prism::LambdaNode then node
59
+ when Prism::CallNode
60
+ block = node.block
61
+ return nil unless block.is_a?(Prism::BlockNode)
62
+
63
+ block if bare_proc_call?(node) || proc_new_call?(node)
64
+ end
65
+ end
66
+
67
+ def bare_proc_call?(node)
68
+ BARE_PROC_CALLS.include?(node.name) && (node.receiver.nil? || top_constant?(node.receiver, :Kernel))
69
+ end
70
+
71
+ def proc_new_call?(node)
72
+ node.name == :new && top_constant?(node.receiver, :Proc)
73
+ end
74
+
75
+ # `Name` or `::Name`.
76
+ def top_constant?(node, name)
77
+ (node.is_a?(Prism::ConstantReadNode) || (node.is_a?(Prism::ConstantPathNode) && node.parent.nil?)) &&
78
+ node.name == name
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module Inference
5
+ class InEffectRefinements
6
+ # Issue #1664 — the `def`s one file's refine bodies define, keyed by refining module, refined class (each name
7
+ # the `refine` argument's spelling can denote) and method, with the `Module.nesting` each is written in. The
8
+ # last `def` of a name wins, as Ruby's method table keeps it.
9
+ class RefineDefs
10
+ # ADR-121 WD7 — what {#lookup} answers for a name whose last definer has no body (`define_method`, `attr_*`,
11
+ # `undef`): the search for a body ends there, so the typed arm answers `Dynamic[top]` rather than another
12
+ # file's `def`.
13
+ BODILESS = :bodiless
14
+ # Issue #1749 — what {#lookup} answers for a name whose refinement the walk cannot attribute: one from a
15
+ # `refine` that runs inside a method (it exists once the method has run, replacing what a refine body wrote for
16
+ # the name, or adding nothing if it never runs, and `using` sees it only if it runs first), or from an eval on
17
+ # a constant that several names denote (it runs on one of them, which another file decides, and replaces or is
18
+ # replaced by a `def` of the name in a refine body of the module, whichever runs first). A `def` of the name
19
+ # in a module-body refine, in this file or another, cannot say which one answers, so the name is never typed
20
+ # from any body: the lookup ends there across every file that declares the module, and unlike {BODILESS} a
21
+ # later record does not replace it.
22
+ UNDECIDED = :undecided
23
+
24
+ attr_reader :nestings
25
+
26
+ def initialize
27
+ @nodes = {}
28
+ @nestings = {}.compare_by_identity
29
+ end
30
+
31
+ # `owners` are the refining modules (several where an eval on a constant may run on any of its names), each
32
+ # filed under every class of `targets`. `name` is the method `def_node` answers for: its own name, or an
33
+ # alias's (ADR-121 WD7). A nil `def_node` is a later definer with no body (`define_method`, `attr_*`, `undef`),
34
+ # which replaces the earlier `def`. A name whose refinement the walk cannot attribute keeps no body and is
35
+ # {UNDECIDED}: several owners (Ruby runs the body on one of them only), or `created`, true for a `refine` that
36
+ # runs inside a method.
37
+ def record(owners, targets, name, def_node, nesting, created: false)
38
+ undecided = created || owners.size > 1
39
+ def_node = nil if undecided
40
+ owners.each do |owner|
41
+ by_class = (@nodes[owner] ||= {})
42
+ targets.each do |class_name|
43
+ by_name = (by_class[class_name] ||= {})
44
+ by_name[name] = undecided ? UNDECIDED : (def_node || BODILESS) unless by_name[name] == UNDECIDED
45
+ end
46
+ end
47
+ @nestings[def_node] = nesting if def_node
48
+ end
49
+
50
+ def lookup(module_name, class_name, method_name) = @nodes.dig(module_name, class_name, method_name)
51
+ end
52
+ end
53
+ end
54
+ end