Project

shirobai

0.0
The project is in a healthy, maintained state
Drop-in Rust replacement for heavy RuboCop cops
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

= 1.87.0
 Project Readme

shirobai

shirobai is an experimental gem that speeds up RuboCop by replacing some of its cops with fully compatible Rust implementations.

日本語版は README.ja.md

Warning

This gem is experimental. I try hard to stay compatible with RuboCop, but I make no guarantee about production use.

Why shirobai exists

A drop-in for RuboCop, not a replacement

When people try to speed up a linter, they often rewrite everything from scratch with a new interface. shirobai does the opposite: RuboCop stays in charge, and shirobai only replaces the slow parts of each cop (like AST walks) with Rust code.

I respect RuboCop's large ecosystem and its design that lets developers write their own cops. I have no intention to compete with it.

Full compatibility with RuboCop

shirobai treats the behavior tested by each stock cop's spec as the absolute truth.

I also run the real rubocop CLI on these repositories using each project's own config, and check that shirobai gives the same results as stock RuboCop:

I also hope to contribute back to RuboCop when I find behavior that should be tested by spec but isn't.

About the name

In Japan, police officers who patrol on motorcycles are called "shiro-bai" (white bikes). The image is simple: RuboCop hops on a shiro-bai and gets faster.

Current status

  • 63 cops reimplemented in Rust (Lint / Layout / Metrics / Naming / Style).

  • Full drop-in compatibility verified on real codebases. For every implemented cop, every offense position, message, and autocorrected byte matches stock RuboCop. I do not ship a cop with pending autocorrect. If a cop cannot reach full compatibility, I remove it.

  • Real-world speedup — real CLI, each project's own .rubocop.yml, all plugin gems installed, 3-round median:

    Corpus files stock shirobai saving
    Mastodon 3,225 92.00s 73.82s -18.18s (-19.8%)
    Discourse 10,519 218.75s 199.12s -19.62s (-9.0%)
    Redmine 1,125 44.11s 34.26s -9.85s (-22.3%)

    Measured on: Intel Core i9-9900K (8C/16T, 3.60 GHz) / 32 GB RAM / KIOXIA EXCERIA SSD / devcontainer (Docker 29.1) on Ubuntu 24.04 / Ruby 4.0.5 (+PRISM) / Rust 1.96.0

    shirobai only replaces cops from the rubocop gem itself. Plugin cops (rubocop-rails, rubocop-rspec, etc.) run unchanged, so projects that spend more time on plugin cops see a lower percentage improvement.

    RuboCop itself and fluentd are also used for compatibility verification, but their configs disable most default cops or exclude most files, leaving very few rubocop-gem cops for shirobai to replace.

Requirements

Important

shirobai's native extension is written in Rust. bundle install runs cargo build --release, so you need Rust toolchain (stable, 1.75 or newer) on the machine where you install. Install it with rustup first.

RuboCop pinned to = 1.87.0
Ruby >= 3.1
Rust >= 1.75 (stable)
Platforms Linux / macOS (anywhere cargo build --release works)
Ruby parser ruby-prism (Latest grammar ≈ Ruby 4.1)

The hard pin on RuboCop is on purpose. shirobai copies cop behavior at the byte level, so even a minor RuboCop update can break compatibility. I prefer a failed install over a silent difference.

Known limitation: AllCops/TargetRubyVersion

shirobai always parses with prism's Latest grammar. In practice, the only cop affected is Layout/SpaceAroundKeyword when detecting the Ruby 2.7 expr in pat one-line pattern match. All other implemented cops work the same regardless of TargetRubyVersion. If you need strict target-version behavior for that one cop, you can disable shirobai's replacement in your config; the stock cop will run instead.

Installation

Add to your Gemfile next to rubocop:

gem "rubocop", "= 1.87.0"
gem "shirobai"

Then run bundle install.

Usage

Add one line to your .rubocop.yml:

require:
  - shirobai

That's it. shirobai registers each Rust-backed cop under the same badge as the stock cop, so everything in RuboCop keeps working as before: config, disable comments, --only, --except, --auto-correct, ResultCache, and so on. No other .rubocop.yml change is needed.

