rigortype 0.4.7 → 0.4.8

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 (268) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -1
  3. data/data/core_overlay/bigdecimal.rbs +33 -0
  4. data/data/core_overlay/resolv.rbs +30 -0
  5. data/data/gem_overlay/activesupport/core_ext.rbs +29 -0
  6. data/data/gem_overlay/addressable/addressable.rbs +40 -0
  7. data/docs/handbook/01-getting-started.md +1 -1
  8. data/docs/handbook/11-sig-gen.md +35 -0
  9. data/docs/install.md +20 -0
  10. data/docs/manual/01-installation.md +67 -26
  11. data/docs/manual/02-cli-reference.md +307 -91
  12. data/docs/manual/03-configuration.md +41 -3
  13. data/docs/manual/04-diagnostics.md +5 -4
  14. data/docs/manual/08-skills.md +11 -0
  15. data/docs/manual/09-editor-integration.md +8 -0
  16. data/docs/manual/14-rails-quickstart.md +1 -1
  17. data/docs/manual/19-effect-labels.md +3 -4
  18. data/docs/manual/plugins/rigor-actionpack.md +15 -6
  19. data/docs/manual/plugins/rigor-activesupport-core-ext.md +3 -0
  20. data/docs/manual/plugins/rigor-sorbet.md +17 -3
  21. data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
  22. data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
  23. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +8 -0
  24. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +13 -2
  25. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -0
  26. data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
  27. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +40 -11
  28. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
  29. data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
  30. data/lib/rigor/analysis/check_rules/rule_ids.rb +8 -1
  31. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  32. data/lib/rigor/analysis/check_rules.rb +598 -99
  33. data/lib/rigor/analysis/crash_signature.rb +43 -1
  34. data/lib/rigor/analysis/dependency_recorder.rb +233 -76
  35. data/lib/rigor/analysis/incremental.rb +11 -1
  36. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -1
  37. data/lib/rigor/analysis/incremental_session.rb +279 -37
  38. data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
  39. data/lib/rigor/analysis/rule_catalog.rb +37 -1
  40. data/lib/rigor/analysis/run_cache_key.rb +27 -3
  41. data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
  42. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +4 -0
  43. data/lib/rigor/analysis/runner/declaration_position.rb +7 -7
  44. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +72 -20
  45. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +3 -8
  46. data/lib/rigor/analysis/runner/pool_coordinator.rb +26 -5
  47. data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
  48. data/lib/rigor/analysis/runner.rb +222 -39
  49. data/lib/rigor/analysis/self_call_resolution_recorder.rb +61 -22
  50. data/lib/rigor/analysis/worker_session.rb +42 -41
  51. data/lib/rigor/bleeding_edge.rb +19 -4
  52. data/lib/rigor/cache/file_digest.rb +39 -31
  53. data/lib/rigor/cache/incremental_snapshot.rb +66 -7
  54. data/lib/rigor/cache/rbs_descriptor.rb +37 -2
  55. data/lib/rigor/cache/rbs_environment.rb +5 -1
  56. data/lib/rigor/cli/baseline_command.rb +15 -2
  57. data/lib/rigor/cli/check_command.rb +4 -2
  58. data/lib/rigor/cli/effects_snapshot_command.rb +8 -1
  59. data/lib/rigor/cli/lens_command.rb +17 -2
  60. data/lib/rigor/cli/sig_gen_command.rb +8 -0
  61. data/lib/rigor/cli/skill_describe.rb +1 -0
  62. data/lib/rigor/cli.rb +5 -2
  63. data/lib/rigor/config_audit.rb +32 -18
  64. data/lib/rigor/configuration/severity_profile.rb +9 -0
  65. data/lib/rigor/configuration.rb +32 -5
  66. data/lib/rigor/effects/collector.rb +53 -21
  67. data/lib/rigor/environment/bundle_sig_discovery.rb +63 -28
  68. data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
  69. data/lib/rigor/environment/default_libraries.rb +17 -0
  70. data/lib/rigor/environment/deferred_signature_plan.rb +104 -6
  71. data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
  72. data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
  73. data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
  74. data/lib/rigor/environment/rbs_hierarchy.rb +6 -0
  75. data/lib/rigor/environment/rbs_loader.rb +312 -63
  76. data/lib/rigor/environment.rb +131 -35
  77. data/lib/rigor/inference/acceptance.rb +50 -15
  78. data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
  79. data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
  80. data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
  81. data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
  82. data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
  83. data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
  84. data/lib/rigor/inference/block_auto_splat.rb +1 -1
  85. data/lib/rigor/inference/block_run_count.rb +9 -2
  86. data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
  87. data/lib/rigor/inference/callee_mutation_chain.rb +1 -1
  88. data/lib/rigor/inference/captured_locals.rb +2 -0
  89. data/lib/rigor/inference/census_foreign_block.rb +354 -0
  90. data/lib/rigor/inference/clause_widening.rb +1 -1
  91. data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
  92. data/lib/rigor/inference/construction_escape.rb +56 -15
  93. data/lib/rigor/inference/declaration_walk.rb +4 -0
  94. data/lib/rigor/inference/def_node_resolver.rb +32 -14
  95. data/lib/rigor/inference/def_return_typer.rb +1 -1
  96. data/lib/rigor/inference/definer_resolution.rb +15 -7
  97. data/lib/rigor/inference/definers_index.rb +72 -16
  98. data/lib/rigor/inference/error_info.rb +16 -0
  99. data/lib/rigor/inference/expression_typer.rb +1459 -287
  100. data/lib/rigor/inference/flow_tracer.rb +31 -14
  101. data/lib/rigor/inference/fork_map.rb +3 -0
  102. data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
  103. data/lib/rigor/inference/global_write_census.rb +8 -6
  104. data/lib/rigor/inference/in_effect_refinements.rb +7 -0
  105. data/lib/rigor/inference/indexed_narrowing.rb +57 -8
  106. data/lib/rigor/inference/ivar_writer_census.rb +106 -11
  107. data/lib/rigor/inference/key_presence_guard.rb +51 -25
  108. data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
  109. data/lib/rigor/inference/last_line.rb +5 -2
  110. data/lib/rigor/inference/loop_value_record.rb +63 -15
  111. data/lib/rigor/inference/match_rebinding/frame.rb +11 -0
  112. data/lib/rigor/inference/match_rebinding/yielding_match.rb +74 -18
  113. data/lib/rigor/inference/match_rebinding.rb +21 -8
  114. data/lib/rigor/inference/method_dispatcher/block_folding.rb +43 -29
  115. data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
  116. data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
  117. data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
  118. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
  119. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
  120. data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
  121. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +12 -0
  122. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +409 -67
  123. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +1 -1
  124. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +384 -64
  125. data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
  126. data/lib/rigor/inference/method_dispatcher.rb +66 -18
  127. data/lib/rigor/inference/method_parameter_binder.rb +136 -56
  128. data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
  129. data/lib/rigor/inference/module_function_state.rb +13 -2
  130. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  131. data/lib/rigor/inference/mutation_widening.rb +44 -4
  132. data/lib/rigor/inference/narrowing.rb +201 -27
  133. data/lib/rigor/inference/operand_effects.rb +43 -2
  134. data/lib/rigor/inference/optimistic_origin.rb +133 -8
  135. data/lib/rigor/inference/origin_lookup.rb +1 -1
  136. data/lib/rigor/inference/precision_scanner.rb +1 -1
  137. data/lib/rigor/inference/project_method_ownership.rb +28 -10
  138. data/lib/rigor/inference/project_patched_methods.rb +65 -13
  139. data/lib/rigor/inference/project_patched_scanner.rb +120 -25
  140. data/lib/rigor/inference/project_patched_visibility.rb +39 -3
  141. data/lib/rigor/inference/rbs_type_translator.rb +68 -15
  142. data/lib/rigor/inference/receiver_writer_call.rb +67 -0
  143. data/lib/rigor/inference/recursion_rebinding.rb +157 -0
  144. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +56 -18
  145. data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -17
  146. data/lib/rigor/inference/refined_dispatch.rb +13 -0
  147. data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
  148. data/lib/rigor/inference/remover_holder_scan.rb +76 -22
  149. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
  150. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +12 -11
  151. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +188 -28
  152. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
  153. data/lib/rigor/inference/scope_indexer.rb +2079 -183
  154. data/lib/rigor/inference/self_call_ivar_writes.rb +26 -7
  155. data/lib/rigor/inference/self_preserving_block.rb +87 -0
  156. data/lib/rigor/inference/singleton_hook_decline.rb +41 -6
  157. data/lib/rigor/inference/singleton_object_constant.rb +32 -2
  158. data/lib/rigor/inference/statement_evaluator.rb +970 -250
  159. data/lib/rigor/inference/string_mutation.rb +14 -0
  160. data/lib/rigor/inference/synthetic_method_scanner.rb +35 -11
  161. data/lib/rigor/inference/toplevel_def_census.rb +14 -6
  162. data/lib/rigor/inference/unknown_store_widening.rb +1 -1
  163. data/lib/rigor/inference/untyped_rejoin.rb +1 -1
  164. data/lib/rigor/inference/void_tail_summary.rb +29 -10
  165. data/lib/rigor/lens/claims/comparison.rb +78 -0
  166. data/lib/rigor/lens/claims/request.rb +131 -0
  167. data/lib/rigor/lens/claims/type_reader.rb +216 -0
  168. data/lib/rigor/lens/claims.rb +208 -0
  169. data/lib/rigor/lens/contract_reader.rb +5 -0
  170. data/lib/rigor/lens/document.rb +1 -1
  171. data/lib/rigor/lens/file_reader.rb +21 -8
  172. data/lib/rigor/lens/run.rb +8 -0
  173. data/lib/rigor/lens.rb +1 -1
  174. data/lib/rigor/library_probe.rb +68 -0
  175. data/lib/rigor/memo_audit.rb +282 -0
  176. data/lib/rigor/plugin/base.rb +15 -2
  177. data/lib/rigor/plugin/io_boundary.rb +25 -3
  178. data/lib/rigor/plugin/manifest.rb +7 -3
  179. data/lib/rigor/plugin/registry.rb +13 -2
  180. data/lib/rigor/plugin/trust_policy.rb +46 -0
  181. data/lib/rigor/protection/mutation_cache.rb +4 -1
  182. data/lib/rigor/reflection/constant_ancestors.rb +79 -22
  183. data/lib/rigor/reflection/declaration_collectors.rb +55 -0
  184. data/lib/rigor/reflection/declarations.rb +58 -0
  185. data/lib/rigor/reflection/project_method.rb +177 -0
  186. data/lib/rigor/reflection.rb +2 -0
  187. data/lib/rigor/rooted_glob.rb +18 -1
  188. data/lib/rigor/run_scoped_slots.rb +20 -20
  189. data/lib/rigor/scope/discovery_index.rb +149 -2
  190. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
  191. data/lib/rigor/scope/resolution_chain.rb +900 -55
  192. data/lib/rigor/scope.rb +307 -34
  193. data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
  194. data/lib/rigor/sig_gen/classification.rb +14 -1
  195. data/lib/rigor/sig_gen/generator.rb +222 -13
  196. data/lib/rigor/sig_gen/method_candidate.rb +14 -0
  197. data/lib/rigor/sig_gen/observation_collector.rb +11 -2
  198. data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
  199. data/lib/rigor/sig_gen/skip_reason_catalog.rb +47 -0
  200. data/lib/rigor/sig_gen/writer.rb +21 -0
  201. data/lib/rigor/sig_gen.rb +1 -0
  202. data/lib/rigor/source/forwardable_delegators.rb +122 -0
  203. data/lib/rigor/source/node_children.rb +13 -0
  204. data/lib/rigor/type/combinator.rb +164 -8
  205. data/lib/rigor/type/interface.rb +60 -0
  206. data/lib/rigor/type.rb +1 -0
  207. data/lib/rigor/type_of_memo/serving.rb +99 -0
  208. data/lib/rigor/type_of_memo/stats.rb +43 -0
  209. data/lib/rigor/type_of_memo.rb +337 -0
  210. data/lib/rigor/typing_state.rb +1094 -0
  211. data/lib/rigor/version.rb +1 -1
  212. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +2 -2
  213. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +57 -25
  214. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
  215. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
  216. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
  217. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
  218. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
  219. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +130 -30
  220. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -5
  221. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
  222. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +49 -13
  223. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
  224. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +4 -2
  225. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +13 -5
  226. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +29 -0
  227. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
  228. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
  229. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -2
  230. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
  231. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +6 -0
  232. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +199 -1
  233. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +3 -2
  234. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +4 -4
  235. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +209 -2
  236. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
  237. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
  238. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +80 -3
  239. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
  240. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +1 -6
  241. data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
  242. data/sig/rigor/analysis/fact_store.rbs +2 -3
  243. data/sig/rigor/cache/descriptor.rbs +14 -0
  244. data/sig/rigor/environment.rbs +4 -1
  245. data/sig/rigor/plugin/base.rbs +53 -19
  246. data/sig/rigor/plugin/interfaces.rbs +51 -0
  247. data/sig/rigor/plugin/io_boundary.rbs +2 -3
  248. data/sig/rigor/plugin/node_context.rbs +21 -0
  249. data/sig/rigor/reflection.rbs +37 -0
  250. data/sig/rigor/scope.rbs +22 -3
  251. data/sig/rigor/trinary.rbs +1 -0
  252. data/sig/rigor/type.rbs +9 -3
  253. data/sig/rigor.rbs +3 -2
  254. data/skills/rigor-ask/SKILL.md +1 -0
  255. data/skills/rigor-project-init/references/01-detect.md +1 -0
  256. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +5 -1
  257. data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
  258. data/skills/rigor-rbs-contract/SKILL.md +147 -0
  259. data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
  260. data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
  261. data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
  262. data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
  263. data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
  264. data/skills/rigor-rbs-setup/SKILL.md +3 -1
  265. data/skills/rigor-type-oracle/SKILL.md +26 -1
  266. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
  267. metadata +48 -2
  268. data/lib/rigor/inference/memo_audit.rb +0 -55
