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,
69}
70
71#[derive(Debug)]
73pub struct DeadCodeAnalysisWithHashes {
74 pub results: AnalysisResults,
76 pub file_hashes: FxHashMap<PathBuf, u64>,
78}
79
80#[derive(Debug)]
82pub struct DeadCodeAnalysisOutput {
83 pub results: AnalysisResults,
85 pub modules: Option<Vec<ModuleInfo>>,
87 pub files: Option<Vec<DiscoveredFile>>,
89}
90
91#[derive(Debug)]
93pub struct DeadCodeAnalysisArtifacts {
94 pub results: AnalysisResults,
96 pub timings: Option<trace::PipelineTimings>,
98 pub graph: Option<module_graph::RetainedModuleGraph>,
100 pub modules: Option<Vec<ModuleInfo>>,
102 pub files: Option<Vec<DiscoveredFile>>,
104 pub script_used_packages: FxHashSet<String>,
107 pub file_hashes: FxHashMap<PathBuf, u64>,
109}
110
111#[doc(hidden)]
117#[derive(Debug)]
118pub struct SharedDeadCodeAnalysisArtifacts {
119 pub results: AnalysisResults,
120 pub timings: Option<trace::PipelineTimings>,
121 pub graph: Option<module_graph::RetainedModuleGraph>,
122 pub modules: Option<Arc<[ModuleInfo]>>,
123 pub files: Option<Vec<DiscoveredFile>>,
124 pub script_used_packages: FxHashSet<String>,
125 pub file_hashes: FxHashMap<PathBuf, u64>,
126}
127
128impl SharedDeadCodeAnalysisArtifacts {
129 #[must_use]
131 pub fn into_owned(self) -> DeadCodeAnalysisArtifacts {
132 let modules = self.modules.map(|modules| {
133 let mut owned = modules.to_vec();
134 for module in &mut owned {
135 module.release_resolution_payload();
136 }
137 owned
138 });
139 DeadCodeAnalysisArtifacts {
140 results: self.results,
141 timings: self.timings,
142 graph: self.graph,
143 modules,
144 files: self.files,
145 script_used_packages: self.script_used_packages,
146 file_hashes: self.file_hashes,
147 }
148 }
149}
150
151#[derive(Debug)]
153pub struct ProjectAnalysisOutput {
154 pub dead_code: DeadCodeAnalysisOutput,
156 pub duplication: duplicates::DuplicationReport,
158}
159
160#[derive(Debug)]
162pub struct ProjectAnalysisArtifacts {
163 pub dead_code: DeadCodeAnalysisArtifacts,
165 pub duplication: duplicates::DuplicationReport,
167 pub changed_files: Option<FxHashSet<PathBuf>>,
169 pub source_fingerprints: Option<FxHashMap<PathBuf, SourceFingerprint>>,
171}
172
173impl ProjectAnalysisArtifacts {
174 #[must_use]
176 pub fn into_output(self) -> ProjectAnalysisOutput {
177 ProjectAnalysisOutput {
178 dead_code: DeadCodeAnalysisOutput {
179 results: self.dead_code.results,
180 modules: self.dead_code.modules,
181 files: self.dead_code.files,
182 },
183 duplication: self.duplication,
184 }
185 }
186}
187
188#[derive(Debug)]
190pub struct DuplicationAnalysis {
191 pub report: duplicates::DuplicationReport,
192 pub default_ignore_skips: duplicates::DefaultIgnoreSkips,
193}
194
195#[derive(Debug)]
200pub struct HealthAnalysisResult<GroupResolver = ()> {
201 pub report: HealthReport,
203 pub type_aware_meta: Option<fallow_types::envelope::TypeAwareMeta>,
205 pub grouping: Option<HealthGrouping>,
211 pub group_resolver: Option<GroupResolver>,
214 pub config: ResolvedConfig,
216 pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
218 pub elapsed: Duration,
220 pub timings: Option<HealthTimings>,
222 pub coverage_gaps_has_findings: bool,
224 pub should_fail_on_coverage_gaps: bool,
227}
228
229impl<GroupResolver> HealthAnalysisResult<GroupResolver> {
230 #[must_use]
233 pub fn without_group_resolver(self) -> HealthAnalysisResult<()> {
234 HealthAnalysisResult {
235 report: self.report,
236 type_aware_meta: self.type_aware_meta,
237 grouping: self.grouping,
238 group_resolver: None,
239 config: self.config,
240 workspace_diagnostics: self.workspace_diagnostics,
241 elapsed: self.elapsed,
242 timings: self.timings,
243 coverage_gaps_has_findings: self.coverage_gaps_has_findings,
244 should_fail_on_coverage_gaps: self.should_fail_on_coverage_gaps,
245 }
246 }
247}
248
249#[cfg(test)]
250mod tests {
251 use crate::project_config::{ProjectConfigOptions, config_for_project_analysis};
252 use fallow_config::ProductionAnalysis;
253 use fallow_types::output_format::OutputFormat;
254
255 use super::*;
256
257 #[test]
258 fn health_analysis_result_drops_presentation_resolver() {
259 let project = tempfile::tempdir().expect("temp dir");
260 let project_config = config_for_project_analysis(
261 project.path(),
262 None,
263 ProjectConfigOptions {
264 output: OutputFormat::Json,
265 no_cache: true,
266 threads: 1,
267 production_override: None,
268 quiet: true,
269 analysis: ProductionAnalysis::Health,
270 allow_remote_extends: false,
271 },
272 )
273 .expect("project config loads");
274 let result = HealthAnalysisResult {
275 report: HealthReport::default(),
276 grouping: None,
277 group_resolver: Some("resolver"),
278 config: project_config.config,
279 workspace_diagnostics: Vec::new(),
280 elapsed: Duration::from_millis(7),
281 timings: None,
282 coverage_gaps_has_findings: true,
283 should_fail_on_coverage_gaps: true,
284 type_aware_meta: None,
285 };
286
287 let neutral = result.without_group_resolver();
288
289 assert!(neutral.group_resolver.is_none());
290 assert_eq!(neutral.elapsed, Duration::from_millis(7));
291 assert!(neutral.coverage_gaps_has_findings);
292 assert!(neutral.should_fail_on_coverage_gaps);
293 }
294
295 #[test]
296 fn engine_result_surface_uses_explicit_reexports() {
297 let source = include_str!("results.rs");
298 let output_dead_code_wildcard = concat!("pub use fallow_types::output_dead_code::", "*");
299 let results_wildcard = concat!("pub use fallow_types::results::", "*");
300
301 assert!(!source.contains(output_dead_code_wildcard));
302 assert!(!source.contains(results_wildcard));
303 }
304}