rigortype 0.3.9 → 0.4.0

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 (363) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/core_overlay/enumerable.rbs +51 -0
  4. data/data/core_overlay/enumerator.rbs +84 -0
  5. data/data/core_overlay/hash_rbs3.rbs +41 -0
  6. data/data/core_overlay/process.rbs +40 -0
  7. data/data/core_overlay/string_io.rbs +33 -0
  8. data/data/effects/core.yml +3 -3
  9. data/data/gem_overlay/activesupport/core_ext.rbs +127 -4
  10. data/docs/handbook/03-narrowing.md +95 -10
  11. data/docs/handbook/04-tuples-and-shapes.md +8 -6
  12. data/docs/handbook/07-rbs-and-extended.md +16 -11
  13. data/docs/handbook/10-sorbet.md +9 -10
  14. data/docs/handbook/11-sig-gen.md +454 -13
  15. data/docs/manual/02-cli-reference.md +63 -2
  16. data/docs/manual/03-configuration.md +7 -0
  17. data/docs/manual/04-diagnostics.md +4 -0
  18. data/docs/manual/07-plugins.md +1 -1
  19. data/docs/manual/10-mcp-server.md +3 -2
  20. data/docs/manual/16-rbs-extended-annotations.md +40 -7
  21. data/docs/manual/19-effect-labels.md +10 -0
  22. data/docs/manual/plugins/README.md +7 -0
  23. data/docs/manual/plugins/rigor-actioncable.md +8 -1
  24. data/docs/manual/plugins/rigor-actionmailer.md +7 -0
  25. data/docs/manual/plugins/rigor-actionpack.md +243 -0
  26. data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
  27. data/docs/manual/plugins/rigor-activejob.md +7 -0
  28. data/docs/manual/plugins/rigor-activerecord.md +110 -4
  29. data/docs/manual/plugins/rigor-activestorage.md +8 -1
  30. data/docs/manual/plugins/rigor-activesupport-core-ext.md +11 -5
  31. data/docs/manual/plugins/rigor-grape.md +106 -0
  32. data/docs/manual/plugins/rigor-graphql.md +23 -2
  33. data/docs/manual/plugins/rigor-pundit.md +8 -1
  34. data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
  35. data/docs/manual/plugins/rigor-rails-routes.md +7 -0
  36. data/docs/manual/plugins/rigor-rbs-inline.md +212 -25
  37. data/docs/manual/plugins/rigor-sidekiq.md +8 -1
  38. data/docs/manual/plugins/rigor-sorbet.md +21 -5
  39. data/exe/rigor +19 -4
  40. data/lib/rigor/analysis/baseline.rb +1 -1
  41. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
  42. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
  43. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -3
  44. data/lib/rigor/analysis/check_rules/rule_ids.rb +20 -5
  45. data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
  46. data/lib/rigor/analysis/check_rules/source_arity.rb +323 -0
  47. data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
  48. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +36 -2
  49. data/lib/rigor/analysis/check_rules.rb +520 -49
  50. data/lib/rigor/analysis/dependency_recorder.rb +23 -0
  51. data/lib/rigor/analysis/fact_store.rb +9 -0
  52. data/lib/rigor/analysis/incremental.rb +26 -0
  53. data/lib/rigor/analysis/incremental_session.rb +36 -4
  54. data/lib/rigor/analysis/project_scan.rb +11 -1
  55. data/lib/rigor/analysis/reachability/plugin_roots.rb +0 -1
  56. data/lib/rigor/analysis/reachability/scan_cache.rb +1 -0
  57. data/lib/rigor/analysis/rule_catalog.rb +127 -0
  58. data/lib/rigor/analysis/run_cache_key.rb +20 -11
  59. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +167 -11
  60. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -4
  61. data/lib/rigor/analysis/runner/pool_coordinator.rb +107 -15
  62. data/lib/rigor/analysis/runner/project_pre_passes.rb +26 -7
  63. data/lib/rigor/analysis/runner.rb +278 -20
  64. data/lib/rigor/analysis/template_unit_collector.rb +303 -0
  65. data/lib/rigor/analysis/template_unit_paths.rb +91 -0
  66. data/lib/rigor/analysis/template_unit_positions.rb +293 -0
  67. data/lib/rigor/analysis/template_units.rb +399 -0
  68. data/lib/rigor/analysis/worker_session.rb +53 -10
  69. data/lib/rigor/bleeding_edge.rb +0 -2
  70. data/lib/rigor/builtins/hkt_builtins.rb +1 -0
  71. data/lib/rigor/builtins/imported_refinements.rb +4 -0
  72. data/lib/rigor/builtins/regex_refinement.rb +17 -9
  73. data/lib/rigor/builtins/static_return_refinements.rb +2 -0
  74. data/lib/rigor/cache/descriptor.rb +6 -1
  75. data/lib/rigor/cache/engine_source.rb +29 -1
  76. data/lib/rigor/cache/incremental_snapshot.rb +33 -1
  77. data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
  78. data/lib/rigor/cache/rbs_descriptor.rb +37 -7
  79. data/lib/rigor/cache/store.rb +0 -1
  80. data/lib/rigor/ci_detector.rb +1 -0
  81. data/lib/rigor/cli/doc_links.rb +1 -1
  82. data/lib/rigor/cli/docs_command.rb +4 -4
  83. data/lib/rigor/cli/plugin_command.rb +3 -3
  84. data/lib/rigor/cli/prism_colorizer.rb +0 -1
  85. data/lib/rigor/cli/sig_gen_command.rb +210 -29
  86. data/lib/rigor/cli/skill_command.rb +1 -1
  87. data/lib/rigor/cli/skill_describe.rb +0 -1
  88. data/lib/rigor/cli/type_of_command.rb +19 -5
  89. data/lib/rigor/cli/type_of_renderer.rb +20 -9
  90. data/lib/rigor/cli/type_of_template_probe.rb +189 -0
  91. data/lib/rigor/cli.rb +21 -3
  92. data/lib/rigor/configuration/severity_profile.rb +19 -3
  93. data/lib/rigor/configuration.rb +66 -3
  94. data/lib/rigor/effects/ancestry_recorder.rb +191 -0
  95. data/lib/rigor/effects/attribution.rb +11 -2
  96. data/lib/rigor/effects/callee_rule.rb +368 -0
  97. data/lib/rigor/effects/catalog.rb +7 -4
  98. data/lib/rigor/effects/collector.rb +11 -5
  99. data/lib/rigor/effects/config_envelopes.rb +9 -2
  100. data/lib/rigor/effects/definition_context.rb +179 -0
  101. data/lib/rigor/effects/effect_table.rb +11 -3
  102. data/lib/rigor/effects/envelope_check.rb +1 -1
  103. data/lib/rigor/effects/envelope_index.rb +15 -0
  104. data/lib/rigor/effects/file_collection.rb +59 -5
  105. data/lib/rigor/effects/framework_units.rb +1 -1
  106. data/lib/rigor/effects/identity.rb +16 -0
  107. data/lib/rigor/effects/local_ownership.rb +38 -12
  108. data/lib/rigor/effects/method_key.rb +21 -0
  109. data/lib/rigor/effects/mutation_classifier.rb +23 -12
  110. data/lib/rigor/effects/plugin_facts.rb +43 -31
  111. data/lib/rigor/effects/propagator.rb +295 -16
  112. data/lib/rigor/effects/registry.rb +1 -1
  113. data/lib/rigor/effects/scanner.rb +121 -72
  114. data/lib/rigor/effects/signature_sources.rb +1 -1
  115. data/lib/rigor/effects/snapshot.rb +2 -1
  116. data/lib/rigor/effects/summary.rb +27 -4
  117. data/lib/rigor/effects/unit_scan.rb +385 -30
  118. data/lib/rigor/effects/visibility.rb +101 -0
  119. data/lib/rigor/environment/lockfile_resolver.rb +17 -0
  120. data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
  121. data/lib/rigor/environment/member_consistency.rb +298 -0
  122. data/lib/rigor/environment/rbs_loader.rb +399 -133
  123. data/lib/rigor/environment.rb +103 -38
  124. data/lib/rigor/hashing/xxh3.rb +264 -0
  125. data/lib/rigor/inference/acceptance.rb +139 -9
  126. data/lib/rigor/inference/block_auto_splat.rb +216 -0
  127. data/lib/rigor/inference/block_call_timing.rb +338 -0
  128. data/lib/rigor/inference/block_parameter_binder.rb +73 -27
  129. data/lib/rigor/inference/block_repetition.rb +71 -0
  130. data/lib/rigor/inference/body_fixpoint.rb +2 -1
  131. data/lib/rigor/inference/budget_trace.rb +2 -1
  132. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  133. data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
  134. data/lib/rigor/inference/captured_locals.rb +387 -15
  135. data/lib/rigor/inference/closure_escape_analyzer.rb +157 -13
  136. data/lib/rigor/inference/content_join.rb +200 -27
  137. data/lib/rigor/inference/def_return_typer.rb +11 -7
  138. data/lib/rigor/inference/define_method_block_self.rb +64 -0
  139. data/lib/rigor/inference/element_read_widening.rb +22 -10
  140. data/lib/rigor/inference/error_info.rb +196 -0
  141. data/lib/rigor/inference/expression_typer.rb +1532 -496
  142. data/lib/rigor/inference/external_ancestor_resolution.rb +267 -0
  143. data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
  144. data/lib/rigor/inference/global_write_census.rb +239 -0
  145. data/lib/rigor/inference/guard_rebinding.rb +447 -0
  146. data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
  147. data/lib/rigor/inference/index_write_widening.rb +16 -3
  148. data/lib/rigor/inference/indexed_narrowing.rb +61 -9
  149. data/lib/rigor/inference/jump_targets.rb +82 -0
  150. data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
  151. data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
  152. data/lib/rigor/inference/last_line.rb +340 -0
  153. data/lib/rigor/inference/last_status.rb +144 -0
  154. data/lib/rigor/inference/macro_block_self_type.rb +167 -17
  155. data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
  156. data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
  157. data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
  158. data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
  159. data/lib/rigor/inference/match_rebinding.rb +392 -0
  160. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  161. data/lib/rigor/inference/method_dispatcher/block_folding.rb +85 -24
  162. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
  163. data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
  164. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +4 -0
  165. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
  166. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +113 -73
  167. data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
  168. data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
  169. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +881 -38
  170. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
  171. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +128 -53
  172. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +1 -1
  173. data/lib/rigor/inference/method_dispatcher.rb +214 -13
  174. data/lib/rigor/inference/method_parameter_binder.rb +8 -3
  175. data/lib/rigor/inference/multi_target_binder.rb +340 -52
  176. data/lib/rigor/inference/mutation_rejoin.rb +4 -1
  177. data/lib/rigor/inference/mutation_widening.rb +70 -48
  178. data/lib/rigor/inference/narrowing.rb +540 -120
  179. data/lib/rigor/inference/operand_effects.rb +167 -0
  180. data/lib/rigor/inference/operand_walk.rb +88 -0
  181. data/lib/rigor/inference/optimistic_origin.rb +152 -9
  182. data/lib/rigor/inference/parameter_inference_collector.rb +3 -2
  183. data/lib/rigor/inference/project_method_ownership.rb +136 -0
  184. data/lib/rigor/inference/project_patched_methods.rb +7 -2
  185. data/lib/rigor/inference/project_patched_scanner.rb +7 -3
  186. data/lib/rigor/inference/receiver_alias.rb +90 -1
  187. data/lib/rigor/inference/receiver_blind_block.rb +219 -0
  188. data/lib/rigor/inference/refinement_mutation.rb +15 -11
  189. data/lib/rigor/inference/repeated_or_writes.rb +463 -0
  190. data/lib/rigor/inference/return_barrier.rb +54 -0
  191. data/lib/rigor/inference/rewrite_mutation.rb +120 -0
  192. data/lib/rigor/inference/scope_indexer.rb +4615 -551
  193. data/lib/rigor/inference/statement_evaluator.rb +3176 -490
  194. data/lib/rigor/inference/stored_block_call.rb +54 -0
  195. data/lib/rigor/inference/string_mutation.rb +44 -7
  196. data/lib/rigor/inference/unknown_store_widening.rb +200 -0
  197. data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
  198. data/lib/rigor/language_server/debouncer.rb +0 -1
  199. data/lib/rigor/language_server/diagnostic_publisher.rb +3 -2
  200. data/lib/rigor/language_server/hover_renderer.rb +3 -3
  201. data/lib/rigor/language_server/project_context.rb +5 -3
  202. data/lib/rigor/mcp/server.rb +2 -1
  203. data/lib/rigor/plugin/base.rb +168 -5
  204. data/lib/rigor/plugin/box_probe.rb +91 -0
  205. data/lib/rigor/plugin/bundled_catalog.rb +1 -1
  206. data/lib/rigor/plugin/effect_attribution.rb +58 -4
  207. data/lib/rigor/plugin/loader.rb +2 -1
  208. data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
  209. data/lib/rigor/plugin/manifest.rb +71 -10
  210. data/lib/rigor/plugin/registry.rb +35 -1
  211. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
  212. data/lib/rigor/plugin/template_unit.rb +196 -0
  213. data/lib/rigor/plugin.rb +1 -0
  214. data/lib/rigor/protection/discovery_seed.rb +3 -1
  215. data/lib/rigor/protection/kill_signature.rb +0 -1
  216. data/lib/rigor/protection/mutation_cache.rb +1 -2
  217. data/lib/rigor/rbs_extended.rb +27 -0
  218. data/lib/rigor/reflection/constant_ancestors.rb +97 -0
  219. data/lib/rigor/reflection/constant_path.rb +19 -6
  220. data/lib/rigor/reflection.rb +72 -105
  221. data/lib/rigor/scope/discovery_index.rb +135 -2
  222. data/lib/rigor/scope.rb +859 -49
  223. data/lib/rigor/sig_gen/alias_index.rb +289 -0
  224. data/lib/rigor/sig_gen/classification.rb +22 -5
  225. data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
  226. data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
  227. data/lib/rigor/sig_gen/generator.rb +558 -31
  228. data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
  229. data/lib/rigor/sig_gen/method_candidate.rb +47 -3
  230. data/lib/rigor/sig_gen/observation_collector.rb +1 -1
  231. data/lib/rigor/sig_gen/renderer.rb +124 -9
  232. data/lib/rigor/sig_gen/skip_reason_catalog.rb +44 -1
  233. data/lib/rigor/sig_gen/write_result.rb +19 -3
  234. data/lib/rigor/sig_gen/writer.rb +166 -35
  235. data/lib/rigor/sig_gen.rb +3 -0
  236. data/lib/rigor/signature_path_audit.rb +1 -1
  237. data/lib/rigor/source/node_walker.rb +0 -3
  238. data/lib/rigor/source/parameter_envelope.rb +72 -0
  239. data/lib/rigor/source.rb +1 -0
  240. data/lib/rigor/type/combinator.rb +88 -12
  241. data/lib/rigor/type/difference.rb +1 -0
  242. data/lib/rigor/type/hash_shape.rb +1 -1
  243. data/lib/rigor/type/refined.rb +1 -0
  244. data/lib/rigor/version.rb +1 -1
  245. data/lib/rigor.rb +1 -0
  246. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
  247. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +9 -5
  248. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -4
  249. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +0 -1
  250. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
  251. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
  252. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
  253. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
  254. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
  255. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
  256. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +239 -4
  257. data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
  258. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
  259. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
  260. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
  261. data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
  262. data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
  263. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -0
  264. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +9 -5
  265. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
  266. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
  267. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +380 -3
  268. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
  269. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +281 -35
  270. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
  271. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -14
  272. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
  273. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +171 -5
  274. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +2 -0
  275. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +1 -0
  276. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
  277. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
  278. data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
  279. data/plugins/rigor-grape/sig/grape.rbs +263 -0
  280. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +72 -2
  281. data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
  282. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -0
  283. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +11 -7
  284. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +35 -37
  285. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -1
  286. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -0
  287. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +1 -0
  288. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +9 -17
  289. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
  290. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +217 -36
  291. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +1 -0
  292. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +9 -5
  293. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +0 -1
  294. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +46 -1
  295. data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
  296. data/sig/rigor/analysis/baseline.rbs +69 -7
  297. data/sig/rigor/analysis/fact_store.rbs +1 -1
  298. data/sig/rigor/analysis/project_scan.rbs +74 -0
  299. data/sig/rigor/effects/config_envelopes.rbs +100 -0
  300. data/sig/rigor/effects/effect_table.rbs +60 -0
  301. data/sig/rigor/effects/envelope.rbs +97 -0
  302. data/sig/rigor/effects/envelope_index.rbs +33 -0
  303. data/sig/rigor/effects/file_collection.rbs +81 -0
  304. data/sig/rigor/effects/label.rbs +26 -0
  305. data/sig/rigor/effects/label_set.rbs +46 -0
  306. data/sig/rigor/effects/method_key.rbs +24 -0
  307. data/sig/rigor/effects/origin.rbs +50 -0
  308. data/sig/rigor/effects/plugin_facts.rbs +141 -0
  309. data/sig/rigor/effects/registry.rbs +68 -0
  310. data/sig/rigor/effects/summary.rbs +50 -0
  311. data/sig/rigor/effects/taint_cause.rbs +12 -0
  312. data/sig/rigor/environment.rbs +12 -10
  313. data/sig/rigor/inference/optimistic_origin.rbs +10 -0
  314. data/sig/rigor/inference.rbs +6 -4
  315. data/sig/rigor/plugin/additional_initializer.rbs +36 -0
  316. data/sig/rigor/plugin/base.rbs +30 -7
  317. data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
  318. data/sig/rigor/plugin/effect_attribution.rbs +66 -0
  319. data/sig/rigor/plugin/effect_edge.rbs +33 -0
  320. data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
  321. data/sig/rigor/plugin/io_boundary.rbs +1 -1
  322. data/sig/rigor/plugin/loader.rbs +3 -3
  323. data/sig/rigor/plugin/manifest.rbs +50 -11
  324. data/sig/rigor/plugin/protocol_contract.rbs +68 -0
  325. data/sig/rigor/plugin/registry.rbs +62 -1
  326. data/sig/rigor/plugin.rbs +1 -1
  327. data/sig/rigor/rbs_extended.rbs +1 -1
  328. data/sig/rigor/reflection.rbs +7 -6
  329. data/sig/rigor/scope.rbs +135 -20
  330. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -7
  331. data/sig/rigor/source.rbs +4 -4
  332. data/sig/rigor/testing.rbs +10 -4
  333. data/sig/rigor/type.rbs +6 -0
  334. data/sig/rigor.rbs +38 -20
  335. data/skills/rigor-ask/SKILL.md +8 -5
  336. data/skills/rigor-baseline-reduce/SKILL.md +4 -6
  337. data/skills/rigor-ci-setup/SKILL.md +17 -21
  338. data/skills/rigor-doctor/SKILL.md +24 -22
  339. data/skills/rigor-doctor/references/01-checks.md +97 -33
  340. data/skills/rigor-editor-setup/SKILL.md +6 -4
  341. data/skills/rigor-mcp-setup/SKILL.md +5 -4
  342. data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
  343. data/skills/rigor-next-steps/SKILL.md +4 -2
  344. data/skills/rigor-plugin-author/SKILL.md +19 -23
  345. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
  346. data/skills/rigor-plugin-author/references/02-walker-and-types.md +12 -25
  347. data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
  348. data/skills/rigor-plugin-review/SKILL.md +6 -4
  349. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
  350. data/skills/rigor-plugin-tune/SKILL.md +4 -2
  351. data/skills/rigor-project-init/SKILL.md +9 -7
  352. data/skills/rigor-project-init/references/01-detect.md +13 -9
  353. data/skills/rigor-project-init/references/02-configure.md +33 -8
  354. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
  355. data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
  356. data/skills/rigor-protection-uplift/SKILL.md +4 -6
  357. data/skills/rigor-rbs-setup/SKILL.md +4 -2
  358. data/skills/rigor-type-oracle/SKILL.md +4 -6
  359. data/skills/rigor-type-oracle/references/01-oracle-commands.md +12 -8
  360. data/skills/rigor-type-oracle/references/03-gap-protocol.md +0 -3
  361. data/skills/rigor-unused-adjudicate/SKILL.md +4 -2
  362. data/skills/rigor-upgrade/SKILL.md +13 -8
  363. metadata +108 -1
