Skip to main content

fallow_engine/
results.rs

1//! Internal analysis result contracts re-exported through typed engine modules.
2
3#![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::time::Duration;
10
11use fallow_config::ResolvedConfig;
12use fallow_output::{HealthGrouping, HealthReport, HealthTimings};
13use fallow_types::discover::DiscoveredFile;
14use fallow_types::extract::ModuleInfo;
15use fallow_types::source_fingerprint::SourceFingerprint;
16use fallow_types::workspace::WorkspaceDiagnostic;
17use rustc_hash::{FxHashMap, FxHashSet};
18
19use crate::{duplicates, module_graph, trace};
20
21pub use crate::security::{derive_security_severity, security_catalogue_title};
22pub use fallow_types::output_dead_code::{
23    BoundaryCallViolationFinding, BoundaryCoverageViolationFinding, BoundaryViolationFinding,
24    CircularDependencyFinding, DuplicateExportFinding, DuplicatePropShapeFinding,
25    DynamicSegmentNameConflictFinding, EmptyCatalogGroupFinding, InvalidClientExportFinding,
26    MisconfiguredDependencyOverrideFinding, MisplacedDirectiveFinding,
27    MixedClientServerBarrelFinding, PolicyViolationFinding, PrivateTypeLeakFinding,
28    PropDrillingChainFinding, ReExportCycleFinding, RouteCollisionFinding,
29    TestOnlyDependencyFinding, ThinWrapperFinding, TypeOnlyDependencyFinding,
30    UnlistedDependencyFinding, UnprovidedInjectFinding, UnrenderedComponentFinding,
31    UnresolvedCatalogReferenceFinding, UnresolvedImportFinding, UnusedCatalogEntryFinding,
32    UnusedClassMemberFinding, UnusedComponentEmitFinding, UnusedComponentInputFinding,
33    UnusedComponentOutputFinding, UnusedComponentPropFinding, UnusedDependencyFinding,
34    UnusedDependencyOverrideFinding, UnusedDevDependencyFinding, UnusedEnumMemberFinding,
35    UnusedExportFinding, UnusedFileFinding, UnusedLoadDataKeyFinding,
36    UnusedOptionalDependencyFinding, UnusedServerActionFinding, UnusedStoreMemberFinding,
37    UnusedSvelteEventFinding, UnusedTypeFinding,
38};
39pub use fallow_types::results::{
40    ActiveSuppression, AnalysisResults, BoundaryCallViolation, BoundaryCoverageViolation,
41    BoundaryViolation, CircularDependency, CircularDependencyEdge, DependencyLocation,
42    DependencyOverrideMisconfigReason, DependencyOverrideSource, DuplicateExport,
43    DuplicateLocation, DuplicatePropShape, DuplicatePropShapeMember, DynamicSegmentNameConflict,
44    EmptyCatalogGroup, EntryPointSummary, ExportUsage, FeatureFlag, FlagConfidence, FlagKind,
45    ImportSite, InvalidClientExport, MisconfiguredDependencyOverride, MisplacedDirective,
46    MixedClientServerBarrel, PolicyRuleKind, PolicyViolation, PolicyViolationSeverity,
47    PrivateTypeLeak, PropDrillHop, PropDrillingChain, ReExportCycle, ReExportCycleKind,
48    ReactComponentIntel, ReactHookSummary, ReactPropDrill, ReactPropIntel, ReferenceLocation,
49    RenderFanInComponent, RenderFanInMetric, RouteCollision, SecurityAttackSurfaceEntry,
50    SecurityCandidate, SecurityCandidateBoundary, SecurityCandidateSink, SecurityDeadCodeContext,
51    SecurityDeadCodeKind, SecurityDefensiveBoundary, SecurityDefensiveControl, SecurityFinding,
52    SecurityFindingKind, SecurityNetworkContext, SecurityReachability, SecurityRuntimeContext,
53    SecurityRuntimeState, SecuritySeverity, SecurityTaintFlow, SecurityUnresolvedCalleeDiagnostic,
54    SecurityZoneCrossing, StaleSuppression, SuppressionOrigin, TaintConfidence, TaintEndpoint,
55    TaintPath, TestOnlyDependency, ThinWrapper, TraceHop, TraceHopRole, TypeOnlyDependency,
56    UnlistedDependency, UnprovidedInject, UnrenderedComponent, UnresolvedCatalogReference,
57    UnresolvedImport, UnusedCatalogEntry, UnusedComponentEmit, UnusedComponentInput,
58    UnusedComponentOutput, UnusedComponentProp, UnusedDependency, UnusedDependencyOverride,
59    UnusedExport, UnusedFile, UnusedLoadDataKey, UnusedMember, UnusedServerAction,
60    UnusedSvelteEvent,
61};
62
63/// Typed dead-code analysis result.
64#[derive(Debug)]
65pub struct DeadCodeAnalysis {
66    pub results: AnalysisResults,
67}
68
69/// Typed dead-code analysis result with per-file source hashes.
70#[derive(Debug)]
71pub struct DeadCodeAnalysisWithHashes {
72    pub results: AnalysisResults,
73    pub file_hashes: FxHashMap<PathBuf, u64>,
74}
75
76/// Typed dead-code analysis result with retained parser artifacts.
77#[derive(Debug)]
78pub struct DeadCodeAnalysisOutput {
79    pub results: AnalysisResults,
80    pub modules: Option<Vec<ModuleInfo>>,
81    pub files: Option<Vec<DiscoveredFile>>,
82}
83
84/// Typed dead-code analysis result with all reusable pipeline artifacts.
85#[derive(Debug)]
86pub struct DeadCodeAnalysisArtifacts {
87    pub results: AnalysisResults,
88    pub timings: Option<trace::PipelineTimings>,
89    pub graph: Option<module_graph::RetainedModuleGraph>,
90    pub modules: Option<Vec<ModuleInfo>>,
91    pub files: Option<Vec<DiscoveredFile>>,
92    pub script_used_packages: FxHashSet<String>,
93    pub file_hashes: FxHashMap<PathBuf, u64>,
94}
95
96/// Typed project analysis result combining dead-code and duplication outputs.
97#[derive(Debug)]
98pub struct ProjectAnalysisOutput {
99    pub dead_code: DeadCodeAnalysisOutput,
100    pub duplication: duplicates::DuplicationReport,
101}
102
103/// Typed project analysis result with reusable session artifacts.
104#[derive(Debug)]
105pub struct ProjectAnalysisArtifacts {
106    pub dead_code: DeadCodeAnalysisArtifacts,
107    pub duplication: duplicates::DuplicationReport,
108    pub changed_files: Option<FxHashSet<PathBuf>>,
109    pub source_fingerprints: Option<FxHashMap<PathBuf, SourceFingerprint>>,
110}
111
112impl ProjectAnalysisArtifacts {
113    /// Drop retained reuse-only artifacts and return the stable project output.
114    #[must_use]
115    pub fn into_output(self) -> ProjectAnalysisOutput {
116        ProjectAnalysisOutput {
117            dead_code: DeadCodeAnalysisOutput {
118                results: self.dead_code.results,
119                modules: self.dead_code.modules,
120                files: self.dead_code.files,
121            },
122            duplication: self.duplication,
123        }
124    }
125}
126
127/// Typed duplication analysis result.
128#[derive(Debug)]
129pub struct DuplicationAnalysis {
130    pub report: duplicates::DuplicationReport,
131    pub default_ignore_skips: duplicates::DefaultIgnoreSkips,
132}
133
134/// Typed health analysis result shared by CLI, API, NAPI, and future embedders.
135///
136/// The result contract belongs at the engine boundary so downstream callers can
137/// depend on a command-neutral shape.
138#[derive(Debug)]
139pub struct HealthAnalysisResult<GroupResolver = ()> {
140    pub report: HealthReport,
141    /// Per-group health output when grouping is active.
142    ///
143    /// `None` for the default run; `Some` for any grouped invocation. The
144    /// top-level report reflects the active run scope; consumers that want
145    /// per-group metrics read from `grouping.groups`.
146    pub grouping: Option<HealthGrouping>,
147    /// Optional grouping resolver retained by callers that need to tag findings
148    /// after analysis without rediscovering ownership or package metadata.
149    pub group_resolver: Option<GroupResolver>,
150    pub config: ResolvedConfig,
151    pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
152    pub elapsed: Duration,
153    pub timings: Option<HealthTimings>,
154    pub coverage_gaps_has_findings: bool,
155    pub should_fail_on_coverage_gaps: bool,
156}
157
158impl<GroupResolver> HealthAnalysisResult<GroupResolver> {
159    /// Drop presentation-only grouping resolver state while preserving the
160    /// command-neutral health analysis payload.
161    #[must_use]
162    pub fn without_group_resolver(self) -> HealthAnalysisResult<()> {
163        HealthAnalysisResult {
164            report: self.report,
165            grouping: self.grouping,
166            group_resolver: None,
167            config: self.config,
168            workspace_diagnostics: self.workspace_diagnostics,
169            elapsed: self.elapsed,
170            timings: self.timings,
171            coverage_gaps_has_findings: self.coverage_gaps_has_findings,
172            should_fail_on_coverage_gaps: self.should_fail_on_coverage_gaps,
173        }
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use crate::project_config::{ProjectConfigOptions, config_for_project_analysis};
180    use fallow_config::ProductionAnalysis;
181    use fallow_types::output_format::OutputFormat;
182
183    use super::*;
184
185    #[test]
186    fn health_analysis_result_drops_presentation_resolver() {
187        let project = tempfile::tempdir().expect("temp dir");
188        let project_config = config_for_project_analysis(
189            project.path(),
190            None,
191            ProjectConfigOptions {
192                output: OutputFormat::Json,
193                no_cache: true,
194                threads: 1,
195                production_override: None,
196                quiet: true,
197                analysis: ProductionAnalysis::Health,
198            },
199        )
200        .expect("project config loads");
201        let result = HealthAnalysisResult {
202            report: HealthReport::default(),
203            grouping: None,
204            group_resolver: Some("resolver"),
205            config: project_config.config,
206            workspace_diagnostics: Vec::new(),
207            elapsed: Duration::from_millis(7),
208            timings: None,
209            coverage_gaps_has_findings: true,
210            should_fail_on_coverage_gaps: true,
211        };
212
213        let neutral = result.without_group_resolver();
214
215        assert!(neutral.group_resolver.is_none());
216        assert_eq!(neutral.elapsed, Duration::from_millis(7));
217        assert!(neutral.coverage_gaps_has_findings);
218        assert!(neutral.should_fail_on_coverage_gaps);
219    }
220
221    #[test]
222    fn engine_result_surface_uses_explicit_reexports() {
223        let source = include_str!("results.rs");
224        let output_dead_code_wildcard = concat!("pub use fallow_types::output_dead_code::", "*");
225        let results_wildcard = concat!("pub use fallow_types::results::", "*");
226
227        assert!(!source.contains(output_dead_code_wildcard));
228        assert!(!source.contains(results_wildcard));
229    }
230}