rigortype 0.3.9 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (407) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -4
  3. data/data/core_overlay/enumerable.rbs +51 -0
  4. data/data/core_overlay/enumerator.rbs +84 -0
  5. data/data/core_overlay/hash_rbs3.rbs +41 -0
  6. data/data/core_overlay/process.rbs +40 -0
  7. data/data/core_overlay/string_io.rbs +33 -0
  8. data/data/effects/core.yml +3 -3
  9. data/data/gem_overlay/activesupport/core_ext.rbs +127 -4
  10. data/docs/handbook/03-narrowing.md +95 -10
  11. data/docs/handbook/04-tuples-and-shapes.md +8 -6
  12. data/docs/handbook/07-rbs-and-extended.md +16 -11
  13. data/docs/handbook/08-understanding-errors.md +3 -2
  14. data/docs/handbook/10-sorbet.md +9 -10
  15. data/docs/handbook/11-sig-gen.md +454 -13
  16. data/docs/install.md +87 -15
  17. data/docs/manual/02-cli-reference.md +65 -4
  18. data/docs/manual/03-configuration.md +12 -0
  19. data/docs/manual/04-diagnostics.md +4 -0
  20. data/docs/manual/07-plugins.md +1 -1
  21. data/docs/manual/08-skills.md +4 -3
  22. data/docs/manual/10-mcp-server.md +3 -2
  23. data/docs/manual/12-caching.md +43 -4
  24. data/docs/manual/14-rails-quickstart.md +19 -11
  25. data/docs/manual/16-rbs-extended-annotations.md +40 -7
  26. data/docs/manual/19-effect-labels.md +10 -0
  27. data/docs/manual/plugins/README.md +7 -0
  28. data/docs/manual/plugins/rigor-actioncable.md +8 -1
  29. data/docs/manual/plugins/rigor-actionmailer.md +7 -0
  30. data/docs/manual/plugins/rigor-actionpack.md +243 -0
  31. data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
  32. data/docs/manual/plugins/rigor-activejob.md +7 -0
  33. data/docs/manual/plugins/rigor-activerecord.md +110 -4
  34. data/docs/manual/plugins/rigor-activestorage.md +8 -1
  35. data/docs/manual/plugins/rigor-activesupport-core-ext.md +11 -5
  36. data/docs/manual/plugins/rigor-grape.md +106 -0
  37. data/docs/manual/plugins/rigor-graphql.md +23 -2
  38. data/docs/manual/plugins/rigor-pundit.md +8 -1
  39. data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
  40. data/docs/manual/plugins/rigor-rails-routes.md +7 -0
  41. data/docs/manual/plugins/rigor-rbs-inline.md +212 -25
  42. data/docs/manual/plugins/rigor-sidekiq.md +8 -1
  43. data/docs/manual/plugins/rigor-sorbet.md +21 -5
  44. data/exe/rigor +19 -4
  45. data/lib/rigor/analysis/baseline.rb +1 -1
  46. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
  47. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
  48. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -3
  49. data/lib/rigor/analysis/check_rules/rule_ids.rb +20 -5
  50. data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
  51. data/lib/rigor/analysis/check_rules/source_arity.rb +427 -0
  52. data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
  53. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +36 -2
  54. data/lib/rigor/analysis/check_rules.rb +638 -136
  55. data/lib/rigor/analysis/dependency_recorder.rb +222 -17
  56. data/lib/rigor/analysis/fact_store.rb +9 -0
  57. data/lib/rigor/analysis/incremental.rb +26 -0
  58. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +266 -0
  59. data/lib/rigor/analysis/incremental_run_slot.rb +171 -0
  60. data/lib/rigor/analysis/incremental_session.rb +309 -53
  61. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
  62. data/lib/rigor/analysis/project_scan.rb +11 -1
  63. data/lib/rigor/analysis/reachability/plugin_roots.rb +0 -1
  64. data/lib/rigor/analysis/reachability/scan_cache.rb +3 -1
  65. data/lib/rigor/analysis/rule_catalog.rb +127 -0
  66. data/lib/rigor/analysis/run_cache_key.rb +46 -18
  67. data/lib/rigor/analysis/run_stats.rb +2 -2
  68. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +168 -12
  69. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -4
  70. data/lib/rigor/analysis/runner/pool_coordinator.rb +109 -17
  71. data/lib/rigor/analysis/runner/project_pre_passes.rb +44 -17
  72. data/lib/rigor/analysis/runner.rb +603 -54
  73. data/lib/rigor/analysis/shadow_harness.rb +27 -0
  74. data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
  75. data/lib/rigor/analysis/template_unit_collector.rb +303 -0
  76. data/lib/rigor/analysis/template_unit_paths.rb +93 -0
  77. data/lib/rigor/analysis/template_unit_positions.rb +293 -0
  78. data/lib/rigor/analysis/template_units.rb +409 -0
  79. data/lib/rigor/analysis/worker_session.rb +57 -13
  80. data/lib/rigor/bleeding_edge.rb +0 -2
  81. data/lib/rigor/builtins/hkt_builtins.rb +1 -0
  82. data/lib/rigor/builtins/imported_refinements.rb +4 -0
  83. data/lib/rigor/builtins/regex_refinement.rb +17 -9
  84. data/lib/rigor/builtins/static_return_refinements.rb +2 -0
  85. data/lib/rigor/cache/descriptor.rb +45 -1
  86. data/lib/rigor/cache/engine_source.rb +29 -1
  87. data/lib/rigor/cache/file_digest.rb +28 -0
  88. data/lib/rigor/cache/incremental_snapshot.rb +102 -8
  89. data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
  90. data/lib/rigor/cache/rbs_descriptor.rb +37 -7
  91. data/lib/rigor/cache/store.rb +63 -4
  92. data/lib/rigor/cache/value_digest.rb +325 -0
  93. data/lib/rigor/ci_detector.rb +1 -0
  94. data/lib/rigor/cli/check_command.rb +83 -13
  95. data/lib/rigor/cli/doc_links.rb +1 -1
  96. data/lib/rigor/cli/docs_command.rb +4 -4
  97. data/lib/rigor/cli/doctor_command.rb +1 -1
  98. data/lib/rigor/cli/effects_report.rb +2 -2
  99. data/lib/rigor/cli/plugin_command.rb +3 -3
  100. data/lib/rigor/cli/prism_colorizer.rb +0 -1
  101. data/lib/rigor/cli/sig_gen_command.rb +210 -29
  102. data/lib/rigor/cli/skill_command.rb +1 -1
  103. data/lib/rigor/cli/skill_describe.rb +0 -1
  104. data/lib/rigor/cli/type_of_command.rb +19 -5
  105. data/lib/rigor/cli/type_of_renderer.rb +20 -9
  106. data/lib/rigor/cli/type_of_template_probe.rb +189 -0
  107. data/lib/rigor/cli/unused_command.rb +3 -3
  108. data/lib/rigor/cli.rb +21 -3
  109. data/lib/rigor/config_audit.rb +3 -2
  110. data/lib/rigor/config_path.rb +29 -0
  111. data/lib/rigor/configuration/severity_profile.rb +24 -3
  112. data/lib/rigor/configuration.rb +71 -7
  113. data/lib/rigor/effects/ancestry_recorder.rb +191 -0
  114. data/lib/rigor/effects/attribution.rb +11 -2
  115. data/lib/rigor/effects/callee_rule.rb +368 -0
  116. data/lib/rigor/effects/catalog.rb +7 -4
  117. data/lib/rigor/effects/collector.rb +11 -5
  118. data/lib/rigor/effects/config_envelopes.rb +9 -2
  119. data/lib/rigor/effects/definition_context.rb +179 -0
  120. data/lib/rigor/effects/effect_table.rb +11 -3
  121. data/lib/rigor/effects/envelope_check.rb +1 -1
  122. data/lib/rigor/effects/envelope_index.rb +15 -0
  123. data/lib/rigor/effects/file_collection.rb +59 -5
  124. data/lib/rigor/effects/framework_units.rb +1 -1
  125. data/lib/rigor/effects/identity.rb +16 -0
  126. data/lib/rigor/effects/local_ownership.rb +38 -12
  127. data/lib/rigor/effects/method_key.rb +21 -0
  128. data/lib/rigor/effects/mutation_classifier.rb +23 -12
  129. data/lib/rigor/effects/plugin_facts.rb +49 -37
  130. data/lib/rigor/effects/propagator.rb +295 -16
  131. data/lib/rigor/effects/registry.rb +1 -1
  132. data/lib/rigor/effects/scanner.rb +121 -72
  133. data/lib/rigor/effects/signature_sources.rb +1 -1
  134. data/lib/rigor/effects/snapshot.rb +2 -1
  135. data/lib/rigor/effects/summary.rb +27 -4
  136. data/lib/rigor/effects/unit_scan.rb +385 -30
  137. data/lib/rigor/effects/visibility.rb +101 -0
  138. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
  139. data/lib/rigor/environment/lockfile_resolver.rb +27 -1
  140. data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
  141. data/lib/rigor/environment/member_consistency.rb +298 -0
  142. data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
  143. data/lib/rigor/environment/rbs_loader.rb +400 -134
  144. data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
  145. data/lib/rigor/environment.rb +104 -96
  146. data/lib/rigor/hashing/xxh3.rb +264 -0
  147. data/lib/rigor/inference/acceptance.rb +139 -9
  148. data/lib/rigor/inference/block_auto_splat.rb +216 -0
  149. data/lib/rigor/inference/block_call_timing.rb +338 -0
  150. data/lib/rigor/inference/block_parameter_binder.rb +88 -26
  151. data/lib/rigor/inference/block_repetition.rb +71 -0
  152. data/lib/rigor/inference/body_fixpoint.rb +2 -1
  153. data/lib/rigor/inference/budget_trace.rb +2 -1
  154. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  155. data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
  156. data/lib/rigor/inference/captured_locals.rb +393 -15
  157. data/lib/rigor/inference/closure_escape_analyzer.rb +157 -13
  158. data/lib/rigor/inference/content_join.rb +200 -27
  159. data/lib/rigor/inference/declaration_walk/context.rb +195 -0
  160. data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
  161. data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
  162. data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
  163. data/lib/rigor/inference/declaration_walk.rb +7 -0
  164. data/lib/rigor/inference/def_node_resolver.rb +3 -2
  165. data/lib/rigor/inference/def_return_typer.rb +11 -7
  166. data/lib/rigor/inference/define_method_block_self.rb +64 -0
  167. data/lib/rigor/inference/definer_resolution.rb +266 -0
  168. data/lib/rigor/inference/element_read_widening.rb +22 -10
  169. data/lib/rigor/inference/error_info.rb +196 -0
  170. data/lib/rigor/inference/expression_typer.rb +1665 -538
  171. data/lib/rigor/inference/external_ancestor_resolution.rb +264 -0
  172. data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
  173. data/lib/rigor/inference/global_write_census.rb +239 -0
  174. data/lib/rigor/inference/guard_rebinding.rb +488 -0
  175. data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
  176. data/lib/rigor/inference/index_write_widening.rb +16 -3
  177. data/lib/rigor/inference/indexed_narrowing.rb +61 -9
  178. data/lib/rigor/inference/jump_targets.rb +82 -0
  179. data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
  180. data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
  181. data/lib/rigor/inference/last_line.rb +340 -0
  182. data/lib/rigor/inference/last_status.rb +144 -0
  183. data/lib/rigor/inference/macro_block_self_type.rb +179 -17
  184. data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
  185. data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
  186. data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
  187. data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
  188. data/lib/rigor/inference/match_rebinding.rb +392 -0
  189. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  190. data/lib/rigor/inference/method_dispatcher/block_folding.rb +85 -24
  191. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
  192. data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
  193. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +4 -0
  194. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
  195. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +113 -73
  196. data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
  197. data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
  198. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +901 -38
  199. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
  200. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +128 -53
  201. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +1 -1
  202. data/lib/rigor/inference/method_dispatcher.rb +214 -13
  203. data/lib/rigor/inference/method_parameter_binder.rb +8 -3
  204. data/lib/rigor/inference/module_function_state.rb +213 -0
  205. data/lib/rigor/inference/multi_target_binder.rb +345 -52
  206. data/lib/rigor/inference/mutation_rejoin.rb +4 -1
  207. data/lib/rigor/inference/mutation_widening.rb +70 -48
  208. data/lib/rigor/inference/narrowing.rb +674 -124
  209. data/lib/rigor/inference/operand_effects.rb +167 -0
  210. data/lib/rigor/inference/operand_walk.rb +88 -0
  211. data/lib/rigor/inference/optimistic_origin.rb +152 -9
  212. data/lib/rigor/inference/parameter_inference_collector.rb +13 -11
  213. data/lib/rigor/inference/project_method_ownership.rb +136 -0
  214. data/lib/rigor/inference/project_patched_methods.rb +7 -2
  215. data/lib/rigor/inference/project_patched_scanner.rb +7 -3
  216. data/lib/rigor/inference/receiver_alias.rb +90 -1
  217. data/lib/rigor/inference/receiver_blind_block.rb +219 -0
  218. data/lib/rigor/inference/refinement_mutation.rb +15 -11
  219. data/lib/rigor/inference/repeated_or_writes.rb +463 -0
  220. data/lib/rigor/inference/return_barrier.rb +54 -0
  221. data/lib/rigor/inference/rewrite_mutation.rb +120 -0
  222. data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
  223. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
  224. data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
  225. data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
  226. data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
  227. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +130 -0
  228. data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
  229. data/lib/rigor/inference/scope_indexer.rb +5913 -701
  230. data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
  231. data/lib/rigor/inference/statement_evaluator.rb +3249 -497
  232. data/lib/rigor/inference/stored_block_call.rb +54 -0
  233. data/lib/rigor/inference/string_mutation.rb +44 -7
  234. data/lib/rigor/inference/unknown_store_widening.rb +200 -0
  235. data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
  236. data/lib/rigor/language_server/debouncer.rb +0 -1
  237. data/lib/rigor/language_server/diagnostic_publisher.rb +3 -2
  238. data/lib/rigor/language_server/hover_renderer.rb +3 -3
  239. data/lib/rigor/language_server/project_context.rb +5 -3
  240. data/lib/rigor/mcp/server.rb +2 -1
  241. data/lib/rigor/plugin/base.rb +196 -13
  242. data/lib/rigor/plugin/box_probe.rb +91 -0
  243. data/lib/rigor/plugin/bundled_catalog.rb +1 -1
  244. data/lib/rigor/plugin/effect_attribution.rb +58 -4
  245. data/lib/rigor/plugin/io_boundary.rb +82 -4
  246. data/lib/rigor/plugin/isolation.rb +1 -1
  247. data/lib/rigor/plugin/loader.rb +2 -1
  248. data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
  249. data/lib/rigor/plugin/manifest.rb +71 -10
  250. data/lib/rigor/plugin/registry.rb +35 -1
  251. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
  252. data/lib/rigor/plugin/template_unit.rb +196 -0
  253. data/lib/rigor/plugin/trust_policy.rb +3 -3
  254. data/lib/rigor/plugin.rb +1 -0
  255. data/lib/rigor/protection/discovery_seed.rb +20 -9
  256. data/lib/rigor/protection/kill_signature.rb +0 -1
  257. data/lib/rigor/protection/mutation_cache.rb +4 -5
  258. data/lib/rigor/rbs_extended.rb +27 -0
  259. data/lib/rigor/reflection/constant_ancestors.rb +146 -0
  260. data/lib/rigor/reflection/constant_path.rb +41 -13
  261. data/lib/rigor/reflection.rb +73 -107
  262. data/lib/rigor/scope/discovery_index.rb +351 -2
  263. data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
  264. data/lib/rigor/scope/resolution_chain.rb +884 -0
  265. data/lib/rigor/scope.rb +981 -198
  266. data/lib/rigor/sig_gen/alias_index.rb +289 -0
  267. data/lib/rigor/sig_gen/classification.rb +22 -5
  268. data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
  269. data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
  270. data/lib/rigor/sig_gen/generator.rb +580 -47
  271. data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
  272. data/lib/rigor/sig_gen/method_candidate.rb +47 -3
  273. data/lib/rigor/sig_gen/observation_collector.rb +1 -1
  274. data/lib/rigor/sig_gen/renderer.rb +124 -9
  275. data/lib/rigor/sig_gen/skip_reason_catalog.rb +44 -1
  276. data/lib/rigor/sig_gen/write_result.rb +19 -3
  277. data/lib/rigor/sig_gen/writer.rb +166 -35
  278. data/lib/rigor/sig_gen.rb +3 -0
  279. data/lib/rigor/signature_path_audit.rb +1 -1
  280. data/lib/rigor/source/node_walker.rb +0 -3
  281. data/lib/rigor/source/parameter_envelope.rb +72 -0
  282. data/lib/rigor/source.rb +1 -0
  283. data/lib/rigor/type/combinator.rb +112 -12
  284. data/lib/rigor/type/difference.rb +1 -0
  285. data/lib/rigor/type/hash_shape.rb +1 -1
  286. data/lib/rigor/type/refined.rb +1 -0
  287. data/lib/rigor/version.rb +1 -1
  288. data/lib/rigor.rb +1 -0
  289. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
  290. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +9 -5
  291. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -4
  292. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +0 -1
  293. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
  294. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
  295. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
  296. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
  297. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
  298. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
  299. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +239 -4
  300. data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
  301. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
  302. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
  303. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
  304. data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
  305. data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
  306. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -0
  307. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +9 -5
  308. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
  309. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
  310. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +380 -3
  311. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
  312. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +281 -35
  313. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
  314. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -14
  315. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
  316. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +171 -5
  317. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +2 -0
  318. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +1 -0
  319. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
  320. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +10 -0
  321. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
  322. data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
  323. data/plugins/rigor-grape/sig/grape.rbs +263 -0
  324. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +72 -2
  325. data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
  326. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -0
  327. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +11 -7
  328. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +35 -37
  329. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -1
  330. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -0
  331. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +1 -0
  332. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +9 -17
  333. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
  334. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +217 -36
  335. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +1 -0
  336. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +9 -5
  337. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +0 -1
  338. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +46 -1
  339. data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
  340. data/sig/rigor/analysis/baseline.rbs +69 -7
  341. data/sig/rigor/analysis/fact_store.rbs +1 -1
  342. data/sig/rigor/analysis/project_scan.rbs +74 -0
  343. data/sig/rigor/effects/config_envelopes.rbs +100 -0
  344. data/sig/rigor/effects/effect_table.rbs +60 -0
  345. data/sig/rigor/effects/envelope.rbs +97 -0
  346. data/sig/rigor/effects/envelope_index.rbs +33 -0
  347. data/sig/rigor/effects/file_collection.rbs +81 -0
  348. data/sig/rigor/effects/label.rbs +26 -0
  349. data/sig/rigor/effects/label_set.rbs +46 -0
  350. data/sig/rigor/effects/method_key.rbs +24 -0
  351. data/sig/rigor/effects/origin.rbs +50 -0
  352. data/sig/rigor/effects/plugin_facts.rbs +141 -0
  353. data/sig/rigor/effects/registry.rbs +68 -0
  354. data/sig/rigor/effects/summary.rbs +50 -0
  355. data/sig/rigor/effects/taint_cause.rbs +12 -0
  356. data/sig/rigor/environment.rbs +14 -10
  357. data/sig/rigor/inference/optimistic_origin.rbs +10 -0
  358. data/sig/rigor/inference.rbs +6 -4
  359. data/sig/rigor/plugin/additional_initializer.rbs +36 -0
  360. data/sig/rigor/plugin/base.rbs +30 -7
  361. data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
  362. data/sig/rigor/plugin/effect_attribution.rbs +66 -0
  363. data/sig/rigor/plugin/effect_edge.rbs +33 -0
  364. data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
  365. data/sig/rigor/plugin/io_boundary.rbs +2 -1
  366. data/sig/rigor/plugin/loader.rbs +3 -3
  367. data/sig/rigor/plugin/manifest.rbs +50 -11
  368. data/sig/rigor/plugin/protocol_contract.rbs +68 -0
  369. data/sig/rigor/plugin/registry.rbs +62 -1
  370. data/sig/rigor/plugin.rbs +1 -1
  371. data/sig/rigor/rbs_extended.rbs +1 -1
  372. data/sig/rigor/reflection.rbs +7 -6
  373. data/sig/rigor/scope.rbs +177 -20
  374. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -7
  375. data/sig/rigor/source.rbs +4 -4
  376. data/sig/rigor/testing.rbs +10 -4
  377. data/sig/rigor/type.rbs +6 -0
  378. data/sig/rigor.rbs +38 -20
  379. data/skills/rigor-ask/SKILL.md +8 -5
  380. data/skills/rigor-baseline-reduce/SKILL.md +4 -6
  381. data/skills/rigor-ci-setup/SKILL.md +17 -21
  382. data/skills/rigor-doctor/SKILL.md +24 -22
  383. data/skills/rigor-doctor/references/01-checks.md +97 -33
  384. data/skills/rigor-editor-setup/SKILL.md +6 -4
  385. data/skills/rigor-mcp-setup/SKILL.md +5 -4
  386. data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
  387. data/skills/rigor-next-steps/SKILL.md +4 -2
  388. data/skills/rigor-plugin-author/SKILL.md +19 -23
  389. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
  390. data/skills/rigor-plugin-author/references/02-walker-and-types.md +12 -25
  391. data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
  392. data/skills/rigor-plugin-review/SKILL.md +6 -4
  393. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
  394. data/skills/rigor-plugin-tune/SKILL.md +4 -2
  395. data/skills/rigor-project-init/SKILL.md +46 -18
  396. data/skills/rigor-project-init/references/01-detect.md +13 -9
  397. data/skills/rigor-project-init/references/02-configure.md +40 -10
  398. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
  399. data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
  400. data/skills/rigor-protection-uplift/SKILL.md +4 -6
  401. data/skills/rigor-rbs-setup/SKILL.md +4 -2
  402. data/skills/rigor-type-oracle/SKILL.md +4 -6
  403. data/skills/rigor-type-oracle/references/01-oracle-commands.md +12 -8
  404. data/skills/rigor-type-oracle/references/03-gap-protocol.md +0 -3
  405. data/skills/rigor-unused-adjudicate/SKILL.md +4 -2
  406. data/skills/rigor-upgrade/SKILL.md +13 -8
  407. metadata +132 -1
