astral-sh/ruff
github.com/astral-sh/ruff ↗ · 3993 files · 17.2 MB · scan capped
Ruff Docs | Playground An extremely fast Python linter and code formatter, written in Rust.
Overview
Ruff 0.16.8 is Astral's Rust monorepo for a Python linter/formatter and the ty type checker. The user-facing `ruff` binary lints and formats Python (and notebooks) from the CLI, a stdio language server, or WASM; `ty` type-checks projects with a Salsa database and an optional uv integration. The PyPI package is a thin launcher around the native binary, not a Python implementation of the linter.
System map
The Cargo workspace (edition 2024, rust-version 1.96, toolchain channel 1.98.0) builds two binaries, `ruff` and `ty`, plus libraries they share: the Python parser and AST, diagnostics, and workspace settings. Ruff's check and format paths are parallel file walks over hierarchical TOML config with an on-disk `.ruff_cache`. ty is a separate CLI and language server on a Salsa 0.28 `ProjectDatabase`, with typeshed vendored under `crates/ty_vendored`. Release artifacts are a scratch Docker image, cargo-dist installers, PyPI (maturin bin binding), WASM playgrounds, and crate publishes, all driven from GitHub Actions.
- The workspace members are `crates/*`, package version 0.16.8, Rust edition 2024, minimum rust-version 1.96, license MIT, and the shared dependency set includes both `ruff_*` crates and `ty_*` crates plus salsa 0.28.2.
- The developer toolchain pin is Rust 1.98.0 in rust-toolchain.toml, which the Dockerfile copies before `rustup target add` and `cargo zigbuild --bin ruff --release`.
- `ruff` starts at `crates/ruff/src/main.rs`: it expands `@file` args with `argfile`, parses `Args` with clap, and calls `ruff::run`. Windows uses mimalloc; other supported arches use jemalloc. Broken-pipe errors exit 0; other failures print `ruff failed` and return `ExitStatus::Error` (code 2).
- `ty` starts at `crates/ty/src/main.rs` and calls `ty::run`. `ty::run` sets up Rayon, expands `@file` arguments, and dispatches `Cli` to check, server, version, shell completion, or explain. Exit statuses are Success 0, Failure 1, Error 2, InternalError 101, and Interrupted 130.
- PyPI packaging uses maturin with `bindings = "bin"`, `manifest-path = "crates/ruff/Cargo.toml"`, and `python-source = "python"`. `python/ruff/__main__.py` execs (or on Windows `subprocess.run`s) the native binary located by `find_ruff_bin`, which searches sysconfig script directories rather than embedding the linter in Python.
- Shared front-end: `ruff_python_parser::parse` builds a `Parsed<Mod>` via `Parser::new`, and both the linter (`lint_only`) and the formatter (`format_module_source`) consume that parse. The linter crate documents its public API as completely unstable.
- Ruff settings come from `ruff_workspace`. `project_files_in_path` normalizes paths, walks ancestors for `pyproject.toml` when hierarchical discovery is on, and walks the tree with the `ignore` crate's `WalkBuilder` (gitignore on, hidden files included, at most 12 threads).
- Lint/format caches live in `.ruff_cache` (`ruff_cache::CACHE_DIR_NAME`). `Cache::open` stores one rkyv file per package at `cache_dir/VERSION/<settings key>` and drops entries if the package root does not match.
- ty's persistent state is a Salsa `ProjectDatabase` created by `ProjectDatabase::fallible`. `check` and `check_with_reporter` delegate to `Project::check`. One-shot CLI checks call `disable_lru` and may `freeze` inputs; `run_check` then `mem::forget`s the database after optional `TY_MEMORY_REPORT` dumps.
- Language servers are separate crates speaking stdio LSP: `ruff server` calls `ruff_server::run` (worker threads capped at 4); `ty server` calls `ty_server::run_server` with an `OsSystem` fallback and the same thread cap.
- Release shape is cargo-dist 0.31.0 (`dist = false` on the workspace, local artifact jobs `./build-binaries`, `./build-docker`, `./build-wasm`; publish jobs PyPI, wasm, and crates). Installers are shell and PowerShell for a long target list, hosted at `https://releases.astral.sh/github/ruff/releases/download/{tag}`. The Docker image is `FROM scratch` with `ENTRYPOINT ["/ruff"]` and `WORKDIR /io`.
- In-browser entry points are `ruff_wasm` (`Workspace::new` deserializes `Options` into `Settings`) and the npm workspaces `ruff`, `ty`, and `shared` under `playground/`, built with Vite, React, Tailwind, and wasm-pack. Sharing uses a Cloudflare Worker in `playground/api`.
Feature catalog
Ruff's clap `Command` enum is the product surface: lint, format, an LSP server, rule and config help, cache cleaning, shell completions, and an import graph. The linter reimplements a large set of Flake8-family plugins as native rules under `ruff_linter::rules`, selected by code or category. ty exposes check (with fix and ignore insertion), a language server, and `explain rule`. Both tools have WASM playgrounds; Ruff also formats notebooks and can watch the filesystem.
- `ruff check` (`CheckCommand`) accepts paths or `-` for stdin, `--fix` / `--fix-only` / `--diff` / `--unsafe-fixes`, `--watch`, `--ignore-noqa`, `--add-noqa` and `--add-ignore`, `--select` / rule selectors, `--target-version`, `--preview`, and `--output-format` from `RUFF_OUTPUT_FORMAT` plus `--output-file` from `RUFF_OUTPUT_FILE`. `--show-settings` and `--show-files` short-circuit before linting.
- Default fix behavior generates fixes but does not write them. `--diff` selects `FixMode::Diff`; `--fix` or `--fix-only` selects `FixMode::Apply`; otherwise `FixMode::Generate`. Applicability stays automatic unless `--unsafe-fixes` is set. `--add-noqa` / `--add-ignore` refuse newlines in the reason and are incompatible with `--fix`.
- `ruff format` (`FormatCommand`) writes files by default. `--diff` selects `FormatMode::Diff` and `--check` selects `FormatMode::Check`. `--range` is rejected when the resolved path set contains more than one file. Stdin is handled by `commands::format_stdin::format_stdin` instead of `commands::format::format`.
- Formatting itself is `format_module_source`: parse with `ParseOptions` from the source type, build `TriviaRanges`, `format_module_ast`, then `print`. Parse, format, and print failures become `FormatModuleError` and map to `DiagnosticId::InternalError`.
- `ruff server` is the editor integration. Request handlers registered in `ruff_server` are code action, code-action resolve, document diagnostic, execute command, format, format range, hover, and shutdown. Notifications include didOpen/didChange/didClose for text documents and notebooks, configuration, watched files, and workspace changes. Code-action kinds include `source.fixAll.ruff` and `source.organizeImports.ruff` (plus notebook variants).
- Other Ruff subcommands: `rule` (one rule or `--all`), `config` (one option or the catalog), `linter` (upstream linter list), `clean`, hidden `generate-shell-completion`, `version`, and `analyze graph` (`AnalyzeGraphCommand`: direction, string-import detection, `--python` venv, type-checking imports, preview, target version).
- Native lint plugins are the `Linter` enum in `registry.rs` (prefixes such as AIR, E/W via pycodestyle, F, S, B, I, UP, PL, RUF) and matching modules under `crates/ruff_linter/src/rules/` (`airflow` through `tryceratops`, including `pyflakes`, `pylint`, `pyupgrade`, `isort`, `flake8_bugbear`, `ruff`). `code_to_rule` is the `#[ruff_macros::map_codes]` table that maps `(Linter, code)` to a rule struct.
- `ty` subcommands are `check`, `server`, `version`, hidden `generate-shell-completion`, and `explain rule`. `ty check` takes paths, `--project`, `--config-file`, `--fix`, `--add-ignore`, `--watch`, `--exit-zero`, color, verbosity, and progress flags. Fix mode is `FixMode::ApplyFixes` or `FixMode::AddIgnore`; plain runs are `MainLoopMode::Check`.
- ty diagnostics come from `ty_python_semantic::check_file`: I/O errors, parse errors, unsupported-syntax errors (with an inferred-version hint), then `check_types`. Lints are `LintMetadata` values created with `declare_lint!` (kebab-case name, summary, docs, default `Level` of ignore/warn/error, status) and stored in `default_lint_registry`.
- The Ruff WASM API exposes `Workspace` with `version`, a constructor from JS `Options`, and `defaultSettings`. The playground README describes two SPAs (Ruff at play.ruff.rs and a ty playground) using Monaco, Vite, and localStorage, plus a Worker KV share store.
- Notebooks are first-class inputs: watch mode treats `.py`, `.pyi`, `.pyw`, and `.ipynb` as source and `.toml` as configuration, and `lint_path` lints `pyproject.toml` / `ruff.toml` when a TOML rule is enabled via `lint_fix_toml` / `lint_toml`.
- `cargo dev` (`crates/ruff_dev`) is the maintainer CLI: `generate-all`, JSON schemas, rules tables, options docs, ty CLI/env/schema docs, `print-ast` / `print-cst` / `print-tokens`, formatter stability (`format-dev`), and `repeat` for profiling `ruff check`.
Data flow
Ruff and ty both start from OS arguments and project files, never from a network service in the CLI. Ruff turns paths into per-file source text, a parse, noqa/suppression directives, and diagnostics, then either prints them, writes fixes, or stores a negative cache entry. ty loads project metadata into a Salsa database, type-checks files in parallel, and optionally rewrites source. The only intentional subprocess is uv, and only when `TY_UV` enables it. The playground is the network boundary: the browser runs WASM locally and optionally POSTs source to Workers KV.
- Input boundary for both CLIs is `wild::args_os` expanded by `argfile::expand_args_from` (prefix `@`) before clap parsing. Ruff also honors `FORCE_COLOR` via `colored_override` and `RUFF_OUTPUT_FORMAT` / `RUFF_OUTPUT_FILE`.
- `ruff check` calls `resolve::resolve` for the root config, then `project_files_in_path`. Enabled rules decide whether `.py` and pyproject/ruff TOML files stay in the set. Package roots come from `resolver.package_roots`, and per-file `LinterSettings` come from `resolver.resolve(path)`.
- Per-file lint transform in `diagnostics::lint_path`: cache lookup by relative path plus `FileCacheKey` (mtime and permission mode); on miss, `SourceKind::from_path`; then `lint_fix` (apply or diff) or `lint_only`. `lint_only` parses, builds `Locator`, `Stylist`, and `Indexer`, extracts noqa directives and `Suppressions`, and calls `check_path`.
- `check_path` is phased: token rules, filesystem rules, logical-line rules, then (only if the parse is valid) `check_ast` plus import sorting and docstring line collection, then physical-line rules. AST checking is `checkers::ast::check_ast`. Diagnostics are sorted with `Diagnostic::ruff_start_ordering` before return.
- Output boundary for a check: `Printer` writes violations to stdout, or to `RUFF_OUTPUT_FILE` (parent directories created, color forced off) unless `--watch`. `--fix` writes transformed source with `File::create`; `--diff` writes a unified diff to stdout. Watch mode uses `notify::recommended_watcher` and re-resolves config when a `.toml` changes.
- Cache write path: after the parallel fold, `caches.persist()` runs `Cache::persist`, which rkyv-serializes `PackageCache` to a temp file in the cache directory and renames it into place. Windows `PermissionDenied` on that rename is warned, not failed. A cache hit with noqa enabled returns empty diagnostics and skips re-lint.
- ty input flow: cwd from `current_directory`, optional `--project` directory, absolute check paths, `OsSystem`, and `UseUv::from_system`. Metadata is `ProjectMetadata::from_config_file` or `discover_with_uv`, then `apply_configuration_files` and `set_override_options`. A single PEP 723 script file forces `UseUv::Scripts` even when uv is otherwise on.
- ty check execution: `MainLoop::main_loop` synchronizes script environments, then on `CheckWorkspace` clones the database and Rayon-spawns `db.check_with_reporter`. `Project::check` reports settings and file-set diagnostics, then `into_par_iter` calls salsa `check_file_impl`, which calls `ty_python_semantic::check_file` unless script settings are invalid. Results are discarded if the revision moved.
- ty fix output: `FixMode::ApplyFixes` calls `fix_all_diagnostics` with `Applicability::Safe` only; `FixMode::AddIgnore` calls `suppress_all_diagnostics`. `--exit-zero` forces `ExitStatus::Success` after a non-internal result. `TY_MEMORY_REPORT=short|full|json` dumps Salsa memory on the summary stream.
- When uv is enabled, `Uv::execute` runs `Command::new(executable)` with args `workspace metadata --quiet`. Workspace targets add `--active`; script targets add `--sync --script <path>` and optional `--python`. The executable is `UV` if set, otherwise `system.which("uv")`.
- Playground data flow: the SPA keeps editor state in localStorage and runs Ruff or ty as WASM. Share URLs hit the Worker: GET `/{id}` reads Workers KV namespace `PLAYGROUND`; POST reads the raw body, stores it under `crypto.randomUUID()`, and returns the id. Production CORS allows `https://play.ruff.rs` plus `playknot.ruff.rs`, `types.ruff.rs`, and `play.ty.dev`.
Behavior traces
Three user-visible pipelines dominate: process startup for each binary, `ruff check` from arguments to printed diagnostics and cache, and `ty check` from project discovery through a cancellable Salsa main loop. Format and the Ruff language server are the same file pipeline entered from different adapters. Watch modes re-enter those pipelines on filesystem events instead of exiting.
- Ruff startup order: `main` enables Windows virtual terminal, expands args, `Args::parse_from`, `run`. `run` applies color, `ruff_db::set_program_version`, installs a panic hook that points at the GitHub issue URL, skips logging for `Command::Server`, then matches `Command` to `check`, `format`, `server`, `analyze_graph`, or the help commands.
- `ruff check` order: `CheckCommand::partition` -> `resolve::resolve` -> optional `show_settings` / `show_files` -> compute `FixMode` -> if suppression flags, `commands::add_noqa::add_noqa` and return -> else build `Printer` -> if `--watch`, `recommended_watcher` and an initial `commands::check::check` -> otherwise the same `check` once.
- `commands::check::check` order: `project_files_in_path` -> retain Python and ruff/pyproject TOML -> `package_roots` -> `PackageCacheMap::init` if cache enabled -> `paths.par_iter` calling `lint_path` -> `catch_unwind` around `diagnostics::lint_path` -> fold/reduce diagnostics -> `sort_by(Diagnostic::ruff_start_ordering)` -> `caches.persist`. A panic becomes `create_panic_diagnostic` instead of aborting the process.
- `diagnostics::lint_path` order for Python: `FileCacheKey::from_path` and `cache.get` -> `settings.extension.get_source_type` -> `SourceKind::from_path` -> `lint_fix` or `lint_only` -> on `FixMode::Apply`, `transformed.write` to `File::create(path)`; on `Diff`, write `source_kind.diff` to stdout. TOML files branch earlier into `lint_fix_toml` / `lint_toml`.
- `lint_only` order: `settings.resolve_target_version` -> `ParseSource::into_parsed` -> `Locator::new` -> `Stylist::from_tokens` -> `Indexer::from_tokens` -> `directives::extract_directives` -> `Suppressions::from_tokens` -> `check_path`. `lint_fix` repeats that loop up to 100 iterations, tracking `FixTable`.
- `ruff format` order: `FormatCommand::partition` -> `resolve::resolve` -> `resolve_default_files` -> `project_files_in_path` -> `package_roots` -> optional `PackageCacheMap::init` -> parallel per-path format. Each file reaches `format_module_source` -> `parse` -> `format_module_ast` -> `Printed`.
- ty startup and check order: `main` -> `run` -> `setup_rayon` -> `Cli::parse_from` -> `run_check` -> `setup_tracing` -> resolve cwd/project/paths -> `UseUv::from_system` -> `ProjectMetadata::{from_config_file|discover_with_uv}` -> `ProjectDatabase::fallible` -> `MainLoop::new` -> `ctrlc::set_handler` -> `watch` or `run`.
- ty main-loop order: `synchronize_scripts` -> `request_check` -> biased select on uv sync wakeups, watcher messages, and check requests. `CheckWorkspace` defers while `has_pending_synchronizations`, else spawns `check_with_reporter` + `into_sorted_diagnostics`. `CheckCompleted` either `write_diagnostics` or `fix_all_diagnostics` / `suppress_all_diagnostics`. Ctrl+C calls `CancellationTokenSource::stop`.
- Per-file ty order inside `check_file_impl`: if the file is a script without valid settings, return script settings diagnostics; else `ty_python_semantic::check_file`; then append script settings diagnostics and a dependency-metadata diagnostic. `check_file` itself loads `source_text`, returns `IOErrorDiagnostic` on read error, extends parse and unsupported-syntax diagnostics, extends `check_types`, and sorts by `rendering_sort_key`.
- Ruff LSP diagnostic order: `publish_diagnostics_for_document` calls `generate_diagnostics`, which no-ops unless `client_settings().lint()`, otherwise `crate::lint::check` on the document snapshot, then `Client::send_notification` of `PublishDiagnosticsNotification` per URI with the document version. Server boot is `ConnectionInitializer::stdio` then `Server::new(...).run` and `io_threads.join`.
- Cache persistence order on a dirty cache: `Cache::persist` -> `save` merges changes -> `tempfile_in(parent)` -> `rkyv::to_bytes` of `PackageCache` -> `write_all` -> `NamedTempFile::persist`. No-change caches return before opening a temp file.
Testing signals
Tests are Rust-native: cargo-nextest in CI, insta snapshots, and ty Markdown fixtures executed by `datatest-stable`. Parser, linter, and formatter crates carry large snapshot and fixture trees; the Ruff CLI has integration tests under `crates/ruff/tests`. CI path-filters which jobs run, and additionally dogfoods `ty check` on in-repo Python. Fuzz targets cover parse, fix, and format idempotency. Playground Worker code and file-watching tests are the obvious gaps.
- Linux CI job `cargo test (linux)` runs `uv run --locked --only-dev cargo insta test --all-features --unreferenced reject --test-runner nextest --disable-nextest-doctest`, then `cargo test --doc --all-features`. It is skipped when the PR has label `no-test` or `determine_changes` says no code changed (except on main).
- The same job dogfoods ty: `cargo run -p ty check` on `python/py-fuzzer`, `scripts/ty_benchmark`, and `scripts/*.py` (the last with `TY_UV=scripts` and `UV_LOCKED=true`). A prior step runs `cargo test -p ty_python_semantic --test mdtest` with `MDTEST_GITHUB_ANNOTATIONS_FORMAT=1` and ignores its exit code so annotations cannot fail the job.
- CI also runs `cargo fmt`, `shellcheck`, `cargo clippy` (including a wasm clippy job), `cargo hawk` at HAWK_VERSION 0.1.13, and a `cargo publish` dry-run. `determine_changes` sets outputs `parser`, `linter`, `formatter`, `ty`, `playground`, `fuzz`, `py-fuzzer`, `benchmarks`, and `release` from path filters. Workflow permissions are `contents: read`.
- ty semantic tests are Markdown files. `crates/ty_python_semantic/tests/mdtest.rs` registers `mdtest` and `lint_doc` via datatest-stable; each fixture runs on a one-thread Rayon pool. `lint_doc` looks the filename stem up in `default_lint_registry` and passes it as `default_error_rule`. Fixtures live under `./resources/mdtest` and `./resources/lint_docs`.
- Ruff CLI coverage includes `crates/ruff/tests` (integration and CLI snapshot tests) and a unit test `unreadable_files` in `commands/check.rs` that creates mode-0o000 files and snapshots E902 / invalid-pyproject diagnostics with insta. Rule crates keep insta snapshots beside rules (for example `rules/pyflakes` and `rules/pylint`).
- Parser tests are fixture-driven: `crates/ruff_python_parser/resources` holds hundreds of `.py` fixtures and `crates/ruff_python_parser/tests` holds matching snapshots. Formatter tests similarly pair `resources/test` fixtures with `tests` snapshots. The parser fuzz target `ruff_parse_simple` and idempotency target `ruff_parse_idempotency` sit outside the workspace.
- Fuzz package `ruff-fuzz` (separate `[workspace]`, cargo-fuzz, libfuzzer-sys) builds bins `ty_check_invalid_syntax`, `ruff_parse_simple`, `ruff_fix_validity`, `ruff_formatter_validity`, `ruff_parse_idempotency`, and `ruff_formatter_idempotency`. It depends on the linter, parser, formatter, and ty semantic crates with the semantic `testing` feature.
- AGENTS.md requires `INSTA_FORCE_PASS=1 INSTA_UPDATE=always MDTEST_UPDATE_SNAPSHOTS=1 cargo nextest run` (or `cargo test` if nextest is missing), forbids hand-editing snapshot bodies, and says file-watcher tests do not work in the sandbox. Clippy gate is `cargo clippy --workspace --all-targets --all-features -- -D warnings`.
- `pyproject.toml` defines a `ruff-lsp-test` dependency group (pytest, pygls 1.x, lsprotocol) separate from the Rust LSP crates, and a `ty-ecosystem` group on `ecosystem-analyzer`. Those are integration/ecosystem harnesses, not the in-crate nextest suites.
- The playground Worker (`playground/api/src/index.ts`) has no test module beside it, and `playground/package.json` scripts are `dev:wasm`, oxlint, `tsc`, and prettier rather than a behavioral test runner. WASM API tests exist as `crates/ruff_wasm/tests/api.rs` and `crates/ty_wasm/tests/api.rs`, which is a different layer.
- An ignored `linter_quick_test` in `ruff_linter` is an ad-hoc debug entry point (`#[ignore]`) that runs one rule through `test_contents`. It is not part of normal CI coverage.
Risk map
The repository's own threat model treats project source and config as attacker-controlled and forbids analysis from executing that code. The sharp edges that do cross a process or network boundary are ty's uv subprocess, fix/format writes, cache files, stdio LSP, and the unauthenticated playground share API. Internally, the rule table in `codes.rs`, Salsa cancellation, and the unstable linter API are the maintenance blast radii.
- The CLI threat model states analysis must not execute user-supplied code; `--isolated` must ignore config files; reading files reached via imports, config extensions, or symlinks is expected; fixes, format, ignore insertion, and cache cleanup may change only the operation's selected targets; `ruff format --check` must not write source.
- ty's uv integration is off unless `TY_UV` is `1`, `true` (`UseUv::On`), or `scripts` (`UseUv::Scripts`). When on, `Uv::execute` runs the `UV` env var or `PATH`'s `uv`. Script metadata uses `workspace metadata --sync --script`, which the threat model says may install dependencies.
- `--fix` and format `Write` mode replace file contents via `File::create` and a full write of the transformed source. A failed fix falls back to `lint_only` on the original. `--add-noqa` also rewrites files. These are the intentional mutation paths.
- Cache files are written outside the source tree only in the sense that `.ruff_cache` (or `cache-dir`) sits in the project and is excluded from linting by default. Persistence is atomic rename, but a mismatched package root silently clears `package.files`, and unreadable cache files are replaced with an empty cache after a warning.
- The playground share Worker stores the entire POST body in KV with a random UUID and no authentication, size check, or content-type check in `fetch`. GET returns the stored text to any caller who has the id. Production CORS is an allowlist, not an auth scheme. Dev mode sets `Access-Control-Allow-Origin: *`.
- Both language servers read editor documents over stdio and trust the client connection. Ruff publishes diagnostics from in-memory snapshots (`snapshot.query()`), so a hostile document is linted with the server's filesystem rights. ty's server additionally requires a Unicode cwd and keeps an `OsSystem` fallback for files not in the index.
- Python launcher risk is local binary lookup: `find_ruff_bin` returns the first `ruff` executable on a fixed list of script directories and `__main__` `execvp`s it. A planted binary earlier in that search list would run with the user's privileges. On Windows the launcher uses `subprocess.run` and maps `KeyboardInterrupt` to exit 2.
- Lint panics are isolated per file with `catch_unwind` and reported as diagnostics, but `Cache::open` and the Windows color setup still use `unwrap`/`assert` on paths that are not panic-isolated (`SystemTime::UNIX_EPOCH.elapsed().unwrap()`, `set_virtual_terminal` assert). ty's check thread uses `salsa::Cancelled::catch` and drops stale revisions.
- `codes.rs` `code_to_rule` is a single macro-expanded match from every rule code to a rule type. A new rule that is not added there is unreachable from `--select` and noqa codes. `Rule::from_code` searches `Linter::all_rules` by noqa suffix.
- ty one-shot checks `freeze` the database and then `std::mem::forget(db)` so destructors do not run. `system_mut` documents that it cancels other handles and can deadlock if the refcount is not 1. Watch mode, fix modes, and memory reports must not freeze.
- The linter library is explicitly unstable. WASM, the CLI, and the language server all call into `check_path` / settings types, so a signature change in `ruff_linter` fans out across `crates/ruff`, `ruff_server`, and `ruff_wasm`.
- Docker build fetches rustup with `curl | sh` and compiles a musl binary. That is a release-supply-chain path, not a runtime path for users of the scratch image. cargo-dist comments that CI customizes cargo-dist install specifically to avoid `curl | sh` for the dist tool itself.
Extension points
New Ruff rules plug into a plugin module, the `Linter` enum, and the `code_to_rule` table, usually scaffolded by `scripts/add_rule.py`. New ty lints plug into `declare_lint!` plus `LintRegistryBuilder::register_lint`. User-facing commands, LSP methods, TOML options, and WASM settings are each a closed table that must be updated and then regenerated into docs and JSON schema. Checker phases and `FormatNodeRule` are the extension points inside an existing rule or syntax node.
- `scripts/add_rule.py` scaffolds a rule (`--name`, `--prefix`, `--code`, `--linter`, `--category`). The implementation then lives under `crates/ruff_linter/src/rules/<plugin>/` and must be referenced from `code_to_rule` in `codes.rs`. Plugin modules are declared in `rules/mod.rs`.
- `Linter` is a `RuleNamespace` enum. `#[derive(RuleNamespace)]` in `ruff_macros` generates the prefix and rule-iteration impls. `Rule::from_code` parses a code into that namespace and matches the noqa suffix.
- Rule structs use `#[derive(ViolationMetadata)]` (`ruff_macros::derive_violation_metadata`). `Violation` and `AlwaysFixableViolation` are the public traits re-exported from `ruff_linter`. Test-only rules are compiled in under `feature = "test-rules"` inside `check_path`.
- ty lints are not in the Ruff `Rule` enum. `declare_lint!` builds a `LintMetadata` static; `register_lints` in `ty_python_semantic` calls `types::register_lints` and then registers ignore-comment lints. `LintRegistryBuilder::register_lint` panics on a duplicate name. Aliases go through `register_alias`.
- CLI extension is the clap `Command` enum in `crates/ruff/src/args.rs` and the match in `ruff::run`. ty's parallel table is `crates/ty/src/args.rs` `Command` and the match in `ty::run`. Hidden shell-completion variants call `clap_complete_command`.
- Ruff LSP methods are one module per request under `server/api/requests/` and one per notification under `server/api/notifications/`, re-exported from `requests.rs` and `notifications.rs`. Adding a method means a new handler type plus registration in those modules, not a dynamic plugin loader.
- Configuration options use `#[derive(OptionsMetadata)]` and `CombineOptions` from `ruff_macros`. `ruff config` prints that metadata. `cargo dev generate-all` / `generate-json-schema` / `generate-options` / `generate-ty-schema` regenerate `ruff.schema.json`, `ty.schema.json`, and docs. CLI overrides arrive as `ConfigurationTransformer` implementations passed into `resolve_root_settings`.
- Lint passes are selected by `rule.lint_source()` inside `check_path`: tokens, filesystem, logical lines, AST, imports, physical lines, and TOML. A rule that needs a new pass has to extend that match of sources, not only the rule body. AST work enters through `check_ast`.
- Formatter node coverage is the `FormatNodeRule<N>` trait (`fmt` handles comments and source-map positions; `fmt_fields` is implemented per node). `format_module_ast` is the stable entry for callers that already have a `Parsed<Mod>`.
- WASM settings are whatever `Options` / `Configuration::from_options` accepts. `Workspace::new` is the JS constructor; `defaultSettings` re-serializes defaults including `DEFAULT_SELECTORS`. There is no runtime plugin registry in the WASM crate.
- File discovery can be customized per invocation by implementing `ConfigurationTransformer` and passing it to `project_files_in_path` / `resolve_scoped_settings`. The CLI's `ConfigArguments` is that transformer for `--config` key/value overrides and `--isolated`.
Contributor handoff
Read the CLI dispatch and the crate you are changing before editing; Ruff and ty share a parser but not a rule registry. Semantic changes need a focused test, snapshot updates via the environment variables rather than hand edits, and `cargo dev generate-all` when options, rules, or CLI help change. ty code must keep `db` in the Salsa parameter position and must not touch another file's AST from an untracked query. Do not drop Python repros inside this checkout, and do not expect file-watcher tests to pass in a sandbox.
- Start with `AGENTS.md` (test commands, mdtest style, Salsa rules, `db` parameter order, prek) and `crates/ruff/src/lib.rs` plus `args.rs` for Ruff CLI work, or `crates/ty/src/lib.rs` for ty CLI work. Rule work starts at `codes.rs` `code_to_rule` and the plugin module; ty lint work starts at `declare_lint!` and `register_lints`.
- Safe Ruff lint edit: add or change a rule under `crates/ruff_linter/src/rules/<plugin>/`, register it in `code_to_rule`, add a fixture and insta snapshot, and leave unrelated plugins alone. The public `ruff_linter` API is documented as unstable, so do not churn signatures to make a single rule compile.
- Tests to run for a linter change: `cargo nextest run -p ruff_linter` (or the narrower test binary) with `INSTA_FORCE_PASS=1 INSTA_UPDATE=always MDTEST_UPDATE_SNAPSHOTS=1`, then review snapshot diffs. For ty semantic changes, run `cargo nextest run -p ty_python_semantic --test mdtest`. Full CI uses `cargo insta test --all-features --unreferenced reject --test-runner nextest`.
- Never edit `.snap` files or inline snapshot bodies by hand. After option, CLI, rule, or environment-variable definition changes, run `cargo dev generate-all` so `ruff.schema.json`, `ty.schema.json`, and generated docs stay aligned. Finish with `uv run --only-dev --locked prek run --files <each changed path>`.
- ty parameter convention: `db` is the first argument of free functions and comes immediately after `self` on methods. Methods that read `.node()` should be `#[salsa::tracked]`. `check_file_impl` is already a tracked query; do not call raw AST helpers from module A while checking module B.
- Ad-hoc Python repros must be created outside the checkout. The root `pyproject.toml` sets `requires-python = ">=3.7"` and `tool.ruff.target-version = "py38"`, so ty discovered from this tree infers an old Python version. ty preview for 3.15 is still warned unless preview is enabled.
- Do not use `panic!`, `unreachable!`, `unwrap`, or `expect` for conditions the type system can encode. Existing exceptions include the Windows VT assert in `main` and cache timestamp truncation; do not add more. Lint-path failures should become diagnostics the way `catch_unwind` already does.
- ty PR titles should start with `[ty]`. LSP-only changes use the `server` label when you can set labels. Avoid competing with an open PR; `CONTRIBUTING.md` is the duplicate-work check.
- File-watcher tests do not work in the sandbox. Ruff watch mode is `recommended_watcher` in `ruff::check`; ty watch mode is `watch::directory_watcher` plus `ProjectWatcher`. Skip those locally unless the change is specifically about watching.
- Cache keys include the crate version string, so behavior changes that should invalidate user caches need a version bump or an explicit settings-key change. Do not hand-delete logic that compares `package.package_root` or the atomic rename; both exist because of real corruption bugs.
- Formatter and parser changes have their own fixture trees and fuzz targets (`ruff_formatter_idempotency`, `ruff_parse_idempotency`). A parser change that still snap-passes can fail idempotency fuzz; run the crate tests, not only a single rule snapshot.
- Security-sensitive edits (subprocess, LSP trust, playground KV, cache paths, fix application) should be checked against `agents/references/cli-threat-model.md`, `language-server-threat-model.md`, `playground-threat-model.md`, or `repository-threat-model.md` as appropriate. uv must stay default-off.
Languages
| Language | Files | Share of source |
|---|---|---|
| Python | 2979 | 57% |
| Rust | 2003 | 39% |
| Markdown | 679 | docs/config |
| JSON | 148 | docs/config |
| TOML | 147 | 3% |
| TypeScript | 49 | 1% |
| YAML | 32 | docs/config |
| Shell | 17 | 0% |
Start reading here
Probable source entrypoint: Cargo.toml — inferred from the directory tree and manifest, not the upstream docs.
Source roots
crates/ (3681 files)
Source layout — crates/
| Path | Files |
|---|---|
crates/mdtest/ | 1 |
crates/ruff/ | 86 |
crates/ruff_annotate_snippets/ | 102 |
crates/ruff_benchmark/ | 3 |
crates/ruff_cache/ | 3 |
crates/ruff_db/ | 4 |
crates/ruff_dev/ | 1 |
crates/ruff_diagnostics/ | 2 |
crates/ruff_formatter/ | 2 |
crates/ruff_graph/ | 2 |
crates/ruff_index/ | 2 |
crates/ruff_linter/ | 36 |
Repository structure
| Top-level path | Files |
|---|---|
crates/ | 3681 |
scripts/ | 82 |
playground/ | 72 |
.github/ | 34 |
docs/ | 25 |
python/ | 18 |
changelogs/ | 15 |
fuzz/ | 10 |
Docs found: AGENTS.mdCLAUDE.mdCONTRIBUTING.mdREADME.mddocs/configuration.mddocs/editors/features.mddocs/editors/index.mddocs/editors/migration.mddocs/editors/settings.mddocs/editors/setup.md
Package
Cargo.toml — , 0 declared dependencies.
CI & config entrypoints
README.md— Repository readmeAGENTS.md— Agent instructionsCLAUDE.md— Claude instructions.github/workflows/build-binaries.yml— GitHub Actions workflow.github/workflows/build-docker.yml— GitHub Actions workflow.github/workflows/build-wasm.yml— GitHub Actions workflow.github/workflows/notify-dependents.yml— GitHub Actions workflow.github/workflows/publish-crates.yml— GitHub Actions workflow.github/workflows/publish-docs.yml— GitHub Actions workflow.github/workflows/publish-mirror.yml— GitHub Actions workflow
Structural health signals
Average score 7/10 across 96 analyzed source files; 26 hotspots surfaced.
playground/ty/src/Editor/Editor.tsx— 1186 lines, score 5 (Large file, Deep nesting)crates/ruff_linter/src/test.rs— 606 lines, score 5.2 (Large file, Deep nesting)scripts/build_ruff_pgo.py— 579 lines, score 5.2 (Large file, Deep nesting)
These are deterministic review leads — size, churn, and structural signals — not proof of a bug in this project.
Recent history
c131cbad5a0f2026-09-22 — [ty] Fix staticmethod shadowing through generic receivers and unions (#28766)4b45f36fa6622026-09-22 — [ty] Correct descriptor prose in method documentation (#28765)660350be26482026-09-21 — [ty] Infer constant membership in inline list and set literals (#28676)e86121d0d1652026-09-21 — [ty] Refresh diagnostics after workspace configuration changes (#28755)67b3bd4122472026-09-21 — Update rules table with category information (#28651)