rigortype 0.3.9 → 0.4.1

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 (407) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -4
  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/08-understanding-errors.md +3 -2
  14. data/docs/handbook/10-sorbet.md +9 -10
  15. data/docs/handbook/11-sig-gen.md +454 -13
  16. data/docs/install.md +87 -15
  17. data/docs/manual/02-cli-reference.md +65 -4
  18. data/docs/manual/03-configuration.md +12 -0
  19. data/docs/manual/04-diagnostics.md +4 -0
  20. data/docs/manual/07-plugins.md +1 -1
  21. data/docs/manual/08-skills.md +4 -3
  22. data/docs/manual/10-mcp-server.md +3 -2
  23. data/docs/manual/12-caching.md +43 -4
  24. data/docs/manual/14-rails-quickstart.md +19 -11
  25. data/docs/manual/16-rbs-extended-annotations.md +40 -7
  26. data/docs/manual/19-effect-labels.md +10 -0
  27. data/docs/manual/plugins/README.md +7 -0
  28. data/docs/manual/plugins/rigor-actioncable.md +8 -1
  29. data/docs/manual/plugins/rigor-actionmailer.md +7 -0
  30. data/docs/manual/plugins/rigor-actionpack.md +243 -0
  31. data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
  32. data/docs/manual/plugins/rigor-activejob.md +7 -0
  33. data/docs/manual/plugins/rigor-activerecord.md +110 -4
  34. data/docs/manual/plugins/rigor-activestorage.md +8 -1
  35. data/docs/manual/plugins/rigor-activesupport-core-ext.md +11 -5
  36. data/docs/manual/plugins/rigor-grape.md +106 -0
  37. data/docs/manual/plugins/rigor-graphql.md +23 -2
  38. data/docs/manual/plugins/rigor-pundit.md +8 -1
  39. data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
  40. data/docs/manual/plugins/rigor-rails-routes.md +7 -0
  41. data/docs/manual/plugins/rigor-rbs-inline.md +212 -25
  42. data/docs/manual/plugins/rigor-sidekiq.md +8 -1
  43. data/docs/manual/plugins/rigor-sorbet.md +21 -5
  44. data/exe/rigor +19 -4
  45. data/lib/rigor/analysis/baseline.rb +1 -1
  46. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
  47. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
  48. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -3
  49. data/lib/rigor/analysis/check_rules/rule_ids.rb +20 -5
  50. data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
  51. data/lib/rigor/analysis/check_rules/source_arity.rb +427 -0
  52. data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
  53. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +36 -2
  54. data/lib/rigor/analysis/check_rules.rb +638 -136
  55. data/lib/rigor/analysis/dependency_recorder.rb +222 -17
  56. data/lib/rigor/analysis/fact_store.rb +9 -0
  57. data/lib/rigor/analysis/incremental.rb +26 -0
  58. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +266 -0
  59. data/lib/rigor/analysis/incremental_run_slot.rb +171 -0
  60. data/lib/rigor/analysis/incremental_session.rb +309 -53
  61. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
  62. data/lib/rigor/analysis/project_scan.rb +11 -1
  63. data/lib/rigor/analysis/reachability/plugin_roots.rb +0 -1
  64. data/lib/rigor/analysis/reachability/scan_cache.rb +3 -1
  65. data/lib/rigor/analysis/rule_catalog.rb +127 -0
  66. data/lib/rigor/analysis/run_cache_key.rb +46 -18
  67. data/lib/rigor/analysis/run_stats.rb +2 -2
  68. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +168 -12
  69. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -4
  70. data/lib/rigor/analysis/runner/pool_coordinator.rb +109 -17
  71. data/lib/rigor/analysis/runner/project_pre_passes.rb +44 -17
  72. data/lib/rigor/analysis/runner.rb +603 -54
  73. data/lib/rigor/analysis/shadow_harness.rb +27 -0
  74. data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
  75. data/lib/rigor/analysis/template_unit_collector.rb +303 -0
  76. data/lib/rigor/analysis/template_unit_paths.rb +93 -0
  77. data/lib/rigor/analysis/template_unit_positions.rb +293 -0
  78. data/lib/rigor/analysis/template_units.rb +409 -0
  79. data/lib/rigor/analysis/worker_session.rb +57 -13
  80. data/lib/rigor/bleeding_edge.rb +0 -2
  81. data/lib/rigor/builtins/hkt_builtins.rb +1 -0
  82. data/lib/rigor/builtins/imported_refinements.rb +4 -0
  83. data/lib/rigor/builtins/regex_refinement.rb +17 -9
  84. data/lib/rigor/builtins/static_return_refinements.rb +2 -0
  85. data/lib/rigor/cache/descriptor.rb +45 -1
  86. data/lib/rigor/cache/engine_source.rb +29 -1
  87. data/lib/rigor/cache/file_digest.rb +28 -0
  88. data/lib/rigor/cache/incremental_snapshot.rb +102 -8
  89. data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
  90. data/lib/rigor/cache/rbs_descriptor.rb +37 -7
  91. data/lib/rigor/cache/store.rb +63 -4
  92. data/lib/rigor/cache/value_digest.rb +325 -0
  93. data/lib/rigor/ci_detector.rb +1 -0
  94. data/lib/rigor/cli/check_command.rb +83 -13
  95. data/lib/rigor/cli/doc_links.rb +1 -1
  96. data/lib/rigor/cli/docs_command.rb +4 -4
  97. data/lib/rigor/cli/doctor_command.rb +1 -1
  98. data/lib/rigor/cli/effects_report.rb +2 -2
  99. data/lib/rigor/cli/plugin_command.rb +3 -3
  100. data/lib/rigor/cli/prism_colorizer.rb +0 -1
  101. data/lib/rigor/cli/sig_gen_command.rb +210 -29
  102. data/lib/rigor/cli/skill_command.rb +1 -1
  103. data/lib/rigor/cli/skill_describe.rb +0 -1
  104. data/lib/rigor/cli/type_of_command.rb +19 -5
  105. data/lib/rigor/cli/type_of_renderer.rb +20 -9
  106. data/lib/rigor/cli/type_of_template_probe.rb +189 -0
  107. data/lib/rigor/cli/unused_command.rb +3 -3
  108. data/lib/rigor/cli.rb +21 -3
  109. data/lib/rigor/config_audit.rb +3 -2
  110. data/lib/rigor/config_path.rb +29 -0
  111. data/lib/rigor/configuration/severity_profile.rb +24 -3
  112. data/lib/rigor/configuration.rb +71 -7
  113. data/lib/rigor/effects/ancestry_recorder.rb +191 -0
  114. data/lib/rigor/effects/attribution.rb +11 -2
  115. data/lib/rigor/effects/callee_rule.rb +368 -0
  116. data/lib/rigor/effects/catalog.rb +7 -4
  117. data/lib/rigor/effects/collector.rb +11 -5
  118. data/lib/rigor/effects/config_envelopes.rb +9 -2
  119. data/lib/rigor/effects/definition_context.rb +179 -0
  120. data/lib/rigor/effects/effect_table.rb +11 -3
  121. data/lib/rigor/effects/envelope_check.rb +1 -1
  122. data/lib/rigor/effects/envelope_index.rb +15 -0
  123. data/lib/rigor/effects/file_collection.rb +59 -5
  124. data/lib/rigor/effects/framework_units.rb +1 -1
  125. data/lib/rigor/effects/identity.rb +16 -0
  126. data/lib/rigor/effects/local_ownership.rb +38 -12
  127. data/lib/rigor/effects/method_key.rb +21 -0
  128. data/lib/rigor/effects/mutation_classifier.rb +23 -12
  129. data/lib/rigor/effects/plugin_facts.rb +49 -37
  130. data/lib/rigor/effects/propagator.rb +295 -16
  131. data/lib/rigor/effects/registry.rb +1 -1
  132. data/lib/rigor/effects/scanner.rb +121 -72
  133. data/lib/rigor/effects/signature_sources.rb +1 -1
  134. data/lib/rigor/effects/snapshot.rb +2 -1
  135. data/lib/rigor/effects/summary.rb +27 -4
  136. data/lib/rigor/effects/unit_scan.rb +385 -30
  137. data/lib/rigor/effects/visibility.rb +101 -0
  138. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
  139. data/lib/rigor/environment/lockfile_resolver.rb +27 -1
  140. data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
  141. data/lib/rigor/environment/member_consistency.rb +298 -0
  142. data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
  143. data/lib/rigor/environment/rbs_loader.rb +400 -134
  144. data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
  145. data/lib/rigor/environment.rb +104 -96
  146. data/lib/rigor/hashing/xxh3.rb +264 -0
  147. data/lib/rigor/inference/acceptance.rb +139 -9
  148. data/lib/rigor/inference/block_auto_splat.rb +216 -0
  149. data/lib/rigor/inference/block_call_timing.rb +338 -0
  150. data/lib/rigor/inference/block_parameter_binder.rb +88 -26
  151. data/lib/rigor/inference/block_repetition.rb +71 -0
  152. data/lib/rigor/inference/body_fixpoint.rb +2 -1
  153. data/lib/rigor/inference/budget_trace.rb +2 -1
  154. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  155. data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
  156. data/lib/rigor/inference/captured_locals.rb +393 -15
  157. data/lib/rigor/inference/closure_escape_analyzer.rb +157 -13
  158. data/lib/rigor/inference/content_join.rb +200 -27
  159. data/lib/rigor/inference/declaration_walk/context.rb +195 -0
  160. data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
  161. data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
  162. data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
  163. data/lib/rigor/inference/declaration_walk.rb +7 -0
  164. data/lib/rigor/inference/def_node_resolver.rb +3 -2
  165. data/lib/rigor/inference/def_return_typer.rb +11 -7
  166. data/lib/rigor/inference/define_method_block_self.rb +64 -0
  167. data/lib/rigor/inference/definer_resolution.rb +266 -0
  168. data/lib/rigor/inference/element_read_widening.rb +22 -10
  169. data/lib/rigor/inference/error_info.rb +196 -0
  170. data/lib/rigor/inference/expression_typer.rb +1665 -538
  171. data/lib/rigor/inference/external_ancestor_resolution.rb +264 -0
  172. data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
  173. data/lib/rigor/inference/global_write_census.rb +239 -0
  174. data/lib/rigor/inference/guard_rebinding.rb +488 -0
  175. data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
  176. data/lib/rigor/inference/index_write_widening.rb +16 -3
  177. data/lib/rigor/inference/indexed_narrowing.rb +61 -9
  178. data/lib/rigor/inference/jump_targets.rb +82 -0
  179. data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
  180. data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
  181. data/lib/rigor/inference/last_line.rb +340 -0
  182. data/lib/rigor/inference/last_status.rb +144 -0
  183. data/lib/rigor/inference/macro_block_self_type.rb +179 -17
  184. data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
  185. data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
  186. data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
  187. data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
  188. data/lib/rigor/inference/match_rebinding.rb +392 -0
  189. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  190. data/lib/rigor/inference/method_dispatcher/block_folding.rb +85 -24
  191. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
  192. data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
  193. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +4 -0
  194. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
  195. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +113 -73
  196. data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
  197. data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
  198. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +901 -38
  199. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
  200. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +128 -53
  201. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +1 -1
  202. data/lib/rigor/inference/method_dispatcher.rb +214 -13
  203. data/lib/rigor/inference/method_parameter_binder.rb +8 -3
  204. data/lib/rigor/inference/module_function_state.rb +213 -0
  205. data/lib/rigor/inference/multi_target_binder.rb +345 -52
  206. data/lib/rigor/inference/mutation_rejoin.rb +4 -1
  207. data/lib/rigor/inference/mutation_widening.rb +70 -48
  208. data/lib/rigor/inference/narrowing.rb +674 -124
  209. data/lib/rigor/inference/operand_effects.rb +167 -0
  210. data/lib/rigor/inference/operand_walk.rb +88 -0
  211. data/lib/rigor/inference/optimistic_origin.rb +152 -9
  212. data/lib/rigor/inference/parameter_inference_collector.rb +13 -11
  213. data/lib/rigor/inference/project_method_ownership.rb +136 -0
  214. data/lib/rigor/inference/project_patched_methods.rb +7 -2
  215. data/lib/rigor/inference/project_patched_scanner.rb +7 -3
  216. data/lib/rigor/inference/receiver_alias.rb +90 -1
  217. data/lib/rigor/inference/receiver_blind_block.rb +219 -0
  218. data/lib/rigor/inference/refinement_mutation.rb +15 -11
  219. data/lib/rigor/inference/repeated_or_writes.rb +463 -0
  220. data/lib/rigor/inference/return_barrier.rb +54 -0
  221. data/lib/rigor/inference/rewrite_mutation.rb +120 -0
  222. data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
  223. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
  224. data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
  225. data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
  226. data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
  227. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +130 -0
  228. data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
  229. data/lib/rigor/inference/scope_indexer.rb +5913 -701
  230. data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
  231. data/lib/rigor/inference/statement_evaluator.rb +3249 -497
  232. data/lib/rigor/inference/stored_block_call.rb +54 -0
  233. data/lib/rigor/inference/string_mutation.rb +44 -7
  234. data/lib/rigor/inference/unknown_store_widening.rb +200 -0
  235. data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
  236. data/lib/rigor/language_server/debouncer.rb +0 -1
  237. data/lib/rigor/language_server/diagnostic_publisher.rb +3 -2
  238. data/lib/rigor/language_server/hover_renderer.rb +3 -3
  239. data/lib/rigor/language_server/project_context.rb +5 -3
  240. data/lib/rigor/mcp/server.rb +2 -1
  241. data/lib/rigor/plugin/base.rb +196 -13
  242. data/lib/rigor/plugin/box_probe.rb +91 -0
  243. data/lib/rigor/plugin/bundled_catalog.rb +1 -1
  244. data/lib/rigor/plugin/effect_attribution.rb +58 -4
  245. data/lib/rigor/plugin/io_boundary.rb +82 -4
  246. data/lib/rigor/plugin/isolation.rb +1 -1
  247. data/lib/rigor/plugin/loader.rb +2 -1
  248. data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
  249. data/lib/rigor/plugin/manifest.rb +71 -10
  250. data/lib/rigor/plugin/registry.rb +35 -1
  251. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
  252. data/lib/rigor/plugin/template_unit.rb +196 -0
  253. data/lib/rigor/plugin/trust_policy.rb +3 -3
  254. data/lib/rigor/plugin.rb +1 -0
  255. data/lib/rigor/protection/discovery_seed.rb +20 -9
  256. data/lib/rigor/protection/kill_signature.rb +0 -1
  257. data/lib/rigor/protection/mutation_cache.rb +4 -5
  258. data/lib/rigor/rbs_extended.rb +27 -0
  259. data/lib/rigor/reflection/constant_ancestors.rb +146 -0
  260. data/lib/rigor/reflection/constant_path.rb +41 -13
  261. data/lib/rigor/reflection.rb +73 -107
  262. data/lib/rigor/scope/discovery_index.rb +351 -2
  263. data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
  264. data/lib/rigor/scope/resolution_chain.rb +884 -0
  265. data/lib/rigor/scope.rb +981 -198
  266. data/lib/rigor/sig_gen/alias_index.rb +289 -0
  267. data/lib/rigor/sig_gen/classification.rb +22 -5
  268. data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
  269. data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
  270. data/lib/rigor/sig_gen/generator.rb +580 -47
  271. data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
  272. data/lib/rigor/sig_gen/method_candidate.rb +47 -3
  273. data/lib/rigor/sig_gen/observation_collector.rb +1 -1
  274. data/lib/rigor/sig_gen/renderer.rb +124 -9
  275. data/lib/rigor/sig_gen/skip_reason_catalog.rb +44 -1
  276. data/lib/rigor/sig_gen/write_result.rb +19 -3
  277. data/lib/rigor/sig_gen/writer.rb +166 -35
  278. data/lib/rigor/sig_gen.rb +3 -0
  279. data/lib/rigor/signature_path_audit.rb +1 -1
  280. data/lib/rigor/source/node_walker.rb +0 -3
  281. data/lib/rigor/source/parameter_envelope.rb +72 -0
  282. data/lib/rigor/source.rb +1 -0
  283. data/lib/rigor/type/combinator.rb +112 -12
  284. data/lib/rigor/type/difference.rb +1 -0
  285. data/lib/rigor/type/hash_shape.rb +1 -1
  286. data/lib/rigor/type/refined.rb +1 -0
  287. data/lib/rigor/version.rb +1 -1
  288. data/lib/rigor.rb +1 -0
  289. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
  290. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +9 -5
  291. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -4
  292. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +0 -1
  293. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
  294. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
  295. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
  296. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
  297. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
  298. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
  299. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +239 -4
  300. data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
  301. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
  302. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
  303. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
  304. data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
  305. data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
  306. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -0
  307. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +9 -5
  308. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
  309. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
  310. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +380 -3
  311. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
  312. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +281 -35
  313. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
  314. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -14
  315. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
  316. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +171 -5
  317. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +2 -0
  318. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +1 -0
  319. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
  320. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +10 -0
  321. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
  322. data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
  323. data/plugins/rigor-grape/sig/grape.rbs +263 -0
  324. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +72 -2
  325. data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
  326. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -0
  327. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +11 -7
  328. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +35 -37
  329. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -1
  330. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -0
  331. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +1 -0
  332. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +9 -17
  333. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
  334. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +217 -36
  335. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +1 -0
  336. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +9 -5
  337. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +0 -1
  338. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +46 -1
  339. data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
  340. data/sig/rigor/analysis/baseline.rbs +69 -7
  341. data/sig/rigor/analysis/fact_store.rbs +1 -1
  342. data/sig/rigor/analysis/project_scan.rbs +74 -0
  343. data/sig/rigor/effects/config_envelopes.rbs +100 -0
  344. data/sig/rigor/effects/effect_table.rbs +60 -0
  345. data/sig/rigor/effects/envelope.rbs +97 -0
  346. data/sig/rigor/effects/envelope_index.rbs +33 -0
  347. data/sig/rigor/effects/file_collection.rbs +81 -0
  348. data/sig/rigor/effects/label.rbs +26 -0
  349. data/sig/rigor/effects/label_set.rbs +46 -0
  350. data/sig/rigor/effects/method_key.rbs +24 -0
  351. data/sig/rigor/effects/origin.rbs +50 -0
  352. data/sig/rigor/effects/plugin_facts.rbs +141 -0
  353. data/sig/rigor/effects/registry.rbs +68 -0
  354. data/sig/rigor/effects/summary.rbs +50 -0
  355. data/sig/rigor/effects/taint_cause.rbs +12 -0
  356. data/sig/rigor/environment.rbs +14 -10
  357. data/sig/rigor/inference/optimistic_origin.rbs +10 -0
  358. data/sig/rigor/inference.rbs +6 -4
  359. data/sig/rigor/plugin/additional_initializer.rbs +36 -0
  360. data/sig/rigor/plugin/base.rbs +30 -7
  361. data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
  362. data/sig/rigor/plugin/effect_attribution.rbs +66 -0
  363. data/sig/rigor/plugin/effect_edge.rbs +33 -0
  364. data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
  365. data/sig/rigor/plugin/io_boundary.rbs +2 -1
  366. data/sig/rigor/plugin/loader.rbs +3 -3
  367. data/sig/rigor/plugin/manifest.rbs +50 -11
  368. data/sig/rigor/plugin/protocol_contract.rbs +68 -0
  369. data/sig/rigor/plugin/registry.rbs +62 -1
  370. data/sig/rigor/plugin.rbs +1 -1
  371. data/sig/rigor/rbs_extended.rbs +1 -1
  372. data/sig/rigor/reflection.rbs +7 -6
  373. data/sig/rigor/scope.rbs +177 -20
  374. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -7
  375. data/sig/rigor/source.rbs +4 -4
  376. data/sig/rigor/testing.rbs +10 -4
  377. data/sig/rigor/type.rbs +6 -0
  378. data/sig/rigor.rbs +38 -20
  379. data/skills/rigor-ask/SKILL.md +8 -5
  380. data/skills/rigor-baseline-reduce/SKILL.md +4 -6
  381. data/skills/rigor-ci-setup/SKILL.md +17 -21
  382. data/skills/rigor-doctor/SKILL.md +24 -22
  383. data/skills/rigor-doctor/references/01-checks.md +97 -33
  384. data/skills/rigor-editor-setup/SKILL.md +6 -4
  385. data/skills/rigor-mcp-setup/SKILL.md +5 -4
  386. data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
  387. data/skills/rigor-next-steps/SKILL.md +4 -2
  388. data/skills/rigor-plugin-author/SKILL.md +19 -23
  389. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
  390. data/skills/rigor-plugin-author/references/02-walker-and-types.md +12 -25
  391. data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
  392. data/skills/rigor-plugin-review/SKILL.md +6 -4
  393. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
  394. data/skills/rigor-plugin-tune/SKILL.md +4 -2
  395. data/skills/rigor-project-init/SKILL.md +46 -18
  396. data/skills/rigor-project-init/references/01-detect.md +13 -9
  397. data/skills/rigor-project-init/references/02-configure.md +40 -10
  398. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
  399. data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
  400. data/skills/rigor-protection-uplift/SKILL.md +4 -6
  401. data/skills/rigor-rbs-setup/SKILL.md +4 -2
  402. data/skills/rigor-type-oracle/SKILL.md +4 -6
  403. data/skills/rigor-type-oracle/references/01-oracle-commands.md +12 -8
  404. data/skills/rigor-type-oracle/references/03-gap-protocol.md +0 -3
  405. data/skills/rigor-unused-adjudicate/SKILL.md +4 -2
  406. data/skills/rigor-upgrade/SKILL.md +13 -8
  407. metadata +132 -1
