use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::process;
use aver::ast::TopLevel;
use aver::lexer::Lexer;
use aver::parser::Parser;
use aver::types::{Type, parse_type_str_strict};
use colored::Colorize;
#[allow(dead_code)]
pub(super) fn cmd_format(path: &str, check: bool) {
let root = Path::new(path);
let mut files = Vec::new();
if let Err(e) = collect_av_files(root, &mut files) {
eprintln!("{}", e.red());
process::exit(1);
}
files.sort();
if files.is_empty() {
eprintln!(
"{}",
format!("No .av files found under '{}'", root.display()).red()
);
process::exit(1);
}
let mut changed = Vec::new();
for file in &files {
let src = match fs::read_to_string(file) {
Ok(s) => s,
Err(e) => {
eprintln!(
"{}",
format!("Cannot read '{}': {}", file.display(), e).red()
);
process::exit(1);
}
};
let formatted = match try_format_source(&src) {
Ok(s) => s,
Err(e) => {
eprintln!(
"{}",
format!("Cannot format '{}': {}", file.display(), e).red()
);
process::exit(1);
}
};
if formatted != src {
changed.push(file.clone());
if !check && let Err(e) = fs::write(file, formatted) {
eprintln!(
"{}",
format!("Cannot write '{}': {}", file.display(), e).red()
);
process::exit(1);
}
}
}
if check {
if changed.is_empty() {
println!("{}", "Format check passed".green());
return;
}
println!("{}", "Format check failed".red());
println!("Files that need formatting:");
for f in &changed {
println!(" {}", f.display());
}
process::exit(1);
}
if changed.is_empty() {
println!("{}", "Already formatted".green());
} else {
for f in &changed {
println!("{} {}", "formatted".green(), f.display());
}
println!("{}", format!("Formatted {} file(s)", changed.len()).green());
}
}
#[allow(dead_code)]
fn collect_av_files(path: &Path, out: &mut Vec<PathBuf>) -> Result<(), String> {
if !path.exists() {
return Err(format!("Path '{}' does not exist", path.display()));
}
if path.is_file() {
if is_av_file(path) {
out.push(path.to_path_buf());
return Ok(());
}
return Err(format!("'{}' is not an .av file", path.display()));
}
let entries = fs::read_dir(path)
.map_err(|e| format!("Cannot read directory '{}': {}", path.display(), e))?;
for entry_res in entries {
let entry = entry_res
.map_err(|e| format!("Cannot read directory entry in '{}': {}", path.display(), e))?;
let p = entry.path();
if p.is_dir() {
collect_av_files(&p, out)?;
} else if is_av_file(&p) {
out.push(p);
}
}
Ok(())
}
#[allow(dead_code)]
fn is_av_file(path: &Path) -> bool {
path.extension().and_then(|e| e.to_str()) == Some("av")
}
fn normalize_leading_indent(line: &str) -> String {
let mut end = 0usize;
for (idx, ch) in line.char_indices() {
if ch == ' ' || ch == '\t' {
end = idx + ch.len_utf8();
} else {
break;
}
}
let (indent, rest) = line.split_at(end);
if rest.is_empty() {
return String::new();
}
let mut out = String::new();
for ch in indent.chars() {
if ch == '\t' {
out.push_str(" ");
} else {
out.push(ch);
}
}
out.push_str(rest);
out
}
fn effect_namespace(effect: &str) -> &str {
match effect.split_once('.') {
Some((namespace, _)) => namespace,
None => effect,
}
}
fn sorted_effects(effects: &[String]) -> Vec<String> {
let mut sorted = effects.to_vec();
sorted.sort();
sorted
}
fn format_block_effect_declaration(indent: &str, effects: &[String]) -> Vec<String> {
let effects = sorted_effects(effects);
let inline = format!("{}! [{}]", indent, effects.join(", "));
if inline.len() <= 100 {
return vec![inline];
}
let mut out = vec![format!("{}! [", indent)];
let mut start = 0usize;
while start < effects.len() {
let namespace = effect_namespace(&effects[start]);
let mut end = start + 1;
while end < effects.len() && effect_namespace(&effects[end]) == namespace {
end += 1;
}
out.push(format!("{} {},", indent, effects[start..end].join(", ")));
start = end;
}
out.push(format!("{}]", indent));
out
}
fn split_top_level(src: &str, delimiter: char) -> Option<Vec<String>> {
let mut parts = Vec::new();
let mut start = 0usize;
let mut paren_depth = 0usize;
let mut bracket_depth = 0usize;
let mut angle_depth = 0usize;
let mut prev = None;
for (idx, ch) in src.char_indices() {
match ch {
'(' => paren_depth += 1,
')' => paren_depth = paren_depth.checked_sub(1)?,
'[' => bracket_depth += 1,
']' => bracket_depth = bracket_depth.checked_sub(1)?,
'<' => angle_depth += 1,
'>' if prev != Some('-') && angle_depth > 0 => angle_depth -= 1,
_ => {}
}
if ch == delimiter && paren_depth == 0 && bracket_depth == 0 && angle_depth == 0 {
parts.push(src[start..idx].to_string());
start = idx + ch.len_utf8();
}
prev = Some(ch);
}
if paren_depth != 0 || bracket_depth != 0 || angle_depth != 0 {
return None;
}
parts.push(src[start..].to_string());
Some(parts)
}
fn find_matching_paren(src: &str, open_idx: usize) -> Option<usize> {
let mut depth = 0usize;
for (idx, ch) in src.char_indices().skip_while(|(idx, _)| *idx < open_idx) {
match ch {
'(' => depth += 1,
')' => {
depth = depth.checked_sub(1)?;
if depth == 0 {
return Some(idx);
}
}
_ => {}
}
}
None
}
fn format_type_for_source(ty: &Type) -> String {
match ty {
Type::Int => "Int".to_string(),
Type::Float => "Float".to_string(),
Type::Str => "String".to_string(),
Type::Bool => "Bool".to_string(),
Type::Unit => "Unit".to_string(),
Type::Result(ok, err) => format!(
"Result<{}, {}>",
format_type_for_source(ok),
format_type_for_source(err)
),
Type::Option(inner) => format!("Option<{}>", format_type_for_source(inner)),
Type::List(inner) => format!("List<{}>", format_type_for_source(inner)),
Type::Vector(inner) => format!("Vector<{}>", format_type_for_source(inner)),
Type::Tuple(items) => format!(
"({})",
items
.iter()
.map(format_type_for_source)
.collect::<Vec<_>>()
.join(", ")
),
Type::Map(key, value) => format!(
"Map<{}, {}>",
format_type_for_source(key),
format_type_for_source(value)
),
Type::Fn(params, ret, effects) => {
let params = params
.iter()
.map(format_type_for_source)
.collect::<Vec<_>>()
.join(", ");
let ret = format_type_for_source(ret);
let effects = sorted_effects(effects);
if effects.is_empty() {
format!("Fn({params}) -> {ret}")
} else {
format!("Fn({params}) -> {ret} ! [{}]", effects.join(", "))
}
}
Type::Unknown => "Unknown".to_string(),
Type::Named(name) => name.clone(),
}
}
fn normalize_type_annotation(type_src: &str) -> String {
let trimmed = type_src.trim();
match parse_type_str_strict(trimmed) {
Ok(ty) => format_type_for_source(&ty),
Err(_) => trimmed.to_string(),
}
}
fn normalize_function_header_effects_line(line: &str) -> String {
let indent_len = line.chars().take_while(|c| *c == ' ').count();
let indent = " ".repeat(indent_len);
let trimmed = line.trim();
if !trimmed.starts_with("fn ") {
return line.to_string();
}
let open_idx = match trimmed.find('(') {
Some(idx) => idx,
None => return line.to_string(),
};
let close_idx = match find_matching_paren(trimmed, open_idx) {
Some(idx) => idx,
None => return line.to_string(),
};
let params_src = &trimmed[open_idx + 1..close_idx];
let params = match split_top_level(params_src, ',') {
Some(parts) => parts,
None => return line.to_string(),
};
let formatted_params = params
.into_iter()
.filter(|part| !part.trim().is_empty())
.map(|param| {
let (name, ty) = match param.split_once(':') {
Some(parts) => parts,
None => return param.trim().to_string(),
};
format!("{}: {}", name.trim(), normalize_type_annotation(ty))
})
.collect::<Vec<_>>()
.join(", ");
let mut formatted = format!(
"{}{}{})",
indent,
&trimmed[..open_idx + 1],
formatted_params
);
let remainder = trimmed[close_idx + 1..].trim();
if let Some(return_type) = remainder.strip_prefix("->") {
formatted.push_str(" -> ");
formatted.push_str(&normalize_type_annotation(return_type));
} else if !remainder.is_empty() {
formatted.push(' ');
formatted.push_str(remainder);
}
formatted
}
fn normalize_function_header_effects(lines: Vec<String>) -> Vec<String> {
lines
.into_iter()
.map(|line| normalize_function_header_effects_line(&line))
.collect()
}
fn normalize_effect_declaration_blocks(lines: Vec<String>) -> Vec<String> {
let mut out = Vec::with_capacity(lines.len());
let mut i = 0usize;
while i < lines.len() {
let line = &lines[i];
let trimmed = line.trim();
if !trimmed.starts_with("! [") {
out.push(line.clone());
i += 1;
continue;
}
let indent_len = line.chars().take_while(|c| *c == ' ').count();
let indent = " ".repeat(indent_len);
let mut inner = String::new();
let mut consumed = 0usize;
let mut found_close = false;
while i + consumed < lines.len() {
let current = &lines[i + consumed];
let current_trimmed = current.trim();
let segment = if consumed == 0 {
current_trimmed.trim_start_matches("! [")
} else {
current_trimmed
};
if let Some(before_close) = segment.strip_suffix(']') {
if !inner.is_empty() && !before_close.trim().is_empty() {
inner.push(' ');
}
inner.push_str(before_close.trim());
found_close = true;
consumed += 1;
break;
}
if !inner.is_empty() && !segment.trim().is_empty() {
inner.push(' ');
}
inner.push_str(segment.trim());
consumed += 1;
}
if !found_close {
out.push(line.clone());
i += 1;
continue;
}
let effects: Vec<String> = if inner.trim().is_empty() {
vec![]
} else {
inner
.split(',')
.map(str::trim)
.filter(|part| !part.is_empty())
.map(ToString::to_string)
.collect()
};
out.extend(format_block_effect_declaration(&indent, &effects));
i += consumed;
}
out
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum BlockKind {
Fn(String),
Verify(String),
Other,
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct TopBlock {
text: String,
kind: BlockKind,
start_line: usize,
}
#[derive(Default)]
struct FormatAstInfo {
kind_by_line: HashMap<usize, BlockKind>,
}
fn classify_block(header_line: &str) -> BlockKind {
let trimmed = header_line.trim();
if let Some(rest) = trimmed.strip_prefix("fn ") {
let name = rest
.split(['(', ' ', '\t'])
.next()
.unwrap_or_default()
.to_string();
if !name.is_empty() {
return BlockKind::Fn(name);
}
}
if let Some(rest) = trimmed.strip_prefix("verify ") {
let name = rest
.split([' ', '\t'])
.next()
.unwrap_or_default()
.to_string();
if !name.is_empty() {
return BlockKind::Verify(name);
}
}
BlockKind::Other
}
fn is_top_level_start(line: &str) -> bool {
if line.is_empty() {
return false;
}
if line.starts_with(' ') || line.starts_with('\t') {
return false;
}
!line.trim_start().starts_with("//")
}
fn split_top_level_blocks(lines: &[String], ast_info: Option<&FormatAstInfo>) -> Vec<TopBlock> {
if lines.is_empty() {
return Vec::new();
}
let starts: Vec<usize> = lines
.iter()
.enumerate()
.filter_map(|(idx, line)| is_top_level_start(line).then_some(idx))
.collect();
if starts.is_empty() {
let text = lines.join("\n").trim_end_matches('\n').to_string();
if text.is_empty() {
return Vec::new();
}
return vec![TopBlock {
text,
kind: BlockKind::Other,
start_line: 1,
}];
}
let mut blocks = Vec::new();
let first = starts[0];
if first > 0 {
let mut pre = lines[..first].to_vec();
while pre.last().is_some_and(|l| l.is_empty()) {
pre.pop();
}
if !pre.is_empty() {
blocks.push(TopBlock {
text: pre.join("\n"),
kind: BlockKind::Other,
start_line: 1,
});
}
}
for (i, start) in starts.iter().enumerate() {
let end = starts.get(i + 1).copied().unwrap_or(lines.len());
let mut segment = lines[*start..end].to_vec();
while segment.last().is_some_and(|l| l.is_empty()) {
segment.pop();
}
if segment.is_empty() {
continue;
}
let header = segment[0].clone();
let start_line = *start + 1;
let kind = ast_info
.and_then(|info| info.kind_by_line.get(&start_line).cloned())
.unwrap_or_else(|| classify_block(&header));
blocks.push(TopBlock {
text: segment.join("\n"),
kind,
start_line,
});
}
blocks
}
fn reorder_verify_blocks(blocks: Vec<TopBlock>) -> Vec<TopBlock> {
let verify_blocks: Vec<TopBlock> = blocks
.iter()
.filter(|b| matches!(b.kind, BlockKind::Verify(_)))
.cloned()
.collect();
if verify_blocks.is_empty() {
return blocks;
}
let mut by_fn: HashMap<String, Vec<usize>> = HashMap::new();
for (idx, block) in verify_blocks.iter().enumerate() {
if let BlockKind::Verify(name) = &block.kind {
by_fn.entry(name.clone()).or_default().push(idx);
}
}
let mut used = vec![false; verify_blocks.len()];
let mut out = Vec::new();
for block in blocks {
match block.kind.clone() {
BlockKind::Verify(_) => {}
BlockKind::Fn(name) => {
out.push(block);
if let Some(indices) = by_fn.remove(&name) {
for idx in indices {
used[idx] = true;
out.push(verify_blocks[idx].clone());
}
}
}
BlockKind::Other => out.push(block),
}
}
for (idx, block) in verify_blocks.iter().enumerate() {
if !used[idx] {
out.push(block.clone());
}
}
out
}
fn parse_ast_info_checked(source: &str) -> Result<FormatAstInfo, String> {
let mut lexer = Lexer::new(source);
let tokens = lexer.tokenize().map_err(|e| e.to_string())?;
let mut parser = Parser::new(tokens);
let items = parser.parse().map_err(|e| e.to_string())?;
let mut info = FormatAstInfo::default();
for item in items {
match item {
TopLevel::FnDef(fd) => {
info.kind_by_line
.insert(fd.line, BlockKind::Fn(fd.name.clone()));
}
TopLevel::Verify(vb) => {
info.kind_by_line
.insert(vb.line, BlockKind::Verify(vb.fn_name.clone()));
}
_ => {}
}
}
Ok(info)
}
fn normalize_source_lines(source: &str) -> Vec<String> {
let normalized = source.replace("\r\n", "\n").replace('\r', "\n");
let mut lines = Vec::new();
for raw in normalized.split('\n') {
let trimmed = raw.trim_end_matches([' ', '\t']);
let line = normalize_leading_indent(trimmed);
lines.push(line);
}
let lines = normalize_effect_declaration_blocks(lines);
let lines = normalize_function_header_effects(lines);
let lines = normalize_module_intent_blocks(lines);
normalize_inline_decision_fields(lines)
}
fn normalize_module_intent_blocks(lines: Vec<String>) -> Vec<String> {
let mut out = Vec::with_capacity(lines.len());
let mut in_module_header = false;
let mut i = 0usize;
while i < lines.len() {
let line = &lines[i];
let trimmed = line.trim();
let indent = line.chars().take_while(|c| *c == ' ').count();
if indent == 0 && trimmed.starts_with("module ") {
in_module_header = true;
out.push(line.clone());
i += 1;
continue;
}
if in_module_header && indent == 0 && !trimmed.is_empty() && !trimmed.starts_with("//") {
in_module_header = false;
}
if in_module_header && indent > 0 {
let head = &line[indent..];
if let Some(rhs) = head.strip_prefix("intent =") {
let rhs_trimmed = rhs.trim_start();
if rhs_trimmed.starts_with('"') {
let mut parts = vec![rhs_trimmed.to_string()];
let mut consumed = 1usize;
while i + consumed < lines.len() {
let next = &lines[i + consumed];
let next_indent = next.chars().take_while(|c| *c == ' ').count();
let next_trimmed = next.trim();
if next_indent <= indent || next_trimmed.is_empty() {
break;
}
if !next_trimmed.starts_with('"') {
break;
}
parts.push(next_trimmed.to_string());
consumed += 1;
}
if parts.len() > 1 {
out.push(format!("{}intent =", " ".repeat(indent)));
for part in parts {
out.push(format!("{}{}", " ".repeat(indent + 4), part));
}
i += consumed;
continue;
}
}
}
}
out.push(line.clone());
i += 1;
}
out
}
fn normalize_internal_blank_runs(text: &str) -> String {
let mut out = Vec::new();
let mut blank_run = 0usize;
for raw in text.split('\n') {
if raw.is_empty() {
blank_run += 1;
if blank_run <= 2 {
out.push(String::new());
}
} else {
blank_run = 0;
out.push(raw.to_string());
}
}
while out.first().is_some_and(|l| l.is_empty()) {
out.remove(0);
}
while out.last().is_some_and(|l| l.is_empty()) {
out.pop();
}
out.join("\n")
}
const DECISION_FIELDS: [&str; 6] = ["date", "author", "reason", "chosen", "rejected", "impacts"];
fn starts_with_decision_field(content: &str) -> bool {
DECISION_FIELDS
.iter()
.any(|field| content.starts_with(&format!("{field} =")))
}
fn find_next_decision_field_boundary(s: &str) -> Option<usize> {
let mut best: Option<usize> = None;
for field in DECISION_FIELDS {
let needle = format!(" {field} =");
let mut search_from = 0usize;
while let Some(rel) = s[search_from..].find(&needle) {
let idx = search_from + rel;
let spaces_before = s[..idx].chars().rev().take_while(|c| *c == ' ').count();
let total_separator_spaces = spaces_before + 1;
if total_separator_spaces >= 2 {
let field_start = idx + 1;
best = Some(best.map_or(field_start, |cur| cur.min(field_start)));
break;
}
search_from = idx + 1;
}
}
best
}
fn split_inline_decision_fields(content: &str) -> Vec<String> {
if !starts_with_decision_field(content) {
return vec![content.to_string()];
}
let mut out = Vec::new();
let mut rest = content.trim_end().to_string();
while let Some(idx) = find_next_decision_field_boundary(&rest) {
let left = rest[..idx].trim_end().to_string();
if left.is_empty() {
break;
}
out.push(left);
rest = rest[idx..].trim_start().to_string();
}
if !rest.is_empty() {
out.push(rest.trim_end().to_string());
}
if out.is_empty() {
vec![content.to_string()]
} else {
out
}
}
fn normalize_inline_decision_fields(lines: Vec<String>) -> Vec<String> {
let mut out = Vec::with_capacity(lines.len());
let mut in_decision = false;
for line in lines {
let trimmed = line.trim();
let indent = line.chars().take_while(|c| *c == ' ').count();
if indent == 0 && trimmed.starts_with("decision ") {
in_decision = true;
out.push(line);
continue;
}
if in_decision && indent == 0 && !trimmed.is_empty() && !trimmed.starts_with("//") {
in_decision = false;
}
if in_decision && trimmed.is_empty() {
continue;
}
if in_decision && indent > 0 {
let content = &line[indent..];
let parts = split_inline_decision_fields(content);
if parts.len() > 1 {
for part in parts {
out.push(format!("{}{}", " ".repeat(indent), part));
}
continue;
}
}
out.push(line);
}
out
}
pub fn try_format_source(source: &str) -> Result<String, String> {
let lines = normalize_source_lines(source);
let normalized = lines.join("\n");
let ast_info = parse_ast_info_checked(&normalized)?;
let blocks = split_top_level_blocks(&lines, Some(&ast_info));
let reordered = reorder_verify_blocks(blocks);
let mut non_empty_blocks = Vec::new();
for block in reordered {
let text = normalize_internal_blank_runs(&block.text);
let text = text.trim_matches('\n').to_string();
if !text.is_empty() {
non_empty_blocks.push(text);
}
}
if non_empty_blocks.is_empty() {
return Ok("\n".to_string());
}
let mut out = non_empty_blocks.join("\n\n");
out.push('\n');
Ok(out)
}
#[cfg(test)]
pub fn format_source(source: &str) -> String {
match try_format_source(source) {
Ok(formatted) => formatted,
Err(err) => panic!("format_source received invalid Aver source: {err}"),
}
}
#[cfg(test)]
mod tests {
use super::{format_source, try_format_source};
#[test]
fn normalizes_line_endings_and_trailing_ws() {
let src = "module A\r\n fn x() -> Int \r\n 1\t \r\n";
let got = format_source(src);
assert_eq!(got, "module A\n fn x() -> Int\n 1\n");
}
#[test]
fn converts_leading_tabs_only() {
let src = "\tfn x() -> String\n\t\t\"a\\tb\"\n";
let got = format_source(src);
assert_eq!(got, " fn x() -> String\n \"a\\tb\"\n");
}
#[test]
fn collapses_long_blank_runs() {
let src = "module A\n\n\n\nfn x() -> Int\n 1\n";
let got = format_source(src);
assert_eq!(got, "module A\n\nfn x() -> Int\n 1\n");
}
#[test]
fn keeps_single_final_newline() {
let src = "module A\nfn x() -> Int\n 1\n\n\n";
let got = format_source(src);
assert_eq!(got, "module A\n\nfn x() -> Int\n 1\n");
}
#[test]
fn rejects_removed_eq_expr_syntax() {
let src = "fn x() -> Int\n = 1\n";
let err = try_format_source(src).expect_err("old '= expr' syntax should fail");
assert!(
err.contains("no longer use '= expr'"),
"unexpected error: {}",
err
);
}
#[test]
fn moves_verify_directly_under_function() {
let src = r#"module Demo
fn a(x: Int) -> Int
x + 1
fn b(x: Int) -> Int
x + 2
verify a
a(1) => 2
verify b
b(1) => 3
"#;
let got = format_source(src);
assert_eq!(
got,
r#"module Demo
fn a(x: Int) -> Int
x + 1
verify a
a(1) => 2
fn b(x: Int) -> Int
x + 2
verify b
b(1) => 3
"#
);
}
#[test]
fn leaves_orphan_verify_at_end() {
let src = r#"module Demo
verify missing
missing(1) => 2
"#;
let got = format_source(src);
assert_eq!(
got,
r#"module Demo
verify missing
missing(1) => 2
"#
);
}
#[test]
fn keeps_inline_module_intent_inline() {
let src = r#"module Demo
intent = "Inline intent."
exposes [x]
fn x() -> Int
1
"#;
let got = format_source(src);
assert_eq!(
got,
r#"module Demo
intent = "Inline intent."
exposes [x]
fn x() -> Int
1
"#
);
}
#[test]
fn expands_multiline_module_intent_to_block() {
let src = r#"module Demo
intent = "First line."
"Second line."
exposes [x]
fn x() -> Int
1
"#;
let got = format_source(src);
assert_eq!(
got,
r#"module Demo
intent =
"First line."
"Second line."
exposes [x]
fn x() -> Int
1
"#
);
}
#[test]
fn splits_inline_decision_fields_to_separate_lines() {
let src = r#"module Demo
intent = "x"
exposes [main]
decision D
date = "2026-03-02"
chosen = "A" rejected = ["B"]
impacts = [main]
"#;
let got = format_source(src);
assert_eq!(
got,
r#"module Demo
intent = "x"
exposes [main]
decision D
date = "2026-03-02"
chosen = "A"
rejected = ["B"]
impacts = [main]
"#
);
}
#[test]
fn keeps_inline_function_description_inline() {
let src = r#"fn add(a: Int, b: Int) -> Int
? "Adds two numbers."
a + b
"#;
let got = format_source(src);
assert_eq!(
got,
r#"fn add(a: Int, b: Int) -> Int
? "Adds two numbers."
a + b
"#
);
}
#[test]
fn keeps_short_effect_lists_inline() {
let src = r#"fn apply(f: Fn(Int) -> Int ! [Console.warn, Console.print], x: Int) -> Int
! [Http.post, Console.print, Http.get, Console.warn]
f(x)
"#;
let got = format_source(src);
assert_eq!(
got,
r#"fn apply(f: Fn(Int) -> Int ! [Console.print, Console.warn], x: Int) -> Int
! [Console.print, Console.warn, Http.get, Http.post]
f(x)
"#
);
}
#[test]
fn keeps_medium_effect_lists_inline_when_they_fit() {
let src = r#"fn run() -> Unit
! [Args, Console, Disk, Http, Random, Tcp, Terminal, Time]
Unit
"#;
let got = format_source(src);
assert_eq!(
got,
r#"fn run() -> Unit
! [Args, Console, Disk, Http, Random, Tcp, Terminal, Time]
Unit
"#
);
}
#[test]
fn expands_long_effect_lists_to_multiline_alphabetical_groups() {
let src = r#"fn main() -> Unit
! [Args.get, Console.print, Console.warn, Time.now, Disk.makeDir, Disk.exists, Disk.readText, Disk.writeText, Disk.appendText]
Unit
"#;
let got = format_source(src);
assert_eq!(
got,
r#"fn main() -> Unit
! [
Args.get,
Console.print, Console.warn,
Disk.appendText, Disk.exists, Disk.makeDir, Disk.readText, Disk.writeText,
Time.now,
]
Unit
"#
);
}
#[test]
fn sorts_function_type_effects_inline() {
let src = r#"fn useHandler(handler: Fn(Int) -> Result<String, String> ! [Time.now, Args.get, Console.warn, Console.print, Disk.readText], value: Int) -> Unit
handler(value)
"#;
let got = format_source(src);
assert_eq!(
got,
r#"fn useHandler(handler: Fn(Int) -> Result<String, String> ! [Args.get, Console.print, Console.warn, Disk.readText, Time.now], value: Int) -> Unit
handler(value)
"#
);
}
#[test]
fn keeps_long_function_type_effects_inline() {
let src = r#"fn apply(handler: Fn(Int) -> Int ! [Time.now, Args.get, Console.warn, Console.print, Disk.readText], value: Int) -> Int
handler(value)
"#;
let got = format_source(src);
assert_eq!(
got,
r#"fn apply(handler: Fn(Int) -> Int ! [Args.get, Console.print, Console.warn, Disk.readText, Time.now], value: Int) -> Int
handler(value)
"#
);
}
}