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
@@ -31,7 +31,7 @@
31
31
  #
32
32
  # Every method carries an effect annotation, and the split down
33
33
  # the middle of this file is the whole point of the Rails effect
34
- # layer: a **builder** is `%a{pure}` because it issues nothing —
34
+ # layer: a **query builder** is `%a{pure}` because it issues nothing —
35
35
  # `where` composes an Arel tree and returns a relation — while a
36
36
  # **materializer** is `%a{rigor:v1:effect io.db.read}` because it
37
37
  # is the call that finally runs the query. A presenter that builds
@@ -46,6 +46,136 @@
46
46
  # `Enumerable` surface, which materialise by delegating to `each`
47
47
  # — are covered by the plugin's `effect_attributions:` instead,
48
48
  # because declaring them here would change how they TYPE.
49
+ #
50
+ # ## Every bound here also bounds the association's relations
51
+ #
52
+ # A `has_many` reader returns an
53
+ # `ActiveRecord::Associations::CollectionProxy`, and a query
54
+ # builder called on the proxy (`user.posts.where(…)`) returns an
55
+ # `ActiveRecord::AssociationRelation`. Both are Relation subclasses
56
+ # that still hold the association, and the plugin types both as
57
+ # `Relation[Model]`. A call on either imports this file's bound, so
58
+ # each annotation has to hold for all three classes. The facts below
59
+ # are checked against activerecord 8.1.3.1.
60
+ #
61
+ # Where one of them adds records to the association's in-memory
62
+ # target or discards them from it, the bound carries `mutate.self`:
63
+ #
64
+ # - `build` / `new`, `create` / `create!` and the `find_or_*`,
65
+ # `create_or_find_by` and `first_or_*` builders reach
66
+ # `@association.build` / `create`, which call `add_to_target`. On
67
+ # the proxy they are overrides. On an AssociationRelation they are
68
+ # its private `_new` / `_create`.
69
+ # - `reset`, `reload`, `delete_all` and `destroy_all` on the proxy
70
+ # reset the association, which drops every record built there and
71
+ # not yet saved. So do `update_all` and `touch_all`: the proxy does
72
+ # not override them, and Relation's version ends with
73
+ # `.tap { reset }`, which reaches the proxy's `reset`.
74
+ # - `insert`, `insert!` and `upsert`, and their `_all` forms, on the
75
+ # proxy reset the target only when it holds no unsaved record.
76
+ # Rails 8.1 deprecates the other branch and says those records will
77
+ # be lost in 8.2. The bound is written for 8.2, and it over-states
78
+ # 8.1.
79
+ #
80
+ # So `user.posts.build` changes an object the caller can still reach
81
+ # through `user`: `user.posts.size` counts the new record, and
82
+ # `user.save` saves it. On a plain Relation, `new` and `build` change
83
+ # nothing, so `mutate.self` over-states them there. The type cannot
84
+ # tell the receivers apart, and an upper bound may be too wide but
85
+ # never too narrow.
86
+ #
87
+ # Loading is not counted as a change. `to_a` and `load` fill the
88
+ # target with what the database holds, which is the `io.db.read` they
89
+ # already carry.
90
+ #
91
+ # `mutate.self` is written from the callee's side, as any envelope
92
+ # is: the receiver changes itself. A caller imports the label
93
+ # unchanged, so in the caller's row it names the proxy it called,
94
+ # not the caller's own `self`.
95
+ #
96
+ # `insert`, `insert!` and `upsert` are declared here, beside their
97
+ # `_all` forms, because a plain Relation defines them too. The
98
+ # declaration changes no result type: an undeclared method on this
99
+ # open receiver already types as `untyped`. It adds the checks any
100
+ # declared method gets, `call.wrong-arity` and
101
+ # `call.possible-nil-receiver`, and its parameter list admits every
102
+ # call Rails accepts.
103
+ #
104
+ # The writers a plain Relation does not define (`<<`, `push`,
105
+ # `append`, `concat`, `replace`, `clear`) are rows in the plugin's
106
+ # `effect_attributions:` rather than declarations here. The same rows
107
+ # cover `delete` / `destroy`, which a Relation does define, as
108
+ # `delete(id_or_array)` and `destroy(id)`, but the proxy overrides as
109
+ # `delete(*records)` and `destroy(*records)`. A declaration here
110
+ # would need a parameter list and a return wide enough for both, and
111
+ # it would give the two methods a second effect channel next to
112
+ # their rows.
113
+ #
114
+ # ## Every parameter list here also admits the overrides
115
+ #
116
+ # The rule above holds for parameter lists as well as effect bounds.
117
+ # A call on the proxy or on an AssociationRelation is checked against
118
+ # the parameter list declared here, so a declaration must accept
119
+ # every argument an override accepts, or valid Rails draws
120
+ # `call.wrong-arity`. Of the overrides this file declares, only the
121
+ # proxy's `delete_all(dependent = nil)` accepts more than the
122
+ # Relation's `delete_all`. It takes `:nullify` or `:delete_all`, so
123
+ # `delete_all` declares an optional argument. The price is that
124
+ # `delete_all(:nullify)` on a plain Relation or on an
125
+ # AssociationRelation (`user.posts.where(…)`), neither of which
126
+ # overrides it, raises `ArgumentError` and goes unreported.
127
+ #
128
+ # Of the rest, the `(...)` methods of the `insert` and `insert_all`
129
+ # families and the proxy's `delegate … to: :scope` block (the query
130
+ # builders and `scoping`) forward their arguments unchanged to the
131
+ # Relation's own methods, and the proxy's other overrides (`find`,
132
+ # `last`, `take`, `build`, `create`, `calculate`, `pluck`, …) take no
133
+ # more than the declarations here. `delete` and `destroy`, whose
134
+ # overrides take more arguments than the Relation's, stay undeclared
135
+ # for the reasons above.
136
+ #
137
+ # ## A writer that also queries carries the read too
138
+ #
139
+ # `io.db.write` does not include `io.db.read`: they are sibling
140
+ # leaves. So a writer that queries before its write, or after a failed
141
+ # one, names both. In activerecord 8.1.3.1:
142
+ #
143
+ # - `find_or_create_by(!)` is `find_by || create_or_find_by`, and
144
+ # `first_or_create(!)` is `first || create`.
145
+ # - `create_or_find_by(!)` rescues `RecordNotUnique` with `find_by!`.
146
+ # - `destroy_all` is `records.each(&:destroy)`, which loads first, and
147
+ # `destroy_by` is `where(…).destroy_all`. `update(!)` walks `each`,
148
+ # or passes an id to the model's `update`, which calls `find`.
149
+ # - `update_all` and `delete_all`, and so `touch_all` and `delete_by`,
150
+ # first `SELECT` the distinct primary keys when the relation
151
+ # eager-loads, has a limit or offset and no `group`, and eager-loads
152
+ # or joins a collection association (`apply_join_dependency`). The
153
+ # proxy's `delete_all` on a `has_many :through` loads the target
154
+ # before it deletes.
155
+ # - `create(!)` on the proxy of a `has_many :through` a `has_one`
156
+ # builds the join record through the `has_one`, which loads the
157
+ # record it replaces.
158
+ #
159
+ # The `insert` and `insert_all` families stay `io.db.write` alone.
160
+ # Each compiles one `INSERT` statement and queries nothing else: the
161
+ # proxy's override reads only the target it already holds in memory.
162
+ #
163
+ # Two kinds of query are not counted. Schema reflection is one: the
164
+ # column and index lookups the schema cache serves would otherwise
165
+ # make `where` a read. The model's callbacks and validators are the
166
+ # other, including those an association option such as `dependent:`
167
+ # or `touch:` registers. They are not part of any bound here. The
168
+ # plugin's callback edge carries only symbol-argument callback macros
169
+ # and a uniqueness validator, and only on the model's own `save` /
170
+ # `destroy`. Nothing carries the reads a required `belongs_to`,
171
+ # `touch:` or `dependent:` registers.
172
+ #
173
+ # One case is not covered. On that same `has_many :through` a
174
+ # `has_one`, `new` / `build` also build the join record when the
175
+ # source reflection has an inverse, and so do `find_or_initialize_by`
176
+ # and `first_or_initialize`, which reach `new`. The `has_one` then
177
+ # replaces its record: it loads the old one, and even without a
178
+ # save it can delete, destroy or save it.
49
179
 
