rigortype 0.1.17 → 0.1.19
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 +159 -222
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +24 -1
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +25 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +29 -0
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +54 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +213 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +24 -1
- data/lib/rigor/analysis/check_rules.rb +275 -44
- data/lib/rigor/analysis/diagnostic.rb +8 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +581 -0
- data/lib/rigor/analysis/runner/pool_coordinator.rb +569 -0
- data/lib/rigor/analysis/runner/project_pre_passes.rb +321 -0
- data/lib/rigor/analysis/runner/run_snapshots.rb +46 -0
- data/lib/rigor/analysis/runner.rb +207 -1200
- data/lib/rigor/analysis/worker_session.rb +60 -11
- data/lib/rigor/bleeding_edge.rb +123 -0
- data/lib/rigor/cache/descriptor.rb +86 -8
- data/lib/rigor/cache/incremental_snapshot.rb +10 -4
- data/lib/rigor/cache/rbs_cache_producer.rb +5 -1
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +46 -13
- data/lib/rigor/cli/annotate_command.rb +100 -15
- data/lib/rigor/cli/check_command.rb +708 -0
- data/lib/rigor/cli/ci_detector.rb +94 -0
- data/lib/rigor/cli/diagnostic_formats.rb +345 -0
- data/lib/rigor/cli/plugins_command.rb +2 -4
- data/lib/rigor/cli/plugins_renderer.rb +0 -2
- data/lib/rigor/cli/prism_colorizer.rb +10 -3
- data/lib/rigor/cli/show_bleedingedge_command.rb +114 -0
- data/lib/rigor/cli/trace_command.rb +143 -0
- data/lib/rigor/cli/trace_renderer.rb +310 -0
- data/lib/rigor/cli/triage_command.rb +6 -3
- data/lib/rigor/cli/triage_renderer.rb +15 -1
- data/lib/rigor/cli.rb +21 -612
- data/lib/rigor/configuration/severity_profile.rb +13 -1
- data/lib/rigor/configuration.rb +66 -7
- data/lib/rigor/environment/rbs_loader.rb +78 -68
- data/lib/rigor/environment.rb +1 -1
- data/lib/rigor/inference/acceptance.rb +10 -0
- data/lib/rigor/inference/body_fixpoint.rb +89 -0
- data/lib/rigor/inference/budget_trace.rb +29 -2
- data/lib/rigor/inference/expression_typer.rb +1080 -105
- data/lib/rigor/inference/flow_tracer.rb +180 -0
- data/lib/rigor/inference/macro_block_self_type.rb +11 -12
- data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +60 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +54 -14
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +33 -1
- data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +281 -0
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +71 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +148 -10
- data/lib/rigor/inference/method_dispatcher.rb +187 -55
- data/lib/rigor/inference/method_parameter_binder.rb +56 -2
- data/lib/rigor/inference/multi_target_binder.rb +46 -3
- data/lib/rigor/inference/mutation_widening.rb +142 -0
- data/lib/rigor/inference/narrowing.rb +330 -37
- data/lib/rigor/inference/scope_indexer.rb +770 -39
- data/lib/rigor/inference/statement_evaluator.rb +998 -68
- data/lib/rigor/inference/synthetic_method_scanner.rb +1 -1
- data/lib/rigor/plugin/additional_initializer.rb +61 -38
- data/lib/rigor/plugin/base.rb +517 -120
- data/lib/rigor/plugin/macro/block_as_method.rb +22 -21
- data/lib/rigor/plugin/macro/nested_class_template.rb +9 -7
- data/lib/rigor/plugin/macro.rb +2 -3
- data/lib/rigor/plugin/manifest.rb +4 -24
- data/lib/rigor/plugin/node_rule_walk.rb +192 -0
- data/lib/rigor/plugin/registry.rb +264 -35
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/rbs_extended/conformance_checker.rb +86 -1
- data/lib/rigor/scope/discovery_index.rb +60 -0
- data/lib/rigor/scope.rb +199 -204
- data/lib/rigor/sig_gen/generator.rb +8 -0
- data/lib/rigor/sig_gen/observation_collector.rb +6 -6
- data/lib/rigor/source/literals.rb +14 -0
- data/lib/rigor/triage/catalogue.rb +4 -19
- data/lib/rigor/triage.rb +69 -1
- data/lib/rigor/type/combinator.rb +34 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +0 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +13 -29
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -32
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -2
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +27 -90
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +13 -30
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +2 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +90 -51
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -3
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -29
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +1 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -2
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +11 -40
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -2
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +10 -21
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +21 -34
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +11 -18
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +0 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +12 -2
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +37 -31
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +3 -23
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +8 -21
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +8 -29
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +17 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +2 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +108 -36
- data/sig/rigor/analysis/fact_store.rbs +3 -0
- data/sig/rigor/environment.rbs +0 -2
- data/sig/rigor/inference/builtins/method_catalog.rbs +1 -1
- data/sig/rigor/inference.rbs +5 -0
- data/sig/rigor/plugin/base.rbs +6 -4
- data/sig/rigor/plugin/manifest.rbs +1 -2
- data/sig/rigor/scope.rbs +50 -29
- data/sig/rigor/source.rbs +1 -0
- data/sig/rigor/type.rbs +1 -0
- data/sig/rigor.rbs +1 -1
- data/skills/rigor-baseline-reduce/references/01-classify.md +27 -0
- data/skills/rigor-ci-setup/SKILL.md +319 -0
- data/skills/rigor-plugin-author/SKILL.md +6 -4
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +22 -17
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +18 -1
- metadata +21 -3
- data/lib/rigor/cache/rbs_instance_definitions.rb +0 -66
- data/lib/rigor/plugin/macro/external_file.rb +0 -143
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Analysis
|
|
7
|
+
module CheckRules
|
|
8
|
+
# ADR-53 Track B (slice B3c) — hosts the main per-node check-rule
|
|
9
|
+
# pass on the shared {RuleWalk} instead of its own
|
|
10
|
+
# `Source::NodeWalker.each` traversal.
|
|
11
|
+
#
|
|
12
|
+
# The main pass is the stateless half of the catalogue: each rule
|
|
13
|
+
# reads only `scope_index[node]` and decides, with NO loop / block
|
|
14
|
+
# suppression and NO threaded context — so the collector declares no
|
|
15
|
+
# `RULE_WALK_GATES` and ignores the walk's `context`.
|
|
16
|
+
#
|
|
17
|
+
# The per-node dispatch itself stays in `CheckRules` (the verbatim
|
|
18
|
+
# `case` from `diagnose`'s former inline walk — only the traversal
|
|
19
|
+
# moved, ADR-53 WD4) and is handed in as a callable built in the
|
|
20
|
+
# `CheckRules` module context, so its calls to the private
|
|
21
|
+
# diagnostic builders remain implicit-self. The collector only owns
|
|
22
|
+
# the traversal hook and accumulation. Unlike the fact collectors
|
|
23
|
+
# its `#results` are the accumulated {Diagnostic}s, in the same
|
|
24
|
+
# emission order the inline `NodeWalker.each` produced them (the
|
|
25
|
+
# shared walk is the same visit-before-descend DFS over
|
|
26
|
+
# `compact_child_nodes`).
|
|
27
|
+
class MainPassCollector
|
|
28
|
+
# The node classes the former inline pass branched on. A plain
|
|
29
|
+
# `Prism::IfNode` covers ternaries and postfix `if` too (the
|
|
30
|
+
# legacy `when Prism::IfNode, Prism::UnlessNode` arm did the same).
|
|
31
|
+
NODE_CLASSES = [
|
|
32
|
+
Prism::CallNode, Prism::DefNode, Prism::IfNode, Prism::UnlessNode
|
|
33
|
+
].freeze
|
|
34
|
+
|
|
35
|
+
# @param node_diagnostics [#call] maps a `Prism::Node` to the
|
|
36
|
+
# array of diagnostics the main pass emits for it.
|
|
37
|
+
def initialize(node_diagnostics)
|
|
38
|
+
@node_diagnostics = node_diagnostics
|
|
39
|
+
@diagnostics = []
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# {RuleWalk} entry point: the per-node logic of the former inline
|
|
43
|
+
# `NodeWalker.each` `case`, invoked under the shared traversal.
|
|
44
|
+
def visit(node, _context = nil)
|
|
45
|
+
@diagnostics.concat(@node_diagnostics.call(node))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def results
|
|
49
|
+
@diagnostics
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Analysis
|
|
7
|
+
module CheckRules
|
|
8
|
+
# ADR-53 Track B — the engine-owned AST walk that hosts CheckRules'
|
|
9
|
+
# per-node collectors, so a file is traversed once for all of them
|
|
10
|
+
# instead of once per collector (the ADR-52 WD4 model applied to
|
|
11
|
+
# the built-in rules; this walk and {Plugin::NodeRuleWalk} converge
|
|
12
|
+
# in slice B4).
|
|
13
|
+
#
|
|
14
|
+
# The walk is a single visit-before-descend DFS over
|
|
15
|
+
# `compact_child_nodes`. It threads the union of the context the
|
|
16
|
+
# hosted collectors need — the per-collector hand-rolled walks each
|
|
17
|
+
# tracked some subset of it — in one immutable {Context} object,
|
|
18
|
+
# recomputed once per node during the single descent:
|
|
19
|
+
#
|
|
20
|
+
# - `in_loop_or_block` — whether the node is inside a loop / block
|
|
21
|
+
# body (the two flow collectors suppress collection there because
|
|
22
|
+
# mutation tracking through those is incomplete).
|
|
23
|
+
# - `qualified_prefix` — the enclosing class / module name stack
|
|
24
|
+
# ({IvarWriteCollector} builds its `class_name` from it).
|
|
25
|
+
# - `inside_def` — whether the node is lexically inside a `DefNode`
|
|
26
|
+
# ({IvarWriteCollector} collects only at a `def` that sits
|
|
27
|
+
# directly in a class / module body, never one nested in another
|
|
28
|
+
# `def`).
|
|
29
|
+
#
|
|
30
|
+
# A collector joins the walk by declaring `NODE_CLASSES` (the node
|
|
31
|
+
# classes it cares about — Prism node classes are leaves, so a
|
|
32
|
+
# class-equality dispatch matches the collectors' `is_a?` gates),
|
|
33
|
+
# optionally `RULE_WALK_GATES` (the {Context}-derived suppressions
|
|
34
|
+
# the walk applies before calling it), and `#visit(node, context)`
|
|
35
|
+
# with its per-node logic transplanted verbatim. Only the
|
|
36
|
+
# *traversal* merges here; each collector's gather / filter logic is
|
|
37
|
+
# unchanged from its legacy `#collect` walk (ADR-53 WD4).
|
|
38
|
+
#
|
|
39
|
+
# Equivalence is load-bearing: every hosted collector keeps its
|
|
40
|
+
# legacy single-collector `#collect` walk as the oracle, compared
|
|
41
|
+
# against this walk by the permanent `rule_walk_equivalence_spec`
|
|
42
|
+
# and, over whole corpora, by `RIGOR_SHADOW_RULE_WALK=1`
|
|
43
|
+
# (see `CheckRules.run_node_collectors`).
|
|
44
|
+
module RuleWalk
|
|
45
|
+
LOOP_OR_BLOCK_NODE_CLASSES = [
|
|
46
|
+
Prism::WhileNode, Prism::UntilNode, Prism::ForNode, Prism::BlockNode
|
|
47
|
+
].freeze
|
|
48
|
+
|
|
49
|
+
CLASS_OR_MODULE_NODE_CLASSES = [Prism::ClassNode, Prism::ModuleNode].freeze
|
|
50
|
+
|
|
51
|
+
# The immutable per-node descent context. Recomputed for each
|
|
52
|
+
# child from its parent's context as the walk descends; collectors
|
|
53
|
+
# read only the fields they declared a need for.
|
|
54
|
+
Context = Data.define(:in_loop_or_block, :qualified_prefix, :inside_def) do
|
|
55
|
+
def self.root
|
|
56
|
+
new(in_loop_or_block: false, qualified_prefix: [], inside_def: false)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Suppression gates a collector may declare via `RULE_WALK_GATES`.
|
|
61
|
+
# The walk skips a collector's `#visit` for a node when any of the
|
|
62
|
+
# collector's declared gates holds in that node's context — exactly
|
|
63
|
+
# reproducing the per-collector legacy walk's traversal suppression.
|
|
64
|
+
# Each gate names the {Context} predicate method that, when true,
|
|
65
|
+
# suppresses the visit:
|
|
66
|
+
#
|
|
67
|
+
# - `:loop_or_block` (`in_loop_or_block`) — inside a loop / block
|
|
68
|
+
# body, the two flow collectors' envelope.
|
|
69
|
+
# - `:inside_def` (`inside_def`) — lexically inside a `DefNode`;
|
|
70
|
+
# IvarWrite collects only at a `def` that sits directly in a
|
|
71
|
+
# class / module body (its legacy walk `return`s at the first
|
|
72
|
+
# `def` it meets, so a nested def is never reached).
|
|
73
|
+
GATE_CONTEXT_PREDICATES = {
|
|
74
|
+
loop_or_block: :in_loop_or_block,
|
|
75
|
+
inside_def: :inside_def
|
|
76
|
+
}.freeze
|
|
77
|
+
|
|
78
|
+
# A per-node driver over a fixed collector set: holds the compiled
|
|
79
|
+
# `node_class => [[collector, gates], …]` hook table and applies
|
|
80
|
+
# the dispatch + context-descent that {RuleWalk.run} performs, but
|
|
81
|
+
# one node at a time. This lets a foreign traversal (the converged
|
|
82
|
+
# {Plugin::NodeRuleWalk}, ADR-53 B4) drive the built-in collectors
|
|
83
|
+
# from inside its own single walk: it calls {#visit} on each node
|
|
84
|
+
# with that node's {Context}, and derives a child's context with
|
|
85
|
+
# {#descend}. The dispatch / gate / descend logic is identical to
|
|
86
|
+
# the standalone {RuleWalk.run} walk — only the traversal driving
|
|
87
|
+
# it differs, keeping diagnostics byte-identical (ADR-53 WD4).
|
|
88
|
+
class CollectorDriver
|
|
89
|
+
def initialize(collectors)
|
|
90
|
+
@hooks = RuleWalk.build_hooks(collectors)
|
|
91
|
+
freeze
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Dispatch `node` (under its own `context`) to every matching,
|
|
95
|
+
# un-gated collector. Mirrors {RuleWalk.dispatch}.
|
|
96
|
+
def visit(node, context)
|
|
97
|
+
matched = @hooks[node.class]
|
|
98
|
+
return if matched.nil?
|
|
99
|
+
|
|
100
|
+
matched.each do |collector, gates|
|
|
101
|
+
next if gates.any? { |predicate| context.public_send(predicate) }
|
|
102
|
+
|
|
103
|
+
collector.visit(node, context)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# The context the children of `node` descend under. Mirrors
|
|
108
|
+
# {RuleWalk.descend}.
|
|
109
|
+
def descend(node, context)
|
|
110
|
+
RuleWalk.descend(node, context)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
class << self
|
|
115
|
+
# Walks `root` once, dispatching each visited node to every
|
|
116
|
+
# collector whose `NODE_CLASSES` covers the node's class and
|
|
117
|
+
# whose declared `RULE_WALK_GATES` do not suppress it in that
|
|
118
|
+
# node's context.
|
|
119
|
+
def run(root, collectors)
|
|
120
|
+
hooks = build_hooks(collectors)
|
|
121
|
+
walk(root, hooks, Context.root)
|
|
122
|
+
collectors
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def build_hooks(collectors)
|
|
126
|
+
hooks = {}
|
|
127
|
+
collectors.each do |collector|
|
|
128
|
+
gates = collector_gates(collector)
|
|
129
|
+
collector.class::NODE_CLASSES.each do |node_class|
|
|
130
|
+
(hooks[node_class] ||= []) << [collector, gates]
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
hooks
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Computes the context the children of `node` descend under from
|
|
137
|
+
# the node's own context. `in_loop_or_block` latches on once a
|
|
138
|
+
# loop / block is entered; `qualified_prefix` extends only on a
|
|
139
|
+
# nameable class / module (an unnamed one descends with the same
|
|
140
|
+
# prefix, matching IvarWrite's fall-through); `inside_def`
|
|
141
|
+
# latches on once a `DefNode` is entered.
|
|
142
|
+
def descend(node, context)
|
|
143
|
+
in_loop_or_block = context.in_loop_or_block ||
|
|
144
|
+
LOOP_OR_BLOCK_NODE_CLASSES.any? { |klass| node.is_a?(klass) }
|
|
145
|
+
inside_def = context.inside_def || node.is_a?(Prism::DefNode)
|
|
146
|
+
qualified_prefix = extend_prefix(node, context.qualified_prefix)
|
|
147
|
+
|
|
148
|
+
Context.new(
|
|
149
|
+
in_loop_or_block: in_loop_or_block,
|
|
150
|
+
qualified_prefix: qualified_prefix,
|
|
151
|
+
inside_def: inside_def
|
|
152
|
+
)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
private
|
|
156
|
+
|
|
157
|
+
def collector_gates(collector)
|
|
158
|
+
klass = collector.class
|
|
159
|
+
return [] unless klass.const_defined?(:RULE_WALK_GATES)
|
|
160
|
+
|
|
161
|
+
klass::RULE_WALK_GATES.map { |gate| GATE_CONTEXT_PREDICATES.fetch(gate) }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def walk(node, hooks, context)
|
|
165
|
+
return unless node.is_a?(Prism::Node)
|
|
166
|
+
|
|
167
|
+
dispatch(node, hooks, context)
|
|
168
|
+
child_context = descend(node, context)
|
|
169
|
+
node.compact_child_nodes.each { |child| walk(child, hooks, child_context) }
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def dispatch(node, hooks, context)
|
|
173
|
+
matched = hooks[node.class]
|
|
174
|
+
return if matched.nil?
|
|
175
|
+
|
|
176
|
+
matched.each do |collector, gates|
|
|
177
|
+
next if gates.any? { |predicate| context.public_send(predicate) }
|
|
178
|
+
|
|
179
|
+
collector.visit(node, context)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def extend_prefix(node, prefix)
|
|
184
|
+
return prefix unless CLASS_OR_MODULE_NODE_CLASSES.any? { |klass| node.is_a?(klass) }
|
|
185
|
+
|
|
186
|
+
name = qualified_name_for(node.constant_path)
|
|
187
|
+
name ? prefix + [name] : prefix
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Same shape resolution as `IvarWriteCollector#qualified_name_for`
|
|
191
|
+
# and `ScopeIndexer.qualified_name_for` (single-segment
|
|
192
|
+
# ConstantReadNode and dotted ConstantPathNode).
|
|
193
|
+
def qualified_name_for(constant_path_node)
|
|
194
|
+
case constant_path_node
|
|
195
|
+
when Prism::ConstantReadNode then constant_path_node.name.to_s
|
|
196
|
+
when Prism::ConstantPathNode then render_constant_path(constant_path_node)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def render_constant_path(node)
|
|
201
|
+
prefix =
|
|
202
|
+
case node.parent
|
|
203
|
+
when Prism::ConstantReadNode then "#{node.parent.name}::"
|
|
204
|
+
when Prism::ConstantPathNode then "#{render_constant_path(node.parent)}::"
|
|
205
|
+
else ""
|
|
206
|
+
end
|
|
207
|
+
"#{prefix}#{node.name}"
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
@@ -55,23 +55,46 @@ module Rigor
|
|
|
55
55
|
# `else` never runs).
|
|
56
56
|
Result = Data.define(:clause, :body, :subject_name, :condition_source, :kind, :keyword)
|
|
57
57
|
|
|
58
|
+
# ADR-53 Track B — the node classes the shared {RuleWalk}
|
|
59
|
+
# dispatches to this collector, and the context gate under which
|
|
60
|
+
# the walk suppresses it (loop / block bodies — see the envelope
|
|
61
|
+
# above). The legacy walk's `!in_loop_or_block` guard is now the
|
|
62
|
+
# walk's `:loop_or_block` gate, applied before `#visit`.
|
|
63
|
+
NODE_CLASSES = [Prism::CaseNode, Prism::CaseMatchNode].freeze
|
|
64
|
+
RULE_WALK_GATES = [:loop_or_block].freeze
|
|
65
|
+
|
|
58
66
|
def initialize(scope_index)
|
|
59
67
|
@scope_index = scope_index
|
|
60
68
|
@results = []
|
|
61
69
|
end
|
|
62
70
|
|
|
71
|
+
# Legacy single-collector walk — kept as the oracle the
|
|
72
|
+
# ADR-53 Track B equivalence harness compares {RuleWalk}
|
|
73
|
+
# against; deleted when Track B completes.
|
|
63
74
|
# @return [Array<Result>] one entry per provably-dead `when` clause.
|
|
64
75
|
def collect(root)
|
|
65
76
|
walk(root, in_loop_or_block: false)
|
|
66
77
|
@results.freeze
|
|
67
78
|
end
|
|
68
79
|
|
|
80
|
+
# {RuleWalk} entry point: the per-node logic of the legacy walk,
|
|
81
|
+
# invoked under the same traversal contract. The `context` is
|
|
82
|
+
# unused — the loop / block suppression this collector relied on
|
|
83
|
+
# is the walk's `:loop_or_block` gate now.
|
|
84
|
+
def visit(node, _context = nil)
|
|
85
|
+
collect_case(node)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def results
|
|
89
|
+
@results.freeze
|
|
90
|
+
end
|
|
91
|
+
|
|
69
92
|
private
|
|
70
93
|
|
|
71
94
|
def walk(node, in_loop_or_block:)
|
|
72
95
|
return unless node.is_a?(Prism::Node)
|
|
73
96
|
|
|
74
|
-
|
|
97
|
+
visit(node) if case_like?(node) && !in_loop_or_block
|
|
75
98
|
|
|
76
99
|
child_in_loop_or_block = in_loop_or_block || enters_loop_or_block?(node)
|
|
77
100
|
node.compact_child_nodes.each { |child| walk(child, in_loop_or_block: child_in_loop_or_block) }
|