rigortype 0.4.2 → 0.4.4

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 (128) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/data/core_overlay/ostruct.rbs +40 -0
  4. data/docs/handbook/11-sig-gen.md +28 -5
  5. data/docs/manual/02-cli-reference.md +23 -4
  6. data/docs/manual/03-configuration.md +21 -8
  7. data/docs/manual/04-diagnostics.md +44 -2
  8. data/docs/manual/05-inspecting-types.md +50 -0
  9. data/docs/manual/07-plugins.md +1 -1
  10. data/docs/manual/10-mcp-server.md +32 -3
  11. data/docs/manual/12-caching.md +18 -1
  12. data/docs/manual/14-rails-quickstart.md +2 -2
  13. data/docs/manual/18-removing-dead-code.md +26 -9
  14. data/docs/manual/plugins/rigor-rails-routes.md +2 -2
  15. data/docs/manual/plugins/rigor-shoulda-matchers.md +4 -1
  16. data/exe/rigor +12 -17
  17. data/lib/rigor/analysis/check_rules/rule_ids.rb +64 -0
  18. data/lib/rigor/analysis/check_rules.rb +68 -57
  19. data/lib/rigor/analysis/effects_cache_probe.rb +1 -1
  20. data/lib/rigor/analysis/incremental.rb +12 -0
  21. data/lib/rigor/analysis/incremental_run_slot.rb +3 -3
  22. data/lib/rigor/analysis/incremental_session.rb +207 -40
  23. data/lib/rigor/analysis/path_expansion.rb +62 -6
  24. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +124 -37
  25. data/lib/rigor/analysis/reachability/graph.rb +139 -15
  26. data/lib/rigor/analysis/reachability/scan.rb +374 -173
  27. data/lib/rigor/analysis/reachability/scan_cache.rb +4 -2
  28. data/lib/rigor/analysis/rule_catalog.rb +6 -1
  29. data/lib/rigor/analysis/run_cache_key.rb +31 -7
  30. data/lib/rigor/analysis/run_cache_probe.rb +6 -4
  31. data/lib/rigor/analysis/run_stats.rb +28 -6
  32. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +74 -13
  33. data/lib/rigor/analysis/runner/pool_coordinator.rb +133 -19
  34. data/lib/rigor/analysis/runner/project_pre_passes.rb +46 -1
  35. data/lib/rigor/analysis/runner.rb +62 -18
  36. data/lib/rigor/analysis/severity_stamp.rb +34 -1
  37. data/lib/rigor/analysis/worker_session.rb +58 -2
  38. data/lib/rigor/cache/descriptor.rb +40 -6
  39. data/lib/rigor/cache/engine_source.rb +21 -7
  40. data/lib/rigor/cache/incremental_snapshot.rb +56 -12
  41. data/lib/rigor/cli/check_command.rb +138 -39
  42. data/lib/rigor/cli/docs_command.rb +5 -4
  43. data/lib/rigor/cli/lens_command.rb +144 -0
  44. data/lib/rigor/cli/lsp_command.rb +5 -2
  45. data/lib/rigor/cli/mcp_command.rb +1 -1
  46. data/lib/rigor/cli/skill_command.rb +3 -3
  47. data/lib/rigor/cli/skill_describe.rb +1 -1
  48. data/lib/rigor/cli/type_of_command.rb +2 -1
  49. data/lib/rigor/cli/unused_command.rb +157 -24
  50. data/lib/rigor/cli.rb +30 -4
  51. data/lib/rigor/config_audit.rb +111 -15
  52. data/lib/rigor/configuration/dependencies.rb +19 -11
  53. data/lib/rigor/configuration.rb +15 -0
  54. data/lib/rigor/environment/bundle_sig_discovery.rb +11 -7
  55. data/lib/rigor/environment/installed_gem_set.rb +38 -11
  56. data/lib/rigor/environment/lockfile_resolver.rb +37 -6
  57. data/lib/rigor/environment/missing_gem_constant_index.rb +22 -7
  58. data/lib/rigor/environment/rbs_loader.rb +79 -10
  59. data/lib/rigor/environment/required_features.rb +4 -2
  60. data/lib/rigor/environment.rb +12 -5
  61. data/lib/rigor/inference/block_call_timing.rb +5 -0
  62. data/lib/rigor/inference/builtins/cruby_definers.rb +22 -5
  63. data/lib/rigor/inference/captured_locals.rb +69 -6
  64. data/lib/rigor/inference/definer_resolution.rb +16 -7
  65. data/lib/rigor/inference/expression_typer.rb +117 -29
  66. data/lib/rigor/inference/guard_rebinding.rb +7 -2
  67. data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +21 -17
  68. data/lib/rigor/inference/last_line/self_evidence.rb +1 -1
  69. data/lib/rigor/inference/match_rebinding.rb +1 -1
  70. data/lib/rigor/inference/method_dispatcher/argument_nodes.rb +25 -0
  71. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +3 -2
  72. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +2 -2
  73. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +3 -3
  74. data/lib/rigor/inference/method_dispatcher/optional_count.rb +63 -0
  75. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +4 -16
  76. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -7
  77. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +8 -26
  78. data/lib/rigor/inference/method_dispatcher/unit_denominator_rational.rb +67 -0
  79. data/lib/rigor/inference/method_dispatcher.rb +52 -3
  80. data/lib/rigor/inference/module_function_state.rb +80 -13
  81. data/lib/rigor/inference/narrowing.rb +184 -68
  82. data/lib/rigor/inference/project_patched_definedness_guard.rb +86 -0
  83. data/lib/rigor/inference/project_patched_methods.rb +2 -2
  84. data/lib/rigor/inference/project_patched_scanner.rb +38 -24
  85. data/lib/rigor/inference/refined_dispatch/class_object.rb +350 -0
  86. data/lib/rigor/inference/refined_dispatch/own_method.rb +112 -40
  87. data/lib/rigor/inference/refined_dispatch.rb +62 -14
  88. data/lib/rigor/inference/reflective_ivar_write.rb +116 -0
  89. data/lib/rigor/inference/repeated_or_writes.rb +416 -1
  90. data/lib/rigor/inference/scope_indexer.rb +565 -61
  91. data/lib/rigor/inference/self_call_ivar_writes.rb +341 -0
  92. data/lib/rigor/inference/statement_evaluator.rb +307 -60
  93. data/lib/rigor/inference/untyped_rejoin.rb +75 -0
  94. data/lib/rigor/language_server/project_context.rb +56 -5
  95. data/lib/rigor/lens/anchor.rb +115 -0
  96. data/lib/rigor/lens/contract_reader.rb +133 -0
  97. data/lib/rigor/lens/document.rb +113 -0
  98. data/lib/rigor/lens/file_reader.rb +802 -0
  99. data/lib/rigor/lens/members.rb +203 -0
  100. data/lib/rigor/lens/parameters.rb +91 -0
  101. data/lib/rigor/lens/plugins.rb +40 -0
  102. data/lib/rigor/lens/run.rb +322 -0
  103. data/lib/rigor/lens/slot.rb +79 -0
  104. data/lib/rigor/lens.rb +41 -0
  105. data/lib/rigor/mcp/server.rb +61 -20
  106. data/lib/rigor/plugin/box_probe.rb +154 -7
  107. data/lib/rigor/project_environment.rb +23 -1
  108. data/lib/rigor/protection/mutation_cache.rb +2 -1
  109. data/lib/rigor/scope.rb +33 -17
  110. data/lib/rigor/sig_gen/generator.rb +45 -5
  111. data/lib/rigor/sig_gen/layout_index.rb +36 -0
  112. data/lib/rigor/sig_gen/method_candidate.rb +8 -0
  113. data/lib/rigor/sig_gen/path_mapper.rb +18 -2
  114. data/lib/rigor/sig_gen/writer.rb +47 -34
  115. data/lib/rigor/version.rb +1 -1
  116. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +29 -3
  117. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +5 -1
  118. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +5 -1
  119. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
  120. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +28 -6
  121. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +1 -1
  122. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +5 -1
  123. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +155 -12
  124. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +10 -8
  125. data/sig/rigor/scope.rbs +3 -0
  126. data/sig/rigor.rbs +6 -3
  127. data/skills/rigor-project-init/references/02-configure.md +2 -2
  128. metadata +21 -1
