rigortype 0.4.3 → 0.4.5

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 (172) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -5
  3. data/data/core_overlay/ostruct.rbs +40 -0
  4. data/docs/handbook/11-sig-gen.md +104 -15
  5. data/docs/manual/02-cli-reference.md +43 -12
  6. data/docs/manual/03-configuration.md +20 -7
  7. data/docs/manual/04-diagnostics.md +56 -3
  8. data/docs/manual/05-inspecting-types.md +2 -2
  9. data/docs/manual/06-baseline.md +1 -1
  10. data/docs/manual/07-plugins.md +3 -2
  11. data/docs/manual/08-skills.md +7 -0
  12. data/docs/manual/10-mcp-server.md +40 -4
  13. data/docs/manual/18-removing-dead-code.md +23 -2
  14. data/docs/manual/plugins/rigor-rails-routes.md +2 -2
  15. data/docs/manual/plugins/rigor-rspec.md +101 -1
  16. data/docs/manual/plugins/rigor-shoulda-matchers.md +4 -1
  17. data/exe/rigor +12 -17
  18. data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
  19. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +111 -7
  20. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +190 -0
  21. data/lib/rigor/analysis/check_rules/rule_ids.rb +60 -1
  22. data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
  23. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +13 -4
  24. data/lib/rigor/analysis/check_rules.rb +274 -51
  25. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
  26. data/lib/rigor/analysis/incremental.rb +61 -0
  27. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
  28. data/lib/rigor/analysis/incremental_session.rb +270 -28
  29. data/lib/rigor/analysis/path_expansion.rb +3 -1
  30. data/lib/rigor/analysis/reachability/graph.rb +111 -14
  31. data/lib/rigor/analysis/reachability/scan.rb +305 -23
  32. data/lib/rigor/analysis/rule_catalog.rb +22 -4
  33. data/lib/rigor/analysis/run_cache_key.rb +31 -7
  34. data/lib/rigor/analysis/run_cache_probe.rb +5 -3
  35. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +48 -12
  36. data/lib/rigor/analysis/runner/pool_coordinator.rb +179 -31
  37. data/lib/rigor/analysis/runner/project_pre_passes.rb +29 -4
  38. data/lib/rigor/analysis/runner.rb +102 -72
  39. data/lib/rigor/analysis/severity_stamp.rb +45 -1
  40. data/lib/rigor/analysis/worker_session.rb +69 -2
  41. data/lib/rigor/cache/descriptor.rb +79 -12
  42. data/lib/rigor/cache/engine_source.rb +3 -2
  43. data/lib/rigor/cache/incremental_snapshot.rb +39 -5
  44. data/lib/rigor/cache/rbs_descriptor.rb +2 -1
  45. data/lib/rigor/cache/store.rb +4 -3
  46. data/lib/rigor/cli/command.rb +3 -1
  47. data/lib/rigor/cli/docs_command.rb +3 -2
  48. data/lib/rigor/cli/doctor_command.rb +1 -1
  49. data/lib/rigor/cli/explain_command.rb +3 -2
  50. data/lib/rigor/cli/mcp_command.rb +1 -1
  51. data/lib/rigor/cli/plugin_command.rb +2 -1
  52. data/lib/rigor/cli/plugins_command.rb +2 -1
  53. data/lib/rigor/cli/sig_gen_command.rb +32 -11
  54. data/lib/rigor/cli/skill_command.rb +2 -1
  55. data/lib/rigor/cli/skill_describe.rb +3 -2
  56. data/lib/rigor/cli/unused_command.rb +10 -7
  57. data/lib/rigor/cli.rb +3 -2
  58. data/lib/rigor/config_audit.rb +163 -22
  59. data/lib/rigor/configuration/dependencies.rb +19 -11
  60. data/lib/rigor/configuration/severity_profile.rb +20 -2
  61. data/lib/rigor/configuration.rb +29 -3
  62. data/lib/rigor/effects/signature_sources.rb +3 -1
  63. data/lib/rigor/environment/bundle_sig_discovery.rb +3 -2
  64. data/lib/rigor/environment/gated_signature_guard.rb +2 -1
  65. data/lib/rigor/environment/gated_signature_plan.rb +2 -1
  66. data/lib/rigor/environment/installed_gem_set.rb +2 -1
  67. data/lib/rigor/environment/missing_gem_constant_index.rb +4 -3
  68. data/lib/rigor/environment/rbs_loader.rb +67 -11
  69. data/lib/rigor/inference/block_call_timing.rb +12 -1
  70. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  71. data/lib/rigor/inference/captured_locals.rb +67 -13
  72. data/lib/rigor/inference/clause_widening.rb +90 -0
  73. data/lib/rigor/inference/definer_resolution.rb +101 -25
  74. data/lib/rigor/inference/expression_typer.rb +94 -9
  75. data/lib/rigor/inference/guard_rebinding.rb +7 -2
  76. data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
  77. data/lib/rigor/inference/last_line/self_evidence.rb +1 -1
  78. data/lib/rigor/inference/macro_block_self_type.rb +20 -6
  79. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +170 -35
  80. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
  81. data/lib/rigor/inference/method_dispatcher.rb +67 -3
  82. data/lib/rigor/inference/module_function_state.rb +213 -51
  83. data/lib/rigor/inference/narrowing.rb +235 -75
  84. data/lib/rigor/inference/parameter_inference_collector.rb +4 -26
  85. data/lib/rigor/inference/project_patched_methods.rb +168 -4
  86. data/lib/rigor/inference/project_patched_mixins.rb +162 -0
  87. data/lib/rigor/inference/project_patched_scanner.rb +51 -12
  88. data/lib/rigor/inference/project_patched_visibility.rb +214 -0
  89. data/lib/rigor/inference/project_seed_tables.rb +120 -0
  90. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +113 -0
  91. data/lib/rigor/inference/refined_dispatch/own_method.rb +41 -12
  92. data/lib/rigor/inference/reflective_ivar_write.rb +116 -0
  93. data/lib/rigor/inference/repeated_or_writes.rb +414 -1
  94. data/lib/rigor/inference/return_barrier.rb +16 -0
  95. data/lib/rigor/inference/scope_indexer.rb +1061 -122
  96. data/lib/rigor/inference/self_call_ivar_writes.rb +346 -0
  97. data/lib/rigor/inference/singleton_external_ahead.rb +64 -0
  98. data/lib/rigor/inference/singleton_hook_decline.rb +7 -0
  99. data/lib/rigor/inference/statement_evaluator.rb +512 -150
  100. data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
  101. data/lib/rigor/lens/anchor.rb +3 -3
  102. data/lib/rigor/lens/contract_reader.rb +60 -2
  103. data/lib/rigor/lens/document.rb +4 -3
  104. data/lib/rigor/lens/file_reader.rb +452 -73
  105. data/lib/rigor/lens/members.rb +81 -17
  106. data/lib/rigor/lens/run.rb +73 -4
  107. data/lib/rigor/lens.rb +2 -1
  108. data/lib/rigor/mcp/server.rb +63 -22
  109. data/lib/rigor/plugin/base.rb +3 -2
  110. data/lib/rigor/plugin/box_probe.rb +154 -7
  111. data/lib/rigor/plugin/io_boundary.rb +42 -6
  112. data/lib/rigor/plugin/isolation.rb +4 -3
  113. data/lib/rigor/project_environment.rb +6 -3
  114. data/lib/rigor/protection/discovery_seed.rb +9 -36
  115. data/lib/rigor/protection/mutation_cache.rb +8 -2
  116. data/lib/rigor/rooted_glob.rb +27 -0
  117. data/lib/rigor/scope/discovery_index.rb +5 -0
  118. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -0
  119. data/lib/rigor/scope/resolution_chain.rb +40 -5
  120. data/lib/rigor/scope.rb +56 -8
  121. data/lib/rigor/sig_gen/alias_index.rb +2 -1
  122. data/lib/rigor/sig_gen/classification.rb +4 -1
  123. data/lib/rigor/sig_gen/generator.rb +167 -22
  124. data/lib/rigor/sig_gen/layout_index.rb +40 -1
  125. data/lib/rigor/sig_gen/method_candidate.rb +8 -0
  126. data/lib/rigor/sig_gen/observation_collector.rb +137 -11
  127. data/lib/rigor/sig_gen/path_mapper.rb +18 -2
  128. data/lib/rigor/sig_gen/renderer.rb +13 -155
  129. data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
  130. data/lib/rigor/sig_gen/write_reporter.rb +203 -0
  131. data/lib/rigor/sig_gen/write_result.rb +3 -1
  132. data/lib/rigor/sig_gen/writer.rb +50 -35
  133. data/lib/rigor/signature_path_audit.rb +4 -2
  134. data/lib/rigor/type/constant.rb +10 -1
  135. data/lib/rigor/version.rb +1 -1
  136. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
  137. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +1 -1
  138. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +1 -1
  139. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +6 -1
  140. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  141. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +1 -1
  142. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +4 -3
  143. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +1 -1
  144. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +1 -1
  145. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +1 -1
  146. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -1
  147. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +5 -1
  148. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
  149. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
  150. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  151. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +57 -6
  152. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
  153. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +1 -1
  154. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +5 -1
  155. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +1 -1
  156. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +1 -1
  157. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +7 -2
  158. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -5
  159. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +18 -17
  160. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  161. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +155 -12
  162. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +10 -8
  163. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
  164. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +4 -2
  165. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +2 -2
  166. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +2 -2
  167. data/sig/rigor/plugin/io_boundary.rbs +5 -2
  168. data/sig/rigor/rooted_glob.rbs +7 -0
  169. data/sig/rigor/scope.rbs +4 -3
  170. data/skills/rigor-doctor/references/01-checks.md +23 -11
  171. data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
  172. metadata +16 -1
