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