@@ -35,23 +35,28 @@ module Rigor
35
35
  # argument and the value the last.
36
36
  HASH_CONTENT_ADDERS = %i[[]= store].to_set.freeze
37
37
 
38
- # String content-mutators that append to the buffer. String carries no element parameter, so
39
- # these contribute nothing to a join — they are listed so the orchestrator recognises them as
40
- # content mutators (the binding already widens to `String` via normal typing); the join
41
- # helpers below short-circuit on a non-collection pre-state.
42
- STRING_CONTENT_ADDERS = %i[<< concat prepend insert replace].to_set.freeze
38
+ # Every method name that adds to a collection's CONTENT — the evidence the joins read, and the
39
+ # gate the straight-line path types its arguments behind. A String carries no element
40
+ # parameter, so it has no adders of its own; `StatementEvaluator::CONTENT_MUTATORS` adds the
41
+ # String mutators to what its scans count.
42
+ CONTENT_ADDERS = (ARRAY_CONTENT_ADDERS | HASH_CONTENT_ADDERS).freeze
43
43
 
44
- # Every method name that mutates a collection's CONTENT — the union the orchestrators scan a
45
- # block body for, and the gate the straight-line path types its arguments behind.
46
- CONTENT_ADDERS = (ARRAY_CONTENT_ADDERS | HASH_CONTENT_ADDERS | STRING_CONTENT_ADDERS).freeze
44
+ # The probes {#could_be_range?} passes to `accepts` — interned because the questions are
45
+ # asked once per `[]=` index member.
46
+ RANGE_INDEX_PROBE = Type::Combinator.nominal_of("Range")
47
+ OBJECT_INDEX_PROBE = Type::Combinator.nominal_of("Object")
48
+ ARRAY_PROBE = Type::Combinator.nominal_of("Array")
49
+ private_constant :RANGE_INDEX_PROBE, :OBJECT_INDEX_PROBE, :ARRAY_PROBE
47
50
 