50
180
  module ActiveRecord
51
181
  class Relation[Elem]
@@ -101,6 +231,10 @@ module ActiveRecord
101
231
  def left_outer_joins: (*untyped) -> self
102
232
  %a{pure}
103
233
  def references: (*untyped) -> self
234
+ # With a block, `select` is Enumerable's and returns an Array. It
235
+ # stays `self`: on this open receiver a method the file does not
236
+ # declare is not reported, while on `Array[Elem]` any ActiveSupport
237
+ # method the bundled overlay leaves out would be.
104
238
  %a{pure}
105
239
  def select: (*untyped) -> self
106
240
  %a{pure}
@@ -145,9 +279,33 @@ module ActiveRecord
145
279
  def with: (*untyped) -> self
146
280
  def scoping: () { () -> untyped } -> untyped
147
281
 
148
- # --- finders (return an element) ---
149
- %a{rigor:v1:effect io.db.read}
150
- def find: (*untyped) -> Elem
282
+ # --- finders (return an element, or an Array of them; `find`
283
+ # with a block, the element or nil) ---
284
+ # Two or more ids return an Array, unless dropping `nil`s and
285
+ # duplicates leaves one. One argument returns an Array when it is an
286
+ # Array of ids, but one record when it is a composite key's tuple
287
+ # (`[shop_id, id]`), and nothing here can tell those apart. An
288
+ # overload keyed on an `Array` argument would be selected for every
289
+ # untyped id too (`find(params[:id])`). Its return would be joined
290
+ # with this one's into `Dynamic[Array[Elem] | Elem]`, on which a
291
+ # following `update` or `save` no longer resolves on the model and
292
+ # loses its `io.db.write`. So a single argument stays `Elem`. A
293
+ # splat and a `**` hash each count as one argument, so `find(*ids)`
294
+ # is `Elem`, while `find(id, *ids)` and `find(*args, **opts)` are
295
+ # Arrays even when the splat or the hash turns out empty.
296
+ #
297
+ # With a block `find` is `Enumerable#find` (`return super if
298
+ # block_given?`), so the id overloads do not apply: no argument
299
+ # returns the element or `nil`, and one argument is the `ifnone`
300
+ # callable, whose result joins the union. That form is rare, so
301
+ # it stays `untyped` rather than typing the callable. The
302
+ # class-side `Model.find { … }` reaches this same method through
303
+ # `all`.
304
+ %a{rigor:v1:effect io.db.read}
305
+ def find: () { (Elem) -> boolish } -> Elem?
306
+ | (untyped ifnone) { (Elem) -> boolish } -> untyped
307
+ | (untyped, untyped, *untyped) -> Array[Elem]
308
+ | (?untyped id) -> Elem
151
309
  %a{rigor:v1:effect io.db.read}
