rigortype 0.3.9 → 0.4.1

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 (407) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -4
  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/08-understanding-errors.md +3 -2
  14. data/docs/handbook/10-sorbet.md +9 -10
  15. data/docs/handbook/11-sig-gen.md +454 -13
  16. data/docs/install.md +87 -15
  17. data/docs/manual/02-cli-reference.md +65 -4
  18. data/docs/manual/03-configuration.md +12 -0
  19. data/docs/manual/04-diagnostics.md +4 -0
  20. data/docs/manual/07-plugins.md +1 -1
  21. data/docs/manual/08-skills.md +4 -3
  22. data/docs/manual/10-mcp-server.md +3 -2
  23. data/docs/manual/12-caching.md +43 -4
  24. data/docs/manual/14-rails-quickstart.md +19 -11
  25. data/docs/manual/16-rbs-extended-annotations.md +40 -7
  26. data/docs/manual/19-effect-labels.md +10 -0
  27. data/docs/manual/plugins/README.md +7 -0
  28. data/docs/manual/plugins/rigor-actioncable.md +8 -1
  29. data/docs/manual/plugins/rigor-actionmailer.md +7 -0
  30. data/docs/manual/plugins/rigor-actionpack.md +243 -0
  31. data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
  32. data/docs/manual/plugins/rigor-activejob.md +7 -0
  33. data/docs/manual/plugins/rigor-activerecord.md +110 -4
  34. data/docs/manual/plugins/rigor-activestorage.md +8 -1
  35. data/docs/manual/plugins/rigor-activesupport-core-ext.md +11 -5
  36. data/docs/manual/plugins/rigor-grape.md +106 -0
  37. data/docs/manual/plugins/rigor-graphql.md +23 -2
  38. data/docs/manual/plugins/rigor-pundit.md +8 -1
  39. data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
  40. data/docs/manual/plugins/rigor-rails-routes.md +7 -0
  41. data/docs/manual/plugins/rigor-rbs-inline.md +212 -25
  42. data/docs/manual/plugins/rigor-sidekiq.md +8 -1
  43. data/docs/manual/plugins/rigor-sorbet.md +21 -5
  44. data/exe/rigor +19 -4
  45. data/lib/rigor/analysis/baseline.rb +1 -1
  46. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
  47. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
  48. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -3
  49. data/lib/rigor/analysis/check_rules/rule_ids.rb +20 -5
  50. data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
  51. data/lib/rigor/analysis/check_rules/source_arity.rb +427 -0
  52. data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
  53. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +36 -2
  54. data/lib/rigor/analysis/check_rules.rb +638 -136
  55. data/lib/rigor/analysis/dependency_recorder.rb +222 -17
  56. data/lib/rigor/analysis/fact_store.rb +9 -0
  57. data/lib/rigor/analysis/incremental.rb +26 -0
  58. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +266 -0
  59. data/lib/rigor/analysis/incremental_run_slot.rb +171 -0
  60. data/lib/rigor/analysis/incremental_session.rb +309 -53
  61. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
  62. data/lib/rigor/analysis/project_scan.rb +11 -1
  63. data/lib/rigor/analysis/reachability/plugin_roots.rb +0 -1
  64. data/lib/rigor/analysis/reachability/scan_cache.rb +3 -1
  65. data/lib/rigor/analysis/rule_catalog.rb +127 -0
  66. data/lib/rigor/analysis/run_cache_key.rb +46 -18
  67. data/lib/rigor/analysis/run_stats.rb +2 -2
  68. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +168 -12
  69. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -4
  70. data/lib/rigor/analysis/runner/pool_coordinator.rb +109 -17
  71. data/lib/rigor/analysis/runner/project_pre_passes.rb +44 -17
  72. data/lib/rigor/analysis/runner.rb +603 -54
  73. data/lib/rigor/analysis/shadow_harness.rb +27 -0
  74. data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
  75. data/lib/rigor/analysis/template_unit_collector.rb +303 -0
  76. data/lib/rigor/analysis/template_unit_paths.rb +93 -0
  77. data/lib/rigor/analysis/template_unit_positions.rb +293 -0
  78. data/lib/rigor/analysis/template_units.rb +409 -0
  79. data/lib/rigor/analysis/worker_session.rb +57 -13
  80. data/lib/rigor/bleeding_edge.rb +0 -2
  81. data/lib/rigor/builtins/hkt_builtins.rb +1 -0
  82. data/lib/rigor/builtins/imported_refinements.rb +4 -0
  83. data/lib/rigor/builtins/regex_refinement.rb +17 -9
  84. data/lib/rigor/builtins/static_return_refinements.rb +2 -0
  85. data/lib/rigor/cache/descriptor.rb +45 -1
  86. data/lib/rigor/cache/engine_source.rb +29 -1
  87. data/lib/rigor/cache/file_digest.rb +28 -0
  88. data/lib/rigor/cache/incremental_snapshot.rb +102 -8
  89. data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
  90. data/lib/rigor/cache/rbs_descriptor.rb +37 -7
  91. data/lib/rigor/cache/store.rb +63 -4
  92. data/lib/rigor/cache/value_digest.rb +325 -0
  93. data/lib/rigor/ci_detector.rb +1 -0
  94. data/lib/rigor/cli/check_command.rb +83 -13
  95. data/lib/rigor/cli/doc_links.rb +1 -1
  96. data/lib/rigor/cli/docs_command.rb +4 -4
  97. data/lib/rigor/cli/doctor_command.rb +1 -1
  98. data/lib/rigor/cli/effects_report.rb +2 -2
  99. data/lib/rigor/cli/plugin_command.rb +3 -3
  100. data/lib/rigor/cli/prism_colorizer.rb +0 -1
  101. data/lib/rigor/cli/sig_gen_command.rb +210 -29
  102. data/lib/rigor/cli/skill_command.rb +1 -1
  103. data/lib/rigor/cli/skill_describe.rb +0 -1
  104. data/lib/rigor/cli/type_of_command.rb +19 -5
  105. data/lib/rigor/cli/type_of_renderer.rb +20 -9
  106. data/lib/rigor/cli/type_of_template_probe.rb +189 -0
  107. data/lib/rigor/cli/unused_command.rb +3 -3
  108. data/lib/rigor/cli.rb +21 -3
  109. data/lib/rigor/config_audit.rb +3 -2
  110. data/lib/rigor/config_path.rb +29 -0
  111. data/lib/rigor/configuration/severity_profile.rb +24 -3
  112. data/lib/rigor/configuration.rb +71 -7
  113. data/lib/rigor/effects/ancestry_recorder.rb +191 -0
  114. data/lib/rigor/effects/attribution.rb +11 -2
  115. data/lib/rigor/effects/callee_rule.rb +368 -0
  116. data/lib/rigor/effects/catalog.rb +7 -4
  117. data/lib/rigor/effects/collector.rb +11 -5
  118. data/lib/rigor/effects/config_envelopes.rb +9 -2
  119. data/lib/rigor/effects/definition_context.rb +179 -0
  120. data/lib/rigor/effects/effect_table.rb +11 -3
  121. data/lib/rigor/effects/envelope_check.rb +1 -1
  122. data/lib/rigor/effects/envelope_index.rb +15 -0
  123. data/lib/rigor/effects/file_collection.rb +59 -5
  124. data/lib/rigor/effects/framework_units.rb +1 -1
  125. data/lib/rigor/effects/identity.rb +16 -0
  126. data/lib/rigor/effects/local_ownership.rb +38 -12
  127. data/lib/rigor/effects/method_key.rb +21 -0
  128. data/lib/rigor/effects/mutation_classifier.rb +23 -12
  129. data/lib/rigor/effects/plugin_facts.rb +49 -37
  130. data/lib/rigor/effects/propagator.rb +295 -16
  131. data/lib/rigor/effects/registry.rb +1 -1
  132. data/lib/rigor/effects/scanner.rb +121 -72
  133. data/lib/rigor/effects/signature_sources.rb +1 -1
  134. data/lib/rigor/effects/snapshot.rb +2 -1
  135. data/lib/rigor/effects/summary.rb +27 -4
  136. data/lib/rigor/effects/unit_scan.rb +385 -30
  137. data/lib/rigor/effects/visibility.rb +101 -0
  138. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
  139. data/lib/rigor/environment/lockfile_resolver.rb +27 -1
  140. data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
  141. data/lib/rigor/environment/member_consistency.rb +298 -0
  142. data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
  143. data/lib/rigor/environment/rbs_loader.rb +400 -134
  144. data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
  145. data/lib/rigor/environment.rb +104 -96
  146. data/lib/rigor/hashing/xxh3.rb +264 -0
  147. data/lib/rigor/inference/acceptance.rb +139 -9
  148. data/lib/rigor/inference/block_auto_splat.rb +216 -0
  149. data/lib/rigor/inference/block_call_timing.rb +338 -0
  150. data/lib/rigor/inference/block_parameter_binder.rb +88 -26
  151. data/lib/rigor/inference/block_repetition.rb +71 -0
  152. data/lib/rigor/inference/body_fixpoint.rb +2 -1
  153. data/lib/rigor/inference/budget_trace.rb +2 -1
  154. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  155. data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
  156. data/lib/rigor/inference/captured_locals.rb +393 -15
  157. data/lib/rigor/inference/closure_escape_analyzer.rb +157 -13
  158. data/lib/rigor/inference/content_join.rb +200 -27
  159. data/lib/rigor/inference/declaration_walk/context.rb +195 -0
  160. data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
  161. data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
  162. data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
  163. data/lib/rigor/inference/declaration_walk.rb +7 -0
  164. data/lib/rigor/inference/def_node_resolver.rb +3 -2
  165. data/lib/rigor/inference/def_return_typer.rb +11 -7
  166. data/lib/rigor/inference/define_method_block_self.rb +64 -0
  167. data/lib/rigor/inference/definer_resolution.rb +266 -0
  168. data/lib/rigor/inference/element_read_widening.rb +22 -10
  169. data/lib/rigor/inference/error_info.rb +196 -0
  170. data/lib/rigor/inference/expression_typer.rb +1665 -538
  171. data/lib/rigor/inference/external_ancestor_resolution.rb +264 -0
  172. data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
  173. data/lib/rigor/inference/global_write_census.rb +239 -0
  174. data/lib/rigor/inference/guard_rebinding.rb +488 -0
  175. data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
  176. data/lib/rigor/inference/index_write_widening.rb +16 -3
  177. data/lib/rigor/inference/indexed_narrowing.rb +61 -9
  178. data/lib/rigor/inference/jump_targets.rb +82 -0
  179. data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
  180. data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
  181. data/lib/rigor/inference/last_line.rb +340 -0
  182. data/lib/rigor/inference/last_status.rb +144 -0
  183. data/lib/rigor/inference/macro_block_self_type.rb +179 -17
  184. data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
  185. data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
  186. data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
  187. data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
  188. data/lib/rigor/inference/match_rebinding.rb +392 -0
  189. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  190. data/lib/rigor/inference/method_dispatcher/block_folding.rb +85 -24
  191. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
  192. data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
  193. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +4 -0
  194. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
  195. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +113 -73
  196. data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
  197. data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
  198. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +901 -38
  199. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
  200. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +128 -53
  201. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +1 -1
  202. data/lib/rigor/inference/method_dispatcher.rb +214 -13
  203. data/lib/rigor/inference/method_parameter_binder.rb +8 -3
  204. data/lib/rigor/inference/module_function_state.rb +213 -0
  205. data/lib/rigor/inference/multi_target_binder.rb +345 -52
  206. data/lib/rigor/inference/mutation_rejoin.rb +4 -1
  207. data/lib/rigor/inference/mutation_widening.rb +70 -48
  208. data/lib/rigor/inference/narrowing.rb +674 -124
  209. data/lib/rigor/inference/operand_effects.rb +167 -0
  210. data/lib/rigor/inference/operand_walk.rb +88 -0
  211. data/lib/rigor/inference/optimistic_origin.rb +152 -9
  212. data/lib/rigor/inference/parameter_inference_collector.rb +13 -11
  213. data/lib/rigor/inference/project_method_ownership.rb +136 -0
  214. data/lib/rigor/inference/project_patched_methods.rb +7 -2
  215. data/lib/rigor/inference/project_patched_scanner.rb +7 -3
  216. data/lib/rigor/inference/receiver_alias.rb +90 -1
  217. data/lib/rigor/inference/receiver_blind_block.rb +219 -0
  218. data/lib/rigor/inference/refinement_mutation.rb +15 -11
  219. data/lib/rigor/inference/repeated_or_writes.rb +463 -0
  220. data/lib/rigor/inference/return_barrier.rb +54 -0
  221. data/lib/rigor/inference/rewrite_mutation.rb +120 -0
  222. data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
  223. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
  224. data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
  225. data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
  226. data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
  227. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +130 -0
  228. data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
  229. data/lib/rigor/inference/scope_indexer.rb +5913 -701
  230. data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
  231. data/lib/rigor/inference/statement_evaluator.rb +3249 -497
  232. data/lib/rigor/inference/stored_block_call.rb +54 -0
  233. data/lib/rigor/inference/string_mutation.rb +44 -7
  234. data/lib/rigor/inference/unknown_store_widening.rb +200 -0
  235. data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
  236. data/lib/rigor/language_server/debouncer.rb +0 -1
  237. data/lib/rigor/language_server/diagnostic_publisher.rb +3 -2
  238. data/lib/rigor/language_server/hover_renderer.rb +3 -3
  239. data/lib/rigor/language_server/project_context.rb +5 -3
  240. data/lib/rigor/mcp/server.rb +2 -1
  241. data/lib/rigor/plugin/base.rb +196 -13
  242. data/lib/rigor/plugin/box_probe.rb +91 -0
  243. data/lib/rigor/plugin/bundled_catalog.rb +1 -1
  244. data/lib/rigor/plugin/effect_attribution.rb +58 -4
  245. data/lib/rigor/plugin/io_boundary.rb +82 -4
  246. data/lib/rigor/plugin/isolation.rb +1 -1
  247. data/lib/rigor/plugin/loader.rb +2 -1
  248. data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
  249. data/lib/rigor/plugin/manifest.rb +71 -10
  250. data/lib/rigor/plugin/registry.rb +35 -1
  251. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
  252. data/lib/rigor/plugin/template_unit.rb +196 -0
  253. data/lib/rigor/plugin/trust_policy.rb +3 -3
  254. data/lib/rigor/plugin.rb +1 -0
  255. data/lib/rigor/protection/discovery_seed.rb +20 -9
  256. data/lib/rigor/protection/kill_signature.rb +0 -1
  257. data/lib/rigor/protection/mutation_cache.rb +4 -5
  258. data/lib/rigor/rbs_extended.rb +27 -0
  259. data/lib/rigor/reflection/constant_ancestors.rb +146 -0
  260. data/lib/rigor/reflection/constant_path.rb +41 -13
  261. data/lib/rigor/reflection.rb +73 -107
  262. data/lib/rigor/scope/discovery_index.rb +351 -2
  263. data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
  264. data/lib/rigor/scope/resolution_chain.rb +884 -0
  265. data/lib/rigor/scope.rb +981 -198
  266. data/lib/rigor/sig_gen/alias_index.rb +289 -0
  267. data/lib/rigor/sig_gen/classification.rb +22 -5
  268. data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
  269. data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
  270. data/lib/rigor/sig_gen/generator.rb +580 -47
  271. data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
  272. data/lib/rigor/sig_gen/method_candidate.rb +47 -3
  273. data/lib/rigor/sig_gen/observation_collector.rb +1 -1
  274. data/lib/rigor/sig_gen/renderer.rb +124 -9
  275. data/lib/rigor/sig_gen/skip_reason_catalog.rb +44 -1
  276. data/lib/rigor/sig_gen/write_result.rb +19 -3
  277. data/lib/rigor/sig_gen/writer.rb +166 -35
  278. data/lib/rigor/sig_gen.rb +3 -0
  279. data/lib/rigor/signature_path_audit.rb +1 -1
  280. data/lib/rigor/source/node_walker.rb +0 -3
  281. data/lib/rigor/source/parameter_envelope.rb +72 -0
  282. data/lib/rigor/source.rb +1 -0
  283. data/lib/rigor/type/combinator.rb +112 -12
  284. data/lib/rigor/type/difference.rb +1 -0
  285. data/lib/rigor/type/hash_shape.rb +1 -1
  286. data/lib/rigor/type/refined.rb +1 -0
  287. data/lib/rigor/version.rb +1 -1
  288. data/lib/rigor.rb +1 -0
  289. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
  290. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +9 -5
  291. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -4
  292. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +0 -1
  293. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
  294. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
  295. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
  296. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
  297. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
  298. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
  299. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +239 -4
  300. data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
  301. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
  302. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
  303. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
  304. data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
  305. data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
  306. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -0
  307. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +9 -5
  308. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
  309. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
  310. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +380 -3
  311. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
  312. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +281 -35
  313. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
  314. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -14
  315. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
  316. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +171 -5
  317. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +2 -0
  318. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +1 -0
  319. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
  320. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +10 -0
  321. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
  322. data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
  323. data/plugins/rigor-grape/sig/grape.rbs +263 -0
  324. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +72 -2
  325. data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
  326. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -0
  327. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +11 -7
  328. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +35 -37
  329. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -1
  330. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -0
  331. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +1 -0
  332. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +9 -17
  333. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
  334. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +217 -36
  335. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +1 -0
  336. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +9 -5
  337. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +0 -1
  338. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +46 -1
  339. data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
  340. data/sig/rigor/analysis/baseline.rbs +69 -7
  341. data/sig/rigor/analysis/fact_store.rbs +1 -1
  342. data/sig/rigor/analysis/project_scan.rbs +74 -0
  343. data/sig/rigor/effects/config_envelopes.rbs +100 -0
  344. data/sig/rigor/effects/effect_table.rbs +60 -0
  345. data/sig/rigor/effects/envelope.rbs +97 -0
  346. data/sig/rigor/effects/envelope_index.rbs +33 -0
  347. data/sig/rigor/effects/file_collection.rbs +81 -0
  348. data/sig/rigor/effects/label.rbs +26 -0
  349. data/sig/rigor/effects/label_set.rbs +46 -0
  350. data/sig/rigor/effects/method_key.rbs +24 -0
  351. data/sig/rigor/effects/origin.rbs +50 -0
  352. data/sig/rigor/effects/plugin_facts.rbs +141 -0
  353. data/sig/rigor/effects/registry.rbs +68 -0
  354. data/sig/rigor/effects/summary.rbs +50 -0
  355. data/sig/rigor/effects/taint_cause.rbs +12 -0
  356. data/sig/rigor/environment.rbs +14 -10
  357. data/sig/rigor/inference/optimistic_origin.rbs +10 -0
  358. data/sig/rigor/inference.rbs +6 -4
  359. data/sig/rigor/plugin/additional_initializer.rbs +36 -0
  360. data/sig/rigor/plugin/base.rbs +30 -7
  361. data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
  362. data/sig/rigor/plugin/effect_attribution.rbs +66 -0
  363. data/sig/rigor/plugin/effect_edge.rbs +33 -0
  364. data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
  365. data/sig/rigor/plugin/io_boundary.rbs +2 -1
  366. data/sig/rigor/plugin/loader.rbs +3 -3
  367. data/sig/rigor/plugin/manifest.rbs +50 -11
  368. data/sig/rigor/plugin/protocol_contract.rbs +68 -0
  369. data/sig/rigor/plugin/registry.rbs +62 -1
  370. data/sig/rigor/plugin.rbs +1 -1
  371. data/sig/rigor/rbs_extended.rbs +1 -1
  372. data/sig/rigor/reflection.rbs +7 -6
  373. data/sig/rigor/scope.rbs +177 -20
  374. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -7
  375. data/sig/rigor/source.rbs +4 -4
  376. data/sig/rigor/testing.rbs +10 -4
  377. data/sig/rigor/type.rbs +6 -0
  378. data/sig/rigor.rbs +38 -20
  379. data/skills/rigor-ask/SKILL.md +8 -5
  380. data/skills/rigor-baseline-reduce/SKILL.md +4 -6
  381. data/skills/rigor-ci-setup/SKILL.md +17 -21
  382. data/skills/rigor-doctor/SKILL.md +24 -22
  383. data/skills/rigor-doctor/references/01-checks.md +97 -33
  384. data/skills/rigor-editor-setup/SKILL.md +6 -4
  385. data/skills/rigor-mcp-setup/SKILL.md +5 -4
  386. data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
  387. data/skills/rigor-next-steps/SKILL.md +4 -2
  388. data/skills/rigor-plugin-author/SKILL.md +19 -23
  389. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
  390. data/skills/rigor-plugin-author/references/02-walker-and-types.md +12 -25
  391. data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
  392. data/skills/rigor-plugin-review/SKILL.md +6 -4
  393. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
  394. data/skills/rigor-plugin-tune/SKILL.md +4 -2
  395. data/skills/rigor-project-init/SKILL.md +46 -18
  396. data/skills/rigor-project-init/references/01-detect.md +13 -9
  397. data/skills/rigor-project-init/references/02-configure.md +40 -10
  398. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
  399. data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
  400. data/skills/rigor-protection-uplift/SKILL.md +4 -6
  401. data/skills/rigor-rbs-setup/SKILL.md +4 -2
  402. data/skills/rigor-type-oracle/SKILL.md +4 -6
  403. data/skills/rigor-type-oracle/references/01-oracle-commands.md +12 -8
  404. data/skills/rigor-type-oracle/references/03-gap-protocol.md +0 -3
  405. data/skills/rigor-unused-adjudicate/SKILL.md +4 -2
  406. data/skills/rigor-upgrade/SKILL.md +13 -8
  407. metadata +132 -1