@@ -3,6 +3,7 @@
3
3
  require "prism"
4
4
 
5
5
  require_relative "../type"
6
+ require_relative "block_auto_splat"
6
7
  require_relative "multi_target_binder"
7
8
 
8
9
  module Rigor
@@ -22,12 +23,23 @@ module Rigor
22
23
  # MultiTargetNode parameters (`|(a, b), c|`) are bound by delegating each destructuring slot to
23
24
  # {Rigor::Inference::MultiTargetBinder}, so a Tuple-shaped expected element type projects element-wise
24
25
  # into the inner locals (Slice 6 phase C sub-phase 2). Numbered parameters (`_1`, `_2`, ...) are bound
25
- # from `Prism::NumberedParametersNode` using the same per-position `expected_param_types:` array, so
26
- # `[1, 2, 3].each { _1 + _2 }` sees `_1`/`_2` typed identically to their explicit `|x, y|` counterparts.
26
+ # from `Prism::NumberedParametersNode` as the explicit list of that many required positionals, the
27
+ # highest `_N` the body references being the arity (CRuby's own reading: `proc { _2 }.parameters` is
28
+ # `[[:opt, :_1], [:opt, :_2]]`, and `lambda { _2 }.arity` is 2). So `h.each { _1; _2 }` auto-splats like
29
+ # `|k, v|`, while a body that uses only `_1` reads as `|a|` and does not (issue #1108).
27
30
  #