@@ -0,0 +1,350 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module Inference
5
+ module RefinedDispatch
6
+ # Issue #1752 — the lookup of a class object, which Ruby walks like any object's: a refinement of `Object`,
7
+ # `Module`, `Class`, `Kernel` or `BasicObject` answers `Integer.oo`, because a class object is an `Object`.
8
+ #
9
+ # The walk is `C`'s singleton class with the modules it extends, then each superclass's singleton class with its
10
+ # extends, then the instance levels of `Class` (a class only), `Module`, `Object`, `Kernel` and `BasicObject`.
11
+ # A singleton class is a level whose own entry is a {SingletonClass}: a refinement table key names an instance
12
+ # side, so it never matches one (`refine C.singleton_class` records a class-unknown row), and it defines the
13
+ # name through its own singleton methods. An extended module is an instance-side entry, so `refine(Ext)`
14
+ # answers `C.z` where `C` extends `Ext` (Ruby 4.0.5).
15
+ #
16
+ # Every place the walk cannot read makes the whole walk unreadable (nil), where each reader declines: a chain
17
+ # cut at its limit or holding a mixin the tables cannot name, a superclass the tables do not know (`class K <
18
+ # <expr>`, `K = Class.new(Parent)`, an external superclass RBS does not declare), a class some constant write
19
+ # may have made (`Point = Struct.new(:x)`, whose superclass the tables do not record). A singleton level whose
20
+ # class records an `extend` the chain did not place there is read as `mixed` ({.skipped_extend?}).
21
+ module ClassObject
22
+ EMPTY = [].freeze
23
+ # The classes on every class object's lookup: the instance sides that hold `Class#new`, `Module#name` and
24
+ # `Object#tap`. A class no table places among them may still be one a class object reaches only when it is a
25
+ # module or a class the walk cannot place (`ClassObject.reachable?`).
26
+ TAIL_CLASSES = %w[Class Module Object Kernel BasicObject].freeze
27
+ private_constant :EMPTY
28
+
29
+ module_function
30
+
31
+ # `[[level class, entries, mixed, every entry], …]` in {RefinedDispatch.levels}' shape for the class object
32
+ # `class_name`, or nil when the walk cannot be read. A singleton level's class is a {SingletonClass}.
33
+ def levels(scope, class_name)
34
+ environment = scope.environment
35
+ return nil if environment.nil?
36
+ return rbs_levels(scope, class_name) if environment.class_known?(class_name)
37
+
38
+ project_levels(scope, class_name)
39
+ end
40
+
41
+ # RBS's singleton ancestry: each singleton class a level of its own, `mixed` where the project extends it at
42
+ # a position RBS does not record, and each instance side (a declared `extend`, `Class`, `Module`, …) a level
43
+ # `mixed` where the project mixes into it. `from` drops the leading singleton classes a project level already
44
+ # holds.
45
+ def rbs_levels(scope, class_name, from: 0)
46
+ loader = scope.environment&.rbs_loader
47
+ pairs = loader ? loader.singleton_ancestors_for(class_name) : EMPTY
48
+ return nil if pairs.empty?
49
+
50
+ readable(pairs.drop(from).map do |kind, name|
51
+ kind == :instance ? instance_level(scope, name) : singleton_level(scope, name)
52
+ end)
53
+ end
54
+
55
+ # RBS's instance ancestry of `class_name` (`Module`'s, for a module object's tail), each a level.
56
+ def rbs_instance_levels(scope, class_name)
57
+ loader = scope.environment&.rbs_loader
58
+ names = loader ? loader.ancestor_names_for(class_name) : EMPTY
59
+ return nil if names.empty?
60
+
61
+ readable(names.map { |name| instance_level(scope, name) })
62
+ end
63
+
64
+ def readable(levels) = levels.include?(nil) ? nil : levels
65
+
66
+ # A `mixed` level carries the modules the project mixes in beside its own entry, so a refinement of one of them
67
+ # makes the walk past the level {UNKNOWN} (`RefinedDispatch.targeted_after?`) and `call.undefined-method`'s
68
+ # reach holds it. Nil when that mixin chain cannot be read.
69
+ def instance_level(scope, name)
70
+ return [name, [name], false] unless RefinedDispatch.project_mixin?(scope, name)
71
+
72
+ mixins = mixin_entries(scope, name, :instance)
73
+ mixins && [name, [name, *mixins], true]
74
+ end
75
+
76
+ def singleton_level(scope, name)
77
+ return nil if instance_unreadable?(scope, name)
78
+
79
+ own = SingletonClass.new(name)
80
+ return [own, [own], false] unless singleton_mixin?(scope, name)
81
+
82
+ mixins = mixin_entries(scope, name, :singleton)
83
+ mixins && [own, [own, *mixins], true]
84
+ end
85
+
86
+ # The names of the modules on `name`'s own level of its project chain on `side`, an unnamed entry's candidate
87
+ # names included, or nil when the chain is cut or holds a mixin the tables cannot name.
88
+ def mixin_entries(scope, name, side)
89
+ chain = Scope::ResolutionChain.for(scope, name, side, :methods)
90
+ chain.record(scope) if Analysis::DependencyRecorder.active?
91
+ return nil if unreadable?(chain)
92
+ return EMPTY if chain.level_count.zero?
93
+
94
+ names = level_names(chain, 0)
95
+ return nil if side == :singleton && skipped_extend?(scope, name, names)
96
+
97
+ names.reject { |entry| entry == name }
98
+ end
99
+
100
+ # The names on level `index` of `chain` (an unnamed entry's candidate names included), the level's own
101
+ # singleton entry left out.
102
+ def level_names(chain, index)
103
+ chain.level_entries(index).flat_map do |entry|
104
+ next entry.candidates if entry.external?
105
+
106
+ entry.side == :instance ? [entry.name] : EMPTY
107
+ end.uniq
108
+ end
109
+
110
+ # Does `name` record an `extend` that the chain did not place on `name`'s own singleton level (`entries`, its
111
+ # names or candidate lists)? The chain skips an `extend` of a module already on the lookup, as Ruby does, but
112
+ # the tables record `class << self; prepend M` (and `singleton_class.prepend(M)`) as an `extend` too, and
113
+ # Ruby's `prepend` puts the module ahead of the class's own singleton methods again (`class Base3 < Top;
114
+ # class << self; prepend Ext` with `Top` extending `Ext`). Such a level cannot place that module, so it reads as
115
+ # `mixed`.
116
+ def skipped_extend?(scope, name, entries)
117
+ raws = scope.discovered_extends[name]
118
+ return false if raws.nil? || raws.empty?
119
+
120
+ placed = entries.flat_map { |entry| entry.is_a?(Array) ? entry : [entry] }
121
+ raws.any? { |raw| scope.ancestor_name_candidates(name, raw).none? { |candidate| placed.include?(candidate) } }
122
+ end
123
+
124
+ # The project's singleton-side `Scope::ResolutionChain` (`C`'s singleton, its `extend`s' instance chains,
125
+ # then each project superclass's), spliced into RBS's singleton ancestry at an external superclass, or
126
+ # followed by the implicit tail after the last project class.
127
+ def project_levels(scope, class_name)
128
+ chain = Scope::ResolutionChain.for(scope, class_name, :singleton, :methods)
129
+ chain.record(scope) if Analysis::DependencyRecorder.active?
130
+ return nil if unreadable?(chain) || chain.level_count.zero?
131
+
132
+ out = []
133
+ chain.level_count.times do |index|
134
+ level_class = chain.level_classes[index]
135
+ return splice_external(scope, out, chain.level_entries(index)) if level_class.nil?
136
+ return nil if instance_unreadable?(scope, level_class)
137
+
138
+ own = SingletonClass.new(level_class)
139
+ every = chain.level_entries(index).map { |entry| level_entry(scope, entry, own) }
140
+ out << [own, every.grep_v(Array), skipped_extend?(scope, level_class, every), every]
141
+ end
142
+ tail = implicit_tail(scope, chain.level_classes[chain.level_count - 1])
143
+ tail && out.concat(tail)
144
+ end
145
+
146
+ # A chain cut at its limit, or holding a mixin the tables cannot name.
147
+ def unreadable?(chain) = chain.truncated? || chain.wildcard_mixin?
148
+
149
+ # Is `name`'s instance chain unreadable? A mixin the tables cannot name there includes the one an
150
+ # `included` hook makes (`base.extend(ClassMethods)`, `base.singleton_class.include(CM)`), which installs
151
+ # singleton methods on `name` that its singleton chain never records.
152
+ def instance_unreadable?(scope, name)
153
+ chain = Scope::ResolutionChain.for(scope, name, :instance, :methods)
154
+ chain.record(scope) if Analysis::DependencyRecorder.active?
155
+ unreadable?(chain)
156
+ end
157
+
158
+ def level_entry(scope, entry, own)
159
+ return own if !entry.external? && entry.side == :singleton && entry.name == own.of
160
+ return SingletonClass.new(entry.name) if !entry.external? && entry.side == :singleton
161
+
162
+ RefinedDispatch.entry_name(scope, entry) || RefinedDispatch.unnamed(entry)
163
+ end
164
+
165
+ # An external superclass stands for its whole singleton ancestry, which RBS gives when it declares the class.
166
+ #
167
+ # ADR-46 — a project superclass with no methods is not admitted by the `:methods` resolver, so it is external
168
+ # here and the walk is unreadable; each candidate's `class:` edge lets a new file that gives it a method (and so
169
+ # a project entry) re-check the consumer.
170
+ def splice_external(scope, out, entries)
171
+ external = entries.find(&:external?)
172
+ if Analysis::DependencyRecorder.active?
173
+ external&.candidates&.each { |candidate| Analysis::DependencyRecorder.read_last_segment(:class, candidate) }
174
+ end
175
+ name = external&.candidates&.find { |candidate| scope.environment&.class_known?(candidate) }
176
+ tail = name && rbs_levels(scope, name)
177
+ tail && out.concat(tail)
178
+ end
179
+
180
+ # What follows the last project class on the walk: `Module` onwards for a module; RBS's singleton ancestry
181
+ # past the class's own singleton for a class RBS declares (a reopened core class, a `sig/` class);
182
+ # `Object`'s for a class with no superclass written. Nil where the superclass is not known: one the tables
183
+ # record as unknown, and one a constant write may have given the class (a class only a constant write names
184
+ # carries no superclass entry, and a reopening keeps the superclass that write made).
185
+ def implicit_tail(scope, name)
186
+ return rbs_instance_levels(scope, "Module") if module?(scope, name)
187
+ return nil if unknown_superclass?(scope, name)
188
+ return rbs_levels(scope, name, from: 1) if scope.environment&.class_known?(name)
189
+
190
+ rbs_levels(scope, "Object")
191
+ end
192
+
193
+ def module?(scope, name)
194
+ scope.discovered_parameter_envelopes[name]&.key?(Scope::DiscoveryIndex::ENVELOPE_MODULE_MARK) ||
195
+ scope.environment&.rbs_module?(name) || false
196
+ end
197
+
198
+ def unknown_superclass?(scope, name)
199
+ superclasses = scope.discovered_superclasses
200
+ return true if superclasses.key?(name) && superclasses[name].nil?
201
+ return true if scope.discovered_parameter_envelopes[name]&.key?(
202
+ Scope::DiscoveryIndex::ENVELOPE_META_SUPERCLASS_MARK
203
+ )
204
+
205
+ Analysis::DependencyRecorder.read_last_segment(:constant, name) if Analysis::DependencyRecorder.active?
206
+ !scope.bound_constant_names(name).empty?
207
+ end
208
+
209
+ # Does the project mix a module into `name`'s singleton class, at a position RBS does not record?
210
+ def singleton_mixin?(scope, name)
211
+ Analysis::DependencyRecorder.read_last_segment(:class, name) if Analysis::DependencyRecorder.active?
212
+ return true if scope.discovered_extends.key?(name)
213
+
214
+ Scope::ResolutionChain.for(scope, name, :singleton, :methods).wildcard_mixin?
215
+ end
216
+
217
+ # Does the singleton class of `name` define `method_name` itself: a singleton method the project writes in
218
+ # `name`'s own body (`def self.x`, `class << self`, an `attr_*` there), or one RBS declares on `name`'s
219
+ # singleton? A method the extends fold copied onto the singleton from a module `name` extends is that
220
+ # module's, found where the module sits.
221
+ def defines?(scope, name, method_name)
222
+ return true if own_singleton_definer?(scope, name, method_name)
223
+
224
+ definition = ExternalAncestorResolution.method_definition(name, method_name, :singleton, scope: scope)
225
+ ExternalAncestorResolution.declared_on_class?(definition, name)
226
+ end
227
+
228
+ # A singleton `def` is the class's own unless it is the def a module the class extends holds, which the extends
229
+ # fold copied ({.fold_copy?}). A singleton name without a node of its own (`attr_reader` or `define_method` in
230
+ # `class << self`) counts when the existence table holds it and the class's envelope table records it, which
231
+ # the fold never writes. A `define_singleton_method` in the body records only the opaque envelope entry, so it
232
+ # is no definer here; the typed arm reads that entry as a rewritten singleton name ({.rewritten?}).
233
+ def own_singleton_definer?(scope, name, method_name)
234
+ node = scope.singleton_def_for(name, method_name)
235
+ return true if node && !fold_copy?(scope, name, method_name)
236
+ return false unless scope.discovered_method?(name, method_name, :singleton)
237
+
238
+ node.nil? || scope.parameter_envelopes_of(name).key?([:singleton, method_name.to_sym])
239
+ end
240
+
241
+ # Is `name`'s singleton `def` of the name the receiverless instance `def` of the module its `Module.nesting`
242
+ # starts with, which the extends fold copied? The node is that module's own
243
+ # (`SingletonHookDecline.copied_from?`), or, where the two tables came from two parses of the file, one with
244
+ # its location and text, and no site of a singleton `def` of the name is recorded for `name` (the fold copies
245
+ # nodes, not sites).
246
+ def fold_copy?(scope, name, method_name)
247
+ head = SingletonHookDecline.def_head(scope, name, method_name)
248
+ return false if head.nil? || head == name
249
+ return true if SingletonHookDecline.copied_from?(scope, head, method_name, name)
250
+
251
+ node = scope.singleton_def_for(name, method_name)
252
+ source = scope.user_def_for(head, method_name)
253
+ !node.nil? && node.receiver.nil? && !source.nil? && same_def?(node, source) &&
254
+ scope.discovery.discovered_singleton_def_sources.dig(name, method_name.to_sym).nil?
255
+ end
256
+
257
+ def same_def?(node, other)
258
+ node.location.start_offset == other.location.start_offset &&
259
+ node.location.end_offset == other.location.end_offset && node.slice == other.slice
260
+ end
261
+
262
+ # The typed arm's answer at a singleton level (`entries` is every entry, an unnamed one kept as its candidate
263
+ # names): {Winner}, {UNKNOWN}, nil (the class object's own lookup answers), `:continue`, or `:continue_unsure`
264
+ # when only an unnamed entry may answer, so a refinement the walk reaches later is {UNKNOWN}
265
+ # (`RefinedDispatch.walk`). The tables record `class << self; prepend M` (and `singleton_class.prepend(M)`) as
266
+ # an `extend`, so a module on the level may sit ahead of the singleton class or of another module: one entry
267
+ # that answers decides, several that answer decide only when none of them is refined.
268
+ def walk_level(scope, entries, method_name, targets, unread)
269
+ answers = entries.filter_map { |entry| entry_answer(scope, entry, method_name, targets, unread) }
270
+ return :continue if answers.empty?
271
+
272
+ known = answers.reject { |answer| answer == UNNAMED }
273
+ return :continue_unsure if known.empty?
274
+ return known.first.answer if answers.size == 1
275
+
276
+ known.all? { |answer| answer.answer.nil? } ? nil : UNKNOWN
277
+ end
278
+
279
+ Answer = Data.define(:answer)
280
+ # An entry no table names (an `extend` of a module from outside the analysed paths): it may define the name.
281
+ UNNAMED = Answer.new(:unnamed)
282
+ private_constant :Answer, :UNNAMED
283
+
284
+ def entry_answer(scope, entry, method_name, targets, unread)
285
+ if entry.is_a?(Array)
286
+ refined = entry.any? { |candidate| targets.key?(candidate) || unread.key?(candidate) }
287
+ return refined ? Answer.new(UNKNOWN) : UNNAMED
288
+ end
289
+
290
+ if entry.is_a?(SingletonClass)
291
+ return Answer.new(UNKNOWN) if rewritten?(scope, entry.of, method_name)
292
+ else
293
+ return Answer.new(UNKNOWN) if unread.key?(entry)
294
+
295
+ refining = targets[entry]
296
+ return Answer.new(Winner.new(module_name: refining, refined_class: entry)) if refining
297
+ end
298
+ Answer.new(nil) if RefinedDispatch.defines?(scope, entry, method_name)
299
+ end
300
+
301
+ # Does the project rewrite `name`'s singleton methods in a way the walk cannot read: the class's dynamic mark
302
+ # (a `class_eval`, a computed definer, a `send`), or an opaque envelope entry for the singleton name (a
303
+ # `define_singleton_method`, a `def Const.x` written outside `name`'s body, an alias or a wrapping macro)?
304
+ def rewritten?(scope, name, method_name)
305
+ bucket = scope.parameter_envelopes_of(name)
306
+ bucket.key?(Scope::DiscoveryIndex::ENVELOPE_DYNAMIC_MARK) ||
307
+ Source::ParameterEnvelope.opaque?(bucket[[:singleton, method_name.to_sym]])
308
+ end
309
+
310
+ # Every class and module whose refinements a call on the class object `class_name` may reach, for
311
+ # `call.undefined-method`'s decline: each name on the levels (an unnamed entry's candidates and a `mixed`
312
+ # level's project mixins included), and the RBS ancestry of a module an extend names that the project does
313
+ # not declare. Nil when the walk cannot be read.
314
+ def reach(scope, class_name)
315
+ levels = levels(scope, class_name)
316
+ return nil if levels.nil?
317
+
318
+ names = Set.new
319
+ levels.each do |_level_class, entries, _mixed, every|
320
+ (every || entries).each { |entry| add_reach(scope, names, entry) }
321
+ end
322
+ names
323
+ end
324
+
325
+ def add_reach(scope, names, entry)
326
+ return names.merge(entry) if entry.is_a?(Array)
327
+ return if entry.is_a?(SingletonClass)
328
+
329
+ names << entry
330
+ return if scope.discovered_classes.key?(entry)
331
+
332
+ loader = scope.environment&.rbs_loader
333
+ names.merge(loader.ancestor_names_for(entry)) if loader
334
+ end
335
+
336
+ # Can a refined-class key sit on some class object's lookup the walk could not read? Every class object
337
+ # reaches the {TAIL_CLASSES} and whatever modules it extends, so only a class known to be none of those is
338
+ # out of reach: a project class that is not a module, or one RBS declares as a class. A key nothing declares
339
+ # may be a module.
340
+ def reachable?(scope, key)
341
+ return true if TAIL_CLASSES.include?(key)
342
+ return false if scope.discovered_classes.key?(key) && !module?(scope, key)
343
+
344
+ environment = scope.environment
345
+ !(environment&.class_known?(key) && !environment.rbs_module?(key))
346
+ end
347
+ end
348
+ end
349
+ end
350
+ end
@@ -26,6 +26,12 @@ module Rigor
26
26
  #
