Skip to main content

swc_ecma_codegen/
lit.rs

1use std::{fmt::Write, io, str};
2
3use ascii::AsciiChar;
4use compact_str::CompactString;
5use swc_atoms::wtf8::{CodePoint, Wtf8};
6use swc_common::{Spanned, DUMMY_SP};
7use swc_ecma_ast::*;
8use swc_ecma_codegen_macros::node_impl;
9use swc_ecma_utils::number::minify_number;
10
11#[cfg(swc_ast_unknown)]
12use crate::unknown_error;
13use crate::{text_writer::WriteJs, CowStr, Emitter, ExprPrecedence, SourceMapperExt};
14
15#[derive(Clone, Copy)]
16enum NonFiniteNumber {
17    NaN,
18    PositiveInfinity,
19    NegativeInfinity,
20}
21
22impl NonFiniteNumber {
23    #[inline]
24    fn from_value(value: f64) -> Self {
25        debug_assert!(!value.is_finite());
26
27        if value.is_nan() {
28            Self::NaN
29        } else if value.is_sign_positive() {
30            Self::PositiveInfinity
31        } else {
32            Self::NegativeInfinity
33        }
34    }
35
36    #[inline]
37    fn division_text(self, minify: bool) -> &'static str {
38        match (self, minify) {
39            (Self::NaN, false) => "0 / 0",
40            (Self::PositiveInfinity, false) => "1 / 0",
41            (Self::NegativeInfinity, false) => "-1 / 0",
42            (Self::NaN, true) => "0/0",
43            (Self::PositiveInfinity, true) => "1/0",
44            (Self::NegativeInfinity, true) => "-1/0",
45        }
46    }
47}
48
49#[node_impl]
50impl MacroNode for Lit {
51    fn emit(&mut self, emitter: &mut Macro) -> Result {
52        emitter.emit_leading_comments_of_span(self.span(), false)?;
53
54        srcmap!(emitter, self, true);
55
56        match self {
57            Lit::Bool(Bool { value, .. }) => {
58                if *value {
59                    keyword!(emitter, "true")
60                } else {
61                    keyword!(emitter, "false")
62                }
63            }
64            Lit::Null(Null { .. }) => keyword!(emitter, "null"),
65            Lit::Str(ref s) => emit!(s),
66            Lit::BigInt(ref s) => emit!(s),
67            Lit::Num(ref n) => {
68                emitter.emit_num_lit_expr(n, ExprPrecedence::LOWEST, false)?;
69            }
70            Lit::Regex(ref n) => {
71                punct!(emitter, "/");
72                // Encode non-ASCII characters in regex pattern when ascii_only is enabled
73                let encoded_exp = encode_regex_for_ascii(&n.exp, emitter.cfg.ascii_only);
74                emitter.wr.write_str(&encoded_exp)?;
75                punct!(emitter, "/");
76                emitter.wr.write_str(&n.flags)?;
77            }
78            Lit::JSXText(ref n) => emit!(n),
79            #[cfg(swc_ast_unknown)]
80            _ => return Err(unknown_error()),
81        }
82
83        Ok(())
84    }
85}
86
87#[node_impl]
88impl MacroNode for Str {
89    fn emit(&mut self, emitter: &mut Macro) -> Result {
90        emitter.wr.commit_pending_semi()?;
91
92        emitter.emit_leading_comments_of_span(self.span(), false)?;
93
94        srcmap!(emitter, self, true);
95
96        if &*self.value == "use strict"
97            && self.raw.is_some()
98            && self.raw.as_ref().unwrap().contains('\\')
99            && (!emitter.cfg.inline_script || !self.raw.as_ref().unwrap().contains("script"))
100        {
101            emitter
102                .wr
103                .write_str_lit(DUMMY_SP, self.raw.as_ref().unwrap())?;
104
105            srcmap!(emitter, self, false);
106
107            return Ok(());
108        }
109
110        let target = emitter.cfg.target;
111
112        if !emitter.cfg.minify {
113            if let Some(raw) = &self.raw {
114                let es5_safe = match emitter.cfg.target {
115                    EsVersion::Es3 | EsVersion::Es5 => {
116                        // Block raw strings containing ES6+ Unicode escapes (\u{...}) for ES3/ES5
117                        // targets
118                        !raw.contains("\\u{")
119                    }
120                    _ => true,
121                };
122
123                if es5_safe
124                    && (!emitter.cfg.ascii_only || raw.is_ascii())
125                    && (!emitter.cfg.inline_script
126                        || !self.raw.as_ref().unwrap().contains("script"))
127                {
128                    emitter.wr.write_str_lit(DUMMY_SP, raw)?;
129                    return Ok(());
130                }
131            }
132        }
133
134        let (quote_char, value) = get_quoted_utf16(&self.value, emitter.cfg.ascii_only, target);
135        let value = if emitter.cfg.inline_script {
136            escape_inline_script(&value)
137        } else {
138            CowStr::Borrowed(&value)
139        };
140
141        let quote_str = [quote_char.as_byte()];
142        let quote_str = unsafe {
143            // Safety: quote_char is valid ascii
144            str::from_utf8_unchecked(&quote_str)
145        };
146
147        emitter.wr.write_str(quote_str)?;
148        emitter.wr.write_str_lit(DUMMY_SP, &value)?;
149        emitter.wr.write_str(quote_str)?;
150
151        // srcmap!(emitter,self, false);
152
153        Ok(())
154    }
155}
156
157#[node_impl]
158impl MacroNode for Number {
159    fn emit(&mut self, emitter: &mut Macro) -> Result {
160        emitter.emit_num_lit_internal(self, false)?;
161
162        Ok(())
163    }
164}
165
166#[node_impl]
167impl MacroNode for BigInt {
168    fn emit(&mut self, emitter: &mut Macro) -> Result {
169        emitter.emit_leading_comments_of_span(self.span, false)?;
170
171        if emitter.cfg.minify {
172            let value = if *self.value >= 10000000000000000_i64.into() {
173                format!("0x{}", self.value.to_str_radix(16))
174            } else if *self.value <= (-10000000000000000_i64).into() {
175                format!("-0x{}", (-*self.value.clone()).to_str_radix(16))
176            } else {
177                self.value.to_string()
178            };
179            emitter.wr.write_lit(self.span, &value)?;
180            emitter.wr.write_lit(self.span, "n")?;
181        } else {
182            match &self.raw {
183                Some(raw) => {
184                    if raw.len() > 2 && emitter.cfg.target < EsVersion::Es2021 && raw.contains('_')
185                    {
186                        emitter.wr.write_str_lit(self.span, &raw.replace('_', ""))?;
187                    } else {
188                        emitter.wr.write_str_lit(self.span, raw)?;
189                    }
190                }
191                _ => {
192                    emitter.wr.write_lit(self.span, &self.value.to_string())?;
193                    emitter.wr.write_lit(self.span, "n")?;
194                }
195            }
196        }
197
198        Ok(())
199    }
200}
201
202#[node_impl]
203impl MacroNode for Bool {
204    fn emit(&mut self, emitter: &mut Macro) -> Result {
205        emitter.emit_leading_comments_of_span(self.span(), false)?;
206
207        if self.value {
208            keyword!(emitter, self.span, "true")
209        } else {
210            keyword!(emitter, self.span, "false")
211        }
212
213        Ok(())
214    }
215}
216
217/// Escape HTML-sensitive sequences after JavaScript string escaping is
218/// complete. Borrow the input when no inline-script protection is needed.
219pub(crate) fn escape_inline_script(raw: &str) -> CowStr<'_> {
220    let value = replace_close_inline_script(raw);
221    if value.contains("<!--") || value.contains("-->") {
222        CowStr::Owned(
223            value
224                .replace("<!--", "\\x3c!--")
225                .replace("-->", "--\\x3e")
226                .into(),
227        )
228    } else {
229        value
230    }
231}
232
233fn replace_close_inline_script(raw: &str) -> CowStr<'_> {
234    let chars = raw.as_bytes();
235    let pattern_len = 8; // </script>
236
237    let mut matched_indexes = chars
238        .iter()
239        .enumerate()
240        .filter(|(index, byte)| {
241            byte == &&b'<'
242                && index + pattern_len < chars.len()
243                && chars[index + 1..index + pattern_len].eq_ignore_ascii_case(b"/script")
244                && matches!(
245                    chars[index + pattern_len],
246                    b'>' | b'/' | b' ' | b'\t' | b'\n' | b'\x0C' | b'\r'
247                )
248        })
249        .map(|(index, _)| index)
250        .peekable();
251
252    if matched_indexes.peek().is_none() {
253        return CowStr::Borrowed(raw);
254    }
255
256    let mut result = CompactString::new(raw);
257
258    for (offset, i) in matched_indexes.enumerate() {
259        result.insert(i + 1 + offset, '\\');
260    }
261
262    CowStr::Owned(result)
263}
264
265impl<W, S: swc_common::SourceMapper> Emitter<'_, W, S>
266where
267    W: WriteJs,
268    S: SourceMapperExt,
269{
270    /// Emits a numeric literal in expression position.
271    ///
272    /// Readable output preserves a non-finite value's raw text when present.
273    /// Otherwise, non-finite values use division expressions, with spaces
274    /// omitted in minified output. Positive infinity uses an overflow literal
275    /// when division would require parentheses, avoiding an opening
276    /// parenthesis that could interact with automatic semicolon insertion.
277    /// Other generated expressions are parenthesized here because the fixer
278    /// cannot see code-generation-only expressions.
279    pub(crate) fn emit_num_lit_expr(
280        &mut self,
281        num: &Number,
282        precedence: ExprPrecedence,
283        detect_dot: bool,
284    ) -> std::result::Result<bool, io::Error> {
285        if num.value.is_finite() {
286            return self.emit_num_lit_internal(num, detect_dot);
287        }
288
289        if !self.cfg.minify && num.raw.is_some() {
290            return self.emit_num_lit_internal(num, detect_dot);
291        }
292
293        self.wr.commit_pending_semi()?;
294        self.emit_leading_comments_of_span(num.span(), false)?;
295
296        srcmap!(self, num, true);
297
298        let kind = NonFiniteNumber::from_value(num.value);
299        let requires_parentheses = precedence >= ExprPrecedence::MULTIPLICATIVE;
300        let (text, wrap) =
301            if requires_parentheses && matches!(kind, NonFiniteNumber::PositiveInfinity) {
302                ("2e308", false)
303            } else {
304                (kind.division_text(self.cfg.minify), requires_parentheses)
305            };
306
307        if wrap {
308            punct!(self, "(");
309        }
310        self.wr.write_str_lit(num.span, text)?;
311        if wrap {
312            punct!(self, ")");
313        }
314
315        Ok(false)
316    }
317
318    pub(crate) fn non_finite_expr_starts_with_alpha_num(
319        &self,
320        num: &Number,
321        precedence: ExprPrecedence,
322    ) -> bool {
323        debug_assert!(!num.value.is_finite());
324
325        if !self.cfg.minify {
326            if let Some(raw) = &num.raw {
327                return raw
328                    .as_bytes()
329                    .first()
330                    .is_some_and(u8::is_ascii_alphanumeric);
331            }
332        }
333
334        if precedence >= ExprPrecedence::MULTIPLICATIVE {
335            return num.value == f64::INFINITY;
336        }
337
338        !num.value.is_sign_negative() || num.value.is_nan()
339    }
340
341    /// `1.toString` is an invalid property access,
342    /// should emit a dot after the literal if return true
343    pub fn emit_num_lit_internal(
344        &mut self,
345        num: &Number,
346        mut detect_dot: bool,
347    ) -> std::result::Result<bool, io::Error> {
348        self.wr.commit_pending_semi()?;
349
350        self.emit_leading_comments_of_span(num.span(), false)?;
351
352        // Handle infinity
353        if num.value.is_infinite() && num.raw.is_none() {
354            self.wr.write_str_lit(num.span, &num.value.print())?;
355
356            return Ok(false);
357        }
358
359        let mut striped_raw = None;
360        let mut value = String::default();
361
362        srcmap!(self, num, true);
363
364        if self.cfg.minify {
365            if let Some(raw) = &num.raw {
366                if num.value.is_infinite() {
367                    self.wr.write_str_lit(DUMMY_SP, raw)?;
368                } else {
369                    value = minify_number(num.value, &mut detect_dot);
370                    self.wr.write_str_lit(DUMMY_SP, &value)?;
371                }
372            } else {
373                value = minify_number(num.value, &mut detect_dot);
374                self.wr.write_str_lit(DUMMY_SP, &value)?;
375            }
376        } else {
377            match &num.raw {
378                Some(raw) => {
379                    if raw.len() > 2 && self.cfg.target < EsVersion::Es2015 && {
380                        let slice = &raw.as_bytes()[..2];
381                        slice == b"0b" || slice == b"0o" || slice == b"0B" || slice == b"0O"
382                    } {
383                        if num.value.is_infinite() {
384                            self.wr.write_str_lit(DUMMY_SP, raw)?;
385                        } else {
386                            value = num.value.print();
387                            self.wr.write_str_lit(DUMMY_SP, &value)?;
388                        }
389                    } else if raw.len() > 2
390                        && self.cfg.target < EsVersion::Es2021
391                        && raw.contains('_')
392                    {
393                        let value = raw.replace('_', "");
394                        self.wr.write_str_lit(DUMMY_SP, &value)?;
395
396                        striped_raw = Some(value);
397                    } else {
398                        self.wr.write_str_lit(DUMMY_SP, raw)?;
399
400                        if !detect_dot {
401                            return Ok(false);
402                        }
403
404                        striped_raw = Some(raw.replace('_', ""));
405                    }
406                }
407                _ => {
408                    value = num.value.print();
409                    self.wr.write_str_lit(DUMMY_SP, &value)?;
410                }
411            }
412        }
413
414        // fast return
415        if !detect_dot {
416            return Ok(false);
417        }
418
419        Ok(striped_raw
420            .map(|raw| {
421                if raw.bytes().all(|c| c.is_ascii_digit()) {
422                    // Maybe legacy octal
423                    // Do we really need to support pre es5?
424                    let slice = raw.as_bytes();
425                    if slice.len() >= 2 && slice[0] == b'0' {
426                        return false;
427                    }
428
429                    return true;
430                }
431
432                false
433            })
434            .unwrap_or_else(|| {
435                let bytes = value.as_bytes();
436
437                if !bytes.contains(&b'.') && !bytes.contains(&b'e') {
438                    return true;
439                }
440
441                false
442            }))
443    }
444}
445
446/// Encodes non-ASCII characters in regex patterns when ascii_only is enabled.
447///
448/// This function converts non-ASCII characters to their Unicode escape
449/// sequences to ensure the regex can be safely used in ASCII-only contexts.
450///
451/// # Arguments
452/// * `pattern` - The regex pattern string to encode
453/// * `ascii_only` - Whether to encode non-ASCII characters
454///
455/// # Returns
456/// A string with non-ASCII characters encoded as Unicode escapes
457pub fn encode_regex_for_ascii(pattern: &str, ascii_only: bool) -> CowStr<'_> {
458    if !ascii_only || pattern.is_ascii() {
459        return CowStr::Borrowed(pattern);
460    }
461
462    let mut buf = CompactString::with_capacity(pattern.len());
463
464    for c in pattern.chars() {
465        match c {
466            // ASCII characters are preserved as-is
467            '\x00'..='\x7e' => buf.push(c),
468            // Characters in the \x7f to \xff range use \xHH format
469            '\u{7f}'..='\u{ff}' => {
470                buf.push_str("\\x");
471                write!(&mut buf, "{:02x}", c as u8).unwrap();
472            }
473            // Line/paragraph separators need escaping in all contexts
474            '\u{2028}' => buf.push_str("\\u2028"),
475            '\u{2029}' => buf.push_str("\\u2029"),
476            // Characters above \xff use \uHHHH format
477            _ => {
478                if c > '\u{FFFF}' {
479                    // Characters beyond BMP are encoded as surrogate pairs for compatibility
480                    let code_point = c as u32;
481                    let h = ((code_point - 0x10000) / 0x400) + 0xd800;
482                    let l = ((code_point - 0x10000) % 0x400) + 0xdc00;
483                    write!(&mut buf, "\\u{h:04x}\\u{l:04x}").unwrap();
484                } else {
485                    write!(&mut buf, "\\u{:04x}", c as u16).unwrap();
486                }
487            }
488        }
489    }
490
491    CowStr::Owned(buf)
492}
493
494macro_rules! cp {
495    ($c:expr) => {
496        unsafe { CodePoint::from_u32_unchecked($c as u32) }
497    };
498}
499
500const DOUBLE_QUOTE: CodePoint = cp!('"');
501const SINGLE_QUOTE: CodePoint = cp!('\'');
502const NULL_CHAR: CodePoint = cp!('\x00');
503const BACKSPACE: CodePoint = cp!('\u{0008}');
504const FORM_FEED: CodePoint = cp!('\u{000c}');
505const LINE_FEED: CodePoint = cp!('\n');
506const CARRIAGE_RETURN: CodePoint = cp!('\r');
507const VERTICAL_TAB: CodePoint = cp!('\u{000b}');
508const TAB: CodePoint = cp!('\t');
509const BACKSLASH: CodePoint = cp!('\\');
510const CTRL_START_1: CodePoint = cp!('\x01');
511const CTRL_END_1: CodePoint = cp!('\x0f');
512const CTRL_START_2: CodePoint = cp!('\x10');
513const CTRL_END_2: CodePoint = cp!('\x1f');
514const PRINTABLE_START: CodePoint = cp!('\x20');
515const PRINTABLE_END: CodePoint = cp!('\x7e');
516const DEL_START: CodePoint = cp!('\u{7f}');
517const DEL_END: CodePoint = cp!('\u{ff}');
518const LINE_SEPARATOR: CodePoint = cp!('\u{2028}');
519const PARAGRAPH_SEPARATOR: CodePoint = cp!('\u{2029}');
520const ZERO_WIDTH_NO_BREAK_SPACE: CodePoint = cp!('\u{FEFF}');
521
522const SURROGATE_START: CodePoint = cp!(0xd800);
523const SURROGATE_END: CodePoint = cp!(0xdfff);
524
525/// Returns `(quote_char, value)`
526pub fn get_quoted_utf16(v: &Wtf8, ascii_only: bool, target: EsVersion) -> (AsciiChar, CowStr<'_>) {
527    // Fast path: If the string is ASCII and doesn't need escaping, we can avoid
528    // allocation
529    if v.is_ascii() {
530        let mut needs_escaping = false;
531        let mut single_quote_count = 0;
532        let mut double_quote_count = 0;
533
534        for &b in v.as_bytes() {
535            match b {
536                b'\'' => single_quote_count += 1,
537                b'"' => double_quote_count += 1,
538                // Control characters and backslash need escaping
539                0..=0x1f | b'\\' => {
540                    needs_escaping = true;
541                    break;
542                }
543                _ => {}
544            }
545        }
546
547        if !needs_escaping {
548            let quote_char = if double_quote_count > single_quote_count {
549                AsciiChar::Apostrophe
550            } else {
551                AsciiChar::Quotation
552            };
553
554            // If there are no quotes to escape, we can return the original string
555            if (quote_char == AsciiChar::Apostrophe && single_quote_count == 0)
556                || (quote_char == AsciiChar::Quotation && double_quote_count == 0)
557            {
558                return (
559                    quote_char,
560                    // SAFETY: We have checked that the string is ASCII. So it does not contain any
561                    // unpaired surrogate.
562                    CowStr::Borrowed(v.as_str().unwrap()),
563                );
564            }
565        }
566    }
567
568    // Slow path: Original implementation for strings that need processing
569    // Count quotes first to determine which quote character to use
570    let (mut single_quote_count, mut double_quote_count) = (0, 0);
571    for c in v.code_points() {
572        match c {
573            SINGLE_QUOTE => single_quote_count += 1,
574            DOUBLE_QUOTE => double_quote_count += 1,
575            _ => {}
576        }
577    }
578
579    // Pre-calculate capacity to avoid reallocations
580    let quote_char = if double_quote_count > single_quote_count {
581        AsciiChar::Apostrophe
582    } else {
583        AsciiChar::Quotation
584    };
585    let escape_char = if quote_char == AsciiChar::Apostrophe {
586        AsciiChar::Apostrophe
587    } else {
588        AsciiChar::Quotation
589    };
590    let escape_count = if quote_char == AsciiChar::Apostrophe {
591        single_quote_count
592    } else {
593        double_quote_count
594    };
595
596    // Add 1 for each escaped quote
597    let capacity = v.len() + escape_count;
598    let mut buf = CompactString::with_capacity(capacity);
599
600    let mut iter = v.code_points().peekable();
601    while let Some(c) = iter.next() {
602        match c {
603            NULL_CHAR => {
604                if target < EsVersion::Es5
605                    || matches!(iter.peek(), Some(x) if *x >= cp!('0') && *x <= cp!('9'))
606                {
607                    buf.push_str("\\x00");
608                } else {
609                    buf.push_str("\\0");
610                }
611            }
612            BACKSPACE => buf.push_str("\\b"),
613            FORM_FEED => buf.push_str("\\f"),
614            LINE_FEED => buf.push_str("\\n"),
615            CARRIAGE_RETURN => buf.push_str("\\r"),
616            VERTICAL_TAB => buf.push_str("\\v"),
617            TAB => buf.push('\t'),
618            BACKSLASH => buf.push_str("\\\\"),
619            c if matches!(c.to_char(), Some(c) if c == escape_char) => {
620                buf.push('\\');
621                // SAFETY: `escape_char` is a valid ASCII character.
622                buf.push(c.to_char().unwrap());
623            }
624            c if c >= CTRL_START_1 && c <= CTRL_END_1 => {
625                buf.push_str("\\x0");
626                write!(&mut buf, "{:x}", c.to_u32() as u8).unwrap();
627            }
628            c if c >= CTRL_START_2 && c <= CTRL_END_2 => {
629                buf.push_str("\\x");
630                write!(&mut buf, "{:x}", c.to_u32() as u8).unwrap();
631            }
632            c if c >= PRINTABLE_START && c <= PRINTABLE_END => {
633                // SAFETY: c is a valid ASCII character.
634                buf.push(c.to_char().unwrap())
635            }
636            c if c >= DEL_START && c <= DEL_END => {
637                if ascii_only || target <= EsVersion::Es5 {
638                    buf.push_str("\\x");
639                    write!(&mut buf, "{:x}", c.to_u32() as u8).unwrap();
640                } else {
641                    // SAFETY: c is a valid Rust char.
642                    buf.push(c.to_char().unwrap());
643                }
644            }
645            LINE_SEPARATOR => buf.push_str("\\u2028"),
646            PARAGRAPH_SEPARATOR => buf.push_str("\\u2029"),
647            ZERO_WIDTH_NO_BREAK_SPACE => buf.push_str("\\uFEFF"),
648            c => {
649                if c.is_ascii() {
650                    // SAFETY: c is a valid ASCII character.
651                    buf.push(c.to_char().unwrap());
652                } else if c > cp!('\u{FFFF}') {
653                    if target <= EsVersion::Es5 {
654                        let h = ((c.to_u32() - 0x10000) / 0x400) + 0xd800;
655                        let l = (c.to_u32() - 0x10000) % 0x400 + 0xdc00;
656                        write!(&mut buf, "\\u{h:04X}\\u{l:04X}").unwrap();
657                    } else if ascii_only {
658                        write!(&mut buf, "\\u{{{:04X}}}", c.to_u32()).unwrap();
659                    } else {
660                        // SAFETY: c is a valid Rust char. (> U+FFFF && <= U+10FFFF)
661                        // The latter condition is guaranteed by CodePoint.
662                        buf.push(c.to_char().unwrap());
663                    }
664                } else if c >= SURROGATE_START && c <= SURROGATE_END {
665                    // Unparied Surrogate
666                    // Escape as \uXXXX
667                    write!(&mut buf, "\\u{:04X}", c.to_u32()).unwrap();
668                } else if ascii_only {
669                    write!(&mut buf, "\\u{:04X}", c.to_u32() as u16).unwrap();
670                } else {
671                    // SAFETY: c is a valid Rust char. (>= U+0080 && <= U+FFFF, excluding
672                    // surrogates)
673                    buf.push(c.to_char().unwrap());
674                }
675            }
676        }
677    }
678
679    (quote_char, CowStr::Owned(buf))
680}
681
682pub trait Print {
683    fn print(&self) -> String;
684}
685
686impl Print for f64 {
687    fn print(&self) -> String {
688        // preserve -0.0
689        if *self == 0.0 {
690            return self.to_string();
691        }
692
693        let mut buffer = dragonbox_ecma::Buffer::new();
694        buffer.format(*self).to_string()
695    }
696}