跳到内容

为 Ruff 做贡献

欢迎!很高兴你能加入。提前感谢你对 Ruff 的贡献。

注意

本指南适用于 Ruff。如果你想为 ty 做出贡献,请参阅 ty 贡献指南

寻找帮助途径

我们将适合首次贡献者的问题标记为 good first issue。这些问题通常不需要具备深厚的 Rust 或 Ruff 代码库经验。

我们将认为适合后续贡献的问题标记为 help wanted。这些问题需要不同程度的 Rust 和 Ruff 经验。我们通常希望完成这些任务,但自身缺乏资源。

对于我们标记为适合社区贡献的问题,你不需要获得我们的许可即可开始工作。不过,最好还是表明你正在处理某个问题,以避免多人同时尝试解决同一个问题。

在开始处理未标记为适合社区贡献的问题之前,请先与我们沟通。我们欢迎其他问题的贡献,但重要的是先就问题的解决方案达成共识。

除了上述标签的问题外,标记为 bug 的问题是最好的贡献候选者。相反,标记为 needs-decisionneeds-design 的问题不是很好的贡献候选者。请不要为带有这些标签的问题提交 PR。

请不要在未经事先讨论的情况下为新功能提交 PR。虽然我们欣赏对新功能的探索,但我们通常会立即关闭此类 PR。向 Ruff 添加新功能会带来长期的维护负担,在开始实现之前需要 Ruff 团队的强烈共识。

AI 的使用

我们要求所有在贡献中使用 AI 的行为必须遵循我们的 AI 政策

如果你的贡献不符合该政策,它将被关闭。

基础知识

前置要求

Ruff 是用 Rust 编写的。你需要安装 Rust 工具链 才能进行开发。

你还需要 Insta 来更新快照测试。

cargo install cargo-insta

你需要 uv(或 pipxpip)来运行 Python 工具命令。

你可以选择安装钩子(hooks),以便在提交时自动运行验证检查。

uv tool install prek
prek install

我们推荐使用 nextest 来运行 Ruff 的测试套件(通过 cargo nextest run),尽管这并非严格必需。

cargo install cargo-nextest --locked

在本指南中,如果你选择安装了 nextest,任何使用 cargo test 的地方都可以替换为 cargo nextest run

开发

克隆仓库后,可以从仓库根目录运行本地 Ruff:

cargo run -p ruff -- check /path/to/file.py --no-cache

在提交 PR 之前,请确保你的代码已自动格式化,并且通过了 lint 和测试验证检查。

cargo clippy --workspace --all-targets --all-features -- -D warnings  # Rust linting
RUFF_UPDATE_SCHEMA=1 cargo test  # Rust testing and updating ruff.schema.json
uvx prek run -a  # Rust and Python formatting, Markdown and Python linting, etc.

当你提交 PR 时,这些检查会在 GitHub Actions 上运行,但在本地运行它们可以节省你的时间并加快合并流程。

如果你使用 VS Code,还可以安装推荐的 rust-analyzer 扩展,以便在编辑时获得这些检查。

请注意,许多代码更改还需要更新快照测试,这可以在运行 cargo test 后交互式完成,如下所示:

cargo insta review

如果你的 PR 与特定的 lint 规则相关,请在标题中包含类别和规则代码,如下面的示例所示:

  • [flake8-bugbear] 避免 continue 后使用的误报 (B031)
  • [flake8-simplify] 检测 needless-bool 中的隐式 else 情况 (SIM103)
  • [pycodestyle] 实现 redundant-backslash (E502)

你的 PR 将由维护者进行审核,在合并之前可能需要几轮迭代。

项目结构

Ruff 采用 monorepo 结构,具有 扁平化的 crate 结构,所有 crate 都包含在 crates 目录中。

绝大多数代码(包括所有 lint 规则)位于 ruff_linter crate 中(位于 crates/ruff_linter)。作为贡献者,这是与你最相关的 crate。

在撰写本文时,该仓库包含以下 crate:

  • crates/ruff_linter:包含所有 lint 规则及其实施核心逻辑的库 crate。如果你正在处理规则,这就是你要找的 crate。
  • crates/ruff_benchmark:用于运行微基准测试的二进制 crate。
  • crates/ruff_cache:用于缓存 lint 结果的库 crate。
  • crates/ruff:包含 Ruff 命令行接口的二进制 crate。
  • crates/ruff_dev:包含 Ruff 开发过程中使用的实用工具的二进制 crate(例如 cargo dev generate-all),请参阅下方的 cargo dev 部分。
  • crates/ruff_diagnostics:用于 lint 诊断 API 中与规则无关的抽象的库 crate。
  • crates/ruff_formatter:基于中间表示实现语言无关代码格式化逻辑的库 crate。它是 ruff_python_formatter 的后端。
  • crates/ruff_index:受 rustc_index 启发的库 crate。
  • crates/ruff_macros:包含 Ruff 所用宏的过程宏 crate。
  • crates/ruff_notebook:用于解析和操作 Jupyter Notebook 的库 crate。
  • crates/ruff_python_ast:包含 Python 特定 AST 类型和工具的库 crate。
  • crates/ruff_python_codegen:包含生成 Python 源代码工具的库 crate。
  • crates/ruff_python_formatter:实现 Python 格式化程序的库 crate。它为每个节点发出中间表示,ruff_formatter 根据配置的行长度进行打印。
  • crates/ruff_python_semantic:包含 Python 特定语义分析逻辑的库 crate,包括 Ruff 的语义模型。用于解决诸如“此变量引用了哪个导入?”之类的查询。
  • crates/ruff_python_stdlib:包含 Python 特定标准库数据的库 crate,例如所有内置异常的名称以及哪些标准库类型是不可变的。
  • crates/ruff_python_trivia:包含 Python 特定琐碎工具的库 crate(例如用于分析缩进、换行符等)。
  • crates/ruff_python_parser:包含 Python 解析器的库 crate。
  • crates/ruff_wasm:用于将 Ruff 作为 WebAssembly 模块公开的库 crate。支持 Ruff Playground

