Sonicop

A fast, native RuboCop-compatible Ruby linter and formatter written in Rust.

CI Gem Version License

English | 日本語


Overview

Sonicop is a fast Ruby linter and formatter that runs as a native executable without starting a Ruby process. Existing .rubocop.yml files work as-is, including nested configuration, inheritance, file inclusion and exclusion, severity, and autocorrect settings.

It uses the actively maintained owayo/tree-sitter-ruby grammar, inspects files in parallel, and applies corrections atomically. Its RuboCop 1.89-compatible CLI and JSON output fit existing editor and CI integrations with minimal changes.

Features

Sonicop implements cops in the Bundler, Gemspec, Layout, Lint, Metrics, Migration, Naming, Security, and Style departments. The implemented set grows with each release, so the binary itself is the authoritative list:

# Every recognized cop and its implementation status
sonicop --show-cops

The bundled upstream configuration recognizes all 609 RuboCop 1.89 cops. Implemented cops run normally; recognized but not-yet-implemented cops remain configuration-compatible and are reported by --debug. Truly unknown cop names still fail validation unless --ignore-unrecognized-cops is supplied.

Installation

gem install sonicop

Platform gems include native executables for Linux, macOS, and Windows. When a prebuilt platform gem is unavailable, the source gem builds the executable with Cargo during installation.

You can also install the latest source directly:

cargo install --git https://github.com/owayo/sonicop

Usage

# Inspect the current project
sonicop

# Select cops or departments
sonicop --only Layout,Style/StringLiterals app spec

# Safe correction / all correction
sonicop -a
sonicop -A

# RuboCop-shaped JSON
sonicop --format json

# Editor input
printf '%s\n' 'value=10000' | sonicop --stdin example.rb --format json

# List recognized cops and their implementation status
sonicop --show-cops

Key compatibility flags include -l, -x, --only, --except, -s/--stdin, -P/--parallel, -f/--format, -a/--autocorrect, -A/--autocorrect-all, -L/--list-target-files, -c/--config, -v/--version, and -V/--verbose-version.

Configuration

Sonicop resolves .rubocop.yml from each target file, so nested configurations apply within one run. Local and HTTPS inherit_from, inherit_gem, inherit_mode, AllCops/DisabledByDefault, Include, and Exclude, plus per-cop Enabled, Exclude, Severity, Safe, SafeAutoCorrect, and cop settings are supported. Cops supplied by declared plugins are accepted as recognized-but-unimplemented without executing Ruby plugin code.

inherit_from: .rubocop_todo.yml

AllCops:
  Exclude:
    - "vendor/**/*"

Layout/LineLength:
  Max: 100

Style/StringLiterals:
  EnforcedStyle: single_quotes

The CLI accepts RuboCop's server/LSP/MCP, plugin, and cache flags to keep existing command lines parse-compatible. These flags are reported as compatibility no-ops when they request unsupported functionality. Sonicop does not currently provide server transports, Ruby plugin execution, cache reuse, custom Ruby cops, or cops outside the implemented set.

Conformance

The implemented cops are verified against RuboCop 1.89.0 over five Ruby projects — RuboCop itself, Rails, Ruby, Homebrew and Mastodon — totalling 18,246 files, with the upstream default configuration on both sides. Every offense is compared by cop, path, line, column, last line, last column, length, message, severity and correctability.

Three of the five match exactly: RuboCop's own tree (5,766 offenses), Rails (167,535) and Mastodon (15,286), with no excess, no shortfall and no metadata differences. The target file lists match exactly on all five. What remains is concentrated in Lint/Syntax, where RuboCop's LALR parser recovers from an error and emits diagnostics a tree-sitter parse cannot reconstruct. Autocorrect is byte-identical on RuboCop's own tree and on Mastodon, the two corpora held as a hard line: a change that breaks byte equality there is a regression, not a new known divergence.

