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,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rbs"
4
+
5
+ require_relative "../configuration"
6
+ require_relative "../rbs_extended"
7
+
8
+ module Rigor
9
+ module SigGen
10
+ # The methods the inline reader declares for each analysed file (ADR-112 WD4): what `# @rbs` / `#:` in a
11
+ # `.rb` file says, member by member, as `rigor sig-gen` needs it to copy that declaration into `sig/`.
12
+ #
13
+ # Read from the RBS the `rigor-rbs-inline` synthesizer contributed (`RbsLoader#virtual_rbs`), not from the
14
+ # built environment. The environment is the wrong witness twice over: once `sig/` declares a member, the
15
+ # loader stands one of the two down before the build (ADR-112 WD5), so the declaration a stale `sig/` copy
16
+ # should be compared against may no longer be there to read; and a member's origin is only recoverable off
17
+ # its location, which the ADR-54 environment cache does not keep. The synthesized text is the loader's input,
18
+ # identical on every run.
19
+ #
20
+ # A member the synthesizer marks `rigor:v1:inferred-signature` is in the text only because rbs-inline
21
+ # declares every `def` of a file it reads: the author annotated nothing on it, so it is present here
22
+ # ({#lookup} finds it, which is what `sig_gen.inline_declared: skip` needs) but not {Member#declared?}.
23
+ class InlineDeclarations
24
+ # One inline-declared method.
25
+ #
26
+ # - `method_types` — the declared overloads (`RBS::MethodType`, names as written, unresolved).
27
+ # - `annotations` — the annotation strings the author wrote on the member (`deprecated` for
28
+ # `%a{deprecated}`), without the synthesizer's own `rigor:v1:inferred-*` markers.
29
+ # - `return_inferred` — the author declared the parameters and not the return: rbs-inline defaulted it,
30
+ # and the synthesizer marked it `rigor:v1:inferred-return`.
31
+ # - `signature_inferred` — every type slot defaulted; the author said nothing about this member.
32
+ # - `attr_line` — for an attribute, the member in RBS's own `attr_*` spelling (`attr_reader name: String`),
33
+ # which is what replaces an `attr_*` declaration in `sig/` under `--overwrite`; nil for a `def`.
34
+ Member = Data.define(:method_types, :annotations, :return_inferred, :signature_inferred, :attr_line) do
35
+ def declared?
36
+ !signature_inferred
37
+ end
38
+
39
+ # The member's annotations as RBS lines, each in a delimiter its content does not close.
40
+ def annotation_lines
41
+ annotations.filter_map { |string| InlineDeclarations.annotation_line(string) }
42
+ end
43
+ end
44
+
45
+ # The synthesizer channel this index reads; another plugin's virtual RBS is not an inline declaration.
46
+ SYNTHESIZER_ID = Configuration::AUTOWIRED_RBS_INLINE_ID
47
+ private_constant :SYNTHESIZER_ID
48
+
49
+ MARKERS = [RbsExtended::INFERRED_RETURN_DIRECTIVE, RbsExtended::INFERRED_SIGNATURE_DIRECTIVE].freeze
50
+ private_constant :MARKERS
51
+
52
+ # rbs lexes `%a` with each of these pairs, up to the first matching closer and with no escape
53
+ # (`rbs/src/lexer.re`), so an annotation is spelled in the first pair its content does not close.
54
+ ANNOTATION_DELIMITERS = Ractor.make_shareable([%w[{ }], %w[( )], %w[[ ]], %w[< >], %w[| |]])
55
+ private_constant :ANNOTATION_DELIMITERS
56
+
57
+ def self.annotation_line(string)
58
+ pair = ANNOTATION_DELIMITERS.find { |_open, close| !string.include?(close) }
59
+ pair && "%a#{pair[0]}#{string}#{pair[1]}"
60
+ end
61
+
62
+ # Fail-soft: an environment with no loader, or a loader this index cannot read, declares nothing inline,
63
+ # which is the answer for a project without the plugin.
64
+ def self.build(environment)
65
+ loader = environment&.rbs_loader
66
+ new(loader.respond_to?(:virtual_rbs) ? loader.virtual_rbs : [])
67
+ rescue StandardError
68
+ new([])
69
+ end
70
+
71
+ def initialize(virtual_rbs)
72
+ @by_path = {}
73
+ @generic_classes = {}
74
+ virtual_rbs.each do |name, content|
75
+ prefix, plugin_id, path = name.to_s.split(":", 3)
76
+ next unless prefix == "virtual" && plugin_id == SYNTHESIZER_ID && path
77
+
78
+ decls = parse(content.to_s)
79
+ next if decls.nil?
80
+
81
+ table = (@by_path[path] ||= {})
82
+ each_member(decls, []) { |class_name, member| record(table, class_name, member) }
83
+ end
84
+ @by_path.freeze
85
+ @generic_classes.freeze
86
+ end
87
+
88
+ # `{ class name => [type parameter names] }` for the classes and modules an inline declaration gives type
89
+ # parameters (`# @rbs generic T`). sig-gen does not write a class's type parameters, and a `sig/` header
90
+ # without them makes rbs reject the class (`GenericParameterMismatchError`), so the generator writes nothing
91
+ # that would open one of these, nor members into a `sig/` declaration that names the parameters otherwise.
92
+ attr_reader :generic_classes
93
+
94
+ # @return the inline declaration of `class_name`'s `method_name` on the `kind` side, declared in the file
95
+ # at `path` — or nil.
96
+ def lookup(path, class_name, method_name, kind)
97
+ @by_path.dig(path.to_s, [class_name, method_name, kind])
98
+ end
99
+
100
+ private
101
+
102
+ def parse(content)
103
+ return nil if content.empty? || !content.valid_encoding?
104
+
105
+ _buffer, _directives, decls = ::RBS::Parser.parse_signature(content)
106
+ decls
107
+ rescue ::RBS::BaseError
108
+ nil
109
+ end
110
+
111
+ def each_member(decls, prefix, &block)
112
+ decls.each do |decl|
113
+ next unless decl.is_a?(::RBS::AST::Declarations::Class) || decl.is_a?(::RBS::AST::Declarations::Module)
114
+
115
+ inner = prefix + [decl.name.to_s.delete_prefix("::")]
116
+ @generic_classes[inner.join("::")] ||= decl.type_params.map(&:name) unless decl.type_params.empty?
117
+ decl.members.each { |member| block.call(inner.join("::"), member) }
118
+ each_member(decl.members, inner, &block)
119
+ end
120
+ end
121
+
122
+ def record(table, class_name, member)
123
+ method_entries(member).each do |method_name, kind, method_types|
124
+ table[[class_name, method_name, kind]] ||= build_member(member, method_types)
125
+ end
126
+ end
127
+
128
+ # `[method_name, kind, method_types]` per method a member defines, mirroring what the RBS definition
129
+ # builder files it under: an attribute is its reader and / or writer, `def self?.x` is both sides. An
130
+ # `overloading?` member (`def x: ... | ...`) composes with another declaration rather than being one,
131
+ # and an alias declares no signature of its own, so neither is recorded.
132
+ def method_entries(member)
133
+ case member
134
+ when ::RBS::AST::Members::MethodDefinition then definition_entries(member)
135
+ when ::RBS::AST::Members::AttrReader then [reader_entry(member)]
136
+ when ::RBS::AST::Members::AttrWriter then [writer_entry(member)]
137
+ when ::RBS::AST::Members::AttrAccessor then [reader_entry(member), writer_entry(member)]
138
+ else []
139
+ end
140
+ end
141
+
142
+ def definition_entries(member)
143
+ return [] if member.overloading?
144
+
145
+ types = member.overloads.map(&:method_type)
146
+ kinds = member.kind == :singleton_instance ? %i[instance singleton] : [member.kind]
147
+ kinds.map { |kind| [member.name, kind, types] }
148
+ end
149
+
150
+ def reader_entry(member)
151
+ [member.name, member.kind, [::RBS::Parser.parse_method_type("() -> #{member.type}")]]
152
+ end
153
+
154
+ def writer_entry(member)
155
+ [:"#{member.name}=", member.kind, [::RBS::Parser.parse_method_type("(#{member.type}) -> #{member.type}")]]
156
+ end
157
+
158
+ ATTR_KEYWORDS = {
159
+ ::RBS::AST::Members::AttrReader => "attr_reader",
160
+ ::RBS::AST::Members::AttrWriter => "attr_writer",
161
+ ::RBS::AST::Members::AttrAccessor => "attr_accessor"
162
+ }.freeze
163
+ private_constant :ATTR_KEYWORDS
164
+
165
+ def attr_line(member)
166
+ keyword = ATTR_KEYWORDS[member.class]
167
+ return nil if keyword.nil?
168
+
169
+ "#{keyword} #{'self.' if member.kind == :singleton}#{member.name}: #{member.type}"
170
+ end
171
+
172
+ def build_member(member, method_types)
173
+ strings = member.annotations.map { |annotation| annotation.string.to_s.strip }
174
+ Member.new(
175
+ method_types: method_types.freeze,
176
+ annotations: (strings - MARKERS).freeze,
177
+ return_inferred: strings.include?(RbsExtended::INFERRED_RETURN_DIRECTIVE),
178
+ signature_inferred: strings.include?(RbsExtended::INFERRED_SIGNATURE_DIRECTIVE),
179
+ attr_line: attr_line(member)
180
+ )
181
+ end
182
+ end
183
+ end
184
+ end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "classification"
4
+ require_relative "effect_annotation"
5
+
3
6
  module Rigor
