rule-options

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Purpose

用途

Use this skill when implementing configurable options for lint rules. Covers defining option types, JSON deserialization, configuration merging, and testing with options.
当你为lint规则实现可配置选项时使用本指南。内容涵盖选项类型定义、JSON反序列化、配置合并以及带选项的测试。

Prerequisites

前置条件

  1. Understand that options should be minimal - only add when needed
  2. Options must follow Technical Philosophy
  3. Rule must be implemented before adding options
  1. 理解选项应尽可能精简——仅在必要时添加
  2. 选项必须遵循技术理念
  3. 必须先实现规则,再添加选项

Common Workflows

常见工作流

Define Rule Options Type

定义规则选项类型

Options live in
biome_rule_options
crate. After running
just gen-analyzer
, a file is created for your rule.
Example for
useThisConvention
rule in
biome_rule_options/src/use_this_convention.rs
:
rust
use biome_deserialize_macros::{Deserializable, Merge};
use serde::{Deserialize, Serialize};

#[derive(Debug, Default, Clone, Serialize, Deserialize, Deserializable)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields, default)]
pub struct UseThisConventionOptions {
    /// What behavior to enforce
    #[serde(skip_serializing_if = "Option::is_none")]
    behavior: Option<Behavior>,
    
    /// Threshold value between 0-255
    #[serde(skip_serializing_if = "Option::is_none")]
    threshold: Option<u8>,
    
    /// Exceptions to the behavior
    #[serde(skip_serializing_if = "Option::is_none")]
    behavior_exceptions: Option<Box<[Box<str>]>>,
}

#[derive(Debug, Default, Clone, Serialize, Deserialize, Deserializable, Merge)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum Behavior {
    #[default]
    A,
    B,
    C,
}
Key points:
  • All fields wrapped in
    Option<_>
    for proper merging
  • Use
    Box<[Box<str>]>
    instead of
    Vec<String>
    (saves memory)
  • #[serde(rename_all = "camelCase")]
    for JavaScript naming
  • #[serde(deny_unknown_fields)]
    to catch typos
  • #[serde(default)]
    makes all fields optional
选项存放在
biome_rule_options
crate中。运行
just gen-analyzer
后,会为你的规则创建一个文件。
biome_rule_options/src/use_this_convention.rs
中的
useThisConvention
规则为例:
rust
use biome_deserialize_macros::{Deserializable, Merge};
use serde::{Deserialize, Serialize};

#[derive(Debug, Default, Clone, Serialize, Deserialize, Deserializable)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields, default)]
pub struct UseThisConventionOptions {
    /// What behavior to enforce
    #[serde(skip_serializing_if = "Option::is_none")]
    behavior: Option<Behavior>,
    
    /// Threshold value between 0-255
    #[serde(skip_serializing_if = "Option::is_none")]
    threshold: Option<u8>,
    
    /// Exceptions to the behavior
    #[serde(skip_serializing_if = "Option::is_none")]
    behavior_exceptions: Option<Box<[Box<str>]>>,
}

#[derive(Debug, Default, Clone, Serialize, Deserialize, Deserializable, Merge)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum Behavior {
    #[default]
    A,
    B,
    C,
}
关键点:
  • 所有字段都用
    Option<_>
    包裹,以支持正确的合并
  • 使用
    Box<[Box<str>]>
    而非
    Vec<String>
    (节省内存)
  • 使用
    #[serde(rename_all = "camelCase")]
    以符合JavaScript命名规范
  • 使用
    #[serde(deny_unknown_fields)]
    捕获拼写错误
  • #[serde(default)]
    让所有字段变为可选

Implement Merge Trait

实现Merge trait

Options from shared config + user config need merging:
rust
impl biome_deserialize::Merge for UseThisConventionOptions {
    fn merge_with(&mut self, other: Self) {
        // `self` = shared config
        // `other` = user config
        
        // For simple values, use helper
        self.behavior.merge_with(other.behavior);
        self.threshold.merge_with(other.threshold);
        
        // For collections, typically reset instead of combine
        if let Some(exceptions) = other.behavior_exceptions {
            self.behavior_exceptions = Some(exceptions);
        }
    }
}
Merge strategies:
  • Simple values (enums, numbers): Use
    merge_with()
    (takes user value if present)
  • Collections: Usually reset to user value, not combine
  • Derive macro: Can use
    #[derive(Merge)]
    for simple cases
来自共享配置与用户配置的选项需要合并:
rust
impl biome_deserialize::Merge for UseThisConventionOptions {
    fn merge_with(&mut self, other: Self) {
        // `self` = shared config
        // `other` = user config
        
        // For simple values, use helper
        self.behavior.merge_with(other.behavior);
        self.threshold.merge_with(other.threshold);
        
        // For collections, typically reset instead of combine
        if let Some(exceptions) = other.behavior_exceptions {
            self.behavior_exceptions = Some(exceptions);
        }
    }
}
合并策略:
  • 简单值(枚举、数字):使用
    merge_with()
    (如果存在用户值则优先使用)
  • 集合:通常重置为用户值,而非合并
  • 派生宏:简单场景可使用
    #[derive(Merge)]