@@ -0,0 +1,488 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../source/node_children"
6
+ require_relative "../type"
7
+ require_relative "external_ancestor_resolution"
8
+ require_relative "fresh_frame_blocks"
9
+ require_relative "project_method_ownership"
10
+ require_relative "stored_block_call"
11
+
12
+ module Rigor
13
+ module Inference
14
+ # Issue #1429 — where a guard's narrowing of a global or constant stops holding. A guard narrows `$stdout` or
15
+ # `STDOUT` on its edge ({Narrowing}), but any Ruby code that runs between the guard and a read may rebind the
16
+ # global (`$stdout = StringIO.new`) or the constant (`const_set`), and the analysis cannot see that code. So the
17
+ # statement evaluator restores each narrowed name to the union of its pre-guard binding and its narrowed one
18
+ # ({Scope#forget_guard_narrowings}) wherever code this module counts may run:
19
+ #
20
+ # - a call that may run project, gem or unresolved code ({.call_runs_foreign_code?}): a method the project
21
+ # defines on the receiver's class or its ancestry, one whose signature is owned outside Ruby core and the
22
+ # standard library, a core method on a project or gem receiver (`Enumerable#map` runs the class's `each`),
23
+ # an unresolved callee (a `Dynamic` receiver, a name no signature declares), a call that runs code by name
24
+ # (`send`, `instance_eval`, `eval`, `require`, `load`), a call on a code object (`Proc`, `Method`,
25
+ # `Enumerator`, `Fiber`, `Thread`, a delegator), and a block-pass argument (`&blk`);
26
+ # - a literal block whose body may do either, or writes a global or constant itself;
27
+ # - `yield` and `super`, which run code the method does not show.
28
+ #
29
+ # A core or standard-library method on a core or standard-library receiver keeps the narrowing (`$sep.strip`,
30
+ # `$stdout.flush`, `File.read(path)`), and so does a method `Kernel`, `Object` or `BasicObject` owns on any
31
+ # receiver (`puts`, `format`, `obj.frozen?`), so `if $sep; $sep.strip; $sep.length; end` keeps `$sep` non-nil.
32
+ #
33
+ # The accepted gap is implicit conversion: a core method that calls back into a project method the program
34
+ # does not spell (`puts obj` runs `obj.to_s`, `hash[obj]` runs `obj.hash`, `a.sort` runs `<=>`) is read as the
35
+ # core method alone.
36
+ #
37
+ # Issue #1446 — a class guard's narrowing of an instance variable is restored at the same code, since code that
38
+ # may rebind a global may reach `self` and rebind the variable too, and at two more: `instance_variable_set` and
39
+ # `remove_instance_variable` on any receiver, and a write to the narrowed variable in code the scan reads (a
40
+ # block that may run again, a loop's next pass).
41
+ module GuardRebinding
42
+ # Calls that run code chosen by name or by a String.
43
+ CODE_RUNNING_NAMES = Set[
44
+ :send, :__send__, :public_send, :eval, :require, :require_relative, :load,
45
+ # These rebind a constant on any receiver (`Object.const_set(:SEP, nil)`).
46
+ :const_set, :remove_const,
47
+ # These rebind an instance variable of a receiver that may be `self` (#1446).
48
+ :instance_variable_set, :remove_instance_variable
49
+ ].freeze
50
+ # These run their literal block, which the scan reads as any block, or a String of code, which it cannot read.
51
+ BLOCK_OR_CODE_NAMES = Set[
52
+ :instance_eval, :instance_exec, :class_eval, :class_exec, :module_eval, :module_exec
53
+ ].freeze
54
+ # Methods `Kernel`, `Object` or `BasicObject` own that call another method of the receiver, which a project class
55
+ # may define: `r != 1` runs `r == 1`.
56
+ UNIVERSAL_DELEGATES = { :!= => :==, :!~ => :=~, :=== => :==, :respond_to? => :respond_to_missing? }.freeze
57
+ # Receivers whose methods run code the receiver holds: a block, a method, a generator or a delegate.
58
+ CODE_OBJECT_CLASSES = Set[
59
+ "Proc", "Method", "UnboundMethod", "Binding", "Enumerator", "Enumerator::Lazy", "Enumerator::Chain",
60
+ "Enumerator::Yielder", "Fiber", "Thread", "Delegator", "SimpleDelegator"
61
+ ].freeze
62
+ # The owners whose methods a project object answers the same way any object does.
63
+ UNIVERSAL_OWNERS = Set["Kernel", "Object", "BasicObject"].freeze
64
+ # The nodes that write a global or a constant.
65
+ WRITE_NODES = [
66
+ Prism::GlobalVariableWriteNode, Prism::GlobalVariableOrWriteNode, Prism::GlobalVariableAndWriteNode,
67
+ Prism::GlobalVariableOperatorWriteNode, Prism::GlobalVariableTargetNode,
68
+ Prism::ConstantWriteNode, Prism::ConstantOrWriteNode, Prism::ConstantAndWriteNode,
69
+ Prism::ConstantOperatorWriteNode, Prism::ConstantTargetNode, Prism::ConstantPathWriteNode,
70
+ Prism::ConstantPathOrWriteNode, Prism::ConstantPathAndWriteNode, Prism::ConstantPathOperatorWriteNode,
71
+ Prism::ConstantPathTargetNode
72
+ ].freeze
73
+ # Code that runs code the method does not show.
74
+ FOREIGN_NODES = [Prism::YieldNode, Prism::SuperNode, Prism::ForwardingSuperNode].freeze
75
+ REBINDING_NODES = (WRITE_NODES + FOREIGN_NODES).to_set.freeze
76
+ # Nodes that call methods the syntax does not spell as a `CallNode`: an operator write calls its operator (`r +=
77
+ # 1` calls `r.+`), an attribute or index compound write calls the reader and the writer (`r.val ||= 1`, `r[0] +=
78
+ # 1`), and a `for` loop calls `each` on its collection.
79
+ IMPLICIT_CALL_NODES = Set[
80
+ Prism::LocalVariableOperatorWriteNode, Prism::InstanceVariableOperatorWriteNode,
81
+ Prism::ClassVariableOperatorWriteNode, Prism::CallOperatorWriteNode, Prism::CallOrWriteNode,
82
+ Prism::CallAndWriteNode, Prism::IndexOperatorWriteNode, Prism::IndexOrWriteNode, Prism::IndexAndWriteNode,
83
+ Prism::ForNode
84
+ ].freeze
85
+ VARIABLE_OPERATOR_WRITES = {
86
+ Prism::LocalVariableOperatorWriteNode => :local, Prism::InstanceVariableOperatorWriteNode => :ivar,
87
+ Prism::ClassVariableOperatorWriteNode => :cvar
88
+ }.freeze
89
+ # Calls that keep their literal block to run at a later event, after the method returns or between any two of
90
+ # its statements: an exit handler, a signal handler, a global-assignment hook, a trace hook and a finalizer.
91
+ # `nil` keys the Kernel function's spellings (`at_exit`, `self.trap`, `Kernel.at_exit`); a constant keys its
92
+ # singleton methods.
93
+ DEFERRED_BLOCK_CALLS = {
94
+ nil => %i[at_exit trap trace_var], Signal: %i[trap], TracePoint: %i[new trace],
95
+ ObjectSpace: %i[define_finalizer]
96
+ }.transform_values(&:freeze).freeze
97
+ private_constant :CODE_RUNNING_NAMES, :BLOCK_OR_CODE_NAMES, :UNIVERSAL_DELEGATES, :CODE_OBJECT_CLASSES,
98
+ :UNIVERSAL_OWNERS, :WRITE_NODES, :FOREIGN_NODES,
99
+ :REBINDING_NODES, :IMPLICIT_CALL_NODES, :VARIABLE_OPERATOR_WRITES, :DEFERRED_BLOCK_CALLS
100
+
101
+ module_function
102
+
103
+ # True when `call_node`, a statement's own call, may rebind a global or constant once its operands ran: its
104
+ # method may run foreign code, or its literal block may.
105
+ def call_may_rebind?(call_node, scope)
106
+ return true unless call_node.is_a?(Prism::CallNode)
107
+ return true if call_runs_foreign_code?(call_node, scope)
108
+
109
+ block = call_node.block
110
+ block.is_a?(Prism::BlockNode) && may_rebind?(block, ScanScope.block_parameter_scope(call_node, block, scope))
111
+ end
112
+
113
+ # True when the receiver chain or the arguments of `call_node` may rebind one; Ruby runs them before the call.
114
+ def operands_may_rebind?(call_node, scope)
115
+ may_rebind?(call_node.receiver, scope) || may_rebind?(call_node.arguments, scope) ||
116
+ (call_node.block.is_a?(Prism::BlockArgumentNode) && may_rebind?(call_node.block, scope))
117
+ end
118
+
119
+ # True when running `node` may rebind one: it writes a global, a constant or an instance variable a class guard
120
+ # narrowed, yields, calls `super`, or holds a call, spelled or implicit ({.implicit_call_may_rebind?}), whose
121
+ # method may run foreign code. A `def` and a lambda literal run nothing where they are written. Each node is
122
+ # visited once: a call's literal block is reached as one of its children, so a nested block chain costs its
123
+ # size, not its depth's power.
124
+ #
125
+ # A local the scanned code writes has no binding in `scope` yet, so the scan reads it as the code writes it
126
+ # ({ScanScope.with_scanned_locals}): `copy = $sep; copy.length` is a `String` call. A literal block's parameters
127
+ # read as the method's signature yields them ({ScanScope.block_parameter_scope}).
128
+ def may_rebind?(node, scope)
129
+ return false unless node.is_a?(Prism::Node)
130
+
131
+ scan(node, ScanScope.with_scanned_locals(node, scope))
132
+ end
133
+
134
+ def scan(node, scope)
135
+ return false unless node.is_a?(Prism::Node)
136
+ return true if REBINDING_NODES.include?(node.class)
137
+ return true if guarded_ivar_write?(node, scope)
138
+ return false if node.is_a?(Prism::DefNode) || node.is_a?(Prism::LambdaNode)
139
+ return scan_call(node, scope) if node.is_a?(Prism::CallNode)
140
+ return true if IMPLICIT_CALL_NODES.include?(node.class) && implicit_call_may_rebind?(node, scope)
141
+
142
+ found = false
143
+ node.rigor_each_child { |child| found ||= scan(child, scope) }
144
+ found
145
+ end
146
+
147
+ # Issue #1446 — true when `node` writes an instance variable a class guard narrowed
148
+ # ({Scope#guard_narrowed_ivar?}). A write to any other instance variable rebinds nothing a guard recorded.
149
+ def guarded_ivar_write?(node, scope)
150
+ case node
151
+ when Prism::InstanceVariableWriteNode, Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode,
152
+ Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableTargetNode
153
+ scope.guard_narrowed_ivar?(node.name)
154
+ else
155
+ false
156
+ end
157
+ end
158
+
159
+ def scan_call(node, scope)
160
+ return true if call_runs_foreign_code?(node, scope)
161
+ return true if scan(node.receiver, scope) || scan(node.arguments, scope)
162
+
163
+ block = node.block
164
+ return scan(block, scope) unless block.is_a?(Prism::BlockNode)
165
+
166
+ scan(block, ScanScope.block_scope(node, block, scope))
167
+ end
168
+
169
+ # True when `node` is a compound write or `for` loop that calls a method its syntax does not spell.
170
+ def implicit_call_node?(node)
171
+ IMPLICIT_CALL_NODES.include?(node.class)
172
+ end
173
+
174
+ # True when the method a compound write or a `for` loop calls without spelling it may run foreign code
175
+ # ({IMPLICIT_CALL_NODES}). The operator of an attribute or index operator write runs on the value the reader
176
+ # returns, typed through the dispatcher; one it cannot type counts.
177
+ def implicit_call_may_rebind?(node, scope)
178
+ kind = VARIABLE_OPERATOR_WRITES[node.class]
179
+ return type_method_foreign?(variable_type(kind, node.name, scope), node.binary_operator, scope) if kind
180
+ if node.is_a?(Prism::ForNode)
181
+ return type_method_foreign?(ScanScope.dispatch_type(node.collection, scope), :each, scope)
182
+ end
183
+
184
+ compound_write_foreign?(node, scope)
185
+ rescue StandardError
186
+ true
187
+ end
188
+
189
+ def compound_write_foreign?(node, scope)
190
+ receiver_type = compound_receiver_type(node, scope)
191
+ reader, writer = compound_accessors(node)
192
+ return true if [reader, writer].any? { |name| type_method_foreign?(receiver_type, name, scope) }
193
+ return false unless node.respond_to?(:binary_operator)
194
+
195
+ read = compound_read_type(node, receiver_type, reader, scope)
196
+ read.nil? || type_method_foreign?(read, node.binary_operator, scope)
197
+ end
198
+
199
+ def compound_receiver_type(node, scope)
200
+ return ScanScope.dispatch_type(node.receiver, scope) if node.receiver
201
+
202
+ scope.self_type || Type::Combinator.nominal_of("Object")
203
+ end
204
+
205
+ def compound_accessors(node)
206
+ return %i[[] []=] if node.respond_to?(:arguments) && !node.respond_to?(:read_name)
207
+
208
+ [node.read_name, node.write_name]
209
+ end
210
+
211
+ def compound_read_type(node, receiver_type, reader, scope)
212
+ arguments = node.respond_to?(:arguments) && node.arguments ? node.arguments.arguments : []
213
+ MethodDispatcher.dispatch(receiver_type: receiver_type, method_name: reader,
214
+ arg_types: arguments.map { |argument| scope.type_of(argument) },
215
+ environment: scope.environment, scope: scope)
216
+ end
217
+
218
+ def variable_type(kind, name, scope)
219
+ case kind
220
+ when :local then scope.local(name)
221
+ when :ivar then scope.ivar(name)
222
+ else scope.cvar(name)
223
+ end || Type::Combinator.untyped
224
+ end
225
+
226
+ # True when `method_name` on a value of `type` may run foreign code ({.foreign_target?}).
227
+ def type_method_foreign?(type, method_name, scope)
228
+ targets = ProjectMethodOwnership.targets(type)
229
+ return true if targets.nil? || targets.empty?
230
+
231
+ targets.any? { |class_name, kind| foreign_target?(class_name, method_name, kind, scope) }
232
+ end
233
+
234
+ # The scope the body of `block_node` (a block, a lambda literal or an `END { }` body) enters with: `scope` with
235
+ # its guard narrowings restored where the body may run after code that rebinds them. A lambda, an `END` body,
236
+ # a block the call keeps to run later (`proc`, `define_method`) or at a later event ({DEFERRED_BLOCK_CALLS}),
237
+ # the root block of a thread or fiber, the block of a call that itself may run foreign code (`with_retry {
238
+ # $g.length }` runs after the helper's body), and a body that may rebind one itself (a later run reads what an
239
+ # earlier one wrote). A block with no owning call is left alone.
240
+ def block_entry(scope, block_node, call_node)
241
+ return scope unless scope.guard_narrowed?
242
+ return scope.forget_guard_narrowings if block_node.is_a?(Prism::LambdaNode) ||
243
+ block_node.is_a?(Prism::PostExecutionNode)
244
+ return scope unless call_node.is_a?(Prism::CallNode)
245
+
246
+ if StoredBlockCall.stores_block?(call_node) || deferred_block_call?(call_node) ||
247
+ FreshFrameBlocks.fresh_entry?(call_node, scope) || call_runs_foreign_code?(call_node, scope) ||
248
+ may_rebind?(block_node, ScanScope.block_parameter_scope(call_node, block_node, scope))
249
+ return scope.forget_guard_narrowings
250
+ end
251
+
252
+ scope
253
+ end
254
+
255
+ # True when `call_node` is one of {DEFERRED_BLOCK_CALLS}, spelled on its literal receiver.
256
+ def deferred_block_call?(call_node)
257
+ receiver = call_node.receiver
258
+ key = StoredBlockCall.kernel_spelled?(receiver) ? nil : StoredBlockCall.root_constant_name(receiver)
259
+ names = DEFERRED_BLOCK_CALLS[key]
260
+ !names.nil? && names.include?(call_node.name)
261
+ end
262
+
263
+ # True when the method `call_node` calls may run code other than Ruby core and the standard library (see the
264
+ # module comment). Its operands and literal block are not read here.
265
+ def call_runs_foreign_code?(call_node, scope)
266
+ return true if call_node.block.is_a?(Prism::BlockArgumentNode)
267
+ return true if CODE_RUNNING_NAMES.include?(call_node.name)
268
+ return true if BLOCK_OR_CODE_NAMES.include?(call_node.name) &&
269
+ !(call_node.block.is_a?(Prism::BlockNode) && call_node.arguments.nil?)
270
+
271
+ targets = receiver_targets(call_node, scope)
272
+ return true if targets.nil? || targets.empty?
273
+
274
+ targets.any? { |class_name, kind| foreign_target?(class_name, call_node.name, kind, scope) }
275
+ rescue StandardError
276
+ true
277
+ end
278
+
279
+ # The `[class_name, kind]` pairs the call dispatches on ({ProjectMethodOwnership.targets}); an implicit or
280
+ # `self.` receiver reads the scope's `self`, and the top level's `main` is an `Object`.
281
+ def receiver_targets(call_node, scope)
282
+ receiver = call_node.receiver
283
+ if receiver.nil? || receiver.is_a?(Prism::SelfNode)
284
+ self_type = scope.self_type
285
+ return [["Object", :instance]] if self_type.nil?
286
+
287
+ return ProjectMethodOwnership.targets(self_type)
288
+ end
289
+
290
+ ProjectMethodOwnership.targets(ScanScope.dispatch_type(receiver, scope))
291
+ end
292
+
293
+ def foreign_target?(class_name, method_name, kind, scope)
294
+ return true if CODE_OBJECT_CLASSES.include?(class_name)
295
+ return true if ProjectMethodOwnership.defines?(class_name, method_name, kind, scope)
296
+
297
+ owner = method_owner(class_name, method_name, kind, scope)
298
+ return true if owner.nil?
299
+ return universal_delegate_foreign?(class_name, method_name, kind, scope) if UNIVERSAL_OWNERS.include?(owner)
300
+
301
+ loader = scope.environment.rbs_loader
302
+ return true if loader.nil?
303
+
304
+ !(loader.core_or_stdlib_class?(owner) && loader.core_or_stdlib_class?(class_name))
305
+ end
306
+
307
+ # A universal method is foreign only when it calls another method of the receiver the project defines
308
+ # ({UNIVERSAL_DELEGATES}).
309
+ def universal_delegate_foreign?(class_name, method_name, kind, scope)
310
+ delegate = UNIVERSAL_DELEGATES[method_name]
311
+ !delegate.nil? && ProjectMethodOwnership.defines?(class_name, delegate, kind, scope)
312
+ end
313
+
314
+ # The class or module whose signature answers `method_name` on `class_name`: its own RBS, an RBS ancestor of a
315
+ # project class ({ExternalAncestorResolution.resolve}), or `Object`'s for an instance method nothing earlier
316
+ # declares (a project class with no signature calling `puts`). nil when none does.
317
+ def method_owner(class_name, method_name, kind, scope)
318
+ definition = ExternalAncestorResolution.method_definition(class_name, method_name, kind, scope: scope)
319
+ if definition.nil? && kind == :instance
320
+ definition = ExternalAncestorResolution.resolve(class_name, method_name, :instance, scope: scope)&.first
321
+ definition ||= ExternalAncestorResolution.method_definition("Object", method_name, :instance, scope: scope)
322
+ end
323
+ owner = definition.respond_to?(:defined_in) ? definition.defined_in : nil
324
+ owner&.to_s&.delete_prefix("::")
325
+ end
326
+ private_class_method :scan, :guarded_ivar_write?, :scan_call, :receiver_targets, :foreign_target?,
327
+ :universal_delegate_foreign?, :method_owner, :deferred_block_call?,
328
+ :compound_write_foreign?,
329
+ :compound_receiver_type, :compound_accessors, :compound_read_type, :variable_type,
330
+ :type_method_foreign?
331
+ end
332
+
333
+ module GuardRebinding
334
+ # The scope {GuardRebinding.may_rebind?} types a scanned node's receivers in: the locals the scanned code writes
335
+ # and a literal block's parameters, which the scope before the code does not bind.
336
+ module ScanScope
337
+ LOCAL_SCAN_BARRIERS = [Prism::DefNode, Prism::LambdaNode, Prism::ClassNode, Prism::ModuleNode].freeze
338
+ private_constant :LOCAL_SCAN_BARRIERS
339
+
340
+ module_function
341
+
342
+ # `scope` with each local `node` writes read as the union of what it held and each value the code writes it,
343
+ # in source order, so a later write reads an earlier one. A local `scope` already binds keeps its binding in
344
+ # the union: the scanned code may run before or after its own write (a loop body, a block, a rescue clause).
345
+ # A nested `def`, lambda, class or module body is not read.
346
+ def with_scanned_locals(node, scope)
347
+ writes = []
348
+ collect_local_writes(node, writes)
349
+ writes.reduce(scope) do |acc, write|
350
+ written = acc.type_of(write.value)
351
+ existing = acc.local(write.name)
352
+ acc.with_local(write.name, existing ? Type::Combinator.union(existing, written) : written)
353
+ end
354
+ rescue StandardError
355
+ scope
356
+ end
357
+
358
+ def collect_local_writes(node, writes)
359
+ return unless node.is_a?(Prism::Node)
360
+ return if LOCAL_SCAN_BARRIERS.any? { |barrier| node.is_a?(barrier) }
361
+
362
+ writes << node if node.is_a?(Prism::LocalVariableWriteNode)
363
+ node.rigor_each_child { |child| collect_local_writes(child, writes) }
364
+ end
365
+
366
+ # The scope the body of `call_node`'s literal `block` is scanned in, from `scope`, the enclosing scan's: the
367
+ # block's parameters bound ({.block_parameter_scope}), and the locals the body writes typed again from them
368
+ # ({.with_scanned_locals}), since the enclosing scan typed those writes before the parameters were bound. A
369
+ # block without parameters keeps `scope`.
370
+ def block_scope(call_node, block, scope)
371
+ bound = block_parameter_scope(call_node, block, scope)
372
+ bound.equal?(scope) ? scope : with_scanned_locals(block, bound)
373
+ end
374
+
375
+ # `scope` with every local the block's parameter list names bound, so none reads a binding of the same name
376
+ # from outside the block. A plain required parameter reads what the method yields at its position in
377
+ # `requireds` (`MethodDispatcher.expected_block_param_types`); every other name, a destructured, optional,
378
+ # splat, post, keyword, block or `;`-block-local one, reads `Dynamic[top]`, so a call on it counts as an
379
+ # unresolved callee. `scope` itself when the body reads no parameter where the scan types it
380
+ # ({.reads_parameters?}): the bindings would change nothing.
381
+ def block_parameter_scope(call_node, block, scope)
382
+ parameters = block.parameters
383
+ return scope unless parameters.is_a?(Prism::BlockParametersNode)
384
+
385
+ names = []
386
+ collect_parameter_names(parameters, names)
387
+ return scope if names.empty? || !reads_parameters?(block.body, names)
388
+
389
+ bindings = names.to_h { |name| [name, Type::Combinator.untyped] }
390
+ requireds = parameters.parameters&.requireds || []
391
+ unless requireds.none?(Prism::RequiredParameterNode)
392
+ yielded = yielded_types(call_node, scope)
393
+ requireds.each_with_index do |parameter, index|
394
+ next unless parameter.is_a?(Prism::RequiredParameterNode)
395
+
396
+ bindings[parameter.name] = yielded[index] || Type::Combinator.untyped
397
+ end
398
+ end
399
+ bindings.reduce(scope) { |acc, (name, type)| acc.with_local(name, type) }
400
+ end
401
+
402
+ # What the method `call_node` calls yields its block, by position, or `[]` when that cannot be read. A receiver
403
+ # a class guard left `bot` yields what the guarded class does ({.dispatch_type}, #1446).
404
+ def yielded_types(call_node, scope)
405
+ receiver = call_node.receiver ? dispatch_type(call_node.receiver, scope) : scope.self_type
406
+ arguments = call_node.arguments&.arguments || []
407
+ MethodDispatcher.expected_block_param_types(
408
+ receiver_type: receiver, method_name: call_node.name, environment: scope.environment, scope: scope,
409
+ arg_types: arguments.map { |argument| scope.type_of(argument) }
410
+ )
411
+ rescue StandardError
412
+ []
413
+ end
414
+
415
+ # Issue #1446 — the type a call on the receiver `node` dispatches on: its type in `scope`, or, where a class
416
+ # guard disjoint from the receiver's binding left it `bot` (`return unless @io.is_a?(StringIO)` on an `IO`),
417
+ # the classes the guard named ({Narrowing.bot_guard_classes_of}), since the value that passes the guard is one
418
+ # of them. So `@io.rewind` reads as `StringIO#rewind` and keeps the narrowing, while `@cb.call` past
419
+ # `@cb.is_a?(Proc)` reads as a call on a code object, and a method a project subclass or reopening defines on
420
+ # the class still counts. A `bot` no class guard produced stays `bot`, which no target resolves, so a call on
421
+ # it counts as an unresolved callee.
422
+ def dispatch_type(node, scope)
423
+ type = scope.type_of(node)
424
+ return type unless type.is_a?(Type::Bot)
425
+
426
+ classes = Narrowing.bot_guard_classes_of(node, scope)
427
+ return type if classes.nil?
428
+
429
+ Type::Combinator.union(*classes.map { |name| Type::Combinator.nominal_of(name) })
430
+ end
431
+
432
+ # The names a block's parameter list declares: every kind of parameter, a destructured one's parts included,
433
+ # and a `;`-block-local. A default value is not read.
434
+ def collect_parameter_names(node, names)
435
+ case node
436
+ when Prism::RequiredParameterNode, Prism::OptionalParameterNode, Prism::RestParameterNode,
437
+ Prism::RequiredKeywordParameterNode, Prism::OptionalKeywordParameterNode, Prism::KeywordRestParameterNode,
438
+ Prism::BlockParameterNode, Prism::BlockLocalVariableNode
439
+ names << node.name if node.name
440
+ when Prism::Node
441
+ node.rigor_each_child { |child| collect_parameter_names(child, names) }
442
+ end
443
+ end
444
+
445
+ # True when the block body `body` reads one of `names` where the scan types it. A read the scan never types is
446
+ # a bare argument of a statement that calls a method on `self` without a literal block (`puts i`): the scan
447
+ # reads such a call by its receiver and name alone, and types none of its arguments. Any other read counts,
448
+ # and so does every read under a body that is not a plain statement list (a `rescue` in a `do` block).
449
+ def reads_parameters?(body, names)
450
+ return false if body.nil?
451
+ return names_read?(body, names) unless body.is_a?(Prism::StatementsNode)
452
+
453
+ body.body.any? { |statement| statement_reads?(statement, names) }
454
+ end
455
+
456
+ def statement_reads?(statement, names)
457
+ return names_read?(statement, names) unless self_call_without_block?(statement)
458
+
459
+ arguments = statement.arguments&.arguments || []
460
+ arguments.any? { |argument| !argument.is_a?(Prism::LocalVariableReadNode) && names_read?(argument, names) } ||
461
+ names_read?(statement.block, names)
462
+ end
463
+
464
+ def self_call_without_block?(node)
465
+ node.is_a?(Prism::CallNode) && (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)) &&
466
+ !node.block.is_a?(Prism::BlockNode)
467
+ end
468
+
469
+ # True when `node` reads one of `names`: a read, or an operator write, which reads the local before it writes.
470
+ def names_read?(node, names)
471
+ case node
472
+ when Prism::LocalVariableReadNode, Prism::LocalVariableOperatorWriteNode, Prism::LocalVariableOrWriteNode,
473
+ Prism::LocalVariableAndWriteNode
474
+ return true if names.include?(node.name)
475
+ end
476
+ return false unless node.is_a?(Prism::Node)
477
+
478
+ found = false
479
+ node.rigor_each_child { |child| found ||= names_read?(child, names) }
480
+ found
481
+ end
482
+
483
+ private_class_method :collect_local_writes, :yielded_types, :collect_parameter_names, :reads_parameters?,
484
+ :statement_reads?, :self_call_without_block?, :names_read?
485
+ end
486
+ end
487
+ end
488
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../type"
4
+
5
+ module Rigor
6
+ module Inference
7
+ # The lookup half of {MutationWidening}'s Hash seam.
8
+ #
9
+ # `Hash#default=`, `Hash#default_proc=` and `Hash#compare_by_identity` mutate the receiver without storing,
10
+ # removing or rewriting a pair. What they change is what a READ of the pairs answers, and a closed `HashShape`
11
+ # makes a claim about exactly that: a key outside `pairs` is provably missing, so `[]` reads `Constant[nil]`.
12
+ # `counts = { a: 1 }; counts.default = 0` kept that claim, and `counts[:b] + 1` drew an error-level
13
+ # `call.undefined-method` on code Ruby runs and prints 1 for.
14
+ #
15
+ # They are kept out of {MutationWidening::HASH_MUTATORS} on purpose. That table answers "the pair set changed",
16
+ # and its other readers act on that answer: the widening there replaces the shape with a `Hash[K, V]` nominal,
17
+ # which costs every present key's value for a mutation that left every pair where it was, and the `non-empty-hash`
18
+ # witness reads it as a store or a removal. This module answers the narrower question instead, so the present keys
19
+ # keep their values. The effect classifier asks only whether the receiver was written, which each of these three
20
+ # does, so it reads the two tables as one ({Effects::MutationClassifier::HASH_MUTATORS}).
21
+ module HashLookupMutation
22
+ MUTATORS = %i[default= default_proc= compare_by_identity].to_set.freeze
23
+
24
+ module_function
25
+
26
+ # The binding `shape` holds after `method_name`, or `nil` when the call changes no read of it.
27
+ #
28
+ # `default=` / `default_proc=` open the shape: a key outside `pairs` now reads the default, which the shape
29
+ # cannot state, so it reads `untyped` — the answer an open shape gives an undeclared key. A present key still
30
+ # reads its own value. A default proc can also store (`proc { |h, k| h[k] = [] }`), so a read may add a pair,
31
+ # which is what the open policy says too: `size` / `keys` / `key?` stop folding.
32
+ #
33
+ # `compare_by_identity` changes the key a read has to pass, not the answer to a miss, so it leaves a closed
34
+ # shape closed. What it can falsify is a HIT: see {.identity_sensitive?}.
35
+ def widen_shape(shape, method_name)
36
+ case method_name
37
+ when :default=, :default_proc= then open_shape(shape)
38
+ when :compare_by_identity then identity_widening(shape) if identity_sensitive?(shape)
39
+ end
40
+ end
41
+
42
+ def open_shape(shape)
43
+ return nil if shape.open?
44
+
45
+ Type::HashShape.new(
46
+ shape.pairs,
47
+ required_keys: shape.required_keys,
48
+ optional_keys: shape.optional_keys,
49
+ read_only_keys: shape.read_only_keys,
50
+ extra_keys: :open
51
+ )
52
+ end
53
+
54
+ # True when some key of `shape` stops finding its pair under `compare_by_identity`. A read spells its key as a
55
+ # new literal, and after the switch it hits only when that literal is the very object the hash stored. For a
56
+ # Symbol, `true` / `false` / `nil` and an Integer in the fixnum range it always is. A String key is the frozen
57
+ # copy the literal stored, which a later `"k"` is not unless `# frozen_string_literal: true` interns both — so
58
+ # `s = { "k" => 1 }; s.compare_by_identity; s["k"]` is `nil` in one file and `1` in the next — and a bignum or
59
+ # a heap Float is a fresh object per evaluation.
60
+ #
61
+ # A shape whose every key is identity-stable reads exactly as before; any other is widened by
62
+ # {.identity_widening}.
63
+ def identity_sensitive?(shape)
64
+ shape.pairs.each_key.any? { |key| !identity_stable_key?(key) }
65
+ end
66
+
67
+ # An identity-sensitive shape widened as a storing mutator widens it, its values unpinned, with a `Dynamic[top]`
68
+ # arm on the value side. A read of a String key may find its pair or miss it and answer `nil`, or a default set
69
+ # before or after, and the nominal cannot say which: the arm keeps that read from folding (`s["k"] == 1`) or
70
+ # drawing `possible nil receiver` in the file where the key does hit. It also keeps the answer independent of
71
+ # order — a nominal ignores a later `default=`, so without the arm `s.compare_by_identity; s.default = "x"` read
72
+ # `s["zz"]` as `Integer`.
73
+ def identity_widening(shape)
74
+ key, value = MutationWidening.widen_hash_shape(shape, values: :widen).type_args
75
+ Type::Combinator.nominal_of("Hash", type_args: [key, Type::Combinator.union(value, Type::Combinator.untyped)])
76
+ end
77
+
78
+ # `Integer#bit_length` is at most 62 for exactly CRuby's 64-bit fixnum range, `-(2**62)..(2**62 - 1)`.
79
+ def identity_stable_key?(key)
80
+ case key
81
+ when Symbol, true, false, nil then true
82
+ when Integer then key.bit_length <= 62
83
+ else false
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -17,11 +17,24 @@ module Rigor
17
17
  # `||=` / `&&=` are conditional at runtime, but a widening may only LOSE precision, so answering on the branch that
