https://github.com/momiji-rs/oxidecop
OxideCop: a fast, native, RuboCop-compatible Ruby linter and autocorrector over the Prism parser
https://github.com/momiji-rs/oxidecop
autocorrect cli developer-tools formatter linter prism rubocop ruby rust static-analysis
Last synced: about 20 hours ago
JSON representation
OxideCop: a fast, native, RuboCop-compatible Ruby linter and autocorrector over the Prism parser
- Host: GitHub
- URL: https://github.com/momiji-rs/oxidecop
- Owner: momiji-rs
- License: mit
- Created: 2026-07-02T13:50:45.000Z (14 days ago)
- Default Branch: master
- Last Pushed: 2026-07-02T14:43:11.000Z (14 days ago)
- Last Synced: 2026-07-02T15:28:25.604Z (14 days ago)
- Topics: autocorrect, cli, developer-tools, formatter, linter, prism, rubocop, ruby, rust, static-analysis
- Language: Ruby
- Size: 464 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# OxideCop
A fast, native, **RuboCop-compatible** Ruby linter and autocorrector — written
in Rust over the official [Prism](https://github.com/ruby/prism) parser. Think
*ruff, but for Ruby, and bug-compatible with the RuboCop everyone already uses.*
```sh
cargo install oxidecop
oxidecop . # same offenses, same messages, same exit code — just fast
```
> **Status: early / experimental.** The core idea is proven — the 47
> implemented cops pass **100% of RuboCop's own representable spec examples**,
> detection (1051/1051) *and* autocorrection (477/477, `--fix`), and match
> RuboCop **byte-for-byte on real repos** (Rails, Mastodon, rubygems.org,
> RuboCop's own source — see
> [BENCHMARKS.md](https://github.com/momiji-rs/oxidecop/blob/master/BENCHMARKS.md))
> — but 47 cops is not a shippable linter yet.
## Why
RuboCop is the de-facto Ruby linter, but it runs on CRuby and is dominated by
**parse time in the interpreted `parser` gem**. A native linter that speaks
Prism is an order of magnitude faster while producing **byte-identical
output**, so it drops into existing projects with zero config changes.
The bet only pays off if we are **faithful to RuboCop's actual behavior**, not
"morally equivalent." That is why fidelity is measured against RuboCop's *own*
test suite, not by eyeballing:
| | |
|---|---|
| Whole-tree lint, Rails (~3,400 files) | **57 ms** |
| vs [oxicop](https://crates.io/crates/oxicop) (regex-based, no parser) | 2–5× faster — and correct where it reports thousands of false positives |
| vs [nitrocop](https://github.com/6/nitrocop) (prism-based, 915 cops) | 4–31× faster cold; on RuboCop's own CI-clean repo it reports 17,174 offenses, OxideCop reports the true zero |
| Warm rerun (nothing changed) | **~29 ms** (stat-keyed result cache, no file reads) |
## Fidelity, measured
RuboCop's spec suite encodes exact expectations inline:
```ruby
expect_offense(<<~RUBY)
x == nil
^^ Prefer the use of the `nil?` predicate.
RUBY
```
The **oracle** (`oracle/oracle.rb`) parses those fixtures, runs the
de-annotated source through the `oxidecop` binary under *the example's own*
`cop_config`, and diffs at three levels: **LOC** (right line:col), **FULL**
(line:col *and* identical message), and **FIX** (the `--fix` output against
`expect_correction`).
Current leaderboard (`ruby oracle/leaderboard.rb`), against RuboCop v1.88.0:
```
47 cops implemented — every one at 100% FULL match, 100% FIX (autocorrect).
TOTAL (representable examples) 1051 108 1051/1051 100% FIX 477/477
```
**Read this honestly:** the score is over *representable* examples only.
RuboCop's specs encode some text dynamically (RSpec loop variables,
`%{identifier}` substitutions, shared_examples that run under a caller's
config); the oracle renders what is static and **skips** what isn't, rather
than scoring a harness limitation as a cop miss — hence the `skip` column.
End-to-end, `tools/parity.sh ` lints a tree with both linters
(both under `--only` with the implemented cop list, which RuboCop
force-enables) and diffs the normalized offense lists. Byte-identical on all
four corpora: **Rails (12,271 offense lines), Mastodon (4,123), rubygems.org
(772), and RuboCop's own 1,712-file source tree (0 vs 0)**. CI re-verifies the
oracle at 100% and the parity on every push.
## Usage
```sh
oxidecop lib/ spec/ # lint trees in parallel (nearest .rubocop.yml per file)
oxidecop file.rb my.yml # explicit config
oxidecop -a . # autocorrect in place (like rubocop -a)
oxidecop file.rb --fix # autocorrect a single file -> stdout
oxidecop --only Style/SymbolProc --format json .
```
Output mimics RuboCop's simple formatter (severity letters, `[Correctable]`
tags, per-cop `Severity` config honored); `--format json` emits
RuboCop-compatible JSON. Exit code is 1 when offenses were found.
Config support covers the surface real projects use: `.rubocop.yml` discovery
(nearest config per file), `inherit_from` chains, `inherit_gem`, `AllCops:
DisabledByDefault / Exclude / Include`, per-cop `Enabled` / `Exclude` /
`EnforcedStyle` / `AllowedMethods` / `AllowedPatterns` (including
`!ruby/regexp`), magic-comment `rubocop:disable` directives (with `-- reason`
trailers), and `TargetRubyVersion`. Parameter defaults for **all 606 cops**
are generated from RuboCop's own `config/default.yml` into one schema table,
so a newly ported cop's defaults are already correct.
A result cache (stat-keyed, content-hash fallback) makes unchanged-tree reruns
~2× faster; `--cache false` disables it.
## Repo layout
```
src/main.rs the runner: argv, file/config discovery, output, --fix loop
src/config.rs .rubocop.yml subset + the generated per-cop SCHEMA
src/declarative.rs the DECLARATIVE pattern-cop table (node-pattern rows)
src/nodepattern.rs RuboCop's node-pattern DSL, parsed and matched over Prism
src/cops/ the visitor + per-department cop logic
src/cops/breakable.rs Layout/LineLength autocorrection (CheckLineBreakable port)
oracle/ the spec-suite oracle + leaderboard (fidelity measurement)
tools/parity.sh whole-tree byte-identical diff vs reference RuboCop
bench/compare.sh the speed scoreboard vs competing linters
```
## How to add / improve a cop
1. **Find RuboCop's real pattern.** Copy the `def_node_matcher` string and
`MSG` from RuboCop's source *verbatim* — do not paraphrase. (Guessed
patterns are systematically over-broad; the oracle catches this every time.)
2. **Express it.** A pattern cop -> one row in the `DECLARATIVE` table. A logic
cop -> a small `Visit` method. If the DSL can't express the pattern yet,
extend `src/nodepattern.rs`.
3. **Measure.** Map the cop to its spec in `oracle/leaderboard.rb`, run it —
the miss list is your exact TODO, down to the failing example.
4. **Verify end-to-end.** `tools/parity.sh` against a real tree must stay
byte-identical.
## Roadmap
- **Widen**: port the next tiers of cops (the schema and node-pattern DSL
already cover their config surface).
- **Distribution**: prebuilt binaries, `brew install oxidecop`.
- **Close the warm-rerun gap**: cache the directory walk to take no-change
reruns from ~29 ms toward ~5 ms.
## License
MIT — see [LICENSE](https://github.com/momiji-rs/oxidecop/blob/master/LICENSE).
OxideCop is not affiliated with the RuboCop project; RuboCop is the work of
its authors and contributors, and this project exists to be faithful to it.