1use std::collections::HashMap;
6
7use super::types::*;
8
9const IGNORED_NAMES: &[&str] = &[
11 "if",
12 "for",
13 "while",
14 "switch",
15 "catch",
16 "with",
17 "function",
18 "class",
19 "return",
20 "throw",
21 "typeof",
22 "instanceof",
23 "void",
24 "delete",
25 "await",
26 "async",
27 "yield",
28 "new",
29 "super",
30 "this",
31 "console",
32 "Math",
33 "JSON",
34 "Object",
35 "Array",
36 "String",
37 "Number",
38 "Boolean",
39 "Date",
40 "RegExp",
41 "Error",
42 "Promise",
43 "Map",
44 "Set",
45 "parseInt",
46 "parseFloat",
47 "isNaN",
48 "isFinite",
49 "print",
50 "len",
51 "range",
52 "str",
53 "int",
54 "float",
55 "list",
56 "dict",
57];
58
59pub struct CallGraphBuilder {
61 function_index: HashMap<String, CallGraphNode>,
62 name_to_ids: HashMap<String, Vec<String>>,
63}
64
65impl CallGraphBuilder {
66 pub fn new() -> Self {
67 Self {
68 function_index: HashMap::new(),
69 name_to_ids: HashMap::new(),
70 }
71 }
72
73 pub fn build_call_graph(&mut self, modules: &[ModuleNode]) -> CallGraph {
75 let mut nodes = Vec::new();
76 let mut edges = Vec::new();
77
78 self.build_function_index(modules, &mut nodes);
79
80 for module in modules {
81 self.analyze_module_calls(module, &mut edges);
82 }
83
84 let merged_edges = self.merge_edges(edges);
85 CallGraph {
86 nodes,
87 edges: merged_edges,
88 }
89 }
90
91 fn build_function_index(&mut self, modules: &[ModuleNode], nodes: &mut Vec<CallGraphNode>) {
93 self.function_index.clear();
94 self.name_to_ids.clear();
95
96 for module in modules {
97 for func in &module.functions {
98 let node = CallGraphNode {
99 id: func.id.clone(),
100 name: func.name.clone(),
101 node_type: CallGraphNodeType::Function,
102 module_id: module.id.clone(),
103 class_name: None,
104 signature: Some(func.signature.clone()),
105 };
106 nodes.push(node.clone());
107 self.function_index.insert(func.id.clone(), node);
108 self.add_to_name_index(&func.name, &func.id);
109 }
110
111 for cls in &module.classes {
112 for method in &cls.methods {
113 let node = CallGraphNode {
114 id: method.id.clone(),
115 name: method.name.clone(),
116 node_type: if method.name == "constructor" {
117 CallGraphNodeType::Constructor
118 } else {
119 CallGraphNodeType::Method
120 },
121 module_id: module.id.clone(),
122 class_name: Some(cls.name.clone()),
123 signature: Some(method.signature.clone()),
124 };
125 nodes.push(node.clone());
126 self.function_index.insert(method.id.clone(), node);
127 self.add_to_name_index(&method.name, &method.id);
128 self.add_to_name_index(&format!("{}.{}", cls.name, method.name), &method.id);
129 }
130 }
131 }
132 }
133
134 fn add_to_name_index(&mut self, name: &str, id: &str) {
135 self.name_to_ids
136 .entry(name.to_string())
137 .or_default()
138 .push(id.to_string());
139 }
140
141 fn analyze_module_calls(&self, module: &ModuleNode, edges: &mut Vec<CallGraphEdge>) {
143 let content = match std::fs::read_to_string(&module.path) {
144 Ok(c) => c,
145 Err(_) => return,
146 };
147 let lines: Vec<&str> = content.lines().collect();
148 let call_re = regex::Regex::new(r"\b([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(").unwrap();
149
150 for func in &module.functions {
151 self.analyze_function_calls(func, &lines, &call_re, &module.id, edges);
152 }
153
154 for cls in &module.classes {
155 for method in &cls.methods {
156 self.analyze_method_calls(method, &lines, &call_re, &module.id, edges);
157 }
158 }
159 }
160
161 fn analyze_function_calls(
162 &self,
163 func: &FunctionNode,
164 lines: &[&str],
165 call_re: ®ex::Regex,
166 module_id: &str,
167 edges: &mut Vec<CallGraphEdge>,
168 ) {
169 let start = func.location.start_line.saturating_sub(1) as usize;
170 let end = (func.location.end_line as usize).min(lines.len());
171
172 for (i, line) in lines[start..end].iter().enumerate() {
173 let line_num = start + i + 1;
174 for cap in call_re.captures_iter(line) {
175 let called_name = &cap[1];
176 if IGNORED_NAMES.contains(&called_name) || called_name == func.name {
177 continue;
178 }
179 if let Some(target_ids) = self.name_to_ids.get(called_name) {
180 if let Some(target_id) = target_ids.iter().next() {
181 edges.push(CallGraphEdge {
182 source: func.id.clone(),
183 target: target_id.clone(),
184 edge_type: self.detect_call_type(line, called_name),
185 count: 1,
186 locations: vec![LocationInfo {
187 file: module_id.to_string(),
188 start_line: line_num as u32,
189 start_column: 0,
190 end_line: line_num as u32,
191 end_column: line.len() as u32,
192 }],
193 });
194 }
195 }
196 }
197 }
198 }
199
200 fn analyze_method_calls(
201 &self,
202 method: &MethodNode,
203 lines: &[&str],
204 call_re: ®ex::Regex,
205 module_id: &str,
206 edges: &mut Vec<CallGraphEdge>,
207 ) {
208 let start = method.location.start_line.saturating_sub(1) as usize;
209 let end = (method.location.end_line as usize).min(lines.len());
210
211 for (i, line) in lines[start..end].iter().enumerate() {
212 let line_num = start + i + 1;
213 for cap in call_re.captures_iter(line) {
214 let called_name = &cap[1];
215 if IGNORED_NAMES.contains(&called_name) || called_name == method.name {
216 continue;
217 }
218 if let Some(target_ids) = self.name_to_ids.get(called_name) {
219 if let Some(target_id) = target_ids.iter().next() {
220 edges.push(CallGraphEdge {
221 source: method.id.clone(),
222 target: target_id.clone(),
223 edge_type: self.detect_call_type(line, called_name),
224 count: 1,
225 locations: vec![LocationInfo {
226 file: module_id.to_string(),
227 start_line: line_num as u32,
228 start_column: 0,
229 end_line: line_num as u32,
230 end_column: line.len() as u32,
231 }],
232 });
233 }
234 }
235 }
236 }
237 }
238
239 fn detect_call_type(&self, line: &str, name: &str) -> CallType {
240 if line.contains(&format!(".{}(", name)) || line.contains(&format!("?.{}(", name)) {
241 CallType::Method
242 } else if line.contains(&format!("({})", name)) || line.contains(&format!(", {})", name)) {
243 CallType::Callback
244 } else if line.contains(&format!("[{}](", name)) {
245 CallType::Dynamic
246 } else {
247 CallType::Direct
248 }
249 }
250
251 fn merge_edges(&self, edges: Vec<CallGraphEdge>) -> Vec<CallGraphEdge> {
253 let mut edge_map: HashMap<String, CallGraphEdge> = HashMap::new();
254
255 for edge in edges {
256 let key = format!("{}|{}", edge.source, edge.target);
257 if let Some(existing) = edge_map.get_mut(&key) {
258 existing.count += edge.count;
259 existing.locations.extend(edge.locations);
260 } else {
261 edge_map.insert(key, edge);
262 }
263 }
264
265 edge_map.into_values().collect()
266 }
267}
268
269impl Default for CallGraphBuilder {
270 fn default() -> Self {
271 Self::new()
272 }
273}
274
275pub fn build_call_graph(modules: &[ModuleNode]) -> CallGraph {
277 let mut builder = CallGraphBuilder::new();
278 builder.build_call_graph(modules)
279}