28
31
  # The `it` implicit parameter (Ruby 3.4+) is bound from `Prism::ItParametersNode`. It is the
29
32
  # single-argument cousin of `_1`: the binder produces `{ it: expected_param_types[0] }` so the body's
30
- # `Prism::ItLocalVariableReadNode` lookup sees the same type as the explicit `|x|` form would.
33
+ # `Prism::ItLocalVariableReadNode` lookup sees the same type as the explicit `|x|` form would, and it
34
+ # never auto-splats.
35
+ #
36
+ # A block whose parameter list CRuby splats spreads the one yielded value across its positionals;
37
+ # {BlockAutoSplat} owns that rule and its carrier arms, and this binder only applies the table it
38
+ # answers. Every name the spread binds from an `Array[T]` slot — and every name a nested `|(g, h)|`
39
+ # destructure binds from one — is reported as optimistic, and {#bind_onto} records those through
40
+ # `Scope#with_optimistic_local` exactly as the statement-level `a, b = ints` does (issue #1093; the
41
+ # shared rule lives in {MultiTargetBinder}). A nested `|(g, h)|` destructures the one value the
42
+ # signature already placed, so it wraps like the statement form.
31
43
  #
32
44
  # Block-local declarations after `;` (e.g., `|x; y, z|`) are still skipped — they are explicitly
