Chapter 04

自动修复 --fix 与 unsafe

让 Ruff 一次性改掉几百处问题,同时弄懂 safe 与 unsafe 修复的区别,避免被"自动修改"坑到。

4.1 什么是自动修复

对于很多问题,机器能明确写出"正确答案"——比如多余的 import"foo" % bar 应该改成 f-string、List[int] 应写成 list[int]。Ruff 把这类规则标记为 fixable,加上 --fix 就会直接改文件。

$ ruff check --fix .
# Found 127 errors (103 fixed, 24 remaining).

输出会告诉你:发现了 127 个问题,自动修掉了 103 个,剩余 24 个需要人工处理。

4.2 safe 修复 vs unsafe 修复

Ruff 把自动修复分成两级:

Safe fix(安全修复)
几乎保证语义不变——删未使用的 import、排序 import、== Noneis None。默认 --fix 只跑这一级。
Unsafe fix(不安全修复)
机器"有很高把握"是正确的,但在极端情况下可能改变语义或破坏代码。例如删除未使用的变量(该变量的 构造 可能有副作用)、把 typing.List 替换为 list(若某些上游代码依赖 typing 对象的 identity 会出问题)。需要显式加 --unsafe-fixes 才会执行。
典型 unsafe 修复的反例

规则 F841(未使用变量)的修复是"删除赋值":

result = compute_and_log()   # compute_and_log() 里写了日志!

直接删会让副作用(日志)一起消失,所以它被标记为 unsafe。

4.3 预览修改:--diff 与 --show-fixes

不想盲改文件?先看 diff:

$ ruff check --fix --diff .
# 打印将被改动的 diff,但不写入文件

$ ruff check --fix --show-fixes .
# 改完后,每条被修的问题都单独列出"改了什么"

--diff 输出是标准 unified diff,可直接 | less> fix.patch 存盘。

4.4 混合使用 safe + unsafe

工程实践上推荐:

  1. 第一步:ruff check --fix .(只跑 safe)
  2. 第二步:ruff check --fix --unsafe-fixes --diff .(预览 unsafe)
  3. 第三步:逐处确认后再执行 ruff check --fix --unsafe-fixes .
  4. 第四步:跑单元测试确认没破坏行为

4.5 限定规则再修复

一次性把 800 条规则的修复都应用,diff 会巨大、code review 难看。更务实的做法是"分族分批"修:

# 第一批:只修 import 问题
$ ruff check --select I,F401 --fix .

# 第二批:pyupgrade 把旧语法升级
$ ruff check --select UP --fix .

# 第三批:bugbear 反模式
$ ruff check --select B --fix --unsafe-fixes .

每一批都能形成一个独立的 commit,方便 review 和回滚。

4.6 禁止某条规则被 --fix

想开启某规则的检查,但不允许 Ruff 自动改它?用 unfixable

[tool.ruff.lint]
select = ["ALL"]
unfixable = [
    "F841",   # 未使用变量——让人工判断副作用
    "B008",   # 函数默认值中调用
]

4.7 实战:将老项目一键升级语法

假设你接手了一个 Python 3.8 时代的项目,现在环境升级到 3.12,想让全仓用上现代语法:

# 1. 更新 target-version
$ echo '[tool.ruff]\ntarget-version = "py312"' >> pyproject.toml

# 2. 先预览
$ ruff check --select UP --fix --diff . | head -50

# 3. 执行 safe 修复
$ ruff check --select UP --fix .

# 4. 看 unsafe 部分
$ ruff check --select UP --fix --unsafe-fixes --diff .

# 5. 跑测试确认
$ pytest

这一套下来,通常能自动完成 90%+ 的语法现代化——相比手写迁移节省几天时间。

4.8 修复无限循环?—— --fix-only 和迭代

某些情况下一条修复会触发另一条新的错误(例如删 import 后出现 E501 变短的行),Ruff 会自动做多轮修复直到稳定。若只想做一轮:

$ ruff check --fix --no-fix-loop .

--fix-only 则只跑修复不打印剩余错误(便于脚本里静默运行):

$ ruff check --fix-only .   # 静默修复

4.9 与 git 配合的"安全修复"工作流

# 确保工作区干净
$ git status --porcelain | grep . && echo "存在未提交变更,终止" && exit 1

# 每族独立 commit
$ ruff check --select I --fix . && git commit -am "style(ruff): sort imports (I)"
$ ruff check --select F --fix . && git commit -am "fix(ruff): remove unused (F)"
$ ruff check --select UP --fix . && git commit -am "refactor(ruff): modern syntax (UP)"

任何一步出错都可以 git reset --hard HEAD~1 回退单族。

4.10 小结