AgentSkillsCN

lint-rule-development

在 Biome 的分析器中,循序渐进地创建并实施 lint 规则的指南。适用于实现 noVar、useConst 等内置规则,或自定义各类 lint/辅助规则时使用。示例:<example>用户希望创建一条可检测未使用变量的规则</example><example>用户需为诊断问题添加修复代码操作</example><example>用户正为绑定引用的语义分析进行规则开发</example>

SKILL.md
--- frontmatter
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:<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>

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

Create Snapshot Tests

Create test files in tests/specs/nursery/useMyRuleName/:

code
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<Node> query when you need binding/scope analysis
  • Multiple signals: Return Vec<Self::State> 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<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 Semantic< in existing rules
  • Testing guide: Main CONTRIBUTING.md testing section