Skip to main content

stylex_transform/shared/utils/log/
build_code_frame_error.rs

1use anyhow::Error;
2use log::{debug, warn};
3use std::collections::hash_map::DefaultHasher;
4use std::hash::{Hash, Hasher};
5use std::{fs, path::Path, sync::Arc};
6use stylex_macros::{panic_macros::__stylex_panic, stylex_error::StyleXError, stylex_panic};
7use swc_compiler_base::{PrintArgs, SourceMapsConfig, TransformOutput, parse_js, print};
8use swc_config::is_module::IsModule;
9use swc_core::{
10  common::{
11    DUMMY_SP, EqIgnoreSpan, FileName, Mark, SourceMap, Span, Spanned, SyntaxContext,
12    errors::{Handler, *},
13  },
14  ecma::{
15    ast::*,
16    codegen::Config,
17    parser::{Syntax, TsSyntax},
18    transforms::typescript::strip,
19    visit::*,
20  },
21};
22
23use crate::shared::structures::state_manager::StateManager;
24use crate::shared::utils::ast::convertors::{
25  convert_concat_to_tpl_expr, convert_simple_tpl_to_str_expr,
26};
27use stylex_regex::regex::URL_REGEX;
28
29pub(crate) struct CodeFrame {
30  source_map: Arc<SourceMap>,
31  handler: Handler,
32}
33
34impl CodeFrame {
35  pub(crate) fn new() -> Self {
36    let source_map = Arc::new(SourceMap::default());
37    let handler =
38      Handler::with_tty_emitter(ColorConfig::Auto, true, false, Some(source_map.clone()));
39
40    Self {
41      source_map,
42      handler,
43    }
44  }
45
46  pub(crate) fn create_error<'a>(&'a self, span: Span, message: &str) -> DiagnosticBuilder<'a> {
47    let prefixed_message = format!("[StyleX] {}", message);
48    let mut diagnostic = self.handler.struct_span_err(span, &prefixed_message);
49
50    let urls = URL_REGEX
51      .find_iter(message)
52      .filter_map(|m| m.ok().map(|m| m.as_str()))
53      .collect::<Vec<_>>();
54
55    let note = format!("\n{}", urls.join("\n"));
56
57    diagnostic.warn("Line number isn't real, it's just a placeholder, Please check the actual line number in your editor.");
58
59    diagnostic.note(note.as_str());
60
61    diagnostic
62  }
63
64  pub(crate) fn get_span_line_number(&self, span: Span) -> usize {
65    self.source_map.lookup_char_pos(span.lo).line
66  }
67}
68
69fn read_source_file(file_name: &FileName) -> Result<String, std::io::Error> {
70  match file_name {
71    FileName::Real(path) => fs::read_to_string(path),
72    FileName::Custom(path) => fs::read_to_string(path),
73    FileName::Url(url) => fs::read_to_string(Path::new(url.path())),
74    _ => Err(std::io::Error::other("Unsupported file name type")),
75  }
76}
77
78pub(crate) fn build_code_frame_error<'a>(
79  wrapped_expression: &'a Expr,
80  fault_expression: &'a Expr,
81  error_message: &'a str,
82  state: &mut StateManager,
83) -> &'a str {
84  match get_span_from_source_code(wrapped_expression, fault_expression, state) {
85    Ok((code_frame, span)) => {
86      code_frame.create_error(span, error_message).emit();
87    },
88    Err(error) => {
89      if log::log_enabled!(log::Level::Debug) {
90        debug!(
91          "Failed to generate code frame error: {:?}. File: {}. Expression: {:?}.",
92          error,
93          state.get_filename(),
94          fault_expression,
95        );
96      } else {
97        warn!(
98          "Failed to generate code frame error: {:?}. File: {}. For more information enable debug logging.",
99          error,
100          state.get_filename(),
101        )
102      };
103    },
104  }
105
106  error_message
107}
108
109/// Finds the span (source location) of a target expression within the source code.
110/// Uses caching to avoid redundant AST traversals for the same expression.
111///
112/// # Arguments
113/// * `wrapped_expression` - The parent expression containing the target
114/// * `target_expression` - The specific expression to locate
115/// * `state` - Mutable reference to the state manager (for caching)
116///
117/// # Returns
118/// A tuple of (CodeFrame, Span) where CodeFrame contains the source map for error display
119pub(crate) fn get_span_from_source_code(
120  wrapped_expression: &Expr,
121  target_expression: &Expr,
122  state: &mut StateManager,
123) -> Result<(CodeFrame, Span), Error> {
124  let cache_key = compute_cache_key(target_expression);
125  let file_name = FileName::Custom(state.get_filename().to_owned());
126
127  // Check cache first - avoid expensive AST operations if we've seen this before
128  if let Some(&cached_span) = state.span_cache.get(&cache_key) {
129    let code_frame = load_code_frame_from_cache(&file_name)?;
130    return Ok((code_frame, cached_span));
131  }
132
133  let code_frame = CodeFrame::new();
134  let program = get_memoized_frame_source_code(
135    wrapped_expression,
136    target_expression,
137    state,
138    &file_name,
139    &code_frame,
140  )
141  .ok_or_else(|| anyhow::anyhow!("Failed to parse source file: {}", state.get_filename()))?;
142
143  let span = find_expression_span(program, target_expression);
144
145  // Cache the result for future lookups
146  state.span_cache.insert(cache_key, span);
147
148  Ok((code_frame, span))
149}
150
151/// Computes a cache key for an expression based on its type and structure
152fn compute_cache_key(expr: &Expr) -> u64 {
153  let mut hasher = DefaultHasher::new();
154  std::mem::discriminant(expr).hash(&mut hasher);
155  expr.hash(&mut hasher);
156  hasher.finish()
157}
158
159/// Loads a CodeFrame with the source file for error display
160fn load_code_frame_from_cache(file_name: &FileName) -> Result<CodeFrame, Error> {
161  let code_frame = CodeFrame::new();
162  let source = read_source_file(file_name)
163    .map_err(|e| anyhow::anyhow!("Failed to read source file: {}", e))?;
164  code_frame
165    .source_map
166    .new_source_file(file_name.clone().into(), source);
167  Ok(code_frame)
168}
169
170/// Finds the span of a target expression within a program AST
171fn find_expression_span(program: Program, target_expression: &Expr) -> Span {
172  let mut finder = ExpressionFinder::new(target_expression);
173  let program = program.fold_with(&mut finder);
174
175  if let Some(span) = finder.get_span() {
176    return span;
177  }
178
179  // Fallback: try finding after template literal conversion
180  let converted_target = target_expression.clone().fold_with(&mut TplConverter {});
181  let mut fallback_finder = ExpressionFinder::new(&converted_target);
182  let _program = program.fold_with(&mut fallback_finder);
183
184  fallback_finder
185    .get_span()
186    .unwrap_or_else(|| target_expression.span())
187}
188
189/// Gets or parses the source code as a Program AST, with memoization.
190/// Returns a cleaned and normalized Program that can be used for expression finding.
191fn get_memoized_frame_source_code(
192  wrapped_expression: &Expr,
193  target_expression: &Expr,
194  state: &mut StateManager,
195  file_name: &FileName,
196  code_frame: &CodeFrame,
197) -> Option<Program> {
198  if let Some((cached_program, source_code)) = state.get_seen_module_source_code()
199    && let Some(source_code) = source_code
200  {
201    code_frame
202      .source_map
203      .new_source_file(Arc::new(file_name.clone()), source_code.to_owned());
204    return Some(Program::Module(cached_program.clone()));
205  }
206
207  let source_code = get_source_code(wrapped_expression, state, file_name, code_frame)?;
208
209  let source_file = code_frame
210    .source_map
211    .new_source_file(Arc::new(file_name.clone()), source_code.clone());
212
213  let program = parse_and_normalize_program(
214    &source_file,
215    code_frame,
216    state.get_filename(),
217    target_expression,
218  )?;
219
220  state.set_seen_module_source_code(
221    match program.as_module() {
222      Some(module) => module,
223      None => stylex_panic!("Expected a module program for source code caching."),
224    },
225    Some(source_code),
226  );
227
228  Some(program)
229}
230
231/// Gets the source code with the following priority:
232/// 1. seen_source_code from state (if not yet normalized)
233/// 2. Read from file (original source)
234/// 3. Create synthetic module (fallback)
235fn get_source_code(
236  wrapped_expression: &Expr,
237  state: &StateManager,
238  file_name: &FileName,
239  code_frame: &CodeFrame,
240) -> Option<String> {
241  if let Some((module, source_code)) = state.get_seen_module_source_code() {
242    if let Some(source_code) = source_code {
243      return Some(source_code.clone());
244    } else {
245      return Some(print_module(
246        code_frame,
247        module.clone(),
248        Some(
249          Config::default()
250            .with_minify(false)
251            .with_omit_last_semi(false)
252            .with_reduce_escaped_newline(false)
253            .with_inline_script(false),
254        ),
255      ));
256    }
257  }
258  if let Ok(source) = read_source_file(file_name) {
259    return Some(source);
260  }
261
262  let synthetic_module = create_module(wrapped_expression);
263  Some(print_module(code_frame, synthetic_module, None))
264}
265
266/// Parses source code into a Program AST and normalizes it
267fn parse_and_normalize_program(
268  source_file: &Arc<swc_core::common::SourceFile>,
269  code_frame: &CodeFrame,
270  filename: &str,
271  target_expression: &Expr,
272) -> Option<Program> {
273  let parse_result = parse_js(
274    code_frame.source_map.clone(),
275    source_file.clone(),
276    &code_frame.handler,
277    EsVersion::EsNext,
278    Syntax::Typescript(TsSyntax {
279      tsx: true,
280      ..Default::default()
281    }),
282    IsModule::Bool(true),
283    None,
284  );
285
286  match parse_result {
287    Ok(program) => {
288      let unresolved_mark = Mark::new();
289      let top_level_mark = Mark::new();
290
291      // Clean and normalize: remove syntax contexts, convert template literals
292      let normalized = program
293        .apply(strip(unresolved_mark, top_level_mark))
294        .fold_with(&mut TplConverter {});
295      Some(normalized)
296    },
297    Err(error) => {
298      if log::log_enabled!(log::Level::Debug) {
299        debug!(
300          "Failed to parse program: {:?}. File: {}. Expression: {:?}",
301          error, filename, target_expression
302        );
303      } else {
304        warn!("Failed to parse program: {:?}. File: {}", error, filename);
305      }
306      None
307    },
308  }
309}
310
311pub(crate) fn print_module(
312  code_frame: &CodeFrame,
313  module: Module,
314  codegen_config: Option<Config>,
315) -> String {
316  print_program(code_frame, Program::Module(module), codegen_config)
317}
318
319pub(crate) fn print_program(
320  code_frame: &CodeFrame,
321  program: Program,
322  codegen_config: Option<Config>,
323) -> String {
324  let printed_source_code = print(
325    code_frame.source_map.clone(),
326    &program,
327    PrintArgs {
328      source_map: SourceMapsConfig::Bool(false),
329      codegen_config: codegen_config.unwrap_or_default(),
330      ..Default::default()
331    },
332  )
333  .unwrap_or_else(|_| TransformOutput {
334    code: "".to_string(),
335    map: None,
336    output: None,
337    diagnostics: Vec::default(),
338  });
339
340  printed_source_code.code
341}
342
343pub(crate) fn create_module(wrapped_expression: &Expr) -> Module {
344  Module {
345    span: DUMMY_SP,
346    body: vec![ModuleItem::Stmt(Stmt::Expr(ExprStmt {
347      span: DUMMY_SP,
348      expr: Box::new(wrapped_expression.clone()),
349    }))],
350    shebang: None,
351  }
352}
353
354/// Visitor that searches for a specific expression in an AST.
355/// Uses discriminant matching for fast filtering before expensive eq_ignore_span checks.
356#[derive(Debug)]
357struct ExpressionFinder {
358  target: Expr,
359  target_discriminant: std::mem::Discriminant<Expr>,
360  found_expr: Option<Expr>,
361}
362
363/// Visitor that normalizes AST by removing syntax contexts and type annotations.
364/// This allows for more reliable expression matching across different parsing contexts.
365#[derive(Debug)]
366struct Cleaner {}
367impl Fold for Cleaner {
368  noop_fold_type!();
369
370  fn fold_binding_ident(&mut self, mut node: BindingIdent) -> BindingIdent {
371    node.id.ctxt = SyntaxContext::empty();
372    node.type_ann = None;
373    node.fold_children_with(self)
374  }
375
376  fn fold_ident(&mut self, mut ident: Ident) -> Ident {
377    ident.ctxt = SyntaxContext::empty();
378    ident.fold_children_with(self)
379  }
380}
381
382impl ExpressionFinder {
383  fn new(target: &Expr) -> Self {
384    let cleaned_target = target.clone().fold_children_with(&mut Cleaner {});
385    let target_discriminant = std::mem::discriminant(&cleaned_target);
386
387    Self {
388      target: cleaned_target,
389      target_discriminant,
390      found_expr: None,
391    }
392  }
393
394  fn get_span(&self) -> Option<Span> {
395    let expr = self.found_expr.as_ref()?;
396
397    Some(Span::new(expr.span_lo(), expr.span_hi()))
398  }
399}
400
401/// Visitor that normalizes template literals and string concatenations.
402/// Helps match expressions that may be written differently in source vs AST.
403#[derive(Debug)]
404struct TplConverter {}
405
406impl Fold for TplConverter {
407  noop_fold_type!();
408
409  fn fold_expr(&mut self, expr: Expr) -> Expr {
410    let expr = convert_concat_to_tpl_expr(expr);
411    let expr = convert_simple_tpl_to_str_expr(expr);
412    expr.fold_children_with(self)
413  }
414}
415
416impl Fold for ExpressionFinder {
417  noop_fold_type!();
418
419  fn fold_expr(&mut self, expr: Expr) -> Expr {
420    if self.found_expr.is_some() {
421      return expr;
422    }
423
424    // Fast discriminant check filters expressions by variant type
425    if std::mem::discriminant(&expr) != self.target_discriminant {
426      return expr.fold_children_with(self);
427    }
428
429    // Expensive structural comparison only for matching variants
430    if self.target.eq_ignore_span(&expr) {
431      self.found_expr = Some(expr.clone());
432      return expr;
433    }
434
435    expr.fold_children_with(self)
436  }
437}
438
439#[track_caller]
440pub(crate) fn build_code_frame_error_and_panic(
441  wrapped_expression: &Expr,
442  fault_expression: &Expr,
443  error_message: &str,
444  state: &mut StateManager,
445) -> ! {
446  let caller_location = std::panic::Location::caller();
447
448  // Emit the code frame diagnostic to stderr (already [StyleX]-prefixed)
449  let (file, line) = match get_span_from_source_code(wrapped_expression, fault_expression, state) {
450    Ok((code_frame, span)) => {
451      code_frame.create_error(span, error_message).emit();
452      let line_num = code_frame.get_span_line_number(span);
453      (Some(state.get_filename().to_owned()), Some(line_num))
454    },
455    Err(error) => {
456      if log::log_enabled!(log::Level::Debug) {
457        debug!(
458          "Failed to generate code frame error: {:?}. File: {}. Expression: {:?}.",
459          error,
460          state.get_filename(),
461          fault_expression,
462        );
463      } else {
464        warn!(
465          "Failed to generate code frame error: {:?}. File: {}. For more information enable debug logging.",
466          error,
467          state.get_filename(),
468        );
469      }
470      (Some(state.get_filename().to_owned()), None)
471    },
472  };
473
474  let err = StyleXError {
475    message: error_message.to_string(),
476    file,
477    key_path: None,
478    line,
479    col: None,
480    source_location: Some(format!(
481      "{}:{}",
482      caller_location.file(),
483      caller_location.line()
484    )),
485  };
486
487  __stylex_panic(err)
488}