@@ -128,9 +128,11 @@ impl<'a, 'o> JsdocFormatter<'a, 'o> {
128128 if !desc_trimmed. is_empty ( ) {
129129 merged_desc. push_str ( desc_trimmed) ;
130130 }
131- // Collect effective tags, absorbing @description tag content
131+ // Collect effective tags, absorbing @description tag content.
132+ // Track whether @import tags exist to skip import processing (common case).
132133 let mut effective_tags: Vec < ( & oxc_jsdoc:: parser:: JSDocTag < ' _ > , & str ) > =
133134 Vec :: with_capacity ( sorted_tags. len ( ) ) ;
135+ let mut has_import_tags = false ;
134136 for ( tag, normalized_kind) in & sorted_tags {
135137 if should_remove_empty_tag ( normalized_kind) && !tag_has_content ( tag) {
136138 continue ;
@@ -146,6 +148,9 @@ impl<'a, 'o> JsdocFormatter<'a, 'o> {
146148 }
147149 continue ;
148150 }
151+ if * normalized_kind == "import" {
152+ has_import_tags = true ;
153+ }
149154 effective_tags. push ( ( tag, normalized_kind) ) ;
150155 }
151156
@@ -172,9 +177,15 @@ impl<'a, 'o> JsdocFormatter<'a, 'o> {
172177 // Reorder @param tags to match the function signature order
173178 reorder_param_tags ( & mut effective_tags, comment, source_text) ;
174179
175- // Pre-process @import tags: merge by module, sort, format
176- let ( mut import_lines, parsed_import_indices) = process_import_tags ( & effective_tags) ;
177- let has_imports = !import_lines. is_empty ( ) ;
180+ // Pre-process @import tags: merge by module, sort, format.
181+ // Skip entirely when no @import tags exist (common case) to avoid allocation.
182+ let ( mut import_lines, parsed_import_indices) = if has_import_tags {
183+ let ( lines, indices) = process_import_tags ( & effective_tags) ;
184+ ( Some ( lines) , indices)
185+ } else {
186+ ( None , smallvec:: SmallVec :: new ( ) )
187+ } ;
188+ let has_imports = import_lines. as_ref ( ) . is_some_and ( |l| !l. is_empty ( ) ) ;
178189 let mut imports_emitted = false ;
179190
180191 // Format tags
@@ -189,8 +200,7 @@ impl<'a, 'o> JsdocFormatter<'a, 'o> {
189200 if !self . content_lines . is_empty ( ) && !self . content_lines . last_is_empty ( ) {
190201 self . content_lines . push_empty ( ) ;
191202 }
192- let import_str =
193- std:: mem:: replace ( & mut import_lines, LineBuffer :: new ( ) ) . into_string ( ) ;
203+ let import_str = import_lines. take ( ) . unwrap ( ) . into_string ( ) ;
194204 self . content_lines . push ( import_str) ;
195205 imports_emitted = true ;
196206 prev_normalized_kind = Some ( "import" ) ;
@@ -322,26 +332,9 @@ impl<'a, 'o> JsdocFormatter<'a, 'o> {
322332 return Some ( FormattedJsdoc :: SingleLine ( alloc_first) ) ;
323333 }
324334
325- // Build temp string with `/** ... */` wrapper for comparison against original
326- let capacity =
327- content_str. len ( ) + content_str. bytes ( ) . filter ( |& b| b == b'\n' ) . count ( ) * 4 + 10 ;
328- let mut tmp = String :: with_capacity ( capacity) ;
329- tmp. push_str ( "/**" ) ;
330-
331- for line in std:: iter:: once ( first) . chain ( second) . chain ( iter) {
332- tmp. push ( '\n' ) ;
333- if line. is_empty ( ) {
334- tmp. push_str ( " *" ) ;
335- } else {
336- tmp. push_str ( " * " ) ;
337- tmp. push_str ( line) ;
338- }
339- }
340- tmp. push ( '\n' ) ;
341- tmp. push_str ( " */" ) ;
342-
343- // Compare with original — if unchanged, return None
344- if tmp == content {
335+ // Compare with original without allocating a temp string.
336+ // Stream-compare the formatted output against `content` byte-by-byte.
337+ if compare_multiline_jsdoc ( content, first, second, iter) {
345338 return None ;
346339 }
347340
@@ -450,6 +443,57 @@ impl<'a, 'o> JsdocFormatter<'a, 'o> {
450443 }
451444}
452445
446+ /// Compare a multi-line JSDoc comment's formatted content against the original source
447+ /// without allocating a temp string. Returns `true` if the formatted output would be
448+ /// identical to `original`.
449+ fn compare_multiline_jsdoc < ' a > (
450+ original : & str ,
451+ first_line : & ' a str ,
452+ second_line : Option < & ' a str > ,
453+ rest : impl Iterator < Item = & ' a str > ,
454+ ) -> bool {
455+ let original = original. as_bytes ( ) ;
456+ let mut pos = 0 ;
457+
458+ // Match "/**"
459+ if original. get ( pos..pos + 3 ) . is_none_or ( |s| s != b"/**" ) {
460+ return false ;
461+ }
462+ pos += 3 ;
463+
464+ // Match each content line: "\n * {line}" or "\n *" for empty lines
465+ for line in std:: iter:: once ( first_line) . chain ( second_line) . chain ( rest) {
466+ if pos >= original. len ( ) || original[ pos] != b'\n' {
467+ return false ;
468+ }
469+ pos += 1 ;
470+ if line. is_empty ( ) {
471+ if original. get ( pos..pos + 2 ) . is_none_or ( |s| s != b" *" ) {
472+ return false ;
473+ }
474+ pos += 2 ;
475+ } else {
476+ if original. get ( pos..pos + 3 ) . is_none_or ( |s| s != b" * " ) {
477+ return false ;
478+ }
479+ pos += 3 ;
480+ let line_bytes = line. as_bytes ( ) ;
481+ if original. get ( pos..pos + line_bytes. len ( ) ) . is_none_or ( |s| s != line_bytes) {
482+ return false ;
483+ }
484+ pos += line_bytes. len ( ) ;
485+ }
486+ }
487+
488+ // Match "\n */"
489+ if original. get ( pos..pos + 4 ) . is_none_or ( |s| s != b"\n */" ) {
490+ return false ;
491+ }
492+ pos += 4 ;
493+
494+ pos == original. len ( )
495+ }
496+
453497/// Trim trailing whitespace from an owned `String` in place, avoiding a reallocation.
454498pub ( super ) fn truncate_trim_end ( s : & mut String ) {
455499 let trimmed_len = s. trim_end ( ) . len ( ) ;
0 commit comments