48
51
  module_function
49
52
 
50
53
  # The element types a single content-mutator call introduces into an Array, given the
51
54
  # per-argument types (already typed by the caller in the scope the arguments are evaluated
52
55
  # in). `concat`/`replace` take collection arguments, so their element evidence is the
53
- # arguments' OWN element types unioned; the rest append the argument values directly. Returns
54
- # `[]` when there is no element evidence (e.g. a `<<` with no resolvable arg).
56
+ # arguments' OWN element types unioned; the `[]=` splice forms (`arr[i, n] = other` /
57
+ # `arr[range] = other`) store their value's elements the same way and read it through the
58
+ # same unwrap (issue #1140). The rest append the argument values directly. Returns `[]` when
59
+ # there is no element evidence (e.g. a `<<` with no resolvable arg).
55
60
  def array_added_elements(method_name, arg_types)
56
61
  return [] if arg_types.empty?
57
62
 
@@ -62,21 +67,27 @@ module Rigor
62
67
  # `insert(index, *objs)` — first arg is the position.
63
68
  arg_types.drop(1)
64
69
  when :[]=
65
- # `arr[i] = v` / `arr[i, n] = v` — value is the last argument.
66
- #
67
- # The SPLICE forms (`arr[i, n] = other` / `arr[range] = other`) store `other`'s ELEMENTS,
68
- # not `other` itself, so reading the value as one element over-widens: `a[0, 2] = [1, 2]`
69
- # contributes `Array[Integer]` where `Integer` is the truth. Left alone deliberately —
70
- # the answer is a superset either way, so it can only cost precision, and splitting the
71
- # arities here would need the receiver's own element type to unwrap against. Revisit if a
72
- # corpus site ever reads an element back through a splice-built array.
73
- [arg_types.last]
70
+ # `arr[i] = v` stores `v` as ONE element — the index arguments precede the value.
71
+ # The splice forms store the value's ELEMENTS instead (issue #1140): `a[0, 2] = [1, 2]`
72
+ # puts `Integer`s in the receiver, not an `Array[Integer]`. An index the engine cannot
73
+ # classify (`a[x] = v` with `x` untyped, a `Range | Integer` union, or a splat that
74
+ # may vanish inside the brackets) may be either form, so both readings join — the
75
+ # union is a superset of the truth either way.
76
+ case index_store_form(arg_types)
77
+ when :splice then splice_stored_elements(arg_types.last)
78
+ when :either then [arg_types.last] + splice_stored_elements(arg_types.last)
79
+ else [arg_types.last]
80
+ end
74
81
  when :fill
