rigortype 0.4.4 → 0.4.6

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 (233) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -5
  3. data/docs/handbook/11-sig-gen.md +100 -18
  4. data/docs/manual/02-cli-reference.md +85 -16
  5. data/docs/manual/03-configuration.md +12 -9
  6. data/docs/manual/04-diagnostics.md +17 -6
  7. data/docs/manual/05-inspecting-types.md +2 -2
  8. data/docs/manual/06-baseline.md +1 -1
  9. data/docs/manual/07-plugins.md +2 -1
  10. data/docs/manual/08-skills.md +7 -0
  11. data/docs/manual/10-mcp-server.md +17 -2
  12. data/docs/manual/12-caching.md +8 -1
  13. data/docs/manual/18-removing-dead-code.md +18 -7
  14. data/docs/manual/plugins/rigor-actionmailer.md +30 -0
  15. data/docs/manual/plugins/rigor-actionpack.md +30 -0
  16. data/docs/manual/plugins/rigor-devise.md +15 -0
  17. data/docs/manual/plugins/rigor-rspec.md +102 -1
  18. data/lib/rigor/analysis/buffer_binding.rb +14 -0
  19. data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
  20. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +30 -9
  21. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +142 -15
  22. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +115 -7
  23. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +253 -0
  24. data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
  25. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
  26. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
  27. data/lib/rigor/analysis/check_rules/rule_ids.rb +3 -1
  28. data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
  29. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +64 -4
  30. data/lib/rigor/analysis/check_rules.rb +319 -21
  31. data/lib/rigor/analysis/dependency_recorder.rb +11 -0
  32. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
  33. data/lib/rigor/analysis/edit_replay.rb +67 -0
  34. data/lib/rigor/analysis/incremental.rb +86 -0
  35. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
  36. data/lib/rigor/analysis/incremental_session.rb +336 -46
  37. data/lib/rigor/analysis/path_expansion.rb +44 -1
  38. data/lib/rigor/analysis/reachability/graph.rb +90 -13
  39. data/lib/rigor/analysis/reachability/scan.rb +279 -39
  40. data/lib/rigor/analysis/require_relative_closure.rb +100 -0
  41. data/lib/rigor/analysis/rule_catalog.rb +22 -5
  42. data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
  43. data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
  44. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
  45. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +11 -3
  46. data/lib/rigor/analysis/runner/pool_coordinator.rb +82 -16
  47. data/lib/rigor/analysis/runner/project_pre_passes.rb +45 -3
  48. data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
  49. data/lib/rigor/analysis/runner.rb +222 -111
  50. data/lib/rigor/analysis/severity_stamp.rb +11 -0
  51. data/lib/rigor/analysis/worker_session.rb +24 -1
  52. data/lib/rigor/cache/descriptor.rb +40 -7
  53. data/lib/rigor/cache/engine_source.rb +3 -2
  54. data/lib/rigor/cache/incremental_snapshot.rb +49 -8
  55. data/lib/rigor/cache/rbs_descriptor.rb +2 -1
  56. data/lib/rigor/cache/store.rb +4 -3
  57. data/lib/rigor/cli/check_command.rb +93 -4
  58. data/lib/rigor/cli/command.rb +3 -1
  59. data/lib/rigor/cli/diff_command.rb +2 -1
  60. data/lib/rigor/cli/docs_command.rb +3 -2
  61. data/lib/rigor/cli/doctor_command.rb +1 -1
  62. data/lib/rigor/cli/explain_command.rb +3 -2
  63. data/lib/rigor/cli/lens_command.rb +5 -5
  64. data/lib/rigor/cli/mcp_command.rb +2 -1
  65. data/lib/rigor/cli/options.rb +20 -3
  66. data/lib/rigor/cli/plugin_command.rb +2 -1
  67. data/lib/rigor/cli/plugins_command.rb +7 -1
  68. data/lib/rigor/cli/plugins_renderer.rb +24 -2
  69. data/lib/rigor/cli/show_bleedingedge_command.rb +2 -1
  70. data/lib/rigor/cli/sig_gen_command.rb +73 -13
  71. data/lib/rigor/cli/skill_command.rb +2 -1
  72. data/lib/rigor/cli/skill_describe.rb +3 -2
  73. data/lib/rigor/cli/type_of_command.rb +2 -1
  74. data/lib/rigor/cli/type_of_project_scope.rb +27 -0
  75. data/lib/rigor/cli/unused_command.rb +10 -7
  76. data/lib/rigor/cli.rb +3 -2
  77. data/lib/rigor/config_audit.rb +72 -16
  78. data/lib/rigor/configuration/severity_profile.rb +20 -2
  79. data/lib/rigor/configuration.rb +43 -7
  80. data/lib/rigor/effects/signature_sources.rb +3 -1
  81. data/lib/rigor/environment/bundle_sig_discovery.rb +3 -2
  82. data/lib/rigor/environment/gated_signature_guard.rb +2 -1
  83. data/lib/rigor/environment/gated_signature_plan.rb +2 -1
  84. data/lib/rigor/environment/installed_gem_set.rb +2 -1
  85. data/lib/rigor/environment/missing_gem_constant_index.rb +4 -3
  86. data/lib/rigor/environment/rbs_loader.rb +136 -7
  87. data/lib/rigor/environment/required_features.rb +136 -29
  88. data/lib/rigor/environment/ruby_library_roots.rb +122 -0
  89. data/lib/rigor/environment.rb +4 -2
  90. data/lib/rigor/inference/block_call_timing.rb +7 -1
  91. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  92. data/lib/rigor/inference/callee_mutation_chain.rb +115 -0
  93. data/lib/rigor/inference/captured_locals.rb +44 -13
  94. data/lib/rigor/inference/clause_widening.rb +90 -0
  95. data/lib/rigor/inference/deep_erasure.rb +138 -0
  96. data/lib/rigor/inference/def_return_typer.rb +159 -9
  97. data/lib/rigor/inference/definer_resolution.rb +101 -20
  98. data/lib/rigor/inference/error_info.rb +17 -3
  99. data/lib/rigor/inference/expression_typer.rb +97 -18
  100. data/lib/rigor/inference/global_write_census.rb +36 -1
  101. data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
  102. data/lib/rigor/inference/ivar_writer_census.rb +195 -0
  103. data/lib/rigor/inference/loop_value_record.rb +53 -0
  104. data/lib/rigor/inference/macro_block_self_type.rb +20 -6
  105. data/lib/rigor/inference/match_rebinding/frame.rb +19 -0
  106. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +278 -42
  107. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +6 -2
  108. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
  109. data/lib/rigor/inference/method_dispatcher/set_folding.rb +53 -20
  110. data/lib/rigor/inference/method_dispatcher.rb +113 -19
  111. data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
  112. data/lib/rigor/inference/module_function_state.rb +149 -42
  113. data/lib/rigor/inference/mutation_widening.rb +5 -2
  114. data/lib/rigor/inference/narrowing.rb +116 -11
  115. data/lib/rigor/inference/object_mixins.rb +13 -1
  116. data/lib/rigor/inference/parameter_inference_collector.rb +4 -26
  117. data/lib/rigor/inference/project_patched_methods.rb +168 -4
  118. data/lib/rigor/inference/project_patched_mixins.rb +162 -0
  119. data/lib/rigor/inference/project_patched_scanner.rb +51 -12
  120. data/lib/rigor/inference/project_patched_visibility.rb +214 -0
  121. data/lib/rigor/inference/project_seed_tables.rb +121 -0
  122. data/lib/rigor/inference/project_yielder.rb +128 -0
  123. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +113 -0
  124. data/lib/rigor/inference/refined_dispatch/own_method.rb +41 -12
  125. data/lib/rigor/inference/reflective_ivar_write.rb +6 -6
  126. data/lib/rigor/inference/return_barrier.rb +16 -0
  127. data/lib/rigor/inference/scope_indexer/certainty.rb +9 -0
  128. data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
  129. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +162 -0
  130. data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
  131. data/lib/rigor/inference/scope_indexer.rb +1144 -197
  132. data/lib/rigor/inference/self_call_ivar_writes.rb +29 -12
  133. data/lib/rigor/inference/singleton_external_ahead.rb +64 -0
  134. data/lib/rigor/inference/singleton_hook_decline.rb +7 -0
  135. data/lib/rigor/inference/statement_evaluator.rb +1088 -213
  136. data/lib/rigor/inference/synthetic_method_index.rb +23 -0
  137. data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
  138. data/lib/rigor/inference/synthetic_method_scanner.rb +72 -12
  139. data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
  140. data/lib/rigor/language_server/buffer_table.rb +6 -0
  141. data/lib/rigor/language_server/diagnostic_publisher.rb +20 -6
  142. data/lib/rigor/language_server/project_context.rb +59 -3
  143. data/lib/rigor/language_server/server.rb +5 -3
  144. data/lib/rigor/lens/contract_reader.rb +45 -2
  145. data/lib/rigor/lens/document.rb +2 -1
  146. data/lib/rigor/lens/file_reader.rb +12 -2
  147. data/lib/rigor/lens/members.rb +16 -2
  148. data/lib/rigor/lens/run.rb +34 -7
  149. data/lib/rigor/mcp/server.rb +11 -3
  150. data/lib/rigor/plugin/base.rb +29 -13
  151. data/lib/rigor/plugin/io_boundary.rb +42 -6
  152. data/lib/rigor/plugin/isolation.rb +4 -3
  153. data/lib/rigor/plugin/manifest.rb +29 -3
  154. data/lib/rigor/plugin/registry.rb +38 -0
  155. data/lib/rigor/protection/discovery_seed.rb +64 -36
  156. data/lib/rigor/protection/mutation_cache.rb +8 -2
  157. data/lib/rigor/rooted_glob.rb +36 -0
  158. data/lib/rigor/scope/discovery_index.rb +5 -0
  159. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -0
  160. data/lib/rigor/scope/resolution_chain.rb +40 -5
  161. data/lib/rigor/scope.rb +59 -7
  162. data/lib/rigor/sig_gen/alias_index.rb +2 -1
  163. data/lib/rigor/sig_gen/classification.rb +4 -1
  164. data/lib/rigor/sig_gen/generator.rb +465 -49
  165. data/lib/rigor/sig_gen/layout_index.rb +5 -2
  166. data/lib/rigor/sig_gen/observation_collector.rb +141 -12
  167. data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
  168. data/lib/rigor/sig_gen/renderer.rb +16 -155
  169. data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
  170. data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
  171. data/lib/rigor/sig_gen/write_reporter.rb +203 -0
  172. data/lib/rigor/sig_gen/write_result.rb +3 -1
  173. data/lib/rigor/sig_gen/writer.rb +5 -3
  174. data/lib/rigor/signature_path_audit.rb +4 -2
  175. data/lib/rigor/type/combinator.rb +4 -3
  176. data/lib/rigor/type/constant.rb +54 -3
  177. data/lib/rigor/type/hash_shape.rb +14 -9
  178. data/lib/rigor/type/struct_instance.rb +3 -2
  179. data/lib/rigor/version.rb +1 -1
  180. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
  181. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +61 -44
  182. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +20 -1
  183. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +1 -1
  184. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +6 -1
  185. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  186. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +13 -0
  187. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +1 -1
  188. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +4 -3
  189. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +1 -1
  190. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -2
  191. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +1 -1
  192. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +1 -1
  193. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -1
  194. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
  195. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +7 -6
  196. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  197. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +38 -9
  198. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
  199. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +1 -1
  200. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +1 -1
  201. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +7 -2
  202. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -5
  203. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +18 -17
  204. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  205. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
  206. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
  207. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +4 -2
  208. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +4 -2
  209. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +2 -2
  210. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +2 -2
  211. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
  212. data/sig/rigor/analysis/baseline.rbs +3 -2
  213. data/sig/rigor/effects/file_collection.rbs +5 -6
  214. data/sig/rigor/effects/registry.rbs +8 -6
  215. data/sig/rigor/effects/summary.rbs +6 -5
  216. data/sig/rigor/environment.rbs +5 -3
  217. data/sig/rigor/inference/optimistic_origin.rbs +11 -0
  218. data/sig/rigor/inference.rbs +6 -0
  219. data/sig/rigor/plugin/additional_initializer.rbs +5 -5
  220. data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
  221. data/sig/rigor/plugin/effect_attribution.rbs +25 -10
  222. data/sig/rigor/plugin/effect_edge.rbs +12 -5
  223. data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
  224. data/sig/rigor/plugin/io_boundary.rbs +5 -2
  225. data/sig/rigor/plugin/manifest.rbs +16 -18
  226. data/sig/rigor/plugin/protocol_contract.rbs +10 -9
  227. data/sig/rigor/plugin/registry.rbs +1 -1
  228. data/sig/rigor/rooted_glob.rbs +7 -0
  229. data/sig/rigor/scope.rbs +34 -52
  230. data/sig/rigor/type.rbs +0 -6
  231. data/skills/rigor-doctor/references/01-checks.md +23 -11
  232. data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
  233. metadata +32 -1
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../source/node_children"
6
+ require_relative "../type"
7
+ require_relative "captured_locals"
8
+
9
+ module Rigor
10
+ module Inference
11
+ # Issues #1231 and #1839 — a rescue clause runs after the `begin` body raised from any point in it, and an `ensure`
12
+ # clause after the body, its `else` or a rescue clause raised, so each local or instance variable those wrote may
13
+ # hold any value one of their writes gave it, or the one it entered with. The engine does not track where the body
14
+ # raises; a clause enters with such a name rebound to `Dynamic[top]` instead ({.widen}).
15
+ #
16
+ # Not every name the body writes: one the clause mentions, whose binding the clause reads, and one the body
17
+ # writes more than once, which can hold a value neither the entry nor the body's exit binds. A name written once
18
+ # and never mentioned leaves the clause at its entry binding, which the join past the `begin` already pairs with
19
+ # the body's exit binding, the value of that one write (a write in a block or loop body included: the scope a
20
+ # block call or loop leaves joins every iteration's).
21
+ module ClauseWidening
22
+ WRITE_NODES = Set[
23
+ *CapturedLocals::LOCAL_WRITE_NODES,
24
+ Prism::InstanceVariableWriteNode,
25
+ Prism::InstanceVariableOperatorWriteNode,
26
+ Prism::InstanceVariableOrWriteNode,
27
+ Prism::InstanceVariableAndWriteNode,
28
+ Prism::InstanceVariableTargetNode
29
+ ].freeze
30
+
31
+ MENTION_NODES = Set[Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode, *WRITE_NODES].freeze
32
+
33
+ SCOPE_BODY_NODES = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
34
+
35
+ # The names `sources` write: `names` all of them, `repeated` those written more than once, a nested block's
36
+ # included (it may write an outer local). A `def` or class body writes nothing here.
37
+ Writes = Data.define(:names, :repeated)
38
+
39
+ module_function
40
+
41
+ # The {Writes} of `sources`, or nil for none.
42
+ def writes(sources)
43
+ counts = nil
44
+ sources.each { |source| counts = count_writes(source, counts) if source }
45
+ return nil if counts.nil?
46
+
47
+ Writes.new(names: counts.keys.to_set, repeated: counts.filter_map { |name, count| name if count > 1 }.to_set)
48
+ end
49
+
50
+ # `scope` with each name of `writes` that `clause` mentions, or that is repeated, rebound to `Dynamic[top]` where
51
+ # `scope` binds it, or nil when none changes. `clause` may be nil (an empty rescue clause).
52
+ def widen(scope, writes, clause)
53
+ return nil if writes.nil?
54
+
55
+ names = mentions(clause, writes.names, writes.repeated.dup)
56
+ untyped = Type::Combinator.untyped
57
+ widened = names.reduce(scope) do |acc, name|
58
+ ivar = name.start_with?("@")
59
+ current = ivar ? scope.ivar(name) : scope.local(name)
60
+ next acc if current.nil? || current == untyped
61
+
62
+ ivar ? acc.with_ivar(name, untyped).without_ivar_guard(name) : acc.with_local(name, untyped)
63
+ end
64
+ widened.equal?(scope) ? nil : widened
65
+ end
66
+
67
+ # `counts` with each write under `node` counted.
68
+ def count_writes(node, counts)
69
+ return counts if SCOPE_BODY_NODES.any? { |klass| node.is_a?(klass) }
70
+
71
+ if WRITE_NODES.include?(node.class)
72
+ counts ||= Hash.new(0)
73
+ counts[node.name] += 1
74
+ end
75
+ node.rigor_each_child { |child| counts = count_writes(child, counts) }
76
+ counts
77
+ end
78
+
79
+ # `found` with each name of `among` that `node` mentions added.
80
+ def mentions(node, among, found)
81
+ return found if node.nil? || SCOPE_BODY_NODES.any? { |klass| node.is_a?(klass) }
82
+
83
+ found << node.name if MENTION_NODES.include?(node.class) && among.include?(node.name)
84
+ node.rigor_each_child { |child| found = mentions(child, among, found) }
85
+ found
86
+ end
87
+ private_class_method :count_writes, :mentions
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+ require_relative "../type"
5
+ require_relative "../source/node_walker"
6
+ require_relative "mutation_rejoin"
7
+ require_relative "receiver_alias"
8
+
9
+ module Rigor
10
+ module Inference
11
+ # A loop hands its collection's own elements to its body (issue #2262), so a body that mutates one in place
12
+ # changes a shape that the collection's binding still carries one level down. This module is the two halves of
13
+ # dropping that shape: whether the body mutates an element, and the type with every shape dropped at every depth.
14
+ module DeepErasure
15
+ module_function
16
+
17
+ # {MutationWidening.widen_tuple} / {MutationWidening.widen_hash_shape} applied at every depth: each `Tuple` /
18
+ # `HashShape` the type holds, as a union member or a type argument, drops to its nominal, so `[[1]]` reads
19
+ # `Array[Array[Integer]]`. A loop's element is the collection's own object, so a body that mutates it in place
20
+ # (`x << 2`) changes a shape one level down that the collection's binding still carries.
21
+ def widen(type)
22
+ case type
23
+ when Type::Tuple then widen_args(MutationWidening.widen_tuple(type))
24
+ when Type::HashShape then widen_args(MutationWidening.widen_hash_shape(type))
25
+ when Type::Nominal then widen_args(type)
26
+ when Type::Difference then widen(type.base)
27
+ when Type::Union then Type::Combinator.union(*type.members.map { |member| widen(member) })
28
+ else Type::Combinator.widen_value_pinned(type)
29
+ end
30
+ end
31
+
32
+ def widen_args(nominal)
33
+ return nominal unless nominal.is_a?(Type::Nominal) && !nominal.type_args.empty?
34
+
35
+ Type::Combinator.nominal_of(nominal.class_name, type_args: nominal.type_args.map { |arg| widen(arg) })
36
+ end
37
+
38
+ # The parameter nodes whose names a block (or a `for` index) binds to the collection's elements.
39
+ ELEMENT_PARAMETER_NODES = [
40
+ Prism::RequiredParameterNode, Prism::OptionalParameterNode, Prism::RestParameterNode,
41
+ Prism::KeywordRestParameterNode, Prism::LocalVariableTargetNode
42
+ ].freeze
43
+ ELEMENT_WRITE_NODES = [
44
+ Prism::IndexOperatorWriteNode, Prism::IndexOrWriteNode, Prism::IndexAndWriteNode,
45
+ Prism::CallOperatorWriteNode, Prism::CallOrWriteNode, Prism::CallAndWriteNode,
46
+ Prism::IndexTargetNode, Prism::CallTargetNode
47
+ ].freeze
48
+ private_constant :ELEMENT_PARAMETER_NODES, :ELEMENT_WRITE_NODES
49
+
50
+ # True when `body` mutates in place a value that a name in `names` (a loop's element) holds: a mutator
51
+ # (`MutationWidening::SHAPE_MUTATORS`) called on a read or an index chain rooted at it (`x << 1`,
52
+ # `x[0].push(1)`, `x[0] = 1`), or an operator write on one (`x[0] += 1`). Every name a block or a
53
+ # `for` inside the body binds counts as an element too (`x.each { |y| y << 1 }`, `for c in x; c << 1; end`),
54
+ # and so does `it` / `_1` wherever it is read, and an accumulator parameter (`inject([]) { |acc, e| acc << e }`):
55
+ # the rule is wider than "an element", which only loses precision. It does not follow an alias (`y = x`) or
56
+ # an element passed to a call (`add(x)`), which the straight-line widening does not either.
57
+ def element_mutation?(body, names)
58
+ return false if body.nil?
59
+
60
+ names |= nested_parameter_names(body)
61
+ Source::NodeWalker.each(body).any? { |node| element_mutation_site?(node, names) }
62
+ end
63
+
64
+ # The names `parameters` (a block's parameter list, or a `for` index) binds.
65
+ def parameter_names(parameters)
66
+ return [] if parameters.nil?
67
+
68
+ Source::NodeWalker.each(parameters).filter_map do |node|
69
+ node.name if ELEMENT_PARAMETER_NODES.include?(node.class)
70
+ end
71
+ end
72
+
73
+ def nested_parameter_names(body)
74
+ Source::NodeWalker.each(body).flat_map do |node|
75
+ case node
76
+ when Prism::BlockNode then parameter_names(node.parameters)
77
+ when Prism::ForNode then parameter_names(node.index)
78
+ else []
79
+ end
80
+ end
81
+ end
82
+
83
+ def element_mutation_site?(node, names)
84
+ if node.is_a?(Prism::CallNode)
85
+ MutationWidening::SHAPE_MUTATORS.include?(node.name) && element_rooted?(node.receiver, names)
86
+ else
87
+ ELEMENT_WRITE_NODES.include?(node.class) && element_rooted?(node.receiver, names)
88
+ end
89
+ end
90
+
91
+ def element_rooted?(node, names)
92
+ node = node.receiver while node.is_a?(Prism::CallNode) && node.receiver
93
+ node = node.body.body.last while node.is_a?(Prism::ParenthesesNode) && node.body.is_a?(Prism::StatementsNode)
94
+ case node
95
+ when Prism::LocalVariableReadNode then names.include?(node.name) || /\A_[1-9]\z/.match?(node.name)
96
+ when Prism::ItLocalVariableReadNode then true
97
+ else false
98
+ end
99
+ end
100
+
101
+ # A block call hands each of its receiver's elements to the block's parameters, and a body that mutates one in
102
+ # place (`a.each { |x| x << 2 }`) changes the shape the receiver's binding still carries one level down
103
+ # (`[[1]]`). The receiver's variables drop their shapes at every depth ({widen}), as a `for` loop's
104
+ # value does ({StatementEvaluator#for_collection_after}).
105
+ def widen_element_aliases(receiver, body, names, scope)
106
+ return scope unless element_mutation?(body, names)
107
+
108
+ widen_receiver(receiver, scope)
109
+ end
110
+
111
+ def widen_receiver(receiver, scope)
112
+ ReceiverAlias.candidates(receiver).reduce(scope) { |acc, read| widen_read(read, acc) }
113
+ end
114
+
115
+ # `a.each(&:strip!)`: a block argument naming a mutator mutates every element it is handed.
116
+ def symbol_proc_mutation?(block)
117
+ block.is_a?(Prism::BlockArgumentNode) && block.expression.is_a?(Prism::SymbolNode) &&
118
+ MutationWidening::SHAPE_MUTATORS.include?(block.expression.unescaped.to_sym)
119
+ end
120
+
121
+ def widen_symbol_proc(call_node, scope) = widen_receiver(call_node.receiver, scope)
122
+
123
+ def widen_read(read, scope)
124
+ getter, builder = MutationWidening::ALIAS_ACCESSORS[read.class]
125
+ return scope if getter.nil?
126
+
127
+ name = ReceiverAlias.read_name(read)
128
+ current = scope.public_send(getter, name)
129
+ return scope if current.nil?
130
+
131
+ widened = widen(current)
132
+ return scope if widened == current
133
+
134
+ MutationRejoin.rebind(scope, builder, name, widened, pre_state: current, kind: getter)
135
+ end
136
+ end
137
+ end
138
+ end
@@ -5,6 +5,7 @@ require "prism"
5
5
  require_relative "../type"
