rigortype 0.3.9 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (363) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/core_overlay/enumerable.rbs +51 -0
  4. data/data/core_overlay/enumerator.rbs +84 -0
  5. data/data/core_overlay/hash_rbs3.rbs +41 -0
  6. data/data/core_overlay/process.rbs +40 -0
  7. data/data/core_overlay/string_io.rbs +33 -0
  8. data/data/effects/core.yml +3 -3
  9. data/data/gem_overlay/activesupport/core_ext.rbs +127 -4
  10. data/docs/handbook/03-narrowing.md +95 -10
  11. data/docs/handbook/04-tuples-and-shapes.md +8 -6
  12. data/docs/handbook/07-rbs-and-extended.md +16 -11
  13. data/docs/handbook/10-sorbet.md +9 -10
  14. data/docs/handbook/11-sig-gen.md +454 -13
  15. data/docs/manual/02-cli-reference.md +63 -2
  16. data/docs/manual/03-configuration.md +7 -0
  17. data/docs/manual/04-diagnostics.md +4 -0
  18. data/docs/manual/07-plugins.md +1 -1
  19. data/docs/manual/10-mcp-server.md +3 -2
  20. data/docs/manual/16-rbs-extended-annotations.md +40 -7
  21. data/docs/manual/19-effect-labels.md +10 -0
  22. data/docs/manual/plugins/README.md +7 -0
  23. data/docs/manual/plugins/rigor-actioncable.md +8 -1
  24. data/docs/manual/plugins/rigor-actionmailer.md +7 -0
  25. data/docs/manual/plugins/rigor-actionpack.md +243 -0
  26. data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
  27. data/docs/manual/plugins/rigor-activejob.md +7 -0
  28. data/docs/manual/plugins/rigor-activerecord.md +110 -4
  29. data/docs/manual/plugins/rigor-activestorage.md +8 -1
  30. data/docs/manual/plugins/rigor-activesupport-core-ext.md +11 -5
  31. data/docs/manual/plugins/rigor-grape.md +106 -0
  32. data/docs/manual/plugins/rigor-graphql.md +23 -2
  33. data/docs/manual/plugins/rigor-pundit.md +8 -1
  34. data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
  35. data/docs/manual/plugins/rigor-rails-routes.md +7 -0
  36. data/docs/manual/plugins/rigor-rbs-inline.md +212 -25
  37. data/docs/manual/plugins/rigor-sidekiq.md +8 -1
  38. data/docs/manual/plugins/rigor-sorbet.md +21 -5
  39. data/exe/rigor +19 -4
  40. data/lib/rigor/analysis/baseline.rb +1 -1
  41. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
  42. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
  43. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -3
  44. data/lib/rigor/analysis/check_rules/rule_ids.rb +20 -5
  45. data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
  46. data/lib/rigor/analysis/check_rules/source_arity.rb +323 -0
  47. data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
  48. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +36 -2
  49. data/lib/rigor/analysis/check_rules.rb +520 -49
  50. data/lib/rigor/analysis/dependency_recorder.rb +23 -0
  51. data/lib/rigor/analysis/fact_store.rb +9 -0
  52. data/lib/rigor/analysis/incremental.rb +26 -0
  53. data/lib/rigor/analysis/incremental_session.rb +36 -4
  54. data/lib/rigor/analysis/project_scan.rb +11 -1
  55. data/lib/rigor/analysis/reachability/plugin_roots.rb +0 -1
  56. data/lib/rigor/analysis/reachability/scan_cache.rb +1 -0
  57. data/lib/rigor/analysis/rule_catalog.rb +127 -0
  58. data/lib/rigor/analysis/run_cache_key.rb +20 -11
  59. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +167 -11
  60. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -4
  61. data/lib/rigor/analysis/runner/pool_coordinator.rb +107 -15
  62. data/lib/rigor/analysis/runner/project_pre_passes.rb +26 -7
  63. data/lib/rigor/analysis/runner.rb +278 -20
  64. data/lib/rigor/analysis/template_unit_collector.rb +303 -0
  65. data/lib/rigor/analysis/template_unit_paths.rb +91 -0
  66. data/lib/rigor/analysis/template_unit_positions.rb +293 -0
  67. data/lib/rigor/analysis/template_units.rb +399 -0
  68. data/lib/rigor/analysis/worker_session.rb +53 -10
  69. data/lib/rigor/bleeding_edge.rb +0 -2
  70. data/lib/rigor/builtins/hkt_builtins.rb +1 -0
  71. data/lib/rigor/builtins/imported_refinements.rb +4 -0
  72. data/lib/rigor/builtins/regex_refinement.rb +17 -9
  73. data/lib/rigor/builtins/static_return_refinements.rb +2 -0
  74. data/lib/rigor/cache/descriptor.rb +6 -1
  75. data/lib/rigor/cache/engine_source.rb +29 -1
  76. data/lib/rigor/cache/incremental_snapshot.rb +33 -1
  77. data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
  78. data/lib/rigor/cache/rbs_descriptor.rb +37 -7
  79. data/lib/rigor/cache/store.rb +0 -1
  80. data/lib/rigor/ci_detector.rb +1 -0
  81. data/lib/rigor/cli/doc_links.rb +1 -1
  82. data/lib/rigor/cli/docs_command.rb +4 -4
  83. data/lib/rigor/cli/plugin_command.rb +3 -3
  84. data/lib/rigor/cli/prism_colorizer.rb +0 -1
  85. data/lib/rigor/cli/sig_gen_command.rb +210 -29
  86. data/lib/rigor/cli/skill_command.rb +1 -1
  87. data/lib/rigor/cli/skill_describe.rb +0 -1
  88. data/lib/rigor/cli/type_of_command.rb +19 -5
  89. data/lib/rigor/cli/type_of_renderer.rb +20 -9
  90. data/lib/rigor/cli/type_of_template_probe.rb +189 -0
  91. data/lib/rigor/cli.rb +21 -3
  92. data/lib/rigor/configuration/severity_profile.rb +19 -3
  93. data/lib/rigor/configuration.rb +66 -3
  94. data/lib/rigor/effects/ancestry_recorder.rb +191 -0
  95. data/lib/rigor/effects/attribution.rb +11 -2
  96. data/lib/rigor/effects/callee_rule.rb +368 -0
  97. data/lib/rigor/effects/catalog.rb +7 -4
  98. data/lib/rigor/effects/collector.rb +11 -5
  99. data/lib/rigor/effects/config_envelopes.rb +9 -2
  100. data/lib/rigor/effects/definition_context.rb +179 -0
  101. data/lib/rigor/effects/effect_table.rb +11 -3
  102. data/lib/rigor/effects/envelope_check.rb +1 -1
  103. data/lib/rigor/effects/envelope_index.rb +15 -0
  104. data/lib/rigor/effects/file_collection.rb +59 -5
  105. data/lib/rigor/effects/framework_units.rb +1 -1
  106. data/lib/rigor/effects/identity.rb +16 -0
  107. data/lib/rigor/effects/local_ownership.rb +38 -12
  108. data/lib/rigor/effects/method_key.rb +21 -0
  109. data/lib/rigor/effects/mutation_classifier.rb +23 -12
  110. data/lib/rigor/effects/plugin_facts.rb +43 -31
  111. data/lib/rigor/effects/propagator.rb +295 -16
  112. data/lib/rigor/effects/registry.rb +1 -1
  113. data/lib/rigor/effects/scanner.rb +121 -72
  114. data/lib/rigor/effects/signature_sources.rb +1 -1
  115. data/lib/rigor/effects/snapshot.rb +2 -1
  116. data/lib/rigor/effects/summary.rb +27 -4
  117. data/lib/rigor/effects/unit_scan.rb +385 -30
  118. data/lib/rigor/effects/visibility.rb +101 -0
  119. data/lib/rigor/environment/lockfile_resolver.rb +17 -0
  120. data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
  121. data/lib/rigor/environment/member_consistency.rb +298 -0
  122. data/lib/rigor/environment/rbs_loader.rb +399 -133
  123. data/lib/rigor/environment.rb +103 -38
  124. data/lib/rigor/hashing/xxh3.rb +264 -0
  125. data/lib/rigor/inference/acceptance.rb +139 -9
  126. data/lib/rigor/inference/block_auto_splat.rb +216 -0
  127. data/lib/rigor/inference/block_call_timing.rb +338 -0
  128. data/lib/rigor/inference/block_parameter_binder.rb +73 -27
  129. data/lib/rigor/inference/block_repetition.rb +71 -0
  130. data/lib/rigor/inference/body_fixpoint.rb +2 -1
  131. data/lib/rigor/inference/budget_trace.rb +2 -1
  132. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  133. data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
  134. data/lib/rigor/inference/captured_locals.rb +387 -15
  135. data/lib/rigor/inference/closure_escape_analyzer.rb +157 -13
  136. data/lib/rigor/inference/content_join.rb +200 -27
  137. data/lib/rigor/inference/def_return_typer.rb +11 -7
  138. data/lib/rigor/inference/define_method_block_self.rb +64 -0
  139. data/lib/rigor/inference/element_read_widening.rb +22 -10
  140. data/lib/rigor/inference/error_info.rb +196 -0
  141. data/lib/rigor/inference/expression_typer.rb +1532 -496
  142. data/lib/rigor/inference/external_ancestor_resolution.rb +267 -0
  143. data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
  144. data/lib/rigor/inference/global_write_census.rb +239 -0
  145. data/lib/rigor/inference/guard_rebinding.rb +447 -0
  146. data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
  147. data/lib/rigor/inference/index_write_widening.rb +16 -3
  148. data/lib/rigor/inference/indexed_narrowing.rb +61 -9
  149. data/lib/rigor/inference/jump_targets.rb +82 -0
  150. data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
  151. data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
  152. data/lib/rigor/inference/last_line.rb +340 -0
  153. data/lib/rigor/inference/last_status.rb +144 -0
  154. data/lib/rigor/inference/macro_block_self_type.rb +167 -17
  155. data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
  156. data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
  157. data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
  158. data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
  159. data/lib/rigor/inference/match_rebinding.rb +392 -0
  160. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  161. data/lib/rigor/inference/method_dispatcher/block_folding.rb +85 -24
  162. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
  163. data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
  164. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +4 -0
  165. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
  166. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +113 -73
  167. data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
  168. data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
  169. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +881 -38
  170. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
  171. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +128 -53
  172. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +1 -1
  173. data/lib/rigor/inference/method_dispatcher.rb +214 -13
  174. data/lib/rigor/inference/method_parameter_binder.rb +8 -3
  175. data/lib/rigor/inference/multi_target_binder.rb +340 -52
  176. data/lib/rigor/inference/mutation_rejoin.rb +4 -1
  177. data/lib/rigor/inference/mutation_widening.rb +70 -48
  178. data/lib/rigor/inference/narrowing.rb +540 -120
  179. data/lib/rigor/inference/operand_effects.rb +167 -0
  180. data/lib/rigor/inference/operand_walk.rb +88 -0
  181. data/lib/rigor/inference/optimistic_origin.rb +152 -9
  182. data/lib/rigor/inference/parameter_inference_collector.rb +3 -2
  183. data/lib/rigor/inference/project_method_ownership.rb +136 -0
  184. data/lib/rigor/inference/project_patched_methods.rb +7 -2
  185. data/lib/rigor/inference/project_patched_scanner.rb +7 -3
  186. data/lib/rigor/inference/receiver_alias.rb +90 -1
  187. data/lib/rigor/inference/receiver_blind_block.rb +219 -0
  188. data/lib/rigor/inference/refinement_mutation.rb +15 -11
  189. data/lib/rigor/inference/repeated_or_writes.rb +463 -0
  190. data/lib/rigor/inference/return_barrier.rb +54 -0
  191. data/lib/rigor/inference/rewrite_mutation.rb +120 -0
  192. data/lib/rigor/inference/scope_indexer.rb +4615 -551
  193. data/lib/rigor/inference/statement_evaluator.rb +3176 -490
  194. data/lib/rigor/inference/stored_block_call.rb +54 -0
  195. data/lib/rigor/inference/string_mutation.rb +44 -7
  196. data/lib/rigor/inference/unknown_store_widening.rb +200 -0
  197. data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
  198. data/lib/rigor/language_server/debouncer.rb +0 -1
  199. data/lib/rigor/language_server/diagnostic_publisher.rb +3 -2
  200. data/lib/rigor/language_server/hover_renderer.rb +3 -3
  201. data/lib/rigor/language_server/project_context.rb +5 -3
  202. data/lib/rigor/mcp/server.rb +2 -1
  203. data/lib/rigor/plugin/base.rb +168 -5
  204. data/lib/rigor/plugin/box_probe.rb +91 -0
  205. data/lib/rigor/plugin/bundled_catalog.rb +1 -1
  206. data/lib/rigor/plugin/effect_attribution.rb +58 -4
  207. data/lib/rigor/plugin/loader.rb +2 -1
  208. data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
  209. data/lib/rigor/plugin/manifest.rb +71 -10
  210. data/lib/rigor/plugin/registry.rb +35 -1
  211. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
  212. data/lib/rigor/plugin/template_unit.rb +196 -0
  213. data/lib/rigor/plugin.rb +1 -0
  214. data/lib/rigor/protection/discovery_seed.rb +3 -1
  215. data/lib/rigor/protection/kill_signature.rb +0 -1
  216. data/lib/rigor/protection/mutation_cache.rb +1 -2
  217. data/lib/rigor/rbs_extended.rb +27 -0
  218. data/lib/rigor/reflection/constant_ancestors.rb +97 -0
  219. data/lib/rigor/reflection/constant_path.rb +19 -6
  220. data/lib/rigor/reflection.rb +72 -105
  221. data/lib/rigor/scope/discovery_index.rb +135 -2
  222. data/lib/rigor/scope.rb +859 -49
  223. data/lib/rigor/sig_gen/alias_index.rb +289 -0
  224. data/lib/rigor/sig_gen/classification.rb +22 -5
  225. data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
  226. data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
  227. data/lib/rigor/sig_gen/generator.rb +558 -31
  228. data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
  229. data/lib/rigor/sig_gen/method_candidate.rb +47 -3
  230. data/lib/rigor/sig_gen/observation_collector.rb +1 -1
  231. data/lib/rigor/sig_gen/renderer.rb +124 -9
  232. data/lib/rigor/sig_gen/skip_reason_catalog.rb +44 -1
  233. data/lib/rigor/sig_gen/write_result.rb +19 -3
  234. data/lib/rigor/sig_gen/writer.rb +166 -35
  235. data/lib/rigor/sig_gen.rb +3 -0
  236. data/lib/rigor/signature_path_audit.rb +1 -1
  237. data/lib/rigor/source/node_walker.rb +0 -3
  238. data/lib/rigor/source/parameter_envelope.rb +72 -0
  239. data/lib/rigor/source.rb +1 -0
  240. data/lib/rigor/type/combinator.rb +88 -12
  241. data/lib/rigor/type/difference.rb +1 -0
  242. data/lib/rigor/type/hash_shape.rb +1 -1
  243. data/lib/rigor/type/refined.rb +1 -0
  244. data/lib/rigor/version.rb +1 -1
  245. data/lib/rigor.rb +1 -0
  246. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
  247. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +9 -5
  248. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -4
  249. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +0 -1
  250. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
  251. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
  252. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
  253. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
  254. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
  255. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
  256. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +239 -4
  257. data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
  258. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
  259. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
  260. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
  261. data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
  262. data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
  263. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -0
  264. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +9 -5
  265. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
  266. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
  267. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +380 -3
  268. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
  269. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +281 -35
  270. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
  271. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -14
  272. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
  273. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +171 -5
  274. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +2 -0
  275. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +1 -0
  276. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
  277. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
  278. data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
  279. data/plugins/rigor-grape/sig/grape.rbs +263 -0
  280. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +72 -2
  281. data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
  282. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -0
  283. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +11 -7
  284. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +35 -37
  285. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -1
  286. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -0
  287. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +1 -0
  288. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +9 -17
  289. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
  290. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +217 -36
  291. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +1 -0
  292. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +9 -5
  293. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +0 -1
  294. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +46 -1
  295. data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
  296. data/sig/rigor/analysis/baseline.rbs +69 -7
  297. data/sig/rigor/analysis/fact_store.rbs +1 -1
  298. data/sig/rigor/analysis/project_scan.rbs +74 -0
  299. data/sig/rigor/effects/config_envelopes.rbs +100 -0
  300. data/sig/rigor/effects/effect_table.rbs +60 -0
  301. data/sig/rigor/effects/envelope.rbs +97 -0
  302. data/sig/rigor/effects/envelope_index.rbs +33 -0
  303. data/sig/rigor/effects/file_collection.rbs +81 -0
  304. data/sig/rigor/effects/label.rbs +26 -0
  305. data/sig/rigor/effects/label_set.rbs +46 -0
  306. data/sig/rigor/effects/method_key.rbs +24 -0
  307. data/sig/rigor/effects/origin.rbs +50 -0
  308. data/sig/rigor/effects/plugin_facts.rbs +141 -0
  309. data/sig/rigor/effects/registry.rbs +68 -0
  310. data/sig/rigor/effects/summary.rbs +50 -0
  311. data/sig/rigor/effects/taint_cause.rbs +12 -0
  312. data/sig/rigor/environment.rbs +12 -10
  313. data/sig/rigor/inference/optimistic_origin.rbs +10 -0
  314. data/sig/rigor/inference.rbs +6 -4
  315. data/sig/rigor/plugin/additional_initializer.rbs +36 -0
  316. data/sig/rigor/plugin/base.rbs +30 -7
  317. data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
  318. data/sig/rigor/plugin/effect_attribution.rbs +66 -0
  319. data/sig/rigor/plugin/effect_edge.rbs +33 -0
  320. data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
  321. data/sig/rigor/plugin/io_boundary.rbs +1 -1
  322. data/sig/rigor/plugin/loader.rbs +3 -3
  323. data/sig/rigor/plugin/manifest.rbs +50 -11
  324. data/sig/rigor/plugin/protocol_contract.rbs +68 -0
  325. data/sig/rigor/plugin/registry.rbs +62 -1
  326. data/sig/rigor/plugin.rbs +1 -1
  327. data/sig/rigor/rbs_extended.rbs +1 -1
  328. data/sig/rigor/reflection.rbs +7 -6
  329. data/sig/rigor/scope.rbs +135 -20
  330. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -7
  331. data/sig/rigor/source.rbs +4 -4
  332. data/sig/rigor/testing.rbs +10 -4
  333. data/sig/rigor/type.rbs +6 -0
  334. data/sig/rigor.rbs +38 -20
  335. data/skills/rigor-ask/SKILL.md +8 -5
  336. data/skills/rigor-baseline-reduce/SKILL.md +4 -6
  337. data/skills/rigor-ci-setup/SKILL.md +17 -21
  338. data/skills/rigor-doctor/SKILL.md +24 -22
  339. data/skills/rigor-doctor/references/01-checks.md +97 -33
  340. data/skills/rigor-editor-setup/SKILL.md +6 -4
  341. data/skills/rigor-mcp-setup/SKILL.md +5 -4
  342. data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
  343. data/skills/rigor-next-steps/SKILL.md +4 -2
  344. data/skills/rigor-plugin-author/SKILL.md +19 -23
  345. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
  346. data/skills/rigor-plugin-author/references/02-walker-and-types.md +12 -25
  347. data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
  348. data/skills/rigor-plugin-review/SKILL.md +6 -4
  349. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
  350. data/skills/rigor-plugin-tune/SKILL.md +4 -2
  351. data/skills/rigor-project-init/SKILL.md +9 -7
  352. data/skills/rigor-project-init/references/01-detect.md +13 -9
  353. data/skills/rigor-project-init/references/02-configure.md +33 -8
  354. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
  355. data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
  356. data/skills/rigor-protection-uplift/SKILL.md +4 -6
  357. data/skills/rigor-rbs-setup/SKILL.md +4 -2
  358. data/skills/rigor-type-oracle/SKILL.md +4 -6
  359. data/skills/rigor-type-oracle/references/01-oracle-commands.md +12 -8
  360. data/skills/rigor-type-oracle/references/03-gap-protocol.md +0 -3
  361. data/skills/rigor-unused-adjudicate/SKILL.md +4 -2
  362. data/skills/rigor-upgrade/SKILL.md +13 -8
  363. metadata +108 -1
