rigortype 0.4.0 → 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 (237) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -3
  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/08-understanding-errors.md +3 -2
  25. data/docs/handbook/11-sig-gen.md +6 -0
  26. data/docs/handbook/appendix-liskov.md +5 -0
  27. data/docs/install.md +87 -15
  28. data/docs/manual/02-cli-reference.md +2 -2
  29. data/docs/manual/03-configuration.md +6 -1
  30. data/docs/manual/04-diagnostics.md +2 -1
  31. data/docs/manual/06-baseline.md +2 -0
  32. data/docs/manual/07-plugins.md +1 -1
  33. data/docs/manual/08-skills.md +4 -3
  34. data/docs/manual/12-caching.md +43 -4
  35. data/docs/manual/14-rails-quickstart.md +19 -11
  36. data/docs/manual/18-removing-dead-code.md +32 -2
  37. data/docs/manual/plugins/README.md +6 -1
  38. data/docs/manual/plugins/rigor-activerecord.md +21 -0
  39. data/docs/manual/plugins/rigor-alba.md +60 -0
  40. data/docs/manual/plugins/rigor-minitest.md +22 -4
  41. data/docs/manual/plugins/rigor-typelizer.md +71 -0
  42. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
  43. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
  44. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
  45. data/lib/rigor/analysis/check_rules/rule_ids.rb +4 -0
  46. data/lib/rigor/analysis/check_rules/source_arity.rb +184 -61
  47. data/lib/rigor/analysis/check_rules.rb +407 -144
  48. data/lib/rigor/analysis/dependency_recorder.rb +215 -18
  49. data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
  50. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
  51. data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
  52. data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
  53. data/lib/rigor/analysis/erb_template_detector.rb +2 -1
  54. data/lib/rigor/analysis/incremental.rb +29 -9
  55. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +291 -0
  56. data/lib/rigor/analysis/incremental_run_slot.rb +173 -0
  57. data/lib/rigor/analysis/incremental_session.rb +287 -50
  58. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
  59. data/lib/rigor/analysis/reachability/graph.rb +123 -23
  60. data/lib/rigor/analysis/reachability/scan.rb +290 -36
  61. data/lib/rigor/analysis/reachability/scan_cache.rb +2 -1
  62. data/lib/rigor/analysis/rule_catalog.rb +43 -0
  63. data/lib/rigor/analysis/run_cache_key.rb +43 -9
  64. data/lib/rigor/analysis/run_stats.rb +2 -2
  65. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +88 -6
  66. data/lib/rigor/analysis/runner/pool_coordinator.rb +52 -13
  67. data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -13
  68. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  69. data/lib/rigor/analysis/runner.rb +371 -49
  70. data/lib/rigor/analysis/shadow_harness.rb +27 -0
  71. data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
  72. data/lib/rigor/analysis/template_unit_paths.rb +6 -4
  73. data/lib/rigor/analysis/template_units.rb +15 -5
  74. data/lib/rigor/analysis/worker_session.rb +11 -7
  75. data/lib/rigor/cache/descriptor.rb +40 -1
  76. data/lib/rigor/cache/file_digest.rb +28 -0
  77. data/lib/rigor/cache/incremental_snapshot.rb +92 -8
  78. data/lib/rigor/cache/store.rb +63 -3
  79. data/lib/rigor/cache/value_digest.rb +325 -0
  80. data/lib/rigor/cli/annotate_command.rb +5 -0
  81. data/lib/rigor/cli/baseline_command.rb +7 -18
  82. data/lib/rigor/cli/check_command.rb +85 -14
  83. data/lib/rigor/cli/doctor_command.rb +1 -1
  84. data/lib/rigor/cli/effects_report.rb +2 -2
  85. data/lib/rigor/cli/trace_command.rb +5 -0
  86. data/lib/rigor/cli/type_of_command.rb +5 -3
  87. data/lib/rigor/cli/unused_command.rb +16 -10
  88. data/lib/rigor/cli.rb +11 -3
  89. data/lib/rigor/config_audit.rb +3 -2
  90. data/lib/rigor/config_path.rb +29 -0
  91. data/lib/rigor/configuration/severity_profile.rb +11 -0
  92. data/lib/rigor/configuration.rb +52 -7
  93. data/lib/rigor/effects/plugin_facts.rb +6 -6
  94. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
  95. data/lib/rigor/environment/gated_signature_guard.rb +43 -0
  96. data/lib/rigor/environment/gated_signature_plan.rb +339 -0
  97. data/lib/rigor/environment/lockfile_resolver.rb +10 -1
  98. data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
  99. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  100. data/lib/rigor/environment/rbs_loader.rb +284 -15
  101. data/lib/rigor/environment/required_features.rb +206 -0
  102. data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
  103. data/lib/rigor/environment.rb +34 -73
  104. data/lib/rigor/inference/acceptance.rb +28 -3
  105. data/lib/rigor/inference/block_parameter_binder.rb +29 -13
  106. data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
  107. data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
  108. data/lib/rigor/inference/captured_locals.rb +7 -1
  109. data/lib/rigor/inference/declaration_walk/context.rb +195 -0
  110. data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
  111. data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
  112. data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
  113. data/lib/rigor/inference/declaration_walk.rb +7 -0
  114. data/lib/rigor/inference/def_node_resolver.rb +26 -3
  115. data/lib/rigor/inference/definer_resolution.rb +266 -0
  116. data/lib/rigor/inference/expression_typer.rb +351 -61
  117. data/lib/rigor/inference/external_ancestor_resolution.rb +13 -16
  118. data/lib/rigor/inference/global_write_census.rb +7 -4
  119. data/lib/rigor/inference/guard_rebinding.rb +54 -13
  120. data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
  121. data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
  122. data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +282 -0
  123. data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
  124. data/lib/rigor/inference/in_effect_refinements.rb +256 -0
  125. data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
  126. data/lib/rigor/inference/key_presence_guard.rb +303 -0
  127. data/lib/rigor/inference/macro_block_self_type.rb +73 -14
  128. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  129. data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
  130. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
  131. data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
  132. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
  133. data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
  134. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
  135. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
  136. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +305 -54
  137. data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
  138. data/lib/rigor/inference/method_dispatcher.rb +44 -11
  139. data/lib/rigor/inference/module_function_state.rb +214 -0
  140. data/lib/rigor/inference/multi_target_binder.rb +21 -16
  141. data/lib/rigor/inference/narrowing.rb +166 -22
  142. data/lib/rigor/inference/object_mixins.rb +180 -0
  143. data/lib/rigor/inference/parameter_inference_collector.rb +10 -9
  144. data/lib/rigor/inference/project_patched_methods.rb +10 -5
  145. data/lib/rigor/inference/project_patched_scanner.rb +71 -3
  146. data/lib/rigor/inference/refine_census.rb +239 -0
  147. data/lib/rigor/inference/refine_self.rb +317 -0
  148. data/lib/rigor/inference/refined_dispatch/own_method.rb +403 -0
  149. data/lib/rigor/inference/refined_dispatch.rb +272 -0
  150. data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
  151. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
  152. data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
  153. data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
  154. data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
  155. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +143 -0
  156. data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
  157. data/lib/rigor/inference/scope_indexer.rb +2129 -354
  158. data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
  159. data/lib/rigor/inference/statement_evaluator.rb +149 -32
  160. data/lib/rigor/inference/string_mutation.rb +9 -3
  161. data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
  162. data/lib/rigor/inference/version_guard.rb +19 -10
  163. data/lib/rigor/language_server/project_context.rb +31 -0
  164. data/lib/rigor/language_server/server.rb +18 -0
  165. data/lib/rigor/plugin/base.rb +28 -8
  166. data/lib/rigor/plugin/inflector.rb +1 -1
  167. data/lib/rigor/plugin/io_boundary.rb +124 -7
  168. data/lib/rigor/plugin/isolation.rb +1 -1
  169. data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
  170. data/lib/rigor/plugin/trust_policy.rb +3 -3
  171. data/lib/rigor/project_environment.rb +29 -6
  172. data/lib/rigor/protection/discovery_seed.rb +18 -9
  173. data/lib/rigor/protection/mutation_cache.rb +4 -4
  174. data/lib/rigor/protection/mutator.rb +1 -1
  175. data/lib/rigor/reflection/constant_ancestors.rb +211 -46
  176. data/lib/rigor/reflection/constant_path.rb +33 -7
  177. data/lib/rigor/reflection.rb +41 -10
  178. data/lib/rigor/scope/discovery_index.rb +258 -3
  179. data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
  180. data/lib/rigor/scope/resolution_chain.rb +1032 -0
  181. data/lib/rigor/scope.rb +480 -328
  182. data/lib/rigor/sig_gen/alias_index.rb +1 -1
  183. data/lib/rigor/sig_gen/classification.rb +4 -1
  184. data/lib/rigor/sig_gen/effect_annotation.rb +2 -2
  185. data/lib/rigor/sig_gen/generator.rb +77 -21
  186. data/lib/rigor/sig_gen/method_candidate.rb +2 -2
  187. data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
  188. data/lib/rigor/source/alias_names.rb +52 -0
  189. data/lib/rigor/source/literals.rb +3 -2
  190. data/lib/rigor/source/symbol_name.rb +20 -0
  191. data/lib/rigor/type/combinator.rb +59 -3
  192. data/lib/rigor/type/float_range.rb +4 -1
  193. data/lib/rigor/type/nominal.rb +6 -1
  194. data/lib/rigor/version.rb +1 -1
  195. data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
  196. data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
  197. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
  198. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
  199. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
  200. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
  201. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
  202. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
  203. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
  204. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
  205. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
  206. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
  207. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
  208. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
  209. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
  210. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
  211. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
  212. data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
  213. data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
  214. data/plugins/rigor-alba/sig/alba.rbs +4 -0
  215. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
  216. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
  217. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +13 -2
  218. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
  219. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
  220. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
  221. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
  222. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
  223. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
  224. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
  225. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
  226. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
  227. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
  228. data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
  229. data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
  230. data/sig/rigor/environment.rbs +2 -0
  231. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  232. data/sig/rigor/scope.rbs +69 -4
  233. data/sig/rigor/source.rbs +4 -0
  234. data/sig/rigor.rbs +3 -1
  235. data/skills/rigor-project-init/SKILL.md +37 -11
  236. data/skills/rigor-project-init/references/02-configure.md +7 -2
  237. metadata +90 -1
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../reflection"
4
+ require_relative "../../inference/definer_resolution"
4
5
  require_relative "../../source/parameter_envelope"