6
6
  require_relative "../source/node_children"
7
7
  require_relative "return_barrier"
8
+ require_relative "optimistic_origin"
8
9
 
9
10
  module Rigor
10
11
  module Inference
@@ -21,10 +22,16 @@ module Rigor
21
22
  #
22
23
  # Extracted from `Rigor::SigGen::Generator#infer_return_type` so `LineTypeCollector` (`rigor annotate`'s
23
24
  # def-line annotator) and the sig-generator share one source of truth.
25
+ #
26
+ # `keep_lookup_nil: true` is the reading a signature needs (#2197). The engine reads a lookup that can miss
27
+ # (`Hash#[]`, `Array#first`, the {OptimisticOrigin} family) as nil-free on purpose, so `if h[k]` stays
28
+ # silent, but a `-> V` written into `sig/` is a contract that every caller's `nil` check is dead code. Each
29
+ # returned expression whose value is such a lookup therefore contributes `nil` beside its type. Only the
30
+ # value positions a lookup's `nil` reaches the return through are followed ({#optimistic_value?}).
24
31
  module DefReturnTyper
25
32
  module_function
26
33
 
27
- def call(def_node, scope_index)
34
+ def call(def_node, scope_index, keep_lookup_nil: false)
28
35
  body = def_node.body
29
36
  return nil if body.nil?
