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
@@ -41,10 +41,11 @@ module Rigor
41
41
  def scan(paths, buffer: nil)
42
42
  entries = []
43
43
  diagnostics = []
44
- paths.each { |path| scan_file(path, entries, diagnostics, buffer) }
44
+ census = Set.new
45
+ paths.each { |path| scan_file(path, entries, diagnostics, buffer, census) }
45
46
  diagnostics.concat(duplicate_declaration_diagnostics(entries))
46
47
  Result.new(
47
- registry: ProjectPatchedMethods.new(entries: entries),
48
+ registry: ProjectPatchedMethods.new(entries: entries, write_census: census.freeze),
48
49
  diagnostics: diagnostics
49
50
  )
50
51
  end
@@ -77,7 +78,7 @@ module Rigor
77
78
  end
78
79
  private_class_method :duplicate_declaration_diagnostics
79
80
 
80
- def scan_file(path, entries, diagnostics, buffer = nil)
81
+ def scan_file(path, entries, diagnostics, buffer = nil, census = Set.new)
81
82
  physical = buffer ? buffer.resolve(path) : path
82
83
  parse_result =
83
84
  if physical == path
@@ -91,6 +92,9 @@ module Rigor
91
92
  end
92
93
 
93
94
  walk_node(parse_result.value, [], false, path, entries)
95
+ # Issue #1367 — the file's `global.*` write facts: a patch file may alias a special or give a class the
96
+ # method a setter asks for, as any project file may.
97
+ census.merge(GlobalWriteCensus.scan(parse_result.value))
94
98
  rescue StandardError => e