33
45
  # block-local, so the outer scope MUST NOT observe them and the binder leaves them unbound.
@@ -38,14 +50,31 @@ module Rigor
38
50
  # the binder cannot fill from this array (because the array is shorter than the parameter list, or
39
51
  # because the slot is a kind we do not pull from the array) default to `Dynamic[Top]`.
40
52
  def initialize(expected_param_types: [])
41
- @expected_param_types = expected_param_types
53
+ @declared_param_types = expected_param_types
54
+ reset_per_bind_state
55
+ end
56
+
57
+ # The names the last {#bind} bound optimistically nil-free (see the class comment). Read-only: it is a
58
+ # shared frozen empty when the bind marked nothing.
59
+ attr_reader :optimistic
60
+
61
+ # Binds the block's parameters into `scope`: {#bind}'s types through `Scope#with_local`, then the
62
+ # optimistic mark for every name in {#optimistic}. It applies them through
63
+ # {MultiTargetBinder.apply_bindings} directly rather than through a `Result`, which every block entry
64
+ # built only to apply at once ([#1504](https://github.com/rigortype/rigor/issues/1504)).
65
+ def bind_onto(block_node, scope)
66
+ MultiTargetBinder.apply_bindings(scope, bind(block_node, scope: scope), @optimistic)
42
67
  end