152
310
  def find_by: (*untyped) -> Elem?
153
311
  %a{rigor:v1:effect io.db.read}
@@ -243,9 +401,9 @@ module ActiveRecord
243
401
  def entries: () -> Array[Elem]
244
402
  %a{rigor:v1:effect io.db.read}
245
403
  def load: (?untyped) -> self
246
- %a{rigor:v1:effect io.db.read}
404
+ %a{rigor:v1:effect io.db.read, mutate.self}
247
405
  def reload: () -> self
248
- %a{pure}
406
+ %a{rigor:v1:effect mutate.self}
249
407
  def reset: () -> self
250
408
  %a{pure}
251
409
  def loaded?: () -> bool
@@ -272,56 +430,73 @@ module ActiveRecord
272
430
  %a{pure}
273
431
  def table: () -> untyped
274
432
  %a{pure}
275
- def arel: () -> untyped
433
+ def arel: (?untyped aliases) -> untyped
276
434
 
277
435
  # --- whole-relation persistence ---
278
- %a{rigor:v1:effect io.db.write}
436
+ %a{rigor:v1:effect io.db.read, io.db.write, mutate.self}
279
437
  def update_all: (untyped) -> Integer
280
- %a{rigor:v1:effect io.db.write}
281
- def delete_all: () -> Integer
282
- %a{rigor:v1:effect io.db.write}
438
+ # The optional argument is the association proxy's `dependent`
439
+ # (see the header). A plain Relation's and an AssociationRelation's
440
+ # `delete_all` take none.
441
+ %a{rigor:v1:effect io.db.read, io.db.write, mutate.self}
442
+ def delete_all: (?untyped dependent) -> Integer
443
+ # On a plain `has_many`, the proxy's `destroy_all` returns `nil`
444
+ # when a `before_remove` callback throws `:abort`. A `:through` one,
445
+ # which includes `has_and_belongs_to_many`, still returns the
446
+ # records. `Array[Elem]?` would report `call.possible-nil-receiver`
447
+ # on every `destroyed.each` for a case only such a callback reaches.
448
+ %a{rigor:v1:effect io.db.read, io.db.write, mutate.self}
283
449
  def destroy_all: () -> Array[Elem]
284
- %a{rigor:v1:effect io.db.write}
450
+ %a{rigor:v1:effect io.db.read, io.db.write}
285
451
  def update: (*untyped) -> untyped
286
- %a{rigor:v1:effect io.db.write}
452
+ %a{rigor:v1:effect io.db.read, io.db.write}
287
453
  def update!: (*untyped) -> untyped
288
- %a{rigor:v1:effect io.db.write}
454
+ %a{rigor:v1:effect io.db.read, io.db.write}
289
455
  def delete_by: (*untyped) -> Integer
290
- %a{rigor:v1:effect io.db.write}
456
+ %a{rigor:v1:effect io.db.read, io.db.write}
291
457
  def destroy_by: (*untyped) -> Array[Elem]
292
- %a{rigor:v1:effect io.db.write}
458
+ %a{rigor:v1:effect io.db.read, io.db.write, mutate.self}
293
459
  def touch_all: (*untyped) -> Integer
294
- %a{rigor:v1:effect io.db.write}
460
+ %a{rigor:v1:effect io.db.write, mutate.self}
461
+ def insert: (untyped, **untyped) -> untyped
462
+ %a{rigor:v1:effect io.db.write, mutate.self}
295
463
  def insert_all: (untyped, **untyped) -> untyped
296
- %a{rigor:v1:effect io.db.write}
464
+ %a{rigor:v1:effect io.db.write, mutate.self}
465
+ def insert!: (untyped, **untyped) -> untyped
466
+ %a{rigor:v1:effect io.db.write, mutate.self}
297
467
  def insert_all!: (untyped, **untyped) -> untyped
