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