18
18
  # does not store is safe.
19
19
  #
20
- # `Prism::IndexTargetNode` is deliberately absent: it is a multi-assign TARGET, and the `MultiWriteNode` that owns
21
- # it is where that write is observed.
20
+ # `Prism::IndexTargetNode` stores through `[]=` too, but it is absent from {NODE_CLASSES}: it is a TARGET, and the
21
+ # value it stores comes from the construct that owns it — a multi-assign slot (`h[:a], z = 1, 2`), a `for` index
22
+ # (`for h[:a] in xs`, alone, splatted or in a multi-target) or a rescue reference (`rescue => h[:e]`). Only that
23
+ # construct can type the value, so `StatementEvaluator#eval_multi_write`, `#bind_for_index` and
24
+ # `#bind_rescue_reference` observe the straight-line write, passing each target to {.widen} with the slot
25
+ # {MultiTargetBinder} decomposed for it, the element or the rescued exception. {CONTENT_WRITE_NODE_CLASSES} adds
26
+ # it for the nested-block write-back, and `ScopeIndexer`'s pre-pass, which needs no stored value, names the class
27
+ # next to {NODE_CLASSES}.
22
28
  module IndexWriteWidening
23
29
  NODE_CLASSES = [Prism::IndexOrWriteNode, Prism::IndexAndWriteNode, Prism::IndexOperatorWriteNode].freeze