4
7
  module SigGen
5
8
  # Per-method record produced by the generator.
@@ -15,17 +18,28 @@ module Rigor
15
18
  # - `inferred_return` — `Rigor::Type` instance (or `nil` when the inference pass disqualified the def).
16
19
  # - `declared_return_rbs` — the existing RBS-declared return spelling, or `nil` when no RBS declares the
17
20
  # method.
21
+ # - `declared_rbs` — for an `inline_overwrite`, or a `tighter_return` over the class's own declaration, the
22
+ # whole declared line the proposal replaces, so `--diff` and `--check` can show both; `nil` otherwise.
23
+ # - `declared_annotations` — for a member declared inline, the annotations the author wrote on it
24
+ # (`%a{deprecated}`), rendered above `rbs` like `annotations`. Empty otherwise.
18
25
  # - `rbs` — the rendered RBS one-liner the generator would emit (`nil` for skipped / equivalent rows).
19
26
  # - `skip_reason` — one of {Classification::SKIP_DIAGNOSTIC_IDS} keys when classification is `:skipped`,
20
27
  # else `nil`.
28
+ # - `annotations` — RBS annotation lines to render ABOVE `rbs` (`%a{pure}` /
29
+ # `%a{rigor:v1:effect …}`, ADR-103 WD9). Empty unless the effects opt-in is on and the method's
30
+ # summary earned one; see {EffectAnnotation}.
31
+ # - `effect_reason` — one of {EffectAnnotation::DIAGNOSTIC_IDS} keys saying why `annotations` is what
32
+ # it is, or `nil` when the run had nothing to say about this method's effects.
21
33
  class MethodCandidate
