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
@@ -35,15 +35,21 @@ module Rigor
35
35
  # a reopened gem or stdlib class must never be a candidate (WD6). Defaults to "nothing is foreign".
36
36
  # @param dynamic_uses — sites where a constant is reached by name at runtime.
37
37
  # A literal-argument site contributes a real reference; a dynamic one taints a namespace (WD4).
38
- def initialize(declarations:, references:, root_fqns: [], dynamic_uses: [], foreign: ->(_fqn) { false })
38
+ # @param shadows — declarations made in files outside the analysed paths. They are never nodes, but a
39
+ # reference written in the SAME file resolves to them first, as Ruby would (#1732, see {#resolve_ref}).
40
+ def initialize(declarations:, references:, root_fqns: [], dynamic_uses: [], shadows: [],
41
+ foreign: ->(_fqn) { false })
39
42
  @declarations = declarations
40
- @dynamic_uses = dynamic_uses
41
43
  @references = references + literal_dynamic_references(dynamic_uses)
42
44
  @root_fqns = root_fqns.to_set
43
45
  @foreign = foreign
44
46
  @by_fqn = declarations.group_by(&:fqn)
47
+ @shadows = shadows.group_by(&:path).transform_values { |decls| decls.to_set(&:fqn) }
48
+ @shadows.default = Set.new.freeze
45
49
  @owned = @by_fqn.keys.reject { |fqn| @foreign.call(fqn) }.to_set
46
50
  @ancestors = {}
51
+ # `Foo.const_get("V#{k}")` bounds its reach by `Foo`, a reference like any other (#1734).
52
+ @dynamic_uses = dynamic_uses.flat_map { |use| use.anchored { |ref| resolve_ref(ref) } }
47
53
  end
48
54
 
49
55
  def report
@@ -51,21 +57,31 @@ module Rigor
51
57
  production = walk(edges, seeds: production_seeds, roles: %i[production task config])
52
58
  reachable = walk(edges, seeds: production_seeds | test_seeds, roles: %i[production task config test])
53
59
  unreached = @owned - reachable
54
- namespaces = namespace_only(unreached, reachable)
60
+ # A namespace is excused by a member PRODUCTION reaches. One whose members only tests reach is dead
61
+ # production code like them, so it joins them under test-only rather than vanishing from every
62
+ # bucket (#1732).
63
+ namespaces = namespace_only(unreached, production)
64
+ test_only = (reachable - production) | namespace_only(unreached - namespaces, reachable)
65
+ unreached -= namespaces | test_only
66
+ # A namespace a test names (`Ns::CONST`) while production reaches a declaration under it is live in
67
+ # production for the same reason an unreached one is not dead (#1732).
68
+ live_namespaces = namespace_only(test_only, production)
69
+ test_only -= live_namespaces
55
70
  # The data-file demotion applies to BOTH buckets it can speak to, which is what the tier
56
71
  # contract says and what the implementation had narrowed (#370). See {#tainted}.
57
- test_only = reachable - production
58
- undecidable = tainted(unreached - namespaces).merge(tainted(test_only))
59
- build_report(edges: edges, reachable: reachable, unreached: unreached, namespaces: namespaces,
60
- test_only: test_only, undecidable: undecidable)
72
+ undecidable = tainted(unreached).merge(tainted(test_only))
73
+ undecidable = spread_undecidable(edges, undecidable, unreached | test_only,
74
+ through: namespaces | live_namespaces)
75
+ build_report(edges:, reachable:, unreached:, namespaces:, test_only:, undecidable:)
61
76
  end
62
77
 
63
78
  private
64
79
 
65
80
  def build_report(edges:, reachable:, unreached:, namespaces:, test_only:, undecidable:)
66
81
  demoted = undecidable.keys.to_set
