From 7bc37a023057ac99626f96e66b41e2e8f4636acc Mon Sep 17 00:00:00 2001 From: Hung Pham Date: Sun, 13 Sep 2026 06:36:12 +0700 Subject: [PATCH 1/2] Fix binary callees/callers/flow always empty: route id >= bin_base to BinaryGraph (#26 regression) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After the storage split, binary symbols live only in binary.sqlite (id range bin_base = 2e9) while codegraph_callees/callers/flow still queried the main GraphIndex, so every binary returned empty results. Now those tools route to BinaryGraph when the node id falls in the binary range, with a fallback to the old path if the binary DB is unavailable. - BinaryGraph: add callees (call records resolved by name within the same binary), callers (BFS over a new caller_of:{name} reverse index built at ingest) and flow (chain render with CFG markers + call sites, same shape as GraphIndex::flow). - Ingest: stop corrupting chain entries — CFG markers (id < SYMBOL_BASE) and unresolved-call placeholders (0) are kept as-is; only real symbol ids are remapped into the bin_base range. - MCP: dispatch_binary_graph routes callees/callers/impact/flow for binary ids. - Config template: document the [bingraph] section (enabled/bin_base/storage) with a warning against overlapping id ranges. - Docs: binary-analysis.md updated for the split-storage architecture. - Tests: bingraph unit tests extended + end-to-end test compiling a real shared library and running it through r2 extraction and queries. --- crates/codegraph-extract/src/bingraph.rs | 354 ++++++++++++++++-- crates/codegraph-extract/src/config.rs | 13 + .../tests/binary_callees_e2e.rs | 94 +++++ crates/codegraph-mcp/src/tools.rs | 102 +++++ docs/binary-analysis.md | 28 +- 5 files changed, 560 insertions(+), 31 deletions(-) create mode 100644 crates/codegraph-extract/tests/binary_callees_e2e.rs diff --git a/crates/codegraph-extract/src/bingraph.rs b/crates/codegraph-extract/src/bingraph.rs index 2218dd053..382dbca6a 100644 --- a/crates/codegraph-extract/src/bingraph.rs +++ b/crates/codegraph-extract/src/bingraph.rs @@ -15,7 +15,10 @@ use crate::config::ExtractConfig; use camino::Utf8Path; -use codegraph_core::{CallRecord, Error, Result, Symbol, SymbolKind}; +use codegraph_core::{ + is_marker, marker_name, CallRecord, Error, FlowCall, FlowResult, Result, Symbol, SymbolKind, + SYMBOL_BASE, +}; use codegraph_graph::{ open_keyspace_storage, ParseResult, Search, SearchError, Storage, StorageError, }; @@ -379,9 +382,20 @@ impl BinaryGraph { } meta_set_ids(&self.storage, "next_record", &[next_record as u64]).await?; - // 4. Chains (u64 native) + call records (JSON). + // 4. Chains (u64 native) + call records (JSON). Chain chứa marker CFG + // (id < SYMBOL_BASE) và placeholder `0` cho call-site chưa resolve — + // cả hai phải giữ nguyên, chỉ remap symbol id thật sang dải `bin_base`. for (local_id, chain) in &parsed.chains { - let global: Vec = chain.iter().map(|v| bin_base + v).collect(); + let global: Vec = chain + .iter() + .map(|&v| { + if v == 0 || v < SYMBOL_BASE { + v + } else { + bin_base + v + } + }) + .collect(); self.storage .write() .await @@ -409,6 +423,17 @@ impl BinaryGraph { .set_call_records(bin_base + call.caller_id, &blob) .await .map_err(db_err)?; + // Reverse index cho `callers` — tra ngược theo tên callee. Re-ingest + // gỡ symbol cũ khỏi index chính nhưng entry stale ở đây chỉ bị bỏ + // qua lúc query (load_symbol → None), không cần dọn. + if !call.call_name.is_empty() { + meta_add_id( + &self.storage, + &format!("caller_of:{}", call.call_name), + bin_base + call.caller_id, + ) + .await?; + } } Ok(()) } @@ -656,6 +681,195 @@ impl BinaryGraph { .collect()) } + /// Call records thô của một caller id (kèm call_name/line/effect/args). + async fn call_records(&self, caller: u64) -> Result> { + let blob = self + .storage + .read() + .await + .get_call_records(caller) + .await + .map_err(db_err)?; + Ok(blob + .and_then(|b| serde_json::from_slice(&b).ok()) + .unwrap_or_default()) + } + + /// Resolve một call name thành symbol trong cùng binary path — exact match + /// qua name trie. Ưu tiên Function; trả `None` nếu không khớp symbol nào + /// (call ra ngoài binary, `sub_xxx` chưa recover, …). + async fn resolve_call_name(&self, name: &str, path: &str) -> Result> { + if name.is_empty() { + return Ok(None); + } + let Some(record) = self.name_record_lookup(name).await? else { + return Ok(None); + }; + let ids = meta_ids_at(&self.storage, record).await?; + let mut fallback: Option = None; + for id in ids { + let Some(sym) = self.load_symbol(id).await? else { + continue; + }; + if sym.file != path { + continue; + } + if sym.kind == SymbolKind::Function { + return Ok(Some(sym)); + } + fallback.get_or_insert(sym); + } + Ok(fallback) + } + + /// Callees trực tiếp của một hàm — resolve call records theo tên trong cùng + /// binary. Call không resolve được (import ngoài, `sub_xxx`) bị bỏ qua, giữ + /// hành vi "rỗng, không lỗi" như `GraphIndex::callees`. + pub async fn callees(&self, id: u64) -> Result> { + let Some(caller) = self.get_symbol(id).await? else { + return Ok(Vec::new()); + }; + let mut recs = self.call_records(id).await?; + recs.sort_by_key(|c| c.position); + let mut seen = std::collections::HashSet::new(); + let mut out = Vec::new(); + for rec in recs { + if let Some(sym) = self.resolve_call_name(&rec.call_name, &caller.file).await? { + if sym.id != id && seen.insert(sym.id) { + out.push(sym); + } + } + } + Ok(out) + } + + /// Callers của một hàm (BFS tới `depth` hop) — đi qua reverse index + /// `caller_of:{name}` ghi lúc ingest. Symbol đã bị gỡ (re-ingest) hoặc ở + /// binary khác bị lọc bỏ. + pub async fn callers(&self, id: u64, depth: u32) -> Result> { + let Some(target) = self.get_symbol(id).await? else { + return Ok(Vec::new()); + }; + let mut frontier = vec![target.name.clone()]; + let mut seen_ids = std::collections::HashSet::from([id]); + let mut out = Vec::new(); + for _ in 0..depth.max(1) { + let mut next_names = Vec::new(); + for name in &frontier { + for caller_id in meta_ids( + &self.storage, + &format!("caller_of:{name}"), + ) + .await? + { + if !seen_ids.insert(caller_id) { + continue; + } + let Some(sym) = self.load_symbol(caller_id).await? else { + continue; + }; + if sym.file != target.file || sym.kind != SymbolKind::Function { + continue; + } + next_names.push(sym.name.clone()); + out.push(sym); + } + } + if next_names.is_empty() { + break; + } + frontier = next_names; + } + Ok(out) + } + + /// Flow của một hàm binary — tương đương `GraphIndex::flow`: chain render + /// (marker name / symbol name / call thô cho placeholder) + call sites kèm + /// line/condition/effect/args. + pub async fn flow(&self, id: u64) -> Result { + let sym = self + .get_symbol(id) + .await? + .ok_or_else(|| Error::Invalid(format!("symbol id {id} not found")))?; + let chain = self + .get_chain(id) + .await? + .ok_or_else(|| Error::Invalid(format!("chain for {:?} not found", sym.name)))?; + let mut recs = self.call_records(id).await?; + recs.sort_by_key(|c| c.position); + let rec_by_pos: HashMap = + recs.iter().map(|r| (r.position, r)).collect(); + + let mut chain_desc: Vec = Vec::with_capacity(chain.len()); + for (i, &e) in chain.iter().enumerate() { + let desc = if is_marker(e) { + marker_name(e).unwrap_or("MARKER").to_string() + } else if e >= self.bin_base { + match self.get_symbol(e).await { + Ok(Some(s)) => s.name, + _ => format!("unknown({e})"), + } + } else if let Some(rec) = rec_by_pos.get(&i) { + if !rec.call_name.is_empty() { + rec.call_name.clone() + } else { + format!("unknown({e})") + } + } else { + format!("unknown({e})") + }; + chain_desc.push(desc); + } + + let mut calls = Vec::new(); + for (i, &e) in chain.iter().enumerate() { + if is_marker(e) || e == id { + continue; + } + let rec = rec_by_pos.get(&i); + let (to_name, to_id) = if e >= self.bin_base { + match self.get_symbol(e).await { + Ok(Some(s)) => (s.name, Some(e)), + _ => ( + rec.map(|r| r.call_name.clone()) + .unwrap_or_else(|| format!("unknown({e})")), + None, + ), + } + } else if e == 0 { + match rec { + Some(rec) => { + let resolved = self + .resolve_call_name(&rec.call_name, &sym.file) + .await? + .map(|s| s.id); + (rec.call_name.clone(), resolved) + } + None => ("unknown(0)".to_string(), None), + } + } else { + (format!("unknown({e})"), None) + }; + let rec = rec.copied(); + calls.push(FlowCall { + position: i, + to_name, + to_id, + line: rec.map(|r| r.line).unwrap_or(0), + condition: rec.and_then(|r| r.condition.clone()), + effect: rec.map(|r| r.effect).unwrap_or(codegraph_core::EffectType::None), + effect_desc: rec.and_then(|r| r.effect_desc.clone()), + args: rec.map(|r| r.arg_exprs.clone()).unwrap_or_default(), + }); + } + Ok(FlowResult { + symbol: sym, + chain, + chain_desc, + calls, + }) + } + /// Thống kê — đếm từ secondary index (chỉ đọc danh sách id, không load /// symbol). pub async fn stats(&self) -> Result { @@ -713,6 +927,9 @@ mod tests { } fn sample() -> ParseResult { + // Id local thật của binary bắt đầu từ SYMBOL_BASE + 1 — mọi id + // < SYMBOL_BASE là marker CFG, không được remap. + let sid = |n: u64| SYMBOL_BASE + n; ParseResult { path: "/tmp/fake.so".to_string(), language: "binary".to_string(), @@ -720,31 +937,55 @@ mod tests { lines: 0, symbols: vec![ sym( - 1, + sid(1), "entry0", SymbolKind::Function, 4096, vec![ann("entrypoint")], ), - sym(2, "foo", SymbolKind::Function, 4200, vec![ann("export")]), - sym(3, "memcpy", SymbolKind::Function, 100, vec![ann("import")]), - sym(4, "local_fn", SymbolKind::Function, 5000, Vec::new()), - sym(5, "str:6000", SymbolKind::Constant, 6000, Vec::new()), + sym(sid(2), "foo", SymbolKind::Function, 4200, vec![ann("export")]), + sym(sid(3), "memcpy", SymbolKind::Function, 100, vec![ann("import")]), + sym(sid(4), "local_fn", SymbolKind::Function, 5000, Vec::new()), + sym(sid(5), "str:6000", SymbolKind::Constant, 6000, Vec::new()), + ], + chains: HashMap::from([( + sid(1), + vec![ + sid(1), + codegraph_core::MARKER_IF_TRUE, + 0, + sid(3), + codegraph_core::MARKER_RETURN, + ], + )]), + calls: vec![ + CallRecord { + caller_id: sid(1), + call_name: "memcpy".to_string(), + position: 3, + arg_exprs: Vec::new(), + line: 4100, + condition: None, + is_loop_body: false, + effect: EffectType::None, + effect_desc: None, + target_class: None, + target_method: None, + }, + CallRecord { + caller_id: sid(1), + call_name: "external_unresolved".to_string(), + position: 2, + arg_exprs: Vec::new(), + line: 4090, + condition: None, + is_loop_body: false, + effect: EffectType::None, + effect_desc: None, + target_class: None, + target_method: None, + }, ], - chains: HashMap::from([(1u64, vec![1u64, 3u64])]), - calls: vec![CallRecord { - caller_id: 1, - call_name: "memcpy".to_string(), - position: 1, - arg_exprs: Vec::new(), - line: 4100, - condition: None, - is_loop_body: false, - effect: EffectType::None, - effect_desc: None, - target_class: None, - target_method: None, - }], } } @@ -804,14 +1045,25 @@ mod tests { assert_eq!(eps.len(), 1); assert_eq!(eps[0].1, "entry0"); - // get_symbol lazy hydrate + chain (u64 native trên Storage). - let s = g.get_symbol(DEFAULT_BIN_BASE + 2).await.unwrap().unwrap(); + // get_symbol lazy hydrate + chain (u64 native trên Storage) — marker và + // placeholder 0 giữ nguyên, chỉ symbol id thật được remap. + let g2 = DEFAULT_BIN_BASE + SYMBOL_BASE; + let s = g.get_symbol(g2 + 2).await.unwrap().unwrap(); assert_eq!(s.name, "foo"); - let chain = g.get_chain(DEFAULT_BIN_BASE + 1).await.unwrap().unwrap(); - assert_eq!(chain, vec![DEFAULT_BIN_BASE + 1, DEFAULT_BIN_BASE + 3]); - let calls = g.get_calls(DEFAULT_BIN_BASE + 1).await.unwrap(); - assert_eq!(calls.len(), 1); - assert_eq!(calls[0].1.as_deref(), Some("memcpy")); + let chain = g.get_chain(g2 + 1).await.unwrap().unwrap(); + assert_eq!( + chain, + vec![ + g2 + 1, + codegraph_core::MARKER_IF_TRUE, + 0, + g2 + 3, + codegraph_core::MARKER_RETURN + ] + ); + let calls = g.get_calls(g2 + 1).await.unwrap(); + assert_eq!(calls.len(), 2); + assert!(calls.iter().any(|c| c.1.as_deref() == Some("memcpy"))); // stats. let stats = g.stats().await.unwrap(); @@ -822,6 +1074,50 @@ mod tests { assert_eq!(stats.binaries, 1); } + #[tokio::test] + async fn binary_callees_callers_flow() { + let g = mem_graph().await; + g.ingest(&sample(), DEFAULT_BIN_BASE).await.unwrap(); + let g2 = DEFAULT_BIN_BASE + SYMBOL_BASE; + + // callees — resolve qua call records; call ngoài binary bị bỏ qua. + let callees = g.callees(g2 + 1).await.unwrap(); + assert_eq!(callees.len(), 1); + assert_eq!(callees[0].name, "memcpy"); + + // callers — reverse index theo tên callee. + let memcpy_id = g2 + 3; + let callers = g.callers(memcpy_id, 2).await.unwrap(); + assert_eq!(callers.len(), 1); + assert_eq!(callers[0].name, "entry0"); + + // flow — chain_desc render marker + call thô cho placeholder. + let flow = g.flow(g2 + 1).await.unwrap(); + assert_eq!(flow.symbol.name, "entry0"); + assert_eq!( + flow.chain_desc, + vec![ + "entry0", + "IF_TRUE", + "external_unresolved", + "memcpy", + "RETURN" + ] + ); + assert_eq!(flow.calls.len(), 2, "skip marker + self, giữ placeholder"); + let resolved = flow + .calls + .iter() + .find(|c| c.to_name == "memcpy") + .expect("memcpy call site"); + assert_eq!(resolved.to_id, Some(memcpy_id)); + assert_eq!(flow.calls[0].to_name, "external_unresolved"); + assert_eq!(flow.calls[0].to_id, None); + + // flow cho id không tồn tại → lỗi rõ ràng. + assert!(g.flow(g2 + 999).await.is_err()); + } + #[tokio::test] async fn reingest_replaces_path() { let g = mem_graph().await; diff --git a/crates/codegraph-extract/src/config.rs b/crates/codegraph-extract/src/config.rs index 7874e88e1..ef653e782 100644 --- a/crates/codegraph-extract/src/config.rs +++ b/crates/codegraph-extract/src/config.rs @@ -612,6 +612,19 @@ type = "sqlite" # cfg_markers = true # xây marker IF/LOOP/SWITCH từ CFG của mỗi function # cache = true # cache kết quả phân tích theo (path, mtime, size) +# [bingraph] +# Dataset riêng cho symbol binary (mặc định .codegraph/binary.sqlite) — tool +# callees/callers/flow route symbol binary sang dataset này theo khoảng id +# `bin_base`. KHÔNG đặt bin_base chồng lên dải docs (1e9/3e9) hoặc dải code +# index (< 1e9); đổi bin_base giữa chừng cần re-index binary. +# enabled = true +# bin_base = 2_000_000_000 # base id symbol binary (mặc định 2e9) +# +# Storage — mặc định dataset RIÊNG cùng backend kind của [storage]. +# [bingraph.storage] +# type = "sqlite" +# dsn = "sqlite:///tmp/binary.db" + # [docgraph] # Document graph — ingest tài liệu cấu trúc (HCL/Terraform, YAML, JSON, TOML) # lúc `codegraph init`, truy vấn qua MCP (`codegraph_doc_*`) hoặc `codegraph doc`. diff --git a/crates/codegraph-extract/tests/binary_callees_e2e.rs b/crates/codegraph-extract/tests/binary_callees_e2e.rs new file mode 100644 index 000000000..44df4250b --- /dev/null +++ b/crates/codegraph-extract/tests/binary_callees_e2e.rs @@ -0,0 +1,94 @@ +//! End-to-end: compile một shared library thật → r2 extract → BinaryGraph +//! ingest → callees/callers/flow không rỗng. Bỏ qua nếu không có `cc`/`r2`. + +#![cfg(feature = "binary")] + +use camino::Utf8Path; + +#[tokio::test] +async fn real_so_callees_flow() { + if which_failed("cc") || which_failed("r2") { + eprintln!("skip: cc hoặc r2 không có trong PATH"); + return; + } + let dir = tempfile::tempdir().unwrap(); + let dir_path = Utf8Path::from_path(dir.path()).unwrap(); + + let c = r#" +int helper(int x) { return x + 1; } +int entry_fn(int x) { return helper(x) * 2; } +"#; + std::fs::write(dir.path().join("tiny.c"), c).unwrap(); + let so = dir.path().join("libtiny.so"); + let status = std::process::Command::new("cc") + .args(["-shared", "-fPIC", "-o"]) + .arg(&so) + .arg(dir.path().join("tiny.c")) + .status() + .expect("chạy cc"); + assert!(status.success(), "cc thất bại"); + + let cfg = codegraph_extract::ExtractConfig::load(dir_path); + let (parsed, skipped) = codegraph_binary::collect_binaries(dir_path, &cfg.binary); + assert_eq!(skipped, 0); + assert_eq!(parsed.len(), 1, "phải tìm thấy libtiny.so"); + + let g = codegraph_extract::BinaryGraph::open(None, 2_000_000_000) + .await + .unwrap(); + for p in &parsed { + g.ingest(p, 2_000_000_000).await.unwrap(); + } + + // Tìm entry_fn qua search tên. + let page = g + .search_name( + "entry_fn", + codegraph_extract::NameMatch::Contains, + None, + None, + 0, + 10, + ) + .await + .unwrap(); + assert_eq!(page.total, 1, "entry_fn phải được extract"); + let entry_id = page.rows[0].id; + + // callees — entry_fn gọi helper: không được rỗng (bug cũ: luôn rỗng vì + // query nhầm vào GraphIndex chính). + let callees = g.callees(entry_id).await.unwrap(); + assert!( + callees.iter().any(|s| s.name.contains("helper")), + "entry_fn phải gọi helper, callees = {:?}", + callees.iter().map(|s| &s.name).collect::>() + ); + + // callers — helper được entry_fn gọi. + let helper = callees + .iter() + .find(|s| s.name.contains("helper")) + .expect("helper phải nằm trong callees"); + let callers = g.callers(helper.id, 1).await.unwrap(); + assert!( + callers.iter().any(|s| s.name.contains("entry_fn")), + "helper phải có caller entry_fn, callers = {:?}", + callers.iter().map(|s| &s.name).collect::>() + ); + + // flow — chain có ít nhất symbol + 1 call site, chain_desc hiển thị tên. + let flow = g.flow(entry_id).await.unwrap(); + assert_eq!(flow.symbol.name, page.rows[0].name); + assert!(!flow.calls.is_empty(), "flow.calls không được rỗng"); + assert!(flow + .chain_desc + .iter() + .any(|d| d.contains("helper") || d.contains("entry_fn"))); +} + +fn which_failed(bin: &str) -> bool { + std::process::Command::new(bin) + .arg("--version") + .output() + .is_err() +} diff --git a/crates/codegraph-mcp/src/tools.rs b/crates/codegraph-mcp/src/tools.rs index 949b9aaab..06bc9e0a8 100644 --- a/crates/codegraph-mcp/src/tools.rs +++ b/crates/codegraph-mcp/src/tools.rs @@ -483,6 +483,12 @@ pub async fn dispatch_with_api( } "codegraph_callers" => { let id = arg_u64(&args, "node")?; + if let Some(out) = + dispatch_binary_graph(root, name, &args, id, session_detail, session_format) + .await? + { + return Ok(out); + } let depth = args.get("depth").and_then(|v| v.as_u64()).unwrap_or(1) as u32; let hits = api.callers(id, depth).await?; let detail = detail_from_args(&args, session_detail); @@ -495,6 +501,12 @@ pub async fn dispatch_with_api( } "codegraph_callees" => { let id = arg_u64(&args, "node")?; + if let Some(out) = + dispatch_binary_graph(root, name, &args, id, session_detail, session_format) + .await? + { + return Ok(out); + } let hits = api.callees(id).await?; let detail = detail_from_args(&args, session_detail); let format = format_from_args(&args, session_format); @@ -506,6 +518,12 @@ pub async fn dispatch_with_api( } "codegraph_impact" => { let id = arg_u64(&args, "node")?; + if let Some(out) = + dispatch_binary_graph(root, name, &args, id, session_detail, session_format) + .await? + { + return Ok(out); + } let depth = args.get("max_depth").and_then(|v| v.as_u64()).unwrap_or(3) as u32; let hits = api.impact(id, depth).await?; let detail = detail_from_args(&args, session_detail); @@ -518,6 +536,12 @@ pub async fn dispatch_with_api( } "codegraph_flow" => { let id = arg_u64(&args, "node")?; + if let Some(out) = + dispatch_binary_graph(root, name, &args, id, session_detail, session_format) + .await? + { + return Ok(out); + } let flow = api.flow(id).await?; let detail = detail_from_args(&args, session_detail); let format = format_from_args(&args, session_format); @@ -954,6 +978,84 @@ fn arg_u64(v: &Value, k: &str) -> Result { .ok_or_else(|| Error::Invalid(format!("missing int arg: {k}"))) } +// ── Binary graph routing ── +// Symbol binary (id >= `[bingraph] bin_base`, mặc định 2e9) nằm trong dataset +// riêng `binary.sqlite` chứ không phải GraphIndex chính — `callees`/`callers`/ +// `flow`/`impact` phải route sang `BinaryGraph`, không thì luôn rỗng. + +/// Mở BinaryGraph nếu `id` thuộc dải binary; `None` khi id thường hoặc +/// `[bingraph]` không mở được (fallback query code index như cũ). +async fn binary_graph_for( + root: &Utf8Path, + id: u64, +) -> Option { + let bin_base = codegraph_extract::ExtractConfig::load(root).bin_base(); + if id < bin_base { + return None; + } + codegraph_extract::BinaryGraph::open_from_config(root) + .await + .ok() +} + +/// Xử lý callees/callers/impact/flow cho symbol binary — output shape giống hệt +/// nhánh code index. Trả `None` nếu tool không thuộc nhóm này (caller fallback). +async fn dispatch_binary_graph( + root: &Utf8Path, + name: &str, + args: &Value, + id: u64, + session_detail: DetailLevel, + session_format: OutputStyle, +) -> Result> { + let Some(graph) = binary_graph_for(root, id).await else { + return Ok(None); + }; + let detail = detail_from_args(args, session_detail); + let format = format_from_args(args, session_format); + let out = match name { + "codegraph_callees" => { + let hits = graph.callees(id).await?; + let arr: Vec = hits + .iter() + .map(|s| symbol_json(root.as_str(), s, detail, format)) + .collect(); + emit_value(root.as_str(), Value::Array(arr))? + } + "codegraph_callers" | "codegraph_impact" => { + let depth = args + .get(if name == "codegraph_impact" { + "max_depth" + } else { + "depth" + }) + .and_then(|v| v.as_u64()) + .unwrap_or(1) + .max(1) as u32; + let hits = graph.callers(id, depth).await?; + let arr: Vec = hits + .iter() + .map(|s| symbol_json(root.as_str(), s, detail, format)) + .collect(); + emit_value(root.as_str(), Value::Array(arr))? + } + "codegraph_flow" => { + let flow = graph.flow(id).await?; + emit_value( + root.as_str(), + json!({ + "symbol": symbol_json(root.as_str(), &flow.symbol, detail, format), + "chain": flow.chain, + "chain_desc": flow.chain_desc, + "calls": flow.calls, + }), + )? + } + _ => return Ok(None), + }; + Ok(Some(out)) +} + // ── Symbol detail + path relativization ── // List tools trả symbol theo `DetailLevel` của session (`codegraph_init // {"detail": ...}`), ghi đè từng call bằng arg `detail`. Mọi response đi qua diff --git a/docs/binary-analysis.md b/docs/binary-analysis.md index a1c84ae73..70145960b 100644 --- a/docs/binary-analysis.md +++ b/docs/binary-analysis.md @@ -7,7 +7,7 @@ CodeGraph có thể xây dựng semantic graph **trực tiếp từ file binary* ``` files → tree-sitter (source) ─┐ ├→ GraphIndex::ingest → semgraph → MCP server -binaries → radare2 (r2pipe) ──┘ +binaries → radare2 (r2pipe) ──┘ → BinaryGraph (binary.sqlite) ``` Sau khi tree-sitter parse các file source, orchestrator gọi `codegraph_binary::collect_binaries` để scan và phân tích binary, rồi append kết quả `ParseResult` (với `language = "binary"`) vào cùng danh sách ingest. @@ -30,7 +30,16 @@ Các bước chính trong `crates/codegraph-binary`: | `swi` / `syscall` | `THROW` | Nếu tắt `cfg_markers`: chỉ lấy call edges nhẹ từ `agCj` (không có markers). -4. **Ingest** — `ParseResult` được nạp vào `GraphIndex` như mọi nguồn khác; từ đó `codegraph_search_symbol`, `codegraph_flow`, `codegraph_callers`, `codegraph_impact`, `codegraph_context`… hoạt động trên binary y như source. +4. **Ingest** — `ParseResult` binary được nạp vào dataset **riêng** `BinaryGraph` + (mặc định `.codegraph/binary.sqlite`, cấu hình qua `[bingraph]`) với dải id + riêng bắt đầu từ `bin_base` (mặc định 2e9), **không** nạp vào `GraphIndex` + chính (tránh làm phình name trie/RAM của code index). Các MCP tool + `codegraph_binary_list` / `codegraph_binary_search` / `codegraph_binary_addr` + / `codegraph_binary_stats` query trực tiếp dataset này; còn + `codegraph_callees` / `codegraph_callers` / `codegraph_flow` / + `codegraph_impact` tự route sang `BinaryGraph` khi nhận symbol id ≥ + `bin_base` — dùng chung giao diện như với source code. `codegraph_search_symbol` + và `codegraph_context` chỉ thấy source code, không thấy binary. ## Cấu hình @@ -44,6 +53,21 @@ cfg_markers = true # xây markers IF/LOOP/RETURN/THROW từ CFG từng functi cache = true # cache kết quả theo (path, mtime, size) trong .codegraph/binary-cache/ ``` +Dataset binary graph — section `[bingraph]`: + +```toml +[bingraph] +enabled = true # dataset binary riêng (mặc định bật) +bin_base = 2_000_000_000 # base id symbol binary (mặc định 2e9) + +[bingraph.storage] +type = "sqlite" # mặc định theo backend kind của [storage] +dsn = "sqlite:///tmp/binary.db" +``` + +Lưu ý: `bin_base` không được chồng lên dải id docs (1e9/3e9) hay code index +(< 1e9) — route của callees/callers/flow dựa vào khoảng id này. + Ghi chú: - `depth = "fast"` phù hợp binary lớn — bỏ qua phân tích sâu của `aaa`. From 2fe1a52e4d7dd3bafcd6325c6d984ae4d5af3f8b Mon Sep 17 00:00:00 2001 From: hungpham10 <136320753+hungpham10@users.noreply.github.com> Date: Sat, 12 Sep 2026 23:44:59 +0000 Subject: [PATCH 2/2] style: apply rustfmt --- crates/codegraph-extract/src/bingraph.rs | 27 ++++++++++++++++-------- crates/codegraph-mcp/src/tools.rs | 17 +++++---------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/crates/codegraph-extract/src/bingraph.rs b/crates/codegraph-extract/src/bingraph.rs index 382dbca6a..d693bb83a 100644 --- a/crates/codegraph-extract/src/bingraph.rs +++ b/crates/codegraph-extract/src/bingraph.rs @@ -756,12 +756,7 @@ impl BinaryGraph { for _ in 0..depth.max(1) { let mut next_names = Vec::new(); for name in &frontier { - for caller_id in meta_ids( - &self.storage, - &format!("caller_of:{name}"), - ) - .await? - { + for caller_id in meta_ids(&self.storage, &format!("caller_of:{name}")).await? { if !seen_ids.insert(caller_id) { continue; } @@ -857,7 +852,9 @@ impl BinaryGraph { to_id, line: rec.map(|r| r.line).unwrap_or(0), condition: rec.and_then(|r| r.condition.clone()), - effect: rec.map(|r| r.effect).unwrap_or(codegraph_core::EffectType::None), + effect: rec + .map(|r| r.effect) + .unwrap_or(codegraph_core::EffectType::None), effect_desc: rec.and_then(|r| r.effect_desc.clone()), args: rec.map(|r| r.arg_exprs.clone()).unwrap_or_default(), }); @@ -943,8 +940,20 @@ mod tests { 4096, vec![ann("entrypoint")], ), - sym(sid(2), "foo", SymbolKind::Function, 4200, vec![ann("export")]), - sym(sid(3), "memcpy", SymbolKind::Function, 100, vec![ann("import")]), + sym( + sid(2), + "foo", + SymbolKind::Function, + 4200, + vec![ann("export")], + ), + sym( + sid(3), + "memcpy", + SymbolKind::Function, + 100, + vec![ann("import")], + ), sym(sid(4), "local_fn", SymbolKind::Function, 5000, Vec::new()), sym(sid(5), "str:6000", SymbolKind::Constant, 6000, Vec::new()), ], diff --git a/crates/codegraph-mcp/src/tools.rs b/crates/codegraph-mcp/src/tools.rs index 06bc9e0a8..56e4e831e 100644 --- a/crates/codegraph-mcp/src/tools.rs +++ b/crates/codegraph-mcp/src/tools.rs @@ -484,8 +484,7 @@ pub async fn dispatch_with_api( "codegraph_callers" => { let id = arg_u64(&args, "node")?; if let Some(out) = - dispatch_binary_graph(root, name, &args, id, session_detail, session_format) - .await? + dispatch_binary_graph(root, name, &args, id, session_detail, session_format).await? { return Ok(out); } @@ -502,8 +501,7 @@ pub async fn dispatch_with_api( "codegraph_callees" => { let id = arg_u64(&args, "node")?; if let Some(out) = - dispatch_binary_graph(root, name, &args, id, session_detail, session_format) - .await? + dispatch_binary_graph(root, name, &args, id, session_detail, session_format).await? { return Ok(out); } @@ -519,8 +517,7 @@ pub async fn dispatch_with_api( "codegraph_impact" => { let id = arg_u64(&args, "node")?; if let Some(out) = - dispatch_binary_graph(root, name, &args, id, session_detail, session_format) - .await? + dispatch_binary_graph(root, name, &args, id, session_detail, session_format).await? { return Ok(out); } @@ -537,8 +534,7 @@ pub async fn dispatch_with_api( "codegraph_flow" => { let id = arg_u64(&args, "node")?; if let Some(out) = - dispatch_binary_graph(root, name, &args, id, session_detail, session_format) - .await? + dispatch_binary_graph(root, name, &args, id, session_detail, session_format).await? { return Ok(out); } @@ -985,10 +981,7 @@ fn arg_u64(v: &Value, k: &str) -> Result { /// Mở BinaryGraph nếu `id` thuộc dải binary; `None` khi id thường hoặc /// `[bingraph]` không mở được (fallback query code index như cũ). -async fn binary_graph_for( - root: &Utf8Path, - id: u64, -) -> Option { +async fn binary_graph_for(root: &Utf8Path, id: u64) -> Option { let bin_base = codegraph_extract::ExtractConfig::load(root).bin_base(); if id < bin_base { return None;