5
6
 
6
7
  module Rigor
@@ -14,9 +15,10 @@ module Rigor
14
15
  # Asked in two stages, because every condition past the first can only WITHHOLD a firing:
15
16
  #
16
17
  # 1. {#owner_envelope} walks the receiver's superclass chain to the first class whose own body, or one of
17
- # whose project mixins, records the name. Every record at that level must agree — `include` and
18
- # `prepend` share one table, so a mixin's definition may shadow the class's own — and none may be
19
- # opaque. A call that fits this envelope is silent, and nothing below is consulted.
18
+ # whose project mixins, records the name. Every record at that level must agree — deliberately wider
19
+ # than Ruby's order, so a mixin whose definition may shadow the class's own, or a load-order-dependent
20
+ # pair (#986), declines instead of answering — and none may be opaque. A call that fits this envelope
21
+ # is silent, and nothing below is consulted.
20
22
  # 2. {#authoritative?} runs only for a call that would fire. It declines when a `method_missing` /
21
23
  # `respond_to_missing?` hook or a dynamic-surface mark sits anywhere in the chain; when a mixin the
22
24
  # project does not declare is not a known RBS module that lacks the name; when a `pre_eval:` patch or
@@ -24,20 +26,45 @@ module Rigor
24
26
  # project subclass of the receiver — whose instances the receiver's type admits — records a different
