---
name: rule-options
description: Guide for implementing configurable options for lint rules and assists. Use when rules need user-configurable behavior. Examples:User wants to add options to a lint ruleUser needs to implement JSON deserialization for rule configUser is testing rule behavior with different options
---
## Purpose
Use this skill when implementing configurable options for lint rules. Covers defining option types, JSON deserialization, configuration merging, and testing with options.
## Prerequisites
1. Understand that options should be minimal - only add when needed
2. Options must follow [Technical Philosophy](https://biomejs.dev/internals/philosophy/#technical)
3. Rule must be implemented before adding options
## 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,
/// Threshold value between 0-255
#[serde(skip_serializing_if = "Option::is_none")]
threshold: Option,
/// Exceptions to the behavior
#[serde(skip_serializing_if = "Option::is_none")]
behavior_exceptions: Option]>>,
}
#[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]>` instead of `Vec` (saves memory)
- `#[serde(rename_all = "camelCase")]` for JavaScript naming
- `#[serde(deny_unknown_fields)]` to catch typos
- `#[serde(default)]` makes all fields optional
### Implement 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
### Use Options in Rule
```rust
use biome_rule_options::use_this_convention::UseThisConventionOptions;
impl Rule for UseThisConvention {
type Query = Semantic;
type State = Fix;
type Signals = Vec;
type Options = UseThisConventionOptions;
fn run(ctx: &RuleContext) -> Self::Signals {
// Get options for current location
let options = ctx.options();
// Access option values (all are Option)
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
### Configure in biome.json
Users configure options like this:
```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.
### 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,
}
}
```
### Generate Schema and Bindings
After implementing options:
```shell
just gen-analyzer
```
This updates:
- JSON schema in configuration
- TypeScript bindings
- Documentation exports
## 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]>
// ❌ Bad - unclear, technical names
flag: bool
n: u8
list: Vec
```
### 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]>
ignore_names: Box<[Box]>
// Complex nested options
#[derive(Deserializable)]
struct AdvancedOptions {
mode: Mode,
exclusions: Box<[Box]>,
}
```
## Common Patterns
```rust
// Pattern 1: Boolean option with default false
#[derive(Default)]
struct MyOptions {
allow_something: Option,
}
impl Rule for MyRule {
fn run(ctx: &RuleContext) -> 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::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::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]>` not `Vec` for arrays
- **Optional wrapping**: All option fields should be `Option` 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
## 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)
```
## 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