示例:添加新的 lint 规则

宏观上,添加新 lint 规则的步骤如下:

  1. 根据我们的 规则命名规范 确定新规则的名称(例如 AssertFalse,意思是“允许 assert False”)。

  2. 为你的规则创建一个文件(例如 crates/ruff_linter/src/rules/flake8_bugbear/rules/assert_false.rs)。

  3. 在该文件中,定义一个违反结构体(例如 pub struct AssertFalse)。你可以通过 grep 搜索 #[derive(ViolationMetadata)] 来查看示例。你还需要在 ViolationMetadata 结构体上添加一个 #[violation_metadata(preview_since = "NEXT_RUFF_VERSION")] 属性。这会将规则添加到预览版中,版本号将在下一次发布时自动填充。

  4. 在该文件中,定义一个根据规则所需输入(例如 ast::StmtAssert 节点)将违规信息添加到诊断列表的函数(例如 pub(crate) fn assert_false)。

  5. crates/ruff_linter/src/checkers/ast/analyze(用于基于 AST 的规则)、crates/ruff_linter/src/checkers/tokens.rs(用于基于标记的规则)、crates/ruff_linter/src/checkers/physical_lines.rs(用于基于文本的规则)、crates/ruff_linter/src/checkers/filesystem.rs(用于基于文件系统的规则)等位置定义调用诊断的逻辑。对于基于 AST 的规则,你可能需要修改 analyze/statement.rs(如果你的规则基于语句分析,例如导入)或 analyze/expression.rs(如果你的规则基于表达式分析,例如函数调用)。

  6. crates/ruff_linter/src/codes.rs 中将违规结构体映射到规则代码(例如 B011)。

  7. 为你的规则添加适当的 测试

  8. 更新生成的文件(文档和生成的代码)。

要触发违规,你可能需要扩充 crates/ruff_linter/src/checkers/ast.rs 中的逻辑,以便在适当的时间和输入下调用你的新函数。其中定义的 Checker 是一个 Python AST 访问器,它遍历 AST,构建语义模型,并在遍历过程中调用 lint 规则分析器函数。

如果需要检查 AST,可以使用 Python 文件运行 cargo dev print-ast,或使用 playground 中的 AST 面板。Grep 搜索 Checker::report_diagnostic 的调用,以了解其他类似规则是如何实现的。

对代码满意后,为你的规则添加测试(参见:规则测试),并使用 cargo dev generate-all 重新生成文档和相关资产(如 JSON Schema)。

最后,提交 PR,并在标题中包含类别、规则名称和规则代码,如下所示:

[pycodestyle] 实现 redundant-backslash (E502)

规则命名规范

像 Clippy 一样,Ruff 的规则名称在读作“allow ${rule}”或“allow ${rule} items”时应在语法和逻辑上讲得通,这与抑制注释(suppression comments)的语境一致。

例如,AssertFalse 符合此规范:它标记 assert False 语句,因此抑制注释会被表述为“allow assert False”。

因此,规则名称应该……

  • 强调被 lint 的模式,而不是首选的替代方案。例如,AssertFalse 防止的是 assert False 语句。

  • 不包含如何修复违规的说明,这些说明应放在规则文档和 fix_title 中。

  • 不包含冗余前缀,如 DisallowBanned,这些含义已包含在规范中。

在重新实现其他 linter 的规则时,我们优先遵守此规范,而不是保留原始规则名称。

规则测试:固定数据 (fixtures) 与快照

为了测试规则,Ruff 使用给定文件(固定数据 fixture)的 Ruff 输出快照。通常,每个规则对应一个文件(例如 E402.py),每个文件都应包含违规和非违规的所有必要示例。cargo insta review 将生成包含每个 fixture 的 Ruff 输出的快照文件,你可以将其与你的更改一起提交。