30
37
 
@@ -37,11 +44,152 @@ module Rigor
37
44
  last_type = safe_type_of(inner_scope, last)
38
45
  return nil if last_type.nil?
39
46
 
40
- union_with_explicit_returns(body, last_type, scope_index)
47
+ lookup_context = keep_lookup_nil ? LookupContext.new(scope_index, truthy_guards(body)) : nil
48
+ last_type = with_lookup_nil(last_type, last, inner_scope, lookup_context) if lookup_context
49
+ union_with_explicit_returns(body, last_type, scope_index, lookup_context: lookup_context)
41
50
  rescue StandardError
42
51
  nil
43
52
  end
44
53
 
54
+ # What {#optimistic_value?} reads beside a node's scope: the engine's per-node scopes, and for each lookup
55
+ # under a guard the source of the guarding predicates ({#truthy_guards}).
56
+ LookupContext = Data.define(:scope_index, :guards)
57
+
58
+ def with_lookup_nil(type, node, scope, context = nil)
59
+ return type unless optimistic_value?(node, scope, context)
60
+
61
+ Type::Combinator.union(type, Type::Combinator.constant_of(nil))
62
+ end
63
+
64
+ # Whether `node`'s value can be the `nil` an optimistic lookup's miss returns: the lookup itself (a call
65
+ # the dispatcher marked, or `recv&.m` over one), a local or ivar bound to one, or a branch or operand that
66
+ # hands its value on. `a && b` hands on either operand (a falsy `a`, a missed lookup's `nil` included, is
67
+ # the result); `a || b` hands on `b` only, since `a` is returned only when truthy, so `h[k] || 0` stays
68
+ # nil-free. `x ||= v` and `x &&= v` hand on `v` and, for `&&=`, the binding. A call ON the lookup
69
+ # (`h[k].to_s`) is a different value.
70
+ #
71
+ # Each child is read under the scope the engine recorded for it (`scope_index`), so a branch a guard narrows
72
+ # (`v ? v : "d"`, `return "z" unless v`) reads the binding the narrowing left, not the enclosing one.
73
+ def optimistic_value?(node, scope, context = nil)
74
+ scope = context&.scope_index&.[](node) || scope
75
+ case node
76
+ when Prism::CallNode then optimistic_call?(node, scope, context)
77
+ when Prism::LocalVariableReadNode then !scope.optimistic_local(node.name).nil?
78
+ when Prism::InstanceVariableReadNode then !scope.optimistic_ivar(node.name).nil?
79
+ when Prism::AndNode then [node.left, node.right].any? { optimistic_value?(it, scope, context) }
80
+ when Prism::OrNode then optimistic_value?(node.right, scope, context)
81
+ else optimistic_write?(node, scope, context) || optimistic_branch?(node, scope, context)
82
+ end
83
+ end
84
+
85
+ def optimistic_call?(node, scope, context)
86
+ if scope.optimistic_origins[node]
87
+ return false if guarded_lookup?(node, context) || non_empty_end?(node, scope)
88
+
89
+ return true
90
+ end
91
+ node.safe_navigation? && optimistic_value?(node.receiver, scope, context)
92
+ end
93
+
94
+ # `return h[k] if h[k]`, `h[k] ? h[k] : d`: the same lookup, read again under a predicate that just found it
95
+ # truthy. Only a lookup whose receiver and arguments are plain reads or literals counts, so the second read
96
+ # sees what the first did.
97
+ GUARDABLE_LOOKUPS = %i[[] first last dig].freeze
98
+ private_constant :GUARDABLE_LOOKUPS
99
+
100
+ def guarded_lookup?(node, context)
101
+ return false if context.nil? || !GUARDABLE_LOOKUPS.include?(node.name)
102
+ return false unless [node.receiver, *node.arguments&.arguments].all? { stable_operand?(it) }
103
+
104
+ context.guards.fetch(node, EMPTY_GUARDS).include?(node.slice)
105
+ end
106
+
107
+ EMPTY_GUARDS = [].freeze
108
+ private_constant :EMPTY_GUARDS
109
+
110
+ def stable_operand?(node)
111
+ case node
112
+ when Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode, Prism::SymbolNode, Prism::StringNode,
113
+ Prism::IntegerNode, Prism::SelfNode, Prism::ConstantReadNode then true
114
+ else false
115
+ end
116
+ end
117
+
118
+ # `a.first` / `a.last` on a receiver the engine has narrowed to a non-empty array (`a.empty? ? d : a.first`)
119
+ # cannot miss.
120
+ def non_empty_end?(node, scope)
121
+ return false unless %i[first last].include?(node.name) && node.arguments.nil? && node.receiver
122
+
123
+ receiver = scope.type_of(node.receiver)
124
+ receiver.is_a?(Type::Difference) && receiver.removed == Type::Combinator.tuple_of
125
+ rescue StandardError
126
+ false
127
+ end
128
+
129
+ def optimistic_write?(node, scope, context)
130
+ case node
131
+ when Prism::LocalVariableWriteNode, Prism::InstanceVariableWriteNode,
132
+ Prism::LocalVariableOrWriteNode, Prism::InstanceVariableOrWriteNode
133
+ optimistic_value?(node.value, scope, context)
134
+ when Prism::LocalVariableAndWriteNode
135
+ !scope.optimistic_local(node.name).nil? || optimistic_value?(node.value, scope, context)
136
+ when Prism::InstanceVariableAndWriteNode
137
+ !scope.optimistic_ivar(node.name).nil? || optimistic_value?(node.value, scope, context)
138
+ else false
139
+ end
140
+ end
141
+
142
+ # The value-carrying children of a statement list or a conditional: any of them can be the value returned.
143
+ def optimistic_branch?(node, scope, context)
144
+ value_branches(node).any? { |branch| !branch.nil? && optimistic_value?(branch, scope, context) }
145
+ end
146
+
147
+ # The lookup a truthy arm (the body of an `if` or ternary, the `else` of an `unless`) reads FIRST, mapped to
148
+ # the source of the predicate that just tested it, for {#guarded_lookup?}. Only the first expression, or the
149
+ # value of a first `return`, counts: anything run between the test and the re-read (`h.delete(k)`, a
150
+ # reassignment, a block, `yield`, another call that mutates the receiver) can make the re-read miss.
151
+ def truthy_guards(body)
152
+ guards = {}.compare_by_identity
153
+ collect_truthy_guards(body, guards)
154
+ guards
155
+ end
156
+
157
+ def collect_truthy_guards(node, guards)
158
+ return unless node.is_a?(Prism::Node)
159
+
160
+ predicate, arm = truthy_arm(node)
161
+ first = first_value(arm)
162
+ guards[first] = [predicate.slice] if first
163
+ node.rigor_each_child { collect_truthy_guards(it, guards) }
164
+ end
165
+
166
+ def first_value(arm)
167
+ return nil unless arm.is_a?(Prism::StatementsNode)
168
+
169
+ first = arm.body.first
170
+ args = first.is_a?(Prism::ReturnNode) ? first.arguments&.arguments : nil
171
+ args&.size == 1 ? args.first : first
172
+ end
173
+
174
+ def truthy_arm(node)
175
+ case node
176
+ when Prism::IfNode then [node.predicate, node.statements]
177
+ when Prism::UnlessNode then [node.predicate, node.else_clause&.statements]
178
+ end
179
+ end
180
+
181
+ def value_branches(node)
182
+ case node
183
+ when Prism::ParenthesesNode then [node.body && body_last_expression(node.body)]
184
+ when Prism::StatementsNode, Prism::BeginNode then [body_last_expression(node)]
185
+ when Prism::IfNode then [node.statements, node.subsequent]
186
+ when Prism::UnlessNode then [node.statements, node.else_clause]
187
+ when Prism::ElseNode, Prism::WhenNode, Prism::InNode then [node.statements]
188
+ when Prism::CaseNode, Prism::CaseMatchNode then [*node.conditions, node.else_clause]
189
+ else []
190
+ end
191
+ end
192
+
45
193
  def body_last_expression(body)
46
194
  case body
47
195
  when Prism::StatementsNode then body.body.last
@@ -50,27 +198,27 @@ module Rigor
50
198
  end
51
199
  end
52
200
 
53
- def union_with_explicit_returns(body, last_type, scope_index)
201
+ def union_with_explicit_returns(body, last_type, scope_index, lookup_context: nil)
54
202
  return_types = []
55
- collect_return_types(body, scope_index, return_types)
203
+ collect_return_types(body, scope_index, return_types, lookup_context: lookup_context)
56
204
  return last_type if return_types.empty?
57
205
 
58
206
  Type::Combinator.union(last_type, *return_types)
59
207
  end
60
208
 
61
- def collect_return_types(node, scope_index, out)
209
+ def collect_return_types(node, scope_index, out, lookup_context: nil)
62
210
  return unless node.is_a?(Prism::Node)
63
211
  return if ReturnBarrier.node?(node)
64
212
 
65
- type_return_node(node, scope_index, out) if node.is_a?(Prism::ReturnNode)
213
+ type_return_node(node, scope_index, out, lookup_context: lookup_context) if node.is_a?(Prism::ReturnNode)
66
214
  # A barrier call's receiver and arguments still run in the method; only its block is cut off.
67
215
  barrier_block = node.block if node.is_a?(Prism::CallNode) && ReturnBarrier.block_call?(node)