@@ -9,9 +9,11 @@ require_relative "../external_ancestor_resolution"
9
9
  require_relative "../void_origin"
10
10
  require_relative "../optimistic_origin"
11
11
  require_relative "argument_distribution"
12
+ require_relative "interface_shape"
13
+ require_relative "class_keyed_block_params"
12
14
  require_relative "overload_selector"
13
15
  require_relative "self_substitute"
14
- require_relative "../../run_scoped_slots"
16
+ require_relative "../../typing_state"
15
17
 
16
18
  module Rigor
17
19
  module Inference
@@ -51,9 +53,9 @@ module Rigor
51
53
  # * Method-level type parameters bind from the block return type (Slice 6 phase C) and from a
52
54
  # positional parameter whose declared type is EXACTLY a type variable (issue #303 —
53
55
  # `def foo[T]: (T) -> T` binds `T` from the first argument, and carries it into a generic return
54
- # such as `-> Array[T]`), plus a `Range[A]` element (#834). A variable reachable only through other
55
- # container positions (`Array[T] arg`), a rest positional (`*T`), or a keyword parameter is still
56
- # unbound and degrades to `Dynamic[Top]`.
56
+ # such as `-> Array[T]`), plus a `Range[A]` element (#834) and a `singleton(T)` class object (#2828).
57
+ # A variable reachable only through other container positions (`Array[T] arg`), a rest positional
58
+ # (`*T`), or a keyword parameter is still unbound and degrades to `Dynamic[Top]`.
57
59
  # A block-return variable that a parameter also names takes the value class the argument and the
