use std::collections::HashSet;
use std::path::PathBuf;
use anyhow::Result;
use clap::Args;
use tldr_core::ast::function_finder::find_function_bounds_from_path_or_source;
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};
fn resolve_fn_bounds(
source_or_path: &str,
function_name: &str,
language: Language,
) -> Option<(u32, u32)> {
find_function_bounds_from_path_or_source(source_or_path, function_name, language)
}
fn line_outside_with_bounds(
line: u32,
function: &str,
source_or_path: &str,
language: Language,
) -> ContractsError {
match resolve_fn_bounds(source_or_path, function, language) {
Some((start, end)) => {
if line >= start && line <= end {
ContractsError::ParseError {
file: PathBuf::from(source_or_path),
message: format!(
"line {} is within function '{}' (lines {}-{}) but no PDG node \
is anchored there (likely a brace, comment, or part of a \
multi-line statement attributed to a neighbouring line)",
line, function, start, end
),
}
} else {
ContractsError::LineOutsideFunction {
line,
function: function.to_string(),
start,
end,
}
}
}
None => ContractsError::ParseError {
file: PathBuf::from(source_or_path),
message: format!(
"could not determine function bounds for '{}' in source",
function
),
},
}
}
#[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 user_path_str = self.file.display().to_string();
let mut 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 {
file: user_path_str.clone(),
lines: vec![],
count: 0,
line_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)),
}
}
};
result.file = user_path_str;
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(line_outside_with_bounds(
source_line,
function_name,
source_or_path,
language,
));
}
if target_line == 0 {
return Err(line_outside_with_bounds(
target_line,
function_name,
source_or_path,
language,
));
}
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") {
line_outside_with_bounds(source_line, function_name, source_or_path, language)
} else {
ContractsError::ParseError {
file: PathBuf::from(source_or_path),
message: err_str,
}
}
})?;
if forward_slice.is_empty() {
return Err(line_outside_with_bounds(
source_line,
function_name,
source_or_path,
language,
));
}
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") {
line_outside_with_bounds(target_line, function_name, source_or_path, language)
} else {
ContractsError::ParseError {
file: PathBuf::from(source_or_path),
message: err_str,
}
}
})?;
if backward_slice.is_empty() {
return Err(line_outside_with_bounds(
target_line,
function_name,
source_or_path,
language,
));
}
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 {
file: source_or_path.to_string(),
lines,
count,
line_count: 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 {
file: "test.py".to_string(),
lines: vec![2, 3, 4],
count: 3,
line_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"));
}
}