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