logicaffeine_compile/analysis/
import_hygiene.rs1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum LintSeverity {
20 Warning,
21 Error,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum ImportLintKind {
27 Unused,
29 Duplicate,
31 SelfImport,
33}
34
35#[derive(Debug, Clone)]
37pub struct ImportLint {
38 pub kind: ImportLintKind,
39 pub severity: LintSeverity,
40 pub module: String,
42 pub message: String,
44 pub span: (usize, usize),
46}
47
48fn canonical(path: &Path) -> PathBuf {
49 path.canonicalize().unwrap_or_else(|_| path.to_path_buf())
50}
51
52fn word_appears(code: &str, name: &str) -> bool {
54 code.split(|c: char| !c.is_alphanumeric() && c != '_')
55 .any(|w| w == name)
56}
57
58pub fn lint_imports(entry: &Path) -> Result<Vec<ImportLint>, String> {
62 lint_imports_with_deps(entry, Vec::new())
63}
64
65pub 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 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 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 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}