1use std::{borrow::Cow, fmt::Debug};
2
3use rayon::prelude::*;
4use rspack_collections::{IdentifierMap, IdentifierSet};
5use rspack_core::{
6 AsyncModulesArtifact, BoxModule, Compilation, CompilationFinishModules,
7 CompilationOptimizeDependencies, ConnectionState, DependencyExtraMeta, DependencyId,
8 ExportsInfoArtifact, FactoryMeta, GetTargetResult, Logger, ModuleFactoryCreateData, ModuleGraph,
9 ModuleGraphConnection, ModuleIdentifier, NormalModuleCreateData, NormalModuleFactoryModule,
10 OptimizationBailoutItem, Plugin, ResolvedExportInfoTarget, SideEffectsDoOptimize,
11 SideEffectsDoOptimizeMoveTarget, SideEffectsOptimizeArtifact, SideEffectsState,
12 SideEffectsStateArtifact,
13 build_module_graph::BuildModuleGraphArtifact,
14 can_move_target, get_target,
15 incremental::{self, IncrementalPasses, Mutation},
16};
17use rspack_error::{Diagnostic, Result};
18use rspack_hook::{plugin, plugin_hook};
19use rspack_paths::{AssertUtf8, Utf8Path};
20use sugar_path::SugarPath;
21use swc_experimental_ecma_ast::{ClassMember, Key, PropName};
22
23use crate::{
24 FLAG_DEPENDENCY_EXPORTS_STAGE, deferred_pure_check_is_impure,
25 dependency::{ESMExportImportedSpecifierDependency, ESMImportSpecifierDependency},
26};
27
28pub static SIDE_EFFECTS_FLAG_PLUGIN_STAGE: i32 = FLAG_DEPENDENCY_EXPORTS_STAGE + 10;
29const CALL_WITH_SIDE_EFFECTS_BAILOUT: &str = "Call with side effects";
30
31#[derive(Clone, Debug)]
32enum SideEffects {
33 Bool(bool),
34 String(String),
35 Array(Vec<String>),
36}
37
38impl SideEffects {
39 pub fn from_description(description: &serde_json::Value) -> Option<Self> {
40 description.get("sideEffects").and_then(|value| {
41 if let Some(b) = value.as_bool() {
42 Some(SideEffects::Bool(b))
43 } else if let Some(s) = value.as_str() {
44 Some(SideEffects::String(s.to_owned()))
45 } else if let Some(vec) = value.as_array() {
46 let mut side_effects = vec![];
47 for value in vec {
48 if let Some(str) = value.as_str() {
49 side_effects.push(str.to_string());
50 } else {
51 return None;
52 }
53 }
54 Some(SideEffects::Array(side_effects))
55 } else {
56 None
57 }
58 })
59 }
60}
61
62fn get_side_effects_from_package_json(side_effects: SideEffects, relative_path: &Utf8Path) -> bool {
63 match side_effects {
64 SideEffects::Bool(s) => s,
65 SideEffects::String(s) => glob_match_with_normalized_pattern(&s, relative_path.as_str()),
66 SideEffects::Array(patterns) => patterns
67 .iter()
68 .any(|pattern| glob_match_with_normalized_pattern(pattern, relative_path.as_str())),
69 }
70}
71
72fn glob_match_with_normalized_pattern(pattern: &str, string: &str) -> bool {
73 let trim_start = pattern.trim_start_matches("./");
74 let normalized_glob = if trim_start.contains('/') {
75 trim_start.to_string()
76 } else {
77 String::from("**/") + trim_start
78 };
79 fast_glob::glob_match(&normalized_glob, string.trim_start_matches("./"))
80}
81
82pub trait ClassExt<'a> {
83 fn class_key(&'a self) -> Option<&'a PropName<'a>>;
84 fn is_static(&self) -> bool;
85}
86
87impl<'a> ClassExt<'a> for ClassMember<'a> {
88 fn class_key(&'a self) -> Option<&'a PropName<'a>> {
89 match self {
90 ClassMember::Constructor(c) => Some(&c.key),
91 ClassMember::Method(m) => Some(&m.key),
92 ClassMember::PrivateMethod(_) => None,
93 ClassMember::ClassProp(c) => Some(&c.key),
94 ClassMember::PrivateProp(_) => None,
95 ClassMember::Empty(_) => None,
96 ClassMember::StaticBlock(_) => None,
97 ClassMember::AutoAccessor(a) => match &a.key {
98 Key::Private(_) => None,
99 Key::Public(public) => Some(public),
100 },
101 }
102 }
103
104 fn is_static(&self) -> bool {
105 match self {
106 ClassMember::Constructor(_) => false,
107 ClassMember::Method(m) => m.is_static,
108 ClassMember::PrivateMethod(m) => m.is_static,
109 ClassMember::ClassProp(p) => p.is_static,
110 ClassMember::PrivateProp(p) => p.is_static,
111 ClassMember::Empty(_) => false,
112 ClassMember::StaticBlock(_) => true,
113 ClassMember::AutoAccessor(a) => a.is_static,
114 }
115 }
116}
117
118#[plugin]
119#[derive(Debug, Default)]
120pub struct SideEffectsFlagPlugin {
121 analyze_side_effects_free: bool,
122}
123
124impl SideEffectsFlagPlugin {
125 pub fn new(analyze_side_effects_free: bool) -> Self {
126 Self::new_inner(analyze_side_effects_free)
127 }
128}
129
130#[plugin_hook(NormalModuleFactoryModule for SideEffectsFlagPlugin,tracing=false)]
131async fn nmf_module(
132 &self,
133 _data: &mut ModuleFactoryCreateData,
134 create_data: &NormalModuleCreateData,
135 module: &mut BoxModule,
136) -> Result<()> {
137 if let Some(has_side_effects) = create_data.side_effects {
138 module.set_factory_meta(FactoryMeta {
139 side_effect_free: Some(!has_side_effects),
140 });
141 return Ok(());
142 }
143
144 let resource_data = create_data.resource_resolve_data.as_ref();
145 let Some(resource_path) = resource_data.path() else {
146 return Ok(());
147 };
148 let Some(description) = resource_data.description() else {
149 return Ok(());
150 };
151 let package_path = description.path();
152 let Some(side_effects) = SideEffects::from_description(description.json()) else {
153 return Ok(());
154 };
155 let relative_path = resource_path
156 .as_std_path()
157 .relative(package_path)
158 .assert_utf8();
159 let has_side_effects = get_side_effects_from_package_json(side_effects, relative_path.as_path());
160
161 module.set_factory_meta(FactoryMeta {
162 side_effect_free: Some(!has_side_effects),
163 });
164 Ok(())
165}
166
167#[plugin_hook(CompilationFinishModules for SideEffectsFlagPlugin, stage = SIDE_EFFECTS_FLAG_PLUGIN_STAGE)]
169async fn finish_modules(
170 &self,
171 compilation: &Compilation,
172 _modules: &mut AsyncModulesArtifact,
173 exports_info_artifact: &mut ExportsInfoArtifact,
174 side_effects_state_artifact: &mut SideEffectsStateArtifact,
175) -> Result<()> {
176 let modules: IdentifierSet = compilation
177 .get_module_graph()
178 .modules()
179 .map(|(id, _)| *id)
180 .collect();
181
182 let module_graph = compilation.get_module_graph();
183 let mut deferred_side_effect_states = Vec::new();
184 for module_identifier in &modules {
185 let Some(module) = module_graph.module_by_identifier(module_identifier) else {
186 continue;
187 };
188
189 let build_info = module.build_info();
190 if !self.analyze_side_effects_free || build_info.deferred_pure_checks.is_empty() {
191 continue;
192 }
193
194 let baseline_side_effect_free = module.build_meta().side_effect_free();
195
196 let has_impure_deferred_check =
197 build_info
199 .deferred_pure_checks
200 .iter()
201 .any(|deferred_check| {
202 deferred_pure_check_is_impure(
203 module_graph,
204 exports_info_artifact,
205 &deferred_check.dep_id,
206 &deferred_check.atom,
207 )
208 });
209
210 deferred_side_effect_states.push((
211 *module_identifier,
212 baseline_side_effect_free && !has_impure_deferred_check,
213 has_impure_deferred_check,
214 ));
215 }
216
217 side_effects_state_artifact.clear();
218 for (module_id, side_effect_free, has_impure_deferred_check) in deferred_side_effect_states {
219 side_effects_state_artifact.insert(
220 module_id,
221 SideEffectsState {
222 side_effect_free,
223 optimization_bailouts_to_add: if has_impure_deferred_check {
224 vec![OptimizationBailoutItem::Message(String::from(
225 CALL_WITH_SIDE_EFFECTS_BAILOUT,
226 ))]
227 } else {
228 Vec::new()
229 },
230 optimization_bailouts_to_remove: vec![OptimizationBailoutItem::Message(String::from(
231 CALL_WITH_SIDE_EFFECTS_BAILOUT,
232 ))],
233 },
234 );
235 }
236
237 Ok(())
238}
239
240#[plugin_hook(CompilationOptimizeDependencies for SideEffectsFlagPlugin,tracing=false)]
241async fn optimize_dependencies(
242 &self,
243 compilation: &Compilation,
244 side_effects_optimize_artifact: &mut SideEffectsOptimizeArtifact,
245 build_module_graph_artifact: &mut BuildModuleGraphArtifact,
246 exports_info_artifact: &mut ExportsInfoArtifact,
247 _diagnostics: &mut Vec<Diagnostic>,
248) -> Result<Option<bool>> {
249 let logger = compilation.get_logger("rspack.SideEffectsFlagPlugin");
250 let start = logger.time("update connections");
251
252 let side_effects_state_map: IdentifierMap<ConnectionState> = {
253 let module_graph = &build_module_graph_artifact.module_graph;
254 let side_effects_state_artifact = &build_module_graph_artifact.side_effects_state_artifact;
255
256 module_graph
257 .modules_par()
258 .map(|(module_identifier, module)| {
259 (
260 *module_identifier,
261 module.get_side_effects_connection_state(
262 module_graph,
263 &compilation.module_graph_cache_artifact,
264 side_effects_state_artifact,
265 &mut Default::default(),
266 &mut Default::default(),
267 ),
268 )
269 })
270 .collect()
271 };
272 let module_graph = build_module_graph_artifact.get_module_graph();
273
274 if self.analyze_side_effects_free {
275 side_effects_optimize_artifact.clear();
279 }
280
281 let inner_start = logger.time("prepare connections");
282 let modules: Vec<_> = if !self.analyze_side_effects_free
283 && let Some(mutations) = compilation
284 .incremental
285 .mutations_read(IncrementalPasses::OPTIMIZE_DEPENDENCIES)
286 && !side_effects_optimize_artifact.is_empty()
287 {
288 side_effects_optimize_artifact.retain(|dependency_id, do_optimize| {
289 let dep_exist = module_graph
290 .connection_by_dependency_id(dependency_id)
291 .is_some();
292 let target_module_exist = module_graph
293 .module_by_identifier(&do_optimize.target_module)
294 .is_some();
295 dep_exist && target_module_exist
296 });
297
298 fn affected_incoming_modules(
299 module: &ModuleIdentifier,
300 module_graph: &ModuleGraph,
301 modules: &mut IdentifierSet,
302 ) {
303 for connection in module_graph.get_incoming_connections(module) {
304 let Some(original_module) = connection.original_module_identifier else {
305 continue;
306 };
307 if modules.contains(&original_module) {
308 continue;
309 }
310 let dep = module_graph.dependency_by_id(&connection.dependency_id);
311 if dep.is::<ESMExportImportedSpecifierDependency>() && modules.insert(original_module) {
312 affected_incoming_modules(&original_module, module_graph, modules);
313 }
314 }
315 }
316
317 let modules: IdentifierSet = mutations.iter().fold(
318 IdentifierSet::default(),
319 |mut modules, mutation| match mutation {
320 Mutation::ModuleAdd { module } | Mutation::ModuleUpdate { module } => {
321 if modules.insert(*module) {
322 affected_incoming_modules(module, module_graph, &mut modules);
323 }
324 modules.extend(
325 module_graph
326 .get_outgoing_connections(module)
327 .map(|connection| *connection.module_identifier()),
328 );
329 modules
330 }
331 _ => modules,
332 },
333 );
334
335 tracing::debug!(target: incremental::TRACING_TARGET, passes = %IncrementalPasses::OPTIMIZE_DEPENDENCIES, %mutations, ?modules);
336 let logger = compilation.get_logger("rspack.incremental.optimizeDependencies");
337 logger.log(format!(
338 "{} modules are affected, {} in total",
339 modules.len(),
340 module_graph.modules_len()
341 ));
342
343 let mut modules = modules.into_iter().collect::<Vec<_>>();
344 modules.sort_unstable();
345 modules
346 } else {
347 let mut modules = module_graph.modules_keys().copied().collect::<Vec<_>>();
348 modules.sort_unstable();
349 modules
350 };
351 logger.time_end(inner_start);
352
353 let inner_start = logger.time("find optimizable connections");
354 let mut optimized_connections = modules
355 .par_iter()
356 .filter(|module| side_effects_state_map[module] == ConnectionState::Active(false))
357 .flat_map(|module| {
358 module_graph
359 .get_incoming_connections(module)
360 .collect::<Vec<_>>()
361 })
362 .map(|connection| {
363 (
364 connection.dependency_id,
365 can_optimize_connection(
366 connection,
367 &side_effects_state_map,
368 module_graph,
369 exports_info_artifact,
370 ),
371 )
372 })
373 .collect::<Vec<_>>();
374 optimized_connections.sort_unstable_by_key(|(dep_id, _)| *dep_id);
375 for (dep_id, can_optimize) in optimized_connections {
376 if let Some(do_optimize) = can_optimize {
377 side_effects_optimize_artifact.insert(dep_id, do_optimize);
378 } else {
379 side_effects_optimize_artifact.remove(&dep_id);
380 }
381 }
382 logger.time_end(inner_start);
383
384 let mut do_optimizes = side_effects_optimize_artifact
385 .iter()
386 .map(|(dependency, do_optimize)| (*dependency, do_optimize.clone()))
387 .collect::<Vec<_>>();
388 do_optimizes.sort_unstable_by_key(|(dependency, _)| *dependency);
389
390 let inner_start = logger.time("do optimize connections");
391 let mut do_optimized_count = 0;
392 while !do_optimizes.is_empty() {
393 do_optimized_count += do_optimizes.len();
394
395 let module_graph = build_module_graph_artifact.get_module_graph_mut();
396
397 let new_connections: Vec<_> = do_optimizes
398 .into_iter()
399 .map(|(dependency, do_optimize)| {
400 do_optimize_connection(dependency, do_optimize, module_graph, exports_info_artifact)
401 })
402 .collect();
403
404 let module_graph = build_module_graph_artifact.get_module_graph();
405 do_optimizes = new_connections
406 .into_par_iter()
407 .filter(|(_, module)| side_effects_state_map[module] == ConnectionState::Active(false))
408 .filter_map(|(connection, _)| module_graph.connection_by_dependency_id(&connection))
409 .filter_map(|connection| {
410 can_optimize_connection(
411 connection,
412 &side_effects_state_map,
413 module_graph,
414 exports_info_artifact,
415 )
416 .map(|i| (connection.dependency_id, i))
417 })
418 .collect();
419 do_optimizes.sort_unstable_by_key(|(dependency, _)| *dependency);
420 }
421 logger.time_end(inner_start);
422
423 build_module_graph_artifact
424 .side_effects_state_artifact
425 .set_module_evaluation_side_effects_states(side_effects_state_map);
426
427 logger.time_end(start);
428 logger.log(format!("optimized {do_optimized_count} connections"));
429 Ok(None)
430}
431
432#[tracing::instrument(skip_all)]
433fn do_optimize_connection(
434 dependency: DependencyId,
435 do_optimize: SideEffectsDoOptimize,
436 module_graph: &mut ModuleGraph,
437 exports_info_artifact: &mut ExportsInfoArtifact,
438) -> (DependencyId, ModuleIdentifier) {
439 let SideEffectsDoOptimize {
440 ids,
441 target_module,
442 need_move_target,
443 } = do_optimize;
444 module_graph.do_update_module(&dependency, &target_module);
445 module_graph.set_dependency_extra_meta(
446 dependency,
447 DependencyExtraMeta {
448 ids,
449 explanation: Some("(skipped side-effect-free modules)"),
450 },
451 );
452 if let Some(SideEffectsDoOptimizeMoveTarget {
453 export_info,
454 target_export,
455 }) = need_move_target
456 {
457 export_info
458 .as_data_mut(exports_info_artifact)
459 .do_move_target(dependency, target_export);
460 }
461 (dependency, target_module)
462}
463
464#[tracing::instrument("can_optimize_connection", level = "trace", skip_all)]
465fn can_optimize_connection(
466 connection: &ModuleGraphConnection,
467 side_effects_state_map: &IdentifierMap<ConnectionState>,
468 module_graph: &ModuleGraph,
469 exports_info_artifact: &ExportsInfoArtifact,
470) -> Option<SideEffectsDoOptimize> {
471 let original_module = connection.original_module_identifier?;
472 let dependency_id = connection.dependency_id;
473 let dep = module_graph.dependency_by_id(&dependency_id);
474
475 if let Some(dep) = dep.downcast_ref::<ESMExportImportedSpecifierDependency>()
476 && let Some(name) = &dep.name
477 {
478 let exports_info = exports_info_artifact.get_exports_info_data(&original_module);
479 let export_info = exports_info.get_export_info_without_mut_module_graph(name);
480
481 let resolve_filter = |target: &ResolvedExportInfoTarget| {
482 side_effects_state_map[&target.module] == ConnectionState::Active(false)
483 };
484 let target = can_move_target(
485 &export_info,
486 module_graph,
487 exports_info_artifact,
488 &resolve_filter,
489 )?;
490 if !module_graph.can_update_module(&dependency_id, &target.module) {
491 return None;
492 }
493
494 let ids = dep.get_ids(module_graph);
495 let processed_ids = target.export.as_ref().map_or_else(
496 || ids.get(1..).unwrap_or_default().to_vec(),
497 |item| {
498 let mut ret = item.clone();
499 ret.extend_from_slice(ids.get(1..).unwrap_or_default());
500 ret
501 },
502 );
503 let need_move_target = match export_info {
504 Cow::Borrowed(export_info) => Some(SideEffectsDoOptimizeMoveTarget {
505 export_info: export_info.id(),
506 target_export: target.export,
507 }),
508 Cow::Owned { .. } => None,
509 };
510
511 return Some(SideEffectsDoOptimize {
512 ids: processed_ids,
513 target_module: target.module,
514 need_move_target,
515 });
516 }
517
518 if let Some(dep) = dep.downcast_ref::<ESMImportSpecifierDependency>()
519 && !dep.namespace_object_as_context
520 && let ids = dep.get_ids(module_graph)
521 && !ids.is_empty()
522 {
523 let exports_info = exports_info_artifact.get_exports_info_data(connection.module_identifier());
524 let export_info = exports_info.get_export_info_without_mut_module_graph(&ids[0]);
525
526 let resolve_filter = |target: &ResolvedExportInfoTarget| {
527 side_effects_state_map[&target.module] == ConnectionState::Active(false)
528 };
529 let Some(GetTargetResult::Target(target)) = get_target(
530 &export_info,
531 module_graph,
532 exports_info_artifact,
533 &resolve_filter,
534 &mut Default::default(),
535 ) else {
536 return None;
537 };
538
539 if !module_graph.can_update_module(&dependency_id, &target.module) {
540 return None;
541 }
542
543 let processed_ids = target.export.map_or_else(
544 || ids[1..].to_vec(),
545 |mut item| {
546 item.extend_from_slice(&ids[1..]);
547 item
548 },
549 );
550
551 return Some(SideEffectsDoOptimize {
552 ids: processed_ids,
553 target_module: target.module,
554 need_move_target: None,
555 });
556 }
557
558 None
559}
560
561impl Plugin for SideEffectsFlagPlugin {
562 fn name(&self) -> &'static str {
563 "SideEffectsFlagPlugin"
564 }
565
566 fn apply(&self, ctx: &mut rspack_core::ApplyContext<'_>) -> Result<()> {
567 ctx
568 .normal_module_factory_hooks
569 .module
570 .tap(nmf_module::new(self));
571
572 ctx
573 .compilation_hooks
574 .finish_modules
575 .tap(finish_modules::new(self));
576
577 ctx
578 .compilation_hooks
579 .optimize_dependencies
580 .tap(optimize_dependencies::new(self));
581 Ok(())
582 }
583}
584
585#[cfg(test)]
586mod test_side_effects {
587 use super::*;
588
589 fn get_side_effects_from_package_json_helper(
590 side_effects_config: Vec<&str>,
591 relative_path: &str,
592 ) -> bool {
593 assert!(!side_effects_config.is_empty());
594 let relative_path = Utf8Path::new(relative_path);
595 let side_effects = if side_effects_config.len() > 1 {
596 SideEffects::Array(
597 side_effects_config
598 .into_iter()
599 .map(String::from)
600 .collect::<Vec<_>>(),
601 )
602 } else {
603 SideEffects::String((&side_effects_config[0]).to_string())
604 };
605
606 get_side_effects_from_package_json(side_effects, relative_path)
607 }
608
609 #[test]
610 fn cases() {
611 assert!(get_side_effects_from_package_json_helper(
612 vec!["./src/**/*.js"],
613 "./src/x/y/z.js"
614 ));
615 assert!(get_side_effects_from_package_json_helper(
616 vec!["./src/index.js", "./src/selection/index.js"],
617 "./src/selection/index.js"
618 ));
619 assert!(!get_side_effects_from_package_json_helper(
620 vec!["./src/**/*.js"],
621 "./x.js"
622 ));
623 assert!(get_side_effects_from_package_json_helper(
624 vec!["./**/src/x/y/z.js"],
625 "./src/x/y/z.js"
626 ));
627 assert!(get_side_effects_from_package_json_helper(
630 vec!["./src/**/z.js"],
631 "./src/x/y/z.js"
632 ));
633 assert!(get_side_effects_from_package_json_helper(
636 vec!["./**/x/**/z.js"],
637 "./src/x/y/z.js"
638 ));
639 assert!(get_side_effects_from_package_json_helper(
642 vec!["./**/src/**"],
643 "./src/x/y/z.js"
644 ));
645 assert!(!get_side_effects_from_package_json_helper(
648 vec!["./src/x/y/z.js"],
649 "./**/src/*"
650 ));
651 assert!(get_side_effects_from_package_json_helper(
654 vec!["*.js"],
655 "./src/x/y/z.js"
656 ));
657 assert!(!get_side_effects_from_package_json_helper(
660 vec!["./src/x/y/z.js"],
661 "x/**/z.js"
662 ));
663 assert!(get_side_effects_from_package_json_helper(
666 vec!["./src/**/z.js"],
667 "./src/x/y/z.js"
668 ));
669 assert!(get_side_effects_from_package_json_helper(
672 vec!["src/**/{x,y,z}.js"],
673 "./src/x/y/z.js"
674 ));
675 assert!(get_side_effects_from_package_json_helper(
678 vec!["./src/**/[x-z].js"],
679 "./src/x/y/z.js"
680 ));
681 assert!(get_side_effects_from_package_json_helper(
683 vec!["./src/**/*.js", "./dirty.js"],
684 "./src/x/y/z.js"
685 ));
686 assert!(get_side_effects_from_package_json_helper(
687 vec!["./src/**/*.js", "./dirty.js"],
688 "./dirty.js"
689 ));
690 assert!(!get_side_effects_from_package_json_helper(
691 vec!["./src/**/*.js", "./dirty.js"],
692 "./clean.js"
693 ));
694 assert!(get_side_effects_from_package_json_helper(
695 vec!["./src/**/*/z.js"],
696 "./src/x/y/z.js"
697 ));
698 }
699}