Skip to main content

agentshield/analysis/interprocedural/
typescript.rs

1use 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    // 1. Extract function definitions
24    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                // Braceless arrow function: expression terminates at semicolon
76                if !seen_open_brace && next_trimmed.ends_with(';') {
77                    break;
78                }
79                // Another function definition starts; this function's scope ended
80                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    // 2. Call sites
125    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}