298
- %a{rigor:v1:effect io.db.write}
468
+ %a{rigor:v1:effect io.db.write, mutate.self}
469
+ def upsert: (untyped, **untyped) -> untyped
470
+ %a{rigor:v1:effect io.db.write, mutate.self}
299
471
  def upsert_all: (untyped, **untyped) -> untyped
300
472
 
301
473
  # --- instantiating builders (return an element) ---
302
- %a{pure}
474
+ # Given an Array of attribute hashes, `new`, `build`, `create` and
475
+ # `create!` return an Array of records. No overload says so, for
476
+ # the reason `find` gives: an untyped argument would select it too.
477
+ %a{rigor:v1:effect mutate.self}
303
478
  def new: (*untyped) ?{ (Elem) -> void } -> Elem
304
- %a{pure}
479
+ %a{rigor:v1:effect mutate.self}
305
480
  def build: (*untyped) ?{ (Elem) -> void } -> Elem
306
- %a{rigor:v1:effect io.db.write}
481
+ %a{rigor:v1:effect io.db.read, io.db.write, mutate.self}
307
482
  def create: (*untyped) ?{ (Elem) -> void } -> Elem
308
- %a{rigor:v1:effect io.db.write}
483
+ %a{rigor:v1:effect io.db.read, io.db.write, mutate.self}
309
484
  def create!: (*untyped) ?{ (Elem) -> void } -> Elem
310
- %a{rigor:v1:effect io.db.write}
485
+ %a{rigor:v1:effect io.db.read, io.db.write, mutate.self}
311
486
  def find_or_create_by: (untyped) ?{ (Elem) -> void } -> Elem
312
- %a{rigor:v1:effect io.db.write}
487
+ %a{rigor:v1:effect io.db.read, io.db.write, mutate.self}
313
488
  def find_or_create_by!: (untyped) ?{ (Elem) -> void } -> Elem
314
- %a{rigor:v1:effect io.db.read}
489
+ %a{rigor:v1:effect io.db.read, mutate.self}
315
490
  def find_or_initialize_by: (untyped) ?{ (Elem) -> void } -> Elem
316
- %a{rigor:v1:effect io.db.write}
491
+ %a{rigor:v1:effect io.db.read, io.db.write, mutate.self}
317
492
  def create_or_find_by: (untyped) ?{ (Elem) -> void } -> Elem
318
- %a{rigor:v1:effect io.db.write}
493
+ %a{rigor:v1:effect io.db.read, io.db.write, mutate.self}
319
494
  def create_or_find_by!: (untyped) ?{ (Elem) -> void } -> Elem
320
- %a{rigor:v1:effect io.db.write}
495
+ %a{rigor:v1:effect io.db.read, io.db.write, mutate.self}
321
496
  def first_or_create: (?untyped) ?{ (Elem) -> void } -> Elem
322
- %a{rigor:v1:effect io.db.write}
497
+ %a{rigor:v1:effect io.db.read, io.db.write, mutate.self}
323
498
  def first_or_create!: (?untyped) ?{ (Elem) -> void } -> Elem
324
- %a{rigor:v1:effect io.db.read}
499
+ %a{rigor:v1:effect io.db.read, mutate.self}
325
500
  def first_or_initialize: (?untyped) ?{ (Elem) -> void } -> Elem
326
501
  end
327
502
  end
@@ -67,12 +67,15 @@ module Rigor
67
67
  def init(_services)
68
68
  @model_search_paths = Array(config.fetch("model_search_paths")).map(&:to_s)
69
69
  @attachment_index = nil
70
- @load_errors = []
70
+ @load_errors = {}
71
71
  end
72
72
 
73
73
  def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
74
74
  index = attachment_index
75
- return load_error_diagnostics(path) if index.nil?
75
+ if index.nil?
76
+ disclose_load_errors
77
+ return []
78
+ end
76
79
  return [] if index.empty?
77
80
 
78
81
  Analyzer.new(path: path, attachment_index: index).analyze(root).diagnostics
@@ -126,24 +129,32 @@ module Rigor
126
129
  # covers model-file additions.
127
130
  @attachment_index = cache_for(:attachment_index, params: {}).call
128
131
  rescue Plugin::AccessDeniedError => e
129
- @load_errors << "rigor-activestorage: #{e.message}"
132
+ @load_errors["1-read-refused"] ||= "rigor-activestorage: #{e.message}"
130
133
  nil
131
134
  rescue StandardError => e
132
- @load_errors << "rigor-activestorage: discovery failed: #{e.class}: #{e.message}"
135
+ @load_errors["2-discovery-failed"] ||=
136
+ "rigor-activestorage: discovery failed: #{e.class}: #{e.message}"
133
137
  nil
134
138
  end
135
139
 