@@ -0,0 +1,153 @@
1
+ # rigor-active-model-serializers
2
+
3
+ Teaches Rigor what `object` is inside an ActiveModel::Serializer
4
+ subclass, so the reads below it — `object.username`,
5
+ `object.account.display_name` — resolve against your model instead of
6
+ dispatching on an unknown receiver. It reads source only; no
7
+ ActiveModelSerializers runtime dependency.
8
+
9
+ It ships bundled in `rigortype`. Activate it under `plugins:`, alongside
10
+ `rigor-activerecord`, which is what tells this plugin which models exist
11
+ and what they answer:
12
+
13
+ ```yaml
14
+ plugins:
15
+ - rigor-activerecord
16
+ - rigor-active-model-serializers
17
+ ```
18
+
19
+ ## What it does — no diagnostics
20
+
21
+ The plugin emits nothing of its own. It contributes one return type and
22
+ the gem's framework constants, so that
23
+
24
+ ```ruby
25
+ class REST::AccountSerializer < ActiveModel::Serializer
26
+ attributes :id, :username, :display_name
27
+
28
+ def display_name
29
+ object.display_name
30
+ end
31
+ end
32
+ ```
33
+
34
+ types `object` as `Account`, and `object.display_name` as the
35
+ `accounts.display_name` column's type. What you notice is the diagnostics
36
+ that now CAN fire — a typo below a serializer's `object` used to be
37
+ invisible:
38
+
39
+ ```text
40
+ app/serializers/errors_serializer.rb:11:23: error: undefined method `nickname' for String [call.undefined-method]
41
+ ```
42
+
43
+ and `rigor coverage` reporting a larger share of `app/serializers` as
44
+ precise. On Mastodon that share went from 51.1 % to 53.2 % with no change
45
+ at all to the diagnostic set
46
+ ([the measurement](../../notes/20260917-ams-object-recognizer.md)).
47
+
48
+ ## When `object` is typed, and when it is not
49
+
50
+ Nothing in a serializer's source states which class it serializes — AMS
51
+ binds the resource when the serializer is constructed. So the type is
52
+ always DERIVED, and two independent things have to agree before the
53
+ plugin contributes one:
54
+
55
+ 1. **The name resolves.** `<Model>Serializer` names exactly one model
56
+ your project has — `REST::AccountSerializer` → `Account`.
57
+ 2. **The model answers the serializer.** Every name the serializer will
58
+ read off its resource — the `attributes` / `attribute` / `has_many` /
59
+ `has_one` / `belongs_to` declarations it does not define itself, plus
60
+ every `object.<name>` in its body — is a column, per-column predicate,
61
+ association, enum, alias, scope or macro-defined method of that model,
62
+ or a method your project defines on it or an ancestor.
63
+
64
+ One unanswered name declines the whole serializer, because the resource
65
+ is then something else — commonly a presenter or a decorator around the
66
+ model, which shares most of its surface and is given away by the one or
67
+ two names that differ. On Mastodon that check is what stops
68
+ `REST::ConversationSerializer` typing as `Conversation` when its resource
69
+ is an `AccountConversation`.
70
+
71
+ | your code | `object` types as |
72
+ | --- | --- |
73
+ | `REST::AccountSerializer`, `Account` exists and answers it | `Account` |
74
+ | `Admin::AccountSerializer`, where `Admin::Account` and `Account` both exist | unchanged — the reading is ambiguous and nothing here ranks one above the other |
75
+ | `ConversationSerializer` whose declarations `Conversation` cannot answer | unchanged (`Dynamic`) |
76
+ | a serializer that reads nothing off its resource | unchanged — there is nothing to check the name against |
77
+ | `ContextSerializer`, and no `Context` **model** exists | unchanged (`Dynamic`) |
78
+ | a serializer you listed in `model_overrides` | the model you named |
79
+ | `object` written outside a serializer, or over your own `def object` | unchanged (`Dynamic`) |
80
+
81
+ A serializer is a class under `serializer_search_paths` whose superclass
82
+ chain reaches `ActiveModel::Serializer` — including through your own base
83
+ serializer (`class ApplicationSerializer < ActiveModel::Serializer`, then
84
+ `class AccountSerializer < ApplicationSerializer`). A class the chain does
85
+ not reach is not a serializer here whatever it is called: `object` is an
86
+ ordinary method name, and a `Json::ConversationSerializer` of your own is
87
+ entitled to define it.
88
+
89
+ `class << self` and `def self.` bodies are left alone. An `object` read
90
+ there is a `NoMethodError` at run time — AMS's reader is an instance
91
+ method — so there is nothing to type and nothing the plugin should say
92
+ about it.
93
+
94
+ **The plugin never guesses.** Where the resource cannot be established it
95
+ contributes nothing and `object` keeps the answer it had, because a wrong
96
+ class here would turn every read on it into a false
97
+ `call.undefined-method` on working code.
98
+
99
+ It also does nothing at all without `rigor-activerecord`, or on a project
100
+ with no `db/schema.rb` / `db/structure.sql`: the model set it resolves and
101
+ checks against comes from that plugin, and is withheld when the schema is
102
+ missing.
103
+
104
+ ## Configuration
105
+
106
+ ```yaml
107
+ plugins:
108
+ - gem: rigor-active-model-serializers
109
+ config:
110
+ serializer_search_paths: ["app/serializers", "app/lib"]
111
+ serializer_base_classes: ["ActiveModel::Serializer"]
112
+ model_overrides:
113
+ REST::InstanceSerializer: InstancePresenter
114
+ ```
115
+
116
+ | Key | Default | Meaning |
117
+ | --- | --- | --- |
118
+ | `serializer_search_paths` | `["app/serializers", "app/lib"]` | Where to look for serializer classes. `app/lib` is in the default set because a base serializer routinely lives outside `app/serializers` — Mastodon's `ActivityPub::Serializer`, the parent of 60 of its serializers, is one. A directory you do not have costs one probe. |
119
+ | `serializer_base_classes` | `["ActiveModel::Serializer"]` | The roots the ancestry walk starts from. |
120
+ | `model_overrides` | `{}` | Serializer class name → resource class name, for a serializer the derivation cannot reach — a presenter, a decorator, or a class named for a JSON shape. Your assertion; it is not re-checked. |
121
+
122
+ ## Limitations
123
+
124
+ - **SimpleForm inputs.** `SimpleForm::Inputs::Base#object` shares the name
125
+ and nothing else — its resource comes from the `simple_form_for` call
126
+ site, not from the input class.
127
+ - **`serializer:` / `each_serializer:` options.** They name the
128
+ serializer for an association, never the model for a serializer.
129
+ - **Model surface your schema and source do not state.** A method the
130
+ model gets from `method_missing`, from a gem's macro `rigor-activerecord`
131
+ does not recognise, or from a module included at runtime is invisible to
132
+ the model index, so a serializer reading one is declined.
133
+ `rigor-activerecord` reads `delegate`, the associations a concern
134
+ declares in its `included do`, the Paperclip / Active Storage attachment
135
+ macros and `enum` value predicates
136
+ ([#1049](https://github.com/rigortype/rigor/issues/1049)); anything else
137
+ a macro defines is not folded yet.
138
+ - **Some ways of reading the resource.** `object[:key]`, `object.title =`,
139
+ `object.try(:name)`, `object.present?` and an `attribute(:x) { ... }`
140
+ block are not read as evidence, so a serializer using one may decline
141
+ even where the model is right. Always the safe direction — a decline,
142
+ never a wrong type.
143
+ - **Serializers for plain objects** — `ActiveModelSerializers::Model`
144
+ subclasses, Structs, presenters. Use `model_overrides` where the class
145
+ you want is a real constant you are happy to have checked.
146
+
147
+ ## Plugin internals
148
+
149
+ The serializer discoverer / index, the ancestry closure and the contract
150
+ surfaces this plugin exercises are in the
151
+ [plugin's README](../../../plugins/rigor-active-model-serializers/README.md).
152
+ To write a plugin, see [`examples/`](../../../examples/README.md) and the
153
+ [`rigor-plugin-author`](../08-skills.md) skill.
@@ -95,6 +95,13 @@ nothing loads Solid Queue.
95
95
  evidence. A schedule loaded from Ruby rather than from a file under
96
96
  `recurring_paths` supplies nothing, by the same "read only what is
97
97
  written" rule.
98
+ - **The "failed to discover jobs" warning is run-scoped.** It is
99
+ a fact about your configuration, not about any one source file, so
100
+ it is reported once per run on `.rigor.yml` rather than repeated on
101
+ every analysed file. It is `:warning`, so if you baselined it at its
102
+ old position that entry no longer matches and the row fails a
103
+ `--fail-on=warning` run — regenerate with `rigor baseline
104
+ regenerate`.
98
105
 
99
106
  ## Plugin internals
100
107
 
@@ -29,13 +29,21 @@ errors_demo.rb:24:1: error: `User.find` expects at least 1 argument, got 0 [plug
29
29
  | --- | --- | --- |
30
30
  | Recognised `Model.find` / `Model.find_by` / `Model.where` call | `:info` | `plugin.activerecord.model-call` |
31
31
  | `Model.find_by(unknown: ...)` / `Model.where(unknown: ...)` | `:error` | `plugin.activerecord.unknown-column` |
32
- | `Model.find` with 0 args | `:error` | `plugin.activerecord.wrong-arity` |
32
+ | `Model.find` with 0 args and no block (unless the model defines `self.find`) | `:error` | `plugin.activerecord.wrong-arity` |
33
33
  | No schema source (`db/schema.rb` or `db/structure.sql`) present — reduced mode | `:info` | `plugin.activerecord.load-error` |
34
34
  | A schema source that exists but cannot be read or parsed | `:warning` | `plugin.activerecord.load-error` |
35
35
 
36
36
  Did-you-mean suggestions use `DidYouMean` fuzzy matching against
37
37
  the resolved table's column names.
38
38
 
39
+ A model that defines its own `self.find` owns that method's arity.
40
+ The plugin reports no `wrong-arity` for it, and no note for the
41
+ several-id or block form, where the call types as the model's
42
+ method rather than Rails' `find`. A single id is still noted as the
43
+ model, which is how it types. In an editor, a `self.find` in the
44
+ same file is not yet seen here, so the note and the error still
45
+ appear there ([#1329](https://github.com/rigortype/rigor/issues/1329)).
46
+
39
47
  ## Configuration
40
48
 
41
49
  ```yaml
@@ -61,8 +69,17 @@ All keys are optional. Tweak them when:
61
69
  ## What it infers
62
70
 
63
71
  The plugin contributes call-site types as well as diagnostics.
64
- Class-side: `User.find(1)` → `User`, `User.find_by(...)` →
65
- `User | nil`, `User.find_by!(...)` → non-nullable `User`.
72
+ Class-side: `User.find(1)` → `User`, `User.find(1, 2)` →
73
+ `Array[User]`, `User.find_by(...)` → `User | nil`,
74
+ `User.find_by!(...)` → non-nullable `User`. A single argument
75
+ stays `User` even when it is an Array (`User.find([1, 2])`), so
76
+ that an untyped one, such as `params[:id]`, keeps the model type.
77
+ A relation or association (`user.posts.find(1, 2)`) answers the
78
+ same way. With a block, `find` is `Enumerable#find` over the
79
+ records, on the class and on a relation alike:
80
+ `User.find { |u| u.admin? }` → `User | nil`, and it takes no id.
81
+ The block's parameter is the model on a relation; on the class
82
+ side it stays untyped.
66
83
  Instance-side: a column read (`user.name`) narrows to the
67
84
  column's value type, `user.admin?` to `bool`, and a singular
68
85
  association (`post.user`) to the target model.
@@ -76,6 +93,71 @@ Chained query methods keep the element type, and iteration
76
93
  scope invoked on a typed relation (`User.where(...).published`)
77
94
  never surfaces a false `call.undefined-method`.
78
95
 
96
+ A `scope` declared inside a concern's `included do ... end` block
97
+ counts as the including model's own. `Account.without_suspended`
98
+ types as `ActiveRecord::Relation[Account]` when `Account` includes
99
+ the concern that declares the scope — directly, or through another
100
+ concern that concern includes. A concern included once in your
101
+ base class (`ApplicationRecord`) reaches every model under it.
102
+ Attribution follows the `include` you wrote, not the name: a
103
+ model that includes nothing gets nothing, whatever some other
104
+ concern in the project declares.
105
+
106
+ The gate is the `included do ... end` block itself. A scope
107
+ declared some other way — in a `class_methods do` block, a
108
+ hand-written `def self.included(base)` with `base.class_eval`, or
109
+ directly in the body of your base class rather than in a concern —
110
+ is not folded, and the call stays as untyped as it was.
111
+
112
+ `belongs_to` / `has_one` / `has_many` /
113
+ `has_and_belongs_to_many` declared in that same `included do ...
114
+ end` block count as the including model's own too, with the same
115
+ `include`-edge attribution: `account.user` narrows to
116
+ `User | nil` when `Account` includes the concern that declares
117
+ `has_one :user`, and `where(user: ...)` stops reporting a false
118
+ `unknown-column`. A model that declares the association itself
119
+ keeps its own version.
120
+
121
+ ### The names a macro defines
122
+
123
+ Three macro families define ordinary instance methods that are
124
+ neither columns nor associations, and the model's recorded
125
+ surface now carries their names:
126
+
127
+ - `delegate :followers_count, to: :account_stat` — every
128
+ delegated name, including the `prefix:` spellings
129
+ (`delegate :can?, to: :user, prefix: true` defines
130
+ `user_can?`). `delegate` is read from the model body, from a
131
+ concern's `included do ... end`, and from a concern's own
132
+ top level, where it defines an instance method the including
133
+ model inherits.
134
+ - Attachment macros — Paperclip's `has_attached_file :avatar`
135
+ (`avatar`, `avatar=`, `avatar?`) and Active Storage's
136
+ `has_one_attached` / `has_many_attached` (`banner`,
137
+ `banner_attachment`, `banner_blob`; `docs`,
138
+ `docs_attachments`, `docs_blobs`). Paperclip's
139
+ `avatar_file_name` / `avatar_content_type` / `avatar_file_size`
140
+ / `avatar_updated_at` are real columns, so they come from your
141
+ schema rather than from the macro.
142
+ - `enum` value predicates — `enum :visibility, { limited: 4 },
143
+ suffix: :visibility` defines `limited_visibility?`, and
144
+ `prefix:` / `_prefix:` / `_suffix:` are read the same way. An
145
+ `enum` declared `instance_methods: false` defines none of
146
+ them, and none are recorded.
147
+
148
+ These are recorded as NAMES. Nothing here says what a delegated
149
+ method returns, and the plugin contributes no type for one — the
150
+ value is that a consumer asking "does this model answer
151
+ `followers_count`?" gets the right answer. The consumer this was
152
+ built for is
153
+ [`rigor-active-model-serializers`](rigor-active-model-serializers.md),
154
+ which types a serializer's `object` only when the candidate model
155
+ answers every name the serializer reads.
156
+
157
+ A declaration this plugin cannot read off the source contributes
158
+ nothing rather than a guess: a `prefix: true` whose `to:` is a
159
+ method call, a non-literal `prefix:`, a computed enum value list.
160
+
79
161
  If the project also installs `activerecord` through
80
162
  `rbs collection install`, the collection declares
81
163
  `ActiveRecord::Relation` without a type parameter while the
@@ -182,12 +264,25 @@ close every model in the project.
182
264
  the column-dependent half stands down — column readers stay
183
265
  untyped and `where(col:)` keys are not validated, exactly as they
184
266
  are for a table the schema does not describe. The plugin says so
185
- once per run at `:info`. Committing a schema dump (or pointing
267
+ once per run at `:info`, positioned on `.rigor.yml` — it is a
268
+ fact about your configuration, not about any one source file,
269
+ and you get the same single row with `--workers` as without. If
270
+ you baselined this row at its old position (a controller or a
271
+ model), that baseline entry no longer matches it — run `rigor
272
+ baseline regenerate`. Committing a schema dump (or pointing
186
273
  `schema_file` / `structure_sql_file` at one) turns the column half
187
274
  back on from the next cold run — a warm cache keeps serving the
188
275
  reduced index until it is invalidated, so use `rigor check
189
276
  --no-cache` (or `make cache-clean`) if you want to see the change
190
277
  immediately.
278
+ - **Every kind of relation shares one signature.**
279
+ `user.posts`, `user.posts.where(...)` and `Post.where(...)` all
280
+ type as `ActiveRecord::Relation[Post]`, although only the first
281
+ is an association's `CollectionProxy`, so the signature they share
282
+ accepts the widest argument list any of them takes.
283
+ `user.posts.delete_all(:nullify)` is valid and not reported. The
284
+ same call on the other two raises `ArgumentError` at run time, and
285
+ is not reported either.
191
286
  - **Column reads, not setters.** The plugin types instance-side
192
287
  column *reads* (`user.name`, `user.admin?`) and singular
193
288
  associations, but not the `name=` setter or the dirty-tracking
@@ -201,6 +296,17 @@ close every model in the project.
201
296
 
202
297
  ## Plugin internals
203
298
 
299
+ The plugin also answers `Plugin::Base#declared_members` (ADR-113
300
+ WD4): it enumerates each model's synthesized members — column
301
+ readers and `column?` predicates, association accessors, declared
302
+ scopes, enum attributes, and macro-defined names — off the prepared
303
+ model index, so `rigor lens` can list `User`'s members without a
304
+ grep-able declaration. A row carries the member-level type the
305
+ plugin commits to: `Dynamic[top]` for column readers on purpose
306
+ (the bare, receiver-less read's answer — a written `user.name`
307
+ still narrows to the column's type), and a type only where the
308
+ plugin already answers one at `check`.
309
+
204
310
  Architecture (the cached schema-parser → model-index → analyzer
205
311
  chain), the source layout, how to run the demo, and the plugin
206
312
  contract surfaces this plugin exercises are documented in the
@@ -40,12 +40,19 @@ arguments decline — those are covered by ActiveStorage's own RBS.
40
40
  | Rule | Severity | When |
41
41
  | --- | --- | --- |
42
42
  | `plugin.activestorage.attachment-call` | info | a recognised `model.attachment_name` call surfaces; confirms the model → attachment mapping |
43
- | `plugin.activestorage.load-error` | warning | discovery failed (e.g. the model directory is inaccessible under the IoBoundary trust policy) |
43
+ | `plugin.activestorage.load-error` | warning | discovery failed (e.g. the model directory is inaccessible under the IoBoundary trust policy) — once per run, on `.rigor.yml` |
44
44
 
45
45
  No `:error` diagnostics in this slice — the value is the
46
46
  return-type contribution; an "unknown attachment name" rule is a
47
47
  future slice.
48
48
 
49
+ The `load-error` warning is run-scoped: it is a fact about your
50
+ configuration, not about any one source file, so it is reported once
51
+ per run on `.rigor.yml` rather than repeated on every analysed file.
52
+ It is `:warning`, so if you baselined it at its old position that
53
+ entry no longer matches and the row fails a `--fail-on=warning` run —
54
+ regenerate with `rigor baseline regenerate`.
55
+
49
56
  ## Configuration
50
57
 
51
58
  ```yaml
@@ -36,21 +36,27 @@ path, no vendoring, no `signature_paths:` wiring.
36
36
 
37
37
  Roughly the top ~40 selectors plus their close neighbours, across:
38
38
 
39
- - **Object (universal)** — `#blank?`, `#present?`, `#presence`, `#try`,
40
- `#try!`, `#acts_like?` (+ `NilClass` / `TrueClass` / `FalseClass`).
39
+ - **Object (universal)** — `#blank?`, `#present?`, `#presence`,
40
+ `#presence_in`, `#try`, `#try!`, `#acts_like?`, `#deep_dup`, `#with`,
41
+ `#with_options`, `#html_safe?`, `Kernel#class_eval` (+ `NilClass` /
42
+ `TrueClass` / `FalseClass`).
41
43
  - **Integer / Float** — Duration multipliers (`#days`, `#hours`,
42
44
  `#minutes`, …) and Bytes multipliers (`#megabytes`, `#gigabytes`, …).
43
45
  - **String** — inflections (`#underscore`, `#camelize`, `#classify`,
44
46
  `#constantize`, `#pluralize`, …), filters (`#squish`, `#truncate`),
45
- `#html_safe`, `#starts_with?` / `#ends_with?`, conversions.
47
+ `#html_safe`, `#starts_with?` / `#ends_with?` (also on **Symbol**),
48
+ conversions and `#in_time_zone`.
46
49
  - **Time / Date / DateTime** — `.current`, `.zone`, `#yesterday`,
47
50
  `#tomorrow`, `#beginning_of_*` / `#end_of_*`, `#ago`, `#since`. `Time`
48
51
  additionally carries its **whole** Rails instance surface (see below);
49
52
  `Date` and `DateTime` carry the same subset they always did.
50
53
  - **Array** — `.wrap`, `#to_sentence`, `#in_groups_of`, `#second` …
51
54
  `#fifth`, `#compact_blank`, `#exclude?`.
52
- - **Hash** — `#symbolize_keys` / `#stringify_keys` (+ deep / bang),
53
- `#deep_merge`, `#with_indifferent_access`, `#except!`.
55
+ - **Hash** — `#symbolize_keys` / `#stringify_keys` (+ deep / bang) and
56
+ their `#to_options` alias, `#deep_merge`, `#reverse_merge` /
57
+ `#with_defaults` (+ bang), `#with_indifferent_access`, `#except!`,
58
+ `#extract!`.
59
+ - **Range** — `#overlaps?`, `#to_fs` / `#to_formatted_s`.
54
60
  - **Enumerable** — `#index_by`, `#index_with`, `#pluck`, `#exclude?`.
55
61
 
56
62
  ```ruby
@@ -0,0 +1,106 @@
1
+ # rigor-grape
2
+
3
+ Types the [Grape](https://github.com/ruby-grape/grape)
4
+ endpoint-declaration DSL so calls inside `class API < Grape::API`
5
+ bodies — and inside `Grape::Entity` subclasses from
6
+ [grape-entity](https://github.com/ruby-grape/grape-entity) — resolve to
7
+ real carriers instead of `Dynamic[top]`. It reads source only, with no
8
+ `grape` runtime dependency.
9
+
10
+ It ships bundled in `rigortype`. Activate it under `plugins:` (or let
11
+ bundler auto-detection pick it up when `grape` or `grape-entity` is in
12
+ `Gemfile.lock`):
13
+
14
+ ```yaml
15
+ plugins:
16
+ - rigor-grape
17
+ ```
18
+
19
+ ## What it types
20
+
21
+ ```ruby
22
+ class ThingsAPI < Grape::API
23
+ version "v1", using: :path
24
+ format :json
25
+
26
+ desc "List things" # Object?
27
+ route_setting :swagger, tags: %w[things] # Object?
28
+
29
+ params do # → Grape::Validations::ParamsScope
30
+ requires :id, type: Integer # Object?
31
+ optional :q, type: String # Object?
32
+ requires :filter, type: Hash do # nested scope re-enters ParamsScope
33
+ requires :state, type: String
34
+ end
35
+ mutually_exclusive :q, :filter
36
+ end
37
+
38
+ namespace :things do # self: the API::Instance class object
39
+ get "/:id" do # self: Grape::Endpoint
40
+ params # Hash[untyped, untyped]
41
+ error!("nope", 404) # bot
42
+ present Thing.first, with: Entities::Thing
43
+ end
44
+ end
45
+ end
46
+
47
+ class Entities::Thing < Grape::Entity
48
+ expose :id # Array[untyped]
49
+ expose :name do # `self` stays the class object
50
+ expose :first
51
+ end
52
+ format_with(:iso) { |d| d.to_s } # Object
53
+ end
54
+ ```
55
+
56
+ Recognised surfaces:
57
+
58
+ - **Class-body declarations** on `Grape::API` subclasses (including
59
+ through intermediate source base classes): `params`, `namespace` /
60
+ `group` / `resource` / `resources` / `segment` / `route_param` /
61
+ `version` / `given` / `mounted`, `get`/`put`/`post`/`delete`/`head`/
62
+ `patch`/`options`/`route`, `desc`, `route_setting`, `helpers`, `use`,
63
+ `mount`, `rescue_from`, the format/error-format setters, callbacks
64
+ (`before`/`after`/`before_validation`/`after_validation`), `prefix`,
65
+ `scope`, `contract`, and friends.
66
+ - **`params` block bodies** bind `self` to
67
+ `Grape::Validations::ParamsScope`, so `requires`, `optional`, `given`,
68
+ `with`, `use`, and the grouping macros (`mutually_exclusive`,
69
+ `exactly_one_of`, `at_least_one_of`, `all_or_none_of`) resolve —
70
+ including nested `requires :x, type: Hash do ... end` bodies.
71
+ - **Namespace-family bodies** (`namespace`, `route_param`, `version`,
72
+ `given`, `mounted`) bind `self` to the `Grape::API::Instance` class
73
+ object, matching Grape's `instance_eval`-on-class semantics, so nested
74
+ declarations resolve the same surface.
75
+ - **`desc 'x' do ... end` bodies** bind `self` to
76
+ `Grape::DSL::Desc::ConfigContext` — the closed `ROUTE_ATTRIBUTES`
77
+ setter surface — so `detail`, `success`, `failure`, `tags`,
78
+ `entity`, `hidden`, `is_array`, `consumes`, `headers`, `summary`,
79
+ `deprecated`, `named`, `nickname`, `produces`, `security`,
80
+ `http_codes`, `body_name`, `default`, `description`, and `params`
81
+ (the documentation setter, not `ParamsScope`) resolve.
82
+ - **Verb bodies** bind `self` to `Grape::Endpoint`, so `params`,
83
+ `headers`, `cookies`, `env`, `declared`, `present`, `error!`,
84
+ `status`, `redirect`, `body`, `content_type`, `route`,
85
+ `route_setting`, `stream`, `sendfile` resolve.
86
+ - **`Grape::Entity` class bodies**: `expose`, `unexpose`,
87
+ `with_options`, `documentation`, `format_with`, `root`,
88
+ `root_element`, `represent`, `present_collection`, `root_exposures`.
89
+ Nested `expose` bodies keep `self` on the class object, so they
90
+ resolve the same way.
91
+
92
+ ## Deferred
93
+
94
+ - `helpers do ... end` bodies (an anonymous module is `self` —
95
+ unnameable).
96
+ - Named `params :name` scopes inside `helpers` and `contract` schema
97
+ blocks.
98
+ - Value-level typing: `present`/`declared` results against the declared
99
+ entity or params, entity `represent` result typing.
100
+ - `Grape::Middleware` and custom-middleware DSL.
101
+
102
+ Calls on classes outside `Grape::API` / `Grape::Entity` ancestry keep
103
+ the `Dynamic` fallback, and DSL names the bundled signature does not
104
+ declare stay opaque rather than diagnosed — the declared classes are
105
+ registered as open receivers because Grape's runtime surface is
106
+ generated (`override_all_methods!`) and user base classes extend it.
@@ -58,11 +58,25 @@ name. `null:` extracts to `nullable:` (defaulting to `true`, mirroring
58
58
  graphql-ruby); `required:` defaults to `false`. The single-element
59
59
  `[String]` list form is recognised.
60
60
 
61
+ ## Typed DSL surface
62
+
63
+ Beyond the fact tables, the plugin bundles the graphql-ruby class-level
64
+ DSL signature, so calls inside a recognised subclass type as their real
65
+ carriers instead of staying opaque: `field` returns
66
+ `GraphQL::Schema::Field`, `argument` returns `GraphQL::Schema::Argument`,
67
+ `Enum.value` returns `GraphQL::Schema::EnumValue`, `description` /
68
+ `graphql_name` return their configured strings, and the `Schema`
69
+ registration macros (`query`, `mutation`, `use`, `orphan_types`, …)
70
+ return what they registered. A block parameter on `field`/`argument`
71
+ (`field(:comments) { |f| … }`) types as `GraphQL::Schema::Field` /
72
+ `GraphQL::Schema::Argument`.
73
+
61
74
  ## No diagnostics, no config
62
75
 
63
76
  The plugin emits no diagnostics and has no configuration knobs — it
64
- contributes the type tables above for other plugins to consume. It
65
- walks every `paths:` entry's `.rb` files for the schema-class shapes.
77
+ contributes the type tables and signature above for the engine and
78
+ other plugins to consume. It walks every `paths:` entry's `.rb` files
79
+ for the schema-class shapes.
66
80
 
67
81
  ## Limitations
68
82
 
@@ -72,6 +86,13 @@ walks every `paths:` entry's `.rb` files for the schema-class shapes.
72
86
  - **No `Schema.execute(...)` result typing.** Typing
73
87
  `Schema.execute(query).to_h` against the queried fields is a future
74
88
  plugin.
89
+ - **Interface DSL stays opaque.** `include GraphQL::Schema::Interface`
90
+ wires the DSL onto the includer through an include-time `extend`
91
+ hook RBS cannot express, so `field` inside an interface module is
92
+ not yet typed.
93
+ - **Zero-arity definition blocks keep an opaque `self`.**
94
+ `field :x do ... end` `instance_eval`s on the new `Field` at
95
+ runtime; only the explicit-parameter form (`{ |f| … }`) is typed.
75
96
  - **Constant-form types only.** The string form (`field :foo, "User"`)
76
97
  and the `<Type>.array` / `<Type>!` sugar chains are not recognised
77
98
  (the `[String]` bracket form is). Multi-element and empty list
@@ -33,7 +33,7 @@ authorize(Comment, :edit) # error: no policy class CommentPolicy (did you mean
33
33
  | `plugin.pundit.policy-call` | info | an `authorize` / `policy` / `policy_scope` call resolved to a discovered policy |
34
34
  | `plugin.pundit.unknown-policy-class` | error | the record maps to a `<Type>Policy` with no entry in the index (with a did-you-mean) |
35
35
  | `plugin.pundit.unknown-policy-method` | error | the policy exists but the `:action` has no `<action>?` predicate (lists known predicates + a did-you-mean) |
36
- | `plugin.pundit.load-error` | warning | policy discovery failed (parse/read error) — once per file |
36
+ | `plugin.pundit.load-error` | warning | policy discovery failed (parse/read error) — once per run, on `.rigor.yml` |
37
37
 
38
38
  The record maps to a policy by constant name or inferred
39
39
  `Nominal[T]` (`Post` → `PostPolicy`); `:update` normalises to
@@ -88,6 +88,13 @@ candidate rather than being guessed at.
88
88
  not validated when `local` has no inferred `Nominal[T]`.
89
89
  - **`Scope` policies** are validated for class existence, not for
90
90
  `Scope#resolve`.
91
+ - **The "failed to discover policies" warning is run-scoped.** It is
92
+ a fact about your configuration, not about any one source file, so
93
+ it is reported once per run on `.rigor.yml` rather than repeated on
94
+ every analysed file. It is `:warning`, so if you baselined it at its
95
+ old position that entry no longer matches and the row fails a
96
+ `--fail-on=warning` run — regenerate with `rigor baseline
97
+ regenerate`.
91
98
 
92
99
  ## Plugin internals
93
100
 
@@ -88,11 +88,16 @@ view templates containing lazy `t('.key')` calls.
88
88
  may come from controller instance variables not visible in the
89
89
  template source. Configure `view_search_paths:` to override the
90
90
  default `["app/views"]`.
91
- - **View diagnostics duplicate under `--workers`** — the view
92
- scan is a project-wide pass surfaced through the per-file
93
- diagnostic hook, so each fork-pool worker re-emits the full set
94
- (the same once-per-run limitation the `load-error` diagnostics
95
- carry). Default `rigor check` (sequential) is unaffected.
91
+ - **`load-error` rows are reported on `.rigor.yml`** — a locale
92
+ file that does not parse, or a locale or view scan that fails,
93
+ is reported once per run at `.rigor.yml:1:1`. They are
94
+ `:warning`, so if you baselined one at its old position on a
95
+ Ruby file, that entry no longer matches and the row fails a
96
+ `--fail-on=warning` run — regenerate with `rigor baseline
97
+ regenerate`. View diagnostics keep their view file position and
98
+ are reported once per run whatever `--workers` is set to
99
+ ([#1060](https://github.com/rigortype/rigor/issues/1060)); a
100
+ baseline entry recorded against a view still matches.
96
101
  - **Pluralization is recognised but not validated** — `count:` is
97
102
  treated as a reserved option; whether the locale defines
98
103
  `:zero` / `:one` / `:other` is not checked.
@@ -129,6 +129,13 @@ routed. Helper-name recognition for all three is unaffected.
129
129
  doesn't model) may not register, which can surface a false
130
130
  `unknown-helper`. Record those in a baseline, or
131
131
  `# rigor:disable` the line.
132
+ - **The "routes file did not load" warning is run-scoped.** It is a
133
+ fact about your configuration, not about any one source file, so
134
+ it is reported once per run on `.rigor.yml` rather than on the
135
+ first analysed file. It is `:warning`, so if you baselined it at
136
+ its old position that entry no longer matches and the row fails a
137
+ `--fail-on=warning` run — regenerate with `rigor baseline
138
+ regenerate`.
132
139
  - **Project-custom inflections** declared in
133
140
  `config/initializers/inflections.rb` are not yet fully ingested
134
141
  (ADR-39 slice 3); the standard ActiveSupport inflections are