Module: Rigor::Analysis::CheckRules
- Defined in:
- lib/rigor/analysis/check_rules.rb,
lib/rigor/analysis/check_rules/rule_walk.rb,
lib/rigor/analysis/check_rules/main_pass_collector.rb,
lib/rigor/analysis/check_rules/ivar_write_collector.rb,
lib/rigor/analysis/check_rules/self_closedness_scanner.rb,
lib/rigor/analysis/check_rules/dead_assignment_collector.rb,
lib/rigor/analysis/check_rules/unreachable_clause_collector.rb,
lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb
Overview
Catalogue of ‘rigor check` diagnostic rules.
Rules fire ONLY when the engine is confident enough to make a useful claim and MUST NOT raise on unrecognised AST shapes, RBS gaps, or missing scope information. Each rule consumes the per-node scope index produced by ‘Rigor::Inference::ScopeIndexer.index` and yields zero or more `Rigor::Analysis::Diagnostic` values.
The primary rule (‘call.undefined-method`) flags an explicit-receiver `Prism::CallNode` whose receiver statically resolves to a class known to the RBS environment and whose method name does not appear on that class’s method table. It does NOT fire for:
-
implicit-self calls (no ‘node.receiver`) — too noisy without per-method RBS for every helper in the class.
-
dynamic / unknown receivers (‘Dynamic`, `Top`, `Union`) — by definition we cannot enumerate the method set.
-
shape carriers: ‘Tuple` → “Array”, `HashShape` → “Hash”, `Constant` → the constant’s class — ‘concrete_class_name` resolves these to their runtime class for dispatch.
-
receivers whose class name is NOT registered in the loader (RBS-blind environments, unknown stdlib).
rubocop:disable Metrics/ModuleLength
Defined Under Namespace
Modules: RuleWalk Classes: AlwaysTruthyConditionCollector, DeadAssignmentCollector, IvarWriteCollector, MainPassCollector, SelfClosednessScanner, UnreachableClauseCollector
Constant Summary collapse
- RULE_UNDEFINED_METHOD =
Canonical identifiers for each rule. Per ADR-8 §“Diagnostic ID family hierarchy”, rule names are ‘family.rule-name` two-segment strings; the families group diagnostics by where they originate (`call.*` for call-site rules, `flow.*` for flow-analysis proofs, `assert.*` for runtime-assertion rules, `dump.*` for debug helpers, `def.*` for method-definition rules). Used by the configuration `disable:` list and the in-source `# rigor:disable <rule>` suppression comment system; new rules MUST register here so user configuration can refer to them.
"call.undefined-method"- RULE_SELF_UNDEFINED_METHOD =
"call.self-undefined-method"- RULE_UNRESOLVED_TOPLEVEL =
"call.unresolved-toplevel"- RULE_WRONG_ARITY =
"call.wrong-arity"- RULE_ARGUMENT_TYPE =
"call.argument-type-mismatch"- RULE_NIL_RECEIVER =
"call.possible-nil-receiver"- RULE_DUMP_TYPE =
"dump.type"- RULE_ASSERT_TYPE =
"assert.type-mismatch"- RULE_ALWAYS_RAISES =
"flow.always-raises"- RULE_UNREACHABLE_BRANCH =
"flow.unreachable-branch"- RULE_RETURN_TYPE =
"def.return-type-mismatch"- RULE_VISIBILITY_MISMATCH =
"def.method-visibility-mismatch"- RULE_OVERRIDE_VISIBILITY_REDUCED =
"def.override-visibility-reduced"- RULE_OVERRIDE_RETURN_WIDENED =
"def.override-return-widened"- RULE_OVERRIDE_PARAM_NARROWED =
"def.override-param-narrowed"- RULE_IVAR_WRITE_MISMATCH =
"def.ivar-write-mismatch"- RULE_DEAD_ASSIGNMENT =
"flow.dead-assignment"- RULE_ALWAYS_TRUTHY_CONDITION =
"flow.always-truthy-condition"- RULE_UNREACHABLE_CLAUSE =
"flow.unreachable-clause"- ALL_RULES =
[ RULE_UNDEFINED_METHOD, RULE_SELF_UNDEFINED_METHOD, RULE_UNRESOLVED_TOPLEVEL, RULE_WRONG_ARITY, RULE_ARGUMENT_TYPE, RULE_NIL_RECEIVER, RULE_DUMP_TYPE, RULE_ASSERT_TYPE, RULE_ALWAYS_RAISES, RULE_UNREACHABLE_BRANCH, RULE_DEAD_ASSIGNMENT, RULE_ALWAYS_TRUTHY_CONDITION, RULE_UNREACHABLE_CLAUSE, RULE_RETURN_TYPE, RULE_VISIBILITY_MISMATCH, RULE_OVERRIDE_VISIBILITY_REDUCED, RULE_OVERRIDE_RETURN_WIDENED, RULE_OVERRIDE_PARAM_NARROWED, RULE_IVAR_WRITE_MISMATCH ].freeze
- LEGACY_RULE_ALIASES =
Backward-compat alias table (ADR-8 § “Backward compatibility”). Existing user code with ‘# rigor:disable undefined-method` / `disable: [undefined-method]` keeps working — the legacy unprefixed identifiers map to their canonical `family.rule-name` form here. Removing the aliases is a future ADR once user code has migrated; until then, both spellings resolve identically.
{ "undefined-method" => RULE_UNDEFINED_METHOD, "self-undefined-method" => RULE_SELF_UNDEFINED_METHOD, "wrong-arity" => RULE_WRONG_ARITY, "argument-type-mismatch" => RULE_ARGUMENT_TYPE, "possible-nil-receiver" => RULE_NIL_RECEIVER, "dump-type" => RULE_DUMP_TYPE, "assert-type" => RULE_ASSERT_TYPE, "always-raises" => RULE_ALWAYS_RAISES, "unreachable-branch" => RULE_UNREACHABLE_BRANCH, "method-visibility-mismatch" => RULE_VISIBILITY_MISMATCH, "ivar-write-mismatch" => RULE_IVAR_WRITE_MISMATCH, "dead-assignment" => RULE_DEAD_ASSIGNMENT, "always-truthy-condition" => RULE_ALWAYS_TRUTHY_CONDITION, "unreachable-clause" => RULE_UNREACHABLE_CLAUSE }.freeze
- RULE_FAMILIES =
Family wildcard — a ‘<family>` token in a suppression comment or `disable:` list disables every rule whose canonical id starts with `<family>.`. Per ADR-8 § “1”.
%w[call flow assert dump def].freeze
- OVERRIDE_ANCESTOR_WALK_LIMIT =
ADR-35 slice 1 — bound for the ‘def.override-visibility-reduced` ancestor walk, and the public > protected > private ordering used to decide whether an override reduces visibility.
100- VISIBILITY_RANK =
{ public: 2, protected: 1, private: 0 }.freeze
Class Method Summary collapse
- .absorb_suppression_tokens(raw, target) ⇒ Object
-
.always_truthy_condition_diagnostics(path, results) ⇒ Object
v0.1.2 — ‘flow.always-truthy-condition`.
-
.build_node_collectors(path, scope_index) ⇒ Object
Constructs the fresh, unpopulated built-in collector set keyed by role, including the main pass.
- .call_node_diagnostics(path, node, scope_index) ⇒ Object
-
.comparable(results) ⇒ Object
Normalises a collector’s result for value comparison.
-
.dead_assignment_diagnostics(path, dead_assignments) ⇒ Object
v0.1.2 — ‘flow.dead-assignment`.
-
.diagnose(path:, root:, scope_index:, self_call_misses: [], comments: [], disabled_rules: [], node_collectors: nil) ⇒ Array<Rigor::Analysis::Diagnostic>
Yields diagnostics for every unrecognised method call on a typed receiver in ‘root`’s subtree.
-
.expand_rule_tokens(tokens) ⇒ Object
Expands a list of user-supplied rule tokens into the canonical-id set per ADR-8 § “Backward compatibility”.
- .expand_token(token) ⇒ Object
-
.filter_suppressed(diagnostics, comments:, disabled_rules:) ⇒ Object
v0.0.2 #6 — diagnostic suppression.
- .ivar_mismatch_diagnostics_for(path, class_name, ivar_name, writes) ⇒ Object
-
.ivar_write_mismatch_diagnostics(path, ivar_writes) ⇒ Object
v0.1.2 — ‘def.ivar-write-mismatch`.
-
.main_pass_node_diagnostics(path, node, scope_index) ⇒ Object
The verbatim per-node dispatch of the former inline main pass (‘diagnose`’s ‘Source::NodeWalker.each` `case`), now invoked by MainPassCollector on the shared RuleWalk.
-
.main_pass_oracle(path, root, scope_index) ⇒ Object
The former inline main pass, kept as the shadow oracle: walks the tree with ‘Source::NodeWalker.each` and accumulates the same per-node diagnostics in the same order MainPassCollector now produces them on the shared walk.
-
.node_collector_driver(collectors) ⇒ Object
A RuleWalk::CollectorDriver over a built-in collector set, for a foreign traversal to drive (ADR-53 B4).
-
.oracle_results(role, collector, path, root, scope_index) ⇒ Object
The oracle each hosted collector’s walk result is checked against.
-
.parse_suppression_comments(comments) ⇒ Array<(Hash{Integer => Set}, Set)>
Pair of ‘(line_suppressions, file_suppressions)`.
-
.resolve_rule_token(token) ⇒ Object
Resolves a user-supplied rule token (‘undefined-method`, `call.undefined-method`, or the family wildcard `call`) to the set of canonical rule identifiers it disables.
-
.run_node_collectors(path, root, scope_index) ⇒ Object
ADR-53 Track B — the RuleWalk-hosted built-in collectors (the main pass and the four fact collectors) all ride one traversal of the file instead of one walk each.
-
.shadow_verify_converged_collectors(path, root, scope_index, collectors) ⇒ Object
ADR-53 B4 — corpus-scale oracle for the CONVERGED walk: the collectors (including the main pass, ADR-53 B3c) were populated by the Plugin::NodeRuleWalk traversal, not by ‘RuleWalk.run`, so re-run each collector’s legacy oracle (the fact collectors’ ‘#collect` walk, the main pass’s inline ‘Source::NodeWalker` `case`) and assert the converged walk produced byte-identical results.
- .shadow_verify_node_collectors(path, root, scope_index, collectors) ⇒ Object
-
.unreachable_clause_diagnostics(path, results) ⇒ Object
ADR-47 — ‘flow.unreachable-clause`.
Class Method Details
.absorb_suppression_tokens(raw, target) ⇒ Object
475 476 477 478 479 |
# File 'lib/rigor/analysis/check_rules.rb', line 475 def absorb_suppression_tokens(raw, target) raw.to_s.split(/[\s,]+/).reject(&:empty?).each do |token| target.merge((token)) end end |
.always_truthy_condition_diagnostics(path, results) ⇒ Object
v0.1.2 — ‘flow.always-truthy-condition`. Fires on `if` / `unless` / ternary predicates whose inferred type is a `Type::Constant` AND that don’t fall in the literal-only / inside-loop-or-block / defensive- predicate skip envelope (see ‘Analysis::CheckRules::AlwaysTruthyConditionCollector` for the full triage rationale).
367 368 369 370 371 |
# File 'lib/rigor/analysis/check_rules.rb', line 367 def always_truthy_condition_diagnostics(path, results) results.map do |result| build_always_truthy_condition_diagnostic(path, result.node, result.polarity) end end |
.build_node_collectors(path, scope_index) ⇒ Object
Constructs the fresh, unpopulated built-in collector set keyed by role, including the main pass. Split out so the converged walk (ADR-53 B4) can build the collectors, drive them via a Rigor::Analysis::CheckRules::RuleWalk::CollectorDriver inside the single Plugin::NodeRuleWalk traversal, and hand the populated set back to diagnose as ‘node_collectors:`. The main pass needs `path` because its per-node diagnostics carry it (ADR-53 B3c hosts it on the same walk).
210 211 212 213 214 215 216 217 218 |
# File 'lib/rigor/analysis/check_rules.rb', line 210 def build_node_collectors(path, scope_index) { main_pass: MainPassCollector.new(->(node) { main_pass_node_diagnostics(path, node, scope_index) }), always_truthy: AlwaysTruthyConditionCollector.new(scope_index), unreachable_clauses: UnreachableClauseCollector.new(scope_index), ivar_writes: IvarWriteCollector.new(scope_index), dead_assignments: DeadAssignmentCollector.new(scope_index) } end |
.call_node_diagnostics(path, node, scope_index) ⇒ Object
306 307 308 309 310 311 312 313 314 315 316 317 318 |
# File 'lib/rigor/analysis/check_rules.rb', line 306 def call_node_diagnostics(path, node, scope_index) [ undefined_method_diagnostic(path, node, scope_index), unresolved_toplevel_diagnostic(path, node, scope_index), wrong_arity_diagnostic(path, node, scope_index), argument_type_diagnostic(path, node, scope_index), nil_receiver_diagnostic(path, node, scope_index), dump_type_diagnostic(path, node, scope_index), assert_type_diagnostic(path, node, scope_index), always_raises_diagnostic(path, node, scope_index), visibility_mismatch_diagnostic(path, node, scope_index) ].compact end |
.comparable(results) ⇒ Object
Normalises a collector’s result for value comparison. The fact collectors return ‘Data` / Hash structures that already compare by value; the main pass returns Diagnostic objects (plain objects with identity `==`), so serialise those to hashes first.
263 264 265 266 267 |
# File 'lib/rigor/analysis/check_rules.rb', line 263 def comparable(results) return results.map(&:to_h) if results.is_a?(Array) && results.first.is_a?(Diagnostic) results end |
.dead_assignment_diagnostics(path, dead_assignments) ⇒ Object
v0.1.2 — ‘flow.dead-assignment`. Walks every `DefNode` body and emits a diagnostic for each plain `LocalVariableWriteNode` whose target name is never read in the same body. The `Analysis::CheckRules::DeadAssignmentCollector` describes the conservative envelope.
354 355 356 357 358 |
# File 'lib/rigor/analysis/check_rules.rb', line 354 def dead_assignment_diagnostics(path, dead_assignments) dead_assignments.map do |result| build_dead_assignment_diagnostic(path, result[:write_node], result[:def_node]) end end |
.diagnose(path:, root:, scope_index:, self_call_misses: [], comments: [], disabled_rules: [], node_collectors: nil) ⇒ Array<Rigor::Analysis::Diagnostic>
Yields diagnostics for every unrecognised method call on a typed receiver in ‘root`’s subtree. The caller MUST have already produced ‘scope_index` through `Rigor::Inference::ScopeIndexer.index(root, default_scope:)`.
ADR-53 B4 — when ‘node_collectors` is supplied, the converged Plugin::NodeRuleWalk traversal has already populated the built-in collectors (including the main pass) in one shared walk with the plugin node-rules, so they are consumed as-is. When it is nil (a direct caller with no plugin walk, e.g. a unit test), the standalone RuleWalk walk runs here instead, so `diagnose` stays correct without the converged path.
169 170 171 172 173 174 175 176 177 178 179 |
# File 'lib/rigor/analysis/check_rules.rb', line 169 def diagnose(path:, root:, scope_index:, self_call_misses: [], comments: [], disabled_rules: [], node_collectors: nil) collectors = node_collectors || run_node_collectors(path, root, scope_index) diagnostics = collectors[:main_pass].results.dup diagnostics.concat(self_undefined_method_diagnostics(path, self_call_misses, root, scope_index)) diagnostics.concat(always_truthy_condition_diagnostics(path, collectors[:always_truthy].results)) diagnostics.concat(unreachable_clause_diagnostics(path, collectors[:unreachable_clauses].results)) diagnostics.concat(ivar_write_mismatch_diagnostics(path, collectors[:ivar_writes].results)) diagnostics.concat(dead_assignment_diagnostics(path, collectors[:dead_assignments].results)) filter_suppressed(diagnostics, comments: comments, disabled_rules: disabled_rules) end |
.expand_rule_tokens(tokens) ⇒ Object
Expands a list of user-supplied rule tokens into the canonical-id set per ADR-8 § “Backward compatibility”. ‘disabled_rules` accepts unprefixed legacy names (`undefined-method`), canonical names (`call.undefined-method`), and family wildcards (`call`).
486 487 488 489 490 |
# File 'lib/rigor/analysis/check_rules.rb', line 486 def (tokens) Array(tokens).each_with_object(Set.new) do |token, set| set.merge((token.to_s)) end end |
.expand_token(token) ⇒ Object
492 493 494 495 496 497 |
# File 'lib/rigor/analysis/check_rules.rb', line 492 def (token) return ["all"] if token == "all" resolved = resolve_rule_token(token) resolved.nil? || resolved.empty? ? [token] : resolved end |
.filter_suppressed(diagnostics, comments:, disabled_rules:) ⇒ Object
v0.0.2 #6 — diagnostic suppression. Three kinds of suppression compose:
-
Project-level: ‘disabled_rules` is the project’s ‘.rigor.yml` `disable:` list. Any diagnostic whose `rule` is in the list is dropped.
-
File-level (v0.1.2): ‘# rigor:disable-file <rule1>, <rule2>` anywhere in the file suppresses the matching diagnostic for every line. `# rigor:disable-file all` suppresses every rule across the file. Convention is to put the comment near the top, but Rigor accepts it anywhere — the comment scope is “this file” regardless of position.
-
**In-source line**: ‘# rigor:disable <rule1>, <rule2>` on the same line as the offending expression suppresses the matching diagnostic for that line only. `# rigor:disable all` on a line suppresses every rule on that line.
Diagnostics with ‘rule == nil` (parse errors, path errors, internal analyzer errors) are NEVER suppressed — they represent failures the user cannot silence away.
436 437 438 439 440 441 442 443 444 445 446 447 448 449 |
# File 'lib/rigor/analysis/check_rules.rb', line 436 def filter_suppressed(diagnostics, comments:, disabled_rules:) line_suppressions, file_suppressions = parse_suppression_comments(comments) disabled = (disabled_rules) diagnostics.reject do |diagnostic| rule = diagnostic.rule next false if rule.nil? next true if disabled.include?(rule) next true if file_suppressions.include?("all") || file_suppressions.include?(rule) line_rules = line_suppressions[diagnostic.line] line_rules && (line_rules.include?("all") || line_rules.include?(rule)) end end |
.ivar_mismatch_diagnostics_for(path, class_name, ivar_name, writes) ⇒ Object
383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 |
# File 'lib/rigor/analysis/check_rules.rb', line 383 def ivar_mismatch_diagnostics_for(path, class_name, ivar_name, writes) return [] if writes.size < 2 # Skip past leading `NilClass` writes when establishing # the canonical type. The common nullable-slot idiom # (`@x = nil` placeholder in `initialize` / a default # state slot, then `@x = :foo` on first concrete state) # would otherwise fire a false positive on every # concrete write because `first_class` was `NilClass` # and every subsequent `Symbol` / `String` / `Hash` # write triggered the divergence rule. The first # concrete (non-nil) write is the canonical type; # additional `NilClass` writes are still tolerated # downstream by the existing `other_class == "NilClass"` # check (the nullable-slot resets to nil between work). canonical = writes.find { |w| ivar_class_for(w[:type]) != "NilClass" } return [] if canonical.nil? first_class = ivar_class_for(canonical[:type]) return [] if first_class.nil? canonical_index = writes.index(canonical) writes[(canonical_index + 1)..].filter_map do |write| other_class = ivar_class_for(write[:type]) next nil if other_class.nil? || other_class == "NilClass" || other_class == first_class build_ivar_write_mismatch_diagnostic(path, write[:node], class_name, ivar_name, first_class, other_class) end end |
.ivar_write_mismatch_diagnostics(path, ivar_writes) ⇒ Object
v0.1.2 — ‘def.ivar-write-mismatch`. Walks every ClassNode / ModuleNode body, gathers per-class ivar writes with their rvalue types, and emits a diagnostic when a later write’s concrete class disagrees with the first write’s. The first write per (class, ivar) is treated as the “declared” type; subsequent writes that land on a different concrete class trigger.
Conservative envelope:
-
Only fires when both the first and the offending write resolve to a ‘concrete_class_name` (Nominal / Singleton / Constant / Tuple → “Array” / HashShape →“Hash”). Unions / Dynamic / IntegerRange / shape- varied carriers fall through.
-
‘NilClass` is an intentional widening idiom (`@x = “value”` then later `@x = nil` to “clear”) — skipped.
-
Singleton-method (‘def self.foo`) bodies are skipped. Class-level ivars (`@x = 1` outside any def, in the class body) are also skipped — they’re a separate surface (‘Module#@var`) the engine doesn’t yet model.
340 341 342 343 344 345 346 |
# File 'lib/rigor/analysis/check_rules.rb', line 340 def ivar_write_mismatch_diagnostics(path, ivar_writes) ivar_writes.flat_map do |class_name, writes_by_ivar| writes_by_ivar.flat_map do |ivar_name, writes| ivar_mismatch_diagnostics_for(path, class_name, ivar_name, writes) end end end |
.main_pass_node_diagnostics(path, node, scope_index) ⇒ Object
The verbatim per-node dispatch of the former inline main pass (‘diagnose`’s ‘Source::NodeWalker.each` `case`), now invoked by MainPassCollector on the shared RuleWalk. Returns the diagnostics for one node, in the same emission order as before.
185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 |
# File 'lib/rigor/analysis/check_rules.rb', line 185 def main_pass_node_diagnostics(path, node, scope_index) case node when Prism::CallNode call_node_diagnostics(path, node, scope_index) when Prism::DefNode [ return_type_mismatch_diagnostic(path, node, scope_index), override_visibility_diagnostic(path, node, scope_index), override_return_widened_diagnostic(path, node, scope_index), override_param_narrowed_diagnostic(path, node, scope_index) ].compact when Prism::IfNode, Prism::UnlessNode [unreachable_branch_diagnostic(path, node, scope_index)].compact else [] end end |
.main_pass_oracle(path, root, scope_index) ⇒ Object
The former inline main pass, kept as the shadow oracle: walks the tree with ‘Source::NodeWalker.each` and accumulates the same per-node diagnostics in the same order MainPassCollector now produces them on the shared walk.
283 284 285 286 287 288 289 |
# File 'lib/rigor/analysis/check_rules.rb', line 283 def main_pass_oracle(path, root, scope_index) diagnostics = [] Source::NodeWalker.each(root) do |node| diagnostics.concat(main_pass_node_diagnostics(path, node, scope_index)) end diagnostics end |
.node_collector_driver(collectors) ⇒ Object
A Rigor::Analysis::CheckRules::RuleWalk::CollectorDriver over a built-in collector set, for a foreign traversal to drive (ADR-53 B4). The driver visits each node and derives child contexts exactly as the standalone RuleWalk walk would.
224 225 226 |
# File 'lib/rigor/analysis/check_rules.rb', line 224 def node_collector_driver(collectors) RuleWalk::CollectorDriver.new(collectors.values) end |
.oracle_results(role, collector, path, root, scope_index) ⇒ Object
The oracle each hosted collector’s walk result is checked against. The fact collectors re-run their legacy single-collector ‘#collect` walk; the main pass re-runs the former inline `Source::NodeWalker` `case` (`main_pass_oracle`) since its diagnostics are the result.
273 274 275 276 277 |
# File 'lib/rigor/analysis/check_rules.rb', line 273 def oracle_results(role, collector, path, root, scope_index) return main_pass_oracle(path, root, scope_index) if role == :main_pass collector.class.new(scope_index).collect(root) end |
.parse_suppression_comments(comments) ⇒ Array<(Hash{Integer => Set}, Set)>
Returns pair of ‘(line_suppressions, file_suppressions)`. Line suppressions are keyed by source line number; file suppressions apply to every line.
461 462 463 464 465 466 467 468 469 470 471 472 473 |
# File 'lib/rigor/analysis/check_rules.rb', line 461 def parse_suppression_comments(comments) line_suppressions = Hash.new { |h, k| h[k] = Set.new } file_suppressions = Set.new comments.each do |comment| source = comment.location.slice if (match = FILE_SUPPRESSION_PATTERN.match(source)) absorb_suppression_tokens(match[:rules], file_suppressions) elsif (match = LINE_SUPPRESSION_PATTERN.match(source)) absorb_suppression_tokens(match[:rules], line_suppressions[comment.location.start_line]) end end [line_suppressions, file_suppressions] end |
.resolve_rule_token(token) ⇒ Object
Resolves a user-supplied rule token (‘undefined-method`, `call.undefined-method`, or the family wildcard `call`) to the set of canonical rule identifiers it disables. Returns `nil` for `“all”` (the existing wildcard meaning “every rule”), or for unknown tokens.
141 142 143 144 145 146 147 |
# File 'lib/rigor/analysis/check_rules.rb', line 141 def self.resolve_rule_token(token) return nil if token == "all" return [LEGACY_RULE_ALIASES.fetch(token)] if LEGACY_RULE_ALIASES.key?(token) return ALL_RULES.select { |r| r.start_with?("#{token}.") } if RULE_FAMILIES.include?(token) ALL_RULES.include?(token) ? [token] : [] end |
.run_node_collectors(path, root, scope_index) ⇒ Object
ADR-53 Track B — the RuleWalk-hosted built-in collectors (the main pass and the four fact collectors) all ride one traversal of the file instead of one walk each. Returns the populated collectors keyed by role so the caller can build the diagnostics from each collector’s ‘results`. Used on the standalone path (no converged plugin walk); the converged path populates the same collector set via node_collector_driver instead.
Under ‘RIGOR_SHADOW_RULE_WALK=1` each hosted collector’s legacy single-collector ‘#collect` walk also runs as the oracle and any divergence aborts the run — the corpus-scale half of the equivalence harness (the curated half is `rule_walk_equivalence_spec`).
240 241 242 243 244 245 |
# File 'lib/rigor/analysis/check_rules.rb', line 240 def run_node_collectors(path, root, scope_index) collectors = build_node_collectors(path, scope_index) RuleWalk.run(root, collectors.values) shadow_verify_node_collectors(path, root, scope_index, collectors) if ENV["RIGOR_SHADOW_RULE_WALK"] collectors end |
.shadow_verify_converged_collectors(path, root, scope_index, collectors) ⇒ Object
ADR-53 B4 — corpus-scale oracle for the CONVERGED walk: the collectors (including the main pass, ADR-53 B3c) were populated by the Plugin::NodeRuleWalk traversal, not by ‘RuleWalk.run`, so re-run each collector’s legacy oracle (the fact collectors’ ‘#collect` walk, the main pass’s inline ‘Source::NodeWalker` `case`) and assert the converged walk produced byte-identical results. Same divergence contract as shadow_verify_node_collectors; nil collectors (caller without built-in collection) is a no-op. `path` is threaded because the main pass’s oracle carries it.
300 301 302 303 304 |
# File 'lib/rigor/analysis/check_rules.rb', line 300 def shadow_verify_converged_collectors(path, root, scope_index, collectors) return if collectors.nil? shadow_verify_node_collectors(path, root, scope_index, collectors) end |
.shadow_verify_node_collectors(path, root, scope_index, collectors) ⇒ Object
247 248 249 250 251 252 253 254 255 256 257 |
# File 'lib/rigor/analysis/check_rules.rb', line 247 def shadow_verify_node_collectors(path, root, scope_index, collectors) divergences = collectors.filter_map do |role, collector| legacy = oracle_results(role, collector, path, root, scope_index) next if comparable(legacy) == comparable(collector.results) "#{role} legacy=#{legacy.size} walk=#{collector.results.size}" end return if divergences.empty? raise "RIGOR_SHADOW_RULE_WALK divergence: #{divergences.join('; ')}" end |
.unreachable_clause_diagnostics(path, results) ⇒ Object
ADR-47 — ‘flow.unreachable-clause`. One diagnostic per `when` clause the flow engine’s narrowing proves can never match (its narrowed subject is ‘bot`). The squiggle lands on the dead clause’s body, mirroring ‘flow.unreachable-branch`.
377 378 379 380 381 |
# File 'lib/rigor/analysis/check_rules.rb', line 377 def unreachable_clause_diagnostics(path, results) results.map do |result| build_unreachable_clause_diagnostic(path, result) end end |