25
27
  # or opaque envelope for it.
26
28
  #
29
+ # ADR-119 C1b / C2-c — on both sides stage 1 and the subclass check ask `Inference::DefinerResolution`, and a
30
+ # chain it declines (`UNKNOWN`) answers no envelope: where Ruby's order depends on a fact the tables cannot
31
+ # prove, the rule is silent instead of checking the call against master's order (#1570, #1607).
32
+ #
27
33
  # Deliberately NOT in scope: a constructor reached through `Class#new` (nothing records `initialize` as
28
34
  # `[:singleton, :new]`, so `.new` finds no owner unless the class writes `def self.new` itself); keyword
29
35
  # arity (the envelope's required-keyword flag declines, as `compute_arity_envelope` declines an RBS
30
36
  # function with required keywords); and a `class_eval` whose receiver is not a constant, which the
31
37
  # discovery walk cannot name.
32
- class SourceArity
38
+ class SourceArity # rubocop:disable Metrics/ClassLength
33
39
  MISSING_HOOKS = %i[method_missing respond_to_missing?].freeze
34
40
  private_constant :MISSING_HOOKS
35
41
 
36
42
  # One class on the walk: its own name, the project modules its mixins resolve to (transitively through
37
43
  # their own `include`s), and the candidate-name lists of the mixins that resolve to no project module.