75
82
  # `fill(value)` — only the no-block single-value form adds a concrete element; block /
76
83
  # range forms are conservatively ignored (the arity-forget already widened the binding).
77
84
  arg_types.size == 1 ? arg_types : []
78
- else # << push append prepend unshift
85
+ when :<<, :push, :append, :prepend, :unshift
79
86
  arg_types
87
+ else
88
+ # Not an Array adder — a String mutator a content scan counted (`x.sub!("a", "b")` on an
89
+ # `Array | String` capture): its arguments are not elements.
90
+ []
80
91
  end
81
92
  end
82
93
 
@@ -122,6 +133,23 @@ module Rigor
122
133
  with_residue(Type::Combinator.nominal_of("Hash", type_args: [key_t, value_t]), hash_residue(pre_state))
123
134
  end
124
135
 
136
+ # Builds the continuation of a pre-state that carries BOTH an Array and a Hash member, from the
137
+ # pairs and the elements the body stores. Each side is joined with its own class's evidence, and
138
+ # every member neither side absorbs survives beside the Hash carrier ({#hash_residue}).
139
+ #
140
+ # Joined as one carrier, the other class's member was residue and survived exactly as the seed
141
+ # had it: an unwidened literal `Tuple` / `HashShape`, so what the body did to it was gone — `x =
142
+ # gets ? [1] : { a: 1 }; [0].each { x.map!(&:to_s) if x.is_a?(Array); x[:k] = 2 if
143
+ # x.is_a?(Hash) }` read `[1]`, and `x.first.upcase` drew `undefined method` on correct code. The
144
+ # caller routes the evidence: a store may reach either member, and only the caller sees its
145
+ # index (`StatementEvaluator#mixed_content_evidence`).
146
+ def join_mixed_content(pre_state, added_pairs, added_elements)
147
+ members = pre_state.is_a?(Type::Union) ? pre_state.members : [pre_state]
148
+ arrays, rest = members.partition { |member| array_residue(member).empty? }
149
+ array = join_array_content(Type::Combinator.union(*arrays), added_elements)
150
+ Type::Combinator.union(array, join_hash_content(Type::Combinator.union(*rest), added_pairs))
151
+ end
152
+
125
153
  # ----------------------------------------------------------------
