feat(transformer): report errors when options have unknown fields (#3322)

This commit is contained in:
Dunqing 2024-05-19 01:19:40 +08:00 committed by GitHub
parent 46cb5f97a0
commit e2c6fe0cb1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 155 additions and 57 deletions

1
Cargo.lock generated
View file

@ -1678,7 +1678,6 @@ dependencies = [
"oxc_tasks_common", "oxc_tasks_common",
"oxc_transformer", "oxc_transformer",
"pico-args", "pico-args",
"serde_json",
"walkdir", "walkdir",
] ]

View file

@ -6,29 +6,52 @@ use serde::Deserialize;
/// ///
/// See <https://babeljs.io/docs/assumptions> /// See <https://babeljs.io/docs/assumptions>
#[derive(Debug, Default, Clone, Copy, Deserialize)] #[derive(Debug, Default, Clone, Copy, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CompilerAssumptions { pub struct CompilerAssumptions {
#[serde(default)]
pub array_like_is_iterable: bool, pub array_like_is_iterable: bool,
#[serde(default)]
pub constant_reexports: bool, pub constant_reexports: bool,
#[serde(default)]
pub constant_super: bool, pub constant_super: bool,
#[serde(default)]
pub enumerable_module_meta: bool, pub enumerable_module_meta: bool,
#[serde(default)]
pub ignore_function_length: bool, pub ignore_function_length: bool,
#[serde(default)]
pub ignore_to_primitive_hint: bool, pub ignore_to_primitive_hint: bool,
#[serde(default)]
pub iterable_is_array: bool, pub iterable_is_array: bool,
#[serde(default)]
pub mutable_template_object: bool, pub mutable_template_object: bool,
#[serde(default)]
pub no_class_calls: bool, pub no_class_calls: bool,
#[serde(default)]
pub no_document_all: bool, pub no_document_all: bool,
#[serde(default)]
pub no_incomplete_ns_import_detection: bool, pub no_incomplete_ns_import_detection: bool,
#[serde(default)]
pub no_new_arrows: bool, pub no_new_arrows: bool,
#[serde(default)]
pub no_uninitialized_private_field_access: bool, pub no_uninitialized_private_field_access: bool,
#[serde(default)]
pub object_rest_no_symbols: bool, pub object_rest_no_symbols: bool,
#[serde(default)]
pub private_fields_as_symbols: bool, pub private_fields_as_symbols: bool,
#[serde(default)]
pub private_fields_as_properties: bool, pub private_fields_as_properties: bool,
#[serde(default)]
pub pure_getters: bool, pub pure_getters: bool,
#[serde(default)]
pub set_class_methods: bool, pub set_class_methods: bool,
#[serde(default)]
pub set_computed_properties: bool, pub set_computed_properties: bool,
#[serde(default)]
pub set_public_class_fields: bool, pub set_public_class_fields: bool,
#[serde(default)]
pub set_spread_properties: bool, pub set_spread_properties: bool,
#[serde(default)]
pub skip_for_of_iterator_closing: bool, pub skip_for_of_iterator_closing: bool,
#[serde(default)]
pub super_is_callable_constructor: bool, pub super_is_callable_constructor: bool,
} }

View file

