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::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/// Typed dead-code analysis result.
65#[derive(Debug)]
66pub struct DeadCodeAnalysis {
67    pub results: AnalysisResults,
68}
69
70/// Typed dead-code analysis result with per-file source hashes.
71#[derive(Debug)]
72pub struct DeadCodeAnalysisWithHashes {
73    pub results: AnalysisResults,
74    pub file_hashes: FxHashMap<PathBuf, u64>,
75}
76
77/// Typed dead-code analysis result with retained parser artifacts.
78#[derive(Debug)]
79pub struct DeadCodeAnalysisOutput {
80    pub results: AnalysisResults,
81    pub modules: Option<Vec<ModuleInfo>>,
82    pub files: Option<Vec<DiscoveredFile>>,
83}
84
85/// Typed dead-code analysis result with all reusable pipeline artifacts.
86#[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/// Shared parser artifacts for workspace-internal session consumers.
98///
99/// This additive contract lets internal callers retain immutable parsed
100/// modules without deep-cloning the session cache. Stable owned APIs continue
101/// to return [`DeadCodeAnalysisArtifacts`].
102#[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    /// Convert shared parser artifacts to the stable owned result contract.
116    #[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/// Typed project analysis result combining dead-code and duplication outputs.
138#[derive(Debug)]
139pub struct ProjectAnalysisOutput {
140    pub dead_code: DeadCodeAnalysisOutput,
141    pub duplication: duplicates::DuplicationReport,
142}
143
144/// Typed project analysis result with reusable session artifacts.
145#[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    /// Drop retained reuse-only artifacts and return the stable project output.
155    #[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/// Typed duplication analysis result.
169#[derive(Debug)]
170pub struct DuplicationAnalysis {
171    pub report: duplicates::DuplicationReport,
172    pub default_ignore_skips: duplicates::DefaultIgnoreSkips,
173}
174
175/// Typed health analysis result shared by CLI, API, NAPI, and future embedders.
176///
177/// The result contract belongs at the engine boundary so downstream callers can
178/// depend on a command-neutral shape.
179#[derive(Debug)]
180pub struct HealthAnalysisResult<GroupResolver = ()> {
181    pub report: HealthReport,
182    /// Per-group health output when grouping is active.
183    ///
184    /// `None` for the default run; `Some` for any grouped invocation. The
185    /// top-level report reflects the active run scope; consumers that want
186    /// per-group metrics read from `grouping.groups`.
187    pub grouping: Option<HealthGrouping>,
188    /// Optional grouping resolver retained by callers that need to tag findings
189    /// after analysis without rediscovering ownership or package metadata.
190    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    /// Drop presentation-only grouping resolver state while preserving the
201    /// command-neutral health analysis payload.
202    #[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}