rigortype 0.4.1 → 0.4.3

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 (230) 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 +18 -5
  25. data/docs/handbook/appendix-liskov.md +5 -0
  26. data/docs/manual/02-cli-reference.md +19 -1
  27. data/docs/manual/03-configuration.md +7 -5
  28. data/docs/manual/04-diagnostics.md +2 -1
  29. data/docs/manual/05-inspecting-types.md +50 -0
  30. data/docs/manual/06-baseline.md +2 -0
  31. data/docs/manual/07-plugins.md +1 -1
  32. data/docs/manual/12-caching.md +18 -1
  33. data/docs/manual/14-rails-quickstart.md +2 -2
  34. data/docs/manual/18-removing-dead-code.md +41 -4
  35. data/docs/manual/plugins/README.md +6 -1
  36. data/docs/manual/plugins/rigor-activerecord.md +21 -0
  37. data/docs/manual/plugins/rigor-alba.md +60 -0
  38. data/docs/manual/plugins/rigor-minitest.md +22 -4
  39. data/docs/manual/plugins/rigor-typelizer.md +71 -0
  40. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
  41. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
  42. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
  43. data/lib/rigor/analysis/check_rules/rule_ids.rb +11 -0
  44. data/lib/rigor/analysis/check_rules/source_arity.rb +21 -2
  45. data/lib/rigor/analysis/check_rules.rb +292 -59
  46. data/lib/rigor/analysis/dependency_recorder.rb +15 -0
  47. data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
  48. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
  49. data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
  50. data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
  51. data/lib/rigor/analysis/effects_cache_probe.rb +1 -1
  52. data/lib/rigor/analysis/erb_template_detector.rb +2 -1
  53. data/lib/rigor/analysis/incremental.rb +29 -9
  54. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +30 -5
  55. data/lib/rigor/analysis/incremental_run_slot.rb +6 -4
  56. data/lib/rigor/analysis/incremental_session.rb +103 -23
  57. data/lib/rigor/analysis/path_expansion.rb +62 -6
  58. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +124 -37
  59. data/lib/rigor/analysis/reachability/graph.rb +241 -37
  60. data/lib/rigor/analysis/reachability/scan.rb +490 -77
  61. data/lib/rigor/analysis/reachability/scan_cache.rb +4 -2
  62. data/lib/rigor/analysis/rule_catalog.rb +43 -0
  63. data/lib/rigor/analysis/run_cache_key.rb +16 -1
  64. data/lib/rigor/analysis/run_cache_probe.rb +1 -1
  65. data/lib/rigor/analysis/run_stats.rb +28 -6
  66. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +113 -6
  67. data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -19
  68. data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -1
  69. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  70. data/lib/rigor/analysis/runner.rb +62 -13
  71. data/lib/rigor/analysis/worker_session.rb +8 -5
  72. data/lib/rigor/cache/engine_source.rb +21 -7
  73. data/lib/rigor/cache/incremental_snapshot.rb +55 -5
  74. data/lib/rigor/cli/annotate_command.rb +5 -0
  75. data/lib/rigor/cli/baseline_command.rb +7 -18
  76. data/lib/rigor/cli/check_command.rb +140 -40
  77. data/lib/rigor/cli/docs_command.rb +5 -4
  78. data/lib/rigor/cli/lens_command.rb +144 -0
  79. data/lib/rigor/cli/lsp_command.rb +5 -2
  80. data/lib/rigor/cli/skill_command.rb +3 -3
  81. data/lib/rigor/cli/skill_describe.rb +1 -1
  82. data/lib/rigor/cli/trace_command.rb +5 -0
  83. data/lib/rigor/cli/type_of_command.rb +6 -3
  84. data/lib/rigor/cli/unused_command.rb +166 -27
  85. data/lib/rigor/cli.rb +39 -5
  86. data/lib/rigor/configuration/severity_profile.rb +6 -0
  87. data/lib/rigor/configuration.rb +61 -2
  88. data/lib/rigor/environment/bundle_sig_discovery.rb +11 -7
  89. data/lib/rigor/environment/gated_signature_guard.rb +43 -0
  90. data/lib/rigor/environment/gated_signature_plan.rb +339 -0
  91. data/lib/rigor/environment/installed_gem_set.rb +38 -11
  92. data/lib/rigor/environment/lockfile_resolver.rb +37 -6
  93. data/lib/rigor/environment/missing_gem_constant_index.rb +22 -7
  94. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  95. data/lib/rigor/environment/rbs_loader.rb +316 -14
  96. data/lib/rigor/environment/required_features.rb +208 -0
  97. data/lib/rigor/environment.rb +33 -8
  98. data/lib/rigor/inference/acceptance.rb +28 -3
  99. data/lib/rigor/inference/builtins/cruby_definers.rb +78 -0
  100. data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
  101. data/lib/rigor/inference/captured_locals.rb +43 -3
  102. data/lib/rigor/inference/def_node_resolver.rb +23 -1
  103. data/lib/rigor/inference/expression_typer.rb +261 -29
  104. data/lib/rigor/inference/global_write_census.rb +7 -4
  105. data/lib/rigor/inference/guard_rebinding.rb +1 -1
  106. data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
  107. data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
  108. data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +286 -0
  109. data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
  110. data/lib/rigor/inference/in_effect_refinements.rb +256 -0
  111. data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
  112. data/lib/rigor/inference/key_presence_guard.rb +303 -0
  113. data/lib/rigor/inference/macro_block_self_type.rb +59 -12
  114. data/lib/rigor/inference/match_rebinding.rb +1 -1
  115. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  116. data/lib/rigor/inference/method_dispatcher/argument_nodes.rb +25 -0
  117. data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
  118. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
  119. data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
  120. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
  121. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +3 -2
  122. data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
  123. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +2 -2
  124. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +3 -3
  125. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
  126. data/lib/rigor/inference/method_dispatcher/optional_count.rb +63 -0
  127. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
  128. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +247 -28
  129. data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
  130. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -7
  131. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +8 -26
  132. data/lib/rigor/inference/method_dispatcher/unit_denominator_rational.rb +67 -0
  133. data/lib/rigor/inference/method_dispatcher.rb +96 -14
  134. data/lib/rigor/inference/module_function_state.rb +2 -1
  135. data/lib/rigor/inference/narrowing.rb +20 -6
  136. data/lib/rigor/inference/object_mixins.rb +180 -0
  137. data/lib/rigor/inference/project_patched_definedness_guard.rb +86 -0
  138. data/lib/rigor/inference/project_patched_methods.rb +12 -7
  139. data/lib/rigor/inference/project_patched_scanner.rb +108 -26
  140. data/lib/rigor/inference/refine_census.rb +239 -0
  141. data/lib/rigor/inference/refine_self.rb +317 -0
  142. data/lib/rigor/inference/refined_dispatch/class_object.rb +350 -0
  143. data/lib/rigor/inference/refined_dispatch/own_method.rb +475 -0
  144. data/lib/rigor/inference/refined_dispatch.rb +320 -0
  145. data/lib/rigor/inference/repeated_or_writes.rb +2 -0
  146. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +13 -0
  147. data/lib/rigor/inference/scope_indexer.rb +827 -65
  148. data/lib/rigor/inference/statement_evaluator.rb +294 -51
  149. data/lib/rigor/inference/string_mutation.rb +9 -3
  150. data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
  151. data/lib/rigor/inference/untyped_rejoin.rb +75 -0
  152. data/lib/rigor/inference/version_guard.rb +19 -10
  153. data/lib/rigor/language_server/project_context.rb +87 -5
  154. data/lib/rigor/language_server/server.rb +18 -0
  155. data/lib/rigor/lens/anchor.rb +115 -0
  156. data/lib/rigor/lens/contract_reader.rb +118 -0
  157. data/lib/rigor/lens/document.rb +113 -0
  158. data/lib/rigor/lens/file_reader.rb +433 -0
  159. data/lib/rigor/lens/members.rb +153 -0
  160. data/lib/rigor/lens/parameters.rb +91 -0
  161. data/lib/rigor/lens/plugins.rb +40 -0
  162. data/lib/rigor/lens/run.rb +280 -0
  163. data/lib/rigor/lens/slot.rb +79 -0
  164. data/lib/rigor/lens.rb +40 -0
  165. data/lib/rigor/plugin/inflector.rb +1 -1
  166. data/lib/rigor/plugin/io_boundary.rb +42 -3
  167. data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
  168. data/lib/rigor/project_environment.rb +48 -6
  169. data/lib/rigor/protection/mutation_cache.rb +3 -2
  170. data/lib/rigor/protection/mutator.rb +1 -1
  171. data/lib/rigor/reflection/constant_ancestors.rb +127 -11
  172. data/lib/rigor/reflection/constant_path.rb +13 -2
  173. data/lib/rigor/reflection.rb +40 -8
  174. data/lib/rigor/scope/discovery_index.rb +40 -1
  175. data/lib/rigor/scope/resolution_chain.rb +159 -11
  176. data/lib/rigor/scope.rb +211 -21
  177. data/lib/rigor/sig_gen/classification.rb +4 -1
  178. data/lib/rigor/sig_gen/generator.rb +100 -10
  179. data/lib/rigor/sig_gen/method_candidate.rb +2 -2
  180. data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
  181. data/lib/rigor/source/alias_names.rb +52 -0
  182. data/lib/rigor/source/literals.rb +3 -2
  183. data/lib/rigor/source/symbol_name.rb +20 -0
  184. data/lib/rigor/type/combinator.rb +34 -2
  185. data/lib/rigor/type/float_range.rb +4 -1
  186. data/lib/rigor/type/nominal.rb +6 -1
  187. data/lib/rigor/version.rb +1 -1
  188. data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
  189. data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
  190. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
  191. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
  192. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
  193. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
  194. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
  195. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
  196. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
  197. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
  198. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +29 -3
  199. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +5 -1
  200. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
  201. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
  202. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
  203. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
  204. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
  205. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
  206. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
  207. data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
  208. data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
  209. data/plugins/rigor-alba/sig/alba.rbs +4 -0
  210. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
  211. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
  212. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +7 -6
  213. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
  214. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
  215. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
  216. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
  217. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
  218. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
  219. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
  220. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
  221. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
  222. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
  223. data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
  224. data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
  225. data/sig/rigor/plugin/io_boundary.rbs +3 -0
  226. data/sig/rigor/scope.rbs +30 -4
  227. data/sig/rigor/source.rbs +4 -0
  228. data/sig/rigor.rbs +9 -4
  229. data/skills/rigor-project-init/references/02-configure.md +2 -2
  230. metadata +83 -1
