/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
$ npx -y skills add biomejs/biome --skill lint-rule-development --agent claude-codeHow 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.mdname: 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().aRead more
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().aA toolchain for web projects, aimed to provide functionalities to maintain them. Biome offers formatter and linter, usable via CLI and LSP.
Repo: biomejs/biome
Other skills on biome.
- /biome-code-review
Static, read-only code review of a Biome (github.com/biomejs/biome) pull request, local branch, commit range, diff, or working tree being prepared as a PR. Applies Biome's own conventions - nursery placement and naming for lint rules, diagnostics quality, allocation and borrow
Open skill - /biome-developer
General development best practices and common gotchas when working on Biome. Use for avoiding common mistakes, understanding Biome-specific patterns (AST, syntax nodes, string extraction, embedded languages), and learning technical tips.
Open skill - /changeset
Guide for creating and writing proper changesets for Biome PRs. Use when a PR introduces user-visible changes (bug fixes, new features, rule changes, formatter changes, parser changes) that need a changeset entry for the CHANGELOG. Trigger when creating changesets, writing
Open skill - /diagnostics-development
Guide for creating high-quality, user-friendly diagnostics in Biome. Use when creating diagnostics for lint rules, adding helpful advice to error messages, implementing code frame displays, or improving diagnostic quality.
Open skill - /doc-comments
How to write inline comments, rustdoc, and module documentation in the Biome codebase. The audience is Biome developers reading the source, not end users. Use whenever writing or editing `//` comments, `///` item docs, or `//!` module docs — including comments added incidentally
Open skill - /eslint-migrate-options
Guide for implementing ESLint-to-Biome rule option migrators inside `biome migrate eslint`. Use whenever you add or update a Biome lint rule that has an ESLint source rule with configurable options, need to deserialize plugin-specific ESLint options, or need custom migration
Open skill