See CONFORMANCE.md for the commands, the per-corpus numbers, and the two ways a measurement of this kind can mislead you.

Performance

Measured over all five conformance corpora. Both tools were given their own bundled default configuration (--force-default-config), so neither reads the project's .rubocop.yml, and on every corpus the two resolve the same file list.

Both tools run their full default set — the same 394 cops, matched name for name — so neither side is restricted and the comparison is like-for-like as it stands. Times are the fastest of two warmed runs.

Corpus Files RuboCop parallel Sonicop parallel RuboCop single Sonicop single
rubocop/rubocop 1,765 10.64 s 2.84 s 40.66 s 13.60 s
mastodon/mastodon 3,290 10.78 s 3.13 s 33.22 s 13.57 s
Homebrew/brew 2,176 11.13 s 4.61 s 37.89 s 10.81 s
rails/rails 3,550 23.84 s 10.15 s 84.93 s 33.69 s
ruby/ruby 7,465 95.71 s 35.74 s 258.87 s 107.37 s

The gap is 2.4x to 3.8x in parallel and 2.4x to 3.5x single-process, so no single corpus summarizes it. The single-process column is the steadier of the two — it measures the engines rather than how well each one's parallelism happens to fit the tree it was pointed at.

The speed is not bought by skipping work: over those same 394 cops the two agree on every offense on RuboCop's own tree and on Mastodon, and autocorrect there is byte-identical.

Two details matter for reproducing this. RuboCop silently turns --parallel off when combined with --cache false, so its parallel runs here use a cache directory that is deleted before each run rather than disabled; timing it with --cache false --parallel measures a single process and overstates the difference. RuboCop's default is a single process, while Sonicop is parallel unless --no-parallel is passed.

# RuboCop, parallel, cold cache, its full default set of 394 cops
rubocop --force-default-config --cache true --cache-root "$(mktemp -d)" \
        --no-color --parallel -f quiet

# Sonicop
sonicop --force-default-config --format quiet

Machine: Apple M2 (8 cores), Ruby 4.0.6 with YJIT available, RubyGems-installed RuboCop 1.89.0. The numbers above were taken with a one-minute load average between 4 and 29 — the machine was in use, not idle. Both tools ran under the same conditions, so the ratios hold, but the absolute seconds are not a floor: expect better on a quiet machine. Anything competing for cores inflates both sides, and not by the same factor on each, so measure on an otherwise idle machine when the absolute numbers matter to you.

Development

make is the single entry point; make help lists every target. The Rakefile holds the gem packaging tasks that make delegates to.

make build   # debug build
make check   # fmt, clippy, Rust tests, Ruby wrapper tests, version consistency
make gem     # source gem

Adding a cop

A cop is one file under src/rules/<department>/<cop>.rs exposing a single check(context, offenses), plus one line in that department's mod.rs:

department_rules! {
    "Layout";
    line_length => ("LineLength", Convention),
}

That line is the only place the cop's name and default severity are written. Inside the cop the name stays implicit: context.setting("Max") reads Layout/LineLength: Max, and context.offense(message, range) reports under the cop's own name at its configured severity. A cop that spelled its name a second time could disagree with the registry, and nothing in the type system would catch it.

Prefer context.nodes_of("kind") over walking every node: each cop runs on every file, so a full walk per cop is what makes inspection scale with the registry rather than with the file.

Cargo.toml is the single source of truth for the version. lib/sonicop/version.rb is generated from it by make version-sync and committed, because the gemspec reads it at package time. CI fails when the two disagree.

config/default.yml is vendored from upstream RuboCop; re-fetch it with scripts/sync_default_yml.sh <rubocop-version>, which records the source version in the file header.

Dependencies are updated with depup --install. The Ruby grammar dependency is pinned to an exact fork commit in Cargo.toml for reproducible builds.

License

MIT. The bundled RuboCop default configuration and parser dependency retain their upstream notices in NOTICE and licenses/.