136
- def load_error_diagnostics(path)
137
- @load_errors.uniq.map do |message|
138
- Rigor::Analysis::Diagnostic.new(
139
- path: path,
140
- line: 1,
141
- column: 1,
142
- message: message,
143
- severity: :warning,
144
- rule: "load-error"
145
- )
140
+ # Issue #1056 — "the attachment index did not load" is a fact about the run's INPUTS, not about the
141
+ # file being analysed, so both outcomes are registered rather than returned: see
142
+ # {Plugin::Base#disclose_once} for the channel and the position. Returned from the per-file hook they
143
+ # carried no once-guard at all — the `.uniq` collapsed repeats within one instance's list, but the
144
+ # whole list was re-emitted on every analysed file.
145
+ #
146
+ # The `key`s carry an ordinal prefix because the engine emits a plugin's disclosures in KEY order
147
+ # (#1051), and a refused read must still precede a discovery failure the way `@load_errors` records
148
+ # them — `@load_errors` is now keyed by that same string, which also caps it: `#attachment_index`
149
+ # re-attempts the load per call site, and the old Array appended a fresh interpolated string every
150
+ # time (the unbounded-growth shape #569 measured at 4.2 M retained strings on Redmine). The key, not
151
+ # the message, is the identity: a second refusal naming a different path collapses into the first row
152
+ # rather than adding one — the notice is "the index did not load", said once.
153
+ def disclose_load_errors
154
+ @load_errors.each do |key, message|
155
+ disclose_once(key, message: message, severity: :warning, rule: "load-error")
146
156
  end
157
+ nil
147
158
  end
148
159
  end
149
160
 
@@ -8,7 +8,7 @@ module Rigor
8
8
  # rigor-activesupport-core-ext's effect contract (ADR-103 WD10; design note § 11.2; issue #387).
9
9
  #
10
10
  # **Scope note.** This is the *impure* half of ActiveSupport only: the clock, the notification bus and
11
- # `CurrentAttributes`. The `%a{pure}` sweep over `blank?` / `present?` / `deep_dup` and the rest of
11
+ # `CurrentAttributes`. The `%a{pure}` sweep over `blank?` / `present?` / `presence` and the rest of
12
12
  # the core_ext predicate surface — the single cheapest purity win in a Rails app, per WD10 — is issue
13
13
  # #388 and lands in this plugin's shipped RBS, not here (`try` was audited and skipped: it dispatches
14
14
  # to whatever method the caller names, so its purity is a fact about the call site, not about `try`).
@@ -115,11 +115,18 @@ module Rigor
115
115
  # which is what earns `Date` its row here — the label is a fact about the module, not about the
116
116
  # receiver, and the three only ever differed in which spellings the bundle had got round to
117
117
  # declaring.
118
+ #
119
+ # `String#in_time_zone` is the exception: it has no instant to convert. It parses through
120
+ # `TimeZone#parse(str, now = now())`, whose default argument reads the clock on every call, or through
121
+ # `to_time` when no zone is set — so it takes `CLOCK`, not `ZONE_READ`.
118
122
  def in_time_zone_rows
119
123
  [TIME, DATE, DATETIME].map do |receiver|
120
124
  row(receiver, :in_time_zone, ZONE_READ,
121
125
  why: "the default argument reads `Time.zone` when the caller doesn't name one explicitly")
122
- end
126
+ end + [
127
+ row("String", :in_time_zone, CLOCK,
128
+ why: "parses in `Time.zone` (the default argument) and fills the parts from the zone's `now`")
129
+ ]
123
130
  end
124
131
 
125
132
  def current_rows
@@ -163,6 +163,10 @@ class Object
163
163
  # exception to the try-shaped-dispatch rule above (design note § 11.2).
164
164
  %a{pure}
165
165
  def in?: (untyped) -> bool
166
+ # `in?(another_object) ? self : nil` — `params[:kind].presence_in(%w[a b])`.
167
+ # The same `#include?` dispatch as `in?`, and the same exception.
168
+ %a{pure}
169
+ def presence_in: (untyped) -> self?
166
170
 
167
171
  # Issue #673 — `core_ext/object/to_query`, `core_ext/object/duplicable` and
168
172
  # `core_ext/object/instance_variables` all hang off `Object`, so every
@@ -191,6 +195,63 @@ class Object
191
195
  # `instance_variable_names`.
192
196
  def instance_values: () -> Hash[String, untyped]
193
197
  def instance_variable_names: () -> Array[String]
