Ruff RUF201 规则详解:在 ruff.toml 选择器中使用人类可读规则名(rule-codes-in-selectors)
【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff
本文围绕 Ruff 的RUF201(rule-codes-in-selectors)规则展开,完整解析这条“面向配置文件本身”的 Lint 规则:它如何检查ruff.toml与pyproject.toml中select、ignore、per-file-ignores等选择器是否误用了F401这类规则代码,如何给出安全自动修复把代码替换为unused-import这类人类可读名称,以及它在什么条件下生效(Preview 模式)。读完后你能在项目中正确启用该规则、理解其扫描范围与边界行为,并能对照 Ruff 源码验证其底层实现。
规则定位:检查配置文件中的人类可读规则名
RUF201属于 Ruff 自有的Ruff规则集(代码前缀RUF),分类为Pedantic,自0.15.22起以 Preview 状态提供(见 crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_selectors.rs 中的violation_metadata(preview_since = "0.15.22", category = Category::Pedantic);RUF201于 changelogs/0.15.x.md 的 Preview features 一节随 PR #26772 引入)。
它的职责与其他规则不同——不是检查 Python 源码,而是检查Ruff 的配置文件本身:
What it does: Checks for any configuration files that use rule codes as selectors.
Why is this bad?: Human-readable rule names are easier to understand than rule codes. Using names also avoids requiring readers to look up the meaning of each code.
(What it does:检查配置文件中把规则代码当作选择器使用的情况;Why is this bad:人类可读的规则名比代码更易理解,无需读者逐个查表。)
— 规则文档注释(rule_codes_in_selectors.rs)
官方文档注释给出的最小示例:
[tool.ruff.lint] select = ["F401"]应改写为:
[tool.ruff.lint] select = ["unused-import"]规则注册表中,RUF201 的映射为(Ruff, "201") => rules::ruff::rules::RuleCodesInSelectors(见 crates/ruff_linter/src/codes.rs);并且它在 crates/ruff_linter/src/registry.rs 中被标记为LintSource::Toml——这决定了它只会在 Lint TOML 配置文件时被触发,而不会出现在普通 Python 文件上。
启用条件:必须开启 Preview
RUF201是 Preview 规则。规则入口函数的第一件事就是检查开关:
// crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_selectors.rs (L67-L74) pub(crate) fn rule_codes_in_selectors( context: &LintContext, document: &DeTable<'_>, source_type: TomlSourceType, ) { if !is_human_readable_names_enabled(context.settings().preview) { return; } ...而 crates/ruff_linter/src/preview.rs 中的判定非常简单:
pub const fn is_human_readable_names_enabled(preview: PreviewMode) -> bool { preview.is_enabled() }也就是说,从源码结构看,只要preview开启,is_human_readable_names_enabled即返回 true,规则即可工作(注释中注明这是“人类可读规则名”特性的统一开关,后续稳定化时才有机会收紧)。对应到配置文件,就是文档测试中使用的标准启用方式:
[lint] preview = true select = ["rule-codes-in-selectors"]注意这里select里直接写的是规则名rule-codes-in-selectors而不是RUF201——在 Preview 模式下,Ruff 的解析器本身就接受人类可读规则名作为选择器(解析逻辑见 crates/ruff_linter/src/rule_selector.rs,其中Rule::from_name(selector)分支在is_human_readable_names_enabled为真时才放行)。
触发链路:从 lint_toml 到规则函数
TOML 配置文件的 Lint 入口在 crates/ruff_linter/src/toml.rs:
pub fn lint_toml( path: &Path, contents: &str, settings: &LinterSettings, source_type: TomlSourceType, ) -> Vec<Diagnostic> { let context = LintContext::new(path, contents, settings); let document = DeTable::parse(contents); if context.is_rule_enabled(Rule::RuleCodesInSelectors) && let Ok(document) = &document { rule_codes_in_selectors(&context, document.get_ref(), source_type); } ... }可以看到整个调用链:DeTable::parse把配置解析成带位置信息的 TOML 文档 → 规则启用检查(is_rule_enabled)→rule_codes_in_selectors遍历选择器字段 → 通过context.report_diagnostic上报诊断。若配置了--fix,同文件的lint_fix_toml会迭代应用修复直到收敛(最多MAX_ITERATIONS轮)。
配置文件定位:ruff.toml 与 pyproject.toml
规则函数按文件类型(TomlSourceType)定位 Ruff 配置根:
// rule_codes_in_selectors.rs (L76-L93) let ruff = match source_type { TomlSourceType::Pyproject => document .get("tool") .and_then(|tool| tool.get_ref().get("ruff")) .and_then(|ruff| ruff.get_ref().as_table()), TomlSourceType::Ruff => Some(document), _ => None, };ruff.toml:整个文档就是 Ruff 配置,文档根即配置根;pyproject.toml:只检查[tool.ruff]表;- 其余 TOML 文件:直接跳过。
定位到配置根后,规则会对顶层(已废弃的顶层设置)与[lint]表两处都调用check_selectors,后者通过in_lint_table布尔值影响诊断消息前缀(见下文)。
检查范围:哪些选择器字段会被扫描
源码用两个常量清单定义扫描目标(rule_codes_in_selectors.rs):
数组型选择器(ARRAY_SELECTORS)——值本身是字符串数组:
| 字段 | 用途 |
|---|---|
select | 选定的规则集合 |
extend-select | 扩展选定规则 |
fixable/extend-fixable | 可自动修复的规则 |
ignore/extend-ignore | 忽略的规则 |
unfixable/extend-unfixable | 禁止自动修复的规则 |
extend-safe-fixes/extend-unsafe-fixes | 追加的安全/不安全修复开关 |
表型选择器(TABLE_SELECTORS)——值是按 glob 路径分组的表:
| 字段 | 示例 |
|---|---|
per-file-ignores/extend-per-file-ignores | per-file-ignores = { "foo.py" = ["E501"] } |
check_selectors的遍历逻辑:对数组型字段,若值是DeValue::Array则逐项检查;对表型字段,先确认值是DeValue::Table,再对每个路径条目确认其值是数组后逐项检查。
一个值得注意的工程约定写在 crates/ruff_linter/src/rule_selector.rs 的文档注释里:
If you add a new field that uses this type, be sure to update
rule-codes-in-selectors(RUF201) to validate the additional selector field.
即:未来新增使用UnresolvedRuleSelector类型的配置字段时,必须同步扩展 RUF201 的扫描清单——这从源码层面保证了该规则与配置 schema 的演进保持同步。
实战演示:文档测试中的五组场景
仓库自带的 mdtest 文档 crates/ruff_linter/resources/mdtest/ruff/rule-codes-in-selectors.md 本身就是一份“可运行的规格说明”:它由 mdtest 测试框架(crates/mdtest/src 中的断言与解析器)执行,文中# snapshot: rule-codes-in-selectors标记的行会触发快照断言,# error: [rule-codes-in-selectors]注释标记行会断言对应行必须产出诊断。以下按文档原始小节完整还原其行为。
场景一:各种引号风格均能命中
ruff.toml:
[lint] select = [ "F401", # snapshot: rule-codes-in-selectors 'F402', # snapshot: rule-codes-in-selectors """F403""", # snapshot: rule-codes-in-selectors '''F404''', # snapshot: rule-codes-in-selectors ]四条字符串(单引号、双引号、三引号两种形式)全部被标记,诊断输出形如(首条示例):
error[RUF201]: Rule code used instead of name in `lint.select` --> src/ruff.toml:3:6 | 3 | "F401", # snapshot: rule-codes-in-selectors | ^^^^ help: Replace rule code with `unused-import` | 2 | select = [ - "F401", # snapshot: rule-codes-in-selectors 3 + "unused-import", # snapshot: rule-codes-in-selectors 4 | 'F402', # snapshot: rule-codes-in-selectors |四个代码到规则名的映射(来自该文档的快照输出):
| 代码 | 人类可读名 |
|---|---|
F401 | unused-import |
F402 | import-shadowed-by-loop-var |
F403 | undefined-local-with-import-star |
F404 | late-future-import |
注意高亮位置^^^^精确落在引号内部的代码上,而非整段字符串。这由RuleCode::from_spanned完成:先取 TOML span 覆盖的范围,再从两侧裁掉等长的引号(rule_codes_in_selectors.rs):
// 注释原文:提取的代码范围对应代码本身而非周围的引号 let content = string.trim_start_matches(['"', '\'']); let quote_len = string.text_len() - content.text_len(); let start = range.start() + quote_len; let end = range.end() - quote_len;场景二:无效代码不误报,合法代码照常分析
文档明确指出:无效规则代码不会被标记,包括像"'F401'"(带嵌套引号)这类畸形值,但同一选择器数组里的合法代码仍然会被分析:
ruff.toml:
[lint] # snapshot: rule-codes-in-selectors select = ["'F401'", "F402"]只有第二个元素F402被标记:
error[RUF201]: Rule code used instead of name in `lint.select` --> src/ruff.toml:3:22 | 3 | select = ["'F401'", "F402"] | ^^^^ help: Replace rule code with `import-shadowed-by-loop-var`对应实现上,RuleCode::from_spanned要求Rule::from_code(code)解析成功才返回Some(失败的项直接continue),并先经过get_redirect_target处理重定向代码——因此重定向前的旧代码也能被解析并映射到规范规则名。
场景三:畸形选择器形状被安全跳过
为防止极端情况(这些形状本应被配置反序列化拦截,但规则做了防御):
ruff.toml:
[lint] select = { nested = ["F401"] } per-file-ignores = ["F401"]该场景不产生任何诊断。源码中check_selectors对select只做if let DeValue::Array(...)匹配、对per-file-ignores只做if let DeValue::Table(...)匹配,形状不符即跳过——这是“宁可不报也不 panic”的健壮性设计。
场景四:前缀与规则名保持原样
[lint] select = ["F", "unused-import"]单字母/单段前缀(如F表示全部 Pyflakes 规则)和已经是人类可读名的选择器都不标记。源码中这与Rule::from_code的行为一致:仅当字符串能解析为“某个具体规则”的代码时才处理,前缀(F)和规则名(unused-import)都不满足。
场景五:全覆盖——顶层废弃设置与[lint]表都扫描
这是文档中最能说明扫描面的用例,顶层(已废弃)与[lint]表中的全部 12 个选择器字段逐一验证:
select = ["F401"] # error: [rule-codes-in-selectors] extend-select = ["F841"] # error: [rule-codes-in-selectors] fixable = ["E501"] # error: [rule-codes-in-selectors] extend-fixable = ["UP035"] # error: [rule-codes-in-selectors] ignore = ["F401"] # error: [rule-codes-in-selectors] extend-ignore = ["F841"] # error: [rule-codes-in-selectors] per-file-ignores = { "foo.py" = ["E501"] } # error: [rule-codes-in-selectors] extend-per-file-ignores = { "bar.py" = ["UP035"] } # error: [rule-codes-in-selectors] unfixable = ["F401"] # error: [rule-codes-in-selectors] extend-unfixable = ["F841"] # error: [rule-codes-in-selectors] extend-safe-fixes = ["E501"] # error: [rule-codes-in-selectors] extend-unsafe-fixes = ["UP035"] # error: [rule-codes-in-selectors] [lint] select = ["F401"] # error: [rule-codes-in-selectors] extend-select = ["F841"] # error: [rule-codes-in-selectors] fixable = ["E501"] # error: [rule-codes-in-selectors] extend-fixable = ["UP035"] # error: [rule-codes-in-selectors] ignore = ["F401"] # error: [rule-codes-in-selectors] extend-ignore = ["F841"] # error: [rule-codes-in-selectors] per-file-ignores = { "foo.py" = ["E501"] } # error: [rule-codes-in-selectors] extend-per-file-ignores = { "bar.py" = ["UP035"] } # error: [rule-codes-in-selectors] unfixable = ["F401"] # error: [rule-codes-in-selectors] extend-unfixable = ["F841"] # error: [rule-codes-in-selectors] extend-safe-fixes = ["E501"] # error: [rule-codes-in-selectors] extend-unsafe-fixes = ["UP035"] # error: [rule-codes-in-selectors]每行都必须产出诊断。这也解释了诊断消息中的in_lint_table分支(rule_codes_in_selectors.rs):在[lint]表内时报Rule code used instead of name in `lint.select`,在顶层废弃设置处则报in `select`。
场景六:pyproject.toml 的 [tool.ruff]
[tool.ruff] ignore = ["F401"] # error: [rule-codes-in-selectors] [tool.ruff.lint] select = ["F402"] # error: [rule-codes-in-selectors]pyproject.toml中同样同时覆盖顶层[tool.ruff]与[tool.ruff.lint]两层,与ruff.toml的行为一致。
场景七:尊重用户的 unfixable 设置
文档最后验证 RUF201 这类 TOML 专用规则也遵守unfixable配置:
[lint] preview = true select = ["rule-codes-in-selectors"] unfixable = ["rule-codes-in-selectors"]# ruff.toml # snapshot: rule-codes-in-selectors lint.select = ["F401"]此时诊断照常产出,但不再附带修复(快照中只剩消息,没有+修复行):
error[RUF201]: Rule code used instead of name in `lint.select` --> src/ruff.toml:2:17 | 2 | lint.select = ["F401"] | ^^^^ help: Replace rule code with `unused-import`文档说明该行为同样覆盖extend-unsafe-fixes、per-file-ignores等所有经由LintContext统一处理的修复开关。修复本体是一个安全编辑(Fix::safe_edit(Edit::range_replacement(name, range)),rule_codes_in_selectors.rs),只替换引号内的代码部分,保留原有引号风格与行尾注释。
从命令行验证:一次可复现的运行
集成测试 crates/ruff/tests/cli/lint.rs 展示了端到端的 CLI 用法:对包含lint.select = ["F401"]的ruff.toml执行:
ruff check --no-cache --isolated --preview --select RUF201预期输出(摘自该测试的内联快照):
rule-codes-in-selectors: [*] Rule code used instead of name in `lint.select` --> ruff.toml:1:17 | 1 | lint.select = ["F401"] | ^^^^ help: Replace rule code with `unused-import` | - lint.select = ["F401"] 1 + lint.select = ["unused-import"]要点:
- 必须带
--preview,否则规则不启用(见上文 Preview 门槛); --isolated保证不受其他配置干扰,--no-cache保证每次真实执行;- 诊断标记
[*]表示附带可自动修复(未声明unfixable时)。
配合--fix,Ruff 会迭代应用修复直到诊断稳定(lint_fix_toml的循环实现,见 crates/ruff_linter/src/toml.rs)。
小结:RUF201 的行为边界一览
| 维度 | 行为 | 依据 |
|---|---|---|
| 生效前提 | Preview 模式(preview = true或--preview) | preview.rs |
| 适用文件 | ruff.toml与pyproject.toml的[tool.ruff] | rule_codes_in_selectors.rs |
| 扫描字段 | 10 个数组型 + 2 个表型选择器,顶层与[lint]两处 | rule_codes_in_selectors.rs |
| 不标记 | 无效代码、嵌套引号值、前缀(如F)、已是规则名的选择器 | rule-codes-in-selectors.md 场景二/四 |
| 安全跳过 | 形状错误的值(select为表、per-file-ignores为数组) | check_selectors的if let收窄 |
| 高亮范围 | 精确到引号内的代码,不含引号 | RuleCode::from_spanned |
| 修复 | 安全修复,替换为规则名;受unfixable等LintContext设置约束 | rule_codes_in_selectors.rs |
| 演进约定 | 新增选择器字段时必须同步更新本规则 | rule_selector.rs |
作为“人类可读规则名”这一 Preview 特性在配置侧的组成部分,RUF201 让ruff.toml/pyproject.toml本身也能被 Lint:在开启 Preview 的项目里,select = ["unused-import"]这类写法从此有工具层面的保障,而F401这类代码一旦出现在选择器中,会获得带精确位置和安全修复的即时反馈。
【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考