1use std::collections::HashSet;
14use std::path::{Path, PathBuf};
15
16use nova_diagnostics::{Diagnostic, FileDb, FileId, Severity};
17use nova_pm::{Graph, PackageId};
18use nova_resolver::{Definitions, ModuleSource};
19use nova_typeck::{CheckOptions, ProbePoint, ProbeResult};
20
21use crate::program::{a_dependencys_warning, load_program, registry_files, Load, Program};
22
23pub trait Sources {
25 fn read(&self, path: &Path) -> std::io::Result<String>;
27
28 fn same_file(&self, a: &Path, b: &Path) -> bool {
31 a == b
32 }
33}
34
35pub struct DiskSources;
37
38impl Sources for DiskSources {
39 fn read(&self, path: &Path) -> std::io::Result<String> {
40 std::fs::read_to_string(path)
41 }
42}
43
44#[derive(Debug, Clone, Default)]
46pub struct Options {
47 pub keep_going: bool,
49 pub tests: bool,
52 pub module_only: bool,
55 pub probe: Option<Probe>,
57 pub index: bool,
61}
62
63#[derive(Debug, Clone)]
65pub struct Probe {
66 pub path: PathBuf,
67 pub offset: u32,
68}
69
70pub struct Analysis {
72 pub db: FileDb,
74 pub diagnostics: Vec<Diagnostic>,
75 pub modules: Vec<(FileId, PathBuf)>,
78 pub module_packages: Vec<Option<PackageId>>,
81 pub graph: Option<Graph>,
83 pub manifest: Option<FileId>,
86 pub definitions: Option<Definitions>,
87 pub file: Option<nova_ast::File>,
91 pub module: Option<nova_hir::Module>,
93 pub probe: ProbeResult,
94 pub index: Option<nova_resolver::Index>,
97}
98
99pub fn analyze(
102 entry: &Path,
103 sources: &dyn Sources,
104 options: &Options,
105) -> std::io::Result<Analysis> {
106 analyze_program(Program::for_file(entry), sources, options)
107}
108
109pub fn analyze_program(
112 program: Program,
113 sources: &dyn Sources,
114 options: &Options,
115) -> std::io::Result<Analysis> {
116 let manifest = program.manifest();
117 let Program {
118 mut db,
119 graph,
120 roots,
121 runs,
122 mut diagnostics,
123 } = program;
124 let stop = |diags: &[Diagnostic]| !options.keep_going && has_error(diags);
125 let load = if stop(&diagnostics) {
126 Load::default()
127 } else {
128 load_program(graph.as_ref(), &roots, sources, &mut db)?
129 };
130 diagnostics.extend(load.diagnostics);
131 let dropped = load.dropped;
132 let registry = registry_files(graph.as_ref(), &load.modules);
133 let mut modules = load.modules;
134 let mut analysis = Analysis {
135 db,
136 diagnostics: Vec::new(),
137 modules: modules.iter().map(|m| (m.file, m.path.clone())).collect(),
138 module_packages: modules.iter().map(|m| m.package).collect(),
139 graph,
140 manifest,
141 definitions: None,
142 file: None,
143 module: None,
144 probe: ProbeResult::default(),
145 index: None,
146 };
147 if stop(&diagnostics) {
148 analysis.diagnostics = shown(diagnostics, ®istry);
149 return Ok(analysis);
150 }
151
152 crate::strip_test_functions(&mut modules, options.tests);
153 let std_files: Vec<FileId> = nova_resolver::STD_MODULES
154 .iter()
155 .map(|&(name, src)| {
156 let short = name.strip_prefix("$std.").unwrap_or(name);
157 analysis.db.add(format!("<std/{short}>"), src)
158 })
159 .collect();
160 let extra_std = options.tests.then(|| {
161 let (name, src) = nova_resolver::STD_TEST_MODULE;
162 let short = name.strip_prefix("$std.").unwrap_or(name);
163 let file = analysis.db.add(format!("<std/{short}>"), src);
164 (nova_resolver::STD_TEST_MODULE, file)
165 });
166 let module_sources: Vec<ModuleSource> = modules
167 .iter()
168 .map(|m| ModuleSource {
169 name: m.name.clone(),
170 file: &m.ast,
171 imports: m.imports.clone(),
172 text: analysis.db.get_source(m.file),
173 package: m.package.map(|p| p.0),
174 importable: m.importable.clone(),
175 })
176 .collect();
177 let resolved = nova_resolver::resolve_program(&module_sources, &std_files, extra_std);
178 diagnostics.extend(resolved.diagnostics);
179 let imports = resolved.imports;
180 if stop(&diagnostics) {
181 analysis.diagnostics = shown(diagnostics, ®istry);
182 analysis.definitions = Some(resolved.definitions);
183 analysis.file = Some(resolved.file);
184 return Ok(analysis);
185 }
186
187 let probe = options.probe.as_ref().and_then(|p| {
188 analysis
189 .modules
190 .iter()
191 .find(|(_, path)| sources.same_file(path, &p.path))
192 .map(|(file, _)| ProbePoint {
193 file: *file,
194 offset: p.offset,
195 })
196 });
197 let checked = nova_typeck::check_with(
198 &resolved.file,
199 &resolved.definitions,
200 &CheckOptions {
201 probe,
202 index: options.index,
203 sources: Some(&analysis.db),
204 },
205 );
206 diagnostics.extend(checked.diagnostics);
207 let module_only = options.module_only || !runs;
210 let mut module = checked.module;
211 if !module_only && !has_error(&diagnostics) {
212 crate::keep_the_entry_main(&mut module, &resolved.definitions);
213 if let Err(mir) = nova_mir::lower_module(&module) {
214 diagnostics.extend(mir);
215 }
216 }
217 if options.keep_going {
218 diagnostics.retain(|d| !about_a_dropped_name(d, &dropped));
219 }
220 analysis.diagnostics = shown(diagnostics, ®istry);
221 analysis.definitions = Some(resolved.definitions);
222 analysis.module = Some(module);
223 analysis.probe = checked.probe;
224 if let Some(mut index) = checked.index {
225 index.extend(imports);
226 analysis.index = Some(index);
227 }
228 analysis.file = Some(resolved.file);
229 Ok(analysis)
230}
231
232fn has_error(diagnostics: &[Diagnostic]) -> bool {
233 diagnostics.iter().any(|d| d.severity == Severity::Error)
234}
235
236fn shown(mut diagnostics: Vec<Diagnostic>, registry: &HashSet<FileId>) -> Vec<Diagnostic> {
238 diagnostics.retain(|d| !a_dependencys_warning(d, registry));
239 diagnostics
240}
241
242fn about_a_dropped_name(d: &Diagnostic, dropped: &[String]) -> bool {
254 if d.code != "E0001" {
255 return false;
256 }
257 let mut parts = d.message.split('`');
258 parts.next();
259 parts
260 .next()
261 .is_some_and(|name| dropped.iter().any(|n| n == name))
262}