---
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. Examples:User wants to create a rule that detects unused variablesUser needs to add code actions to fix diagnostic issuesUser is implementing semantic analysis for binding references
---
## 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:
```shell
just new-js-lintrule useMyRuleName
```
For other languages:
```shell
just new-css-lintrule myRuleName
just new-json-lintrule myRuleName
just new-graphql-lintrule myRuleName
```
This creates a file in `crates/biome_js_analyze/src/lint/nursery/use_my_rule_name.rs`
### Implement 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;
type State = ();
type Signals = Option;
type Options = ();
fn run(ctx: &RuleContext) -> 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, _state: &Self::State) -> Option {
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;
fn run(ctx: &RuleContext) -> 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, _state: &Self::State) -> Option {
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-output
```
### Create 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 configuration
```
Example `invalid.js`:
```javascript
const prohibited_name = 1;
const another_prohibited = 2;
```
Run snapshot tests:
```shell
just test-lintrule useMyRuleName
```
Review snapshots:
```shell
cargo insta review
```
### Generate Analyzer Code
After modifying rules, generate updated boilerplate:
```shell
just gen-analyzer
```
This updates:
- Rule registrations
- Configuration schemas
- Documentation exports
- Type bindings
### Format and Lint
Before committing:
```shell
just f # Format code
just l # Lint code
```
## Tips
- **Rule naming**: Use `no*` prefix for rules that forbid something (e.g., `noVar`), `use*` for rules that mandate something (e.g., `useConst`)
- **Nursery group**: All new rules start in the `nursery` group
- **Semantic queries**: Use `Semantic` query when you need binding/scope analysis
- **Multiple signals**: Return `Vec` or `Box<[Self::State]>` to emit multiple diagnostics
- **Safe vs Unsafe fixes**: Mark fixes as `Unsafe` if they could change program behavior
- **Check for globals**: Always verify if a variable is global before reporting it (use semantic model)
- **Error recovery**: When navigating CST, use `.ok()?` pattern to handle missing nodes gracefully
- **Testing arrays**: Use `.jsonc` files with arrays of code snippets for multiple test cases
## Common Query Types
```rust
// Simple AST query
type Query = Ast;
// Semantic query (needs binding info)
type Query = Semantic;
// Multiple node types (requires declare_node_union!)
declare_node_union! {
pub AnyFunctionLike = AnyJsFunction | JsMethodObjectMember | JsMethodClassMember
}
type Query = Semantic;
```
## References
- Full guide: `crates/biome_analyze/CONTRIBUTING.md`
- Rule examples: `crates/biome_js_analyze/src/lint/`
- Semantic model: Search for `Semantic<` in existing rules
- Testing guide: Main `CONTRIBUTING.md` testing section