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
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../source/node_children"
6
+ require_relative "captured_locals"
7
+ require_relative "element_read_widening"
8
+ require_relative "jump_targets"
9
+ require_relative "mutation_widening"
10
+ require_relative "receiver_alias"
11
+
12
+ module Rigor
13
+ module Inference
14
+ # Whether an expression `StatementEvaluator` would otherwise type as a pure value — a call's receiver or
15
+ # argument, a literal, an interpolation, a `rescue` modifier — holds something the scope after it must see:
16
+ # a variable write whose binding outlives the expression, or a `next` / `break` whose path a jump join
17
+ # reads. Issue #1223: `out << (n += 1)` left `n` on its pre-write binding, straight-line and through
18
+ # ADR-56's block write-back, and `puts(x && next)` never reached the block's `next` join.
19
+ #
20
+ # A local write counts only when it binds past every block and lambda it is nested in within the
21
+ # expression (its `depth` reaches the expression's own scope): `puts(xs.map { |x| y = x })` binds `y` in
22
+ # the block alone, while `puts(xs.each { t = 1 })` rebinds the outer `t`. Every instance-variable,
23
+ # class-variable and global write counts wherever it is, and so does an index `||=` / `&&=` / `op=`,
24
+ # whose store widens its receiver. A jump counts only where it targets the construct around the
25
+ # expression ({JumpTargets}). A `def`, class or module body is a scope of its own, and `defined?`
26
+ # evaluates nothing, so neither is looked into.
27
+ #
28
+ # An in-place mutation counts on the same terms as a write ({.outliving_mutation?}): a call to a name the
29
+ # straight-line widening responds to ({MutationWidening::SHAPE_MUTATORS}) on a receiver naming a variable
30
+ # that outlives the expression. The mutator's widening runs in the evaluator's post-call effects, which a
31
+ # pure-value operand never reached, so a chained `b.push(2).size`, an argument `puts(b.push(2))` or
32
+ # `d.map!.with_index { … }` left `b` / `d` on the literal their assignment wrote.
33
+ #
34
+ # Short-circuiting and allocation-free outside a mutator-named call: the evaluator asks it of every call's
35
+ # operands, and asks it again of each operand it threads. A found effect stops the recursion without a
36
+ # `return` out of the child block, which would allocate once per frame it unwinds and make a deep literal
37
+ # quadratic.
38
+ module OperandEffects
39
+ LOCAL_WRITE_NODES = CapturedLocals::LOCAL_WRITE_NODES
40
+ OUTLIVING_WRITE_NODES = (
41
+ CapturedLocals::NON_LOCAL_WRITE_NODES |
42
+ Set[Prism::IndexOrWriteNode, Prism::IndexAndWriteNode, Prism::IndexOperatorWriteNode]
43
+ ).freeze
44
+ INSTANCE_WRITE_NODES = Set[
45
+ Prism::InstanceVariableWriteNode, Prism::InstanceVariableOperatorWriteNode,
46
+ Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode, Prism::InstanceVariableTargetNode
47
+ ].freeze
48
+ GLOBAL_WRITE_NODES = Set[
49
+ Prism::GlobalVariableWriteNode, Prism::GlobalVariableOperatorWriteNode, Prism::GlobalVariableOrWriteNode,
50
+ Prism::GlobalVariableAndWriteNode, Prism::GlobalVariableTargetNode
51
+ ].freeze
52
+ COMPOUND_LOCAL_WRITES = Set[
53
+ Prism::LocalVariableOperatorWriteNode, Prism::LocalVariableOrWriteNode, Prism::LocalVariableAndWriteNode
54
+ ].freeze
55
+ COMPOUND_INSTANCE_WRITES = Set[
56
+ Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableOrWriteNode,
57
+ Prism::InstanceVariableAndWriteNode
58
+ ].freeze
59
+ JUMP_NODES = Set[Prism::NextNode, Prism::BreakNode].freeze
60
+ SCOPE_NODES = Set[Prism::BlockNode, Prism::LambdaNode].freeze
61
+ OPAQUE_NODES = Set[
62
+ Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode, Prism::DefinedNode
63
+ ].freeze
64
+ private_constant :LOCAL_WRITE_NODES, :OUTLIVING_WRITE_NODES, :INSTANCE_WRITE_NODES, :GLOBAL_WRITE_NODES,
65
+ :COMPOUND_LOCAL_WRITES,
66
+ :COMPOUND_INSTANCE_WRITES, :JUMP_NODES, :SCOPE_NODES, :OPAQUE_NODES
67
+
68
+ module_function
69
+
70
+ def any?(node)
71
+ node.is_a?(Prism::Node) && effect?(node, 0, true)
72
+ end
73
+
74
+ # `nesting` counts the blocks and lambdas between `node` and the expression's root; `jumps` is false once
75
+ # the walk has crossed a construct that retargets a jump.
76
+ def effect?(node, nesting, jumps)
77
+ klass = node.class
78
+ return true if LOCAL_WRITE_NODES.include?(klass) && node.depth >= nesting
79
+ return true if OUTLIVING_WRITE_NODES.include?(klass)
80
+ return true if jumps && JUMP_NODES.include?(klass)
81
+ return true if klass == Prism::CallNode && outliving_mutation?(node, nesting)
82
+ return false if OPAQUE_NODES.include?(klass)
83
+
84
+ nesting += 1 if SCOPE_NODES.include?(klass)
85
+ jumps &&= !JumpTargets.boundary?(node)
86
+ found = false
87
+ node.rigor_each_child { |child| found ||= effect?(child, nesting, jumps) }
88
+ found
89
+ end
90
+ private_class_method :effect?
91
+
92
+ # True when `node` is a call the straight-line widening would answer for a variable that outlives the
93
+ # expression: a {MutationWidening::SHAPE_MUTATORS} name on a receiver whose {ReceiverAlias.mutated_reads},
94
+ # or the local an element read is rooted at ({ElementReadWidening.element_read_path}, `a[0] << e`), reach
95
+ # past the blocks between `node` and the expression's root. A block's own local — its parameter, a name it
96
+ # introduces — is that block's to widen.
97
+ def outliving_mutation?(node, nesting)
98
+ return false unless MutationWidening::SHAPE_MUTATORS.include?(node.name)
99
+
100
+ receiver = node.receiver
101
+ return false if receiver.nil? || receiver.is_a?(Prism::SelfNode)
102
+
103
+ path = ElementReadWidening.element_read_path(receiver)
104
+ return outliving_read?(path.first, nesting) if path
105
+
106
+ ReceiverAlias.mutated_reads(receiver).any? { |read| outliving_read?(read, nesting) }
107
+ end
108
+ private_class_method :outliving_mutation?
109
+
110
+ # A local read reaches the expression's own scope when its `depth` does; an `it` read is always the
111
+ # innermost block's parameter, so only one outside every block in the expression does. An instance
112
+ # variable, class variable or global always outlives it.
113
+ def outliving_read?(read, nesting)
114
+ case read
115
+ when Prism::LocalVariableReadNode then read.depth >= nesting
116
+ when Prism::ItLocalVariableReadNode then nesting.zero?
117
+ else true
118
+ end
119
+ end
120
+ private_class_method :outliving_read?
121
+
122
+ # The locals (bare names), instance variables and globals (sigil-prefixed names, as {CapturedLocals.bind}
123
+ # reads them) `node` writes on the terms {.any?} counts a write, in first-write order.
124
+ def written_variables(node)
125
+ names = []
126
+ collect_written(node, 0, names) if node.is_a?(Prism::Node)
127
+ names.uniq
128
+ end
129
+
130
+ # The locals and instance variables `node` reads, named as {.written_variables} names them. A compound write
131
+ # (`x += 1`, `x ||= v`) reads the variable it writes.
132
+ def read_variables(node)
133
+ names = []
134
+ collect_read(node, 0, names) if node.is_a?(Prism::Node)
135
+ names.uniq
136
+ end
137
+
138
+ def collect_read(node, nesting, names)
139
+ klass = node.class
140
+ if klass == Prism::LocalVariableReadNode || COMPOUND_LOCAL_WRITES.include?(klass)
141
+ names << node.name if node.depth >= nesting
142
+ elsif klass == Prism::InstanceVariableReadNode || COMPOUND_INSTANCE_WRITES.include?(klass)
143
+ names << node.name
144
+ end
145
+ return if OPAQUE_NODES.include?(klass)
146
+
147
+ nesting += 1 if SCOPE_NODES.include?(klass)
148
+ node.rigor_each_child { |child| collect_read(child, nesting, names) }
149
+ end
150
+ private_class_method :collect_read
151
+
152
+ def collect_written(node, nesting, names)
153
+ klass = node.class
154
+ if LOCAL_WRITE_NODES.include?(klass)
155
+ names << node.name if node.depth >= nesting
156
+ elsif INSTANCE_WRITE_NODES.include?(klass) || GLOBAL_WRITE_NODES.include?(klass)
157
+ names << node.name
158
+ end
159
+ return if OPAQUE_NODES.include?(klass)
160
+
161
+ nesting += 1 if SCOPE_NODES.include?(klass)
162
+ node.rigor_each_child { |child| collect_written(child, nesting, names) }
163
+ end
164
+ private_class_method :collect_written
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ module Rigor
6
+ module Inference
7
+ # Issue #1256 — the later operands of one expression `StatementEvaluator` threads and types as a whole (a
8
+ # call, a literal, a `rescue` modifier): the operands entered from a scope an earlier operand moved, rather
9
+ # than from the one the whole expression is typed from. `puts(b.unshift("s"), b.first.upcase)` enters
10
+ # `b.first.upcase` after the `unshift` widened `b`, and `[n += 1, n += 1]` its second element after the first
11
+ # write.
12
+ #
13
+ # Each such operand is recorded into the per-node scope index with the scope it was entered from, so the
14
+ # diagnostics read it there, and typed from that scope once, so the whole expression's value holds it
15
+ # ({#types}). The operand holding a write is still typed from its own entry: `out << (n += 1)` appends `1`.
16
+ #
17
+ # The walk takes an operand before anything below it, and compares an operand's entry against the scope its
18
+ # nearest taken ancestor is typed from, so each later operand is typed exactly once: from the bottom up, each
19
+ # holding the values of the ones below it. A threaded call below the root is still not typed as a whole
20
+ # (`StatementEvaluator#call_effects`); its later operands join the root's walk.
21
+ class OperandWalk
22
+ # Positions `ExpressionTyper` gives no value of their own: it reads their children directly, never the node,
23
+ # so a later one is recorded but not typed, and its children are compared against its ancestor's scope.
24
+ # A keyword hash is typed whole only as a plain hash: the `**h` form reads the splatted value itself
25
+ # (`ExpressionTyper#double_splat_hash_shape`), so its children are taken instead.
26
+ NON_VALUE_NODES = Set[
27
+ Prism::ArgumentsNode, Prism::AssocNode, Prism::AssocSplatNode, Prism::BlockArgumentNode, Prism::SplatNode,
28
+ Prism::KeywordHashNode
29
+ ].freeze
30
+ private_constant :NON_VALUE_NODES
31
+
32
+ # The per-node scope index's `(node, scope) ->` recorder, or nil for an unrecorded pass.
33
+ attr_reader :recorder
34
+
35
+ def initialize(recorder)
36
+ @recorder = recorder
37
+ @entries = nil
38
+ @types = nil
39
+ end
40
+
41
+ # The position the next taken operand lands at, for {#types}' `since:`.
42
+ def mark
43
+ @entries.nil? ? 0 : @entries.size
44
+ end
45
+
46
+ # Takes `node`, entered from `entry`. Answers the slot {#resolve} fills, or nil for a {NON_VALUE_NODES}
47
+ # position.
48
+ def later(node, entry)
49
+ @recorder&.call(node, entry)
50
+ return nil if NON_VALUE_NODES.include?(node.class)
51
+
52
+ (@entries ||= []) << [node, entry, nil]
53
+ @entries.size - 1
54
+ end
55
+
56
+ # The value the handler that evaluated the operand in `slot` typed it to, from its entry.
57
+ def resolve(slot, type)
58
+ @entries[slot][2] = type
59
+ end
60
+
61
+ # The identity-comparing table of every taken operand's value, for `ExpressionTyper`'s `operand_types:`, or
62
+ # nil when the walk took none at or after `since` (a {#mark}). An operand the evaluator did not type is typed
63
+ # from its entry here, after every operand below it. A threaded call below the root asks for the operands
64
+ # taken under it once they are all taken; they are typed then and only then, and the table keeps growing,
65
+ # so the root's own request types only what is left.
66
+ def types(tracer, since: 0)
67
+ return nil if @entries.nil? || @entries.size <= since
68
+
69
+ @types ||= {}.compare_by_identity
70
+ (@entries.size - 1).downto(since) do |index|
71
+ node, entry, type = @entries[index]
72
+ next if @types.key?(node)
73
+
74
+ @types[node] = type || self.class.type_of(entry, node, tracer, @types)
75
+ end
76
+ @types
77
+ end
78
+
79
+ # `node`'s type from `scope`, answering each node `operand_types` holds with its value there. `Scope#type_of`
80
+ # itself is plugin-facing and keeps its surface, so the table goes to the typer directly.
81
+ def self.type_of(scope, node, tracer, operand_types)
82
+ return scope.type_of(node, tracer: tracer) if operand_types.nil?
83
+
84
+ ExpressionTyper.new(scope: scope, tracer: tracer, operand_types: operand_types).type_of(node)
85
+ end
86
+ end
87
+ end
88
+ end
@@ -43,16 +43,68 @@ module Rigor
43
43
  # them would widen this channel into a taint that silences genuine diagnostics.
