swc_ecma_codegen/
lit.rs

1use std::{fmt::Write, io, str};
2
3use ascii::AsciiChar;
4use compact_str::CompactString;
5use swc_common::{Spanned, DUMMY_SP};
6use swc_ecma_ast::*;
7use swc_ecma_codegen_macros::node_impl;
8
9use crate::{text_writer::WriteJs, CowStr, Emitter, SourceMapperExt};
10
11#[node_impl]
12impl MacroNode for Lit {
13    fn emit(&mut self, emitter: &mut Macro) -> Result {
14        emitter.emit_leading_comments_of_span(self.span(), false)?;
15
16        srcmap!(emitter, self, true);
17
18        match self {
19            Lit::Bool(Bool { value, .. }) => {
20                if *value {
21                    keyword!(emitter, "true")
22                } else {
23                    keyword!(emitter, "false")
24                }
25            }
26            Lit::Null(Null { .. }) => keyword!(emitter, "null"),
27            Lit::Str(ref s) => emit!(s),
28            Lit::BigInt(ref s) => emit!(s),
29            Lit::Num(ref n) => emit!(n),
30            Lit::Regex(ref n) => {
31                punct!(emitter, "/");
32                emitter.wr.write_str(&n.exp)?;
33                punct!(emitter, "/");
34                emitter.wr.write_str(&n.flags)?;
35            }
36            Lit::JSXText(ref n) => emit!(n),
37        }
38
39        Ok(())
40    }
41}
42
43#[node_impl]
44impl MacroNode for Str {
45    fn emit(&mut self, emitter: &mut Macro) -> Result {
46        emitter.wr.commit_pending_semi()?;
47
48        emitter.emit_leading_comments_of_span(self.span(), false)?;
49
50        srcmap!(emitter, self, true);
51
52        if &*self.value == "use strict"
53            && self.raw.is_some()
54            && self.raw.as_ref().unwrap().contains('\\')
55            && (!emitter.cfg.inline_script || !self.raw.as_ref().unwrap().contains("script"))
56        {
57            emitter
58                .wr
59                .write_str_lit(DUMMY_SP, self.raw.as_ref().unwrap())?;
60
61            srcmap!(emitter, self, false);
62
63            return Ok(());
64        }
65
66        let target = emitter.cfg.target;
67
68        if !emitter.cfg.minify {
69            if let Some(raw) = &self.raw {
70                let es5_safe = match emitter.cfg.target {
71                    EsVersion::Es3 | EsVersion::Es5 => {
72                        // Block raw strings containing ES6+ Unicode escapes (\u{...}) for ES3/ES5
73                        // targets
74                        !raw.contains("\\u{")
75                    }
76                    _ => true,
77                };
78
79                if es5_safe
80                    && (!emitter.cfg.ascii_only || raw.is_ascii())
81                    && (!emitter.cfg.inline_script
82                        || !self.raw.as_ref().unwrap().contains("script"))
83                {
84                    emitter.wr.write_str_lit(DUMMY_SP, raw)?;
85                    return Ok(());
86                }
87            }
88        }
89
90        let (quote_char, mut value) = get_quoted_utf16(&self.value, emitter.cfg.ascii_only, target);
91
92        if emitter.cfg.inline_script {
93            value = CowStr::Owned(
94                replace_close_inline_script(&value)
95                    .replace("\x3c!--", "\\x3c!--")
96                    .replace("--\x3e", "--\\x3e")
97                    .into(),
98            );
99        }
100
101        let quote_str = [quote_char.as_byte()];
102        let quote_str = unsafe {
103            // Safety: quote_char is valid ascii
104            str::from_utf8_unchecked(&quote_str)
105        };
106
107        emitter.wr.write_str(quote_str)?;
108        emitter.wr.write_str_lit(DUMMY_SP, &value)?;
109        emitter.wr.write_str(quote_str)?;
110
111        // srcmap!(emitter,self, false);
112
113        Ok(())
114    }
115}
116
117#[node_impl]
118impl MacroNode for Number {
119    fn emit(&mut self, emitter: &mut Macro) -> Result {
120        emitter.emit_num_lit_internal(self, false)?;
121
122        Ok(())
123    }
124}
125
126#[node_impl]
127impl MacroNode for BigInt {
128    fn emit(&mut self, emitter: &mut Macro) -> Result {
129        emitter.emit_leading_comments_of_span(self.span, false)?;
130
131        if emitter.cfg.minify {
132            let value = if *self.value >= 10000000000000000_i64.into() {
133                format!("0x{}", self.value.to_str_radix(16))
134            } else if *self.value <= (-10000000000000000_i64).into() {
135                format!("-0x{}", (-*self.value.clone()).to_str_radix(16))
136            } else {
137                self.value.to_string()
138            };
139            emitter.wr.write_lit(self.span, &value)?;
140            emitter.wr.write_lit(self.span, "n")?;
141        } else {
142            match &self.raw {
143                Some(raw) => {
144                    if raw.len() > 2 && emitter.cfg.target < EsVersion::Es2021 && raw.contains('_')
145                    {
146                        emitter.wr.write_str_lit(self.span, &raw.replace('_', ""))?;
147                    } else {
148                        emitter.wr.write_str_lit(self.span, raw)?;
149                    }
150                }
151                _ => {
152                    emitter.wr.write_lit(self.span, &self.value.to_string())?;
153                    emitter.wr.write_lit(self.span, "n")?;
154                }
155            }
156        }
157
158        Ok(())
159    }
160}
161
162#[node_impl]
163impl MacroNode for Bool {
164    fn emit(&mut self, emitter: &mut Macro) -> Result {
165        emitter.emit_leading_comments_of_span(self.span(), false)?;
166
167        if self.value {
168            keyword!(emitter, self.span, "true")
169        } else {
170            keyword!(emitter, self.span, "false")
171        }
172
173        Ok(())
174    }
175}
176
177pub fn replace_close_inline_script(raw: &str) -> CowStr {
178    let chars = raw.as_bytes();
179    let pattern_len = 8; // </script>
180
181    let mut matched_indexes = chars
182        .iter()
183        .enumerate()
184        .filter(|(index, byte)| {
185            byte == &&b'<'
186                && index + pattern_len < chars.len()
187                && chars[index + 1..index + pattern_len].eq_ignore_ascii_case(b"/script")
188                && matches!(
189                    chars[index + pattern_len],
190                    b'>' | b' ' | b'\t' | b'\n' | b'\x0C' | b'\r'
191                )
192        })
193        .map(|(index, _)| index)
194        .peekable();
195
196    if matched_indexes.peek().is_none() {
197        return CowStr::Borrowed(raw);
198    }
199
200    let mut result = CompactString::new(raw);
201
202    for (offset, i) in matched_indexes.enumerate() {
203        result.insert(i + 1 + offset, '\\');
204    }
205
206    CowStr::Owned(result)
207}
208
209impl<W, S: swc_common::SourceMapper> Emitter<'_, W, S>
210where
211    W: WriteJs,
212    S: SourceMapperExt,
213{
214    /// `1.toString` is an invalid property access,
215    /// should emit a dot after the literal if return true
216    pub fn emit_num_lit_internal(
217        &mut self,
218        num: &Number,
219        mut detect_dot: bool,
220    ) -> std::result::Result<bool, io::Error> {
221        self.wr.commit_pending_semi()?;
222
223        self.emit_leading_comments_of_span(num.span(), false)?;
224
225        // Handle infinity
226        if num.value.is_infinite() && num.raw.is_none() {
227            self.wr.write_str_lit(num.span, &num.value.print())?;
228
229            return Ok(false);
230        }
231
232        let mut striped_raw = None;
233        let mut value = String::default();
234
235        srcmap!(self, num, true);
236
237        if self.cfg.minify {
238            if num.value.is_infinite() && num.raw.is_some() {
239                self.wr.write_str_lit(DUMMY_SP, num.raw.as_ref().unwrap())?;
240            } else {
241                value = minify_number(num.value, &mut detect_dot);
242                self.wr.write_str_lit(DUMMY_SP, &value)?;
243            }
244        } else {
245            match &num.raw {
246                Some(raw) => {
247                    if raw.len() > 2 && self.cfg.target < EsVersion::Es2015 && {
248                        let slice = &raw.as_bytes()[..2];
249                        slice == b"0b" || slice == b"0o" || slice == b"0B" || slice == b"0O"
250                    } {
251                        if num.value.is_infinite() && num.raw.is_some() {
252                            self.wr.write_str_lit(DUMMY_SP, num.raw.as_ref().unwrap())?;
253                        } else {
254                            value = num.value.print();
255                            self.wr.write_str_lit(DUMMY_SP, &value)?;
256                        }
257                    } else if raw.len() > 2
258                        && self.cfg.target < EsVersion::Es2021
259                        && raw.contains('_')
260                    {
261                        let value = raw.replace('_', "");
262                        self.wr.write_str_lit(DUMMY_SP, &value)?;
263
264                        striped_raw = Some(value);
265                    } else {
266                        self.wr.write_str_lit(DUMMY_SP, raw)?;
267
268                        if !detect_dot {
269                            return Ok(false);
270                        }
271
272                        striped_raw = Some(raw.replace('_', ""));
273                    }
274                }
275                _ => {
276                    value = num.value.print();
277                    self.wr.write_str_lit(DUMMY_SP, &value)?;
278                }
279            }
280        }
281
282        // fast return
283        if !detect_dot {
284            return Ok(false);
285        }
286
287        Ok(striped_raw
288            .map(|raw| {
289                if raw.bytes().all(|c| c.is_ascii_digit()) {
290                    // Maybe legacy octal
291                    // Do we really need to support pre es5?
292                    let slice = raw.as_bytes();
293                    if slice.len() >= 2 && slice[0] == b'0' {
294                        return false;
295                    }
296
297                    return true;
298                }
299
300                false
301            })
302            .unwrap_or_else(|| {
303                let bytes = value.as_bytes();
304
305                if !bytes.contains(&b'.') && !bytes.contains(&b'e') {
306                    return true;
307                }
308
309                false
310            }))
311    }
312}
313
314/// Returns `(quote_char, value)`
315pub fn get_quoted_utf16(v: &str, ascii_only: bool, target: EsVersion) -> (AsciiChar, CowStr) {
316    // Fast path: If the string is ASCII and doesn't need escaping, we can avoid
317    // allocation
318    if v.is_ascii() {
319        let mut needs_escaping = false;
320        let mut single_quote_count = 0;
321        let mut double_quote_count = 0;
322
323        for &b in v.as_bytes() {
324            match b {
325                b'\'' => single_quote_count += 1,
326                b'"' => double_quote_count += 1,
327                // Control characters and backslash need escaping
328                0..=0x1f | b'\\' => {
329                    needs_escaping = true;
330                    break;
331                }
332                _ => {}
333            }
334        }
335
336        if !needs_escaping {
337            let quote_char = if double_quote_count > single_quote_count {
338                AsciiChar::Apostrophe
339            } else {
340                AsciiChar::Quotation
341            };
342
343            // If there are no quotes to escape, we can return the original string
344            if (quote_char == AsciiChar::Apostrophe && single_quote_count == 0)
345                || (quote_char == AsciiChar::Quotation && double_quote_count == 0)
346            {
347                return (quote_char, CowStr::Borrowed(v));
348            }
349        }
350    }
351
352    // Slow path: Original implementation for strings that need processing
353    // Count quotes first to determine which quote character to use
354    let (mut single_quote_count, mut double_quote_count) = (0, 0);
355    for c in v.chars() {
356        match c {
357            '\'' => single_quote_count += 1,
358            '"' => double_quote_count += 1,
359            _ => {}
360        }
361    }
362
363    // Pre-calculate capacity to avoid reallocations
364    let quote_char = if double_quote_count > single_quote_count {
365        AsciiChar::Apostrophe
366    } else {
367        AsciiChar::Quotation
368    };
369    let escape_char = if quote_char == AsciiChar::Apostrophe {
370        AsciiChar::Apostrophe
371    } else {
372        AsciiChar::Quotation
373    };
374    let escape_count = if quote_char == AsciiChar::Apostrophe {
375        single_quote_count
376    } else {
377        double_quote_count
378    };
379
380    // Add 1 for each escaped quote
381    let capacity = v.len() + escape_count;
382    let mut buf = CompactString::with_capacity(capacity);
383
384    let mut iter = v.chars().peekable();
385    while let Some(c) = iter.next() {
386        match c {
387            '\x00' => {
388                if target < EsVersion::Es5 || matches!(iter.peek(), Some('0'..='9')) {
389                    buf.push_str("\\x00");
390                } else {
391                    buf.push_str("\\0");
392                }
393            }
394            '\u{0008}' => buf.push_str("\\b"),
395            '\u{000c}' => buf.push_str("\\f"),
396            '\n' => buf.push_str("\\n"),
397            '\r' => buf.push_str("\\r"),
398            '\u{000b}' => buf.push_str("\\v"),
399            '\t' => buf.push('\t'),
400            '\\' => {
401                let next = iter.peek();
402                match next {
403                    Some('u') => {
404                        let mut inner_iter = iter.clone();
405                        inner_iter.next();
406
407                        let mut is_curly = false;
408                        let mut next = inner_iter.peek();
409
410                        if next == Some(&'{') {
411                            is_curly = true;
412                            inner_iter.next();
413                            next = inner_iter.peek();
414                        } else if next != Some(&'D') && next != Some(&'d') {
415                            buf.push('\\');
416                        }
417
418                        if let Some(c @ 'D' | c @ 'd') = next {
419                            let mut inner_buf = String::with_capacity(8);
420                            inner_buf.push('\\');
421                            inner_buf.push('u');
422
423                            if is_curly {
424                                inner_buf.push('{');
425                            }
426
427                            inner_buf.push(*c);
428                            inner_iter.next();
429
430                            let mut is_valid = true;
431                            for _ in 0..3 {
432                                match inner_iter.next() {
433                                    Some(c @ '0'..='9') | Some(c @ 'a'..='f')
434                                    | Some(c @ 'A'..='F') => {
435                                        inner_buf.push(c);
436                                    }
437                                    _ => {
438                                        is_valid = false;
439                                        break;
440                                    }
441                                }
442                            }
443
444                            if is_curly {
445                                inner_buf.push('}');
446                            }
447
448                            let range = if is_curly {
449                                3..(inner_buf.len() - 1)
450                            } else {
451                                2..6
452                            };
453
454                            if is_valid {
455                                let val_str = &inner_buf[range];
456                                if let Ok(v) = u32::from_str_radix(val_str, 16) {
457                                    if v > 0xffff {
458                                        buf.push_str(&inner_buf);
459                                        let end = if is_curly { 7 } else { 5 };
460                                        for _ in 0..end {
461                                            iter.next();
462                                        }
463                                    } else if (0xd800..=0xdfff).contains(&v) {
464                                        buf.push('\\');
465                                    } else {
466                                        buf.push_str("\\\\");
467                                    }
468                                } else {
469                                    buf.push_str("\\\\");
470                                }
471                            } else {
472                                buf.push_str("\\\\");
473                            }
474                        } else if is_curly {
475                            buf.push_str("\\\\");
476                        } else {
477                            buf.push('\\');
478                        }
479                    }
480                    _ => buf.push_str("\\\\"),
481                }
482            }
483            c if c == escape_char => {
484                buf.push('\\');
485                buf.push(c);
486            }
487            '\x01'..='\x0f' => {
488                buf.push_str("\\x0");
489                write!(&mut buf, "{:x}", c as u8).unwrap();
490            }
491            '\x10'..='\x1f' => {
492                buf.push_str("\\x");
493                write!(&mut buf, "{:x}", c as u8).unwrap();
494            }
495            '\x20'..='\x7e' => buf.push(c),
496            '\u{7f}'..='\u{ff}' => {
497                if ascii_only || target <= EsVersion::Es5 {
498                    buf.push_str("\\x");
499                    write!(&mut buf, "{:x}", c as u8).unwrap();
500                } else {
501                    buf.push(c);
502                }
503            }
504            '\u{2028}' => buf.push_str("\\u2028"),
505            '\u{2029}' => buf.push_str("\\u2029"),
506            '\u{FEFF}' => buf.push_str("\\uFEFF"),
507            c => {
508                if c.is_ascii() {
509                    buf.push(c);
510                } else if c > '\u{FFFF}' {
511                    if target <= EsVersion::Es5 {
512                        let h = ((c as u32 - 0x10000) / 0x400) + 0xd800;
513                        let l = (c as u32 - 0x10000) % 0x400 + 0xdc00;
514                        write!(&mut buf, "\\u{h:04X}\\u{l:04X}").unwrap();
515                    } else if ascii_only {
516                        write!(&mut buf, "\\u{{{:04X}}}", c as u32).unwrap();
517                    } else {
518                        buf.push(c);
519                    }
520                } else if ascii_only {
521                    write!(&mut buf, "\\u{:04X}", c as u16).unwrap();
522                } else {
523                    buf.push(c);
524                }
525            }
526        }
527    }
528
529    (quote_char, CowStr::Owned(buf))
530}
531
532pub fn minify_number(num: f64, detect_dot: &mut bool) -> String {
533    // ddddd -> 0xhhhh
534    // len(0xhhhh) == len(ddddd)
535    // 10000000 <= num <= 0xffffff
536    'hex: {
537        if num.fract() == 0.0 && num.abs() <= u64::MAX as f64 {
538            let int = num.abs() as u64;
539
540            if int < 10000000 {
541                break 'hex;
542            }
543
544            // use scientific notation
545            if int % 1000 == 0 {
546                break 'hex;
547            }
548
549            *detect_dot = false;
550            return format!(
551                "{}{:#x}",
552                if num.is_sign_negative() { "-" } else { "" },
553                int
554            );
555        }
556    }
557
558    let mut num = num.to_string();
559
560    if num.contains(".") {
561        *detect_dot = false;
562    }
563
564    if let Some(num) = num.strip_prefix("0.") {
565        let cnt = clz(num);
566        if cnt > 2 {
567            return format!("{}e-{}", &num[cnt..], num.len());
568        }
569        return format!(".{num}");
570    }
571
572    if let Some(num) = num.strip_prefix("-0.") {
573        let cnt = clz(num);
574        if cnt > 2 {
575            return format!("-{}e-{}", &num[cnt..], num.len());
576        }
577        return format!("-.{num}");
578    }
579
580    if num.ends_with("000") {
581        *detect_dot = false;
582
583        let cnt = num
584            .as_bytes()
585            .iter()
586            .rev()
587            .skip(3)
588            .take_while(|&&c| c == b'0')
589            .count()
590            + 3;
591
592        num.truncate(num.len() - cnt);
593        num.push('e');
594        num.push_str(&cnt.to_string());
595    }
596
597    num
598}
599
600fn clz(s: &str) -> usize {
601    s.as_bytes().iter().take_while(|&&c| c == b'0').count()
602}
603
604pub trait Print {
605    fn print(&self) -> String;
606}
607
608impl Print for f64 {
609    fn print(&self) -> String {
610        // preserve -0.0
611        if *self == 0.0 {
612            return self.to_string();
613        }
614
615        let mut buffer = ryu_js::Buffer::new();
616        buffer.format(*self).to_string()
617    }
618}