# Lint Rule Development

> Step-by-step guide for creating and implementing lint rules in Biome's analyzer. Use when implementing rules like noVar, useConst, or any custom lint/assist rule, adding code actions to fix diagnostics, implementing semantic analysis for binding references, or adding configurable options to rules.

- Skill: `qdhenry/lint-rule-development` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add qdhenry/lint-rule-development`
- Raw SKILL.md: https://api.skillmd.com/api/skills/qdhenry/lint-rule-development/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: qdhenry (https://skillmd.com/u/qdhenry)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/qdhenry/lint-rule-development

---


## Purpose

Use this skill when creating new lint rules or assist actions for Biome. It provides scaffolding commands, implementation patterns, testing workflows, and documentation guidelines.

## Prerequisites

1. Install required tools: `just install-tools`
2. Ensure `cargo`, `just`, and `pnpm` are available
3. Read `crates/biome_analyze/CONTRIBUTING.md` for in-depth concepts

## Common Workflows

### Create a New Lint Rule

Generate scaffolding for a JavaScript lint rule:

```shell
just new-js-lintrule useMyRuleName
```

For other languages:
```shell
just new-css-lintrule myRuleName
just new-json-lintrule myRuleName
just new-graphql-lintrule myRuleName
```

This creates a file in `crates/biome_js_analyze/src/lint/nursery/use_my_rule_name.rs`

### Implement the Rule

Basic rule structure (generated by scaffolding):

```rust
use biome_analyze::{context::RuleContext, declare_lint_rule, Rule, RuleDiagnostic};
use biome_js_syntax::JsIdentifierBinding;
use biome_rowan::AstNode;

declare_lint_rule! {
    /// Disallows the use of prohibited identifiers.
    pub UseMyRuleName {
        version: "next",
        name: "useMyRuleName",
        language: "js",
        recommended: false,
    }
}

impl Rule for UseMyRuleName {
    type Query = Ast<JsIdentifierBinding>;
    type State = ();
    type Signals = Option<Self::State>;
    type Options = ();

    fn run(ctx: &RuleContext<Self>) -> Self::Signals {
        let binding = ctx.query();

        // Check if identifier matches your rule logic
        if binding.name_token().ok()?.text() == "prohibited_name" {
            return Some(());
        }

        None
    }

    fn diagnostic(ctx: &RuleContext<Self>, _state: &Self::State) -> Option<RuleDiagnostic> {
        let node = ctx.query();
        Some(
            RuleDiagnostic::new(
                rule_category!(),
                node.range(),
                markup! {
                    "Avoid using this identifier."
                },
            )
            .note(markup! {
                "This identifier is prohibited because..."
            }),
        )
    }
}
```

### Using Semantic Model

For rules that need binding analysis:

```rust
use crate::services::semantic::Semantic;

impl Rule for MySemanticRule {
    type Query = Semantic<JsReferenceIdentifier>;

    fn run(ctx: &RuleContext<Self>) -> Self::Signals {
        let node = ctx.query();
        let model = ctx.model();

        // Check if binding is declared
        let binding = node.binding(model)?;

        // Get all references to this binding
        let all_refs = binding.all_references(model);

        // Get only read references
        let read_refs = binding.all_reads(model);

        // Get only write references
        let write_refs = binding.all_writes(model);

        Some(())
    }
}
```

### Add Code Actions (Fixes)

To provide automatic fixes:

```rust
use biome_analyze::FixKind;

declare_lint_rule! {
    pub UseMyRuleName {
        version: "next",
        name: "useMyRuleName",
        language: "js",
        recommended: false,
        fix_kind: FixKind::Safe, // or FixKind::Unsafe
    }
}

impl Rule for UseMyRuleName {
    fn action(ctx: &RuleContext<Self>, _state: &Self::State) -> Option<JsRuleAction> {
        let node = ctx.query();
        let mut mutation = ctx.root().begin();

        // Example: Replace the node
        mutation.replace_node(
            node.clone(),
            make::js_identifier_binding(make::ident("replacement"))
        );

        Some(JsRuleAction::new(
            ctx.metadata().action_category(ctx.category(), ctx.group()),
            ctx.metadata().applicability(),
            markup! { "Use 'replacement' instead" }.to_owned(),
            mutation,
        ))
    }
}
```

### Quick Testing

Use the quick test for rapid iteration:

```rust
// In crates/biome_js_analyze/tests/quick_test.rs
// Uncomment #[ignore] and modify:

const SOURCE: &str = r#"
const prohibited_name = 1;
"#;