126
154
  # Union residue — the members the rederived carrier does NOT stand for (issue #631).
127
155
  #
@@ -277,15 +305,158 @@ module Rigor
277
305
  type.is_a?(Type::Union) ? type.members : [type]
278
306
  end
279
307
 
308
+ # How an `arr[...] = v` call's index arguments place the stored value: `:splice` — the
309
+ # value's ELEMENTS land in the receiver (`arr[i, n] = v` takes two index slots,
310
+ # `arr[range] = v` takes a Range); `:element` — the value itself is one stored element
311
+ # (`arr[i] = v`); `:either` — the store may be either form.
312
+ #
313
+ # A splat inside the brackets is marked `nil` by the caller — its expansion can change the
314
+ # store's arity (`a[0, *xs] = v` stores `v` itself when `xs` is empty and `v`'s elements
315
+ # otherwise). An ordinary argument counts as a provable index however untyped it is:
316
+ # `a[i, n] = v` is a splice at every binding of `i` and `n`. Two or more provable indices
317
+ # are therefore always a splice however a splat expands; one provable index beside a splat
318
+ # reduces to the Range question (extra expansion only reaches an arity error, never a
319
+ # scalar store); zero provable indices leaves the form to whatever the splat yields.
320
+ def index_store_form(arg_types)
321
+ leading = arg_types[0...-1]
322
+ definite = leading.compact
323
+ return :splice if definite.size >= 2
324
+ return :either if definite.empty?
325
+ return single_index_form(definite.first) if definite.size == leading.size
326
+
327
+ range_index?(definite.first) ? :splice : :either
328
+ end
329
+
330
+ # The one-index question: a Range carrier is a splice, a definite scalar an element store,
331
+ # and anything between — a union straddling both, a broad index that may still hold a
332
+ # Range at runtime — may be either.
333
+ def single_index_form(index_type)
334
+ members = union_members(index_type)
335
+ return :splice if members.all? { |m| definite_range?(m) }
336
+ return :element if members.none? { |m| could_be_range?(m) }
337
+
338
+ :either
339
+ end
340
+
341
+ # True when the index position PROVABLY holds a Range: a Range carrier {#range_index?}
342
+ # recognizes, or a member acceptance proves a subtype of `Range` — a loaded
343
+ # `class MyRange < Range` answers `yes` there where the name check cannot see it. An
344
+ # unresolvable subclass answers `maybe`, which correctly declines here and still reaches
345
+ # {#could_be_range?}'s `either` reading. A gradual member accepts in BOTH directions
346
+ # (`Range` accepts it optimistically), so it must be excluded before asking — an untyped
347
+ # index may hold a scalar just as well and stays `either`. The exclusion reads through a
348
+ # `Difference` / `Refined` to its base: acceptance projects the wrapper onto the base too,
349
+ # so a `non-nil` or refined `Dynamic` answers `yes` there exactly like the bare gradual
350
+ # while still being free to hold a scalar at runtime.
351
+ def definite_range?(member)
352
+ return true if range_index?(member)
353
+
354
+ case member
355
+ when Type::Dynamic, Type::Top then false
356
+ when Type::Difference, Type::Refined then definite_range?(member.base)
357
+ else member.respond_to?(:accepts) && RANGE_INDEX_PROBE.accepts(member).yes?
358
+ end
359
+ end
360
+
361
+ # True when the index position MAY hold a Range at runtime: a definite Range carrier
362
+ # (`Constant[0..1]` accepts only its own value, so acceptance alone cannot see it),
363
+ # `Dynamic`, a carrier the engine cannot answer for, and every type related to
364
+ # `Nominal[Range]` in EITHER direction — `Object`/`Enumerable`/`top` above it, a
365
+ # project `class MyRange < Range` below it (an unresolvable class name answers `maybe`,
366
+ # never `no`, so subclass-ness cannot be ruled out).
367
+ #
368
+ # `no` in both directions is a real disjointness only when the member's values are
369
+ # provably `Object` instances — a fixed-class value set that single inheritance keeps
370
+ # from ever being a Range (`Integer`, a non-Range `Constant`, a `Tuple`). A MODULE
371
+ # member fails that probe (`Comparable`'s ancestors exclude `Object` — a module has no
372
+ # superclass chain), and rightly so: `class MyRange < Range; include Comparable; end`
373
+ # satisfies the annotation and still splices at runtime.
374
+ def could_be_range?(member)
375
+ return true if range_index?(member)
376
+
377
+ case member
378
+ when Type::Intersection
379
+ # An intersected value sits in EVERY member's value set, so it can be a Range only
380
+ # when every member allows one — `String & Comparable` cannot be (a String never
381
+ # is), `Object & Comparable` can (`MyRange < Range; include Comparable` satisfies
382
+ # both). Asked member-wise because the `Object` probe below answers yes when ANY
383
+ # intersected member accepts it.
384
+ member.members.all? { |part| could_be_range?(part) }
385
+ when Type::Maybe then could_be_range?(member.value_type)
386
+ else
387
+ return true unless member.respond_to?(:accepts)
388
+ return true unless member.accepts(RANGE_INDEX_PROBE).no? &&
389
+ RANGE_INDEX_PROBE.accepts(member).no?
390
+
391
+ !OBJECT_INDEX_PROBE.accepts(member).yes?
392
+ end
393
+ end
394
+
395
+ # Element types a splice RHS adds to the receiver, read member-wise. Ruby splices an
396
+ # Array RHS (`a[0, 1] = [1, 2]` puts `Integer`s in — a bare `Nominal[Array]` splices
397
+ # elements the signature left untyped, which are unknown rather than none), and stores a
398
+ # NON-Array RHS as one element UNLESS the value coerces through `to_ary`, in which case
399
+ # the returned array's elements splice instead (`a[0, 1] = obj` where `obj.to_ary`
400
+ # returns `["x"]` stores `"x"`). `nil` is no exception — `a[i, n] = nil` stores a single
401
+ # nil.
402
+ #
403
+ # Every member that is not an Array carrier keeps a `Dynamic[top]` arm beside itself:
404
+ # `to_ary` is consulted for a NON-Array value, and nothing here can prove the member's
405
+ # class does not define one — a `Nominal` may be a subclass that declares it, and the
406
+ # project may reopen even a non-subclassable core class (ADR-17's patched-methods tier
407
+ # resolves exactly those). The two exact carriers are the ones `Array#[]=` splices
408
+ # without a coercion: a `Tuple` literal's elements, and a `Nominal[Array]`'s own type
409
+ # arguments — an Array object is spliced directly even when its class overrides
410
+ # `to_ary`.
411
+ def splice_stored_elements(value_type)
412
+ union_members(value_type).flat_map do |member|
413
+ base = member
414
+ base = base.base while base.is_a?(Type::Difference) || base.is_a?(Type::Refined)
415
+
416
+ case base
417
+ when Type::Tuple
418
+ base.elements
419
+ when Type::Nominal
420
+ if base.class_name == "Array"
421
+ # `Array#[]=` splices an Array RHS directly — `to_ary` is never consulted
422
+ # for an Array object, even a subclass overriding it — so the elements
423
+ # join exactly (a bare `Array` signature left them unknown, not absent).
424
+ base.type_args.empty? ? [Type::Combinator.untyped] : base.type_args
425
+ elsif ARRAY_PROBE.accepts(base).yes?
426
+ # A definite Array subclass is spliced the same way; its elements are
427
+ # unknown to this seam.
428
+ [Type::Combinator.untyped]
429
+ else
430
+ [member, Type::Combinator.untyped]
431
+ end
432
+ else
433
+ [member, Type::Combinator.untyped]
434
+ end
435
+ end
436
+ end
437
+
438
+ # True when an index position holds a Range carrier — a static `Constant<Range>` (`0..1`),
439
+ # a `Nominal[Range]` (`(0..n)`), or a refinement / difference over either. `IntegerRange`
440
+ # and `FloatRange` are scalar number refinements rather than Range objects and correctly
441
+ # decline, as does every ordinary scalar index.
442
+ def range_index?(type)
443
+ case type
444
+ when Type::Constant then type.value.is_a?(::Range)
445
+ when Type::Nominal then type.class_name == "Range"
446
+ when Type::Difference, Type::Refined then range_index?(type.base)
447
+ else false
448
+ end
449
+ end
450
+
280
451
  # Element types carried by a collection binding, regardless of which carrier holds them: a
