mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
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:
1 parent
3543672c43
commit
1d3ef102fe
8 files changed
+64
-77
No files matched your search
Generated
-1
@@ -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()`).
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user