24
30
 
31
+ # Every node that stores through `[]=` without being a `[]=` call: {NODE_CLASSES} plus the index TARGET a
32
+ # multi-assign, `rescue =>` or `for` writes through. `StatementEvaluator`'s captured-local write-back widens
33
+ # a receiver a nested block stores into through any of them, `ExpressionTyper`'s block-return threading
34
+ # gate predicts that widening, and `CapturedLocals.content_mutations` finds the per-element fold's in-place
35
+ # captures by it, so all three read this one list.
36
+ CONTENT_WRITE_NODE_CLASSES = [*NODE_CLASSES, Prism::IndexTargetNode].freeze
37
+
25
38
  # The method these forms store through — the name the mutator tables are keyed on.
26
39
  MUTATOR = :[]=
27
40
 
@@ -35,7 +48,7 @@ module Rigor
35
48
  # the widening joins the stored value into the carrier's content evidence exactly as a real `[]=` does
36
49
  # (issue #560). Empty means "no evidence" and widens without joining.
37
50
  #
38
- # @param node — one of {NODE_CLASSES}
51
+ # @param node — one of {NODE_CLASSES}, or a `Prism::IndexTargetNode`
39
52
  def widen(node:, current_scope:, arg_types: MutationWidening::NO_ARG_TYPES)
40
53
  MutationWidening.widen_receiver_aliases(node.receiver, MUTATOR, current_scope, arg_types: arg_types)
41
54
  end