58
60
  # block share once the call passes an argument, or stays unbound when they share none (see
59
61
  # {compose_block_type_vars}).
@@ -72,6 +74,23 @@ module Rigor
72
74
  # objects answer to methods their RBS omits (`ActionController::Base`, `Hash`, …).
73
75
  ALLOWED_RBS_COMPLETE_ANCESTORS = ["Rigor::Plugin::Base"].freeze
74
76
 
77
+ # #2681 — the owners whose members every class answers to. A name only they declare is not part of an
78
+ # allow-listed ancestor's contract, so a `def` of it never walks for the declaration it overrides.
79
+ OBJECT_SURFACE_OWNERS = %w[BasicObject Object Kernel Module Class].freeze
80
+
81
+ # Per-thread memo, keyed by environment: the Set of the names an allow-listed ancestor's RBS declarations
82
+ # declare outside {OBJECT_SURFACE_OWNERS}. It keeps {.def_declaration} off the ancestor walk for any other
83
+ # `def`.
84
+ #
85
+ # ADR-129 WD1 — a run-scoped Memo: the names are a pure function of the environment the entry is keyed on.
86
+ RBS_COMPLETE_MEMBER_NAMES = TypingState.memo(
87
+ :rbs_complete_member_names,
88
+ run_scoped: true, audit: "rbs complete member names",
89
+ reason: "a WeakKeyMap keyed on the environment; the names are read from its loader " \
90
+ "and plugin registry alone"
91
+ )
92
+ private_constant :RBS_COMPLETE_MEMBER_NAMES
93
+
75
94
  # Issue #1094 — the methods through which a value can answer Ruby's implicit `to_ary` conversion.
76
95
  # Multiple assignment and block auto-splat convert through `rb_check_array_type`, which calls
77
96
  # `to_ary` when defined and otherwise asks `respond_to_missing?`, dispatching to `method_missing`
@@ -219,6 +238,17 @@ module Rigor
219
238
  )
220
239
  end
221
240
 
241
+ # ADR-130 WD5 — interface dispatch switched off. Inside the block a `Rigor::Plugin` interface value keeps its
242
+ # carrier, but no call on it resolves through the interface's members: the receiver names no class, as
243
+ # `Dynamic[top]` names none. `rigor sig-gen` analyses here, so it writes no RBS from an interface a plugin
244
+ # author did not write against, and the behavioural parity gate's `:dispatch_off` mode runs here
245
+ # (`spec/integration/interface_parity_spec.rb`). The switch is a per-thread `TypingState` frame
246
+ # ({InterfaceShape.without_dispatch}), so a run under it is sequential: a pooled run raises (`PoolCoordinator`).
247
+ # No CLI flag or configuration key sets it.
248
+ def without_interface_dispatch(&) = InterfaceShape.without_dispatch(&)
249
+
250
+ def interface_dispatch? = InterfaceShape.dispatch?
251
+
222
252
  # Slice 6 (Phase C sub-phase 1) probe: returns the positional block-parameter types declared by
223
253
  # the receiving method's selected RBS overload, translated into `Rigor::Type`. Used by the
224
254
  # StatementEvaluator to bind block parameter names before evaluating the block body.
@@ -251,10 +281,89 @@ module Rigor
251
281
  )
252
282
  end
253
283
 
284
+ # The RBS declaration a `def method_name` on `class_name` (`kind` side) binds its parameters from
285
+ # (`MethodParameterBinder`): the class's own, or nil.
286
+ #
287
+ # #2681 — when `class_name` is a Ruby-source class with no RBS, the declaration the `def` overrides on an
288
+ # ADR-43 allow-listed ancestor, as an empty RBS declaration of the class would inherit it. Nil whenever the
289
+ # call bridge would decline for a reason other than `class_name`'s own definition (no scope, no allow-listed
290
+ # ancestor, a nearer definer between them), and for a declaration one of {OBJECT_SURFACE_OWNERS} makes:
291
+ # `Kernel#load` is every class's, not that ancestor's contract. `initialize` is never bridged: a constructor
292
+ # does not override its superclass's contract, and a subclass routinely takes different arguments.
293
+ def def_declaration(environment, class_name, kind, method_name, scope)
294
+ direct = lookup_method_on(environment, class_name, kind, method_name)
295
+ return direct if direct
296
+ return nil if environment.nil? || scope.nil? || method_name == :initialize
297
+ return nil unless rbs_complete_member_name?(environment, method_name)
298
+
299
+ ancestor = allowed_rbs_complete_ancestor(environment, class_name, kind, method_name, scope,
300
+ overriding: true)
301
+ overridden = ancestor && lookup_method_on(environment, ancestor, kind, method_name)
302
+ overridden unless overridden.nil? || object_surface?(overridden)
303
+ end
304
+
305
+ def object_surface?(method_definition)
306
+ OBJECT_SURFACE_OWNERS.include?(method_definition.defined_in.to_s.delete_prefix("::"))
307
+ end
308
+
309
+ # A class object carries no type arguments, so a singleton declaration binds none from the receiver.
310
+ NO_RECEIVER_TYPE_VARS = {}.freeze
311
+ private_constant :NO_RECEIVER_TYPE_VARS
312
+
313
+ # Issue #2884 — the return of a call on `Singleton[class_name]` through `method_definition`, a singleton
314
+ # declaration {DeclaredConstructor} found on `class_name` or an ancestor, so `instance` and `self` read
315
+ # `class_name` whichever class wrote it. Nil when the declaration states no return
316
+ # (`%a{rigor:v1:inferred-return}`).
317
+ def declared_singleton_return(context, method_definition, class_name)
318
+ return nil if RbsExtended.inferred_return?(method_definition)
319
+
320
+ translate_return_type(
321
+ method_definition,
322
+ class_name: class_name, kind: :singleton, method_name: context.method_name, args: context.args || [],
323
+ type_vars: NO_RECEIVER_TYPE_VARS, block_type: context.block_type, environment: context.environment,
324
+ scope: context.scope, call_node: context.call_node
325
+ )
326
+ end
327
+
254
328
  # rubocop:disable-next Metrics/ClassLength
