1use std::sync::LazyLock;
2
3use rspack_core::SideEffectsBailoutItemWithSpan;
4use swc_core::{
5 common::{
6 Mark, Spanned, SyntaxContext,
7 comments::{CommentKind, Comments},
8 },
9 ecma::{
10 ast::{
11 Class, ClassMember, Decl, Expr, Function, ModuleDecl, Pat, PropName, VarDecl, VarDeclOrExpr,
12 },
13 utils::{ExprCtx, ExprExt},
14 },
15};
16
17use crate::{
18 ClassExt, JavascriptParserPlugin,
19 visitors::{JavascriptParser, Statement, VariableDeclaration},
20};
21
22static PURE_COMMENTS: LazyLock<regex::Regex> =
23 LazyLock::new(|| regex::Regex::new("^\\s*(#|@)__PURE__\\s*$").expect("Should create the regex"));
24
25pub struct SideEffectsParserPlugin {
26 unresolve_ctxt: SyntaxContext,
27}
28
29impl SideEffectsParserPlugin {
30 pub fn new(unresolved_mark: Mark) -> Self {
31 Self {
32 unresolve_ctxt: SyntaxContext::empty().apply_mark(unresolved_mark),
33 }
34 }
35}
36
37impl JavascriptParserPlugin for SideEffectsParserPlugin {
38 fn module_declaration(&self, parser: &mut JavascriptParser, decl: &ModuleDecl) -> Option<bool> {
39 match decl {
40 ModuleDecl::ExportDefaultExpr(expr) => {
41 if !is_pure_expression(parser, &expr.expr, self.unresolve_ctxt, parser.comments) {
42 parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
43 expr.span,
44 String::from("ExportDefaultExpr"),
45 ));
46 }
47 }
48 ModuleDecl::ExportDecl(decl) => {
49 if !is_pure_decl(parser, &decl.decl, self.unresolve_ctxt, parser.comments) {
50 parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
51 decl.decl.span(),
52 String::from("Decl"),
53 ));
54 }
55 }
56 _ => {}
57 };
58 None
59 }
60 fn statement(&self, parser: &mut JavascriptParser, stmt: Statement) -> Option<bool> {
61 if !parser.is_top_level_scope() {
62 return None;
63 }
64 self.analyze_stmt_side_effects(&stmt, parser);
65 None
66 }
67}
68
69fn is_pure_call_expr(
70 parser: &mut JavascriptParser,
71 expr: &Expr,
72 unresolved_ctxt: SyntaxContext,
73 comments: Option<&dyn Comments>,
74) -> bool {
75 let Expr::Call(call_expr) = expr else {
76 unreachable!();
77 };
78 let callee = &call_expr.callee;
79 let pure_flag = comments
80 .and_then(|comments| {
81 if let Some(comment_list) = comments.get_leading(callee.span().lo) {
82 return Some(comment_list.iter().any(|comment| {
83 comment.kind == CommentKind::Block && PURE_COMMENTS.is_match(&comment.text)
84 }));
85 }
86 None
87 })
88 .unwrap_or(false);
89 if !pure_flag {
90 !expr.may_have_side_effects(ExprCtx {
91 unresolved_ctxt,
92 in_strict: false,
93 is_unresolved_ref_safe: false,
94 remaining_depth: 4,
95 })
96 } else {
97 call_expr.args.iter().all(|arg| {
98 if arg.spread.is_some() {
99 false
100 } else {
101 is_pure_expression(parser, &arg.expr, unresolved_ctxt, comments)
102 }
103 })
104 }
105}
106
107impl SideEffectsParserPlugin {
108 fn analyze_stmt_side_effects(&self, stmt: &Statement, parser: &mut JavascriptParser) {
109 if parser.side_effects_item.is_some() {
110 return;
111 }
112 match stmt {
113 Statement::If(if_stmt) => {
114 if !is_pure_expression(parser, &if_stmt.test, self.unresolve_ctxt, parser.comments) {
115 parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
116 if_stmt.span(),
117 String::from("Statement"),
118 ));
119 }
120 }
121 Statement::While(while_stmt) => {
122 if !is_pure_expression(
123 parser,
124 &while_stmt.test,
125 self.unresolve_ctxt,
126 parser.comments,
127 ) {
128 parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
129 while_stmt.span(),
130 String::from("Statement"),
131 ));
132 }
133 }
134 Statement::DoWhile(do_while_stmt) => {
135 if !is_pure_expression(
136 parser,
137 &do_while_stmt.test,
138 self.unresolve_ctxt,
139 parser.comments,
140 ) {
141 parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
142 do_while_stmt.span(),
143 String::from("Statement"),
144 ));
145 }
146 }
147 Statement::For(for_stmt) => {
148 let pure_init = match for_stmt.init {
149 Some(ref init) => match init {
150 VarDeclOrExpr::VarDecl(decl) => {
151 is_pure_var_decl(parser, decl, self.unresolve_ctxt, parser.comments)
152 }
153 VarDeclOrExpr::Expr(expr) => {
154 is_pure_expression(parser, expr, self.unresolve_ctxt, parser.comments)
155 }
156 },
157 None => true,
158 };
159
160 if !pure_init {
161 parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
162 for_stmt.span(),
163 String::from("Statement"),
164 ));
165 return;
166 }
167
168 let pure_test = match &for_stmt.test {
169 Some(test) => is_pure_expression(parser, test, self.unresolve_ctxt, parser.comments),
170 None => true,
171 };
172
173 if !pure_test {
174 parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
175 for_stmt.span(),
176 String::from("Statement"),
177 ));
178 return;
179 }
180
181 let pure_update = match for_stmt.update {
182 Some(ref expr) => is_pure_expression(parser, expr, self.unresolve_ctxt, parser.comments),
183 None => true,
184 };
185
186 if !pure_update {
187 parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
188 for_stmt.span(),
189 String::from("Statement"),
190 ));
191 }
192 }
193 Statement::Expr(expr_stmt) => {
194 if !is_pure_expression(
195 parser,
196 &expr_stmt.expr,
197 self.unresolve_ctxt,
198 parser.comments,
199 ) {
200 parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
201 expr_stmt.span(),
202 String::from("Statement"),
203 ));
204 }
205 }
206 Statement::Switch(switch_stmt) => {
207 if !is_pure_expression(
208 parser,
209 &switch_stmt.discriminant,
210 self.unresolve_ctxt,
211 parser.comments,
212 ) {
213 parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
214 switch_stmt.span(),
215 String::from("Statement"),
216 ));
217 }
218 }
219 Statement::Class(class_stmt) => {
220 if !is_pure_class(
221 parser,
222 class_stmt.class(),
223 self.unresolve_ctxt,
224 parser.comments,
225 ) {
226 parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
227 class_stmt.span(),
228 String::from("Statement"),
229 ));
230 }
231 }
232 Statement::Var(var_stmt) => match var_stmt {
233 VariableDeclaration::VarDecl(var_decl) => {
234 if !is_pure_var_decl(parser, var_decl, self.unresolve_ctxt, parser.comments) {
235 parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
236 var_stmt.span(),
237 String::from("Statement"),
238 ));
239 }
240 }
241 VariableDeclaration::UsingDecl(_) => {
242 parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
243 var_stmt.span(),
244 String::from("Statement"),
245 ));
246 }
247 },
248 Statement::Empty(_) => {}
249 Statement::Labeled(_) => {}
250 Statement::Block(_) => {}
251 Statement::Fn(_) => {}
252 _ => {
253 parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
254 stmt.span(),
255 String::from("Statement"),
256 ))
257 }
258 };
259 }
260}
261
262pub fn is_pure_pat<'a>(
263 parser: &mut JavascriptParser,
264 pat: &'a Pat,
265 unresolved_ctxt: SyntaxContext,
266 comments: Option<&'a dyn Comments>,
267) -> bool {
268 match pat {
269 Pat::Ident(_) => true,
270 Pat::Array(array_pat) => array_pat.elems.iter().all(|ele| {
271 if let Some(pat) = ele {
272 is_pure_pat(parser, pat, unresolved_ctxt, comments)
273 } else {
274 true
275 }
276 }),
277 Pat::Rest(_) => true,
278 Pat::Invalid(_) | Pat::Assign(_) | Pat::Object(_) => false,
279 Pat::Expr(expr) => is_pure_expression(parser, expr, unresolved_ctxt, comments),
280 }
281}
282
283pub fn is_pure_function<'a>(
284 parser: &mut JavascriptParser,
285 function: &'a Function,
286 unresolved_ctxt: SyntaxContext,
287 comments: Option<&'a dyn Comments>,
288) -> bool {
289 if !function
290 .params
291 .iter()
292 .all(|param| is_pure_pat(parser, ¶m.pat, unresolved_ctxt, comments))
293 {
294 return false;
295 }
296
297 true
298}
299
300pub fn is_pure_expression<'a>(
301 parser: &mut JavascriptParser,
302 expr: &'a Expr,
303 unresolved_ctxt: SyntaxContext,
304 comments: Option<&'a dyn Comments>,
305) -> bool {
306 pub fn _is_pure_expression<'a>(
307 parser: &mut JavascriptParser,
308 expr: &'a Expr,
309 unresolved_ctxt: SyntaxContext,
310 comments: Option<&'a dyn Comments>,
311 ) -> bool {
312 let drive = parser.plugin_drive.clone();
313 if let Some(res) = drive.is_pure(parser, expr) {
314 return res;
315 }
316
317 match expr {
318 Expr::Call(_) => is_pure_call_expr(parser, expr, unresolved_ctxt, comments),
319 Expr::Paren(_) => unreachable!(),
320 Expr::Seq(seq_expr) => seq_expr
321 .exprs
322 .iter()
323 .all(|expr| is_pure_expression(parser, expr, unresolved_ctxt, comments)),
324 _ => !expr.may_have_side_effects(ExprCtx {
325 unresolved_ctxt,
326 is_unresolved_ref_safe: true,
327 in_strict: false,
328 remaining_depth: 4,
329 }),
330 }
331 }
332 _is_pure_expression(parser, expr, unresolved_ctxt, comments)
333}
334
335pub fn is_pure_class_member<'a>(
336 parser: &mut JavascriptParser,
337 member: &'a ClassMember,
338 unresolved_ctxt: SyntaxContext,
339 comments: Option<&'a dyn Comments>,
340) -> bool {
341 let is_key_pure = match member.class_key() {
342 Some(PropName::Ident(_ident)) => true,
343 Some(PropName::Str(_)) => true,
344 Some(PropName::Num(_)) => true,
345 Some(PropName::Computed(computed)) => {
346 is_pure_expression(parser, &computed.expr, unresolved_ctxt, comments)
347 }
348 Some(PropName::BigInt(_)) => true,
349 None => true,
350 };
351 if !is_key_pure {
352 return false;
353 }
354 let is_static = member.is_static();
355 let is_value_pure = match member {
356 ClassMember::Constructor(_) => true,
357 ClassMember::Method(_) => true,
358 ClassMember::PrivateMethod(_) => true,
359 ClassMember::ClassProp(prop) => {
360 if let Some(ref value) = prop.value {
361 is_pure_expression(parser, value, unresolved_ctxt, comments)
362 } else {
363 true
364 }
365 }
366 ClassMember::PrivateProp(prop) => {
367 if let Some(ref value) = prop.value {
368 is_pure_expression(parser, value, unresolved_ctxt, comments)
369 } else {
370 true
371 }
372 }
373 ClassMember::TsIndexSignature(_) => unreachable!(),
374 ClassMember::Empty(_) => true,
375 ClassMember::StaticBlock(_) => false,
376 ClassMember::AutoAccessor(_) => false,
377 };
378 if is_static && !is_value_pure {
379 return false;
380 }
381 true
382}
383
384pub fn is_pure_decl(
385 parser: &mut JavascriptParser,
386 stmt: &Decl,
387 unresolved_ctxt: SyntaxContext,
388 comments: Option<&dyn Comments>,
389) -> bool {
390 match stmt {
391 Decl::Class(class) => is_pure_class(parser, &class.class, unresolved_ctxt, comments),
392 Decl::Fn(_) => true,
393 Decl::Var(var) => is_pure_var_decl(parser, var, unresolved_ctxt, comments),
394 Decl::Using(_) => false,
395 Decl::TsInterface(_) => unreachable!(),
396 Decl::TsTypeAlias(_) => unreachable!(),
397
398 Decl::TsEnum(_) => unreachable!(),
399 Decl::TsModule(_) => unreachable!(),
400 }
401}
402
403pub fn is_pure_class(
404 parser: &mut JavascriptParser,
405 class: &Class,
406 unresolved_ctxt: SyntaxContext,
407 comments: Option<&dyn Comments>,
408) -> bool {
409 if let Some(ref super_class) = class.super_class
410 && !is_pure_expression(parser, super_class, unresolved_ctxt, comments)
411 {
412 return false;
413 }
414 let is_pure_key = |parser: &mut JavascriptParser, key: &PropName| -> bool {
415 match key {
416 PropName::BigInt(_) | PropName::Ident(_) | PropName::Str(_) | PropName::Num(_) => true,
417 PropName::Computed(computed) => {
418 is_pure_expression(parser, &computed.expr, unresolved_ctxt, comments)
419 }
420 }
421 };
422
423 class.body.iter().all(|item| -> bool {
424 match item {
425 ClassMember::Constructor(_) => class.super_class.is_none(),
426 ClassMember::Method(method) => is_pure_key(parser, &method.key),
427 ClassMember::PrivateMethod(method) => is_pure_expression(
428 parser,
429 &Expr::PrivateName(method.key.clone()),
430 unresolved_ctxt,
431 comments,
432 ),
433 ClassMember::ClassProp(prop) => {
434 is_pure_key(parser, &prop.key)
435 && (!prop.is_static
436 || if let Some(ref value) = prop.value {
437 is_pure_expression(parser, value, unresolved_ctxt, comments)
438 } else {
439 true
440 })
441 }
442 ClassMember::PrivateProp(prop) => {
443 is_pure_expression(
444 parser,
445 &Expr::PrivateName(prop.key.clone()),
446 unresolved_ctxt,
447 comments,
448 ) && (!prop.is_static
449 || if let Some(ref value) = prop.value {
450 is_pure_expression(parser, value, unresolved_ctxt, comments)
451 } else {
452 true
453 })
454 }
455 ClassMember::TsIndexSignature(_) => unreachable!(),
456 ClassMember::Empty(_) => true,
457 ClassMember::StaticBlock(_) => true,
458 ClassMember::AutoAccessor(_) => true,
459 }
460 })
461}
462
463fn is_pure_var_decl<'a>(
464 parser: &mut JavascriptParser,
465 var: &'a VarDecl,
466 unresolved_ctxt: SyntaxContext,
467 comments: Option<&'a dyn Comments>,
468) -> bool {
469 var.decls.iter().all(|decl| {
470 if let Some(ref init) = decl.init {
471 is_pure_expression(parser, init, unresolved_ctxt, comments)
472 } else {
473 true
474 }
475 })
476}