lint-rule-development
Original:🇺🇸 English
Translated
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. Examples:<example>User wants to create a rule that detects unused variables</example><example>User needs to add code actions to fix diagnostic issues</example><example>User is implementing semantic analysis for binding references</example>
2installs
Sourcebiomejs/biome
Added on
NPX Install
npx skill4agent add biomejs/biome lint-rule-developmentTags
Translated version includes tags in frontmatterSKILL.md Content
View Translation Comparison →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
- Install required tools:
just install-tools - Ensure ,
cargo, andjustare availablepnpm - Read for in-depth concepts
crates/biome_analyze/CONTRIBUTING.md
Common Workflows
Create a New Lint Rule
Generate scaffolding for a JavaScript lint rule:
shell
just new-js-lintrule useMyRuleNameFor other languages:
shell
just new-css-lintrule myRuleName
just new-json-lintrule myRuleName
just new-graphql-lintrule myRuleNameThis creates a file in
crates/biome_js_analyze/src/lint/nursery/use_my_rule_name.rsImplement the Rule
Basic rule structure (generated by scaffolding):
rust
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(),
markup! {
"Avoid using this identifier."
},
)
.note(markup! {
"This identifier is prohibited because..."
}),
)
}
}Using Semantic Model
For rules that need binding analysis:
rust
use biome_analyze::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:
rust
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.action_category(ctx.category(), ctx.group()),
ctx.metadata().applicability(),
markup! { "Use 'replacement' instead" }.to_owned(),
mutation,
))
}
}Quick Testing
Use the quick test for rapid iteration:
rust
// In crates/biome_js_analyze/tests/quick_test.rs
// Uncomment #[ignore] and modify:
const SOURCE: &str = r#"
const prohibited_name = 1;
"#;
let rule_filter = RuleFilter::Rule("nursery", "useMyRuleName");Run the test:
shell
cd crates/biome_js_analyze
cargo test quick_test -- --show-outputCreate Snapshot Tests
Create test files in :
tests/specs/nursery/useMyRuleName/tests/specs/nursery/useMyRuleName/
├── invalid.js # Code that triggers the rule
├── valid.js # Code that doesn't trigger the rule
└── options.json # Optional rule configurationExample :
invalid.jsjavascript
const prohibited_name = 1;
const another_prohibited = 2;Run snapshot tests:
shell
just test-lintrule useMyRuleNameReview snapshots:
shell
cargo insta reviewGenerate Analyzer Code
After modifying rules, generate updated boilerplate:
shell
just gen-analyzerThis updates:
- Rule registrations
- Configuration schemas
- Documentation exports
- Type bindings
Format and Lint
Before committing:
shell
just f # Format code
just l # Lint codeTips
- Rule naming: Use prefix for rules that forbid something (e.g.,
no*),noVarfor rules that mandate something (e.g.,use*)useConst - Nursery group: All new rules start in the group
nursery - Semantic queries: Use query when you need binding/scope analysis
Semantic<Node> - Multiple signals: Return or
Vec<Self::State>to emit multiple diagnosticsBox<[Self::State]> - Safe vs Unsafe fixes: Mark fixes as if they could change program behavior
Unsafe - Check for globals: Always verify if a variable is global before reporting it (use semantic model)
- Error recovery: When navigating CST, use pattern to handle missing nodes gracefully
.ok()? - Testing arrays: Use files with arrays of code snippets for multiple test cases
.jsonc
Common Query Types
rust
// Simple AST query
type Query = Ast<JsVariableDeclaration>;
// Semantic query (needs binding info)
type Query = Semantic<JsReferenceIdentifier>;
// Multiple node types (requires declare_node_union!)
declare_node_union! {
pub AnyFunctionLike = AnyJsFunction | JsMethodObjectMember | JsMethodClassMember
}
type Query = Semantic<AnyFunctionLike>;References
- Full guide:
crates/biome_analyze/CONTRIBUTING.md - Rule examples:
crates/biome_js_analyze/src/lint/ - Semantic model: Search for in existing rules
Semantic< - Testing guide: Main testing section
CONTRIBUTING.md