68
216
  node.rigor_each_child do |c|
69
- collect_return_types(c, scope_index, out) unless c.equal?(barrier_block)
217
+ collect_return_types(c, scope_index, out, lookup_context: lookup_context) unless c.equal?(barrier_block)
70
218
  end
71
219
  end
72
220
 
73
- def type_return_node(return_node, scope_index, out)
221
+ def type_return_node(return_node, scope_index, out, lookup_context: nil)
74
222
  args = return_node.arguments&.arguments || []
75
223
  if args.empty?
76
224
  out << Type::Combinator.constant_of(nil)
@@ -84,7 +232,9 @@ module Rigor
84
232
  return unless args.size == 1
85
233
 
86
234
  type = safe_type_of(scope, args.first)
87
- out << type unless type.nil?
235
+ return if type.nil?
236
+
237
+ out << (lookup_context ? with_lookup_nil(type, args.first, scope, lookup_context) : type)
88
238
  end
89
239
 
90
240
  def safe_type_of(scope, node)
@@ -61,7 +61,7 @@ module Rigor
61
61
  flavor = question == :arity ? :arity : :methods
62
62
  chain = Scope::ResolutionChain.for(scope, class_name.to_s, side, flavor)
63
63
 
64
- reads_visibility = VISIBILITY_QUESTIONS.include?(question)
64
+ reads_visibility = VISIBILITY_QUESTIONS.include?(question) ? question : nil
65
65
  hits = candidates(scope, chain, method_name, from, answer_in, reads_visibility)