44
44
  NIL_COLLAPSING_PREDICATES = %i[nil? !].freeze
45
45
 
46
+ # Issue #1094 — the binary comparisons that are the same statement as `nil?` when one operand is the `nil`
47
+ # literal: `x == nil`, `nil != x`, `x.equal?(nil)`, `nil === x`. Every one folds from the other operand's
48
+ # nil-freeness alone, so the derivation is exactly as sound as the unary one. A comparison against any
49
+ # other value folds from the carrier's value and stays out, for the reason the value predicates do.
50
+ NIL_COMPARISONS = %i[== != eql? equal? ===].freeze
51
+
52
+ # {.miss_answer}'s answer for an expression whose value on a miss cannot be told.
53
+ UNKNOWN_MISS = Object.new.freeze
54
+
55
+ # Issue #1302 — a local / ivar mark that also records what the bound value answers on a miss, so
56
+ # {.miss_answer} reads a predicate through the binding as it reads the inline form. `Scope` stores it in
57
+ # the mark table in place of the bare cause, so the answer is kept and dropped with the mark itself; a
58
+ # binding whose miss answer cannot be told stores the bare cause, as before.
59
+ BoundMark = Data.define(:cause, :miss)
60
+
61
+ # The marks a binding of today's single cause records, interned so that marking `v = h[k]` allocates
62
+ # nothing beyond the table it always rebuilt.
63
+ INTERNED_BOUND_MARKS = [nil, true, false].to_h do |miss|
64
+ [miss, BoundMark.new(cause: IMPLICITLY_RETURNS_NIL, miss: miss)]
65
+ end.freeze
66
+ private_constant :INTERNED_BOUND_MARKS
67
+
46
68
  module_function
