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    /// Optional TypeScript semantic metadata for advisory health overlays.
183    pub type_aware_meta: Option<fallow_types::envelope::TypeAwareMeta>,
184    /// Per-group health output when grouping is active.
185    ///
186    /// `None` for the default run; `Some` for any grouped invocation. The
187    /// top-level report reflects the active run scope; consumers that want
188    /// per-group metrics read from `grouping.groups`.
189    pub grouping: Option<HealthGrouping>,
190    /// Optional grouping resolver retained by callers that need to tag findings
191    /// after analysis without rediscovering ownership or package metadata.
192    pub group_resolver: Option<GroupResolver>,
193    pub config: ResolvedConfig,
194    pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
195    pub elapsed: Duration,
196    pub timings: Option<HealthTimings>,
197    pub coverage_gaps_has_findings: bool,
198    pub should_fail_on_coverage_gaps: bool,
199}
200
201impl<GroupResolver> HealthAnalysisResult<GroupResolver> {
202    /// Drop presentation-only grouping resolver state while preserving the
203    /// command-neutral health analysis payload.
204    #[must_use]
205    pub fn without_group_resolver(self) -> HealthAnalysisResult<()> {
206        HealthAnalysisResult {
207            report: self.report,
208            type_aware_meta: self.type_aware_meta,
209            grouping: self.grouping,
210            group_resolver: None,
211            config: self.config,
212            workspace_diagnostics: self.workspace_diagnostics,
213            elapsed: self.elapsed,
214            timings: self.timings,
215            coverage_gaps_has_findings: self.coverage_gaps_has_findings,
216            should_fail_on_coverage_gaps: self.should_fail_on_coverage_gaps,
217        }
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use crate::project_config::{ProjectConfigOptions, config_for_project_analysis};
224    use fallow_config::ProductionAnalysis;
225    use fallow_types::output_format::OutputFormat;
226
227    use super::*;
228
229    #[test]
230    fn health_analysis_result_drops_presentation_resolver() {
231        let project = tempfile::tempdir().expect("temp dir");
232        let project_config = config_for_project_analysis(
233            project.path(),
234            None,
235            ProjectConfigOptions {
236                output: OutputFormat::Json,
237                no_cache: true,
238                threads: 1,
239                production_override: None,
240                quiet: true,
241                analysis: ProductionAnalysis::Health,
242                allow_remote_extends: false,
243            },
244        )
245        .expect("project config loads");
246        let result = HealthAnalysisResult {
247            report: HealthReport::default(),
248            grouping: None,
249            group_resolver: Some("resolver"),
250            config: project_config.config,
251            workspace_diagnostics: Vec::new(),
252            elapsed: Duration::from_millis(7),
253            timings: None,
254            coverage_gaps_has_findings: true,
255            should_fail_on_coverage_gaps: true,
256            type_aware_meta: None,
257        };
258
259        let neutral = result.without_group_resolver();
260
261        assert!(neutral.group_resolver.is_none());
262        assert_eq!(neutral.elapsed, Duration::from_millis(7));
263        assert!(neutral.coverage_gaps_has_findings);
264        assert!(neutral.should_fail_on_coverage_gaps);
265    }
266
267    #[test]
268    fn engine_result_surface_uses_explicit_reexports() {
269        let source = include_str!("results.rs");
270        let output_dead_code_wildcard = concat!("pub use fallow_types::output_dead_code::", "*");
271        let results_wildcard = concat!("pub use fallow_types::results::", "*");
272
273        assert!(!source.contains(output_dead_code_wildcard));
274        assert!(!source.contains(results_wildcard));
275    }
276}