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.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -2
  3. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +18 -1
  4. data/lib/rigor/analysis/check_rules/rule_walk.rb +67 -0
  5. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +18 -1
  6. data/lib/rigor/analysis/check_rules.rb +34 -6
  7. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +580 -0
  8. data/lib/rigor/analysis/runner/pool_coordinator.rb +569 -0
  9. data/lib/rigor/analysis/runner/project_pre_passes.rb +318 -0
  10. data/lib/rigor/analysis/runner/run_snapshots.rb +46 -0
  11. data/lib/rigor/analysis/runner.rb +160 -1190
  12. data/lib/rigor/analysis/worker_session.rb +47 -8
  13. data/lib/rigor/cache/incremental_snapshot.rb +10 -4
  14. data/lib/rigor/cache/rbs_cache_producer.rb +5 -1
  15. data/lib/rigor/cache/store.rb +46 -13
  16. data/lib/rigor/cli/check_command.rb +705 -0
  17. data/lib/rigor/cli/ci_detector.rb +94 -0
  18. data/lib/rigor/cli/diagnostic_formats.rb +345 -0
  19. data/lib/rigor/cli/prism_colorizer.rb +10 -3
  20. data/lib/rigor/cli/trace_command.rb +143 -0
  21. data/lib/rigor/cli/trace_renderer.rb +310 -0
  22. data/lib/rigor/cli.rb +15 -614
  23. data/lib/rigor/configuration.rb +9 -6
  24. data/lib/rigor/environment/rbs_loader.rb +53 -68
  25. data/lib/rigor/environment.rb +1 -1
  26. data/lib/rigor/inference/acceptance.rb +10 -0
  27. data/lib/rigor/inference/expression_typer.rb +28 -62
  28. data/lib/rigor/inference/flow_tracer.rb +180 -0
  29. data/lib/rigor/inference/macro_block_self_type.rb +10 -11
  30. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +33 -1
  31. data/lib/rigor/inference/method_dispatcher.rb +115 -54
  32. data/lib/rigor/inference/narrowing.rb +60 -0
  33. data/lib/rigor/inference/scope_indexer.rb +75 -15
  34. data/lib/rigor/inference/statement_evaluator.rb +35 -52
  35. data/lib/rigor/plugin/additional_initializer.rb +61 -38
  36. data/lib/rigor/plugin/base.rb +282 -41
  37. data/lib/rigor/plugin/node_rule_walk.rb +147 -0
  38. data/lib/rigor/plugin/registry.rb +263 -35
  39. data/lib/rigor/plugin.rb +1 -0
  40. data/lib/rigor/rbs_extended/conformance_checker.rb +86 -1
  41. data/lib/rigor/scope/discovery_index.rb +58 -0
  42. data/lib/rigor/scope.rb +67 -198
  43. data/lib/rigor/sig_gen/observation_collector.rb +6 -6
  44. data/lib/rigor/source/literals.rb +14 -0
  45. data/lib/rigor/type/combinator.rb +5 -0
  46. data/lib/rigor/version.rb +1 -1
  47. data/lib/rigor.rb +0 -1
  48. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -2
  49. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +2 -4
  50. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +70 -32
  51. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -3
  52. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +15 -21
  53. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +1 -1
  54. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -2
  55. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -2
  56. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +12 -2
  57. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
  58. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +35 -18
  59. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +8 -29
  60. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +17 -1
  61. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +2 -2
  62. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +83 -36
  63. data/sig/rigor/environment.rbs +0 -2
  64. data/sig/rigor/inference.rbs +5 -0
  65. data/sig/rigor/plugin/base.rbs +1 -2
  66. data/sig/rigor/scope.rbs +41 -29
  67. data/sig/rigor/source.rbs +1 -0
  68. data/skills/rigor-ci-setup/SKILL.md +319 -0
  69. metadata +15 -2
  70. 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: 9379e94547d874f10fc2fcb79e354b16d0459350bdd68a9025dc5bd0ee6bdc3d
4
- data.tar.gz: 4b773c4952be32d673358758340d85a34f9839890eede1669ff863a779b30d69
3
+ metadata.gz: 02ca1aeff8df7e80afcbc2f1fb03a808466dd307e1a935c68c4af7055dfa975b
4
+ data.tar.gz: 2a7b829348f75fe48bf3741a691fcfb1aa67b863675384feffe91a6fabb6ee6b
5
5
  SHA512:
6
- metadata.gz: '08680ad9e159ff52c031548a45e862539419f0719a251b0b46dc3dddc5313373a7434806024429c1ebf1c580bafcdb7507fed1b2574139c17cb2394129bc0581'
7
- data.tar.gz: 4469fa97e287f84772543d0525866a733fd0e075c486e8a8dc650b09e31a1a02e1b91602d61b0678de9c2fb914bd999a4cea998e0ab6c39f3ee4970892e3b39c
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.15`** (2026-05-29). The analyzer is
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); `v0.2.0` will open the first evaluation release.
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
- collect_predicate(node) if conditional_node?(node) && !in_loop_or_block
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
- collect_case(node) if case_like?(node) && !in_loop_or_block
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
- diagnostics.concat(always_truthy_condition_diagnostics(path, root, scope_index))
187
- diagnostics.concat(unreachable_clause_diagnostics(path, root, scope_index))
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, root, scope_index)
255
- AlwaysTruthyConditionCollector.new(scope_index).collect(root).map do |result|
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, root, scope_index)
265
- UnreachableClauseCollector.new(scope_index).collect(root).map do |result|
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