Skip to main content

fallow_engine/
results.rs

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