43
68
 
44
69
  # @return ordered map from parameter name to bound type. Anonymous
45
70
  # parameters are skipped; MultiTargetNode destructuring slots delegate to {MultiTargetBinder} and
46
71
  # contribute every named local in declaration order. Numbered-parameter forms (`_1`, `_2`, ...) bind
47
72
  # `:_1`, `:_2`, ... up to the maximum the block body refers to.
48
- def bind(block_node)
73
+ # @param scope — the scope the block is entered from, which answers a nested destructure's `to_ary`
74
+ # question ({MultiTargetBinder.bind_marked}).
75
+ def bind(block_node, scope: nil)
76
+ reset_per_bind_state
77
+ @scope = scope
49
78
  params_root = block_node.parameters
50
79
  return {} if params_root.nil?
51
80
 
@@ -63,14 +92,40 @@ module Rigor
63
92
 
64
93
  private
65
94
 
95
+ NO_POSITIONS = [].freeze
96
+ NO_NAMES = [].freeze
97
+ private_constant :NO_POSITIONS, :NO_NAMES
98
+
99
+ # {#apply_auto_splat} rewrites the positional table for the block it is binding, so every {#bind} starts
100
+ # from the declared types again; a binder reused across blocks must not see the previous block's splat.
101
+ # The two lists start as shared frozen empties, since most blocks mark nothing ({#mark_optimistic}).
102
+ def reset_per_bind_state
103
+ @expected_param_types = @declared_param_types
104
+ @splat_rest_type = nil
105
+ @optimistic_positions = NO_POSITIONS
106
+ @optimistic = NO_NAMES
107
+ @scope = nil
108
+ end
109
+
110
+ # Records `name` as bound optimistically, allocating {#optimistic} on the first mark of a {#bind}.
111
+ def mark_optimistic(name)
112
+ @optimistic = [] if @optimistic.equal?(NO_NAMES)
113
+ @optimistic << name
114
+ end
115
+
66
116
  # `|_1, _2|` numbered-parameter form. Prism exposes the implicit count through