281
452
  # `Tuple` lists them, a `Nominal[Array, [E]]` has one element param, a bare `Array` /
282
453
  # anything else yields none.
283
454
  #
284
455
  # A `Difference` reads through to its base: `non-empty-array[T]` holds `T`s, and the seams
285
456
  # that read a seed from BEFORE the arity-forget ran (see {#join_array_content}) meet the
286
- # refinement carrier itself where they used to meet the base the widening had left. Declining
287
- # it there would hand the continuation the widened base ALONE, with every appended arm missing
288
- # — a wrong type, not a wide one.
457
+ # refinement carrier itself where they used to meet the base the widening had left.
458
+ # Declining it there would hand the continuation the widened base ALONE, with every appended
459
+ # arm missing — a wrong type, not a wide one.
289
460
  def collection_element_types(type)
290
461
  case type
291
462
  when Type::Tuple
@@ -305,13 +476,15 @@ module Rigor
305
476
 
306
477
  # `[keys, values]` evidence from a Hash-ish pre-state binding — a `HashShape` (literal pairs)
307
478
  # or a `Nominal[Hash, [K, V]]`. A `Difference` (`non-empty-hash[K, V]`) reads through to its
308
- # base, as {#collection_element_types} does for the Array side.
479
+ # base, as {#collection_element_types} does for the Array side. An open shape adds a
480
+ # `Dynamic[top]` key and value: its unseen keys may hold anything, and the joined carrier's
481
+ # read of one must not answer a known value (`flags = { debug: true }; flags.default = false`
482
+ # then a store in a block read `flags[:verbose]` as `true`).
309
483
  def hash_shape_key_values(type)
