refactor(es/react/fast-refresh): Use VisitMut (#3129)

This commit is contained in:
Austaras 2021-12-28 05:26:59 -08:00 committed by GitHub
parent 2ab65c2cea
commit f8f04e031e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 760 additions and 698 deletions

View File

@ -0,0 +1,523 @@
use std::mem;
use indexmap::IndexSet;
use once_cell::sync::Lazy;
use regex::Regex;
use sha1::{Digest, Sha1};
use swc_atoms::JsWord;
use swc_common::{util::take::Take, SourceMap, Spanned, DUMMY_SP};
use swc_ecma_ast::*;
use swc_ecma_utils::{private_ident, quote_ident, ExprFactory};
use swc_ecma_visit::{
noop_visit_mut_type, noop_visit_type, Visit, VisitMut, VisitMutWith, VisitWith,
};
use crate::RefreshOptions;
use super::util::{is_builtin_hook, make_call_expr, make_call_stmt, CollectIdent};
// function that use hooks
struct HookSig {
handle: Ident,
// need to add an extra register, or alreay inlined
hooks: Vec<Hook>,
}
impl HookSig {
fn new(hooks: Vec<Hook>) -> Self {
HookSig {
handle: private_ident!("_s"),
hooks,
}
}
}
struct Hook {
callee: HookCall,
key: String,
}
// we only consider two kinds of callee as hook call
enum HookCall {
Ident(Ident),
Member(Expr, Ident), // for obj and prop
}
pub struct HookRegister<'a> {
pub options: &'a RefreshOptions,
pub ident: Vec<Ident>,
pub extra_stmt: Vec<Stmt>,
pub scope_binding: IndexSet<JsWord>,
pub cm: &'a SourceMap,
pub should_reset: bool,
}
impl<'a> HookRegister<'a> {
pub fn gen_hook_handle(&mut self) -> Stmt {
Stmt::Decl(Decl::Var(VarDecl {
span: DUMMY_SP,
kind: VarDeclKind::Var,
decls: self
.ident
.take()
.into_iter()
.map(|id| VarDeclarator {
span: DUMMY_SP,
name: Pat::Ident(BindingIdent::from(id)),
init: Some(Box::new(make_call_expr(quote_ident!(self
.options
.refresh_sig
.clone())))),
definite: false,
})
.collect(),
declare: false,
}))
}
// The second call is around the function itself. This is used to associate a
// type with a signature.
// Unlike with $RefreshReg$, this needs to work for nested declarations too.
fn wrap_with_register(&self, handle: Ident, func: Expr, hooks: Vec<Hook>) -> Expr {
let mut args = vec![func.as_arg()];
let mut sign = Vec::new();
let mut custom_hook = Vec::new();
for hook in hooks {
let name = match &hook.callee {
HookCall::Ident(i) => i,
HookCall::Member(_, i) => i,
};
sign.push(format!("{}{{{}}}", name.sym, hook.key));
match &hook.callee {
HookCall::Ident(ident) if !is_builtin_hook(ident) => {
custom_hook.push(hook.callee);
}
HookCall::Member(obj, prop) if !is_builtin_hook(prop) => {
if let Expr::Ident(ident) = obj {
if ident.sym.as_ref() != "React" {
custom_hook.push(hook.callee);
}
}
}
_ => (),
};
}
// this is just for pass test
let has_escape = sign.len() > 1;
let sign = sign.join("\n");
let sign = if self.options.emit_full_signatures {
sign
} else {
let mut hasher = Sha1::new();
hasher.update(sign);
base64::encode(hasher.finalize())
};
args.push(
Expr::Lit(Lit::Str(Str {
span: DUMMY_SP,
value: sign.into(),
has_escape,
kind: StrKind::Synthesized,
}))
.as_arg(),
);
let mut should_reset = self.should_reset;
let mut custom_hook_in_scope = Vec::new();
for hook in custom_hook {
let ident = match &hook {
HookCall::Ident(ident) => Some(ident),
HookCall::Member(Expr::Ident(ident), _) => Some(ident),
_ => None,
};
if let None = ident.and_then(|id| self.scope_binding.get(&id.sym)) {
// We don't have anything to put in the array because Hook is out of scope.
// Since it could potentially have been edited, remount the component.
should_reset = true;
} else {
custom_hook_in_scope.push(hook);
}
}
if should_reset || custom_hook_in_scope.len() > 0 {
args.push(
Expr::Lit(Lit::Bool(Bool {
span: DUMMY_SP,
value: should_reset,
}))
.as_arg(),
);
}
if custom_hook_in_scope.len() > 0 {
let elems = custom_hook_in_scope
.into_iter()
.map(|hook| {
Some(ExprOrSpread {
spread: None,
expr: Box::new(match hook {
HookCall::Ident(ident) => Expr::Ident(ident),
HookCall::Member(obj, prop) => Expr::Member(MemberExpr {
span: DUMMY_SP,
obj: ExprOrSuper::Expr(Box::new(obj)),
prop: Box::new(Expr::Ident(prop)),
computed: false,
}),
}),
})
})
.collect();
args.push(
Expr::Fn(FnExpr {
ident: None,
function: Function {
is_generator: false,
is_async: false,
params: Vec::new(),
decorators: Vec::new(),
span: DUMMY_SP,
body: Some(BlockStmt {
span: DUMMY_SP,
stmts: vec![Stmt::Return(ReturnStmt {
span: DUMMY_SP,
arg: Some(Box::new(Expr::Array(ArrayLit {
span: DUMMY_SP,
elems,
}))),
})],
}),
type_params: None,
return_type: None,
},
})
.as_arg(),
);
}
Expr::Call(CallExpr {
span: DUMMY_SP,
callee: ExprOrSuper::Expr(Box::new(Expr::Ident(handle))),
args,
type_args: None,
})
}
fn gen_hook_register_stmt(&mut self, ident: Ident, sig: HookSig) {
self.ident.push(sig.handle.clone());
self.extra_stmt.push(Stmt::Expr(ExprStmt {
span: DUMMY_SP,
expr: Box::new(self.wrap_with_register(sig.handle, Expr::Ident(ident), sig.hooks)),
}))
}
}
impl<'a> VisitMut for HookRegister<'a> {
noop_visit_mut_type!();
fn visit_mut_block_stmt(&mut self, b: &mut BlockStmt) {
let mut current_scope = IndexSet::new();
// TODO: merge with collect_decls
for stmt in &b.stmts {
stmt.collect_ident(&mut current_scope);
}
let orig_binding = self.scope_binding.len();
self.scope_binding.extend(current_scope);
let current_binding = self.scope_binding.len();
let old_ident = self.ident.take();
let old_stmts = self.extra_stmt.take();
let stmt_count = b.stmts.len();
let stmts = mem::replace(&mut b.stmts, Vec::with_capacity(stmt_count));
for mut stmt in stmts {
stmt.visit_mut_children_with(self);
self.scope_binding.truncate(current_binding);
b.stmts.push(stmt);
b.stmts.append(&mut self.extra_stmt);
}
if self.ident.len() > 0 {
b.stmts.insert(0, self.gen_hook_handle())
}
self.scope_binding.truncate(orig_binding);
self.ident = old_ident;
self.extra_stmt = old_stmts;
}
fn visit_mut_expr(&mut self, e: &mut Expr) {
e.visit_mut_children_with(self);
match e {
Expr::Fn(FnExpr {
function: Function {
body: Some(body), ..
},
..
}) => {
let sig = collect_hooks(&mut body.stmts, self.cm);
if let Some(HookSig { handle, hooks }) = sig {
self.ident.push(handle.clone());
*e = self.wrap_with_register(handle, e.take(), hooks);
}
}
Expr::Arrow(ArrowExpr { body, .. }) => {
let sig = collect_hooks_arrow(body, self.cm);
if let Some(HookSig { handle, hooks }) = sig {
self.ident.push(handle.clone());
*e = self.wrap_with_register(handle, e.take(), hooks);
}
}
_ => (),
}
}
fn visit_mut_var_decl(&mut self, n: &mut VarDecl) {
// we don't want visit_mut_expr to mess up with function name inference
// so intercept it here
for decl in n.decls.iter_mut() {
if let VarDeclarator {
// it doesn't quite make sense for other Pat to appear here
name: Pat::Ident(BindingIdent { id, .. }),
init: Some(init),
..
} = decl
{
match init.as_mut() {
Expr::Fn(FnExpr {
function:
Function {
body: Some(body), ..
},
..
}) => {
if let Some(sig) = collect_hooks(&mut body.stmts, self.cm) {
self.gen_hook_register_stmt(id.clone(), sig);
}
}
Expr::Arrow(ArrowExpr { body, .. }) => {
if let Some(sig) = collect_hooks_arrow(body, self.cm) {
self.gen_hook_register_stmt(id.clone(), sig);
}
}
_ => self.visit_mut_expr(init),
}
} else {
decl.visit_mut_children_with(self)
}
}
}
fn visit_mut_default_decl(&mut self, d: &mut DefaultDecl) {
d.visit_mut_children_with(self);
// only when expr has ident
if let DefaultDecl::Fn(FnExpr {
ident: Some(ident),
function: Function {
body: Some(body), ..
},
}) = d
{
if let Some(sig) = collect_hooks(&mut body.stmts, self.cm) {
self.gen_hook_register_stmt(ident.clone(), sig);
}
}
}
fn visit_mut_fn_decl(&mut self, f: &mut FnDecl) {
f.visit_mut_children_with(self);
if let Some(body) = &mut f.function.body {
if let Some(sig) = collect_hooks(&mut body.stmts, self.cm) {
self.gen_hook_register_stmt(f.ident.clone(), sig);
}
}
}
}
fn collect_hooks(stmts: &mut Vec<Stmt>, cm: &SourceMap) -> Option<HookSig> {
let mut hook = HookCollector {
state: Vec::new(),
cm,
};
stmts.visit_with(&mut hook);
if hook.state.len() > 0 {
let sig = HookSig::new(hook.state);
stmts.insert(0, make_call_stmt(sig.handle.clone()));
Some(sig)
} else {
None
}
}
fn collect_hooks_arrow(body: &mut BlockStmtOrExpr, cm: &SourceMap) -> Option<HookSig> {
match body {
BlockStmtOrExpr::BlockStmt(block) => collect_hooks(&mut block.stmts, cm),
BlockStmtOrExpr::Expr(expr) => {
let mut hook = HookCollector {
state: Vec::new(),
cm,
};
expr.visit_with(&mut hook);
if hook.state.len() > 0 {
let sig = HookSig::new(hook.state);
*body = BlockStmtOrExpr::BlockStmt(BlockStmt {
span: expr.span(),
stmts: vec![
make_call_stmt(sig.handle.clone()),
Stmt::Return(ReturnStmt {
span: expr.span(),
arg: Some(Box::new(expr.as_mut().take())),
}),
],
});
Some(sig)
} else {
None
}
}
}
}
struct HookCollector<'a> {
state: Vec<Hook>,
cm: &'a SourceMap,
}
static IS_HOOK_LIKE: Lazy<Regex> = Lazy::new(|| Regex::new("^use[A-Z]").unwrap());
impl<'a> HookCollector<'a> {
fn get_hook_from_call_expr(&self, expr: &CallExpr, lhs: Option<&Pat>) -> Option<Hook> {
let callee = if let ExprOrSuper::Expr(callee) = &expr.callee {
Some(callee.as_ref())
} else {
None
}?;
let mut hook_call = None;
let ident = match callee {
Expr::Ident(ident) => {
hook_call = Some(HookCall::Ident(ident.clone()));
Some(ident)
}
Expr::Member(MemberExpr {
obj: ExprOrSuper::Expr(obj),
prop,
..
}) => {
if let Expr::Ident(ident) = prop.as_ref() {
hook_call = Some(HookCall::Member(*obj.clone(), ident.clone()));
Some(ident)
} else {
None
}
}
_ => None,
}?;
let name = if IS_HOOK_LIKE.is_match(&ident.sym) {
Some(ident)
} else {
None
}?;
let mut key = if let Some(name) = lhs {
self.cm
.span_to_snippet(name.span())
.unwrap_or_else(|_| String::new())
} else {
String::new()
};
// Some built-in Hooks reset on edits to arguments.
if &name.sym == "useState" && expr.args.len() > 0 {
// useState first argument is initial state.
key += &format!(
"({})",
self.cm
.span_to_snippet(expr.args[0].span())
.unwrap_or_else(|_| String::new())
);
} else if &name.sym == "useReducer" && expr.args.len() > 1 {
// useReducer second argument is initial state.
key += &format!(
"({})",
self.cm
.span_to_snippet(expr.args[1].span())
.unwrap_or("".to_string())
);
}
let callee = hook_call?;
Some(Hook { callee, key })
}
fn get_hook_from_expr(&self, expr: &Expr, lhs: Option<&Pat>) -> Option<Hook> {
if let Expr::Call(call) = expr {
self.get_hook_from_call_expr(call, lhs)
} else {
None
}
}
}
impl<'a> Visit for HookCollector<'a> {
noop_visit_type!();
fn visit_block_stmt_or_expr(&mut self, _: &BlockStmtOrExpr) {}
fn visit_block_stmt(&mut self, _: &BlockStmt) {}
fn visit_expr(&mut self, expr: &Expr) {
expr.visit_children_with(self);
if let Expr::Call(call) = expr {
if let Some(hook) = self.get_hook_from_call_expr(call, None) {
self.state.push(hook)
}
}
}
fn visit_stmt(&mut self, stmt: &Stmt) {
match stmt {
Stmt::Expr(ExprStmt { expr, .. }) => {
if let Some(hook) = self.get_hook_from_expr(expr, None) {
self.state.push(hook)
} else {
stmt.visit_children_with(self)
}
}
Stmt::Decl(Decl::Var(var_decl)) => {
for decl in &var_decl.decls {
if let Some(init) = &decl.init {
if let Some(hook) = self.get_hook_from_expr(init, Some(&decl.name)) {
self.state.push(hook)
} else {
stmt.visit_children_with(self)
}
} else {
stmt.visit_children_with(self)
}
}
}
Stmt::Return(ReturnStmt { arg: Some(arg), .. }) => {
if let Some(hook) = self.get_hook_from_expr(arg.as_ref(), None) {
self.state.push(hook)
} else {
stmt.visit_children_with(self)
}
}
_ => stmt.visit_children_with(self),
}
}
}

