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