27
27
  # It shares the level walk with the typed arm's {RefinedDispatch.winner} and nothing else: the typer types
28
28
  # only from rows the lexical lookup resolves, while a decline must count every row.
29
+ #
30
+ # Issue #1752 — `kind: :singleton` asks the same of a class object, over its singleton levels
31
+ # ({ClassObject}): `Integer.sqrt` (CRuby's own singleton method of `Integer`) answers ahead of a refinement of
32
+ # `Object#sqrt`, while a refinement of a module the class extends answers ahead of the class's methods
33
+ # further up. A singleton class's definer is proven by its own `def self.x` / `class << self` definer
34
+ # (`DefinerResolution` `Known` on the singleton side) or by the catalogue's singleton methods.
29
35
  module OwnMethod # rubocop:disable Metrics/ModuleLength
30
36
  # One candidate definer as `DefinerResolution`'s per-chain answer function reports it, read through the
31
37
  # same four readers as the module's own hit (a call site may not name that one).
@@ -35,9 +41,10 @@ module Rigor
35
41
 
36
42
  module_function
37
43
 
38
- # Is `method_name` on an instance of `class_name` answered by the receiver's own method, ahead of every
39
- # refinement the in-effect list `list` (no unknown marker, no opaque module) puts in effect?
40
- def answers?(scope, class_name, method_name, list)
44
+ # Is `method_name` on an instance of `class_name` (on the class object itself for `kind: :singleton`)
45
+ # answered by the receiver's own method, ahead of every refinement the in-effect list `list` (no unknown
46
+ # marker, no opaque module) puts in effect?
47
+ def answers?(scope, class_name, method_name, list, kind: :instance)
41
48
  InEffectRefinements.record_refinement_names(method_name)