255
329
  class << self
256
330
  private
257
331
 
332
+ # Whether some allow-listed ancestor's RBS declarations declare `method_name` outside
333
+ # {OBJECT_SURFACE_OWNERS}: the one-probe gate that leaves a `def` no ancestor declares, the common case
334
+ # in a project with no plugin class, on the cost it had before #2681. Read from the declarations, so a
335
+ # gem signature that fails to build reports nothing more than it did.
336
+ def rbs_complete_member_name?(environment, method_name)
337
+ rbs_complete_member_names(environment).include?(method_name)
338
+ end
339
+
340
+ def rbs_complete_member_names(environment)
341
+ return collect_rbs_complete_member_names(environment) if MemoAudit.enabled? && MemoAudit.suspended?
342
+
343
+ table = TypingState.current.weak_key_map(RBS_COMPLETE_MEMBER_NAMES)
344
+ names = table[environment]
345
+ if names.nil?
346
+ return table[environment] = MemoAudit.key_audit("rbs complete member names") do
347
+ collect_rbs_complete_member_names(environment)
348
+ end
349
+ end
350
+
351
+ MemoAudit.check("rbs complete member names", names, :environment) do
352
+ collect_rbs_complete_member_names(environment)
353
+ end
354
+ end
355
+
356
+ def collect_rbs_complete_member_names(environment)
357
+ loader = environment.rbs_loader
358
+ # An RBS-blind environment declares nothing, so no override takes an ancestor's parameters.
359
+ return Set.new.freeze if loader.nil?
360
+
361
+ ancestors = ALLOWED_RBS_COMPLETE_ANCESTORS + (environment.plugin_registry&.rbs_complete_ancestors || [])
362
+ ancestors.each_with_object(Set.new) do |ancestor, names|
363
+ names.merge(loader.declared_method_names(ancestor, skip: OBJECT_SURFACE_OWNERS))
364
+ end.freeze
365
+ end
366
+
258
367
  def dispatch_for(receiver:, method_name:, args:, environment:, block_type:, self_type_override: nil, # rubocop:disable Metrics/ParameterLists
259
368
  public_only: false, scope: nil, call_node: nil)
260
369
  args ||= []
@@ -284,14 +393,17 @@ module Rigor
284
393
  # Issue #1794 — a sequence's inherited Enumerable surface reads its Rigor-private element argument, with
285
394
  # `self` still the sequence.
286
395
  element = ArithmeticSequenceElements.element_receiver(receiver, method_name, environment)
287
- self_type_override ||= receiver if element
396
+ self_type_override = sequence_self(element, receiver, self_type_override)
288
397
  receiver = element || receiver
289
398
  descriptor = receiver_descriptor(receiver)
290
399
  return nil unless descriptor
291
400
 
292
401
  class_name, kind, receiver_args = descriptor
293
- method_definition = lookup_method(environment, class_name, kind, method_name, scope,
294
- call_node: call_node)
402
+ if kind == :interface
403
+ return dispatch_interface(receiver, class_name, receiver_args, method_name, args, environment, block_type,
404
+ scope: scope, call_node: call_node)
405
+ end
406
+ method_definition = lookup_method(environment, class_name, kind, method_name, scope, call_node: call_node)
295
407
  return nil unless method_definition
296
408
  return nil if public_only && method_private?(method_definition)
297
409
  # Issue #823 — a declaration that states the member's presence and parameters but not its
@@ -307,17 +419,11 @@ module Rigor
307
419
  type_vars = build_type_vars(environment, class_name, receiver_args)
308
420
  translate_return_type(
309
421
  method_definition,
310
- class_name: class_name,
311
- kind: kind,
312
- method_name: method_name,
313
- args: args,
314
- type_vars: type_vars,
315
- block_type: block_type,
316
- environment: environment,
422
+ class_name: class_name, kind: kind, method_name: method_name, args: args, type_vars: type_vars,
423
+ block_type: block_type, environment: environment,
317
424
  self_type_override: self_type_override ||
318
425
  SelfSubstitute.for(receiver, receiver_args, method_name, args, block_type),
319
- scope: scope,
320
- call_node: call_node
426
+ scope: scope, call_node: call_node
321
427
  )
322
428
  rescue StandardError
323
429
  # Defensive: if RBS' definition builder raises on a broken hierarchy (e.g., partially loaded
@@ -325,6 +431,55 @@ module Rigor
325
431
  nil
326
432
  end
327
433
 
434
+ # The `self` a call on a sequence's element keeps: the sequence (#1794), unless the caller already set one.
435
+ # A method rather than a two-value return, which would allocate an Array on every dispatch.
436
+ def sequence_self(element, receiver, self_type_override)
437
+ element ? (self_type_override || receiver) : self_type_override
438
+ end
439
+
440
+ # ADR-130 WD3 — a call on a `Rigor::Plugin` interface value (`Dynamic[Interface]`). A member the interface
441
+ # does not declare declines, so the call falls through the later tiers as one on `Dynamic[top]` does:
442
+ # the value's class is wider than the interface, and reporting such a call is #2686's opt-in rule. A
443
+ # declared member answers through the overloads that take the call's shape ({InterfaceShape}), with
444
+ # `self` the receiver and `instance` untyped. A block-pass that may be nil may run the call block-less, so
445
+ # it answers only when both shapes are taken and their returns agree.
446
+ # rubocop:disable-next Metrics/ParameterLists
447
+ def dispatch_interface(receiver, name, receiver_args, method_name, args, environment, block_type, scope:,
448
+ call_node:)
449
+ definition = interface_method(environment, name, method_name)
450
+ return nil if definition.nil?
451
+
452
+ type_vars = interface_type_vars(environment, name, receiver_args)
453
+ shape = lambda do |shape_block|
454
+ accepted = InterfaceShape.accepting(definition, args, block: !shape_block.nil?, call_node: call_node)
455
+ accepted && translate_return_type(
456
+ accepted, class_name: name, kind: :interface, method_name: method_name, args: args,
457
+ type_vars: type_vars, block_type: shape_block, environment: environment,
458
+ self_type_override: receiver, scope: scope, call_node: call_node
459
+ )
460
+ end
461
+ return shape.call(block_type) unless InterfaceShape.nilable_block_pass?(call_node, scope)
462
+
463
+ block_less = shape.call(nil)
464
+ block_less if block_less && block_less == shape.call(block_type || Type::Combinator.untyped)
465
+ end
466
+
467
+ # The interface's declaration of `method_name`, or nil when it declares none.
468
+ def interface_method(environment, name, method_name)
469
+ environment.rbs_loader.interface_definition(name)&.methods&.[](method_name.to_sym)
470
+ end
471
+
472
+ # The interface's own type parameters bound to the value's type arguments, as {#build_type_vars} binds a
473
+ # class's: a parameter left unbound (a defaulted one) reads `Dynamic[top]`.
474
+ def interface_type_vars(environment, name, receiver_args)
475
+ return NO_TYPE_VARS if receiver_args.empty?
476
+
477
+ param_names = environment.rbs_loader.interface_definition(name)&.type_params || []
478
+ return NO_TYPE_VARS if param_names.empty? || receiver_args.size > param_names.size
479
+
480
+ param_names.first(receiver_args.size).zip(receiver_args).to_h
481
+ end
482
+
328
483
  # Issue #661 — `Kernel#<=>: (untyped other) -> 0?` is the IDENTITY comparison: `0` when the two
