Skip to main content

lisette_semantics/module_graph/
mod.rs

1pub mod kahn;
2
3use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
4
5use deps::{TypedefLocator, TypedefLocatorResult};
6use syntax::ast::Span;
7use syntax::program::File;
8
9use crate::loader::Loader;
10use crate::store::Store;
11use diagnostics::DiagnosticSink;
12
13pub type ModuleId = String;
14
15#[derive(Debug)]
16pub struct ModuleGraphResult {
17    pub order: Vec<ModuleId>,
18    pub cycles: Vec<Vec<ModuleId>>,
19    pub files: HashMap<ModuleId, Vec<File>>,
20    /// Direct dependencies of each module (module_id -> set of dependency module_ids).
21    /// Used for transitive cache invalidation.
22    pub edges: HashMap<ModuleId, HashSet<ModuleId>>,
23}
24
25pub fn build_module_graph(
26    store: &mut Store,
27    loader: Option<&dyn Loader>,
28    entry_module: &str,
29    sink: &DiagnosticSink,
30    standalone_mode: bool,
31    locator: &TypedefLocator,
32) -> ModuleGraphResult {
33    let mut edges: HashMap<ModuleId, HashSet<ModuleId>> = HashMap::default();
34    let mut to_visit = vec![entry_module.to_string()];
35    let mut visited = HashSet::default();
36    let mut files: HashMap<ModuleId, Vec<File>> = HashMap::default();
37    let mut import_spans: HashMap<ModuleId, Span> = HashMap::default();
38
39    while let Some(module_id) = to_visit.pop() {
40        if visited.contains(&module_id) {
41            continue;
42        }
43        visited.insert(module_id.clone());
44
45        let (imports_with_spans, module_files) =
46            collect_imports(&module_id, store, loader, sink, standalone_mode, locator);
47
48        let module_exists = !module_files.is_empty()
49            || store.has(&module_id)
50            || module_id == entry_module
51            || module_id.starts_with("go:"); // go modules are virtual
52
53        if !module_exists {
54            if let Some(span) = import_spans.get(&module_id) {
55                let is_go_stdlib = stdlib::get_go_stdlib_typedef(&module_id).is_some();
56
57                let src_prefix_hint = module_id
58                    .strip_prefix("src/")
59                    .filter(|stripped| {
60                        loader.is_some_and(|fs| !fs.scan_folder(stripped).is_empty())
61                    })
62                    .map(String::from);
63
64                sink.push(diagnostics::module_graph::module_not_found(
65                    &module_id,
66                    *span,
67                    is_go_stdlib,
68                    standalone_mode,
69                    src_prefix_hint,
70                ));
71            }
72            continue;
73        }
74
75        files.insert(module_id.clone(), module_files);
76
77        let imports: HashSet<_> = imports_with_spans.keys().cloned().collect();
78
79        for (import, span) in imports_with_spans {
80            if !visited.contains(&import) {
81                to_visit.push(import.clone());
82            }
83            import_spans.entry(import).or_insert(span);
84        }
85
86        edges.insert(module_id, imports);
87    }
88
89    let (order, cycles) = kahn::topological_sort(&edges);
90
91    ModuleGraphResult {
92        order,
93        cycles,
94        files,
95        edges,
96    }
97}
98
99fn parse_module_files(
100    module_id: &ModuleId,
101    store: &mut Store,
102    loader: Option<&dyn Loader>,
103    sink: &DiagnosticSink,
104) -> Vec<File> {
105    let Some(fs) = loader else {
106        return vec![];
107    };
108    let mut files = Vec::new();
109    for (filename, source) in fs.scan_folder(module_id) {
110        if filename.ends_with("_test.lis") {
111            sink.push(diagnostics::module_graph::test_file_not_supported(
112                &filename,
113            ));
114            continue;
115        }
116        // Ensure the module exists in the store before adding the first file
117        if files.is_empty() {
118            store.add_module(module_id);
119        }
120        let file_id = store.new_file_id();
121        let result = syntax::build_ast(&source, file_id);
122        sink.extend_parse_errors(result.errors);
123        let file = File::new(module_id, &filename, &source, result.ast, file_id);
124        // Register the file immediately so diagnostic rendering works
125        store.store_file(module_id, file.clone());
126        files.push(file);
127    }
128    files
129}
130
131fn collect_imports(
132    module_id: &ModuleId,
133    store: &mut Store,
134    loader: Option<&dyn Loader>,
135    sink: &DiagnosticSink,
136    standalone_mode: bool,
137    locator: &TypedefLocator,
138) -> (HashMap<ModuleId, Span>, Vec<File>) {
139    let mut imports = HashMap::default();
140
141    let (files, file_imports): (Vec<File>, Vec<_>) =
142        if let Some(module) = store.get_module(module_id) {
143            // Module already in store (entry module or prelude): get imports from stored files
144            let lis_imports = module.files.values().flat_map(|f| f.imports());
145            let typedef_imports = module.all_typedefs().flat_map(|f| f.imports());
146            let all_imports: Vec<_> = lis_imports.chain(typedef_imports).collect();
147            (vec![], all_imports)
148        } else {
149            // Module not in store: parse from filesystem
150            let parsed = parse_module_files(module_id, store, loader, sink);
151            let file_imports = parsed.iter().flat_map(|f| f.imports()).collect();
152            (parsed, file_imports)
153        };
154
155    for file_import in file_imports {
156        if file_import.name == "prelude" {
157            sink.push(diagnostics::module_graph::cannot_import_prelude(
158                file_import.span,
159            ));
160            continue;
161        }
162
163        if let Some(go_pkg) = file_import.name.strip_prefix("go:") {
164            match locator.find_typedef_content(go_pkg) {
165                TypedefLocatorResult::Found { .. } => {
166                    imports.insert(file_import.name.to_string(), file_import.name_span);
167                }
168                TypedefLocatorResult::UnknownStdlib => {
169                    sink.push(diagnostics::module_graph::module_not_found(
170                        &file_import.name,
171                        file_import.name_span,
172                        false,
173                        standalone_mode,
174                        None,
175                    ));
176                }
177                TypedefLocatorResult::UndeclaredImport => {
178                    if standalone_mode {
179                        sink.push(diagnostics::module_graph::module_not_found(
180                            &file_import.name,
181                            file_import.name_span,
182                            false,
183                            true,
184                            None,
185                        ));
186                    } else {
187                        sink.push(diagnostics::module_graph::undeclared_go_import(
188                            go_pkg,
189                            file_import.name_span,
190                        ));
191                    }
192                }
193                TypedefLocatorResult::MissingTypedef { module, version } => {
194                    sink.push(diagnostics::module_graph::missing_go_typedef(
195                        go_pkg,
196                        &module,
197                        &version,
198                        file_import.name_span,
199                    ));
200                }
201                TypedefLocatorResult::UnreadableTypedef { path, error } => {
202                    sink.push(diagnostics::module_graph::unreadable_go_typedef(
203                        &path,
204                        &error,
205                        file_import.name_span,
206                    ));
207                }
208            }
209            continue;
210        }
211
212        if file_import.name.contains('.') {
213            sink.push(diagnostics::module_graph::invalid_module_path(
214                &file_import.name,
215                file_import.name_span,
216            ));
217            continue;
218        }
219
220        imports
221            .entry(file_import.name.to_string())
222            .or_insert(file_import.name_span);
223    }
224
225    (imports, files)
226}