42
49
  raw = raw_targets(scope.discovered_refinements, method_name, list)
43
50
  return true if raw.nil?
@@ -47,11 +54,11 @@ module Rigor
47
54
  record_targets(scope, raw)
48
55
  return false if raw.each_key.any? { |refined| InEffectRefinements.class_wildcard_key?(scope, refined) }
49
56
 
50
- levels = RefinedDispatch.levels(scope, class_name)
57
+ levels = RefinedDispatch.levels(scope, class_name, kind: kind)
51
58
  return false if levels.nil?
52
- return false if refined_off_levels?(scope, class_name, levels, raw)
59
+ return false if refined_off_levels?(scope, [class_name, kind], levels, raw)
53
60
 
54
- shadowing_definer?(scope, class_name, levels, method_name, raw)
61
+ shadowing_definer?(scope, [class_name, kind], levels, method_name, raw)
55
62
  end
56
63
 
57
64
  # `{refined-class key => [its in-effect refining modules]}` for every key with a row for `method_name` or a
@@ -119,15 +126,20 @@ module Rigor
119
126
  # module (a module already on the class's levels is prepended again by `prepend`); a class RBS declares as
120
127
  # a subclass; a module not on the levels, which `obj.extend` or a subclass's mixin can put ahead of the
121
128
  # definer; and a module on the levels that some code prepends to an object's singleton class.
