rfmt 1.5.2 → 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,220 @@
1
+ //! Doc IR (Intermediate Representation) for rfmt
2
+ //!
3
+ //! This module provides a Prettier/Biome-style intermediate representation
4
+ //! for code formatting. The Doc IR separates formatting intent from string generation,
5
+ //! enabling automatic line breaking and consistent formatting.
6
+ //!
7
+ //! # Architecture
8
+ //!
9
+ //! ```text
10
+ //! AST → DocBuilder → Doc IR → Printer → String
11
+ //! ```
12
+ //!
13
+ //! # Example
14
+ //!
15
+ //! ```rust
16
+ //! use rfmt::doc::builders::*;
17
+ //! use rfmt::doc::Printer;
18
+ //!
19
+ //! let doc = concat(vec![
20
+ //! text("def foo"),
21
+ //! hardline(),
22
+ //! indent(text("body")),
23
+ //! hardline(),
24
+ //! text("end"),
25
+ //! ]);
26
+ //!
27
+ //! let mut printer = Printer::new(config);
28
+ //! let output = printer.print(&doc);
29
+ //! // => "def foo\n body\nend"
30
+ //! ```
31
+
32
+ pub mod builders;
33
+ pub mod printer;
34
+
35
+ pub use builders::*;
36
+ pub use printer::Printer;
37
+
38
+ /// Document intermediate representation for code formatting.
39
+ ///
40
+ /// Doc is a tree structure that describes how code should be formatted
41
+ /// without committing to specific line breaks. The Printer converts
42
+ /// Doc to a string, making line break decisions based on line width.
43
+ #[derive(Debug, Clone, PartialEq)]
44
+ pub enum Doc {
45
+ /// A text literal that is printed as-is.
46
+ ///
47
+ /// # Example
48
+ /// ```rust
49
+ /// Doc::Text("hello".to_string())
50
+ /// ```
51
+ Text(String),
52
+
53
+ /// Concatenation of multiple documents.
54
+ ///
55
+ /// # Example
56
+ /// ```rust
57
+ /// Doc::Concat(vec![Doc::Text("a".into()), Doc::Text("b".into())])
58
+ /// // Prints: "ab"
59
+ /// ```
60
+ Concat(Vec<Doc>),
61
+
62
+ /// A group that tries to fit on one line.
63
+ ///
64
+ /// If the contents fit within the remaining line width, Line docs
65
+ /// inside are replaced with spaces. Otherwise, they become newlines.
66
+ ///
67
+ /// # Fields
68
+ /// - `contents`: The document to group
69
+ /// - `break_parent`: If true, forces parent groups to break
70
+ /// - `id`: Optional identifier for IfBreak references
71
+ Group {
72
+ contents: Box<Doc>,
73
+ break_parent: bool,
74
+ id: Option<GroupId>,
75
+ },
76
+
77
+ /// A potential line break.
78
+ ///
79
+ /// # Variants (controlled by fields)
80
+ /// - `soft=false, hard=false`: Space if flat, newline if broken
81
+ /// - `soft=true, hard=false`: Nothing if flat, newline if broken
82
+ /// - `hard=true`: Always a newline
83
+ /// - `literal=true`: Newline without indentation (for heredocs)
84
+ Line {
85
+ soft: bool,
86
+ hard: bool,
87
+ literal: bool,
88
+ },
89
+
90
+ /// Increases indentation for the contents.
91
+ ///
92
+ /// # Example
93
+ /// ```rust
94
+ /// concat(vec![
95
+ /// text("if true"),
96
+ /// hardline(),
97
+ /// indent(text("body")),
98
+ /// hardline(),
99
+ /// text("end"),
100
+ /// ])
101
+ /// // Prints:
102
+ /// // if true
103
+ /// // body
104
+ /// // end
105
+ /// ```
106
+ Indent(Box<Doc>),
107
+
108
+ /// Conditional content based on group break state.
109
+ ///
110
+ /// If the group is broken (multi-line), prints `break_contents`.
111
+ /// If the group is flat (single line), prints `flat_contents`.
112
+ ///
113
+ /// # Fields
114
+ /// - `break_contents`: Printed when group is broken
115
+ /// - `flat_contents`: Printed when group is flat
116
+ /// - `group_id`: Optional reference to a specific group
117
+ IfBreak {
118
+ break_contents: Box<Doc>,
119
+ flat_contents: Box<Doc>,
120
+ group_id: Option<GroupId>,
121
+ },
122
+
123
+ /// An empty document that prints nothing.
124
+ Empty,
125
+
126
+ /// A trailing comment that appears at the end of a line.
127
+ ///
128
+ /// # Example
129
+ /// ```rust
130
+ /// concat(vec![text("code"), Doc::TrailingComment("# comment".into())])
131
+ /// // Prints: "code # comment"
132
+ /// ```
133
+ TrailingComment(String),
134
+
135
+ /// A leading comment that appears before code.
136
+ ///
137
+ /// # Fields
138
+ /// - `text`: The comment text
139
+ /// - `hard_line_after`: Whether to add a hard line after the comment
140
+ LeadingComment { text: String, hard_line_after: bool },
141
+
142
+ /// Aligns content to a specific column offset.
143
+ ///
144
+ /// Used for aligning continuation lines or heredoc content.
145
+ Align { n: usize, contents: Box<Doc> },
146
+
147
+ /// Content to be appended at the end of the line.
148
+ ///
149
+ /// Useful for trailing commas that should only appear in multi-line mode.
150
+ LineSuffix(Box<Doc>),
151
+
152
+ /// Fill: packs content into lines as tightly as possible.
153
+ ///
154
+ /// Used for array elements, argument lists, etc.
155
+ Fill(Vec<Doc>),
156
+ }
157
+
158
+ /// Identifier for referencing groups in IfBreak.
159
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
160
+ pub struct GroupId(pub u32);
161
+
162
+ impl Doc {
163
+ /// Returns true if this doc is empty.
164
+ pub fn is_empty(&self) -> bool {
165
+ matches!(self, Doc::Empty)
166
+ }
167
+
168
+ /// Returns true if this doc contains only text (no line breaks possible).
169
+ pub fn is_flat(&self) -> bool {
170
+ match self {
171
+ Doc::Text(_) => true,
172
+ Doc::Concat(docs) => docs.iter().all(|d| d.is_flat()),
173
+ Doc::Group { contents, .. } => contents.is_flat(),
174
+ Doc::Indent(contents) => contents.is_flat(),
175
+ Doc::IfBreak { flat_contents, .. } => flat_contents.is_flat(),
176
+ Doc::Empty => true,
177
+ Doc::TrailingComment(_) | Doc::LeadingComment { .. } => true,
178
+ Doc::Line { .. } => false,
179
+ Doc::Align { contents, .. } => contents.is_flat(),
180
+ Doc::LineSuffix(_) => true,
181
+ Doc::Fill(docs) => docs.iter().all(|d| d.is_flat()),
182
+ }
183
+ }
184
+ }
185
+
186
+ #[cfg(test)]
187
+ mod tests {
188
+ use super::*;
189
+
190
+ #[test]
191
+ fn test_doc_is_empty() {
192
+ assert!(Doc::Empty.is_empty());
193
+ assert!(!Doc::Text("hello".into()).is_empty());
194
+ }
195
+
196
+ #[test]
197
+ fn test_doc_is_flat() {
198
+ assert!(Doc::Text("hello".into()).is_flat());
199
+ assert!(Doc::Empty.is_flat());
200
+ assert!(Doc::Concat(vec![Doc::Text("a".into()), Doc::Text("b".into())]).is_flat());
201
+
202
+ // Line is not flat
203
+ assert!(!Doc::Line {
204
+ soft: false,
205
+ hard: false,
206
+ literal: false
207
+ }
208
+ .is_flat());
209
+ }
210
+
211
+ #[test]
212
+ fn test_group_id_equality() {
213
+ let id1 = GroupId(1);
214
+ let id2 = GroupId(1);
215
+ let id3 = GroupId(2);
216
+
217
+ assert_eq!(id1, id2);
218
+ assert_ne!(id1, id3);
219
+ }
220
+ }