shirobai
shirobai is an experimental gem that speeds up RuboCop by replacing some of its cops with fully compatible Rust implementations.
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
-
106 cops reimplemented in Rust (Lint / Layout / Metrics / Naming / Style).
-
Plugin-cop gems (37 more cops through the same shared native extension, no second cargo build):
gems/shirobai-rspecreplaces 21 rubocop-rspec cops,gems/shirobai-railsreplaces 11 rubocop-rails cops, andgems/shirobai-performancereplaces 5 rubocop-performance cops. Seedocs/cop-status.mdand each gem's README; the setup below describes the core gem only. -
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, 5-round median:Corpus files offenses stock shirobai (core only) + plugin gems Mastodon 3,206 0 105.89s 73.72s (-30.4%) 62.17s (-41.3%) Discourse 10,229 16 220.09s 174.44s (-20.7%) 164.19s (-25.4%) Redmine 1,058 2 56.20s 38.27s (-31.9%) 36.05s (-35.9%) fluentd 456 0 6.89s 7.00s (+1.6%) 7.77s (+12.7%) The "shirobai (core only)" column installs the core gem alone; the "+ plugin gems" column adds shirobai-rspec / shirobai-rails / shirobai-performance on top (each required only when the corpus's own config loads the matching stock plugin, exactly as a real user would). Measured on GitHub Actions
ubuntu-latest(4-vCPU shared runner) against shirobai at commit0b09c69. Each run first verifies that stock and shirobai report the same offense set on the corpus's own config; the table shows the median time to lint the same code. Rerun on any commit viagh workflow run bench.yml(.github/workflows/bench.yml).Projects that spend their time on plugin cops gain from the plugin gems what the core gem alone cannot reach (Discourse is a heavy plugin user: core -20.7%, with plugin gems -25.4%; Mastodon's spec-heavy suite gains 11 points from shirobai-rspec/-rails). fluentd is the honest fine print: its config disables most default cops, so there is little for shirobai to replace and the fixed cost of loading the native extension slightly exceeds the saving — and the plugin shells only add fixed cost there. On plugin-light or heavily-restricted configs, measure first; installing shirobai (or its plugin gems) is not automatically a win.
RuboCop itself is also used for compatibility verification but not benchmarked, because its own config needs
rubocop-internal_affairs/rubocop-rakeand leaves few rubocop-gem cops enabled.
Requirements
Important
shirobai's native extension is written in Rust.
On x86_64 Linux (glibc) and Apple Silicon macOS (arm64-darwin),
gem install pulls a prebuilt platform gem and no Rust toolchain is needed.
On every other platform 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.88.2 |
| Ruby | >= 3.1 |
| Rust |
>= 1.75 (stable) — not needed on the prebuilt platforms below |
| Prebuilt platforms |
x86_64-linux (glibc) / arm64-darwin (Apple Silicon), Ruby 3.1–4.0 |
| Other platforms | Linux / macOS (anywhere cargo build --release works, e.g. musl, Intel macOS) |
| 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, only four cops are affected:
-
Layout/SpaceAroundKeyword when detecting the Ruby 2.7
expr in patone-line pattern match. -
Lint/DuplicateMagicComment when a file has an INDENTED
__END__line (parsers before 3.4 stop reading there; prism reads on). -
Lint/DuplicateMethods when a method is defined inside a block
that uses the bare
itparameter (anitblock only exists in 3.4+). -
Naming/AsciiIdentifiers for a non-ASCII method NAME ending in
!or?in adef self.foo!/undef foo!/alias foo!position (parser-gem tokenizes it astIDENTIFIERbefore 3.0 and astFIDon newer grammars; prism follows the newer grammar). A plaindef foo!is unaffected. Real code has no non-ASCII!/?method names, so this is theoretical.
All other implemented cops work the same regardless of TargetRubyVersion. If you need strict target-version behavior for these cops, 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.88.2"
gem "shirobai"Then run bundle install.
Usage
Add one line to your .rubocop.yml:
require:
- shirobaiThat'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 testchecks that they stay equal. -
Drop-in via badge replacement.
inject.rbcallsregistry.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 oracles |
spec/ |
RSpec, vendor spec inclusion, edge-case parity |
vendor/rubocop/ |
Git submodule pinned to 1.88.2 for vendor specs |
gems/shirobai-performance/ |
Plugin gem (rubocop-performance cops) |
gems/shirobai-rspec/ |
Plugin gem (rubocop-rspec cops) |
gems/shirobai-rails/ |
Plugin gem (rubocop-rails cops) |
vendor/rubocop-performance/ |
Git submodule pinned to 1.26.1 for plugin vendor specs |
vendor/rubocop-rspec/ |
Git submodule pinned to 3.10.2 for plugin vendor specs |
vendor/rubocop-rails/ |
Git submodule pinned to 2.36.0 for plugin 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 barParity check (drop-in compatibility)
First, clone the test corpora:
bin/setup-corporaThis 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_sourceEach 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 3This 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.