rigortype 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -3
  3. data/docs/handbook/08-understanding-errors.md +3 -2
  4. data/docs/install.md +87 -15
  5. data/docs/manual/02-cli-reference.md +2 -2
  6. data/docs/manual/03-configuration.md +5 -0
  7. data/docs/manual/08-skills.md +4 -3
  8. data/docs/manual/12-caching.md +43 -4
  9. data/docs/manual/14-rails-quickstart.md +19 -11
  10. data/lib/rigor/analysis/check_rules/source_arity.rb +163 -59
  11. data/lib/rigor/analysis/check_rules.rb +118 -87
  12. data/lib/rigor/analysis/dependency_recorder.rb +200 -18
  13. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +266 -0
  14. data/lib/rigor/analysis/incremental_run_slot.rb +171 -0
  15. data/lib/rigor/analysis/incremental_session.rb +273 -49
  16. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
  17. data/lib/rigor/analysis/reachability/scan_cache.rb +2 -1
  18. data/lib/rigor/analysis/run_cache_key.rb +27 -8
  19. data/lib/rigor/analysis/run_stats.rb +2 -2
  20. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +1 -1
  21. data/lib/rigor/analysis/runner/pool_coordinator.rb +2 -2
  22. data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -13
  23. data/lib/rigor/analysis/runner.rb +337 -46
  24. data/lib/rigor/analysis/shadow_harness.rb +27 -0
  25. data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
  26. data/lib/rigor/analysis/template_unit_paths.rb +6 -4
  27. data/lib/rigor/analysis/template_units.rb +15 -5
  28. data/lib/rigor/analysis/worker_session.rb +4 -3
  29. data/lib/rigor/cache/descriptor.rb +40 -1
  30. data/lib/rigor/cache/file_digest.rb +28 -0
  31. data/lib/rigor/cache/incremental_snapshot.rb +70 -8
  32. data/lib/rigor/cache/store.rb +63 -3
  33. data/lib/rigor/cache/value_digest.rb +325 -0
  34. data/lib/rigor/cli/check_command.rb +83 -13
  35. data/lib/rigor/cli/doctor_command.rb +1 -1
  36. data/lib/rigor/cli/effects_report.rb +2 -2
  37. data/lib/rigor/cli/unused_command.rb +3 -3
  38. data/lib/rigor/cli.rb +1 -1
  39. data/lib/rigor/config_audit.rb +3 -2
  40. data/lib/rigor/config_path.rb +29 -0
  41. data/lib/rigor/configuration/severity_profile.rb +5 -0
  42. data/lib/rigor/configuration.rb +6 -5
  43. data/lib/rigor/effects/plugin_facts.rb +6 -6
  44. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
  45. data/lib/rigor/environment/lockfile_resolver.rb +10 -1
  46. data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
  47. data/lib/rigor/environment/rbs_loader.rb +1 -1
  48. data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
  49. data/lib/rigor/environment.rb +13 -70
  50. data/lib/rigor/inference/block_parameter_binder.rb +29 -13
  51. data/lib/rigor/inference/captured_locals.rb +7 -1
  52. data/lib/rigor/inference/declaration_walk/context.rb +195 -0
  53. data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
  54. data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
  55. data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
  56. data/lib/rigor/inference/declaration_walk.rb +7 -0
  57. data/lib/rigor/inference/def_node_resolver.rb +3 -2
  58. data/lib/rigor/inference/definer_resolution.rb +266 -0
  59. data/lib/rigor/inference/expression_typer.rb +136 -45
  60. data/lib/rigor/inference/external_ancestor_resolution.rb +13 -16
  61. data/lib/rigor/inference/guard_rebinding.rb +53 -12
  62. data/lib/rigor/inference/macro_block_self_type.rb +14 -2
  63. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +46 -26
  64. data/lib/rigor/inference/module_function_state.rb +213 -0
  65. data/lib/rigor/inference/multi_target_binder.rb +21 -16
  66. data/lib/rigor/inference/narrowing.rb +147 -17
  67. data/lib/rigor/inference/parameter_inference_collector.rb +10 -9
  68. data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
  69. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
  70. data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
  71. data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
  72. data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
  73. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +130 -0
  74. data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
  75. data/lib/rigor/inference/scope_indexer.rb +1443 -295
  76. data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
  77. data/lib/rigor/inference/statement_evaluator.rb +80 -14
  78. data/lib/rigor/plugin/base.rb +28 -8
  79. data/lib/rigor/plugin/io_boundary.rb +82 -4
  80. data/lib/rigor/plugin/isolation.rb +1 -1
  81. data/lib/rigor/plugin/trust_policy.rb +3 -3
  82. data/lib/rigor/protection/discovery_seed.rb +18 -9
  83. data/lib/rigor/protection/mutation_cache.rb +3 -3
  84. data/lib/rigor/reflection/constant_ancestors.rb +96 -47
  85. data/lib/rigor/reflection/constant_path.rb +22 -7
  86. data/lib/rigor/reflection.rb +2 -3
  87. data/lib/rigor/scope/discovery_index.rb +218 -2
  88. data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
  89. data/lib/rigor/scope/resolution_chain.rb +884 -0
  90. data/lib/rigor/scope.rb +295 -322
  91. data/lib/rigor/sig_gen/alias_index.rb +1 -1
  92. data/lib/rigor/sig_gen/effect_annotation.rb +2 -2
  93. data/lib/rigor/sig_gen/generator.rb +22 -16
  94. data/lib/rigor/type/combinator.rb +25 -1
  95. data/lib/rigor/version.rb +1 -1
  96. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +10 -0
  97. data/sig/rigor/environment.rbs +2 -0
  98. data/sig/rigor/plugin/io_boundary.rbs +1 -0
  99. data/sig/rigor/scope.rbs +43 -1
  100. data/skills/rigor-project-init/SKILL.md +37 -11
  101. data/skills/rigor-project-init/references/02-configure.md +7 -2
  102. metadata +25 -1
