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 aRuleDiagnosticcontaining the message, source range, and optional notes.action– (Optional) Generates aRuleActionthat 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:
- The file is parsed into an AST using
biome_js_parser. - If required by the active rules, a
SemanticModelis constructed viabiome_js_semantic. - The
analyze_with_inspect_matcherfunction (exposed incrates/biome_js_analyze/src/lib.rs) creates aRuleRegistry, walks the AST, and invokes each rule'srunmethod. - If
runreturns a state, the engine invokesdiagnosticand optionallyactionto 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
- BiomeJS linting rules are Rust implementations of the
Ruletrait withrun,diagnostic, and optionalactionmethods. - The
declare_lint_rule!macro incrates/biome_js_analyze/src/lint/automatically registers rules with the generated registry atcrates/biome_js_analyze/src/registry.rs. - Rules execute via
analyze_with_inspect_matcherincrates/biome_js_analyze/src/lib.rs, which walks the AST and manages theRuleRegistry. - Fixes are generated through
BatchMutationobjects (defined incrates/biome_js_analyze/src/utils/batch.rs) and categorized asSafeorUnsafe. - Semantic rules access type information through services defined in
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, 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →