rigortype 0.4.2 → 0.4.3
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/docs/handbook/11-sig-gen.md +12 -5
- data/docs/manual/02-cli-reference.md +19 -1
- data/docs/manual/03-configuration.md +6 -4
- data/docs/manual/05-inspecting-types.md +50 -0
- data/docs/manual/12-caching.md +18 -1
- data/docs/manual/14-rails-quickstart.md +2 -2
- data/docs/manual/18-removing-dead-code.md +16 -9
- data/lib/rigor/analysis/check_rules/rule_ids.rb +7 -0
- data/lib/rigor/analysis/check_rules.rb +23 -22
- data/lib/rigor/analysis/effects_cache_probe.rb +1 -1
- data/lib/rigor/analysis/incremental_run_slot.rb +3 -3
- data/lib/rigor/analysis/incremental_session.rb +89 -22
- data/lib/rigor/analysis/path_expansion.rb +62 -6
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +124 -37
- data/lib/rigor/analysis/reachability/graph.rb +119 -15
- data/lib/rigor/analysis/reachability/scan.rb +332 -173
- data/lib/rigor/analysis/reachability/scan_cache.rb +4 -2
- data/lib/rigor/analysis/run_cache_probe.rb +1 -1
- data/lib/rigor/analysis/run_stats.rb +28 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +26 -1
- data/lib/rigor/analysis/runner/pool_coordinator.rb +28 -9
- data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -1
- data/lib/rigor/analysis/runner.rb +29 -11
- data/lib/rigor/analysis/worker_session.rb +1 -1
- data/lib/rigor/cache/engine_source.rb +21 -7
- data/lib/rigor/cache/incremental_snapshot.rb +36 -8
- data/lib/rigor/cli/check_command.rb +138 -39
- data/lib/rigor/cli/docs_command.rb +5 -4
- data/lib/rigor/cli/lens_command.rb +144 -0
- data/lib/rigor/cli/lsp_command.rb +5 -2
- data/lib/rigor/cli/skill_command.rb +3 -3
- data/lib/rigor/cli/skill_describe.rb +1 -1
- data/lib/rigor/cli/type_of_command.rb +2 -1
- data/lib/rigor/cli/unused_command.rb +157 -24
- data/lib/rigor/cli.rb +30 -4
- data/lib/rigor/configuration.rb +15 -0
- data/lib/rigor/environment/bundle_sig_discovery.rb +11 -7
- data/lib/rigor/environment/installed_gem_set.rb +38 -11
- data/lib/rigor/environment/lockfile_resolver.rb +37 -6
- data/lib/rigor/environment/missing_gem_constant_index.rb +22 -7
- data/lib/rigor/environment/rbs_loader.rb +33 -0
- data/lib/rigor/environment/required_features.rb +4 -2
- data/lib/rigor/environment.rb +12 -5
- data/lib/rigor/inference/builtins/cruby_definers.rb +22 -5
- data/lib/rigor/inference/captured_locals.rb +43 -3
- data/lib/rigor/inference/expression_typer.rb +58 -25
- data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +21 -17
- data/lib/rigor/inference/match_rebinding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/argument_nodes.rb +25 -0
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +3 -2
- data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +3 -3
- data/lib/rigor/inference/method_dispatcher/optional_count.rb +63 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +4 -16
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -7
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +8 -26
- data/lib/rigor/inference/method_dispatcher/unit_denominator_rational.rb +67 -0
- data/lib/rigor/inference/method_dispatcher.rb +52 -3
- data/lib/rigor/inference/project_patched_definedness_guard.rb +86 -0
- data/lib/rigor/inference/project_patched_methods.rb +2 -2
- data/lib/rigor/inference/project_patched_scanner.rb +38 -24
- data/lib/rigor/inference/refined_dispatch/class_object.rb +350 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +112 -40
- data/lib/rigor/inference/refined_dispatch.rb +62 -14
- data/lib/rigor/inference/repeated_or_writes.rb +2 -0
- data/lib/rigor/inference/scope_indexer.rb +140 -5
- data/lib/rigor/inference/statement_evaluator.rb +223 -31
- data/lib/rigor/inference/untyped_rejoin.rb +75 -0
- data/lib/rigor/language_server/project_context.rb +56 -5
- data/lib/rigor/lens/anchor.rb +115 -0
- data/lib/rigor/lens/contract_reader.rb +118 -0
- data/lib/rigor/lens/document.rb +113 -0
- data/lib/rigor/lens/file_reader.rb +433 -0
- data/lib/rigor/lens/members.rb +153 -0
- data/lib/rigor/lens/parameters.rb +91 -0
- data/lib/rigor/lens/plugins.rb +40 -0
- data/lib/rigor/lens/run.rb +280 -0
- data/lib/rigor/lens/slot.rb +79 -0
- data/lib/rigor/lens.rb +40 -0
- data/lib/rigor/project_environment.rb +20 -1
- data/lib/rigor/protection/mutation_cache.rb +2 -1
- data/lib/rigor/scope.rb +22 -11
- data/lib/rigor/sig_gen/generator.rb +45 -5
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +29 -3
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +5 -1
- data/sig/rigor/scope.rbs +3 -0
- data/sig/rigor.rbs +6 -3
- data/skills/rigor-project-init/references/02-configure.md +2 -2
- metadata +18 -1
|
@@ -5,6 +5,121 @@ require_relative "scan"
|
|
|
5
5
|
module Rigor
|
|
6
6
|
module Analysis
|
|
7
7
|
module Reachability
|
|
8
|
+
# ADR-102 WD4 — how {Graph} reads a `constantize` / `const_get` site: a literal name becomes a reference
|
|
9
|
+
# looked up where Ruby looks it up, and an interpolated one on a known receiver is anchored there.
|
|
10
|
+
module DynamicLookup
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
# A literal-argument `"Foo::Bar".constantize` names its constant exactly, so it is a REFERENCE, not an
|
|
14
|
+
# unknown. Keeping this distinct from {Graph#tainted} is what stops the tier being a blanket namespace
|
|
15
|
+
# poison — Rigor knows the argument's shape, and a type-free indexer does not.
|
|
16
|
+
#
|
|
17
|
+
# A literal `const_get` on a known receiver is looked up where Ruby looks it up (#1761): the receiver,
|
|
18
|
+
# then its ancestors unless the call passes `inherit = false`, then the top level unless it does. A
|
|
19
|
+
# receiver holding the name is the answer — resolving it anywhere else too would make a dead same-named
|
|
20
|
+
# constant reachable; past the receiver see {#literal_targets}. A receiver that does not resolve as a
|
|
21
|
+
# whole is unknown — `Outer::Missing` peeled to `Outer` may alias anything, and `const_get` never
|
|
22
|
+
# searches `Outer` — so it falls back to the top level as an unknown receiver does.
|
|
23
|
+
#
|
|
24
|
+
# The reference carries the referring file's role (WD8): a spec's `const_get("Foo")` keeps `Foo`
|
|
25
|
+
# test-reachable, as its written `Foo` would, rather than making it live in production.
|
|
26
|
+
def literal_dynamic_references(dynamic_uses)
|
|
27
|
+
dynamic_uses.flat_map do |use|
|
|
28
|
+
next [] if use.name.nil?
|
|
29
|
+
|
|
30
|
+
name = use.name.delete_prefix("::")
|
|
31
|
+
targets = use.within ? literal_targets(use, name) : [nil]
|
|
32
|
+
targets.map do |target|
|
|
33
|
+
Scan::Reference.new(as_written: target || name, nesting: [].freeze, from: nil,
|
|
34
|
+
role: use.role, path: use.path, line: use.line,
|
|
35
|
+
rooted: !target.nil? || use.name.start_with?("::"))
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The declarations a literal `name` can name when looked up from `use.within`, a member's owner peeled
|
|
41
|
+
# as {#resolve} peels it; nil stands for the top level. The receiver declaring it is the answer. Past
|
|
42
|
+
# the receiver the walk only approximates Ruby: an ancestor named as written may resolve elsewhere
|
|
43
|
+
# (`class Api::Thing < Base` inherits `::Base`, not `Api::Base`), so every scope an ancestor could be
|
|
44
|
+
# that declares the name is kept, and the top level with them. Keeping a dead constant reachable is
|
|
45
|
+
# the cheaper mistake than listing a live one. A receiver that does not resolve is the top level; with
|
|
46
|
+
# `inherit = false` a resolved receiver that does not declare the name is nothing (Ruby raises
|
|
47
|
+
# `NameError` there).
|
|
48
|
+
def literal_targets(use, name)
|
|
49
|
+
receiver = use.receiver { |ref| resolve_ref(ref) }
|
|
50
|
+
return [nil] if receiver.nil?
|
|
51
|
+
|
|
52
|
+
local = @shadows[use.path]
|
|
53
|
+
own = declared_member(receiver, name, local)
|
|
54
|
+
return [own] if own
|
|
55
|
+
return [] unless use.inherit
|
|
56
|
+
|
|
57
|
+
plausible_ancestors(receiver).filter_map { |scope| declared_member(scope, name, local) }.uniq + [nil]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# `scope::name`, or the longest declared prefix of it, when one is declared.
|
|
61
|
+
def declared_member(scope, name, local)
|
|
62
|
+
segments = name.split("::")
|
|
63
|
+
segments.length.downto(1) do |count|
|
|
64
|
+
candidate = "#{scope}::#{segments.first(count).join('::')}"
|
|
65
|
+
return candidate if declared?(candidate, nil, local)
|
|
66
|
+
end
|
|
67
|
+
nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Every declaration an ancestor of `fqn` could be: each as-written superclass or mixin resolved at
|
|
71
|
+
# every enclosing scope that declares it, not only the innermost, and transitively.
|
|
72
|
+
def plausible_ancestors(fqn)
|
|
73
|
+
(@plausible_ancestors ||= {})[fqn] ||= plausible_ancestor_walk(fqn)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def plausible_ancestor_walk(fqn)
|
|
77
|
+
seen = Set[fqn]
|
|
78
|
+
queue = [fqn]
|
|
79
|
+
out = []
|
|
80
|
+
until queue.empty?
|
|
81
|
+
current = queue.shift
|
|
82
|
+
@by_fqn.fetch(current, []).each do |decl|
|
|
83
|
+
(decl.includes + [decl.superclass]).compact.each do |raw|
|
|
84
|
+
plausible_resolutions(current, raw).each do |resolved|
|
|
85
|
+
next if seen.include?(resolved)
|
|
86
|
+
|
|
87
|
+
seen << resolved
|
|
88
|
+
out << resolved
|
|
89
|
+
queue << resolved
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
out.freeze
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def plausible_resolutions(subclass_fqn, raw)
|
|
98
|
+
segments = subclass_fqn.split("::")
|
|
99
|
+
(segments.length - 1).downto(0).filter_map do |i|
|
|
100
|
+
candidate = (segments[0, i] + [raw]).join("::")
|
|
101
|
+
candidate if candidate != subclass_fqn && @by_fqn.key?(candidate)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# The namespaces a `const_get` on `anchors` searches before the top level: the receiver, then, with the
|
|
106
|
+
# default `inherit = true`, every scope its superclass chain and mixins could be (#1761) — a taint is
|
|
107
|
+
# safe to over-reach.
|
|
108
|
+
def receiver_scopes(use, anchors)
|
|
109
|
+
return anchors unless use.inherit
|
|
110
|
+
|
|
111
|
+
anchors.flat_map { |anchor| [anchor, *plausible_ancestors(anchor)] }.uniq
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# An interpolated site on a known receiver, with the receiver resolved into absolute `prefix`es.
|
|
115
|
+
def anchored(use)
|
|
116
|
+
return [use] if use.within.nil? || use.prefix.nil?
|
|
117
|
+
|
|
118
|
+
anchors = use.anchors { |ref| resolve_ref(ref) }
|
|
119
|
+
receiver_scopes(use, anchors).map { |scope| use.with(prefix: "#{scope}::#{use.prefix}", within: nil) }
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
8
123
|
# ADR-102 — the cross-file half: resolves every as-written reference to a declaration, then marks from the
|
|
9
124
|
# root set. Pure data; no engine coupling, so `rigor check`'s diagnostic stream is untouched by
|
|
10
125
|
# construction rather than by a gate (WD1).
|
|
@@ -15,6 +130,8 @@ module Rigor
|
|
|
15
130
|
# believe in. The two walks are separate implementations (this one is name-level and needs no types), so
|
|
16
131
|
# `spec/rigor/analysis/reachability/graph_spec.rb` pins them to the same answers on shared fixtures.
|
|
17
132
|
class Graph
|
|
133
|
+
include DynamicLookup
|
|
134
|
+
|
|
18
135
|
Candidate = Data.define(:fqn, :path, :line)
|
|
19
136
|
|
|
20
137
|
# `test_only` is its own list, not a flag on `candidates`: a candidate is by definition unreachable, so
|
|
@@ -40,7 +157,6 @@ module Rigor
|
|
|
40
157
|
def initialize(declarations:, references:, root_fqns: [], dynamic_uses: [], shadows: [],
|
|
41
158
|
foreign: ->(_fqn) { false })
|
|
42
159
|
@declarations = declarations
|
|
43
|
-
@references = references + literal_dynamic_references(dynamic_uses)
|
|
44
160
|
@root_fqns = root_fqns.to_set
|
|
45
161
|
@foreign = foreign
|
|
46
162
|
@by_fqn = declarations.group_by(&:fqn)
|
|
@@ -48,8 +164,9 @@ module Rigor
|
|
|
48
164
|
@shadows.default = Set.new.freeze
|
|
49
165
|
@owned = @by_fqn.keys.reject { |fqn| @foreign.call(fqn) }.to_set
|
|
50
166
|
@ancestors = {}
|
|
167
|
+
@references = references + literal_dynamic_references(dynamic_uses)
|
|
51
168
|
# `Foo.const_get("V#{k}")` bounds its reach by `Foo`, a reference like any other (#1734).
|
|
52
|
-
@dynamic_uses = dynamic_uses.flat_map { |use|
|
|
169
|
+
@dynamic_uses = dynamic_uses.flat_map { |use| anchored(use) }
|
|
53
170
|
end
|
|
54
171
|
|
|
55
172
|
def report
|
|
@@ -87,19 +204,6 @@ module Rigor
|
|
|
87
204
|
namespaces: namespaces.size, roots: production_seeds.size, edges: edges.size)
|
|
88
205
|
end
|
|
89
206
|
|
|
90
|
-
# A literal-argument `"Foo::Bar".constantize` names its constant exactly, so it is a REFERENCE, not an
|
|
91
|
-
# unknown. Keeping this distinct from the taint below is what stops the tier being a blanket namespace
|
|
92
|
-
# poison — Rigor knows the argument's shape, and a type-free indexer does not.
|
|
93
|
-
def literal_dynamic_references(dynamic_uses)
|
|
94
|
-
dynamic_uses.filter_map do |use|
|
|
95
|
-
next if use.name.nil?
|
|
96
|
-
|
|
97
|
-
Scan::Reference.new(as_written: use.name.sub(/\A::/, ""), nesting: [].freeze, from: nil,
|
|
98
|
-
role: :production, path: use.path, line: use.line,
|
|
99
|
-
rooted: use.name.start_with?("::"))
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
|
|
103
207
|
# `{fqn => reason}` for every declaration in `fqns` a dynamic site could still be naming. A site with a
|
|
104
208
|
# literal prefix taints that namespace and everything under it; a site with no prefix at all cannot be
|
|
105
209
|
# bounded, so it taints nothing rather than everything — poisoning the whole project would empty the
|