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
@@ -0,0 +1,253 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module Inference
5
+ # ADR-119 WD3 (errata 2026-10-08, PR C2-a) — the singleton side's positional hook decline. A hook's edge on
6
+ # the singleton side is recorded on no includer, so no chain state carries it (WD3); what the read CAN say
7
+ # is where such an edge could land. A hook touches the singleton chain of a class `K` in two ways only:
8
+ #
9
+ # - (U1) `K.extend(X)`, `class_methods do`, a concern's `ClassMethods`: Ruby inserts `X` after `#<Class:K>`
10
+ # and before `#<Class:K.superclass>`, inside `K`'s own level;
11
+ # - (U2) `included do def self.x end`, `define_singleton_method`, `singleton_class.class_eval`: a definition ON
12
+ # `#<Class:K>`, at the level's head.
13
+ #
14
+ # So a hook at level `i` cannot move a definer at an index at or before the level's head, and a definer found
15
+ # at `#<Class:K_j>` itself, written in `K_j`'s own body, is immune to every hook of levels `j` and deeper. The
16
+ # read's last candidate gives a BOUND: its index when it is that class entry and its `def` was written in
17
+ # `K_j`'s body; one past it for an extended module's entry, or for a copy the extends fold put on `K_j`
18
+ # (the `def`'s recorded nesting does not start with `K_j`), and every entry when nothing answers. The levels
19
+ # tested are those that start before the bound and do not end before `from`. The read declines when a tested
20
+ # level's own instance level (the class, its prepends and includes with their closures) or its singleton
21
+ # segment (the class's singleton and its extends' closures) holds a HOOK-CAPABLE entry, or when a level
22
+ # deeper than the shallowest tested level is hook-capable by the same test: its `inherited` defines on
23
+ # every subclass's singleton, and it need not be the class's own `def`. A deeper external superclass RBS
24
+ # does not know counts, its `inherited` being unknown by construction.
25
+ #
26
+ # Hook-capable, for a project entry or a declared module the chain holds as external (a candidate of its
27
+ # spelling is a `discovered_class_sources`, `discovered_classes`, `discovered_includes` or
28
+ # `discovered_extends` key; an ambiguous spelling tests every declared candidate and any capable one
29
+ # declines): lists `"*"` on either side, records a {Scope::ResolutionChain::Relevance::HOOKS} name on
30
+ # either side or as an envelope key, or extends `ActiveSupport::Concern`. An undeclared external is capable
31
+ # unless RBS knows it (WD2(i)'s limit: an RBS-known external is clean). The dynamic mark is not a hook signal.
32
+ # The verdict does not depend on the name asked, so it is memoised per entry in the chain's flavor bucket
33
+ # with the ADR-46 edges it read (a class edge and the negative class edge on the unqualified name per tested
34
+ # project entry, the negative class edge on the spelling's last segment and the candidates' class edges per
35
+ # tested external), replayed on every call while a recording is active.
36
+ #
37
+ # Known remainders, pinned in `spec/integration/definer_resolution_witness_spec.rb`: `class << self; prepend
38
+ # P` is recorded as an `extend` (the chain places `P` after the singleton, so an own `def self.x` is trusted
39
+ # against it); a concern is capable for every name; an own `def self.x` is trusted against its own level's
40
+ # U2 hooks and same-class redefinitions; a `class Class; def inherited` monkeypatch is unseen; and a
41
+ # superclass that only `extend`s adds two forks, so every read on its subclasses declines.
42
+ module SingletonHookDecline
43
+ WILDCARD = "*"
44
+ CONCERN = "ActiveSupport::Concern"
45
+ SIDES = %i[include extend].freeze
46
+
47
+ module_function
48
+
49
+ # True when a hook may have placed a definer of `method_name` ahead of the read's last candidate on the
50
+ # singleton `chain` (the candidate set `hits`, read from `from`).
51
+ def decline?(scope, chain, method_name, from, hits)
52
+ bound = bound_of(scope, chain, method_name, hits.last)
53
+ tested = tested_levels(chain, from, bound)
54
+ return false if tested.empty?
55
+ return true if chain.truncated?
56
+
57
+ context = Context.new(scope, chain.flavor)
58
+ tested.any? { |level| context.level_capable?(chain, level) } || context.deeper_hook?(chain, tested.first)
59
+ end
60
+
61
+ def bound_of(scope, chain, method_name, hit)
62
+ return chain.entries.size if hit.nil?
63
+
64
+ index = hit.index
65
+ entry = chain.entries[index]
66
+ # A singleton project entry on a singleton chain is always a level's class entry.
67
+ own = !entry.external? && entry.side == :singleton && def_head(scope, entry.name, method_name) == entry.name
68
+ own ? index : index + 1
69
+ end
70
+
71
+ # What a class entry's answer is when its `def` was written in another module's body: `:copy` when that
72
+ # module is on the same level's singleton segment (the extends fold copied it there; the copy is not where
73
+ # Ruby finds the method — the module's own entry is, and an entry between them may answer first — so the
74
+ # read asks again past it), `:stray` when it is not (the fold and the chain resolved the `extend`'s name to
75
+ # different modules, `extend X` inside `module A` with both `X` and `A::X` declared; or the `def` is not the
76
+ # node that module's instance table holds, a `def C.x` written in its body: the read declines), and
77
+ # nil otherwise (the class's own `def`, no `def`, or one with no recorded nesting).
78
+ def copy_kind(scope, chain, method_name, hit)
79
+ entry = chain.entries[hit.index]
80
+ return nil if entry.external? || entry.side != :singleton
81
+
82
+ head = def_head(scope, entry.name, method_name)
83
+ return nil if head.nil? || head == entry.name
84
+
85
+ level = chain.level_starts.rindex { |start| start <= hit.index }
86
+ return :stray if level.nil? || level >= chain.level_count
87
+
88
+ found = chain.level_entries(level).any? { |candidate| candidate.side == :instance && candidate.name == head }
89
+ found && copied_from?(scope, head, method_name, entry.name) ? :copy : :stray
90
+ end
91
+
92
+ # Whether the class's singleton `def` is the very node `head`'s instance table holds, the one the fold
93
+ # copied: a receiverless `def`. A `def C.x` or `class << C; def x; end` written inside `head`'s body has the
94
+ # same nesting head and is the class's own, not a copy. (The walk records a `def C.x` written in `module X`
95
+ # as `X#x` too, so identity alone does not tell it from a copy; its receiver does.)
96
+ def copied_from?(scope, head, method_name, owner)
97
+ node = scope.singleton_def_for(owner, method_name)
98
+ return false if node.nil? || !node.receiver.nil?
99
+
100
+ scope.user_def_for(head, method_name).equal?(node)
101
+ end
102
+
103
+ # The innermost `Module.nesting` entry `owner`'s singleton `def` of the name was written in, or nil.
104
+ def def_head(scope, owner, method_name)
105
+ node = scope.singleton_def_for(owner, method_name)
106
+ return nil if node.nil?
107
+
108
+ nesting = scope.discovery.discovered_def_nestings[node] || DefNodeResolver.rehydrated_nesting(node)
109
+ nesting&.first
110
+ end
111
+
112
+ def tested_levels(chain, from, bound)
113
+ starts = chain.level_starts
114
+ size = chain.entries.size
115
+ (0...chain.level_count).select { |level| starts[level] < bound && (starts[level + 1] || size) >= from }
116
+ end
117
+
118
+ # The verdicts' working state: the scope, the flavor and the memo the verdicts share.
119
+ class Context
120
+ def initialize(scope, flavor)
121
+ @scope = scope
122
+ @flavor = flavor
123
+ @memo = Scope::ResolutionChain.hook_memo(scope, flavor)
124
+ @recording = Analysis::DependencyRecorder.active?
125
+ end
126
+
127
+ # A tested level: its singleton segment, and its class's own instance level.
128
+ def level_capable?(chain, level)
129
+ return true if chain.level_entries(level).any? { |entry| capable?(entry) }
130
+
131
+ class_name = chain.level_classes[level]
132
+ !class_name.nil? && instance_level_capable?(class_name)
133
+ end
134
+
135
+ # A level deeper than `level` whose `inherited` can define on every subclass's singleton: one that is
136
+ # hook-capable as a tested level is. Its class records `inherited` (own, folded, or a literal
137
+ # `define_method`), or an entry of its own instance level or singleton segment can define it unseen (a
138
+ # concern's `class_methods do def inherited`, a hook that extends the class with a module defining it),
139
+ # or it is an external superclass that is hook-capable.
140
+ def deeper_hook?(chain, level)
141
+ ((level + 1)...chain.level_count).any? { |deeper| level_capable?(chain, deeper) }
142
+ end
143
+
144
+ def capable?(entry)
145
+ return verdict(:project, entry.name) { project_capable?(entry.name) } unless entry.external?
146
+
147
+ verdict(:external, entry) { external_capable?(entry) }
148
+ end
149
+
150
+ private
151
+
152
+ def instance_level_capable?(class_name)
153
+ verdict(:instance_level, class_name) do |edges|
154
+ chain = Scope::ResolutionChain.for(@scope, class_name, :instance, @flavor)
155
+ next true if chain.truncated?
156
+
157
+ entries = chain.level_count.zero? ? chain.entries : chain.level_entries(0)
158
+ entries.any? do |entry|
159
+ edges.concat(entry_edges(entry))
160
+ capable?(entry)
161
+ end
162
+ end
163
+ end
164
+
165
+ # The memoised `[verdict, edges]` of one test, its edges replayed while a recording is active.
166
+ def verdict(kind, key)
167
+ verdict, edges = @memo[[kind, key]] ||= begin
168
+ edges = []
169
+ value = yield(edges)
170
+ [value ? true : false, edges.concat(own_edges(kind, key)).uniq.freeze].freeze
171
+ end
172
+ replay(edges) if @recording
173
+ verdict
174
+ end
175
+
176
+ def own_edges(kind, key)
177
+ return entry_edges(key) if kind == :external
178
+ return EMPTY_EDGES if kind == :instance_level
179
+
180
+ [[:class, key], [:missing, key.to_s.split("::").last]]
181
+ end
182
+
183
+ def entry_edges(entry)
184
+ return [[:class, entry.name], [:missing, entry.last_segment]] unless entry.external?
185
+
186
+ entry.candidates.map { |candidate| [:class, candidate] } << [:missing, entry.raw.to_s.split("::").last]
187
+ end
188
+
189
+ def replay(edges)
190
+ edges.each do |kind, value|
191
+ case kind
192
+ when :class then Scope::ResolutionChain.record_class(@scope, value)
193
+ when :missing then Analysis::DependencyRecorder.read_missing(:class, value)
194
+ end
195
+ end
196
+ end
197
+
198
+ def project_capable?(name)
199
+ sides = @scope.discovery.unpositioned_mixins[name]
200
+ return true if sides && SIDES.any? { |side| sides[side]&.include?(WILDCARD) }
201
+
202
+ return true if records_hook?(name)
203
+
204
+ extends_concern?(name)
205
+ end
206
+
207
+ # Whether `name` records a hook name on either side, or in its envelope table: a `def self.inherited`, a
208
+ # module's instance `def inherited` (a hook once extended), `singleton_class.define_method(:inherited)`
209
+ # (recorded as an instance method) and `define_singleton_method(:inherited)` (recorded only as an opaque
210
+ # envelope, the name-literal evidence `ScopeIndexer#record_surface_evidence` files).
211
+ def records_hook?(name)
212
+ envelopes = @scope.parameter_envelopes_of(name)
213
+ Scope::ResolutionChain::Relevance::HOOKS.any? do |hook|
214
+ @scope.discovered_method?(name, hook, :singleton) || @scope.discovered_method?(name, hook, :instance) ||
215
+ envelopes.key?([:singleton, hook]) || envelopes.key?([:instance, hook])
216
+ end
217
+ end
218
+
219
+ # Whether `name`'s own singleton segment (its `extend`s, as the chain places them) holds
220
+ # `ActiveSupport::Concern` — the concern shape's only signal, a framework name the follow-up ADR moves
221
+ # behind the plugin API (WD3, Q11).
222
+ def extends_concern?(name)
223
+ chain = Scope::ResolutionChain.for(@scope, name, :singleton, @flavor)
224
+ entries = chain.level_count.zero? ? chain.entries : chain.level_entries(0)
225
+ entries.any? do |entry|
226
+ entry.external? ? entry.candidates.include?(CONCERN) : entry.name == CONCERN
227
+ end
228
+ end
229
+
230
+ # A declared module the chain holds as external is tested as that module; an undeclared one is capable
231
+ # unless RBS knows it.
232
+ def external_capable?(entry)
233
+ declared = entry.candidates.select { |candidate| declared?(candidate) }
234
+ return declared.any? { |name| verdict(:project, name) { project_capable?(name) } } unless declared.empty?
235
+
236
+ entry.candidates.none? { |candidate| Rigor::Reflection.rbs_class_known?(candidate, scope: @scope) }
237
+ rescue StandardError
238
+ true
239
+ end
240
+
241
+ def declared?(name)
242
+ discovery = @scope.discovery
243
+ discovery.discovered_class_sources.key?(name) || discovery.discovered_classes.key?(name) ||
244
+ discovery.discovered_includes.key?(name) || discovery.discovered_extends.key?(name)
245
+ end
246
+ end
247
+ private_constant :Context
248
+
249
+ EMPTY_EDGES = [].freeze
250
+ private_constant :EMPTY_EDGES
251
+ end
252
+ end
253
+ end