95
99
  diagnostics << build_diagnostic(
96
100
  path: path, line: 1, column: 1,
@@ -24,10 +24,40 @@ module Rigor
24
24
  # object no binding can be attributed to and yields `[]`, which is what every receiver form
25
25
  # outside the single-read case already contributed. The walk is depth-capped so a pathological
26
26
  # nest cannot make receiver classification unbounded.
27
+ #
28
+ # The `it` parameter (Ruby 3.4) is a local like any other, but Prism reads it through
29
+ # `Prism::ItLocalVariableReadNode`, which carries no `name`; {.read_name} answers `:it` for it, the
30
+ # binding `BlockParameterBinder` installs. A consumer names a read through {.read_name}, never `#name`.
31
+ #
32
+ # A local or instance-variable write evaluates to the variable it writes, so `(buf ||= []) << x`
33
+ # mutates `buf` and yields a read of it ({.read_of}). Issue #1223 made that binding visible: the
34
+ # statement evaluator threads a write in a receiver, and without the alias `buf` kept the `[]`
35
+ # the `||=` stored.
27
36
  module ReceiverAlias
28
37
  # Deep enough for any hand-written selection; a nest beyond it degrades to "names no binding".
29
38
  WALK_DEPTH_CAP = 6
30
39
 
40
+ NON_ALIASED_READS = [Prism::ClassVariableReadNode, Prism::GlobalVariableReadNode].freeze
41
+ private_constant :NON_ALIASED_READS
42
+
43
+ LOCAL_WRITE_NODES = Set[
44
+ Prism::LocalVariableWriteNode, Prism::LocalVariableOrWriteNode,
45
+ Prism::LocalVariableAndWriteNode, Prism::LocalVariableOperatorWriteNode
46
+ ].freeze
47
+ INSTANCE_WRITE_NODES = Set[
48
+ Prism::InstanceVariableWriteNode, Prism::InstanceVariableOrWriteNode,
49
+ Prism::InstanceVariableAndWriteNode, Prism::InstanceVariableOperatorWriteNode
50
+ ].freeze
51
+ CLASS_VARIABLE_WRITE_NODES = Set[
52
+ Prism::ClassVariableWriteNode, Prism::ClassVariableOrWriteNode,
53
+ Prism::ClassVariableAndWriteNode, Prism::ClassVariableOperatorWriteNode
54
+ ].freeze
55
+ GLOBAL_WRITE_NODES = Set[
56
+ Prism::GlobalVariableWriteNode, Prism::GlobalVariableOrWriteNode,
57
+ Prism::GlobalVariableAndWriteNode, Prism::GlobalVariableOperatorWriteNode
58
+ ].freeze
59
+ private_constant :LOCAL_WRITE_NODES, :INSTANCE_WRITE_NODES, :CLASS_VARIABLE_WRITE_NODES, :GLOBAL_WRITE_NODES
60
+
31
61
  module_function
32
62
 
33
63
  # @param node — the receiver expression.
@@ -36,9 +66,11 @@ module Rigor
36
66
  # read the expression can evaluate to; empty when it can evaluate to none.
37
67
  def candidates(node, depth = 0)
38
68
  return [] if node.nil? || depth > WALK_DEPTH_CAP
69
+ # Tested before the `case`: a `when *SET` arm would copy the set into an Array on every call.
70
+ return [read_of(node)] if variable_write?(node)
39
71
 
40
72
  case node
41
- when Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode then [node]
73
+ when Prism::LocalVariableReadNode, Prism::ItLocalVariableReadNode, Prism::InstanceVariableReadNode then [node]
42
74
  when Prism::ParenthesesNode then candidates(node.body, depth + 1)
43
75
  when Prism::StatementsNode then candidates(node.body.last, depth + 1)
44
76
  when Prism::ElseNode then candidates(node.statements, depth + 1)
@@ -52,6 +84,63 @@ module Rigor
52
84
  def branches(first, second, depth)
53
85
  candidates(first, depth + 1) + candidates(second, depth + 1)
54
86
  end
87
+
88
+ # The variable reads an in-place mutation of `receiver` changes: {.candidates}' locals and instance
89
+ # variables, or the class variable or global the receiver reads or writes directly, parenthesised or not
90
+ # (`(@@c ||= []) << x` mutates `@@c`, as `(buf ||= []) << x` mutates `buf`). A class variable or global counts
91
+ # only as the receiver itself, not through a branch that selects it. This is the
92
+ # one answer the straight-line widening ({MutationWidening.widen_receiver_aliases}), the block-return
93
+ # threading gate that predicts it (`ExpressionTyper#prefix_statement_jump_free?`) and the per-element fold's
94
+ # content-mutation scan ({CapturedLocals.content_mutations}) all read, so "the scan says the body changed
95
+ # it" and "evaluating the body changes it" cannot disagree about which kinds of variable count.
96
+ def mutated_reads(receiver)
97
+ direct = receiver
98
+ direct = direct.body.body.last while direct.is_a?(Prism::ParenthesesNode) && direct.body.is_a?(Prism::StatementsNode)
99
+ return [direct] if NON_ALIASED_READS.include?(direct.class)
100
+ return [read_of(direct)] if non_aliased_write?(direct)
101
+
102
+ candidates(receiver)
103
+ end
104
+
105
+ # True when `node` is a class-variable or global write, which {.mutated_reads} answers only as the receiver
106
+ # itself, as it answers a class-variable or global read.
107
+ def non_aliased_write?(node)
108
+ CLASS_VARIABLE_WRITE_NODES.include?(node.class) || GLOBAL_WRITE_NODES.include?(node.class)
109
+ end
110
+
111
+ # The binding a read from {.candidates} or {.mutated_reads} names.
112
+ def read_name(read) = read.is_a?(Prism::ItLocalVariableReadNode) ? :it : read.name
113
+
114
+ # True when `read` is a local the innermost block around it owns: one Prism resolves at `depth == 0`, or an
115
+ # `it` read, which is always that block's own parameter and never a capture of an enclosing scope.
116
+ def block_local?(read)
117
+ read.is_a?(Prism::ItLocalVariableReadNode) || (read.is_a?(Prism::LocalVariableReadNode) && read.depth.zero?)
118
+ end
119
+
120
+ # True when `node` is a local or instance-variable write, whose value is the variable it leaves.
121
+ def variable_write?(node)
122
+ LOCAL_WRITE_NODES.include?(node.class) || INSTANCE_WRITE_NODES.include?(node.class)
123
+ end
124
+
125
+ # A read of the variable `write` writes, at the write's name, for the analysers that key a receiver on a
126
+ # read node — a local, instance-variable, class-variable or global write. `Prism` 1.x constructs every node
127
+ # from `source, node_id, location, flags` and its fields.
128
+ def read_of(write)
129
+ source = write.send(:source)
130
+ if LOCAL_WRITE_NODES.include?(write.class)
131
+ Prism::LocalVariableReadNode.new(source, write.node_id, write.name_loc, 0, write.name, write.depth)
132
+ else
133
+ named_read_class(write.class).new(source, write.node_id, write.name_loc, 0, write.name)
134
+ end
135
+ end
136
+
137
+ # The read node class for an instance-variable, class-variable or global write class.
138
+ def named_read_class(write_class)
139
+ if INSTANCE_WRITE_NODES.include?(write_class) then Prism::InstanceVariableReadNode
140
+ elsif CLASS_VARIABLE_WRITE_NODES.include?(write_class) then Prism::ClassVariableReadNode
141
+ else Prism::GlobalVariableReadNode
142
+ end
143
+ end
55
144
  end
56
145
  end
57
146
  end
@@ -0,0 +1,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "block_parameter_binder"
6
+
7
+ module Rigor
8
+ module Inference
9
+ # Can a block see the hash that an in-place transform is rewriting? This is the gate for the in-place forms
10
+ # of the HashShape per-pair fold (`ExpressionTyper#try_hash_shape_block_fold`).
11
+ #
12
+ # `transform_values!` / `transform_keys!` rewrite their receiver pair by pair while they iterate, so a block
13
+ # that reads the receiver sees the pairs it has already rewritten. The fold types every pair against the
14
+ # pre-call state held by the entry scope, so it gives a precise wrong answer: `h = { x: 1, y: 2 };
15
+ # h.transform_values! { |e| h[:x] + e }` folded to `{ x: 2, y: 3 }`, where Ruby answers `{ x: 2, y: 4 }`.
16
+ #
17
+ # The engine records no aliasing between bindings, and a method can return the receiver under any name.
18
+ # The gate therefore does not look for the receiver. It admits a block only when everything the block
19
+ # reads from outside itself is incapable of describing a hash's contents:
20
+ #
21
+ # - A local declared inside the block tree — a parameter or a block-local — is admitted. Prism's `depth`
22
+ # decides this, so a nested parameter that shadows an outer local is still a parameter.
23
+ # - A captured local, instance, class or global variable, or a constant, is admitted when its entry type
24
+ # carries no hash contents ({#carries_hash_contents?}). A compound write reads first, so it counts too.
25
+ # A constant compound write (`F ||= …`) names no read node to type, and a constant path under a
26
+ # non-constant parent (`m::T`) cannot be typed from the entry scope, so neither is admitted.
27
+ # - A method call is admitted only when its receiver is built from block parameters and literals alone
28
+ # (`e.to_s.upcase`, `[e].map { … }`, `%i[a b].include?(k)`). This excludes an implicit-self call and a
29
+ # call on `self`, a constant, a captured variable or a block-local, because the method can return the
30
+ # receiver however the value it is called on is typed (`Registry.store[:x]`, `tbl[:x]` for
31
+ # `def tbl = TABLE`).
32
+ # - `super`, `yield`, and a nested `def` / `class` / `module` / `class << x` run code the walk cannot
33
+ # see, so they are never admitted.
34
+ # - The operand of `defined?` is not walked: it asks whether a name is defined and reads no value.
35
+ #
36
+ # What a method called on a block parameter reaches is outside the gate. Such a method can read global
37
+ # state, but its inferred return type can hold stale constant contents without any in-place transform,
38
+ # so this is the engine-wide method-effect gap rather than this fold's. The gate only costs precision on
39
+ # the value of the bang call itself, because the receiver's own binding is widened after any bang call.
40
+ module ReceiverBlindBlock
41
+ LOCAL_READS = [
42
+ Prism::LocalVariableReadNode,
43
+ Prism::LocalVariableOperatorWriteNode,
44
+ Prism::LocalVariableOrWriteNode,
45
+ Prism::LocalVariableAndWriteNode
46
+ ].freeze
47
+
48
+ # Each captured non-local read, and the `Scope` accessor holding its entry binding.
49
+ VARIABLE_ACCESSORS = Ractor.make_shareable({
50
+ Prism::InstanceVariableReadNode => :ivar,
51
+ Prism::InstanceVariableOperatorWriteNode => :ivar,
52
+ Prism::InstanceVariableOrWriteNode => :ivar,
53
+ Prism::InstanceVariableAndWriteNode => :ivar,
54
+ Prism::ClassVariableReadNode => :cvar,
55
+ Prism::ClassVariableOperatorWriteNode => :cvar,
56
+ Prism::ClassVariableOrWriteNode => :cvar,
57
+ Prism::ClassVariableAndWriteNode => :cvar,
58
+ Prism::GlobalVariableReadNode => :global,
59
+ Prism::GlobalVariableOperatorWriteNode => :global,
60
+ Prism::GlobalVariableOrWriteNode => :global,
61
+ Prism::GlobalVariableAndWriteNode => :global
62
+ })
63
+
64
+ CONSTANT_READS = [Prism::ConstantReadNode, Prism::ConstantPathNode].freeze
65
+
66
+ # A constant compound write reads the constant by name, with no read node for the walk to type.
67
+ CONSTANT_COMPOUND_WRITES = [
68
+ Prism::ConstantOperatorWriteNode,
69
+ Prism::ConstantOrWriteNode,
70
+ Prism::ConstantAndWriteNode
71
+ ].freeze
72
+
73
+ # Every node that calls a method on a receiver.
74
+ RECEIVER_CALLS = [
75
+ Prism::CallNode,
76
+ Prism::CallAndWriteNode,
77
+ Prism::CallOperatorWriteNode,
78
+ Prism::CallOrWriteNode,
79
+ Prism::CallTargetNode,
80
+ Prism::IndexAndWriteNode,
81
+ Prism::IndexOperatorWriteNode,
82
+ Prism::IndexOrWriteNode,
83
+ Prism::IndexTargetNode
84
+ ].freeze
85
+
86
+ # Nodes that run code the walk cannot see, or open a scope the block's locals are invisible from.
87
+ OPAQUE = [
88
+ Prism::SuperNode,
89
+ Prism::ForwardingSuperNode,
90
+ Prism::YieldNode,
91
+ Prism::DefNode,
92
+ Prism::ClassNode,
93
+ Prism::ModuleNode,
94
+ Prism::SingletonClassNode
95
+ ].freeze
96
+
97
+ # Literal nodes, whose value is built from their children alone.
98
+ LITERALS = [
99
+ Prism::IntegerNode, Prism::FloatNode, Prism::RationalNode, Prism::ImaginaryNode,
100
+ Prism::StringNode, Prism::SymbolNode, Prism::RegularExpressionNode,
101
+ Prism::InterpolatedStringNode, Prism::InterpolatedSymbolNode, Prism::EmbeddedStatementsNode,
102
+ Prism::NilNode, Prism::TrueNode, Prism::FalseNode,
103
+ Prism::ArrayNode, Prism::HashNode, Prism::AssocNode, Prism::RangeNode
104
+ ].freeze
105
+
106
+ # A `Type` carrier is an immutable value built bottom-up, so it cannot contain itself. The cap is
107
+ # defensive only, and exceeding it answers "carries": declining the fold is the safe side.
108
+ TYPE_DEPTH_CAP = 8
109
+
110
+ module_function
111
+
112
+ # @param base_scope — the call-site scope the block closes over.
113
+ def blind?(block_node, base_scope)
114
+ scope_blind?(block_node, [], base_scope)
115
+ end
116
+
117
+ # `levels` holds the parameter names of each block from the transform's own block (index 0) down to
118
+ # the innermost block around the node, so a local read's Prism `depth` names the block that declares
119
+ # it, or none when the read is captured from outside.
120
+ def scope_blind?(block_node, levels, base_scope)
121
+ levels += [Set.new(BlockParameterBinder.new.bind(block_node).keys)]
122
+ [block_node.parameters, block_node.body].all? { |child| node_blind?(child, levels, base_scope) }
123
+ end
124
+
125
+ def node_blind?(node, levels, base_scope)
126
+ case node
127
+ when nil, Prism::DefinedNode then true
128
+ when Prism::BlockNode, Prism::LambdaNode then scope_blind?(node, levels, base_scope)
129
+ when *OPAQUE then false
130
+ else
131
+ admitted?(node, levels, base_scope) &&
132
+ node.compact_child_nodes.all? { |child| node_blind?(child, levels, base_scope) }
133
+ end
134
+ end
135
+
136
+ def admitted?(node, levels, base_scope)
137
+ case node
138
+ when *LOCAL_READS then in_block_tree?(node, levels) || !carries_hash_contents?(base_scope.local(node.name))
139
+ when *VARIABLE_ACCESSORS.keys
140
+ !carries_hash_contents?(base_scope.public_send(VARIABLE_ACCESSORS[node.class], node.name))
141
+ when *CONSTANT_READS then !constant_carries?(node, base_scope)
142
+ when *CONSTANT_COMPOUND_WRITES then false
143
+ when *RECEIVER_CALLS then built_from_parameters?(node.receiver, levels)
144
+ else true
145
+ end
146
+ end
147
+
148
+ def in_block_tree?(read, levels) = !declaring_level(read, levels).nil?
149
+
150
+ # The index into `levels` of the block that declares a local read, or `nil` for a captured read.
151
+ def declaring_level(read, levels)
152
+ level = levels.size - 1 - read.depth
153
+ level unless level.negative?
154
+ end
155
+
156
+ # Whether a call receiver's value comes from block parameters and literals alone. A call inside it is
157
+ # followed to its own receiver; its arguments are admitted by the walk on their own.
158
+ def built_from_parameters?(receiver, levels)
159
+ case receiver
160
+ when Prism::LocalVariableReadNode
161
+ level = declaring_level(receiver, levels)
162
+ !level.nil? && levels[level].include?(receiver.name)
163
+ when Prism::ItLocalVariableReadNode then true
164
+ when Prism::ParenthesesNode then built_from_parameters?(receiver.body, levels)
165
+ when Prism::StatementsNode then built_from_parameters?(receiver.body.last, levels)
166
+ when *RECEIVER_CALLS then built_from_parameters?(receiver.receiver, levels)
167
+ when *LITERALS then receiver.compact_child_nodes.all? { |child| built_from_parameters?(child, levels) }
168
+ else false
169
+ end
170
+ end
171
+
172
+ def constant_carries?(node, base_scope)
173
+ return true if node.is_a?(Prism::ConstantPathNode) && !constant_parent?(node.parent)
174
+
175
+ carries_hash_contents?(base_scope.type_of(node))
176
+ rescue StandardError
177
+ true
178
+ end
179
+
180
+ # `::T` has no parent; `A::T` and `A::B::T` have constant parents.
181
+ def constant_parent?(parent)
182
+ parent.nil? || CONSTANT_READS.include?(parent.class)
183
+ end
184
+
185
+ # Whether a value of `type` can tell a reader what some hash holds, key by key or as `Hash[K, V]`
186
+ # parameters. A read through such a value in an in-place transform's block can be stale by the next
187
+ # pair. `Dynamic[top]`, a bare `Hash` and a scalar cannot, and neither can a `Hash` whose parameters are
188
+ # untyped.
189
+ def carries_hash_contents?(type, depth = 0)
190
+ return false if type.nil?
191
+ return true if depth > TYPE_DEPTH_CAP || type.is_a?(Type::HashShape) || typed_hash_nominal?(type)
192
+
193
+ component_types(type).any? { |inner| carries_hash_contents?(inner, depth + 1) }
194
+ end
195
+
196
+ def typed_hash_nominal?(type)
197
+ type.is_a?(Type::Nominal) && type.class_name == "Hash" &&
198
+ type.type_args.any? { |arg| !arg.is_a?(Type::Dynamic) && !arg.is_a?(Type::Top) }
199
+ end
200
+
201
+ # The types a carrier holds. The list is empty for a carrier that cannot hold another value's type.
202
+ def component_types(type)
203
+ case type
204
+ when Type::Union, Type::Intersection then type.members
205
+ when Type::Tuple then type.elements
206
+ when Type::Nominal then type.type_args
207
+ when Type::Refined, Type::Difference then [type.base]
208
+ when Type::StructInstance, Type::DataInstance then type.members.values
209
+ when Type::Dynamic then [type.static_facet]
210
+ when Type::BoundMethod then [type.receiver_type]
211
+ when Type::Maybe then [type.value_type]
212
+ when Type::Result then [type.ok_type, type.err_type]
213
+ when Type::App then [*type.args, type.bound]
214
+ else []
215
+ end
216
+ end
217
+ end
218
+ end
219
+ end
@@ -20,9 +20,11 @@ module Rigor
20
20
  module RefinementMutation
21
21
  # Mutators that CANNOT leave a non-empty receiver empty. Whitelists rather than a table of
22
22
  # emptiers: a name missing from these retracts the witness, which is the conservative answer
23
- # every mutator got before. The listed ones either only add (`<< push …`) or only permute a
24
- # fixed number of slots (`sort! map! …`); `replace`, `compact!`, `flatten!`, `uniq!` and every
25
- # remover are deliberately absent, since each has an input that empties the receiver.
23
+ # every mutator got before. The listed ones either only add (`<< push …`) or only reorder or
24
+ # rewrite a fixed number of slots (`sort! map! …`); `replace`, `compact!`, `flatten!`, `uniq!`
25
+ # and every remover are deliberately absent, since each has an input that empties the receiver.
26
+ # Keeping the witness is all this table decides: what a rewrite leaves in those slots is
27
+ # {RewriteMutation}'s answer, so `map!` keeps `non-empty-array` but not its element.
26
28
  EMPTY_PRESERVING = {
27
29
  "Array" => %i[
28
30
  << push append prepend unshift concat insert
@@ -40,23 +42,25 @@ module Rigor
40
42
  # The block receives the refinement's BASE and answers the base with the mutator's added
41
43
  # content joined in, or a falsey value to decline (the mutator is not in that base's table).
42
44
  # A String base is answered here instead: it carries no content parameter, so the only
43
- # question it has is the witness one.
44
- def widen(difference, method_name)
45
+ # question it has is the witness one, which for `tr!` / `tr_s!` turns on `arg_types`.
46
+ def widen(difference, method_name, arg_types = StringMutation::NO_ARG_TYPES)
45
47
  return nil unless difference.removes_empty_witness?
46
48
 
47
49
  base = difference.base
48
- joined = base.class_name == "String" ? string_base(base, method_name) : yield(base)
50
+ joined = base.class_name == "String" ? string_base(base, method_name, arg_types) : yield(base)
49
51
  return nil unless joined
50
52
 
51
53
  widened = preserves_witness?(base.class_name, method_name) ? refine(joined, difference) : joined
52
54
  widened == difference ? nil : widened
53
55
  end
54
56
 
55
- # A `non-empty-string` survives every mutator that cannot empty the buffer, and those are
56
- # exactly the ones {StringMutation} does not list as emptying — so this arm declines them
57
- # and the refinement stands, rather than widening to `String` and dropping it.
58
- def string_base(base, method_name)
59
- StringMutation::EMPTYING_MUTATORS.include?(method_name) ? base : nil
57
+ # A `non-empty-string` survives every mutator call that cannot empty the buffer
58
+ # ({StringMutation.may_empty?}), so this arm declines those and the refinement stands, rather
59
+ # than widening to `String` and dropping it. A name that is not a String mutator declines too.
60
+ def string_base(base, method_name, arg_types)
61
+ return nil unless StringMutation::MUTATORS.include?(method_name)
62
+
63
+ StringMutation.may_empty?(method_name, arg_types) ? base : nil
60
64
  end
61
65
 
62
66
  def preserves_witness?(class_name, method_name)