use std::path::PathBuf;
use anyhow::Result;
use clap::Args;
use tldr_core::dataflow::{compute_abstract_interp, AbstractInterpInfo, Nullability};
use tldr_core::{get_cfg_context, get_dfg_context, Language};
use crate::output::OutputFormat;
#[derive(Debug, Args)]
pub struct AbstractInterpArgs {
pub file: PathBuf,
pub function: String,
#[arg(long, short = 'l')]
pub lang: Option<Language>,
#[arg(long)]
pub var: Option<String>,
#[arg(long)]
pub line: Option<usize>,
#[arg(long)]
pub check_zero: Option<String>,
#[arg(long)]
pub check_null: Option<String>,
#[arg(long)]
pub warnings_only: bool,
}
impl AbstractInterpArgs {
pub fn run(&self, format: OutputFormat, quiet: bool) -> Result<()> {
use crate::output::OutputWriter;
let writer = OutputWriter::new(format, quiet);
let language = self.lang.unwrap_or_else(|| {
Language::from_path(&self.file).unwrap_or(Language::Python)
});
let lang_str = match language {
Language::Python => "python",
Language::TypeScript => "typescript",
Language::Go => "go",
Language::Rust => "rust",
Language::JavaScript => "javascript",
_ => "python", };
writer.progress(&format!(
"Analyzing abstract interpretation for {} in {}...",
self.function,
self.file.display()
));
if !self.file.exists() {
return Err(anyhow::anyhow!(
"File not found: {}",
self.file.display()
));
}
let source = std::fs::read_to_string(&self.file)?;
let source_lines: Vec<&str> = source.lines().collect();
let cfg = get_cfg_context(
self.file.to_str().unwrap_or_default(),
&self.function,
language,
)?;
let dfg = get_dfg_context(
self.file.to_str().unwrap_or_default(),
&self.function,
language,
)?;
let result = compute_abstract_interp(&cfg, &dfg, Some(&source_lines), lang_str)?;
if let Some(ref var) = self.var {
return self.handle_var_query(&result, var, &writer);
}
if let Some(line) = self.line {
return self.handle_line_query(&result, line, &writer);
}
if let Some(ref var) = self.check_zero {
return self.handle_check_zero_query(&result, var, &writer);
}
if let Some(ref var) = self.check_null {
return self.handle_check_null_query(&result, var, &writer);
}
if self.warnings_only {
return self.handle_warnings_only(&result, &writer, format);
}
match format {
OutputFormat::Json => {
let json_value = result.to_json();
let json = serde_json::to_string_pretty(&json_value)
.map_err(|e| anyhow::anyhow!("JSON serialization failed: {}", e))?;
writer.write_text(&json)?;
}
OutputFormat::Text => {
let text = self.format_text_output(&result);
writer.write_text(&text)?;
}
OutputFormat::Compact => {
let json_value = result.to_json();
let json = serde_json::to_string(&json_value)
.map_err(|e| anyhow::anyhow!("JSON serialization failed: {}", e))?;
writer.write_text(&json)?;
}
_ => {
let json_value = result.to_json();
let json = serde_json::to_string_pretty(&json_value)
.map_err(|e| anyhow::anyhow!("JSON serialization failed: {}", e))?;
writer.write_text(&json)?;
}
}
Ok(())
}
fn handle_var_query(
&self,
result: &AbstractInterpInfo,
var: &str,
writer: &crate::output::OutputWriter,
) -> Result<()> {
let mut values = Vec::new();
for (block_id, state) in &result.state_out {
let abstract_val = state.values.get(var);
if let Some(val) = abstract_val {
let range_str = val.range_.as_ref().map(|(low, high)| {
let l = low.map_or("?".to_string(), |v| v.to_string());
let h = high.map_or("?".to_string(), |v| v.to_string());
vec![l, h]
});
values.push(serde_json::json!({
"block": block_id,
"type": val.type_,
"range": range_str,
"nullable": format!("{:?}", val.nullable),
"constant": val.constant,
}));
}
}
let output = serde_json::json!({
"variable": var,
"abstract_values": values,
});
let json = serde_json::to_string_pretty(&output)
.map_err(|e| anyhow::anyhow!("JSON serialization failed: {}", e))?;
writer.write_text(&json)?;
Ok(())
}
fn handle_line_query(
&self,
result: &AbstractInterpInfo,
line: usize,
writer: &crate::output::OutputWriter,
) -> Result<()> {
let mut state_at_line = serde_json::Map::new();
for (_block_id, state) in &result.state_out {
for (var, val) in &state.values {
let range_str = val.range_.as_ref().map(|(low, high)| {
let l = low.map_or("?".to_string(), |v| v.to_string());
let h = high.map_or("?".to_string(), |v| v.to_string());
vec![l, h]
});
state_at_line.insert(
var.clone(),
serde_json::json!({
"type": val.type_,
"range": range_str,
"nullable": format!("{:?}", val.nullable),
"constant": val.constant,
}),
);
}
}
let output = serde_json::json!({
"line": line,
"state": state_at_line,
});
let json = serde_json::to_string_pretty(&output)
.map_err(|e| anyhow::anyhow!("JSON serialization failed: {}", e))?;
writer.write_text(&json)?;
Ok(())
}
fn handle_check_zero_query(
&self,
result: &AbstractInterpInfo,
var: &str,
writer: &crate::output::OutputWriter,
) -> Result<()> {
let mut may_be_zero = false;
let mut must_be_zero = false;
let mut range_info: Option<Vec<String>> = None;
for (_block_id, state) in &result.state_out {
if let Some(abstract_val) = state.values.get(var) {
if let Some(ref range) = abstract_val.range_ {
let low = range.0.unwrap_or(i64::MIN);
let high = range.1.unwrap_or(i64::MAX);
if low <= 0 && high >= 0 {
may_be_zero = true;
}
if low == 0 && high == 0 {
must_be_zero = true;
}
range_info = Some(vec![
range.0.map_or("?".to_string(), |v| v.to_string()),
range.1.map_or("?".to_string(), |v| v.to_string()),
]);
}
}
}
let in_warnings = result
.potential_div_zero
.iter()
.any(|(_line, v)| v == var);
let output = serde_json::json!({
"variable": var,
"may_be_zero": may_be_zero,
"must_be_zero": must_be_zero,
"range": range_info,
"flagged_in_warnings": in_warnings,
"safe_for_division": !may_be_zero,
});
let json = serde_json::to_string_pretty(&output)
.map_err(|e| anyhow::anyhow!("JSON serialization failed: {}", e))?;
writer.write_text(&json)?;
Ok(())
}
fn handle_check_null_query(
&self,
result: &AbstractInterpInfo,
var: &str,
writer: &crate::output::OutputWriter,
) -> Result<()> {
let mut nullability = Nullability::Never;
for (_block_id, state) in &result.state_out {
if let Some(abstract_val) = state.values.get(var) {
match abstract_val.nullable {
Nullability::Always => nullability = Nullability::Always,
Nullability::Maybe if nullability != Nullability::Always => {
nullability = Nullability::Maybe
}
_ => {}
}
}
}
let in_warnings = result
.potential_null_deref
.iter()
.any(|(_line, v)| v == var);
let output = serde_json::json!({
"variable": var,
"nullability": format!("{:?}", nullability),
"may_be_null": matches!(nullability, Nullability::Maybe | Nullability::Always),
"must_be_null": matches!(nullability, Nullability::Always),
"flagged_in_warnings": in_warnings,
"safe_for_dereference": matches!(nullability, Nullability::Never),
});
let json = serde_json::to_string_pretty(&output)
.map_err(|e| anyhow::anyhow!("JSON serialization failed: {}", e))?;
writer.write_text(&json)?;
Ok(())
}
fn handle_warnings_only(
&self,
result: &AbstractInterpInfo,
writer: &crate::output::OutputWriter,
format: OutputFormat,
) -> Result<()> {
let output = serde_json::json!({
"potential_division_by_zero": result.potential_div_zero,
"potential_null_dereference": result.potential_null_deref,
"total_warnings": result.potential_div_zero.len() + result.potential_null_deref.len(),
});
match format {
OutputFormat::Text => {
let mut text = String::from("Safety Warnings:\n\n");
if result.potential_div_zero.is_empty() && result.potential_null_deref.is_empty() {
text.push_str(" No warnings detected.\n");
} else {
if !result.potential_div_zero.is_empty() {
text.push_str(" Division by zero risks:\n");
for (line, var) in &result.potential_div_zero {
text.push_str(&format!(" - Line {}: variable '{}' may be zero\n", line, var));
}
}
if !result.potential_null_deref.is_empty() {
text.push_str(" Null dereference risks:\n");
for (line, var) in &result.potential_null_deref {
text.push_str(&format!(" - Line {}: variable '{}' may be null\n", line, var));
}
}
}
writer.write_text(&text)?;
Ok(())
}
_ => {
let json = serde_json::to_string_pretty(&output)
.map_err(|e| anyhow::anyhow!("JSON serialization failed: {}", e))?;
writer.write_text(&json)?;
Ok(())
}
}
}
fn format_text_output(&self, result: &AbstractInterpInfo) -> String {
let mut output = String::new();
output.push_str(&format!(
"Abstract Interpretation: {} in {}\n\n",
self.function,
self.file.display()
));
let total_warnings = result.potential_div_zero.len() + result.potential_null_deref.len();
if total_warnings > 0 {
output.push_str(&format!("Safety Warnings ({}):\n", total_warnings));
for (line, var) in &result.potential_div_zero {
output.push_str(&format!(" [DIV0] Line {}: '{}' may be zero\n", line, var));
}
for (line, var) in &result.potential_null_deref {
output.push_str(&format!(" [NULL] Line {}: '{}' may be null\n", line, var));
}
output.push('\n');
} else {
output.push_str("No safety warnings detected.\n\n");
}
output.push_str("Variable states by block:\n");
let mut blocks: Vec<_> = result.state_out.keys().collect();
blocks.sort();
for block_id in blocks {
if let Some(state) = result.state_out.get(block_id) {
if !state.values.is_empty() {
output.push_str(&format!(" Block {}:\n", block_id));
for (var, val) in &state.values {
let range_str = val
.range_
.as_ref()
.map(|(low, high)| {
let l = low.map_or("?".to_string(), |v| v.to_string());
let h = high.map_or("?".to_string(), |v| v.to_string());
format!("[{}, {}]", l, h)
})
.unwrap_or_else(|| "?".to_string());
let null_str = match val.nullable {
Nullability::Never => "non-null",
Nullability::Maybe => "nullable",
Nullability::Always => "null",
};
output.push_str(&format!(" {}: {} {}\n", var, range_str, null_str));
}
}
}
}
output
}
}