44
+ #
45
+ # Issue #1570 — the level is a SEGMENT of the receiver's `Scope::ResolutionChain` (`:arity` flavor): the
46
+ # class with the modules Ruby inserts around it before the superclass's entries begin. Ruby skips an
47
+ # `include` of a module the superclass chain already carries, so `class C < Base; include M` where
48
+ # `Base` includes `M` leaves `M` at `Base`'s level; collecting every module a class's own `include`s
49
+ # name put it at `C`'s level instead, where its `def` answered a call `Base#foo` receives.
38
50
  Level = Data.define(:class_name, :modules, :externals)
39
51
  private_constant :Level
40
52
 
53
+ # ADR-119 C1b — the answer of a level whose records are not one non-opaque envelope. It equals itself in
54
+ # every world, so two worlds that both reach such a level agree, and no envelope equals it.
55
+ AMBIGUOUS = Object.new.freeze
56
+ private_constant :AMBIGUOUS
57
+
58
+ # What `Inference::DefinerResolution` reads off a candidate: the answer, the entry that gave it, its
59
+ # position on the chain and its side. The same four readers as `DefinerResolution::Hit`, which a call site
60
+ # may not name (the case/in contract spec allows no other reference to the module).
61
+ ArityHit = Data.define(:answer, :owner, :index, :side)
62
+ private_constant :ArityHit
63
+
64
+ # A subclass level the walk cannot read: `subclasses_agree?` reads the nil as a decline.
65
+ NO_LEVELS = [nil].freeze
66
+ private_constant :NO_LEVELS
67
+
41
68
  def initialize(scope, method_name, kind)
42
69
  @scope = scope
43
70
  @method_name = method_name.to_sym
@@ -53,6 +80,7 @@ module Rigor
53
80
  def owner_envelope(class_name)
54
81
  @owner_entries = []
55
82
  @ambiguous = false
83
+ @unknown = false
56
84
  envelope, @withheld = Analysis::DependencyRecorder.withhold { walk_to_owner(class_name) }
57
85
  envelope
58
86
  end
@@ -67,6 +95,11 @@ module Rigor
67
95
  # withhold, so none of these needs the file-level class edge the bucket reads would otherwise record.
68
96
  def settle_by_definitions
