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
@@ -9,24 +9,36 @@ module Rigor
9
9
  # The one home for what a receiverless `module_function` call does to the methods around it
10
10
  # ([#1507](https://github.com/rigortype/rigor/issues/1507),
11
11
  # [#1197](https://github.com/rigortype/rigor/issues/1197)). Four readers used to compute it in four
12
- # places. They disagree with each other and, in places, with Ruby. Each entry point below reproduces
13
- # one reader's answer exactly: this module gives the readings one place to live, and does not yet make
14
- # them agree. A change to one reading lands here, beside the other three. Examples are #1550's
15
- # named-form snapshot and the reset a bare `public`, `private` or `protected` makes.
12
+ # places, and the method-visibility table reads it too. They disagree with each other and, in places, with
13
+ # Ruby. Each entry point below reproduces one reader's answer exactly: this module gives the readings one
14
+ # place to live, and does not yet make them agree. A change to one reading lands here, beside the others.
15
+ #
16
+ # In Ruby a bare `module_function` call turns the body's module_function mode on, and a later bare
17
+ # `public`, `private` or `protected` ({.visibility_reset?}) turns it off again (#1968). A named call
18
+ # (`public :x`, `private def y`) leaves the mode alone, and each `module` body starts with it off. A reset
19
+ # written in the body's own statements, or in a plain `begin` or `ensure` there, runs. One nested anywhere
20
+ # else (control flow, a block, a `rescue` clause, an argument) may not, so the mode after it is uncertain.
21
+ # A reset in a nested `class`, `module`, `class <<` or `def` body, a `*_eval` / `*_exec` block or a
22
+ # class-creating block (`Class.new do`, `Struct.new do`, ...) belongs to another scope and does not count.
16
23
  #
17
24
  # - The singleton def-node table (`ScopeIndexer#walk_singleton_body`) reads
18
25
  # {.each_singleton_sibling} and {.each_singleton_copy}. A bare call covers the later direct
19
- # statements of its own body and is never reset.
26
+ # statements of its own body, up to a reset; after an uncertain one the copy is possible.
20
27
  # - The deferred ranges (`ScopeIndexer#walk_deferred_body`) read {.prescan_deferred} and
21
- # {.deferred_module_function?}. A bare call covers every later def by offset, and a call inside
22
- # control flow or a block counts.
28
+ # {.deferred_module_function?}. A bare call covers every later def by offset, up to a later reset,
29
+ # and a call inside control flow or a block counts. An uncertain reset changes nothing here.
23
30
  # - The extends table (`ScopeIndexer#record_extend_call`) reads {.extends_self?}. A bare call covers
24
- # every instance def of the module, in any order.
31
+ # every instance def of the module, in any order, and is never reset.
25
32
  # - sig-gen (`SigGen::Generator#walk_statements`) reads {.each_sig_gen_statement} and
26
33
  # {.sig_gen_module_function?}. A bare call covers the later statements of its own statement list and
27
- # what nests in them.
34
+ # what nests in them, up to a reset; after an uncertain one {.sig_gen_uncertain?} says so.
35
+ # - The method-visibility table (`ScopeIndexer#walk_method_visibilities`) reads {.instance_visibility}. It
36
+ # applies a `module_function` call as it applies a `private` one, bare or named: the call makes the instance
37
+ # copy private (#1569), and a later bare `public`, `private` or `protected` sets the running default as it
38
+ # would anyway, so the reset needs no reading of its own. A call or reset that may not run leaves the slots
39
+ # after it contested, as any visibility call there does.
28
40
  #
29
- # A fifth reader does not come here yet: `rigor-activerecord`'s `ModelDiscoverer#table_name_decorators`
41
+ # Another reader does not come here yet: `rigor-activerecord`'s `ModelDiscoverer#table_name_decorators`
30
42
  # (`plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb`) reads a module body's
31
43
  # direct statements for its table-name readers. It also accepts `self.module_function`, and there a bare
