1use std::path::{Path, PathBuf};
4use std::time::Instant;
5
6use fallow_config::{DuplicatesConfig, ResolvedConfig};
7use fallow_types::discover::DiscoveredFile;
8use fallow_types::extract::{ModuleInfo, ParseResult};
9use fallow_types::source_fingerprint::SourceFingerprint;
10use fallow_types::workspace::WorkspaceDiagnostic;
11use rustc_hash::{FxHashMap, FxHashSet};
12
13use crate::{
14 DeadCodeAnalysis, DeadCodeAnalysisArtifacts, DeadCodeAnalysisOutput, DuplicationAnalysis,
15 EngineResult, ProjectAnalysisOutput, ProjectConfig, config_for_project, core_backend,
16 duplicates, project_config::default_project_config,
17};
18
19#[derive(Debug)]
25pub struct AnalysisSession {
26 config: ResolvedConfig,
27 config_path: Option<PathBuf>,
28 discovery: crate::discover::AnalysisDiscovery,
29 workspace_diagnostics: Vec<WorkspaceDiagnostic>,
30}
31
32#[derive(Debug)]
34pub struct AnalysisSessionParts {
35 pub config: ResolvedConfig,
36 pub config_path: Option<PathBuf>,
37 pub files: Vec<DiscoveredFile>,
38 pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
39}
40
41#[derive(Debug)]
43pub struct ParsedAnalysisSessionParts {
44 pub config: ResolvedConfig,
45 pub config_path: Option<PathBuf>,
46 pub files: Vec<DiscoveredFile>,
47 pub modules: Vec<ModuleInfo>,
48 pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
49 pub parse_ms: f64,
50 pub parse_cpu_ms: f64,
51}
52
53#[derive(Debug)]
55pub struct AnalysisSessionArtifacts {
56 pub analysis: DeadCodeAnalysisArtifacts,
57 pub changed_files: Option<FxHashSet<PathBuf>>,
58 pub source_fingerprints: FxHashMap<PathBuf, SourceFingerprint>,
59}
60
61struct AnalysisSessionView<'a> {
67 config: &'a ResolvedConfig,
68 discovery: crate::discover::AnalysisDiscovery,
69}
70
71impl<'a> AnalysisSessionView<'a> {
72 fn new(config: &'a ResolvedConfig) -> Self {
73 Self {
74 config,
75 discovery: core_backend::prepare_analysis_discovery(config),
76 }
77 }
78
79 fn analyze_dead_code(&self) -> EngineResult<DeadCodeAnalysis> {
80 core_backend::analyze_with_usages_from_discovery(self.config, &self.discovery)
81 }
82
83 fn analyze_dead_code_with_complexity(&self) -> EngineResult<DeadCodeAnalysisOutput> {
84 core_backend::analyze_with_usages_and_complexity_from_discovery(
85 self.config,
86 &self.discovery,
87 )
88 }
89
90 fn analyze_dead_code_with_artifacts(
91 &self,
92 need_complexity: bool,
93 retain_graph: bool,
94 ) -> EngineResult<DeadCodeAnalysisArtifacts> {
95 core_backend::analyze_retaining_modules_from_discovery(
96 self.config,
97 &self.discovery,
98 need_complexity,
99 retain_graph,
100 )
101 }
102}
103
104impl AnalysisSession {
105 pub fn load(root: &Path, config_path: Option<&Path>) -> EngineResult<Self> {
111 let project_config = config_for_project(root, config_path)?;
112 Ok(Self::from_config(project_config))
113 }
114
115 pub fn load_with_config(
122 root: &Path,
123 config_path: Option<&Path>,
124 configure: impl FnOnce(&mut ResolvedConfig),
125 ) -> EngineResult<Self> {
126 let mut project_config = config_for_project(root, config_path)?;
127 configure(&mut project_config.config);
128 Ok(Self::from_config(project_config))
129 }
130
131 #[must_use]
136 pub fn load_default(root: &Path) -> Self {
137 Self::from_config(default_project_config(root))
138 }
139
140 #[must_use]
142 pub fn from_config(project_config: ProjectConfig) -> Self {
143 let discovery = core_backend::prepare_analysis_discovery(&project_config.config);
144 let workspace_diagnostics = merge_workspace_diagnostics(
145 project_config.workspace_diagnostics,
146 fallow_config::workspace_diagnostics_for(&project_config.config.root),
147 );
148 Self {
149 config: project_config.config,
150 config_path: project_config.path,
151 discovery,
152 workspace_diagnostics,
153 }
154 }
155
156 #[must_use]
159 pub fn from_resolved_config(config: ResolvedConfig) -> Self {
160 Self::from_config(ProjectConfig {
161 config,
162 path: None,
163 workspace_diagnostics: Vec::new(),
164 })
165 }
166
167 #[must_use]
169 pub fn root(&self) -> &Path {
170 &self.config.root
171 }
172
173 #[must_use]
175 pub fn config(&self) -> &ResolvedConfig {
176 &self.config
177 }
178
179 #[must_use]
181 pub fn config_path(&self) -> Option<&Path> {
182 self.config_path.as_deref()
183 }
184
185 #[must_use]
187 pub fn files(&self) -> &[DiscoveredFile] {
188 self.discovery.files()
189 }
190
191 #[must_use]
193 pub fn source_fingerprints(&self) -> FxHashMap<PathBuf, SourceFingerprint> {
194 self.discovery
195 .files()
196 .iter()
197 .map(|file| {
198 let fingerprint = std::fs::metadata(&file.path).map_or_else(
199 |_| SourceFingerprint::new(0, file.size_bytes),
200 |metadata| SourceFingerprint::from_metadata(&metadata),
201 );
202 (file.path.clone(), fingerprint)
203 })
204 .collect()
205 }
206
207 pub fn changed_files_since(
214 &self,
215 git_ref: &str,
216 ) -> Result<FxHashSet<PathBuf>, crate::ChangedFilesError> {
217 crate::changed_files(&self.config.root, git_ref)
218 }
219
220 #[must_use]
222 pub fn workspace_diagnostics(&self) -> &[WorkspaceDiagnostic] {
223 &self.workspace_diagnostics
224 }
225
226 #[must_use]
228 pub fn into_parts(self) -> AnalysisSessionParts {
229 AnalysisSessionParts {
230 config: self.config,
231 config_path: self.config_path,
232 files: self.discovery.into_files(),
233 workspace_diagnostics: self.workspace_diagnostics,
234 }
235 }
236
237 #[must_use]
239 pub fn into_parsed_parts(self, need_complexity: bool) -> ParsedAnalysisSessionParts {
240 let AnalysisSessionParts {
241 config,
242 config_path,
243 files,
244 workspace_diagnostics,
245 } = self.into_parts();
246 let (parse_result, parse_ms) = parse_files_with_config(&config, &files, need_complexity);
247 ParsedAnalysisSessionParts {
248 config,
249 config_path,
250 files,
251 modules: parse_result.modules,
252 workspace_diagnostics,
253 parse_ms,
254 parse_cpu_ms: parse_result.parse_cpu_ms,
255 }
256 }
257
258 pub fn analyze_dead_code(&self) -> EngineResult<DeadCodeAnalysis> {
264 core_backend::analyze_with_usages_from_discovery(&self.config, &self.discovery)
265 }
266
267 pub fn analyze_dead_code_with_complexity(&self) -> EngineResult<DeadCodeAnalysisOutput> {
273 core_backend::analyze_with_usages_and_complexity_from_discovery(
274 &self.config,
275 &self.discovery,
276 )
277 }
278
279 pub fn analyze_dead_code_with_artifacts(
285 &self,
286 need_complexity: bool,
287 retain_graph: bool,
288 ) -> EngineResult<DeadCodeAnalysisArtifacts> {
289 core_backend::analyze_retaining_modules_from_discovery(
290 &self.config,
291 &self.discovery,
292 need_complexity,
293 retain_graph,
294 )
295 }
296
297 pub fn analyze_dead_code_with_session_artifacts(
308 &self,
309 need_complexity: bool,
310 retain_graph: bool,
311 changed_files: Option<FxHashSet<PathBuf>>,
312 ) -> EngineResult<AnalysisSessionArtifacts> {
313 Ok(AnalysisSessionArtifacts {
314 analysis: self.analyze_dead_code_with_artifacts(need_complexity, retain_graph)?,
315 changed_files,
316 source_fingerprints: self.source_fingerprints(),
317 })
318 }
319
320 #[must_use]
322 pub fn find_duplicates(&self) -> duplicates::DuplicationReport {
323 duplicates::find_duplicates(&self.config.root, self.files(), &self.config.duplicates)
324 }
325
326 #[must_use]
328 pub fn find_duplicates_with(&self, config: &DuplicatesConfig) -> duplicates::DuplicationReport {
329 duplicates::find_duplicates(&self.config.root, self.files(), config)
330 }
331
332 pub fn analyze_project_with(
341 &self,
342 duplicates_config: &DuplicatesConfig,
343 retain_complexity_artifacts: bool,
344 ) -> EngineResult<ProjectAnalysisOutput> {
345 let dead_code = if retain_complexity_artifacts {
346 self.analyze_dead_code_with_complexity()?
347 } else {
348 let analysis = self.analyze_dead_code()?;
349 DeadCodeAnalysisOutput {
350 results: analysis.results,
351 modules: None,
352 files: None,
353 }
354 };
355 let duplication = self.find_duplicates_with(duplicates_config);
356 Ok(ProjectAnalysisOutput {
357 dead_code,
358 duplication,
359 })
360 }
361
362 #[must_use]
364 pub fn find_duplicates_with_defaults(
365 &self,
366 config: &DuplicatesConfig,
367 cache_dir: Option<&Path>,
368 ) -> DuplicationAnalysis {
369 duplicates::find_duplicates_with_defaults(
370 &self.config.root,
371 self.files(),
372 config,
373 cache_dir,
374 )
375 }
376
377 #[must_use]
379 pub fn find_duplicates_touching_files_with_defaults(
380 &self,
381 config: &DuplicatesConfig,
382 changed_files: &[PathBuf],
383 cache_dir: Option<&Path>,
384 ) -> DuplicationAnalysis {
385 duplicates::find_duplicates_touching_files_with_defaults(
386 &self.config.root,
387 self.files(),
388 config,
389 changed_files,
390 cache_dir,
391 )
392 }
393}
394
395pub fn parse_files_for_config(
396 config: &ResolvedConfig,
397 files: &[DiscoveredFile],
398 need_complexity: bool,
399) -> ParseResult {
400 parse_files_with_config(config, files, need_complexity).0
401}
402
403fn merge_workspace_diagnostics(
404 primary: Vec<WorkspaceDiagnostic>,
405 secondary: Vec<WorkspaceDiagnostic>,
406) -> Vec<WorkspaceDiagnostic> {
407 let mut merged = Vec::with_capacity(primary.len() + secondary.len());
408 let mut seen: FxHashSet<(String, PathBuf)> = FxHashSet::default();
409 for diagnostic in primary.into_iter().chain(secondary) {
410 let key = (diagnostic.kind.id().to_owned(), diagnostic.path.clone());
411 if seen.insert(key) {
412 merged.push(diagnostic);
413 }
414 }
415 merged
416}
417
418fn parse_files_with_config(
419 config: &ResolvedConfig,
420 files: &[DiscoveredFile],
421 need_complexity: bool,
422) -> (ParseResult, f64) {
423 let parse_start = Instant::now();
424 let cache = if config.no_cache {
425 None
426 } else {
427 fallow_extract::cache::CacheStore::load(
428 &config.cache_dir,
429 config.cache_config_hash,
430 crate::resolve_cache_max_size_bytes(config),
431 )
432 };
433 let parse_result = crate::source::parse_all_files(files, cache.as_ref(), need_complexity);
434 (parse_result, parse_start.elapsed().as_secs_f64() * 1000.0)
435}
436
437pub fn analyze_dead_code_from_config(config: &ResolvedConfig) -> EngineResult<DeadCodeAnalysis> {
438 AnalysisSessionView::new(config).analyze_dead_code()
439}
440
441pub fn analyze_dead_code_with_complexity_from_config(
442 config: &ResolvedConfig,
443) -> EngineResult<DeadCodeAnalysisOutput> {
444 AnalysisSessionView::new(config).analyze_dead_code_with_complexity()
445}
446
447pub fn analyze_dead_code_with_artifacts_from_config(
448 config: &ResolvedConfig,
449 need_complexity: bool,
450 retain_graph: bool,
451) -> EngineResult<DeadCodeAnalysisArtifacts> {
452 AnalysisSessionView::new(config).analyze_dead_code_with_artifacts(need_complexity, retain_graph)
453}