1#![allow(
4 unused_imports,
5 reason = "private result contract aggregation re-exports types consumed through typed engine modules"
6)]
7
8use std::path::PathBuf;
9use std::sync::Arc;
10use std::time::Duration;
11
12use fallow_config::ResolvedConfig;
13use fallow_output::{HealthGrouping, HealthReport, HealthTimings};
14use fallow_types::discover::DiscoveredFile;
15use fallow_types::extract::ModuleInfo;
16use fallow_types::source_fingerprint::SourceFingerprint;
17use fallow_types::workspace::WorkspaceDiagnostic;
18use rustc_hash::{FxHashMap, FxHashSet};
19
20use crate::{duplicates, module_graph, trace};
21
22pub use crate::security::{derive_security_severity, security_catalogue_title};
23pub use fallow_types::output_dead_code::{
24 BoundaryCallViolationFinding, BoundaryCoverageViolationFinding, BoundaryViolationFinding,
25 CircularDependencyFinding, DuplicateExportFinding, DuplicatePropShapeFinding,
26 DynamicSegmentNameConflictFinding, EmptyCatalogGroupFinding, InvalidClientExportFinding,
27 MisconfiguredDependencyOverrideFinding, MisplacedDirectiveFinding,
28 MixedClientServerBarrelFinding, PolicyViolationFinding, PrivateTypeLeakFinding,
29 PropDrillingChainFinding, ReExportCycleFinding, RouteCollisionFinding,
30 TestOnlyDependencyFinding, ThinWrapperFinding, TypeOnlyDependencyFinding,
31 UnlistedDependencyFinding, UnprovidedInjectFinding, UnrenderedComponentFinding,
32 UnresolvedCatalogReferenceFinding, UnresolvedImportFinding, UnusedCatalogEntryFinding,
33 UnusedClassMemberFinding, UnusedComponentEmitFinding, UnusedComponentInputFinding,
34 UnusedComponentOutputFinding, UnusedComponentPropFinding, UnusedDependencyFinding,
35 UnusedDependencyOverrideFinding, UnusedDevDependencyFinding, UnusedEnumMemberFinding,
36 UnusedExportFinding, UnusedFileFinding, UnusedLoadDataKeyFinding,
37 UnusedOptionalDependencyFinding, UnusedServerActionFinding, UnusedStoreMemberFinding,
38 UnusedSvelteEventFinding, UnusedTypeFinding,
39};
40pub use fallow_types::results::{
41 ActiveSuppression, AnalysisResults, BoundaryCallViolation, BoundaryCoverageViolation,
42 BoundaryViolation, CircularDependency, CircularDependencyEdge, DependencyLocation,
43 DependencyOverrideMisconfigReason, DependencyOverrideSource, DuplicateExport,
44 DuplicateLocation, DuplicatePropShape, DuplicatePropShapeMember, DynamicSegmentNameConflict,
45 EmptyCatalogGroup, EntryPointSummary, ExportUsage, FeatureFlag, FlagConfidence, FlagKind,
46 ImportSite, InvalidClientExport, MisconfiguredDependencyOverride, MisplacedDirective,
47 MixedClientServerBarrel, PolicyRuleKind, PolicyViolation, PolicyViolationSeverity,
48 PrivateTypeLeak, PropDrillHop, PropDrillingChain, ReExportCycle, ReExportCycleKind,
49 ReactComponentIntel, ReactHookSummary, ReactPropDrill, ReactPropIntel, ReferenceLocation,
50 RenderFanInComponent, RenderFanInMetric, RouteCollision, SecurityAttackSurfaceEntry,
51 SecurityCandidate, SecurityCandidateBoundary, SecurityCandidateSink, SecurityDeadCodeContext,
52 SecurityDeadCodeKind, SecurityDefensiveBoundary, SecurityDefensiveControl, SecurityFinding,
53 SecurityFindingKind, SecurityNetworkContext, SecurityReachability, SecurityRuntimeContext,
54 SecurityRuntimeState, SecuritySeverity, SecurityTaintFlow, SecurityUnresolvedCalleeDiagnostic,
55 SecurityZoneCrossing, StaleSuppression, SuppressionOrigin, TaintConfidence, TaintEndpoint,
56 TaintPath, TestOnlyDependency, ThinWrapper, TraceHop, TraceHopRole, TypeOnlyDependency,
57 UnlistedDependency, UnprovidedInject, UnrenderedComponent, UnresolvedCatalogReference,
58 UnresolvedImport, UnusedCatalogEntry, UnusedComponentEmit, UnusedComponentInput,
59 UnusedComponentOutput, UnusedComponentProp, UnusedDependency, UnusedDependencyOverride,
60 UnusedExport, UnusedFile, UnusedLoadDataKey, UnusedMember, UnusedServerAction,
61 UnusedSvelteEvent,
62};
63
64#[derive(Debug)]
66pub struct DeadCodeAnalysis {
67 pub results: AnalysisResults,
68}
69
70#[derive(Debug)]
72pub struct DeadCodeAnalysisWithHashes {
73 pub results: AnalysisResults,
74 pub file_hashes: FxHashMap<PathBuf, u64>,
75}
76
77#[derive(Debug)]
79pub struct DeadCodeAnalysisOutput {
80 pub results: AnalysisResults,
81 pub modules: Option<Vec<ModuleInfo>>,
82 pub files: Option<Vec<DiscoveredFile>>,
83}
84
85#[derive(Debug)]
87pub struct DeadCodeAnalysisArtifacts {
88 pub results: AnalysisResults,
89 pub timings: Option<trace::PipelineTimings>,
90 pub graph: Option<module_graph::RetainedModuleGraph>,
91 pub modules: Option<Vec<ModuleInfo>>,
92 pub files: Option<Vec<DiscoveredFile>>,
93 pub script_used_packages: FxHashSet<String>,
94 pub file_hashes: FxHashMap<PathBuf, u64>,
95}
96
97#[doc(hidden)]
103#[derive(Debug)]
104pub struct SharedDeadCodeAnalysisArtifacts {
105 pub results: AnalysisResults,
106 pub timings: Option<trace::PipelineTimings>,
107 pub graph: Option<module_graph::RetainedModuleGraph>,
108 pub modules: Option<Arc<[ModuleInfo]>>,
109 pub files: Option<Vec<DiscoveredFile>>,
110 pub script_used_packages: FxHashSet<String>,
111 pub file_hashes: FxHashMap<PathBuf, u64>,
112}
113
114impl SharedDeadCodeAnalysisArtifacts {
115 #[must_use]
117 pub fn into_owned(self) -> DeadCodeAnalysisArtifacts {
118 let modules = self.modules.map(|modules| {
119 let mut owned = modules.to_vec();
120 for module in &mut owned {
121 module.release_resolution_payload();
122 }
123 owned
124 });
125 DeadCodeAnalysisArtifacts {
126 results: self.results,
127 timings: self.timings,
128 graph: self.graph,
129 modules,
130 files: self.files,
131 script_used_packages: self.script_used_packages,
132 file_hashes: self.file_hashes,
133 }
134 }
135}
136
137#[derive(Debug)]
139pub struct ProjectAnalysisOutput {
140 pub dead_code: DeadCodeAnalysisOutput,
141 pub duplication: duplicates::DuplicationReport,
142}
143
144#[derive(Debug)]
146pub struct ProjectAnalysisArtifacts {
147 pub dead_code: DeadCodeAnalysisArtifacts,
148 pub duplication: duplicates::DuplicationReport,
149 pub changed_files: Option<FxHashSet<PathBuf>>,
150 pub source_fingerprints: Option<FxHashMap<PathBuf, SourceFingerprint>>,
151}
152
153impl ProjectAnalysisArtifacts {
154 #[must_use]
156 pub fn into_output(self) -> ProjectAnalysisOutput {
157 ProjectAnalysisOutput {
158 dead_code: DeadCodeAnalysisOutput {
159 results: self.dead_code.results,
160 modules: self.dead_code.modules,
161 files: self.dead_code.files,
162 },
163 duplication: self.duplication,
164 }
165 }
166}
167
168#[derive(Debug)]
170pub struct DuplicationAnalysis {
171 pub report: duplicates::DuplicationReport,
172 pub default_ignore_skips: duplicates::DefaultIgnoreSkips,
173}
174
175#[derive(Debug)]
180pub struct HealthAnalysisResult<GroupResolver = ()> {
181 pub report: HealthReport,
182 pub grouping: Option<HealthGrouping>,
188 pub group_resolver: Option<GroupResolver>,
191 pub config: ResolvedConfig,
192 pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
193 pub elapsed: Duration,
194 pub timings: Option<HealthTimings>,
195 pub coverage_gaps_has_findings: bool,
196 pub should_fail_on_coverage_gaps: bool,
197}
198
199impl<GroupResolver> HealthAnalysisResult<GroupResolver> {
200 #[must_use]
203 pub fn without_group_resolver(self) -> HealthAnalysisResult<()> {
204 HealthAnalysisResult {
205 report: self.report,
206 grouping: self.grouping,
207 group_resolver: None,
208 config: self.config,
209 workspace_diagnostics: self.workspace_diagnostics,
210 elapsed: self.elapsed,
211 timings: self.timings,
212 coverage_gaps_has_findings: self.coverage_gaps_has_findings,
213 should_fail_on_coverage_gaps: self.should_fail_on_coverage_gaps,
214 }
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use crate::project_config::{ProjectConfigOptions, config_for_project_analysis};
221 use fallow_config::ProductionAnalysis;
222 use fallow_types::output_format::OutputFormat;
223
224 use super::*;
225
226 #[test]
227 fn health_analysis_result_drops_presentation_resolver() {
228 let project = tempfile::tempdir().expect("temp dir");
229 let project_config = config_for_project_analysis(
230 project.path(),
231 None,
232 ProjectConfigOptions {
233 output: OutputFormat::Json,
234 no_cache: true,
235 threads: 1,
236 production_override: None,
237 quiet: true,
238 analysis: ProductionAnalysis::Health,
239 allow_remote_extends: false,
240 },
241 )
242 .expect("project config loads");
243 let result = HealthAnalysisResult {
244 report: HealthReport::default(),
245 grouping: None,
246 group_resolver: Some("resolver"),
247 config: project_config.config,
248 workspace_diagnostics: Vec::new(),
249 elapsed: Duration::from_millis(7),
250 timings: None,
251 coverage_gaps_has_findings: true,
252 should_fail_on_coverage_gaps: true,
253 };
254
255 let neutral = result.without_group_resolver();
256
257 assert!(neutral.group_resolver.is_none());
258 assert_eq!(neutral.elapsed, Duration::from_millis(7));
259 assert!(neutral.coverage_gaps_has_findings);
260 assert!(neutral.should_fail_on_coverage_gaps);
261 }
262
263 #[test]
264 fn engine_result_surface_uses_explicit_reexports() {
265 let source = include_str!("results.rs");
266 let output_dead_code_wildcard = concat!("pub use fallow_types::output_dead_code::", "*");
267 let results_wildcard = concat!("pub use fallow_types::results::", "*");
268
269 assert!(!source.contains(output_dead_code_wildcard));
270 assert!(!source.contains(results_wildcard));
271 }
272}