Skip to main content

fallow_engine/
trace.rs

1//! Read-only trace helpers exposed through the engine boundary.
2
3use std::path::Path;
4
5use rustc_hash::FxHashSet;
6
7use crate::duplicates::DuplicationReport;
8use crate::module_graph::RetainedModuleGraph;
9
10#[path = "trace_impl.rs"]
11pub(crate) mod trace_impl;
12#[path = "trace_usage_impl.rs"]
13mod trace_usage_impl;
14
15pub use fallow_types::trace_usage::{DependencyUsage, DependencyUsageQuery, SitePageRequest};
16pub use trace_usage_impl::UsageError;
17
18pub use fallow_types::trace::{
19    ClassMemberTrace, CloneTrace, DependencyTrace, ExportReference, ExportTrace, FileTrace,
20    ImpactClosureGap, ImpactClosureTrace, ImportPathHop, ImportPathTrace, PipelineTimings,
21    ReExportChain, TraceProvenance, TraceSource, TracedCloneGroup, TracedExport, TracedReExport,
22};
23pub use trace_impl::{ImportPathEndpoint, SemanticClassMethodResolutionError};
24
25/// Trace why an export is considered used or unused.
26#[must_use]
27pub fn trace_export(
28    graph: &RetainedModuleGraph,
29    root: &Path,
30    file_path: &str,
31    export_name: &str,
32) -> Option<ExportTrace> {
33    trace_impl::trace_export(graph.as_graph(), root, file_path, export_name)
34}
35
36/// Reconcile checker-backed trace evidence with graph reachability while
37/// retaining non-crediting evidence for inspection.
38pub fn reconcile_semantic_trace_reachability(
39    graph: &RetainedModuleGraph,
40    root: &Path,
41    target_reachable: bool,
42    trace: &mut fallow_types::semantic::SemanticSymbolTrace,
43) {
44    trace_impl::reconcile_semantic_trace_reachability(
45        graph.as_graph(),
46        root,
47        target_reachable,
48        trace,
49    );
50}
51
52/// Resolve the source identity for an exact semantic export query.
53#[must_use]
54pub fn semantic_symbol_for_export(
55    graph: &RetainedModuleGraph,
56    root: &Path,
57    file_path: &str,
58    export_name: &str,
59) -> Option<fallow_types::semantic::SemanticSymbol> {
60    trace_impl::semantic_symbol_for_export(graph.as_graph(), root, file_path, export_name)
61}
62
63/// Resolve the source identity for an exact semantic class-member query.
64#[must_use]
65pub fn semantic_symbol_for_class_member(
66    graph: &RetainedModuleGraph,
67    root: &Path,
68    file_path: &str,
69    member_name: &str,
70) -> Option<fallow_types::semantic::SemanticSymbol> {
71    trace_impl::semantic_symbol_for_class_member(graph.as_graph(), root, file_path, member_name)
72}
73
74/// Resolve one exact exported class method for semantic impact analysis.
75pub fn semantic_symbol_for_exact_class_method(
76    graph: &RetainedModuleGraph,
77    root: &Path,
78    file_path: &str,
79    owner_name: &str,
80    member_name: &str,
81) -> Result<fallow_types::semantic::SemanticSymbol, SemanticClassMethodResolutionError> {
82    trace_impl::semantic_symbol_for_exact_class_method(
83        graph.as_graph(),
84        root,
85        file_path,
86        owner_name,
87        member_name,
88    )
89}
90
91/// Trace a class / enum / store member (the `--trace FILE:MEMBER` fallback when
92/// `MEMBER` is not a top-level export). See issue #1744.
93#[must_use]
94pub fn trace_class_member(
95    graph: &RetainedModuleGraph,
96    root: &Path,
97    file_path: &str,
98    member_name: &str,
99) -> Option<ClassMemberTrace> {
100    trace_impl::trace_class_member(graph.as_graph(), root, file_path, member_name)
101}
102
103/// Trace all graph edges for a file.
104#[must_use]
105pub fn trace_file(graph: &RetainedModuleGraph, root: &Path, file_path: &str) -> Option<FileTrace> {
106    trace_impl::trace_file(graph.as_graph(), root, file_path)
107}
108
109/// Trace where a dependency is used.
110///
111/// `workspace_roots` are the roots of the workspaces, so the peer-dependency
112/// credit runs one closure per workspace, as the unused-dependency check does.
113/// `ignore_patterns` are the configured `ignorePatterns`. A workspace
114/// `package.json` that they match does not turn on an optional peer, as in the
115/// unused-dependency check.
116#[must_use]
117#[expect(
118    clippy::implicit_hasher,
119    reason = "fallow standardizes on FxHashSet across the workspace"
120)]
121pub fn trace_dependency(
122    graph: &RetainedModuleGraph,
123    root: &Path,
124    workspace_roots: &[&Path],
125    ignore_patterns: &fallow_config::IgnorePatternSet,
126    package_name: &str,
127    script_used_packages: &FxHashSet<String>,
128) -> DependencyTrace {
129    trace_impl::trace_dependency(
130        graph.as_graph(),
131        root,
132        workspace_roots,
133        ignore_patterns,
134        package_name,
135        script_used_packages,
136    )
137}
138
139/// The analysis facts that a dependency trace reads.
140pub struct DependencyTraceInputs<'a> {
141    /// The retained module graph.
142    pub graph: &'a RetainedModuleGraph,
143    /// The project root.
144    pub root: &'a Path,
145    /// The roots of the workspaces, for the peer-dependency credit.
146    pub workspace_roots: &'a [&'a Path],
147    /// The configured `ignorePatterns`.
148    pub ignore_patterns: &'a fallow_config::IgnorePatternSet,
149    /// Packages invoked from package.json scripts, CI configs and git hooks.
150    pub script_used_packages: &'a FxHashSet<String>,
151    /// Which configs name which files and dependency names.
152    pub provenance: &'a TraceProvenance,
153    /// The findings of the analysis, which name the manifests that the
154    /// unused-dependency check flags.
155    pub results: &'a fallow_types::results::AnalysisResults,
156}
157
158/// Build the complete dependency trace: the importers, the config sources,
159/// the tooling credit and the flagged manifests.
160///
161/// `fallow dead-code --trace-dependency`, `fallow trace --dependency` and the
162/// programmatic API share this builder, so the base fields agree on every
163/// surface.
164#[must_use]
165pub fn build_dependency_trace(
166    inputs: &DependencyTraceInputs<'_>,
167    package_name: &str,
168) -> DependencyTrace {
169    let mut trace = trace_dependency(
170        inputs.graph,
171        inputs.root,
172        inputs.workspace_roots,
173        inputs.ignore_patterns,
174        package_name,
175        inputs.script_used_packages,
176    );
177    trace.sources = inputs.provenance.dependency_sources(package_name);
178    trace.apply_tooling_credit(inputs.provenance.tooling_credit(package_name));
179    trace.apply_unused_declarations(inputs.results, inputs.root);
180    trace
181}
182
183/// Trace a dependency with its per-specifier usage through an existing
184/// analysis session.
185///
186/// The inner `Result` carries a query error, such as a cursor that belongs
187/// to another query.
188///
189/// # Errors
190///
191/// Returns an error if parsing, graph construction, or analysis fails.
192pub fn trace_dependency_with_session(
193    session: &crate::session::AnalysisSession,
194    package_name: &str,
195    query: &DependencyUsageQuery,
196) -> crate::EngineResult<Result<DependencyTrace, UsageError>> {
197    // Keep the parsed modules: the usage walk reads their import facts.
198    let output = session.analyze_dead_code_with_shared_artifacts(true, true)?;
199    let graph = output.graph.as_ref().ok_or_else(|| {
200        crate::EngineError::new("trace --dependency requires a retained module graph")
201    })?;
202    let modules = output.modules.as_deref().unwrap_or(&[]);
203    let workspace_roots: Vec<&Path> = session
204        .workspaces()
205        .iter()
206        .map(|workspace| workspace.root.as_path())
207        .collect();
208    let mut trace = build_dependency_trace(
209        &DependencyTraceInputs {
210            graph,
211            root: session.root(),
212            workspace_roots: &workspace_roots,
213            ignore_patterns: &session.config().ignore_patterns,
214            script_used_packages: &output.script_used_packages,
215            provenance: &output.trace_provenance,
216            results: &output.results,
217        },
218        package_name,
219    );
220    match trace_usage_impl::dependency_usage(
221        graph.as_graph(),
222        modules,
223        session.root(),
224        package_name,
225        &trace.imported_by,
226        query,
227    ) {
228        Ok(usage) => {
229            trace.usage = Some(usage);
230            Ok(Ok(trace))
231        }
232        Err(err) => Ok(Err(err)),
233    }
234}
235
236/// Trace duplicate-code groups that contain a source location.
237#[must_use]
238pub fn trace_clone(
239    report: &DuplicationReport,
240    root: &Path,
241    file_path: &str,
242    line: usize,
243) -> CloneTrace {
244    trace_impl::trace_clone(report, root, file_path, line)
245}
246
247/// Trace a duplicate-code group by its stable content fingerprint.
248#[must_use]
249pub fn trace_clone_by_fingerprint(
250    report: &DuplicationReport,
251    root: &Path,
252    fingerprint: &str,
253) -> CloneTrace {
254    trace_impl::trace_clone_by_fingerprint(report, root, fingerprint)
255}
256
257/// Trace the impact closure for a file.
258#[must_use]
259pub fn trace_impact_closure(
260    graph: &RetainedModuleGraph,
261    root: &Path,
262    file_path: &str,
263) -> Option<ImpactClosureTrace> {
264    trace_impl::trace_impact_closure(graph.as_graph(), root, file_path)
265}
266
267/// Trace the shortest import path between two modules.
268///
269/// # Errors
270///
271/// Returns the endpoint that did not resolve to exactly one module in the graph.
272pub fn trace_import_path(
273    graph: &RetainedModuleGraph,
274    root: &Path,
275    from_path: &str,
276    to_path: &str,
277) -> Result<ImportPathTrace, ImportPathEndpoint> {
278    trace_impl::trace_import_path(graph.as_graph(), root, from_path, to_path, false)
279}
280
281/// Trace the shortest import path over eager edges only: static imports that
282/// carry a runtime value. The route explains why `to_path` loads before
283/// `from_path` runs.
284///
285/// # Errors
286///
287/// Returns the endpoint that did not resolve to exactly one module in the graph.
288pub fn trace_eager_import_path(
289    graph: &RetainedModuleGraph,
290    root: &Path,
291    from_path: &str,
292    to_path: &str,
293) -> Result<ImportPathTrace, ImportPathEndpoint> {
294    trace_impl::trace_import_path(graph.as_graph(), root, from_path, to_path, true)
295}
296
297/// Trace the shortest import path through an existing analysis session.
298/// With `eager_only`, the walk follows static value imports only.
299///
300/// The inner `Result` carries the endpoint that did not resolve to a module.
301///
302/// # Errors
303///
304/// Returns an error if parsing or graph construction fails.
305pub fn trace_import_path_with_session(
306    session: &crate::session::AnalysisSession,
307    from_path: &str,
308    to_path: &str,
309    eager_only: bool,
310) -> crate::EngineResult<Result<ImportPathTrace, ImportPathEndpoint>> {
311    let output = session.analyze_dead_code_with_shared_artifacts(false, true)?;
312    let graph = output
313        .graph
314        .as_ref()
315        .ok_or_else(|| crate::EngineError::new("trace --path requires a retained module graph"))?;
316    Ok(trace_impl::trace_import_path(
317        graph.as_graph(),
318        session.root(),
319        from_path,
320        to_path,
321        eager_only,
322    ))
323}