# BiomeJS Linting Rules: How the Analyzer Works and How to Create Custom Rules

> Discover how BiomeJS linting rules function and learn to build your own custom rules. Understand the analyzer's mechanics and Rust trait implementations for effective code quality.

- Repository: [Biome/biome](https://github.com/biomejs/biome)
- Tags: internals
- Published: 2026-06-19

---

**BiomeJS linting rules are Rust trait implementations that define `run`, `diagnostic`, and optional `action` methods, registered automatically via the `declare_lint_rule!` macro in the `biome_js_analyze` crate.**

The `biomejs/biome` repository implements a high-performance JavaScript and TypeScript linter where each rule is a Rust struct adhering to the `Rule` trait. Understanding **BiomeJS linting rules** requires examining how the analysis engine matches AST nodes, generates diagnostics, and applies automatic fixes through the `biome_js_analyze` crate. This architecture enables everything from simple syntax bans to complex semantic checks powered by type information.

## How BiomeJS Linting Rules Are Structured

Every lint rule in Biome is defined by implementing the `Rule` trait with three core methods that handle detection, reporting, and fixing.

### The Rule Trait Interface

Each rule provides three essential methods that the analysis engine invokes:

- **`run`** – Performs a fast match on the AST or semantic model to determine if the rule should emit a diagnostic.
- **`diagnostic`** – Constructs a `RuleDiagnostic` containing the message, source range, and optional notes.
- **`action`** – (Optional) Generates a `RuleAction` that can automatically fix the problem, such as replacing or removing a token.

The trait implementation uses associated types to specify the query type (e.g., `Ast<JsDebuggerStatement>`) and state, allowing the engine to perform type-safe AST traversals.

### Metadata Declaration with declare_lint_rule!

Rules begin with the `declare_lint_rule!` macro, which records metadata including the rule name, version, ESLint source mapping, severity, and fix kind. For example, in [`crates/biome_js_analyze/src/lint/suspicious/no_debugger.rs`](https://github.com/biomejs/biome/blob/main/crates/biome_js_analyze/src/lint/suspicious/no_debugger.rs), the macro declares:

```rust
declare_lint_rule! {
    pub NoDebugger {
        version: "1.0.0",
        name: "noDebugger",
        sources: &[RuleSource::Eslint("no-debugger")],
        fix_kind: FixKind::Unsafe,
        // ...
    }
}

```

This macro automatically registers the rule with the global registry, eliminating the need for manual entry in configuration files.

## Rule Registration and the Analysis Engine

The linting infrastructure centers on a generated registry that collects all rules and orchestrates their execution against source files.

### The Registry System

All rules are registered through the generated `visit_registry` function in [`crates/biome_js_analyze/src/registry.rs`](https://github.com/biomejs/biome/blob/main/crates/biome_js_analyze/src/registry.rs). This function populates the `MetadataRegistry` with three top-level categories: **assist**, **lint**, and **syntax**. Each rule is added to the `Lint` category via the macro expansion within its source file. According to the `biomejs/biome` source code, the registry builder processes the `AnalysisFilter` supplied by the CLI to determine which rules to enable.

### The Analysis Pipeline

When the Biome CLI executes `biome lint file.js`, the following occurs:

1. The file is parsed into an AST using `biome_js_parser`.
2. If required by the active rules, a `SemanticModel` is constructed via `biome_js_semantic`.
3. The `analyze_with_inspect_matcher` function (exposed in [`crates/biome_js_analyze/src/lib.rs`](https://github.com/biomejs/biome/blob/main/crates/biome_js_analyze/src/lib.rs)) creates a `RuleRegistry`, walks the AST, and invokes each rule's `run` method.
4. If `run` returns a state, the engine invokes `diagnostic` and optionally `action` to produce the final output.

The `JsAnalyzerServices` struct provides access to semantic information and utility helpers during this process.

## Built-in Rule Categories and Examples

Biome organizes rules into categories such as `suspicious`, `style`, and `correctness`, each housed in dedicated directories under `crates/biome_js_analyze/src/lint/`.

### Suspicious Rules (noDebugger)

The `noDebugger` rule in [`crates/biome_js_analyze/src/lint/suspicious/no_debugger.rs`](https://github.com/biomejs/biome/blob/main/crates/biome_js_analyze/src/lint/suspicious/no_debugger.rs) demonstrates the complete implementation pattern. Its `run` method returns `Some(())` for any `JsDebuggerStatement` node, while the `action` method creates a `JsRuleAction` that removes the statement:

```rust
fn action(ctx: &RuleContext<Self>, _: &Self::State) -> Option<JsRuleAction> {
    let mut mutation = BatchMutation::new(ctx.root().into());
    mutation.remove_statement(ctx.query().clone().into());
    Some(JsRuleAction::new(
        ActionCategory::QuickFix,
        ctx.metadata().action_category(ctx.category(), ctx.rule_groups(), ctx.rule_name()),
        markup! { "Remove debugger statement" },
        mutation,
    ))
}

```

Because `fix_kind` is set to `FixKind::Unsafe`, the CLI marks the fix as potentially unsafe, requiring explicit user approval.

### Style Rules and Semantic Services

Rules requiring type information utilize the semantic services defined in [`crates/biome_js_analyze/src/services/semantic.rs`](https://github.com/biomejs/biome/blob/main/crates/biome_js_analyze/src/services/semantic.rs). The `Semantic` wrapper enables rules like `useConst` to query binding information and determine if variables are reassigned. Style rules reside in `crates/biome_js_analyze/src/lint/style/` and include transformations like `useTrimStartEnd` and `useObjectSpread`.

## Creating a Custom BiomeJS Linting Rule

Developers can extend Biome by implementing the `Rule` trait in a new file under the `lint/` directory. Below is a complete example that disallows the identifier `foo`:

```rust
use biome_analyze::{
    Ast, FixKind, Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule,
};
use biome_console::markup;
use biome_diagnostics::Severity;
use biome_js_syntax::JsIdentifier;

declare_lint_rule! {
    /// Disallow the identifier `foo`
    pub NoFoo {
        version: "1.0.0",
        name: "noFoo",
        language: "js",
        sources: &[RuleSource::Eslint("no-foo").same()],
        recommended: false,
        severity: Severity::Error,
        fix_kind: FixKind::Safe,
    }
}

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

    fn run(ctx: &RuleContext<Self>) -> Self::Signals {
        let ident = ctx.query();
        if ident.text() == "foo" { Some(()) } else { None }
    }

    fn diagnostic(ctx: &RuleContext<Self>, _: &Self::State) -> Option<RuleDiagnostic> {
        Some(RuleDiagnostic::new(
            rule_category!(),
            ctx.query().syntax().text_trimmed_range(),
            markup!("`foo` is prohibited")
        ))
    }
}

```

Place this file under [`crates/biome_js_analyze/src/lint/custom/no_foo.rs`](https://github.com/biomejs/biome/blob/main/crates/biome_js_analyze/src/lint/custom/no_foo.rs) and run `just gen-rules` to update the registry in [`crates/biome_js_analyze/src/registry.rs`](https://github.com/biomejs/biome/blob/main/crates/biome_js_analyze/src/registry.rs).

## Running the Linter Programmatically

You can invoke the analyzer from Rust code using the `analyze_with_inspect_matcher` function:

```rust
use biome_analyze::{AnalyzerOptions, AnalyzerPluginSlice, AnalysisFilter};
use biome_js_analyze::{analyze_with_inspect_matcher, JsAnalyzerServices};
use biome_js_parser::parse;
use biome_js_syntax::JsFileSource;

fn lint_source(source: &str) {
    // Parse the source into a JsLanguage root
    let parse = parse(source, JsFileSource::default());
    let root = parse.syntax();

    // Build analyzer options (empty for default)
    let options = AnalyzerOptions::default();
    let filter = AnalysisFilter::default();

    // Run the analysis, printing each diagnostic
    analyze_with_inspect_matcher(
        root,
        filter,
        |_| {}, // no custom matcher inspection
        &options,
        AnalyzerPluginSlice::empty(),
        JsAnalyzerServices::default(),
        |signal| {
            if let Some(diagnostic) = signal.as_diagnostic() {
                println!("⚠️ {}", diagnostic);
            }
            biome_analyze::ControlFlow::Continue
        },
    );
}

```

Calling `lint_source("debugger;")` executes the same analysis pipeline as the CLI, printing diagnostics for any matched rules.

## Summary

- **BiomeJS linting rules** are Rust implementations of the `Rule` trait with `run`, `diagnostic`, and optional `action` methods.
- The `declare_lint_rule!` macro in `crates/biome_js_analyze/src/lint/` automatically registers rules with the generated registry at [`crates/biome_js_analyze/src/registry.rs`](https://github.com/biomejs/biome/blob/main/crates/biome_js_analyze/src/registry.rs).
- Rules execute via `analyze_with_inspect_matcher` in [`crates/biome_js_analyze/src/lib.rs`](https://github.com/biomejs/biome/blob/main/crates/biome_js_analyze/src/lib.rs), which walks the AST and manages the `RuleRegistry`.
- **Fixes** are generated through `BatchMutation` objects (defined in [`crates/biome_js_analyze/src/utils/batch.rs`](https://github.com/biomejs/biome/blob/main/crates/biome_js_analyze/src/utils/batch.rs)) and categorized as `Safe` or `Unsafe`.
- **Semantic rules** access type information through services defined in [`crates/biome_js_analyze/src/services/semantic.rs`](https://github.com/biomejs/biome/blob/main/crates/biome_js_analyze/src/services/semantic.rs).

## Frequently Asked Questions

### How are BiomeJS linting rules registered automatically?

The `declare_lint_rule!` macro expands to register the rule with the global `MetadataRegistry` when the crate compiles. This generated registration flows into `visit_registry` in [`crates/biome_js_analyze/src/registry.rs`](https://github.com/biomejs/biome/blob/main/crates/biome_js_analyze/src/registry.rs), which populates the `Lint` category without requiring manual entry in configuration files.

### What is the difference between FixKind::Safe and FixKind::Unsafe?

`FixKind::Safe` indicates that applying the fix will not change the program's semantics, allowing the CLI to apply it automatically. `FixKind::Unsafe` (used by rules like `noDebugger`) signifies that the fix might alter behavior, requiring explicit user approval via the `--apply-unsafe` flag.

### Can BiomeJS linting rules access type information?

Yes, rules that need type information can utilize the `Semantic` service provided in [`crates/biome_js_analyze/src/services/semantic.rs`](https://github.com/biomejs/biome/blob/main/crates/biome_js_analyze/src/services/semantic.rs). This enables the `SemanticModel` to be queried for binding information, type facts, and scope data during the `run` phase.

### How do I run a specific lint rule in Biome?

The CLI accepts an `--only` flag to run specific rules, or you can construct an `AnalysisFilter` programmatically when using `analyze_with_inspect_matcher`. The filter accepts rule names and categories, allowing precise control over which rules execute during the analysis pass.