66
66
  return UNKNOWN if hits.nil?
67
67
 
@@ -159,23 +159,39 @@ module Rigor
159
159
  end
160
160
 
161
161
  # The implicit tail after a chain's last project superclass: `Object` (with `Kernel` and `BasicObject`) on the
162
- # instance side, `#<Class:Object>` through `Class`, `Module` and `Kernel` on the singleton side.
162
+ # instance side, `#<Class:Object>` through `Class`, `Module` and `Kernel` on the singleton side. It may answer
163
+ # where RBS declares the name there, where the last class's superclass is not a constant the tables can follow
164
+ # (#1624, {Scope::ResolutionChain#opaque_tail?}: `class C < Struct.new(:a)`, `C = Class.new(Base)`), and where
165
+ # the project itself reopens a tail class with the name (`class Object; def self.x`), which is no entry on the
166
+ # chain because the class names no superclass.
163
167
  def implicit_object_answers?(scope, chain, method_name)
164
- !external_lacks?(scope, ["Object"], chain.side, method_name)
168
+ !external_lacks?(scope, ["Object"], chain.side, method_name) || chain.opaque_tail?(scope) ||
169
+ project_tail_defines?(scope, chain.side, method_name)
170
+ end
171
+
172
+ # The implicit tail's classes and the side each is read on, per chain side: a class object reaches the instance
173
+ # methods of `Class`, `Module`, `Object`, `Kernel` and `BasicObject` and the singleton methods of `Object` and
174
+ # `BasicObject`.
175
+ CLASS_OBJECT_TAIL = %w[Class Module Object Kernel BasicObject].product([:instance]) +
176
+ %w[Object BasicObject].product([:singleton])
177
+ IMPLICIT_TAIL = Ractor.make_shareable({ instance: %w[Object Kernel BasicObject].product([:instance]),
178
+ singleton: CLASS_OBJECT_TAIL })
179
+ private_constant :CLASS_OBJECT_TAIL, :IMPLICIT_TAIL
180
+
181
+ # Whether a project reopening of a tail class records the name: a `def`, whose read files the method edge, or
182
+ # an `attr_*` / `define_method` the existence table holds.
183
+ def project_tail_defines?(scope, side, method_name)
184
+ IMPLICIT_TAIL.fetch(side).any? do |name, kind|
185
+ (kind == :singleton ? scope.singleton_def_for(name, method_name) : scope.user_def_for(name, method_name)) ||
186
+ scope.discovered_method?(name, method_name, kind)
187
+ end
165
188
  end
