swc_ecma_minifier/compress/pure/
strings.rs1use std::{borrow::Cow, mem::take};
2
3use swc_atoms::{
4 wtf8::{Wtf8, Wtf8Buf},
5 Atom, Wtf8Atom,
6};
7use swc_common::{util::take::Take, DUMMY_SP};
8use swc_ecma_ast::*;
9use swc_ecma_utils::{ExprExt, Type, Value};
10use Value::Known;
11
12use super::Pure;
13
14impl Pure<'_> {
15 pub(super) fn eval_str_addition(&mut self, e: &mut Expr) {
18 let (span, l_l, r_l, r_r) = match e {
19 Expr::Bin(
20 e @ BinExpr {
21 op: op!(bin, "+"), ..
22 },
23 ) => match &mut *e.right {
24 Expr::Bin(
25 r @ BinExpr {
26 op: op!(bin, "+"), ..
27 },
28 ) => (e.span, &mut *e.left, &mut *r.left, &mut r.right),
29 _ => return,
30 },
31 _ => return,
32 };
33
34 match l_l.get_type(self.expr_ctx) {
35 Known(Type::Str) => {}
36 _ => return,
37 }
38 match r_l.get_type(self.expr_ctx) {
39 Known(Type::Str) => {}
40 _ => return,
41 }
42
43 let lls = l_l.as_pure_string(self.expr_ctx);
44 let rls = r_l.as_pure_string(self.expr_ctx);
45
46 if let (Known(lls), Known(rls)) = (lls, rls) {
47 self.changed = true;
48 report_change!("evaluate: 'foo' + ('bar' + baz) => 'foobar' + baz");
49
50 let s = lls.into_owned() + &*rls;
51 *e = BinExpr {
52 span,
53 op: op!(bin, "+"),
54 left: s.into(),
55 right: r_r.take(),
56 }
57 .into();
58 }
59 }
60
61 pub(super) fn eval_tpl_as_str(&mut self, e: &mut Expr) {
62 if !self.options.evaluate {
63 return;
64 }
65
66 fn need_unsafe(e: &Expr) -> bool {
67 match e {
68 Expr::Lit(..) => false,
69 Expr::Bin(e) => need_unsafe(&e.left) || need_unsafe(&e.right),
70 _ => true,
71 }
72 }
73
74 let tpl = match e {
75 Expr::Tpl(e) => e,
76 _ => return,
77 };
78
79 if tpl.quasis.len() == 2
80 && (tpl.quasis[0].cooked.is_some() || !tpl.quasis[0].raw.contains('\\'))
81 && tpl.quasis[1].raw.is_empty()
82 {
83 if !self.options.unsafe_passes && need_unsafe(&tpl.exprs[0]) {
84 return;
85 }
86
87 self.changed = true;
88 report_change!("evaluating a template to a string");
89 *e = BinExpr {
90 span: tpl.span,
91 op: op!(bin, "+"),
92 left: tpl.quasis[0]
93 .cooked
94 .clone()
95 .unwrap_or_else(|| tpl.quasis[0].raw.clone().into())
96 .into(),
97 right: tpl.exprs[0].take(),
98 }
99 .into();
100 }
101 }
102
103 pub(super) fn eval_nested_tpl(&mut self, e: &mut Expr) {
104 let tpl = match e {
105 Expr::Tpl(e) => e,
106 _ => return,
107 };
108
109 if !tpl.exprs.iter().any(|e| match &**e {
110 Expr::Tpl(t) => t
111 .quasis
112 .iter()
113 .all(|q| q.cooked.is_some() && !q.raw.contains('\\')),
114 _ => false,
115 }) {
116 return;
117 }
118
119 self.changed = true;
120 report_change!("evaluating nested template literals");
121
122 let mut new_tpl = Tpl {
123 span: tpl.span,
124 quasis: Default::default(),
125 exprs: Default::default(),
126 };
127 let mut cur_cooked_str = Wtf8Buf::new();
128 let mut cur_raw_str = String::new();
129
130 for idx in 0..(tpl.quasis.len() + tpl.exprs.len()) {
131 if idx % 2 == 0 {
132 let q = tpl.quasis[idx / 2].take();
133
134 cur_cooked_str.push_str(&Str::from_tpl_raw(&q.raw));
135 cur_raw_str.push_str(&q.raw);
136 } else {
137 let mut e = tpl.exprs[idx / 2].take();
138 self.eval_nested_tpl(&mut e);
139
140 match *e {
141 Expr::Tpl(mut e) => {
142 for idx in 0..(e.quasis.len() + e.exprs.len()) {
146 if idx % 2 == 0 {
147 let q = e.quasis[idx / 2].take();
148
149 cur_cooked_str.push_str(Str::from_tpl_raw(&q.raw).as_ref());
150 cur_raw_str.push_str(&q.raw);
151 } else {
152 let cooked = Wtf8Atom::from(&*cur_cooked_str);
153 let raw = Atom::from(&*cur_raw_str);
154 cur_cooked_str.clear();
155 cur_raw_str.clear();
156
157 new_tpl.quasis.push(TplElement {
158 span: DUMMY_SP,
159 tail: false,
160 cooked: Some(cooked),
161 raw,
162 });
163
164 let e = e.exprs[idx / 2].take();
165
166 new_tpl.exprs.push(e);
167 }
168 }
169 }
170 _ => {
171 let cooked = Wtf8Atom::from(&*cur_cooked_str);
172 let raw = Atom::from(&*cur_raw_str);
173 cur_cooked_str.clear();
174 cur_raw_str.clear();
175
176 new_tpl.quasis.push(TplElement {
177 span: DUMMY_SP,
178 tail: false,
179 cooked: Some(cooked),
180 raw,
181 });
182
183 new_tpl.exprs.push(e);
184 }
185 }
186 }
187 }
188
189 let cooked = Wtf8Atom::from(&*cur_cooked_str);
190 let raw = Atom::from(&*cur_raw_str);
191 new_tpl.quasis.push(TplElement {
192 span: DUMMY_SP,
193 tail: false,
194 cooked: Some(cooked),
195 raw,
196 });
197
198 *e = new_tpl.into();
199 }
200
201 pub(super) fn convert_tpl_to_str(&mut self, e: &mut Expr) {
203 match e {
204 Expr::Tpl(t) if t.quasis.len() == 1 && t.exprs.is_empty() => {
205 if let Some(value) = &t.quasis[0].cooked {
206 if let Some(value) = value.as_str() {
207 if value.chars().all(|c| match c {
208 '\\' => false,
209 '\u{0020}'..='\u{007e}' => true,
210 '\n' | '\r' => self.config.force_str_for_tpl,
211 _ => false,
212 }) {
213 report_change!("converting a template literal to a string literal");
214
215 *e = Lit::Str(Str {
216 span: t.span,
217 raw: None,
218 value: t.quasis[0].cooked.clone().unwrap(),
219 })
220 .into();
221 return;
222 }
223 }
224 }
225
226 let c = &t.quasis[0].raw;
227
228 if c.chars().all(|c| match c {
229 '\u{0020}'..='\u{007e}' => true,
230 '\n' | '\r' => self.config.force_str_for_tpl,
231 _ => false,
232 }) && (self.config.force_str_for_tpl
233 || c.contains("\\`")
234 || (!c.contains("\\n") && !c.contains("\\r")))
235 && !c.contains("\\0")
236 && !c.contains("\\x")
237 && !c.contains("\\u")
238 {
239 let value = Str::from_tpl_raw(c);
240
241 report_change!("converting a template literal to a string literal");
242
243 *e = Lit::Str(Str {
244 span: t.span,
245 raw: None,
246 value: value.into(),
247 })
248 .into();
249 }
250 }
251 _ => {}
252 }
253 }
254
255 pub(super) fn compress_tpl(&mut self, tpl: &mut Tpl) {
261 debug_assert_eq!(tpl.exprs.len() + 1, tpl.quasis.len());
262 let has_str_lit = tpl
263 .exprs
264 .iter()
265 .any(|expr| matches!(&**expr, Expr::Lit(Lit::Str(..))));
266 if !has_str_lit {
267 return;
268 }
269
270 trace_op!("compress_tpl");
271
272 let mut quasis = Vec::new();
273 let mut exprs = Vec::new();
274 let mut cur_raw = String::new();
275 let mut cur_cooked = Some(Wtf8Buf::new());
276
277 for i in 0..(tpl.exprs.len() + tpl.quasis.len()) {
278 if i % 2 == 0 {
279 let i = i / 2;
280 let q = tpl.quasis[i].clone();
281
282 if q.cooked.is_some() {
283 if let Some(cur_cooked) = &mut cur_cooked {
284 cur_cooked.push_str("");
285 }
286 } else {
287 cur_cooked = None;
290 }
291 } else {
292 let i = i / 2;
293 let e = &tpl.exprs[i];
294
295 match &**e {
296 Expr::Lit(Lit::Str(s)) => {
297 if cur_cooked.is_none() && s.raw.is_none() {
298 return;
299 }
300
301 if let Some(cur_cooked) = &mut cur_cooked {
302 cur_cooked.push_str("");
303 }
304 }
305 _ => {
306 cur_cooked = Some(Wtf8Buf::new());
307 }
308 }
309 }
310 }
311
312 cur_cooked = Some(Default::default());
313
314 for i in 0..(tpl.exprs.len() + tpl.quasis.len()) {
315 if i % 2 == 0 {
316 let i = i / 2;
317 let q = tpl.quasis[i].take();
318
319 cur_raw.push_str(&q.raw);
320 if let Some(cooked) = q.cooked {
321 if let Some(cur_cooked) = &mut cur_cooked {
322 cur_cooked.push_wtf8(&cooked);
323 }
324 } else {
325 cur_cooked = None;
328 }
329 } else {
330 let i = i / 2;
331 let e = tpl.exprs[i].take();
332
333 match *e {
334 Expr::Lit(Lit::Str(s)) => {
335 if let Some(cur_cooked) = &mut cur_cooked {
336 cur_cooked.push_wtf8(&convert_str_value_to_tpl_cooked(&s.value));
337 }
338
339 if let Some(raw) = &s.raw {
340 if raw.len() >= 2 {
341 cur_raw
343 .push_str(&convert_str_raw_to_tpl_raw(&raw[1..raw.len() - 1]));
344 }
345 } else {
346 cur_raw.push_str(&convert_str_value_to_tpl_raw(&s.value));
347 }
348 }
349 _ => {
350 quasis.push(TplElement {
351 span: DUMMY_SP,
352 tail: true,
353 cooked: cur_cooked.take().map(From::from),
354 raw: take(&mut cur_raw).into(),
355 });
356 cur_cooked = Some(Wtf8Buf::new());
357
358 exprs.push(e);
359 }
360 }
361 }
362 }
363
364 report_change!("compressing template literals");
365
366 quasis.push(TplElement {
367 span: DUMMY_SP,
368 tail: true,
369 cooked: cur_cooked.map(From::from),
370 raw: cur_raw.into(),
371 });
372
373 debug_assert_eq!(exprs.len() + 1, quasis.len());
374
375 tpl.quasis = quasis;
376 tpl.exprs = exprs;
377 }
378
379 pub(super) fn concat_tpl(&mut self, l: &mut Expr, r: &mut Expr) {
381 match (&mut *l, &mut *r) {
382 (Expr::Tpl(l), Expr::Lit(Lit::Str(rs))) => {
383 if let Some(raw) = &rs.raw {
384 if raw.len() <= 2 {
385 return;
386 }
387 }
388
389 if let Some(l_last) = l.quasis.last_mut() {
391 self.changed = true;
392
393 report_change!(
394 "template: Concatted a string (`{}`) on rhs of `+` to a template literal",
395 rs.value
396 );
397
398 if let Some(cooked) = &mut l_last.cooked {
399 let mut c = Wtf8Buf::from(&*cooked);
400 c.push_wtf8(&convert_str_value_to_tpl_cooked(&rs.value));
401 *cooked = c.into();
402 }
403
404 l_last.raw = format!(
405 "{}{}",
406 l_last.raw,
407 rs.raw
408 .clone()
409 .map(|s| convert_str_raw_to_tpl_raw(&s[1..s.len() - 1]))
410 .unwrap_or_else(|| convert_str_value_to_tpl_raw(&rs.value).into())
411 )
412 .into();
413
414 r.take();
415 }
416 }
417
418 (Expr::Lit(Lit::Str(ls)), Expr::Tpl(r)) => {
419 if let Some(raw) = &ls.raw {
420 if raw.len() <= 2 {
421 return;
422 }
423 }
424
425 if let Some(r_first) = r.quasis.first_mut() {
427 self.changed = true;
428
429 report_change!(
430 "template: Prepended a string (`{}`) on lhs of `+` to a template literal",
431 ls.value
432 );
433
434 if let Some(cooked) = &mut r_first.cooked {
435 let mut c = Wtf8Buf::new();
436 c.push_wtf8(&convert_str_value_to_tpl_cooked(&ls.value));
437 c.push_wtf8(&*cooked);
438 *cooked = c.into();
439 }
440
441 let new: Atom = format!(
442 "{}{}",
443 ls.raw
444 .clone()
445 .map(|s| convert_str_raw_to_tpl_raw(&s[1..s.len() - 1]))
446 .unwrap_or_else(|| convert_str_value_to_tpl_raw(&ls.value).into()),
447 r_first.raw
448 )
449 .into();
450 r_first.raw = new;
451
452 l.take();
453 }
454 }
455
456 (Expr::Tpl(l), Expr::Tpl(rt)) => {
457 {
461 let l_last = l.quasis.pop().unwrap();
462 let r_first = rt.quasis.first_mut().unwrap();
463 let new: Atom = format!("{}{}", l_last.raw, r_first.raw).into();
464
465 r_first.raw = new;
466 }
467
468 l.quasis.extend(rt.quasis.take());
469 l.exprs.extend(rt.exprs.take());
470 r.take();
472
473 debug_assert!(l.quasis.len() == l.exprs.len() + 1, "{l:?} is invalid");
474 self.changed = true;
475 report_change!("strings: Merged two template literals");
476 }
477 _ => {}
478 }
479 }
480
481 pub(super) fn concat_str(&mut self, e: &mut Expr) {
484 if let Expr::Bin(
485 bin @ BinExpr {
486 op: op!(bin, "+"), ..
487 },
488 ) = e
489 {
490 if let Expr::Bin(
491 left @ BinExpr {
492 op: op!(bin, "+"), ..
493 },
494 ) = &mut *bin.left
495 {
496 let type_of_second = left.right.get_type(self.expr_ctx);
497 let type_of_third = bin.right.get_type(self.expr_ctx);
498
499 if let Value::Known(Type::Str) = type_of_second {
500 if let Value::Known(Type::Str) = type_of_third {
501 if let Value::Known(second_str) = left.right.as_pure_wtf8(self.expr_ctx) {
502 if let Value::Known(third_str) = bin.right.as_pure_wtf8(self.expr_ctx) {
503 let new_str = second_str.into_owned() + &*third_str;
504 let left_span = left.span;
505
506 self.changed = true;
507 report_change!(
508 "strings: Concatting `{} + {}` to `{}`",
509 second_str,
510 third_str,
511 new_str
512 );
513
514 *e = BinExpr {
515 span: bin.span,
516 op: op!(bin, "+"),
517 left: left.left.take(),
518 right: Lit::Str(Str {
519 span: left_span,
520 raw: None,
521 value: new_str.into(),
522 })
523 .into(),
524 }
525 .into();
526 }
527 }
528 }
529 }
530 }
531 }
532 }
533
534 pub(super) fn drop_useless_addition_of_str(&mut self, e: &mut Expr) {
535 if let Expr::Bin(BinExpr {
536 op: op!(bin, "+"),
537 left,
538 right,
539 ..
540 }) = e
541 {
542 let lt = left.get_type(self.expr_ctx);
543 let rt = right.get_type(self.expr_ctx);
544 if let Value::Known(Type::Str) = lt {
545 if let Value::Known(Type::Str) = rt {
546 match &**left {
547 Expr::Lit(Lit::Str(Str { value, .. })) if value.is_empty() => {
548 self.changed = true;
549 report_change!(
550 "string: Dropping empty string literal (in lhs) because it does \
551 not changes type"
552 );
553
554 *e = *right.take();
555 return;
556 }
557 _ => (),
558 }
559
560 match &**right {
561 Expr::Lit(Lit::Str(Str { value, .. })) if value.is_empty() => {
562 self.changed = true;
563 report_change!(
564 "string: Dropping empty string literal (in rhs) because it does \
565 not changes type"
566 );
567
568 *e = *left.take();
569 }
570 _ => (),
571 }
572 }
573 }
574 }
575 }
576}
577
578pub(super) fn convert_str_value_to_tpl_cooked(value: &Wtf8) -> Cow<Wtf8> {
579 let mut result = Wtf8Buf::default();
580 let mut need_replace = false;
581
582 let mut iter = value.code_points().peekable();
583 while let Some(code_point) = iter.next() {
584 if let Some(ch) = code_point.to_char() {
585 match ch {
586 '\\' => {
587 if let Some(next) = iter.peek().and_then(|c| c.to_char()) {
588 match next {
589 '\\' => {
590 need_replace = true;
591 result.push_char('\\');
592 iter.next();
593 }
594 '`' => {
595 need_replace = true;
596 result.push_char('`');
597 iter.next();
598 }
599 '$' => {
600 need_replace = true;
601 result.push_char('$');
602 iter.next();
603 }
604 _ => result.push_char(ch),
605 }
606 } else {
607 result.push_char(ch);
608 }
609 }
610 _ => result.push_char(ch),
611 }
612 } else {
613 need_replace = true;
614 result.push_str(&format!("\\u{:04X}", code_point.to_u32()));
615 }
616 }
617
618 if need_replace {
619 result.into()
620 } else {
621 Cow::Borrowed(value)
622 }
623}
624
625pub(super) fn convert_str_value_to_tpl_raw(value: &Wtf8) -> Cow<str> {
626 let mut result = String::default();
627
628 let iter = value.code_points();
629 for code_point in iter {
630 if let Some(ch) = code_point.to_char() {
631 match ch {
632 '\\' => {
633 result.push_str("\\\\");
634 }
635 '`' => {
636 result.push_str("\\`");
637 }
638 '$' => {
639 result.push_str("\\$");
640 }
641 '\n' => {
642 result.push_str("\\n");
643 }
644 '\r' => {
645 result.push_str("\\r");
646 }
647 _ => result.push(ch),
648 }
649 } else {
650 result.push_str(&format!("\\u{:04X}", code_point.to_u32()));
651 }
652 }
653
654 result.into()
655}
656
657pub(super) fn convert_str_raw_to_tpl_raw(value: &str) -> Atom {
658 value.replace('`', "\\`").replace('$', "\\$").into()
659}