67
117
  # `NumberedParametersNode#maximum` (the highest `_N` referenced in the body); we materialise bindings
68
- # for `:_1` through `:_maximum` so the block body's `LocalVariableReadNode` lookups see the same types
69
- # as the equivalent explicit `|x, y|` form would.
118
+ # for `:_1` through `:_maximum`, auto-splatting exactly as the explicit list of that many required
119
+ # positionals would, so the body's `LocalVariableReadNode` lookups see the same types and marks.
70
120
  def bind_numbered_parameters(numbered_node)
121
+ arity = numbered_node.maximum
122
+ apply_auto_splat { BlockAutoSplat::ParameterShape.of_arity(arity) }
123
+
71
124
  bindings = {}
72
- numbered_node.maximum.times do |i|
73
- bindings[:"_#{i + 1}"] = positional_type_at(i)
125
+ arity.times do |i|
126
+ name = :"_#{i + 1}"
127
+ bindings[name] = positional_type_at(i)
128
+ mark_optimistic(name) if @optimistic_positions.include?(i)
74
129
  end
75
130
  bindings
76
131
  end
@@ -85,7 +140,7 @@ module Rigor
85
140
  params_node = params_root.parameters
86
141
  return {} if params_node.nil?
87
142
 
88
- apply_auto_splat(params_node)
143
+ apply_auto_splat { BlockAutoSplat::ParameterShape.of(params_node) }
89
144
 
90
145
  bindings = {}
91
146
  bind_positionals(params_node, bindings, 0)
@@ -96,8 +151,8 @@ module Rigor
96
151
  bindings
97
152
  end
98
153
 
99
- # Ruby blocks (NOT lambdas) auto-splat a single yielded Tuple-shaped value when the block declares more
100
- # than one required positional parameter:
154
+ # Ruby blocks (NOT lambdas) auto-splat a single yielded array-ish value when the parameter list is
155
+ # one CRuby splats:
101
156
  #
102
157
  # { a: 1 }.each { |k, v| ... }
103
158
  #
@@ -106,21 +161,24 @@ module Rigor
106
161
  # the binder would assign `k = Tuple[K, V]` and `v = Dynamic[Top]`, and any call on
107
162
  # `k.<method-not-on-Tuple>` would false-fire.
108
163
  #
