mirror of
https://github.com/danbulant/oxc
synced 2026-05-21 05:08:45 +00:00
feat(napi/transform,napi/parser): return structured error object (#7724)
closes #7261
This commit is contained in:
parent
02f9903211
commit
85eec3c82e
19 changed files with 191 additions and 73 deletions
11
Cargo.lock
generated
11
Cargo.lock
generated
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
2
crates/oxc_diagnostics/README.md
Normal file
2
crates/oxc_diagnostics/README.md
Normal file
|
|
@ -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.
|
||||
28
crates/oxc_napi/Cargo.toml
Normal file
28
crates/oxc_napi/Cargo.toml
Normal file
|
|
@ -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"]
|
||||
68
crates/oxc_napi/src/lib.rs
Normal file
68
crates/oxc_napi/src/lib.rs
Normal file
|
|
@ -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<ErrorLabel>,
|
||||
pub help_message: Option<String>,
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub fn new(message: String) -> Self {
|
||||
Self { severity: Severity::Error, message, labels: vec![], help_message: None }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OxcDiagnostic> for Error {
|
||||
fn from(diagnostic: OxcDiagnostic) -> Self {
|
||||
let labels = diagnostic
|
||||
.labels
|
||||
.as_ref()
|
||||
.map(|labels| labels.iter().map(ErrorLabel::from).collect::<Vec<_>>())
|
||||
.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<String>,
|
||||
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<oxc_diagnostics::Severity> 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@ doctest = false
|
|||
|
||||
[dependencies]
|
||||
oxc = { workspace = true, features = ["serialize"] }
|
||||
oxc_napi = { workspace = true }
|
||||
|
||||
rustc-hash = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
21
napi/parser/index.d.ts
vendored
21
napi/parser/index.d.ts
vendored
|
|
@ -26,6 +26,19 @@ export interface EcmaScriptModule {
|
|||
importMetas: Array<Span>
|
||||
}
|
||||
|
||||
export interface Error {
|
||||
severity: Severity
|
||||
message: string
|
||||
labels: Array<ErrorLabel>
|
||||
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<Comment>
|
||||
errors: Array<string>
|
||||
errors: Array<Error>
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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::<Vec<_>>();
|
||||
|
||||
let comments = ret
|
||||
.program
|
||||
|
|
|
|||
|
|
@ -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<Comment>,
|
||||
pub errors: Vec<String>,
|
||||
pub errors: Vec<Error>,
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
23
napi/transform/index.d.ts
vendored
23
napi/transform/index.d.ts
vendored
|
|
@ -20,6 +20,19 @@ export interface CompilerAssumptions {
|
|||
setPublicClassFields?: boolean
|
||||
}
|
||||
|
||||
export interface Error {
|
||||
severity: Severity
|
||||
message: string
|
||||
labels: Array<ErrorLabel>
|
||||
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<string>
|
||||
errors: Array<Error>
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -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<string>
|
||||
errors: Array<Error>
|
||||
}
|
||||
|
||||
export interface TypeScriptOptions {
|
||||
|
|
|
|||
|
|
@ -362,4 +362,5 @@ if (!nativeBinding) {
|
|||
}
|
||||
|
||||
module.exports.isolatedDeclaration = nativeBinding.isolatedDeclaration
|
||||
module.exports.Severity = nativeBinding.Severity
|
||||
module.exports.transform = nativeBinding.transform
|
||||
|
|
|
|||
|
|
@ -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<OxcDiagnostic>,
|
||||
) -> Vec<String> {
|
||||
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()
|
||||
}
|
||||
|
|
@ -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<SourceMap>,
|
||||
pub errors: Vec<String>,
|
||||
pub errors: Vec<Error>,
|
||||
}
|
||||
|
||||
#[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,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
mod errors;
|
||||
|
||||
mod isolated_declaration;
|
||||
pub use isolated_declaration::*;
|
||||
|
||||
|
|
|
|||
|
|
@ -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<String>,
|
||||
pub errors: Vec<Error>,
|
||||
}
|
||||
|
||||
/// 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(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -808,7 +808,7 @@ fn add_rules_entry(ctx: &Context, rule_kind: RuleKind) -> Result<(), Box<dyn std
|
|||
let rule_mod_def_start = mod_rules
|
||||
.lines()
|
||||
.filter_map(|line| line.split_once("pub mod ").map(|(_, rest)| rest))
|
||||
.position(|rule_mod| rule_mod < &rule_mod_def)
|
||||
.position(|rule_mod| rule_mod < rule_mod_def.as_str())
|
||||
.map(|i| i + 1)
|
||||
.and_then(|i| rules[mod_start + i..].find("pub mod ").map(|j| i + j))
|
||||
.ok_or(format!(
|
||||
|
|
@ -830,7 +830,7 @@ fn add_rules_entry(ctx: &Context, rule_kind: RuleKind) -> Result<(), Box<dyn std
|
|||
.lines()
|
||||
.filter_map(|line| line.trim().split_once("::"))
|
||||
.find_map(|(plugin, rule)| {
|
||||
if plugin == mod_name && rule > &ctx.kebab_rule_name {
|
||||
if plugin == mod_name && rule > ctx.kebab_rule_name.as_str() {
|
||||
let def = format!("{plugin}::{rule}");
|
||||
rules.find(&def)
|
||||
} else {
|
||||
|
|
|
|||
Loading…
Reference in a new issue