Skip to main content

hotspots_core/
parser.rs

1//! TypeScript and JavaScript parser using SWC
2//!
3//! Global invariants enforced:
4//! - Deterministic parsing order
5//! - Formatting, comments, and whitespace must not affect results
6
7use anyhow::Result;
8use swc_common::{sync::Lrc, FileName, SourceFile, SourceMap};
9use swc_ecma_ast::{EsVersion, Module};
10use swc_ecma_parser::{lexer::Lexer, Parser, StringInput, Syntax};
11
12/// Determine the appropriate syntax configuration based on file extension
13fn syntax_for_file(filename: &str) -> Syntax {
14    // Check if this is a TypeScript file
15    if filename.ends_with(".tsx") || filename.ends_with(".mtsx") || filename.ends_with(".ctsx") {
16        // TypeScript with JSX (TSX)
17        Syntax::Typescript(swc_ecma_parser::TsSyntax {
18            tsx: true,
19            decorators: true, // Angular and other decorator-heavy frameworks
20            dts: false,
21            ..Default::default()
22        })
23    } else if filename.ends_with(".ts") || filename.ends_with(".mts") || filename.ends_with(".cts")
24    {
25        // TypeScript without JSX
26        let is_dts = filename.ends_with(".d.ts");
27        Syntax::Typescript(swc_ecma_parser::TsSyntax {
28            tsx: false,
29            decorators: true, // Angular uses @Component, @Injectable, etc.
30            dts: is_dts,
31            ..Default::default()
32        })
33    } else if filename.ends_with(".jsx")
34        || filename.ends_with(".mjsx")
35        || filename.ends_with(".cjsx")
36    {
37        // JavaScript with JSX
38        Syntax::Es(swc_ecma_parser::EsSyntax {
39            jsx: true,
40            decorators: false,
41            ..Default::default()
42        })
43    } else {
44        // Plain JavaScript (.js, .mjs, .cjs) — jsx: true is a superset of plain JS
45        Syntax::Es(swc_ecma_parser::EsSyntax {
46            jsx: true, // React webpack projects put JSX in .js files by convention
47            decorators: false,
48            ..Default::default()
49        })
50    }
51}
52
53/// Parse TypeScript, JavaScript, JSX, or TSX source code into an AST module
54///
55/// Automatically detects file type based on extension and uses appropriate parser configuration.
56///
57/// Supported file types:
58/// - `.ts`, `.mts`, `.cts` - TypeScript
59/// - `.tsx`, `.mtsx`, `.ctsx` - TypeScript with JSX
60/// - `.js`, `.mjs`, `.cjs` - JavaScript
61/// - `.jsx`, `.mjsx`, `.cjsx` - JavaScript with JSX
62///
63/// Returns an error if parse errors occur.
64pub fn parse_source(src: &str, source_map: &Lrc<SourceMap>, filename: &str) -> Result<Module> {
65    // Determine syntax based on file extension
66    let syntax = syntax_for_file(filename);
67
68    // Create SourceFile for the source code
69    let source_file: Lrc<SourceFile> =
70        source_map.new_source_file(FileName::Custom(filename.into()).into(), src.to_string());
71
72    // Create StringInput from SourceFile
73    let input = StringInput::from(&*source_file);
74
75    // Create lexer with detected syntax
76    let lexer = Lexer::new(syntax, EsVersion::Es2022, input, None);
77
78    // Create parser
79    let mut parser = Parser::new_from(lexer);
80
81    // Parse module
82    parser.parse_module().map_err(|e| {
83        let error_msg = e.kind().msg();
84        anyhow::anyhow!("Parse error: {}", error_msg)
85            .context(format!("Failed to parse source file: {}", filename))
86    })
87}
88
89/// Legacy alias for backwards compatibility
90///
91/// Use `parse_source` instead for new code.
92#[deprecated(since = "0.1.0", note = "Use parse_source instead")]
93pub fn parse_typescript(src: &str, source_map: &Lrc<SourceMap>, filename: &str) -> Result<Module> {
94    parse_source(src, source_map, filename)
95}
96
97#[cfg(test)]
98#[path = "parser/tests.rs"]
99mod tests;