122
- def refined_off_levels?(scope, class_name, levels, raw)
123
- return true if unplaced_target?(scope, raw) || subclass_refined?(scope, class_name, raw)
129
+ #
130
+ # Issue #1752 — for a class object (`receiver` `[class_name, :singleton]`) a value typed as the class object
131
+ # may be a subclass's, whose singleton level may extend a refined module; and a class is off its lookup when it
132
+ # is placed away from `Module`, of which every class object is an instance.
133
+ def refined_off_levels?(scope, (class_name, kind), levels, raw)
134
+ return true if unplaced_target?(scope, raw) || subclass_refined?(scope, class_name, raw, kind)
124
135
 
136
+ placed_from = kind == :singleton ? "Module" : class_name
125
137
  on_levels = level_names(levels)
126
138
  raw.each_key.any? do |refined|
127
139
  next object_prepended?(scope, refined) if on_levels.include?(refined)
128
140
  next true if module_name?(scope, refined)
129
141
 
130
- declared_class?(scope, refined) && !placed_away?(scope, refined, class_name)
142
+ declared_class?(scope, refined) && !placed_away?(scope, refined, placed_from)
131
143
  end
132
144
  end
133
145
 
@@ -233,12 +245,16 @@ module Rigor
233
245
  # refined entry on its own level, or a level the walk cannot read?