22
34
  attr_reader :path, :class_name, :method_name, :kind, :classification,
23
35
  :inferred_return, :declared_return_rbs, :rbs, :skip_reason,
24
- :namespace_kinds, :class_shells, :class_superclasses
36
+ :namespace_kinds, :class_shells, :class_superclasses,
37
+ :annotations, :effect_reason, :declared_rbs, :declared_annotations
25
38
 
26
39
  def initialize(path:, class_name:, method_name:, kind:, classification:, # rubocop:disable Metrics/ParameterLists
27
40
  inferred_return: nil, declared_return_rbs: nil, rbs: nil, skip_reason: nil,
28
- namespace_kinds: {}, class_shells: [], class_superclasses: {})
41
+ namespace_kinds: {}, class_shells: [], class_superclasses: {},
42
+ annotations: [], effect_reason: nil, declared_rbs: nil, declared_annotations: [])
29
43
  @path = path
30
44
  @class_name = class_name
31
45
  @method_name = method_name
@@ -41,9 +55,32 @@ module Rigor
41
55
  # superclasses appear; computed ones are absent. The Writer emits `class Bar < Base` for the leaf when
42
56
  # present.
43
57
  @class_superclasses = class_superclasses.freeze
58
+ @annotations = annotations.freeze
59
+ @effect_reason = effect_reason
60
+ @declared_rbs = declared_rbs
61
+ @declared_annotations = declared_annotations.freeze
44
62
  freeze
