Skip to main content

ronin_core/
formatter.rs

1//! The deterministic, lossless-by-semantics RON formatter (E005 Wave 1).
2//!
3//! The formatter turns a [`CstDocument`] (or a single [`SyntaxNode`] subtree) into
4//! a **canonically laid-out** RON string: one element per line for multi-line
5//! collections, canonical indentation per nesting depth, normalized spacing, and a
6//! deterministic trailing-comma rule. It is the formatter half of RONin's "smart
7//! authoring" epic and lives in `ronin-core` so every surface (desktop, future LSP)
8//! shares one engine (project-instructions §II, "One Core, Many Surfaces").
9//!
10//! # Hard invariants (project-instructions §I, "Never Corrupt User Data")
11//!
12//! The formatter ONLY changes whitespace / layout. It MUST:
13//!
14//! * **preserve every comment** — line `//` and block `/* */`, including comments
15//!   at construct boundaries (before `)`, `]`, `}`) and dangling comments inside
16//!   otherwise-empty collections — re-emitted attached to the same node (T007);
17//! * **preserve order** — fields, variants, list elements, map entries, tuple
18//!   elements stay in source order (never sorted / reordered);
19//! * **preserve every name and value** — struct/variant names and all scalar
20//!   tokens are emitted verbatim (never normalized / re-escaped);
21//! * **be idempotent** — `format(format(x)) == format(x)` (T019);
22//! * **be a no-op on failure** — unparseable / in-progress input, or any internal
23//!   inconsistency, returns [`FormatResult::NoOp`] with the document byte-unchanged
24//!   (T011), and the candidate output is verified to be semantically identical to
25//!   the input before it is ever returned (T012, AD-008 verify-before-replace).
26//!
27//! # WASM-clean (project-instructions §II, INV-9)
28//!
29//! This module adds **no** filesystem / UI / async / native dependency — it uses
30//! only `std` and `ronin-core`'s own CST types, so the `wasm32` build of `ronin-core`
31//! stays green.
32//!
33//! # Canonical style
34//!
35//! * **Indent** — `indent_width` spaces per nesting depth (clamped to `1..=16`,
36//!   default `4`).
37//! * **Element-per-line** — a collection that spans more than one line in the
38//!   source (or that contains a comment) is laid out one element per line; a
39//!   collection that fits on a single source line and has no comments stays on one
40//!   line.
41//! * **Trailing comma** — a multi-line collection gets a trailing comma after
42//!   every element including the last; a single-line collection gets none (T008).
43//! * **Spacing** — `name: value` (one space after `:`), `key: value` in maps, one
44//!   space after a struct/variant name has no gap before its `(`/`{`, etc.
45//! * **Blank lines** — [`BlankLinePolicy::Collapse`] (default) collapses any run of
46//!   blank lines to at most one; [`BlankLinePolicy::Preserve`] keeps the original
47//!   blank-line count between elements.
48//!
49//! # Deferred seams
50//!
51//! The formatter is **structural only** — it lays out the CST it is given and never
52//! consults type information. The intelligence that layers on top is deferred to
53//! later epics and attaches around (never inside) this pure-CST engine:
54//!
55//! * **type-aware formatting / completion** (e.g. canonical layout choices keyed to
56//!   an expected type) → **E006** (schema-optional type model);
57//! * **semantic / CST-backed undo-redo** of a format apply → **E007** (the format
58//!   command's buffer replacement is the seam an undo stack records against);
59//! * **tree / table structured editing** that would re-emit through this formatter
60//!   → **E008**;
61//! * **Bevy-registry-aware** formatting (component-aware layout) → **E009**;
62//! * **RON⇄JSON interop / `derive`-driven canonicalization** → **E010** (interop
63//!   lives outside this CST formatter; the `ron` crate is used there, never here).
64
65use crate::parser::{parse, CstDocument};
66use crate::syntax::{SyntaxElement, SyntaxKind, SyntaxNode, SyntaxToken};
67
68/// How the formatter treats runs of blank lines between elements / fields.
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
70pub enum BlankLinePolicy {
71    /// Collapse any run of blank lines to at most one (the default). A blank line
72    /// the user inserted between two fields is kept (a single blank), but a run of
73    /// several is reduced to one.
74    Collapse,
75    /// Preserve the original number of blank lines between elements verbatim.
76    Preserve,
77}
78
79impl Default for BlankLinePolicy {
80    #[inline]
81    fn default() -> Self {
82        Self::Collapse
83    }
84}
85
86/// Formatter configuration (the formatter-side mirror of `ronin-app`'s
87/// `FormattingConfig`). A pure value type with no I/O — WASM-clean.
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
89pub struct FormatConfig {
90    /// Spaces of indent per nesting depth. Constructed values are clamped to
91    /// [`FormatConfig::MIN_INDENT`]`..=`[`FormatConfig::MAX_INDENT`] via
92    /// [`FormatConfig::new`] / [`FormatConfig::with_indent_width`]; read it back
93    /// through [`FormatConfig::indent_width`].
94    indent_width: u32,
95    /// How runs of blank lines are treated.
96    blank_line_policy: BlankLinePolicy,
97}
98
99impl FormatConfig {
100    /// The smallest permitted indent width (1 space).
101    pub const MIN_INDENT: u32 = 1;
102    /// The largest permitted indent width (16 spaces).
103    pub const MAX_INDENT: u32 = 16;
104    /// The default indent width (4 spaces).
105    pub const DEFAULT_INDENT: u32 = 4;
106
107    /// Build a config, clamping `indent_width` to the sane range
108    /// [`MIN_INDENT`](Self::MIN_INDENT)`..=`[`MAX_INDENT`](Self::MAX_INDENT).
109    #[must_use]
110    pub fn new(indent_width: u32, blank_line_policy: BlankLinePolicy) -> Self {
111        Self {
112            indent_width: indent_width.clamp(Self::MIN_INDENT, Self::MAX_INDENT),
113            blank_line_policy,
114        }
115    }
116
117    /// Builder-style override of the indent width (clamped to the sane range).
118    #[must_use]
119    pub fn with_indent_width(mut self, indent_width: u32) -> Self {
120        self.indent_width = indent_width.clamp(Self::MIN_INDENT, Self::MAX_INDENT);
121        self
122    }
123
124    /// Builder-style override of the blank-line policy.
125    #[must_use]
126    pub fn with_blank_line_policy(mut self, policy: BlankLinePolicy) -> Self {
127        self.blank_line_policy = policy;
128        self
129    }
130
131    /// The (already-clamped) indent width in spaces.
132    #[must_use]
133    pub fn indent_width(self) -> u32 {
134        self.indent_width
135    }
136
137    /// The blank-line policy.
138    #[must_use]
139    pub fn blank_line_policy(self) -> BlankLinePolicy {
140        self.blank_line_policy
141    }
142}
143
144impl Default for FormatConfig {
145    #[inline]
146    fn default() -> Self {
147        Self {
148            indent_width: Self::DEFAULT_INDENT,
149            blank_line_policy: BlankLinePolicy::default(),
150        }
151    }
152}
153
154/// The outcome of a format request.
155///
156/// On success it carries the canonically-formatted text. On any failure path —
157/// unparseable input, error-recovered tree, no clean subtree boundary, or a
158/// verify-before-replace mismatch — it carries a human-readable reason and the
159/// **caller's original bytes are left unchanged** (the formatter never performs a
160/// partial rewrite, project-instructions §I).
161#[derive(Debug, Clone, PartialEq, Eq)]
162pub enum FormatResult {
163    /// Formatting succeeded; the canonical text is enclosed.
164    Formatted(String),
165    /// Formatting was declined; the document is unchanged. `reason` explains why
166    /// (suitable for a non-blocking notice).
167    NoOp {
168        /// Why the formatter declined (e.g. "input has parse errors").
169        reason: String,
170    },
171}
172
173impl FormatResult {
174    /// A [`FormatResult::NoOp`] carrying `reason`.
175    fn no_op(reason: impl Into<String>) -> Self {
176        Self::NoOp {
177            reason: reason.into(),
178        }
179    }
180
181    /// The formatted text, if formatting succeeded.
182    #[must_use]
183    pub fn formatted(&self) -> Option<&str> {
184        match self {
185            Self::Formatted(s) => Some(s),
186            Self::NoOp { .. } => None,
187        }
188    }
189
190    /// `true` if formatting was declined (a no-op).
191    #[must_use]
192    pub fn is_no_op(&self) -> bool {
193        matches!(self, Self::NoOp { .. })
194    }
195}
196
197/// Format the whole document into canonical RON (T009).
198///
199/// Returns [`FormatResult::Formatted`] with the canonical text, or
200/// [`FormatResult::NoOp`] (input unchanged) when the input does not parse cleanly
201/// (error-recovered tree, T011) or the candidate output fails the
202/// verify-before-replace semantic check (T012).
203#[must_use]
204pub fn format(doc: &CstDocument, config: &FormatConfig) -> FormatResult {
205    // T011 — no-op on unparseable / in-progress input: a tree that triggered error
206    // recovery is never reflowed (a partial / wrong reflow would corrupt data).
207    if !doc.diagnostics().is_empty() {
208        return FormatResult::no_op("input has parse errors; formatting skipped");
209    }
210    let root = doc.root();
211    format_subtree(&root, config, FormatScope::WholeDocument)
212}
213
214/// Format the smallest enclosing CST subtree for "Format Selection" (T010).
215///
216/// `node` is expected to be a value-position node (struct / tuple / list / map /
217/// enum-variant / unit / literal) or the document root. For any other node — i.e.
218/// no clean subtree boundary — this returns [`FormatResult::NoOp`]. The rest of the
219/// document is the caller's responsibility (the caller splices the returned text in
220/// place); this function only produces the canonical text for `node`'s span.
221#[must_use]
222pub fn format_node(node: &SyntaxNode, config: &FormatConfig) -> FormatResult {
223    // T011 — refuse to format any subtree that contains an error-recovery node:
224    // reflowing partially-parsed input risks corruption.
225    if subtree_has_errors(node) {
226        return FormatResult::no_op("selection contains parse errors; formatting skipped");
227    }
228    // A clean subtree boundary is a value node or the root. Anything else (a bare
229    // StructField, MapEntry, a token, the ExtensionAttr, an Error node) has no
230    // standalone canonical form here → no-op (T010).
231    let scope = match node.kind() {
232        SyntaxKind::Root => FormatScope::WholeDocument,
233        SyntaxKind::Struct
234        | SyntaxKind::Tuple
235        | SyntaxKind::List
236        | SyntaxKind::Map
237        | SyntaxKind::EnumVariant
238        | SyntaxKind::Unit
239        | SyntaxKind::Literal => FormatScope::Subtree,
240        _ => {
241            return FormatResult::no_op(
242                "no clean subtree boundary at the selection; formatting skipped",
243            )
244        }
245    };
246    format_subtree(node, config, scope)
247}
248
249/// Whether a format pass covers the whole document (so it owns leading extension
250/// attributes + a final trailing newline) or a single embedded subtree.
251#[derive(Debug, Clone, Copy, PartialEq, Eq)]
252enum FormatScope {
253    WholeDocument,
254    Subtree,
255}
256
257/// Shared implementation behind [`format`] and [`format_node`].
258///
259/// Emits the canonical text for `node`, then runs verify-before-replace (T012)
260/// before returning it. Any internal inconsistency degrades to a no-op (T011), so
261/// the formatter is never the source of a partial / corrupting rewrite.
262fn format_subtree(node: &SyntaxNode, config: &FormatConfig, scope: FormatScope) -> FormatResult {
263    let original = node.text();
264
265    let mut writer = Writer::new(config);
266    match scope {
267        FormatScope::WholeDocument => emit_root(node, &mut writer, config),
268        FormatScope::Subtree => emit_value_like(node, &mut writer, config),
269    }
270    let candidate = writer.finish(scope, &original);
271
272    // T012 — verify-before-replace (AD-008): re-parse the candidate and confirm it
273    // is SEMANTICALLY identical to the input (same names / order / values /
274    // comments, ignoring only whitespace layout). On ANY mismatch, no-op.
275    match scope {
276        FormatScope::WholeDocument => {
277            let reparsed = parse(&candidate);
278            if !reparsed.diagnostics().is_empty() {
279                return FormatResult::no_op(
280                    "internal: formatted output did not re-parse cleanly; left unchanged",
281                );
282            }
283            if !semantically_equal(&original, &candidate) {
284                return FormatResult::no_op(
285                    "internal: semantic verification failed; document left unchanged",
286                );
287            }
288        }
289        FormatScope::Subtree => {
290            // A subtree's candidate is verified against the original subtree text by
291            // comparing significant + comment token streams directly (the candidate
292            // may not be a stand-alone whole document, e.g. a bare literal).
293            if !semantic_tokens_equal(&original, &candidate) {
294                return FormatResult::no_op(
295                    "internal: semantic verification failed; selection left unchanged",
296                );
297            }
298        }
299    }
300
301    // Idempotence guard: if the input was already canonical, the candidate equals
302    // the original — still a successful Formatted (the caller can compare to detect
303    // a no-change result if it cares).
304    FormatResult::Formatted(candidate)
305}
306
307// =============================================================================
308// Trivia model (T007): classify and re-emit comments attached to the right node.
309// =============================================================================
310
311/// A comment captured from the CST, with its leading-blank context.
312#[derive(Debug, Clone)]
313struct Comment {
314    /// Verbatim comment text (e.g. `// foo` or `/* bar */`), never normalized.
315    text: String,
316    /// Number of blank lines that preceded this comment in the source (already
317    /// resolved against the blank-line policy by the caller).
318    blanks_before: usize,
319    /// `true` if this comment began on the same source line as the preceding
320    /// significant token (an inline trailing comment, e.g. `1, // note`).
321    same_line_as_prev: bool,
322}
323
324/// The trivia found between two significant tokens (or around a construct), split
325/// into the comments it carries and the blank-line run that trails it.
326#[derive(Debug, Clone, Default)]
327struct TriviaRun {
328    /// Comments in source order.
329    comments: Vec<Comment>,
330    /// Blank lines after the last comment (or in an all-whitespace run), already
331    /// resolved against the policy.
332    trailing_blanks: usize,
333    /// `true` if the run contained at least one newline (used to decide whether a
334    /// following inline comment really is "same line").
335    has_newline: bool,
336}
337
338impl TriviaRun {
339    fn is_empty(&self) -> bool {
340        self.comments.is_empty()
341    }
342}
343
344/// Count the blank lines represented by a run of whitespace text.
345///
346/// A "blank line" is a newline beyond the first: `"\n"` (end of one line) is zero
347/// blanks; `"\n\n"` is one blank line; `"\n\n\n"` is two. CRLF is handled by
348/// counting `\n` only.
349fn count_blank_lines(ws: &str) -> usize {
350    let newlines = ws.bytes().filter(|&b| b == b'\n').count();
351    newlines.saturating_sub(1)
352}
353
354/// Resolve a raw blank-line count against the policy.
355fn resolve_blanks(raw: usize, policy: BlankLinePolicy) -> usize {
356    match policy {
357        BlankLinePolicy::Collapse => raw.min(1),
358        BlankLinePolicy::Preserve => raw,
359    }
360}
361
362// =============================================================================
363// Emit: the canonical layout walk.
364// =============================================================================
365
366/// A small indent-tracking string writer.
367struct Writer {
368    out: String,
369    indent_level: usize,
370    indent_width: usize,
371    /// `true` when the current line has no content yet (so indentation is pending).
372    at_line_start: bool,
373}
374
375impl Writer {
376    fn new(config: &FormatConfig) -> Self {
377        Self {
378            out: String::new(),
379            indent_level: 0,
380            indent_width: config.indent_width() as usize,
381            at_line_start: true,
382        }
383    }
384
385    fn indent(&mut self) {
386        self.indent_level += 1;
387    }
388
389    fn dedent(&mut self) {
390        self.indent_level = self.indent_level.saturating_sub(1);
391    }
392
393    /// Write `s` (no newlines expected inside) at the current position, emitting
394    /// pending indentation first if at line start.
395    fn write(&mut self, s: &str) {
396        if s.is_empty() {
397            return;
398        }
399        if self.at_line_start {
400            for _ in 0..(self.indent_level * self.indent_width) {
401                self.out.push(' ');
402            }
403            self.at_line_start = false;
404        }
405        self.out.push_str(s);
406    }
407
408    /// End the current line.
409    fn newline(&mut self) {
410        // Trim trailing spaces on the line we are closing (no trailing whitespace).
411        while self.out.ends_with(' ') {
412            self.out.pop();
413        }
414        self.out.push('\n');
415        self.at_line_start = true;
416    }
417
418    /// Emit `count` blank lines (each an empty line).
419    fn blank_lines(&mut self, count: usize) {
420        for _ in 0..count {
421            // A blank line is just a newline with no content.
422            while self.out.ends_with(' ') {
423                self.out.pop();
424            }
425            self.out.push('\n');
426            self.at_line_start = true;
427        }
428    }
429
430    /// Finish the formatted text, normalizing the document-final newline.
431    fn finish(mut self, scope: FormatScope, original: &str) -> String {
432        // Trim trailing spaces on the final line.
433        while self.out.ends_with(' ') {
434            self.out.pop();
435        }
436        match scope {
437            FormatScope::WholeDocument => {
438                // Canonical whole-document output ends in exactly one newline when
439                // there is content; an empty/trivia-only doc keeps its emptiness.
440                while self.out.ends_with('\n') {
441                    self.out.pop();
442                }
443                // Preserve a leading BOM if the original carried one.
444                if original.starts_with('\u{FEFF}') && !self.out.starts_with('\u{FEFF}') {
445                    self.out.insert(0, '\u{FEFF}');
446                }
447                let body_is_empty =
448                    self.out.is_empty() || self.out == "\u{FEFF}" || self.out.trim().is_empty();
449                if !body_is_empty {
450                    self.out.push('\n');
451                }
452                self.out
453            }
454            FormatScope::Subtree => {
455                // A subtree carries no document-final newline; strip any trailing
456                // newline we may have emitted so the splice site stays exact.
457                while self.out.ends_with('\n') {
458                    self.out.pop();
459                }
460                self.out
461            }
462        }
463    }
464}
465
466/// Emit the canonical layout for the document root.
467///
468/// Walks the root's children in source order, emitting each significant child
469/// (extension attributes, the single top-level value) on its own line(s) and
470/// threading every comment between them so nothing is lost (T007). A leading BOM is
471/// re-inserted by [`Writer::finish`], so it is skipped here.
472fn emit_root(root: &SyntaxNode, w: &mut Writer, config: &FormatConfig) {
473    let policy = config.blank_line_policy();
474    let children: Vec<SyntaxElement> = root.children_with_tokens().collect();
475
476    // Pending trivia buffer between significant children.
477    let mut pending: Vec<SyntaxToken> = Vec::new();
478    // Whether we have emitted any significant content yet (so we know when to start
479    // a fresh line before the next item).
480    let mut emitted_any = false;
481    // Whether the last thing emitted was a significant item on the current line
482    // (so an inline trailing comment can attach to it).
483    let mut last_was_item = false;
484
485    for el in &children {
486        match el {
487            SyntaxElement::Token(t) if t.is_trivia() => {
488                // BOM is layout metadata re-emitted by `finish`; skip it here.
489                if t.kind() != SyntaxKind::Bom {
490                    pending.push(t.clone());
491                }
492            }
493            SyntaxElement::Node(n) if n.kind() == SyntaxKind::ExtensionAttr => {
494                emit_root_pending(&pending, w, policy, last_was_item, &mut emitted_any);
495                pending.clear();
496                if emitted_any {
497                    w.newline();
498                }
499                emit_extension_attr(n, w);
500                emitted_any = true;
501                last_was_item = true;
502            }
503            SyntaxElement::Node(n) if is_value_kind(n.kind()) => {
504                emit_root_pending(&pending, w, policy, last_was_item, &mut emitted_any);
505                pending.clear();
506                if emitted_any {
507                    w.newline();
508                }
509                emit_value_like(n, w, config);
510                emitted_any = true;
511                last_was_item = true;
512            }
513            // Any other node (Error etc.) — emit its verbatim trimmed text so bytes
514            // are never dropped; verification will catch a real problem.
515            SyntaxElement::Node(n) => {
516                emit_root_pending(&pending, w, policy, last_was_item, &mut emitted_any);
517                pending.clear();
518                if emitted_any {
519                    w.newline();
520                }
521                w.write(n.text().trim());
522                emitted_any = true;
523                last_was_item = true;
524            }
525            // A stray significant token at root level (recovery): keep it verbatim.
526            SyntaxElement::Token(t) => {
527                emit_root_pending(&pending, w, policy, last_was_item, &mut emitted_any);
528                pending.clear();
529                if emitted_any {
530                    w.newline();
531                }
532                w.write(t.text());
533                emitted_any = true;
534                last_was_item = true;
535            }
536        }
537    }
538
539    // Trailing comments bound to the root (after the last significant child).
540    emit_root_pending(&pending, w, policy, last_was_item, &mut emitted_any);
541}
542
543/// Emit the buffered root-level trivia (comments) between significant children.
544///
545/// Comments on their own line are emitted at column 0; a comment that shared the
546/// line with the previous item is emitted inline (one space after it). Updates
547/// `emitted_any` when it emits content.
548fn emit_root_pending(
549    pending: &[SyntaxToken],
550    w: &mut Writer,
551    policy: BlankLinePolicy,
552    last_was_item: bool,
553    emitted_any: &mut bool,
554) {
555    if pending.is_empty() {
556        return;
557    }
558    let (inline, leading) = split_pending_trivia(pending);
559
560    // Inline comment(s) on the same line as the previous item.
561    let inline_run = build_trivia_run(&inline, policy, last_was_item);
562    for c in &inline_run.comments {
563        if c.same_line_as_prev && *emitted_any {
564            w.write(" ");
565            w.write(&c.text);
566        } else {
567            if *emitted_any {
568                w.newline();
569                w.blank_lines(c.blanks_before);
570            }
571            w.write(&c.text);
572            *emitted_any = true;
573        }
574    }
575
576    // Own-line leading comments.
577    let leading_run = build_trivia_run(&leading, policy, false);
578    for c in &leading_run.comments {
579        if *emitted_any {
580            w.newline();
581            w.blank_lines(c.blanks_before);
582        }
583        w.write(&c.text);
584        *emitted_any = true;
585    }
586}
587
588/// Emit an extension attribute node verbatim (significant tokens with single
589/// spaces, comments preserved). Extension attrs are rare and structurally fixed
590/// (`#![enable(a, b)]`), so we emit a conservative canonical form.
591fn emit_extension_attr(attr: &SyntaxNode, w: &mut Writer) {
592    // Re-emit the significant tokens with no internal reflow other than trimming —
593    // the canonical form of `#![enable(implicit_some)]` is itself. We rebuild it
594    // from significant tokens to drop any odd internal whitespace, preserving any
595    // comments inline.
596    let mut first = true;
597    let mut prev: Option<SyntaxKind> = None;
598    for el in attr.children_with_tokens() {
599        if let SyntaxElement::Token(t) = el {
600            if t.is_trivia() {
601                if matches!(t.kind(), SyntaxKind::LineComment | SyntaxKind::BlockComment) {
602                    w.write(" ");
603                    w.write(t.text());
604                }
605                continue;
606            }
607            let k = t.kind();
608            if !first {
609                // Space rule inside an extension attr: a space before `enable`-ident
610                // after `[`, and `, ` between idents; none around `#`, `!`, `(`, `)`,
611                // `[`, `]`.
612                if needs_space_in_ext_attr(prev, k) {
613                    w.write(" ");
614                }
615            }
616            w.write(t.text());
617            first = false;
618            prev = Some(k);
619        }
620    }
621}
622
623/// Spacing rule for the two adjacent significant tokens inside an extension attr.
624fn needs_space_in_ext_attr(prev: Option<SyntaxKind>, cur: SyntaxKind) -> bool {
625    match (prev, cur) {
626        // `, ident` → space after comma.
627        (Some(SyntaxKind::Comma), _) => true,
628        _ => false,
629    }
630}
631
632/// Whether `kind` is a value-position node kind.
633fn is_value_kind(kind: SyntaxKind) -> bool {
634    matches!(
635        kind,
636        SyntaxKind::Struct
637            | SyntaxKind::Tuple
638            | SyntaxKind::List
639            | SyntaxKind::Map
640            | SyntaxKind::EnumVariant
641            | SyntaxKind::Unit
642            | SyntaxKind::Literal
643    )
644}
645
646/// Emit any value-like node (dispatch by kind).
647fn emit_value_like(node: &SyntaxNode, w: &mut Writer, config: &FormatConfig) {
648    match node.kind() {
649        SyntaxKind::Literal => emit_literal(node, w),
650        SyntaxKind::Unit => emit_unit(node, w, config),
651        SyntaxKind::Struct => emit_struct(node, w, config),
652        SyntaxKind::Tuple => emit_tuple(node, w, config),
653        SyntaxKind::List => emit_list(node, w, config),
654        SyntaxKind::Map => emit_map(node, w, config),
655        SyntaxKind::EnumVariant => emit_enum_variant(node, w, config),
656        SyntaxKind::Root => emit_root(node, w, config),
657        // Any other node (Error, etc.) should have been rejected earlier; emit its
658        // verbatim text as a last-resort so we never drop bytes (verification will
659        // then decide).
660        _ => w.write(node.text().trim()),
661    }
662}
663
664/// Emit a scalar literal verbatim (its single token text).
665fn emit_literal(node: &SyntaxNode, w: &mut Writer) {
666    if let Some(tok) = node
667        .children_with_tokens()
668        .filter_map(|el| el.as_token().cloned())
669        .find(|t| !t.is_trivia())
670    {
671        w.write(tok.text());
672    }
673}
674
675/// Emit the unit value `()` (or a named empty `Foo()`), preserving any dangling
676/// comment inside the parens (T007).
677///
678/// The parser classifies `Foo()` and `()` — and `Foo(/* c */)` / `(/* c */)` —
679/// as [`SyntaxKind::Unit`]; this emitter therefore handles a possible leading
680/// name and an interior dangling comment so neither the name nor the comment is
681/// ever dropped.
682fn emit_unit(node: &SyntaxNode, w: &mut Writer, config: &FormatConfig) {
683    if let Some(name) = leading_name_token(node) {
684        w.write(name.text());
685    }
686    // No elements; reuse the collection emitter so a dangling comment is threaded.
687    emit_paren_collection(node, &[], w, config, EntryKind::Value);
688}
689
690/// Emit a bare enum variant (`Ident`) or struct-like variant (`Ident { .. }`).
691fn emit_enum_variant(node: &SyntaxNode, w: &mut Writer, config: &FormatConfig) {
692    // The variant name token.
693    if let Some(name) = node.first_token_of(SyntaxKind::Ident) {
694        w.write(name.text());
695    }
696    // A struct-like payload `{ .. }` if present (an LBrace child token).
697    if node
698        .children_with_tokens()
699        .any(|el| el.kind() == SyntaxKind::LBrace)
700    {
701        let entries: Vec<SyntaxNode> = node
702            .children()
703            .filter(|n| n.kind() == SyntaxKind::MapEntry)
704            .collect();
705        emit_brace_collection(node, &entries, w, config, Delim::Brace, EntryKind::MapEntry);
706    }
707}
708
709/// Emit a named or anonymous struct.
710fn emit_struct(node: &SyntaxNode, w: &mut Writer, config: &FormatConfig) {
711    if let Some(name) = node.first_token_of(SyntaxKind::Ident) {
712        w.write(name.text());
713    }
714    let fields: Vec<SyntaxNode> = node
715        .children()
716        .filter(|n| n.kind() == SyntaxKind::StructField)
717        .collect();
718    emit_paren_collection(node, &fields, w, config, EntryKind::StructField);
719}
720
721/// Emit a positional tuple, including a leading name for a tuple-struct /
722/// newtype-variant payload such as `Some(5)` or `Foo(1, 2)`.
723fn emit_tuple(node: &SyntaxNode, w: &mut Writer, config: &FormatConfig) {
724    // A named tuple (tuple struct / variant payload) carries a leading `Ident`
725    // token before its `(`; emit it verbatim so the name is never dropped.
726    if let Some(name) = leading_name_token(node) {
727        w.write(name.text());
728    }
729    let items: Vec<SyntaxNode> = node
730        .children()
731        .filter(|n| is_value_kind(n.kind()))
732        .collect();
733    emit_paren_collection(node, &items, w, config, EntryKind::Value);
734}
735
736/// The leading name `Ident` token of a node (before its first delimiter), if any.
737///
738/// Distinguishes a tuple-struct / named struct (`Foo(..)`) from an anonymous one.
739/// Returns the `Ident` only when it appears before the first `(`/`{`/`[` delimiter
740/// (a name), never a stray ident inside the body.
741fn leading_name_token(node: &SyntaxNode) -> Option<SyntaxToken> {
742    for el in node.children_with_tokens() {
743        match el {
744            SyntaxElement::Token(t) if t.is_trivia() => continue,
745            SyntaxElement::Token(t) if t.kind() == SyntaxKind::Ident => return Some(t),
746            // First non-trivia, non-ident element (a delimiter or a value node):
747            // there is no leading name.
748            _ => return None,
749        }
750    }
751    None
752}
753
754/// Emit a list `[ .. ]`.
755fn emit_list(node: &SyntaxNode, w: &mut Writer, config: &FormatConfig) {
756    let items: Vec<SyntaxNode> = node
757        .children()
758        .filter(|n| is_value_kind(n.kind()))
759        .collect();
760    emit_bracket_collection(node, &items, w, config, EntryKind::Value);
761}
762
763/// Emit a map `{ .. }`.
764fn emit_map(node: &SyntaxNode, w: &mut Writer, config: &FormatConfig) {
765    let entries: Vec<SyntaxNode> = node
766        .children()
767        .filter(|n| n.kind() == SyntaxKind::MapEntry)
768        .collect();
769    emit_brace_collection(node, &entries, w, config, Delim::Brace, EntryKind::MapEntry);
770}
771
772/// The delimiter family of a collection.
773#[derive(Debug, Clone, Copy, PartialEq, Eq)]
774enum Delim {
775    Paren,
776    Bracket,
777    Brace,
778}
779
780impl Delim {
781    fn open(self) -> &'static str {
782        match self {
783            Delim::Paren => "(",
784            Delim::Bracket => "[",
785            Delim::Brace => "{",
786        }
787    }
788    fn close(self) -> &'static str {
789        match self {
790            Delim::Paren => ")",
791            Delim::Bracket => "]",
792            Delim::Brace => "}",
793        }
794    }
795    fn close_kind(self) -> SyntaxKind {
796        match self {
797            Delim::Paren => SyntaxKind::RParen,
798            Delim::Bracket => SyntaxKind::RBracket,
799            Delim::Brace => SyntaxKind::RBrace,
800        }
801    }
802}
803
804/// The kind of element inside a collection (controls how it is emitted).
805#[derive(Debug, Clone, Copy, PartialEq, Eq)]
806enum EntryKind {
807    /// A `field: value` struct field.
808    StructField,
809    /// A `key: value` map entry.
810    MapEntry,
811    /// A bare positional value (list/tuple element).
812    Value,
813}
814
815fn emit_paren_collection(
816    node: &SyntaxNode,
817    elements: &[SyntaxNode],
818    w: &mut Writer,
819    config: &FormatConfig,
820    entry: EntryKind,
821) {
822    emit_collection(node, elements, w, config, Delim::Paren, entry);
823}
824
825fn emit_bracket_collection(
826    node: &SyntaxNode,
827    elements: &[SyntaxNode],
828    w: &mut Writer,
829    config: &FormatConfig,
830    entry: EntryKind,
831) {
832    emit_collection(node, elements, w, config, Delim::Bracket, entry);
833}
834
835fn emit_brace_collection(
836    node: &SyntaxNode,
837    elements: &[SyntaxNode],
838    w: &mut Writer,
839    config: &FormatConfig,
840    delim: Delim,
841    entry: EntryKind,
842) {
843    emit_collection(node, elements, w, config, delim, entry);
844}
845
846/// The core collection emitter: decide single- vs multi-line, then lay out the
847/// elements, threading comments (T007) and applying the trailing-comma oracle
848/// (T008).
849fn emit_collection(
850    node: &SyntaxNode,
851    elements: &[SyntaxNode],
852    w: &mut Writer,
853    config: &FormatConfig,
854    delim: Delim,
855    entry: EntryKind,
856) {
857    // Gather the interior trivia segments around / between elements.
858    let layout = collect_collection_layout(node, elements, config, delim);
859
860    let multiline = decide_multiline(node, &layout, elements.len());
861
862    w.write(delim.open());
863
864    if elements.is_empty() {
865        // Possibly a dangling comment inside an empty collection (T007).
866        emit_empty_collection_body(&layout, w, multiline);
867        w.write(delim.close());
868        return;
869    }
870
871    if multiline {
872        w.indent();
873        for (i, el) in elements.iter().enumerate() {
874            let seg = &layout.before[i];
875            // Newline to start this element's line, plus any leading comments.
876            w.newline();
877            w.blank_lines(if i == 0 { 0 } else { seg_blank(seg) });
878            emit_leading_comments_block(seg, w);
879            emit_entry(el, w, config, entry);
880            // Trailing comma (oracle: multi-line ⇒ every element, incl. last).
881            w.write(",");
882            // An inline trailing comment after this element (e.g. `1, // note`).
883            emit_inline_trailing_comment(&layout.after[i], w);
884        }
885        // Comments that sit just before the closing delimiter (boundary comments).
886        emit_pre_close_comments(&layout.pre_close, w);
887        w.dedent();
888        w.newline();
889        w.write(delim.close());
890    } else {
891        // Single line: `( a, b, c )`-style with no trailing comma.
892        for (i, el) in elements.iter().enumerate() {
893            if i > 0 {
894                w.write(", ");
895            }
896            emit_entry(el, w, config, entry);
897        }
898        w.write(delim.close());
899    }
900}
901
902/// Blank-lines to emit before an element, derived from its leading trivia segment.
903fn seg_blank(seg: &TriviaRun) -> usize {
904    seg.trailing_blanks
905}
906
907/// Emit an element's leading comments, each on its own line (multi-line layout).
908fn emit_leading_comments_block(seg: &TriviaRun, w: &mut Writer) {
909    for c in &seg.comments {
910        w.write(&c.text);
911        w.newline();
912        w.blank_lines(c.blanks_before_or(0));
913    }
914}
915
916impl Comment {
917    /// Blank lines before this comment, or `default` when none recorded.
918    fn blanks_before_or(&self, _default: usize) -> usize {
919        self.blanks_before
920    }
921}
922
923/// Emit an inline trailing comment (same source line as the element) after the
924/// element + comma, e.g. `1, // note`. If the comment was on its own line it is
925/// emitted as a leading comment of the *next* element instead, so here we only
926/// handle the genuinely-inline case.
927fn emit_inline_trailing_comment(seg: &TriviaRun, w: &mut Writer) {
928    for c in &seg.comments {
929        if c.same_line_as_prev {
930            w.write(" ");
931            w.write(&c.text);
932        } else {
933            // Own-line comment trailing the element: put it on its own line.
934            w.newline();
935            w.blank_lines(c.blanks_before);
936            w.write(&c.text);
937        }
938    }
939}
940
941/// Emit comments that sit between the last element and the closing delimiter.
942fn emit_pre_close_comments(seg: &TriviaRun, w: &mut Writer) {
943    for c in &seg.comments {
944        w.newline();
945        w.blank_lines(c.blanks_before);
946        w.write(&c.text);
947    }
948}
949
950/// Emit the body of an empty collection — possibly a dangling comment (T007).
951fn emit_empty_collection_body(layout: &CollectionLayout, w: &mut Writer, multiline: bool) {
952    if layout.pre_close.is_empty() {
953        return;
954    }
955    if multiline {
956        w.indent();
957        for c in &layout.pre_close.comments {
958            w.newline();
959            w.blank_lines(c.blanks_before);
960            w.write(&c.text);
961        }
962        w.dedent();
963        w.newline();
964    } else {
965        // Single-line dangling comment: keep it inline with spaces.
966        for c in &layout.pre_close.comments {
967            w.write(" ");
968            w.write(&c.text);
969            w.write(" ");
970        }
971    }
972}
973
974/// Emit one collection element by kind.
975fn emit_entry(el: &SyntaxNode, w: &mut Writer, config: &FormatConfig, kind: EntryKind) {
976    match kind {
977        EntryKind::StructField => emit_struct_field(el, w, config),
978        EntryKind::MapEntry => emit_map_entry(el, w, config),
979        EntryKind::Value => emit_value_like(el, w, config),
980    }
981}
982
983/// Emit `name: value` for a struct field.
984fn emit_struct_field(field: &SyntaxNode, w: &mut Writer, config: &FormatConfig) {
985    if let Some(name) = field.first_token_of(SyntaxKind::Ident) {
986        w.write(name.text());
987    }
988    w.write(":");
989    if let Some(value) = field.children().find(|n| is_value_kind(n.kind())) {
990        w.write(" ");
991        emit_value_like(&value, w, config);
992    }
993}
994
995/// Emit `key: value` for a map entry (the key can be any value).
996fn emit_map_entry(entry: &SyntaxNode, w: &mut Writer, config: &FormatConfig) {
997    let values: Vec<SyntaxNode> = entry
998        .children()
999        .filter(|n| is_value_kind(n.kind()))
1000        .collect();
1001    if let Some(key) = values.first() {
1002        emit_value_like(key, w, config);
1003    }
1004    w.write(":");
1005    if let Some(value) = values.get(1) {
1006        w.write(" ");
1007        emit_value_like(value, w, config);
1008    }
1009}
1010
1011// =============================================================================
1012// Collection-layout extraction: split a collection's interior trivia into the
1013// segments the emitter consumes (before each element, after each element, and
1014// before the close delimiter).
1015// =============================================================================
1016
1017/// The trivia layout of a collection, indexed alongside its elements.
1018#[derive(Debug, Default)]
1019struct CollectionLayout {
1020    /// Trivia immediately before element `i` (leading comments + blank context).
1021    before: Vec<TriviaRun>,
1022    /// Trivia immediately after element `i` up to the next separator/element
1023    /// (an inline trailing comment lives here).
1024    after: Vec<TriviaRun>,
1025    /// Trivia between the last element (or the open delim, if empty) and the close
1026    /// delimiter — boundary / dangling comments (T007).
1027    pre_close: TriviaRun,
1028    /// `true` if the open delimiter and close delimiter were on different source
1029    /// lines in the input (a primary multi-line signal).
1030    spans_multiple_lines: bool,
1031}
1032
1033/// Walk a collection node's `children_with_tokens` and bucket trivia into
1034/// per-element segments. Elements are identified by their node identity (kind +
1035/// range) in `elements`.
1036fn collect_collection_layout(
1037    node: &SyntaxNode,
1038    elements: &[SyntaxNode],
1039    config: &FormatConfig,
1040    delim: Delim,
1041) -> CollectionLayout {
1042    let policy = config.blank_line_policy();
1043    let mut layout = CollectionLayout {
1044        before: vec![TriviaRun::default(); elements.len()],
1045        after: vec![TriviaRun::default(); elements.len()],
1046        pre_close: TriviaRun::default(),
1047        spans_multiple_lines: false,
1048    };
1049
1050    // Build a flat, ordered list of this node's *direct* children (tokens + nodes),
1051    // so we can scan from the open delimiter to the close delimiter.
1052    let children: Vec<SyntaxElement> = node.children_with_tokens().collect();
1053
1054    // Locate the open delimiter index and the close delimiter index (last matching
1055    // close token at this level).
1056    let open_kind = match delim {
1057        Delim::Paren => SyntaxKind::LParen,
1058        Delim::Bracket => SyntaxKind::LBracket,
1059        Delim::Brace => SyntaxKind::LBrace,
1060    };
1061    let close_kind = delim.close_kind();
1062
1063    let open_idx = children
1064        .iter()
1065        .position(|el| el.kind() == open_kind && el.as_token().is_some());
1066    let close_idx = children
1067        .iter()
1068        .rposition(|el| el.kind() == close_kind && el.as_token().is_some());
1069
1070    let (open_idx, close_idx) = match (open_idx, close_idx) {
1071        (Some(o), Some(c)) if c > o => (o, c),
1072        // Degenerate (recovery) shape: no clean delimiters; treat as single-line
1073        // with no captured trivia. Verification will then decide.
1074        _ => return layout,
1075    };
1076
1077    // Determine element node order: a slice over the interior children that are
1078    // element nodes (in source order).
1079    let element_ranges: Vec<(usize, usize)> = elements
1080        .iter()
1081        .map(|n| {
1082            let r = n.text_range();
1083            (r.start(), r.end())
1084        })
1085        .collect();
1086
1087    // Track newline span between open and close for the multi-line signal.
1088    let mut saw_newline_between = false;
1089
1090    // Pending trivia buffer (whitespace text + comments) accumulating between
1091    // significant items.
1092    let mut pending: Vec<SyntaxToken> = Vec::new();
1093
1094    // The index of the element we last emitted (so a trailing-comment after it can
1095    // be bucketed into `after[that]`). `None` before the first element.
1096    let mut last_element: Option<usize> = None;
1097
1098    // Helper to find which element (if any) a node child corresponds to.
1099    let element_index_of = |start: usize, end: usize| -> Option<usize> {
1100        element_ranges
1101            .iter()
1102            .position(|&(s, e)| s == start && e == end)
1103    };
1104
1105    for el in &children[(open_idx + 1)..close_idx] {
1106        match el {
1107            SyntaxElement::Token(t) if t.is_trivia() => {
1108                if t.text().contains('\n') {
1109                    saw_newline_between = true;
1110                }
1111                pending.push(t.clone());
1112            }
1113            SyntaxElement::Token(t) if t.kind() == SyntaxKind::Comma => {
1114                // A separator: flush pending trivia. Comments before a comma that
1115                // are on the same line as the previous element are inline-trailing
1116                // for that element; otherwise they lead the next element. We attach
1117                // pending here as `after[last_element]`.
1118                let run = build_trivia_run(&pending, policy, /*after_element=*/ true);
1119                if let Some(idx) = last_element {
1120                    merge_run(&mut layout.after[idx], run);
1121                } else {
1122                    // Comma with no preceding element (recovery) — drop into
1123                    // pre_close as a fallback so comments survive.
1124                    merge_run(&mut layout.pre_close, run);
1125                }
1126                pending.clear();
1127            }
1128            SyntaxElement::Node(n) => {
1129                let r = n.text_range();
1130                if let Some(idx) = element_index_of(r.start(), r.end()) {
1131                    // Pending trivia precedes this element. Split it: comments on the
1132                    // same line as the previous element/comma (before the first
1133                    // newline) are inline-trailing for `last_element`; the rest lead
1134                    // this element (T007).
1135                    let (inline, leading) = split_pending_trivia(&pending);
1136                    if let Some(prev) = last_element {
1137                        let inline_run = build_trivia_run(&inline, policy, true);
1138                        merge_run(&mut layout.after[prev], inline_run);
1139                    } else if !inline.is_empty() {
1140                        // No previous element: fold it into this element's leading.
1141                        let inline_run = build_trivia_run(&inline, policy, false);
1142                        merge_run(&mut layout.before[idx], inline_run);
1143                    }
1144                    let leading_run = build_trivia_run(&leading, policy, false);
1145                    merge_run(&mut layout.before[idx], leading_run);
1146                    pending.clear();
1147                    last_element = Some(idx);
1148                } else {
1149                    // A nested non-element node (shouldn't happen for clean trees);
1150                    // keep its preceding trivia bucketed conservatively.
1151                    let run = build_trivia_run(&pending, policy, false);
1152                    merge_run(&mut layout.pre_close, run);
1153                    pending.clear();
1154                }
1155            }
1156            // Any other significant token inside the group (recovery): flush.
1157            SyntaxElement::Token(_) => {
1158                let run = build_trivia_run(&pending, policy, false);
1159                merge_run(&mut layout.pre_close, run);
1160                pending.clear();
1161            }
1162        }
1163    }
1164
1165    // Whatever trivia remains before the close delimiter. Split it: a comment on
1166    // the same line as the last element/comma is inline-trailing for that element;
1167    // the rest are pre-close (boundary / dangling) comments (T007).
1168    let (inline, boundary) = split_pending_trivia(&pending);
1169    if let Some(prev) = last_element {
1170        let inline_run = build_trivia_run(&inline, policy, true);
1171        merge_run(&mut layout.after[prev], inline_run);
1172    } else {
1173        let inline_run = build_trivia_run(&inline, policy, false);
1174        merge_run(&mut layout.pre_close, inline_run);
1175    }
1176    let boundary_run = build_trivia_run(&boundary, policy, false);
1177    merge_run(&mut layout.pre_close, boundary_run);
1178
1179    layout.spans_multiple_lines = saw_newline_between;
1180    layout
1181}
1182
1183/// Split a buffer of trivia tokens at the first line break.
1184///
1185/// Returns `(inline, rest)` where `inline` is the prefix up to and including the
1186/// first whitespace token that contains a newline (so any comment on the same line
1187/// as the preceding significant token stays in `inline`, an inline-trailing
1188/// comment), and `rest` is everything after that newline (leading the next
1189/// element). If there is no newline at all, everything is `inline`.
1190fn split_pending_trivia(tokens: &[SyntaxToken]) -> (Vec<SyntaxToken>, Vec<SyntaxToken>) {
1191    for (i, t) in tokens.iter().enumerate() {
1192        if t.kind() == SyntaxKind::Whitespace && t.text().contains('\n') {
1193            // `inline` = tokens[0..i] (the comments/ws before the first newline);
1194            // `rest` = tokens[i..] (the newline-bearing ws and everything after).
1195            return (tokens[..i].to_vec(), tokens[i..].to_vec());
1196        }
1197    }
1198    (tokens.to_vec(), Vec::new())
1199}
1200
1201/// Merge `src` into `dst` (append comments, take the max blank context).
1202fn merge_run(dst: &mut TriviaRun, src: TriviaRun) {
1203    if src.has_newline {
1204        dst.has_newline = true;
1205    }
1206    dst.trailing_blanks = dst.trailing_blanks.max(src.trailing_blanks);
1207    dst.comments.extend(src.comments);
1208}
1209
1210/// Build a [`TriviaRun`] from a buffer of trivia tokens.
1211///
1212/// `after_element` hints whether the first comment, if it shares the line with the
1213/// preceding significant token (no newline before it), is an inline trailing
1214/// comment.
1215fn build_trivia_run(
1216    tokens: &[SyntaxToken],
1217    policy: BlankLinePolicy,
1218    after_element: bool,
1219) -> TriviaRun {
1220    let mut run = TriviaRun::default();
1221    let mut blanks_acc = 0usize; // raw blank lines accumulated before the next comment
1222    let mut seen_newline = false;
1223    let mut first_comment = true;
1224
1225    for t in tokens {
1226        match t.kind() {
1227            SyntaxKind::Whitespace => {
1228                if t.text().contains('\n') {
1229                    seen_newline = true;
1230                    run.has_newline = true;
1231                }
1232                blanks_acc += count_blank_lines(t.text());
1233            }
1234            SyntaxKind::Bom => {
1235                // BOM only appears at document start; ignore inside collections.
1236            }
1237            SyntaxKind::LineComment | SyntaxKind::BlockComment => {
1238                let same_line = first_comment && after_element && !seen_newline;
1239                run.comments.push(Comment {
1240                    text: t.text().to_string(),
1241                    blanks_before: resolve_blanks(blanks_acc, policy),
1242                    same_line_as_prev: same_line,
1243                });
1244                blanks_acc = 0;
1245                first_comment = false;
1246                // After a line comment the line necessarily ends; after a block
1247                // comment it may not, but treat subsequent comments as own-line.
1248                seen_newline = true;
1249            }
1250            _ => {}
1251        }
1252    }
1253
1254    run.trailing_blanks = resolve_blanks(blanks_acc, policy);
1255    run
1256}
1257
1258/// Decide whether a collection lays out multi-line.
1259///
1260/// Rules (deterministic, T009):
1261/// * empty collection → single-line (unless it carries a dangling comment that
1262///   itself forces multi-line, handled by the comment presence test);
1263/// * a collection whose open/close delimiters were on different source lines →
1264///   multi-line;
1265/// * a collection that contains ANY comment → multi-line (so comments get their
1266///   own clean lines and are never lost);
1267/// * otherwise → single-line.
1268fn decide_multiline(_node: &SyntaxNode, layout: &CollectionLayout, element_count: usize) -> bool {
1269    if layout.spans_multiple_lines {
1270        return true;
1271    }
1272    // Any comment anywhere in the collection forces multi-line so we never have to
1273    // jam a comment onto a crowded single line (and never drop one).
1274    let has_comments = layout.before.iter().any(|r| !r.is_empty())
1275        || layout.after.iter().any(|r| !r.is_empty())
1276        || !layout.pre_close.is_empty();
1277    if has_comments {
1278        return true;
1279    }
1280    let _ = element_count;
1281    false
1282}
1283
1284// =============================================================================
1285// Error detection + semantic verification (T011, T012).
1286// =============================================================================
1287
1288/// `true` if `node` (or any descendant) is an `Error` recovery node.
1289fn subtree_has_errors(node: &SyntaxNode) -> bool {
1290    if node.kind() == SyntaxKind::Error {
1291        return true;
1292    }
1293    node.children().any(|c| subtree_has_errors(&c))
1294}
1295
1296/// Verify two whole-document strings are semantically identical: same significant
1297/// tokens (verbatim) in the same order, and same comments (verbatim) in the same
1298/// order — ignoring only whitespace layout (T012).
1299fn semantically_equal(a: &str, b: &str) -> bool {
1300    semantic_tokens_equal(a, b)
1301}
1302
1303/// Compare the semantic token streams of two RON fragments: significant tokens and
1304/// comments, both verbatim and in order; whitespace and BOM are ignored.
1305///
1306/// This is the verify-before-replace oracle (AD-008): names, ordering, values, and
1307/// comments must be byte-identical (modulo layout). Re-lexing via [`parse`] reuses
1308/// the single engine tokenizer, so no second lexer is introduced.
1309fn semantic_tokens_equal(a: &str, b: &str) -> bool {
1310    let ta = semantic_token_stream(a);
1311    let tb = semantic_token_stream(b);
1312    ta == tb
1313}
1314
1315/// The ordered list of semantically-significant token texts (significant tokens +
1316/// comments, verbatim), with layout-only tokens dropped.
1317///
1318/// Dropped tokens (layout, not data):
1319/// * `Whitespace` / `Bom` — pure layout;
1320/// * `Comma` — a separator whose presence/absence is the formatter's
1321///   trailing-comma canonicalization (T008), never a data change. Element ORDER is
1322///   still verified because the elements' own tokens stay in order between the
1323///   commas.
1324///
1325/// Kept tokens carry the data the formatter MUST preserve: struct/variant names
1326/// (`Ident`), all scalar values, delimiters (so structure cannot silently change),
1327/// and every comment (verbatim).
1328fn semantic_token_stream(src: &str) -> Vec<(SyntaxKind, String)> {
1329    let doc = parse(src);
1330    doc.root()
1331        .descendant_tokens()
1332        .filter(|t| {
1333            !matches!(
1334                t.kind(),
1335                SyntaxKind::Whitespace | SyntaxKind::Bom | SyntaxKind::Comma
1336            )
1337        })
1338        .map(|t| (t.kind(), t.text().to_string()))
1339        .collect()
1340}
1341
1342#[cfg(test)]
1343mod tests {
1344    use super::*;
1345
1346    fn fmt(src: &str) -> String {
1347        let doc = parse(src);
1348        match format(&doc, &FormatConfig::default()) {
1349            FormatResult::Formatted(s) => s,
1350            FormatResult::NoOp { reason } => panic!("unexpected no-op for {src:?}: {reason}"),
1351        }
1352    }
1353
1354    #[test]
1355    fn config_clamps_indent() {
1356        assert_eq!(
1357            FormatConfig::new(0, BlankLinePolicy::Collapse).indent_width(),
1358            1
1359        );
1360        assert_eq!(
1361            FormatConfig::new(99, BlankLinePolicy::Collapse).indent_width(),
1362            16
1363        );
1364        assert_eq!(FormatConfig::default().indent_width(), 4);
1365    }
1366
1367    #[test]
1368    fn single_line_collection_stays_single_line() {
1369        assert_eq!(fmt("[1, 2, 3]"), "[1, 2, 3]\n");
1370        assert_eq!(fmt("[1,2,3]"), "[1, 2, 3]\n");
1371        assert_eq!(fmt("(1, 2)"), "(1, 2)\n");
1372        assert_eq!(fmt("Foo(x: 1, y: 2)"), "Foo(x: 1, y: 2)\n");
1373    }
1374
1375    #[test]
1376    fn single_line_drops_trailing_comma() {
1377        assert_eq!(fmt("[1, 2, 3,]"), "[1, 2, 3]\n");
1378    }
1379
1380    #[test]
1381    fn multiline_gets_trailing_comma_on_every_element() {
1382        let out = fmt("[\n1,\n2,\n3\n]");
1383        assert_eq!(out, "[\n    1,\n    2,\n    3,\n]\n");
1384    }
1385
1386    #[test]
1387    fn multiline_struct_canonical_indent() {
1388        let out = fmt("Foo(\nx: 1,\ny: 2\n)");
1389        assert_eq!(out, "Foo(\n    x: 1,\n    y: 2,\n)\n");
1390    }
1391
1392    #[test]
1393    fn nested_indentation() {
1394        let out = fmt("Foo(\na: [\n1,\n2\n]\n)");
1395        assert_eq!(out, "Foo(\n    a: [\n        1,\n        2,\n    ],\n)\n");
1396    }
1397
1398    #[test]
1399    fn literal_passthrough() {
1400        assert_eq!(fmt("42"), "42\n");
1401        assert_eq!(fmt("  42  "), "42\n");
1402        assert_eq!(fmt("\"hi\""), "\"hi\"\n");
1403        assert_eq!(fmt("true"), "true\n");
1404    }
1405
1406    #[test]
1407    fn unit_value() {
1408        assert_eq!(fmt("()"), "()\n");
1409    }
1410
1411    #[test]
1412    fn comment_forces_multiline_and_is_preserved() {
1413        let out = fmt("[1, 2] // trailing");
1414        // The comment trails the value (root-level), preserved.
1415        assert!(out.contains("// trailing"), "comment lost: {out:?}");
1416    }
1417
1418    #[test]
1419    fn leading_comment_preserved() {
1420        let out = fmt("// header\n42");
1421        assert_eq!(out, "// header\n42\n");
1422    }
1423
1424    #[test]
1425    fn inline_field_comment_preserved() {
1426        let out = fmt("Foo(\nx: 1, // note\ny: 2\n)");
1427        assert!(out.contains("// note"), "inline comment lost: {out:?}");
1428        assert!(
1429            out.contains("x: 1, // note"),
1430            "inline comment misplaced: {out:?}"
1431        );
1432    }
1433
1434    #[test]
1435    fn dangling_comment_in_empty_collection_preserved() {
1436        let out = fmt("[\n// empty\n]");
1437        assert!(out.contains("// empty"), "dangling comment lost: {out:?}");
1438    }
1439
1440    #[test]
1441    fn boundary_comment_before_close_preserved() {
1442        let out = fmt("[\n1,\n// last\n]");
1443        assert!(out.contains("// last"), "boundary comment lost: {out:?}");
1444    }
1445
1446    #[test]
1447    fn idempotent_on_corpus_samples() {
1448        for src in [
1449            "[1, 2, 3]",
1450            "Foo(\nx: 1,\ny: 2\n)",
1451            "// header\n42\n",
1452            "{ \"a\": 1, \"b\": 2 }",
1453            "Foo(\na: [\n1,\n2\n]\n)",
1454        ] {
1455            let once = fmt(src);
1456            let twice = fmt(&once);
1457            assert_eq!(once, twice, "not idempotent for {src:?}");
1458        }
1459    }
1460
1461    #[test]
1462    fn no_op_on_parse_errors() {
1463        let doc = parse("[1, 2");
1464        assert!(format(&doc, &FormatConfig::default()).is_no_op());
1465    }
1466
1467    #[test]
1468    fn extension_attr_preserved() {
1469        let out = fmt("#![enable(implicit_some)]\nSome(5)");
1470        assert!(
1471            out.contains("#![enable(implicit_some)]"),
1472            "ext attr lost: {out:?}"
1473        );
1474        assert!(out.contains("Some(5)"));
1475    }
1476
1477    #[test]
1478    fn map_canonical() {
1479        assert_eq!(fmt("{\"a\":1,\"b\":2}"), "{\"a\": 1, \"b\": 2}\n");
1480    }
1481
1482    #[test]
1483    fn format_node_subtree() {
1484        let doc = parse("Foo(\nx: 1,\ny: 2\n)");
1485        let value = doc
1486            .root()
1487            .children()
1488            .find(|n| n.kind() == SyntaxKind::Struct)
1489            .unwrap();
1490        let res = format_node(&value, &FormatConfig::default());
1491        match res {
1492            FormatResult::Formatted(s) => assert_eq!(s, "Foo(\n    x: 1,\n    y: 2,\n)"),
1493            FormatResult::NoOp { reason } => panic!("subtree no-op: {reason}"),
1494        }
1495    }
1496
1497    #[test]
1498    fn format_node_rejects_non_value_node() {
1499        let doc = parse("Foo(x: 1)");
1500        // A bare StructField is not a clean value-position subtree boundary, so
1501        // Format Selection on it must no-op (T010).
1502        let field = find_kind(&doc.root(), SyntaxKind::StructField).expect("has a field");
1503        assert!(format_node(&field, &FormatConfig::default()).is_no_op());
1504    }
1505
1506    /// Find the first descendant node of `kind` (depth-first), if any.
1507    fn find_kind(node: &SyntaxNode, kind: SyntaxKind) -> Option<SyntaxNode> {
1508        if node.kind() == kind {
1509            return Some(node.clone());
1510        }
1511        for c in node.children() {
1512            if let Some(found) = find_kind(&c, kind) {
1513                return Some(found);
1514            }
1515        }
1516        None
1517    }
1518
1519    #[test]
1520    fn empty_document_stays_empty() {
1521        assert_eq!(fmt(""), "");
1522        assert_eq!(fmt("   "), "");
1523    }
1524
1525    #[test]
1526    fn blank_line_collapse_default() {
1527        let out = fmt("Foo(\nx: 1,\n\n\n\ny: 2\n)");
1528        // Collapse: at most one blank between fields.
1529        assert_eq!(out, "Foo(\n    x: 1,\n\n    y: 2,\n)\n");
1530    }
1531
1532    #[test]
1533    fn blank_line_preserve() {
1534        let doc = parse("Foo(\nx: 1,\n\n\ny: 2\n)");
1535        let cfg = FormatConfig::new(4, BlankLinePolicy::Preserve);
1536        let FormatResult::Formatted(out) = format(&doc, &cfg) else {
1537            panic!("no-op");
1538        };
1539        assert_eq!(out, "Foo(\n    x: 1,\n\n\n    y: 2,\n)\n");
1540    }
1541}