Skip to main content

oxc_codegen/
lib.rs

1//! Oxc Codegen
2//!
3//! Code adapted from
4//! * [esbuild](https://github.com/evanw/esbuild/blob/v0.24.0/internal/js_printer/js_printer.go)
5
6use std::{borrow::Cow, cmp, slice};
7
8use cow_utils::CowUtils;
9
10use oxc_ast::ast::*;
11use oxc_data_structures::{code_buffer::CodeBuffer, stack::Stack};
12use oxc_index::IndexVec;
13use oxc_semantic::Scoping;
14use oxc_span::{CompactStr, GetSpan, Span};
15use oxc_syntax::{
16    class::ClassId,
17    identifier::{is_identifier_part, is_identifier_part_ascii},
18    operator::{BinaryOperator, UnaryOperator, UpdateOperator},
19    precedence::Precedence,
20};
21use rustc_hash::FxHashMap;
22
23mod binary_expr_visitor;
24mod comment;
25mod context;
26mod r#gen;
27mod operator;
28mod options;
29#[cfg(feature = "sourcemap")]
30mod sourcemap_builder;
31mod str;
32
33use binary_expr_visitor::BinaryExpressionVisitor;
34use comment::CommentsMap;
35use operator::Operator;
36#[cfg(feature = "sourcemap")]
37use sourcemap_builder::SourcemapBuilder;
38use str::{Quote, cold_branch, is_script_close_tag};
39
40pub use context::Context;
41pub use r#gen::{Gen, GenExpr};
42pub use options::{CodegenOptions, CommentOptions, LegalComment};
43
44// Re-export `IndentChar` from `oxc_data_structures`
45pub use oxc_data_structures::code_buffer::IndentChar;
46
47/// Output from [`Codegen::build`]
48#[non_exhaustive]
49pub struct CodegenReturn {
50    /// The generated source code.
51    pub code: String,
52
53    /// The source map from the input source code to the generated source code.
54    ///
55    /// You must set [`CodegenOptions::source_map_path`] for this to be [`Some`].
56    #[cfg(feature = "sourcemap")]
57    pub map: Option<oxc_sourcemap::SourceMap>,
58
59    /// All the legal comments returned from [LegalComment::Linked] or [LegalComment::External].
60    pub legal_comments: Vec<Comment>,
61}
62
63/// A code generator for printing JavaScript and TypeScript code.
64///
65/// ## Example
66/// ```rust
67/// use oxc_codegen::{Codegen, CodegenOptions};
68/// use oxc_ast::ast::Program;
69/// use oxc_parser::Parser;
70/// use oxc_allocator::Allocator;
71/// use oxc_span::SourceType;
72///
73/// let allocator = Allocator::default();
74/// let source = "const a = 1 + 2;";
75/// let parsed = Parser::new(&allocator, source, SourceType::mjs()).parse();
76/// assert!(parsed.errors.is_empty());
77///
78/// let js = Codegen::new().build(&parsed.program);
79/// assert_eq!(js.code, "const a = 1 + 2;\n");
80/// ```
81pub struct Codegen<'a> {
82    pub(crate) options: CodegenOptions,
83
84    /// Original source code of the AST
85    source_text: Option<&'a str>,
86
87    scoping: Option<Scoping>,
88
89    /// Private member name mappings for mangling
90    private_member_mappings: Option<IndexVec<ClassId, FxHashMap<String, CompactStr>>>,
91
92    /// Output Code
93    code: CodeBuffer,
94
95    // states
96    prev_op_end: usize,
97    prev_reg_exp_end: usize,
98    need_space_before_dot: usize,
99    print_next_indent_as_space: bool,
100    binary_expr_stack: Stack<BinaryExpressionVisitor<'a>>,
101    class_stack: Stack<ClassId>,
102    next_class_id: ClassId,
103    /// Indicates the output is JSX type, it is set in [`Program::gen`] and the result
104    /// is obtained by [`oxc_span::SourceType::is_jsx`]
105    is_jsx: bool,
106
107    /// For avoiding `;` if the previous statement ends with `}`.
108    needs_semicolon: bool,
109
110    prev_op: Option<Operator>,
111
112    start_of_stmt: usize,
113    start_of_arrow_expr: usize,
114    start_of_default_export: usize,
115
116    /// Track the current indentation level
117    indent: u32,
118
119    /// Fast path for [CodegenOptions::single_quote]
120    quote: Quote,
121
122    // Builders
123    comments: CommentsMap,
124
125    #[cfg(feature = "sourcemap")]
126    sourcemap_builder: Option<SourcemapBuilder<'a>>,
127}
128
129impl Default for Codegen<'_> {
130    fn default() -> Self {
131        Self::new()
132    }
133}
134
135impl<'a> From<Codegen<'a>> for String {
136    fn from(val: Codegen<'a>) -> Self {
137        val.into_source_text()
138    }
139}
140
141impl<'a> From<Codegen<'a>> for Cow<'a, str> {
142    fn from(val: Codegen<'a>) -> Self {
143        Cow::Owned(val.into_source_text())
144    }
145}
146
147// Public APIs
148impl<'a> Codegen<'a> {
149    /// Create a new code generator.
150    ///
151    /// This is equivalent to [`Codegen::default`].
152    #[must_use]
153    pub fn new() -> Self {
154        let options = CodegenOptions::default();
155        Self {
156            options,
157            source_text: None,
158            scoping: None,
159            private_member_mappings: None,
160            code: CodeBuffer::default(),
161            needs_semicolon: false,
162            need_space_before_dot: 0,
163            print_next_indent_as_space: false,
164            binary_expr_stack: Stack::with_capacity(12),
165            class_stack: Stack::with_capacity(4),
166            next_class_id: ClassId::from_usize(0),
167            prev_op_end: 0,
168            prev_reg_exp_end: 0,
169            prev_op: None,
170            start_of_stmt: 0,
171            start_of_arrow_expr: 0,
172            start_of_default_export: 0,
173            is_jsx: false,
174            indent: 0,
175            quote: Quote::Double,
176            comments: CommentsMap::default(),
177            #[cfg(feature = "sourcemap")]
178            sourcemap_builder: None,
179        }
180    }
181
182    /// Pass options to the code generator.
183    #[must_use]
184    pub fn with_options(mut self, options: CodegenOptions) -> Self {
185        self.quote = if options.single_quote { Quote::Single } else { Quote::Double };
186        self.code = CodeBuffer::with_indent(options.indent_char, options.indent_width);
187        self.options = options;
188        self
189    }
190
191    /// Sets the source text for the code generator.
192    #[must_use]
193    pub fn with_source_text(mut self, source_text: &'a str) -> Self {
194        self.source_text = Some(source_text);
195        self
196    }
197
198    /// Set the symbol table used for identifier renaming.
199    ///
200    /// Can be used for easy renaming of variables (based on semantic analysis).
201    #[must_use]
202    pub fn with_scoping(mut self, scoping: Option<Scoping>) -> Self {
203        self.scoping = scoping;
204        self
205    }
206
207    /// Set private member name mappings for mangling.
208    ///
209    /// This allows renaming of private class members like `#field` -> `#a`.
210    /// The Vec contains per-class mappings, indexed by class declaration order.
211    #[must_use]
212    pub fn with_private_member_mappings(
213        mut self,
214        mappings: Option<IndexVec<ClassId, FxHashMap<String, CompactStr>>>,
215    ) -> Self {
216        self.private_member_mappings = mappings;
217        self
218    }
219
220    /// Print a [`Program`] into a string of source code.
221    ///
222    /// A source map will be generated if [`CodegenOptions::source_map_path`] is set.
223    #[must_use]
224    pub fn build(mut self, program: &Program<'a>) -> CodegenReturn {
225        self.quote = if self.options.single_quote { Quote::Single } else { Quote::Double };
226        self.source_text = Some(program.source_text);
227        self.indent = self.options.initial_indent;
228        self.code.reserve(program.source_text.len());
229        self.build_comments(&program.comments);
230        #[cfg(feature = "sourcemap")]
231        if let Some(path) = &self.options.source_map_path {
232            self.sourcemap_builder = Some(SourcemapBuilder::new(path, program.source_text));
233        }
234        program.print(&mut self, Context::default());
235        let legal_comments = self.handle_eof_linked_or_external_comments(program);
236        let code = self.code.into_string();
237        #[cfg(feature = "sourcemap")]
238        let map = self.sourcemap_builder.map(SourcemapBuilder::into_sourcemap);
239        CodegenReturn {
240            code,
241            #[cfg(feature = "sourcemap")]
242            map,
243            legal_comments,
244        }
245    }
246
247    /// Turn what's been built so far into a string. Like [`build`],
248    /// this fininishes a print and returns the generated source code. Unlike
249    /// [`build`], no source map is generated.
250    ///
251    /// This is more useful for cases that progressively build code using [`print_expression`].
252    ///
253    /// [`build`]: Codegen::build
254    /// [`print_expression`]: Codegen::print_expression
255    #[must_use]
256    pub fn into_source_text(self) -> String {
257        self.code.into_string()
258    }
259
260    /// Push a single ASCII byte into the buffer.
261    ///
262    /// # Panics
263    /// Panics if `byte` is not an ASCII byte (`0 - 0x7F`).
264    #[inline]
265    pub fn print_ascii_byte(&mut self, byte: u8) {
266        self.code.print_ascii_byte(byte);
267    }
268
269    /// Push str into the buffer
270    #[inline]
271    pub fn print_str(&mut self, s: &str) {
272        self.code.print_str(s);
273    }
274
275    /// Push str into the buffer, escaping `</script` to `<\/script`.
276    #[inline]
277    pub fn print_str_escaping_script_close_tag(&mut self, s: &str) {
278        // `</script` will be very rare. So we try to make the search as quick as possible by:
279        // 1. Searching for `<` first, and only checking if followed by `/script` once `<` is found.
280        // 2. Searching longer strings for `<` in chunks of 16 bytes using SIMD, and only doing the
281        //    more expensive byte-by-byte search once a `<` is found.
282
283        let bytes = s.as_bytes();
284        let mut consumed = 0;
285
286        // Search range of bytes for `</script`, byte by byte.
287        //
288        // Bytes between `ptr` and `last_ptr` (inclusive) are searched for `<`.
289        // If `<` is found, the following 7 bytes are checked to see if they're `/script`.
290        //
291        // Requirements for the closure below:
292        // * `ptr` and `last_ptr` must be within bounds of `bytes`.
293        // * `last_ptr` must be greater or equal to `ptr`.
294        // * `last_ptr` must be no later than 8 bytes before end of string.
295        //   i.e. safe to read 8 bytes at `end_ptr`.
296        let mut search_bytes = |mut ptr: *const u8, last_ptr| {
297            loop {
298                // SAFETY: `ptr` is always less than or equal to `last_ptr`.
299                // `last_ptr` is within bounds of `bytes`, so safe to read a byte at `ptr`.
300                let byte = unsafe { *ptr.as_ref().unwrap_unchecked() };
301                if byte == b'<' {
302                    // SAFETY: `ptr <= last_ptr`, and `last_ptr` points to no later than
303                    // 8 bytes before end of string, so safe to read 8 bytes from `ptr`
304                    let slice = unsafe { slice::from_raw_parts(ptr, 8) };
305                    if is_script_close_tag(slice) {
306                        // Push str up to and including `<`. Skip `/`. Write `\/` instead.
307                        // SAFETY:
308                        // `consumed` is initially 0, and only updated below to be after `/`,
309                        // so in bounds, and on a UTF-8 char boundary.
310                        // `index` is on `<`, so `index + 1` is in bounds and a UTF-8 char boundary.
311                        // `consumed` is always less than `index + 1` as it's set on a previous round.
312                        unsafe {
313                            let index = ptr.offset_from_unsigned(bytes.as_ptr());
314                            let before = bytes.get_unchecked(consumed..=index);
315                            self.code.print_bytes_unchecked(before);
316
317                            // Set `consumed` to after `/`
318                            consumed = index + 2;
319                        }
320                        self.print_str("\\/");
321                        // Note: We could advance `ptr` by 8 bytes here to skip over `</script`,
322                        // but this branch will be very rarely taken, so it's better to keep it simple
323                    }
324                }
325
326                if ptr == last_ptr {
327                    break;
328                }
329                // SAFETY: `ptr` is less than `last_ptr`, which is in bounds, so safe to increment `ptr`
330                ptr = unsafe { ptr.add(1) };
331            }
332        };
333
334        // Search string in chunks of 16 bytes
335        let mut chunks = bytes.chunks_exact(16);
336        for (chunk_index, chunk) in chunks.by_ref().enumerate() {
337            #[expect(clippy::missing_panics_doc, reason = "infallible")]
338            let chunk: &[u8; 16] = chunk.try_into().unwrap();
339
340            // Compiler vectorizes this loop to a few SIMD ops
341            let mut contains_lt = false;
342            for &byte in chunk {
343                if byte == b'<' {
344                    contains_lt = true;
345                }
346            }
347
348            if contains_lt {
349                // Chunk contains at least one `<`.
350                // Find them, and check if they're the start of `</script`.
351                //
352                // SAFETY: `index` is byte index of start of chunk.
353                // We search bytes starting with first byte of chunk, and ending with last byte of chunk.
354                // i.e. `index` to `index + 15` (inclusive).
355                // If this chunk is towards the end of the string, reduce the range of bytes searched
356                // so the last byte searched has at least 7 further bytes after it.
357                // i.e. safe to read 8 bytes at `last_ptr`.
358                cold_branch(|| unsafe {
359                    let index = chunk_index * 16;
360                    let remaining_bytes = bytes.len() - index;
361                    let last_offset = cmp::min(remaining_bytes - 8, 15);
362                    let ptr = bytes.as_ptr().add(index);
363                    let last_ptr = ptr.add(last_offset);
364                    search_bytes(ptr, last_ptr);
365                });
366            }
367        }
368
369        // Search last chunk byte-by-byte.
370        // Skip this if less than 8 bytes remaining, because less than 8 bytes can't contain `</script`.
371        let last_chunk = chunks.remainder();
372        if last_chunk.len() >= 8 {
373            let ptr = last_chunk.as_ptr();
374            // SAFETY: `last_chunk.len() >= 8`, so `- 8` cannot wrap.
375            // `last_chunk.as_ptr().add(last_chunk.len() - 8)` is in bounds of `last_chunk`.
376            let last_ptr = unsafe { ptr.add(last_chunk.len() - 8) };
377            search_bytes(ptr, last_ptr);
378        }
379
380        // SAFETY: `consumed` is either 0, or after `/`, so on a UTF-8 char boundary, and in bounds
381        unsafe {
382            let remaining = bytes.get_unchecked(consumed..);
383            self.code.print_bytes_unchecked(remaining);
384        }
385    }
386
387    /// Print a single [`Expression`], adding it to the code generator's
388    /// internal buffer. Unlike [`Codegen::build`], this does not consume `self`.
389    #[inline]
390    pub fn print_expression(&mut self, expr: &Expression<'_>) {
391        expr.print_expr(self, Precedence::Lowest, Context::empty());
392    }
393}
394
395// Private APIs
396impl<'a> Codegen<'a> {
397    fn code(&self) -> &CodeBuffer {
398        &self.code
399    }
400
401    fn code_len(&self) -> usize {
402        self.code().len()
403    }
404
405    #[inline]
406    fn print_soft_space(&mut self) {
407        if !self.options.minify {
408            self.print_ascii_byte(b' ');
409        }
410    }
411
412    #[inline]
413    fn print_hard_space(&mut self) {
414        self.print_ascii_byte(b' ');
415    }
416
417    #[inline]
418    fn print_soft_newline(&mut self) {
419        if !self.options.minify {
420            self.print_ascii_byte(b'\n');
421        }
422    }
423
424    #[inline]
425    fn print_hard_newline(&mut self) {
426        self.print_ascii_byte(b'\n');
427    }
428
429    #[inline]
430    fn print_semicolon(&mut self) {
431        self.print_ascii_byte(b';');
432    }
433
434    #[inline]
435    fn print_comma(&mut self) {
436        self.print_ascii_byte(b',');
437    }
438
439    #[inline]
440    fn print_space_before_identifier(&mut self) {
441        let Some(byte) = self.last_byte() else { return };
442
443        if self.prev_reg_exp_end != self.code.len() {
444            let is_identifier = if byte.is_ascii() {
445                // Fast path for ASCII (very common case)
446                is_identifier_part_ascii(byte as char)
447            } else {
448                is_identifier_part(self.last_char().unwrap())
449            };
450            if !is_identifier {
451                return;
452            }
453        }
454
455        self.print_hard_space();
456    }
457
458    #[inline]
459    fn last_byte(&self) -> Option<u8> {
460        self.code.last_byte()
461    }
462
463    #[inline]
464    fn last_char(&self) -> Option<char> {
465        self.code.last_char()
466    }
467
468    #[inline]
469    fn indent(&mut self) {
470        if !self.options.minify {
471            self.indent += 1;
472        }
473    }
474
475    #[inline]
476    fn dedent(&mut self) {
477        if !self.options.minify {
478            self.indent -= 1;
479        }
480    }
481
482    #[inline]
483    fn enter_class(&mut self) {
484        let class_id = self.next_class_id;
485        self.next_class_id = ClassId::from_usize(self.next_class_id.index() + 1);
486        self.class_stack.push(class_id);
487    }
488
489    #[inline]
490    fn exit_class(&mut self) {
491        self.class_stack.pop();
492    }
493
494    #[inline]
495    fn current_class_ids(&self) -> impl Iterator<Item = ClassId> {
496        self.class_stack.iter().rev().copied()
497    }
498
499    #[inline]
500    fn wrap<F: FnMut(&mut Self)>(&mut self, wrap: bool, mut f: F) {
501        if wrap {
502            self.print_ascii_byte(b'(');
503        }
504        f(self);
505        if wrap {
506            self.print_ascii_byte(b')');
507        }
508    }
509
510    #[inline]
511    fn print_indent(&mut self) {
512        if self.options.minify {
513            return;
514        }
515        if self.print_next_indent_as_space {
516            self.print_hard_space();
517            self.print_next_indent_as_space = false;
518            return;
519        }
520        self.code.print_indent(self.indent as usize);
521    }
522
523    #[inline]
524    fn print_semicolon_after_statement(&mut self) {
525        if self.options.minify {
526            self.needs_semicolon = true;
527        } else {
528            self.print_str(";\n");
529        }
530    }
531
532    #[inline]
533    fn print_semicolon_if_needed(&mut self) {
534        if self.needs_semicolon {
535            self.print_semicolon();
536            self.needs_semicolon = false;
537        }
538    }
539
540    #[inline]
541    fn print_ellipsis(&mut self) {
542        self.print_str("...");
543    }
544
545    #[inline]
546    fn print_colon(&mut self) {
547        self.print_ascii_byte(b':');
548    }
549
550    #[inline]
551    fn print_equal(&mut self) {
552        self.print_ascii_byte(b'=');
553    }
554
555    fn print_curly_braces<F: FnOnce(&mut Self)>(&mut self, span: Span, single_line: bool, op: F) {
556        self.add_source_mapping(span);
557        self.print_ascii_byte(b'{');
558        if !single_line {
559            self.print_soft_newline();
560            self.indent();
561        }
562        op(self);
563        if !single_line {
564            self.dedent();
565            self.print_indent();
566        }
567        self.print_ascii_byte(b'}');
568    }
569
570    fn print_block_start(&mut self, span: Span) {
571        self.add_source_mapping(span);
572        self.print_ascii_byte(b'{');
573        self.print_soft_newline();
574        self.indent();
575    }
576
577    fn print_block_end(&mut self, _span: Span) {
578        self.dedent();
579        self.print_indent();
580        self.print_ascii_byte(b'}');
581    }
582
583    fn print_body(&mut self, stmt: &Statement<'_>, need_space: bool, ctx: Context) {
584        match stmt {
585            Statement::BlockStatement(stmt) => {
586                self.print_soft_space();
587                self.print_block_statement(stmt, ctx);
588                self.print_soft_newline();
589            }
590            Statement::EmptyStatement(_) => {
591                self.print_semicolon();
592                self.print_soft_newline();
593            }
594            stmt => {
595                if need_space && self.options.minify {
596                    self.print_hard_space();
597                }
598                self.print_next_indent_as_space = true;
599                stmt.print(self, ctx);
600            }
601        }
602    }
603
604    fn print_block_statement(&mut self, stmt: &BlockStatement<'_>, ctx: Context) {
605        self.print_curly_braces(stmt.span, stmt.body.is_empty(), |p| {
606            for stmt in &stmt.body {
607                p.print_semicolon_if_needed();
608                stmt.print(p, ctx);
609            }
610        });
611        self.needs_semicolon = false;
612    }
613
614    fn print_directives_and_statements(
615        &mut self,
616        directives: &[Directive<'_>],
617        stmts: &[Statement<'_>],
618        ctx: Context,
619    ) {
620        for directive in directives {
621            directive.print(self, ctx);
622        }
623        let Some((first, rest)) = stmts.split_first() else {
624            return;
625        };
626
627        // Ensure first string literal is not a directive.
628        let mut first_needs_parens = false;
629        if directives.is_empty()
630            && !self.options.minify
631            && let Statement::ExpressionStatement(s) = first
632        {
633            let s = s.expression.without_parentheses();
634            if matches!(s, Expression::StringLiteral(_)) {
635                first_needs_parens = true;
636                self.print_ascii_byte(b'(');
637                s.print_expr(self, Precedence::Lowest, ctx);
638                self.print_ascii_byte(b')');
639                self.print_semicolon_after_statement();
640            }
641        }
642
643        if !first_needs_parens {
644            first.print(self, ctx);
645        }
646
647        for stmt in rest {
648            self.print_semicolon_if_needed();
649            stmt.print(self, ctx);
650        }
651    }
652
653    #[inline]
654    fn print_list<T: Gen>(&mut self, items: &[T], ctx: Context) {
655        let Some((first, rest)) = items.split_first() else {
656            return;
657        };
658        first.print(self, ctx);
659        for item in rest {
660            self.print_comma();
661            self.print_soft_space();
662            item.print(self, ctx);
663        }
664    }
665
666    #[inline]
667    fn print_expressions<T: GenExpr>(&mut self, items: &[T], precedence: Precedence, ctx: Context) {
668        let Some((first, rest)) = items.split_first() else {
669            return;
670        };
671        first.print_expr(self, precedence, ctx);
672        for item in rest {
673            self.print_comma();
674            self.print_soft_space();
675            item.print_expr(self, precedence, ctx);
676        }
677    }
678
679    fn print_arguments(&mut self, span: Span, arguments: &[Argument<'_>], ctx: Context) {
680        self.print_ascii_byte(b'(');
681
682        let has_comment_before_right_paren = span.end > 0 && self.has_comment(span.end - 1);
683
684        let has_comment = has_comment_before_right_paren
685            || arguments.iter().any(|item| self.has_comment(item.span().start));
686
687        if has_comment {
688            self.indent();
689            self.print_list_with_comments(arguments, ctx);
690            // Handle `/* comment */);`
691            if !has_comment_before_right_paren
692                || (span.end > 0 && !self.print_expr_comments(span.end - 1))
693            {
694                self.print_soft_newline();
695            }
696            self.dedent();
697            self.print_indent();
698        } else {
699            self.print_list(arguments, ctx);
700        }
701        self.print_ascii_byte(b')');
702        self.add_source_mapping_end(span);
703    }
704
705    fn print_list_with_comments(&mut self, items: &[Argument<'_>], ctx: Context) {
706        let Some((first, rest)) = items.split_first() else {
707            return;
708        };
709        if self.print_expr_comments(first.span().start) {
710            self.print_indent();
711        } else {
712            self.print_soft_newline();
713            self.print_indent();
714        }
715        first.print(self, ctx);
716        for item in rest {
717            self.print_comma();
718            if self.print_expr_comments(item.span().start) {
719                self.print_indent();
720            } else {
721                self.print_soft_newline();
722                self.print_indent();
723            }
724            item.print(self, ctx);
725        }
726    }
727
728    fn get_identifier_reference_name(&self, reference: &IdentifierReference<'a>) -> &'a str {
729        if let Some(scoping) = &self.scoping
730            && let Some(reference_id) = reference.reference_id.get()
731            && let Some(name) = scoping.get_reference_name(reference_id)
732        {
733            // SAFETY: Hack the lifetime to be part of the allocator.
734            return unsafe { std::mem::transmute_copy(&name) };
735        }
736        reference.name.as_str()
737    }
738
739    fn get_binding_identifier_name(&self, ident: &BindingIdentifier<'a>) -> &'a str {
740        if let Some(scoping) = &self.scoping
741            && let Some(symbol_id) = ident.symbol_id.get()
742        {
743            let name = scoping.symbol_name(symbol_id);
744            // SAFETY: Hack the lifetime to be part of the allocator.
745            return unsafe { std::mem::transmute_copy(&name) };
746        }
747        ident.name.as_str()
748    }
749
750    fn print_space_before_operator(&mut self, next: Operator) {
751        if self.prev_op_end != self.code.len() {
752            return;
753        }
754        let Some(prev) = self.prev_op else { return };
755        // "+ + y" => "+ +y"
756        // "+ ++ y" => "+ ++y"
757        // "x + + y" => "x+ +y"
758        // "x ++ + y" => "x+++y"
759        // "x + ++ y" => "x+ ++y"
760        // "-- >" => "-- >"
761        // "< ! --" => "<! --"
762        let bin_op_add = Operator::Binary(BinaryOperator::Addition);
763        let bin_op_sub = Operator::Binary(BinaryOperator::Subtraction);
764        let un_op_pos = Operator::Unary(UnaryOperator::UnaryPlus);
765        let un_op_pre_inc = Operator::Update(UpdateOperator::Increment);
766        let un_op_neg = Operator::Unary(UnaryOperator::UnaryNegation);
767        let un_op_pre_dec = Operator::Update(UpdateOperator::Decrement);
768        let un_op_post_dec = Operator::Update(UpdateOperator::Decrement);
769        let bin_op_gt = Operator::Binary(BinaryOperator::GreaterThan);
770        let un_op_not = Operator::Unary(UnaryOperator::LogicalNot);
771        if ((prev == bin_op_add || prev == un_op_pos)
772            && (next == bin_op_add || next == un_op_pos || next == un_op_pre_inc))
773            || ((prev == bin_op_sub || prev == un_op_neg)
774                && (next == bin_op_sub || next == un_op_neg || next == un_op_pre_dec))
775            || (prev == un_op_post_dec && next == bin_op_gt)
776            || (prev == un_op_not
777                && next == un_op_pre_dec
778                // `prev == UnaryOperator::LogicalNot` which means last byte is ASCII,
779                // and therefore previous character is 1 byte from end of buffer
780                && self.code.peek_nth_byte_back(1) == Some(b'<'))
781        {
782            self.print_hard_space();
783        }
784    }
785
786    fn print_non_negative_float(&mut self, num: f64) {
787        // Inline the buffer here to avoid heap allocation on `buffer.format(*self).to_string()`.
788        let mut buffer = dragonbox_ecma::Buffer::new();
789        if num < 1000.0 && num.fract() == 0.0 {
790            self.print_str(buffer.format(num));
791            self.need_space_before_dot = self.code_len();
792        } else {
793            self.print_minified_number(num, &mut buffer);
794        }
795    }
796
797    fn print_decorators(&mut self, decorators: &[Decorator<'_>], ctx: Context) {
798        for decorator in decorators {
799            decorator.print(self, ctx);
800            self.print_hard_space();
801        }
802    }
803
804    // Optimized version of `get_minified_number` from terser
805    // https://github.com/terser/terser/blob/c5315c3fd6321d6b2e076af35a70ef532f498505/lib/output.js#L2418
806    // Instead of building all candidates and finding the shortest, we track the shortest as we go
807    // and use self.print_str directly instead of returning intermediate strings
808    #[expect(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_possible_wrap)]
809    fn print_minified_number(&mut self, num: f64, buffer: &mut dragonbox_ecma::Buffer) {
810        if num < 1000.0 && num.fract() == 0.0 {
811            self.print_str(buffer.format(num));
812            self.need_space_before_dot = self.code_len();
813            return;
814        }
815
816        let mut s = buffer.format(num);
817
818        if s.starts_with("0.") {
819            s = &s[1..];
820        }
821
822        let mut best_candidate = s.cow_replacen("e+", "e", 1);
823        let mut is_hex = false;
824
825        // Track the best candidate found so far
826        if num.fract() == 0.0 {
827            // For integers, check hex format and other optimizations
828            let hex_candidate = format!("0x{:x}", num as u128);
829            if hex_candidate.len() < best_candidate.len() {
830                is_hex = true;
831                best_candidate = hex_candidate.into();
832            }
833        }
834        // Check for scientific notation optimizations for numbers starting with ".0"
835        else if best_candidate.starts_with(".0") {
836            // Skip the first '0' since we know it's there from the starts_with check
837            if let Some(i) = best_candidate.bytes().skip(2).position(|c| c != b'0') {
838                let len = i + 2; // `+2` to include the dot and first zero.
839                let digits = &best_candidate[len..];
840                let exp = digits.len() + len - 1;
841                let exp_str_len = itoa::Buffer::new().format(exp).len();
842                // Calculate expected length: digits + 'e-' + exp_length
843                let expected_len = digits.len() + 2 + exp_str_len;
844                if expected_len < best_candidate.len() {
845                    best_candidate = format!("{digits}e-{exp}").into();
846                    debug_assert_eq!(best_candidate.len(), expected_len);
847                }
848            }
849        }
850
851        // Check for numbers ending with zeros (but not hex numbers)
852        // The `!is_hex` check is necessary to prevent hex numbers like `0x8000000000000000`
853        // from being incorrectly converted to scientific notation
854        if !is_hex
855            && best_candidate.ends_with('0')
856            && let Some(len) = best_candidate.bytes().rev().position(|c| c != b'0')
857        {
858            let base = &best_candidate[0..best_candidate.len() - len];
859            let exp_str_len = itoa::Buffer::new().format(len).len();
860            // Calculate expected length: base + 'e' + len
861            let expected_len = base.len() + 1 + exp_str_len;
862            if expected_len < best_candidate.len() {
863                best_candidate = format!("{base}e{len}").into();
864                debug_assert_eq!(best_candidate.len(), expected_len);
865            }
866        }
867
868        // Check for scientific notation optimization: `1.2e101` -> `12e100`
869        if let Some((integer, point, exponent)) = best_candidate
870            .split_once('.')
871            .and_then(|(a, b)| b.split_once('e').map(|e| (a, e.0, e.1)))
872        {
873            let new_expr = exponent.parse::<isize>().unwrap() - point.len() as isize;
874            let new_exp_str_len = itoa::Buffer::new().format(new_expr).len();
875            // Calculate expected length: integer + point + 'e' + new_exp_str_len
876            let expected_len = integer.len() + point.len() + 1 + new_exp_str_len;
877            if expected_len < best_candidate.len() {
878                best_candidate = format!("{integer}{point}e{new_expr}").into();
879                debug_assert_eq!(best_candidate.len(), expected_len);
880            }
881        }
882
883        // Print the best candidate and update need_space_before_dot
884        self.print_str(&best_candidate);
885        if !best_candidate.bytes().any(|b| matches!(b, b'.' | b'e' | b'x')) {
886            self.need_space_before_dot = self.code_len();
887        }
888    }
889
890    #[cfg(feature = "sourcemap")]
891    fn add_source_mapping(&mut self, span: Span) {
892        if let Some(sourcemap_builder) = self.sourcemap_builder.as_mut()
893            && !span.is_empty()
894        {
895            sourcemap_builder.add_source_mapping(self.code.as_bytes(), span.start, None);
896        }
897    }
898
899    #[cfg(not(feature = "sourcemap"))]
900    #[inline]
901    #[expect(clippy::needless_pass_by_ref_mut, clippy::unused_self)]
902    fn add_source_mapping(&mut self, _span: Span) {}
903
904    #[cfg(feature = "sourcemap")]
905    fn add_source_mapping_end(&mut self, span: Span) {
906        if let Some(sourcemap_builder) = self.sourcemap_builder.as_mut()
907            && !span.is_empty()
908        {
909            // Validate that span.end is within source content bounds.
910            // When oxc_codegen adds punctuation (semicolons, newlines) that don't exist in the
911            // original source, span.end may be at or beyond the source content length.
912            // We should not create sourcemap tokens for such positions as they would be invalid.
913            if let Some(source_text) = self.source_text {
914                #[expect(clippy::cast_possible_truncation)]
915                if span.end >= source_text.len() as u32 {
916                    return;
917                }
918            }
919            sourcemap_builder.add_source_mapping(self.code.as_bytes(), span.end, None);
920        }
921    }
922
923    #[cfg(not(feature = "sourcemap"))]
924    #[inline]
925    #[expect(clippy::needless_pass_by_ref_mut, clippy::unused_self)]
926    fn add_source_mapping_end(&mut self, _span: Span) {}
927
928    #[cfg(feature = "sourcemap")]
929    fn add_source_mapping_for_name(&mut self, span: Span, name: &str) {
930        if let Some(sourcemap_builder) = self.sourcemap_builder.as_mut()
931            && !span.is_empty()
932        {
933            sourcemap_builder.add_source_mapping_for_name(self.code.as_bytes(), span, name);
934        }
935    }
936
937    #[cfg(not(feature = "sourcemap"))]
938    #[inline]
939    #[expect(clippy::needless_pass_by_ref_mut, clippy::unused_self)]
940    fn add_source_mapping_for_name(&mut self, _span: Span, _name: &str) {}
941}