From 40792b4440f2b5dbe99ea01c97cac55c8d0d4856 Mon Sep 17 00:00:00 2001 From: Boshen <1430279+Boshen@users.noreply.github.com> Date: Tue, 3 Dec 2024 12:09:48 +0000 Subject: [PATCH] feat(napi/parser): change parse API to accept mandatory `filename` and optional `lang` (#7605) --- crates/oxc/src/napi/parse.rs | 9 ++++- napi/parser/index.d.ts | 10 ++--- napi/parser/src/lib.rs | 62 ++++++++++++++++++++----------- napi/parser/test/esm.test.ts | 6 +-- napi/parser/test/parse.test.ts | 12 ++++-- napi/parser/test/parser.test-d.ts | 4 +- 6 files changed, 67 insertions(+), 36 deletions(-) diff --git a/crates/oxc/src/napi/parse.rs b/crates/oxc/src/napi/parse.rs index f94496636cb5e..9fc35d8c3f07b 100644 --- a/crates/oxc/src/napi/parse.rs +++ b/crates/oxc/src/napi/parse.rs @@ -5,13 +5,16 @@ use rustc_hash::FxHashMap; use oxc_span::Span; use oxc_syntax::module_record::{self, ModuleRecord}; -/// Babel Parser Options #[napi(object)] #[derive(Default)] pub struct ParserOptions { #[napi(ts_type = "'script' | 'module' | 'unambiguous' | undefined")] pub source_type: Option, - pub source_filename: Option, + + /// Treat the source text as `js`, `jsx`, `ts`, or `tsx`. + #[napi(ts_type = "'js' | 'jsx' | 'ts' | 'tsx'")] + pub lang: Option, + /// Emit `ParenthesizedExpression` in AST. /// /// If this option is true, parenthesized expressions are represented by @@ -23,6 +26,7 @@ pub struct ParserOptions { } #[napi(object)] +#[derive(Default)] pub struct ParseResult { #[napi(ts_type = "import(\"@oxc-project/types\").Program")] pub program: String, @@ -41,6 +45,7 @@ pub struct Comment { } #[napi(object)] +#[derive(Default)] pub struct EcmaScriptModule { /// Import Statements. pub static_imports: Vec, diff --git a/napi/parser/index.d.ts b/napi/parser/index.d.ts index 3112c64280e33..ab2164ce2c59e 100644 --- a/napi/parser/index.d.ts +++ b/napi/parser/index.d.ts @@ -136,7 +136,7 @@ export declare const enum ImportNameKind { * * Note: This function can be slower than `parseSync` due to the overhead of spawning a thread. */ -export declare function parseAsync(sourceText: string, options?: ParserOptions | undefined | null): Promise +export declare function parseAsync(filename: string, sourceText: string, options?: ParserOptions | undefined | null): Promise export interface ParseResult { program: import("@oxc-project/types").Program @@ -145,10 +145,10 @@ export interface ParseResult { errors: Array } -/** Babel Parser Options */ export interface ParserOptions { sourceType?: 'script' | 'module' | 'unambiguous' | undefined - sourceFilename?: string + /** Treat the source text as `js`, `jsx`, `ts`, or `tsx`. */ + lang?: 'js' | 'jsx' | 'ts' | 'tsx' /** * Emit `ParenthesizedExpression` in AST. * @@ -162,14 +162,14 @@ export interface ParserOptions { } /** Parse synchronously. */ -export declare function parseSync(sourceText: string, options?: ParserOptions | undefined | null): ParseResult +export declare function parseSync(filename: string, sourceText: string, options?: ParserOptions | undefined | null): ParseResult /** * Parse without returning anything. * * This is for benchmark purposes such as measuring napi communication overhead. */ -export declare function parseWithoutReturn(sourceText: string, options?: ParserOptions | undefined | null): void +export declare function parseWithoutReturn(filename: string, sourceText: string, options?: ParserOptions | undefined | null): void export interface StaticExport { start: number diff --git a/napi/parser/src/lib.rs b/napi/parser/src/lib.rs index 071afddf852ee..016eaf9dec8eb 100644 --- a/napi/parser/src/lib.rs +++ b/napi/parser/src/lib.rs @@ -16,21 +16,31 @@ use oxc::{ span::SourceType, }; +fn get_source_type(filename: &str, options: &ParserOptions) -> SourceType { + match options.lang.as_deref() { + Some("js") => SourceType::mjs(), + Some("jsx") => SourceType::jsx(), + Some("ts") => SourceType::ts(), + Some("tsx") => SourceType::tsx(), + _ => { + let mut source_type = SourceType::from_path(filename).unwrap_or_default(); + // Force `script` or `module` + match options.source_type.as_deref() { + Some("script") => source_type = source_type.with_script(true), + Some("module") => source_type = source_type.with_module(true), + _ => {} + } + source_type + } + } +} + fn parse<'a>( allocator: &'a Allocator, + source_type: SourceType, source_text: &'a str, options: &ParserOptions, ) -> ParserReturn<'a> { - let source_type = options - .source_filename - .as_ref() - .and_then(|name| SourceType::from_path(name).ok()) - .unwrap_or_default(); - let source_type = match options.source_type.as_deref() { - Some("script") => source_type.with_script(true), - Some("module") => source_type.with_module(true), - _ => source_type, - }; Parser::new(allocator, source_text, source_type) .with_options(ParseOptions { preserve_parens: options.preserve_parens.unwrap_or(true), @@ -43,22 +53,23 @@ fn parse<'a>( /// /// This is for benchmark purposes such as measuring napi communication overhead. #[napi] -pub fn parse_without_return(source_text: String, options: Option) { +pub fn parse_without_return(filename: String, source_text: String, options: Option) { let options = options.unwrap_or_default(); let allocator = Allocator::default(); - parse(&allocator, &source_text, &options); + let source_type = get_source_type(&filename, &options); + parse(&allocator, source_type, &source_text, &options); } -fn parse_with_return(source_text: &str, options: &ParserOptions) -> ParseResult { +fn parse_with_return(filename: &str, source_text: &str, options: &ParserOptions) -> ParseResult { let allocator = Allocator::default(); - let ret = parse(&allocator, source_text, options); + let source_type = get_source_type(filename, options); + let ret = parse(&allocator, source_type, source_text, options); let program = serde_json::to_string(&ret.program).unwrap(); let errors = if ret.errors.is_empty() { vec![] } else { - let file_name = options.source_filename.clone().unwrap_or_default(); - let source = Arc::new(NamedSource::new(file_name, source_text.to_string())); + let source = Arc::new(NamedSource::new(filename, source_text.to_string())); ret.errors .into_iter() .map(|diagnostic| Error::from(diagnostic).with_source_code(Arc::clone(&source))) @@ -87,12 +98,17 @@ fn parse_with_return(source_text: &str, options: &ParserOptions) -> ParseResult /// Parse synchronously. #[napi] -pub fn parse_sync(source_text: String, options: Option) -> ParseResult { +pub fn parse_sync( + filename: String, + source_text: String, + options: Option, +) -> ParseResult { let options = options.unwrap_or_default(); - parse_with_return(&source_text, &options) + parse_with_return(&filename, &source_text, &options) } pub struct ResolveTask { + filename: String, source_text: String, options: ParserOptions, } @@ -103,7 +119,7 @@ impl Task for ResolveTask { type Output = ParseResult; fn compute(&mut self) -> napi::Result { - Ok(parse_with_return(&self.source_text, &self.options)) + Ok(parse_with_return(&self.filename, &self.source_text, &self.options)) } fn resolve(&mut self, _: napi::Env, result: Self::Output) -> napi::Result { @@ -115,7 +131,11 @@ impl Task for ResolveTask { /// /// Note: This function can be slower than `parseSync` due to the overhead of spawning a thread. #[napi] -pub fn parse_async(source_text: String, options: Option) -> AsyncTask { +pub fn parse_async( + filename: String, + source_text: String, + options: Option, +) -> AsyncTask { let options = options.unwrap_or_default(); - AsyncTask::new(ResolveTask { source_text, options }) + AsyncTask::new(ResolveTask { filename, source_text, options }) } diff --git a/napi/parser/test/esm.test.ts b/napi/parser/test/esm.test.ts index b4a1eef200ed4..2537fe74dc072 100644 --- a/napi/parser/test/esm.test.ts +++ b/napi/parser/test/esm.test.ts @@ -1,6 +1,6 @@ -import { describe, expect, it, test } from 'vitest'; +import { describe, expect, test } from 'vitest'; -import * as oxc from '../index.js'; +import { parseSync } from '../index.js'; describe('esm', () => { // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#syntax @@ -48,7 +48,7 @@ export { default as name1 } from "module-name"; `.split('\n').map((s) => s.trim()).filter(Boolean); test.each(code)('%s', (s) => { - const ret = oxc.parseSync(s, { sourceFilename: 'test.ts' }); + const ret = parseSync('test.js', s); expect(ret.program.body.length).toBeGreaterThan(0); expect(ret.errors.length).toBe(0); expect(JSON.stringify(ret.module, null, 2)).toMatchSnapshot(); diff --git a/napi/parser/test/parse.test.ts b/napi/parser/test/parse.test.ts index 909094c321033..8a4ccac079f8a 100644 --- a/napi/parser/test/parse.test.ts +++ b/napi/parser/test/parse.test.ts @@ -1,12 +1,18 @@ import { describe, expect, it } from 'vitest'; -import * as oxc from '../index.js'; +import { parseAsync, parseSync } from '../index.js'; describe('parse', () => { const code = '/* comment */ foo'; + it('uses the `lang` option', () => { + const ret = parseSync('test.vue', code, { lang: 'ts' }); + expect(ret.program.body.length).toBe(1); + expect(ret.errors.length).toBe(0); + }); + it('matches output', async () => { - const ret = oxc.parseSync(code); + const ret = await parseAsync('test.js', code); expect(ret.program.body.length).toBe(1); expect(ret.errors.length).toBe(0); expect(ret.comments.length).toBe(1); @@ -20,7 +26,7 @@ describe('parse', () => { }); expect(code.substring(comment.start, comment.end)).toBe('/*' + comment.value + '*/'); - const ret2 = await oxc.parseAsync(code); + const ret2 = await parseAsync('test.js', code); expect(ret).toEqual(ret2); }); }); diff --git a/napi/parser/test/parser.test-d.ts b/napi/parser/test/parser.test-d.ts index 74bff1ac9711d..45d3dbf141ede 100644 --- a/napi/parser/test/parser.test-d.ts +++ b/napi/parser/test/parser.test-d.ts @@ -1,13 +1,13 @@ import { assertType, describe, it } from 'vitest'; import type { Statement } from '../index'; -import * as oxc from '../index'; +import { parseSync } from '../index'; describe('parse', () => { const code = '/* comment */ foo'; it('checks type', async () => { - const ret = oxc.parseSync(code); + const ret = parseSync('test.js', code); assertType(ret.program.body[0]); }); });