diff --git a/Cargo.lock b/Cargo.lock index 7f3b1dc279f39..73fa2ad27307b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1784,6 +1784,15 @@ dependencies = [ "rustc-hash", ] +[[package]] +name = "oxc_napi" +version = "0.39.0" +dependencies = [ + "napi", + "napi-derive", + "oxc_diagnostics", +] + [[package]] name = "oxc_parser" version = "0.39.0" @@ -1815,6 +1824,7 @@ dependencies = [ "napi-build", "napi-derive", "oxc", + "oxc_napi", "rustc-hash", "serde_json", ] @@ -2015,6 +2025,7 @@ dependencies = [ "napi-build", "napi-derive", "oxc", + "oxc_napi", "oxc_sourcemap", "rustc-hash", ] diff --git a/Cargo.toml b/Cargo.toml index 6a7ad404b44b0..a59e45f327dde 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -91,6 +91,7 @@ oxc_estree = { version = "0.39.0", path = "crates/oxc_estree" } oxc_isolated_declarations = { version = "0.39.0", path = "crates/oxc_isolated_declarations" } oxc_mangler = { version = "0.39.0", path = "crates/oxc_mangler" } oxc_minifier = { version = "0.39.0", path = "crates/oxc_minifier" } +oxc_napi = { version = "0.39.0", path = "crates/oxc_napi" } oxc_parser = { version = "0.39.0", path = "crates/oxc_parser" } oxc_regular_expression = { version = "0.39.0", path = "crates/oxc_regular_expression" } oxc_semantic = { version = "0.39.0", path = "crates/oxc_semantic" } diff --git a/crates/oxc_diagnostics/README.md b/crates/oxc_diagnostics/README.md new file mode 100644 index 0000000000000..de793366d8637 --- /dev/null +++ b/crates/oxc_diagnostics/README.md @@ -0,0 +1,2 @@ +Feature gating napi in other crates will lead to symbol not found when compiling, +use this crate for common napi interfaces. diff --git a/crates/oxc_napi/Cargo.toml b/crates/oxc_napi/Cargo.toml new file mode 100644 index 0000000000000..206f9e4797902 --- /dev/null +++ b/crates/oxc_napi/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "oxc_napi" +version = "0.39.0" +authors.workspace = true +categories.workspace = true +edition.workspace = true +homepage.workspace = true +include = ["/src"] +keywords.workspace = true +license.workspace = true +publish = true +repository.workspace = true +rust-version.workspace = true +description.workspace = true + +[lints] +workspace = true + +[lib] +doctest = false + +[dependencies] +napi = { workspace = true } +napi-derive = { workspace = true } +oxc_diagnostics = { workspace = true } + +[package.metadata.cargo-shear] +ignored = ["napi"] diff --git a/crates/oxc_napi/src/lib.rs b/crates/oxc_napi/src/lib.rs new file mode 100644 index 0000000000000..e05e785bb1fa9 --- /dev/null +++ b/crates/oxc_napi/src/lib.rs @@ -0,0 +1,68 @@ +use napi_derive::napi; + +use oxc_diagnostics::{LabeledSpan, OxcDiagnostic}; + +#[napi(object)] +pub struct Error { + pub severity: Severity, + pub message: String, + pub labels: Vec, + pub help_message: Option, +} + +impl Error { + pub fn new(message: String) -> Self { + Self { severity: Severity::Error, message, labels: vec![], help_message: None } + } +} + +impl From for Error { + fn from(diagnostic: OxcDiagnostic) -> Self { + let labels = diagnostic + .labels + .as_ref() + .map(|labels| labels.iter().map(ErrorLabel::from).collect::>()) + .unwrap_or_default(); + Self { + severity: Severity::from(diagnostic.severity), + message: diagnostic.message.to_string(), + labels, + help_message: diagnostic.help.as_ref().map(ToString::to_string), + } + } +} + +#[napi(object)] +pub struct ErrorLabel { + pub message: Option, + pub start: u32, + pub end: u32, +} + +impl From<&LabeledSpan> for ErrorLabel { + #[allow(clippy::cast_possible_truncation)] + fn from(label: &LabeledSpan) -> Self { + Self { + message: label.label().map(ToString::to_string), + start: label.offset() as u32, + end: (label.offset() + label.len()) as u32, + } + } +} + +#[napi(string_enum)] +pub enum Severity { + Error, + Warning, + Advice, +} + +impl From for Severity { + fn from(value: oxc_diagnostics::Severity) -> Self { + match value { + oxc_diagnostics::Severity::Error => Self::Error, + oxc_diagnostics::Severity::Warning => Self::Warning, + oxc_diagnostics::Severity::Advice => Self::Advice, + } + } +} diff --git a/napi/parser/Cargo.toml b/napi/parser/Cargo.toml index e5f8526815ab3..f166d346fd17d 100644 --- a/napi/parser/Cargo.toml +++ b/napi/parser/Cargo.toml @@ -22,6 +22,7 @@ doctest = false [dependencies] oxc = { workspace = true, features = ["serialize"] } +oxc_napi = { workspace = true } rustc-hash = { workspace = true } serde_json = { workspace = true } diff --git a/napi/parser/bindings.js b/napi/parser/bindings.js index e2a65710d14fc..2865f9494222e 100644 --- a/napi/parser/bindings.js +++ b/napi/parser/bindings.js @@ -368,3 +368,4 @@ module.exports.ImportNameKind = nativeBinding.ImportNameKind module.exports.parseAsync = nativeBinding.parseAsync module.exports.parseSync = nativeBinding.parseSync module.exports.parseWithoutReturn = nativeBinding.parseWithoutReturn +module.exports.Severity = nativeBinding.Severity diff --git a/napi/parser/index.d.ts b/napi/parser/index.d.ts index 5cc9dfafd328b..45cafbe02e3d2 100644 --- a/napi/parser/index.d.ts +++ b/napi/parser/index.d.ts @@ -26,6 +26,19 @@ export interface EcmaScriptModule { importMetas: Array } +export interface Error { + severity: Severity + message: string + labels: Array + helpMessage?: string +} + +export interface ErrorLabel { + message?: string + start: number + end: number +} + export interface ExportExportName { kind: ExportExportNameKind name?: string @@ -106,7 +119,7 @@ export interface ParseResult { program: import("@oxc-project/types").Program module: EcmaScriptModule comments: Array - errors: Array + errors: Array } export interface ParserOptions { @@ -135,6 +148,12 @@ export declare function parseSync(filename: string, sourceText: string, options? */ export declare function parseWithoutReturn(filename: string, sourceText: string, options?: ParserOptions | undefined | null): void +export declare const enum Severity { + Error = 'Error', + Warning = 'Warning', + Advice = 'Advice' +} + export interface Span { start: number end: number diff --git a/napi/parser/src/lib.rs b/napi/parser/src/lib.rs index 459a9929720b1..ab028921eb3ae 100644 --- a/napi/parser/src/lib.rs +++ b/napi/parser/src/lib.rs @@ -5,18 +5,16 @@ mod convert; mod types; -use std::sync::Arc; - use napi::{bindgen_prelude::AsyncTask, Task}; use napi_derive::napi; use oxc::{ allocator::Allocator, ast::CommentKind, - diagnostics::{Error, NamedSource}, parser::{ParseOptions, Parser, ParserReturn}, span::SourceType, }; +use oxc_napi::Error; pub use crate::types::{Comment, EcmaScriptModule, ParseResult, ParserOptions}; @@ -70,16 +68,7 @@ fn parse_with_return(filename: &str, source_text: &str, options: &ParserOptions) 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 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))) - .map(|error| format!("{error:?}")) - .collect() - }; + let errors = ret.errors.into_iter().map(Error::from).collect::>(); let comments = ret .program diff --git a/napi/parser/src/types.rs b/napi/parser/src/types.rs index a3c309fcef756..9b80619972536 100644 --- a/napi/parser/src/types.rs +++ b/napi/parser/src/types.rs @@ -1,5 +1,7 @@ use napi_derive::napi; +use oxc_napi::Error; + #[napi(object)] #[derive(Default)] pub struct ParserOptions { @@ -26,7 +28,7 @@ pub struct ParseResult { pub program: String, pub module: EcmaScriptModule, pub comments: Vec, - pub errors: Vec, + pub errors: Vec, } #[napi(object)] diff --git a/napi/parser/test/parse.test.ts b/napi/parser/test/parse.test.ts index 8a4ccac079f8a..1afc623dc90e3 100644 --- a/napi/parser/test/parse.test.ts +++ b/napi/parser/test/parse.test.ts @@ -30,3 +30,23 @@ describe('parse', () => { expect(ret).toEqual(ret2); }); }); + +describe('error', () => { + const code = 'asdf asdf'; + + it('returns structured error', () => { + const ret = parseSync('test.js', code); + expect(ret.errors.length).toBe(1); + expect(ret.errors[0]).toStrictEqual({ + 'helpMessage': 'Try insert a semicolon here', + 'labels': [ + { + 'end': 4, + 'start': 4, + }, + ], + 'message': 'Expected a semicolon or an implicit semicolon after a statement, but found none', + 'severity': 'Error', + }); + }); +}); diff --git a/napi/transform/Cargo.toml b/napi/transform/Cargo.toml index 6a7cb38181427..49b7b33e5d057 100644 --- a/napi/transform/Cargo.toml +++ b/napi/transform/Cargo.toml @@ -22,6 +22,7 @@ doctest = false [dependencies] oxc = { workspace = true, features = ["full"] } +oxc_napi = { workspace = true } oxc_sourcemap = { workspace = true, features = ["napi", "rayon"] } rustc-hash = { workspace = true } diff --git a/napi/transform/index.d.ts b/napi/transform/index.d.ts index 155b71a0a1d13..d74148c2f6466 100644 --- a/napi/transform/index.d.ts +++ b/napi/transform/index.d.ts @@ -20,6 +20,19 @@ export interface CompilerAssumptions { setPublicClassFields?: boolean } +export interface Error { + severity: Severity + message: string + labels: Array + helpMessage?: string +} + +export interface ErrorLabel { + message?: string + start: number + end: number +} + export interface Es2015Options { /** Transform arrow functions into function expressions. */ arrowFunction?: ArrowFunctionsOptions @@ -44,7 +57,7 @@ export interface IsolatedDeclarationsOptions { export interface IsolatedDeclarationsResult { code: string map?: SourceMap - errors: Array + errors: Array } /** @@ -158,6 +171,12 @@ export interface ReactRefreshOptions { emitFullSignatures?: boolean } +export declare const enum Severity { + Error = 'Error', + Warning = 'Warning', + Advice = 'Advice' +} + export interface SourceMap { file?: string mappings: string @@ -271,7 +290,7 @@ export interface TransformResult { * transformed code may still be available even if there are errors in this * list. */ - errors: Array + errors: Array } export interface TypeScriptOptions { diff --git a/napi/transform/index.js b/napi/transform/index.js index e1092b640ae6c..223db4c0b0f57 100644 --- a/napi/transform/index.js +++ b/napi/transform/index.js @@ -362,4 +362,5 @@ if (!nativeBinding) { } module.exports.isolatedDeclaration = nativeBinding.isolatedDeclaration +module.exports.Severity = nativeBinding.Severity module.exports.transform = nativeBinding.transform diff --git a/napi/transform/src/errors.rs b/napi/transform/src/errors.rs deleted file mode 100644 index 15d76b4ae7a43..0000000000000 --- a/napi/transform/src/errors.rs +++ /dev/null @@ -1,41 +0,0 @@ -use std::{path::Path, sync::Arc}; - -use oxc::{ - diagnostics::{Error, NamedSource, OxcDiagnostic}, - span::SourceType, -}; - -pub fn wrap_diagnostics( - filename: &Path, - source_type: SourceType, - source_text: &str, - errors: Vec, -) -> Vec { - if errors.is_empty() { - return vec![]; - } - let source = { - let lang = match (source_type.is_javascript(), source_type.is_jsx()) { - (true, false) => "JavaScript", - (true, true) => "JSX", - (false, true) => "TypeScript React", - (false, false) => { - if source_type.is_typescript_definition() { - "TypeScript Declaration" - } else { - "TypeScript" - } - } - }; - - let ns = NamedSource::new(filename.to_string_lossy(), source_text.to_string()) - .with_language(lang); - Arc::new(ns) - }; - - errors - .into_iter() - .map(move |diagnostic| Error::from(diagnostic).with_source_code(Arc::clone(&source))) - .map(|error| format!("{error:?}")) - .collect() -} diff --git a/napi/transform/src/isolated_declaration.rs b/napi/transform/src/isolated_declaration.rs index 49f33cd8b2437..6d985b2d97bad 100644 --- a/napi/transform/src/isolated_declaration.rs +++ b/napi/transform/src/isolated_declaration.rs @@ -9,16 +9,14 @@ use oxc::{ parser::Parser, span::SourceType, }; - -use crate::errors::wrap_diagnostics; - +use oxc_napi::Error; use oxc_sourcemap::napi::SourceMap; #[napi(object)] pub struct IsolatedDeclarationsResult { pub code: String, pub map: Option, - pub errors: Vec, + pub errors: Vec, } #[napi(object)] @@ -72,8 +70,7 @@ pub fn isolated_declaration( .with_options(CodegenOptions { source_map_path, ..CodegenOptions::default() }) .build(&transformed_ret.program); - let errors = ret.errors.into_iter().chain(transformed_ret.errors).collect(); - let errors = wrap_diagnostics(source_path, source_type, &source_text, errors); + let errors = ret.errors.into_iter().chain(transformed_ret.errors).map(Error::from).collect(); IsolatedDeclarationsResult { code: codegen_ret.code, diff --git a/napi/transform/src/lib.rs b/napi/transform/src/lib.rs index eff0ebcfd8893..cdd4925245b62 100644 --- a/napi/transform/src/lib.rs +++ b/napi/transform/src/lib.rs @@ -1,5 +1,3 @@ -mod errors; - mod isolated_declaration; pub use isolated_declaration::*; diff --git a/napi/transform/src/transformer.rs b/napi/transform/src/transformer.rs index 30af9f9753783..e1e5e04300e04 100644 --- a/napi/transform/src/transformer.rs +++ b/napi/transform/src/transformer.rs @@ -17,9 +17,10 @@ use oxc::{ }, CompilerInterface, }; +use oxc_napi::Error; use oxc_sourcemap::napi::SourceMap; -use crate::{errors::wrap_diagnostics, IsolatedDeclarationsOptions}; +use crate::IsolatedDeclarationsOptions; #[derive(Default)] #[napi(object)] @@ -54,7 +55,7 @@ pub struct TransformResult { /// Oxc's parser recovers from common syntax errors, meaning that /// transformed code may still be available even if there are errors in this /// list. - pub errors: Vec, + pub errors: Vec, } /// Options for transforming a JavaScript or TypeScript file. @@ -543,7 +544,7 @@ pub fn transform( Some("tsx") => SourceType::tsx(), Some(lang) => { return TransformResult { - errors: vec![format!("Incorrect lang '{lang}'")], + errors: vec![Error::new(format!("Incorrect lang '{lang}'"))], ..Default::default() } } @@ -563,7 +564,7 @@ pub fn transform( Ok(compiler) => compiler, Err(errors) => { return TransformResult { - errors: wrap_diagnostics(source_path, source_type, &source_text, errors), + errors: errors.into_iter().map(Error::from).collect(), ..Default::default() } } @@ -576,6 +577,6 @@ pub fn transform( map: compiler.printed_sourcemap, declaration: compiler.declaration, declaration_map: compiler.declaration_map, - errors: wrap_diagnostics(source_path, source_type, &source_text, compiler.errors), + errors: compiler.errors.into_iter().map(Error::from).collect(), } } diff --git a/tasks/rulegen/src/main.rs b/tasks/rulegen/src/main.rs index 0b49997228633..7d7eecd10ac5c 100644 --- a/tasks/rulegen/src/main.rs +++ b/tasks/rulegen/src/main.rs @@ -808,7 +808,7 @@ fn add_rules_entry(ctx: &Context, rule_kind: RuleKind) -> Result<(), Box Result<(), Box &ctx.kebab_rule_name { + if plugin == mod_name && rule > ctx.kebab_rule_name.as_str() { let def = format!("{plugin}::{rule}"); rules.find(&def) } else {