198
+
199
+ # `core_ext/object/deep_dup` — `duplicable? ? dup : self`, plus `Array`
200
+ # (`map(&:deep_dup)`), `Hash` and `Module` overrides. `self` over-claims on
201
+ # an `Array` subclass, whose override answers a plain `Array`. Declared on
202
+ # `Object` and not only on `Hash`, where it sat alone: `[[1], [2]].deep_dup`
203
+ # is the common shape.
204
+ #
205
+ # NOT `%a{pure}`: on `Object` the `dup` runs the receiver class's own
206
+ # `initialize_copy`, an unknown-at-this-declaration dispatch like the one
207
+ # that keeps `as_json` bare. `Hash#deep_dup` below reaches the same `dup`
208
+ # through `value.deep_dup` on every value and is bare for the same reason.
209
+ def deep_dup: () -> self
210
+
211
+ # `core_ext/object/with` (ActiveSupport 7.1+) — sets each keyword through
212
+ # its public writer, yields `self`, and restores the old values in an
213
+ # `ensure`; the answer is the block's. There is no blockless form: `yield` without a block raises.
214
+ # ActiveSupport `undef_method`s it on `NilClass`, `TrueClass`, `FalseClass`,
215
+ # `Integer`, `Float` and `Symbol`, which RBS cannot express, so `1.with(a: 1)`
216
+ # resolves here where Ruby raises — a missed error, not a false one.
217
+ # `Data#with` (core) is a subclass override and keeps its own `-> self`.
218
+ #
219
+ # NOT `%a{pure}`: it writes the receiver's attributes through `public_send`.
220
+ def with: [T] (**untyped attributes) { (self) -> T } -> T
221
+
222
+ # `core_ext/object/with_options` — without a block, the
223
+ # `ActiveSupport::OptionMerger` proxy (not declared here); with one, the
224
+ # block's value. A zero-arity block is `instance_eval`ed on the proxy, the
225
+ # `with_options dependent: :destroy do has_many … end` model-body shape.
226
+ #
227
+ # NOT `%a{pure}`: the proxy forwards every call to the receiver through
228
+ # `method_missing`, a dispatch this audit cannot see.
229
+ def with_options: (untyped options) -> untyped
230
+ | [T] (untyped options) { (?) -> T } -> T
231
+
232
+ # `core_ext/string/output_safety` — `false` on `Object`, `true` on
233
+ # `Numeric`, the buffer's own state on `ActiveSupport::SafeBuffer`. `bool`
234
+ # for the reason `duplicable?` is: `-> false` on the universal receiver makes
235
+ # every `if x.html_safe?` an always-falsy branch on the subclasses that answer
236
+ # true. All three bodies are a constant or an ivar read.
237
+ %a{pure}
238
+ def html_safe?: () -> bool
239
+ end
240
+
241
+ # ---------------------------------------------------------------
242
+ # Kernel — `core_ext/kernel/singleton_class`
243
+ # ---------------------------------------------------------------
244
+
245
+ # `singleton_class.class_eval(*args, &block)` on any object. Declared on
246
+ # `Kernel`, where ActiveSupport defines it; a `Module` receiver still answers
247
+ # core `Module#class_eval`, which is its own alias of `module_eval`. The two
248
+ # overloads mirror that `module_eval`: a string, or a block given the
249
+ # singleton class.
250
+ module Kernel
251
+ # NOT `%a{pure}`: it evaluates a string or a block in the singleton class,
252
+ # and a block that defines methods is the point.
253
+ def class_eval: (string code, ?string? filename, ?int lineno) -> untyped
254
+ | [U] () { (untyped singleton_class) -> U } -> U
194
255
  end
195
256
 
196
257
  # `nil.blank?` / `nil.present?` / `nil.try` are the most frequent
@@ -269,6 +330,8 @@ class String
269
330
  def dasherize: () -> String
270
331
  %a{pure}
271
332
  def upcase_first: () -> String
333
+ %a{pure}
334
+ def downcase_first: () -> String # ActiveSupport 7.1+
272
335
  # NOT %a{pure}: transliterates through `I18n.transliterate`, which reads
273
336
  # `I18n.locale` when `locale:` is left at its `nil` default — a request-
274
337
  # scoped global read the inflection-table assumption does not cover.
@@ -284,6 +347,11 @@ class String
284
347
  %a{pure}
285
348
  def humanize: (?capitalize: bool, ?keep_id_suffix: bool) -> String
286
349
 
350
+ # `core_ext/string/behavior` — the `acts_like?(:string)` duck-type hook, a
351
+ # hardcoded `true`.
352
+ %a{pure}
353
+ def acts_like_string?: () -> true
354
+
287
355
  # `core_ext/string/filters`
288
356
  %a{pure}
289
357
  def squish: () -> String
@@ -336,6 +404,15 @@ class String
336
404
  # follow-up to confirm and either implement or remove the declaration.
337
405
  def to_hours: () -> Float
338
406
 
407
+ # `core_ext/string/zones` — `Time.find_zone!(zone).parse(self)`, or
408
+ # `to_time` when no zone is set. `untyped` like the `Time` / `Date` rows:
409
+ # the parse answers an `ActiveSupport::TimeWithZone`, or `nil` on a string
410
+ # with no date parts. NOT `%a{pure}`: the default argument reads
411
+ # `Time.zone`, and `TimeZone#parse(str, now = now())` reads the clock on
412
+ # every call, whether or not the string names every part. The plugin's
413
+ # `effect_attributions:` carry both labels.
414
+ def in_time_zone: (?untyped zone) -> untyped
415
+
339
416
  # `core_ext/string/access`
340
417
  %a{pure}
341
418
  def at: (Integer | Range[Integer] | Regexp position) -> String?
@@ -355,12 +432,29 @@ class String
355
432
  # `core_ext/string/multibyte`
356
433
  %a{pure}
357
434
  def mb_chars: () -> untyped
