Module: Rigor::Analysis::CheckRules
- Defined in:
- lib/rigor/analysis/check_rules.rb
Overview
First-preview catalogue of ‘rigor check` diagnostic rules.
The rules are intentionally narrow: they fire ONLY when the engine is confident enough to make a useful claim, and they 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 first shipped rule, ‘UndefinedMethodOnTypedReceiver`, flags an explicit-receiver `Prism::CallNode` whose receiver statically resolves to a `Type::Nominal` or `Type::Singleton` known to the analyzer’s RBS environment AND whose method name does not appear on that class’s instance / singleton method table. This is the canonical “type check” signal (“Foo has no method bar”), but it explicitly 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`, `HashShape`, `Constant`) — their dispatch goes through `ShapeDispatch` / `ConstantFolding` which the rule does not yet model.
-
receivers whose class name is NOT registered in the loader (RBS-blind environments, unknown stdlib).
The above list is the deliberate conservative envelope of the first preview; later slices broaden it. rubocop:disable Metrics/ModuleLength
Constant Summary collapse
- RULE_UNDEFINED_METHOD =
Stable identifiers for each rule. Used by the configuration ‘disable:` list and the in-source `# rigor:disable <rule>` suppression comment system to identify diagnostics by category. Rule identifiers are kebab-case strings; new rules MUST register here so user configuration can refer to them.
"undefined-method"- RULE_WRONG_ARITY =
"wrong-arity"- RULE_ARGUMENT_TYPE =
"argument-type-mismatch"- RULE_NIL_RECEIVER =
"possible-nil-receiver"- RULE_DUMP_TYPE =
"dump-type"- RULE_ASSERT_TYPE =
"assert-type"- ALL_RULES =
[ RULE_UNDEFINED_METHOD, RULE_WRONG_ARITY, RULE_ARGUMENT_TYPE, RULE_NIL_RECEIVER, RULE_DUMP_TYPE, RULE_ASSERT_TYPE ].freeze
Class Method Summary collapse
-
.diagnose(path:, root:, scope_index:, comments: [], disabled_rules: []) ⇒ Array<Rigor::Analysis::Diagnostic>
Yields diagnostics for every unrecognised method call on a typed receiver in ‘root`’s subtree.
-
.filter_suppressed(diagnostics, comments:, disabled_rules:) ⇒ Object
v0.0.2 #6 — diagnostic suppression.
- .parse_suppression_comments(comments) ⇒ Object
Class Method Details
.diagnose(path:, root:, scope_index:, comments: [], disabled_rules: []) ⇒ 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:)`.
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
# File 'lib/rigor/analysis/check_rules.rb', line 78 def diagnose(path:, root:, scope_index:, comments: [], disabled_rules: []) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity diagnostics = [] Source::NodeWalker.each(root) do |node| next unless node.is_a?(Prism::CallNode) diagnostic = undefined_method_diagnostic(path, node, scope_index) diagnostics << diagnostic if diagnostic arity_diagnostic = wrong_arity_diagnostic(path, node, scope_index) diagnostics << arity_diagnostic if arity_diagnostic arg_type_diagnostic = argument_type_diagnostic(path, node, scope_index) diagnostics << arg_type_diagnostic if arg_type_diagnostic nil_diagnostic = nil_receiver_diagnostic(path, node, scope_index) diagnostics << nil_diagnostic if nil_diagnostic dump_diagnostic = dump_type_diagnostic(path, node, scope_index) diagnostics << dump_diagnostic if dump_diagnostic assert_diagnostic = assert_type_diagnostic(path, node, scope_index) diagnostics << assert_diagnostic if assert_diagnostic end filter_suppressed(diagnostics, comments: comments, disabled_rules: disabled_rules) end |
.filter_suppressed(diagnostics, comments:, disabled_rules:) ⇒ Object
v0.0.2 #6 — diagnostic suppression. Two 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.
-
In-source: ‘# 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.
120 121 122 123 124 125 126 127 128 129 130 131 132 |
# File 'lib/rigor/analysis/check_rules.rb', line 120 def filter_suppressed(diagnostics, comments:, disabled_rules:) suppressions = parse_suppression_comments(comments) disabled = disabled_rules.to_set(&:to_s) diagnostics.reject do |diagnostic| rule = diagnostic.rule next false if rule.nil? next true if disabled.include?(rule) line_rules = suppressions[diagnostic.line] line_rules && (line_rules.include?("all") || line_rules.include?(rule)) end end |
.parse_suppression_comments(comments) ⇒ Object
137 138 139 140 141 142 143 144 145 146 147 148 |
# File 'lib/rigor/analysis/check_rules.rb', line 137 def parse_suppression_comments(comments) result = Hash.new { |h, k| h[k] = Set.new } comments.each do |comment| source = comment.location.slice match = SUPPRESSION_PATTERN.match(source) next if match.nil? rules = match[:rules].to_s.split(/[\s,]+/).reject(&:empty?) rules.each { |rule| result[comment.location.start_line] << rule } end result end |