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.
Files changed (125) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +159 -222
  3. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +24 -1
  4. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +25 -0
  5. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +29 -0
  6. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +54 -0
  7. data/lib/rigor/analysis/check_rules/rule_walk.rb +213 -0
  8. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +24 -1
  9. data/lib/rigor/analysis/check_rules.rb +275 -44
  10. data/lib/rigor/analysis/diagnostic.rb +8 -0
  11. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +581 -0
  12. data/lib/rigor/analysis/runner/pool_coordinator.rb +569 -0
  13. data/lib/rigor/analysis/runner/project_pre_passes.rb +321 -0
  14. data/lib/rigor/analysis/runner/run_snapshots.rb +46 -0
  15. data/lib/rigor/analysis/runner.rb +207 -1200
  16. data/lib/rigor/analysis/worker_session.rb +60 -11
  17. data/lib/rigor/bleeding_edge.rb +123 -0
  18. data/lib/rigor/cache/descriptor.rb +86 -8
  19. data/lib/rigor/cache/incremental_snapshot.rb +10 -4
  20. data/lib/rigor/cache/rbs_cache_producer.rb +5 -1
  21. data/lib/rigor/cache/rbs_descriptor.rb +2 -1
  22. data/lib/rigor/cache/store.rb +46 -13
  23. data/lib/rigor/cli/annotate_command.rb +100 -15
  24. data/lib/rigor/cli/check_command.rb +708 -0
  25. data/lib/rigor/cli/ci_detector.rb +94 -0
  26. data/lib/rigor/cli/diagnostic_formats.rb +345 -0
  27. data/lib/rigor/cli/plugins_command.rb +2 -4
  28. data/lib/rigor/cli/plugins_renderer.rb +0 -2
  29. data/lib/rigor/cli/prism_colorizer.rb +10 -3
  30. data/lib/rigor/cli/show_bleedingedge_command.rb +114 -0
  31. data/lib/rigor/cli/trace_command.rb +143 -0
  32. data/lib/rigor/cli/trace_renderer.rb +310 -0
  33. data/lib/rigor/cli/triage_command.rb +6 -3
  34. data/lib/rigor/cli/triage_renderer.rb +15 -1
  35. data/lib/rigor/cli.rb +21 -612
  36. data/lib/rigor/configuration/severity_profile.rb +13 -1
  37. data/lib/rigor/configuration.rb +66 -7
  38. data/lib/rigor/environment/rbs_loader.rb +78 -68
  39. data/lib/rigor/environment.rb +1 -1
  40. data/lib/rigor/inference/acceptance.rb +10 -0
  41. data/lib/rigor/inference/body_fixpoint.rb +89 -0
  42. data/lib/rigor/inference/budget_trace.rb +29 -2
  43. data/lib/rigor/inference/expression_typer.rb +1080 -105
  44. data/lib/rigor/inference/flow_tracer.rb +180 -0
  45. data/lib/rigor/inference/macro_block_self_type.rb +11 -12
  46. data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +60 -0
  47. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +54 -14
  48. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +33 -1
  49. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +281 -0
  50. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +71 -0
  51. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +148 -10
  52. data/lib/rigor/inference/method_dispatcher.rb +187 -55
  53. data/lib/rigor/inference/method_parameter_binder.rb +56 -2
  54. data/lib/rigor/inference/multi_target_binder.rb +46 -3
  55. data/lib/rigor/inference/mutation_widening.rb +142 -0
  56. data/lib/rigor/inference/narrowing.rb +330 -37
  57. data/lib/rigor/inference/scope_indexer.rb +770 -39
  58. data/lib/rigor/inference/statement_evaluator.rb +998 -68
  59. data/lib/rigor/inference/synthetic_method_scanner.rb +1 -1
  60. data/lib/rigor/plugin/additional_initializer.rb +61 -38
  61. data/lib/rigor/plugin/base.rb +517 -120
  62. data/lib/rigor/plugin/macro/block_as_method.rb +22 -21
  63. data/lib/rigor/plugin/macro/nested_class_template.rb +9 -7
  64. data/lib/rigor/plugin/macro.rb +2 -3
  65. data/lib/rigor/plugin/manifest.rb +4 -24
  66. data/lib/rigor/plugin/node_rule_walk.rb +192 -0
  67. data/lib/rigor/plugin/registry.rb +264 -35
  68. data/lib/rigor/plugin.rb +1 -0
  69. data/lib/rigor/rbs_extended/conformance_checker.rb +86 -1
  70. data/lib/rigor/scope/discovery_index.rb +60 -0
  71. data/lib/rigor/scope.rb +199 -204
  72. data/lib/rigor/sig_gen/generator.rb +8 -0
  73. data/lib/rigor/sig_gen/observation_collector.rb +6 -6
  74. data/lib/rigor/source/literals.rb +14 -0
  75. data/lib/rigor/triage/catalogue.rb +4 -19
  76. data/lib/rigor/triage.rb +69 -1
  77. data/lib/rigor/type/combinator.rb +34 -0
  78. data/lib/rigor/version.rb +1 -1
  79. data/lib/rigor.rb +0 -1
  80. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +13 -29
  81. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -32
  82. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -2
  83. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +27 -90
  84. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +13 -30
  85. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +2 -4
  86. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +90 -51
  87. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -3
  88. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -29
  89. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +1 -1
  90. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -2
  91. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +11 -40
  92. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -2
  93. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +1 -1
  94. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +10 -21
  95. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +21 -34
  96. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +11 -18
  97. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +0 -1
  98. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +12 -2
  99. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
  100. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +37 -31
  101. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +3 -23
  102. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +8 -21
  103. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -1
  104. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +8 -29
  105. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +17 -1
  106. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +2 -2
  107. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +108 -36
  108. data/sig/rigor/analysis/fact_store.rbs +3 -0
  109. data/sig/rigor/environment.rbs +0 -2
  110. data/sig/rigor/inference/builtins/method_catalog.rbs +1 -1
  111. data/sig/rigor/inference.rbs +5 -0
  112. data/sig/rigor/plugin/base.rbs +6 -4
  113. data/sig/rigor/plugin/manifest.rbs +1 -2
  114. data/sig/rigor/scope.rbs +50 -29
  115. data/sig/rigor/source.rbs +1 -0
  116. data/sig/rigor/type.rbs +1 -0
  117. data/sig/rigor.rbs +1 -1
  118. data/skills/rigor-baseline-reduce/references/01-classify.md +27 -0
  119. data/skills/rigor-ci-setup/SKILL.md +319 -0
  120. data/skills/rigor-plugin-author/SKILL.md +6 -4
  121. data/skills/rigor-plugin-author/references/02-walker-and-types.md +22 -17
  122. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +18 -1
  123. metadata +21 -3
  124. data/lib/rigor/cache/rbs_instance_definitions.rb +0 -66
  125. 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
- collect_case(node) if case_like?(node) && !in_loop_or_block
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) }