47
69
 
70
+ # What `Scope` stores for a binding marked with `cause` whose value answers `miss` on a miss.
71
+ def bound_mark(cause, miss)
72
+ return cause if miss.equal?(UNKNOWN_MISS)
73
+
74
+ (cause == IMPLICITLY_RETURNS_NIL && INTERNED_BOUND_MARKS[miss]) || BoundMark.new(cause: cause, miss: miss)
75
+ end
76
+
77
+ # The cause and the recorded miss answer of an entry {.bound_mark} made (or nil for no entry).
78
+ def bound_cause(mark) = mark.is_a?(BoundMark) ? mark.cause : mark
79
+ def bound_miss(mark) = mark.is_a?(BoundMark) ? mark.miss : UNKNOWN_MISS
80
+
81
+ # The entry a control-flow join keeps for a name both arms mark: the answer survives only when the arms
82
+ # agree on it, since otherwise a miss can take either arm's.
83
+ def join_bound_marks(mine, theirs) = mine == theirs ? mine : bound_cause(mine)
84
+
85
+ # `rebound` — `scope` with the local `name` rebound to its type across a loop's iterations
86
+ # (`CapturedLocals.bind`) — re-marked with the mark `scope` holds for `name` or `optimistic`, the one an
87
+ # iteration's own rebind made. `scope`'s recorded miss answer is kept unless an iteration's mark joins it:
88
+ # that one comes without its answer, so the pair's cannot be told.
89
+ def carry_local_mark(scope, rebound, name, optimistic)
90
+ miss = optimistic ? UNKNOWN_MISS : scope.optimistic_local_miss(name)
91
+ rebound.with_optimistic_local(name, scope.optimistic_local(name) || optimistic, miss: miss)
92
+ end
93
+
94
+ # {.carry_local_mark} for an instance variable.
95
+ def carry_ivar_mark(scope, rebound, name, optimistic)
96
+ miss = optimistic ? UNKNOWN_MISS : scope.optimistic_ivar_miss(name)
97
+ rebound.with_optimistic_ivar(name, scope.optimistic_ivar(name) || optimistic, miss: miss)
98
+ end
99
+
48
100
  # The effective optimistic-nil-free cause of an expression under `scope`, or nil when its nil-freeness is