@@ -0,0 +1,427 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../reflection"
4
+ require_relative "../../inference/definer_resolution"
5
+ require_relative "../../source/parameter_envelope"
6
+
7
+ module Rigor
8
+ module Analysis
9
+ module CheckRules
10
+ # Issue #992 — the positional envelope `call.wrong-arity` may check a call against when no signature
11
+ # declares the method: the {Source::ParameterEnvelope} the project's own `def` records in
12
+ # `Scope::DiscoveryIndex#discovered_parameter_envelopes`, and only when nothing the project can see
13
+ # could make a different definition of that name the one that runs.
14
+ #
15
+ # Asked in two stages, because every condition past the first can only WITHHOLD a firing:
16
+ #
17
+ # 1. {#owner_envelope} walks the receiver's superclass chain to the first class whose own body, or one of
18
+ # whose project mixins, records the name. Every record at that level must agree — deliberately wider
19
+ # than Ruby's order, so a mixin whose definition may shadow the class's own, or a load-order-dependent
20
+ # pair (#986), declines instead of answering — and none may be opaque. A call that fits this envelope
21
+ # is silent, and nothing below is consulted.
22
+ # 2. {#authoritative?} runs only for a call that would fire. It declines when a `method_missing` /
23
+ # `respond_to_missing?` hook or a dynamic-surface mark sits anywhere in the chain; when a mixin the
24
+ # project does not declare is not a known RBS module that lacks the name; when a `pre_eval:` patch or
25
+ # a plugin's synthetic method names it; when the owner does not make the method public; and when any
26
+ # project subclass of the receiver — whose instances the receiver's type admits — records a different
27
+ # or opaque envelope for it.
28
+ #
29
+ # ADR-119 C1b / C2-c — on both sides stage 1 and the subclass check ask `Inference::DefinerResolution`, and a
30
+ # chain it declines (`UNKNOWN`) answers no envelope: where Ruby's order depends on a fact the tables cannot
31
+ # prove, the rule is silent instead of checking the call against master's order (#1570, #1607).
32
+ #
33
+ # Deliberately NOT in scope: a constructor reached through `Class#new` (nothing records `initialize` as
34
+ # `[:singleton, :new]`, so `.new` finds no owner unless the class writes `def self.new` itself); keyword
35
+ # arity (the envelope's required-keyword flag declines, as `compute_arity_envelope` declines an RBS
36
+ # function with required keywords); and a `class_eval` whose receiver is not a constant, which the
37
+ # discovery walk cannot name.
38
+ class SourceArity # rubocop:disable Metrics/ClassLength
39
+ MISSING_HOOKS = %i[method_missing respond_to_missing?].freeze
40
+ private_constant :MISSING_HOOKS
41
+
42
+ # One class on the walk: its own name, the project modules its mixins resolve to (transitively through
43
+ # their own `include`s), and the candidate-name lists of the mixins that resolve to no project module.
44
+ #
45
+ # Issue #1570 — the level is a SEGMENT of the receiver's `Scope::ResolutionChain` (`:arity` flavor): the
46
+ # class with the modules Ruby inserts around it before the superclass's entries begin. Ruby skips an
47
+ # `include` of a module the superclass chain already carries, so `class C < Base; include M` where
48
+ # `Base` includes `M` leaves `M` at `Base`'s level; collecting every module a class's own `include`s
49
+ # name put it at `C`'s level instead, where its `def` answered a call `Base#foo` receives.
50
+ Level = Data.define(:class_name, :modules, :externals)
51
+ private_constant :Level
52
+
53
+ # ADR-119 C1b — the answer of a level whose records are not one non-opaque envelope. It equals itself in
54
+ # every world, so two worlds that both reach such a level agree, and no envelope equals it.
55
+ AMBIGUOUS = Object.new.freeze
56
+ private_constant :AMBIGUOUS
57
+
58
+ # What `Inference::DefinerResolution` reads off a candidate: the answer, the entry that gave it, its
59
+ # position on the chain and its side. The same four readers as `DefinerResolution::Hit`, which a call site
60
+ # may not name (the case/in contract spec allows no other reference to the module).
61
+ ArityHit = Data.define(:answer, :owner, :index, :side)
62
+ private_constant :ArityHit
63
+
64
+ # A subclass level the walk cannot read: `subclasses_agree?` reads the nil as a decline.
65
+ NO_LEVELS = [nil].freeze
66
+ private_constant :NO_LEVELS
67
+
68
+ def initialize(scope, method_name, kind)
69
+ @scope = scope
70
+ @method_name = method_name.to_sym
71
+ @kind = kind
72
+ @name_memo = {}
73
+ end
74
+
75
+ # The nearest definition's envelope, or nil when there is none or it is not one shape. Remembers the
76
+ # levels it walked for {#authoritative?}.
77
+ #
78
+ # Its ADR-46 reads are withheld ({Analysis::DependencyRecorder.withhold}) until the caller settles
79
+ # the verdict with {#settle_by_definitions} or {#settle_by_walk}.
80
+ def owner_envelope(class_name)
81
+ @owner_entries = []
82
+ @ambiguous = false
83
+ @unknown = false
84
+ envelope, @withheld = Analysis::DependencyRecorder.withhold { walk_to_owner(class_name) }
85
+ envelope
86
+ end
87
+
88
+ # True when the nearest level recorded the name but not as one envelope.
89
+ def ambiguous? = @ambiguous
90
+
91
+ # For a silent verdict that only a definition can overturn — the call fits, the owner takes a required
92
+ # keyword, or nothing on the chain records the name: the owner `def`s themselves (their symbol edges)
93
+ # and a definition appearing at a level the walk passed over (a name-keyed negative edge). A mixin or
94
+ # reopening added at the owner's level makes it opaque, and everything {#authoritative?} reads can only
95
+ # withhold, so none of these needs the file-level class edge the bucket reads would otherwise record.
96
+ def settle_by_definitions
97
+ return if @withheld.nil?
98
+ # ADR-119 C1b — a declined chain (`@unknown`) that some level records the name on is silent only until
99
+ # another file's edit lifts the decline, so its verdict depends on everything the walk read, as an opaque
100
+ # level's does. A decline over a name no project level records is not `@unknown` (see
101
+ # {#decline_unless_recorded_nowhere}): it settles by definitions, as an `ABSENT` read does.
102
+ return settle_by_walk if @unknown
103
+
104
+ @owner_entries.each do |name, kind|
105
+ if kind == :singleton
106
+ @scope.user_singleton_def_site_for(name, @method_name)
107
+ else
108
+ @scope.user_def_site_for(name, @method_name)
109
+ end
110
+ end
111
+ separator = @kind == :singleton ? "." : "#"
112
+ @passed.each { |name| Analysis::DependencyRecorder.read_missing(:method, "#{name}#{separator}#{@method_name}") }
113
+ end
114
+
115
+ # For every other verdict — a firing, or an opaque owner level whose opacity another file's edit can
116
+ # lift — the walk's reads are the dependency, so they are recorded as read.
117
+ def settle_by_walk
118
+ Analysis::DependencyRecorder.replay(@withheld) unless @withheld.nil?
119
+ end
120
+
121
+ # Stage 2, for the class {#owner_envelope} last answered.
122
+ def authoritative?(class_name)
123
+ !load_order_dependent?(class_name) && !object_extension_may_shadow? &&
124
+ @levels.all? { |level| clean_level?(level) && public_at?(level.class_name) } &&
125
+ chain_free_of_hooks?(class_name) &&
126
+ subclasses_agree?(class_name, @envelope)
127
+ end
128
+
129
+ private
130
+
131
+ # ADR-119 C1b / C2-c, #1570 / #1607 — the nearest level is asked of {Inference::DefinerResolution}, on the
132
+ # receiver's own side: the chain stands only where every candidate level gives one envelope in every
133
+ # world Ruby may have run (a fork, an unproven mixin order, a `possible`-only definer, and on the
134
+ # singleton side a hook that could have put a definer ahead). Where it does not, the answer is NO
135
+ # envelope and `@unknown` — the rule stays silent — instead of master's order, which is what #1570 and
136
+ # #1607 fired on. The levels the walk keeps for {#authoritative?} are the chain's.
137
+ def walk_to_owner(class_name)
138
+ @levels = []
139
+ @passed = []
140
+ @recorded = false
141
+
142
+ case Inference::DefinerResolution.resolve(@scope, class_name, @method_name, side,
143
+ question: :arity) { |chain, from| arity_hit(chain, from) }
144
+ in Inference::DefinerResolution::Known(answer: _answer, owner: _owner) then nearest_envelope(class_name)
145
+ in Inference::DefinerResolution::ABSENT then nearest_envelope(class_name) # rubocop:disable Lint/DuplicateBranch
146
+ in Inference::DefinerResolution::UNKNOWN then decline_unless_recorded_nowhere(class_name)
147
+ end
148
+ end
149
+
150
+ # A declined read of a name no project level records (`arity_hit` found no level that does, in any world it
151
+ # was asked) answers no envelope whether or not it stands, so the walk's reads are not the verdict's
152
+ # dependency: only a `def` appearing can change it, which {#settle_by_definitions} files, as for an
153
+ # `ABSENT` read. It can leave a stale false negative in a warm run, never a firing. Without this, every
154
+ # `Widget.new` (the implicit `Class` tail may answer it) would replay the whole walk as a file-level
155
+ # class edge.
156
+ def decline_unless_recorded_nowhere(class_name)
157
+ nowhere = !@recorded && !arity_chain(class_name).truncated?
158
+ nowhere ? nearest_envelope(class_name) : decline
159
+ end
160
+
161
+ # The chain's nearest level's envelope. `Known#answer` is that envelope, but {#authoritative?} and
162
+ # {#settle_by_definitions} also read the levels, passed names and owner entries this walk fills, so the
163
+ # answer is not used on its own. Run only once the candidate-set read has answered, so the chain stands
164
+ # for the name.
165
+ def nearest_envelope(class_name) = owner_in(chain_levels(arity_chain(class_name)).first)
166
+
167
+ def decline
168
+ @levels = []
169
+ @passed = []
170
+ @owner_entries = []
171
+ @envelope = nil
172
+ @unknown = true
173
+ nil
174
+ end
175
+
176
+ # The first level at or after entry position `from` whose records name the method: its envelope (or
177
+ # {AMBIGUOUS}), the entry that recorded it and that entry's position and side on the chain. On the
178
+ # singleton side the class's own record is a `:singleton` entry and an extended module's an `:instance`
179
+ # one (`owner_entries` names the kind of each).
180
+ def arity_hit(chain, from)
181
+ start = 0
182
+ chain.levels.each_with_index do |raw, index|
183
+ level = Level.new(*raw)
184
+ entries = chain.level_entries(index)
185
+ first = start
186
+ start += entries.size
187
+ next if first < from
188
+
189
+ found = level_envelopes(level)
190
+ next if found.empty?
191
+
192
+ envelope = found.first
193
+ answer = found.all?(envelope) && !Source::ParameterEnvelope.opaque?(envelope) ? envelope : AMBIGUOUS
194
+ owner, owner_kind = owner_entries(level).first
195
+ position = first + (entries.index { |entry| entry.name == owner && entry.side == owner_kind } || 0)
196
+ @recorded = true
197
+ return ArityHit.new(answer, owner, position, owner_kind)
198
+ end
199
+ nil
200
+ end
201
+
202
+ def owner_in(levels)
203
+ levels.each do |level|
204
+ @levels << level
205
+ found = level_envelopes(level)
206
+ if found.empty?
207
+ @passed.concat([level.class_name] + level.modules)
208
+ next
209
+ end
210
+
211
+ envelope = found.first
212
+ @owner_entries |= owner_entries(level)
213
+ unless found.all?(envelope) && !Source::ParameterEnvelope.opaque?(envelope)
214
+ @ambiguous = true
215
+ return nil
216
+ end
217
+
218
+ return @envelope = envelope
219
+ end
220
+ nil
221
+ end
222
+
223
+ def owner_entries(level)
224
+ own = @scope.parameter_envelopes_of(level.class_name).key?([@kind, @method_name])
225
+ entries = own ? [[level.class_name, @kind]] : []
226
+ level.modules.each do |mod|
227
+ entries << [mod, :instance] if @scope.parameter_envelopes_of(mod).key?([:instance, @method_name])
228
+ end
229
+ entries
230
+ end
231
+
232
+ # The survey corpus's own lesson. `PStore.new(path).transaction { … }` inside `module TDiary::IO`
233
+ # resolves `PStore` to the project's `TDiary::IO::PStore` when that class's file is loaded, and to the
234
+ # stdlib `::PStore` when it is not — and tdiary loads it only under one `io_class` setting, so under
235
+ # the default one the call is correct code. The analysis universe holds every file at once, so it
236
+ # always picks the inner class. Whenever the receiver's class is one of SEVERAL known classes the
237
+ # call site's `Module.nesting` spells with the same last segment, which one the receiver is depends
238
+ # on load order, and a verdict against the project class's `def` is not one the program backs.
239
+ def load_order_dependent?(class_name)
240
+ segment = class_name.to_s.split("::").last
241
+ candidates = Rigor::Reflection.lexical_nesting_chain(@scope).map { |entry| "#{entry}::#{segment}" }
242
+ candidates << segment
243
+ known = candidates.uniq.select { |name| known_class?(name) }
244
+ known.include?(class_name.to_s) && known.size > 1
245
+ end
246
+
247
+ # A module some method body hands to `obj.extend` sits ahead of the object's class, so if it defines
248
+ # the name at all, that object answers with the module's definition.
249
+ def object_extension_may_shadow?
250
+ @scope.discovered_parameter_envelopes.any? do |_name, bucket|
251
+ bucket.key?(Scope::DiscoveryIndex::ENVELOPE_OBJECT_EXTENDED_MARK) && bucket.key?([:instance, @method_name])
252
+ end
253
+ end
254
+
255
+ def known_class?(name)
256
+ @scope.discovered_classes.key?(name) || Rigor::Reflection.rbs_class_known?(name, scope: @scope)
257
+ end
258
+
259
+ # Yields each level of `class_name`'s chain. False when the walk stopped at the ADR-41 budget rather than
260
+ # ending: budget exhaustion is uncertainty, and every caller reads it as a reason to decline.
261
+ def walked_whole_chain?(class_name, &)
262
+ levels, whole = chain_levels(arity_chain(class_name))
263
+ levels.each(&)
264
+ whole
265
+ end
266
+
267
+ # `[levels, whole]` for one world of the chain, up to the first superclass the project does not declare.
268
+ def chain_levels(chain) = [chain.levels.map { |level| Level.new(*level) }, !chain.truncated?]
269
+
270
+ def side = @kind == :singleton ? :singleton : :instance
271
+
272
+ # Instance methods reach a receiver through `include` / `prepend`; class methods through `extend`, and
273
+ # an extended module's own `include`s reach the same singleton — the chain's singleton side.
274
+ #
275
+ # Issue #986 — the `:arity` flavor expands a name the compact-header rename collision left ambiguous
276
+ # to BOTH classes it names: both are ancestors at runtime and only their MRO order is unknowable. Taking
277
+ # both into the level is what keeps this rule alive for the rest of the receiver: a method only one of
278
+ # them declares still has one envelope and still fires, a method they disagree about lands two on the
279
+ # level's join and declines there, and the class's own `def`s, its superclass's and its unambiguous
280
+ # mixins are untouched. Letting the decline arrive as an unknown EXTERNAL mixin instead suppressed the
281
+ # rule for every level of the receiver.
282
+ def arity_chain(class_name) = Scope::ResolutionChain.for(@scope, class_name.to_s, side, :arity)
283
+
284
+ def resolve(owner, raw)
285
+ return nil if raw.nil?
286
+
287
+ bucket = (@name_memo[owner] ||= {})
288
+ return bucket[raw] if bucket.key?(raw)
289
+
290
+ bucket[raw] = @scope.ancestor_name_candidates(owner, raw).find { |name| project_class?(name) }
291
+ end
292
+
293
+ def project_class?(name)
294
+ @scope.known_user_class?(name) || @scope.discovered_parameter_envelopes.key?(name)
295
+ end
296
+
297
+ # A module's methods reach the receiver as instance methods whichever side the receiver is on.
298
+ def level_envelopes(level)
299
+ own = @scope.parameter_envelopes_of(level.class_name)[[@kind, @method_name]]
300
+ mixed = level.modules.map { |mod| @scope.parameter_envelopes_of(mod)[[:instance, @method_name]] }
301
+ ([own] + mixed).compact
302
+ end
303
+
304
+ def clean_level?(level)
305
+ ([level.class_name] + level.modules).none? { |name| dynamic_surface?(name) || project_patched?(name) } &&
306
+ level.externals.all? { |candidates| external_mixin_lacks_method?(candidates) }
307
+ end
308
+
309
+ def dynamic_surface?(class_name)
310
+ Scope::DiscoveryIndex.rewritten_surface?(@scope.parameter_envelopes_of(class_name))
311
+ end
312
+
313
+ def project_patched?(class_name)
314
+ environment = @scope.environment
315
+ patched = environment&.project_patched_methods
316
+ return true if patched && !patched.empty? && patch_names_method?(patched, class_name)
317
+
318
+ synthetic = environment&.synthetic_method_index
319
+ return false if synthetic.nil? || synthetic.empty?
320
+
321
+ synthetic.knows_class?(class_name) || !synthetic.lookup_instance(class_name, @method_name).empty? ||
322
+ !synthetic.lookup_singleton(class_name, @method_name).empty?
323
+ end
324
+
325
+ def patch_names_method?(patched, class_name)
326
+ %i[instance singleton].any? do |kind|
327
+ !patched.lookup(class_name: class_name, method_name: @method_name, kind: kind).nil?
328
+ end
329
+ end
330
+
331
+ # A mixin the project does not declare may define the name with any arity, and `prepend` lets it win
332
+ # over the class's own `def`. Only a module RBS knows, and whose declaration lacks the name, is
333
+ # evidence that it does not.
334
+ def external_mixin_lacks_method?(candidates)
335
+ name = candidates.find { |candidate| Rigor::Reflection.rbs_class_known?(candidate, scope: @scope) }
336
+ return false if name.nil?
337
+
338
+ Rigor::Reflection.instance_method_definition(name, @method_name, scope: @scope).nil?
339
+ rescue StandardError
340
+ false
341
+ end
342
+
343
+ def public_at?(class_name)
344
+ visibility = @scope.discovered_method_visibility(class_name, @method_name)
345
+ visibility.nil? || visibility == :public
346
+ end
347
+
348
+ # A hook anywhere in the chain, including above the owner, and on either side: a class that answers
349
+ # names it does not define is a class whose `def`s are not the whole story of what a call reaches.
350
+ def chain_free_of_hooks?(class_name)
351
+ walked_whole_chain?(class_name) do |level|
352
+ ([level.class_name] + level.modules).each do |name|
353
+ envelopes = @scope.parameter_envelopes_of(name)
354
+ return false if dynamic_surface?(name)
355
+ return false if MISSING_HOOKS.any? { |hook| hook_recorded?(envelopes, hook) }
356
+ end
357
+ end
358
+ end
359
+
360
+ def hook_recorded?(envelopes, hook)
361
+ envelopes.key?([:instance, hook]) || envelopes.key?([:singleton, hook])
362
+ end
363
+
364
+ # Every project class whose superclass chain reaches the receiver's class: a value typed as the
365
+ # receiver may be any of them, and each one's own definition of the name is what runs for it.
366
+ def subclasses_agree?(class_name, envelope)
367
+ each_subclass(class_name) do |subclass|
368
+ subclass_levels(subclass).each do |level|
369
+ # A subclass whose own level the budget cut cannot be read, which is a reason to decline.
370
+ return false if level.nil?
371
+ return false unless clean_level?(level)
372
+ return false unless level_envelopes(level).all?(envelope)
373
+ end
374
+ end
375
+ true
376
+ end
377
+
378
+ # A subclass's own level (nil where the budget cut it or the chain does not stand for the name). It asks
379
+ # {Inference::DefinerResolution} on the receiver's side (ADR-119 C1b, C2-c): the subclass's own level
380
+ # answers, and a fork, an unproven mixin order, a `possible`-only definer or a hook there is `UNKNOWN`,
381
+ # which `subclasses_agree?` reads as a reason to decline.
382
+ def subclass_levels(subclass)
383
+ case Inference::DefinerResolution.resolve(@scope, subclass, @method_name, side, question: :arity,
384
+ &own_level_answer(subclass))
385
+ in Inference::DefinerResolution::Known(answer: _answer, owner: _owner) then [first_level(subclass)]
386
+ in Inference::DefinerResolution::ABSENT then [nil]
387
+ in Inference::DefinerResolution::UNKNOWN then NO_LEVELS
388
+ end
389
+ end
390
+
391
+ # The subclass's own level, always a candidate (an empty answer where it records nothing), so that the
392
+ # read asks whether the chain stands for the subclass and never whether an ancestor defines the name.
393
+ def own_level_answer(subclass)
394
+ lambda do |chain, from|
395
+ next nil unless from.zero? && !chain.levels.empty?
396
+
397
+ level = Level.new(*chain.levels.first)
398
+ ArityHit.new(level_envelopes(level).uniq, subclass, 0, side)
399
+ end
400
+ end
401
+
402
+ def first_level(class_name) = chain_levels(arity_chain(class_name)).first.first
403
+
404
+ def each_subclass(class_name)
405
+ children = children_by_parent
406
+ queue = children.fetch(class_name.to_s, []).dup
407
+ seen = {}
408
+ until queue.empty?
409
+ subclass = queue.shift
410
+ next if seen[subclass]
411
+
412
+ seen[subclass] = true
413
+ yield subclass
414
+ queue.concat(children.fetch(subclass, []))
415
+ end
416
+ end
417
+
418
+ def children_by_parent
419
+ @scope.discovered_superclasses.each_with_object({}) do |(child, raw_parent), children|
420
+ parent = resolve(child, raw_parent)
421
+ (children[parent] ||= []) << child if parent
422
+ end
423
+ end
424
+ end
425
+ end
426
+ end
427
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ module Rigor
6
+ module Analysis
7
+ module CheckRules
8
+ # Issue #1367 (ADR-117 Decision point 1, WD2) — what the interpreter's setter for each special global accepts:
9
+ # the envelope `global.write-type-mismatch` and `global.readonly-write` judge a write against.
10
+ #
11
+ # The setter is the authority, not the RBS declaration of the global. Where the two differ, the setter wins:
12
+ # `$;` is declared `Regexp | String | nil`, but its setter also takes any object with `to_str`, and `$stdout` is
13
+ # declared `IO`, but its setter takes anything that responds to `write`. Each entry names the CRuby function
14
+ # that enforces it and was confirmed on Ruby 4.0.5, whose error the comment quotes.
15
+ #
16
+ # A special whose setter accepts every value is absent, so a write to it is never checked: `$stdin`
17
+ # (ADR-117 WD2), `$_`, `$VERBOSE` / `$-v` / `$-w`, `$DEBUG` / `$-d` and `$=`. So is `$@`, whose setter raises
18
+ # `ArgumentError` or `TypeError` depending on whether `$!` is set when it runs. `$&`, `` $` ``, `$'`, `$+`
19
+ # and `$1`..`$9` cannot be assigned at all: that is a syntax error, not a write.
20
+ module SpecialGlobalSetters
21
+ # A setter that raises `TypeError` unless the value is an instance of one of `classes` (a subclass counts,
22
+ # since the setter tests the object's built-in type) or, when `conversion` names a method, responds to it:
23
+ # the implicit conversion the setter calls (`to_str`, `to_int`) or the method it asks `respond_to?` about
24
+ # (`write`). `accepts` is the phrase the diagnostic quotes.
25
+ Contract = Data.define(:classes, :conversion, :accepts) do
26
+ # Whether an instance of the core class `class_name` is one the setter takes by type. Both sides are core
27
+ # classes, whose hierarchy is Ruby's own, so the running interpreter answers it.
28
+ def accepts_class?(class_name)
29
+ klass = ::Object.const_get(class_name)
30
+ classes.any? { |accepted| klass <= ::Object.const_get(accepted) }
31
+ end
32
+ end
33
+
34
+ # `rb_str_setter` (string.c), reached through `rb_deprecated_str_setter` and io.c's `deprecated_rs_setter`,
35
+ # tests `T_STRING` and calls no conversion: `$/ = 1` raises "value of $/ must be String", and so do a
36
+ # `Regexp` and an object with `to_str`.
37
+ STRING_OR_NIL = Contract.new(classes: %w[String NilClass].freeze, conversion: nil, accepts: "a String or nil")
38
+ # `rb_fs_setter` (string.c): nil, a String, a Regexp, or an object with `to_str` (`rb_fs_check`).
39
+ # `$; = 1` raises "value of $; must be String or Regexp".
40
+ FIELD_SEPARATOR = Contract.new(
41
+ classes: %w[String Regexp NilClass].freeze, conversion: :to_str,
42
+ accepts: "a String, a Regexp, nil, or an object with `to_str'"
43
+ )
44
+ # `match_setter` (re.c): nil or a MatchData (`Check_Type(val, T_MATCH)`). `$~ = 1` raises
45
+ # "wrong argument type Integer (expected MatchData)".
46
+ MATCH_DATA = Contract.new(classes: %w[MatchData NilClass].freeze, conversion: nil,
47
+ accepts: "a MatchData or nil")
48
+ # `set_arg0` (ruby.c) calls `StringValueCStr`: a String or an object with `to_str`, and not nil. `$0 = 1`
49
+ # raises "no implicit conversion of Integer into String".
50
+ PROGRAM_NAME = Contract.new(
51
+ classes: %w[String].freeze, conversion: :to_str, accepts: "a String or an object with `to_str'"
52
+ )
53
+ # `argf_lineno_setter` (io.c) calls `NUM2INT`: an Integer, a Float, or an object with `to_int`, and not nil.
54
+ # `$. = "1"` raises "no implicit conversion of String into Integer".
55
+ LINE_NUMBER = Contract.new(
56
+ classes: %w[Integer
57
+ Float].freeze, conversion: :to_int, accepts: "an Integer, a Float, or an object with `to_int'"
58
+ )
59
+ # `opt_i_set` (io.c) through `argf_inplace_mode_set`: nil or false turns in-place mode off; anything else
60
+ # goes through `StringValueCStr`. `$-i = 1` raises "no implicit conversion of Integer into String".
61
+ INPLACE_MODE = Contract.new(
62
+ classes: %w[String NilClass FalseClass].freeze, conversion: :to_str,
63
+ accepts: "a String, nil, false, or an object with `to_str'"
64
+ )
65
+ # `stdout_setter` / `stderr_setter` (io.c) call `must_respond_to(id_write, …)`, which asks `respond_to?` and so
66
+ # honours `respond_to_missing?`. `$stdout = 1` raises "$stdout must have write method, Integer given".
67
+ # ADR-117 WD2: this `_Writer` requirement is the only contract a stream write is checked against.
68
+ WRITER = Contract.new(classes: [].freeze, conversion: :write, accepts: "an object that responds to `write'")
69
+
70
+ CONTRACTS = {
71
+ "$/": STRING_OR_NIL,
72
+ "$-0": STRING_OR_NIL,
73
+ "$,": STRING_OR_NIL,
74
+ "$\\": STRING_OR_NIL,
75
+ "$;": FIELD_SEPARATOR,
76
+ "$-F": FIELD_SEPARATOR,
77
+ "$~": MATCH_DATA,
78
+ "$0": PROGRAM_NAME,
79
+ "$PROGRAM_NAME": PROGRAM_NAME,
80
+ "$.": LINE_NUMBER,
81
+ "$-i": INPLACE_MODE,
82
+ "$stdout": WRITER,
83
+ "$>": WRITER,
84
+ "$stderr": WRITER
85
+ }.freeze
86
+
87
+ # The specials whose setter is `rb_gvar_readonly_setter` — declared with `rb_define_readonly_variable`, as a
88
+ # virtual variable without a setter, or with that setter by name. Any write raises `NameError`
89
+ # ("$! is a read-only variable"), whatever the value.
90
+ #
91
+ # - `$!` (eval.c); `$$` and `$?` (process.c).
92
+ # - `$<`, `$FILENAME` and `$*` (io.c, ARGF).
93
+ # - `$:` / `$LOAD_PATH` / `$-I`, `$"` / `$LOADED_FEATURES` (load.c).
94
+ # - `$-W`, and the `-p` / `-l` / `-a` switches `$-p`, `$-l` and `$-a` (ruby.c).
95
+ READ_ONLY = Set[
96
+ :$!, :$$, :$?, :$<, :$FILENAME, :$*, :$:, :$LOAD_PATH, :$-I, :$", :$LOADED_FEATURES,
97
+ :$-W, :$-p, :$-l, :$-a
98
+ ].freeze
99
+
100
+ # The literal nodes `global.write-type-mismatch` judges, by the core class of the object each evaluates to.
101
+ # An interpolated String, Symbol or Regexp is still a new instance of that class.
102
+ LITERAL_CLASSES = {
103
+ Prism::IntegerNode => "Integer", Prism::FloatNode => "Float", Prism::RationalNode => "Rational",
104
+ Prism::ImaginaryNode => "Complex", Prism::StringNode => "String", Prism::InterpolatedStringNode => "String",
105
+ Prism::SymbolNode => "Symbol", Prism::InterpolatedSymbolNode => "Symbol", Prism::ArrayNode => "Array",
106
+ Prism::HashNode => "Hash", Prism::RegularExpressionNode => "Regexp",
107
+ Prism::InterpolatedRegularExpressionNode => "Regexp", Prism::NilNode => "NilClass",
108
+ Prism::TrueNode => "TrueClass", Prism::FalseNode => "FalseClass"
109
+ }.freeze
110
+
111
+ # How the diagnostic names a literal of each class.
112
+ LITERAL_DESCRIPTIONS = { "NilClass" => "nil", "TrueClass" => "true", "FalseClass" => "false" }.freeze
113
+
114
+ module_function
115
+
116
+ # The setter contract a write of `name` is checked against, or nil when its setter accepts every value or the
117
+ # global is no special.
118
+ def contract_for(name)
119
+ CONTRACTS[name]
120
+ end
121
+
122
+ # The core class of the object `node` evaluates to when it is a literal ({LITERAL_CLASSES}), looking
123
+ # through parentheses around a single expression, or nil for any other expression.
124
+ def literal_class(node)
125
+ while node.is_a?(Prism::ParenthesesNode)
126
+ body = node.body
127
+ return nil unless body.is_a?(Prism::StatementsNode) && body.body.size == 1
128
+
129
+ node = body.body.first
130
+ end
131
+ LITERAL_CLASSES[node.class]
132
+ end
133
+
134
+ def literal_description(class_name)
135
+ LITERAL_DESCRIPTIONS.fetch(class_name) do
136
+ "#{class_name.start_with?('A', 'E', 'I', 'O', 'U') ? 'an' : 'a'} #{class_name} literal"
137
+ end
138
+ end
139
+
140
+ def read_only?(name)
141
+ READ_ONLY.include?(name)
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
@@ -3,6 +3,7 @@
3
3
  require "prism"
