Skip to main content

aver/main/
format_cmd.rs

1use std::collections::HashMap;
2use std::fs;
3use std::path::{Path, PathBuf};
4use std::process;
5
6use aver::ast::TopLevel;
7use aver::lexer::Lexer;
8use aver::parser::Parser;
9use aver::types::{Type, parse_type_str_strict};
10use colored::Colorize;
11
12#[allow(dead_code)]
13pub(super) fn cmd_format(path: &str, check: bool) {
14    let root = Path::new(path);
15    let mut files = Vec::new();
16    if let Err(e) = collect_av_files(root, &mut files) {
17        eprintln!("{}", e.red());
18        process::exit(1);
19    }
20    files.sort();
21
22    if files.is_empty() {
23        eprintln!(
24            "{}",
25            format!("No .av files found under '{}'", root.display()).red()
26        );
27        process::exit(1);
28    }
29
30    let mut changed = Vec::new();
31    for file in &files {
32        let src = match fs::read_to_string(file) {
33            Ok(s) => s,
34            Err(e) => {
35                eprintln!(
36                    "{}",
37                    format!("Cannot read '{}': {}", file.display(), e).red()
38                );
39                process::exit(1);
40            }
41        };
42        let formatted = match try_format_source(&src) {
43            Ok(s) => s,
44            Err(e) => {
45                eprintln!(
46                    "{}",
47                    format!("Cannot format '{}': {}", file.display(), e).red()
48                );
49                process::exit(1);
50            }
51        };
52        if formatted != src {
53            changed.push(file.clone());
54            if !check && let Err(e) = fs::write(file, formatted) {
55                eprintln!(
56                    "{}",
57                    format!("Cannot write '{}': {}", file.display(), e).red()
58                );
59                process::exit(1);
60            }
61        }
62    }
63
64    if check {
65        if changed.is_empty() {
66            println!("{}", "Format check passed".green());
67            return;
68        }
69        println!("{}", "Format check failed".red());
70        println!("Files that need formatting:");
71        for f in &changed {
72            println!("  {}", f.display());
73        }
74        process::exit(1);
75    }
76
77    if changed.is_empty() {
78        println!("{}", "Already formatted".green());
79    } else {
80        for f in &changed {
81            println!("{} {}", "formatted".green(), f.display());
82        }
83        println!("{}", format!("Formatted {} file(s)", changed.len()).green());
84    }
85}
86
87#[allow(dead_code)]
88fn collect_av_files(path: &Path, out: &mut Vec<PathBuf>) -> Result<(), String> {
89    if !path.exists() {
90        return Err(format!("Path '{}' does not exist", path.display()));
91    }
92
93    if path.is_file() {
94        if is_av_file(path) {
95            out.push(path.to_path_buf());
96            return Ok(());
97        }
98        return Err(format!("'{}' is not an .av file", path.display()));
99    }
100
101    let entries = fs::read_dir(path)
102        .map_err(|e| format!("Cannot read directory '{}': {}", path.display(), e))?;
103    for entry_res in entries {
104        let entry = entry_res
105            .map_err(|e| format!("Cannot read directory entry in '{}': {}", path.display(), e))?;
106        let p = entry.path();
107        if p.is_dir() {
108            collect_av_files(&p, out)?;
109        } else if is_av_file(&p) {
110            out.push(p);
111        }
112    }
113    Ok(())
114}
115
116#[allow(dead_code)]
117fn is_av_file(path: &Path) -> bool {
118    path.extension().and_then(|e| e.to_str()) == Some("av")
119}
120
121fn normalize_leading_indent(line: &str) -> String {
122    let mut end = 0usize;
123    for (idx, ch) in line.char_indices() {
124        if ch == ' ' || ch == '\t' {
125            end = idx + ch.len_utf8();
126        } else {
127            break;
128        }
129    }
130
131    let (indent, rest) = line.split_at(end);
132    if rest.is_empty() {
133        return String::new();
134    }
135
136    let mut out = String::new();
137    for ch in indent.chars() {
138        if ch == '\t' {
139            out.push_str("    ");
140        } else {
141            out.push(ch);
142        }
143    }
144    out.push_str(rest);
145    out
146}
147
148fn effect_namespace(effect: &str) -> &str {
149    match effect.split_once('.') {
150        Some((namespace, _)) => namespace,
151        None => effect,
152    }
153}
154
155fn sorted_effects(effects: &[String]) -> Vec<String> {
156    let mut sorted = effects.to_vec();
157    sorted.sort();
158    sorted
159}
160
161fn format_block_effect_declaration(indent: &str, effects: &[String]) -> Vec<String> {
162    let effects = sorted_effects(effects);
163    if effects.len() <= 4 {
164        return vec![format!("{}! [{}]", indent, effects.join(", "))];
165    }
166
167    let mut out = vec![format!("{}! [", indent)];
168    let mut start = 0usize;
169    while start < effects.len() {
170        let namespace = effect_namespace(&effects[start]);
171        let mut end = start + 1;
172        while end < effects.len() && effect_namespace(&effects[end]) == namespace {
173            end += 1;
174        }
175        out.push(format!("{}    {},", indent, effects[start..end].join(", ")));
176        start = end;
177    }
178    out.push(format!("{}]", indent));
179    out
180}
181
182fn split_top_level(src: &str, delimiter: char) -> Option<Vec<String>> {
183    let mut parts = Vec::new();
184    let mut start = 0usize;
185    let mut paren_depth = 0usize;
186    let mut bracket_depth = 0usize;
187    let mut angle_depth = 0usize;
188    let mut prev = None;
189
190    for (idx, ch) in src.char_indices() {
191        match ch {
192            '(' => paren_depth += 1,
193            ')' => paren_depth = paren_depth.checked_sub(1)?,
194            '[' => bracket_depth += 1,
195            ']' => bracket_depth = bracket_depth.checked_sub(1)?,
196            '<' => angle_depth += 1,
197            '>' if prev != Some('-') && angle_depth > 0 => angle_depth -= 1,
198            _ => {}
199        }
200
201        if ch == delimiter && paren_depth == 0 && bracket_depth == 0 && angle_depth == 0 {
202            parts.push(src[start..idx].to_string());
203            start = idx + ch.len_utf8();
204        }
205        prev = Some(ch);
206    }
207
208    if paren_depth != 0 || bracket_depth != 0 || angle_depth != 0 {
209        return None;
210    }
211
212    parts.push(src[start..].to_string());
213    Some(parts)
214}
215
216fn find_matching_paren(src: &str, open_idx: usize) -> Option<usize> {
217    let mut depth = 0usize;
218    for (idx, ch) in src.char_indices().skip_while(|(idx, _)| *idx < open_idx) {
219        match ch {
220            '(' => depth += 1,
221            ')' => {
222                depth = depth.checked_sub(1)?;
223                if depth == 0 {
224                    return Some(idx);
225                }
226            }
227            _ => {}
228        }
229    }
230    None
231}
232
233fn format_type_for_source(ty: &Type) -> String {
234    match ty {
235        Type::Int => "Int".to_string(),
236        Type::Float => "Float".to_string(),
237        Type::Str => "String".to_string(),
238        Type::Bool => "Bool".to_string(),
239        Type::Unit => "Unit".to_string(),
240        Type::Result(ok, err) => format!(
241            "Result<{}, {}>",
242            format_type_for_source(ok),
243            format_type_for_source(err)
244        ),
245        Type::Option(inner) => format!("Option<{}>", format_type_for_source(inner)),
246        Type::List(inner) => format!("List<{}>", format_type_for_source(inner)),
247        Type::Vector(inner) => format!("Vector<{}>", format_type_for_source(inner)),
248        Type::Tuple(items) => format!(
249            "({})",
250            items
251                .iter()
252                .map(format_type_for_source)
253                .collect::<Vec<_>>()
254                .join(", ")
255        ),
256        Type::Map(key, value) => format!(
257            "Map<{}, {}>",
258            format_type_for_source(key),
259            format_type_for_source(value)
260        ),
261        Type::Fn(params, ret, effects) => {
262            let params = params
263                .iter()
264                .map(format_type_for_source)
265                .collect::<Vec<_>>()
266                .join(", ");
267            let ret = format_type_for_source(ret);
268            let effects = sorted_effects(effects);
269            if effects.is_empty() {
270                format!("Fn({params}) -> {ret}")
271            } else {
272                format!("Fn({params}) -> {ret} ! [{}]", effects.join(", "))
273            }
274        }
275        Type::Unknown => "Unknown".to_string(),
276        Type::Named(name) => name.clone(),
277    }
278}
279
280fn normalize_type_annotation(type_src: &str) -> String {
281    let trimmed = type_src.trim();
282    match parse_type_str_strict(trimmed) {
283        Ok(ty) => format_type_for_source(&ty),
284        Err(_) => trimmed.to_string(),
285    }
286}
287
288fn normalize_function_header_effects_line(line: &str) -> String {
289    let indent_len = line.chars().take_while(|c| *c == ' ').count();
290    let indent = " ".repeat(indent_len);
291    let trimmed = line.trim();
292    if !trimmed.starts_with("fn ") {
293        return line.to_string();
294    }
295
296    let open_idx = match trimmed.find('(') {
297        Some(idx) => idx,
298        None => return line.to_string(),
299    };
300    let close_idx = match find_matching_paren(trimmed, open_idx) {
301        Some(idx) => idx,
302        None => return line.to_string(),
303    };
304
305    let params_src = &trimmed[open_idx + 1..close_idx];
306    let params = match split_top_level(params_src, ',') {
307        Some(parts) => parts,
308        None => return line.to_string(),
309    };
310    let formatted_params = params
311        .into_iter()
312        .filter(|part| !part.trim().is_empty())
313        .map(|param| {
314            let (name, ty) = match param.split_once(':') {
315                Some(parts) => parts,
316                None => return param.trim().to_string(),
317            };
318            format!("{}: {}", name.trim(), normalize_type_annotation(ty))
319        })
320        .collect::<Vec<_>>()
321        .join(", ");
322
323    let mut formatted = format!(
324        "{}{}{})",
325        indent,
326        &trimmed[..open_idx + 1],
327        formatted_params
328    );
329    let remainder = trimmed[close_idx + 1..].trim();
330    if let Some(return_type) = remainder.strip_prefix("->") {
331        formatted.push_str(" -> ");
332        formatted.push_str(&normalize_type_annotation(return_type));
333    } else if !remainder.is_empty() {
334        formatted.push(' ');
335        formatted.push_str(remainder);
336    }
337
338    formatted
339}
340
341fn normalize_function_header_effects(lines: Vec<String>) -> Vec<String> {
342    lines
343        .into_iter()
344        .map(|line| normalize_function_header_effects_line(&line))
345        .collect()
346}
347
348fn normalize_effect_declaration_blocks(lines: Vec<String>) -> Vec<String> {
349    let mut out = Vec::with_capacity(lines.len());
350    let mut i = 0usize;
351
352    while i < lines.len() {
353        let line = &lines[i];
354        let trimmed = line.trim();
355        if !trimmed.starts_with("! [") {
356            out.push(line.clone());
357            i += 1;
358            continue;
359        }
360
361        let indent_len = line.chars().take_while(|c| *c == ' ').count();
362        let indent = " ".repeat(indent_len);
363        let mut inner = String::new();
364        let mut consumed = 0usize;
365        let mut found_close = false;
366
367        while i + consumed < lines.len() {
368            let current = &lines[i + consumed];
369            let current_trimmed = current.trim();
370            let segment = if consumed == 0 {
371                current_trimmed.trim_start_matches("! [")
372            } else {
373                current_trimmed
374            };
375
376            if let Some(before_close) = segment.strip_suffix(']') {
377                if !inner.is_empty() && !before_close.trim().is_empty() {
378                    inner.push(' ');
379                }
380                inner.push_str(before_close.trim());
381                found_close = true;
382                consumed += 1;
383                break;
384            }
385
386            if !inner.is_empty() && !segment.trim().is_empty() {
387                inner.push(' ');
388            }
389            inner.push_str(segment.trim());
390            consumed += 1;
391        }
392
393        if !found_close {
394            out.push(line.clone());
395            i += 1;
396            continue;
397        }
398
399        let effects: Vec<String> = if inner.trim().is_empty() {
400            vec![]
401        } else {
402            inner
403                .split(',')
404                .map(str::trim)
405                .filter(|part| !part.is_empty())
406                .map(ToString::to_string)
407                .collect()
408        };
409
410        out.extend(format_block_effect_declaration(&indent, &effects));
411        i += consumed;
412    }
413
414    out
415}
416
417#[derive(Clone, Debug, PartialEq, Eq)]
418enum BlockKind {
419    Fn(String),
420    Verify(String),
421    Other,
422}
423
424#[derive(Clone, Debug, PartialEq, Eq)]
425struct TopBlock {
426    text: String,
427    kind: BlockKind,
428    start_line: usize,
429}
430
431#[derive(Default)]
432struct FormatAstInfo {
433    kind_by_line: HashMap<usize, BlockKind>,
434}
435
436fn classify_block(header_line: &str) -> BlockKind {
437    let trimmed = header_line.trim();
438    if let Some(rest) = trimmed.strip_prefix("fn ") {
439        let name = rest
440            .split(['(', ' ', '\t'])
441            .next()
442            .unwrap_or_default()
443            .to_string();
444        if !name.is_empty() {
445            return BlockKind::Fn(name);
446        }
447    }
448    if let Some(rest) = trimmed.strip_prefix("verify ") {
449        let name = rest
450            .split([' ', '\t'])
451            .next()
452            .unwrap_or_default()
453            .to_string();
454        if !name.is_empty() {
455            return BlockKind::Verify(name);
456        }
457    }
458    BlockKind::Other
459}
460
461fn is_top_level_start(line: &str) -> bool {
462    if line.is_empty() {
463        return false;
464    }
465    if line.starts_with(' ') || line.starts_with('\t') {
466        return false;
467    }
468    !line.trim_start().starts_with("//")
469}
470
471fn split_top_level_blocks(lines: &[String], ast_info: Option<&FormatAstInfo>) -> Vec<TopBlock> {
472    if lines.is_empty() {
473        return Vec::new();
474    }
475
476    let starts: Vec<usize> = lines
477        .iter()
478        .enumerate()
479        .filter_map(|(idx, line)| is_top_level_start(line).then_some(idx))
480        .collect();
481
482    if starts.is_empty() {
483        let text = lines.join("\n").trim_end_matches('\n').to_string();
484        if text.is_empty() {
485            return Vec::new();
486        }
487        return vec![TopBlock {
488            text,
489            kind: BlockKind::Other,
490            start_line: 1,
491        }];
492    }
493
494    let mut blocks = Vec::new();
495
496    // Preserve preamble comments/metadata before first top-level declaration.
497    let first = starts[0];
498    if first > 0 {
499        let mut pre = lines[..first].to_vec();
500        while pre.last().is_some_and(|l| l.is_empty()) {
501            pre.pop();
502        }
503        if !pre.is_empty() {
504            blocks.push(TopBlock {
505                text: pre.join("\n"),
506                kind: BlockKind::Other,
507                start_line: 1,
508            });
509        }
510    }
511
512    for (i, start) in starts.iter().enumerate() {
513        let end = starts.get(i + 1).copied().unwrap_or(lines.len());
514        let mut segment = lines[*start..end].to_vec();
515        while segment.last().is_some_and(|l| l.is_empty()) {
516            segment.pop();
517        }
518        if segment.is_empty() {
519            continue;
520        }
521        let header = segment[0].clone();
522        let start_line = *start + 1;
523        let kind = ast_info
524            .and_then(|info| info.kind_by_line.get(&start_line).cloned())
525            .unwrap_or_else(|| classify_block(&header));
526        blocks.push(TopBlock {
527            text: segment.join("\n"),
528            kind,
529            start_line,
530        });
531    }
532
533    blocks
534}
535
536fn reorder_verify_blocks(blocks: Vec<TopBlock>) -> Vec<TopBlock> {
537    let verify_blocks: Vec<TopBlock> = blocks
538        .iter()
539        .filter(|b| matches!(b.kind, BlockKind::Verify(_)))
540        .cloned()
541        .collect();
542
543    if verify_blocks.is_empty() {
544        return blocks;
545    }
546
547    let mut by_fn: HashMap<String, Vec<usize>> = HashMap::new();
548    for (idx, block) in verify_blocks.iter().enumerate() {
549        if let BlockKind::Verify(name) = &block.kind {
550            by_fn.entry(name.clone()).or_default().push(idx);
551        }
552    }
553
554    let mut used = vec![false; verify_blocks.len()];
555    let mut out = Vec::new();
556
557    for block in blocks {
558        match block.kind.clone() {
559            BlockKind::Verify(_) => {}
560            BlockKind::Fn(name) => {
561                out.push(block);
562                if let Some(indices) = by_fn.remove(&name) {
563                    for idx in indices {
564                        used[idx] = true;
565                        out.push(verify_blocks[idx].clone());
566                    }
567                }
568            }
569            BlockKind::Other => out.push(block),
570        }
571    }
572
573    for (idx, block) in verify_blocks.iter().enumerate() {
574        if !used[idx] {
575            out.push(block.clone());
576        }
577    }
578
579    out
580}
581
582fn parse_ast_info_checked(source: &str) -> Result<FormatAstInfo, String> {
583    let mut lexer = Lexer::new(source);
584    let tokens = lexer.tokenize().map_err(|e| e.to_string())?;
585    let mut parser = Parser::new(tokens);
586    let items = parser.parse().map_err(|e| e.to_string())?;
587
588    let mut info = FormatAstInfo::default();
589    for item in items {
590        match item {
591            TopLevel::FnDef(fd) => {
592                info.kind_by_line
593                    .insert(fd.line, BlockKind::Fn(fd.name.clone()));
594            }
595            TopLevel::Verify(vb) => {
596                info.kind_by_line
597                    .insert(vb.line, BlockKind::Verify(vb.fn_name.clone()));
598            }
599            _ => {}
600        }
601    }
602    Ok(info)
603}
604
605fn normalize_source_lines(source: &str) -> Vec<String> {
606    let normalized = source.replace("\r\n", "\n").replace('\r', "\n");
607
608    let mut lines = Vec::new();
609    for raw in normalized.split('\n') {
610        let trimmed = raw.trim_end_matches([' ', '\t']);
611        let line = normalize_leading_indent(trimmed);
612        lines.push(line);
613    }
614
615    let lines = normalize_effect_declaration_blocks(lines);
616    let lines = normalize_function_header_effects(lines);
617    let lines = normalize_module_intent_blocks(lines);
618    normalize_inline_decision_fields(lines)
619}
620
621fn normalize_module_intent_blocks(lines: Vec<String>) -> Vec<String> {
622    let mut out = Vec::with_capacity(lines.len());
623    let mut in_module_header = false;
624    let mut i = 0usize;
625
626    while i < lines.len() {
627        let line = &lines[i];
628        let trimmed = line.trim();
629        let indent = line.chars().take_while(|c| *c == ' ').count();
630
631        if indent == 0 && trimmed.starts_with("module ") {
632            in_module_header = true;
633            out.push(line.clone());
634            i += 1;
635            continue;
636        }
637
638        if in_module_header && indent == 0 && !trimmed.is_empty() && !trimmed.starts_with("//") {
639            in_module_header = false;
640        }
641
642        if in_module_header && indent > 0 {
643            let head = &line[indent..];
644            if let Some(rhs) = head.strip_prefix("intent =") {
645                let rhs_trimmed = rhs.trim_start();
646                if rhs_trimmed.starts_with('"') {
647                    let mut parts = vec![rhs_trimmed.to_string()];
648                    let mut consumed = 1usize;
649
650                    while i + consumed < lines.len() {
651                        let next = &lines[i + consumed];
652                        let next_indent = next.chars().take_while(|c| *c == ' ').count();
653                        let next_trimmed = next.trim();
654
655                        if next_indent <= indent || next_trimmed.is_empty() {
656                            break;
657                        }
658                        if !next_trimmed.starts_with('"') {
659                            break;
660                        }
661
662                        parts.push(next_trimmed.to_string());
663                        consumed += 1;
664                    }
665
666                    if parts.len() > 1 {
667                        out.push(format!("{}intent =", " ".repeat(indent)));
668                        for part in parts {
669                            out.push(format!("{}{}", " ".repeat(indent + 4), part));
670                        }
671                        i += consumed;
672                        continue;
673                    }
674                }
675            }
676        }
677
678        out.push(line.clone());
679        i += 1;
680    }
681
682    out
683}
684
685fn normalize_internal_blank_runs(text: &str) -> String {
686    let mut out = Vec::new();
687    let mut blank_run = 0usize;
688    for raw in text.split('\n') {
689        if raw.is_empty() {
690            blank_run += 1;
691            if blank_run <= 2 {
692                out.push(String::new());
693            }
694        } else {
695            blank_run = 0;
696            out.push(raw.to_string());
697        }
698    }
699    while out.first().is_some_and(|l| l.is_empty()) {
700        out.remove(0);
701    }
702    while out.last().is_some_and(|l| l.is_empty()) {
703        out.pop();
704    }
705    out.join("\n")
706}
707
708const DECISION_FIELDS: [&str; 6] = ["date", "author", "reason", "chosen", "rejected", "impacts"];
709
710fn starts_with_decision_field(content: &str) -> bool {
711    DECISION_FIELDS
712        .iter()
713        .any(|field| content.starts_with(&format!("{field} =")))
714}
715
716fn find_next_decision_field_boundary(s: &str) -> Option<usize> {
717    let mut best: Option<usize> = None;
718    for field in DECISION_FIELDS {
719        let needle = format!(" {field} =");
720        let mut search_from = 0usize;
721        while let Some(rel) = s[search_from..].find(&needle) {
722            let idx = search_from + rel;
723            // Require at least two spaces before the next field marker, so
724            // normal single-space tokens don't split accidentally.
725            let spaces_before = s[..idx].chars().rev().take_while(|c| *c == ' ').count();
726            // `needle` starts at one of the separating spaces, so include it.
727            let total_separator_spaces = spaces_before + 1;
728            if total_separator_spaces >= 2 {
729                let field_start = idx + 1;
730                best = Some(best.map_or(field_start, |cur| cur.min(field_start)));
731                break;
732            }
733            search_from = idx + 1;
734        }
735    }
736    best
737}
738
739fn split_inline_decision_fields(content: &str) -> Vec<String> {
740    if !starts_with_decision_field(content) {
741        return vec![content.to_string()];
742    }
743    let mut out = Vec::new();
744    let mut rest = content.trim_end().to_string();
745    while let Some(idx) = find_next_decision_field_boundary(&rest) {
746        let left = rest[..idx].trim_end().to_string();
747        if left.is_empty() {
748            break;
749        }
750        out.push(left);
751        rest = rest[idx..].trim_start().to_string();
752    }
753    if !rest.is_empty() {
754        out.push(rest.trim_end().to_string());
755    }
756    if out.is_empty() {
757        vec![content.to_string()]
758    } else {
759        out
760    }
761}
762
763fn normalize_inline_decision_fields(lines: Vec<String>) -> Vec<String> {
764    let mut out = Vec::with_capacity(lines.len());
765    let mut in_decision = false;
766
767    for line in lines {
768        let trimmed = line.trim();
769        let indent = line.chars().take_while(|c| *c == ' ').count();
770
771        if indent == 0 && trimmed.starts_with("decision ") {
772            in_decision = true;
773            out.push(line);
774            continue;
775        }
776
777        if in_decision && indent == 0 && !trimmed.is_empty() && !trimmed.starts_with("//") {
778            in_decision = false;
779        }
780
781        if in_decision && trimmed.is_empty() {
782            continue;
783        }
784
785        if in_decision && indent > 0 {
786            let content = &line[indent..];
787            let parts = split_inline_decision_fields(content);
788            if parts.len() > 1 {
789                for part in parts {
790                    out.push(format!("{}{}", " ".repeat(indent), part));
791                }
792                continue;
793            }
794        }
795
796        out.push(line);
797    }
798
799    out
800}
801
802pub fn try_format_source(source: &str) -> Result<String, String> {
803    let lines = normalize_source_lines(source);
804    let normalized = lines.join("\n");
805    let ast_info = parse_ast_info_checked(&normalized)?;
806
807    // 3) Split into top-level blocks and co-locate verify blocks under their functions.
808    let blocks = split_top_level_blocks(&lines, Some(&ast_info));
809    let reordered = reorder_verify_blocks(blocks);
810
811    // 4) Rejoin with one blank line between top-level blocks.
812    let mut non_empty_blocks = Vec::new();
813    for block in reordered {
814        let text = normalize_internal_blank_runs(&block.text);
815        let text = text.trim_matches('\n').to_string();
816        if !text.is_empty() {
817            non_empty_blocks.push(text);
818        }
819    }
820
821    if non_empty_blocks.is_empty() {
822        return Ok("\n".to_string());
823    }
824    let mut out = non_empty_blocks.join("\n\n");
825    out.push('\n');
826    Ok(out)
827}
828
829#[cfg(test)]
830pub fn format_source(source: &str) -> String {
831    match try_format_source(source) {
832        Ok(formatted) => formatted,
833        Err(err) => panic!("format_source received invalid Aver source: {err}"),
834    }
835}
836
837#[cfg(test)]
838mod tests {
839    use super::{format_source, try_format_source};
840
841    #[test]
842    fn normalizes_line_endings_and_trailing_ws() {
843        let src = "module A\r\n    fn x() -> Int   \r\n        1\t \r\n";
844        let got = format_source(src);
845        assert_eq!(got, "module A\n    fn x() -> Int\n        1\n");
846    }
847
848    #[test]
849    fn converts_leading_tabs_only() {
850        let src = "\tfn x() -> String\n\t\t\"a\\tb\"\n";
851        let got = format_source(src);
852        assert_eq!(got, "    fn x() -> String\n        \"a\\tb\"\n");
853    }
854
855    #[test]
856    fn collapses_long_blank_runs() {
857        let src = "module A\n\n\n\nfn x() -> Int\n    1\n";
858        let got = format_source(src);
859        assert_eq!(got, "module A\n\nfn x() -> Int\n    1\n");
860    }
861
862    #[test]
863    fn keeps_single_final_newline() {
864        let src = "module A\nfn x() -> Int\n    1\n\n\n";
865        let got = format_source(src);
866        assert_eq!(got, "module A\n\nfn x() -> Int\n    1\n");
867    }
868
869    #[test]
870    fn rejects_removed_eq_expr_syntax() {
871        let src = "fn x() -> Int\n    = 1\n";
872        let err = try_format_source(src).expect_err("old '= expr' syntax should fail");
873        assert!(
874            err.contains("no longer use '= expr'"),
875            "unexpected error: {}",
876            err
877        );
878    }
879
880    #[test]
881    fn moves_verify_directly_under_function() {
882        let src = r#"module Demo
883
884fn a(x: Int) -> Int
885    x + 1
886
887fn b(x: Int) -> Int
888    x + 2
889
890verify a
891    a(1) => 2
892
893verify b
894    b(1) => 3
895"#;
896        let got = format_source(src);
897        assert_eq!(
898            got,
899            r#"module Demo
900
901fn a(x: Int) -> Int
902    x + 1
903
904verify a
905    a(1) => 2
906
907fn b(x: Int) -> Int
908    x + 2
909
910verify b
911    b(1) => 3
912"#
913        );
914    }
915
916    #[test]
917    fn leaves_orphan_verify_at_end() {
918        let src = r#"module Demo
919
920verify missing
921    missing(1) => 2
922"#;
923        let got = format_source(src);
924        assert_eq!(
925            got,
926            r#"module Demo
927
928verify missing
929    missing(1) => 2
930"#
931        );
932    }
933
934    #[test]
935    fn keeps_inline_module_intent_inline() {
936        let src = r#"module Demo
937    intent = "Inline intent."
938    exposes [x]
939fn x() -> Int
940    1
941"#;
942        let got = format_source(src);
943        assert_eq!(
944            got,
945            r#"module Demo
946    intent = "Inline intent."
947    exposes [x]
948
949fn x() -> Int
950    1
951"#
952        );
953    }
954
955    #[test]
956    fn expands_multiline_module_intent_to_block() {
957        let src = r#"module Demo
958    intent = "First line."
959        "Second line."
960    exposes [x]
961fn x() -> Int
962    1
963"#;
964        let got = format_source(src);
965        assert_eq!(
966            got,
967            r#"module Demo
968    intent =
969        "First line."
970        "Second line."
971    exposes [x]
972
973fn x() -> Int
974    1
975"#
976        );
977    }
978
979    #[test]
980    fn splits_inline_decision_fields_to_separate_lines() {
981        let src = r#"module Demo
982    intent = "x"
983    exposes [main]
984
985decision D
986    date = "2026-03-02"
987    chosen = "A"    rejected = ["B"]
988    impacts = [main]
989"#;
990        let got = format_source(src);
991        assert_eq!(
992            got,
993            r#"module Demo
994    intent = "x"
995    exposes [main]
996
997decision D
998    date = "2026-03-02"
999    chosen = "A"
1000    rejected = ["B"]
1001    impacts = [main]
1002"#
1003        );
1004    }
1005
1006    #[test]
1007    fn keeps_inline_function_description_inline() {
1008        let src = r#"fn add(a: Int, b: Int) -> Int
1009    ? "Adds two numbers."
1010    a + b
1011"#;
1012        let got = format_source(src);
1013        assert_eq!(
1014            got,
1015            r#"fn add(a: Int, b: Int) -> Int
1016    ? "Adds two numbers."
1017    a + b
1018"#
1019        );
1020    }
1021
1022    #[test]
1023    fn keeps_short_effect_lists_inline() {
1024        let src = r#"fn apply(f: Fn(Int) -> Int ! [Console.warn, Console.print], x: Int) -> Int
1025    ! [Http.post, Console.print, Http.get, Console.warn]
1026    f(x)
1027"#;
1028        let got = format_source(src);
1029        assert_eq!(
1030            got,
1031            r#"fn apply(f: Fn(Int) -> Int ! [Console.print, Console.warn], x: Int) -> Int
1032    ! [Console.print, Console.warn, Http.get, Http.post]
1033    f(x)
1034"#
1035        );
1036    }
1037
1038    #[test]
1039    fn expands_long_effect_lists_to_multiline_alphabetical_groups() {
1040        let src = r#"fn main() -> Unit
1041    ! [Args.get, Console.print, Console.warn, Time.now, Disk.makeDir, Disk.exists, Disk.readText, Disk.writeText, Disk.appendText]
1042    Unit
1043"#;
1044        let got = format_source(src);
1045        assert_eq!(
1046            got,
1047            r#"fn main() -> Unit
1048    ! [
1049        Args.get,
1050        Console.print, Console.warn,
1051        Disk.appendText, Disk.exists, Disk.makeDir, Disk.readText, Disk.writeText,
1052        Time.now,
1053    ]
1054    Unit
1055"#
1056        );
1057    }
1058
1059    #[test]
1060    fn sorts_function_type_effects_inline() {
1061        let src = r#"fn useHandler(handler: Fn(Int) -> Result<String, String> ! [Time.now, Args.get, Console.warn, Console.print, Disk.readText], value: Int) -> Unit
1062    handler(value)
1063"#;
1064        let got = format_source(src);
1065        assert_eq!(
1066            got,
1067            r#"fn useHandler(handler: Fn(Int) -> Result<String, String> ! [Args.get, Console.print, Console.warn, Disk.readText, Time.now], value: Int) -> Unit
1068    handler(value)
1069"#
1070        );
1071    }
1072
1073    #[test]
1074    fn keeps_long_function_type_effects_inline() {
1075        let src = r#"fn apply(handler: Fn(Int) -> Int ! [Time.now, Args.get, Console.warn, Console.print, Disk.readText], value: Int) -> Int
1076    handler(value)
1077"#;
1078        let got = format_source(src);
1079        assert_eq!(
1080            got,
1081            r#"fn apply(handler: Fn(Int) -> Int ! [Args.get, Console.print, Console.warn, Disk.readText, Time.now], value: Int) -> Int
1082    handler(value)
1083"#
1084        );
1085    }
1086}