ron2/ast/
fmt.rs

1//! AST formatter for RON documents.
2//!
3//! This module provides formatting of AST expressions and documents.
4//! All serialization flows through this module via [`format_expr`] or [`format_document`].
5//!
6//! # Configuration
7//!
8//! Use [`FormatConfig::new()`] for pretty output with sensible defaults, or
9//! [`FormatConfig::minimal()`] for the most compact output (no whitespace, no comments).
10
11use alloc::string::String;
12
13use crate::ast::{
14    Attribute, AttributeContent, Comment, CommentKind, Document, Expr, StructBody, Trivia,
15};
16
17/// Controls how RON output is formatted.
18#[derive(Clone, Debug)]
19pub struct FormatConfig {
20    /// Indentation string. Empty string = no indentation.
21    pub indent: String,
22
23    /// Spacing style for compact mode.
24    pub spacing: Spacing,
25
26    /// How to handle comments during formatting.
27    pub comments: CommentMode,
28
29    /// Rules for when to use compact (single-line) formatting.
30    pub compaction: Compaction,
31}
32
33/// Spacing style for compact output.
34#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
35pub enum Spacing {
36    /// No spaces: `x:1,y:2`
37    None,
38    /// Normal spacing: `x: 1, y: 2`
39    #[default]
40    Normal,
41}
42
43/// How to handle comments during formatting.
44#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
45pub enum CommentMode {
46    /// Delete all comments from output.
47    Delete,
48    /// Preserve comments. Line comments prevent ANY compaction.
49    Preserve,
50    /// Smart mode: preserve in multiline, convert `// foo` → `/* foo */`
51    /// only when depth/type rules force compaction. Length-based compaction
52    /// is still blocked by line comments.
53    #[default]
54    Auto,
55}
56
57/// Rules for when to switch from multiline to compact formatting.
58///
59/// Compaction is triggered when ANY of these conditions is met (OR logic):
60/// - Collection depth >= `compact_from_depth`
61/// - Collection type is in `compact_types`
62/// - Collection fits within `char_limit`
63///
64/// In compact mode:
65/// - No newlines or indentation
66/// - Line comments converted to block: `// foo` → `/* foo */`
67#[derive(Clone, Debug)]
68pub struct Compaction {
69    /// Maximum line length before forcing multiline.
70    /// Set to 0 to disable length-based compaction.
71    /// Default: 20
72    pub char_limit: usize,
73
74    /// Depth at which to start compacting collections.
75    /// - `None`: No depth-based compaction (default)
76    /// - `Some(0)`: Compact even root collections
77    /// - `Some(1)`: Compact first nesting level and deeper
78    /// - `Some(2)`: Compact second nesting level and deeper
79    pub compact_from_depth: Option<usize>,
80
81    /// Collection types to always compact regardless of length/depth.
82    /// Default: none
83    pub compact_types: CompactTypes,
84}
85
86/// Collection types that can be marked for automatic compaction.
87#[derive(Clone, Debug, Default)]
88#[allow(clippy::struct_excessive_bools)]
89pub struct CompactTypes {
90    /// Compact tuple expressions: `(1, 2, 3)` and `Point(1, 2, 3)`
91    pub tuples: bool,
92    /// Compact array/sequence expressions: `[1, 2, 3]`
93    pub arrays: bool,
94    /// Compact map expressions: `{"a": 1}`
95    pub maps: bool,
96    /// Compact struct field expressions: `Point(x: 1, y: 2)` and `(x: 1, y: 2)`
97    pub structs: bool,
98}
99
100impl Default for FormatConfig {
101    fn default() -> Self {
102        Self {
103            indent: String::from("    "),
104            spacing: Spacing::Normal,
105            comments: CommentMode::Auto,
106            compaction: Compaction::default(),
107        }
108    }
109}
110
111impl Default for Compaction {
112    fn default() -> Self {
113        Self {
114            char_limit: 20,
115            compact_from_depth: None,
116            compact_types: CompactTypes::default(),
117        }
118    }
119}
120
121impl CompactTypes {
122    /// Create `CompactTypes` with all types enabled.
123    #[must_use]
124    pub fn all() -> Self {
125        Self {
126            tuples: true,
127            arrays: true,
128            maps: true,
129            structs: true,
130        }
131    }
132
133    /// Create `CompactTypes` with no types enabled (default).
134    #[must_use]
135    pub fn none() -> Self {
136        Self::default()
137    }
138}
139
140impl FormatConfig {
141    /// Create a new `FormatConfig` with default pretty settings.
142    ///
143    /// Defaults: 4-space indent, normal spacing, auto comments, 80 char limit.
144    #[must_use]
145    pub fn new() -> Self {
146        Self::default()
147    }
148
149    /// Create a minimal `FormatConfig` for most compact output.
150    ///
151    /// No whitespace, no comments, fully compact.
152    /// Output example: `Point(x:1,y:2)`
153    #[must_use]
154    pub fn minimal() -> Self {
155        Self {
156            indent: String::new(),
157            spacing: Spacing::None,
158            comments: CommentMode::Delete,
159            compaction: Compaction {
160                char_limit: usize::MAX,
161                compact_from_depth: Some(0),
162                compact_types: CompactTypes::default(),
163            },
164        }
165    }
166
167    /// Set the indentation string.
168    #[must_use]
169    pub fn indent(mut self, indent: impl Into<String>) -> Self {
170        self.indent = indent.into();
171        self
172    }
173
174    /// Set the spacing style.
175    #[must_use]
176    pub fn spacing(mut self, spacing: Spacing) -> Self {
177        self.spacing = spacing;
178        self
179    }
180
181    /// Set the comment handling mode.
182    #[must_use]
183    pub fn comments(mut self, comments: CommentMode) -> Self {
184        self.comments = comments;
185        self
186    }
187
188    /// Set the character limit for compact formatting.
189    /// Set to 0 to disable length-based compaction.
190    #[must_use]
191    pub fn char_limit(mut self, char_limit: usize) -> Self {
192        self.compaction.char_limit = char_limit;
193        self
194    }
195
196    /// Set the depth at which to start compacting.
197    #[must_use]
198    pub fn compact_from_depth(mut self, depth: usize) -> Self {
199        self.compaction.compact_from_depth = Some(depth);
200        self
201    }
202
203    /// Set the collection types to always compact.
204    #[must_use]
205    pub fn compact_types(mut self, types: CompactTypes) -> Self {
206        self.compaction.compact_types = types;
207        self
208    }
209}
210
211// ============================================================================
212// ItemTrivia - Unified trivia abstraction for collection items
213// ============================================================================
214
215/// Unified trivia abstraction for collection items.
216///
217/// Different collection types have trivia in different positions:
218/// - `SeqItem`: `leading`, `trailing`
219/// - `MapEntry`: `leading`, `pre_colon`, `post_colon`, `trailing`
220/// - `StructField`: `leading`, `pre_colon`, `post_colon`, `trailing`
221///
222/// `ItemTrivia` captures all possible positions with `Option` for each.
223#[derive(Default, Clone, Copy)]
224pub struct ItemTrivia<'a> {
225    /// Trivia before the item (e.g., leading comments).
226    pub leading: Option<&'a Trivia<'a>>,
227    /// Trivia between key and colon (for key-value items).
228    pub pre_colon: Option<&'a Trivia<'a>>,
229    /// Trivia between colon and value (for key-value items).
230    pub post_colon: Option<&'a Trivia<'a>>,
231    /// Trivia after the item (e.g., trailing comments).
232    pub trailing: Option<&'a Trivia<'a>>,
233}
234
235impl<'a> ItemTrivia<'a> {
236    /// Create empty trivia (for Value serialization where there is no trivia).
237    #[inline]
238    #[must_use]
239    pub const fn empty() -> Self {
240        Self {
241            leading: None,
242            pre_colon: None,
243            post_colon: None,
244            trailing: None,
245        }
246    }
247
248    /// Create trivia for sequence-like items (leading and trailing only).
249    #[inline]
250    #[must_use]
251    pub const fn seq(leading: &'a Trivia<'a>, trailing: &'a Trivia<'a>) -> Self {
252        Self {
253            leading: Some(leading),
254            pre_colon: None,
255            post_colon: None,
256            trailing: Some(trailing),
257        }
258    }
259
260    /// Create trivia for key-value items (all positions).
261    #[inline]
262    #[must_use]
263    pub const fn kv(
264        leading: &'a Trivia<'a>,
265        pre_colon: &'a Trivia<'a>,
266        post_colon: &'a Trivia<'a>,
267        trailing: &'a Trivia<'a>,
268    ) -> Self {
269        Self {
270            leading: Some(leading),
271            pre_colon: Some(pre_colon),
272            post_colon: Some(post_colon),
273            trailing: Some(trailing),
274        }
275    }
276
277    /// Check if any trivia position contains a line comment.
278    #[inline]
279    #[must_use]
280    pub fn has_line_comment(&self) -> bool {
281        // Short-circuit: avoid array allocation and full iteration
282        self.leading.is_some_and(has_line_comment)
283            || self.pre_colon.is_some_and(has_line_comment)
284            || self.post_colon.is_some_and(has_line_comment)
285            || self.trailing.is_some_and(has_line_comment)
286    }
287}
288
289// ============================================================================
290// SerializeRon trait
291// ============================================================================
292
293/// Trait for types that can be serialized to RON format.
294///
295/// This trait provides a unified interface for serializing both:
296/// - `Expr<'a>` (AST with trivia/comments preserved)
297/// - `Value` (semantic values without trivia)
298pub trait SerializeRon {
299    /// Serialize this value to the formatter.
300    fn serialize(&self, fmt: &mut RonFormatter<'_>);
301}
302
303impl SerializeRon for Expr<'_> {
304    fn serialize(&self, fmt: &mut RonFormatter<'_>) {
305        match self {
306            // Primitives - use raw representation to preserve formatting
307            Expr::Unit(_) => fmt.write_str("()"),
308            Expr::Bool(b) => fmt.write_str(if b.value { "true" } else { "false" }),
309            Expr::Char(c) => fmt.write_str(&c.raw),
310            Expr::Byte(b) => fmt.write_str(&b.raw),
311            Expr::Number(n) => fmt.write_str(&n.raw),
312            Expr::String(s) => fmt.write_str(&s.raw),
313            Expr::Bytes(b) => fmt.write_str(&b.raw),
314
315            // Option
316            Expr::Option(opt) => {
317                let value = opt
318                    .value
319                    .as_ref()
320                    .map(|v| (&v.expr, ItemTrivia::seq(&v.leading, &v.trailing)));
321                fmt.format_option_with(value);
322            }
323
324            // Sequence - use AST-specific method (no allocation)
325            Expr::Seq(seq) => fmt.format_seq_items(seq),
326
327            // Map - use AST-specific method (no allocation)
328            Expr::Map(map) => fmt.format_map_items(map),
329
330            // Tuple - use AST-specific method (no allocation)
331            Expr::Tuple(tuple) => fmt.format_tuple_items(tuple),
332
333            // Anonymous struct - use AST-specific method (no allocation)
334            Expr::AnonStruct(s) => fmt.format_anon_struct_items(s),
335
336            // Named struct
337            Expr::Struct(s) => {
338                fmt.write_str(&s.name.name);
339                if let Some(ref body) = s.body {
340                    match body {
341                        StructBody::Tuple(tuple) => fmt.format_tuple_body(tuple),
342                        StructBody::Fields(fields) => fmt.format_fields_body(fields),
343                    }
344                }
345            }
346
347            // Error placeholder
348            Expr::Error(_) => fmt.write_str("/* parse error */"),
349        }
350    }
351}
352
353// ============================================================================
354// Public API
355// ============================================================================
356
357/// Format a RON document with the given configuration.
358///
359/// This preserves comments while applying consistent formatting.
360/// For Pretty mode, the output always ends with a newline.
361///
362/// Root-level collections are always formatted multiline in Pretty mode,
363/// while nested collections use compact format if they fit within the character limit.
364///
365/// # Example
366///
367/// ```
368/// use ron2::ast::{parse_document, format_document, FormatConfig};
369///
370/// let source = "Config(x:1,y:2)";
371/// let doc = parse_document(source).unwrap();
372/// let formatted = format_document(&doc, &FormatConfig::default());
373/// // Root collections are always multiline in Pretty mode
374/// assert_eq!(formatted, "Config(\n    x: 1,\n    y: 2,\n)\n");
375/// ```
376#[must_use]
377pub fn format_document(doc: &Document<'_>, config: &FormatConfig) -> String {
378    let mut formatter = RonFormatter::new(config);
379    formatter.format_document(doc);
380    formatter.output
381}
382
383/// Format a RON expression with the given configuration.
384///
385/// This is the primary entry point for serializing values to RON strings.
386/// Unlike [`format_document`], this does not include document-level concerns
387/// like attributes or trailing newlines.
388///
389/// # Example
390///
391/// ```
392/// use ron2::ast::{format_expr, FormatConfig, value_to_expr};
393/// use ron2::Value;
394///
395/// let value = Value::Seq(vec![Value::Number(1.into()), Value::Number(2.into())]);
396/// let expr = value_to_expr(value);
397/// let minimal = format_expr(&expr, &FormatConfig::minimal());
398/// assert_eq!(minimal, "[1,2]");
399/// ```
400#[must_use]
401pub fn format_expr(expr: &Expr<'_>, config: &FormatConfig) -> String {
402    let mut formatter = RonFormatter::new(config);
403    // For standalone expression formatting, don't force root to be multiline
404    // Set is_root based on whether we have a non-empty indent (pretty mode behavior)
405    formatter.is_root = !config.indent.is_empty();
406    expr.serialize(&mut formatter);
407    formatter.output
408}
409
410/// Serialize any `SerializeRon` type to a RON string with default configuration.
411///
412/// This is a convenience function for quick serialization. For custom formatting
413/// options, use [`to_ron_string_with`].
414///
415/// # Example
416///
417/// ```
418/// use ron2::ast::to_ron_string;
419/// use ron2::Value;
420///
421/// let value = Value::Seq(vec![Value::Number(1.into()), Value::Number(2.into())]);
422/// let ron = to_ron_string(&value);
423/// // Output will be formatted with default settings
424/// ```
425#[must_use]
426pub fn to_ron_string<T: SerializeRon>(value: &T) -> String {
427    to_ron_string_with(value, &FormatConfig::default())
428}
429
430/// Serialize any `SerializeRon` type to a RON string with custom configuration.
431///
432/// # Example
433///
434/// ```
435/// use ron2::ast::{to_ron_string_with, FormatConfig};
436/// use ron2::Value;
437///
438/// let value = Value::Seq(vec![Value::Number(1.into()), Value::Number(2.into())]);
439/// let ron = to_ron_string_with(&value, &FormatConfig::minimal());
440/// assert_eq!(ron, "[1,2]");
441/// ```
442#[must_use]
443pub fn to_ron_string_with<T: SerializeRon>(value: &T, config: &FormatConfig) -> String {
444    let mut formatter = RonFormatter::new(config);
445    // For value serialization, set is_root based on pretty mode
446    formatter.is_root = !config.indent.is_empty();
447    value.serialize(&mut formatter);
448    formatter.output
449}
450
451/// Collection types for compaction decisions.
452#[derive(Clone, Copy, Debug, PartialEq, Eq)]
453enum CollectionType {
454    Tuple,
455    Array,
456    Map,
457    Struct,
458}
459
460/// Internal formatter state.
461pub struct RonFormatter<'a> {
462    config: &'a FormatConfig,
463    output: String,
464    indent_level: usize,
465    /// Current nesting depth (0 = root level).
466    depth: usize,
467    /// Whether we're formatting the root expression (forces multiline in Pretty mode).
468    is_root: bool,
469    /// Whether we're in compact mode (single line, no indentation).
470    is_compact: bool,
471}
472
473impl<'a> RonFormatter<'a> {
474    fn new(config: &'a FormatConfig) -> Self {
475        Self {
476            config,
477            output: String::new(),
478            indent_level: 0,
479            depth: 0,
480            is_root: true,
481            is_compact: false,
482        }
483    }
484
485    /// Returns the char limit from config.
486    fn char_limit(&self) -> usize {
487        self.config.compaction.char_limit
488    }
489
490    /// Determine if compaction is requested by depth or type rules.
491    ///
492    /// Returns true if depth-based or type-based compaction rules apply.
493    /// Length-based compaction is handled separately in `format_collection`.
494    fn wants_compact(&self, collection_type: CollectionType, depth: usize) -> bool {
495        let compaction = &self.config.compaction;
496
497        // 1. Depth-based
498        if let Some(threshold) = compaction.compact_from_depth
499            && depth >= threshold
500        {
501            return true;
502        }
503
504        // 2. Type-based
505        match collection_type {
506            CollectionType::Tuple => compaction.compact_types.tuples,
507            CollectionType::Array => compaction.compact_types.arrays,
508            CollectionType::Map => compaction.compact_types.maps,
509            CollectionType::Struct => compaction.compact_types.structs,
510        }
511    }
512
513    /// Check if we're in "pretty" mode (non-empty indent).
514    fn is_pretty(&self) -> bool {
515        !self.config.indent.is_empty()
516    }
517
518    fn format_document(&mut self, doc: &Document<'_>) {
519        let is_pretty = self.is_pretty();
520
521        // Format leading comments (before attributes) - Pretty mode only
522        self.format_leading_comments(&doc.leading);
523
524        // Format attributes (one per line in Pretty mode, inline in others)
525        for attr in &doc.attributes {
526            self.format_attribute(attr);
527        }
528
529        // Empty line between attributes and value (if both exist) - Pretty mode only
530        if is_pretty && !doc.attributes.is_empty() && doc.value.is_some() {
531            self.output.push('\n');
532        }
533
534        // Format pre-value comments - Pretty mode only
535        self.format_leading_comments(&doc.pre_value);
536
537        // Format the main value
538        if let Some(ref value) = doc.value {
539            value.serialize(self);
540        }
541
542        // Format trailing comments (at end of document) - Pretty mode only
543        self.format_leading_comments(&doc.trailing);
544
545        // Ensure file ends with newline if it has content - Pretty mode only
546        if is_pretty && !self.output.is_empty() && !self.output.ends_with('\n') {
547            self.output.push('\n');
548        }
549    }
550
551    fn format_attribute(&mut self, attr: &Attribute<'_>) {
552        // Leading comments for this attribute
553        self.format_leading_comments(&attr.leading);
554
555        self.output.push_str("#![");
556        self.output.push_str(&attr.name);
557
558        match &attr.content {
559            AttributeContent::None => {}
560            AttributeContent::Value(v) => {
561                self.output.push_str(" = ");
562                self.output.push_str(v);
563            }
564            AttributeContent::Args(args) => {
565                self.output.push('(');
566                for (i, arg) in args.iter().enumerate() {
567                    if i > 0 {
568                        self.output.push_str(", ");
569                    }
570                    self.output.push_str(arg);
571                }
572                self.output.push(')');
573            }
574        }
575
576        self.output.push_str("]\n");
577    }
578
579    // =========================================================================
580    // AST-specific format methods (zero allocation)
581    // =========================================================================
582
583    /// Format sequence items directly from AST (no allocation).
584    pub fn format_seq_items(&mut self, seq: &crate::ast::SeqExpr<'_>) {
585        let has_line_comments = seq
586            .items
587            .iter()
588            .any(|item| has_line_comment(&item.leading) || has_line_comment(&item.trailing))
589            || has_line_comment(&seq.leading)
590            || has_line_comment(&seq.trailing);
591
592        self.format_collection_generic(
593            CollectionType::Array,
594            '[',
595            ']',
596            &seq.leading,
597            &seq.trailing,
598            &seq.items,
599            has_line_comments,
600            |fmt, item| {
601                fmt.format_leading_comments(&item.leading);
602                fmt.write_indent();
603                item.expr.serialize(fmt);
604                fmt.format_trailing_inline_comment(&item.trailing);
605            },
606        );
607    }
608
609    /// Format tuple elements directly from AST - `TupleExpr` variant (no allocation).
610    pub fn format_tuple_items(&mut self, tuple: &crate::ast::TupleExpr<'_>) {
611        let has_line_comments = tuple
612            .elements
613            .iter()
614            .any(|e| has_line_comment(&e.leading) || has_line_comment(&e.trailing))
615            || has_line_comment(&tuple.leading)
616            || has_line_comment(&tuple.trailing);
617
618        self.format_collection_generic(
619            CollectionType::Tuple,
620            '(',
621            ')',
622            &tuple.leading,
623            &tuple.trailing,
624            &tuple.elements,
625            has_line_comments,
626            |fmt, elem| {
627                fmt.format_leading_comments(&elem.leading);
628                fmt.write_indent();
629                elem.expr.serialize(fmt);
630                fmt.format_trailing_inline_comment(&elem.trailing);
631            },
632        );
633    }
634
635    /// Format tuple elements directly from AST - `TupleBody` variant (no allocation).
636    pub fn format_tuple_body(&mut self, tuple: &crate::ast::TupleBody<'_>) {
637        let has_line_comments = tuple
638            .elements
639            .iter()
640            .any(|e| has_line_comment(&e.leading) || has_line_comment(&e.trailing))
641            || has_line_comment(&tuple.leading)
642            || has_line_comment(&tuple.trailing);
643
644        self.format_collection_generic(
645            CollectionType::Tuple,
646            '(',
647            ')',
648            &tuple.leading,
649            &tuple.trailing,
650            &tuple.elements,
651            has_line_comments,
652            |fmt, elem| {
653                fmt.format_leading_comments(&elem.leading);
654                fmt.write_indent();
655                elem.expr.serialize(fmt);
656                fmt.format_trailing_inline_comment(&elem.trailing);
657            },
658        );
659    }
660
661    /// Format map entries directly from AST (no allocation).
662    pub fn format_map_items(&mut self, map: &crate::ast::MapExpr<'_>) {
663        let has_line_comments = map.entries.iter().any(|e| {
664            has_line_comment(&e.leading)
665                || has_line_comment(&e.pre_colon)
666                || has_line_comment(&e.post_colon)
667                || has_line_comment(&e.trailing)
668        }) || has_line_comment(&map.leading)
669            || has_line_comment(&map.trailing);
670
671        self.format_collection_generic(
672            CollectionType::Map,
673            '{',
674            '}',
675            &map.leading,
676            &map.trailing,
677            &map.entries,
678            has_line_comments,
679            |fmt, entry| {
680                fmt.format_leading_comments(&entry.leading);
681                fmt.write_indent();
682                entry.key.serialize(fmt);
683                fmt.format_trailing_inline_comment(&entry.pre_colon);
684                fmt.write_colon();
685                fmt.format_leading_comments_inline(&entry.post_colon);
686                entry.value.serialize(fmt);
687                fmt.format_trailing_inline_comment(&entry.trailing);
688            },
689        );
690    }
691
692    /// Format anonymous struct fields directly from AST (no allocation).
693    pub fn format_anon_struct_items(&mut self, s: &crate::ast::AnonStructExpr<'_>) {
694        let has_line_comments = s.fields.iter().any(|f| {
695            has_line_comment(&f.leading)
696                || has_line_comment(&f.pre_colon)
697                || has_line_comment(&f.post_colon)
698                || has_line_comment(&f.trailing)
699        }) || has_line_comment(&s.leading)
700            || has_line_comment(&s.trailing);
701
702        self.format_collection_generic(
703            CollectionType::Struct,
704            '(',
705            ')',
706            &s.leading,
707            &s.trailing,
708            &s.fields,
709            has_line_comments,
710            |fmt, field| {
711                fmt.format_leading_comments(&field.leading);
712                fmt.write_indent();
713                fmt.write_str(&field.name.name);
714                fmt.format_trailing_inline_comment(&field.pre_colon);
715                fmt.write_colon();
716                fmt.format_leading_comments_inline(&field.post_colon);
717                field.value.serialize(fmt);
718                fmt.format_trailing_inline_comment(&field.trailing);
719            },
720        );
721    }
722
723    /// Format struct fields directly from AST - `FieldsBody` variant (no allocation).
724    pub fn format_fields_body(&mut self, fields: &crate::ast::FieldsBody<'_>) {
725        let has_line_comments = fields.fields.iter().any(|f| {
726            has_line_comment(&f.leading)
727                || has_line_comment(&f.pre_colon)
728                || has_line_comment(&f.post_colon)
729                || has_line_comment(&f.trailing)
730        }) || has_line_comment(&fields.leading)
731            || has_line_comment(&fields.trailing);
732
733        self.format_collection_generic(
734            CollectionType::Struct,
735            '(',
736            ')',
737            &fields.leading,
738            &fields.trailing,
739            &fields.fields,
740            has_line_comments,
741            |fmt, field| {
742                fmt.format_leading_comments(&field.leading);
743                fmt.write_indent();
744                fmt.write_str(&field.name.name);
745                fmt.format_trailing_inline_comment(&field.pre_colon);
746                fmt.write_colon();
747                fmt.format_leading_comments_inline(&field.post_colon);
748                field.value.serialize(fmt);
749                fmt.format_trailing_inline_comment(&field.trailing);
750            },
751        );
752    }
753
754    // =========================================================================
755    // Generic collection methods for SerializeRon
756    // =========================================================================
757
758    /// Format a sequence `[a, b, c]` from a pre-collected slice.
759    pub fn format_seq_slice<'t, T>(
760        &mut self,
761        leading: &Trivia<'_>,
762        trailing: &Trivia<'_>,
763        items: &[(ItemTrivia<'t>, &'t T)],
764    ) where
765        T: SerializeRon + 't,
766    {
767        let has_line_comments = items.iter().any(|(trivia, _)| trivia.has_line_comment())
768            || has_line_comment(leading)
769            || has_line_comment(trailing);
770
771        self.format_collection_generic(
772            CollectionType::Array,
773            '[',
774            ']',
775            leading,
776            trailing,
777            items,
778            has_line_comments,
779            |fmt, &(trivia, item)| {
780                fmt.format_leading_comments_opt(trivia.leading);
781                fmt.write_indent();
782                item.serialize(fmt);
783                fmt.format_trailing_inline_comment_opt(trivia.trailing);
784            },
785        );
786    }
787
788    /// Format a sequence `[a, b, c]` using `SerializeRon` and `ItemTrivia`.
789    pub fn format_seq_with<'t, T, I>(
790        &mut self,
791        leading: Option<&Trivia<'_>>,
792        trailing: Option<&Trivia<'_>>,
793        items: I,
794    ) where
795        T: SerializeRon + 't,
796        I: IntoIterator<Item = (ItemTrivia<'t>, &'t T)>,
797    {
798        let empty_trivia = Trivia::empty();
799        let leading_ref = leading.unwrap_or(&empty_trivia);
800        let trailing_ref = trailing.unwrap_or(&empty_trivia);
801        let items_vec: Vec<_> = items.into_iter().collect();
802        self.format_seq_slice(leading_ref, trailing_ref, &items_vec);
803    }
804
805    /// Format a tuple `(a, b, c)` from a pre-collected slice.
806    pub fn format_tuple_slice<'t, T>(
807        &mut self,
808        leading: &Trivia<'_>,
809        trailing: &Trivia<'_>,
810        items: &[(ItemTrivia<'t>, &'t T)],
811    ) where
812        T: SerializeRon + 't,
813    {
814        let has_line_comments = items.iter().any(|(trivia, _)| trivia.has_line_comment())
815            || has_line_comment(leading)
816            || has_line_comment(trailing);
817
818        self.format_collection_generic(
819            CollectionType::Tuple,
820            '(',
821            ')',
822            leading,
823            trailing,
824            items,
825            has_line_comments,
826            |fmt, &(trivia, item)| {
827                fmt.format_leading_comments_opt(trivia.leading);
828                fmt.write_indent();
829                item.serialize(fmt);
830                fmt.format_trailing_inline_comment_opt(trivia.trailing);
831            },
832        );
833    }
834
835    /// Format a tuple `(a, b, c)` using `SerializeRon` and `ItemTrivia`.
836    pub fn format_tuple_with<'t, T, I>(
837        &mut self,
838        leading: Option<&Trivia<'_>>,
839        trailing: Option<&Trivia<'_>>,
840        items: I,
841    ) where
842        T: SerializeRon + 't,
843        I: IntoIterator<Item = (ItemTrivia<'t>, &'t T)>,
844    {
845        let empty_trivia = Trivia::empty();
846        let leading_ref = leading.unwrap_or(&empty_trivia);
847        let trailing_ref = trailing.unwrap_or(&empty_trivia);
848        let items_vec: Vec<_> = items.into_iter().collect();
849        self.format_tuple_slice(leading_ref, trailing_ref, &items_vec);
850    }
851
852    /// Format a map `{k: v, ...}` from a pre-collected slice.
853    pub fn format_map_slice<'t, K, V>(
854        &mut self,
855        leading: &Trivia<'_>,
856        trailing: &Trivia<'_>,
857        entries: &[(ItemTrivia<'t>, &'t K, &'t V)],
858    ) where
859        K: SerializeRon + 't,
860        V: SerializeRon + 't,
861    {
862        let has_line_comments = entries
863            .iter()
864            .any(|(trivia, _, _)| trivia.has_line_comment())
865            || has_line_comment(leading)
866            || has_line_comment(trailing);
867
868        self.format_collection_generic(
869            CollectionType::Map,
870            '{',
871            '}',
872            leading,
873            trailing,
874            entries,
875            has_line_comments,
876            |fmt, &(trivia, key, value)| {
877                fmt.format_leading_comments_opt(trivia.leading);
878                fmt.write_indent();
879                key.serialize(fmt);
880                fmt.format_trailing_inline_comment_opt(trivia.pre_colon);
881                fmt.write_colon();
882                fmt.format_leading_comments_inline_opt(trivia.post_colon);
883                value.serialize(fmt);
884                fmt.format_trailing_inline_comment_opt(trivia.trailing);
885            },
886        );
887    }
888
889    /// Format a map `{k: v, ...}` using `SerializeRon` and `ItemTrivia`.
890    pub fn format_map_with<'t, K, V, I>(
891        &mut self,
892        leading: Option<&Trivia<'_>>,
893        trailing: Option<&Trivia<'_>>,
894        entries: I,
895    ) where
896        K: SerializeRon + 't,
897        V: SerializeRon + 't,
898        I: IntoIterator<Item = (ItemTrivia<'t>, &'t K, &'t V)>,
899    {
900        let empty_trivia = Trivia::empty();
901        let leading_ref = leading.unwrap_or(&empty_trivia);
902        let trailing_ref = trailing.unwrap_or(&empty_trivia);
903        let entries_vec: Vec<_> = entries.into_iter().collect();
904        self.format_map_slice(leading_ref, trailing_ref, &entries_vec);
905    }
906
907    /// Format an anonymous struct `(x: 1, y: 2)` from a pre-collected slice.
908    pub fn format_anon_struct_slice<'t, V>(
909        &mut self,
910        leading: &Trivia<'_>,
911        trailing: &Trivia<'_>,
912        fields: &[(ItemTrivia<'t>, &'t str, &'t V)],
913    ) where
914        V: SerializeRon + 't,
915    {
916        let has_line_comments = fields
917            .iter()
918            .any(|(trivia, _, _)| trivia.has_line_comment())
919            || has_line_comment(leading)
920            || has_line_comment(trailing);
921
922        self.format_collection_generic(
923            CollectionType::Struct,
924            '(',
925            ')',
926            leading,
927            trailing,
928            fields,
929            has_line_comments,
930            |fmt, &(trivia, name, value)| {
931                fmt.format_leading_comments_opt(trivia.leading);
932                fmt.write_indent();
933                fmt.write_str(name);
934                fmt.format_trailing_inline_comment_opt(trivia.pre_colon);
935                fmt.write_colon();
936                fmt.format_leading_comments_inline_opt(trivia.post_colon);
937                value.serialize(fmt);
938                fmt.format_trailing_inline_comment_opt(trivia.trailing);
939            },
940        );
941    }
942
943    /// Format an anonymous struct `(x: 1, y: 2)` using field name as `&str`.
944    pub fn format_anon_struct_with<'t, V, I>(
945        &mut self,
946        leading: Option<&Trivia<'_>>,
947        trailing: Option<&Trivia<'_>>,
948        fields: I,
949    ) where
950        V: SerializeRon + 't,
951        I: IntoIterator<Item = (ItemTrivia<'t>, &'t str, &'t V)>,
952    {
953        let empty_trivia = Trivia::empty();
954        let leading_ref = leading.unwrap_or(&empty_trivia);
955        let trailing_ref = trailing.unwrap_or(&empty_trivia);
956        let fields_vec: Vec<_> = fields.into_iter().collect();
957        self.format_anon_struct_slice(leading_ref, trailing_ref, &fields_vec);
958    }
959
960    /// Format struct fields body `(x: 1, y: 2)` for named structs.
961    pub fn format_struct_fields_with<'t, V, I>(
962        &mut self,
963        leading: Option<&Trivia<'_>>,
964        trailing: Option<&Trivia<'_>>,
965        fields: I,
966    ) where
967        V: SerializeRon + 't,
968        I: IntoIterator<Item = (ItemTrivia<'t>, &'t str, &'t V)>,
969        I::IntoIter: Clone,
970    {
971        // Same as anon_struct but for named structs
972        self.format_anon_struct_with(leading, trailing, fields);
973    }
974
975    /// Format Option: `Some(value)` or `None`.
976    pub fn format_option_with<T: SerializeRon>(&mut self, value: Option<(&T, ItemTrivia<'_>)>) {
977        match value {
978            Some((inner, trivia)) => {
979                self.write_str("Some(");
980                self.format_leading_comments_opt(trivia.leading);
981                inner.serialize(self);
982                self.format_trailing_inline_comment_opt(trivia.trailing);
983                self.write_char(')');
984            }
985            None => self.write_str("None"),
986        }
987    }
988
989    /// Generic collection formatting with pre-computed line comment info.
990    #[allow(clippy::too_many_arguments)]
991    fn format_collection_generic<T, F>(
992        &mut self,
993        collection_type: CollectionType,
994        open: char,
995        close: char,
996        leading: &Trivia<'_>,
997        trailing: &Trivia<'_>,
998        items: &[T],
999        has_line_comments: bool,
1000        format_item: F,
1001    ) where
1002        F: Fn(&mut Self, &T),
1003    {
1004        // Track depth for nested collections
1005        let current_depth = self.depth;
1006        self.depth += 1;
1007
1008        let is_root = self.is_root;
1009        if is_root {
1010            self.is_root = false;
1011        }
1012
1013        // Check if depth/type rules want compaction
1014        let wants_compact = self.wants_compact(collection_type, current_depth);
1015
1016        // Determine if we can actually compact based on comment mode
1017        let can_compact = match self.config.comments {
1018            CommentMode::Delete => true,
1019            CommentMode::Preserve => !has_line_comments,
1020            CommentMode::Auto => {
1021                if wants_compact {
1022                    true
1023                } else {
1024                    !has_line_comments
1025                }
1026            }
1027        };
1028
1029        // 1. If depth/type rules want compaction AND we can compact
1030        if wants_compact && can_compact {
1031            let compact = self.try_format_compact_generic(
1032                open,
1033                close,
1034                leading,
1035                trailing,
1036                items,
1037                &format_item,
1038            );
1039            self.output.push_str(&compact);
1040            self.depth = current_depth;
1041            return;
1042        }
1043
1044        // 2. Root collections default to multiline
1045        if is_root && !items.is_empty() {
1046            self.format_multiline_generic(open, close, leading, trailing, items, format_item);
1047            self.depth = current_depth;
1048            return;
1049        }
1050
1051        // 3. Can't compact due to comments - use multiline
1052        if !can_compact {
1053            self.format_multiline_generic(open, close, leading, trailing, items, format_item);
1054            self.depth = current_depth;
1055            return;
1056        }
1057
1058        // 4. Try length-based compaction
1059        let compact =
1060            self.try_format_compact_generic(open, close, leading, trailing, items, &format_item);
1061        if compact.len() <= self.char_limit() {
1062            self.output.push_str(&compact);
1063        } else {
1064            self.format_multiline_generic(open, close, leading, trailing, items, format_item);
1065        }
1066        self.depth = current_depth;
1067    }
1068
1069    fn try_format_compact_generic<T, F>(
1070        &self,
1071        open: char,
1072        close: char,
1073        leading: &Trivia<'_>,
1074        trailing: &Trivia<'_>,
1075        items: &[T],
1076        format_item: F,
1077    ) -> String
1078    where
1079        F: Fn(&mut Self, &T),
1080    {
1081        let mut compact_formatter = RonFormatter::new(self.config);
1082        compact_formatter.is_root = false;
1083        compact_formatter.is_compact = true;
1084        compact_formatter.depth = self.depth;
1085        compact_formatter.output.push(open);
1086
1087        // Leading trivia (converted to compact format)
1088        compact_formatter.format_trivia_compact(leading);
1089
1090        for (i, item) in items.iter().enumerate() {
1091            if i > 0 {
1092                compact_formatter.write_separator();
1093            }
1094            format_item(&mut compact_formatter, item);
1095        }
1096
1097        // Trailing trivia (converted to compact format)
1098        compact_formatter.format_trivia_compact(trailing);
1099
1100        compact_formatter.output.push(close);
1101        compact_formatter.output
1102    }
1103
1104    fn format_multiline_generic<T, F>(
1105        &mut self,
1106        open: char,
1107        close: char,
1108        leading: &Trivia<'_>,
1109        trailing: &Trivia<'_>,
1110        items: &[T],
1111        format_item: F,
1112    ) where
1113        F: Fn(&mut Self, &T),
1114    {
1115        self.output.push(open);
1116
1117        if items.is_empty() {
1118            // Handle leading/trailing comments for empty collections
1119            self.format_leading_comments(leading);
1120            self.format_trailing_inline_comment(trailing);
1121            self.output.push(close);
1122            return;
1123        }
1124
1125        self.output.push('\n');
1126        self.indent_level += 1;
1127
1128        // Leading trivia inside the collection
1129        self.format_leading_comments(leading);
1130
1131        for (i, item) in items.iter().enumerate() {
1132            format_item(self, item);
1133            self.output.push(',');
1134            if i < items.len() - 1 {
1135                self.output.push('\n');
1136            }
1137        }
1138
1139        // Trailing trivia before closing delimiter
1140        if has_line_comment(trailing) {
1141            self.output.push('\n');
1142            self.format_leading_comments(trailing);
1143        }
1144
1145        self.output.push('\n');
1146        self.indent_level -= 1;
1147        self.write_indent();
1148        self.output.push(close);
1149    }
1150
1151    // =========================================================================
1152    // Optional trivia helpers
1153    // =========================================================================
1154
1155    fn format_leading_comments_opt(&mut self, trivia: Option<&Trivia<'_>>) {
1156        if let Some(t) = trivia {
1157            self.format_leading_comments(t);
1158        }
1159    }
1160
1161    fn format_trailing_inline_comment_opt(&mut self, trivia: Option<&Trivia<'_>>) {
1162        if let Some(t) = trivia {
1163            self.format_trailing_inline_comment(t);
1164        }
1165    }
1166
1167    fn format_leading_comments_inline_opt(&mut self, trivia: Option<&Trivia<'_>>) {
1168        if let Some(t) = trivia {
1169            self.format_leading_comments_inline(t);
1170        }
1171    }
1172
1173    // =========================================================================
1174    // Low-level output helpers
1175    // =========================================================================
1176
1177    /// Write a string to the output.
1178    #[inline]
1179    pub fn write_str(&mut self, s: &str) {
1180        self.output.push_str(s);
1181    }
1182
1183    /// Write a character to the output.
1184    #[inline]
1185    pub fn write_char(&mut self, c: char) {
1186        self.output.push(c);
1187    }
1188
1189    /// Write formatted data to the output (implements `core::fmt::Write`).
1190    #[inline]
1191    pub fn write_fmt(&mut self, args: core::fmt::Arguments<'_>) {
1192        use core::fmt::Write;
1193        let _ = self.output.write_fmt(args);
1194    }
1195
1196    // =========================================================================
1197    // Primitive value formatting (for Value serialization)
1198    // =========================================================================
1199
1200    /// Format a char value with proper escaping.
1201    pub fn format_char_value(&mut self, c: char) {
1202        match c {
1203            '\'' => self.output.push_str("'\\''"),
1204            '\\' => self.output.push_str("'\\\\'"),
1205            '\n' => self.output.push_str("'\\n'"),
1206            '\r' => self.output.push_str("'\\r'"),
1207            '\t' => self.output.push_str("'\\t'"),
1208            '\0' => self.output.push_str("'\\0'"),
1209            c if c.is_ascii_control() => {
1210                use core::fmt::Write;
1211                let _ = write!(self.output, "'\\x{:02x}'", c as u8);
1212            }
1213            c => {
1214                self.output.push('\'');
1215                self.output.push(c);
1216                self.output.push('\'');
1217            }
1218        }
1219    }
1220
1221    /// Format a string value with proper escaping.
1222    pub fn format_string_value(&mut self, s: &str) {
1223        self.output.push('"');
1224        for c in s.chars() {
1225            match c {
1226                '"' => self.output.push_str("\\\""),
1227                '\\' => self.output.push_str("\\\\"),
1228                '\n' => self.output.push_str("\\n"),
1229                '\r' => self.output.push_str("\\r"),
1230                '\t' => self.output.push_str("\\t"),
1231                '\0' => self.output.push_str("\\0"),
1232                c if c.is_ascii_control() => {
1233                    use core::fmt::Write;
1234                    let _ = write!(self.output, "\\x{:02x}", c as u8);
1235                }
1236                c => self.output.push(c),
1237            }
1238        }
1239        self.output.push('"');
1240    }
1241
1242    /// Format a byte string value with proper escaping.
1243    pub fn format_bytes_value(&mut self, bytes: &[u8]) {
1244        self.output.push_str("b\"");
1245        for &b in bytes {
1246            match b {
1247                b'"' => self.output.push_str("\\\""),
1248                b'\\' => self.output.push_str("\\\\"),
1249                b'\n' => self.output.push_str("\\n"),
1250                b'\r' => self.output.push_str("\\r"),
1251                b'\t' => self.output.push_str("\\t"),
1252                0 => self.output.push_str("\\0"),
1253                b if b.is_ascii_graphic() || b == b' ' => self.output.push(b as char),
1254                b => {
1255                    use core::fmt::Write;
1256                    let _ = write!(self.output, "\\x{b:02x}");
1257                }
1258            }
1259        }
1260        self.output.push('"');
1261    }
1262
1263    fn write_indent(&mut self) {
1264        // No indentation in compact mode or if indent is empty
1265        if self.is_compact || self.config.indent.is_empty() {
1266            return;
1267        }
1268        for _ in 0..self.indent_level {
1269            self.output.push_str(&self.config.indent);
1270        }
1271    }
1272
1273    fn write_colon(&mut self) {
1274        match self.config.spacing {
1275            Spacing::None => self.output.push(':'),
1276            Spacing::Normal => self.output.push_str(": "),
1277        }
1278    }
1279
1280    fn write_separator(&mut self) {
1281        match self.config.spacing {
1282            Spacing::None => self.output.push(','),
1283            Spacing::Normal => self.output.push_str(", "),
1284        }
1285    }
1286
1287    /// Format leading comments (comments that appear before an item on their own lines).
1288    fn format_leading_comments(&mut self, trivia: &Trivia<'_>) {
1289        // Delete mode strips all comments
1290        if self.config.comments == CommentMode::Delete {
1291            return;
1292        }
1293        if trivia.comments.is_empty() {
1294            return;
1295        }
1296        // In compact mode, format comments inline with conversion
1297        if self.is_compact {
1298            self.format_trivia_compact(trivia);
1299            return;
1300        }
1301        for comment in &trivia.comments {
1302            self.format_comment(comment);
1303        }
1304    }
1305
1306    /// Format leading comments inline (for comments after colons, don't add newlines).
1307    fn format_leading_comments_inline(&mut self, trivia: &Trivia<'_>) {
1308        // Delete mode strips all comments
1309        if self.config.comments == CommentMode::Delete {
1310            return;
1311        }
1312        if trivia.comments.is_empty() {
1313            return;
1314        }
1315        // In compact mode, use compact comment formatting
1316        if self.is_compact {
1317            self.format_trivia_compact(trivia);
1318            return;
1319        }
1320        for comment in &trivia.comments {
1321            match comment.kind {
1322                CommentKind::Block => {
1323                    self.output.push(' ');
1324                    self.output.push_str(&comment.text);
1325                    self.output.push(' ');
1326                }
1327                CommentKind::Line => {
1328                    // Line comments can't really be inline, but if they're here,
1329                    // they'll force a newline (only if we're not in minimal-like mode)
1330                    if self.is_pretty() {
1331                        self.output.push_str("  ");
1332                        self.output.push_str(&comment.text);
1333                    }
1334                }
1335            }
1336        }
1337    }
1338
1339    /// Format trailing inline comments (comments on the same line after a value).
1340    fn format_trailing_inline_comment(&mut self, trivia: &Trivia<'_>) {
1341        // Delete mode strips all comments
1342        if self.config.comments == CommentMode::Delete {
1343            return;
1344        }
1345        if trivia.comments.is_empty() {
1346            return;
1347        }
1348        // In compact mode, use compact comment formatting
1349        if self.is_compact {
1350            self.format_trivia_compact(trivia);
1351            return;
1352        }
1353        for comment in &trivia.comments {
1354            match comment.kind {
1355                CommentKind::Line => {
1356                    // Line comments only in pretty mode (they need newlines)
1357                    if self.is_pretty() {
1358                        self.output.push_str("  ");
1359                        self.output.push_str(&comment.text);
1360                    }
1361                }
1362                CommentKind::Block => {
1363                    self.output.push(' ');
1364                    self.output.push_str(&comment.text);
1365                }
1366            }
1367        }
1368    }
1369
1370    fn format_comment(&mut self, comment: &Comment<'_>) {
1371        // Only called from format_leading_comments which already checks for Pretty mode
1372        match comment.kind {
1373            CommentKind::Line => {
1374                self.write_indent();
1375                self.output.push_str(&comment.text);
1376                if !comment.text.ends_with('\n') {
1377                    self.output.push('\n');
1378                }
1379            }
1380            CommentKind::Block => {
1381                self.write_indent();
1382                self.output.push_str(&comment.text);
1383                self.output.push('\n');
1384            }
1385        }
1386    }
1387
1388    /// Format a comment in compact mode, converting line comments to block comments.
1389    ///
1390    /// Line comments `// foo\n` become `/* foo */` (no newline).
1391    fn format_comment_compact(&mut self, comment: &Comment<'_>) {
1392        match comment.kind {
1393            CommentKind::Line => {
1394                // Convert: "// foo\n" → "/* foo */"
1395                let text = comment
1396                    .text
1397                    .trim_start_matches("//")
1398                    .trim_end_matches('\n')
1399                    .trim();
1400                self.output.push_str("/* ");
1401                self.output.push_str(text);
1402                self.output.push_str(" */");
1403            }
1404            CommentKind::Block => {
1405                self.output.push_str(&comment.text);
1406            }
1407        }
1408    }
1409
1410    /// Format trivia (comments) in compact mode.
1411    ///
1412    /// All comments are formatted inline with space separators,
1413    /// and line comments are converted to block comments.
1414    fn format_trivia_compact(&mut self, trivia: &Trivia<'_>) {
1415        for comment in &trivia.comments {
1416            self.output.push(' ');
1417            self.format_comment_compact(comment);
1418        }
1419    }
1420}
1421
1422/// Check if trivia contains any line comments.
1423fn has_line_comment(trivia: &Trivia<'_>) -> bool {
1424    trivia.comments.iter().any(|c| c.kind == CommentKind::Line)
1425}
1426
1427#[cfg(test)]
1428mod tests {
1429    use super::*;
1430    use crate::ast::parse_document;
1431
1432    fn format(source: &str) -> String {
1433        let doc = parse_document(source).unwrap();
1434        format_document(&doc, &FormatConfig::default())
1435    }
1436
1437    fn format_with(source: &str, config: &FormatConfig) -> String {
1438        let doc = parse_document(source).unwrap();
1439        format_document(&doc, config)
1440    }
1441
1442    // =========================================================================
1443    // Basic values
1444    // =========================================================================
1445
1446    #[test]
1447    fn test_simple_values() {
1448        assert_eq!(format("42"), "42\n");
1449        assert_eq!(format("true"), "true\n");
1450        assert_eq!(format("false"), "false\n");
1451        assert_eq!(format("\"hello\""), "\"hello\"\n");
1452        assert_eq!(format("'c'"), "'c'\n");
1453        assert_eq!(format("()"), "()\n");
1454        assert_eq!(format("None"), "None\n");
1455        assert_eq!(format("Some(42)"), "Some(42)\n");
1456    }
1457
1458    #[test]
1459    fn test_preserves_number_format() {
1460        // Hex, binary, octal are preserved
1461        assert_eq!(format("0xFF"), "0xFF\n");
1462        assert_eq!(format("0b1010"), "0b1010\n");
1463        assert_eq!(format("0o777"), "0o777\n");
1464        assert_eq!(format("1_000_000"), "1_000_000\n");
1465    }
1466
1467    #[test]
1468    fn test_preserves_string_format() {
1469        // Raw strings are preserved
1470        assert_eq!(format(r#"r"raw string""#), "r\"raw string\"\n");
1471        assert_eq!(format(r##"r#"hash raw"#"##), "r#\"hash raw\"#\n");
1472    }
1473
1474    // =========================================================================
1475    // Sequences
1476    // =========================================================================
1477
1478    #[test]
1479    fn test_root_seq_multiline() {
1480        // Root collections are always multiline
1481        assert_eq!(format("[1, 2, 3]"), "[\n    1,\n    2,\n    3,\n]\n");
1482        assert_eq!(format("[1,2,3]"), "[\n    1,\n    2,\n    3,\n]\n");
1483        assert_eq!(format("[ 1 , 2 , 3 ]"), "[\n    1,\n    2,\n    3,\n]\n");
1484    }
1485
1486    #[test]
1487    fn test_empty_seq() {
1488        assert_eq!(format("[]"), "[]\n");
1489        assert_eq!(format("[  ]"), "[]\n");
1490    }
1491
1492    #[test]
1493    fn test_seq_exceeds_limit() {
1494        let config = FormatConfig::default().char_limit(10);
1495        let formatted = format_with("[1, 2, 3, 4, 5]", &config);
1496        assert_eq!(formatted, "[\n    1,\n    2,\n    3,\n    4,\n    5,\n]\n");
1497    }
1498
1499    #[test]
1500    fn test_seq_with_line_comment() {
1501        let source = "[\n    // first item\n    1,\n    2,\n]";
1502        let formatted = format(source);
1503        assert!(formatted.contains("// first item"));
1504        assert!(formatted.contains("    1,"));
1505    }
1506
1507    // =========================================================================
1508    // Structs
1509    // =========================================================================
1510
1511    #[test]
1512    fn test_root_struct_multiline() {
1513        // Root collections are always multiline
1514        assert_eq!(
1515            format("Point(x:1,y:2)"),
1516            "Point(\n    x: 1,\n    y: 2,\n)\n"
1517        );
1518        assert_eq!(
1519            format("Point(x: 1, y: 2)"),
1520            "Point(\n    x: 1,\n    y: 2,\n)\n"
1521        );
1522        assert_eq!(
1523            format("Point( x : 1 , y : 2 )"),
1524            "Point(\n    x: 1,\n    y: 2,\n)\n"
1525        );
1526    }
1527
1528    #[test]
1529    fn test_unit_struct() {
1530        assert_eq!(format("Empty"), "Empty\n");
1531    }
1532
1533    #[test]
1534    fn test_tuple_struct() {
1535        // Root tuple structs are multiline
1536        assert_eq!(
1537            format("Point(1, 2, 3)"),
1538            "Point(\n    1,\n    2,\n    3,\n)\n"
1539        );
1540    }
1541
1542    #[test]
1543    fn test_struct_exceeds_limit() {
1544        let config = FormatConfig::default().char_limit(20);
1545        let formatted = format_with("Config(name: \"test\", value: 42)", &config);
1546        assert!(
1547            formatted.contains('\n'),
1548            "Expected multiline, got: {formatted:?}"
1549        );
1550        assert!(formatted.contains("name: \"test\","));
1551        assert!(formatted.contains("value: 42,"));
1552    }
1553
1554    #[test]
1555    fn test_struct_with_comments() {
1556        let source = r#"Config(
1557    // Server port
1558    port: 8080,
1559    host: "localhost",
1560)"#;
1561        let formatted = format(source);
1562        assert!(formatted.contains("// Server port"));
1563        assert!(formatted.contains("port: 8080,"));
1564        assert!(formatted.contains("host: \"localhost\","));
1565    }
1566
1567    #[test]
1568    fn test_nested_struct() {
1569        // Root is multiline, nested stays compact
1570        let source = "Config(inner: Point(x: 1, y: 2))";
1571        let formatted = format(source);
1572        assert_eq!(formatted, "Config(\n    inner: Point(x: 1, y: 2),\n)\n");
1573    }
1574
1575    #[test]
1576    fn test_deeply_nested() {
1577        let config = FormatConfig::default().char_limit(30);
1578        let source = "A(b: B(c: C(d: D(e: 1))))";
1579        let formatted = format_with(source, &config);
1580        // Should expand due to length
1581        assert!(formatted.contains('\n'));
1582    }
1583
1584    // =========================================================================
1585    // Maps
1586    // =========================================================================
1587
1588    #[test]
1589    fn test_root_map_multiline() {
1590        // Root collections are always multiline
1591        let source = "{\"a\": 1, \"b\": 2}";
1592        let formatted = format(source);
1593        assert_eq!(formatted, "{\n    \"a\": 1,\n    \"b\": 2,\n}\n");
1594    }
1595
1596    #[test]
1597    fn test_empty_map() {
1598        assert_eq!(format("{}"), "{}\n");
1599    }
1600
1601    #[test]
1602    fn test_map_exceeds_limit() {
1603        let config = FormatConfig::default().char_limit(15);
1604        let formatted = format_with("{\"key\": \"value\"}", &config);
1605        assert!(formatted.contains('\n'));
1606    }
1607
1608    // =========================================================================
1609    // Tuples
1610    // =========================================================================
1611
1612    #[test]
1613    fn test_root_tuple_multiline() {
1614        // Root collections are always multiline
1615        assert_eq!(format("(1, 2, 3)"), "(\n    1,\n    2,\n    3,\n)\n");
1616        assert_eq!(format("(1,2,3)"), "(\n    1,\n    2,\n    3,\n)\n");
1617    }
1618
1619    #[test]
1620    fn test_single_element_tuple() {
1621        // Root collections are always multiline
1622        assert_eq!(format("(42,)"), "(\n    42,\n)\n");
1623    }
1624
1625    // =========================================================================
1626    // Comments
1627    // =========================================================================
1628
1629    #[test]
1630    fn test_leading_comment() {
1631        let source = "// header comment\n42";
1632        let formatted = format(source);
1633        assert!(formatted.starts_with("// header comment\n"));
1634        assert!(formatted.contains("42"));
1635    }
1636
1637    #[test]
1638    fn test_block_comment() {
1639        let source = "/* block */ 42";
1640        let formatted = format(source);
1641        assert!(formatted.contains("/* block */"));
1642    }
1643
1644    #[test]
1645    fn test_comment_between_fields() {
1646        let source = "(
1647    x: 1,
1648    // separator
1649    y: 2,
1650)";
1651        let formatted = format(source);
1652        assert!(formatted.contains("// separator"));
1653        assert!(formatted.contains("x: 1,"));
1654        assert!(formatted.contains("y: 2,"));
1655    }
1656
1657    // =========================================================================
1658    // Attributes
1659    // =========================================================================
1660
1661    #[test]
1662    fn test_single_attribute() {
1663        let source = "#![type = \"Foo\"]\n42";
1664        let formatted = format(source);
1665        assert!(formatted.starts_with("#![type = \"Foo\"]"));
1666        assert!(formatted.contains("\n\n42"));
1667    }
1668
1669    #[test]
1670    fn test_multiple_attributes() {
1671        let source = "#![type = \"Foo\"]\n#![enable(unwrap_newtypes)]\n42";
1672        let formatted = format(source);
1673        assert!(formatted.contains("#![type = \"Foo\"]"));
1674        assert!(formatted.contains("#![enable(unwrap_newtypes)]"));
1675    }
1676
1677    #[test]
1678    fn test_attribute_with_args() {
1679        let source = "#![enable(implicit_some, unwrap_newtypes)]\n42";
1680        let formatted = format(source);
1681        assert!(formatted.contains("#![enable(implicit_some, unwrap_newtypes)]"));
1682    }
1683
1684    // =========================================================================
1685    // Configuration
1686    // =========================================================================
1687
1688    #[test]
1689    fn test_custom_indent() {
1690        let config = FormatConfig::new().indent("  ").char_limit(5);
1691        let formatted = format_with("[1, 2, 3]", &config);
1692        assert!(
1693            formatted.contains("  1,"),
1694            "Expected 2-space indent in: {formatted:?}"
1695        );
1696    }
1697
1698    #[test]
1699    fn test_tab_indent() {
1700        let config = FormatConfig::new().indent("\t").char_limit(5);
1701        let formatted = format_with("[1, 2, 3]", &config);
1702        assert!(
1703            formatted.contains("\t1,"),
1704            "Expected tab indent in: {formatted:?}"
1705        );
1706    }
1707
1708    #[test]
1709    fn test_large_char_limit_nested() {
1710        // Char limit only applies to nested collections, not root
1711        let config = FormatConfig::new().char_limit(1000);
1712        // Test with nested array inside a struct
1713        let long_array = (1..50)
1714            .map(|n| n.to_string())
1715            .collect::<Vec<_>>()
1716            .join(", ");
1717        let source = format!("Config(items: [{long_array}])");
1718        let formatted = format_with(&source, &config);
1719        // Root should be multiline, but nested array stays compact
1720        assert!(formatted.contains(&format!("[{long_array}]")));
1721    }
1722
1723    // =========================================================================
1724    // Edge cases
1725    // =========================================================================
1726
1727    #[test]
1728    fn test_empty_document() {
1729        // Empty document produces empty output
1730        assert_eq!(format(""), "");
1731    }
1732
1733    #[test]
1734    fn test_comment_only_document() {
1735        let source = "// just a comment\n";
1736        let formatted = format(source);
1737        assert!(formatted.contains("// just a comment"));
1738    }
1739
1740    #[test]
1741    fn test_trailing_comma_preserved_multiline() {
1742        let config = FormatConfig::default().char_limit(5);
1743        let formatted = format_with("[1, 2]", &config);
1744        // All items should have trailing commas in multiline mode
1745        assert!(formatted.contains("1,"), "Expected '1,' in: {formatted:?}");
1746        assert!(formatted.contains("2,"), "Expected '2,' in: {formatted:?}");
1747    }
1748
1749    #[test]
1750    fn test_no_trailing_comma_compact_nested() {
1751        // Test compact nested collection has no trailing comma
1752        let formatted = format("Config(items: [1, 2, 3])");
1753        // Root is multiline, nested is compact without trailing comma
1754        assert!(formatted.contains("[1, 2, 3]"));
1755    }
1756
1757    #[test]
1758    fn test_anonymous_struct() {
1759        // Root collections are always multiline
1760        let source = "(x: 1, y: 2)";
1761        let formatted = format(source);
1762        assert_eq!(formatted, "(\n    x: 1,\n    y: 2,\n)\n");
1763    }
1764
1765    #[test]
1766    fn test_option_some() {
1767        assert_eq!(format("Some(42)"), "Some(42)\n");
1768        assert_eq!(format("Some( 42 )"), "Some(42)\n");
1769    }
1770
1771    #[test]
1772    fn test_option_none() {
1773        assert_eq!(format("None"), "None\n");
1774    }
1775
1776    #[test]
1777    fn test_bytes() {
1778        assert_eq!(format("b\"hello\""), "b\"hello\"\n");
1779    }
1780
1781    #[test]
1782    fn test_empty_collections() {
1783        assert_eq!(format("[]"), "[]\n");
1784        assert_eq!(format("{}"), "{}\n");
1785        assert_eq!(format("()"), "()\n");
1786    }
1787
1788    // =========================================================================
1789    // Compaction - Depth-based
1790    // =========================================================================
1791
1792    #[test]
1793    fn test_compact_from_depth_0() {
1794        // Compact from depth 0 means compact even the root (unless root which is always multiline)
1795        let config = FormatConfig::new().compact_from_depth(0);
1796        // Nested collections should be compacted
1797        let source = "Config(items: [1, 2, 3], point: (1, 2))";
1798        let formatted = format_with(source, &config);
1799        // Root is still multiline, but nested should be compact
1800        assert!(formatted.contains("[1, 2, 3]"));
1801        assert!(formatted.contains("(1, 2)"));
1802    }
1803
1804    #[test]
1805    fn test_compact_from_depth_1() {
1806        // Compact from depth 1 means first level nested compacts
1807        let config = FormatConfig::new().compact_from_depth(1);
1808        let source = "Config(items: [1, 2, 3])";
1809        let formatted = format_with(source, &config);
1810        // Root multiline, nested compact
1811        assert!(
1812            formatted.contains("items: [1, 2, 3]"),
1813            "Expected compact array: {formatted:?}"
1814        );
1815    }
1816
1817    // =========================================================================
1818    // Compaction - Type-based
1819    // =========================================================================
1820
1821    #[test]
1822    fn test_compact_types_arrays() {
1823        let config = FormatConfig::new()
1824            .char_limit(5) // Would normally expand due to length
1825            .compact_types(CompactTypes { arrays: true, ..Default::default() });
1826        let source = "Config(items: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10])";
1827        let formatted = format_with(source, &config);
1828        // Arrays should be compact despite exceeding char limit
1829        assert!(
1830            formatted.contains("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]"),
1831            "Expected compact array: {formatted:?}"
1832        );
1833    }
1834
1835    #[test]
1836    fn test_compact_types_tuples() {
1837        let config = FormatConfig::new()
1838            .char_limit(5)
1839            .compact_types(CompactTypes {
1840                tuples: true,
1841                ..Default::default()
1842            });
1843        let source = "Config(point: (1, 2, 3, 4, 5, 6, 7, 8, 9, 10))";
1844        let formatted = format_with(source, &config);
1845        // Tuples should be compact
1846        assert!(
1847            formatted.contains("(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)"),
1848            "Expected compact tuple: {formatted:?}"
1849        );
1850    }
1851
1852    #[test]
1853    fn test_compact_types_structs() {
1854        let config = FormatConfig::new()
1855            .char_limit(5)
1856            .compact_types(CompactTypes {
1857                structs: true,
1858                ..Default::default()
1859            });
1860        let source = "Outer(inner: Inner(x: 1, y: 2, z: 3))";
1861        let formatted = format_with(source, &config);
1862        // Nested struct should be compact
1863        assert!(
1864            formatted.contains("Inner(x: 1, y: 2, z: 3)"),
1865            "Expected compact struct: {formatted:?}"
1866        );
1867    }
1868
1869    #[test]
1870    fn test_compact_types_all() {
1871        let config = FormatConfig::new()
1872            .char_limit(5)
1873            .compact_types(CompactTypes::all());
1874        let source = "Config(a: [1, 2], b: (3, 4), c: Point(x: 5))";
1875        let formatted = format_with(source, &config);
1876        assert!(formatted.contains("[1, 2]"));
1877        assert!(formatted.contains("(3, 4)"));
1878        assert!(formatted.contains("Point(x: 5)"));
1879    }
1880
1881    // =========================================================================
1882    // Compaction - Comment conversion
1883    // =========================================================================
1884
1885    #[test]
1886    fn test_compact_converts_line_comments_to_block() {
1887        // When compacting with a line comment, it should convert to block comment
1888        let config = FormatConfig::new().compact_from_depth(1);
1889        let source = "Config(
1890    items: [
1891        // comment
1892        1,
1893        2,
1894    ],
1895)";
1896        let formatted = format_with(source, &config);
1897        // In compact mode, line comment should become block comment
1898        assert!(
1899            formatted.contains("/* comment */"),
1900            "Expected block comment: {formatted:?}"
1901        );
1902        // Should not have newlines in the compact section
1903        assert!(formatted.contains("items: ["));
1904    }
1905
1906    #[test]
1907    fn test_compact_preserves_block_comments() {
1908        let config = FormatConfig::new().compact_from_depth(1);
1909        let source = "Config(
1910    items: [
1911        /* existing block */
1912        1,
1913    ],
1914)";
1915        let formatted = format_with(source, &config);
1916        assert!(
1917            formatted.contains("/* existing block */"),
1918            "Expected block comment preserved: {formatted:?}"
1919        );
1920    }
1921
1922    // =========================================================================
1923    // AR-1: Minimal Mode
1924    // =========================================================================
1925
1926    #[test]
1927    fn test_minimal_no_whitespace() {
1928        // AR-1.1, AR-1.3, AR-1.4: No whitespace between tokens
1929        let source = "Point(x: 1, y: 2)";
1930        let formatted = format_with(source, &FormatConfig::minimal());
1931        assert_eq!(formatted, "Point(x:1,y:2)");
1932    }
1933
1934    #[test]
1935    fn test_minimal_strips_comments() {
1936        // AR-1.2: Comments are stripped
1937        let source = "// header\nPoint(x: 1, /* inline */ y: 2)";
1938        let formatted = format_with(source, &FormatConfig::minimal());
1939        assert_eq!(formatted, "Point(x:1,y:2)");
1940    }
1941
1942    #[test]
1943    fn test_minimal_nested_collections() {
1944        // AR-1.1: Nested collections also have no whitespace
1945        let source = "Config(items: [1, 2], point: (3, 4), map: {\"a\": 1})";
1946        let formatted = format_with(source, &FormatConfig::minimal());
1947        assert_eq!(formatted, "Config(items:[1,2],point:(3,4),map:{\"a\":1})");
1948    }
1949
1950    // =========================================================================
1951    // AR-2: Root Collection Behavior
1952    // =========================================================================
1953
1954    #[test]
1955    fn test_root_multiline_by_default() {
1956        // AR-2.1: Root collections are multiline by default
1957        let config = FormatConfig::new(); // No compaction rules
1958        let formatted = format_with("[1, 2, 3]", &config);
1959        assert!(
1960            formatted.contains('\n'),
1961            "Root should be multiline: {formatted:?}"
1962        );
1963    }
1964
1965    #[test]
1966    fn test_root_compacts_with_depth_0() {
1967        // AR-2.2: compact_from_depth(0) forces root to be compact
1968        let config = FormatConfig::new().compact_from_depth(0);
1969        let formatted = format_with("[1, 2, 3]", &config);
1970        assert_eq!(formatted, "[1, 2, 3]\n");
1971    }
1972
1973    #[test]
1974    fn test_root_compacts_with_matching_type() {
1975        // AR-2.3: compact_types matching root forces root to be compact
1976        let config = FormatConfig::new().compact_types(CompactTypes {
1977            arrays: true,
1978            ..Default::default()
1979        });
1980        let formatted = format_with("[1, 2, 3]", &config);
1981        assert_eq!(formatted, "[1, 2, 3]\n");
1982    }
1983
1984    // =========================================================================
1985    // AR-3: Depth Counting
1986    // =========================================================================
1987
1988    #[test]
1989    fn test_depth_counting_nested() {
1990        // AR-3.3, AR-3.4: Depth increments for each nested collection
1991        // depth 0 = root, depth 1 = first nested, depth 2 = second nested
1992        let config = FormatConfig::new().compact_from_depth(2).char_limit(0);
1993        let source = "Outer(a: Inner(b: [1, 2, 3]))";
1994        let formatted = format_with(source, &config);
1995        // Root (depth 0): multiline
1996        // Inner (depth 1): multiline (below threshold)
1997        // Array (depth 2): compact (at threshold)
1998        assert!(
1999            formatted.contains("[1, 2, 3]"),
2000            "Array should be compact: {formatted:?}"
2001        );
2002        assert!(
2003            formatted.contains("a: Inner("),
2004            "Should have newlines: {formatted:?}"
2005        );
2006    }
2007
2008    #[test]
2009    fn test_compact_from_depth_2() {
2010        // AR-3.1: compact_from_depth(N) compacts at depth >= N
2011        let config = FormatConfig::new().compact_from_depth(2).char_limit(0);
2012        let source = "Config(items: [1, 2, 3])";
2013        let formatted = format_with(source, &config);
2014        // Root (depth 0): multiline
2015        // Array (depth 1): still multiline (1 < 2)
2016        assert!(
2017            formatted.contains("items: [\n"),
2018            "Depth 1 should be multiline: {formatted:?}"
2019        );
2020    }
2021
2022    #[test]
2023    fn test_compact_from_depth_3_deep_nesting() {
2024        // AR-3.4: Deep nesting respects depth threshold
2025        let config = FormatConfig::new().compact_from_depth(3).char_limit(0);
2026        let source = "A(b: B(c: C(d: [1, 2])))";
2027        let formatted = format_with(source, &config);
2028        // Root A (depth 0): multiline
2029        // B (depth 1): multiline
2030        // C (depth 2): multiline
2031        // Array (depth 3): compact
2032        assert!(
2033            formatted.contains("[1, 2]"),
2034            "Depth 3 array should be compact: {formatted:?}"
2035        );
2036    }
2037
2038    // =========================================================================
2039    // AR-4: Type-Based Compaction
2040    // =========================================================================
2041
2042    #[test]
2043    fn test_compact_types_maps() {
2044        // AR-4.3: compact_types.maps compacts all {...} expressions
2045        let config = FormatConfig::new()
2046            .char_limit(0) // Disable length-based
2047            .compact_types(CompactTypes {
2048                maps: true,
2049                ..Default::default()
2050            });
2051        let source = "Config(data: {\"a\": 1, \"b\": 2})";
2052        let formatted = format_with(source, &config);
2053        assert!(
2054            formatted.contains("{\"a\": 1, \"b\": 2}"),
2055            "Map should be compact: {formatted:?}"
2056        );
2057    }
2058
2059    #[test]
2060    fn test_compact_types_anonymous_struct() {
2061        // AR-4.4: compact_types.structs compacts (x: 1) anonymous structs
2062        let config = FormatConfig::new()
2063            .char_limit(0)
2064            .compact_types(CompactTypes {
2065                structs: true,
2066                ..Default::default()
2067            });
2068        let source = "Config(point: (x: 1, y: 2))";
2069        let formatted = format_with(source, &config);
2070        assert!(
2071            formatted.contains("(x: 1, y: 2)"),
2072            "Anonymous struct should be compact: {formatted:?}"
2073        );
2074    }
2075
2076    #[test]
2077    fn test_compact_types_ignores_char_limit() {
2078        // AR-4.5: Type-based compaction ignores char_limit
2079        let config = FormatConfig::new()
2080            .char_limit(5) // Very small limit
2081            .compact_types(CompactTypes {
2082                arrays: true,
2083                ..Default::default()
2084            });
2085        let source = "Config(items: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10])";
2086        let formatted = format_with(source, &config);
2087        // Array should be compact despite exceeding char_limit
2088        assert!(
2089            formatted.contains("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]"),
2090            "Type rule should override char_limit: {formatted:?}"
2091        );
2092    }
2093
2094    // =========================================================================
2095    // AR-5: Length-Based Compaction
2096    // =========================================================================
2097
2098    #[test]
2099    fn test_char_limit_zero_disables() {
2100        // AR-5.3: char_limit = 0 disables length-based compaction
2101        let config = FormatConfig::new().char_limit(0);
2102        let source = "Config(items: [1])"; // Very short
2103        let formatted = format_with(source, &config);
2104        // Nested should be multiline since no length-based compaction
2105        assert!(
2106            formatted.contains("items: [\n"),
2107            "char_limit=0 should disable length-based: {formatted:?}"
2108        );
2109    }
2110
2111    #[test]
2112    fn test_default_char_limit_80() {
2113        // AR-5.4: Default char_limit is 80
2114        let config = FormatConfig::default();
2115        // 75 chars fits in 80
2116        let short = "Config(items: [1, 2, 3, 4, 5])";
2117        let formatted_short = format_with(short, &config);
2118        assert!(
2119            formatted_short.contains("[1, 2, 3, 4, 5]"),
2120            "Should fit: {formatted_short:?}"
2121        );
2122    }
2123
2124    // =========================================================================
2125    // AR-6: OR Logic (Rule Combination)
2126    // =========================================================================
2127
2128    #[test]
2129    fn test_or_logic_depth_triggers() {
2130        // AR-6.1: Compaction triggers if depth rule matches
2131        let config = FormatConfig::new().compact_from_depth(1).char_limit(0); // Disable length
2132        let source = "Config(items: [1, 2, 3])";
2133        let formatted = format_with(source, &config);
2134        assert!(
2135            formatted.contains("[1, 2, 3]"),
2136            "Depth rule should trigger: {formatted:?}"
2137        );
2138    }
2139
2140    #[test]
2141    fn test_or_logic_type_triggers() {
2142        // AR-6.2: Compaction triggers if type rule matches
2143        let config = FormatConfig::new()
2144            .compact_types(CompactTypes {
2145                arrays: true,
2146                ..Default::default()
2147            })
2148            .char_limit(0); // Disable length
2149        let source = "Config(items: [1, 2, 3])";
2150        let formatted = format_with(source, &config);
2151        assert!(
2152            formatted.contains("[1, 2, 3]"),
2153            "Type rule should trigger: {formatted:?}"
2154        );
2155    }
2156
2157    #[test]
2158    fn test_or_logic_length_triggers() {
2159        // AR-6.3: Compaction triggers if length rule matches
2160        let config = FormatConfig::new().char_limit(100); // No depth/type rules
2161        let source = "Config(items: [1, 2, 3])";
2162        let formatted = format_with(source, &config);
2163        assert!(
2164            formatted.contains("[1, 2, 3]"),
2165            "Length rule should trigger: {formatted:?}"
2166        );
2167    }
2168
2169    #[test]
2170    fn test_no_rules_match_multiline() {
2171        // AR-6.4: When NO rule matches, collection uses multiline
2172        let config = FormatConfig::new().char_limit(0); // Disable all
2173        let source = "Config(items: [1, 2, 3])";
2174        let formatted = format_with(source, &config);
2175        assert!(
2176            formatted.contains("items: [\n"),
2177            "No rules = multiline: {formatted:?}"
2178        );
2179    }
2180
2181    // =========================================================================
2182    // AR-7: Soft/Hard Compaction & Comments
2183    // =========================================================================
2184
2185    #[test]
2186    fn test_length_based_soft_line_comments_prevent() {
2187        // AR-7.1: Length-based compaction is "soft" - line comments prevent it
2188        let config = FormatConfig::new().char_limit(1000); // Large limit
2189        let source = "Config(
2190    items: [
2191        // comment
2192        1,
2193        2,
2194    ],
2195)";
2196        let formatted = format_with(source, &config);
2197        // Line comment should PREVENT compact mode (stay multiline)
2198        assert!(
2199            formatted.contains("// comment"),
2200            "Line comment preserved: {formatted:?}"
2201        );
2202        assert!(
2203            formatted.contains("items: [\n"),
2204            "Should stay multiline: {formatted:?}"
2205        );
2206    }
2207
2208    #[test]
2209    fn test_depth_based_hard_converts_comments() {
2210        // AR-7.2, AR-7.4: Depth-based is "hard" - converts line comments to block
2211        let config = FormatConfig::new().compact_from_depth(1);
2212        let source = "Config(
2213    items: [
2214        // comment
2215        1,
2216    ],
2217)";
2218        let formatted = format_with(source, &config);
2219        // Hard compaction converts line comment to block
2220        assert!(
2221            formatted.contains("/* comment */"),
2222            "Line → block: {formatted:?}"
2223        );
2224        assert!(
2225            !formatted.contains("// comment"),
2226            "No line comment: {formatted:?}"
2227        );
2228    }
2229
2230    #[test]
2231    fn test_type_based_hard_converts_comments() {
2232        // AR-7.3, AR-7.4: Type-based is "hard" - converts line comments to block
2233        let config = FormatConfig::new()
2234            .char_limit(0)
2235            .compact_types(CompactTypes {
2236                arrays: true,
2237                ..Default::default()
2238            });
2239        let source = "Config(
2240    items: [
2241        // comment
2242        1,
2243    ],
2244)";
2245        let formatted = format_with(source, &config);
2246        // Hard compaction converts line comment to block
2247        assert!(
2248            formatted.contains("/* comment */"),
2249            "Line → block: {formatted:?}"
2250        );
2251    }
2252
2253    #[test]
2254    fn test_compact_multiple_line_comments() {
2255        // AR-7.7: Multiple comments in compact mode are space-separated
2256        let config = FormatConfig::new().compact_from_depth(1);
2257        let source = "Config(
2258    items: [
2259        // first
2260        1,
2261        // second
2262        2,
2263    ],
2264)";
2265        let formatted = format_with(source, &config);
2266        assert!(
2267            formatted.contains("/* first */"),
2268            "First comment: {formatted:?}"
2269        );
2270        assert!(
2271            formatted.contains("/* second */"),
2272            "Second comment: {formatted:?}"
2273        );
2274    }
2275
2276    #[test]
2277    fn test_minimal_strips_all_comments() {
2278        // AR-7.8: Minimal mode strips ALL comments
2279        let source = "Config(
2280    // line comment
2281    items: [
2282        /* block comment */
2283        1,
2284    ],
2285)";
2286        let formatted = format_with(source, &FormatConfig::minimal());
2287        assert!(
2288            !formatted.contains("comment"),
2289            "No comments in minimal: {formatted:?}"
2290        );
2291    }
2292
2293    #[test]
2294    fn test_hard_compact_block_comments_preserved() {
2295        // AR-7.5: Block comments are preserved unchanged in hard compact
2296        let config = FormatConfig::new().compact_from_depth(1);
2297        let source = "Config(
2298    items: [
2299        /* existing block */
2300        1,
2301    ],
2302)";
2303        let formatted = format_with(source, &config);
2304        assert!(
2305            formatted.contains("/* existing block */"),
2306            "Block comment unchanged: {formatted:?}"
2307        );
2308    }
2309}