49
101
  # a property of the value rather than a bet. The single owner of the judgment: `ExpressionTyper`,
50
102
  # `StatementEvaluator` and `AlwaysTruthyConditionCollector` all route here, so the three consumers the
51
103
  # spec binds cannot drift apart.
52
104
  #
53
105
  # Resolution order — the mark recorded on the node itself, then the binding a bare local / ivar read (or
54
- # a write in value position, `if (x = MAP[k])`) resolves through, then the predicate-fold derivation
55
- # issue #313 added.
106
+ # a write in value position, `if (x = MAP[k])`) resolves through, then a safe-navigation call and the
107
+ # predicate-fold derivation issue #313 added.
56
108
  def resolve(node, scope)
57
109
  return nil if node.nil? || scope.nil?
58
110
 
@@ -63,20 +115,85 @@ module Rigor
63
115
  when Prism::LocalVariableReadNode, Prism::LocalVariableWriteNode then scope.optimistic_local(node.name)
64
116
  when Prism::InstanceVariableReadNode, Prism::InstanceVariableWriteNode then scope.optimistic_ivar(node.name)
65
117
  when Prism::AndNode, Prism::OrNode then resolve(node.left, scope) || resolve(node.right, scope)
66
- when Prism::CallNode then resolve_through_predicate(node, scope)
118
+ when Prism::CallNode then resolve_through_safe_navigation(node, scope) || resolve_through_predicate(node, scope)
67
119
  when Prism::ParenthesesNode then resolve_through_parentheses(node, scope)
