use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use serde::{Deserialize, Serialize};
use serde_json;
use super::types::BlockId;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Nullability {
Never,
#[serde(rename = "maybe")]
Maybe,
Always,
}
impl Default for Nullability {
fn default() -> Self {
Nullability::Maybe
}
}
impl Nullability {
pub fn as_str(&self) -> &'static str {
match self {
Nullability::Never => "never",
Nullability::Maybe => "maybe",
Nullability::Always => "always",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ConstantValue {
Int(i64),
Float(f64),
String(String),
Bool(bool),
Null,
}
impl PartialEq for ConstantValue {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(ConstantValue::Int(a), ConstantValue::Int(b)) => a == b,
(ConstantValue::Float(a), ConstantValue::Float(b)) => {
(a.is_nan() && b.is_nan()) || a == b
}
(ConstantValue::String(a), ConstantValue::String(b)) => a == b,
(ConstantValue::Bool(a), ConstantValue::Bool(b)) => a == b,
(ConstantValue::Null, ConstantValue::Null) => true,
_ => false,
}
}
}
impl ConstantValue {
pub fn to_json_value(&self) -> serde_json::Value {
match self {
ConstantValue::Int(v) => serde_json::json!(v),
ConstantValue::Float(v) => serde_json::json!(v),
ConstantValue::String(v) => serde_json::json!(v),
ConstantValue::Bool(v) => serde_json::json!(v),
ConstantValue::Null => serde_json::Value::Null,
}
}
}
#[derive(Debug, Clone)]
pub struct AbstractValue {
pub type_: Option<String>,
pub range_: Option<(Option<i64>, Option<i64>)>,
pub nullable: Nullability,
pub constant: Option<ConstantValue>,
}
impl PartialEq for AbstractValue {
fn eq(&self, other: &Self) -> bool {
self.type_ == other.type_
&& self.range_ == other.range_
&& self.nullable == other.nullable
&& self.constant == other.constant
}
}
impl Eq for AbstractValue {}
impl Hash for AbstractValue {
fn hash<H: Hasher>(&self, state: &mut H) {
self.type_.hash(state);
self.range_.hash(state);
self.nullable.hash(state);
}
}
impl AbstractValue {
pub fn top() -> Self {
AbstractValue {
type_: None,
range_: None,
nullable: Nullability::Maybe,
constant: None,
}
}
pub fn bottom() -> Self {
AbstractValue {
type_: Some("<bottom>".to_string()),
range_: Some((None, None)),
nullable: Nullability::Never,
constant: None,
}
}
pub fn from_constant(value: ConstantValue) -> Self {
match value {
ConstantValue::Int(v) => AbstractValue {
type_: Some("int".to_string()),
range_: Some((Some(v), Some(v))),
nullable: Nullability::Never,
constant: Some(ConstantValue::Int(v)),
},
ConstantValue::Float(v) => AbstractValue {
type_: Some("float".to_string()),
range_: None, nullable: Nullability::Never,
constant: Some(ConstantValue::Float(v)),
},
ConstantValue::String(ref s) => {
let len = s.len() as i64;
AbstractValue {
type_: Some("str".to_string()),
range_: Some((Some(len), Some(len))),
nullable: Nullability::Never,
constant: Some(value),
}
}
ConstantValue::Bool(v) => AbstractValue {
type_: Some("bool".to_string()),
range_: Some((Some(v as i64), Some(v as i64))),
nullable: Nullability::Never,
constant: Some(ConstantValue::Bool(v)),
},
ConstantValue::Null => AbstractValue {
type_: Some("NoneType".to_string()),
range_: None,
nullable: Nullability::Always,
constant: None, },
}
}
pub fn may_be_zero(&self) -> bool {
match &self.range_ {
None => true, Some((low, high)) => {
let low = low.unwrap_or(i64::MIN);
let high = high.unwrap_or(i64::MAX);
low <= 0 && 0 <= high
}
}
}
pub fn may_be_null(&self) -> bool {
self.nullable != Nullability::Never
}
pub fn is_constant(&self) -> bool {
self.constant.is_some()
}
pub fn to_json_value(&self) -> serde_json::Value {
let mut obj = serde_json::Map::new();
if let Some(ref t) = self.type_ {
obj.insert("type".to_string(), serde_json::json!(t));
}
if let Some((low, high)) = &self.range_ {
let range = serde_json::json!([low, high]);
obj.insert("range".to_string(), range);
}
obj.insert(
"nullable".to_string(),
serde_json::json!(self.nullable.as_str()),
);
if let Some(ref c) = self.constant {
obj.insert("constant".to_string(), c.to_json_value());
}
serde_json::Value::Object(obj)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct AbstractState {
pub values: HashMap<String, AbstractValue>,
}
impl AbstractState {
pub fn new() -> Self {
Self::default()
}
pub fn get(&self, var: &str) -> AbstractValue {
self.values
.get(var)
.cloned()
.unwrap_or_else(AbstractValue::top)
}
pub fn set(&self, var: &str, value: AbstractValue) -> Self {
let mut new_values = self.values.clone();
new_values.insert(var.to_string(), value);
AbstractState { values: new_values }
}
pub fn copy(&self) -> Self {
self.clone()
}
}
#[derive(Debug, Clone, Default)]
pub struct AbstractInterpInfo {
pub state_in: HashMap<BlockId, AbstractState>,
pub state_out: HashMap<BlockId, AbstractState>,
pub potential_div_zero: Vec<(usize, String)>,
pub potential_null_deref: Vec<(usize, String)>,
pub function_name: String,
}
impl AbstractInterpInfo {
pub fn new(function_name: &str) -> Self {
Self {
function_name: function_name.to_string(),
..Default::default()
}
}
pub fn value_at(&self, block: BlockId, var: &str) -> AbstractValue {
self.state_in
.get(&block)
.map(|s| s.get(var))
.unwrap_or_else(AbstractValue::top)
}
pub fn value_at_exit(&self, block: BlockId, var: &str) -> AbstractValue {
self.state_out
.get(&block)
.map(|s| s.get(var))
.unwrap_or_else(AbstractValue::top)
}
pub fn range_at(&self, block: BlockId, var: &str) -> Option<(Option<i64>, Option<i64>)> {
self.value_at(block, var).range_
}
pub fn type_at(&self, block: BlockId, var: &str) -> Option<String> {
self.value_at(block, var).type_
}
pub fn is_definitely_not_null(&self, block: BlockId, var: &str) -> bool {
self.value_at(block, var).nullable == Nullability::Never
}
pub fn get_constants(&self) -> HashMap<String, ConstantValue> {
let mut constants = HashMap::new();
for state in self.state_out.values() {
for (var, val) in &state.values {
if let Some(c) = &val.constant {
constants.insert(var.clone(), c.clone());
}
}
}
constants
}
pub fn to_json(&self) -> serde_json::Value {
let state_in: HashMap<String, serde_json::Value> = self
.state_in
.iter()
.map(|(k, state)| {
let vars: HashMap<String, serde_json::Value> = state
.values
.iter()
.map(|(var, val)| (var.clone(), val.to_json_value()))
.collect();
(k.to_string(), serde_json::json!(vars))
})
.collect();
let state_out: HashMap<String, serde_json::Value> = self
.state_out
.iter()
.map(|(k, state)| {
let vars: HashMap<String, serde_json::Value> = state
.values
.iter()
.map(|(var, val)| (var.clone(), val.to_json_value()))
.collect();
(k.to_string(), serde_json::json!(vars))
})
.collect();
let div_zero: Vec<_> = self
.potential_div_zero
.iter()
.map(|(line, var)| serde_json::json!({"line": line, "var": var}))
.collect();
let null_deref: Vec<_> = self
.potential_null_deref
.iter()
.map(|(line, var)| serde_json::json!({"line": line, "var": var}))
.collect();
serde_json::json!({
"function": self.function_name,
"state_in": state_in,
"state_out": state_out,
"potential_div_zero": div_zero,
"potential_null_deref": null_deref,
})
}
}
pub fn get_null_keywords(language: &str) -> Vec<&'static str> {
match language.to_lowercase().as_str() {
"python" => vec!["None"],
"typescript" | "javascript" => vec!["null", "undefined"],
"go" => vec!["nil"],
"rust" => vec![], "java" | "kotlin" | "csharp" | "c#" => vec!["null"],
"swift" => vec!["nil"],
_ => vec!["null", "nil", "None"], }
}
pub fn get_boolean_keywords(language: &str) -> HashMap<&'static str, bool> {
match language.to_lowercase().as_str() {
"python" => [("True", true), ("False", false)].into_iter().collect(),
"typescript" | "javascript" | "go" | "rust" | "java" | "kotlin" | "csharp" | "c#"
| "swift" => [("true", true), ("false", false)].into_iter().collect(),
_ => {
[
("True", true),
("False", false),
("true", true),
("false", false),
]
.into_iter()
.collect()
}
}
}
pub fn get_comment_pattern(language: &str) -> &'static str {
match language.to_lowercase().as_str() {
"python" => "#",
"typescript" | "javascript" | "go" | "rust" | "java" | "csharp" | "c#" | "kotlin"
| "swift" => "//",
_ => "#", }
}
pub fn strip_comment<'a>(line: &'a str, language: &str) -> &'a str {
let pattern = get_comment_pattern(language);
let mut in_string = false;
let mut string_char: Option<char> = None;
let mut escape_next = false;
for (i, c) in line.char_indices() {
if escape_next {
escape_next = false;
continue;
}
if c == '\\' {
escape_next = true;
continue;
}
if in_string {
if Some(c) == string_char {
in_string = false;
string_char = None;
}
} else if c == '"' || c == '\'' {
in_string = true;
string_char = Some(c);
} else if line[i..].starts_with(pattern) {
return &line[..i];
}
}
line
}
pub fn strip_strings(line: &str, language: &str) -> String {
let bytes = line.as_bytes();
let len = bytes.len();
let mut result = String::with_capacity(len);
let mut i = 0;
while i < len {
let c = bytes[i];
if language == "rust" && c == b'r' {
let mut hashes = 0;
let mut j = i + 1;
while j < len && bytes[j] == b'#' {
hashes += 1;
j += 1;
}
if j < len && bytes[j] == b'"' {
for &b in &bytes[i..=j] {
result.push(b as char);
}
i = j + 1;
let close_start = b'"';
loop {
if i >= len {
break; }
if bytes[i] == close_start {
let mut matched = 0;
let mut k = i + 1;
while k < len && bytes[k] == b'#' && matched < hashes {
matched += 1;
k += 1;
}
if matched == hashes {
for &b in &bytes[i..k] {
result.push(b as char);
}
i = k;
break;
}
}
result.push(' ');
i += 1;
}
continue;
}
}
if c == b'"' || c == b'\'' || c == b'`' {
let delim = c;
result.push(c as char);
i += 1;
while i < len {
if bytes[i] == b'\\' {
result.push(' ');
i += 1;
if i < len {
result.push(' ');
i += 1;
}
} else if bytes[i] == delim {
result.push(delim as char);
i += 1;
break;
} else {
result.push(' ');
i += 1;
}
}
continue;
}
result.push(c as char);
i += 1;
}
result
}
pub fn is_identifier(s: &str) -> bool {
if s.is_empty() {
return false;
}
let mut chars = s.chars();
match chars.next() {
Some(c) if c.is_alphabetic() || c == '_' => {}
_ => return false,
}
chars.all(|c| c.is_alphanumeric() || c == '_')
}
pub fn extract_rhs(line: &str, var: &str) -> Option<String> {
let line = line.trim();
let augmented_ops = &[
("+=", '+'),
("-=", '-'),
("*=", '*'),
("/=", '/'),
("%=", '%'),
];
for (op_str, op_char) in augmented_ops {
let pattern_spaced = format!("{} {} ", var, op_str);
let pattern_left_space = format!("{} {}", var, op_str);
let pattern_right_space = format!("{}{} ", var, op_str);
let pattern_no_space = format!("{}{}", var, op_str);
if let Some(idx) = line.find(&pattern_spaced) {
if idx == 0
|| !line[..idx]
.chars()
.last()
.map(|c| c.is_alphanumeric() || c == '_')
.unwrap_or(false)
{
let rhs_start = idx + pattern_spaced.len();
let rhs = line[rhs_start..].trim();
return Some(format!("{} {} {}", var, op_char, rhs));
}
}
if let Some(idx) = line.find(&pattern_left_space) {
if idx == 0
|| !line[..idx]
.chars()
.last()
.map(|c| c.is_alphanumeric() || c == '_')
.unwrap_or(false)
{
let rhs_start = idx + pattern_left_space.len();
let rhs = line[rhs_start..].trim();
return Some(format!("{} {} {}", var, op_char, rhs));
}
}
if let Some(idx) = line.find(&pattern_right_space) {
if idx == 0
|| !line[..idx]
.chars()
.last()
.map(|c| c.is_alphanumeric() || c == '_')
.unwrap_or(false)
{
let rhs_start = idx + pattern_right_space.len();
let rhs = line[rhs_start..].trim();
return Some(format!("{} {} {}", var, op_char, rhs));
}
}
if let Some(idx) = line.find(&pattern_no_space) {
if idx == 0
|| !line[..idx]
.chars()
.last()
.map(|c| c.is_alphanumeric() || c == '_')
.unwrap_or(false)
{
let rhs_start = idx + pattern_no_space.len();
let rhs = line[rhs_start..].trim();
return Some(format!("{} {} {}", var, op_char, rhs));
}
}
}
let patterns = [
format!("{} = ", var),
format!("{}= ", var),
format!("{} =", var),
format!("{}=", var),
];
for pattern in &patterns {
if let Some(idx) = line.find(pattern) {
let valid_start = idx == 0
|| !line[..idx]
.chars()
.last()
.map(|c| c.is_alphanumeric() || c == '_')
.unwrap_or(false);
if valid_start {
let rhs_start = idx + pattern.len();
return Some(line[rhs_start..].trim().to_string());
}
}
}
let walrus_pattern = format!("{} := ", var);
if let Some(idx) = line.find(&walrus_pattern) {
let valid_start = idx == 0
|| !line[..idx]
.chars()
.last()
.map(|c| c.is_alphanumeric() || c == '_')
.unwrap_or(false);
if valid_start {
let rhs_start = idx + walrus_pattern.len();
return Some(line[rhs_start..].trim().to_string());
}
}
None
}
pub fn parse_simple_arithmetic(rhs: &str) -> Option<(String, char, i64)> {
let rhs = rhs.trim();
for op in ['+', '-', '*'] {
let parts: Vec<&str> = if rhs.contains(&format!(" {} ", op)) {
rhs.splitn(2, &format!(" {} ", op)).collect()
} else if rhs.contains(op) {
rhs.splitn(2, op).collect()
} else {
continue;
};
if parts.len() != 2 {
continue;
}
let left = parts[0].trim();
let right = parts[1].trim();
if is_identifier(left) {
if let Ok(c) = right.parse::<i64>() {
return Some((left.to_string(), op, c));
}
}
if op == '+' || op == '*' {
if let Ok(c) = left.parse::<i64>() {
if is_identifier(right) {
return Some((right.to_string(), op, c));
}
}
}
}
None
}
pub fn parse_rhs_abstract(
line: &str,
var: &str,
state: &AbstractState,
language: &str,
) -> AbstractValue {
let line = strip_comment(line, language);
let rhs = match extract_rhs(line, var) {
Some(r) => r,
None => return AbstractValue::top(),
};
let rhs = rhs.trim();
if rhs.is_empty() {
return AbstractValue::top();
}
if let Ok(v) = rhs.parse::<i64>() {
return AbstractValue::from_constant(ConstantValue::Int(v));
}
if rhs.contains('.') || rhs.to_lowercase().contains('e') {
if let Ok(v) = rhs.parse::<f64>() {
return AbstractValue::from_constant(ConstantValue::Float(v));
}
}
if (rhs.starts_with('"') && rhs.ends_with('"') && rhs.len() >= 2)
|| (rhs.starts_with('\'') && rhs.ends_with('\'') && rhs.len() >= 2)
{
let s = rhs[1..rhs.len() - 1].to_string();
return AbstractValue::from_constant(ConstantValue::String(s));
}
if (rhs.starts_with("\"\"\"") && rhs.ends_with("\"\"\"") && rhs.len() >= 6)
|| (rhs.starts_with("'''") && rhs.ends_with("'''") && rhs.len() >= 6)
{
let s = rhs[3..rhs.len() - 3].to_string();
return AbstractValue::from_constant(ConstantValue::String(s));
}
let null_keywords = get_null_keywords(language);
if null_keywords.contains(&rhs) {
if rhs == "undefined" {
return AbstractValue {
type_: Some("undefined".to_string()),
range_: None,
nullable: Nullability::Always,
constant: None,
};
}
return AbstractValue::from_constant(ConstantValue::Null);
}
let bool_keywords = get_boolean_keywords(language);
if let Some(&b) = bool_keywords.get(rhs) {
return AbstractValue::from_constant(ConstantValue::Bool(b));
}
if is_identifier(rhs) {
return state.get(rhs);
}
if let Some((operand_var, op, constant)) = parse_simple_arithmetic(rhs) {
let operand_value = state.get(&operand_var);
return apply_arithmetic(&operand_value, op, constant);
}
AbstractValue::top()
}
pub fn apply_arithmetic(operand: &AbstractValue, op: char, constant: i64) -> AbstractValue {
let new_range = operand.range_.map(|(low, high)| {
match op {
'+' => {
let new_low = low.and_then(|l| {
let result = l.saturating_add(constant);
if (constant > 0 && result == i64::MAX && l != i64::MAX - constant)
|| (constant < 0 && result == i64::MIN && l != i64::MIN - constant)
{
return None;
}
Some(result)
});
let new_high = high.and_then(|h| {
let result = h.saturating_add(constant);
if (constant > 0 && result == i64::MAX && h != i64::MAX - constant)
|| (constant < 0 && result == i64::MIN && h != i64::MIN - constant)
{
return None;
}
Some(result)
});
(new_low, new_high)
}
'-' => {
let new_low = low.and_then(|l| {
let result = l.saturating_sub(constant);
if (constant > 0 && result == i64::MIN && l != i64::MIN + constant)
|| (constant < 0 && result == i64::MAX && l != i64::MAX + constant)
{
return None;
}
Some(result)
});
let new_high = high.and_then(|h| {
let result = h.saturating_sub(constant);
if (constant > 0 && result == i64::MIN && h != i64::MIN + constant)
|| (constant < 0 && result == i64::MAX && h != i64::MAX + constant)
{
return None;
}
Some(result)
});
(new_low, new_high)
}
'*' => {
let compute_mul = |bound: Option<i64>| -> Option<i64> {
bound.and_then(|b| {
if constant == 0 {
return Some(0);
}
b.checked_mul(constant)
})
};
let low_mul = compute_mul(low);
let high_mul = compute_mul(high);
if constant < 0 {
(high_mul, low_mul)
} else if constant == 0 {
(Some(0), Some(0))
} else {
(low_mul, high_mul)
}
}
_ => {
(None, None)
}
}
});
let new_constant = if operand.is_constant() {
if let Some((Some(l), Some(h))) = new_range {
if l == h {
Some(ConstantValue::Int(l))
} else {
None
}
} else {
None
}
} else {
None
};
AbstractValue {
type_: operand.type_.clone(),
range_: new_range,
nullable: operand.nullable,
constant: new_constant,
}
}
pub fn join_values(a: &AbstractValue, b: &AbstractValue) -> AbstractValue {
let joined_range = match (&a.range_, &b.range_) {
(None, None) => None,
(Some(r), None) | (None, Some(r)) => Some(*r),
(Some((a_low, a_high)), Some((b_low, b_high))) => {
let low = match (a_low, b_low) {
(None, _) | (_, None) => None,
(Some(a), Some(b)) => Some(std::cmp::min(*a, *b)),
};
let high = match (a_high, b_high) {
(None, _) | (_, None) => None,
(Some(a), Some(b)) => Some(std::cmp::max(*a, *b)),
};
Some((low, high))
}
};
let joined_type = if a.type_ == b.type_ {
a.type_.clone()
} else {
None
};
let joined_nullable = match (a.nullable, b.nullable) {
(Nullability::Never, Nullability::Never) => Nullability::Never,
(Nullability::Always, Nullability::Always) => Nullability::Always,
_ => Nullability::Maybe,
};
let joined_constant = match (&a.constant, &b.constant) {
(Some(ca), Some(cb)) if ca == cb => Some(ca.clone()),
_ => None,
};
AbstractValue {
type_: joined_type,
range_: joined_range,
nullable: joined_nullable,
constant: joined_constant,
}
}
pub fn join_states(states: &[&AbstractState]) -> AbstractState {
if states.is_empty() {
return AbstractState::default();
}
if states.len() == 1 {
return states[0].clone();
}
let all_vars: std::collections::HashSet<_> = states
.iter()
.flat_map(|s| s.values.keys().cloned())
.collect();
let mut result = HashMap::new();
for var in all_vars {
let values: Vec<AbstractValue> = states.iter().map(|s| s.get(&var)).collect();
let mut joined = values[0].clone();
for val in values.iter().skip(1) {
joined = join_values(&joined, val);
}
result.insert(var, joined);
}
AbstractState { values: result }
}
pub fn widen_value(old: &AbstractValue, new: &AbstractValue) -> AbstractValue {
let widened_range = match (&old.range_, &new.range_) {
(None, None) => None,
(None, r) => *r,
(_, None) => None, (Some((old_low, old_high)), Some((new_low, new_high))) => {
let widened_low = match (old_low, new_low) {
(None, _) => None, (_, None) => None, (Some(o), Some(n)) if *n < *o => None, (_, n) => *n, };
let widened_high = match (old_high, new_high) {
(None, _) => None, (_, None) => None, (Some(o), Some(n)) if *n > *o => None, (_, n) => *n, };
Some((widened_low, widened_high))
}
};
AbstractValue {
type_: new.type_.clone(),
range_: widened_range,
nullable: new.nullable,
constant: None, }
}
pub fn widen_state(old: &AbstractState, new: &AbstractState) -> AbstractState {
let all_vars: std::collections::HashSet<_> = old
.values
.keys()
.chain(new.values.keys())
.cloned()
.collect();
let mut result = HashMap::new();
for var in all_vars {
let old_val = old.get(&var);
let new_val = new.get(&var);
result.insert(var, widen_value(&old_val, &new_val));
}
AbstractState { values: result }
}
use super::types::{
build_predecessors, find_back_edges, reverse_postorder, validate_cfg, DataflowError,
};
use crate::types::{CfgInfo, DfgInfo, RefType, VarRef};
pub fn init_params(cfg: &CfgInfo, dfg: &DfgInfo) -> AbstractState {
let mut state = AbstractState::new();
let entry_block = cfg.blocks.iter().find(|b| b.id == cfg.entry_block);
if let Some(entry) = entry_block {
for var_ref in &dfg.refs {
if var_ref.ref_type == RefType::Definition {
if var_ref.line >= entry.lines.0 && var_ref.line <= entry.lines.1 {
state
.values
.insert(var_ref.name.clone(), AbstractValue::top());
}
}
}
}
state
}
pub fn transfer_block(
state: &AbstractState,
block: &crate::types::CfgBlock,
dfg: &DfgInfo,
source_lines: Option<&[&str]>,
language: &str,
) -> AbstractState {
let mut current_state = state.clone();
let mut defs_in_block: Vec<&VarRef> = dfg
.refs
.iter()
.filter(|r| {
r.ref_type == RefType::Definition && r.line >= block.lines.0 && r.line <= block.lines.1
})
.collect();
defs_in_block.sort_by_key(|r| (r.line, r.column));
for var_ref in defs_in_block {
let new_value = if let Some(lines) = source_lines {
let line_idx = var_ref.line.saturating_sub(1) as usize;
if line_idx < lines.len() {
let line = lines[line_idx];
parse_rhs_abstract(line, &var_ref.name, ¤t_state, language)
} else {
AbstractValue::top()
}
} else {
AbstractValue::top()
};
current_state = current_state.set(&var_ref.name, new_value);
}
current_state
}
pub fn find_div_zero(
cfg: &CfgInfo,
dfg: &DfgInfo,
state_in: &HashMap<BlockId, AbstractState>,
source_lines: Option<&[&str]>,
_state_out: &HashMap<BlockId, AbstractState>,
language: &str,
) -> Vec<(usize, String)> {
let mut warnings = Vec::new();
let Some(lines) = source_lines else {
return warnings;
};
let div_patterns: &[&str] = match language {
"python" => &["/", "//", "%"],
"rust" | "go" | "typescript" | "javascript" | "java" | "c" | "cpp" => &["/", "%"],
_ => &["/", "%"],
};
for (line_idx, line) in lines.iter().enumerate() {
let line_num = line_idx + 1;
let code_no_comments = strip_comment(line, language);
let code = strip_strings(code_no_comments, language);
for &op in div_patterns {
let mut search_start = 0;
while let Some(pos) = code[search_start..].find(op) {
let actual_pos = search_start + pos;
if op == "/" && code.len() > actual_pos + 1 {
let next_char = code.chars().nth(actual_pos + 1);
if next_char == Some('/') {
search_start = actual_pos + 2;
continue;
}
if actual_pos > 0 && code.chars().nth(actual_pos - 1) == Some('/') {
search_start = actual_pos + 1;
continue;
}
}
let after_op = &code[actual_pos + op.len()..];
let divisor = extract_divisor(after_op.trim());
if let Some(div_var) = divisor {
if is_identifier(&div_var) {
let block = cfg
.blocks
.iter()
.find(|b| line_num as u32 >= b.lines.0 && line_num as u32 <= b.lines.1);
if let Some(block) = block {
let state_at_div = compute_state_at_line(
block,
dfg,
state_in.get(&block.id).cloned().unwrap_or_default(),
source_lines,
line_num,
language,
);
let divisor_val = state_at_div.get(&div_var);
if divisor_val.may_be_zero() {
warnings.push((line_num, div_var));
}
}
}
}
search_start = actual_pos + op.len();
}
}
}
warnings.sort();
warnings.dedup();
warnings
}
fn extract_divisor(s: &str) -> Option<String> {
let s = s.trim();
if s.is_empty() {
return None;
}
let mut chars = s.chars().peekable();
if chars.peek() == Some(&'(') {
return None;
}
let mut ident = String::new();
while let Some(&c) = chars.peek() {
if c.is_alphanumeric() || c == '_' {
ident.push(c);
chars.next();
} else {
break;
}
}
if ident.is_empty() || ident.chars().next().unwrap().is_ascii_digit() {
None
} else {
Some(ident)
}
}
fn compute_state_at_line(
block: &crate::types::CfgBlock,
dfg: &DfgInfo,
state_in: AbstractState,
source_lines: Option<&[&str]>,
target_line: usize,
language: &str,
) -> AbstractState {
let mut current_state = state_in;
let mut defs_in_block: Vec<&VarRef> = dfg
.refs
.iter()
.filter(|r| {
r.ref_type == RefType::Definition
&& r.line >= block.lines.0
&& r.line <= block.lines.1
&& (r.line as usize) < target_line })
.collect();
defs_in_block.sort_by_key(|r| (r.line, r.column));
for var_ref in defs_in_block {
let new_value = if let Some(lines) = source_lines {
let line_idx = var_ref.line.saturating_sub(1) as usize;
if line_idx < lines.len() {
let line = lines[line_idx];
parse_rhs_abstract(line, &var_ref.name, ¤t_state, language)
} else {
AbstractValue::top()
}
} else {
AbstractValue::top()
};
current_state = current_state.set(&var_ref.name, new_value);
}
current_state
}
pub fn find_null_deref(
cfg: &CfgInfo,
dfg: &DfgInfo,
state_in: &HashMap<BlockId, AbstractState>,
source_lines: Option<&[&str]>,
language: &str,
) -> Vec<(usize, String)> {
let mut warnings = Vec::new();
let Some(lines) = source_lines else {
return warnings;
};
for (line_idx, line) in lines.iter().enumerate() {
let line_num = line_idx + 1;
let code = strip_comment(line, language);
let patterns = extract_deref_patterns(code);
for var in patterns {
if is_identifier(&var) {
let block = cfg
.blocks
.iter()
.find(|b| line_num as u32 >= b.lines.0 && line_num as u32 <= b.lines.1);
if let Some(block) = block {
let state_at_deref = compute_state_at_line(
block,
dfg,
state_in.get(&block.id).cloned().unwrap_or_default(),
source_lines,
line_num,
language,
);
let var_val = state_at_deref.get(&var);
if var_val.may_be_null() {
warnings.push((line_num, var));
}
}
}
}
}
warnings.sort();
warnings.dedup();
warnings
}
fn extract_deref_patterns(code: &str) -> Vec<String> {
let mut patterns = Vec::new();
let chars: Vec<char> = code.chars().collect();
let len = chars.len();
let mut i = 0;
while i < len {
while i < len && !chars[i].is_alphabetic() && chars[i] != '_' {
i += 1;
}
if i >= len {
break;
}
let start = i;
while i < len && (chars[i].is_alphanumeric() || chars[i] == '_') {
i += 1;
}
let ident: String = chars[start..i].iter().collect();
if i < len && (chars[i] == '.' || chars[i] == '[') {
if !ident.is_empty() && !ident.chars().next().unwrap().is_ascii_digit() {
let keywords = ["self", "this", "super", "cls"];
if !keywords.contains(&ident.as_str()) {
patterns.push(ident);
}
}
}
}
patterns
}
pub fn compute_abstract_interp(
cfg: &CfgInfo,
dfg: &DfgInfo,
source_lines: Option<&[&str]>,
language: &str,
) -> Result<AbstractInterpInfo, DataflowError> {
validate_cfg(cfg)?;
let predecessors = build_predecessors(cfg);
let loop_headers = find_back_edges(cfg);
let block_order = reverse_postorder(cfg);
let mut state_in: HashMap<BlockId, AbstractState> = HashMap::new();
let mut state_out: HashMap<BlockId, AbstractState> = HashMap::new();
let entry = cfg.entry_block;
let init_state = init_params(cfg, dfg);
state_in.insert(entry, init_state.clone());
if let Some(entry_block) = cfg.blocks.iter().find(|b| b.id == entry) {
let entry_out = transfer_block(&init_state, entry_block, dfg, source_lines, language);
state_out.insert(entry, entry_out);
} else {
state_out.insert(entry, init_state);
}
for block in &cfg.blocks {
if block.id != entry {
state_in.insert(block.id, AbstractState::default());
state_out.insert(block.id, AbstractState::default());
}
}
let max_iterations = cfg.blocks.len() * 10 + 100;
let mut iteration = 0;
let mut changed = true;
while changed && iteration < max_iterations {
changed = false;
iteration += 1;
for &block_id in &block_order {
if block_id == entry {
continue;
}
let block = match cfg.blocks.iter().find(|b| b.id == block_id) {
Some(b) => b,
None => continue,
};
let preds = predecessors.get(&block_id).cloned().unwrap_or_default();
let mut new_in = if preds.is_empty() {
AbstractState::default()
} else {
let pred_states: Vec<&AbstractState> =
preds.iter().filter_map(|p| state_out.get(p)).collect();
if pred_states.is_empty() {
AbstractState::default()
} else {
join_states(&pred_states)
}
};
if loop_headers.contains(&block_id) {
if let Some(old_in) = state_in.get(&block_id) {
new_in = widen_state(old_in, &new_in);
}
}
let new_out = transfer_block(&new_in, block, dfg, source_lines, language);
let old_in = state_in.get(&block_id);
let old_out = state_out.get(&block_id);
if old_in != Some(&new_in) || old_out != Some(&new_out) {
changed = true;
state_in.insert(block_id, new_in);
state_out.insert(block_id, new_out);
}
}
}
let potential_div_zero = find_div_zero(cfg, dfg, &state_in, source_lines, &state_out, language);
let potential_null_deref = find_null_deref(cfg, dfg, &state_in, source_lines, language);
Ok(AbstractInterpInfo {
state_in,
state_out,
potential_div_zero,
potential_null_deref,
function_name: cfg.function.clone(),
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::hash_map::DefaultHasher;
use std::f64::consts::PI;
#[test]
fn test_nullability_enum_has_three_values() {
let _never = Nullability::Never;
let _maybe = Nullability::Maybe;
let _always = Nullability::Always;
assert_eq!(Nullability::Never.as_str(), "never");
assert_eq!(Nullability::Maybe.as_str(), "maybe");
assert_eq!(Nullability::Always.as_str(), "always");
}
#[test]
fn test_nullability_default_is_maybe() {
let default: Nullability = Default::default();
assert_eq!(default, Nullability::Maybe);
}
#[test]
fn test_abstract_value_has_required_fields() {
let value = AbstractValue {
type_: Some("int".to_string()),
range_: Some((Some(1), Some(10))),
nullable: Nullability::Never,
constant: Some(ConstantValue::Int(5)),
};
assert_eq!(value.type_, Some("int".to_string()));
assert_eq!(value.range_, Some((Some(1), Some(10))));
assert_eq!(value.nullable, Nullability::Never);
assert!(value.constant.is_some());
}
#[test]
fn test_abstract_value_is_hashable() {
let value1 = AbstractValue::from_constant(ConstantValue::Int(5));
let value2 = AbstractValue::from_constant(ConstantValue::Int(5));
let mut hasher1 = DefaultHasher::new();
let mut hasher2 = DefaultHasher::new();
value1.hash(&mut hasher1);
value2.hash(&mut hasher2);
assert_eq!(hasher1.finish(), hasher2.finish());
}
#[test]
fn test_abstract_value_top_creates_unknown() {
let top = AbstractValue::top();
assert_eq!(top.type_, None);
assert_eq!(top.range_, None);
assert_eq!(top.nullable, Nullability::Maybe);
assert!(top.constant.is_none());
}
#[test]
fn test_abstract_value_bottom_creates_contradiction() {
let bottom = AbstractValue::bottom();
assert_eq!(bottom.type_, Some("<bottom>".to_string()));
assert_eq!(bottom.range_, Some((None, None)));
assert_eq!(bottom.nullable, Nullability::Never);
assert!(bottom.constant.is_none());
}
#[test]
fn test_abstract_value_from_constant_int() {
let value = AbstractValue::from_constant(ConstantValue::Int(5));
assert_eq!(value.type_, Some("int".to_string()));
assert_eq!(value.range_, Some((Some(5), Some(5))));
assert_eq!(value.nullable, Nullability::Never);
assert_eq!(value.constant, Some(ConstantValue::Int(5)));
}
#[test]
fn test_abstract_value_from_constant_negative_int() {
let value = AbstractValue::from_constant(ConstantValue::Int(-42));
assert_eq!(value.type_, Some("int".to_string()));
assert_eq!(value.range_, Some((Some(-42), Some(-42))));
assert_eq!(value.nullable, Nullability::Never);
assert_eq!(value.constant, Some(ConstantValue::Int(-42)));
}
#[test]
fn test_abstract_value_from_constant_string() {
let value = AbstractValue::from_constant(ConstantValue::String("hello".to_string()));
assert_eq!(value.type_, Some("str".to_string()));
assert_eq!(value.nullable, Nullability::Never);
assert!(value.constant.is_some());
}
#[test]
fn test_abstract_value_string_tracks_length() {
let value = AbstractValue::from_constant(ConstantValue::String("hello".to_string()));
assert_eq!(value.range_, Some((Some(5), Some(5))));
}
#[test]
fn test_abstract_value_from_constant_none() {
let value = AbstractValue::from_constant(ConstantValue::Null);
assert_eq!(value.type_, Some("NoneType".to_string()));
assert_eq!(value.range_, None);
assert_eq!(value.nullable, Nullability::Always);
assert!(value.constant.is_none()); }
#[test]
fn test_abstract_value_from_constant_bool() {
let value_true = AbstractValue::from_constant(ConstantValue::Bool(true));
let value_false = AbstractValue::from_constant(ConstantValue::Bool(false));
assert_eq!(value_true.type_, Some("bool".to_string()));
assert_eq!(value_true.range_, Some((Some(1), Some(1)))); assert_eq!(value_false.range_, Some((Some(0), Some(0)))); }
#[test]
fn test_abstract_value_from_constant_float() {
let value = AbstractValue::from_constant(ConstantValue::Float(PI));
assert_eq!(value.type_, Some("float".to_string()));
assert_eq!(value.range_, None); assert_eq!(value.nullable, Nullability::Never);
}
#[test]
fn test_may_be_zero_returns_true_when_range_includes_zero() {
let value = AbstractValue {
type_: Some("int".to_string()),
range_: Some((Some(-5), Some(5))),
nullable: Nullability::Never,
constant: None,
};
assert!(value.may_be_zero());
let exact_zero = AbstractValue::from_constant(ConstantValue::Int(0));
assert!(exact_zero.may_be_zero());
}
#[test]
fn test_may_be_zero_returns_false_when_range_excludes_zero() {
let positive = AbstractValue {
type_: Some("int".to_string()),
range_: Some((Some(1), Some(10))),
nullable: Nullability::Never,
constant: None,
};
assert!(!positive.may_be_zero());
let negative = AbstractValue {
type_: Some("int".to_string()),
range_: Some((Some(-10), Some(-1))),
nullable: Nullability::Never,
constant: None,
};
assert!(!negative.may_be_zero());
}
#[test]
fn test_may_be_zero_returns_true_for_unknown_range() {
let top = AbstractValue::top();
assert!(top.may_be_zero());
}
#[test]
fn test_may_be_null_for_maybe() {
let value = AbstractValue {
type_: None,
range_: None,
nullable: Nullability::Maybe,
constant: None,
};
assert!(value.may_be_null());
}
#[test]
fn test_may_be_null_for_never() {
let value = AbstractValue::from_constant(ConstantValue::Int(5));
assert!(!value.may_be_null());
}
#[test]
fn test_may_be_null_for_always() {
let value = AbstractValue::from_constant(ConstantValue::Null);
assert!(value.may_be_null());
}
#[test]
fn test_is_constant_true_when_constant_set() {
let value = AbstractValue::from_constant(ConstantValue::Int(42));
assert!(value.is_constant());
}
#[test]
fn test_is_constant_false_when_constant_none() {
let value = AbstractValue::top();
assert!(!value.is_constant());
}
#[test]
fn test_abstract_state_empty_initialization() {
let state = AbstractState::new();
assert!(state.values.is_empty());
}
#[test]
fn test_abstract_state_get_returns_value_for_existing_var() {
let mut state = AbstractState::new();
let value = AbstractValue::from_constant(ConstantValue::Int(5));
state.values.insert("x".to_string(), value.clone());
let retrieved = state.get("x");
assert_eq!(retrieved.range_, Some((Some(5), Some(5))));
}
#[test]
fn test_abstract_state_get_returns_top_for_missing_var() {
let state = AbstractState::new();
let value = state.get("nonexistent");
assert_eq!(value.type_, None);
assert_eq!(value.range_, None);
assert_eq!(value.nullable, Nullability::Maybe);
}
#[test]
fn test_abstract_state_set_returns_new_state() {
let state1 = AbstractState::new();
let state2 = state1.set("x", AbstractValue::from_constant(ConstantValue::Int(5)));
assert!(state1.values.is_empty());
assert!(state2.values.contains_key("x"));
}
#[test]
fn test_abstract_state_copy_creates_independent_copy() {
let mut state1 = AbstractState::new();
state1.values.insert(
"x".to_string(),
AbstractValue::from_constant(ConstantValue::Int(5)),
);
let state2 = state1.copy();
state1.values.insert(
"y".to_string(),
AbstractValue::from_constant(ConstantValue::Int(10)),
);
assert!(state2.values.contains_key("x"));
assert!(!state2.values.contains_key("y"));
}
#[test]
fn test_abstract_state_equality() {
let state1 =
AbstractState::new().set("x", AbstractValue::from_constant(ConstantValue::Int(5)));
let state2 =
AbstractState::new().set("x", AbstractValue::from_constant(ConstantValue::Int(5)));
let state3 =
AbstractState::new().set("x", AbstractValue::from_constant(ConstantValue::Int(10)));
assert_eq!(state1, state2);
assert_ne!(state1, state3);
}
#[test]
fn test_abstract_interp_info_has_required_fields() {
let info = AbstractInterpInfo::new("test_func");
assert_eq!(info.function_name, "test_func");
assert!(info.state_in.is_empty());
assert!(info.state_out.is_empty());
assert!(info.potential_div_zero.is_empty());
assert!(info.potential_null_deref.is_empty());
}
#[test]
fn test_value_at_returns_abstract_value_at_block_entry() {
let mut info = AbstractInterpInfo::new("test");
let state =
AbstractState::new().set("x", AbstractValue::from_constant(ConstantValue::Int(42)));
info.state_in.insert(0, state);
let value = info.value_at(0, "x");
assert_eq!(value.range_, Some((Some(42), Some(42))));
}
#[test]
fn test_value_at_returns_top_for_missing_block() {
let info = AbstractInterpInfo::new("test");
let value = info.value_at(999, "x");
assert_eq!(value.type_, None);
assert_eq!(value.range_, None);
}
#[test]
fn test_value_at_exit_returns_value_at_block_exit() {
let mut info = AbstractInterpInfo::new("test");
let state =
AbstractState::new().set("y", AbstractValue::from_constant(ConstantValue::Int(100)));
info.state_out.insert(1, state);
let value = info.value_at_exit(1, "y");
assert_eq!(value.range_, Some((Some(100), Some(100))));
}
#[test]
fn test_range_at_returns_range_tuple() {
let mut info = AbstractInterpInfo::new("test");
let state =
AbstractState::new().set("x", AbstractValue::from_constant(ConstantValue::Int(5)));
info.state_in.insert(0, state);
let range = info.range_at(0, "x");
assert_eq!(range, Some((Some(5), Some(5))));
}
#[test]
fn test_type_at_returns_inferred_type() {
let mut info = AbstractInterpInfo::new("test");
let state = AbstractState::new().set(
"x",
AbstractValue::from_constant(ConstantValue::String("hello".to_string())),
);
info.state_in.insert(0, state);
let type_ = info.type_at(0, "x");
assert_eq!(type_, Some("str".to_string()));
}
#[test]
fn test_is_definitely_not_null_for_never_nullable() {
let mut info = AbstractInterpInfo::new("test");
let state =
AbstractState::new().set("x", AbstractValue::from_constant(ConstantValue::Int(5)));
info.state_in.insert(0, state);
assert!(info.is_definitely_not_null(0, "x"));
}
#[test]
fn test_is_definitely_not_null_for_maybe_nullable() {
let mut info = AbstractInterpInfo::new("test");
let state = AbstractState::new().set("x", AbstractValue::top());
info.state_in.insert(0, state);
assert!(!info.is_definitely_not_null(0, "x"));
}
#[test]
fn test_get_constants_returns_known_constant_values() {
let mut info = AbstractInterpInfo::new("test");
let state = AbstractState::new()
.set("x", AbstractValue::from_constant(ConstantValue::Int(5)))
.set(
"y",
AbstractValue::from_constant(ConstantValue::String("hello".to_string())),
)
.set("z", AbstractValue::top()); info.state_out.insert(0, state);
let constants = info.get_constants();
assert_eq!(constants.len(), 2);
assert!(constants.contains_key("x"));
assert!(constants.contains_key("y"));
assert!(!constants.contains_key("z"));
}
#[test]
fn test_abstract_interp_to_json_serializable() {
let mut info = AbstractInterpInfo::new("example");
let state =
AbstractState::new().set("x", AbstractValue::from_constant(ConstantValue::Int(42)));
info.state_in.insert(0, state.clone());
info.state_out.insert(0, state);
info.potential_div_zero.push((10, "y".to_string()));
info.potential_null_deref.push((15, "obj".to_string()));
let json = info.to_json();
assert!(json.is_object());
assert_eq!(json["function"], "example");
assert!(json["state_in"].is_object());
assert!(json["state_out"].is_object());
assert!(json["potential_div_zero"].is_array());
assert!(json["potential_null_deref"].is_array());
let serialized = serde_json::to_string(&json);
assert!(serialized.is_ok());
}
#[test]
fn test_python_none_keyword_recognized() {
let keywords = get_null_keywords("python");
assert!(keywords.contains(&"None"));
}
#[test]
fn test_typescript_null_keyword_recognized() {
let keywords = get_null_keywords("typescript");
assert!(keywords.contains(&"null"));
}
#[test]
fn test_typescript_undefined_keyword_recognized() {
let keywords = get_null_keywords("typescript");
assert!(keywords.contains(&"undefined"));
}
#[test]
fn test_go_nil_keyword_recognized() {
let keywords = get_null_keywords("go");
assert!(keywords.contains(&"nil"));
}
#[test]
fn test_rust_has_no_null_keyword() {
let keywords = get_null_keywords("rust");
assert!(keywords.is_empty(), "Rust should have no null keywords");
}
#[test]
fn test_python_boolean_capitalized() {
let bools = get_boolean_keywords("python");
assert_eq!(bools.get("True"), Some(&true));
assert_eq!(bools.get("False"), Some(&false));
}
#[test]
fn test_typescript_boolean_lowercase() {
let bools = get_boolean_keywords("typescript");
assert_eq!(bools.get("true"), Some(&true));
assert_eq!(bools.get("false"), Some(&false));
}
#[test]
fn test_python_comment_pattern() {
let pattern = get_comment_pattern("python");
assert_eq!(pattern, "#");
}
#[test]
fn test_typescript_comment_pattern() {
let pattern = get_comment_pattern("typescript");
assert_eq!(pattern, "//");
}
#[test]
fn test_arithmetic_add() {
let operand = AbstractValue::from_constant(ConstantValue::Int(5));
let result = apply_arithmetic(&operand, '+', 3);
assert_eq!(result.range_, Some((Some(8), Some(8))));
assert_eq!(result.constant, Some(ConstantValue::Int(8)));
}
#[test]
fn test_arithmetic_subtract() {
let operand = AbstractValue::from_constant(ConstantValue::Int(10));
let result = apply_arithmetic(&operand, '-', 3);
assert_eq!(result.range_, Some((Some(7), Some(7))));
assert_eq!(result.constant, Some(ConstantValue::Int(7)));
}
#[test]
fn test_arithmetic_multiply() {
let operand = AbstractValue::from_constant(ConstantValue::Int(4));
let result = apply_arithmetic(&operand, '*', 2);
assert_eq!(result.range_, Some((Some(8), Some(8))));
assert_eq!(result.constant, Some(ConstantValue::Int(8)));
}
#[test]
fn test_arithmetic_on_range() {
let operand = AbstractValue {
type_: Some("int".to_string()),
range_: Some((Some(1), Some(5))),
nullable: Nullability::Never,
constant: None,
};
let result = apply_arithmetic(&operand, '+', 10);
assert_eq!(result.range_, Some((Some(11), Some(15))));
assert!(result.constant.is_none());
}
#[test]
fn test_arithmetic_overflow_saturates_add() {
let operand = AbstractValue::from_constant(ConstantValue::Int(i64::MAX));
let result = apply_arithmetic(&operand, '+', 1);
match result.range_ {
Some((low, high)) => {
assert!(
low.is_none() || high.is_none(),
"Overflow should widen to unbounded: got ({:?}, {:?})",
low,
high
);
}
None => {
}
}
}
#[test]
fn test_arithmetic_overflow_saturates_sub() {
let operand = AbstractValue::from_constant(ConstantValue::Int(i64::MIN));
let result = apply_arithmetic(&operand, '-', 1);
if let Some((low, high)) = result.range_ {
assert!(
low.is_none() || high.is_none(),
"Underflow should widen to unbounded: got ({:?}, {:?})",
low,
high
);
}
}
#[test]
fn test_arithmetic_multiply_by_negative() {
let operand = AbstractValue {
type_: Some("int".to_string()),
range_: Some((Some(2), Some(4))),
nullable: Nullability::Never,
constant: None,
};
let result = apply_arithmetic(&operand, '*', -3);
assert_eq!(result.range_, Some((Some(-12), Some(-6))));
}
#[test]
fn test_arithmetic_multiply_by_zero() {
let operand = AbstractValue {
type_: Some("int".to_string()),
range_: Some((Some(1), Some(100))),
nullable: Nullability::Never,
constant: None,
};
let result = apply_arithmetic(&operand, '*', 0);
assert_eq!(result.range_, Some((Some(0), Some(0))));
}
#[test]
fn test_arithmetic_preserves_type() {
let operand = AbstractValue::from_constant(ConstantValue::Int(5));
let result = apply_arithmetic(&operand, '+', 3);
assert_eq!(result.type_, Some("int".to_string()));
}
#[test]
fn test_arithmetic_preserves_nullable() {
let operand = AbstractValue::from_constant(ConstantValue::Int(5));
assert_eq!(operand.nullable, Nullability::Never);
let result = apply_arithmetic(&operand, '+', 3);
assert_eq!(result.nullable, Nullability::Never);
}
#[test]
fn test_arithmetic_unknown_op() {
let operand = AbstractValue::from_constant(ConstantValue::Int(5));
let result = apply_arithmetic(&operand, '^', 3);
assert_eq!(result.range_, Some((None, None)));
assert!(result.constant.is_none());
}
#[test]
fn test_arithmetic_on_no_range() {
let operand = AbstractValue::top();
let result = apply_arithmetic(&operand, '+', 5);
assert!(result.range_.is_none());
}
#[test]
fn test_join_values_ranges_union() {
let val1 = AbstractValue::from_constant(ConstantValue::Int(1));
let val2 = AbstractValue::from_constant(ConstantValue::Int(10));
let joined = join_values(&val1, &val2);
assert_eq!(joined.range_, Some((Some(1), Some(10))));
}
#[test]
fn test_join_values_loses_constant_on_disagreement() {
let val1 = AbstractValue::from_constant(ConstantValue::Int(1));
let val2 = AbstractValue::from_constant(ConstantValue::Int(10));
let joined = join_values(&val1, &val2);
assert!(
joined.constant.is_none(),
"Constant should be lost on disagreement"
);
}
#[test]
fn test_join_values_preserves_constant_on_agreement() {
let val1 = AbstractValue::from_constant(ConstantValue::Int(5));
let val2 = AbstractValue::from_constant(ConstantValue::Int(5));
let joined = join_values(&val1, &val2);
assert_eq!(joined.constant, Some(ConstantValue::Int(5)));
}
#[test]
fn test_join_values_nullable_maybe_if_any_maybe() {
let val1 = AbstractValue {
type_: None,
range_: None,
nullable: Nullability::Never,
constant: None,
};
let val2 = AbstractValue {
type_: None,
range_: None,
nullable: Nullability::Maybe,
constant: None,
};
let joined = join_values(&val1, &val2);
assert_eq!(joined.nullable, Nullability::Maybe);
}
#[test]
fn test_join_values_nullable_never_if_both_never() {
let val1 = AbstractValue::from_constant(ConstantValue::Int(1));
let val2 = AbstractValue::from_constant(ConstantValue::Int(2));
let joined = join_values(&val1, &val2);
assert_eq!(joined.nullable, Nullability::Never);
}
#[test]
fn test_join_values_type_preserved_when_same() {
let val1 = AbstractValue::from_constant(ConstantValue::Int(1));
let val2 = AbstractValue::from_constant(ConstantValue::Int(2));
let joined = join_values(&val1, &val2);
assert_eq!(joined.type_, Some("int".to_string()));
}
#[test]
fn test_join_values_type_lost_when_different() {
let val1 = AbstractValue::from_constant(ConstantValue::Int(1));
let val2 = AbstractValue::from_constant(ConstantValue::String("hello".to_string()));
let joined = join_values(&val1, &val2);
assert_eq!(joined.type_, None);
}
#[test]
fn test_join_states_empty() {
let states: Vec<&AbstractState> = vec![];
let joined = join_states(&states);
assert!(joined.values.is_empty());
}
#[test]
fn test_join_states_single() {
let state =
AbstractState::new().set("x", AbstractValue::from_constant(ConstantValue::Int(5)));
let states: Vec<&AbstractState> = vec![&state];
let joined = join_states(&states);
assert_eq!(joined.get("x").range_, Some((Some(5), Some(5))));
}
#[test]
fn test_join_states_multiple() {
let state1 =
AbstractState::new().set("x", AbstractValue::from_constant(ConstantValue::Int(1)));
let state2 =
AbstractState::new().set("x", AbstractValue::from_constant(ConstantValue::Int(10)));
let states: Vec<&AbstractState> = vec![&state1, &state2];
let joined = join_states(&states);
assert_eq!(joined.get("x").range_, Some((Some(1), Some(10))));
}
#[test]
fn test_widen_value_upper_bound_to_infinity() {
let old = AbstractValue {
type_: Some("int".to_string()),
range_: Some((Some(0), Some(5))),
nullable: Nullability::Never,
constant: None,
};
let new = AbstractValue {
type_: Some("int".to_string()),
range_: Some((Some(0), Some(10))), nullable: Nullability::Never,
constant: None,
};
let widened = widen_value(&old, &new);
assert_eq!(widened.range_, Some((Some(0), None)));
}
#[test]
fn test_widen_value_lower_bound_to_infinity() {
let old = AbstractValue {
type_: Some("int".to_string()),
range_: Some((Some(-5), Some(10))),
nullable: Nullability::Never,
constant: None,
};
let new = AbstractValue {
type_: Some("int".to_string()),
range_: Some((Some(-10), Some(10))), nullable: Nullability::Never,
constant: None,
};
let widened = widen_value(&old, &new);
assert_eq!(widened.range_, Some((None, Some(10))));
}
#[test]
fn test_widen_value_loses_constant() {
let old = AbstractValue::from_constant(ConstantValue::Int(5));
let new = AbstractValue::from_constant(ConstantValue::Int(6));
let widened = widen_value(&old, &new);
assert!(widened.constant.is_none(), "Widening should lose constant");
}
#[test]
fn test_widen_value_stable_bounds_not_widened() {
let old = AbstractValue {
type_: Some("int".to_string()),
range_: Some((Some(0), Some(10))),
nullable: Nullability::Never,
constant: None,
};
let new = AbstractValue {
type_: Some("int".to_string()),
range_: Some((Some(0), Some(10))), nullable: Nullability::Never,
constant: None,
};
let widened = widen_value(&old, &new);
assert_eq!(widened.range_, Some((Some(0), Some(10))));
}
#[test]
fn test_widen_state_applies_to_all_vars() {
let old = AbstractState::new()
.set("x", AbstractValue::from_constant(ConstantValue::Int(5)))
.set("y", AbstractValue::from_constant(ConstantValue::Int(0)));
let new = AbstractState::new()
.set("x", AbstractValue::from_constant(ConstantValue::Int(10)))
.set("y", AbstractValue::from_constant(ConstantValue::Int(0)));
let widened = widen_state(&old, &new);
assert_eq!(widened.get("x").range_, Some((Some(10), None)));
assert_eq!(widened.get("y").range_, Some((Some(0), Some(0))));
}
#[test]
fn test_extract_rhs_simple_assignment() {
let rhs = extract_rhs("x = a + b", "x");
assert_eq!(rhs, Some("a + b".to_string()));
let rhs = extract_rhs("foo = 42", "foo");
assert_eq!(rhs, Some("42".to_string()));
let rhs = extract_rhs("result = None", "result");
assert_eq!(rhs, Some("None".to_string()));
}
#[test]
fn test_extract_rhs_augmented_assignment() {
let rhs = extract_rhs("x += 5", "x");
assert_eq!(rhs, Some("x + 5".to_string()));
let rhs = extract_rhs("y -= 3", "y");
assert_eq!(rhs, Some("y - 3".to_string()));
let rhs = extract_rhs("count *= 2", "count");
assert_eq!(rhs, Some("count * 2".to_string()));
}
#[test]
fn test_extract_rhs_with_spaces() {
let rhs = extract_rhs("x=5", "x");
assert_eq!(rhs, Some("5".to_string()));
let rhs = extract_rhs("x =5", "x");
assert_eq!(rhs, Some("5".to_string()));
let rhs = extract_rhs("x= 5", "x");
assert_eq!(rhs, Some("5".to_string()));
}
#[test]
fn test_extract_rhs_not_found() {
let rhs = extract_rhs("y = 5", "x");
assert_eq!(rhs, None);
let rhs = extract_rhs("xy = 5", "x");
assert_eq!(rhs, None);
}
#[test]
fn test_strip_comment_python() {
let stripped = strip_comment("x = 5 # this is a comment", "python");
assert_eq!(stripped, "x = 5 ");
let stripped = strip_comment("x = 5", "python");
assert_eq!(stripped, "x = 5");
}
#[test]
fn test_strip_comment_typescript() {
let stripped = strip_comment("x = 5 // this is a comment", "typescript");
assert_eq!(stripped, "x = 5 ");
let stripped = strip_comment("x = 5", "typescript");
assert_eq!(stripped, "x = 5");
}
#[test]
fn test_strip_comment_preserves_string() {
let stripped = strip_comment("x = \"hello # world\"", "python");
assert_eq!(stripped, "x = \"hello # world\"");
let stripped = strip_comment("x = 'hello // world'", "typescript");
assert_eq!(stripped, "x = 'hello // world'");
}
#[test]
fn test_strip_strings_blanks_path_separators() {
let result = strip_strings("Path::new(\"src/main.rs\")", "rust");
assert_eq!(result, "Path::new(\" \")");
assert!(!result.contains('/'), "slashes inside strings must be blanked");
}
#[test]
fn test_strip_strings_preserves_code() {
let result = strip_strings("let ratio = a / b;", "rust");
assert_eq!(result, "let ratio = a / b;");
}
#[test]
fn test_strip_strings_handles_escapes() {
let result = strip_strings(r#"let s = "path/to/\"file\""; a / b"#, "rust");
assert!(result.contains("a / b"), "code division must survive");
assert!(!result[8..25].contains('/'), "slashes in string must be blanked");
}
#[test]
fn test_strip_strings_single_quotes() {
let result = strip_strings("let c = '/'; x / y", "rust");
assert!(result.contains("x / y"), "code division must survive");
assert_eq!(result.matches('/').count(), 1, "only code division remains");
}
#[test]
fn test_strip_strings_rust_raw_string() {
let result = strip_strings(r##"let xml = r#"</coverage>"#;"##, "rust");
assert!(!result.contains('/'), "slashes inside raw strings must be blanked");
assert!(!result.contains("coverage"), "identifiers inside raw strings must be blanked");
}
#[test]
fn test_strip_strings_rust_raw_no_hashes() {
let result = strip_strings(r#"let p = r"/src/main.rs"; a / b"#, "rust");
assert!(result.contains("a / b"), "code division must survive");
assert_eq!(result.matches('/').count(), 1, "only code division remains");
}
#[test]
fn test_strip_strings_rust_raw_double_hash() {
let result = strip_strings(r###"let s = r##"a/b"##;"###, "rust");
assert!(!result.contains("a/b"), "contents of r##\"...\"## must be blanked");
}
#[test]
fn test_parse_simple_arithmetic_var_plus_const() {
let result = parse_simple_arithmetic("a + 1");
assert_eq!(result, Some(("a".to_string(), '+', 1)));
let result = parse_simple_arithmetic("count - 5");
assert_eq!(result, Some(("count".to_string(), '-', 5)));
let result = parse_simple_arithmetic("x * 2");
assert_eq!(result, Some(("x".to_string(), '*', 2)));
}
#[test]
fn test_parse_simple_arithmetic_const_plus_var() {
let result = parse_simple_arithmetic("1 + a");
assert_eq!(result, Some(("a".to_string(), '+', 1)));
let result = parse_simple_arithmetic("2 * x");
assert_eq!(result, Some(("x".to_string(), '*', 2)));
}
#[test]
fn test_parse_simple_arithmetic_negative_const() {
let result = parse_simple_arithmetic("a + -5");
assert_eq!(result, Some(("a".to_string(), '+', -5)));
}
#[test]
fn test_parse_simple_arithmetic_no_match() {
let result = parse_simple_arithmetic("a + b"); assert_eq!(result, None);
let result = parse_simple_arithmetic("5"); assert_eq!(result, None);
let result = parse_simple_arithmetic("foo"); assert_eq!(result, None);
}
#[test]
fn test_is_identifier() {
assert!(is_identifier("x"));
assert!(is_identifier("foo"));
assert!(is_identifier("_bar"));
assert!(is_identifier("var123"));
assert!(is_identifier("__init__"));
assert!(!is_identifier(""));
assert!(!is_identifier("123var"));
assert!(!is_identifier("foo.bar"));
assert!(!is_identifier("foo bar"));
assert!(!is_identifier("foo-bar"));
}
#[test]
fn test_parse_rhs_abstract_integer() {
let state = AbstractState::new();
let val = parse_rhs_abstract("x = 5", "x", &state, "python");
assert_eq!(val.range_, Some((Some(5), Some(5))));
assert_eq!(val.constant, Some(ConstantValue::Int(5)));
assert_eq!(val.type_, Some("int".to_string()));
}
#[test]
fn test_parse_rhs_abstract_negative_integer() {
let state = AbstractState::new();
let val = parse_rhs_abstract("x = -42", "x", &state, "python");
assert_eq!(val.range_, Some((Some(-42), Some(-42))));
assert_eq!(val.constant, Some(ConstantValue::Int(-42)));
}
#[test]
fn test_parse_rhs_abstract_float() {
let state = AbstractState::new();
let val = parse_rhs_abstract("x = 3.14", "x", &state, "python");
assert_eq!(val.type_, Some("float".to_string()));
if let Some(ConstantValue::Float(f)) = val.constant {
assert!((f - PI).abs() < f64::EPSILON);
} else {
panic!("Expected float constant");
}
}
#[test]
fn test_parse_rhs_abstract_string_double_quotes() {
let state = AbstractState::new();
let val = parse_rhs_abstract("x = \"hello\"", "x", &state, "python");
assert_eq!(val.type_, Some("str".to_string()));
assert_eq!(
val.constant,
Some(ConstantValue::String("hello".to_string()))
);
assert_eq!(val.range_, Some((Some(5), Some(5))));
}
#[test]
fn test_parse_rhs_abstract_string_single_quotes() {
let state = AbstractState::new();
let val = parse_rhs_abstract("x = 'world'", "x", &state, "python");
assert_eq!(val.type_, Some("str".to_string()));
assert_eq!(
val.constant,
Some(ConstantValue::String("world".to_string()))
);
}
#[test]
fn test_parse_rhs_abstract_python_none() {
let state = AbstractState::new();
let val = parse_rhs_abstract("x = None", "x", &state, "python");
assert_eq!(val.nullable, Nullability::Always);
assert_eq!(val.type_, Some("NoneType".to_string()));
}
#[test]
fn test_parse_rhs_abstract_typescript_null() {
let state = AbstractState::new();
let val = parse_rhs_abstract("x = null", "x", &state, "typescript");
assert_eq!(val.nullable, Nullability::Always);
}
#[test]
fn test_parse_rhs_abstract_typescript_undefined() {
let state = AbstractState::new();
let val = parse_rhs_abstract("x = undefined", "x", &state, "typescript");
assert_eq!(val.nullable, Nullability::Always);
assert_eq!(val.type_, Some("undefined".to_string()));
}
#[test]
fn test_parse_rhs_abstract_go_nil() {
let state = AbstractState::new();
let val = parse_rhs_abstract("x = nil", "x", &state, "go");
assert_eq!(val.nullable, Nullability::Always);
}
#[test]
fn test_parse_rhs_abstract_python_bool() {
let state = AbstractState::new();
let val = parse_rhs_abstract("x = True", "x", &state, "python");
assert_eq!(val.type_, Some("bool".to_string()));
assert_eq!(val.constant, Some(ConstantValue::Bool(true)));
let val = parse_rhs_abstract("y = False", "y", &state, "python");
assert_eq!(val.constant, Some(ConstantValue::Bool(false)));
}
#[test]
fn test_parse_rhs_abstract_typescript_bool() {
let state = AbstractState::new();
let val = parse_rhs_abstract("x = true", "x", &state, "typescript");
assert_eq!(val.type_, Some("bool".to_string()));
assert_eq!(val.constant, Some(ConstantValue::Bool(true)));
}
#[test]
fn test_parse_rhs_abstract_variable_copy() {
let state =
AbstractState::new().set("a", AbstractValue::from_constant(ConstantValue::Int(42)));
let val = parse_rhs_abstract("x = a", "x", &state, "python");
assert_eq!(val.range_, Some((Some(42), Some(42))));
assert_eq!(val.constant, Some(ConstantValue::Int(42)));
}
#[test]
fn test_parse_rhs_abstract_simple_arithmetic() {
let state =
AbstractState::new().set("a", AbstractValue::from_constant(ConstantValue::Int(5)));
let val = parse_rhs_abstract("x = a + 3", "x", &state, "python");
assert_eq!(val.range_, Some((Some(8), Some(8))));
assert_eq!(val.constant, Some(ConstantValue::Int(8)));
}
#[test]
fn test_parse_rhs_abstract_augmented_assignment() {
let state =
AbstractState::new().set("x", AbstractValue::from_constant(ConstantValue::Int(10)));
let val = parse_rhs_abstract("x += 5", "x", &state, "python");
assert_eq!(val.range_, Some((Some(15), Some(15))));
assert_eq!(val.constant, Some(ConstantValue::Int(15)));
}
#[test]
fn test_parse_rhs_abstract_with_comment() {
let state = AbstractState::new();
let val = parse_rhs_abstract("x = 5 # this is the value", "x", &state, "python");
assert_eq!(val.range_, Some((Some(5), Some(5))));
}
#[test]
fn test_parse_rhs_abstract_unknown_returns_top() {
let state = AbstractState::new();
let val = parse_rhs_abstract("x = foo(a, b)", "x", &state, "python");
assert_eq!(val.type_, None);
assert_eq!(val.range_, None);
assert_eq!(val.nullable, Nullability::Maybe);
}
#[test]
fn test_parse_rhs_abstract_no_assignment() {
let state = AbstractState::new();
let val = parse_rhs_abstract("y = 5", "x", &state, "python");
assert_eq!(val.type_, None);
assert_eq!(val.range_, None);
}
use crate::types::{
BlockType, CfgBlock, CfgEdge, CfgInfo, DfgInfo, EdgeType, VarRef,
};
fn make_test_cfg(function: &str, blocks: Vec<CfgBlock>, edges: Vec<CfgEdge>) -> CfgInfo {
CfgInfo {
function: function.to_string(),
blocks,
edges,
entry_block: 0,
exit_blocks: vec![0], cyclomatic_complexity: 1,
nested_functions: HashMap::new(),
}
}
fn make_var_ref(name: &str, ref_type: RefType, line: u32, column: u32) -> VarRef {
VarRef {
name: name.to_string(),
ref_type,
line,
column,
context: None,
group_id: None,
}
}
#[test]
fn test_compute_abstract_interp_returns_info() {
let cfg = make_test_cfg(
"test_func",
vec![CfgBlock {
id: 0,
block_type: BlockType::Entry,
lines: (1, 1),
calls: vec![],
}],
vec![],
);
let dfg = DfgInfo {
function: "test_func".to_string(),
refs: vec![],
edges: vec![],
variables: vec![],
};
let result = compute_abstract_interp(&cfg, &dfg, None, "python").unwrap();
assert_eq!(result.function_name, "test_func");
}
#[test]
fn test_compute_tracks_constant_assignment() {
let cfg = make_test_cfg(
"const_test",
vec![CfgBlock {
id: 0,
block_type: BlockType::Entry,
lines: (1, 1),
calls: vec![],
}],
vec![],
);
let dfg = DfgInfo {
function: "const_test".to_string(),
refs: vec![make_var_ref("x", RefType::Definition, 1, 0)],
edges: vec![],
variables: vec!["x".to_string()],
};
let source = ["x = 5"];
let source_refs: Vec<&str> = source.to_vec();
let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "python").unwrap();
let val = result.value_at_exit(0, "x");
assert_eq!(val.range_, Some((Some(5), Some(5))));
}
#[test]
fn test_compute_tracks_variable_copy() {
let cfg = make_test_cfg(
"copy_test",
vec![CfgBlock {
id: 0,
block_type: BlockType::Entry,
lines: (1, 2),
calls: vec![],
}],
vec![],
);
let dfg = DfgInfo {
function: "copy_test".to_string(),
refs: vec![
make_var_ref("x", RefType::Definition, 1, 0),
make_var_ref("x", RefType::Use, 2, 4),
make_var_ref("y", RefType::Definition, 2, 0),
],
edges: vec![],
variables: vec!["x".to_string(), "y".to_string()],
};
let source = ["x = 5", "y = x"];
let source_refs: Vec<&str> = source.to_vec();
let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "python").unwrap();
let val_x = result.value_at_exit(0, "x");
let val_y = result.value_at_exit(0, "y");
assert_eq!(val_x.range_, val_y.range_);
}
#[test]
fn test_compute_tracks_none_assignment() {
let cfg = make_test_cfg(
"none_test",
vec![CfgBlock {
id: 0,
block_type: BlockType::Entry,
lines: (1, 1),
calls: vec![],
}],
vec![],
);
let dfg = DfgInfo {
function: "none_test".to_string(),
refs: vec![make_var_ref("x", RefType::Definition, 1, 0)],
edges: vec![],
variables: vec!["x".to_string()],
};
let source = ["x = None"];
let source_refs: Vec<&str> = source.to_vec();
let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "python").unwrap();
let val = result.value_at_exit(0, "x");
assert_eq!(val.nullable, Nullability::Always);
}
#[test]
fn test_abstract_interp_empty_function_no_crash() {
let cfg = make_test_cfg(
"empty_func",
vec![CfgBlock {
id: 0,
block_type: BlockType::Entry,
lines: (1, 1),
calls: vec![],
}],
vec![],
);
let dfg = DfgInfo {
function: "empty_func".to_string(),
refs: vec![],
edges: vec![],
variables: vec![],
};
let result = compute_abstract_interp(&cfg, &dfg, None, "python");
assert!(result.is_ok());
}
#[test]
fn test_unknown_rhs_defaults_to_top() {
let cfg = make_test_cfg(
"unknown_test",
vec![CfgBlock {
id: 0,
block_type: BlockType::Entry,
lines: (1, 1),
calls: vec![],
}],
vec![],
);
let dfg = DfgInfo {
function: "unknown_test".to_string(),
refs: vec![make_var_ref("x", RefType::Definition, 1, 0)],
edges: vec![],
variables: vec!["x".to_string()],
};
let source = ["x = some_function()"];
let source_refs: Vec<&str> = source.to_vec();
let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "python").unwrap();
let val = result.value_at_exit(0, "x");
assert_eq!(val.type_, None);
assert_eq!(val.range_, None);
assert_eq!(val.nullable, Nullability::Maybe);
}
#[test]
fn test_parameter_starts_as_top() {
let cfg = make_test_cfg(
"param_test",
vec![CfgBlock {
id: 0,
block_type: BlockType::Entry,
lines: (1, 1),
calls: vec![],
}],
vec![],
);
let dfg = DfgInfo {
function: "param_test".to_string(),
refs: vec![make_var_ref("param", RefType::Definition, 1, 0)],
edges: vec![],
variables: vec!["param".to_string()],
};
let result = compute_abstract_interp(&cfg, &dfg, None, "python").unwrap();
let val = result.value_at(0, "param");
assert_eq!(val.type_, None);
assert_eq!(val.range_, None);
assert_eq!(val.nullable, Nullability::Maybe);
}
#[test]
fn test_nested_loops_terminate() {
let cfg = CfgInfo {
function: "nested_loop".to_string(),
blocks: vec![
CfgBlock {
id: 0,
block_type: BlockType::Entry,
lines: (1, 1),
calls: vec![],
},
CfgBlock {
id: 1,
block_type: BlockType::LoopHeader,
lines: (2, 2),
calls: vec![],
},
CfgBlock {
id: 2,
block_type: BlockType::LoopHeader,
lines: (3, 3),
calls: vec![],
},
CfgBlock {
id: 3,
block_type: BlockType::LoopBody,
lines: (4, 4),
calls: vec![],
},
CfgBlock {
id: 4,
block_type: BlockType::Exit,
lines: (5, 5),
calls: vec![],
},
],
edges: vec![
CfgEdge {
from: 0,
to: 1,
edge_type: EdgeType::Unconditional,
condition: None,
},
CfgEdge {
from: 1,
to: 2,
edge_type: EdgeType::True,
condition: Some("i < n".to_string()),
},
CfgEdge {
from: 1,
to: 4,
edge_type: EdgeType::False,
condition: None,
},
CfgEdge {
from: 2,
to: 3,
edge_type: EdgeType::True,
condition: Some("j < m".to_string()),
},
CfgEdge {
from: 2,
to: 1,
edge_type: EdgeType::False,
condition: None,
},
CfgEdge {
from: 3,
to: 2,
edge_type: EdgeType::BackEdge,
condition: None,
},
],
entry_block: 0,
exit_blocks: vec![4],
cyclomatic_complexity: 3,
nested_functions: HashMap::new(),
};
let dfg = DfgInfo {
function: "nested_loop".to_string(),
refs: vec![
make_var_ref("x", RefType::Definition, 4, 0),
make_var_ref("x", RefType::Use, 4, 4),
],
edges: vec![],
variables: vec!["x".to_string()],
};
let source = ["x = 0",
"for i in range(n):",
" for j in range(m):",
" x = x + 1",
"return x"];
let source_refs: Vec<&str> = source.to_vec();
let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "python");
assert!(result.is_ok());
}
#[test]
fn test_compute_accepts_language_parameter() {
let cfg = make_test_cfg(
"lang_test",
vec![CfgBlock {
id: 0,
block_type: BlockType::Entry,
lines: (1, 1),
calls: vec![],
}],
vec![],
);
let dfg = DfgInfo {
function: "lang_test".to_string(),
refs: vec![],
edges: vec![],
variables: vec![],
};
let result_py = compute_abstract_interp(&cfg, &dfg, None, "python");
let result_ts = compute_abstract_interp(&cfg, &dfg, None, "typescript");
assert!(result_py.is_ok());
assert!(result_ts.is_ok());
}
#[test]
fn test_compute_with_typescript_null() {
let cfg = make_test_cfg(
"ts_null_test",
vec![CfgBlock {
id: 0,
block_type: BlockType::Entry,
lines: (1, 1),
calls: vec![],
}],
vec![],
);
let dfg = DfgInfo {
function: "ts_null_test".to_string(),
refs: vec![make_var_ref("x", RefType::Definition, 1, 0)],
edges: vec![],
variables: vec!["x".to_string()],
};
let source = ["let x = null"];
let source_refs: Vec<&str> = source.to_vec();
let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "typescript").unwrap();
let val = result.value_at_exit(0, "x");
assert_eq!(val.nullable, Nullability::Always);
}
#[test]
fn test_compute_with_go_nil() {
let cfg = make_test_cfg(
"go_nil_test",
vec![CfgBlock {
id: 0,
block_type: BlockType::Entry,
lines: (1, 1),
calls: vec![],
}],
vec![],
);
let dfg = DfgInfo {
function: "go_nil_test".to_string(),
refs: vec![make_var_ref("x", RefType::Definition, 1, 0)],
edges: vec![],
variables: vec!["x".to_string()],
};
let source = ["x := nil"];
let source_refs: Vec<&str> = source.to_vec();
let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "go").unwrap();
let val = result.value_at_exit(0, "x");
assert_eq!(val.nullable, Nullability::Always);
}
#[test]
fn test_div_zero_detected_for_constant_zero() {
let cfg = make_test_cfg(
"div_zero_const",
vec![CfgBlock {
id: 0,
block_type: BlockType::Entry,
lines: (1, 2),
calls: vec![],
}],
vec![],
);
let dfg = DfgInfo {
function: "div_zero_const".to_string(),
refs: vec![
make_var_ref("x", RefType::Definition, 1, 0),
make_var_ref("x", RefType::Use, 2, 6),
make_var_ref("y", RefType::Definition, 2, 0),
],
edges: vec![],
variables: vec!["x".to_string(), "y".to_string()],
};
let source = ["x = 0", "y = 1 / x"];
let source_refs: Vec<&str> = source.to_vec();
let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "python").unwrap();
assert!(
!result.potential_div_zero.is_empty(),
"Should detect division by zero"
);
assert!(
result
.potential_div_zero
.iter()
.any(|(line, var)| *line == 2 && var == "x"),
"Should flag x at line 2 as potential div-by-zero"
);
}
#[test]
fn test_div_zero_detected_for_range_including_zero() {
let cfg = make_test_cfg(
"div_zero_range",
vec![CfgBlock {
id: 0,
block_type: BlockType::Entry,
lines: (1, 2),
calls: vec![],
}],
vec![],
);
let dfg = DfgInfo {
function: "div_zero_range".to_string(),
refs: vec![
make_var_ref("x", RefType::Definition, 1, 0), make_var_ref("x", RefType::Use, 2, 6),
make_var_ref("y", RefType::Definition, 2, 0),
],
edges: vec![],
variables: vec!["x".to_string(), "y".to_string()],
};
let source = ["x = foo()", "y = 1 / x"];
let source_refs: Vec<&str> = source.to_vec();
let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "python").unwrap();
assert!(
!result.potential_div_zero.is_empty(),
"Should detect potential division by zero for unknown value"
);
}
#[test]
fn test_div_safe_no_warning_for_constant_nonzero() {
let cfg = make_test_cfg(
"div_safe_const",
vec![CfgBlock {
id: 0,
block_type: BlockType::Entry,
lines: (1, 2),
calls: vec![],
}],
vec![],
);
let dfg = DfgInfo {
function: "div_safe_const".to_string(),
refs: vec![
make_var_ref("x", RefType::Definition, 1, 0),
make_var_ref("x", RefType::Use, 2, 6),
make_var_ref("y", RefType::Definition, 2, 0),
],
edges: vec![],
variables: vec!["x".to_string(), "y".to_string()],
};
let source = ["x = 5", "y = 1 / x"];
let source_refs: Vec<&str> = source.to_vec();
let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "python").unwrap();
assert!(
result.potential_div_zero.is_empty()
|| !result
.potential_div_zero
.iter()
.any(|(line, var)| *line == 2 && var == "x"),
"Should NOT warn for division by constant non-zero"
);
}
#[test]
fn test_div_safe_no_warning_for_positive_range() {
let cfg = make_test_cfg(
"div_safe_range",
vec![CfgBlock {
id: 0,
block_type: BlockType::Entry,
lines: (1, 3),
calls: vec![],
}],
vec![],
);
let dfg = DfgInfo {
function: "div_safe_range".to_string(),
refs: vec![
make_var_ref("x", RefType::Definition, 1, 0),
make_var_ref("x", RefType::Use, 2, 4),
make_var_ref("x", RefType::Definition, 2, 0),
make_var_ref("x", RefType::Use, 3, 6),
make_var_ref("y", RefType::Definition, 3, 0),
],
edges: vec![],
variables: vec!["x".to_string(), "y".to_string()],
};
let source = ["x = 5", "x = x + 1", "y = 1 / x"];
let source_refs: Vec<&str> = source.to_vec();
let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "python").unwrap();
assert!(
result.potential_div_zero.is_empty()
|| !result
.potential_div_zero
.iter()
.any(|(line, var)| *line == 3 && var == "x"),
"Should NOT warn for positive range that excludes zero"
);
}
#[test]
fn test_div_zero_intra_block_accuracy() {
let cfg = make_test_cfg(
"div_intra_block",
vec![CfgBlock {
id: 0,
block_type: BlockType::Entry,
lines: (1, 3),
calls: vec![],
}],
vec![],
);
let dfg = DfgInfo {
function: "div_intra_block".to_string(),
refs: vec![
make_var_ref("x", RefType::Definition, 1, 0),
make_var_ref("x", RefType::Definition, 2, 0), make_var_ref("x", RefType::Use, 3, 6),
make_var_ref("y", RefType::Definition, 3, 0),
],
edges: vec![],
variables: vec!["x".to_string(), "y".to_string()],
};
let source = ["x = 0", "x = 5", "y = 1 / x"];
let source_refs: Vec<&str> = source.to_vec();
let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "python").unwrap();
assert!(result.potential_div_zero.is_empty() ||
!result.potential_div_zero.iter().any(|(line, var)| *line == 3 && var == "x"),
"Should NOT warn when divisor is redefined to non-zero before division (intra-block precision)");
}
#[test]
fn test_div_zero_not_triggered_by_path_strings() {
let cfg = make_test_cfg(
"path_strings",
vec![CfgBlock {
id: 0,
block_type: BlockType::Entry,
lines: (1, 2),
calls: vec![],
}],
vec![],
);
let dfg = DfgInfo {
function: "path_strings".to_string(),
refs: vec![
make_var_ref("root", RefType::Definition, 1, 0),
make_var_ref("child", RefType::Definition, 2, 0),
],
edges: vec![],
variables: vec!["root".to_string(), "child".to_string()],
};
let source = ["root = \"/projects/myapp\"",
"child = \"/src/main.rs\""];
let source_refs: Vec<&str> = source.to_vec();
let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "python").unwrap();
assert!(
result.potential_div_zero.is_empty(),
"Path separators inside string literals must not trigger div-by-zero; got: {:?}",
result.potential_div_zero
);
}
#[test]
fn test_div_zero_still_detects_real_division_with_strings() {
let cfg = make_test_cfg(
"mixed_strings_div",
vec![CfgBlock {
id: 0,
block_type: BlockType::Entry,
lines: (1, 3),
calls: vec![],
}],
vec![],
);
let dfg = DfgInfo {
function: "mixed_strings_div".to_string(),
refs: vec![
make_var_ref("path", RefType::Definition, 1, 0),
make_var_ref("x", RefType::Definition, 2, 0),
make_var_ref("y", RefType::Definition, 3, 0),
make_var_ref("x", RefType::Use, 3, 10),
],
edges: vec![],
variables: vec!["path".to_string(), "x".to_string(), "y".to_string()],
};
let source = ["path = \"/src/main.rs\"",
"x = foo()",
"y = 100 / x"];
let source_refs: Vec<&str> = source.to_vec();
let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "python").unwrap();
assert!(
result.potential_div_zero.iter().any(|(line, var)| *line == 3 && var == "x"),
"Real division by unknown x should still be flagged; got: {:?}",
result.potential_div_zero
);
assert!(
!result.potential_div_zero.iter().any(|(_, var)| var == "main" || var == "src"),
"Path components in strings must not be flagged; got: {:?}",
result.potential_div_zero
);
}
#[test]
fn test_null_deref_detected_at_attribute_access() {
let cfg = make_test_cfg(
"null_deref",
vec![CfgBlock {
id: 0,
block_type: BlockType::Entry,
lines: (1, 2),
calls: vec![],
}],
vec![],
);
let dfg = DfgInfo {
function: "null_deref".to_string(),
refs: vec![
make_var_ref("x", RefType::Definition, 1, 0),
make_var_ref("x", RefType::Use, 2, 4),
make_var_ref("y", RefType::Definition, 2, 0),
],
edges: vec![],
variables: vec!["x".to_string(), "y".to_string()],
};
let source = ["x = None", "y = x.foo"];
let source_refs: Vec<&str> = source.to_vec();
let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "python").unwrap();
assert!(
!result.potential_null_deref.is_empty(),
"Should detect null dereference"
);
assert!(
result
.potential_null_deref
.iter()
.any(|(line, var)| *line == 2 && var == "x"),
"Should flag x at line 2 as potential null deref"
);
}
#[test]
fn test_null_deref_safe_for_non_null_constant() {
let cfg = make_test_cfg(
"null_safe",
vec![CfgBlock {
id: 0,
block_type: BlockType::Entry,
lines: (1, 2),
calls: vec![],
}],
vec![],
);
let dfg = DfgInfo {
function: "null_safe".to_string(),
refs: vec![
make_var_ref("x", RefType::Definition, 1, 0),
make_var_ref("x", RefType::Use, 2, 4),
make_var_ref("y", RefType::Definition, 2, 0),
],
edges: vec![],
variables: vec!["x".to_string(), "y".to_string()],
};
let source = ["x = 'hello'", "y = x.upper()"];
let source_refs: Vec<&str> = source.to_vec();
let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "python").unwrap();
assert!(
result.potential_null_deref.is_empty()
|| !result
.potential_null_deref
.iter()
.any(|(line, var)| *line == 2 && var == "x"),
"Should NOT warn for dereference of non-null constant"
);
}
}