oxc_parser_napi/
lib.rs

1// Napi value need to be passed as value
2#![expect(clippy::needless_pass_by_value)]
3
4use std::mem;
5
6use napi::{Task, bindgen_prelude::AsyncTask};
7use napi_derive::napi;
8
9use oxc::{
10    allocator::Allocator,
11    ast::CommentKind,
12    ast_visit::utf8_to_utf16::Utf8ToUtf16,
13    parser::{ParseOptions, Parser, ParserReturn},
14    semantic::SemanticBuilder,
15    span::SourceType,
16};
17use oxc_napi::OxcError;
18
19mod convert;
20mod raw_transfer;
21mod raw_transfer_types;
22mod types;
23pub use raw_transfer::{get_buffer_offset, parse_sync_raw, raw_transfer_supported};
24pub use types::{Comment, EcmaScriptModule, ParseResult, ParserOptions};
25
26mod generated {
27    // Note: We intentionally don't import `generated/derive_estree.rs`. It's not needed.
28    #[cfg(debug_assertions)]
29    pub mod assert_layouts;
30}
31
32#[derive(Clone, Copy, PartialEq, Eq)]
33enum AstType {
34    JavaScript,
35    TypeScript,
36}
37
38fn get_source_and_ast_type(filename: &str, options: &ParserOptions) -> (SourceType, AstType) {
39    let source_type = match options.lang.as_deref() {
40        Some("js") => SourceType::mjs(),
41        Some("jsx") => SourceType::jsx(),
42        Some("ts") => SourceType::ts(),
43        Some("tsx") => SourceType::tsx(),
44        _ => {
45            let mut source_type = SourceType::from_path(filename).unwrap_or_default();
46            // Force `script` or `module`
47            match options.source_type.as_deref() {
48                Some("script") => source_type = source_type.with_script(true),
49                Some("module") => source_type = source_type.with_module(true),
50                _ => {}
51            }
52            source_type
53        }
54    };
55
56    let ast_type = match options.ast_type.as_deref() {
57        Some("js") => AstType::JavaScript,
58        Some("ts") => AstType::TypeScript,
59        _ => {
60            if source_type.is_javascript() {
61                AstType::JavaScript
62            } else {
63                AstType::TypeScript
64            }
65        }
66    };
67
68    (source_type, ast_type)
69}
70
71fn parse<'a>(
72    allocator: &'a Allocator,
73    source_type: SourceType,
74    source_text: &'a str,
75    options: &ParserOptions,
76) -> ParserReturn<'a> {
77    Parser::new(allocator, source_text, source_type)
78        .with_options(ParseOptions {
79            preserve_parens: options.preserve_parens.unwrap_or(true),
80            ..ParseOptions::default()
81        })
82        .parse()
83}
84
85fn parse_with_return(filename: &str, source_text: String, options: &ParserOptions) -> ParseResult {
86    let allocator = Allocator::default();
87    let (source_type, ast_type) = get_source_and_ast_type(filename, options);
88    let ret = parse(&allocator, source_type, &source_text, options);
89
90    let mut program = ret.program;
91    let mut module_record = ret.module_record;
92    let mut errors = ret.errors.into_iter().map(OxcError::from).collect::<Vec<_>>();
93
94    if options.show_semantic_errors == Some(true) {
95        let semantic_ret = SemanticBuilder::new().with_check_syntax_error(true).build(&program);
96        errors.extend(semantic_ret.errors.into_iter().map(OxcError::from));
97    }
98
99    // Convert spans to UTF-16
100    let span_converter = Utf8ToUtf16::new(&source_text);
101    span_converter.convert_program(&mut program);
102
103    // Convert comments
104    let mut offset_converter = span_converter.converter();
105    let comments = program
106        .comments
107        .iter()
108        .map(|comment| {
109            let value = comment.content_span().source_text(&source_text).to_string();
110            let mut span = comment.span;
111            if let Some(converter) = offset_converter.as_mut() {
112                converter.convert_span(&mut span);
113            }
114
115            Comment {
116                r#type: match comment.kind {
117                    CommentKind::Line => String::from("Line"),
118                    CommentKind::Block => String::from("Block"),
119                },
120                value,
121                start: span.start,
122                end: span.end,
123            }
124        })
125        .collect::<Vec<_>>();
126
127    // Convert spans in module record to UTF-16
128    span_converter.convert_module_record(&mut module_record);
129
130    // Convert spans in errors to UTF-16
131    if let Some(mut converter) = span_converter.converter() {
132        for error in &mut errors {
133            for label in &mut error.labels {
134                converter.convert_offset(&mut label.start);
135                converter.convert_offset(&mut label.end);
136            }
137        }
138    }
139
140    let program = match ast_type {
141        AstType::JavaScript => program.to_estree_js_json(),
142        AstType::TypeScript => program.to_estree_ts_json(),
143    };
144
145    let module = EcmaScriptModule::from(&module_record);
146
147    ParseResult { program, module, comments, errors }
148}
149
150/// Parse synchronously.
151#[napi]
152pub fn parse_sync(
153    filename: String,
154    source_text: String,
155    options: Option<ParserOptions>,
156) -> ParseResult {
157    let options = options.unwrap_or_default();
158    parse_with_return(&filename, source_text, &options)
159}
160
161pub struct ResolveTask {
162    filename: String,
163    source_text: String,
164    options: ParserOptions,
165}
166
167#[napi]
168impl Task for ResolveTask {
169    type JsValue = ParseResult;
170    type Output = ParseResult;
171
172    fn compute(&mut self) -> napi::Result<Self::Output> {
173        let source_text = mem::take(&mut self.source_text);
174        Ok(parse_with_return(&self.filename, source_text, &self.options))
175    }
176
177    fn resolve(&mut self, _: napi::Env, result: Self::Output) -> napi::Result<Self::JsValue> {
178        Ok(result)
179    }
180}
181
182/// Parse asynchronously.
183///
184/// Note: This function can be slower than `parseSync` due to the overhead of spawning a thread.
185#[napi]
186pub fn parse_async(
187    filename: String,
188    source_text: String,
189    options: Option<ParserOptions>,
190) -> AsyncTask<ResolveTask> {
191    let options = options.unwrap_or_default();
192    AsyncTask::new(ResolveTask { filename, source_text, options })
193}