Skip to main content

react_compiler_diagnostics/
lib.rs

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