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::{
23 derive_security_severity, enable_security_rules, security_catalogue_title, security_finding_id,
24 security_rule_id,
25};
26pub use fallow_types::output_dead_code::{
27 BoundaryCallViolationFinding, BoundaryCoverageViolationFinding, BoundaryViolationFinding,
28 CircularDependencyFinding, DuplicateExportFinding, DuplicatePropShapeFinding,
29 DynamicSegmentNameConflictFinding, EmptyCatalogGroupFinding, InvalidClientExportFinding,
30 MisconfiguredDependencyOverrideFinding, MisplacedDirectiveFinding,
31 MixedClientServerBarrelFinding, PolicyViolationFinding, PrivateTypeLeakFinding,
32 PropDrillingChainFinding, ReExportCycleFinding, RouteCollisionFinding,
33 TestOnlyDependencyFinding, ThinWrapperFinding, TypeOnlyDependencyFinding,
34 UnlistedDependencyFinding, UnprovidedInjectFinding, UnrenderedComponentFinding,
35 UnresolvedCatalogReferenceFinding, UnresolvedImportFinding, UnusedCatalogEntryFinding,
36 UnusedClassMemberFinding, UnusedComponentEmitFinding, UnusedComponentInputFinding,
37 UnusedComponentOutputFinding, UnusedComponentPropFinding, UnusedDependencyFinding,
38 UnusedDependencyOverrideFinding, UnusedDevDependencyFinding, UnusedEnumMemberFinding,
39 UnusedExportFinding, UnusedFileFinding, UnusedLoadDataKeyFinding,
40 UnusedOptionalDependencyFinding, UnusedServerActionFinding, UnusedStoreMemberFinding,
41 UnusedSvelteEventFinding, UnusedTypeFinding,
42};
43pub use fallow_types::results::{
44 ActiveSuppression, AnalysisResults, BoundaryCallViolation, BoundaryCoverageViolation,
45 BoundaryViolation, CircularDependency, CircularDependencyEdge, DependencyLocation,
46 DependencyOverrideMisconfigReason, DependencyOverrideSource, DuplicateExport,
47 DuplicateLocation, DuplicatePropShape, DuplicatePropShapeMember, DynamicSegmentNameConflict,
48 EmptyCatalogGroup, EntryPointSummary, ExportUsage, FeatureFlag, FlagConfidence, FlagKind,
49 ImportSite, InvalidClientExport, MisconfiguredDependencyOverride, MisplacedDirective,
50 MixedClientServerBarrel, PolicyRuleKind, PolicyViolation, PolicyViolationSeverity,
51 PrivateTypeLeak, PropDrillHop, PropDrillingChain, ReExportCycle, ReExportCycleKind,
52 ReactComponentIntel, ReactHookSummary, ReactPropDrill, ReactPropIntel, ReferenceLocation,
53 RenderFanInComponent, RenderFanInMetric, RouteCollision, SecurityAttackSurfaceEntry,
54 SecurityCandidate, SecurityCandidateBoundary, SecurityCandidateSink, SecurityDeadCodeContext,
55 SecurityDeadCodeKind, SecurityDefensiveBoundary, SecurityDefensiveControl, SecurityFinding,
56 SecurityFindingKind, SecurityNetworkContext, SecurityReachability, SecurityRuntimeContext,
57 SecurityRuntimeState, SecuritySeverity, SecurityTaintFlow, SecurityUnresolvedCalleeDiagnostic,
58 SecurityZoneCrossing, StaleSuppression, SuppressionOrigin, TaintConfidence, TaintEndpoint,
59 TaintPath, TestOnlyDependency, ThinWrapper, TraceHop, TraceHopRole, TypeOnlyDependency,
60 UnlistedDependency, UnprovidedInject, UnrenderedComponent, UnresolvedCatalogReference,
61 UnresolvedImport, UnusedCatalogEntry, UnusedComponentEmit, UnusedComponentInput,
62 UnusedComponentOutput, UnusedComponentProp, UnusedDependency, UnusedDependencyOverride,
63 UnusedExport, UnusedFile, UnusedLoadDataKey, UnusedMember, UnusedServerAction,
64 UnusedSvelteEvent,
65};
66
67#[derive(Debug)]
69pub struct DeadCodeAnalysis {
70 pub results: AnalysisResults,
72}
73
74#[derive(Debug)]
76pub struct DeadCodeAnalysisWithHashes {
77 pub results: AnalysisResults,
79 pub file_hashes: FxHashMap<PathBuf, u64>,
81}
82
83#[derive(Debug)]
85pub struct DeadCodeAnalysisOutput {
86 pub results: AnalysisResults,
88 pub modules: Option<Vec<ModuleInfo>>,
90 pub files: Option<Vec<DiscoveredFile>>,
92}
93
94#[derive(Debug)]
96pub struct DeadCodeAnalysisArtifacts {
97 pub results: AnalysisResults,
99 pub timings: Option<trace::PipelineTimings>,
101 pub graph: Option<module_graph::RetainedModuleGraph>,
103 pub modules: Option<Vec<ModuleInfo>>,
105 pub files: Option<Vec<DiscoveredFile>>,
107 pub script_used_packages: FxHashSet<String>,
110 pub trace_provenance: trace::TraceProvenance,
113 pub file_hashes: FxHashMap<PathBuf, u64>,
115}
116
117#[doc(hidden)]
123#[derive(Debug)]
124pub struct SharedDeadCodeAnalysisArtifacts {
125 pub results: AnalysisResults,
126 pub timings: Option<trace::PipelineTimings>,
127 pub graph: Option<module_graph::RetainedModuleGraph>,
128 pub modules: Option<Arc<[ModuleInfo]>>,
129 pub files: Option<Vec<DiscoveredFile>>,
130 pub script_used_packages: FxHashSet<String>,
131 pub trace_provenance: trace::TraceProvenance,
132 pub file_hashes: FxHashMap<PathBuf, u64>,
133}
134
135impl SharedDeadCodeAnalysisArtifacts {
136 #[must_use]
138 pub fn into_owned(self) -> DeadCodeAnalysisArtifacts {
139 let modules = self.modules.map(|modules| {
140 let mut owned = modules.to_vec();
141 for module in &mut owned {
142 module.release_resolution_payload();
143 }
144 owned
145 });
146 DeadCodeAnalysisArtifacts {
147 results: self.results,
148 timings: self.timings,
149 graph: self.graph,
150 modules,
151 files: self.files,
152 script_used_packages: self.script_used_packages,
153 trace_provenance: self.trace_provenance,
154 file_hashes: self.file_hashes,
155 }
156 }
157}
158
159#[derive(Debug)]
161pub struct ProjectAnalysisOutput {
162 pub dead_code: DeadCodeAnalysisOutput,
164 pub duplication: duplicates::DuplicationReport,
166}
167
168#[derive(Debug)]
170pub struct ProjectAnalysisArtifacts {
171 pub dead_code: DeadCodeAnalysisArtifacts,
173 pub duplication: duplicates::DuplicationReport,
175 pub changed_files: Option<FxHashSet<PathBuf>>,
177 pub source_fingerprints: Option<FxHashMap<PathBuf, SourceFingerprint>>,
179}
180
181impl ProjectAnalysisArtifacts {
182 #[must_use]
184 pub fn into_output(self) -> ProjectAnalysisOutput {
185 ProjectAnalysisOutput {
186 dead_code: DeadCodeAnalysisOutput {
187 results: self.dead_code.results,
188 modules: self.dead_code.modules,
189 files: self.dead_code.files,
190 },
191 duplication: self.duplication,
192 }
193 }
194}
195
196#[derive(Debug)]
198pub struct DuplicationAnalysis {
199 pub report: duplicates::DuplicationReport,
200 pub default_ignore_skips: duplicates::DefaultIgnoreSkips,
201}
202
203#[derive(Debug)]
208pub struct HealthAnalysisResult<GroupResolver = ()> {
209 pub report: HealthReport,
211 pub type_aware_meta: Option<fallow_types::envelope::TypeAwareMeta>,
213 pub branching_by_file: crate::health::BranchingByFile,
220 pub grouping: Option<HealthGrouping>,
226 pub group_resolver: Option<GroupResolver>,
229 pub config: ResolvedConfig,
231 pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
233 pub elapsed: Duration,
235 pub timings: Option<HealthTimings>,
237 pub coverage_gaps_has_findings: bool,
239 pub should_fail_on_coverage_gaps: bool,
242 pub changed_files_analyzed: Option<Vec<std::path::PathBuf>>,
247}
248
249impl<GroupResolver> HealthAnalysisResult<GroupResolver> {
250 #[must_use]
253 pub fn without_group_resolver(self) -> HealthAnalysisResult<()> {
254 HealthAnalysisResult {
255 report: self.report,
256 type_aware_meta: self.type_aware_meta,
257 branching_by_file: self.branching_by_file,
258 grouping: self.grouping,
259 group_resolver: None,
260 config: self.config,
261 workspace_diagnostics: self.workspace_diagnostics,
262 elapsed: self.elapsed,
263 timings: self.timings,
264 coverage_gaps_has_findings: self.coverage_gaps_has_findings,
265 should_fail_on_coverage_gaps: self.should_fail_on_coverage_gaps,
266 changed_files_analyzed: self.changed_files_analyzed,
267 }
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use crate::project_config::{ProjectConfigOptions, config_for_project_analysis};
274 use fallow_config::ProductionAnalysis;
275 use fallow_types::output_format::OutputFormat;
276
277 use super::*;
278
279 #[test]
280 fn health_analysis_result_drops_presentation_resolver() {
281 let project = tempfile::tempdir().expect("temp dir");
282 let project_config = config_for_project_analysis(
283 project.path(),
284 None,
285 ProjectConfigOptions {
286 output: OutputFormat::Json,
287 no_cache: true,
288 threads: 1,
289 production_override: None,
290 quiet: true,
291 analysis: ProductionAnalysis::Health,
292 allow_remote_extends: false,
293 },
294 )
295 .expect("project config loads");
296 let result = HealthAnalysisResult {
297 report: HealthReport::default(),
298 branching_by_file: crate::health::BranchingByFile::default(),
299 grouping: None,
300 group_resolver: Some("resolver"),
301 config: project_config.config,
302 workspace_diagnostics: Vec::new(),
303 elapsed: Duration::from_millis(7),
304 timings: None,
305 coverage_gaps_has_findings: true,
306 should_fail_on_coverage_gaps: true,
307 type_aware_meta: None,
308 changed_files_analyzed: None,
309 };
310
311 let neutral = result.without_group_resolver();
312
313 assert!(neutral.group_resolver.is_none());
314 assert_eq!(neutral.elapsed, Duration::from_millis(7));
315 assert!(neutral.coverage_gaps_has_findings);
316 assert!(neutral.should_fail_on_coverage_gaps);
317 }
318
319 #[test]
320 fn engine_result_surface_uses_explicit_reexports() {
321 let source = include_str!("results.rs");
322 let output_dead_code_wildcard = concat!("pub use fallow_types::output_dead_code::", "*");
323 let results_wildcard = concat!("pub use fallow_types::results::", "*");
324
325 assert!(!source.contains(output_dead_code_wildcard));
326 assert!(!source.contains(results_wildcard));
327 }
328}