agentshield/analysis/interprocedural/
typescript.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 TS_FUNC_DEF_RE: Lazy<Regex> = Lazy::new(|| {
11 Regex::new(r#"(?m)^\s*(?:export\s+)?(?:async\s+)?function\s+([A-Za-z0-9_]+)\s*\(([^)]*)\)|(?:const|let|var)\s+([A-Za-z0-9_]+)\s*=\s*(?:async\s*)?\(([^)]*)\)(?:\s*:[^=]+)?\s*=>"#)
12 .expect("valid regex")
13});
14
15pub(crate) fn parse_typescript_file(
16 graph: &mut CallGraph,
17 file_path: &Path,
18 content: &str,
19 target: &ScanTarget,
20) {
21 let lines: Vec<&str> = content.lines().collect();
22
23 for (line_idx, line) in lines.iter().enumerate() {
25 let line_num = line_idx + 1;
26 if let Some(cap) = TS_FUNC_DEF_RE.captures(line) {
27 let func_name = cap
28 .get(1)
29 .or_else(|| cap.get(3))
30 .map(|m| m.as_str().to_string())
31 .unwrap_or_default();
32
33 if func_name.is_empty() {
34 continue;
35 }
36
37 let raw_params = cap
38 .get(2)
39 .or_else(|| cap.get(4))
40 .map(|m| m.as_str())
41 .unwrap_or("");
42
43 let params: Vec<String> = raw_params
44 .split(',')
45 .map(|p| {
46 p.split(':')
47 .next()
48 .unwrap_or("")
49 .split('=')
50 .next()
51 .unwrap_or("")
52 .trim()
53 .to_string()
54 })
55 .filter(|p| !p.is_empty())
56 .collect();
57
58 let start_line = line_num;
59 let mut end_line = start_line;
60 let mut brace_count: i32 = 0;
61 let mut seen_open_brace = false;
62 for (sub_idx, next_line) in lines.iter().enumerate().skip(start_line.saturating_sub(1))
63 {
64 let next_trimmed = next_line.trim();
65 if next_trimmed.is_empty() || next_trimmed.starts_with("//") {
66 continue;
67 }
68 let opens = next_line.chars().filter(|&c| c == '{').count() as i32;
69 let closes = next_line.chars().filter(|&c| c == '}').count() as i32;
70 if opens > 0 {
71 seen_open_brace = true;
72 }
73 brace_count += opens - closes;
74 end_line = sub_idx + 1;
75 if !seen_open_brace && next_trimmed.ends_with(';') {
77 break;
78 }
79 if !seen_open_brace
81 && sub_idx + 1 > start_line
82 && TS_FUNC_DEF_RE.is_match(next_line)
83 {
84 end_line = end_line.saturating_sub(1).max(start_line);
85 break;
86 }
87 if seen_open_brace && brace_count <= 0 && sub_idx + 1 >= start_line {
88 break;
89 }
90 }
91
92 let func_sinks = target
93 .data
94 .sinks
95 .iter()
96 .filter(|s| {
97 s.location.file == file_path
98 && s.location.line >= start_line
99 && s.location.line <= end_line
100 })
101 .cloned()
102 .collect();
103
104 let node = FunctionNode {
105 name: func_name.clone(),
106 file_path: file_path.to_path_buf(),
107 params,
108 start_line,
109 end_line,
110 location: SourceLocation {
111 file: file_path.to_path_buf(),
112 line: start_line,
113 column: 0,
114 end_line: Some(end_line),
115 end_column: None,
116 },
117 sinks: func_sinks,
118 };
119
120 graph.functions.entry(func_name).or_default().push(node);
121 }
122 }
123
124 for (line_idx, line) in lines.iter().enumerate() {
126 let line_num = line_idx + 1;
127 let trimmed = line.trim();
128 if trimmed.starts_with("//") || trimmed.starts_with("/*") || TS_FUNC_DEF_RE.is_match(line) {
129 continue;
130 }
131
132 for cap in CALL_EXPR_RE.captures_iter(line) {
133 let callee_name = cap[1].to_string();
134 let raw_args = &cap[2];
135 let caller_name = graph.find_enclosing_function(file_path, line_num);
136
137 let args: Vec<String> = raw_args
138 .split(',')
139 .map(|a| a.trim().to_string())
140 .filter(|a| !a.is_empty())
141 .collect();
142
143 graph.call_sites.push(CallSite {
144 caller_name,
145 callee_name,
146 file_path: file_path.to_path_buf(),
147 line_number: line_num,
148 args,
149 location: SourceLocation {
150 file: file_path.to_path_buf(),
151 line: line_num,
152 column: line.find(&cap[0]).unwrap_or(0),
153 end_line: None,
154 end_column: None,
155 },
156 });
157 }
158 }
159}