fallow_api/runtime/
trace.rs1use 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
18pub 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
54pub 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
82pub 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
106pub 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}