Use Options in Rule

在规则中使用选项

rust
use biome_rule_options::use_this_convention::UseThisConventionOptions;

impl Rule for UseThisConvention {
    type Query = Semantic<JsCallExpression>;
    type State = Fix;
    type Signals = Vec<Self::State>;
    type Options = UseThisConventionOptions;

    fn run(ctx: &RuleContext<Self>) -> Self::Signals {
        // Get options for current location
        let options = ctx.options();
        
        // Access option values (all are Option<T>)
        let behavior = options.behavior.as_ref();
        let threshold = options.threshold.unwrap_or(50);  // default to 50
        
        if let Some(exceptions) = &options.behavior_exceptions {
            if exceptions.iter().any(|ex| ex.as_ref() == name) {
                // Name is in exceptions, skip rule
                return vec![];
            }
        }
        
        // Rule logic using options...
        vec![]
    }
}
Context automatically handles:
  • Configuration file location
  • extends
    inheritance
  • overrides
    for specific files
rust
use biome_rule_options::use_this_convention::UseThisConventionOptions;

impl Rule for UseThisConvention {
    type Query = Semantic<JsCallExpression>;
    type State = Fix;
    type Signals = Vec<Self::State>;
    type Options = UseThisConventionOptions;

    fn run(ctx: &RuleContext<Self>) -> Self::Signals {
        // Get options for current location
        let options = ctx.options();
        
        // Access option values (all are Option<T>)
        let behavior = options.behavior.as_ref();
        let threshold = options.threshold.unwrap_or(50);  // default to 50
        
        if let Some(exceptions) = &options.behavior_exceptions {
            if exceptions.iter().any(|ex| ex.as_ref() == name) {
                // Name is in exceptions, skip rule
                return vec![];
            }
        }
        
        // Rule logic using options...
        vec![]
    }
}
上下文会自动处理:
  • 配置文件位置
  • extends
    继承
  • 针对特定文件的
    overrides

Configure in biome.json

在biome.json中配置

Users configure options like this:
json
{
  "linter": {
    "rules": {
      "nursery": {
        "useThisConvention": {
          "level": "error",
          "options": {
            "behavior": "A",
            "threshold": 30,
            "behaviorExceptions": ["foo", "bar"]
          }
        }
      }
    }
  }
}
用户可按如下方式配置选项:
json
{
  "linter": {
    "rules": {
      "nursery": {
        "useThisConvention": {
          "level": "error",
          "options": {
            "behavior": "A",
            "threshold": 30,
            "behaviorExceptions": ["foo", "bar"]
          }
        }
      }
    }
  }
}

Test with Options

带选项测试

Create
options.json
in test directory:
tests/specs/nursery/useThisConvention/
├── invalid.js
├── valid.js
├── with_behavior_a/
│   ├── options.json
│   ├── invalid.js
│   └── valid.js
└── with_exceptions/
    ├── options.json
    └── valid.js
Example
with_behavior_a/options.json
:
json
{
  "linter": {
    "rules": {
      "nursery": {
        "useThisConvention": {
          "level": "error",
          "options": {
            "behavior": "A",
            "threshold": 10
          }
        }
      }
    }
  }
}
Options apply to all test files in that directory.
在测试目录中创建
options.json
tests/specs/nursery/useThisConvention/
├── invalid.js
├── valid.js
├── with_behavior_a/
│   ├── options.json
│   ├── invalid.js
│   └── valid.js
└── with_exceptions/
    ├── options.json
    └── valid.js
with_behavior_a/options.json
示例:
json
{
  "linter": {
    "rules": {
      "nursery": {
        "useThisConvention": {
          "level": "error",
          "options": {
            "behavior": "A",
            "threshold": 10
          }
        }
      }
    }
  }
}
选项会应用到该目录下的所有测试文件。

Document Options in Rule

在规则中记录选项

