rigortype 0.1.1 → 0.1.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/data/builtins/ruby_core/range.yml +6 -4
- data/data/builtins/ruby_core/string.yml +15 -10
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +116 -0
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +123 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +118 -0
- data/lib/rigor/analysis/check_rules.rb +346 -18
- data/lib/rigor/analysis/rule_catalog.rb +343 -0
- data/lib/rigor/analysis/runner.rb +2 -1
- data/lib/rigor/cli/diff_command.rb +169 -0
- data/lib/rigor/cli/explain_command.rb +129 -0
- data/lib/rigor/cli.rb +18 -1
- data/lib/rigor/configuration/severity_profile.rb +18 -3
- data/lib/rigor/configuration.rb +10 -4
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +104 -12
- data/lib/rigor/inference/scope_indexer.rb +171 -2
- data/lib/rigor/plugin/io_boundary.rb +92 -19
- data/lib/rigor/plugin/trust_policy.rb +30 -7
- data/lib/rigor/scope.rb +30 -5
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/scope.rbs +3 -0
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 47346567152367cb408115b6a33214e43468f2255bd632b4894d8c68164b8ada
|
|
4
|
+
data.tar.gz: 407d5cda9e08342670a4eeb05ae2c565d398866174d292c586793cb213a77fdb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f6ffecd573e1bbb387c861cbe3a6d909d217dfadef453a7a84339a4f7fd44e2c86dbbac54c8369298b56a2de3771c0f75c28cfe098cb895355a52155657f4f9d
|
|
7
|
+
data.tar.gz: 6f924aed4bc15581bae58ffa62a5d8194db3fabbdc3056f2b6cfc4f068f972063e0148f85df8b8ef3e5f5003fad35b4e9016e534ef604cbfc581d0cdab76bb7b
|
|
@@ -33,8 +33,9 @@ classes:
|
|
|
33
33
|
arity: -1
|
|
34
34
|
defined_at: references/ruby/range.c:2851
|
|
35
35
|
c_body_at: references/ruby/range.c:103
|
|
36
|
-
c_effects:
|
|
37
|
-
|
|
36
|
+
c_effects:
|
|
37
|
+
- mutate
|
|
38
|
+
purity: mutates_self
|
|
38
39
|
rbs:
|
|
39
40
|
- "(Elem from, Elem to, ?boolish exclude_end) -> void"
|
|
40
41
|
rbs_at: references/rbs/core/range.rbs:608
|
|
@@ -44,8 +45,9 @@ classes:
|
|
|
44
45
|
arity: 1
|
|
45
46
|
defined_at: references/ruby/range.c:2852
|
|
46
47
|
c_body_at: references/ruby/range.c:115
|
|
47
|
-
c_effects:
|
|
48
|
-
|
|
48
|
+
c_effects:
|
|
49
|
+
- mutate
|
|
50
|
+
purity: mutates_self
|
|
49
51
|
"==":
|
|
50
52
|
source: c
|
|
51
53
|
cfunc: range_eq
|
|
@@ -50,8 +50,9 @@ classes:
|
|
|
50
50
|
arity: 1
|
|
51
51
|
defined_at: references/ruby/string.c:12794
|
|
52
52
|
c_body_at: references/ruby/string.c:6548
|
|
53
|
-
c_effects:
|
|
54
|
-
|
|
53
|
+
c_effects:
|
|
54
|
+
- mutate
|
|
55
|
+
purity: mutates_self
|
|
55
56
|
rbs:
|
|
56
57
|
- "(string other_string) -> self"
|
|
57
58
|
rbs_at: references/rbs/core/string.rbs:4019
|
|
@@ -61,8 +62,9 @@ classes:
|
|
|
61
62
|
arity: 1
|
|
62
63
|
defined_at: references/ruby/string.c:12795
|
|
63
64
|
c_body_at: references/ruby/string.c:6548
|
|
64
|
-
c_effects:
|
|
65
|
-
|
|
65
|
+
c_effects:
|
|
66
|
+
- mutate
|
|
67
|
+
purity: mutates_self
|
|
66
68
|
"<=>":
|
|
67
69
|
source: c
|
|
68
70
|
cfunc: rb_str_cmp_m
|
|
@@ -1167,8 +1169,9 @@ classes:
|
|
|
1167
1169
|
arity: -1
|
|
1168
1170
|
defined_at: references/ruby/string.c:12904
|
|
1169
1171
|
c_body_at: references/ruby/string.c:10172
|
|
1170
|
-
c_effects:
|
|
1171
|
-
|
|
1172
|
+
c_effects:
|
|
1173
|
+
- mutate
|
|
1174
|
+
purity: mutates_self
|
|
1172
1175
|
rbs:
|
|
1173
1176
|
- "(nil) -> nil"
|
|
1174
1177
|
- "(?string? separator) -> self?"
|
|
@@ -1223,8 +1226,9 @@ classes:
|
|
|
1223
1226
|
arity: 1
|
|
1224
1227
|
defined_at: references/ruby/string.c:12909
|
|
1225
1228
|
c_body_at: references/ruby/string.c:11476
|
|
1226
|
-
c_effects:
|
|
1227
|
-
|
|
1229
|
+
c_effects:
|
|
1230
|
+
- mutate
|
|
1231
|
+
purity: mutates_self
|
|
1228
1232
|
rbs:
|
|
1229
1233
|
- "(string suffix) -> self?"
|
|
1230
1234
|
rbs_at: references/rbs/core/string.rbs:2664
|
|
@@ -1459,8 +1463,9 @@ classes:
|
|
|
1459
1463
|
arity: 1
|
|
1460
1464
|
defined_at: references/ruby/string.c:12937
|
|
1461
1465
|
c_body_at: references/ruby/string.c:11563
|
|
1462
|
-
c_effects:
|
|
1463
|
-
|
|
1466
|
+
c_effects:
|
|
1467
|
+
- mutate
|
|
1468
|
+
purity: mutates_self
|
|
1464
1469
|
rbs:
|
|
1465
1470
|
- "(encoding enc) -> self"
|
|
1466
1471
|
rbs_at: references/rbs/core/string.rbs:3207
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Analysis
|
|
7
|
+
module CheckRules
|
|
8
|
+
# Walks a parse tree and collects every `IfNode` / `UnlessNode`
|
|
9
|
+
# whose predicate folds to a `Type::Constant` AND is NOT
|
|
10
|
+
# disqualified by the rule's conservative envelope.
|
|
11
|
+
#
|
|
12
|
+
# The companion rule (`flow.always-truthy-condition`) is
|
|
13
|
+
# the inferred-constant counterpart to
|
|
14
|
+
# `flow.unreachable-branch` (which only fires on syntactic
|
|
15
|
+
# literals). The literal-only rule was the v0.1.2 first-cut
|
|
16
|
+
# because Rigor's incomplete loop / mutation / RBS-strictness
|
|
17
|
+
# modelling produces inferred constants that look real but
|
|
18
|
+
# are pragmatically false-positives. This collector adds two
|
|
19
|
+
# surgical skips to bring the inferred-constant case in
|
|
20
|
+
# without resurfacing those false positives:
|
|
21
|
+
#
|
|
22
|
+
# - **Inside a loop / block**: predicates nested inside
|
|
23
|
+
# `WhileNode` / `UntilNode` / `ForNode` / `BlockNode`
|
|
24
|
+
# ancestors. Mutation tracking through loop bodies is
|
|
25
|
+
# incomplete (`shift = 0; loop { shift += 7 }` keeps
|
|
26
|
+
# `shift` at `Constant<7>` per the analyzer), so a
|
|
27
|
+
# `Constant<bool>` predicate inside the loop body is
|
|
28
|
+
# suspect.
|
|
29
|
+
# - **Defensive predicate calls**: `.nil?` / `.empty?` /
|
|
30
|
+
# `.zero?` / `.any?` / `.none?` / `.all?`. These read
|
|
31
|
+
# like the user explicitly checking for a runtime
|
|
32
|
+
# condition that the type system already proves can't
|
|
33
|
+
# happen — Rigor's strict-on-returns RBS often disagrees
|
|
34
|
+
# with the user's defensive check (e.g. `Module#name`
|
|
35
|
+
# returns `String` per RBS but anonymous classes really
|
|
36
|
+
# do return `nil`). Skipping these forms keeps the rule
|
|
37
|
+
# useful for genuine logic errors without faulting
|
|
38
|
+
# defensive code.
|
|
39
|
+
#
|
|
40
|
+
# Also skipped (so the literal-only `unreachable-branch`
|
|
41
|
+
# rule doesn't double-fire alongside this one):
|
|
42
|
+
#
|
|
43
|
+
# - Predicates whose direct AST shape is a literal
|
|
44
|
+
# (`TrueNode` / `FalseNode` / `NilNode` / numeric / string
|
|
45
|
+
# / symbol / regexp).
|
|
46
|
+
class AlwaysTruthyConditionCollector
|
|
47
|
+
DEFENSIVE_PREDICATES = %i[nil? empty? zero? any? none? all? respond_to?].freeze
|
|
48
|
+
LOOP_OR_BLOCK_NODE_CLASSES = [
|
|
49
|
+
Prism::WhileNode, Prism::UntilNode, Prism::ForNode, Prism::BlockNode
|
|
50
|
+
].freeze
|
|
51
|
+
LITERAL_PREDICATE_NODE_CLASSES = [
|
|
52
|
+
Prism::TrueNode, Prism::FalseNode, Prism::NilNode,
|
|
53
|
+
Prism::IntegerNode, Prism::FloatNode,
|
|
54
|
+
Prism::StringNode, Prism::SymbolNode, Prism::RegularExpressionNode
|
|
55
|
+
].freeze
|
|
56
|
+
|
|
57
|
+
Result = Data.define(:node, :polarity)
|
|
58
|
+
|
|
59
|
+
# @return [Array<Result>] one entry per qualifying
|
|
60
|
+
# predicate. Empty when the tree carries no firing
|
|
61
|
+
# predicates.
|
|
62
|
+
def initialize(scope_index)
|
|
63
|
+
@scope_index = scope_index
|
|
64
|
+
@results = []
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def collect(root)
|
|
68
|
+
walk(root, in_loop_or_block: false)
|
|
69
|
+
@results.freeze
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def walk(node, in_loop_or_block:)
|
|
75
|
+
return unless node.is_a?(Prism::Node)
|
|
76
|
+
|
|
77
|
+
collect_predicate(node) if conditional_node?(node) && !in_loop_or_block
|
|
78
|
+
|
|
79
|
+
child_in_loop_or_block = in_loop_or_block || enters_loop_or_block?(node)
|
|
80
|
+
node.compact_child_nodes.each { |child| walk(child, in_loop_or_block: child_in_loop_or_block) }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def conditional_node?(node)
|
|
84
|
+
node.is_a?(Prism::IfNode) || node.is_a?(Prism::UnlessNode)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def enters_loop_or_block?(node)
|
|
88
|
+
LOOP_OR_BLOCK_NODE_CLASSES.any? { |klass| node.is_a?(klass) }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def collect_predicate(node)
|
|
92
|
+
predicate = node.predicate
|
|
93
|
+
return if literal_predicate?(predicate)
|
|
94
|
+
return if defensive_predicate?(predicate)
|
|
95
|
+
|
|
96
|
+
scope = @scope_index[node]
|
|
97
|
+
return if scope.nil?
|
|
98
|
+
|
|
99
|
+
predicate_type = scope.type_of(predicate)
|
|
100
|
+
return unless predicate_type.is_a?(Type::Constant)
|
|
101
|
+
|
|
102
|
+
polarity = predicate_type.value.nil? || predicate_type.value == false ? :falsey : :truthy
|
|
103
|
+
@results << Result.new(node: predicate, polarity: polarity)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def literal_predicate?(predicate)
|
|
107
|
+
LITERAL_PREDICATE_NODE_CLASSES.any? { |klass| predicate.is_a?(klass) }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def defensive_predicate?(predicate)
|
|
111
|
+
predicate.is_a?(Prism::CallNode) && DEFENSIVE_PREDICATES.include?(predicate.name)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Analysis
|
|
7
|
+
module CheckRules
|
|
8
|
+
# Walks a parse tree and collects every `LocalVariableWriteNode`
|
|
9
|
+
# inside a `DefNode` body whose target name is **never read**
|
|
10
|
+
# within the same body.
|
|
11
|
+
#
|
|
12
|
+
# The collector is the read-side companion to the
|
|
13
|
+
# `def.dead-assignment` rule. v0.1.2 ships the narrowest
|
|
14
|
+
# envelope that catches the most common typo / refactoring-
|
|
15
|
+
# leftover shape ("wrote and never used") without surfacing
|
|
16
|
+
# false positives:
|
|
17
|
+
#
|
|
18
|
+
# - Only `Prism::DefNode` bodies are scanned. Top-level
|
|
19
|
+
# scripts and class-body assignments are skipped — their
|
|
20
|
+
# variable surface bleeds across requires / introspection
|
|
21
|
+
# in ways the rule cannot reason about.
|
|
22
|
+
# - Only plain `LocalVariableWriteNode` writes are
|
|
23
|
+
# considered. Operator-writes (`x += 1`), and-/or-writes
|
|
24
|
+
# (`x ||= 1`), and `MultiWriteNode` destructures (`a, b =
|
|
25
|
+
# foo`) are skipped because their write semantics are
|
|
26
|
+
# intertwined with reads or with a wider tuple binding.
|
|
27
|
+
# - The "is this name ever read" question is answered by
|
|
28
|
+
# any `LocalVariableReadNode` anywhere in the def
|
|
29
|
+
# subtree — so a closure capture, a `return x`, a
|
|
30
|
+
# block-call argument, an interpolated string all count
|
|
31
|
+
# as a read.
|
|
32
|
+
# - A write whose name starts with `_` is skipped per the
|
|
33
|
+
# Ruby convention that `_` / `_foo` declares
|
|
34
|
+
# "intentionally unused".
|
|
35
|
+
# - The last statement of a def body is skipped — Ruby's
|
|
36
|
+
# implicit return treats `def foo; x = 1; end` as
|
|
37
|
+
# returning `1`, so the trailing write is intentional.
|
|
38
|
+
class DeadAssignmentCollector
|
|
39
|
+
# Returns `Array<{def_class:, def_name:, write_node:}>`
|
|
40
|
+
# — one entry per dead-assignment write. Empty when
|
|
41
|
+
# the tree has no qualifying writes.
|
|
42
|
+
def initialize(scope_index)
|
|
43
|
+
@scope_index = scope_index
|
|
44
|
+
@results = []
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def collect(root)
|
|
48
|
+
walk_for_def_nodes(root)
|
|
49
|
+
@results.freeze
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def walk_for_def_nodes(node)
|
|
55
|
+
return unless node.is_a?(Prism::Node)
|
|
56
|
+
|
|
57
|
+
collect_def_assignments(node) if node.is_a?(Prism::DefNode)
|
|
58
|
+
node.compact_child_nodes.each { |child| walk_for_def_nodes(child) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def collect_def_assignments(def_node)
|
|
62
|
+
body = def_node.body
|
|
63
|
+
return if body.nil?
|
|
64
|
+
|
|
65
|
+
read_names = gather_read_names(body)
|
|
66
|
+
last_node = trailing_statement(body)
|
|
67
|
+
|
|
68
|
+
gather_write_nodes(body).each do |write|
|
|
69
|
+
next if write.equal?(last_node)
|
|
70
|
+
next if write.name.to_s.start_with?("_")
|
|
71
|
+
next if read_names.include?(write.name)
|
|
72
|
+
|
|
73
|
+
@results << { def_node: def_node, write_node: write }
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def gather_read_names(node, accumulator = Set.new)
|
|
78
|
+
return accumulator unless node.is_a?(Prism::Node)
|
|
79
|
+
|
|
80
|
+
accumulator << node.name if node.is_a?(Prism::LocalVariableReadNode)
|
|
81
|
+
# Operator/and/or-writes implicitly read the prior
|
|
82
|
+
# binding — count them too so `x = 1; x ||= 2; x` /
|
|
83
|
+
# similar shapes don't trip the rule.
|
|
84
|
+
accumulator << node.name if reading_assignment?(node)
|
|
85
|
+
|
|
86
|
+
node.compact_child_nodes.each { |child| gather_read_names(child, accumulator) }
|
|
87
|
+
accumulator
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def reading_assignment?(node)
|
|
91
|
+
node.is_a?(Prism::LocalVariableOperatorWriteNode) ||
|
|
92
|
+
node.is_a?(Prism::LocalVariableAndWriteNode) ||
|
|
93
|
+
node.is_a?(Prism::LocalVariableOrWriteNode)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def gather_write_nodes(node, accumulator = [])
|
|
97
|
+
return accumulator unless node.is_a?(Prism::Node)
|
|
98
|
+
|
|
99
|
+
accumulator << node if node.is_a?(Prism::LocalVariableWriteNode)
|
|
100
|
+
# Don't recurse into nested DefNodes — their bodies
|
|
101
|
+
# carry their own dead-assignment scope and the
|
|
102
|
+
# outer walker visits them separately.
|
|
103
|
+
return accumulator if node.is_a?(Prism::DefNode) && !accumulator.last.equal?(node)
|
|
104
|
+
|
|
105
|
+
node.compact_child_nodes.each { |child| gather_write_nodes(child, accumulator) }
|
|
106
|
+
accumulator
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Returns the final statement of a body node, descending
|
|
110
|
+
# into wrappers Ruby preserves verbatim (`begin ... end`
|
|
111
|
+
# blocks). Used to skip the implicit-return write at the
|
|
112
|
+
# tail of a method body.
|
|
113
|
+
def trailing_statement(body)
|
|
114
|
+
case body
|
|
115
|
+
when Prism::StatementsNode then body.body.last
|
|
116
|
+
when Prism::BeginNode
|
|
117
|
+
body.statements ? trailing_statement(body.statements) : nil
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Analysis
|
|
7
|
+
module CheckRules
|
|
8
|
+
# Walks a parse tree and collects every
|
|
9
|
+
# `Prism::InstanceVariableWriteNode` inside an instance
|
|
10
|
+
# method body of a `Prism::ClassNode` / `Prism::ModuleNode`,
|
|
11
|
+
# grouped by (class qualified name, ivar name).
|
|
12
|
+
#
|
|
13
|
+
# The collector is the read-side companion to
|
|
14
|
+
# `Inference::ScopeIndexer#build_class_ivar_index`. The
|
|
15
|
+
# indexer unions the rvalue types into a single per-ivar
|
|
16
|
+
# carrier; this collector preserves the per-write list so
|
|
17
|
+
# the `def.ivar-write-mismatch` rule can compare each
|
|
18
|
+
# write's concrete class against the first.
|
|
19
|
+
#
|
|
20
|
+
# Skipped on purpose:
|
|
21
|
+
#
|
|
22
|
+
# - Singleton-method bodies (`def self.foo`). Their ivars
|
|
23
|
+
# live on the class object, not on instances.
|
|
24
|
+
# - Class-body ivar writes outside any def — the
|
|
25
|
+
# `Module#@var` surface is a separate slice the engine
|
|
26
|
+
# doesn't yet model.
|
|
27
|
+
# - Nested classes / modules / defs inside a method body
|
|
28
|
+
# are barriers, mirroring the indexer's
|
|
29
|
+
# `IVAR_BARRIER_NODES` policy.
|
|
30
|
+
class IvarWriteCollector
|
|
31
|
+
BARRIER_NODES = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode].freeze
|
|
32
|
+
private_constant :BARRIER_NODES
|
|
33
|
+
|
|
34
|
+
# Returns `Hash[class_name (String) => Hash[ivar_name
|
|
35
|
+
# (Symbol) => Array<{node:, type:}>]]`. Empty when the
|
|
36
|
+
# tree has no qualifying writes.
|
|
37
|
+
def initialize(scope_index)
|
|
38
|
+
@scope_index = scope_index
|
|
39
|
+
@accumulator = {}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def collect(root)
|
|
43
|
+
walk(root, [])
|
|
44
|
+
@accumulator.transform_values(&:freeze).freeze
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def walk(node, qualified_prefix)
|
|
50
|
+
return unless node.is_a?(Prism::Node)
|
|
51
|
+
|
|
52
|
+
case node
|
|
53
|
+
when Prism::ClassNode, Prism::ModuleNode
|
|
54
|
+
name = qualified_name_for(node.constant_path)
|
|
55
|
+
if name
|
|
56
|
+
walk(node.body, qualified_prefix + [name]) if node.body
|
|
57
|
+
return
|
|
58
|
+
end
|
|
59
|
+
when Prism::DefNode
|
|
60
|
+
collect_def_writes(node, qualified_prefix)
|
|
61
|
+
return
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
node.compact_child_nodes.each { |child| walk(child, qualified_prefix) }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def collect_def_writes(def_node, qualified_prefix)
|
|
68
|
+
return if def_node.body.nil? || qualified_prefix.empty?
|
|
69
|
+
return if def_node.receiver.is_a?(Prism::SelfNode)
|
|
70
|
+
|
|
71
|
+
class_name = qualified_prefix.join("::")
|
|
72
|
+
gather_writes(def_node.body, class_name)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def gather_writes(node, class_name)
|
|
76
|
+
return unless node.is_a?(Prism::Node)
|
|
77
|
+
|
|
78
|
+
record_write(node, class_name) if node.is_a?(Prism::InstanceVariableWriteNode)
|
|
79
|
+
return if BARRIER_NODES.any? { |klass| node.is_a?(klass) }
|
|
80
|
+
|
|
81
|
+
node.compact_child_nodes.each { |child| gather_writes(child, class_name) }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def record_write(node, class_name)
|
|
85
|
+
scope = @scope_index[node]
|
|
86
|
+
return if scope.nil?
|
|
87
|
+
|
|
88
|
+
rvalue_type = scope.type_of(node.value)
|
|
89
|
+
@accumulator[class_name] ||= {}
|
|
90
|
+
@accumulator[class_name][node.name] ||= []
|
|
91
|
+
@accumulator[class_name][node.name] << { node: node, type: rvalue_type }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Same shape resolution as `ScopeIndexer.qualified_name_for`
|
|
95
|
+
# (single-segment ConstantReadNode and dotted
|
|
96
|
+
# ConstantPathNode). Inlined here to keep the collector
|
|
97
|
+
# self-contained — the rule lives outside the indexer's
|
|
98
|
+
# private surface.
|
|
99
|
+
def qualified_name_for(constant_path_node)
|
|
100
|
+
case constant_path_node
|
|
101
|
+
when Prism::ConstantReadNode then constant_path_node.name.to_s
|
|
102
|
+
when Prism::ConstantPathNode then render_constant_path(constant_path_node)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def render_constant_path(node)
|
|
107
|
+
prefix =
|
|
108
|
+
case node.parent
|
|
109
|
+
when Prism::ConstantReadNode then "#{node.parent.name}::"
|
|
110
|
+
when Prism::ConstantPathNode then "#{render_constant_path(node.parent)}::"
|
|
111
|
+
else ""
|
|
112
|
+
end
|
|
113
|
+
"#{prefix}#{node.name}"
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|