@@ -0,0 +1,317 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../source/constant_path"
6
+ require_relative "refine_census"
7
+
8
+ module Rigor
9
+ module Inference
10
+ # ADR-121 WD7 — the one function that answers what `self` a `refine` (or a `:refine` literal) at a program point
11
+ # runs on. The in-effect walk ({InEffectRefinements}) works a {Context} out with these functions for every body,
12
+ # method and block it enters and records it for each `refine`-shaped node; the census (`ScopeIndexer`'s module-body
13
+ # walk and its scan of method and refine bodies) reads that record back ({InEffectRefinements#refine_context}), so
14
+ # the table and the activations charge the same module.
15
+ module RefineSelf
16
+ # `here`, for a `refine` call directly at that point; `literal`, for a `:refine` literal there (which names
17
+ # `Module#refine` for an alias or a `send` whose method runs where a method defined there does: in a module's
18
+ # `class << self` body that is the module, though a `refine` call there runs on the singleton class); and
19
+ # `instance` / `singleton`, for a method defined there on the instance / singleton side. Each is a module name,
20
+ # the names a constant's spelling can denote (an Array, innermost first, for a module only another file may
21
+ # declare), the wildcard (a module the walk cannot name), {CLASS}, {MAIN}, {ANONYMOUS} or {REFINEMENT}.
22
+ # `deferred` is true inside a method body (a `def`, a `define_method` block), where a `refine` runs only once the
23
+ # method is called.
24
+ # `own_method` is true inside a singleton `def` of a module the walk names ({.def_body}), and in the blocks that
25
+ # keep its `self`: `here` is then that module itself, so a `refine X do … end` written there refines X for it
26
+ # once the method is called, and the census records its rows where it would otherwise charge a wildcard.
27
+ # `remote` is true in the body of an eval on a constant (`M.module_eval { … }`, and a `class << self` there),
28
+ # whose file need not declare the module it runs on: a singleton method defined there is not `own_method`.
29
+ Context = Data.define(:here, :literal, :instance, :singleton, :deferred, :own_method, :remote)
30
+ # A plain class, or an instance of one: `Class` undefines `refine`, so a `refine` there is the class's own
31
+ # method (#1689) and a `:refine` literal is data.
32
+ CLASS = :class
33
+ # The top level's `main`, which has no `refine`.
34
+ MAIN = :main
35
+ # A module a bare `Module.new { … }` creates, which the census names by its position.
36
+ ANONYMOUS = :anonymous
37
+ # The refinement a `refine` block creates, where a method defined on its singleton side (`def self.x`, a
38
+ # `define_singleton_method` block, a `class << self` body) runs. `using` rejects a refinement with `TypeError`,
39
+ # so a `refine` there reaches no `using` span and the census charges it to nothing.
40
+ REFINEMENT = :refinement
41
+
42
+ # Core iteration methods: the Enumerable protocol never rebinds `self` in their block.
43
+ CORE_ITERATORS = Set[
44
+ :each, :each_with_index, :each_with_object, :each_pair, :each_key, :each_value, :map, :flat_map, :times,
45
+ :select, :reject
46
+ ].freeze
47
+ LITERALS = [Prism::ArrayNode, Prism::HashNode, Prism::RangeNode, Prism::IntegerNode, Prism::FloatNode,
48
+ Prism::StringNode, Prism::InterpolatedStringNode, Prism::SymbolNode].freeze
49
+ NAMED_RECEIVERS = [Prism::ConstantReadNode, Prism::ConstantPathNode, Prism::LocalVariableReadNode,
50
+ Prism::InstanceVariableReadNode].freeze
51
+ METHOD_BLOCK_CALLS = Set[:define_method, :define_singleton_method].freeze
52
+ INSTANCE_EVAL_CALLS = Set[:instance_eval, :instance_exec].freeze
53
+ # Bindings of a local or an instance variable that are not a plain write: each may hold any value.
54
+ VARIABLE_BINDINGS = [
55
+ Prism::LocalVariableOperatorWriteNode, Prism::LocalVariableOrWriteNode, Prism::LocalVariableAndWriteNode,
56
+ Prism::LocalVariableTargetNode, Prism::RequiredParameterNode, Prism::OptionalParameterNode,
57
+ Prism::RestParameterNode, Prism::KeywordRestParameterNode, Prism::RequiredKeywordParameterNode,
58
+ Prism::OptionalKeywordParameterNode, Prism::BlockParameterNode, Prism::BlockLocalVariableNode,
59
+ Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableOrWriteNode,
60
+ Prism::InstanceVariableAndWriteNode, Prism::InstanceVariableTargetNode
61
+ ].freeze
62
+ # Bindings of a constant other than a bare `K = …`, matched by the constant's last segment.
63
+ CONSTANT_BINDINGS = [
64
+ Prism::ConstantOrWriteNode, Prism::ConstantOperatorWriteNode, Prism::ConstantAndWriteNode,
65
+ Prism::ConstantTargetNode, Prism::ConstantPathTargetNode
66
+ ].freeze
67
+ CONSTANT_PATH_BINDINGS = [
68
+ Prism::ConstantPathWriteNode, Prism::ConstantPathOrWriteNode, Prism::ConstantPathOperatorWriteNode,
69
+ Prism::ConstantPathAndWriteNode
70
+ ].freeze
71
+
72
+ private_constant :CORE_ITERATORS, :LITERALS, :NAMED_RECEIVERS, :METHOD_BLOCK_CALLS, :INSTANCE_EVAL_CALLS,
73
+ :VARIABLE_BINDINGS, :CONSTANT_BINDINGS, :CONSTANT_PATH_BINDINGS
74
+
75
+ module_function
76
+
77
+ def wildcard = RefineCensus.wildcard
78
+
79
+ # The top level: `main`; a method defined there is `Object`'s, which any module may run.
80
+ def top = context(MAIN, wildcard, MAIN)
81
+
82
+ # A body whose `self` the walk cannot name.
83
+ def unknown = context(wildcard, wildcard, wildcard)
84
+
85
+ def context(here, instance, singleton, deferred: false, literal: here, own_method: false, remote: false)
86
+ Context.new(here: here, literal: literal, instance: instance, singleton: singleton, deferred: deferred,
87
+ own_method: own_method, remote: remote)
88
+ end
89
+
90
+ # A `module` body (or a `Module.new` block) of the module `name` (nil when the walk cannot name it, or
91
+ # {ANONYMOUS}): its instance methods run on whatever includes or extends it, its singleton methods on it.
92
+ def module_body(name)
93
+ return unknown if name.nil?
94
+
95
+ context(name, wildcard, name)
96
+ end
97
+
98
+ # A `class` body (or a `Class.new` / `Struct.new` / `Data.define` block): `self` is a class; a `Module`
99
+ # subclass's instances are modules the walk cannot name.
100
+ def class_body(module_subclass) = context(CLASS, module_subclass ? wildcard : CLASS, CLASS)
101
+
102
+ # A `class << expr` body: `self` is a singleton class, so a `refine` call there is its own method; for
103
+ # `class << self` its methods (and a `:refine` literal an alias there names) run on the enclosing `self`, for any
104
+ # other expression on an object the walk cannot name.
105
+ def singleton_class_body(parent, of_self)
106
+ runs_on = of_self ? parent.singleton : wildcard
107
+ context(CLASS, runs_on, CLASS, literal: runs_on, remote: of_self && parent.remote)
108
+ end
109
+
110
+ # A method body defined in `parent` on the instance or singleton side: `self` is what that side runs on, and a
111
+ # method defined inside it is defined on something the walk cannot name.
112
+ def method_body(parent, singleton)
113
+ context(singleton ? parent.singleton : parent.instance, wildcard, wildcard, deferred: true)
114
+ end
115
+
116
+ # A `def` body in `parent`: on no receiver an instance method, on `self` (or on the constant that is the last
117
+ # segment of the module `parent` names, `def M.x` inside `module M`) a singleton one, and on any other object a
118
+ # method defined on something the walk cannot name. A singleton method of a module the walk names is the
119
+ # module's own (`own_method`): `def self.x`, or a `def` written in that module's `class << self`
120
+ # (`parent.instance` is the module there). `self` is then the module itself, whichever way the method is called,
121
+ # so a `refine` in it refines for that module; for an instance `def`, whose `parent.instance` is the wildcard,
122
+ # Ruby runs it on whatever module extends the owner. `def M.x` is not marked, as the constant names the module by
123
+ # its last segment only, and neither is a method an eval on a constant defines (`remote`): its file need not
124
+ # declare the module, so the lookup of a refine body, which consults the files that do, would not see it.
125
+ def def_body(parent, receiver)
126
+ return mark_own_method(method_body(parent, false), parent) if receiver.nil?
127
+ return unknown.with(deferred: true) unless lexical_self_receiver?(receiver, parent)
128
+
129
+ context = method_body(parent, true)
130
+ receiver.is_a?(Prism::SelfNode) ? mark_own_method(context, parent) : context
131
+ end
132
+
133
+ def mark_own_method(context, parent)
134
+ named_module?(context.here) && !parent.remote ? context.with(own_method: true) : context
135
+ end
136
+
137
+ def lexical_self_receiver?(receiver, context)
138
+ return true if receiver.is_a?(Prism::SelfNode)
139
+
140
+ here = context.here
141
+ receiver.is_a?(Prism::ConstantReadNode) && here.is_a?(String) && here.split("::").last == receiver.name.to_s
142
+ end
143
+
144
+ # A module name the walk can charge a `refine` to: not the wildcard, {CLASS}, {MAIN} or {ANONYMOUS}, and not the
145
+ # several names a constant's spelling can denote.
146
+ def named_module?(here) = here.is_a?(String) && here != wildcard
147
+
148
+ # The context inside the block of `node` (a block-carrying call, or a lambda) written in `parent`. `eval_names`
149
+ # are the modules a `*_eval` / `*_exec` call's constant receiver may name ({.eval_receiver_names}), or nil; the
150
+ # given block answers whether a constant, local or instance variable receiver holds a literal ({.keeps_self?}).
151
+ # A block keeps `self` only where its call is known to: a core iteration method on a receiver whose value is a
152
+ # literal in this file, and a `refine` call (its body is the refinement, handled by the reader). A `*_eval` /
153
+ # `*_exec` block runs on its receiver, a `define_method` / `define_singleton_method` block is a method body, and
154
+ # a `Module.new` / `Class.new` block is a module / class body. Any other block (a DSL may `module_eval` it,
155
+ # `tap`, `loop`, a project method, a core method's name on any other receiver), a lambda, and every block at the
156
+ # top level may run under any `self`.
157
+ def block(parent, node, eval_names, &)
158
+ return unknown if node.is_a?(Prism::LambdaNode)
159
+ return refinement_body(parent) if node.name == :refine
160
+ return eval_body(parent, node, eval_names) if RefineCensus::SELF_EVAL_CALLS.include?(node.name)
161
+ return meta_new_body(node) if ScopeIndexer.meta_new_constant_rvalue?(node)
162
+ return unknown if parent.here == MAIN
163
+ return method_body(parent, node.name == :define_singleton_method) if method_block?(node)
164
+ return parent if keeps_self?(node, &)
165
+
166
+ unknown
167
+ end
168
+
169
+ def method_block?(node) = METHOD_BLOCK_CALLS.include?(node.name) && RefineCensus.self_call?(node)
170
+
171
+ # The block of a `refine` call: a `refine` written in it (a nested one) is charged where the call is, but `self`
172
+ # is the refinement, so a method defined in it is defined on something the walk cannot name: a singleton method
173
+ # on the refinement itself ({REFINEMENT}, whichever the method the `refine` call sits in, as long as the module it
174
+ # refines for is one), an instance method on an instance of the refined class (the wildcard), not on the module
175
+ # around.
176
+ def refinement_body(parent)
177
+ singleton = module_here?(parent.here) ? REFINEMENT : parent.singleton
178
+ parent.with(instance: refinement_side(parent.instance), singleton: singleton, own_method: false)
179
+ end
180
+
181
+ def refinement_side(side) = side.is_a?(String) || side.is_a?(Array) ? wildcard : side
182
+
183
+ # Is `here` a module a `refine` there refines for (so its block is a refinement's body), or the refinement itself?
184
+ def module_here?(here) = named_module?(here) || here.is_a?(Array) || here == ANONYMOUS || here == REFINEMENT
185
+
186
+ def meta_new_body(node)
187
+ ScopeIndexer.module_new_call?(node) ? module_body(ANONYMOUS) : class_body(false)
188
+ end
189
+
190
+ # A core iteration method on a receiver whose value is a literal in this file: a literal, or a constant, local
191
+ # or instance variable the block answers true for ({.literal_bindings}). Any other name, even on a literal, and
192
+ # any other receiver may be a project method that runs the block under another `self`: on Ruby 4.0.5
193
+ # `Config.each { refine(String) { … } }` with `def self.each(&blk) = Ext.module_eval(&blk)` refines for `Ext`,
194
+ # and so does `[1].configure { … }` with `class Array; def configure(&blk) = Ext.module_eval(&blk); end`.
195
+ def keeps_self?(node)
196
+ return false unless CORE_ITERATORS.include?(node.name)
197
+
198
+ receiver = node.receiver
199
+ return true if literal?(receiver)
200
+
201
+ NAMED_RECEIVERS.any? { |klass| receiver.is_a?(klass) } && block_given? && yield(receiver)
202
+ end
203
+
204
+ def literal?(node) = LITERALS.any? { |klass| node.is_a?(klass) }
205
+
206
+ # A literal, or a literal's `.freeze`.
207
+ def literal_value?(node)
208
+ return true if literal?(node)
209
+
210
+ node.is_a?(Prism::CallNode) && node.name == :freeze && node.arguments.nil? && node.block.nil? &&
211
+ literal?(node.receiver)
212
+ end
213
+
214
+ # The constants (by qualified name), locals and instance variables (by name) that `root` binds, every binding a
215
+ # plain write of a literal ({.literal_value?}). A name any other binding gives a value (a parameter, a
216
+ # multiple assignment, `||=`, `+=`, a `const_set`, a path write `A::K = …`) is not one, wherever in the file
217
+ # it sits.
218
+ def literal_bindings(root)
219
+ literal = Set.new
220
+ other = Set.new
221
+ spoiled = ScopeIndexer.loose_constant_segments(root).dup
222
+ collect_bindings(root, [], literal, other, spoiled)
223
+ (literal - other).reject { |key| key.is_a?(String) && spoiled.include?(key.split("::").last) }.to_set.freeze
224
+ end
225
+
226
+ def collect_bindings(node, prefix, literal, other, spoiled)
227
+ case node
228
+ when Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode
229
+ return collect_declaration_bindings(node, prefix, literal, other, spoiled)
230
+ when Prism::LocalVariableWriteNode, Prism::InstanceVariableWriteNode
231
+ bind(node.value, node.name, literal, other)
232
+ when *VARIABLE_BINDINGS then other << node.name if node.name
233
+ else collect_constant_binding(node, prefix, literal, other, spoiled)
234
+ end
235
+ node.rigor_each_child { |child| collect_bindings(child, prefix, literal, other, spoiled) }
236
+ end
237
+
238
+ def collect_constant_binding(node, prefix, literal, other, spoiled)
239
+ case node
240
+ when Prism::ConstantWriteNode then bind(node.value, (prefix + [node.name.to_s]).join("::"), literal, other)
241
+ when *CONSTANT_BINDINGS then spoiled << node.name.to_s
242
+ when *CONSTANT_PATH_BINDINGS then spoiled << node.target.name.to_s
243
+ end
244
+ end
245
+
246
+ # A `class` / `module` body's constants are its own; a `class << expr` body's are the singleton class's, which
247
+ # no read outside it names.
248
+ def collect_declaration_bindings(node, prefix, literal, other, spoiled)
249
+ if node.is_a?(Prism::SingletonClassNode)
250
+ collect_bindings(node.expression, prefix, literal, other, spoiled)
251
+ inner = prefix + ["<singleton>"]
252
+ else
253
+ inner = Source::ConstantPath.declaration_prefix(prefix, node.constant_path) || prefix
254
+ superclass = node.is_a?(Prism::ClassNode) && node.superclass
255
+ collect_bindings(superclass, prefix, literal, other, spoiled) if superclass
256
+ end
257
+ collect_bindings(node.body, inner, literal, other, spoiled) if node.body
258
+ end
259
+
260
+ def bind(value, key, literal, other) = (literal_value?(value) ? literal : other) << key
261
+
262
+ # The key {.literal_bindings} holds for a receiver read in `prefix`: a constant's innermost lexical candidate (the
263
+ # one Ruby's lookup finds once this file's write defines it), a local's or an instance variable's name.
264
+ def binding_key(receiver, prefix)
265
+ case receiver
266
+ when Prism::ConstantReadNode, Prism::ConstantPathNode
267
+ ScopeIndexer.constant_receiver_candidates(receiver, prefix).first
268
+ else receiver.name
269
+ end
270
+ end
271
+
272
+ # A `*_eval` / `*_exec` block: on `self` (or no receiver) it keeps `self`, on a constant it runs on the module the
273
+ # constant names (any name its spelling can denote), on anything else on a module the walk cannot name. A `def` in
274
+ # an `instance_eval` binds on the receiver's singleton, so its instance side is the receiver itself.
275
+ def eval_body(parent, node, eval_names)
276
+ here = RefineCensus.self_call?(node) ? parent.here : eval_names
277
+ here = here.first if here.is_a?(Array) && here.size == 1
278
+ return context(CLASS, CLASS, CLASS, deferred: parent.deferred) if here == CLASS
279
+ return unknown unless here.is_a?(String) || here.is_a?(Array) || here == ANONYMOUS || here == REFINEMENT
280
+ return unknown if here == wildcard
281
+
282
+ instance = INSTANCE_EVAL_CALLS.include?(node.name) ? here : wildcard
283
+ remote = parent.remote || !RefineCensus.self_call?(node)
284
+ context(here, instance, here, deferred: parent.deferred, remote: remote)
285
+ end
286
+
287
+ # The modules a `*_eval` / `*_exec` call `node` written in `context` at the lexical `prefix` may run its block
288
+ # on, when its receiver is a constant: every name the constant's spelling can denote, innermost first, as another
289
+ # file may declare any of them (`M.module_eval { … }` in `module L` runs on `L::M` where some file declares it);
290
+ # a `self::`-anchored one as the method walk resolves it (`ScopeIndexer.eval_receiver_prefix`). Else nil.
291
+ def eval_receiver_names(node, prefix, context)
292
+ return nil unless RefineCensus::SELF_EVAL_CALLS.include?(node.name) && !RefineCensus.self_call?(node)
293
+
294
+ receiver = node.receiver
295
+ if Source::ConstantPath.qualified_name_or_nil(receiver)
296
+ return ScopeIndexer.constant_receiver_candidates(receiver, prefix)
297
+ end
298
+
299
+ here = context.here
300
+ self_prefix = here.is_a?(String) && here != wildcard ? here.split("::") : []
301
+ resolved = ScopeIndexer.eval_receiver_prefix(node, self_prefix, prefix, unnameable_self: self_prefix.empty?)
302
+ [resolved.join("::")] unless resolved.nil? || resolved.empty?
303
+ end
304
+
305
+ # The modules a context's `refine` call (or, with `literal`, its `:refine` literal) may refine for, as names a
306
+ # table row can hold: the module, each name a constant's spelling can denote, else the wildcard.
307
+ def charged_modules(context, literal: false)
308
+ here = literal ? context.literal : context.here
309
+ case here
310
+ when String then [here]
311
+ when Array then here
312
+ else [wildcard]
313
+ end
314
+ end
315
+ end
316
+ end
317
+ end
@@ -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