329
484
  # are the same object, `nil` otherwise, and correct as written for a bare `Object`. Reached by
330
485
  # inheritance it stops being a statement about the call. `1.day <=> 2.days` typed as `0?`, and a
@@ -408,10 +563,18 @@ module Rigor
408
563
  # (ConstantFolding, ShapeDispatch#dispatch_integer_range) do not own.
409
564
  ["Integer", :instance, []]
410
565
  when Type::Dynamic
411
- receiver_descriptor(receiver.static_facet)
566
+ facet_descriptor(receiver.static_facet)
412
567
  end
413
568
  end
414
569
 
570
+ # A `Dynamic`'s facet names what the facet names, and a `Rigor::Plugin` interface facet names the interface
571
+ # (ADR-130 WD3), unless dispatch is switched off, where it names nothing, as `top` does.
572
+ def facet_descriptor(facet)
573
+ return receiver_descriptor(facet) unless facet.is_a?(Type::Interface)
574
+
575
+ [facet.name, :interface, facet.type_args] if interface_dispatch?
576
+ end
577
+
415
578
  # ADR-48 — project a `Data`/`Struct` member carrier to its tagging class (or the `Data`/`Struct`
416
579
  # supertype) so non-member calls (`inspect`, `==`, `frozen?`, ...) resolve through RBS rather
417
580
  # than mis-firing undefined-method. Precise member reads were already folded by DataFolding /
@@ -515,7 +678,10 @@ module Rigor
515
678
  # ADR-43 WD4 — the allow-list's manifest-declared half: a loaded plugin may name its own
516
679
  # contract classes in `rbs_complete_ancestors:` (e.g. rigor-graphql's `GraphQL::Schema::Object`),
517
680
  # extending the engine's hard-coded seed without editing this constant.
518
- def allowed_rbs_complete_ancestor(environment, class_name, kind, method_name, scope)
681
+ #
682
+ # #2681 — `overriding: true` asks for the declaration a `def` on `class_name` overrides (its parameter
683
+ # binding), so `class_name`'s own definition of the name is set aside; every nearer ancestor still shadows.
684
+ def allowed_rbs_complete_ancestor(environment, class_name, kind, method_name, scope, overriding: false)
519
685
  return nil if scope.nil?
520
686
  return nil if Rigor::Reflection.rbs_class_known?(class_name, environment: environment)
521
687
 
@@ -524,15 +690,22 @@ module Rigor
524
690
  # this guard the bridge would resolve e.g. `field` on `GraphQL::Schema::Object` while
525
691
  # the runtime actually calls a user `def self.field` on an intermediate `BaseObject` —
526
692
  # a wrong return type and a false `undefined-method`/arity reading downstream.
527
- return nil if scope.discovered_method?(class_name, method_name, kind)
693
+ return nil if !overriding && scope.discovered_method?(class_name, method_name, kind)
528
694
 
529
695
  registry = environment&.plugin_registry
530
696
  each_source_ancestor_candidate(scope, class_name) do |candidate|
531
- return nil if scope.discovered_method?(candidate, method_name, kind)
532
- next unless ALLOWED_RBS_COMPLETE_ANCESTORS.include?(candidate) ||
533
- registry&.rbs_complete_ancestor?(candidate)
534
-
535
- return shadowed_in_ruby_order?(scope, class_name, kind, method_name, candidate) ? nil : candidate
697
+ # #2681 — the engine seed's own source (`lib/rigor/plugin/base.rb`, which this repository's plugin
698
+ # check discovers) is the implementation its RBS describes, not a nearer definer. A project `def` on
699
+ # a manifest-declared ancestor is a reopen (`class Grape::API; def self.desc`), which Ruby calls
700
+ # instead of the declaration, so it still shadows.
701
+ seed = ALLOWED_RBS_COMPLETE_ANCESTORS.include?(candidate)
702
+ return nil if !seed && scope.discovered_method?(candidate, method_name, kind)
703
+ next unless seed || registry&.rbs_complete_ancestor?(candidate)
704
+
705
+ return nil if shadowed_in_ruby_order?(scope, class_name, kind, method_name, candidate,
706
+ overriding: overriding)
707
+
708
+ return candidate
536
709
  end
537
710
  nil
538
711
  end
@@ -546,7 +719,8 @@ module Rigor
546
719
  # passed over, as the breadth-first walk always did. An unsettled chain withholds too: a mixin edge the
547
720
  # tables cannot place (a receiver-form `Demo.include(M)` after the body, an edge in a conditional, a block or
548
721
  # a hook) may put a definer ahead of the ancestor.
549
- def shadowed_in_ruby_order?(scope, class_name, kind, method_name, ancestor)
722
+ # With `overriding`, `class_name`'s own entry is the overriding `def` itself and does not shadow.
723
+ def shadowed_in_ruby_order?(scope, class_name, kind, method_name, ancestor, overriding: false)
550
724
  chain = Scope::ResolutionChain.for(scope, class_name.to_s, kind, :methods)
551
725
  return true if chain.unsettled?
552
726
 
@@ -557,6 +731,8 @@ module Rigor
557
731
  return true if stop.nil?
558
732
 
559
733
  entries.first(stop).any? do |entry|
734
+ next false if overriding && !entry.external? && entry.name == class_name.to_s
735
+
560
736
  if entry.external?
561
737
  rbs_declares_ahead?(scope, entry, method_name)
562
738
  else
@@ -619,12 +795,22 @@ module Rigor
619
795
  return nil if scope.nil? || kind != :instance
620
796
  return nil if environment.nil?
621
797
 
798
+ if MemoAudit.enabled? && MemoAudit.suspended?
799
+ return compute_core_stdlib_ancestor_method(environment, class_name, method_name, scope)
800
+ end
801
+
622
802
  memo = core_stdlib_memo(environment, scope)
623
803
  key = [class_name.to_s, method_name.to_sym]
624
- return memo[key] if memo.key?(key)
804
+ if memo.key?(key)
805
+ return MemoAudit.check("core stdlib ancestor dispatch", memo[key], key[0], key[1]) do
806
+ compute_core_stdlib_ancestor_method(environment, class_name, method_name, scope)
807
+ end
808
+ end
625
809
 
626
810
  store_unless_recorded(memo, key) do
627
- compute_core_stdlib_ancestor_method(environment, class_name, method_name, scope)
811
+ MemoAudit.key_audit("core stdlib ancestor dispatch") do
812
+ compute_core_stdlib_ancestor_method(environment, class_name, method_name, scope)
813
+ end
628
814
  end
629
815
  end
