oxc_parser_napi/
lib.rs

1// Napi value need to be passed as value
2#![expect(clippy::needless_pass_by_value)]
3
4#[cfg(all(
5    feature = "allocator",
6    not(any(target_arch = "arm", target_os = "freebsd", target_family = "wasm"))
7))]
8#[global_allocator]
9static ALLOC: mimalloc_safe::MiMalloc = mimalloc_safe::MiMalloc;
10
11use std::mem;
12
13use napi::{Task, bindgen_prelude::AsyncTask};
14use napi_derive::napi;
15
16use oxc::{
17    allocator::Allocator,
18    parser::{ParseOptions, Parser, ParserReturn},
19    semantic::SemanticBuilder,
20    span::SourceType,
21};
22use oxc_napi::{Comment, OxcError, convert_utf8_to_utf16, get_source_type};
23
24mod convert;
25mod types;
26pub use types::{EcmaScriptModule, ParseResult, ParserOptions};
27
28// Raw transfer is only supported on 64-bit little-endian systems.
29// Don't include raw transfer code on other platforms (notably WASM32).
30// `raw_transfer_types` still needs to be compiled, as `assert_layouts` refers to those types,
31// but it's all dead code on unsupported platforms, and will be excluded from binary.
32#[cfg(all(target_pointer_width = "64", target_endian = "little"))]
33mod raw_transfer;
34mod raw_transfer_types;
35#[cfg(all(target_pointer_width = "64", target_endian = "little"))]
36pub use raw_transfer::{
37    get_buffer_offset, parse_async_raw, parse_sync_raw, raw_transfer_supported,
38};
39
40// Fallback for 32-bit or big-endian platforms.
41/// Returns `true` if raw transfer is supported on this platform.
42#[cfg(not(all(target_pointer_width = "64", target_endian = "little")))]
43#[napi]
44pub fn raw_transfer_supported() -> bool {
45    false
46}
47
48mod generated {
49    // Note: We intentionally don't import `generated/derive_estree.rs`. It's not needed.
50    #[cfg(debug_assertions)]
51    mod assert_layouts;
52    #[cfg(all(target_pointer_width = "64", target_endian = "little"))]
53    pub mod raw_transfer_constants;
54}
55#[cfg(all(target_pointer_width = "64", target_endian = "little"))]
56use generated::raw_transfer_constants;
57
58#[derive(Clone, Copy, PartialEq, Eq)]
59enum AstType {
60    JavaScript,
61    TypeScript,
62}
63
64fn get_ast_type(source_type: SourceType, options: &ParserOptions) -> AstType {
65    match options.ast_type.as_deref() {
66        Some("js") => AstType::JavaScript,
67        Some("ts") => AstType::TypeScript,
68        _ => {
69            if source_type.is_javascript() {
70                AstType::JavaScript
71            } else {
72                AstType::TypeScript
73            }
74        }
75    }
76}
77
78fn parse<'a>(
79    allocator: &'a Allocator,
80    source_type: SourceType,
81    source_text: &'a str,
82    options: &ParserOptions,
83) -> ParserReturn<'a> {
84    Parser::new(allocator, source_text, source_type)
85        .with_options(ParseOptions {
86            preserve_parens: options.preserve_parens.unwrap_or(true),
87            ..ParseOptions::default()
88        })
89        .parse()
90}
91
92fn parse_with_return(filename: &str, source_text: String, options: &ParserOptions) -> ParseResult {
93    let allocator = Allocator::default();
94    let source_type =
95        get_source_type(filename, options.lang.as_deref(), options.source_type.as_deref());
96    let ast_type = get_ast_type(source_type, options);
97    let ranges = options.range.unwrap_or(false);
98    let ret = parse(&allocator, source_type, &source_text, options);
99
100    let mut program = ret.program;
101    let mut module_record = ret.module_record;
102    let mut diagnostics = ret.errors;
103
104    if options.show_semantic_errors == Some(true) {
105        let semantic_ret = SemanticBuilder::new().with_check_syntax_error(true).build(&program);
106        diagnostics.extend(semantic_ret.errors);
107    }
108
109    let mut errors = OxcError::from_diagnostics(filename, &source_text, diagnostics);
110
111    let mut comments =
112        convert_utf8_to_utf16(&source_text, &mut program, &mut module_record, &mut errors);
113
114    let program_and_fixes = match ast_type {
115        AstType::JavaScript => {
116            // Add hashbang to start of comments
117            if let Some(hashbang) = &program.hashbang {
118                comments.insert(
119                    0,
120                    Comment {
121                        r#type: "Line".to_string(),
122                        value: hashbang.value.to_string(),
123                        start: hashbang.span.start,
124                        end: hashbang.span.end,
125                    },
126                );
127            }
128
129            program.to_estree_js_json_with_fixes(ranges)
130        }
131        AstType::TypeScript => {
132            // Note: `@typescript-eslint/parser` ignores hashbangs,
133            // despite appearances to the contrary in AST explorers.
134            // So we ignore them too.
135            // See: https://github.com/typescript-eslint/typescript-eslint/issues/6500
136            program.to_estree_ts_json_with_fixes(ranges)
137        }
138    };
139
140    let module = EcmaScriptModule::from(&module_record);
141
142    ParseResult { program_and_fixes, module, comments, errors }
143}
144
145/// Parse synchronously.
146#[napi]
147pub fn parse_sync(
148    filename: String,
149    source_text: String,
150    options: Option<ParserOptions>,
151) -> ParseResult {
152    let options = options.unwrap_or_default();
153    parse_with_return(&filename, source_text, &options)
154}
155
156pub struct ResolveTask {
157    filename: String,
158    source_text: String,
159    options: ParserOptions,
160}
161
162#[napi]
163impl Task for ResolveTask {
164    type JsValue = ParseResult;
165    type Output = ParseResult;
166
167    fn compute(&mut self) -> napi::Result<Self::Output> {
168        let source_text = mem::take(&mut self.source_text);
169        Ok(parse_with_return(&self.filename, source_text, &self.options))
170    }
171
172    fn resolve(&mut self, _: napi::Env, result: Self::Output) -> napi::Result<Self::JsValue> {
173        Ok(result)
174    }
175}
176
177/// Parse asynchronously.
178///
179/// Note: This function can be slower than `parseSync` due to the overhead of spawning a thread.
180#[napi]
181pub fn parse_async(
182    filename: String,
183    source_text: String,
184    options: Option<ParserOptions>,
185) -> AsyncTask<ResolveTask> {
186    let options = options.unwrap_or_default();
187    AsyncTask::new(ResolveTask { filename, source_text, options })
188}