perf(diagnostics): migrate to allocation-optimized oxc-miette (#23094)

## Summary

Adopts the allocation-optimized oxc-miette diagnostic APIs (oxc-miette v3):

- `OxcDiagnostic` stores labels in `miette::Labels` (1–2 inline, no `Vec`); **drops the `smallvec` dependency**.
- `Diagnostic::labels`/`related` return owned inline containers; `code`/`help`/`note`/`url` return `Cow<str>`; `SourceCode::read_span` returns the concrete `MietteSpanContents` — all consumer call sites updated (linter `fixer`/`lib`/`runtime`/`tsgolint`, `oxc_napi`, napi/parser, oxlint LSP, coverage).
- `usize → u32` span conversions for the u32 `SourceSpan`.

## Allocation impact (parser benchmark)

`just allocs` — **~33% fewer sys allocations, no change in reallocations**:

| File | Sys allocs (before → after) | Sys reallocs |
|------|---|---|
| checker.ts | 5303 → 3543 | 10 (unchanged) |
| antd.js | 5392 → 3720 | 221 (unchanged) |
| App.tsx | 632 → 444 | 13 (unchanged) |
| pdf.mjs | 518 → 356 | 67 (unchanged) |
| binder.ts | 281 → 169 | 0 (unchanged) |

(The inline `Labels` removes the per-diagnostic `Vec`; an earlier `Box<str>` realloc regression was fixed in oxc-miette before this snapshot was taken.)
This commit is contained in:
Boshen committed 2026-06-08 16:00:57 +00:00
1 parent b7b08ce804
commit da1a6c6e2a
18 files changed
+90 -127

No files matched your search

Generated
+4 -4
View File
@@ -1840,9 +1840,9 @@ dependencies = [
[[package]]
name = "oxc-miette"
version = "2.7.1"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4356a61f2ed4c9b3610245215fbf48970eb277126919f87db9d0efa93a74245c"
checksum = "b776084bf11ad750806cb63f9ed2606a12ab374cf458f36a7731eba1f5d753fc"
dependencies = [
"cfg-if",
"owo-colors",
@@ -1855,9 +1855,9 @@ dependencies = [
[[package]]
name = "oxc-miette-derive"
version = "2.7.1"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b237422b014f8f8fff75bb9379e697d13f8d57551a22c88bebb39f073c1bf696"
checksum = "66e0bafc397eee2f94c5bf89bb5a82ba6d522d1b79e2924c8169f053febb433f"
dependencies = [
"proc-macro2",
"quote",
+1 -1
View File
@@ -206,7 +206,7 @@ language-tags = "0.3.2" # Language tag parsing
lazy-regex = "3.5.1" # Lazy regex compilation
markdown = "1.0.0" # Markdown parsing
memchr = "2.7.6" # Fast byte searching
miette = { package = "oxc-miette", version = "2.7.1", features = [
miette = { package = "oxc-miette", version = "3.0.0", features = [
"fancy-no-syscall",
] } # Error reporting
mimalloc-safe = "0.1.62" # Fast allocator
+3
View File
@@ -237,6 +237,9 @@ impl ConfigLoadError {
///
/// This groups together failures related to the root configuration file
/// and to any nested configuration files discovered during loading.
// `OxcDiagnostic` is intentionally inlined (~176 bytes) to keep its construction
// allocation-free, so the variant size difference is a deliberate trade-off.
#[expect(clippy::large_enum_variant)]
#[derive(Debug)]
pub enum CliConfigLoadError {
/// An error that occurred while loading or parsing the root configuration.
+25 -23
View File
@@ -76,9 +76,6 @@ fn miette_severity_to_lsp_severity(value: Severity) -> DiagnosticSeverity {
Severity::Advice => DiagnosticSeverity::HINT,
}
}
// clippy: the source field is checked and assumed to be less than 4GB, and
// we assume that the fix offset will not exceed 2GB in either direction
#[expect(clippy::cast_possible_truncation)]
pub fn message_to_lsp_diagnostic(
message: Message,
uri: &Uri,
@@ -97,27 +94,32 @@ pub fn message_to_lsp_diagnostic(
miette_severity_to_lsp_severity(message.error.severity)
};
let related_information = message.error.labels.as_ref().map(|spans| {
spans
.iter()
.map(|span| {
let offset = span.offset() as u32;
let start_position = offset_to_position(rope, offset, source_text);
let end_position =
offset_to_position(rope, offset + span.len() as u32, source_text);
let related_information = if message.error.labels.is_empty() {
None
} else {
Some(
message
.error
.labels
.iter()
.map(|span| {
let offset = span.offset();
let start_position = offset_to_position(rope, offset, source_text);
let end_position = offset_to_position(rope, offset + span.len(), source_text);
ls_types::DiagnosticRelatedInformation {
location: ls_types::Location {
uri: uri.clone(),
range: ls_types::Range::new(start_position, end_position),
},
message: span
.label()
.map_or_else(String::new, std::string::ToString::to_string),
}
})
.collect()
});
ls_types::DiagnosticRelatedInformation {
location: ls_types::Location {
uri: uri.clone(),
range: ls_types::Range::new(start_position, end_position),
},
message: span
.label()
.map_or_else(String::new, std::string::ToString::to_string),
}
})
.collect(),
)
};
let start_position = offset_to_position(rope, message.span.start, source_text);
let end_position = offset_to_position(rope, message.span.end, source_text);
+1 -1
View File
@@ -51,7 +51,7 @@ fn format_agent(diagnostic: &Error) -> String {
let message = compact_message(&diagnostic.to_string());
let help = diagnostic
.help()
.map(|help| format!(" help: {}", compact_message(&help.to_string())))
.map(|help| format!(" help: {}", compact_message(&help)))
.unwrap_or_default();
let location =
if start.line == 0 { String::new() } else { format!(":{}:{}", start.line, start.column) };
+18 -26
View File
@@ -64,7 +64,7 @@ pub type Severity = miette::Severity;
pub type Result<T> = std::result::Result<T, OxcDiagnostic>;
use miette::{Diagnostic, SourceCode};
pub use miette::{GraphicalReportHandler, GraphicalTheme, LabeledSpan, NamedSource};
pub use miette::{GraphicalReportHandler, GraphicalTheme, LabeledSpan, Labels, NamedSource};
/// Describes an error or warning that occurred.
///
@@ -115,7 +115,7 @@ impl Display for OxcCode {
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct OxcDiagnosticInner {
pub message: Cow<'static, str>,
pub labels: Option<Vec<LabeledSpan>>,
pub labels: Labels,
pub help: Option<Cow<'static, str>>,
pub note: Option<Cow<'static, str>>,
pub severity: Severity,
@@ -133,16 +133,16 @@ impl std::error::Error for OxcDiagnostic {}
impl Diagnostic for OxcDiagnostic {
/// The secondary help message.
fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
self.help.as_ref().map(Box::new).map(|c| c as Box<dyn Display>)
fn help(&self) -> Option<Cow<'_, str>> {
self.help.as_deref().map(Cow::Borrowed)
}
/// A note for the diagnostic.
///
/// Similar to rustc - intended for additional explanation and information,
/// e.g. why an error was emitted, how to turn it off.
fn note<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
self.note.as_ref().map(Box::new).map(|c| c as Box<dyn Display>)
fn note(&self) -> Option<Cow<'_, str>> {
self.note.as_deref().map(Cow::Borrowed)
}
/// The severity level of this diagnostic.
@@ -153,24 +153,20 @@ impl Diagnostic for OxcDiagnostic {
}
/// Labels covering problematic portions of source code.
fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
self.labels
.as_ref()
.map(|ls| ls.iter().cloned())
.map(Box::new)
.map(|b| b as Box<dyn Iterator<Item = LabeledSpan>>)
fn labels(&self) -> Labels {
self.labels.clone()
}
/// An error code uniquely identifying this diagnostic.
///
/// Note that codes may be scoped, which will be rendered as `scope(code)`.
fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
self.code.is_some().then(|| Box::new(&self.code) as Box<dyn Display>)
fn code(&self) -> Option<Cow<'_, str>> {
self.code.is_some().then(|| Cow::Owned(self.code.to_string()))
}
/// A URL that provides more information about the problem that occurred.
fn url<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
self.url.as_ref().map(Box::new).map(|c| c as Box<dyn Display>)
fn url(&self) -> Option<Cow<'_, str>> {
self.url.as_deref().map(Cow::Borrowed)
}
}
@@ -180,7 +176,7 @@ impl OxcDiagnostic {
Self {
inner: OxcDiagnosticInner {
message: message.into(),
labels: None,
labels: Labels::None,
note: None,
help: None,
severity: Severity::Error,
@@ -195,7 +191,7 @@ impl OxcDiagnostic {
Self {
inner: OxcDiagnosticInner {
message: message.into(),
labels: None,
labels: Labels::None,
help: None,
note: None,
severity: Severity::Warning,
@@ -310,7 +306,7 @@ impl OxcDiagnostic {
/// [`oxc_span::Span`]: https://docs.rs/oxc_span/latest/oxc_span/struct.Span.html
/// [`label`]: https://docs.rs/oxc_span/latest/oxc_span/struct.Span.html#method.label
pub fn with_label<T: Into<LabeledSpan>>(mut self, label: T) -> Self {
self.inner.labels = Some(vec![label.into()]);
self.inner.labels = Labels::One([label.into()]);
self
}
@@ -331,15 +327,13 @@ impl OxcDiagnostic {
mut self,
labels: T,
) -> Self {
self.inner.labels = Some(labels.into_iter().map(Into::into).collect());
self.inner.labels = labels.into_iter().map(Into::into).collect();
self
}
/// Add a label to this diagnostic without clobbering existing labels.
pub fn and_label<T: Into<LabeledSpan>>(mut self, label: T) -> Self {
let mut labels = self.inner.labels.unwrap_or_default();
labels.push(label.into());
self.inner.labels = Some(labels);
self.inner.labels.push(label.into());
self
}
@@ -348,9 +342,7 @@ impl OxcDiagnostic {
mut self,
labels: T,
) -> Self {
let mut all_labels = self.inner.labels.unwrap_or_default();
all_labels.extend(labels.into_iter().map(Into::into));
self.inner.labels = Some(all_labels);
self.inner.labels.extend(labels.into_iter().map(Into::into));
self
}
+3 -4
View File
@@ -1,6 +1,6 @@
//! [Reporters](DiagnosticReporter) for rendering and writing diagnostics.
use miette::SourceSpan;
use miette::{SourceSpan, SpanContents};
use crate::{Error, Severity};
@@ -125,9 +125,8 @@ impl Info {
let mut severity = Severity::Warning;
let rule_id = diagnostic.code().map(|code| code.to_string());
if let Some(mut labels) = diagnostic.labels()
&& let Some(source) = diagnostic.source_code()
&& let Some(label) = labels.next()
if let Some(source) = diagnostic.source_code()
&& let Some(label) = diagnostic.labels().first()
&& let Ok(span_content) = source.read_span(label.inner(), 0, 0)
{
start.line = span_content.line() + 1;
+6 -8
View File
@@ -273,13 +273,13 @@ pub struct Message {
}
impl Message {
#[expect(clippy::cast_possible_truncation)] // for `as u32`
pub fn new(error: OxcDiagnostic, fixes: PossibleFixes) -> Self {
let span = error
.labels
.as_ref()
.and_then(|labels| labels.iter().find(|span| span.primary()).or_else(|| labels.first()))
.map(|span| Span::new(span.offset() as u32, (span.offset() + span.len()) as u32))
.iter()
.find(|span| span.primary())
.or_else(|| error.labels.first())
.map(|span| Span::new(span.offset(), span.offset() + span.len()))
.unwrap_or_default();
Self { error, span, fixes, fixed: false, section_offset: 0, rule: None }
@@ -303,10 +303,8 @@ impl Message {
self.span = self.span.move_right(offset);
if let Some(labels) = &mut self.error.labels {
for label in labels {
label.set_span_offset(label.offset().saturating_add(offset as usize));
}
for label in &mut self.error.labels {
label.set_span_offset(label.offset().saturating_add(offset));
}
match &mut self.fixes {
+3 -8
View File
@@ -418,14 +418,9 @@ impl Linter {
let labels = m
.error
.labels
.as_ref()
.map(|labels| {
labels
.iter()
.map(|label| (label.offset(), label.len(), label.primary()))
.collect::<Vec<_>>()
})
.unwrap_or_default();
.iter()
.map(|label| (label.offset(), label.len(), label.primary()))
.collect::<Vec<_>>();
let fix_span = m.fixes.span();
(labels, m.error.code.clone(), (m.span.start, m.span.end), (fix_span.start, fix_span.end))
};
@@ -67,10 +67,7 @@ impl Rule for NoSparseArrays {
_ => None,
})
.map(|elision| {
LabeledSpan::at(
(elision.span.start as usize)..(elision.span.start as usize),
"unexpected comma",
)
LabeledSpan::at(elision.span.start..elision.span.start, "unexpected comma")
})
.collect::<Vec<_>>();
@@ -86,7 +83,7 @@ impl Rule for NoSparseArrays {
LabeledSpan::at(array_expr.span, "the array here")
} else {
LabeledSpan::at(
(array_expr.span.start as usize)..(array_expr.span.start as usize),
array_expr.span.start..array_expr.span.start,
"the array starting here",
)
};
@@ -102,12 +102,7 @@ impl Rule for CheckPropertyNames {
for (type_name, spans) in seen.iter().filter(|(_, spans)| 1 < spans.len()) {
let labels = spans
.iter()
.map(|span| {
LabeledSpan::at(
(span.start as usize)..(span.end as usize),
"Duplicated property",
)
})
.map(|span| LabeledSpan::at(span.start..span.end, "Duplicated property"))
.collect::<Vec<_>>();
ctx.diagnostic(
OxcDiagnostic::warn("Duplicate @property found.")
+2 -6
View File
@@ -1092,12 +1092,8 @@ impl Runtime {
let err: Vec<OxcDiagnostic> = err
.into_iter()
.map(|mut diagnostic| {
if let Some(labels) = &mut diagnostic.labels {
for label in labels.iter_mut() {
label.set_span_offset(
label.offset() + section_source.start as usize,
);
}
for label in &mut diagnostic.labels {
label.set_span_offset(label.offset() + section_source.start);
}
diagnostic
})
+5 -5
View File
@@ -1366,9 +1366,9 @@ mod test {
message.error.code,
OxcCode { scope: Some("typescript".into()), number: Some("some_rule".into()) }
);
assert!(message.error.labels.as_ref().is_some());
assert_eq!(message.error.labels.as_ref().unwrap().len(), 1);
assert_eq!(message.error.labels.as_ref().unwrap()[0], LabeledSpan::new(None, 0, 10));
assert!(!message.error.labels.is_empty());
assert_eq!(message.error.labels.len(), 1);
assert_eq!(message.error.labels[0], LabeledSpan::new(None, 0, 10));
assert_eq!(message.error.help, Some("Some help".into()));
assert!(message.fixes.is_empty());
}
@@ -1529,8 +1529,8 @@ mod test {
let message = Message::from_tsgo_lint_diagnostic(diagnostic, "Some text over 10 bytes.");
assert!(message.error.labels.is_some());
let labels = message.error.labels.as_ref().unwrap();
assert!(!message.error.labels.is_empty());
let labels = &message.error.labels;
assert_eq!(labels.len(), 3);
assert_eq!(labels[0], LabeledSpan::new(Some("Label 1".into()), 0, 5));
assert_eq!(labels[1], LabeledSpan::new(Some("Label 2".into()), 5, 5));
+3 -8
View File
@@ -49,11 +49,7 @@ impl OxcError {
impl From<&OxcDiagnostic> for OxcError {
fn from(diagnostic: &OxcDiagnostic) -> Self {
let labels = diagnostic
.labels
.as_ref()
.map(|labels| labels.iter().map(ErrorLabel::from).collect::<Vec<_>>())
.unwrap_or_default();
let labels = diagnostic.labels.iter().map(ErrorLabel::from).collect::<Vec<_>>();
Self {
severity: Severity::from(diagnostic.severity),
message: diagnostic.message.to_string(),
@@ -72,12 +68,11 @@ pub struct ErrorLabel {
}
impl From<&LabeledSpan> for ErrorLabel {
#[expect(clippy::cast_possible_truncation)]
fn from(label: &LabeledSpan) -> Self {
Self {
message: label.label().map(ToString::to_string),
start: label.offset() as u32,
end: (label.offset() + label.len()) as u32,
start: label.offset(),
end: label.offset() + label.len(),
}
}
}
+1 -1
View File
@@ -563,7 +563,7 @@ impl From<Range<u32>> for Span {
impl From<Span> for SourceSpan {
fn from(val: Span) -> Self {
Self::new(SourceOffset::from(val.start as usize), val.size() as usize)
Self::new(SourceOffset::from(val.start), val.size())
}
}
+4 -10
View File
@@ -92,14 +92,9 @@ impl<'a> Error<'a> {
named_source: &Arc<NamedSource<String>>,
allocator: &'a Allocator,
) -> Self {
let labels = diagnostic.labels.as_ref().map_or_else(
|| Vec::new_in(allocator),
|labels| {
Vec::from_iter_in(
labels.iter().map(|label| ErrorLabel::from_in(label, allocator)),
allocator,
)
},
let labels = Vec::from_iter_in(
diagnostic.labels.iter().map(|label| ErrorLabel::from_in(label, allocator)),
allocator,
);
let severity = ErrorSeverity::from(diagnostic.severity);
@@ -146,8 +141,7 @@ impl<'a> FromIn<'a, &LabeledSpan> for ErrorLabel<'a> {
fn from_in(label: &LabeledSpan, allocator: &'a Allocator) -> Self {
Self {
message: label.label().map(|message| Str::from_in(message, allocator)),
#[expect(clippy::cast_possible_truncation)]
span: Span::sized(label.offset() as u32, label.len() as u32),
span: Span::sized(label.offset(), label.len()),
}
}
}
+2 -5
View File
@@ -144,13 +144,10 @@ fn is_error_suppressed_by_ts_ignore(
}
// Get the error's byte offset from the first label
let Some(labels) = &error.labels else {
let Some(first_label) = error.labels.first() else {
return false;
};
let Some(first_label) = labels.first() else {
return false;
};
let error_offset = first_label.offset();
let error_offset = first_label.offset() as usize;
// Check if any ts-ignore span covers the line before this error
for ts_ignore_span in ts_ignore_spans {
@@ -1,16 +1,16 @@
File | File size || Sys allocs | Sys reallocs || Arena allocs | Arena reallocs
---------------------------------------------------------------------------------------------------------------------------
checker.ts | 2.92 MB || 2725 | 10 || 264366 | 22860
checker.ts | 2.92 MB || 1825 | 10 || 264366 | 22860
App.tsx | 415.34 kB || 520 | 13 || 44464 | 3537
App.tsx | 415.34 kB || 364 | 13 || 44464 | 3537
RadixUIAdoptionSection.jsx | 2.52 kB || 1 | 0 || 367 | 66
pdf.mjs | 567.30 kB || 518 | 67 || 90583 | 8153
pdf.mjs | 567.30 kB || 356 | 67 || 90583 | 8153
antd.js | 6.69 MB || 5392 | 221 || 528577 | 55355
antd.js | 6.69 MB || 3720 | 221 || 528577 | 55355
binder.ts | 193.08 kB || 141 | 0 || 16620 | 1476
binder.ts | 193.08 kB || 85 | 0 || 16620 | 1476
kitchen-sink.tsx | 732.90 kB || 2991 | 160 || 138059 | 11905
kitchen-sink.tsx | 732.90 kB || 2108 | 160 || 138059 | 11905