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