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,708 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rbs"
4
+
5
+ require_relative "../../type"
6
+ require_relative "../../inference/acceptance"
7
+ require_relative "../../inference/rbs_type_translator"
8
+ require_relative "../../rbs_extended"
9
+
10
+ module Rigor
11
+ class Environment
12
+ module MemberConsistency
13
+ # Compares two declarations of one member (ADR-112 WD5), and a member's refinements with its own
14
+ # declared types. See {MemberConsistency} for the rule; this class holds the type work.
15
+ #
16
+ # A contradiction is reported only where it is PROVEN: no value, and no call, satisfies both sides.
17
+ # Everything short of a proof is undecided. Four places carry that burden:
18
+ #
19
+ # - {#disjoint?} proves two types share no value, from the RBS hierarchy of Ruby core and stdlib
20
+ # classes ({RbsProof}) only: two such classes neither of which is an RBS ancestor of the other, or a
21
+ # literal whose class is not below such a class. Never Ruby constants the analyzer process happens to
22
+ # have loaded (rbs declares `Tempfile < File`, the `tempfile` library `Tempfile < Delegator`), and
23
+ # never a class the project declares anywhere. A module, an interface, a carrier without a class of
24
+ # its own, or a class-name refinement payload proves nothing; and without an {RbsProof} (the
25
+ # environment build) nothing is ever proven.
26
+ # - {#provable_names?} admits only class names written absolutely (`::String`). A relative name may be
27
+ # a class the project defines only in Ruby (`String` inside `module App` may be `App::String`), and
28
+ # seeing that would take a scan of the run's files, which a path-set-independent answer cannot use.
29
+ # - {#faithful?} refuses a type position whose Rigor translation could mean something else: an alias,
30
+ # an interface, `self`, a type variable, a proc, and a relative class name the project's RBS inputs
31
+ # declare (a relative `Data` inside `module App` may be `App::Data`).
32
+ # - A position a call may leave empty — an optional or rest parameter, an optional keyword — never
33
+ # contradicts, since the call that omits it satisfies both sides; nor does any block position, since
34
+ # a body that never yields, or an untyped block, satisfies both.
35
+ # - Overloads are paired by correspondence, never by position, and a member whose overloads do not
36
+ # pair one to one is undecided.
37
+ #
38
+ # Which side binds ({#subset?}) is decided by names alone — the same class, a literal of exactly that
39
+ # class, a refinement of that class — with no hierarchy at all. The environment build takes that
40
+ # decision before the environment exists, and the reported rows re-derive it on a cache hit, so it
41
+ # must read nothing but the two declarations. A subclass relation (`Integer` against `Numeric`) is
42
+ # therefore undecided, as it was before the rule existed.
43
+ class Comparator # rubocop:disable Metrics/ClassLength
44
+ # A type position: the declared RBS type, and the `rigor:v1:` refinement that overrides it (or nil).
45
+ Slot = Data.define(:rbs_type, :override)
46
+ private_constant :Slot
47
+
48
+ # The RBS leaf types whose Rigor translation loses nothing a precision comparison could depend on.
49
+ FAITHFUL_LEAVES = [
50
+ ::RBS::Types::Bases::Any, ::RBS::Types::Bases::Bool, ::RBS::Types::Bases::Nil,
51
+ ::RBS::Types::Bases::Top, ::RBS::Types::Bases::Bottom, ::RBS::Types::Bases::Void,
52
+ ::RBS::Types::Literal
53
+ ].freeze
54
+ private_constant :FAITHFUL_LEAVES
55
+
56
+ # RBS's three spellings of the top type ("they are all equivalent for the type system" — rbs
57
+ # `docs/syntax.md`). None says anything about the value, so each is the least precise answer a
58
+ # position can give, and two of them are equal.
59
+ TOP_SPELLINGS = [::RBS::Types::Bases::Any, ::RBS::Types::Bases::Top, ::RBS::Types::Bases::Void].freeze
60
+ private_constant :TOP_SPELLINGS
61
+
62
+ # Classes whose instances are exactly one value, read as that value so `bool` and `TrueClass`
63
+ # compare as a union and its member rather than as two unrelated names.
64
+ SINGLE_VALUE_CLASSES = { "TrueClass" => true, "FalseClass" => false, "NilClass" => nil }.freeze
65
+ private_constant :SINGLE_VALUE_CLASSES
66
+
67
+ EQUAL = [:equal, nil].freeze
68
+ SIGNATURE_MORE_PRECISE = [:signature, nil].freeze
69
+ CONSISTENT = %i[equal signature inline].freeze
70
+ NO_STRINGS = [].freeze
71
+ private_constant :EQUAL, :SIGNATURE_MORE_PRECISE, :CONSISTENT, :NO_STRINGS
72
+
73
+ RETURN_DIRECTIVE_PREFIX = "rigor:v1:return:"
74
+ PARAM_DIRECTIVE_PREFIX = "rigor:v1:param:"
75
+ INFERRED_SIGNATURE_DIRECTIVE = RbsExtended::INFERRED_SIGNATURE_DIRECTIVE
76
+ private_constant :RETURN_DIRECTIVE_PREFIX, :PARAM_DIRECTIVE_PREFIX, :INFERRED_SIGNATURE_DIRECTIVE
77
+
78
+ # More overloads than this per member are left undecided rather than paired: the pairing compares
79
+ # every overload with every other.
80
+ PAIRING_LIMIT = 16
81
+ private_constant :PAIRING_LIMIT
82
+
83
+ # The carriers {#refinement_subset?} hands to `Inference::Acceptance`: a literal or a refinement
84
+ # against a refinement, which it answers from the value and the refinement alone.
85
+ REFINEMENT_OR_VALUE = [Type::Constant, Type::Refined, Type::Difference, Type::IntegerRange,
86
+ Type::FloatRange].freeze
87
+ # The class a range or record carrier's values all belong to.
88
+ CARRIER_CLASSES = { Type::IntegerRange => "Integer", Type::FloatRange => "Float",
89
+ Type::HashShape => "Hash" }.freeze
90
+ private_constant :REFINEMENT_OR_VALUE, :CARRIER_CLASSES
91
+
92
+ # @param shadowable — the first `::` segment of every class / module name the project's RBS inputs
93
+ # declare. A relative class name starting with one of them may resolve inside an enclosing
94
+ # namespace, so it is not compared.
95
+ # @param proof — an {RbsProof}, or nil when nothing may be proven disjoint (the environment build,
96
+ # whose decisions do not depend on it).
97
+ def initialize(shadowable, proof: nil)
98
+ @shadowable = shadowable
99
+ @proof = proof
100
+ end
101
+
102
+ # @return `[outcome, detail]` — `outcome` is `:equal`, `:signature` (`sig/` is more precise),
103
+ # `:inline` (the inline side is more precise), `:contradiction` or `:undecided`; `detail` names the
104
+ # deciding position for the two reported outcomes and is nil otherwise.
105
+ def compare(signature_member, inline_member, method_name)
106
+ return EQUAL if same_declaration?(signature_member, inline_member)
107
+ return SIGNATURE_MORE_PRECISE if inferred_signature?(inline_member)
108
+
109
+ if signature_member.is_a?(::RBS::AST::Members::Alias) || inline_member.is_a?(::RBS::AST::Members::Alias)
110
+ return compare_aliases(signature_member, inline_member)
111
+ end
112
+
113
+ compare_overloads(
114
+ overloads_for(signature_member, method_name), overloads_for(inline_member, method_name),
115
+ refinements_of(signature_member), refinements_of(inline_member)
116
+ )
117
+ end
118
+
119
+ # The details of every member-level `rigor:v1:return:` / `rigor:v1:param:` refinement on `member`
120
+ # that is provably disjoint from its own declared type in every overload it applies to. Only
121
+ # member-level annotations count: `RBS::Definition::Method#annotations`, which call sites read, does
122
+ # not carry an overload's own annotations, so the engine never honours those.
123
+ def refinement_conflicts(member)
124
+ return NO_STRINGS unless member_annotation_strings(member).any? { |string| refinement_directive?(string) }
125
+
126
+ return_override, param_overrides = refinements_of(member)
127
+ method_types = member_method_types(member)
128
+ return NO_STRINGS if method_types.empty?
129
+
130
+ conflicts = []
131
+ conflicts << return_conflict(method_types, return_override) if return_override
132
+ param_overrides.each { |name, override| conflicts << param_conflict(method_types, name, override) }
133
+ conflicts.compact
134
+ end
135
+
136
+ # Every annotation string on the member and its overloads, in declaration order.
137
+ def member_annotation_strings(member)
138
+ strings = nil
139
+ if member.respond_to?(:annotations)
140
+ Array(member.annotations).each { |annotation| (strings ||= []) << annotation.string }
141
+ end
142
+ if member.is_a?(::RBS::AST::Members::MethodDefinition)
143
+ member.overloads.each do |overload|
144
+ Array(overload.annotations).each { |annotation| (strings ||= []) << annotation.string }
145
+ end
146
+ end
147
+ strings || NO_STRINGS
148
+ end
149
+
150
+ def refinement_directive?(string)
151
+ string.start_with?(RETURN_DIRECTIVE_PREFIX, PARAM_DIRECTIVE_PREFIX)
152
+ end
153
+
154
+ private
155
+
156
+ # The common case after `rigor sig-gen` has written an annotated member into `sig/`: the two
157
+ # declarations are the same text, annotations included, and nothing needs translating.
158
+ def same_declaration?(signature_member, inline_member)
159
+ return false unless signature_member.instance_of?(inline_member.class)
160
+ return false unless signature_member.annotations == inline_member.annotations
161
+
162
+ case signature_member
163
+ when ::RBS::AST::Members::MethodDefinition then signature_member.overloads == inline_member.overloads
164
+ when ::RBS::AST::Members::AttrReader, ::RBS::AST::Members::AttrWriter, ::RBS::AST::Members::AttrAccessor
165
+ signature_member.type == inline_member.type
166
+ else false
167
+ end
168
+ end
169
+
170
+ # rbs-inline's skeleton for a `def` whose annotations asserted nothing about it (ADR-93 WD6,
171
+ # `%a{rigor:v1:inferred-signature}`): no author wrote this inline side, so it has nothing to be
172
+ # consistent or inconsistent with, and `sig/` binds without a word.
173
+ def inferred_signature?(member)
174
+ member_annotation_strings(member).include?(INFERRED_SIGNATURE_DIRECTIVE)
175
+ end
176
+
177
+ def compare_aliases(signature_member, inline_member)
178
+ both = signature_member.is_a?(::RBS::AST::Members::Alias) && inline_member.is_a?(::RBS::AST::Members::Alias)
179
+ return EQUAL if both && signature_member.old_name == inline_member.old_name
180
+
181
+ [:undecided, "an alias cannot be compared with another declaration"]
182
+ end
183
+
184
+ # One overload each: compared directly, and the only shape where a contradiction can be reported. A
185
+ # contradiction between two overload SETS would need every pairing refuted, and "these two overloads
186
+ # disagree" says nothing when a third one covers the call; so several overloads either pair one to
187
+ # one — each overload consistent with exactly one on the other side — or stay undecided.
188
+ def compare_overloads(sig_types, inline_types, sig_refinements, inline_refinements)
189
+ return [:undecided, "the two declare a different number of overloads"] if sig_types.size != inline_types.size
190
+ if sig_types.size == 1
191
+ return combine(compare_method_types(sig_types[0], inline_types[0], sig_refinements, inline_refinements))
192
+ end
193
+ return [:undecided, "too many overloads to pair"] if sig_types.size > PAIRING_LIMIT
194
+
195
+ matrix = sig_types.map do |sig_type|
196
+ inline_types.map do |inline_type|
197
+ combine(compare_method_types(sig_type, inline_type, sig_refinements, inline_refinements))
198
+ end
199
+ end
200
+ paired = unique_pairing(matrix, [:equal]) || unique_pairing(matrix, CONSISTENT)
201
+ return [:undecided, "the overloads do not pair one to one"] if paired.nil?
202
+
203
+ combine(paired)
204
+ end
205
+
206
+ # The verdicts of a one-to-one pairing in which every row has exactly one column whose outcome is in
207
+ # `accepted`, all columns distinct; nil when there is no such pairing.
208
+ def unique_pairing(matrix, accepted)
209
+ columns = matrix.map do |row|
210
+ candidates = row.each_index.select { |j| accepted.include?(row[j].first) }
211
+ return nil unless candidates.size == 1
212
+
213
+ candidates.first
214
+ end
215
+ return nil unless columns.uniq.size == columns.size
216
+
217
+ columns.each_with_index.map { |j, i| matrix[i][j] }
218
+ end
219
+
220
+ # The method types one member declares for `method_name`: an attribute reads as `() -> T` (reader)
221
+ # or `(T) -> T` (writer), which is the method RBS builds from it.
222
+ def overloads_for(member, method_name)
223
+ case member
224
+ when ::RBS::AST::Members::MethodDefinition then member.overloads.map(&:method_type)
225
+ when ::RBS::AST::Members::AttrReader, ::RBS::AST::Members::AttrWriter, ::RBS::AST::Members::AttrAccessor
226
+ [attribute_method_type(member.type, writer: method_name.to_s.end_with?("="))]
227
+ else []
228
+ end
229
+ end
230
+
231
+ def member_method_types(member)
232
+ case member
233
+ when ::RBS::AST::Members::MethodDefinition then member.overloads.map(&:method_type)
234
+ when ::RBS::AST::Members::AttrReader, ::RBS::AST::Members::AttrAccessor
235
+ [attribute_method_type(member.type, writer: false)]
236
+ when ::RBS::AST::Members::AttrWriter then [attribute_method_type(member.type, writer: true)]
237
+ else []
238
+ end
239
+ end
240
+
241
+ def attribute_method_type(type, writer:)
242
+ positionals = writer ? [::RBS::Types::Function::Param.new(type: type, name: nil)] : []
243
+ function = ::RBS::Types::Function.new(
244
+ required_positionals: positionals, optional_positionals: [], rest_positionals: nil,
245
+ trailing_positionals: [], required_keywords: {}, optional_keywords: {}, rest_keywords: nil,
246
+ return_type: type
247
+ )
248
+ ::RBS::MethodType.new(type_params: [], type: function, block: nil, location: nil)
249
+ end
250
+
251
+ def compare_method_types(sig_type, inline_type, sig_refinements, inline_refinements)
252
+ return [EQUAL] if sig_refinements == inline_refinements && sig_type == inline_type
253
+ unless sig_type.type_params.empty? && inline_type.type_params.empty?
254
+ return [[:undecided, "a generic method type"]]
255
+ end
256
+
257
+ compare_functions(sig_type.type, inline_type.type, sig_refinements, inline_refinements, "",
258
+ block: false, provable: true) +
259
+ compare_blocks(sig_type.block, inline_type.block)
260
+ end
261
+
262
+ def compare_blocks(sig_block, inline_block)
263
+ return [] if sig_block.nil? && inline_block.nil?
264
+ return [[:undecided, "only one side declares a block"]] if sig_block.nil? || inline_block.nil?
265
+ return [[:undecided, "the block is required on one side only"]] if sig_block.required != inline_block.required
266
+
267
+ # A block position never contradicts: a call without an optional block, a body that never yields,
268
+ # or an untyped block satisfies both sides.
269
+ compare_functions(sig_block.type, inline_block.type, [nil, {}], [nil, {}], "block ",
270
+ block: true, provable: false)
271
+ end
272
+
273
+ def compare_functions(sig_fn, inline_fn, sig_refinements, inline_refinements, prefix, block:, provable:)
274
+ results = compare_parameters(sig_fn, inline_fn, [sig_refinements[1], inline_refinements[1]], prefix,
275
+ block, provable)
276
+ results << compare_slot(
277
+ "#{prefix}return type",
278
+ Slot.new(rbs_type: sig_fn.return_type, override: sig_refinements[0]),
279
+ Slot.new(rbs_type: inline_fn.return_type, override: inline_refinements[0]),
280
+ provable: provable
281
+ )
282
+ results
283
+ end
284
+
285
+ # `(?) -> T` declares nothing about its parameters, so the other side's parameter list is the more
286
+ # precise one — or they are equal when both are untyped.
287
+ def compare_parameters(sig_fn, inline_fn, param_refinements, prefix, block, provable)
288
+ sig_params, inline_params = param_refinements
289
+ sig_untyped = sig_fn.is_a?(::RBS::Types::UntypedFunction)
290
+ inline_untyped = inline_fn.is_a?(::RBS::Types::UntypedFunction)
291
+ return [] if sig_untyped && inline_untyped
292
+ return [[:inline, nil]] if sig_untyped
293
+ return [[:signature, nil]] if inline_untyped
294
+
295
+ unless function_shape(sig_fn) == function_shape(inline_fn)
296
+ return [(!block && shape_contradiction(sig_fn, inline_fn, prefix)) || shape_undecided(block)]
297
+ end
298
+
299
+ parameter_pairs(sig_fn, inline_fn).map do |label, sig_param, inline_param, required|
300
+ compare_slot(
301
+ "#{prefix}#{label}",
302
+ Slot.new(rbs_type: sig_param.type, override: sig_param.name && sig_params[sig_param.name]),
303
+ Slot.new(rbs_type: inline_param.type, override: inline_param.name && inline_params[inline_param.name]),
304
+ provable: provable && required
305
+ )
306
+ end
307
+ end
308
+
309
+ def shape_undecided(block)
310
+ [:undecided,
311
+ block ? "the block's parameter lists have different shapes" : "the parameter lists have different shapes"]
312
+ end
313
+
314
+ def function_shape(function)
315
+ [function.required_positionals.size, function.optional_positionals.size, !function.rest_positionals.nil?,
316
+ function.trailing_positionals.size, function.required_keywords.keys.sort,
317
+ function.optional_keywords.keys.sort, !function.rest_keywords.nil?]
318
+ end
319
+
320
+ # Aligned `[label, sig_param, inline_param, required]` for two functions of the same shape. `required`
321
+ # is false for a position a call may leave empty.
322
+ def parameter_pairs(sig_fn, inline_fn)
323
+ positional_pairs(sig_fn, inline_fn) + keyword_pairs(sig_fn, inline_fn)
324
+ end
325
+
326
+ def positional_pairs(sig_fn, inline_fn)
327
+ inline_leading = inline_fn.required_positionals + inline_fn.optional_positionals
328
+ required = sig_fn.required_positionals.size
329
+ pairs = (sig_fn.required_positionals + sig_fn.optional_positionals).each_with_index.map do |param, i|
330
+ ["parameter #{i + 1}", param, inline_leading[i], i < required]
331
+ end
332
+ if sig_fn.rest_positionals
333
+ pairs << ["rest parameter", sig_fn.rest_positionals, inline_fn.rest_positionals, false]
334
+ end
335
+ sig_fn.trailing_positionals.each_with_index do |param, i|
336
+ pairs << ["trailing parameter #{i + 1}", param, inline_fn.trailing_positionals[i], true]
337
+ end
338
+ pairs
339
+ end
340
+
341
+ def keyword_pairs(sig_fn, inline_fn)
342
+ inline_keywords = inline_fn.required_keywords.merge(inline_fn.optional_keywords)
343
+ pairs = sig_fn.required_keywords.merge(sig_fn.optional_keywords).map do |name, param|
344
+ ["keyword `#{name}:`", param, inline_keywords[name], sig_fn.required_keywords.key?(name)]
345
+ end
346
+ if sig_fn.rest_keywords
347
+ pairs << ["keyword rest parameter", sig_fn.rest_keywords, inline_fn.rest_keywords, false]
348
+ end
349
+ pairs
350
+ end
351
+
352
+ # A shape difference is a contradiction only where no call can satisfy both. Ruby passes keywords to
353
+ # a method without keyword parameters as a trailing positional Hash, so the positional counts only
354
+ # mean something when neither side takes keywords; and a keyword one side requires is refused by the
355
+ # other only when the other has no positional parameter a Hash could land in and no `**rest`. A
356
+ # block's parameters never contradict by count: a block is called with whatever its caller passes.
357
+ def shape_contradiction(sig_fn, inline_fn, prefix)
358
+ if keywordless?(sig_fn) && keywordless?(inline_fn)
359
+ sig_range = positional_range(sig_fn)
360
+ inline_range = positional_range(inline_fn)
361
+ if sig_range.last < inline_range.first || inline_range.last < sig_range.first
362
+ return [:contradiction, "#{prefix}`sig/` accepts #{describe_range(sig_range)} positional " \
363
+ "argument(s) and the inline annotation #{describe_range(inline_range)}"]
364
+ end
365
+ end
366
+
367
+ keyword = unacceptable_keyword(sig_fn, inline_fn) || unacceptable_keyword(inline_fn, sig_fn)
368
+ return nil if keyword.nil?
369
+
370
+ [:contradiction, "#{prefix}keyword `#{keyword}:` is required by one side and not accepted by the other"]
371
+ end
372
+
373
+ def keywordless?(function)
374
+ function.required_keywords.empty? && function.optional_keywords.empty? && function.rest_keywords.nil?
375
+ end
376
+
377
+ def positional_range(function)
378
+ minimum = function.required_positionals.size + function.trailing_positionals.size
379
+ maximum = function.rest_positionals ? Float::INFINITY : minimum + function.optional_positionals.size
380
+ (minimum..maximum)
381
+ end
382
+
383
+ def describe_range(range)
384
+ return range.first.to_s if range.first == range.last
385
+ return "#{range.first} or more" if range.last == Float::INFINITY
386
+
387
+ "#{range.first} to #{range.last}"
388
+ end
389
+
390
+ def unacceptable_keyword(requiring, other)
391
+ return nil unless other.rest_keywords.nil? && other.rest_positionals.nil?
392
+ unless (other.required_positionals + other.optional_positionals + other.trailing_positionals).empty?
393
+ return nil
394
+ end
395
+
396
+ requiring.required_keywords.keys.find do |name|
397
+ !other.required_keywords.key?(name) && !other.optional_keywords.key?(name)
398
+ end
399
+ end
400
+
401
+ # One type position. The top spellings are the least precise answer; a position Rigor cannot
402
+ # translate faithfully is undecided unless the two are spelled identically. `provable` is false for a
403
+ # position a call may leave empty, which can never contradict.
404
+ def compare_slot(label, sig_slot, inline_slot, provable:)
405
+ return EQUAL if sig_slot == inline_slot
406
+
407
+ sig_top = top_slot?(sig_slot)
408
+ inline_top = top_slot?(inline_slot)
409
+ return EQUAL if sig_top && inline_top
410
+ return [:inline, nil] if sig_top
411
+ return SIGNATURE_MORE_PRECISE if inline_top
412
+
413
+ sig_type = slot_type(sig_slot)
414
+ inline_type = slot_type(inline_slot)
415
+ return [:undecided, "#{label} is a type Rigor cannot compare"] if sig_type.nil? || inline_type.nil?
416
+
417
+ slot_verdict(label, [sig_slot, inline_slot], sig_type, inline_type, provable)
418
+ end
419
+
420
+ def slot_verdict(label, slots, sig_type, inline_type, provable)
421
+ inline_fits = subset?(inline_type, sig_type)
422
+ sig_fits = subset?(sig_type, inline_type)
423
+ return EQUAL if inline_fits && sig_fits
424
+ return [:inline, nil] if inline_fits
425
+ return SIGNATURE_MORE_PRECISE if sig_fits
426
+ unless provable && provable_names?(slots) && disjoint?(sig_type, inline_type)
427
+ return [:undecided, "Rigor cannot rank or separate the two #{label}s"]
428
+ end
429
+
430
+ [:contradiction,
431
+ "#{label} is `#{describe_slot(slots[0])}` in `sig/` and `#{describe_slot(slots[1])}` inline, " \
432
+ "and no value is both"]
433
+ end
434
+
435
+ def top_slot?(slot)
436
+ slot.override.nil? && TOP_SPELLINGS.any? { |klass| slot.rbs_type.is_a?(klass) }
437
+ end
438
+
439
+ def slot_type(slot)
440
+ type = slot.override || (faithful?(slot.rbs_type) ? translate(slot.rbs_type) : nil)
441
+ type && single_values(type)
442
+ end
443
+
444
+ # `TrueClass` / `FalseClass` / `NilClass` as the one value each holds, through a union.
445
+ def single_values(type)
446
+ case type
447
+ when Type::Nominal
448
+ return type unless type.type_args.empty? && SINGLE_VALUE_CLASSES.key?(type.class_name)
449
+
450
+ Type::Combinator.constant_of(SINGLE_VALUE_CLASSES.fetch(type.class_name))
451
+ when Type::Union then Type::Combinator.union(*type.members.map { |member| single_values(member) })
452
+ else type
453
+ end
454
+ end
455
+
456
+ def describe_slot(slot)
457
+ slot.override ? slot.override.describe(:short) : slot.rbs_type.to_s
458
+ end
459
+
460
+ # Whether the Rigor translation of `type` keeps everything a subtype answer could depend on.
461
+ # `RbsTypeTranslator` reads an alias, an interface, `self`, `instance`, `class` and a type variable as
462
+ # `untyped`, a proc as the bare `Proc` class, and an intersection as one of its members; and it reads
463
+ # a relative class name as a top-level one, which it is not when an enclosing namespace declares the
464
+ # same name. Comparing any of those would let a lost detail decide which side binds, or report a
465
+ # contradiction between two spellings of one class.
466
+ def faithful?(type)
467
+ case type
468
+ when *FAITHFUL_LEAVES then true
469
+ when ::RBS::Types::ClassInstance then faithful_name?(type.name) && type.args.all? { |arg| faithful?(arg) }
470
+ when ::RBS::Types::ClassSingleton then faithful_name?(type.name)
471
+ when ::RBS::Types::Optional then faithful?(type.type)
472
+ when ::RBS::Types::Union, ::RBS::Types::Tuple then type.types.all? { |member| faithful?(member) }
473
+ when ::RBS::Types::Record then (type.fields.values + type.optional_fields.values).all? { |v| faithful?(v) }
474
+ else false
475
+ end
476
+ end
477
+
478
+ # An absolute name means what it says. A relative one resolves outward from the member's class, so it
479
+ # is read as top-level only when the project declares no class or module whose name starts with the
480
+ # same segment — which is also what makes the `sig/` and the inline spelling of it comparable.
481
+ def faithful_name?(name)
482
+ return true if name.absolute?
483
+
484
+ head = name.namespace.path.first || name.name
485
+ !@shadowable.include?(head.to_s)
486
+ end
487
+
488
+ def translate(type)
489
+ Inference::RbsTypeTranslator.translate(type)
490
+ rescue StandardError
491
+ nil
492
+ end
493
+
494
+ # Whether `sub` is provably a subset of `sup`, decided by names alone (see the class comment): equal
495
+ # types, a union member by member, the same class with element types that are each a subset (or
496
+ # unstated on `sup`), a literal of exactly that class, a refinement or a range of that class, a tuple
497
+ # of an `Array`, a record of a `Hash`; `untyped` either side. A refinement on the `sup` side is left
498
+ # to `Inference::Acceptance`, which answers it from the value and the refinement alone.
499
+ def subset?(sub, sup)
500
+ return true if sub == sup || gradual?(sub) || gradual?(sup) || sub.is_a?(Type::Bot)
501
+ return sub.members.all? { |member| subset?(member, sup) } if sub.is_a?(Type::Union)
502
+ return sup.members.any? { |member| subset?(sub, member) } if sup.is_a?(Type::Union)
503
+
504
+ subset_of_member?(sub, sup)
505
+ end
506
+
507
+ def subset_of_member?(sub, sup)
508
+ case sup
509
+ when Type::Nominal then subset_of_class?(sub, sup)
510
+ when Type::Refined, Type::Difference, Type::IntegerRange, Type::FloatRange then refinement_subset?(sub, sup)
511
+ when Type::Tuple then sub.is_a?(Type::Tuple) && pairwise_subset?(sub.elements, sup.elements)
512
+ else false
513
+ end
514
+ end
515
+
516
+ def pairwise_subset?(subs, sups)
517
+ subs.size == sups.size && subs.zip(sups).all? { |left, right| subset?(left, right) }
518
+ end
519
+
520
+ def gradual?(type)
521
+ type.is_a?(Type::Dynamic) || type.is_a?(Type::Top)
522
+ end
523
+
524
+ def subset_of_class?(sub, sup)
525
+ case sub
526
+ when Type::Constant then bare_class?(sup, literal_class_name(sub.value))
527
+ when Type::Nominal
528
+ sub.class_name == sup.class_name && (sup.type_args.empty? || pairwise_subset?(sub.type_args, sup.type_args))
529
+ when Type::Refined, Type::Difference then subset?(sub.base, sup)
530
+ when Type::Tuple then tuple_of_array?(sub, sup)
531
+ else bare_class?(sup, CARRIER_CLASSES[sub.class])
532
+ end
533
+ end
534
+
535
+ def tuple_of_array?(tuple, sup)
536
+ sup.class_name == "Array" &&
537
+ (sup.type_args.empty? || tuple.elements.all? { |element| subset?(element, sup.type_args.first) })
538
+ end
539
+
540
+ # A literal's class name. The literal's own class is exact, and its name is compared with the RBS
541
+ # name the other side spells — no constant is resolved.
542
+ def literal_class_name(value)
543
+ value.class.name
544
+ end
545
+
546
+ def bare_class?(type, name)
547
+ !name.nil? && type.class_name == name && type.type_args.empty?
548
+ end
549
+
550
+ def refinement_subset?(sub, sup)
551
+ return false unless REFINEMENT_OR_VALUE.any? { |klass| sub.is_a?(klass) }
552
+
553
+ Inference::Acceptance.accepts(sup, sub).yes?
554
+ end
555
+
556
+ # Whether the class names a disjointness proof would lean on mean what they say: every class name in
557
+ # both declared types is written absolutely, and a refinement override is a value or a built-in
558
+ # refinement (whose base is a core class), never a class-name payload, which resolves like a
559
+ # relative name.
560
+ def provable_names?(slots)
561
+ return false if @proof.nil?
562
+
563
+ slots.all? do |slot|
564
+ slot.override ? provable_override?(slot.override) : absolute_names_only?(slot.rbs_type)
565
+ end
566
+ end
567
+
568
+ def absolute_names_only?(type)
569
+ case type
570
+ when ::RBS::Types::ClassInstance, ::RBS::Types::ClassSingleton
571
+ return false unless type.name.absolute?
572
+ end
573
+ type.each_type { |inner| return false unless absolute_names_only?(inner) }
574
+ true
575
+ end
576
+
577
+ def provable_override?(type)
578
+ members = type.is_a?(Type::Union) ? type.members : [type]
579
+ members.all? { |member| REFINEMENT_OR_VALUE.any? { |klass| member.is_a?(klass) } }
580
+ end
581
+
582
+ # Whether `left` and `right` provably share no value: every pairing of their members is two distinct
583
+ # literals, a literal whose class is not below an RBS class, or two RBS classes neither of which is
584
+ # an RBS ancestor of the other. Anything else is left unproven, so the answer is false.
585
+ def disjoint?(left, right)
586
+ return false if @proof.nil?
587
+
588
+ left_atoms = atoms(left)
589
+ right_atoms = atoms(right)
590
+ return false if left_atoms.nil? || right_atoms.nil?
591
+
592
+ left_atoms.all? { |a| right_atoms.all? { |b| disjoint_atoms?(a, b) } }
593
+ end
594
+
595
+ # `[:value, v]` or `[:class, name]` per union member, or nil when a member has neither.
596
+ def atoms(type)
597
+ members = type.is_a?(Type::Union) ? type.members : [type]
598
+ result = members.map { |member| atom(member) }
599
+ result.include?(nil) ? nil : result
600
+ end
601
+
602
+ def atom(type)
603
+ case type
604
+ when Type::Constant then [:value, type.value]
605
+ when Type::Nominal then [:class, type.class_name]
606
+ when Type::Refined, Type::Difference then atom(type.base)
607
+ when Type::Tuple then [:class, "Array"]
608
+ else
609
+ name = CARRIER_CLASSES[type.class]
610
+ name && [:class, name]
611
+ end
612
+ end
613
+
614
+ def disjoint_atoms?(left, right)
615
+ case [left[0], right[0]]
616
+ when %i[value value] then !left[1].eql?(right[1])
617
+ when %i[value class] then outside_class?(left[1], right[1])
618
+ when %i[class value] then outside_class?(right[1], left[1])
619
+ else unrelated_classes?(left[1], right[1])
620
+ end
621
+ end
622
+
623
+ # A literal's own class is exact (`1` is an `Integer`, not a subclass), so it is outside `name` when
624
+ # RBS places that class nowhere below `name`.
625
+ def outside_class?(value, name)
626
+ ancestors = @proof.class_ancestors(literal_class_name(value))
627
+ !ancestors.nil? && !@proof.class_ancestors(name).nil? && !ancestors.include?(name)
628
+ end
629
+
630
+ def unrelated_classes?(left, right)
631
+ left_ancestors = @proof.class_ancestors(left)
632
+ right_ancestors = @proof.class_ancestors(right)
633
+ return false if left_ancestors.nil? || right_ancestors.nil?
634
+
635
+ !left_ancestors.include?(right) && !right_ancestors.include?(left)
636
+ end
637
+
638
+ # Folds per-position results into one verdict: any contradiction wins, then any undecided position,
639
+ # then the direction every precise position agrees on. Positions pointing both ways are undecided —
640
+ # each side says something the other does not.
641
+ def combine(results)
642
+ contradiction = results.find { |outcome, _| outcome == :contradiction }
643
+ return contradiction if contradiction
644
+
645
+ undecided = results.find { |outcome, _| outcome == :undecided }
646
+ return undecided if undecided
647
+
648
+ directions = results.map(&:first)
649
+ if directions.include?(:inline) && directions.include?(:signature)
650
+ return [:undecided, "each declaration is more precise than the other in a different position"]
651
+ end
652
+ return [:inline, nil] if directions.include?(:inline)
653
+ return SIGNATURE_MORE_PRECISE if directions.include?(:signature)
654
+
655
+ EQUAL
656
+ end
657
+
658
+ # `[return_override, {param_name => override}]` read off the MEMBER's annotations — the ones
659
+ # `RBS::Definition::Method#annotations` carries and call sites honour — with `RbsExtended`'s own
660
+ # parsers, so a refinement means here what it means at a call site.
661
+ def refinements_of(member)
662
+ strings = Array(member.annotations).map(&:string).select { |string| refinement_directive?(string) }
663
+ return [nil, {}.freeze] if strings.empty?
664
+
665
+ return_override = strings.lazy.filter_map { |string| RbsExtended.parse_return_type_override(string) }.first
666
+ params = strings.filter_map { |string| RbsExtended.parse_param_annotation(string) }
667
+ [return_override, params.to_h { |override| [override.param_name, override.type] }]
668
+ end
669
+
670
+ def return_conflict(method_types, override)
671
+ declared = method_types.map { |method_type| method_type.type.return_type }
672
+ return nil unless declared.all? { |type| exceeds?(type, override) }
673
+
674
+ "`rigor:v1:return: #{override.describe(:short)}` is disjoint from the declared return type " \
675
+ "`#{declared.map(&:to_s).uniq.join('` / `')}`"
676
+ end
677
+
678
+ def param_conflict(method_types, name, override)
679
+ declared = method_types.flat_map do |method_type|
680
+ function = method_type.type
681
+ function.is_a?(::RBS::Types::UntypedFunction) ? [] : named_params(function, name)
682
+ end
683
+ return nil if declared.empty? || !declared.all? { |param| exceeds?(param.type, override) }
684
+
685
+ "`rigor:v1:param: #{name} #{override.describe(:short)}` is disjoint from the declared parameter type " \
686
+ "`#{declared.map { |param| param.type.to_s }.uniq.join('` / `')}`"
687
+ end
688
+
689
+ def named_params(function, name)
690
+ (function.required_positionals + function.optional_positionals + function.trailing_positionals +
691
+ [function.rest_positionals, function.rest_keywords].compact +
692
+ function.required_keywords.values + function.optional_keywords.values).select { |p| p.name == name }
693
+ end
694
+
695
+ # A refinement exceeds its declared type when the two provably share no value. A top spelling holds
696
+ # every value, and a declared type Rigor cannot read faithfully proves nothing.
697
+ def exceeds?(declared, refinement)
698
+ return false if TOP_SPELLINGS.any? { |klass| declared.is_a?(klass) } || !faithful?(declared)
699
+ return false unless provable_names?([Slot.new(rbs_type: declared, override: nil)]) &&
700
+ provable_override?(refinement)
701
+
702
+ declared_type = translate(declared)
703
+ !declared_type.nil? && disjoint?(single_values(declared_type), single_values(refinement))
704
+ end
705
+ end
706
+ end
707
+ end
708
+ end