fallow_api/runtime/
trace.rs1use 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
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_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 = 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
81pub 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
105pub 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}