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