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
@@ -45,40 +45,97 @@ plugin id/version + config), so an unchanged second run skips the parse.
45
45
  | Rule | Severity | Fires when |
46
46
  | --- | --- | --- |
47
47
  | `plugin.rbs-inline.source-rbs-synthesis-failed` | info | rbs-inline could not parse a file; analysis falls back to no inline-RBS contribution and the diagnostic carries the upstream error |
48
- | `plugin.rbs-inline.source-rbs-annotation-not-honoured` | info | an annotation parsed successfully but contributed nothing — the file's other annotations still apply. Two causes: a member your `sig/` also declares (see [Precedence](#precedence)), and the `# @rbs module-self: Foo` spelling (see below) |
48
+ | `plugin.rbs-inline.source-rbs-annotation-not-honoured` | info | an annotation parsed successfully but contributed nothing — the file's other annotations still apply. Six causes: a member your `sig/` also declares where Rigor cannot tell which of the two is more precise (see [Precedence](#precedence)), the `# @rbs module-self: Foo` spelling (see below), a `#:` line whose type does not parse (see [Unparseable `#:` types](#unparseable--types)), a same-line `# @rbs %a{…}` whose method type does not parse (see [Same-line annotations](#same-line-annotations)), a `# @rbs name: T` parameter type that does not parse (see [Unparseable `# @rbs name:` types](#unparseable--rbs-name-types)), and an `@rbs`-prefixed tag the gem does not recognise (see [An unrecognised tag after `@rbs`](#an-unrecognised-tag-after-rbs)) |
49
+ | `rbs.contradicting-signature` | error | an inline declaration contradicts the `sig/` declaration of the same method, or an inline `%a{rigor:v1:…}` refinement is outside its own declared type (see [Precedence](#precedence)). A core rule rather than the plugin's own, so it carries no `plugin.rbs-inline.` prefix |
49
50
 
50
51
  ## Precedence
51
52
 
52
53
  When a method is declared **both** in `sig/` and by an inline
53
- annotation, **the `.rbs` wins, per member.** The inline signature for
54
- that one method is dropped; every other annotation in the file still
55
- binds, and the class keeps its method surface.
54
+ annotation, Rigor **compares the two**, position by position — each
55
+ parameter, the return type, a block's parameters and return — and
56
+ exactly one of them binds. Every other annotation in the file still
57
+ binds either way, and the class keeps its method surface.
56
58
 
57
59
  ```ruby
58
- # lib/demo.rb # sig/demo.rbs
59
- class Demo # class Demo
60
- # @rbs (Integer) -> String # def shared: (String) -> Integer ← this one wins
61
- def shared(v) = v.to_s # def only_sig: () -> String
62
- # end
63
- # @rbs (Integer) -> Integer
64
- def only_inline(v) = v + 1 # ← inline-only: still binds
60
+ # lib/demo.rb # sig/demo.rbs
61
+ class Demo # class Demo
62
+ # @rbs dir: :asc | :desc # def order: (Symbol dir) -> void
63
+ def order(dir) = nil # def shared: (String) -> Integer
64
+ # end
65
+ # @rbs (Integer) -> String
66
+ def shared(v) = v.to_s
65
67
  end
66
68
  ```
67
69
 
68
- Each dropped member is reported once as
69
- `plugin.rbs-inline.source-rbs-annotation-not-honoured`, naming the
70
- member and the `.rbs` that won. Delete one of the two declarations to
71
- make the inline annotation take effect.
72
-
73
- `sig/` wins because it is the reviewed artefact — the one you diff in
74
- review and the one `rigor sig-gen --diff` reasons about. There is no
75
- upstream rule to defer to: rbs merges an inline `.rb` declaration and a
76
- `.rbs` one into a single class entry and ranks neither, so Steep reports
77
- the same overlap as a signature error and the class still fails to
78
- build. Rigor keeps the reporting and drops the degradation
79
- ([ADR-32](../../adr/32-rbs-inline-comment-ingestion.md) WD13) — left to
80
- collide, one duplicated method costs the class every other method, and
81
- each call on it, real methods and typos alike, reads `Dynamic[top]`.
70
+ - **One refines the other: the more precise one binds, silently.**
71
+ `:asc | :desc` is a subtype of `Symbol`, so `Demo#order` takes the
72
+ inline contract and `order(:up)` is an argument-type error, where
73
+ the `.rbs` alone let it pass. The merge takes the narrower type in
74
+ every position, parameters included, on purpose: both declarations
75
+ are yours, and the narrower one is what you stated. `String` in
76
+ `sig/` beside `non-empty-string` inline reads as `non-empty-string`.
77
+ `untyped`, `void` and `top` are consistent with anything and say the
78
+ least, so `sig/`'s `-> untyped` beside an inline `-> void` is quiet.
79
+ Identical declarations — what `rigor sig-gen` writes for an annotated
80
+ method — are quiet too. The inline side binds only if the `.rbs`
81
+ member loses nothing by it: the same visibility, and every
82
+ annotation it carries (a predicate, an assertion, an effect
83
+ envelope) also on the inline one.
84
+ - **They contradict: an error, and the `.rbs` binds.** If `Demo#shared`
85
+ took a `::String` in `sig/` and an `::Integer` inline, no value would
86
+ be both, so the run would report
87
+ [`rbs.contradicting-signature`](../04-diagnostics.md#rule-rbs-contradicting-signature)
88
+ at the `sig/` line, naming the annotated file. Spelled `String` and
89
+ `Integer`, as in the example, the pair is undecided instead (below). Positional counts that
90
+ cannot meet, or a keyword one side requires and the other cannot take
91
+ in any form, contradict the same way. The error needs a proof, and
92
+ only Ruby core or stdlib classes written absolutely (`::String`,
93
+ `::Integer`) give one, read from their RBS hierarchy. A module such
94
+ as `Comparable` never counts, since any class may include it; nor
95
+ does a class of your own (your `sig/` may omit the superclass Ruby
96
+ gives it), a gem's class, a relative name such as plain `String`
97
+ (it may be your own `App::String`), an optional or rest parameter
98
+ (a call may leave it out), or a block's parameters (the body may
99
+ never yield). A stale generated signature is the usual cause:
100
+ regenerate it, or fix the annotation.
101
+ - **Rigor cannot tell: the `.rbs` binds, with an `:info`.** When a
102
+ position names a type alias, an interface, `self`, a type variable,
103
+ or a relative class name your project also declares, when Rigor
104
+ cannot prove two types disjoint (a subclass relation such as
105
+ `Integer` against `Numeric` included), when the parameter lists are shaped
106
+ differently but overlap, when the overloads do not pair one to one
107
+ (they are paired by what they declare, not by their order), or when
108
+ each side is more precise somewhere, the inline signature is dropped
109
+ and reported as
110
+ `plugin.rbs-inline.source-rbs-annotation-not-honoured`, naming the
111
+ member and the `.rbs` that bound. Make one a refinement of the
112
+ other, or delete one, to settle it.
113
+
114
+ `rigor sig-gen --write` produces this overlap on purpose: by default it
115
+ copies each inline declaration into `sig/`, so the generated signature
116
+ is the complete contract a gem ships. An identical copy is quiet, as
117
+ above. When an inline annotation later disagrees with its copy,
118
+ `rigor sig-gen --write` and `--check` refuse the method and exit `1`
119
+ until you make the two agree or pass `--overwrite`, which replaces the
120
+ `sig/` member with the inline declaration. A project whose Steep reads the same
121
+ annotations sets `sig_gen.inline_declared: skip` instead
122
+ ([handbook chapter 11](../../handbook/11-sig-gen.md#methods-declared-inline)).
123
+
124
+ A `%a{rigor:v1:return: …}` or `%a{rigor:v1:param: …}` refinement on an
125
+ inline annotation must also share values with its own declared type:
126
+ `# @rbs %a{rigor:v1:return: positive-int} () -> ::String` is reported as
127
+ `rbs.contradicting-signature` at the annotated file (the same proof rule
128
+ applies, so `-> String` without `::` is left alone).
129
+
130
+ There is no upstream rule to defer to: rbs merges an inline `.rb`
131
+ declaration and a `.rbs` one into a single class entry and ranks
132
+ neither, so Steep reports the same overlap as a signature error and
133
+ the class fails to build. Rigor keeps the reporting and drops the
134
+ degradation ([ADR-112](../../adr/112-extrbs-comment-channel.md) WD5,
135
+ which replaced [ADR-32](../../adr/32-rbs-inline-comment-ingestion.md)
136
+ WD13's "the `.rbs` always wins") — left to collide, one duplicated
137
+ method costs the class every other method, and each call on it, real
138
+ methods and typos alike, reads `Dynamic[top]`.
82
139
 
83
140
  Two overlaps this does **not** cover: a `.rbs` that collides with
84
141
  *bundled* RBS (Ruby core, stdlib, a gem's signatures) is quarantined
@@ -109,6 +166,136 @@ in silence. Constructs the gem supports and the built-in parser does not —
109
166
  `@rbs generic T`, `@rbs!` embedded RBS blocks, `@rbs inherits`, method
110
167
  visibility — all work here.
111
168
 
169
+ ## Same-line annotations
170
+
171
+ An `%a{…}` annotation can sit on the same line as a method type:
172
+
173
+ ```ruby
174
+ class Reader
175
+ # @rbs %a{rigor:v1:return: non-empty-string} () -> String
176
+ def title = "x"
177
+
178
+ #: %a{pure} () -> String
179
+ def label = "x"
180
+ end
181
+ ```
182
+
183
+ This is the spelling the built-in parser and Steep's inline mode accept. The
184
+ gem itself does not: it keeps the annotation and drops the method type in the
185
+ `@rbs` form, and drops the whole `#:` line. Rigor splits the line back into the
186
+ annotation and the method type before the gem's writer runs, so both apply, as
187
+ they do when the annotation has a line of its own
188
+ ([ADR-32](../../adr/32-rbs-inline-comment-ingestion.md) WD11). The gem's own
189
+ `rbs-inline --output` is unchanged and still drops them.
190
+
191
+ When the method type after the annotations does not parse, nothing is split:
192
+ the method types as if no signature had been written, and Rigor reports it — a
193
+ `#:` line under [Unparseable `#:` types](#unparseable--types), and an `@rbs`
194
+ line as `plugin.rbs-inline.source-rbs-annotation-not-honoured` naming the line
195
+ and the text it could not read.
196
+
197
+ ## Unparseable `#:` types
198
+
199
+ A `#:` line whose type does not parse as RBS is dropped — the signature never
200
+ applies, and the method types as if the line had never been written, not
201
+ merely as if its type were wrong:
202
+
203
+ ```ruby
204
+ class BadRefProbe
205
+ #: (finite-float) -> String
206
+ def show(f)
207
+ f.to_s
208
+ end
209
+ end
210
+ ```
211
+
212
+ `finite-float` is a [Rigor refinement](../16-rbs-extended-annotations.md)
213
+ name, not an RBS type, and does not belong in an ordinary type position.
214
+ Rigor reports the drop as
215
+ `plugin.rbs-inline.source-rbs-annotation-not-honoured`, naming the line and
216
+ the text that failed to parse, rather than leaving `show` silently `untyped`
217
+ with no diagnostic anywhere. The `# @rbs name: TYPE` tag form of the same
218
+ mistake is a different failure shape — see the next section.
219
+
220
+ ## An unresolvable type name in `# @rbs`
221
+
222
+ Naming a Rigor refinement (or any other unresolvable name) where an RBS type
223
+ belongs in the `# @rbs name: TYPE` tag form does not drop silently the way
224
+ `#:` does — it takes the whole class down:
225
+
226
+ ```ruby
227
+ class ProbeZZ
228
+ # @rbs g: finite-float
229
+ def probe(g)
230
+ g.to_s
231
+ end
232
+ end
233
+ ```
234
+
235
+ Upstream's own type parser truncates `finite-float` to `finite` (a hyphen
236
+ cannot continue an RBS type name) before this ever reaches Rigor, so `finite`
237
+ is the only token that reaches `RBS::DefinitionBuilder` — and it names no
238
+ loaded type, so the build for the WHOLE class fails
239
+ (`RBS::NoTypeFoundError`). `probe`, and every other real method on `ProbeZZ`,
240
+ reads `Dynamic[top]`. This surfaces as
241
+ `rbs.coverage.definition-build-failed`, naming the token and, when it is the
242
+ truncated head of a registered refinement name, the `%a{rigor:v1:…}` spelling
243
+ that IS valid today (see
244
+ [RBS::Extended annotations](../16-rbs-extended-annotations.md)).
245
+
246
+ ## Unparseable `# @rbs name:` types
247
+
248
+ A bounded or parameterised refinement — `Integer[1..10]`, `non-empty-array
249
+ [Integer]` — does not truncate at a hyphen the way `finite-float` does, so it
250
+ does not take the whole class down. In a `# @rbs name: TYPE` position it
251
+ leaves the parameter untyped instead, silently:
252
+
253
+ ```ruby
254
+ class BoundedProbe
255
+ # @rbs n: Integer[1..10]
256
+ def probe(n)
257
+ n
258
+ end
259
+ end
260
+ ```
261
+
262
+ The gem's grammar makes the colon-and-type optional on this annotation, so a
263
+ `TYPE` it cannot parse leaves the parameter's name recorded and its type
264
+ unset rather than raising — indistinguishable downstream from a parameter
265
+ nobody annotated at all. Rigor reports the drop as
266
+ `plugin.rbs-inline.source-rbs-annotation-not-honoured`, naming the line and
267
+ the text that failed to parse, and points at the `%a{rigor:v1:param:}`
268
+ spelling that carries a refinement like `Integer[1..10]` correctly (see
269
+ [RBS::Extended annotations](../16-rbs-extended-annotations.md)) — the same
270
+ advice the `%a{rigor:v1:…}` pointer above gives for the tag form.
271
+
272
+ ## An unrecognised tag after `@rbs`
273
+
274
+ The gem recognises a comment as an `@rbs` annotation attempt as soon as it
275
+ sees the word boundary right after `@rbs` — which matches a hyphen, not only
276
+ whitespace or end-of-line:
277
+
278
+ ```ruby
279
+ class TagProbe
280
+ # @rbs-ext return: non-empty-string
281
+ # @rbs return: String
282
+ def name
283
+ "x"
284
+ end
285
+ end
286
+ ```
287
+
288
+ `# @rbs-ext …` is inside that net, but nothing in the gem's grammar
289
+ recognises `-ext`, so the gem gives up on the whole paragraph and folds it
290
+ back into an ordinary comment — the neighbouring `# @rbs return: String`
291
+ line still binds, but the `@rbs-ext` line contributes nothing and, before
292
+ this, said nothing either. Rigor reports the drop as
293
+ `plugin.rbs-inline.source-rbs-annotation-not-honoured`, naming the line and
294
+ the comment text — without implying `@rbs-ext` is a recognised tag of any
295
+ kind. A comment that merely mentions `@rbs` in prose, or one that opens with
296
+ a tag not starting with `@rbs` (`# @extrbs …`), is outside the gem's
297
+ detector and stays silent.
298
+
112
299
  ## Configuration
113
300
 
114
301
  ```yaml
@@ -40,7 +40,7 @@ arguments.
40
40
  | `plugin.sidekiq.worker-call` | info | a `Worker.perform_*` call matched a discovered worker's `#perform` arity |
41
41
  | `plugin.sidekiq.wrong-arity` | error | the forwarded argument count falls outside `#perform`'s arity envelope (message names the schedule carve-out for `perform_in` / `perform_at`) |
42
42
  | `plugin.sidekiq.missing-schedule` | error | `perform_in()` / `perform_at()` called with zero arguments (the schedule is required even when `#perform` takes none) |
43
- | `plugin.sidekiq.load-error` | warning | worker discovery failed (parse/read error) — once per file |
43
+ | `plugin.sidekiq.load-error` | warning | worker discovery failed (parse/read error) — once per run, on `.rigor.yml` |
44
44
 
45
45
  ## What it types
46
46
 
@@ -144,6 +144,13 @@ added it is 100.
144
144
  alternative `klass:` spelling, and a schedule built in Ruby with
145
145
  `Sidekiq::Cron::Job.load_from_hash!`, supply no roots — the worker
146
146
  stays a `rigor unused` candidate rather than being guessed at.
147
+ - **The "failed to discover workers" warning is run-scoped.** It is
148
+ a fact about your configuration, not about any one source file, so
149
+ it is reported once per run on `.rigor.yml` rather than repeated on
150
+ every analysed file. It is `:warning`, so if you baselined it at its
151
+ old position that entry no longer matches and the row fails a
152
+ `--fail-on=warning` run — regenerate with `rigor baseline
153
+ regenerate`.
147
154
 
148
155
  ## Plugin internals
149
156
 
@@ -47,11 +47,27 @@ plugins:
47
47
  The plugin is **input-side only**: it translates Sorbet's syntax
48
48
  into Rigor's type model. It does **not** run Sorbet's checker,
49
49
  ship `sorbet-runtime`, or enforce Sorbet's runtime guarantees.
50
- When an RBS sig and a Sorbet sig disagree, RBS wins (the Sorbet
51
- sig may refine but not contradict it). Forms outside the
52
- translation table (`T.proc`, `T.self_type`, `T::Struct` /
53
- `T::Enum` subclasses, …) degrade to `Dynamic[top]`. Chapter 10
54
- documents the full vocabulary and these edges.
50
+ When an RBS sig and a Sorbet sig disagree, call sites get the
51
+ Sorbet sig's return type, and the method's body is checked
52
+ against the RBS return. Forms outside the
53
+ translation table (`T.proc`, `T.self_type`, …) degrade to
54
+ `Dynamic[top]`. Chapter 10 documents the full vocabulary and
55
+ these edges.
56
+
57
+ The plugin also types the annotation DSL's own expressions
58
+ (`sig`, `params`, `returns`, `void`, `abstract`, `override`,
59
+ `type_member`, `T::Array[...]`, `T::Helpers`/`T::Generic` macros,
60
+ `T::Struct`/`T::Enum` class bodies — `sig` on a plain
61
+ `T::Struct` subclass still needs the subclass's own `extend T::Sig`,
62
+ matching the runtime) through a bundled
63
+ `sorbet-runtime` RBS surface — so the sig DSL no longer reads as
64
+ `Dynamic[top]` on a Sorbet codebase. What it does not model:
65
+ `T::Struct` does not synthesise `prop`-declared readers (a
66
+ `Doc#name` accessor still reads opaque), `include T::Props` does
67
+ not confer the `ClassMethods` surface (only the `< T::Struct`
68
+ subclass path does), and the deeper type-member machinery
69
+ (`T.attached_class` variance, `type_member` bounds) is not
70
+ propagated into call sites.
55
71
 
56
72
  ## Plugin internals
57
73
 
data/exe/rigor CHANGED
@@ -11,13 +11,28 @@
11
11
  # context across the exec. The `none` and `process` (default) strategies
12
12
  # need no flag, so this is a no-op for them — behaviour is unchanged
13
13
  # unless a project opts into `ruby_box`.
14
+ #
15
+ # Before re-exec'ing, a child Ruby probes whether this Ruby can run
16
+ # Rigor under `RUBY_BOX=1` at all (Rigor::Plugin::BoxProbe — released
17
+ # Rubies through 4.0.7 segfault on Ruby Bug #22260). When it cannot,
18
+ # the request is dropped with a warning and the run proceeds under the
19
+ # configured strategy (`process` by default) instead of crashing.
14
20
  rigor_isolation = ENV["RIGOR_PLUGIN_ISOLATION"].to_s
15
21
  rigor_wants_box = rigor_isolation == "ruby_box" || !ENV["RIGOR_BOX"].to_s.empty?
16
22
  if rigor_wants_box && ENV["RUBY_BOX"].to_s.empty?
17
- require "rbconfig"
18
- ENV["RUBY_BOX"] = "1"
19
- ENV["RIGOR_PLUGIN_ISOLATION"] = "ruby_box" if rigor_isolation.empty?
20
- exec(RbConfig.ruby, __FILE__, *ARGV)
23
+ require_relative "../lib/rigor/plugin/box_probe"
24
+ if (rigor_box_unsupported = Rigor::Plugin::BoxProbe.unsupported_reason)
25
+ rigor_box_warning = "rigor: ruby_box plugin isolation is unavailable: #{rigor_box_unsupported}. " \
26
+ "Falling back to the configured plugins_isolation strategy (default: process)."
27
+ # `$stderr.puts`, not `warn`: `-W0` silences `warn`, and this fallback must never be silent.
28
+ $stderr.puts rigor_box_warning # rubocop:disable Style/StderrPuts
29
+ ENV.delete("RIGOR_PLUGIN_ISOLATION") if rigor_isolation == "ruby_box"
30
+ ENV.delete("RIGOR_BOX")
31
+ else
32
+ ENV["RUBY_BOX"] = "1"
33
+ ENV["RIGOR_PLUGIN_ISOLATION"] = "ruby_box" if rigor_isolation.empty?
34
+ exec(RbConfig.ruby, __FILE__, *ARGV)
35
+ end
21
36
  end
22
37
 
23
38
  lib = File.expand_path("../lib", __dir__)
@@ -275,7 +275,7 @@ module Rigor
275
275
  YAML.dump(document)
276
276
  end
277
277
 
278
- # The number of buckets recorded. Useful for the CLI summary on `generate`.
278
+ # The bucket count the `generate` summary reports.
279
279
  def size
280
280
  buckets.size
281
281
  end
@@ -32,9 +32,19 @@ module Rigor
32
32
  # `docs/internal-spec/inference-engine.md` § "Declaration-sourced provenance mark (ADR-58)"; keep this
33
33
  # comment subordinate to it.
34
34
  #
35
- # So `marked?` matches exactly the two node shapes the scope actually models and invents no propagation
36
- # of its own; anything flow-live keeps firing.
35
+ # So `marked?` matches exactly the node shapes the scope actually models and invents no propagation of its
36
+ # own; anything flow-live keeps firing.
37
+ #
38
+ # Issue #1362 adds a third carrier, a global still on its program-global seed, which joins the non-nil part of
39
+ # the declared type of a global Ruby's own signatures declare with the file's writes (ADR-117 Decision point
40
+ # 2: never warn that a value might deviate from the idiom). Its declared members are the declaration-sourced
41
+ # part. The consumers judge such a value by the file's writes ({.written_type}, {.withholds_nil?}), and a
42
+ # value that mixes it with something else without the declared members the file never writes
43
+ # ({.declared_only_rejection?}), so a report the writes alone earn still fires.
37
44
  module DeclarationSourcedGuard
45
+ NO_GLOBALS = [].freeze
46
+ private_constant :NO_GLOBALS
47
+
38
48
  module_function
39
49
 
40
50
  # True when `node` is a direct read of a binding whose optionality is purely declaration-sourced. Any
@@ -44,9 +54,164 @@ module Rigor
44
54
  case node
45
55
  when Prism::InstanceVariableReadNode then scope.declaration_sourced?(:ivar, node.name)
46
56
  when Prism::LocalVariableReadNode then scope.declaration_sourced?(:local, node.name)
57
+ when Prism::GlobalVariableReadNode then scope.declaration_sourced?(:global, node.name)
47
58
  else false
48
59
  end
49
60
  end
61
+
62
+ # The globals whose declared seeds `node` still reads, empty for none: a read of a marked global, or of a
63
+ # local copied from one or more (`sep = $/`, `Scope#declaration_sourced_global_copies`), bare or in
64
+ # parentheses (`($stdout)`).
65
+ def global_sources(node, scope)
66
+ node = unparenthesised(node)
67
+ case node
68
+ when Prism::GlobalVariableReadNode
69
+ scope.declaration_sourced?(:global, node.name) ? [node.name] : NO_GLOBALS
70
+ when Prism::LocalVariableReadNode
71
+ scope.declaration_sourced_global_copies(node.name)
72
+ else NO_GLOBALS
73
+ end
74
+ end
75
+
76
+ # The globals a local written from `value` copies the declared seeds of, empty for none: a read of a marked
77
+ # global, bare or in parentheses (`sep = ($/)`), or a bare read of a local that copies some (`s = sep`).
78
+ # Unlike an ivar copy's, this mark follows bare local-to-local copies; a method result (`$/.dup`) or a
79
+ # container element (`[$/].each { |s| … }`) carries none, as ADR-58's one-hop boundary has it.
80
+ def copied_globals(value, scope)
81
+ return scope.declaration_sourced_global_copies(value.name) if value.is_a?(Prism::LocalVariableReadNode)
82
+
83
+ read = unparenthesised(value)
84
+ return NO_GLOBALS unless read.is_a?(Prism::GlobalVariableReadNode) &&
85
+ scope.declaration_sourced?(:global, read.name)
86
+
87
+ [read.name]
88
+ end
89
+
90
+ # `node` with its enclosing parentheses taken off while they hold a single expression.
91
+ def unparenthesised(node)
92
+ while node.is_a?(Prism::ParenthesesNode)
93
+ body = node.body
94
+ break unless body.is_a?(Prism::StatementsNode) && body.body.size == 1
95
+
96
+ node = body.body.first
97
+ end
98
+ node
99
+ end
100
+
101
+ # The union of the file's own writes to the globals `node` reads the seeds of ({.global_sources}), or nil.
102
+ def written_type(node, scope)
103
+ sources = global_sources(node, scope)
104
+ return nil if sources.empty?
105
+
106
+ written = sources.map { |source| scope.program_globals[source] }
107
+ written.include?(nil) ? nil : Type::Combinator.union(*written)
108
+ end
109
+
110
+ # True when a `nil` in `node`'s type is not diagnostic fuel: for a global's seed or its copy, when the file
111
+ # never writes `nil` to the global, so the declaration alone contributes it; otherwise when `node` carries
112
+ # the ADR-58 mark.
113
+ def withholds_nil?(node, scope)
114
+ written = written_type(node, scope)
115
+ return marked?(node, scope) if written.nil?
116
+
117
+ !nil_bearing?(written)
118
+ end
119
+
120
+ # Issue #1362 — true when every member of `type` that `expected` rejects is a declared member of a builtin
121
+ # global this file writes and joins ({#declared_only_members}), so the rejection rests on the join alone. The
122
+ # argument and return gates read it for a value that mixes such a global with something else
123
+ # (`c ? $stdout : StringIO.new`), which the ADR-58 mark does not follow. It is a type-level reading: it also
124
+ # withholds a rejection of such a member that comes from elsewhere in the file (`STDOUT` passed where a
125
+ # `StringIO` is required, in a file that writes `$stdout`).
126
+ #
127
+ # Otherwise the value with those members taken out of every union inside it ({#without_declared_only}) is
128
+ # handed to the block, which answers whether the caller's rule accepts it: the value as it was before the
129
+ # join, one level into a container it builds as well (`[$stdout]` or `{ io: $stdout }` passed where an
130
+ # `Array[StringIO]` is required), and with a `nil` the file writes judged as the rule judges a `nil`.
131
+ def declared_only_rejection?(type, expected, scope)
132
+ declared_only = declared_only_members(scope)
133
+ return false if declared_only.empty?
134
+
135
+ members = type.is_a?(Type::Union) ? type.members : [type]
136
+ rejected = members.select { |member| Inference::Acceptance.accepts(expected, member, mode: :gradual).no? }
137
+ return false if rejected.empty?
138
+ return true if rejected.all? { |member| declared_only.include?(member) }
139
+
140
+ stripped = without_declared_only(type, declared_only)
141
+ !stripped.equal?(type) && yield(stripped)
142
+ end
143
+
144
+ # `type` with each member of `declared_only` taken out of every union in it, through tuples, hash shapes and
145
+ # generic arguments; a union with nothing else left keeps its members. Answers `type` itself when nothing
146
+ # was taken out.
147
+ def without_declared_only(type, declared_only)
148
+ case type
149
+ when Type::Union then union_without_declared_only(type, declared_only)
150
+ when Type::Tuple
151
+ elements = type.elements.map { |element| without_declared_only(element, declared_only) }
152
+ elements == type.elements ? type : Type::Combinator.tuple_of(*elements)
153
+ when Type::HashShape
154
+ pairs = type.pairs.transform_values { |value| without_declared_only(value, declared_only) }
155
+ pairs == type.pairs ? type : rebuild_hash_shape(type, pairs)
156
+ when Type::Nominal
157
+ args = type.type_args.map { |arg| without_declared_only(arg, declared_only) }
158
+ args == type.type_args ? type : Type::Combinator.nominal_of(type.class_name, type_args: args)
159
+ else type
160
+ end
161
+ end
162
+
163
+ def union_without_declared_only(union, declared_only)
164
+ kept = union.members.reject { |member| declared_only.include?(member) }
165
+ kept = union.members if kept.empty?
166
+ rebuilt = kept.map { |member| without_declared_only(member, declared_only) }
167
+ rebuilt == union.members ? union : Type::Combinator.union(*rebuilt)
168
+ end
169
+
170
+ def rebuild_hash_shape(shape, pairs)
171
+ Type::Combinator.hash_shape_of(
172
+ pairs, required_keys: shape.required_keys, optional_keys: shape.optional_keys,
173
+ read_only_keys: shape.read_only_keys, extra_keys: shape.extra_keys
174
+ )
175
+ end
176
+
177
+ # For each builtin global this file writes and joins, the non-nil members of its declared type that the file's
178
+ # writes to that global do not hold (`IO` after `$stdout = StringIO.new`, nothing after `$stderr = STDERR`),
179
+ # gathered over the globals. A declared literal also counts in its class form, which a method called on the
180
+ # joined union returns: `$VERBOSE.itself` reads `FalseClass | TrueClass` after `$VERBOSE = true`.
181
+ def declared_only_members(scope)
182
+ seeds = scope.discovery.program_global_seeds
183
+ return NO_GLOBALS if seeds.empty?
184
+
185
+ seeds.keys.flat_map do |name|
186
+ written = type_members(scope.program_globals[name])
187
+ type_members(scope.environment.global_for_name(name, builtin: true))
188
+ .reject { |member| nil_bearing?(member) }
189
+ .flat_map { |member| [member, *class_form(member)] }
190
+ .reject { |member| written.include?(member) }
191
+ end.uniq
192
+ end
193
+
194
+ def class_form(member)
195
+ return NO_GLOBALS unless member.is_a?(Type::Constant)
196
+
197
+ [Type::Combinator.nominal_of(member.value.class.name)]
198
+ end
199
+
200
+ def type_members(type)
201
+ return NO_GLOBALS if type.nil?
202
+
203
+ type.is_a?(Type::Union) ? type.members : [type]
204
+ end
205
+
206
+ def nil_bearing?(type)
207
+ members = type.is_a?(Type::Union) ? type.members : [type]
208
+ members.any? do |member|
209
+ (member.is_a?(Type::Constant) && member.value.nil?) ||
210
+ (member.is_a?(Type::Nominal) && member.class_name == "NilClass")
211
+ end
212
+ end
213
+ private_class_method :nil_bearing?, :unparenthesised, :declared_only_members, :type_members, :class_form,
214
+ :without_declared_only, :union_without_declared_only, :rebuild_hash_shape
50
215
  end
51
216
  end
52
217
  end