improve(transformer): validate JSX pragma options (#3536)

Validate React JSX `pragma` and `pragma_frag` options. Don't allow:

* empty string
* `foo.bar.qux`
* `foo.`
* `.bar`
* `.`

If options provided are invalid, raise an error and use the defaults.

Also fast path the defaults.
This commit is contained in:
overlookmotel 2024-06-06 03:19:34 +00:00
parent 6506d086b3
commit 14c00a5c1d
3 changed files with 58 additions and 35 deletions

View file

@ -6,6 +6,11 @@ pub fn pragma_and_pragma_frag_cannot_be_set() -> OxcDiagnostic {
.with_help("Remove `pragma` and `pragmaFrag` options.")
}
pub fn invalid_pragma() -> OxcDiagnostic {
OxcDiagnostic::warn("pragma and pragmaFrag must be of the form `foo` or `foo.bar`.")
.with_help("Fix `pragma` and `pragmaFrag` options.")
}
pub fn import_source_cannot_be_set() -> OxcDiagnostic {
OxcDiagnostic::warn("importSource cannot be set when runtime is classic.")
.with_help("Remove `importSource` option.")

View file

@ -79,14 +79,42 @@ struct Pragma<'a> {
}
impl<'a> Pragma<'a> {
fn parse(pragma: &str, ast: &AstBuilder<'a>) -> Self {
let mut parts = pragma.split('.');
let object = ast.new_atom(parts.next().unwrap());
let property = parts.next().map(|property| {
assert!(parts.next().is_none(), "Invalid pragma");
ast.new_atom(property)
});
Self { object, property }
/// Parse `options.pragma` or `options.pragma_frag`.
///
/// If provided option is invalid, raise an error and use default.
fn parse(pragma: Option<&String>, default_property_name: &'static str, ctx: &Ctx<'a>) -> Self {
if let Some(pragma) = pragma {
let mut parts = pragma.split('.');
let object_name = parts.next().unwrap();
if object_name.is_empty() {
return Self::invalid(default_property_name, ctx);
}
let property = match parts.next() {
Some(property_name) => {
if property_name.is_empty() || parts.next().is_some() {
return Self::invalid(default_property_name, ctx);
}
Some(ctx.ast.new_atom(property_name))
}
None => None,
};
let object = ctx.ast.new_atom(object_name);
Self { object, property }
} else {
Self::default(default_property_name)
}
}
fn invalid(default_property_name: &'static str, ctx: &Ctx<'a>) -> Self {
ctx.error(diagnostics::invalid_pragma());
Self::default(default_property_name)
}
fn default(default_property_name: &'static str) -> Self {
Self { object: Atom::from("React"), property: Some(Atom::from(default_property_name)) }
}
fn create_expression(&self, ctx: &mut TraverseCtx<'a>) -> Expression<'a> {
@ -119,11 +147,16 @@ impl<'a> ReactJsx<'a> {
// Parse pragmas
let (pragma, pragma_frag) = match options.runtime {
ReactJsxRuntime::Classic => {
let pragma = Pragma::parse(&options.pragma, &ctx.ast);
let pragma_frag = Pragma::parse(&options.pragma_frag, &ctx.ast);
let pragma = Pragma::parse(options.pragma.as_ref(), "createElement", ctx);
let pragma_frag = Pragma::parse(options.pragma_frag.as_ref(), "Fragment", ctx);
(Some(pragma), Some(pragma_frag))
}
ReactJsxRuntime::Automatic => (None, None),
ReactJsxRuntime::Automatic => {
if options.pragma.is_some() || options.pragma_frag.is_some() {
ctx.error(diagnostics::pragma_and_pragma_frag_cannot_be_set());
}
(None, None)
}
};
Self {
@ -186,13 +219,6 @@ impl<'a> ReactJsx<'a> {
return;
}
if self.options.pragma != "React.createElement"
|| self.options.pragma_frag != "React.Fragment"
{
self.ctx.error(diagnostics::pragma_and_pragma_frag_cannot_be_set());
return;
}
let imports = self.ctx.module_imports.get_import_statements();
let mut index = program
.body

View file

@ -14,14 +14,6 @@ fn default_for_import_source() -> Cow<'static, str> {
Cow::Borrowed("react")
}
fn default_for_pragma() -> Cow<'static, str> {
Cow::Borrowed("React.createElement")
}
fn default_for_pragma_frag() -> Cow<'static, str> {
Cow::Borrowed("React.Fragment")
}
/// Decides which runtime to use.
///
/// Auto imports the functions that JSX transpiles to.
@ -101,14 +93,14 @@ pub struct ReactOptions {
/// Note that the @jsx React.DOM pragma has been deprecated as of React v0.12
///
/// Defaults to `React.createElement`.
#[serde(default = "default_for_pragma")]
pub pragma: Cow<'static, str>,
#[serde(default)]
pub pragma: Option<String>,
/// Replace the component used when compiling JSX fragments. It should be a valid JSX tag name.
///
/// Defaults to `React.Fragment`.
#[serde(default = "default_for_pragma_frag")]
pub pragma_frag: Cow<'static, str>,
#[serde(default)]
pub pragma_frag: Option<String>,
/// `useBuiltIns` is deprecated in Babel 8.
///
@ -133,8 +125,8 @@ impl Default for ReactOptions {
throw_if_namespace: default_as_true(),
pure: default_as_true(),
import_source: default_for_import_source(),
pragma: default_for_pragma(),
pragma_frag: default_for_pragma_frag(),
pragma: None,
pragma_frag: None,
use_built_ins: None,
use_spread: None,
}
@ -187,20 +179,20 @@ impl ReactOptions {
// read jsxImportSource
if let Some(import_source) = comment.strip_prefix("jsxImportSource").map(str::trim) {
self.import_source = Cow::from(import_source.to_string());
self.import_source = Cow::Owned(import_source.to_string());
continue;
}
// read jsxFrag
if let Some(pragma_frag) = comment.strip_prefix("jsxFrag").map(str::trim) {
self.pragma_frag = Cow::from(pragma_frag.to_string());
self.pragma_frag = Some(pragma_frag.to_string());
continue;
}
// Put this condition at the end to avoid breaking @jsxXX
// read jsx
if let Some(pragma) = comment.strip_prefix("jsx").map(str::trim) {
self.pragma = Cow::from(pragma.to_string());
self.pragma = Some(pragma.to_string());
}
}
}