use std::fmt::Write;
use crate::file::SourceMap;
use crate::span::{BytePos, FileSpan};
use crate::style;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[non_exhaustive]
pub enum Severity {
Error,
Warning,
Note,
Hint,
}
impl Severity {
pub fn label(self) -> &'static str {
match self {
Severity::Error => "error",
Severity::Warning => "warning",
Severity::Note => "note",
Severity::Hint => "hint",
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum DiagnosticCategory {
Lex,
Parse,
Name,
Type,
Input,
Runtime,
}
impl DiagnosticCategory {
pub fn prefix(self) -> char {
match self {
DiagnosticCategory::Lex => 'T',
DiagnosticCategory::Parse => 'P',
DiagnosticCategory::Name => 'N',
DiagnosticCategory::Type => 'Y',
DiagnosticCategory::Input => 'I',
DiagnosticCategory::Runtime => 'R',
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct DiagnosticCode {
category: DiagnosticCategory,
number: u32,
}
impl DiagnosticCode {
#[inline]
pub(crate) const fn new(category: DiagnosticCategory, number: u32) -> DiagnosticCode {
DiagnosticCode { category, number }
}
#[inline]
pub const fn category(self) -> DiagnosticCategory {
self.category
}
#[inline]
pub const fn number(self) -> u32 {
self.number
}
}
impl std::fmt::Display for DiagnosticCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let n = self.number;
if n < 1000 {
write!(f, "{}{:03}", self.category.prefix(), n)
} else {
write!(f, "{}{}", self.category.prefix(), n)
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum DiagCode {
UnterminatedBlockComment,
UnterminatedTemplate,
UnexpectedCharacter,
UnterminatedTextLiteral,
InvalidEscape,
UnterminatedCharLiteral,
CharLiteralIsNotOneCharacter,
UnexpectedToken,
ExpectedStatementSeparator,
InternalNotASourceFile,
UnknownName,
UnknownType,
NameIsNotAType,
DuplicateDeclaration,
NestedFunction,
RecursiveTypeDeclaration,
FunctionReadsOuterBinding,
NotARecordLiteralHead,
RetiredKeyword,
TypeMismatch,
InfiniteType,
AnnotationConflict,
NotEquatable,
NotIterable,
NotOrderable,
WrongTypeArgumentCount,
DuplicateMember,
CompoundAssignNonNumeric,
ReturnOutsideFunction,
BreakOutsideLoop,
IntLiteralOutOfRange,
NotHashable,
NotNumeric,
OperatorNotDefined,
ValueBreakOutsideLoopExpression,
GenericFunctionAsValue,
NoTupleElement,
NotIndexable,
NotAnAssignmentTarget,
NameHasNoFunctionValue,
ParserTemplateOutsideRead,
CallArityMismatch,
InternalMissingType,
NoMethodOnType,
NoFieldOnType,
MissingRecordFields,
UnknownRecordField,
DuplicateRecordField,
NonExhaustiveMatch,
UnreachableArm,
UnknownEnumVariant,
NotAPatternForType,
RefutableBinding,
PayloadArityMismatch,
MalformedParserExpression,
ParserConversion,
UnknownAtomic,
InvalidCaptureName,
UnknownCaptureKind,
UnknownConstructor,
InvalidConstructorArgument,
MixedCaptureNaming,
DuplicateCaptureName,
ConstructorArity,
EmptySeparator,
DuplicateSectionField,
EmptyFieldList,
UnnamedScalarBlockItem,
DuplicateChoiceCase,
MisplacedRepeatedTail,
TemplateScan,
}
impl DiagCode {
#[must_use]
pub const fn code(self) -> DiagnosticCode {
use DiagCode::*;
use DiagnosticCategory::{Input, Lex, Name, Parse, Type};
match self {
UnterminatedBlockComment => DiagnosticCode::new(Lex, 1),
UnterminatedTemplate => DiagnosticCode::new(Lex, 2),
UnexpectedCharacter => DiagnosticCode::new(Lex, 3),
UnterminatedTextLiteral => DiagnosticCode::new(Lex, 4),
InvalidEscape => DiagnosticCode::new(Lex, 5),
UnterminatedCharLiteral => DiagnosticCode::new(Lex, 6),
CharLiteralIsNotOneCharacter => DiagnosticCode::new(Lex, 7),
UnexpectedToken => DiagnosticCode::new(Parse, 1),
ExpectedStatementSeparator => DiagnosticCode::new(Parse, 2),
InternalNotASourceFile => DiagnosticCode::new(Name, 0),
UnknownName => DiagnosticCode::new(Name, 1),
UnknownType => DiagnosticCode::new(Name, 2),
NameIsNotAType => DiagnosticCode::new(Name, 3),
DuplicateDeclaration => DiagnosticCode::new(Name, 4),
NestedFunction => DiagnosticCode::new(Name, 5),
RecursiveTypeDeclaration => DiagnosticCode::new(Name, 6),
FunctionReadsOuterBinding => DiagnosticCode::new(Name, 7),
NotARecordLiteralHead => DiagnosticCode::new(Name, 8),
RetiredKeyword => DiagnosticCode::new(Name, 9),
TypeMismatch => DiagnosticCode::new(Type, 1),
InfiniteType => DiagnosticCode::new(Type, 2),
AnnotationConflict => DiagnosticCode::new(Type, 3),
NotEquatable => DiagnosticCode::new(Type, 4),
NotIterable => DiagnosticCode::new(Type, 5),
NotOrderable => DiagnosticCode::new(Type, 6),
WrongTypeArgumentCount => DiagnosticCode::new(Type, 7),
DuplicateMember => DiagnosticCode::new(Type, 8),
CompoundAssignNonNumeric => DiagnosticCode::new(Type, 10),
ReturnOutsideFunction => DiagnosticCode::new(Type, 11),
BreakOutsideLoop => DiagnosticCode::new(Type, 12),
IntLiteralOutOfRange => DiagnosticCode::new(Type, 13),
NotHashable => DiagnosticCode::new(Type, 14),
NotNumeric => DiagnosticCode::new(Type, 15),
OperatorNotDefined => DiagnosticCode::new(Type, 16),
ValueBreakOutsideLoopExpression => DiagnosticCode::new(Type, 17),
GenericFunctionAsValue => DiagnosticCode::new(Type, 18),
NoTupleElement => DiagnosticCode::new(Type, 19),
NotIndexable => DiagnosticCode::new(Type, 20),
NotAnAssignmentTarget => DiagnosticCode::new(Type, 21),
NameHasNoFunctionValue => DiagnosticCode::new(Type, 22),
ParserTemplateOutsideRead => DiagnosticCode::new(Type, 23),
CallArityMismatch => DiagnosticCode::new(Type, 24),
InternalMissingType => DiagnosticCode::new(Type, 99),
NoMethodOnType => DiagnosticCode::new(Type, 110),
NoFieldOnType => DiagnosticCode::new(Type, 112),
MissingRecordFields => DiagnosticCode::new(Type, 113),
UnknownRecordField => DiagnosticCode::new(Type, 114),
DuplicateRecordField => DiagnosticCode::new(Type, 115),
NonExhaustiveMatch => DiagnosticCode::new(Type, 120),
UnreachableArm => DiagnosticCode::new(Type, 121),
UnknownEnumVariant => DiagnosticCode::new(Type, 122),
NotAPatternForType => DiagnosticCode::new(Type, 123),
PayloadArityMismatch => DiagnosticCode::new(Type, 124),
RefutableBinding => DiagnosticCode::new(Type, 125),
MalformedParserExpression => DiagnosticCode::new(Input, 0),
ParserConversion => DiagnosticCode::new(Input, 1),
UnknownAtomic => DiagnosticCode::new(Input, 10),
InvalidCaptureName => DiagnosticCode::new(Input, 11),
UnknownCaptureKind => DiagnosticCode::new(Input, 12),
UnknownConstructor => DiagnosticCode::new(Input, 13),
InvalidConstructorArgument => DiagnosticCode::new(Input, 14),
MixedCaptureNaming => DiagnosticCode::new(Input, 20),
DuplicateCaptureName => DiagnosticCode::new(Input, 21),
ConstructorArity => DiagnosticCode::new(Input, 22),
EmptySeparator => DiagnosticCode::new(Input, 23),
DuplicateSectionField => DiagnosticCode::new(Input, 24),
EmptyFieldList => DiagnosticCode::new(Input, 25),
UnnamedScalarBlockItem => DiagnosticCode::new(Input, 26),
DuplicateChoiceCase => DiagnosticCode::new(Input, 27),
MisplacedRepeatedTail => DiagnosticCode::new(Input, 28),
TemplateScan => DiagnosticCode::new(Input, 30),
}
}
pub const ALL: &'static [DiagCode] = {
use DiagCode::*;
&[
UnterminatedBlockComment,
UnterminatedTemplate,
UnexpectedCharacter,
UnterminatedTextLiteral,
InvalidEscape,
UnterminatedCharLiteral,
CharLiteralIsNotOneCharacter,
UnexpectedToken,
ExpectedStatementSeparator,
InternalNotASourceFile,
UnknownName,
UnknownType,
NameIsNotAType,
DuplicateDeclaration,
NestedFunction,
RecursiveTypeDeclaration,
FunctionReadsOuterBinding,
NotARecordLiteralHead,
RetiredKeyword,
TypeMismatch,
InfiniteType,
AnnotationConflict,
NotEquatable,
NotIterable,
NotOrderable,
WrongTypeArgumentCount,
DuplicateMember,
CompoundAssignNonNumeric,
ReturnOutsideFunction,
BreakOutsideLoop,
IntLiteralOutOfRange,
NotHashable,
NotNumeric,
OperatorNotDefined,
ValueBreakOutsideLoopExpression,
GenericFunctionAsValue,
NoTupleElement,
NotIndexable,
NotAnAssignmentTarget,
NameHasNoFunctionValue,
ParserTemplateOutsideRead,
CallArityMismatch,
InternalMissingType,
NoMethodOnType,
NoFieldOnType,
MissingRecordFields,
UnknownRecordField,
DuplicateRecordField,
NonExhaustiveMatch,
UnreachableArm,
UnknownEnumVariant,
NotAPatternForType,
PayloadArityMismatch,
RefutableBinding,
MalformedParserExpression,
ParserConversion,
UnknownAtomic,
InvalidCaptureName,
UnknownCaptureKind,
UnknownConstructor,
InvalidConstructorArgument,
MixedCaptureNaming,
DuplicateCaptureName,
ConstructorArity,
EmptySeparator,
DuplicateSectionField,
EmptyFieldList,
UnnamedScalarBlockItem,
DuplicateChoiceCase,
MisplacedRepeatedTail,
TemplateScan,
]
};
}
impl std::fmt::Display for DiagCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.code().fmt(f)
}
}
#[derive(Clone, Debug)]
pub struct DiagnosticNote {
pub span: FileSpan,
pub message: String,
}
#[derive(Clone, Debug)]
pub struct Suggestion {
pub span: FileSpan,
pub replacement: Option<String>,
pub label: String,
}
#[derive(Clone, Debug)]
pub struct Diagnostic {
severity: Severity,
code: DiagCode,
message: String,
primary: FileSpan,
notes: Vec<DiagnosticNote>,
suggestions: Vec<Suggestion>,
}
impl Diagnostic {
#[inline]
pub fn new(
severity: Severity,
code: DiagCode,
message: impl Into<String>,
primary: FileSpan,
) -> Diagnostic {
Diagnostic {
severity,
code,
message: message.into(),
primary,
notes: Vec::new(),
suggestions: Vec::new(),
}
}
#[inline]
pub fn build(
severity: Severity,
code: DiagCode,
message: impl Into<String>,
primary: FileSpan,
) -> DiagnosticBuilder {
DiagnosticBuilder {
diag: Diagnostic::new(severity, code, message, primary),
}
}
#[inline]
pub fn severity(&self) -> Severity {
self.severity
}
#[inline]
pub fn code(&self) -> DiagnosticCode {
self.code.code()
}
#[inline]
pub fn kind(&self) -> DiagCode {
self.code
}
#[inline]
pub fn message(&self) -> &str {
&self.message
}
#[inline]
pub fn primary(&self) -> FileSpan {
self.primary
}
#[inline]
pub fn sort_key(&self) -> (BytePos, BytePos) {
(self.primary.span.start(), self.primary.span.end())
}
#[inline]
pub fn notes(&self) -> &[DiagnosticNote] {
&self.notes
}
#[inline]
pub fn suggestions(&self) -> &[Suggestion] {
&self.suggestions
}
#[must_use]
pub fn with_note(mut self, span: FileSpan, message: impl Into<String>) -> Diagnostic {
self.notes.push(DiagnosticNote {
span,
message: message.into(),
});
self
}
#[must_use]
pub fn with_suggestion(
mut self,
span: FileSpan,
replacement: impl Into<String>,
label: impl Into<String>,
) -> Diagnostic {
self.suggestions.push(Suggestion {
span,
replacement: Some(replacement.into()),
label: label.into(),
});
self
}
#[must_use]
pub fn with_did_you_mean(self, at: FileSpan, near: impl Into<String>) -> Diagnostic {
let near = near.into();
let label = format!("did you mean `{near}`?");
self.with_suggestion(at, near, label)
}
}
pub struct DiagnosticBuilder {
diag: Diagnostic,
}
impl DiagnosticBuilder {
pub fn note(mut self, span: FileSpan, message: impl Into<String>) -> Self {
self.diag.notes.push(DiagnosticNote {
span,
message: message.into(),
});
self
}
pub fn suggestion(
mut self,
span: FileSpan,
replacement: impl Into<String>,
label: impl Into<String>,
) -> Self {
self.diag.suggestions.push(Suggestion {
span,
replacement: Some(replacement.into()),
label: label.into(),
});
self
}
pub fn help(mut self, span: FileSpan, label: impl Into<String>) -> Self {
self.diag.suggestions.push(Suggestion {
span,
replacement: None,
label: label.into(),
});
self
}
#[inline]
pub fn finish(self) -> Diagnostic {
self.diag
}
}
pub fn sort_by_position(diags: &mut [Diagnostic]) {
diags.sort_by_key(Diagnostic::sort_key);
}
pub struct Renderer<'a> {
source: &'a SourceMap,
palette: style::Palette,
}
impl<'a> Renderer<'a> {
pub fn new(source: &'a SourceMap) -> Renderer<'a> {
Renderer {
source,
palette: style::Palette::plain(),
}
}
pub fn new_styled(source: &'a SourceMap, palette: style::Palette) -> Renderer<'a> {
Renderer { source, palette }
}
fn style_severity(sev: Severity) -> style::Severity {
match sev {
Severity::Error => style::Severity::Error,
Severity::Warning => style::Severity::Warning,
Severity::Note => style::Severity::Note,
Severity::Hint => style::Severity::Help,
}
}
pub fn render(&self, diag: &Diagnostic, out: &mut String) {
self.render_header(diag, out);
out.push('\n');
self.render_location_and_snippet(
diag.primary,
Some(diag.message.as_str()),
diag.severity,
out,
);
for note in &diag.notes {
out.push('\n');
let label = self
.palette
.paint(style::Style::Severity(style::Severity::Note), "note:");
let _ = writeln!(out, "{label} {}", note.message);
self.render_location_and_snippet(note.span, None, Severity::Note, out);
}
for sugg in &diag.suggestions {
out.push('\n');
let label = self
.palette
.paint(style::Style::Severity(style::Severity::Help), "help:");
let _ = writeln!(out, "{label} {}", sugg.label);
if let Some(repl) = &sugg.replacement {
for line in repl.trim_start_matches('\n').lines() {
let _ = writeln!(out, " {line}");
}
}
}
}
fn render_header(&self, diag: &Diagnostic, out: &mut String) {
let sev = Self::style_severity(diag.severity);
let label = self
.palette
.paint(style::Style::Severity(sev), diag.severity.label());
let code = self
.palette
.paint(style::Style::Code, &format!("[{}]", diag.code));
let _ = write!(out, "{label}{code}: {}", diag.message);
}
fn render_location_and_snippet(
&self,
span: FileSpan,
label: Option<&str>,
sev: Severity,
out: &mut String,
) {
let Some(file) = self.source.get(span.file) else {
let _ = writeln!(out, " <unknown file> [{:?}]", span);
return;
};
let caret_label = match label {
Some(s) if !s.is_empty() => crate::snippet::CaretLabel::Labelled(s),
_ => crate::snippet::CaretLabel::Plain,
};
crate::snippet::render_span_snippet_styled(
&file,
span,
caret_label,
out,
crate::snippet::MAX_SNIPPET_LINES,
&self.palette,
Some(Self::style_severity(sev)),
);
}
}
pub fn render_one(source: &SourceMap, diag: &Diagnostic) -> String {
let mut out = String::new();
Renderer::new(source).render(diag, &mut out);
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::file::FileId;
use crate::span::Span;
fn span(file: FileId, start: u32, end: u32) -> FileSpan {
FileSpan::new(file, Span::new(start, end))
}
#[test]
fn code_renders_zero_padded() {
let code = DiagnosticCode::new(DiagnosticCategory::Lex, 12);
assert_eq!(code.to_string(), "T012");
}
#[test]
fn every_code_is_distinct() {
let mut seen = std::collections::HashMap::new();
for &code in DiagCode::ALL {
if let Some(other) = seen.insert(code.to_string(), code) {
panic!("{other:?} and {code:?} both render {code}");
}
}
}
#[test]
fn all_lists_every_variant() {
use DiagCode::*;
let unique: std::collections::HashSet<_> = DiagCode::ALL.iter().collect();
assert_eq!(
unique.len(),
DiagCode::ALL.len(),
"a variant is listed twice"
);
for &code in DiagCode::ALL {
match code {
UnterminatedBlockComment
| UnterminatedTemplate
| UnexpectedCharacter
| UnterminatedTextLiteral
| InvalidEscape
| UnterminatedCharLiteral
| CharLiteralIsNotOneCharacter
| UnexpectedToken
| ExpectedStatementSeparator
| InternalNotASourceFile
| UnknownName
| UnknownType
| NameIsNotAType
| DuplicateDeclaration
| NestedFunction
| RecursiveTypeDeclaration
| FunctionReadsOuterBinding
| NotARecordLiteralHead
| RetiredKeyword
| TypeMismatch
| InfiniteType
| AnnotationConflict
| NotEquatable
| NotIterable
| NotOrderable
| WrongTypeArgumentCount
| DuplicateMember
| CompoundAssignNonNumeric
| ReturnOutsideFunction
| BreakOutsideLoop
| IntLiteralOutOfRange
| NotHashable
| NotNumeric
| OperatorNotDefined
| ValueBreakOutsideLoopExpression
| GenericFunctionAsValue
| NoTupleElement
| NotIndexable
| NotAnAssignmentTarget
| NameHasNoFunctionValue
| ParserTemplateOutsideRead
| CallArityMismatch
| InternalMissingType
| NoMethodOnType
| NoFieldOnType
| MissingRecordFields
| UnknownRecordField
| DuplicateRecordField
| NonExhaustiveMatch
| UnreachableArm
| UnknownEnumVariant
| NotAPatternForType
| RefutableBinding
| PayloadArityMismatch
| MalformedParserExpression
| ParserConversion
| UnknownAtomic
| InvalidCaptureName
| UnknownCaptureKind
| UnknownConstructor
| InvalidConstructorArgument
| MixedCaptureNaming
| DuplicateCaptureName
| ConstructorArity
| EmptySeparator
| DuplicateSectionField
| EmptyFieldList
| UnnamedScalarBlockItem
| DuplicateChoiceCase
| MisplacedRepeatedTail
| TemplateScan => {}
}
}
}
#[test]
fn code_distinguishes_categories() {
let lex = DiagnosticCode::new(DiagnosticCategory::Lex, 3);
let parse = DiagnosticCode::new(DiagnosticCategory::Parse, 3);
assert_eq!(lex.to_string(), "T003");
assert_eq!(parse.to_string(), "P003");
assert_ne!(lex, parse);
}
#[test]
fn code_large_number_not_padded() {
let code = DiagnosticCode::new(DiagnosticCategory::Type, 1234);
assert_eq!(code.to_string(), "Y1234");
}
#[test]
fn diagnostic_carries_required_fields() {
let d = Diagnostic::new(
Severity::Error,
DiagCode::BreakOutsideLoop,
"expected Int, found Text",
span(FileId::SYNTHETIC, 0, 1),
);
assert_eq!(d.severity(), Severity::Error);
assert_eq!(d.code().to_string(), "Y012");
assert_eq!(d.kind(), DiagCode::BreakOutsideLoop);
assert_eq!(d.message(), "expected Int, found Text");
assert!(d.notes().is_empty());
assert!(d.suggestions().is_empty());
}
#[test]
fn builder_adds_notes_and_suggestions() {
let d = Diagnostic::build(
Severity::Error,
DiagCode::UnknownName,
"undefined name",
span(FileId::SYNTHETIC, 0, 1),
)
.note(span(FileId::SYNTHETIC, 5, 6), "defined here")
.suggestion(span(FileId::SYNTHETIC, 0, 1), "value", "did you mean")
.finish();
assert_eq!(d.notes().len(), 1);
assert_eq!(d.suggestions().len(), 1);
assert_eq!(d.suggestions()[0].replacement.as_deref(), Some("value"));
}
#[test]
fn did_you_mean_labels_the_fix() {
let at = span(FileId::SYNTHETIC, 0, 4);
let d = Diagnostic::new(
Severity::Error,
DiagCode::UnknownName,
"cannot find `lien`",
at,
)
.with_did_you_mean(at, "line");
assert_eq!(d.suggestions()[0].replacement.as_deref(), Some("line"));
assert_eq!(d.suggestions()[0].label, "did you mean `line`?");
}
#[test]
fn sort_by_position_is_stable_source_order() {
let f = FileId::SYNTHETIC;
let d = |start, end, msg: &str| {
Diagnostic::new(
Severity::Error,
DiagCode::UnknownName,
msg,
span(f, start, end),
)
};
let mut diags = vec![d(10, 12, "c"), d(0, 5, "a"), d(0, 3, "b"), d(0, 5, "a2")];
sort_by_position(&mut diags);
let order: Vec<&str> = diags.iter().map(Diagnostic::message).collect();
assert_eq!(order, ["b", "a", "a2", "c"]);
}
#[test]
fn render_snapshot_single_line() {
let map = SourceMap::new();
let id = map.intern("day03.px", "total += line\n");
let d = Diagnostic::build(
Severity::Error,
DiagCode::BreakOutsideLoop,
"expected Int, found Text",
span(id, 9, 13),
)
.suggestion(
span(id, 9, 13),
"line.int()",
"parse it with the input parser",
)
.finish();
let rendered = render_one(&map, &d);
insta::assert_snapshot!(rendered, @r"
error[Y012]: expected Int, found Text
day03.px:1:10
1 | total += line
| ^^^^ expected Int, found Text
help: parse it with the input parser
line.int()
");
}
#[test]
fn render_snapshot_two_lines_with_note() {
let map = SourceMap::new();
let id = map.intern("f.px", "var a = value\nvar b = a + 1\n");
let primary = span(id, 8, 13);
let d = Diagnostic::build(
Severity::Error,
DiagCode::UnknownName,
"undefined name `value`",
primary,
)
.note(span(id, 23, 24), "the name `a` is defined here")
.finish();
let rendered = render_one(&map, &d);
insta::assert_snapshot!(rendered, @r"
error[N001]: undefined name `value`
f.px:1:9
1 | var a = value
| ^^^^^ undefined name `value`
note: the name `a` is defined here
f.px:2:10
2 | var b = a + 1
| ^
");
}
#[test]
fn styled_renderer_emits_ansi() {
let map = SourceMap::new();
let id = map.intern("f.px", "x = 1\n");
let d = Diagnostic::build(
Severity::Error,
DiagCode::TypeMismatch,
"expected Int, found Text",
span(id, 0, 1),
)
.help(span(id, 0, 1), "call .int()")
.finish();
let mut plain = String::new();
Renderer::new(&map).render(&d, &mut plain);
assert!(
!plain.contains("\x1b["),
"plain output has no ANSI: {plain:?}"
);
let mut styled = String::new();
Renderer::new_styled(&map, style::Palette::styled()).render(&d, &mut styled);
assert!(
styled.contains("\x1b[1;31merror\x1b[0m"),
"styled error label: {styled:?}"
);
assert!(
styled.contains("\x1b[1m[Y001]\x1b[0m"),
"styled code: {styled:?}"
);
assert!(
styled.contains("\x1b[31m^\x1b[0m"),
"styled caret: {styled:?}"
);
assert!(
styled.contains("\x1b[1;36mhelp:\x1b[0m"),
"styled help label: {styled:?}"
);
}
}