68
120
  end
69
121
  end
70
122
 
71
- # `recv.nil?` / `!recv` — the fold is a statement about `recv`, so it is exactly as optimistic as `recv`
72
- # is. A block or any argument means this is not the unary predicate it looks like (`x.!(y)` is a
73
- # user-defined operator), and the derivation declines.
123
+ # `recv&.m` is `nil` exactly when `recv` is (or when `m` answers `nil`), so it restates `recv`'s presence
124
+ # and is as optimistic as `recv`. Only the call carrying the `&.` is derived: Ruby skips that one call and
125
+ # no more, so `recv&.m.n` sends `n` to the `nil` and raises (or answers `NilClass#n`), and neither it nor
126
+ # a plain read `recv.m` produces the `nil` a miss would — their own nil-freeness rests on the method's
127
+ # answer. A nil-collapsing predicate over the call (`recv&.m.nil?`) still resolves, through
128
+ # {.resolve_through_predicate} and back here.
129
+ def resolve_through_safe_navigation(node, scope)
130
+ resolve(node.receiver, scope) if node.safe_navigation?
131
+ end
132
+
133
+ # `recv.nil?` / `!recv` / `recv == nil` — the fold is a statement about `recv`, so it is exactly as
134
+ # optimistic as `recv` is.
74
135
  def resolve_through_predicate(node, scope)
75
- return nil unless NIL_COLLAPSING_PREDICATES.include?(node.name)
136
+ operand = nil_question_operand(node)
137
+ operand && resolve(operand, scope)
138
+ end
139
+
140
+ # The operand a nil-collapsing predicate restates the nil-ness of, or nil when `node` is not one. A block
141
+ # or any argument means `nil?` / `!` is not the unary predicate it looks like (`x.!(y)` is a user-defined
142
+ # operator). A comparison needs exactly one positional argument and exactly one `nil` literal side, and
143
+ # answers the other side (`nil == nil` states nothing about a carrier).
144
+ def nil_question_operand(node)
76
145
  return nil unless node.block.nil?
77
- return nil unless node.arguments.nil? || node.arguments.arguments.empty?
78
146
 