435
+ # Reads `encoding` / `valid_encoding?`; the `ASCII-8BIT` branch
436
+ # `force_encoding`s a `dup`, never the receiver.
437
+ %a{pure}
438
+ def is_utf8?: () -> bool
358
439
 
359
440
  # `core_ext/string/inquiry`
360
441
  %a{pure}
361
442
  def inquiry: () -> untyped # ActiveSupport::StringInquirer
362
443
  end
363
444
 
445
+ # ---------------------------------------------------------------
446
+ # Symbol — `core_ext/symbol/starts_ends_with`
447
+ # ---------------------------------------------------------------
448
+
449
+ # `alias_method`s of core `start_with?` / `end_with?`, declared with the core
450
+ # parameter lists.
451
+ class Symbol
452
+ %a{pure}
453
+ def starts_with?: (*Regexp | string prefixes) -> bool
454
+ %a{pure}
455
+ def ends_with?: (*string suffixes) -> bool
456
+ end
457
+
364
458
  # ---------------------------------------------------------------
365
459
  # Integer — `core_ext/integer/*`
366
460
  # ---------------------------------------------------------------
@@ -1369,6 +1463,10 @@ class Array[unchecked out Elem]
1369
1463
  def fifth: () -> Elem?
1370
1464
  %a{pure}
1371
1465
  def forty_two: () -> Elem?
1466
+ %a{pure}
1467
+ def second_to_last: () -> Elem?
1468
+ %a{pure}
1469
+ def third_to_last: () -> Elem?
1372
1470
 
1373
1471
  # `core_ext/array/grouping`
1374
1472
  %a{pure}
@@ -1400,6 +1498,9 @@ class Array[unchecked out Elem]
1400
1498
  # `core_ext/array/extract`
1401
1499
  def extract!: () { (Elem) -> bool } -> Array[Elem] # NOT %a{pure}: bang, mutates in place.
1402
1500
 
1501
+ # `core_ext/array/extract_options`
1502
+ def extract_options!: () -> Hash[untyped, untyped] # NOT %a{pure}: pops the trailing options Hash.
1503
+
1403
1504
  # `core_ext/object/blank` / `core_ext/enumerable` — both
1404
1505
  # ActiveSupport additions, shipped on Array specifically.
1405
1506
  %a{pure}
@@ -1444,6 +1545,15 @@ module Enumerable[unchecked out Elem]
1444
1545
  def sole: () -> Elem
1445
1546
  %a{pure}
1446
1547
  def compact_blank: () -> Array[Elem]
1548
+ %a{pure}
1549
+ def many?: () -> bool
1550
+ | () { (Elem) -> boolish } -> bool
1551
+ # `key` and `series` are untyped on purpose: the default `filter: true`
1552
+ # path is `group_by(&key).values_at(*series)`, so a Proc key or a Set /
1553
+ # Range series is correct Rails code. `filter:` is ActiveSupport 8.x;
1554
+ # passing it on 7.x raises, which this unversioned row does not catch.
1555
+ %a{pure}
1556
+ def in_order_of: (untyped key, untyped series, ?filter: boolish) -> Array[Elem]
1447
1557
  end
1448
1558
 
1449
1559
  # ---------------------------------------------------------------
@@ -1474,10 +1584,17 @@ class Hash[unchecked out K, unchecked out V]
1474
1584
  %a{pure}
1475
1585
  def deep_transform_values: () { (V) -> untyped } -> Hash[K, untyped]
1476
1586
  def deep_transform_values!: () { (V) -> untyped } -> self
1587
+ # `alias_method`s of `symbolize_keys` / `symbolize_keys!`.
1588
+ %a{pure}
1589
+ def to_options: () -> Hash[Symbol, V]
1590
+ def to_options!: () -> self
1477
1591
 
1478
1592
  # `core_ext/hash/deep_dup` — allocates a deep copy, never touches the
1479
- # receiver.
1480
- %a{pure}
1593
+ # receiver. NOT `%a{pure}`: it calls `deep_dup` on every value, and on
1594
+ # every key that is not a `String` or `Symbol` — an ELEMENT of unknown class
1595
+ # whose `dup` runs that class's own `initialize_copy`. That is the dispatch
1596
+ # the purity rule at the top of this file excludes, and the reason
1597
+ # `Object#deep_dup` above is bare.
1481
1598
  def deep_dup: () -> Hash[K, V]
1482
1599
 
1483
1600
  # `core_ext/hash/deep_merge` — the block receives `(key, this_val,
@@ -1488,6 +1605,10 @@ class Hash[unchecked out K, unchecked out V]
1488
1605
  | (Hash[K, V]) { (K, V, V) -> V } -> Hash[K, V]
1489
1606
  def deep_merge!: (Hash[K, V]) -> self
1490
1607
  | (Hash[K, V]) { (K, V, V) -> V } -> self
1608
+ # `other.is_a?(Hash)` — the hook `ActiveSupport::DeepMergeable` asks before
1609
+ # recursing into a value. `:nodoc:`, but public.
1610
+ %a{pure}
1611
+ def deep_merge?: (untyped other) -> bool
1491
1612
 