45
63
  end
46
64
 
65
+ # Every line this candidate contributes to a declaration body, annotations first. The single place
66
+ # the renderer and the writer agree that an annotation precedes the `def` line it binds.
67
+ def rbs_lines
68
+ @declared_annotations + @annotations + [@rbs].compact
69
+ end
70
+
71
+ # A copy carrying a different annotation decision. The generator's effect pass rebuilds rather than
72
+ # mutates, because a candidate is frozen the moment it is built.
73
+ def with_effect_annotation(annotations, reason)
74
+ self.class.new(
75
+ path: @path, class_name: @class_name, method_name: @method_name, kind: @kind,
76
+ classification: @classification, inferred_return: @inferred_return,
77
+ declared_return_rbs: @declared_return_rbs, rbs: @rbs, skip_reason: @skip_reason,
78
+ namespace_kinds: @namespace_kinds, class_shells: @class_shells,
79
+ class_superclasses: @class_superclasses, annotations: annotations, effect_reason: reason,
80
+ declared_rbs: @declared_rbs, declared_annotations: @declared_annotations
81
+ )
82
+ end
83
+
47
84
  def to_h
48
85
  {
49
86
  file: path,
@@ -54,7 +91,14 @@ module Rigor
54
91
  rbs: rbs,
55
92
  inferred_return: inferred_return&.erase_to_rbs,
56
93
  declared_return_rbs: declared_return_rbs,
57
- skip_reason: skip_reason ? Classification::SKIP_DIAGNOSTIC_IDS.fetch(skip_reason) : nil
94
+ declared_rbs: declared_rbs,
95
+ declared_annotations: declared_annotations.empty? ? nil : declared_annotations,
96
+ skip_reason: skip_reason ? Classification::SKIP_DIAGNOSTIC_IDS.fetch(skip_reason) : nil,
97
+ # Named fields rather than a merge into `rbs`: a consumer routing on the annotation has to be
98
+ # able to find it without re-lexing the rendered line. Both are absent (not empty / null) when
99
+ # the run had no effect answer, so an effects-off payload is byte-identical to a pre-#391 one.
100
+ effect_annotations: annotations.empty? ? nil : annotations,
101
+ effect_reason: effect_reason ? EffectAnnotation::DIAGNOSTIC_IDS.fetch(effect_reason) : nil
58
102
  }.compact
59
103
  end
60
104
  end
@@ -15,7 +15,7 @@ module Rigor
15
15
  module SigGen
16
16
  # ADR-14 slice 3 — caller-side argument-type observation collector.
17
17
  #
18
- # Walks the user-supplied `--observe=PATH...` tree (default `spec/`), parses every `.rb` file with `Prism`,
18
+ # Walks the `--observe=PATH...` tree (default: the project's test roots), parses every `.rb` file with `Prism`,
19
19
  # scope- indexes it the same way the main generator does, and records the per-call-site argument-type tuples
20
20
  # for every `Prism::CallNode` whose receiver types as a `Type::Nominal`. The {Generator} consumes the
21
21
  # resulting map to render `--params=observed` RBS:
@@ -74,9 +74,13 @@ module Rigor
74
74
  when Classification::NEW_FILE then "[new-file]"
75
75
  when Classification::TIGHTER_RETURN
76
76
  "[tighter, was: #{candidate.declared_return_rbs}]"
77
+ when Classification::INLINE_OVERWRITE
78
+ "[inline-overwrite, was: #{candidate.declared_rbs}]"
77
79
  end
78
80
  @out.puts(" # #{tag}")
79
- @out.puts(" #{candidate.rbs}")
81
+ # Annotations first: an RBS annotation binds the declaration BELOW it, so `%a{pure}` printed
82
+ # after the `def` line would bind the next member — or nothing at all at the end of a class.
83
+ candidate.rbs_lines.each { |line| @out.puts(" #{line}") }
80
84
  end
81
85
  @out.puts("end")
82
86
  end
