CodeVetter · Repo Unpack · Extension points
astral-sh/ruff
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`.