| status | shipped |
|---|---|
| stability | first cut |
| ship_state | full applicable rule set; type-discipline rules silently skip |
| updated | 2026-05-05 |
Ruby (.rb) is a first-class language for slop's structural,
information-theoretic, and lexical rules, including the CK
class-metrics suite with open-class aggregation. Add it to your
.slop.toml languages list or pass --language ruby on the CLI.
| Rule | Ruby status | Notes |
|---|---|---|
structural.complexity.cyclomatic (McCabe CCX) |
full | Decision nodes: if, elsif, unless_modifier, if_modifier, while_modifier, until_modifier, rescue_modifier, while, until, for, when, rescue, conditional (ternary). Boolean operators: &&, ||, and, or on binary nodes. |
structural.complexity.cognitive (Campbell CogC) |
full | Same nodes; elsif and when are compensating decisions (logically same depth as the enclosing if/case). |
structural.complexity.npath (Nejmeh NPATH) |
full | Sequential postfix-ifs multiply, when-clauses sum, rescue contributes a path. Ruby's positional body shape is handled via body_skip_types (Julia precedent). |
information.volume / information.difficulty |
full | Operator/operand tables enumerate Ruby keywords (def, class, module, if, case, when, begin, rescue, require, include, attr_accessor, ...) and the symbol set including Ruby-unique tokens (<=>, .., ..., =>, ->, &&=, ||=). |
structural.deps |
best-effort | Tree-sitter query matches require, require_relative, load calls; the kernel's index resolves require_relative './foo' to a peer file. require 'gem_name' (no ./ prefix) is treated as external. Does not follow gem $LOAD_PATH — slop has no Ruby runtime context. |
structural.packages (Martin's I/A/D′) |
full | Modules are the natural abstract analog (Module instances cannot be instantiated, only mixed in via include/extend). Classes are concrete. Ruby has no final keyword. Default severity = "warning". |
structural.class.complexity (WMC) |
full, with open-class aggregation | Per-class WMC is the sum of method CCXs across every class Foo re-opening. Ruby's open-class semantics let the same class be redefined in multiple files; _aggregate_ruby_open_classes merges them by name into a single CK entity post-WMC, summing method counts and unioning superclasses. See "Open-class aggregation" below. |
structural.class.coupling (CBO) |
full | Counts distinct types referenced inside the class body (member references, method call receivers, mixin targets). Mixed-in module names DO count toward CBO. |
structural.class.inheritance.depth (DIT) |
full | Walks the superclass field. Mixins (include/extend) do NOT count toward DIT — Ruby community convention treats them as composition. CBO captures mixin coupling. |
structural.class.inheritance.children (NOC) |
full | Counts direct subclasses via the inverse superclass relation. |
structural.hotspots |
full | Composes structural.complexity.cyclomatic + git churn. |
structural.orphans |
full | Definition queries cover regular methods, singleton methods (def self.foo), operator methods (def ==), classes, and modules. |
structural.god_module |
full | Counts top-level method, singleton_method, class, module definitions. |
structural.duplication (clone density) |
full | Function-body fingerprinting on method, singleton_method, lambda, do_block, block. |
structural.redundancy (sibling calls) |
partial | Top-level free methods only (Ruby's idiom for "free function" — def outside any class/module). Class methods are not analysed for sibling redundancy. Ruby stdlib idioms (puts, each, map, attr_accessor, ...) filtered out. |
structural.local_imports |
yes | Flags require/require_relative/load calls inside method bodies. v1.0.3 added a per-language predicate hook (IMPORT_NODE_PREDICATES) so the kernel can distinguish require-style calls from ordinary calls. |
structural.types.sentinels (stringly-typed) |
yes | Flags method parameters whose name (lowercased, trailing _ stripped) is in the sentinel list. Ruby has no static type annotations to gate on, so the rule fires on any sentinel-named parameter. The advisory message text references "Literal[...] or Enum" — the suggested remedy in Ruby would be a typed Symbol enum or a Set constant. Wording will be language-aware in a follow-up. |
information.magic_literals |
full | Counts distinct non-trivial integer / float / complex / rational literals per method. |
information.section_comments |
full | Detects #-style dividers inside method bodies. |
lexical.stutter |
full | Function and class scopes; namespaces (modules) also count as class-equivalent scope. |
lexical.verbosity / lexical.tersity |
full | Reuses the language-agnostic identifier-token splitter. |
Two rules don't apply to Ruby and are intentionally not registered:
structural.types.escape_hatches(any-type density). Ruby is dynamically typed — every parameter is implicitlyObject. There is no type system to escape, so "fraction of annotations using an escape-hatch type" has no meaningful definition. Same posture as CK on C (no class concept).structural.types.hidden_mutators(out-parameters). Ruby's parameter-passing is always by reference; every object is mutable. "Callee mutates a parameter" is the language default rather than an anti-pattern. Without a static type system the rule has no signal-to-noise floor; silent no-op.
Both decisions are documented with a comment block in the respective kernel's _LANG_GLOBS / _LANG_CONFIG location.
Ruby's open-class semantics let the same class be redefined across multiple files. Each re-opening parses as a fresh class node, so each becomes a distinct _ClassInfo entry in slop's CK kernel. Without aggregation, a single class redefined in 3 files would produce 3 entries in the output.
v1.0.3's _aggregate_ruby_open_classes post-WMC pass merges them by name within the Ruby subset:
- WMC and
method_countare summed (every method definition contributes). - CBO, DIT, NOC are taken as the max (CBO is per-class-instance and may legitimately differ; DIT/NOC are computed once per class name, so max is a safe defense).
- Superclasses are unioned (Ruby allows different re-openings to declare different superclasses, though that's pathological — slop reports both).
- The canonical file:line is the first-encountered definition.
This means a single output entry like:
shapes.rb:21 Circle — WMC 12, methods 5
reflects every method of Circle across every file that re-opened it, even if no single file contains all 5 methods.
Limitation: glob-bounded aggregation. If your --root glob covers only some of the files that re-open Foo, the aggregated WMC reflects only the visible openings. Widening the scan changes the metric — same posture as deps without -I paths.
Ruby mixins (include MyMod / extend MyMod / prepend MyMod) are call-statements inside the class body, not part of the superclass field. v1.0.3's posture:
- CBO: yes, mixed-in module names count as references (matches the rule's "distinct types referenced" intent).
- DIT: no, mixin chains do NOT contribute to inheritance depth.
- NOC: no, including a module does not count as "subclassing" it.
The Ruby community is split on whether mixins should count for DIT. The conservative posture (don't) keeps DIT's semantics consistent across languages. A Rails application heavy on Concern mixins might surface high CBO without high DIT — that's correct: the modules are coupled in but not inherited from.
require_relative './foo'resolves to a peerfoo.rb.require_relative '../lib/util'resolves through directory traversal.require 'gem_name'(no./prefix) is treated as external (gem from$LOAD_PATH).load 'config.rb'resolves the same way asrequire_relative.
slop does not follow Ruby's $LOAD_PATH, Bundler's Gemfile, or RUBYLIB. A codebase that adds custom load paths will see those imports as external. Same posture as Rust/C/C++ deps.
Defaults are tuned for Python/JS/Go and not re-calibrated against a Ruby corpus. Expected deviations:
- Ruby methods tend to run lower in CCX than Python on the same problem — Ruby idiomatically uses block-style iteration (
.each { ... },.map { ... }) which slop counts as separate anonymous functions rather than as part of the enclosing method. - Halstead operand counts are inflated by Ruby's symbol-rich syntax (
@var,@@var,$var,:foosymbols all contribute distinct operand types). structural.types.sentinelsmay produce noise on Rails / DSL-heavy codebases where parameters namedkind,mode,stateare pervasive idioms. Defaultseverity = "warning"keeps it advisory.structural.duplicationmay catch boilerplateattr_accessor/def initializepatterns. Consider raising the threshold on Rails models.
If you're using slop on a Ruby codebase, run slop lint --output json and inspect the per-method distribution before settling on thresholds.
tree-sitter-ruby >= 0.21.0. Node-type names that the kernels depend on:
- Method shape:
method,singleton_method,lambda,do_block,block,block_body,body_statement. - Method-name carriers:
identifier,operator(for operator overloads). - Class / module shape:
class,module,superclass,constant. - Control flow:
if,elsif,else,case,when,while,until,for,begin,rescue,ensure,conditional,if_modifier,unless_modifier,while_modifier,until_modifier,rescue_modifier. - Calls / imports:
call(withmethod:andarguments:fields),argument_list,string,string_content. - Operands:
identifier,instance_variable,class_variable,global_variable,constant,integer,float,complex,rational,string,symbol.
If queries fail after a tree-sitter-ruby upgrade, the candidates are:
- The
method/singleton_methodshape (positionaldef/self/.then identifier/operator). - The
superclassfield onclass. - The
callnode'smethod:field name.
- Mixin-aware DIT. Some teams want
include/extendto count toward inheritance depth (the way RubyMethod.ancestorsincludes mixed-in modules). Could be exposed as a per-rule param (include_mixins=true) with a follow-up. define_methodand other metaprogramming. Methods defined dynamically don't appear asmethodnodes in the AST. Same posture as macro-aware analysis in C/C++ — out of slop's substrate.- Sentinel rule message wording. Currently references "Literal[...] or Enum"; should suggest typed
Symbolconstants for Ruby.
- Cross-file open-class definitions outside the scan. If
class Foois re-opened in a file outside your--rootscan, those methods don't aggregate. Slop only sees what's visible. $LOAD_PATH/ Bundler /RUBYLIBfor deps. Requires Ruby runtime context to evaluate; out of substrate.
structural.types.escape_hatchesandstructural.types.hidden_mutators— silent no-op, see "Structurally N/A" above.- Class methods inside
structural.redundancy. Sibling-call redundancy is free-function-only on Ruby. Class methods coordinate within a class — different signal from free-function siblings. - ERB / template files.
.rb.erbfiles contain mixed Ruby + ERB markup; not a clean parse target.
Ruby uses .rb for both library and application code. .rake, .gemspec, .ru, etc. are not currently mapped — add them to your globs explicitly if you want them analysed as Ruby.