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::{
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/// Typed dead-code analysis result.
68#[derive(Debug)]
69pub struct DeadCodeAnalysis {
70    /// Findings across all dead-code categories.
71    pub results: AnalysisResults,
72}
73
74/// Typed dead-code analysis result with per-file source hashes.
75#[derive(Debug)]
76pub struct DeadCodeAnalysisWithHashes {
77    /// Findings across all dead-code categories.
78    pub results: AnalysisResults,
79    /// Per-file source content hashes for cache invalidation.
80    pub file_hashes: FxHashMap<PathBuf, u64>,
81}
82
83/// Typed dead-code analysis result with retained parser artifacts.
84#[derive(Debug)]
85pub struct DeadCodeAnalysisOutput {
86    /// Findings across all dead-code categories.
87    pub results: AnalysisResults,
88    /// Parsed modules retained for reuse, when the caller asked for them.
89    pub modules: Option<Vec<ModuleInfo>>,
90    /// Discovered files retained for reuse, when the caller asked for them.
91    pub files: Option<Vec<DiscoveredFile>>,
92}
93
94/// Typed dead-code analysis result with all reusable pipeline artifacts.
95#[derive(Debug)]
96pub struct DeadCodeAnalysisArtifacts {
97    /// Findings across all dead-code categories.
98    pub results: AnalysisResults,
99    /// Per-phase pipeline timings, when the run measured them.
100    pub timings: Option<trace::PipelineTimings>,
101    /// Retained module graph for downstream passes (health, trace, impact).
102    pub graph: Option<module_graph::RetainedModuleGraph>,
103    /// Parsed modules retained for reuse, when the caller asked for them.
104    pub modules: Option<Vec<ModuleInfo>>,
105    /// Discovered files retained for reuse, when the caller asked for them.
106    pub files: Option<Vec<DiscoveredFile>>,
107    /// Package names referenced from package.json scripts, which keeps those
108    /// dependencies from being reported unused.
109    pub script_used_packages: FxHashSet<String>,
110    /// Per-file source content hashes for cache invalidation.
111    pub file_hashes: FxHashMap<PathBuf, u64>,
112}
113
114/// Shared parser artifacts for workspace-internal session consumers.
115///
116/// This additive contract lets internal callers retain immutable parsed
117/// modules without deep-cloning the session cache. Stable owned APIs continue
118/// to return [`DeadCodeAnalysisArtifacts`].
119#[doc(hidden)]
120#[derive(Debug)]
121pub struct SharedDeadCodeAnalysisArtifacts {
122    pub results: AnalysisResults,
123    pub timings: Option<trace::PipelineTimings>,
124    pub graph: Option<module_graph::RetainedModuleGraph>,
125    pub modules: Option<Arc<[ModuleInfo]>>,
126    pub files: Option<Vec<DiscoveredFile>>,
127    pub script_used_packages: FxHashSet<String>,
128    pub file_hashes: FxHashMap<PathBuf, u64>,
129}
130
131impl SharedDeadCodeAnalysisArtifacts {
132    /// Convert shared parser artifacts to the stable owned result contract.
133    #[must_use]
134    pub fn into_owned(self) -> DeadCodeAnalysisArtifacts {
135        let modules = self.modules.map(|modules| {
136            let mut owned = modules.to_vec();
137            for module in &mut owned {
138                module.release_resolution_payload();
139            }
140            owned
141        });
142        DeadCodeAnalysisArtifacts {
143            results: self.results,
144            timings: self.timings,
145            graph: self.graph,
146            modules,
147            files: self.files,
148            script_used_packages: self.script_used_packages,
149            file_hashes: self.file_hashes,
150        }
151    }
152}
153
154/// Typed project analysis result combining dead-code and duplication outputs.
155#[derive(Debug)]
156pub struct ProjectAnalysisOutput {
157    /// Dead-code findings with optionally retained parser artifacts.
158    pub dead_code: DeadCodeAnalysisOutput,
159    /// Duplication report for the same file set.
160    pub duplication: duplicates::DuplicationReport,
161}
162
163/// Typed project analysis result with reusable session artifacts.
164#[derive(Debug)]
165pub struct ProjectAnalysisArtifacts {
166    /// Dead-code findings with all reusable pipeline artifacts.
167    pub dead_code: DeadCodeAnalysisArtifacts,
168    /// Duplication report for the same file set.
169    pub duplication: duplicates::DuplicationReport,
170    /// Diff scope the run was limited to, when one was resolved.
171    pub changed_files: Option<FxHashSet<PathBuf>>,
172    /// Per-file source fingerprints for downstream cache invalidation.
173    pub source_fingerprints: Option<FxHashMap<PathBuf, SourceFingerprint>>,
174}
175
176impl ProjectAnalysisArtifacts {
177    /// Drop retained reuse-only artifacts and return the stable project output.
178    #[must_use]
179    pub fn into_output(self) -> ProjectAnalysisOutput {
180        ProjectAnalysisOutput {
181            dead_code: DeadCodeAnalysisOutput {
182                results: self.dead_code.results,
183                modules: self.dead_code.modules,
184                files: self.dead_code.files,
185            },
186            duplication: self.duplication,
187        }
188    }
189}
190
191/// Typed duplication analysis result.
192#[derive(Debug)]
193pub struct DuplicationAnalysis {
194    pub report: duplicates::DuplicationReport,
195    pub default_ignore_skips: duplicates::DefaultIgnoreSkips,
196}
197
198/// Typed health analysis result shared by CLI, API, NAPI, and future embedders.
199///
200/// The result contract belongs at the engine boundary so downstream callers can
201/// depend on a command-neutral shape.
202#[derive(Debug)]
203pub struct HealthAnalysisResult<GroupResolver = ()> {
204    /// The assembled health report for the active run scope.
205    pub report: HealthReport,
206    /// Optional TypeScript semantic metadata for advisory health overlays.
207    pub type_aware_meta: Option<fallow_types::envelope::TypeAwareMeta>,
208    /// Per-file branching totals, keyed by absolute path.
209    ///
210    /// Threshold-blind and suppression-blind, unlike `report`, which holds only
211    /// units that breached a threshold. The audit compares this map across the
212    /// base and head revisions; nothing else reads it, and it is not
213    /// serialized, so it carries no schema version.
214    pub branching_by_file: crate::health::BranchingByFile,
215    /// Per-group health output when grouping is active.
216    ///
217    /// `None` for the default run; `Some` for any grouped invocation. The
218    /// top-level report reflects the active run scope; consumers that want
219    /// per-group metrics read from `grouping.groups`.
220    pub grouping: Option<HealthGrouping>,
221    /// Optional grouping resolver retained by callers that need to tag findings
222    /// after analysis without rediscovering ownership or package metadata.
223    pub group_resolver: Option<GroupResolver>,
224    /// Resolved config the run executed under.
225    pub config: ResolvedConfig,
226    /// Diagnostics from workspace discovery (undeclared or invalid members).
227    pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
228    /// Total wall time of the health run.
229    pub elapsed: Duration,
230    /// Per-phase timings, when the run measured them.
231    pub timings: Option<HealthTimings>,
232    /// True when the coverage gaps section produced findings.
233    pub coverage_gaps_has_findings: bool,
234    /// True when coverage gap findings should fail the run (the gate is
235    /// enforced rather than advisory).
236    pub should_fail_on_coverage_gaps: bool,
237}
238
239impl<GroupResolver> HealthAnalysisResult<GroupResolver> {
240    /// Drop presentation-only grouping resolver state while preserving the
241    /// command-neutral health analysis payload.
242    #[must_use]
243    pub fn without_group_resolver(self) -> HealthAnalysisResult<()> {
244        HealthAnalysisResult {
245            report: self.report,
246            type_aware_meta: self.type_aware_meta,
247            branching_by_file: self.branching_by_file,
248            grouping: self.grouping,
249            group_resolver: None,
250            config: self.config,
251            workspace_diagnostics: self.workspace_diagnostics,
252            elapsed: self.elapsed,
253            timings: self.timings,
254            coverage_gaps_has_findings: self.coverage_gaps_has_findings,
255            should_fail_on_coverage_gaps: self.should_fail_on_coverage_gaps,
256        }
257    }
258}
259
260#[cfg(test)]
261mod tests {
262    use crate::project_config::{ProjectConfigOptions, config_for_project_analysis};
263    use fallow_config::ProductionAnalysis;
264    use fallow_types::output_format::OutputFormat;
265
266    use super::*;
267
268    #[test]
269    fn health_analysis_result_drops_presentation_resolver() {
270        let project = tempfile::tempdir().expect("temp dir");
271        let project_config = config_for_project_analysis(
272            project.path(),
273            None,
274            ProjectConfigOptions {
275                output: OutputFormat::Json,
276                no_cache: true,
277                threads: 1,
278                production_override: None,
279                quiet: true,
280                analysis: ProductionAnalysis::Health,
281                allow_remote_extends: false,
282            },
283        )
284        .expect("project config loads");
285        let result = HealthAnalysisResult {
286            report: HealthReport::default(),
287            branching_by_file: crate::health::BranchingByFile::default(),
288            grouping: None,
289            group_resolver: Some("resolver"),
290            config: project_config.config,
291            workspace_diagnostics: Vec::new(),
292            elapsed: Duration::from_millis(7),
293            timings: None,
294            coverage_gaps_has_findings: true,
295            should_fail_on_coverage_gaps: true,
296            type_aware_meta: None,
297        };
298
299        let neutral = result.without_group_resolver();
300
301        assert!(neutral.group_resolver.is_none());
302        assert_eq!(neutral.elapsed, Duration::from_millis(7));
303        assert!(neutral.coverage_gaps_has_findings);
304        assert!(neutral.should_fail_on_coverage_gaps);
305    }
306
307    #[test]
308    fn engine_result_surface_uses_explicit_reexports() {
309        let source = include_str!("results.rs");
310        let output_dead_code_wildcard = concat!("pub use fallow_types::output_dead_code::", "*");
311        let results_wildcard = concat!("pub use fallow_types::results::", "*");
312
313        assert!(!source.contains(output_dead_code_wildcard));
314        assert!(!source.contains(results_wildcard));
315    }
316}