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

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, the macro declares:

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. 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) 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 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:

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. 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:

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 and run just gen-rules to update the registry in 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:

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

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, 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. 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →