Guide for creating high-quality, user-friendly diagnostics in Biome. Use when creating diagnostics for lint rules, adding helpful advice to error messages, implementing code frame displays, or improving diagnostic quality.
68
82%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
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.
crates/biome_diagnostics/CONTRIBUTING.md for conceptsFollow Technical Principles:
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'")
}
}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 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.
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."
})
)
}
}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."
}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",
}#[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>,
}#[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 featuresGood 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"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'" },
}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// 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(())
}
}area/group/ruleName format (e.g., lint/correctness/noVar)markup! macro for all user-facing textcategories.rscrates/biome_diagnostics/CONTRIBUTING.mdcrates/biome_diagnostics/src/diagnostic.rscrates/biome_diagnostics/src/advice.rs#[derive(Diagnostic)] in codebasec50a853
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.