234
246
  #
235
247
  # A subclass level the walk cannot read (a truncated chain, or a mixin the tables cannot name) hides only
236
- # modules, so it declines only when some in-effect row names a module.
237
- def subclass_refined?(scope, class_name, raw)
248
+ # modules, so it declines only when some in-effect row names a module. `kind` `:singleton` reads each
249
+ # subclass's singleton level, whose own singleton class no refinement table key names, and which cannot place
250
+ # an `extend` the chain skipped there (`ClassObject.skipped_extend?`: it may be a `prepend` the tables record as
251
+ # one).
252
+ def subclass_refined?(scope, class_name, raw, kind = :instance)
238
253
  each_subclass(scope, class_name) do |subclass|
239
- chain = Scope::ResolutionChain.for(scope, subclass, :instance, :methods)
254
+ chain = Scope::ResolutionChain.for(scope, subclass, kind, :methods)
240
255
  chain.record(scope) if Analysis::DependencyRecorder.active?
241
- if chain.truncated? || chain.wildcard_mixin? || chain.level_count.zero?
256
+ if chain.truncated? || chain.wildcard_mixin? || chain.level_count.zero? ||
257
+ (kind == :singleton && singleton_level_unreadable?(scope, subclass, chain))
242
258
  return true if raw.each_key.any? { |refined| module_name?(scope, refined) }
