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
|
@@ -28,8 +28,11 @@ module Rigor
|
|
|
28
28
|
|
|
29
29
|
# One constant reference. `from` is the fully-qualified name of the innermost enclosing declaration, or
|
|
30
30
|
# nil for a reference written at file level — that is what makes the graph a reachability graph rather
|
|
31
|
-
# than a reference count (ADR-102 WD2).
|
|
32
|
-
#
|
|
31
|
+
# than a reference count (ADR-102 WD2). A reference in a declaration's own header — `class Sub < Base`,
|
|
32
|
+
# `Sub = Class.new(Base)` — is credited to that declaration, while `nesting` stays the outer scope it
|
|
33
|
+
# resolves in (#1720); that header case is the only one where `from` is not `nesting` joined. `role` is
|
|
34
|
+
# the referring FILE's role (WD8). `rooted` is true when the constant was written with a leading `::`
|
|
35
|
+
# (#625).
|
|
33
36
|
Reference = Data.define(:as_written, :nesting, :from, :role, :path, :line, :rooted) do
|
|
34
37
|
def initialize(rooted: false, **) = super
|
|
35
38
|
end
|
|
@@ -45,31 +48,69 @@ module Rigor
|
|
|
45
48
|
# children (#370). `config/recurring.yml` mentioning `Admin` is not evidence about
|
|
46
49
|
# `Admin::CollectionPolicy`; treating it as such demoted 18 unrelated Mastodon rows off one
|
|
47
50
|
# Afrikaans word ("Administrasie" contains "Admin").
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
# - `:leading` — `"V#{version}"` is the START of a name, not a namespace, so it can construct `V1_0`
|
|
52
|
+
# and `V2_2` as well as anything beneath them: a string-prefix match (#1734).
|
|
53
|
+
#
|
|
54
|
+
# `within` is set for `Foo.const_get("Bar::#{k}")`: `prefix` is then relative to the receiver, a
|
|
55
|
+
# {Reference} that {Graph} resolves as it resolves any other before matching.
|
|
56
|
+
DynamicUse = Data.define(:name, :prefix, :reason, :site, :path, :line, :scope, :within) do
|
|
57
|
+
def initialize(scope: :namespace, within: nil, **) = super
|
|
50
58
|
|
|
51
59
|
# Whether this site's evidence reaches `fqn`.
|
|
52
60
|
def taints?(fqn)
|
|
53
61
|
return false if prefix.nil?
|
|
54
62
|
return fqn == prefix if scope == :exact
|
|
63
|
+
return fqn.start_with?(prefix) if scope == :leading
|
|
55
64
|
|
|
56
65
|
fqn == prefix || fqn.start_with?("#{prefix}::")
|
|
57
66
|
end
|
|
67
|
+
|
|
68
|
+
# This site with `within` resolved by the block into absolute `prefix`es. A resolution of the whole
|
|
69
|
+
# written path is the anchor. {Graph#resolve} peels an unknown `Foo::Bar` to `Foo`, though, and
|
|
70
|
+
# anchoring there alone looked for `Foo::V*` and missed `Foo::Bar::V1`; the written name is then the
|
|
71
|
+
# anchor as well as the peeled one, since an undeclared receiver may be an alias of either. A
|
|
72
|
+
# receiver that does not resolve at all keeps its written name.
|
|
73
|
+
def anchored
|
|
74
|
+
return [self] if within.nil?
|
|
75
|
+
|
|
76
|
+
written = within.as_written
|
|
77
|
+
resolved = yield(within)
|
|
78
|
+
anchors = resolved == written || resolved&.end_with?("::#{written}") ? [resolved] : [written, resolved]
|
|
79
|
+
anchors.compact.map { |anchor| with(prefix: "#{anchor}::#{prefix}", within: nil) }
|
|
80
|
+
end
|
|
58
81
|
end
|
|
59
82
|
|
|
60
83
|
Result = Data.define(:declarations, :references, :dynamic_uses)
|
|
61
84
|
|
|
62
85
|
# Roles a referring file can have (ADR-102 WD8). A reference edge carries its referrer's role so
|
|
63
86
|
# "used only by its own test" is a reportable category rather than a bucket boundary.
|
|
64
|
-
|
|
87
|
+
#
|
|
88
|
+
# End-to-end suites (`qa/`, `e2e/`, GitLab's QA; `features/`, cucumber) are test code too (#1751). They
|
|
89
|
+
# are matched at the project root only: `app/models/features/` is an ordinary namespace directory, and
|
|
90
|
+
# a false :test would hide a production reference. Tooling directories (`rubocop/`, `keeps/`,
|
|
91
|
+
# `tooling/`, `scripts/`) stay :production on purpose — deleting a class they use breaks the tooling.
|
|
92
|
+
#
|
|
93
|
+
# The role is decided from the path RELATIVE to `root` (the project root), never from the absolute
|
|
94
|
+
# path: `/home/me/test/app/lib/a.rb` is not test code because a directory above the project is called
|
|
95
|
+
# `test`. With no `root` the path is taken as already relative.
|
|
96
|
+
def self.role_for(path, root: nil)
|
|
97
|
+
path = relative_to(path, root)
|
|
65
98
|
case path
|
|
66
|
-
when %r{(\A|/)(spec|test)/}, /_(spec|test)\.rb\z/ then :test
|
|
99
|
+
when %r{(\A|/)(spec|test)/}, /_(spec|test)\.rb\z/, %r{\A(\./)?(qa|e2e|features)/} then :test
|
|
67
100
|
when /\.rake\z/, %r{(\A|/)(lib/)?tasks/} then :task
|
|
68
101
|
when %r{(\A|/)config/} then :config
|
|
69
102
|
else :production
|
|
70
103
|
end
|
|
71
104
|
end
|
|
72
105
|
|
|
106
|
+
def self.relative_to(path, root)
|
|
107
|
+
return path if root.nil?
|
|
108
|
+
|
|
109
|
+
prefix = "#{root.to_s.chomp('/')}/"
|
|
110
|
+
path.start_with?(prefix) ? path.delete_prefix(prefix) : path
|
|
111
|
+
end
|
|
112
|
+
private_class_method :relative_to
|
|
113
|
+
|
|
73
114
|
# A constant name is ASCII by construction, so a byte sequence that is not valid UTF-8 cannot be one.
|
|
74
115
|
# Dropping it is both correct and the only safe answer: carrying it forward crashed the whole run on
|
|
75
116
|
# the first `String#sub` downstream, which is how this surfaced — `rigor unused` on Rigor's own
|
|
@@ -84,9 +125,10 @@ module Rigor
|
|
|
84
125
|
# @param path — the file's path, as the report should render it.
|
|
85
126
|
# @param source — the file's bytes.
|
|
86
127
|
# @param target_ruby — Prism version string, threaded from the project configuration.
|
|
128
|
+
# @param root — the project root `path` is made relative to when its role is decided.
|
|
87
129
|
# @return nil when the file does not parse (a parse error is the analyzer's business, not
|
|
88
130
|
# this scan's — it simply contributes nothing rather than half a file).
|
|
89
|
-
def self.call(path:, source:, target_ruby: nil)
|
|
131
|
+
def self.call(path:, source:, target_ruby: nil, root: nil)
|
|
90
132
|
parsed = if target_ruby
|
|
91
133
|
Prism.parse(source, filepath: path,
|
|
92
134
|
version: target_ruby)
|
|
@@ -95,12 +137,66 @@ module Rigor
|
|
|
95
137
|
end
|
|
96
138
|
return nil unless parsed.success?
|
|
97
139
|
|
|
98
|
-
walker = Walker.new(path: path, role: role_for(path))
|
|
140
|
+
walker = Walker.new(path: path, role: role_for(path, root: root))
|
|
99
141
|
walker.walk(parsed.value, [])
|
|
100
142
|
Result.new(declarations: walker.declarations.freeze, references: walker.references.freeze,
|
|
101
143
|
dynamic_uses: walker.dynamic_uses.freeze)
|
|
102
144
|
end
|
|
103
145
|
|
|
146
|
+
# The values a local variable can hold, read off its assignments in one local scope, for a
|
|
147
|
+
# dynamic-resolution argument built a line before the call (#1734).
|
|
148
|
+
module LocalValues
|
|
149
|
+
module_function
|
|
150
|
+
|
|
151
|
+
# The assigned value nodes of `name` in `scope`, or nil when there is none or one of the assignments
|
|
152
|
+
# cannot be read off its node (`+=`, a multiple assignment, `rescue => name`).
|
|
153
|
+
def values(scope, name)
|
|
154
|
+
writes = []
|
|
155
|
+
collect_local_writes(scope, name, writes)
|
|
156
|
+
return nil if writes.empty? || writes.any? { |write| !VALUE_WRITES.include?(write.class) }
|
|
157
|
+
|
|
158
|
+
writes.map(&:value)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# The names a block's parameter list binds, block-locals included.
|
|
162
|
+
def bindings(parameters)
|
|
163
|
+
out = []
|
|
164
|
+
collect_block_bindings(parameters, out)
|
|
165
|
+
out
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
VALUE_WRITES = [Prism::LocalVariableWriteNode, Prism::LocalVariableOrWriteNode,
|
|
169
|
+
Prism::LocalVariableAndWriteNode].freeze
|
|
170
|
+
private_constant :VALUE_WRITES
|
|
171
|
+
|
|
172
|
+
LOCAL_WRITES = (VALUE_WRITES + [Prism::LocalVariableOperatorWriteNode, Prism::LocalVariableTargetNode]).freeze
|
|
173
|
+
private_constant :LOCAL_WRITES
|
|
174
|
+
|
|
175
|
+
# Nodes that open a new local-variable scope. A block does not: it shares its method's locals.
|
|
176
|
+
SCOPE_GATES = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
|
|
177
|
+
private_constant :SCOPE_GATES
|
|
178
|
+
|
|
179
|
+
def collect_local_writes(node, name, out)
|
|
180
|
+
node.rigor_each_child do |child|
|
|
181
|
+
next if SCOPE_GATES.any? { |gate| child.is_a?(gate) }
|
|
182
|
+
|
|
183
|
+
out << child if LOCAL_WRITES.any? { |kind| child.is_a?(kind) } && child.name == name
|
|
184
|
+
collect_local_writes(child, name, out)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
BLOCK_BINDINGS = [Prism::RequiredParameterNode, Prism::OptionalParameterNode, Prism::RestParameterNode,
|
|
189
|
+
Prism::RequiredKeywordParameterNode, Prism::OptionalKeywordParameterNode,
|
|
190
|
+
Prism::KeywordRestParameterNode, Prism::BlockParameterNode,
|
|
191
|
+
Prism::BlockLocalVariableNode].freeze
|
|
192
|
+
private_constant :BLOCK_BINDINGS
|
|
193
|
+
|
|
194
|
+
def collect_block_bindings(node, out)
|
|
195
|
+
out << node.name if BLOCK_BINDINGS.any? { |kind| node.is_a?(kind) } && node.name
|
|
196
|
+
node.rigor_each_child { |child| collect_block_bindings(child, out) }
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
104
200
|
# Single-pass walker. Tracks `nesting` as the stack of enclosing declaration names.
|
|
105
201
|
class Walker
|
|
106
202
|
attr_reader :declarations, :references, :dynamic_uses
|
|
@@ -111,6 +207,12 @@ module Rigor
|
|
|
111
207
|
@declarations = []
|
|
112
208
|
@references = []
|
|
113
209
|
@dynamic_uses = []
|
|
210
|
+
# The declaration a meta-new rvalue is being walked for, or nil. See {#walk_constant_write}.
|
|
211
|
+
@owner = nil
|
|
212
|
+
# The node whose subtree holds the local variables in scope: the file, a method, or a class body.
|
|
213
|
+
@local_scope = nil
|
|
214
|
+
# Names an enclosing block's parameters or block-locals shadow within @local_scope.
|
|
215
|
+
@block_params = Set.new.freeze
|
|
114
216
|
end
|
|
115
217
|
|
|
116
218
|
def walk(node, nesting)
|
|
@@ -124,13 +226,21 @@ module Rigor
|
|
|
124
226
|
# A constant path's segments are not separate references — `A::B::C` is one reference to the leaf,
|
|
125
227
|
# and descending would record `A` and `A::B` as references in their own right (22 spurious
|
|
126
228
|
# candidates on Rigor's own lib came from exactly that in the #345 probe).
|
|
229
|
+
#
|
|
230
|
+
# A path on a computed base is the exception: `Migration[2.2]::MigrationRecord` names no constant
|
|
231
|
+
# this reading can resolve, but its base is a call whose receiver and arguments do.
|
|
232
|
+
walk(node.parent, nesting) if node.is_a?(Prism::ConstantPathNode) &&
|
|
233
|
+
Source::ConstantPath.qualified_name_or_nil(node).nil?
|
|
127
234
|
return
|
|
128
235
|
when Prism::ConstantWriteNode
|
|
129
|
-
|
|
130
|
-
walk(node.value, nesting)
|
|
236
|
+
walk_constant_write(node, nesting)
|
|
131
237
|
return
|
|
132
238
|
when Prism::CallNode
|
|
133
|
-
record_dynamic_use(node)
|
|
239
|
+
record_dynamic_use(node, nesting)
|
|
240
|
+
when Prism::ProgramNode, Prism::DefNode, Prism::SingletonClassNode
|
|
241
|
+
return in_local_scope(node) { node.rigor_each_child { |child| walk(child, nesting) } }
|
|
242
|
+
when Prism::BlockNode, Prism::LambdaNode
|
|
243
|
+
return in_block(node) { node.rigor_each_child { |child| walk(child, nesting) } }
|
|
134
244
|
end
|
|
135
245
|
|
|
136
246
|
node.rigor_each_child { |child| walk(child, nesting) }
|
|
@@ -144,13 +254,27 @@ module Rigor
|
|
|
144
254
|
|
|
145
255
|
fqn = (nesting + [name]).join("::")
|
|
146
256
|
# The superclass position IS a reference — `class Sub < Base` reads `Base` — and it resolves against
|
|
147
|
-
# the OUTER nesting, not inside the body being opened.
|
|
148
|
-
|
|
257
|
+
# the OUTER nesting, not inside the body being opened. It is part of `Sub`'s declaration, though, so
|
|
258
|
+
# it is credited to `Sub` (#1720): crediting the enclosing scope left a base nested in a module that
|
|
259
|
+
# is never itself reached unreachable, and rooted a top-level base even when every subclass is dead.
|
|
260
|
+
#
|
|
261
|
+
# The superclass is WALKED, not only read as a name: `< DelegateClass(Foo)`, `< Struct.new(:a,
|
|
262
|
+
# Foo::X)` and `< ActiveRecord::Migration[7.1]` are calls whose receiver and arguments name
|
|
263
|
+
# constants, and reading only a constant node recorded none of them, so `Foo` was a false candidate
|
|
264
|
+
# (#1733). Its references take the credit a meta-new rvalue's do.
|
|
265
|
+
walk_owned(superclass, nesting, fqn) if superclass
|
|
149
266
|
includes = node.body ? mixin_names(node.body) : []
|
|
150
267
|
@declarations << Declaration.new(fqn: fqn, path: @path, line: node.location.start_line,
|
|
151
268
|
superclass: superclass && Source::ConstantPath.qualified_name(superclass),
|
|
152
269
|
includes: includes.freeze)
|
|
153
|
-
|
|
270
|
+
return unless node.body
|
|
271
|
+
|
|
272
|
+
# The body's nesting names the declaration itself, so a credit inherited from a meta-new rvalue
|
|
273
|
+
# this declaration sits in must not leak into it.
|
|
274
|
+
owner = @owner
|
|
275
|
+
@owner = nil
|
|
276
|
+
in_local_scope(node.body) { walk(node.body, nesting + [name]) }
|
|
277
|
+
@owner = owner
|
|
154
278
|
end
|
|
155
279
|
|
|
156
280
|
# `Const = Class.new` / `Module.new` / `Data.define(...)` / `Struct.new(...)` declare a class under a
|
|
@@ -160,6 +284,26 @@ module Rigor
|
|
|
160
284
|
META_NEW = { "Class" => :new, "Module" => :new, "Data" => :define, "Struct" => :new }.freeze
|
|
161
285
|
private_constant :META_NEW
|
|
162
286
|
|
|
287
|
+
# A meta-new rvalue — `Class.new(Base) { ... }`, `Struct.new(:a) do ... end` — is the declaration's
|
|
288
|
+
# header and body, so the references in it are credited to the declared constant, exactly as a
|
|
289
|
+
# `class Sub < Base` superclass is (#1720). Its lexical nesting is unchanged: a block opens no cref.
|
|
290
|
+
def walk_constant_write(node, nesting)
|
|
291
|
+
fqn = record_meta_new(node, nesting)
|
|
292
|
+
return walk(node.value, nesting) if fqn.nil?
|
|
293
|
+
|
|
294
|
+
walk_owned(node.value, nesting, fqn)
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Walks a declaration's header expression in the outer `nesting` it is evaluated in, crediting the
|
|
298
|
+
# references in it to the declared `fqn`.
|
|
299
|
+
def walk_owned(node, nesting, fqn)
|
|
300
|
+
owner = @owner
|
|
301
|
+
@owner = fqn
|
|
302
|
+
walk(node, nesting)
|
|
303
|
+
@owner = owner
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# @return the declared FQN, or nil when the rvalue is not a meta-new form.
|
|
163
307
|
def record_meta_new(node, nesting)
|
|
164
308
|
call = node.value
|
|
165
309
|
return unless call.is_a?(Prism::CallNode)
|
|
@@ -167,8 +311,10 @@ module Rigor
|
|
|
167
311
|
recv = call.receiver
|
|
168
312
|
return unless recv.is_a?(Prism::ConstantReadNode) && META_NEW[recv.name.to_s] == call.name
|
|
169
313
|
|
|
170
|
-
|
|
314
|
+
fqn = (nesting + [node.name.to_s]).join("::")
|
|
315
|
+
@declarations << Declaration.new(fqn: fqn, path: @path,
|
|
171
316
|
line: node.location.start_line, superclass: nil, includes: [].freeze)
|
|
317
|
+
fqn
|
|
172
318
|
end
|
|
173
319
|
|
|
174
320
|
# `include` / `prepend` / `extend` argument names written directly in the declaration body. Only the
|
|
@@ -189,6 +335,10 @@ module Rigor
|
|
|
189
335
|
DYNAMIC_RESOLVERS = %i[constantize safe_constantize const_get].freeze
|
|
190
336
|
private_constant :DYNAMIC_RESOLVERS
|
|
191
337
|
|
|
338
|
+
# The anchor of a lookup that starts at the top level.
|
|
339
|
+
TOP_LEVEL = ""
|
|
340
|
+
private_constant :TOP_LEVEL
|
|
341
|
+
|
|
192
342
|
SUBJECT_SHAPES = {
|
|
193
343
|
Prism::StringNode => "a literal string",
|
|
194
344
|
Prism::SymbolNode => "a literal symbol",
|
|
@@ -198,59 +348,163 @@ module Rigor
|
|
|
198
348
|
|
|
199
349
|
# Rigor knows the argument's shape, which is the whole reason this can be tiered rather than treated
|
|
200
350
|
# as a blanket namespace poison: a literal argument names the exact constant and is as good as a
|
|
201
|
-
# written reference, while an interpolated one can only bound the
|
|
202
|
-
|
|
351
|
+
# written reference, while an interpolated one can only bound the names its literal head reaches.
|
|
352
|
+
#
|
|
353
|
+
# A local variable argument is read through its assignments in the same local scope, because the
|
|
354
|
+
# name is usually built one line up: GitLab's `Migration[2.2]` is `name = "V#{...}"` followed by
|
|
355
|
+
# `const_get(name, false)` (#1734).
|
|
356
|
+
def record_dynamic_use(node, nesting)
|
|
203
357
|
return unless DYNAMIC_RESOLVERS.include?(node.name)
|
|
204
358
|
|
|
205
359
|
subject = node.name == :const_get ? node.arguments&.arguments&.first : node.receiver
|
|
206
360
|
return if subject.nil?
|
|
207
361
|
|
|
208
|
-
name
|
|
362
|
+
anchors = node.name == :const_get ? const_get_anchors(node, nesting) : [TOP_LEVEL]
|
|
363
|
+
subjects(subject).each { |value| record_dynamic_subject(node, value, anchors) }
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def record_dynamic_subject(node, subject, anchors)
|
|
367
|
+
name, head = dynamic_target(subject)
|
|
209
368
|
# A literal whose bytes are not valid UTF-8 cannot name a constant; dropping it is the only safe
|
|
210
369
|
# answer, and carrying it forward crashed the whole run downstream.
|
|
211
370
|
return if subject.is_a?(Prism::StringNode) && name.nil?
|
|
212
371
|
return if subject.is_a?(Prism::SymbolNode) && name.nil?
|
|
213
372
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
373
|
+
reason = "#{node.name} on #{SUBJECT_SHAPES.fetch(subject.class, 'a computed value')}"
|
|
374
|
+
bounds = head ? anchors.filter_map { |anchor| bound(head, anchor) }.uniq : []
|
|
375
|
+
bounds = [[nil, :namespace, nil]] if bounds.empty?
|
|
376
|
+
bounds.each do |prefix, scope, within|
|
|
377
|
+
@dynamic_uses << DynamicUse.new(name: name, prefix: prefix, scope: scope, within: within,
|
|
378
|
+
site: site(node), path: @path, line: node.location.start_line,
|
|
379
|
+
reason: reason)
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
# Where a `const_get` looks a name up, as the namespaces its interpolated argument is bounded by. The
|
|
384
|
+
# receiver's namespace comes first: an implicit receiver, `self` or `self.class` is the enclosing
|
|
385
|
+
# declaration, and a constant receiver is resolved by {Graph} as any reference is (`within`). With
|
|
386
|
+
# the default `inherit = true` the lookup also falls through to the top level, which is all the
|
|
387
|
+
# reading had before, so that stays tainted too; an explicit `false` confines it to the receiver. An
|
|
388
|
+
# unknown receiver bounds nothing beyond the top level.
|
|
389
|
+
def const_get_anchors(node, nesting)
|
|
390
|
+
receiver = node.receiver
|
|
391
|
+
namespaces =
|
|
392
|
+
if self_receiver?(receiver)
|
|
393
|
+
# Inside a meta-new rvalue `self` is the declared class in its block but the outer scope in its
|
|
394
|
+
# arguments; the walk does not tell the two apart, so both are taken.
|
|
395
|
+
[@owner, nesting.join("::")].compact.uniq - [TOP_LEVEL]
|
|
396
|
+
elsif constant_receiver?(receiver)
|
|
397
|
+
[receiver_reference(receiver, nesting)]
|
|
398
|
+
else
|
|
399
|
+
[]
|
|
400
|
+
end
|
|
401
|
+
return [TOP_LEVEL] if namespaces.empty?
|
|
402
|
+
|
|
403
|
+
inherit = node.arguments.arguments[1]
|
|
404
|
+
inherit.is_a?(Prism::FalseNode) ? namespaces : namespaces + [TOP_LEVEL]
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def self_receiver?(receiver)
|
|
408
|
+
receiver.nil? || receiver.is_a?(Prism::SelfNode) ||
|
|
409
|
+
(receiver.is_a?(Prism::CallNode) && receiver.name == :class && receiver.receiver.is_a?(Prism::SelfNode))
|
|
218
410
|
end
|
|
219
411
|
|
|
220
|
-
# `
|
|
221
|
-
|
|
222
|
-
|
|
412
|
+
# `Object.const_get` is the top level, which every lookup already reaches.
|
|
413
|
+
def constant_receiver?(receiver)
|
|
414
|
+
return false unless receiver.is_a?(Prism::ConstantReadNode) || receiver.is_a?(Prism::ConstantPathNode)
|
|
415
|
+
|
|
416
|
+
as_written = Source::ConstantPath.qualified_name_or_nil(receiver)
|
|
417
|
+
!as_written.nil? && as_written.delete_prefix("::") != "Object"
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def receiver_reference(receiver, nesting)
|
|
421
|
+
Reference.new(as_written: Source::ConstantPath.qualified_name_or_nil(receiver),
|
|
422
|
+
nesting: nesting.dup.freeze, from: nil, role: @role, path: @path,
|
|
423
|
+
line: receiver.location.start_line, rooted: Source::ConstantPath.rooted?(receiver))
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
# `[prefix, scope, within]` for a literal head under one anchor. A head ending in `::` names a
|
|
427
|
+
# namespace and reaches everything beneath it; any other head is the start of a name, so
|
|
428
|
+
# `"V#{version}"` reaches `V1_0` and `V2_2` alike, and only a string-prefix match says that. A rooted
|
|
429
|
+
# head ignores the receiver. `within` carries a constant receiver for {Graph} to resolve.
|
|
430
|
+
def bound(head, anchor)
|
|
431
|
+
anchor = TOP_LEVEL if head.start_with?("::")
|
|
432
|
+
head = head.delete_prefix("::")
|
|
433
|
+
scope = head.end_with?("::") ? :namespace : :leading
|
|
434
|
+
head = head.delete_suffix("::")
|
|
435
|
+
return nil if head.empty?
|
|
436
|
+
|
|
437
|
+
case anchor
|
|
438
|
+
when Reference then [head, scope, anchor]
|
|
439
|
+
when TOP_LEVEL then [head, scope, nil]
|
|
440
|
+
else ["#{anchor}::#{head}", scope, nil]
|
|
441
|
+
end
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
# The values a dynamic-resolution subject can hold: the subject itself, or for a local variable every
|
|
445
|
+
# value assigned to it in the enclosing local scope. A local with an assignment whose value cannot be
|
|
446
|
+
# read off the node (`+=`, a multiple assignment, `rescue => name`) stays a computed value.
|
|
447
|
+
#
|
|
448
|
+
# A name an enclosing block binds as a parameter or block-local (`|name|`, `|x; name|`) is that
|
|
449
|
+
# block's value, not the method's: reading it through the method's assignments named a literal the
|
|
450
|
+
# block never sees, and a constant only that literal named was hidden from the report.
|
|
451
|
+
def subjects(subject)
|
|
452
|
+
return [subject] unless subject.is_a?(Prism::LocalVariableReadNode) && @local_scope
|
|
453
|
+
return [subject] if @block_params.include?(subject.name)
|
|
454
|
+
|
|
455
|
+
LocalValues.values(@local_scope, subject.name) || [subject]
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def in_local_scope(scope)
|
|
459
|
+
saved = [@local_scope, @block_params]
|
|
460
|
+
@local_scope = scope
|
|
461
|
+
@block_params = Set.new.freeze
|
|
462
|
+
yield
|
|
463
|
+
ensure
|
|
464
|
+
@local_scope, @block_params = saved
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
def in_block(block)
|
|
468
|
+
saved = @block_params
|
|
469
|
+
names = block.parameters ? LocalValues.bindings(block.parameters) : []
|
|
470
|
+
@block_params = (@block_params | names).freeze unless names.empty?
|
|
471
|
+
yield
|
|
472
|
+
ensure
|
|
473
|
+
@block_params = saved
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
# `[exact name, literal head]` for a dynamic-resolution subject. A literal names its constant exactly;
|
|
477
|
+
# an interpolation can only bound the names its literal head starts; anything else bounds nothing.
|
|
223
478
|
def dynamic_target(subject)
|
|
224
479
|
case subject
|
|
225
480
|
when Prism::StringNode, Prism::SymbolNode then [Scan.usable_name(subject.unescaped), nil]
|
|
226
|
-
when Prism::InterpolatedStringNode then [nil,
|
|
481
|
+
when Prism::InterpolatedStringNode then [nil, literal_head(subject)]
|
|
227
482
|
else [nil, nil]
|
|
228
483
|
end
|
|
229
484
|
end
|
|
230
485
|
|
|
231
|
-
# The literal head of an interpolated name: `"Foo::Bar::#{k}"` bounds the reach to `Foo::Bar`. Returns
|
|
232
|
-
# nil when the interpolation starts the string, which bounds nothing.
|
|
233
486
|
def site(node)
|
|
234
487
|
"#{@path}:#{node.location.start_line}"
|
|
235
488
|
end
|
|
236
489
|
|
|
237
|
-
|
|
490
|
+
# The literal head of an interpolated name, as written: `"Foo::Bar::#{k}"` gives `Foo::Bar::`. Returns
|
|
491
|
+
# nil when the interpolation starts the string, which bounds nothing.
|
|
492
|
+
def literal_head(node)
|
|
238
493
|
head = node.parts.first
|
|
239
494
|
return nil unless head.is_a?(Prism::StringNode)
|
|
240
495
|
|
|
241
496
|
literal = Scan.usable_name(head.unescaped)
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
trimmed = literal.sub(/::\z/, "")
|
|
245
|
-
trimmed.empty? ? nil : trimmed
|
|
497
|
+
literal.nil? || literal.empty? ? nil : literal
|
|
246
498
|
end
|
|
247
499
|
|
|
248
|
-
|
|
500
|
+
# `from` defaults to the innermost enclosing declaration; a declaration's own header (its superclass,
|
|
501
|
+
# its meta-new rvalue) is credited to that declaration instead.
|
|
502
|
+
def record_reference(node, nesting, from: @owner)
|
|
249
503
|
as_written = Source::ConstantPath.qualified_name_or_nil(node)
|
|
250
504
|
return if as_written.nil?
|
|
251
505
|
|
|
252
|
-
|
|
253
|
-
|
|
506
|
+
from ||= nesting.empty? ? nil : nesting.join("::")
|
|
507
|
+
@references << Reference.new(as_written: as_written, nesting: nesting.dup.freeze, from: from,
|
|
254
508
|
role: @role, path: @path, line: node.location.start_line,
|
|
255
509
|
rooted: Source::ConstantPath.rooted?(node))
|
|
256
510
|
end
|
|
@@ -292,6 +292,38 @@ module Rigor
|
|
|
292
292
|
since: "0.0.1"
|
|
293
293
|
),
|
|
294
294
|
|
|
295
|
+
CheckRules::RULE_DEPRECATED_RUBY2_KEYWORDS => Entry.new(
|
|
296
|
+
id: CheckRules::RULE_DEPRECATED_RUBY2_KEYWORDS,
|
|
297
|
+
summary: "A call to the ruby2_keywords family Ruby 4.1 deprecates, under an explicit target_ruby >= 4.1.",
|
|
298
|
+
fires_when: [
|
|
299
|
+
"`.rigor.yml` sets `target_ruby` explicitly to \"4.1\" or later.",
|
|
300
|
+
"The call reaches `Module#ruby2_keywords` (no receiver or `self` in a class / module body, a " \
|
|
301
|
+
"`class << self` body or a singleton method), top-level `ruby2_keywords` (at the top level, outside " \
|
|
302
|
+
"any block), or `Proc#ruby2_keywords` (a receiver typed `Proc`): removed in Ruby 4.4.",
|
|
303
|
+
"The call reaches `Hash.ruby2_keywords_hash?` or `Hash.ruby2_keywords_hash` (on `Hash` or a subclass " \
|
|
304
|
+
"RBS knows): removed in Ruby 4.5.",
|
|
305
|
+
"The same names through `send` / `__send__` with a literal Symbol."
|
|
306
|
+
],
|
|
307
|
+
does_not_fire_when: [
|
|
308
|
+
"`target_ruby` is absent, is the default, is below 4.1, or is \"latest\" (which names a parser, " \
|
|
309
|
+
"not a runtime).",
|
|
310
|
+
"A project or `pre_eval:` file defines a method by the same name anywhere.",
|
|
311
|
+
"The call sits in a block at the top level, in an `instance_eval` / `instance_exec` block, in an " \
|
|
312
|
+
"instance method of a class or module, or on an explicit receiver that is not `Proc` / `Hash`.",
|
|
313
|
+
"The call sits in a version-guard arm that cannot run on the stated Ruby, under a guard on " \
|
|
314
|
+
"`RUBY_VERSION` that cannot be decided, or after a guard that returns or raises."
|
|
315
|
+
],
|
|
316
|
+
suppression: "`# rigor:disable call.deprecated-ruby2-keywords` on the call line. To fix it instead, " \
|
|
317
|
+
"rewrite the delegation the flag serves to `(...)` or `(*args, **kwargs, &block)` first, then " \
|
|
318
|
+
"drop the `ruby2_keywords` call: removing it alone breaks keyword pass-through.",
|
|
319
|
+
severity_authored: :warning,
|
|
320
|
+
severity_by_profile: { lenient: :info, balanced: :warning, strict: :error },
|
|
321
|
+
# Each call names a known core method on a receiver whose class the syntax or the scope fixes, and a
|
|
322
|
+
# project definition of the name silences it. A file loaded only on an older Ruby is the residual.
|
|
323
|
+
evidence_tier: :high,
|
|
324
|
+
since: "0.4.2"
|
|
325
|
+
),
|
|
326
|
+
|
|
295
327
|
CheckRules::RULE_ALWAYS_RAISES => Entry.new(
|
|
296
328
|
id: CheckRules::RULE_ALWAYS_RAISES,
|
|
297
329
|
summary: "Call provably raises (today: Integer division-by-zero).",
|
|
@@ -552,6 +584,9 @@ module Rigor
|
|
|
552
584
|
],
|
|
553
585
|
does_not_fire_when: [
|
|
554
586
|
"Override raises or preserves visibility (only reductions break substitutability).",
|
|
587
|
+
"The method is one Ruby makes private wherever it is defined (`initialize`, " \
|
|
588
|
+
"`initialize_copy` / `_dup` / `_clone`, `respond_to_missing?`), so its section is not its " \
|
|
589
|
+
"runtime visibility (ADR-35 WD10).",
|
|
555
590
|
"The shadowed method lives on an RBS-known / third-party ancestor (RBS models only " \
|
|
556
591
|
"public/private; RBS-parent visibility is a deferred follow-on).",
|
|
557
592
|
"`def self.foo` singleton methods (visibility is instance-side only).",
|
|
@@ -578,6 +613,9 @@ module Rigor
|
|
|
578
613
|
does_not_fire_when: [
|
|
579
614
|
"Either side lacks an authored RBS signature (WD1 both-sides-authored gate).",
|
|
580
615
|
"The override narrows or preserves the return (covariant-safe).",
|
|
616
|
+
"`initialize` and the `initialize_copy` / `_dup` / `_clone` hooks: `Class#new` / `dup` / " \
|
|
617
|
+
"`clone` reach them on the receiver's own class and discard their return, outside " \
|
|
618
|
+
"substitutability (ADR-35 WD10).",
|
|
581
619
|
"The ancestor's return is `untyped` / `self` / an unbound generic (degrades to " \
|
|
582
620
|
"`Dynamic[Top]`, which accepts everything — FP-safe).",
|
|
583
621
|
"The subtype relationship between the two return types is not resolvable from loaded " \
|
|
@@ -609,6 +647,9 @@ module Rigor
|
|
|
609
647
|
does_not_fire_when: [
|
|
610
648
|
"Either side lacks an authored RBS signature (WD1 both-sides-authored gate).",
|
|
611
649
|
"The override widens or preserves the parameter (contravariant-safe).",
|
|
650
|
+
"`initialize` and the `initialize_copy` / `_dup` / `_clone` hooks: `Class#new` / `dup` / " \
|
|
651
|
+
"`clone` reach them only on the receiver's own class, so no parent-typed caller passes the " \
|
|
652
|
+
"parent's arguments (ADR-35 WD10).",
|
|
612
653
|
"Either side is overloaded (more than one method type — arm mapping is ambiguous).",
|
|
613
654
|
"The ancestor's parameter is `untyped` / an unbound generic / an interface (degrades " \
|
|
614
655
|
"to `Dynamic[Top]`, which is passable to anything — FP-safe).",
|
|
@@ -640,6 +681,8 @@ module Rigor
|
|
|
640
681
|
],
|
|
641
682
|
does_not_fire_when: [
|
|
642
683
|
"Later write is `nil` — the `@cache = nil` clear-idiom is allowlisted.",
|
|
684
|
+
"The class's RBS declares the ivar (`@var: T`, or the slot an `attr_*` member implies, own or " \
|
|
685
|
+
"inherited) and that type accepts both the first write and the later one.",
|
|
643
686
|
"Either side is Union / Dynamic / IntegerRange / a shape-varied carrier.",
|
|
644
687
|
"Writes live in different classes that happen to share an ivar name.",
|
|
645
688
|
"Writes are in a singleton body — `def self.foo` or a `class << self` def — those track " \
|
|
@@ -21,6 +21,9 @@ require_relative "../environment/default_libraries"
|
|
|
21
21
|
# lockfile paths without loading `rigor/environment` or the RBS machinery.
|
|
22
22
|
require_relative "../environment/lockfile_resolver"
|
|
23
23
|
require_relative "../environment/rbs_collection_discovery"
|
|
24
|
+
# A leaf file (it loads only the cache descriptor), unlike `dependency_source_inference`, whose walker loads the
|
|
25
|
+
# inference engine the boot-slimming probe exists to avoid.
|
|
26
|
+
require_relative "dependency_source_inference/gem_resolver"
|
|
24
27
|
require_relative "shadow_harness"
|
|
25
28
|
|
|
26
29
|
module Rigor
|
|
@@ -93,6 +96,7 @@ module Rigor
|
|
|
93
96
|
def descriptor(configuration:, files:, explain:, rbs_config_entries:, template_units_digest: nil)
|
|
94
97
|
Cache::Descriptor.new(
|
|
95
98
|
gems: [Cache::RbsDescriptor.rbs_gem_entry],
|
|
99
|
+
dependencies: dependency_source_entries(configuration),
|
|
96
100
|
configs: rbs_config_entries + engine_source_entries + lockfile_entries(configuration) +
|
|
97
101
|
template_unit_entries(template_units_digest) + shadow_harness_entries + [
|
|
98
102
|
config_entry("configuration", Marshal.dump(configuration.to_h)),
|
|
@@ -151,6 +155,14 @@ module Rigor
|
|
|
151
155
|
[lockfile_entry("bundler.lockfile", bundler), lockfile_entry("rbs_collection.lockfile", collection)]
|
|
152
156
|
end
|
|
153
157
|
|
|
158
|
+
# ADR-10 slice 3, issue #1814 — the `dependencies.source_inference` gems' `(name, version, mode)` rows, which the
|
|
159
|
+
# walker's output depends on and no lockfile digest covers when the project has none or the gem sits beside the
|
|
160
|
+
# bundle. Shared by this key and by {Cache::IncrementalSnapshot.fingerprint}, as the lockfiles are. A project
|
|
161
|
+
# that lists no gem adds no row, so its key is what it was.
|
|
162
|
+
def dependency_source_entries(configuration)
|
|
163
|
+
DependencySourceInference::GemResolver.dependency_entries(configuration.dependencies)
|
|
164
|
+
end
|
|
165
|
+
|
|
154
166
|
# The one resolution of the two dependency lockfiles — `[bundler, rbs_collection]`, each a Pathname or nil
|
|
155
167
|
# — shared by this key and by {Cache::IncrementalSnapshot.fingerprint} so the two caches identify the gem
|
|
156
168
|
# set by the same files (issue #1532): `bundler.lockfile:` / `rbs_collection.lockfile:` and auto-detection.
|
|
@@ -180,7 +192,10 @@ module Rigor
|
|
|
180
192
|
|
|
181
193
|
# The `rbs.libraries` config slot reconstructed from configuration alone — byte-identical to the
|
|
182
194
|
# loader's `RbsDescriptor.libraries_entry(loader.libraries)` because `Environment.for_project` merges
|
|
183
|
-
# exactly `DEFAULT_LIBRARIES + config.libraries` (uniq) into `loader.libraries
|
|
195
|
+
# exactly `DEFAULT_LIBRARIES + config.libraries` (uniq) into `loader.libraries`, plus the
|
|
196
|
+
# `Environment::RequiredFeatures` tokens the run's source activates (issue #1700). Those are left out
|
|
197
|
+
# here: reading every source file would cost the boot-slimming probe the time it exists to save, and a
|
|
198
|
+
# project that requires a gated feature then only misses this probe, never hits a wrong slot.
|
|
184
199
|
def libraries_config_entries(configuration)
|
|
185
200
|
merged = (Environment::DEFAULT_LIBRARIES + configuration.libraries.map(&:to_s)).uniq
|
|
186
201
|
[Cache::RbsDescriptor.libraries_entry(merged)]
|