Skip to content

Commit e36ca9a

Browse files
committed
feat(transformer/react-refresh): support checking a variable whether is used in JSX elements or JSX-like calls (#11859)
Fixes: #11824 We have to introduce a visitor to pre-collect all bindings that are used in JSX elements or JSX-like calls, because a variable can be referenced before or after the declaration.
1 parent 5efc8ab commit e36ca9a

File tree

2 files changed

+96
-14
lines changed

2 files changed

+96
-14
lines changed

crates/oxc_transformer/src/jsx/refresh.rs

Lines changed: 94 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,18 @@ use base64::{
44
encoded_len as base64_encoded_len,
55
prelude::{BASE64_STANDARD, Engine},
66
};
7-
use rustc_hash::FxHashMap;
7+
use rustc_hash::{FxHashMap, FxHashSet};
88
use sha1::{Digest, Sha1};
99

1010
use oxc_allocator::{
1111
Address, CloneIn, GetAddress, StringBuilder as ArenaStringBuilder, TakeIn, Vec as ArenaVec,
1212
};
1313
use oxc_ast::{AstBuilder, NONE, ast::*, match_expression};
14-
use oxc_semantic::{Reference, ReferenceFlags, ScopeFlags, ScopeId, SymbolFlags};
14+
use oxc_ast_visit::{
15+
Visit,
16+
walk::{walk_call_expression, walk_declaration},
17+
};
18+
use oxc_semantic::{ReferenceFlags, ScopeFlags, ScopeId, SymbolFlags, SymbolId};
1519
use oxc_span::{Atom, GetSpan, SPAN};
1620
use oxc_syntax::operator::AssignmentOperator;
1721
use oxc_traverse::{Ancestor, BoundIdentifier, Traverse};
@@ -120,6 +124,8 @@ pub struct ReactRefresh<'a, 'ctx> {
120124
// (function_scope_id, key)
121125
function_signature_keys: FxHashMap<ScopeId, String>,
122126
non_builtin_hooks_callee: FxHashMap<ScopeId, Vec<Option<Expression<'a>>>>,
127+
/// Used to determine which bindings are used in JSX calls.
128+
used_in_jsx_bindings: FxHashSet<SymbolId>,
123129
}
124130

125131
impl<'a, 'ctx> ReactRefresh<'a, 'ctx> {
@@ -137,12 +143,15 @@ impl<'a, 'ctx> ReactRefresh<'a, 'ctx> {
137143
last_signature: None,
138144
function_signature_keys: FxHashMap::default(),
139145
non_builtin_hooks_callee: FxHashMap::default(),
146+
used_in_jsx_bindings: FxHashSet::default(),
140147
}
141148
}
142149
}
143150

