1use std::fmt;
13
14use rowan::NodeOrToken;
15
16use crate::SyntaxKind::{self, *};
17use crate::ast::{AstNode, SourceFile, unescape};
18use crate::{Diagnostic, SyntaxNode, SyntaxToken, parse};
19
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct FormatOptions {
22 pub indent_width: usize,
24 pub use_tabs: bool,
26}
27
28impl Default for FormatOptions {
29 fn default() -> Self {
30 Self {
31 indent_width: 2,
32 use_tabs: false,
33 }
34 }
35}
36
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub enum FormatError {
39 Syntax(Vec<Diagnostic>),
41 Unstable(String),
43}
44
45impl fmt::Display for FormatError {
46 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47 match self {
48 FormatError::Syntax(diagnostics) => {
49 write!(f, "cannot format: {} syntax error(s)", diagnostics.len())
50 }
51 FormatError::Unstable(reason) => {
52 write!(f, "formatter produced different code: {reason}")
53 }
54 }
55 }
56}
57
58impl std::error::Error for FormatError {}
59
60pub fn format(text: &str, options: &FormatOptions) -> Result<String, FormatError> {
62 let parse = parse(text);
63 if parse.has_errors() {
64 return Err(FormatError::Syntax(parse.diagnostics().to_vec()));
65 }
66 let mut output = format_tree(&parse.tree(), options);
67 if uses_crlf(text) {
68 output = output.replace("\r\n", "\n").replace('\n', "\r\n");
69 }
70 verify(&parse.syntax(), &output)?;
71 Ok(output)
72}
73
74fn uses_crlf(text: &str) -> bool {
76 text.find('\n')
77 .is_some_and(|index| text[..index].ends_with('\r'))
78}
79
80pub fn format_tree(file: &SourceFile, options: &FormatOptions) -> String {
83 let mut formatter = Formatter {
84 options,
85 out: String::new(),
86 depth: 0,
87 last: None,
88 };
89 formatter.items(file.syntax(), Container::File);
90 let mut out = formatter.out;
91 let trimmed = out.trim_end().len();
92 out.truncate(trimmed);
93 if !out.is_empty() {
94 out.push('\n');
95 }
96 out
97}
98
99fn verify(original: &SyntaxNode, output: &str) -> Result<(), FormatError> {
101 let reparsed = parse(output);
102 if reparsed.has_errors() {
103 return Err(FormatError::Unstable("output has syntax errors".into()));
104 }
105 let tokens = |node: &SyntaxNode| -> Vec<(SyntaxKind, String)> {
106 node.descendants_with_tokens()
107 .filter_map(NodeOrToken::into_token)
108 .filter(|t| t.kind() != Whitespace)
109 .map(|t| (t.kind(), t.text().trim_end().to_owned()))
110 .collect()
111 };
112 let before = tokens(original);
113 let after = tokens(&reparsed.syntax());
114 if before.len() != after.len() {
115 return Err(FormatError::Unstable(format!(
116 "{} tokens became {}",
117 before.len(),
118 after.len()
119 )));
120 }
121 if let Some((a, b)) = before.iter().zip(&after).find(|(a, b)| a != b) {
122 return Err(FormatError::Unstable(format!("`{}` became `{}`", a.1, b.1)));
123 }
124 Ok(())
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128enum Container {
129 File,
130 Block,
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
135enum Section {
136 Metadata,
137 ConceptMap,
138 Map,
139 Uses,
140 Imports,
141 Constants,
142 Group,
143 Other,
144}
145
146impl Section {
147 fn of(kind: SyntaxKind) -> Section {
148 match kind {
149 Metadata => Section::Metadata,
150 ConceptMap => Section::ConceptMap,
151 MapDeclaration => Section::Map,
152 UsesDeclaration => Section::Uses,
153 ImportsDeclaration => Section::Imports,
154 ConstantDeclaration => Section::Constants,
155 Group => Section::Group,
156 _ => Section::Other,
157 }
158 }
159
160 fn spaced(self) -> bool {
162 matches!(self, Section::Group | Section::ConceptMap)
163 }
164}
165
166enum Item {
167 Node(SyntaxNode),
168 Comment(SyntaxToken),
169}
170
171struct Entry {
172 item: Item,
173 newlines: usize,
175}
176
177struct Formatter<'a> {
178 options: &'a FormatOptions,
179 out: String,
180 depth: usize,
181 last: Option<SyntaxToken>,
183}
184
185impl Formatter<'_> {
186 fn newline(&mut self, blank: bool) {
187 let trimmed = self.out.trim_end_matches([' ', '\t']).len();
188 self.out.truncate(trimmed);
189 if self.out.is_empty() {
190 return;
191 }
192 self.out.push('\n');
193 if blank {
194 self.out.push('\n');
195 }
196 for _ in 0..self.depth {
197 if self.options.use_tabs {
198 self.out.push('\t');
199 } else {
200 self.out
201 .extend(std::iter::repeat_n(' ', self.options.indent_width));
202 }
203 }
204 self.last = None;
205 }
206
207 fn items(&mut self, node: &SyntaxNode, container: Container) {
210 let entries = collect_entries(node, container);
211 let sections: Vec<Option<Section>> = (0..entries.len())
212 .map(|i| {
213 entries[i..].iter().find_map(|e| match &e.item {
214 Item::Node(n) => Some(Section::of(n.kind())),
215 Item::Comment(_) => None,
216 })
217 })
218 .collect();
219 let mut previous: Option<(Section, bool)> = None;
220 for (index, entry) in entries.iter().enumerate() {
221 let first = index == 0;
222 let same_line = entry.newlines == 0 && (!first || container == Container::Block);
223 if let (Item::Comment(comment), true) = (&entry.item, same_line) {
224 self.out.push(' ');
225 self.out.push_str(comment.text());
226 self.last = None;
227 continue;
228 }
229 let mut blank = entry.newlines >= 2 && !first;
230 if container == Container::File
231 && let (Some((previous_section, previous_was_node)), Some(section)) =
232 (previous, sections[index])
233 && (previous_section != section || (section.spaced() && previous_was_node))
234 {
235 blank = true;
236 }
237 self.newline(blank);
238 match &entry.item {
239 Item::Comment(comment) => {
240 self.out.push_str(comment.text().trim_end());
241 self.last = None;
242 if let Some(section) = sections[index] {
243 previous = Some((section, false));
244 }
245 }
246 Item::Node(node) => {
247 self.node(node);
248 previous = Some((Section::of(node.kind()), true));
249 }
250 }
251 }
252 }
253
254 fn node(&mut self, node: &SyntaxNode) {
255 if node.kind() == Error || has_loose_comments(node) {
256 return self.verbatim(node);
257 }
258 if matches!(node.kind(), RuleBlock | ConceptMap) {
259 return self.block(node);
260 }
261 for element in node.children_with_tokens() {
262 match element {
263 NodeOrToken::Node(child) => self.node(&child),
264 NodeOrToken::Token(token) if !token.kind().is_trivia() => self.token(&token),
265 NodeOrToken::Token(_) => {}
266 }
267 }
268 }
269
270 fn block(&mut self, node: &SyntaxNode) {
272 let mut inside = false;
273 for element in node.children_with_tokens() {
274 match element {
275 NodeOrToken::Token(token) if token.kind() == LBrace && !inside => {
276 inside = true;
277 self.token(&token);
278 let empty = !node
279 .children_with_tokens()
280 .skip_while(|e| e.kind() != LBrace)
281 .skip(1)
282 .any(|e| e.kind() != RBrace && e.kind() != Whitespace);
283 if empty {
284 continue;
285 }
286 self.depth += 1;
287 self.items(node, Container::Block);
288 self.depth -= 1;
289 self.newline(false);
290 }
291 NodeOrToken::Token(token) if token.kind() == RBrace => {
292 self.out.push('}');
293 self.last = Some(token);
294 inside = false;
295 }
296 _ if inside => {}
297 NodeOrToken::Node(child) => self.node(&child),
298 NodeOrToken::Token(token) if !token.kind().is_trivia() => self.token(&token),
299 NodeOrToken::Token(_) => {}
300 }
301 }
302 }
303
304 fn token(&mut self, token: &SyntaxToken) {
305 if let Some(last) = &self.last
306 && space_between(last, token)
307 {
308 self.out.push(' ');
309 }
310 self.out.push_str(token.text());
311 self.last = Some(token.clone());
312 }
313
314 fn verbatim(&mut self, node: &SyntaxNode) {
316 let first = node
317 .descendants_with_tokens()
318 .filter_map(NodeOrToken::into_token)
319 .find(|t| !t.kind().is_trivia());
320 if let (Some(last), Some(first)) = (&self.last, &first)
321 && space_between(last, first)
322 {
323 self.out.push(' ');
324 }
325 self.out.push_str(node.text().to_string().trim());
326 self.last = node
327 .descendants_with_tokens()
328 .filter_map(NodeOrToken::into_token)
329 .filter(|t| !t.kind().is_trivia())
330 .last();
331 }
332}
333
334fn collect_entries(node: &SyntaxNode, container: Container) -> Vec<Entry> {
335 let mut entries = Vec::new();
336 let mut newlines = 0;
337 let mut inside = container == Container::File;
338 for element in node.children_with_tokens() {
339 match element {
340 NodeOrToken::Token(token) if token.kind() == LBrace && !inside => inside = true,
341 NodeOrToken::Token(token) if token.kind() == RBrace && inside => break,
342 _ if !inside => {}
343 NodeOrToken::Token(token) if token.kind() == Whitespace => {
344 newlines += line_breaks(token.text());
345 }
346 NodeOrToken::Token(token) if token.kind().is_comment() => entries.push(Entry {
347 item: Item::Comment(token),
348 newlines: std::mem::take(&mut newlines),
349 }),
350 NodeOrToken::Token(_) => {}
351 NodeOrToken::Node(child) => entries.push(Entry {
352 item: Item::Node(child),
353 newlines: std::mem::take(&mut newlines),
354 }),
355 }
356 }
357 entries
358}
359
360fn line_breaks(text: &str) -> usize {
361 text.matches('\n').count() + text.matches('\r').count() - text.matches("\r\n").count()
362}
363
364fn has_loose_comments(node: &SyntaxNode) -> bool {
368 let is_block = matches!(node.kind(), RuleBlock | ConceptMap);
369 let mut after_brace = false;
370 for element in node.children_with_tokens() {
371 match element {
372 NodeOrToken::Token(token) => {
373 after_brace |= token.kind() == LBrace;
374 if token.kind().is_comment() && !(is_block && after_brace) {
375 return true;
376 }
377 }
378 NodeOrToken::Node(child) => {
379 if !(is_block && after_brace) && contains_comment_outside_blocks(&child) {
381 return true;
382 }
383 }
384 }
385 }
386 false
387}
388
389fn contains_comment_outside_blocks(node: &SyntaxNode) -> bool {
390 if matches!(node.kind(), RuleBlock | ConceptMap) {
391 return false;
392 }
393 node.children_with_tokens().any(|element| match element {
394 NodeOrToken::Token(token) => token.kind().is_comment(),
395 NodeOrToken::Node(child) => contains_comment_outside_blocks(&child),
396 })
397}
398
399pub fn expression_text(node: &SyntaxNode) -> String {
403 let mut out = String::new();
404 let mut last: Option<SyntaxToken> = None;
405 for token in node
406 .descendants_with_tokens()
407 .filter_map(NodeOrToken::into_token)
408 .filter(|t| !t.kind().is_trivia())
409 {
410 if let Some(previous) = &last
411 && space_between(previous, &token)
412 {
413 out.push(' ');
414 }
415 if token.kind() == DoubleQuotedString {
416 out.push_str("e_fhirpath_string(&unescape(token.text())));
417 } else {
418 out.push_str(token.text());
419 }
420 last = Some(token);
421 }
422 out
423}
424
425pub fn quote_fhirpath_string(value: &str) -> String {
427 let mut out = String::with_capacity(value.len() + 2);
428 out.push('\'');
429 for c in value.chars() {
430 match c {
431 '\'' => out.push_str("\\'"),
432 '\\' => out.push_str("\\\\"),
433 '\n' => out.push_str("\\n"),
434 '\r' => out.push_str("\\r"),
435 '\t' => out.push_str("\\t"),
436 '\u{c}' => out.push_str("\\f"),
437 c => out.push(c),
438 }
439 }
440 out.push('\'');
441 out
442}
443
444fn space_between(previous: &SyntaxToken, next: &SyntaxToken) -> bool {
445 let previous_parent = previous.parent().map(|p| p.kind());
446 let next_parent = next.parent().map(|p| p.kind());
447 let same_parent = |kind: SyntaxKind| previous_parent == Some(kind) && next_parent == Some(kind);
448 match (previous.kind(), next.kind()) {
449 (_, Comma | Semicolon | RParen | RBracket | Dot | DotDot) => false,
450 (LParen | LBracket | Dot | DotDot | Percent, _) => false,
451 (_, LBracket) => false,
452 (Plus | Minus, _) if previous_parent == Some(PrefixExpr) => false,
453 (LBrace, RBrace) => false,
454 (_, Colon) if next_parent == Some(IdentityList) => false,
455 (_, LParen) => !matches!(next_parent, Some(ArgumentList | ParameterList)),
456 _ if same_parent(TypeMode) || same_parent(ConceptMapCode) => false,
457 _ if same_parent(ConceptMapRelation) => false,
458 _ if same_parent(QualifiedName) => false,
459 _ => true,
460 }
461}
462
463#[cfg(test)]
464mod tests {
465 use super::*;
466
467 fn fmt(text: &str) -> String {
468 format(text, &FormatOptions::default()).unwrap_or_else(|e| panic!("{e}: {e:?}"))
469 }
470
471 #[test]
472 fn normalizes_layout() {
473 let source = r#"/// url='http://x/Map'
474uses "http://hl7.org/fhir/StructureDefinition/Patient" alias P as source
475uses "http://hl7.org/fhir/StructureDefinition/Patient" as target
476group G(source src:P,target tgt) extends Base <<type+>> {
477src.name as n where (n.use='official')->tgt.name as t then {
478n.family->t.family;
479}"named";
480src.a->tgt.b=create('Coding') as c,c.code=(src.x+1);
481}
482group H(source s, target t){}
483"#;
484 insta::assert_snapshot!(fmt(source), @r#"
485 /// url = 'http://x/Map'
486
487 uses "http://hl7.org/fhir/StructureDefinition/Patient" alias P as source
488 uses "http://hl7.org/fhir/StructureDefinition/Patient" as target
489
490 group G(source src : P, target tgt) extends Base <<type+>> {
491 src.name as n where (n.use = 'official') -> tgt.name as t then {
492 n.family -> t.family;
493 } "named";
494 src.a -> tgt.b = create('Coding') as c, c.code = (src.x + 1);
495 }
496
497 group H(source s, target t) {}
498 "#);
499 }
500
501 #[test]
502 fn keeps_comments() {
503 let source = "// header\n\nuses \"u\" as source // why\n\n// group doc\ngroup G(source s, target t) { // opens\n // first\n s.a -> t.a; // trailing\n\n\n s.b /* inline */ -> t.b;\n // last\n}\n";
504 insta::assert_snapshot!(fmt(source), @r#"
505 // header
506
507 uses "u" as source // why
508
509 // group doc
510 group G(source s, target t) { // opens
511 // first
512 s.a -> t.a; // trailing
513
514 s.b /* inline */ -> t.b;
515 // last
516 }
517 "#);
518 }
519
520 #[test]
521 fn idempotent() {
522 let source =
523 "group G(source s, target t) {\n s.a as v -> t.a = v, t.b = 'x' share k \"r\";\n}\n";
524 let once = fmt(source);
525 assert_eq!(fmt(&once), once);
526 }
527
528 #[test]
529 fn keeps_crlf_line_endings() {
530 let source =
531 "uses \"u\" as source\r\n\r\n\r\ngroup G(source s, target t) {\r\n s.a->t.a;\r\n}\r\n";
532 assert_eq!(
533 fmt(source),
534 "uses \"u\" as source\r\n\r\ngroup G(source s, target t) {\r\n s.a -> t.a;\r\n}\r\n"
535 );
536 }
537
538 #[test]
539 fn refuses_broken_input() {
540 assert!(matches!(
541 format("group G(source s { s.a -> ", &FormatOptions::default()),
542 Err(FormatError::Syntax(_))
543 ));
544 }
545}