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_transformer",
"pico-args",
"serde_json",
"walkdir",
]

View file

@ -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,
}

View file

@ -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>,

View file

@ -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`"));
}

View file

@ -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,

View file

@ -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(),
}
}
}

View file

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

View file

@ -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

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)
}
@ -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 \",\"",