79
- resolve(node.receiver, scope)
147
+ arguments = node.arguments&.arguments || []
148
+ if NIL_COLLAPSING_PREDICATES.include?(node.name)
149
+ node.receiver if arguments.empty?
150
+ elsif NIL_COMPARISONS.include?(node.name) && arguments.size == 1
151
+ nil_comparison_operand(node.receiver, arguments.first)
152
+ end
153
+ end
154
+
155
+ def nil_comparison_operand(receiver, argument)
156
+ if argument.is_a?(Prism::NilNode) && !receiver.nil? && !receiver.is_a?(Prism::NilNode)
157
+ receiver
158
+ elsif receiver.is_a?(Prism::NilNode) && !argument.is_a?(Prism::NilNode)
159
+ argument
160
+ end
161
+ end
162
+
163
+ # What a marked expression answers when the carrier its mark rests on misses, or {UNKNOWN_MISS}. The
164
+ # carrier itself — the read the mark is recorded on, or a `recv&.m` over a marked receiver — answers `nil`,
165
+ # and each nil-collapsing predicate over it answers what it answers for that value: `!` its negation,
166
+ # `nil?` / `== nil` whether it is `nil`, `!= nil` the reverse. A local / ivar read answers what its binding
167
+ # recorded beside the mark ({BoundMark}, issue #1302): `nil` for `x = h[k]` and for `x = recv&.m?`. An
168
+ # `&&` / `||`, a binding that recorded nothing, and anything else is unknown. `ExpressionTyper` widens a
169
+ # predicate's folded boolean only when the miss answers the other boolean (or cannot be told), so
170
+ # `!recv&.empty?` — `!false` on a hit, `!nil` on a miss — stays `true`, and so does `x = recv&.empty?; !x`.
171
+ def miss_answer(node, scope)
172
+ return nil if scope.optimistic_origins[node]
173
+
174
+ case node
175
+ when Prism::LocalVariableReadNode then scope.optimistic_local_miss(node.name)
176
+ when Prism::InstanceVariableReadNode then scope.optimistic_ivar_miss(node.name)
177
+ when Prism::CallNode then miss_answer_of_call(node, scope)
178
+ when Prism::ParenthesesNode
179
+ body = node.body
180
+ body.is_a?(Prism::StatementsNode) && body.body.size == 1 ? miss_answer(body.body.first, scope) : UNKNOWN_MISS
181
+ else UNKNOWN_MISS
182
+ end
183
+ end
184
+
185
+ def miss_answer_of_call(node, scope)
186
+ return nil if node.safe_navigation? && resolve(node.receiver, scope)
187
+
188
+ operand = nil_question_operand(node)
189
+ inner = operand ? miss_answer(operand, scope) : UNKNOWN_MISS
190
+ return UNKNOWN_MISS if inner.equal?(UNKNOWN_MISS)
191
+
192
+ case node.name
193
+ when :! then !inner
194
+ when :!= then !inner.nil?
195
+ else inner.nil?
196
+ end
80
197
  end
81
198
 
82
199
  # `(x.nil?)` — a single-statement parenthesised body is its own value, and authors do parenthesise a
@@ -89,6 +206,32 @@ module Rigor
89
206
  resolve(body.body.first, scope)
90
207
  end
91
208
 
209
+ # The mark a multiple assignment's right-hand side hands to the slots it fills, in the shape
210
+ # `MultiTargetBinder.bind_marked`'s `optimistic:` takes. `true` when the whole right-hand side resolves
211
+ # to a mark: a miss makes it `nil`, and `k, v = nil` binds `nil` to every fixed slot, so each slot's
212
+ # nil-freeness is the same bet (the rule a nested target under an optimistic slot already follows). A
213
+ # literal array right-hand side (`x, y = h[k], 1`) is judged element-wise instead — an Array of each
214
+ # element's own marks, nested for a nested literal — since each element is its slot's value; a splat
215
+ # element makes the slot positions unknowable and declines. `false` when nothing is marked.
216
+ def destructuring_marks(node, scope)
217
+ return true if resolve(node, scope)
218
+ return false unless node.is_a?(Prism::ArrayNode) && node.elements.none?(Prism::SplatNode)
219
+
220
+ marks = node.elements.map { |element| destructuring_marks(element, scope) }
221
+ marks.any? { |mark| mark != false } ? marks : false
222
+ end
223
+
224
+ # Issue #1302 — the miss answer every slot {.destructuring_marks} marks records: `nil` when each marked
225
+ # part of the right-hand side answers `nil` on a miss, since `nil` destructures to `nil` in every slot, and
226
+ # {UNKNOWN_MISS} otherwise. A marked right-hand side a miss makes a boolean (`a, b = !h[k]`) binds it to
227
+ # the first slot only, so its slots are not told apart and all of them record nothing.
228
+ def destructuring_miss(node, scope)
229
+ return miss_answer(node, scope).nil? ? nil : UNKNOWN_MISS if resolve(node, scope)
230
+ return nil unless node.is_a?(Prism::ArrayNode)
231
+
232
+ node.elements.all? { |element| destructuring_miss(element, scope).nil? } ? nil : UNKNOWN_MISS
233
+ end
234
+
92
235
  # Whether the overload the selector actually picked carries the ignored annotation. The judgment is
93
236
  # per-overload, which is what makes it precise: `Array#first` is optimistic while `Array#first(3)` is
94
237
  # not, and `String#[]` / `Enumerable#find` are honest because they already spell the miss as `?`.
@@ -265,7 +265,7 @@ module Rigor
265
265
  method_visibilities: :discovered_method_visibilities,
266
266
  methods: :discovered_methods,
267
267
  data_member_layouts: :data_member_layouts,
