Skip to content
Development
Skill

/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

From plugin
biome
26k12 skills
Install
$ npx -y skills add biomejs/biome --skill lint-rule-development --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/lint-rule-development

Context preview

The summary Claude sees to decide when to auto-load this skill.

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

SKILL.md

lint-rule-development.SKILL.md
name: lint-rule-development
description: 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.
compatibility: Designed for coding agents working on the Biome codebase (github.com/biomejs/biome).

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:

just new-js-lintrule useMyRuleName

For other languages:

just new-css-lintrule myRuleName
just new-json-lintrule myRuleName
just new-graphql-lintrule myRuleName

This creates a file in `crates/biome_<language>_analyze/src/lint/nursery/use_my_rule_name.rs`

All new lint rules **must** be placed in the `nursery` group, and require a patch changeset. Use the changeset skill to learn more about writing good changesets.

Implement the Rule

Basic rule structure (generated by scaffolding):

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(),
                // Pillar 1 — WHAT the error is.
                markup! {
                    "This identifier "<Emphasis>"prohibited_name"</Emphasis>" is not allowed."
                },
            )
            // Pillar 2 — WHY it is triggered / why it is a problem.
            .note(markup! {
                "Using this identifier leads to [specific problem]."
            })
            // Pillar 3 — WHAT the user should do to fix it.
            // Use a code action instead when an automated fix is possible.
            .note(markup! {
                "Replace it with [alternative] or remove it entirely."
            }),
        )
    }
}

Note: It's critically important to follow the guidelines in the `High Quality Diagnostics` section below when writing diagnostics.

The Three Diagnostic Pillars (REQUIRED)

Every diagnostic **must** follow the three pillars defined in `crates/biome_analyze/CONTRIBUTING.md`:

| Pillar | Question answered | Implemented as | | --- | --- | --- | | 1 | **What** is the error? | The `RuleDiagnostic` message (first argument to `markup!`) | | 2 | **Why** is it a problem? | A `.note()` explaining the consequence or rationale | | 3 | **What should the user do?** | A code action (`action` fn), or a second `.note()` if no fix is available |

**Example from `noUnusedVariables`:**

RuleDiagnostic::new(
    rule_category!(),
    range,
    // Pillar 1: what
    markup! { "This variable "<Emphasis>{name}</Emphasis>" is unused." },
)
// Pillar 2: why
.note(markup! {
    "Unused variables are often the result of typos, incomplete refactors, or other sources of bugs."
})
// Pillar 3: what to do (here as a note; ideally a code action)
.note(markup! {
    "Remove the variable or use it."
})

**Common mistakes to avoid:**

  • Combining pillars 2 and 3 into a single note — keep them separate.
  • Writing pillar 3 as the only note, skipping pillar 2.
  • Writing a pillar 1 message that already contains "why" — the message should stay short and factual; move the rationale to pillar 2.

Using Semantic Model

For rules that need binding analysis:

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:

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().a
Read more
Ships withbiome

A toolchain for web projects, aimed to provide functionalities to maintain them. Biome offers formatter and linter, usable via CLI and LSP.

Get the whole plugin
Stats
25,543
Stars
1,180
Forks
Active
Maintenance
Rust
Language
Apache-2.0
License
1h ago
Last commit
3y ago
Created

Repo: biomejs/biome

Other skills on biome.