243
259
 
244
260
  next
@@ -248,8 +264,16 @@ module Rigor
248
264
  false
249
265
  end
250
266
 
267
+ # A subclass's singleton level is unreadable where it records an `extend` the chain skipped, or where its
268
+ # instance chain holds a mixin the tables cannot name, which an `included` hook can use to extend it.
269
+ def singleton_level_unreadable?(scope, subclass, chain)
270
+ ClassObject.skipped_extend?(scope, subclass, ClassObject.level_names(chain, 0)) ||
271
+ ClassObject.instance_unreadable?(scope, subclass)
272
+ end
273
+
251
274
  def refined_entry?(entry, raw)
252
275
  return entry.candidates.any? { |candidate| raw.include?(candidate) } if entry.external?
276
+ return false if entry.side == :singleton
253
277
 
254
278
  raw.include?(entry.name)
255
279
  end
@@ -306,20 +330,34 @@ module Rigor
306
330
  # may be prepended or refined), an unproven definer, and the end of the walk all answer false. An entry no
307
331
  # table names is passed over otherwise: it cannot be proven a definer, and a definer the walk misses there
308
332
  # sits ahead of every refinement the walk then reaches.
309
- def shadowing_definer?(scope, class_name, levels, method_name, raw)
333
+ #
334
+ # Issue #1752 — a singleton level with a refined module answers false whatever its order: the tables record
335
+ # `class << self; prepend M` as an `extend`, so the module may sit ahead of the class's own singleton methods.
336
+ def shadowing_definer?(scope, receiver, levels, method_name, raw)
310
337
  levels.each do |level_class, entries, mixed, every|
311
338
  return false if mixed || (level_class && raw.include?(level_class))
312
339
 
313
- (every || entries).each do |entry|
340
+ row = every || entries
341
+ return false if level_class.is_a?(SingletonClass) && singleton_level_refined?(row, raw)
342
+
343
+ row.each do |entry|
314
344
  case entry_answer(scope, entry, level_class, method_name, raw)
315
345
  when :refined then return false
316
- when :defines then return proven_definer?(scope, class_name, entry, method_name)
346
+ when :defines then return proven_definer?(scope, receiver, entry, method_name)
317
347
  end
318
348
  end
319
349
  end
320
350
  false
321
351
  end
322
352
 
353
+ def singleton_level_refined?(row, raw)
354
+ row.any? do |entry|
355
+ next entry.any? { |candidate| raw.include?(candidate) } if entry.is_a?(Array)
356
+
357
+ !entry.is_a?(SingletonClass) && raw.include?(entry)
358
+ end
359
+ end
360
+
323
361
  # `:refined`, `:defines`, or nil to move on. The level's own class was asked before its entries.
324
362
  def entry_answer(scope, entry, level_class, method_name, raw)
325
363
  return entry.any? { |candidate| raw.include?(candidate) } ? :refined : nil if entry.is_a?(Array)
@@ -346,57 +384,91 @@ module Rigor
346
384
  #
347
385
  # ADR-46 — the envelope bucket records the files declaring `entry`, and the class edge a new file reopening
348
386
  # it (to `remove_method` the name, say) fires.
349
- def proven_definer?(scope, class_name, entry, method_name)
350
- Analysis::DependencyRecorder.read_last_segment(:class, entry) if Analysis::DependencyRecorder.active?
351
- bucket = scope.parameter_envelopes_of(entry)
387
+ #
388
+ # Issue #1752 — a {SingletonClass} entry is proven on its class's singleton side: its own singleton definer,
389
+ # `Known` on the class object's chain, or the catalogue's singleton methods. Any other entry of a class
390
+ # object's walk (an extended module; `Class`, `Module`, `Object`, `Kernel`) is proven as an instance-side
391
+ # entry is, with the certainty read over the class object's chain.
392
+ def proven_definer?(scope, receiver, entry, method_name)
393
+ owner, side = entry.is_a?(SingletonClass) ? [entry.of, :singleton] : [entry, :instance]
394
+ Analysis::DependencyRecorder.read_last_segment(:class, owner) if Analysis::DependencyRecorder.active?
395
+ bucket = scope.parameter_envelopes_of(owner)
352
396
  return false if bucket.key?(Scope::DiscoveryIndex::ENVELOPE_DYNAMIC_MARK)
353
397
  return false if bucket.key?([Scope::DiscoveryIndex::ENVELOPE_REMOVED_KIND, method_name.to_sym])
