1use std::path::{Path, PathBuf};
4use std::sync::Mutex;
5use std::time::Instant;
6
7use fallow_config::{DuplicatesConfig, ResolvedConfig, WorkspaceInfo};
8use fallow_types::discover::DiscoveredFile;
9use fallow_types::extract::ModuleInfo;
10use fallow_types::source_fingerprint::SourceFingerprint;
11use fallow_types::workspace::WorkspaceDiagnostic;
12use rustc_hash::{FxHashMap, FxHashSet};
13
14use crate::{
15 EngineResult, core_backend, duplicates,
16 project_analysis::{
17 ProjectAnalysisArtifactOptions, ProjectAnalysisArtifacts, ProjectAnalysisOutput,
18 },
19 project_config::{ProjectConfig, config_for_project, default_project_config},
20 results::{
21 DeadCodeAnalysis, DeadCodeAnalysisArtifacts, DeadCodeAnalysisOutput, DuplicationAnalysis,
22 },
23};
24
25#[derive(Debug)]
31pub struct AnalysisSession {
32 config: ResolvedConfig,
33 config_path: Option<PathBuf>,
34 discovery: crate::discover::AnalysisDiscovery,
35 workspaces: Vec<WorkspaceInfo>,
36 workspace_diagnostics: Vec<WorkspaceDiagnostic>,
37 parsed_cache: Mutex<Option<ParsedModuleCache>>,
38 styling_cache: Mutex<Option<crate::health::StylingAnalysisArtifacts>>,
39}
40
41#[derive(Debug)]
42struct ParsedModuleCache {
43 need_complexity: bool,
44 fingerprints: Vec<SourceFingerprint>,
45 modules: Vec<ModuleInfo>,
46}
47
48#[derive(Debug)]
50pub struct AnalysisSessionParts {
51 pub config: ResolvedConfig,
52 pub config_path: Option<PathBuf>,
53 pub files: Vec<DiscoveredFile>,
54 pub workspaces: Vec<WorkspaceInfo>,
55 pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
56}
57
58#[derive(Debug)]
60pub struct ParsedAnalysisSessionParts {
61 pub config: ResolvedConfig,
62 pub config_path: Option<PathBuf>,
63 pub files: Vec<DiscoveredFile>,
64 pub modules: Vec<ModuleInfo>,
65 pub workspaces: Vec<WorkspaceInfo>,
66 pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
67 pub parse_ms: f64,
68 pub cache_update_ms: f64,
69 pub cache_hits: usize,
70 pub cache_misses: usize,
71 pub parse_cpu_ms: f64,
72}
73
74#[derive(Debug)]
76pub struct AnalysisSessionArtifacts {
77 pub analysis: DeadCodeAnalysisArtifacts,
78 pub changed_files: Option<FxHashSet<PathBuf>>,
79 pub source_fingerprints: FxHashMap<PathBuf, SourceFingerprint>,
80}
81
82impl AnalysisSession {
83 pub fn load(root: &Path, config_path: Option<&Path>) -> EngineResult<Self> {
89 let project_config = config_for_project(root, config_path)?;
90 Ok(Self::from_config(project_config))
91 }
92
93 pub fn load_with_config(
100 root: &Path,
101 config_path: Option<&Path>,
102 configure: impl FnOnce(&mut ResolvedConfig),
103 ) -> EngineResult<Self> {
104 let mut project_config = config_for_project(root, config_path)?;
105 configure(&mut project_config.config);
106 project_config.workspaces.clear();
107 project_config.workspace_diagnostics.clear();
108 project_config.workspace_discovery_ms = None;
109 Ok(Self::from_config(project_config))
110 }
111
112 #[must_use]
117 pub fn load_default(root: &Path) -> Self {
118 Self::from_config(default_project_config(root))
119 }
120
121 #[must_use]
123 pub fn from_config(project_config: ProjectConfig) -> Self {
124 let uses_preloaded_workspaces = project_config.workspace_discovery_ms.is_some();
125 let discovery = if let Some(workspace_discovery_ms) = project_config.workspace_discovery_ms
126 {
127 crate::discover::prepare_analysis_discovery_with_workspaces(
128 &project_config.config,
129 &project_config.workspaces,
130 workspace_discovery_ms,
131 )
132 } else {
133 crate::discover::prepare_analysis_discovery(&project_config.config)
134 };
135 let workspaces = if uses_preloaded_workspaces {
136 project_config.workspaces
137 } else {
138 discovery.workspaces().to_vec()
139 };
140 let workspace_diagnostics = merge_workspace_diagnostics(
141 project_config.workspace_diagnostics,
142 fallow_config::workspace_diagnostics_for(&project_config.config.root),
143 );
144 Self {
145 config: project_config.config,
146 config_path: project_config.path,
147 discovery,
148 workspaces,
149 workspace_diagnostics,
150 parsed_cache: Mutex::new(None),
151 styling_cache: Mutex::new(None),
152 }
153 }
154
155 #[must_use]
158 pub fn from_resolved_config(config: ResolvedConfig) -> Self {
159 Self::from_config(ProjectConfig {
160 config,
161 path: None,
162 workspaces: Vec::new(),
163 workspace_diagnostics: Vec::new(),
164 workspace_discovery_ms: None,
165 })
166 }
167
168 #[must_use]
170 pub fn root(&self) -> &Path {
171 &self.config.root
172 }
173
174 #[must_use]
176 pub fn config(&self) -> &ResolvedConfig {
177 &self.config
178 }
179
180 #[must_use]
182 pub fn config_path(&self) -> Option<&Path> {
183 self.config_path.as_deref()
184 }
185
186 #[must_use]
188 pub fn files(&self) -> &[DiscoveredFile] {
189 self.discovery.files()
190 }
191
192 #[must_use]
194 pub fn workspaces(&self) -> &[WorkspaceInfo] {
195 &self.workspaces
196 }
197
198 #[must_use]
200 pub fn source_fingerprints(&self) -> FxHashMap<PathBuf, SourceFingerprint> {
201 self.discovery
202 .files()
203 .iter()
204 .map(|file| {
205 let fingerprint = std::fs::metadata(&file.path).map_or_else(
206 |_| SourceFingerprint::new(0, file.size_bytes),
207 |metadata| SourceFingerprint::from_metadata(&metadata),
208 );
209 (file.path.clone(), fingerprint)
210 })
211 .collect()
212 }
213
214 pub fn changed_files_since(
221 &self,
222 git_ref: &str,
223 ) -> Result<FxHashSet<PathBuf>, crate::changed_files::ChangedFilesError> {
224 crate::changed_files::changed_files(&self.config.root, git_ref)
225 }
226
227 #[must_use]
229 pub fn workspace_diagnostics(&self) -> &[WorkspaceDiagnostic] {
230 &self.workspace_diagnostics
231 }
232
233 pub(crate) fn styling_analysis_artifacts(&self) -> crate::health::StylingAnalysisArtifacts {
234 if let Ok(cache) = self.styling_cache.lock()
235 && let Some(artifacts) = cache.as_ref()
236 {
237 return artifacts.clone();
238 }
239
240 let artifacts =
241 crate::health::build_styling_analysis_artifacts(self.files(), self.config());
242 if let Ok(mut cache) = self.styling_cache.lock() {
243 *cache = Some(artifacts.clone());
244 }
245 artifacts
246 }
247
248 #[must_use]
250 pub fn into_parts(self) -> AnalysisSessionParts {
251 AnalysisSessionParts {
252 config: self.config,
253 config_path: self.config_path,
254 files: self.discovery.into_files(),
255 workspaces: self.workspaces,
256 workspace_diagnostics: self.workspace_diagnostics,
257 }
258 }
259
260 #[must_use]
262 pub fn into_parsed_parts(self, need_complexity: bool) -> ParsedAnalysisSessionParts {
263 let AnalysisSessionParts {
264 config,
265 config_path,
266 files,
267 workspaces,
268 workspace_diagnostics,
269 } = self.into_parts();
270 let ParsedModules { modules, metrics } =
271 parse_files_with_config(&config, &files, need_complexity);
272 ParsedAnalysisSessionParts {
273 config,
274 config_path,
275 files,
276 modules,
277 workspaces,
278 workspace_diagnostics,
279 parse_ms: metrics.parse_ms,
280 cache_update_ms: metrics.cache_ms,
281 cache_hits: metrics.cache_hits,
282 cache_misses: metrics.cache_misses,
283 parse_cpu_ms: metrics.parse_cpu_ms,
284 }
285 }
286
287 #[must_use]
289 pub fn parsed_parts(&self, need_complexity: bool) -> ParsedAnalysisSessionParts {
290 let ParsedModules { modules, metrics } = self.parse_modules(need_complexity);
291 self.parsed_parts_from_modules(modules, metrics)
292 }
293
294 #[must_use]
297 pub fn parsed_parts_uncached(&self, need_complexity: bool) -> ParsedAnalysisSessionParts {
298 let ParsedModules { modules, metrics } =
299 parse_files_with_config(&self.config, self.files(), need_complexity);
300 self.parsed_parts_from_modules(modules, metrics)
301 }
302
303 fn parsed_parts_from_modules(
304 &self,
305 modules: Vec<ModuleInfo>,
306 metrics: core_backend::ParseMetrics,
307 ) -> ParsedAnalysisSessionParts {
308 ParsedAnalysisSessionParts {
309 config: self.config.clone(),
310 config_path: self.config_path.clone(),
311 files: self.discovery.files().to_vec(),
312 modules,
313 workspaces: self.workspaces.clone(),
314 workspace_diagnostics: self.workspace_diagnostics.clone(),
315 parse_ms: metrics.parse_ms,
316 cache_update_ms: metrics.cache_ms,
317 cache_hits: metrics.cache_hits,
318 cache_misses: metrics.cache_misses,
319 parse_cpu_ms: metrics.parse_cpu_ms,
320 }
321 }
322
323 pub fn analyze_dead_code(&self) -> EngineResult<DeadCodeAnalysis> {
329 self.analyze_dead_code_with_artifacts(false, false)
330 .map(|output| DeadCodeAnalysis {
331 results: output.results,
332 })
333 }
334
335 pub fn analyze_dead_code_with_complexity(&self) -> EngineResult<DeadCodeAnalysisOutput> {
341 self.analyze_dead_code_with_artifacts(true, false)
342 .map(|output| DeadCodeAnalysisOutput {
343 results: output.results,
344 modules: output.modules,
345 files: output.files,
346 })
347 }
348
349 pub fn analyze_dead_code_with_artifacts(
355 &self,
356 need_complexity: bool,
357 retain_graph: bool,
358 ) -> EngineResult<DeadCodeAnalysisArtifacts> {
359 self.analyze_dead_code_with_reuse_artifacts(need_complexity, retain_graph, need_complexity)
360 }
361
362 pub fn analyze_dead_code_retaining_files(
369 &self,
370 need_complexity: bool,
371 retain_graph: bool,
372 ) -> EngineResult<DeadCodeAnalysisArtifacts> {
373 self.analyze_dead_code_with_reuse_artifacts(need_complexity, retain_graph, true)
374 }
375
376 pub fn analyze_dead_code_with_parsed_modules(
385 &self,
386 modules: &[ModuleInfo],
387 ) -> EngineResult<DeadCodeAnalysisArtifacts> {
388 run_engine_owned_dead_code_pipeline(EngineDeadCodePipelineInput {
389 config: &self.config,
390 discovery: &self.discovery,
391 modules: modules.to_vec(),
392 metrics: reused_parse_metrics(),
393 collect_usages: true,
394 retain_graph: true,
395 retain_modules: false,
396 retain_files: false,
397 })
398 }
399
400 fn analyze_dead_code_with_reuse_artifacts(
401 &self,
402 need_complexity: bool,
403 retain_graph: bool,
404 retain_files: bool,
405 ) -> EngineResult<DeadCodeAnalysisArtifacts> {
406 let ParsedModules { modules, metrics } = self.parse_modules(need_complexity);
407 run_engine_owned_dead_code_pipeline(EngineDeadCodePipelineInput {
408 config: &self.config,
409 discovery: &self.discovery,
410 modules,
411 metrics,
412 collect_usages: true,
413 retain_graph,
414 retain_modules: need_complexity,
415 retain_files,
416 })
417 }
418
419 pub fn analyze_dead_code_with_session_artifacts(
430 &self,
431 need_complexity: bool,
432 retain_graph: bool,
433 changed_files: Option<FxHashSet<PathBuf>>,
434 ) -> EngineResult<AnalysisSessionArtifacts> {
435 Ok(AnalysisSessionArtifacts {
436 analysis: self.analyze_dead_code_with_artifacts(need_complexity, retain_graph)?,
437 changed_files,
438 source_fingerprints: self.source_fingerprints(),
439 })
440 }
441
442 #[must_use]
444 pub fn find_duplicates(&self) -> duplicates::DuplicationReport {
445 duplicates::find_duplicates(&self.config.root, self.files(), &self.config.duplicates)
446 }
447
448 #[must_use]
450 pub fn find_duplicates_with(&self, config: &DuplicatesConfig) -> duplicates::DuplicationReport {
451 duplicates::find_duplicates(&self.config.root, self.files(), config)
452 }
453
454 pub fn analyze_project_with(
463 &self,
464 duplicates_config: &DuplicatesConfig,
465 retain_complexity_artifacts: bool,
466 ) -> EngineResult<ProjectAnalysisOutput> {
467 self.analyze_project_with_artifacts(
468 duplicates_config,
469 ProjectAnalysisArtifactOptions {
470 retain_complexity_artifacts,
471 ..ProjectAnalysisArtifactOptions::default()
472 },
473 )
474 .map(ProjectAnalysisArtifacts::into_output)
475 }
476
477 pub fn analyze_project_with_artifacts(
487 &self,
488 duplicates_config: &DuplicatesConfig,
489 options: ProjectAnalysisArtifactOptions,
490 ) -> EngineResult<ProjectAnalysisArtifacts> {
491 let cache_dir = (!self.config.no_cache).then_some(self.config.cache_dir.as_path());
492 let duplication = if let Some(changed_files) = options.changed_files.as_ref() {
493 let changed_files = changed_files.iter().cloned().collect::<Vec<_>>();
494 self.find_duplicates_touching_files_with_defaults(
495 duplicates_config,
496 &changed_files,
497 cache_dir,
498 )
499 .report
500 } else {
501 self.find_duplicates_with_defaults(duplicates_config, cache_dir)
502 .report
503 };
504 let source_fingerprints = options
505 .collect_source_fingerprints
506 .then(|| self.source_fingerprints());
507 Ok(ProjectAnalysisArtifacts {
508 dead_code: self.analyze_dead_code_with_artifacts(
509 options.retain_complexity_artifacts,
510 options.retain_graph,
511 )?,
512 duplication,
513 changed_files: options.changed_files,
514 source_fingerprints,
515 })
516 }
517
518 #[must_use]
520 pub fn find_duplicates_with_defaults(
521 &self,
522 config: &DuplicatesConfig,
523 cache_dir: Option<&Path>,
524 ) -> DuplicationAnalysis {
525 duplicates::find_duplicates_with_defaults(
526 &self.config.root,
527 self.files(),
528 config,
529 cache_dir,
530 )
531 }
532
533 #[must_use]
535 pub fn find_duplicates_touching_files_with_defaults(
536 &self,
537 config: &DuplicatesConfig,
538 changed_files: &[PathBuf],
539 cache_dir: Option<&Path>,
540 ) -> DuplicationAnalysis {
541 duplicates::find_duplicates_touching_files_with_defaults(
542 &self.config.root,
543 self.files(),
544 config,
545 changed_files,
546 cache_dir,
547 )
548 }
549
550 fn parse_modules(&self, need_complexity: bool) -> ParsedModules {
551 let fingerprints = source_fingerprints_for_files(self.files());
552 if let Some(fingerprints) = fingerprints.as_ref()
553 && let Some(modules) = self.cached_modules(need_complexity, fingerprints)
554 {
555 return ParsedModules {
556 modules,
557 metrics: core_backend::ParseMetrics {
558 parse_ms: 0.0,
559 cache_ms: 0.0,
560 cache_hits: 0,
561 cache_misses: 0,
562 parse_cpu_ms: 0.0,
563 },
564 };
565 }
566
567 let parsed = parse_files_with_config(&self.config, self.files(), need_complexity);
568 if let Some(fingerprints) = fingerprints
569 && let Ok(mut cache) = self.parsed_cache.lock()
570 {
571 *cache = Some(ParsedModuleCache {
572 need_complexity,
573 fingerprints,
574 modules: parsed.modules.clone(),
575 });
576 }
577 parsed
578 }
579
580 fn cached_modules(
581 &self,
582 need_complexity: bool,
583 fingerprints: &[SourceFingerprint],
584 ) -> Option<Vec<ModuleInfo>> {
585 let Ok(cache) = self.parsed_cache.lock() else {
586 return None;
587 };
588 let cache = cache.as_ref()?;
589 let complexity_mode_satisfies_request = cache.need_complexity || !need_complexity;
590 if complexity_mode_satisfies_request && cache.fingerprints == fingerprints {
591 return Some(cache.modules.clone());
592 }
593 None
594 }
595}
596
597fn merge_workspace_diagnostics(
598 primary: Vec<WorkspaceDiagnostic>,
599 secondary: Vec<WorkspaceDiagnostic>,
600) -> Vec<WorkspaceDiagnostic> {
601 let mut merged = Vec::with_capacity(primary.len() + secondary.len());
602 let mut seen: FxHashSet<(String, PathBuf)> = FxHashSet::default();
603 for diagnostic in primary.into_iter().chain(secondary) {
604 let key = (diagnostic.kind.id().to_owned(), diagnostic.path.clone());
605 if seen.insert(key) {
606 merged.push(diagnostic);
607 }
608 }
609 merged
610}
611
612struct ParsedModules {
613 modules: Vec<ModuleInfo>,
614 metrics: core_backend::ParseMetrics,
615}
616
617fn parse_files_with_config(
618 config: &ResolvedConfig,
619 files: &[DiscoveredFile],
620 need_complexity: bool,
621) -> ParsedModules {
622 let parse_start = Instant::now();
623 let cache_max_size_bytes = crate::project_config::resolve_cache_max_size_bytes(config);
624 let mut cache = if config.no_cache {
625 None
626 } else {
627 fallow_extract::cache::CacheStore::load(
628 &config.cache_dir,
629 config.cache_config_hash,
630 cache_max_size_bytes,
631 )
632 };
633 let parse_result = crate::source::parse_all_files(files, cache.as_ref(), need_complexity);
634 let parse_ms = parse_start.elapsed().as_secs_f64() * 1000.0;
635 let cache_ms = update_parse_cache_if_enabled(config, &mut cache, &parse_result.modules, files);
636 let metrics = core_backend::ParseMetrics {
637 parse_ms,
638 cache_ms,
639 cache_hits: parse_result.cache_hits,
640 cache_misses: parse_result.cache_misses,
641 parse_cpu_ms: parse_result.parse_cpu_ms,
642 };
643 ParsedModules {
644 modules: parse_result.modules,
645 metrics,
646 }
647}
648
649fn reused_parse_metrics() -> core_backend::ParseMetrics {
650 core_backend::ParseMetrics {
651 parse_ms: 0.0,
652 cache_ms: 0.0,
653 cache_hits: 0,
654 cache_misses: 0,
655 parse_cpu_ms: 0.0,
656 }
657}
658
659fn source_fingerprints_for_files(files: &[DiscoveredFile]) -> Option<Vec<SourceFingerprint>> {
660 files
661 .iter()
662 .map(|file| {
663 std::fs::metadata(&file.path)
664 .ok()
665 .map(|metadata| SourceFingerprint::from_metadata(&metadata))
666 .filter(|fingerprint| fingerprint.has_known_mtime())
667 })
668 .collect()
669}
670
671fn update_parse_cache_if_enabled(
672 config: &ResolvedConfig,
673 cache: &mut Option<fallow_extract::cache::CacheStore>,
674 modules: &[ModuleInfo],
675 files: &[DiscoveredFile],
676) -> f64 {
677 let start = Instant::now();
678 if config.no_cache {
679 return start.elapsed().as_secs_f64() * 1000.0;
680 }
681
682 let cache_max_size_bytes = crate::project_config::resolve_cache_max_size_bytes(config);
683 let store = cache.get_or_insert_with(fallow_extract::cache::CacheStore::new);
684 if update_parse_cache(store, modules, files)
685 && let Err(error) = store.save(
686 &config.cache_dir,
687 config.cache_config_hash,
688 cache_max_size_bytes,
689 )
690 {
691 tracing::warn!("Failed to save cache: {error}");
692 }
693 start.elapsed().as_secs_f64() * 1000.0
694}
695
696fn update_parse_cache(
697 store: &mut fallow_extract::cache::CacheStore,
698 modules: &[ModuleInfo],
699 files: &[DiscoveredFile],
700) -> bool {
701 let mut dirty = false;
702 for module in modules {
703 if let Some(file) = files.get(module.file_id.0 as usize) {
704 let fingerprint = source_fingerprint(&file.path);
705 if let Some(cached) = store.get_by_path_only(&file.path)
706 && cached.content_hash == module.content_hash
707 {
708 if cached.source_fingerprint() != fingerprint {
709 let preserved_last_access = cached.last_access_secs;
710 let mut refreshed =
711 fallow_extract::cache::module_to_cached(module, fingerprint);
712 refreshed.last_access_secs = preserved_last_access;
713 store.insert(&file.path, refreshed);
714 dirty = true;
715 }
716 continue;
717 }
718 store.insert(
719 &file.path,
720 fallow_extract::cache::module_to_cached(module, fingerprint),
721 );
722 dirty = true;
723 }
724 }
725 store.retain_paths(files) || dirty
726}
727
728fn source_fingerprint(path: &Path) -> SourceFingerprint {
729 std::fs::metadata(path).map_or_else(
730 |_| SourceFingerprint::new(0, 0),
731 |metadata| SourceFingerprint::from_metadata(&metadata),
732 )
733}
734
735struct EngineDeadCodePipelineInput<'a> {
736 config: &'a ResolvedConfig,
737 discovery: &'a crate::discover::AnalysisDiscovery,
738 modules: Vec<ModuleInfo>,
739 metrics: core_backend::ParseMetrics,
740 collect_usages: bool,
741 retain_graph: bool,
742 retain_modules: bool,
743 retain_files: bool,
744}
745
746fn run_engine_owned_dead_code_pipeline(
747 input: EngineDeadCodePipelineInput<'_>,
748) -> EngineResult<DeadCodeAnalysisArtifacts> {
749 let EngineDeadCodePipelineInput {
750 config,
751 discovery,
752 mut modules,
753 metrics,
754 collect_usages,
755 retain_graph,
756 retain_modules,
757 retain_files,
758 } = input;
759 let prelude = core_backend::prepare_dead_code_backend_prelude(config, discovery)?;
760 let prelude_timings = prelude.timings();
761 let entry_points = core_backend::discover_dead_code_entry_points(&prelude);
762 let (resolved, graph) = resolve_or_build_dead_code_graph(&prelude, &entry_points, &modules);
763
764 for module in &mut modules {
765 module.release_resolution_payload();
766 }
767
768 let detector = core_backend::run_dead_code_detectors(
769 &prelude,
770 &graph.graph,
771 &resolved.resolved,
772 &modules,
773 collect_usages,
774 &entry_points,
775 );
776 let profile =
777 core_backend::dead_code_pipeline_profile(core_backend::DeadCodePipelineProfileInput {
778 retain_timings: retain_graph,
779 prelude: &prelude,
780 prelude_timings,
781 parse_metrics: metrics,
782 module_count: modules.len(),
783 entry_points: &entry_points,
784 resolved: &resolved,
785 graph: &graph,
786 detector: &detector,
787 file_count: discovery.files().len(),
788 workspace_count: discovery.workspaces().len(),
789 });
790 let script_used_packages = prelude.script_used_packages();
791 prelude.finish();
792 let file_hashes = collect_file_hashes(&modules, discovery.files());
793
794 Ok(DeadCodeAnalysisArtifacts {
795 results: detector.results,
796 timings: profile.timings,
797 graph: retain_graph.then_some(graph.graph),
798 modules: retain_modules.then_some(modules),
799 files: retain_files.then(|| discovery.files().to_vec()),
800 script_used_packages,
801 file_hashes,
802 })
803}
804
805fn resolve_or_build_dead_code_graph(
806 prelude: &core_backend::DeadCodeBackendPrelude,
807 entry_points: &core_backend::DeadCodeEntryPoints,
808 modules: &[ModuleInfo],
809) -> (
810 core_backend::DeadCodeResolvedModules,
811 core_backend::DeadCodeGraphRun,
812) {
813 if let Some((resolved, graph)) =
814 core_backend::try_load_dead_code_graph_cache(prelude, entry_points, modules)
815 {
816 return (resolved, graph);
817 }
818
819 let resolved = core_backend::resolve_dead_code_imports(prelude, modules);
820 let graph =
821 core_backend::build_dead_code_graph(prelude, &resolved.resolved, entry_points, modules);
822 (resolved, graph)
823}
824
825fn collect_file_hashes(
826 modules: &[ModuleInfo],
827 files: &[DiscoveredFile],
828) -> FxHashMap<PathBuf, u64> {
829 modules
830 .iter()
831 .filter_map(|module| {
832 files
833 .get(module.file_id.0 as usize)
834 .map(|file| (file.path.clone(), module.content_hash))
835 })
836 .collect()
837}
838
839pub(crate) fn analyze_dead_code_with_parse_result_from_config(
840 config: &ResolvedConfig,
841 modules: &[ModuleInfo],
842) -> EngineResult<DeadCodeAnalysisArtifacts> {
843 let discovery = crate::discover::prepare_analysis_discovery(config);
844 run_engine_owned_dead_code_pipeline(EngineDeadCodePipelineInput {
845 config,
846 discovery: &discovery,
847 modules: modules.to_vec(),
848 metrics: reused_parse_metrics(),
849 collect_usages: true,
850 retain_graph: true,
851 retain_modules: false,
852 retain_files: false,
853 })
854}
855
856#[cfg(test)]
857mod tests {
858 use super::*;
859
860 #[test]
861 fn session_retains_workspace_metadata_from_config_load() {
862 let project = tempfile::tempdir().expect("project");
863 let root = project.path();
864 std::fs::write(
865 root.join("package.json"),
866 r#"{"name":"root","workspaces":["packages/*"]}"#,
867 )
868 .expect("write root package");
869 std::fs::create_dir_all(root.join("packages/a")).expect("create workspace");
870 std::fs::write(
871 root.join("packages/a/package.json"),
872 r#"{"name":"pkg-a","type":"module"}"#,
873 )
874 .expect("write workspace package");
875
876 let session = AnalysisSession::load(root, None).expect("session loads");
877
878 assert!(
879 session
880 .workspaces()
881 .iter()
882 .any(|workspace| workspace.name == "pkg-a"),
883 "session must retain workspace metadata discovered during config load"
884 );
885 }
886}