agentshield/analysis/interprocedural/
python.rs1use std::path::Path;
2
3use once_cell::sync::Lazy;
4use regex::Regex;
5
6use crate::ir::{ScanTarget, SourceLocation};
7
8use super::types::{CALL_EXPR_RE, CallGraph, CallSite, FunctionNode};
9
10pub(crate) static PY_FUNC_DEF_RE: Lazy<Regex> = Lazy::new(|| {
11 Regex::new(r#"(?m)^\s*(?:async\s+)?def\s+([A-Za-z0-9_]+)\s*\(([^)]*)\)"#).expect("valid regex")
12});
13
14pub(crate) fn parse_python_file(
15 graph: &mut CallGraph,
16 file_path: &Path,
17 content: &str,
18 target: &ScanTarget,
19) {
20 let lines: Vec<&str> = content.lines().collect();
21
22 for (line_idx, line) in lines.iter().enumerate() {
24 let line_num = line_idx + 1;
25 if let Some(cap) = PY_FUNC_DEF_RE.captures(line) {
26 let func_name = cap[1].to_string();
27 let raw_params = &cap[2];
28
29 let params: Vec<String> = raw_params
30 .split(',')
31 .map(|p| {
32 p.split(':')
33 .next()
34 .unwrap_or("")
35 .split('=')
36 .next()
37 .unwrap_or("")
38 .trim()
39 .to_string()
40 })
41 .filter(|p| !p.is_empty() && p != "self" && p != "cls")
42 .collect();
43
44 let start_line = line_num;
45 let func_indent = line.len() - line.trim_start().len();
46 let mut end_line = start_line;
47
48 for (sub_idx, next_line) in lines.iter().enumerate().skip(start_line) {
49 let next_trimmed = next_line.trim();
50 if next_trimmed.is_empty() || next_trimmed.starts_with('#') {
51 continue;
52 }
53 let next_indent = next_line.len() - next_line.trim_start().len();
54 if next_indent <= func_indent
55 || next_trimmed.starts_with('@')
56 || next_trimmed.starts_with("def ")
57 || next_trimmed.starts_with("async def ")
58 || next_trimmed.starts_with("class ")
59 {
60 break;
61 }
62 end_line = sub_idx + 1;
63 }
64
65 let func_sinks = target
66 .data
67 .sinks
68 .iter()
69 .filter(|s| {
70 s.location.file == file_path
71 && s.location.line >= start_line
72 && s.location.line <= end_line
73 })
74 .cloned()
75 .collect();
76
77 let node = FunctionNode {
78 name: func_name.clone(),
79 file_path: file_path.to_path_buf(),
80 params,
81 start_line,
82 end_line,
83 location: SourceLocation {
84 file: file_path.to_path_buf(),
85 line: start_line,
86 column: func_indent,
87 end_line: Some(end_line),
88 end_column: None,
89 },
90 sinks: func_sinks,
91 };
92
93 graph.functions.entry(func_name).or_default().push(node);
94 }
95 }
96
97 for (line_idx, line) in lines.iter().enumerate() {
99 let line_num = line_idx + 1;
100 let trimmed = line.trim();
101 if trimmed.starts_with('#')
102 || trimmed.starts_with("def ")
103 || trimmed.starts_with("async def ")
104 {
105 continue;
106 }
107
108 for cap in CALL_EXPR_RE.captures_iter(line) {
109 let callee_name = cap[1].to_string();
110 let raw_args = &cap[2];
111
112 let caller_name = graph.find_enclosing_function(file_path, line_num);
114
115 let args: Vec<String> = raw_args
116 .split(',')
117 .map(|a| a.split('=').next().unwrap_or("").trim().to_string())
118 .filter(|a| !a.is_empty())
119 .collect();
120
121 graph.call_sites.push(CallSite {
122 caller_name,
123 callee_name,
124 file_path: file_path.to_path_buf(),
125 line_number: line_num,
126 args,
127 location: SourceLocation {
128 file: file_path.to_path_buf(),
129 line: line_num,
130 column: line.find(&cap[0]).unwrap_or(0),
131 end_line: None,
132 end_column: None,
133 },
134 });
135 }
136 }
137}