@@ -101,13 +105,22 @@ module Rigor
101
105
 
102
106
  candidates.each do |candidate|
103
107
  @out.puts("--- #{candidate.path}: #{candidate.class_name}##{candidate.method_name}")
104
- declared = candidate.declared_return_rbs
105
- @out.puts("- def #{candidate.method_name}: () -> #{declared}") if declared
106
- @out.puts("+ #{candidate.rbs}")
108
+ render_removed_line(candidate)
109
+ candidate.rbs_lines.each { |line| @out.puts("+ #{line}") }
107
110
  @out.puts
108
111
  end
109
112
  end
110
113
 
114
+ # A tighter return or an inline update replaces a whole `sig/` line, which it carries; any other row
115
+ # knows only the declared return.
116
+ def render_removed_line(candidate)
117
+ if candidate.declared_rbs
118
+ @out.puts("- #{candidate.declared_rbs}")
119
+ elsif candidate.declared_return_rbs
120
+ @out.puts("- def #{candidate.method_name}: () -> #{candidate.declared_return_rbs}")
121
+ end
122
+ end
123
+
111
124
  def render_json(candidates)
112
125
  payload = { candidates: candidates.map(&:to_h) }
113
126
  @out.puts(JSON.pretty_generate(payload))
@@ -117,16 +130,94 @@ module Rigor
117
130
 
118
131
  # Renders the per-source-file outcomes of a `--write` run. Distinct from {#render} because the write
119
132
  # path's reporting surface is action-oriented (created / updated / skipped) rather than candidate-oriented.
120
- def render_write(results:, format:)
133
+ # @param refused — methods refused as `sig.skipped.inline-differs`; the JSON payload names them under
134
+ # `refused` (absent when there are none, so an ordinary payload is unchanged), and text mode leaves them
135
+ # to the command's stderr `REFUSED` lines.
136
+ def render_write(results:, format:, refused: [])
121
137
  case format
122
- when "json" then render_write_json(results)
138
+ when "json" then render_write_json(results, refused)
123
139
  when "text" then render_write_text(results)
124
140
  else raise ArgumentError, "unsupported format: #{format}"
125
141
  end
126
142
  end
127
143
 
144
+ # ADR-112 WD4 — `sig-gen --check`: the results of a dry-run `--write`. Only the targets `--write` would
145
+ # change (or refuse) are shown, each with the lines it would add; the verdict is the exit status, which
146
+ # the command derives from the same results ({.out_of_date}).
147
+ #
148
+ # @param refused — the methods the generator refused to reconcile (`sig.skipped.inline-differs`):
149
+ # `sig/` is not up to date while one stands, and `--write` cannot fix it.
150
+ def render_check(results:, format:, refused: [])
151
+ stale = self.class.out_of_date(results)
152
+ case format
153
+ when "json"
154
+ @out.puts(JSON.pretty_generate({ up_to_date: stale.empty? && refused.empty?,
155
+ results: stale.map { |r| check_entry(r) },
156
+ refused: refused.map(&:to_h) }))
157
+ when "text" then render_check_text(stale, refused)
158
+ else raise ArgumentError, "unsupported format: #{format}"
159
+ end
160
+ end
161
+
162
+ # One line per method sig-gen refused to reconcile with its `sig/` copy. Shared by `--write` (on stderr,
163
+ # next to the write report) and `--check`.
164
+ def self.refusal_lines(refused)
165
+ refused.map do |candidate|
166
+ separator = candidate.kind == :singleton ? "." : "#"
167
+ "REFUSED #{candidate.path}: #{candidate.class_name}#{separator}#{candidate.method_name} — its inline " \
168
+ "declaration and its sig/ declaration disagree, so neither was changed " \
169
+ "(#{Classification::SKIP_DIAGNOSTIC_IDS.fetch(candidate.skip_reason)}). Make them agree, or pass " \
170
+ "--overwrite to replace the sig/ member with the inline declaration; `rigor explain " \
171
+ "#{Classification::SKIP_DIAGNOSTIC_IDS.fetch(candidate.skip_reason)}` says when that is not enough."
172
+ end
173
+ end
174
+
175
+ # The results that make a `--check` fail: a target `--write` would create or change, and one it would
176
+ # refuse, since a write that cannot happen is not an up-to-date `sig/` either.
177
+ def self.out_of_date(results)
178
+ results.reject { |result| %i[noop skipped_outside_sig_root].include?(result.action) }
179
+ end
180
+
181
+ # Nothing was written, so the entry must not read like one that was: `created` / `updated` become
182
+ # `would_create` / `would_update`. A refusal keeps its action — `--write` would refuse the same way.
183
+ CHECK_ACTIONS = { created: "would_create", updated: "would_update" }.freeze
184
+ private_constant :CHECK_ACTIONS
185
+
128
186
  private