166
189
 
167
190
  # True only for an ancestor RBS knows whose declaration lacks the name, on the side the entry stands for: an
168
191
  # external superclass on a singleton chain (`:singleton`, its whole tail) is asked for a singleton method, an
169
192
  # extended module (`:instance`) and every instance-side entry for an instance method.
170
193
  def external_lacks?(scope, candidates, side, method_name)
171
- return Scope::ResolutionChain::Relevance.external_lacks?(scope, candidates, method_name) if side == :instance
172
-
173
- known = candidates.find { |candidate| Rigor::Reflection.rbs_class_known?(candidate, scope: scope) }
174
- return false if known.nil?
175
-
176
- Rigor::Reflection.singleton_method_definition(known, method_name, scope: scope).nil?
177
- rescue StandardError
178
- false
194
+ Scope::ResolutionChain::Relevance.external_lacks_on?(scope, candidates, side, method_name)
179
195
  end
180
196
 
181
197
  # What a candidate set says, for {Scope::ResolutionChain#settle} to compare across worlds.
@@ -198,14 +214,27 @@ module Rigor
198
214
  # that reads the visibility (`reads_visibility`): a slot is contested there for a visibility call the walk
199
215
  # cannot place (an uncertain toggle, `private(*names)`, `private def self.x`), which leaves the definer and its
200
216
  # `def` certain; a possible `def` or `attr_*` call contests its def-node slot or its name's `possible` side too.
217
+ #
218
+ # `reads_visibility` is the question when it reads one. The `:visibility` question does not read a contested
219
+ # instance def-node slot (#1989): it answers from the visibility slot and the name's existence, not from the
220
+ # `def` body, and a possible `def` that contests the slot also makes its name `possible`. The slot is contested
221
+ # alone where a later `attr_*` replaced the `def`: the attr writes the name's visibility slot, so the reader's
222
+ # visibility is certain. A `define_method` or alias that replaced it contests the visibility slot as well, which
223
+ # this question does read.
224
+ #
225
+ # Issue #2134 — the contest is a read of the slot, so a question that reads it files `visibility:<owner>#<name>`:
226
+ # an answer block may answer from RBS without reading the slot itself (`nearest_ancestor_method_def`).
201
227
  def possible?(scope, hit, method_name, reads_visibility)
202
228
  discovery = scope.discovery
203
229
  owner = hit.owner
204
230
  kind = hit.side == :singleton ? :singleton : :instance
205
231
  nodes = kind == :singleton ? :discovered_singleton_def_nodes : :discovered_def_nodes
232
+ Analysis::DependencyRecorder.read_visibility(owner, method_name) if reads_visibility
206
233
  discovery.possible_method?(owner, method_name, kind) ||
207
- discovery.contested?(nodes, [owner, method_name.to_sym]) ||
208
- (reads_visibility && discovery.contested?(:discovered_method_visibilities, [owner, method_name.to_sym])) ||
234
+ (!(reads_visibility == :visibility && kind == :instance) &&
235
+ discovery.contested?(nodes, [owner, method_name.to_sym])) ||
236
+ (reads_visibility && discovery.contested?(:discovered_method_visibilities,
237
+ [owner, visibility_slot(method_name, kind)])) ||
209
238
  discovery.contested?(:discovered_parameter_envelopes, [owner, [kind, method_name.to_sym]])
210
239
  end
211
240
 
@@ -235,20 +264,71 @@ module Rigor
235
264
 
236
265
  # The first project entry that records the name (a discovered method, an instance `def` the def table
237
266
  # holds, or a visibility change such as `private :foo` recorded for it), with its visibility (`:public` where
238
- # none was stated).
267
+ # none was stated). Issue #1984 — a singleton-side entry reads its class's singleton slot
268
+ # (`private_class_method :new` records `new` there with no `def`), never the instance one.
239
269
  def visibility_answer(scope, method_name)
240
270
  lambda do |chain, from|
241
271
  first_hit(chain, scope, from) do |entry|
242
272
  kind = entry.side == :singleton ? :singleton : :instance
243
- next unless scope.discovered_method?(entry.name, method_name, kind) ||
244
- (kind == :instance && (scope.user_def_for(entry.name, method_name) ||
245
- scope.discovered_method_visibility(entry.name, method_name)))
273
+ recorded = side_visibility(scope, entry.name, method_name, kind)
274
+ next unless scope.discovered_method?(entry.name, method_name, kind) || recorded ||
275
+ (kind == :instance && scope.user_def_for(entry.name, method_name))
276
+
277
+ recorded || :public
278
+ end
279
+ end
280
+ end
281
+
282
+ def side_visibility(scope, owner, method_name, kind)
283
+ if kind == :singleton
284
+ scope.discovered_method_visibility(owner, method_name, singleton: true) ||
285
+ self_extended_copy_visibility(scope, owner, method_name)
286
+ else
287
+ scope.discovered_method_visibility(owner, method_name)
288
+ end
289
+ end
246
290
 