310
484
  case type
311
485
  when Type::HashShape
312
- return [[], []] if type.pairs.empty?
313
-
314
- [[key_union_for(type.pairs.keys)], type.pairs.values]
486
+ keys, values = type.pairs.empty? ? [[], []] : [[key_union_for(type.pairs.keys)], type.pairs.values]
487
+ type.open? ? [keys + [Type::Combinator.untyped], values + [Type::Combinator.untyped]] : [keys, values]
315
488
  when Type::Nominal
316
489
  type.class_name == "Hash" && type.type_args.size == 2 ? [[type.type_args[0]], [type.type_args[1]]] : [[], []]
317
490
  when Type::Union
@@ -4,6 +4,7 @@ require "prism"
4
4
 
5
5
  require_relative "../type"
6
6
  require_relative "../source/node_children"
7
+ require_relative "return_barrier"
7
8
 
8
9
  module Rigor
9
10
  module Inference
@@ -13,15 +14,14 @@ module Rigor
13
14
  # The inferred type is the union of:
14
15
  #
15
16
  # - the body's last-statement type, and
16
- # - the type of every explicit `return value` reachable in the body. Nested `def` / lambda / block bodies
17
- # are return barriers — their `return`s do not bubble up to the enclosing method.
17
+ # - the type of every explicit `return value` that exits the method. A `return` inside an ordinary block does
18
+ # (control-flow-analysis.md § "Non-local exits"); one inside a {ReturnBarrier} — a nested `def`, a lambda,
19
+ # or a `define_method` body — does not. The barriers are the ones `StatementEvaluator`'s return sink stops
20
+ # at, so the proposal and `rigor check`'s view of callers agree (issue #1382).
18
21
  #
19
22
  # Extracted from `Rigor::SigGen::Generator#infer_return_type` so `LineTypeCollector` (`rigor annotate`'s
20
23
  # def-line annotator) and the sig-generator share one source of truth.
21
24
  module DefReturnTyper
22
- RETURN_BARRIER_NODES = [Prism::DefNode, Prism::LambdaNode, Prism::BlockNode].freeze
23
- private_constant :RETURN_BARRIER_NODES
24
-
25
25
  module_function
26
26
 
27
27
  def call(def_node, scope_index)
@@ -60,10 +60,14 @@ module Rigor
60
60
 
61
61
  def collect_return_types(node, scope_index, out)
62
62
  return unless node.is_a?(Prism::Node)
63
- return if RETURN_BARRIER_NODES.any? { |klass| node.is_a?(klass) }
63
+ return if ReturnBarrier.node?(node)
64
64
 
65
65
  type_return_node(node, scope_index, out) if node.is_a?(Prism::ReturnNode)
66
- node.rigor_each_child { |c| collect_return_types(c, scope_index, out) }
66
+ # A barrier call's receiver and arguments still run in the method; only its block is cut off.
67
+ barrier_block = node.block if node.is_a?(Prism::CallNode) && ReturnBarrier.block_call?(node)
68
+ node.rigor_each_child do |c|
69
+ collect_return_types(c, scope_index, out) unless c.equal?(barrier_block)
70
+ end
67
71
  end
68
72
 
