use oxc_allocator::{
Box as ArenaBox, StringBuilder as ArenaStringBuilder, TakeIn, Vec as ArenaVec,
};
use oxc_ast::{AstBuilder, NONE, ast::*};
use oxc_ecmascript::PropName;
use oxc_span::{SPAN, Span};
use oxc_str::{Ident, Str};
use oxc_syntax::{
identifier::{is_identifier_name, is_white_space_single_line},
keyword::is_reserved_keyword,
line_terminator::is_line_terminator,
reference::ReferenceFlags,
symbol::SymbolFlags,
xml_entities::XML_ENTITIES,
};
use oxc_traverse::{BoundIdentifier, Traverse};
use crate::{
context::TraverseCtx,
es2018::{ObjectRestSpread, ObjectRestSpreadOptions},
state::TransformState,
};
use super::{
diagnostics,
jsx_self::JsxSelf,
jsx_source::JsxSource,
options::{JsxOptions, JsxRuntime},
};
pub struct JsxImpl<'a> {
pure: bool,
options: JsxOptions,
object_rest_spread_options: Option<ObjectRestSpreadOptions>,
pub(super) jsx_self: JsxSelf,
pub(super) jsx_source: JsxSource<'a>,
bindings: Bindings<'a>,
}
enum Bindings<'a> {
Classic(ClassicBindings<'a>),
AutomaticScript(AutomaticScriptBindings<'a>),
AutomaticModule(AutomaticModuleBindings<'a>),
}
impl Bindings<'_> {
#[inline]
fn is_classic(&self) -> bool {
matches!(self, Self::Classic(_))
}
}
struct ClassicBindings<'a> {
pragma: Pragma<'a>,
pragma_frag: Pragma<'a>,
}
struct AutomaticScriptBindings<'a> {
jsx_runtime_importer: Str<'a>,
react_importer_len: u32,
require_create_element: Option<BoundIdentifier<'a>>,
require_jsx: Option<BoundIdentifier<'a>>,
is_development: bool,
}
impl<'a> AutomaticScriptBindings<'a> {
fn new(jsx_runtime_importer: Str<'a>, react_importer_len: u32, is_development: bool) -> Self {
Self {
jsx_runtime_importer,
react_importer_len,
require_create_element: None,
require_jsx: None,
is_development,
}
}
fn require_create_element(&mut self, ctx: &mut TraverseCtx<'a>) -> IdentifierReference<'a> {
if self.require_create_element.is_none() {
let source =
get_import_source(self.jsx_runtime_importer.as_str(), self.react_importer_len);
let id = self.add_require_statement("react", source, true, ctx);
self.require_create_element = Some(id);
}
self.require_create_element.as_ref().unwrap().create_read_reference(ctx)
}
fn require_jsx(&mut self, ctx: &mut TraverseCtx<'a>) -> IdentifierReference<'a> {
if self.require_jsx.is_none() {
let var_name =
if self.is_development { "reactJsxDevRuntime" } else { "reactJsxRuntime" };
let id = self.add_require_statement(var_name, self.jsx_runtime_importer, false, ctx);
self.require_jsx = Some(id);
}
self.require_jsx.as_ref().unwrap().create_read_reference(ctx)
}
#[expect(clippy::unused_self)]
fn add_require_statement(
&self,
variable_name: &str,
source: Str<'a>,
front: bool,
ctx: &mut TraverseCtx<'a>,
) -> BoundIdentifier<'a> {
let binding =
ctx.generate_uid_in_root_scope(variable_name, SymbolFlags::FunctionScopedVariable);
ctx.state.module_imports.add_default_import(source, binding.clone(), front);
binding
}
}
struct AutomaticModuleBindings<'a> {
jsx_runtime_importer: Str<'a>,
react_importer_len: u32,
import_create_element: Option<BoundIdentifier<'a>>,
import_fragment: Option<BoundIdentifier<'a>>,
import_jsx: Option<BoundIdentifier<'a>>,
import_jsxs: Option<BoundIdentifier<'a>>,
is_development: bool,
}
impl<'a> AutomaticModuleBindings<'a> {
fn new(jsx_runtime_importer: Str<'a>, react_importer_len: u32, is_development: bool) -> Self {
Self {
jsx_runtime_importer,
react_importer_len,
import_create_element: None,
import_fragment: None,
import_jsx: None,
import_jsxs: None,
is_development,
}
}
fn import_create_element(&mut self, ctx: &mut TraverseCtx<'a>) -> IdentifierReference<'a> {
if self.import_create_element.is_none() {
let source =
get_import_source(self.jsx_runtime_importer.as_str(), self.react_importer_len);
let id = self.add_import_statement("createElement", source, ctx);
self.import_create_element = Some(id);
}
self.import_create_element.as_ref().unwrap().create_read_reference(ctx)
}
fn import_fragment(&mut self, ctx: &mut TraverseCtx<'a>) -> IdentifierReference<'a> {
if self.import_fragment.is_none() {
self.import_fragment = Some(self.add_jsx_import_statement("Fragment", ctx));
}
self.import_fragment.as_ref().unwrap().create_read_reference(ctx)
}
fn import_jsx(&mut self, ctx: &mut TraverseCtx<'a>) -> IdentifierReference<'a> {
if self.import_jsx.is_none() {
if self.is_development {
self.add_import_jsx_dev(ctx);
} else {
self.import_jsx = Some(self.add_jsx_import_statement("jsx", ctx));
}
}
self.import_jsx.as_ref().unwrap().create_read_reference(ctx)
}
fn import_jsxs(&mut self, ctx: &mut TraverseCtx<'a>) -> IdentifierReference<'a> {
if self.import_jsxs.is_none() {
if self.is_development {
self.add_import_jsx_dev(ctx);
} else {
self.import_jsxs = Some(self.add_jsx_import_statement("jsxs", ctx));
}
}
self.import_jsxs.as_ref().unwrap().create_read_reference(ctx)
}
#[inline]
fn add_import_jsx_dev(&mut self, ctx: &mut TraverseCtx<'a>) {
let id = self.add_jsx_import_statement("jsxDEV", ctx);
self.import_jsx = Some(id.clone());
self.import_jsxs = Some(id);
}
fn add_jsx_import_statement(
&self,
name: &'static str,
ctx: &mut TraverseCtx<'a>,
) -> BoundIdentifier<'a> {
self.add_import_statement(name, self.jsx_runtime_importer, ctx)
}
#[expect(clippy::unused_self)]
fn add_import_statement(
&self,
name: &'static str,
source: Str<'a>,
ctx: &mut TraverseCtx<'a>,
) -> BoundIdentifier<'a> {
let binding = ctx.generate_uid_in_root_scope(name, SymbolFlags::Import);
ctx.state.module_imports.add_named_import(source, Str::from(name), binding.clone(), false);
binding
}
}
#[inline]
fn get_import_source(jsx_runtime_importer: &str, react_importer_len: u32) -> Str<'_> {
Str::from(&jsx_runtime_importer[..react_importer_len as usize])
}
enum Pragma<'a> {
Double(Str<'a>, Str<'a>),
Single(Str<'a>),
Multiple(Vec<Str<'a>>),
This(Vec<Str<'a>>),
ImportMeta(Vec<Str<'a>>),
}
impl<'a> Pragma<'a> {
#[cfg(test)]
fn parse(
pragma: Option<&str>,
default_property_name: &'static str,
ast: AstBuilder<'a>,
ctx: &mut TransformState<'a>,
) -> Self {
if let Some(pragma) = pragma {
if pragma.is_empty() {
ctx.error(diagnostics::invalid_pragma());
} else if let Some(pragma) = Self::parse_impl(pragma, ast) {
return pragma;
} else {
ctx.error(diagnostics::invalid_pragma());
}
}
Self::Double(Str::from("React"), Str::from(default_property_name))
}
fn parse_no_ctx(
pragma: Option<&str>,
default_property_name: &'static str,
ast: AstBuilder<'a>,
) -> Self {
if let Some(pragma) = pragma
&& !pragma.is_empty()
&& let Some(pragma) = Self::parse_impl(pragma, ast)
{
return pragma;
}
Self::Double(Str::from("React"), Str::from(default_property_name))
}
fn parse_impl(pragma: &str, ast: AstBuilder<'a>) -> Option<Self> {
let strs_to_arena_strs = |parts: &[&str]| parts.iter().map(|part| ast.str(part)).collect();
let parts = pragma.split('.').collect::<Vec<_>>();
let [root, tail @ ..] = &parts[..] else {
unreachable!();
};
match *root {
"this" => {
if !tail.iter().all(|part| is_identifier_name(part)) {
return None;
}
return Some(Self::This(strs_to_arena_strs(tail)));
}
"import" => {
let ["meta", rest @ ..] = tail else {
return None;
};
if !rest.iter().all(|part| is_identifier_name(part)) {
return None;
}
return Some(Self::ImportMeta(strs_to_arena_strs(rest)));
}
_ => {
if is_reserved_keyword(root) || !is_identifier_name(root) {
return None;
}
if !tail.iter().all(|part| is_identifier_name(part)) {
return None;
}
}
}
Some(match &parts[..] {
[first, second] => Self::Double(ast.str(first), ast.str(second)),
[only] => Self::Single(ast.str(only)),
parts => Self::Multiple(strs_to_arena_strs(parts)),
})
}
fn create_expression(&self, ctx: &mut TraverseCtx<'a>) -> Expression<'a> {
let (object, parts) = match self {
Self::Double(first, second) => {
let object = get_read_identifier_reference(SPAN, *first, ctx);
return Expression::from(ctx.ast.member_expression_static(
SPAN,
object,
ctx.ast.identifier_name(SPAN, *second),
false,
));
}
Self::Single(single) => {
return get_read_identifier_reference(SPAN, *single, ctx);
}
Self::Multiple(parts) => {
let mut parts = parts.iter();
let first = *parts.next().unwrap();
let object = get_read_identifier_reference(SPAN, first, ctx);
(object, parts)
}
Self::This(parts) => {
let object = ctx.ast.expression_this(SPAN);
(object, parts.iter())
}
Self::ImportMeta(parts) => {
let object = ctx.ast.expression_meta_property(
SPAN,
ctx.ast.identifier_name(SPAN, Str::from("import")),
ctx.ast.identifier_name(SPAN, Str::from("meta")),
);
(object, parts.iter())
}
};
let mut expr = object;
for &item in parts {
let name = ctx.ast.identifier_name(SPAN, item);
expr = ctx.ast.member_expression_static(SPAN, expr, name, false).into();
}
expr
}
}
impl<'a> JsxImpl<'a> {
pub fn new(
options: JsxOptions,
object_rest_spread_options: Option<ObjectRestSpreadOptions>,
ast: AstBuilder<'a>,
source_type: oxc_span::SourceType,
) -> Self {
let pure = options.pure || (options.import_source.is_none() && options.pragma.is_none());
let bindings = match options.runtime {
JsxRuntime::Classic => {
let pragma = Pragma::parse_no_ctx(options.pragma.as_deref(), "createElement", ast);
let pragma_frag =
Pragma::parse_no_ctx(options.pragma_frag.as_deref(), "Fragment", ast);
Bindings::Classic(ClassicBindings { pragma, pragma_frag })
}
JsxRuntime::Automatic => {
let is_development = options.development;
#[expect(clippy::single_match_else, clippy::cast_possible_truncation)]
let (jsx_runtime_importer, source_len) = match options.import_source.as_ref() {
Some(import_source) => {
let mut import_source = &**import_source;
let source_len = match u32::try_from(import_source.len()) {
Ok(0) | Err(_) => {
import_source = "react";
import_source.len() as u32
}
Ok(source_len) => source_len,
};
let jsx_runtime_importer = ast.str(&format!(
"{}/jsx-{}runtime",
import_source,
if is_development { "dev-" } else { "" }
));
(jsx_runtime_importer, source_len)
}
None => {
let jsx_runtime_importer = if is_development {
Str::from("react/jsx-dev-runtime")
} else {
Str::from("react/jsx-runtime")
};
(jsx_runtime_importer, "react".len() as u32)
}
};
if source_type.is_module() {
Bindings::AutomaticModule(AutomaticModuleBindings::new(
jsx_runtime_importer,
source_len,
is_development,
))
} else {
Bindings::AutomaticScript(AutomaticScriptBindings::new(
jsx_runtime_importer,
source_len,
is_development,
))
}
}
};
Self {
pure,
options,
object_rest_spread_options,
jsx_self: JsxSelf::new(),
jsx_source: JsxSource::new(),
bindings,
}
}
}
impl<'a> Traverse<'a, TransformState<'a>> for JsxImpl<'a> {
fn exit_program(&mut self, _program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) {
self.insert_filename_var_statement(ctx);
}
#[inline]
fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
if !expr.is_jsx() {
return;
}
*expr = match expr.take_in(ctx.ast) {
Expression::JSXElement(e) => self.transform_jsx_element(e, ctx),
Expression::JSXFragment(e) => self.transform_jsx(e.span, None, e.unbox().children, ctx),
_ => unreachable!(),
};
}
}
impl<'a> JsxImpl<'a> {
fn insert_filename_var_statement(&self, ctx: &mut TraverseCtx<'a>) {
let Some(declarator) = self.jsx_source.get_filename_var_declarator(ctx) else { return };
if self.bindings.is_classic() || ctx.state.source_type.is_module() {
let stmt = Statement::VariableDeclaration(ctx.ast.alloc_variable_declaration(
SPAN,
VariableDeclarationKind::Var,
ctx.ast.vec1(declarator),
false,
));
ctx.state.top_level_statements.insert_statement(stmt);
} else {
ctx.state.var_declarations.insert_var_declarator(declarator, ctx.ast);
}
}
fn transform_jsx_element(
&mut self,
element: ArenaBox<'a, JSXElement<'a>>,
ctx: &mut TraverseCtx<'a>,
) -> Expression<'a> {
let JSXElement { span, opening_element, closing_element, children, .. } = element.unbox();
Self::delete_reference_for_closing_element(closing_element.as_deref(), ctx);
self.transform_jsx(span, Some(opening_element), children, ctx)
}
fn transform_jsx(
&mut self,
span: Span,
opening_element: Option<ArenaBox<'a, JSXOpeningElement<'a>>>,
children: ArenaVec<JSXChild<'a>>,
ctx: &mut TraverseCtx<'a>,
) -> Expression<'a> {
let has_key_after_props_spread =
opening_element.as_ref().is_some_and(|e| Self::has_key_after_props_spread(e));
let is_classic = self.bindings.is_classic() || has_key_after_props_spread;
let is_automatic = !is_classic;
let is_development = self.options.development;
let is_element = opening_element.is_some();
let capacity = if is_classic { 3 + children.len() } else { 6 };
let mut arguments = ctx.ast.vec_with_capacity(capacity);
let mut key_prop = None;
let mut properties = ctx.ast.vec();
let (element_name, attributes) = opening_element
.map(|e| {
let e = e.unbox();
(Some(e.name), Some(e.attributes))
})
.unwrap_or_default();
if let Some(attributes) = attributes {
let attributes_len = attributes.len();
for attribute in attributes {
match attribute {
JSXAttributeItem::Attribute(attr) => {
let JSXAttribute { span, name, value, .. } = attr.unbox();
match &name {
JSXAttributeName::Identifier(ident) if ident.name == "key" => {
if value.is_none() {
ctx.state.error(diagnostics::valueless_key(ident.span));
} else if is_automatic {
key_prop = value;
continue;
}
}
_ => {}
}
let kind = PropertyKind::Init;
let key = Self::get_attribute_name(name, ctx);
let value = self.transform_jsx_attribute_value(value, ctx);
let object_property = ctx.ast.object_property_kind_object_property(
span, kind, key, value, false, false, false,
);
properties.push(object_property);
}
JSXAttributeItem::SpreadAttribute(spread) => {
let JSXSpreadAttribute { argument, span, .. } = spread.unbox();
if is_classic
&& attributes_len == 1
&& !(self.options.jsx_self_plugin || self.options.jsx_source_plugin)
{
if !matches!(&argument, Expression::ObjectExpression(o) if has_proto(o))
{
arguments.push(Argument::from(argument));
continue;
}
}
match argument {
Expression::ObjectExpression(expr) if !has_proto(&expr) => {
properties.extend(expr.unbox().properties);
}
argument => {
let object_property =
ctx.ast.object_property_kind_spread_property(span, argument);
properties.push(object_property);
}
}
}
}
}
}
let mut need_jsxs = false;
if is_automatic {
let mut children = ctx.ast.vec_from_iter(
children
.into_iter()
.filter_map(|child| self.transform_jsx_child_automatic(child, ctx)),
);
let children_len = children.len();
if children_len != 0 {
let value = if children_len == 1
&& !matches!(children[0], ArrayExpressionElement::SpreadElement(_))
{
Expression::try_from(children.pop().unwrap()).unwrap()
} else {
need_jsxs = children_len > 1;
ctx.ast.expression_array(span, children)
};
let children = ctx.ast.property_key_static_identifier(SPAN, "children");
let kind = PropertyKind::Init;
let property = ctx.ast.object_property_kind_object_property(
SPAN, kind, children, value, false, false, false,
);
properties.push(property);
}
let mut object_expression = ctx.ast.expression_object(span, properties);
if let Some(options) = self.object_rest_spread_options {
ObjectRestSpread::transform_object_expression(options, &mut object_expression, ctx);
}
arguments.push(Argument::from(object_expression));
if key_prop.is_some() {
arguments.push(Argument::from(self.transform_jsx_attribute_value(key_prop, ctx)));
} else if is_development {
arguments.push(Argument::from(ctx.ast.void_0(SPAN)));
}
if is_development {
arguments.push(Argument::from(
ctx.ast.expression_boolean_literal(SPAN, children_len > 1),
));
}
if self.options.jsx_source_plugin {
let (line, column) = self.jsx_source.get_line_column(span.start, ctx);
let expr = self.jsx_source.get_source_object(line, column, ctx);
arguments.push(Argument::from(expr));
}
if self.options.jsx_self_plugin && JsxSelf::can_add_self_attribute(ctx) {
arguments.push(Argument::from(ctx.ast.expression_this(SPAN)));
}
} else {
if is_element {
if self.options.jsx_self_plugin && JsxSelf::can_add_self_attribute(ctx) {
properties.push(JsxSelf::get_object_property_kind_for_jsx_plugin(ctx));
}
if self.options.jsx_source_plugin {
let (line, column) = self.jsx_source.get_line_column(span.start, ctx);
properties.push(
self.jsx_source.get_object_property_kind_for_jsx_plugin(line, column, ctx),
);
}
}
if !properties.is_empty() {
let mut object_expression = ctx.ast.expression_object(span, properties);
if let Some(options) = self.object_rest_spread_options {
ObjectRestSpread::transform_object_expression(
options,
&mut object_expression,
ctx,
);
}
arguments.push(Argument::from(object_expression));
} else if arguments.is_empty() {
let null_expr = ctx.ast.expression_null_literal(SPAN);
arguments.push(Argument::from(null_expr));
}
arguments.extend(
children
.into_iter()
.filter_map(|child| self.transform_jsx_child_classic(child, ctx)),
);
}
let argument_expr = if let Some(element_name) = element_name {
self.transform_element_name(element_name, ctx)
} else {
self.get_fragment(ctx)
};
arguments.insert(0, Argument::from(argument_expr));
debug_assert!(arguments.len() <= capacity);
let callee = self.get_create_element(has_key_after_props_spread, need_jsxs, ctx);
ctx.ast.expression_call_with_pure(span, callee, NONE, arguments, false, self.pure)
}
fn transform_element_name(
&self,
name: JSXElementName<'a>,
ctx: &mut TraverseCtx<'a>,
) -> Expression<'a> {
match name {
JSXElementName::Identifier(ident) => {
ctx.ast.expression_string_literal(ident.span, ident.name, None)
}
JSXElementName::IdentifierReference(ident) => Expression::Identifier(ident),
JSXElementName::MemberExpression(member_expr) => {
Self::transform_jsx_member_expression(member_expr, ctx)
}
JSXElementName::NamespacedName(namespaced) => {
if self.options.throw_if_namespace {
ctx.state.error(diagnostics::namespace_does_not_support(namespaced.span));
}
let namespace_name = ctx.ast.str_from_strs_array([
&namespaced.namespace.name,
":",
&namespaced.name.name,
]);
ctx.ast.expression_string_literal(namespaced.span, namespace_name, None)
}
JSXElementName::ThisExpression(expr) => ctx.ast.expression_this(expr.span),
}
}
fn get_fragment(&mut self, ctx: &mut TraverseCtx<'a>) -> Expression<'a> {
match &mut self.bindings {
Bindings::Classic(bindings) => bindings.pragma_frag.create_expression(ctx),
Bindings::AutomaticScript(bindings) => {
let object_ident = bindings.require_jsx(ctx);
let property_name = Str::from("Fragment");
create_static_member_expression(object_ident, property_name, ctx)
}
Bindings::AutomaticModule(bindings) => {
let ident = bindings.import_fragment(ctx);
Expression::Identifier(ctx.alloc(ident))
}
}
}
fn get_create_element(
&mut self,
has_key_after_props_spread: bool,
jsxs: bool,
ctx: &mut TraverseCtx<'a>,
) -> Expression<'a> {
match &mut self.bindings {
Bindings::Classic(bindings) => bindings.pragma.create_expression(ctx),
Bindings::AutomaticScript(bindings) => {
let (ident, property_name) = if has_key_after_props_spread {
(bindings.require_create_element(ctx), Str::from("createElement"))
} else {
let property_name = if bindings.is_development {
Str::from("jsxDEV")
} else if jsxs {
Str::from("jsxs")
} else {
Str::from("jsx")
};
(bindings.require_jsx(ctx), property_name)
};
create_static_member_expression(ident, property_name, ctx)
}
Bindings::AutomaticModule(bindings) => {
let ident = if has_key_after_props_spread {
bindings.import_create_element(ctx)
} else if jsxs {
bindings.import_jsxs(ctx)
} else {
bindings.import_jsx(ctx)
};
Expression::Identifier(ctx.alloc(ident))
}
}
}
fn transform_jsx_member_expression(
expr: ArenaBox<'a, JSXMemberExpression<'a>>,
ctx: &TraverseCtx<'a>,
) -> Expression<'a> {
let JSXMemberExpression { span, object, property, .. } = expr.unbox();
let object = match object {
JSXMemberExpressionObject::IdentifierReference(ident) => Expression::Identifier(ident),
JSXMemberExpressionObject::MemberExpression(expr) => {
Self::transform_jsx_member_expression(expr, ctx)
}
JSXMemberExpressionObject::ThisExpression(expr) => ctx.ast.expression_this(expr.span),
};
let property = ctx.ast.identifier_name(property.span, property.name);
ctx.ast.member_expression_static(span, object, property, false).into()
}
fn transform_jsx_attribute_value(
&mut self,
value: Option<JSXAttributeValue<'a>>,
ctx: &mut TraverseCtx<'a>,
) -> Expression<'a> {
match value {
Some(JSXAttributeValue::StringLiteral(s)) => {
let mut decoded = None;
Self::decode_entities(s.value.as_str(), &mut decoded, s.value.len(), ctx);
let jsx_text = if let Some(decoded) = decoded {
Str::from(decoded)
} else {
s.value
};
ctx.ast.expression_string_literal(s.span, jsx_text, None)
}
Some(JSXAttributeValue::Element(e)) => self.transform_jsx_element(e, ctx),
Some(JSXAttributeValue::Fragment(e)) => {
self.transform_jsx(e.span, None, e.unbox().children, ctx)
}
Some(JSXAttributeValue::ExpressionContainer(c)) => match c.unbox().expression {
jsx_expr @ match_expression!(JSXExpression) => jsx_expr.into_expression(),
JSXExpression::EmptyExpression(e) => {
ctx.ast.expression_boolean_literal(e.span, true)
}
},
None => ctx.ast.expression_boolean_literal(SPAN, true),
}
}
fn transform_jsx_child_automatic(
&mut self,
child: JSXChild<'a>,
ctx: &mut TraverseCtx<'a>,
) -> Option<ArrayExpressionElement<'a>> {
if let JSXChild::Spread(e) = child {
let JSXSpreadChild { span, expression, .. } = e.unbox();
Some(ctx.ast.array_expression_element_spread_element(span, expression))
} else {
self.transform_jsx_child(child, ctx).map(ArrayExpressionElement::from)
}
}
fn transform_jsx_child_classic(
&mut self,
child: JSXChild<'a>,
ctx: &mut TraverseCtx<'a>,
) -> Option<Argument<'a>> {
if let JSXChild::Spread(e) = child {
let JSXSpreadChild { span, expression, .. } = e.unbox();
Some(ctx.ast.argument_spread_element(span, expression))
} else {
self.transform_jsx_child(child, ctx).map(Argument::from)
}
}
fn transform_jsx_child(
&mut self,
child: JSXChild<'a>,
ctx: &mut TraverseCtx<'a>,
) -> Option<Expression<'a>> {
match child {
JSXChild::Text(text) => Self::transform_jsx_text(&text, ctx),
JSXChild::ExpressionContainer(e) => match e.unbox().expression {
jsx_expr @ match_expression!(JSXExpression) => Some(jsx_expr.into_expression()),
JSXExpression::EmptyExpression(_) => None,
},
JSXChild::Element(e) => Some(self.transform_jsx_element(e, ctx)),
JSXChild::Fragment(e) => {
Some(self.transform_jsx(e.span, None, e.unbox().children, ctx))
}
JSXChild::Spread(_) => unreachable!(),
}
}
fn get_attribute_name(name: JSXAttributeName<'a>, ctx: &TraverseCtx<'a>) -> PropertyKey<'a> {
match name {
JSXAttributeName::Identifier(ident) => {
let name = ident.name;
if ident.name.contains('-') {
PropertyKey::from(ctx.ast.expression_string_literal(ident.span, name, None))
} else {
ctx.ast.property_key_static_identifier(ident.span, name)
}
}
JSXAttributeName::NamespacedName(namespaced) => {
let name = ctx.ast.str(&namespaced.to_string());
PropertyKey::from(ctx.ast.expression_string_literal(namespaced.span, name, None))
}
}
}
fn transform_jsx_text(text: &JSXText<'a>, ctx: &TraverseCtx<'a>) -> Option<Expression<'a>> {
Self::fixup_whitespace_and_decode_entities(text.value, ctx)
.map(|value| ctx.ast.expression_string_literal(text.span, value, None))
}
fn fixup_whitespace_and_decode_entities(
text: Str<'a>,
ctx: &TraverseCtx<'a>,
) -> Option<Str<'a>> {
let mut acc: Option<ArenaStringBuilder> = None;
let mut only_line: Option<Str<'a>> = None;
let mut first_non_whitespace: Option<usize> = Some(0);
let mut last_non_whitespace: Option<usize> = None;
for (index, c) in text.char_indices() {
if is_line_terminator(c) {
if let (Some(first), Some(last)) = (first_non_whitespace, last_non_whitespace) {
Self::add_line_of_jsx_text(
Str::from(&text.as_str()[first..last]),
&mut acc,
&mut only_line,
text.len(),
ctx,
);
}
first_non_whitespace = None;
} else if !is_white_space_single_line(c) {
last_non_whitespace = Some(index + c.len_utf8());
if first_non_whitespace.is_none() {
first_non_whitespace.replace(index);
}
}
}
if let Some(first) = first_non_whitespace {
Self::add_line_of_jsx_text(
Str::from(&text.as_str()[first..]),
&mut acc,
&mut only_line,
text.len(),
ctx,
);
}
if let Some(acc) = acc { Some(Str::from(acc)) } else { only_line }
}
fn add_line_of_jsx_text(
trimmed_line: Str<'a>,
acc: &mut Option<ArenaStringBuilder<'a>>,
only_line: &mut Option<Str<'a>>,
text_len: usize,
ctx: &TraverseCtx<'a>,
) {
if let Some(buffer) = acc.as_mut() {
buffer.push(' ');
} else if let Some(only_line) = only_line.take() {
let mut buffer = ArenaStringBuilder::with_capacity_in(text_len, ctx.ast.allocator);
buffer.push_str(only_line.as_str());
buffer.push(' ');
*acc = Some(buffer);
}
Self::decode_entities(trimmed_line.as_str(), acc, text_len, ctx);
if acc.is_none() {
*only_line = Some(trimmed_line);
}
}
fn decode_entities(
s: &str,
acc: &mut Option<ArenaStringBuilder<'a>>,
text_len: usize,
ctx: &TraverseCtx<'a>,
) {
let mut chars = s.char_indices();
let mut prev = 0;
while let Some((i, c)) = chars.next() {
if c == '&' {
let mut start = i;
let mut end = None;
for (j, c) in chars.by_ref() {
if c == ';' {
end.replace(j);
break;
} else if c == '&' {
start = j;
}
}
if let Some(end) = end {
let buffer = acc.get_or_insert_with(|| {
ArenaStringBuilder::with_capacity_in(text_len, ctx.ast.allocator)
});
buffer.push_str(&s[prev..start]);
prev = end + 1;
let word = &s[start + 1..end];
if let Some(decimal) = word.strip_prefix('#') {
if let Some(hex) = decimal.strip_prefix('x') {
if let Some(c) =
u32::from_str_radix(hex, 16).ok().and_then(char::from_u32)
{
buffer.push(c);
continue;
}
} else if let Some(c) = decimal.parse::<u32>().ok().and_then(char::from_u32)
{
buffer.push(c);
continue;
}
} else if let Some(c) = XML_ENTITIES.get(word) {
buffer.push(*c);
continue;
}
buffer.push('&');
buffer.push_str(word);
buffer.push(';');
} else {
break;
}
}
}
if let Some(buffer) = acc.as_mut() {
buffer.push_str(&s[prev..]);
}
}
fn has_key_after_props_spread(opening_element: &JSXOpeningElement<'a>) -> bool {
let mut spread = false;
for attr in &opening_element.attributes {
if matches!(attr, JSXAttributeItem::SpreadAttribute(_)) {
spread = true;
} else if spread && matches!(attr, JSXAttributeItem::Attribute(a) if a.is_key()) {
return true;
}
}
false
}
fn delete_reference_for_closing_element(
element: Option<&JSXClosingElement<'a>>,
ctx: &mut TraverseCtx<'a>,
) {
if let Some(element) = &element
&& let Some(ident) = element.name.get_identifier()
{
ctx.delete_reference_for_identifier(ident);
}
}
}
fn get_read_identifier_reference<'a>(
span: Span,
name: Str<'a>,
ctx: &mut TraverseCtx<'a>,
) -> Expression<'a> {
let name = Ident::from(name);
let reference_id = ctx.create_reference_in_current_scope(name, ReferenceFlags::Read);
let ident = ctx.ast.alloc_identifier_reference_with_reference_id(span, name, reference_id);
Expression::Identifier(ident)
}
fn create_static_member_expression<'a>(
object_ident: IdentifierReference<'a>,
property_name: Str<'a>,
ctx: &TraverseCtx<'a>,
) -> Expression<'a> {
let object = Expression::Identifier(ctx.alloc(object_ident));
let property = ctx.ast.identifier_name(SPAN, property_name);
ctx.ast.member_expression_static(SPAN, object, property, false).into()
}
fn has_proto(e: &ObjectExpression<'_>) -> bool {
e.properties.iter().any(|p| p.prop_name().is_some_and(|(name, _)| name == "__proto__"))
}
#[cfg(test)]
mod test {
use std::path::Path;
use oxc_allocator::Allocator;
use oxc_ast::ast::Expression;
use oxc_semantic::Scoping;
use oxc_syntax::{node::NodeId, scope::ScopeFlags};
use oxc_traverse::ReusableTraverseCtx;
use super::Pragma;
use crate::{TransformCtx, TransformOptions, state::TransformState};
macro_rules! setup {
($traverse_ctx:ident, $transform_ctx:ident) => {
let allocator = Allocator::default();
let mut scoping = Scoping::default();
scoping.add_scope(None, NodeId::DUMMY, ScopeFlags::Top);
let state = TransformState::default();
let traverse_ctx = ReusableTraverseCtx::new(state, scoping, &allocator);
let mut traverse_ctx = unsafe { traverse_ctx.unwrap() };
let $traverse_ctx = &mut traverse_ctx;
let mut $transform_ctx =
TransformCtx::new(Path::new("test.jsx"), &TransformOptions::default());
};
}
#[test]
fn default_pragma() {
setup!(traverse_ctx, transform_ctx);
let pragma = None;
let pragma = Pragma::parse(pragma, "createElement", traverse_ctx.ast, &mut transform_ctx);
let expr = pragma.create_expression(traverse_ctx);
let Expression::StaticMemberExpression(member) = &expr else { panic!() };
let Expression::Identifier(object) = &member.object else { panic!() };
assert_eq!(object.name, "React");
assert_eq!(member.property.name, "createElement");
}
#[test]
fn single_part_pragma() {
setup!(traverse_ctx, transform_ctx);
let pragma = Some("single");
let pragma = Pragma::parse(pragma, "createElement", traverse_ctx.ast, &mut transform_ctx);
let expr = pragma.create_expression(traverse_ctx);
let Expression::Identifier(ident) = &expr else { panic!() };
assert_eq!(ident.name, "single");
}
#[test]
fn two_part_pragma() {
setup!(traverse_ctx, transform_ctx);
let pragma = Some("first.second");
let pragma = Pragma::parse(pragma, "createElement", traverse_ctx.ast, &mut transform_ctx);
let expr = pragma.create_expression(traverse_ctx);
let Expression::StaticMemberExpression(member) = &expr else { panic!() };
let Expression::Identifier(object) = &member.object else { panic!() };
assert_eq!(object.name, "first");
assert_eq!(member.property.name, "second");
}
#[test]
fn multi_part_pragma() {
setup!(traverse_ctx, transform_ctx);
let pragma = Some("first.second.third");
let pragma = Pragma::parse(pragma, "createElement", traverse_ctx.ast, &mut transform_ctx);
let expr = pragma.create_expression(traverse_ctx);
let Expression::StaticMemberExpression(outer_member) = &expr else { panic!() };
let Expression::StaticMemberExpression(inner_member) = &outer_member.object else {
panic!()
};
let Expression::Identifier(object) = &inner_member.object else { panic!() };
assert_eq!(object.name, "first");
assert_eq!(inner_member.property.name, "second");
assert_eq!(outer_member.property.name, "third");
}
#[test]
fn this_pragma() {
setup!(traverse_ctx, transform_ctx);
let pragma = Some("this");
let pragma = Pragma::parse(pragma, "createElement", traverse_ctx.ast, &mut transform_ctx);
let expr = pragma.create_expression(traverse_ctx);
assert!(matches!(&expr, Expression::ThisExpression(_)));
}
#[test]
fn this_prop_pragma() {
setup!(traverse_ctx, transform_ctx);
let pragma = Some("this.a.b");
let pragma = Pragma::parse(pragma, "createElement", traverse_ctx.ast, &mut transform_ctx);
let expr = pragma.create_expression(traverse_ctx);
let Expression::StaticMemberExpression(outer_member) = &expr else { panic!() };
let Expression::StaticMemberExpression(inner_member) = &outer_member.object else {
panic!()
};
assert!(matches!(&inner_member.object, Expression::ThisExpression(_)));
assert_eq!(inner_member.property.name, "a");
assert_eq!(outer_member.property.name, "b");
}
#[test]
fn import_meta_pragma() {
setup!(traverse_ctx, transform_ctx);
let pragma = Some("import.meta");
let pragma = Pragma::parse(pragma, "createElement", traverse_ctx.ast, &mut transform_ctx);
let expr = pragma.create_expression(traverse_ctx);
let Expression::MetaProperty(meta_prop) = &expr else { panic!() };
assert_eq!(&meta_prop.meta.name, "import");
assert_eq!(&meta_prop.property.name, "meta");
}
#[test]
fn import_meta_prop_pragma() {
setup!(traverse_ctx, transform_ctx);
let pragma = Some("import.meta.prop");
let pragma = Pragma::parse(pragma, "createElement", traverse_ctx.ast, &mut transform_ctx);
let expr = pragma.create_expression(traverse_ctx);
let Expression::StaticMemberExpression(member) = &expr else { panic!() };
let Expression::MetaProperty(meta_prop) = &member.object else { panic!() };
assert_eq!(&meta_prop.meta.name, "import");
assert_eq!(&meta_prop.property.name, "meta");
assert_eq!(member.property.name, "prop");
}
#[test]
fn invalid_pragma_falls_back_to_default() {
setup!(traverse_ctx, _transform_ctx);
let pragma = Some("`");
let pragma = Pragma::parse_no_ctx(pragma, "Fragment", traverse_ctx.ast);
let expr = pragma.create_expression(traverse_ctx);
let Expression::StaticMemberExpression(member) = &expr else { panic!() };
let Expression::Identifier(object) = &member.object else { panic!() };
assert_eq!(object.name, "React");
assert_eq!(member.property.name, "Fragment");
}
#[test]
fn entity_after_stray_amp() {
setup!(traverse_ctx, _transform_ctx);
let input = "& &";
let mut acc = None;
super::JsxImpl::decode_entities(input, &mut acc, input.len(), traverse_ctx);
let out = acc.as_ref().unwrap().as_str();
assert_eq!(out, "& &");
}
}