@@ -72,6 +72,12 @@ module Rigor
72
72
 
73
73
  module_function
74
74
 
75
+ # Whether `call_node` is spelled to reach the private `Kernel#loop` (see {.loop_may_complete?}), which runs its
76
+ # block until it breaks or raises.
77
+ def kernel_loop?(call_node)
78
+ call_node.name == :loop && kernel_spelled_receiver?(call_node.receiver)
79
+ end
80
+
75
81
  # Issue #1107 — whether a `loop` call can complete normally although its declared return is `bot`.
76
82
  #
77
83
  # `Kernel#loop` is `() { () -> void } -> bot` in core RBS, yet it rescues a `StopIteration` its block
@@ -86,7 +92,7 @@ module Rigor
86
92
  # promise. A project redefinition of `loop` itself is not excluded: widening its `bot` costs precision,
87
93
  # never a false positive.
88
94
  def loop_may_complete?(call_node)
89
- return false unless call_node.name == :loop && kernel_spelled_receiver?(call_node.receiver)
95
+ return false unless kernel_loop?(call_node)
90
96
 
91
97
  block = call_node.block
92
98
  return false if block.nil?
@@ -165,7 +171,12 @@ module Rigor
165
171
  # nothing for names this rare, and the block-return pass is no independent check here: it types a
