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,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../type"
6
+ require_relative "block_call_timing"
7
+ require_relative "stored_block_call"
8
+
9
+ module Rigor
10
+ module Inference
11
+ # Issue #1360 — `$?`, the status of the last child process the thread waited for. Ruby keeps it per thread, not in
12
+ # the method frame's special-variable slot: a subprocess that a called method runs sets its caller's `$?`, a fiber
13
+ # shares its thread's, and a new thread starts without one. A subprocess that certainly ran ({.sets?}) sets it to
14
+ # a `Process::Status`: a backtick or `%x` command, `Kernel#system` (also when the command cannot run, with exit
15
+ # status 127), and `Process.wait`, `waitpid`, `wait2` or `waitpid2` without a flags argument, which blocks until a
16
+ # child exits.
17
+ #
18
+ # Ruby 4.0.5 sets it back to `nil` in four ways. One of those four waits given the `WNOHANG` flag sets it so when
19
+ # no child has exited, and `Process.waitall` when there is no child; either can run inside any method the thread
20
+ # calls, or in a signal handler, so a file that holds one ({.clears?}) binds `$?` nowhere. A backtick, `%x` or
21
+ # `system` sets it to nil before it runs the child, so an exception raised while it waits (`Timeout::Error`,
22
+ # `Interrupt`, `Thread#raise`) leaves it nil: a rescue clause or modifier fallback reads it unbound
23
+ # ({ErrorInfo.rescue_entry}), and so do the code past a rescue modifier and a body a `retry` re-enters. And
24
+ # `IO#close` on an `IO.popen` stream whose child was already reaped sets it to nil, which the analysis does not
25
+ # model. Otherwise a `$?` once set stays a `Process::Status`, so the binding survives later calls, blocks and loops
26
+ # until a join with a path that did not set it drops it. The root block of a new thread or ractor
27
+ # ({FreshFrameBlocks.entry}) and a closure's body ({FreshFrameBlocks.closure_entry}) start without it, and so does
28
+ # a method body.
29
+ module LastStatus
30
+ LAST_STATUS = :$?
31
+ # The `Process` functions that wait for a child and set `$?` to its status. Their optional flags argument may
32
+ # hold `WNOHANG`, which sets `$?` to nil instead when no child has exited.
33
+ WAITS = Set[:wait, :waitpid, :wait2, :waitpid2].freeze
34
+ CLEARERS = (WAITS | Set[:waitall]).freeze
35
+ # Compared as Strings: a literal with an invalid byte (`send("\xff")`) cannot become a Symbol.
36
+ CLEARER_NAMES = CLEARERS.to_set(&:to_s).freeze
37
+ SENDS = Set[:send, :__send__, :public_send].freeze
38
+ private_constant :LAST_STATUS, :WAITS, :CLEARERS, :CLEARER_NAMES, :SENDS
39
+
40
+ module_function
41
+
42
+ # `after`, the scope past `node`, with `$?` bound to `Process::Status` when `node` certainly ran a subprocess
43
+ # ({.certainly_sets?}) in a file that clears `$?` nowhere.
44
+ def after(node, after, scope)
45
+ return after if scope.discovery.clears_last_status || !certainly_sets?(node, scope)
46
+
47
+ after.with_global(LAST_STATUS, Type::Combinator.nominal_of("Process::Status"))
48
+ end
49
+
50
+ # True when running `node` certainly runs a subprocess that sets `$?` ({.sets?}): `node` itself, or a call whose
51
+ # receiver chain does, or whose positional arguments do unless a safe-navigation call may skip them
52
+ # (`%x(git rev-parse HEAD).strip`, `puts(%x(date))`). A subprocess in a branch, a block or an `&&` operand may not
53
+ # run, so it does not count.
54
+ def certainly_sets?(node, scope)
55
+ return true if sets?(node, scope)
56
+
57
+ case node
58
+ when Prism::CallNode
59
+ return true if certainly_sets?(node.receiver, scope)
60
+ return false if node.safe_navigation?
61
+
62
+ node.arguments&.arguments&.any? { |argument| certainly_sets?(argument, scope) } || false
63
+ when Prism::ParenthesesNode
64
+ body = node.body
65
+ body.is_a?(Prism::StatementsNode) && body.body.any? { |statement| certainly_sets?(statement, scope) }
66
+ else false
67
+ end
68
+ end
69
+
70
+ # True when `node` itself runs a subprocess and waits for it, which leaves `$?` a `Process::Status`: a backtick or
71
+ # `%x` command, `system` on implicit `self`, `self.` or `Kernel`, or `wait`, `waitpid`, `wait2` or `waitpid2`
72
+ # on the core `Process` module with at most one plain argument, the child's pid. A program that defines a
73
+ # method of that name anywhere ({BlockCallTiming.project_defines_anywhere?}) may run Ruby code in its place, so
74
+ # the name sets nothing there.
75
+ def sets?(node, scope)
76
+ case node
77
+ when Prism::XStringNode, Prism::InterpolatedXStringNode
78
+ !BlockCallTiming.project_defines_anywhere?(:`, scope)
79
+ when Prism::CallNode then subprocess_call?(node, scope)
80
+ else false
81
+ end
82
+ end
83
+
84
+ # True when `node` is a call that may set `$?` to nil: `wait`, `waitpid`, `wait2` or `waitpid2` with a flags
85
+ # argument, a splat, keywords or a forwarded `...`, on any receiver, `waitall`, or a `send`,
86
+ # `__send__` or `public_send` whose literal name is one of these. `Inference::ScopeIndexer` records whether a
87
+ # file holds one ({Scope::DiscoveryIndex#clears_last_status}).
88
+ def clears?(node)
89
+ return false unless node.is_a?(Prism::CallNode)
90
+
91
+ name = node.name
92
+ return !blocking_wait_arguments?(node) if WAITS.include?(name)
93
+ return true if name == :waitall
94
+ return false unless SENDS.include?(name)
95
+
96
+ sent = node.arguments&.arguments&.first
97
+ (sent.is_a?(Prism::SymbolNode) || sent.is_a?(Prism::StringNode)) && CLEARER_NAMES.include?(sent.unescaped)
98
+ end
99
+
100
+ # `after`, the scope an `ensure` clause leaves, with `$?` as `entry`, the scope the clause started from, binds it
101
+ # unless the clause bound it itself by running a subprocess: the clause read `$?` unbound, since it may run after
102
+ # a raise that came before the body's subprocess, but the code past it runs from the `begin` that finished.
103
+ def restore_unless_set(after, entry)
104
+ bound = entry.global(LAST_STATUS)
105
+ bound ? after.with_global(LAST_STATUS, bound) : after
106
+ end
107
+
108
+ def subprocess_call?(call_node, scope)
109
+ name = call_node.name
110
+ certain =
111
+ if name == :system
112
+ StoredBlockCall.kernel_spelled?(call_node.receiver)
113
+ elsif WAITS.include?(name)
114
+ blocking_wait_arguments?(call_node) && process_module?(call_node.receiver, scope)
115
+ else
116
+ false
117
+ end
118
+ certain && !BlockCallTiming.project_defines_anywhere?(name, scope)
119
+ end
120
+
121
+ # True when a wait call passes no flags: at most one positional argument, the pid, and no splat, keywords or
122
+ # forwarded `...` that may hold more.
123
+ def blocking_wait_arguments?(call_node)
124
+ arguments = call_node.arguments&.arguments
125
+ arguments.nil? || (arguments.size == 1 && plain_argument?(arguments.first))
126
+ end
127
+
128
+ def plain_argument?(argument)
129
+ !(argument.is_a?(Prism::SplatNode) || argument.is_a?(Prism::KeywordHashNode) ||
130
+ argument.is_a?(Prism::ForwardingArgumentsNode))
131
+ end
132
+
133
+ def process_module?(receiver, scope)
134
+ return false unless StoredBlockCall.root_constant_name(receiver) == :Process
135
+
136
+ type = scope.type_of(receiver)
137
+ type.is_a?(Type::Singleton) && type.class_name == "Process"
138
+ rescue StandardError
139
+ false
140
+ end
141
+ private_class_method :subprocess_call?, :blocking_wait_arguments?, :plain_argument?, :process_module?
142
+ end
143
+ end
144
+ end
@@ -15,10 +15,11 @@ module Rigor
15
15
  # - the underlying class `X` equals or inherits from the entry's `receiver_constraint`;
16
16
  # - the call's method name is in the entry's `method_names`.
17
17
  #
18
- # On a match the helper returns the **instance** type of the receiver class (`Nominal[X]`) —
19
- # the narrowed `self_type` for the block body, matching Sinatra's runtime semantics where
20
- # `Sinatra::Base#generate_method` turns the block into an instance method of the user's app
21
- # class.
18
+ # On a match the helper returns the narrowed `self_type` for the block body: the receiver class's
19
+ # instance type (`Nominal[X]`) for `:receiver_instance` entries — Sinatra's `generate_method`
20
+ # contract — or the entry's named `self_type` class when the DSL `instance_eval`s the block on a
21
+ # different object (Grape's `params` body on `Grape::Validations::ParamsScope`, `namespace` body on
22
+ # the `Grape::API::Instance` class object, verb bodies on `Grape::Endpoint`).
22
23
  #
23
24
  # Slice 1b ships the floor only (per ADR-16 § WD13): bare-identifier method lookups inside the
24
25
  # block resolve through the inference engine's normal `self_type`-driven path, so methods
@@ -37,40 +38,189 @@ module Rigor
37
38
  registry = environment&.plugin_registry
38
39
  return nil if registry.nil? || registry.empty?
39
40
 
40
- receiver_class_name = singleton_receiver_class_name(receiver_type)
41
- return nil if receiver_class_name.nil?
41
+ singleton_name = singleton_receiver_class_name(receiver_type)
42
+ nominal_name = nominal_receiver_class_name(receiver_type)
43
+ return nil if singleton_name.nil? && nominal_name.nil?
42
44
 
43
45
  # ADR-52 WD1 — the verb-keyed table compiled at registry build. Entries arrive in
44
46
  # (plugin registration, declaration) order; the method-name membership is guaranteed
45
47
  # by the table key.
46
48
  entries = registry.contribution_index.block_entries_for(call_node.name)
47
49
  entries.each do |entry|
48
- if receiver_class_inherits_from?(receiver_class_name, entry.receiver_constraint, environment)
49
- return instance_type_for(receiver_class_name, environment)
50
- end
50
+ narrowed = entry_self_type_for(entry, singleton_name, nominal_name, call_node,
51
+ scope, environment)
52
+ return narrowed if narrowed
51
53
  end
52
54
  nil
53
55
  end
54
56
 
55
- # Tier A's match contract is intentionally narrow: class-level DSL calls (receiver is
56
- # `Singleton[X]`) only. Instance-receiver calls and DSL forms whose block body binds a
57
- # different `self` (Concern's `included do`, `instance_eval { ... }`) are handled by later
58
- # slices (Concern walker, Tier D, etc.) — not Tier A.
57
+ # The narrowed `self_type` one entry contributes for this receiver, or nil on a miss. Nominal
58
+ # receivers exist only inside an already-narrowed `instance_eval` body — they can only re-enter a
59
+ # *named instance*-binding entry (`params`-family nesting on `Nominal[ParamsScope]`).
60
+ # `:receiver_instance` and `singleton(...)` entries keep their Singleton-only contract.
61
+ def entry_self_type_for(entry, singleton_name, nominal_name, call_node, scope, environment)
62
+ return nil if singleton_name.nil? && !entry.named_instance_binding?
63
+
64
+ method_name = call_node.name
65
+ receiver_name = singleton_name || nominal_name
66
+ matched = receiver_class_inherits_from?(receiver_name, entry.receiver_constraint, environment, scope)
67
+ # `extend M` lifts M's instance surface onto the class object — `class F; extend T::Sig;
68
+ # sig { ... }; end` calls `sig` on `Singleton[F]` even though F does not INHERIT from
69
+ # T::Sig. Singleton receivers therefore also match through the extends edge — but only when
70
+ # the module that actually ANSWERS `method_name` is the constrained one: a nearer `extend`
71
+ # whose module defines the same name owns the call and picks the block's self at runtime.
72
+ if !matched && singleton_name
73
+ matched = singleton_extends_reach?(receiver_name, entry.receiver_constraint, method_name,
74
+ call_node, scope, environment)
75
+ end
76
+ return nil unless matched
77
+
78
+ narrowed_self_type(entry, receiver_name, environment)
79
+ end
80
+
81
+ # The match contract stays narrow: `Singleton[X]` receivers (class-level DSL calls) for every
82
+ # entry, plus `Nominal[Y]` receivers only for entries whose declared `self_type` binds an
83
+ # instance — the `requires do ... requires do ... end` nesting shape inside a Grape `params`
84
+ # body, where `self` is already the ParamsScope instance the call evaluates on.
59
85
  def singleton_receiver_class_name(receiver_type)
60
86
  return nil unless receiver_type.is_a?(Type::Singleton)
61
87
 
62
88
  receiver_type.class_name
63
89
  end
64
90
 
65
- def receiver_class_inherits_from?(class_name, constraint, environment)
66
- return true if class_name == constraint
91
+ def nominal_receiver_class_name(receiver_type)
92
+ return nil unless receiver_type.is_a?(Type::Nominal)
93
+
94
+ receiver_type.class_name
95
+ end
96
+
97
+ # The narrowed `self_type` an entry contributes: `:receiver_instance` keeps the receiver class's
98
+ # instance type; a String `self_type` names the class the DSL `instance_eval`s the block on —
99
+ # `singleton(Foo)` for class-object evaluation (Grape's `namespace` body on
100
+ # `Grape::API::Instance`), `Foo` for instance evaluation (Grape's verb bodies on
101
+ # `Grape::Endpoint`, `params` bodies on `Grape::Validations::ParamsScope`).
102
+ def narrowed_self_type(entry, receiver_class_name, environment)
103
+ self_type = entry.self_type
104
+ return instance_type_for(receiver_class_name, environment) if self_type == :receiver_instance
105
+
106
+ if entry.singleton_binding?
107
+ return environment.singleton_for_name(entry.self_type_name) || Type::Singleton.new(entry.self_type_name)
108
+ end
67
109
 
68
- ordering = environment.class_ordering(class_name, constraint)
69
- %i[equal subclass].include?(ordering)
110
+ instance_type_for(entry.self_type_name, environment)
111
+ end
112
+
113
+ def receiver_class_inherits_from?(class_name, constraint, environment, scope = nil)
114
+ name = class_name.to_s
115
+ return true if name == constraint
116
+ return true if rbs_inherits?(name, constraint, environment)
117
+
118
+ # Source-side ancestry — `class API < Grape::API` lives on the scope's discovery tables, not in
119
+ # the environment's RBS/registry ordering (the same reason ADR-43's bridge walks them).
120
+ source_ancestors_reach?(name, constraint, environment, scope)
70
121
  rescue StandardError
71
122
  false
72
123
  end
73
124
 
125
+ # BFS over the source-side superclass table. The table stores names AS WRITTEN (`"::API::Base"`,
126
+ # bare `"Base"`), so each hop resolves through `ancestor_name_candidates` (rooted names, header
127
+ # nesting) rather than a raw lookup. Deliberately NOT `external_ancestor_name_candidates`: that
128
+ # walk records `ancestry_sources` edges through `record_class_dependency`, which would mislabel
129
+ # a DSL-call lookup as an ancestry dependency.
130
+ def source_ancestors_reach?(name, constraint, environment, scope)
131
+ supers = scope&.discovered_superclasses
132
+ queue = [name]
133
+ seen = {}
134
+ until queue.empty?
135
+ current = queue.shift
136
+ next if current.nil? || seen[current]
137
+
138
+ seen[current] = true
139
+ raw = supers&.[](current)
140
+ next if raw.nil?
141
+
142
+ scope.ancestor_name_candidates(current, raw).each do |candidate|
143
+ return true if candidate == constraint || rbs_inherits?(candidate, constraint, environment)
144
+
145
+ queue << candidate if supers.key?(candidate)
146
+ end
147
+ end
148
+ false
149
+ end
150
+
151
+ # The `extend`-edge twin of `source_ancestors_reach?`: true when the module that would answer
152
+ # `method_name` on `class_name`'s singleton — walked through `extend` edges, nearest first, then
153
+ # up the discovered superclass chain — resolves to `constraint`. Source `extend` targets are
154
+ # stored in singleton-ancestor search order and resolve through `ancestor_name_candidates`;
155
+ # RBS-side `extend` edges come from `Environment#singleton_extended_modules`, which is how
156
+ # `class Doc < T::ImmutableStruct` picks up `T::ImmutableStruct`'s own `extend T::Sig`.
157
+ def singleton_extends_reach?(class_name, constraint, method_name, call_node, scope, environment)
158
+ return false if scope.nil?
159
+
160
+ supers = scope.discovered_superclasses
161
+ extends = scope.discovered_extends
162
+ queue = [class_name.to_s]
163
+ seen = {}
164
+ until queue.empty?
165
+ current = queue.shift
166
+ next if current.nil? || seen[current]
167
+
168
+ seen[current] = true
169
+ # The class's own singleton defs sit ahead of EVERY `extend` in its singleton ancestry —
170
+ # `class F; extend T::Sig; def self.sig(&b); end; end` calls F's method, not T::Sig's —
171
+ # once the def has executed; a `def` written after this call has not run yet.
172
+ return current == constraint if scope.singleton_def_shadows_call?(current, method_name, call_node)
173
+
174
+ owner = extended_module_call_owner(current, extends, method_name, call_node, scope, environment)
175
+ return owner == constraint || rbs_inherits?(owner, constraint, environment) if owner
176
+
177
+ raw = supers[current]
178
+ scope.ancestor_name_candidates(current, raw).each { |c| queue << c } if raw
179
+ end
180
+ false
181
+ rescue StandardError
182
+ false
183
+ end
184
+
185
+ # One hop of `singleton_extends_reach?`: the module that answers `method_name` among `current`'s
186
+ # `extend` edges, or nil when none of them define it (the walk then continues to the
187
+ # superclass). An `extend` edge binds the first resolution candidate that exists at runtime —
188
+ # a project class or an RBS-known name — and an edge whose bound module lacks the method simply
189
+ # does not answer, so the search moves to the next edge, exactly like the singleton ancestry.
190
+ def extended_module_call_owner(current, extends, method_name, call_node, scope, environment)
191
+ (extends[current] || []).each do |mod_name|
192
+ owner = scope.ancestor_name_candidates(current, mod_name).find do |candidate|
193
+ scope.known_user_class?(candidate) ||
194
+ Rigor::Reflection.rbs_class_known?(candidate, environment: environment)
195
+ end
196
+ next if owner.nil? ||
197
+ !extended_module_defines?(owner, method_name, call_node, scope, environment)
198
+
199
+ return owner
200
+ end
201
+ (environment.singleton_extended_modules(current) || []).each do |mod_name|
202
+ return mod_name if extended_module_defines?(mod_name, method_name, call_node,
203
+ scope, environment)
204
+ end
205
+ nil
206
+ end
207
+
208
+ # `extend M` answers through M's INSTANCE surface — a source `def` inside the module or an RBS
209
+ # instance declaration (which already resolves through M's own `include`s, so `T::Generic`'s
210
+ # `include T::Helpers` answers `abstract!` on the extend edge). The source `def` counts only
211
+ # once it has executed: a module reopened AFTER the call site still lacks the method at call
212
+ # time, so the walk moves on exactly as the runtime ancestry would.
213
+ def extended_module_defines?(mod_name, method_name, call_node, scope, environment)
214
+ return true if scope.instance_def_shadows_call?(mod_name, method_name, call_node)
215
+
216
+ !Rigor::Reflection.instance_method_definition(mod_name, method_name,
217
+ environment: environment).nil?
218
+ end
219
+
220
+ def rbs_inherits?(class_name, constraint, environment)
221
+ %i[equal subclass].include?(environment.class_ordering(class_name, constraint))
222
+ end
223
+
74
224
  def instance_type_for(class_name, environment)
75
225
  environment.nominal_for_name(class_name) || Type::Nominal.new(class_name)
76
226
  end
@@ -0,0 +1,281 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ module Rigor
6
+ module Inference
7
+ module MatchRebinding
8
+ # Whether a call a statement runs rebinds the `$~` of the frame it is made in by the method it calls, on any
9
+ # receiver (issue #1365). Two readings, split by what the analyzer did before, so that no position newly keeps
10
+ # a narrowing Ruby rebinds and none newly forgets one it keeps on a guess:
11
+ #
12
+ # - a call the statement-level name table forgot on ({MATCH_CAPABLE_METHODS}, and for an implicit-self call
13
+ # {SelfCalls.named_match?}) still forgets unless its syntax proves it cannot match ({.forgets_by_name?}):
14
+ # `row[:name]`, `csv.split(",")` and `list.index(3)` keep the narrowing, `row[key]` does not. A flow type
15
+ # is not proof, because it can be stale while the value is a Regexp (#1380);
16
+ # - a call in a position that never forgot — an operand, an explicit-receiver builtin outside the table, an
17
+ # index write, a `send` or an `eval` — forgets only on evidence that it matches ({.rebinds?}).
18
+ module Calls
19
+ # These rebind `$~` whatever their argument: `sub` / `gsub` / `scan` with a String pattern still set it,
20
+ # `match` compiles one, and `!~` runs `=~`.
21
+ ALWAYS_MATCHING = Set[:=~, :!~, :match, :sub, :sub!, :gsub, :gsub!, :scan].freeze
22
+ # These rebind it only when an argument is a Regexp: `"a,b".split(",")`, `"abc"["z"]`, `list.index(3)`,
23
+ # `{name: 1}[:name]` and `%w[a].any?(String)` leave it alone.
24
+ LOOKUPS = Set[
25
+ :[], :slice, :slice!, :index, :rindex, :byteindex, :byterindex, :partition, :rpartition, :split,
26
+ :start_with?, :any?, :all?, :none?, :one?
27
+ ].freeze
28
+ # `grep` / `grep_v` rebind the caller's `$~` only in their block form.
29
+ BLOCK_FORM_LOOKUPS = Set[:grep, :grep_v].freeze
30
+ # `a === b` is `a`'s method, and `~re` matches `$_`, so these read the receiver: `String === s` and `~1`
31
+ # run no match.
32
+ RECEIVER_PATTERNS = Set[:===, :~].freeze
33
+ # These run a String of code in the frame that calls them: `eval` (`Kernel.eval`, `binding.eval`), and the
34
+ # other three in their String form, since their block form is a block.
35
+ EVALS = Set[:eval, :instance_eval, :class_eval, :module_eval].freeze
36
+ # These run the method they name.
37
+ SENDS = Set[:send, :__send__, :public_send].freeze
38
+ # A literal name a `send` compares by its bytes, which an invalid one (`send("\xff")`) cannot turn into a
39
+ # Symbol.
40
+ NAMES_BY_STRING = (
41
+ ALWAYS_MATCHING | LOOKUPS | BLOCK_FORM_LOOKUPS | RECEIVER_PATTERNS | EVALS | SENDS | Set[:[]=]
42
+ ).to_h { |name| [name.to_s, name] }.freeze
43
+ # An interpolated String of code is parsed with each interpolation standing for this identifier.
44
+ PLACEHOLDER = "__rigor_interpolation__"
45
+ # Code past these bounds is not read: parsing and scanning it would cost more than it tells, and a deeply
46
+ # nested literal (`[[[…]]]`) overflows the scan's recursion.
47
+ MAX_CODE_BYTES = 64 * 1024
48
+ MAX_CODE_NESTING = 256
49
+ OPENERS = "([{".bytes.to_set.freeze
50
+ CLOSERS = ")]}".bytes.to_set.freeze
51
+ # Operators a computed `send` name may end in that are not attribute writers.
52
+ OPERATOR_SUFFIXES = %w[== != <= >=].freeze
53
+ # `split` without a separator splits on `$;` (also spelled `$-F`), which may hold a Regexp.
54
+ FIELD_SEPARATORS = %i[$; $-F].freeze
55
+ EMPTY = [].freeze
56
+ private_constant :ALWAYS_MATCHING, :LOOKUPS, :BLOCK_FORM_LOOKUPS, :RECEIVER_PATTERNS, :EVALS, :SENDS,
57
+ :NAMES_BY_STRING, :PLACEHOLDER, :MAX_CODE_BYTES, :MAX_CODE_NESTING, :OPENERS,
58
+ :CLOSERS, :OPERATOR_SUFFIXES, :FIELD_SEPARATORS, :EMPTY
59
+
60
+ module_function
61
+
62
+ # True when the statement-level name table forgot on `call_node` before issue #1365: one of its names on any
63
+ # receiver, or, for an implicit-self call and a call in its arguments, a name {SelfCalls.named_match?} reads.
64
+ def base_named?(call_node, implicit:)
65
+ MATCH_CAPABLE_METHODS.include?(call_node.name) || (implicit && SelfCalls.named_match?(call_node))
66
+ end
67
+
68
+ # True when a call {.base_named?} names may still rebind `$~`: every such call does unless its syntax proves
69
+ # it cannot. `match?` never matches; `grep` / `grep_v` without a block do not rebind the caller's `$~`; a
70
+ # lookup, `start_with?`, `byteindex`, `byterindex` or pattern predicate whose arguments are all non-Regexp
71
+ # literals ({Operands.keep_literal?}), and `[]=` whose index is, run no match; nor does `===` on such a
72
+ # literal or on a constant naming a class or module. Any other argument counts as it did, whatever its flow
73
+ # type (#1380), and so do `=~`, `!~`, `match`, `sub`, `gsub`, `scan`, `~`, an eval and a `send`.
74
+ def forgets_by_name?(call_node, scope)
75
+ name = call_node.name
76
+ arguments = call_node.arguments&.arguments || EMPTY
77
+ return true if ALWAYS_MATCHING.include?(name) || name == :~
78
+ return false if name == :match?
79
+ return !keep_receiver?(call_node.receiver, scope) if name == :===
80
+ return !call_node.block.nil? && !keep_arguments?(arguments) if BLOCK_FORM_LOOKUPS.include?(name)
81
+
82
+ lookup_forgets?(name, arguments, scope)
83
+ end
84
+
85
+ # A lookup keeps the narrowing on literal arguments alone, and `split` on none (or a nil separator) only
86
+ # while this file never writes `$;`; `[]=` is read by its index; any other name forgets.
87
+ def lookup_forgets?(name, arguments, scope)
88
+ return !keep_arguments?(arguments[0...-1]) if name == :[]=
89
+ return true unless LOOKUPS.include?(name) && keep_arguments?(arguments)
90
+
91
+ default_separator?(name, arguments) && field_separator_written?(scope)
92
+ end
93
+ private_class_method :lookup_forgets?
94
+
95
+ # True when `call_node`, a statement's own call, may rebind the match globals of the frame it is made in once
96
+ # its operands have run. A call {.base_named?} names still forgets unless its syntax proves it cannot
97
+ # ({.forgets_by_name?}); any other forgets when it is known to match ({.rebinds?}). An implicit-self or
98
+ # `self.` call also forgets where it may reach the frame's slot although it does not match itself: where the
99
+ # frame hands its slot to code the analyzer does not trace ({Frame#self_call_fallback?}), where no body
100
+ # stamped a frame, or where its arguments hold something {MatchRebinding.operand_may_match?} counts. Any
101
+ # other call runs a method in a frame of its own (issue #1364) or a C method that does not match.
102
+ def statement_rebinds?(call_node, scope)
103
+ receiver = call_node.receiver
104
+ implicit = receiver.nil? || receiver.is_a?(Prism::SelfNode)
105
+ if base_named?(call_node, implicit: implicit)
106
+ return true if forgets_by_name?(call_node, scope)
107
+ elsif rebinds?(call_node, scope)
108
+ return true
109
+ end
110
+ return false unless implicit
111
+
112
+ frame = scope&.match_frame
113
+ frame.nil? || frame.self_call_fallback?(scope) ||
114
+ MatchRebinding.operand_may_match?(call_node.arguments, scope)
115
+ end
116
+
117
+ # True when `node`, a call, is known to rebind the `$~` of the frame it is made in by the method it calls:
118
+ # an {ALWAYS_MATCHING} name; a {LOOKUPS} name with an argument known to be a Regexp
119
+ # ({Operands.known_regexp_operand?}), and `grep` / `grep_v` on the same terms in their block form; `[]=` with
120
+ # such an index (`s[re] = v`); `===` or unary `~` on such a receiver; an eval whose code may match
121
+ # ({.eval_may_match?}); or a {SENDS} call that names one of these with the arguments it is sent, or whose
122
+ # computed name is sent a known Regexp (not an interpolated attribute writer, `"#{name}="`). `match?` never
123
+ # counts, and `Dynamic[top]` is not a known Regexp. `node` may also be an index `||=` / `&&=` / `op=` write,
124
+ # read by its index (`s[re] ||= v`). `scope` types the operands.
125
+ def rebinds?(node, scope)
126
+ arguments = node.arguments&.arguments || EMPTY
127
+ return known_arguments?(arguments, scope) unless node.is_a?(Prism::CallNode)
128
+
129
+ named?(node.name, node.receiver, arguments, !node.block.nil?, scope)
130
+ end
131
+
132
+ def named?(name, receiver, arguments, block, scope)
133
+ return true if ALWAYS_MATCHING.include?(name)
134
+ return !receiver.nil? && Operands.known_regexp_operand?(receiver, scope) if RECEIVER_PATTERNS.include?(name)
135
+ return field_separator_regexp?(scope) if default_separator?(name, arguments)
136
+ return false if arguments.empty?
137
+ return eval_may_match?(name, receiver, arguments.first, scope) if EVALS.include?(name)
138
+ return sent?(receiver, arguments, block, scope) if SENDS.include?(name)
139
+ return known_arguments?(arguments[0...-1], scope) if name == :[]=
140
+ return block && known_arguments?(arguments, scope) if BLOCK_FORM_LOOKUPS.include?(name)
141
+
142
+ LOOKUPS.include?(name) && known_arguments?(arguments, scope)
143
+ end
144
+ private_class_method :named?
145
+
146
+ def sent?(receiver, arguments, block, scope)
147
+ name_node, *rest = arguments
148
+ if name_node.is_a?(Prism::SymbolNode) || name_node.is_a?(Prism::StringNode)
149
+ name = NAMES_BY_STRING[name_node.unescaped]
150
+ return !name.nil? && named?(name, receiver, rest, block, scope)
151
+ end
152
+ return false if attribute_writer_name?(name_node)
153
+
154
+ known_arguments?(rest, scope)
155
+ end
156
+ private_class_method :sent?
157
+
158
+ # `"#{name}="` or `:"#{name}="` names a writer, which runs in a frame of its own.
159
+ def attribute_writer_name?(node)
160
+ return false unless node.is_a?(Prism::InterpolatedStringNode) || node.is_a?(Prism::InterpolatedSymbolNode)
161
+
162
+ tail = node.parts.last
163
+ text = tail.is_a?(Prism::StringNode) ? tail.unescaped : ""
164
+ text.end_with?("=") && OPERATOR_SUFFIXES.none? { |suffix| text.end_with?(suffix) }
165
+ end
166
+ private_class_method :attribute_writer_name?
167
+
168
+ # `split` with no argument, or a nil first one (`split(nil, 2)`), splits on `$;`.
169
+ def default_separator?(name, arguments)
170
+ name == :split && (arguments.empty? || arguments.first.is_a?(Prism::NilNode))
171
+ end
172
+ private_class_method :default_separator?
173
+
174
+ # True when this file writes `$;` anywhere ({Scope#program_globals}), so a `split` on it may split on a
175
+ # Regexp. `$;` set in another file, by `ruby -F`, or only through an operator write is not seen.
176
+ def field_separator_written?(scope)
177
+ globals = scope&.program_globals
178
+ !globals.nil? && FIELD_SEPARATORS.any? { |name| globals.key?(name) }
179
+ end
180
+ private_class_method :field_separator_written?
181
+
182
+ # True when this file writes a known Regexp to `$;`.
183
+ def field_separator_regexp?(scope)
184
+ globals = scope&.program_globals
185
+ !globals.nil? && FIELD_SEPARATORS.any? { |name| Operands.regexp_type?(globals[name], scope) }
186
+ end
187
+ private_class_method :field_separator_regexp?
188
+
189
+ # An eval of a literal or interpolated String counts when the analyzer reads its code and it may match
190
+ # ({.code_reading}). Code it cannot read — a variable, or a literal past the bounds — counts only for
191
+ # `binding.eval` (any receiver ending in a `binding` call) and `Kernel.eval`, whose whole purpose is to run
192
+ # code in this frame; `instance_eval`, `class_eval` and `module_eval` of such code are the metaprogramming
193
+ # idiom that defines methods, and counting them would forget on correct code.
194
+ def eval_may_match?(name, receiver, source, scope)
195
+ answer = code_literal?(source) ? code_reading(source, scope) : nil
196
+ return answer unless answer.nil?
197
+
198
+ name == :eval && frame_eval_receiver?(receiver)
199
+ end
200
+ private_class_method :eval_may_match?
201
+
202
+ def code_literal?(node)
203
+ node.is_a?(Prism::StringNode) || node.is_a?(Prism::InterpolatedStringNode)
204
+ end
205
+ private_class_method :code_literal?
206
+
207
+ def frame_eval_receiver?(receiver)
208
+ case receiver
209
+ when Prism::CallNode then receiver.name == :binding
210
+ when Prism::ConstantReadNode then receiver.name == :Kernel
211
+ when Prism::ConstantPathNode then receiver.parent.nil? && receiver.name == :Kernel
212
+ else false
213
+ end
214
+ end
215
+ private_class_method :frame_eval_receiver?
216
+
217
+ # Whether the String of code an eval runs may match, or nil when the analyzer cannot read it. A literal that
218
+ # parses is read on the block scan's terms ({MatchRebinding.program_may_match?}), and an interpolated one
219
+ # the same way with each interpolation replaced by an identifier. A literal that does not parse raises
220
+ # before it runs, so it does not match. An interpolated one that does not parse once so replaced, code
221
+ # longer than {MAX_CODE_BYTES} or nested deeper than {MAX_CODE_NESTING}, and code whose scan raises is not
222
+ # read. The answer is kept on the frame as {Frame#memo} keeps a scan, since every pass over the call asks
223
+ # again.
224
+ def code_reading(node, scope)
225
+ MatchRebinding.remember(node, scope, :code) { read_code(node, scope) }
226
+ end
227
+ private_class_method :code_reading
228
+
229
+ def read_code(node, scope)
230
+ interpolated = node.is_a?(Prism::InterpolatedStringNode)
231
+ text = if interpolated
232
+ node.parts.map { |part| part.is_a?(Prism::StringNode) ? part.unescaped : PLACEHOLDER }.join
233
+ else
234
+ node.unescaped
235
+ end
236
+ return nil if oversized?(text)
237
+
238
+ result = Prism.parse(text)
239
+ return (interpolated ? nil : false) unless result.errors.empty?
240
+
241
+ MatchRebinding.program_may_match?(result.value, scope)
242
+ rescue SystemStackError, StandardError
243
+ nil
244
+ end
245
+ private_class_method :read_code
246
+
247
+ # Longer than {MAX_CODE_BYTES}, or with brackets nested deeper than {MAX_CODE_NESTING}.
248
+ def oversized?(text)
249
+ return true if text.bytesize > MAX_CODE_BYTES
250
+
251
+ depth = 0
252
+ text.each_byte do |byte|
253
+ if OPENERS.include?(byte)
254
+ depth += 1
255
+ return true if depth > MAX_CODE_NESTING
256
+ elsif CLOSERS.include?(byte)
257
+ depth -= 1
258
+ end
259
+ end
260
+ false
261
+ end
262
+ private_class_method :oversized?
263
+
264
+ def keep_arguments?(arguments)
265
+ arguments.all? { |argument| Operands.keep_literal?(argument) }
266
+ end
267
+ private_class_method :keep_arguments?
268
+
269
+ def keep_receiver?(receiver, scope)
270
+ !receiver.nil? && (Operands.keep_literal?(receiver) || Operands.class_constant?(receiver, scope))
271
+ end
272
+ private_class_method :keep_receiver?
273
+
274
+ def known_arguments?(arguments, scope)
275
+ arguments.any? { |argument| Operands.known_regexp_operand?(argument, scope) }
276
+ end
277
+ private_class_method :known_arguments?
278
+ end
279
+ end
280
+ end
281
+ end