630
816
 
@@ -651,12 +837,23 @@ module Rigor
651
837
  return nil if scope.nil? || kind != :instance
652
838
  return nil if environment.nil?
653
839
 
840
+ if MemoAudit.enabled? && MemoAudit.suspended?
841
+ return compute_included_module_method(environment, class_name, method_name,
842
+ scope)
843
+ end
844
+
654
845
  memo = mixin_ancestor_memo(environment, scope)
655
846
  key = [class_name.to_s, method_name.to_sym]
656
- return memo[key] if memo.key?(key)
847
+ if memo.key?(key)
848
+ return MemoAudit.check("mixin ancestor dispatch", memo[key], key[0], key[1]) do
849
+ compute_included_module_method(environment, class_name, method_name, scope)
850
+ end
851
+ end
657
852
 
658
853
  store_unless_recorded(memo, key) do
659
- compute_included_module_method(environment, class_name, method_name, scope)
854
+ MemoAudit.key_audit("mixin ancestor dispatch") do
855
+ compute_included_module_method(environment, class_name, method_name, scope)
856
+ end
660
857
  end
661
858
  end
662
859
 
@@ -684,11 +881,18 @@ module Rigor
684
881
 
685
882
  # The same one-slot memo shape as {#core_stdlib_memo}; see there for why it is one slot keyed
686
883
  # on the discovery index's identity, and what a recording run may store in it.
687
- MIXIN_ANCESTOR_MEMO_KEY = RunScopedSlots.register(:__rigor_mixin_ancestor_dispatch__)
688
- private_constant :MIXIN_ANCESTOR_MEMO_KEY
884
+ #
885
+ # ADR-129 WD1 — a run-scoped Memo, keyed as {#ancestor_memo_slot} says.
886
+ MIXIN_ANCESTOR_MEMO = TypingState.memo(
887
+ :mixin_ancestor_dispatch,
888
+ run_scoped: true, audit: "mixin ancestor dispatch",
889
+ reason: "one slot keyed on the identities of the discovery index and environment, and " \
890
+ "on whether the thread is recording; the answers read nothing else"
891
+ )
892
+ private_constant :MIXIN_ANCESTOR_MEMO
689
893
 
690
894
  def mixin_ancestor_memo(environment, scope)
691
- ancestor_memo_slot(MIXIN_ANCESTOR_MEMO_KEY, environment, scope)
895
+ ancestor_memo_slot(MIXIN_ANCESTOR_MEMO, environment, scope)
692
896
  end
693
897
 
694
898
  # The declines are conjunctive, so their ORDER is free — and it is chosen so the two that walk
@@ -858,23 +1062,31 @@ module Rigor
858
1062
  # the measurement. A `Scope` hands each analysed file its own discovery index, so an
859
1063
  # identity-keyed store would pin every file's index, and every RBS definition resolved
860
1064
  # against it, for the length of the run.
861
- CORE_STDLIB_ANCESTOR_MEMO_KEY = RunScopedSlots.register(:__rigor_core_stdlib_ancestor_dispatch__)
862
- private_constant :CORE_STDLIB_ANCESTOR_MEMO_KEY
1065
+ #
1066
+ # ADR-129 WD1 — a run-scoped Memo, keyed as {#ancestor_memo_slot} says.
1067
+ CORE_STDLIB_ANCESTOR_MEMO = TypingState.memo(
1068
+ :core_stdlib_ancestor_dispatch,
1069
+ run_scoped: true, audit: "core stdlib ancestor dispatch",
1070
+ reason: "one slot keyed on the identities of the discovery index and environment, and " \
1071
+ "on whether the thread is recording; the answers read nothing else"
1072
+ )
1073
+ private_constant :CORE_STDLIB_ANCESTOR_MEMO
863
1074
 
864
1075
  def core_stdlib_memo(environment, scope)
865
- ancestor_memo_slot(CORE_STDLIB_ANCESTOR_MEMO_KEY, environment, scope)
1076
+ ancestor_memo_slot(CORE_STDLIB_ANCESTOR_MEMO, environment, scope)
866
1077
  end
867
1078
 
868
1079
  # The slot behind both memos. Whether this thread is recording is part of its key, so a slot
869
1080
  # filled with every answer is never read by a recording caller, which may only reuse the
870
1081
  # answers {#store_unless_recorded} lets through.
871
- def ancestor_memo_slot(memo_key, environment, scope)
1082
+ def ancestor_memo_slot(memo_slot, environment, scope)
872
1083
  discovery = scope.discovery
873
1084
  recording = Rigor::Analysis::DependencyRecorder.recording?
874
- slot = Thread.current[memo_key]
1085
+ state = TypingState.current
1086
+ slot = state[memo_slot]
875
1087
  unless slot && slot[0].equal?(discovery) && slot[1].equal?(environment) && slot[2] == recording
876
1088
  slot = [discovery, environment, recording, {}]
877
- Thread.current[memo_key] = slot
1089
+ state[memo_slot] = slot
878
1090
  end
879
1091
  slot[3]
880
1092
  end
@@ -1684,7 +1896,8 @@ module Rigor
1684
1896
  #
1685
1897
  # * only a positional parameter (required or optional) whose declared type is EXACTLY
1686
1898
  # `RBS::Types::Variable`, plus the one container shape issue #834 admits (see
1687
- # {#range_element_binding}) — no general container walk, so `(Array[T]) -> T` still degrades;
1899
+ # {#range_element_binding}) and the class-object shape issue #2828 admits ({#singleton_param_binding}) —
1900
+ # no general container walk, so `(Array[T]) -> T` still degrades;
1688
1901
  # * only names the SELECTED overload declares in its own `type_params` (a class-level variable
1689
1902
  # keeps its receiver-derived binding);
1690
1903
  # * an existing key wins, so the receiver and the block return type both outrank an argument;
@@ -1693,7 +1906,7 @@ module Rigor
1693
1906
  #
1694
1907
  # See {#arg_binding_permitted?} for why the contribution is gated on the call site.
1695
1908
  def compose_arg_type_vars(method_type, type_vars, args, scope:, call_node:, call_site:)
1696
- bindings = arg_type_var_bindings(method_type, type_vars, args)
1909
+ bindings = arg_type_var_bindings(method_type, type_vars, args, scope&.environment, call_node)
1697
1910
  return type_vars if bindings.empty?
1698
1911
  return type_vars unless arg_binding_permitted?(scope, call_node, call_site)
1699
1912
 
@@ -1702,7 +1915,7 @@ module Rigor
1702
1915
 
1703
1916
  # The candidate bindings, computed before the guard so a non-generic overload (the overwhelming
1704
1917
  # majority) never pays for the `scope` probes.
1705
- def arg_type_var_bindings(method_type, type_vars, args)
1918
+ def arg_type_var_bindings(method_type, type_vars, args, environment = nil, call_node = nil)
1706
1919
  declared = declared_type_param_names(method_type)
1707
1920
  return EMPTY_TYPE_VARS if declared.empty? || args.empty?
1708
1921
 
@@ -1714,6 +1927,11 @@ module Rigor
1714
1927
  next if arg.nil?
1715
1928
 
