| 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 | } |