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::Tuple(items) => format!(
248 "({})",
249 items
250 .iter()
251 .map(format_type_for_source)
252 .collect::<Vec<_>>()
253 .join(", ")
254 ),
255 Type::Map(key, value) => format!(
256 "Map<{}, {}>",
257 format_type_for_source(key),
258 format_type_for_source(value)
259 ),
260 Type::Fn(params, ret, effects) => {
261 let params = params
262 .iter()
263 .map(format_type_for_source)
264 .collect::<Vec<_>>()
265 .join(", ");
266 let ret = format_type_for_source(ret);
267 let effects = sorted_effects(effects);
268 if effects.is_empty() {
269 format!("Fn({params}) -> {ret}")
270 } else {
271 format!("Fn({params}) -> {ret} ! [{}]", effects.join(", "))
272 }
273 }
274 Type::Unknown => "Unknown".to_string(),
275 Type::Named(name) => name.clone(),
276 }
277}
278
279fn normalize_type_annotation(type_src: &str) -> String {
280 let trimmed = type_src.trim();
281 match parse_type_str_strict(trimmed) {
282 Ok(ty) => format_type_for_source(&ty),
283 Err(_) => trimmed.to_string(),
284 }
285}
286
287fn normalize_function_header_effects_line(line: &str) -> String {
288 let indent_len = line.chars().take_while(|c| *c == ' ').count();
289 let indent = " ".repeat(indent_len);
290 let trimmed = line.trim();
291 if !trimmed.starts_with("fn ") {
292 return line.to_string();
293 }
294
295 let open_idx = match trimmed.find('(') {
296 Some(idx) => idx,
297 None => return line.to_string(),
298 };
299 let close_idx = match find_matching_paren(trimmed, open_idx) {
300 Some(idx) => idx,
301 None => return line.to_string(),
302 };
303
304 let params_src = &trimmed[open_idx + 1..close_idx];
305 let params = match split_top_level(params_src, ',') {
306 Some(parts) => parts,
307 None => return line.to_string(),
308 };
309 let formatted_params = params
310 .into_iter()
311 .filter(|part| !part.trim().is_empty())
312 .map(|param| {
313 let (name, ty) = match param.split_once(':') {
314 Some(parts) => parts,
315 None => return param.trim().to_string(),
316 };
317 format!("{}: {}", name.trim(), normalize_type_annotation(ty))
318 })
319 .collect::<Vec<_>>()
320 .join(", ");
321
322 let mut formatted = format!(
323 "{}{}{})",
324 indent,
325 &trimmed[..open_idx + 1],
326 formatted_params
327 );
328 let remainder = trimmed[close_idx + 1..].trim();
329 if let Some(return_type) = remainder.strip_prefix("->") {
330 formatted.push_str(" -> ");
331 formatted.push_str(&normalize_type_annotation(return_type));
332 } else if !remainder.is_empty() {
333 formatted.push(' ');
334 formatted.push_str(remainder);
335 }
336
337 formatted
338}
339
340fn normalize_function_header_effects(lines: Vec<String>) -> Vec<String> {
341 lines
342 .into_iter()
343 .map(|line| normalize_function_header_effects_line(&line))
344 .collect()
345}
346
347fn normalize_effect_declaration_blocks(lines: Vec<String>) -> Vec<String> {
348 let mut out = Vec::with_capacity(lines.len());
349 let mut i = 0usize;
350
351 while i < lines.len() {
352 let line = &lines[i];
353 let trimmed = line.trim();
354 if !trimmed.starts_with("! [") {
355 out.push(line.clone());
356 i += 1;
357 continue;
358 }
359
360 let indent_len = line.chars().take_while(|c| *c == ' ').count();
361 let indent = " ".repeat(indent_len);
362 let mut inner = String::new();
363 let mut consumed = 0usize;
364 let mut found_close = false;
365
366 while i + consumed < lines.len() {
367 let current = &lines[i + consumed];
368 let current_trimmed = current.trim();
369 let segment = if consumed == 0 {
370 current_trimmed.trim_start_matches("! [")
371 } else {
372 current_trimmed
373 };
374
375 if let Some(before_close) = segment.strip_suffix(']') {
376 if !inner.is_empty() && !before_close.trim().is_empty() {
377 inner.push(' ');
378 }
379 inner.push_str(before_close.trim());
380 found_close = true;
381 consumed += 1;
382 break;
383 }
384
385 if !inner.is_empty() && !segment.trim().is_empty() {
386 inner.push(' ');
387 }
388 inner.push_str(segment.trim());
389 consumed += 1;
390 }
391
392 if !found_close {
393 out.push(line.clone());
394 i += 1;
395 continue;
396 }
397
398 let effects: Vec<String> = if inner.trim().is_empty() {
399 vec![]
400 } else {
401 inner
402 .split(',')
403 .map(str::trim)
404 .filter(|part| !part.is_empty())
405 .map(ToString::to_string)
406 .collect()
407 };
408
409 out.extend(format_block_effect_declaration(&indent, &effects));
410 i += consumed;
411 }
412
413 out
414}
415
416#[derive(Clone, Debug, PartialEq, Eq)]
417enum BlockKind {
418 Fn(String),
419 Verify(String),
420 Other,
421}
422
423#[derive(Clone, Debug, PartialEq, Eq)]
424struct TopBlock {
425 text: String,
426 kind: BlockKind,
427 start_line: usize,
428}
429
430#[derive(Default)]
431struct FormatAstInfo {
432 kind_by_line: HashMap<usize, BlockKind>,
433}
434
435fn classify_block(header_line: &str) -> BlockKind {
436 let trimmed = header_line.trim();
437 if let Some(rest) = trimmed.strip_prefix("fn ") {
438 let name = rest
439 .split(['(', ' ', '\t'])
440 .next()
441 .unwrap_or_default()
442 .to_string();
443 if !name.is_empty() {
444 return BlockKind::Fn(name);
445 }
446 }
447 if let Some(rest) = trimmed.strip_prefix("verify ") {
448 let name = rest
449 .split([' ', '\t'])
450 .next()
451 .unwrap_or_default()
452 .to_string();
453 if !name.is_empty() {
454 return BlockKind::Verify(name);
455 }
456 }
457 BlockKind::Other
458}
459
460fn is_top_level_start(line: &str) -> bool {
461 if line.is_empty() {
462 return false;
463 }
464 if line.starts_with(' ') || line.starts_with('\t') {
465 return false;
466 }
467 !line.trim_start().starts_with("//")
468}
469
470fn split_top_level_blocks(lines: &[String], ast_info: Option<&FormatAstInfo>) -> Vec<TopBlock> {
471 if lines.is_empty() {
472 return Vec::new();
473 }
474
475 let starts: Vec<usize> = lines
476 .iter()
477 .enumerate()
478 .filter_map(|(idx, line)| is_top_level_start(line).then_some(idx))
479 .collect();
480
481 if starts.is_empty() {
482 let text = lines.join("\n").trim_end_matches('\n').to_string();
483 if text.is_empty() {
484 return Vec::new();
485 }
486 return vec![TopBlock {
487 text,
488 kind: BlockKind::Other,
489 start_line: 1,
490 }];
491 }
492
493 let mut blocks = Vec::new();
494
495 let first = starts[0];
497 if first > 0 {
498 let mut pre = lines[..first].to_vec();
499 while pre.last().is_some_and(|l| l.is_empty()) {
500 pre.pop();
501 }
502 if !pre.is_empty() {
503 blocks.push(TopBlock {
504 text: pre.join("\n"),
505 kind: BlockKind::Other,
506 start_line: 1,
507 });
508 }
509 }
510
511 for (i, start) in starts.iter().enumerate() {
512 let end = starts.get(i + 1).copied().unwrap_or(lines.len());
513 let mut segment = lines[*start..end].to_vec();
514 while segment.last().is_some_and(|l| l.is_empty()) {
515 segment.pop();
516 }
517 if segment.is_empty() {
518 continue;
519 }
520 let header = segment[0].clone();
521 let start_line = *start + 1;
522 let kind = ast_info
523 .and_then(|info| info.kind_by_line.get(&start_line).cloned())
524 .unwrap_or_else(|| classify_block(&header));
525 blocks.push(TopBlock {
526 text: segment.join("\n"),
527 kind,
528 start_line,
529 });
530 }
531
532 blocks
533}
534
535fn reorder_verify_blocks(blocks: Vec<TopBlock>) -> Vec<TopBlock> {
536 let verify_blocks: Vec<TopBlock> = blocks
537 .iter()
538 .filter(|b| matches!(b.kind, BlockKind::Verify(_)))
539 .cloned()
540 .collect();
541
542 if verify_blocks.is_empty() {
543 return blocks;
544 }
545
546 let mut by_fn: HashMap<String, Vec<usize>> = HashMap::new();
547 for (idx, block) in verify_blocks.iter().enumerate() {
548 if let BlockKind::Verify(name) = &block.kind {
549 by_fn.entry(name.clone()).or_default().push(idx);
550 }
551 }
552
553 let mut used = vec![false; verify_blocks.len()];
554 let mut out = Vec::new();
555
556 for block in blocks {
557 match block.kind.clone() {
558 BlockKind::Verify(_) => {}
559 BlockKind::Fn(name) => {
560 out.push(block);
561 if let Some(indices) = by_fn.remove(&name) {
562 for idx in indices {
563 used[idx] = true;
564 out.push(verify_blocks[idx].clone());
565 }
566 }
567 }
568 BlockKind::Other => out.push(block),
569 }
570 }
571
572 for (idx, block) in verify_blocks.iter().enumerate() {
573 if !used[idx] {
574 out.push(block.clone());
575 }
576 }
577
578 out
579}
580
581fn parse_ast_info_checked(source: &str) -> Result<FormatAstInfo, String> {
582 let mut lexer = Lexer::new(source);
583 let tokens = lexer.tokenize().map_err(|e| e.to_string())?;
584 let mut parser = Parser::new(tokens);
585 let items = parser.parse().map_err(|e| e.to_string())?;
586
587 let mut info = FormatAstInfo::default();
588 for item in items {
589 match item {
590 TopLevel::FnDef(fd) => {
591 info.kind_by_line
592 .insert(fd.line, BlockKind::Fn(fd.name.clone()));
593 }
594 TopLevel::Verify(vb) => {
595 info.kind_by_line
596 .insert(vb.line, BlockKind::Verify(vb.fn_name.clone()));
597 }
598 _ => {}
599 }
600 }
601 Ok(info)
602}
603
604fn normalize_source_lines(source: &str) -> Vec<String> {
605 let normalized = source.replace("\r\n", "\n").replace('\r', "\n");
606
607 let mut lines = Vec::new();
608 for raw in normalized.split('\n') {
609 let trimmed = raw.trim_end_matches([' ', '\t']);
610 let line = normalize_leading_indent(trimmed);
611 lines.push(line);
612 }
613
614 let lines = normalize_effect_declaration_blocks(lines);
615 let lines = normalize_function_header_effects(lines);
616 let lines = normalize_module_intent_blocks(lines);
617 normalize_inline_decision_fields(lines)
618}
619
620fn normalize_module_intent_blocks(lines: Vec<String>) -> Vec<String> {
621 let mut out = Vec::with_capacity(lines.len());
622 let mut in_module_header = false;
623 let mut i = 0usize;
624
625 while i < lines.len() {
626 let line = &lines[i];
627 let trimmed = line.trim();
628 let indent = line.chars().take_while(|c| *c == ' ').count();
629
630 if indent == 0 && trimmed.starts_with("module ") {
631 in_module_header = true;
632 out.push(line.clone());
633 i += 1;
634 continue;
635 }
636
637 if in_module_header && indent == 0 && !trimmed.is_empty() && !trimmed.starts_with("//") {
638 in_module_header = false;
639 }
640
641 if in_module_header && indent > 0 {
642 let head = &line[indent..];
643 if let Some(rhs) = head.strip_prefix("intent =") {
644 let rhs_trimmed = rhs.trim_start();
645 if rhs_trimmed.starts_with('"') {
646 let mut parts = vec![rhs_trimmed.to_string()];
647 let mut consumed = 1usize;
648
649 while i + consumed < lines.len() {
650 let next = &lines[i + consumed];
651 let next_indent = next.chars().take_while(|c| *c == ' ').count();
652 let next_trimmed = next.trim();
653
654 if next_indent <= indent || next_trimmed.is_empty() {
655 break;
656 }
657 if !next_trimmed.starts_with('"') {
658 break;
659 }
660
661 parts.push(next_trimmed.to_string());
662 consumed += 1;
663 }
664
665 if parts.len() > 1 {
666 out.push(format!("{}intent =", " ".repeat(indent)));
667 for part in parts {
668 out.push(format!("{}{}", " ".repeat(indent + 4), part));
669 }
670 i += consumed;
671 continue;
672 }
673 }
674 }
675 }
676
677 out.push(line.clone());
678 i += 1;
679 }
680
681 out
682}
683
684fn normalize_internal_blank_runs(text: &str) -> String {
685 let mut out = Vec::new();
686 let mut blank_run = 0usize;
687 for raw in text.split('\n') {
688 if raw.is_empty() {
689 blank_run += 1;
690 if blank_run <= 2 {
691 out.push(String::new());
692 }
693 } else {
694 blank_run = 0;
695 out.push(raw.to_string());
696 }
697 }
698 while out.first().is_some_and(|l| l.is_empty()) {
699 out.remove(0);
700 }
701 while out.last().is_some_and(|l| l.is_empty()) {
702 out.pop();
703 }
704 out.join("\n")
705}
706
707const DECISION_FIELDS: [&str; 6] = ["date", "author", "reason", "chosen", "rejected", "impacts"];
708
709fn starts_with_decision_field(content: &str) -> bool {
710 DECISION_FIELDS
711 .iter()
712 .any(|field| content.starts_with(&format!("{field} =")))
713}
714
715fn find_next_decision_field_boundary(s: &str) -> Option<usize> {
716 let mut best: Option<usize> = None;
717 for field in DECISION_FIELDS {
718 let needle = format!(" {field} =");
719 let mut search_from = 0usize;
720 while let Some(rel) = s[search_from..].find(&needle) {
721 let idx = search_from + rel;
722 let spaces_before = s[..idx].chars().rev().take_while(|c| *c == ' ').count();
725 let total_separator_spaces = spaces_before + 1;
727 if total_separator_spaces >= 2 {
728 let field_start = idx + 1;
729 best = Some(best.map_or(field_start, |cur| cur.min(field_start)));
730 break;
731 }
732 search_from = idx + 1;
733 }
734 }
735 best
736}
737
738fn split_inline_decision_fields(content: &str) -> Vec<String> {
739 if !starts_with_decision_field(content) {
740 return vec![content.to_string()];
741 }
742 let mut out = Vec::new();
743 let mut rest = content.trim_end().to_string();
744 while let Some(idx) = find_next_decision_field_boundary(&rest) {
745 let left = rest[..idx].trim_end().to_string();
746 if left.is_empty() {
747 break;
748 }
749 out.push(left);
750 rest = rest[idx..].trim_start().to_string();
751 }
752 if !rest.is_empty() {
753 out.push(rest.trim_end().to_string());
754 }
755 if out.is_empty() {
756 vec![content.to_string()]
757 } else {
758 out
759 }
760}
761
762fn normalize_inline_decision_fields(lines: Vec<String>) -> Vec<String> {
763 let mut out = Vec::with_capacity(lines.len());
764 let mut in_decision = false;
765
766 for line in lines {
767 let trimmed = line.trim();
768 let indent = line.chars().take_while(|c| *c == ' ').count();
769
770 if indent == 0 && trimmed.starts_with("decision ") {
771 in_decision = true;
772 out.push(line);
773 continue;
774 }
775
776 if in_decision && indent == 0 && !trimmed.is_empty() && !trimmed.starts_with("//") {
777 in_decision = false;
778 }
779
780 if in_decision && trimmed.is_empty() {
781 continue;
782 }
783
784 if in_decision && indent > 0 {
785 let content = &line[indent..];
786 let parts = split_inline_decision_fields(content);
787 if parts.len() > 1 {
788 for part in parts {
789 out.push(format!("{}{}", " ".repeat(indent), part));
790 }
791 continue;
792 }
793 }
794
795 out.push(line);
796 }
797
798 out
799}
800
801pub fn try_format_source(source: &str) -> Result<String, String> {
802 let lines = normalize_source_lines(source);
803 let normalized = lines.join("\n");
804 let ast_info = parse_ast_info_checked(&normalized)?;
805
806 let blocks = split_top_level_blocks(&lines, Some(&ast_info));
808 let reordered = reorder_verify_blocks(blocks);
809
810 let mut non_empty_blocks = Vec::new();
812 for block in reordered {
813 let text = normalize_internal_blank_runs(&block.text);
814 let text = text.trim_matches('\n').to_string();
815 if !text.is_empty() {
816 non_empty_blocks.push(text);
817 }
818 }
819
820 if non_empty_blocks.is_empty() {
821 return Ok("\n".to_string());
822 }
823 let mut out = non_empty_blocks.join("\n\n");
824 out.push('\n');
825 Ok(out)
826}
827
828#[cfg(test)]
829pub fn format_source(source: &str) -> String {
830 match try_format_source(source) {
831 Ok(formatted) => formatted,
832 Err(err) => panic!("format_source received invalid Aver source: {err}"),
833 }
834}
835
836#[cfg(test)]
837mod tests {
838 use super::{format_source, try_format_source};
839
840 #[test]
841 fn normalizes_line_endings_and_trailing_ws() {
842 let src = "module A\r\n fn x() -> Int \r\n 1\t \r\n";
843 let got = format_source(src);
844 assert_eq!(got, "module A\n fn x() -> Int\n 1\n");
845 }
846
847 #[test]
848 fn converts_leading_tabs_only() {
849 let src = "\tfn x() -> String\n\t\t\"a\\tb\"\n";
850 let got = format_source(src);
851 assert_eq!(got, " fn x() -> String\n \"a\\tb\"\n");
852 }
853
854 #[test]
855 fn collapses_long_blank_runs() {
856 let src = "module A\n\n\n\nfn x() -> Int\n 1\n";
857 let got = format_source(src);
858 assert_eq!(got, "module A\n\nfn x() -> Int\n 1\n");
859 }
860
861 #[test]
862 fn keeps_single_final_newline() {
863 let src = "module A\nfn x() -> Int\n 1\n\n\n";
864 let got = format_source(src);
865 assert_eq!(got, "module A\n\nfn x() -> Int\n 1\n");
866 }
867
868 #[test]
869 fn rejects_removed_eq_expr_syntax() {
870 let src = "fn x() -> Int\n = 1\n";
871 let err = try_format_source(src).expect_err("old '= expr' syntax should fail");
872 assert!(
873 err.contains("no longer use '= expr'"),
874 "unexpected error: {}",
875 err
876 );
877 }
878
879 #[test]
880 fn moves_verify_directly_under_function() {
881 let src = r#"module Demo
882
883fn a(x: Int) -> Int
884 x + 1
885
886fn b(x: Int) -> Int
887 x + 2
888
889verify a
890 a(1) => 2
891
892verify b
893 b(1) => 3
894"#;
895 let got = format_source(src);
896 assert_eq!(
897 got,
898 r#"module Demo
899
900fn a(x: Int) -> Int
901 x + 1
902
903verify a
904 a(1) => 2
905
906fn b(x: Int) -> Int
907 x + 2
908
909verify b
910 b(1) => 3
911"#
912 );
913 }
914
915 #[test]
916 fn leaves_orphan_verify_at_end() {
917 let src = r#"module Demo
918
919verify missing
920 missing(1) => 2
921"#;
922 let got = format_source(src);
923 assert_eq!(
924 got,
925 r#"module Demo
926
927verify missing
928 missing(1) => 2
929"#
930 );
931 }
932
933 #[test]
934 fn keeps_inline_module_intent_inline() {
935 let src = r#"module Demo
936 intent = "Inline intent."
937 exposes [x]
938fn x() -> Int
939 1
940"#;
941 let got = format_source(src);
942 assert_eq!(
943 got,
944 r#"module Demo
945 intent = "Inline intent."
946 exposes [x]
947
948fn x() -> Int
949 1
950"#
951 );
952 }
953
954 #[test]
955 fn expands_multiline_module_intent_to_block() {
956 let src = r#"module Demo
957 intent = "First line."
958 "Second line."
959 exposes [x]
960fn x() -> Int
961 1
962"#;
963 let got = format_source(src);
964 assert_eq!(
965 got,
966 r#"module Demo
967 intent =
968 "First line."
969 "Second line."
970 exposes [x]
971
972fn x() -> Int
973 1
974"#
975 );
976 }
977
978 #[test]
979 fn splits_inline_decision_fields_to_separate_lines() {
980 let src = r#"module Demo
981 intent = "x"
982 exposes [main]
983
984decision D
985 date = "2026-03-02"
986 chosen = "A" rejected = ["B"]
987 impacts = [main]
988"#;
989 let got = format_source(src);
990 assert_eq!(
991 got,
992 r#"module Demo
993 intent = "x"
994 exposes [main]
995
996decision D
997 date = "2026-03-02"
998 chosen = "A"
999 rejected = ["B"]
1000 impacts = [main]
1001"#
1002 );
1003 }
1004
1005 #[test]
1006 fn keeps_inline_function_description_inline() {
1007 let src = r#"fn add(a: Int, b: Int) -> Int
1008 ? "Adds two numbers."
1009 a + b
1010"#;
1011 let got = format_source(src);
1012 assert_eq!(
1013 got,
1014 r#"fn add(a: Int, b: Int) -> Int
1015 ? "Adds two numbers."
1016 a + b
1017"#
1018 );
1019 }
1020
1021 #[test]
1022 fn keeps_short_effect_lists_inline() {
1023 let src = r#"fn apply(f: Fn(Int) -> Int ! [Console.warn, Console.print], x: Int) -> Int
1024 ! [Http.post, Console.print, Http.get, Console.warn]
1025 f(x)
1026"#;
1027 let got = format_source(src);
1028 assert_eq!(
1029 got,
1030 r#"fn apply(f: Fn(Int) -> Int ! [Console.print, Console.warn], x: Int) -> Int
1031 ! [Console.print, Console.warn, Http.get, Http.post]
1032 f(x)
1033"#
1034 );
1035 }
1036
1037 #[test]
1038 fn expands_long_effect_lists_to_multiline_alphabetical_groups() {
1039 let src = r#"fn main() -> Unit
1040 ! [Args.get, Console.print, Console.warn, Time.now, Disk.makeDir, Disk.exists, Disk.readText, Disk.writeText, Disk.appendText]
1041 Unit
1042"#;
1043 let got = format_source(src);
1044 assert_eq!(
1045 got,
1046 r#"fn main() -> Unit
1047 ! [
1048 Args.get,
1049 Console.print, Console.warn,
1050 Disk.appendText, Disk.exists, Disk.makeDir, Disk.readText, Disk.writeText,
1051 Time.now,
1052 ]
1053 Unit
1054"#
1055 );
1056 }
1057
1058 #[test]
1059 fn sorts_function_type_effects_inline() {
1060 let src = r#"fn useHandler(handler: Fn(Int) -> Result<String, String> ! [Time.now, Args.get, Console.warn, Console.print, Disk.readText], value: Int) -> Unit
1061 handler(value)
1062"#;
1063 let got = format_source(src);
1064 assert_eq!(
1065 got,
1066 r#"fn useHandler(handler: Fn(Int) -> Result<String, String> ! [Args.get, Console.print, Console.warn, Disk.readText, Time.now], value: Int) -> Unit
1067 handler(value)
1068"#
1069 );
1070 }
1071
1072 #[test]
1073 fn keeps_long_function_type_effects_inline() {
1074 let src = r#"fn apply(handler: Fn(Int) -> Int ! [Time.now, Args.get, Console.warn, Console.print, Disk.readText], value: Int) -> Int
1075 handler(value)
1076"#;
1077 let got = format_source(src);
1078 assert_eq!(
1079 got,
1080 r#"fn apply(handler: Fn(Int) -> Int ! [Args.get, Console.print, Console.warn, Disk.readText, Time.now], value: Int) -> Int
1081 handler(value)
1082"#
1083 );
1084 }
1085}