我从仓库筛出 21 个一方 Python 文件,固定同一份工作区快照,分别在两个独立环境安装 Ruff 0.15.0 与 0.16.0,用各自默认规则跑 ruff check。项目无 ruff.toml / pyproject.toml 覆盖,比较的是版本自带默认政策。结果从 36 条升到 121 条:集合差分新增 93、消失 8。告警变多,并不等于旧问题仍在,也不等于代码突然变差;它首先说明,默认规则集合本身发生了替换。

默认规则被重写,不是单纯更严格
官方 changelog 写得很清楚:0.16 默认规则从 59 条扩到 413 条。Ruff 0.16 不是把旧规则线性加严,而是重写了默认质量政策。 交叉验证足够直接:在 0.16 中显式写回旧默认 select = ["E4", "E7", "E9", "F"],输出逐条恢复为与 0.15 默认一致的 36 条。由此可以断定,那 8 条消失的旧告警(E401 多 import 同行、E741 歧义变量名、E701 单行复合语句、E402 模块级 import 位置)并非代码被修好,而是被新默认集合放出了视野。升级若只盯总数,会同时高估“新发现”,也低估“旧覆盖的丢失”。
新增里最容易误导读数的是 import 排序。I001 出现 21 条,且 applicability 均为 safe,适合当作噪声对照:它会抬高告警数,却几乎不改变风险判断。真正有工程信号的是另外两类问题。
第一类是不可见字符。fetch-newsletter.py 被 0.16 标出零宽空格与控制字符(PLE2515 / PLE2502)。肉眼 diff 往往看不出,拷贝粘贴、网页源码或编辑器差异却可能把控制字符带进仓库;这类规则解释了“扩大默认面”的真实价值——它抓的是旧默认集容易漏掉的维护风险,而不是又一条风格偏好。
第二类是宽泛捕获与静默吞异常。本次共 22 条 BLE001(except Exception)与 5 条 S110 / S112(except 后 pass / continue)。它们集中暴露自动化脚本“失败但不报”的可观测性缺口:任务看似跑完,实际错误被吞掉。这类问题通常没有 fix 元数据,必须决定捕获范围、日志级别,以及哪些失败允许继续。把它们与 I001 混成一个“升级后一键修干净”的故事,会把真正的风险冲淡。
升级应按差分迁移,不能直接等价于自动修复
93 条新增里,50 条带 fix 元数据,但有修复建议不等于可安全一键修:其中 39 条 applicability=safe,10 条 unsafe,1 条 display-only;其余 43 条没有修复建议。默认的 ruff --fix 只会应用 safe 一类,不会把全部 50 条都改掉;把升级等同于一次 --fix,既高估可自动化比例,也低估人工判断量。
可执行的迁移顺序可以收敛为三步。先把 CI 与本地的 Ruff 锁死版本,避免 dev dependency 漂移把默认政策悄悄换掉。再在同一份工作区快照上分别用旧版、新版默认规则跑 ruff check --output-format json,保存两份基线后做集合差分,分清新增、消失与净增,而不是只看总数。最后按三类分批处理:safe 修复建议可先自动应用并单独提交;unsafe、display-only 与无修复建议的项进入人工判断清单;旧规则退出默认集的,必须显式选择“接受放过”或继续在配置中写回。三类混在一次大提交里,事后几乎无法解释 CI 红灯从何而来。
若团队暂时吃不下 413 条默认政策,可以先在 0.16 显式保留旧默认,再按规则组逐批放开。边界必须写清:本次数字只描述本仓库实验,不能外推为所有代码库的固定比例;可复用的是差分方法与分批策略,不是这些数字本身。升级完成的标志,不是告警清零,而是团队清楚新默认抓什么、放过什么,以及哪些修复可安全自动化、哪些必须人工拍板。