69
97
  return if @withheld.nil?
98
+ # ADR-119 C1b — a declined chain (`@unknown`) that some level records the name on is silent only until
99
+ # another file's edit lifts the decline, so its verdict depends on everything the walk read, as an opaque
100
+ # level's does. A decline over a name no project level records is not `@unknown` (see
101
+ # {#decline_unless_recorded_nowhere}): it settles by definitions, as an `ABSENT` read does.
102
+ return settle_by_walk if @unknown
70
103
 
71
104
  @owner_entries.each do |name, kind|
72
105
  if kind == :singleton
@@ -95,20 +128,88 @@ module Rigor
95
128
 
96
129
  private
97
130
 
131
+ # ADR-119 C1b / C2-c, #1570 / #1607 — the nearest level is asked of {Inference::DefinerResolution}, on the
132
+ # receiver's own side: the chain stands only where every candidate level gives one envelope in every
133
+ # world Ruby may have run (a fork, an unproven mixin order, a `possible`-only definer, and on the
134
+ # singleton side a hook that could have put a definer ahead). Where it does not, the answer is NO
135
+ # envelope and `@unknown` — the rule stays silent — instead of master's order, which is what #1570 and
136
+ # #1607 fired on. The levels the walk keeps for {#authoritative?} are the chain's.
98
137
  def walk_to_owner(class_name)
99
138
  @levels = []
100
139
  @passed = []
101
- walked_whole_chain?(class_name) do |current|
102
- level = level_for(current)
140
+ @recorded = false
141
+
142
+ case Inference::DefinerResolution.resolve(@scope, class_name, @method_name, side,
143
+ question: :arity) { |chain, from| arity_hit(chain, from) }
144
+ in Inference::DefinerResolution::Known(answer: _answer, owner: _owner) then nearest_envelope(class_name)
145
+ in Inference::DefinerResolution::ABSENT then nearest_envelope(class_name) # rubocop:disable Lint/DuplicateBranch
146
+ in Inference::DefinerResolution::UNKNOWN then decline_unless_recorded_nowhere(class_name)
147
+ end
148
+ end
149
+
150
+ # A declined read of a name no project level records (`arity_hit` found no level that does, in any world it
151
+ # was asked) answers no envelope whether or not it stands, so the walk's reads are not the verdict's
152
+ # dependency: only a `def` appearing can change it, which {#settle_by_definitions} files, as for an
153
+ # `ABSENT` read. It can leave a stale false negative in a warm run, never a firing. Without this, every
154
+ # `Widget.new` (the implicit `Class` tail may answer it) would replay the whole walk as a file-level
155
+ # class edge.
156
+ def decline_unless_recorded_nowhere(class_name)
157
+ nowhere = !@recorded && !arity_chain(class_name).truncated?
158
+ nowhere ? nearest_envelope(class_name) : decline
159
+ end
160
+
161
+ # The chain's nearest level's envelope. `Known#answer` is that envelope, but {#authoritative?} and
162
+ # {#settle_by_definitions} also read the levels, passed names and owner entries this walk fills, so the
163
+ # answer is not used on its own. Run only once the candidate-set read has answered, so the chain stands
164
+ # for the name.
165
+ def nearest_envelope(class_name) = owner_in(chain_levels(arity_chain(class_name)).first)
166
+
167
+ def decline
168
+ @levels = []
169
+ @passed = []
170
+ @owner_entries = []
171
+ @envelope = nil
172
+ @unknown = true
173
+ nil
174
+ end
175
+
176
+ # The first level at or after entry position `from` whose records name the method: its envelope (or
177
+ # {AMBIGUOUS}), the entry that recorded it and that entry's position and side on the chain. On the
178
+ # singleton side the class's own record is a `:singleton` entry and an extended module's an `:instance`
179
+ # one (`owner_entries` names the kind of each).
180
+ def arity_hit(chain, from)
181
+ start = 0
182
+ chain.levels.each_with_index do |raw, index|
183
+ level = Level.new(*raw)
184
+ entries = chain.level_entries(index)
185
+ first = start
186
+ start += entries.size
187
+ next if first < from
188
+
189
+ found = level_envelopes(level)
190
+ next if found.empty?
191
+
192
+ envelope = found.first
193
+ answer = found.all?(envelope) && !Source::ParameterEnvelope.opaque?(envelope) ? envelope : AMBIGUOUS
194
+ owner, owner_kind = owner_entries(level).first
195
+ position = first + (entries.index { |entry| entry.name == owner && entry.side == owner_kind } || 0)
196
+ @recorded = true
197
+ return ArityHit.new(answer, owner, position, owner_kind)
198
+ end
199
+ nil
200
+ end
201
+
202
+ def owner_in(levels)
203
+ levels.each do |level|
103
204
  @levels << level
