pub mod config;
mod format_rules;
mod layout_ast;
use daml_parser::ast::DiagnosticCategory;
use daml_parser::lexer::{TokenKind, TriviaKind};
use daml_syntax::{CharColumn, LineNumber, SourceFile, SourceTokens};
pub use format_rules::{FormatRule, FormatRuleParseError, FormatRuleSet};
use std::error::Error;
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FormatDiagnostic {
line: LineNumber,
column: CharColumn,
category: DiagnosticCategory,
message: String,
}
impl FormatDiagnostic {
#[must_use]
pub const fn line(&self) -> LineNumber {
self.line
}
#[must_use]
pub const fn column(&self) -> CharColumn {
self.column
}
#[must_use]
pub const fn category(&self) -> DiagnosticCategory {
self.category
}
#[must_use]
pub fn message(&self) -> &str {
&self.message
}
}
impl fmt::Display for FormatDiagnostic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.category == DiagnosticCategory::Lex {
write!(f, "{}:{}: {}", self.line, self.column, self.message)
} else {
write!(
f,
"{}:{}: [{}] {}",
self.line, self.column, self.category, self.message
)
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FormatError {
diagnostics: Vec<FormatDiagnostic>,
}
impl FormatError {
#[must_use]
pub fn diagnostics(&self) -> &[FormatDiagnostic] {
&self.diagnostics
}
}
impl AsRef<[FormatDiagnostic]> for FormatError {
fn as_ref(&self) -> &[FormatDiagnostic] {
self.diagnostics()
}
}
impl fmt::Display for FormatError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (index, diagnostic) in self.diagnostics.iter().enumerate() {
if index > 0 {
f.write_str("\n")?;
}
diagnostic.fmt(f)?;
}
Ok(())
}
}
impl Error for FormatError {}
#[must_use]
pub fn lex_diagnostics(src: &str) -> Vec<FormatDiagnostic> {
collect_lex_diagnostics(src)
}
#[must_use]
pub fn source_diagnostics(src: &str) -> Vec<FormatDiagnostic> {
let cpp_module_header_ranges = cpp_conditional_module_header_ranges(src);
let suppress_cpp_recovery = has_cpp_conditionals(src);
let source_line_count = src.lines().count();
SourceFile::parse(src)
.diagnostics()
.iter()
.filter(|diagnostic| {
!suppress_cpp_recovery
|| !is_cpp_suppressed_parser_diagnostic(
diagnostic,
src,
&cpp_module_header_ranges,
source_line_count,
)
})
.map(|diagnostic| FormatDiagnostic {
line: diagnostic.line(),
column: diagnostic.column(),
category: diagnostic.category(),
message: diagnostic.message().to_string(),
})
.collect()
}
fn collect_lex_diagnostics(src: &str) -> Vec<FormatDiagnostic> {
SourceTokens::lex(src)
.lex_errors()
.iter()
.map(|error| FormatDiagnostic {
line: LineNumber::new(error.pos.line),
column: CharColumn::new(error.pos.column),
category: DiagnosticCategory::Lex,
message: error.to_string(),
})
.collect()
}
fn has_cpp_conditionals(src: &str) -> bool {
src.lines().any(|line| {
let line = line.trim_start();
line.starts_with("#if")
|| line.starts_with("#elif")
|| line.starts_with("#else")
|| line.starts_with("#endif")
})
}
fn cpp_conditional_module_header_ranges(src: &str) -> Vec<std::ops::RangeInclusive<usize>> {
let mut ranges = Vec::new();
let mut cpp_depth = 0usize;
let mut module_header_start = None;
for (index, line) in src.lines().enumerate() {
let line_no = index + 1;
let trimmed = line.trim_start();
if let Some(start) = module_header_start {
if is_cpp_branch_boundary(trimmed) {
ranges.push(start..=line_no.saturating_sub(1));
module_header_start = None;
} else if module_header_ends(trimmed) {
ranges.push(start..=line_no);
module_header_start = None;
continue;
} else {
continue;
}
}
if trimmed.starts_with("#if") {
cpp_depth += 1;
continue;
}
if trimmed.starts_with("#endif") {
cpp_depth = cpp_depth.saturating_sub(1);
continue;
}
if cpp_depth > 0 && trimmed.starts_with("module ") {
if module_header_ends(trimmed) {
ranges.push(line_no..=line_no);
} else {
module_header_start = Some(line_no);
}
}
}
if let Some(start) = module_header_start {
let end = src.lines().count().max(start);
ranges.push(start..=end);
}
ranges
}
fn is_cpp_branch_boundary(line: &str) -> bool {
line.starts_with("#elif") || line.starts_with("#else") || line.starts_with("#endif")
}
fn module_header_ends(line: &str) -> bool {
line.split(|c: char| !(c.is_ascii_alphanumeric() || c == '_' || c == '\''))
.any(|token| token == "where")
}
fn is_cpp_suppressed_parser_diagnostic(
diagnostic: &daml_syntax::Diagnostic,
src: &str,
cpp_module_header_ranges: &[std::ops::RangeInclusive<usize>],
source_line_count: usize,
) -> bool {
if diagnostic.category() == DiagnosticCategory::SkippedDecl {
let line = diagnostic.line().get();
return cpp_module_header_ranges
.iter()
.any(|range| range.contains(&line));
}
if diagnostic.category() == DiagnosticCategory::Malformed {
let line = diagnostic.line().get();
let in_cpp_module_header = cpp_module_header_ranges
.iter()
.any(|range| range.contains(&line));
let eof_recovery_artifact = line > source_line_count;
if in_cpp_module_header || eof_recovery_artifact {
return matches!(
diagnostic.message(),
"bad parameter pattern in 'module'" | "bad parameter pattern in 'where'"
);
}
let Some(source_line) = src.lines().nth(line.saturating_sub(1)) else {
return matches!(
diagnostic.message(),
"bad parameter pattern in 'module'" | "bad parameter pattern in 'where'"
);
};
match diagnostic.message() {
"bad parameter pattern in 'module'" => !looks_like_module_declaration(source_line),
"bad parameter pattern in 'where'" => !contains_where_keyword(source_line),
_ => false,
}
} else {
false
}
}
fn looks_like_module_declaration(line: &str) -> bool {
let trimmed = line.trim_start();
trimmed.starts_with("module ")
}
fn contains_where_keyword(line: &str) -> bool {
line.split(|c: char| !(c.is_ascii_alphanumeric() || c == '_' || c == '\''))
.any(|token| token == "where")
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ImportOrder {
#[default]
Organize,
Preserve,
}
impl fmt::Display for ImportOrder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Organize => f.write_str("organize"),
Self::Preserve => f.write_str("preserve"),
}
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct FormatOptions {
import_order: ImportOrder,
rules: FormatRuleSet,
}
impl FormatOptions {
#[must_use]
pub const fn new() -> Self {
Self {
import_order: ImportOrder::Organize,
rules: FormatRuleSet::all(),
}
}
#[must_use]
pub const fn import_order(self) -> ImportOrder {
self.import_order
}
#[must_use]
pub const fn rules(self) -> FormatRuleSet {
self.rules
}
#[must_use]
pub const fn with_rules(mut self, rules: FormatRuleSet) -> Self {
self.rules = rules;
self
}
#[must_use]
pub const fn with_import_order(mut self, import_order: ImportOrder) -> Self {
self.import_order = import_order;
self
}
}
#[must_use]
pub fn format_source(src: &str) -> String {
format_source_with_options(src, FormatOptions::default())
}
#[must_use]
pub fn format_source_with_options(src: &str, options: FormatOptions) -> String {
layout_ast::format_ast(src, options)
}
pub fn try_format_source_with_options(
src: &str,
options: FormatOptions,
) -> Result<String, FormatError> {
reject_source_diagnostics(src)?;
Ok(layout_ast::format_ast(src, options))
}
pub fn try_format_source(src: &str) -> Result<String, FormatError> {
try_format_source_with_options(src, FormatOptions::default())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FormatCoverage {
edit_candidates: usize,
modeled_constructs: usize,
}
impl FormatCoverage {
#[must_use]
pub const fn edit_candidates(self) -> usize {
self.edit_candidates
}
#[must_use]
pub const fn modeled_constructs(self) -> usize {
self.modeled_constructs
}
}
pub fn coverage(src: &str) -> Result<FormatCoverage, FormatError> {
reject_source_diagnostics(src)?;
Ok(layout_ast::coverage(src))
}
fn reject_source_diagnostics(src: &str) -> Result<(), FormatError> {
let diagnostics = source_diagnostics(src);
if diagnostics.is_empty() {
Ok(())
} else {
Err(FormatError { diagnostics })
}
}
pub(crate) fn normalize_gaps(src: &str, mode: ColonSpacingMode) -> String {
rewrite(src, mode)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ColonSpacingMode {
Canonical,
Preserve,
}
impl ColonSpacingMode {
const fn do_canonicalize_colons(&self) -> bool {
matches!(self, Self::Canonical)
}
}
#[derive(Debug, Clone, Copy)]
struct GapTokenSpan {
start: usize,
end: usize,
brace_depth_delta: i32,
is_lone_colon: bool,
is_rparen: bool,
is_token: bool,
}
fn rewrite(src: &str, mode: ColonSpacingMode) -> String {
let source_tokens = SourceTokens::lex(src);
let mut items: Vec<GapTokenSpan> = source_tokens
.tokens()
.iter()
.filter(|t| {
!matches!(
t.kind(),
TokenKind::VLBrace | TokenKind::VRBrace | TokenKind::VSemi
)
})
.map(|t| GapTokenSpan {
start: t.start().get(),
end: t.end().get(),
brace_depth_delta: brace_delta(t.kind()),
is_lone_colon: is_lone_colon(t.kind()),
is_rparen: matches!(t.kind(), TokenKind::RParen),
is_token: true,
})
.chain(
source_tokens
.trivia()
.iter()
.filter(|t| !matches!(t.kind(), TriviaKind::BlankLines(_)))
.map(|t| GapTokenSpan {
start: t.start().get(),
end: t.end().get(),
brace_depth_delta: 0,
is_lone_colon: false,
is_rparen: false,
is_token: false,
}),
)
.collect();
items.sort_by_key(|item| item.start);
let mut out = String::with_capacity(src.len());
let mut prev = 0usize;
let mut brace_depth: i32 = 0;
let mut prev_was_rparen = false;
let mut prev_was_canon_colon = false;
for item in items {
let GapTokenSpan {
start,
end,
brace_depth_delta: delta,
is_lone_colon: is_colon,
is_rparen,
is_token,
} = item;
if start < prev {
return src.to_string(); }
let gap = &src[prev..start];
if !gap.chars().all(char::is_whitespace) {
return src.to_string(); }
let this_is_canon_colon =
mode.do_canonicalize_colons() && is_colon && brace_depth == 0 && !prev_was_rparen;
if this_is_canon_colon && !gap.is_empty() && !gap.contains('\n') {
} else if prev_was_canon_colon && !gap.is_empty() && !gap.contains('\n') {
out.push(' ');
} else {
out.push_str(&collapse_blank_lines(&strip_trailing_ws(gap)));
}
out.push_str(&src[start..end]);
prev = end;
brace_depth += delta;
prev_was_canon_colon = this_is_canon_colon;
if is_token {
prev_was_rparen = is_rparen; }
}
let tail = &src[prev..];
if !tail.chars().all(char::is_whitespace) {
return src.to_string();
}
out.push_str(&collapse_blank_lines(&strip_trailing_ws(tail)));
normalize_final_newline(&mut out);
out
}
fn is_lone_colon(t: &TokenKind) -> bool {
matches!(t, TokenKind::Op(op) if op.as_str() == ":")
}
const fn brace_delta(t: &TokenKind) -> i32 {
match t {
TokenKind::LBrace | TokenKind::LParen => 1,
TokenKind::RBrace | TokenKind::RParen => -1,
_ => 0,
}
}
fn strip_trailing_ws(gap: &str) -> String {
let mut out = String::with_capacity(gap.len());
let bytes = gap.as_bytes();
let mut i = 0usize;
while i < bytes.len() {
let b = bytes[i];
if b == b' ' || b == b'\t' {
let mut j = i + 1;
while j < bytes.len() && (bytes[j] == b' ' || bytes[j] == b'\t') {
j += 1;
}
if !matches!(bytes.get(j), Some(b'\n' | b'\r')) {
out.push_str(&gap[i..j]);
}
i = j;
} else {
let ch = gap[i..].chars().next().expect("non-empty gap substring");
let width = ch.len_utf8();
if width == 1 {
out.push(ch);
i += 1;
} else {
out.push_str(&gap[i..i + width]);
i += width;
}
}
}
out
}
fn collapse_blank_lines(gap: &str) -> String {
let mut out = String::with_capacity(gap.len());
let mut i = 0;
let mut newline_run = 0usize;
while i < gap.len() {
let rest = &gap[i..];
let (line_ending, width) = if rest.starts_with("\r\n") {
("\r\n", 2)
} else if rest.starts_with('\n') {
("\n", 1)
} else {
let ch = rest.chars().next().expect("non-empty rest");
out.push(ch);
newline_run = 0;
i += ch.len_utf8();
continue;
};
newline_run += 1;
if newline_run <= 2 {
out.push_str(line_ending);
}
i += width;
}
out
}
fn normalize_final_newline(out: &mut String) {
let trimmed_len = out.trim_end_matches(['\n', '\r', ' ', '\t']).len();
if trimmed_len == 0 {
return;
}
let crlf = out.contains("\r\n");
out.truncate(trimmed_len);
out.push_str(if crlf { "\r\n" } else { "\n" });
}