Skip to main content

react_compiler_diagnostics/
lib.rs

1pub mod code_frame;
2
3use serde::{Serialize, Deserialize};
4
5/// Error categories matching the TS ErrorCategory enum
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7pub enum ErrorCategory {
8    Hooks,
9    CapitalizedCalls,
10    StaticComponents,
11    UseMemo,
12    VoidUseMemo,
13    PreserveManualMemo,
14    MemoDependencies,
15    IncompatibleLibrary,
16    Immutability,
17    Globals,
18    Refs,
19    EffectDependencies,
20    EffectExhaustiveDependencies,
21    EffectSetState,
22    EffectDerivationsOfState,
23    ErrorBoundaries,
24    Purity,
25    RenderSetState,
26    Invariant,
27    Todo,
28    Syntax,
29    UnsupportedSyntax,
30    Config,
31    Gating,
32    Suppression,
33    FBT,
34}
35
36/// Error severity levels
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
38pub enum ErrorSeverity {
39    Error,
40    Warning,
41    Hint,
42    Off,
43}
44
45impl ErrorCategory {
46    pub fn severity(&self) -> ErrorSeverity {
47        match self {
48            // These map to "Compilation Skipped" (Warning severity)
49            ErrorCategory::EffectDependencies
50            | ErrorCategory::IncompatibleLibrary
51            | ErrorCategory::PreserveManualMemo
52            | ErrorCategory::UnsupportedSyntax => ErrorSeverity::Warning,
53
54            // Todo is Hint
55            ErrorCategory::Todo => ErrorSeverity::Hint,
56
57            // Invariant and all others are Error severity
58            _ => ErrorSeverity::Error,
59        }
60    }
61
62    /// The severity to use in logged output, matching the TS compiler's
63    /// `getRuleForCategory()`. This may differ from the internal `severity()`
64    /// used for panicThreshold logic. In particular, `PreserveManualMemo` is
65    /// `Warning` internally (so it doesn't trigger panicThreshold throws) but
66    /// `Error` in logged output (matching TS behavior).
67    pub fn logged_severity(&self) -> ErrorSeverity {
68        match self {
69            ErrorCategory::PreserveManualMemo => ErrorSeverity::Error,
70            _ => self.severity(),
71        }
72    }
73}
74
75/// Suggestion operations for auto-fixes
76#[derive(Debug, Clone, Serialize)]
77pub enum CompilerSuggestionOperation {
78    InsertBefore,
79    InsertAfter,
80    Remove,
81    Replace,
82}
83
84/// A compiler suggestion for fixing an error
85#[derive(Debug, Clone, Serialize)]
86pub struct CompilerSuggestion {
87    pub op: CompilerSuggestionOperation,
88    pub range: (usize, usize),
89    pub description: String,
90    pub text: Option<String>, // None for Remove operations
91}
92
93/// Source location (matches Babel's SourceLocation format)
94/// This is the HIR source location, separate from AST's BaseNode location.
95/// GeneratedSource is represented as None.
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
97pub struct SourceLocation {
98    pub start: Position,
99    pub end: Position,
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
103pub struct Position {
104    pub line: u32,
105    pub column: u32,
106    /// Byte offset in the source file. Preserved for logger event serialization.
107    #[serde(default, skip_serializing)]
108    pub index: Option<u32>,
109}
110
111/// Sentinel value for generated/synthetic source locations
112pub const GENERATED_SOURCE: Option<SourceLocation> = None;
113
114/// Detail for a diagnostic
115#[derive(Debug, Clone, Serialize)]
116pub enum CompilerDiagnosticDetail {
117    Error {
118        loc: Option<SourceLocation>,
119        message: Option<String>,
120        /// The identifier name from the AST source location, if this error
121        /// points to an identifier node. Preserved for logger event serialization
122        /// to match Babel's SourceLocation.identifierName field.
123        #[serde(skip)]
124        identifier_name: Option<String>,
125    },
126    Hint {
127        message: String,
128    },
129}
130
131/// A single compiler diagnostic (new-style)
132#[derive(Debug, Clone)]
133pub struct CompilerDiagnostic {
134    pub category: ErrorCategory,
135    pub reason: String,
136    pub description: Option<String>,
137    pub details: Vec<CompilerDiagnosticDetail>,
138    pub suggestions: Option<Vec<CompilerSuggestion>>,
139}
140
141impl CompilerDiagnostic {
142    pub fn new(
143        category: ErrorCategory,
144        reason: impl Into<String>,
145        description: Option<String>,
146    ) -> Self {
147        Self {
148            category,
149            reason: reason.into(),
150            description,
151            details: Vec::new(),
152            suggestions: None,
153        }
154    }
155
156    pub fn severity(&self) -> ErrorSeverity {
157        self.category.severity()
158    }
159
160    pub fn logged_severity(&self) -> ErrorSeverity {
161        self.category.logged_severity()
162    }
163
164    pub fn with_detail(mut self, detail: CompilerDiagnosticDetail) -> Self {
165        self.details.push(detail);
166        self
167    }
168
169    /// Create a Todo diagnostic (matches TS `CompilerError.throwTodo()`).
170    pub fn todo(reason: impl Into<String>, loc: Option<SourceLocation>) -> Self {
171        let reason = reason.into();
172        let mut diag = Self::new(ErrorCategory::Todo, reason.clone(), None);
173        diag.details.push(CompilerDiagnosticDetail::Error {
174            loc,
175            message: Some(reason),
176            identifier_name: None,
177        });
178        diag
179    }
180
181    /// Create a diagnostic from a CompilerErrorDetail.
182    pub fn from_detail(detail: CompilerErrorDetail) -> Self {
183        Self::new(detail.category, detail.reason.clone(), detail.description.clone())
184            .with_detail(CompilerDiagnosticDetail::Error {
185                loc: detail.loc,
186                message: Some(detail.reason),
187                identifier_name: None,
188            })
189    }
190
191    pub fn primary_location(&self) -> Option<&SourceLocation> {
192        self.details.iter().find_map(|d| match d {
193            CompilerDiagnosticDetail::Error { loc, .. } => loc.as_ref(), // identifier_name covered by ..
194            _ => None,
195        })
196    }
197
198}
199
200/// Legacy-style error detail (matches CompilerErrorDetail in TS)
201#[derive(Debug, Clone, Serialize)]
202pub struct CompilerErrorDetail {
203    pub category: ErrorCategory,
204    pub reason: String,
205    pub description: Option<String>,
206    pub loc: Option<SourceLocation>,
207    pub suggestions: Option<Vec<CompilerSuggestion>>,
208}
209
210impl CompilerErrorDetail {
211    pub fn new(category: ErrorCategory, reason: impl Into<String>) -> Self {
212        Self {
213            category,
214            reason: reason.into(),
215            description: None,
216            loc: None,
217            suggestions: None,
218        }
219    }
220
221    pub fn with_description(mut self, description: impl Into<String>) -> Self {
222        self.description = Some(description.into());
223        self
224    }
225
226    pub fn with_loc(mut self, loc: Option<SourceLocation>) -> Self {
227        self.loc = loc;
228        self
229    }
230
231    pub fn severity(&self) -> ErrorSeverity {
232        self.category.severity()
233    }
234
235    pub fn logged_severity(&self) -> ErrorSeverity {
236        self.category.logged_severity()
237    }
238}
239
240/// Aggregate compiler error - can contain multiple diagnostics.
241/// This is the main error type thrown/returned by the compiler.
242#[derive(Debug, Clone)]
243pub struct CompilerError {
244    pub details: Vec<CompilerErrorOrDiagnostic>,
245    /// When false, this error was accumulated on the Environment via
246    /// `record_error()` / `record_diagnostic()` and returned at the end
247    /// of the pipeline. In TS, `CompileUnexpectedThrow` is only emitted
248    /// for errors that are **thrown** (not accumulated). Defaults to `true`
249    /// because errors created directly (e.g., via `?` from a pass) are
250    /// analogous to thrown errors in the TS code.
251    pub is_thrown: bool,
252}
253
254/// Either a new-style diagnostic or legacy error detail
255#[derive(Debug, Clone)]
256pub enum CompilerErrorOrDiagnostic {
257    Diagnostic(CompilerDiagnostic),
258    ErrorDetail(CompilerErrorDetail),
259}
260
261impl CompilerErrorOrDiagnostic {
262    pub fn severity(&self) -> ErrorSeverity {
263        match self {
264            Self::Diagnostic(d) => d.severity(),
265            Self::ErrorDetail(d) => d.severity(),
266        }
267    }
268
269    pub fn logged_severity(&self) -> ErrorSeverity {
270        match self {
271            Self::Diagnostic(d) => d.logged_severity(),
272            Self::ErrorDetail(d) => d.logged_severity(),
273        }
274    }
275}
276
277impl CompilerError {
278    pub fn new() -> Self {
279        Self {
280            details: Vec::new(),
281            is_thrown: true,
282        }
283    }
284
285    pub fn push_diagnostic(&mut self, diagnostic: CompilerDiagnostic) {
286        if diagnostic.severity() != ErrorSeverity::Off {
287            self.details
288                .push(CompilerErrorOrDiagnostic::Diagnostic(diagnostic));
289        }
290    }
291
292    pub fn push_error_detail(&mut self, detail: CompilerErrorDetail) {
293        if detail.severity() != ErrorSeverity::Off {
294            self.details
295                .push(CompilerErrorOrDiagnostic::ErrorDetail(detail));
296        }
297    }
298
299    pub fn has_errors(&self) -> bool {
300        self.details
301            .iter()
302            .any(|d| d.severity() == ErrorSeverity::Error)
303    }
304
305    pub fn has_any_errors(&self) -> bool {
306        !self.details.is_empty()
307    }
308
309    /// Check if any error detail has Invariant category.
310    pub fn has_invariant_errors(&self) -> bool {
311        self.details.iter().any(|d| {
312            let cat = match d {
313                CompilerErrorOrDiagnostic::Diagnostic(d) => d.category,
314                CompilerErrorOrDiagnostic::ErrorDetail(d) => d.category,
315            };
316            cat == ErrorCategory::Invariant
317        })
318    }
319
320    pub fn merge(&mut self, other: CompilerError) {
321        self.details.extend(other.details);
322    }
323
324    /// Check if all error details are non-invariant.
325    /// In TS, this is used to determine if an error thrown during compilation
326    /// should be logged as CompileUnexpectedThrow.
327    pub fn is_all_non_invariant(&self) -> bool {
328        self.details.iter().all(|d| {
329            let cat = match d {
330                CompilerErrorOrDiagnostic::Diagnostic(d) => d.category,
331                CompilerErrorOrDiagnostic::ErrorDetail(d) => d.category,
332            };
333            cat != ErrorCategory::Invariant
334        })
335    }
336
337    /// Format as a string matching the TS `CompilerError.toString()` output.
338    /// Used for the `data` field of `CompileUnexpectedThrow` events.
339    ///
340    /// Format per detail: `"Category: reason. Description. (line:column)"`
341    /// Multiple details are joined with `"\n\n"`.
342    pub fn to_string_for_event(&self) -> String {
343        self.details
344            .iter()
345            .map(|d| {
346                let (category, reason, description, loc) = match d {
347                    CompilerErrorOrDiagnostic::Diagnostic(d) => {
348                        let loc = d.primary_location().cloned();
349                        (d.category, &d.reason, &d.description, loc)
350                    }
351                    CompilerErrorOrDiagnostic::ErrorDetail(d) => {
352                        (d.category, &d.reason, &d.description, d.loc)
353                    }
354                };
355                let mut buf = format!("{}: {}", format_category_heading(category), reason);
356                if let Some(desc) = description {
357                    buf.push_str(&format!(". {}.", desc));
358                }
359                if let Some(loc) = loc {
360                    buf.push_str(&format!(" ({}:{})", loc.start.line, loc.start.column));
361                }
362                buf
363            })
364            .collect::<Vec<_>>()
365            .join("\n\n")
366    }
367}
368
369impl Default for CompilerError {
370    fn default() -> Self {
371        Self::new()
372    }
373}
374
375/// Allow `?` to convert a `CompilerError` into a `CompilerDiagnostic`
376/// when the enclosing function returns `Result<T, CompilerDiagnostic>`.
377///
378/// This typically happens when `record_error()` returns `Err(CompilerError)`
379/// for an Invariant error, and the calling function already returns
380/// `Result<T, CompilerDiagnostic>`. The conversion extracts the first
381/// error detail from the aggregate error.
382impl From<CompilerError> for CompilerDiagnostic {
383    fn from(err: CompilerError) -> Self {
384        if let Some(first) = err.details.into_iter().next() {
385            match first {
386                CompilerErrorOrDiagnostic::Diagnostic(d) => d,
387                CompilerErrorOrDiagnostic::ErrorDetail(d) => CompilerDiagnostic::from_detail(d),
388            }
389        } else {
390            CompilerDiagnostic::new(
391                ErrorCategory::Invariant,
392                "Unknown compiler error",
393                None,
394            )
395        }
396    }
397}
398
399impl From<CompilerDiagnostic> for CompilerError {
400    fn from(diagnostic: CompilerDiagnostic) -> Self {
401        let mut error = CompilerError::new();
402        // Todo diagnostics should produce ErrorDetail (flat loc format), matching
403        // the TS behavior where CompilerError.throwTodo() creates a CompilerErrorDetail
404        // with loc directly on it, not a CompilerDiagnostic with sub-details.
405        if diagnostic.category == ErrorCategory::Todo {
406            let loc = diagnostic.primary_location().cloned();
407            error.push_error_detail(CompilerErrorDetail {
408                category: diagnostic.category,
409                reason: diagnostic.reason,
410                description: diagnostic.description,
411                loc,
412                suggestions: diagnostic.suggestions,
413            });
414        } else {
415            error.push_diagnostic(diagnostic);
416        }
417        error
418    }
419}
420
421impl std::fmt::Display for CompilerError {
422    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
423        for detail in &self.details {
424            match detail {
425                CompilerErrorOrDiagnostic::Diagnostic(d) => {
426                    write!(f, "{}: {}", format_category_heading(d.category), d.reason)?;
427                    if let Some(desc) = &d.description {
428                        write!(f, ". {}.", desc)?;
429                    }
430                }
431                CompilerErrorOrDiagnostic::ErrorDetail(d) => {
432                    write!(f, "{}: {}", format_category_heading(d.category), d.reason)?;
433                    if let Some(desc) = &d.description {
434                        write!(f, ". {}.", desc)?;
435                    }
436                }
437            }
438            writeln!(f)?;
439        }
440        Ok(())
441    }
442}
443
444impl std::error::Error for CompilerError {}
445
446pub fn format_category_heading(category: ErrorCategory) -> &'static str {
447    match category {
448        ErrorCategory::EffectDependencies
449        | ErrorCategory::IncompatibleLibrary
450        | ErrorCategory::PreserveManualMemo
451        | ErrorCategory::UnsupportedSyntax => "Compilation Skipped",
452        ErrorCategory::Invariant => "Invariant",
453        ErrorCategory::Todo => "Todo",
454        _ => "Error",
455    }
456}