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,329 @@
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
+ module LastLine
11
+ # Issue #1415 (ADR-117 WD5) — what a file shows about the `self` its implicit-self and `self.` readers run with,
12
+ # read in one walk of the file the first time a reader condition asks ({ImplicitSelf.reader?}), and kept only
13
+ # once complete. The walk runs only for a file that holds such a condition. `Inference::ScopeIndexer` keeps one
14
+ # per file on the discovery index ({Scope::DiscoveryIndex#implicit_self_evidence}), so a callee body another
15
+ # file wrote finds none of its readers here and declines.
16
+ #
17
+ # Each reader is placed in the body whose `self` it runs with: `main` (the script body, a top-level method, a
18
+ # `class << self` at the top level), or a class or module body and its methods. A reader has none, and
19
+ # declines, where the file may give it another `self`:
20
+ #
21
+ # - in a block of `instance_eval`, `instance_exec`, `class_eval`, `module_eval`, `class_exec`, `module_exec`,
22
+ # `define_method` or `define_singleton_method` (or a `send` that may name one), or of `Class.new`,
23
+ # `Module.new`, `Struct.new` or `Data.define`, whose block is a class body;
24
+ # - in a method written inside a block or another method, whose owner is the block's or method's `self`, in a
25
+ # module's instance method, whose `self` is whatever includes the module, and in `class << obj` for any `obj`
26
+ # but `self`, or in a `class << self` a method runs, whose `self` is the method's receiver.
27
+ #
28
+ # A class or module is dirty ({#dirty?}), and every reader whose `self`'s ancestry reaches it declines, when a
29
+ # body of it may give it an ancestor the discovery tables do not record: a superclass that is not a constant
30
+ # (`DelegateClass(File)`, `Struct.new(:a)`), and an `include`, `extend` or `prepend` other than a statement of
31
+ # the body with no receiver naming constants (`self.include(M)`, one in a method, a block or `class << self`,
32
+ # `singleton_class.include(M)`, `include(*mods)`). Dirtiness is kept by the declaration's last name segment,
33
+ # so a reopening, a subclass and an includer of the class see it too, and a same-named class elsewhere
34
+ # declines with it.
35
+ #
36
+ # A name is tainted, and every reader of it in the file declines, when the file may change the method that name
37
+ # reaches from `main`, `Object` or an object the analyzer does not follow:
38
+ #
39
+ # - both names: a mixin into `main` (`include`, `extend` or `prepend` on implicit `self` outside a class or
40
+ # module body, `class << self; include M; end` at the top level, `singleton_class.include(M)`), a mixin on
41
+ # any explicit receiver (`Object.include(M)`, `TOPLEVEL_BINDING.receiver.extend(M)`, `obj.extend(M)`), any
42
+ # mixin in an `Object`, `Kernel` or `BasicObject` body, a `send` that may name one, a method object taken
43
+ # for a mixin or a `self`-rebinding call (`method(:extend)`, `Module.instance_method(:include)`), any
44
+ # `using`, a proc handed to one of the `self`-rebinding calls above (`obj.instance_exec(&blk)`,
45
+ # `X.send(:define_method, :m, blk)`), whose body may be a block the file wrote anywhere, an eval of a
46
+ # String, whose code may do any of these, and a `const_set`, which may make a class a `class` body reopens;
47
+ # - one name: a `def` of it anywhere (`def self.gets` at the top level defines it on `main`, which
48
+ # {BlockCallTiming.project_defines_anywhere?} does not record), and a Symbol or String literal naming it,
49
+ # which a macro may define it through (`def_delegators :@io, :gets`, `attr_reader :gets`,
50
+ # `undef_method :gets`), other than `&:gets` and the first argument of a call that only names a method
51
+ # (`respond_to?(:gets)`, `send(:gets)`).
52
+ class SelfEvidence
53
+ # The context of a reader whose `self` is `main`.
54
+ MAIN = :main
55
+ UNKNOWN = :unknown
56
+ READER_NAMES = Set["gets", "readline"].freeze
57
+ MIXINS = Set[:include, :extend, :prepend].freeze
58
+ SENT_MIXINS = Set["include", "extend", "prepend", "using"].freeze
59
+ SENDS = Set[:send, :__send__, :public_send].freeze
60
+ REBINDERS = Set[
61
+ :instance_eval, :instance_exec, :class_eval, :module_eval, :class_exec, :module_exec, :define_method,
62
+ :define_singleton_method
63
+ ].freeze
64
+ REBINDER_NAMES = REBINDERS.to_set(&:to_s).freeze
65
+ # `using` refines for the rest of the file, and `const_set` may make a class a `class` body then reopens
66
+ # (`Object.const_set(:W, Class.new(CSV))`), whatever the receiver.
67
+ FILE_WIDE = Set[:using, :const_set].freeze
68
+ # The rebinders a proc argument can give a body to (`define_method(:m, blk)`).
69
+ BODY_DEFINERS = Set[:define_method, :define_singleton_method].freeze
70
+ BODY_DEFINER_NAMES = BODY_DEFINERS.to_set(&:to_s).freeze
71
+ CONSTRUCTORS = { Class: :new, Module: :new, Struct: :new, Data: :define }.freeze
72
+ # The calls that take a method object, which a later `call` or `bind_call` runs with any receiver.
73
+ METHOD_OBJECTS = Set[:method, :public_method, :singleton_method, :instance_method, :public_instance_method]
74
+ .freeze
75
+ METHOD_OBJECT_NAMES = (SENT_MIXINS | REBINDER_NAMES).freeze
76
+ # Calls whose first argument names a method without putting one in place.
77
+ NAMING_CALLS = Set[:send, :__send__, :public_send, :respond_to?, :method, :public_method].freeze
78
+ ROOTS = Set["Object", "Kernel", "BasicObject"].freeze
79
+ private_constant :UNKNOWN, :READER_NAMES, :MIXINS, :SENT_MIXINS, :SENDS, :REBINDERS, :REBINDER_NAMES,
80
+ :FILE_WIDE, :BODY_DEFINERS, :BODY_DEFINER_NAMES, :METHOD_OBJECTS, :METHOD_OBJECT_NAMES,
81
+ :CONSTRUCTORS, :NAMING_CALLS, :ROOTS
82
+
83
+ def initialize(root)
84
+ @root = root
85
+ @scan = nil
86
+ end
87
+
88
+ # {MAIN}, the `Prism::ClassNode` or `Prism::ModuleNode` whose body the implicit-self reader `call_node` runs
89
+ # in, or nil: the file may give it another `self`, or it is not one of this file's readers.
90
+ def context(call_node) = scan.context(call_node)
91
+
92
+ # True when a class or module of `class_name`'s last name segment is dirty in this file.
93
+ def dirty?(class_name) = scan.dirty?(class_name)
94
+
95
+ private
96
+
97
+ def scan = (@scan ||= Scan.new(@root))
98
+
99
+ # One walk of the file, whose answers {SelfEvidence} keeps once the walk is complete.
100
+ class Scan
101
+ def initialize(root)
102
+ @contexts = {}.compare_by_identity
103
+ @dirty = Set.new
104
+ @tainted = Set.new
105
+ visit(root, MAIN, false, false, false)
106
+ @contexts.freeze
107
+ @dirty.freeze
108
+ @tainted.freeze
109
+ end
110
+
111
+ def context(call_node)
112
+ return nil if @tainted.include?(call_node.name)
113
+
114
+ context = @contexts[call_node]
115
+ context unless context.is_a?(Prism::Node) && @dirty.include?(segment(context))
116
+ end
117
+
118
+ def dirty?(class_name) = @dirty.include?(class_name.to_s.split("::").last)
119
+
120
+ private
121
+
122
+ # `context` is {MAIN}, a declaration node or {UNKNOWN}; `statement` is true for a statement of a class or
123
+ # module body itself; `block` is true inside a block or lambda of the body; `singleton` is true inside a
124
+ # `class << self` body.
125
+ def visit(node, context, statement, block, singleton)
126
+ case node
127
+ when Prism::ClassNode, Prism::ModuleNode then visit_declaration(node, context, block, singleton)
128
+ when Prism::SingletonClassNode then visit_singleton_class(node, context, block)
129
+ when Prism::DefNode then visit_def(node, context, block, singleton)
130
+ when Prism::CallNode then visit_call(node, context, statement, block, singleton)
131
+ when Prism::BlockNode, Prism::LambdaNode then visit_children(node, context, true, singleton)
132
+ when Prism::BlockArgumentNode then visit_block_argument(node, context, block, singleton)
133
+ when Prism::SymbolNode, Prism::StringNode then taint(node.unescaped)
134
+ else visit_children(node, context, block, singleton)
135
+ end
136
+ end
137
+
138
+ def visit_children(node, context, block, singleton)
139
+ node.rigor_each_child { |child| visit(child, context, false, block, singleton) }
140
+ end
141
+
142
+ def visit_declaration(node, context, block, singleton)
143
+ visit(node.constant_path, context, false, block, singleton)
144
+ superclass = node.is_a?(Prism::ClassNode) ? node.superclass : nil
145
+ if superclass
146
+ visit(superclass, context, false, block, singleton)
147
+ dirty(node) unless constant?(superclass)
148
+ end
149
+ visit_body(node.body, node)
150
+ end
151
+
152
+ def visit_body(body, declaration)
153
+ if body.is_a?(Prism::StatementsNode)
154
+ body.body.each { |statement| visit(statement, declaration, true, false, false) }
155
+ elsif body
156
+ visit(body, declaration, false, false, false)
157
+ end
158
+ end
159
+
160
+ # `class << self` keeps the body's `self` for the methods it defines; `class << obj` gives them `obj`, and
161
+ # so does `class << self` in a method or a block, whose `self` is the method's receiver or the block's.
162
+ def visit_singleton_class(node, context, block)
163
+ visit(node.expression, context, false, block, false)
164
+ inner = node.expression.is_a?(Prism::SelfNode) && !block ? context : UNKNOWN
165
+ visit(node.body, inner, false, false, true) if node.body
166
+ end
167
+
168
+ def visit_def(node, context, block, singleton)
169
+ taint(node.name.to_s)
170
+ receiver = node.receiver
171
+ visit(receiver, context, false, block, singleton) if receiver
172
+ inner = def_context(receiver, context, block, singleton)
173
+ visit(node.parameters, inner, false, true, false) if node.parameters
174
+ visit(node.body, inner, false, true, false) if node.body
175
+ end
176
+
177
+ def def_context(receiver, context, block, singleton)
178
+ return UNKNOWN if block || context == UNKNOWN
179
+ return context if receiver.is_a?(Prism::SelfNode)
180
+ return UNKNOWN unless receiver.nil?
181
+
182
+ context.is_a?(Prism::ModuleNode) && !singleton ? UNKNOWN : context
183
+ end
184
+
185
+ def visit_call(node, context, statement, block, singleton)
186
+ classify(node, context, statement)
187
+ @contexts[node] = context if implicit_reader?(node) && context != UNKNOWN
188
+ visit(node.receiver, context, false, block, singleton) if node.receiver
189
+ visit_arguments(node, context, block, singleton)
190
+ visit_call_block(node, context, block, singleton)
191
+ end
192
+
193
+ def visit_arguments(node, context, block, singleton)
194
+ arguments = node.arguments&.arguments
195
+ return if arguments.nil?
196
+
197
+ arguments.each_with_index do |argument, index|
198
+ next if index.zero? && NAMING_CALLS.include?(node.name) && literal?(argument)
199
+
200
+ visit(argument, context, false, block, singleton)
201
+ end
202
+ end
203
+
204
+ def visit_call_block(node, context, block, singleton)
205
+ call_block = node.block
206
+ case call_block
207
+ when Prism::BlockNode then visit(call_block, rebinder?(node) ? UNKNOWN : context, false, true, singleton)
208
+ when Prism::BlockArgumentNode
209
+ taint_all if rebinder?(node) && !call_block.expression.is_a?(Prism::SymbolNode)
210
+ visit(call_block, context, false, block, singleton)
211
+ end
212
+ end
213
+
214
+ # `&:gets` runs the element's own reader.
215
+ def visit_block_argument(node, context, block, singleton)
216
+ expression = node.expression
217
+ return if expression.nil? || expression.is_a?(Prism::SymbolNode)
218
+
219
+ visit(expression, context, false, block, singleton)
220
+ end
221
+
222
+ def classify(node, context, statement)
223
+ name = node.name
224
+ if MIXINS.include?(name) then mixin(node, context, statement)
225
+ elsif taints_file?(name, node.arguments&.arguments) then taint_all
226
+ end
227
+ end
228
+
229
+ # `using`, `const_set`, an eval of a String, which may run any of the shapes this walk reads, a proc handed
230
+ # to a definer, a `send` that may name a mixin, and a method object taken for a mixin or a rebinder.
231
+ def taints_file?(name, arguments)
232
+ return true if FILE_WIDE.include?(name)
233
+ return false if arguments.nil?
234
+ return true if EVALS.include?(name) || (BODY_DEFINERS.include?(name) && arguments.size > 1)
235
+ return sent_mixin?(arguments) if SENDS.include?(name)
236
+
237
+ METHOD_OBJECTS.include?(name) && literal?(arguments.first) &&
238
+ METHOD_OBJECT_NAMES.include?(arguments.first.unescaped)
239
+ end
240
+
241
+ def mixin(node, context, statement)
242
+ receiver = node.receiver
243
+ declaration = context.is_a?(Prism::ClassNode) || context.is_a?(Prism::ModuleNode)
244
+ if declaration && !root?(context) && (receiver.nil? || self_or_singleton_class?(receiver))
245
+ dirty(context) unless receiver.nil? && statement && constant_arguments?(node)
246
+ else
247
+ taint_all
248
+ end
249
+ end
250
+
251
+ def sent_mixin?(arguments)
252
+ first = arguments&.first
253
+ case first
254
+ when nil then false
255
+ when Prism::SymbolNode, Prism::StringNode
256
+ name = first.unescaped
257
+ SENT_MIXINS.include?(name) || (BODY_DEFINER_NAMES.include?(name) && arguments.size > 2)
258
+ when Prism::SplatNode then true
259
+ else arguments.size > 1
260
+ end
261
+ end
262
+
263
+ # A rebinder, a `send` that may name one (`obj.send(:instance_eval) { … }`), or a metaclass constructor.
264
+ def rebinder?(node)
265
+ name = node.name
266
+ return true if REBINDERS.include?(name)
267
+ return sent_rebinder?(node.arguments&.arguments&.first) if SENDS.include?(name)
268
+
269
+ receiver = node.receiver
270
+ constructor =
271
+ case receiver
272
+ when Prism::ConstantReadNode then receiver.name
273
+ when Prism::ConstantPathNode then receiver.name if receiver.parent.nil?
274
+ end
275
+ !constructor.nil? && CONSTRUCTORS[constructor] == name
276
+ end
277
+
278
+ def sent_rebinder?(first)
279
+ literal?(first) ? REBINDER_NAMES.include?(first.unescaped) : true
280
+ end
281
+
282
+ def implicit_reader?(node)
283
+ LastLine::READERS.include?(node.name) && node.block.nil? &&
284
+ (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
285
+ end
286
+
287
+ # `self.include(M)` and `singleton_class.include(M)`, which the discovery tables do not record.
288
+ def self_or_singleton_class?(receiver)
289
+ return true if receiver.is_a?(Prism::SelfNode)
290
+
291
+ receiver.is_a?(Prism::CallNode) && receiver.name == :singleton_class &&
292
+ (receiver.receiver.nil? || receiver.receiver.is_a?(Prism::SelfNode))
293
+ end
294
+
295
+ def dirty(declaration)
296
+ @dirty << segment(declaration)
297
+ end
298
+
299
+ def segment(declaration) = declaration.constant_path.slice.split("::").last
300
+
301
+ def root?(declaration) = ROOTS.include?(segment(declaration))
302
+
303
+ # `extend self` adds the module's own methods, which the program's `def`s already answer for.
304
+ def constant_arguments?(node)
305
+ arguments = node.arguments&.arguments
306
+ !arguments.nil? && !arguments.empty? &&
307
+ arguments.all? { |argument| constant?(argument) || argument.is_a?(Prism::SelfNode) }
308
+ end
309
+
310
+ def constant?(node)
311
+ (node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)) &&
312
+ !Source::ConstantPath.qualified_name_or_nil(node).nil?
313
+ end
314
+
315
+ def literal?(node) = node.is_a?(Prism::SymbolNode) || node.is_a?(Prism::StringNode)
316
+
317
+ def taint(name)
318
+ @tainted << name.to_sym if READER_NAMES.include?(name)
319
+ end
320
+
321
+ def taint_all
322
+ @tainted.merge(LastLine::READERS)
323
+ end
324
+ end
325
+ private_constant :Scan
326
+ end
327
+ end
328
+ end
329
+ end
@@ -0,0 +1,340 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../source/node_children"
6
+ require_relative "../reflection"
7
+ require_relative "../type"
8
+ require_relative "block_call_timing"
9
+ require_relative "fresh_frame_blocks"
10
+
11
+ module Rigor
12
+ module Inference
13
+ # Issue #1359 — which code may set `$_`, the last line read, that a scope's narrowing speaks for. Ruby keeps it in
14
+ # the method frame's special-variable slot beside `$~`, so the frame rules are the match globals'
15
+ # ({MatchRebinding}): a `def`, class, module or file body has a slot of its own; a block, and a closure made in
16
+ # the body, shares it; a call into a method defined in Ruby writes that method's slot, never its caller's; and
17
+ # the root block of a thread, fiber or ractor has one of its own ({FreshFrameBlocks}). Only a C-implemented
18
+ # reader sets its caller's `$_`, to the line it returns: `Kernel#gets` / `#readline` and the same methods of
19
+ # `IO`, `StringIO`, `ARGF` and `Zlib::GzipReader`; `IO.foreach` sets it to each line it yields. `each_line` and
20
+ # `readlines` leave it alone.
21
+ #
22
+ # A call is read two ways, as the match globals' calls are:
23
+ #
24
+ # - {.reads_line?}: the call certainly is such a reader, run in this frame, so a condition on its value narrows
25
+ # `$_` on both edges ({Narrowing}). Apart from a write, that is the only place `$_` is bound.
26
+ # - {.may_set?}: running the code may set `$_` in this frame, so a narrowing of it is forgotten: a reader's or
27
+ # `foreach`'s name on any receiver (a `Dynamic` one, or one whose method turns out to be defined in Ruby, which
28
+ # only costs the narrowing), a `send` that may name one, an eval of a String, a write, or a block argument that
29
+ # may run one.
30
+ #
31
+ # A reader that is not a condition leaves `$_` unbound, not bound to its `String?`: nothing proves the line
32
+ # non-nil there, and a `String?` would report correct code that checks the reader's value another way before
33
+ # it reads `$_` (`line = gets; return unless line; $_.chomp`). The scans run only while `$_` is bound.
34
+ module LastLine
35
+ # The C readers' names.
36
+ READERS = Set[:gets, :readline].freeze
37
+ # Every name that may set its caller's `$_`, on any receiver: the readers, and `IO.foreach`, which sets it to
38
+ # each line before it yields the line and to nil once the input ends. Compared as Strings where a literal
39
+ # names one.
40
+ SETTERS = (READERS | Set[:foreach]).freeze
41
+ SETTER_NAMES = SETTERS.to_set(&:to_s).freeze
42
+ # The `Kernel` functions `ruby -n` / `-p` adds, which edit `$_` in place of an implicit-self receiver.
43
+ LINE_EDITORS = Set[:sub, :gsub, :chop, :chomp].freeze
44
+ # `$stdin`, and `$<`, which is `ARGF`, while the file binds them to nothing but a reader.
45
+ READER_GLOBALS = Set[:$stdin, :$<].freeze
46
+ # The RBS owners of a C reader. `Kernel`'s is not one: it is private, and RBS places there any reader it does not
47
+ # declare (`CSV#gets`, an alias of its Ruby `shift`, reads as `Kernel`'s). `Kernel.gets` itself is read by the
48
+ # receiver's type.
49
+ READER_OWNERS = Set["::IO", "::StringIO", "::RBS::Unnamed::ARGFClass", "::Zlib::GzipReader"].freeze
50
+ # RBS declares `Tempfile < File`, but its methods are `DelegateClass(File)`'s Ruby forwarders, so its `gets`
51
+ # sets the forwarder's `$_` and never its caller's.
52
+ DELEGATING_CLASSES = %w[Tempfile].freeze
53
+ DELEGATING_ORDERINGS = Set[:equal, :subclass].freeze
54
+ SENDS = Set[:send, :__send__, :public_send].freeze
55
+ # These run a String of code in the frame that calls them; their block form is a block, scanned as one.
56
+ EVALS = Set[:eval, :instance_eval, :class_eval, :module_eval].freeze
57
+ WRITES = Set[
58
+ Prism::GlobalVariableWriteNode, Prism::GlobalVariableOperatorWriteNode, Prism::GlobalVariableOrWriteNode,
59
+ Prism::GlobalVariableAndWriteNode, Prism::GlobalVariableTargetNode
60
+ ].freeze
61
+ LAST_LINE = :$_
62
+ # The calls that put a method in place under the name they are given: the new name, for `alias_method`.
63
+ DEFINERS = Set[:define_method, :define_singleton_method, :alias_method].freeze
64
+ DEFINER_NAMES = DEFINERS.to_set(&:to_s).freeze
65
+ ALL_READERS = READERS.to_a.freeze
66
+ # Compared as Strings: a literal with an invalid byte (`define_method("\xff")`) cannot become a Symbol.
67
+ READER_STRINGS = READERS.to_set(&:to_s).freeze
68
+ private_constant :DEFINERS, :DEFINER_NAMES, :ALL_READERS, :READER_STRINGS
69
+ private_constant :SETTER_NAMES, :LINE_EDITORS, :READER_GLOBALS, :READER_OWNERS, :DELEGATING_CLASSES,
70
+ :DELEGATING_ORDERINGS, :SENDS, :EVALS, :WRITES, :LAST_LINE
71
+
72
+ module_function
73
+
74
+ # True when `call_node` certainly is a C reader run in this frame, so `$_` holds what it returns: `gets` or
75
+ # `readline` without a block, on the `Kernel` module, on `$stdin` or `$<`, or on a receiver typed as a class
76
+ # whose reader RBS places in `IO`, `StringIO`, `ARGF` or `Zlib::GzipReader`, other than `Tempfile` (`STDIN` and
77
+ # `ARGF` read so by their types, and a project constant that shadows either reads by its own). A receiver typed
78
+ # `IO` or `File` that is a subclass instance at runtime is read as the class it is typed as.
79
+ #
80
+ # Issue #1415 (ADR-117 WD5) — an implicit-self or `self.` `gets` is one while the file shows no `self` whose
81
+ # reader may be Ruby's ({ImplicitSelf.reader?}). An implicit-self `readline` is not yet: it never returns
82
+ # falsey, so a `while readline` loop leaves only through a `break` or its `EOFError`, but the scope after the
83
+ # loop still joins the scope the body started from, where its narrowed line would report correct code.
84
+ #
85
+ # No reader is one whose name the program defines or patches in anywhere
86
+ # ({BlockCallTiming.project_defines_anywhere?}): a reopened `IO` or `Kernel`, or a project object bound to
87
+ # `$stdin` in another file, runs the program's Ruby method, which sets its own frame's `$_`.
88
+ def reads_line?(call_node, scope)
89
+ return false unless call_node.is_a?(Prism::CallNode) && READERS.include?(call_node.name)
90
+ return false unless call_node.block.nil?
91
+
92
+ name = call_node.name
93
+ return false if BlockCallTiming.project_defines_anywhere?(name, scope)
94
+ return false if scope.discovery.patched_line_readers.include?(name)
95
+
96
+ receiver = call_node.receiver
97
+ case receiver
98
+ when nil, Prism::SelfNode
99
+ name == :gets && ImplicitSelf.reader?(call_node, scope)
100
+ when Prism::GlobalVariableReadNode then reader_global?(receiver.name, name, scope)
101
+ else reader_receiver?(scope.type_of(receiver), name, scope)
102
+ end
103
+ rescue StandardError
104
+ false
105
+ end
106
+
107
+ # The edges of a condition whose value is `call_node`'s ({.reads_line?}): `pair`, the edges the condition
108
+ # narrows otherwise (nil for none), with `$_` bound on each to the line the call returned, narrowed as that
109
+ # value is — `String` where it is truthy and `nil` where it is not, for `gets`. The line is typed by the
110
+ # readers' RBS return, which every owner {.reads_line?} accepts declares alike (`String?` for `gets`, `String`
111
+ # for `readline`), rather than by the call's inferred type, which is `Dynamic[top]` on `$stdin`. A
112
+ # safe-navigation call answers nil without reading when its receiver is nil, so its falsey edge leaves `$_` as
113
+ # the call left it.
114
+ def predicate_scopes(call_node, scope, pair)
115
+ return pair unless reads_line?(call_node, scope)
116
+
117
+ line = line_type(call_node.name)
118
+ truthy, falsey = pair || [scope, scope]
119
+ truthy = truthy.with_global(LAST_LINE, Narrowing.narrow_truthy(line))
120
+ falsey = falsey.with_global(LAST_LINE, Narrowing.narrow_falsey(line)) unless call_node.safe_navigation?
121
+ [truthy, falsey]
122
+ end
123
+
124
+ # The reader names `node` patches in through the `define_method` family, on any receiver, or nil: a
125
+ # `define_method`, `define_singleton_method` or `alias_method` call (or a `send`, `__send__` or `public_send`
126
+ # naming one) and an `alias` whose new name is `gets` or `readline`, or is not a literal, which may be either.
127
+ # `Inference::ScopeIndexer` gathers them for the file ({Scope::DiscoveryIndex#patched_line_readers}).
128
+ def patched_readers(node)
129
+ case node
130
+ when Prism::AliasMethodNode then reader_named(node.new_name)
131
+ when Prism::CallNode
132
+ arguments = node.arguments&.arguments
133
+ if DEFINERS.include?(node.name)
134
+ reader_named(arguments&.first)
135
+ elsif SENDS.include?(node.name) && arguments && definer_name?(arguments.first)
136
+ reader_named(arguments[1])
137
+ end
138
+ end
139
+ end
140
+
141
+ # The readers a method-name argument may name: its own when it is a reader's literal, both when it is not a
142
+ # literal, and none for another literal or no argument at all.
143
+ def reader_named(name_node)
144
+ case name_node
145
+ when nil then nil
146
+ when Prism::SymbolNode, Prism::StringNode
147
+ name = name_node.unescaped
148
+ [name.to_sym] if READER_STRINGS.include?(name)
149
+ else ALL_READERS
150
+ end
151
+ end
152
+
153
+ def definer_name?(name_node)
154
+ (name_node.is_a?(Prism::SymbolNode) || name_node.is_a?(Prism::StringNode)) &&
155
+ DEFINER_NAMES.include?(name_node.unescaped)
156
+ end
157
+ private_class_method :reader_named, :definer_name?
158
+
159
+ # True when running `node` may set `$_` in the frame it runs in, anywhere in `node` but a nested `def`, class
160
+ # or module body or a root block ({FreshFrameBlocks.any_frame_child?}): a call {.call_may_set?} counts, a write
161
+ # to `$_`, or a block argument {.block_argument_may_set?} counts. A block or lambda inside `node` counts, since
162
+ # it runs in the same frame. Kept on `scope`'s frame as {MatchRebinding.may_match?} is.
163
+ def may_set?(node, scope = nil)
164
+ return false unless node.is_a?(Prism::Node)
165
+
166
+ MatchRebinding.remember(node, scope, :last_line) { scan(node, scope) }
167
+ end
168
+
169
+ def scan(node, scope)
170
+ return true if setting_node?(node, scope)
171
+ return false if MatchRebinding::OWN_FRAME_NODES.include?(node.class)
172
+
173
+ FreshFrameBlocks.any_frame_child?(node, scope) { |child| scan(child, scope) }
174
+ end
175
+
176
+ def setting_node?(node, scope)
177
+ case node
178
+ when Prism::CallNode then call_may_set?(node)
179
+ when Prism::BlockArgumentNode then block_argument_may_set?(node, scope)
180
+ else WRITES.include?(node.class) && node.name == LAST_LINE
181
+ end
182
+ end
183
+ private_class_method :scan, :setting_node?
184
+
185
+ # True when `call_node` itself may set its caller's `$_`: a {SETTERS} name on any receiver, a {LINE_EDITORS}
186
+ # name on implicit `self`, a `send` whose name is not a literal or names a setter, or an eval of a String.
187
+ def call_may_set?(call_node)
188
+ name = call_node.name
189
+ return true if SETTERS.include?(name) || (call_node.receiver.nil? && LINE_EDITORS.include?(name))
190
+
191
+ arguments = call_node.arguments&.arguments
192
+ return sent_reader?(arguments&.first) if SENDS.include?(name)
193
+
194
+ EVALS.include?(name) && !arguments.nil?
195
+ end
196
+
197
+ # True when a `&expr` block argument may pass a proc that sets `$_` in this frame: a `&:gets` or `&:readline`,
198
+ # which the method runs from C, or a proc made in this frame, as {MatchRebinding.block_argument_may_match?}
199
+ # reads one — not an anonymous `&`, nor the method's own `&block` it only forwards.
200
+ def block_argument_may_set?(block_argument, scope)
201
+ expression = block_argument.expression
202
+ case expression
203
+ when nil then false
204
+ when Prism::SymbolNode then SETTER_NAMES.include?(expression.unescaped)
205
+ when Prism::LocalVariableReadNode
206
+ frame = scope&.match_frame
207
+ frame.nil? || !frame.forwarded_block?(expression.name)
208
+ else true
209
+ end
210
+ end
211
+
212
+ # True when the block `call_node` passes may set `$_` while the call runs, as {MatchRebinding.block_may_match?}
213
+ # reads a block: a root block never does, though a root `&expr` argument's expression runs here first.
214
+ def block_may_set?(call_node, scope)
215
+ block = call_node.block
216
+ if FreshFrameBlocks.root_call?(call_node, scope)
217
+ return block.is_a?(Prism::BlockArgumentNode) && may_set?(block.expression, scope)
218
+ end
219
+
220
+ case block
221
+ when Prism::BlockNode then may_set?(block.body, scope)
222
+ when Prism::BlockArgumentNode then block_argument_may_set?(block, scope)
223
+ else false
224
+ end
225
+ end
226
+
227
+ # True when a call's receiver chain or arguments, which Ruby runs before the method, may set `$_`.
228
+ def operands_may_set?(call_node, scope)
229
+ may_set?(call_node.receiver, scope) || may_set?(call_node.arguments, scope)
230
+ end
231
+
232
+ # True when a statement's own call may set `$_` in this frame, once its operands ran: it may itself
233
+ # ({.call_may_set?}), its block may ({.block_may_set?}), the frame makes a closure that may, which any call
234
+ # can run ({.closure?}), or it is an implicit-self or `self.` call in a frame that hands `$_`'s slot to code the
235
+ # analyzer does not trace ({.fallback?}) or that no body stamped. A call into a method defined in Ruby sets
236
+ # that method's `$_`, so any other call keeps the narrowing.
237
+ def call_rebinds?(call_node, scope)
238
+ return true if call_may_set?(call_node) || block_may_set?(call_node, scope)
239
+
240
+ frame = scope.match_frame
241
+ implicit = call_node.receiver.nil? || call_node.receiver.is_a?(Prism::SelfNode)
242
+ return implicit if frame.nil?
243
+ return true if frame.last_line_closure?(scope)
244
+
245
+ implicit && frame.last_line_fallback?(scope)
246
+ end
247
+
248
+ # True when `node`, a frame's body or parameters, makes a lambda, or a block a call keeps to run later, whose
249
+ # body {.may_set?} ({MatchRebinding.makes_closure?}).
250
+ def closure?(node, scope)
251
+ MatchRebinding.makes_closure?(node, scope) { |body| may_set?(body, scope) }
252
+ end
253
+
254
+ # True when `node`, a frame's body or parameters, hands `$_`'s slot to code the analyzer does not trace: a block
255
+ # or lambda whose body {.may_set?}, which the method it is handed to may keep and run from a later call,
256
+ # `binding`, or a forward of the method's own block ({MatchRebinding.hands_out_slot?}).
257
+ def fallback?(node, block_name, scope)
258
+ MatchRebinding.hands_out_slot?(node, block_name, scope) { |body| may_set?(body, scope) }
259
+ end
260
+
261
+ # The scope a block or lambda body enters with, as {MatchRebinding.block_entry} gives the match globals': `$_`
262
+ # forgotten when the body may set it, which a later iteration then reads, when the frame makes a closure that
263
+ # may, or when the owning call may: its receiver chain and arguments run before the method yields, and
264
+ # `IO.foreach` sets `$_` to each line it yields.
265
+ def block_entry(scope, block_node, call_node = nil)
266
+ return scope unless scope.last_line_bound?
267
+ return scope unless scope.last_line_closure? || may_set?(block_node.body, scope) || call_sets?(call_node, scope)
268
+
269
+ scope.forget_last_line
270
+ end
271
+
272
+ def call_sets?(call_node, scope)
273
+ call_node.is_a?(Prism::CallNode) && (call_may_set?(call_node) || operands_may_set?(call_node, scope))
274
+ end
275
+ private_class_method :call_sets?
276
+
277
+ # `scope` with `$_` forgotten when any of `bodies` may set it: a loop body that runs again after it ran, a
278
+ # `begin` body a `rescue` clause reads after any prefix of it ran, or a `case` clause's tests.
279
+ def forget_if_set(scope, *bodies)
280
+ return scope unless scope.last_line_bound? && bodies.any? { |body| may_set?(body, scope) }
281
+
282
+ scope.forget_last_line
283
+ end
284
+
285
+ def line_type(reader)
286
+ string = Type::Combinator.nominal_of("String")
287
+ reader == :readline ? string : Type::Combinator.union(string, Type::Combinator.constant_of(nil))
288
+ end
289
+
290
+ def sent_reader?(name_node)
291
+ case name_node
292
+ when nil then false
293
+ when Prism::SymbolNode, Prism::StringNode then SETTER_NAMES.include?(name_node.unescaped)
294
+ else true
295
+ end
296
+ end
297
+ private_class_method :line_type, :sent_reader?
298
+
299
+ def reader_receiver?(type, name, scope)
300
+ return type.class_name == "Kernel" if type.is_a?(Type::Singleton)
301
+
302
+ reader_type?(type, name, scope)
303
+ end
304
+
305
+ # Issue #1362 (ADR-117 WD1) — a `$stdin` the file writes reads its declared `IO` joined with the written values,
306
+ # so a union whose every member is a reader's class (`IO | StringIO`) counts as one.
307
+ def reader_global?(global_name, name, scope)
308
+ return false unless READER_GLOBALS.include?(global_name)
309
+
310
+ bound = scope.global(global_name)
311
+ return true if bound.nil?
312
+
313
+ members = bound.is_a?(Type::Union) ? bound.members : [bound]
314
+ members.all? { |member| reader_type?(member, name, scope) }
315
+ end
316
+
317
+ def reader_type?(type, name, scope)
318
+ type.is_a?(Type::Nominal) && reader_class?(type.class_name, name, scope)
319
+ end
320
+
321
+ # A class RBS does not know answers false: its ancestry is the program's or a gem's, and may hold a Ruby reader.
322
+ def reader_class?(class_name, name, scope)
323
+ return false if delegating_class?(class_name, scope)
324
+
325
+ definition = Reflection.instance_method_definition(class_name, name, scope: scope)
326
+ !definition.nil? && READER_OWNERS.include?(definition.defined_in.to_s)
327
+ end
328
+
329
+ def delegating_class?(class_name, scope)
330
+ DELEGATING_CLASSES.any? do |delegating|
331
+ DELEGATING_ORDERINGS.include?(scope.environment.class_ordering(class_name, delegating))
332
+ end
333
+ end
334
+
335
+ private_class_method :reader_receiver?, :reader_global?, :reader_type?, :reader_class?, :delegating_class?
336
+ end
337
+ end
338
+ end
339
+
340
+ require_relative "last_line/implicit_self"