use fallow_engine::session::AnalysisSession;
use rustc_hash::FxHashSet;
use crate::{
ProgrammaticAnalysisContext, ProgrammaticError, TraceCloneOptions,
TraceCloneProgrammaticOutput, TraceCloneTarget, TraceDependencyOptions,
TraceDependencyProgrammaticOutput, TraceExportOptions, TraceExportProgrammaticOutput,
TraceFileOptions, TraceFileProgrammaticOutput,
};
use super::{ProgrammaticResult, duplication, resolve_programmatic_analysis_context};
struct TraceArtifacts {
graph: fallow_engine::module_graph::RetainedModuleGraph,
script_used_packages: FxHashSet<String>,
}
pub fn run_trace_export(
options: &TraceExportOptions,
) -> ProgrammaticResult<TraceExportProgrammaticOutput> {
validate_non_empty("file", &options.file)?;
validate_non_empty("export_name", &options.export_name)?;
let resolved = resolve_programmatic_analysis_context(&options.analysis)?;
resolved.install(|| {
let session = load_trace_session(&resolved)?;
let artifacts = trace_artifacts(&session)?;
let output = fallow_engine::trace::trace_export(
&artifacts.graph,
session.root(),
&options.file,
&options.export_name,
)
.ok_or_else(|| {
ProgrammaticError::new(
format!(
"export '{}' not found in '{}'",
options.export_name, options.file
),
2,
)
.with_code("FALLOW_TRACE_TARGET_NOT_FOUND")
.with_context("trace_export")
})?;
Ok(TraceExportProgrammaticOutput { output })
})
}
pub fn run_trace_file(
options: &TraceFileOptions,
) -> ProgrammaticResult<TraceFileProgrammaticOutput> {
validate_non_empty("file", &options.file)?;
let resolved = resolve_programmatic_analysis_context(&options.analysis)?;
resolved.install(|| {
let session = load_trace_session(&resolved)?;
let artifacts = trace_artifacts(&session)?;
let output =
fallow_engine::trace::trace_file(&artifacts.graph, session.root(), &options.file)
.ok_or_else(|| {
ProgrammaticError::new(
format!("file '{}' not found in module graph", options.file),
2,
)
.with_code("FALLOW_TRACE_TARGET_NOT_FOUND")
.with_context("trace_file")
})?;
Ok(TraceFileProgrammaticOutput { output })
})
}
pub fn run_trace_dependency(
options: &TraceDependencyOptions,
) -> ProgrammaticResult<TraceDependencyProgrammaticOutput> {
validate_non_empty("package_name", &options.package_name)?;
let resolved = resolve_programmatic_analysis_context(&options.analysis)?;
resolved.install(|| {
let session = load_trace_session(&resolved)?;
let artifacts = trace_artifacts(&session)?;
let output = fallow_engine::trace::trace_dependency(
&artifacts.graph,
session.root(),
&options.package_name,
&artifacts.script_used_packages,
);
Ok(TraceDependencyProgrammaticOutput { output })
})
}
pub fn run_trace_clone(
options: &TraceCloneOptions,
) -> ProgrammaticResult<TraceCloneProgrammaticOutput> {
validate_trace_clone_target(&options.target)?;
let resolved = resolve_programmatic_analysis_context(&options.duplication.analysis)?;
resolved.install(|| {
let session = duplication::load_duplication_session(&options.duplication, &resolved)?;
let dupes_config =
duplication::build_dupes_config(&options.duplication, &session.config().duplicates);
let cache_dir = (!resolved.no_cache).then_some(session.config().cache_dir.as_path());
let report = session
.find_duplicates_with_defaults(&dupes_config, cache_dir)
.report;
let (trace, not_found) = match &options.target {
TraceCloneTarget::Location { file, line } => (
fallow_engine::trace::trace_clone(&report, session.root(), file, *line),
format!("no clone found at {file}:{line}"),
),
TraceCloneTarget::Fingerprint(fingerprint) => (
fallow_engine::trace::trace_clone_by_fingerprint(
&report,
session.root(),
fingerprint,
),
format!("no clone group with fingerprint {fingerprint}"),
),
};
if trace.matched_instance.is_none() {
return Err(ProgrammaticError::new(not_found, 2)
.with_code("FALLOW_TRACE_TARGET_NOT_FOUND")
.with_context("trace_clone"));
}
Ok(TraceCloneProgrammaticOutput { output: trace })
})
}
fn validate_non_empty(field: &str, value: &str) -> ProgrammaticResult<()> {
if value.trim().is_empty() {
return Err(
ProgrammaticError::new(format!("{field} must not be empty"), 2)
.with_code("FALLOW_INVALID_TRACE_OPTIONS")
.with_context(field.to_string()),
);
}
Ok(())
}
fn validate_trace_clone_target(target: &TraceCloneTarget) -> ProgrammaticResult<()> {
match target {
TraceCloneTarget::Location { file, line } => {
validate_non_empty("file", file)?;
if *line == 0 {
return Err(ProgrammaticError::new("line must be greater than 0", 2)
.with_code("FALLOW_INVALID_TRACE_OPTIONS")
.with_context("trace_clone.line"));
}
}
TraceCloneTarget::Fingerprint(fingerprint) => {
validate_non_empty("fingerprint", fingerprint)?;
}
}
Ok(())
}
fn load_trace_session(
resolved: &ProgrammaticAnalysisContext,
) -> ProgrammaticResult<AnalysisSession> {
super::dead_code::load_dead_code_session(
&super::dead_code::default_dead_code_options_for_context(resolved),
resolved,
)
}
fn trace_artifacts(session: &AnalysisSession) -> ProgrammaticResult<TraceArtifacts> {
let artifacts = session
.analyze_dead_code_with_session_artifacts(false, true, None)
.map_err(|err| {
ProgrammaticError::new(format!("trace analysis failed: {err}"), 2)
.with_code("FALLOW_TRACE_FAILED")
.with_context("trace")
})?;
let graph = artifacts.analysis.graph.ok_or_else(|| {
ProgrammaticError::new("trace requires a retained module graph", 2)
.with_code("FALLOW_TRACE_GRAPH_UNAVAILABLE")
.with_context("trace.graph")
})?;
Ok(TraceArtifacts {
graph,
script_used_packages: artifacts.analysis.script_used_packages,
})
}