perf(formatter): write string literals with swapped quotes into the arena (#27558)

A string literal whose quotes change was built with `format!` on the
heap, and then copied into the arena to be written.
It is now built in the arena directly by `alloc_concat_strs_array`,
through one helper for every place that puts content in quotes (string
literals, directives, JSX attributes).

Tested on vscode `src/` (NOTE: This repo is written with single quotes,
no formatter installed, so formatted with the default double quotes), 1
thread: 2.6% fewer instructions, 2.2% fewer cycles.

The output is unchanged byte for byte.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yuji SugiuraandClaude Opus 5.5 authored and GitHub committed 2026-10-14 16:53:28 +09:00
1 parent 073413771b
commit d8ce750585
2 files changed
+40 -50

No files matched your search

+24 -34
View File
@@ -2,6 +2,7 @@ use std::{borrow::Cow, ops::Deref};
use unicode_width::UnicodeWidthStr;
use oxc_allocator::Allocator;
use oxc_formatter_core::{Format, arena_cow_str, spec::normalize_string};
use oxc_span::SourceType;
use oxc_syntax::identifier::is_identifier_name_patched;
@@ -55,7 +56,7 @@ impl<'a> FormatLiteralStringToken<'a> {
};
let string_cleaner =
LiteralStringNormalizer::new(*self, chosen_quote_style, is_quote_needed);
LiteralStringNormalizer::new(*self, chosen_quote_style, is_quote_needed, f.allocator());
let content = string_cleaner.normalize_text(source_type);
@@ -83,9 +84,8 @@ impl CleanedStringLiteralText<'_> {
impl<'a> Format<'a, JsFormatContext<'a>> for CleanedStringLiteralText<'a> {
fn fmt(&self, f: &mut JsFormatter<'_, 'a>) {
// In the common case the literal needs no normalization, so `text` borrows a slice of the
// source (`Cow::Borrowed`) which already outlives the buffer; pass it straight through and
// only copy into the arena for the owned (normalized) case.
// `text` borrows the source or the arena (quotes swapped) in all but the rare cases left owned,
// which are copied into the arena here.
text(arena_cow_str(&self.text, f)).fmt(f);
}
}
@@ -189,6 +189,8 @@ struct LiteralStringNormalizer<'a> {
chosen_quote_style: QuoteStyle,
/// State whether we need to print the quotes or not.
is_quote_needed: bool,
/// Arena for the strings with other quotes, so that `fmt` does not copy them again.
allocator: &'a Allocator,
}
impl<'a> LiteralStringNormalizer<'a> {
@@ -196,8 +198,9 @@ impl<'a> LiteralStringNormalizer<'a> {
token: FormatLiteralStringToken<'a>,
chosen_quote_style: QuoteStyle,
is_quote_needed: bool,
allocator: &'a Allocator,
) -> Self {
Self { token, chosen_quote_style, is_quote_needed }
Self { token, chosen_quote_style, is_quote_needed, allocator }
}
fn normalize_text(&self, source_type: SourceType) -> Cow<'a, str> {
@@ -223,23 +226,13 @@ impl<'a> LiteralStringNormalizer<'a> {
let (normalized, chosen_quote) = normalize_jsx_string(raw_content, self.chosen_quote_style);
let quote_char = chosen_quote.as_char();
match normalized {
Cow::Borrowed(s) if s == raw_content && current_quote == chosen_quote => {
// No changes needed, return original
normalize_newlines(self.token.string, ['\r'])
}
Cow::Borrowed(s) => {
// Content unchanged but quotes need swapping
let normalized_newlines = normalize_newlines(s, ['\r']);
Cow::Owned(std::format!("{quote_char}{normalized_newlines}{quote_char}"))
}
Cow::Owned(s) => {
// Content changed
let normalized_newlines = normalize_newlines(&s, ['\r']);
Cow::Owned(std::format!("{quote_char}{normalized_newlines}{quote_char}"))
}
// Content changed or quotes need swapping
_ => self.quoted(chosen_quote, &normalize_newlines(&normalized, ['\r'])),
}
}
@@ -273,16 +266,12 @@ impl<'a> LiteralStringNormalizer<'a> {
}
}
Cow::Owned(string) => {
let mut s = String::with_capacity(string.len() + 2);
let quote = if string_information.raw_content_has_quotes {
string_information.current_quote.as_char()
string_information.current_quote
} else {
string_information.preferred_quote.as_char()
string_information.preferred_quote
};
s.push(quote);
s.push_str(&string);
s.push(quote);
Cow::Owned(s)
self.quoted(quote, &string)
}
}
}
@@ -333,13 +322,9 @@ impl<'a> LiteralStringNormalizer<'a> {
match polished_raw_content {
Cow::Borrowed(raw_content) => self.swap_quotes(raw_content, string_information),
Cow::Owned(mut s) => {
// content is owned, meaning we allocated a new string,
// so we force replacing quotes, regardless
s.insert(0, preferred_quote.as_char());
s.push(preferred_quote.as_char());
Cow::Owned(s)
}
// content is owned, meaning we allocated a new string,
// so we force replacing quotes, regardless
Cow::Owned(s) => self.quoted(preferred_quote, &s),
}
}
@@ -350,15 +335,20 @@ impl<'a> LiteralStringNormalizer<'a> {
}
fn swap_quotes(&self, content_to_use: &'a str, str_info: StringInformation) -> Cow<'a, str> {
let preferred_quote = str_info.preferred_quote.as_char();
let original = self.token.string;
if original.starts_with(preferred_quote) {
if original.starts_with(str_info.preferred_quote.as_char()) {
Cow::Borrowed(original)
} else {
Cow::Owned(std::format!("{preferred_quote}{content_to_use}{preferred_quote}"))
self.quoted(str_info.preferred_quote, content_to_use)
}
}
/// `content` in `quote`s, in the arena.
fn quoted(&self, quote: QuoteStyle, content: &str) -> Cow<'a, str> {
let quote = quote.as_str();
Cow::Borrowed(self.allocator.alloc_concat_strs_array([quote, content, quote]))
}
}
impl<'a> Format<'a, JsFormatContext<'a>> for FormatLiteralStringToken<'a> {
@@ -22,9 +22,9 @@ App.tsx:
RadixUIAdoptionSection.jsx:
file size: 2520 # 2.52 kB
sys allocs: 92
sys reallocs: 74
sys deallocs: 91
sys allocs: 69
sys reallocs: 69
sys deallocs: 68
sys alloc bytes: 36132 # 36.13 kB
sys peak growth: 12192 # 12.19 kB
arena allocs: 1149
@@ -33,9 +33,9 @@ RadixUIAdoptionSection.jsx:
pdf.mjs:
file size: 567296 # 567.30 kB
sys allocs: 9782
sys reallocs: 1328
sys deallocs: 9781
sys allocs: 9777
sys reallocs: 1323
sys deallocs: 9776
sys alloc bytes: 3141552 # 3.14 MB
sys peak growth: 1156432 # 1.16 MB
arena allocs: 429815
@@ -44,12 +44,12 @@ pdf.mjs:
antd.js:
file size: 6686316 # 6.69 MB
sys allocs: 90046
sys reallocs: 7658
sys deallocs: 90045
sys allocs: 84592
sys reallocs: 4017
sys deallocs: 84591
sys alloc bytes: 76472316 # 76.47 MB
sys peak growth: 50737424 # 50.74 MB
arena allocs: 2535459
arena allocs: 2535528
arena reallocs: 1889
arena size: 246383800 # 246.38 MB
@@ -66,11 +66,11 @@ binder.ts:
kitchen-sink.tsx:
file size: 732898 # 732.90 kB
sys allocs: 17090
sys reallocs: 4956
sys deallocs: 17089
sys alloc bytes: 4436340 # 4.44 MB
sys allocs: 12672
sys reallocs: 2347
sys deallocs: 12671
sys alloc bytes: 4353874 # 4.35 MB
sys peak growth: 1491796 # 1.49 MB
arena allocs: 557531
arena allocs: 557605
arena reallocs: 1041
arena size: 38432064 # 38.43 MB
arena size: 38433168 # 38.43 MB