69
73
  def type_return_node(return_node, scope_index, out)
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../type"
4
+
5
+ module Rigor
6
+ module Inference
7
+ # Issue #963 — the lexical `self` of a `define_method(:name) { ... }` block.
8
+ #
9
+ # A block has no `self` of its own, so every block body enters with `Scope#entering_opaque_block`
10
+ # and the enclosing class body's `self_type` (`Singleton[C]`) rides along unchanged. For
11
+ # `define_method` that carrier is the wrong side of the class: `Module#define_method` turns the
12
+ # block into an INSTANCE method of the receiver, and Ruby runs its body with `self` bound to the
13
+ # receiving instance. A bare `text` inside the block therefore reaches `C`'s member / attr reader,
14
+ # while `Singleton[C]` answers only `C`'s class methods.
15
+ #
16
+ # The consequence #963 records is #618's veto going the wrong way: `ExpressionTyper#self_type_answers?`
17
+ # asked the singleton side, found nothing, and let a same-named top-level `def text` bind ahead of the
18
+ # reader — typing `text.upcase` inside `class Line < Struct.new(:text); define_method(:shout) { text.upcase }`
19
+ # as the top-level def's `nil`. Narrowing the block body's `self_type` to `Nominal[C]` makes the
20
+ # existing veto ask the instance side, which is the side Ruby dispatches on; nothing in the veto
21
+ # itself changes.
22
+ #
23
+ # The match is deliberately narrow — the receiver must be the class body's own `self`, and that `self`
24
+ # must be a known `Singleton[C]` on the instance side of the class. `Foo.define_method(...)` on some other
25
+ # receiver keeps the unmodelled-self answer, and so does a `class << ...` BODY, where `self` is the
26
+ # singleton class and the call defines a CLASS method: an instance reader is not in that MRO, MRI reaches
27
+ # the top-level `def` there, and Rigor must answer the same. The body is the exclusion, not the lexical
28
+ # singleton frame — a `def` reached from that body has the class object as its `self`, so the call defines
29
+ # an instance method and the narrowing applies. `Scope#singleton_class_body?` is what draws that line, and
30
+ # carrying it on the scope is what lets both block-entry paths apply it identically.
31
+ # A `module_function` module is the one shape where both answers are right at once: MRI makes the block
32
+ # BOTH an instance method, where the reader resolves, and a module function on the module object, where it
33
+ # does not. Narrowing silences the module-function half, which is the false-positive-averse direction
34
+ # AGENTS.md § Implementation Guidelines asks for.
35
+ module DefineMethodBlockSelf
36
+ module_function
37
+
38
+ # @return the block body's narrowed `self_type`, or `nil` when the call shape does not match.
39
+ def narrow_self_type_for(scope:, call_node:)
40
+ return nil if scope&.singleton_class_body?
41
+ return nil unless define_method_on_lexical_self?(call_node)
42
+
43
+ self_type = scope&.self_type
44
+ return nil unless self_type.is_a?(Type::Singleton)
45
+
46
+ class_name = self_type.class_name
47
+ return nil if class_name.nil?
48
+
49
+ instance_type_for(class_name, scope.environment)
50
+ end
51
+
52
+ def define_method_on_lexical_self?(call_node)
53
+ return false unless call_node.is_a?(Prism::CallNode)
54
+ return false unless call_node.name == :define_method
55
+
56
+ call_node.receiver.nil? || call_node.receiver.is_a?(Prism::SelfNode)
57
+ end
58
+
59
+ def instance_type_for(class_name, environment)
60
+ environment&.nominal_for_name(class_name) || Type::Nominal.new(class_name)
61
+ end
62
+ end
63
+ end
64
+ end
@@ -45,7 +45,7 @@ module Rigor
45
45
  widened = widen_through_path(current_scope.local(read.name), steps, call_node.name, arg_types)
46
46
  return current_scope if widened.nil?
47
47
 
48
- current_scope.with_local(read.name, widened)
48
+ current_scope.with_mutated_local(read.name, widened)
49
49
  end
50
50
 
51
51
  # True when `receiver` is an element read whose target element is a literal-shape carrier —
@@ -62,7 +62,12 @@ module Rigor
62
62
  # `[<local read>, [step, …]]` for a chain of element reads rooted at a local variable, or
63
63
  # `nil` when the expression is anything else. A step is an Integer position (negative counts
64
64
  # from the end, as Ruby's own indexing does) or `:all` when the index is not a literal.
65
+ #
66
+ # Every mutation site's receiver is asked, and nearly every one is a plain variable read, so a
67
+ # receiver that is no call answers before the step list is allocated.
65
68
  def element_read_path(node)
69
+ return nil unless node.is_a?(Prism::CallNode)
70
+
66
71
  steps = []
67
72
  cursor = node
68
73
  while cursor.is_a?(Prism::CallNode)
@@ -118,22 +123,29 @@ module Rigor
118
123
  end
119
124
 
120
125
  # Rebuilds `type` with the element at `steps` widened against `method_name`, or `nil` when
121
- # nothing along the path widened.
122
- def widen_through_path(type, steps, method_name, arg_types)
123
- return MutationWidening.widen_for_mutator(type, method_name, arg_types: arg_types) if steps.empty?
126
+ # nothing along the path widened. A block, when given, replaces the straight-line widening of the
127
+ # element itself: it receives the element's type and answers the widened one, or `nil` to decline.
128
+ # {UnknownStoreWidening} passes one so a site on an element takes the same unknown-store widening a
129
+ # site on a variable does.
130
+ def widen_through_path(type, steps, method_name, arg_types, &element_widening)
131
+ if steps.empty?
132
+ return element_widening.call(type) if element_widening
133
+
134
+ return MutationWidening.widen_for_mutator(type, method_name, arg_types: arg_types)
135
+ end
124
136
 
125
137
  case type
126
- when Type::Tuple then widen_tuple_path(type, steps, method_name, arg_types)
127
- when Type::Union then widen_union_path(type, steps, method_name, arg_types)
138
+ when Type::Tuple then widen_tuple_path(type, steps, method_name, arg_types, &element_widening)
139
+ when Type::Union then widen_union_path(type, steps, method_name, arg_types, &element_widening)
128
140
  end
129
141
  end
130
142
 
131
- def widen_tuple_path(tuple, steps, method_name, arg_types)
143
+ def widen_tuple_path(tuple, steps, method_name, arg_types, &)
132
144
  elements = tuple.elements.dup
133
145
  rest = steps.drop(1)
134
146
  widened_any = false
135
147
  tuple_positions(tuple, steps.first).each do |position|
136
- inner = widen_through_path(elements[position], rest, method_name, arg_types)
148
+ inner = widen_through_path(elements[position], rest, method_name, arg_types, &)
137
149
  next if inner.nil?
138
150
 
139
151
  elements[position] = inner
@@ -142,10 +154,10 @@ module Rigor
142
154
  widened_any ? Type::Tuple.new(elements) : nil
143
155
  end
144
156
 
145
- def widen_union_path(union, steps, method_name, arg_types)
157
+ def widen_union_path(union, steps, method_name, arg_types, &)
146
158
  widened_any = false
147
159
  members = union.members.map do |member|
148
- widened = widen_through_path(member, steps, method_name, arg_types)
160
+ widened = widen_through_path(member, steps, method_name, arg_types, &)
149
161
  next member if widened.nil?
150
162
 
151
163
  widened_any = true