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    );
60}
61
62// Query / Response structures ------------------------------------------------
63
64/// Parameters for the graph endpoint.
65#[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/// Parameters for the diff endpoint.
77#[derive(Deserialize)]
78struct DiffQuery {
79    old: String,
80    new: String,
81    language: Option<String>,
82}
83
84/// Payload for the load endpoint.
85#[derive(Deserialize)]
86#[allow(dead_code)]
87struct LoadRequest {
88    project: String,
89    languages: Option<Vec<String>>,
90}
91
92/// Parameters for the call‑tree endpoint.
93///
94/// * `root` – name of the function to use as root, or `"*"` for a forest of
95///   all top‑level functions.
96/// * `depth` – maximum expansion depth (default: 3).
97/// * `direction` – `"callees"` (default) or `"callers"`.
98#[derive(Deserialize)]
99struct CallTreeQuery {
100    root: String,
101    depth: Option<usize>,
102    direction: Option<String>,
103}
104
105/// A single node in the call tree returned to the client.
106#[derive(Serialize)]
107struct TreeNode {
108    name: String,
109    kind: String,
110    line: usize,
111    file: Option<String>,
112    children: Vec<TreeNode>,
113}
114
115// Handlers ------------------------------------------------------------------
116
117/// Return a subgraph of the main CPG, filtered by the given parameters.
118async 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
213/// Return detailed information about a single function.
214async 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
262/// Return all function metrics.
263async 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
269/// Return all class metrics.
270async 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
276/// Return per‑file aggregate metrics.
277async 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
283/// Compare two projects or cached graphs and return a diff report.
284async 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
298/// Load a new project, auto‑detecting its language.
299async 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
327/// Return a call tree (or forest) starting from the requested root.
328///
329/// * `root = "*"` returns a forest of all top‑level functions (those with
330///   no incoming calls).
331/// * Otherwise a single tree is built from the named function.
332async 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
387/// Collect every function (or class) that has no incoming [`Edge::Call`]
388/// edges – i.e. the roots of the call forest.
389fn 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
422/// Look up a node index by name (linear scan, used only for annotations).
423fn 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
431/// Recursively build a call tree from `node_idx`.
432///
433/// Traverses outgoing [`Edge::Call`] edges (or incoming when `reverse` is
434/// true).  A visited set prevents infinite recursion on cycles.
435fn 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
522/// Build a subgraph centred on a specific function.
523fn 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
621/// Build a subgraph limited to nodes of a given kind.
622fn 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}