Skip to main content

fallow_api/runtime/
trace.rs

1use fallow_engine::session::AnalysisSession;
2use rustc_hash::FxHashSet;
3
4use crate::{
5    ProgrammaticAnalysisContext, ProgrammaticError, TraceCloneOptions,
6    TraceCloneProgrammaticOutput, TraceCloneTarget, TraceDependencyOptions,
7    TraceDependencyProgrammaticOutput, TraceExportOptions, TraceExportProgrammaticOutput,
8    TraceFileOptions, TraceFileProgrammaticOutput,
9};
10
11use super::{ProgrammaticResult, duplication, resolve_programmatic_analysis_context};
12
13struct TraceArtifacts {
14    graph: fallow_engine::module_graph::RetainedModuleGraph,
15    script_used_packages: FxHashSet<String>,
16}
17
18/// Trace why an export is considered used or unused.
19///
20/// # Errors
21///
22/// Returns a structured programmatic error for invalid options, config load
23/// failures, graph construction failures, or missing trace targets.
24pub fn run_trace_export(
25    options: &TraceExportOptions,
26) -> ProgrammaticResult<TraceExportProgrammaticOutput> {
27    validate_non_empty("file", &options.file)?;
28    validate_non_empty("export_name", &options.export_name)?;
29    let resolved = resolve_programmatic_analysis_context(&options.analysis)?;
30    resolved.install(|| {
31        let session = load_trace_session(&resolved)?;
32        let artifacts = trace_artifacts(&session)?;
33        let output = fallow_engine::trace::trace_export(
34            &artifacts.graph,
35            session.root(),
36            &options.file,
37            &options.export_name,
38        )
39        .ok_or_else(|| {
40            ProgrammaticError::new(
41                format!(
42                    "export '{}' not found in '{}'",
43                    options.export_name, options.file
44                ),
45                2,
46            )
47            .with_code("FALLOW_TRACE_TARGET_NOT_FOUND")
48            .with_context("trace_export")
49        })?;
50        Ok(TraceExportProgrammaticOutput { output })
51    })
52}
53
54/// Trace all graph edges for a file.
55///
56/// # Errors
57///
58/// Returns a structured programmatic error for invalid options, config load
59/// failures, graph construction failures, or missing trace targets.
60pub fn run_trace_file(
61    options: &TraceFileOptions,
62) -> ProgrammaticResult<TraceFileProgrammaticOutput> {
63    validate_non_empty("file", &options.file)?;
64    let resolved = resolve_programmatic_analysis_context(&options.analysis)?;
65    resolved.install(|| {
66        let session = load_trace_session(&resolved)?;
67        let artifacts = trace_artifacts(&session)?;
68        let output =
69            fallow_engine::trace::trace_file(&artifacts.graph, session.root(), &options.file)
70                .ok_or_else(|| {
71                    ProgrammaticError::new(
72                        format!("file '{}' not found in module graph", options.file),
73                        2,
74                    )
75                    .with_code("FALLOW_TRACE_TARGET_NOT_FOUND")
76                    .with_context("trace_file")
77                })?;
78        Ok(TraceFileProgrammaticOutput { output })
79    })
80}
81
82/// Trace where a dependency is used.
83///
84/// # Errors
85///
86/// Returns a structured programmatic error for invalid options, config load, or
87/// graph construction failures.
88pub fn run_trace_dependency(
89    options: &TraceDependencyOptions,
90) -> ProgrammaticResult<TraceDependencyProgrammaticOutput> {
91    validate_non_empty("package_name", &options.package_name)?;
92    let resolved = resolve_programmatic_analysis_context(&options.analysis)?;
93    resolved.install(|| {
94        let session = load_trace_session(&resolved)?;
95        let artifacts = trace_artifacts(&session)?;
96        let output = fallow_engine::trace::trace_dependency(
97            &artifacts.graph,
98            session.root(),
99            &options.package_name,
100            &artifacts.script_used_packages,
101        );
102        Ok(TraceDependencyProgrammaticOutput { output })
103    })
104}
105
106/// Trace duplicate-code groups by location or stable fingerprint.
107///
108/// # Errors
109///
110/// Returns a structured programmatic error for invalid options, config load
111/// failures, duplicate detection failures, or missing trace targets.
112pub fn run_trace_clone(
113    options: &TraceCloneOptions,
114) -> ProgrammaticResult<TraceCloneProgrammaticOutput> {
115    validate_trace_clone_target(&options.target)?;
116    let resolved = resolve_programmatic_analysis_context(&options.duplication.analysis)?;
117    resolved.install(|| {
118        let session = duplication::load_duplication_session(&options.duplication, &resolved)?;
119        let dupes_config =
120            duplication::build_dupes_config(&options.duplication, &session.config().duplicates);
121        let cache_dir = (!resolved.no_cache).then_some(session.config().cache_dir.as_path());
122        let report = session
123            .find_duplicates_with_defaults(&dupes_config, cache_dir)
124            .report;
125        let (trace, not_found) = match &options.target {
126            TraceCloneTarget::Location { file, line } => (
127                fallow_engine::trace::trace_clone(&report, session.root(), file, *line),
128                format!("no clone found at {file}:{line}"),
129            ),
130            TraceCloneTarget::Fingerprint(fingerprint) => (
131                fallow_engine::trace::trace_clone_by_fingerprint(
132                    &report,
133                    session.root(),
134                    fingerprint,
135                ),
136                format!("no clone group with fingerprint {fingerprint}"),
137            ),
138        };
139        if trace.matched_instance.is_none() {
140            return Err(ProgrammaticError::new(not_found, 2)
141                .with_code("FALLOW_TRACE_TARGET_NOT_FOUND")
142                .with_context("trace_clone"));
143        }
144        Ok(TraceCloneProgrammaticOutput { output: trace })
145    })
146}
147
148fn validate_non_empty(field: &str, value: &str) -> ProgrammaticResult<()> {
149    if value.trim().is_empty() {
150        return Err(
151            ProgrammaticError::new(format!("{field} must not be empty"), 2)
152                .with_code("FALLOW_INVALID_TRACE_OPTIONS")
153                .with_context(field.to_string()),
154        );
155    }
156    Ok(())
157}
158
159fn validate_trace_clone_target(target: &TraceCloneTarget) -> ProgrammaticResult<()> {
160    match target {
161        TraceCloneTarget::Location { file, line } => {
162            validate_non_empty("file", file)?;
163            if *line == 0 {
164                return Err(ProgrammaticError::new("line must be greater than 0", 2)
165                    .with_code("FALLOW_INVALID_TRACE_OPTIONS")
166                    .with_context("trace_clone.line"));
167            }
168        }
169        TraceCloneTarget::Fingerprint(fingerprint) => {
170            validate_non_empty("fingerprint", fingerprint)?;
171        }
172    }
173    Ok(())
174}
175
176fn load_trace_session(
177    resolved: &ProgrammaticAnalysisContext,
178) -> ProgrammaticResult<AnalysisSession> {
179    super::dead_code::load_dead_code_session(
180        &super::dead_code::default_dead_code_options_for_context(resolved),
181        resolved,
182    )
183}
184
185fn trace_artifacts(session: &AnalysisSession) -> ProgrammaticResult<TraceArtifacts> {
186    let artifacts = session
187        .analyze_dead_code_with_session_artifacts(false, true, None)
188        .map_err(|err| {
189            ProgrammaticError::new(format!("trace analysis failed: {err}"), 2)
190                .with_code("FALLOW_TRACE_FAILED")
191                .with_context("trace")
192        })?;
193    let graph = artifacts.analysis.graph.ok_or_else(|| {
194        ProgrammaticError::new("trace requires a retained module graph", 2)
195            .with_code("FALLOW_TRACE_GRAPH_UNAVAILABLE")
196            .with_context("trace.graph")
197    })?;
198    Ok(TraceArtifacts {
199        graph,
200        script_used_packages: artifacts.analysis.script_used_packages,
201    })
202}