129
187
 
188
+ def check_entry(result)
189
+ entry = result.to_h
190
+ entry[:action] = CHECK_ACTIONS.fetch(result.action, entry[:action])
191
+ entry
192
+ end
193
+
194
+ def render_check_text(stale, refused)
195
+ if stale.empty? && refused.empty?
196
+ @out.puts("sig/ is up to date")
197
+ return
198
+ end
199
+
200
+ self.class.refusal_lines(refused).each { |line| @out.puts(line) }
201
+
202
+ stale.each do |result|
203
+ case result.action
204
+ when :created, :updated then render_check_change(result)
205
+ when :skipped_invalid_rbs then render_write_invalid(result)
206
+ when :skipped_invalid_encoding then render_write_invalid_encoding(result)
207
+ end
208
+ end
209
+ end
210
+
211
+ def render_check_change(result)
212
+ counts = result.action == :created ? "#{result.applied.size} method(s)" : applied_counts(result)
213
+ verb = result.action == :created ? "would create" : "would update"
214
+ @out.puts("#{verb} #{result.target_path} (#{counts})")
215
+ result.applied.each do |candidate|
216
+ @out.puts(" - #{candidate.declared_rbs}") if candidate.declared_rbs
217
+ candidate.rbs_lines.each { |line| @out.puts(" + #{line}") }
218
+ end
219
+ end
220
+
130
221
  def render_write_text(results)
131
222
  if results.all? { |r| r.action == :noop }
132
223
  @out.puts("No changes")
@@ -144,13 +235,35 @@ module Rigor
144
235
  end
145
236
  end
146
237
 
238
+ # `+N` counts added lines; an existing line replaced (`--overwrite`, or an inline update) is counted apart,
239
+ # because "added 2" when one of them rewrote a line the project already had would understate the change.
240
+ def applied_counts(result)
241
+ added = "+#{result.applied.size - result.replaced.size}"
242
+ result.replaced.empty? ? added : "#{added}, replaced #{result.replaced.size}"
243
+ end
244
+
147
245
  def render_write_created(result)
148
246
  @out.puts("created #{result.target_path} (#{result.applied.size} method(s))")
149
247
  end
150
248
 
151
249
  def render_write_updated(result)
152
- @out.puts("updated #{result.target_path} (+#{result.applied.size}, " \
250
+ @out.puts("updated #{result.target_path} (#{applied_counts(result)}, " \
153
251
  "skipped #{result.skipped.size} user-authored)")
252
+ render_left_unreadable(result)
253
+ end
254
+
255
+ # ADR-103 WD9 — the declarations whose annotation region the writer refused to rewrite. Named per
256
+ # method rather than counted: the fix is a human reading one existing annotation and deciding what
257
+ # it should say, and there is no count of those a reader could act on.
258
+ def render_left_unreadable(result)
259
+ return if result.left_unreadable.empty?
260
+
261
+ @out.puts(" left #{result.left_unreadable.size} existing annotation(s) byte-untouched " \
262
+ "(sig.effect.left-unreadable):")
263
+ result.left_unreadable.each do |candidate|
264
+ @out.puts(" #{candidate.class_name}##{candidate.method_name} — " \
265
+ "would have emitted #{candidate.annotations.join(' ')}")
266
+ end
154
267
  end
155
268
 
156
269
  def render_write_skipped(result)
@@ -173,8 +286,10 @@ module Rigor
173
286
  @out.puts(" Re-save the file as UTF-8 and re-run; sig-gen never modifies a file it cannot read faithfully.")
174
287
  end
175
288
 
176
- def render_write_json(results)
177
- @out.puts(JSON.pretty_generate({ results: results.map(&:to_h) }))
289
+ def render_write_json(results, refused)
290
+ payload = { results: results.map(&:to_h) }
291
+ payload[:refused] = refused.map(&:to_h) unless refused.empty?
292
+ @out.puts(JSON.pretty_generate(payload))
178
293
  end
179
294
  end
180
295
  end
@@ -52,7 +52,7 @@ module Rigor
52
52
  summary: "An RBS declaration for this method already exists and was left alone.",
53
53
  explanation: "Not a gap. The project already states a type for this member, and sig-gen does not " \
54
54
  "overwrite a hand-authored contract without being told to. Where `sig/` and an inline " \
55
- "annotation both declare the member, `sig/` wins.",
55
+ "annotation both declare the member, ADR-112 WD5's consistency rule decides which binds.",
56
56
  next_step: "Read the existing declaration. If it disagrees with what Rigor infers, raise the " \
57
57
  "disagreement rather than silently retyping it; pass `--overwrite` only once you have " \
58
58
  "decided the generated type is the better contract."
@@ -84,6 +84,49 @@ module Rigor
84
84
  "defines it, or add the project file that declares it to the analysed paths — then " \
85
85
  "re-run `rigor sig-gen`."
86
86
  ),