166
172
  # self-call to an overridden `raise` as Kernel's `bot` too, so a missed override is a wrong `bot`.
167
173
  # {LastLine.reads_line?} declines on it the same way for `gets` and `readline`.
174
+ #
175
+ # Issue #1459 — the answer reads every project file's tables whole, so it records a `defines:<name>` edge
176
+ # whichever way it answers: an edit that makes a file start or stop defining the name re-checks the reader
177
+ # (`Incremental.changed_defined_names`). A `pre_eval:` patch moves the snapshot's own fingerprint.
168
178
  def project_defines_anywhere?(method_name, scope)
179
+ Analysis::DependencyRecorder.read_missing(:defines, method_name) if Analysis::DependencyRecorder.active?
169
180
  return true if scope.top_level_def_for(method_name)
170
181
  # `discovered_methods` withholds a plain cross-file `def` (the ADR-17 monkey-patch contract); the
171
182
  # def-node tables still carry it, on both sides.
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "yaml"
4
+ require_relative "../../rooted_glob"
4
5
 
5
6
  module Rigor
6
7
  module Inference
@@ -49,7 +50,7 @@ module Rigor
49
50
  end
50
51
 
51
52
  # Every catalogue file under {DATA_ROOT}, in a stable order.
52
- def self.topic_paths = Dir[File.join(DATA_ROOT, "*.yml")]
53
+ def self.topic_paths = RootedGlob.glob(DATA_ROOT, "*.yml")
53
54
 
