rfmt 1.5.3 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,684 @@
1
+ //! Printer converts Doc IR to a formatted string.
2
+ //!
3
+ //! The printer implements a Prettier-style algorithm that:
4
+ //! 1. Tries to fit groups on a single line
5
+ //! 2. Breaks groups into multiple lines when they don't fit
6
+ //! 3. Manages indentation automatically
7
+ //!
8
+ //! # Algorithm
9
+ //!
10
+ //! The printer uses a stack-based approach to process Doc nodes.
11
+ //! For each Group, it measures whether the contents fit in the remaining
12
+ //! line width. If they fit, Line docs become spaces (flat mode).
13
+ //! If they don't fit, Line docs become newlines (break mode).
14
+
15
+ use super::Doc;
16
+ use crate::config::{Config, IndentStyle};
17
+
18
+ /// Print mode for Line docs within a group.
19
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
20
+ pub enum Mode {
21
+ /// Group fits on one line. Line docs become spaces.
22
+ Flat,
23
+ /// Group doesn't fit. Line docs become newlines.
24
+ Break,
25
+ }
26
+
27
+ /// A command in the print stack.
28
+ #[derive(Debug)]
29
+ struct PrintCommand<'a> {
30
+ /// Current indentation level (in spaces)
31
+ indent: usize,
32
+ /// Print mode (Flat or Break)
33
+ mode: Mode,
34
+ /// The Doc to print
35
+ doc: &'a Doc,
36
+ }
37
+
38
+ /// Converts Doc IR to a formatted string.
39
+ pub struct Printer<'a> {
40
+ /// Configuration for formatting
41
+ config: &'a Config,
42
+ /// Output buffer
43
+ output: String,
44
+ /// Current column position (0-indexed)
45
+ pos: usize,
46
+ /// Pre-computed indent strings by width (avoids allocation during print)
47
+ indent_cache: Vec<String>,
48
+ }
49
+
50
+ const MAX_PRECACHED_INDENT: usize = 32;
51
+
52
+ impl<'a> Printer<'a> {
53
+ /// Creates a new printer with the given configuration.
54
+ pub fn new(config: &'a Config) -> Self {
55
+ let indent_width = config.formatting.indent_width;
56
+ let indent_style = &config.formatting.indent_style;
57
+
58
+ let indent_cache = (0..=MAX_PRECACHED_INDENT)
59
+ .map(|width| Self::build_indent_string(width, indent_width, indent_style))
60
+ .collect();
61
+
62
+ Self {
63
+ config,
64
+ output: String::new(),
65
+ pos: 0,
66
+ indent_cache,
67
+ }
68
+ }
69
+
70
+ #[inline]
71
+ fn build_indent_string(width: usize, indent_width: usize, style: &IndentStyle) -> String {
72
+ match style {
73
+ IndentStyle::Spaces => " ".repeat(width),
74
+ IndentStyle::Tabs if indent_width == 0 => String::new(),
75
+ IndentStyle::Tabs => {
76
+ let tabs = width / indent_width;
77
+ let spaces = width % indent_width;
78
+ if spaces == 0 {
79
+ "\t".repeat(tabs)
80
+ } else {
81
+ let mut result = String::with_capacity(tabs + spaces);
82
+ result.extend(std::iter::repeat_n('\t', tabs));
83
+ result.extend(std::iter::repeat_n(' ', spaces));
84
+ result
85
+ }
86
+ }
87
+ }
88
+ }
89
+
90
+ /// Prints a Doc to a string.
91
+ #[inline]
92
+ pub fn print(&mut self, doc: &Doc) -> String {
93
+ self.output.clear();
94
+ self.pos = 0;
95
+
96
+ let mut commands: Vec<PrintCommand> = vec![PrintCommand {
97
+ indent: 0,
98
+ mode: Mode::Break, // Start in break mode
99
+ doc,
100
+ }];
101
+
102
+ while let Some(cmd) = commands.pop() {
103
+ self.process_command(cmd, &mut commands);
104
+ }
105
+
106
+ // Ensure trailing newline
107
+ if !self.output.is_empty() && !self.output.ends_with('\n') {
108
+ self.output.push('\n');
109
+ }
110
+
111
+ std::mem::take(&mut self.output)
112
+ }
113
+
114
+ /// Processes a single print command.
115
+ #[inline]
116
+ fn process_command<'b>(&mut self, cmd: PrintCommand<'b>, commands: &mut Vec<PrintCommand<'b>>) {
117
+ match cmd.doc {
118
+ Doc::Text(s) => {
119
+ self.output.push_str(s);
120
+ self.pos += s.chars().count();
121
+ }
122
+
123
+ Doc::Concat(docs) => {
124
+ // Push in reverse order so first doc is processed first
125
+ for doc in docs.iter().rev() {
126
+ commands.push(PrintCommand {
127
+ indent: cmd.indent,
128
+ mode: cmd.mode,
129
+ doc,
130
+ });
131
+ }
132
+ }
133
+
134
+ Doc::Group {
135
+ contents,
136
+ break_parent,
137
+ ..
138
+ } => {
139
+ // Determine if the group fits on the remaining line
140
+ let remaining = self.config.formatting.line_length.saturating_sub(self.pos);
141
+ let fits = !*break_parent && self.fits(contents, cmd.indent, remaining, cmd.mode);
142
+
143
+ let mode = if fits { Mode::Flat } else { Mode::Break };
144
+
145
+ commands.push(PrintCommand {
146
+ indent: cmd.indent,
147
+ mode,
148
+ doc: contents,
149
+ });
150
+ }
151
+
152
+ Doc::Line {
153
+ soft,
154
+ hard,
155
+ literal,
156
+ } => match (cmd.mode, *hard) {
157
+ (_, true) => {
158
+ self.output.push('\n');
159
+ if !*literal {
160
+ let indent_str = self.get_indent(cmd.indent);
161
+ self.output.push_str(&indent_str);
162
+ self.pos = cmd.indent;
163
+ } else {
164
+ self.pos = 0;
165
+ }
166
+ }
167
+ (Mode::Flat, false) if *soft => {}
168
+ (Mode::Flat, false) => {
169
+ self.output.push(' ');
170
+ self.pos += 1;
171
+ }
172
+ (Mode::Break, false) => {
173
+ self.output.push('\n');
174
+ let indent_str = self.get_indent(cmd.indent);
175
+ self.output.push_str(&indent_str);
176
+ self.pos = cmd.indent;
177
+ }
178
+ },
179
+
180
+ Doc::Indent(contents) => {
181
+ let new_indent = cmd.indent + self.config.formatting.indent_width;
182
+ commands.push(PrintCommand {
183
+ indent: new_indent,
184
+ mode: cmd.mode,
185
+ doc: contents,
186
+ });
187
+ }
188
+
189
+ Doc::IfBreak {
190
+ break_contents,
191
+ flat_contents,
192
+ ..
193
+ } => {
194
+ let doc = match cmd.mode {
195
+ Mode::Break => break_contents,
196
+ Mode::Flat => flat_contents,
197
+ };
198
+ commands.push(PrintCommand {
199
+ indent: cmd.indent,
200
+ mode: cmd.mode,
201
+ doc,
202
+ });
203
+ }
204
+
205
+ Doc::Empty => {}
206
+
207
+ Doc::TrailingComment(text) => {
208
+ self.output.push(' ');
209
+ self.output.push_str(text);
210
+ self.pos += 1 + text.chars().count();
211
+ }
212
+
213
+ Doc::LeadingComment {
214
+ text,
215
+ hard_line_after,
216
+ } => {
217
+ self.output.push_str(text);
218
+ self.pos += text.chars().count();
219
+ if *hard_line_after {
220
+ self.output.push('\n');
221
+ let indent_str = self.get_indent(cmd.indent);
222
+ self.output.push_str(&indent_str);
223
+ self.pos = cmd.indent;
224
+ }
225
+ }
226
+
227
+ Doc::Align { n, contents } => {
228
+ let new_indent = cmd.indent + n;
229
+ commands.push(PrintCommand {
230
+ indent: new_indent,
231
+ mode: cmd.mode,
232
+ doc: contents,
233
+ });
234
+ }
235
+
236
+ Doc::LineSuffix(contents) => {
237
+ // For now, just print inline. Proper line suffix handling
238
+ // would buffer these until end of line.
239
+ commands.push(PrintCommand {
240
+ indent: cmd.indent,
241
+ mode: cmd.mode,
242
+ doc: contents,
243
+ });
244
+ }
245
+
246
+ Doc::Fill(docs) => {
247
+ // Fill tries to fit as many items on each line as possible.
248
+ // For now, simple implementation: join with softline behavior.
249
+ for (i, doc) in docs.iter().rev().enumerate() {
250
+ if i > 0 {
251
+ commands.push(PrintCommand {
252
+ indent: cmd.indent,
253
+ mode: cmd.mode,
254
+ doc: &Doc::Line {
255
+ soft: true,
256
+ hard: false,
257
+ literal: false,
258
+ },
259
+ });
260
+ }
261
+ commands.push(PrintCommand {
262
+ indent: cmd.indent,
263
+ mode: cmd.mode,
264
+ doc,
265
+ });
266
+ }
267
+ }
268
+ }
269
+ }
270
+
271
+ /// Determines if a Doc fits within the remaining width.
272
+ ///
273
+ /// Uses flat mode for inner groups and returns early when width is exceeded.
274
+ /// This is a hot path, so we pre-allocate a reasonable stack size.
275
+ #[inline]
276
+ fn fits(&self, doc: &Doc, indent: usize, remaining: usize, mode: Mode) -> bool {
277
+ let mut width = 0usize;
278
+ // Pre-allocate stack with reasonable capacity for typical nesting depth
279
+ let mut stack: Vec<(&Doc, usize, Mode)> = Vec::with_capacity(16);
280
+ stack.push((doc, indent, mode));
281
+
282
+ while let Some((doc, indent, mode)) = stack.pop() {
283
+ // Early exit: use >= for slightly earlier termination
284
+ if width >= remaining {
285
+ return false;
286
+ }
287
+
288
+ match doc {
289
+ Doc::Text(s) => {
290
+ // Use len() for ASCII strings (common case), chars().count() for accuracy
291
+ width += if s.is_ascii() {
292
+ s.len()
293
+ } else {
294
+ s.chars().count()
295
+ };
296
+ }
297
+
298
+ Doc::Concat(docs) => {
299
+ for d in docs.iter().rev() {
300
+ stack.push((d, indent, mode));
301
+ }
302
+ }
303
+
304
+ Doc::Group { contents, .. } => {
305
+ // Assume nested groups stay flat during fit check
306
+ stack.push((contents, indent, Mode::Flat));
307
+ }
308
+
309
+ Doc::Line { soft, hard, .. } => {
310
+ if *hard {
311
+ // Hard line forces a break - content after fits on new line
312
+ return true;
313
+ }
314
+ match mode {
315
+ Mode::Flat if *soft => {} // soft line: nothing
316
+ Mode::Flat => width += 1, // regular line: space
317
+ Mode::Break => return true, // break mode: newline ends measurement
318
+ }
319
+ }
320
+
321
+ Doc::Indent(contents) => {
322
+ let new_indent = indent + self.config.formatting.indent_width;
323
+ stack.push((contents, new_indent, mode));
324
+ }
325
+
326
+ Doc::IfBreak {
327
+ flat_contents,
328
+ break_contents,
329
+ ..
330
+ } => {
331
+ let contents = match mode {
332
+ Mode::Flat => flat_contents,
333
+ Mode::Break => break_contents,
334
+ };
335
+ stack.push((contents, indent, mode));
336
+ }
337
+
338
+ Doc::Empty => {}
339
+
340
+ Doc::TrailingComment(s) => {
341
+ width += 1 + if s.is_ascii() {
342
+ s.len()
343
+ } else {
344
+ s.chars().count()
345
+ };
346
+ }
347
+
348
+ Doc::LeadingComment { text, .. } => {
349
+ width += if text.is_ascii() {
350
+ text.len()
351
+ } else {
352
+ text.chars().count()
353
+ };
354
+ }
355
+
356
+ Doc::Align { n, contents } => {
357
+ stack.push((contents, indent + n, mode));
358
+ }
359
+
360
+ Doc::LineSuffix(contents) => {
361
+ stack.push((contents, indent, mode));
362
+ }
363
+
364
+ Doc::Fill(docs) => {
365
+ for d in docs.iter().rev() {
366
+ stack.push((d, indent, mode));
367
+ }
368
+ }
369
+ }
370
+ }
371
+
372
+ width <= remaining
373
+ }
374
+
375
+ fn get_indent(&mut self, width: usize) -> String {
376
+ if width < self.indent_cache.len() {
377
+ return self.indent_cache[width].clone();
378
+ }
379
+
380
+ let indent_width = self.config.formatting.indent_width;
381
+ let indent_style = &self.config.formatting.indent_style;
382
+ while self.indent_cache.len() <= width {
383
+ let w = self.indent_cache.len();
384
+ self.indent_cache
385
+ .push(Self::build_indent_string(w, indent_width, indent_style));
386
+ }
387
+ self.indent_cache[width].clone()
388
+ }
389
+ }
390
+
391
+ #[cfg(test)]
392
+ mod tests {
393
+ use super::*;
394
+ use crate::doc::builders::*;
395
+
396
+ /// Helper to print a doc with default config.
397
+ fn print_doc(doc: &Doc) -> String {
398
+ let config = Config::default();
399
+ let mut printer = Printer::new(&config);
400
+ printer.print(doc)
401
+ }
402
+
403
+ #[test]
404
+ fn test_print_text() {
405
+ let doc = text("hello");
406
+ let result = print_doc(&doc);
407
+ assert_eq!(result, "hello\n");
408
+ }
409
+
410
+ #[test]
411
+ fn test_print_concat() {
412
+ let doc = concat(vec![text("a"), text("b"), text("c")]);
413
+ let result = print_doc(&doc);
414
+ assert_eq!(result, "abc\n");
415
+ }
416
+
417
+ #[test]
418
+ fn test_print_hardline() {
419
+ let doc = concat(vec![text("line1"), hardline(), text("line2")]);
420
+ let result = print_doc(&doc);
421
+ assert_eq!(result, "line1\nline2\n");
422
+ }
423
+
424
+ #[test]
425
+ fn test_print_indent() {
426
+ // Correct Doc structure: hardline inside indent
427
+ let doc = concat(vec![
428
+ text("def foo"),
429
+ indent(concat(vec![hardline(), text("body")])),
430
+ hardline(),
431
+ text("end"),
432
+ ]);
433
+ let result = print_doc(&doc);
434
+ assert_eq!(result, "def foo\n body\nend\n");
435
+ }
436
+
437
+ #[test]
438
+ fn test_print_nested_indent() {
439
+ // Correct Doc structure: hardline inside indent
440
+ let doc = concat(vec![
441
+ text("class Foo"),
442
+ indent(concat(vec![
443
+ hardline(),
444
+ text("def bar"),
445
+ indent(concat(vec![hardline(), text("puts 'hello'")])),
446
+ hardline(),
447
+ text("end"),
448
+ ])),
449
+ hardline(),
450
+ text("end"),
451
+ ]);
452
+ let result = print_doc(&doc);
453
+ assert_eq!(
454
+ result,
455
+ "class Foo\n def bar\n puts 'hello'\n end\nend\n"
456
+ );
457
+ }
458
+
459
+ #[test]
460
+ fn test_print_group_fits() {
461
+ // Short content should stay on one line
462
+ let doc = group(concat(vec![text("short"), line(), text("text")]));
463
+ let result = print_doc(&doc);
464
+ assert_eq!(result, "short text\n");
465
+ }
466
+
467
+ #[test]
468
+ fn test_print_group_breaks() {
469
+ // Create content that doesn't fit on one line
470
+ let long = "a".repeat(80);
471
+ let doc = group(concat(vec![text(&long), line(), text("more")]));
472
+ let result = print_doc(&doc);
473
+ assert!(result.contains('\n'));
474
+ // Should break: long\nmore
475
+ assert!(result.starts_with(&long));
476
+ }
477
+
478
+ #[test]
479
+ fn test_print_softline_flat() {
480
+ // Softline disappears in flat mode
481
+ let doc = group(concat(vec![
482
+ text("["),
483
+ softline(),
484
+ text("1"),
485
+ softline(),
486
+ text("]"),
487
+ ]));
488
+ let result = print_doc(&doc);
489
+ assert_eq!(result, "[1]\n");
490
+ }
491
+
492
+ #[test]
493
+ fn test_print_if_break_flat() {
494
+ let doc = group(concat(vec![
495
+ text("["),
496
+ if_break(text("BROKEN"), text("FLAT")),
497
+ text("]"),
498
+ ]));
499
+ let result = print_doc(&doc);
500
+ assert_eq!(result, "[FLAT]\n");
501
+ }
502
+
503
+ #[test]
504
+ fn test_print_if_break_broken() {
505
+ // Force break with group_break (break_parent = true)
506
+ let doc = group_break(concat(vec![
507
+ text("["),
508
+ line(),
509
+ if_break(text("BROKEN"), text("FLAT")),
510
+ text("]"),
511
+ ]));
512
+ let result = print_doc(&doc);
513
+ assert!(
514
+ result.contains("BROKEN"),
515
+ "Expected BROKEN but got: {}",
516
+ result
517
+ );
518
+ }
519
+
520
+ #[test]
521
+ fn test_print_trailing_comment() {
522
+ let doc = concat(vec![text("code"), trailing_comment("# comment")]);
523
+ let result = print_doc(&doc);
524
+ assert_eq!(result, "code # comment\n");
525
+ }
526
+
527
+ #[test]
528
+ fn test_print_leading_comment() {
529
+ let doc = concat(vec![leading_comment("# comment", true), text("code")]);
530
+ let result = print_doc(&doc);
531
+ assert_eq!(result, "# comment\ncode\n");
532
+ }
533
+
534
+ #[test]
535
+ fn test_print_empty() {
536
+ let doc = empty();
537
+ let result = print_doc(&doc);
538
+ assert_eq!(result, "");
539
+ }
540
+
541
+ #[test]
542
+ fn test_print_join() {
543
+ let doc = join(text(", "), vec![text("a"), text("b"), text("c")]);
544
+ let result = print_doc(&doc);
545
+ assert_eq!(result, "a, b, c\n");
546
+ }
547
+
548
+ #[test]
549
+ fn test_print_ruby_class() {
550
+ // Correct Doc structure: hardline inside indent
551
+ let doc = concat(vec![
552
+ text("class Foo"),
553
+ indent(concat(vec![
554
+ hardline(),
555
+ text("def initialize"),
556
+ indent(concat(vec![hardline(), text("@value = 1")])),
557
+ hardline(),
558
+ text("end"),
559
+ ])),
560
+ hardline(),
561
+ text("end"),
562
+ ]);
563
+ let result = print_doc(&doc);
564
+
565
+ let expected = "\
566
+ class Foo
567
+ def initialize
568
+ @value = 1
569
+ end
570
+ end
571
+ ";
572
+ assert_eq!(result, expected);
573
+ }
574
+
575
+ #[test]
576
+ fn test_print_ruby_array_fits() {
577
+ // Array that fits on one line
578
+ let doc = group(concat(vec![
579
+ text("["),
580
+ softline(),
581
+ join(
582
+ concat(vec![text(","), line()]),
583
+ vec![text("1"), text("2"), text("3")],
584
+ ),
585
+ softline(),
586
+ text("]"),
587
+ ]));
588
+ let result = print_doc(&doc);
589
+ assert_eq!(result, "[1, 2, 3]\n");
590
+ }
591
+
592
+ #[test]
593
+ fn test_print_literal_line() {
594
+ // Literal line should not add indentation
595
+ let doc = concat(vec![
596
+ text("<<~HEREDOC"),
597
+ indent(concat(vec![literalline(), text("content"), literalline()])),
598
+ text("HEREDOC"),
599
+ ]);
600
+ let result = print_doc(&doc);
601
+ // literalline should not add indent despite being inside indent()
602
+ assert!(result.contains("\ncontent\n"));
603
+ }
604
+
605
+ // Performance regression tests
606
+ // Run with: cargo test --release perf_
607
+
608
+ #[test]
609
+ fn perf_deep_nesting() {
610
+ let config = Config::default();
611
+
612
+ // Create 20-level deep nesting
613
+ let mut doc = text("deepest");
614
+ for _ in 0..20 {
615
+ doc = indent(concat(vec![hardline(), doc]));
616
+ }
617
+
618
+ let mut printer = Printer::new(&config);
619
+ let result = printer.print(&doc);
620
+
621
+ // Verify it produces valid output
622
+ assert!(result.lines().count() >= 20);
623
+ }
624
+
625
+ #[test]
626
+ fn perf_many_hardlines() {
627
+ let config = Config::default();
628
+
629
+ // Create 500 lines
630
+ let mut lines: Vec<Doc> = Vec::new();
631
+ for i in 0..500 {
632
+ if i > 0 {
633
+ lines.push(hardline());
634
+ }
635
+ lines.push(text("line"));
636
+ }
637
+ let doc = indent(concat(lines));
638
+
639
+ let mut printer = Printer::new(&config);
640
+ let result = printer.print(&doc);
641
+
642
+ assert_eq!(result.lines().count(), 500);
643
+ }
644
+
645
+ #[test]
646
+ fn perf_nested_groups() {
647
+ let config = Config::default();
648
+
649
+ // Create nested group structure
650
+ let inner = group(concat(vec![
651
+ text("["),
652
+ softline(),
653
+ join(
654
+ concat(vec![text(","), line()]),
655
+ vec![text("1"), text("2"), text("3")],
656
+ ),
657
+ softline(),
658
+ text("]"),
659
+ ]));
660
+
661
+ let doc = group(concat(vec![
662
+ text("{"),
663
+ softline(),
664
+ text("a: "),
665
+ inner.clone(),
666
+ text(","),
667
+ line(),
668
+ text("b: "),
669
+ inner.clone(),
670
+ text(","),
671
+ line(),
672
+ text("c: "),
673
+ inner,
674
+ softline(),
675
+ text("}"),
676
+ ]));
677
+
678
+ let mut printer = Printer::new(&config);
679
+ let result = printer.print(&doc);
680
+
681
+ // Should produce valid output
682
+ assert!(result.contains("[1, 2, 3]") || result.contains("[\n"));
683
+ }
684
+ }