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