Skip to main content

icb_server/
routes.rs

1//! API route definitions and handlers for the ICB server.
2//!
3//! # Overview
4//!
5//! This module exposes a REST API consumed by the ICB dashboard.  All
6//! routes are mounted under `/api` and operate on a shared
7//! [`CodePropertyGraph`] that is built once at startup and held in an
8//! `Arc<Mutex<…>>`.
9//!
10//! # Endpoints
11//!
12//! | Method | Path              | Description |
13//! |--------|-------------------|-------------|
14//! | GET    | `/api/graph`      | Subgraph filtered by kind, focus, depth, max nodes, cycle/dead highlights |
15//! | GET    | `/api/node`       | Detailed information about a single function |
16//! | GET    | `/api/functions`  | All function metrics |
17//! | GET    | `/api/classes`    | All class metrics |
18//! | GET    | `/api/files`      | Per‑file aggregate metrics |
19//! | GET    | `/api/diff`       | Compare two projects or cached graphs |
20//! | POST   | `/api/load`       | Load a new project, auto‑detecting its language |
21//! | POST   | `/api/upload`     | Upload a ZIP archive and analyse it |
22//! | GET    | `/api/call-tree`  | Call tree from a root function or forest of all roots (`*`) |
23//!
24//! # Security
25//!
26//! In its current form the server is intended for local use.  The diff
27//! endpoint can read arbitrary files reachable from the server process;
28//! restrict network access appropriately.
29
30use 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// Query / Response structures ------------------------------------------------
64
65/// Parameters for the graph endpoint.
66#[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/// Parameters for the diff endpoint.
78#[derive(Deserialize)]
79struct DiffQuery {
80    old: String,
81    new: String,
82    language: Option<String>,
83}
84
85/// Payload for the load endpoint.
86#[derive(Deserialize)]
87#[allow(dead_code)]
88struct LoadRequest {
89    project: String,
90    languages: Option<Vec<String>>,
91}
92
93/// Parameters for the call‑tree endpoint.
94///
95/// * `root` – name of the function to use as root, or `"*"` for a forest of
96///   all top‑level functions.
97/// * `depth` – maximum expansion depth (default: 3).
98/// * `direction` – `"callees"` (default) or `"callers"`.
99#[derive(Deserialize)]
100struct CallTreeQuery {
101    root: String,
102    depth: Option<usize>,
103    direction: Option<String>,
104}
105
106/// A single node in the call tree returned to the client.
107#[derive(Serialize)]
108struct TreeNode {
109    name: String,
110    kind: String,
111    line: usize,
112    file: Option<String>,
113    children: Vec<TreeNode>,
114}
115
116// Handlers ------------------------------------------------------------------
117
118/// Return a subgraph of the main CPG, filtered by the given parameters.
119async 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
214/// Return detailed information about a single function.
215async 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
263/// Return all function metrics.
264async 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
270/// Return all class metrics.
271async 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
277/// Return per‑file aggregate metrics.
278async 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
284/// Compare two projects or cached graphs and return a diff report.
285async 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
299/// Load a new project, auto‑detecting its language.
300async 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
328/// Return a call tree (or forest) starting from the requested root.
329///
330/// * `root = "*"` returns a forest of all top‑level functions (those with
331///   no incoming calls).
332/// * Otherwise a single tree is built from the named function.
333async 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
388/// Collect every function (or class) that has no incoming [`Edge::Call`]
389/// edges – i.e. the roots of the call forest.
390fn 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
423/// Look up a node index by name (linear scan, used only for annotations).
424fn 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
432/// Recursively build a call tree from `node_idx`.
433///
434/// Traverses outgoing [`Edge::Call`] edges (or incoming when `reverse` is
435/// true).  A visited set prevents infinite recursion on cycles.
436fn 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
523/// Build a subgraph centred on a specific function.
524fn 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
622/// Build a subgraph limited to nodes of a given kind.
623fn 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}