Skip to main content

stylex_macros/
stylex_error.rs

1//! Structured error types and formatting for StyleX diagnostics.
2//!
3//! Provides `StyleXError` and helper functions/macros that produce clean,
4//! branded `[StyleX] <message>` output on both stderr and NAPI boundaries.
5
6use colored::Colorize;
7use std::fmt;
8use stylex_constants::logger::STYLEX_LOG_PREFIX;
9
10/// Structured error for all user-facing StyleX diagnostics.
11///
12/// `Display` produces:
13/// ```text
14/// [StyleX] key > path > message
15///   --> file:line
16/// [Stack trace]: source_location    (whenever source_location is set)
17/// ```
18#[derive(Debug, Clone)]
19pub struct StyleXError {
20  pub message: String,
21  pub file: Option<String>,
22  pub key_path: Option<Vec<String>>,
23  pub line: Option<usize>,
24  pub col: Option<usize>,
25  pub source_location: Option<String>,
26}
27
28impl fmt::Display for StyleXError {
29  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30    // Colored [StyleX] prefix
31    write!(f, "{} ", "[StyleX]".bright_blue().bold())?;
32
33    // Key path breadcrumbs (if any)
34    if let Some(ref keys) = self.key_path {
35      for key in keys {
36        write!(f, "{} > ", key.dimmed().cyan())?;
37      }
38    }
39
40    // Main error message
41    write!(f, "{}", self.message.red())?;
42
43    // File location (when available)
44    if let Some(ref file) = self.file {
45      match (self.line, self.col) {
46        (Some(line), Some(col)) => {
47          write!(f, "\n  --> {file}:{line}:{col}")?;
48        },
49        (Some(line), None) => {
50          write!(f, "\n  --> {file}:{line}")?;
51        },
52        _ => {
53          write!(f, "\n  --> {file}")?;
54        },
55      }
56    }
57
58    // Stack trace (printed whenever source_location is set)
59    if let Some(ref src) = self.source_location
60      && log::log_enabled!(log::Level::Info)
61    {
62      write!(f, "\n{}: {src}", "[Stack trace]".dimmed().yellow(),)?;
63    }
64
65    Ok(())
66  }
67}
68
69impl std::error::Error for StyleXError {}
70
71/// Returns `true` while a [`SuppressPanicStderr`] guard is alive on this thread.
72///
73/// Used by the custom panic hook installed in `logger::initialize()` to avoid
74/// printing anything when a panic is caught by `std::panic::catch_unwind`.
75pub fn is_panic_stderr_suppressed() -> bool {
76  SUPPRESS_PANIC_STDERR.with(|f| f.get())
77}
78
79/// RAII guard that suppresses panic-hook stderr output for its lifetime.
80///
81/// Create one immediately before `catch_unwind`; the hook will stay silent
82/// until the guard is dropped (on exit from scope, including on panic).
83///
84/// ```rust,ignore
85/// let _guard = SuppressPanicStderr::new();
86/// let result = std::panic::catch_unwind(|| { /* … */ });
87/// guard dropped here → suppression lifted
88/// ```
89pub struct SuppressPanicStderr;
90
91impl SuppressPanicStderr {
92  pub fn new() -> Self {
93    SUPPRESS_PANIC_STDERR.with(|f| f.set(true));
94    Self
95  }
96}
97
98impl Default for SuppressPanicStderr {
99  fn default() -> Self {
100    Self::new()
101  }
102}
103
104impl Drop for SuppressPanicStderr {
105  fn drop(&mut self) {
106    SUPPRESS_PANIC_STDERR.with(|f| f.set(false));
107  }
108}
109
110// ---------------------------------------------------------------------------
111// Utilities for the NAPI boundary
112// ---------------------------------------------------------------------------
113
114/// Strip ANSI escape sequences from a string.
115fn strip_ansi(s: &str) -> String {
116  let mut result = String::with_capacity(s.len());
117  let mut chars = s.chars().peekable();
118  while let Some(ch) = chars.next() {
119    if ch == '\x1B' && chars.peek() == Some(&'[') {
120      chars.next(); // consume '['
121      for c in chars.by_ref() {
122        if c == 'm' {
123          break;
124        }
125      }
126    } else {
127      result.push(ch);
128    }
129  }
130  result
131}
132
133/// Extract a plain-text error message from a caught panic payload.
134///
135/// ANSI codes are stripped before the prefix check so that colored
136/// `StyleXError` payloads are detected correctly.  The returned string
137/// is always plain text, safe to pass to the NAPI boundary or non-TTY logs.
138///
139/// If the stripped message contains `[StyleX]`, it is returned as-is.
140/// Otherwise, it is wrapped as `[StyleX] <message>`.
141pub fn format_panic_message(error: &Box<dyn std::any::Any + Send>) -> String {
142  // How to get stack trace from the error?
143  let raw = match error.downcast_ref::<String>() {
144    Some(s) => s.clone(),
145    None => match error.downcast_ref::<&str>() {
146      Some(s) => s.to_string(),
147      None => {
148        return format!("{} Unknown error during transformation", STYLEX_LOG_PREFIX);
149      },
150    },
151  };
152
153  if raw.contains(STYLEX_LOG_PREFIX) {
154    raw
155  } else {
156    let plain = strip_ansi(&raw);
157
158    format!("{} {}", STYLEX_LOG_PREFIX, plain)
159  }
160}
161
162// ---------------------------------------------------------------------------
163// Panic-output suppression (used around `catch_unwind` at the NAPI boundary)
164// ---------------------------------------------------------------------------
165
166thread_local! {
167  static SUPPRESS_PANIC_STDERR: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
168}