1use std::collections::HashMap;
25use std::hash::{Hash, Hasher};
26
27use serde::{Deserialize, Serialize};
28use serde_json;
29
30use super::types::BlockId;
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
54#[serde(rename_all = "lowercase")]
55pub enum Nullability {
56 Never,
58 #[serde(rename = "maybe")]
60 Maybe,
61 Always,
63}
64
65impl Default for Nullability {
66 fn default() -> Self {
68 Nullability::Maybe
69 }
70}
71
72impl Nullability {
73 pub fn as_str(&self) -> &'static str {
75 match self {
76 Nullability::Never => "never",
77 Nullability::Maybe => "maybe",
78 Nullability::Always => "always",
79 }
80 }
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
100#[serde(untagged)]
101pub enum ConstantValue {
102 Int(i64),
104 Float(f64),
106 String(String),
108 Bool(bool),
110 Null,
112}
113
114impl PartialEq for ConstantValue {
116 fn eq(&self, other: &Self) -> bool {
117 match (self, other) {
118 (ConstantValue::Int(a), ConstantValue::Int(b)) => a == b,
119 (ConstantValue::Float(a), ConstantValue::Float(b)) => {
120 (a.is_nan() && b.is_nan()) || a == b
122 }
123 (ConstantValue::String(a), ConstantValue::String(b)) => a == b,
124 (ConstantValue::Bool(a), ConstantValue::Bool(b)) => a == b,
125 (ConstantValue::Null, ConstantValue::Null) => true,
126 _ => false,
127 }
128 }
129}
130
131impl ConstantValue {
132 pub fn to_json_value(&self) -> serde_json::Value {
134 match self {
135 ConstantValue::Int(v) => serde_json::json!(v),
136 ConstantValue::Float(v) => serde_json::json!(v),
137 ConstantValue::String(v) => serde_json::json!(v),
138 ConstantValue::Bool(v) => serde_json::json!(v),
139 ConstantValue::Null => serde_json::Value::Null,
140 }
141 }
142}
143
144#[derive(Debug, Clone)]
176pub struct AbstractValue {
177 pub type_: Option<String>,
179
180 pub range_: Option<(Option<i64>, Option<i64>)>,
183
184 pub nullable: Nullability,
186
187 pub constant: Option<ConstantValue>,
189}
190
191impl PartialEq for AbstractValue {
193 fn eq(&self, other: &Self) -> bool {
194 self.type_ == other.type_
195 && self.range_ == other.range_
196 && self.nullable == other.nullable
197 && self.constant == other.constant
198 }
199}
200
201impl Eq for AbstractValue {}
204
205impl Hash for AbstractValue {
206 fn hash<H: Hasher>(&self, state: &mut H) {
207 self.type_.hash(state);
209 self.range_.hash(state);
210 self.nullable.hash(state);
211 }
213}
214
215impl AbstractValue {
216 pub fn top() -> Self {
226 AbstractValue {
227 type_: None,
228 range_: None,
229 nullable: Nullability::Maybe,
230 constant: None,
231 }
232 }
233
234 pub fn bottom() -> Self {
245 AbstractValue {
246 type_: Some("<bottom>".to_string()),
247 range_: Some((None, None)),
248 nullable: Nullability::Never,
249 constant: None,
250 }
251 }
252
253 pub fn from_constant(value: ConstantValue) -> Self {
273 match value {
274 ConstantValue::Int(v) => AbstractValue {
275 type_: Some("int".to_string()),
276 range_: Some((Some(v), Some(v))),
277 nullable: Nullability::Never,
278 constant: Some(ConstantValue::Int(v)),
279 },
280 ConstantValue::Float(v) => AbstractValue {
281 type_: Some("float".to_string()),
282 range_: None, nullable: Nullability::Never,
284 constant: Some(ConstantValue::Float(v)),
285 },
286 ConstantValue::String(ref s) => {
287 let len = s.len() as i64;
288 AbstractValue {
289 type_: Some("str".to_string()),
290 range_: Some((Some(len), Some(len))),
292 nullable: Nullability::Never,
293 constant: Some(value),
294 }
295 }
296 ConstantValue::Bool(v) => AbstractValue {
297 type_: Some("bool".to_string()),
298 range_: Some((Some(v as i64), Some(v as i64))),
299 nullable: Nullability::Never,
300 constant: Some(ConstantValue::Bool(v)),
301 },
302 ConstantValue::Null => AbstractValue {
303 type_: Some("NoneType".to_string()),
304 range_: None,
305 nullable: Nullability::Always,
306 constant: None, },
308 }
309 }
310
311 pub fn may_be_zero(&self) -> bool {
323 match &self.range_ {
324 None => true, Some((low, high)) => {
326 let low = low.unwrap_or(i64::MIN);
327 let high = high.unwrap_or(i64::MAX);
328 low <= 0 && 0 <= high
329 }
330 }
331 }
332
333 pub fn may_be_null(&self) -> bool {
344 self.nullable != Nullability::Never
345 }
346
347 pub fn is_constant(&self) -> bool {
351 self.constant.is_some()
352 }
353
354 pub fn to_json_value(&self) -> serde_json::Value {
367 let mut obj = serde_json::Map::new();
368
369 if let Some(ref t) = self.type_ {
370 obj.insert("type".to_string(), serde_json::json!(t));
371 }
372
373 if let Some((low, high)) = &self.range_ {
374 let range = serde_json::json!([low, high]);
376 obj.insert("range".to_string(), range);
377 }
378
379 obj.insert(
380 "nullable".to_string(),
381 serde_json::json!(self.nullable.as_str()),
382 );
383
384 if let Some(ref c) = self.constant {
385 obj.insert("constant".to_string(), c.to_json_value());
386 }
387
388 serde_json::Value::Object(obj)
389 }
390}
391
392#[derive(Debug, Clone, Default, PartialEq, Eq)]
411pub struct AbstractState {
412 pub values: HashMap<String, AbstractValue>,
414}
415
416impl AbstractState {
417 pub fn new() -> Self {
419 Self::default()
420 }
421
422 pub fn get(&self, var: &str) -> AbstractValue {
428 self.values
429 .get(var)
430 .cloned()
431 .unwrap_or_else(AbstractValue::top)
432 }
433
434 pub fn set(&self, var: &str, value: AbstractValue) -> Self {
448 let mut new_values = self.values.clone();
449 new_values.insert(var.to_string(), value);
450 AbstractState { values: new_values }
451 }
452
453 pub fn copy(&self) -> Self {
457 self.clone()
458 }
459}
460
461#[derive(Debug, Clone, Default)]
492pub struct AbstractInterpInfo {
493 pub state_in: HashMap<BlockId, AbstractState>,
495
496 pub state_out: HashMap<BlockId, AbstractState>,
498
499 pub potential_div_zero: Vec<(usize, String)>,
501
502 pub potential_null_deref: Vec<(usize, String)>,
504
505 pub function_name: String,
507}
508
509impl AbstractInterpInfo {
510 pub fn new(function_name: &str) -> Self {
512 Self {
513 function_name: function_name.to_string(),
514 ..Default::default()
515 }
516 }
517
518 pub fn value_at(&self, block: BlockId, var: &str) -> AbstractValue {
522 self.state_in
523 .get(&block)
524 .map(|s| s.get(var))
525 .unwrap_or_else(AbstractValue::top)
526 }
527
528 pub fn value_at_exit(&self, block: BlockId, var: &str) -> AbstractValue {
532 self.state_out
533 .get(&block)
534 .map(|s| s.get(var))
535 .unwrap_or_else(AbstractValue::top)
536 }
537
538 pub fn range_at(&self, block: BlockId, var: &str) -> Option<(Option<i64>, Option<i64>)> {
542 self.value_at(block, var).range_
543 }
544
545 pub fn type_at(&self, block: BlockId, var: &str) -> Option<String> {
549 self.value_at(block, var).type_
550 }
551
552 pub fn is_definitely_not_null(&self, block: BlockId, var: &str) -> bool {
556 self.value_at(block, var).nullable == Nullability::Never
557 }
558
559 pub fn get_constants(&self) -> HashMap<String, ConstantValue> {
563 let mut constants = HashMap::new();
564 for state in self.state_out.values() {
565 for (var, val) in &state.values {
566 if let Some(c) = &val.constant {
567 constants.insert(var.clone(), c.clone());
568 }
569 }
570 }
571 constants
572 }
573
574 pub fn to_json(&self) -> serde_json::Value {
578 let state_in: HashMap<String, serde_json::Value> = self
579 .state_in
580 .iter()
581 .map(|(k, state)| {
582 let vars: HashMap<String, serde_json::Value> = state
583 .values
584 .iter()
585 .map(|(var, val)| (var.clone(), val.to_json_value()))
586 .collect();
587 (k.to_string(), serde_json::json!(vars))
588 })
589 .collect();
590
591 let state_out: HashMap<String, serde_json::Value> = self
592 .state_out
593 .iter()
594 .map(|(k, state)| {
595 let vars: HashMap<String, serde_json::Value> = state
596 .values
597 .iter()
598 .map(|(var, val)| (var.clone(), val.to_json_value()))
599 .collect();
600 (k.to_string(), serde_json::json!(vars))
601 })
602 .collect();
603
604 let div_zero: Vec<_> = self
605 .potential_div_zero
606 .iter()
607 .map(|(line, var)| serde_json::json!({"line": line, "var": var}))
608 .collect();
609
610 let null_deref: Vec<_> = self
611 .potential_null_deref
612 .iter()
613 .map(|(line, var)| serde_json::json!({"line": line, "var": var}))
614 .collect();
615
616 serde_json::json!({
617 "function": self.function_name,
618 "state_in": state_in,
619 "state_out": state_out,
620 "potential_div_zero": div_zero,
621 "potential_null_deref": null_deref,
622 })
623 }
624}
625
626pub fn get_null_keywords(language: &str) -> Vec<&'static str> {
666 match language.to_lowercase().as_str() {
667 "python" => vec!["None"],
668 "typescript" | "javascript" => vec!["null", "undefined"],
669 "go" => vec!["nil"],
670 "rust" => vec![], "java" | "kotlin" | "csharp" | "c#" => vec!["null"],
672 "swift" => vec!["nil"],
673 _ => vec!["null", "nil", "None"], }
675}
676
677pub fn get_boolean_keywords(language: &str) -> HashMap<&'static str, bool> {
701 match language.to_lowercase().as_str() {
702 "python" => [("True", true), ("False", false)].into_iter().collect(),
703 "typescript" | "javascript" | "go" | "rust" | "java" | "kotlin" | "csharp" | "c#"
704 | "swift" => [("true", true), ("false", false)].into_iter().collect(),
705 _ => {
706 [
708 ("True", true),
709 ("False", false),
710 ("true", true),
711 ("false", false),
712 ]
713 .into_iter()
714 .collect()
715 }
716 }
717}
718
719pub fn get_comment_pattern(language: &str) -> &'static str {
747 match language.to_lowercase().as_str() {
748 "python" => "#",
749 "typescript" | "javascript" | "go" | "rust" | "java" | "csharp" | "c#" | "kotlin"
750 | "swift" => "//",
751 _ => "#", }
753}
754
755pub fn strip_comment<'a>(line: &'a str, language: &str) -> &'a str {
786 let pattern = get_comment_pattern(language);
787
788 let mut in_string = false;
791 let mut string_char: Option<char> = None;
792 let mut escape_next = false;
793
794 for (i, c) in line.char_indices() {
795 if escape_next {
796 escape_next = false;
797 continue;
798 }
799
800 if c == '\\' {
801 escape_next = true;
802 continue;
803 }
804
805 if in_string {
806 if Some(c) == string_char {
807 in_string = false;
808 string_char = None;
809 }
810 } else if c == '"' || c == '\'' {
811 in_string = true;
812 string_char = Some(c);
813 } else if line[i..].starts_with(pattern) {
814 return &line[..i];
815 }
816 }
817
818 line
819}
820
821pub fn strip_strings(line: &str, language: &str) -> String {
831 let bytes = line.as_bytes();
832 let len = bytes.len();
833 let mut result = String::with_capacity(len);
834 let mut i = 0;
835
836 while i < len {
837 let c = bytes[i];
838
839 if language == "rust" && c == b'r' {
841 let mut hashes = 0;
843 let mut j = i + 1;
844 while j < len && bytes[j] == b'#' {
845 hashes += 1;
846 j += 1;
847 }
848 if j < len && bytes[j] == b'"' {
849 for &b in &bytes[i..=j] {
852 result.push(b as char);
853 }
854 i = j + 1;
855 let close_start = b'"';
857 loop {
858 if i >= len {
859 break; }
861 if bytes[i] == close_start {
862 let mut matched = 0;
864 let mut k = i + 1;
865 while k < len && bytes[k] == b'#' && matched < hashes {
866 matched += 1;
867 k += 1;
868 }
869 if matched == hashes {
870 for &b in &bytes[i..k] {
872 result.push(b as char);
873 }
874 i = k;
875 break;
876 }
877 }
878 result.push(' ');
880 i += 1;
881 }
882 continue;
883 }
884 }
886
887 if c == b'"' || c == b'\'' || c == b'`' {
889 let delim = c;
890 result.push(c as char);
891 i += 1;
892 while i < len {
893 if bytes[i] == b'\\' {
894 result.push(' ');
896 i += 1;
897 if i < len {
898 result.push(' ');
899 i += 1;
900 }
901 } else if bytes[i] == delim {
902 result.push(delim as char);
904 i += 1;
905 break;
906 } else {
907 result.push(' ');
909 i += 1;
910 }
911 }
912 continue;
913 }
914
915 result.push(c as char);
917 i += 1;
918 }
919
920 result
921}
922
923pub fn is_identifier(s: &str) -> bool {
938 if s.is_empty() {
939 return false;
940 }
941
942 let mut chars = s.chars();
943 match chars.next() {
944 Some(c) if c.is_alphabetic() || c == '_' => {}
945 _ => return false,
946 }
947
948 chars.all(|c| c.is_alphanumeric() || c == '_')
949}
950
951pub fn extract_rhs(line: &str, var: &str) -> Option<String> {
982 let line = line.trim();
983
984 let augmented_ops = &[
986 ("+=", '+'),
987 ("-=", '-'),
988 ("*=", '*'),
989 ("/=", '/'),
990 ("%=", '%'),
991 ];
992
993 for (op_str, op_char) in augmented_ops {
994 let pattern_spaced = format!("{} {} ", var, op_str);
996 let pattern_left_space = format!("{} {}", var, op_str);
997 let pattern_right_space = format!("{}{} ", var, op_str);
998 let pattern_no_space = format!("{}{}", var, op_str);
999
1000 if let Some(idx) = line.find(&pattern_spaced) {
1001 if idx == 0
1002 || !line[..idx]
1003 .chars()
1004 .last()
1005 .map(|c| c.is_alphanumeric() || c == '_')
1006 .unwrap_or(false)
1007 {
1008 let rhs_start = idx + pattern_spaced.len();
1009 let rhs = line[rhs_start..].trim();
1010 return Some(format!("{} {} {}", var, op_char, rhs));
1012 }
1013 }
1014
1015 if let Some(idx) = line.find(&pattern_left_space) {
1016 if idx == 0
1017 || !line[..idx]
1018 .chars()
1019 .last()
1020 .map(|c| c.is_alphanumeric() || c == '_')
1021 .unwrap_or(false)
1022 {
1023 let rhs_start = idx + pattern_left_space.len();
1024 let rhs = line[rhs_start..].trim();
1025 return Some(format!("{} {} {}", var, op_char, rhs));
1026 }
1027 }
1028
1029 if let Some(idx) = line.find(&pattern_right_space) {
1030 if idx == 0
1031 || !line[..idx]
1032 .chars()
1033 .last()
1034 .map(|c| c.is_alphanumeric() || c == '_')
1035 .unwrap_or(false)
1036 {
1037 let rhs_start = idx + pattern_right_space.len();
1038 let rhs = line[rhs_start..].trim();
1039 return Some(format!("{} {} {}", var, op_char, rhs));
1040 }
1041 }
1042
1043 if let Some(idx) = line.find(&pattern_no_space) {
1044 if idx == 0
1045 || !line[..idx]
1046 .chars()
1047 .last()
1048 .map(|c| c.is_alphanumeric() || c == '_')
1049 .unwrap_or(false)
1050 {
1051 let rhs_start = idx + pattern_no_space.len();
1052 let rhs = line[rhs_start..].trim();
1053 return Some(format!("{} {} {}", var, op_char, rhs));
1054 }
1055 }
1056 }
1057
1058 let patterns = [
1061 format!("{} = ", var),
1062 format!("{}= ", var),
1063 format!("{} =", var),
1064 format!("{}=", var),
1065 ];
1066
1067 for pattern in &patterns {
1068 if let Some(idx) = line.find(pattern) {
1069 let valid_start = idx == 0
1072 || !line[..idx]
1073 .chars()
1074 .last()
1075 .map(|c| c.is_alphanumeric() || c == '_')
1076 .unwrap_or(false);
1077
1078 if valid_start {
1079 let rhs_start = idx + pattern.len();
1080 return Some(line[rhs_start..].trim().to_string());
1081 }
1082 }
1083 }
1084
1085 let walrus_pattern = format!("{} := ", var);
1087 if let Some(idx) = line.find(&walrus_pattern) {
1088 let valid_start = idx == 0
1089 || !line[..idx]
1090 .chars()
1091 .last()
1092 .map(|c| c.is_alphanumeric() || c == '_')
1093 .unwrap_or(false);
1094
1095 if valid_start {
1096 let rhs_start = idx + walrus_pattern.len();
1097 return Some(line[rhs_start..].trim().to_string());
1098 }
1099 }
1100
1101 None
1102}
1103
1104pub fn parse_simple_arithmetic(rhs: &str) -> Option<(String, char, i64)> {
1129 let rhs = rhs.trim();
1130
1131 for op in ['+', '-', '*'] {
1134 let parts: Vec<&str> = if rhs.contains(&format!(" {} ", op)) {
1136 rhs.splitn(2, &format!(" {} ", op)).collect()
1137 } else if rhs.contains(op) {
1138 rhs.splitn(2, op).collect()
1139 } else {
1140 continue;
1141 };
1142
1143 if parts.len() != 2 {
1144 continue;
1145 }
1146
1147 let left = parts[0].trim();
1148 let right = parts[1].trim();
1149
1150 if is_identifier(left) {
1152 if let Ok(c) = right.parse::<i64>() {
1153 return Some((left.to_string(), op, c));
1154 }
1155 }
1156
1157 if op == '+' || op == '*' {
1159 if let Ok(c) = left.parse::<i64>() {
1160 if is_identifier(right) {
1161 return Some((right.to_string(), op, c));
1162 }
1163 }
1164 }
1165 }
1166
1167 None
1168}
1169
1170pub fn parse_rhs_abstract(
1208 line: &str,
1209 var: &str,
1210 state: &AbstractState,
1211 language: &str,
1212) -> AbstractValue {
1213 let line = strip_comment(line, language);
1215
1216 let rhs = match extract_rhs(line, var) {
1218 Some(r) => r,
1219 None => return AbstractValue::top(),
1220 };
1221
1222 let rhs = rhs.trim();
1223
1224 if rhs.is_empty() {
1226 return AbstractValue::top();
1227 }
1228
1229 if let Ok(v) = rhs.parse::<i64>() {
1231 return AbstractValue::from_constant(ConstantValue::Int(v));
1232 }
1233
1234 if rhs.contains('.') || rhs.to_lowercase().contains('e') {
1237 if let Ok(v) = rhs.parse::<f64>() {
1238 return AbstractValue::from_constant(ConstantValue::Float(v));
1239 }
1240 }
1241
1242 if (rhs.starts_with('"') && rhs.ends_with('"') && rhs.len() >= 2)
1244 || (rhs.starts_with('\'') && rhs.ends_with('\'') && rhs.len() >= 2)
1245 {
1246 let s = rhs[1..rhs.len() - 1].to_string();
1247 return AbstractValue::from_constant(ConstantValue::String(s));
1248 }
1249
1250 if (rhs.starts_with("\"\"\"") && rhs.ends_with("\"\"\"") && rhs.len() >= 6)
1252 || (rhs.starts_with("'''") && rhs.ends_with("'''") && rhs.len() >= 6)
1253 {
1254 let s = rhs[3..rhs.len() - 3].to_string();
1255 return AbstractValue::from_constant(ConstantValue::String(s));
1256 }
1257
1258 let null_keywords = get_null_keywords(language);
1260 if null_keywords.contains(&rhs) {
1261 if rhs == "undefined" {
1263 return AbstractValue {
1264 type_: Some("undefined".to_string()),
1265 range_: None,
1266 nullable: Nullability::Always,
1267 constant: None,
1268 };
1269 }
1270 return AbstractValue::from_constant(ConstantValue::Null);
1271 }
1272
1273 let bool_keywords = get_boolean_keywords(language);
1275 if let Some(&b) = bool_keywords.get(rhs) {
1276 return AbstractValue::from_constant(ConstantValue::Bool(b));
1277 }
1278
1279 if is_identifier(rhs) {
1281 return state.get(rhs);
1282 }
1283
1284 if let Some((operand_var, op, constant)) = parse_simple_arithmetic(rhs) {
1286 let operand_value = state.get(&operand_var);
1287 return apply_arithmetic(&operand_value, op, constant);
1288 }
1289
1290 AbstractValue::top()
1292}
1293
1294pub fn apply_arithmetic(operand: &AbstractValue, op: char, constant: i64) -> AbstractValue {
1339 let new_range = operand.range_.map(|(low, high)| {
1340 match op {
1341 '+' => {
1342 let new_low = low.and_then(|l| {
1344 let result = l.saturating_add(constant);
1345 if (constant > 0 && result == i64::MAX && l != i64::MAX - constant)
1347 || (constant < 0 && result == i64::MIN && l != i64::MIN - constant)
1348 {
1349 return None;
1350 }
1351 Some(result)
1352 });
1353
1354 let new_high = high.and_then(|h| {
1355 let result = h.saturating_add(constant);
1356 if (constant > 0 && result == i64::MAX && h != i64::MAX - constant)
1358 || (constant < 0 && result == i64::MIN && h != i64::MIN - constant)
1359 {
1360 return None;
1361 }
1362 Some(result)
1363 });
1364
1365 (new_low, new_high)
1366 }
1367 '-' => {
1368 let new_low = low.and_then(|l| {
1370 let result = l.saturating_sub(constant);
1371 if (constant > 0 && result == i64::MIN && l != i64::MIN + constant)
1373 || (constant < 0 && result == i64::MAX && l != i64::MAX + constant)
1374 {
1375 return None;
1376 }
1377 Some(result)
1378 });
1379
1380 let new_high = high.and_then(|h| {
1381 let result = h.saturating_sub(constant);
1382 if (constant > 0 && result == i64::MIN && h != i64::MIN + constant)
1384 || (constant < 0 && result == i64::MAX && h != i64::MAX + constant)
1385 {
1386 return None;
1387 }
1388 Some(result)
1389 });
1390
1391 (new_low, new_high)
1392 }
1393 '*' => {
1394 let compute_mul = |bound: Option<i64>| -> Option<i64> {
1399 bound.and_then(|b| {
1400 if constant == 0 {
1402 return Some(0);
1403 }
1404 b.checked_mul(constant)
1406 })
1407 };
1408
1409 let low_mul = compute_mul(low);
1410 let high_mul = compute_mul(high);
1411
1412 if constant < 0 {
1414 (high_mul, low_mul)
1415 } else if constant == 0 {
1416 (Some(0), Some(0))
1418 } else {
1419 (low_mul, high_mul)
1420 }
1421 }
1422 _ => {
1423 (None, None)
1425 }
1426 }
1427 });
1428
1429 let new_constant = if operand.is_constant() {
1431 if let Some((Some(l), Some(h))) = new_range {
1432 if l == h {
1433 Some(ConstantValue::Int(l))
1434 } else {
1435 None
1436 }
1437 } else {
1438 None
1439 }
1440 } else {
1441 None
1442 };
1443
1444 AbstractValue {
1445 type_: operand.type_.clone(),
1446 range_: new_range,
1447 nullable: operand.nullable,
1448 constant: new_constant,
1449 }
1450}
1451
1452pub fn join_values(a: &AbstractValue, b: &AbstractValue) -> AbstractValue {
1474 let joined_range = match (&a.range_, &b.range_) {
1476 (None, None) => None,
1477 (Some(r), None) | (None, Some(r)) => Some(*r),
1478 (Some((a_low, a_high)), Some((b_low, b_high))) => {
1479 let low = match (a_low, b_low) {
1481 (None, _) | (_, None) => None,
1482 (Some(a), Some(b)) => Some(std::cmp::min(*a, *b)),
1483 };
1484 let high = match (a_high, b_high) {
1485 (None, _) | (_, None) => None,
1486 (Some(a), Some(b)) => Some(std::cmp::max(*a, *b)),
1487 };
1488 Some((low, high))
1489 }
1490 };
1491
1492 let joined_type = if a.type_ == b.type_ {
1494 a.type_.clone()
1495 } else {
1496 None
1497 };
1498
1499 let joined_nullable = match (a.nullable, b.nullable) {
1501 (Nullability::Never, Nullability::Never) => Nullability::Never,
1502 (Nullability::Always, Nullability::Always) => Nullability::Always,
1503 _ => Nullability::Maybe,
1504 };
1505
1506 let joined_constant = match (&a.constant, &b.constant) {
1508 (Some(ca), Some(cb)) if ca == cb => Some(ca.clone()),
1509 _ => None,
1510 };
1511
1512 AbstractValue {
1513 type_: joined_type,
1514 range_: joined_range,
1515 nullable: joined_nullable,
1516 constant: joined_constant,
1517 }
1518}
1519
1520pub fn join_states(states: &[&AbstractState]) -> AbstractState {
1535 if states.is_empty() {
1536 return AbstractState::default();
1537 }
1538 if states.len() == 1 {
1539 return states[0].clone();
1540 }
1541
1542 let all_vars: std::collections::HashSet<_> = states
1544 .iter()
1545 .flat_map(|s| s.values.keys().cloned())
1546 .collect();
1547
1548 let mut result = HashMap::new();
1549 for var in all_vars {
1550 let values: Vec<AbstractValue> = states.iter().map(|s| s.get(&var)).collect();
1552
1553 let mut joined = values[0].clone();
1555 for val in values.iter().skip(1) {
1556 joined = join_values(&joined, val);
1557 }
1558 result.insert(var, joined);
1559 }
1560
1561 AbstractState { values: result }
1562}
1563
1564pub fn widen_value(old: &AbstractValue, new: &AbstractValue) -> AbstractValue {
1584 let widened_range = match (&old.range_, &new.range_) {
1585 (None, None) => None,
1586 (None, r) => *r,
1587 (_, None) => None, (Some((old_low, old_high)), Some((new_low, new_high))) => {
1589 let widened_low = match (old_low, new_low) {
1591 (None, _) => None, (_, None) => None, (Some(o), Some(n)) if *n < *o => None, (_, n) => *n, };
1596
1597 let widened_high = match (old_high, new_high) {
1599 (None, _) => None, (_, None) => None, (Some(o), Some(n)) if *n > *o => None, (_, n) => *n, };
1604
1605 Some((widened_low, widened_high))
1606 }
1607 };
1608
1609 AbstractValue {
1610 type_: new.type_.clone(),
1611 range_: widened_range,
1612 nullable: new.nullable,
1613 constant: None, }
1615}
1616
1617pub fn widen_state(old: &AbstractState, new: &AbstractState) -> AbstractState {
1630 let all_vars: std::collections::HashSet<_> = old
1632 .values
1633 .keys()
1634 .chain(new.values.keys())
1635 .cloned()
1636 .collect();
1637
1638 let mut result = HashMap::new();
1639 for var in all_vars {
1640 let old_val = old.get(&var);
1641 let new_val = new.get(&var);
1642 result.insert(var, widen_value(&old_val, &new_val));
1643 }
1644
1645 AbstractState { values: result }
1646}
1647
1648use super::types::{
1653 build_predecessors, find_back_edges, reverse_postorder, validate_cfg, DataflowError,
1654};
1655use crate::types::{CfgInfo, DfgInfo, RefType, VarRef};
1656
1657pub fn init_params(cfg: &CfgInfo, dfg: &DfgInfo) -> AbstractState {
1673 let mut state = AbstractState::new();
1674
1675 let entry_block = cfg.blocks.iter().find(|b| b.id == cfg.entry_block);
1677
1678 if let Some(entry) = entry_block {
1679 for var_ref in &dfg.refs {
1682 if var_ref.ref_type == RefType::Definition {
1684 if var_ref.line >= entry.lines.0 && var_ref.line <= entry.lines.1 {
1686 state
1688 .values
1689 .insert(var_ref.name.clone(), AbstractValue::top());
1690 }
1691 }
1692 }
1693 }
1694
1695 state
1696}
1697
1698pub fn transfer_block(
1722 state: &AbstractState,
1723 block: &crate::types::CfgBlock,
1724 dfg: &DfgInfo,
1725 source_lines: Option<&[&str]>,
1726 language: &str,
1727) -> AbstractState {
1728 let mut current_state = state.clone();
1729
1730 let mut defs_in_block: Vec<&VarRef> = dfg
1732 .refs
1733 .iter()
1734 .filter(|r| {
1735 r.ref_type == RefType::Definition && r.line >= block.lines.0 && r.line <= block.lines.1
1736 })
1737 .collect();
1738
1739 defs_in_block.sort_by_key(|r| (r.line, r.column));
1741
1742 for var_ref in defs_in_block {
1744 let new_value = if let Some(lines) = source_lines {
1746 let line_idx = var_ref.line.saturating_sub(1) as usize;
1748 if line_idx < lines.len() {
1749 let line = lines[line_idx];
1750 parse_rhs_abstract(line, &var_ref.name, ¤t_state, language)
1751 } else {
1752 AbstractValue::top()
1753 }
1754 } else {
1755 AbstractValue::top()
1757 };
1758
1759 current_state = current_state.set(&var_ref.name, new_value);
1761 }
1762
1763 current_state
1764}
1765
1766pub fn find_div_zero(
1805 cfg: &CfgInfo,
1806 dfg: &DfgInfo,
1807 state_in: &HashMap<BlockId, AbstractState>,
1808 source_lines: Option<&[&str]>,
1809 _state_out: &HashMap<BlockId, AbstractState>,
1810 language: &str,
1811) -> Vec<(usize, String)> {
1812 let mut warnings = Vec::new();
1813
1814 let Some(lines) = source_lines else {
1815 return warnings;
1816 };
1817
1818 let div_patterns: &[&str] = match language {
1820 "python" => &["/", "//", "%"],
1821 "rust" | "go" | "typescript" | "javascript" | "java" | "c" | "cpp" => &["/", "%"],
1822 _ => &["/", "%"],
1823 };
1824
1825 for (line_idx, line) in lines.iter().enumerate() {
1827 let line_num = line_idx + 1; let code_no_comments = strip_comment(line, language);
1832 let code = strip_strings(code_no_comments, language);
1833
1834 for &op in div_patterns {
1836 let mut search_start = 0;
1838 while let Some(pos) = code[search_start..].find(op) {
1839 let actual_pos = search_start + pos;
1840
1841 if op == "/" && code.len() > actual_pos + 1 {
1843 let next_char = code.chars().nth(actual_pos + 1);
1844 if next_char == Some('/') {
1845 search_start = actual_pos + 2;
1847 continue;
1848 }
1849 if actual_pos > 0 && code.chars().nth(actual_pos - 1) == Some('/') {
1851 search_start = actual_pos + 1;
1852 continue;
1853 }
1854 }
1855
1856 let after_op = &code[actual_pos + op.len()..];
1858 let divisor = extract_divisor(after_op.trim());
1859
1860 if let Some(div_var) = divisor {
1861 if is_identifier(&div_var) {
1862 let block = cfg
1864 .blocks
1865 .iter()
1866 .find(|b| line_num as u32 >= b.lines.0 && line_num as u32 <= b.lines.1);
1867
1868 if let Some(block) = block {
1869 let state_at_div = compute_state_at_line(
1871 block,
1872 dfg,
1873 state_in.get(&block.id).cloned().unwrap_or_default(),
1874 source_lines,
1875 line_num,
1876 language,
1877 );
1878
1879 let divisor_val = state_at_div.get(&div_var);
1880 if divisor_val.may_be_zero() {
1881 warnings.push((line_num, div_var));
1882 }
1883 }
1884 }
1885 }
1886
1887 search_start = actual_pos + op.len();
1888 }
1889 }
1890 }
1891
1892 warnings.sort();
1894 warnings.dedup();
1895
1896 warnings
1897}
1898
1899fn extract_divisor(s: &str) -> Option<String> {
1904 let s = s.trim();
1905 if s.is_empty() {
1906 return None;
1907 }
1908
1909 let mut chars = s.chars().peekable();
1911
1912 if chars.peek() == Some(&'(') {
1914 return None;
1915 }
1916
1917 let mut ident = String::new();
1918 while let Some(&c) = chars.peek() {
1919 if c.is_alphanumeric() || c == '_' {
1920 ident.push(c);
1921 chars.next();
1922 } else {
1923 break;
1924 }
1925 }
1926
1927 if ident.is_empty() || ident.chars().next().unwrap().is_ascii_digit() {
1928 None
1931 } else {
1932 Some(ident)
1933 }
1934}
1935
1936fn compute_state_at_line(
1941 block: &crate::types::CfgBlock,
1942 dfg: &DfgInfo,
1943 state_in: AbstractState,
1944 source_lines: Option<&[&str]>,
1945 target_line: usize,
1946 language: &str,
1947) -> AbstractState {
1948 let mut current_state = state_in;
1949
1950 let mut defs_in_block: Vec<&VarRef> = dfg
1952 .refs
1953 .iter()
1954 .filter(|r| {
1955 r.ref_type == RefType::Definition
1956 && r.line >= block.lines.0
1957 && r.line <= block.lines.1
1958 && (r.line as usize) < target_line })
1960 .collect();
1961
1962 defs_in_block.sort_by_key(|r| (r.line, r.column));
1964
1965 for var_ref in defs_in_block {
1967 let new_value = if let Some(lines) = source_lines {
1969 let line_idx = var_ref.line.saturating_sub(1) as usize;
1971 if line_idx < lines.len() {
1972 let line = lines[line_idx];
1973 parse_rhs_abstract(line, &var_ref.name, ¤t_state, language)
1974 } else {
1975 AbstractValue::top()
1976 }
1977 } else {
1978 AbstractValue::top()
1979 };
1980
1981 current_state = current_state.set(&var_ref.name, new_value);
1983 }
1984
1985 current_state
1986}
1987
1988pub fn find_null_deref(
2004 cfg: &CfgInfo,
2005 dfg: &DfgInfo,
2006 state_in: &HashMap<BlockId, AbstractState>,
2007 source_lines: Option<&[&str]>,
2008 language: &str,
2009) -> Vec<(usize, String)> {
2010 let mut warnings = Vec::new();
2011
2012 let Some(lines) = source_lines else {
2013 return warnings;
2014 };
2015
2016 for (line_idx, line) in lines.iter().enumerate() {
2018 let line_num = line_idx + 1; let code = strip_comment(line, language);
2022
2023 let patterns = extract_deref_patterns(code);
2026
2027 for var in patterns {
2028 if is_identifier(&var) {
2029 let block = cfg
2031 .blocks
2032 .iter()
2033 .find(|b| line_num as u32 >= b.lines.0 && line_num as u32 <= b.lines.1);
2034
2035 if let Some(block) = block {
2036 let state_at_deref = compute_state_at_line(
2038 block,
2039 dfg,
2040 state_in.get(&block.id).cloned().unwrap_or_default(),
2041 source_lines,
2042 line_num,
2043 language,
2044 );
2045
2046 let var_val = state_at_deref.get(&var);
2047 if var_val.may_be_null() {
2048 warnings.push((line_num, var));
2049 }
2050 }
2051 }
2052 }
2053 }
2054
2055 warnings.sort();
2057 warnings.dedup();
2058
2059 warnings
2060}
2061
2062fn extract_deref_patterns(code: &str) -> Vec<String> {
2070 let mut patterns = Vec::new();
2071 let chars: Vec<char> = code.chars().collect();
2072 let len = chars.len();
2073 let mut i = 0;
2074
2075 while i < len {
2076 while i < len && !chars[i].is_alphabetic() && chars[i] != '_' {
2078 i += 1;
2079 }
2080
2081 if i >= len {
2082 break;
2083 }
2084
2085 let start = i;
2087 while i < len && (chars[i].is_alphanumeric() || chars[i] == '_') {
2088 i += 1;
2089 }
2090
2091 let ident: String = chars[start..i].iter().collect();
2092
2093 if i < len && (chars[i] == '.' || chars[i] == '[') {
2095 if !ident.is_empty() && !ident.chars().next().unwrap().is_ascii_digit() {
2097 let keywords = ["self", "this", "super", "cls"];
2099 if !keywords.contains(&ident.as_str()) {
2100 patterns.push(ident);
2101 }
2102 }
2103 }
2104 }
2105
2106 patterns
2107}
2108
2109pub fn compute_abstract_interp(
2158 cfg: &CfgInfo,
2159 dfg: &DfgInfo,
2160 source_lines: Option<&[&str]>,
2161 language: &str,
2162) -> Result<AbstractInterpInfo, DataflowError> {
2163 validate_cfg(cfg)?;
2165
2166 let predecessors = build_predecessors(cfg);
2168 let loop_headers = find_back_edges(cfg);
2169 let block_order = reverse_postorder(cfg);
2170
2171 let mut state_in: HashMap<BlockId, AbstractState> = HashMap::new();
2173 let mut state_out: HashMap<BlockId, AbstractState> = HashMap::new();
2174
2175 let entry = cfg.entry_block;
2176
2177 let init_state = init_params(cfg, dfg);
2179 state_in.insert(entry, init_state.clone());
2180
2181 if let Some(entry_block) = cfg.blocks.iter().find(|b| b.id == entry) {
2183 let entry_out = transfer_block(&init_state, entry_block, dfg, source_lines, language);
2184 state_out.insert(entry, entry_out);
2185 } else {
2186 state_out.insert(entry, init_state);
2187 }
2188
2189 for block in &cfg.blocks {
2191 if block.id != entry {
2192 state_in.insert(block.id, AbstractState::default());
2193 state_out.insert(block.id, AbstractState::default());
2194 }
2195 }
2196
2197 let max_iterations = cfg.blocks.len() * 10 + 100;
2199 let mut iteration = 0;
2200 let mut changed = true;
2201
2202 while changed && iteration < max_iterations {
2204 changed = false;
2205 iteration += 1;
2206
2207 for &block_id in &block_order {
2208 if block_id == entry {
2210 continue;
2211 }
2212
2213 let block = match cfg.blocks.iter().find(|b| b.id == block_id) {
2215 Some(b) => b,
2216 None => continue,
2217 };
2218
2219 let preds = predecessors.get(&block_id).cloned().unwrap_or_default();
2221
2222 let mut new_in = if preds.is_empty() {
2224 AbstractState::default()
2225 } else {
2226 let pred_states: Vec<&AbstractState> =
2228 preds.iter().filter_map(|p| state_out.get(p)).collect();
2229
2230 if pred_states.is_empty() {
2231 AbstractState::default()
2232 } else {
2233 join_states(&pred_states)
2234 }
2235 };
2236
2237 if loop_headers.contains(&block_id) {
2239 if let Some(old_in) = state_in.get(&block_id) {
2240 new_in = widen_state(old_in, &new_in);
2241 }
2242 }
2243
2244 let new_out = transfer_block(&new_in, block, dfg, source_lines, language);
2246
2247 let old_in = state_in.get(&block_id);
2249 let old_out = state_out.get(&block_id);
2250
2251 if old_in != Some(&new_in) || old_out != Some(&new_out) {
2252 changed = true;
2253 state_in.insert(block_id, new_in);
2254 state_out.insert(block_id, new_out);
2255 }
2256 }
2257 }
2258
2259 let potential_div_zero = find_div_zero(cfg, dfg, &state_in, source_lines, &state_out, language);
2261 let potential_null_deref = find_null_deref(cfg, dfg, &state_in, source_lines, language);
2262
2263 Ok(AbstractInterpInfo {
2265 state_in,
2266 state_out,
2267 potential_div_zero,
2268 potential_null_deref,
2269 function_name: cfg.function.clone(),
2270 })
2271}
2272
2273#[cfg(test)]
2278mod tests {
2279 use super::*;
2280 use std::collections::hash_map::DefaultHasher;
2281 use std::f64::consts::PI;
2282
2283 #[test]
2288 fn test_nullability_enum_has_three_values() {
2289 let _never = Nullability::Never;
2291 let _maybe = Nullability::Maybe;
2292 let _always = Nullability::Always;
2293
2294 assert_eq!(Nullability::Never.as_str(), "never");
2296 assert_eq!(Nullability::Maybe.as_str(), "maybe");
2297 assert_eq!(Nullability::Always.as_str(), "always");
2298 }
2299
2300 #[test]
2301 fn test_nullability_default_is_maybe() {
2302 let default: Nullability = Default::default();
2304 assert_eq!(default, Nullability::Maybe);
2305 }
2306
2307 #[test]
2312 fn test_abstract_value_has_required_fields() {
2313 let value = AbstractValue {
2315 type_: Some("int".to_string()),
2316 range_: Some((Some(1), Some(10))),
2317 nullable: Nullability::Never,
2318 constant: Some(ConstantValue::Int(5)),
2319 };
2320
2321 assert_eq!(value.type_, Some("int".to_string()));
2322 assert_eq!(value.range_, Some((Some(1), Some(10))));
2323 assert_eq!(value.nullable, Nullability::Never);
2324 assert!(value.constant.is_some());
2325 }
2326
2327 #[test]
2328 fn test_abstract_value_is_hashable() {
2329 let value1 = AbstractValue::from_constant(ConstantValue::Int(5));
2331 let value2 = AbstractValue::from_constant(ConstantValue::Int(5));
2332
2333 let mut hasher1 = DefaultHasher::new();
2334 let mut hasher2 = DefaultHasher::new();
2335 value1.hash(&mut hasher1);
2336 value2.hash(&mut hasher2);
2337
2338 assert_eq!(hasher1.finish(), hasher2.finish());
2339 }
2340
2341 #[test]
2342 fn test_abstract_value_top_creates_unknown() {
2343 let top = AbstractValue::top();
2345
2346 assert_eq!(top.type_, None);
2347 assert_eq!(top.range_, None);
2348 assert_eq!(top.nullable, Nullability::Maybe);
2349 assert!(top.constant.is_none());
2350 }
2351
2352 #[test]
2353 fn test_abstract_value_bottom_creates_contradiction() {
2354 let bottom = AbstractValue::bottom();
2356
2357 assert_eq!(bottom.type_, Some("<bottom>".to_string()));
2358 assert_eq!(bottom.range_, Some((None, None)));
2359 assert_eq!(bottom.nullable, Nullability::Never);
2360 assert!(bottom.constant.is_none());
2361 }
2362
2363 #[test]
2364 fn test_abstract_value_from_constant_int() {
2365 let value = AbstractValue::from_constant(ConstantValue::Int(5));
2367
2368 assert_eq!(value.type_, Some("int".to_string()));
2369 assert_eq!(value.range_, Some((Some(5), Some(5))));
2370 assert_eq!(value.nullable, Nullability::Never);
2371 assert_eq!(value.constant, Some(ConstantValue::Int(5)));
2372 }
2373
2374 #[test]
2375 fn test_abstract_value_from_constant_negative_int() {
2376 let value = AbstractValue::from_constant(ConstantValue::Int(-42));
2378
2379 assert_eq!(value.type_, Some("int".to_string()));
2380 assert_eq!(value.range_, Some((Some(-42), Some(-42))));
2381 assert_eq!(value.nullable, Nullability::Never);
2382 assert_eq!(value.constant, Some(ConstantValue::Int(-42)));
2383 }
2384
2385 #[test]
2386 fn test_abstract_value_from_constant_string() {
2387 let value = AbstractValue::from_constant(ConstantValue::String("hello".to_string()));
2389
2390 assert_eq!(value.type_, Some("str".to_string()));
2391 assert_eq!(value.nullable, Nullability::Never);
2392 assert!(value.constant.is_some());
2393 }
2394
2395 #[test]
2396 fn test_abstract_value_string_tracks_length() {
2397 let value = AbstractValue::from_constant(ConstantValue::String("hello".to_string()));
2399
2400 assert_eq!(value.range_, Some((Some(5), Some(5))));
2402 }
2403
2404 #[test]
2405 fn test_abstract_value_from_constant_none() {
2406 let value = AbstractValue::from_constant(ConstantValue::Null);
2408
2409 assert_eq!(value.type_, Some("NoneType".to_string()));
2410 assert_eq!(value.range_, None);
2411 assert_eq!(value.nullable, Nullability::Always);
2412 assert!(value.constant.is_none()); }
2414
2415 #[test]
2416 fn test_abstract_value_from_constant_bool() {
2417 let value_true = AbstractValue::from_constant(ConstantValue::Bool(true));
2419 let value_false = AbstractValue::from_constant(ConstantValue::Bool(false));
2420
2421 assert_eq!(value_true.type_, Some("bool".to_string()));
2422 assert_eq!(value_true.range_, Some((Some(1), Some(1)))); assert_eq!(value_false.range_, Some((Some(0), Some(0)))); }
2425
2426 #[test]
2427 fn test_abstract_value_from_constant_float() {
2428 let value = AbstractValue::from_constant(ConstantValue::Float(PI));
2430
2431 assert_eq!(value.type_, Some("float".to_string()));
2432 assert_eq!(value.range_, None); assert_eq!(value.nullable, Nullability::Never);
2434 }
2435
2436 #[test]
2441 fn test_may_be_zero_returns_true_when_range_includes_zero() {
2442 let value = AbstractValue {
2444 type_: Some("int".to_string()),
2445 range_: Some((Some(-5), Some(5))),
2446 nullable: Nullability::Never,
2447 constant: None,
2448 };
2449 assert!(value.may_be_zero());
2450
2451 let exact_zero = AbstractValue::from_constant(ConstantValue::Int(0));
2453 assert!(exact_zero.may_be_zero());
2454 }
2455
2456 #[test]
2457 fn test_may_be_zero_returns_false_when_range_excludes_zero() {
2458 let positive = AbstractValue {
2460 type_: Some("int".to_string()),
2461 range_: Some((Some(1), Some(10))),
2462 nullable: Nullability::Never,
2463 constant: None,
2464 };
2465 assert!(!positive.may_be_zero());
2466
2467 let negative = AbstractValue {
2468 type_: Some("int".to_string()),
2469 range_: Some((Some(-10), Some(-1))),
2470 nullable: Nullability::Never,
2471 constant: None,
2472 };
2473 assert!(!negative.may_be_zero());
2474 }
2475
2476 #[test]
2477 fn test_may_be_zero_returns_true_for_unknown_range() {
2478 let top = AbstractValue::top();
2480 assert!(top.may_be_zero());
2481 }
2482
2483 #[test]
2488 fn test_may_be_null_for_maybe() {
2489 let value = AbstractValue {
2491 type_: None,
2492 range_: None,
2493 nullable: Nullability::Maybe,
2494 constant: None,
2495 };
2496 assert!(value.may_be_null());
2497 }
2498
2499 #[test]
2500 fn test_may_be_null_for_never() {
2501 let value = AbstractValue::from_constant(ConstantValue::Int(5));
2503 assert!(!value.may_be_null());
2504 }
2505
2506 #[test]
2507 fn test_may_be_null_for_always() {
2508 let value = AbstractValue::from_constant(ConstantValue::Null);
2510 assert!(value.may_be_null());
2511 }
2512
2513 #[test]
2518 fn test_is_constant_true_when_constant_set() {
2519 let value = AbstractValue::from_constant(ConstantValue::Int(42));
2520 assert!(value.is_constant());
2521 }
2522
2523 #[test]
2524 fn test_is_constant_false_when_constant_none() {
2525 let value = AbstractValue::top();
2526 assert!(!value.is_constant());
2527 }
2528
2529 #[test]
2534 fn test_abstract_state_empty_initialization() {
2535 let state = AbstractState::new();
2536 assert!(state.values.is_empty());
2537 }
2538
2539 #[test]
2540 fn test_abstract_state_get_returns_value_for_existing_var() {
2541 let mut state = AbstractState::new();
2542 let value = AbstractValue::from_constant(ConstantValue::Int(5));
2543 state.values.insert("x".to_string(), value.clone());
2544
2545 let retrieved = state.get("x");
2546 assert_eq!(retrieved.range_, Some((Some(5), Some(5))));
2547 }
2548
2549 #[test]
2550 fn test_abstract_state_get_returns_top_for_missing_var() {
2551 let state = AbstractState::new();
2553 let value = state.get("nonexistent");
2554
2555 assert_eq!(value.type_, None);
2556 assert_eq!(value.range_, None);
2557 assert_eq!(value.nullable, Nullability::Maybe);
2558 }
2559
2560 #[test]
2561 fn test_abstract_state_set_returns_new_state() {
2562 let state1 = AbstractState::new();
2564 let state2 = state1.set("x", AbstractValue::from_constant(ConstantValue::Int(5)));
2565
2566 assert!(state1.values.is_empty());
2568 assert!(state2.values.contains_key("x"));
2570 }
2571
2572 #[test]
2573 fn test_abstract_state_copy_creates_independent_copy() {
2574 let mut state1 = AbstractState::new();
2575 state1.values.insert(
2576 "x".to_string(),
2577 AbstractValue::from_constant(ConstantValue::Int(5)),
2578 );
2579
2580 let state2 = state1.copy();
2581
2582 state1.values.insert(
2584 "y".to_string(),
2585 AbstractValue::from_constant(ConstantValue::Int(10)),
2586 );
2587
2588 assert!(state2.values.contains_key("x"));
2590 assert!(!state2.values.contains_key("y"));
2591 }
2592
2593 #[test]
2594 fn test_abstract_state_equality() {
2595 let state1 =
2596 AbstractState::new().set("x", AbstractValue::from_constant(ConstantValue::Int(5)));
2597 let state2 =
2598 AbstractState::new().set("x", AbstractValue::from_constant(ConstantValue::Int(5)));
2599 let state3 =
2600 AbstractState::new().set("x", AbstractValue::from_constant(ConstantValue::Int(10)));
2601
2602 assert_eq!(state1, state2);
2603 assert_ne!(state1, state3);
2604 }
2605
2606 #[test]
2611 fn test_abstract_interp_info_has_required_fields() {
2612 let info = AbstractInterpInfo::new("test_func");
2613
2614 assert_eq!(info.function_name, "test_func");
2615 assert!(info.state_in.is_empty());
2616 assert!(info.state_out.is_empty());
2617 assert!(info.potential_div_zero.is_empty());
2618 assert!(info.potential_null_deref.is_empty());
2619 }
2620
2621 #[test]
2622 fn test_value_at_returns_abstract_value_at_block_entry() {
2623 let mut info = AbstractInterpInfo::new("test");
2624 let state =
2625 AbstractState::new().set("x", AbstractValue::from_constant(ConstantValue::Int(42)));
2626 info.state_in.insert(0, state);
2627
2628 let value = info.value_at(0, "x");
2629 assert_eq!(value.range_, Some((Some(42), Some(42))));
2630 }
2631
2632 #[test]
2633 fn test_value_at_returns_top_for_missing_block() {
2634 let info = AbstractInterpInfo::new("test");
2635 let value = info.value_at(999, "x");
2636
2637 assert_eq!(value.type_, None);
2639 assert_eq!(value.range_, None);
2640 }
2641
2642 #[test]
2643 fn test_value_at_exit_returns_value_at_block_exit() {
2644 let mut info = AbstractInterpInfo::new("test");
2645 let state =
2646 AbstractState::new().set("y", AbstractValue::from_constant(ConstantValue::Int(100)));
2647 info.state_out.insert(1, state);
2648
2649 let value = info.value_at_exit(1, "y");
2650 assert_eq!(value.range_, Some((Some(100), Some(100))));
2651 }
2652
2653 #[test]
2654 fn test_range_at_returns_range_tuple() {
2655 let mut info = AbstractInterpInfo::new("test");
2656 let state =
2657 AbstractState::new().set("x", AbstractValue::from_constant(ConstantValue::Int(5)));
2658 info.state_in.insert(0, state);
2659
2660 let range = info.range_at(0, "x");
2661 assert_eq!(range, Some((Some(5), Some(5))));
2662 }
2663
2664 #[test]
2665 fn test_type_at_returns_inferred_type() {
2666 let mut info = AbstractInterpInfo::new("test");
2667 let state = AbstractState::new().set(
2668 "x",
2669 AbstractValue::from_constant(ConstantValue::String("hello".to_string())),
2670 );
2671 info.state_in.insert(0, state);
2672
2673 let type_ = info.type_at(0, "x");
2674 assert_eq!(type_, Some("str".to_string()));
2675 }
2676
2677 #[test]
2678 fn test_is_definitely_not_null_for_never_nullable() {
2679 let mut info = AbstractInterpInfo::new("test");
2680 let state =
2681 AbstractState::new().set("x", AbstractValue::from_constant(ConstantValue::Int(5)));
2682 info.state_in.insert(0, state);
2683
2684 assert!(info.is_definitely_not_null(0, "x"));
2685 }
2686
2687 #[test]
2688 fn test_is_definitely_not_null_for_maybe_nullable() {
2689 let mut info = AbstractInterpInfo::new("test");
2690 let state = AbstractState::new().set("x", AbstractValue::top());
2691 info.state_in.insert(0, state);
2692
2693 assert!(!info.is_definitely_not_null(0, "x"));
2694 }
2695
2696 #[test]
2697 fn test_get_constants_returns_known_constant_values() {
2698 let mut info = AbstractInterpInfo::new("test");
2699 let state = AbstractState::new()
2700 .set("x", AbstractValue::from_constant(ConstantValue::Int(5)))
2701 .set(
2702 "y",
2703 AbstractValue::from_constant(ConstantValue::String("hello".to_string())),
2704 )
2705 .set("z", AbstractValue::top()); info.state_out.insert(0, state);
2707
2708 let constants = info.get_constants();
2709 assert_eq!(constants.len(), 2);
2710 assert!(constants.contains_key("x"));
2711 assert!(constants.contains_key("y"));
2712 assert!(!constants.contains_key("z"));
2713 }
2714
2715 #[test]
2716 fn test_abstract_interp_to_json_serializable() {
2717 let mut info = AbstractInterpInfo::new("example");
2718 let state =
2719 AbstractState::new().set("x", AbstractValue::from_constant(ConstantValue::Int(42)));
2720 info.state_in.insert(0, state.clone());
2721 info.state_out.insert(0, state);
2722 info.potential_div_zero.push((10, "y".to_string()));
2723 info.potential_null_deref.push((15, "obj".to_string()));
2724
2725 let json = info.to_json();
2726
2727 assert!(json.is_object());
2729 assert_eq!(json["function"], "example");
2730 assert!(json["state_in"].is_object());
2731 assert!(json["state_out"].is_object());
2732 assert!(json["potential_div_zero"].is_array());
2733 assert!(json["potential_null_deref"].is_array());
2734
2735 let serialized = serde_json::to_string(&json);
2737 assert!(serialized.is_ok());
2738 }
2739
2740 #[test]
2745 fn test_python_none_keyword_recognized() {
2746 let keywords = get_null_keywords("python");
2748 assert!(keywords.contains(&"None"));
2749 }
2750
2751 #[test]
2752 fn test_typescript_null_keyword_recognized() {
2753 let keywords = get_null_keywords("typescript");
2755 assert!(keywords.contains(&"null"));
2756 }
2757
2758 #[test]
2759 fn test_typescript_undefined_keyword_recognized() {
2760 let keywords = get_null_keywords("typescript");
2762 assert!(keywords.contains(&"undefined"));
2763 }
2764
2765 #[test]
2766 fn test_go_nil_keyword_recognized() {
2767 let keywords = get_null_keywords("go");
2769 assert!(keywords.contains(&"nil"));
2770 }
2771
2772 #[test]
2773 fn test_rust_has_no_null_keyword() {
2774 let keywords = get_null_keywords("rust");
2776 assert!(keywords.is_empty(), "Rust should have no null keywords");
2777 }
2778
2779 #[test]
2780 fn test_python_boolean_capitalized() {
2781 let bools = get_boolean_keywords("python");
2783 assert_eq!(bools.get("True"), Some(&true));
2784 assert_eq!(bools.get("False"), Some(&false));
2785 }
2786
2787 #[test]
2788 fn test_typescript_boolean_lowercase() {
2789 let bools = get_boolean_keywords("typescript");
2791 assert_eq!(bools.get("true"), Some(&true));
2792 assert_eq!(bools.get("false"), Some(&false));
2793 }
2794
2795 #[test]
2796 fn test_python_comment_pattern() {
2797 let pattern = get_comment_pattern("python");
2799 assert_eq!(pattern, "#");
2800 }
2801
2802 #[test]
2803 fn test_typescript_comment_pattern() {
2804 let pattern = get_comment_pattern("typescript");
2806 assert_eq!(pattern, "//");
2807 }
2808
2809 #[test]
2814 fn test_arithmetic_add() {
2815 let operand = AbstractValue::from_constant(ConstantValue::Int(5));
2818 let result = apply_arithmetic(&operand, '+', 3);
2819
2820 assert_eq!(result.range_, Some((Some(8), Some(8))));
2821 assert_eq!(result.constant, Some(ConstantValue::Int(8)));
2822 }
2823
2824 #[test]
2825 fn test_arithmetic_subtract() {
2826 let operand = AbstractValue::from_constant(ConstantValue::Int(10));
2829 let result = apply_arithmetic(&operand, '-', 3);
2830
2831 assert_eq!(result.range_, Some((Some(7), Some(7))));
2832 assert_eq!(result.constant, Some(ConstantValue::Int(7)));
2833 }
2834
2835 #[test]
2836 fn test_arithmetic_multiply() {
2837 let operand = AbstractValue::from_constant(ConstantValue::Int(4));
2840 let result = apply_arithmetic(&operand, '*', 2);
2841
2842 assert_eq!(result.range_, Some((Some(8), Some(8))));
2843 assert_eq!(result.constant, Some(ConstantValue::Int(8)));
2844 }
2845
2846 #[test]
2847 fn test_arithmetic_on_range() {
2848 let operand = AbstractValue {
2851 type_: Some("int".to_string()),
2852 range_: Some((Some(1), Some(5))),
2853 nullable: Nullability::Never,
2854 constant: None,
2855 };
2856
2857 let result = apply_arithmetic(&operand, '+', 10);
2858
2859 assert_eq!(result.range_, Some((Some(11), Some(15))));
2860 assert!(result.constant.is_none());
2862 }
2863
2864 #[test]
2865 fn test_arithmetic_overflow_saturates_add() {
2866 let operand = AbstractValue::from_constant(ConstantValue::Int(i64::MAX));
2869 let result = apply_arithmetic(&operand, '+', 1);
2870
2871 match result.range_ {
2873 Some((low, high)) => {
2874 assert!(
2876 low.is_none() || high.is_none(),
2877 "Overflow should widen to unbounded: got ({:?}, {:?})",
2878 low,
2879 high
2880 );
2881 }
2882 None => {
2883 }
2885 }
2886 }
2887
2888 #[test]
2889 fn test_arithmetic_overflow_saturates_sub() {
2890 let operand = AbstractValue::from_constant(ConstantValue::Int(i64::MIN));
2893 let result = apply_arithmetic(&operand, '-', 1);
2894
2895 if let Some((low, high)) = result.range_ {
2897 assert!(
2898 low.is_none() || high.is_none(),
2899 "Underflow should widen to unbounded: got ({:?}, {:?})",
2900 low,
2901 high
2902 );
2903 }
2904 }
2905
2906 #[test]
2907 fn test_arithmetic_multiply_by_negative() {
2908 let operand = AbstractValue {
2911 type_: Some("int".to_string()),
2912 range_: Some((Some(2), Some(4))),
2913 nullable: Nullability::Never,
2914 constant: None,
2915 };
2916
2917 let result = apply_arithmetic(&operand, '*', -3);
2918
2919 assert_eq!(result.range_, Some((Some(-12), Some(-6))));
2920 }
2921
2922 #[test]
2923 fn test_arithmetic_multiply_by_zero() {
2924 let operand = AbstractValue {
2926 type_: Some("int".to_string()),
2927 range_: Some((Some(1), Some(100))),
2928 nullable: Nullability::Never,
2929 constant: None,
2930 };
2931
2932 let result = apply_arithmetic(&operand, '*', 0);
2933
2934 assert_eq!(result.range_, Some((Some(0), Some(0))));
2935 }
2936
2937 #[test]
2938 fn test_arithmetic_preserves_type() {
2939 let operand = AbstractValue::from_constant(ConstantValue::Int(5));
2941 let result = apply_arithmetic(&operand, '+', 3);
2942
2943 assert_eq!(result.type_, Some("int".to_string()));
2944 }
2945
2946 #[test]
2947 fn test_arithmetic_preserves_nullable() {
2948 let operand = AbstractValue::from_constant(ConstantValue::Int(5));
2950 assert_eq!(operand.nullable, Nullability::Never);
2951
2952 let result = apply_arithmetic(&operand, '+', 3);
2953 assert_eq!(result.nullable, Nullability::Never);
2954 }
2955
2956 #[test]
2957 fn test_arithmetic_unknown_op() {
2958 let operand = AbstractValue::from_constant(ConstantValue::Int(5));
2960 let result = apply_arithmetic(&operand, '^', 3); assert_eq!(result.range_, Some((None, None)));
2964 assert!(result.constant.is_none());
2965 }
2966
2967 #[test]
2968 fn test_arithmetic_on_no_range() {
2969 let operand = AbstractValue::top();
2971 let result = apply_arithmetic(&operand, '+', 5);
2972
2973 assert!(result.range_.is_none());
2974 }
2975
2976 #[test]
2981 fn test_join_values_ranges_union() {
2982 let val1 = AbstractValue::from_constant(ConstantValue::Int(1));
2984 let val2 = AbstractValue::from_constant(ConstantValue::Int(10));
2985
2986 let joined = join_values(&val1, &val2);
2987
2988 assert_eq!(joined.range_, Some((Some(1), Some(10))));
2990 }
2991
2992 #[test]
2993 fn test_join_values_loses_constant_on_disagreement() {
2994 let val1 = AbstractValue::from_constant(ConstantValue::Int(1));
2996 let val2 = AbstractValue::from_constant(ConstantValue::Int(10));
2997
2998 let joined = join_values(&val1, &val2);
2999
3000 assert!(
3001 joined.constant.is_none(),
3002 "Constant should be lost on disagreement"
3003 );
3004 }
3005
3006 #[test]
3007 fn test_join_values_preserves_constant_on_agreement() {
3008 let val1 = AbstractValue::from_constant(ConstantValue::Int(5));
3010 let val2 = AbstractValue::from_constant(ConstantValue::Int(5));
3011
3012 let joined = join_values(&val1, &val2);
3013
3014 assert_eq!(joined.constant, Some(ConstantValue::Int(5)));
3015 }
3016
3017 #[test]
3018 fn test_join_values_nullable_maybe_if_any_maybe() {
3019 let val1 = AbstractValue {
3021 type_: None,
3022 range_: None,
3023 nullable: Nullability::Never,
3024 constant: None,
3025 };
3026 let val2 = AbstractValue {
3027 type_: None,
3028 range_: None,
3029 nullable: Nullability::Maybe,
3030 constant: None,
3031 };
3032
3033 let joined = join_values(&val1, &val2);
3034
3035 assert_eq!(joined.nullable, Nullability::Maybe);
3036 }
3037
3038 #[test]
3039 fn test_join_values_nullable_never_if_both_never() {
3040 let val1 = AbstractValue::from_constant(ConstantValue::Int(1));
3042 let val2 = AbstractValue::from_constant(ConstantValue::Int(2));
3043
3044 let joined = join_values(&val1, &val2);
3045
3046 assert_eq!(joined.nullable, Nullability::Never);
3047 }
3048
3049 #[test]
3050 fn test_join_values_type_preserved_when_same() {
3051 let val1 = AbstractValue::from_constant(ConstantValue::Int(1));
3053 let val2 = AbstractValue::from_constant(ConstantValue::Int(2));
3054
3055 let joined = join_values(&val1, &val2);
3056
3057 assert_eq!(joined.type_, Some("int".to_string()));
3058 }
3059
3060 #[test]
3061 fn test_join_values_type_lost_when_different() {
3062 let val1 = AbstractValue::from_constant(ConstantValue::Int(1));
3064 let val2 = AbstractValue::from_constant(ConstantValue::String("hello".to_string()));
3065
3066 let joined = join_values(&val1, &val2);
3067
3068 assert_eq!(joined.type_, None);
3069 }
3070
3071 #[test]
3072 fn test_join_states_empty() {
3073 let states: Vec<&AbstractState> = vec![];
3075 let joined = join_states(&states);
3076
3077 assert!(joined.values.is_empty());
3078 }
3079
3080 #[test]
3081 fn test_join_states_single() {
3082 let state =
3084 AbstractState::new().set("x", AbstractValue::from_constant(ConstantValue::Int(5)));
3085 let states: Vec<&AbstractState> = vec![&state];
3086
3087 let joined = join_states(&states);
3088
3089 assert_eq!(joined.get("x").range_, Some((Some(5), Some(5))));
3090 }
3091
3092 #[test]
3093 fn test_join_states_multiple() {
3094 let state1 =
3096 AbstractState::new().set("x", AbstractValue::from_constant(ConstantValue::Int(1)));
3097 let state2 =
3098 AbstractState::new().set("x", AbstractValue::from_constant(ConstantValue::Int(10)));
3099 let states: Vec<&AbstractState> = vec![&state1, &state2];
3100
3101 let joined = join_states(&states);
3102
3103 assert_eq!(joined.get("x").range_, Some((Some(1), Some(10))));
3105 }
3106
3107 #[test]
3108 fn test_widen_value_upper_bound_to_infinity() {
3109 let old = AbstractValue {
3111 type_: Some("int".to_string()),
3112 range_: Some((Some(0), Some(5))),
3113 nullable: Nullability::Never,
3114 constant: None,
3115 };
3116 let new = AbstractValue {
3117 type_: Some("int".to_string()),
3118 range_: Some((Some(0), Some(10))), nullable: Nullability::Never,
3120 constant: None,
3121 };
3122
3123 let widened = widen_value(&old, &new);
3124
3125 assert_eq!(widened.range_, Some((Some(0), None)));
3127 }
3128
3129 #[test]
3130 fn test_widen_value_lower_bound_to_infinity() {
3131 let old = AbstractValue {
3133 type_: Some("int".to_string()),
3134 range_: Some((Some(-5), Some(10))),
3135 nullable: Nullability::Never,
3136 constant: None,
3137 };
3138 let new = AbstractValue {
3139 type_: Some("int".to_string()),
3140 range_: Some((Some(-10), Some(10))), nullable: Nullability::Never,
3142 constant: None,
3143 };
3144
3145 let widened = widen_value(&old, &new);
3146
3147 assert_eq!(widened.range_, Some((None, Some(10))));
3149 }
3150
3151 #[test]
3152 fn test_widen_value_loses_constant() {
3153 let old = AbstractValue::from_constant(ConstantValue::Int(5));
3155 let new = AbstractValue::from_constant(ConstantValue::Int(6));
3156
3157 let widened = widen_value(&old, &new);
3158
3159 assert!(widened.constant.is_none(), "Widening should lose constant");
3160 }
3161
3162 #[test]
3163 fn test_widen_value_stable_bounds_not_widened() {
3164 let old = AbstractValue {
3166 type_: Some("int".to_string()),
3167 range_: Some((Some(0), Some(10))),
3168 nullable: Nullability::Never,
3169 constant: None,
3170 };
3171 let new = AbstractValue {
3172 type_: Some("int".to_string()),
3173 range_: Some((Some(0), Some(10))), nullable: Nullability::Never,
3175 constant: None,
3176 };
3177
3178 let widened = widen_value(&old, &new);
3179
3180 assert_eq!(widened.range_, Some((Some(0), Some(10))));
3182 }
3183
3184 #[test]
3185 fn test_widen_state_applies_to_all_vars() {
3186 let old = AbstractState::new()
3188 .set("x", AbstractValue::from_constant(ConstantValue::Int(5)))
3189 .set("y", AbstractValue::from_constant(ConstantValue::Int(0)));
3190 let new = AbstractState::new()
3191 .set("x", AbstractValue::from_constant(ConstantValue::Int(10)))
3192 .set("y", AbstractValue::from_constant(ConstantValue::Int(0)));
3193
3194 let widened = widen_state(&old, &new);
3195
3196 assert_eq!(widened.get("x").range_, Some((Some(10), None)));
3201 assert_eq!(widened.get("y").range_, Some((Some(0), Some(0))));
3203 }
3204
3205 #[test]
3210 fn test_extract_rhs_simple_assignment() {
3211 let rhs = extract_rhs("x = a + b", "x");
3213 assert_eq!(rhs, Some("a + b".to_string()));
3214
3215 let rhs = extract_rhs("foo = 42", "foo");
3216 assert_eq!(rhs, Some("42".to_string()));
3217
3218 let rhs = extract_rhs("result = None", "result");
3219 assert_eq!(rhs, Some("None".to_string()));
3220 }
3221
3222 #[test]
3223 fn test_extract_rhs_augmented_assignment() {
3224 let rhs = extract_rhs("x += 5", "x");
3226 assert_eq!(rhs, Some("x + 5".to_string()));
3227
3228 let rhs = extract_rhs("y -= 3", "y");
3229 assert_eq!(rhs, Some("y - 3".to_string()));
3230
3231 let rhs = extract_rhs("count *= 2", "count");
3232 assert_eq!(rhs, Some("count * 2".to_string()));
3233 }
3234
3235 #[test]
3236 fn test_extract_rhs_with_spaces() {
3237 let rhs = extract_rhs("x=5", "x");
3239 assert_eq!(rhs, Some("5".to_string()));
3240
3241 let rhs = extract_rhs("x =5", "x");
3242 assert_eq!(rhs, Some("5".to_string()));
3243
3244 let rhs = extract_rhs("x= 5", "x");
3245 assert_eq!(rhs, Some("5".to_string()));
3246 }
3247
3248 #[test]
3249 fn test_extract_rhs_not_found() {
3250 let rhs = extract_rhs("y = 5", "x");
3252 assert_eq!(rhs, None);
3253
3254 let rhs = extract_rhs("xy = 5", "x");
3256 assert_eq!(rhs, None);
3257 }
3258
3259 #[test]
3260 fn test_strip_comment_python() {
3261 let stripped = strip_comment("x = 5 # this is a comment", "python");
3263 assert_eq!(stripped, "x = 5 ");
3264
3265 let stripped = strip_comment("x = 5", "python");
3266 assert_eq!(stripped, "x = 5");
3267 }
3268
3269 #[test]
3270 fn test_strip_comment_typescript() {
3271 let stripped = strip_comment("x = 5 // this is a comment", "typescript");
3272 assert_eq!(stripped, "x = 5 ");
3273
3274 let stripped = strip_comment("x = 5", "typescript");
3275 assert_eq!(stripped, "x = 5");
3276 }
3277
3278 #[test]
3279 fn test_strip_comment_preserves_string() {
3280 let stripped = strip_comment("x = \"hello # world\"", "python");
3282 assert_eq!(stripped, "x = \"hello # world\"");
3283
3284 let stripped = strip_comment("x = 'hello // world'", "typescript");
3285 assert_eq!(stripped, "x = 'hello // world'");
3286 }
3287
3288 #[test]
3289 fn test_strip_strings_blanks_path_separators() {
3290 let result = strip_strings("Path::new(\"src/main.rs\")", "rust");
3292 assert_eq!(result, "Path::new(\" \")");
3293 assert!(!result.contains('/'), "slashes inside strings must be blanked");
3294 }
3295
3296 #[test]
3297 fn test_strip_strings_preserves_code() {
3298 let result = strip_strings("let ratio = a / b;", "rust");
3300 assert_eq!(result, "let ratio = a / b;");
3301 }
3302
3303 #[test]
3304 fn test_strip_strings_handles_escapes() {
3305 let result = strip_strings(r#"let s = "path/to/\"file\""; a / b"#, "rust");
3307 assert!(result.contains("a / b"), "code division must survive");
3308 assert!(!result[8..25].contains('/'), "slashes in string must be blanked");
3310 }
3311
3312 #[test]
3313 fn test_strip_strings_single_quotes() {
3314 let result = strip_strings("let c = '/'; x / y", "rust");
3315 assert!(result.contains("x / y"), "code division must survive");
3316 assert_eq!(result.matches('/').count(), 1, "only code division remains");
3318 }
3319
3320 #[test]
3321 fn test_strip_strings_rust_raw_string() {
3322 let result = strip_strings(r##"let xml = r#"</coverage>"#;"##, "rust");
3324 assert!(!result.contains('/'), "slashes inside raw strings must be blanked");
3325 assert!(!result.contains("coverage"), "identifiers inside raw strings must be blanked");
3326 }
3327
3328 #[test]
3329 fn test_strip_strings_rust_raw_no_hashes() {
3330 let result = strip_strings(r#"let p = r"/src/main.rs"; a / b"#, "rust");
3332 assert!(result.contains("a / b"), "code division must survive");
3333 assert_eq!(result.matches('/').count(), 1, "only code division remains");
3335 }
3336
3337 #[test]
3338 fn test_strip_strings_rust_raw_double_hash() {
3339 let result = strip_strings(r###"let s = r##"a/b"##;"###, "rust");
3341 assert!(!result.contains("a/b"), "contents of r##\"...\"## must be blanked");
3342 }
3343
3344 #[test]
3345 fn test_parse_simple_arithmetic_var_plus_const() {
3346 let result = parse_simple_arithmetic("a + 1");
3348 assert_eq!(result, Some(("a".to_string(), '+', 1)));
3349
3350 let result = parse_simple_arithmetic("count - 5");
3351 assert_eq!(result, Some(("count".to_string(), '-', 5)));
3352
3353 let result = parse_simple_arithmetic("x * 2");
3354 assert_eq!(result, Some(("x".to_string(), '*', 2)));
3355 }
3356
3357 #[test]
3358 fn test_parse_simple_arithmetic_const_plus_var() {
3359 let result = parse_simple_arithmetic("1 + a");
3361 assert_eq!(result, Some(("a".to_string(), '+', 1)));
3362
3363 let result = parse_simple_arithmetic("2 * x");
3364 assert_eq!(result, Some(("x".to_string(), '*', 2)));
3365 }
3366
3367 #[test]
3368 fn test_parse_simple_arithmetic_negative_const() {
3369 let result = parse_simple_arithmetic("a + -5");
3371 assert_eq!(result, Some(("a".to_string(), '+', -5)));
3372 }
3373
3374 #[test]
3375 fn test_parse_simple_arithmetic_no_match() {
3376 let result = parse_simple_arithmetic("a + b"); assert_eq!(result, None);
3379
3380 let result = parse_simple_arithmetic("5"); assert_eq!(result, None);
3382
3383 let result = parse_simple_arithmetic("foo"); assert_eq!(result, None);
3385 }
3386
3387 #[test]
3388 fn test_is_identifier() {
3389 assert!(is_identifier("x"));
3390 assert!(is_identifier("foo"));
3391 assert!(is_identifier("_bar"));
3392 assert!(is_identifier("var123"));
3393 assert!(is_identifier("__init__"));
3394
3395 assert!(!is_identifier(""));
3396 assert!(!is_identifier("123var"));
3397 assert!(!is_identifier("foo.bar"));
3398 assert!(!is_identifier("foo bar"));
3399 assert!(!is_identifier("foo-bar"));
3400 }
3401
3402 #[test]
3403 fn test_parse_rhs_abstract_integer() {
3404 let state = AbstractState::new();
3406 let val = parse_rhs_abstract("x = 5", "x", &state, "python");
3407
3408 assert_eq!(val.range_, Some((Some(5), Some(5))));
3409 assert_eq!(val.constant, Some(ConstantValue::Int(5)));
3410 assert_eq!(val.type_, Some("int".to_string()));
3411 }
3412
3413 #[test]
3414 fn test_parse_rhs_abstract_negative_integer() {
3415 let state = AbstractState::new();
3416 let val = parse_rhs_abstract("x = -42", "x", &state, "python");
3417
3418 assert_eq!(val.range_, Some((Some(-42), Some(-42))));
3419 assert_eq!(val.constant, Some(ConstantValue::Int(-42)));
3420 }
3421
3422 #[test]
3423 fn test_parse_rhs_abstract_float() {
3424 let state = AbstractState::new();
3426 let val = parse_rhs_abstract("x = 3.14", "x", &state, "python");
3427
3428 assert_eq!(val.type_, Some("float".to_string()));
3429 if let Some(ConstantValue::Float(f)) = val.constant {
3430 assert_eq!(f, 3.14);
3431 } else {
3432 panic!("Expected float constant");
3433 }
3434 }
3435
3436 #[test]
3437 fn test_parse_rhs_abstract_string_double_quotes() {
3438 let state = AbstractState::new();
3440 let val = parse_rhs_abstract("x = \"hello\"", "x", &state, "python");
3441
3442 assert_eq!(val.type_, Some("str".to_string()));
3443 assert_eq!(
3444 val.constant,
3445 Some(ConstantValue::String("hello".to_string()))
3446 );
3447 assert_eq!(val.range_, Some((Some(5), Some(5))));
3449 }
3450
3451 #[test]
3452 fn test_parse_rhs_abstract_string_single_quotes() {
3453 let state = AbstractState::new();
3455 let val = parse_rhs_abstract("x = 'world'", "x", &state, "python");
3456
3457 assert_eq!(val.type_, Some("str".to_string()));
3458 assert_eq!(
3459 val.constant,
3460 Some(ConstantValue::String("world".to_string()))
3461 );
3462 }
3463
3464 #[test]
3465 fn test_parse_rhs_abstract_python_none() {
3466 let state = AbstractState::new();
3468 let val = parse_rhs_abstract("x = None", "x", &state, "python");
3469
3470 assert_eq!(val.nullable, Nullability::Always);
3471 assert_eq!(val.type_, Some("NoneType".to_string()));
3472 }
3473
3474 #[test]
3475 fn test_parse_rhs_abstract_typescript_null() {
3476 let state = AbstractState::new();
3478 let val = parse_rhs_abstract("x = null", "x", &state, "typescript");
3479
3480 assert_eq!(val.nullable, Nullability::Always);
3481 }
3482
3483 #[test]
3484 fn test_parse_rhs_abstract_typescript_undefined() {
3485 let state = AbstractState::new();
3487 let val = parse_rhs_abstract("x = undefined", "x", &state, "typescript");
3488
3489 assert_eq!(val.nullable, Nullability::Always);
3490 assert_eq!(val.type_, Some("undefined".to_string()));
3491 }
3492
3493 #[test]
3494 fn test_parse_rhs_abstract_go_nil() {
3495 let state = AbstractState::new();
3497 let val = parse_rhs_abstract("x = nil", "x", &state, "go");
3498
3499 assert_eq!(val.nullable, Nullability::Always);
3500 }
3501
3502 #[test]
3503 fn test_parse_rhs_abstract_python_bool() {
3504 let state = AbstractState::new();
3506 let val = parse_rhs_abstract("x = True", "x", &state, "python");
3507
3508 assert_eq!(val.type_, Some("bool".to_string()));
3509 assert_eq!(val.constant, Some(ConstantValue::Bool(true)));
3510
3511 let val = parse_rhs_abstract("y = False", "y", &state, "python");
3512 assert_eq!(val.constant, Some(ConstantValue::Bool(false)));
3513 }
3514
3515 #[test]
3516 fn test_parse_rhs_abstract_typescript_bool() {
3517 let state = AbstractState::new();
3519 let val = parse_rhs_abstract("x = true", "x", &state, "typescript");
3520
3521 assert_eq!(val.type_, Some("bool".to_string()));
3522 assert_eq!(val.constant, Some(ConstantValue::Bool(true)));
3523 }
3524
3525 #[test]
3526 fn test_parse_rhs_abstract_variable_copy() {
3527 let state =
3529 AbstractState::new().set("a", AbstractValue::from_constant(ConstantValue::Int(42)));
3530
3531 let val = parse_rhs_abstract("x = a", "x", &state, "python");
3532
3533 assert_eq!(val.range_, Some((Some(42), Some(42))));
3534 assert_eq!(val.constant, Some(ConstantValue::Int(42)));
3535 }
3536
3537 #[test]
3538 fn test_parse_rhs_abstract_simple_arithmetic() {
3539 let state =
3541 AbstractState::new().set("a", AbstractValue::from_constant(ConstantValue::Int(5)));
3542
3543 let val = parse_rhs_abstract("x = a + 3", "x", &state, "python");
3544
3545 assert_eq!(val.range_, Some((Some(8), Some(8))));
3546 assert_eq!(val.constant, Some(ConstantValue::Int(8)));
3547 }
3548
3549 #[test]
3550 fn test_parse_rhs_abstract_augmented_assignment() {
3551 let state =
3553 AbstractState::new().set("x", AbstractValue::from_constant(ConstantValue::Int(10)));
3554
3555 let val = parse_rhs_abstract("x += 5", "x", &state, "python");
3556
3557 assert_eq!(val.range_, Some((Some(15), Some(15))));
3558 assert_eq!(val.constant, Some(ConstantValue::Int(15)));
3559 }
3560
3561 #[test]
3562 fn test_parse_rhs_abstract_with_comment() {
3563 let state = AbstractState::new();
3565 let val = parse_rhs_abstract("x = 5 # this is the value", "x", &state, "python");
3566
3567 assert_eq!(val.range_, Some((Some(5), Some(5))));
3568 }
3569
3570 #[test]
3571 fn test_parse_rhs_abstract_unknown_returns_top() {
3572 let state = AbstractState::new();
3574 let val = parse_rhs_abstract("x = foo(a, b)", "x", &state, "python");
3575
3576 assert_eq!(val.type_, None);
3578 assert_eq!(val.range_, None);
3579 assert_eq!(val.nullable, Nullability::Maybe);
3580 }
3581
3582 #[test]
3583 fn test_parse_rhs_abstract_no_assignment() {
3584 let state = AbstractState::new();
3586 let val = parse_rhs_abstract("y = 5", "x", &state, "python");
3587
3588 assert_eq!(val.type_, None);
3590 assert_eq!(val.range_, None);
3591 }
3592
3593 use crate::types::{
3598 BlockType, CfgBlock, CfgEdge, CfgInfo, DfgInfo, EdgeType, VarRef,
3599 };
3600
3601 fn make_test_cfg(function: &str, blocks: Vec<CfgBlock>, edges: Vec<CfgEdge>) -> CfgInfo {
3603 CfgInfo {
3604 function: function.to_string(),
3605 blocks,
3606 edges,
3607 entry_block: 0,
3608 exit_blocks: vec![0], cyclomatic_complexity: 1,
3610 nested_functions: HashMap::new(),
3611 }
3612 }
3613
3614 fn make_var_ref(name: &str, ref_type: RefType, line: u32, column: u32) -> VarRef {
3616 VarRef {
3617 name: name.to_string(),
3618 ref_type,
3619 line,
3620 column,
3621 context: None,
3622 group_id: None,
3623 }
3624 }
3625
3626 #[test]
3627 fn test_compute_abstract_interp_returns_info() {
3628 let cfg = make_test_cfg(
3630 "test_func",
3631 vec![CfgBlock {
3632 id: 0,
3633 block_type: BlockType::Entry,
3634 lines: (1, 1),
3635 calls: vec![],
3636 }],
3637 vec![],
3638 );
3639 let dfg = DfgInfo {
3640 function: "test_func".to_string(),
3641 refs: vec![],
3642 edges: vec![],
3643 variables: vec![],
3644 };
3645
3646 let result = compute_abstract_interp(&cfg, &dfg, None, "python").unwrap();
3647 assert_eq!(result.function_name, "test_func");
3648 }
3649
3650 #[test]
3651 fn test_compute_tracks_constant_assignment() {
3652 let cfg = make_test_cfg(
3654 "const_test",
3655 vec![CfgBlock {
3656 id: 0,
3657 block_type: BlockType::Entry,
3658 lines: (1, 1),
3659 calls: vec![],
3660 }],
3661 vec![],
3662 );
3663 let dfg = DfgInfo {
3664 function: "const_test".to_string(),
3665 refs: vec![make_var_ref("x", RefType::Definition, 1, 0)],
3666 edges: vec![],
3667 variables: vec!["x".to_string()],
3668 };
3669 let source = ["x = 5"];
3670 let source_refs: Vec<&str> = source.to_vec();
3671
3672 let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "python").unwrap();
3673 let val = result.value_at_exit(0, "x");
3674 assert_eq!(val.range_, Some((Some(5), Some(5))));
3675 }
3676
3677 #[test]
3678 fn test_compute_tracks_variable_copy() {
3679 let cfg = make_test_cfg(
3683 "copy_test",
3684 vec![CfgBlock {
3685 id: 0,
3686 block_type: BlockType::Entry,
3687 lines: (1, 2),
3688 calls: vec![],
3689 }],
3690 vec![],
3691 );
3692 let dfg = DfgInfo {
3693 function: "copy_test".to_string(),
3694 refs: vec![
3695 make_var_ref("x", RefType::Definition, 1, 0),
3696 make_var_ref("x", RefType::Use, 2, 4),
3697 make_var_ref("y", RefType::Definition, 2, 0),
3698 ],
3699 edges: vec![],
3700 variables: vec!["x".to_string(), "y".to_string()],
3701 };
3702 let source = ["x = 5", "y = x"];
3703 let source_refs: Vec<&str> = source.to_vec();
3704
3705 let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "python").unwrap();
3706 let val_x = result.value_at_exit(0, "x");
3707 let val_y = result.value_at_exit(0, "y");
3708 assert_eq!(val_x.range_, val_y.range_);
3709 }
3710
3711 #[test]
3712 fn test_compute_tracks_none_assignment() {
3713 let cfg = make_test_cfg(
3715 "none_test",
3716 vec![CfgBlock {
3717 id: 0,
3718 block_type: BlockType::Entry,
3719 lines: (1, 1),
3720 calls: vec![],
3721 }],
3722 vec![],
3723 );
3724 let dfg = DfgInfo {
3725 function: "none_test".to_string(),
3726 refs: vec![make_var_ref("x", RefType::Definition, 1, 0)],
3727 edges: vec![],
3728 variables: vec!["x".to_string()],
3729 };
3730 let source = ["x = None"];
3731 let source_refs: Vec<&str> = source.to_vec();
3732
3733 let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "python").unwrap();
3734 let val = result.value_at_exit(0, "x");
3735 assert_eq!(val.nullable, Nullability::Always);
3736 }
3737
3738 #[test]
3739 fn test_abstract_interp_empty_function_no_crash() {
3740 let cfg = make_test_cfg(
3742 "empty_func",
3743 vec![CfgBlock {
3744 id: 0,
3745 block_type: BlockType::Entry,
3746 lines: (1, 1),
3747 calls: vec![],
3748 }],
3749 vec![],
3750 );
3751 let dfg = DfgInfo {
3752 function: "empty_func".to_string(),
3753 refs: vec![],
3754 edges: vec![],
3755 variables: vec![],
3756 };
3757
3758 let result = compute_abstract_interp(&cfg, &dfg, None, "python");
3759 assert!(result.is_ok());
3760 }
3761
3762 #[test]
3763 fn test_unknown_rhs_defaults_to_top() {
3764 let cfg = make_test_cfg(
3767 "unknown_test",
3768 vec![CfgBlock {
3769 id: 0,
3770 block_type: BlockType::Entry,
3771 lines: (1, 1),
3772 calls: vec![],
3773 }],
3774 vec![],
3775 );
3776 let dfg = DfgInfo {
3777 function: "unknown_test".to_string(),
3778 refs: vec![make_var_ref("x", RefType::Definition, 1, 0)],
3779 edges: vec![],
3780 variables: vec!["x".to_string()],
3781 };
3782 let source = ["x = some_function()"];
3783 let source_refs: Vec<&str> = source.to_vec();
3784
3785 let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "python").unwrap();
3786 let val = result.value_at_exit(0, "x");
3787 assert_eq!(val.type_, None);
3789 assert_eq!(val.range_, None);
3790 assert_eq!(val.nullable, Nullability::Maybe);
3791 }
3792
3793 #[test]
3794 fn test_parameter_starts_as_top() {
3795 let cfg = make_test_cfg(
3797 "param_test",
3798 vec![CfgBlock {
3799 id: 0,
3800 block_type: BlockType::Entry,
3801 lines: (1, 1),
3802 calls: vec![],
3803 }],
3804 vec![],
3805 );
3806 let dfg = DfgInfo {
3807 function: "param_test".to_string(),
3808 refs: vec![make_var_ref("param", RefType::Definition, 1, 0)],
3809 edges: vec![],
3810 variables: vec!["param".to_string()],
3811 };
3812 let result = compute_abstract_interp(&cfg, &dfg, None, "python").unwrap();
3814 let val = result.value_at(0, "param");
3815 assert_eq!(val.type_, None);
3817 assert_eq!(val.range_, None);
3818 assert_eq!(val.nullable, Nullability::Maybe);
3819 }
3820
3821 #[test]
3822 fn test_nested_loops_terminate() {
3823 let cfg = CfgInfo {
3826 function: "nested_loop".to_string(),
3827 blocks: vec![
3828 CfgBlock {
3829 id: 0,
3830 block_type: BlockType::Entry,
3831 lines: (1, 1),
3832 calls: vec![],
3833 },
3834 CfgBlock {
3835 id: 1,
3836 block_type: BlockType::LoopHeader,
3837 lines: (2, 2),
3838 calls: vec![],
3839 },
3840 CfgBlock {
3841 id: 2,
3842 block_type: BlockType::LoopHeader,
3843 lines: (3, 3),
3844 calls: vec![],
3845 },
3846 CfgBlock {
3847 id: 3,
3848 block_type: BlockType::LoopBody,
3849 lines: (4, 4),
3850 calls: vec![],
3851 },
3852 CfgBlock {
3853 id: 4,
3854 block_type: BlockType::Exit,
3855 lines: (5, 5),
3856 calls: vec![],
3857 },
3858 ],
3859 edges: vec![
3860 CfgEdge {
3861 from: 0,
3862 to: 1,
3863 edge_type: EdgeType::Unconditional,
3864 condition: None,
3865 },
3866 CfgEdge {
3867 from: 1,
3868 to: 2,
3869 edge_type: EdgeType::True,
3870 condition: Some("i < n".to_string()),
3871 },
3872 CfgEdge {
3873 from: 1,
3874 to: 4,
3875 edge_type: EdgeType::False,
3876 condition: None,
3877 },
3878 CfgEdge {
3879 from: 2,
3880 to: 3,
3881 edge_type: EdgeType::True,
3882 condition: Some("j < m".to_string()),
3883 },
3884 CfgEdge {
3885 from: 2,
3886 to: 1,
3887 edge_type: EdgeType::False,
3888 condition: None,
3889 },
3890 CfgEdge {
3891 from: 3,
3892 to: 2,
3893 edge_type: EdgeType::BackEdge,
3894 condition: None,
3895 },
3896 ],
3897 entry_block: 0,
3898 exit_blocks: vec![4],
3899 cyclomatic_complexity: 3,
3900 nested_functions: HashMap::new(),
3901 };
3902 let dfg = DfgInfo {
3903 function: "nested_loop".to_string(),
3904 refs: vec![
3905 make_var_ref("x", RefType::Definition, 4, 0),
3906 make_var_ref("x", RefType::Use, 4, 4),
3907 ],
3908 edges: vec![],
3909 variables: vec!["x".to_string()],
3910 };
3911 let source = ["x = 0",
3912 "for i in range(n):",
3913 " for j in range(m):",
3914 " x = x + 1",
3915 "return x"];
3916 let source_refs: Vec<&str> = source.to_vec();
3917
3918 let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "python");
3920 assert!(result.is_ok());
3921 }
3922
3923 #[test]
3924 fn test_compute_accepts_language_parameter() {
3925 let cfg = make_test_cfg(
3927 "lang_test",
3928 vec![CfgBlock {
3929 id: 0,
3930 block_type: BlockType::Entry,
3931 lines: (1, 1),
3932 calls: vec![],
3933 }],
3934 vec![],
3935 );
3936 let dfg = DfgInfo {
3937 function: "lang_test".to_string(),
3938 refs: vec![],
3939 edges: vec![],
3940 variables: vec![],
3941 };
3942
3943 let result_py = compute_abstract_interp(&cfg, &dfg, None, "python");
3945 let result_ts = compute_abstract_interp(&cfg, &dfg, None, "typescript");
3946 assert!(result_py.is_ok());
3947 assert!(result_ts.is_ok());
3948 }
3949
3950 #[test]
3951 fn test_compute_with_typescript_null() {
3952 let cfg = make_test_cfg(
3955 "ts_null_test",
3956 vec![CfgBlock {
3957 id: 0,
3958 block_type: BlockType::Entry,
3959 lines: (1, 1),
3960 calls: vec![],
3961 }],
3962 vec![],
3963 );
3964 let dfg = DfgInfo {
3965 function: "ts_null_test".to_string(),
3966 refs: vec![make_var_ref("x", RefType::Definition, 1, 0)],
3967 edges: vec![],
3968 variables: vec!["x".to_string()],
3969 };
3970 let source = ["let x = null"];
3971 let source_refs: Vec<&str> = source.to_vec();
3972
3973 let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "typescript").unwrap();
3974 let val = result.value_at_exit(0, "x");
3975 assert_eq!(val.nullable, Nullability::Always);
3976 }
3977
3978 #[test]
3979 fn test_compute_with_go_nil() {
3980 let cfg = make_test_cfg(
3983 "go_nil_test",
3984 vec![CfgBlock {
3985 id: 0,
3986 block_type: BlockType::Entry,
3987 lines: (1, 1),
3988 calls: vec![],
3989 }],
3990 vec![],
3991 );
3992 let dfg = DfgInfo {
3993 function: "go_nil_test".to_string(),
3994 refs: vec![make_var_ref("x", RefType::Definition, 1, 0)],
3995 edges: vec![],
3996 variables: vec!["x".to_string()],
3997 };
3998 let source = ["x := nil"];
3999 let source_refs: Vec<&str> = source.to_vec();
4000
4001 let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "go").unwrap();
4002 let val = result.value_at_exit(0, "x");
4003 assert_eq!(val.nullable, Nullability::Always);
4004 }
4005
4006 #[test]
4011 fn test_div_zero_detected_for_constant_zero() {
4012 let cfg = make_test_cfg(
4014 "div_zero_const",
4015 vec![CfgBlock {
4016 id: 0,
4017 block_type: BlockType::Entry,
4018 lines: (1, 2),
4019 calls: vec![],
4020 }],
4021 vec![],
4022 );
4023 let dfg = DfgInfo {
4024 function: "div_zero_const".to_string(),
4025 refs: vec![
4026 make_var_ref("x", RefType::Definition, 1, 0),
4027 make_var_ref("x", RefType::Use, 2, 6),
4028 make_var_ref("y", RefType::Definition, 2, 0),
4029 ],
4030 edges: vec![],
4031 variables: vec!["x".to_string(), "y".to_string()],
4032 };
4033 let source = ["x = 0", "y = 1 / x"];
4034 let source_refs: Vec<&str> = source.to_vec();
4035
4036 let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "python").unwrap();
4037
4038 assert!(
4040 !result.potential_div_zero.is_empty(),
4041 "Should detect division by zero"
4042 );
4043 assert!(
4044 result
4045 .potential_div_zero
4046 .iter()
4047 .any(|(line, var)| *line == 2 && var == "x"),
4048 "Should flag x at line 2 as potential div-by-zero"
4049 );
4050 }
4051
4052 #[test]
4053 fn test_div_zero_detected_for_range_including_zero() {
4054 let cfg = make_test_cfg(
4056 "div_zero_range",
4057 vec![CfgBlock {
4058 id: 0,
4059 block_type: BlockType::Entry,
4060 lines: (1, 2),
4061 calls: vec![],
4062 }],
4063 vec![],
4064 );
4065 let dfg = DfgInfo {
4066 function: "div_zero_range".to_string(),
4067 refs: vec![
4068 make_var_ref("x", RefType::Definition, 1, 0), make_var_ref("x", RefType::Use, 2, 6),
4070 make_var_ref("y", RefType::Definition, 2, 0),
4071 ],
4072 edges: vec![],
4073 variables: vec!["x".to_string(), "y".to_string()],
4074 };
4075 let source = ["x = foo()", "y = 1 / x"];
4077 let source_refs: Vec<&str> = source.to_vec();
4078
4079 let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "python").unwrap();
4080
4081 assert!(
4083 !result.potential_div_zero.is_empty(),
4084 "Should detect potential division by zero for unknown value"
4085 );
4086 }
4087
4088 #[test]
4089 fn test_div_safe_no_warning_for_constant_nonzero() {
4090 let cfg = make_test_cfg(
4092 "div_safe_const",
4093 vec![CfgBlock {
4094 id: 0,
4095 block_type: BlockType::Entry,
4096 lines: (1, 2),
4097 calls: vec![],
4098 }],
4099 vec![],
4100 );
4101 let dfg = DfgInfo {
4102 function: "div_safe_const".to_string(),
4103 refs: vec![
4104 make_var_ref("x", RefType::Definition, 1, 0),
4105 make_var_ref("x", RefType::Use, 2, 6),
4106 make_var_ref("y", RefType::Definition, 2, 0),
4107 ],
4108 edges: vec![],
4109 variables: vec!["x".to_string(), "y".to_string()],
4110 };
4111 let source = ["x = 5", "y = 1 / x"];
4112 let source_refs: Vec<&str> = source.to_vec();
4113
4114 let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "python").unwrap();
4115
4116 assert!(
4118 result.potential_div_zero.is_empty()
4119 || !result
4120 .potential_div_zero
4121 .iter()
4122 .any(|(line, var)| *line == 2 && var == "x"),
4123 "Should NOT warn for division by constant non-zero"
4124 );
4125 }
4126
4127 #[test]
4128 fn test_div_safe_no_warning_for_positive_range() {
4129 let cfg = make_test_cfg(
4131 "div_safe_range",
4132 vec![CfgBlock {
4133 id: 0,
4134 block_type: BlockType::Entry,
4135 lines: (1, 3),
4136 calls: vec![],
4137 }],
4138 vec![],
4139 );
4140 let dfg = DfgInfo {
4141 function: "div_safe_range".to_string(),
4142 refs: vec![
4143 make_var_ref("x", RefType::Definition, 1, 0),
4144 make_var_ref("x", RefType::Use, 2, 4),
4145 make_var_ref("x", RefType::Definition, 2, 0),
4146 make_var_ref("x", RefType::Use, 3, 6),
4147 make_var_ref("y", RefType::Definition, 3, 0),
4148 ],
4149 edges: vec![],
4150 variables: vec!["x".to_string(), "y".to_string()],
4151 };
4152 let source = ["x = 5", "x = x + 1", "y = 1 / x"];
4153 let source_refs: Vec<&str> = source.to_vec();
4154
4155 let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "python").unwrap();
4156
4157 assert!(
4159 result.potential_div_zero.is_empty()
4160 || !result
4161 .potential_div_zero
4162 .iter()
4163 .any(|(line, var)| *line == 3 && var == "x"),
4164 "Should NOT warn for positive range that excludes zero"
4165 );
4166 }
4167
4168 #[test]
4169 fn test_div_zero_intra_block_accuracy() {
4170 let cfg = make_test_cfg(
4175 "div_intra_block",
4176 vec![CfgBlock {
4177 id: 0,
4178 block_type: BlockType::Entry,
4179 lines: (1, 3),
4180 calls: vec![],
4181 }],
4182 vec![],
4183 );
4184 let dfg = DfgInfo {
4185 function: "div_intra_block".to_string(),
4186 refs: vec![
4187 make_var_ref("x", RefType::Definition, 1, 0),
4188 make_var_ref("x", RefType::Definition, 2, 0), make_var_ref("x", RefType::Use, 3, 6),
4190 make_var_ref("y", RefType::Definition, 3, 0),
4191 ],
4192 edges: vec![],
4193 variables: vec!["x".to_string(), "y".to_string()],
4194 };
4195 let source = ["x = 0", "x = 5", "y = 1 / x"];
4196 let source_refs: Vec<&str> = source.to_vec();
4197
4198 let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "python").unwrap();
4199
4200 assert!(result.potential_div_zero.is_empty() ||
4203 !result.potential_div_zero.iter().any(|(line, var)| *line == 3 && var == "x"),
4204 "Should NOT warn when divisor is redefined to non-zero before division (intra-block precision)");
4205 }
4206
4207 #[test]
4208 fn test_div_zero_not_triggered_by_path_strings() {
4209 let cfg = make_test_cfg(
4212 "path_strings",
4213 vec![CfgBlock {
4214 id: 0,
4215 block_type: BlockType::Entry,
4216 lines: (1, 2),
4217 calls: vec![],
4218 }],
4219 vec![],
4220 );
4221 let dfg = DfgInfo {
4222 function: "path_strings".to_string(),
4223 refs: vec![
4224 make_var_ref("root", RefType::Definition, 1, 0),
4225 make_var_ref("child", RefType::Definition, 2, 0),
4226 ],
4227 edges: vec![],
4228 variables: vec!["root".to_string(), "child".to_string()],
4229 };
4230 let source = ["root = \"/projects/myapp\"",
4231 "child = \"/src/main.rs\""];
4232 let source_refs: Vec<&str> = source.to_vec();
4233
4234 let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "python").unwrap();
4235
4236 assert!(
4237 result.potential_div_zero.is_empty(),
4238 "Path separators inside string literals must not trigger div-by-zero; got: {:?}",
4239 result.potential_div_zero
4240 );
4241 }
4242
4243 #[test]
4244 fn test_div_zero_still_detects_real_division_with_strings() {
4245 let cfg = make_test_cfg(
4247 "mixed_strings_div",
4248 vec![CfgBlock {
4249 id: 0,
4250 block_type: BlockType::Entry,
4251 lines: (1, 3),
4252 calls: vec![],
4253 }],
4254 vec![],
4255 );
4256 let dfg = DfgInfo {
4257 function: "mixed_strings_div".to_string(),
4258 refs: vec![
4259 make_var_ref("path", RefType::Definition, 1, 0),
4260 make_var_ref("x", RefType::Definition, 2, 0),
4261 make_var_ref("y", RefType::Definition, 3, 0),
4262 make_var_ref("x", RefType::Use, 3, 10),
4263 ],
4264 edges: vec![],
4265 variables: vec!["path".to_string(), "x".to_string(), "y".to_string()],
4266 };
4267 let source = ["path = \"/src/main.rs\"",
4268 "x = foo()",
4269 "y = 100 / x"];
4270 let source_refs: Vec<&str> = source.to_vec();
4271
4272 let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "python").unwrap();
4273
4274 assert!(
4275 result.potential_div_zero.iter().any(|(line, var)| *line == 3 && var == "x"),
4276 "Real division by unknown x should still be flagged; got: {:?}",
4277 result.potential_div_zero
4278 );
4279 assert!(
4281 !result.potential_div_zero.iter().any(|(_, var)| var == "main" || var == "src"),
4282 "Path components in strings must not be flagged; got: {:?}",
4283 result.potential_div_zero
4284 );
4285 }
4286
4287 #[test]
4288 fn test_null_deref_detected_at_attribute_access() {
4289 let cfg = make_test_cfg(
4291 "null_deref",
4292 vec![CfgBlock {
4293 id: 0,
4294 block_type: BlockType::Entry,
4295 lines: (1, 2),
4296 calls: vec![],
4297 }],
4298 vec![],
4299 );
4300 let dfg = DfgInfo {
4301 function: "null_deref".to_string(),
4302 refs: vec![
4303 make_var_ref("x", RefType::Definition, 1, 0),
4304 make_var_ref("x", RefType::Use, 2, 4),
4305 make_var_ref("y", RefType::Definition, 2, 0),
4306 ],
4307 edges: vec![],
4308 variables: vec!["x".to_string(), "y".to_string()],
4309 };
4310 let source = ["x = None", "y = x.foo"];
4311 let source_refs: Vec<&str> = source.to_vec();
4312
4313 let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "python").unwrap();
4314
4315 assert!(
4317 !result.potential_null_deref.is_empty(),
4318 "Should detect null dereference"
4319 );
4320 assert!(
4321 result
4322 .potential_null_deref
4323 .iter()
4324 .any(|(line, var)| *line == 2 && var == "x"),
4325 "Should flag x at line 2 as potential null deref"
4326 );
4327 }
4328
4329 #[test]
4330 fn test_null_deref_safe_for_non_null_constant() {
4331 let cfg = make_test_cfg(
4333 "null_safe",
4334 vec![CfgBlock {
4335 id: 0,
4336 block_type: BlockType::Entry,
4337 lines: (1, 2),
4338 calls: vec![],
4339 }],
4340 vec![],
4341 );
4342 let dfg = DfgInfo {
4343 function: "null_safe".to_string(),
4344 refs: vec![
4345 make_var_ref("x", RefType::Definition, 1, 0),
4346 make_var_ref("x", RefType::Use, 2, 4),
4347 make_var_ref("y", RefType::Definition, 2, 0),
4348 ],
4349 edges: vec![],
4350 variables: vec!["x".to_string(), "y".to_string()],
4351 };
4352 let source = ["x = 'hello'", "y = x.upper()"];
4353 let source_refs: Vec<&str> = source.to_vec();
4354
4355 let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "python").unwrap();
4356
4357 assert!(
4359 result.potential_null_deref.is_empty()
4360 || !result
4361 .potential_null_deref
4362 .iter()
4363 .any(|(line, var)| *line == 2 && var == "x"),
4364 "Should NOT warn for dereference of non-null constant"
4365 );
4366 }
4367}