How it works

┌───────────────────────────────────────────────────────────────────┐
│ RuboCop (Ruby front end)                                          │
│   Runner -> Team -> Commissioner -> cop instances (per file)      │
└───────────────────────────────────────────────────────────────────┘
                          │
                          │ Rust-backed cops register
                          │ under the same badge as stock
                          ▼
┌───────────────────────────────────────────────────────────────────┐
│ lib/shirobai/cop/<dept>/<name>.rb (Ruby wrapper)                  │
│   - Turns Rust result tuples into Parser::Source::Range,          │
│     offenses, and corrector calls                                 │
│   - Converts byte offsets to char offsets for non-ASCII sources   │
│     (prism uses bytes, parser-gem uses chars)                     │
└───────────────────────────────────────────────────────────────────┘
                          │
                          │ One pass per file via Dispatch
                          ▼
┌───────────────────────────────────────────────────────────────────┐
│ crates/shirobai-core (Rust)                                       │
│   - Shared walk: one prism AST traversal produces results for     │
│     all cops at once (rules/bundle.rs)                            │
│   - Each cop publishes a Rule via build_rule(); standalone and    │
│     shared-walk paths run the same logic (no copy)                │
│ ext/shirobai (magnus bridge): exposes check_all_bundle to Ruby    │
└───────────────────────────────────────────────────────────────────┘

Key ideas:

  • Shared walk. Shirobai.check_all(src, token) walks the prism AST once per file and produces results for all active Rust cops at once. Adding one more cop does not add another full-file walk.
  • Same logic, two drivers. Each Rust rule is published via build_rule(). The standalone path (per-cop fallback) and the bundle path (shared walk) run the same code. cargo test checks that they stay equal.
  • Drop-in via badge replacement. inject.rb calls registry.enlist(klass) so each Rust cop takes the same registry slot as the stock cop. RuboCop sees no difference.

Repository layout

Each directory has its own README.md with details.

Directory What it is
lib/shirobai/ Ruby wrappers, Dispatch, SourceOffsets, inject
crates/shirobai-core/ Rust analysis core (per-cop rules + shared walk)
ext/shirobai/ magnus bridge (cdylib)
benches/ Benchmarks and the parity oracle
spec/ RSpec, vendor spec inclusion, edge-case parity
vendor/rubocop/ Git submodule pinned to 1.87.0 for vendor specs

Building and testing

bundle install
bundle exec rake compile          # cargo build --release + copy .so into lib/
bundle exec rspec                 # Ruby: vendor spec + parity spec
cargo test                        # Rust: rule equivalence and unit tests
cargo clippy --all-targets        # No new warnings is the merge bar

Parity check (drop-in compatibility)

First, clone the test corpora:

bin/setup-corpora

This clones Mastodon, Discourse, Redmine, and fluentd into .tmp/ at pinned commits. rubocop_source is a symlink to vendor/rubocop (already tracked in git).

Then run the parity oracle on each corpus:

benches/parity_diff.sh .tmp/mastodon
benches/parity_diff.sh .tmp/discourse
benches/parity_diff.sh .tmp/redmine
benches/parity_diff.sh .tmp/fluentd
benches/parity_diff.sh .tmp/rubocop_source

Each run launches the real rubocop CLI twice — once with Gemfile.stock (no shirobai), once with Gemfile.with_shirobai — and diffs per-cop / per-offense (path:line:column:message). Zero diff on all 5 corpora is required before merging.

Speed benchmark

benches/run_e2e.sh .tmp/mastodon 3

This measures in-process speed on Mastodon using its .rubocop.yml (cop enable/disable and parameters are loaded; plugin gems are not required). It runs three modes per round:

  • stock — all default cops, unchanged
  • removed — the implemented cops dropped entirely (speed floor)
  • shirobai — the implemented cops replaced by Rust (actual speed)

The script prints a summary with compute/cpu/gc medians and the net win.

For Claude Code agents

This repository is developed with Claude Code. See .claude/CLAUDE.md for project rules. This README is symlinked into .claude/rules/repository-overview.md.

License

MIT. See LICENSE.txt.