diff --git a/Cargo.lock b/Cargo.lock index f1198351e0..ff0b6be044 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -720,7 +720,7 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "codegraph" -version = "2.2.8" +version = "2.2.9" dependencies = [ "anyhow", "camino", @@ -743,7 +743,7 @@ dependencies = [ [[package]] name = "codegraph-api" -version = "2.2.8" +version = "2.2.9" dependencies = [ "anyhow", "camino", @@ -761,7 +761,7 @@ dependencies = [ [[package]] name = "codegraph-bench" -version = "2.2.8" +version = "2.2.9" dependencies = [ "anyhow", "camino", @@ -780,7 +780,7 @@ dependencies = [ [[package]] name = "codegraph-binary" -version = "2.2.8" +version = "2.2.9" dependencies = [ "camino", "codegraph-core", @@ -797,7 +797,7 @@ dependencies = [ [[package]] name = "codegraph-context" -version = "2.2.8" +version = "2.2.9" dependencies = [ "camino", "codegraph-core", @@ -812,7 +812,7 @@ dependencies = [ [[package]] name = "codegraph-core" -version = "2.2.8" +version = "2.2.9" dependencies = [ "async-graphql", "camino", @@ -823,7 +823,7 @@ dependencies = [ [[package]] name = "codegraph-docs" -version = "2.2.8" +version = "2.2.9" dependencies = [ "anyhow", "codegraph-core", @@ -841,7 +841,7 @@ dependencies = [ [[package]] name = "codegraph-extract" -version = "2.2.8" +version = "2.2.9" dependencies = [ "camino", "codegraph-binary", @@ -880,7 +880,7 @@ dependencies = [ [[package]] name = "codegraph-graph" -version = "2.2.8" +version = "2.2.9" dependencies = [ "async-trait", "bincode", @@ -910,7 +910,7 @@ dependencies = [ [[package]] name = "codegraph-graphql" -version = "2.2.8" +version = "2.2.9" dependencies = [ "anyhow", "async-graphql", @@ -936,7 +936,7 @@ dependencies = [ [[package]] name = "codegraph-installer" -version = "2.2.8" +version = "2.2.9" dependencies = [ "anyhow", "camino", @@ -952,7 +952,7 @@ dependencies = [ [[package]] name = "codegraph-mcp" -version = "2.2.8" +version = "2.2.9" dependencies = [ "anyhow", "axum", @@ -976,7 +976,7 @@ dependencies = [ [[package]] name = "codegraph-sboxes" -version = "2.2.8" +version = "2.2.9" dependencies = [ "camino", "codegraph-core", @@ -998,7 +998,7 @@ dependencies = [ [[package]] name = "codegraph-source" -version = "2.2.8" +version = "2.2.9" dependencies = [ "async-trait", "camino", @@ -1011,7 +1011,7 @@ dependencies = [ [[package]] name = "codegraph-web" -version = "2.2.8" +version = "2.2.9" dependencies = [ "axum", "mime_guess", diff --git a/Cargo.toml b/Cargo.toml index 973357cb4a..74af68f333 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ members = [ ] [workspace.package] -version = "2.2.8" +version = "2.2.9" edition = "2021" rust-version = "1.80" license = "MIT" diff --git a/crates/codegraph-api/src/mermaid.rs b/crates/codegraph-api/src/mermaid.rs index 083e201138..3ccb89ac3f 100644 --- a/crates/codegraph-api/src/mermaid.rs +++ b/crates/codegraph-api/src/mermaid.rs @@ -6,8 +6,9 @@ use crate::GraphApi; use codegraph_core::{ is_marker, marker_name, EffectType, FlowCall, FlowResult, SymbolId, MARKER_BRANCH_END, - MARKER_IF_FALSE, MARKER_IF_TRUE, MARKER_LOOP, MARKER_LOOP_BACK, MARKER_SWITCH_CASE, - MARKER_SWITCH_END, + MARKER_CALL_ENTER, MARKER_CALL_EXIT, MARKER_IF_FALSE, MARKER_IF_TRUE, MARKER_LOOP, + MARKER_LOOP_BACK, MARKER_STMT_END, MARKER_SWITCH_CASE, MARKER_SWITCH_CLOSE, MARKER_SWITCH_END, + MARKER_SWITCH_START, }; use std::collections::{HashMap, HashSet}; @@ -33,18 +34,55 @@ pub fn control_flow(flow: &FlowResult) -> String { .map(|b| (b.position, b.label.as_str())) .collect(); - let mut out = String::from("flowchart TD\n"); + // Skip CALL_ENTER/EXIT, STMT_END when rendering nodes — they're structural only. + // Build a filtered chain: (filtered_index, original_index, raw_value) + let mut filtered: Vec<(usize, usize, u64)> = Vec::new(); for (i, &raw) in flow.chain.iter().enumerate() { - let desc = flow.chain_desc.get(i).map(String::as_str).unwrap_or(""); - let (open, close, label) = node_style(&flow.chain, i, raw, desc, &calls, &labels); - out.push_str(&format!(" c{i}{open}\"{}\"{close}\n", sanitize(&label))); + if raw == MARKER_CALL_ENTER || raw == MARKER_CALL_EXIT || raw == MARKER_STMT_END { + continue; + } + filtered.push((filtered.len(), i, raw)); + } + + let mut out = String::from("flowchart TD\n"); + for (new_i, orig_i, raw) in &filtered { + let desc = flow + .chain_desc + .get(*orig_i) + .map(String::as_str) + .unwrap_or(""); + let (open, close, label) = node_style(&flow.chain, *orig_i, *raw, desc, &calls, &labels); + out.push_str(&format!( + " c{new_i}{open}\"{}\"{close}\n", + sanitize(&label) + )); } // Cạnh cấu trúc, sắp xếp để output ổn định (không phụ thuộc thứ tự build). - let mut edges = structural_edges(&flow.chain); - edges.sort_unstable(); - edges.dedup(); + // Gom tất cả cạnh từ cả 2 pass rồi dedup một lần để tránh duplicate edges. + let mut all_edges: Vec<(usize, usize)> = Vec::new(); + + // Pass 1: structural edges on original chain, remap to filtered indices. + let edges = structural_edges(&flow.chain); + let mut orig_to_filtered: Vec> = vec![None; flow.chain.len()]; + for (new_i, orig_i, _) in &filtered { + orig_to_filtered[*orig_i] = Some(*new_i); + } for (a, b) in edges { + if let (Some(fa), Some(fb)) = (orig_to_filtered[a], orig_to_filtered[b]) { + all_edges.push((fa, fb)); + } + } + + // Pass 2: structural edges on filtered chain (preserves structural markers except CALL_ENTER/EXIT/STMT_END). + let filtered_chain: Vec = filtered.iter().map(|(_, _, raw)| *raw).collect(); + let filtered_edges = structural_edges(&filtered_chain); + all_edges.extend(filtered_edges); + + // Dedup toàn bộ cạnh. + all_edges.sort_unstable(); + all_edges.dedup(); + for (a, b) in all_edges { out.push_str(&format!(" c{a} --> c{b}\n")); } out @@ -62,9 +100,10 @@ fn node_style( if is_marker(raw) { let name = marker_name(raw).unwrap_or("MARKER"); let (open, close) = match name { - "IF_TRUE" | "IF_FALSE" | "SWITCH_CASE" => ("{", "}"), + "IF_TRUE" | "IF_FALSE" | "SWITCH_CASE" | "SWITCH_START" => ("{", "}"), "RETURN" | "THROW" | "BREAK" | "CONTINUE" | "LOOP" | "LOOP_BACK" | "BRANCH_END" - | "SWITCH_END" | "RECURSIVE_CALL" => ("([", "])"), + | "SWITCH_END" | "SWITCH_CLOSE" | "RECURSIVE_CALL" | "CALL_ENTER" | "CALL_EXIT" + | "STMT_END" => ("([", "])"), _ => ("[", "]"), }; // Nhãn trigger: ưu tiên `branch_labels` (điều kiện/case persist), rồi @@ -73,11 +112,13 @@ fn node_style( .get(&i) .map(|s| s.to_string()) .or_else(|| match name { - "IF_TRUE" | "IF_FALSE" | "SWITCH_CASE" | "LOOP" => guard_for(chain, i, calls), + "IF_TRUE" | "IF_FALSE" | "SWITCH_CASE" | "SWITCH_START" | "LOOP" => { + guard_for(chain, i, calls) + } _ => None, }); let label = match name { - "IF_TRUE" | "IF_FALSE" | "SWITCH_CASE" | "LOOP" => match branch { + "IF_TRUE" | "IF_FALSE" | "SWITCH_CASE" | "SWITCH_START" | "LOOP" => match branch { Some(g) if !g.is_empty() => format!("{name}: {g}"), _ => name.to_string(), }, @@ -105,6 +146,12 @@ fn node_style( label.push_str(" · "); label.push_str(c.effect.as_str()); } + // Thêm biến gán nếu có. + if let Some(var) = &c.into_var { + if !var.is_empty() { + label.push_str(&format!(" → {var}")); + } + } if c.to_id.is_none() { label.push_str(" · ext"); } @@ -142,55 +189,132 @@ fn guard_for(chain: &[u64], marker_at: usize, calls: &HashMap) fn structural_edges(chain: &[u64]) -> Vec<(usize, usize)> { let n = chain.len(); let mut edges: Vec<(usize, usize)> = Vec::new(); - // Ngăn xếp index của node điều kiện (if) đang mở. - let mut if_stack: Vec = Vec::new(); + // Ngăn xếp (cond_index, then_end_index, has_else) của node điều kiện (if) đang mở. + let mut if_stack: Vec<(usize, Option, bool)> = Vec::new(); // Ngăn xếp index của header loop đang mở. let mut loop_stack: Vec = Vec::new(); - // Node trước switch đầu tiên của switch đang mở (các case toả từ đây). - let mut switch_entry: Option = None; + // Ngăn xếp entry point của các switch đang mở. + // Mỗi entry là index của node discriminant (điều kiện match) của switch đó. + let mut switch_stack: Vec = Vec::new(); + // Ngăn xếp depth của call nesting (cho CALL_ENTER/CALL_EXIT). + let mut call_stack: Vec = Vec::new(); let mut i = 1usize; while i < n { let prev = i - 1; - match chain[i] { + let curr = chain[i]; + let curr_is_marker = is_marker(curr); + let prev_is_marker = is_marker(chain[prev]); + + // Sequential edge rules: + // - non-marker -> non-marker: statement to statement + // - marker -> non-marker: marker to first statement in body (but NOT from STMT_END) + // - non-marker -> marker: for linear entry (IF_TRUE, LOOP, SWITCH_START) + // AND for block end markers (BRANCH_END, SWITCH_END, SWITCH_CLOSE) + // - marker -> marker: NO sequential edge + let is_linear_entry = matches!(curr, MARKER_IF_TRUE | MARKER_LOOP | MARKER_SWITCH_START); + let is_block_end = matches!( + curr, + MARKER_BRANCH_END | MARKER_SWITCH_END | MARKER_SWITCH_CLOSE + ); + let prev_is_stmt_end = chain[prev] == MARKER_STMT_END; + + if !prev_is_marker && !curr_is_marker { + edges.push((prev, i)); + } else if prev_is_marker && !curr_is_marker && !prev_is_stmt_end { + // marker -> non-marker, but NOT from STMT_END + edges.push((prev, i)); + } else if !prev_is_marker && curr_is_marker && (is_linear_entry || is_block_end) { + edges.push((prev, i)); + } + // marker -> marker: no sequential edge + // non-marker -> branch marker (IF_FALSE, SWITCH_CASE): no sequential edge + // STMT_END -> next statement: NO edge (separator) + + match curr { MARKER_IF_TRUE => { - edges.push((prev, i)); - if_stack.push(prev); + // IF_TRUE already connected from prev (cond) via sequential + if_stack.push((prev, None, false)); } MARKER_IF_FALSE => { - // else rẽ từ chính điều kiện `if`. - let src = if_stack.last().copied().unwrap_or(prev); + // IF_FALSE connects from cond (structural) + let src = if_stack.last().map(|(s, _, _)| *s).unwrap_or(prev); edges.push((src, i)); + if let Some(top) = if_stack.last_mut() { + top.1 = Some(prev); + top.2 = true; + } } MARKER_BRANCH_END => { - let cond = if_stack.pop().unwrap_or(prev); - edges.push((cond, i)); // đường điều kiện sai (không có else) - edges.push((prev, i)); // cuối nhánh + let (cond, then_end, has_else) = if_stack.pop().unwrap_or((prev, None, false)); + // BRANCH_END connected from prev (last body stmt) via sequential + if has_else { + if let Some(then_end_idx) = then_end { + edges.push((then_end_idx, i)); + } + } else { + let mut j = i + 1; + while j < n && is_marker(chain[j]) { + j += 1; + } + if j < n { + edges.push((cond, j)); + } else { + edges.push((cond, i)); + } + } } MARKER_LOOP => { - edges.push((prev, i)); loop_stack.push(i); } MARKER_LOOP_BACK => { edges.push((prev, i)); if let Some(header) = loop_stack.last().copied() { - edges.push((i, header)); // back edge + edges.push((i, header)); } } - MARKER_SWITCH_CASE => match switch_entry { - Some(entry) if entry != prev => edges.push((entry, i)), - _ => { - switch_entry = Some(prev); + MARKER_SWITCH_START => { + // Switch start: prev is the discriminant (or previous statement) + // Sequential edge from prev handled above + switch_stack.push(i); // SWITCH_START is the entry + } + MARKER_SWITCH_CASE => { + if let Some(&entry) = switch_stack.last() { + edges.push((entry, i)); + } else { edges.push((prev, i)); } - }, + } MARKER_SWITCH_END => { edges.push((prev, i)); - if chain.get(i + 1) != Some(&MARKER_SWITCH_CASE) { - switch_entry = None; + if (i + 1 >= n || chain[i + 1] != MARKER_SWITCH_CASE) && !switch_stack.is_empty() { + switch_stack.pop(); + } + } + MARKER_SWITCH_CLOSE => { + edges.push((prev, i)); + if !switch_stack.is_empty() { + switch_stack.pop(); + } + } + MARKER_CALL_ENTER => { + call_stack.push(i); + } + MARKER_CALL_EXIT => { + if !call_stack.is_empty() { + call_stack.pop(); } } - _ => edges.push((prev, i)), + MARKER_STMT_END => { + // Statement end: acts as a barrier between statements. + // No structural edges. Sequential edge logic already handles + // the edge from previous statement to STMT_END (if non-marker -> marker). + // We DON'T want edge from STMT_END to next statement. + // The sequential rule `prev_is_marker && !curr_is_marker` would add + // edge from STMT_END to next statement, which we DON'T want. + // Fix: treat STMT_END as a marker that should NOT connect to next. + } + _ => {} } i += 1; } @@ -404,7 +528,7 @@ fn sanitize(s: &str) -> String { #[cfg(test)] mod tests { use super::*; - use codegraph_core::{BranchLabel, ScopeLevel, Symbol, SymbolKind}; + use codegraph_core::{BranchLabel, ScopeLevel, Symbol, SymbolKind, MARKER_SWITCH_CLOSE}; fn flow(chain: Vec, desc: Vec<&str>, calls: Vec) -> FlowResult { FlowResult { @@ -459,6 +583,8 @@ mod tests { effect: EffectType::None, effect_desc: None, args: Vec::new(), + into_var: None, + is_new_var: false, } } @@ -496,13 +622,37 @@ mod tests { assert!(m.contains("c1 --> c2"), "{m}"); assert!(m.contains("c0 --> c3"), "{m}"); assert!(!m.contains("c2 --> c3"), "{m}"); - // cuối nhánh else nhập về BRANCH_END, và BRANCH_END nối tiếp done. - assert!(m.contains("c4 --> c5"), "{m}"); - assert!(m.contains("c0 --> c5"), "{m}"); + // Cả hai nhánh then/else đều hội tụ tại BRANCH_END, rồi tiếp done. + assert!(m.contains("c2 --> c5"), "{m}"); // then → BRANCH_END + assert!(m.contains("c4 --> c5"), "{m}"); // else → BRANCH_END assert!(m.contains("c5 --> c6"), "{m}"); + // Không có cạnh trực tiếp cond → BRANCH_END khi có else (false path đi qua IF_FALSE). + assert!(!m.contains("c0 --> c5"), "{m}"); assert!(m.contains("c1{\"IF_TRUE\"}"), "{m}"); } + #[test] + fn if_without_else_false_branch_skips_branch_end() { + // if (cond) { then() } done() — không có else + let chain = vec![100, MARKER_IF_TRUE, 0, MARKER_BRANCH_END, 101]; + let f = flow( + chain, + vec!["root", "IF_TRUE", "then", "BRANCH_END", "done"], + vec![call(2, "then", None, 3), call(4, "done", Some(102), 5)], + ); + let m = control_flow(&f); + // cond -> IF_TRUE -> then -> BRANCH_END + assert!(m.contains("c0 --> c1"), "{m}"); + assert!(m.contains("c1 --> c2"), "{m}"); + assert!(m.contains("c2 --> c3"), "{m}"); + // False path: cond -> done (SKIP BRANCH_END) + assert!(m.contains("c0 --> c4"), "{m}"); + // BRANCH_END -> done (then branch continues) + assert!(m.contains("c3 --> c4"), "{m}"); + // Không có cond -> BRANCH_END + assert!(!m.contains("c0 --> c3"), "{m}"); + } + #[test] fn loop_back_edge_returns_to_header() { // while (x) { poll() } @@ -523,40 +673,86 @@ mod tests { // switch { case a(); case b(); } done() let chain = vec![ 100, + MARKER_SWITCH_START, MARKER_SWITCH_CASE, 0, MARKER_SWITCH_END, MARKER_SWITCH_CASE, 0, MARKER_SWITCH_END, + MARKER_SWITCH_CLOSE, 101, ]; let f = flow( chain, vec![ "root", + "SWITCH_START", "SWITCH_CASE", "a", "SWITCH_END", "SWITCH_CASE", "b", "SWITCH_END", + "SWITCH_CLOSE", "done", ], vec![ call(2, "a", None, 2), call(5, "b", None, 3), - call(7, "done", Some(102), 4), + call(9, "done", Some(102), 4), ], ); let m = control_flow(&f); - // cả hai case toả từ node entry (c0). - assert!(m.contains("c0 --> c1"), "{m}"); + // cả hai case toả từ node entry (c0 - discriminant). + assert!(m.contains("c1 --> c2"), "{m}"); // SWITCH_START -> first case assert!( - m.contains("c0 --> c4"), + m.contains("c1 --> c5"), "second case should fan out from entry: {m}" ); - assert!(!m.contains("c3 --> c4"), "{m}"); + assert!(!m.contains("c4 --> c5"), "{m}"); // first case end NOT -> second case + } + + #[test] + fn filter_call_enter_exit_stmt_end_markers() { + // Chain with CALL_ENTER/EXIT and STMT_END markers that should be filtered out + let chain = vec![ + 100, + MARKER_CALL_ENTER, + 0, + MARKER_CALL_EXIT, + MARKER_STMT_END, + 101, + ]; + let f = flow( + chain, + vec![ + "root", + "CALL_ENTER", + "call_fn", + "CALL_EXIT", + "STMT_END", + "done", + ], + vec![call(2, "call_fn", None, 3)], + ); + let m = control_flow(&f); + eprintln!("Test mermaid output:\n{}", m); + // CALL_ENTER, CALL_EXIT, STMT_END should be filtered out + // Result should have: root(0) -> call_fn(1) -> done(2) + assert!(m.contains("c0[\"root\"]"), "missing root: {m}"); + // call_fn is external (to_id=None) so gets " · ext" suffix + assert!(m.contains("call_fn · L3 · ext"), "missing call_fn: {m}"); + // done has no line in the test (line 4 is from the chain desc) + assert!(m.contains("c2[\"done\"]"), "missing done: {m}"); + // No MARKER nodes for CALL_ENTER/EXIT/STMT_END + assert!( + !m.contains("MARKER"), + "should not contain filtered markers: {m}" + ); + // Edges: root -> call_fn -> done + assert!(m.contains("c0 --> c1"), "missing edge c0->c1: {m}"); + assert!(m.contains("c1 --> c2"), "missing edge c1->c2: {m}"); } #[test] diff --git a/crates/codegraph-api/tests/api.rs b/crates/codegraph-api/tests/api.rs index 86a41d36ee..a2412e6cc3 100644 --- a/crates/codegraph-api/tests/api.rs +++ b/crates/codegraph-api/tests/api.rs @@ -58,6 +58,8 @@ async fn seed_index(path: &str) -> (u64, u64, u64) { effect_desc: None, target_class: None, target_method: None, + into_var: None, + is_new_var: false, }], }; idx.ingest(&[r]).await.unwrap(); @@ -353,6 +355,8 @@ async fn seed_references(db: &str, count: usize) { effect_desc: None, target_class: None, target_method: None, + into_var: None, + is_new_var: false, }], }); } diff --git a/crates/codegraph-bench/src/bin/mem.rs b/crates/codegraph-bench/src/bin/mem.rs index 90b913cead..add64df42f 100644 --- a/crates/codegraph-bench/src/bin/mem.rs +++ b/crates/codegraph-bench/src/bin/mem.rs @@ -364,6 +364,8 @@ fn synthetic_parse_result(n: usize, fanout: usize, shape: Shape) -> codegraph_gr effect_desc: None, target_class: None, target_method: None, + into_var: None, + is_new_var: false, }); } } diff --git a/crates/codegraph-binary/src/extract.rs b/crates/codegraph-binary/src/extract.rs index 5a0f2152db..90674d9891 100644 --- a/crates/codegraph-binary/src/extract.rs +++ b/crates/codegraph-binary/src/extract.rs @@ -550,6 +550,8 @@ fn build_chains_with_cfg( effect_desc: None, target_class: None, target_method: None, + into_var: None, + is_new_var: false, }); } "cjmp" => { @@ -631,6 +633,8 @@ fn build_chains_from_graph( effect_desc: None, target_class: None, target_method: None, + into_var: None, + is_new_var: false, }); } chains.insert(caller_id, chain); diff --git a/crates/codegraph-core/src/lib.rs b/crates/codegraph-core/src/lib.rs index dbc2aba63d..3c213555b8 100644 --- a/crates/codegraph-core/src/lib.rs +++ b/crates/codegraph-core/src/lib.rs @@ -15,7 +15,8 @@ pub use semgraph::{ CallSiteResult, ClassInfo, DbStats as SemgraphStats, DependenciesReport, Dependency, EffectCallPattern, EffectRule, EffectType, FileInfo, FlowCall, FlowResult, FunctionScope, MemberInfo, ResolveResult, ScopeLevel, SearchFlowResult, Symbol, SymbolId, SymbolKind, - SymbolMatch, MARKER_BRANCH_END, MARKER_BREAK, MARKER_CONTINUE, MARKER_IF_FALSE, MARKER_IF_TRUE, - MARKER_LOOP, MARKER_LOOP_BACK, MARKER_REC_CALL, MARKER_RETURN, MARKER_SWITCH_CASE, - MARKER_SWITCH_END, MARKER_THROW, SYMBOL_BASE, + SymbolMatch, MARKER_BRANCH_END, MARKER_BREAK, MARKER_CALL_ENTER, MARKER_CALL_EXIT, + MARKER_CONTINUE, MARKER_IF_FALSE, MARKER_IF_TRUE, MARKER_LOOP, MARKER_LOOP_BACK, + MARKER_REC_CALL, MARKER_RETURN, MARKER_STMT_END, MARKER_SWITCH_CASE, MARKER_SWITCH_CLOSE, + MARKER_SWITCH_END, MARKER_SWITCH_START, MARKER_THROW, SYMBOL_BASE, }; diff --git a/crates/codegraph-core/src/semgraph.rs b/crates/codegraph-core/src/semgraph.rs index 8bf0ba209e..f1968ea5d8 100644 --- a/crates/codegraph-core/src/semgraph.rs +++ b/crates/codegraph-core/src/semgraph.rs @@ -53,6 +53,21 @@ pub const MARKER_CONTINUE: u64 = 11; /// Marker: throw/raise exception. pub const MARKER_THROW: u64 = 12; +/// Marker: bắt đầu call expression (dùng cho nested call depth). +pub const MARKER_CALL_ENTER: u64 = 17; + +/// Marker: kết thúc call expression. +pub const MARKER_CALL_EXIT: u64 = 18; + +/// Marker: đóng switch expression (tách các switch riêng biệt). +pub const MARKER_SWITCH_CLOSE: u64 = 19; + +/// Marker: bắt đầu switch expression (dùng cho discriminant text). +pub const MARKER_SWITCH_START: u64 = 20; + +/// Marker: kết thúc statement (tách các statements trong block). +pub const MARKER_STMT_END: u64 = 21; + /// `true` nếu `id` là một fixed marker (nằm trong vùng reserved). #[inline] pub fn is_marker(id: u64) -> bool { @@ -74,6 +89,11 @@ pub fn marker_name(id: u64) -> Option<&'static str> { MARKER_BREAK => "BREAK", MARKER_CONTINUE => "CONTINUE", MARKER_THROW => "THROW", + MARKER_CALL_ENTER => "CALL_ENTER", + MARKER_CALL_EXIT => "CALL_EXIT", + MARKER_SWITCH_CLOSE => "SWITCH_CLOSE", + MARKER_SWITCH_START => "SWITCH_START", + MARKER_STMT_END => "STMT_END", _ => return None, }) } @@ -94,6 +114,11 @@ pub fn marker_id(name: &str) -> Option { "BREAK" => MARKER_BREAK, "CONTINUE" => MARKER_CONTINUE, "THROW" => MARKER_THROW, + "CALL_ENTER" => MARKER_CALL_ENTER, + "CALL_EXIT" => MARKER_CALL_EXIT, + "SWITCH_CLOSE" => MARKER_SWITCH_CLOSE, + "SWITCH_START" => MARKER_SWITCH_START, + "STMT_END" => MARKER_STMT_END, _ => return None, }) } @@ -336,6 +361,12 @@ pub struct CallRecord { /// Gợi ý structural khi resolve (VD Java class literal). pub target_class: Option, pub target_method: Option, + /// Biến gán kết quả call này (VD: `let cli = Cli::parse()` → `into_var = Some("cli")`). + #[serde(default)] + pub into_var: Option, + /// `true` nếu assignment tạo biến mới, `false` nếu update biến đã có. + #[serde(default)] + pub is_new_var: bool, } /// Giá trị của inverted index `call name → call sites` (dùng cho query @@ -407,6 +438,12 @@ pub struct FlowCall { pub effect_desc: Option, #[serde(default)] pub args: Vec, + /// Biến gán kết quả call này (VD: `let cli = Cli::parse()` → `into_var = Some("cli")`). + #[serde(default)] + pub into_var: Option, + /// `true` nếu assignment tạo biến mới, `false` nếu update biến đã có. + #[serde(default)] + pub is_new_var: bool, } /// Kết quả resolve symbol theo id/name — `ambiguous=true` khi name trùng nhiều diff --git a/crates/codegraph-extract/src/bingraph.rs b/crates/codegraph-extract/src/bingraph.rs index fb884e280c..a2615980ff 100644 --- a/crates/codegraph-extract/src/bingraph.rs +++ b/crates/codegraph-extract/src/bingraph.rs @@ -857,6 +857,8 @@ impl BinaryGraph { .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(), + into_var: rec.and_then(|r| r.into_var.clone()), + is_new_var: rec.map(|r| r.is_new_var).unwrap_or(false), }); } Ok(FlowResult { @@ -981,6 +983,8 @@ mod tests { effect_desc: None, target_class: None, target_method: None, + into_var: None, + is_new_var: false, }, CallRecord { caller_id: sid(1), @@ -994,6 +998,8 @@ mod tests { effect_desc: None, target_class: None, target_method: None, + into_var: None, + is_new_var: false, }, ], } diff --git a/crates/codegraph-extract/src/languages/common.rs b/crates/codegraph-extract/src/languages/common.rs index 6117ba347b..ec6c90f9a0 100644 --- a/crates/codegraph-extract/src/languages/common.rs +++ b/crates/codegraph-extract/src/languages/common.rs @@ -16,8 +16,10 @@ use crate::languages::effects::classify_effect; use codegraph_core::{ Annotation, CallRecord, Result, ScopeLevel, Symbol, SymbolKind, MARKER_BRANCH_END, - MARKER_BREAK, MARKER_CONTINUE, MARKER_IF_FALSE, MARKER_IF_TRUE, MARKER_LOOP, MARKER_LOOP_BACK, - MARKER_RETURN, MARKER_SWITCH_CASE, MARKER_SWITCH_END, MARKER_THROW, SYMBOL_BASE, + MARKER_BREAK, MARKER_CALL_ENTER, MARKER_CALL_EXIT, MARKER_CONTINUE, MARKER_IF_FALSE, + MARKER_IF_TRUE, MARKER_LOOP, MARKER_LOOP_BACK, MARKER_RETURN, MARKER_STMT_END, + MARKER_SWITCH_CASE, MARKER_SWITCH_CLOSE, MARKER_SWITCH_END, MARKER_SWITCH_START, MARKER_THROW, + SYMBOL_BASE, }; use codegraph_graph::ParseResult; use std::collections::HashMap; @@ -622,10 +624,12 @@ fn walk_chain( // 1. Call sites. if let Some(rule) = ctx.spec.calls.iter().find(|r| r.kind == k) { + chain_push(ctx, MARKER_CALL_ENTER); emit_call(ctx, node, rule, in_loop, condition); for ch in named_children(node) { walk_chain(ctx, &ch, depth + 1, in_loop, None); } + chain_push(ctx, MARKER_CALL_EXIT); return; } @@ -718,10 +722,24 @@ fn walk_chain( // 4. Switch. if ctx.spec.switch_kinds.contains(&k) { - // Discriminant (`switch (getType(x))`) — emit calls trước các case. - if let Some(cn) = node.child_by_field_name(ctx.spec.if_cond_field) { + // SWITCH_START: emit before discriminant, will hold discriminant text as label. + chain_push(ctx, MARKER_SWITCH_START); + // Discriminant: find the expression after "match" keyword, before match_block. + // match_expression structure: "match" "{" match_arm+ "}" + let discriminant_node = node + .children(&mut node.walk()) + .find(|c| c.is_named() && c.kind() != "match_block"); + if let Some(cn) = discriminant_node { walk_chain(ctx, &cn, depth + 1, in_loop, condition.clone()); } + // Emit discriminant text as branch label on SWITCH_START. + if let Some(cn) = discriminant_node { + if let Some(dt) = text(&cn, ctx.src) { + if !dt.is_empty() { + emit_branch_label(ctx, &dt); + } + } + } for case in switch_cases(node, ctx.spec) { chain_push(ctx, MARKER_SWITCH_CASE); // Nhãn trigger của case (`Cmd::Init`, `1`, `_`, `'store'`). @@ -733,6 +751,7 @@ fn walk_chain( walk_block(ctx, &case, depth + 1, in_loop, condition.clone()); chain_push(ctx, MARKER_SWITCH_END); } + chain_push(ctx, MARKER_SWITCH_CLOSE); return; } @@ -805,9 +824,54 @@ fn walk_chain( return; } + // 9. Let declaration (biến mới: `let x = expr`). + if k == "let_declaration" { + // Extract variable name from pattern. + let var = node + .child_by_field_name("pattern") + .and_then(|p| first_identifier(&p)) + .and_then(|n| text(&n, ctx.src)); + // Walk the initializer (rhs) to emit calls. + // Record call count before walking to detect if new calls were emitted. + let calls_before = ctx.calls.len(); + if let Some(value) = node.child_by_field_name("value") { + walk_chain(ctx, &value, depth + 1, in_loop, condition.clone()); + // Only tag if new call records were emitted during the walk. + if ctx.calls.len() > calls_before { + if let Some(rec) = ctx.calls.last_mut() { + if let Some(v) = var { + rec.into_var = Some(v); + rec.is_new_var = true; + } + } + } + } + return; + } + + // 10. Assignment expression (cập nhật biến: `x = expr`). + if k == "assignment_expression" { + let target = node.child_by_field_name("left"); + let value = node.child_by_field_name("right"); + let var = target.and_then(|t| text(&t, ctx.src)); + if let Some(v) = value { + let calls_before = ctx.calls.len(); + walk_chain(ctx, &v, depth + 1, in_loop, condition.clone()); + if ctx.calls.len() > calls_before { + if let Some(rec) = ctx.calls.last_mut() { + if let Some(var_name) = var { + rec.into_var = Some(var_name); + rec.is_new_var = false; + } + } + } + } + return; + } + // 8. Default: recurse. for ch in named_children(node) { - walk_chain(ctx, &ch, depth + 1, in_loop, condition.clone()); + walk_chain(ctx, &ch, depth, in_loop, condition.clone()); } } @@ -855,8 +919,24 @@ fn walk_block( in_loop: u32, condition: Option, ) { - for ch in named_children(node) { - walk_chain(ctx, &ch, depth, in_loop, condition.clone()); + // Only emit STMT_END between statements in actual block-like nodes. + // Match arms, if branches, etc. have their own structural markers. + let is_stmt_block = matches!( + node.kind(), + "block" + | "block_expression" + | "statement_block" + | "function_body" + | "program" + | "module" + | "source_file" + ); + let children: Vec<_> = named_children(node).into_iter().collect(); + for (i, ch) in children.iter().enumerate() { + walk_chain(ctx, ch, depth, in_loop, condition.clone()); + if is_stmt_block && i + 1 < children.len() { + chain_push(ctx, MARKER_STMT_END); + } } } @@ -1002,6 +1082,8 @@ fn emit_case_label(ctx: &mut ChainCtx, case: &Node, in_loop: u32, condition: Opt effect_desc, target_class: None, target_method: None, + into_var: None, + is_new_var: false, }); } @@ -1053,6 +1135,8 @@ fn emit_branch_label(ctx: &mut ChainCtx, label: &str) { effect_desc: None, target_class: None, target_method: None, + into_var: None, + is_new_var: false, }); } @@ -1129,6 +1213,8 @@ fn emit_call( effect_desc, target_class, target_method, + into_var: None, + is_new_var: false, }); } diff --git a/crates/codegraph-extract/src/languages/csharp.rs b/crates/codegraph-extract/src/languages/csharp.rs index 63bcf7ca36..1a95055e5c 100644 --- a/crates/codegraph-extract/src/languages/csharp.rs +++ b/crates/codegraph-extract/src/languages/csharp.rs @@ -8,7 +8,7 @@ fn ts_language() -> tree_sitter::Language { /// `new List(...)` — tên class gốc (strip generic args để resolve được). fn new_call_name(node: &Node, src: &[u8]) -> Option { - let tn = node + let tn: String = node .child_by_field_name("type") .and_then(|t| text(&t, src))?; let base = tn.split('<').next().unwrap_or(&tn); diff --git a/crates/codegraph-extract/tests/chains.rs b/crates/codegraph-extract/tests/chains.rs index d6608c53e1..8556d00cca 100644 --- a/crates/codegraph-extract/tests/chains.rs +++ b/crates/codegraph-extract/tests/chains.rs @@ -43,11 +43,21 @@ fn walk(lang: &str, src: &str) -> Vec { return s.name.clone(); } } - res.calls + let call_name = res + .calls .iter() .find(|c| c.position == i) .map(|c| c.call_name.clone()) - .unwrap_or_else(|| format!("?{id}")) + .unwrap_or_else(|| format!("?{id}")); + // Append variable assignment if present + if let Some(c) = res.calls.iter().find(|c| c.position == i) { + if let Some(var) = &c.into_var { + if !var.is_empty() { + return format!("{call_name} → {var}"); + } + } + } + call_name }; chain .iter() @@ -76,12 +86,16 @@ def process(x): [ "[LOOP]", "[IF_TRUE]", + "[CALL_ENTER]", "save", + "[CALL_EXIT]", "[IF_FALSE]", + "[CALL_ENTER]", "skip", + "[CALL_EXIT]", "[BRANCH_END]", "[LOOP_BACK]", - "[RETURN]" + "[RETURN]", ] ); } @@ -105,12 +119,20 @@ def process(x): assert_eq!( c, [ + "[CALL_ENTER]", "risky", + "[CALL_EXIT]", "[IF_TRUE]", + "[CALL_ENTER]", "handle", + "[CALL_EXIT]", "[BRANCH_END]", + "[CALL_ENTER]", "ok", + "[CALL_EXIT]", + "[CALL_ENTER]", "cleanup", + "[CALL_EXIT]", ] ); } @@ -131,12 +153,18 @@ def process(x): assert_eq!( c, [ + "[SWITCH_START]", "[SWITCH_CASE]", + "[CALL_ENTER]", "one", + "[CALL_EXIT]", "[SWITCH_END]", "[SWITCH_CASE]", + "[CALL_ENTER]", "other", - "[SWITCH_END]" + "[CALL_EXIT]", + "[SWITCH_END]", + "[SWITCH_CLOSE]", ] ); } @@ -161,12 +189,18 @@ class Foo { assert_eq!( c, [ + "[CALL_ENTER]", "obj.run", + "[CALL_EXIT]", "[IF_TRUE]", + "[CALL_ENTER]", "this.helper", + "[CALL_EXIT]", "[IF_FALSE]", + "[CALL_ENTER]", "fallback", - "[BRANCH_END]" + "[CALL_EXIT]", + "[BRANCH_END]", ] ); } @@ -194,14 +228,22 @@ func process(u *User) { assert_eq!( c, [ + "[SWITCH_START]", "[SWITCH_CASE]", + "[CALL_ENTER]", "fmt.Println", + "[CALL_EXIT]", "[SWITCH_END]", "[SWITCH_CASE]", + "[CALL_ENTER]", "fmt.Println", + "[CALL_EXIT]", "[SWITCH_END]", + "[SWITCH_CLOSE]", "[LOOP]", + "[CALL_ENTER]", "save", + "[CALL_EXIT]", "[LOOP_BACK]", ] ); @@ -227,12 +269,18 @@ end c, [ "[IF_TRUE]", + "[CALL_ENTER]", "validate", + "[CALL_EXIT]", "[IF_TRUE]", + "[CALL_ENTER]", "warn", + "[CALL_EXIT]", + "[CALL_ENTER]", "fail", + "[CALL_EXIT]", + "[BRANCH_END]", "[BRANCH_END]", - "[BRANCH_END]" ] ); } @@ -258,7 +306,9 @@ int add(int a, int b) { "[LOOP]", "[IF_TRUE]", "[RETURN]", + "[CALL_ENTER]", "compute", + "[CALL_EXIT]", "[BRANCH_END]", "[LOOP_BACK]", "[RETURN]", @@ -289,14 +339,22 @@ func process(x: Int) -> Int { c, [ "[LOOP]", + "[CALL_ENTER]", "save", + "[CALL_EXIT]", "[LOOP_BACK]", + "[SWITCH_START]", "[SWITCH_CASE]", + "[CALL_ENTER]", "run", + "[CALL_EXIT]", "[SWITCH_END]", "[SWITCH_CASE]", + "[CALL_ENTER]", "stop", + "[CALL_EXIT]", "[SWITCH_END]", + "[SWITCH_CLOSE]", "[RETURN]", ] ); @@ -323,15 +381,23 @@ function f(y) { c, [ "[LOOP]", + "[CALL_ENTER]", "qux", + "[CALL_EXIT]", "[LOOP_BACK]", + "[SWITCH_START]", "[SWITCH_CASE]", + "[CALL_ENTER]", "one", + "[CALL_EXIT]", "[BREAK]", "[SWITCH_END]", "[SWITCH_CASE]", + "[CALL_ENTER]", "two", + "[CALL_EXIT]", "[SWITCH_END]", + "[SWITCH_CLOSE]", "[RETURN]", ] ); @@ -353,7 +419,65 @@ class Service { } "#, ); - assert_eq!(c, ["this.repo.find", "[IF_TRUE]", "log", "[BRANCH_END]"]); + assert_eq!( + c, + [ + "[CALL_ENTER]", + "this.repo.find", + "[CALL_EXIT]", + "[IF_TRUE]", + "[CALL_ENTER]", + "log", + "[CALL_EXIT]", + "[BRANCH_END]", + ] + ); +} + +#[test] +fn rust_let_declaration() { + let c = walk( + "rust", + r#" +fn f() { + let x = compute(); + let y = x + 1; + return y; +} +"#, + ); + assert_eq!( + c, + ["[CALL_ENTER]", "compute → x", "[CALL_EXIT]", "[RETURN]"] + ); +} + +#[test] +fn rust_multiple_let_declarations() { + let c = walk( + "rust", + r#" +fn f() { + let a = foo(); + let b = bar(); + let c = baz(); +} +"#, + ); + assert_eq!( + c, + [ + "[CALL_ENTER]", + "foo → a", + "[CALL_EXIT]", + "[CALL_ENTER]", + "bar → b", + "[CALL_EXIT]", + "[CALL_ENTER]", + "baz → c", + "[CALL_EXIT]" + ] + ); } #[test] @@ -373,12 +497,18 @@ fn f(x: i32) -> i32 { assert_eq!( c, [ + "[SWITCH_START]", "[SWITCH_CASE]", + "[CALL_ENTER]", "one", + "[CALL_EXIT]", "[SWITCH_END]", "[SWITCH_CASE]", + "[CALL_ENTER]", "other", + "[CALL_EXIT]", "[SWITCH_END]", + "[SWITCH_CLOSE]", "[RETURN]" ] ); @@ -402,13 +532,19 @@ class Foo { assert_eq!( c, [ + "[SWITCH_START]", "[SWITCH_CASE]", + "[CALL_ENTER]", "a", + "[CALL_EXIT]", "[BREAK]", "[SWITCH_END]", "[SWITCH_CASE]", + "[CALL_ENTER]", "b", - "[SWITCH_END]" + "[CALL_EXIT]", + "[SWITCH_END]", + "[SWITCH_CLOSE]", ] ); } @@ -435,14 +571,20 @@ end c, [ "[IF_TRUE]", + "[CALL_ENTER]", "validate", + "[CALL_EXIT]", "[IF_FALSE]", + "[CALL_ENTER]", "fail", + "[CALL_EXIT]", "[BRANCH_END]", "[LOOP]", + "[CALL_ENTER]", "save", + "[CALL_EXIT]", "[LOOP_BACK]", - "[RETURN]" + "[RETURN]", ] ); } @@ -467,10 +609,16 @@ function process($x) { c, [ "[LOOP]", + "[CALL_ENTER]", "save", + "[CALL_EXIT]", "[LOOP_BACK]", + "[CALL_ENTER]", "obj.method", + "[CALL_EXIT]", + "[CALL_ENTER]", "self.run", + "[CALL_EXIT]", "[RETURN]" ] ); @@ -493,13 +641,19 @@ def f(x: Int) = { assert_eq!( c, [ + "[SWITCH_START]", "[SWITCH_CASE]", + "[CALL_ENTER]", "one", + "[CALL_EXIT]", "[SWITCH_END]", "[SWITCH_CASE]", + "[CALL_ENTER]", "other", + "[CALL_EXIT]", "[SWITCH_END]", - "[RETURN]" + "[SWITCH_CLOSE]", + "[RETURN]", ] ); } @@ -523,7 +677,9 @@ int add(int a, int b) { [ "[IF_TRUE]", "[RETURN]", + "[CALL_ENTER]", "compute", + "[CALL_EXIT]", "[IF_FALSE]", "[RETURN]", "[BRANCH_END]" @@ -551,14 +707,26 @@ int f(int x) { c, [ "[IF_TRUE]", + "[CALL_ENTER]", "b", + "[CALL_ENTER]", "c", + "[CALL_EXIT]", + "[CALL_EXIT]", + "[CALL_ENTER]", "d", + "[CALL_EXIT]", "[BRANCH_END]", "[LOOP]", + "[CALL_ENTER]", "b", + "[CALL_ENTER]", "c", + "[CALL_EXIT]", + "[CALL_EXIT]", + "[CALL_ENTER]", "d", + "[CALL_EXIT]", "[LOOP_BACK]", "[RETURN]", ] @@ -577,7 +745,26 @@ int f(int x) { } "#, ); - assert_eq!(c, ["[LOOP]", "e", "a", "b", "c", "[LOOP_BACK]", "[RETURN]"]); + assert_eq!( + c, + [ + "[LOOP]", + "[CALL_ENTER]", + "e", + "[CALL_EXIT]", + "[CALL_ENTER]", + "a", + "[CALL_EXIT]", + "[CALL_ENTER]", + "b", + "[CALL_ENTER]", + "c", + "[CALL_EXIT]", + "[CALL_EXIT]", + "[LOOP_BACK]", + "[RETURN]", + ] + ); } /// Text condition của `if` được giữ làm metadata (CallRecord.condition của call @@ -634,16 +821,32 @@ def f(x): c, [ "[IF_TRUE]", + "[CALL_ENTER]", "a", + "[CALL_EXIT]", + "[CALL_ENTER]", "b", + "[CALL_ENTER]", "c", + "[CALL_EXIT]", + "[CALL_EXIT]", + "[CALL_ENTER]", "d", + "[CALL_EXIT]", "[BRANCH_END]", "[LOOP]", + "[CALL_ENTER]", "a", + "[CALL_EXIT]", + "[CALL_ENTER]", "b", + "[CALL_ENTER]", "c", + "[CALL_EXIT]", + "[CALL_EXIT]", + "[CALL_ENTER]", "d", + "[CALL_EXIT]", "[LOOP_BACK]", "[RETURN]", ] @@ -665,7 +868,26 @@ func f() { } "#, ); - assert_eq!(c, ["[LOOP]", "a", "b", "c", "d", "[LOOP_BACK]", "[RETURN]"]); + assert_eq!( + c, + [ + "[LOOP]", + "[CALL_ENTER]", + "a", + "[CALL_EXIT]", + "[CALL_ENTER]", + "b", + "[CALL_ENTER]", + "c", + "[CALL_EXIT]", + "[CALL_EXIT]", + "[CALL_ENTER]", + "d", + "[CALL_EXIT]", + "[LOOP_BACK]", + "[RETURN]", + ] + ); } /// Switch discriminant (`switch (getType(x))`) cũng vào chain trước các case. @@ -687,14 +909,22 @@ class Foo { assert_eq!( c, [ + "[SWITCH_START]", + "[CALL_ENTER]", "getType", + "[CALL_EXIT]", "[SWITCH_CASE]", + "[CALL_ENTER]", "one", + "[CALL_EXIT]", "[BREAK]", "[SWITCH_END]", "[SWITCH_CASE]", + "[CALL_ENTER]", "other", + "[CALL_EXIT]", "[SWITCH_END]", + "[SWITCH_CLOSE]", ] ); } @@ -715,7 +945,18 @@ class Foo { } "#, ); - assert_eq!(c, ["[RETURN]", "a.run(abc.class).exec", "a.run"]); + assert_eq!( + c, + [ + "[RETURN]", + "[CALL_ENTER]", + "a.run(abc.class).exec", + "[CALL_ENTER]", + "a.run", + "[CALL_EXIT]", + "[CALL_EXIT]", + ] + ); } /// Bug A: string-literal case labels (`case 'optimize_text':`) — dispatch key @@ -739,6 +980,7 @@ function dispatch(name: string): number { assert_eq!( c, [ + "[SWITCH_START]", "[SWITCH_CASE]", "optimize_text", "[RETURN]", @@ -750,6 +992,7 @@ function dispatch(name: string): number { "[SWITCH_CASE]", "[RETURN]", "[SWITCH_END]", + "[SWITCH_CLOSE]", ] ); } @@ -901,19 +1144,25 @@ function runStorage(op: string, s: Store): void { assert_eq!( c, [ + "[SWITCH_START]", "[SWITCH_CASE]", "store", + "[CALL_ENTER]", "s.save", + "[CALL_EXIT]", "[BREAK]", "[SWITCH_END]", "[SWITCH_CASE]", "retrieve", + "[CALL_ENTER]", "s.get", + "[CALL_EXIT]", "[BREAK]", "[SWITCH_END]", "[SWITCH_CASE]", "[BREAK]", "[SWITCH_END]", + "[SWITCH_CLOSE]", ] ); } @@ -966,13 +1215,13 @@ a(); #[test] fn js_const_arrow_chain() { let c = walk("javascript", "const f = () => g();\n"); - assert_eq!(c, ["g"]); + assert_eq!(c, ["[CALL_ENTER]", "g", "[CALL_EXIT]"]); } #[test] fn ts_const_arrow_chain() { let c = walk("typescript", "const f = (): void => g();\n"); - assert_eq!(c, ["g"]); + assert_eq!(c, ["[CALL_ENTER]", "g", "[CALL_EXIT]"]); } #[test] @@ -1124,3 +1373,99 @@ fn php_assigned_anonymous_and_arrow() { .iter() .any(|c| c.caller_id == h.id && c.call_name == "h2")); } + +#[test] +fn rust_let_declaration_method_chain() { + let c = walk( + "rust", + r#" +fn f() { + let __t = cg_t!("rebuild TOTAL"); + let __t_load = cg_t!("load_all_symbols"); + let symbols = self.storage.read().await.load_all_symbols().await.map_err(serr)?; + cg_d!(__t_load, "load_all_symbols"); + self.files = self.storage.read().await.load_all_files().await.map_err(serr)?; +} +"#, + ); + assert_eq!( + c, + [ + "[CALL_ENTER]", + "self.storage.read().await.load_all_symbols().await.map_err", + "[CALL_ENTER]", + "self.storage.read().await.load_all_symbols", + "[CALL_ENTER]", + "self.storage.read → symbols", + "[CALL_EXIT]", + "[CALL_EXIT]", + "[CALL_EXIT]", + "[CALL_ENTER]", + "self.storage.read().await.load_all_files().await.map_err", + "[CALL_ENTER]", + "self.storage.read().await.load_all_files", + "[CALL_ENTER]", + "self.storage.read → self.files", + "[CALL_EXIT]", + "[CALL_EXIT]", + "[CALL_EXIT]", + ] + ); +} + +#[test] +fn rust_assignment_expression_no_call_rhs() { + let c = walk( + "rust", + r#" +fn f() { + let x = compute(); + x = 5; + let y = foo(); +} +"#, + ); + // x = 5 has no call on RHS, so previous call (compute) should NOT get → x + // foo() should get → y (let declaration) + assert_eq!( + c, + [ + "[CALL_ENTER]", + "compute → x", + "[CALL_EXIT]", + "[CALL_ENTER]", + "foo → y", + "[CALL_EXIT]", + ] + ); +} + +#[test] +fn rust_assignment_expression_with_call_rhs() { + let c = walk( + "rust", + r#" +fn f() { + let x = compute(); + x = other(); + let y = foo(); +} +"#, + ); + // x = other() has a call on RHS, so other() should get → x (is_new_var = false) + // foo() should get → y + assert_eq!( + c, + [ + "[CALL_ENTER]", + "compute → x", + "[CALL_EXIT]", + "[CALL_ENTER]", + "other → x", + "[CALL_EXIT]", + "[CALL_ENTER]", + "foo → y", + "[CALL_EXIT]", + ] + ); +} diff --git a/crates/codegraph-graph/src/diff.rs b/crates/codegraph-graph/src/diff.rs index bf7eaf55c5..13d185f117 100644 --- a/crates/codegraph-graph/src/diff.rs +++ b/crates/codegraph-graph/src/diff.rs @@ -628,6 +628,8 @@ index 0000000..2222222 effect_desc: None, target_class: None, target_method: None, + into_var: None, + is_new_var: false, }]; let r = result( "a.ts", diff --git a/crates/codegraph-graph/src/lib.rs b/crates/codegraph-graph/src/lib.rs index eeb1040e7b..e86ae2fe4f 100644 --- a/crates/codegraph-graph/src/lib.rs +++ b/crates/codegraph-graph/src/lib.rs @@ -1986,6 +1986,8 @@ impl GraphIndex { effect: rec.effect, effect_desc: rec.effect_desc.clone(), args: rec.arg_exprs.clone(), + into_var: rec.into_var.clone(), + is_new_var: rec.is_new_var, }); } continue; @@ -2005,6 +2007,8 @@ impl GraphIndex { effect: rec.map(|r| r.effect).unwrap_or_default(), effect_desc: rec.and_then(|r| r.effect_desc.clone()), args: rec.map(|r| r.arg_exprs.clone()).unwrap_or_default(), + into_var: rec.and_then(|r| r.into_var.clone()), + is_new_var: rec.map(|r| r.is_new_var).unwrap_or(false), }); } @@ -3218,6 +3222,8 @@ mod tests { effect_desc: None, target_class: None, target_method: None, + into_var: None, + is_new_var: false, }, CallRecord { caller_id: SYMBOL_BASE, @@ -3231,6 +3237,8 @@ mod tests { effect_desc: None, target_class: None, target_method: None, + into_var: None, + is_new_var: false, }, ]; let r = result( @@ -3272,6 +3280,8 @@ mod tests { effect_desc: None, target_class: None, target_method: None, + into_var: None, + is_new_var: false, }]; let r = result( "f.ts", @@ -3356,6 +3366,8 @@ mod tests { effect_desc: None, target_class: None, target_method: None, + into_var: None, + is_new_var: false, }]; let r = result( "f.ts", @@ -3391,6 +3403,8 @@ mod tests { effect_desc: None, target_class: None, target_method: None, + into_var: None, + is_new_var: false, }; let mut idx = GraphIndex::in_memory(); let first = result( @@ -4032,6 +4046,8 @@ mod tests { effect_desc: None, target_class: None, target_method: None, + into_var: None, + is_new_var: false, }, CallRecord { caller_id: SYMBOL_BASE, @@ -4045,6 +4061,8 @@ mod tests { effect_desc: None, target_class: None, target_method: None, + into_var: None, + is_new_var: false, }, // Internal call: class "OrderService" trong repo, method getOrders. CallRecord { @@ -4059,6 +4077,8 @@ mod tests { effect_desc: None, target_class: None, target_method: None, + into_var: None, + is_new_var: false, }, ]; let mut cls = sym("svc.rs", "OrderService", SYMBOL_BASE + 1); diff --git a/crates/codegraph-graph/tests/lmdb.rs b/crates/codegraph-graph/tests/lmdb.rs index d25bc27f23..1eed149067 100644 --- a/crates/codegraph-graph/tests/lmdb.rs +++ b/crates/codegraph-graph/tests/lmdb.rs @@ -67,6 +67,8 @@ async fn index_ingest_reopen_roundtrip() { effect_desc: None, target_class: None, target_method: None, + into_var: None, + is_new_var: false, }]; let r = result( "a.ts", @@ -267,6 +269,8 @@ async fn long_path_and_call_name_survive_roundtrip() { effect_desc: None, target_class: None, target_method: None, + into_var: None, + is_new_var: false, }]; let r = result( &long_path, diff --git a/crates/codegraph-graph/tests/rdbms.rs b/crates/codegraph-graph/tests/rdbms.rs index c6b60d8139..5c0fbb9e2e 100644 --- a/crates/codegraph-graph/tests/rdbms.rs +++ b/crates/codegraph-graph/tests/rdbms.rs @@ -93,6 +93,8 @@ async fn rdbms_ingest_reopen_roundtrip() { effect_desc: None, target_class: None, target_method: None, + into_var: None, + is_new_var: false, }]; let r = result( "a.ts", diff --git a/crates/codegraph-graph/tests/redis.rs b/crates/codegraph-graph/tests/redis.rs index f2da02e6e8..fe897e89cd 100644 --- a/crates/codegraph-graph/tests/redis.rs +++ b/crates/codegraph-graph/tests/redis.rs @@ -87,6 +87,8 @@ async fn redis_ingest_reopen_roundtrip() { effect_desc: None, target_class: None, target_method: None, + into_var: None, + is_new_var: false, }]; let r = result( "a.ts", diff --git a/crates/codegraph-graph/tests/sqlite.rs b/crates/codegraph-graph/tests/sqlite.rs index b450ba2b14..6a3ff72c65 100644 --- a/crates/codegraph-graph/tests/sqlite.rs +++ b/crates/codegraph-graph/tests/sqlite.rs @@ -74,6 +74,8 @@ async fn index_ingest_reopen_roundtrip() { effect_desc: None, target_class: None, target_method: None, + into_var: None, + is_new_var: false, }]; let r = result( "a.ts", @@ -347,6 +349,8 @@ async fn external_qualified_call_not_linked_to_local_variable() { effect_desc: None, target_class: None, target_method: None, + into_var: None, + is_new_var: false, }]; let r = result( "UserController.java", diff --git a/crates/codegraph-sboxes/tests/control_flow.rs b/crates/codegraph-sboxes/tests/control_flow.rs index d96c587c7c..57fbb1c85a 100644 --- a/crates/codegraph-sboxes/tests/control_flow.rs +++ b/crates/codegraph-sboxes/tests/control_flow.rs @@ -46,6 +46,8 @@ fn rec(caller_id: u64, pos: usize, name: &str, args: usize) -> CallRecord { effect_desc: None, target_class: None, target_method: None, + into_var: None, + is_new_var: false, } } diff --git a/crates/codegraph-sboxes/tests/end_to_end.rs b/crates/codegraph-sboxes/tests/end_to_end.rs index dc348c9bc3..8e04883811 100644 --- a/crates/codegraph-sboxes/tests/end_to_end.rs +++ b/crates/codegraph-sboxes/tests/end_to_end.rs @@ -42,6 +42,8 @@ fn rec(caller_id: u64, pos: usize, name: &str, args: usize) -> CallRecord { effect_desc: None, target_class: None, target_method: None, + into_var: None, + is_new_var: false, } } diff --git a/crates/codegraph-web/assets/.gitkeep b/crates/codegraph-web/assets/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packaging/aur/codegraph-rs-bin/PKGBUILD b/packaging/aur/codegraph-rs-bin/PKGBUILD index b3b71a4e9f..d2c2a86e60 100644 --- a/packaging/aur/codegraph-rs-bin/PKGBUILD +++ b/packaging/aur/codegraph-rs-bin/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: Hung Pham pkgname=codegraph-rs-bin -pkgver=2.2.8 +pkgver=2.2.9 pkgrel=1 pkgdesc="Local-first code intelligence: tree-sitter knowledge graph + MCP server (prebuilt binary)" arch=('x86_64' 'aarch64') diff --git a/packaging/choco/codegraph.nuspec b/packaging/choco/codegraph.nuspec index 58b5bb9aca..b7ebffe2f5 100644 --- a/packaging/choco/codegraph.nuspec +++ b/packaging/choco/codegraph.nuspec @@ -2,7 +2,7 @@ codegraph - 2.2.8 + 2.2.9 codegraph Hung Pham https://github.com/hungpham10/codegraph-rs diff --git a/packaging/winget/codegraph.yaml b/packaging/winget/codegraph.yaml index 3fb59275cf..664a9a7551 100644 --- a/packaging/winget/codegraph.yaml +++ b/packaging/winget/codegraph.yaml @@ -6,7 +6,7 @@ # release time (or automate it in the release pipeline before submitting to # microsoft/winget-pkgs). PackageIdentifier: hungpham10.codegraph -PackageVersion: 2.2.8 +PackageVersion: 2.2.9 PackageName: codegraph Publisher: Hung Pham PublisherUrl: https://github.com/hungpham10/codegraph-rs @@ -18,7 +18,7 @@ PackageUrl: https://github.com/hungpham10/codegraph-rs InstallerType: zip Installers: - Architecture: x64 - InstallerUrl: https://github.com/hungpham10/codegraph-rs/releases/download/v2.2.8/codegraph-x86_64-pc-windows-msvc.zip + InstallerUrl: https://github.com/hungpham10/codegraph-rs/releases/download/v2.2.9/codegraph-x86_64-pc-windows-msvc.zip InstallerSha256: 0000000000000000000000000000000000000000000000000000000000000000 InstallerType: zip ManifestType: singleton diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 4145e2c5f3..93f19b3d78 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -5,11 +5,11 @@ # # Usage (pin a version / download the script first): # irm https://raw.githubusercontent.com/hungpham10/codegraph-rs/main/scripts/install.ps1 -OutFile install.ps1 -# .\install.ps1 -Version 2.2.8 +# .\install.ps1 -Version 2.2.9 [CmdletBinding()] param( - # Pin a specific version, e.g. "2.2.8". Empty = latest release. + # Pin a specific version, e.g. "2.2.9". Empty = latest release. [string]$Version ) diff --git a/web/src/app.css b/web/src/app.css index 21cbf8f76d..4828688994 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -27,8 +27,9 @@ body { background: transparent; } -/* Mermaid render ra SVG — cho nền tối, canh giữa. */ +/* Mermaid render ra SVG — không co lại; zoom do transform của stage đảm nhiệm. */ .mermaid-host svg { - max-width: 100%; + display: block; + max-width: none; height: auto; } diff --git a/web/src/lib/components/MermaidView.svelte b/web/src/lib/components/MermaidView.svelte index 0b53e60995..5fe00065d8 100644 --- a/web/src/lib/components/MermaidView.svelte +++ b/web/src/lib/components/MermaidView.svelte @@ -1,10 +1,24 @@
{title} - -
-
- {#if error} -
- Mermaid error: {error} -
{code}
-
- {/if} -
- {#if !rendered && !error} -
Rendering diagram…
- {/if} + +
+ + {Math.round(scale * 100)}% + + + + +
-
+ + {#if error} +
+ Mermaid error: {error} +
{code}
+
+ {:else} + +
+ +
+ + {#if !rendered} +
+ Rendering diagram… +
+ {/if} +
+ {/if} + \ No newline at end of file