feat(formatter_graphql): update oxc-graphql-parser 0.0.4 (#24039)

Updated from 0.0.1 -> 0.0.4.

As part of the process of improving performance, all CST were rewritten as AST.
Whilst this involved a major rewrite for the formatter, it had no impact on the user side.
This commit is contained in:
leaysgur committed 2026-07-02 04:52:35 +00:00
1 parent f25694147f
commit 0e5bcc9a8e
12 files changed
+630 -698

No files matched your search

Generated
+59 -89
View File
@@ -262,7 +262,7 @@ version = "3.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34"
dependencies = [
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -282,9 +282,9 @@ dependencies = [
[[package]]
name = "console"
version = "0.16.3"
version = "0.16.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87"
checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c"
dependencies = [
"encode_unicode",
"libc",
@@ -336,12 +336,6 @@ dependencies = [
"url",
]
[[package]]
name = "countme"
version = "3.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636"
[[package]]
name = "cow-utils"
version = "0.1.3"
@@ -651,7 +645,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -1106,9 +1100,9 @@ dependencies = [
[[package]]
name = "ignore"
version = "0.4.26"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b915661dd01db3f05050265b2477bcc6527b3792388e2749b41623cc592be67d"
checksum = "fe112b004901c62c2faa11f4f75e9864e0cc5af8da71c9115d184a3aa888749f"
dependencies = [
"crossbeam-deque",
"globset",
@@ -1179,9 +1173,9 @@ dependencies = [
[[package]]
name = "json-escape-simd"
version = "3.0.2"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35e770254dd7802184595b1d30da2a15cb72569e2aca2b177aef8d22eac8a693"
checksum = "2a18a0eb3c0a783620d2ae8cdda9590aae18b1608964ee9c7c43162562786424"
[[package]]
name = "json-strip-comments"
@@ -1352,7 +1346,7 @@ dependencies = [
"napi-build",
"napi-sys",
"nohash-hasher",
"rustc-hash 2.1.2",
"rustc-hash",
"serde",
"serde_json",
"tokio",
@@ -1442,7 +1436,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -1557,7 +1551,7 @@ checksum = "2b87743c2f6963036971ca587539cd9235693d953dc018a734014241afb20d5b"
dependencies = [
"flate2",
"postcard",
"rustc-hash 2.1.2",
"rustc-hash",
"serde",
"thiserror",
]
@@ -1573,12 +1567,12 @@ dependencies = [
[[package]]
name = "oxc-graphql-parser"
version = "0.0.1"
version = "0.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ff384edd4b618b5144b758ed42d03afa22c6aa0c263621fd8476b5990523c1f"
checksum = "1680d78ac0c54073910aedbedfbcd9cb86b1b1f897e42af87b351186717a8736"
dependencies = [
"memchr",
"rowan",
"oxc_allocator",
"thiserror",
]
@@ -1640,7 +1634,7 @@ dependencies = [
"oxc_estree",
"quickcheck",
"rand",
"rustc-hash 2.1.2",
"rustc-hash",
"serde",
"serde_json",
"windows-sys 0.61.2",
@@ -1700,7 +1694,7 @@ dependencies = [
"proc-macro2",
"quote",
"rayon",
"rustc-hash 2.1.2",
"rustc-hash",
"serde",
"serde_json",
"syn",
@@ -1740,7 +1734,7 @@ dependencies = [
"oxc_span",
"oxc_tasks_common",
"oxc_transformer",
"rustc-hash 2.1.2",
"rustc-hash",
"serde",
"serde_json",
]
@@ -1754,7 +1748,7 @@ dependencies = [
"oxc_index",
"oxc_syntax",
"petgraph",
"rustc-hash 2.1.2",
"rustc-hash",
]
[[package]]
@@ -1777,7 +1771,7 @@ dependencies = [
"oxc_str",
"oxc_syntax",
"pico-args",
"rustc-hash 2.1.2",
"rustc-hash",
]
[[package]]
@@ -1787,7 +1781,7 @@ dependencies = [
"cow-utils",
"oxc-browserslist",
"oxc_syntax",
"rustc-hash 2.1.2",
"rustc-hash",
"serde",
]
@@ -1814,7 +1808,7 @@ dependencies = [
"ignore",
"oxc-schemars",
"oxc_diagnostics",
"rustc-hash 2.1.2",
"rustc-hash",
"serde",
"serde_json",
"tempfile",
@@ -1839,7 +1833,7 @@ dependencies = [
"phf",
"pico-args",
"rayon",
"rustc-hash 2.1.2",
"rustc-hash",
"saphyr",
"serde",
"serde_json",
@@ -1923,7 +1917,7 @@ dependencies = [
"oxc_tasks_common",
"phf",
"pico-args",
"rustc-hash 2.1.2",
"rustc-hash",
"serde",
"serde_json",
"smallvec",
@@ -1938,7 +1932,7 @@ dependencies = [
"oxc_allocator",
"oxc_data_structures",
"oxc_span",
"rustc-hash 2.1.2",
"rustc-hash",
"serde_json",
"unicode-width",
]
@@ -2016,7 +2010,7 @@ dependencies = [
"oxc_span",
"oxc_str",
"oxc_syntax",
"rustc-hash 2.1.2",
"rustc-hash",
]
[[package]]
@@ -2025,7 +2019,7 @@ version = "0.138.0"
dependencies = [
"oxc_ast",
"oxc_span",
"rustc-hash 2.1.2",
"rustc-hash",
]
[[package]]
@@ -2035,7 +2029,7 @@ dependencies = [
"cow-utils",
"futures",
"papaya",
"rustc-hash 2.1.2",
"rustc-hash",
"serde",
"serde_json",
"tokio",
@@ -2089,7 +2083,7 @@ dependencies = [
"phf",
"rayon",
"rust-lapper",
"rustc-hash 2.1.2",
"rustc-hash",
"self_cell",
"serde",
"serde_json",
@@ -2107,7 +2101,7 @@ dependencies = [
"proc-macro2",
"project-root",
"quote",
"rustc-hash 2.1.2",
"rustc-hash",
"syn",
]
@@ -2136,7 +2130,7 @@ dependencies = [
"oxc_span",
"oxc_str",
"oxc_syntax",
"rustc-hash 2.1.2",
"rustc-hash",
]
[[package]]
@@ -2165,7 +2159,7 @@ dependencies = [
"oxc_syntax",
"oxc_traverse",
"pico-args",
"rustc-hash 2.1.2",
"rustc-hash",
]
[[package]]
@@ -2204,7 +2198,7 @@ dependencies = [
"oxc_tasks_common",
"oxc_transformer_plugins",
"pico-args",
"rustc-hash 2.1.2",
"rustc-hash",
"similar-asserts",
]
@@ -2242,7 +2236,7 @@ dependencies = [
"oxc_str",
"oxc_syntax",
"pico-args",
"rustc-hash 2.1.2",
"rustc-hash",
"seq-macro",
]
@@ -2260,7 +2254,7 @@ dependencies = [
"oxc_estree_tokens",
"oxc_napi",
"oxc_str",
"rustc-hash 2.1.2",
"rustc-hash",
]
[[package]]
@@ -2277,7 +2271,7 @@ dependencies = [
"oxc_linter",
"oxc_napi",
"oxc_transformer_plugins",
"rustc-hash 2.1.2",
"rustc-hash",
"serde",
"serde_json",
]
@@ -2299,7 +2293,7 @@ dependencies = [
"oxc_span",
"oxc_tasks_common",
"pico-args",
"rustc-hash 2.1.2",
"rustc-hash",
"serde_json",
"similar 3.1.1",
"walkdir",
@@ -2320,7 +2314,7 @@ dependencies = [
"oxc_semantic",
"oxc_span",
"oxc_syntax",
"rustc-hash 2.1.2",
"rustc-hash",
]
[[package]]
@@ -2335,7 +2329,7 @@ dependencies = [
"oxc_span",
"oxc_str",
"phf",
"rustc-hash 2.1.2",
"rustc-hash",
"unicode-id-start",
]
@@ -2355,7 +2349,7 @@ dependencies = [
"nodejs-built-in-modules",
"once_cell",
"percent-encoding",
"rustc-hash 2.1.2",
"rustc-hash",
"rustix",
"self_cell",
"serde",
@@ -2399,7 +2393,7 @@ dependencies = [
"oxc_span",
"oxc_str",
"oxc_syntax",
"rustc-hash 2.1.2",
"rustc-hash",
"self_cell",
"serde_json",
"smallvec",
@@ -2416,7 +2410,7 @@ dependencies = [
"napi",
"napi-build",
"napi-derive",
"rustc-hash 2.1.2",
"rustc-hash",
"serde",
"serde_json",
]
@@ -2489,7 +2483,7 @@ dependencies = [
"oxc_semantic",
"oxc_str",
"oxc_syntax",
"rustc-hash 2.1.2",
"rustc-hash",
]
[[package]]
@@ -2533,7 +2527,7 @@ dependencies = [
"oxc",
"oxc_napi",
"oxc_sourcemap",
"rustc-hash 2.1.2",
"rustc-hash",
]
[[package]]
@@ -2564,7 +2558,7 @@ dependencies = [
"oxc_syntax",
"oxc_traverse",
"pico-args",
"rustc-hash 2.1.2",
"rustc-hash",
"serde",
"serde_json",
]
@@ -2593,7 +2587,7 @@ dependencies = [
"oxc_transformer",
"oxc_traverse",
"pico-args",
"rustc-hash 2.1.2",
"rustc-hash",
]
[[package]]
@@ -2610,7 +2604,7 @@ dependencies = [
"oxc_span",
"oxc_str",
"oxc_syntax",
"rustc-hash 2.1.2",
"rustc-hash",
]
[[package]]
@@ -2657,7 +2651,7 @@ dependencies = [
"oxc_span",
"phf",
"rayon",
"rustc-hash 2.1.2",
"rustc-hash",
"serde",
"serde_json",
"simdutf8",
@@ -2698,7 +2692,7 @@ dependencies = [
"oxc_span",
"oxc_str",
"rayon",
"rustc-hash 2.1.2",
"rustc-hash",
"serde",
"serde_json",
"tempfile",
@@ -3054,18 +3048,6 @@ dependencies = [
"str_indices",
]
[[package]]
name = "rowan"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "417a3a9f582e349834051b8a10c8d71ca88da4211e4093528e36b9845f6b5f21"
dependencies = [
"countme",
"hashbrown 0.14.5",
"rustc-hash 1.1.0",
"text-size",
]
[[package]]
name = "rulegen"
version = "0.0.0"
@@ -3081,7 +3063,7 @@ dependencies = [
"oxc_span",
"oxc_tasks_common",
"proc-macro2",
"rustc-hash 2.1.2",
"rustc-hash",
"serde",
"serde_json",
"syn",
@@ -3096,12 +3078,6 @@ dependencies = [
"num-traits",
]
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc-hash"
version = "2.1.2"
@@ -3118,7 +3094,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -3138,9 +3114,9 @@ dependencies = [
[[package]]
name = "rustls-pki-types"
version = "1.14.1"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046"
dependencies = [
"zeroize",
]
@@ -3213,7 +3189,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b55fb86dfd3a2f5f76ea78310a88f96c4ea21a3031f8d212443d56123fd0521"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -3497,15 +3473,9 @@ dependencies = [
"getrandom 0.4.3",
"once_cell",
"rustix",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
name = "text-size"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233"
[[package]]
name = "textwrap"
version = "0.16.2"
@@ -3548,9 +3518,9 @@ dependencies = [
[[package]]
name = "time"
version = "0.3.51"
version = "0.3.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327"
checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50"
dependencies = [
"deranged",
"num-conv",
@@ -3568,9 +3538,9 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
[[package]]
name = "time-macros"
version = "0.2.30"
version = "0.2.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcef1a61bdb119096e153208ec5cbec23944ce8bca13be5c7f60c634f7403935"
checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f"
dependencies = [
"num-conv",
"time-core",
@@ -3974,7 +3944,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
+1 -1
View File
@@ -182,7 +182,7 @@ oxc_sourcemap = "8.0.2" # Source map generation
#
allocator-api2 = "=0.2.21" # Allocator trait
oxc-graphql-parser = "0.0.1" # GraphQL parser (lossless CST; apollo-parser 0.8.6 fork with opt-in 2025 descriptions + legacy fragment variables)
oxc-graphql-parser = "0.0.4" # GraphQL parser (direct AST; apollo-parser 0.8.6 fork with opt-in 2025 descriptions + legacy fragment variables)
base64 = "0.22.1" # Base64 encoding
bitflags = "2.10.0" # Bitflag types
bpaf = "0.9.20" # CLI parser
+16 -11
View File
@@ -33,26 +33,31 @@ For Prettier(v3.9)'s graphql-js v17.x syntax support, see the Roadmap below.
`format()` / `format_to_ir()` return `Err` whenever they cannot produce output they can stand behind:
- oxc-graphql-parser is error-tolerant (returns a CST even for invalid input),
but any parse error bails out; never format a broken CST
- print-stage internal errors are also `Err`
- `oxc-graphql-parser` is error-tolerant (returns an AST even for invalid input), but any parse error bails out; never format a broken AST.
- Several printer shortcuts (e.g. `close_delim_start`) are sound only under this guarantee
- The caller (oxfmt) decides what happens next
(diagnostics for standalone files, template-as-is for embedded)
- Diagnostics for standalone files, template-as-is for embedded
### Comments
`graphql-js` does not attach comments to the AST;
Prettier collects them from the token stream and attaches leading/trailing/dangling per node.
This crate instead collects `COMMENT` trivia tokens from the CST into a positional cursor (`src/comments.rs`, mirrors `oxc_formatter_json`) and flushes them at sequence items, closing delimiters, and document tail.
This crate instead collects comment spans into a positional cursor and flushes them at sequence items, closing delimiters, and document tail.
`oxc-graphql-parser` attaches pending trivia to whichever node is open when the next significant token is consumed, so node ranges may start at a preceding comment.
All layout decisions use significant-token positions (`sig_start` / `sig_end` in `src/print/mod.rs`), never `text_range()` directly.
Node spans are significant-token spans (trivia is never included), so layout decisions use them directly.
All span bridging lives in `src/print/span.rs`:
- the usize→u32 conversion (`to_span`), the crate-local `Spanned` trait
- (the `oxc_span::Span` facade over the parser's own `span` fields / `span()` accessors)
- and closing-delimiter derivation: `close_delim_start` for containers whose span ends one past their `}` / `]`
- `find_close_after` for paren lists that have no wrapper node
### Strings
Prettier prints `StringValue` from `graphql-js`'s _cooked_ value and re-encodes it.
`oxc-graphql-parser` hands us raw source, so `src/print/value.rs` reimplements:
`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:
- the GraphQL spec `BlockStringValue` algorithm (dedent + blank-line trimming)
- escape decoding for regular strings (incl. surrogate pairs)
@@ -83,7 +88,7 @@ Run `clippy` and resolve all warnings.
### Fixtures tests
Snapshot tests driven by fixture files under `tests/fixtures/format/`,
Snapshot tests driven by fixture files under `tests/fixtures/graphql/`,
covering what the Prettier conformance suite does not:
`# oxfmt-ignore` suppression, blank-line runs inside block strings,
string escape re-encoding (incl. the `\r` divergence), empty `[]` / `{}` values,
@@ -139,7 +144,7 @@ npx prettier --parser=graphql [filename]
A good large real-world stress input is GitHub's public GraphQL schema (~72k lines).
It is too large and third-party to commit as a fixture,
bug-catching shapes are distilled into `tests/fixtures/format/implements-width.graphql`):
bug-catching shapes are distilled into `tests/fixtures/graphql/implements-width.graphql`):
```sh
curl -sL https://docs.github.com/public/fpt/schema.docs.graphql -o /tmp/github-schema.graphql
@@ -159,7 +164,7 @@ These are in Prettier's unreleased changelog (main has them, next stable will).
- [#18582](https://github.com/prettier/prettier/blob/main/changelog_unreleased/graphql/18582.md):
allow `implements` lists to break. We currently implement never break
(see `tests/fixtures/format/implements-width.graphql`),
(see `tests/fixtures/graphql/implements-width.graphql`),
so this is a layout divergence that will become incompatible, not a new-syntax item, lands sooner.
- [#19171](https://github.com/prettier/prettier/blob/main/changelog_unreleased/graphql/19171.md):
directives on directive definitions (`directive @a @b on QUERY`) + `extend directive`.
+28 -35
View File
@@ -5,26 +5,29 @@ use oxc_formatter_core::{
builders::{hard_line_break, text},
write,
};
use oxc_graphql_parser::{Parser, SyntaxKind, cst, cst::CstNode};
use oxc_graphql_parser::{Parser, ast::Document as GraphQLDocument};
use oxc_span::Span;
use crate::{
context::GraphqlFormatContext,
options::GraphqlFormatOptions,
print::{self, GraphqlFormatter},
print::{self, GraphqlFormatter, to_span},
};
/// Parse `source_text` as a GraphQL document and build its formatter IR.
///
/// # Errors
/// Returns an [`OxcDiagnostic`] when the parse produces any error.
/// oxc-graphql-parser is error-tolerant (it returns a CST even for invalid input),
/// but a CST with errors cannot be formatted faithfully, so a single error is enough to bail out.
/// `oxc-graphql-parser` is error-tolerant (it still returns an AST for invalid input),
/// but an AST with errors cannot be formatted faithfully, so a single error is enough to bail out.
pub fn format<'a>(
allocator: &'a Allocator,
source_text: &str,
options: GraphqlFormatOptions,
) -> Result<Formatted<'a, GraphqlFormatContext<'a>>, OxcDiagnostic> {
// Checked against the original input: `parse_document` strips the BOM
// before copying into the arena, so spans and gap scans never see it.
let has_bom = source_text.starts_with('\u{feff}');
let (document, source, comments) = parse_document(allocator, source_text)?;
let context = GraphqlFormatContext::new(options, source, comments);
@@ -32,8 +35,7 @@ pub fn format<'a>(
// TODO: Use `with_capacity` for perf, like `oxc_formatter` does
let mut buffer = VecBuffer::new(&mut state);
let has_bom = source.starts_with('\u{feff}');
write!(&mut buffer, FormatGraphqlRoot { document: &document, has_bom });
write!(&mut buffer, FormatGraphqlRoot { document, has_bom });
let elements = buffer.into_vec();
let context = state.into_context();
@@ -68,63 +70,54 @@ pub fn format_to_ir<'a>(
let mut state = FormatState::new(context, allocator);
let mut buffer = VecBuffer::new(&mut state);
write!(&mut buffer, FormatGraphqlEmbedded { document: &document });
write!(&mut buffer, FormatGraphqlEmbedded { document });
// GraphQL never collects Tailwind classes.
Ok(EmbeddedIr { ir: buffer.into_vec(), tailwind_classes: Vec::new() })
}
/// Parse the source into a CST and collect comment trivia,
/// Parse the source into a direct AST and collect comment trivia,
/// bailing out on any parse error.
///
/// Copies the source into the arena so every slice taken from it carries `'a`.
/// Copies the source into the arena so every slice taken from it carries `'a`,
/// stripping any BOM first so offset-based scans never see it
/// (the caller re-emits it from `has_bom`).
fn parse_document<'a>(
allocator: &'a Allocator,
source_text: &str,
) -> Result<(cst::Document, &'a str, &'a [Span]), OxcDiagnostic> {
let source: &'a str = allocator.alloc_str(source_text);
) -> Result<(&'a GraphQLDocument<'a>, &'a str, &'a [Span]), OxcDiagnostic> {
let source: &'a str =
allocator.alloc_str(source_text.strip_prefix('\u{feff}').unwrap_or(source_text));
// Opt-in graphql-js 16 syntax
let tree = Parser::new(source)
let ast = Parser::new(allocator, source)
.allow_executable_descriptions(true)
.allow_legacy_fragment_variables(true)
.parse();
if let Some(error) = tree.errors().next() {
if let Some(error) = ast.errors().next() {
let start = u32::try_from(error.index()).unwrap_or(0);
let len = u32::try_from(error.data().len()).unwrap_or(0);
return Err(OxcDiagnostic::error(format!("Syntax error: {}", error.message()))
.with_label(Span::sized(start, len)));
}
let document = tree.document();
let comments: &'a [Span] =
ArenaVec::from_iter_in(ast.comments().iter().copied().map(to_span), &allocator)
.into_arena_slice();
// Collect comment trivia in document order.
// GraphQL comments are `# ...` line comments.
let comments: &'a [Span] = ArenaVec::from_iter_in(
document
.syntax()
.descendants_with_tokens()
.filter_map(oxc_graphql_parser::SyntaxElement::into_token)
.filter(|t| t.kind() == SyntaxKind::COMMENT)
.map(|t| {
let range = t.text_range();
Span::new(range.start().into(), range.end().into())
}),
&allocator,
)
.into_arena_slice();
let document = allocator.alloc(ast.into_root());
Ok((document, source, comments))
}
/// Emits the document's definitions followed by any trailing comments,
/// and the final newline.
struct FormatGraphqlRoot<'b> {
document: &'b cst::Document,
struct FormatGraphqlRoot<'a> {
document: &'a GraphQLDocument<'a>,
has_bom: bool,
}
impl<'a> Format<'a, GraphqlFormatContext<'a>> for FormatGraphqlRoot<'_> {
impl<'a> Format<'a, GraphqlFormatContext<'a>> for FormatGraphqlRoot<'a> {
fn fmt(&self, f: &mut GraphqlFormatter<'_, 'a>) {
if self.has_bom {
write!(f, text("\u{feff}"));
@@ -139,11 +132,11 @@ impl<'a> Format<'a, GraphqlFormatContext<'a>> for FormatGraphqlRoot<'_> {
/// Emits the document's definitions and trailing comments only;
/// no BOM, no final newline (the parent document owns the surrounding layout).
struct FormatGraphqlEmbedded<'b> {
document: &'b cst::Document,
struct FormatGraphqlEmbedded<'a> {
document: &'a GraphQLDocument<'a>,
}
impl<'a> Format<'a, GraphqlFormatContext<'a>> for FormatGraphqlEmbedded<'_> {
impl<'a> Format<'a, GraphqlFormatContext<'a>> for FormatGraphqlEmbedded<'a> {
fn fmt(&self, f: &mut GraphqlFormatter<'_, 'a>) {
print::write_document(self.document, f);
}
+94 -128
View File
@@ -1,8 +1,6 @@
//! Shared printers: names, descriptions, directives, argument lists,
//! variable definitions, types, and input value definitions.
use oxc_graphql_parser::{cst, cst::CstNode};
use oxc_formatter_core::{
Buffer, Format,
builders::{
@@ -11,6 +9,10 @@ use oxc_formatter_core::{
},
write,
};
use oxc_graphql_parser::ast::{
Argument, Directive, InputValueDefinition, Name, NamedType, StringValue, Type, Value, Variable,
VariableDefinition,
};
use crate::comments::{
flush_leading_comments, flush_trailing_inside_comments, write_dangling_comments,
@@ -18,39 +20,36 @@ use crate::comments::{
use super::{
GraphqlFormatter, SeparatorKind, format_with,
sig::{closing_token_start, node_text},
span::{Spanned, find_close_after, to_span},
string, value, write_sequence,
};
pub fn write_name(name: &cst::Name, f: &mut GraphqlFormatter<'_, '_>) {
write!(f, text(node_text(f, name.syntax())));
}
/// Whether a description string is a block string (`"""..."""`).
fn is_block_string(sv: &cst::StringValue, f: &GraphqlFormatter<'_, '_>) -> bool {
node_text(f, sv.syntax()).starts_with("\"\"\"")
pub(super) fn write_name<'a>(name: &Name<'a>, f: &mut GraphqlFormatter<'_, 'a>) {
write!(f, text(name.value));
}
/// Description followed by a hard line break (the default placement).
pub fn write_description(description: Option<cst::Description>, f: &mut GraphqlFormatter<'_, '_>) {
let Some(description) = description else { return };
let Some(sv) = description.string_value() else { return };
string::write_string_value(&sv, f);
pub(super) fn write_description<'a>(
description: Option<&StringValue<'a>>,
f: &mut GraphqlFormatter<'_, 'a>,
) {
let Some(sv) = description else { return };
string::write_string_value(sv, f);
write!(f, hard_line_break());
}
/// Description placement for `InputValueDefinition`:
/// non-block descriptions are followed by a soft line (they may stay inline in an
/// argument list), block descriptions by a hard line break.
/// Mirrors Prettier's `printDescription`.
fn write_description_input_value(
description: Option<cst::Description>,
f: &mut GraphqlFormatter<'_, '_>,
/// Description placement for `InputValueDefinition`:
/// non-block descriptions are followed by a soft line
/// (they may stay inline in an argument list),
/// block descriptions by a hard line break.
fn write_description_input_value<'a>(
description: Option<&StringValue<'a>>,
f: &mut GraphqlFormatter<'_, 'a>,
) {
let Some(description) = description else { return };
let Some(sv) = description.string_value() else { return };
let is_block = is_block_string(&sv, f);
string::write_string_value(&sv, f);
let Some(sv) = description else { return };
let is_block = sv.block;
string::write_string_value(sv, f);
if is_block {
write!(f, hard_line_break());
} else {
@@ -60,26 +59,24 @@ fn write_description_input_value(
/// Directive placement style. Mirrors Prettier's `printDirectives`.
#[derive(Clone, Copy, Eq, PartialEq)]
pub enum DirectivesStyle {
pub(super) enum DirectivesStyle {
/// On `OperationDefinition` / `FragmentDefinition`: `group([line, joined])`.
Definition,
/// Everywhere else: `[" ", group(indent([softline, joined]))]`.
Attached,
}
pub fn write_directives<'a>(
directives: Option<cst::Directives>,
pub(super) fn write_directives<'a>(
directives: &'a [Directive<'a>],
style: DirectivesStyle,
f: &mut GraphqlFormatter<'_, 'a>,
) {
let Some(directives) = directives else { return };
let list: Vec<cst::Directive> = directives.directives().collect();
if list.is_empty() {
if directives.is_empty() {
return;
}
let joined = format_with(|f: &mut GraphqlFormatter<'_, 'a>| {
for (i, directive) in list.iter().enumerate() {
let joined = format_with(move |f: &mut GraphqlFormatter<'_, 'a>| {
for (i, directive) in directives.iter().enumerate() {
if i > 0 {
write!(f, soft_line_break_or_space());
}
@@ -105,19 +102,17 @@ pub fn write_directives<'a>(
}
}
fn write_directive(directive: &cst::Directive, f: &mut GraphqlFormatter<'_, '_>) {
fn write_directive<'a>(directive: &'a Directive<'a>, f: &mut GraphqlFormatter<'_, 'a>) {
write!(f, "@");
if let Some(name) = directive.name() {
write_name(&name, f);
}
write_arguments(directive.arguments(), f);
write_name(&directive.name, f);
write_arguments(&directive.arguments, f);
}
/// Close an empty delimited container (`[]`, `{}`, `{ }` selection set): drains any comments
/// pending before `close_start`, emits them block-indented when present, then writes `close`.
/// The caller has already written the opening delimiter. Sibling of [`write_paren_list`] /
/// `write_braced_body` for the empty case.
pub fn write_empty_delimited<'a>(
pub(super) fn write_empty_delimited<'a>(
close_start: u32,
close: &'static str,
f: &mut GraphqlFormatter<'_, 'a>,
@@ -136,17 +131,22 @@ pub fn write_empty_delimited<'a>(
/// A parenthesized, comma-soft-separated list:
/// `group(["(", indent([softline, join(...)]), softline, ")"])`.
/// Comments pending before `)` are flushed inside the group body.
pub fn write_paren_list<'a, T, F>(
/// Emits nothing for an empty list.
/// Comments pending before `)` are flushed inside the group body;
/// the `)` position is derived by scanning past trivia
/// from the last item's end (paren lists have no wrapper node carrying it).
pub(super) fn write_paren_list<'a, T, F>(
f: &mut GraphqlFormatter<'_, 'a>,
items: &[T],
r_paren_start: u32,
preserve_blank: bool,
write_item: F,
) where
T: CstNode,
T: Spanned,
F: Fn(usize, &mut GraphqlFormatter<'_, 'a>),
{
let Some(last) = items.last() else { return };
let r_paren_start = find_close_after(&f.context().source_text(), last.span().end, b')');
write!(f, "(");
let body = format_with(|f: &mut GraphqlFormatter<'_, 'a>| {
let last_end =
@@ -160,119 +160,87 @@ pub fn write_paren_list<'a, T, F>(
/// `(arg: value, ...)` on fields, directives, and fragment spreads.
/// Blank lines between arguments are preserved (Prettier routes these through `printSequence`).
pub fn write_arguments(arguments: Option<cst::Arguments>, f: &mut GraphqlFormatter<'_, '_>) {
let Some(arguments) = arguments else { return };
let items: Vec<cst::Argument> = arguments.arguments().collect();
if items.is_empty() {
return;
}
let r_paren = closing_token_start(arguments.r_paren_token(), arguments.syntax());
write_paren_list(f, &items, r_paren, true, |i, f| {
let argument = &items[i];
if let Some(name) = argument.name() {
write_name(&name, f);
}
pub(super) fn write_arguments<'a>(arguments: &'a [Argument<'a>], f: &mut GraphqlFormatter<'_, 'a>) {
write_paren_list(f, arguments, true, |i, f| {
let argument = &arguments[i];
write_name(&argument.name, f);
write!(f, ": ");
if let Some(v) = argument.value() {
value::write_value(&v, f);
if let Some(v) = argument.value.as_ref() {
value::write_value(v, f);
}
});
}
/// `($var: Type = default, ...)` on operations.
/// No blank-line preservation (Prettier uses a plain `path.map` here).
pub fn write_variable_definitions(
variable_definitions: Option<cst::VariableDefinitions>,
f: &mut GraphqlFormatter<'_, '_>,
pub(super) fn write_variable_definitions<'a>(
variable_definitions: &'a [VariableDefinition<'a>],
f: &mut GraphqlFormatter<'_, 'a>,
) {
let Some(variable_definitions) = variable_definitions else { return };
let items: Vec<cst::VariableDefinition> = variable_definitions.variable_definitions().collect();
if items.is_empty() {
return;
}
let r_paren =
closing_token_start(variable_definitions.r_paren_token(), variable_definitions.syntax());
write_paren_list(f, &items, r_paren, false, |i, f| {
write_variable_definition(&items[i], f);
write_paren_list(f, variable_definitions, false, |i, f| {
write_variable_definition(&variable_definitions[i], f);
});
}
fn write_variable_definition(
variable_definition: &cst::VariableDefinition,
f: &mut GraphqlFormatter<'_, '_>,
fn write_variable_definition<'a>(
variable_definition: &'a VariableDefinition<'a>,
f: &mut GraphqlFormatter<'_, 'a>,
) {
write_description(variable_definition.description(), f);
if let Some(variable) = variable_definition.variable() {
write_variable(&variable, f);
}
write_description(variable_definition.description.as_ref(), f);
write_variable(&variable_definition.variable, f);
write!(f, ": ");
if let Some(ty) = variable_definition.ty() {
write_type(&ty, f);
if let Some(ty) = variable_definition.ty.as_ref() {
write_type(ty, f);
}
write_default_value(variable_definition.default_value(), f);
write_directives(variable_definition.directives(), DirectivesStyle::Attached, f);
write_default_value(variable_definition.default_value.as_ref(), f);
write_directives(&variable_definition.directives, DirectivesStyle::Attached, f);
}
pub fn write_variable(variable: &cst::Variable, f: &mut GraphqlFormatter<'_, '_>) {
pub(super) fn write_variable<'a>(variable: &Variable<'a>, f: &mut GraphqlFormatter<'_, 'a>) {
write!(f, "$");
if let Some(name) = variable.name() {
write_name(&name, f);
}
write_name(&variable.name, f);
}
fn write_default_value(default_value: Option<cst::DefaultValue>, f: &mut GraphqlFormatter<'_, '_>) {
let Some(default_value) = default_value else { return };
fn write_default_value<'a>(default_value: Option<&'a Value<'a>>, f: &mut GraphqlFormatter<'_, 'a>) {
let Some(v) = default_value else { return };
write!(f, " = ");
if let Some(v) = default_value.value() {
value::write_value(&v, f);
}
value::write_value(v, f);
}
pub fn write_type(ty: &cst::Type, f: &mut GraphqlFormatter<'_, '_>) {
pub(super) fn write_type<'a>(ty: &Type<'a>, f: &mut GraphqlFormatter<'_, 'a>) {
match ty {
cst::Type::NamedType(named) => write_named_type(named, f),
cst::Type::ListType(list) => write_list_type(list, f),
cst::Type::NonNullType(non_null) => {
if let Some(named) = non_null.named_type() {
write_named_type(&named, f);
} else if let Some(list) = non_null.list_type() {
write_list_type(&list, f);
}
Type::Named(named) => write_named_type(named, f),
Type::List(list) => {
write!(f, "[");
write_type(&list.ty, f);
write!(f, "]");
}
Type::NonNull(non_null) => {
write_type(&non_null.ty, f);
write!(f, "!");
}
Type::Missing(_) => {}
}
}
pub fn write_named_type(named: &cst::NamedType, f: &mut GraphqlFormatter<'_, '_>) {
if let Some(name) = named.name() {
write_name(&name, f);
}
}
fn write_list_type(list: &cst::ListType, f: &mut GraphqlFormatter<'_, '_>) {
write!(f, "[");
if let Some(inner) = list.ty() {
write_type(&inner, f);
}
write!(f, "]");
pub(super) fn write_named_type<'a>(named: &NamedType<'a>, f: &mut GraphqlFormatter<'_, 'a>) {
write_name(&named.name, f);
}
/// `name: Type = default @dir` (+ leading description) inside
/// `ArgumentsDefinition` / `InputFieldsDefinition`.
pub fn write_input_value_definition(
input_value: &cst::InputValueDefinition,
f: &mut GraphqlFormatter<'_, '_>,
pub(super) fn write_input_value_definition<'a>(
input_value: &'a InputValueDefinition<'a>,
f: &mut GraphqlFormatter<'_, 'a>,
) {
write_description_input_value(input_value.description(), f);
if let Some(name) = input_value.name() {
write_name(&name, f);
}
write_description_input_value(input_value.description.as_ref(), f);
write_name(&input_value.name, f);
write!(f, ": ");
if let Some(ty) = input_value.ty() {
write_type(&ty, f);
if let Some(ty) = input_value.ty.as_ref() {
write_type(ty, f);
}
write_default_value(input_value.default_value(), f);
write_directives(input_value.directives(), DirectivesStyle::Attached, f);
write_default_value(input_value.default_value.as_ref(), f);
write_directives(&input_value.directives, DirectivesStyle::Attached, f);
}
/// ` implements A & B`, mirroring Prettier 3.8.4's `printInterfaces`:
@@ -282,18 +250,16 @@ pub fn write_input_value_definition(
/// A `line` replaces the space only when a comment sits between two names;
/// outside any group it always prints as a newline,
/// so the list breaks exactly at the comment position (at zero extra indentation).
pub fn write_implements_interfaces(
implements: Option<cst::ImplementsInterfaces>,
f: &mut GraphqlFormatter<'_, '_>,
pub(super) fn write_implements_interfaces<'a>(
interfaces: &'a [NamedType<'a>],
f: &mut GraphqlFormatter<'_, 'a>,
) {
let Some(implements) = implements else { return };
let names: Vec<cst::NamedType> = implements.named_types().collect();
if names.is_empty() {
if interfaces.is_empty() {
return;
}
write!(f, " implements ");
for (i, named) in names.iter().enumerate() {
let start = super::sig::sig_start(named.syntax());
for (i, named) in interfaces.iter().enumerate() {
let start = to_span(named.name.span).start;
if i > 0 {
write!(f, " &");
// Pending comments before this name = comments between the two names
@@ -1,7 +1,5 @@
//! Document-level definition printers: operations, fragments, and the type system.
use oxc_graphql_parser::{cst, cst::CstNode};
use oxc_formatter_core::{
Buffer,
builders::{
@@ -10,6 +8,11 @@ use oxc_formatter_core::{
},
write,
};
use oxc_graphql_parser::ast::{
Definition, Directive, DirectiveDefinition, EnumValueDefinition, FieldDefinition,
FragmentDefinition, InputValueDefinition, Name, NamedType, OperationDefinition, OperationType,
RootOperationTypeDefinition, SchemaDefinition, SchemaExtension, StringValue,
};
use crate::comments::flush_trailing_inside_comments;
@@ -17,226 +20,234 @@ use super::{
GraphqlFormatter, SeparatorKind, common,
common::DirectivesStyle,
format_with, selection,
sig::{closing_token_start, node_text},
span::{Spanned, close_delim_start},
write_sequence,
};
pub fn write_definition(definition: &cst::Definition, f: &mut GraphqlFormatter<'_, '_>) {
pub(super) fn write_definition<'a>(
definition: &'a Definition<'a>,
f: &mut GraphqlFormatter<'_, 'a>,
) {
match definition {
cst::Definition::OperationDefinition(d) => write_operation_definition(d, f),
cst::Definition::FragmentDefinition(d) => write_fragment_definition(d, f),
cst::Definition::DirectiveDefinition(d) => write_directive_definition(d, f),
cst::Definition::SchemaDefinition(d) => write_schema_definition(d, f),
cst::Definition::ScalarTypeDefinition(d) => {
write_scalar_type(d.description(), false, d.name(), d.directives(), f);
Definition::Operation(d) => write_operation_definition(d, f),
Definition::Fragment(d) => write_fragment_definition(d, f),
Definition::Directive(d) => write_directive_definition(d, f),
Definition::Schema(d) => write_schema_definition(d, f),
Definition::SchemaExtension(d) => write_schema_extension(d, f),
Definition::ScalarType(d) => {
write_scalar_type(d.description.as_ref(), false, &d.name, &d.directives, f);
}
cst::Definition::ScalarTypeExtension(d) => {
write_scalar_type(None, true, d.name(), d.directives(), f);
Definition::ScalarTypeExtension(d) => {
write_scalar_type(None, true, &d.name, &d.directives, f);
}
cst::Definition::ObjectTypeDefinition(d) => write_object_like(
d.description(),
Definition::ObjectType(d) => write_object_like(
d.description.as_ref(),
false,
"type",
d.name(),
d.implements_interfaces(),
d.directives(),
d.fields_definition(),
&d.name,
&d.interfaces,
&d.directives,
&d.fields,
close_delim_start(d.span),
f,
),
cst::Definition::ObjectTypeExtension(d) => write_object_like(
Definition::ObjectTypeExtension(d) => write_object_like(
None,
true,
"type",
d.name(),
d.implements_interfaces(),
d.directives(),
d.fields_definition(),
&d.name,
&d.interfaces,
&d.directives,
&d.fields,
close_delim_start(d.span),
f,
),
cst::Definition::InterfaceTypeDefinition(d) => write_object_like(
d.description(),
Definition::InterfaceType(d) => write_object_like(
d.description.as_ref(),
false,
"interface",
d.name(),
d.implements_interfaces(),
d.directives(),
d.fields_definition(),
&d.name,
&d.interfaces,
&d.directives,
&d.fields,
close_delim_start(d.span),
f,
),
cst::Definition::InterfaceTypeExtension(d) => write_object_like(
Definition::InterfaceTypeExtension(d) => write_object_like(
None,
true,
"interface",
d.name(),
d.implements_interfaces(),
d.directives(),
d.fields_definition(),
&d.name,
&d.interfaces,
&d.directives,
&d.fields,
close_delim_start(d.span),
f,
),
cst::Definition::InputObjectTypeDefinition(d) => write_input_object_like(
d.description(),
Definition::InputObjectType(d) => write_input_object_like(
d.description.as_ref(),
false,
d.name(),
d.directives(),
d.input_fields_definition(),
&d.name,
&d.directives,
&d.fields,
close_delim_start(d.span),
f,
),
cst::Definition::InputObjectTypeExtension(d) => write_input_object_like(
Definition::InputObjectTypeExtension(d) => write_input_object_like(
None,
true,
d.name(),
d.directives(),
d.input_fields_definition(),
&d.name,
&d.directives,
&d.fields,
close_delim_start(d.span),
f,
),
cst::Definition::UnionTypeDefinition(d) => write_union_like(
d.description(),
false,
d.name(),
d.directives(),
d.union_member_types(),
f,
),
cst::Definition::UnionTypeExtension(d) => {
write_union_like(None, true, d.name(), d.directives(), d.union_member_types(), f);
Definition::UnionType(d) => {
write_union_like(d.description.as_ref(), false, &d.name, &d.directives, &d.members, f);
}
cst::Definition::EnumTypeDefinition(d) => write_enum_like(
d.description(),
Definition::UnionTypeExtension(d) => {
write_union_like(None, true, &d.name, &d.directives, &d.members, f);
}
Definition::EnumType(d) => write_enum_like(
d.description.as_ref(),
false,
d.name(),
d.directives(),
d.enum_values_definition(),
&d.name,
&d.directives,
&d.values,
close_delim_start(d.span),
f,
),
cst::Definition::EnumTypeExtension(d) => {
write_enum_like(None, true, d.name(), d.directives(), d.enum_values_definition(), f);
Definition::EnumTypeExtension(d) => {
write_enum_like(
None,
true,
&d.name,
&d.directives,
&d.values,
close_delim_start(d.span),
f,
);
}
cst::Definition::SchemaExtension(d) => write_schema_extension(d, f),
}
}
fn write_operation_definition(
operation: &cst::OperationDefinition,
f: &mut GraphqlFormatter<'_, '_>,
fn operation_type_keyword(kind: OperationType) -> &'static str {
match kind {
OperationType::Query => "query",
OperationType::Mutation => "mutation",
OperationType::Subscription => "subscription",
}
}
fn write_operation_definition<'a>(
operation: &'a OperationDefinition<'a>,
f: &mut GraphqlFormatter<'_, 'a>,
) {
common::write_description(operation.description(), f);
common::write_description(operation.description.as_ref(), f);
let has_operation = operation.operation_type().is_some();
let has_name = operation.name().is_some();
// Direct-AST `operation_type` is always set (shorthand `{ ... }` parses as Query).
// Shorthand means the operation's first significant token IS the selection set's `{`,
// so their span starts coincide. Mirrors Prettier's
// `locStart(node) !== locStart(node.selectionSet)` check.
let is_shorthand =
operation.selection_set.as_ref().is_some_and(|ss| ss.span.start == operation.span.start);
let has_operation = !is_shorthand;
let has_name = operation.name.is_some();
if let Some(operation_type) = operation.operation_type() {
write!(f, text(node_text(f, operation_type.syntax())));
if has_operation {
write!(f, text(operation_type_keyword(operation.operation_type)));
}
if let Some(name) = operation.name() {
if let Some(name) = operation.name.as_ref() {
write!(f, space());
common::write_name(&name, f);
common::write_name(name, f);
}
let has_variable_definitions = operation
.variable_definitions()
.is_some_and(|vd| vd.variable_definitions().next().is_some());
let has_variable_definitions = !operation.variable_definitions.is_empty();
if has_operation && !has_name && has_variable_definitions {
write!(f, space());
}
common::write_variable_definitions(operation.variable_definitions(), f);
common::write_directives(operation.directives(), DirectivesStyle::Definition, f);
common::write_variable_definitions(&operation.variable_definitions, f);
common::write_directives(&operation.directives, DirectivesStyle::Definition, f);
if has_operation || has_name {
write!(f, space());
}
if let Some(selection_set) = operation.selection_set() {
selection::write_selection_set(&selection_set, f);
if let Some(selection_set) = operation.selection_set.as_ref() {
selection::write_selection_set(selection_set, f);
}
}
fn write_fragment_definition(fragment: &cst::FragmentDefinition, f: &mut GraphqlFormatter<'_, '_>) {
common::write_description(fragment.description(), f);
write!(f, "fragment ");
if let Some(fragment_name) = fragment.fragment_name()
&& let Some(name) = fragment_name.name()
{
common::write_name(&name, f);
}
// Legacy fragment variables (graphql-js `allowLegacyFragmentVariables`).
common::write_variable_definitions(fragment.variable_definitions(), f);
if let Some(type_condition) = fragment.type_condition()
&& let Some(named) = type_condition.named_type()
{
write!(f, " on ");
common::write_named_type(&named, f);
}
common::write_directives(fragment.directives(), DirectivesStyle::Definition, f);
write!(f, space());
if let Some(selection_set) = fragment.selection_set() {
selection::write_selection_set(&selection_set, f);
}
}
fn write_directive_definition(
directive: &cst::DirectiveDefinition,
f: &mut GraphqlFormatter<'_, '_>,
fn write_fragment_definition<'a>(
fragment: &'a FragmentDefinition<'a>,
f: &mut GraphqlFormatter<'_, 'a>,
) {
common::write_description(directive.description(), f);
write!(f, "directive @");
if let Some(name) = directive.name() {
common::write_name(&name, f);
common::write_description(fragment.description.as_ref(), f);
write!(f, "fragment ");
common::write_name(&fragment.name, f);
// Legacy fragment variables (graphql-js `allowLegacyFragmentVariables`).
common::write_variable_definitions(&fragment.variable_definitions, f);
write!(f, " on ");
common::write_named_type(&fragment.type_condition, f);
common::write_directives(&fragment.directives, DirectivesStyle::Definition, f);
write!(f, space());
if let Some(selection_set) = fragment.selection_set.as_ref() {
selection::write_selection_set(selection_set, f);
}
write_arguments_definition(directive.arguments_definition(), f);
if directive.repeatable_token().is_some() {
}
fn write_directive_definition<'a>(
directive: &'a DirectiveDefinition<'a>,
f: &mut GraphqlFormatter<'_, 'a>,
) {
common::write_description(directive.description.as_ref(), f);
write!(f, "directive @");
common::write_name(&directive.name, f);
write_arguments_definition(&directive.arguments, f);
if directive.repeatable {
write!(f, " repeatable");
}
write!(f, " on ");
if let Some(locations) = directive.directive_locations() {
let list: Vec<cst::DirectiveLocation> = locations.directive_locations().collect();
for (i, location) in list.iter().enumerate() {
if i > 0 {
write!(f, " | ");
}
write!(f, text(node_text(f, location.syntax())));
let locations = directive.locations.as_slice();
for (i, location) in locations.iter().enumerate() {
if i > 0 {
write!(f, " | ");
}
write!(f, text(location.name));
}
}
fn write_schema_definition(schema: &cst::SchemaDefinition, f: &mut GraphqlFormatter<'_, '_>) {
common::write_description(schema.description(), f);
fn write_schema_definition<'a>(schema: &'a SchemaDefinition<'a>, f: &mut GraphqlFormatter<'_, 'a>) {
common::write_description(schema.description.as_ref(), f);
write!(f, "schema");
common::write_directives(schema.directives(), DirectivesStyle::Attached, f);
common::write_directives(&schema.directives, DirectivesStyle::Attached, f);
write!(f, " {");
let operation_types: Vec<cst::RootOperationTypeDefinition> =
schema.root_operation_type_definitions().collect();
let operation_types = schema.root_operations.as_slice();
if operation_types.is_empty() {
write!(f, [hard_line_break(), "}"]);
} else {
let r_curly = closing_token_start(schema.r_curly_token(), schema.syntax());
write_braced_body(f, &operation_types, r_curly, |i, f| {
write_braced_body(f, operation_types, close_delim_start(schema.span), |i, f| {
write_root_operation_type_definition(&operation_types[i], f);
});
}
}
fn write_schema_extension(schema: &cst::SchemaExtension, f: &mut GraphqlFormatter<'_, '_>) {
fn write_schema_extension<'a>(schema: &'a SchemaExtension<'a>, f: &mut GraphqlFormatter<'_, 'a>) {
write!(f, "extend schema");
common::write_directives(schema.directives(), DirectivesStyle::Attached, f);
let operation_types: Vec<cst::RootOperationTypeDefinition> =
schema.root_operation_type_definitions().collect();
common::write_directives(&schema.directives, DirectivesStyle::Attached, f);
let operation_types = schema.root_operations.as_slice();
if !operation_types.is_empty() {
write!(f, " {");
let r_curly = closing_token_start(schema.r_curly_token(), schema.syntax());
write_braced_body(f, &operation_types, r_curly, |i, f| {
write_braced_body(f, operation_types, close_delim_start(schema.span), |i, f| {
write_root_operation_type_definition(&operation_types[i], f);
});
}
}
fn write_root_operation_type_definition(
operation_type_definition: &cst::RootOperationTypeDefinition,
f: &mut GraphqlFormatter<'_, '_>,
fn write_root_operation_type_definition<'a>(
def: &'a RootOperationTypeDefinition<'a>,
f: &mut GraphqlFormatter<'_, 'a>,
) {
if let Some(operation_type) = operation_type_definition.operation_type() {
write!(f, text(node_text(f, operation_type.syntax())));
}
write!(f, text(operation_type_keyword(def.operation_type)));
write!(f, ": ");
if let Some(named) = operation_type_definition.named_type() {
common::write_named_type(&named, f);
}
common::write_named_type(&def.named_type, f);
}
/// The body of an already-opened `{`: a hard-line sequence (blank lines preserved),
@@ -247,7 +258,7 @@ fn write_braced_body<'a, T, F>(
r_curly_start: u32,
write_item: F,
) where
T: CstNode,
T: Spanned,
F: Fn(usize, &mut GraphqlFormatter<'_, 'a>),
{
let body = format_with(|f: &mut GraphqlFormatter<'_, 'a>| {
@@ -259,169 +270,143 @@ fn write_braced_body<'a, T, F>(
write!(f, [block_indent(&body), "}"]);
}
fn write_scalar_type(
description: Option<cst::Description>,
fn write_scalar_type<'a>(
description: Option<&StringValue<'a>>,
extend: bool,
name: Option<cst::Name>,
directives: Option<cst::Directives>,
f: &mut GraphqlFormatter<'_, '_>,
name: &Name<'a>,
directives: &'a [Directive<'a>],
f: &mut GraphqlFormatter<'_, 'a>,
) {
common::write_description(description, f);
if extend {
write!(f, "extend ");
}
write!(f, "scalar ");
if let Some(name) = name {
common::write_name(&name, f);
}
common::write_name(name, f);
common::write_directives(directives, DirectivesStyle::Attached, f);
}
#[expect(clippy::too_many_arguments)]
fn write_object_like(
description: Option<cst::Description>,
fn write_object_like<'a>(
description: Option<&StringValue<'a>>,
extend: bool,
keyword: &'static str,
name: Option<cst::Name>,
implements: Option<cst::ImplementsInterfaces>,
directives: Option<cst::Directives>,
fields: Option<cst::FieldsDefinition>,
f: &mut GraphqlFormatter<'_, '_>,
name: &Name<'a>,
interfaces: &'a [NamedType<'a>],
directives: &'a [Directive<'a>],
fields: &'a [FieldDefinition<'a>],
r_curly_start: u32,
f: &mut GraphqlFormatter<'_, 'a>,
) {
common::write_description(description, f);
if extend {
write!(f, "extend ");
}
write!(f, [keyword, space()]);
if let Some(name) = name {
common::write_name(&name, f);
}
common::write_implements_interfaces(implements, f);
common::write_name(name, f);
common::write_implements_interfaces(interfaces, f);
common::write_directives(directives, DirectivesStyle::Attached, f);
if let Some(fields) = fields {
let items: Vec<cst::FieldDefinition> = fields.field_definitions().collect();
if !items.is_empty() {
write!(f, " {");
let r_curly = closing_token_start(fields.r_curly_token(), fields.syntax());
write_braced_body(f, &items, r_curly, |i, f| {
write_field_definition(&items[i], f);
});
}
if !fields.is_empty() {
write!(f, " {");
write_braced_body(f, fields, r_curly_start, |i, f| {
write_field_definition(&fields[i], f);
});
}
}
fn write_input_object_like(
description: Option<cst::Description>,
fn write_input_object_like<'a>(
description: Option<&StringValue<'a>>,
extend: bool,
name: Option<cst::Name>,
directives: Option<cst::Directives>,
fields: Option<cst::InputFieldsDefinition>,
f: &mut GraphqlFormatter<'_, '_>,
name: &Name<'a>,
directives: &'a [Directive<'a>],
fields: &'a [InputValueDefinition<'a>],
r_curly_start: u32,
f: &mut GraphqlFormatter<'_, 'a>,
) {
common::write_description(description, f);
if extend {
write!(f, "extend ");
}
write!(f, "input ");
if let Some(name) = name {
common::write_name(&name, f);
}
common::write_name(name, f);
common::write_directives(directives, DirectivesStyle::Attached, f);
if let Some(fields) = fields {
let items: Vec<cst::InputValueDefinition> = fields.input_value_definitions().collect();
if !items.is_empty() {
write!(f, " {");
let r_curly = closing_token_start(fields.r_curly_token(), fields.syntax());
write_braced_body(f, &items, r_curly, |i, f| {
common::write_input_value_definition(&items[i], f);
});
}
if !fields.is_empty() {
write!(f, " {");
write_braced_body(f, fields, r_curly_start, |i, f| {
common::write_input_value_definition(&fields[i], f);
});
}
}
fn write_enum_like(
description: Option<cst::Description>,
fn write_enum_like<'a>(
description: Option<&StringValue<'a>>,
extend: bool,
name: Option<cst::Name>,
directives: Option<cst::Directives>,
values: Option<cst::EnumValuesDefinition>,
f: &mut GraphqlFormatter<'_, '_>,
name: &Name<'a>,
directives: &'a [Directive<'a>],
values: &'a [EnumValueDefinition<'a>],
r_curly_start: u32,
f: &mut GraphqlFormatter<'_, 'a>,
) {
common::write_description(description, f);
if extend {
write!(f, "extend ");
}
write!(f, "enum ");
if let Some(name) = name {
common::write_name(&name, f);
}
common::write_name(name, f);
common::write_directives(directives, DirectivesStyle::Attached, f);
if let Some(values) = values {
let items: Vec<cst::EnumValueDefinition> = values.enum_value_definitions().collect();
if !items.is_empty() {
write!(f, " {");
let r_curly = closing_token_start(values.r_curly_token(), values.syntax());
write_braced_body(f, &items, r_curly, |i, f| {
write_enum_value_definition(&items[i], f);
});
}
if !values.is_empty() {
write!(f, " {");
write_braced_body(f, values, r_curly_start, |i, f| {
write_enum_value_definition(&values[i], f);
});
}
}
fn write_enum_value_definition(value: &cst::EnumValueDefinition, f: &mut GraphqlFormatter<'_, '_>) {
common::write_description(value.description(), f);
if let Some(enum_value) = value.enum_value()
&& let Some(name) = enum_value.name()
{
common::write_name(&name, f);
}
common::write_directives(value.directives(), DirectivesStyle::Attached, f);
}
// The CST handles are captured by `Fn` closures (callable multiple times),
// so they are cloned inside; pass-by-value keeps the call sites uniform.
#[expect(clippy::needless_pass_by_value)]
fn write_union_like<'a>(
description: Option<cst::Description>,
extend: bool,
name: Option<cst::Name>,
directives: Option<cst::Directives>,
members: Option<cst::UnionMemberTypes>,
fn write_enum_value_definition<'a>(
value: &'a EnumValueDefinition<'a>,
f: &mut GraphqlFormatter<'_, 'a>,
) {
let content = format_with(|f: &mut GraphqlFormatter<'_, 'a>| {
common::write_description(description.clone(), f);
common::write_description(value.description.as_ref(), f);
common::write_name(&value.value.name, f);
common::write_directives(&value.directives, DirectivesStyle::Attached, f);
}
fn write_union_like<'a>(
description: Option<&StringValue<'a>>,
extend: bool,
name: &'a Name<'a>,
directives: &'a [Directive<'a>],
members: &'a [NamedType<'a>],
f: &mut GraphqlFormatter<'_, 'a>,
) {
let content = format_with(move |f: &mut GraphqlFormatter<'_, 'a>| {
common::write_description(description, f);
let inner = format_with(|f: &mut GraphqlFormatter<'_, 'a>| {
if extend {
write!(f, "extend ");
}
write!(f, "union ");
if let Some(name) = name.clone() {
common::write_name(&name, f);
}
common::write_directives(directives.clone(), DirectivesStyle::Attached, f);
if let Some(members) = members.clone() {
let types: Vec<cst::NamedType> = members.named_types().collect();
if !types.is_empty() {
write!(f, " =");
write!(f, if_group_fits_on_line(&space()));
let body = format_with(|f: &mut GraphqlFormatter<'_, 'a>| {
let leader = format_with(|f: &mut GraphqlFormatter<'_, 'a>| {
write!(f, [soft_line_break_or_space(), "| "]);
});
write!(f, if_group_breaks(&leader));
for (i, named) in types.iter().enumerate() {
if i > 0 {
write!(f, [soft_line_break_or_space(), "| "]);
}
common::write_named_type(named, f);
}
common::write_name(name, f);
common::write_directives(directives, DirectivesStyle::Attached, f);
if !members.is_empty() {
write!(f, " =");
write!(f, if_group_fits_on_line(&space()));
let body = format_with(|f: &mut GraphqlFormatter<'_, 'a>| {
let leader = format_with(|f: &mut GraphqlFormatter<'_, 'a>| {
write!(f, [soft_line_break_or_space(), "| "]);
});
write!(f, indent(&body));
}
write!(f, if_group_breaks(&leader));
for (i, named) in members.iter().enumerate() {
if i > 0 {
write!(f, [soft_line_break_or_space(), "| "]);
}
common::write_named_type(named, f);
}
});
write!(f, indent(&body));
}
});
write!(f, group(&inner));
@@ -429,32 +414,24 @@ fn write_union_like<'a>(
write!(f, group(&content));
}
fn write_field_definition(field: &cst::FieldDefinition, f: &mut GraphqlFormatter<'_, '_>) {
common::write_description(field.description(), f);
if let Some(name) = field.name() {
common::write_name(&name, f);
}
write_arguments_definition(field.arguments_definition(), f);
fn write_field_definition<'a>(field: &'a FieldDefinition<'a>, f: &mut GraphqlFormatter<'_, 'a>) {
common::write_description(field.description.as_ref(), f);
common::write_name(&field.name, f);
write_arguments_definition(&field.arguments, f);
write!(f, ": ");
if let Some(ty) = field.ty() {
common::write_type(&ty, f);
if let Some(ty) = field.ty.as_ref() {
common::write_type(ty, f);
}
common::write_directives(field.directives(), DirectivesStyle::Attached, f);
common::write_directives(&field.directives, DirectivesStyle::Attached, f);
}
/// `(name: Type = default @dir, ...)` on field and directive definitions.
/// Blank lines between entries are preserved (Prettier routes these through `printSequence`).
fn write_arguments_definition(
arguments: Option<cst::ArgumentsDefinition>,
f: &mut GraphqlFormatter<'_, '_>,
fn write_arguments_definition<'a>(
arguments: &'a [InputValueDefinition<'a>],
f: &mut GraphqlFormatter<'_, 'a>,
) {
let Some(arguments) = arguments else { return };
let items: Vec<cst::InputValueDefinition> = arguments.input_value_definitions().collect();
if items.is_empty() {
return;
}
let r_paren = closing_token_start(arguments.r_paren_token(), arguments.syntax());
common::write_paren_list(f, &items, r_paren, true, |i, f| {
common::write_input_value_definition(&items[i], f);
common::write_paren_list(f, arguments, true, |i, f| {
common::write_input_value_definition(&arguments[i], f);
});
}
+19 -20
View File
@@ -1,11 +1,9 @@
use oxc_graphql_parser::{cst, cst::CstNode};
use oxc_formatter_core::{
Buffer, Format, Formatter,
builders::{FormatWith, empty_line, hard_line_break, if_group_fits_on_line, soft_line_break},
write,
};
use oxc_span::Span;
use oxc_graphql_parser::ast::Document;
use crate::{
comments::{
@@ -16,14 +14,15 @@ use crate::{
context::GraphqlFormatContext,
};
pub mod common;
pub mod definition;
pub mod selection;
mod sig;
pub mod string;
pub mod value;
mod common;
mod definition;
mod selection;
mod span;
mod string;
mod value;
use sig::{sig_end, sig_start};
use span::Spanned;
pub use span::to_span;
pub type GraphqlFormatter<'buf, 'a> = Formatter<'buf, 'a, GraphqlFormatContext<'a>>;
@@ -50,7 +49,7 @@ where
/// How consecutive sequence items are separated.
#[derive(Clone, Copy, Eq, PartialEq)]
pub enum SeparatorKind {
enum SeparatorKind {
/// One item per line (definitions, selections, fields, enum values, ...).
Hard,
/// Prettier's `join([ifBreak("", ", "), softline], items)`
@@ -64,7 +63,7 @@ pub enum SeparatorKind {
///
/// Returns the significant end of the last item (`None` for an empty sequence),
/// so callers can seed `flush_trailing_inside_comments` without re-scanning.
pub fn write_sequence<'a, T, F>(
fn write_sequence<'a, T, F>(
f: &mut GraphqlFormatter<'_, 'a>,
items: &[T],
separator: SeparatorKind,
@@ -72,13 +71,13 @@ pub fn write_sequence<'a, T, F>(
mut write_item: F,
) -> Option<u32>
where
T: CstNode,
T: Spanned,
F: FnMut(usize, &mut GraphqlFormatter<'_, 'a>),
{
let mut prev_end: Option<u32> = None;
for (i, item) in items.iter().enumerate() {
let node = item.syntax();
let start = sig_start(node);
let item_span = item.span();
let start = item_span.start;
if let Some(pe) = prev_end {
write_trailing_same_line_comment(pe, f);
// Measure the gap up to the next pending comment (if it precedes the item),
@@ -111,9 +110,9 @@ where
}
}
}
let end = sig_end(node);
let end = item_span.end;
if is_suppressed_before(f, start) {
write_suppressed_node(Span::new(start, end), f);
write_suppressed_node(item_span, f);
} else {
flush_leading_comments(start, f);
write_item(i, f);
@@ -125,8 +124,8 @@ where
/// Top-level document: definitions joined by hard lines (blank lines preserved),
/// then any comments trailing the last definition.
pub fn write_document(document: &cst::Document, f: &mut GraphqlFormatter<'_, '_>) {
let defs: Vec<cst::Definition> = document.definitions().collect();
pub fn write_document<'a>(document: &'a Document<'a>, f: &mut GraphqlFormatter<'_, 'a>) {
let defs = document.definitions.as_slice();
if defs.is_empty() {
// Defensive: oxc-graphql-parser errors on empty/comments-only documents,
// so this branch is unreachable on the normal path.
@@ -135,7 +134,7 @@ pub fn write_document(document: &cst::Document, f: &mut GraphqlFormatter<'_, '_>
return;
}
let last_end = write_sequence(f, &defs, SeparatorKind::Hard, true, |i, f| {
let last_end = write_sequence(f, defs, SeparatorKind::Hard, true, |i, f| {
definition::write_definition(&defs[i], f);
});
if let Some(last_end) = last_end {
@@ -1,27 +1,26 @@
//! Selection set printers: fields, fragment spreads, inline fragments.
use oxc_graphql_parser::{cst, cst::CstNode};
use oxc_formatter_core::{
Buffer,
builders::{block_indent, group, space},
write,
};
use oxc_graphql_parser::ast::{Field, FragmentSpread, InlineFragment, Selection, SelectionSet};
use crate::comments::flush_trailing_inside_comments;
use super::{
GraphqlFormatter, SeparatorKind, common, common::DirectivesStyle, format_with,
sig::closing_token_start, write_sequence,
span::close_delim_start, write_sequence,
};
/// `{ selections... }`, always multi-line.
pub fn write_selection_set<'a>(
selection_set: &cst::SelectionSet,
pub(super) fn write_selection_set<'a>(
selection_set: &'a SelectionSet<'a>,
f: &mut GraphqlFormatter<'_, 'a>,
) {
let selections: Vec<cst::Selection> = selection_set.selections().collect();
let r_curly = closing_token_start(selection_set.r_curly_token(), selection_set.syntax());
let selections = selection_set.selections.as_slice();
let r_curly = close_delim_start(selection_set.span);
write!(f, "{");
if selections.is_empty() {
@@ -31,7 +30,7 @@ pub fn write_selection_set<'a>(
}
let body = format_with(|f: &mut GraphqlFormatter<'_, 'a>| {
let last_end = write_sequence(f, &selections, SeparatorKind::Hard, true, |i, f| {
let last_end = write_sequence(f, selections, SeparatorKind::Hard, true, |i, f| {
write_selection(&selections[i], f);
});
if let Some(last_end) = last_end {
@@ -41,56 +40,46 @@ pub fn write_selection_set<'a>(
write!(f, [block_indent(&body), "}"]);
}
fn write_selection(selection: &cst::Selection, f: &mut GraphqlFormatter<'_, '_>) {
fn write_selection<'a>(selection: &'a Selection<'a>, f: &mut GraphqlFormatter<'_, 'a>) {
match selection {
cst::Selection::Field(field) => write_field(field, f),
cst::Selection::FragmentSpread(spread) => write_fragment_spread(spread, f),
cst::Selection::InlineFragment(inline) => write_inline_fragment(inline, f),
Selection::Field(field) => write_field(field, f),
Selection::FragmentSpread(spread) => write_fragment_spread(spread, f),
Selection::InlineFragment(inline) => write_inline_fragment(inline, f),
}
}
fn write_field<'a>(field: &cst::Field, f: &mut GraphqlFormatter<'_, 'a>) {
fn write_field<'a>(field: &'a Field<'a>, f: &mut GraphqlFormatter<'_, 'a>) {
let content = format_with(|f: &mut GraphqlFormatter<'_, 'a>| {
if let Some(alias) = field.alias()
&& let Some(name) = alias.name()
{
common::write_name(&name, f);
if let Some(alias) = field.alias.as_ref() {
common::write_name(alias, f);
write!(f, ": ");
}
if let Some(name) = field.name() {
common::write_name(&name, f);
}
common::write_arguments(field.arguments(), f);
common::write_directives(field.directives(), DirectivesStyle::Attached, f);
if let Some(selection_set) = field.selection_set() {
common::write_name(&field.name, f);
common::write_arguments(&field.arguments, f);
common::write_directives(&field.directives, DirectivesStyle::Attached, f);
if let Some(selection_set) = field.selection_set.as_ref() {
write!(f, space());
write_selection_set(&selection_set, f);
write_selection_set(selection_set, f);
}
});
write!(f, group(&content));
}
fn write_fragment_spread(spread: &cst::FragmentSpread, f: &mut GraphqlFormatter<'_, '_>) {
fn write_fragment_spread<'a>(spread: &'a FragmentSpread<'a>, f: &mut GraphqlFormatter<'_, 'a>) {
write!(f, "...");
if let Some(fragment_name) = spread.fragment_name()
&& let Some(name) = fragment_name.name()
{
common::write_name(&name, f);
}
common::write_directives(spread.directives(), DirectivesStyle::Attached, f);
common::write_name(&spread.name, f);
common::write_directives(&spread.directives, DirectivesStyle::Attached, f);
}
fn write_inline_fragment(inline: &cst::InlineFragment, f: &mut GraphqlFormatter<'_, '_>) {
fn write_inline_fragment<'a>(inline: &'a InlineFragment<'a>, f: &mut GraphqlFormatter<'_, 'a>) {
write!(f, "...");
if let Some(type_condition) = inline.type_condition()
&& let Some(named) = type_condition.named_type()
{
if let Some(type_condition) = inline.type_condition.as_ref() {
write!(f, " on ");
common::write_named_type(&named, f);
common::write_named_type(type_condition, f);
}
common::write_directives(inline.directives(), DirectivesStyle::Attached, f);
if let Some(selection_set) = inline.selection_set() {
common::write_directives(&inline.directives, DirectivesStyle::Attached, f);
if let Some(selection_set) = inline.selection_set.as_ref() {
write!(f, space());
write_selection_set(&selection_set, f);
write_selection_set(selection_set, f);
}
}
@@ -1,71 +0,0 @@
//! CST position utilities over oxc-graphql-parser's lossless tree.
//!
//! `oxc-graphql-parser` attaches pending trivia (whitespace, comments, insignificant commas)
//! to whichever node is open when the next significant token is consumed,
//! so a node's raw `text_range()` may start or end on trivia.
//! Every layout decision therefore uses significant-token positions computed here.
use oxc_graphql_parser::{SyntaxKind, SyntaxNode, SyntaxToken};
use oxc_span::Span;
use super::GraphqlFormatter;
/// Whether `kind` is a trivia token. In GraphQL, commas are trivia too.
fn is_trivia(kind: SyntaxKind) -> bool {
matches!(kind, SyntaxKind::WHITESPACE | SyntaxKind::COMMENT | SyntaxKind::COMMA)
}
/// Start offset of the first significant (non-trivia) token within `node`.
///
/// oxc-graphql-parser attaches pending trivia to the node that is open when the next
/// significant token is consumed, so `node.text_range().start()` may point at a
/// comment that logically precedes the node. All layout decisions use significant
/// token positions instead.
pub fn sig_start(node: &SyntaxNode) -> u32 {
let node_end = u32::from(node.text_range().end());
let mut tok = node.first_token();
while let Some(t) = tok {
if u32::from(t.text_range().start()) >= node_end {
break;
}
if !is_trivia(t.kind()) {
return t.text_range().start().into();
}
tok = t.next_token();
}
node.text_range().start().into()
}
/// End offset of the last significant (non-trivia) token within `node`.
pub fn sig_end(node: &SyntaxNode) -> u32 {
let node_start = u32::from(node.text_range().start());
let mut tok = node.last_token();
while let Some(t) = tok {
if u32::from(t.text_range().end()) <= node_start {
break;
}
if !is_trivia(t.kind()) {
return t.text_range().end().into();
}
tok = t.prev_token();
}
node.text_range().end().into()
}
/// Significant-token span of `node`.
fn sig_span(node: &SyntaxNode) -> Span {
Span::new(sig_start(node), sig_end(node))
}
/// Start offset of a closing delimiter: the token's start when present,
/// the container's significant end otherwise (error-resilient fallback).
pub fn closing_token_start(token: Option<SyntaxToken>, container: &SyntaxNode) -> u32 {
token.map_or_else(|| sig_end(container), |t| t.text_range().start().into())
}
/// Source slice of `node`'s significant span, carrying the arena lifetime.
pub fn node_text<'a>(f: &GraphqlFormatter<'_, 'a>, node: &SyntaxNode) -> &'a str {
let span = sig_span(node);
f.context().source_text().slice_range(span.start, span.end)
}
@@ -0,0 +1,105 @@
//! Span utilities bridging the parser AST and the formatter core.
//!
//! `oxc-graphql-parser` spans are usize-based ([`ast::Span`]) while the
//! formatter core APIs take u32-based [`oxc_span::Span`]s; [`to_span`] converts.
//! Node spans are significant-token spans (trivia is never included),
//! so layout decisions use them directly, via [`Spanned`] when generic.
//!
//! Closing-delimiter positions are derived here too:
//! - Braced/bracketed containers (`SelectionSet`, `ListValue`, ...) own their delimiter:
//! the span's last consumed token IS the 1-byte closer, see [`close_delim_start`].
//! - Paren lists (arguments, variable definitions) are bare `AstVec`s with no wrapper node,
//! so the `)` is found by scanning past trivia from the last item's end, see [`find_close_after`].
use oxc_graphql_parser::ast;
use oxc_span::Span;
/// Converts an `oxc-graphql-parser` [`ast::Span`] (usize-based) into
/// an [`oxc_span::Span`] (u32-based) for use with the formatter core APIs.
/// `pub`: also used by `format.rs` for the parser's comment spans (re-exported through `print`).
#[inline]
pub fn to_span(s: ast::Span) -> Span {
Span::new(u32::try_from(s.start).unwrap_or(u32::MAX), u32::try_from(s.end).unwrap_or(u32::MAX))
}
/// Significant span of an AST node as an [`oxc_span::Span`].
pub(super) trait Spanned {
fn span(&self) -> Span;
}
macro_rules! impl_spanned {
($($ty:ident),* $(,)?) => {
$(
impl Spanned for ast::$ty<'_> {
fn span(&self) -> Span {
to_span(self.span)
}
}
)*
};
}
impl_spanned!(
Argument,
EnumValueDefinition,
FieldDefinition,
InputValueDefinition,
ObjectField,
RootOperationTypeDefinition,
VariableDefinition,
);
// Enums: forward to the parser's inherent `span()` accessors.
// Explicit calls: the inherent method (returning `ast::Span`) would shadow
// the trait method under plain `self.span()`.
impl Spanned for ast::Definition<'_> {
fn span(&self) -> Span {
to_span(ast::Definition::span(self))
}
}
impl Spanned for ast::Selection<'_> {
fn span(&self) -> Span {
to_span(ast::Selection::span(self))
}
}
impl Spanned for ast::Value<'_> {
fn span(&self) -> Span {
to_span(ast::Value::span(self))
}
}
/// Start offset of a container's 1-byte closing delimiter (`}` / `]`).
///
/// Sound only on a clean parse — the only case that reaches the printer,
/// since `format()` bails on any parse error.
/// Error-recovery spans may end elsewhere (e.g. at EOF),
/// which would misplace comments flushed against this position.
pub(super) fn close_delim_start(span: ast::Span) -> u32 {
to_span(span).end.saturating_sub(1)
}
/// Scans `source` from `from` for the first byte equal to `close`,
/// treating GraphQL trivia (whitespace, insignificant commas, `#` line comments) as skippable.
/// Returns `from` if not found
/// (defensive fallback; the caller's parse succeeded so the closing byte is expected to exist).
pub(super) fn find_close_after(source: &str, from: u32, close: u8) -> u32 {
let bytes = source.as_bytes();
let mut i = from as usize;
while i < bytes.len() {
let b = bytes[i];
if b == close {
return u32::try_from(i).unwrap_or(from);
}
if b == b'#' {
// Line comment: skip to end of line (matching GraphQL's line terminators)
while i < bytes.len() && bytes[i] != b'\n' && bytes[i] != b'\r' {
i += 1;
}
continue;
}
i += 1;
}
from
}
@@ -2,23 +2,25 @@
//!
//! Prettier prints `StringValue` from graphql-js's *cooked* `node.value`
//! (escapes decoded, block strings dedented), then re-encodes it.
//! `oxc-graphql-parser` hands us the raw source, so this module reimplements both the
//! cooking (GraphQL spec string/block-string value algorithms)
//! and Prettier's re-encoding (`"`/`\` escaped, newline as `\n`, `"""` as `\"""`).
//! `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 `\"""`).
use cow_utils::CowUtils;
use oxc_graphql_parser::{cst, cst::CstNode};
use oxc_formatter_core::{
Buffer,
builders::{hard_line_break, text},
write,
};
use oxc_graphql_parser::ast::StringValue;
use super::{GraphqlFormatter, sig::node_text};
use super::GraphqlFormatter;
pub fn write_string_value(sv: &cst::StringValue, f: &mut GraphqlFormatter<'_, '_>) {
let raw = node_text(f, sv.syntax());
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);
} else if let Some(body) = raw.strip_prefix('"').and_then(|r| r.strip_suffix('"')) {
+27 -30
View File
@@ -2,39 +2,38 @@
//!
//! `StringValue` cooking and re-encoding lives in the sibling [`super::string`] module.
use oxc_graphql_parser::{cst, cst::CstNode};
use oxc_formatter_core::{
Buffer, FormatContext,
builders::{group, soft_block_indent_with_maybe_space, text},
builders::{group, soft_block_indent, soft_block_indent_with_maybe_space, text},
write,
};
use oxc_graphql_parser::ast::{ListValue, ObjectValue, Value};
use crate::comments::flush_trailing_inside_comments;
use super::{
GraphqlFormatter, SeparatorKind, common, format_with,
sig::{closing_token_start, node_text},
string, write_sequence,
GraphqlFormatter, SeparatorKind, common, format_with, span::close_delim_start, string,
write_sequence,
};
pub fn write_value(value: &cst::Value, f: &mut GraphqlFormatter<'_, '_>) {
pub(super) fn write_value<'a>(value: &'a Value<'a>, f: &mut GraphqlFormatter<'_, 'a>) {
match value {
cst::Value::Variable(variable) => common::write_variable(variable, f),
cst::Value::StringValue(sv) => string::write_string_value(sv, f),
cst::Value::FloatValue(v) => write!(f, text(node_text(f, v.syntax()))),
cst::Value::IntValue(v) => write!(f, text(node_text(f, v.syntax()))),
cst::Value::BooleanValue(v) => write!(f, text(node_text(f, v.syntax()))),
cst::Value::NullValue(_) => write!(f, "null"),
cst::Value::EnumValue(v) => write!(f, text(node_text(f, v.syntax()))),
cst::Value::ListValue(list) => write_list_value(list, f),
cst::Value::ObjectValue(object) => write_object_value(object, f),
Value::Variable(variable) => common::write_variable(variable, f),
Value::String(sv) => string::write_string_value(sv, f),
Value::Float(v) => write!(f, text(v.raw)),
Value::Int(v) => write!(f, text(v.raw)),
Value::Boolean(v) => write!(f, text(if v.value { "true" } else { "false" })),
Value::Null(_) => write!(f, "null"),
Value::Enum(v) => write!(f, text(v.name.value)),
Value::List(list) => write_list_value(list, f),
Value::Object(object) => write_object_value(object, f),
Value::Missing(_) => {}
}
}
fn write_list_value<'a>(list: &cst::ListValue, f: &mut GraphqlFormatter<'_, 'a>) {
let values: Vec<cst::Value> = list.values().collect();
let r_brack = closing_token_start(list.r_brack_token(), list.syntax());
fn write_list_value<'a>(list: &'a ListValue<'a>, f: &mut GraphqlFormatter<'_, 'a>) {
let values = list.values.as_slice();
let r_brack = close_delim_start(list.span);
write!(f, "[");
if values.is_empty() {
@@ -44,19 +43,19 @@ fn write_list_value<'a>(list: &cst::ListValue, f: &mut GraphqlFormatter<'_, 'a>)
let body = format_with(|f: &mut GraphqlFormatter<'_, 'a>| {
// No blank-line preservation (Prettier uses a plain `path.map` for list values).
let last_end = write_sequence(f, &values, SeparatorKind::CommaSoftline, false, |i, f| {
let last_end = write_sequence(f, values, SeparatorKind::CommaSoftline, false, |i, f| {
write_value(&values[i], f);
});
if let Some(last_end) = last_end {
flush_trailing_inside_comments(last_end, r_brack, f);
}
});
write!(f, [group(&oxc_formatter_core::builders::soft_block_indent(&body)), "]"]);
write!(f, [group(&soft_block_indent(&body)), "]"]);
}
fn write_object_value<'a>(object: &cst::ObjectValue, f: &mut GraphqlFormatter<'_, 'a>) {
let fields: Vec<cst::ObjectField> = object.object_fields().collect();
let r_curly = closing_token_start(object.r_curly_token(), object.syntax());
fn write_object_value<'a>(object: &'a ObjectValue<'a>, f: &mut GraphqlFormatter<'_, 'a>) {
let fields = object.fields.as_slice();
let r_curly = close_delim_start(object.span);
write!(f, "{");
if fields.is_empty() {
@@ -66,14 +65,12 @@ fn write_object_value<'a>(object: &cst::ObjectValue, f: &mut GraphqlFormatter<'_
let bracket_spacing = f.context().options().bracket_spacing.value();
let body = format_with(|f: &mut GraphqlFormatter<'_, 'a>| {
let last_end = write_sequence(f, &fields, SeparatorKind::CommaSoftline, false, |i, f| {
let last_end = write_sequence(f, fields, SeparatorKind::CommaSoftline, false, |i, f| {
let field = &fields[i];
if let Some(name) = field.name() {
common::write_name(&name, f);
}
common::write_name(&field.name, f);
write!(f, ": ");
if let Some(v) = field.value() {
write_value(&v, f);
if let Some(v) = field.value.as_ref() {
write_value(v, f);
}
});
if let Some(last_end) = last_end {