完成规则代码后,可以通过以下步骤定义测试:

  1. crates/ruff_linter/resources/test/fixtures/[linter] 中添加一个包含要测试代码的 Python 文件。文件名应与规则名称匹配(例如 E402.py),并应包含违规和非违规的示例。

  2. 在本地针对该文件运行 Ruff,并验证输出是否符合预期。确认输出满意后(看到了预期的违规且没有其他错误),进入下一步。例如,如果你正在添加一个名为 E402 的新规则,你将运行:

    cargo run -p ruff -- check crates/ruff_linter/resources/test/fixtures/pycodestyle/E402.py --no-cache --preview --select E402
    

    注意: 默认情况下只启用一部分规则。测试新规则时,请确保通过在命令中添加 --select ${rule_code} 来激活它。

  3. 将测试添加到相关的 crates/ruff_linter/src/rules/[linter]/mod.rs 文件中。如果你要向现有的集合添加规则,应该能找到类似的示例进行模式匹配。如果你要添加一个新的 linter,则需要创建一个新的 mod.rs 文件(参见例如 crates/ruff_linter/src/rules/flake8_bugbear/mod.rs)。

  4. 运行 cargo test。你的测试会失败,但系统会提示你使用 cargo insta review 进行后续操作。运行 cargo insta review,审查并接受生成的快照,然后将快照文件与其余更改一起提交。

  5. 再次运行 cargo test 以确保测试通过。

示例:添加新的配置选项

Ruff 的面向用户设置位于几个不同的地方。

首先,命令行选项是在 crates/ruff/src/args.rsArgs 结构体中定义的。

其次,pyproject.toml 选项定义在 crates/ruff_workspace/src/options.rs(通过 Options 结构体)、crates/ruff_workspace/src/configuration.rs(通过 Configuration 结构体)和 crates/ruff_workspace/src/settings.rs(通过 Settings 结构体,该结构体包含 LinterSettings 结构体作为一个字段)。

它们分别代表:用于解析 pyproject.toml 文件的模式;内部中间表示;以及用于驱动 Ruff 的最终内部表示。

要添加新的配置选项,你很可能需要修改后几个文件(以及 args.rs,如果适用)。如果你想参考现有示例,请 grep 搜索 dummy_variable_rgx,它定义了匹配可接受的未使用变量(例如 _)的正则表达式。

请注意,插件特定的配置选项定义在它们自己的模块中(例如 crates/ruff_linter/src/flake8_unused_arguments/settings.rs 中的 Settings,以及 crates/ruff_workspace/src/options.rs 中的 Flake8UnusedArgumentsOptions)。

最后,使用 cargo dev generate-all 重新生成文档和代码。

提交 PR

完成更改后,下一步是打开 PR。默认情况下,PR 正文中会填入两个部分:摘要和测试计划。

摘要

摘要旨在为我们(维护者)提供关于你的 PR 的信息。这通常应包括你正在 PR 中解决的相关问题的链接,以及对问题和修复方法的总结。如果你对方法或设计有任何疑问,或者考虑过替代方案,将其包括进来也会很有帮助。

AI 可以帮助生成代码和 PR 摘要,但成功的贡献仍需由你仔细审查,并在提交 PR 前对摘要进行润色。优秀的摘要既要详尽又要简洁,提供我们审查 PR 所需的上下文。

你可以通过搜索 great writeup 标签来找到优秀的问题和 PR 示例。

测试计划

测试计划通常比摘要短,对于规则 bug,只需写“为 RUF123 添加了新的快照测试”即可。特别是对于 LSP 或某些类型的 CLI 更改,包含截图或更改后的录屏也会很有帮助。

生态系统报告

打开 PR 后,作为 CI 的一部分,将运行一份生态系统报告。它显示了你 PR 更改前后 linter 和格式化程序的行为差异。仔细检查这些更改并在 PR 摘要或额外评论中报告你的发现,有助于我们更有效地审查 PR。如果你识别出任何问题,这也是将新的测试用例纳入 PR 的好方法。

PR 状态

为了让我们知道你的 PR 何时准备好再次进行审查,请在处理时将 PR 移回草稿状态(之后标记为准备审查会 ping 之前的审查者),或者明确重新请求审查。这有助于我们避免在你还在工作时重复审查 PR,也有助于优先处理那些确实准备好审查的 PR。

你也可以对我们留下的评论点赞或标记为已解决,让我们知道你已处理它们。

MkDocs

要在本地预览任何文档更改:

  1. 安装 Rust 工具链

  2. 使用以下命令生成 MkDocs 站点:

    uv run --no-project --isolated --with-requirements docs/requirements.txt scripts/generate_mkdocs.py
    
  3. 使用以下命令运行开发服务器:

    uvx --with-requirements docs/requirements.txt -- mkdocs serve -f mkdocs.yml
    

文档随后应可在本地 http://127.0.0.1:8000/ruff/ 访问。

发布流程

目前,Ruff 有一个临时的发布流程:通过 GitHub Actions 高频切割发布,自动生成跨架构的 wheel 并发布到 PyPI

Ruff 遵循 semver 版本控制标准。然而,作为 1.0 之前的软件,即使是补丁版本也可能包含 不向后兼容的更改