354
398
 
355
- opaque = Source::ParameterEnvelope.opaque?(bucket[[:instance, method_name.to_sym]])
356
- return true if project_definer?(scope, class_name, entry, method_name, opaque)
399
+ opaque = Source::ParameterEnvelope.opaque?(bucket[[side, method_name.to_sym]])
400
+ return true if project_definer?(scope, receiver, [owner, side], method_name, opaque)
401
+ return false if opaque
357
402
 
358
- !opaque && Builtins::CRubyDefiners.defines?(entry, method_name)
403
+ if side == :singleton
404
+ Builtins::CRubyDefiners.singleton_defines?(owner, method_name)
405
+ else
406
+ Builtins::CRubyDefiners.defines?(owner, method_name)
407
+ end
359
408
  end
360
409
 
361
410
  # A `def` counts while its envelope is its own (an alias or a wrapping macro makes it opaque); an `attr_*` or
362
411
  # `define_method` always records an opaque one, so for those the existence table and the certainty read are
363
- # the proof, beside the removal check above.
364
- def project_definer?(scope, class_name, entry, method_name, opaque)
365
- if scope.user_def_for(entry, method_name)
366
- return false if opaque
367
- elsif !scope.discovered_method?(entry, method_name, :instance)
368
- return false
369
- end
412
+ # the proof, beside the removal check above. A singleton name the extends fold copied from an extended module
413
+ # is that module's (`ClassObject.own_singleton_definer?`).
414
+ def project_definer?(scope, receiver, (owner, side), method_name, opaque)
415
+ own = side == :singleton ? own_singleton_kind(scope, owner, method_name) : own_kind(scope, owner, method_name)
416
+ return false if own.nil? || (own == :def && opaque)
417
+
418
+ certain_owner?(scope, receiver, owner, method_name)
419
+ end
420
+
421
+ # `:def` for a `def` of the owner's own, `:other` for another definer the existence table records, or nil.
422
+ def own_kind(scope, owner, method_name)
423
+ return :def if scope.user_def_for(owner, method_name)
370
424
 
371
- certain_owner?(scope, class_name, entry, method_name)
425
+ :other if scope.discovered_method?(owner, method_name, :instance)
372
426
  end
373
427
 
374
- # ADR-119 WD2 — the candidate-set read over the receiver's chain, with every project `def`, `attr_*` and
375
- # `define_method` as a candidate: true only for a `Known` answer whose owner is `entry`.
376
- def certain_owner?(scope, class_name, entry, method_name)
377
- case DefinerResolution.resolve(scope, class_name, method_name, :instance, question: :definer,
428
+ def own_singleton_kind(scope, owner, method_name)
429
+ node = scope.singleton_def_for(owner, method_name)
430
+ return :def if node && !ClassObject.fold_copy?(scope, owner, method_name)
431
+
432
+ :other if ClassObject.own_singleton_definer?(scope, owner, method_name)
433
+ end
434
+
435
+ # ADR-119 WD2 — the candidate-set read over the receiver's chain (the class object's for `kind` `:singleton`),
436
+ # with every project `def`, `attr_*` and `define_method` as a candidate: true only for a `Known` answer whose
437
+ # owner is `owner`.
438
+ def certain_owner?(scope, (class_name, kind), owner, method_name)
439
+ case DefinerResolution.resolve(scope, class_name, method_name, kind, question: :definer,
378
440
  &definer_answer(scope, method_name))
379
- in DefinerResolution::Known(answer: _answer, owner:) then owner == entry
441
+ in DefinerResolution::Known(answer: _answer, owner: found) then found == owner
380
442
  in DefinerResolution::UNKNOWN then false
381
443
  in DefinerResolution::ABSENT then false # rubocop:disable Lint/DuplicateBranch
382
444
  end
383
445
  end
384
446
 
385
- # The first project entry at or after `from` that defines `method_name` on its instance side, by a `def` or
386
- # by another definer the existence table records (`attr_*`, `define_method`).
447
+ # The first project entry at or after `from` that defines `method_name` on its side, by a `def` or by another
448
+ # definer the existence table records (`attr_*`, `define_method`; an `attr_*` or `define_method` in
449
+ # `class << self` on a singleton entry, while a body's `define_singleton_method` records no existence entry
450
+ # and proves nothing). A singleton name the extends fold copied is asked past by `DefinerResolution` itself,
451
+ # so the module that wrote it answers at its own position.
387
452
  def definer_answer(scope, method_name)
388
453
  lambda do |chain, from|
389
454
  index = from - 1
390
455
  chain.search(scope, from) do |entry|
391
456
  index += 1
392
- next if entry.external? || entry.side != :instance
393
- next unless scope.user_def_for(entry.name, method_name) ||
394
- scope.discovered_method?(entry.name, method_name, :instance)
457
+ next if entry.external? || !entry_defines?(scope, entry, method_name)
395
458
 
396
459
  DefinerHit.new(entry.name, entry.name, index, entry.side)
397
460
  end
398
461
  end
399
462
  end
463
+
464
+ def entry_defines?(scope, entry, method_name)
465
+ name = entry.name
466
+ if entry.side == :singleton
467
+ scope.singleton_def_for(name, method_name) || scope.discovered_method?(name, method_name, :singleton)
468
+ else
469
+ scope.user_def_for(name, method_name) || scope.discovered_method?(name, method_name, :instance)
470
+ end
471
+ end
400
472
  end
401
473
  end
402
474
  end