寫 Ruby 的人大概都認識 fasterer 這個 gem,會幫你找出常見的效能反模式。但專案越長越大之後,fasterer 自己的執行速度反而變成痛點。

rubyfast 是用 Rust 把 fasterer 重寫了一次,19 條偵測規則照搬,但速度差異很有感:2,235 個 Ruby 檔案,原本 34.1 秒,現在 0.21 秒,快了 162 倍

為什麼要用 Rust 重寫

fasterer 的瓶頸在 Ruby 的 parser 和單執行緒架構。rubyfast 換掉了這兩件事:parser 改用 ruby-prism(Ruby 3.3+ 之後的官方 parser)解析 AST,掃描則交給 rayon 平行跑。順便 Ruby runtime 也不用了,一個 binary 就能執行。

能偵測什麼

rubyfast 支援 19 種效能反模式偵測,以下列出幾個常見的:

反模式建議替代可自動修復
.shuffle.first.sample
.select{}.first.detect{}
.reverse.each.reverse_each
.keys.each.each_key
.map{}.flatten(1).flat_map{}
.gsub("x","y")(單字元).tr("x","y")
for x in arrarr.each
.sort { |a,b| ... }.sort_by
rescue NoMethodErrorrespond_to?
getter/setter 方法attr_reader / attr_writer

其中 8 種支援 --fix 自動修復。修補完會再 parse 一次確認沒語法錯,才把檔案寫回去。

安裝與使用

# 透過 Cargo 安裝
cargo install rubyfast

# 掃描目前目錄
rubyfast

# 掃描特定路徑
rubyfast path/to/file.rb
rubyfast path/to/directory

# 自動修復
rubyfast --fix .

也有 Docker image 可以直接使用:

docker run --rm -v $(pwd):/app ghcr.io/7a6163/rubyfast:latest

輸出格式

三種格式對應不同情境:

# 依檔案分組(預設)
rubyfast --format file .

# 依規則分組,附數量統計
rubyfast --format rule .

# 一行一個 offense,適合 CI / grep / reviewdog
rubyfast --format plain .

設定檔

支援 .rubyfast.yml.fasterer.yml,可以停用特定規則或排除路徑:

speedups:
  shuffle_first_vs_sample: false

exclude_paths:
  - vendor/**
  - db/migrate/**

設定檔會自動往上層目錄搜尋,跟 .rubocop.yml 的行為一致。

Inline Disable

跟 RuboCop 類似,可以用註解在程式碼中局部停用規則:

# 單行停用
arr.shuffle.first # rubyfast:disable shuffle_first_vs_sample

# 下一行停用
# rubyfast:disable-next-line shuffle_first_vs_sample
arr.shuffle.first

# 區塊停用
# rubyfast:disable shuffle_first_vs_sample
arr.shuffle.first
# rubyfast:enable shuffle_first_vs_sample

也相容舊版 fasterer:disable 語法。

CI 整合

在 GitHub Actions 中可以搭配 rubyfast-action 使用:

- uses: 7a6163/rubyfast-action@v1

搭配 --format plain 輸出和 reviewdog,可以在 PR 上直接顯示 inline comments。

GitLab CI 也能透過 Docker image 整合。

幾個實作細節

修復是會互相位移的,所以多個 patch 會按 byte offset 反向排序再依序套用,免得後面的修補蓋掉前面的。byte → line 轉換用 binary search 查預先建好的 newline 陣列,O(log n)。掃描是 rayon 的 parallel iterator,每個檔案獨立 parse + 掃描。修復寫回前會再 parse 一次驗證語法。

收尾

Ruby 專案還在用 fasterer 的話,rubyfast 規則和設定檔都相容,可以直接抽換掉。多了自動修復功能。丟進 CI pipeline 也幾乎感覺不到等待時間。