diagnostics-development
Purpose
Use this skill when creating diagnostics - the error messages, warnings, and hints shown to users. Covers the Diagnostic trait, advice types, and best practices for clear, actionable messages.
Prerequisites
- Read
crates/biome_diagnostics/CONTRIBUTING.mdfor concepts - Understand Biome's Technical Principles
- Follow the "show don't tell" philosophy
Diagnostic Principles
- Explain what - State what the error is (diagnostic message)
- Explain why - Explain why it's an error (advice notes)
- Tell how to fix - Provide actionable fixes (code actions, diff advice, command advice)
Follow Technical Principles:
- Informative: Explain, don't just state
- Concise: Short messages, rich context via advices
- Actionable: Always suggest how to fix
- Show don't tell: Prefer code frames over textual explanations
Common Workflows
Create a Diagnostic Type
Use the #[derive(Diagnostic)] macro:
use biome_diagnostics::{Diagnostic, category};
#[derive(Debug, Diagnostic)]
#[diagnostic(
severity = Error,
category = "lint/correctness/noVar"
)]
struct NoVarDiagnostic {
#[location(span)]
span: TextRange,
#[message]
#[description]
message: MessageAndDescription,
#[advice]
advice: NoVarAdvice,
}
#[derive(Debug)]
struct MessageAndDescription;
impl fmt::Display for MessageAndDescription {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Use 'let' or 'const' instead of 'var'")
}
}
Implement Advices
Create advice types that implement Advices trait:
use biome_diagnostics::{Advices, Visit};
use biome_console::markup;
struct NoVarAdvice {
is_const_candidate: bool,
}
impl Advices for NoVarAdvice {
fn record(&self, visitor: &mut dyn Visit) -> std::io::Result<()> {
if self.is_const_candidate {
visitor.record_log(
LogCategory::Info,
&markup! {
"This variable is never reassigned, use 'const' instead."
}
)?;
} else {
visitor.record_log(
LogCategory::Info,
&markup! {
"Variables declared with 'var' are function-scoped, use 'let' for block-scoping."
}
)?;
}
Ok(())
}
}
Use Built-in Advice Types
use biome_diagnostics::{LogAdvice, CodeFrameAdvice, DiffAdvice, CommandAdvice, LogCategory};
// Log advice - simple text message
LogAdvice {
category: LogCategory::Info,
text: markup! { "Consider using arrow functions." },
}
// Code frame advice - highlight code location
// Fields: path (AsResource), span (AsSpan), source_code (AsSourceCode)
CodeFrameAdvice {
path: "file.js",
span: node.text_range(),
source_code: ctx.source_code(),
}
// Diff advice - show a TextEdit diff
DiffAdvice {
diff: text_edit, // must implement AsRef<TextEdit>
}
// Command advice - suggest CLI command
CommandAdvice {
command: "biome check --write",
}
In practice, most lint rules use the RuleDiagnostic builder pattern instead of constructing advice types directly. See the Add Diagnostic to Rule section below.
Add Diagnostic to Rule
use biome_analyze::{Rule, RuleDiagnostic};
impl Rule for NoVar {
fn diagnostic(ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
let node = ctx.query();
Some(
RuleDiagnostic::new(
rule_category!(),
node.range(),
markup! {
"Using "<Emphasis>"var"</Emphasis>" is not recommended."
},
)
.note(markup! {
"Variables declared with "<Emphasis>"var"</Emphasis>" are function-scoped, not block-scoped, which means they can leak outside of loops and conditionals and cause unexpected behavior."
})
.note(markup! {
"Consider using "<Emphasis>"let"</Emphasis>" or "<Emphasis>"const"</Emphasis>" instead."
})
)
}
}
Use Markup for Rich Text
Biome supports rich markup in diagnostic messages:
use biome_console::markup;
markup! {
// Emphasis (bold/colored)
"Use "<Emphasis>"const"</Emphasis>" instead."
// Code/identifiers
"The variable "<Emphasis>{variable_name}</Emphasis>" is never used."
// Hyperlinks
"See the "<Hyperlink href="https://example.com">"documentation"</Hyperlink>"."
// Interpolation
"Found "{count}" issues."
}
Register Diagnostic Category
Add new categories to crates/biome_diagnostics_categories/src/categories.rs:
define_categories! {
// Existing categories...
"lint/correctness/noVar": "https://biomejs.dev/linter/rules/no-var",
"lint/style/useConst": "https://biomejs.dev/linter/rules/use-const",
}
Create Multi-Advice Diagnostics
#[derive(Debug, Diagnostic)]
#[diagnostic(severity = Warning)]
struct ComplexDiagnostic {
#[location(span)]
span: TextRange,
#[message]
message: &'static str,
// Multiple advices
#[advice]
first_advice: LogAdvice<MarkupBuf>,
#[advice]
code_frame: CodeFrameAdvice<String, TextRange, String>,
#[verbose_advice]
verbose_help: LogAdvice<MarkupBuf>,
}
Add Tags to Diagnostics
#[derive(Debug, Diagnostic)]
#[diagnostic(
severity = Warning,
tags(FIXABLE, DEPRECATED_CODE) // Add diagnostic tags
)]
struct MyDiagnostic {
// ...
}
Available tags:
FIXABLE- Diagnostic has fix informationINTERNAL- Internal error in BiomeUNNECESSARY_CODE- Code is unusedDEPRECATED_CODE- Code uses deprecated features
Best Practices
Message Guidelines
Good messages:
// Good - specific and actionable
"Use 'let' or 'const' instead of 'var'"
// Good - explains why
"This variable is never reassigned, consider using 'const'"
// Good - shows what to do
"Remove the unused import statement"
Bad messages:
// Bad - too vague
"Invalid syntax"
// Bad - just states the obvious
"Variable declared with 'var'"
// Bad - no guidance
"This code has a problem"
Advice Guidelines
Show, don't tell:
// Good - shows code frame
CodeFrameAdvice {
path: "file.js",
span: node.text_range(),
source_code: source,
}
// Less helpful - just text
LogAdvice {
category: LogCategory::Info,
text: markup! { "The expression at line 5 is always truthy" },
}
Provide actionable fixes:
// Good - shows exact change
DiffAdvice {
diff: text_edit, // AsRef<TextEdit>
}
// Less helpful - describes change
LogAdvice {
category: LogCategory::Info,
text: markup! { "Change 'var' to 'const'" },
}
Severity Levels
Choose appropriate severity:
// Fatal - Biome can't continue
severity = Fatal
// Error - Must be fixed (correctness, security, a11y)
severity = Error
// Warning - Should be fixed (suspicious code)
severity = Warning
// Information - Style suggestions
severity = Information
// Hint - Minor improvements
severity = Hint
Common Patterns
// Pattern 1: Simple diagnostic with note
RuleDiagnostic::new(
rule_category!(),
node.range(),
markup! { "Main message" },
)
.note(markup! { "Additional context" })
// Pattern 2: Diagnostic with code frame
RuleDiagnostic::new(
rule_category!(),
node.range(),
markup! { "Main message" },
)
.detail(
node.syntax().text_range(),
markup! { "This part is problematic" }
)
// Pattern 3: Diagnostic with link
RuleDiagnostic::new(
rule_category!(),
node.range(),
markup! { "Main message" },
)
.note(markup! {
"See "<Hyperlink href="https://biomejs.dev/linter">"documentation"</Hyperlink>"."
})
// Pattern 4: Conditional advice
impl Advices for MyAdvice {
fn record(&self, visitor: &mut dyn Visit) -> std::io::Result<()> {
if self.show_hint {
visitor.record_log(
LogCategory::Info,
&markup! { "Hint: ..." }
)?;
}
Ok(())
}
}
Tips
- Category format: Use
area/group/ruleNameformat (e.g.,lint/correctness/noVar) - Markup formatting: Use
markup!macro for all user-facing text - Hyperlinks: Always link to documentation for more details
- Code frames: Include for spatial context when helpful
- Multiple advices: Chain multiple pieces of information
- Verbose advices: Use for extra details users can opt into
- Description vs Message: Description for plain text contexts (IDE popover), message for rich display
- Register categories: Don't forget to add to
categories.rs
References
- Full guide:
crates/biome_diagnostics/CONTRIBUTING.md - Technical principles: https://biomejs.dev/internals/philosophy/#technical
- Diagnostic trait:
crates/biome_diagnostics/src/diagnostic.rs - Advice types:
crates/biome_diagnostics/src/advice.rs - Examples: Search for
#[derive(Diagnostic)]in codebase
More from biomejs/biome
biome-developer
General development best practices and common gotchas when working on Biome. Use for avoiding common mistakes, understanding Biome-specific patterns (AST, syntax nodes, string extraction, embedded languages), and learning technical tips.
134parser-development
Guide for implementing parsers with error recovery for new languages in Biome. Use when adding parsing support for a new language, implementing error recovery in a parser, or writing grammar definitions in .ungram format for JavaScript, CSS, JSON, HTML, GraphQL, or other languages.
80lint-rule-development
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, adding code actions to fix diagnostics, implementing semantic analysis for binding references, or adding configurable options to rules.
74formatter-development
Guide for implementing formatting rules using Biome's IR-based formatter infrastructure. Use when implementing formatting for new syntax nodes, handling comments in formatted output, writing or debugging formatter snapshot tests, diagnosing idempotency failures, or comparing Biome's formatting against Prettier for JavaScript, CSS, JSON, HTML, Markdown, or other languages.
71testing-codegen
Guide for testing workflows and code generation commands in Biome. Use when running snapshot tests for lint rules, managing insta snapshots, or regenerating analyzer/parser/formatter code after changes.
69type-inference
Guide for working with Biome's module graph and type inference system. Use when implementing type-aware lint rules, understanding type resolution, working on the module graph infrastructure, or implementing type inference for new features.
67