# How to create a Ruff lint rule for Airflow 2-to-3 migrations
First, read Ruff’s documentation on contributing. You’ll at least need to read most of [“The Basics”](https://docs.astral.sh/ruff/contributing/#the-basics).
Rust has a somewhat perculiar way to write functions, mainly regarding its distinctive expression-statement separation. You probably want to read [Rust’s documentation on functions](https://doc.rust-lang.org/book/ch03-03-how-functions-work.html) first.
## Choose an error code
All codes for Airflow 2-3 should use the format `AIR3xx`, where `x` is a decimal number. Currently there are no additional rules.
It may make more sense for some migrations to combine them into one single error code, or an existing one. For example, `AIR302` covers all removals of deprecated functions and object members across the entire Airflow code base.
TODO: We should find a place to track all codes used by people so we don’t step on each other’s toes.
## Implement the rule
This is done by adding a file in `crates/ruff_linter/src/rules/airflow/rules`. Use a relatively descriptive name for the rule you want to implement.
A rule should typically include
* One or more Viloation types defined with `pub struct`.
* A lint function defined with `pub(create) fn` that produces violations.
### The Violation type
Each violation corresponds to a lint rule. It should be defined like this:
```rust
/// ## What it does
/// [ Briefly describing what this lint rule is for ... ]
///
/// ## Why is this bad?
/// [ Briefly describing why this lint rule is needed ... ]
///
/// ## Example
/// [ Provide examples ... ]
/// ```
#[violation]
pub struct MyRuleViolation {
}
impl Violation for MyRuleViolation {
#[derive_message_formats]
fn message(&self) -> String {
// Implement me...
}
}
```
The part before the definition with triple slashes is the struct documentation. It generates documentation for the lint rule, and will end up in [Ruff’s Rules documentation page](https://docs.astral.sh/ruff/rules/). See also [rustdoc’s documentation](https://doc.rust-lang.org/rustdoc/how-to-write-documentation.html) for details.
At runtime, Ruff uses the `message` function to convert a violation to a string for display. If you need context, it should be added as fields on the violation inside the `struct` definition, like this:
```rust
#[violation]
pub struct MyRuleViolation {
additional_information: String,
}
```
One important tool when implementing `message` is `format!`. It’s kind of a combination of Python’s `format` and f-string. See [Rust’s documentation on this](https://doc.rust-lang.org/std/fmt/index.html).
You don’t need `foramt!` if you just want a static string. Instead, just do something like `"my message ...".to_string()`. The additional `to_string` call is due to we need a string on the heap, while a string literal is on the stack. Don’t bother if you don’t have a clue what this means; you don’t really need to know (for our purposes), just do what you need to do.
### The linting function
This should be something like
```rust
pub(crate) fn my_lint_rule(checker: &mut Checker, expr: &Expr) {
// Implement me...
}
```
The function should take a checker, which you can push errors into, and the thing to lint. The above example takes an Expr—a [Python expression](https://docs.python.org/3/reference/expressions.html). This can be different if you want to lint a statement instead, or want to accept additional information (for example, what type this expression actually is, and components it consists of), but most of the time `expr: &Expr` is good enough.
It’s not really possible to describe hat the lint function should actually do (too context-dependant), so you’ll need to figure this part out yourself. There are many examples in the Ruff code base to copy from though!
The lint function should eventually create Violation objects, and wrap each of them in a `Diagnostic` object to be pushed into `check.diagnostics`, like this:
```rust
let violation = MyRuleViolation {};
checker.diagnostics.push(Diagnostic::new(violation, expr.range()));
```
The second argument to `Diagnostic::new` is used by Ruff to show the `^^^^^^ This part is wrong` message.
## Register the rule
Add the following lines:
```rust
// crates/ruff_linter/src/rules/airflow/rules/mod.rs
pub(crate) use my_rule_module::*;
mod my_rule_module;
```
```rust
// crates/ruff_linter/src/codes.rs
(Airflow, "XXX", RuleGroup::Preview, rules::airflow::rules::MyRuleViolation),
```
It should be relatively obvious where the lines should go in the `mod.rs` file. Search for “Airflow” in the `codes.rs` file to find where other Airflow errors are defined, and just add a line near them.
## Call the rule
Ruff’s documentation describes this well: https://docs.astral.sh/ruff/contributing/#example-adding-a-new-lint-rule
We’re usually creating an AST rule, so the call should go into either `expression.rs` (for expression rules) or `statement.rs` (for statement rules).
## Add a test case and run it
Again, Ruff’s documentation describes this well: https://docs.astral.sh/ruff/contributing/#rule-testing-fixtures-and-snapshots
## Re-generate Ruff schema
After the rule is registered in source, you must run
```
cargo dev generate-all
```
to update metadata. I didn’t look into what the command does exactly, but it
seems to at least update `ruff.schema.json`.
I believe Ruff has a pre-commit rule to enforce this, so you can install
pre-commit to have this taken care automatically if you prefer that.
## Submit a pull request
Please be polite! Ruff has a nice community, and we want to also be nice to them. Be a good citizen, provide good information in your PR, and follow existing styles. Read existing PRs for examples. Be responsive and answer reviews!
Looking forward to your rule being accepted.