Open-Source Wikis

/

Ruff

/

How to contribute

/

Patterns and conventions

astral-sh/ruff

Patterns and conventions

What the codebase expects of new code. The bulk of these come from AGENTS.md, CONTRIBUTING.md, and reviewer custom; a few are inferred from how the code is shaped.

Rust style

  • Edition 2024, MSRV 1.93 (see Cargo.toml and rust-toolchain.toml).
  • Imports at the top of the file, never inside functions. The pre-commit hook will flag this; reviewers will too.
  • Prefer let chains (if let Pat = expr && cond) over nested if let to flatten control flow.
  • Sentence case in headings in docs and /// comments.
  • #[expect(lint)] over #[allow(lint)] when you must suppress a Clippy/rustc warning. If a warning is about unused/dead code, prefer to delete the code.
  • Comments are for invariants and rationale, not narration. Don't write // increment x.

Avoid panics

The repo's stance is firm. From AGENTS.md:

Try hard to avoid patterns that require panic!, unreachable!, or .unwrap(). Instead, try to encode those constraints in the type system. Don't be afraid to write code that's more verbose or requires largeish refactors if it enables you to avoid these unsafe calls.

In practice:

  • Return Option/Result rather than panicking on missing data.
  • Use enums to model exhaustive choices, so the compiler enforces match completeness.
  • If you really do have a "should never happen" branch, prefer tracing::error! + a sensible fallback over panic!.

Avoid writing too much new code

Also from AGENTS.md:

Avoid writing significant amounts of new code. This is often a sign that we're missing an existing method or mechanism that could help solve the problem. Look for existing utilities first.

Before introducing a new helper, search for a similar one. The ruff_python_ast::helpers module, the SemanticModel, and ruff_python_semantic::analyze modules contain a lot of utilities that beginners often re-implement.

Diagnostics

  • Concise messages. From AGENTS.md: "Think about how an error message would look on a narrow terminal screen." Long detail belongs in subdiagnostics or annotations.
  • Stable rule codes. Once a rule code is published, it's a public API. Renumbering or repurposing is a breaking change.
  • Severity — most rules are Warning; reserve Error for things that would prevent the file from running.

AST navigation

  • The ruff_python_ast::Visitor and ruff_python_ast::visitor::transformer::Transformer traits are the canonical way to walk and rewrite trees. New rules typically don't write a custom visitor; they hook into crates/ruff_linter/src/checkers/ast.rs.
  • ruff_python_ast::helpers has is_const_none, is_dunder, pep_604_union, map_subscript, etc. — search before reimplementing.
  • Use TextRange from ruff_text_size for spans. Never compute byte offsets by hand.

Semantic model usage

Lint rules access the semantic model via Checker::semantic(). Common patterns:

  • Resolve a name to its binding: semantic.resolve_name(expr_name).
  • Walk to the enclosing scope: semantic.scope().kind — distinguishes module / function / class / lambda / type-param scopes.
  • Check whether a call resolves to a typing import: semantic.match_typing_expr(...) and friends in ruff_python_semantic::analyze::typing.

Salsa rules (ty)

From AGENTS.md:

Salsa incrementality (ty): Any method that accesses .node() must be #[salsa::tracked], or it will break incrementality. Prefer higher-level semantic APIs over raw AST access.

This is the single most common review comment on ty PRs. When in doubt, look for a tracked function that already returns what you need.

Configuration options

  • New options live in ruff_workspace::Settings (or a sub-struct).
  • Options are documented in their derive(Options) macro attributes (see crates/ruff_options_metadata).
  • After adding an option: run RUFF_UPDATE_SCHEMA=1 cargo test (or cargo dev generate-all) to regenerate ruff.schema.json.
  • Avoid breaking changes to existing options. Deprecate first, then remove in a major version.

Naming

  • Crates: ruff_* for Ruff-side code, ty_* for ty-side code. Shared crates use the ruff_ prefix and are imported by ty.
  • Rules: <plugin>/rules/<rule_name>.rs, with the function name matching the rule's snake_case identifier.
  • Test files: crates/<crate>/tests/<feature>.rs for integration tests; inline #[cfg(test)] modules for unit tests.

Documentation

  • New rules: write a triple-slash doc on the rule's struct describing What it does, Why it's bad, Example, and (where relevant) Options. The mkdocs site and the --explain CLI both read from these.
  • Don't update README.md or docs/ to reflect implementation details; reviewers will steer you to the right place.
  • ty's contributing notes (crates/ty/CONTRIBUTING.md) cover ty-specific style.

Pull request etiquette

  • Small, focused PRs.
  • One rule, one option, one ty check per PR — usually.
  • ty PRs: title prefix [ty], label ty on GitHub.
  • Run uvx prek run -a after every rebase and after addressing review comments.
  • Don't push generated files that haven't been regenerated; CI will tell you if they drifted.

What good looks like

A typical "add a new lint rule" PR contains:

  1. The rule implementation under crates/ruff_linter/src/rules/<plugin>/rules/<rule>.rs.
  2. A registration in crates/ruff_linter/src/codes.rs.
  3. A fixture under crates/ruff_linter/resources/test/fixtures/<plugin>/<rule>.py.
  4. A snapshot test that asserts the diagnostics, and the resulting .snap file.
  5. Generated artifacts (ruff.schema.json, docs) updated by cargo dev generate-all.
  6. A changelog entry (added to the unreleased section).

Reviewers will check the diff for all six.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Patterns and conventions – Ruff wiki | Factory