104
205
  found = level_envelopes(level)
105
206
  if found.empty?
106
- @passed.concat([current] + level.modules)
207
+ @passed.concat([level.class_name] + level.modules)
107
208
  next
108
209
  end
109
210
 
110
211
  envelope = found.first
111
- @owner_entries = owner_entries(level)
212
+ @owner_entries |= owner_entries(level)
112
213
  unless found.all?(envelope) && !Source::ParameterEnvelope.opaque?(envelope)
113
214
  @ambiguous = true
114
215
  return nil
@@ -155,56 +256,30 @@ module Rigor
155
256
  @scope.discovered_classes.key?(name) || Rigor::Reflection.rbs_class_known?(name, scope: @scope)
156
257
  end
157
258
 
158
- # False when the walk stopped at the ADR-41 budget rather than at the top of the chain: budget
159
- # exhaustion is uncertainty, and every caller reads it as a reason to decline.
160
- def walked_whole_chain?(class_name)
161
- current = class_name.to_s
162
- seen = {}
163
- while current && !seen[current]
164
- return false if seen.size >= Scope::ANCESTOR_WALK_LIMIT
165
-
166
- seen[current] = true
167
- yield current
168
- current = resolve(current, @scope.superclass_of(current))
169
- end
170
- true
259
+ # Yields each level of `class_name`'s chain. False when the walk stopped at the ADR-41 budget rather than
260
+ # ending: budget exhaustion is uncertainty, and every caller reads it as a reason to decline.
261
+ def walked_whole_chain?(class_name, &)
262
+ levels, whole = chain_levels(arity_chain(class_name))
263
+ levels.each(&)
264
+ whole
171
265
  end
172
266
 
173
- def level_for(class_name)
174
- modules = []
175
- externals = []
176
- collect_mixins(class_name, own_mixins(class_name), modules, externals, {})
177
- Level.new(class_name: class_name, modules: modules, externals: externals)
178
- end
267
+ # `[levels, whole]` for one world of the chain, up to the first superclass the project does not declare.
268
+ def chain_levels(chain) = [chain.levels.map { |level| Level.new(*level) }, !chain.truncated?]
269
+
270
+ def side = @kind == :singleton ? :singleton : :instance
179
271
 
180
272
  # Instance methods reach a receiver through `include` / `prepend`; class methods through `extend`, and