4
4
 
5
5
  require_relative "../../source/node_children"
6
+ require_relative "../../inference/narrowing"
6
7
  require_relative "inferred_param_guard"
7
8
 
8
9
  module Rigor
@@ -29,6 +30,12 @@ module Rigor
29
30
  # The false-positive envelope mirrors `flow.always-truthy-condition`: clauses inside loops / blocks are
30
31
  # skipped (mutation tracking through those is incomplete), and the rule reads the engine's own
31
32
  # narrowing rather than recomputing it, so the diagnostic and the body typing can never diverge.
33
+ #
34
+ # Issue #1429 — a `:disjoint` clause is not reported when its `Bot` rests on a `Nominal` subject member whose
35
+ # class is disjoint from a condition's ({Inference::Narrowing.disjoint_nominal_guard?}): the `when C` or `in C`
36
+ # is code evidence that the subject can hold such an object (`io = STDOUT; case io when StringIO`), and Rigor's
37
+ # reading of the subject's type is what the evidence contradicts. The body scope stays `Bot`, so no call on the
38
+ # subject in it is checked. A subject that is a literal carrier (`x = 1; case x when String`) is still reported.
32
39
  class UnreachableClauseCollector
33
40
  LOOP_OR_BLOCK_NODE_CLASSES = [
34
41
  Prism::WhileNode, Prism::UntilNode, Prism::ForNode, Prism::BlockNode
@@ -139,13 +146,30 @@ module Rigor
139
146
  return if scope.nil?
140
147
  return unless scope.local(subject_name).is_a?(Type::Bot)
141
148
 
149
+ kind = when_clause_kind(clause, subject_name)
150
+ return if kind == :disjoint &&
151
+ disjoint_nominal_subject?(@scope_index[clause.conditions.first], subject_name, clause.conditions)
152
+
142
153
  @results << Result.new(
143
154
  clause: clause, body: clause.statements, subject_name: subject_name,
144
155
  condition_source: clause.conditions.map(&:slice).join(", "),
145
- kind: when_clause_kind(clause, subject_name), keyword: "when"
156
+ kind: kind, keyword: "when"
146
157
  )
147
158
  end
148
159
 
160
+ # True when the subject, as `entry` binds it, holds a non-literal `Nominal` whose class the class one of
161
+ # `patterns` names is disjoint from (issue #1429, see the class comment).
162
+ def disjoint_nominal_subject?(entry, subject_name, patterns)
163
+ subject_type = entry&.local(subject_name)
164
+ return false if subject_type.nil?
165
+
166
+ patterns.any? do |condition|
167
+ class_name = Inference::Narrowing.lexical_class_name(condition, entry)
168
+ class_name &&
169
+ Inference::Narrowing.disjoint_nominal_guard?(subject_type, class_name, environment: entry.environment)
170
+ end
171
+ end
172
+
149
173
  # A dead `when` is `:prior_exhaustion` when the subject was already narrowed to `bot` BEFORE this
150
174
  # clause (an earlier clause covered it) — read from the entry scope the engine recorded on the
151
175
  # clause's first condition node (ADR-47 WD2). Otherwise the subject was still concrete entering the
@@ -170,13 +194,23 @@ module Rigor
170
194
  return if scope.nil?
171
195
  return unless scope.local(subject_name).is_a?(Type::Bot)
172
196
 
197
+ kind = in_clause_kind(clause, subject_name)
198
+ return if kind == :disjoint && disjoint_nominal_pattern?(clause, subject_name)
199
+
173
200
  @results << Result.new(
174
201
  clause: clause, body: clause.statements, subject_name: subject_name,
175
202
  condition_source: clause.pattern.slice,
176
- kind: in_clause_kind(clause, subject_name), keyword: "in"
203
+ kind: kind, keyword: "in"
177
204
  )
178
205
  end
179
206
 
207
+ # {#disjoint_nominal_subject?} for a bare class pattern, `in C` or `in C => x`.
208
+ def disjoint_nominal_pattern?(clause, subject_name)
209
+ pattern = clause.pattern
210
+ pattern = pattern.value if pattern.is_a?(Prism::CapturePatternNode)
211
+ disjoint_nominal_subject?(@scope_index[clause.pattern], subject_name, [pattern])
212
+ end
213
+
180
214
  def in_clause_kind(clause, subject_name)
181
215
  entry = @scope_index[clause.pattern]
182
216
  return :prior_exhaustion if entry && entry.local(subject_name).is_a?(Type::Bot)