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,267 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../analysis/dependency_recorder"
4
+ require_relative "../reflection"
5
+
6
+ module Rigor
7
+ module Inference
8
+ # Issue #527 slice 0 — the single owner of one question: which RBS declaration answers
9
+ # `method_name` on `class_name` through an ancestor the PROJECT does not declare?
10
+ #
11
+ # A project class is usually absent from the RBS environment, so the declaration that decides the
12
+ # question is written about an ancestor the project does not declare — the `< StandardError`
13
+ # superclass, the `include Comparable` mixin, `< ::StringScanner`. `Scope#external_ancestor_name_candidates`
14
+ # gathers those as-written names breadth-first over the project ancestry; this module resolves each
15
+ # group against the RBS environment and reports WHICH declaration answered, not merely that one did.
16
+ #
17
+ # The logic existed twice before: `ExpressionTyper#rbs_ancestor_answers?` asked it as a boolean (the
18
+ # implicit-self binding veto of issue #633 / ADR-110), and the dispatch side of #527 needs the same
19
+ # walk but wants the definition and its owner so it can dispatch there. Two copies of an MRO cut-off
20
+ # rule is one copy too many, so the boolean is now `!resolve(...).nil?`.
21
+ #
22
+ # The one thing the consumers do NOT share is the ADR-46 cross-file edge, and that is
23
+ # `record_dependencies:`. The implicit-self veto genuinely READ the ancestor's declaration sites to
24
+ # decide a binding, so its walk records them. A dispatch lookup asking "does some ancestor happen to
25
+ # declare this?" did not, and filing an edge for it would mislabel the lookup as an ancestry edge —
26
+ # which is why `RbsDispatch.each_source_ancestor_candidate` read the raw discovery tables instead of
27
+ # this walk. The suppression is #992's {Analysis::DependencyRecorder.withhold} around the walk, NOT
28
+ # a flag threaded into `Scope`: an ADR-2 plugin-facing bypass of dependency recording would produce
29
+ # a silently stale warm cache that no diagnostic diff can show.
30
+ #
31
+ # There is deliberately NO memo here. The dispatch consumer this module is being extracted FOR does
32
+ # not exist yet, and a memo keyed on the tables this slice happens to name would be keyed on less
33
+ # than the walk reads — `Scope#resolve_ancestor_class_name` consults `discovered_def_nodes` and
34
+ # `discovered_methods` through `known_user_class?`, and `ancestor_name_candidates` reads
35
+ # `discovered_header_nestings`. The slice that brings the hot path brings the memo, keyed on what
36
+ # that consumer actually reads.
37
+ module ExternalAncestorResolution
38
+ # The owner a name must PRECEDE for its declaration to win an MRO. See {declared_before_object?}.
39
+ OBJECT_OWNER = "Object"
40
+ private_constant :OBJECT_OWNER
41
+
42
+ module_function
43
+
44
+ # Resolves `method_name` on `class_name` against the RBS environment, through the project's
45
+ # ancestry where `class_name` itself is absent from RBS.
46
+ #
47
+ # Returns `[definition, owner_name]` — the `RBS::Definition::Method` and the name of the class the
48
+ # walk asked it of (NOT `definition.defined_in`, which may be further up that class's own RBS
49
+ # ancestry) — or `nil` when no ancestor answers.
50
+ #
51
+ # The cut-off is `::Object`, not the own class: a top-level `def` IS a private `Object` instance
52
+ # method, the last link of every MRO, so any declared owner that comes before `::Object` wins at
53
+ # runtime (`Exception#message`, `Array#first`, `Comparable#clamp`) while a name owned by `Object`
54
+ # or `Kernel` themselves sits at or after that rung and contributes no evidence (#316 / #319).
55
+ #
56
+ # `kind` is `:instance` today. The singleton side of the walk (a module's `def self.` reached
57
+ # through a discovered `include`, #527 slice 6) is not implemented, and declines rather than
58
+ # guessing — which is what the engine answers there now.
59
+ #
60
+ # `record_dependencies: false` runs the ancestry walk under {Analysis::DependencyRecorder.withhold}
61
+ # so its reads reach no consumer. Only the WALK is withheld: the `Reflection` lookups around it
62
+ # read the RBS environment, which files no cross-file edge, so this suppresses exactly the intended
63
+ # one. `withhold` is a `[yield, nil]` fast path when nothing is recording, which is every ordinary
64
+ # run.
65
+ #
66
+ # `mixins: false` narrows the walk to the SUPERCLASS chain. #527 slice 1 lands `< Hash` before
67
+ # `include Enumerable` so that slice 2's measurement stays its own, and the narrowing belongs to
68
+ # the CALLER rather than to this module: the implicit-self veto must keep walking both edges,
69
+ # because Ruby reaches an included module's methods too.
70
+ # rubocop:disable-next Metrics/ParameterLists
71
+ def resolve(class_name, method_name, kind = :instance, scope:, environment: nil, name_memo: nil,
72
+ record_dependencies: true, mixins: true)
73
+ return nil if class_name.nil? || scope.nil?
74
+ return nil unless kind == :instance
75
+
76
+ compute(class_name, method_name, scope, environment, name_memo, record_dependencies, mixins)
77
+ end
78
+
79
+ # The RBS method definition for `class_name`, or nil for a class the environment does not know, a
80
+ # name it does not declare, or a signature it cannot build. The single owner of the `rescue` —
81
+ # a malformed third-party signature is a gap, never an exception escaping into inference.
82
+ def method_definition(class_name, method_name, kind, scope: nil, environment: nil)
83
+ if kind == :singleton
84
+ Rigor::Reflection.singleton_method_definition(class_name, method_name, scope: scope,
85
+ environment: environment)
86
+ else
87
+ Rigor::Reflection.instance_method_definition(class_name, method_name, scope: scope,
88
+ environment: environment)
89
+ end
90
+ rescue StandardError
91
+ nil
92
+ end
93
+
94
+ # True when the RBS declaration found for the name sits on `class_name` itself rather than on an
95
+ # ancestor; mirrors `CheckRules#defined_on?` and `SigGen::Generator#declared_on_class_itself?`.
96
+ def declared_on_class?(definition, class_name)
97
+ return false if definition.nil?
98
+ return false unless definition.respond_to?(:defined_in)
99
+
100
+ defined_in = definition.defined_in
101
+ return false if defined_in.nil?
102
+
103
+ defined_in.to_s.delete_prefix("::") == class_name.to_s.delete_prefix("::")
104
+ end
105
+
106
+ # Issue #633 — true when the declaration's owner sits strictly before `::Object` in `class_name`'s
107
+ # instance MRO, i.e. Ruby dispatches to it ahead of a top-level `def` (which is `Object`'s own
108
+ # private instance method). The own class trivially qualifies. An owner absent from the ancestor
109
+ # list, an unbuildable class, and an ancestry that does not reach `Object` (a `BasicObject`
110
+ # descendant) all answer false, leaving the historical top-level binding untouched.
111
+ def declared_before_object?(definition, class_name, scope: nil, environment: nil)
112
+ return true if declared_on_class?(definition, class_name)
113
+ return false if definition.nil? || !definition.respond_to?(:defined_in)
114
+
115
+ owner = definition.defined_in
116
+ return false if owner.nil?
117
+
118
+ ancestors = instance_ancestor_names(class_name, scope: scope, environment: environment)
119
+ object_index = ancestors.index(OBJECT_OWNER)
120
+ owner_index = ancestors.index(owner.to_s.delete_prefix("::"))
121
+ !object_index.nil? && !owner_index.nil? && owner_index < object_index
122
+ end
123
+
124
+ # The class's instance-side ancestors in MRO order, `::`-stripped, or `[]` for a class the RBS
125
+ # environment does not know or cannot build. Read through the loader's accessor rather than
126
+ # `instance_definition(...).ancestors` because that is the one wired to the ancestor-name cache and
127
+ # marked as RIGOR'S OWN demand — ordering two ancestors is not the analysis asking whether either
128
+ # one's methods resolve, and the `rbs.coverage` bookkeeping must not record it as such.
129
+ def instance_ancestor_names(class_name, scope: nil, environment: nil)
130
+ loader = rbs_loader_for(scope, environment)
131
+ loader ? loader.ancestor_names_for(class_name.to_s) : []
132
+ rescue StandardError
133
+ []
134
+ end
135
+
136
+ def compute(class_name, method_name, scope, environment, name_memo, record_dependencies, mixins)
137
+ kind = :instance
138
+ own = method_definition(class_name, method_name, kind, scope: scope, environment: environment)
139
+ if own
140
+ own_ancestors = instance_ancestor_names(class_name, scope: scope, environment: environment)
141
+ if declared_before_object?(own, class_name, scope: scope, environment: environment) ||
142
+ owned_within_candidate_chain?(own, own_ancestors)
143
+ return [own, class_name.to_s].freeze
144
+ end
145
+ end
146
+
147
+ groups = ancestor_candidate_groups(scope, class_name, name_memo, record_dependencies, mixins)
148
+ farther_ancestors = {}
149
+ groups.each_with_index do |candidates, index|
150
+ answer = first_known_candidate_answer(candidates, method_name, kind, scope, environment)
151
+ next if answer.nil?
152
+ next if resited_member?(answer, groups, index, farther_ancestors, scope, environment)
153
+
154
+ return answer
155
+ end
156
+ nil
157
+ end
158
+ private_class_method :compute
159
+
160
+ # The first candidate spelling the RBS environment knows is the ancestor Ruby resolves; a name it
161
+ # knows nothing about contributes no evidence either way, and a known one that does NOT answer
162
+ # ends this group rather than falling through to a less-qualified spelling of the same name.
163
+ def first_known_candidate_answer(candidates, method_name, kind, scope, environment)
164
+ candidates.each do |candidate|
165
+ ancestors = instance_ancestor_names(candidate, scope: scope, environment: environment)
166
+ next if ancestors.empty?
167
+
168
+ definition = method_definition(candidate, method_name, kind, scope: scope,
169
+ environment: environment)
170
+ return nil unless declared_before_object?(definition, candidate,
171
+ scope: scope, environment: environment) ||
172
+ owned_within_candidate_chain?(definition, ancestors)
173
+
174
+ return [definition, candidate].freeze
175
+ end
176
+ nil
177
+ end
178
+ private_class_method :first_known_candidate_answer
179
+
180
+ # The module-candidate half of the answering test (#1173 review). A module's RBS ancestry is
181
+ # `[itself, its own includes…]` and never reaches `Object`, so {declared_before_object?} — whose
182
+ # cut-off exists to keep an Object- / Kernel-owned declaration from outranking a top-level `def`
183
+ # — always answers false for it: the cut-off has nothing to cut. What decides instead is that
184
+ # the declaration came from the candidate's OWN chain: when the candidate sits in the receiver's
185
+ # MRO, Ruby dispatches the name through exactly those ancestors, so a `defined_in` anywhere in
186
+ # the list is the method that runs. Without this, a nearer RBS module that inherits the name
187
+ # from its own RBS `include` read as a dead group and a FARTHER ancestor's declaration was
188
+ # adopted in its place — `class C; include A; include B` where `B`'s RBS includes `N` answering
189
+ # A's declaration rather than N's. The gate keeps the Object cut-off's reach: a chain that DOES
190
+ # contain `Object` (every ordinary class candidate) is untouched.
191
+ def owned_within_candidate_chain?(definition, ancestors)
192
+ return false if ancestors.include?(OBJECT_OWNER)
193
+ return false if definition.nil? || !definition.respond_to?(:defined_in)
194
+
195
+ owner = definition.defined_in
196
+ !owner.nil? && ancestors.include?(owner.to_s.delete_prefix("::"))
197
+ end
198
+ private_class_method :owned_within_candidate_chain?
199
+
200
+ # The re-siting guard (#1173 review, second pass). Ruby's `include` is a no-op for a module
201
+ # already in the ancestry, so a member's real position is set by the EARLIEST source statement
202
+ # that pulled it in — which is the FARTHEST group in this search-order walk that carries it.
203
+ # Adopting group `index`'s answer claims the owner (and the declaration's `defined_in`) sit
204
+ # inside this group's segment; when either name also appears in a farther group's RBS chain,
205
+ # the runtime actually sites it there and everything between can shadow it —
206
+ # `class C < Array; include M` where `M`'s RBS includes `Enumerable` must not adopt
207
+ # `Enumerable#first` through `M` while `Array#first` intervenes. Skipping the answer is not a
208
+ # decline of the method: the farther group that truly carries the member resolves it at its
209
+ # real position (or nothing does, and the honest `Dynamic[top]` stands).
210
+ #
211
+ # `farther_ancestors` memoizes each farther group's resolved ancestor list — the first
212
+ # candidate spelling the RBS environment knows, the same spelling rule
213
+ # {first_known_candidate_answer} applies. A group whose names all stay project- or
214
+ # unresolved-side has no visible chain to compare. The deferral can trade precision for
215
+ # safety: when the farther carrier resolves the member without type-argument bindings the
216
+ # answer degrades to `Dynamic[top]` where the skipped group carried one — the intended
217
+ # direction, and in the pinned shapes the runtime-correct answer anyway.
218
+ def resited_member?(answer, groups, index, farther_ancestors, scope, environment)
219
+ definition, owner = answer
220
+ members = [owner.to_s.delete_prefix("::")]
221
+ defined_in = definition.defined_in if definition.respond_to?(:defined_in)
222
+ members << defined_in.to_s.delete_prefix("::") if defined_in
223
+
224
+ ((index + 1)...groups.size).any? do |j|
225
+ ancestors = farther_ancestors.fetch(j) do
226
+ farther_ancestors[j] = resolved_group_ancestors(groups[j], scope, environment)
227
+ end
228
+ ancestors&.any? { |name| members.include?(name) }
229
+ end
230
+ end
231
+ private_class_method :resited_member?
232
+
233
+ # The ancestor list of the first candidate spelling the RBS environment knows — nil when no
234
+ # spelling resolves, which is what the walk emitted the group for in the first place.
235
+ def resolved_group_ancestors(candidates, scope, environment)
236
+ candidates.each do |candidate|
237
+ ancestors = instance_ancestor_names(candidate, scope: scope, environment: environment)
238
+ return ancestors unless ancestors.empty?
239
+ end
240
+ nil
241
+ end
242
+ private_class_method :resolved_group_ancestors
243
+
244
+ # The walk, with its ADR-46 reads attached or detached. `withhold` returns `[result, read_set]`
245
+ # and the read set is dropped: a caller that suppresses is saying these reads are not a dependency
246
+ # of its answer, not that they should be replayed somewhere else.
247
+ def ancestor_candidate_groups(scope, class_name, name_memo, record_dependencies, mixins)
248
+ memo = name_memo || {}
249
+ if record_dependencies
250
+ return scope.external_ancestor_name_candidates(class_name, name_memo: memo, mixins: mixins)
251
+ end
252
+
253
+ Analysis::DependencyRecorder.withhold do
254
+ scope.external_ancestor_name_candidates(class_name, name_memo: memo, mixins: mixins)
255
+ end.first
256
+ end
257
+ private_class_method :ancestor_candidate_groups
258
+
259
+ def rbs_loader_for(scope, environment)
260
+ (environment || scope&.environment)&.rbs_loader
261
+ rescue StandardError
262
+ nil
263
+ end
264
+ private_class_method :rbs_loader_for
265
+ end
266
+ end
267
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../source/node_children"
6
+ require_relative "../type"
7
+ require_relative "stored_block_call"
8
+
9
+ module Rigor
10
+ module Inference
11
+ # Issue #1361 — the blocks whose body does not read the narrowing of the frame-local special variables (the regex
12
+ # match globals, and `$_` since issue #1359) where it is written ({.fresh_entry?}, {.entry}):
13
+ #
14
+ # - a root block: the block of `Thread.new` / `Thread.start` / `Thread.fork`, `Fiber.new` or `Ractor.new` on the
15
+ # core class ({.root_call?}). It runs as the root of a new thread, fiber or ractor, and Ruby keeps that
16
+ # execution context's own special-variable slot (`ec->root_svar`) for every piece of code whose local frame is
17
+ # the block's, the block and each block nested in it included. So the block reads `$1` nil whatever the
18
+ # creator matched, and a match it runs never rebinds the creator's `$~`. A `&expr` block argument's
19
+ # expression still runs in the creator's frame, before the thread starts;
20
+ # - a definer block: the block of `define_method` / `define_singleton_method`, on any receiver. It runs as a
21
+ # method body whenever the method is called, and reads the defining frame's slot as it is at that time, which
22
+ # each call's own matches write too, so the narrowing where the method is defined neither proves nor refutes
23
+ # what it reads. Unlike a root block it shares that slot, so a match in it still rebinds the definer's `$~`.
24
+ #
25
+ # `Enumerator.new` is neither: its block runs on an internal fiber whose root is not the block, so it reads and
26
+ # rebinds the creator's slot like any other block.
27
+ #
28
+ # The receiver of a root call must be the literal constant, and must resolve to the core class where a scope is
29
+ # at hand, so a project's own `Thread` (`module Pool; class Thread; …`) keeps the ordinary block rules.
30
+ module FreshFrameBlocks
31
+ ROOT_CONSTRUCTORS = {
32
+ Thread: %i[new start fork].freeze, Fiber: %i[new].freeze, Ractor: %i[new].freeze
33
+ }.freeze
34
+ DEFINERS = Set[:define_method, :define_singleton_method].freeze
35
+ private_constant :ROOT_CONSTRUCTORS, :DEFINERS
36
+
37
+ module_function
38
+
39
+ # True when `node` is a call whose block (a literal or a `&expr` argument) runs as the root of a new thread,
40
+ # fiber or ractor. `scope` resolves the receiver; without one the literal constant is taken at its word.
41
+ def root_call?(node, scope = nil)
42
+ return false unless node.is_a?(Prism::CallNode) && !node.block.nil?
43
+
44
+ constant = StoredBlockCall.root_constant_name(node.receiver)
45
+ return false unless ROOT_CONSTRUCTORS[constant]&.include?(node.name)
46
+
47
+ scope.nil? || core_class?(node.receiver, constant, scope)
48
+ end
49
+
50
+ # The block of `node` when {.root_call?} holds, else nil.
51
+ def root_block(node, scope = nil)
52
+ root_call?(node, scope) ? node.block : nil
53
+ end
54
+
55
+ # True when the block answers true for a child of `node` that runs in the frame `node` runs in: any child but
56
+ # a root block ({.root_block}), which runs with a slot of its own. Every {MatchRebinding} reading of what
57
+ # running code may match walks through here. A root `&expr` block argument's own children are asked in its
58
+ # place, since Ruby evaluates `expr` in this frame before the thread starts
59
+ # (`Thread.new(&HANDLERS.fetch(name.sub(re, "")))` rebinds the creator's `$~`). `into_root` asks a root block's
60
+ # children too, for a reading of what the code makes rather than what it runs: a closure made in a root block
61
+ # may be handed back to this frame's thread. Stops at the first true answer, without a `return` out of the
62
+ # walk's block.
63
+ def any_frame_child?(node, scope, into_root: false)
64
+ root = root_block(node, scope)
65
+ found = false
66
+ node.rigor_each_child do |child|
67
+ if !child.equal?(root)
68
+ found ||= yield(child)
69
+ elsif into_root || child.is_a?(Prism::BlockArgumentNode)
70
+ child.rigor_each_child { |inner| found ||= yield(inner) }
71
+ end
72
+ end
73
+ found
74
+ end
75
+
76
+ # True when the block of `call_node` does not read the narrowing where it is written: a root block, or a
77
+ # definer block. {.entry} gives the scope it enters with.
78
+ def fresh_entry?(call_node, scope = nil)
79
+ return false unless call_node.is_a?(Prism::CallNode)
80
+
81
+ DEFINERS.include?(call_node.name) || root_call?(call_node, scope)
82
+ end
83
+
84
+ # The scope the block of `call_node`, which {.fresh_entry?} names, enters with. A root block reads a slot of its
85
+ # own, so the match globals are unbound there, and `k = $1; k.upcase` in an entered `Thread.new { … }` block is
86
+ # a true positive. A definer body reads the definer's slot whenever the method is called, which the analyzer
87
+ # does not follow, so a global narrowed where it is written reads `Dynamic[top]` there, neither narrowed nor
88
+ # flagged: the dynamic-finder idiom defines `find_by_email` in a `method_missing` guard whose `$1` its body
89
+ # reads. This is the one place the specials are reset at such an entry: the match globals, `$_` (#1359), and
90
+ # `$!`, `$@` and `$?` (#1360).
91
+ #
92
+ # A root block runs in an execution context of its own, so it reads `$!` and `$@` as `nil` even where it is
93
+ # written in a rescue clause ({ErrorInfo}). `$?` is the thread's, so a thread's or a ractor's root block starts
94
+ # without one, while a fiber shares the thread's. A definer body reads all three as they are when the method is
95
+ # called.
96
+ def entry(scope, call_node)
97
+ if DEFINERS.include?(call_node.name)
98
+ scope.untyped_match_globals.untyped_last_line.untyped_error_info.untyped_last_status
99
+ else
100
+ fresh = scope.forget_match_globals.forget_last_line.forget_error_info
101
+ StoredBlockCall.root_constant_name(call_node.receiver) == :Fiber ? fresh : fresh.forget_last_status
102
+ end
103
+ end
104
+
105
+ # Issue #1360 — the scope a closure's body enters with: a lambda literal's (`block_node` a `Prism::LambdaNode`),
106
+ # or the block of a call that keeps it to run later ({StoredBlockCall.stores_block?}: `lambda`, `proc`,
107
+ # `Proc.new`, `Enumerator.new`, `Hash.new`). The body runs whenever the closure is called, which is usually after
108
+ # the rescue clause it is written in has exited (`f = -> { $! }` in a clause reads `nil` once called past the
109
+ # `begin`), and may be on another thread (`Thread.new(&f)`), so `$!`, `$@` and `$?` are unbound there. A root or
110
+ # definer block enters as {.entry} gives; every other block reads them where it is written.
111
+ def closure_entry(scope, block_node, call_node)
112
+ return scope unless block_node.is_a?(Prism::LambdaNode) ||
113
+ (call_node.is_a?(Prism::CallNode) && StoredBlockCall.stores_block?(call_node))
114
+
115
+ scope.forget_error_info.forget_last_status
116
+ end
117
+
118
+ def core_class?(receiver, constant, scope)
119
+ type = scope.type_of(receiver)
120
+ type.is_a?(Type::Singleton) && type.class_name == constant.name
121
+ rescue StandardError
122
+ false
123
+ end
124
+ private_class_method :core_class?
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,239 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../source/constant_path"
6
+ require_relative "../source/node_children"
7
+
8
+ module Rigor
9
+ module Inference
10
+ # Issue #1367 — the program facts the `global.*` write rules read, gathered syntactically over every file:
11
+ #
12
+ # - `[:alias, name]` — a global variable name an `alias $new $old` statement names, on either side. After it,
13
+ # `$new` is `$old`'s variable, setter included, so both rules exempt the name.
14
+ # - `[:defines, name]` — a method named after one of {ANSWER_NAMES} is defined somewhere, in any spelling and on
15
+ # any receiver: `def`, `def obj.m`, a `class << obj` body, `define_method`, `define_singleton_method`,
16
+ # `alias`, `alias_method`, `attr_*`, a delegation macro, or the same call through `send`. The
17
+ # type check does not ask where it lands: an object can reach any of them (`class << nil`, `K = Integer;
18
+ # class K`, `[Integer].each { |k| k.define_method(:write) }`), so a literal of any class declines.
19
+ # - {DEFINES_ANY} — a definition whose name no literal spells: a computed `define_method` name, a `send` whose
20
+ # method name is computed, a string `eval` / `class_eval` whose code is interpolated or not a literal, a name
21
+ # literal whose bytes are not valid in its encoding, or a top-level mixin of a non-constant. A node the
22
+ # collector fails to read records it too ({Collector#visit}).
23
+ # - `[:refines, name]` / {REFINES_ANY} — the same, inside a `refine` block. A refinement changes what
24
+ # `respond_to?(:write)` answers where a `using` is in effect, and nothing else the setters consult: an
25
+ # implicit conversion and a refined `respond_to?` / `respond_to_missing?` ignore it, so these entries are
26
+ # kept apart from `[:defines, …]`.
27
+ # - `[:main_mixin, name]` — a module a top-level `include`, `prepend` or `extend` mixes into `Object` (or
28
+ # `main`), as written. A module RBS does not know may carry any method, so it reaches every class.
29
+ #
30
+ # The census is a `Set` of those frozen entries, merged by union across files, the project pre-pass and the
31
+ # `pre_eval:` files. It is recorded inside descents that already visit every node ({Collector#visit}), never
32
+ # by a walk of its own, except for a `pre_eval:` file ({.scan}).
33
+ module GlobalWriteCensus
34
+ # The methods whose presence lets an object answer a setter: the one it asks `respond_to?` about (`write`),
35
+ # the implicit conversions (`to_str`, `to_int`), and the escape hatches both consult.
36
+ ANSWER_NAMES = %i[write to_str to_int method_missing respond_to_missing? respond_to?].to_set.freeze
37
+
38
+ DEFINES_ANY = [:defines_any].freeze
39
+ REFINES_ANY = [:refines_any].freeze
40
+
41
+ # `define_method`-family calls whose first argument names the method they define.
42
+ NAMING_CALLS = %i[define_method define_singleton_method alias_method].to_set.freeze
43
+ # Calls every literal argument of which may name a method they define.
44
+ ATTRIBUTE_CALLS = %i[attr attr_reader attr_writer attr_accessor].to_set.freeze
45
+ DELEGATION_CALLS = %i[
46
+ def_delegator def_delegators def_instance_delegator def_instance_delegators def_single_delegator
47
+ def_single_delegators delegate instance_delegate single_delegate
48
+ ].to_set.freeze
49
+ # `delegate_missing_to` defines `method_missing` and `respond_to_missing?` whatever it is handed.
50
+ MISSING_DELEGATION_CALLS = %i[delegate_missing_to].to_set.freeze
51
+ SEND_CALLS = %i[send __send__ public_send].to_set.freeze
52
+ EVAL_CALLS = %i[eval class_eval module_eval instance_eval].to_set.freeze
53
+ MIXIN_CALLS = %i[include prepend extend].to_set.freeze
54
+ DEFINING_CALLS =
55
+ (NAMING_CALLS | ATTRIBUTE_CALLS | DELEGATION_CALLS | MISSING_DELEGATION_CALLS | EVAL_CALLS).freeze
56
+ # Every call name {Collector#visit} reads; any other call is skipped at once.
57
+ VISITED_CALLS = (DEFINING_CALLS | SEND_CALLS | MIXIN_CALLS | %i[import_methods]).freeze
58
+ NO_ARGUMENTS = [].freeze
59
+ NAME_PATTERN = /(?<![\w@$])(?:write|to_str|to_int|method_missing|respond_to_missing\?|respond_to\?)(?![\w?!=])/
60
+ private_constant :NAMING_CALLS, :ATTRIBUTE_CALLS, :DELEGATION_CALLS, :MISSING_DELEGATION_CALLS,
61
+ :SEND_CALLS, :EVAL_CALLS, :MIXIN_CALLS, :DEFINING_CALLS, :VISITED_CALLS, :NO_ARGUMENTS,
62
+ :NAME_PATTERN
63
+
64
+ EMPTY = Set.new.freeze
65
+
66
+ module_function
67
+
68
+ def alias_entry(name) = [:alias, name].freeze
69
+
70
+ def aliased?(census, name) = census.include?([:alias, name])
71
+
72
+ # Whether the program may define one of `names` somewhere, outside a refinement.
73
+ def defines_any_of?(census, names)
74
+ census.include?(DEFINES_ANY) || names.any? { |name| census.include?([:defines, name]) }
75
+ end
76
+
77
+ # Whether some refinement may add `write`.
78
+ def refines_write?(census) = census.include?(REFINES_ANY) || census.include?(%i[refines write])
79
+
80
+ # The module names the top-level mixins spell.
81
+ def main_mixins(census) = census.filter_map { |entry| entry[1] if entry[0] == :main_mixin }
82
+
83
+ # A standalone walk, for a file no existing descent visits (a `pre_eval:` entry).
84
+ def scan(root)
85
+ collector = Collector.new
86
+ walk(root, collector, true)
87
+ collector.census.freeze
88
+ end
89
+
90
+ def walk(node, collector, top_level)
91
+ return unless node.is_a?(Prism::Node)
92
+
93
+ collector.visit(node, top_level: top_level)
94
+ inner = top_level && !(node.is_a?(Prism::ClassNode) || node.is_a?(Prism::ModuleNode))
95
+ node.rigor_each_child { |child| walk(child, collector, inner) }
96
+ end
97
+ private_class_method :walk
98
+
99
+ # Accumulates one file's census, fed each node of a pre-order descent. `top_level` says whether the node sits
100
+ # outside every `class` / `module` body; it is the host walk's to track. `refine` blocks are remembered by
101
+ # offset as they are met, so a node inside one is known without the host walk tracking it.
102
+ class Collector
103
+ attr_reader :census
104
+
105
+ def initialize
106
+ @census = Set.new
107
+ @refine_ranges = []
108
+ end
109
+
110
+ # A node the collector cannot read must not fail the analysis of its file, nor of every file the pre-pass
111
+ # feeds: the census then counts it as a definition of every name, which only declines a report.
112
+ def visit(node, top_level:)
113
+ case node
114
+ when Prism::AliasGlobalVariableNode then visit_global_alias(node)
115
+ when Prism::DefNode then record(node, node.name)
116
+ when Prism::AliasMethodNode then record_argument(node, node.new_name)
117
+ when Prism::CallNode then visit_call(node, top_level)
118
+ end
119
+ rescue StandardError
120
+ @census << DEFINES_ANY
121
+ end
122
+
123
+ private
124
+
125
+ def visit_global_alias(node)
126
+ [node.new_name, node.old_name].each do |side|
127
+ @census << GlobalWriteCensus.alias_entry(side.name) if side.is_a?(Prism::GlobalVariableReadNode)
128
+ end
129
+ end
130
+
131
+ def visit_call(node, top_level)
132
+ remember_refine(node)
133
+ name = node.name
134
+ return unless VISITED_CALLS.include?(name)
135
+
136
+ arguments = node.arguments&.arguments || NO_ARGUMENTS
137
+ if SEND_CALLS.include?(name)
138
+ return if arguments.empty?
139
+
140
+ # A method name no literal spells (computed, or with bytes invalid in its encoding) may be any of them. A
141
+ # literal one is read as that call written directly: `send(:include, M)` at the top level is a mixin, and
142
+ # `send(:size)` defines nothing.
143
+ first = literal_name(arguments.first)
144
+ return @census << any_entry(node) if first.nil?
145
+
146
+ name = first
147
+ arguments = arguments.drop(1)
148
+ end
149
+ visit_defining_call(node, name, arguments)
150
+ visit_main_mixin(node, arguments) if top_level && MIXIN_CALLS.include?(name) && self_receiver?(node)
151
+ end
152
+
153
+ def visit_defining_call(node, name, arguments)
154
+ if NAMING_CALLS.include?(name)
155
+ record_argument(node, arguments.first)
156
+ elsif ATTRIBUTE_CALLS.include?(name) || DELEGATION_CALLS.include?(name)
157
+ arguments.each { |argument| record_every_literal(node, argument) }
158
+ elsif MISSING_DELEGATION_CALLS.include?(name)
159
+ record(node, :method_missing)
160
+ record(node, :respond_to_missing?)
161
+ elsif EVAL_CALLS.include?(name)
162
+ # Only the first argument is code; the rest are a binding, a file name and a line number.
163
+ record_eval_text(node, arguments.first) unless arguments.empty?
164
+ elsif name == :import_methods && inside_refine?(node)
165
+ @census << REFINES_ANY
166
+ end
167
+ end
168
+
169
+ # A top-level `include M` / `prepend M` / `extend M` mixes M into `Object` (or `main`).
170
+ def visit_main_mixin(node, arguments)
171
+ arguments.each do |argument|
172
+ constant = Source::ConstantPath.qualified_name(argument) if constant_node?(argument)
173
+ @census << (constant ? [:main_mixin, constant.delete_prefix("::").freeze].freeze : any_entry(node))
174
+ end
175
+ end
176
+
177
+ def record(node, name)
178
+ return unless ANSWER_NAMES.include?(name)
179
+
180
+ @census << [inside_refine?(node) ? :refines : :defines, name].freeze
181
+ end
182
+
183
+ # A literal method name, or any name when the argument spells none.
184
+ def record_argument(node, argument)
185
+ name = literal_name(argument)
186
+ name ? record(node, name) : @census << any_entry(node)
187
+ end
188
+
189
+ def record_every_literal(node, argument)
190
+ case argument
191
+ when Prism::SymbolNode, Prism::StringNode then record_argument(node, argument)
192
+ when Prism::ArrayNode then argument.elements.each { |element| record_every_literal(node, element) }
193
+ when Prism::KeywordHashNode, Prism::HashNode
194
+ argument.elements.each { |pair| record_every_literal(node, pair) }
195
+ when Prism::AssocNode
196
+ record_every_literal(node, argument.key)
197
+ record_every_literal(node, argument.value)
198
+ else @census << any_entry(node)
199
+ end
200
+ end
201
+
202
+ def record_eval_text(node, argument)
203
+ if argument.is_a?(Prism::StringNode) && argument.unescaped.valid_encoding?
204
+ argument.unescaped.scan(NAME_PATTERN) { |name| record(node, name.to_sym) }
205
+ else
206
+ @census << any_entry(node)
207
+ end
208
+ end
209
+
210
+ def any_entry(node) = inside_refine?(node) ? REFINES_ANY : DEFINES_ANY
211
+
212
+ # The name a Symbol or String literal spells, or nil for any other node and for a literal whose bytes are
213
+ # not valid in its encoding, which names no method Ruby can define.
214
+ def literal_name(argument)
215
+ return unless argument.is_a?(Prism::SymbolNode) || argument.is_a?(Prism::StringNode)
216
+
217
+ text = argument.unescaped
218
+ text.to_sym if text.valid_encoding?
219
+ end
220
+
221
+ def constant_node?(node) = node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
222
+
223
+ def self_receiver?(node) = node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
224
+
225
+ def remember_refine(node)
226
+ return unless node.name == :refine && self_receiver?(node) && node.block.is_a?(Prism::BlockNode)
227
+
228
+ location = node.block.location
229
+ @refine_ranges << (location.start_offset...location.end_offset)
230
+ end
231
+
232
+ def inside_refine?(node)
233
+ offset = node.location.start_offset
234
+ @refine_ranges.any? { |range| range.cover?(offset) }
235
+ end
236
+ end
237
+ end
238
+ end
239
+ end