1pub mod config;
61mod format_rules;
62mod layout_ast;
63
64use daml_parser::ast::DiagnosticCategory;
65use daml_parser::lexer::{TokenKind, TriviaKind};
66use daml_syntax::{CharColumn, LineNumber, SourceFile, SourceTokens};
67pub use format_rules::{FormatRule, FormatRuleParseError, FormatRuleSet};
68use std::error::Error;
69use std::fmt;
70
71#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct FormatDiagnostic {
74 line: LineNumber,
75 column: CharColumn,
76 category: DiagnosticCategory,
77 message: String,
78}
79
80impl FormatDiagnostic {
81 #[must_use]
83 pub const fn line(&self) -> LineNumber {
84 self.line
85 }
86
87 #[must_use]
89 pub const fn column(&self) -> CharColumn {
90 self.column
91 }
92
93 #[must_use]
95 pub const fn category(&self) -> DiagnosticCategory {
96 self.category
97 }
98
99 #[must_use]
101 pub fn message(&self) -> &str {
102 &self.message
103 }
104}
105
106impl fmt::Display for FormatDiagnostic {
107 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108 if self.category == DiagnosticCategory::Lex {
109 write!(f, "{}:{}: {}", self.line, self.column, self.message)
110 } else {
111 write!(
112 f,
113 "{}:{}: [{}] {}",
114 self.line, self.column, self.category, self.message
115 )
116 }
117 }
118}
119
120#[derive(Debug, Clone, PartialEq, Eq)]
126pub struct FormatError {
127 diagnostics: Vec<FormatDiagnostic>,
128}
129
130impl FormatError {
131 #[must_use]
133 pub fn diagnostics(&self) -> &[FormatDiagnostic] {
134 &self.diagnostics
135 }
136}
137
138impl AsRef<[FormatDiagnostic]> for FormatError {
139 fn as_ref(&self) -> &[FormatDiagnostic] {
140 self.diagnostics()
141 }
142}
143
144impl fmt::Display for FormatError {
145 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146 for (index, diagnostic) in self.diagnostics.iter().enumerate() {
147 if index > 0 {
148 f.write_str("\n")?;
149 }
150 diagnostic.fmt(f)?;
151 }
152 Ok(())
153 }
154}
155
156impl Error for FormatError {}
157
158#[must_use]
164pub fn lex_diagnostics(src: &str) -> Vec<FormatDiagnostic> {
165 collect_lex_diagnostics(src)
166}
167
168#[must_use]
176pub fn source_diagnostics(src: &str) -> Vec<FormatDiagnostic> {
177 let cpp_module_header_ranges = cpp_conditional_module_header_ranges(src);
178 let suppress_cpp_recovery = has_cpp_conditionals(src);
179 let source_line_count = src.lines().count();
180
181 SourceFile::parse(src)
182 .diagnostics()
183 .iter()
184 .filter(|diagnostic| {
185 !suppress_cpp_recovery
186 || !is_cpp_suppressed_parser_diagnostic(
187 diagnostic,
188 src,
189 &cpp_module_header_ranges,
190 source_line_count,
191 )
192 })
193 .map(|diagnostic| FormatDiagnostic {
194 line: diagnostic.line(),
195 column: diagnostic.column(),
196 category: diagnostic.category(),
197 message: diagnostic.message().to_string(),
198 })
199 .collect()
200}
201
202fn collect_lex_diagnostics(src: &str) -> Vec<FormatDiagnostic> {
203 SourceTokens::lex(src)
204 .lex_errors()
205 .iter()
206 .map(|error| FormatDiagnostic {
207 line: LineNumber::new(error.pos.line),
208 column: CharColumn::new(error.pos.column),
209 category: DiagnosticCategory::Lex,
210 message: error.to_string(),
211 })
212 .collect()
213}
214
215fn has_cpp_conditionals(src: &str) -> bool {
216 src.lines().any(|line| {
217 let line = line.trim_start();
218 line.starts_with("#if")
219 || line.starts_with("#elif")
220 || line.starts_with("#else")
221 || line.starts_with("#endif")
222 })
223}
224
225fn cpp_conditional_module_header_ranges(src: &str) -> Vec<std::ops::RangeInclusive<usize>> {
226 let mut ranges = Vec::new();
227 let mut cpp_depth = 0usize;
228 let mut module_header_start = None;
229
230 for (index, line) in src.lines().enumerate() {
231 let line_no = index + 1;
232 let trimmed = line.trim_start();
233
234 if let Some(start) = module_header_start {
235 if is_cpp_branch_boundary(trimmed) {
236 ranges.push(start..=line_no.saturating_sub(1));
237 module_header_start = None;
238 } else if module_header_ends(trimmed) {
239 ranges.push(start..=line_no);
240 module_header_start = None;
241 continue;
242 } else {
243 continue;
244 }
245 }
246
247 if trimmed.starts_with("#if") {
248 cpp_depth += 1;
249 continue;
250 }
251 if trimmed.starts_with("#endif") {
252 cpp_depth = cpp_depth.saturating_sub(1);
253 continue;
254 }
255
256 if cpp_depth > 0 && trimmed.starts_with("module ") {
257 if module_header_ends(trimmed) {
258 ranges.push(line_no..=line_no);
259 } else {
260 module_header_start = Some(line_no);
261 }
262 }
263 }
264
265 if let Some(start) = module_header_start {
266 let end = src.lines().count().max(start);
267 ranges.push(start..=end);
268 }
269
270 ranges
271}
272
273fn is_cpp_branch_boundary(line: &str) -> bool {
274 line.starts_with("#elif") || line.starts_with("#else") || line.starts_with("#endif")
275}
276
277fn module_header_ends(line: &str) -> bool {
278 line.split(|c: char| !(c.is_ascii_alphanumeric() || c == '_' || c == '\''))
279 .any(|token| token == "where")
280}
281
282fn is_cpp_suppressed_parser_diagnostic(
283 diagnostic: &daml_syntax::Diagnostic,
284 src: &str,
285 cpp_module_header_ranges: &[std::ops::RangeInclusive<usize>],
286 source_line_count: usize,
287) -> bool {
288 if diagnostic.category() == DiagnosticCategory::SkippedDecl {
289 let line = diagnostic.line().get();
290 return cpp_module_header_ranges
291 .iter()
292 .any(|range| range.contains(&line));
293 }
294 if diagnostic.category() == DiagnosticCategory::Malformed {
295 let line = diagnostic.line().get();
296 let in_cpp_module_header = cpp_module_header_ranges
297 .iter()
298 .any(|range| range.contains(&line));
299 let eof_recovery_artifact = line > source_line_count;
300 if in_cpp_module_header || eof_recovery_artifact {
301 return matches!(
302 diagnostic.message(),
303 "bad parameter pattern in 'module'" | "bad parameter pattern in 'where'"
304 );
305 }
306 let Some(source_line) = src.lines().nth(line.saturating_sub(1)) else {
307 return matches!(
308 diagnostic.message(),
309 "bad parameter pattern in 'module'" | "bad parameter pattern in 'where'"
310 );
311 };
312 match diagnostic.message() {
313 "bad parameter pattern in 'module'" => !looks_like_module_declaration(source_line),
314 "bad parameter pattern in 'where'" => !contains_where_keyword(source_line),
315 _ => false,
316 }
317 } else {
318 false
319 }
320}
321
322fn looks_like_module_declaration(line: &str) -> bool {
323 let trimmed = line.trim_start();
324 trimmed.starts_with("module ")
325}
326
327fn contains_where_keyword(line: &str) -> bool {
328 line.split(|c: char| !(c.is_ascii_alphanumeric() || c == '_' || c == '\''))
329 .any(|token| token == "where")
330}
331
332#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
338#[non_exhaustive]
339pub enum ImportOrder {
340 #[default]
342 Organize,
343 Preserve,
345}
346
347impl fmt::Display for ImportOrder {
348 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
349 match self {
350 Self::Organize => f.write_str("organize"),
351 Self::Preserve => f.write_str("preserve"),
352 }
353 }
354}
355
356#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
374pub struct FormatOptions {
375 import_order: ImportOrder,
376 rules: FormatRuleSet,
377}
378
379impl FormatOptions {
380 #[must_use]
385 pub const fn new() -> Self {
386 Self {
387 import_order: ImportOrder::Organize,
388 rules: FormatRuleSet::all(),
389 }
390 }
391
392 #[must_use]
401 pub const fn import_order(self) -> ImportOrder {
402 self.import_order
403 }
404
405 #[must_use]
407 pub const fn rules(self) -> FormatRuleSet {
408 self.rules
409 }
410
411 #[must_use]
413 pub const fn with_rules(mut self, rules: FormatRuleSet) -> Self {
414 self.rules = rules;
415 self
416 }
417
418 #[must_use]
423 pub const fn with_import_order(mut self, import_order: ImportOrder) -> Self {
424 self.import_order = import_order;
425 self
426 }
427}
428
429#[must_use]
436pub fn format_source(src: &str) -> String {
437 format_source_with_options(src, FormatOptions::default())
438}
439
440#[must_use]
445pub fn format_source_with_options(src: &str, options: FormatOptions) -> String {
446 layout_ast::format_ast(src, options)
447}
448
449pub fn try_format_source_with_options(
463 src: &str,
464 options: FormatOptions,
465) -> Result<String, FormatError> {
466 reject_source_diagnostics(src)?;
467 Ok(layout_ast::format_ast(src, options))
468}
469
470pub fn try_format_source(src: &str) -> Result<String, FormatError> {
488 try_format_source_with_options(src, FormatOptions::default())
489}
490
491#[derive(Debug, Clone, Copy, PartialEq, Eq)]
497pub struct FormatCoverage {
498 edit_candidates: usize,
499 modeled_constructs: usize,
500}
501
502impl FormatCoverage {
503 #[must_use]
505 pub const fn edit_candidates(self) -> usize {
506 self.edit_candidates
507 }
508
509 #[must_use]
511 pub const fn modeled_constructs(self) -> usize {
512 self.modeled_constructs
513 }
514}
515
516pub fn coverage(src: &str) -> Result<FormatCoverage, FormatError> {
523 reject_source_diagnostics(src)?;
524 Ok(layout_ast::coverage(src))
525}
526
527fn reject_source_diagnostics(src: &str) -> Result<(), FormatError> {
528 let diagnostics = source_diagnostics(src);
529 if diagnostics.is_empty() {
530 Ok(())
531 } else {
532 Err(FormatError { diagnostics })
533 }
534}
535
536pub(crate) fn normalize_gaps(src: &str, mode: ColonSpacingMode) -> String {
540 rewrite(src, mode)
541}
542
543#[derive(Debug, Clone, Copy, PartialEq, Eq)]
544enum ColonSpacingMode {
545 Canonical,
546 Preserve,
547}
548
549impl ColonSpacingMode {
550 const fn do_canonicalize_colons(&self) -> bool {
551 matches!(self, Self::Canonical)
552 }
553}
554
555#[derive(Debug, Clone, Copy)]
556struct GapTokenSpan {
557 start: usize,
558 end: usize,
559 brace_depth_delta: i32,
560 is_lone_colon: bool,
561 is_rparen: bool,
562 is_token: bool,
563}
564
565fn rewrite(src: &str, mode: ColonSpacingMode) -> String {
566 let source_tokens = SourceTokens::lex(src);
567
568 let mut items: Vec<GapTokenSpan> = source_tokens
571 .tokens()
572 .iter()
573 .filter(|t| {
574 !matches!(
575 t.kind(),
576 TokenKind::VLBrace | TokenKind::VRBrace | TokenKind::VSemi
577 )
578 })
579 .map(|t| GapTokenSpan {
580 start: t.start().get(),
581 end: t.end().get(),
582 brace_depth_delta: brace_delta(t.kind()),
583 is_lone_colon: is_lone_colon(t.kind()),
584 is_rparen: matches!(t.kind(), TokenKind::RParen),
585 is_token: true,
586 })
587 .chain(
588 source_tokens
589 .trivia()
590 .iter()
591 .filter(|t| !matches!(t.kind(), TriviaKind::BlankLines(_)))
592 .map(|t| GapTokenSpan {
593 start: t.start().get(),
594 end: t.end().get(),
595 brace_depth_delta: 0,
596 is_lone_colon: false,
597 is_rparen: false,
598 is_token: false,
599 }),
600 )
601 .collect();
602 items.sort_by_key(|item| item.start);
603
604 let mut out = String::with_capacity(src.len());
605 let mut prev = 0usize;
606 let mut brace_depth: i32 = 0;
607 let mut prev_was_rparen = false;
608 let mut prev_was_canon_colon = false;
612 for item in items {
613 let GapTokenSpan {
614 start,
615 end,
616 brace_depth_delta: delta,
617 is_lone_colon: is_colon,
618 is_rparen,
619 is_token,
620 } = item;
621 if start < prev {
622 return src.to_string(); }
624 let gap = &src[prev..start];
625 if !gap.chars().all(char::is_whitespace) {
626 return src.to_string(); }
628 let this_is_canon_colon =
633 mode.do_canonicalize_colons() && is_colon && brace_depth == 0 && !prev_was_rparen;
634 if this_is_canon_colon && !gap.is_empty() && !gap.contains('\n') {
635 } else if prev_was_canon_colon && !gap.is_empty() && !gap.contains('\n') {
637 out.push(' ');
639 } else {
640 out.push_str(&collapse_blank_lines(&strip_trailing_ws(gap)));
641 }
642 out.push_str(&src[start..end]);
643 prev = end;
644 brace_depth += delta;
645 prev_was_canon_colon = this_is_canon_colon;
646 if is_token {
647 prev_was_rparen = is_rparen; }
649 }
650 let tail = &src[prev..];
651 if !tail.chars().all(char::is_whitespace) {
652 return src.to_string();
653 }
654 out.push_str(&collapse_blank_lines(&strip_trailing_ws(tail)));
655
656 normalize_final_newline(&mut out);
657 out
658}
659
660fn is_lone_colon(t: &TokenKind) -> bool {
661 matches!(t, TokenKind::Op(op) if op.as_str() == ":")
662}
663
664const fn brace_delta(t: &TokenKind) -> i32 {
665 match t {
666 TokenKind::LBrace | TokenKind::LParen => 1,
667 TokenKind::RBrace | TokenKind::RParen => -1,
668 _ => 0,
669 }
670}
671
672fn strip_trailing_ws(gap: &str) -> String {
675 let mut out = String::with_capacity(gap.len());
676 let bytes = gap.as_bytes();
677 let mut i = 0usize;
678 while i < bytes.len() {
679 let b = bytes[i];
680 if b == b' ' || b == b'\t' {
681 let mut j = i + 1;
682 while j < bytes.len() && (bytes[j] == b' ' || bytes[j] == b'\t') {
683 j += 1;
684 }
685 if !matches!(bytes.get(j), Some(b'\n' | b'\r')) {
686 out.push_str(&gap[i..j]);
687 }
688 i = j;
689 } else {
690 let ch = gap[i..].chars().next().expect("non-empty gap substring");
691 let width = ch.len_utf8();
692 if width == 1 {
693 out.push(ch);
694 i += 1;
695 } else {
696 out.push_str(&gap[i..i + width]);
697 i += width;
698 }
699 }
700 }
701 out
702}
703
704fn collapse_blank_lines(gap: &str) -> String {
707 let mut out = String::with_capacity(gap.len());
708 let mut i = 0;
709 let mut newline_run = 0usize;
710 while i < gap.len() {
711 let rest = &gap[i..];
712 let (line_ending, width) = if rest.starts_with("\r\n") {
713 ("\r\n", 2)
714 } else if rest.starts_with('\n') {
715 ("\n", 1)
716 } else {
717 let ch = rest.chars().next().expect("non-empty rest");
718 out.push(ch);
719 newline_run = 0;
720 i += ch.len_utf8();
721 continue;
722 };
723 newline_run += 1;
724 if newline_run <= 2 {
725 out.push_str(line_ending);
726 }
727 i += width;
728 }
729 out
730}
731
732fn normalize_final_newline(out: &mut String) {
736 let trimmed_len = out.trim_end_matches(['\n', '\r', ' ', '\t']).len();
737 if trimmed_len == 0 {
738 return;
739 }
740 let crlf = out.contains("\r\n");
741 out.truncate(trimmed_len);
742 out.push_str(if crlf { "\r\n" } else { "\n" });
743}