Skip to main content

logicaffeine_compile/analysis/
import_hygiene.rs

1//! Import-hygiene lints — the pit-of-success guardrails that keep a project's
2//! import blocks honest. An UNRESOLVABLE import is already a hard error at
3//! assembly (with a "did you mean" suggestion from the loader); this pass finds
4//! the problems that ASSEMBLE FINE but are still mistakes: an import that is never
5//! used, the same module imported twice, and a module importing itself.
6//!
7//! The pass is deliberately separate from [`assemble_project`](super::assemble)
8//! so the assembler stays a pure Result-or-Err function; `largo check` runs this
9//! on top and surfaces the findings (warnings keep exit 0; errors fail).
10
11use std::collections::HashMap;
12use std::path::{Path, PathBuf};
13
14use crate::analysis::scan_dependencies;
15use crate::loader::{defined_names, module_code, strip_note_blocks, DepRoot, Loader};
16
17/// The severity of an import lint. Warnings never block a build; errors do.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum LintSeverity {
20    Warning,
21    Error,
22}
23
24/// What kind of import-hygiene problem was found.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum ImportLintKind {
27    /// An import whose names are never referenced in the file.
28    Unused,
29    /// The same module imported more than once.
30    Duplicate,
31    /// A module that imports itself.
32    SelfImport,
33}
34
35/// One import-hygiene finding.
36#[derive(Debug, Clone)]
37pub struct ImportLint {
38    pub kind: ImportLintKind,
39    pub severity: LintSeverity,
40    /// The module alias the import binds (`Util`).
41    pub module: String,
42    /// The human-facing message.
43    pub message: String,
44    /// The `[start, end)` byte span of the import statement (for a fmt/LSP fix).
45    pub span: (usize, usize),
46}
47
48fn canonical(path: &Path) -> PathBuf {
49    path.canonicalize().unwrap_or_else(|_| path.to_path_buf())
50}
51
52/// Whether `name` appears as a whole word anywhere in `code`.
53fn word_appears(code: &str, name: &str) -> bool {
54    code.split(|c: char| !c.is_alphanumeric() && c != '_')
55        .any(|w| w == name)
56}
57
58/// Lint the entry file's imports for unused / duplicate / self-import problems.
59/// Unresolvable imports are NOT reported here — the assembler already rejects
60/// those with a suggestion. Returns `Err` only if the entry can't be read.
61pub fn lint_imports(entry: &Path) -> Result<Vec<ImportLint>, String> {
62    lint_imports_with_deps(entry, Vec::new())
63}
64
65/// [`lint_imports`], resolving imports with the declared dependency roots so a
66/// `path`/`git`-local import is lint-checked too. `dep_roots` empty is identical
67/// to [`lint_imports`].
68pub fn lint_imports_with_deps(
69    entry: &Path,
70    dep_roots: Vec<DepRoot>,
71) -> Result<Vec<ImportLint>, String> {
72    let raw = std::fs::read_to_string(entry)
73        .map_err(|e| format!("could not read `{}`: {e}", entry.display()))?;
74    let root = entry.parent().unwrap_or(Path::new(".")).to_path_buf();
75    let mut loader = Loader::with_dep_roots(root, dep_roots);
76    let entry_canon = canonical(entry);
77
78    // The entry's own code — where a reference to an imported name would appear.
79    let code = strip_note_blocks(module_code(&raw));
80
81    let mut lints = Vec::new();
82    let mut seen: HashMap<PathBuf, String> = HashMap::new();
83
84    for dep in scan_dependencies(&raw) {
85        let span = (dep.start, dep.end);
86        let (resolved, content) = match loader.resolve(entry, &dep.uri) {
87            Ok(m) => (canonical(&m.path), m.content.clone()),
88            // Unresolvable — the assembler will report it (with a suggestion).
89            Err(_) => continue,
90        };
91
92        if resolved == entry_canon {
93            lints.push(ImportLint {
94                kind: ImportLintKind::SelfImport,
95                severity: LintSeverity::Error,
96                module: dep.alias.clone(),
97                message: format!("`{}` imports itself — remove the self-import.", dep.alias),
98                span,
99            });
100            continue;
101        }
102
103        if let Some(first) = seen.get(&resolved) {
104            lints.push(ImportLint {
105                kind: ImportLintKind::Duplicate,
106                severity: LintSeverity::Warning,
107                module: dep.alias.clone(),
108                message: format!(
109                    "`{}` is imported more than once (already imported as `{first}`) — remove the duplicate.",
110                    dep.alias
111                ),
112                span,
113            });
114            continue;
115        }
116        seen.insert(resolved, dep.alias.clone());
117
118        // Which names this import brings — the selected ones (surface alias) for a
119        // selective import, else every public name the module exports.
120        let names: Vec<String> = match &dep.selection {
121            Some(sel) => sel
122                .iter()
123                .map(|s| s.alias.clone().unwrap_or_else(|| s.name.clone()))
124                .collect(),
125            None => defined_names(&strip_note_blocks(module_code(&content))),
126        };
127        if !names.is_empty() && !names.iter().any(|n| word_appears(&code, n)) {
128            lints.push(ImportLint {
129                kind: ImportLintKind::Unused,
130                severity: LintSeverity::Warning,
131                module: dep.alias.clone(),
132                message: format!(
133                    "`Import {}` is unused — no name from it is referenced here.",
134                    dep.alias
135                ),
136                span,
137            });
138        }
139    }
140
141    Ok(lints)
142}