1use actix_web::{web, HttpResponse};
31use icb_common::NodeKind;
32use icb_graph::analysis;
33use icb_graph::graph::{CodePropertyGraph, Edge, GraphData};
34use petgraph::stable_graph::NodeIndex;
35use petgraph::visit::{EdgeRef, IntoEdgeReferences};
36use serde::Deserialize;
37use serde::Serialize;
38use std::collections::{HashMap, HashSet};
39use std::path::Path;
40use std::sync::Mutex;
41
42use crate::analytics;
43use crate::diff;
44use crate::graph_builder;
45use crate::upload;
46
47pub fn configure(cfg: &mut web::ServiceConfig) {
48 cfg.service(
49 web::scope("/api")
50 .route("/graph", web::get().to(get_graph))
51 .route("/node", web::get().to(get_node_detail))
52 .route("/functions", web::get().to(get_functions))
53 .route("/classes", web::get().to(get_classes))
54 .route("/files", web::get().to(get_files))
55 .route("/diff", web::get().to(get_diff))
56 .route("/load", web::post().to(post_load))
57 .route("/upload", web::post().to(upload::handle_upload))
58 .route("/call-tree", web::get().to(get_call_tree))
59 .route("/stats", web::get().to(get_stats)),
60 );
61}
62
63#[derive(Deserialize)]
67struct GraphQuery {
68 kind: Option<String>,
69 max_nodes: Option<usize>,
70 focus: Option<String>,
71 depth: Option<usize>,
72 show_cycles: Option<bool>,
73 show_dead: Option<bool>,
74 entries: Option<String>,
75}
76
77#[derive(Deserialize)]
79struct DiffQuery {
80 old: String,
81 new: String,
82 language: Option<String>,
83}
84
85#[derive(Deserialize)]
87#[allow(dead_code)]
88struct LoadRequest {
89 project: String,
90 languages: Option<Vec<String>>,
91}
92
93#[derive(Deserialize)]
100struct CallTreeQuery {
101 root: String,
102 depth: Option<usize>,
103 direction: Option<String>,
104}
105
106#[derive(Serialize)]
108struct TreeNode {
109 name: String,
110 kind: String,
111 line: usize,
112 file: Option<String>,
113 children: Vec<TreeNode>,
114}
115
116async fn get_graph(
120 data: web::Data<Mutex<CodePropertyGraph>>,
121 query: web::Query<GraphQuery>,
122) -> HttpResponse {
123 let graph = data.lock().unwrap();
124 let GraphQuery {
125 kind,
126 max_nodes,
127 focus,
128 depth,
129 show_cycles,
130 show_dead,
131 entries,
132 } = query.into_inner();
133 let max = max_nodes.unwrap_or(200);
134 let d = depth.unwrap_or(1);
135 let show_cycles = show_cycles.unwrap_or(false);
136 let show_dead = show_dead.unwrap_or(false);
137
138 let filtered = if let Some(ref func) = focus {
139 focal_graph(&graph, func, max, d)
140 } else {
141 subgraph_by_kind(&graph, kind.as_deref(), max)
142 };
143
144 if !show_cycles && !show_dead {
145 return HttpResponse::Ok().json(&filtered);
146 }
147
148 let mut value = serde_json::to_value(&filtered).unwrap();
149 if let Some(nodes) = value.get_mut("nodes").and_then(|n| n.as_array_mut()) {
150 let cycle_nodes: HashSet<usize> = if show_cycles {
151 let cycles = analysis::detect_call_cycles(&graph);
152 cycles
153 .iter()
154 .flat_map(|c| &c.functions)
155 .filter_map(|name| {
156 graph
157 .graph
158 .node_indices()
159 .find(|&idx| graph.graph[idx].name.as_deref() == Some(name))
160 .map(|idx| idx.index())
161 })
162 .collect()
163 } else {
164 HashSet::new()
165 };
166
167 let dead_nodes: HashSet<usize> = if show_dead {
168 let entry_list: Vec<String> = entries
169 .as_deref()
170 .unwrap_or("main")
171 .split(',')
172 .map(|s| s.trim().to_string())
173 .collect();
174 analysis::detect_dead_code(&graph, &entry_list)
175 .iter()
176 .filter_map(|node| {
177 graph
178 .graph
179 .node_indices()
180 .find(|&idx| graph.graph[idx].name.as_deref() == node.name.as_deref())
181 .map(|idx| idx.index())
182 })
183 .collect()
184 } else {
185 HashSet::new()
186 };
187
188 for node_val in nodes.iter_mut() {
189 if let Some(obj) = node_val.as_object_mut() {
190 let name = obj
191 .get("name")
192 .and_then(|v| v.as_str())
193 .unwrap_or("")
194 .to_string();
195 obj.insert(
196 "is_cycle".to_string(),
197 serde_json::Value::Bool(
198 cycle_nodes.contains(&find_node_index_by_name(&graph, &name)),
199 ),
200 );
201 obj.insert(
202 "is_dead".to_string(),
203 serde_json::Value::Bool(
204 dead_nodes.contains(&find_node_index_by_name(&graph, &name)),
205 ),
206 );
207 }
208 }
209 }
210
211 HttpResponse::Ok().json(&value)
212}
213
214async fn get_node_detail(
216 data: web::Data<Mutex<CodePropertyGraph>>,
217 query: web::Query<HashMap<String, String>>,
218) -> HttpResponse {
219 let graph = data.lock().unwrap();
220 let name = match query.get("name") {
221 Some(n) => n.clone(),
222 None => return HttpResponse::BadRequest().json("missing 'name' parameter"),
223 };
224
225 let node_idx = match graph
226 .graph
227 .node_indices()
228 .find(|&idx| graph.graph[idx].name.as_deref() == Some(&name))
229 {
230 Some(idx) => idx,
231 None => return HttpResponse::NotFound().json("function not found"),
232 };
233
234 let node = &graph.graph[node_idx];
235 let callers: Vec<String> = icb_graph::query::callers_of(&graph, &name)
236 .iter()
237 .map(|(n, _)| n.name.clone().unwrap_or_default())
238 .collect();
239 let callees: Vec<String> = icb_graph::query::callees_of(&graph, &name)
240 .iter()
241 .map(|(n, _)| n.name.clone().unwrap_or_default())
242 .collect();
243 let cycles = analysis::detect_call_cycles(&graph);
244 let is_cycle = cycles.iter().any(|c| c.functions.contains(&name));
245 let dead_entries = vec!["main".to_string()];
246 let is_dead = analysis::detect_dead_code(&graph, &dead_entries)
247 .iter()
248 .any(|n| n.name.as_deref() == Some(&name));
249
250 let detail = serde_json::json!({
251 "name": node.name.clone().unwrap_or_default(),
252 "kind": format!("{:?}", node.kind),
253 "line": node.start_line,
254 "file": node.usr.clone().unwrap_or_default(),
255 "callers": callers,
256 "callees": callees,
257 "is_cycle": is_cycle,
258 "is_dead": is_dead,
259 });
260 HttpResponse::Ok().json(&detail)
261}
262
263async fn get_functions(data: web::Data<Mutex<CodePropertyGraph>>) -> HttpResponse {
265 let graph = data.lock().unwrap();
266 let functions = analytics::collect_function_metrics(&graph);
267 HttpResponse::Ok().json(&functions)
268}
269
270async fn get_classes(data: web::Data<Mutex<CodePropertyGraph>>) -> HttpResponse {
272 let graph = data.lock().unwrap();
273 let classes = analytics::collect_class_metrics(&graph);
274 HttpResponse::Ok().json(&classes)
275}
276
277async fn get_files(data: web::Data<Mutex<CodePropertyGraph>>) -> HttpResponse {
279 let graph = data.lock().unwrap();
280 let files = analytics::collect_file_metrics(&graph);
281 HttpResponse::Ok().json(&files)
282}
283
284async fn get_diff(query: web::Query<DiffQuery>) -> HttpResponse {
286 let lang = query.language.clone().unwrap_or_else(|| "cpp".into());
287
288 let old_graph =
289 graph_builder::build_or_load_graph(Path::new(&query.old), &lang, None, None, true);
290 let new_graph =
291 graph_builder::build_or_load_graph(Path::new(&query.new), &lang, None, None, true);
292
293 match (old_graph, new_graph) {
294 (Ok(old), Ok(new)) => HttpResponse::Ok().json(diff::diff_graphs(&old, &new)),
295 (Err(e), _) | (_, Err(e)) => HttpResponse::BadRequest().body(e.to_string()),
296 }
297}
298
299async fn post_load(
301 data: web::Data<Mutex<CodePropertyGraph>>,
302 body: web::Json<LoadRequest>,
303) -> HttpResponse {
304 let languages = body.languages.clone().unwrap_or_default();
305 match graph_builder::build_or_load_graph_multi(
306 Path::new(&body.project),
307 &languages,
308 None,
309 None,
310 true,
311 ) {
312 Ok(new_graph) => {
313 let nodes = new_graph.graph.node_count();
314 let edges = new_graph.graph.edge_count();
315 if let Ok(mut locked) = data.lock() {
316 *locked = new_graph;
317 }
318 HttpResponse::Ok().json(serde_json::json!({
319 "status": "ok",
320 "nodes": nodes,
321 "edges": edges,
322 }))
323 }
324 Err(e) => HttpResponse::BadRequest().body(e.to_string()),
325 }
326}
327
328async fn get_call_tree(
334 data: web::Data<Mutex<CodePropertyGraph>>,
335 query: web::Query<CallTreeQuery>,
336) -> HttpResponse {
337 let graph = data.lock().unwrap();
338 let CallTreeQuery {
339 root,
340 depth,
341 direction,
342 } = query.into_inner();
343
344 let max_depth = depth.unwrap_or(3);
345 let dir = direction.as_deref().unwrap_or("callees");
346 let reverse = dir == "callers";
347
348 if root == "*" {
349 let roots = find_root_functions(&graph);
350
351 let forest: Vec<TreeNode> = roots
352 .iter()
353 .map(|&idx| {
354 let mut visited = HashSet::new();
355 build_call_tree(&graph, idx, max_depth, reverse, &mut visited)
356 })
357 .collect();
358
359 return HttpResponse::Ok().json(forest);
360 }
361
362 let root_idx = match graph
363 .graph
364 .node_indices()
365 .find(|&idx| graph.graph[idx].name.as_deref() == Some(&root))
366 {
367 Some(idx) => idx,
368 None => {
369 let fallback = graph
370 .graph
371 .node_indices()
372 .find(|&i| graph.graph[i].kind == NodeKind::Function);
373
374 if let Some(idx) = fallback {
375 idx
376 } else {
377 return HttpResponse::NotFound().json("function not found");
378 }
379 }
380 };
381
382 let mut visited = HashSet::new();
383 let tree = build_call_tree(&graph, root_idx, max_depth, reverse, &mut visited);
384
385 HttpResponse::Ok().json(tree)
386}
387
388fn find_root_functions(cpg: &CodePropertyGraph) -> Vec<NodeIndex> {
391 let mut has_incoming = HashSet::new();
392
393 for edge_ref in cpg.graph.edge_references() {
394 if *edge_ref.weight() == Edge::Call {
395 has_incoming.insert(edge_ref.target());
396 }
397 }
398
399 let mut roots: Vec<NodeIndex> = cpg
400 .graph
401 .node_indices()
402 .filter(|&idx| {
403 let node = &cpg.graph[idx];
404 (node.kind == NodeKind::Function || node.kind == NodeKind::Class)
405 && !has_incoming.contains(&idx)
406 })
407 .collect();
408
409 if roots.is_empty() {
410 roots = cpg
411 .graph
412 .node_indices()
413 .filter(|&idx| {
414 let node = &cpg.graph[idx];
415 node.kind == NodeKind::Function || node.kind == NodeKind::Class
416 })
417 .collect();
418 }
419
420 roots
421}
422
423fn find_node_index_by_name(cpg: &CodePropertyGraph, name: &str) -> usize {
425 cpg.graph
426 .node_indices()
427 .find(|&idx| cpg.graph[idx].name.as_deref() == Some(name))
428 .map(|idx| idx.index())
429 .unwrap_or(usize::MAX)
430}
431
432fn build_call_tree(
437 cpg: &CodePropertyGraph,
438 node_idx: NodeIndex,
439 max_depth: usize,
440 reverse: bool,
441 visited: &mut HashSet<NodeIndex>,
442) -> TreeNode {
443 let node = &cpg.graph[node_idx];
444
445 if !visited.insert(node_idx) {
446 return TreeNode {
447 name: node.name.clone().unwrap_or_default(),
448 kind: format!("{:?}", node.kind),
449 line: node.start_line,
450 file: node.usr.clone(),
451 children: vec![],
452 };
453 }
454
455 let mut children = Vec::new();
456
457 if max_depth > 0 {
458 if reverse {
459 for edge in cpg
460 .graph
461 .edges_directed(node_idx, petgraph::Direction::Incoming)
462 {
463 if *edge.weight() == Edge::Call {
464 children.push(build_call_tree(
465 cpg,
466 edge.source(),
467 max_depth - 1,
468 reverse,
469 visited,
470 ));
471 }
472 }
473 } else {
474 for edge in cpg.graph.edges(node_idx) {
475 if *edge.weight() == Edge::Call {
476 children.push(build_call_tree(
477 cpg,
478 edge.target(),
479 max_depth - 1,
480 reverse,
481 visited,
482 ));
483 }
484 }
485 }
486
487 if children.is_empty() && max_depth > 1 {
488 for edge in cpg.graph.edges(node_idx) {
489 if let Some(edge_target) = edge.target().index().checked_sub(0) {
490 let idx = petgraph::stable_graph::NodeIndex::new(edge_target);
491
492 let n = &cpg.graph[idx];
493
494 if n.kind == NodeKind::Function || n.kind == NodeKind::Class {
495 children.push(TreeNode {
496 name: n.name.clone().unwrap_or_default(),
497 kind: format!("{:?}", n.kind),
498 line: n.start_line,
499 file: n.usr.clone(),
500 children: vec![],
501 });
502
503 if children.len() >= 5 {
504 break;
505 }
506 }
507 }
508 }
509 }
510 }
511
512 visited.remove(&node_idx);
513
514 TreeNode {
515 name: node.name.clone().unwrap_or_default(),
516 kind: format!("{:?}", node.kind),
517 line: node.start_line,
518 file: node.usr.clone(),
519 children,
520 }
521}
522
523fn focal_graph(
525 cpg: &CodePropertyGraph,
526 func_name: &str,
527 max_nodes: usize,
528 depth: usize,
529) -> GraphData {
530 let mut included = HashSet::new();
531 let mut frontier = Vec::new();
532
533 for idx in cpg.graph.node_indices() {
534 let node = &cpg.graph[idx];
535 if (node.kind == NodeKind::Function || node.kind == NodeKind::Class)
536 && node.name.as_deref() == Some(func_name)
537 {
538 included.insert(idx.index());
539 frontier.push(idx);
540 }
541 }
542
543 if included.is_empty() {
544 return GraphData {
545 nodes: vec![],
546 edges: vec![],
547 };
548 }
549
550 for _ in 0..depth {
551 let mut next_frontier = Vec::new();
552 for &node_idx in &frontier {
553 for edge_ref in cpg.graph.edges(node_idx) {
554 if *edge_ref.weight() == Edge::Call {
555 let other = edge_ref.target();
556 if !included.contains(&other.index()) {
557 included.insert(other.index());
558 next_frontier.push(other);
559 }
560 }
561 }
562 for edge_ref in cpg
563 .graph
564 .edges_directed(node_idx, petgraph::Direction::Incoming)
565 {
566 if *edge_ref.weight() == Edge::Call {
567 let other = edge_ref.source();
568 if !included.contains(&other.index()) {
569 included.insert(other.index());
570 next_frontier.push(other);
571 }
572 }
573 }
574 }
575 frontier = next_frontier;
576 if included.len() >= max_nodes {
577 break;
578 }
579 }
580
581 if included.len() > max_nodes {
582 let mut limited = HashSet::new();
583 for &idx in &included {
584 if limited.len() >= max_nodes {
585 break;
586 }
587 limited.insert(idx);
588 }
589 included = limited;
590 }
591
592 let mut index_map = std::collections::HashMap::new();
593 let mut selected_nodes = Vec::new();
594 for &idx in &included {
595 let node = &cpg.graph[petgraph::stable_graph::NodeIndex::new(idx)];
596 let new_idx = selected_nodes.len();
597 selected_nodes.push(node.clone());
598 index_map.insert(idx, new_idx);
599 }
600
601 let mut selected_edges = Vec::new();
602 for &src_idx in &included {
603 let src_node = petgraph::stable_graph::NodeIndex::new(src_idx);
604 for edge_ref in cpg.graph.edges(src_node) {
605 let tgt_idx = edge_ref.target().index();
606 if included.contains(&tgt_idx) && *edge_ref.weight() == Edge::Call {
607 selected_edges.push((
608 index_map[&src_idx],
609 index_map[&tgt_idx],
610 edge_ref.weight().clone(),
611 ));
612 }
613 }
614 }
615
616 GraphData {
617 nodes: selected_nodes,
618 edges: selected_edges,
619 }
620}
621
622fn subgraph_by_kind(cpg: &CodePropertyGraph, kind: Option<&str>, max_nodes: usize) -> GraphData {
624 let target_kind = match kind {
625 Some("Function") => Some(NodeKind::Function),
626 Some("Class") => Some(NodeKind::Class),
627 _ => None,
628 };
629
630 let mut selected_nodes = Vec::new();
631 let mut index_map = std::collections::HashMap::new();
632
633 for idx in cpg.graph.node_indices() {
634 if let Some(ref k) = target_kind {
635 if cpg.graph[idx].kind != *k {
636 continue;
637 }
638 }
639 if selected_nodes.len() >= max_nodes {
640 break;
641 }
642 let new_idx = selected_nodes.len();
643 selected_nodes.push(cpg.graph[idx].clone());
644 index_map.insert(idx.index(), new_idx);
645 }
646
647 let mut selected_edges = Vec::new();
648 for idx in cpg.graph.node_indices() {
649 if let Some(&mapped_src) = index_map.get(&idx.index()) {
650 for edge_ref in cpg.graph.edges(idx) {
651 let tgt_idx = edge_ref.target().index();
652 if let Some(&mapped_tgt) = index_map.get(&tgt_idx) {
653 selected_edges.push((mapped_src, mapped_tgt, edge_ref.weight().clone()));
654 }
655 }
656 }
657 }
658
659 GraphData {
660 nodes: selected_nodes,
661 edges: selected_edges,
662 }
663}
664
665async fn get_stats(data: web::Data<Mutex<CodePropertyGraph>>) -> HttpResponse {
666 let graph = data.lock().unwrap();
667 HttpResponse::Ok().json(serde_json::json!({
668 "nodes": graph.graph.node_count(),
669 "edges": graph.graph.edge_count(),
670 }))
671}
672
673#[doc(hidden)]
674pub fn __bench_focal_graph(
675 cpg: &CodePropertyGraph,
676 func_name: &str,
677 max_nodes: usize,
678 depth: usize,
679) -> GraphData {
680 focal_graph(cpg, func_name, max_nodes, depth)
681}
682
683#[doc(hidden)]
684pub fn __bench_subgraph_by_kind(
685 cpg: &CodePropertyGraph,
686 kind: Option<&str>,
687 max_nodes: usize,
688) -> GraphData {
689 subgraph_by_kind(cpg, kind, max_nodes)
690}
691
692#[cfg(test)]
693mod tests {
694 use super::*;
695 use actix_web::{test, App};
696 use icb_graph::graph::{Edge, Node};
697
698 fn test_graph() -> CodePropertyGraph {
699 let mut cpg = CodePropertyGraph::new();
700 let f1 = cpg.graph.add_node(Node {
701 kind: NodeKind::Function,
702 name: Some("foo".into()),
703 usr: Some("unit.cpp".into()),
704 start_line: 1,
705 end_line: 2,
706 });
707 let f2 = cpg.graph.add_node(Node {
708 kind: NodeKind::Function,
709 name: Some("bar".into()),
710 usr: Some("unit.cpp".into()),
711 start_line: 3,
712 end_line: 4,
713 });
714 cpg.graph.add_edge(f1, f2, Edge::Call);
715 cpg
716 }
717
718 #[actix_web::test]
719 async fn test_get_functions() {
720 let graph = test_graph();
721 let data = web::Data::new(Mutex::new(graph));
722 let app = test::init_service(App::new().app_data(data.clone()).configure(configure)).await;
723 let req = test::TestRequest::get().uri("/api/functions").to_request();
724 let resp = test::call_service(&app, req).await;
725 assert!(resp.status().is_success());
726 let body: Vec<serde_json::Value> = test::read_body_json(resp).await;
727 assert_eq!(body.len(), 2);
728 }
729
730 #[actix_web::test]
731 async fn test_get_classes() {
732 let graph = test_graph();
733 let data = web::Data::new(Mutex::new(graph));
734 let app = test::init_service(App::new().app_data(data.clone()).configure(configure)).await;
735 let req = test::TestRequest::get().uri("/api/classes").to_request();
736 let resp = test::call_service(&app, req).await;
737 assert!(resp.status().is_success());
738 }
739
740 #[actix_web::test]
741 async fn test_get_files() {
742 let graph = test_graph();
743 let data = web::Data::new(Mutex::new(graph));
744 let app = test::init_service(App::new().app_data(data.clone()).configure(configure)).await;
745 let req = test::TestRequest::get().uri("/api/files").to_request();
746 let resp = test::call_service(&app, req).await;
747 assert!(resp.status().is_success());
748 }
749}