Add options documentation to rule's rustdoc:
rust
declare_lint_rule! {
    /// Enforces a specific convention for code organization.
    ///
    /// ## Options
    ///
    /// ### `behavior`
    ///
    /// Specifies which behavior to enforce. Accepted values are:
    /// - `"A"` (default): Enforces behavior A
    /// - `"B"`: Enforces behavior B
    /// - `"C"`: Enforces behavior C
    ///
    /// ### `threshold`
    ///
    /// A number between 0-255 (default: 50). Controls sensitivity of detection.
    ///
    /// ### `behaviorExceptions`
    ///
    /// An array of strings. Names listed here are excluded from the rule.
    ///
    /// ## Examples
    ///
    /// ### With default options
    ///
    /// [examples with default behavior]
    ///
    /// ### With `behavior` set to "B"
    ///
    /// ```json
    /// {
    ///   "useThisConvention": {
    ///     "level": "error",
    ///     "options": {
    ///       "behavior": "B"
    ///     }
    ///   }
    /// }
    /// ```
    ///
    /// [examples with behavior B]
    pub UseThisConvention {
        version: "next",
        name: "useThisConvention",
        language: "js",
        recommended: false,
    }
}
将选项文档添加到规则的rustdoc中:
rust
declare_lint_rule! {
    /// Enforces a specific convention for code organization.
    ///
    /// ## Options
    ///
    /// ### `behavior`
    ///
    /// Specifies which behavior to enforce. Accepted values are:
    /// - `"A"` (default): Enforces behavior A
    /// - `"B"`: Enforces behavior B
    /// - `"C"`: Enforces behavior C
    ///
    /// ### `threshold`
    ///
    /// A number between 0-255 (default: 50). Controls sensitivity of detection.
    ///
    /// ### `behaviorExceptions`
    ///
    /// An array of strings. Names listed here are excluded from the rule.
    ///
    /// ## Examples
    ///
    /// ### With default options
    ///
    /// [examples with default behavior]
    ///
    /// ### With `behavior` set to "B"
    ///
    /// ```json
    /// {
    ///   "useThisConvention": {
    ///     "level": "error",
    ///     "options": {
    ///       "behavior": "B"
    ///     }
    ///   }
    /// }
    /// ```
    ///
    /// [examples with behavior B]
    pub UseThisConvention {
        version: "next",
        name: "useThisConvention",
        language: "js",
        recommended: false,
    }
}

Generate Schema and Bindings

生成Schema与绑定

After implementing options:
shell
just gen-analyzer
This updates:
  • JSON schema in configuration
  • TypeScript bindings
  • Documentation exports
实现选项后运行:
shell
just gen-analyzer
这会更新:
  • 配置中的JSON schema
  • TypeScript绑定
  • 文档导出

Option Design Guidelines

选项设计指南

When to Add Options

何时添加选项

Good reasons:
  • Conflicting style preferences in community
  • Rule has multiple valid interpretations
  • Different behavior needed for different environments
Bad reasons:
  • Making rule "more flexible" without clear use case
  • Avoiding making opinionated decision
  • Working around incomplete implementation
合理理由:
  • 社区存在冲突的风格偏好
  • 规则有多种合理解释
  • 不同环境需要不同行为
不合理理由:
  • 无明确使用场景却想让规则“更灵活”
  • 避免做出有主见的决策
  • 规避不完整的实现

Option Naming

选项命名

rust
// ✅ Good - clear, semantic names
allow_single_line: bool
max_depth: u8
ignore_patterns: Box<[Box<str>]>

// ❌ Bad - unclear, technical names
flag: bool
n: u8
list: Vec<String>
rust
// ✅ Good - clear, semantic names
allow_single_line: bool
max_depth: u8
ignore_patterns: Box<[Box<str>]>

// ❌ Bad - unclear, technical names
flag: bool
n: u8
list: Vec<String>

Option Types

选项类型

rust
// Simple values
enabled: bool
max_count: u8  // or u16, u32
min_length: usize

// Enums for fixed choices
#[derive(Deserializable, Merge)]
enum QuoteStyle {
    Single,
    Double,
    Preserve,
}

// Collections (use boxed slices)
patterns: Box<[Box<str>]>
ignore_names: Box<[Box<str>]>

// Complex nested options
#[derive(Deserializable)]
struct AdvancedOptions {
    mode: Mode,
    exclusions: Box<[Box<str>]>,
}
rust
// Simple values
enabled: bool
max_count: u8  // or u16, u32
min_length: usize

// Enums for fixed choices
#[derive(Deserializable, Merge)]
enum QuoteStyle {
    Single,
    Double,
    Preserve,
}

// Collections (use boxed slices)
patterns: Box<[Box<str>]>
ignore_names: Box<[Box<str>]>

// Complex nested options
#[derive(Deserializable)]
struct AdvancedOptions {
    mode: Mode,
    exclusions: Box<[Box<str>]>,
}

Common Patterns

常见模式

rust
// Pattern 1: Boolean option with default false
#[derive(Default)]
struct MyOptions {
    allow_something: Option<bool>,
}

impl Rule for MyRule {
    fn run(ctx: &RuleContext<Self>) -> Self::Signals {
        let allow = ctx.options().allow_something.unwrap_or(false);
        if allow { return None; }
        // ...
    }
}

// Pattern 2: Enum option with default
#[derive(Default)]
enum Mode {
    #[default]
    Strict,
    Loose,
}