109
- # The rule fires only when (a) the receiver yields exactly one value (`expected_param_types.size ==
110
- # 1`), (b) the block declares more than one positional slot, and (c) that single expected element is a
111
- # Tuple. Multi-arg yields (e.g. `each_with_index`'s `(element, index)` pair) are NOT auto-splatted —
112
- # matching Ruby semantics where a multi-arg yield to a `|a, b, c|` block fills the extra slot with nil
113
- # rather than splatting any element.
114
- def apply_auto_splat(params_node)
164
+ # The rule fires only when the receiver yields exactly one value (`expected_param_types.size == 1`)
165
+ # and {BlockAutoSplat} answers a table for that value and `shape`. A multi-arg yield (e.g.
166
+ # `each_with_index`'s `(element, index)` pair) is NOT auto-splatted — matching Ruby semantics where a
167
+ # multi-arg yield to a `|a, b, c|` block fills the extra slot with nil rather than splatting any
168
+ # element. The block answers the parameter list's shape, built only past that test
169
+ # ([#1504](https://github.com/rigortype/rigor/issues/1504)).
170
+ def apply_auto_splat
115
171
  return unless @expected_param_types.size == 1
116
172
 
117
- pos_count = params_node.requireds.size + params_node.optionals.size + params_node.posts.size
118
- return unless pos_count > 1
173
+ shape = yield
174
+ return unless shape.splats?
119
175
 
120
- first = @expected_param_types[0]
121
- return unless first.is_a?(Type::Tuple)
176
+ table = BlockAutoSplat.for(shape, @expected_param_types[0])
177
+ return if table.nil?
122
178
 
123
- @expected_param_types = first.elements
179
+ @expected_param_types = table.types
180
+ @optimistic_positions = table.optimistic_positions
181
+ @splat_rest_type = table.rest_type
124
182
  end
125
183
 
126
184
  def bind_positionals(params_node, bindings, cursor)
@@ -160,7 +218,8 @@ module Rigor
160
218
  rest = params_node.rest
161
219
  return unless rest.respond_to?(:name) && rest&.name
162
220
 
163
- bindings[rest.name] = Type::Combinator.nominal_of("Array", type_args: [Type::Combinator.untyped])
221
+ bindings[rest.name] =
222
+ @splat_rest_type || Type::Combinator.nominal_of("Array", type_args: [Type::Combinator.untyped])
164
223
  end
165
224
 
166
225
  def bind_keywords(params_node, bindings)
@@ -199,9 +258,12 @@ module Rigor
199
258
  case param
200
259
  when Prism::RequiredParameterNode
201
260
  bindings[param.name] = positional_type_at(cursor)
261
+ mark_optimistic(param.name) if @optimistic_positions.include?(cursor)
202
262
  when Prism::MultiTargetNode
203
- nested = MultiTargetBinder.bind(param, positional_type_at(cursor))
204
- bindings.merge!(nested)
263
+ nested = MultiTargetBinder.bind_marked(param, positional_type_at(cursor),
264
+ optimistic: @optimistic_positions.include?(cursor), scope: @scope)
265
+ bindings.merge!(nested.types)
266
+ nested.optimistic.each { |name| mark_optimistic(name) }
205
267
  end
206
268
  end
207
269
 
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../type"
4
+ require_relative "block_call_timing"
5
+ require_relative "closure_escape_analyzer"
6
+
7
+ module Rigor
8
+ module Inference
9
+ # Whether a block-accepting call may run its block more than once, so a later run reads a captured binding an
10
+ # earlier run moved. Two passes ask it and share this one rule: the block-return pass
11
+ # (`ExpressionTyper#block_may_repeat?`), which lays the #587 (b) captured binding under such a block, and the
12
+ # statement pass (`StatementEvaluator#repeating_block_entry`, issue #1412), which lays the ADR-56 WD2.13
13
+ # content-mutation widening on its entry.
14
+ #
15
+ # The core iteration methods {ClosureEscapeAnalyzer} catalogues as non-escaping prove it — for a project class
16
+ # too, through its ancestry (`include Enumerable`); `tap` / `then` / `yield_self` are catalogued there too but
17
+ # run their block exactly once ({BlockCallTiming}).
18
+ #
19
+ # Issue #1234 — a receiver the analyzer cannot classify at all (`Dynamic`, a union, a project class whose
20
+ # ancestry it cannot follow) repeats when the method NAME is a catalogued iterator the project does not define
21
+ # on it ({ClosureEscapeAnalyzer.repeats_by_name?}): `items.all? { seen += 1; seen == 1 }` on an untyped `items`
22
+ # kept the first run's `seen == 1`, the predicate folded to `true`, and the condition on the result was
23
+ # reported as constant. This is a reading of `:unknown` for these two passes alone; escape analysis still
24
+ # treats it as unproven. Any other name, and a receiver classified `:escaping`, answers false, which keeps the
25
+ # entry scope: it is exact for a block run once (`m.synchronize { out = buf; buf = nil; out }` is `buf`'s value,
26
+ # and a cross-iteration binding would add the `nil` a second run never reads).
27
+ #
28
+ # A receiver that provably holds at most one element runs the block at most once however it iterates, so it
29
+ # answers false too: `done = false; [:only].each { break if done; done = true }` is `[:only]`, and the
30
+ # cross-iteration `done` would have typed it `Array[Symbol]?`.
31
+ module BlockRepetition
32
+ module_function
33
+
34
+ # `classification` is the receiver's {ClosureEscapeAnalyzer.classify} answer when the caller already holds it.
35
+ def may_repeat?(method_name:, receiver_type:, scope:, classification: nil)
36
+ return false if BlockCallTiming.candidate_name?(method_name)
37
+ return false if at_most_one_run?(method_name, receiver_type)
38
+
39
+ classification ||= ClosureEscapeAnalyzer.classify(
40
+ receiver_type: receiver_type, method_name: method_name, scope: scope
41
+ )
42
+ case classification
43
+ when :non_escaping then true
44
+ when :unknown
45
+ ClosureEscapeAnalyzer.repeats_by_name?(receiver_type: receiver_type, method_name: method_name, scope: scope)
46
+ else false
47
+ end
48
+ end
49
+
50
+ def at_most_one_run?(method_name, receiver_type)
51
+ case receiver_type
52
+ when Type::Tuple then receiver_type.elements.size <= 1
53
+ when Type::HashShape then receiver_type.closed? && receiver_type.pairs.size <= 1
54
+ when Type::Constant then constant_at_most_one_run?(method_name, receiver_type.value)
55
+ else false
56
+ end
57
+ end
58
+
59
+ # `1.times` and a one-element integer range iterate once.
60
+ def constant_at_most_one_run?(method_name, value)
61
+ case value
62
+ when Integer then method_name == :times && value <= 1
63
+ when Range
64
+ value.begin.is_a?(Integer) && value.end.is_a?(Integer) &&
65
+ value.end - value.begin + (value.exclude_end? ? 0 : 1) <= 1
66
+ else false
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -19,7 +19,8 @@ module Rigor
19
19
  # The mechanism is parameterized over an `evaluate_body` callable so slice B can reuse it: given the