181
- # an extended module's own `include`s reach the same singleton.
182
- def own_mixins(class_name)
183
- @kind == :singleton ? (@scope.discovered_extends[class_name] || []) : @scope.includes_of(class_name)
184
- end
185
-
186
- # Issue #986 — a name the compact-header rename collision left ambiguous resolves to no ONE class,
187
- # but BOTH classes it names are ancestors at runtime; only their MRO order is unknowable. Taking
188
- # both as levels is what keeps this rule alive for the rest of the receiver: a method only one of
189
- # them declares still has one envelope and still fires, a method they disagree about lands two on
190
- # `#envelope_for`'s join and declines there, and the class's own `def`s, its superclass's and its
191
- # unambiguous mixins are untouched. Letting the decline arrive as an unknown EXTERNAL mixin instead
192
- # suppressed the rule for every level of the receiver.
193
- def collect_mixins(owner, raw_names, modules, externals, seen)
194
- raw_names.each do |raw|
195
- resolved = resolve(owner, raw)
196
- names = resolved ? [resolved] : @scope.ambiguous_ancestor_resolutions(owner, raw)
197
- next externals << @scope.ancestor_name_candidates(owner, raw) if names.empty?
198
-
199
- names.each do |name|
200
- next if seen[name]
201
-
202
- seen[name] = true
203
- modules << name
204
- collect_mixins(name, @scope.includes_of(name), modules, externals, seen)
205
- end
206
- end
207
- end
273
+ # an extended module's own `include`s reach the same singleton — the chain's singleton side.
274
+ #
275
+ # Issue #986 — the `:arity` flavor expands a name the compact-header rename collision left ambiguous
276
+ # to BOTH classes it names: both are ancestors at runtime and only their MRO order is unknowable. Taking
277
+ # both into the level is what keeps this rule alive for the rest of the receiver: a method only one of
278
+ # them declares still has one envelope and still fires, a method they disagree about lands two on the
279
+ # level's join and declines there, and the class's own `def`s, its superclass's and its unambiguous
280
+ # mixins are untouched. Letting the decline arrive as an unknown EXTERNAL mixin instead suppressed the
281
+ # rule for every level of the receiver.
282
+ def arity_chain(class_name) = Scope::ResolutionChain.for(@scope, class_name.to_s, side, :arity)
208
283
 
209
284
  def resolve(owner, raw)
210
285
  return nil if raw.nil?
@@ -215,8 +290,11 @@ module Rigor
215
290
  bucket[raw] = @scope.ancestor_name_candidates(owner, raw).find { |name| project_class?(name) }
216
291
  end
217
292
 
293
+ # A bucket of class marks alone (issue #1820: `x.singleton_class.prepend(Comparable)`) does not make a name
294
+ # the project's.
218
295
  def project_class?(name)
219
- @scope.known_user_class?(name) || @scope.discovered_parameter_envelopes.key?(name)
296
+ @scope.known_user_class?(name) ||
297
+ Scope::DiscoveryIndex.declares_surface?(@scope.discovered_parameter_envelopes[name])
220
298
  end
221
299
 
222
300
  # A module's methods reach the receiver as instance methods whichever side the receiver is on.
@@ -231,8 +309,13 @@ module Rigor
231
309
  level.externals.all? { |candidates| external_mixin_lacks_method?(candidates) }
232
310
  end
233
311
 
312
+ # Issue #1797 — a class whose body rewrites its method table in a way no literal names. A `refine` of the
313
+ # class (`ENVELOPE_REFINED_MARK`) is not one here: whether a refinement answers a call is decided per call
314
+ # site, name and `using` by `CheckRules#refined_redefinition_in_effect?`, which also declines where a
315
+ # refinement of a subclass of the receiver's class may answer. Counting the mark silenced every name on
316
+ # every subclass of a refined class, with or without a `using`.
234
317
  def dynamic_surface?(class_name)
235
- Scope::DiscoveryIndex.rewritten_surface?(@scope.parameter_envelopes_of(class_name))
318
+ @scope.parameter_envelopes_of(class_name).key?(Scope::DiscoveryIndex::ENVELOPE_DYNAMIC_MARK)
236
319
  end
237
320
 
238
321
  def project_patched?(class_name)
@@ -273,8 +356,8 @@ module Rigor
273
356
  # A hook anywhere in the chain, including above the owner, and on either side: a class that answers
274
357
  # names it does not define is a class whose `def`s are not the whole story of what a call reaches.
275
358
  def chain_free_of_hooks?(class_name)