// Pattern 3: Collection option (exclusions)
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
    let options = ctx.options();
    
    if let Some(exclusions) = &options.exclusions {
        if exclusions.iter().any(|ex| matches_name(ex, name)) {
            return None;  // Excluded
        }
    }
    
    // Check rule normally
}

// Pattern 4: Numeric threshold
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
    let threshold = ctx.options().max_depth.unwrap_or(3);
    
    if depth > threshold {
        return Some(());
    }
    
    None
}
rust
// Pattern 1: Boolean option with default false
#[derive(Default)]
struct MyOptions {
    allow_something: Option<bool>,
}

impl Rule for MyRule {
    fn run(ctx: &RuleContext<Self>) -> Self::Signals {
        let allow = ctx.options().allow_something.unwrap_or(false);
        if allow { return None; }
        // ...
    }
}

// Pattern 2: Enum option with default
#[derive(Default)]
enum Mode {
    #[default]
    Strict,
    Loose,
}

// Pattern 3: Collection option (exclusions)
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
    let options = ctx.options();
    
    if let Some(exclusions) = &options.exclusions {
        if exclusions.iter().any(|ex| matches_name(ex, name)) {
            return None;  // Excluded
        }
    }
    
    // Check rule normally
}

// Pattern 4: Numeric threshold
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
    let threshold = ctx.options().max_depth.unwrap_or(3);
    
    if depth > threshold {
        return Some(());
    }
    
    None
}

Tips

提示

  • Minimize options: Only add when truly needed
  • Memory efficiency: Use
    Box<[Box<str>]>
    not
    Vec<String>
    for arrays
  • Optional wrapping: All option fields should be
    Option<T>
    for proper merging
  • Serde attributes: Always use
    rename_all = "camelCase"
    and
    deny_unknown_fields
  • Schema generation: Use
    #[cfg_attr(feature = "schema", derive(JsonSchema))]
  • Default trait: Implement or derive
    Default
    for option types
  • Testing: Test with multiple option combinations
  • Documentation: Document each option with examples
  • Codegen: Run
    just gen-analyzer
    after adding options
  • 最小化选项:仅在真正必要时添加
  • 内存效率:数组使用
    Box<[Box<str>]>
    而非
    Vec<String>
  • 可选包裹:所有选项字段都应为
    Option<T>
    以支持正确合并
  • Serde属性:始终使用
    rename_all = "camelCase"
    deny_unknown_fields
  • Schema生成:使用
    #[cfg_attr(feature = "schema", derive(JsonSchema))]
  • Default trait:为选项类型实现或派生
    Default
  • 测试:测试多种选项组合
  • 文档:为每个选项添加示例文档
  • 代码生成:添加选项后运行
    just gen-analyzer

Configuration Merging Example

配置合并示例

json5
// shared.jsonc (extended configuration)
{
  "linter": {
    "rules": {
      "nursery": {
        "myRule": {
          "options": {
            "behavior": "A",
            "exclusions": ["foo"]
          }
        }
      }
    }
  }
}

// biome.jsonc (user configuration)
{
  "extends": ["./shared.jsonc"],
  "linter": {
    "rules": {
      "nursery": {
        "myRule": {
          "options": {
            "threshold": 30,
            "exclusions": ["bar"]  // Replaces ["foo"], doesn't append
          }
        }
      }
    }
  }
}

// Result after merging:
// behavior: "A" (from shared)
// threshold: 30 (from user)
// exclusions: ["bar"] (user replaces shared)
json5
// shared.jsonc (extended configuration)
{
  "linter": {
    "rules": {
      "nursery": {
        "myRule": {
          "options": {
            "behavior": "A",
            "exclusions": ["foo"]
          }
        }
      }
    }
  }
}

// biome.jsonc (user configuration)
{
  "extends": ["./shared.jsonc"],
  "linter": {
    "rules": {
      "nursery": {
        "myRule": {
          "options": {
            "threshold": 30,
            "exclusions": ["bar"]  // Replaces ["foo"], doesn't append
          }
        }
      }
    }
  }
}

// Result after merging:
// behavior: "A" (from shared)
// threshold: 30 (from user)
// exclusions: ["bar"] (user replaces shared)

References

参考资料

  • Analyzer guide:
    crates/biome_analyze/CONTRIBUTING.md
    § Rule Options
  • Options crate:
    crates/biome_rule_options/
  • Deserialize macros:
    crates/biome_deserialize_macros/
  • Example rules with options: Search for
    type Options =
    in
    biome_*_analyze
    crates
  • 分析器指南:
    crates/biome_analyze/CONTRIBUTING.md
    § Rule Options
  • 选项crate:
    crates/biome_rule_options/
  • 反序列化宏:
    crates/biome_deserialize_macros/
  • 带选项的示例规则:在
    biome_*_analyze
    crates中搜索
    type Options =