let rule_filter = RuleFilter::Rule("nursery", "useMyRuleName");
```

Run the test:
```shell
cd crates/biome_js_analyze
cargo test quick_test -- --show-output
```

### Create Snapshot Tests

Create test files in `tests/specs/nursery/useMyRuleName/`:

```
tests/specs/nursery/useMyRuleName/
├── invalid.js          # Code that triggers the rule
├── valid.js            # Code that doesn't trigger the rule
└── options.json        # Optional rule configuration
```

**Every test file must start with a top-level comment** declaring whether it expects diagnostics. The test runner enforces this — see the `testing-codegen` skill for full rules. The short version:

`valid.js` — comment is **mandatory** (test panics without it):
```js
/* should not generate diagnostics */
const x = 1;
const y = 2;
```

`invalid.js` — comment is strongly recommended (also enforced when present):
```js
/* should generate diagnostics */
const prohibited_name = 1;
const another_prohibited = 2;
```

Run snapshot tests:
```shell
just test-lintrule useMyRuleName
```

Review snapshots:
```shell
cargo insta review
```

### Generate Analyzer Code

During development, use the lightweight codegen commands:

```shell
just gen-rules          # Updates rule registrations in *_analyze crates
just gen-configuration  # Updates configuration schemas
```

These generate enough code to compile and test your rule without errors.

For full codegen (migrations, schema, bindings, formatting), run:

```shell
just gen-analyzer
```

**Note:** The CI autofix job runs `gen-analyzer` automatically when you open a PR, so running it locally is optional.

### Format and Lint

Before committing:
```shell
just f  # Format code
just l  # Lint code
```

### Adding Configurable Options

When a rule needs user-configurable behavior, add options via the `biome_rule_options` crate.
For the full reference (merge strategies, design guidelines, common patterns), see
[references/OPTIONS.md](references/OPTIONS.md).

**Quick workflow:**

**Step 1.** Define the options type in `biome_rule_options/src/<snake_case_rule_name>.rs`:

```rust
use biome_deserialize_macros::{Deserializable, Merge};
use serde::{Deserialize, Serialize};

#[derive(Debug, Default, Clone, Serialize, Deserialize, Deserializable, Merge)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields, default)]
pub struct UseMyRuleNameOptions {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub behavior: Option<MyBehavior>,
}
```

**Step 2.** Wire it into the rule:

```rust
use biome_rule_options::use_my_rule_name::UseMyRuleNameOptions;

impl Rule for UseMyRuleName {
    type Options = UseMyRuleNameOptions;

    fn run(ctx: &RuleContext<Self>) -> Self::Signals {
        let options = ctx.options();
        let behavior = options.behavior.unwrap_or_default();
        // ...
    }
}
```

**Step 3.** Test with `options.json` in the test directory (see [references/OPTIONS.md](references/OPTIONS.md) for examples).

**Step 4.** Run codegen: `just gen-rules && just gen-configuration`

**Key rules:**
- All fields must be `Option<T>` for config merging to work
- Use `Box<[Box<str>]>` instead of `Vec<String>` for collection fields
- Use `#[derive(Merge)]` for simple cases, implement `Merge` manually for collections
- Only add options when truly needed (conflicting community preferences, multiple valid interpretations)

## Tips

- **Rule naming**: Use `no*` prefix for rules that forbid something (e.g., `noVar`), `use*` for rules that mandate something (e.g., `useConst`)
- **Nursery group**: All new rules start in the `nursery` group
- **Semantic queries**: Use `Semantic<Node>` query when you need binding/scope analysis
- **Multiple signals**: Return `Vec<Self::State>` or `Box<[Self::State]>` to emit multiple diagnostics
- **Safe vs Unsafe fixes**: Mark fixes as `Unsafe` if they could change program behavior
- **Check for globals**: Always verify if a variable is global before reporting it (use semantic model)
- **Error recovery**: When navigating CST, use `.ok()?` pattern to handle missing nodes gracefully
- **Testing arrays**: Use `.jsonc` files with arrays of code snippets for multiple test cases

## Common Query Types

```rust
// Simple AST query
type Query = Ast<JsVariableDeclaration>;

// Semantic query (needs binding info)
type Query = Semantic<JsReferenceIdentifier>;

// Multiple node types (requires declare_node_union!)
declare_node_union! {
    pub AnyFunctionLike = AnyJsFunction | JsMethodObjectMember | JsMethodClassMember
}
type Query = Semantic<AnyFunctionLike>;
```

## References

- Full guide: `crates/biome_analyze/CONTRIBUTING.md`
- Rule examples: `crates/biome_js_analyze/src/lint/`
- Semantic model: Search for `Semantic<` in existing rules
- Testing guide: Main `CONTRIBUTING.md` testing section