144151
impl<'a> Traverse<'a, TransformState<'a>> for ReactRefresh<'a, '_> {
145152
fn enter_program(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) {
153+
self.used_in_jsx_bindings = UsedInJSXBindingsCollector::collect(program, ctx);
154+
146155
let mut new_statements = ctx.ast.vec_with_capacity(program.body.len() * 2);
147156
for mut statement in program.body.take_in(ctx.ast) {
148157
let next_statement = self.process_statement(&mut statement, ctx);
@@ -808,13 +817,8 @@ impl<'a> ReactRefresh<'a, '_> {
808817
let found_inside = self
809818
.replace_inner_components(&id.name, init, /* is_variable_declarator */ true, ctx);
810819

811-
if !found_inside {
812-
// See if this identifier is used in JSX. Then it's a component.
813-
// TODO: Here we should check if the variable is used in JSX. But now we only check if it has value references.
814-
// https://github.com/facebook/react/blob/ba6a9e94edf0db3ad96432804f9931ce9dc89fec/packages/react-refresh/src/ReactFreshBabelPlugin.js#L161-L199
815-
if !ctx.scoping().get_resolved_references(symbol_id).any(Reference::is_value) {
816-
return None;
817-
}
820+
if !found_inside && !self.used_in_jsx_bindings.contains(&symbol_id) {
821+
return None;
818822
}
819823

820824
Some(self.create_assignment_expression(id, ctx))
@@ -913,3 +917,84 @@ fn is_builtin_hook(hook_name: &str) -> bool {
913917
"useOptimistic"
914918
)
915919
}
920+
921+
/// Collects all bindings that are used in JSX elements or JSX-like calls.
922+
///
923+
/// For <https://github.com/facebook/react/blob/ba6a9e94edf0db3ad96432804f9931ce9dc89fec/packages/react-refresh/src/ReactFreshBabelPlugin.js#L161-L199>
924+
struct UsedInJSXBindingsCollector<'a, 'b> {
925+
ctx: &'b TraverseCtx<'a>,
926+
bindings: FxHashSet<SymbolId>,
927+
}
928+
929+
impl<'a, 'b> UsedInJSXBindingsCollector<'a, 'b> {
930+
fn collect(program: &Program<'a>, ctx: &'b TraverseCtx<'a>) -> FxHashSet<SymbolId> {
931+
let mut visitor = Self { ctx, bindings: FxHashSet::default() };
932+
visitor.visit_program(program);
933+
visitor.bindings
934+
}
935+
936+
fn is_jsx_like_call(name: &str) -> bool {
937+
matches!(name, "createElement" | "jsx" | "jsxDEV" | "jsxs")
938+
}
939+
}
940+
941+
impl<'a> Visit<'a> for UsedInJSXBindingsCollector<'a, '_> {
942+
fn visit_call_expression(&mut self, it: &CallExpression<'a>) {
943+
walk_call_expression(self, it);
944+
945+
let is_jsx_call = match &it.callee {
946+
Expression::Identifier(ident) => Self::is_jsx_like_call(&ident.name),
947+
Expression::StaticMemberExpression(member) => {
948+
Self::is_jsx_like_call(&member.property.name)
949+
}
950+
_ => false,
951+
};
952+
953+
if is_jsx_call {
954+
if let Some(Argument::Identifier(ident)) = it.arguments.first() {
955+
if let Some(symbol_id) =
956+
self.ctx.scoping().get_reference(ident.reference_id()).symbol_id()
957+
{
958+
self.bindings.insert(symbol_id);
959+
}
960+
}
961+
}
962+
}
963+
964+
fn visit_jsx_opening_element(&mut self, it: &JSXOpeningElement<'_>) {
965+
if let Some(ident) = it.name.get_identifier() {
966+
if let Some(symbol_id) =
967+
self.ctx.scoping().get_reference(ident.reference_id()).symbol_id()
968+
{
969+
self.bindings.insert(symbol_id);
970+
}
971+
}
972+
}
973+
974+
#[inline]
975+
fn visit_ts_type_annotation(&mut self, _it: &TSTypeAnnotation<'a>) {
976+
// Skip type annotations because it definitely doesn't have any JSX bindings
977+
}
978+
979+
#[inline]
980+
fn visit_declaration(&mut self, it: &Declaration<'a>) {
981+
if matches!(
982+
it,
983+
Declaration::TSTypeAliasDeclaration(_) | Declaration::TSInterfaceDeclaration(_)
984+
) {
985+
// Skip type-only declarations because it definitely doesn't have any JSX bindings
986+
return;
987+
}
988+
walk_declaration(self, it);
989+
}
990+
991+
#[inline]
992+
fn visit_import_declaration(&mut self, _it: &ImportDeclaration<'a>) {
993+
// Skip import declarations because it definitely doesn't have any JSX bindings
994+
}
995+
996+
#[inline]
997+
fn visit_export_all_declaration(&mut self, _it: &ExportAllDeclaration<'a>) {
998+
// Skip export all declarations because it definitely doesn't have any JSX bindings
999+
}
1000+
}

tasks/transform_conformance/snapshots/oxc.snap.md

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
commit: 1d4546bc
22

3-
Passed: 155/259
3+
Passed: 156/259
44

55
# All Passed:
66
* babel-plugin-transform-class-static-block
@@ -467,10 +467,7 @@ after transform: [ReferenceId(0), ReferenceId(1), ReferenceId(4), ReferenceId(9)
467467
rebuilt : [ReferenceId(5)]
468468

469469

470-
# babel-plugin-transform-react-jsx (43/46)
471-
* refresh/does-not-transform-it-because-it-is-not-used-in-the-AST/input.jsx
472-
x Output mismatch
473-
470+
# babel-plugin-transform-react-jsx (44/46)
474471
* refresh/react-refresh/includes-custom-hooks-into-the-signatures-when-commonjs-target-is-used/input.jsx
475472
x Output mismatch
476473

0 commit comments

Comments
 (0)