1716
1929
  name, bound = param_binding(param, arg, declared, type_vars)
1930
+ if name.nil?
1931
+ name, bound = singleton_param_binding(method_type, param, arg, declared, type_vars,
1932
+ environment)
1933
+ next if name && class_factory_argument?(call_node)
1934
+ end
1717
1935
  next if name.nil?
1718
1936
 
1719
1937
  bound = Type::Combinator.widen_value_pinned(bound) if upper_bounded?(method_type, name)
@@ -1749,6 +1967,80 @@ module Rigor
1749
1967
  range_element_binding(param, arg, declared, type_vars)
1750
1968
  end
1751
1969
 
1970
+ # Issue #2828 — a positional parameter spelled `singleton(T)` for a method-level variable `T` binds `T`
1971
+ # to the instance type of the class object the call passes: `Resolv::DNS#getresources: [T < Resource]
1972
+ # (dns_name, singleton(T)) -> Array[T]` answers `Array[Resolv::DNS::Resource::IN::MX]` for an
1973
+ # `IN::MX` argument. A union of class objects binds the union of their instances, as a bare `T` binds
1974
+ # its argument union. Every other argument — `Dynamic`, an instance, a union with a non-class member —
1975
+ # binds nothing, and neither does a class the declared upper bound does not provably admit: an
1976
+ # interface, alias or other non-class bound, or a class the environment cannot order below the bound.
1977
+ def singleton_param_binding(method_type, param, arg, declared, type_vars, environment)
1978
+ name = singleton_variable_name(param, declared, type_vars)
1979
+ return NO_BINDING if name.nil?
1980
+
1981
+ members = arg.is_a?(Type::Union) ? arg.members : [arg]
1982
+ return NO_BINDING unless members.all?(Type::Singleton)
1983
+
1984
+ within = members.all? { |member| within_upper_bound?(method_type, name, member, environment) }
1985
+ return NO_BINDING unless within
1986
+
1987
+ instances = members.map { |member| Type::Combinator.nominal_of(member.class_name) }
1988
+ [name, instances.size == 1 ? instances.first : Type::Combinator.union(*instances)]
1989
+ end
1990
+
1991
+ # RBS parses `singleton(T)` as a class singleton whose type name is the bare, unqualified `T`; the
1992
+ # parameter names a variable when that name is one the overload declares and has not yet bound.
1993
+ def singleton_variable_name(param, declared, type_vars)
1994
+ declared_type = param.type
1995
+ return nil unless declared_type.is_a?(RBS::Types::ClassSingleton)
1996
+
1997
+ type_name = declared_type.name
1998
+ return nil unless type_name.namespace.empty?
1999
+
2000
+ name = type_name.name
2001
+ return nil unless declared.include?(name)
2002
+ return nil if type_vars.key?(name)
2003
+
2004
+ name
2005
+ end
2006
+
2007
+ # Whether the call passes, as any positional argument, a class built by `Class.new`, `Struct.new` or
2008
+ # `Data.define` with a block. The engine types `Class.new(Widget) { def extra = 1 }` as `singleton(Widget)`,
2009
+ # so binding `T` from it would read the instance as a plain `Widget` and report `extra` as undefined. The
2010
+ # argument node is visible here; a local the class was assigned to is not, and still binds.
2011
+ def class_factory_argument?(call_node)
2012
+ arguments = call_node.respond_to?(:arguments) ? call_node.arguments&.arguments : nil
2013
+ return false if arguments.nil?
2014
+
2015
+ arguments.any? { |argument| class_factory_call?(argument) }
2016
+ end
2017
+
2018
+ CLASS_FACTORIES = { new: %w[Class Struct].freeze, define: %w[Data].freeze }.freeze
2019
+ private_constant :CLASS_FACTORIES
2020
+
2021
+ def class_factory_call?(node)
2022
+ return false unless node.is_a?(Prism::CallNode) && node.block.is_a?(Prism::BlockNode)
2023
+
2024
+ receivers = CLASS_FACTORIES[node.name]
2025
+ return false if receivers.nil?
2026
+
2027
+ receiver = node.receiver
2028
+ name = case receiver
2029
+ when Prism::ConstantReadNode then receiver.name.to_s
2030
+ when Prism::ConstantPathNode then receiver.parent.nil? ? receiver.name.to_s : nil
2031
+ end
2032
+ receivers.include?(name)
2033
+ end
2034
+
2035
+ def within_upper_bound?(method_type, name, singleton, environment)
2036
+ type_param = method_type.type_params.find { |tp| tp.name == name }
2037
+ bound = type_param.respond_to?(:upper_bound_type) ? type_param.upper_bound_type : type_param.upper_bound
2038
+ return true if bound.nil?
2039
+ return false unless bound.is_a?(RBS::Types::ClassInstance) && environment
2040
+
2041
+ %i[equal subclass].include?(environment.class_ordering(singleton.class_name, bound.name.to_s))
2042
+ end
2043
+
1752
2044
  def declared_type_param_names(method_type)
1753
2045
  params = method_type.respond_to?(:type_params) ? method_type.type_params : nil
1754
2046
  return EMPTY_TYPE_PARAM_NAMES if params.nil? || params.empty?
@@ -1824,7 +2116,7 @@ module Rigor
1824
2116
  # dress up an absence of evidence as an inference. The variable stays unbound and degrades as it
1825
2117
  # did before, which is the same value anyway.
1826
2118
  def no_static_evidence?(arg)
1827
- arg.is_a?(Type::Dynamic) && arg.static_facet.is_a?(Type::Top)
2119
+ Type::Combinator.untyped?(arg)
1828
2120
  end
1829
2121
 
1830
2122
  # Issue #303 — the FP guard on the argument-position binding.
@@ -1949,13 +2241,11 @@ module Rigor
1949
2241
 
1950
2242
  def probe_block_param_types(receiver:, method_name:, args:, environment:, scope: nil, call_node: nil)
1951
2243
  args ||= []
1952
- keywords_last = ArgumentNodes.keyword_hash_last?(call_node, args)
1953
- splat = splat_indices(call_node, args)
1954
2244
  case receiver
1955
2245
  when Type::Union
1956
- probe_block_param_types_union(receiver, method_name, args, environment, scope, keywords_last, splat)
2246
+ probe_block_param_types_union(receiver, method_name, args, environment, scope, call_node)
1957
2247
  else
1958
- probe_block_param_types_one(receiver, method_name, args, environment, scope, keywords_last, splat)
2248
+ probe_block_param_types_one(receiver, method_name, args, environment, scope, call_node)
1959
2249
  end
1960
2250
  end
1961
2251
 
@@ -1963,9 +2253,9 @@ module Rigor
1963
2253
  # member resolves the same arity and types (otherwise the call sites would have to thread
1964
2254
  # per-member binders, which the slice does not support yet). Mismatches degrade to the empty
1965
2255
  # array so the binder defaults all params to Dynamic[Top].
1966
- def probe_block_param_types_union(receiver, method_name, args, environment, scope, keywords_last, splat)
2256
+ def probe_block_param_types_union(receiver, method_name, args, environment, scope, call_node)
1967
2257
  results = receiver.members.map do |member|
