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 TraceExportTargetOutput, 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 = if let Some(export) = fallow_engine::trace::trace_export(
38 &artifacts.graph,
39 session.root(),
40 &options.file,
41 &options.export_name,
42 ) {
43 TraceExportTargetOutput::Export(export)
44 } else if let Some(member) = fallow_engine::trace::trace_class_member(
45 &artifacts.graph,
46 session.root(),
47 &options.file,
48 &options.export_name,
49 ) {
50 TraceExportTargetOutput::Member(member)
51 } else {
52 return Err(ProgrammaticError::new(
53 format!(
54 "export or member '{}' not found in '{}'",
55 options.export_name, options.file
56 ),
57 2,
58 )
59 .with_code("FALLOW_TRACE_TARGET_NOT_FOUND")
60 .with_help(
61 "The name is neither a top-level export nor a class / enum / store member of this \
62 file. Run trace_file on the file to list its exports, or project_info for the \
63 project symbol set; confirm the file path is project-relative.",
64 )
65 .with_context("trace_export"));
66 };
67 Ok(TraceExportProgrammaticOutput { output })
68 })
69}
70
71pub fn run_trace_file(
78 options: &TraceFileOptions,
79) -> ProgrammaticResult<TraceFileProgrammaticOutput> {
80 validate_non_empty("file", &options.file)?;
81 let resolved = resolve_programmatic_analysis_context(&options.analysis)?;
82 resolved.install(|| {
83 let session = load_trace_session(&resolved)?;
84 let artifacts = trace_artifacts(&session)?;
85 let output =
86 fallow_engine::trace::trace_file(&artifacts.graph, session.root(), &options.file)
87 .ok_or_else(|| {
88 ProgrammaticError::new(
89 format!("file '{}' not found in module graph", options.file),
90 2,
91 )
92 .with_code("FALLOW_TRACE_TARGET_NOT_FOUND")
93 .with_help(
94 "The file is not in the analyzed module graph. Run project_info to list \
95 discovered files; the path must be project-relative and not excluded by \
96 ignore patterns or outside the analyzed roots.",
97 )
98 .with_context("trace_file")
99 })?;
100 Ok(TraceFileProgrammaticOutput { output })
101 })
102}
103
104pub fn run_trace_dependency(
111 options: &TraceDependencyOptions,
112) -> ProgrammaticResult<TraceDependencyProgrammaticOutput> {
113 validate_non_empty("package_name", &options.package_name)?;
114 let resolved = resolve_programmatic_analysis_context(&options.analysis)?;
115 resolved.install(|| {
116 let session = load_trace_session(&resolved)?;
117 let artifacts = trace_artifacts(&session)?;
118 let output = fallow_engine::trace::trace_dependency(
119 &artifacts.graph,
120 session.root(),
121 &options.package_name,
122 &artifacts.script_used_packages,
123 );
124 Ok(TraceDependencyProgrammaticOutput { output })
125 })
126}
127
128pub fn run_trace_clone(
135 options: &TraceCloneOptions,
136) -> ProgrammaticResult<TraceCloneProgrammaticOutput> {
137 validate_trace_clone_target(&options.target)?;
138 let resolved = resolve_programmatic_analysis_context(&options.duplication.analysis)?;
139 resolved.install(|| {
140 let session = duplication::load_duplication_session(&options.duplication, &resolved)?;
141 let dupes_config =
142 duplication::build_dupes_config(&options.duplication, &session.config().duplicates);
143 let cache_dir = (!resolved.no_cache).then_some(session.config().cache_dir.as_path());
144 let report = session
145 .find_duplicates_with_defaults(&dupes_config, cache_dir)
146 .report;
147 let (trace, not_found) = match &options.target {
148 TraceCloneTarget::Location { file, line } => (
149 fallow_engine::trace::trace_clone(&report, session.root(), file, *line),
150 format!("no clone found at {file}:{line}"),
151 ),
152 TraceCloneTarget::Fingerprint(fingerprint) => (
153 fallow_engine::trace::trace_clone_by_fingerprint(
154 &report,
155 session.root(),
156 fingerprint,
157 ),
158 format!("no clone group with fingerprint {fingerprint}"),
159 ),
160 };
161 if trace.matched_instance.is_none() {
162 return Err(ProgrammaticError::new(not_found, 2)
163 .with_code("FALLOW_TRACE_TARGET_NOT_FOUND")
164 .with_help(
165 "No clone matched. Run find_dupes to list clone groups and their fingerprints; \
166 a location must fall inside a reported clone instance, and a fingerprint must \
167 be a find_dupes clone_groups[].fingerprint (a dup:<id> value).",
168 )
169 .with_context("trace_clone"));
170 }
171 Ok(TraceCloneProgrammaticOutput { output: trace })
172 })
173}
174
175fn validate_non_empty(field: &str, value: &str) -> ProgrammaticResult<()> {
176 if value.trim().is_empty() {
177 return Err(
178 ProgrammaticError::new(format!("{field} must not be empty"), 2)
179 .with_code("FALLOW_INVALID_TRACE_OPTIONS")
180 .with_context(field.to_string()),
181 );
182 }
183 Ok(())
184}
185
186fn validate_trace_clone_target(target: &TraceCloneTarget) -> ProgrammaticResult<()> {
187 match target {
188 TraceCloneTarget::Location { file, line } => {
189 validate_non_empty("file", file)?;
190 if *line == 0 {
191 return Err(ProgrammaticError::new("line must be greater than 0", 2)
192 .with_code("FALLOW_INVALID_TRACE_OPTIONS")
193 .with_context("trace_clone.line"));
194 }
195 }
196 TraceCloneTarget::Fingerprint(fingerprint) => {
197 validate_non_empty("fingerprint", fingerprint)?;
198 }
199 }
200 Ok(())
201}
202
203fn load_trace_session(
204 resolved: &ProgrammaticAnalysisContext,
205) -> ProgrammaticResult<AnalysisSession> {
206 super::dead_code::load_dead_code_session(
207 &super::dead_code::default_dead_code_options_for_context(resolved),
208 resolved,
209 )
210}
211
212fn trace_artifacts(session: &AnalysisSession) -> ProgrammaticResult<TraceArtifacts> {
213 let artifacts = session
214 .analyze_dead_code_with_session_artifacts(false, true, None)
215 .map_err(|err| {
216 ProgrammaticError::new(format!("trace analysis failed: {err}"), 2)
217 .with_code("FALLOW_TRACE_FAILED")
218 .with_context("trace")
219 })?;
220 let graph = artifacts.analysis.graph.ok_or_else(|| {
221 ProgrammaticError::new("trace requires a retained module graph", 2)
222 .with_code("FALLOW_TRACE_GRAPH_UNAVAILABLE")
223 .with_context("trace.graph")
224 })?;
225 Ok(TraceArtifacts {
226 graph,
227 script_used_packages: artifacts.analysis.script_used_packages,
228 })
229}