rigortype 0.4.1 → 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 +1 -1
- 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/11-sig-gen.md +6 -0
- data/docs/handbook/appendix-liskov.md +5 -0
- data/docs/manual/03-configuration.md +1 -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/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 +21 -2
- data/lib/rigor/analysis/check_rules.rb +289 -57
- data/lib/rigor/analysis/dependency_recorder.rb +15 -0
- 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 +30 -5
- data/lib/rigor/analysis/incremental_run_slot.rb +3 -1
- data/lib/rigor/analysis/incremental_session.rb +14 -1
- data/lib/rigor/analysis/reachability/graph.rb +123 -23
- data/lib/rigor/analysis/reachability/scan.rb +290 -36
- data/lib/rigor/analysis/rule_catalog.rb +43 -0
- data/lib/rigor/analysis/run_cache_key.rb +16 -1
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +87 -5
- data/lib/rigor/analysis/runner/pool_coordinator.rb +50 -11
- data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
- data/lib/rigor/analysis/runner.rb +34 -3
- data/lib/rigor/analysis/worker_session.rb +7 -4
- data/lib/rigor/cache/incremental_snapshot.rb +22 -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 +2 -1
- 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 +13 -7
- data/lib/rigor/cli.rb +10 -2
- data/lib/rigor/configuration/severity_profile.rb +6 -0
- data/lib/rigor/configuration.rb +46 -2
- data/lib/rigor/environment/gated_signature_guard.rb +43 -0
- data/lib/rigor/environment/gated_signature_plan.rb +339 -0
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +283 -14
- data/lib/rigor/environment/required_features.rb +206 -0
- data/lib/rigor/environment.rb +21 -3
- data/lib/rigor/inference/acceptance.rb +28 -3
- data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
- data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
- data/lib/rigor/inference/def_node_resolver.rb +23 -1
- data/lib/rigor/inference/expression_typer.rb +215 -16
- data/lib/rigor/inference/global_write_census.rb +7 -4
- data/lib/rigor/inference/guard_rebinding.rb +1 -1
- 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 +59 -12
- 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 +259 -28
- 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 +2 -1
- data/lib/rigor/inference/narrowing.rb +20 -6
- data/lib/rigor/inference/object_mixins.rb +180 -0
- 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/mixin_accumulator.rb +13 -0
- data/lib/rigor/inference/scope_indexer.rb +687 -60
- data/lib/rigor/inference/statement_evaluator.rb +71 -20
- 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/inflector.rb +1 -1
- data/lib/rigor/plugin/io_boundary.rb +42 -3
- data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
- data/lib/rigor/project_environment.rb +29 -6
- data/lib/rigor/protection/mutation_cache.rb +1 -1
- data/lib/rigor/protection/mutator.rb +1 -1
- data/lib/rigor/reflection/constant_ancestors.rb +127 -11
- data/lib/rigor/reflection/constant_path.rb +13 -2
- data/lib/rigor/reflection.rb +40 -8
- data/lib/rigor/scope/discovery_index.rb +40 -1
- data/lib/rigor/scope/resolution_chain.rb +159 -11
- data/lib/rigor/scope.rb +189 -10
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/generator.rb +55 -5
- 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 +34 -2
- 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 +7 -6
- 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/plugin/io_boundary.rbs +3 -0
- data/sig/rigor/scope.rbs +27 -4
- data/sig/rigor/source.rbs +4 -0
- data/sig/rigor.rbs +3 -1
- metadata +66 -1
|
@@ -4,6 +4,7 @@ require "fileutils"
|
|
|
4
4
|
require "securerandom"
|
|
5
5
|
|
|
6
6
|
require_relative "../incremental_run_slot"
|
|
7
|
+
require_relative "../dependency_source_inference/gem_resolver"
|
|
7
8
|
require_relative "../../cache/file_digest"
|
|
8
9
|
require_relative "../../cache/incremental_snapshot"
|
|
9
10
|
|
|
@@ -61,7 +62,9 @@ module Rigor
|
|
|
61
62
|
# signature file under the configured or auto-detected `sig/` roots, or a `pre_eval:` file, changed in
|
|
62
63
|
# between would leave the snapshot the run restores keyed by one tree
|
|
63
64
|
# and the run reading another. From the stamp on, the lockfiles are the guard's to watch ({#admits?}), which
|
|
64
|
-
# is also what lets the slot's key, whose only file inputs they are, be computed when the run ends.
|
|
65
|
+
# is also what lets the slot's key, whose only file inputs they are, be computed when the run ends. The
|
|
66
|
+
# opted-in gems' resolved versions, the key's one input that is neither a file nor the configuration (issue
|
|
67
|
+
# #1814), are read at the mark and watched the same way.
|
|
65
68
|
#
|
|
66
69
|
# The mark also records which of the paths an existence row can name are present: the lockfiles, the
|
|
67
70
|
# analysis roots, the `pre_eval:` entries and the signature roots.
|
|
@@ -78,10 +81,22 @@ module Rigor
|
|
|
78
81
|
presence = (lockfiles + named.map { |path| File.absolute_path(path.to_s) }).to_h do |path|
|
|
79
82
|
[path, File.exist?(path)]
|
|
80
83
|
end
|
|
84
|
+
rows = dependency_rows(configuration)
|
|
85
|
+
return nil if rows.nil?
|
|
86
|
+
|
|
81
87
|
new(started_ns: started_ns, device: device, lockfiles: lockfiles, presence: presence,
|
|
82
|
-
stamp_dir: stamp_dir(cache_root))
|
|
88
|
+
stamp_dir: stamp_dir(cache_root), configuration: configuration, dependency_rows: rows)
|
|
83
89
|
end
|
|
84
90
|
|
|
91
|
+
# Issue #1814 — the opted-in gems' `(name, version, mode)` rows as the key carries them; nil when they cannot
|
|
92
|
+
# be resolved, which takes no mark (the key could not be computed either).
|
|
93
|
+
def self.dependency_rows(configuration)
|
|
94
|
+
DependencySourceInference::GemResolver.dependency_entries(configuration.dependencies)
|
|
95
|
+
rescue StandardError
|
|
96
|
+
nil
|
|
97
|
+
end
|
|
98
|
+
private_class_method :dependency_rows
|
|
99
|
+
|
|
85
100
|
# The mark: the change time and device of the first stamp whose change time is later than that of a
|
|
86
101
|
# stamp taken before it, on the same device; nil when none is written within {TICK_WAIT_LIMIT}.
|
|
87
102
|
def self.stamp(cache_root)
|
|
@@ -143,17 +158,21 @@ module Rigor
|
|
|
143
158
|
end
|
|
144
159
|
private_class_method :lockfile_paths
|
|
145
160
|
|
|
146
|
-
def initialize(started_ns:, device:, lockfiles:, presence: {}, stamp_dir: nil
|
|
161
|
+
def initialize(started_ns:, device:, lockfiles:, presence: {}, stamp_dir: nil, configuration: nil,
|
|
162
|
+
dependency_rows: [])
|
|
147
163
|
@stamp_dir = stamp_dir
|
|
148
164
|
@started_ns = started_ns
|
|
149
165
|
@device = device
|
|
150
166
|
@lockfiles = lockfiles
|
|
151
167
|
@presence = presence
|
|
168
|
+
@configuration = configuration
|
|
169
|
+
@dependency_rows = dependency_rows
|
|
152
170
|
end
|
|
153
171
|
|
|
154
172
|
# True when nothing the guard watches moved since the mark:
|
|
155
173
|
#
|
|
156
|
-
# - no lockfile the key or the fingerprint digests was written, created or removed
|
|
174
|
+
# - no lockfile the key or the fingerprint digests was written, created or removed, and no opted-in gem
|
|
175
|
+
# resolves to another version than at the mark (issue #1814);
|
|
157
176
|
# - no file `rows` or `pinned` names changed. A content row's file changed if its change time moved. An
|
|
158
177
|
# existence row asks only whether its path is there, so a path the mark recorded changed if it came or
|
|
159
178
|
# went, and an editor's lock file created and removed beside it, which moves its directory's change
|
|
@@ -174,7 +193,7 @@ module Rigor
|
|
|
174
193
|
# @param rows — a {Cache::Descriptor} of the rows the key does not pin
|
|
175
194
|
# @param pinned — a {Cache::Descriptor} of the rows it does
|
|
176
195
|
def admits?(rows, pinned: Cache::Descriptor.new)
|
|
177
|
-
!clock_stepped_back? && @lockfiles.none? { |path| lockfile_changed?(path) } &&
|
|
196
|
+
!clock_stepped_back? && @lockfiles.none? { |path| lockfile_changed?(path) } && dependency_rows_unchanged? &&
|
|
178
197
|
files_unchanged?(rows.files, strict: true) && globs_unchanged?(rows.globs, strict: true) &&
|
|
179
198
|
files_unchanged?(pinned.files, strict: false) && globs_unchanged?(pinned.globs, strict: false)
|
|
180
199
|
rescue StandardError
|
|
@@ -196,6 +215,12 @@ module Rigor
|
|
|
196
215
|
|
|
197
216
|
private
|
|
198
217
|
|
|
218
|
+
# Issue #1814 — the opted-in gems resolve to the versions they did at the mark. A guard built without a
|
|
219
|
+
# configuration (the specs' bare constructor) watches none.
|
|
220
|
+
def dependency_rows_unchanged?
|
|
221
|
+
@configuration.nil? || self.class.send(:dependency_rows, @configuration) == @dependency_rows
|
|
222
|
+
end
|
|
223
|
+
|
|
199
224
|
# One more stamp: a change time earlier than the mark means the clock stepped back since it was taken, so
|
|
200
225
|
# a save since then may carry a change time the guard reads as before it. A stamp that cannot be written,
|
|
201
226
|
# or lands on another device, cannot vouch for the clock either.
|
|
@@ -84,7 +84,9 @@ module Rigor
|
|
|
84
84
|
return nil if base.nil?
|
|
85
85
|
|
|
86
86
|
roots = RunCacheKey.config_entry("incremental.roots", normalize_roots(target.roots).sort.join("\n"))
|
|
87
|
-
|
|
87
|
+
# Issue #1814 — the opted-in gems' `(name, version, mode)` rows travel with the key, as they do in the plain
|
|
88
|
+
# probe's: the gem's walked source reaches every file's answer and no analysed file records it.
|
|
89
|
+
Cache::Descriptor.new(gems: base.gems, dependencies: base.dependencies, configs: base.configs + [roots])
|
|
88
90
|
end
|
|
89
91
|
|
|
90
92
|
# The roots as absolute paths, which the KEY holds sorted: `lib`, `./lib` and `lib/` are one root there. A
|
|
@@ -1090,12 +1090,24 @@ module Rigor
|
|
|
1090
1090
|
changed + removed, @constant_decls, new_constant_decls
|
|
1091
1091
|
)
|
|
1092
1092
|
keys = appeared_methods.map { |symbol| negative_key_for(symbol) }
|
|
1093
|
-
keys.concat(moved_classes
|
|
1093
|
+
keys.concat(class_move_keys(moved_classes))
|
|
1094
1094
|
keys.concat(moved_constants.map { |name| "constant:#{name.split('::').last}" })
|
|
1095
1095
|
Incremental.negative_closure(keys, @negative_dependents) | refinement_affected(changed + removed, summary) |
|
|
1096
1096
|
global_write_reporters
|
|
1097
1097
|
end
|
|
1098
1098
|
|
|
1099
|
+
# The negative keys a class-declaration diff fires: `class:<last segment>` for a class name, and (issue #1820)
|
|
1100
|
+
# `subclass_of:<last segment of the superclass>` for a `[class, superclass]` pair appearing or vanishing. Any
|
|
1101
|
+
# class declaration moving may move the set of classes whose superclass the tables do not know, which
|
|
1102
|
+
# `subclass_of:*` stands for.
|
|
1103
|
+
def class_move_keys(moved)
|
|
1104
|
+
names, edges = moved.partition { |entry| entry.is_a?(String) }
|
|
1105
|
+
keys = names.map { |klass| "class:#{klass.split('::').last}" }
|
|
1106
|
+
keys.concat(edges.map { |(_child, parent)| "subclass_of:#{parent.split('::').last}" })
|
|
1107
|
+
keys << "subclass_of:*" unless moved.empty?
|
|
1108
|
+
keys
|
|
1109
|
+
end
|
|
1110
|
+
|
|
1099
1111
|
# Issue #1120 — the consumers whose `call.undefined-method` answer read the refinement table for a method
|
|
1100
1112
|
# name whose refinements moved in this edit. The before-state is each file's seed bundle (the per-file
|
|
1101
1113
|
# table the last run recorded), the after-state the scan's per-file tables. A refine body is not a class's
|
|
@@ -1118,6 +1130,7 @@ module Rigor
|
|
|
1118
1130
|
index[:class_sources].each do |class_name, files|
|
|
1119
1131
|
files.each { |file| result[file] << class_name }
|
|
1120
1132
|
end
|
|
1133
|
+
Incremental.add_superclass_edges(result, index[:class_sources], index[:superclasses])
|
|
1121
1134
|
result.transform_values(&:freeze).freeze
|
|
1122
1135
|
end
|
|
1123
1136
|
|
|
@@ -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
|