Skip to main content

fallow_api/runtime/
trace.rs

1use fallow_engine::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::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_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 = fallow_engine::trace_file(&artifacts.graph, session.root(), &options.file)
69            .ok_or_else(|| {
70                ProgrammaticError::new(
71                    format!("file '{}' not found in module graph", options.file),
72                    2,
73                )
74                .with_code("FALLOW_TRACE_TARGET_NOT_FOUND")
75                .with_context("trace_file")
76            })?;
77        Ok(TraceFileProgrammaticOutput { output })
78    })
79}
80
81/// Trace where a dependency is used.
82///
83/// # Errors
84///
85/// Returns a structured programmatic error for invalid options, config load, or
86/// graph construction failures.
87pub fn run_trace_dependency(
88    options: &TraceDependencyOptions,
89) -> ProgrammaticResult<TraceDependencyProgrammaticOutput> {
90    validate_non_empty("package_name", &options.package_name)?;
91    let resolved = resolve_programmatic_analysis_context(&options.analysis)?;
92    resolved.install(|| {
93        let session = load_trace_session(&resolved)?;
94        let artifacts = trace_artifacts(&session)?;
95        let output = fallow_engine::trace_dependency(
96            &artifacts.graph,
97            session.root(),
98            &options.package_name,
99            &artifacts.script_used_packages,
100        );
101        Ok(TraceDependencyProgrammaticOutput { output })
102    })
103}
104
105/// Trace duplicate-code groups by location or stable fingerprint.
106///
107/// # Errors
108///
109/// Returns a structured programmatic error for invalid options, config load
110/// failures, duplicate detection failures, or missing trace targets.
111pub fn run_trace_clone(
112    options: &TraceCloneOptions,
113) -> ProgrammaticResult<TraceCloneProgrammaticOutput> {
114    validate_trace_clone_target(&options.target)?;
115    let resolved = resolve_programmatic_analysis_context(&options.duplication.analysis)?;
116    resolved.install(|| {
117        let session = duplication::load_duplication_session(&options.duplication, &resolved)?;
118        let dupes_config =
119            duplication::build_dupes_config(&options.duplication, &session.config().duplicates);
120        let cache_dir = (!resolved.no_cache).then_some(session.config().cache_dir.as_path());
121        let report = session
122            .find_duplicates_with_defaults(&dupes_config, cache_dir)
123            .report;
124        let (trace, not_found) = match &options.target {
125            TraceCloneTarget::Location { file, line } => (
126                fallow_engine::trace_clone(&report, session.root(), file, *line),
127                format!("no clone found at {file}:{line}"),
128            ),
129            TraceCloneTarget::Fingerprint(fingerprint) => (
130                fallow_engine::trace_clone_by_fingerprint(&report, session.root(), fingerprint),
131                format!("no clone group with fingerprint {fingerprint}"),
132            ),
133        };
134        if trace.matched_instance.is_none() {
135            return Err(ProgrammaticError::new(not_found, 2)
136                .with_code("FALLOW_TRACE_TARGET_NOT_FOUND")
137                .with_context("trace_clone"));
138        }
139        Ok(TraceCloneProgrammaticOutput { output: trace })
140    })
141}
142
143fn validate_non_empty(field: &str, value: &str) -> ProgrammaticResult<()> {
144    if value.trim().is_empty() {
145        return Err(
146            ProgrammaticError::new(format!("{field} must not be empty"), 2)
147                .with_code("FALLOW_INVALID_TRACE_OPTIONS")
148                .with_context(field.to_string()),
149        );
150    }
151    Ok(())
152}
153
154fn validate_trace_clone_target(target: &TraceCloneTarget) -> ProgrammaticResult<()> {
155    match target {
156        TraceCloneTarget::Location { file, line } => {
157            validate_non_empty("file", file)?;
158            if *line == 0 {
159                return Err(ProgrammaticError::new("line must be greater than 0", 2)
160                    .with_code("FALLOW_INVALID_TRACE_OPTIONS")
161                    .with_context("trace_clone.line"));
162            }
163        }
164        TraceCloneTarget::Fingerprint(fingerprint) => {
165            validate_non_empty("fingerprint", fingerprint)?;
166        }
167    }
168    Ok(())
169}
170
171fn load_trace_session(
172    resolved: &ProgrammaticAnalysisContext,
173) -> ProgrammaticResult<AnalysisSession> {
174    super::dead_code::load_dead_code_session(
175        &super::dead_code::default_dead_code_options_for_context(resolved),
176        resolved,
177    )
178}
179
180fn trace_artifacts(session: &AnalysisSession) -> ProgrammaticResult<TraceArtifacts> {
181    let artifacts = session
182        .analyze_dead_code_with_session_artifacts(false, true, None)
183        .map_err(|err| {
184            ProgrammaticError::new(format!("trace analysis failed: {err}"), 2)
185                .with_code("FALLOW_TRACE_FAILED")
186                .with_context("trace")
187        })?;
188    let graph = artifacts.analysis.graph.ok_or_else(|| {
189        ProgrammaticError::new("trace requires a retained module graph", 2)
190            .with_code("FALLOW_TRACE_GRAPH_UNAVAILABLE")
191            .with_context("trace.graph")
192    })?;
193    Ok(TraceArtifacts {
194        graph,
195        script_used_packages: artifacts.analysis.script_used_packages,
196    })
197}