54
55
  def initialize(path:, mutating_selectors: {})
55
56
  @path = path
@@ -6,9 +6,11 @@ require_relative "../source/node_walker"
6
6
  require_relative "block_parameter_binder"
7
7
  require_relative "element_read_widening"
8
8
  require_relative "index_write_widening"
9
+ require_relative "intervening_ivar_reset"
9
10
  require_relative "mutation_widening"
10
11
  require_relative "optimistic_origin"
11
12
  require_relative "receiver_alias"
13
+ require_relative "reflective_ivar_write"
12
14
  require_relative "unknown_store_widening"
13
15
 
14
16
  module Rigor
@@ -108,16 +110,22 @@ module Rigor
108
110
  # `base_scope`), except that one still on its class-wide binding does not: ADR-58's declaration seed is
109
111
  # the union of every write in the class, this body's included, so nothing the body stores can move it,
110
112
  # and rebinding it would only drop the declaration mark that keeps its nil from being diagnostic fuel.
111
- # A nested block that rebinds `self` (`o.instance_eval`) writes another object's ivar, and a nested
112
- # `def` runs only when called; both still count, because an `instance_eval` without a receiver, or a
113
- # call to that `def` inside the body, does write this one.
113
+ # Issue #1402 — an instance variable written in a nested block that runs on an object provably not `self`
114
+ # (`Other.class_eval { @x = 1 }`, {InterveningIvarReset.foreign_self_block?}, its receiver typed in
115
+ # `base_scope`) is that object's, and does not count; one on a receiver that may be `self` does. A class
116
+ # variable or global written there counts as anywhere else: Ruby resolves a class variable lexically, and
117
+ # a global is the same one whatever `self` is. A nested `def` runs only when called; it still counts,
118
+ # because a call to it inside the body writes this one.
119
+ # @param foreign_self — the block itself runs on an object provably not `self`
120
+ # ({InterveningIvarReset.foreign_self_block?} of its call): no instance variable it writes is the caller's.
121
+ # Its locals, class variables and globals still count.
114
122
  # @param non_locals — the per-element fold's superset of `ivars`: the class variables and globals the body
115
123
  # rebinds count too, on the same terms, and so does an attribute setter called on `self` (`self.w = v`)
116
124
  # as a rebind of the instance variable it is named after, `@w`: the `attr_writer` / `attr_accessor`
117
125
  # convention stores there, and no write node shows it. A hand-written setter storing elsewhere is not
118
126
  # seen.
119
127
  # @return the captured names the body writes, each once, in first-write order.
120
- def writes(block_node, base_scope, ivars: false, non_locals: false)
128
+ def writes(block_node, base_scope, ivars: false, non_locals: false, foreign_self: false)
121
129
  body = block_node.body
122
130
  return NO_NAMES if body.nil?
123
131
 
@@ -125,17 +133,51 @@ module Rigor
125
133
  names = nil
126
134
  outliving = non_locals || ivars
127
135
  Source::NodeWalker.each_with_ancestors(body) do |descendant, ancestors|
128
- name =
129
- if LOCAL_WRITE_NODES.include?(descendant.class)
130
- captured_local_write(descendant, ancestors, base_scope) { introduced ||= introduced_locals(block_node) }
131
- elsif outliving
132
- rebindable_outliving_write(descendant, base_scope, non_locals)
136
+ if LOCAL_WRITE_NODES.include?(descendant.class)
137
+ name = captured_local_write(descendant, ancestors, base_scope) do
138
+ introduced ||= introduced_locals(block_node)
133
139
  end
