use std::collections::HashSet;
use std::path::PathBuf;
use anyhow::Result;
use clap::Args;
use tldr_core::pdg::get_slice;
use tldr_core::{Language, SliceDirection};
use crate::output::{OutputFormat, OutputWriter};
use super::error::{ContractsError, ContractsResult};
use super::types::{ChopResult, OutputFormat as ContractsOutputFormat};
use super::validation::{validate_file_path, validate_function_name, MAX_CFG_DEPTH};
#[derive(Debug, Args)]
pub struct ChopArgs {
#[arg(value_name = "file")]
pub file: PathBuf,
#[arg(value_name = "function")]
pub function: String,
#[arg(value_name = "source_line")]
pub source_line: u32,
#[arg(value_name = "target_line")]
pub target_line: u32,
#[arg(
long = "output-format",
short = 'o',
hide = true,
default_value = "json"
)]
pub output_format: ContractsOutputFormat,
#[arg(long, short = 'l')]
pub lang: Option<Language>,
}
impl ChopArgs {
pub fn run(&self, format: OutputFormat, quiet: bool) -> Result<()> {
let writer = OutputWriter::new(format, quiet);
let canonical_path = validate_file_path(&self.file)?;
validate_function_name(&self.function)?;
let language = self
.lang
.unwrap_or_else(|| Language::from_path(&self.file).unwrap_or(Language::Python));
writer.progress(&format!(
"Computing chop from line {} to line {} in {}::{}...",
self.source_line,
self.target_line,
self.file.display(),
self.function
));
let result = match compute_chop(
canonical_path.to_str().unwrap_or_default(),
&self.function,
self.source_line,
self.target_line,
language,
) {
Ok(r) => r,
Err(e) => {
ChopResult {
lines: vec![],
count: 0,
source_line: self.source_line,
target_line: self.target_line,
path_exists: false,
function: self.function.clone(),
explanation: Some(format!("Analysis could not be completed: {}", e)),
}
}
};
let use_text = matches!(self.output_format, ContractsOutputFormat::Text)
|| matches!(format, OutputFormat::Text);
if use_text {
let text = format_chop_text(&result);
writer.write_text(&text)?;
} else {
writer.write(&result)?;
}
Ok(())
}
}
pub fn compute_chop(
source_or_path: &str,
function_name: &str,
source_line: u32,
target_line: u32,
language: Language,
) -> ContractsResult<ChopResult> {
let _ = MAX_CFG_DEPTH;
if source_line == target_line {
return Ok(ChopResult::same_line(source_line, function_name));
}
if source_line == 0 {
return Err(ContractsError::LineOutsideFunction {
line: source_line,
function: function_name.to_string(),
start: 1,
end: u32::MAX,
});
}
if target_line == 0 {
return Err(ContractsError::LineOutsideFunction {
line: target_line,
function: function_name.to_string(),
start: 1,
end: u32::MAX,
});
}
let forward_slice = get_slice(
source_or_path,
function_name,
source_line,
SliceDirection::Forward,
None,
language,
)
.map_err(|e| {
let err_str = e.to_string();
if err_str.contains("not found") || err_str.contains("Function") {
ContractsError::FunctionNotFound {
function: function_name.to_string(),
file: PathBuf::from(source_or_path),
}
} else if err_str.contains("outside") || err_str.contains("line") {
ContractsError::LineOutsideFunction {
line: source_line,
function: function_name.to_string(),
start: 1,
end: u32::MAX,
}
} else {
ContractsError::ParseError {
file: PathBuf::from(source_or_path),
message: err_str,
}
}
})?;
if forward_slice.is_empty() {
return Err(ContractsError::LineOutsideFunction {
line: source_line,
function: function_name.to_string(),
start: 1,
end: u32::MAX,
});
}
let backward_slice = get_slice(
source_or_path,
function_name,
target_line,
SliceDirection::Backward,
None,
language,
)
.map_err(|e| {
let err_str = e.to_string();
if err_str.contains("outside") || err_str.contains("line") {
ContractsError::LineOutsideFunction {
line: target_line,
function: function_name.to_string(),
start: 1,
end: u32::MAX,
}
} else {
ContractsError::ParseError {
file: PathBuf::from(source_or_path),
message: err_str,
}
}
})?;
if backward_slice.is_empty() {
return Err(ContractsError::LineOutsideFunction {
line: target_line,
function: function_name.to_string(),
start: 1,
end: u32::MAX,
});
}
let path_exists = backward_slice.contains(&source_line);
if !path_exists {
return Ok(ChopResult::no_path(source_line, target_line, function_name));
}
let chop_lines: HashSet<u32> = forward_slice
.intersection(&backward_slice)
.copied()
.collect();
let mut lines: Vec<u32> = chop_lines.into_iter().collect();
lines.sort();
let count = lines.len() as u32;
Ok(ChopResult {
lines,
count,
source_line,
target_line,
path_exists: true,
function: function_name.to_string(),
explanation: Some(format!(
"Found {} lines on the dependency path from line {} to line {}.",
count, source_line, target_line
)),
})
}
fn format_chop_text(result: &ChopResult) -> String {
let mut text = String::new();
text.push_str(&format!(
"Chop Analysis: {} -> {}\n",
result.source_line, result.target_line
));
text.push_str(&format!("Function: {}\n\n", result.function));
if result.path_exists {
text.push_str(&format!(
"Path exists: {} lines on dependency path\n\n",
result.count
));
text.push_str("Lines:\n");
for line in &result.lines {
text.push_str(&format!(" Line {}\n", line));
}
} else {
text.push_str("No dependency path exists.\n");
text.push_str(&format!(
"Line {} does not affect line {}.\n",
result.source_line, result.target_line
));
}
if let Some(ref explanation) = result.explanation {
text.push_str(&format!("\nExplanation: {}\n", explanation));
}
text
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_same_line_chop() {
let result = ChopResult::same_line(42, "test_func");
assert!(result.path_exists);
assert_eq!(result.lines, vec![42]);
assert_eq!(result.count, 1);
assert_eq!(result.source_line, 42);
assert_eq!(result.target_line, 42);
}
#[test]
fn test_no_path_chop() {
let result = ChopResult::no_path(10, 20, "test_func");
assert!(!result.path_exists);
assert!(result.lines.is_empty());
assert_eq!(result.count, 0);
assert_eq!(result.source_line, 10);
assert_eq!(result.target_line, 20);
}
#[test]
fn test_format_with_path() {
let result = ChopResult {
lines: vec![2, 3, 4],
count: 3,
source_line: 2,
target_line: 4,
path_exists: true,
function: "compute".to_string(),
explanation: Some("Found 3 lines".to_string()),
};
let text = format_chop_text(&result);
assert!(text.contains("2 -> 4"));
assert!(text.contains("compute"));
assert!(text.contains("3 lines"));
assert!(text.contains("Line 2"));
assert!(text.contains("Line 3"));
assert!(text.contains("Line 4"));
}
#[test]
fn test_format_no_path() {
let result = ChopResult::no_path(10, 20, "func");
let text = format_chop_text(&result);
assert!(text.contains("No dependency path"));
assert!(text.contains("10"));
assert!(text.contains("20"));
}
}