32
44
  # call covers the instance methods written before it as well as after it, while a named call takes only a
@@ -47,23 +59,60 @@ module Rigor
47
59
  node.name == :module_function && node.receiver.nil?
48
60
  end
49
61
 
62
+ # The visibility a `module_function` call gives the instance method of each name it covers, bare or named:
63
+ # Ruby makes the instance copy private and adds a public singleton copy, which the instance-side
64
+ # method-visibility table does not hold (#1569).
65
+ def instance_visibility = :private
66
+
50
67
  # The toggle form: a `module_function` call with no arguments.
51
68
  def bare?(node)
52
69
  node.arguments.nil? || node.arguments.arguments.empty?
53
70
  end
54
71
 
72
+ # A bare `public`, `private` or `protected` call, receiverless or on `self`: Ruby ends the module_function
73
+ # mode there (#1968). A call with arguments sets the named methods' visibility and leaves the mode alone.
74
+ def visibility_reset?(node)
75
+ %i[public private protected].include?(node.name) && bare?(node) &&
76
+ (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
77
+ end
78
+
79
+ # The mode after `stmt`, a direct statement of a body, for the readers that walk the statements: `mode`
80
+ # is `true`, `false` or `:uncertain` before it. A {.visibility_reset?} the statement runs ends the mode,
81
+ # and one it may not run (`:maybe_off`, below {.prescan_deferred}) makes an on mode uncertain. A
82
+ # `module_function` nested in the statement is not read here, as before. `nested` says the statement
83
+ # itself may not run, so every reset in it may not either.
84
+ def mode_after(mode, stmt, nested: false)
85
+ return mode unless mode
86
+
87
+ events = []
88
+ prescan_deferred(stmt, EMPTY_PREFIX, false, events, [], nested: nested)
89
+ events.sort_by(&:first).each do |_offset, event|
90
+ case event
91
+ when :off then mode = false
92
+ when :maybe_off then mode = :uncertain if mode == true
93
+ end
94
+ end
95
+ mode
96
+ end
97
+
98
+ # The owner prefix {.mode_after} hands the prescan, which records no row a reset needs.
99
+ EMPTY_PREFIX = [].freeze
100
+
55
101
  # The singleton def-node table's reading, over one class-ish body's direct statements in source
56
102
  # order. `ScopeIndexer#statements_of` builds that list, folding a body-level `rescue`, `else` or
57
103
  # `ensure` clause into it. Yields `stmt, module_function_on, named_call` for every statement except a
58
104
  # bare call:
59
105
  #
60
- # - `module_function_on` says whether a bare call precedes the statement. The toggle never switches
61
- # off, although a later bare `public`, `private` or `protected` resets it in Ruby.
106
+ # - `module_function_on` is `true` when a bare call precedes the statement with no reset between them,
107
+ # `:uncertain` when a reset that may not run is between them ({.mode_after}), and `false` otherwise.
62
108
  # - `named_call` is true for a named call (`module_function :a` or `module_function def x`), which
63
109
  # {.each_singleton_copy} resolves; the caller records nothing else for it.
64
110
  #
65
- # A call nested in control flow or a block is not seen.
66
- def each_singleton_sibling(statements)
111
+ # A `module_function` call nested in control flow or a block is not seen. `conditional` lists the
112
+ # statements of a body-level `rescue` or `else` clause ({.conditional_clause_statements}), which may not
113
+ # run, so a reset there makes the mode uncertain rather than ending it.
114
+ def each_singleton_sibling(statements, conditional = [])
115
+ conditional = Set.new(conditional).compare_by_identity
67
116
  module_function_on = false
68
117
  statements.each do |stmt|
69
118
  if stmt.is_a?(Prism::CallNode) && call?(stmt)
@@ -75,30 +124,74 @@ module Rigor
75
124
  next
76
125
  end
77
126
  yield stmt, module_function_on, false
127
+ module_function_on = mode_after(module_function_on, stmt, nested: conditional.include?(stmt))
128
+ end
129
+ end
130
+
131
+ # The statements of a body-level `rescue` or `else` clause, which `ScopeIndexer#statements_of` folds into
132
+ # the body's direct statements although they run only on an exception, or only without one. An `ensure`
133
+ # clause runs either way and is not listed.
134
+ def conditional_clause_statements(body)
135
+ return [] unless body.is_a?(Prism::BeginNode)
136
+
137
+ statements = []
138
+ clause = body.rescue_clause
139
+ while clause
140
+ statements.concat(ScopeIndexer.statements_of(clause.statements))
141
+ clause = clause.subsequent
78
142
  end
143
+ statements.concat(ScopeIndexer.statements_of(body.else_clause.statements)) if body.else_clause
144
+ statements
79
145
  end
80
146
 
81
- # The defs a named call copies onto the module's singleton, for the singleton def-node table. Yields
82
- # `name, def_node` for each Symbol or String argument that names a receiverless def among the body's
83
- # direct `statements`. The last def of that name wins wherever it sits. A def written after the call
84
- # is therefore taken, although Ruby copies the one in effect at the call (#1550), and a name defined
85
- # only after the call resolves, although Ruby raises `NameError` there. A `module_function def x`
86
- # argument names nothing here.
147
+ # The defs a named call copies onto the module's singleton, for the singleton def-node table. Ruby
148
+ # copies the method in effect at the call (#1550), so a later def of the name redefines only the
149
+ # instance method. This reading approximates that from the body's direct `statements`: it yields
150
+ # `name, def_node` for each Symbol or String argument, where `def_node` is the last receiverless def
151
+ # of that name written as a direct statement before the call.
152
+ #
153
+ # A name declines (nothing is yielded) when no def precedes the call, where Ruby raises `NameError` or
154
+ # copies a def another file may load first, and when a statement after that def and before the call
155
+ # may rebind it in a way this reading cannot name: a def nested in control flow or a block, an `alias`,
156
+ # an `undef`, or a receiverless or `self.` `alias_method`, `define_method`, `attr_*`, `remove_method`
157
+ # or `undef_method` call. Such a call or `alias` with a non-literal name anywhere before the call, even
158
+ # before the def, declines every name. A `module_function def x` argument names nothing here.
159
+ #
160
+ # It does not see `send(:define_method, ...)`, a `define_method` on an explicit module receiver, a
161
+ # `prepend`, another file's singleton defs, or that a `rescue` clause's statements may run after an
162
+ # `ensure` call's (#1969).
87
163
  def each_singleton_copy(call, statements)
88
- defs_by_name = statements.each_with_object({}) do |stmt, acc|
89
- acc[stmt.name] = stmt if stmt.is_a?(Prism::DefNode) && stmt.receiver.nil?
164
+ call_start = call.location.start_offset
165
+ in_effect = {}
166
+ statements.each do |stmt|
167
+ next unless stmt.location.end_offset <= call_start
168
+
169
+ if stmt.is_a?(Prism::DefNode)
170
+ in_effect[stmt.name] = stmt if stmt.receiver.nil?
171
+ else
172
+ each_rebound_name(stmt) { |name| in_effect[name] = :unknown }
173
+ end
90
174
  end
91
175
  call.arguments&.arguments&.each do |arg|
92
176
  name = symbol_argument_name(arg)
93
- def_node = name && defs_by_name[name]
94
- yield name, def_node if def_node
177
+ def_node = name && !in_effect.key?(ANY_NAME) && in_effect[name]
178
+ yield name, def_node if def_node.is_a?(Prism::DefNode)
95
179
  end
96
180
  end
97
181
 
98
- # The deferred-ranges reading, over one class-ish body's subtree. Appends to `offsets` the start of
99
- # every bare call; each def that starts after one is a module function
100
- # ({.deferred_module_function?}). Appends to `ranges` one row per named call, in the table's
101
- # `[start, end, name, kind, owner]` shape:
182
+ # The key {.each_singleton_copy} records when a statement may rebind a name it cannot read.
183
+ ANY_NAME = Object.new.freeze
184
+
185
+ # Calls that may define, alias or remove an instance method of the receiverless module.
186
+ REBINDING_CALLS = %i[
187
+ alias_method define_method attr attr_reader attr_writer attr_accessor remove_method undef_method
188
+ ].freeze
189
+
190
+ # The deferred-ranges reading, over one class-ish body's subtree. Appends to `offsets` an
191
+ # `[offset, :on]` event for the start of every bare call, and for every {.visibility_reset?} an
192
+ # `[offset, :off]` event where it runs or a `[offset, :maybe_off]` one where it may not: `nested` is true
193
+ # once the walk has left the body's statements, a plain `begin` and an `ensure`. Appends to `ranges` one
194
+ # row per named call, in the table's `[start, end, name, kind, owner]` shape:
102
195
  #
103
196
  # - a `:name` argument gives a `:singleton` row over the call itself, because the copy happens at
104
197
  # the call and not at the def;
@@ -108,33 +201,36 @@ module Rigor
108
201
  # The prescan looks through control flow and blocks, because a call that ran there still flips the
109
202
  # later defs. It stops at a nested class, module, `class <<` or def body, where the call targets
110
203
  # another module. It skips an `END` body, which runs after every def, and a `*_eval` / `*_exec`
111
- # block, whose self is the receiver's module; that block gets its own prescan as a body. Nothing
112
- # resets it.
204
+ # block, whose self is the receiver's module, and a class-creating block (`Class.new do`, `Struct.new do`,
205
+ # `Data.define do`, `Module.new do`), whose self is the new class; such a block gets its own prescan as a
206
+ # body where the walk opens one.
113
207
  #
114
208
  # `qualified_prefix` names the owner, and `in_singleton_class` says whether the body is a
115
209
  # `class <<` body; both feed `ScopeIndexer.def_singleton?`.
116
- def prescan_deferred(node, qualified_prefix, in_singleton_class, offsets, ranges)
210
+ def prescan_deferred(node, qualified_prefix, in_singleton_class, offsets, ranges, nested: false)
117
211
  return unless node.is_a?(Prism::Node)
118
212
  return if node.is_a?(Prism::ClassNode) || node.is_a?(Prism::ModuleNode) ||
119
213
  node.is_a?(Prism::SingletonClassNode) || node.is_a?(Prism::DefNode) ||
120
214
  node.is_a?(Prism::PostExecutionNode)
215
+ return if node.is_a?(Prism::CallNode) &&
216
+ prescanned_call?(node, qualified_prefix, in_singleton_class, offsets, ranges, nested)
121
217
 
122
- if node.is_a?(Prism::CallNode)
123
- return prescan_call(node, qualified_prefix, in_singleton_class, offsets, ranges) if call?(node)
124
- if ScopeIndexer.receiver_eval_call?(node)
125
- return prescan_eval_call(node, qualified_prefix, in_singleton_class, offsets, ranges)
126
- end
127
- end
128
-
218
+ nested ||= !RUN_IN_PLACE.include?(node.class)
129
219
  node.rigor_each_child do |child|
130
- prescan_deferred(child, qualified_prefix, in_singleton_class, offsets, ranges)
220
+ prescan_deferred(child, qualified_prefix, in_singleton_class, offsets, ranges, nested: nested)
131
221
  end
132
222
  end
133
223
 
134
- # Whether the deferred-ranges reading makes a def starting at `start` a module function: a bare call
135
- # starts before it, at any depth the prescan entered.
224
+ # The nodes whose statements run where they are written, so a reset there runs ({.prescan_deferred}).
225
+ RUN_IN_PLACE = [Prism::StatementsNode, Prism::BeginNode, Prism::EnsureNode].freeze
226
+
227
+ # Whether the deferred-ranges reading makes a def starting at `start` a module function: the last
228
+ # `:on` or `:off` event {.prescan_deferred} recorded before it, at any depth the prescan entered, is
229
+ # `:on`. A `:maybe_off` leaves the answer as it was: the row stays `:both`, the reading that counts the
230
+ # def as run on the singleton side too, since the table has no possible row for it.
136
231
  def deferred_module_function?(offsets, start)
137
- offsets.any? { |offset| offset < start }
232
+ last = offsets.select { |offset, event| offset < start && event != :maybe_off }.max_by(&:first)
233
+ last ? last[1] == :on : false
138
234
  end
139
235
 
140
236
  # The extends table's reading. A bare call makes the module extend itself, so every instance def the
@@ -148,10 +244,12 @@ module Rigor
148
244
  end
149
245
 
150
246
  # sig-gen's reading, over one statement list. Yields every statement except a bare call, with
151
- # whether a bare call came before it in this list or `active` was already true on entry. The
247
+ # whether a bare call came before it in this list or `active` was already on at entry: `true`, `false`
248
+ # or `:uncertain` after a reset that may not run ({.mode_after}). The
152
249
  # generator passes the enclosing list's answer into an `if`, a block or a `begin`, but not into a
153
250
  # nested class, module or `class << self`. A bare call in a nested list covers only that list's
154
- # later statements. Nothing resets it, and a named call is not a directive at all.
251
+ # later statements. A reset ends it for the statements after it, and a named call is not a directive at
252
+ # all.
155
253
  def each_sig_gen_statement(statements, active)
156
254
  statements.each do |stmt|
157
255
  if sig_gen_directive?(stmt)
@@ -159,6 +257,7 @@ module Rigor
159
257
  next
160
258
  end
161
259
  yield stmt, active
260
+ active = mode_after(active, stmt)
162
261
  end
163
262
  end
164
263
 
@@ -170,13 +269,73 @@ module Rigor
170
269
  # Whether sig-gen renders a def as `def self?.name`: an instance-side def under an active directive,
171
270
  # in a class or a module alike. `kind` is `:singleton` for `def self.x` and a `class << self` body.
172
271
  def sig_gen_module_function?(active, kind)
173
- active && kind == :instance
272
+ active == true && kind == :instance
273
+ end
274
+
275
+ # Whether sig-gen cannot tell a def's kind: an instance-side def after a reset that may not run, which
276
+ # is a module function only if the reset did not run.
277
+ def sig_gen_uncertain?(active, kind)
278
+ active == :uncertain && kind == :instance
279
+ end
280
+
281
+ # Yields each instance-method name `node` may rebind on the module whose body holds it, or
282
+ # {ANY_NAME} when one is not a literal. It does not enter a nested class, module or `class <<` body,
283
+ # whose methods belong to another module.
284
+ def each_rebound_name(node, &)
285
+ case node
286
+ when Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode then nil
287
+ when Prism::DefNode then yield node.name if node.receiver.nil?
288
+ else
289
+ each_own_rebound_name(node, &)
290
+ node.compact_child_nodes.each { |child| each_rebound_name(child, &) }
291
+ end
292
+ end
293
+
294
+ # The names `node` itself may rebind, apart from its children: an `alias`, an `undef` or a
295
+ # {REBINDING_CALLS} call on the module.
296
+ def each_own_rebound_name(node, &)
297
+ case node
298
+ when Prism::AliasMethodNode then yield symbol_argument_name(node.new_name) || ANY_NAME
299
+ when Prism::UndefNode then node.names.each { |name| yield symbol_argument_name(name) || ANY_NAME }
300
+ when Prism::CallNode
301
+ each_rebinding_call_name(node, &) if REBINDING_CALLS.include?(node.name) &&
302
+ (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
303
+ end
304
+ end
305
+
306
+ # The names a {REBINDING_CALLS} call may rebind: the first argument of `alias_method` and
307
+ # `define_method`, every argument of the others (and its writer, for the `attr_*` family), or
308
+ # {ANY_NAME} for a non-literal one.
309
+ def each_rebinding_call_name(call)
310
+ arguments = call.arguments&.arguments || []
311
+ arguments = arguments.first(1) if %i[alias_method define_method].include?(call.name)
312
+ arguments.each do |arg|
313
+ name = symbol_argument_name(arg)
314
+ next yield(ANY_NAME) unless name
315
+
316
+ yield name
317
+ yield :"#{name}=" if call.name.start_with?("attr")
318
+ end
319
+ end
320
+
321
+ # The prescan's arms for a call; true when the arm took the call and its subtree.
322
+ def prescanned_call?(node, qualified_prefix, in_singleton_class, offsets, ranges, nested)
323
+ if call?(node)
324
+ prescan_call(node, qualified_prefix, in_singleton_class, offsets, ranges)
325
+ elsif visibility_reset?(node)
326
+ offsets << [node.location.start_offset, nested ? :maybe_off : :off]
327
+ elsif ScopeIndexer.receiver_eval_call?(node) || (node.block && ScopeIndexer.meta_new_constant_rvalue?(node))
328
+ prescan_eval_call(node, qualified_prefix, in_singleton_class, offsets, ranges)
329
+ else
330
+ return false
331
+ end
332
+ true
174
333
  end
175
334
 
176
335
  # The prescan's call arm.
177
336
  def prescan_call(node, qualified_prefix, in_singleton_class, offsets, ranges)
178
337
  if bare?(node)
179
- offsets << node.location.start_offset
338
+ offsets << [node.location.start_offset, :on]
180
339
  else
181
340
  owner = qualified_prefix.empty? ? nil : qualified_prefix.join("::")
182
341
  node.arguments&.arguments&.each do |arg|
@@ -186,20 +345,22 @@ module Rigor
186
345
  elsif (name = symbol_argument_name(arg))
187
346
  ranges << [node.location.start_offset, node.location.end_offset, name, :singleton, owner]
188
347
  else
189
- prescan_deferred(arg, qualified_prefix, in_singleton_class, offsets, ranges)
348
+ prescan_deferred(arg, qualified_prefix, in_singleton_class, offsets, ranges, nested: true)
190
349
  end
191
350
  end
192
351
  end
193
352
  return unless node.block
194
353
 
195
- prescan_deferred(node.block, qualified_prefix, in_singleton_class, offsets, ranges)
354
+ prescan_deferred(node.block, qualified_prefix, in_singleton_class, offsets, ranges, nested: true)
196
355
  end
197
356
 
198
357
  # The prescan's eval arm: only the call's receiver and arguments keep this module's context.
199
358
  def prescan_eval_call(node, qualified_prefix, in_singleton_class, offsets, ranges)
200
- prescan_deferred(node.receiver, qualified_prefix, in_singleton_class, offsets, ranges) if node.receiver
359
+ if node.receiver
360
+ prescan_deferred(node.receiver, qualified_prefix, in_singleton_class, offsets, ranges, nested: true)
361
+ end
201
362
  node.arguments&.arguments&.each do |arg|
202
- prescan_deferred(arg, qualified_prefix, in_singleton_class, offsets, ranges)
363
+ prescan_deferred(arg, qualified_prefix, in_singleton_class, offsets, ranges, nested: true)
203
364
  end
204
365
  end
205
366
 
@@ -208,7 +369,8 @@ module Rigor
208
369
  Source::Literals.symbol_or_string(arg)
209
370
  end
210
371
 
211
- private_class_method :prescan_call, :prescan_eval_call, :symbol_argument_name
372
+ private_class_method :prescan_call, :prescanned_call?, :prescan_eval_call, :symbol_argument_name,
373
+ :each_rebound_name, :each_own_rebound_name, :each_rebinding_call_name
212
374
  end
213
375
  end
214
376
  end