134
- (names ||= []) << name if name
140
+ (names ||= []) << name if name
141
+ elsif outliving
142
+ found = outliving_writes(descendant, ancestors, base_scope, non_locals, foreign_self)
143
+ (names ||= []).concat(found) unless found.empty?
144
+ end
135
145
  end
136
146
  names ? names.uniq : NO_NAMES
137
147
  end
138
148
 
149
+ # The non-local names `node` writes ({.reflective_unknown_ivars} for an `instance_variable_set` on `self` whose
150
+ # target no literal names, then {.rebindable_outliving_write}), less the instance variables when the write
151
+ # runs on an object provably not `self` (`foreign_self`, or a nested block {.under_foreign_self_block?}).
152
+ def outliving_writes(node, ancestors, base_scope, non_locals, foreign_self)
153
+ found = ReflectiveIvarWrite.unknown_target?(node) ? reflective_unknown_ivars(base_scope) : NO_NAMES
154
+ written = rebindable_outliving_write(node, base_scope, non_locals)
155
+ return found if written.nil? && found.empty?
156
+
157
+ names = written ? found + [written] : found
158
+ return names unless foreign_self || under_foreign_self_block?(ancestors, base_scope)
159
+
160
+ names.reject { |name| ivar_name?(name) }
161
+ end
162
+
163
+ # Issue #1402 — whether a block on the path `ancestors` runs on an object provably not `self`: its call is a
164
+ # {InterveningIvarReset.foreign_self_block?}, the receiver typed in `base_scope`.
165
+ def under_foreign_self_block?(ancestors, base_scope)
166
+ (1...ancestors.size).any? do |index|
167
+ child = ancestors[index]
168
+ parent = ancestors[index - 1]
169
+ child.is_a?(Prism::BlockNode) && parent.is_a?(Prism::CallNode) && parent.block.equal?(child) &&
170
+ InterveningIvarReset.foreign_self_block?(parent, base_scope)
171
+ end
172
+ end
173
+
174
+ # Issue #1889 — every ivar `base_scope` binds: an `instance_variable_set` on `self` whose target no literal
175
+ # names ({ReflectiveIvarWrite.unknown_target?}) may write any of them. One on its ADR-58 declaration seed
176
+ # counts too, since the census behind the seed sees no reflective write; {.bind} keeps its mark.
177
+ def reflective_unknown_ivars(base_scope)
178
+ base_scope.ivars.keys
179
+ end
180
+
139
181
  def rebindable_outliving_write(node, base_scope, non_locals)
140
182
  name = outliving_write_name(node, non_locals)
141
183
  name if name && rebindable_non_local?(base_scope, name)
@@ -151,8 +193,12 @@ module Rigor
151
193
  yield.include?(name) ? nil : name
152
194
  end
153
195
 
154
- # The non-local `node` rebinds under `non_locals:` — or, without it, the instance variable only.
196
+ # The non-local `node` rebinds under `non_locals:` — or, without it, the instance variable only. A literal-name
197
+ # `instance_variable_set` on `self` ({ReflectiveIvarWrite.literal_name}) is a write of that ivar under either.
155
198
  def outliving_write_name(node, non_locals)
199
+ reflective = ReflectiveIvarWrite.literal_name(node)
200
+ return reflective if reflective
201
+
156
202
  name = non_local_write_name(node)
157
203
  non_locals || (name && ivar_name?(name) && NON_LOCAL_WRITE_NODES.include?(node.class)) ? name : nil
158
204
  end
@@ -201,13 +247,21 @@ module Rigor
201
247
  # it the miss answer the mark records (issue #1302).
202
248
  def bind(scope, name, type, optimistic: nil)
203
249
  case variable_kind(name)