20
20
  # current per-name bindings it returns the per-name exit bindings produced by one body evaluation from
21
21
  # those bindings (names the body leaves unwritten in a given pass simply do not appear in the returned
22
- # hash).
22
+ # hash). Slice C's content join reuses it too, with each "name" one evidence slot of a collection
23
+ # (`StatementEvaluator#join_content_to_fixpoint`) and a `bot` seed standing for "nothing stored yet".
23
24
  module BodyFixpoint
24
25
  # One body evaluation per iteration; ADR-55's shape (cap 3).
25
26
  CAP = 3
@@ -49,7 +49,8 @@ module Rigor
49
49
  RECURSION_FIXPOINT_CAP = :recursion_fixpoint_cap
50
50
  # `BodyFixpoint#converge` (ADR-56 slice A — non-escaping block captured-local write-back) ran its
51
51
  # 3-evaluation cap without the written local's join converging and collapsed that local to
52
- # `Dynamic[top]` (the escaping-block floor). Shared by slice B's loop-body fixpoint.
52
+ # `Dynamic[top]` (the escaping-block floor). Shared by slice B's loop-body fixpoint and by slice C's
53
+ # content-evidence fixpoint, where the collapsed name is one element / key / value slot of a collection join.
53
54
  BLOCK_WRITEBACK_CAP = :block_writeback_cap
54
55
 
55
56
  # ADR-57 return-memo profile counters (not cutoffs — see the module doc). All bumped by
@@ -20,6 +20,7 @@ module Rigor
20
20
  class MethodCatalog
21
21
  FOLDABLE_PURITIES = Set["leaf", "trivial", "leaf_when_numeric"].freeze
22
22
  EMPTY_CATALOG = { "classes" => {} }.freeze
23
+ Ractor.make_shareable(EMPTY_CATALOG)
23
24
 
24
25
  # Selectors that are classified `:leaf` by the C-body analysis (they read no global mutable state
25
26
  # in the C sense) but whose result is NOT reproducible across Ruby processes, so they must never
@@ -37,7 +38,7 @@ module Rigor
37
38
 
38
39
  # Shared root for the offline-generated catalogues. Resolving it here keeps the repo-relative
39
40
  # `../../../../` hop in one place instead of copying it into every per-topic loader.
40
- DATA_ROOT = File.expand_path("../../../../data/builtins/ruby_core", __dir__)
41
+ DATA_ROOT = File.expand_path("../../../../data/builtins/ruby_core", __dir__).freeze
41
42
  private_constant :DATA_ROOT
42
43
 
43
44
  # Build a catalog for a named topic, resolving its YAML path under {DATA_ROOT}. Equivalent to
@@ -18,7 +18,7 @@ module Rigor
18
18
  :replace, :initialize, :initialize_copy, :clear, :<<, :concat, :insert,
19
19
  :prepend, :force_encoding, :encode, :scrub, :unicode_normalize, :"[]=",
20
20
  :upto, :each_byte, :each_char, :each_codepoint,
21
- :each_grapheme_cluster, :each_line, :bytesplice,
21
+ :each_grapheme_cluster, :each_line, :bytesplice, :append_as_bytes,
22
22
  # `crypt` is not a mutator but is blocked from folding for the same "do not bake a non-pure
23
23
  # result into a Constant" reason: `rb_str_crypt` delegates to the platform `crypt(3)`, whose
24
24
  # output (algorithm and digest) varies by libc / OS, so `"x".crypt("ab")` is not deterministic