mirror of
https://github.com/danbulant/oxc
synced 2026-05-25 04:42:10 +00:00
feat(transformer): report errors when options have unknown fields (#3322)
This commit is contained in:
parent
46cb5f97a0
commit
e2c6fe0cb1
9 changed files with 155 additions and 57 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -1678,7 +1678,6 @@ dependencies = [
|
|||
"oxc_tasks_common",
|
||||
"oxc_transformer",
|
||||
"pico-args",
|
||||
"serde_json",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -6,29 +6,52 @@ use serde::Deserialize;
|
|||
///
|
||||
/// See <https://babeljs.io/docs/assumptions>
|
||||
#[derive(Debug, Default, Clone, Copy, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct CompilerAssumptions {
|
||||
#[serde(default)]
|
||||
pub array_like_is_iterable: bool,
|
||||
#[serde(default)]
|
||||
pub constant_reexports: bool,
|
||||
#[serde(default)]
|
||||
pub constant_super: bool,
|
||||
#[serde(default)]
|
||||
pub enumerable_module_meta: bool,
|
||||
#[serde(default)]
|
||||
pub ignore_function_length: bool,
|
||||
#[serde(default)]
|
||||
pub ignore_to_primitive_hint: bool,
|
||||
#[serde(default)]
|
||||
pub iterable_is_array: bool,
|
||||
#[serde(default)]
|
||||
pub mutable_template_object: bool,
|
||||
#[serde(default)]
|
||||
pub no_class_calls: bool,
|
||||
#[serde(default)]
|
||||
pub no_document_all: bool,
|
||||
#[serde(default)]
|
||||
pub no_incomplete_ns_import_detection: bool,
|
||||
#[serde(default)]
|
||||
pub no_new_arrows: bool,
|
||||
#[serde(default)]
|
||||
pub no_uninitialized_private_field_access: bool,
|
||||
#[serde(default)]
|
||||
pub object_rest_no_symbols: bool,
|
||||
#[serde(default)]
|
||||
pub private_fields_as_symbols: bool,
|
||||
#[serde(default)]
|
||||
pub private_fields_as_properties: bool,
|
||||
#[serde(default)]
|
||||
pub pure_getters: bool,
|
||||
#[serde(default)]
|
||||
pub set_class_methods: bool,
|
||||
#[serde(default)]
|
||||
pub set_computed_properties: bool,
|
||||
#[serde(default)]
|
||||
pub set_public_class_fields: bool,
|
||||
#[serde(default)]
|
||||
pub set_spread_properties: bool,
|
||||
#[serde(default)]
|
||||
pub skip_for_of_iterator_closing: bool,
|
||||
#[serde(default)]
|
||||
pub super_is_callable_constructor: bool,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use serde::Deserialize;
|
|||
use super::ArrowFunctionsOptions;
|
||||
|
||||
#[derive(Debug, Default, Clone, Deserialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
#[serde(default, rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct ES2015Options {
|
||||
#[serde(skip)]
|
||||
pub arrow_function: Option<ArrowFunctionsOptions>,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
use oxc_diagnostics::{Error, OxcDiagnostic};
|
||||
use serde::{de::DeserializeOwned, Deserialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{
|
||||
compiler_assumptions::CompilerAssumptions, es2015::ES2015Options, react::ReactOptions,
|
||||
compiler_assumptions::CompilerAssumptions,
|
||||
es2015::{ArrowFunctionsOptions, ES2015Options},
|
||||
react::ReactOptions,
|
||||
typescript::TypeScriptOptions,
|
||||
};
|
||||
|
||||
|
|
@ -33,58 +36,94 @@ pub struct TransformOptions {
|
|||
}
|
||||
|
||||
impl TransformOptions {
|
||||
/// # Panics
|
||||
/// Panics if the options are invalid.
|
||||
/// # Errors
|
||||
pub fn from_babel_options(options: &BabelOptions) -> serde_json::Result<Self> {
|
||||
///
|
||||
pub fn from_babel_options(options: &BabelOptions) -> Result<Self, Vec<Error>> {
|
||||
fn get_options<T: Default + DeserializeOwned>(
|
||||
value: Option<Value>,
|
||||
) -> serde_json::Result<T> {
|
||||
match value {
|
||||
Some(v) => serde_json::from_value::<T>(v),
|
||||
None => Ok(T::default()),
|
||||
}
|
||||
name: &str,
|
||||
babel_options: &BabelOptions,
|
||||
errors: &mut Vec<Error>,
|
||||
is_preset: bool,
|
||||
) -> T {
|
||||
let target = if is_preset {
|
||||
babel_options.get_preset(name)
|
||||
} else {
|
||||
babel_options.get_plugin(name)
|
||||
};
|
||||
target
|
||||
.and_then(|plugin_options| {
|
||||
plugin_options.and_then(|options| match serde_json::from_value::<T>(options) {
|
||||
Ok(options) => Some(options),
|
||||
Err(err) => {
|
||||
let kind_msg =
|
||||
if is_preset { format!("preset-{name}") } else { name.to_string() };
|
||||
errors.push(OxcDiagnostic::error(format!("{kind_msg}: {err}")).into());
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| T::default())
|
||||
}
|
||||
|
||||
let react = if let Some(options) = options.get_preset("react") {
|
||||
get_options::<ReactOptions>(options)?
|
||||
let mut errors = Vec::<Error>::new();
|
||||
|
||||
let react = if options.has_preset("react") {
|
||||
get_options::<ReactOptions>("react", options, &mut errors, true)
|
||||
} else {
|
||||
let jsx_plugin = options.get_plugin("transform-react-jsx");
|
||||
let jsx_development_plugin = options.get_plugin("transform-react-jsx-development");
|
||||
let has_jsx_plugin =
|
||||
jsx_plugin.as_ref().is_some() || jsx_development_plugin.as_ref().is_some();
|
||||
let mut react_options = jsx_plugin
|
||||
.map(get_options::<ReactOptions>)
|
||||
.or_else(|| jsx_development_plugin.map(get_options::<ReactOptions>))
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
react_options.development =
|
||||
options.get_plugin("transform-react-jsx-development").is_some();
|
||||
react_options.jsx_plugin = has_jsx_plugin;
|
||||
react_options.display_name_plugin =
|
||||
options.get_plugin("transform-react-display-name").is_some();
|
||||
react_options.jsx_self_plugin =
|
||||
options.get_plugin("transform-react-jsx-self").is_some();
|
||||
react_options.jsx_source_plugin =
|
||||
options.get_plugin("transform-react-jsx-source").is_some();
|
||||
let has_jsx_plugin = options.has_plugin("transform-react-jsx");
|
||||
let has_jsx_development_plugin = options.has_plugin("transform-react-jsx-development");
|
||||
let mut react_options = if has_jsx_plugin {
|
||||
get_options::<ReactOptions>("transform-react-jsx", options, &mut errors, false)
|
||||
} else {
|
||||
get_options::<ReactOptions>(
|
||||
"transform-react-jsx-development",
|
||||
options,
|
||||
&mut errors,
|
||||
false,
|
||||
)
|
||||
};
|
||||
react_options.development = options.has_plugin("transform-react-jsx-development");
|
||||
react_options.jsx_plugin = has_jsx_plugin || has_jsx_development_plugin;
|
||||
react_options.display_name_plugin = options.has_plugin("transform-react-display-name");
|
||||
react_options.jsx_self_plugin = options.has_plugin("transform-react-jsx-self");
|
||||
react_options.jsx_source_plugin = options.has_plugin("transform-react-jsx-source");
|
||||
react_options
|
||||
};
|
||||
|
||||
let es2015 = ES2015Options {
|
||||
arrow_function: options
|
||||
.get_plugin("transform-arrow-functions")
|
||||
.map(get_options)
|
||||
.transpose()?,
|
||||
arrow_function: options.has_plugin("transform-arrow-functions").then(|| {
|
||||
get_options::<ArrowFunctionsOptions>(
|
||||
"transform-arrow-functions",
|
||||
options,
|
||||
&mut errors,
|
||||
false,
|
||||
)
|
||||
}),
|
||||
};
|
||||
|
||||
let typescript =
|
||||
get_options::<TypeScriptOptions>("transform-typescript", options, &mut errors, false);
|
||||
|
||||
let assumptions = if options.assumptions.is_null() {
|
||||
CompilerAssumptions::default()
|
||||
} else {
|
||||
match serde_json::from_value::<CompilerAssumptions>(options.assumptions.clone()) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
errors.push(OxcDiagnostic::error(err.to_string()).into());
|
||||
CompilerAssumptions::default()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if !errors.is_empty() {
|
||||
return Err(errors);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
cwd: options.cwd.clone().unwrap(),
|
||||
assumptions: serde_json::from_value(options.assumptions.clone()).unwrap_or_default(),
|
||||
typescript: options
|
||||
.get_plugin("transform-typescript")
|
||||
.map(get_options::<TypeScriptOptions>)
|
||||
.transpose()?
|
||||
.unwrap_or_default(),
|
||||
cwd: options.cwd.clone().unwrap_or_default(),
|
||||
assumptions,
|
||||
typescript,
|
||||
react,
|
||||
es2015,
|
||||
})
|
||||
|
|
@ -205,6 +244,14 @@ impl BabelOptions {
|
|||
self.presets.iter().find_map(|v| Self::get_value(v, name))
|
||||
}
|
||||
|
||||
pub fn has_plugin(&self, name: &str) -> bool {
|
||||
self.get_plugin(name).is_some()
|
||||
}
|
||||
|
||||
pub fn has_preset(&self, name: &str) -> bool {
|
||||
self.get_preset(name).is_some()
|
||||
}
|
||||
|
||||
#[allow(clippy::option_option)]
|
||||
fn get_value(value: &Value, name: &str) -> Option<Option<Value>> {
|
||||
match value {
|
||||
|
|
@ -216,3 +263,17 @@ impl BabelOptions {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deny_unknown_fields() {
|
||||
let options = serde_json::json!({
|
||||
"plugins": [["transform-react-jsx", { "runtime": "automatic", "filter": 1 }]],
|
||||
"sourceType": "module"
|
||||
});
|
||||
let babel_options = serde_json::from_value::<BabelOptions>(options).unwrap();
|
||||
let result = TransformOptions::from_babel_options(&babel_options);
|
||||
assert!(result.is_err());
|
||||
let err_message =
|
||||
result.err().unwrap().iter().map(ToString::to_string).collect::<Vec<_>>().join("\n");
|
||||
assert!(err_message.contains("transform-react-jsx: unknown field `filter`"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ impl ReactJsxRuntime {
|
|||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
#[serde(default, rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct ReactOptions {
|
||||
#[serde(skip)]
|
||||
pub jsx_plugin: bool,
|
||||
|
|
|
|||
|
|
@ -12,8 +12,12 @@ fn default_for_jsx_pragma_frag() -> Cow<'static, str> {
|
|||
Cow::Borrowed("React.Fragment")
|
||||
}
|
||||
|
||||
fn default_as_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
#[serde(default, rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct TypeScriptOptions {
|
||||
/// Replace the function used when compiling JSX expressions.
|
||||
/// This is so that we know that the import is not a type import, and should not be removed.
|
||||
|
|
@ -26,9 +30,18 @@ pub struct TypeScriptOptions {
|
|||
/// defaults to React.Fragment
|
||||
#[serde(default = "default_for_jsx_pragma_frag")]
|
||||
pub jsx_pragma_frag: Cow<'static, str>,
|
||||
|
||||
/// When set to true, the transform will only remove type-only imports (introduced in TypeScript 3.8).
|
||||
/// This should only be used if you are using TypeScript >= 3.8.
|
||||
pub only_remove_type_imports: bool,
|
||||
|
||||
// Enables compilation of TypeScript namespaces.
|
||||
#[serde(default = "default_as_true")]
|
||||
pub allow_namespaces: bool,
|
||||
|
||||
// When enabled, type-only class fields are only removed if they are prefixed with the declare modifier:
|
||||
#[serde(default = "default_as_true")]
|
||||
pub allow_declare_fields: bool,
|
||||
}
|
||||
|
||||
impl TypeScriptOptions {
|
||||
|
|
@ -74,6 +87,8 @@ impl Default for TypeScriptOptions {
|
|||
jsx_pragma: default_for_jsx_pragma(),
|
||||
jsx_pragma_frag: default_for_jsx_pragma_frag(),
|
||||
only_remove_type_imports: false,
|
||||
allow_namespaces: default_as_true(),
|
||||
allow_declare_fields: default_as_true(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,4 +32,3 @@ oxc_diagnostics = { workspace = true }
|
|||
walkdir = { workspace = true }
|
||||
pico-args = { workspace = true }
|
||||
indexmap = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
commit: 4bd1b2c2
|
||||
|
||||
Passed: 310/351
|
||||
Passed: 309/351
|
||||
|
||||
# All Passed:
|
||||
* babel-preset-react
|
||||
|
|
@ -54,8 +54,9 @@ Passed: 310/351
|
|||
* optimize-const-enums/merged-exported/input.ts
|
||||
* regression/15768/input.ts
|
||||
|
||||
# babel-plugin-transform-react-jsx (141/142)
|
||||
# babel-plugin-transform-react-jsx (140/142)
|
||||
* autoImport/complicated-scope-module/input.js
|
||||
* react-automatic/does-not-add-source-self-automatic/input.mjs
|
||||
|
||||
# babel-plugin-transform-react-jsx-development (9/10)
|
||||
* cross-platform/within-ts-module-block/input.ts
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ impl TestCaseKind {
|
|||
}
|
||||
}
|
||||
|
||||
fn transform_options(options: &BabelOptions) -> serde_json::Result<TransformOptions> {
|
||||
fn transform_options(options: &BabelOptions) -> Result<TransformOptions, Vec<Error>> {
|
||||
TransformOptions::from_babel_options(options)
|
||||
}
|
||||
|
||||
|
|
@ -78,7 +78,7 @@ pub trait TestCase {
|
|||
|
||||
fn options(&self) -> &BabelOptions;
|
||||
|
||||
fn transform_options(&self) -> &serde_json::Result<TransformOptions>;
|
||||
fn transform_options(&self) -> &Result<TransformOptions, Vec<Error>>;
|
||||
|
||||
fn test(&self, filtered: bool) -> bool;
|
||||
|
||||
|
|
@ -186,7 +186,7 @@ pub trait TestCase {
|
|||
pub struct ConformanceTestCase {
|
||||
path: PathBuf,
|
||||
options: BabelOptions,
|
||||
transform_options: serde_json::Result<TransformOptions>,
|
||||
transform_options: Result<TransformOptions, Vec<Error>>,
|
||||
}
|
||||
|
||||
impl TestCase for ConformanceTestCase {
|
||||
|
|
@ -201,7 +201,7 @@ impl TestCase for ConformanceTestCase {
|
|||
&self.options
|
||||
}
|
||||
|
||||
fn transform_options(&self) -> &serde_json::Result<TransformOptions> {
|
||||
fn transform_options(&self) -> &Result<TransformOptions, Vec<Error>> {
|
||||
&self.transform_options
|
||||
}
|
||||
|
||||
|
|
@ -287,7 +287,7 @@ impl TestCase for ConformanceTestCase {
|
|||
Some(transform_options.clone())
|
||||
}
|
||||
Err(json_err) => {
|
||||
let error = json_err.to_string();
|
||||
let error = json_err.iter().map(ToString::to_string).collect::<Vec<_>>().join("\n");
|
||||
actual_errors = get_babel_error(&error);
|
||||
None
|
||||
}
|
||||
|
|
@ -350,7 +350,7 @@ impl TestCase for ConformanceTestCase {
|
|||
pub struct ExecTestCase {
|
||||
path: PathBuf,
|
||||
options: BabelOptions,
|
||||
transform_options: serde_json::Result<TransformOptions>,
|
||||
transform_options: Result<TransformOptions, Vec<Error>>,
|
||||
}
|
||||
|
||||
impl ExecTestCase {
|
||||
|
|
@ -396,7 +396,7 @@ impl TestCase for ExecTestCase {
|
|||
&self.options
|
||||
}
|
||||
|
||||
fn transform_options(&self) -> &serde_json::Result<TransformOptions> {
|
||||
fn transform_options(&self) -> &Result<TransformOptions, Vec<Error>> {
|
||||
&self.transform_options
|
||||
}
|
||||
|
||||
|
|
@ -421,7 +421,7 @@ impl TestCase for ExecTestCase {
|
|||
|
||||
fn get_babel_error(error: &str) -> String {
|
||||
match error {
|
||||
"unknown variant `invalidOption`, expected `classic` or `automatic`" => "Runtime must be either \"classic\" or \"automatic\".",
|
||||
"transform-react-jsx: unknown variant `invalidOption`, expected `classic` or `automatic`" => "Runtime must be either \"classic\" or \"automatic\".",
|
||||
"Duplicate __self prop found." => "Duplicate __self prop found. You are most likely using the deprecated transform-react-jsx-self Babel plugin. Both __source and __self are automatically set when using the automatic runtime. Please remove transform-react-jsx-source and transform-react-jsx-self from your Babel config.",
|
||||
"Duplicate __source prop found." => "Duplicate __source prop found. You are most likely using the deprecated transform-react-jsx-source Babel plugin. Both __source and __self are automatically set when using the automatic runtime. Please remove transform-react-jsx-source and transform-react-jsx-self from your Babel config.",
|
||||
"Expected `>` but found `/`" => "Unexpected token, expected \",\"",
|
||||
|
|
|
|||
Loading…
Reference in a new issue