204
- when :ivar then OptimisticOrigin.carry_ivar_mark(scope, scope.with_ivar(name, type), name, optimistic)
250
+ when :ivar then bind_ivar(scope, name, type, optimistic)
205
251
  when :cvar then scope.with_cvar(name, type)
206
252
  when :global then scope.with_global(name, type)
207
253
  else OptimisticOrigin.carry_local_mark(scope, scope.with_local(name, type), name, optimistic)
208
254
  end
209
255
  end
210
256
 
257
+ # An ivar on its ADR-58 declaration seed reaches {.bind} through {.reflective_unknown_ivars}, and through
258
+ # `ExpressionTyper#prefix_answered_scope`; rebinding it re-stamps the declaration mark, as
259
+ # `ReflectiveIvarWrite.apply` does, so a nil in the seed fuels no nil-receiver diagnostic.
260
+ def bind_ivar(scope, name, type, optimistic)
261
+ bound = OptimisticOrigin.carry_ivar_mark(scope, scope.with_ivar(name, type), name, optimistic)
262
+ scope.declaration_sourced?(:ivar, name) ? bound.seed_declaration_sourced_ivar(name, type) : bound
263
+ end
264
+
211
265
  # The optimistic nil-freeness mark `scope` holds for a name from {.writes}, or nil.
212
266
  def optimistic_mark(scope, name)
213
267
  case variable_kind(name)
@@ -290,7 +344,7 @@ module Rigor
290
344
  receiver = node.receiver
291
345
  return true if receiver && MutationWidening::SHAPE_MUTATORS.include?(node.name)
292
346
 
293
- callee_call?(node)
347
+ ReflectiveIvarWrite.self_write?(node) || callee_call?(node)
294
348
  end
295
349
 
296
350
  # Issue #1412 — {.content_mutations} for a `while` / `until` / `for` body: the locals bound in `base_scope`
@@ -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
@@ -39,6 +39,9 @@ module Rigor
39
39
  Hit = Data.define(:answer, :owner, :index, :side)
40
40
 
41
41
  QUESTIONS = %i[definer visibility arity override own_side].freeze
42
+ # The questions whose answer reads a recorded visibility: only they treat a contested visibility slot as a
43
+ # possible definer ({#possible?}).
44
+ VISIBILITY_QUESTIONS = %i[visibility override].freeze
42
45
  NONE = :rigor_definer_resolution_none
43
46
  STRAY = :rigor_definer_resolution_stray
44
47
  private_constant :NONE, :STRAY
@@ -58,12 +61,13 @@ module Rigor
58
61
  flavor = question == :arity ? :arity : :methods
59
62
  chain = Scope::ResolutionChain.for(scope, class_name.to_s, side, flavor)
60
63
 
61
- hits = candidates(scope, chain, method_name, from, answer_in)
64
+ reads_visibility = VISIBILITY_QUESTIONS.include?(question) ? question : nil
65
+ hits = candidates(scope, chain, method_name, from, answer_in, reads_visibility)
62
66
  return UNKNOWN if hits.nil?
63
67
 
64
68
  verdict = chain.settle(scope, outcomes(hits), owner: hits.first&.owner, unknown_for: method_name) do |retro|
65
69
  retro_from = retro_position(chain, retro, from)
66
- outcomes(retro_from && candidates(scope, retro, method_name, retro_from, answer_in))
70
+ outcomes(retro_from && candidates(scope, retro, method_name, retro_from, answer_in, reads_visibility))
67
71
  end
68
72
  return UNKNOWN unless verdict == :chain
69
73
  return UNKNOWN if side == :singleton && SingletonHookDecline.decline?(scope, chain, method_name, from, hits)
@@ -101,7 +105,7 @@ module Rigor
101
105
  # follows (the absent candidate). A cut chain, and a chain on which an external ancestor ahead of the
102
106
  # candidate (or, with no candidate, the implicit `Object`) may define the name, is a decline, answered by
103
107
  # nil instead of a set.
104
- def candidates(scope, chain, method_name, from, answer_in)
108
+ def candidates(scope, chain, method_name, from, answer_in, reads_visibility)
105
109
  hits = []
