mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
perf(formatter_graphql): print block string lines borrowed from the source (#28701)
GraphQL block string `"""..."""` may or may not contain CR,CRLF. But if not, we can use source as-is.
This commit is contained in:
1 parent
dce14853f6
commit
46f8efd1fb
4 files changed
+41
-38
No files matched your search
Generated
+1
@@ -2117,6 +2117,7 @@ version = "0.75.0"
|
||||
dependencies = [
|
||||
"cow-utils",
|
||||
"insta",
|
||||
"memchr",
|
||||
"oxc-graphql-parser",
|
||||
"oxc_allocator",
|
||||
"oxc_diagnostics",
|
||||
|
||||
@@ -54,7 +54,8 @@ This crate instead collects comment spans into a positional cursor, drained in s
|
||||
### Strings
|
||||
|
||||
Prettier prints `StringValue` from `graphql-js`'s _cooked_ value and re-encodes it.
|
||||
`oxc-graphql-parser`'s `StringValue.value` is cooked but not to spec (no block-string dedent / blank-line trimming, no surrogate pairing), so `src/print/string.rs` cooks from `raw` itself:
|
||||
`oxc-graphql-parser`'s `StringValue.value` is cooked but not to spec (no block-string dedent / blank-line trimming, no surrogate pairing), so `src/print/string.rs` cooks it itself,
|
||||
a regular string from `raw` and a block string from `value` (only its line endings normalized):
|
||||
|
||||
- the GraphQL spec `BlockStringValue` algorithm (dedent + blank-line trimming)
|
||||
- escape decoding for regular strings (incl. surrogate pairs)
|
||||
|
||||
@@ -19,6 +19,7 @@ workspace = true
|
||||
[dependencies]
|
||||
oxc-graphql-parser = { workspace = true }
|
||||
cow-utils = { workspace = true }
|
||||
memchr = { workspace = true }
|
||||
oxc_allocator = { workspace = true }
|
||||
oxc_diagnostics = { workspace = true }
|
||||
oxc_formatter_core = { workspace = true }
|
||||
|
||||
@@ -5,12 +5,16 @@
|
||||
//! `oxc-graphql-parser`'s `StringValue.value` is cooked too, but NOT to spec:
|
||||
//! no block-string dedent / blank-line trimming, no surrogate pairing, and
|
||||
//! invalid escapes are silently dropped.
|
||||
//! So this module cooks from `raw` itself (GraphQL spec string/block-string value algorithms)
|
||||
//! and then applies Prettier's re-encoding (`"`/`\` escaped, newline as `\n`, `"""` as `\"""`).
|
||||
//! So this module cooks them itself (GraphQL spec string/block-string value algorithms):
|
||||
//! a regular string from `raw`, a block string from `value`, which only has its line endings normalized.
|
||||
//! Then it applies Prettier's re-encoding (`"`/`\` escaped, newline as `\n`, `"""` as `\"""`).
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use cow_utils::CowUtils;
|
||||
|
||||
use oxc_formatter_core::{
|
||||
arena_cow_str,
|
||||
builders::{exact_line_breaks, hard_line_break, text},
|
||||
write,
|
||||
};
|
||||
@@ -20,8 +24,8 @@ use super::GraphqlFormatter;
|
||||
|
||||
pub(super) fn write_string_value<'a>(sv: &StringValue<'a>, f: &mut GraphqlFormatter<'_, 'a>) {
|
||||
let raw = sv.raw;
|
||||
if let Some(body) = raw.strip_prefix("\"\"\"").and_then(|r| r.strip_suffix("\"\"\"")) {
|
||||
write_block_string(body, f);
|
||||
if sv.block {
|
||||
write_block_string(sv.value, f);
|
||||
} else if let Some(body) = raw.strip_prefix('"').and_then(|r| r.strip_suffix('"')) {
|
||||
write_regular_string(body, f);
|
||||
} else {
|
||||
@@ -32,21 +36,28 @@ pub(super) fn write_string_value<'a>(sv: &StringValue<'a>, f: &mut GraphqlFormat
|
||||
|
||||
/// Block string: cook per the GraphQL `BlockStringValue` algorithm, re-escape `"""`,
|
||||
/// then print one line per hard line break (Prettier's shape).
|
||||
fn write_block_string(body: &str, f: &mut GraphqlFormatter<'_, '_>) {
|
||||
// The only escape sequence in block strings.
|
||||
let unescaped = body.cow_replace("\\\"\"\"", "\"\"\"");
|
||||
let cooked_lines = cook_block_string_lines(&unescaped);
|
||||
fn write_block_string<'a>(body: &'a str, f: &mut GraphqlFormatter<'_, 'a>) {
|
||||
// `\"""` is the only escape in block strings.
|
||||
// Precheck its `\`: `cow_replace` builds a substring searcher per call
|
||||
let unescaped = if body.contains('\\') {
|
||||
body.cow_replace("\\\"\"\"", "\"\"\"")
|
||||
} else {
|
||||
Cow::Borrowed(body)
|
||||
};
|
||||
// Without one, no line holds a `"""` to re-escape: the lines print borrowed from the source.
|
||||
// Re-escaping touches no whitespace, so it is done when printing
|
||||
let escapes = matches!(unescaped, Cow::Owned(_));
|
||||
let mut lines = cook_block_string_lines(arena_cow_str(&unescaped, f));
|
||||
|
||||
// Prettier: re-escape, then `lines.length === 1` → trim, all-blank → drop everything.
|
||||
// Prettier: `lines.length === 1` → trim, all-blank → drop everything.
|
||||
// Trailing spaces/tabs are trimmed per line.
|
||||
let mut lines: Vec<String> = cooked_lines
|
||||
.iter()
|
||||
.map(|l| l.trim_end_matches([' ', '\t']).cow_replace("\"\"\"", "\\\"\"\"").into_owned())
|
||||
.collect();
|
||||
if lines.len() == 1 {
|
||||
lines[0] = lines[0].trim().to_string();
|
||||
for line in &mut lines {
|
||||
*line = line.trim_end_matches([' ', '\t']);
|
||||
}
|
||||
if lines.iter().all(std::string::String::is_empty) {
|
||||
if let [line] = lines.as_mut_slice() {
|
||||
*line = line.trim();
|
||||
}
|
||||
if lines.iter().all(|line| line.is_empty()) {
|
||||
lines.clear();
|
||||
}
|
||||
|
||||
@@ -55,14 +66,16 @@ fn write_block_string(body: &str, f: &mut GraphqlFormatter<'_, '_>) {
|
||||
// blank lines inside a block string are part of its VALUE,
|
||||
// so a run of `k` blank lines is emitted as `exact_line_breaks(k + 1)`.
|
||||
let mut pending_blanks = 0usize;
|
||||
for line in &lines {
|
||||
for line in lines {
|
||||
if line.is_empty() {
|
||||
pending_blanks += 1;
|
||||
continue;
|
||||
}
|
||||
write_block_string_break(pending_blanks, f);
|
||||
pending_blanks = 0;
|
||||
write!(f, text(f.allocator().alloc_str(line)));
|
||||
let line =
|
||||
if escapes { arena_cow_str(&line.cow_replace("\"\"\"", "\\\"\"\""), f) } else { line };
|
||||
write!(f, text(line));
|
||||
}
|
||||
write_block_string_break(pending_blanks, f);
|
||||
write!(f, "\"\"\"");
|
||||
@@ -79,7 +92,7 @@ fn write_block_string_break(blank_lines: usize, f: &mut GraphqlFormatter<'_, '_>
|
||||
/// GraphQL spec `BlockStringValue`: split lines, strip the common indentation of
|
||||
/// non-first lines, then drop leading/trailing blank lines.
|
||||
fn cook_block_string_lines(value: &str) -> Vec<&str> {
|
||||
let mut lines: Vec<&str> = split_graphql_lines(value);
|
||||
let mut lines: Vec<&str> = split_lines(value);
|
||||
|
||||
// Common indent over non-blank lines after the first.
|
||||
let mut common_indent: Option<usize> = None;
|
||||
@@ -107,26 +120,13 @@ fn cook_block_string_lines(value: &str) -> Vec<&str> {
|
||||
lines
|
||||
}
|
||||
|
||||
/// Splits on GraphQL line terminators: `\r\n`, lone `\r`, `\n`.
|
||||
fn split_graphql_lines(value: &str) -> Vec<&str> {
|
||||
let bytes = value.as_bytes();
|
||||
/// `value.split('\n')`, by the SIMD `memchr` rather than core's
|
||||
fn split_lines(value: &str) -> Vec<&str> {
|
||||
let mut lines = Vec::new();
|
||||
let mut start = 0;
|
||||
let mut i = 0;
|
||||
while i < bytes.len() {
|
||||
match bytes[i] {
|
||||
b'\n' => {
|
||||
lines.push(&value[start..i]);
|
||||
i += 1;
|
||||
start = i;
|
||||
}
|
||||
b'\r' => {
|
||||
lines.push(&value[start..i]);
|
||||
i += if bytes.get(i + 1) == Some(&b'\n') { 2 } else { 1 };
|
||||
start = i;
|
||||
}
|
||||
_ => i += 1,
|
||||
}
|
||||
for i in memchr::memchr_iter(b'\n', value.as_bytes()) {
|
||||
lines.push(&value[start..i]);
|
||||
start = i + 1;
|
||||
}
|
||||
lines.push(&value[start..]);
|
||||
lines
|
||||
|
||||
Reference in new issue
Block a user