rolldown_error/build_diagnostic/
mod.rs1pub mod constructors;
2pub mod diagnostic;
3pub mod events;
4
5use rustc_hash::FxHashSet;
6use std::{
7 fmt::Display,
8 ops::{Deref, DerefMut},
9};
10
11use crate::{
12 build_diagnostic::events::plugin_error::CausedPlugin,
13 types::diagnostic_options::DiagnosticOptions, utils::downcast_napi_error_diagnostics,
14};
15
16use self::{diagnostic::Diagnostic, events::BuildEvent, events::tsconfig_error::TsConfigError};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum Severity {
20 Info,
21 Error,
22 Warning,
23}
24
25pub struct BuildDiagnostic {
26 inner: Box<dyn BuildEvent>,
27 severity: Severity,
28}
29
30impl std::fmt::Debug for BuildDiagnostic {
40 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41 let diagnostic = self.to_diagnostic();
42 f.debug_struct("BuildDiagnostic")
45 .field("severity", &self.severity)
46 .field("kind", &diagnostic.kind)
47 .field("message", &diagnostic.title)
48 .finish_non_exhaustive()
49 }
50}
51
52impl std::error::Error for BuildDiagnostic {}
53
54impl Display for BuildDiagnostic {
55 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56 self.inner.message(&DiagnosticOptions::default()).fmt(f)
57 }
58}
59
60impl BuildDiagnostic {
61 fn new_inner(inner: impl Into<Box<dyn BuildEvent>>) -> Self {
62 Self { inner: inner.into(), severity: Severity::Error }
63 }
64
65 pub fn id(&self) -> Option<String> {
66 self.inner.id()
67 }
68
69 pub fn plugin(&self) -> Option<String> {
70 self.inner.plugin()
71 }
72
73 pub fn kind(&self) -> crate::types::event_kind::EventKind {
74 self.inner.kind()
75 }
76
77 pub fn exporter(&self) -> Option<String> {
78 self.inner.exporter()
79 }
80
81 pub fn ids(&self) -> Option<Vec<String>> {
82 self.inner.ids()
83 }
84
85 pub fn severity(&self) -> Severity {
86 self.severity
87 }
88
89 #[must_use]
90 pub fn with_severity(mut self, severity: Severity) -> Self {
91 self.severity = severity;
92 self
93 }
94
95 #[must_use]
96 pub fn with_severity_warning(mut self) -> Self {
97 self.severity = Severity::Warning;
98 self
99 }
100
101 pub fn to_diagnostic(&self) -> Diagnostic {
102 self.to_diagnostic_with(&DiagnosticOptions::default())
103 }
104
105 pub fn to_diagnostic_with(&self, opts: &DiagnosticOptions) -> Diagnostic {
106 let mut diagnostic =
107 Diagnostic::new(self.kind().to_string(), self.inner.message(opts), self.severity);
108 self.inner.on_diagnostic(&mut diagnostic, opts);
109 diagnostic
110 }
111
112 pub fn to_message_with(&self, opts: &DiagnosticOptions) -> String {
113 self.inner.message(opts)
114 }
115
116 #[cfg(feature = "napi")]
117 pub fn downcast_napi_error(&self) -> Result<&napi::Error, &Self> {
118 self.inner.as_napi_error().ok_or(self)
119 }
120
121 pub fn downcast_mut<T: 'static + BuildEvent>(&mut self) -> Option<&mut T> {
123 self.inner.as_any_mut().downcast_mut()
124 }
125}
126
127impl From<anyhow::Error> for BuildDiagnostic {
128 fn from(err: anyhow::Error) -> Self {
129 downcast_napi_error_diagnostics(err).unwrap_or_else(BuildDiagnostic::unhandleable_error)
130 }
131}
132
133#[derive(Default)]
134pub struct BatchedBuildDiagnostic(Vec<BuildDiagnostic>);
135
136impl std::fmt::Debug for BatchedBuildDiagnostic {
137 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138 f.debug_tuple("BatchedBuildDiagnostic").field(&self.0).finish()
139 }
140}
141
142impl BatchedBuildDiagnostic {
143 pub fn new(vec: Vec<BuildDiagnostic>) -> Self {
144 Self(vec)
145 }
146
147 pub fn into_vec(self) -> Vec<BuildDiagnostic> {
148 self.0
149 }
150}
151
152impl std::error::Error for BatchedBuildDiagnostic {}
153
154impl Display for BatchedBuildDiagnostic {
155 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156 self.0.iter().map(std::string::ToString::to_string).collect::<Vec<_>>().join("\n").fmt(f)
157 }
158}
159
160impl From<BuildDiagnostic> for BatchedBuildDiagnostic {
161 fn from(v: BuildDiagnostic) -> Self {
162 Self::new(vec![v])
163 }
164}
165
166impl From<Vec<BuildDiagnostic>> for BatchedBuildDiagnostic {
167 fn from(v: Vec<BuildDiagnostic>) -> Self {
168 Self::new(v)
169 }
170}
171
172impl From<anyhow::Error> for BatchedBuildDiagnostic {
173 fn from(error: anyhow::Error) -> Self {
174 let caused_plugin = error.downcast_ref::<CausedPlugin>().cloned();
175 match error.downcast::<Self>() {
176 Ok(batched) => {
177 if let Some(plugin) = caused_plugin {
178 Self::new(
179 batched
180 .into_vec()
181 .into_iter()
182 .map(|diag| BuildDiagnostic::plugin_error(plugin.clone(), diag.into()))
183 .collect(),
184 )
185 } else {
186 batched
187 }
188 }
189 Err(error) => {
190 let diagnostic = if let Some(plugin) = caused_plugin {
192 downcast_napi_error_diagnostics(error)
193 .unwrap_or_else(|error| BuildDiagnostic::plugin_error(plugin, error))
194 } else {
195 BuildDiagnostic::from(error)
196 };
197 Self::new(vec![diagnostic])
198 }
199 }
200 }
201}
202
203impl Deref for BatchedBuildDiagnostic {
204 type Target = Vec<BuildDiagnostic>;
205
206 fn deref(&self) -> &Self::Target {
207 &self.0
208 }
209}
210
211impl DerefMut for BatchedBuildDiagnostic {
212 fn deref_mut(&mut self) -> &mut Self::Target {
213 &mut self.0
214 }
215}
216
217#[derive(Debug, Default)]
228pub struct Diagnostics {
229 diagnostics: Vec<BuildDiagnostic>,
230 has_error: bool,
237}
238
239impl Diagnostics {
240 pub fn new() -> Self {
241 Self::default()
242 }
243
244 pub fn push(&mut self, diagnostic: BuildDiagnostic) {
245 self.has_error |= diagnostic.severity() == Severity::Error;
246 self.diagnostics.push(diagnostic);
247 }
248
249 pub fn extend(&mut self, diagnostics: impl IntoIterator<Item = BuildDiagnostic>) {
250 for diagnostic in diagnostics {
252 self.push(diagnostic);
253 }
254 }
255
256 pub fn is_empty(&self) -> bool {
257 self.diagnostics.is_empty()
258 }
259
260 pub fn has_errors(&self) -> bool {
261 self.has_error
262 }
263
264 pub fn partition(self) -> (Vec<BuildDiagnostic>, Vec<BuildDiagnostic>) {
269 self.diagnostics.into_iter().partition(|d| d.severity() != Severity::Error)
270 }
271
272 pub fn extract_errors(&mut self) -> Vec<BuildDiagnostic> {
274 self.has_error = false;
275 self.diagnostics.extract_if(0.., |d| d.severity() == Severity::Error).collect()
276 }
277
278 pub fn into_result(self) -> crate::BuildResult<Vec<BuildDiagnostic>> {
285 if !self.has_error {
286 return Ok(self.diagnostics);
287 }
288 let (warnings, errors) = self.partition();
289 if errors.is_empty() { Ok(warnings) } else { Err(errors.into()) }
290 }
291}
292
293impl From<Vec<BuildDiagnostic>> for Diagnostics {
294 fn from(diagnostics: Vec<BuildDiagnostic>) -> Self {
295 let has_error = diagnostics.iter().any(|d| d.severity() == Severity::Error);
296 Self { diagnostics, has_error }
297 }
298}
299
300impl IntoIterator for Diagnostics {
301 type Item = BuildDiagnostic;
302 type IntoIter = std::vec::IntoIter<BuildDiagnostic>;
303
304 fn into_iter(self) -> Self::IntoIter {
305 self.diagnostics.into_iter()
306 }
307}
308
309pub fn consolidate_diagnostics(mut diagnostics: Vec<BuildDiagnostic>) -> Vec<BuildDiagnostic> {
314 let mut seen_tsconfig_reasons = FxHashSet::<String>::default();
315 diagnostics.retain_mut(|diag| {
316 diag
317 .downcast_mut::<TsConfigError>()
318 .is_none_or(|tsconfig_err| seen_tsconfig_reasons.insert(tsconfig_err.reason.to_string()))
319 });
320 diagnostics
321}
322
323#[cfg(test)]
324mod tests {
325 use super::BuildDiagnostic;
326 use crate::build_diagnostic::events::plugin_error::CausedPlugin;
327
328 #[test]
332 fn debug_renders_nested_plugin_diagnostic_message() {
333 let inner =
334 BuildDiagnostic::bundler_initialize_error("the underlying failure text".to_string(), None);
335 let plugin_diag =
336 BuildDiagnostic::plugin_error(CausedPlugin::new("my-plugin".into()), inner.into());
337
338 let debug_output = format!("{plugin_diag:?}");
339 assert!(
340 debug_output.contains("the underlying failure text"),
341 "expected non-empty underlying message in Debug output, got: {debug_output}"
342 );
343 }
344}