106
110
  position = from
107
111
  loop do
@@ -113,7 +117,9 @@ module Rigor
113
117
  return nil if hit.nil? && chain.truncated?
114
118
 
115
119
  hits << hit
116
- return decided(scope, chain, method_name, from, hits) if hit.nil? || !possible?(scope, hit, method_name)
120
+ if hit.nil? || !possible?(scope, hit, method_name, reads_visibility)
121
+ return decided(scope, chain, method_name, from, hits)
122
+ end
117
123
 
118
124
  position = hit.index + 1
119
125
  end
@@ -153,23 +159,39 @@ module Rigor
153
159
  end
154
160
 
155
161
  # The implicit tail after a chain's last project superclass: `Object` (with `Kernel` and `BasicObject`) on the
156
- # 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.
157
167
  def implicit_object_answers?(scope, chain, method_name)
158
- !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
159
188
  end
160
189
 
161
190
  # True only for an ancestor RBS knows whose declaration lacks the name, on the side the entry stands for: an
162
191
  # external superclass on a singleton chain (`:singleton`, its whole tail) is asked for a singleton method, an
163
192
  # extended module (`:instance`) and every instance-side entry for an instance method.
164
193
  def external_lacks?(scope, candidates, side, method_name)
165
- return Scope::ResolutionChain::Relevance.external_lacks?(scope, candidates, method_name) if side == :instance
166
-
167
- known = candidates.find { |candidate| Rigor::Reflection.rbs_class_known?(candidate, scope: scope) }
168
- return false if known.nil?
169
-
170
- Rigor::Reflection.singleton_method_definition(known, method_name, scope: scope).nil?
171
- rescue StandardError
172
- false
194
+ Scope::ResolutionChain::Relevance.external_lacks_on?(scope, candidates, side, method_name)
173
195
  end
174
196
 
175
197
  # What a candidate set says, for {Scope::ResolutionChain#settle} to compare across worlds.
@@ -188,15 +210,27 @@ module Rigor
188
210
  end
189
211
 
190
212
  # A definer that rests on a fact the walk could not prove executes: the member's `possible` sibling, or a
191
- # contested slot of one of the single-valued members.
192
- def possible?(scope, hit, method_name)
213
+ # contested slot of one of the single-valued members. A contested visibility slot counts only for a question
214
+ # that reads the visibility (`reads_visibility`): a slot is contested there for a visibility call the walk
215
+ # cannot place (an uncertain toggle, `private(*names)`, `private def self.x`), which leaves the definer and its
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
+ def possible?(scope, hit, method_name, reads_visibility)
193
225
  discovery = scope.discovery
194
226
  owner = hit.owner
195
227
  kind = hit.side == :singleton ? :singleton : :instance
196
228
  nodes = kind == :singleton ? :discovered_singleton_def_nodes : :discovered_def_nodes
197
229
  discovery.possible_method?(owner, method_name, kind) ||
198
- discovery.contested?(nodes, [owner, method_name.to_sym]) ||
199
- discovery.contested?(:discovered_method_visibilities, [owner, method_name.to_sym]) ||
230
+ (!(reads_visibility == :visibility && kind == :instance) &&
231
+ discovery.contested?(nodes, [owner, method_name.to_sym])) ||
232
+ (reads_visibility && discovery.contested?(:discovered_method_visibilities,
233
+ [owner, visibility_slot(method_name, kind)])) ||
200
234
  discovery.contested?(:discovered_parameter_envelopes, [owner, [kind, method_name.to_sym]])
201
235
  end
202
236
 
@@ -226,20 +260,61 @@ module Rigor
226
260
 
227
261
  # The first project entry that records the name (a discovered method, an instance `def` the def table
228
262
  # holds, or a visibility change such as `private :foo` recorded for it), with its visibility (`:public` where
229
- # none was stated).
263
+ # none was stated). Issue #1984 — a singleton-side entry reads its class's singleton slot
264
+ # (`private_class_method :new` records `new` there with no `def`), never the instance one.
230
265
  def visibility_answer(scope, method_name)
