rigortype 0.1.0 → 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/README.md +7 -2
- data/data/builtins/ruby_core/array.yml +6 -6
- data/data/builtins/ruby_core/hash.yml +1 -1
- data/data/builtins/ruby_core/io.yml +3 -3
- data/data/builtins/ruby_core/numeric.yml +1 -1
- data/data/builtins/ruby_core/pathname.yml +100 -100
- data/data/builtins/ruby_core/proc.yml +1 -1
- data/data/builtins/ruby_core/range.yml +6 -4
- data/data/builtins/ruby_core/string.yml +15 -10
- data/data/builtins/ruby_core/time.yml +3 -3
- 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 +90 -6
- data/lib/rigor/builtins/regex_refinement.rb +104 -0
- data/lib/rigor/cli/diff_command.rb +169 -0
- data/lib/rigor/cli/explain_command.rb +129 -0
- data/lib/rigor/cli/type_of_command.rb +3 -3
- data/lib/rigor/cli/type_scan_command.rb +4 -4
- data/lib/rigor/cli.rb +29 -5
- data/lib/rigor/configuration/severity_profile.rb +18 -3
- data/lib/rigor/configuration.rb +186 -13
- data/lib/rigor/environment.rb +12 -4
- data/lib/rigor/inference/expression_typer.rb +3 -1
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +31 -0
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +43 -2
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +104 -12
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +68 -2
- data/lib/rigor/inference/method_dispatcher.rb +50 -1
- data/lib/rigor/inference/narrowing.rb +150 -6
- data/lib/rigor/inference/scope_indexer.rb +220 -17
- data/lib/rigor/inference/statement_evaluator.rb +29 -0
- data/lib/rigor/plugin/base.rb +43 -0
- data/lib/rigor/plugin/fact_store.rb +92 -0
- data/lib/rigor/plugin/io_boundary.rb +92 -19
- data/lib/rigor/plugin/load_error.rb +14 -2
- data/lib/rigor/plugin/loader.rb +116 -0
- data/lib/rigor/plugin/manifest.rb +75 -6
- data/lib/rigor/plugin/services.rb +14 -2
- data/lib/rigor/plugin/trust_policy.rb +30 -7
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/scope.rb +30 -5
- data/lib/rigor/trinary.rb +1 -1
- data/lib/rigor/type/integer_range.rb +6 -2
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/environment.rbs +3 -2
- data/sig/rigor/scope.rbs +3 -0
- data/sig/rigor.rbs +8 -2
- metadata +9 -1
|
@@ -6,6 +6,9 @@ require_relative "../reflection"
|
|
|
6
6
|
require_relative "../source/node_walker"
|
|
7
7
|
require_relative "../type"
|
|
8
8
|
require_relative "diagnostic"
|
|
9
|
+
require_relative "check_rules/always_truthy_condition_collector"
|
|
10
|
+
require_relative "check_rules/dead_assignment_collector"
|
|
11
|
+
require_relative "check_rules/ivar_write_collector"
|
|
9
12
|
|
|
10
13
|
module Rigor
|
|
11
14
|
module Analysis
|
|
@@ -60,7 +63,12 @@ module Rigor
|
|
|
60
63
|
RULE_DUMP_TYPE = "dump.type"
|
|
61
64
|
RULE_ASSERT_TYPE = "assert.type-mismatch"
|
|
62
65
|
RULE_ALWAYS_RAISES = "flow.always-raises"
|
|
66
|
+
RULE_UNREACHABLE_BRANCH = "flow.unreachable-branch"
|
|
63
67
|
RULE_RETURN_TYPE = "def.return-type-mismatch"
|
|
68
|
+
RULE_VISIBILITY_MISMATCH = "def.method-visibility-mismatch"
|
|
69
|
+
RULE_IVAR_WRITE_MISMATCH = "def.ivar-write-mismatch"
|
|
70
|
+
RULE_DEAD_ASSIGNMENT = "flow.dead-assignment"
|
|
71
|
+
RULE_ALWAYS_TRUTHY_CONDITION = "flow.always-truthy-condition"
|
|
64
72
|
|
|
65
73
|
ALL_RULES = [
|
|
66
74
|
RULE_UNDEFINED_METHOD,
|
|
@@ -70,7 +78,12 @@ module Rigor
|
|
|
70
78
|
RULE_DUMP_TYPE,
|
|
71
79
|
RULE_ASSERT_TYPE,
|
|
72
80
|
RULE_ALWAYS_RAISES,
|
|
73
|
-
|
|
81
|
+
RULE_UNREACHABLE_BRANCH,
|
|
82
|
+
RULE_DEAD_ASSIGNMENT,
|
|
83
|
+
RULE_ALWAYS_TRUTHY_CONDITION,
|
|
84
|
+
RULE_RETURN_TYPE,
|
|
85
|
+
RULE_VISIBILITY_MISMATCH,
|
|
86
|
+
RULE_IVAR_WRITE_MISMATCH
|
|
74
87
|
].freeze
|
|
75
88
|
|
|
76
89
|
# Backward-compat alias table (ADR-8 § "Backward
|
|
@@ -88,7 +101,12 @@ module Rigor
|
|
|
88
101
|
"possible-nil-receiver" => RULE_NIL_RECEIVER,
|
|
89
102
|
"dump-type" => RULE_DUMP_TYPE,
|
|
90
103
|
"assert-type" => RULE_ASSERT_TYPE,
|
|
91
|
-
"always-raises" => RULE_ALWAYS_RAISES
|
|
104
|
+
"always-raises" => RULE_ALWAYS_RAISES,
|
|
105
|
+
"unreachable-branch" => RULE_UNREACHABLE_BRANCH,
|
|
106
|
+
"method-visibility-mismatch" => RULE_VISIBILITY_MISMATCH,
|
|
107
|
+
"ivar-write-mismatch" => RULE_IVAR_WRITE_MISMATCH,
|
|
108
|
+
"dead-assignment" => RULE_DEAD_ASSIGNMENT,
|
|
109
|
+
"always-truthy-condition" => RULE_ALWAYS_TRUTHY_CONDITION
|
|
92
110
|
}.freeze
|
|
93
111
|
|
|
94
112
|
# Family wildcard — a `<family>` token in a suppression
|
|
@@ -124,13 +142,20 @@ module Rigor
|
|
|
124
142
|
def diagnose(path:, root:, scope_index:, comments: [], disabled_rules: [])
|
|
125
143
|
diagnostics = []
|
|
126
144
|
Source::NodeWalker.each(root) do |node|
|
|
127
|
-
|
|
145
|
+
case node
|
|
146
|
+
when Prism::CallNode
|
|
128
147
|
diagnostics.concat(call_node_diagnostics(path, node, scope_index))
|
|
129
|
-
|
|
148
|
+
when Prism::DefNode
|
|
130
149
|
return_diagnostic = return_type_mismatch_diagnostic(path, node, scope_index)
|
|
131
150
|
diagnostics << return_diagnostic if return_diagnostic
|
|
151
|
+
when Prism::IfNode, Prism::UnlessNode
|
|
152
|
+
unreachable = unreachable_branch_diagnostic(path, node, scope_index)
|
|
153
|
+
diagnostics << unreachable if unreachable
|
|
132
154
|
end
|
|
133
155
|
end
|
|
156
|
+
diagnostics.concat(always_truthy_condition_diagnostics(path, root, scope_index))
|
|
157
|
+
diagnostics.concat(ivar_write_mismatch_diagnostics(path, root, scope_index))
|
|
158
|
+
diagnostics.concat(dead_assignment_diagnostics(path, root, scope_index))
|
|
134
159
|
filter_suppressed(diagnostics, comments: comments, disabled_rules: disabled_rules)
|
|
135
160
|
end
|
|
136
161
|
|
|
@@ -142,17 +167,92 @@ module Rigor
|
|
|
142
167
|
nil_receiver_diagnostic(path, node, scope_index),
|
|
143
168
|
dump_type_diagnostic(path, node, scope_index),
|
|
144
169
|
assert_type_diagnostic(path, node, scope_index),
|
|
145
|
-
always_raises_diagnostic(path, node, scope_index)
|
|
170
|
+
always_raises_diagnostic(path, node, scope_index),
|
|
171
|
+
visibility_mismatch_diagnostic(path, node, scope_index)
|
|
146
172
|
].compact
|
|
147
173
|
end
|
|
148
174
|
|
|
149
|
-
# v0.
|
|
175
|
+
# v0.1.2 — `def.ivar-write-mismatch`. Walks every
|
|
176
|
+
# ClassNode / ModuleNode body, gathers per-class ivar
|
|
177
|
+
# writes with their rvalue types, and emits a diagnostic
|
|
178
|
+
# when a later write's concrete class disagrees with the
|
|
179
|
+
# first write's. The first write per (class, ivar) is
|
|
180
|
+
# treated as the "declared" type; subsequent writes that
|
|
181
|
+
# land on a different concrete class trigger.
|
|
182
|
+
#
|
|
183
|
+
# Conservative envelope:
|
|
184
|
+
# - Only fires when both the first and the offending
|
|
185
|
+
# write resolve to a `concrete_class_name` (Nominal /
|
|
186
|
+
# Singleton / Constant / Tuple → "Array" / HashShape →
|
|
187
|
+
# "Hash"). Unions / Dynamic / IntegerRange / shape-
|
|
188
|
+
# varied carriers fall through.
|
|
189
|
+
# - `NilClass` is an intentional widening idiom (`@x =
|
|
190
|
+
# "value"` then later `@x = nil` to "clear") — skipped.
|
|
191
|
+
# - Singleton-method (`def self.foo`) bodies are skipped.
|
|
192
|
+
# Class-level ivars (`@x = 1` outside any def, in the
|
|
193
|
+
# class body) are also skipped — they're a separate
|
|
194
|
+
# surface (`Module#@var`) the engine doesn't yet model.
|
|
195
|
+
def ivar_write_mismatch_diagnostics(path, root, scope_index)
|
|
196
|
+
IvarWriteCollector.new(scope_index).collect(root).flat_map do |class_name, writes_by_ivar|
|
|
197
|
+
writes_by_ivar.flat_map do |ivar_name, writes|
|
|
198
|
+
ivar_mismatch_diagnostics_for(path, class_name, ivar_name, writes)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# v0.1.2 — `flow.dead-assignment`. Walks every `DefNode`
|
|
204
|
+
# body and emits a diagnostic for each plain
|
|
205
|
+
# `LocalVariableWriteNode` whose target name is never
|
|
206
|
+
# read in the same body. The
|
|
207
|
+
# `Analysis::CheckRules::DeadAssignmentCollector` describes
|
|
208
|
+
# the conservative envelope.
|
|
209
|
+
def dead_assignment_diagnostics(path, root, scope_index)
|
|
210
|
+
DeadAssignmentCollector.new(scope_index).collect(root).map do |result|
|
|
211
|
+
build_dead_assignment_diagnostic(path, result[:write_node], result[:def_node])
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# v0.1.2 — `flow.always-truthy-condition`. Fires on
|
|
216
|
+
# `if` / `unless` / ternary predicates whose inferred
|
|
217
|
+
# type is a `Type::Constant` AND that don't fall in
|
|
218
|
+
# the literal-only / inside-loop-or-block / defensive-
|
|
219
|
+
# predicate skip envelope (see
|
|
220
|
+
# `Analysis::CheckRules::AlwaysTruthyConditionCollector`
|
|
221
|
+
# for the full triage rationale).
|
|
222
|
+
def always_truthy_condition_diagnostics(path, root, scope_index)
|
|
223
|
+
AlwaysTruthyConditionCollector.new(scope_index).collect(root).map do |result|
|
|
224
|
+
build_always_truthy_condition_diagnostic(path, result.node, result.polarity)
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def ivar_mismatch_diagnostics_for(path, class_name, ivar_name, writes)
|
|
229
|
+
return [] if writes.size < 2
|
|
230
|
+
|
|
231
|
+
first_class = ivar_class_for(writes.first[:type])
|
|
232
|
+
return [] if first_class.nil?
|
|
233
|
+
|
|
234
|
+
writes[1..].filter_map do |write|
|
|
235
|
+
other_class = ivar_class_for(write[:type])
|
|
236
|
+
next nil if other_class.nil? || other_class == "NilClass" || other_class == first_class
|
|
237
|
+
|
|
238
|
+
build_ivar_write_mismatch_diagnostic(path, write[:node], class_name, ivar_name, first_class, other_class)
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# v0.0.2 #6 — diagnostic suppression. Three kinds of
|
|
150
243
|
# suppression compose:
|
|
151
244
|
#
|
|
152
245
|
# - **Project-level**: `disabled_rules` is the
|
|
153
246
|
# project's `.rigor.yml` `disable:` list. Any
|
|
154
247
|
# diagnostic whose `rule` is in the list is dropped.
|
|
155
|
-
# - **
|
|
248
|
+
# - **File-level** (v0.1.2): `# rigor:disable-file <rule1>,
|
|
249
|
+
# <rule2>` anywhere in the file suppresses the matching
|
|
250
|
+
# diagnostic for every line. `# rigor:disable-file all`
|
|
251
|
+
# suppresses every rule across the file. Convention is
|
|
252
|
+
# to put the comment near the top, but Rigor accepts it
|
|
253
|
+
# anywhere — the comment scope is "this file" regardless
|
|
254
|
+
# of position.
|
|
255
|
+
# - **In-source line**: `# rigor:disable <rule1>, <rule2>`
|
|
156
256
|
# on the same line as the offending expression
|
|
157
257
|
# suppresses the matching diagnostic for that line
|
|
158
258
|
# only. `# rigor:disable all` on a line suppresses
|
|
@@ -162,34 +262,49 @@ module Rigor
|
|
|
162
262
|
# errors, internal analyzer errors) are NEVER
|
|
163
263
|
# suppressed — they represent failures the user cannot
|
|
164
264
|
# silence away.
|
|
165
|
-
def filter_suppressed(diagnostics, comments:, disabled_rules:)
|
|
166
|
-
|
|
265
|
+
def filter_suppressed(diagnostics, comments:, disabled_rules:) # rubocop:disable Metrics/CyclomaticComplexity
|
|
266
|
+
line_suppressions, file_suppressions = parse_suppression_comments(comments)
|
|
167
267
|
disabled = expand_rule_tokens(disabled_rules)
|
|
168
268
|
|
|
169
269
|
diagnostics.reject do |diagnostic|
|
|
170
270
|
rule = diagnostic.rule
|
|
171
271
|
next false if rule.nil?
|
|
172
272
|
next true if disabled.include?(rule)
|
|
273
|
+
next true if file_suppressions.include?("all") || file_suppressions.include?(rule)
|
|
173
274
|
|
|
174
|
-
line_rules =
|
|
275
|
+
line_rules = line_suppressions[diagnostic.line]
|
|
175
276
|
line_rules && (line_rules.include?("all") || line_rules.include?(rule))
|
|
176
277
|
end
|
|
177
278
|
end
|
|
178
279
|
|
|
179
|
-
|
|
180
|
-
private_constant :
|
|
280
|
+
LINE_SUPPRESSION_PATTERN = /#\s*rigor:disable(?!-file)\s+(?<rules>[\w.,\s-]+)/
|
|
281
|
+
private_constant :LINE_SUPPRESSION_PATTERN
|
|
181
282
|
|
|
283
|
+
FILE_SUPPRESSION_PATTERN = /#\s*rigor:disable-file\s+(?<rules>[\w.,\s-]+)/
|
|
284
|
+
private_constant :FILE_SUPPRESSION_PATTERN
|
|
285
|
+
|
|
286
|
+
# @return [Array<(Hash{Integer => Set}, Set)>] pair of
|
|
287
|
+
# `(line_suppressions, file_suppressions)`. Line
|
|
288
|
+
# suppressions are keyed by source line number; file
|
|
289
|
+
# suppressions apply to every line.
|
|
182
290
|
def parse_suppression_comments(comments)
|
|
183
|
-
|
|
291
|
+
line_suppressions = Hash.new { |h, k| h[k] = Set.new }
|
|
292
|
+
file_suppressions = Set.new
|
|
184
293
|
comments.each do |comment|
|
|
185
294
|
source = comment.location.slice
|
|
186
|
-
match =
|
|
187
|
-
|
|
295
|
+
if (match = FILE_SUPPRESSION_PATTERN.match(source))
|
|
296
|
+
absorb_suppression_tokens(match[:rules], file_suppressions)
|
|
297
|
+
elsif (match = LINE_SUPPRESSION_PATTERN.match(source))
|
|
298
|
+
absorb_suppression_tokens(match[:rules], line_suppressions[comment.location.start_line])
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
[line_suppressions, file_suppressions]
|
|
302
|
+
end
|
|
188
303
|
|
|
189
|
-
|
|
190
|
-
|
|
304
|
+
def absorb_suppression_tokens(raw, target)
|
|
305
|
+
raw.to_s.split(/[\s,]+/).reject(&:empty?).each do |token|
|
|
306
|
+
target.merge(expand_token(token))
|
|
191
307
|
end
|
|
192
|
-
result
|
|
193
308
|
end
|
|
194
309
|
|
|
195
310
|
# Expands a list of user-supplied rule tokens into the
|
|
@@ -698,6 +813,203 @@ module Rigor
|
|
|
698
813
|
)
|
|
699
814
|
end
|
|
700
815
|
|
|
816
|
+
# v0.1.2 — `flow.unreachable-branch`. Fires when an
|
|
817
|
+
# `IfNode` / `UnlessNode` whose predicate is a literal
|
|
818
|
+
# `true` / `false` / `nil` (or a literal numeric /
|
|
819
|
+
# string / symbol whose Ruby truthiness is known
|
|
820
|
+
# at-a-glance) has an observable dead branch. The
|
|
821
|
+
# diagnostic points at the dead branch (not the
|
|
822
|
+
# predicate) so the squiggle lands on the code that
|
|
823
|
+
# never runs.
|
|
824
|
+
#
|
|
825
|
+
# Conservative envelope — by deliberate v0.1.2 design:
|
|
826
|
+
# - Only **literal-shaped** predicates fire. Inferred-
|
|
827
|
+
# constant predicates (`x.method?` that happens to
|
|
828
|
+
# fold to `Constant<bool>`) are intentionally
|
|
829
|
+
# skipped — Rigor's loop / mutation / RBS-strictness
|
|
830
|
+
# modelling is incomplete enough that an inferred
|
|
831
|
+
# constant can be a false positive (e.g. accumulator
|
|
832
|
+
# `arr << x` doesn't widen the carrier; defensive
|
|
833
|
+
# `module.name.nil?` checks against anonymous-class
|
|
834
|
+
# nil that the RBS `Module#name -> String` sig hides).
|
|
835
|
+
# The literal-only envelope captures the clear
|
|
836
|
+
# "user wrote `if false`" case without false alarms.
|
|
837
|
+
# - Empty dead branches (e.g. `if false; end` with no
|
|
838
|
+
# body) are skipped — there is no useful location to
|
|
839
|
+
# point at.
|
|
840
|
+
# - Postfix-`if` / `unless` modifiers with a literal
|
|
841
|
+
# predicate ARE flagged (`expr if false` body never
|
|
842
|
+
# runs, exactly like the block form).
|
|
843
|
+
# - Elsif chains (`subsequent` is itself an `IfNode`)
|
|
844
|
+
# ARE flagged — the entire downstream chain is
|
|
845
|
+
# unreachable when the outer predicate is a constant
|
|
846
|
+
# literal.
|
|
847
|
+
#
|
|
848
|
+
# Broadening to inferred-constant predicates is queued
|
|
849
|
+
# for a later v0.1.x release once the loop / mutation
|
|
850
|
+
# gaps named above are closed.
|
|
851
|
+
def unreachable_branch_diagnostic(path, node, scope_index)
|
|
852
|
+
scope = scope_index[node]
|
|
853
|
+
return nil if scope.nil?
|
|
854
|
+
|
|
855
|
+
polarity = literal_predicate_polarity(node.predicate)
|
|
856
|
+
return nil if polarity.nil?
|
|
857
|
+
|
|
858
|
+
dead_branch = unreachable_branch_for(node, polarity == :truthy)
|
|
859
|
+
return nil if dead_branch.nil?
|
|
860
|
+
|
|
861
|
+
build_unreachable_branch_diagnostic(path, dead_branch, polarity)
|
|
862
|
+
end
|
|
863
|
+
|
|
864
|
+
# Returns `:truthy` / `:falsey` for a syntactically-
|
|
865
|
+
# literal predicate, or nil for anything else.
|
|
866
|
+
# `TrueNode`, `FalseNode`, `NilNode` are the
|
|
867
|
+
# unambiguous cases. Numeric / string / symbol literals
|
|
868
|
+
# are always truthy in Ruby (any non-`false` / non-`nil`
|
|
869
|
+
# value is truthy, including `0` and `""`).
|
|
870
|
+
TRUTHY_LITERAL_NODES = [
|
|
871
|
+
Prism::TrueNode, Prism::IntegerNode, Prism::FloatNode,
|
|
872
|
+
Prism::StringNode, Prism::SymbolNode, Prism::RegularExpressionNode
|
|
873
|
+
].freeze
|
|
874
|
+
private_constant :TRUTHY_LITERAL_NODES
|
|
875
|
+
|
|
876
|
+
FALSEY_LITERAL_NODES = [Prism::FalseNode, Prism::NilNode].freeze
|
|
877
|
+
private_constant :FALSEY_LITERAL_NODES
|
|
878
|
+
|
|
879
|
+
def literal_predicate_polarity(predicate)
|
|
880
|
+
return :truthy if TRUTHY_LITERAL_NODES.any? { |klass| predicate.is_a?(klass) }
|
|
881
|
+
return :falsey if FALSEY_LITERAL_NODES.any? { |klass| predicate.is_a?(klass) }
|
|
882
|
+
|
|
883
|
+
nil
|
|
884
|
+
end
|
|
885
|
+
|
|
886
|
+
# v0.1.2 — `def.method-visibility-mismatch`. Fires when
|
|
887
|
+
# an explicit-receiver `Prism::CallNode` targets a
|
|
888
|
+
# user-class method whose `discovered_method_visibilities`
|
|
889
|
+
# entry is `:private`. The rule is intentionally narrow:
|
|
890
|
+
#
|
|
891
|
+
# - Only `:private`. `:protected` access depends on
|
|
892
|
+
# subclass tracking the engine does not yet model;
|
|
893
|
+
# broadening waits for that surface.
|
|
894
|
+
# - Only user classes whose visibility table the indexer
|
|
895
|
+
# built. RBS-known classes (stdlib, gems) are NOT
|
|
896
|
+
# consulted yet — RBS visibility is reliable but
|
|
897
|
+
# surfacing it would broaden the rule to a level the
|
|
898
|
+
# per-rule false-positive triage hasn't covered.
|
|
899
|
+
# - Implicit-self calls are skipped (always allowed for
|
|
900
|
+
# private). Calls whose receiver is `Prism::SelfNode`
|
|
901
|
+
# are also skipped — Ruby 2.7+ permits `self.foo` for
|
|
902
|
+
# private methods.
|
|
903
|
+
# - Receiver MUST resolve to a `Type::Nominal` so the
|
|
904
|
+
# rule has a single class identity to query. Unions /
|
|
905
|
+
# Dynamic / shape carriers are skipped.
|
|
906
|
+
def visibility_mismatch_diagnostic(path, call_node, scope_index)
|
|
907
|
+
return nil unless explicit_non_self_receiver?(call_node.receiver)
|
|
908
|
+
|
|
909
|
+
scope = scope_index[call_node]
|
|
910
|
+
return nil if scope.nil?
|
|
911
|
+
|
|
912
|
+
receiver_type = scope.type_of(call_node.receiver)
|
|
913
|
+
return nil unless receiver_type.is_a?(Type::Nominal)
|
|
914
|
+
|
|
915
|
+
visibility = scope.discovered_method_visibility(receiver_type.class_name, call_node.name)
|
|
916
|
+
return nil unless visibility == :private
|
|
917
|
+
|
|
918
|
+
build_visibility_mismatch_diagnostic(path, call_node, receiver_type)
|
|
919
|
+
end
|
|
920
|
+
|
|
921
|
+
def explicit_non_self_receiver?(receiver)
|
|
922
|
+
return false if receiver.nil?
|
|
923
|
+
return false if receiver.is_a?(Prism::SelfNode)
|
|
924
|
+
|
|
925
|
+
true
|
|
926
|
+
end
|
|
927
|
+
|
|
928
|
+
def build_visibility_mismatch_diagnostic(path, call_node, receiver_type)
|
|
929
|
+
location = call_node.message_loc || call_node.location
|
|
930
|
+
Diagnostic.new(
|
|
931
|
+
rule: RULE_VISIBILITY_MISMATCH,
|
|
932
|
+
path: path,
|
|
933
|
+
line: location.start_line,
|
|
934
|
+
column: location.start_column + 1,
|
|
935
|
+
message: "private method `#{call_node.name}' called on #{receiver_type.class_name} receiver",
|
|
936
|
+
severity: :error
|
|
937
|
+
)
|
|
938
|
+
end
|
|
939
|
+
|
|
940
|
+
# Pulls a single concrete class name from an ivar write's
|
|
941
|
+
# rvalue type. Returns nil when the type is too unstable
|
|
942
|
+
# to compare (Union / Dynamic / IntegerRange / etc.).
|
|
943
|
+
# `concrete_class_name` already covers Nominal / Singleton
|
|
944
|
+
# / Constant / Tuple / HashShape; the wrapper exists so
|
|
945
|
+
# the ivar rule can extend the envelope (or apply
|
|
946
|
+
# different filters) without disturbing the call rules.
|
|
947
|
+
def ivar_class_for(type)
|
|
948
|
+
concrete_class_name(type)
|
|
949
|
+
end
|
|
950
|
+
|
|
951
|
+
def build_always_truthy_condition_diagnostic(path, predicate_node, polarity)
|
|
952
|
+
location = predicate_node.location
|
|
953
|
+
Diagnostic.new(
|
|
954
|
+
rule: RULE_ALWAYS_TRUTHY_CONDITION,
|
|
955
|
+
path: path,
|
|
956
|
+
line: location.start_line,
|
|
957
|
+
column: location.start_column + 1,
|
|
958
|
+
message: "condition is always #{polarity} (the surrounding flow proves it folds to a constant)",
|
|
959
|
+
severity: :warning
|
|
960
|
+
)
|
|
961
|
+
end
|
|
962
|
+
|
|
963
|
+
def build_dead_assignment_diagnostic(path, write_node, def_node)
|
|
964
|
+
location = write_node.name_loc || write_node.location
|
|
965
|
+
Diagnostic.new(
|
|
966
|
+
rule: RULE_DEAD_ASSIGNMENT,
|
|
967
|
+
path: path,
|
|
968
|
+
line: location.start_line,
|
|
969
|
+
column: location.start_column + 1,
|
|
970
|
+
message: "local `#{write_node.name}' assigned in `#{def_node.name}' but never read",
|
|
971
|
+
severity: :warning
|
|
972
|
+
)
|
|
973
|
+
end
|
|
974
|
+
|
|
975
|
+
# rubocop:disable Metrics/ParameterLists
|
|
976
|
+
def build_ivar_write_mismatch_diagnostic(path, node, class_name, ivar_name, first_class, other_class)
|
|
977
|
+
location = node.name_loc || node.location
|
|
978
|
+
Diagnostic.new(
|
|
979
|
+
rule: RULE_IVAR_WRITE_MISMATCH,
|
|
980
|
+
path: path,
|
|
981
|
+
line: location.start_line,
|
|
982
|
+
column: location.start_column + 1,
|
|
983
|
+
message: "instance variable `#{ivar_name}' on #{class_name} was previously assigned " \
|
|
984
|
+
"#{first_class}; this write assigns #{other_class}",
|
|
985
|
+
severity: :error
|
|
986
|
+
)
|
|
987
|
+
end
|
|
988
|
+
# rubocop:enable Metrics/ParameterLists
|
|
989
|
+
|
|
990
|
+
# Returns the dead-branch node for a literal-predicate
|
|
991
|
+
# if/unless, or nil when no observable branch is dead.
|
|
992
|
+
def unreachable_branch_for(node, truthy)
|
|
993
|
+
dead =
|
|
994
|
+
case node
|
|
995
|
+
when Prism::IfNode then truthy ? node.subsequent : node.statements
|
|
996
|
+
when Prism::UnlessNode then truthy ? node.statements : node.else_clause
|
|
997
|
+
end
|
|
998
|
+
dead unless dead.nil?
|
|
999
|
+
end
|
|
1000
|
+
|
|
1001
|
+
def build_unreachable_branch_diagnostic(path, dead_branch, polarity)
|
|
1002
|
+
location = dead_branch.location
|
|
1003
|
+
Diagnostic.new(
|
|
1004
|
+
rule: RULE_UNREACHABLE_BRANCH,
|
|
1005
|
+
path: path,
|
|
1006
|
+
line: location.start_line,
|
|
1007
|
+
column: location.start_column + 1,
|
|
1008
|
+
message: "unreachable branch: literal predicate is always #{polarity}",
|
|
1009
|
+
severity: :warning
|
|
1010
|
+
)
|
|
1011
|
+
end
|
|
1012
|
+
|
|
701
1013
|
# v0.0.2 #4 — argument-type-mismatch diagnostic.
|
|
702
1014
|
# Walks a call's positional arguments and checks each
|
|
703
1015
|
# against the matching parameter's RBS type via
|
|
@@ -915,6 +1227,19 @@ module Rigor
|
|
|
915
1227
|
# Method overloads contribute their union of declared
|
|
916
1228
|
# return types (any one of them satisfying the body
|
|
917
1229
|
# silences the rule).
|
|
1230
|
+
#
|
|
1231
|
+
# v0.1.2 — when the RBS sig carries a
|
|
1232
|
+
# `%a{rigor:v1:return: <refinement>}` annotation
|
|
1233
|
+
# (recognised by `RbsExtended.read_return_type_override`),
|
|
1234
|
+
# the refinement carrier replaces the RBS-declared
|
|
1235
|
+
# return for this rule. Annotation-driven refinements
|
|
1236
|
+
# — `non-empty-string`, `positive-int`, `non-empty-
|
|
1237
|
+
# array[Integer]`, etc. — are stricter than the
|
|
1238
|
+
# underlying RBS class, so a body whose inferred type
|
|
1239
|
+
# the bare RBS sig would accept may still fail the
|
|
1240
|
+
# refinement (e.g. `def name; ""; end` returns
|
|
1241
|
+
# `Constant[""]`, accepted by `String` but rejected by
|
|
1242
|
+
# `non-empty-string`).
|
|
918
1243
|
def declared_return_type(def_node, scope_index)
|
|
919
1244
|
scope = scope_index[def_node]
|
|
920
1245
|
return nil if scope.nil?
|
|
@@ -930,6 +1255,9 @@ module Rigor
|
|
|
930
1255
|
end
|
|
931
1256
|
return nil if method_def.nil?
|
|
932
1257
|
|
|
1258
|
+
override = Rigor::RbsExtended.read_return_type_override(method_def)
|
|
1259
|
+
return override if override
|
|
1260
|
+
|
|
933
1261
|
declared_return_union(method_def, scope.environment)
|
|
934
1262
|
end
|
|
935
1263
|
|