@@ -33,12 +33,19 @@ module Rigor
33
33
  # The accepted gap is implicit conversion: a core method that calls back into a project method the program
34
34
  # does not spell (`puts obj` runs `obj.to_s`, `hash[obj]` runs `obj.hash`, `a.sort` runs `<=>`) is read as the
35
35
  # core method alone.
36
+ #
37
+ # Issue #1446 — a class guard's narrowing of an instance variable is restored at the same code, since code that
38
+ # may rebind a global may reach `self` and rebind the variable too, and at two more: `instance_variable_set` and
39
+ # `remove_instance_variable` on any receiver, and a write to the narrowed variable in code the scan reads (a
40
+ # block that may run again, a loop's next pass).
36
41
  module GuardRebinding
37
42
  # Calls that run code chosen by name or by a String.
38
43
  CODE_RUNNING_NAMES = Set[
39
44
  :send, :__send__, :public_send, :eval, :require, :require_relative, :load,
40
45
  # These rebind a constant on any receiver (`Object.const_set(:SEP, nil)`).
41
- :const_set, :remove_const
46
+ :const_set, :remove_const,
47
+ # These rebind an instance variable of a receiver that may be `self` (#1446).
48
+ :instance_variable_set, :remove_instance_variable
42
49
  ].freeze
43
50
  # These run their literal block, which the scan reads as any block, or a String of code, which it cannot read.