231
266
  lambda do |chain, from|
232
267
  first_hit(chain, scope, from) do |entry|
233
268
  kind = entry.side == :singleton ? :singleton : :instance
234
- next unless scope.discovered_method?(entry.name, method_name, kind) ||
235
- (kind == :instance && (scope.user_def_for(entry.name, method_name) ||
236
- scope.discovered_method_visibility(entry.name, method_name)))
269
+ recorded = side_visibility(scope, entry.name, method_name, kind)
270
+ next unless scope.discovered_method?(entry.name, method_name, kind) || recorded ||
271
+ (kind == :instance && scope.user_def_for(entry.name, method_name))
237
272
 
238
- scope.discovered_method_visibility(entry.name, method_name) || :public
273
+ recorded || :public
239
274
  end
240
275
  end
241
276
  end
242
277
 
278
+ def side_visibility(scope, owner, method_name, kind)
279
+ if kind == :singleton
280
+ scope.discovered_method_visibilities.dig(owner.to_s, visibility_slot(method_name, :singleton)) ||
281
+ self_extended_copy_visibility(scope, owner, method_name)
282
+ else
283
+ scope.discovered_method_visibility(owner, method_name)
284
+ end
285
+ end
286
+
287
+ # Issue #2059 — a module that `extend`s itself reaches its own instance method from the singleton side, and the
288
+ # extends fold copies that receiverless `def` onto the module's singleton entry, where no singleton slot records
289
+ # it: the instance slot's visibility is what Ruby applies (`module ES; extend self; private def x` makes `ES.x`
290
+ # private). A module function's singleton copy is public whatever its instance copy is, so a `module_function`
291
+ # row for the name in any file answers nil, as does a contested instance slot.
292
+ def self_extended_copy_visibility(scope, owner, method_name)
293
+ node = scope.singleton_def_for(owner, method_name)
294
+ return nil if node.nil? || !node.receiver.nil?
295
+ return nil unless scope.discovered_extends[owner.to_s]&.include?(owner.to_s)
296
+ return nil if scope.discovery.contested?(:discovered_method_visibilities, [owner.to_s, method_name.to_sym])
297
+ return nil if module_function_row?(scope, owner, method_name)
298
+
299
+ scope.discovered_method_visibility(owner, method_name)
300
+ end
301
+
302
+ # Whether any file's deferred ranges carry a `module_function` row (`:both` or `:singleton`) for the name on
303
+ # `owner` (`ModuleFunctionState`).
304
+ def module_function_row?(scope, owner, method_name)
305
+ name = method_name.to_sym
306
+ scope.discovered_deferred_ranges.each_value.any? do |rows|
307
+ rows.any? do |(_start, _finish, row_name, kind, row_owner)|
308
+ row_name == name && row_owner == owner.to_s && kind != :instance
309
+ end
310
+ end
311
+ end
312
+
313
+ # The key of a name's slot in the visibility table on `kind`'s side (`ScopeIndexer#visibility_slot`).
314
+ def visibility_slot(method_name, kind)
315
+ kind == :singleton ? [:singleton, method_name.to_sym] : method_name.to_sym
316
+ end
317
+
243
318
  def first_hit(chain, scope, from)
244
319
  index = from - 1
245
320
  found = nil
@@ -256,9 +331,10 @@ module Rigor
256
331
  end
257
332
 
258
333
  private_class_method :override_answer, :retro_position, :candidates, :past_fold_copies, :decided,
259
- :external_may_answer?, :implicit_object_answers?, :external_lacks?,
334
+ :external_may_answer?, :implicit_object_answers?, :project_tail_defines?, :external_lacks?,
260
335
  :outcomes, :collapse, :possible?,
261
- :default_answer, :definer_answer, :visibility_answer, :first_hit
336
+ :default_answer, :definer_answer, :visibility_answer, :side_visibility,
337
+ :self_extended_copy_visibility, :module_function_row?, :visibility_slot, :first_hit
262
338
  end
263
339
  end
264
340
  end