rigortype 0.4.1 → 0.4.2

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 (182) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/data/builtins/ruby_core/io.yml +1 -0
  4. data/data/builtins/ruby_core/proc.yml +4 -0
  5. data/data/builtins/ruby_core/random.yml +4 -0
  6. data/data/builtins/ruby_core/struct.yml +3 -0
  7. data/data/core_overlay/env.rbs +34 -0
  8. data/data/core_overlay/gc.rbs +50 -0
  9. data/data/core_overlay/hash.rbs +45 -0
  10. data/data/core_overlay/integer.rbs +31 -0
  11. data/data/core_overlay/io_buffer.rbs +27 -0
  12. data/data/core_overlay/kernel.rbs +40 -0
  13. data/data/core_overlay/match_data.rbs +28 -0
  14. data/data/core_overlay/module.rbs +51 -0
  15. data/data/core_overlay/object_space.rbs +29 -0
  16. data/data/core_overlay/proc.rbs +45 -0
  17. data/data/core_overlay/range.rbs +40 -0
  18. data/data/core_overlay/source_range.rbs +78 -0
  19. data/data/core_overlay/string.rbs +86 -0
  20. data/data/vendored_gem_sigs/prime/integer-extension.rbs +41 -0
  21. data/data/vendored_gem_sigs/prime/prime.rbs +372 -0
  22. data/docs/handbook/01-getting-started.md +3 -3
  23. data/docs/handbook/06-classes.md +3 -1
  24. data/docs/handbook/11-sig-gen.md +6 -0
  25. data/docs/handbook/appendix-liskov.md +5 -0
  26. data/docs/manual/03-configuration.md +1 -1
  27. data/docs/manual/04-diagnostics.md +2 -1
  28. data/docs/manual/06-baseline.md +2 -0
  29. data/docs/manual/07-plugins.md +1 -1
  30. data/docs/manual/18-removing-dead-code.md +32 -2
  31. data/docs/manual/plugins/README.md +6 -1
  32. data/docs/manual/plugins/rigor-activerecord.md +21 -0
  33. data/docs/manual/plugins/rigor-alba.md +60 -0
  34. data/docs/manual/plugins/rigor-minitest.md +22 -4
  35. data/docs/manual/plugins/rigor-typelizer.md +71 -0
  36. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
  37. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
  38. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
  39. data/lib/rigor/analysis/check_rules/rule_ids.rb +4 -0
  40. data/lib/rigor/analysis/check_rules/source_arity.rb +21 -2
  41. data/lib/rigor/analysis/check_rules.rb +289 -57
  42. data/lib/rigor/analysis/dependency_recorder.rb +15 -0
  43. data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
  44. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
  45. data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
  46. data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
  47. data/lib/rigor/analysis/erb_template_detector.rb +2 -1
  48. data/lib/rigor/analysis/incremental.rb +29 -9
  49. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +30 -5
  50. data/lib/rigor/analysis/incremental_run_slot.rb +3 -1
  51. data/lib/rigor/analysis/incremental_session.rb +14 -1
  52. data/lib/rigor/analysis/reachability/graph.rb +123 -23
  53. data/lib/rigor/analysis/reachability/scan.rb +290 -36
  54. data/lib/rigor/analysis/rule_catalog.rb +43 -0
  55. data/lib/rigor/analysis/run_cache_key.rb +16 -1
  56. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +87 -5
  57. data/lib/rigor/analysis/runner/pool_coordinator.rb +50 -11
  58. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  59. data/lib/rigor/analysis/runner.rb +34 -3
  60. data/lib/rigor/analysis/worker_session.rb +7 -4
  61. data/lib/rigor/cache/incremental_snapshot.rb +22 -0
  62. data/lib/rigor/cli/annotate_command.rb +5 -0
  63. data/lib/rigor/cli/baseline_command.rb +7 -18
  64. data/lib/rigor/cli/check_command.rb +2 -1
  65. data/lib/rigor/cli/trace_command.rb +5 -0
  66. data/lib/rigor/cli/type_of_command.rb +5 -3
  67. data/lib/rigor/cli/unused_command.rb +13 -7
  68. data/lib/rigor/cli.rb +10 -2
  69. data/lib/rigor/configuration/severity_profile.rb +6 -0
  70. data/lib/rigor/configuration.rb +46 -2
  71. data/lib/rigor/environment/gated_signature_guard.rb +43 -0
  72. data/lib/rigor/environment/gated_signature_plan.rb +339 -0
  73. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  74. data/lib/rigor/environment/rbs_loader.rb +283 -14
  75. data/lib/rigor/environment/required_features.rb +206 -0
  76. data/lib/rigor/environment.rb +21 -3
  77. data/lib/rigor/inference/acceptance.rb +28 -3
  78. data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
  79. data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
  80. data/lib/rigor/inference/def_node_resolver.rb +23 -1
  81. data/lib/rigor/inference/expression_typer.rb +215 -16
  82. data/lib/rigor/inference/global_write_census.rb +7 -4
  83. data/lib/rigor/inference/guard_rebinding.rb +1 -1
  84. data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
  85. data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
  86. data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +282 -0
  87. data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
  88. data/lib/rigor/inference/in_effect_refinements.rb +256 -0
  89. data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
  90. data/lib/rigor/inference/key_presence_guard.rb +303 -0
  91. data/lib/rigor/inference/macro_block_self_type.rb +59 -12
  92. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  93. data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
  94. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
  95. data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
  96. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
  97. data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
  98. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
  99. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
  100. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +259 -28
  101. data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
  102. data/lib/rigor/inference/method_dispatcher.rb +44 -11
  103. data/lib/rigor/inference/module_function_state.rb +2 -1
  104. data/lib/rigor/inference/narrowing.rb +20 -6
  105. data/lib/rigor/inference/object_mixins.rb +180 -0
  106. data/lib/rigor/inference/project_patched_methods.rb +10 -5
  107. data/lib/rigor/inference/project_patched_scanner.rb +71 -3
  108. data/lib/rigor/inference/refine_census.rb +239 -0
  109. data/lib/rigor/inference/refine_self.rb +317 -0
  110. data/lib/rigor/inference/refined_dispatch/own_method.rb +403 -0
  111. data/lib/rigor/inference/refined_dispatch.rb +272 -0
  112. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +13 -0
  113. data/lib/rigor/inference/scope_indexer.rb +687 -60
  114. data/lib/rigor/inference/statement_evaluator.rb +71 -20
  115. data/lib/rigor/inference/string_mutation.rb +9 -3
  116. data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
  117. data/lib/rigor/inference/version_guard.rb +19 -10
  118. data/lib/rigor/language_server/project_context.rb +31 -0
  119. data/lib/rigor/language_server/server.rb +18 -0
  120. data/lib/rigor/plugin/inflector.rb +1 -1
  121. data/lib/rigor/plugin/io_boundary.rb +42 -3
  122. data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
  123. data/lib/rigor/project_environment.rb +29 -6
  124. data/lib/rigor/protection/mutation_cache.rb +1 -1
  125. data/lib/rigor/protection/mutator.rb +1 -1
  126. data/lib/rigor/reflection/constant_ancestors.rb +127 -11
  127. data/lib/rigor/reflection/constant_path.rb +13 -2
  128. data/lib/rigor/reflection.rb +40 -8
  129. data/lib/rigor/scope/discovery_index.rb +40 -1
  130. data/lib/rigor/scope/resolution_chain.rb +159 -11
  131. data/lib/rigor/scope.rb +189 -10
  132. data/lib/rigor/sig_gen/classification.rb +4 -1
  133. data/lib/rigor/sig_gen/generator.rb +55 -5
  134. data/lib/rigor/sig_gen/method_candidate.rb +2 -2
  135. data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
  136. data/lib/rigor/source/alias_names.rb +52 -0
  137. data/lib/rigor/source/literals.rb +3 -2
  138. data/lib/rigor/source/symbol_name.rb +20 -0
  139. data/lib/rigor/type/combinator.rb +34 -2
  140. data/lib/rigor/type/float_range.rb +4 -1
  141. data/lib/rigor/type/nominal.rb +6 -1
  142. data/lib/rigor/version.rb +1 -1
  143. data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
  144. data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
  145. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
  146. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
  147. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
  148. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
  149. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
  150. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
  151. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
  152. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
  153. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
  154. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
  155. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
  156. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
  157. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
  158. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
  159. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
  160. data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
  161. data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
  162. data/plugins/rigor-alba/sig/alba.rbs +4 -0
  163. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
  164. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
  165. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +7 -6
  166. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
  167. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
  168. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
  169. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
  170. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
  171. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
  172. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
  173. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
  174. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
  175. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
  176. data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
  177. data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
  178. data/sig/rigor/plugin/io_boundary.rbs +3 -0
  179. data/sig/rigor/scope.rbs +27 -4
  180. data/sig/rigor/source.rbs +4 -0
  181. data/sig/rigor.rbs +3 -1
  182. metadata +66 -1