67
- Report.new(declared: @owned.size, reachable: reachable.size,
68
- candidates: rows(unreached - namespaces - demoted),
82
+ # A test-only namespace counts as reachable, like the members it is listed with.
83
+ Report.new(declared: @owned.size, reachable: (reachable | test_only).size,
84
+ candidates: rows(unreached - demoted),
69
85
  undecidable: undecidable.map { |fqn, reason| undecidable_row(fqn, reason) }.freeze,
70
86
  test_only: rows(test_only - demoted),
71
87
  namespaces: namespaces.size, roots: production_seeds.size, edges: edges.size)
@@ -109,6 +125,41 @@ module Rigor
109
125
  end
110
126
  end
111
127
 
128
+ # Whatever an undecidable declaration reaches is undecidable too (#1720 review). `class Sub < Base` with
129
+ # `Sub` reachable through `"Sub#{x}".constantize` left `Base` a DEFINITE candidate, and acting on that row
130
+ # deletes the base of a class that may well be live. Only declarations that would otherwise be
131
+ # reported — unreached or test-only — are moved; the reason names the declaration they hang from.
132
+ #
133
+ # The spread passes `through` a declaration hidden as a namespace without listing it: a service hidden
134
+ # only because something names a constant nested in it (`Svc::Error`) still carries the undecidable
135
+ # evidence on to what its body names. Stopping there left the next service down under "reachable only
136
+ # from tests" (#1732).
137
+ def spread_undecidable(edges, undecidable, reportable, through:)
138
+ return undecidable if undecidable.empty?
139
+
140
+ out = Hash.new { |h, k| h[k] = [] }
141
+ edges.each { |from, to, _role| out[from] << to }
142
+ result = undecidable.dup
143
+ passed = Set.new
144
+ queue = undecidable.keys
145
+ until queue.empty?
146
+ from = queue.shift
147
+ out[from].each do |target|
148
+ next if result.key?(target) || passed.include?(target)
149
+
150
+ if reportable.include?(target)
151
+ result[target] = "reachable from #{from}, which cannot be decided"
152
+ elsif through.include?(target)
153
+ passed << target
154
+ else
155
+ next
156
+ end
157
+ queue << target
158
+ end
159
+ end
160
+ result
161
+ end
162
+
112
163
  def undecidable_row(fqn, reason)
113
164
  site = @by_fqn.fetch(fqn).first
114
165
  Undecidable.new(fqn: fqn, path: site.path, line: site.line, reason: reason)
@@ -132,14 +183,32 @@ module Rigor
132
183
  # Seeds that make a declaration live in PRODUCTION: named entry points, plus anything referenced at
133
184
  # file level by a non-test file (file-level code runs on load, so its target is live).
134
185
  def production_seeds
135
- @production_seeds ||= (@root_fqns & @owned) | seeds_from { |ref| ref.from.nil? && ref.role != :test }
186
+ @production_seeds ||= (@root_fqns & @owned) | seeds_from { |ref| source(ref).nil? && ref.role != :test }
136
187
  end
137
188
 
138
189
  # Seeds that make a declaration live only through TEST code. Kept separate from production seeds
139
190
  # rather than folded in: a spec's file-level `Foo.new` would otherwise promote `Foo` to a root and
140
191
  # erase the very distinction WD8 exists to report.
141
192
  def test_seeds
142
- @test_seeds ||= seeds_from { |ref| ref.from.nil? && ref.role == :test }
193
+ @test_seeds ||= seeds_from { |ref| source(ref).nil? && ref.role == :test }
194
+ end
195
+
196
+ # The node an edge leaves: the owned declaration whose code holds the reference, with a declaration-header
197
+ # reference (`class Sub < Base`) credited to `Sub` (#1720). Only an owned declaration from `paths:` is a
198
+ # node, though. A class an initializer, `config/application.rb` or a spec helper declares, or a reopened
199
+ # gem class (WD6), is outside the declaration set, so the walk can never start from it and an edge
200
+ # leaving it would silently drop the evidence. Falling back to the enclosing nesting (#1724) did not help
201
+ # either: `module MyApp; class Application` and a spec's `module Outer; class Fake < OBase` fall back to a
202
+ # module that is never reached itself, or is not a node at all (#1732).
203
+ #
204
+ # Such a reference counts as file-level code of its file instead, in that file's role: a spec helper's
205
+ # reference keeps its target test-reachable, an initializer's keeps it live in production. A class header
206
+ # and a class body's own statements run when the file loads; a method body is read the same way because
207
+ # whether it runs is not decidable from a declaration this report does not own, and the other reading
208
+ # would report live code as dead. What the reference NAMES is still resolved against its own file's
209
+ # declarations first (see {#resolve_ref}).
210
+ def source(ref)
211
+ @owned.include?(ref.from) ? ref.from : nil
143
212
  end
144
213
 
145
214
  def seeds_from
@@ -147,7 +216,7 @@ module Rigor
147
216
  @references.each do |ref|
148
217
  next unless yield(ref)
149
218
 
150
- target = resolve(ref.as_written, ref.nesting, rooted: ref.rooted)
219
+ target = resolve_ref(ref)
151
220
  set << target if target && @owned.include?(target)
152
221
  end
153
222
  set
@@ -156,14 +225,39 @@ module Rigor
156
225
  # `[from_fqn_or_nil, to_fqn, role]` for every reference that resolves to an owned declaration.
157
226
  def resolved_edges
158
227
  @resolved_edges ||= @references.filter_map do |ref|
159
- target = resolve(ref.as_written, ref.nesting, rooted: ref.rooted)
228
+ target = resolve_ref(ref)
160
229
  next unless target && @owned.include?(target)
161
- next if ref.from == target # a declaration referencing itself is not evidence of use
162
230
 
163
- [ref.from, target, ref.role]
231
+ from = source(ref)
232
+ next if from == target # a declaration referencing itself is not evidence of use
233
+
234
+ [from, target, ref.role]
164
235
  end
165
236
  end
166
237
 
238
+ # A declaration-header reference never names the declaration it belongs to: `module Api; class User <
239
+ # User; end; end` reads `User` before `Api::User` exists, so Ruby answers `::User`. Every other reference
240
+ # resolves as written, and one that lands on its own declaration is dropped as a self-reference.
241
+ #
242
+ # A class its own file declares outside `paths:` shadows a same-named owned one, as Ruby's lookup does:
243
+ # `LegacyThing` inside a migration's local `class LegacyThing < ActiveRecord::Base` stub names the stub,
244
+ # not the app's model. The answer is then not an owned node and the reference is dropped. Only the
245
+ # referring file's own declarations count: a spec support file's stub is loaded with the specs, never
246
+ # with production, so letting it shadow other files could hide a production reference (#1732).
247
+ def resolve_ref(ref)
248
+ local = @shadows[ref.path]
249
+ target = resolve(ref.as_written, ref.nesting, rooted: ref.rooted, local: local)
250
+ return target unless target && target == ref.from && header?(ref)
251
+
252
+ resolve(ref.as_written, ref.nesting, rooted: ref.rooted, exclude: target, local: local)
253
+ end
254
+
255
+ # Only a header reference is credited to something other than the scope it is written in (see
256
+ # {Scan::Reference}).
257
+ def header?(ref)
258
+ ref.from != (ref.nesting.empty? ? nil : ref.nesting.join("::"))
259
+ end
260
+
167
261
  # Mark-and-sweep, not reference counting: an edge only propagates if its SOURCE is itself reachable, so
168
262
  # a cluster of mutually-referencing dead classes stays dead (ADR-102 WD2).
169
263
  #
@@ -198,18 +292,19 @@ module Rigor
198
292
  # Ruby's constant lookup at name granularity: `Module.nesting` innermost first, then the ancestors of
199
293
  # the innermost cresting scope (#354), then the bare name. When `rooted: true`, lexical nesting and
200
294
  # ancestors are skipped, answering the top-level declaration directly (#625).
201
- def resolve(as_written, nesting, rooted: false)
295
+ # `exclude` names a declaration the answer must not be (see {#resolve_ref}).
296
+ def resolve(as_written, nesting, rooted: false, exclude: nil, local: nil)
202
297
  if rooted
203
- return as_written if @by_fqn.key?(as_written)
298
+ return as_written if declared?(as_written, exclude, local)
204
299
 
205
300
  idx = as_written.rindex("::")
206
- return idx ? resolve(as_written[0, idx], nesting, rooted: true) : nil
301
+ return idx ? resolve(as_written[0, idx], nesting, rooted: true, exclude: exclude, local: local) : nil
207
302
  end
208
303
 
209
304
  walker = nesting.dup
210
305
  until walker.empty?
211
306
  candidate = (walker + [as_written]).join("::")
212
- return candidate if @by_fqn.key?(candidate)
307
+ return candidate if declared?(candidate, exclude, local)
213
308
 
214
309
  walker.pop
215
310
  end
@@ -217,11 +312,11 @@ module Rigor
217
312
  unless nesting.empty?
218
313
  ancestor_scopes(nesting.join("::")).each do |ancestor|
219
314
  candidate = "#{ancestor}::#{as_written}"
220
- return candidate if @by_fqn.key?(candidate)
315
+ return candidate if declared?(candidate, exclude, local)
221
316
  end
222
317
  end
223
318
 
224
- return as_written if @by_fqn.key?(as_written)
319
+ return as_written if declared?(as_written, exclude, local)
225
320
 
226
321
  # `Scope::DiscoveryIndex::EMPTY` names a constant INSIDE a class, and reading it is a use of that
227
322
  # class — but the leaf is not itself a declaration, so the reference would resolve to nothing and
@@ -229,7 +324,11 @@ module Rigor
229
324
  # on the first run of this report against Rigor's own `lib`). Peel the trailing segment and retry:
230
325
  # a reference to a member is a reference to its owner.
231
326
  idx = as_written.rindex("::")
232
- idx ? resolve(as_written[0, idx], nesting) : nil
327
+ idx ? resolve(as_written[0, idx], nesting, exclude: exclude, local: local) : nil
328
+ end
329
+
330
+ def declared?(fqn, exclude, local)
331
+ fqn != exclude && (@by_fqn.key?(fqn) || local&.include?(fqn))
233
332
  end
234
333
 
235
334
  # Breadth-first over superclass + included modules, mixins first, terminating on a cycle. As-written
@@ -260,7 +359,8 @@ module Rigor
260
359
  segments = subclass_fqn.split("::")
261
360
  (segments.length - 1).downto(0) do |i|
262
361
  candidate = (segments[0, i] + [raw]).join("::")
263
- return candidate if @by_fqn.key?(candidate)
362
+ # `class Api::User < User` cannot inherit from itself; the name was read before it existed.
363
+ return candidate if candidate != subclass_fqn && @by_fqn.key?(candidate)
264
364
  end
265
365
  nil
266
366
  end