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.
- checksums.yaml +4 -4
- data/README.md +4 -3
- data/data/builtins/ruby_core/io.yml +1 -0
- data/data/builtins/ruby_core/proc.yml +4 -0
- data/data/builtins/ruby_core/random.yml +4 -0
- data/data/builtins/ruby_core/struct.yml +3 -0
- data/data/core_overlay/env.rbs +34 -0
- data/data/core_overlay/gc.rbs +50 -0
- data/data/core_overlay/hash.rbs +45 -0
- data/data/core_overlay/integer.rbs +31 -0
- data/data/core_overlay/io_buffer.rbs +27 -0
- data/data/core_overlay/kernel.rbs +40 -0
- data/data/core_overlay/match_data.rbs +28 -0
- data/data/core_overlay/module.rbs +51 -0
- data/data/core_overlay/object_space.rbs +29 -0
- data/data/core_overlay/proc.rbs +45 -0
- data/data/core_overlay/range.rbs +40 -0
- data/data/core_overlay/source_range.rbs +78 -0
- data/data/core_overlay/string.rbs +86 -0
- data/data/vendored_gem_sigs/prime/integer-extension.rbs +41 -0
- data/data/vendored_gem_sigs/prime/prime.rbs +372 -0
- data/docs/handbook/01-getting-started.md +3 -3
- data/docs/handbook/06-classes.md +3 -1
- data/docs/handbook/08-understanding-errors.md +3 -2
- data/docs/handbook/11-sig-gen.md +6 -0
- data/docs/handbook/appendix-liskov.md +5 -0
- data/docs/install.md +87 -15
- data/docs/manual/02-cli-reference.md +2 -2
- data/docs/manual/03-configuration.md +6 -1
- data/docs/manual/04-diagnostics.md +2 -1
- data/docs/manual/06-baseline.md +2 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/08-skills.md +4 -3
- data/docs/manual/12-caching.md +43 -4
- data/docs/manual/14-rails-quickstart.md +19 -11
- data/docs/manual/18-removing-dead-code.md +32 -2
- data/docs/manual/plugins/README.md +6 -1
- data/docs/manual/plugins/rigor-activerecord.md +21 -0
- data/docs/manual/plugins/rigor-alba.md +60 -0
- data/docs/manual/plugins/rigor-minitest.md +22 -4
- data/docs/manual/plugins/rigor-typelizer.md +71 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
- data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +4 -0
- data/lib/rigor/analysis/check_rules/source_arity.rb +184 -61
- data/lib/rigor/analysis/check_rules.rb +407 -144
- data/lib/rigor/analysis/dependency_recorder.rb +215 -18
- data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
- data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
- data/lib/rigor/analysis/erb_template_detector.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +29 -9
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +291 -0
- data/lib/rigor/analysis/incremental_run_slot.rb +173 -0
- data/lib/rigor/analysis/incremental_session.rb +287 -50
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
- data/lib/rigor/analysis/reachability/graph.rb +123 -23
- data/lib/rigor/analysis/reachability/scan.rb +290 -36
- data/lib/rigor/analysis/reachability/scan_cache.rb +2 -1
- data/lib/rigor/analysis/rule_catalog.rb +43 -0
- data/lib/rigor/analysis/run_cache_key.rb +43 -9
- data/lib/rigor/analysis/run_stats.rb +2 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +88 -6
- data/lib/rigor/analysis/runner/pool_coordinator.rb +52 -13
- data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -13
- data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
- data/lib/rigor/analysis/runner.rb +371 -49
- data/lib/rigor/analysis/shadow_harness.rb +27 -0
- data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
- data/lib/rigor/analysis/template_unit_paths.rb +6 -4
- data/lib/rigor/analysis/template_units.rb +15 -5
- data/lib/rigor/analysis/worker_session.rb +11 -7
- data/lib/rigor/cache/descriptor.rb +40 -1
- data/lib/rigor/cache/file_digest.rb +28 -0
- data/lib/rigor/cache/incremental_snapshot.rb +92 -8
- data/lib/rigor/cache/store.rb +63 -3
- data/lib/rigor/cache/value_digest.rb +325 -0
- data/lib/rigor/cli/annotate_command.rb +5 -0
- data/lib/rigor/cli/baseline_command.rb +7 -18
- data/lib/rigor/cli/check_command.rb +85 -14
- data/lib/rigor/cli/doctor_command.rb +1 -1
- data/lib/rigor/cli/effects_report.rb +2 -2
- data/lib/rigor/cli/trace_command.rb +5 -0
- data/lib/rigor/cli/type_of_command.rb +5 -3
- data/lib/rigor/cli/unused_command.rb +16 -10
- data/lib/rigor/cli.rb +11 -3
- data/lib/rigor/config_audit.rb +3 -2
- data/lib/rigor/config_path.rb +29 -0
- data/lib/rigor/configuration/severity_profile.rb +11 -0
- data/lib/rigor/configuration.rb +52 -7
- data/lib/rigor/effects/plugin_facts.rb +6 -6
- data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
- data/lib/rigor/environment/gated_signature_guard.rb +43 -0
- data/lib/rigor/environment/gated_signature_plan.rb +339 -0
- data/lib/rigor/environment/lockfile_resolver.rb +10 -1
- data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +284 -15
- data/lib/rigor/environment/required_features.rb +206 -0
- data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
- data/lib/rigor/environment.rb +34 -73
- data/lib/rigor/inference/acceptance.rb +28 -3
- data/lib/rigor/inference/block_parameter_binder.rb +29 -13
- data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
- data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
- data/lib/rigor/inference/captured_locals.rb +7 -1
- data/lib/rigor/inference/declaration_walk/context.rb +195 -0
- data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
- data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
- data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
- data/lib/rigor/inference/declaration_walk.rb +7 -0
- data/lib/rigor/inference/def_node_resolver.rb +26 -3
- data/lib/rigor/inference/definer_resolution.rb +266 -0
- data/lib/rigor/inference/expression_typer.rb +351 -61
- data/lib/rigor/inference/external_ancestor_resolution.rb +13 -16
- data/lib/rigor/inference/global_write_census.rb +7 -4
- data/lib/rigor/inference/guard_rebinding.rb +54 -13
- data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
- data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
- data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +282 -0
- data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
- data/lib/rigor/inference/in_effect_refinements.rb +256 -0
- data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
- data/lib/rigor/inference/key_presence_guard.rb +303 -0
- data/lib/rigor/inference/macro_block_self_type.rb +73 -14
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
- data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
- data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
- data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
- data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +305 -54
- data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
- data/lib/rigor/inference/method_dispatcher.rb +44 -11
- data/lib/rigor/inference/module_function_state.rb +214 -0
- data/lib/rigor/inference/multi_target_binder.rb +21 -16
- data/lib/rigor/inference/narrowing.rb +166 -22
- data/lib/rigor/inference/object_mixins.rb +180 -0
- data/lib/rigor/inference/parameter_inference_collector.rb +10 -9
- data/lib/rigor/inference/project_patched_methods.rb +10 -5
- data/lib/rigor/inference/project_patched_scanner.rb +71 -3
- data/lib/rigor/inference/refine_census.rb +239 -0
- data/lib/rigor/inference/refine_self.rb +317 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +403 -0
- data/lib/rigor/inference/refined_dispatch.rb +272 -0
- data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
- data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
- data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
- data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
- data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +143 -0
- data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
- data/lib/rigor/inference/scope_indexer.rb +2129 -354
- data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
- data/lib/rigor/inference/statement_evaluator.rb +149 -32
- data/lib/rigor/inference/string_mutation.rb +9 -3
- data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
- data/lib/rigor/inference/version_guard.rb +19 -10
- data/lib/rigor/language_server/project_context.rb +31 -0
- data/lib/rigor/language_server/server.rb +18 -0
- data/lib/rigor/plugin/base.rb +28 -8
- data/lib/rigor/plugin/inflector.rb +1 -1
- data/lib/rigor/plugin/io_boundary.rb +124 -7
- data/lib/rigor/plugin/isolation.rb +1 -1
- data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
- data/lib/rigor/plugin/trust_policy.rb +3 -3
- data/lib/rigor/project_environment.rb +29 -6
- data/lib/rigor/protection/discovery_seed.rb +18 -9
- data/lib/rigor/protection/mutation_cache.rb +4 -4
- data/lib/rigor/protection/mutator.rb +1 -1
- data/lib/rigor/reflection/constant_ancestors.rb +211 -46
- data/lib/rigor/reflection/constant_path.rb +33 -7
- data/lib/rigor/reflection.rb +41 -10
- data/lib/rigor/scope/discovery_index.rb +258 -3
- data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
- data/lib/rigor/scope/resolution_chain.rb +1032 -0
- data/lib/rigor/scope.rb +480 -328
- data/lib/rigor/sig_gen/alias_index.rb +1 -1
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/effect_annotation.rb +2 -2
- data/lib/rigor/sig_gen/generator.rb +77 -21
- data/lib/rigor/sig_gen/method_candidate.rb +2 -2
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
- data/lib/rigor/source/alias_names.rb +52 -0
- data/lib/rigor/source/literals.rb +3 -2
- data/lib/rigor/source/symbol_name.rb +20 -0
- data/lib/rigor/type/combinator.rb +59 -3
- data/lib/rigor/type/float_range.rb +4 -1
- data/lib/rigor/type/nominal.rb +6 -1
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
- data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
- data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
- data/plugins/rigor-alba/sig/alba.rbs +4 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +13 -2
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
- data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
- data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
- data/sig/rigor/environment.rbs +2 -0
- data/sig/rigor/plugin/io_boundary.rbs +4 -0
- data/sig/rigor/scope.rbs +69 -4
- data/sig/rigor/source.rbs +4 -0
- data/sig/rigor.rbs +3 -1
- data/skills/rigor-project-init/SKILL.md +37 -11
- data/skills/rigor-project-init/references/02-configure.md +7 -2
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
undecidable =
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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.
|
|
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.
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
315
|
+
return candidate if declared?(candidate, exclude, local)
|
|
221
316
|
end
|
|
222
317
|
end
|
|
223
318
|
|
|
224
|
-
return as_written if
|
|
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
|
-
|
|
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
|