main
rs 458 lines 14.3 KB
Raw
1 pub mod code_frame;
2 pub mod js_string;
3
4 pub use js_string::JsString;
5
6 use serde::{Deserialize, Serialize};
7
8 /// Error categories matching the TS ErrorCategory enum
9 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10 pub 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)]
41 pub enum ErrorSeverity {
42 Error,
43 Warning,
44 Hint,
45 Off,
46 }
47
48 impl 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)]
80 pub 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)]
89 pub 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)]
100 pub struct SourceLocation {
101 pub start: Position,
102 pub end: Position,
103 }
104
105 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
106 pub 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
115 pub const GENERATED_SOURCE: Option<SourceLocation> = None;
116
117 /// Detail for a diagnostic
118 #[derive(Debug, Clone, Serialize)]
119 pub 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)]
136 pub 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
144 impl 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)]
208 pub 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
216 impl 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)]
249 pub 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)]
262 pub enum CompilerErrorOrDiagnostic {
263 Diagnostic(CompilerDiagnostic),
264 ErrorDetail(CompilerErrorDetail),
265 }
266
267 impl 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
283 impl 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
375 impl 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.
388 impl 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
401 impl 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
423 impl 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
446 impl std::error::Error for CompilerError {}
447
448 pub 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 }