268
- struct_member_layouts: :struct_member_layouts
268
+ struct_member_layouts: :struct_member_layouts, deferred_ranges: :discovered_deferred_ranges
269
269
  }.freeze
270
270
  private_constant :DISCOVERY_FIELD
271
271
 
@@ -329,7 +329,8 @@ module Rigor
329
329
  arg.is_a?(Prism::AssocSplatNode)
330
330
  end
331
331
 
332
- # @return , nil]
332
+ # @return the callee's `[class_name, method_name, kind, def_node]`, or nil when the receiver's class
333
+ # or the method's `def` is not in the project index
333
334
  def resolve_callee(call_node, scope, index)
334
335
  if call_node.receiver.nil?
335
336
  class_name, kind = implicit_self_target(scope)
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../type"
4
+ require_relative "external_ancestor_resolution"
5
+
6
+ module Rigor
7
+ module Inference
8
+ # Issue #1234 — does the PROJECT define the method a receiver answers a name with? The captured-binding
9
+ # pass reads a catalogued iterator name on an unclassified receiver as repetition
10
+ # ({ClosureEscapeAnalyzer.repeats_by_name?}), and that reading is wrong for a method the project wrote
11
+ # under the same name: `class Vault; def select(key) = yield(key.to_s); end` runs its block once. The
12
+ # ancestry step of {ClosureEscapeAnalyzer.classify} asks the same question, of the same call, to rule the
13
+ # project out as the method's owner.
14
+ #
15
+ # "Defines" is a `def`, `define_method` or `attr_*` anywhere in a project class's ancestry, on the
16
+ # instance side or on the singleton side for a class-object receiver, or a signature whose declaring
17
+ # owner is a project class or module.
18
+ module ProjectMethodOwnership
19
+ NO_TARGETS = [].freeze
20
+ private_constant :NO_TARGETS
21
+
22
+ MEMO_KEY = :__rigor_project_method_ownership_memo__
23
+ private_constant :MEMO_KEY
24
+
25
+ module_function
26
+
27
+ # The `[class_name, kind]` pairs the receiver's method is looked up on — `[]` for a receiver Rigor
28
+ # cannot see at all — or nil when the carrier names a class this cannot resolve, or is one it does not
29
+ # recognise, so a caller that cannot tell whether the project owns the method can decline. The audit,
30
+ # carrier by carrier:
31
+ #
32
+ # - `Dynamic`, `Top` — unseen: `[]`.
33
+ # - `Nominal`, `StructInstance`, `DataInstance` — their `class_name`, instance side.
34
+ # - `Singleton`, `StructClass`, `DataClass` — their `class_name`, singleton side.
35
+ # - `Tuple` → `Array`, `HashShape` → `Hash`, `IntegerRange` → `Integer`, `FloatRange` → `Float`,
36
+ # `Constant` → its value's class, `BoundMethod` → `Method`: instance side of that core class, which a
37
+ # project reopening may still define the method on.
38
+ # - `Union`, `Intersection` — every member's targets; a member it cannot resolve resolves the whole.
39
+ # - `Refined`, `Difference` — their `base`. `App` — its erasure `bound`.
40
+ # - A struct / data carrier with no `class_name` (an anonymous `Struct.new` value), `Bot`, `Maybe`,
41
+ # `Result`, and any carrier added later — nil.
42
+ def targets(receiver_type)
43
+ case receiver_type
44
+ when Type::Dynamic, Type::Top then NO_TARGETS
45
+ when Type::Nominal, Type::StructInstance, Type::DataInstance
46
+ class_target(receiver_type.class_name, :instance)
47
+ when Type::Singleton, Type::StructClass, Type::DataClass
48
+ class_target(receiver_type.class_name, :singleton)
49
+ when Type::Union, Type::Intersection then member_targets(receiver_type.members)
50
+ when Type::Refined, Type::Difference then targets(receiver_type.base)
51
+ when Type::App then targets(receiver_type.bound)
52
+ else core_targets(receiver_type)
53
+ end
54
+ end
55
+
56
+ # Whether the project's source, or a signature whose declaring owner is a project class or module,
57
+ # defines `method_sym` on `class_name` or an ancestor the project declares. Memoised ({.memo}): the
58
+ # ancestry step and the name reading ask it of the same call, and every block call on the class asks
59
+ # it again.
60
+ def defines?(class_name, method_sym, kind, scope)
61
+ return false if scope.nil?
62
+
63
+ by_method = (memo(scope)[kind][class_name] ||= {})
64
+ return by_method[method_sym] if by_method.key?(method_sym)
65
+
66
+ by_method[method_sym] = compute_defines?(class_name, method_sym, kind, scope)
67
+ end
68
+
69
+ # One slot per thread, keyed on the identity of the scope's frozen discovery index and its environment —
70
+ # the only inputs {.defines?} and the ancestry step read — and replaced, not accumulated, when either
71
+ # changes: the shape of `ExpressionTyper#class_graph_buckets`, whose rationale applies unchanged. A
72
+ # `Scope` merges each file's discovery with the project pre-pass, so the slot turns over per file, and
73
+ # an ADR-46 dependency the first lookup recorded belongs to the file every later hit serves. Pool
74
+ # workers are separate processes, and a Ractor has its own `Thread.current`. The `:ancestry` bucket
75
+ # belongs to {ClosureEscapeAnalyzer}'s ancestry step.
76
+ def memo(scope)
77
+ discovery = scope.discovery
78
+ environment = scope.environment
79
+ slot = Thread.current[MEMO_KEY]
80
+ unless slot && slot[0].equal?(discovery) && slot[1].equal?(environment)
81
+ slot = [discovery, environment, { instance: {}, singleton: {}, ancestry: {} }]
82
+ Thread.current[MEMO_KEY] = slot
83
+ end
84
+ slot[2]
85
+ end
86
+
87
+ class << self
88
+ private
89
+
90
+ def class_target(class_name, kind)
91
+ class_name.nil? ? nil : [[class_name.to_s, kind]]
92
+ end
93
+
94
+ def member_targets(members)
95
+ members.each_with_object([]) do |member, out|
96
+ resolved = targets(member)
97
+ return nil if resolved.nil?
98
+
99
+ out.concat(resolved)
100
+ end
101
+ end
102
+
103
+ def core_targets(receiver_type)
104
+ case receiver_type
105
+ when Type::Tuple then class_target("Array", :instance)
106
+ when Type::HashShape then class_target("Hash", :instance)
107
+ when Type::IntegerRange then class_target("Integer", :instance)
108
+ when Type::FloatRange then class_target("Float", :instance)
109
+ when Type::Constant then class_target(receiver_type.value.class.name, :instance)
110
+ when Type::BoundMethod then class_target("Method", :instance)
111
+ end
112
+ end
113
+
114
+ def compute_defines?(class_name, method_sym, kind, scope)
115
+ return true if source_defines?(class_name, method_sym, kind, scope)
116
+
117
+ # `ExternalAncestorResolution.method_definition` owns the rescue for a malformed signature.
118
+ definition = ExternalAncestorResolution.method_definition(class_name, method_sym, kind, scope: scope)
119
+ owner = definition.respond_to?(:defined_in) ? definition.defined_in : nil
120
+ !owner.nil? && scope.known_user_class?(owner.to_s.delete_prefix("::"))
121
+ end
122
+
123
+ def source_defines?(class_name, method_sym, kind, scope)
124
+ return true if scope.discovered_method_through_ancestors?(class_name, method_sym, kind)
125
+
126
+ found, = if kind == :singleton
127
+ scope.singleton_def_through_ancestors(class_name, method_sym)
128
+ else
129
+ scope.user_def_through_ancestors(class_name, method_sym)
130
+ end
131
+ !found.nil?
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "global_write_census"
4
+
3
5
  module Rigor
4
6
  module Inference
5
7
  # ADR-17 § "Inference contract" — project-wide patched-method registry populated by the pre-eval
@@ -26,16 +28,19 @@ module Rigor
26
28
  end
27
29
  end
28
30
 
29
- attr_reader :by_key
31
+ attr_reader :by_key, :write_census
30
32
 
31
33
  # @param entries — flat list of declarations observed during the pre-pass.
32
34
  # First-write-wins on `(class_name, method_name, kind)` duplicates so the
33
35
  # `pre-eval.duplicate-declaration` diagnostic emission stays decoupled from registry behaviour.
34
- def initialize(entries: [])
36
+ # @param write_census — issue #1367: the `pre_eval:` files' {GlobalWriteCensus}, which the `global.*` write
37
+ # rules join with the project's, since a patch file is loaded ahead of the code it patches.
38
+ def initialize(entries: [], write_census: GlobalWriteCensus::EMPTY)
35
39
  @by_key = entries.each_with_object({}) do |entry, acc|
36
40
  key = [entry.class_name, entry.method_name, entry.kind]
37
41
  acc[key] ||= entry
38
42
  end.freeze
43
+ @write_census = write_census.frozen? ? write_census : write_census.dup.freeze
39
44
  freeze
40
45
  end
41
46