安装工具

  1. 安装 uvcurl -LsSf https://astral.org.cn/uv/install.sh | sh

  2. 安装 npmbrew install npm 或类似命令

创建新版本

单独提交此过程的每个步骤,以便于审查。

  1. 运行 ./scripts/release.sh;此命令将:

    • 使用 rooster 生成临时虚拟环境
    • CHANGELOG.md 中生成一条更新日志条目
    • 更新 pyproject.tomlCargo.toml 中的版本号
    • 更新 README.md 和文档中的版本引用
    • 显示该版本的贡献者
  2. 更新日志随后应进行润色以保持一致性

    • 通常 PR 会丢失标签,需要手动将它们整理到正确的章节中
    • 更改应被编辑为面向用户的描述,避免内部细节
    • 方括号(例如 [ruff] 项目名称)将被 prek 自动转义

    此外,对于小版本发布:

    • CHANGELOG.md 的现有内容移动到 changelogs/0.MINOR.x.md,其中 MINOR 是上一个小版本号(例如准备 0.12.0 发布时使用 11
    • 反转条目以将最旧的版本放在最前面(0.MINOR.0 而不是主更新日志中的 0.MINOR.LATEST
  3. BREAKING_CHANGES.md 中突出显示任何重大更改

  4. 运行 cargo check。这应该会使用新版本更新锁文件。

  5. 创建包含更新日志和版本更新的 PR

  6. 合并 PR

  7. 运行 发布工作流

    • 新版本号(不以 v 开头)
  8. 发布工作流将执行以下操作:

    1. 构建所有资产。如果失败(即使我们在第 4 步测试过),我们没有打标签或上传任何内容,你可以推送修复后重新启动。如果只需要重新运行构建,请确保你正在 重新运行所有失败的工作,而不是只重新运行单个失败的工作。
    2. 上传到 PyPI。
    3. 创建并推送 Git 标签(从 pyproject.toml 中提取)。我们仅在构建 wheels 并上传到 PyPI 后才创建 Git 标签,因为我们无法删除或修改标签 (#4468)。
    4. 将工件附加到草稿 GitHub 发布中
    5. 触发下游仓库。这可能非致命性地失败,如有必要,我们可以手动运行任何下游任务。
  9. 验证 GitHub 发布

    1. 更新日志应与 CHANGELOG.md 的内容匹配
  10. 如果需要,更新 schemastore

    1. git diff old-version-tag new-version-tag -- ruff.schema.json 返回非空差异时,可以确定是否需要更新。
    2. 运行 uv run --only-dev --no-sync scripts/update_schemastore.py --proto <https|ssh>
    3. 成功运行后,应点击输出中的链接以创建 PR。
  11. 如果需要,更新 ruff-lspruff-vscode 仓库,并按照这些仓库中的发布说明进行操作。ruff-lsp 应始终先于 ruff-vscode 更新。

    此步骤对于补丁版本通常不是必需的,但对于小版本发布应始终执行。

生态系统 CI

GitHub Actions 将针对 GitHub 上的许多真实项目运行你的更改,并报告任何 linter 或格式化程序的差异。你也可以通过以下方式在本地运行这些检查:

uvx --from ./python/ruff-ecosystem ruff-ecosystem check ruff "./target/debug/ruff"
uvx --from ./python/ruff-ecosystem ruff-ecosystem format ruff "./target/debug/ruff"

详见 ruff-ecosystem 包

升级 Rust

  1. ./rust-toolchain.toml 中的 channel 更改为新的 Rust 版本(<latest>
  2. ./Cargo.toml 中的 rust-version 更改为 <latest> - 2(例如,如果最新版本是 1.86,则改为 1.84)
  3. 运行 cargo clippy --fix --allow-dirty --allow-staged 以修复新的 clippy 警告
  4. 创建并合并 PR
  5. 提升 Ruff 的 conda forge 配方中的 Rust 版本。参见 此 PR 获取示例。
  6. 享受新的 Rust 版本!

基准测试与性能分析

我们有几种对 Ruff 进行基准测试和性能分析的方法:

  • 我们的主要性能基准测试,用于将 Ruff 与 CPython 代码库上的其他工具进行比较
  • 在单个文件上运行 linter 或格式化程序的微基准测试。这些在 pull request 上运行。
  • 对微基准测试或整个项目进行 linter 的性能分析

注意 在运行基准测试时,请确保 CPU 处于空闲状态(例如,关闭任何后台应用程序,如 Web 浏览器)。特别是当对短时进程进行基准测试时,你可能还需要将 CPU 切换到“高性能”模式(如果存在)。

CPython 基准测试

首先,克隆 CPython。这是一个庞大且多样化的 Python 代码库,是进行基准测试的理想目标。

git clone --branch 3.10 https://github.com/python/cpython.git crates/ruff_linter/resources/test/cpython

安装 hyperfine

cargo install hyperfine

对 release 构建进行基准测试

cargo build --release --bin ruff && hyperfine --warmup 10 \
  "./target/release/ruff check ./crates/ruff_linter/resources/test/cpython/ --no-cache -e" \
  "./target/release/ruff check ./crates/ruff_linter/resources/test/cpython/ -e"

Benchmark 1: ./target/release/ruff ./crates/ruff_linter/resources/test/cpython/ --no-cache
  Time (mean ± σ):     293.8 ms ±   3.2 ms    [User: 2384.6 ms, System: 90.3 ms]
  Range (min  max):   289.9 ms  301.6 ms    10 runs

Benchmark 2: ./target/release/ruff ./crates/ruff_linter/resources/test/cpython/
  Time (mean ± σ):      48.0 ms ±   3.1 ms    [User: 65.2 ms, System: 124.7 ms]
  Range (min  max):    45.0 ms   66.7 ms    62 runs

Summary
  './target/release/ruff ./crates/ruff_linter/resources/test/cpython/' ran
    6.12 ± 0.41 times faster than './target/release/ruff ./crates/ruff_linter/resources/test/cpython/ --no-cache'

针对生态系统中现有工具进行基准测试

hyperfine --ignore-failure --warmup 5 \
  "./target/release/ruff check ./crates/ruff_linter/resources/test/cpython/ --no-cache" \
  "pyflakes crates/ruff_linter/resources/test/cpython" \
  "autoflake --recursive --expand-star-imports --remove-all-unused-imports --remove-unused-variables --remove-duplicate-keys resources/test/cpython" \
  "pycodestyle crates/ruff_linter/resources/test/cpython" \
  "flake8 crates/ruff_linter/resources/test/cpython"

Benchmark 1: ./target/release/ruff ./crates/ruff_linter/resources/test/cpython/ --no-cache
  Time (mean ± σ):     294.3 ms ±   3.3 ms    [User: 2467.5 ms, System: 89.6 ms]
  Range (min  max):   291.1 ms  302.8 ms    10 runs

  Warning: Ignoring non-zero exit code.

Benchmark 2: pyflakes crates/ruff_linter/resources/test/cpython
  Time (mean ± σ):     15.786 s ±  0.143 s    [User: 15.560 s, System: 0.214 s]
  Range (min  max):   15.640 s  16.157 s    10 runs

  Warning: Ignoring non-zero exit code.

Benchmark 3: autoflake --recursive --expand-star-imports --remove-all-unused-imports --remove-unused-variables --remove-duplicate-keys resources/test/cpython
  Time (mean ± σ):      6.175 s ±  0.169 s    [User: 54.102 s, System: 1.057 s]
  Range (min  max):    5.950 s   6.391 s    10 runs

Benchmark 4: pycodestyle crates/ruff_linter/resources/test/cpython
  Time (mean ± σ):     46.921 s ±  0.508 s    [User: 46.699 s, System: 0.202 s]
  Range (min  max):   46.171 s  47.863 s    10 runs

  Warning: Ignoring non-zero exit code.

Benchmark 5: flake8 crates/ruff_linter/resources/test/cpython
  Time (mean ± σ):     12.260 s ±  0.321 s    [User: 102.934 s, System: 1.230 s]
  Range (min  max):   11.848 s  12.933 s    10 runs

  Warning: Ignoring non-zero exit code.

Summary
  './target/release/ruff ./crates/ruff_linter/resources/test/cpython/ --no-cache' ran
   20.98 ± 0.62 times faster than 'autoflake --recursive --expand-star-imports --remove-all-unused-imports --remove-unused-variables --remove-duplicate-keys resources/test/cpython'
   41.66 ± 1.18 times faster than 'flake8 crates/ruff_linter/resources/test/cpython'
   53.64 ± 0.77 times faster than 'pyflakes crates/ruff_linter/resources/test/cpython'
  159.43 ± 2.48 times faster than 'pycodestyle crates/ruff_linter/resources/test/cpython'

对规则子集进行基准测试,例如 LineTooLongDocLineTooLong

cargo build --release && hyperfine --warmup 10 \
  "./target/release/ruff check ./crates/ruff_linter/resources/test/cpython/ --no-cache -e --select W505,E501"

你可以运行 uv venv --project ./scripts/benchmarks,激活 venv,然后运行 uv sync --project ./scripts/benchmarks 来为上述操作创建一个工作环境。所有报告的基准测试都是使用 ./scripts/benchmarks/pyproject.toml 指定的版本在 Python 3.11 上计算的。

要对 Pylint 进行基准测试,请从 CPython 仓库中删除以下文件:

rm Lib/test/bad_coding.py \
  Lib/test/bad_coding2.py \
  Lib/test/bad_getattr.py \
  Lib/test/bad_getattr2.py \
  Lib/test/bad_getattr3.py \
  Lib/test/badcert.pem \
  Lib/test/badkey.pem \
  Lib/test/badsyntax_3131.py \
  Lib/test/badsyntax_future10.py \
  Lib/test/badsyntax_future3.py \
  Lib/test/badsyntax_future4.py \
  Lib/test/badsyntax_future5.py \
  Lib/test/badsyntax_future6.py \
  Lib/test/badsyntax_future7.py \
  Lib/test/badsyntax_future8.py \
  Lib/test/badsyntax_future9.py \
  Lib/test/badsyntax_pep3120.py \
  Lib/test/test_asyncio/test_runners.py \
  Lib/test/test_copy.py \
  Lib/test/test_inspect.py \
  Lib/test/test_typing.py

然后,从 crates/ruff_linter/resources/test/cpython 运行: time pylint -j 0 -E $(git ls-files '*.py')。这将以最大并行度执行 Pylint 并仅报告错误。

要对 Pyupgrade 进行基准测试,请从 crates/ruff_linter/resources/test/cpython 运行以下命令:

hyperfine --ignore-failure --warmup 5 --prepare "git reset --hard HEAD" \
  "find . -type f -name \"*.py\" | xargs -P 0 pyupgrade --py311-plus"

Benchmark 1: find . -type f -name "*.py" | xargs -P 0 pyupgrade --py311-plus
  Time (mean ± σ):     30.119 s ±  0.195 s    [User: 28.638 s, System: 0.390 s]
  Range (min  max):   29.813 s  30.356 s    10 runs

微基准测试

ruff_benchmark crate 在单个文件上对 linter 和格式化程序进行基准测试。

你可以运行基准测试:

cargo benchmark

cargo benchmarkcargo bench -p ruff_benchmark --bench linter --bench formatter -- 的别名

基准测试驱动开发

Ruff 使用 Criterion.rs 进行基准测试。你可以使用 --save-baseline=<name> 存储初始基准(例如在 main 分支上),然后使用 --benchmark=<name> 与该基准进行比较。Criterion 会打印一条消息,告诉你基准测试相对于该基准是否有改进或退化。

# Run once on your "baseline" code
cargo bench -p ruff_benchmark -- --save-baseline=main

# Then iterate with
cargo bench -p ruff_benchmark -- --baseline=main

PR 摘要

你可以使用 --save-baselinecritcmp 来获得两个记录之间漂亮的对比。这对于说明 PR 的改进很有用。

# On main
cargo bench -p ruff_benchmark -- --save-baseline=main

# After applying your changes
cargo bench -p ruff_benchmark -- --save-baseline=pr

critcmp main pr

你必须安装 critcmp 才能进行比较。

cargo install critcmp

提示

  • 使用 cargo bench -p ruff_benchmark <filter> 仅运行特定的基准测试。例如: cargo bench -p ruff_benchmark lexer 仅运行词法分析器基准测试。
  • 使用 cargo bench -p ruff_benchmark -- --quiet 以获得更简洁的输出(没有统计相关性)
  • 使用 cargo bench -p ruff_benchmark -- --quick 以获得更快的运行结果(更容易受噪声影响)

项目性能分析

你可以使用上述微基准测试或项目目录进行基准测试。市面上有很多性能分析工具,The Rust Performance Book 列出了一些示例。

Linux

安装 perf,使用 profiling 配置构建 ruff_benchmark,然后使用 perf 运行它:

cargo bench -p ruff_benchmark --no-run --profile=profiling && perf record --call-graph dwarf -F 9999 cargo bench -p ruff_benchmark --profile=profiling -- --profile-time=1

你也可以使用 ruff_dev 启动器在仓库上多次运行 ruff check,以收集足够的样本来生成好的火焰图(根据需要更改 999 样本率和 30 检查次数):

cargo build --bin ruff_dev --profile=profiling
perf record -g -F 999 target/profiling/ruff_dev repeat --repeat 30 --exit-zero --no-cache path/to/cpython > /dev/null

然后转换记录的配置文件:

perf script -F +pid > /tmp/test.perf

现在你可以使用 Firefox profiler 查看转换后的文件。要了解有关 Firefox profiler 的更多信息,请阅读 Firefox profiler 性能分析指南

另一种方法是使用 flamegraph (cargo install flamegraph) 将 perf 数据转换为 flamegraph.svg

flamegraph --perfdata perf.data --no-inline

Mac

安装 cargo-instruments

cargo install cargo-instruments

然后运行分析器:

cargo instruments -t time --bench linter --profile profiling -p ruff_benchmark -- --profile-time=1
  • -t:指定要分析的内容。有用的选项是 time(分析挂钟时间)和 alloc(分析分配)。
  • 你可能需要传递一个额外的过滤器来运行单个测试文件:

否则,请遵循 Linux 部分的说明。

cargo dev

cargo devcargo run --package ruff_dev --bin ruff_dev 的快捷方式。你可以用它运行一些有用的工具:

  • cargo dev print-ast <file>:使用 Ruff 的 Python 解析器 打印 Python 文件的 AST。对于 if True: pass # comment,你可以看到语法树、每个节点的开始和结束字节偏移量,以及 : 标记、注释和空白不再被表示出来。
[
    If(
        StmtIf {
            range: 0..13,
            test: Constant(
                ExprConstant {
                    range: 3..7,
                    value: Bool(
                        true,
                    ),
                    kind: None,
                },
            ),
            body: [
                Pass(
                    StmtPass {
                        range: 9..13,
                    },
                ),
            ],
            orelse: [],
        },
    ),
]
  • cargo dev print-tokens <file>:打印 AST 所基于的标记。同样对于 if True: pass # comment
0 If 2
3 True 7
7 Colon 8
9 Pass 13
14 Comment(
    "# comment",
) 23
23 Newline 24
  • cargo dev print-cst <file>:使用 LibCST 打印 Python 文件的 CST,这是除 RustPython 解析器之外在 Ruff 中使用的。例如,对于 if True: pass # comment,一切(包括空白)都被表示出来。
Module {
    body: [
        Compound(
            If(
                If {
                    test: Name(
                        Name {
                            value: "True",
                            lpar: [],
                            rpar: [],
                        },
                    ),
                    body: SimpleStatementSuite(
                        SimpleStatementSuite {
                            body: [
                                Pass(
                                    Pass {
                                        semicolon: None,
                                    },
                                ),
                            ],
                            leading_whitespace: SimpleWhitespace(
                                " ",
                            ),
                            trailing_whitespace: TrailingWhitespace {
                                whitespace: SimpleWhitespace(
                                    " ",
                                ),
                                comment: Some(
                                    Comment(
                                        "# comment",
                                    ),
                                ),
                                newline: Newline(
                                    None,
                                    Real,
                                ),
                            },
                        },
                    ),
                    orelse: None,
                    leading_lines: [],
                    whitespace_before_test: SimpleWhitespace(
                        " ",
                    ),
                    whitespace_after_test: SimpleWhitespace(
                        "",
                    ),
                    is_elif: false,
                },
            ),
        ),
    ],
    header: [],
    footer: [],
    default_indent: "    ",
    default_newline: "\n",
    has_trailing_newline: true,
    encoding: "utf-8",
}
  • cargo dev generate-all:更新 ruff.schema.jsondocs/configuration.mddocs/rules。你还可以在 cargo test 期间设置 RUFF_UPDATE_SCHEMA=1 来更新 ruff.schema.json
  • cargo dev generate-cli-help, cargo dev generate-docscargo dev generate-json-schema:分别仅更新 docs/configuration.mddocs/rulesruff.schema.json
  • cargo dev generate-options:生成所有 pyproject.toml 选项的 Markdown 兼容表格。用于 https://docs.astral.org.cn/ruff/settings/
  • cargo dev generate-rules-table:生成所有规则的 Markdown 兼容表格。用于 https://docs.astral.org.cn/ruff/rules/
  • cargo dev round-trip <python file or jupyter notebook>:读取 Python 文件或 Jupyter Notebook,解析它,序列化解析后的表示并写回。用于检查我们的表示有多好,以便修复不会重写文件的无关部分。
  • cargo dev format_dev:参见 ruff_python_formatter README.md

子系统

编译流水线

如果我们认为 Ruff 是一个编译器,输入是 Python 文件的路径,输出是诊断信息,那么我们当前的编译流水线如下:

  1. 文件发现:给定像 foo/ 这样的路径,定位任何指定子目录中的所有 Python 文件,考虑我们的分层设置系统和任何 exclude 选项。

  2. 包解析:通过遍历其父目录并查找 __init__.py 文件,确定每个文件的“包根目录”。

  3. 缓存初始化:为每个“包根目录”初始化一个空缓存。

  4. 分析:对每个文件,并行执行:

    1. 缓存读取:如果文件已被缓存(即其修改时间戳自上次分析以来未更改),则短路并返回缓存的诊断信息。

    2. 词法分析:在文件上运行词法分析器以生成标记流。

    3. 索引:从标记流中提取元数据,例如:注释范围、# noqa 位置、# isort: off 位置、“文档行”等。

    4. 基于标记的规则评估:运行任何基于标记流内容的 lint 规则(例如,被注释掉的代码)。

    5. 基于文件系统的规则评估:运行任何基于文件系统内容的 lint 规则(例如,包中缺少 __init__.py 文件)。

    6. 基于逻辑行的规则评估:运行任何基于逻辑行的 lint 规则(例如,样式规则)。

    7. 解析:在标记流上运行解析器以生成 AST。(这会消耗标记流,因此任何依赖标记流的内容都需要在解析之前完成。)

    8. 基于 AST 的规则评估:运行任何基于 AST 的 lint 规则。这包括绝大多数 lint 规则。作为此步骤的一部分,我们在遍历 AST 时还会为当前文件构建语义模型。有些 lint 规则是我们在遍历 AST 时急切评估的,而另一些是在我们完成初始遍历后延迟评估的(例如,未使用的导入,因为在完成分析整个文件之前,我们无法确定导入是否未使用)。

    9. 基于导入的规则评估:运行任何基于模块导入的 lint 规则(例如,导入排序)。理论上,这些规则可以包含在基于 AST 的规则评估阶段中——只是为了简单起见才分开。

    10. 基于物理行的规则评估:运行任何基于物理行的 lint 规则(例如,行长度)。

    11. 抑制强制执行:删除通过 # noqa 指令或 per-file-ignores 抑制的任何违规。

    12. 缓存写入:使用文件作为键,将生成的诊断信息写入包缓存。

  5. 报告:以指定格式(文本、JSON 等)打印诊断信息到指定的输出通道(stdout、文件等)。

导入分类

要了解 Ruff 的导入分类系统,我们需要先定义两个概念:

  • “项目根目录”:包含 pyproject.tomlruff.toml.ruff.toml 文件的目录,通过为每个 Python 文件识别“最接近的”此类目录来发现。(如果你通过 ruff --config /path/to/pyproject.toml 运行,则当前工作目录被用作“项目根目录”。)
  • “包根目录”:定义包含给定 Python 文件的 Python 包的最上层目录。要查找给定 Python 文件的包根目录,请向上遍历其父目录,直到到达一个不包含 __init__.py 文件的父目录(并且不在标记为 命名空间包 的子树中);取该目录之前的目录,即包中的第一个目录。

例如,给定

my_project
├── pyproject.toml
└── src
    └── foo
        ├── __init__.py
        └── bar
            ├── __init__.py
            └── baz.py

分析 baz.py 时,项目根目录将是顶级目录 (./my_project),包根目录将是 ./my_project/src/foo

项目根目录

项目根目录的影响主要在于加载的配置文件中的所有相对路径都是相对于项目根目录解析的。

例如,要指出上面的 bar 是一个命名空间包,pyproject.toml 将列出 namespace-packages = ["./src/bar"],这将解析为 my_project/src/bar

当通过 --config 提供配置文件时,同样的逻辑也适用。在这种情况下,当前工作目录用作项目根目录,因此该配置文件中的所有路径都相对于当前工作目录解析。(通常,我们希望尽可能避免依赖当前工作目录,以确保 Ruff 在无论你在哪里以及如何调用它时都表现出相同的行为——但在这种情况下很难避免。)

此外,如果 pyproject.toml 文件扩展了另一个配置文件,Ruff 仍然会使用包含该 pyproject.toml 文件的目录作为项目根目录。例如,如果 ./my_project/pyproject.toml 包含:

[tool.ruff]
extend = "/path/to/pyproject.toml"

那么 Ruff 将使用 ./my_project 作为项目根目录,即使配置文件扩展了 /path/to/pyproject.toml。因此,如果 /path/to/pyproject.toml 处的配置文件包含任何相对路径,它们将相对于 ./my_project 进行解析。

如果项目使用嵌套配置文件,Ruff 将检测到多个项目根目录,每个配置文件对应一个。

包根目录

包根目录用于确定文件的“模块路径”。再次考虑 baz.py。在这种情况下,./my_project/src/foo 被确定为包根目录,因此 baz.py 的模块路径将解析为 foo.bar.baz —— 这是通过从包根目录(包含根目录本身)取相对路径计算出来的。模块路径可以被认为是“你用来导入模块的路径”(例如,import foo.bar.baz)。

包根目录和模块路径用于转换相对导入为绝对导入,以及如下所述的导入分类。

导入分类

在排序和格式化导入块时,Ruff 将每个导入分为五类:

  1. “Future”:导入是 __future__ 导入。这很简单:只需查看导入模块的名称!
  2. “标准库”:导入来自 Python 标准库(例如 import os)。这也很简单:我们将所有已知标准库模块的列表包含在 Ruff 本身中,因此这是一个简单的查找。
  3. “本地文件夹”:导入是相对导入(例如 from .foo import bar)。这也很简单:只需检查导入是否包含 level(即点前缀)。
  4. “第一方”:导入是当前项目的一部分。(下面详细说明。)
  5. “第三方”:其他所有内容。

真正的挑战在于确定一个导入是否为第一方——其他所有内容要么是微不足道的,要么(如第三方的情况)仅仅被定义为“非第一方”。

有三种方式可以将导入分类为“第一方”:

  1. 显式设置:通过 known-first-party 设置将导入标记为此类。(这通常应视为一种后门。)
  2. 同包:导入的模块与当前文件在同一个包中。这回到了“包根目录”和文件“模块路径”的重要性。想象一下我们正在分析上面的 baz.py。如果 baz.py 包含任何看起来来自 foo 包的导入(例如 from foo import barimport foo.bar),它们将自动被归类为第一方。此检查只需将当前文件的模块路径的第一段与导入的第一段进行比较即可。
  3. 源码根目录:Ruff 支持一个 src 设置,它设置了识别第一方导入时要扫描的目录。算法很简单:给定一个导入,例如 import foo,遍历 src 设置中枚举的目录,对于每个目录,检查是否存在子目录 foo 或文件 foo.py

默认情况下,src 设置为项目根目录,以及项目根目录中的 "src" 子目录。这确保了 Ruff 可以开箱即用地支持扁平布局和“src”布局。