@ -3,7 +3,7 @@ use serde::Deserialize;
use super::ArrowFunctionsOptions; use super::ArrowFunctionsOptions;
#[derive(Debug, Default, Clone, Deserialize)] #[derive(Debug, Default, Clone, Deserialize)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase", deny_unknown_fields)]
pub struct ES2015Options { pub struct ES2015Options {
#[serde(skip)] #[serde(skip)]
pub arrow_function: Option<ArrowFunctionsOptions>, pub arrow_function: Option<ArrowFunctionsOptions>,

View file

@ -1,10 +1,13 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use oxc_diagnostics::{Error, OxcDiagnostic};
use serde::{de::DeserializeOwned, Deserialize}; use serde::{de::DeserializeOwned, Deserialize};
use serde_json::Value; use serde_json::Value;
use crate::{ use crate::{
compiler_assumptions::CompilerAssumptions, es2015::ES2015Options, react::ReactOptions, compiler_assumptions::CompilerAssumptions,
es2015::{ArrowFunctionsOptions, ES2015Options},
react::ReactOptions,
typescript::TypeScriptOptions, typescript::TypeScriptOptions,
}; };
@ -33,58 +36,94 @@ pub struct TransformOptions {
} }
impl TransformOptions { impl TransformOptions {
/// # Panics
/// Panics if the options are invalid.
/// # Errors /// # 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>( fn get_options<T: Default + DeserializeOwned>(
value: Option<Value>, name: &str,
) -> serde_json::Result<T> { babel_options: &BabelOptions,
match value { errors: &mut Vec<Error>,
Some(v) => serde_json::from_value::<T>(v), is_preset: bool,
None => Ok(T::default()), ) -> 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") { let mut errors = Vec::<Error>::new();
get_options::<ReactOptions>(options)?
let react = if options.has_preset("react") {
get_options::<ReactOptions>("react", options, &mut errors, true)
} else { } else {
let jsx_plugin = options.get_plugin("transform-react-jsx"); let has_jsx_plugin = options.has_plugin("transform-react-jsx");
let jsx_development_plugin = options.get_plugin("transform-react-jsx-development"); let has_jsx_development_plugin = options.has_plugin("transform-react-jsx-development");
let has_jsx_plugin = let mut react_options = if has_jsx_plugin {
jsx_plugin.as_ref().is_some() || jsx_development_plugin.as_ref().is_some(); get_options::<ReactOptions>("transform-react-jsx", options, &mut errors, false)
let mut react_options = jsx_plugin } else {
.map(get_options::<ReactOptions>) get_options::<ReactOptions>(
.or_else(|| jsx_development_plugin.map(get_options::<ReactOptions>)) "transform-react-jsx-development",
.transpose()? options,
.unwrap_or_default(); &mut errors,
react_options.development = false,
options.get_plugin("transform-react-jsx-development").is_some(); )
react_options.jsx_plugin = has_jsx_plugin; };
react_options.display_name_plugin = react_options.development = options.has_plugin("transform-react-jsx-development");
options.get_plugin("transform-react-display-name").is_some(); react_options.jsx_plugin = has_jsx_plugin || has_jsx_development_plugin;
react_options.jsx_self_plugin = react_options.display_name_plugin = options.has_plugin("transform-react-display-name");
options.get_plugin("transform-react-jsx-self").is_some(); react_options.jsx_self_plugin = options.has_plugin("transform-react-jsx-self");
react_options.jsx_source_plugin = react_options.jsx_source_plugin = options.has_plugin("transform-react-jsx-source");
options.get_plugin("transform-react-jsx-source").is_some();
react_options react_options
}; };
let es2015 = ES2015Options { let es2015 = ES2015Options {
arrow_function: options arrow_function: options.has_plugin("transform-arrow-functions").then(|| {
.get_plugin("transform-arrow-functions") get_options::<ArrowFunctionsOptions>(
.map(get_options) "transform-arrow-functions",
.transpose()?, 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 { Ok(Self {
cwd: options.cwd.clone().unwrap(), cwd: options.cwd.clone().unwrap_or_default(),
assumptions: serde_json::from_value(options.assumptions.clone()).unwrap_or_default(), assumptions,
typescript: options typescript,
.get_plugin("transform-typescript")
.map(get_options::<TypeScriptOptions>)
.transpose()?
.unwrap_or_default(),
react, react,
es2015, es2015,
}) })
@ -205,6 +244,14 @@ impl BabelOptions {
self.presets.iter().find_map(|v| Self::get_value(v, name)) 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)] #[allow(clippy::option_option)]
fn get_value(value: &Value, name: &str) -> Option<Option<Value>> { fn get_value(value: &Value, name: &str) -> Option<Option<Value>> {
match 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`"));
}

View file

@ -46,7 +46,7 @@ impl ReactJsxRuntime {
} }
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase", deny_unknown_fields)]
pub struct ReactOptions { pub struct ReactOptions {
#[serde(skip)] #[serde(skip)]
pub jsx_plugin: bool, pub jsx_plugin: bool,

View file

@ -12,8 +12,12 @@ fn default_for_jsx_pragma_frag() -> Cow<'static, str> {
Cow::Borrowed("React.Fragment") Cow::Borrowed("React.Fragment")
} }
fn default_as_true() -> bool {
true
}
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase", deny_unknown_fields)]
pub struct TypeScriptOptions { pub struct TypeScriptOptions {
/// Replace the function used when compiling JSX expressions. /// 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. /// 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 /// defaults to React.Fragment
#[serde(default = "default_for_jsx_pragma_frag")] #[serde(default = "default_for_jsx_pragma_frag")]
pub jsx_pragma_frag: Cow<'static, str>, pub jsx_pragma_frag: Cow<'static, str>,
/// When set to true, the transform will only remove type-only imports (introduced in TypeScript 3.8). /// 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. /// This should only be used if you are using TypeScript >= 3.8.
pub only_remove_type_imports: bool, 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 { impl TypeScriptOptions {
@ -74,6 +87,8 @@ impl Default for TypeScriptOptions {
jsx_pragma: default_for_jsx_pragma(), jsx_pragma: default_for_jsx_pragma(),
jsx_pragma_frag: default_for_jsx_pragma_frag(), jsx_pragma_frag: default_for_jsx_pragma_frag(),
only_remove_type_imports: false, only_remove_type_imports: false,
allow_namespaces: default_as_true(),
allow_declare_fields: default_as_true(),
} }
} }
} }

View file

@ -32,4 +32,3 @@ oxc_diagnostics = { workspace = true }
walkdir = { workspace = true } walkdir = { workspace = true }
pico-args = { workspace = true } pico-args = { workspace = true }
indexmap = { workspace = true } indexmap = { workspace = true }
serde_json = { workspace = true }

View file

@ -1,6 +1,6 @@
commit: 4bd1b2c2 commit: 4bd1b2c2
Passed: 310/351 Passed: 309/351
# All Passed: # All Passed:
* babel-preset-react * babel-preset-react
@ -54,8 +54,9 @@ Passed: 310/351
* optimize-const-enums/merged-exported/input.ts * optimize-const-enums/merged-exported/input.ts
* regression/15768/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 * autoImport/complicated-scope-module/input.js
* react-automatic/does-not-add-source-self-automatic/input.mjs
# babel-plugin-transform-react-jsx-development (9/10) # babel-plugin-transform-react-jsx-development (9/10)
* cross-platform/within-ts-module-block/input.ts * cross-platform/within-ts-module-block/input.ts

View file

@ -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) TransformOptions::from_babel_options(options)
} }
@ -78,7 +78,7 @@ pub trait TestCase {
fn options(&self) -> &BabelOptions; 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; fn test(&self, filtered: bool) -> bool;
@ -186,7 +186,7 @@ pub trait TestCase {
pub struct ConformanceTestCase { pub struct ConformanceTestCase {
path: PathBuf, path: PathBuf,
options: BabelOptions, options: BabelOptions,
transform_options: serde_json::Result<TransformOptions>, transform_options: Result<TransformOptions, Vec<Error>>,
} }
impl TestCase for ConformanceTestCase { impl TestCase for ConformanceTestCase {
@ -201,7 +201,7 @@ impl TestCase for ConformanceTestCase {
&self.options &self.options
} }
fn transform_options(&self) -> &serde_json::Result<TransformOptions> { fn transform_options(&self) -> &Result<TransformOptions, Vec<Error>> {
&self.transform_options &self.transform_options
} }
@ -287,7 +287,7 @@ impl TestCase for ConformanceTestCase {
Some(transform_options.clone()) Some(transform_options.clone())
} }
Err(json_err) => { 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); actual_errors = get_babel_error(&error);
None None
} }
@ -350,7 +350,7 @@ impl TestCase for ConformanceTestCase {
pub struct ExecTestCase { pub struct ExecTestCase {
path: PathBuf, path: PathBuf,
options: BabelOptions, options: BabelOptions,
transform_options: serde_json::Result<TransformOptions>, transform_options: Result<TransformOptions, Vec<Error>>,
} }
impl ExecTestCase { impl ExecTestCase {
@ -396,7 +396,7 @@ impl TestCase for ExecTestCase {
&self.options &self.options
} }
fn transform_options(&self) -> &serde_json::Result<TransformOptions> { fn transform_options(&self) -> &Result<TransformOptions, Vec<Error>> {
&self.transform_options &self.transform_options
} }
@ -421,7 +421,7 @@ impl TestCase for ExecTestCase {
fn get_babel_error(error: &str) -> String { fn get_babel_error(error: &str) -> String {
match error { 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 __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.", "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 \",\"", "Expected `>` but found `/`" => "Unexpected token, expected \",\"",