276
- walked_whole_chain?(class_name) do |current|
277
- ([current] + level_for(current).modules).each do |name|
359
+ walked_whole_chain?(class_name) do |level|
360
+ ([level.class_name] + level.modules).each do |name|
278
361
  envelopes = @scope.parameter_envelopes_of(name)
279
362
  return false if dynamic_surface?(name)
280
363
  return false if MISSING_HOOKS.any? { |hook| hook_recorded?(envelopes, hook) }
@@ -290,15 +373,48 @@ module Rigor
290
373
  # receiver may be any of them, and each one's own definition of the name is what runs for it.
291
374
  def subclasses_agree?(class_name, envelope)
292
375
  each_subclass(class_name) do |subclass|
293
- level = level_for(subclass)
294
- return false unless clean_level?(level)
295
- return false unless level_envelopes(level).all?(envelope)
376
+ subclass_levels(subclass).each do |level|
377
+ # A subclass whose own level the budget cut cannot be read, which is a reason to decline.
378
+ return false if level.nil?
379
+ return false unless clean_level?(level)
380
+ return false unless level_envelopes(level).all?(envelope)
381
+ end
296
382
  end
297
383
  true
298
384
  end
299
385
 
386
+ # A subclass's own level (nil where the budget cut it or the chain does not stand for the name). It asks
387
+ # {Inference::DefinerResolution} on the receiver's side (ADR-119 C1b, C2-c): the subclass's own level
388
+ # answers, and a fork, an unproven mixin order, a `possible`-only definer or a hook there is `UNKNOWN`,
389
+ # which `subclasses_agree?` reads as a reason to decline.
390
+ def subclass_levels(subclass)
391
+ case Inference::DefinerResolution.resolve(@scope, subclass, @method_name, side, question: :arity,
392
+ &own_level_answer(subclass))
393
+ in Inference::DefinerResolution::Known(answer: _answer, owner: _owner) then [first_level(subclass)]
394
+ in Inference::DefinerResolution::ABSENT then [nil]
395
+ in Inference::DefinerResolution::UNKNOWN then NO_LEVELS
396
+ end
397
+ end
398
+
399
+ # The subclass's own level, always a candidate (an empty answer where it records nothing), so that the
400
+ # read asks whether the chain stands for the subclass and never whether an ancestor defines the name.
401
+ def own_level_answer(subclass)
402
+ lambda do |chain, from|
403
+ next nil unless from.zero? && !chain.levels.empty?
404
+
405
+ level = Level.new(*chain.levels.first)
406
+ ArityHit.new(level_envelopes(level).uniq, subclass, 0, side)
407
+ end
408
+ end
409
+
410
+ def first_level(class_name) = chain_levels(arity_chain(class_name)).first.first
411
+
412
+ # ADR-46 — every class whose subclasses are enumerated records `subclass_of:<last segment>`, which a class
413
+ # declaration with that superclass appearing or vanishing fires (issue #1820): a new subclass with a
414
+ # different envelope re-checks the call.
300
415
  def each_subclass(class_name)
301
416
  children = children_by_parent
417
+ record_subclasses_read(class_name)
302
418
  queue = children.fetch(class_name.to_s, []).dup
303
419
  seen = {}
304
420
  until queue.empty?
@@ -306,11 +422,18 @@ module Rigor
306
422
  next if seen[subclass]
307
423
 
308
424
  seen[subclass] = true
425
+ record_subclasses_read(subclass)
309
426
  yield subclass
310
427
  queue.concat(children.fetch(subclass, []))
311
428
  end
312
429
  end
313
430
 
431
+ def record_subclasses_read(class_name)
432
+ return unless Analysis::DependencyRecorder.active?
433
+
434
+ Analysis::DependencyRecorder.read_last_segment(:subclass_of, class_name.to_s)
435
+ end
436
+
314
437
  def children_by_parent
315
438
  @scope.discovered_superclasses.each_with_object({}) do |(child, raw_parent), children|
316
439
  parent = resolve(child, raw_parent)