Skip to main content

logicaffeine_language/
import_format.rs

1//! Canonical formatting of a module's `Import` statements — the pit-of-success
2//! default so import blocks never need manual bikeshedding.
3//!
4//! This is a separate pass from [`source_format`](crate::source_format), applied
5//! by `largo fmt`, so the formatter's token-stream invariant is untouched:
6//! reordering imports is semantics-preserving (import order never matters), but it
7//! does change the token stream, so it can't live inside `format_source`.
8//!
9//! Only explicit `Import …` statement lines in the abstract are touched (literate
10//! `[Alias](uri)` links are left as the author wrote them). The block is sorted,
11//! de-duplicated, and collapsed to the position of the first import.
12
13use std::collections::HashSet;
14
15/// Rewrite the entry's abstract `Import` statements into a single canonical block:
16/// sorted, de-duplicated, placed where the first import was, with the rest
17/// removed. Prose and code are untouched. Idempotent.
18pub fn normalize_imports(source: &str) -> String {
19    let lines: Vec<&str> = source.lines().collect();
20
21    // Collect the `Import …` statement lines that appear BEFORE the first code
22    // block (`## `). Imports only ever live in the abstract.
23    let mut import_indices: Vec<usize> = Vec::new();
24    let mut imports: Vec<String> = Vec::new();
25    for (i, line) in lines.iter().enumerate() {
26        let t = line.trim();
27        if t.starts_with("## ") {
28            break;
29        }
30        if is_import_statement(t) {
31            import_indices.push(i);
32            imports.push(t.to_string());
33        }
34    }
35
36    // Nothing to canonicalize unless there are at least two (to sort/dedupe).
37    if imports.len() < 2 {
38        return source.to_string();
39    }
40
41    imports.sort();
42    imports.dedup();
43
44    let first = import_indices[0];
45    let drop: HashSet<usize> = import_indices.iter().copied().collect();
46    let mut out: Vec<String> = Vec::with_capacity(lines.len());
47    for (i, line) in lines.iter().enumerate() {
48        if i == first {
49            out.extend(imports.iter().cloned());
50        } else if drop.contains(&i) {
51            continue;
52        } else {
53            out.push((*line).to_string());
54        }
55    }
56
57    let mut result = out.join("\n");
58    if source.ends_with('\n') {
59        result.push('\n');
60    }
61    result
62}
63
64/// Whether a trimmed line is an `Import …` statement (not just prose starting with
65/// the word "Import").
66fn is_import_statement(trimmed: &str) -> bool {
67    let Some(rest) = trimmed.strip_prefix("Import ") else {
68        return false;
69    };
70    // A real import ends with a period and names a Capitalized module or a
71    // `from`/`,`/`and` clause — reuse the scanner's own view of what parses.
72    trimmed.ends_with('.')
73        && !rest.trim().is_empty()
74        && !crate::analysis::scan_dependencies(&format!("# _\n\n{trimmed}\n")).is_empty()
75}
76
77#[cfg(test)]
78#[path = "../../logicaffeine_tests/unit/logicaffeine_language/import_format/tests.rs"]
79mod tests;