diff --git a/crates/rspack_plugin_javascript/src/dependency/mod.rs b/crates/rspack_plugin_javascript/src/dependency/mod.rs index 432a5b917818..ee87cf89f251 100644 --- a/crates/rspack_plugin_javascript/src/dependency/mod.rs +++ b/crates/rspack_plugin_javascript/src/dependency/mod.rs @@ -4,6 +4,7 @@ mod esm; mod export_info_api_dep; mod hmr; mod module_argument_dependency; +mod pure_expression_dependency; mod url; mod worker; pub use commonjs::*; @@ -12,6 +13,7 @@ pub use esm::*; pub use export_info_api_dep::*; pub use hmr::*; pub use module_argument_dependency::*; +pub use pure_expression_dependency::*; pub use worker::*; pub use self::url::*; diff --git a/crates/rspack_plugin_javascript/src/dependency/pure_expression_dependency.rs b/crates/rspack_plugin_javascript/src/dependency/pure_expression_dependency.rs new file mode 100644 index 000000000000..ee408b97e3b3 --- /dev/null +++ b/crates/rspack_plugin_javascript/src/dependency/pure_expression_dependency.rs @@ -0,0 +1,23 @@ +use rspack_core::{DependencyTemplate, TemplateContext, TemplateReplaceSource}; + +#[derive(Debug, Clone)] +pub struct PureExpressionDependency { + pub start: u32, + pub end: u32, +} + +impl PureExpressionDependency { + pub fn new(start: u32, end: u32) -> Self { + Self { start, end } + } +} + +impl DependencyTemplate for PureExpressionDependency { + fn apply( + &self, + _source: &mut TemplateReplaceSource, + _code_generatable_context: &mut TemplateContext, + ) { + // TODO + } +} diff --git a/crates/rspack_plugin_javascript/src/plugin/inner_graph_plugin.rs b/crates/rspack_plugin_javascript/src/plugin/inner_graph_plugin.rs new file mode 100644 index 000000000000..6cc67752aa64 --- /dev/null +++ b/crates/rspack_plugin_javascript/src/plugin/inner_graph_plugin.rs @@ -0,0 +1,342 @@ +use rspack_core::{DependencyTemplate, SpanExt, UsedByExports}; +use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet}; +use swc_core::{ + common::{Spanned, SyntaxContext}, + ecma::{ + ast::{ + ArrowExpr, CallExpr, Callee, Class, ClassDecl, ClassExpr, ClassMember, Decl, DefaultDecl, + ExportDecl, ExportDefaultDecl, ExportDefaultExpr, Expr, FnDecl, FnExpr, Ident, Pat, + VarDeclarator, + }, + atoms::JsWord, + utils::find_pat_ids, + visit::{noop_visit_type, Visit, VisitWith}, + }, +}; + +use crate::{ + dependency::PureExpressionDependency, + side_effects_flag_plugin::{is_pure_class, is_pure_expression}, +}; + +#[derive(Default)] +pub enum InnerGraphMapValue { + Set(HashSet), + True, + #[default] + Nil, +} + +#[derive(PartialEq, Eq)] +pub enum InnerGraphMapUsage { + Value(JsWord), + True, +} + +pub type UsageCallback = Box; + +#[derive(Default)] +pub struct InnerGraphState { + inner_graph: HashMap, + usage_callback_map: HashMap>, + current_top_level_symbol: Option, +} + +pub struct InnerGraphPlugin<'a> { + presentational_dependencies: &'a mut Vec>, + unresolved_ctxt: SyntaxContext, + top_level_ctxt: SyntaxContext, + state: Option, +} + +impl<'a> Visit for InnerGraphPlugin<'a> { + noop_visit_type!(); + + fn visit_call_expr(&mut self, call_expr: &CallExpr) { + // https://github.com/webpack/webpack/blob/main/lib/JavascriptMetaInfoPlugin.js#L46 + if let Callee::Expr(box Expr::Ident(ident)) = &call_expr.callee && &ident.sym == "eval" { + self.bailout(); + } + } + + fn visit_class_member(&mut self, node: &ClassMember) {} + + fn visit_fn_decl(&mut self, node: &FnDecl) {} + + fn visit_fn_expr(&mut self, node: &FnExpr) {} + + fn visit_arrow_expr(&mut self, node: &ArrowExpr) {} + + fn visit_class_expr(&mut self, node: &ClassExpr) { + if !self.is_enabled() { + return; + } + if let Some(ident) = &node.ident { + self.visit_class(ident.sym.clone(), &node.class); + } + } + + fn visit_class_decl(&mut self, node: &ClassDecl) { + if !self.is_enabled() { + return; + } + self.visit_class(node.ident.sym.clone(), &node.class); + } + + fn visit_ident(&mut self, ident: &Ident) { + if ident.span.ctxt == self.top_level_ctxt { + self.add_usage( + ident.sym.clone(), + if let Some(symbol) = self.get_top_level_symbol() { + InnerGraphMapUsage::Value(symbol) + } else { + InnerGraphMapUsage::True + }, + ); + } + } + + fn visit_var_declarator(&mut self, n: &VarDeclarator) { + if !self.is_enabled() { + return; + } + if let Pat::Ident(ident) = &n.name && let Some(box expr) = &n.init && is_pure_expression(expr, self.unresolved_ctxt){ + let symbol = ident.id.sym.clone(); + match expr { + Expr::Fn(_) | Expr::Arrow(_) | Expr::Lit(_) => {}, + Expr::Class(class) => { + self.visit_class(symbol, &class.class); + } + _ => { + if is_pure_expression(expr, self.unresolved_ctxt) { + expr.visit_children_with(self); + self.on_usage_by_span(Some(symbol), expr.span().real_lo(), expr.span().real_hi()); + } + } + } + } + } + + fn visit_export_decl(&mut self, export_decl: &ExportDecl) { + match &export_decl.decl { + Decl::Class(ClassDecl { ident, .. }) | Decl::Fn(FnDecl { ident, .. }) => { + self.add_variable_usage(ident.sym.clone(), ident.sym.clone()); + } + Decl::Var(v) => { + find_pat_ids::<_, Ident>(&v.decls) + .into_iter() + .for_each(|ident| { + self.add_variable_usage(ident.sym.clone(), ident.sym.clone()); + }); + } + _ => {} + } + export_decl.visit_children_with(self); + } + // TODO add_variable_usage `exportExpression` + fn visit_export_default_expr(&mut self, node: &ExportDefaultExpr) { + if !self.is_enabled() { + return; + } + let symbol: JsWord = "*default*".into(); + match *node.expr { + Expr::Fn(_) | Expr::Arrow(_) | Expr::Lit(_) => {} + Expr::Class(class) => { + self.visit_class(symbol, &class.class); + } + _ => { + if is_pure_expression(&*node.expr, self.unresolved_ctxt) { + node.expr.visit_children_with(self); + self.on_usage_by_span(Some(symbol), node.span.real_lo(), node.span.real_hi()); + } + } + } + } + + fn visit_export_default_decl(&mut self, node: &ExportDefaultDecl) { + if !self.is_enabled() { + return; + } + let symbol: JsWord = "*default*".into(); + match &node.decl { + DefaultDecl::Class(class) => { + self.visit_class(symbol, &class.class); + } + // DefaultDecl::Fn(_) => todo!(), + _ => {} + } + } +} + +impl<'a> InnerGraphPlugin<'a> { + pub fn new( + presentational_dependencies: &'a mut Vec>, + unresolved_ctxt: SyntaxContext, + top_level_ctxt: SyntaxContext, + ) -> Self { + Self { + presentational_dependencies, + unresolved_ctxt, + top_level_ctxt, + state: Some(InnerGraphState::default()), + } + } + + pub fn bailout(&mut self) { + self.state = None; + } + + pub fn is_enabled(&self) -> bool { + self.state.is_some() + } + + pub fn add_usage(&mut self, symbol: JsWord, usage: InnerGraphMapUsage) { + if let Some(state) = self.state.as_mut() { + let info = state.inner_graph.get_mut(&symbol); + match usage { + InnerGraphMapUsage::True => { + state.inner_graph.insert(symbol, InnerGraphMapValue::True); + } + InnerGraphMapUsage::Value(value) => { + if let Some(info) = info { + if let InnerGraphMapValue::Set(set) = info { + set.insert(value); + } + } else { + state.inner_graph.insert( + symbol, + InnerGraphMapValue::Set(HashSet::from_iter(vec![value])), + ); + } + } + } + } + } + + pub fn add_variable_usage(&mut self, name: JsWord, usage: JsWord) { + self.add_usage(name, InnerGraphMapUsage::Value(usage)); + } + + pub fn on_usage(&mut self, symbol: Option, on_usage_callback: UsageCallback) { + if let Some(state) = self.state.as_mut() { + if let Some(symbol) = symbol { + state + .usage_callback_map + .entry(symbol) + .or_insert(vec![]) + .push(on_usage_callback); + } else { + on_usage_callback(UsedByExports::Bool(true)); + } + } else { + on_usage_callback(UsedByExports::Nil); + } + } + + pub fn on_usage_by_span(&mut self, symbol: Option, start: u32, end: u32) { + self.on_usage( + symbol, + Box::new(|used_by_exports| { + if matches!(used_by_exports, UsedByExports::Nil) + || matches!(used_by_exports, UsedByExports::Bool(true)) + { + return; + } else { + // TODO usedByExports + self + .presentational_dependencies + .push(Box::new(PureExpressionDependency::new(start, end))); + } + }), + ) + } + + pub fn visit_class(&mut self, symbol: JsWord, class: &Class) { + self.set_top_level_symbol(Some(symbol.clone())); + for stmt in class.body.iter() { + match stmt { + ClassMember::ClassProp(p) => { + if let Some(box expr) = &p.value && is_pure_expression(expr, self.unresolved_ctxt) && p.is_static { + expr.visit_children_with(self); + self.on_usage_by_span(Some(symbol.clone()), expr.span().real_lo(), expr.span().real_hi()); + } + } + ClassMember::PrivateProp(p) => { + if let Some(box expr) = &p.value && is_pure_expression(expr, self.unresolved_ctxt) && p.is_static { + expr.visit_children_with(self); + self.on_usage_by_span(Some(symbol.clone()), expr.span().real_lo(), expr.span().real_hi()); + } + } + _ => {} + } + } + // `onUsageSuper` + if let Some(box Expr::Ident(ident)) = &class.super_class && is_pure_class(class, self.unresolved_ctxt) { + ident.visit_children_with(self); + self.on_usage_by_span(Some(symbol), class.span.real_lo(), class.span.real_hi()); + } + self.set_top_level_symbol(None); + } + + pub fn set_top_level_symbol(&mut self, symbol: Option) { + if let Some(state) = self.state.as_mut() { + state.current_top_level_symbol = symbol; + } + } + + pub fn get_top_level_symbol(&self) -> Option { + self.state.and_then(|s| s.current_top_level_symbol) + } + + pub fn infer_dependency_usage(&mut self) { + if let Some(state) = self.state.as_mut() { + let non_terminal = HashSet::from_iter(state.inner_graph.keys()); + let mut processed: HashMap> = HashMap::default(); + while !non_terminal.is_empty() { + for key in non_terminal { + let mut new_set = HashSet::default(); + let mut entry = processed.entry(key.clone()).or_default(); + if let Some(InnerGraphMapValue::Set(value)) = state.inner_graph.get(key) { + for item in value.iter() { + entry.insert(item.clone()); + } + for item in value { + if let Some(item_value) = state.inner_graph.get(item) { + match item_value { + InnerGraphMapValue::Set(set) => { + let mut new_set = HashSet::default(); + for i in set { + if i == key { + continue; + } + if entry.contains(i) { + continue; + } + new_set.insert(i); + } + if new_set.is_empty() { + state + .inner_graph + .insert(key.clone(), InnerGraphMapValue::Nil); + } else { + state + .inner_graph + .insert(key.clone(), InnerGraphMapValue::Set(new_set)); + } + } + InnerGraphMapValue::True => { + state + .inner_graph + .insert(key.clone(), InnerGraphMapValue::True); + } + InnerGraphMapValue::Nil => {} + } + } + } + } + // processed.entry(key).and_modify(f). + } + } + } + } +} diff --git a/crates/rspack_plugin_javascript/src/plugin/mod.rs b/crates/rspack_plugin_javascript/src/plugin/mod.rs index 2085384fea60..0a646f235dbc 100644 --- a/crates/rspack_plugin_javascript/src/plugin/mod.rs +++ b/crates/rspack_plugin_javascript/src/plugin/mod.rs @@ -1,9 +1,9 @@ pub mod flag_usage_plugin; pub mod impl_plugin_for_js_plugin; pub mod infer_async_modules_plugin; +pub mod inner_graph_plugin; pub mod provided_exports_plugin; pub mod side_effects_flag_plugin; - use std::hash::Hash; use rspack_core::rspack_sources::{BoxSource, ConcatSource, RawSource, SourceExt}; diff --git a/crates/rspack_plugin_javascript/src/plugin/side_effects_flag_plugin.rs b/crates/rspack_plugin_javascript/src/plugin/side_effects_flag_plugin.rs index a14f0e992106..8b74ac4184cf 100644 --- a/crates/rspack_plugin_javascript/src/plugin/side_effects_flag_plugin.rs +++ b/crates/rspack_plugin_javascript/src/plugin/side_effects_flag_plugin.rs @@ -172,14 +172,14 @@ impl SideEffectsFlagPlugin { } } -fn is_pure_expression(expr: &Expr, unresolved_ctxt: SyntaxContext) -> bool { +pub fn is_pure_expression(expr: &Expr, unresolved_ctxt: SyntaxContext) -> bool { !expr.may_have_side_effects(&ExprCtx { unresolved_ctxt, is_unresolved_ref_safe: false, }) } -fn is_pure_decl(stmt: &Decl, unresolved_ctxt: SyntaxContext) -> bool { +pub fn is_pure_decl(stmt: &Decl, unresolved_ctxt: SyntaxContext) -> bool { match stmt { Decl::Class(class) => is_pure_class(&class.class, unresolved_ctxt), Decl::Fn(_) => true, @@ -193,7 +193,7 @@ fn is_pure_decl(stmt: &Decl, unresolved_ctxt: SyntaxContext) -> bool { } } -fn is_pure_class(class: &Class, unresolved_ctxt: SyntaxContext) -> bool { +pub fn is_pure_class(class: &Class, unresolved_ctxt: SyntaxContext) -> bool { if let Some(ref super_class) = class.super_class { if !is_pure_expression(super_class, unresolved_ctxt) { return false;