rigortype 0.1.17 → 0.1.18
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 +4 -2
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +18 -1
- data/lib/rigor/analysis/check_rules/rule_walk.rb +67 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +18 -1
- data/lib/rigor/analysis/check_rules.rb +34 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +580 -0
- data/lib/rigor/analysis/runner/pool_coordinator.rb +569 -0
- data/lib/rigor/analysis/runner/project_pre_passes.rb +318 -0
- data/lib/rigor/analysis/runner/run_snapshots.rb +46 -0
- data/lib/rigor/analysis/runner.rb +160 -1190
- data/lib/rigor/analysis/worker_session.rb +47 -8
- data/lib/rigor/cache/incremental_snapshot.rb +10 -4
- data/lib/rigor/cache/rbs_cache_producer.rb +5 -1
- data/lib/rigor/cache/store.rb +46 -13
- data/lib/rigor/cli/check_command.rb +705 -0
- data/lib/rigor/cli/ci_detector.rb +94 -0
- data/lib/rigor/cli/diagnostic_formats.rb +345 -0
- data/lib/rigor/cli/prism_colorizer.rb +10 -3
- data/lib/rigor/cli/trace_command.rb +143 -0
- data/lib/rigor/cli/trace_renderer.rb +310 -0
- data/lib/rigor/cli.rb +15 -614
- data/lib/rigor/configuration.rb +9 -6
- data/lib/rigor/environment/rbs_loader.rb +53 -68
- data/lib/rigor/environment.rb +1 -1
- data/lib/rigor/inference/acceptance.rb +10 -0
- data/lib/rigor/inference/expression_typer.rb +28 -62
- data/lib/rigor/inference/flow_tracer.rb +180 -0
- data/lib/rigor/inference/macro_block_self_type.rb +10 -11
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +33 -1
- data/lib/rigor/inference/method_dispatcher.rb +115 -54
- data/lib/rigor/inference/narrowing.rb +60 -0
- data/lib/rigor/inference/scope_indexer.rb +75 -15
- data/lib/rigor/inference/statement_evaluator.rb +35 -52
- data/lib/rigor/plugin/additional_initializer.rb +61 -38
- data/lib/rigor/plugin/base.rb +282 -41
- data/lib/rigor/plugin/node_rule_walk.rb +147 -0
- data/lib/rigor/plugin/registry.rb +263 -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 +58 -0
- data/lib/rigor/scope.rb +67 -198
- data/lib/rigor/sig_gen/observation_collector.rb +6 -6
- data/lib/rigor/source/literals.rb +14 -0
- data/lib/rigor/type/combinator.rb +5 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +0 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +2 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +70 -32
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -3
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +15 -21
- 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-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -2
- 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 +35 -18
- 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 +83 -36
- data/sig/rigor/environment.rbs +0 -2
- data/sig/rigor/inference.rbs +5 -0
- data/sig/rigor/plugin/base.rbs +1 -2
- data/sig/rigor/scope.rbs +41 -29
- data/sig/rigor/source.rbs +1 -0
- data/skills/rigor-ci-setup/SKILL.md +319 -0
- metadata +15 -2
- data/lib/rigor/cache/rbs_instance_definitions.rb +0 -66
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 02ca1aeff8df7e80afcbc2f1fb03a808466dd307e1a935c68c4af7055dfa975b
|
|
4
|
+
data.tar.gz: 2a7b829348f75fe48bf3741a691fcfb1aa67b863675384feffe91a6fabb6ee6b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f1dfc043a607024bc46987acf0fc925b7926294a6e46b668793bc03fa88e0b1066bea51db5e80a038483b602ad59e788269ac8baa5615a1694cbc8afd90614c4
|
|
7
|
+
data.tar.gz: 894eccadf5220d1c9b16d09e07f799e1104265a1e3cbedb9a738330441d931a77eb24314e63768a24da2e4d179221c34f297c32c82e905dcfce749b36264c94c
|
data/README.md
CHANGED
|
@@ -265,12 +265,14 @@ In-source suppression: `# rigor:disable <rule>` silences a single line;
|
|
|
265
265
|
|
|
266
266
|
## Status
|
|
267
267
|
|
|
268
|
-
Current released version: **`v0.1.
|
|
268
|
+
Current released version: **`v0.1.18`** (2026-06-11). The analyzer is
|
|
269
269
|
usable on real Ruby code today; the rule catalogue is deliberately
|
|
270
270
|
conservative — Rigor's stance is to surface zero false positives while
|
|
271
271
|
the inference surface stabilises. The `0.1.x` preview line has been
|
|
272
272
|
hardened against real OSS Rails codebases (Mastodon / Redmine / GitLab
|
|
273
|
-
FOSS)
|
|
273
|
+
FOSS) and wired for CI — `rigor check` emits SARIF / GitHub / GitLab /
|
|
274
|
+
Checkstyle / JUnit / TeamCity output and auto-detects the CI environment;
|
|
275
|
+
`v0.2.0` will open the first evaluation release.
|
|
274
276
|
|
|
275
277
|
Release history: [`CHANGELOG.md`](CHANGELOG.md). Forward-looking
|
|
276
278
|
commitments: [Roadmap](https://rigor.typedduck.fail/reference/roadmap/).
|
|
@@ -56,6 +56,10 @@ module Rigor
|
|
|
56
56
|
|
|
57
57
|
Result = Data.define(:node, :polarity)
|
|
58
58
|
|
|
59
|
+
# ADR-53 Track B — the node classes the shared {RuleWalk}
|
|
60
|
+
# dispatches to this collector (outside loop / block bodies).
|
|
61
|
+
NODE_CLASSES = [Prism::IfNode, Prism::UnlessNode].freeze
|
|
62
|
+
|
|
59
63
|
# @return [Array<Result>] one entry per qualifying
|
|
60
64
|
# predicate. Empty when the tree carries no firing
|
|
61
65
|
# predicates.
|
|
@@ -64,17 +68,30 @@ module Rigor
|
|
|
64
68
|
@results = []
|
|
65
69
|
end
|
|
66
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.
|
|
67
74
|
def collect(root)
|
|
68
75
|
walk(root, in_loop_or_block: false)
|
|
69
76
|
@results.freeze
|
|
70
77
|
end
|
|
71
78
|
|
|
79
|
+
# {RuleWalk} entry point: the per-node logic of the legacy walk,
|
|
80
|
+
# invoked under the same traversal contract.
|
|
81
|
+
def visit(node)
|
|
82
|
+
collect_predicate(node)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def results
|
|
86
|
+
@results.freeze
|
|
87
|
+
end
|
|
88
|
+
|
|
72
89
|
private
|
|
73
90
|
|
|
74
91
|
def walk(node, in_loop_or_block:)
|
|
75
92
|
return unless node.is_a?(Prism::Node)
|
|
76
93
|
|
|
77
|
-
|
|
94
|
+
visit(node) if conditional_node?(node) && !in_loop_or_block
|
|
78
95
|
|
|
79
96
|
child_in_loop_or_block = in_loop_or_block || enters_loop_or_block?(node)
|
|
80
97
|
node.compact_child_nodes.each { |child| walk(child, in_loop_or_block: child_in_loop_or_block) }
|
|
@@ -0,0 +1,67 @@
|
|
|
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
|
+
# Slice B2 hosts the two flow collectors
|
|
15
|
+
# ({AlwaysTruthyConditionCollector} / {UnreachableClauseCollector}),
|
|
16
|
+
# whose hand-rolled walks shared this exact traversal contract:
|
|
17
|
+
# visit-before-descend DFS over `compact_child_nodes`, with
|
|
18
|
+
# collection suppressed anywhere inside a loop / block body
|
|
19
|
+
# (mutation tracking through those is incomplete — each collector's
|
|
20
|
+
# envelope documents why). A collector joins the walk by declaring
|
|
21
|
+
# `NODE_CLASSES` and exposing `#visit(node)` with its per-node logic
|
|
22
|
+
# transplanted verbatim; a collector with a different traversal
|
|
23
|
+
# contract (class-nesting prefix, def-scoped scanning) stays on its
|
|
24
|
+
# own walk until a later slice threads the context it needs.
|
|
25
|
+
#
|
|
26
|
+
# Equivalence is load-bearing: every hosted collector keeps its
|
|
27
|
+
# legacy single-collector `#collect` walk as the oracle, compared
|
|
28
|
+
# against this walk by the permanent `rule_walk_equivalence_spec`
|
|
29
|
+
# and, over whole corpora, by `RIGOR_SHADOW_RULE_WALK=1`
|
|
30
|
+
# (see `CheckRules.flow_collector_results`).
|
|
31
|
+
module RuleWalk
|
|
32
|
+
LOOP_OR_BLOCK_NODE_CLASSES = [
|
|
33
|
+
Prism::WhileNode, Prism::UntilNode, Prism::ForNode, Prism::BlockNode
|
|
34
|
+
].freeze
|
|
35
|
+
|
|
36
|
+
class << self
|
|
37
|
+
# Walks `root` once, dispatching each visited node to every
|
|
38
|
+
# collector whose `NODE_CLASSES` covers the node's class.
|
|
39
|
+
# Dispatch keys on the exact node class — Prism node classes
|
|
40
|
+
# are leaves, so this matches the collectors' `is_a?` gates.
|
|
41
|
+
def run(root, collectors)
|
|
42
|
+
hooks = {}
|
|
43
|
+
collectors.each do |collector|
|
|
44
|
+
collector.class::NODE_CLASSES.each do |node_class|
|
|
45
|
+
(hooks[node_class] ||= []) << collector
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
walk(root, hooks, in_loop_or_block: false)
|
|
49
|
+
collectors
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def walk(node, hooks, in_loop_or_block:)
|
|
55
|
+
return unless node.is_a?(Prism::Node)
|
|
56
|
+
|
|
57
|
+
hooks[node.class]&.each { |collector| collector.visit(node) } unless in_loop_or_block
|
|
58
|
+
|
|
59
|
+
child_in_loop_or_block =
|
|
60
|
+
in_loop_or_block || LOOP_OR_BLOCK_NODE_CLASSES.any? { |klass| node.is_a?(klass) }
|
|
61
|
+
node.compact_child_nodes.each { |child| walk(child, hooks, in_loop_or_block: child_in_loop_or_block) }
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -55,23 +55,40 @@ 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 (outside loop / block bodies).
|
|
60
|
+
NODE_CLASSES = [Prism::CaseNode, Prism::CaseMatchNode].freeze
|
|
61
|
+
|
|
58
62
|
def initialize(scope_index)
|
|
59
63
|
@scope_index = scope_index
|
|
60
64
|
@results = []
|
|
61
65
|
end
|
|
62
66
|
|
|
67
|
+
# Legacy single-collector walk — kept as the oracle the
|
|
68
|
+
# ADR-53 Track B equivalence harness compares {RuleWalk}
|
|
69
|
+
# against; deleted when Track B completes.
|
|
63
70
|
# @return [Array<Result>] one entry per provably-dead `when` clause.
|
|
64
71
|
def collect(root)
|
|
65
72
|
walk(root, in_loop_or_block: false)
|
|
66
73
|
@results.freeze
|
|
67
74
|
end
|
|
68
75
|
|
|
76
|
+
# {RuleWalk} entry point: the per-node logic of the legacy walk,
|
|
77
|
+
# invoked under the same traversal contract.
|
|
78
|
+
def visit(node)
|
|
79
|
+
collect_case(node)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def results
|
|
83
|
+
@results.freeze
|
|
84
|
+
end
|
|
85
|
+
|
|
69
86
|
private
|
|
70
87
|
|
|
71
88
|
def walk(node, in_loop_or_block:)
|
|
72
89
|
return unless node.is_a?(Prism::Node)
|
|
73
90
|
|
|
74
|
-
|
|
91
|
+
visit(node) if case_like?(node) && !in_loop_or_block
|
|
75
92
|
|
|
76
93
|
child_in_loop_or_block = in_loop_or_block || enters_loop_or_block?(node)
|
|
77
94
|
node.compact_child_nodes.each { |child| walk(child, in_loop_or_block: child_in_loop_or_block) }
|
|
@@ -7,6 +7,7 @@ require_relative "../source/node_walker"
|
|
|
7
7
|
require_relative "../type"
|
|
8
8
|
require_relative "diagnostic"
|
|
9
9
|
require_relative "dependency_recorder"
|
|
10
|
+
require_relative "check_rules/rule_walk"
|
|
10
11
|
require_relative "check_rules/always_truthy_condition_collector"
|
|
11
12
|
require_relative "check_rules/unreachable_clause_collector"
|
|
12
13
|
require_relative "check_rules/dead_assignment_collector"
|
|
@@ -183,13 +184,40 @@ module Rigor
|
|
|
183
184
|
end
|
|
184
185
|
end
|
|
185
186
|
diagnostics.concat(self_undefined_method_diagnostics(path, self_call_misses, root, scope_index))
|
|
186
|
-
|
|
187
|
-
diagnostics.concat(
|
|
187
|
+
always_truthy_results, unreachable_clause_results = flow_collector_results(root, scope_index)
|
|
188
|
+
diagnostics.concat(always_truthy_condition_diagnostics(path, always_truthy_results))
|
|
189
|
+
diagnostics.concat(unreachable_clause_diagnostics(path, unreachable_clause_results))
|
|
188
190
|
diagnostics.concat(ivar_write_mismatch_diagnostics(path, root, scope_index))
|
|
189
191
|
diagnostics.concat(dead_assignment_diagnostics(path, root, scope_index))
|
|
190
192
|
filter_suppressed(diagnostics, comments: comments, disabled_rules: disabled_rules)
|
|
191
193
|
end
|
|
192
194
|
|
|
195
|
+
# ADR-53 Track B (slice B2) — both flow collectors ride one
|
|
196
|
+
# {RuleWalk} traversal instead of walking the file once each.
|
|
197
|
+
# Under `RIGOR_SHADOW_RULE_WALK=1` the legacy per-collector walks
|
|
198
|
+
# also run as the oracle and any divergence aborts the run — the
|
|
199
|
+
# corpus-scale half of the equivalence harness (the curated half
|
|
200
|
+
# is `rule_walk_equivalence_spec`).
|
|
201
|
+
def flow_collector_results(root, scope_index)
|
|
202
|
+
always_truthy = AlwaysTruthyConditionCollector.new(scope_index)
|
|
203
|
+
unreachable_clauses = UnreachableClauseCollector.new(scope_index)
|
|
204
|
+
RuleWalk.run(root, [always_truthy, unreachable_clauses])
|
|
205
|
+
if ENV["RIGOR_SHADOW_RULE_WALK"]
|
|
206
|
+
shadow_verify_flow_collectors(root, scope_index, always_truthy.results, unreachable_clauses.results)
|
|
207
|
+
end
|
|
208
|
+
[always_truthy.results, unreachable_clauses.results]
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def shadow_verify_flow_collectors(root, scope_index, always_truthy_results, unreachable_clause_results)
|
|
212
|
+
legacy_always = AlwaysTruthyConditionCollector.new(scope_index).collect(root)
|
|
213
|
+
legacy_clauses = UnreachableClauseCollector.new(scope_index).collect(root)
|
|
214
|
+
return if legacy_always == always_truthy_results && legacy_clauses == unreachable_clause_results
|
|
215
|
+
|
|
216
|
+
raise "RIGOR_SHADOW_RULE_WALK divergence: always-truthy legacy=#{legacy_always.size} " \
|
|
217
|
+
"walk=#{always_truthy_results.size}; unreachable-clause legacy=#{legacy_clauses.size} " \
|
|
218
|
+
"walk=#{unreachable_clause_results.size}"
|
|
219
|
+
end
|
|
220
|
+
|
|
193
221
|
def call_node_diagnostics(path, node, scope_index)
|
|
194
222
|
[
|
|
195
223
|
undefined_method_diagnostic(path, node, scope_index),
|
|
@@ -251,8 +279,8 @@ module Rigor
|
|
|
251
279
|
# predicate skip envelope (see
|
|
252
280
|
# `Analysis::CheckRules::AlwaysTruthyConditionCollector`
|
|
253
281
|
# for the full triage rationale).
|
|
254
|
-
def always_truthy_condition_diagnostics(path,
|
|
255
|
-
|
|
282
|
+
def always_truthy_condition_diagnostics(path, results)
|
|
283
|
+
results.map do |result|
|
|
256
284
|
build_always_truthy_condition_diagnostic(path, result.node, result.polarity)
|
|
257
285
|
end
|
|
258
286
|
end
|
|
@@ -261,8 +289,8 @@ module Rigor
|
|
|
261
289
|
# the flow engine's narrowing proves can never match (its narrowed
|
|
262
290
|
# subject is `bot`). The squiggle lands on the dead clause's body,
|
|
263
291
|
# mirroring `flow.unreachable-branch`.
|
|
264
|
-
def unreachable_clause_diagnostics(path,
|
|
265
|
-
|
|
292
|
+
def unreachable_clause_diagnostics(path, results)
|
|
293
|
+
results.map do |result|
|
|
266
294
|
build_unreachable_clause_diagnostic(path, result)
|
|
267
295
|
end
|
|
268
296
|
end
|