View File

@ -1,30 +1,23 @@
use self::util::{
callee_should_ignore, gen_custom_hook_record, is_body_arrow_fn, is_builtin_hook,
is_import_or_require, make_assign_stmt, make_call_expr, make_call_stmt, CollectIdent,
use self::{
hook::HookRegister,
util::{
collect_ident_in_jsx, is_body_arrow_fn, is_import_or_require, make_assign_stmt,
CollectIdent,
},
};
use base64;
use indexmap::IndexSet;
use once_cell::sync::Lazy;
use regex::Regex;
use sha1::{Digest, Sha1};
use std::{
collections::{HashMap, HashSet},
mem,
};
use swc_atoms::JsWord;
use swc_common::{
collections::{AHashMap, AHashSet},
comments::Comments,
sync::Lrc,
util::take::Take,
BytePos, SourceMap, Span, Spanned, SyntaxContext, DUMMY_SP,
collections::AHashSet, comments::Comments, sync::Lrc, util::take::Take, BytePos, SourceMap,
Span, Spanned, SyntaxContext, DUMMY_SP,
};
use swc_ecma_ast::*;
use swc_ecma_utils::{ident::IdentLike, private_ident, quote_ident, quote_str, Id};
use swc_ecma_visit::{Fold, FoldWith, Visit};
use swc_ecma_visit::{as_folder, Fold, Visit, VisitMut, VisitMutWith};
pub mod options;
use options::RefreshOptions;
mod hook;
mod util;
#[cfg(test)]
@ -33,13 +26,11 @@ mod tests;
struct Hoc {
insert: bool,
reg: Vec<(Ident, Id)>,
// use to register hook for all level of HOC, first is hook register ident
// rest is hook register call args
hook: Option<HocHook>,
}
struct HocHook {
ident: ExprOrSuper,
args: Vec<ExprOrSpread>,
callee: ExprOrSuper,
rest_arg: Vec<ExprOrSpread>,
}
enum Persist {
Hoc(Hoc),
@ -59,55 +50,22 @@ fn get_persistent_id(ident: &Ident) -> Persist {
}
}
fn hook_to_handle_map(hook_fn: Vec<FnWithHook>) -> (AHashMap<Ident, FnWithHook>, AHashSet<Ident>) {
let mut has_ident = HashMap::default();
let mut ignore = HashSet::default();
for hook in hook_fn {
if let Some(binding) = &hook.binding {
has_ident.insert(binding.clone(), hook);
} else {
ignore.insert(hook.handle);
}
}
(has_ident, ignore)
}
// function that use hooks
struct FnWithHook {
binding: Option<Ident>, // ident of function
handle: Ident, // variable to register
hook: Vec<Hook>,
}
struct Hook {
name: Ident,
callee: HookCall,
key: String,
}
// we only consider two kinds of expr as hook call
enum HookCall {
Ident(Ident),
Member(Expr, Ident), // for obj and prop
}
/// `react-refresh/babel`
/// https://github.com/facebook/react/blob/master/packages/react-refresh/src/ReactFreshBabelPlugin.js
/// https://github.com/facebook/react/blob/main/packages/react-refresh/src/ReactFreshBabelPlugin.js
pub fn refresh<C: Comments>(
dev: bool,
options: Option<RefreshOptions>,
cm: Lrc<SourceMap>,
comments: Option<C>,
) -> impl Fold {
Refresh {
) -> impl Fold + VisitMut {
as_folder(Refresh {
enable: dev && options.is_some(),
cm,
comments,
should_reset: false,
options: options.unwrap_or(Default::default()),
used_in_jsx: HashSet::default(),
curr_hook_fn: Vec::new(),
scope_binding: IndexSet::new(),
}
})
}
struct Refresh<C: Comments> {
@ -115,300 +73,17 @@ struct Refresh<C: Comments> {
options: RefreshOptions,
cm: Lrc<SourceMap>,
should_reset: bool,
used_in_jsx: AHashSet<JsWord>,
comments: Option<C>,
curr_hook_fn: Vec<FnWithHook>,
// bindings in current and all parent scope
scope_binding: IndexSet<JsWord>,
}
static IS_HOOK_LIKE: Lazy<Regex> = Lazy::new(|| Regex::new("^use[A-Z]").unwrap());
impl<C: Comments> Refresh<C> {
fn get_hook_from_call_expr(&self, expr: &CallExpr, lhs: Option<&Pat>) -> Option<Hook> {
let callee = if let ExprOrSuper::Expr(callee) = &expr.callee {
Some(callee.as_ref())
} else {
None
}?;
let mut hook_call = None;
let ident = match callee {
Expr::Ident(ident) => {
hook_call = Some(HookCall::Ident(ident.clone()));
Some(ident)
}
Expr::Member(MemberExpr {
obj: ExprOrSuper::Expr(obj),
prop,
..
}) => {
if let Expr::Ident(ident) = prop.as_ref() {
hook_call = Some(HookCall::Member(*obj.clone(), ident.clone()));
Some(ident)
} else {
None
}
}
_ => None,
}?;
let name = if IS_HOOK_LIKE.is_match(&ident.sym) {
Some(ident.clone())
} else {
None
}?;
let mut key = if let Some(name) = lhs {
self.cm
.span_to_snippet(name.span())
.unwrap_or("".to_string())
} else {
"".to_string()
};
// Some built-in Hooks reset on edits to arguments.
if &name.sym == "useState" && expr.args.len() > 0 {
// useState second argument is initial state.
key += &format!(
"({})",
self.cm
.span_to_snippet(expr.args[0].span())
.unwrap_or("".to_string())
);
} else if &name.sym == "useReducer" && expr.args.len() > 1 {
// useReducer second argument is initial state.
key += &format!(
"({})",
self.cm
.span_to_snippet(expr.args[1].span())
.unwrap_or("".to_string())
);
}
let callee = hook_call?;
Some(Hook { name, callee, key })
}
fn get_hook_from_expr(&self, expr: &Expr, lhs: Option<&Pat>, reg: &mut Vec<Hook>) {
match expr {
Expr::Paren(ParenExpr { expr, .. }) => {
if let Expr::Seq(SeqExpr { exprs, .. }) = expr.as_ref() {
for expr in exprs {
self.get_hook_from_expr(expr, lhs, reg);
}
}
}
Expr::Seq(SeqExpr { exprs, .. }) => {
for expr in exprs {
self.get_hook_from_expr(expr, lhs, reg);
}
}
Expr::Call(expr) => {
if let Some(hook) = self.get_hook_from_call_expr(expr, lhs) {
reg.push(hook);
}
}
_ => (),
}
}
fn get_hook_sign(&mut self, body: &mut BlockStmt) -> Option<FnWithHook> {
let mut sign_res = Vec::new();
for stmt in &body.stmts {
match stmt {
Stmt::Expr(ExprStmt { expr, .. }) => {
self.get_hook_from_expr(expr, None, &mut sign_res)
}
Stmt::Decl(Decl::Var(var_decl)) => {
for decl in &var_decl.decls {
if let Some(init) = &decl.init {
self.get_hook_from_expr(init, Some(&decl.name), &mut sign_res);
}
}
}
Stmt::Return(ReturnStmt { arg: Some(arg), .. }) => {
self.get_hook_from_expr(arg.as_ref(), None, &mut sign_res)
}
_ => {}
}
}
// The signature call is split in two parts. One part is called inside the
// function. This is used to signal when first render happens.
if sign_res.len() != 0 {
let handle = private_ident!("_s");
body.stmts.insert(0, make_call_stmt(handle.clone()));
Some(FnWithHook {
binding: None,
handle,
hook: sign_res,
})
} else {
None
}
}
fn get_hook_sign_from_arrow(&mut self, body: &mut BlockStmtOrExpr) -> Option<FnWithHook> {
match body {
BlockStmtOrExpr::BlockStmt(stmt) => self.get_hook_sign(stmt),
BlockStmtOrExpr::Expr(expr) => {
let mut hook = Vec::new();
self.get_hook_from_expr(expr, None, &mut hook);
if hook.len() == 0 {
return None;
}
let mut block = BlockStmt {
span: expr.span(),
stmts: Vec::new(),
};
let handle = private_ident!("_s");
block.stmts.push(make_call_stmt(handle.clone()));
block.stmts.push(Stmt::Return(ReturnStmt {
span: expr.span(),
arg: Some(Box::new(expr.as_mut().take())),
}));
*body = BlockStmtOrExpr::BlockStmt(block);
Some(FnWithHook {
binding: None,
handle,
hook,
})
}
}
}
fn gen_hook_handle(&self, curr_hook_fn: &Vec<FnWithHook>) -> Stmt {
let refresh_sig = self.options.refresh_sig.as_str();
Stmt::Decl(Decl::Var(VarDecl {
span: DUMMY_SP,
kind: VarDeclKind::Var,
decls: curr_hook_fn
.iter()
.map(|FnWithHook { ref handle, .. }| VarDeclarator {
span: DUMMY_SP,
name: Pat::Ident(BindingIdent::from(handle.clone())),
init: Some(Box::new(make_call_expr(quote_ident!(refresh_sig)))),
definite: false,
})
.collect(),
declare: false,
}))
}
// The second call is around the function itself. This is used to associate a
// type with a signature.
// Unlike with $RefreshReg$, this needs to work for nested declarations too.
fn gen_hook_register(&self, func: Expr, hook_fn: &mut FnWithHook) -> Expr {
let mut args = vec![func];
let mut sign = Vec::new();
let hooks = mem::take(&mut hook_fn.hook);
let mut custom_hook = Vec::new();
for hook in hooks {
sign.push(format!("{}{{{}}}", hook.name.sym, hook.key));
match &hook.callee {
HookCall::Ident(ident) if !is_builtin_hook(ident) => {
custom_hook.push(hook.callee);
}
HookCall::Member(obj, prop) if !is_builtin_hook(prop) => {
if let Expr::Ident(ident) = obj {
if ident.sym.as_ref() != "React" {
custom_hook.push(hook.callee);
}
}
}
_ => (),
};
}
// this is just for pass test
let has_escape = sign.len() > 1;
let sign = sign.join("\n");
let sign = if self.options.emit_full_signatures {
sign
} else {
let mut hasher = Sha1::new();
hasher.update(sign);
base64::encode(hasher.finalize())
};
args.push(Expr::Lit(Lit::Str(Str {
span: DUMMY_SP,
value: sign.into(),
has_escape,
kind: StrKind::Synthesized,
})));
let mut should_reset = self.should_reset;
let mut custom_hook_in_scope = Vec::new();
for hook in custom_hook {
let ident = match &hook {
HookCall::Ident(ident) => Some(ident),
HookCall::Member(Expr::Ident(ident), _) => Some(ident),
_ => None,
};
if let None = ident.and_then(|id| self.scope_binding.get(&id.sym)) {
// We don't have anything to put in the array because Hook is out ofscope.
// Since it could potentially have been edited, remount the component.
should_reset = true;
} else {
custom_hook_in_scope.push(hook);
}
}
if should_reset || custom_hook_in_scope.len() > 0 {
args.push(Expr::Lit(Lit::Bool(Bool {
span: DUMMY_SP,
value: should_reset,
})));
}
if custom_hook_in_scope.len() > 0 {
let elems = custom_hook_in_scope
.into_iter()
.map(|hook| {
Some(ExprOrSpread {
spread: None,
expr: Box::new(match hook {
HookCall::Ident(ident) => Expr::Ident(ident),
HookCall::Member(obj, prop) => Expr::Member(MemberExpr {
span: DUMMY_SP,
obj: ExprOrSuper::Expr(Box::new(obj)),
prop: Box::new(Expr::Ident(prop)),
computed: false,
}),
}),
})
})
.collect();
args.push(gen_custom_hook_record(elems));
}
Expr::Call(CallExpr {
span: DUMMY_SP,
callee: ExprOrSuper::Expr(Box::new(Expr::Ident(hook_fn.handle.clone()))),
args: args
.into_iter()
.map(|arg| ExprOrSpread {
spread: None,
expr: Box::new(arg),
})
.collect(),
type_args: None,
})
}
fn gen_hook_register_stmt(&self, func: Expr, hook_fn: &mut FnWithHook) -> Stmt {
Stmt::Expr(ExprStmt {
span: DUMMY_SP,
expr: Box::new(self.gen_hook_register(func, hook_fn)),
})
}
fn get_persistent_id_from_var_decl(
&self,
var_decl: &mut VarDecl,
ignore: &AHashSet<Ident>,
used_in_jsx: &AHashSet<JsWord>,
hook_reg: &mut HookRegister,
) -> Persist {
// We only handle the case when a single variable is declared
if let [VarDeclarator {
@ -417,7 +92,7 @@ impl<C: Comments> Refresh<C> {
..
}] = var_decl.decls.as_mut_slice()
{
if self.used_in_jsx.contains(&binding.id.sym) && !is_import_or_require(init_expr) {
if used_in_jsx.contains(&binding.id.sym) && !is_import_or_require(init_expr) {
match init_expr.as_ref() {
// TaggedTpl is for something like styled.div`...`
Expr::Arrow(_) | Expr::Fn(_) | Expr::TaggedTpl(_) | Expr::Call(_) => {
@ -440,11 +115,28 @@ impl<C: Comments> Refresh<C> {
}
}
// Maybe a HOC.
Expr::Call(call_expr) => self.get_persistent_id_from_possible_hoc(
call_expr,
vec![(private_ident!("_c"), persistent_id.to_id())],
ignore,
),
Expr::Call(call_expr) => {
let res = self.get_persistent_id_from_possible_hoc(
call_expr,
vec![(private_ident!("_c"), persistent_id.to_id())],
hook_reg,
);
if let Persist::Hoc(Hoc {
insert,
reg,
hook: Some(hook),
}) = res
{
make_hook_reg(init_expr.as_mut(), hook);
Persist::Hoc(Hoc {
insert,
reg,
hook: None,
})
} else {
res
}
}
_ => Persist::None,
};
}
@ -456,23 +148,8 @@ impl<C: Comments> Refresh<C> {
&self,
call_expr: &mut CallExpr,
mut reg: Vec<(Ident, Id)>,
// sadly unlike orignal implent our transform for component happens before hook
// so we should just ignore hook register
ignore: &AHashSet<Ident>,
hook_reg: &mut HookRegister,
) -> Persist {
if let Some(ident) = callee_should_ignore(ignore, &call_expr.callee) {
// there's at least one item in reg
return if reg.len() > 1 {
let args = call_expr.args[1..].to_vec();
Persist::Hoc(Hoc {
reg,
insert: true,
hook: Some(HocHook { ident, args }),
})
} else {
Persist::None
};
};
let first_arg = match call_expr.args.as_mut_slice() {
[first, ..] => &mut first.expr,
_ => return Persist::None,
@ -497,24 +174,24 @@ impl<C: Comments> Refresh<C> {
let reg_ident = private_ident!("_c");
reg.push((reg_ident.clone(), reg_str));
if let Persist::Hoc(hoc) =
self.get_persistent_id_from_possible_hoc(expr, reg, ignore)
self.get_persistent_id_from_possible_hoc(expr, reg, hook_reg)
{
*first_arg = Box::new(make_assign_stmt(reg_ident.clone(), first_arg.take()));
if let Some(hook) = &hoc.hook {
let span = call_expr.span;
let first = ExprOrSpread {
let mut first = first_arg.take();
if let Some(HocHook { callee, rest_arg }) = &hoc.hook {
let span = first.span();
let mut args = vec![ExprOrSpread {
spread: None,
expr: Box::new(Expr::Call(call_expr.take())),
};
let mut args = vec![first];
args.extend(hook.args.clone());
*call_expr = CallExpr {
expr: first,
}];
args.extend(rest_arg.clone());
first = Box::new(Expr::Call(CallExpr {
span,
callee: hook.ident.clone(),
callee: callee.clone(),
args,
type_args: None,
};
}))
}
*first_arg = Box::new(make_assign_stmt(reg_ident.clone(), first));
Persist::Hoc(hoc)
} else {
@ -523,12 +200,24 @@ impl<C: Comments> Refresh<C> {
}
Expr::Fn(_) | Expr::Arrow(_) => {
let reg_ident = private_ident!("_c");
*first_arg = Box::new(make_assign_stmt(reg_ident.clone(), first_arg.take()));
let mut first = first_arg.take();
first.visit_mut_with(hook_reg);
let hook = if let Expr::Call(call) = first.as_ref() {
let res = Some(HocHook {
callee: call.callee.clone(),
rest_arg: call.args[1..].to_owned(),
});
*first_arg = Box::new(make_assign_stmt(reg_ident.clone(), first));
res
} else {
*first_arg = Box::new(make_assign_stmt(reg_ident.clone(), first));
None
};
reg.push((reg_ident, reg_str));
Persist::Hoc(Hoc {
reg,
insert: true,
hook: None,
hook,
})
}
// export default hoc(Foo)
@ -549,6 +238,7 @@ impl<C: Comments> Refresh<C> {
}
}
/// We let user do /* @refresh reset */ to reset state in the whole file.
impl<C> Visit for Refresh<C>
where
C: Comments,
@ -579,208 +269,46 @@ where
}
}
/// We let user do /* @refresh reset */ to reset state in the whole file.
///
/// TODO: VisitMut
impl<C: Comments> Fold for Refresh<C> {
fn fold_block_stmt(&mut self, n: BlockStmt) -> BlockStmt {
let mut current_scope = IndexSet::new();
// TODO: figure out if we can insert all registers at once
impl<C: Comments> VisitMut for Refresh<C> {
// Does anyone write react without esmodule?
// fn visit_mut_script(&mut self, _: &mut Script) {}
for stmt in &n.stmts {
stmt.collect_ident(&mut current_scope);
}
let orig_bindings = self.scope_binding.len();
self.scope_binding.extend(current_scope.into_iter());
let orig_hook = mem::take(&mut self.curr_hook_fn);
let mut n = n.fold_children_with(self);
let curr_hook = mem::replace(&mut self.curr_hook_fn, orig_hook);
self.scope_binding.truncate(orig_bindings);
if curr_hook.len() > 0 {
let stmt_count = n.stmts.len();
let stmts = mem::replace(&mut n.stmts, Vec::with_capacity(stmt_count));
n.stmts.push(self.gen_hook_handle(&curr_hook));
let (mut handle_map, _) = hook_to_handle_map(curr_hook);
for stmt in stmts {
let mut reg = Vec::new();
match &stmt {
Stmt::Decl(Decl::Fn(FnDecl { ident, .. })) => {
if let Some(hook) = handle_map.remove(ident) {
reg.push((ident.clone(), hook));
}
}
Stmt::Decl(Decl::Var(VarDecl { decls, .. })) => {
for decl in decls {
if let Pat::Ident(BindingIdent { id, .. }) = &decl.name {
if let Some(hook) = handle_map.remove(id) {
reg.push((id.clone(), hook));
}
}
}
}
_ => (),
};
n.stmts.push(stmt);
if reg.len() > 0 {
for (ident, mut hook) in reg {
n.stmts
.push(self.gen_hook_register_stmt(Expr::Ident(ident), &mut hook));
}
}
}
}
n
}
fn fold_call_expr(&mut self, n: CallExpr) -> CallExpr {
let n = n.fold_children_with(self);
if let ExprOrSuper::Expr(expr) = &n.callee {
let ident = match expr.as_ref() {
Expr::Ident(ident) => ident,
Expr::Member(MemberExpr { prop, .. }) => {
if let Expr::Ident(ident) = prop.as_ref() {
ident
} else {
return n;
}
}
_ => return n,
};
match ident.sym.as_ref() {
"createElement" | "jsx" | "jsxDEV" | "jsxs" => {
if let Some(ExprOrSpread { expr, .. }) = n.args.get(0) {
if let Expr::Ident(ident) = expr.as_ref() {
self.used_in_jsx.insert(ident.sym.clone());
}
}
}
_ => (),
}
}
n
}
fn fold_default_decl(&mut self, n: DefaultDecl) -> DefaultDecl {
let mut n = n.fold_children_with(self);
// arrow function is handled in fold_expr
if let DefaultDecl::Fn(FnExpr {
// original implent somehow doesn't handle unnamed default export either
ident: Some(ident),
function: Function {
body: Some(body), ..
},
}) = &mut n
{
if let Some(mut hook) = self.get_hook_sign(body) {
hook.binding = Some(ident.clone());
self.curr_hook_fn.push(hook);
}
}
n
}
fn fold_expr(&mut self, n: Expr) -> Expr {
let n = n.fold_children_with(self);
match n {
Expr::Fn(mut func) => {
if let Some(mut hook) = func
.function
.body
.as_mut()
.and_then(|body| self.get_hook_sign(body))
{
// I don't believe many people would use it, but anyway
let mut should_remove = false;
if let Some(ident) = &func.ident {
self.scope_binding.insert(ident.sym.clone());
should_remove = true;
};
let reg = self.gen_hook_register(Expr::Fn(func), &mut hook);
if should_remove {
self.scope_binding.truncate(self.scope_binding.len() - 1);
};
self.curr_hook_fn.push(hook);
reg
} else {
Expr::Fn(func)
}
}
Expr::Arrow(mut func) => {
if let Some(mut hook) = self.get_hook_sign_from_arrow(&mut func.body) {
let reg = self.gen_hook_register(Expr::Arrow(func), &mut hook);
self.curr_hook_fn.push(hook);
reg
} else {
Expr::Arrow(func)
}
}
Expr::Call(call) => Expr::Call(call),
_ => n,
}
}
fn fold_fn_decl(&mut self, n: FnDecl) -> FnDecl {
let mut n = n.fold_children_with(self);
if let Some(mut hook) = n
.function
.body
.as_mut()
.and_then(|body| self.get_hook_sign(body))
{
hook.binding = Some(n.ident.clone());
self.curr_hook_fn.push(hook);
}
n
}
fn fold_jsx_opening_element(&mut self, n: JSXOpeningElement) -> JSXOpeningElement {
if let JSXElementName::Ident(ident) = &n.name {
self.used_in_jsx.insert(ident.sym.clone());
}
n
}
fn fold_module_items(&mut self, module_items: Vec<ModuleItem>) -> Vec<ModuleItem> {
fn visit_mut_module_items(&mut self, module_items: &mut Vec<ModuleItem>) {
if !self.enable {
return module_items;
return;
}
self.visit_module_items(&module_items);
// to collect comments
self.visit_module_items(module_items);
for item in &module_items {
item.collect_ident(&mut self.scope_binding);
let mut current_scope = IndexSet::new();
// TODO: merge with collect_decls
for item in module_items.iter() {
item.collect_ident(&mut current_scope);
}
let module_items = module_items.fold_children_with(self);
let current_binding = self.scope_binding.len();
// TODO: false positive, doesn't consider identity shadow
let used_in_jsx = collect_ident_in_jsx(module_items);
let mut items = Vec::with_capacity(module_items.len());
let mut refresh_regs = Vec::<(Ident, Id)>::new();
if self.curr_hook_fn.len() > 0 {
items.push(ModuleItem::Stmt(self.gen_hook_handle(&self.curr_hook_fn)));
}
let curr_hook_fn = mem::take(&mut self.curr_hook_fn);
let (mut handle_map, ignore) = hook_to_handle_map(curr_hook_fn);
for mut item in module_items {
let mut hook_reg = Vec::new();
let mut hook_visitor = HookRegister {
options: &self.options,
ident: Vec::new(),
extra_stmt: Vec::new(),
scope_binding: current_scope,
cm: &self.cm,
should_reset: self.should_reset,
};
for mut item in module_items.take() {
let persistent_id = match &mut item {
// function Foo() {}
ModuleItem::Stmt(Stmt::Decl(Decl::Fn(FnDecl { ident, .. }))) => {
if let Some(hook) = handle_map.remove(&ident) {
hook_reg.push((ident.clone(), hook));
}
get_persistent_id(ident)
}
@ -788,12 +316,7 @@ impl<C: Comments> Fold for Refresh<C> {
ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
decl: Decl::Fn(FnDecl { ident, .. }),
..
})) => {
if let Some(hook) = handle_map.remove(&ident) {
hook_reg.push((ident.clone(), hook));
}
get_persistent_id(ident)
}
})) => get_persistent_id(ident),
// export default function Foo() {}
ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(ExportDefaultDecl {
@ -804,12 +327,7 @@ impl<C: Comments> Fold for Refresh<C> {
..
}),
..
})) => {
if let Some(hook) = handle_map.remove(&ident) {
hook_reg.push((ident.clone(), hook));
};
get_persistent_id(ident)
}
})) => get_persistent_id(ident),
// const Foo = () => {}
// export const Foo = () => {}
@ -818,14 +336,7 @@ impl<C: Comments> Fold for Refresh<C> {
decl: Decl::Var(var_decl),
..
})) => {
for decl in &var_decl.decls {
if let Pat::Ident(BindingIdent { id, .. }) = &decl.name {
if let Some(hook) = handle_map.remove(&id) {
hook_reg.push((id.clone(), hook));
}
}
}
self.get_persistent_id_from_var_decl(var_decl, &ignore)
self.get_persistent_id_from_var_decl(var_decl, &used_in_jsx, &mut hook_visitor)
}
// This code path handles nested cases like:
@ -837,16 +348,19 @@ impl<C: Comments> Fold for Refresh<C> {
span,
})) => {
if let Expr::Call(call) = expr.as_mut() {
if let Persist::Hoc(Hoc { reg, .. }) = self
if let Persist::Hoc(Hoc { reg, hook, .. }) = self
.get_persistent_id_from_possible_hoc(
call,
vec![(
private_ident!("_c"),
("%default%".into(), SyntaxContext::empty()),
)],
&ignore,
&mut hook_visitor,
)
{
if let Some(hook) = hook {
make_hook_reg(expr.as_mut(), hook)
}
item = ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(
ExportDefaultExpr {
expr: Box::new(make_assign_stmt(reg[0].0.clone(), expr.take())),
@ -869,12 +383,22 @@ impl<C: Comments> Fold for Refresh<C> {
_ => Persist::None,
};
items.push(item);
if let Persist::Hoc(_) = persistent_id {
// we need to make hook transform happens after component for
// HOC
items.push(item);
} else {
item.visit_mut_children_with(&mut hook_visitor);
self.scope_binding.truncate(current_binding);
for (ident, mut hook) in hook_reg {
items.push(ModuleItem::Stmt(
self.gen_hook_register_stmt(Expr::Ident(ident), &mut hook),
))
items.push(item);
items.extend(
hook_visitor
.extra_stmt
.take()
.into_iter()
.map(ModuleItem::Stmt),
);
}
match persistent_id {
@ -913,6 +437,10 @@ impl<C: Comments> Fold for Refresh<C> {
}
}
if hook_visitor.ident.len() > 0 {
items.insert(0, ModuleItem::Stmt(hook_visitor.gen_hook_handle()));
}
// Insert
// ```
// var _c, _c1;
@ -961,71 +489,23 @@ impl<C: Comments> Fold for Refresh<C> {
})));
}
items
*module_items = items
}
fn fold_ts_module_decl(&mut self, n: TsModuleDecl) -> TsModuleDecl {
n
}
fn fold_var_decl(&mut self, n: VarDecl) -> VarDecl {
let VarDecl {
span,
kind,
declare,
decls,
} = n;
// we don't want fold_expr to mess up with function name inference
// so intercept it here
let decls = decls
.into_iter()
.map(|decl| match decl {
VarDeclarator {
span,
// it doesn't quite make sense for other Pat to appear here
name: Pat::Ident(BindingIdent { id, type_ann }),
init: Some(init),
definite,
} => {
let init = match *init {
Expr::Fn(mut expr) => {
if let Some(mut hook) = expr
.function
.body
.as_mut()
.and_then(|body| self.get_hook_sign(body))
{
hook.binding = Some(id.clone());
self.curr_hook_fn.push(hook);
}
Expr::Fn(expr.fold_children_with(self))
}
Expr::Arrow(mut expr) => {
if let Some(mut hook) = self.get_hook_sign_from_arrow(&mut expr.body) {
hook.binding = Some(id.clone());
self.curr_hook_fn.push(hook);
}
Expr::Arrow(expr.fold_children_with(self))
}
_ => self.fold_expr(*init),
};
VarDeclarator {
span,
name: Pat::Ident(BindingIdent { id, type_ann }),
init: Some(Box::new(init)),
definite,
}
}
_ => decl.fold_children_with(self),
})
.collect();
VarDecl {
span,
kind,
declare,
decls,
}
}
fn visit_mut_ts_module_decl(&mut self, _: &mut TsModuleDecl) {}
}
fn make_hook_reg(expr: &mut Expr, mut hook: HocHook) {
let span = expr.span();
let mut args = vec![ExprOrSpread {
spread: None,
expr: Box::new(expr.take()),
}];
args.append(&mut hook.rest_arg);
*expr = Expr::Call(CallExpr {
span,
callee: hook.callee,
args,
type_args: None,
});
}

View File

@ -697,6 +697,34 @@ test!(
"#
);
test!(
ignore,
::swc_ecma_parser::Syntax::Es(::swc_ecma_parser::EsConfig {
jsx: true,
..Default::default()
}),
tr,
register_identifiers_used_in_jsx_false_positive,
r#"
const A = foo() {}
const B = () => {
const A = () => null
return <A />
}
"#,
r#"
const A = foo() {}
const B = () => {
const A = () => null
return <A />
}
_c = B;
var _c;
$RefreshReg$(_c, "B");
"#
);
test!(
::swc_ecma_parser::Syntax::Es(::swc_ecma_parser::EsConfig {
jsx: true,
@ -1428,3 +1456,31 @@ test!(
$RefreshReg$(_c1, 'Comp');
"
);
test!(
Default::default(),
tr,
issue_2261,
"
export function App() {
console.log(useState())
return null;
}
",
r#"
var _s = $RefreshSig$();
export function App() {
_s();
console.log(useState())
return null;
}
_s(App, "useState{}");
_c = App;
var _c;
$RefreshReg$(_c, "App")
"#
);

View File

@ -2,6 +2,7 @@ use indexmap::IndexSet;
use swc_atoms::JsWord;
use swc_common::{collections::AHashSet, Spanned, SyntaxContext, DUMMY_SP};
use swc_ecma_ast::*;
use swc_ecma_visit::{noop_visit_type, Visit, VisitWith};
pub trait CollectIdent {
fn collect_ident(&self, collection: &mut IndexSet<JsWord>);
@ -215,46 +216,48 @@ pub fn is_import_or_require(expr: &Expr) -> bool {
false
}
pub fn callee_should_ignore<'a>(
ignore: &'a AHashSet<Ident>,
callee: &ExprOrSuper,
) -> Option<ExprOrSuper> {
let expr = if let ExprOrSuper::Expr(expr) = callee {
Some(expr)
} else {
None
}?;
let ident = if let Expr::Ident(ident) = expr.as_ref() {
Some(ident)
} else {
None
}?;
ignore
.get(ident)
.map(|ident| ExprOrSuper::Expr(Box::new(Expr::Ident(ident.clone()))))
pub struct UsedInJsx(AHashSet<JsWord>);
impl Visit for UsedInJsx {
noop_visit_type!();
fn visit_call_expr(&mut self, n: &CallExpr) {
n.visit_children_with(self);
if let ExprOrSuper::Expr(expr) = &n.callee {
let ident = match expr.as_ref() {
Expr::Ident(ident) => ident,
Expr::Member(MemberExpr { prop, .. }) => {
if let Expr::Ident(ident) = prop.as_ref() {
ident
} else {
return;
}
}
_ => return,
};
if matches!(
ident.sym.as_ref(),
"createElement" | "jsx" | "jsxDEV" | "jsxs"
) {
if let Some(ExprOrSpread { expr, .. }) = n.args.get(0) {
if let Expr::Ident(ident) = expr.as_ref() {
self.0.insert(ident.sym.clone());
}
}
}
}
}
fn visit_jsx_opening_element(&mut self, n: &JSXOpeningElement) {
if let JSXElementName::Ident(ident) = &n.name {
self.0.insert(ident.sym.clone());
}
}
}
pub fn gen_custom_hook_record(elems: Vec<Option<ExprOrSpread>>) -> Expr {
Expr::Fn(FnExpr {
ident: None,
function: Function {
is_generator: false,
is_async: false,
params: Vec::new(),
decorators: Vec::new(),
span: DUMMY_SP,
body: Some(BlockStmt {
span: DUMMY_SP,
stmts: vec![Stmt::Return(ReturnStmt {
span: DUMMY_SP,
arg: Some(Box::new(Expr::Array(ArrayLit {
span: DUMMY_SP,
elems,
}))),
})],
}),
type_params: None,
return_type: None,
},
})
pub fn collect_ident_in_jsx<V: VisitWith<UsedInJsx>>(item: &V) -> AHashSet<JsWord> {
let mut visitor = UsedInJsx(AHashSet::default());
item.visit_with(&mut visitor);
visitor.0
}