247
- scope.discovered_method_visibility(entry.name, method_name) || :public
291
+ # Issue #2059 — a module that `extend`s itself reaches its own instance method from the singleton side, and the
292
+ # extends fold copies that receiverless `def` onto the module's singleton entry, where no singleton slot records
293
+ # it: the instance slot's visibility is what Ruby applies (`module ES; extend self; private def x` makes `ES.x`
294
+ # private). A module function's singleton copy is public whatever its instance copy is, so a `module_function`
295
+ # row for the name in any file answers nil, as does a contested instance slot.
296
+ def self_extended_copy_visibility(scope, owner, method_name)
297
+ node = scope.singleton_def_for(owner, method_name)
298
+ return nil if node.nil? || !node.receiver.nil?
299
+ return nil unless scope.discovered_extends[owner.to_s]&.include?(owner.to_s)
300
+ return nil if scope.discovery.contested?(:discovered_method_visibilities, [owner.to_s, method_name.to_sym])
301
+ return nil if module_function_row?(scope, owner, method_name)
302
+
303
+ scope.discovered_method_visibility(owner, method_name)
304
+ end
305
+
306
+ # Whether any file's deferred ranges carry a `module_function` row (`:both` or `:singleton`) for the name on
307
+ # `owner` (`ModuleFunctionState`).
308
+ #
309
+ # Issue #2161 — the answer reads every file's rows, so it records `visibility:<owner>#<name>` whichever way it
310
+ # answers: `ScopeIndexer.visibility_entries` carries each file's rows of this shape, so a file that starts or
311
+ # stops writing one fires the key. A swap from `private :x` to `module_function :x` in a reopening leaves the
312
+ # file's visibility table and declared classes as they were, and fires nothing else. The caller's own
313
+ # visibility reads record the same key today; this one keeps the reader's edge where it reads.
314
+ #
315
+ # Issue #2209 — a row named {ModuleFunctionState::ANY_ROW_NAME} (a `module_function` spelling whose names the
316
+ # prescan cannot read) stands for every name of its owner, so it counts too, and its key is read as well
317
+ # ({ModuleFunctionState.row_names}).
318
+ def module_function_row?(scope, owner, method_name)
319
+ names = ModuleFunctionState.row_names(method_name).each { Analysis::DependencyRecorder.read_visibility(owner, it) }
320
+ scope.discovered_deferred_ranges.each_value.any? do |rows|
321
+ rows.any? do |(_start, _finish, row_name, kind, row_owner)|
322
+ names.include?(row_name) && row_owner == owner.to_s && kind != :instance
248
323
  end
249
324
  end
250
325
  end
251
326
 
327
+ # The key of a name's slot in the visibility table on `kind`'s side (`ScopeIndexer#visibility_slot`).
328
+ def visibility_slot(method_name, kind)
329
+ kind == :singleton ? [:singleton, method_name.to_sym] : method_name.to_sym
330
+ end
331
+
252
332
  def first_hit(chain, scope, from)
253
333
  index = from - 1
254
334
  found = nil
@@ -265,9 +345,10 @@ module Rigor
265
345
  end
266
346
 
267
347
  private_class_method :override_answer, :retro_position, :candidates, :past_fold_copies, :decided,
268
- :external_may_answer?, :implicit_object_answers?, :external_lacks?,
348
+ :external_may_answer?, :implicit_object_answers?, :project_tail_defines?, :external_lacks?,
269
349
  :outcomes, :collapse, :possible?,
270
- :default_answer, :definer_answer, :visibility_answer, :first_hit
350
+ :default_answer, :definer_answer, :visibility_answer, :side_visibility,
351
+ :self_extended_copy_visibility, :module_function_row?, :visibility_slot, :first_hit
271
352
  end
272
353
  end
273
354
  end
@@ -53,8 +53,8 @@ module Rigor
53
53
  # made a class guard on `$!` itself narrow it; #1447 narrows this decline to the guards that still do not.
54
54
  #
55
55
  # `$@` calls the exception's `backtrace`, which is an `Array[String]` for a raised exception. It is left unbound
56
- # in a program that defines a method named `backtrace` anywhere, which may return anything, and in a clause
57
- # whose body calls `set_backtrace`, which may set it to nil.
56
+ # in a program that defines a method named `backtrace` anywhere, which may return anything, in a clause whose
57
+ # body calls `set_backtrace`, which may set it to nil, and in a clause whose body never reads it.
58
58
  #
59
59
  # `$?` is unbound because the exception may have been raised while a subprocess waited: a backtick, `%x` or
60
60
  # `system` sets `$?` to nil before it runs the child, so a `Timeout::Error`, an `Interrupt` or a `Thread#raise`
@@ -65,6 +65,10 @@ module Rigor
65
65
  return entered if guarded?(body, reference)
66
66
 
67
67
  entered = entered.with_global(ERROR_INFO, rescued_type(exception_type, scope))
68
+ # Issue #1967 — a clause that never reads `$@` leaves it unbound without asking about `backtrace`: the answer
69
+ # could not reach a diagnostic, and asking records a `defines:backtrace` edge that re-checked every `rescue`
70
+ # in the program whenever such a def came or went.
71
+ return entered unless backtrace_read?(body)
68
72
  return entered if calls_set_backtrace?(body) || BlockCallTiming.project_defines_anywhere?(:backtrace, scope)
69
73
 
70
74
  entered.with_global(BACKTRACE, backtrace_type)
@@ -114,7 +118,17 @@ module Rigor
114
118
  node.rigor_each_child { |child| found ||= calls_set_backtrace?(child) }
115
119
  found
116
120
  end
117
- private_class_method :guard?, :error_info_read?, :calls_set_backtrace?
121
+
122
+ # True when `node` reads `$@` anywhere in it, a block included. An empty clause (nil) reads nothing.
123
+ def backtrace_read?(node)
124
+ return false unless node.is_a?(Prism::Node)
125
+ return node.name == BACKTRACE if node.is_a?(Prism::GlobalVariableReadNode)
126
+
127
+ found = false
128
+ node.rigor_each_child { |child| found ||= backtrace_read?(child) }
129
+ found
130
+ end
131
+ private_class_method :guard?, :error_info_read?, :calls_set_backtrace?, :backtrace_read?
118
132
 
119
133
  # True when `node` reads `$!`, `$@` or `$?` anywhere in it: the gate on typing a rescue modifier's fallback
120
134
  # under {.modifier_entry}, which most fallbacks (`rescue nil`) never need.