1968
- probe_block_param_types_one(member, method_name, args, environment, scope, keywords_last, splat)
2258
+ probe_block_param_types_one(member, method_name, args, environment, scope, call_node)
1969
2259
  end
1970
2260
  return [] if results.empty?
1971
2261
  return [] unless results.all? { |r| r == results.first }
@@ -1973,44 +2263,52 @@ module Rigor
1973
2263
  results.first
1974
2264
  end
1975
2265
 
1976
- def probe_block_param_types_one(receiver, method_name, args, environment, scope, keywords_last, splat)
2266
+ def probe_block_param_types_one(receiver, method_name, args, environment, scope, call_node)
1977
2267
  receiver = ArithmeticSequenceElements.element_receiver(receiver, method_name, environment, block: true) ||
1978
2268
  receiver
1979
2269
  descriptor = receiver_descriptor(receiver)
1980
2270
  return [] unless descriptor
1981
2271
 
1982
2272
  class_name, kind, receiver_args = descriptor
1983
- method_definition = lookup_method(environment, class_name, kind, method_name, scope)
2273
+ method_definition = probe_definition(environment, class_name, kind, method_name, args, scope, call_node)
1984
2274
  return [] unless method_definition
1985
2275
 
1986
- type_vars = build_type_vars(environment, class_name, receiver_args)
1987
- extract_block_param_types(
2276
+ type_vars =
2277
+ if kind == :interface
2278
+ interface_type_vars(environment, class_name, receiver_args)
2279
+ else
2280
+ build_type_vars(environment, class_name, receiver_args)
2281
+ end
2282
+ params = extract_block_param_types(
1988
2283
  method_definition,
1989
- class_name: class_name,
1990
- kind: kind,
1991
- args: args,
1992
- type_vars: type_vars,
1993
- environment: environment,
1994
- receiver: receiver,
1995
- receiver_args: receiver_args,
1996
- method_name: method_name,
1997
- keywords_last: keywords_last, positional_splat: splat
2284
+ class_name: class_name, kind: kind, args: args, type_vars: type_vars, environment: environment,
2285
+ receiver: receiver, receiver_args: receiver_args, method_name: method_name,
2286
+ keywords_last: ArgumentNodes.keyword_hash_last?(call_node, args),
2287
+ positional_splat: splat_indices(call_node, args), scope: scope, call_node: call_node
1998
2288
  )
2289
+ ClassKeyedBlockParams.rebind(method_definition, method_name, args, params)
1999
2290
  rescue StandardError
2000
2291
  []
2001
2292
  end
2002
2293
 
2294
+ # The declaration the block-parameter probe reads. On an interface value a block literal binds its parameters
2295
+ # through the overloads that take the call's shape, as the call's return does (ADR-130 WD3,
2296
+ # {#dispatch_interface}).
2297
+ def probe_definition(environment, class_name, kind, method_name, args, scope, call_node)
2298
+ return lookup_method(environment, class_name, kind, method_name, scope) unless kind == :interface
2299
+
2300
+ declared = interface_method(environment, class_name, method_name)
2301
+ declared && InterfaceShape.accepting(declared, args, block: true, call_node: call_node)
2302
+ end
2303
+
2003
2304
  # rubocop:disable Metrics/ParameterLists
2004
2305
  def extract_block_param_types(method_definition, class_name:, kind:, args:, type_vars:,
2005
2306
  environment: nil, receiver: nil, receiver_args: [],
2006
- method_name: nil, keywords_last: false, positional_splat: nil)
2307
+ method_name: nil, keywords_last: false, positional_splat: nil,
2308
+ scope: nil, call_node: nil)
2007
2309
  # rubocop:enable Metrics/ParameterLists
2008
2310
  instance_type = Type::Combinator.nominal_of(class_name)
2009
- self_type =
2010
- case kind
2011
- when :singleton then Type::Combinator.singleton_of(class_name)
2012
- else instance_type
2013
- end
2311
+ self_type = block_self_type(kind, class_name, instance_type, receiver)
2014
2312
 
2015
2313
  # Issue #1130 — a block parameter that receives `self` (`Object#tap` yields it) must see the
2016
2314
  # receiver's type arguments through the SAME {SelfSubstitute} verdict the return path applies
@@ -2052,9 +2350,53 @@ module Rigor
2052
2350
  )
2053
2351
  return [] unless method_type
2054
2352
 
2353
+ type_vars = compose_singleton_arg_type_vars(method_type, type_vars, args, environment,
2354
+ scope: scope, call_node: call_node,
2355
+ call_site: [class_name, method_name, kind])
2055
2356
  block_params_of(method_type, self_type, instance_type, type_vars, environment) || []
2056
2357
  end
2057
2358
 
2359
+ # Issue #2828 — the block-parameter twin of {#compose_arg_type_vars}, for the `singleton(T)` shape alone
2360
+ # ({#singleton_param_binding}): `Resolv::DNS#each_resource: [T < Resource] (dns_name, singleton(T))
2361
+ # { (T) -> void } -> void` yields the typeclass's instances. A bare `T` parameter stays out of the block
2362
+ # path: it binds the argument's own (possibly value-pinned) type, and `each_with_object([])`'s memo
2363
+ # would read as the empty tuple the block then appends to.
2364
+ def compose_singleton_arg_type_vars(method_type, type_vars, args, environment, scope:, call_node:, call_site:)
2365
+ declared = declared_type_param_names(method_type)
2366
+ return type_vars if declared.empty? || args.empty?
2367
+
2368
+ fun = method_type.type
2369
+ return type_vars unless fun.respond_to?(:required_positionals)
2370
+
2371
+ bindings = singleton_bindings(method_type, fun, args, declared, type_vars, environment)
2372
+ return type_vars if bindings.empty? || class_factory_argument?(call_node)
2373
+ return type_vars unless arg_binding_permitted?(scope, call_node, call_site)
2374
+
2375
+ type_vars.merge(bindings)
2376
+ end
2377
+
2378
+ def singleton_bindings(method_type, fun, args, declared, type_vars, environment)
2379
+ positionals = fun.required_positionals + fun.optional_positionals
2380
+ positionals.zip(args).each_with_object({}) do |(param, arg), bindings|
2381
+ next if arg.nil?
2382
+
2383
+ name, bound = singleton_param_binding(method_type, param, arg, declared, type_vars, environment)
2384
+ next if name.nil?
2385
+
2386
+ bindings[name] = bindings.key?(name) ? Type::Combinator.union(bindings[name], bound) : bound
2387
+ end
2388
+ end
2389
+
2390
+ # What RBS `self` reads in a block parameter of the member: the class's instance or singleton, and inside an
2391
+ # interface member the receiver (ADR-130 WD1).
2392
+ def block_self_type(kind, class_name, instance_type, receiver)
2393
+ case kind
2394
+ when :singleton then Type::Combinator.singleton_of(class_name)
2395
+ when :interface then receiver
2396
+ else instance_type
2397
+ end
2398
+ end
2399
+
2058
2400
  # The block parameters every overload in `matches` declares alike, or no information (`[]`).
2059
2401
  def agreed_block_params(matches, self_type, instance_type, type_vars, environment)
2060
2402
  answers = matches.uniq.map do |method_type|