1492
1613
  # `core_ext/hash/except` — `Hash#except` is in core RBS as of
1493
1614
  # Ruby 3.0+; `except!` is ActiveSupport-only. ActiveSupport
@@ -1512,6 +1633,9 @@ class Hash[unchecked out K, unchecked out V]
1512
1633
  # `%a{pure}` explicitly.
1513
1634
  %a{pure}
1514
1635
  def with_indifferent_access: () -> untyped
1636
+ # An `alias` of `with_indifferent_access`.
1637
+ %a{pure}
1638
+ def nested_under_indifferent_access: () -> untyped
1515
1639
 
1516
1640
  # `core_ext/hash/conversions`
1517
1641
  def self.from_xml: (String, ?Symbol disallowed_types) -> Hash[String, untyped]
@@ -1522,15 +1646,57 @@ class Hash[unchecked out K, unchecked out V]
1522
1646
  def compact_blank: () -> Hash[K, V]
1523
1647
  def compact_blank!: () -> self # NOT %a{pure}: bang (`delete_if`), mutates in place.
1524
1648
 
1525
- # `core_ext/hash/reverse_merge`
1649
+ # `core_ext/array/extract_options`
1526
1650
  %a{pure}
1527
- def reverse_merge: (Hash[K, V]) -> Hash[K, V]
1651
+ def extractable_options?: () -> bool
1652
+
1653
+ # `core_ext/hash/reverse_merge` — `other_hash.merge(self)`, so the result
1654
+ # carries the argument's keys and values as well as the receiver's, the
1655
+ # same shape as core `Hash#merge`. `-> Hash[K, V]` typed
1656
+ # `opts.reverse_merge(size: 25, velocity: 10)` as `Hash[:size, 1]`.
1657
+ %a{pure}
1658
+ def reverse_merge: [A, B] (Hash[A, B] other_hash) -> Hash[A | K, B | V]
1528
1659
  def reverse_merge!: (Hash[K, V]) -> self # NOT %a{pure}: bang (`replace`), mutates in place.
1660
+ # `alias_method`s: `with_defaults` of `reverse_merge`, `with_defaults!`
1661
+ # and `reverse_update` of `reverse_merge!`.
1662
+ %a{pure}
1663
+ def with_defaults: [A, B] (Hash[A, B] other_hash) -> Hash[A | K, B | V]
1664
+ def with_defaults!: (Hash[K, V]) -> self # NOT %a{pure}: bang (`replace`), mutates in place.
1665
+ def reverse_update: (Hash[K, V]) -> self # NOT %a{pure}: `reverse_merge!` under another name.
1529
1666
 
1530
1667
  # `core_ext/hash/slice` — `Hash#except` is in core RBS (Ruby 3.0+);
1531
1668
  # `Hash#slice` is in core RBS (Ruby 2.5+); the bang variants are
1532
- # ActiveSupport-only.
1669
+ # ActiveSupport-only. `extract!` deletes the named keys and answers them
1670
+ # as a new hash; a key the receiver lacks is skipped, not `nil`-filled.
1533
1671
  def slice!: (*K) -> Hash[K, V] # NOT %a{pure}: bang (`replace`), mutates in place.
1672
+ def extract!: (*K) -> Hash[K, V] # NOT %a{pure}: bang, mutates via `delete`.
1673
+ end
1674
+
1675
+ # ---------------------------------------------------------------
1676
+ # Range — `core_ext/range/*`
1677
+ # ---------------------------------------------------------------
1678
+
1679
+ # `[out E]` matches core `range.rbs`. A reopening whose parameters differ in
1680
+ # count or variance (`class Range`, `class Range[E]`) raises
1681
+ # `RBS::GenericParameterMismatchError` and collapses `Range`.
1682
+ class Range[out E]
1683
+ # `core_ext/range/overlap` — an `alias` of `overlap?`, which rbs declares
1684
+ # since Ruby 3.3 (ActiveSupport defines the method itself only on an older
1685
+ # Ruby). `overlap?` is therefore NOT redeclared here: a second declaration
1686
+ # is a `DuplicatedMethodDefinitionError` that collapses the whole class.
1687
+ %a{pure}
1688
+ def overlaps?: (Range[untyped]) -> bool
1689
+
1690
+ # `core_ext/range/conversions` — `ActiveSupport::RangeWithFormat`, prepended
1691
+ # onto `Range`. An unknown format falls back to `to_s`; `:db` answers `nil`
1692
+ # for a beginless-and-endless range, which the declared `String` does not
1693
+ # model — `(nil..nil).to_fs(:db)` is not a shape correct code writes.
1694
+ #
1695
+ # NOT `%a{pure}`, for the `Array#to_fs` reasons: `RANGE_FORMATS` is a
1696
+ # mutable constant an initializer extends, and `:db` calls `to_fs(:db)` on
1697
+ # both endpoints.
1698
+ def to_fs: (?Symbol format) -> String
1699
+ def to_formatted_s: (?Symbol format) -> String
1534
1700
  end
1535
1701
 
1536
1702
  # ---------------------------------------------------------------