@@ -0,0 +1,403 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../builtins/cruby_definers"
4
+
5
+ module Rigor
6
+ module Inference
7
+ module RefinedDispatch
8
+ # Issue #1740 — does the receiver's own lookup answer a call ahead of every refinement in effect, so that
9
+ # `call.wrong-arity` and `call.argument-type-mismatch` may check it against the receiver's own method? Ruby
10
+ # finds a method the receiver's class (or a nearer ancestor) defines before it consults an ancestor's
11
+ # refinement: under `refine Object do def center(a, b, c)`, `"x".center(1, 2, 3)` still runs `String#center`
12
+ # and raises.
13
+ #
14
+ # It answers yes only from positive knowledge (the caller has already declined on an in-effect list that
15
+ # carries {InEffectRefinements::UNKNOWN} or an opaque module), and every condition it cannot prove is a no:
16
+ #
17
+ # - it reads the RAW refinement table, every name a `refine` argument's spelling can denote and the wildcard
18
+ # rows included, never {RefinedDispatch.resolved}'s typing subset: a decline needs only that a row may
19
+ # denote a class on the walk;
20
+ # - no in-effect row may name a class the walk cannot name, a project subclass of the receiver's class (a
21
+ # value typed as the class may be one), or a module the receiver's levels do not hold (`obj.extend(M)`,
22
+ # or a subclass's mixin);
23
+ # - walking {RefinedDispatch.levels} from the most derived, the first level or entry that is refined or
24
+ # defines the name must define it, with no `mixed` level (a project mixin RBS does not place) before it;
25
+ # - that definer must be proven ({.proven_definer?}): RBS alone is not proof.
26
+ #
27
+ # It shares the level walk with the typed arm's {RefinedDispatch.winner} and nothing else: the typer types
28
+ # only from rows the lexical lookup resolves, while a decline must count every row.
29
+ module OwnMethod # rubocop:disable Metrics/ModuleLength
30
+ # One candidate definer as `DefinerResolution`'s per-chain answer function reports it, read through the
31
+ # same four readers as the module's own hit (a call site may not name that one).
32
+ DefinerHit = Data.define(:answer, :owner, :index, :side)
33
+ EMPTY = [].freeze
34
+ private_constant :DefinerHit, :EMPTY
35
+
36
+ module_function
37
+
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)
41
+ InEffectRefinements.record_refinement_names(method_name)
42
+ raw = raw_targets(scope.discovered_refinements, method_name, list)
43
+ return true if raw.nil?
44
+
45
+ add_ancestor_spellings(scope, raw)
46
+
47
+ record_targets(scope, raw)
48
+ return false if raw.each_key.any? { |refined| InEffectRefinements.class_wildcard_key?(scope, refined) }
49
+
50
+ levels = RefinedDispatch.levels(scope, class_name)
51
+ return false if levels.nil?
52
+ return false if refined_off_levels?(scope, class_name, levels, raw)
53
+
54
+ shadowing_definer?(scope, class_name, levels, method_name, raw)
55
+ end
56
+
57
+ # `{refined-class key => [its in-effect refining modules]}` for every key with a row for `method_name` or a
58
+ # names-wildcard row that `list` puts in effect, by every spelling the table records, or nil when there is
59
+ # none.
60
+ def raw_targets(refinements, method_name, list)
61
+ out = nil
62
+ refinements.each do |refined, methods|
63
+ modules = in_effect_modules(methods[method_name], list) |
64
+ in_effect_modules(methods[RefinedDispatch.wildcard], list)
65
+ (out ||= {})[refined] = modules unless modules.empty?
66
+ end
67
+ out
68
+ end
69
+
70
+ # Ruby looks a constant up through the refining module's ancestors before the top level, so `refine(Bar)` in
71
+ # a module that includes `Foo` refines `Foo::Bar` when `Foo` holds one; the table records the lexical
72
+ # spellings only (`R::Bar`, `Bar`). Each declared `<ancestor>::<last segment>` joins the raw targets for the
73
+ # same refining module: over-adding only declines.
74
+ def add_ancestor_spellings(scope, raw)
75
+ raw.to_a.each do |refined, modules|
76
+ segment = refined.split("::").last
77
+ modules.each do |module_name|
78
+ ancestor_modules(scope, module_name).each do |ancestor|
79
+ spelling = "#{ancestor}::#{segment}"
80
+ next unless declared_class?(scope, spelling)
81
+
82
+ raw[spelling] = (raw[spelling] || EMPTY) | [module_name]
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ # The modules `module_name` includes or prepends, transitively (`InEffectRefinements.activated_modules`, which
89
+ # records the chain's edges), itself left out.
90
+ def ancestor_modules(scope, module_name)
91
+ return EMPTY if module_name == RefinedDispatch.wildcard
92
+
93
+ ancestors = InEffectRefinements.activated_modules(scope, module_name)
94
+ ancestors.is_a?(Array) ? ancestors - [module_name] : EMPTY
95
+ end
96
+
97
+ def in_effect_modules(rows, list)
98
+ return EMPTY if rows.nil?
99
+
100
+ rows.select { |name| RefinedDispatch.in_effect?([name], list) }
101
+ end
102
+
103
+ # ADR-46 — a refined class appearing or vanishing (a new subclass declaration, a module turning up where a
104
+ # spelling named nothing), or an edit to a file declaring one (`class Kid` becoming `class Kid < Base`),
105
+ # moves the answer.
106
+ def record_targets(scope, raw)
107
+ return unless Analysis::DependencyRecorder.active?
108
+
109
+ raw.each_key do |refined|
110
+ next if refined == RefinedDispatch.wildcard
111
+
112
+ Analysis::DependencyRecorder.read_last_segment(:class, refined)
113
+ Scope::ResolutionChain.record_class(scope, refined)
114
+ end
115
+ end
116
+
117
+ # A refined class the receiver's levels do not show, but which a value typed as the receiver's class may
118
+ # still reach (critique F4): a project subclass of the class, or one whose own level mixes in a refined
119
+ # module (a module already on the class's levels is prepended again by `prepend`); a class RBS declares as
120
+ # a subclass; a module not on the levels, which `obj.extend` or a subclass's mixin can put ahead of the
121
+ # 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)
124
+
125
+ on_levels = level_names(levels)
126
+ raw.each_key.any? do |refined|
127
+ next object_prepended?(scope, refined) if on_levels.include?(refined)
128
+ next true if module_name?(scope, refined)
129
+
130
+ declared_class?(scope, refined) && !placed_away?(scope, refined, class_name)
131
+ end
132
+ end
133
+
134
+ # Issue #1820 — can the class `key`, off the receiver's levels, be proven never to sit at or below
135
+ # `class_name` in an object's ancestry? Only when RBS does not declare it a subclass and its whole instance
136
+ # chain is read: no cut, no mixin the tables cannot name, no superclass the tables do not know (`class K <
137
+ # <expr>`, `K = Class.new(Parent)`, a superclass RBS does not declare), and `class_name` nowhere on it.
138
+ def placed_away?(scope, key, class_name)
139
+ return false if rbs_subclass?(scope, key, class_name)
140
+ return rbs_away?(scope, key, class_name) unless project_class?(scope, key)
141
+
142
+ chain = Scope::ResolutionChain.for(scope, key, :instance, :methods)
143
+ chain.record(scope) if Analysis::DependencyRecorder.active?
144
+ return false if chain.truncated? || chain.wildcard_mixin?
145
+
146
+ chain.level_count.times.all? { |index| level_away?(scope, chain, index, class_name) }
147
+ end
148
+
149
+ # A project level is away when it is not `class_name` and its superclass is known; an external superclass
150
+ # level when RBS declares it and its RBS ancestors do not hold `class_name`.
151
+ def level_away?(scope, chain, index, class_name)
152
+ level_class = chain.level_classes[index]
153
+ return level_class != class_name && !unknown_superclass?(scope, level_class) if level_class
154
+
155
+ external = chain.level_entries(index).find(&:external?)
156
+ name = external&.candidates&.find { |candidate| scope.environment&.class_known?(candidate) }
157
+ !name.nil? && rbs_away?(scope, name, class_name)
158
+ end
159
+
160
+ def rbs_away?(scope, name, class_name)
161
+ loader = scope.environment&.rbs_loader
162
+ ancestors = loader ? loader.ancestor_names_for(name) : EMPTY
163
+ !ancestors.empty? && !ancestors.include?(class_name)
164
+ end
165
+
166
+ def project_class?(scope, name)
167
+ scope.discovered_classes.key?(name) || !(scope.discovered_class_sources[name] || EMPTY).empty? ||
168
+ scope.known_user_class?(name)
169
+ end
170
+
171
+ # A project class whose superclass the tables do not know: `class K < <expr>` (the superclass table records
172
+ # `nil`) or `K = Class.new(Parent)` (`ENVELOPE_META_SUPERCLASS_MARK`).
173
+ def unknown_superclass?(scope, name)
174
+ superclasses = scope.discovered_superclasses
175
+ return true if superclasses.key?(name) && superclasses[name].nil?
176
+
177
+ scope.discovered_parameter_envelopes[name]&.key?(Scope::DiscoveryIndex::ENVELOPE_META_SUPERCLASS_MARK) ||
178
+ false
179
+ end
180
+
181
+ # A module some code `prepend`s to an object's singleton class (or `extend`s an object with in a method
182
+ # body), which puts it ahead of the object's class even where the class already includes it.
183
+ def object_prepended?(scope, name)
184
+ scope.discovered_parameter_envelopes[name]&.key?(Scope::DiscoveryIndex::ENVELOPE_OBJECT_EXTENDED_MARK) ||
185
+ false
186
+ end
187
+
188
+ # Is some in-effect row under a class no table declares by any spelling of it (the keys sharing its last
189
+ # segment that the same refining module refines)? Ruby resolved that `refine` argument to some class, through
190
+ # a lookup the table did not follow (the refining module's own ancestors: `module R; include Foo;
191
+ # refine(Bar)` refines `Foo::Bar`) or to a class outside the analysed paths (a vendored `VendorStr <
192
+ # String`), and Rigor cannot place it in the receiver's hierarchy.
193
+ def unplaced_target?(scope, raw)
194
+ raw.any? do |refined, modules|
195
+ segment = refined.split("::").last
196
+ modules.any? do |module_name|
197
+ raw.none? do |other, others|
198
+ others.include?(module_name) && other.split("::").last == segment && declared_class?(scope, other)
199
+ end
200
+ end
201
+ end
202
+ end
203
+
204
+ def declared_class?(scope, name)
205
+ scope.discovered_classes.key?(name) || !(scope.discovered_class_sources[name] || EMPTY).empty? ||
206
+ scope.known_user_class?(name) || scope.environment&.class_known?(name) || false
207
+ end
208
+
209
+ def level_names(levels)
210
+ names = Set.new
211
+ levels.each do |level_class, entries, _mixed, every|
212
+ names << level_class if level_class
213
+ (every || entries).each { |entry| entry.is_a?(Array) ? names.merge(entry) : names << entry }
214
+ end
215
+ names
216
+ end
217
+
218
+ def module_name?(scope, name)
219
+ bucket = scope.discovered_parameter_envelopes[name]
220
+ return true if bucket&.key?(Scope::DiscoveryIndex::ENVELOPE_MODULE_MARK)
221
+
222
+ scope.environment&.rbs_module?(name) || false
223
+ end
224
+
225
+ def rbs_subclass?(scope, refined, class_name)
226
+ environment = scope.environment
227
+ !environment.nil? && environment.class_known?(refined) &&
228
+ environment.class_ordering(refined, class_name) == :subclass
229
+ end
230
+
231
+ # Does a project subclass of `class_name` (the `discovered_superclasses` walk `SourceArity` makes, its
232
+ # parents resolved to any class the project or RBS knows, so a subclass of a core class counts) carry a
233
+ # refined entry on its own level, or a level the walk cannot read?
234
+ #
235
+ # 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)
238
+ each_subclass(scope, class_name) do |subclass|
239
+ chain = Scope::ResolutionChain.for(scope, subclass, :instance, :methods)
240
+ chain.record(scope) if Analysis::DependencyRecorder.active?
241
+ if chain.truncated? || chain.wildcard_mixin? || chain.level_count.zero?
242
+ return true if raw.each_key.any? { |refined| module_name?(scope, refined) }
243
+
244
+ next
245
+ end
246
+ return true if chain.level_entries(0).any? { |entry| refined_entry?(entry, raw) }
247
+ end
248
+ false
249
+ end
250
+
251
+ def refined_entry?(entry, raw)
252
+ return entry.candidates.any? { |candidate| raw.include?(candidate) } if entry.external?
253
+
254
+ raw.include?(entry.name)
255
+ end
256
+
257
+ # ADR-46 — every class whose subclasses are enumerated records `subclass_of:<last segment>`, which a class
258
+ # declaration with that superclass appearing or vanishing fires (`IncrementalSession#negative_affected`).
259
+ # A class whose superclass the tables do not know may be a subclass of any class, so it joins the walk too
260
+ # (`subclass_of:*`, which any class declaration appearing or vanishing fires).
261
+ def each_subclass(scope, class_name)
262
+ children = children_by_parent(scope)
263
+ record_subclasses_read(class_name)
264
+ queue = children.fetch(class_name.to_s, []) + unknown_superclass_classes(scope, class_name)
265
+ seen = {}
266
+ until queue.empty?
267
+ subclass = queue.shift
268
+ next if seen[subclass]
269
+
270
+ seen[subclass] = true
271
+ record_subclasses_read(subclass)
272
+ yield subclass
273
+ queue.concat(children.fetch(subclass, []))
274
+ end
275
+ end
276
+
277
+ def unknown_superclass_classes(scope, class_name)
278
+ Analysis::DependencyRecorder.read_name(:subclass_of, "*") if Analysis::DependencyRecorder.active?
279
+ mark = Scope::DiscoveryIndex::ENVELOPE_META_SUPERCLASS_MARK
280
+ computed = scope.discovered_superclasses.filter_map { |child, parent| child if parent.nil? }
281
+ meta = scope.discovered_parameter_envelopes.filter_map { |name, bucket| name if bucket.key?(mark) }
282
+ (computed + meta).uniq - [class_name.to_s]
283
+ end
284
+
285
+ def record_subclasses_read(class_name)
286
+ return unless Analysis::DependencyRecorder.active?
287
+
288
+ Analysis::DependencyRecorder.read_last_segment(:subclass_of, class_name.to_s)
289
+ end
290
+
291
+ def children_by_parent(scope)
292
+ scope.discovered_superclasses.each_with_object({}) do |(child, raw_parent), children|
293
+ parent = scope.ancestor_name_candidates(child, raw_parent).find { |name| known_class?(scope, name) }
294
+ (children[parent] ||= []) << child if parent
295
+ end
296
+ end
297
+
298
+ def known_class?(scope, name)
299
+ scope.known_user_class?(name) || scope.discovered_parameter_envelopes.key?(name) ||
300
+ scope.environment&.class_known?(name) || false
301
+ end
302
+
303
+ # Walks the levels from the most derived: true when the first level or entry that a raw target names or
304
+ # that defines `method_name` defines it, and that definer is proven. A refined level or entry (an entry no
305
+ # table names by any of its candidate names), a `mixed` level (a project mixin RBS does not place, which
306
+ # may be prepended or refined), an unproven definer, and the end of the walk all answer false. An entry no
307
+ # table names is passed over otherwise: it cannot be proven a definer, and a definer the walk misses there
308
+ # sits ahead of every refinement the walk then reaches.
309
+ def shadowing_definer?(scope, class_name, levels, method_name, raw)
310
+ levels.each do |level_class, entries, mixed, every|
311
+ return false if mixed || (level_class && raw.include?(level_class))
312
+
313
+ (every || entries).each do |entry|
314
+ case entry_answer(scope, entry, level_class, method_name, raw)
315
+ when :refined then return false
316
+ when :defines then return proven_definer?(scope, class_name, entry, method_name)
317
+ end
318
+ end
319
+ end
320
+ false
321
+ end
322
+
323
+ # `:refined`, `:defines`, or nil to move on. The level's own class was asked before its entries.
324
+ def entry_answer(scope, entry, level_class, method_name, raw)
325
+ return entry.any? { |candidate| raw.include?(candidate) } ? :refined : nil if entry.is_a?(Array)
326
+ return :refined if entry != level_class && raw.include?(entry)
327
+
328
+ :defines if RefinedDispatch.defines?(scope, entry, method_name)
329
+ end
330
+
331
+ # Is Ruby known to find `method_name` on `entry`, the first definer the walk reached for an instance of
332
+ # `class_name`? Neither proof stands on a class that rewrites its method table in a way no literal names
333
+ # (`ENVELOPE_DYNAMIC_MARK`), nor for a name the project removes from it with a literal `undef`,
334
+ # `remove_method` or `undef_method` anywhere (`ENVELOPE_REMOVED_KIND`), whatever defined it.
335
+ #
336
+ # - A project `def`, `attr_*` or `define_method` (ADR-119 A7): `DefinerResolution` must answer `Known` with
337
+ # `entry` as the owner, on the receiver's chain, so a definer that may not have run (a `def` under `if`,
338
+ # which Ruby skips, leaving the ancestor's refinement to answer) or that an unplaced mixin or fork may
339
+ # precede is not proof. A `def` whose envelope something made opaque (`remove_method`, `undef`, an
340
+ # alias, a wrapping macro) is not proof either.
341
+ # - A core method the offline CRuby catalogue lists on `entry` itself ({Builtins::CRubyDefiners}), which the
342
+ # project has not removed or rewritten there (A9: no opaque envelope for the name, which `remove_method`
343
+ # and `undef` record). Core RBS is not proof: it redeclares some inherited methods on a subclass
344
+ # (`Integer#quo`, `File#to_path`, owned by `Numeric` and `IO`) and declares some CRuby no longer defines
345
+ # (`Process::Status#&`).
346
+ #
347
+ # ADR-46 — the envelope bucket records the files declaring `entry`, and the class edge a new file reopening
348
+ # 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)
352
+ return false if bucket.key?(Scope::DiscoveryIndex::ENVELOPE_DYNAMIC_MARK)
353
+ return false if bucket.key?([Scope::DiscoveryIndex::ENVELOPE_REMOVED_KIND, method_name.to_sym])
354
+
355
+ opaque = Source::ParameterEnvelope.opaque?(bucket[[:instance, method_name.to_sym]])
356
+ return true if project_definer?(scope, class_name, entry, method_name, opaque)
357
+
358
+ !opaque && Builtins::CRubyDefiners.defines?(entry, method_name)
359
+ end
360
+
361
+ # A `def` counts while its envelope is its own (an alias or a wrapping macro makes it opaque); an `attr_*` or
362
+ # `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
370
+
371
+ certain_owner?(scope, class_name, entry, method_name)
372
+ end
373
+
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,
378
+ &definer_answer(scope, method_name))
379
+ in DefinerResolution::Known(answer: _answer, owner:) then owner == entry
380
+ in DefinerResolution::UNKNOWN then false
381
+ in DefinerResolution::ABSENT then false # rubocop:disable Lint/DuplicateBranch
382
+ end
383
+ end
384
+
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`).
387
+ def definer_answer(scope, method_name)
388
+ lambda do |chain, from|
389
+ index = from - 1
390
+ chain.search(scope, from) do |entry|
391
+ 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)
395
+
396
+ DefinerHit.new(entry.name, entry.name, index, entry.side)
397
+ end
398
+ end
399
+ end
400
+ end
401
+ end
402
+ end
403
+ end
@@ -0,0 +1,272 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module Inference
5
+ # ADR-121 WD2 (issue #1664) — which in-effect refinement, if any, answers a call on an instance of a class. The
6
+ # typed refined arm (`ExpressionTyper#try_refined_dispatch`) asks it before every other answer, because those
7
+ # answer from the method a refinement replaces.
8
+ #
9
+ # Precedence follows `doc/syntax/refinements.rdoc` § Method Lookup: walk the receiver's classes from the most
10
+ # derived. At each class, the latest in-effect module refining the name for that class wins; otherwise its
11
+ # prepended modules, its own method and its included modules answer in that order, and the refinement
12
+ # declines; otherwise move on. A refinement of a module is decided where that module sits in the walk.
13
+ #
14
+ # The walk reads RBS ancestors for a class RBS knows (each ancestor a level of its own, so a core class's
15
+ # prepends are not told apart), and the project's `Scope::ResolutionChain` levels for one only the project
16
+ # declares, splicing in the RBS ancestors of the superclass it does not declare. "Defines the method" is an RBS
17
+ # declaration on that class itself, or a project `def`, `attr_*` or `define_method` of it. Core RBS sometimes
18
+ # redeclares an inherited method on a subclass, which stops the walk there: a known imprecision, in the
19
+ # declining direction.
20
+ #
21
+ # Issue #1740 — {OwnMethod} shares {.levels} with the walk below and asks the opposite question for the two
22
+ # argument checks: does the receiver's own method answer ahead of every refinement in effect?
23
+ module RefinedDispatch
24
+ # The refinement that answers: `module_name` refines `refined_class` (a class or module on the walk).
25
+ Winner = Data.define(:module_name, :refined_class)
26
+
27
+ # The answer when the in-effect list carries {InEffectRefinements::UNKNOWN} and some refinement defines the
28
+ # name on a class the walk reaches: any of them may answer.
29
+ UNKNOWN = :unknown
30
+
31
+ NO_TARGETS = {}.freeze
32
+ private_constant :NO_TARGETS
33
+
34
+ module_function
35
+
36
+ # `Scope::DiscoveryIndex::REFINEMENT_WILDCARD`, read when asked: this file loads before `Scope`.
37
+ def wildcard = Scope::DiscoveryIndex::REFINEMENT_WILDCARD
38
+
39
+ # A {Winner}, {UNKNOWN}, or nil when no in-effect refinement answers `method_name` on an instance of
40
+ # `class_name` (the class's own lookup does). `list` is the call site's in-effect refinements.
41
+ #
42
+ # ADR-121 WD7 — a row the walk could not read never yields a {Winner}: an in-effect module's row for the name
43
+ # whose class the walk could not name reaches every receiver, and a names-wildcard row of a level the walk
44
+ # reaches before a definer may define the name, so both answer {UNKNOWN}. So does a winner whose module shares
45
+ # its last segment with another listed module (A1): which of a `using`'s declared candidates Ruby finds can
46
+ # depend on load order.
47
+ def winner(scope, class_name, method_name, list)
48
+ refinements = scope.discovered_refinements
49
+ return UNKNOWN if class_unknown_row?(scope, refinements, method_name, list)
50
+
51
+ targets = resolved(scope, targets(refinements, method_name, list), method_name)
52
+ unread = targets(refinements, wildcard, list)
53
+ unknown = list.include?(InEffectRefinements::UNKNOWN)
54
+ return nil if targets.nil? && unread.nil? && !unknown
55
+
56
+ levels = levels(scope, class_name)
57
+ return nil if levels.nil?
58
+ return UNKNOWN if unknown && unknown_reaches?(scope, refinements, method_name, levels)
59
+
60
+ walk_targets(scope, levels, method_name, [targets, unread], list)
61
+ end
62
+
63
+ def walk_targets(scope, levels, method_name, (targets, unread), list)
64
+ return nil if targets.nil? && unread.nil?
65
+
66
+ answer = walk(scope, levels, method_name, targets || NO_TARGETS, unread || NO_TARGETS)
67
+ answer.is_a?(Winner) && rival_spelling?(answer.module_name, list) ? UNKNOWN : answer
68
+ end
69
+
70
+ # ADR-121 WD7 (A2) — does an in-effect module have a row for `method_name` (or a names-wildcard row) whose
71
+ # class the walk could not name ({InEffectRefinements.class_wildcard_key?})? It may refine the receiver's class.
72
+ def class_unknown_row?(scope, refinements, method_name, list)
73
+ refinements.any? do |refined, methods|
74
+ rows = [methods[method_name], methods[wildcard]].compact
75
+ !rows.empty? && rows.any? { |modules| in_effect?(modules, list) } &&
76
+ InEffectRefinements.class_wildcard_key?(scope, refined)
77
+ end
78
+ end
79
+
80
+ # Is one of `modules` (a row's refining modules) in `list`? The wildcard module, which the walk could not name,
81
+ # is any listed module.
82
+ def in_effect?(modules, list)
83
+ return list.any? { |name| name != InEffectRefinements::UNKNOWN } if modules.include?(wildcard)
84
+
85
+ modules.any? { |name| list.include?(name) }
86
+ end
87
+
88
+ def rival_spelling?(module_name, list)
89
+ segment = module_name.split("::").last
90
+ list.any? { |other| other.is_a?(String) && other != module_name && other.split("::").last == segment }
91
+ end
92
+
93
+ # `{refined class => the latest in-effect module refining method_name for it}`, or nil when none is in effect.
94
+ def targets(refinements, method_name, list)
95
+ out = nil
96
+ refinements.each do |refined, methods|
97
+ modules = methods[method_name]
98
+ next if modules.nil?
99
+
100
+ best = latest(modules, list)
101
+ (out ||= {})[refined] = best if best
102
+ end
103
+ out
104
+ end
105
+
106
+ # The refinement table records every name a `refine` argument's spelling can denote (`refine String` inside
107
+ # `module M` records `M::String` and `String`), which only withholds a check. To type, a key must be the class
108
+ # Ruby's lexical lookup resolves: one the project or RBS knows, with no longer spelling of the same last segment
109
+ # that the same module refines the name for and that is known too (that one is innermost, so it wins). Nil
110
+ # when no key survives.
111
+ def resolved(scope, targets, method_name)
112
+ return nil if targets.nil?
113
+
114
+ refinements = scope.discovered_refinements
115
+ kept = targets.select do |refined, module_name|
116
+ known_class?(scope, refined) && !shadowed?(scope, refinements, refined, module_name, method_name)
117
+ end
118
+ kept.empty? ? nil : kept
119
+ end
120
+
121
+ def shadowed?(scope, refinements, refined, module_name, method_name)
122
+ suffix = "::#{refined.split('::').last}"
123
+ depth = refined.count(":")
124
+ refinements.any? do |other, methods|
125
+ other.count(":") > depth && other.end_with?(suffix) && methods[method_name]&.include?(module_name) &&
126
+ known_class?(scope, other)
127
+ end
128
+ end
129
+
130
+ def known_class?(scope, name)
131
+ scope.discovered_classes.key?(name) || scope.environment&.class_known?(name) || false
132
+ end
133
+
134
+ def latest(modules, list)
135
+ best = nil
136
+ best_index = -1
137
+ modules.each do |module_name|
138
+ index = list.index(module_name)
139
+ next unless index && index > best_index
140
+
141
+ best = module_name
142
+ best_index = index
143
+ end
144
+ best
145
+ end
146
+
147
+ # ADR-121 WD7 — a names-wildcard row counts as a row for every name, and a row whose class the walk could not
148
+ # name reaches every level.
149
+ def unknown_reaches?(scope, refinements, method_name, levels)
150
+ unread_class = refinements.any? do |refined, methods|
151
+ (methods.key?(method_name) || methods.key?(wildcard)) &&
152
+ InEffectRefinements.class_wildcard_key?(scope, refined)
153
+ end
154
+ return true if unread_class
155
+
156
+ levels.any? do |_level_class, entries|
157
+ entries.any? do |entry|
158
+ methods = refinements[entry]
159
+ !methods.nil? && (methods.key?(method_name) || methods.key?(wildcard))
160
+ end
161
+ end
162
+ end
163
+
164
+ # A level the project mixes a module into whose position RBS does not record (`mixed`) is decided by its own
165
+ # refinement first, as any level is; past that, the mixin may define the name, so a refinement further up the
166
+ # walk may or may not win and the answer is {UNKNOWN}, never the replaced method's. ADR-121 WD7: a level an
167
+ # in-effect module has a names-wildcard row for (`unread`) may define the name too, so it answers {UNKNOWN}.
168
+ def walk(scope, levels, method_name, targets, unread)
169
+ levels.each_with_index do |(level_class, entries, mixed), index|
170
+ return UNKNOWN if level_class && unread.key?(level_class)
171
+
172
+ refining = level_class && targets[level_class]
173
+ return Winner.new(module_name: refining, refined_class: level_class) if refining
174
+ return targeted_after?(levels, index, targets, unread) ? UNKNOWN : nil if mixed
175
+
176
+ answer = walk_entries(scope, entries, level_class, method_name, targets, unread)
177
+ return answer unless answer == :continue
178
+ end
179
+ nil
180
+ end
181
+
182
+ # A level's prepended modules, its own method and its included modules, in that order: `:continue` when none
183
+ # answers, so the walk moves on.
184
+ def walk_entries(scope, entries, level_class, method_name, targets, unread)
185
+ entries.each do |entry|
186
+ unless entry == level_class
187
+ return UNKNOWN if unread.key?(entry)
188
+
189
+ refining = targets[entry]
190
+ return Winner.new(module_name: refining, refined_class: entry) if refining
191
+ end
192
+ return nil if defines?(scope, entry, method_name)
193
+ end
194
+ :continue
195
+ end
196
+
197
+ def targeted_after?(levels, index, targets, unread)
198
+ levels.drop(index).any? do |_level_class, entries|
199
+ entries.any? { |entry| targets.key?(entry) || unread.key?(entry) }
200
+ end
201
+ end
202
+
203
+ # `[[level class, [entries in lookup order], mixed, every entry], …]` for an instance of `class_name`, or nil
204
+ # when the walk cannot be trusted: a chain cut at its limit, or one that records a mixin the tables cannot
205
+ # name. `every entry` (project levels only) is the level's entries in lookup order with each one that
206
+ # neither the project's resolver nor RBS can name kept as its candidate names (an Array) where `entries`
207
+ # leaves it out: the typer never reads it, and {OwnMethod} declines where a refinement may name one.
208
+ def levels(scope, class_name)
209
+ environment = scope.environment
210
+ return rbs_levels(scope, class_name) if environment&.class_known?(class_name)
211
+
212
+ project_levels(scope, class_name)
213
+ end
214
+
215
+ # The RBS ancestors, each a level of its own, `mixed` where the project reopens one to mix a module in (`class
216
+ # String; include Loud; end`), whose position among them RBS does not record. Each ancestor's name is a
217
+ # dependency, so a file that adds such a reopening re-checks the consumer.
218
+ def rbs_levels(scope, class_name)
219
+ loader = scope.environment.rbs_loader
220
+ names = loader ? loader.ancestor_names_for(class_name) : []
221
+ names = [class_name] if names.empty?
222
+ names.map { |name| [name, [name], project_mixin?(scope, name)] }
223
+ end
224
+
225
+ def project_mixin?(scope, name)
226
+ Analysis::DependencyRecorder.read_last_segment(:class, name) if Analysis::DependencyRecorder.active?
227
+ !scope.includes_of(name).empty? || scope.discovery.discovered_prepends.key?(name) ||
228
+ (scope.discovered_classes.key?(name) &&
229
+ Scope::ResolutionChain.for(scope, name, :instance, :methods).wildcard_mixin?)
230
+ end
231
+
232
+ def project_levels(scope, class_name)
233
+ chain = Scope::ResolutionChain.for(scope, class_name, :instance, :methods)
234
+ chain.record(scope) if Analysis::DependencyRecorder.active?
235
+ return nil if chain.truncated? || chain.wildcard_mixin?
236
+
237
+ out = []
238
+ chain.level_count.times do |index|
239
+ level_class = chain.level_classes[index]
240
+ every = chain.level_entries(index).map { |entry| entry_name(scope, entry) || unnamed(entry) }
241
+ names = every.grep(String)
242
+ next out << [level_class, names, false, every] if level_class
243
+
244
+ names.each { |name| out.concat(rbs_levels(scope, name)) }
245
+ end
246
+ out
247
+ end
248
+
249
+ def unnamed(entry) = entry.candidates.empty? ? [entry.raw.to_s] : entry.candidates
250
+
251
+ def entry_name(scope, entry)
252
+ return entry.name unless entry.external?
253
+
254
+ environment = scope.environment
255
+ entry.candidates.find { |candidate| environment&.class_known?(candidate) }
256
+ end
257
+
258
+ # Does `name` define `method_name` on its instance side itself: a project `def` (its dependency edge recorded
259
+ # through `Scope#user_def_for`), another project definer (`attr_*`, `define_method`), or an RBS declaration
260
+ # written on `name` rather than on an ancestor?
261
+ def defines?(scope, name, method_name)
262
+ return true if scope.user_def_for(name, method_name)
263
+ return true if scope.discovered_method?(name, method_name, :instance)
264
+
265
+ definition = ExternalAncestorResolution.method_definition(name, method_name, :instance, scope: scope)
266
+ ExternalAncestorResolution.declared_on_class?(definition, name)
267
+ end
268
+ end
269
+ end
270
+ end
271
+
272
+ require_relative "refined_dispatch/own_method"