perf(formatter_core): keep the embedded IR when nothing in it is rewritten (#27673)

`map_text_in_ir` now takes the IR by value and returns it as is when the callback rewrites nothing; a rebuild starts at the first change.

Markdown code blocks no longer pre-scan their IR (`scan_texts`), the fence character is found by the same walk.
This commit is contained in:
leaysgur committed 2026-10-14 07:06:46 +00:00
1 parent 3543672c43
commit 1d3ef102fe
8 files changed
+64 -77

No files matched your search

Generated
-1
View File
@@ -2153,7 +2153,6 @@ dependencies = [
"oxc_span",
"oxc_tasks_common",
"pico-args",
"rustc-hash",
"serde_json",
]
@@ -90,7 +90,7 @@ pub(super) fn format_graphql_doc<'a>(
};
Some(ir)
};
let ir = ir.map(|ir| super::escape_template_chars_in_ir(&ir, f));
let ir = ir.map(|ir| super::escape_template_chars_in_ir(ir, f));
ir_parts.push(ir);
}
@@ -73,7 +73,7 @@ pub(super) fn format_html_doc<'a>(
// Re-escape template chars in `Text` runs:
// the IR is reinserted into a JS template literal built from `.cooked` values.
let ir = super::escape_template_chars_in_ir(&ir, f);
let ir = super::escape_template_chars_in_ir(ir, f);
let content = format_once(|f| f.write_elements(ir));
let ws_ignore = f.options().html_whitespace_sensitivity_ignore;
@@ -159,7 +159,7 @@ pub(super) fn format_html_doc<'a>(
// and substituting placeholders inside every `BestFitting` variant.
// Escaping cannot alter placeholders: the sentinel is ASCII word characters only.
let indent_width = f.options().indent_width;
let ir = map_text_in_ir(&ir, allocator, &mut |text, out| {
let ir = map_text_in_ir(ir, allocator, &mut |text, out| {
let escaped = super::escape_template_chars(text, allocator);
let text = escaped.unwrap_or(text);
if text.contains(PLACEHOLDER_PREFIX) {
@@ -39,7 +39,7 @@ pub(super) fn try_embed_markdown<'a>(
// Markdown uses `.raw` quasi values, so only backticks need raw-style escaping
// (template chars `${` / `\` are passed through verbatim).
// https://github.com/prettier/prettier/blob/90983f40dce5e20beea4e5618b5e0426a6a7f4f0/src/language-js/embed/markdown.js#L24
let ir = super::escape_backticks_raw_in_ir(&ir, f);
let ir = super::escape_backticks_raw_in_ir(ir, f);
// Phase 5: Layout
// https://github.com/prettier/prettier/blob/90983f40dce5e20beea4e5618b5e0426a6a7f4f0/src/language-js/embed/markdown.js#L24-L29
@@ -372,7 +372,7 @@ fn split_on_placeholders<'a>(text: &'a str, prefix: &str, suffix: &str) -> Vec<&
/// The IR is re-inserted into a JS template literal built from `.cooked` values,
/// so these characters need escaping.
fn escape_template_chars_in_ir<'a>(
ir: &[FormatElement<'a>],
ir: ArenaVec<'a, FormatElement<'a>>,
f: &JsFormatter<'_, 'a>,
) -> ArenaVec<'a, FormatElement<'a>> {
escape_text_in_ir(ir, f, escape_template_chars)
@@ -381,14 +381,14 @@ fn escape_template_chars_in_ir<'a>(
/// Re-escape backticks in `Text` elements of an embedded IR using Prettier's "raw" escape rule.
/// Used by markdown-in-JS, which uses `.raw` quasi values.
fn escape_backticks_raw_in_ir<'a>(
ir: &[FormatElement<'a>],
ir: ArenaVec<'a, FormatElement<'a>>,
f: &JsFormatter<'_, 'a>,
) -> ArenaVec<'a, FormatElement<'a>> {
escape_text_in_ir(ir, f, escape_backticks_raw)
}
fn escape_text_in_ir<'a>(
ir: &[FormatElement<'a>],
ir: ArenaVec<'a, FormatElement<'a>>,
f: &JsFormatter<'_, 'a>,
escape: fn(&'a str, &'a Allocator) -> Option<&'a str>,
) -> ArenaVec<'a, FormatElement<'a>> {
@@ -141,9 +141,10 @@ pub fn dispatch_ir<'a, C: FormatContext>(
/// `map_text` receives each `Text` and `Token` run (a child may print any character as a token, e.g. JS's `` ` ``);
/// it either pushes replacement elements into the output and returns `true`, or returns `false` to keep the element unchanged.
/// A shared `Interned` subtree is rebuilt once and the rebuilt element re-shared.
/// Nothing is rebuilt until the first change, an IR with none is returned as is.
#[expect(clippy::mutable_key_type)] // `Interned` hashes by pointer identity
pub fn map_text_in_ir<'a, F>(
ir: &[FormatElement<'a>],
ir: ArenaVec<'a, FormatElement<'a>>,
allocator: &'a Allocator,
map_text: &mut F,
) -> ArenaVec<'a, FormatElement<'a>>
@@ -151,54 +152,79 @@ where
F: FnMut(&'a str, &mut ArenaVec<'a, FormatElement<'a>>) -> bool,
{
let mut interned_cache = FxHashMap::default();
map_text_in_ir_impl(ir, allocator, map_text, &mut interned_cache)
map_text_in_ir_impl(&ir, allocator, map_text, &mut interned_cache).unwrap_or(ir)
}
/// Returns `None` if no element changed.
#[expect(clippy::mutable_key_type)] // `Interned` hashes by pointer identity
#[expect(clippy::option_option)]
fn map_text_in_ir_impl<'a, F>(
ir: &[FormatElement<'a>],
allocator: &'a Allocator,
map_text: &mut F,
interned_cache: &mut FxHashMap<Interned<'a>, Option<FormatElement<'a>>>,
) -> ArenaVec<'a, FormatElement<'a>>
// `None` for an unchanged subtree, `Some(None)` for one rebuilt empty
interned_cache: &mut FxHashMap<Interned<'a>, Option<Option<FormatElement<'a>>>>,
) -> Option<ArenaVec<'a, FormatElement<'a>>>
where
F: FnMut(&'a str, &mut ArenaVec<'a, FormatElement<'a>>) -> bool,
{
let mut out = ArenaVec::with_capacity_in(ir.len(), &allocator);
for element in ir {
match element {
// Holds nothing until the first change, then the rebuilt IR
let mut out = ArenaVec::new_in(&allocator);
let mut rebuilt = false;
for (index, element) in ir.iter().enumerate() {
let changed = match element {
FormatElement::Text { text, .. } | FormatElement::Token { text } => {
if !map_text(text, &mut out) {
out.push(element.clone());
}
map_text(text, &mut out)
}
FormatElement::BestFitting(best_fitting) => {
let mut changed = false;
let mut variants =
ArenaVec::with_capacity_in(best_fitting.variants().len(), &allocator);
for variant in best_fitting.variants() {
let mapped = map_text_in_ir_impl(variant, allocator, map_text, interned_cache);
variants.push(mapped.into_arena_slice());
match map_text_in_ir_impl(variant, allocator, map_text, interned_cache) {
Some(mapped) => {
changed = true;
variants.push(mapped.into_arena_slice());
}
None => variants.push(variant),
}
}
changed && {
// SAFETY: This rebuild preserves the original BestFitting's variant count.
out.push(FormatElement::BestFitting(unsafe {
BestFittingElement::from_vec_unchecked(variants)
}));
true
}
// SAFETY: This rebuild preserves the original BestFitting's variant count.
out.push(FormatElement::BestFitting(unsafe {
BestFittingElement::from_vec_unchecked(variants)
}));
}
FormatElement::Interned(interned) => {
let mapped = if let Some(mapped) = interned_cache.get(interned) {
mapped.clone()
} else {
let mapped = map_text_in_ir_impl(interned, allocator, map_text, interned_cache);
let mapped = intern_exact(allocator, mapped.into_iter());
let mapped = map_text_in_ir_impl(interned, allocator, map_text, interned_cache)
.map(|mapped| intern_exact(allocator, mapped.into_iter()));
interned_cache.insert(interned.clone(), mapped.clone());
mapped
};
out.extend(mapped);
mapped.is_some_and(|mapped| {
out.extend(mapped);
true
})
}
_ => out.push(element.clone()),
_ => false,
};
if rebuilt {
if !changed {
out.push(element.clone());
}
} else if changed {
// Put the elements before the first change in front of its replacement
out.reserve(ir.len() - 1);
out.splice(0..0, ir[..index].iter().cloned());
rebuilt = true;
}
}
out
rebuilt.then_some(out)
}
/// Pushes `text` with each newline as a literal line (Prettier's `replaceEndOfLine()`).
-1
View File
@@ -23,7 +23,6 @@ oxc_allocator = { workspace = true }
oxc_diagnostics = { workspace = true }
oxc_formatter_core = { workspace = true }
oxc_span = { workspace = true }
rustc-hash = { workspace = true }
[dev-dependencies]
insta = { workspace = true }
+10 -47
View File
@@ -2,12 +2,11 @@
//! indented blocks stay indented.
use cow_utils::CowUtils;
use rustc_hash::FxHashSet;
use oxc_allocator::ArenaVec;
use oxc_formatter_core::{
Buffer, BufferExtensions, DispatchRequest, Document, FormatElement, FormatOptions, InputKind,
Interned, PrintWidth, arena_cow_str,
PrintWidth, arena_cow_str,
builders::{
FormatOnce, exact_line_breaks, hard_line_break, mark_as_root, space_align, text, token,
},
@@ -124,20 +123,17 @@ fn format_embedded<'a>(
},
)?;
let (has_newline, has_fence_char) = scan_texts(&ir, fence_char);
// Newline inside a text (a template literal, a block comment) becomes a literal line,
// which `mark_as_root` keeps at the fence's column
let ir = if has_newline {
let indent_width = f.options().indent_width();
map_text_in_ir(&ir, f.allocator(), &mut |text, out| {
text.contains('\n') && {
push_text_with_literal_lines(out, text, indent_width);
true
}
})
} else {
ir
};
let indent_width = f.options().indent_width();
let mut has_fence_char = false;
let ir = map_text_in_ir(ir, f.allocator(), &mut |text, out| {
has_fence_char |= text.as_bytes().contains(&fence_char);
text.contains('\n') && {
push_text_with_literal_lines(out, text, indent_width);
true
}
});
if !has_fence_char {
return Some((ir, 0));
}
@@ -153,39 +149,6 @@ fn format_embedded<'a>(
Some((ir, run))
}
/// Whether any text of the IR holds a newline / the fence character;
/// a shared `Interned` subtree is walked once.
#[expect(clippy::mutable_key_type)] // `Interned` hashes by pointer identity
fn scan_texts(ir: &[FormatElement<'_>], fence_char: u8) -> (bool, bool) {
fn walk<'a>(
elements: &[FormatElement<'a>],
fence_char: u8,
seen: &mut FxHashSet<Interned<'a>>,
found: &mut (bool, bool),
) {
for element in elements {
match element {
FormatElement::Text { text, .. } | FormatElement::Token { text } => {
found.0 |= text.contains('\n');
found.1 |= text.as_bytes().contains(&fence_char);
}
FormatElement::BestFitting(best_fitting) => {
for variant in best_fitting.variants() {
walk(variant, fence_char, seen, found);
}
}
FormatElement::Interned(interned) if seen.insert(interned.clone()) => {
walk(interned, fence_char, seen, found);
}
_ => {}
}
}
}
let mut found = (false, false);
walk(ir, fence_char, &mut FxHashSet::default(), &mut found);
found
}
/// Whether the meta addresses lines by number (`{1,3-5}`, VitePress / Docusaurus line highlighting),
/// which formatting the content would shift.
fn has_line_ranges(meta: &str) -> bool {