44
51
  BLOCK_OR_CODE_NAMES = Set[
@@ -109,10 +116,11 @@ module Rigor
109
116
  (call_node.block.is_a?(Prism::BlockArgumentNode) && may_rebind?(call_node.block, scope))
110
117
  end
111
118
 
112
- # True when running `node` may rebind one: it writes a global or constant, yields, calls `super`, or holds a
113
- # call, spelled or implicit ({.implicit_call_may_rebind?}), whose method may run foreign code. A `def` and a
114
- # lambda literal run nothing where they are written. Each node is visited once: a call's literal block is
115
- # reached as one of its children, so a nested block chain costs its size, not its depth's power.
119
+ # True when running `node` may rebind one: it writes a global, a constant or an instance variable a class guard
120
+ # narrowed, yields, calls `super`, or holds a call, spelled or implicit ({.implicit_call_may_rebind?}), whose
121
+ # method may run foreign code. A `def` and a lambda literal run nothing where they are written. Each node is
122
+ # visited once: a call's literal block is reached as one of its children, so a nested block chain costs its
123
+ # size, not its depth's power.
116
124
  #
117
125
  # A local the scanned code writes has no binding in `scope` yet, so the scan reads it as the code writes it
118
126
  # ({ScanScope.with_scanned_locals}): `copy = $sep; copy.length` is a `String` call. A literal block's parameters
@@ -126,6 +134,7 @@ module Rigor
126
134
  def scan(node, scope)
127
135
  return false unless node.is_a?(Prism::Node)
128
136
  return true if REBINDING_NODES.include?(node.class)
137
+ return true if guarded_ivar_write?(node, scope)
129
138
  return false if node.is_a?(Prism::DefNode) || node.is_a?(Prism::LambdaNode)
130
139
  return scan_call(node, scope) if node.is_a?(Prism::CallNode)
131
140
  return true if IMPLICIT_CALL_NODES.include?(node.class) && implicit_call_may_rebind?(node, scope)
@@ -135,6 +144,18 @@ module Rigor
135
144
  found
136
145
  end
137
146
 
147
+ # Issue #1446 — true when `node` writes an instance variable a class guard narrowed
148
+ # ({Scope#guard_narrowed_ivar?}). A write to any other instance variable rebinds nothing a guard recorded.
149
+ def guarded_ivar_write?(node, scope)
150
+ case node
151
+ when Prism::InstanceVariableWriteNode, Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode,
152
+ Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableTargetNode
153
+ scope.guard_narrowed_ivar?(node.name)
154
+ else
155
+ false
156
+ end
157
+ end
158
+
138
159
  def scan_call(node, scope)
139
160
  return true if call_runs_foreign_code?(node, scope)
140
161
  return true if scan(node.receiver, scope) || scan(node.arguments, scope)
@@ -156,7 +177,9 @@ module Rigor
156
177
  def implicit_call_may_rebind?(node, scope)
157
178
  kind = VARIABLE_OPERATOR_WRITES[node.class]
158
179
  return type_method_foreign?(variable_type(kind, node.name, scope), node.binary_operator, scope) if kind
159
- return type_method_foreign?(scope.type_of(node.collection), :each, scope) if node.is_a?(Prism::ForNode)
180
+ if node.is_a?(Prism::ForNode)
181
+ return type_method_foreign?(ScanScope.dispatch_type(node.collection, scope), :each, scope)
182
+ end
160
183
 
161
184
  compound_write_foreign?(node, scope)
162
185
  rescue StandardError
@@ -174,7 +197,7 @@ module Rigor
174
197
  end
175
198
 
176
199
  def compound_receiver_type(node, scope)
177
- return scope.type_of(node.receiver) if node.receiver
200
+ return ScanScope.dispatch_type(node.receiver, scope) if node.receiver
178
201
 
179
202
  scope.self_type || Type::Combinator.nominal_of("Object")
180
203
  end
@@ -264,7 +287,7 @@ module Rigor
264
287
  return ProjectMethodOwnership.targets(self_type)
265
288
  end
266
289
 
267
- ProjectMethodOwnership.targets(scope.type_of(receiver))
290
+ ProjectMethodOwnership.targets(ScanScope.dispatch_type(receiver, scope))
268
291
  end
269
292
 
270
293
  def foreign_target?(class_name, method_name, kind, scope)
@@ -300,8 +323,8 @@ module Rigor
300
323
  owner = definition.respond_to?(:defined_in) ? definition.defined_in : nil
301
324
  owner&.to_s&.delete_prefix("::")
302
325
  end
303
- private_class_method :scan, :scan_call, :receiver_targets, :foreign_target?, :universal_delegate_foreign?,
304
- :method_owner, :deferred_block_call?,
326
+ private_class_method :scan, :guarded_ivar_write?, :scan_call, :receiver_targets, :foreign_target?,
327
+ :universal_delegate_foreign?, :method_owner, :deferred_block_call?,
305
328
  :compound_write_foreign?,
306
329
  :compound_receiver_type, :compound_accessors, :compound_read_type, :variable_type,
307
330
  :type_method_foreign?
@@ -376,9 +399,10 @@ module Rigor
376
399
  bindings.reduce(scope) { |acc, (name, type)| acc.with_local(name, type) }
377
400
  end
378
401
 
379
- # What the method `call_node` calls yields its block, by position, or `[]` when that cannot be read.
402
+ # What the method `call_node` calls yields its block, by position, or `[]` when that cannot be read. A receiver
403
+ # a class guard left `bot` yields what the guarded class does ({.dispatch_type}, #1446).
380
404
  def yielded_types(call_node, scope)
381
- receiver = call_node.receiver ? scope.type_of(call_node.receiver) : scope.self_type
405
+ receiver = call_node.receiver ? dispatch_type(call_node.receiver, scope) : scope.self_type
382
406
  arguments = call_node.arguments&.arguments || []
383
407
  MethodDispatcher.expected_block_param_types(
384
408
  receiver_type: receiver, method_name: call_node.name, environment: scope.environment, scope: scope,
@@ -388,6 +412,23 @@ module Rigor
388
412
  []
389
413
  end
390
414
 
415
+ # Issue #1446 — the type a call on the receiver `node` dispatches on: its type in `scope`, or, where a class
416
+ # guard disjoint from the receiver's binding left it `bot` (`return unless @io.is_a?(StringIO)` on an `IO`),
417
+ # the classes the guard named ({Narrowing.bot_guard_classes_of}), since the value that passes the guard is one
418
+ # of them. So `@io.rewind` reads as `StringIO#rewind` and keeps the narrowing, while `@cb.call` past
419
+ # `@cb.is_a?(Proc)` reads as a call on a code object, and a method a project subclass or reopening defines on
420
+ # the class still counts. A `bot` no class guard produced stays `bot`, which no target resolves, so a call on
421
+ # it counts as an unresolved callee.
422
+ def dispatch_type(node, scope)
423
+ type = scope.type_of(node)
424
+ return type unless type.is_a?(Type::Bot)
425
+
426
+ classes = Narrowing.bot_guard_classes_of(node, scope)
427
+ return type if classes.nil?
428
+
429
+ Type::Combinator.union(*classes.map { |name| Type::Combinator.nominal_of(name) })
430
+ end
431
+
391
432
  # The names a block's parameter list declares: every kind of parameter, a destructured one's parts included,
392
433
  # and a `;`-block-local. A default value is not read.
393
434
  def collect_parameter_names(node, names)
@@ -168,11 +168,13 @@ module Rigor
168
168
  seen[current] = true
169
169
  # The class's own singleton defs sit ahead of EVERY `extend` in its singleton ancestry —
170
170
  # `class F; extend T::Sig; def self.sig(&b); end; end` calls F's method, not T::Sig's —
171
- # once the def has executed; a `def` written after this call has not run yet.
171
+ # once the def has executed; a `def` written after this call has not run yet. Shadowing here
172
+ # WITHHOLDS the narrowing for every class but the constraint itself, so a possible def
173
+ # (ADR-119) keeps the same answer and needs no certainty check.
172
174
  return current == constraint if scope.singleton_def_shadows_call?(current, method_name, call_node)
173
175
 
174
176
  owner = extended_module_call_owner(current, extends, method_name, call_node, scope, environment)
175
- return owner == constraint || rbs_inherits?(owner, constraint, environment) if owner
177
+ return extended_owner_reaches?(owner, constraint, method_name, call_node, scope, environment) if owner
176
178
 
177
179
  raw = supers[current]
178
180
  scope.ancestor_name_candidates(current, raw).each { |c| queue << c } if raw
@@ -217,6 +219,16 @@ module Rigor
217
219
  environment: environment).nil?
218
220
  end
219
221
 
222
+ # Whether the module that answers the call is `constraint`. ADR-119 WD1 errata: here "shadows" NARROWS the
223
+ # block's self to the owner, so an owner whose decisive source def is only possible (`if X; def sig ...; end`)
224
+ # answers false — the module may not answer at runtime.
225
+ def extended_owner_reaches?(owner, constraint, method_name, call_node, scope, environment)
226
+ return false if scope.instance_def_shadows_call?(owner, method_name, call_node) &&
227
+ scope.discovery.possible_method?(owner, method_name, :instance)
228
+
229
+ owner == constraint || rbs_inherits?(owner, constraint, environment)
230
+ end
231
+
220
232
  def rbs_inherits?(class_name, constraint, environment)
221
233
  %i[equal subclass].include?(environment.class_ordering(class_name, constraint))
222
234
  end
@@ -564,11 +564,11 @@ module Rigor
564
564
 
565
565
  memo = core_stdlib_memo(environment, scope)
566
566
  key = [class_name.to_s, method_name.to_sym]
567
- return memo[key] if memo&.key?(key)
567
+ return memo[key] if memo.key?(key)
568
568
 
569
- answer = compute_core_stdlib_ancestor_method(environment, class_name, method_name, scope)
570
- memo[key] = answer if memo
571
- answer
569
+ store_unless_recorded(memo, key) do
570
+ compute_core_stdlib_ancestor_method(environment, class_name, method_name, scope)
571
+ end
572
572
  end
573
573
 
574
574
  # Issue #1173 — the include-edge sibling of {#core_stdlib_ancestor_method}: a discovered
@@ -596,11 +596,11 @@ module Rigor
596
596
 
597
597
  memo = mixin_ancestor_memo(environment, scope)
598
598
  key = [class_name.to_s, method_name.to_sym]
599
- return memo[key] if memo&.key?(key)
599
+ return memo[key] if memo.key?(key)
600
600
 
601
- answer = compute_included_module_method(environment, class_name, method_name, scope)
602
- memo[key] = answer if memo
603
- answer
601
+ store_unless_recorded(memo, key) do
602
+ compute_included_module_method(environment, class_name, method_name, scope)
603
+ end
604
604
  end
605
605
 
606
606
  # The declines are conjunctive and ordered as {compute_core_stdlib_ancestor_method}'s are:
@@ -626,20 +626,12 @@ module Rigor
626
626
  end
627
627
 
628
628
  # The same one-slot memo shape as {#core_stdlib_memo}; see there for why it is one slot keyed
629
- # on the discovery index's identity, and why a recording run bypasses it.
629
+ # on the discovery index's identity, and what a recording run may store in it.
630
630
  MIXIN_ANCESTOR_MEMO_KEY = :__rigor_mixin_ancestor_dispatch__
631
631
  private_constant :MIXIN_ANCESTOR_MEMO_KEY
632
632
 
633
633
  def mixin_ancestor_memo(environment, scope)
634
- return nil if Rigor::Analysis::DependencyRecorder.active?
635
-
636
- discovery = scope.discovery
637
- slot = Thread.current[MIXIN_ANCESTOR_MEMO_KEY]
638
- unless slot && slot[0].equal?(discovery) && slot[1].equal?(environment)
639
- slot = [discovery, environment, {}]
640
- Thread.current[MIXIN_ANCESTOR_MEMO_KEY] = slot
641
- end
642
- slot[2]
634
+ ancestor_memo_slot(MIXIN_ANCESTOR_MEMO_KEY, environment, scope)
643
635
  end
644
636
 
645
637
  # The declines are conjunctive, so their ORDER is free — and it is chosen so the two that walk
@@ -800,8 +792,10 @@ module Rigor
800
792
  # Memo for the whole decision. Every call site of a class asks the same `(class, method)`
801
793
  # question, and the answer is a pure function of the frozen discovery index and the
802
794
  # environment, so it is cacheable on their identity. A run that is RECORDING ADR-46
803
- # dependency edges bypasses it: the shadow probes above read the project's method tables, and
804
- # a memo would swallow that edge for every file after the first.
795
+ # dependency edges stores only the answers whose computation recorded nothing
796
+ # ({#store_unless_recorded}): the shadow probes above read the project's method tables, and
797
+ # serving an answer that read them from the memo would swallow those edges for every later
798
+ # caller.
805
799
  #
806
800
  # ONE slot, replaced rather than accumulated — see {ExternalAncestorResolution}'s twin for
807
801
  # the measurement. A `Scope` hands each analysed file its own discovery index, so an
@@ -811,15 +805,38 @@ module Rigor
811
805
  private_constant :CORE_STDLIB_ANCESTOR_MEMO_KEY
812
806
 
813
807
  def core_stdlib_memo(environment, scope)
814
- return nil if Rigor::Analysis::DependencyRecorder.active?
808
+ ancestor_memo_slot(CORE_STDLIB_ANCESTOR_MEMO_KEY, environment, scope)
809
+ end
815
810
 
811
+ # The slot behind both memos. Whether this thread is recording is part of its key, so a slot
812
+ # filled with every answer is never read by a recording caller, which may only reuse the
813
+ # answers {#store_unless_recorded} lets through.
814
+ def ancestor_memo_slot(memo_key, environment, scope)
816
815
  discovery = scope.discovery
817
- slot = Thread.current[CORE_STDLIB_ANCESTOR_MEMO_KEY]
818
- unless slot && slot[0].equal?(discovery) && slot[1].equal?(environment)
819
- slot = [discovery, environment, {}]
820
- Thread.current[CORE_STDLIB_ANCESTOR_MEMO_KEY] = slot
816
+ recording = Rigor::Analysis::DependencyRecorder.recording?
817
+ slot = Thread.current[memo_key]
818
+ unless slot && slot[0].equal?(discovery) && slot[1].equal?(environment) && slot[2] == recording
819
+ slot = [discovery, environment, recording, {}]
820
+ Thread.current[memo_key] = slot
821
821
  end
822
- slot[2]
822
+ slot[3]
823
+ end
824
+
825
+ # Computes an answer, stores it under `key` when it may be served again, and returns it.
826
+ # Outside recording every answer is stored. Under ADR-46 recording the block runs in an
827
+ # ADR-84 WD2 capture window, which sees every edge the block files for the current consumer
828
+ # (the reads it withholds are not among them). An answer whose window stayed empty is stored:
829
+ # computing it again would read the same frozen tables and file nothing again, so serving it
830
+ # from the memo records exactly what recomputing does. That is most of them, since both
831
+ # computations withhold their ancestry walk and return before any recorded probe unless an
832
+ # RBS declaration answered. Any other answer is recomputed on every call, which files its
833
+ # edges each time, as a recording run always did.
834
+ def store_unless_recorded(memo, key, &)
835
+ return memo[key] = yield unless Rigor::Analysis::DependencyRecorder.recording?
836
+
837
+ answer, read_set = Rigor::Analysis::DependencyRecorder.capture(&)
838
+ memo[key] = answer if read_set.reads.empty? && read_set.missing.empty?
839
+ answer
823
840
  end
824
841
 
825
842
  # BFS over the scope's as-written ancestry tables — include edges first, then the
@@ -889,6 +906,7 @@ module Rigor
889
906
  # The class's own `def self.x` sits ahead of every `extend` — once it has run.
890
907
  # `singleton_def_shadows_call?` orders the def against the call site, so a `def self.sig`
891
908
  # written AFTER this `sig {}` does not suppress the bridge.
909
+ # ADR-119: shadowing already withholds the bridge, so a possible def withholds too.
892
910
  return nil if scope.singleton_def_shadows_call?(current, method_name, call_node)
893
911
 
894
912
  resolved = rbs_complete_extended_module_for(current, extends, environment, scope,
@@ -927,6 +945,8 @@ module Rigor
927
945
  nil
928
946
  end
929
947
 
948
+ # ADR-119: "defines" here stops the allow-list walk (a withholding answer), so a possible def stays
949
+ # a definer — treating it as absent would let a later extend bridge a call the def may own.
930
950
  def extend_owner_defines?(owner, method_name, call_node, scope, environment)
931
951
  return true if scope.instance_def_shadows_call?(owner, method_name, call_node)
932
952
 
@@ -0,0 +1,213 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+ require_relative "../source/node_children"
5
+
6
+ module Rigor
7
+ module Inference
8
+ # The one home for what a receiverless `module_function` call does to the methods around it
9
+ # ([#1507](https://github.com/rigortype/rigor/issues/1507),
10
+ # [#1197](https://github.com/rigortype/rigor/issues/1197)). Four readers used to compute it in four
11
+ # places. They disagree with each other and, in places, with Ruby. Each entry point below reproduces
12
+ # one reader's answer exactly: this module gives the readings one place to live, and does not yet make
13
+ # them agree. A change to one reading lands here, beside the other three. Examples are #1550's
14
+ # named-form snapshot and the reset a bare `public`, `private` or `protected` makes.
15
+ #
16
+ # - The singleton def-node table (`ScopeIndexer#walk_singleton_body`) reads
17
+ # {.each_singleton_sibling} and {.each_singleton_copy}. A bare call covers the later direct
18
+ # statements of its own body and is never reset.
19
+ # - The deferred ranges (`ScopeIndexer#walk_deferred_body`) read {.prescan_deferred} and
20
+ # {.deferred_module_function?}. A bare call covers every later def by offset, and a call inside
21
+ # control flow or a block counts.
22
+ # - The extends table (`ScopeIndexer#record_extend_call`) reads {.extends_self?}. A bare call covers
23
+ # every instance def of the module, in any order.
24
+ # - sig-gen (`SigGen::Generator#walk_statements`) reads {.each_sig_gen_statement} and
25
+ # {.sig_gen_module_function?}. A bare call covers the later statements of its own statement list and
26
+ # what nests in them.
27
+ #
28
+ # A fifth reader does not come here yet: `rigor-activerecord`'s `ModelDiscoverer#table_name_decorators`
29
+ # (`plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb`) reads a module body's
30
+ # direct statements for its table-name readers. It also accepts `self.module_function`, and there a bare
31
+ # call covers the instance methods written before it as well as after it, while a named call takes only a
32
+ # def written before it. A change to what `module_function` means must decide whether that reader follows.
33
+ #
34
+ # The deferred-ranges prescan asks `ScopeIndexer.receiver_eval_call?` and `ScopeIndexer.def_singleton?`, so
35
+ # `rigor/inference/scope_indexer` must be loaded before it runs. This file does not require it:
36
+ # `scope_indexer.rb` requires this file, and a require back would be circular. The other entry points need
37
+ # only Prism.
38
+ #
39
+ # `spec/rigor/inference/module_function_state_spec.rb` pins each reader's answers, and marks the ones
40
+ # Ruby contradicts with a "flip this when" note.
41
+ module ModuleFunctionState
42
+ module_function
43
+
44
+ # A receiverless `module_function` call, bare or named.
45
+ def call?(node)
46
+ node.name == :module_function && node.receiver.nil?
47
+ end
48
+
49
+ # The toggle form: a `module_function` call with no arguments.
50
+ def bare?(node)
51
+ node.arguments.nil? || node.arguments.arguments.empty?
52
+ end
53
+
54
+ # The singleton def-node table's reading, over one class-ish body's direct statements in source
55
+ # order. `ScopeIndexer#statements_of` builds that list, folding a body-level `rescue`, `else` or
56
+ # `ensure` clause into it. Yields `stmt, module_function_on, named_call` for every statement except a
57
+ # bare call:
58
+ #
59
+ # - `module_function_on` says whether a bare call precedes the statement. The toggle never switches
60
+ # off, although a later bare `public`, `private` or `protected` resets it in Ruby.
61
+ # - `named_call` is true for a named call (`module_function :a` or `module_function def x`), which
62
+ # {.each_singleton_copy} resolves; the caller records nothing else for it.
63
+ #
64
+ # A call nested in control flow or a block is not seen.
65
+ def each_singleton_sibling(statements)
66
+ module_function_on = false
67
+ statements.each do |stmt|
68
+ if stmt.is_a?(Prism::CallNode) && call?(stmt)
69
+ if bare?(stmt)
70
+ module_function_on = true
71
+ else
72
+ yield stmt, module_function_on, true
73
+ end
74
+ next
75
+ end
76
+ yield stmt, module_function_on, false
77
+ end
78
+ end
79
+
80
+ # The defs a named call copies onto the module's singleton, for the singleton def-node table. Yields
81
+ # `name, def_node` for each Symbol or String argument that names a receiverless def among the body's
82
+ # direct `statements`. The last def of that name wins wherever it sits. A def written after the call
83
+ # is therefore taken, although Ruby copies the one in effect at the call (#1550), and a name defined
84
+ # only after the call resolves, although Ruby raises `NameError` there. A `module_function def x`
85
+ # argument names nothing here.
86
+ def each_singleton_copy(call, statements)
87
+ defs_by_name = statements.each_with_object({}) do |stmt, acc|
88
+ acc[stmt.name] = stmt if stmt.is_a?(Prism::DefNode) && stmt.receiver.nil?
89
+ end
90
+ call.arguments&.arguments&.each do |arg|
91
+ name = symbol_argument_name(arg)
92
+ def_node = name && defs_by_name[name]
93
+ yield name, def_node if def_node
94
+ end
95
+ end
96
+
97
+ # The deferred-ranges reading, over one class-ish body's subtree. Appends to `offsets` the start of
98
+ # every bare call; each def that starts after one is a module function
99
+ # ({.deferred_module_function?}). Appends to `ranges` one row per named call, in the table's
100
+ # `[start, end, name, kind, owner]` shape:
101
+ #
102
+ # - a `:name` argument gives a `:singleton` row over the call itself, because the copy happens at
103
+ # the call and not at the def;
104
+ # - a `module_function def x` argument gives a `:both` row over the def, or a `:singleton` row for
105
+ # `def self.x`.
106
+ #
107
+ # The prescan looks through control flow and blocks, because a call that ran there still flips the
108
+ # later defs. It stops at a nested class, module, `class <<` or def body, where the call targets
109
+ # another module. It skips an `END` body, which runs after every def, and a `*_eval` / `*_exec`
110
+ # block, whose self is the receiver's module; that block gets its own prescan as a body. Nothing
111
+ # resets it.
112
+ #
113
+ # `qualified_prefix` names the owner, and `in_singleton_class` says whether the body is a
114
+ # `class <<` body; both feed `ScopeIndexer.def_singleton?`.
115
+ def prescan_deferred(node, qualified_prefix, in_singleton_class, offsets, ranges)
116
+ return unless node.is_a?(Prism::Node)
117
+ return if node.is_a?(Prism::ClassNode) || node.is_a?(Prism::ModuleNode) ||
118
+ node.is_a?(Prism::SingletonClassNode) || node.is_a?(Prism::DefNode) ||
119
+ node.is_a?(Prism::PostExecutionNode)
120
+
121
+ if node.is_a?(Prism::CallNode)
122
+ return prescan_call(node, qualified_prefix, in_singleton_class, offsets, ranges) if call?(node)
123
+ if ScopeIndexer.receiver_eval_call?(node)
124
+ return prescan_eval_call(node, qualified_prefix, in_singleton_class, offsets, ranges)
125
+ end
126
+ end
127
+
128
+ node.rigor_each_child do |child|
129
+ prescan_deferred(child, qualified_prefix, in_singleton_class, offsets, ranges)
130
+ end
131
+ end
132
+
133
+ # Whether the deferred-ranges reading makes a def starting at `start` a module function: a bare call
134
+ # starts before it, at any depth the prescan entered.
135
+ def deferred_module_function?(offsets, start)
136
+ offsets.any? { |offset| offset < start }
137
+ end
138
+
139
+ # The extends table's reading. A bare call makes the module extend itself, so every instance def the
140
+ # module has also answers on its singleton. That holds for defs before and after the call, and
141
+ # whatever a later `public` or `private` says. The over-approximation is deliberate (#526): the
142
+ # extra names only silence `call.undefined-method` on calls that raise. The walk reaches a call in
143
+ # control flow, and one in a `class_eval`-family block under that block's receiver. It does not reach
144
+ # one in an ordinary block, an `END` body or a `class << self` body.
145
+ def extends_self?(call)
146
+ call?(call) && bare?(call)
147
+ end
148
+
149
+ # sig-gen's reading, over one statement list. Yields every statement except a bare call, with
150
+ # whether a bare call came before it in this list or `active` was already true on entry. The
151
+ # generator passes the enclosing list's answer into an `if`, a block or a `begin`, but not into a
152
+ # nested class, module or `class << self`. A bare call in a nested list covers only that list's
153
+ # later statements. Nothing resets it, and a named call is not a directive at all.
154
+ def each_sig_gen_statement(statements, active)
155
+ statements.each do |stmt|
156
+ if sig_gen_directive?(stmt)
157
+ active = true
158
+ next
159
+ end
160
+ yield stmt, active
161
+ end
162
+ end
163
+
164
+ # A bare receiverless call, as sig-gen's statement-level directive.
165
+ def sig_gen_directive?(node)
166
+ node.is_a?(Prism::CallNode) && call?(node) && bare?(node)
167
+ end
168
+
169
+ # Whether sig-gen renders a def as `def self?.name`: an instance-side def under an active directive,
170
+ # in a class or a module alike. `kind` is `:singleton` for `def self.x` and a `class << self` body.
171
+ def sig_gen_module_function?(active, kind)
172
+ active && kind == :instance
173
+ end
174
+
175
+ # The prescan's call arm.
176
+ def prescan_call(node, qualified_prefix, in_singleton_class, offsets, ranges)
177
+ if bare?(node)
178
+ offsets << node.location.start_offset
179
+ else
180
+ owner = qualified_prefix.empty? ? nil : qualified_prefix.join("::")
181
+ node.arguments&.arguments&.each do |arg|
182
+ if arg.is_a?(Prism::DefNode)
183
+ kind = ScopeIndexer.def_singleton?(arg, qualified_prefix, in_singleton_class) ? :singleton : :both
184
+ ranges << [arg.location.start_offset, arg.location.end_offset, arg.name, kind, owner]
185
+ elsif (name = symbol_argument_name(arg))
186
+ ranges << [node.location.start_offset, node.location.end_offset, name, :singleton, owner]
187
+ else
188
+ prescan_deferred(arg, qualified_prefix, in_singleton_class, offsets, ranges)
189
+ end
190
+ end
191
+ end
192
+ return unless node.block
193
+
194
+ prescan_deferred(node.block, qualified_prefix, in_singleton_class, offsets, ranges)
195
+ end
196
+
197
+ # The prescan's eval arm: only the call's receiver and arguments keep this module's context.
198
+ def prescan_eval_call(node, qualified_prefix, in_singleton_class, offsets, ranges)
199
+ prescan_deferred(node.receiver, qualified_prefix, in_singleton_class, offsets, ranges) if node.receiver
200
+ node.arguments&.arguments&.each do |arg|
201
+ prescan_deferred(arg, qualified_prefix, in_singleton_class, offsets, ranges)
202
+ end
203
+ end
204
+
205
+ # The Symbol a `:name` or `"name"` literal argument names, or nil.
206
+ def symbol_argument_name(arg)
207
+ arg.unescaped.to_sym if arg.is_a?(Prism::SymbolNode) || arg.is_a?(Prism::StringNode)
208
+ end
209
+
210
+ private_class_method :prescan_call, :prescan_eval_call, :symbol_argument_name
211
+ end
212
+ end
213
+ end
@@ -108,31 +108,36 @@ module Rigor
108
108
  # `index_targets` maps each `Prism::IndexTargetNode` to the value it stores; {#apply_to}
109
109
  # binds nothing for it.
110
110
  Result = Data.define(:types, :optimistic, :ivars, :optimistic_ivars, :index_targets) do
111
- def initialize(types:, optimistic:, ivars: NO_BINDINGS, optimistic_ivars: NO_NAMES,
112
- index_targets: NO_BINDINGS)
111
+ def initialize(types:, optimistic:, ivars: NO_BINDINGS, optimistic_ivars: NO_NAMES, index_targets: NO_BINDINGS)
113
112
  super
114
113
  end
115
114
 
116
- # Binds every name into `scope` and records the optimistic mark after the binding, since
117
- # `Scope#with_local` / `Scope#with_ivar` drop any mark the name carried before. `miss` is what
118
- # each marked slot answers on a miss (issue #1302): `nil` for every mark the binder makes itself —
119
- # a short array pads with `nil`, and a `nil` destructures to `nil` — so only a caller whose
120
- # right-hand side a miss can make something else passes another answer
121
- # ({OptimisticOrigin.destructuring_miss}).
115
+ # Binds every name into `scope` with its mark ({MultiTargetBinder.apply_bindings}).
122
116
  def apply_to(scope, miss: nil)
123
- bound = types.reduce(scope) { |acc, (name, type)| acc.with_local(name, type) }
124
- bound = ivars.reduce(bound) { |acc, (name, type)| acc.with_ivar(name, type) }
125
- bound = optimistic.reduce(bound) do |acc, name|
126
- acc.with_optimistic_local(name, OptimisticOrigin::IMPLICITLY_RETURNS_NIL, miss: miss)
127
- end
128
- optimistic_ivars.reduce(bound) do |acc, name|
129
- acc.with_optimistic_ivar(name, OptimisticOrigin::IMPLICITLY_RETURNS_NIL, miss: miss)
130
- end
117
+ MultiTargetBinder.apply_bindings(scope, types, optimistic, ivars:, optimistic_ivars:, miss:)
131
118
  end
132
119
  end
133
120
 
134
121
  module_function
135
122
 
123
+ # The one place bindings and their optimistic marks are applied, shared by {Result#apply_to} and
124
+ # `BlockParameterBinder#bind_onto`. Every name is bound into the scope `bound` and the mark recorded
125
+ # after the binding, since `Scope#with_local` / `Scope#with_ivar` drop any mark the name carried
126
+ # before; the answer is the bound scope. `miss` is what each marked slot answers on a miss (issue
127
+ # #1302): `nil` for every mark the binder makes itself — a short array pads with `nil`, and a `nil`
128
+ # destructures to `nil` — so only a caller whose right-hand side a miss can make something else passes
129
+ # another answer ({OptimisticOrigin.destructuring_miss}). It loops with `each`, since `reduce`
130
+ # allocates even over the empty collections most of the four are
131
+ # ([#1504](https://github.com/rigortype/rigor/issues/1504)).
132
+ def apply_bindings(bound, types, optimistic, ivars: NO_BINDINGS, optimistic_ivars: NO_NAMES, miss: nil)
133
+ types.each { |name, type| bound = bound.with_local(name, type) }
134
+ ivars.each { |name, type| bound = bound.with_ivar(name, type).without_ivar_guard(name) }
135
+ origin = OptimisticOrigin::IMPLICITLY_RETURNS_NIL
136
+ optimistic.each { |name| bound = bound.with_optimistic_local(name, origin, miss: miss) }
137
+ optimistic_ivars.each { |name| bound = bound.with_optimistic_ivar(name, origin, miss: miss) }
138
+ bound
139
+ end
140
+
136
141
  # @param rhs_type — type of the right-hand side
137
142
  # @param scope — the scope the destructure is evaluated in, which answers the `to_ary`
138
143
  # question for a class outside the core list; without one only the core list wraps.