87
+ "sig.skipped.inline-declared" => Entry.new(
88
+ id: "sig.skipped.inline-declared",
89
+ summary: "The member is declared inline, and `sig_gen.inline_declared: skip` keeps it out of `sig/`.",
90
+ explanation: "Not a gap. The project asked sig-gen to leave every method its inline `# @rbs` / `#:` " \
91
+ "reader declares out of `sig/`, the setting for a project whose Steep reads the same " \
92
+ "annotations (`inline: true` beside `signature \"sig\"`), where a `sig/` copy would " \
93
+ "declare the method twice (`DuplicatedMethodDefinition`). Rigor still reads the inline " \
94
+ "declaration when it analyses the source; a consumer reading only the shipped `sig/` " \
95
+ "does not see this method.",
96
+ next_step: "Nothing, if the setting is intended. To ship the member in `sig/`, remove " \
97
+ "`sig_gen.inline_declared: skip` from `.rigor.yml` and stop Steep from reading the " \
98
+ "annotations of the files you generate for."
99
+ ),
100
+ "sig.skipped.inline-generic-class" => Entry.new(
101
+ id: "sig.skipped.inline-generic-class",
102
+ summary: "The class is generic by an inline declaration, and sig-gen does not write type parameters.",
103
+ explanation: "The class (or one it is nested in) takes type parameters from `# @rbs generic`, and " \
104
+ "`sig/` either does not declare it yet or declares it with differently named parameters. " \
105
+ "sig-gen would have to open it with a header that has no type parameters, which rbs " \
106
+ "rejects (`GenericParameterMismatchError`) — the class, and every class whose signature " \
107
+ "mentions it, would fail its definition build and read `Dynamic[top]` — or write members " \
108
+ "whose `T` the `sig/` declaration does not bind. The inline declaration still binds when " \
109
+ "the source is analysed.",
110
+ next_step: "Declare the class in `sig/` with the same type parameters as the inline declaration " \
111
+ "(`class Box[T]` ... `end`) and re-run; sig-gen then writes the members into it."
112
+ ),
113
+ "sig.skipped.inline-differs" => Entry.new(
114
+ id: "sig.skipped.inline-differs",
115
+ summary: "The method's inline declaration and its `sig/` declaration disagree; neither was changed.",
116
+ explanation: "The method is declared inline by `# @rbs` / `#:` and in `sig/` (a `def` or an `attr_*`), " \
117
+ "and the two do not state the same types, or `sig/` lacks an annotation written inline. " \
118
+ "Parameter names, union spelling and a `::` that does not change what a name resolves to " \
119
+ "are ignored; overload order is not, because RBS answers a call with the first overload " \
120
+ "that matches. For a parameter-only annotation (`# @rbs name: T`, no `return:`) only the " \
121
+ "parameters are compared, and the return follows the ordinary proposal rules. sig-gen " \
122
+ "does not presume either side right, so it refuses, and `--write` and `--check` exit 1.",
123
+ next_step: "Decide which declaration is right. To keep the inline one, re-run with `--overwrite`, which " \
124
+ "replaces the whole `sig/` member with it (an overload written only in `sig/` is dropped); " \
125
+ "to keep the `sig/` one, edit the annotation to match, or delete it. A parameter-only " \
126
+ "annotation whose parameters differ stays refused under `--overwrite`, because its return " \
127
+ "would be inferred under the `sig/` parameters: delete the `sig/` member and re-run, or add " \
128
+ "`# @rbs return:` so nothing is inferred."
129
+ ),
87
130
  "sig.skipped.overridden-by-unsigned-subclass" => Entry.new(
88
131
  id: "sig.skipped.overridden-by-unsigned-subclass",
89
132
  summary: "A subclass overrides this method and its override was not itself emitted.",
@@ -13,23 +13,32 @@ module Rigor
13
13
  # - `action` — one of `:created` / `:updated` / `:noop` / `:skipped_outside_sig_root` /
14
14
  # `:skipped_invalid_rbs` / `:skipped_invalid_encoding`.
15
15
  # - `applied` — the {MethodCandidate}s that actually landed on disk.
16
+ # - `replaced` — the subset of `applied` that replaced an existing declaration rather than adding one
17
+ # (`--overwrite`'s tighter returns, and inline updates).
16
18
  # - `skipped` — the {MethodCandidate}s the writer declined (e.g. tighter-return without `--overwrite`). Each
17
19
  # entry pairs the candidate with a skip reason keyword (`:user_authored`).
20
+ # - `left_unreadable` — the {MethodCandidate}s whose effect annotation was NOT written because the
21
+ # target declaration already carries annotations, so its bytes were left alone
22
+ # (`sig.effect.left-unreadable`; see {Writer#splice_annotations}). The signature line itself was
23
+ # still updated.
18
24
  # - `error` — the refusal cause, when `action` is a refusal: for `:skipped_invalid_rbs` the file the writer
19
25
  # assembled does not parse, so it was NOT written (writing it would poison the project's sig tree — the
20
26
  # consumer quarantines an unparseable `.rbs`, taking every other type in that file down with it); for
21
27
  # `:skipped_invalid_encoding` the EXISTING target file is not valid UTF-8, so the writer refuses to merge
22
28
  # into content it cannot read faithfully.
23
29
  class WriteResult
24
- attr_reader :source_path, :target_path, :action, :applied, :skipped, :error
30
+ attr_reader :source_path, :target_path, :action, :applied, :skipped, :error, :left_unreadable, :replaced
25
31
 
26
- def initialize(source_path:, target_path:, action:, applied: [], skipped: [], error: nil)
32
+ def initialize(source_path:, target_path:, action:, applied: [], skipped: [], error: nil, # rubocop:disable Metrics/ParameterLists
33
+ left_unreadable: [], replaced: [])
27
34
  @source_path = source_path
28
35
  @target_path = target_path
29
36
  @action = action
30
37
  @applied = applied.freeze
31
38
  @skipped = skipped.freeze
32
39
  @error = error
40
+ @left_unreadable = left_unreadable.freeze
41
+ @replaced = replaced.freeze
33
42
  freeze
34
43
  end
35
44
 
@@ -40,7 +49,14 @@ module Rigor
40
49
  action: action.to_s,
41
50
  applied: applied.map(&:to_h),
42
51
  skipped: skipped.map { |c, reason| c.to_h.merge(write_skip_reason: reason.to_s) }
43
- }.tap { |h| h[:error] = error if error }
52
+ }.tap do |h|
53
+ h[:error] = error if error
54
+ # Absent when nothing was replaced, so a run that only adds lines keeps its pre-#1076 payload.
55
+ h[:replaced] = replaced.map(&:to_h) unless replaced.empty?
56
+ # Absent rather than empty when nothing was left: an effects-off payload stays byte-identical
57
+ # to a pre-#391 one.
58
+ h[:effect_left_unreadable] = left_unreadable.map(&:to_h) unless left_unreadable.empty?
59
+ end
44
60
  end
45
61
  end
46
62
  end