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 =
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_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_RETURN_TYPE =
"def.return-type-mismatch"- ALL_RULES =
[ RULE_UNDEFINED_METHOD, RULE_WRONG_ARITY, RULE_ARGUMENT_TYPE, RULE_NIL_RECEIVER, RULE_DUMP_TYPE, RULE_ASSERT_TYPE, RULE_ALWAYS_RAISES, RULE_RETURN_TYPE ].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, "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 }.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
Class Method Summary collapse
- .call_node_diagnostics(path, node, scope_index) ⇒ Object
-
.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.
-
.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.
- .parse_suppression_comments(comments) ⇒ Object
-
.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.
Class Method Details
.call_node_diagnostics(path, node, scope_index) ⇒ Object
137 138 139 140 141 142 143 144 145 146 147 |
# File 'lib/rigor/analysis/check_rules.rb', line 137 def call_node_diagnostics(path, node, scope_index) [ undefined_method_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) ].compact end |
.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:)`.
124 125 126 127 128 129 130 131 132 133 134 135 |
# File 'lib/rigor/analysis/check_rules.rb', line 124 def diagnose(path:, root:, scope_index:, comments: [], disabled_rules: []) diagnostics = [] Source::NodeWalker.each(root) do |node| if node.is_a?(Prism::CallNode) diagnostics.concat(call_node_diagnostics(path, node, scope_index)) elsif node.is_a?(Prism::DefNode) return_diagnostic = return_type_mismatch_diagnostic(path, node, scope_index) diagnostics << return_diagnostic if return_diagnostic end end 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`).
200 201 202 203 204 |
# File 'lib/rigor/analysis/check_rules.rb', line 200 def (tokens) Array(tokens).each_with_object(Set.new) do |token, set| set.merge((token.to_s)) end end |
.expand_token(token) ⇒ Object
206 207 208 209 210 211 |
# File 'lib/rigor/analysis/check_rules.rb', line 206 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. 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.
165 166 167 168 169 170 171 172 173 174 175 176 177 |
# File 'lib/rigor/analysis/check_rules.rb', line 165 def filter_suppressed(diagnostics, comments:, disabled_rules:) 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) line_rules = suppressions[diagnostic.line] line_rules && (line_rules.include?("all") || line_rules.include?(rule)) end end |
.parse_suppression_comments(comments) ⇒ Object
182 183 184 185 186 187 188 189 190 191 192 193 |
# File 'lib/rigor/analysis/check_rules.rb', line 182 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 { |token| result[comment.location.start_line].merge((token)) } end result 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.
104 105 106 107 108 109 110 |
# File 'lib/rigor/analysis/check_rules.rb', line 104 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 |