From 25b9b78ae1f22f0831bf7b2af2fda9386ebb378e Mon Sep 17 00:00:00 2001 From: Hung Pham Date: Sun, 25 Oct 2026 09:23:04 +0700 Subject: [PATCH 01/19] Implement dynamic dispatch flag + fan-out --- crates/codegraph-api/src/mermaid.rs | 15 + crates/codegraph-api/tests/api.rs | 3 + crates/codegraph-bench/benches/context.rs | 1 + crates/codegraph-bench/src/bin/mem.rs | 4 + crates/codegraph-binary/src/extract.rs | 10 + crates/codegraph-context/src/lib.rs | 1 + crates/codegraph-core/src/lib.rs | 13 +- crates/codegraph-core/src/semgraph.rs | 51 +- crates/codegraph-extract/src/bingraph.rs | 5 + crates/codegraph-extract/src/languages/c.rs | 1 + .../codegraph-extract/src/languages/common.rs | 34 ++ crates/codegraph-extract/src/languages/cpp.rs | 1 + .../codegraph-extract/src/languages/csharp.rs | 1 + crates/codegraph-extract/src/languages/go.rs | 1 + .../codegraph-extract/src/languages/java.rs | 1 + .../src/languages/javascript.rs | 1 + crates/codegraph-extract/src/languages/lua.rs | 1 + crates/codegraph-extract/src/languages/php.rs | 1 + .../codegraph-extract/src/languages/python.rs | 1 + .../codegraph-extract/src/languages/ruby.rs | 1 + .../codegraph-extract/src/languages/rust.rs | 80 +++ .../codegraph-extract/src/languages/scala.rs | 1 + .../codegraph-extract/src/languages/swift.rs | 1 + .../src/languages/typescript.rs | 1 + crates/codegraph-graph/src/diff.rs | 2 + crates/codegraph-graph/src/lib.rs | 497 +++++++++++++++++- crates/codegraph-graph/src/memtrack.rs | 3 + crates/codegraph-graph/src/shared.rs | 1 + .../codegraph-graph/src/storage/in_memory.rs | 1 + crates/codegraph-graph/src/storage/mysql.rs | 1 + .../codegraph-graph/src/storage/postgres.rs | 1 + crates/codegraph-graph/tests/lmdb.rs | 3 + crates/codegraph-graph/tests/rdbms.rs | 2 + crates/codegraph-graph/tests/redis.rs | 2 + crates/codegraph-graph/tests/sqlite.rs | 3 + .../codegraph-graph/tests/trace_flow_tests.rs | 2 + crates/codegraph-mcp/src/callers_tests.rs | 1 + crates/codegraph-sboxes/tests/control_flow.rs | 2 + crates/codegraph-sboxes/tests/end_to_end.rs | 2 + 39 files changed, 724 insertions(+), 29 deletions(-) diff --git a/crates/codegraph-api/src/mermaid.rs b/crates/codegraph-api/src/mermaid.rs index 2041e47adf..7bf05cee00 100644 --- a/crates/codegraph-api/src/mermaid.rs +++ b/crates/codegraph-api/src/mermaid.rs @@ -243,6 +243,18 @@ fn node_style( }; let mut label = label; if let Some(c) = call { + // Dynamic dispatch: nhãn `[dyn]` + candidates để người đọc thấy đây là + // điểm mù tĩnh và biết các target khả dĩ. + if matches!( + c.resolved_via, + codegraph_core::ResolutionKind::HierarchyFanOut + | codegraph_core::ResolutionKind::UnresolvedDynamic + ) { + label = format!("[dyn] {label}"); + if !c.candidate_names.is_empty() { + label.push_str(&format!(" → {}", c.candidate_names.join(", "))); + } + } if c.line > 0 { label.push_str(&format!(" · L{}", c.line)); } @@ -741,6 +753,7 @@ mod tests { doc: None, annotations: Vec::new(), language: "typescript".into(), + impl_trait: None, }, chain, chain_desc: desc.into_iter().map(String::from).collect(), @@ -779,6 +792,8 @@ mod tests { args: Vec::new(), into_var: None, is_new_var: false, + resolved_via: codegraph_core::ResolutionKind::default(), + candidate_names: Vec::new(), } } diff --git a/crates/codegraph-api/tests/api.rs b/crates/codegraph-api/tests/api.rs index b5d857f7cf..e33046338a 100644 --- a/crates/codegraph-api/tests/api.rs +++ b/crates/codegraph-api/tests/api.rs @@ -22,6 +22,7 @@ fn sym(id: u64, name: &str) -> Symbol { doc: None, annotations: Vec::new(), language: "typescript".into(), + impl_trait: None, } } @@ -60,6 +61,7 @@ async fn seed_index(path: &str) -> (u64, u64, u64) { target_method: None, into_var: None, is_new_var: false, + ..Default::default() }], }; idx.ingest(&[r], None).await.unwrap(); @@ -357,6 +359,7 @@ async fn seed_references(db: &str, count: usize) { target_method: None, into_var: None, is_new_var: false, + ..Default::default() }], }); } diff --git a/crates/codegraph-bench/benches/context.rs b/crates/codegraph-bench/benches/context.rs index f10ce01b02..ea91833881 100644 --- a/crates/codegraph-bench/benches/context.rs +++ b/crates/codegraph-bench/benches/context.rs @@ -35,6 +35,7 @@ fn fixture(fan_in: usize) -> ParseResult { doc: None, annotations: Vec::new(), language: "rust".into(), + impl_trait: None, }); let chain = if i == 0 { vec![id] diff --git a/crates/codegraph-bench/src/bin/mem.rs b/crates/codegraph-bench/src/bin/mem.rs index add64df42f..ea7a7eba04 100644 --- a/crates/codegraph-bench/src/bin/mem.rs +++ b/crates/codegraph-bench/src/bin/mem.rs @@ -333,6 +333,7 @@ fn synthetic_parse_result(n: usize, fanout: usize, shape: Shape) -> codegraph_gr doc: Some("Synthetic symbol để đo RAM — không phải code thật.".into()), annotations: Vec::::new(), language: "rust".into(), + impl_trait: None, }); } @@ -366,6 +367,9 @@ fn synthetic_parse_result(n: usize, fanout: usize, shape: Shape) -> codegraph_gr target_method: None, into_var: None, is_new_var: false, + receiver_text: None, + resolved_via: codegraph_core::ResolutionKind::default(), + candidates: Vec::new(), }); } } diff --git a/crates/codegraph-binary/src/extract.rs b/crates/codegraph-binary/src/extract.rs index 90674d9891..125fc878cb 100644 --- a/crates/codegraph-binary/src/extract.rs +++ b/crates/codegraph-binary/src/extract.rs @@ -154,6 +154,7 @@ fn do_extract( doc: None, annotations, language: "binary".to_string(), + impl_trait: None, }); } @@ -204,6 +205,7 @@ fn do_extract( doc: None, annotations, language: "binary".to_string(), + impl_trait: None, }); } @@ -247,6 +249,7 @@ fn do_extract( line: 0, }], language: "binary".to_string(), + impl_trait: None, }); } @@ -271,6 +274,7 @@ fn do_extract( doc: s.string.as_deref().map(|s| s.chars().take(200).collect()), annotations: Vec::new(), language: "binary".to_string(), + impl_trait: None, }); } @@ -552,6 +556,9 @@ fn build_chains_with_cfg( target_method: None, into_var: None, is_new_var: false, + receiver_text: None, + resolved_via: codegraph_core::ResolutionKind::default(), + candidates: Vec::new(), }); } "cjmp" => { @@ -635,6 +642,9 @@ fn build_chains_from_graph( target_method: None, into_var: None, is_new_var: false, + receiver_text: None, + resolved_via: codegraph_core::ResolutionKind::default(), + candidates: Vec::new(), }); } chains.insert(caller_id, chain); diff --git a/crates/codegraph-context/src/lib.rs b/crates/codegraph-context/src/lib.rs index 22d148d505..c2346b8f43 100644 --- a/crates/codegraph-context/src/lib.rs +++ b/crates/codegraph-context/src/lib.rs @@ -291,6 +291,7 @@ mod tests { doc: None, annotations: Vec::new(), language: "java".into(), + impl_trait: None, } } diff --git a/crates/codegraph-core/src/lib.rs b/crates/codegraph-core/src/lib.rs index f4a4ba1b5e..c98b620c75 100644 --- a/crates/codegraph-core/src/lib.rs +++ b/crates/codegraph-core/src/lib.rs @@ -14,10 +14,11 @@ pub use semgraph::{ is_marker, marker_id, marker_name, Annotation, BranchLabel, CallRecord, CallSite, CallSiteResult, ClassInfo, DbStats as SemgraphStats, DependenciesReport, Dependency, EffectCallPattern, EffectRule, EffectType, FileInfo, FlowCall, FlowResult, FlowStep, - FullFlowPath, FunctionScope, Marker, MemberInfo, PathEnding, ResolveResult, ScopeLevel, - SearchFlowResult, Symbol, SymbolId, SymbolKind, SymbolMatch, TraceAction, MARKER_BRANCH_END, - MARKER_BREAK, MARKER_CALL_ENTER, MARKER_CALL_EXIT, MARKER_CATCH_START, 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, MARKER_TRY_END, MARKER_TRY_START, SYMBOL_BASE, + FullFlowPath, FunctionScope, Marker, MemberInfo, PathEnding, ResolutionKind, ResolveResult, + ScopeLevel, SearchFlowResult, Symbol, SymbolId, SymbolKind, SymbolMatch, TraceAction, + MARKER_BRANCH_END, MARKER_BREAK, MARKER_CALL_ENTER, MARKER_CALL_EXIT, MARKER_CATCH_START, + 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, MARKER_TRY_END, MARKER_TRY_START, + SYMBOL_BASE, }; diff --git a/crates/codegraph-core/src/semgraph.rs b/crates/codegraph-core/src/semgraph.rs index c50463ee0e..c361a5900c 100644 --- a/crates/codegraph-core/src/semgraph.rs +++ b/crates/codegraph-core/src/semgraph.rs @@ -419,6 +419,10 @@ pub struct Symbol { #[serde(default)] pub annotations: Vec, pub language: String, + /// Trait/interface được impl block hiện thực (VD `impl Display for Foo` + /// → `"Display"`) — nguyên liệu cho dynamic-dispatch fan-out index. + #[serde(default)] + pub impl_trait: Option, } /// Annotation (VD `@Override`, `@Cacheable`). @@ -431,10 +435,32 @@ pub struct Annotation { pub line: u32, } +/// Call site được resolve bằng đường nào. +/// +/// Dynamic dispatch (fan-out qua trait/interface hierarchy) KHÔNG ghi đè +/// chain slot — candidates lưu trong [`CallRecord::candidates`], edges sinh +/// riêng ở `build_call_indexes`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "graphql", derive(async_graphql::Enum))] +#[serde(rename_all = "snake_case")] +pub enum ResolutionKind { + /// Resolve tĩnh bằng exact name. + #[default] + Exact, + /// Qua alias `var.method` → `TypeName.method`. + Alias, + /// Qua short-name fallback (phần sau dấu chấm). + ShortName, + /// Dynamic: fan-out qua trait/interface hierarchy, có `candidates`. + HierarchyFanOut, + /// Dynamic: method thuộc trait/interface nhưng không có impl trong repo. + UnresolvedDynamic, +} + /// Call record thô — persist để render flow khi call không resolve được. /// /// Vị trí `0` trong chain là placeholder, được thay bằng id thật khi resolve. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct CallRecord { pub caller_id: SymbolId, /// Tên call đầy đủ (VD `fmt.Println`, `requests.get`). @@ -457,6 +483,17 @@ pub struct CallRecord { /// `true` nếu assignment tạo biến mới, `false` nếu update biến đã có. #[serde(default)] pub is_new_var: bool, + /// Text receiver khi call dạng `recv.method()` (VD `self.boxed`) — hint + /// cho resolve phase quyết định dynamic dispatch. + #[serde(default)] + pub receiver_text: Option, + /// Call site được resolve bằng đường nào. + #[serde(default)] + pub resolved_via: ResolutionKind, + /// Targets fan-out khi dynamic dispatch (trait/interface hierarchy). + /// KHÔNG đi vào chain — edges sinh riêng ở `build_call_indexes`. + #[serde(default)] + pub candidates: Vec, } /// Giá trị của inverted index `call name → call sites` (dùng cho query @@ -471,6 +508,12 @@ pub struct CallSite { pub is_loop_body: bool, #[serde(default)] pub arg_exprs: Vec, + /// Call site được resolve bằng đường nào (dynamic fan-out flag). + #[serde(default)] + pub resolved_via: ResolutionKind, + /// Số target fan-out khi dynamic (0 = call tĩnh hoặc không fan-out). + #[serde(default)] + pub candidates: u32, } /// Thông tin file trong graph (không lưu content — dùng cho files/status). @@ -534,6 +577,12 @@ pub struct FlowCall { /// `true` nếu assignment tạo biến mới, `false` nếu update biến đã có. #[serde(default)] pub is_new_var: bool, + /// Call site được resolve bằng đường nào (dynamic dispatch flag). + #[serde(default)] + pub resolved_via: ResolutionKind, + /// Tên các target fan-out khi dynamic dispatch (đã resolve id → name). + #[serde(default)] + pub candidate_names: Vec, } /// 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 a2615980ff..2d420bb0d2 100644 --- a/crates/codegraph-extract/src/bingraph.rs +++ b/crates/codegraph-extract/src/bingraph.rs @@ -859,6 +859,8 @@ impl BinaryGraph { 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), + resolved_via: codegraph_core::ResolutionKind::default(), + candidate_names: Vec::new(), }); } Ok(FlowResult { @@ -915,6 +917,7 @@ mod tests { doc: None, annotations, language: "binary".to_string(), + impl_trait: None, } } @@ -985,6 +988,7 @@ mod tests { target_method: None, into_var: None, is_new_var: false, + ..Default::default() }, CallRecord { caller_id: sid(1), @@ -1000,6 +1004,7 @@ mod tests { target_method: None, into_var: None, is_new_var: false, + ..Default::default() }, ], } diff --git a/crates/codegraph-extract/src/languages/c.rs b/crates/codegraph-extract/src/languages/c.rs index 7b808cd558..a1a94b4d07 100644 --- a/crates/codegraph-extract/src/languages/c.rs +++ b/crates/codegraph-extract/src/languages/c.rs @@ -36,6 +36,7 @@ pub static SPEC: LangSpec = LangSpec { target_fn: None, }], class_type_name: None, + impl_trait_fn: None, if_kinds: &["if_statement"], elif_kinds: &[], if_block_kinds: &[], diff --git a/crates/codegraph-extract/src/languages/common.rs b/crates/codegraph-extract/src/languages/common.rs index d9391a8b6d..9a185956ab 100644 --- a/crates/codegraph-extract/src/languages/common.rs +++ b/crates/codegraph-extract/src/languages/common.rs @@ -71,6 +71,9 @@ pub struct LangSpec { pub calls: &'static [CallRule], /// Lấy type_name cho class symbol (TS `extends`/`implements` heritage). pub class_type_name: Option, + /// Trait/interface mà impl block hiện thực: `(class node, src) -> trait name` + /// (Rust `impl Display for Foo` → `"Display"`). + pub impl_trait_fn: Option, /// Cho phép dùng field `type` làm tên khi thiếu name/declarator (VD Rust /// `impl Foo` — tên nằm ở `type`). Bật cho ngôn ngữ không có node kind /// xung đột (C# `variable_declaration{type}` phải để false). @@ -330,6 +333,11 @@ fn push_symbol( } _ => None, }; + let impl_trait = if matches!(kind, SymbolKind::Class | SymbolKind::Interface) { + spec.impl_trait_fn.and_then(|f| f(node, ctx.src)) + } else { + None + }; let line = name_node.start_position().row as u32 + 1; let end_line = node @@ -354,6 +362,7 @@ fn push_symbol( doc: None, annotations, language: ctx.language.to_string(), + impl_trait, }); Some(id) } @@ -1109,6 +1118,9 @@ fn emit_case_label(ctx: &mut ChainCtx, case: &Node, in_loop: u32, condition: Opt target_method: None, into_var: None, is_new_var: false, + receiver_text: None, + resolved_via: codegraph_core::ResolutionKind::default(), + candidates: Vec::new(), }); } @@ -1199,6 +1211,9 @@ fn emit_branch_label(ctx: &mut ChainCtx, label: &str) { target_method: None, into_var: None, is_new_var: false, + receiver_text: None, + resolved_via: codegraph_core::ResolutionKind::default(), + candidates: Vec::new(), }); } @@ -1263,6 +1278,9 @@ fn emit_call( .target_fn .map(|f| f(node, ctx.src)) .unwrap_or((None, None)); + // Receiver text: khi callee là `recv.method(...)` — hint cho resolve phase + // quyết định dynamic dispatch (trait/interface/function pointer). + let receiver_text = receiver_of_callee(&callee, ctx.src); ctx.calls.push(CallRecord { caller_id: ctx.func_id, call_name: name, @@ -1277,9 +1295,25 @@ fn emit_call( target_method, into_var: None, is_new_var: false, + receiver_text, + resolved_via: codegraph_core::ResolutionKind::default(), + candidates: Vec::new(), }); } +/// Text receiver của callee dạng `recv.method()` — node con trước dấu chấm. +/// `field_expression` (Rust/C/Java/Go/TS...) có field `object`; grammar khác +/// fallback về named child đầu tiên. Trả `None` cho call thuần (`plain_fn()`). +fn receiver_of_callee<'a>(callee: &Node<'a>, src: &'a [u8]) -> Option { + if callee.kind() != "field_expression" && callee.kind() != "attribute" { + return None; + } + let obj = callee + .child_by_field_name("object") + .or_else(|| named_children(callee).into_iter().next())?; + text(&obj, src).filter(|t| !t.is_empty()) +} + // ==================== Helpers ==================== pub fn named_children<'a>(node: &Node<'a>) -> Vec> { diff --git a/crates/codegraph-extract/src/languages/cpp.rs b/crates/codegraph-extract/src/languages/cpp.rs index 1ea4de59bd..7d198a384d 100644 --- a/crates/codegraph-extract/src/languages/cpp.rs +++ b/crates/codegraph-extract/src/languages/cpp.rs @@ -38,6 +38,7 @@ pub static SPEC: LangSpec = LangSpec { target_fn: None, }], class_type_name: None, + impl_trait_fn: None, if_kinds: &["if_statement"], elif_kinds: &[], if_block_kinds: &[], diff --git a/crates/codegraph-extract/src/languages/csharp.rs b/crates/codegraph-extract/src/languages/csharp.rs index 1a95055e5c..356a2f5419 100644 --- a/crates/codegraph-extract/src/languages/csharp.rs +++ b/crates/codegraph-extract/src/languages/csharp.rs @@ -64,6 +64,7 @@ pub static SPEC: LangSpec = LangSpec { }, ], class_type_name: None, + impl_trait_fn: None, if_kinds: &["if_statement"], elif_kinds: &[], if_block_kinds: &[], diff --git a/crates/codegraph-extract/src/languages/go.rs b/crates/codegraph-extract/src/languages/go.rs index aab9a5e54b..346781e98e 100644 --- a/crates/codegraph-extract/src/languages/go.rs +++ b/crates/codegraph-extract/src/languages/go.rs @@ -56,6 +56,7 @@ pub static SPEC: LangSpec = LangSpec { target_fn: None, }], class_type_name: None, + impl_trait_fn: None, if_kinds: &["if_statement"], elif_kinds: &[], if_block_kinds: &[], diff --git a/crates/codegraph-extract/src/languages/java.rs b/crates/codegraph-extract/src/languages/java.rs index 43b67a26e3..509d6ef529 100644 --- a/crates/codegraph-extract/src/languages/java.rs +++ b/crates/codegraph-extract/src/languages/java.rs @@ -108,6 +108,7 @@ pub static SPEC: LangSpec = LangSpec { }, ], class_type_name: None, + impl_trait_fn: None, if_kinds: &["if_statement"], elif_kinds: &[], if_block_kinds: &[], diff --git a/crates/codegraph-extract/src/languages/javascript.rs b/crates/codegraph-extract/src/languages/javascript.rs index 77fc7b5783..da1187a9b5 100644 --- a/crates/codegraph-extract/src/languages/javascript.rs +++ b/crates/codegraph-extract/src/languages/javascript.rs @@ -84,6 +84,7 @@ pub static SPEC: LangSpec = LangSpec { }, ], class_type_name: Some(class_type_name), + impl_trait_fn: None, if_kinds: &["if_statement"], elif_kinds: &[], if_block_kinds: &[], diff --git a/crates/codegraph-extract/src/languages/lua.rs b/crates/codegraph-extract/src/languages/lua.rs index 8158c9c174..a04be64615 100644 --- a/crates/codegraph-extract/src/languages/lua.rs +++ b/crates/codegraph-extract/src/languages/lua.rs @@ -55,6 +55,7 @@ pub static SPEC: LangSpec = LangSpec { target_fn: None, }], class_type_name: None, + impl_trait_fn: None, if_kinds: &["if_statement"], elif_kinds: &[], if_block_kinds: &[], diff --git a/crates/codegraph-extract/src/languages/php.rs b/crates/codegraph-extract/src/languages/php.rs index df2436abba..7c72af44ec 100644 --- a/crates/codegraph-extract/src/languages/php.rs +++ b/crates/codegraph-extract/src/languages/php.rs @@ -131,6 +131,7 @@ pub static SPEC: LangSpec = LangSpec { }, ], class_type_name: None, + impl_trait_fn: None, if_kinds: &["if_statement"], elif_kinds: &[], if_block_kinds: &[], diff --git a/crates/codegraph-extract/src/languages/python.rs b/crates/codegraph-extract/src/languages/python.rs index d66d57a0dc..7cc8849d74 100644 --- a/crates/codegraph-extract/src/languages/python.rs +++ b/crates/codegraph-extract/src/languages/python.rs @@ -46,6 +46,7 @@ pub static SPEC: LangSpec = LangSpec { target_fn: None, }], class_type_name: None, + impl_trait_fn: None, if_kinds: &["if_statement"], elif_kinds: &["elif_clause"], if_block_kinds: &[], diff --git a/crates/codegraph-extract/src/languages/ruby.rs b/crates/codegraph-extract/src/languages/ruby.rs index c39ff31291..cdd4d03503 100644 --- a/crates/codegraph-extract/src/languages/ruby.rs +++ b/crates/codegraph-extract/src/languages/ruby.rs @@ -63,6 +63,7 @@ pub static SPEC: LangSpec = LangSpec { }, ], class_type_name: Some(class_type_name), + impl_trait_fn: None, if_kinds: &["if"], elif_kinds: &["elsif"], if_block_kinds: &[], diff --git a/crates/codegraph-extract/src/languages/rust.rs b/crates/codegraph-extract/src/languages/rust.rs index 689ae8bdc0..8cc61fcb69 100644 --- a/crates/codegraph-extract/src/languages/rust.rs +++ b/crates/codegraph-extract/src/languages/rust.rs @@ -44,6 +44,7 @@ pub static SPEC: LangSpec = LangSpec { target_fn: None, }], class_type_name: Some(rust_class_type_name), + impl_trait_fn: Some(rust_impl_trait_name), if_kinds: &["if_expression", "if_let_expression"], elif_kinds: &[], if_block_kinds: &[], @@ -84,6 +85,20 @@ fn rust_class_type_name(node: &tree_sitter::Node, src: &[u8]) -> Option crate::lang_parser!(RustParser, SPEC); +/// Trait name của `impl Trait for Type` — field `trait` của impl_item. +/// Normalize generic: `Iterator` → `"Iterator"` (fan-out index key +/// phải khớp tên trait thuần). +fn rust_impl_trait_name(node: &tree_sitter::Node, src: &[u8]) -> Option { + if node.kind() != "impl_item" { + return None; + } + let raw = node + .child_by_field_name("trait") + .and_then(|t| text(&t, src))?; + let name = raw.split('<').next()?.trim().to_string(); + (!name.is_empty()).then_some(name) +} + #[cfg(test)] mod tests { use crate::LangParser; @@ -176,4 +191,69 @@ impl Foo { "method should be scoped to the struct, not the impl" ); } + + #[test] + fn rust_impl_trait_name_extracted_and_normalized() { + let src = r#" +pub trait Handler { fn handle(&self); } +pub struct A; +pub struct B; +impl Handler for A { fn handle(&self) {} } +impl std::fmt::Display for B { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { Ok(()) } } +impl A { fn inherent(&self) {} } +"#; + let syms = parse(src); + + let trait_impl = syms + .iter() + .find(|s| s.impl_trait.as_deref() == Some("Handler")) + .expect("impl Handler symbol not found"); + assert_eq!(trait_impl.name, "A"); + + // Qualified trait `std::fmt::Display` — normalize giữ phần sau chấm cuối? + // Không: fan-out key khớp tên trait thuần, giữ nguyên text đầy đủ. + let display_impl = syms + .iter() + .find(|s| s.impl_trait.is_some() && s.name == "B") + .expect("impl Display symbol not found"); + assert!(display_impl + .impl_trait + .as_deref() + .unwrap() + .contains("Display")); + + // Inherent impl không có trait. + let inherent = syms + .iter() + .find(|s| s.name == "A" && s.kind == SymbolKind::Class) + .expect("inherent impl not found"); + assert_eq!(inherent.impl_trait, None); + } + + #[test] + fn rust_method_call_records_receiver_text() { + let src = r#" +struct S; +impl S { + fn go(&self, h: &dyn Handler) { + h.handle(); + plain(); + } +} +"#; + let parsed = super::RustParser::new().parse_file("test.rs", src).unwrap(); + let method_call = parsed + .calls + .iter() + .find(|c| c.call_name.ends_with("handle")) + .expect("method call not found"); + assert_eq!(method_call.receiver_text.as_deref(), Some("h")); + + let plain = parsed + .calls + .iter() + .find(|c| c.call_name == "plain") + .expect("plain call not found"); + assert_eq!(plain.receiver_text, None); + } } diff --git a/crates/codegraph-extract/src/languages/scala.rs b/crates/codegraph-extract/src/languages/scala.rs index be1c3f26ba..2b47931ebc 100644 --- a/crates/codegraph-extract/src/languages/scala.rs +++ b/crates/codegraph-extract/src/languages/scala.rs @@ -42,6 +42,7 @@ pub static SPEC: LangSpec = LangSpec { target_fn: None, }], class_type_name: None, + impl_trait_fn: None, if_kinds: &["if_expression"], elif_kinds: &[], if_block_kinds: &[], diff --git a/crates/codegraph-extract/src/languages/swift.rs b/crates/codegraph-extract/src/languages/swift.rs index 1e774455e9..90e7fc32de 100644 --- a/crates/codegraph-extract/src/languages/swift.rs +++ b/crates/codegraph-extract/src/languages/swift.rs @@ -49,6 +49,7 @@ pub static SPEC: LangSpec = LangSpec { target_fn: None, }], class_type_name: None, + impl_trait_fn: None, if_kinds: &["if_statement"], elif_kinds: &[], // Swift if/for không có consequence/body field — body là node `statements` trần. diff --git a/crates/codegraph-extract/src/languages/typescript.rs b/crates/codegraph-extract/src/languages/typescript.rs index 9794995847..6b61a21ee0 100644 --- a/crates/codegraph-extract/src/languages/typescript.rs +++ b/crates/codegraph-extract/src/languages/typescript.rs @@ -91,6 +91,7 @@ pub static SPEC: LangSpec = LangSpec { }, ], class_type_name: Some(class_type_name), + impl_trait_fn: None, if_kinds: &["if_statement"], elif_kinds: &[], if_block_kinds: &[], diff --git a/crates/codegraph-graph/src/diff.rs b/crates/codegraph-graph/src/diff.rs index 782fb1f9cc..3fa2718d6d 100644 --- a/crates/codegraph-graph/src/diff.rs +++ b/crates/codegraph-graph/src/diff.rs @@ -497,6 +497,7 @@ mod tests { doc: None, annotations: Vec::new(), language: "test".to_string(), + impl_trait: None, } } @@ -630,6 +631,7 @@ index 0000000..2222222 target_method: None, into_var: None, is_new_var: false, + ..Default::default() }]; let r = result( "a.ts", diff --git a/crates/codegraph-graph/src/lib.rs b/crates/codegraph-graph/src/lib.rs index ab62b09e2f..8da1ede8c1 100644 --- a/crates/codegraph-graph/src/lib.rs +++ b/crates/codegraph-graph/src/lib.rs @@ -69,9 +69,9 @@ use codegraph_core::{ MARKER_BRANCH_END, MARKER_BREAK, MARKER_CALL_ENTER, MARKER_CALL_EXIT, MARKER_CATCH_START, MARKER_CONTINUE, MARKER_IF_FALSE, MARKER_IF_TRUE, MARKER_LOOP, MARKER_LOOP_BACK, MARKER_RETURN, MARKER_SWITCH_CASE, MARKER_SWITCH_CLOSE, MARKER_SWITCH_END, MARKER_SWITCH_START, MARKER_THROW, - MARKER_TRY_END, MARKER_TRY_START, MemberInfo, PathEnding, ResolveResult, SYMBOL_BASE, - SearchFlowResult, SemgraphStats, StorageRoute, Symbol, SymbolKind, SymbolMatch, TraceAction, - is_marker, marker_name, + MARKER_TRY_END, MARKER_TRY_START, MemberInfo, PathEnding, ResolutionKind, ResolveResult, + SYMBOL_BASE, SearchFlowResult, SemgraphStats, StorageRoute, Symbol, SymbolKind, SymbolMatch, + TraceAction, is_marker, marker_name, }; use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, HashMap, HashSet}; @@ -301,6 +301,16 @@ struct CallRef<'a> { caller: u64, } +/// Kết quả resolve một call placeholder. +enum CallResolution { + /// Không resolve được (behavior cũ: giữ placeholder + raw name). + None, + /// Resolve tĩnh — ghi đè chain slot bằng id thật. + Single(u64, ResolutionKind), + /// Dynamic dispatch — fan-out candidates, chain giữ placeholder. + FanOut(Vec, ResolutionKind), +} + // ==================== GraphIndex ==================== /// Index chính (semgraph-style): registry + 2 engine + inverted indexes. @@ -336,6 +346,10 @@ pub struct GraphIndex { chains_map: HashMap>, /// call name (lowercase, kèm alias type-qualified) → call sites. call_names: HashMap>, + /// Trait/interface name (lowercase) → impl method ids — fan-out dynamic + /// dispatch. Build từ `Symbol.impl_trait` khi ingest; in-memory (blob call + /// record đã persist candidates nên reopen không cần index này). + impl_index: HashMap>, /// Số edge `(caller, callee)` — đếm từ chains lúc ingest/rebuild. /// Metadata của edge (condition/effect/...) KHÔNG materialize thành map: /// đã nằm trong call records persist, `flow()` đọc thẳng từ đó. @@ -1003,6 +1017,7 @@ impl GraphIndex { scope_index: HashMap::new(), chains_map: HashMap::new(), call_names: HashMap::new(), + impl_index: HashMap::new(), edge_count: 0, files: Vec::new(), next_id: SYMBOL_BASE, @@ -1426,6 +1441,7 @@ impl GraphIndex { self.scope_index.clear(); self.chains_map.clear(); self.call_names.clear(); + self.impl_index.clear(); self.edge_count = 0; self.files.clear(); self.name_records.clear(); @@ -1475,10 +1491,12 @@ impl GraphIndex { self.rebuild_scope_index(); // ── Phase 2: resolve placeholder 0 trong chains ── - self.resolve_calls(&all_calls); + self.build_impl_index(); + let mut call_meta: HashMap<(u64, usize), (ResolutionKind, Vec)> = HashMap::new(); + self.resolve_calls(&all_calls, &mut call_meta); // ── Phase 3: call records + call-name index + edge count ── - self.build_call_indexes(&all_calls, p).await?; + self.build_call_indexes(&all_calls, &call_meta, p).await?; // ── Phase 4: files ── if let Some(p) = p { @@ -1585,7 +1603,15 @@ impl GraphIndex { } /// Thay placeholder `0` trong chain bằng id thật (resolve per-caller). - fn resolve_calls(&mut self, calls: &[CallRef<'_>]) { + /// + /// Metadata resolution (`resolved_via` + `candidates` cho dynamic fan-out) + /// gom vào `meta` — không sửa được `CallRecord` vì `CallRef` mượn `&` từ + /// `ParseResult`; `build_call_indexes` inject metadata khi persist blob. + fn resolve_calls( + &mut self, + calls: &[CallRef<'_>], + meta: &mut HashMap<(u64, usize), (ResolutionKind, Vec)>, + ) { let mut caller_calls: HashMap> = HashMap::new(); for c in calls { if c.caller != 0 { @@ -1604,8 +1630,17 @@ impl GraphIndex { if c.position >= chain.len() || chain[c.position] != 0 { continue; } - if let Some(real) = self.resolve_call_placeholder(c, caller_id) { - resolved.push((c.position, real)); + match self.resolve_call_placeholder(c, caller_id) { + CallResolution::None => {} + CallResolution::Single(real, kind) => { + resolved.push((c.position, real)); + meta.insert((caller_id, c.position), (kind, Vec::new())); + } + CallResolution::FanOut(cands, kind) => { + // Dynamic dispatch — KHÔNG ghi đè chain slot + // (placeholder 0 giữ raw name hiển thị). + meta.insert((caller_id, c.position), (kind, cands)); + } } } } @@ -1617,17 +1652,49 @@ impl GraphIndex { } } + /// Index dynamic-dispatch fan-out: trait/interface name → impl methods. + /// Nguyên liệu: `Symbol.impl_trait` (Rust `impl Trait for Type`). + /// + /// Lưu ý: `link_impl_methods_to_def` (extractor) re-parent method của impl + /// về struct def, nên KHÔNG thể map method → impl qua `scope_id`. Dùng + /// file + line range của impl block để gán method (method nằm trong + /// [line..end_line] của đúng một impl block cùng file). + fn build_impl_index(&mut self) { + // (file, line, end_line, trait_name lower) của các impl block có trait. + let mut trait_impls: Vec<(String, u32, u32, String)> = Vec::new(); + for s in self.symbols.values() { + if let Some(tr) = &s.impl_trait { + trait_impls.push((s.file.clone(), s.line, s.end_line, tr.to_lowercase())); + } + } + if trait_impls.is_empty() { + return; + } + for sym in self.symbols.values() { + if !matches!(sym.kind, SymbolKind::Method | SymbolKind::Function) { + continue; + } + if let Some((_, _, _, tr)) = trait_impls + .iter() + .find(|(f, l, e, _)| f == &sym.file && sym.line >= *l && sym.line <= *e) + { + self.impl_index.entry(tr.clone()).or_default().push(sym.id); + } + } + } + /// Resolve một call record về real symbol id — trả `None` nếu không được. /// /// Ưu tiên: structural hint (TargetClass/TargetMethod — VD Java class /// literal) → exact name → short name (phần sau dấu chấm) → best-candidate - /// (@Override +10 / has-chain +5 / same-file +3). - fn resolve_call_placeholder(&self, call: &CallRecord, caller_id: u64) -> Option { + /// (@Override +10 / has-chain +5 / same-file +3) → hierarchy fan-out + /// (dynamic dispatch — chỉ khi mọi đường tĩnh thất bại). + fn resolve_call_placeholder(&self, call: &CallRecord, caller_id: u64) -> CallResolution { // 1. Try class/method target hints (used mainly for Java). if let (Some(tc), Some(tm)) = (&call.target_class, &call.target_method) && let Some(id) = self.lookup_method_of_class(tc, tm) { - return Some(id); + return CallResolution::Single(id, ResolutionKind::Exact); } // 2. Direct name lookup (full qualified name). @@ -1639,10 +1706,14 @@ impl GraphIndex { // 3. Independent lookup paths: alias ("var.method" → "TypeName.method") // AND short name fallback — merge results instead of sequential fallback. + let mut via_alias = false; if candidates.is_empty() { // 3a. Go/Import alias: resolve "var.method" via field type to "TypeName.method". if let Some(qualified) = self.alias_qualified_name(caller_id, &call.call_name) { let alias_ids = self.name_index.get(&qualified).cloned().unwrap_or_default(); + if !alias_ids.is_empty() { + via_alias = true; + } candidates.extend(alias_ids); } @@ -1655,14 +1726,13 @@ impl GraphIndex { .to_lowercase(); if !short.is_empty() { let short_ids = self.name_index.get(&short).cloned().unwrap_or_default(); + if !short_ids.is_empty() { + via_alias = true; + } candidates.extend(short_ids); } } - if candidates.is_empty() { - return None; - } - // 4. Filter to Function/Method kinds AND exclude the caller itself. // The short name fallback can return the caller's own id (e.g. both // LegacyAdapter.doWork and Service.doWork match "dowork"), so we must @@ -1675,10 +1745,117 @@ impl GraphIndex { .is_some_and(|s| matches!(s.kind, SymbolKind::Function | SymbolKind::Method)) }); - if candidates.is_empty() { - return None; + if !candidates.is_empty() { + // Dynamic dispatch ưu tiên best-guess: nếu candidates chứa method + // KHAI BÁO trong trait/interface (không phải impl), đây gần như chắc + // chắn là dispatch động — chọn 1 best guess ở đây chính là bug + // "resolve sai một cách có tự tin". Fan-out mọi impl thay vì chọn. + let trait_names: Vec<&str> = candidates + .iter() + .filter_map(|&id| { + let m = self.symbols.get(&id)?; + if !matches!(m.kind, SymbolKind::Method | SymbolKind::Function) { + return None; + } + let tr = self.symbols.get(&m.scope_id)?; + (tr.kind == SymbolKind::Interface).then_some(tr.name.as_str()) + }) + .collect(); + if !trait_names.is_empty() { + // Fan-out chỉ áp dụng cho method dispatch (có receiver — + // `h.handle`). Bare call `handle` với ứng viên Function tĩnh + // thì resolve tĩnh thắng (free function trùng tên trait method + // là trùng hợp tên, không phải dispatch). + let is_method_dispatch = call.call_name.contains('.'); + let all_interface = candidates.iter().all(|&id| { + self.symbols + .get(&id) + .and_then(|m| self.symbols.get(&m.scope_id)) + .is_some_and(|tr| tr.kind == SymbolKind::Interface) + }); + if is_method_dispatch || all_interface { + return self.collect_fanout(&trait_names, caller_id); + } + } + let kind = if via_alias { + ResolutionKind::Alias + } else { + ResolutionKind::Exact + }; + return CallResolution::Single( + self.pick_best_candidate(&candidates, caller_id, &call.call_name), + kind, + ); } - Some(self.pick_best_candidate(&candidates, caller_id, &call.call_name)) + + // 5. Dynamic dispatch fan-out: short name khớp method khai báo trong + // trait/interface → fan-out mọi impl (CHA-style over-approximation). + self.hierarchy_fanout(call, caller_id) + } + + /// Gom candidates fan-out cho danh sách trait/interface names: mọi impl + /// method của trait (qua `impl_index`). + /// + /// - Có impl → `FanOut(candidates, HierarchyFanOut)`. + /// - Trait method nhưng repo không có impl nào → `FanOut([], UnresolvedDynamic)` + /// (flag ranh giới soundiness — reflection/monkey-patch rơi vào đây). + fn collect_fanout(&self, trait_names: &[&str], caller_id: u64) -> CallResolution { + let mut cands: Vec = Vec::new(); + for tr in trait_names { + if let Some(ids) = self.impl_index.get(&tr.to_lowercase()) { + cands.extend(ids.iter().copied().filter(|&id| id != caller_id)); + } + } + cands.sort_unstable(); + cands.dedup(); + let kind = if cands.is_empty() { + ResolutionKind::UnresolvedDynamic + } else { + ResolutionKind::HierarchyFanOut + }; + CallResolution::FanOut(cands, kind) + } + + /// Fan-out qua trait/interface hierarchy — đường resolve cuối cùng, chỉ chạy + /// khi mọi đường tĩnh thất bại. Short name khớp method thuộc `Interface` + /// symbol → candidates = mọi impl method cùng tên (`impl_index`). + /// + /// - Có impl → `FanOut(candidates, HierarchyFanOut)`. + /// - Trait method nhưng repo không có impl nào → `FanOut([], UnresolvedDynamic)` + /// (flag ranh giới soundiness — reflection/monkey-patch rơi vào đây). + /// - Không khớp trait method nào → `None` (behavior cũ). + fn hierarchy_fanout(&self, call: &CallRecord, caller_id: u64) -> CallResolution { + let short = call + .call_name + .rsplit('.') + .next() + .unwrap_or("") + .to_lowercase(); + if short.is_empty() { + return CallResolution::None; + } + let Some(method_ids) = self.name_index.get(&short) else { + return CallResolution::None; + }; + // Method của trait/interface nào mang tên này? + let mut trait_names: Vec<&str> = Vec::new(); + for &mid in method_ids { + let Some(m) = self.symbols.get(&mid) else { + continue; + }; + if !matches!(m.kind, SymbolKind::Method | SymbolKind::Function) { + continue; + } + if let Some(tr) = self.symbols.get(&m.scope_id) + && tr.kind == SymbolKind::Interface + { + trait_names.push(&tr.name); + } + } + if trait_names.is_empty() { + return CallResolution::None; + } + self.collect_fanout(&trait_names, caller_id) } /// Tìm method của class theo tên (scope_id == class id). @@ -1804,6 +1981,7 @@ impl GraphIndex { async fn build_call_indexes( &mut self, calls: &[CallRef<'_>], + call_meta: &HashMap<(u64, usize), (ResolutionKind, Vec)>, progress: Option<&dyn IngestProgress>, ) -> Result<()> { // Chỉ lưu **index** vào `calls` (4 B) thay vì clone `CallRecord` @@ -1821,6 +1999,10 @@ impl GraphIndex { } // Call-site index: key theo tên thô + alias type-qualified (nếu có). + let (resolved_via, candidates) = call_meta + .get(&(caller, c.position)) + .map(|(k, cands)| (*k, cands.len() as u32)) + .unwrap_or((ResolutionKind::Exact, 0)); let site = CallSite { caller_id: caller, call_name: c.call_name.clone(), @@ -1828,6 +2010,8 @@ impl GraphIndex { condition: c.condition.clone(), is_loop_body: c.is_loop_body, arg_exprs: c.arg_exprs.clone(), + resolved_via, + candidates, }; let raw_key = c.call_name.to_lowercase(); self.call_names @@ -1844,6 +2028,7 @@ impl GraphIndex { // Đếm edges từ mọi chain — metadata không materialize (đã nằm trong // call records persist; `flow()` đọc thẳng từ đó). // Vị trí 0 = chính func id (owner) — không phải call. + // Dynamic fan-out: mỗi candidate là một edge ẩn trên chain slot 0. self.edge_count = self .chains_map .values() @@ -1854,16 +2039,35 @@ impl GraphIndex { .filter(|&&e| e != 0 && !is_marker(e)) .count() as u64 }) - .sum(); - - // Persist call records (gom theo caller). + .sum::() + + call_meta + .values() + .map(|(_, cands)| cands.len() as u64) + .sum::(); + + // Persist call records (gom theo caller) — inject metadata resolve + // (`resolved_via` + `candidates`) vào bản clone trước khi serialize. if let Some(p) = progress && !recs_by_caller.is_empty() { p.phase("save call records", recs_by_caller.len()); } for (caller, idxs) in recs_by_caller { - let recs: Vec<&CallRecord> = idxs.iter().map(|&i| calls[i as usize].rec).collect(); + let recs: Vec = idxs + .iter() + .map(|&i| { + let c = calls[i as usize].rec; + match call_meta.get(&(caller, c.position)) { + Some((kind, cands)) => { + let mut r = c.clone(); + r.resolved_via = *kind; + r.candidates = cands.clone(); + r + } + None => c.clone(), + } + }) + .collect(); // `rec.caller_id` ghi ra đây là id **local** của file, không phải // id global — vô hại: mọi reader (`flow`, `bingraph`) lấy caller id // từ **key** của blob, không đọc field này. Nếu sau này cần id @@ -2221,6 +2425,12 @@ impl GraphIndex { args: rec.arg_exprs.clone(), into_var: rec.into_var.clone(), is_new_var: rec.is_new_var, + resolved_via: rec.resolved_via, + candidate_names: rec + .candidates + .iter() + .filter_map(|&cid| self.symbols.get(&cid).map(|s| s.name.clone())) + .collect(), }); } continue; @@ -2242,6 +2452,15 @@ impl GraphIndex { 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), + resolved_via: rec.map(|r| r.resolved_via).unwrap_or_default(), + candidate_names: rec + .map(|r| { + r.candidates + .iter() + .filter_map(|&cid| self.symbols.get(&cid).map(|s| s.name.clone())) + .collect() + }) + .unwrap_or_default(), }); } @@ -4130,6 +4349,7 @@ mod tests { doc: None, annotations: Vec::new(), language: "test".to_string(), + impl_trait: None, } } @@ -4453,6 +4673,7 @@ mod tests { target_method: None, into_var: None, is_new_var: false, + ..Default::default() }, CallRecord { caller_id: SYMBOL_BASE, @@ -4468,6 +4689,7 @@ mod tests { target_method: None, into_var: None, is_new_var: false, + ..Default::default() }, ]; let r = result( @@ -4493,6 +4715,225 @@ mod tests { assert_eq!(cees.len(), 2); } + // ── Dynamic dispatch: trait/interface fan-out ── + + /// Symbol với kind/scope/impl_trait tùy ý — cho test dynamic dispatch. + /// `line` quan trọng: `build_impl_index` gán method vào trait impl theo + /// line range của impl block (giống extractor re-parent method về def). + #[allow(clippy::too_many_arguments)] + fn dsym( + id: u64, + name: &str, + kind: SymbolKind, + scope_id: u64, + impl_trait: Option<&str>, + line: u32, + ) -> Symbol { + Symbol { + id, + name: name.to_string(), + kind, + scope: ScopeLevel::Global, + scope_id, + type_ref: 0, + type_name: None, + file: "t.rs".to_string(), + line, + // Impl block cần range chứa method con (method line = line + 1). + end_line: if impl_trait.is_some() { line + 9 } else { line }, + signature: None, + doc: None, + annotations: Vec::new(), + language: "rust".to_string(), + impl_trait: impl_trait.map(String::from), + } + } + + #[tokio::test] + async fn dyn_trait_call_fans_out_to_impls() { + let mut idx = GraphIndex::in_memory(); + let (caller, trait_id, m1, m2) = ( + SYMBOL_BASE, + SYMBOL_BASE + 1, + SYMBOL_BASE + 2, + SYMBOL_BASE + 3, + ); + let r = result( + "t.rs", + vec![ + dsym(caller, "caller", SymbolKind::Function, 0, None, 50), + // trait Handler { fn handle(&self); } + dsym(trait_id, "Handler", SymbolKind::Interface, 0, None, 1), + dsym( + SYMBOL_BASE + 4, + "handle", + SymbolKind::Method, + trait_id, + None, + 2, + ), + // impl Handler for A { fn handle ... } / impl Handler for B {...} + dsym( + SYMBOL_BASE + 5, + "ImplA", + SymbolKind::Class, + 0, + Some("Handler"), + 10, + ), + dsym(m1, "handle", SymbolKind::Method, SYMBOL_BASE + 5, None, 11), + dsym( + SYMBOL_BASE + 7, + "ImplB", + SymbolKind::Class, + 0, + Some("Handler"), + 30, + ), + dsym(m2, "handle", SymbolKind::Method, SYMBOL_BASE + 7, None, 31), + ], + HashMap::from([(caller, vec![caller, 0])]), + vec![CallRecord { + caller_id: caller, + call_name: "h.handle".to_string(), + position: 1, + line: 51, + condition: None, + is_loop_body: false, + effect: EffectType::None, + effect_desc: None, + target_class: None, + target_method: None, + into_var: None, + is_new_var: false, + receiver_text: Some("h".to_string()), + ..Default::default() + }], + ); + idx.ingest(&[r], None).await.unwrap(); + + // Chain giữ placeholder + raw name (không ghi đè bằng 1 best guess). + let flow = idx.flow(caller).await.unwrap(); + assert_eq!(flow.chain, vec![caller, 0]); + assert_eq!(flow.chain_desc[1], "h.handle"); + let fc = &flow.calls[0]; + assert_eq!(fc.resolved_via, ResolutionKind::HierarchyFanOut); + let mut names = fc.candidate_names.clone(); + names.sort(); + assert_eq!(names, vec!["handle", "handle"]); + + // Mỗi candidate là một edge ẩn trên slot 0 → edge_count = 2. + assert_eq!(idx.stats().edges, 2); + } + + #[tokio::test] + async fn trait_method_without_impl_is_unresolved_dynamic() { + let mut idx = GraphIndex::in_memory(); + let (caller, trait_id) = (SYMBOL_BASE, SYMBOL_BASE + 1); + let r = result( + "t.rs", + vec![ + dsym(caller, "caller", SymbolKind::Function, 0, None, 10), + dsym(trait_id, "Missing", SymbolKind::Interface, 0, None, 1), + dsym( + SYMBOL_BASE + 2, + "run", + SymbolKind::Method, + trait_id, + None, + 2, + ), + ], + HashMap::from([(caller, vec![caller, 0])]), + vec![CallRecord { + caller_id: caller, + call_name: "x.run".to_string(), + position: 1, + line: 11, + condition: None, + is_loop_body: false, + effect: EffectType::None, + effect_desc: None, + target_class: None, + target_method: None, + into_var: None, + is_new_var: false, + ..Default::default() + }], + ); + idx.ingest(&[r], None).await.unwrap(); + + let flow = idx.flow(caller).await.unwrap(); + assert_eq!(flow.chain, vec![caller, 0]); + assert_eq!(flow.calls[0].to_name, "x.run"); + assert_eq!( + flow.calls[0].resolved_via, + ResolutionKind::UnresolvedDynamic + ); + assert!(flow.calls[0].candidate_names.is_empty()); + assert_eq!(idx.stats().edges, 0); + } + + #[tokio::test] + async fn static_resolution_wins_over_hierarchy_fanout() { + let mut idx = GraphIndex::in_memory(); + let (caller, static_fn, trait_id) = (SYMBOL_BASE, SYMBOL_BASE + 1, SYMBOL_BASE + 2); + let r = result( + "t.rs", + vec![ + dsym(caller, "caller", SymbolKind::Function, 0, None, 10), + // hàm tĩnh trùng tên method của trait. + dsym(static_fn, "handle", SymbolKind::Function, 0, None, 5), + dsym(trait_id, "Handler", SymbolKind::Interface, 0, None, 1), + dsym( + SYMBOL_BASE + 3, + "handle", + SymbolKind::Method, + trait_id, + None, + 2, + ), + ], + HashMap::from([(caller, vec![caller, 0])]), + vec![CallRecord { + caller_id: caller, + call_name: "handle".to_string(), + position: 1, + line: 11, + condition: None, + is_loop_body: false, + effect: EffectType::None, + effect_desc: None, + target_class: None, + target_method: None, + into_var: None, + is_new_var: false, + ..Default::default() + }], + ); + idx.ingest(&[r], None).await.unwrap(); + + // Resolve tĩnh thắng — chain slot được ghi đè, không fan-out. + let flow = idx.flow(caller).await.unwrap(); + assert_eq!(flow.chain, vec![caller, static_fn]); + assert_eq!(flow.calls[0].to_id, Some(static_fn)); + assert_eq!(flow.calls[0].resolved_via, ResolutionKind::Exact); + assert!(flow.calls[0].candidate_names.is_empty()); + } + + #[tokio::test] + async fn serde_old_call_record_blob_defaults() { + // Blob cũ (trước khi thêm receiver_text/resolved_via/candidates) phải + // deserialize đúng với giá trị default. + let json = r#"{"caller_id":1,"call_name":"fmt.Println","position":1,"line":7, + "condition":null,"is_loop_body":false,"effect":"log","effect_desc":null, + "target_class":null,"target_method":null,"into_var":null,"is_new_var":false}"#; + let rec: CallRecord = serde_json::from_str(json).unwrap(); + assert_eq!(rec.resolved_via, ResolutionKind::Exact); + assert_eq!(rec.receiver_text, None); + assert!(rec.candidates.is_empty()); + } + #[tokio::test] async fn unresolved_call_keeps_raw_name_in_flow() { let mut idx = GraphIndex::in_memory(); @@ -4511,6 +4952,7 @@ mod tests { target_method: None, into_var: None, is_new_var: false, + ..Default::default() }]; let r = result( "f.ts", @@ -4597,6 +5039,7 @@ mod tests { target_method: None, into_var: None, is_new_var: false, + ..Default::default() }]; let r = result( "f.ts", @@ -4634,6 +5077,7 @@ mod tests { target_method: None, into_var: None, is_new_var: false, + ..Default::default() }; let mut idx = GraphIndex::in_memory(); let first = result( @@ -5277,6 +5721,7 @@ mod tests { target_method: None, into_var: None, is_new_var: false, + ..Default::default() }, CallRecord { caller_id: SYMBOL_BASE, @@ -5292,6 +5737,7 @@ mod tests { target_method: None, into_var: None, is_new_var: false, + ..Default::default() }, // Internal call: class "OrderService" trong repo, method getOrders. CallRecord { @@ -5308,6 +5754,7 @@ mod tests { target_method: None, into_var: None, is_new_var: false, + ..Default::default() }, ]; let mut cls = sym("svc.rs", "OrderService", SYMBOL_BASE + 1); @@ -5415,6 +5862,7 @@ mod tests { doc: None, annotations: Vec::new(), language: "java".into(), + impl_trait: None, }; let legacy_field = Symbol { id: 101, @@ -5431,6 +5879,7 @@ mod tests { doc: None, annotations: Vec::new(), language: "java".into(), + impl_trait: None, }; let legacy_do_work = Symbol { id: 102, @@ -5447,6 +5896,7 @@ mod tests { doc: None, annotations: Vec::new(), language: "java".into(), + impl_trait: None, }; let service = Symbol { id: 103, @@ -5463,6 +5913,7 @@ mod tests { doc: None, annotations: Vec::new(), language: "java".into(), + impl_trait: None, }; let service_do_work = Symbol { id: 104, @@ -5479,6 +5930,7 @@ mod tests { doc: None, annotations: Vec::new(), language: "java".into(), + impl_trait: None, }; // Tạo router method với scope_id = 200 (không phải class scope) // Để field_declared_type tìm trong scope_id=200 sẽ fail → return None. @@ -5498,6 +5950,7 @@ mod tests { doc: None, annotations: Vec::new(), language: "java".into(), + impl_trait: None, }; idx.ingest( &[ParseResult { diff --git a/crates/codegraph-graph/src/memtrack.rs b/crates/codegraph-graph/src/memtrack.rs index 3e486089b0..95c327d87d 100644 --- a/crates/codegraph-graph/src/memtrack.rs +++ b/crates/codegraph-graph/src/memtrack.rs @@ -246,6 +246,7 @@ mod tests { doc: None, annotations: Vec::new(), language: "rust".into(), + impl_trait: None, }; let mem = symbols_mem(&HashMap::from([(100, sym)])); assert_eq!(mem.entries, 1); @@ -265,6 +266,8 @@ mod tests { condition: Some("x > 0".into()), is_loop_body: false, arg_exprs: vec!["a".into(), "b".into()], + resolved_via: codegraph_core::ResolutionKind::Exact, + candidates: 0, }; let mem = call_names_mem(&HashMap::from([("lib::helper_0".to_string(), vec![site])])); assert_eq!(mem.entries, 1); diff --git a/crates/codegraph-graph/src/shared.rs b/crates/codegraph-graph/src/shared.rs index fe8e7628d4..51f4b7d34e 100644 --- a/crates/codegraph-graph/src/shared.rs +++ b/crates/codegraph-graph/src/shared.rs @@ -341,6 +341,7 @@ mod tests { doc: None, annotations: Vec::new(), language: "ts".into(), + impl_trait: None, } } diff --git a/crates/codegraph-graph/src/storage/in_memory.rs b/crates/codegraph-graph/src/storage/in_memory.rs index 624a5fcfd7..6c88719458 100644 --- a/crates/codegraph-graph/src/storage/in_memory.rs +++ b/crates/codegraph-graph/src/storage/in_memory.rs @@ -930,6 +930,7 @@ mod tests { doc: None, annotations: Vec::new(), language: "rust".into(), + impl_trait: None, }; s.save_symbol(&sym).await.unwrap(); let loaded = s.load_symbol(100).await.unwrap(); diff --git a/crates/codegraph-graph/src/storage/mysql.rs b/crates/codegraph-graph/src/storage/mysql.rs index 0d5ac32f78..bf3324b822 100644 --- a/crates/codegraph-graph/src/storage/mysql.rs +++ b/crates/codegraph-graph/src/storage/mysql.rs @@ -1061,5 +1061,6 @@ fn row_to_symbol(row: &MySqlRow) -> Result { doc, annotations, language, + impl_trait: None, }) } diff --git a/crates/codegraph-graph/src/storage/postgres.rs b/crates/codegraph-graph/src/storage/postgres.rs index 02a89db89f..125a5369e6 100644 --- a/crates/codegraph-graph/src/storage/postgres.rs +++ b/crates/codegraph-graph/src/storage/postgres.rs @@ -1078,5 +1078,6 @@ fn row_to_symbol(row: &PgRow) -> Result { doc, annotations, language, + impl_trait: None, }) } diff --git a/crates/codegraph-graph/tests/lmdb.rs b/crates/codegraph-graph/tests/lmdb.rs index bdabfe9e44..53a90ff2d0 100644 --- a/crates/codegraph-graph/tests/lmdb.rs +++ b/crates/codegraph-graph/tests/lmdb.rs @@ -29,6 +29,7 @@ fn sym(file: &str, name: &str, id: u64) -> Symbol { doc: None, annotations: Vec::new(), language: "ts".to_string(), + impl_trait: None, } } @@ -69,6 +70,7 @@ async fn index_ingest_reopen_roundtrip() { target_method: None, into_var: None, is_new_var: false, + ..Default::default() }]; let r = result( "a.ts", @@ -271,6 +273,7 @@ async fn long_path_and_call_name_survive_roundtrip() { target_method: None, into_var: None, is_new_var: false, + ..Default::default() }]; let r = result( &long_path, diff --git a/crates/codegraph-graph/tests/rdbms.rs b/crates/codegraph-graph/tests/rdbms.rs index 756f4206b8..c2aac0ad3f 100644 --- a/crates/codegraph-graph/tests/rdbms.rs +++ b/crates/codegraph-graph/tests/rdbms.rs @@ -37,6 +37,7 @@ fn sym(file: &str, name: &str, id: u64) -> Symbol { doc: None, annotations: Vec::new(), language: "ts".to_string(), + impl_trait: None, } } @@ -95,6 +96,7 @@ async fn rdbms_ingest_reopen_roundtrip() { target_method: None, into_var: None, is_new_var: false, + ..Default::default() }]; let r = result( "a.ts", diff --git a/crates/codegraph-graph/tests/redis.rs b/crates/codegraph-graph/tests/redis.rs index 7c24114337..96f54e809c 100644 --- a/crates/codegraph-graph/tests/redis.rs +++ b/crates/codegraph-graph/tests/redis.rs @@ -36,6 +36,7 @@ fn sym(file: &str, name: &str, id: u64) -> Symbol { doc: None, annotations: Vec::new(), language: "ts".to_string(), + impl_trait: None, } } @@ -89,6 +90,7 @@ async fn redis_ingest_reopen_roundtrip() { target_method: None, into_var: None, is_new_var: false, + ..Default::default() }]; let r = result( "a.ts", diff --git a/crates/codegraph-graph/tests/sqlite.rs b/crates/codegraph-graph/tests/sqlite.rs index e2eeb53c2a..53bdfaa691 100644 --- a/crates/codegraph-graph/tests/sqlite.rs +++ b/crates/codegraph-graph/tests/sqlite.rs @@ -28,6 +28,7 @@ fn sym(file: &str, name: &str, id: u64) -> Symbol { doc: None, annotations: Vec::new(), language: "ts".to_string(), + impl_trait: None, } } @@ -76,6 +77,7 @@ async fn index_ingest_reopen_roundtrip() { target_method: None, into_var: None, is_new_var: false, + ..Default::default() }]; let r = result( "a.ts", @@ -351,6 +353,7 @@ async fn external_qualified_call_not_linked_to_local_variable() { target_method: None, into_var: None, is_new_var: false, + ..Default::default() }]; let r = result( "UserController.java", diff --git a/crates/codegraph-graph/tests/trace_flow_tests.rs b/crates/codegraph-graph/tests/trace_flow_tests.rs index afe6dbd388..210b54dc3e 100644 --- a/crates/codegraph-graph/tests/trace_flow_tests.rs +++ b/crates/codegraph-graph/tests/trace_flow_tests.rs @@ -30,6 +30,7 @@ fn sym(name: &str, id: u64) -> Symbol { file: "t.py".to_string(), line: 1, end_line: 1, + impl_trait: None, signature: None, doc: None, annotations: Vec::new(), @@ -69,6 +70,7 @@ fn call(caller: u64, name: &str, position: usize) -> CallRecord { target_method: None, into_var: None, is_new_var: false, + ..Default::default() } } diff --git a/crates/codegraph-mcp/src/callers_tests.rs b/crates/codegraph-mcp/src/callers_tests.rs index a4f74dbbbf..9bba87a3a5 100644 --- a/crates/codegraph-mcp/src/callers_tests.rs +++ b/crates/codegraph-mcp/src/callers_tests.rs @@ -32,6 +32,7 @@ async fn callers_timeout_resume_dispatch() { doc: None, annotations: vec![], language: "rust".into(), + impl_trait: None, }; let mut idx = GraphIndex::open(&dsn).await.unwrap(); idx.ingest( diff --git a/crates/codegraph-sboxes/tests/control_flow.rs b/crates/codegraph-sboxes/tests/control_flow.rs index 25766fe08e..275ef928c9 100644 --- a/crates/codegraph-sboxes/tests/control_flow.rs +++ b/crates/codegraph-sboxes/tests/control_flow.rs @@ -30,6 +30,7 @@ fn sym(id: u64, name: &str) -> Symbol { doc: None, annotations: Vec::new(), language: "test".to_string(), + impl_trait: None, } } @@ -48,6 +49,7 @@ fn rec(caller_id: u64, pos: usize, name: &str, args: usize) -> CallRecord { target_method: None, into_var: None, is_new_var: false, + ..Default::default() } } diff --git a/crates/codegraph-sboxes/tests/end_to_end.rs b/crates/codegraph-sboxes/tests/end_to_end.rs index 0e37219987..b1ca57f63c 100644 --- a/crates/codegraph-sboxes/tests/end_to_end.rs +++ b/crates/codegraph-sboxes/tests/end_to_end.rs @@ -26,6 +26,7 @@ fn sym(id: u64, name: &str) -> Symbol { doc: None, annotations: Vec::new(), language: "test".to_string(), + impl_trait: None, } } @@ -44,6 +45,7 @@ fn rec(caller_id: u64, pos: usize, name: &str, args: usize) -> CallRecord { target_method: None, into_var: None, is_new_var: false, + ..Default::default() } } From 88a313e0f797134fa592c9cfc73d0bd73d93d1c4 Mon Sep 17 00:00:00 2001 From: Hung Pham Date: Sun, 25 Oct 2026 12:52:00 +0700 Subject: [PATCH 02/19] Enrich Go semantics: method-set matching, function values, imports, concurrency markers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Method-set matching cho implicit interface: receiver_fn hook lưu receiver base type vào Symbol.type_name (`func (f *File) Write` → "File"); khai báo method trong interface body là node `method_elem` (không phải method_declaration) — giờ được extract với scope đúng vào interface symbol. build_go_method_sets tính structural typing: T implements I ⟺ method names(I) ⊆ method names(T); w.Write() fan-out chỉ tới các type thỏa thay vì naive CHA (fallback giữ nguyên khi method-set chưa đủ dữ liệu). - Function value tracking: go_fn_ptr_assign bắt `handler := process` / `handler = process` → PointerAssign fan-out (dùng chung machinery C). - Package/import: FileDeps { package, imports } mới trong ParseResult; go_file_deps đọc package_clause + import_spec (alias→path); alias_qualified_name resolve `orders.Get` qua import alias cho in-repo packages (match segment cuối path). - Concurrency markers: MARKER_GO/MARKER_DEFER (Marker::Go/Defer) vào chain cho `go f()`/`defer f()`; select_statement wired vào switch machinery (communication_case); EffectType::Panic cho panic/log.Fatal/os.Exit. Tests: 5 extractor (receiver type, interface method scope, fn value, file deps, markers) + 2 e2e graph (method-set precision, import alias). Clippy sạch, fmt xong. --- .gitignore | 1 + crates/codegraph-api/tests/api.rs | 8 + crates/codegraph-bench/benches/context.rs | 2 + crates/codegraph-bench/src/bin/mem.rs | 2 + crates/codegraph-binary/src/cache.rs | 2 + crates/codegraph-binary/src/extract.rs | 2 + crates/codegraph-context/src/lib.rs | 6 + crates/codegraph-core/src/lib.rs | 8 +- crates/codegraph-core/src/semgraph.rs | 24 +- crates/codegraph-extract/src/bingraph.rs | 2 + crates/codegraph-extract/src/languages/c.rs | 65 +- .../codegraph-extract/src/languages/common.rs | 81 +- crates/codegraph-extract/src/languages/cpp.rs | 51 +- .../codegraph-extract/src/languages/csharp.rs | 6 + .../src/languages/effects.rs | 4 + crates/codegraph-extract/src/languages/go.rs | 134 ++- .../codegraph-extract/src/languages/java.rs | 26 +- .../src/languages/javascript.rs | 6 + crates/codegraph-extract/src/languages/lua.rs | 6 + crates/codegraph-extract/src/languages/php.rs | 6 + .../codegraph-extract/src/languages/python.rs | 6 + .../codegraph-extract/src/languages/ruby.rs | 6 + .../codegraph-extract/src/languages/rust.rs | 6 + .../codegraph-extract/src/languages/scala.rs | 6 + .../codegraph-extract/src/languages/swift.rs | 6 + .../src/languages/typescript.rs | 6 + crates/codegraph-extract/src/orchestrator.rs | 5 + .../tests/dynamic_dispatch.rs | 294 +++++++ crates/codegraph-graph/src/diff.rs | 2 + crates/codegraph-graph/src/lib.rs | 761 +++++++++++++++++- crates/codegraph-graph/src/shared.rs | 2 + crates/codegraph-graph/tests/lmdb.rs | 2 + crates/codegraph-graph/tests/rdbms.rs | 2 + crates/codegraph-graph/tests/redis.rs | 2 + crates/codegraph-graph/tests/sqlite.rs | 2 + .../codegraph-graph/tests/trace_flow_tests.rs | 2 + crates/codegraph-mcp/src/callers_tests.rs | 2 + crates/codegraph-sboxes/tests/control_flow.rs | 2 + crates/codegraph-sboxes/tests/end_to_end.rs | 2 + 39 files changed, 1513 insertions(+), 45 deletions(-) create mode 100644 crates/codegraph-extract/tests/dynamic_dispatch.rs diff --git a/.gitignore b/.gitignore index 382b11a615..024adb2aec 100644 --- a/.gitignore +++ b/.gitignore @@ -62,3 +62,4 @@ build/ # Bench repos được fetch về (danh sách nguồn: .github/benches/repos/sources.txt) .github/benches/repos/checkout/ .github/benches/repos/list.txt +.zcode/ diff --git a/crates/codegraph-api/tests/api.rs b/crates/codegraph-api/tests/api.rs index e33046338a..d5ef822b9f 100644 --- a/crates/codegraph-api/tests/api.rs +++ b/crates/codegraph-api/tests/api.rs @@ -63,6 +63,8 @@ async fn seed_index(path: &str) -> (u64, u64, u64) { is_new_var: false, ..Default::default() }], + fn_ptr_assigns: Vec::new(), + file_deps: None, }; idx.ingest(&[r], None).await.unwrap(); (caller, callee, helper) @@ -295,6 +297,8 @@ async fn seed_many(db: &str, count: usize) { symbols: vec![sym(id, &name)], chains: HashMap::new(), calls: vec![], + fn_ptr_assigns: Vec::new(), + file_deps: None, }); } idx.ingest(&results, None).await.unwrap(); @@ -325,6 +329,8 @@ async fn seed_annotations(db: &str, count: usize) { symbols: vec![s], chains: HashMap::new(), calls: vec![], + fn_ptr_assigns: Vec::new(), + file_deps: None, }); } idx.ingest(&results, None).await.unwrap(); @@ -361,6 +367,8 @@ async fn seed_references(db: &str, count: usize) { is_new_var: false, ..Default::default() }], + fn_ptr_assigns: Vec::new(), + file_deps: None, }); } idx.ingest(&results, None).await.unwrap(); diff --git a/crates/codegraph-bench/benches/context.rs b/crates/codegraph-bench/benches/context.rs index ea91833881..69e03060b3 100644 --- a/crates/codegraph-bench/benches/context.rs +++ b/crates/codegraph-bench/benches/context.rs @@ -54,6 +54,8 @@ fn fixture(fan_in: usize) -> ParseResult { symbols, chains, calls: Vec::new(), + fn_ptr_assigns: Vec::new(), + file_deps: None, } } diff --git a/crates/codegraph-bench/src/bin/mem.rs b/crates/codegraph-bench/src/bin/mem.rs index ea7a7eba04..4d6237330e 100644 --- a/crates/codegraph-bench/src/bin/mem.rs +++ b/crates/codegraph-bench/src/bin/mem.rs @@ -386,6 +386,8 @@ fn synthetic_parse_result(n: usize, fanout: usize, shape: Shape) -> codegraph_gr symbols, chains, calls, + fn_ptr_assigns: Vec::new(), + file_deps: None, } } diff --git a/crates/codegraph-binary/src/cache.rs b/crates/codegraph-binary/src/cache.rs index 744985738e..94805ea230 100644 --- a/crates/codegraph-binary/src/cache.rs +++ b/crates/codegraph-binary/src/cache.rs @@ -66,6 +66,8 @@ mod tests { symbols: vec![], chains: Default::default(), calls: vec![], + fn_ptr_assigns: Vec::new(), + file_deps: None, }; let entry = SourceEntry::with_stat("bin/ls", Some(1_000_000), Some(12345)); let p = cache_path(&root, std::path::Path::new("/bin/ls"), &entry); diff --git a/crates/codegraph-binary/src/extract.rs b/crates/codegraph-binary/src/extract.rs index 125fc878cb..45e6458d8b 100644 --- a/crates/codegraph-binary/src/extract.rs +++ b/crates/codegraph-binary/src/extract.rs @@ -315,6 +315,8 @@ fn do_extract( symbols, chains, calls, + fn_ptr_assigns: Vec::new(), + file_deps: None, }) } diff --git a/crates/codegraph-context/src/lib.rs b/crates/codegraph-context/src/lib.rs index c2346b8f43..63c09027d0 100644 --- a/crates/codegraph-context/src/lib.rs +++ b/crates/codegraph-context/src/lib.rs @@ -328,6 +328,8 @@ mod tests { symbols: vec![sym_at("RestEndpoint", 100, abs.as_str())], chains: std::collections::HashMap::new(), calls: vec![], + fn_ptr_assigns: Vec::new(), + file_deps: None, }; idx.ingest(&[r], None).await.unwrap(); } @@ -366,6 +368,8 @@ mod tests { symbols: vec![sym("RestEndpoint", 100)], chains: std::collections::HashMap::new(), calls: vec![], + fn_ptr_assigns: Vec::new(), + file_deps: None, }; idx.ingest(&[r], None).await.unwrap(); } @@ -398,6 +402,8 @@ mod tests { symbols: vec![sym("RestEndpoint", 100)], chains: std::collections::HashMap::new(), calls: vec![], + fn_ptr_assigns: Vec::new(), + file_deps: None, }; idx.ingest(&[r], None).await.unwrap(); } diff --git a/crates/codegraph-core/src/lib.rs b/crates/codegraph-core/src/lib.rs index c98b620c75..30482aee8c 100644 --- a/crates/codegraph-core/src/lib.rs +++ b/crates/codegraph-core/src/lib.rs @@ -17,8 +17,8 @@ pub use semgraph::{ FullFlowPath, FunctionScope, Marker, MemberInfo, PathEnding, ResolutionKind, ResolveResult, ScopeLevel, SearchFlowResult, Symbol, SymbolId, SymbolKind, SymbolMatch, TraceAction, MARKER_BRANCH_END, MARKER_BREAK, MARKER_CALL_ENTER, MARKER_CALL_EXIT, MARKER_CATCH_START, - 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, MARKER_TRY_END, MARKER_TRY_START, - SYMBOL_BASE, + MARKER_CONTINUE, MARKER_DEFER, MARKER_GO, 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, MARKER_TRY_END, + MARKER_TRY_START, SYMBOL_BASE, }; diff --git a/crates/codegraph-core/src/semgraph.rs b/crates/codegraph-core/src/semgraph.rs index c361a5900c..86af573e09 100644 --- a/crates/codegraph-core/src/semgraph.rs +++ b/crates/codegraph-core/src/semgraph.rs @@ -42,11 +42,15 @@ pub enum Marker { TryStart = 22, TryEnd = 23, CatchStart = 24, + /// Go `go f()` — call bất đồng bộ. + Go = 25, + /// Go `defer f()` — call hoãn đến khi return. + Defer = 26, } impl Marker { /// All variant values (các hằng MARKER_*). - pub const ALL: [Marker; 20] = [ + pub const ALL: [Marker; 22] = [ Marker::Loop, Marker::RecCall, Marker::IfTrue, @@ -67,6 +71,8 @@ impl Marker { Marker::TryStart, Marker::TryEnd, Marker::CatchStart, + Marker::Go, + Marker::Defer, ]; /// Giá trị u64 tương ứng (cho tương thích với chain cũ). @@ -99,6 +105,8 @@ impl Marker { 22 => Some(Marker::TryStart), 23 => Some(Marker::TryEnd), 24 => Some(Marker::CatchStart), + 25 => Some(Marker::Go), + 26 => Some(Marker::Defer), _ => None, } } @@ -126,6 +134,8 @@ impl Marker { Marker::TryStart => "TRY_START", Marker::TryEnd => "TRY_END", Marker::CatchStart => "CATCH_START", + Marker::Go => "GO", + Marker::Defer => "DEFER", } } @@ -153,6 +163,8 @@ impl Marker { "try_start" => Some(Marker::TryStart), "try_end" => Some(Marker::TryEnd), "catch_start" => Some(Marker::CatchStart), + "go" => Some(Marker::Go), + "defer" => Some(Marker::Defer), _ => None, } } @@ -190,6 +202,8 @@ pub const MARKER_STMT_END: u64 = 21; pub const MARKER_TRY_START: u64 = 22; pub const MARKER_TRY_END: u64 = 23; pub const MARKER_CATCH_START: u64 = 24; +pub const MARKER_GO: u64 = 25; +pub const MARKER_DEFER: u64 = 26; /// Helper để đọc chain sang danh sách marker — tiện cho code đọc. #[allow(dead_code)] @@ -332,6 +346,8 @@ pub enum EffectType { FileRead, FileWrite, Log, + /// Go `panic(...)` / `log.Fatal*` / `os.Exit` — chấm dứt luồng (error edge). + Panic, } impl EffectType { @@ -347,6 +363,7 @@ impl EffectType { Self::FileRead => "file_read", Self::FileWrite => "file_write", Self::Log => "log", + Self::Panic => "panic", } } @@ -364,6 +381,7 @@ impl EffectType { "file_read" => Self::FileRead, "file_write" => Self::FileWrite, "log" => Self::Log, + "panic" => Self::Panic, _ => return None, }) } @@ -455,6 +473,9 @@ pub enum ResolutionKind { HierarchyFanOut, /// Dynamic: method thuộc trait/interface nhưng không có impl trong repo. UnresolvedDynamic, + /// Dynamic: call qua biến function pointer được track qua assign-site + /// (`p = f;` / `void (*p)(int) = f;`) — C/C++ callback pattern. + PointerAssign, } /// Call record thô — persist để render flow khi call không resolve được. @@ -695,6 +716,7 @@ impl From for TraceAction { Marker::Loop | Marker::LoopBack => Self::Loop, Marker::Break | Marker::Continue | Marker::StmtEnd => Self::Statement, Marker::TryStart | Marker::TryEnd | Marker::CatchStart => Self::Statement, + Marker::Go | Marker::Defer => Self::Statement, } } } diff --git a/crates/codegraph-extract/src/bingraph.rs b/crates/codegraph-extract/src/bingraph.rs index 2d420bb0d2..317edbcc28 100644 --- a/crates/codegraph-extract/src/bingraph.rs +++ b/crates/codegraph-extract/src/bingraph.rs @@ -1007,6 +1007,8 @@ mod tests { ..Default::default() }, ], + fn_ptr_assigns: Vec::new(), + file_deps: None, } } diff --git a/crates/codegraph-extract/src/languages/c.rs b/crates/codegraph-extract/src/languages/c.rs index a1a94b4d07..d3befba1be 100644 --- a/crates/codegraph-extract/src/languages/c.rs +++ b/crates/codegraph-extract/src/languages/c.rs @@ -1,10 +1,67 @@ -use crate::languages::common::{CallRule, LangSpec}; +use crate::languages::common::{text, CallRule, LangSpec}; use codegraph_core::SymbolKind; +use tree_sitter::Node; fn ts_language() -> tree_sitter::Language { tree_sitter_c::LANGUAGE.into() } +/// Gán function pointer cho indirect-call resolution: +/// - `zend_compile_file = compile_file;` (assignment_expression) +/// - `zend_op_array *(*f)(...) = compile_file;` (init_declarator) +/// +/// Chỉ nhận LHS/var là identifier thuần và RHS/value là identifier thuần +/// (địa chỉ hàm; `&f` cũng qua identifier con). +pub(crate) fn c_fn_ptr_assign(node: &Node, src: &[u8]) -> Option<(String, String)> { + match node.kind() { + "assignment_expression" => { + let left = node.child_by_field_name("left")?; + let right = node.child_by_field_name("right")?; + if left.kind() != "identifier" { + return None; + } + let var = text(&left, src)?; + let target = identifier_text(&right, src)?; + Some((var, target)) + } + "init_declarator" => { + let declarator = node.child_by_field_name("declarator")?; + let value = node.child_by_field_name("value")?; + // `(*f)(...)` → pointer_declarator → function_declarator → + // pointer_declarator → identifier. Lấy identifier *cuối* của + // declarator (tên biến, không phải kiểu). + let var = last_identifier(&declarator, src)?; + let target = identifier_text(&value, src)?; + Some((var, target)) + } + _ => None, + } +} + +/// Text identifier khi node là identifier thuần. `&f` (address-of) cố tình bỏ — +/// không có type info để phân biệt fn-ptr var với biến thường (tránh false positive). +fn identifier_text(node: &Node, src: &[u8]) -> Option { + match node.kind() { + "identifier" => text(node, src), + _ => None, + } +} + +/// Identifier cuối cùng trong declarator (tên biến — kiểu nằm trước). +fn last_identifier(node: &Node, src: &[u8]) -> Option { + if node.kind() == "identifier" { + return text(node, src); + } + let mut cursor = node.walk(); + let mut found = None; + for ch in node.children(&mut cursor) { + if let Some(t) = last_identifier(&ch, src) { + found = Some(t); // giữ con CUỐI cùng có identifier + } + } + found +} + pub static SPEC: LangSpec = LangSpec { language_name: "c", extensions: &["c"], @@ -37,6 +94,12 @@ pub static SPEC: LangSpec = LangSpec { }], class_type_name: None, impl_trait_fn: None, + refine_kind_fn: None, + fn_ptr_assign_fn: Some(c_fn_ptr_assign), + receiver_fn: None, + file_deps_fn: None, + go_stmt_kinds: &[], + defer_kinds: &[], if_kinds: &["if_statement"], elif_kinds: &[], if_block_kinds: &[], diff --git a/crates/codegraph-extract/src/languages/common.rs b/crates/codegraph-extract/src/languages/common.rs index 9a185956ab..e04065dc53 100644 --- a/crates/codegraph-extract/src/languages/common.rs +++ b/crates/codegraph-extract/src/languages/common.rs @@ -21,7 +21,7 @@ use codegraph_core::{ MARKER_SWITCH_CASE, MARKER_SWITCH_CLOSE, MARKER_SWITCH_END, MARKER_SWITCH_START, MARKER_THROW, MARKER_TRY_END, MARKER_TRY_START, SYMBOL_BASE, }; -use codegraph_graph::ParseResult; +use codegraph_graph::{FileDeps, FnPtrAssign, ParseResult}; use std::collections::HashMap; use tree_sitter::{Node, Parser, Tree}; @@ -35,6 +35,12 @@ pub type TargetFn = fn(&Node, &[u8]) -> (Option, Option); /// Post-process class symbol: `(class node, src) -> type_name` (VD TS heritage). pub type ClassTypeFn = fn(&Node, &[u8]) -> Option; +/// Gán function pointer: `(node, src) -> (var, target)`. +pub type FnPtrAssignFn = fn(&Node, &[u8]) -> Option<(String, String)>; + +/// File deps (package/imports): root node -> FileDeps. +pub type FileDepsFn = fn(&Node, &[u8]) -> Option; + /// Tìm name node cho hàm anonymous theo ngữ cảnh gán (`var a = function(){}`, /// `f = lambda: ...`) — node trả về làm cả name lẫn line để 2 pass khớp nhau. pub type ContextNameFn = for<'a> fn(&Node<'a>) -> Option>; @@ -74,6 +80,24 @@ pub struct LangSpec { /// Trait/interface mà impl block hiện thực: `(class node, src) -> trait name` /// (Rust `impl Display for Foo` → `"Display"`). pub impl_trait_fn: Option, + /// Refine SymbolKind theo nội dung node (Go: `type_spec` có body + /// `interface_type` → Interface). Trả `None` để giữ kind từ `decls`. + pub refine_kind_fn: Option Option>, + /// Nhận gán function pointer `(node, src) -> (var, target)` — C/C++ + /// `p = f;` / `void (*p)(int) = f;`, Go `handler := process`. Quét + /// full-tree, không phụ thuộc chain. + pub fn_ptr_assign_fn: Option, + /// Receiver base type của method (Go: `func (f *File) Write` -> `"File"`), + /// nguyên liệu method-set matching cho implicit interface — gán vào + /// `Symbol.type_name` của method. + pub receiver_fn: Option, + /// Package + imports cua file (Go package_clause/import_declaration) — + /// goi 1 lan tren root node. + pub file_deps_fn: Option, + /// Node kinds launch goroutine (`go_statement`) — marker GO trong chain. + pub go_stmt_kinds: &'static [&'static str], + /// Node kinds defer call (`defer_statement`) — marker DEFER trong chain. + pub defer_kinds: &'static [&'static str], /// Cho phép dùng field `type` làm tên khi thiếu name/declarator (VD Rust /// `impl Foo` — tên nằm ở `type`). Bật cho ngôn ngữ không có node kind /// xung đột (C# `variable_declaration{type}` phải để false). @@ -185,9 +209,36 @@ pub fn run_spec( symbols, chains, calls, + fn_ptr_assigns: collect_fn_ptr_assigns(&root, src, spec), + file_deps: spec.file_deps_fn.and_then(|f| f(&root, src)), }) } +/// Quét full-tree thu gán function pointer (`spec.fn_ptr_assign_fn`) — gồm cả +/// assignment ở scope toàn cục (`zend_compile_file = compile_file;`) mà pass +/// chain (chỉ đi vào function body) không thấy. +fn collect_fn_ptr_assigns(root: &Node, src: &[u8], spec: &LangSpec) -> Vec { + let Some(f) = spec.fn_ptr_assign_fn else { + return Vec::new(); + }; + let mut out = Vec::new(); + let mut stack = vec![*root]; + while let Some(n) = stack.pop() { + if let Some((var, target)) = f(&n, src) { + out.push(FnPtrAssign { + var, + target, + line: n.start_position().row as u32 + 1, + }); + } + let mut cursor = n.walk(); + for ch in n.children(&mut cursor) { + stack.push(ch); + } + } + out +} + fn parse_tree(spec: &'static LangSpec, source: &str) -> Result { let mut parser = Parser::new(); parser @@ -213,6 +264,9 @@ struct SymbolCtx<'a> { fn collect_symbols(node: &Node, ctx: &mut SymbolCtx, spec: &'static LangSpec) { let k = node.kind(); if let Some(&(_, skind)) = spec.decls.iter().find(|(s, _)| *s == k) { + // Refine kind theo nội dung node (VD Go: `type_spec` chứa `interface_type` + // → Interface thay vì Class). + let skind = spec.refine_kind_fn.and_then(|f| f(node)).unwrap_or(skind); if let Some(id) = push_symbol(ctx, node, spec, skind, k) { let is_class = spec.class_kinds.contains(&k); let is_scope = is_class || spec.func_kinds.contains(&k); @@ -338,6 +392,14 @@ fn push_symbol( } else { None }; + // Method nhận receiver base type (Go method-set matching). + let type_name = if kind == SymbolKind::Method && type_name.is_none() { + spec.receiver_fn + .and_then(|f| f(node, ctx.src)) + .or(type_name) + } else { + type_name + }; let line = name_node.start_position().row as u32 + 1; let end_line = node @@ -631,6 +693,23 @@ fn walk_chain( } let k = node.kind(); + // 0. Concurrency semantics: `go f()` / `defer f()` — marker đặt TRƯỚC call + // để edge đọc được ngữ cảnh async/deferred (call bên trong emit bình thường). + if ctx.spec.go_stmt_kinds.contains(&k) { + chain_push(ctx, codegraph_core::MARKER_GO); + for ch in named_children(node) { + walk_chain(ctx, &ch, depth + 1, in_loop, None); + } + return; + } + if ctx.spec.defer_kinds.contains(&k) { + chain_push(ctx, codegraph_core::MARKER_DEFER); + for ch in named_children(node) { + walk_chain(ctx, &ch, depth + 1, in_loop, None); + } + return; + } + // 1. Call sites. if let Some(rule) = ctx.spec.calls.iter().find(|r| r.kind == k) { chain_push(ctx, MARKER_CALL_ENTER); diff --git a/crates/codegraph-extract/src/languages/cpp.rs b/crates/codegraph-extract/src/languages/cpp.rs index 7d198a384d..0324f1abd8 100644 --- a/crates/codegraph-extract/src/languages/cpp.rs +++ b/crates/codegraph-extract/src/languages/cpp.rs @@ -1,10 +1,51 @@ -use crate::languages::common::{CallRule, LangSpec}; +use crate::languages::common::{text, CallRule, LangSpec}; use codegraph_core::SymbolKind; +use tree_sitter::Node; fn ts_language() -> tree_sitter::Language { tree_sitter_cpp::LANGUAGE.into() } +/// Base class của class/struct có inheritance (`base_class_clause`: +/// `class D : public B1, private B2` → "B1,B2"). Base đóng vai interface cho +/// dynamic dispatch: method `virtual` của base + override ở derived. +fn cpp_base_classes(node: &Node, src: &[u8]) -> Option { + if !matches!(node.kind(), "class_specifier" | "struct_specifier") { + return None; + } + let base = node + .children(&mut node.walk()) + .find(|c| c.kind() == "base_class_clause")?; + let mut names: Vec = Vec::new(); + let mut stack = vec![base]; + while let Some(n) = stack.pop() { + match n.kind() { + "type_identifier" | "qualified_identifier" => { + if let Some(t) = text(&n, src) { + // Template: `Base` → "Base". + let name = t.split('<').next().unwrap_or("").trim().to_string(); + if !name.is_empty() + && !matches!( + name.as_str(), + "virtual" | "public" | "private" | "protected" + ) + { + names.push(name); + } + } + } + _ => { + let mut cursor = n.walk(); + for ch in n.children(&mut cursor) { + stack.push(ch); + } + } + } + } + names.dedup(); + (!names.is_empty()).then(|| names.join(",")) +} + pub static SPEC: LangSpec = LangSpec { language_name: "cpp", extensions: &["cpp", "cc", "cxx", "hpp", "hh", "hxx"], @@ -38,7 +79,13 @@ pub static SPEC: LangSpec = LangSpec { target_fn: None, }], class_type_name: None, - impl_trait_fn: None, + impl_trait_fn: Some(cpp_base_classes), + refine_kind_fn: None, + fn_ptr_assign_fn: Some(crate::languages::c::c_fn_ptr_assign), + receiver_fn: None, + file_deps_fn: None, + go_stmt_kinds: &[], + defer_kinds: &[], if_kinds: &["if_statement"], elif_kinds: &[], if_block_kinds: &[], diff --git a/crates/codegraph-extract/src/languages/csharp.rs b/crates/codegraph-extract/src/languages/csharp.rs index 356a2f5419..03c47c1547 100644 --- a/crates/codegraph-extract/src/languages/csharp.rs +++ b/crates/codegraph-extract/src/languages/csharp.rs @@ -65,6 +65,12 @@ pub static SPEC: LangSpec = LangSpec { ], class_type_name: None, impl_trait_fn: None, + refine_kind_fn: None, + fn_ptr_assign_fn: None, + receiver_fn: None, + file_deps_fn: None, + go_stmt_kinds: &[], + defer_kinds: &[], if_kinds: &["if_statement"], elif_kinds: &[], if_block_kinds: &[], diff --git a/crates/codegraph-extract/src/languages/effects.rs b/crates/codegraph-extract/src/languages/effects.rs index 7c62be42a4..ec37250c79 100644 --- a/crates/codegraph-extract/src/languages/effects.rs +++ b/crates/codegraph-extract/src/languages/effects.rs @@ -36,6 +36,10 @@ struct ClassifierRule { /// Bảng default — thứ tự quan trọng, đọc từ trên xuống, pattern đầu tiên match /// sẽ thắng. const DEFAULT_RULES: &[(MatchTy, &str, EffectType)] = &[ + // ── Terminal / error (xét trước mọi thứ — panic chấm dứt luồng) ── + (MatchTy::Exact, "panic", EffectType::Panic), + (MatchTy::Contains, "Fatal", EffectType::Panic), + (MatchTy::Contains, "os.Exit", EffectType::Panic), // ── Prefix-based (high precision) ── (MatchTy::Prefix, "http.", EffectType::HttpCall), (MatchTy::Prefix, "net/http.", EffectType::HttpCall), diff --git a/crates/codegraph-extract/src/languages/go.rs b/crates/codegraph-extract/src/languages/go.rs index 346781e98e..14b4631f5e 100644 --- a/crates/codegraph-extract/src/languages/go.rs +++ b/crates/codegraph-extract/src/languages/go.rs @@ -1,5 +1,6 @@ -use crate::languages::common::{first_identifier, CallRule, LangSpec}; +use crate::languages::common::{first_identifier, named_children, text, CallRule, LangSpec}; use codegraph_core::SymbolKind; +use codegraph_graph::{FileDeps, ImportAlias}; use tree_sitter::Node; fn ts_language() -> tree_sitter::Language { @@ -33,6 +34,8 @@ pub static SPEC: LangSpec = LangSpec { decls: &[ ("function_declaration", SymbolKind::Function), ("method_declaration", SymbolKind::Method), + // Method KHAI BÁO trong interface body — grammar node riêng (mục 4). + ("method_elem", SymbolKind::Method), ("type_spec", SymbolKind::Class), ("var_spec", SymbolKind::Variable), ("const_spec", SymbolKind::Constant), @@ -40,7 +43,10 @@ pub static SPEC: LangSpec = LangSpec { ("func_literal", SymbolKind::Function), ], func_kinds: &["function_declaration", "method_declaration", "func_literal"], - class_kinds: &[], + // `type_spec` là class-like scope: fields của struct và method của + // interface scope vào symbol của type (interface method cần scope đúng để + // dynamic-dispatch fan-out nhận ra). + class_kinds: &["type_spec"], param_kinds: &["parameter_declaration"], annotation_kinds: &[], name_type_fallback: false, @@ -57,13 +63,23 @@ pub static SPEC: LangSpec = LangSpec { }], class_type_name: None, impl_trait_fn: None, + refine_kind_fn: Some(go_refine_kind), + fn_ptr_assign_fn: Some(go_fn_ptr_assign), + receiver_fn: Some(go_receiver_type), + file_deps_fn: Some(go_file_deps), + go_stmt_kinds: &["go_statement"], + defer_kinds: &["defer_statement"], if_kinds: &["if_statement"], elif_kinds: &[], if_block_kinds: &[], loop_kinds: &["for_statement"], - switch_kinds: &["expression_switch_statement", "type_switch_statement"], + switch_kinds: &[ + "expression_switch_statement", + "type_switch_statement", + "select_statement", + ], switch_block_kinds: &[], - switch_case_kinds: &["expression_case", "type_case"], + switch_case_kinds: &["expression_case", "type_case", "communication_case"], switch_default_kinds: &["default_case"], return_kinds: &["return_statement"], break_kinds: &["break_statement"], @@ -80,3 +96,113 @@ pub static SPEC: LangSpec = LangSpec { }; crate::lang_parser!(GoParser, SPEC); + +/// Go interface là ngầm định: `type_spec` có body `interface_type` → Interface +/// (mặc định type_spec là Class). Struct giữ nguyên Class. +fn go_refine_kind(node: &Node) -> Option { + if node.kind() != "type_spec" { + return None; + } + let body = node.child_by_field_name("type")?; + (body.kind() == "interface_type").then_some(SymbolKind::Interface) +} + +/// Receiver base type cua method: `func (f *File) Write` -> "File" (pointer +/// va value nhu nhau - method set matching theo ten type). Generic `List[T]` +/// -> "List". +fn go_receiver_type(node: &Node, src: &[u8]) -> Option { + if node.kind() != "method_declaration" { + return None; + } + let receiver = node.child_by_field_name("receiver")?; + // receiver là parameter_list → parameter_declaration đầu tiên → type. + let param = receiver + .children(&mut receiver.walk()) + .find(|c| c.kind() == "parameter_declaration")?; + let ty = param.child_by_field_name("type")?; + let mut stack = vec![ty]; + while let Some(n) = stack.pop() { + if n.kind() == "type_identifier" { + let raw = text(&n, src)?; + let name = raw.split('[').next()?.trim().to_string(); + return (!name.is_empty()).then_some(name); + } + let mut cursor = n.walk(); + for ch in n.children(&mut cursor) { + stack.push(ch); + } + } + None +} + +/// Function-value assignment: `handler := process` / `handler = process` - +/// ca hai phia la identifier thuan. Go khong co fn pointer kieu C nhung +/// function value dispatch giong het -> dung chung machinery PointerAssign. +fn go_fn_ptr_assign(node: &Node, src: &[u8]) -> Option<(String, String)> { + match node.kind() { + "short_var_declaration" | "assignment_statement" => { + let left = node.child_by_field_name("left")?; + let right = node.child_by_field_name("right")?; + single_identifier(&left, src).zip(single_identifier(&right, src)) + } + _ => None, + } +} + +/// Package + imports cua file: `package orders` + khoi import (alias -> path). +/// Import khong alias: alias = segment cuoi cua path (`net/http` -> `http`). +fn go_file_deps(root: &Node, src: &[u8]) -> Option { + let mut deps = FileDeps::default(); + let mut stack = vec![*root]; + while let Some(n) = stack.pop() { + match n.kind() { + "package_clause" => { + // package_identifier là unnamed child (không có field "name"). + let mut cursor = n.walk(); + for ch in n.children(&mut cursor) { + if ch.kind() == "package_identifier" { + if let Some(t) = text(&ch, src) { + deps.package = Some(t); + } + } + } + } + "import_spec" => { + let path_node = n.child_by_field_name("path")?; + let raw = text(&path_node, src)?; + let path = raw.trim_matches('"').to_string(); + let alias = if let Some(name) = n.child_by_field_name("name") { + text(&name, src)? + } else { + path.rsplit('/').next().unwrap_or("").to_string() + }; + if !alias.is_empty() { + deps.imports.push(ImportAlias { alias, path }); + } + } + _ => {} + } + let mut cursor = n.walk(); + for ch in n.children(&mut cursor) { + stack.push(ch); + } + } + (deps.package.is_some() || !deps.imports.is_empty()).then_some(deps) +} + +/// Identifier duy nhat trong node (expression_list 1 phan tu hoac identifier). +fn single_identifier(node: &Node, src: &[u8]) -> Option { + if node.kind() == "identifier" { + return text(node, src); + } + let kids = named_children(node); + if kids.len() != 1 && node.kind() == "expression_list" { + return None; // `a, b := f, g` - khong track + } + for k in kids { + if k.kind() == "identifier" { + return text(&k, src); + } + } + None +} diff --git a/crates/codegraph-extract/src/languages/java.rs b/crates/codegraph-extract/src/languages/java.rs index 509d6ef529..327346f692 100644 --- a/crates/codegraph-extract/src/languages/java.rs +++ b/crates/codegraph-extract/src/languages/java.rs @@ -108,7 +108,13 @@ pub static SPEC: LangSpec = LangSpec { }, ], class_type_name: None, - impl_trait_fn: None, + impl_trait_fn: Some(java_interfaces), + refine_kind_fn: None, + fn_ptr_assign_fn: None, + receiver_fn: None, + file_deps_fn: None, + go_stmt_kinds: &[], + defer_kinds: &[], if_kinds: &["if_statement"], elif_kinds: &[], if_block_kinds: &[], @@ -137,3 +143,21 @@ pub static SPEC: LangSpec = LangSpec { }; crate::lang_parser!(JavaParser, SPEC); + +/// Danh sách interface mà class implements (`interfaces` field của +/// class_declaration — type_list "Payment, Serializable"). Nhiều interface +/// nối bằng ',' — `build_impl_index` split khi build fan-out index. +fn java_interfaces(node: &Node, src: &[u8]) -> Option { + if !matches!(node.kind(), "class_declaration" | "record_declaration") { + return None; + } + let tl = node.child_by_field_name("interfaces")?; + // Field text mang cả keyword: "implements Payment, Serializable". + let raw = text(&tl, src)?.replacen("implements", "", 1); + let names: Vec<&str> = raw + .split(',') + .map(str::trim) + .filter(|s| !s.is_empty()) + .collect(); + (!names.is_empty()).then(|| names.join(",")) +} diff --git a/crates/codegraph-extract/src/languages/javascript.rs b/crates/codegraph-extract/src/languages/javascript.rs index da1187a9b5..b4cd8e66fe 100644 --- a/crates/codegraph-extract/src/languages/javascript.rs +++ b/crates/codegraph-extract/src/languages/javascript.rs @@ -85,6 +85,12 @@ pub static SPEC: LangSpec = LangSpec { ], class_type_name: Some(class_type_name), impl_trait_fn: None, + refine_kind_fn: None, + fn_ptr_assign_fn: None, + receiver_fn: None, + file_deps_fn: None, + go_stmt_kinds: &[], + defer_kinds: &[], if_kinds: &["if_statement"], elif_kinds: &[], if_block_kinds: &[], diff --git a/crates/codegraph-extract/src/languages/lua.rs b/crates/codegraph-extract/src/languages/lua.rs index a04be64615..71ca89f7ea 100644 --- a/crates/codegraph-extract/src/languages/lua.rs +++ b/crates/codegraph-extract/src/languages/lua.rs @@ -56,6 +56,12 @@ pub static SPEC: LangSpec = LangSpec { }], class_type_name: None, impl_trait_fn: None, + refine_kind_fn: None, + fn_ptr_assign_fn: None, + receiver_fn: None, + file_deps_fn: None, + go_stmt_kinds: &[], + defer_kinds: &[], if_kinds: &["if_statement"], elif_kinds: &[], if_block_kinds: &[], diff --git a/crates/codegraph-extract/src/languages/php.rs b/crates/codegraph-extract/src/languages/php.rs index 7c72af44ec..1da89eb149 100644 --- a/crates/codegraph-extract/src/languages/php.rs +++ b/crates/codegraph-extract/src/languages/php.rs @@ -132,6 +132,12 @@ pub static SPEC: LangSpec = LangSpec { ], class_type_name: None, impl_trait_fn: None, + refine_kind_fn: None, + fn_ptr_assign_fn: None, + receiver_fn: None, + file_deps_fn: None, + go_stmt_kinds: &[], + defer_kinds: &[], if_kinds: &["if_statement"], elif_kinds: &[], if_block_kinds: &[], diff --git a/crates/codegraph-extract/src/languages/python.rs b/crates/codegraph-extract/src/languages/python.rs index 7cc8849d74..d3228c487d 100644 --- a/crates/codegraph-extract/src/languages/python.rs +++ b/crates/codegraph-extract/src/languages/python.rs @@ -47,6 +47,12 @@ pub static SPEC: LangSpec = LangSpec { }], class_type_name: None, impl_trait_fn: None, + refine_kind_fn: None, + fn_ptr_assign_fn: None, + receiver_fn: None, + file_deps_fn: None, + go_stmt_kinds: &[], + defer_kinds: &[], if_kinds: &["if_statement"], elif_kinds: &["elif_clause"], if_block_kinds: &[], diff --git a/crates/codegraph-extract/src/languages/ruby.rs b/crates/codegraph-extract/src/languages/ruby.rs index cdd4d03503..72ed9b5392 100644 --- a/crates/codegraph-extract/src/languages/ruby.rs +++ b/crates/codegraph-extract/src/languages/ruby.rs @@ -64,6 +64,12 @@ pub static SPEC: LangSpec = LangSpec { ], class_type_name: Some(class_type_name), impl_trait_fn: None, + refine_kind_fn: None, + fn_ptr_assign_fn: None, + receiver_fn: None, + file_deps_fn: None, + go_stmt_kinds: &[], + defer_kinds: &[], if_kinds: &["if"], elif_kinds: &["elsif"], if_block_kinds: &[], diff --git a/crates/codegraph-extract/src/languages/rust.rs b/crates/codegraph-extract/src/languages/rust.rs index 8cc61fcb69..8e8f3b3ae9 100644 --- a/crates/codegraph-extract/src/languages/rust.rs +++ b/crates/codegraph-extract/src/languages/rust.rs @@ -45,6 +45,12 @@ pub static SPEC: LangSpec = LangSpec { }], class_type_name: Some(rust_class_type_name), impl_trait_fn: Some(rust_impl_trait_name), + refine_kind_fn: None, + fn_ptr_assign_fn: None, + receiver_fn: None, + file_deps_fn: None, + go_stmt_kinds: &[], + defer_kinds: &[], if_kinds: &["if_expression", "if_let_expression"], elif_kinds: &[], if_block_kinds: &[], diff --git a/crates/codegraph-extract/src/languages/scala.rs b/crates/codegraph-extract/src/languages/scala.rs index 2b47931ebc..6e35f41174 100644 --- a/crates/codegraph-extract/src/languages/scala.rs +++ b/crates/codegraph-extract/src/languages/scala.rs @@ -43,6 +43,12 @@ pub static SPEC: LangSpec = LangSpec { }], class_type_name: None, impl_trait_fn: None, + refine_kind_fn: None, + fn_ptr_assign_fn: None, + receiver_fn: None, + file_deps_fn: None, + go_stmt_kinds: &[], + defer_kinds: &[], if_kinds: &["if_expression"], elif_kinds: &[], if_block_kinds: &[], diff --git a/crates/codegraph-extract/src/languages/swift.rs b/crates/codegraph-extract/src/languages/swift.rs index 90e7fc32de..38522f4fa2 100644 --- a/crates/codegraph-extract/src/languages/swift.rs +++ b/crates/codegraph-extract/src/languages/swift.rs @@ -50,6 +50,12 @@ pub static SPEC: LangSpec = LangSpec { }], class_type_name: None, impl_trait_fn: None, + refine_kind_fn: None, + fn_ptr_assign_fn: None, + receiver_fn: None, + file_deps_fn: None, + go_stmt_kinds: &[], + defer_kinds: &[], if_kinds: &["if_statement"], elif_kinds: &[], // Swift if/for không có consequence/body field — body là node `statements` trần. diff --git a/crates/codegraph-extract/src/languages/typescript.rs b/crates/codegraph-extract/src/languages/typescript.rs index 6b61a21ee0..26864906db 100644 --- a/crates/codegraph-extract/src/languages/typescript.rs +++ b/crates/codegraph-extract/src/languages/typescript.rs @@ -92,6 +92,12 @@ pub static SPEC: LangSpec = LangSpec { ], class_type_name: Some(class_type_name), impl_trait_fn: None, + refine_kind_fn: None, + fn_ptr_assign_fn: None, + receiver_fn: None, + file_deps_fn: None, + go_stmt_kinds: &[], + defer_kinds: &[], if_kinds: &["if_statement"], elif_kinds: &[], if_block_kinds: &[], diff --git a/crates/codegraph-extract/src/orchestrator.rs b/crates/codegraph-extract/src/orchestrator.rs index 90f44ff2a2..112766b93a 100644 --- a/crates/codegraph-extract/src/orchestrator.rs +++ b/crates/codegraph-extract/src/orchestrator.rs @@ -89,6 +89,7 @@ impl Orchestrator { // GraphIndex). `[bingraph]` tắt → giữ hành vi cũ (đẩy vào code index). #[cfg(feature = "binary")] let mut bin_stats: Vec = Vec::new(); + #[cfg(feature = "binary")] { let (bin, bin_skipped) = codegraph_binary::collect_binaries(root, &config.binary); @@ -125,14 +126,17 @@ impl Orchestrator { .as_ref() .map(|bar| Arc::new(IngestBar(bar.clone())) as Arc); index.ingest(&parsed, ingest_progress).await?; + // Finish the progress bar on success. if let Some(bar) = progress { bar.finish_with_message("Indexing complete"); } + #[allow(unused_mut)] let mut all_for_stats = parsed; #[cfg(feature = "binary")] all_for_stats.extend(bin_stats); + Ok(stats_of(&all_for_stats, skipped)) } @@ -162,6 +166,7 @@ impl Orchestrator { .collect(); let mut parsed = Vec::new(); let mut skipped = 0u64; + for r in results { match r { Ok(Some(p)) => parsed.push(p), diff --git a/crates/codegraph-extract/tests/dynamic_dispatch.rs b/crates/codegraph-extract/tests/dynamic_dispatch.rs new file mode 100644 index 0000000000..6628a2d6e2 --- /dev/null +++ b/crates/codegraph-extract/tests/dynamic_dispatch.rs @@ -0,0 +1,294 @@ +//! Extraction phục vụ dynamic-dispatch fan-out: `Symbol.impl_trait` +//! (Java implements / C++ base class) và `ParseResult.fn_ptr_assigns` (C/C++ +//! function pointer assign-site). + +use codegraph_core::{SymbolKind, MARKER_DEFER, MARKER_GO, MARKER_LOOP, MARKER_SWITCH_START}; +use codegraph_extract::languages::c::CParser; +use codegraph_extract::languages::cpp::CppParser; +use codegraph_extract::languages::go::GoParser; +use codegraph_extract::languages::java::JavaParser; +use codegraph_extract::LangParser; + +#[test] +fn java_interfaces_extracted() { + let src = r#" +interface Payment { void pay(int amount); } +interface Serializable {} +class CardPayment implements Payment { + public void pay(int amount) {} +} +class Multi implements Payment, Serializable { + public void pay(int amount) {} +} +class Plain {} +"#; + let parsed = JavaParser.parse_file("T.java", src).unwrap(); + + let card = parsed + .symbols + .iter() + .find(|s| s.name == "CardPayment") + .unwrap(); + assert_eq!(card.impl_trait.as_deref(), Some("Payment")); + + let multi = parsed.symbols.iter().find(|s| s.name == "Multi").unwrap(); + assert_eq!(multi.impl_trait.as_deref(), Some("Payment,Serializable")); + + let plain = parsed.symbols.iter().find(|s| s.name == "Plain").unwrap(); + assert_eq!(plain.impl_trait, None); +} + +#[test] +fn go_interface_type_refined_to_interface_kind() { + let src = r#"package main + +type Writer interface { + Write(p []byte) (int, error) +} + +type Buf struct{ data []byte } + +func (b *Buf) Write(p []byte) (int, error) { return len(p), nil } + +func sink(w Writer) { + w.Write(nil) +} +"#; + let parsed = GoParser.parse_file("w.go", src).unwrap(); + + let writer = parsed.symbols.iter().find(|s| s.name == "Writer").unwrap(); + assert_eq!(writer.kind, SymbolKind::Interface); + + let buf = parsed.symbols.iter().find(|s| s.name == "Buf").unwrap(); + assert_eq!(buf.kind, SymbolKind::Class); +} + +#[test] +fn cpp_base_classes_extracted() { + let src = r#" +class Base { +public: + virtual void run() {} +}; +class Derived : public Base { +public: + void run() override {} +}; +"#; + let parsed = CppParser.parse_file("T.cpp", src).unwrap(); + + let derived = parsed.symbols.iter().find(|s| s.name == "Derived").unwrap(); + assert_eq!(derived.impl_trait.as_deref(), Some("Base")); + + let base = parsed.symbols.iter().find(|s| s.name == "Base").unwrap(); + assert_eq!(base.impl_trait, None); +} + +#[test] +fn c_fn_ptr_assigns_extracted() { + let src = r#" +typedef int (*handler_t)(int); + +int compile_file(int x) { return x; } +int dtrace_compile_file(int x) { return x; } + +int (*zend_compile_file)(int) = compile_file; + +void setup(void) { + zend_compile_file = dtrace_compile_file; + void (*local)(int) = compile_file; +} +"#; + let parsed = CParser.parse_file("z.c", src).unwrap(); + + let mut assigns: Vec<(&str, &str)> = parsed + .fn_ptr_assigns + .iter() + .map(|a| (a.var.as_str(), a.target.as_str())) + .collect(); + assigns.sort(); + assert_eq!( + assigns, + vec![ + ("local", "compile_file"), + ("zend_compile_file", "compile_file"), + ("zend_compile_file", "dtrace_compile_file"), + ] + ); +} + +#[test] +fn go_receiver_type_and_interface_method_scope() { + // Chốt mục 4: interface method được extract với scope = interface symbol; + // method thường có receiver base type trong type_name (method-set matching). + let src = r#"package main + +type Writer interface { + Write(p []byte) (int, error) + Flush() error +} + +type Buf struct{} + +func (b *Buf) Write(p []byte) (int, error) { return len(p), nil } +func (b *Buf) Flush() error { return nil } +func (f *File) Write(p []byte) (int, error) { return len(p), nil } +"#; + let parsed = GoParser.parse_file("w.go", src).unwrap(); + + let writer_id = parsed + .symbols + .iter() + .find(|s| s.name == "Writer") + .unwrap() + .id; + // Interface methods: scope vào interface symbol, không có receiver type. + for s in &parsed.symbols { + eprintln!( + "DBG {} {:?} scope={} type={:?} line={}", + s.name, s.kind, s.scope_id, s.type_name, s.line + ); + } + let iface_write = parsed + .symbols + .iter() + .find(|s| s.name == "Write" && s.scope_id == writer_id) + .expect("interface method Write not scoped to Writer"); + assert_eq!(iface_write.kind, SymbolKind::Method); + assert_eq!(iface_write.type_name, None); + + // Method thường: receiver base type. + let buf_write = parsed + .symbols + .iter() + .find(|s| s.name == "Write" && s.type_name.as_deref() == Some("Buf")) + .expect("Buf.Write receiver type missing"); + assert_eq!(buf_write.kind, SymbolKind::Method); + let _file_write = parsed + .symbols + .iter() + .find(|s| s.name == "Write" && s.type_name.as_deref() == Some("File")) + .expect("File.Write receiver type missing"); + assert!(_file_write.type_name.is_some()); +} + +#[test] +fn go_fn_value_assign_extracted() { + let src = r#"package main + +func process(x int) int { return x } + +func run() { + handler := process + handler(1) + handler = process + handler(2) + a, b := 1, 2 +} +"#; + let parsed = GoParser.parse_file("h.go", src).unwrap(); + let assigns: Vec<(&str, &str)> = parsed + .fn_ptr_assigns + .iter() + .map(|a| (a.var.as_str(), a.target.as_str())) + .collect(); + assert_eq!( + assigns, + vec![("handler", "process"), ("handler", "process")] + ); +} + +#[test] +fn go_file_deps_extracted() { + let src = r#"package orders + +import ( + "context" + httpx "net/http" + "corp/api/internal/billing" +) +"#; + let parsed = GoParser.parse_file("o.go", src).unwrap(); + let deps = parsed.file_deps.expect("file deps missing"); + assert_eq!(deps.package.as_deref(), Some("orders")); + let mut pairs: Vec<(&str, &str)> = deps + .imports + .iter() + .map(|i| (i.alias.as_str(), i.path.as_str())) + .collect(); + pairs.sort(); + assert_eq!( + pairs, + vec![ + ("billing", "corp/api/internal/billing"), + ("context", "context"), + ("httpx", "net/http"), + ] + ); +} + +#[test] +fn go_go_defer_select_markers_in_chain() { + let src = r#"package main + +func worker() {} +func cleanup() {} +func recv(ch chan int) {} + +func run(ch chan int) { + for i := 0; i < 3; i++ { + go worker() + defer cleanup() + } + select { + case v := <-ch: + recv(ch) + default: + } +} +"#; + let parsed = GoParser.parse_file("m.go", src).unwrap(); + let run_chain = parsed + .chains + .values() + .find(|c| c.contains(&MARKER_GO) && c.contains(&MARKER_DEFER) && c.contains(&MARKER_LOOP)) + .expect("go/defer markers missing from chain"); + assert!(run_chain.contains(&MARKER_GO)); + assert!(run_chain.contains(&MARKER_DEFER)); + // select → switch structure (SWITCH markers tồn tại trong chain nào đó). + let has_switch = parsed + .chains + .values() + .any(|c| c.contains(&MARKER_SWITCH_START)); + assert!(has_switch, "select_statement should emit switch markers"); +} + +#[test] +fn go_ast_dump() { + use tree_sitter::Parser; + let src = "package main\n\ntype Writer interface {\n\tWrite(p []byte) (int, error)\n}\n\nfunc (f *File) Write(b []byte) error { return nil }\n"; + let mut parser = Parser::new(); + parser + .set_language(&tree_sitter_go::LANGUAGE.into()) + .unwrap(); + let tree = parser.parse(src, None).unwrap(); + fn dump(n: tree_sitter::Node, src: &str, d: usize) { + let mut fields = Vec::new(); + for i in 0..n.child_count() { + let fname = n.field_name_for_child(i as u32).unwrap_or("_"); + fields.push(format!("{}:{}", fname, n.child(i).unwrap().kind())); + } + let txt: String = src[n.byte_range()].chars().take(40).collect(); + eprintln!( + "{}{} [{}] {}", + " ".repeat(d), + n.kind(), + fields.join(","), + txt + ); + for i in 0..n.child_count() { + dump(n.child(i).unwrap(), src, d + 1); + } + } + dump(tree.root_node(), src, 0); +} diff --git a/crates/codegraph-graph/src/diff.rs b/crates/codegraph-graph/src/diff.rs index 3fa2718d6d..782ef77ab2 100644 --- a/crates/codegraph-graph/src/diff.rs +++ b/crates/codegraph-graph/src/diff.rs @@ -515,6 +515,8 @@ mod tests { symbols, chains, calls, + fn_ptr_assigns: Vec::new(), + file_deps: None, } } diff --git a/crates/codegraph-graph/src/lib.rs b/crates/codegraph-graph/src/lib.rs index 8da1ede8c1..a3401f151c 100644 --- a/crates/codegraph-graph/src/lib.rs +++ b/crates/codegraph-graph/src/lib.rs @@ -286,6 +286,42 @@ pub struct ParseResult { pub chains: HashMap>, /// Call records (caller_id local; `position` trỏ placeholder `0` trong chain). pub calls: Vec, + /// Gán function pointer (`p = f;` / `void (*p)(int) = f;`) — nguyên liệu + /// resolve indirect call trong C/C++ (`PointerAssign`). + #[serde(default)] + pub fn_ptr_assigns: Vec, + + /// Package + import của file (`None` nếu ngôn ngữ chưa implement). + #[serde(default)] + pub file_deps: Option, +} + +/// Gán function pointer: biến `var` nhận hàm `target` tại `line`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FnPtrAssign { + pub var: String, + pub target: String, + pub line: u32, +} + +/// Package + imports của một file — nguyên liệu resolve qualified call qua +/// import alias (Go `httpx.Get`, Java/C# `import`...). Generic, không khóa ngôn ngữ. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct FileDeps { + /// Tên package của file (Go `package orders`). + #[serde(default)] + pub package: Option, + /// Import alias → path (`httpx "net/http"` → alias "httpx", path "net/http"; + /// import không alias: alias = segment cuối của path). + #[serde(default)] + pub imports: Vec, +} + +/// Một entry import: `alias` là tên dùng trong code, `path` là module/package. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ImportAlias { + pub alias: String, + pub path: String, } /// Một call record của `ParseResult` **mượn** (không clone) + `caller_id` đã @@ -350,6 +386,16 @@ pub struct GraphIndex { /// dispatch. Build từ `Symbol.impl_trait` khi ingest; in-memory (blob call /// record đã persist candidates nên reopen không cần index này). impl_index: HashMap>, + /// Function-pointer variable (lowercase) → các target function đã gán + /// (`p = f;` qua assign-site tracking). In-memory như `impl_index`. + fn_ptr_index: HashMap>, + /// File path → import aliases (Go `httpx "net/http"`): nguyên liệu resolve + /// qualified call qua import alias. In-memory, build khi ingest. + file_imports: HashMap>, + /// Go implicit interface: interface name (lower) → method ids của các type + /// THỎA interface (method-set matching: interface method names ⊆ type + /// method names). Build khi ingest từ receiver type trong `Symbol.type_name`. + go_impls: HashMap>, /// Số edge `(caller, callee)` — đếm từ chains lúc ingest/rebuild. /// Metadata của edge (condition/effect/...) KHÔNG materialize thành map: /// đã nằm trong call records persist, `flow()` đọc thẳng từ đó. @@ -1018,6 +1064,9 @@ impl GraphIndex { chains_map: HashMap::new(), call_names: HashMap::new(), impl_index: HashMap::new(), + fn_ptr_index: HashMap::new(), + go_impls: HashMap::new(), + file_imports: HashMap::new(), edge_count: 0, files: Vec::new(), next_id: SYMBOL_BASE, @@ -1442,6 +1491,9 @@ impl GraphIndex { self.chains_map.clear(); self.call_names.clear(); self.impl_index.clear(); + self.fn_ptr_index.clear(); + self.go_impls.clear(); + self.file_imports.clear(); self.edge_count = 0; self.files.clear(); self.name_records.clear(); @@ -1489,9 +1541,32 @@ impl GraphIndex { } // Scope index chỉ rebuild sau khi toàn bộ scope id đã là global. self.rebuild_scope_index(); + // Import map: file → alias/path (resolve qualified call `httpx.Get`). + for result in results { + if let Some(deps) = &result.file_deps { + self.file_imports + .insert(result.path.clone(), deps.imports.clone()); + } + } + // Function-pointer assign-sites: resolve target name → symbol id + // (name_index đã đủ sau Phase 1). + for result in results { + for a in &result.fn_ptr_assigns { + let targets = self + .name_index + .get(&a.target.to_lowercase()) + .cloned() + .unwrap_or_default(); + self.fn_ptr_index + .entry(a.var.to_lowercase()) + .or_default() + .extend(targets); + } + } // ── Phase 2: resolve placeholder 0 trong chains ── self.build_impl_index(); + self.build_go_method_sets(); let mut call_meta: HashMap<(u64, usize), (ResolutionKind, Vec)> = HashMap::new(); self.resolve_calls(&all_calls, &mut call_meta); @@ -1664,7 +1739,11 @@ impl GraphIndex { let mut trait_impls: Vec<(String, u32, u32, String)> = Vec::new(); for s in self.symbols.values() { if let Some(tr) = &s.impl_trait { - trait_impls.push((s.file.clone(), s.line, s.end_line, tr.to_lowercase())); + // Java implements nhiều interface ("A,B") — mỗi interface một + // entry fan-out riêng. + for name in tr.split(',').map(str::trim).filter(|n| !n.is_empty()) { + trait_impls.push((s.file.clone(), s.line, s.end_line, name.to_lowercase())); + } } } if trait_impls.is_empty() { @@ -1683,6 +1762,62 @@ impl GraphIndex { } } + /// Method-set matching cho Go implicit interface (structural typing): + /// type T implements interface I ⟺ tập method name của I ⊆ tập method name + /// của T. Nguồn: method symbols có `type_name` = receiver base type + /// (`go_receiver_type`) + method scope vào Interface symbol (language go). + fn build_go_method_sets(&mut self) { + let mut type_methods: HashMap> = HashMap::new(); + let mut iface_methods: HashMap> = HashMap::new(); + for sym in self.symbols.values() { + if sym.language != "go" + || !matches!(sym.kind, SymbolKind::Method | SymbolKind::Function) + { + continue; + } + if let Some(recv) = &sym.type_name { + type_methods + .entry(recv.to_lowercase()) + .or_default() + .insert(sym.name.to_lowercase()); + } + if let Some(tr) = self.symbols.get(&sym.scope_id) + && tr.kind == SymbolKind::Interface + && tr.language == "go" + { + iface_methods + .entry(tr.name.to_lowercase()) + .or_default() + .push(sym.name.to_lowercase()); + } + } + // Satisfaction: mọi method name của iface nằm trong method set của + // type → mọi method CỦA TYPE ĐÓ (receiver type khớp) là candidates. + let mut go_impls: HashMap> = HashMap::new(); + for (iface, needed) in &iface_methods { + for (ty, methods) in &type_methods { + if !needed.iter().all(|m| methods.contains(m)) { + continue; + } + let implementing: Vec = self + .symbols + .values() + .filter(|s| { + s.language == "go" + && matches!(s.kind, SymbolKind::Method | SymbolKind::Function) + && s.type_name.as_deref().map(|t| t.to_lowercase()) == Some(ty.clone()) + }) + .map(|s| s.id) + .collect(); + go_impls + .entry(iface.clone()) + .or_default() + .extend(implementing); + } + } + self.go_impls = go_impls; + } + /// Resolve một call record về real symbol id — trả `None` nếu không được. /// /// Ưu tiên: structural hint (TargetClass/TargetMethod — VD Java class @@ -1717,6 +1852,14 @@ impl GraphIndex { candidates.extend(alias_ids); } + // 3a-bis. Import alias: `httpx.Get` với `httpx "corp/api/orders"` + // → method `Get` trong file chứa segment "orders". + let import_ids = self.import_alias_candidates(caller_id, &call.call_name); + if !import_ids.is_empty() { + via_alias = true; + candidates.extend(import_ids); + } + // 3b. Short name fallback (after last dot). let short = call .call_name @@ -1750,31 +1893,45 @@ impl GraphIndex { // KHAI BÁO trong trait/interface (không phải impl), đây gần như chắc // chắn là dispatch động — chọn 1 best guess ở đây chính là bug // "resolve sai một cách có tự tin". Fan-out mọi impl thay vì chọn. - let trait_names: Vec<&str> = candidates - .iter() - .filter_map(|&id| { - let m = self.symbols.get(&id)?; - if !matches!(m.kind, SymbolKind::Method | SymbolKind::Function) { - return None; - } - let tr = self.symbols.get(&m.scope_id)?; - (tr.kind == SymbolKind::Interface).then_some(tr.name.as_str()) - }) - .collect(); - if !trait_names.is_empty() { + // C++: method của Class nằm trong `impl_index` key (là base class + // của class khác) cũng là virtual dispatch — base đóng vai interface. + let mut trait_hits: Vec<(u64, u64)> = Vec::new(); // (trait id, decl method id) + for &id in &candidates { + let Some(m) = self.symbols.get(&id) else { + continue; + }; + if !matches!(m.kind, SymbolKind::Method | SymbolKind::Function) { + continue; + } + let Some(tr) = self.symbols.get(&m.scope_id) else { + continue; + }; + let is_iface = tr.kind == SymbolKind::Interface; + let is_base = tr.kind == SymbolKind::Class + && self.impl_index.contains_key(&tr.name.to_lowercase()); + if is_iface || is_base { + trait_hits.push((tr.id, id)); + } + } + if !trait_hits.is_empty() { // Fan-out chỉ áp dụng cho method dispatch (có receiver — // `h.handle`). Bare call `handle` với ứng viên Function tĩnh // thì resolve tĩnh thắng (free function trùng tên trait method // là trùng hợp tên, không phải dispatch). let is_method_dispatch = call.call_name.contains('.'); - let all_interface = candidates.iter().all(|&id| { + let all_dynamic = candidates.iter().all(|&id| { self.symbols .get(&id) .and_then(|m| self.symbols.get(&m.scope_id)) - .is_some_and(|tr| tr.kind == SymbolKind::Interface) + .is_some_and(|tr| { + tr.kind == SymbolKind::Interface + || (tr.kind == SymbolKind::Class + && self.impl_index.contains_key(&tr.name.to_lowercase())) + }) }); - if is_method_dispatch || all_interface { - return self.collect_fanout(&trait_names, caller_id); + if is_method_dispatch || all_dynamic { + let short = call.call_name.rsplit('.').next().unwrap_or(""); + return self.collect_fanout(&trait_hits, short, caller_id); } } let kind = if via_alias { @@ -1790,20 +1947,103 @@ impl GraphIndex { // 5. Dynamic dispatch fan-out: short name khớp method khai báo trong // trait/interface → fan-out mọi impl (CHA-style over-approximation). - self.hierarchy_fanout(call, caller_id) + match self.hierarchy_fanout(call, caller_id) { + CallResolution::None => {} + res => return res, + } + + // 6. Function pointer assign-site (C/C++): `p = f;` tracked qua + // `FnPtrAssign` → call `p(...)` fan-out các target đã gán. + if !call.call_name.contains('.') + && let Some(targets) = self.fn_ptr_index.get(&call.call_name.to_lowercase()) + { + let cands: Vec = targets + .iter() + .copied() + .filter(|&id| id != caller_id) + .collect(); + let kind = if cands.is_empty() { + ResolutionKind::UnresolvedDynamic + } else { + ResolutionKind::PointerAssign + }; + return CallResolution::FanOut(cands, kind); + } + + CallResolution::None } - /// Gom candidates fan-out cho danh sách trait/interface names: mọi impl - /// method của trait (qua `impl_index`). + /// Gom candidates fan-out từ các "interface source" (trait/interface Rust, + /// interface Java/Go, base class C++). /// - /// - Có impl → `FanOut(candidates, HierarchyFanOut)`. - /// - Trait method nhưng repo không có impl nào → `FanOut([], UnresolvedDynamic)` - /// (flag ranh giới soundiness — reflection/monkey-patch rơi vào đây). - fn collect_fanout(&self, trait_names: &[&str], caller_id: u64) -> CallResolution { + /// - Rust/Java/C++: candidates = impl methods của source (qua `impl_index`), + /// lọc cùng tên method. C++ thêm chính method của base (non-pure virtual + /// có body — là target khả dĩ). + /// - Go: interface thỏa ngầm, không có implements clause → candidates = + /// mọi method cùng tên trong repo (CHA naive). + /// - Không candidate nào → `FanOut([], UnresolvedDynamic)` (flag ranh giới + /// soundiness — reflection/monkey-patch rơi vào đây). + fn collect_fanout(&self, hits: &[(u64, u64)], short: &str, caller_id: u64) -> CallResolution { let mut cands: Vec = Vec::new(); - for tr in trait_names { - if let Some(ids) = self.impl_index.get(&tr.to_lowercase()) { - cands.extend(ids.iter().copied().filter(|&id| id != caller_id)); + for &(tr_id, decl_id) in hits { + let Some(tr) = self.symbols.get(&tr_id) else { + continue; + }; + let key = tr.name.to_lowercase(); + if tr.language == "go" { + let short_lower = short.to_lowercase(); + // Ưu tiên method-set matching: mọi method của các type THỎA + // interface (structural typing) — chính xác hơn naive CHA. + let mut matched = false; + if let Some(ids) = self.go_impls.get(&key) { + let hit: Vec = ids + .iter() + .copied() + .filter(|&id| { + id != caller_id + && self + .symbols + .get(&id) + .is_some_and(|s| s.name.eq_ignore_ascii_case(short)) + }) + .collect(); + if !hit.is_empty() { + cands.extend(hit); + matched = true; + } + } + if matched { + continue; + } + // Fallback naive CHA: interface method chưa extract đủ để build + // method-set → fan-out mọi method cùng tên (không mất soundness). + if let Some(ids) = self.name_index.get(&short_lower) { + cands.extend(ids.iter().copied().filter(|&id| { + id != caller_id + && self.symbols.get(&id).is_some_and(|s| { + s.kind == SymbolKind::Method + && self + .symbols + .get(&s.scope_id) + .is_none_or(|p| p.kind != SymbolKind::Interface) + }) + })); + } + continue; + } + if let Some(ids) = self.impl_index.get(&key) { + cands.extend(ids.iter().copied().filter(|&id| { + id != caller_id + && self + .symbols + .get(&id) + .is_some_and(|s| s.name.eq_ignore_ascii_case(short)) + })); + } + // C++: method của base class (non-pure virtual) là target khả dĩ. + // Java/Rust interface method là abstract → loại decl. + if tr.kind == SymbolKind::Class { + cands.push(decl_id); } } cands.sort_unstable(); @@ -1838,7 +2078,7 @@ impl GraphIndex { return CallResolution::None; }; // Method của trait/interface nào mang tên này? - let mut trait_names: Vec<&str> = Vec::new(); + let mut trait_hits: Vec<(u64, u64)> = Vec::new(); // (trait id, decl method id) for &mid in method_ids { let Some(m) = self.symbols.get(&mid) else { continue; @@ -1849,13 +2089,13 @@ impl GraphIndex { if let Some(tr) = self.symbols.get(&m.scope_id) && tr.kind == SymbolKind::Interface { - trait_names.push(&tr.name); + trait_hits.push((tr.id, mid)); } } - if trait_names.is_empty() { + if trait_hits.is_empty() { return CallResolution::None; } - self.collect_fanout(&trait_names, caller_id) + self.collect_fanout(&trait_hits, &short, caller_id) } /// Tìm method của class theo tên (scope_id == class id). @@ -2104,6 +2344,44 @@ impl GraphIndex { Ok(()) } + /// Resolve call `var.Method` khi `var` là import alias của file caller: + /// tìm method `Method` trong các symbol mà file chứa segment cuối của + /// import path (monorepo: import "corp/api/orders" → file chứa "orders"). + /// Stdlib ngoài index → trả rỗng (giữ raw, không fake). + fn import_alias_candidates(&self, caller_id: u64, call_name: &str) -> Vec { + let Some(dot) = call_name.find('.') else { + return Vec::new(); + }; + let var = &call_name[..dot]; + let method = &call_name[dot + 1..]; + if method.is_empty() || method.contains('.') { + return Vec::new(); + } + let Some(file) = self.symbols.get(&caller_id).map(|s| s.file.as_str()) else { + return Vec::new(); + }; + let Some(imports) = self.file_imports.get(file) else { + return Vec::new(); + }; + let mut out = Vec::new(); + for imp in imports { + if imp.alias != var { + continue; + } + let tail = imp.path.rsplit('/').next().unwrap_or(""); + if let Some(ids) = self.name_index.get(&method.to_lowercase()) { + out.extend(ids.iter().copied().filter(|&id| { + self.symbols.get(&id).is_some_and(|s| { + matches!(s.kind, SymbolKind::Function | SymbolKind::Method) + && !tail.is_empty() + && s.file.contains(tail) + }) + })); + } + } + out + } + /// Alias type-qualified: `svc.validate` → `orderservice.validate` khi caller /// có var `svc` với `type_name = "orderservice.OrderService"` trong scope. fn alias_qualified_name(&self, caller_id: u64, call_name: &str) -> Option { @@ -4367,6 +4645,8 @@ mod tests { symbols, chains, calls, + fn_ptr_assigns: Vec::new(), + file_deps: None, } } @@ -4921,6 +5201,423 @@ mod tests { assert!(flow.calls[0].candidate_names.is_empty()); } + #[tokio::test] + async fn java_interface_call_fans_out_to_implementations() { + let mut idx = GraphIndex::in_memory(); + let (caller, iface_id, m1, m2) = ( + SYMBOL_BASE, + SYMBOL_BASE + 1, + SYMBOL_BASE + 2, + SYMBOL_BASE + 3, + ); + let r = result( + "T.java", + vec![ + dsym(caller, "caller", SymbolKind::Function, 0, None, 30), + dsym(iface_id, "Payment", SymbolKind::Interface, 0, None, 1), + dsym( + SYMBOL_BASE + 4, + "pay", + SymbolKind::Method, + iface_id, + None, + 2, + ), + dsym( + SYMBOL_BASE + 5, + "CardPayment", + SymbolKind::Class, + 0, + Some("Payment"), + 10, + ), + dsym(m1, "pay", SymbolKind::Method, SYMBOL_BASE + 5, None, 11), + dsym( + SYMBOL_BASE + 7, + "CashPayment", + SymbolKind::Class, + 0, + Some("Payment"), + 20, + ), + dsym(m2, "pay", SymbolKind::Method, SYMBOL_BASE + 7, None, 21), + ], + HashMap::from([(caller, vec![caller, 0])]), + vec![CallRecord { + caller_id: caller, + call_name: "p.pay".to_string(), + position: 1, + line: 31, + condition: None, + is_loop_body: false, + effect: EffectType::None, + effect_desc: None, + target_class: None, + target_method: None, + into_var: None, + is_new_var: false, + ..Default::default() + }], + ); + idx.ingest(&[r], None).await.unwrap(); + + let flow = idx.flow(caller).await.unwrap(); + assert_eq!(flow.calls[0].resolved_via, ResolutionKind::HierarchyFanOut); + assert_eq!(flow.calls[0].candidate_names.len(), 2); + assert_eq!(idx.stats().edges, 2); + } + + #[tokio::test] + async fn cpp_base_class_call_includes_overrides_and_base_method() { + let mut idx = GraphIndex::in_memory(); + let (caller, base_id, derived_impl_id, derived_run, base_run) = ( + SYMBOL_BASE, + SYMBOL_BASE + 1, + SYMBOL_BASE + 2, + SYMBOL_BASE + 3, + SYMBOL_BASE + 4, + ); + let r = result( + "T.cpp", + vec![ + dsym(caller, "caller", SymbolKind::Function, 0, None, 30), + dsym(base_id, "Base", SymbolKind::Class, 0, None, 1), + dsym(base_run, "run", SymbolKind::Method, base_id, None, 2), + // Derived : public Base — impl trait "Base". + dsym( + derived_impl_id, + "Derived", + SymbolKind::Class, + 0, + Some("Base"), + 10, + ), + dsym( + derived_run, + "run", + SymbolKind::Method, + derived_impl_id, + None, + 11, + ), + ], + HashMap::from([(caller, vec![caller, 0])]), + vec![CallRecord { + caller_id: caller, + call_name: "b.run".to_string(), + position: 1, + line: 31, + condition: None, + is_loop_body: false, + effect: EffectType::None, + effect_desc: None, + target_class: None, + target_method: None, + into_var: None, + is_new_var: false, + ..Default::default() + }], + ); + idx.ingest(&[r], None).await.unwrap(); + + let flow = idx.flow(caller).await.unwrap(); + assert_eq!(flow.calls[0].resolved_via, ResolutionKind::HierarchyFanOut); + // Derived::run (override) + Base::run (non-pure virtual, có body). + let mut names = flow.calls[0].candidate_names.clone(); + names.sort(); + assert_eq!(names, vec!["run", "run"]); + assert_eq!(idx.stats().edges, 2); + } + + #[tokio::test] + async fn go_interface_call_fans_out_to_all_same_name_methods() { + // Go thỏa interface ngầm — không có implements clause. dsym set + // language "rust", nên dựng symbol inline với language "go". + let mut idx = GraphIndex::in_memory(); + let gsym = |id: u64, name: &str, kind: SymbolKind, scope_id: u64, line: u32| Symbol { + id, + name: name.to_string(), + kind, + scope: ScopeLevel::Global, + scope_id, + type_ref: 0, + type_name: None, + file: "w.go".to_string(), + line, + end_line: line, + signature: None, + doc: None, + annotations: Vec::new(), + language: "go".to_string(), + impl_trait: None, + }; + let (caller, iface_id) = (SYMBOL_BASE, SYMBOL_BASE + 1); + let r = result( + "w.go", + vec![ + gsym(caller, "sink", SymbolKind::Function, 0, 20), + gsym(iface_id, "Writer", SymbolKind::Interface, 0, 1), + gsym(SYMBOL_BASE + 2, "Write", SymbolKind::Method, iface_id, 2), + // 2 type có method Write (thỏa ngầm). + gsym(SYMBOL_BASE + 3, "Buf", SymbolKind::Class, 0, 5), + gsym( + SYMBOL_BASE + 4, + "Write", + SymbolKind::Method, + SYMBOL_BASE + 3, + 6, + ), + gsym(SYMBOL_BASE + 5, "File", SymbolKind::Class, 0, 10), + gsym( + SYMBOL_BASE + 6, + "Write", + SymbolKind::Method, + SYMBOL_BASE + 5, + 11, + ), + ], + HashMap::from([(caller, vec![caller, 0])]), + vec![CallRecord { + caller_id: caller, + call_name: "w.Write".to_string(), + position: 1, + line: 21, + condition: None, + is_loop_body: false, + effect: EffectType::None, + effect_desc: None, + target_class: None, + target_method: None, + into_var: None, + is_new_var: false, + ..Default::default() + }], + ); + idx.ingest(&[r], None).await.unwrap(); + + let flow = idx.flow(caller).await.unwrap(); + assert_eq!(flow.calls[0].resolved_via, ResolutionKind::HierarchyFanOut); + // Buf.Write + File.Write (method của interface bị loại). + assert_eq!(flow.calls[0].candidate_names.len(), 2); + assert_eq!(idx.stats().edges, 2); + } + + #[tokio::test] + async fn fn_ptr_assign_resolves_indirect_call() { + let mut idx = GraphIndex::in_memory(); + let (caller, compile_file, dtrace) = (SYMBOL_BASE, SYMBOL_BASE + 1, SYMBOL_BASE + 2); + let mut r = result( + "zend.c", + vec![ + dsym(caller, "call_site", SymbolKind::Function, 0, None, 10), + dsym( + compile_file, + "compile_file", + SymbolKind::Function, + 0, + None, + 1, + ), + dsym( + dtrace, + "dtrace_compile_file", + SymbolKind::Function, + 0, + None, + 2, + ), + ], + HashMap::from([(caller, vec![caller, 0])]), + vec![CallRecord { + caller_id: caller, + call_name: "zend_compile_file".to_string(), + position: 1, + line: 11, + condition: None, + is_loop_body: false, + effect: EffectType::None, + effect_desc: None, + target_class: None, + target_method: None, + into_var: None, + is_new_var: false, + ..Default::default() + }], + ); + // Hai assign-site: init (mặc định) + override ở runtime. + r.fn_ptr_assigns = vec![ + FnPtrAssign { + var: "zend_compile_file".to_string(), + target: "compile_file".to_string(), + line: 5, + }, + FnPtrAssign { + var: "zend_compile_file".to_string(), + target: "dtrace_compile_file".to_string(), + line: 9, + }, + ]; + idx.ingest(&[r], None).await.unwrap(); + + let flow = idx.flow(caller).await.unwrap(); + // Không ghi đè chain (raw name giữ nguyên). + assert_eq!(flow.chain, vec![caller, 0]); + assert_eq!(flow.calls[0].resolved_via, ResolutionKind::PointerAssign); + let mut names = flow.calls[0].candidate_names.clone(); + names.sort(); + assert_eq!(names, vec!["compile_file", "dtrace_compile_file"]); + assert_eq!(idx.stats().edges, 2); + } + + #[tokio::test] + async fn go_method_set_fanout_only_implementing_types() { + // Writer{Write, Flush}; Buf có đủ {Write, Flush}; File chỉ có {Write} + // → File KHÔNG thỏa Writer. w.Write() fan-out chỉ Buf.Write. + let mut idx = GraphIndex::in_memory(); + let gsym = |id: u64, + name: &str, + kind: SymbolKind, + scope_id: u64, + type_name: Option<&str>, + line: u32| Symbol { + id, + name: name.to_string(), + kind, + scope: ScopeLevel::Global, + scope_id, + type_ref: 0, + type_name: type_name.map(String::from), + file: "w.go".to_string(), + line, + end_line: line, + signature: None, + doc: None, + annotations: Vec::new(), + language: "go".to_string(), + impl_trait: None, + }; + let (caller, iface_id) = (SYMBOL_BASE, SYMBOL_BASE + 1); + let r = result( + "w.go", + vec![ + gsym(caller, "sink", SymbolKind::Function, 0, None, 30), + gsym(iface_id, "Writer", SymbolKind::Interface, 0, None, 1), + gsym( + SYMBOL_BASE + 2, + "Write", + SymbolKind::Method, + iface_id, + None, + 2, + ), + gsym( + SYMBOL_BASE + 3, + "Flush", + SymbolKind::Method, + iface_id, + None, + 3, + ), + gsym(SYMBOL_BASE + 4, "Buf", SymbolKind::Class, 0, None, 6), + gsym( + SYMBOL_BASE + 5, + "Write", + SymbolKind::Method, + SYMBOL_BASE + 4, + Some("Buf"), + 7, + ), + gsym( + SYMBOL_BASE + 6, + "Flush", + SymbolKind::Method, + SYMBOL_BASE + 4, + Some("Buf"), + 8, + ), + gsym(SYMBOL_BASE + 7, "File", SymbolKind::Class, 0, None, 11), + gsym( + SYMBOL_BASE + 8, + "Write", + SymbolKind::Method, + SYMBOL_BASE + 7, + Some("File"), + 12, + ), + ], + HashMap::from([(caller, vec![caller, 0])]), + vec![CallRecord { + caller_id: caller, + call_name: "w.Write".to_string(), + position: 1, + line: 31, + condition: None, + is_loop_body: false, + effect: EffectType::None, + effect_desc: None, + target_class: None, + target_method: None, + into_var: None, + is_new_var: false, + ..Default::default() + }], + ); + idx.ingest(&[r], None).await.unwrap(); + + let flow = idx.flow(caller).await.unwrap(); + assert_eq!(flow.calls[0].resolved_via, ResolutionKind::HierarchyFanOut); + // Chỉ Buf.Write — File không thỏa method set của Writer. + assert_eq!(flow.calls[0].candidate_names, vec!["Write"]); + } + + #[tokio::test] + async fn import_alias_resolves_qualified_call() { + // File caller import alias `orders` → path "corp/api/orders"; symbol Get + // nằm trong file chứa segment "orders" → resolve qua Alias. + let mut idx = GraphIndex::in_memory(); + let (caller, get_id) = (SYMBOL_BASE, SYMBOL_BASE + 1); + let mut r = result( + "cmd/main.go", + vec![ + dsym(caller, "main", SymbolKind::Function, 0, None, 10), + dsym(get_id, "Get", SymbolKind::Function, 0, None, 1), + ], + HashMap::from([(caller, vec![caller, 0])]), + vec![CallRecord { + caller_id: caller, + call_name: "orders.Get".to_string(), + position: 1, + line: 11, + condition: None, + is_loop_body: false, + effect: EffectType::None, + effect_desc: None, + target_class: None, + target_method: None, + into_var: None, + is_new_var: false, + ..Default::default() + }], + ); + // Symbol Get sống trong file package orders (path chứa segment cuối). + if let Some(get) = r.symbols.iter_mut().find(|s| s.name == "Get") { + get.file = "corp/api/orders/handler.go".to_string(); + } + r.file_deps = Some(crate::FileDeps { + package: Some("main".to_string()), + imports: vec![crate::ImportAlias { + alias: "orders".to_string(), + path: "corp/api/orders".to_string(), + }], + }); + idx.ingest(&[r], None).await.unwrap(); + + let flow = idx.flow(caller).await.unwrap(); + assert_eq!(flow.chain, vec![caller, get_id]); + assert_eq!(flow.calls[0].to_id, Some(get_id)); + assert_eq!(flow.calls[0].resolved_via, ResolutionKind::Alias); + } + #[tokio::test] async fn serde_old_call_record_blob_defaults() { // Blob cũ (trước khi thêm receiver_text/resolved_via/candidates) phải @@ -5968,6 +6665,8 @@ mod tests { ], chains: HashMap::new(), calls: vec![], + fn_ptr_assigns: Vec::new(), + file_deps: None, }], None, ) diff --git a/crates/codegraph-graph/src/shared.rs b/crates/codegraph-graph/src/shared.rs index 51f4b7d34e..13cbec53fc 100644 --- a/crates/codegraph-graph/src/shared.rs +++ b/crates/codegraph-graph/src/shared.rs @@ -356,6 +356,8 @@ mod tests { symbols, chains: std::collections::HashMap::from([(SYMBOL_BASE, chain)]), calls: Vec::::new(), + fn_ptr_assigns: Vec::new(), + file_deps: None, } } diff --git a/crates/codegraph-graph/tests/lmdb.rs b/crates/codegraph-graph/tests/lmdb.rs index 53a90ff2d0..99ffbf91ea 100644 --- a/crates/codegraph-graph/tests/lmdb.rs +++ b/crates/codegraph-graph/tests/lmdb.rs @@ -47,6 +47,8 @@ fn result( symbols, chains, calls, + fn_ptr_assigns: Vec::new(), + file_deps: None, } } diff --git a/crates/codegraph-graph/tests/rdbms.rs b/crates/codegraph-graph/tests/rdbms.rs index c2aac0ad3f..a6fd956333 100644 --- a/crates/codegraph-graph/tests/rdbms.rs +++ b/crates/codegraph-graph/tests/rdbms.rs @@ -55,6 +55,8 @@ fn result( symbols, chains, calls, + fn_ptr_assigns: Vec::new(), + file_deps: None, } } diff --git a/crates/codegraph-graph/tests/redis.rs b/crates/codegraph-graph/tests/redis.rs index 96f54e809c..ffe7b51f4c 100644 --- a/crates/codegraph-graph/tests/redis.rs +++ b/crates/codegraph-graph/tests/redis.rs @@ -54,6 +54,8 @@ fn result( symbols, chains, calls, + fn_ptr_assigns: Vec::new(), + file_deps: None, } } diff --git a/crates/codegraph-graph/tests/sqlite.rs b/crates/codegraph-graph/tests/sqlite.rs index 53bdfaa691..48894d385a 100644 --- a/crates/codegraph-graph/tests/sqlite.rs +++ b/crates/codegraph-graph/tests/sqlite.rs @@ -53,6 +53,8 @@ fn result( symbols, chains, calls, + fn_ptr_assigns: Vec::new(), + file_deps: None, } } diff --git a/crates/codegraph-graph/tests/trace_flow_tests.rs b/crates/codegraph-graph/tests/trace_flow_tests.rs index 210b54dc3e..4a6676b1e4 100644 --- a/crates/codegraph-graph/tests/trace_flow_tests.rs +++ b/crates/codegraph-graph/tests/trace_flow_tests.rs @@ -52,6 +52,8 @@ fn result( symbols, chains, calls, + fn_ptr_assigns: Vec::new(), + file_deps: None, } } diff --git a/crates/codegraph-mcp/src/callers_tests.rs b/crates/codegraph-mcp/src/callers_tests.rs index 9bba87a3a5..9e1e0703db 100644 --- a/crates/codegraph-mcp/src/callers_tests.rs +++ b/crates/codegraph-mcp/src/callers_tests.rs @@ -44,6 +44,8 @@ async fn callers_timeout_resume_dispatch() { symbols: vec![symbol(a, "caller"), symbol(b, "callee")], chains: HashMap::from([(a, vec![a, b])]), calls: vec![], + fn_ptr_assigns: Vec::new(), + file_deps: None, }], None, ) diff --git a/crates/codegraph-sboxes/tests/control_flow.rs b/crates/codegraph-sboxes/tests/control_flow.rs index 275ef928c9..51bab48847 100644 --- a/crates/codegraph-sboxes/tests/control_flow.rs +++ b/crates/codegraph-sboxes/tests/control_flow.rs @@ -67,6 +67,8 @@ fn result( symbols, chains, calls, + fn_ptr_assigns: Vec::new(), + file_deps: None, } } diff --git a/crates/codegraph-sboxes/tests/end_to_end.rs b/crates/codegraph-sboxes/tests/end_to_end.rs index b1ca57f63c..c578ea5caf 100644 --- a/crates/codegraph-sboxes/tests/end_to_end.rs +++ b/crates/codegraph-sboxes/tests/end_to_end.rs @@ -63,6 +63,8 @@ fn result( symbols, chains, calls, + fn_ptr_assigns: Vec::new(), + file_deps: None, } } From aea43345e244961a43ed93a1ccdaefdce4400b2f Mon Sep 17 00:00:00 2001 From: Hung Pham Date: Sun, 25 Oct 2026 14:57:30 +0700 Subject: [PATCH 03/19] =?UTF-8?q?Go=20method-set:=20match=20signature,=20k?= =?UTF-8?q?h=C3=B4ng=20ch=E1=BB=89=20t=C3=AAn=20+=20gom=20LangSpec=20th?= =?UTF-8?q?=C3=A0nh=20nh=C3=B3m?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Symbol.sig_key: signature key chuẩn hóa `(param_types)(result_types)` (bỏ tên param) qua sig_key_fn hook; go_signature_key dùng chung cho method_declaration/method_elem/function_declaration. build_go_method_sets thỏa interface ⟺ (name, sig_key) khớp — `Wrong.Read(string) string` không còn bị coi là implement `Reader.Read([]byte) (int, error)`. Thiếu key nào → fallback name-only (lenient); interface 0 method → skip. - LangSpec refactor: gom 3 nhóm base / hooks (HookSpec — các Option, HookSpec::none()) / markers (MarkerSpec — control-flow kinds + field names, MarkerSpec::dflt()); LangSpec::dflt() const + FRU hợp lệ trong static. 14 language spec chỉ khai field KHÁC default — thêm hook mới không còn phải sửa 14 file. --- crates/codegraph-api/src/mermaid.rs | 1 + crates/codegraph-api/tests/api.rs | 1 + crates/codegraph-bench/benches/context.rs | 1 + crates/codegraph-bench/src/bin/mem.rs | 1 + crates/codegraph-binary/src/extract.rs | 4 + crates/codegraph-context/src/lib.rs | 1 + crates/codegraph-core/src/semgraph.rs | 4 + crates/codegraph-extract/src/bingraph.rs | 1 + crates/codegraph-extract/src/languages/c.rs | 53 ++-- .../codegraph-extract/src/languages/common.rs | 249 ++++++++++++------ crates/codegraph-extract/src/languages/cpp.rs | 67 ++--- .../codegraph-extract/src/languages/csharp.rs | 63 ++--- crates/codegraph-extract/src/languages/go.rs | 117 ++++---- .../codegraph-extract/src/languages/java.rs | 66 ++--- .../src/languages/javascript.rs | 73 +++-- crates/codegraph-extract/src/languages/lua.rs | 51 +--- crates/codegraph-extract/src/languages/php.rs | 69 ++--- .../codegraph-extract/src/languages/python.rs | 61 ++--- .../codegraph-extract/src/languages/ruby.rs | 55 ++-- .../codegraph-extract/src/languages/rust.rs | 67 ++--- .../codegraph-extract/src/languages/scala.rs | 52 ++-- .../codegraph-extract/src/languages/swift.rs | 53 ++-- .../src/languages/typescript.rs | 73 +++-- .../tests/dynamic_dispatch.rs | 39 +++ crates/codegraph-graph/src/diff.rs | 1 + crates/codegraph-graph/src/lib.rs | 138 +++++++++- crates/codegraph-graph/src/memtrack.rs | 1 + crates/codegraph-graph/src/shared.rs | 1 + .../codegraph-graph/src/storage/in_memory.rs | 1 + crates/codegraph-graph/src/storage/mysql.rs | 1 + .../codegraph-graph/src/storage/postgres.rs | 1 + crates/codegraph-graph/tests/lmdb.rs | 1 + crates/codegraph-graph/tests/rdbms.rs | 1 + crates/codegraph-graph/tests/redis.rs | 1 + crates/codegraph-graph/tests/sqlite.rs | 1 + .../codegraph-graph/tests/trace_flow_tests.rs | 1 + crates/codegraph-mcp/src/callers_tests.rs | 1 + crates/codegraph-sboxes/tests/control_flow.rs | 1 + crates/codegraph-sboxes/tests/end_to_end.rs | 1 + 39 files changed, 737 insertions(+), 637 deletions(-) diff --git a/crates/codegraph-api/src/mermaid.rs b/crates/codegraph-api/src/mermaid.rs index 7bf05cee00..4932a04504 100644 --- a/crates/codegraph-api/src/mermaid.rs +++ b/crates/codegraph-api/src/mermaid.rs @@ -754,6 +754,7 @@ mod tests { annotations: Vec::new(), language: "typescript".into(), impl_trait: None, + sig_key: None, }, chain, chain_desc: desc.into_iter().map(String::from).collect(), diff --git a/crates/codegraph-api/tests/api.rs b/crates/codegraph-api/tests/api.rs index d5ef822b9f..406722194b 100644 --- a/crates/codegraph-api/tests/api.rs +++ b/crates/codegraph-api/tests/api.rs @@ -23,6 +23,7 @@ fn sym(id: u64, name: &str) -> Symbol { annotations: Vec::new(), language: "typescript".into(), impl_trait: None, + sig_key: None, } } diff --git a/crates/codegraph-bench/benches/context.rs b/crates/codegraph-bench/benches/context.rs index 69e03060b3..753d202146 100644 --- a/crates/codegraph-bench/benches/context.rs +++ b/crates/codegraph-bench/benches/context.rs @@ -36,6 +36,7 @@ fn fixture(fan_in: usize) -> ParseResult { annotations: Vec::new(), language: "rust".into(), impl_trait: None, + sig_key: None, }); let chain = if i == 0 { vec![id] diff --git a/crates/codegraph-bench/src/bin/mem.rs b/crates/codegraph-bench/src/bin/mem.rs index 4d6237330e..310802e420 100644 --- a/crates/codegraph-bench/src/bin/mem.rs +++ b/crates/codegraph-bench/src/bin/mem.rs @@ -334,6 +334,7 @@ fn synthetic_parse_result(n: usize, fanout: usize, shape: Shape) -> codegraph_gr annotations: Vec::::new(), language: "rust".into(), impl_trait: None, + sig_key: None, }); } diff --git a/crates/codegraph-binary/src/extract.rs b/crates/codegraph-binary/src/extract.rs index 45e6458d8b..321038a232 100644 --- a/crates/codegraph-binary/src/extract.rs +++ b/crates/codegraph-binary/src/extract.rs @@ -155,6 +155,7 @@ fn do_extract( annotations, language: "binary".to_string(), impl_trait: None, + sig_key: None, }); } @@ -206,6 +207,7 @@ fn do_extract( annotations, language: "binary".to_string(), impl_trait: None, + sig_key: None, }); } @@ -250,6 +252,7 @@ fn do_extract( }], language: "binary".to_string(), impl_trait: None, + sig_key: None, }); } @@ -275,6 +278,7 @@ fn do_extract( annotations: Vec::new(), language: "binary".to_string(), impl_trait: None, + sig_key: None, }); } diff --git a/crates/codegraph-context/src/lib.rs b/crates/codegraph-context/src/lib.rs index 63c09027d0..ce255d872c 100644 --- a/crates/codegraph-context/src/lib.rs +++ b/crates/codegraph-context/src/lib.rs @@ -292,6 +292,7 @@ mod tests { annotations: Vec::new(), language: "java".into(), impl_trait: None, + sig_key: None, } } diff --git a/crates/codegraph-core/src/semgraph.rs b/crates/codegraph-core/src/semgraph.rs index 86af573e09..943be70158 100644 --- a/crates/codegraph-core/src/semgraph.rs +++ b/crates/codegraph-core/src/semgraph.rs @@ -441,6 +441,10 @@ pub struct Symbol { /// → `"Display"`) — nguyên liệu cho dynamic-dispatch fan-out index. #[serde(default)] pub impl_trait: Option, + /// Signature key chuẩn hóa `(param_types)(result_types)` (bỏ tên param) — + /// Go method-set matching so cả signature, không chỉ tên method. + #[serde(default)] + pub sig_key: Option, } /// Annotation (VD `@Override`, `@Cacheable`). diff --git a/crates/codegraph-extract/src/bingraph.rs b/crates/codegraph-extract/src/bingraph.rs index 317edbcc28..4fe2faa006 100644 --- a/crates/codegraph-extract/src/bingraph.rs +++ b/crates/codegraph-extract/src/bingraph.rs @@ -918,6 +918,7 @@ mod tests { annotations, language: "binary".to_string(), impl_trait: None, + sig_key: None, } } diff --git a/crates/codegraph-extract/src/languages/c.rs b/crates/codegraph-extract/src/languages/c.rs index d3befba1be..9754bac551 100644 --- a/crates/codegraph-extract/src/languages/c.rs +++ b/crates/codegraph-extract/src/languages/c.rs @@ -1,4 +1,4 @@ -use crate::languages::common::{text, CallRule, LangSpec}; +use crate::languages::common::{text, CallRule, HookSpec, LangSpec, MarkerSpec}; use codegraph_core::SymbolKind; use tree_sitter::Node; @@ -79,12 +79,6 @@ pub static SPEC: LangSpec = LangSpec { func_kinds: &["function_definition"], class_kinds: &["struct_specifier", "union_specifier"], param_kinds: &["parameter_declaration"], - annotation_kinds: &[], - name_type_fallback: false, - - link_impl_methods: false, - anonymous_name_fn: None, - value_func_kinds: &[], calls: &[CallRule { kind: "call_expression", callee_field: "function", @@ -92,34 +86,21 @@ pub static SPEC: LangSpec = LangSpec { name_fn: None, target_fn: None, }], - class_type_name: None, - impl_trait_fn: None, - refine_kind_fn: None, - fn_ptr_assign_fn: Some(c_fn_ptr_assign), - receiver_fn: None, - file_deps_fn: None, - go_stmt_kinds: &[], - defer_kinds: &[], - if_kinds: &["if_statement"], - elif_kinds: &[], - if_block_kinds: &[], - loop_kinds: &["for_statement", "while_statement", "do_statement"], - switch_kinds: &["switch_statement"], - switch_block_kinds: &[], - switch_case_kinds: &["case_statement"], - switch_default_kinds: &["default_statement"], - return_kinds: &["return_statement"], - break_kinds: &["break_statement"], - continue_kinds: &["continue_statement"], - throw_kinds: &[], - try_kinds: &[], - except_kinds: &[], - try_else_kinds: &[], - finally_kinds: &[], - if_cond_field: "condition", - if_cons_field: "consequence", - if_alt_field: "alternative", - body_field: "body", + hooks: HookSpec { + fn_ptr_assign_fn: Some(c_fn_ptr_assign), + ..HookSpec::none() + }, + markers: MarkerSpec { + if_kinds: &["if_statement"], + loop_kinds: &["for_statement", "while_statement", "do_statement"], + switch_kinds: &["switch_statement"], + switch_case_kinds: &["case_statement"], + switch_default_kinds: &["default_statement"], + return_kinds: &["return_statement"], + break_kinds: &["break_statement"], + continue_kinds: &["continue_statement"], + ..MarkerSpec::dflt() + }, + ..LangSpec::dflt() }; - crate::lang_parser!(CParser, SPEC); diff --git a/crates/codegraph-extract/src/languages/common.rs b/crates/codegraph-extract/src/languages/common.rs index e04065dc53..430052ff62 100644 --- a/crates/codegraph-extract/src/languages/common.rs +++ b/crates/codegraph-extract/src/languages/common.rs @@ -57,8 +57,10 @@ pub struct CallRule { pub target_fn: Option, } -/// Declarative spec của một ngôn ngữ. -#[allow(clippy::struct_excessive_bools)] +/// Declarative spec của một ngôn ngữ — gom 3 nhóm: base / `hooks` (các +/// callback tùy ngôn ngữ, mặc định none) / `markers` (control-flow rules). +/// Mỗi language file chỉ khai field KHÁC default (FRU `..X::dflt()` hợp lệ +/// trong `static`). pub struct LangSpec { pub language_name: &'static str, pub extensions: &'static [&'static str], @@ -75,45 +77,103 @@ pub struct LangSpec { pub annotation_kinds: &'static [&'static str], /// Call rules. pub calls: &'static [CallRule], + /// Cho phép dùng field `type` làm tên khi thiếu name/declarator (VD Rust + /// `impl Foo` — tên nằm ở `type`). Bật cho ngôn ngữ không có node kind + /// xung đột (C# `variable_declaration{type}` phải để false). + pub name_type_fallback: bool, + /// Rust: tách `struct Foo` (def) và `impl Foo` (impl) thành 2 symbol Class + /// cùng tên, methods scoped vào impl. Bật flag để re-parent methods từ impl + /// về symbol def cùng tên (xem `link_impl_methods_to_def`). Chỉ bật cho Rust. + pub link_impl_methods: bool, + /// Callback tùy ngôn ngữ — mặc định [`HookSpec::none()`]. + pub hooks: HookSpec, + /// Control-flow/marker rules + field names — mặc định [`MarkerSpec::dflt()`]. + pub markers: MarkerSpec, +} + +impl LangSpec { + /// Base defaults (các field bắt buộc — language/decls/calls — language + /// file PHẢI ghi đè; placeholder here chỉ để FRU hợp lệ). + pub const fn dflt() -> Self { + Self { + language_name: "", + extensions: &[], + ts_language: unspecified_ts_language, + decls: &[], + func_kinds: &[], + class_kinds: &[], + param_kinds: &[], + annotation_kinds: &[], + calls: &[], + name_type_fallback: false, + link_impl_methods: false, + hooks: HookSpec::none(), + markers: MarkerSpec::dflt(), + } + } +} + +/// Placeholder cho [`LangSpec::dflt`] — language file không đặt `ts_language` +/// sẽ panic lúc parse (không bao giờ chạy trong thực tế). +fn unspecified_ts_language() -> tree_sitter::Language { + unreachable!("LangSpec.ts_language must be set per language") +} + +/// Callback tùy ngôn ngữ — hầu hết là `None`; ngôn ngữ có hook mới ghi đè +/// qua FRU: `hooks: HookSpec { receiver_fn: Some(..), ..HookSpec::none() }`. +#[allow(clippy::type_complexity)] +pub struct HookSpec { /// Lấy type_name cho class symbol (TS `extends`/`implements` heritage). pub class_type_name: Option, - /// Trait/interface mà impl block hiện thực: `(class node, src) -> trait name` - /// (Rust `impl Display for Foo` → `"Display"`). + /// Trait/interface mà impl block hiện thực (Rust `impl Display for Foo`, + /// Java `implements A, B`, C++ base class). pub impl_trait_fn: Option, /// Refine SymbolKind theo nội dung node (Go: `type_spec` có body /// `interface_type` → Interface). Trả `None` để giữ kind từ `decls`. pub refine_kind_fn: Option Option>, - /// Nhận gán function pointer `(node, src) -> (var, target)` — C/C++ - /// `p = f;` / `void (*p)(int) = f;`, Go `handler := process`. Quét - /// full-tree, không phụ thuộc chain. + /// Nhận gán function pointer (C/C++ `p = f;`, Go `handler := process`) — + /// quét full-tree, không phụ thuộc chain. pub fn_ptr_assign_fn: Option, - /// Receiver base type của method (Go: `func (f *File) Write` -> `"File"`), - /// nguyên liệu method-set matching cho implicit interface — gán vào - /// `Symbol.type_name` của method. + /// Receiver base type của method (Go `func (f *File) Write` → `"File"`) + /// — nguyên liệu method-set matching; gán vào `Symbol.type_name`. pub receiver_fn: Option, - /// Package + imports cua file (Go package_clause/import_declaration) — - /// goi 1 lan tren root node. + /// Signature key chuẩn hóa `(param_types)(result_types)` cho + /// function/method — method-set matching so cả signature. + pub sig_key_fn: Option, + /// Package + imports của file (Go package_clause/import_declaration) — + /// gọi 1 lần trên root node. pub file_deps_fn: Option, + /// Hàm anonymous gán qua biến/property: mượn tên theo ngữ cảnh + /// (JS `var a = function(){}`, Py `f = lambda: ...`). + pub anonymous_name_fn: Option, + /// Node kind của giá trị gán là hàm anonymous — decl Variable/Constant + /// chứa nó bị bỏ để tránh trùng tên với Function từ `anonymous_name_fn`. + pub value_func_kinds: &'static [&'static str], +} + +impl HookSpec { + pub const fn none() -> Self { + Self { + class_type_name: None, + impl_trait_fn: None, + refine_kind_fn: None, + fn_ptr_assign_fn: None, + receiver_fn: None, + sig_key_fn: None, + file_deps_fn: None, + anonymous_name_fn: None, + value_func_kinds: &[], + } + } +} + +/// Control-flow/marker rules + field names — mỗi ngôn ngữ khai phần khác +/// default (field names chuẩn: condition/consequence/alternative/body). +pub struct MarkerSpec { /// Node kinds launch goroutine (`go_statement`) — marker GO trong chain. pub go_stmt_kinds: &'static [&'static str], /// Node kinds defer call (`defer_statement`) — marker DEFER trong chain. pub defer_kinds: &'static [&'static str], - /// Cho phép dùng field `type` làm tên khi thiếu name/declarator (VD Rust - /// `impl Foo` — tên nằm ở `type`). Bật cho ngôn ngữ không có node kind - /// xung đột (C# `variable_declaration{type}` phải để false). - pub name_type_fallback: bool, - /// Rust: tách `struct Foo` (def) và `impl Foo` (impl) thành 2 symbol Class - /// cùng tên, methods scoped vào impl. Bật flag để re-parent methods từ impl - /// về symbol def cùng tên (xem `link_impl_methods_to_def`). Chỉ bật cho Rust. - pub link_impl_methods: bool, - /// Hàm anonymous (lambda/function expression) gán qua biến/property: mượn - /// tên theo ngữ cảnh (JS `var a = function(){}`, Py `f = lambda: ...`, - /// Go `f := func(){}`, PHP `$f = function(){}`). - pub anonymous_name_fn: Option, - /// Node kind của giá trị gán là hàm anonymous — decl Variable/Constant chứa - /// nó bị bỏ để tránh trùng tên với Function sinh từ `anonymous_name_fn`. - pub value_func_kinds: &'static [&'static str], - // ── marker rules ── pub if_kinds: &'static [&'static str], pub elif_kinds: &'static [&'static str], /// Block-like node kinds — fallback consequence/alternative khi thiếu field @@ -140,6 +200,35 @@ pub struct LangSpec { pub body_field: &'static str, } +impl MarkerSpec { + pub const fn dflt() -> Self { + Self { + go_stmt_kinds: &[], + defer_kinds: &[], + if_kinds: &[], + elif_kinds: &[], + if_block_kinds: &[], + loop_kinds: &[], + switch_kinds: &[], + switch_block_kinds: &[], + switch_case_kinds: &[], + switch_default_kinds: &[], + return_kinds: &[], + break_kinds: &[], + continue_kinds: &[], + throw_kinds: &[], + try_kinds: &[], + except_kinds: &[], + try_else_kinds: &[], + finally_kinds: &[], + if_cond_field: "condition", + if_cons_field: "consequence", + if_alt_field: "alternative", + body_field: "body", + } + } +} + // ==================== Pipeline ==================== /// Chạy pipeline đầy đủ cho một file → `ParseResult` (input của `GraphIndex::ingest`). @@ -210,15 +299,15 @@ pub fn run_spec( chains, calls, fn_ptr_assigns: collect_fn_ptr_assigns(&root, src, spec), - file_deps: spec.file_deps_fn.and_then(|f| f(&root, src)), + file_deps: spec.hooks.file_deps_fn.and_then(|f| f(&root, src)), }) } -/// Quét full-tree thu gán function pointer (`spec.fn_ptr_assign_fn`) — gồm cả +/// Quét full-tree thu gán function pointer (`spec.hooks.fn_ptr_assign_fn`) — gồm cả /// assignment ở scope toàn cục (`zend_compile_file = compile_file;`) mà pass /// chain (chỉ đi vào function body) không thấy. fn collect_fn_ptr_assigns(root: &Node, src: &[u8], spec: &LangSpec) -> Vec { - let Some(f) = spec.fn_ptr_assign_fn else { + let Some(f) = spec.hooks.fn_ptr_assign_fn else { return Vec::new(); }; let mut out = Vec::new(); @@ -266,7 +355,11 @@ fn collect_symbols(node: &Node, ctx: &mut SymbolCtx, spec: &'static LangSpec) { if let Some(&(_, skind)) = spec.decls.iter().find(|(s, _)| *s == k) { // Refine kind theo nội dung node (VD Go: `type_spec` chứa `interface_type` // → Interface thay vì Class). - let skind = spec.refine_kind_fn.and_then(|f| f(node)).unwrap_or(skind); + let skind = spec + .hooks + .refine_kind_fn + .and_then(|f| f(node)) + .unwrap_or(skind); if let Some(id) = push_symbol(ctx, node, spec, skind, k) { let is_class = spec.class_kinds.contains(&k); let is_scope = is_class || spec.func_kinds.contains(&k); @@ -324,7 +417,7 @@ fn push_symbol( // Hàm anonymous gán qua biến/property thì mượn tên theo ngữ cảnh // (JS `var a = function(){}`, Py `f = lambda: ...`). if spec.func_kinds.contains(&node_kind) || spec.class_kinds.contains(&node_kind) { - spec.anonymous_name_fn.and_then(|f| f(node)) + spec.hooks.anonymous_name_fn.and_then(|f| f(node)) } else { first_identifier(node) } @@ -383,30 +476,36 @@ fn push_symbol( .and_then(|t| text(&t, ctx.src)) } SymbolKind::Class | SymbolKind::Interface | SymbolKind::Enum | SymbolKind::Module => { - spec.class_type_name.and_then(|f| f(node, ctx.src)) + spec.hooks.class_type_name.and_then(|f| f(node, ctx.src)) } _ => None, }; let impl_trait = if matches!(kind, SymbolKind::Class | SymbolKind::Interface) { - spec.impl_trait_fn.and_then(|f| f(node, ctx.src)) + spec.hooks.impl_trait_fn.and_then(|f| f(node, ctx.src)) } else { None }; // Method nhận receiver base type (Go method-set matching). let type_name = if kind == SymbolKind::Method && type_name.is_none() { - spec.receiver_fn + spec.hooks + .receiver_fn .and_then(|f| f(node, ctx.src)) .or(type_name) } else { type_name }; + let sig_key = if matches!(kind, SymbolKind::Method | SymbolKind::Function) { + spec.hooks.sig_key_fn.and_then(|f| f(node, ctx.src)) + } else { + None + }; let line = name_node.start_position().row as u32 + 1; let end_line = node - .child_by_field_name(spec.body_field) + .child_by_field_name(spec.markers.body_field) .map(|b| b.end_position().row as u32 + 1) .unwrap_or_else(|| node.end_position().row as u32 + 1); - let signature = extract_signature(node, ctx.src, spec.body_field); + let signature = extract_signature(node, ctx.src, spec.markers.body_field); let annotations = extract_annotations(node, ctx.src, spec.annotation_kinds); ctx.symbols.push(Symbol { @@ -425,6 +524,7 @@ fn push_symbol( annotations, language: ctx.language.to_string(), impl_trait, + sig_key, }); Some(id) } @@ -635,7 +735,7 @@ fn func_id_of<'a>( let name_node = node .child_by_field_name("name") .or_else(|| name_from_declarator(node)) - .or_else(|| spec.anonymous_name_fn.and_then(|f| f(node))) + .or_else(|| spec.hooks.anonymous_name_fn.and_then(|f| f(node))) .or_else(|| first_identifier(node))?; let name = text(&name_node, src)?; let line = name_node.start_position().row as u32 + 1; @@ -665,7 +765,7 @@ pub fn build_chain( chain: vec![func_id], calls: Vec::new(), }; - if let Some(body) = node.child_by_field_name(spec.body_field) { + if let Some(body) = node.child_by_field_name(spec.markers.body_field) { walk_chain(&mut ctx, &body, 0, 0, None); } else { walk_chain(&mut ctx, node, 0, 0, None); @@ -695,14 +795,14 @@ fn walk_chain( // 0. Concurrency semantics: `go f()` / `defer f()` — marker đặt TRƯỚC call // để edge đọc được ngữ cảnh async/deferred (call bên trong emit bình thường). - if ctx.spec.go_stmt_kinds.contains(&k) { + if ctx.spec.markers.go_stmt_kinds.contains(&k) { chain_push(ctx, codegraph_core::MARKER_GO); for ch in named_children(node) { walk_chain(ctx, &ch, depth + 1, in_loop, None); } return; } - if ctx.spec.defer_kinds.contains(&k) { + if ctx.spec.markers.defer_kinds.contains(&k) { chain_push(ctx, codegraph_core::MARKER_DEFER); for ch in named_children(node) { walk_chain(ctx, &ch, depth + 1, in_loop, None); @@ -722,9 +822,9 @@ fn walk_chain( } // 2. If / ternary. - if ctx.spec.if_kinds.contains(&k) { + if ctx.spec.markers.if_kinds.contains(&k) { chain_push(ctx, MARKER_IF_TRUE); - let cond_node = node.child_by_field_name(ctx.spec.if_cond_field); + let cond_node = node.child_by_field_name(ctx.spec.markers.if_cond_field); let cond_text = cond_node .and_then(|c| text(&c, ctx.src)) .unwrap_or_else(|| condition.clone().unwrap_or_default()); @@ -743,15 +843,15 @@ fn walk_chain( walk_chain(ctx, &cn, depth + 1, in_loop, if_cond.clone()); } let cons = node - .child_by_field_name(ctx.spec.if_cons_field) + .child_by_field_name(ctx.spec.markers.if_cons_field) .or_else(|| first_blockish(node, ctx.spec)); if let Some(cons) = cons { walk_chain(ctx, &cons, depth + 1, in_loop, if_cond.clone()); let alt = node - .child_by_field_name(ctx.spec.if_alt_field) + .child_by_field_name(ctx.spec.markers.if_alt_field) .or_else(|| alternative_after_else(node, ctx.spec)); if let Some(alt) = alt { - if ctx.spec.elif_kinds.contains(&alt.kind()) { + if ctx.spec.markers.elif_kinds.contains(&alt.kind()) { walk_alternative(ctx, &alt, depth, in_loop, if_cond); } else { chain_push(ctx, MARKER_IF_FALSE); @@ -770,7 +870,7 @@ fn walk_chain( } // 3. Loops. - if ctx.spec.loop_kinds.contains(&k) { + if ctx.spec.markers.loop_kinds.contains(&k) { chain_push(ctx, MARKER_LOOP); // Condition của loop (while/for/do): emit calls trong condition + giữ // text làm metadata — trước đây loop mất cả calls lẫn text. @@ -797,7 +897,7 @@ fn walk_chain( walk_chain(ctx, &cn, depth + 1, in_loop + 1, loop_cond.clone()); } } - if let Some(body) = node.child_by_field_name(ctx.spec.body_field) { + if let Some(body) = node.child_by_field_name(ctx.spec.markers.body_field) { walk_chain(ctx, &body, depth + 1, in_loop + 1, loop_cond.clone()); } else { walk_block(ctx, node, depth + 1, in_loop + 1, loop_cond.clone()); @@ -812,7 +912,7 @@ fn walk_chain( } // 4. Switch. - if ctx.spec.switch_kinds.contains(&k) { + if ctx.spec.markers.switch_kinds.contains(&k) { // 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. @@ -847,7 +947,7 @@ fn walk_chain( } // 5. Return. - if ctx.spec.return_kinds.contains(&k) { + if ctx.spec.markers.return_kinds.contains(&k) { chain_push(ctx, MARKER_RETURN); for ch in named_children(node) { walk_chain(ctx, &ch, depth + 1, in_loop, condition.clone()); @@ -874,15 +974,15 @@ fn walk_chain( } // 6. Break / continue / throw. - if ctx.spec.break_kinds.contains(&k) { + if ctx.spec.markers.break_kinds.contains(&k) { chain_push(ctx, MARKER_BREAK); return; } - if ctx.spec.continue_kinds.contains(&k) { + if ctx.spec.markers.continue_kinds.contains(&k) { chain_push(ctx, MARKER_CONTINUE); return; } - if ctx.spec.throw_kinds.contains(&k) { + if ctx.spec.markers.throw_kinds.contains(&k) { chain_push(ctx, MARKER_THROW); // Nhãn exception type ngay tại THROW (`throw ValueError(...)` → // `throw ValueError`) — interpreter dùng để match catch handler. @@ -902,16 +1002,16 @@ fn walk_chain( // TRY_START, pop tại TRY_END) và khi bắt THROW nhảy tới CATCH_START khớp type // (nhãn `except `/`catch ` gắn tại CATCH_START; handler không type // khớp mọi exception). - if ctx.spec.try_kinds.contains(&k) { + if ctx.spec.markers.try_kinds.contains(&k) { chain_push(ctx, MARKER_TRY_START); - if let Some(body) = node.child_by_field_name(ctx.spec.body_field) { + if let Some(body) = node.child_by_field_name(ctx.spec.markers.body_field) { walk_chain(ctx, &body, depth + 1, in_loop, condition.clone()); } else { walk_block(ctx, node, depth + 1, in_loop, condition.clone()); } chain_push(ctx, MARKER_TRY_END); for ch in named_children(node) { - if ctx.spec.except_kinds.contains(&ch.kind()) { + if ctx.spec.markers.except_kinds.contains(&ch.kind()) { chain_push(ctx, MARKER_CATCH_START); // Nhãn handler: `except ValueError:` / `catch (IOException e)` → type. let label = except_label(&ch, ctx.src) @@ -923,12 +1023,12 @@ fn walk_chain( } } for ch in named_children(node) { - if ctx.spec.try_else_kinds.contains(&ch.kind()) { + if ctx.spec.markers.try_else_kinds.contains(&ch.kind()) { walk_clause(ctx, &ch, depth + 1, in_loop, condition.clone()); } } for ch in named_children(node) { - if ctx.spec.finally_kinds.contains(&ch.kind()) { + if ctx.spec.markers.finally_kinds.contains(&ch.kind()) { walk_clause(ctx, &ch, depth + 1, in_loop, condition.clone()); } } @@ -996,9 +1096,9 @@ fn walk_alternative( in_loop: u32, condition: Option, ) { - if ctx.spec.elif_kinds.contains(&alt.kind()) { + if ctx.spec.markers.elif_kinds.contains(&alt.kind()) { chain_push(ctx, MARKER_IF_TRUE); - let cond_node = alt.child_by_field_name(ctx.spec.if_cond_field); + let cond_node = alt.child_by_field_name(ctx.spec.markers.if_cond_field); let cond_text = cond_node .and_then(|c| text(&c, ctx.src)) .unwrap_or_else(|| condition.clone().unwrap_or_default()); @@ -1013,10 +1113,10 @@ fn walk_alternative( if let Some(cn) = cond_node { walk_chain(ctx, &cn, depth + 1, in_loop, elif_cond.clone()); } - if let Some(cons) = alt.child_by_field_name(ctx.spec.if_cons_field) { + if let Some(cons) = alt.child_by_field_name(ctx.spec.markers.if_cons_field) { walk_chain(ctx, &cons, depth + 1, in_loop, elif_cond.clone()); } - if let Some(next) = alt.child_by_field_name(ctx.spec.if_alt_field) { + if let Some(next) = alt.child_by_field_name(ctx.spec.markers.if_alt_field) { walk_alternative(ctx, &next, depth, in_loop, negate_cond(elif_cond)); } chain_push(ctx, MARKER_BRANCH_END); @@ -1061,7 +1161,7 @@ fn walk_clause( in_loop: u32, condition: Option, ) { - if let Some(b) = node.child_by_field_name(ctx.spec.body_field) { + if let Some(b) = node.child_by_field_name(ctx.spec.markers.body_field) { walk_chain(ctx, &b, depth, in_loop, condition); } else { walk_block(ctx, node, depth, in_loop, condition); @@ -1084,17 +1184,17 @@ fn negate_cond(cond: Option) -> Option { /// expression trần là child. Bỏ qua child có field (`for x in xs` — Python/JS /// `left`/`right` fielded, không phải condition). fn loop_condition_node<'a>(node: &Node<'a>, spec: &'static LangSpec) -> Option> { - if let Some(cn) = node.child_by_field_name(spec.if_cond_field) { + if let Some(cn) = node.child_by_field_name(spec.markers.if_cond_field) { return Some(cn); } - let body = node.child_by_field_name(spec.body_field); + let body = node.child_by_field_name(spec.markers.body_field); for i in 0..node.child_count() { let Some(ch) = node.child(i) else { continue }; if !ch.is_named() || Some(ch) == body || node.field_name_for_child(i as u32).is_some() { continue; } // Go `for_clause` bọc init/cond/post — field nằm trên child. - if let Some(cn) = ch.child_by_field_name(spec.if_cond_field) { + if let Some(cn) = ch.child_by_field_name(spec.markers.if_cond_field) { return Some(cn); } // Go dạng `for cond {}`: child expression trần. @@ -1116,13 +1216,13 @@ fn loop_condition_node<'a>(node: &Node<'a>, spec: &'static LangSpec) -> Option(node: &Node<'a>, spec: &'static LangSpec) -> Option> { named_children(node) .into_iter() - .find(|c| spec.if_block_kinds.contains(&c.kind())) + .find(|c| spec.markers.if_block_kinds.contains(&c.kind())) } /// Alternative fallback: blockish node đứng sau keyword `else` (Swift có node /// `else` tên riêng; các ngôn ngữ khác dùng field alternative). fn alternative_after_else<'a>(node: &Node<'a>, spec: &'static LangSpec) -> Option> { - if spec.if_block_kinds.is_empty() { + if spec.markers.if_block_kinds.is_empty() { return None; } let children = named_children(node); @@ -1130,20 +1230,21 @@ fn alternative_after_else<'a>(node: &Node<'a>, spec: &'static LangSpec) -> Optio children .iter() .skip(else_pos + 1) - .find(|c| spec.if_block_kinds.contains(&c.kind())) + .find(|c| spec.markers.if_block_kinds.contains(&c.kind())) .copied() } /// Gom case children của switch node (hỗ trợ wrapper như Java `switch_block`). fn switch_cases<'a>(node: &Node<'a>, spec: &'static LangSpec) -> Vec> { let is_case = |n: &Node| { - spec.switch_case_kinds.contains(&n.kind()) || spec.switch_default_kinds.contains(&n.kind()) + spec.markers.switch_case_kinds.contains(&n.kind()) + || spec.markers.switch_default_kinds.contains(&n.kind()) }; let mut out = Vec::new(); for ch in named_children(node) { if is_case(&ch) { out.push(ch); - } else if spec.switch_block_kinds.contains(&ch.kind()) { + } else if spec.markers.switch_block_kinds.contains(&ch.kind()) { for cc in named_children(&ch) { if is_case(&cc) { out.push(cc); @@ -1521,7 +1622,7 @@ fn is_conversion_declarator(n: &Node) -> bool { /// trung gian của phép gán (expression_list/assignment_statement/variable_list — /// Go/Lua bọc value) — không vào object/block để khỏi ăn nhầm hàm lồng sâu. fn decl_value_is_func(node: &Node, spec: &LangSpec) -> bool { - if spec.value_func_kinds.is_empty() { + if spec.hooks.value_func_kinds.is_empty() { return false; } decl_value_is_func_at(node, spec, 0) @@ -1532,7 +1633,7 @@ fn decl_value_is_func_at(node: &Node, spec: &LangSpec, depth: u32) -> bool { return false; } named_children(node).into_iter().any(|ch| { - spec.value_func_kinds.contains(&ch.kind()) + spec.hooks.value_func_kinds.contains(&ch.kind()) || (matches!( ch.kind(), "expression_list" | "assignment_statement" | "variable_list" diff --git a/crates/codegraph-extract/src/languages/cpp.rs b/crates/codegraph-extract/src/languages/cpp.rs index 0324f1abd8..a8cf656975 100644 --- a/crates/codegraph-extract/src/languages/cpp.rs +++ b/crates/codegraph-extract/src/languages/cpp.rs @@ -1,4 +1,4 @@ -use crate::languages::common::{text, CallRule, LangSpec}; +use crate::languages::common::{text, CallRule, HookSpec, LangSpec, MarkerSpec}; use codegraph_core::SymbolKind; use tree_sitter::Node; @@ -65,12 +65,6 @@ pub static SPEC: LangSpec = LangSpec { func_kinds: &["function_definition"], class_kinds: &["class_specifier", "struct_specifier", "union_specifier"], param_kinds: &["parameter_declaration"], - annotation_kinds: &[], - name_type_fallback: false, - - link_impl_methods: false, - anonymous_name_fn: None, - value_func_kinds: &[], calls: &[CallRule { kind: "call_expression", callee_field: "function", @@ -78,39 +72,30 @@ pub static SPEC: LangSpec = LangSpec { name_fn: None, target_fn: None, }], - class_type_name: None, - impl_trait_fn: Some(cpp_base_classes), - refine_kind_fn: None, - fn_ptr_assign_fn: Some(crate::languages::c::c_fn_ptr_assign), - receiver_fn: None, - file_deps_fn: None, - go_stmt_kinds: &[], - defer_kinds: &[], - if_kinds: &["if_statement"], - elif_kinds: &[], - if_block_kinds: &[], - loop_kinds: &[ - "for_statement", - "for_range_loop", - "while_statement", - "do_statement", - ], - switch_kinds: &["switch_statement"], - switch_block_kinds: &[], - switch_case_kinds: &["case_statement"], - switch_default_kinds: &["default_statement"], - return_kinds: &["return_statement"], - break_kinds: &["break_statement"], - continue_kinds: &["continue_statement"], - throw_kinds: &["throw_statement"], - try_kinds: &["try_statement"], - except_kinds: &["catch_clause"], - try_else_kinds: &[], - finally_kinds: &[], - if_cond_field: "condition", - if_cons_field: "consequence", - if_alt_field: "alternative", - body_field: "body", + hooks: HookSpec { + impl_trait_fn: Some(cpp_base_classes), + fn_ptr_assign_fn: Some(crate::languages::c::c_fn_ptr_assign), + ..HookSpec::none() + }, + markers: MarkerSpec { + if_kinds: &["if_statement"], + loop_kinds: &[ + "for_statement", + "for_range_loop", + "while_statement", + "do_statement", + ], + switch_kinds: &["switch_statement"], + switch_case_kinds: &["case_statement"], + switch_default_kinds: &["default_statement"], + return_kinds: &["return_statement"], + break_kinds: &["break_statement"], + continue_kinds: &["continue_statement"], + throw_kinds: &["throw_statement"], + try_kinds: &["try_statement"], + except_kinds: &["catch_clause"], + ..MarkerSpec::dflt() + }, + ..LangSpec::dflt() }; - crate::lang_parser!(CppParser, SPEC); diff --git a/crates/codegraph-extract/src/languages/csharp.rs b/crates/codegraph-extract/src/languages/csharp.rs index 03c47c1547..9e4511f451 100644 --- a/crates/codegraph-extract/src/languages/csharp.rs +++ b/crates/codegraph-extract/src/languages/csharp.rs @@ -1,4 +1,4 @@ -use crate::languages::common::{text, CallRule, LangSpec}; +use crate::languages::common::{text, CallRule, HookSpec, LangSpec, MarkerSpec}; use codegraph_core::SymbolKind; use tree_sitter::Node; @@ -42,11 +42,6 @@ pub static SPEC: LangSpec = LangSpec { ], param_kinds: &["parameter"], annotation_kinds: &["attribute"], - name_type_fallback: false, - - link_impl_methods: false, - anonymous_name_fn: None, - value_func_kinds: &[], calls: &[ CallRule { kind: "invocation_expression", @@ -63,41 +58,29 @@ pub static SPEC: LangSpec = LangSpec { target_fn: None, }, ], - class_type_name: None, - impl_trait_fn: None, - refine_kind_fn: None, - fn_ptr_assign_fn: None, - receiver_fn: None, - file_deps_fn: None, - go_stmt_kinds: &[], - defer_kinds: &[], - if_kinds: &["if_statement"], - elif_kinds: &[], - if_block_kinds: &[], - loop_kinds: &[ - "for_statement", - "foreach_statement", - "while_statement", - "do_statement", - ], - switch_kinds: &["switch_statement", "switch_expression"], - switch_block_kinds: &["switch_body"], - switch_case_kinds: &["switch_section", "switch_expression_arm"], - switch_default_kinds: &[], - return_kinds: &["return_statement"], - break_kinds: &["break_statement"], - continue_kinds: &["continue_statement"], - throw_kinds: &["throw_statement"], - try_kinds: &["try_statement"], - except_kinds: &["catch_clause", "catch_declaration"], - try_else_kinds: &[], - finally_kinds: &["finally_clause"], - if_cond_field: "condition", - if_cons_field: "consequence", - if_alt_field: "alternative", - body_field: "body", + hooks: HookSpec::none(), + markers: MarkerSpec { + if_kinds: &["if_statement"], + loop_kinds: &[ + "for_statement", + "foreach_statement", + "while_statement", + "do_statement", + ], + switch_kinds: &["switch_statement", "switch_expression"], + switch_block_kinds: &["switch_body"], + switch_case_kinds: &["switch_section", "switch_expression_arm"], + return_kinds: &["return_statement"], + break_kinds: &["break_statement"], + continue_kinds: &["continue_statement"], + throw_kinds: &["throw_statement"], + try_kinds: &["try_statement"], + except_kinds: &["catch_clause", "catch_declaration"], + finally_kinds: &["finally_clause"], + ..MarkerSpec::dflt() + }, + ..LangSpec::dflt() }; - crate::lang_parser!(CSharpParser, SPEC); #[cfg(test)] diff --git a/crates/codegraph-extract/src/languages/go.rs b/crates/codegraph-extract/src/languages/go.rs index 14b4631f5e..9529716477 100644 --- a/crates/codegraph-extract/src/languages/go.rs +++ b/crates/codegraph-extract/src/languages/go.rs @@ -1,4 +1,6 @@ -use crate::languages::common::{first_identifier, named_children, text, CallRule, LangSpec}; +use crate::languages::common::{ + first_identifier, named_children, text, CallRule, HookSpec, LangSpec, MarkerSpec, +}; use codegraph_core::SymbolKind; use codegraph_graph::{FileDeps, ImportAlias}; use tree_sitter::Node; @@ -34,7 +36,6 @@ pub static SPEC: LangSpec = LangSpec { decls: &[ ("function_declaration", SymbolKind::Function), ("method_declaration", SymbolKind::Method), - // Method KHAI BÁO trong interface body — grammar node riêng (mục 4). ("method_elem", SymbolKind::Method), ("type_spec", SymbolKind::Class), ("var_spec", SymbolKind::Variable), @@ -43,17 +44,8 @@ pub static SPEC: LangSpec = LangSpec { ("func_literal", SymbolKind::Function), ], func_kinds: &["function_declaration", "method_declaration", "func_literal"], - // `type_spec` là class-like scope: fields của struct và method của - // interface scope vào symbol của type (interface method cần scope đúng để - // dynamic-dispatch fan-out nhận ra). class_kinds: &["type_spec"], param_kinds: &["parameter_declaration"], - annotation_kinds: &[], - name_type_fallback: false, - - link_impl_methods: false, - anonymous_name_fn: Some(anonymous_name_node), - value_func_kinds: &["func_literal"], calls: &[CallRule { kind: "call_expression", callee_field: "function", @@ -61,40 +53,35 @@ pub static SPEC: LangSpec = LangSpec { name_fn: None, target_fn: None, }], - class_type_name: None, - impl_trait_fn: None, - refine_kind_fn: Some(go_refine_kind), - fn_ptr_assign_fn: Some(go_fn_ptr_assign), - receiver_fn: Some(go_receiver_type), - file_deps_fn: Some(go_file_deps), - go_stmt_kinds: &["go_statement"], - defer_kinds: &["defer_statement"], - if_kinds: &["if_statement"], - elif_kinds: &[], - if_block_kinds: &[], - loop_kinds: &["for_statement"], - switch_kinds: &[ - "expression_switch_statement", - "type_switch_statement", - "select_statement", - ], - switch_block_kinds: &[], - switch_case_kinds: &["expression_case", "type_case", "communication_case"], - switch_default_kinds: &["default_case"], - return_kinds: &["return_statement"], - break_kinds: &["break_statement"], - continue_kinds: &["continue_statement"], - throw_kinds: &[], - try_kinds: &[], - except_kinds: &[], - try_else_kinds: &[], - finally_kinds: &[], - if_cond_field: "condition", - if_cons_field: "consequence", - if_alt_field: "alternative", - body_field: "body", + hooks: HookSpec { + sig_key_fn: Some(go_signature_key), + refine_kind_fn: Some(go_refine_kind), + fn_ptr_assign_fn: Some(go_fn_ptr_assign), + receiver_fn: Some(go_receiver_type), + file_deps_fn: Some(go_file_deps), + anonymous_name_fn: Some(anonymous_name_node), + value_func_kinds: &["func_literal"], + ..HookSpec::none() + }, + markers: MarkerSpec { + go_stmt_kinds: &["go_statement"], + defer_kinds: &["defer_statement"], + if_kinds: &["if_statement"], + loop_kinds: &["for_statement"], + switch_kinds: &[ + "expression_switch_statement", + "type_switch_statement", + "select_statement", + ], + switch_case_kinds: &["expression_case", "type_case", "communication_case"], + switch_default_kinds: &["default_case"], + return_kinds: &["return_statement"], + break_kinds: &["break_statement"], + continue_kinds: &["continue_statement"], + ..MarkerSpec::dflt() + }, + ..LangSpec::dflt() }; - crate::lang_parser!(GoParser, SPEC); /// Go interface là ngầm định: `type_spec` có body `interface_type` → Interface @@ -107,6 +94,48 @@ fn go_refine_kind(node: &Node) -> Option { (body.kind() == "interface_type").then_some(SymbolKind::Interface) } +/// Signature key chuan hoa `(param_types)(result_types)` — bo ten param, giu +/// nguyen type text (`[]byte`, `map[string]int`, `*T`, `time.Time`, `...int`). +/// method_declaration va method_elem (interface) dung cung ham nen key so +/// sanh truc tiep. `Reader.Read` -> `([]byte)(int,error)`. +fn go_signature_key(node: &Node, src: &[u8]) -> Option { + if !matches!( + node.kind(), + "method_declaration" | "method_elem" | "function_declaration" + ) { + return None; + } + let params = node.child_by_field_name("parameters")?; + let mut pt: Vec = Vec::new(); + for ch in named_children(¶ms) { + if let Some(t) = ch.child_by_field_name("type") { + if let Some(txt) = text(&t, src) { + pt.push(compact(&txt)); + } + } + } + let mut rt: Vec = Vec::new(); + if let Some(res) = node.child_by_field_name("result") { + if res.kind() == "parameter_list" { + for ch in named_children(&res) { + if let Some(t) = ch.child_by_field_name("type") { + if let Some(txt) = text(&t, src) { + rt.push(compact(&txt)); + } + } + } + } else if let Some(txt) = text(&res, src) { + rt.push(compact(&txt)); + } + } + Some(format!("({})({})", pt.join(","), rt.join(","))) +} + +/// Go bo whitespace trong type text: `map[string] int` -> `map[string]int`. +fn compact(t: &str) -> String { + t.chars().filter(|c| !c.is_whitespace()).collect() +} + /// Receiver base type cua method: `func (f *File) Write` -> "File" (pointer /// va value nhu nhau - method set matching theo ten type). Generic `List[T]` /// -> "List". diff --git a/crates/codegraph-extract/src/languages/java.rs b/crates/codegraph-extract/src/languages/java.rs index 327346f692..a648406748 100644 --- a/crates/codegraph-extract/src/languages/java.rs +++ b/crates/codegraph-extract/src/languages/java.rs @@ -1,4 +1,4 @@ -use crate::languages::common::{named_children, text, CallRule, LangSpec}; +use crate::languages::common::{named_children, text, CallRule, HookSpec, LangSpec, MarkerSpec}; use codegraph_core::SymbolKind; use tree_sitter::Node; @@ -86,11 +86,6 @@ pub static SPEC: LangSpec = LangSpec { ], param_kinds: &["formal_parameter"], annotation_kinds: &["annotation", "marker_annotation"], - name_type_fallback: false, - - link_impl_methods: false, - anonymous_name_fn: None, - value_func_kinds: &[], calls: &[ CallRule { kind: "method_invocation", @@ -107,41 +102,32 @@ pub static SPEC: LangSpec = LangSpec { target_fn: None, }, ], - class_type_name: None, - impl_trait_fn: Some(java_interfaces), - refine_kind_fn: None, - fn_ptr_assign_fn: None, - receiver_fn: None, - file_deps_fn: None, - go_stmt_kinds: &[], - defer_kinds: &[], - if_kinds: &["if_statement"], - elif_kinds: &[], - if_block_kinds: &[], - loop_kinds: &[ - "for_statement", - "enhanced_for_statement", - "while_statement", - "do_statement", - ], - switch_kinds: &["switch_expression", "switch_statement"], - switch_block_kinds: &["switch_block"], - switch_case_kinds: &["switch_block_statement_group", "switch_rule"], - switch_default_kinds: &[], - return_kinds: &["return_statement"], - break_kinds: &["break_statement"], - continue_kinds: &["continue_statement"], - throw_kinds: &["throw_statement"], - try_kinds: &["try_statement", "try_with_resources_statement"], - except_kinds: &["catch_clause"], - try_else_kinds: &[], - finally_kinds: &["finally_clause"], - if_cond_field: "condition", - if_cons_field: "consequence", - if_alt_field: "alternative", - body_field: "body", + hooks: HookSpec { + impl_trait_fn: Some(java_interfaces), + ..HookSpec::none() + }, + markers: MarkerSpec { + if_kinds: &["if_statement"], + loop_kinds: &[ + "for_statement", + "enhanced_for_statement", + "while_statement", + "do_statement", + ], + switch_kinds: &["switch_expression", "switch_statement"], + switch_block_kinds: &["switch_block"], + switch_case_kinds: &["switch_block_statement_group", "switch_rule"], + return_kinds: &["return_statement"], + break_kinds: &["break_statement"], + continue_kinds: &["continue_statement"], + throw_kinds: &["throw_statement"], + try_kinds: &["try_statement", "try_with_resources_statement"], + except_kinds: &["catch_clause"], + finally_kinds: &["finally_clause"], + ..MarkerSpec::dflt() + }, + ..LangSpec::dflt() }; - crate::lang_parser!(JavaParser, SPEC); /// Danh sách interface mà class implements (`interfaces` field của diff --git a/crates/codegraph-extract/src/languages/javascript.rs b/crates/codegraph-extract/src/languages/javascript.rs index b4cd8e66fe..66425189b7 100644 --- a/crates/codegraph-extract/src/languages/javascript.rs +++ b/crates/codegraph-extract/src/languages/javascript.rs @@ -1,4 +1,4 @@ -use crate::languages::common::{named_children, text, CallRule, LangSpec}; +use crate::languages::common::{named_children, text, CallRule, HookSpec, LangSpec, MarkerSpec}; use codegraph_core::SymbolKind; use tree_sitter::Node; @@ -60,13 +60,6 @@ pub static SPEC: LangSpec = LangSpec { "method_definition", ], class_kinds: &["class_declaration", "class"], - param_kinds: &[], - annotation_kinds: &[], - name_type_fallback: false, - - link_impl_methods: false, - anonymous_name_fn: Some(anonymous_name_node), - value_func_kinds: &["function_expression", "arrow_function"], calls: &[ CallRule { kind: "call_expression", @@ -83,40 +76,34 @@ pub static SPEC: LangSpec = LangSpec { target_fn: None, }, ], - class_type_name: Some(class_type_name), - impl_trait_fn: None, - refine_kind_fn: None, - fn_ptr_assign_fn: None, - receiver_fn: None, - file_deps_fn: None, - go_stmt_kinds: &[], - defer_kinds: &[], - if_kinds: &["if_statement"], - elif_kinds: &[], - if_block_kinds: &[], - loop_kinds: &[ - "for_statement", - "for_in_statement", - "for_of_statement", - "while_statement", - "do_statement", - ], - switch_kinds: &["switch_statement"], - switch_block_kinds: &["switch_body"], - switch_case_kinds: &["switch_case"], - switch_default_kinds: &["switch_default"], - return_kinds: &["return_statement"], - break_kinds: &["break_statement"], - continue_kinds: &["continue_statement"], - throw_kinds: &["throw_statement"], - try_kinds: &["try_statement"], - except_kinds: &["catch_clause"], - try_else_kinds: &[], - finally_kinds: &["finally_clause"], - if_cond_field: "condition", - if_cons_field: "consequence", - if_alt_field: "alternative", - body_field: "body", + hooks: HookSpec { + class_type_name: Some(class_type_name), + anonymous_name_fn: Some(anonymous_name_node), + value_func_kinds: &["function_expression", "arrow_function"], + ..HookSpec::none() + }, + markers: MarkerSpec { + if_kinds: &["if_statement"], + loop_kinds: &[ + "for_statement", + "for_in_statement", + "for_of_statement", + "while_statement", + "do_statement", + ], + switch_kinds: &["switch_statement"], + switch_block_kinds: &["switch_body"], + switch_case_kinds: &["switch_case"], + switch_default_kinds: &["switch_default"], + return_kinds: &["return_statement"], + break_kinds: &["break_statement"], + continue_kinds: &["continue_statement"], + throw_kinds: &["throw_statement"], + try_kinds: &["try_statement"], + except_kinds: &["catch_clause"], + finally_kinds: &["finally_clause"], + ..MarkerSpec::dflt() + }, + ..LangSpec::dflt() }; - crate::lang_parser!(JavaScriptParser, SPEC); diff --git a/crates/codegraph-extract/src/languages/lua.rs b/crates/codegraph-extract/src/languages/lua.rs index 71ca89f7ea..41dafe8802 100644 --- a/crates/codegraph-extract/src/languages/lua.rs +++ b/crates/codegraph-extract/src/languages/lua.rs @@ -1,4 +1,4 @@ -use crate::languages::common::{named_children, CallRule, LangSpec}; +use crate::languages::common::{named_children, CallRule, HookSpec, LangSpec, MarkerSpec}; use codegraph_core::SymbolKind; use tree_sitter::Node; @@ -40,13 +40,6 @@ pub static SPEC: LangSpec = LangSpec { "local_function", ], class_kinds: &[], - param_kinds: &[], - annotation_kinds: &[], - name_type_fallback: false, - - link_impl_methods: false, - anonymous_name_fn: Some(anonymous_name_node), - value_func_kinds: &["function_definition"], calls: &[CallRule { kind: "function_call", callee_field: "name", @@ -54,34 +47,18 @@ pub static SPEC: LangSpec = LangSpec { name_fn: None, target_fn: None, }], - class_type_name: None, - impl_trait_fn: None, - refine_kind_fn: None, - fn_ptr_assign_fn: None, - receiver_fn: None, - file_deps_fn: None, - go_stmt_kinds: &[], - defer_kinds: &[], - if_kinds: &["if_statement"], - elif_kinds: &[], - if_block_kinds: &[], - loop_kinds: &["for_statement", "while_statement", "repeat_statement"], - switch_kinds: &[], - switch_block_kinds: &[], - switch_case_kinds: &[], - switch_default_kinds: &[], - return_kinds: &["return_statement"], - break_kinds: &["break_statement"], - continue_kinds: &[], - throw_kinds: &[], - try_kinds: &[], - except_kinds: &[], - try_else_kinds: &[], - finally_kinds: &[], - if_cond_field: "condition", - if_cons_field: "consequence", - if_alt_field: "alternative", - body_field: "body", + hooks: HookSpec { + anonymous_name_fn: Some(anonymous_name_node), + value_func_kinds: &["function_definition"], + ..HookSpec::none() + }, + markers: MarkerSpec { + if_kinds: &["if_statement"], + loop_kinds: &["for_statement", "while_statement", "repeat_statement"], + return_kinds: &["return_statement"], + break_kinds: &["break_statement"], + ..MarkerSpec::dflt() + }, + ..LangSpec::dflt() }; - crate::lang_parser!(LuaParser, SPEC); diff --git a/crates/codegraph-extract/src/languages/php.rs b/crates/codegraph-extract/src/languages/php.rs index 1da89eb149..2ca552db43 100644 --- a/crates/codegraph-extract/src/languages/php.rs +++ b/crates/codegraph-extract/src/languages/php.rs @@ -1,4 +1,4 @@ -use crate::languages::common::{text, CallRule, LangSpec}; +use crate::languages::common::{text, CallRule, HookSpec, LangSpec, MarkerSpec}; use codegraph_core::SymbolKind; use tree_sitter::Node; @@ -88,11 +88,6 @@ pub static SPEC: LangSpec = LangSpec { ], param_kinds: &["simple_parameter", "property_promotion_parameter"], annotation_kinds: &["attribute"], - name_type_fallback: false, - - link_impl_methods: false, - anonymous_name_fn: Some(anonymous_name_node), - value_func_kinds: &["anonymous_function", "arrow_function"], calls: &[ CallRule { kind: "function_call_expression", @@ -130,40 +125,32 @@ pub static SPEC: LangSpec = LangSpec { target_fn: None, }, ], - class_type_name: None, - impl_trait_fn: None, - refine_kind_fn: None, - fn_ptr_assign_fn: None, - receiver_fn: None, - file_deps_fn: None, - go_stmt_kinds: &[], - defer_kinds: &[], - if_kinds: &["if_statement"], - elif_kinds: &[], - if_block_kinds: &[], - loop_kinds: &[ - "for_statement", - "foreach_statement", - "while_statement", - "do_statement", - ], - switch_kinds: &["switch_statement"], - switch_block_kinds: &["switch_block"], - switch_case_kinds: &["case_statement"], - switch_default_kinds: &["default_statement"], - return_kinds: &["return_statement"], - break_kinds: &["break_statement"], - continue_kinds: &["continue_statement"], - throw_kinds: &["throw_statement"], - try_kinds: &["try_statement"], - except_kinds: &["catch_clause"], - try_else_kinds: &[], - finally_kinds: &["finally_clause"], - if_cond_field: "condition", - // PHP if_statement đặt nhánh then trong field `body` (không phải `consequence`). - if_cons_field: "body", - if_alt_field: "alternative", - body_field: "body", + hooks: HookSpec { + anonymous_name_fn: Some(anonymous_name_node), + value_func_kinds: &["anonymous_function", "arrow_function"], + ..HookSpec::none() + }, + markers: MarkerSpec { + if_kinds: &["if_statement"], + loop_kinds: &[ + "for_statement", + "foreach_statement", + "while_statement", + "do_statement", + ], + switch_kinds: &["switch_statement"], + switch_block_kinds: &["switch_block"], + switch_case_kinds: &["case_statement"], + switch_default_kinds: &["default_statement"], + return_kinds: &["return_statement"], + break_kinds: &["break_statement"], + continue_kinds: &["continue_statement"], + throw_kinds: &["throw_statement"], + try_kinds: &["try_statement"], + except_kinds: &["catch_clause"], + finally_kinds: &["finally_clause"], + ..MarkerSpec::dflt() + }, + ..LangSpec::dflt() }; - crate::lang_parser!(PhpParser, SPEC); diff --git a/crates/codegraph-extract/src/languages/python.rs b/crates/codegraph-extract/src/languages/python.rs index d3228c487d..b4e4728e17 100644 --- a/crates/codegraph-extract/src/languages/python.rs +++ b/crates/codegraph-extract/src/languages/python.rs @@ -1,4 +1,4 @@ -use crate::languages::common::{CallRule, LangSpec}; +use crate::languages::common::{CallRule, HookSpec, LangSpec, MarkerSpec}; use codegraph_core::SymbolKind; use tree_sitter::Node; @@ -31,13 +31,6 @@ pub static SPEC: LangSpec = LangSpec { ], func_kinds: &["function_definition", "lambda"], class_kinds: &["class_definition"], - param_kinds: &[], - annotation_kinds: &[], - name_type_fallback: false, - - link_impl_methods: false, - anonymous_name_fn: Some(anonymous_name_node), - value_func_kinds: &[], calls: &[CallRule { kind: "call", callee_field: "function", @@ -45,35 +38,27 @@ pub static SPEC: LangSpec = LangSpec { name_fn: None, target_fn: None, }], - class_type_name: None, - impl_trait_fn: None, - refine_kind_fn: None, - fn_ptr_assign_fn: None, - receiver_fn: None, - file_deps_fn: None, - go_stmt_kinds: &[], - defer_kinds: &[], - if_kinds: &["if_statement"], - elif_kinds: &["elif_clause"], - if_block_kinds: &[], - loop_kinds: &["for_statement", "while_statement"], - switch_kinds: &["match_statement"], - // Match cases nằm trong `block [body]` của match_statement. - switch_block_kinds: &["block"], - switch_case_kinds: &["case_clause"], - switch_default_kinds: &[], - return_kinds: &["return_statement"], - break_kinds: &["break_statement"], - continue_kinds: &["continue_statement"], - throw_kinds: &["raise_statement"], - try_kinds: &["try_statement"], - except_kinds: &["except_clause"], - try_else_kinds: &["else_clause"], - finally_kinds: &["finally_clause"], - if_cond_field: "condition", - if_cons_field: "consequence", - if_alt_field: "alternative", - body_field: "body", + hooks: HookSpec { + anonymous_name_fn: Some(anonymous_name_node), + ..HookSpec::none() + }, + markers: MarkerSpec { + if_kinds: &["if_statement"], + elif_kinds: &["elif_clause"], + loop_kinds: &["for_statement", "while_statement"], + switch_kinds: &["match_statement"], + switch_block_kinds: &["block"], + switch_case_kinds: &["case_clause"], + return_kinds: &["return_statement"], + break_kinds: &["break_statement"], + continue_kinds: &["continue_statement"], + throw_kinds: &["raise_statement"], + try_kinds: &["try_statement"], + except_kinds: &["except_clause"], + try_else_kinds: &["else_clause"], + finally_kinds: &["finally_clause"], + ..MarkerSpec::dflt() + }, + ..LangSpec::dflt() }; - crate::lang_parser!(PythonParser, SPEC); diff --git a/crates/codegraph-extract/src/languages/ruby.rs b/crates/codegraph-extract/src/languages/ruby.rs index 72ed9b5392..6fe9cbd865 100644 --- a/crates/codegraph-extract/src/languages/ruby.rs +++ b/crates/codegraph-extract/src/languages/ruby.rs @@ -1,4 +1,4 @@ -use crate::languages::common::{text, CallRule, LangSpec}; +use crate::languages::common::{text, CallRule, HookSpec, LangSpec, MarkerSpec}; use codegraph_core::SymbolKind; use tree_sitter::Node; @@ -39,13 +39,6 @@ pub static SPEC: LangSpec = LangSpec { ], func_kinds: &["method", "singleton_method"], class_kinds: &["class", "module"], - param_kinds: &[], - annotation_kinds: &[], - name_type_fallback: false, - - link_impl_methods: false, - anonymous_name_fn: None, - value_func_kinds: &[], calls: &[ CallRule { kind: "call", @@ -62,34 +55,22 @@ pub static SPEC: LangSpec = LangSpec { target_fn: None, }, ], - class_type_name: Some(class_type_name), - impl_trait_fn: None, - refine_kind_fn: None, - fn_ptr_assign_fn: None, - receiver_fn: None, - file_deps_fn: None, - go_stmt_kinds: &[], - defer_kinds: &[], - if_kinds: &["if"], - elif_kinds: &["elsif"], - if_block_kinds: &[], - loop_kinds: &["while", "until", "for"], - switch_kinds: &["case"], - switch_block_kinds: &[], - switch_case_kinds: &["when"], - switch_default_kinds: &["else"], - return_kinds: &["return"], - break_kinds: &["break"], - continue_kinds: &["next"], - throw_kinds: &[], - try_kinds: &[], - except_kinds: &[], - try_else_kinds: &[], - finally_kinds: &[], - if_cond_field: "condition", - if_cons_field: "consequence", - if_alt_field: "alternative", - body_field: "body", + hooks: HookSpec { + class_type_name: Some(class_type_name), + ..HookSpec::none() + }, + markers: MarkerSpec { + if_kinds: &["if"], + elif_kinds: &["elsif"], + loop_kinds: &["while", "until", "for"], + switch_kinds: &["case"], + switch_case_kinds: &["when"], + switch_default_kinds: &["else"], + return_kinds: &["return"], + break_kinds: &["break"], + continue_kinds: &["next"], + ..MarkerSpec::dflt() + }, + ..LangSpec::dflt() }; - crate::lang_parser!(RubyParser, SPEC); diff --git a/crates/codegraph-extract/src/languages/rust.rs b/crates/codegraph-extract/src/languages/rust.rs index 8e8f3b3ae9..a90f06b78e 100644 --- a/crates/codegraph-extract/src/languages/rust.rs +++ b/crates/codegraph-extract/src/languages/rust.rs @@ -1,4 +1,4 @@ -use crate::languages::common::{text, CallRule, LangSpec}; +use crate::languages::common::{text, CallRule, HookSpec, LangSpec, MarkerSpec}; use codegraph_core::SymbolKind; fn ts_language() -> tree_sitter::Language { @@ -28,14 +28,7 @@ pub static SPEC: LangSpec = LangSpec { "impl_item", "mod_item", ], - param_kinds: &[], annotation_kinds: &["attribute"], - // `impl Foo` không có name field — tên nằm ở field `type`. - name_type_fallback: true, - // Rust: impl_item cũng là Class → re-parent methods về struct def cùng tên. - link_impl_methods: true, - anonymous_name_fn: None, - value_func_kinds: &[], calls: &[CallRule { kind: "call_expression", callee_field: "function", @@ -43,41 +36,31 @@ pub static SPEC: LangSpec = LangSpec { name_fn: None, target_fn: None, }], - class_type_name: Some(rust_class_type_name), - impl_trait_fn: Some(rust_impl_trait_name), - refine_kind_fn: None, - fn_ptr_assign_fn: None, - receiver_fn: None, - file_deps_fn: None, - go_stmt_kinds: &[], - defer_kinds: &[], - if_kinds: &["if_expression", "if_let_expression"], - elif_kinds: &[], - if_block_kinds: &[], - loop_kinds: &[ - "loop_expression", - "while_expression", - "while_let_expression", - "for_expression", - ], - switch_kinds: &["match_expression"], - switch_block_kinds: &["match_block"], - switch_case_kinds: &["match_arm"], - switch_default_kinds: &[], - return_kinds: &["return_expression"], - break_kinds: &["break_expression"], - continue_kinds: &["continue_expression"], - throw_kinds: &[], - try_kinds: &[], - except_kinds: &[], - try_else_kinds: &[], - finally_kinds: &[], - if_cond_field: "condition", - if_cons_field: "consequence", - if_alt_field: "alternative", - body_field: "body", + name_type_fallback: true, + link_impl_methods: true, + hooks: HookSpec { + class_type_name: Some(rust_class_type_name), + impl_trait_fn: Some(rust_impl_trait_name), + ..HookSpec::none() + }, + markers: MarkerSpec { + if_kinds: &["if_expression", "if_let_expression"], + loop_kinds: &[ + "loop_expression", + "while_expression", + "while_let_expression", + "for_expression", + ], + switch_kinds: &["match_expression"], + switch_block_kinds: &["match_block"], + switch_case_kinds: &["match_arm"], + return_kinds: &["return_expression"], + break_kinds: &["break_expression"], + continue_kinds: &["continue_expression"], + ..MarkerSpec::dflt() + }, + ..LangSpec::dflt() }; - /// Chỉ `impl Foo` / `impl Trait for Foo` có `type` field (self type) — dùng làm /// `type_name` để `link_impl_methods_to_def` nối impl → struct def cùng tên. /// Các class-like khác (struct/enum/trait/mod) trả None → không bị coi là impl. diff --git a/crates/codegraph-extract/src/languages/scala.rs b/crates/codegraph-extract/src/languages/scala.rs index 6e35f41174..11b4634495 100644 --- a/crates/codegraph-extract/src/languages/scala.rs +++ b/crates/codegraph-extract/src/languages/scala.rs @@ -1,4 +1,4 @@ -use crate::languages::common::{CallRule, LangSpec}; +use crate::languages::common::{CallRule, HookSpec, LangSpec, MarkerSpec}; use codegraph_core::SymbolKind; fn ts_language() -> tree_sitter::Language { @@ -28,12 +28,6 @@ pub static SPEC: LangSpec = LangSpec { "enum_definition", ], param_kinds: &["parameter"], - annotation_kinds: &[], - name_type_fallback: false, - - link_impl_methods: false, - anonymous_name_fn: None, - value_func_kinds: &[], calls: &[CallRule { kind: "call_expression", callee_field: "function", @@ -41,34 +35,20 @@ pub static SPEC: LangSpec = LangSpec { name_fn: None, target_fn: None, }], - class_type_name: None, - impl_trait_fn: None, - refine_kind_fn: None, - fn_ptr_assign_fn: None, - receiver_fn: None, - file_deps_fn: None, - go_stmt_kinds: &[], - defer_kinds: &[], - if_kinds: &["if_expression"], - elif_kinds: &[], - if_block_kinds: &[], - loop_kinds: &["for_expression", "while_expression", "do_while_expression"], - switch_kinds: &["match_expression"], - switch_block_kinds: &["case_block"], - switch_case_kinds: &["case_clause"], - switch_default_kinds: &[], - return_kinds: &["return_expression"], - break_kinds: &[], - continue_kinds: &[], - throw_kinds: &["throw_expression"], - try_kinds: &["try_expression"], - except_kinds: &["catch_clause"], - try_else_kinds: &[], - finally_kinds: &["finally_clause"], - if_cond_field: "condition", - if_cons_field: "consequence", - if_alt_field: "alternative", - body_field: "body", + hooks: HookSpec::none(), + markers: MarkerSpec { + if_kinds: &["if_expression"], + loop_kinds: &["for_expression", "while_expression", "do_while_expression"], + switch_kinds: &["match_expression"], + switch_block_kinds: &["case_block"], + switch_case_kinds: &["case_clause"], + return_kinds: &["return_expression"], + throw_kinds: &["throw_expression"], + try_kinds: &["try_expression"], + except_kinds: &["catch_clause"], + finally_kinds: &["finally_clause"], + ..MarkerSpec::dflt() + }, + ..LangSpec::dflt() }; - crate::lang_parser!(ScalaParser, SPEC); diff --git a/crates/codegraph-extract/src/languages/swift.rs b/crates/codegraph-extract/src/languages/swift.rs index 38522f4fa2..63e46b44bd 100644 --- a/crates/codegraph-extract/src/languages/swift.rs +++ b/crates/codegraph-extract/src/languages/swift.rs @@ -1,4 +1,4 @@ -use crate::languages::common::{CallRule, LangSpec}; +use crate::languages::common::{CallRule, HookSpec, LangSpec, MarkerSpec}; use codegraph_core::SymbolKind; fn ts_language() -> tree_sitter::Language { @@ -34,49 +34,26 @@ pub static SPEC: LangSpec = LangSpec { ], param_kinds: &["parameter"], annotation_kinds: &["attribute"], - name_type_fallback: false, - - link_impl_methods: false, - anonymous_name_fn: None, - value_func_kinds: &[], calls: &[CallRule { - // Swift call_expression không có callee field — dùng named child đầu tiên - // làm callee (verify bằng dump_tree). kind: "call_expression", callee_field: "", arguments_field: "arguments", name_fn: None, target_fn: None, }], - class_type_name: None, - impl_trait_fn: None, - refine_kind_fn: None, - fn_ptr_assign_fn: None, - receiver_fn: None, - file_deps_fn: None, - go_stmt_kinds: &[], - defer_kinds: &[], - if_kinds: &["if_statement"], - elif_kinds: &[], - // Swift if/for không có consequence/body field — body là node `statements` trần. - if_block_kinds: &["statements"], - loop_kinds: &["for_statement", "while_statement", "repeat_while_statement"], - switch_kinds: &["switch_statement"], - switch_block_kinds: &[], - switch_case_kinds: &["switch_entry"], - switch_default_kinds: &[], - return_kinds: &["return_statement"], - break_kinds: &["break_statement"], - continue_kinds: &["continue_statement"], - throw_kinds: &["throw_statement"], - try_kinds: &[], - except_kinds: &[], - try_else_kinds: &[], - finally_kinds: &[], - if_cond_field: "condition", - if_cons_field: "consequence", - if_alt_field: "alternative", - body_field: "body", + hooks: HookSpec::none(), + markers: MarkerSpec { + if_kinds: &["if_statement"], + if_block_kinds: &["statements"], + loop_kinds: &["for_statement", "while_statement", "repeat_while_statement"], + switch_kinds: &["switch_statement"], + switch_case_kinds: &["switch_entry"], + return_kinds: &["return_statement"], + break_kinds: &["break_statement"], + continue_kinds: &["continue_statement"], + throw_kinds: &["throw_statement"], + ..MarkerSpec::dflt() + }, + ..LangSpec::dflt() }; - crate::lang_parser!(SwiftParser, SPEC); diff --git a/crates/codegraph-extract/src/languages/typescript.rs b/crates/codegraph-extract/src/languages/typescript.rs index 26864906db..9fac296cc6 100644 --- a/crates/codegraph-extract/src/languages/typescript.rs +++ b/crates/codegraph-extract/src/languages/typescript.rs @@ -1,4 +1,4 @@ -use crate::languages::common::{named_children, text, CallRule, LangSpec}; +use crate::languages::common::{named_children, text, CallRule, HookSpec, LangSpec, MarkerSpec}; use codegraph_core::SymbolKind; use tree_sitter::Node; @@ -67,13 +67,6 @@ pub static SPEC: LangSpec = LangSpec { "enum_declaration", "internal_module", ], - param_kinds: &[], - annotation_kinds: &[], - name_type_fallback: false, - - link_impl_methods: false, - anonymous_name_fn: Some(crate::languages::javascript::anonymous_name_node), - value_func_kinds: &["function_expression", "arrow_function"], calls: &[ CallRule { kind: "call_expression", @@ -90,41 +83,35 @@ pub static SPEC: LangSpec = LangSpec { target_fn: None, }, ], - class_type_name: Some(class_type_name), - impl_trait_fn: None, - refine_kind_fn: None, - fn_ptr_assign_fn: None, - receiver_fn: None, - file_deps_fn: None, - go_stmt_kinds: &[], - defer_kinds: &[], - if_kinds: &["if_statement"], - elif_kinds: &[], - if_block_kinds: &[], - loop_kinds: &[ - "for_statement", - "for_in_statement", - "for_of_statement", - "while_statement", - "do_statement", - ], - switch_kinds: &["switch_statement"], - switch_block_kinds: &["switch_body"], - switch_case_kinds: &["switch_case"], - switch_default_kinds: &["switch_default"], - return_kinds: &["return_statement"], - break_kinds: &["break_statement"], - continue_kinds: &["continue_statement"], - throw_kinds: &["throw_statement"], - try_kinds: &["try_statement"], - except_kinds: &["catch_clause"], - try_else_kinds: &[], - finally_kinds: &["finally_clause"], - if_cond_field: "condition", - if_cons_field: "consequence", - if_alt_field: "alternative", - body_field: "body", + hooks: HookSpec { + class_type_name: Some(class_type_name), + anonymous_name_fn: Some(crate::languages::javascript::anonymous_name_node), + value_func_kinds: &["function_expression", "arrow_function"], + ..HookSpec::none() + }, + markers: MarkerSpec { + if_kinds: &["if_statement"], + loop_kinds: &[ + "for_statement", + "for_in_statement", + "for_of_statement", + "while_statement", + "do_statement", + ], + switch_kinds: &["switch_statement"], + switch_block_kinds: &["switch_body"], + switch_case_kinds: &["switch_case"], + switch_default_kinds: &["switch_default"], + return_kinds: &["return_statement"], + break_kinds: &["break_statement"], + continue_kinds: &["continue_statement"], + throw_kinds: &["throw_statement"], + try_kinds: &["try_statement"], + except_kinds: &["catch_clause"], + finally_kinds: &["finally_clause"], + ..MarkerSpec::dflt() + }, + ..LangSpec::dflt() }; - crate::lang_parser!(TypeScriptParser, SPEC); crate::lang_parser!(TsxParser, SPEC, "tsx", &["tsx"], tsx_ts_language); diff --git a/crates/codegraph-extract/tests/dynamic_dispatch.rs b/crates/codegraph-extract/tests/dynamic_dispatch.rs index 6628a2d6e2..b70aa19be9 100644 --- a/crates/codegraph-extract/tests/dynamic_dispatch.rs +++ b/crates/codegraph-extract/tests/dynamic_dispatch.rs @@ -292,3 +292,42 @@ fn go_ast_dump() { } dump(tree.root_node(), src, 0); } + +#[test] +fn go_signature_key_normalized_and_param_name_insensitive() { + let src = r#"package main + +type Reader interface { + Read(p []byte) (int, error) +} + +type Good struct{} +type Wrong struct{} + +func (g Good) Read(b []byte) (int, error) { return 0, nil } +func (w Wrong) Read(s string) string { return "" } +"#; + let parsed = GoParser.parse_file("r.go", src).unwrap(); + let key = |type_name: &str| { + parsed + .symbols + .iter() + .find(|s| s.name == "Read" && s.type_name.as_deref() == Some(type_name)) + .unwrap() + .sig_key + .clone() + .expect("sig_key missing") + }; + // Interface method (method_elem) và impl (method_declaration) cùng hàm. + let iface_key = parsed + .symbols + .iter() + .find(|s| s.name == "Read" && s.type_name.is_none()) + .unwrap() + .sig_key + .clone() + .expect("iface sig_key missing"); + assert_eq!(iface_key, "([]byte)(int,error)"); + assert_eq!(key("Good"), "([]byte)(int,error)"); // khác tên param, cùng sig + assert_eq!(key("Wrong"), "(string)(string)"); // sai signature +} diff --git a/crates/codegraph-graph/src/diff.rs b/crates/codegraph-graph/src/diff.rs index 782ef77ab2..4833a524b6 100644 --- a/crates/codegraph-graph/src/diff.rs +++ b/crates/codegraph-graph/src/diff.rs @@ -498,6 +498,7 @@ mod tests { annotations: Vec::new(), language: "test".to_string(), impl_trait: None, + sig_key: None, } } diff --git a/crates/codegraph-graph/src/lib.rs b/crates/codegraph-graph/src/lib.rs index a3401f151c..da6ea79fad 100644 --- a/crates/codegraph-graph/src/lib.rs +++ b/crates/codegraph-graph/src/lib.rs @@ -1767,8 +1767,10 @@ impl GraphIndex { /// của T. Nguồn: method symbols có `type_name` = receiver base type /// (`go_receiver_type`) + method scope vào Interface symbol (language go). fn build_go_method_sets(&mut self) { - let mut type_methods: HashMap> = HashMap::new(); - let mut iface_methods: HashMap> = HashMap::new(); + // Method set: name (lower) → sig_key — so cả SIGNATURE, không chỉ tên + // (`Wrong.Read(string) string` không thỏa `Reader.Read([]byte) (int, error)`). + let mut type_methods: HashMap>> = HashMap::new(); + let mut iface_methods: HashMap)>> = HashMap::new(); for sym in self.symbols.values() { if sym.language != "go" || !matches!(sym.kind, SymbolKind::Method | SymbolKind::Function) @@ -1779,7 +1781,7 @@ impl GraphIndex { type_methods .entry(recv.to_lowercase()) .or_default() - .insert(sym.name.to_lowercase()); + .insert(sym.name.to_lowercase(), sym.sig_key.clone()); } if let Some(tr) = self.symbols.get(&sym.scope_id) && tr.kind == SymbolKind::Interface @@ -1788,15 +1790,28 @@ impl GraphIndex { iface_methods .entry(tr.name.to_lowercase()) .or_default() - .push(sym.name.to_lowercase()); + .push((sym.name.to_lowercase(), sym.sig_key.clone())); } } - // Satisfaction: mọi method name của iface nằm trong method set của - // type → mọi method CỦA TYPE ĐÓ (receiver type khớp) là candidates. + // Satisfaction: MỌI method của iface có (name, sig_key) khớp trong + // method set của type. Sig so khi cả hai có key; thiếu key nào → + // fallback name-only (lenient — giữ soundness khi extraction thiếu). + // Interface 0 method (interface{}) → skip (fan-out nổ, vô nghĩa). + let sig_matches = |a: &Option, b: &Option| match (a, b) { + (Some(x), Some(y)) => x == y, + _ => true, + }; let mut go_impls: HashMap> = HashMap::new(); for (iface, needed) in &iface_methods { + if needed.is_empty() { + continue; + } for (ty, methods) in &type_methods { - if !needed.iter().all(|m| methods.contains(m)) { + if !needed.iter().all(|(m, sk)| { + methods + .get(m) + .is_some_and(|type_sk| sig_matches(sk, type_sk)) + }) { continue; } let implementing: Vec = self @@ -4628,6 +4643,7 @@ mod tests { annotations: Vec::new(), language: "test".to_string(), impl_trait: None, + sig_key: None, } } @@ -5026,6 +5042,7 @@ mod tests { annotations: Vec::new(), language: "rust".to_string(), impl_trait: impl_trait.map(String::from), + sig_key: None, } } @@ -5350,6 +5367,7 @@ mod tests { annotations: Vec::new(), language: "go".to_string(), impl_trait: None, + sig_key: None, }; let (caller, iface_id) = (SYMBOL_BASE, SYMBOL_BASE + 1); let r = result( @@ -5495,6 +5513,7 @@ mod tests { annotations: Vec::new(), language: "go".to_string(), impl_trait: None, + sig_key: None, }; let (caller, iface_id) = (SYMBOL_BASE, SYMBOL_BASE + 1); let r = result( @@ -5570,6 +5589,105 @@ mod tests { assert_eq!(flow.calls[0].candidate_names, vec!["Write"]); } + #[tokio::test] + async fn go_method_set_rejects_wrong_signature() { + // Reader{Read([]byte)(int,error)}: Good khớp, Wrong.Read(string) string + // sai signature → KHÔNG thỏa interface. + let mut idx = GraphIndex::in_memory(); + let gsym = |id: u64, + name: &str, + kind: SymbolKind, + scope_id: u64, + type_name: Option<&str>, + sig: Option<&str>, + line: u32| Symbol { + id, + name: name.to_string(), + kind, + scope: ScopeLevel::Global, + scope_id, + type_ref: 0, + type_name: type_name.map(String::from), + file: "r.go".to_string(), + line, + end_line: line, + signature: None, + doc: None, + annotations: Vec::new(), + language: "go".to_string(), + impl_trait: None, + sig_key: sig.map(String::from), + }; + let (caller, iface_id) = (SYMBOL_BASE, SYMBOL_BASE + 1); + let r = result( + "r.go", + vec![ + gsym(caller, "sink", SymbolKind::Function, 0, None, None, 30), + gsym(iface_id, "Reader", SymbolKind::Interface, 0, None, None, 1), + gsym( + SYMBOL_BASE + 2, + "Read", + SymbolKind::Method, + iface_id, + None, + Some("([]byte)(int,error)"), + 2, + ), + gsym(SYMBOL_BASE + 3, "Good", SymbolKind::Class, 0, None, None, 5), + gsym( + SYMBOL_BASE + 4, + "Read", + SymbolKind::Method, + SYMBOL_BASE + 3, + Some("Good"), + Some("([]byte)(int,error)"), + 6, + ), + gsym( + SYMBOL_BASE + 5, + "Wrong", + SymbolKind::Class, + 0, + None, + None, + 8, + ), + gsym( + SYMBOL_BASE + 6, + "Read", + SymbolKind::Method, + SYMBOL_BASE + 5, + Some("Wrong"), + Some("(string)(string)"), + 9, + ), + ], + HashMap::from([(caller, vec![caller, 0])]), + vec![CallRecord { + caller_id: caller, + call_name: "r.Read".to_string(), + position: 1, + line: 31, + condition: None, + is_loop_body: false, + effect: EffectType::None, + effect_desc: None, + target_class: None, + target_method: None, + into_var: None, + is_new_var: false, + ..Default::default() + }], + ); + idx.ingest(&[r], None).await.unwrap(); + + let flow = idx.flow(caller).await.unwrap(); + assert_eq!(flow.calls[0].resolved_via, ResolutionKind::HierarchyFanOut); + // Chỉ Good.Read — Wrong sai signature bị loại. + assert_eq!(flow.calls[0].candidate_names, vec!["Read"]); + assert_eq!(idx.stats().edges, 1); + } + #[tokio::test] async fn import_alias_resolves_qualified_call() { // File caller import alias `orders` → path "corp/api/orders"; symbol Get @@ -6560,6 +6678,7 @@ mod tests { annotations: Vec::new(), language: "java".into(), impl_trait: None, + sig_key: None, }; let legacy_field = Symbol { id: 101, @@ -6577,6 +6696,7 @@ mod tests { annotations: Vec::new(), language: "java".into(), impl_trait: None, + sig_key: None, }; let legacy_do_work = Symbol { id: 102, @@ -6594,6 +6714,7 @@ mod tests { annotations: Vec::new(), language: "java".into(), impl_trait: None, + sig_key: None, }; let service = Symbol { id: 103, @@ -6611,6 +6732,7 @@ mod tests { annotations: Vec::new(), language: "java".into(), impl_trait: None, + sig_key: None, }; let service_do_work = Symbol { id: 104, @@ -6628,6 +6750,7 @@ mod tests { annotations: Vec::new(), language: "java".into(), impl_trait: None, + sig_key: None, }; // Tạo router method với scope_id = 200 (không phải class scope) // Để field_declared_type tìm trong scope_id=200 sẽ fail → return None. @@ -6648,6 +6771,7 @@ mod tests { annotations: Vec::new(), language: "java".into(), impl_trait: None, + sig_key: None, }; idx.ingest( &[ParseResult { diff --git a/crates/codegraph-graph/src/memtrack.rs b/crates/codegraph-graph/src/memtrack.rs index 95c327d87d..15f6ec2cf3 100644 --- a/crates/codegraph-graph/src/memtrack.rs +++ b/crates/codegraph-graph/src/memtrack.rs @@ -247,6 +247,7 @@ mod tests { annotations: Vec::new(), language: "rust".into(), impl_trait: None, + sig_key: None, }; let mem = symbols_mem(&HashMap::from([(100, sym)])); assert_eq!(mem.entries, 1); diff --git a/crates/codegraph-graph/src/shared.rs b/crates/codegraph-graph/src/shared.rs index 13cbec53fc..43a4204b98 100644 --- a/crates/codegraph-graph/src/shared.rs +++ b/crates/codegraph-graph/src/shared.rs @@ -342,6 +342,7 @@ mod tests { annotations: Vec::new(), language: "ts".into(), impl_trait: None, + sig_key: None, } } diff --git a/crates/codegraph-graph/src/storage/in_memory.rs b/crates/codegraph-graph/src/storage/in_memory.rs index 6c88719458..346fa5a24a 100644 --- a/crates/codegraph-graph/src/storage/in_memory.rs +++ b/crates/codegraph-graph/src/storage/in_memory.rs @@ -931,6 +931,7 @@ mod tests { annotations: Vec::new(), language: "rust".into(), impl_trait: None, + sig_key: None, }; s.save_symbol(&sym).await.unwrap(); let loaded = s.load_symbol(100).await.unwrap(); diff --git a/crates/codegraph-graph/src/storage/mysql.rs b/crates/codegraph-graph/src/storage/mysql.rs index bf3324b822..ce204314be 100644 --- a/crates/codegraph-graph/src/storage/mysql.rs +++ b/crates/codegraph-graph/src/storage/mysql.rs @@ -1062,5 +1062,6 @@ fn row_to_symbol(row: &MySqlRow) -> Result { annotations, language, impl_trait: None, + sig_key: None, }) } diff --git a/crates/codegraph-graph/src/storage/postgres.rs b/crates/codegraph-graph/src/storage/postgres.rs index 125a5369e6..4ac3dee23c 100644 --- a/crates/codegraph-graph/src/storage/postgres.rs +++ b/crates/codegraph-graph/src/storage/postgres.rs @@ -1079,5 +1079,6 @@ fn row_to_symbol(row: &PgRow) -> Result { annotations, language, impl_trait: None, + sig_key: None, }) } diff --git a/crates/codegraph-graph/tests/lmdb.rs b/crates/codegraph-graph/tests/lmdb.rs index 99ffbf91ea..7978f7d11d 100644 --- a/crates/codegraph-graph/tests/lmdb.rs +++ b/crates/codegraph-graph/tests/lmdb.rs @@ -30,6 +30,7 @@ fn sym(file: &str, name: &str, id: u64) -> Symbol { annotations: Vec::new(), language: "ts".to_string(), impl_trait: None, + sig_key: None, } } diff --git a/crates/codegraph-graph/tests/rdbms.rs b/crates/codegraph-graph/tests/rdbms.rs index a6fd956333..7f2e01bd7b 100644 --- a/crates/codegraph-graph/tests/rdbms.rs +++ b/crates/codegraph-graph/tests/rdbms.rs @@ -38,6 +38,7 @@ fn sym(file: &str, name: &str, id: u64) -> Symbol { annotations: Vec::new(), language: "ts".to_string(), impl_trait: None, + sig_key: None, } } diff --git a/crates/codegraph-graph/tests/redis.rs b/crates/codegraph-graph/tests/redis.rs index ffe7b51f4c..1a9516c373 100644 --- a/crates/codegraph-graph/tests/redis.rs +++ b/crates/codegraph-graph/tests/redis.rs @@ -37,6 +37,7 @@ fn sym(file: &str, name: &str, id: u64) -> Symbol { annotations: Vec::new(), language: "ts".to_string(), impl_trait: None, + sig_key: None, } } diff --git a/crates/codegraph-graph/tests/sqlite.rs b/crates/codegraph-graph/tests/sqlite.rs index 48894d385a..ceeb0b8f48 100644 --- a/crates/codegraph-graph/tests/sqlite.rs +++ b/crates/codegraph-graph/tests/sqlite.rs @@ -29,6 +29,7 @@ fn sym(file: &str, name: &str, id: u64) -> Symbol { annotations: Vec::new(), language: "ts".to_string(), impl_trait: None, + sig_key: None, } } diff --git a/crates/codegraph-graph/tests/trace_flow_tests.rs b/crates/codegraph-graph/tests/trace_flow_tests.rs index 4a6676b1e4..bae00df6d4 100644 --- a/crates/codegraph-graph/tests/trace_flow_tests.rs +++ b/crates/codegraph-graph/tests/trace_flow_tests.rs @@ -31,6 +31,7 @@ fn sym(name: &str, id: u64) -> Symbol { line: 1, end_line: 1, impl_trait: None, + sig_key: None, signature: None, doc: None, annotations: Vec::new(), diff --git a/crates/codegraph-mcp/src/callers_tests.rs b/crates/codegraph-mcp/src/callers_tests.rs index 9e1e0703db..75488ff4c8 100644 --- a/crates/codegraph-mcp/src/callers_tests.rs +++ b/crates/codegraph-mcp/src/callers_tests.rs @@ -33,6 +33,7 @@ async fn callers_timeout_resume_dispatch() { annotations: vec![], language: "rust".into(), impl_trait: None, + sig_key: None, }; let mut idx = GraphIndex::open(&dsn).await.unwrap(); idx.ingest( diff --git a/crates/codegraph-sboxes/tests/control_flow.rs b/crates/codegraph-sboxes/tests/control_flow.rs index 51bab48847..0cdb39937a 100644 --- a/crates/codegraph-sboxes/tests/control_flow.rs +++ b/crates/codegraph-sboxes/tests/control_flow.rs @@ -31,6 +31,7 @@ fn sym(id: u64, name: &str) -> Symbol { annotations: Vec::new(), language: "test".to_string(), impl_trait: None, + sig_key: None, } } diff --git a/crates/codegraph-sboxes/tests/end_to_end.rs b/crates/codegraph-sboxes/tests/end_to_end.rs index c578ea5caf..10909f874a 100644 --- a/crates/codegraph-sboxes/tests/end_to_end.rs +++ b/crates/codegraph-sboxes/tests/end_to_end.rs @@ -27,6 +27,7 @@ fn sym(id: u64, name: &str) -> Symbol { annotations: Vec::new(), language: "test".to_string(), impl_trait: None, + sig_key: None, } } From ca59b0d8c4281a738262c3de03134ec8dbb4fcef Mon Sep 17 00:00:00 2001 From: Hung Pham Date: Sun, 25 Oct 2026 15:16:19 +0700 Subject: [PATCH 04/19] Rust semantic enrichment: field/param typing, use indexing, cross-file impl linking, sig_key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - decls += field_declaration/parameter/let_declaration: struct field, tham số và local giờ là symbol có type_name (từ field `type`) — tie-break field_declared_type resolve `db.query()` theo type của `db`. Parameter lấy tên từ field `pattern` TRƯỚC name_type_fallback (tránh lấy nhầm tên kiểu). - rust_file_deps: extract `use` (alias = `as` alias hoặc segment cuối); import_alias_candidates hỗ trợ `Type::method` — `use crate::storage::Database` + `Database::new()` resolve về method của class Database (Alias). - link_impls_globally: impl linking chuyển lên global ingest — `impl Worker` khác file với `struct Worker` vẫn nối (type_ref → def, re-parent methods). - rust_signature_key: `(params)(return)` cho method/fn (self → "self") — nguyên liệu trait matching theo signature. --- .../codegraph-extract/src/languages/common.rs | 3 + .../codegraph-extract/src/languages/rust.rs | 83 ++++++- .../tests/dynamic_dispatch.rs | 89 +++++++ crates/codegraph-graph/src/lib.rs | 220 +++++++++++++++++- 4 files changed, 387 insertions(+), 8 deletions(-) diff --git a/crates/codegraph-extract/src/languages/common.rs b/crates/codegraph-extract/src/languages/common.rs index 430052ff62..12fa64dbe3 100644 --- a/crates/codegraph-extract/src/languages/common.rs +++ b/crates/codegraph-extract/src/languages/common.rs @@ -404,6 +404,9 @@ fn push_symbol( } else { node.child_by_field_name("name") .or_else(|| name_from_declarator(node)) + // Pattern identifier (Rust `db: Database`, `let cache = ..`) — + // TRƯỚC name_type_fallback để không lấy nhầm tên từ field `type`. + .or_else(|| node.child_by_field_name("pattern")) .or_else(|| { if spec.name_type_fallback { node.child_by_field_name("type") diff --git a/crates/codegraph-extract/src/languages/rust.rs b/crates/codegraph-extract/src/languages/rust.rs index a90f06b78e..46005719f3 100644 --- a/crates/codegraph-extract/src/languages/rust.rs +++ b/crates/codegraph-extract/src/languages/rust.rs @@ -1,5 +1,7 @@ -use crate::languages::common::{text, CallRule, HookSpec, LangSpec, MarkerSpec}; +use crate::languages::common::{named_children, text, CallRule, HookSpec, LangSpec, MarkerSpec}; use codegraph_core::SymbolKind; +use codegraph_graph::{FileDeps, ImportAlias}; +use tree_sitter::Node; fn ts_language() -> tree_sitter::Language { tree_sitter_rust::LANGUAGE.into() @@ -19,6 +21,11 @@ pub static SPEC: LangSpec = LangSpec { ("const_item", SymbolKind::Constant), ("static_item", SymbolKind::Variable), ("type_item", SymbolKind::Class), + // Semantic enrichment: field/param/local — type_name từ field `type` + // giúp tie-break resolve `db.query()` theo type của `db`. + ("field_declaration", SymbolKind::Field), + ("parameter", SymbolKind::Parameter), + ("let_declaration", SymbolKind::Variable), ], func_kinds: &["function_item"], class_kinds: &[ @@ -41,6 +48,8 @@ pub static SPEC: LangSpec = LangSpec { hooks: HookSpec { class_type_name: Some(rust_class_type_name), impl_trait_fn: Some(rust_impl_trait_name), + sig_key_fn: Some(rust_signature_key), + file_deps_fn: Some(rust_file_deps), ..HookSpec::none() }, markers: MarkerSpec { @@ -74,6 +83,78 @@ fn rust_class_type_name(node: &tree_sitter::Node, src: &[u8]) -> Option crate::lang_parser!(RustParser, SPEC); +/// `use` declarations của file — nguyên liệu resolve qualified call +/// (`Database::new` với `use crate::storage::Database;`). +/// - `use foo::bar as baz;` → alias "baz", path "foo::bar" +/// - `use crate::storage::Database;` → alias "Database" (segment cuối) +fn rust_file_deps(root: &Node, src: &[u8]) -> Option { + let mut imports: Vec = Vec::new(); + let mut stack = vec![*root]; + while let Some(n) = stack.pop() { + if n.kind() == "use_declaration" { + if let Some(arg) = n.child_by_field_name("argument") { + let raw = text(&arg, src).unwrap_or_default(); + let (path, alias) = match raw.split_once(" as ") { + Some((p, a)) => (p.trim().to_string(), a.trim().to_string()), + None => { + let seg = raw.rsplit("::").next().unwrap_or("").trim_end_matches('*'); + (raw.clone(), seg.to_string()) + } + }; + if !alias.is_empty() && !path.is_empty() { + imports.push(ImportAlias { alias, path }); + } + } + } + let mut cursor = n.walk(); + for ch in n.children(&mut cursor) { + stack.push(ch); + } + } + if imports.is_empty() { + None + } else { + Some(FileDeps { + package: None, + imports, + }) + } +} + +/// Signature key `(params)(return)` cho method/function — trait matching. +/// `&self`/`&mut self` → "self"; type text giữ nguyên (`&mut [u8]`, `usize`). +fn rust_signature_key(node: &Node, src: &[u8]) -> Option { + if !matches!(node.kind(), "function_item" | "function_signature_item") { + return None; + } + let mut pt: Vec = Vec::new(); + if let Some(params) = node.child_by_field_name("parameters") { + for ch in named_children(¶ms) { + // self param không có field type — chuẩn hóa "self". + let txt = match ch.child_by_field_name("type") { + Some(t) => text(&t, src).map(|s| s.replace(' ', "")), + None => { + if text(&ch, src).map(|s| s.contains("self")).unwrap_or(false) { + Some("self".to_string()) + } else { + None + } + } + }; + if let Some(t) = txt { + pt.push(t); + } + } + } + let mut rt: Vec = Vec::new(); + if let Some(ret) = node.child_by_field_name("return_type") { + if let Some(t) = text(&ret, src) { + rt.push(t.replace(' ', "")); + } + } + Some(format!("({})({})", pt.join(","), rt.join(","))) +} + /// Trait name của `impl Trait for Type` — field `trait` của impl_item. /// Normalize generic: `Iterator` → `"Iterator"` (fan-out index key /// phải khớp tên trait thuần). diff --git a/crates/codegraph-extract/tests/dynamic_dispatch.rs b/crates/codegraph-extract/tests/dynamic_dispatch.rs index b70aa19be9..3d8d3fcd08 100644 --- a/crates/codegraph-extract/tests/dynamic_dispatch.rs +++ b/crates/codegraph-extract/tests/dynamic_dispatch.rs @@ -331,3 +331,92 @@ func (w Wrong) Read(s string) string { return "" } assert_eq!(key("Good"), "([]byte)(int,error)"); // khác tên param, cùng sig assert_eq!(key("Wrong"), "(string)(string)"); // sai signature } + +#[test] +fn rust_field_param_symbols_extracted() { + let src = r#" +pub struct Server { + pub db: Database, +} + +fn run(db: Database, config: Config) -> u32 { + let cache = 1; + db.query(); + cache +} +"#; + let parsed = codegraph_extract::languages::rust::RustParser + .parse_file("s.rs", src) + .unwrap(); + for s in &parsed.symbols { + eprintln!( + "DBG {} {:?} scope={} type={:?}", + s.name, s.kind, s.scope_id, s.type_name + ); + } + let field = parsed + .symbols + .iter() + .find(|s| s.name == "db" && s.kind == SymbolKind::Field); + assert!(field.is_some(), "field db missing"); + assert_eq!(field.unwrap().type_name.as_deref(), Some("Database")); + let param = parsed + .symbols + .iter() + .find(|s| s.name == "config" && s.kind == SymbolKind::Parameter); + assert!(param.is_some(), "parameter config missing"); + assert_eq!(param.unwrap().type_name.as_deref(), Some("Config")); + assert!( + parsed + .symbols + .iter() + .any(|s| s.name == "cache" && s.kind == SymbolKind::Variable), + "let cache missing" + ); + // Không sinh symbol giả mang tên kiểu + assert!(!parsed + .symbols + .iter() + .any(|s| s.name == "Database" && s.kind == SymbolKind::Parameter)); +} + +#[test] +fn rust_use_deps_and_sig_key_extracted() { + let src = r#" +use crate::storage::Database; +use std::collections::HashMap as Map; + +pub struct Database; + +impl Database { + pub fn new() -> Self { Database } + pub fn query(&self, sql: &str) -> u32 { 0 } +} +"#; + let parsed = codegraph_extract::languages::rust::RustParser + .parse_file("main.rs", src) + .unwrap(); + let deps = parsed.file_deps.expect("file deps missing"); + let mut pairs: Vec<(&str, &str)> = deps + .imports + .iter() + .map(|i| (i.alias.as_str(), i.path.as_str())) + .collect(); + pairs.sort(); + assert_eq!( + pairs, + vec![ + ("Database", "crate::storage::Database"), + ("Map", "std::collections::HashMap") + ] + ); + + // sig_key: query(&self, sql: &str) -> u32 → "(self,&str)(u32)"; + // tên param bỏ (`sql` không vào key). + let query = parsed + .symbols + .iter() + .find(|s| s.name == "query") + .expect("query missing"); + assert_eq!(query.sig_key.as_deref(), Some("(self,&str)(u32)")); +} diff --git a/crates/codegraph-graph/src/lib.rs b/crates/codegraph-graph/src/lib.rs index da6ea79fad..fba8859236 100644 --- a/crates/codegraph-graph/src/lib.rs +++ b/crates/codegraph-graph/src/lib.rs @@ -1564,6 +1564,8 @@ impl GraphIndex { } } + self.link_impls_globally(); + // ── Phase 2: resolve placeholder 0 trong chains ── self.build_impl_index(); self.build_go_method_sets(); @@ -1833,6 +1835,61 @@ impl GraphIndex { self.go_impls = go_impls; } + /// Rust impl linking Ở MỨC GLOBAL (extractor chỉ thấy cùng file): + /// `impl Worker` ở file khác `struct Worker` vẫn được nối — impl symbol + /// (Class, type_name = self type, type_ref == 0) tìm def cùng tên trong + /// toàn registry, rồi re-parent methods của impl về def. + fn link_impls_globally(&mut self) { + // def name (lower) → def id — chỉ def thật (type_name None). + let mut defs: HashMap = HashMap::new(); + for sym in self.symbols.values() { + if sym.language == "rust" + && matches!(sym.kind, SymbolKind::Class | SymbolKind::Enum) + && sym.type_name.is_none() + { + defs.entry(sym.name.to_lowercase()).or_insert(sym.id); + } + } + if defs.is_empty() { + return; + } + // impl (type_name Some, type_ref == 0) → def id. + let mut impl_to_def: HashMap = HashMap::new(); + for sym in self.symbols.values() { + if sym.language != "rust" || sym.kind != SymbolKind::Class || sym.type_ref != 0 { + continue; + } + let Some(self_type) = &sym.type_name else { + continue; + }; + // `Store` → "store" (generic base). + let base = self_type + .split('<') + .next() + .unwrap_or("") + .trim() + .to_lowercase(); + if base.is_empty() { + continue; + } + if let Some(&def_id) = defs.get(&base) { + impl_to_def.insert(sym.id, def_id); + } + } + if impl_to_def.is_empty() { + return; + } + for sym in self.symbols.values_mut() { + if let Some(&def_id) = impl_to_def.get(&sym.id) { + sym.type_ref = def_id; + } else if sym.kind == SymbolKind::Method + && let Some(&def_id) = impl_to_def.get(&sym.scope_id) + { + sym.scope_id = def_id; + } + } + } + /// Resolve một call record về real symbol id — trả `None` nếu không được. /// /// Ưu tiên: structural hint (TargetClass/TargetMethod — VD Java class @@ -2364,12 +2421,16 @@ impl GraphIndex { /// import path (monorepo: import "corp/api/orders" → file chứa "orders"). /// Stdlib ngoài index → trả rỗng (giữ raw, không fake). fn import_alias_candidates(&self, caller_id: u64, call_name: &str) -> Vec { - let Some(dot) = call_name.find('.') else { + // Separator theo ngôn ngữ: `var.method` (Go/Java/C) hoặc `Type::method` + // (Rust use-path). + let (var, method) = if let Some(dot) = call_name.find('.') { + (&call_name[..dot], &call_name[dot + 1..]) + } else if let Some(pos) = call_name.find("::") { + (&call_name[..pos], &call_name[pos + 2..]) + } else { return Vec::new(); }; - let var = &call_name[..dot]; - let method = &call_name[dot + 1..]; - if method.is_empty() || method.contains('.') { + if method.is_empty() || method.contains('.') || method.contains("::") { return Vec::new(); } let Some(file) = self.symbols.get(&caller_id).map(|s| s.file.as_str()) else { @@ -2384,12 +2445,23 @@ impl GraphIndex { continue; } let tail = imp.path.rsplit('/').next().unwrap_or(""); + let type_tail = imp.path.rsplit("::").next().unwrap_or(""); if let Some(ids) = self.name_index.get(&method.to_lowercase()) { out.extend(ids.iter().copied().filter(|&id| { self.symbols.get(&id).is_some_and(|s| { - matches!(s.kind, SymbolKind::Function | SymbolKind::Method) - && !tail.is_empty() - && s.file.contains(tail) + if !matches!(s.kind, SymbolKind::Function | SymbolKind::Method) { + return false; + } + if imp.path.contains("::") { + // Rust: `use crate::storage::Database` + call + // `Database::new` → method của class `Database`. + self.symbols + .get(&s.scope_id) + .is_some_and(|p| p.name == type_tail) + } else { + // Go: import path segment khớp directory trong file. + !tail.is_empty() && s.file.contains(tail) + } }) })); } @@ -5736,6 +5808,140 @@ mod tests { assert_eq!(flow.calls[0].resolved_via, ResolutionKind::Alias); } + #[tokio::test] + async fn rust_use_resolves_qualified_call() { + // `use crate::storage::Database;` + call `Database::new()` → method new + // của class Database (file chứa class đó). + let mut idx = GraphIndex::in_memory(); + let (caller, db_class, db_new) = (SYMBOL_BASE, SYMBOL_BASE + 1, SYMBOL_BASE + 2); + let mut r = result( + "src/main.rs", + vec![ + dsym(caller, "main", SymbolKind::Function, 0, None, 10), + dsym(db_class, "Database", SymbolKind::Class, 0, None, 1), + dsym(db_new, "new", SymbolKind::Method, db_class, None, 2), + ], + HashMap::from([(caller, vec![caller, 0])]), + vec![CallRecord { + caller_id: caller, + call_name: "Database::new".to_string(), + position: 1, + line: 11, + condition: None, + is_loop_body: false, + effect: EffectType::None, + effect_desc: None, + target_class: None, + target_method: None, + into_var: None, + is_new_var: false, + ..Default::default() + }], + ); + if let Some(c) = r.symbols.iter_mut().find(|s| s.name == "main") { + c.file = "src/main.rs".to_string(); + } + if let Some(db) = r.symbols.iter_mut().find(|s| s.name == "Database") { + db.file = "src/storage/database.rs".to_string(); + } + if let Some(nm) = r.symbols.iter_mut().find(|s| s.name == "new") { + nm.file = "src/storage/database.rs".to_string(); + } + r.file_deps = Some(crate::FileDeps { + package: None, + imports: vec![crate::ImportAlias { + alias: "Database".to_string(), + path: "crate::storage::Database".to_string(), + }], + }); + idx.ingest(&[r], None).await.unwrap(); + + let flow = idx.flow(caller).await.unwrap(); + assert_eq!(flow.chain, vec![caller, db_new]); + assert_eq!(flow.calls[0].to_id, Some(db_new)); + assert_eq!(flow.calls[0].resolved_via, ResolutionKind::Alias); + } + + #[tokio::test] + async fn rust_impl_linking_across_files() { + // struct Worker ở worker.rs; impl Worker { fn run } ở worker_impl.rs — + // linking global phải nối method về def (extractor per-file không thấy). + let mut idx = GraphIndex::in_memory(); + let (def_id, impl_id, run_id, caller_id) = ( + SYMBOL_BASE, + SYMBOL_BASE + 1, + SYMBOL_BASE + 2, + SYMBOL_BASE + 3, + ); + let rsym = |id: u64, + name: &str, + kind: SymbolKind, + scope_id: u64, + type_name: Option<&str>, + line: u32| Symbol { + id, + name: name.to_string(), + kind, + scope: ScopeLevel::Global, + scope_id, + type_ref: 0, + type_name: type_name.map(String::from), + file: String::new(), + line, + end_line: line, + signature: None, + doc: None, + annotations: Vec::new(), + language: "rust".to_string(), + impl_trait: None, + sig_key: None, + }; + let def_file = result( + "worker.rs", + vec![rsym(def_id, "Worker", SymbolKind::Class, 0, None, 1)], + HashMap::new(), + vec![], + ); + // impl symbol: type_name = self type "Worker", type_ref = 0 (chưa nối). + let mut impl_sym = rsym(impl_id, "Worker", SymbolKind::Class, 0, Some("Worker"), 1); + impl_sym.file = "worker_impl.rs".to_string(); + let mut run_sym = rsym(run_id, "run", SymbolKind::Method, impl_id, None, 2); + run_sym.file = "worker_impl.rs".to_string(); + let mut caller_sym = rsym(caller_id, "caller", SymbolKind::Function, 0, None, 10); + caller_sym.file = "worker_impl.rs".to_string(); + let impl_file = result( + "worker_impl.rs", + vec![impl_sym, run_sym, caller_sym], + HashMap::from([(caller_id, vec![caller_id, 0])]), + vec![CallRecord { + caller_id, + call_name: "run".to_string(), + position: 1, + line: 11, + condition: None, + is_loop_body: false, + effect: EffectType::None, + effect_desc: None, + target_class: None, + target_method: None, + into_var: None, + is_new_var: false, + ..Default::default() + }], + ); + idx.ingest(&[def_file, impl_file], None).await.unwrap(); + + // run scope đã re-parent về Worker def → field_declared_type-style + // resolution tìm thấy method thuộc class Worker; chain resolve tĩnh. + let flow = idx.flow(caller_id).await.unwrap(); + assert_eq!(flow.chain, vec![caller_id, run_id]); + let run = idx.symbol_by_id(run_id).unwrap(); + assert_eq!( + run.scope_id, def_id, + "run phải được re-parent về Worker def" + ); + } + #[tokio::test] async fn serde_old_call_record_blob_defaults() { // Blob cũ (trước khi thêm receiver_text/resolved_via/candidates) phải From e577565b9646e3ddb30187bff55ddc1a72393d1d Mon Sep 17 00:00:00 2001 From: Hung Pham Date: Sun, 25 Oct 2026 15:52:39 +0700 Subject: [PATCH 05/19] Python semantic enrichment + persist dynamic indexes qua reopen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Python (theo review gaps): - param_container_kinds: `parameters` container → Parameter symbols (identifier / typed_parameter / default_parameter, type từ type hint) — field/param typing cho tie-break resolution. - field_from_self_attr: `self.attr = Database()` → Field symbol scope vào class, type từ callee text — instance-attr typing. - py_file_deps: import/import-as/from-import → FileDeps (alias→path), ăn vào import_alias_candidates sẵn có. - py_superclasses: `class File(Writer)` → impl_trait "Writer" — inheritance fan-out qua machinery impl_index (giống C++ base class). Persistence (fix: dynamic indexes chỉ ở RAM, MCP restart là mất): - Storage trait + sqlite (sg_file_deps/sg_fn_ptr_assigns) + in_memory: persist ĐẦU VÀO thô FileDeps + FnPtrAssigns theo file path. - rebuild() khi open: rebuild file_imports/fn_ptr_index từ blob + link_impls_globally/impl_index/go_impls từ sg_symbols — resolution quality giữ nguyên sau restart. E2e: dynamic_indexes_survive_reopen. --- .../codegraph-extract/src/languages/common.rs | 142 ++++++++++++++++++ .../codegraph-extract/src/languages/python.rs | 126 +++++++++++++++- .../tests/dynamic_dispatch.rs | 78 ++++++++++ crates/codegraph-graph/src/lib.rs | 68 +++++++++ crates/codegraph-graph/src/storage.rs | 18 +++ .../codegraph-graph/src/storage/in_memory.rs | 46 ++++++ crates/codegraph-graph/src/storage/sqlite.rs | 57 +++++++ crates/codegraph-graph/tests/sqlite.rs | 91 +++++++++++ 8 files changed, 625 insertions(+), 1 deletion(-) diff --git a/crates/codegraph-extract/src/languages/common.rs b/crates/codegraph-extract/src/languages/common.rs index 12fa64dbe3..234692ff28 100644 --- a/crates/codegraph-extract/src/languages/common.rs +++ b/crates/codegraph-extract/src/languages/common.rs @@ -73,6 +73,12 @@ pub struct LangSpec { pub class_kinds: &'static [&'static str], /// Parameter node kinds — scope Parameter, scope_id = function chứa. pub param_kinds: &'static [&'static str], + /// Container node chứa parameter (Python `parameters`) — walker special- + /// case: identifier / typed_parameter / default_parameter con → Parameter. + pub param_container_kinds: &'static [&'static str], + /// `self.attr = ` trong method → Field symbol (type từ RHS) — + /// Python/Ruby convention. + pub field_from_self_attr: bool, /// Annotation node kinds (VD Java `annotation`/`marker_annotation`). pub annotation_kinds: &'static [&'static str], /// Call rules. @@ -103,6 +109,8 @@ impl LangSpec { func_kinds: &[], class_kinds: &[], param_kinds: &[], + param_container_kinds: &[], + field_from_self_attr: false, annotation_kinds: &[], calls: &[], name_type_fallback: false, @@ -352,6 +360,140 @@ struct SymbolCtx<'a> { fn collect_symbols(node: &Node, ctx: &mut SymbolCtx, spec: &'static LangSpec) { let k = node.kind(); + // Parameter container (Python `parameters`): identifier / typed_parameter / + // default_parameter con → Parameter symbol với type (nếu có type hint). + if spec.param_container_kinds.contains(&k) { + if let Some(&(fid, _)) = ctx.scope_stack.last() { + for ch in named_children(node) { + let (name_node, type_node) = match ch.kind() { + "identifier" => (Some(ch), None), + "typed_parameter" | "default_parameter" => { + let name = ch.child_by_field_name("name").or_else(|| { + // typed_parameter: identifier con TRỰC TIẾP đầu tiên + // (first_identifier duyệt DFS từ cuối → dính type). + let mut cursor = ch.walk(); + let mut found = None; + for g in ch.children(&mut cursor) { + if g.kind() == "identifier" { + found = Some(g); + break; + } + } + found + }); + (name, ch.child_by_field_name("type")) + } + _ => (None, None), + }; + let Some(name_node) = name_node else { + continue; + }; + let Some(pname) = text(&name_node, ctx.src) else { + continue; + }; + if pname.is_empty() { + continue; + } + let id = ctx.next_id; + ctx.next_id += 1; + ctx.symbols.push(Symbol { + id, + name: pname, + kind: SymbolKind::Parameter, + scope: ScopeLevel::Parameter, + scope_id: fid, + type_ref: 0, + type_name: type_node.and_then(|t| text(&t, ctx.src)), + file: ctx.file.to_string(), + line: name_node.start_position().row as u32 + 1, + end_line: name_node.end_position().row as u32 + 1, + signature: None, + doc: None, + annotations: Vec::new(), + language: ctx.language.to_string(), + impl_trait: None, + sig_key: None, + }); + } + } + // KHÔNG return — default_value có thể chứa call (`x=f()`). + } + + // `self.attr = ` trong method → Field (type từ RHS: `Database()` + // → "Database", identifier → text) — nguyên liệu instance-attr typing. + if spec.field_from_self_attr && k == "assignment" { + let left = node.child_by_field_name("left"); + let is_self_attr = left.is_some_and(|left| { + if left.kind() != "attribute" { + return false; + } + let Some(obj) = left.child_by_field_name("object") else { + return false; + }; + text(&obj, ctx.src).as_deref() == Some("self") + && left + .child_by_field_name("attribute") + .and_then(|a| text(&a, ctx.src)) + .is_some() + }); + let attr_info = left + .unwrap() + .child_by_field_name("attribute") + .and_then(|a| text(&a, ctx.src).map(|t| (a, t))); + if is_self_attr { + let Some((attr, fname)) = attr_info else { + return; + }; + // Class scope gần nhất trong stack (method đang đứng trong class). + let class_id = ctx + .scope_stack + .iter() + .rev() + .find(|(_, is_class)| *is_class) + .map(|(id, _)| *id); + if let Some(class_id) = class_id { + // RHS là call (`Database()`, `np.array()`) → type = callee text; + // RHS identifier thuần (`self.db = database`) → None (không có + // type info tin cậy). + let type_name = node.child_by_field_name("right").and_then(|right| { + if right.kind() != "call" { + return None; + } + right + .child_by_field_name("function") + .and_then(|f| text(&f, ctx.src)) + }); + // Dedup: gán nhiều lần cùng attr trong class → 1 Field. + let exists = ctx.symbols.iter().any(|s| { + s.kind == SymbolKind::Field && s.scope_id == class_id && s.name == fname + }); + if !exists { + let id = ctx.next_id; + ctx.next_id += 1; + ctx.symbols.push(Symbol { + id, + name: fname, + kind: SymbolKind::Field, + scope: ScopeLevel::ObjectField, + scope_id: class_id, + type_ref: 0, + type_name, + file: ctx.file.to_string(), + line: attr.start_position().row as u32 + 1, + end_line: attr.end_position().row as u32 + 1, + signature: None, + doc: None, + annotations: Vec::new(), + language: ctx.language.to_string(), + impl_trait: None, + sig_key: None, + }); + } + } + } + // KHÔNG return — RHS vẫn cần đi qua pass chain (calls). + } + if let Some(&(_, skind)) = spec.decls.iter().find(|(s, _)| *s == k) { // Refine kind theo nội dung node (VD Go: `type_spec` chứa `interface_type` // → Interface thay vì Class). diff --git a/crates/codegraph-extract/src/languages/python.rs b/crates/codegraph-extract/src/languages/python.rs index b4e4728e17..70084b9b90 100644 --- a/crates/codegraph-extract/src/languages/python.rs +++ b/crates/codegraph-extract/src/languages/python.rs @@ -1,5 +1,6 @@ -use crate::languages::common::{CallRule, HookSpec, LangSpec, MarkerSpec}; +use crate::languages::common::{text, CallRule, HookSpec, LangSpec, MarkerSpec}; use codegraph_core::SymbolKind; +use codegraph_graph::{FileDeps, ImportAlias}; use tree_sitter::Node; fn ts_language() -> tree_sitter::Language { @@ -31,6 +32,8 @@ pub static SPEC: LangSpec = LangSpec { ], func_kinds: &["function_definition", "lambda"], class_kinds: &["class_definition"], + param_container_kinds: &["parameters"], + field_from_self_attr: true, calls: &[CallRule { kind: "call", callee_field: "function", @@ -40,6 +43,8 @@ pub static SPEC: LangSpec = LangSpec { }], hooks: HookSpec { anonymous_name_fn: Some(anonymous_name_node), + impl_trait_fn: Some(py_superclasses), + file_deps_fn: Some(py_file_deps), ..HookSpec::none() }, markers: MarkerSpec { @@ -62,3 +67,122 @@ pub static SPEC: LangSpec = LangSpec { ..LangSpec::dflt() }; crate::lang_parser!(PythonParser, SPEC); + +/// Base classes của class (`class File(Writer, close)` → "Writer,close") — +/// đóng vai impl_trait: fan-out dynamic dispatch qua machinery impl_index +/// (giống C++ base class: method của base + override ở subclass). +fn py_superclasses(node: &Node, src: &[u8]) -> Option { + if node.kind() != "class_definition" { + return None; + } + let supers = node.child_by_field_name("superclasses")?; + let mut names: Vec = Vec::new(); + let mut stack = vec![supers]; + while let Some(n) = stack.pop() { + match n.kind() { + "identifier" => { + if let Some(t) = text(&n, src) { + names.push(t); + } + } + "attribute" => { + if let Some(t) = text(&n, src) { + names.push(t); // module.Class — giữ nguyên, split ',' sau + } + } + _ => { + let mut cursor = n.walk(); + for ch in n.children(&mut cursor) { + stack.push(ch); + } + } + } + } + names.dedup(); + (!names.is_empty()).then(|| names.join(",")) +} + +/// Imports của file → FileDeps: +/// - `import numpy as np` → alias "np", path "numpy" +/// - `import os.path` → alias "path", path "os.path" +/// - `from orders.models import Order, get_order as fetch` +/// → alias "Order"/"fetch", path "orders.models.Order"/"orders.models.get_order" +fn py_file_deps(root: &Node, src: &[u8]) -> Option { + let mut imports: Vec = Vec::new(); + let mut stack = vec![*root]; + while let Some(n) = stack.pop() { + match n.kind() { + "import_statement" => { + let mut cursor = n.walk(); + for ch in n.children(&mut cursor) { + match ch.kind() { + "dotted_name" => { + let path = text(&ch, src).unwrap_or_default(); + let alias = path.rsplit('.').next().unwrap_or("").to_string(); + if !alias.is_empty() { + imports.push(ImportAlias { alias, path }); + } + } + "aliased_import" => { + let raw = text(&ch, src).unwrap_or_default(); + if let Some((p, a)) = raw.split_once(" as ") { + imports.push(ImportAlias { + alias: a.trim().to_string(), + path: p.trim().to_string(), + }); + } + } + _ => {} + } + } + } + "import_from_statement" => { + let module_node = n.child_by_field_name("module_name"); + let module = module_node + .as_ref() + .and_then(|m| text(m, src)) + .unwrap_or_default(); + let mut cursor = n.walk(); + for ch in n.children(&mut cursor) { + // Bỏ node module_name — chỉ xử lý các name được import. + if module_node.is_some_and(|m| m.id() == ch.id()) { + continue; + } + match ch.kind() { + "dotted_name" => { + let name = text(&ch, src).unwrap_or_default(); + imports.push(ImportAlias { + alias: name.clone(), + path: format!("{module}.{name}"), + }); + } + "aliased_import" => { + let raw = text(&ch, src).unwrap_or_default(); + if let Some((p, a)) = raw.split_once(" as ") { + imports.push(ImportAlias { + alias: a.trim().to_string(), + path: format!("{}.{}", module, p.trim()), + }); + } + } + _ => {} + } + } + } + _ => { + let mut cursor = n.walk(); + for ch in n.children(&mut cursor) { + stack.push(ch); + } + } + } + } + if imports.is_empty() { + None + } else { + Some(FileDeps { + package: None, + imports, + }) + } +} diff --git a/crates/codegraph-extract/tests/dynamic_dispatch.rs b/crates/codegraph-extract/tests/dynamic_dispatch.rs index 3d8d3fcd08..084b6c27e6 100644 --- a/crates/codegraph-extract/tests/dynamic_dispatch.rs +++ b/crates/codegraph-extract/tests/dynamic_dispatch.rs @@ -7,6 +7,7 @@ use codegraph_extract::languages::c::CParser; use codegraph_extract::languages::cpp::CppParser; use codegraph_extract::languages::go::GoParser; use codegraph_extract::languages::java::JavaParser; +use codegraph_extract::languages::python::PythonParser; use codegraph_extract::LangParser; #[test] @@ -420,3 +421,80 @@ impl Database { .expect("query missing"); assert_eq!(query.sig_key.as_deref(), Some("(self,&str)(u32)")); } + +#[test] +fn python_params_self_fields_imports_superclasses() { + let src = r#" +import os +import numpy as np +from orders.models import Order, get_order as fetch + +class Writer: + def write(self, data): pass + +class File(Writer): + def __init__(self, path: str, mode="r"): + self.path = path + self.data = np.array() + + def write(self, data: str) -> int: + return len(data) +"#; + let parsed = PythonParser.parse_file("f.py", src).unwrap(); + + // P1: params — typed + plain + default. + for name in ["self", "path", "mode", "data"] { + assert!( + parsed + .symbols + .iter() + .any(|s| s.kind == SymbolKind::Parameter && s.name == name), + "param {name} missing" + ); + } + let path_param = parsed + .symbols + .iter() + .find(|s| s.name == "path" && s.kind == SymbolKind::Parameter) + .unwrap(); + assert_eq!(path_param.type_name.as_deref(), Some("str")); + + // P1b: self.attr → Field, type từ RHS class call. + let path_field = parsed + .symbols + .iter() + .find(|s| s.name == "path" && s.kind == SymbolKind::Field) + .expect("field self.path missing"); + assert_eq!(path_field.type_name, None); // RHS là param, không phải class call + let data_field = parsed + .symbols + .iter() + .find(|s| s.name == "data" && s.kind == SymbolKind::Field) + .expect("field self.data missing"); + assert_eq!(data_field.type_name.as_deref(), Some("np.array")); + // Field scope vào class File (không phải __init__). + let file_id = parsed.symbols.iter().find(|s| s.name == "File").unwrap().id; + assert_eq!(data_field.scope_id, file_id); + + // P2: imports. + let deps = parsed.file_deps.expect("file deps missing"); + let mut pairs: Vec<(&str, &str)> = deps + .imports + .iter() + .map(|i| (i.alias.as_str(), i.path.as_str())) + .collect(); + pairs.sort(); + assert_eq!( + pairs, + vec![ + ("Order", "orders.models.Order"), + ("fetch", "orders.models.get_order"), + ("np", "numpy"), + ("os", "os"), + ] + ); + + // P5: inheritance → impl_trait "Writer" (fan-out machinery ăn trực tiếp). + let file = parsed.symbols.iter().find(|s| s.name == "File").unwrap(); + assert_eq!(file.impl_trait.as_deref(), Some("Writer")); +} diff --git a/crates/codegraph-graph/src/lib.rs b/crates/codegraph-graph/src/lib.rs index fba8859236..35f1f4e8c1 100644 --- a/crates/codegraph-graph/src/lib.rs +++ b/crates/codegraph-graph/src/lib.rs @@ -1163,6 +1163,52 @@ impl GraphIndex { } } + // ── Dynamic-dispatch indexes — rebuild từ persist ── + // file_imports/fn_ptr_index từ blob (sg_file_deps/sg_fn_ptr_assigns); + // impl_index/go_impls derive từ symbols; link_impls_globally chạy lại + // vì sg_symbols lưu state TRƯỚC linking. + self.file_imports.clear(); + for (path, bytes) in self + .storage + .read() + .await + .all_file_deps() + .await + .map_err(serr)? + { + if let Ok(deps) = serde_json::from_slice::(&bytes) { + self.file_imports.insert(path, deps.imports); + } + } + self.fn_ptr_index.clear(); + for (_, bytes) in self + .storage + .read() + .await + .all_fn_ptr_assigns() + .await + .map_err(serr)? + { + if let Ok(assigns) = serde_json::from_slice::>(&bytes) { + for a in assigns { + let targets = self + .name_index + .get(&a.target.to_lowercase()) + .cloned() + .unwrap_or_default(); + self.fn_ptr_index + .entry(a.var.to_lowercase()) + .or_default() + .extend(targets); + } + } + } + self.impl_index.clear(); + self.go_impls.clear(); + self.link_impls_globally(); + self.build_impl_index(); + self.build_go_method_sets(); + // Edge count — đếm thẳng từ chains (không materialize EdgeMeta: // metadata đã nằm trong call records persist, `flow()` đọc từ đó). // Không cần `all_call_records` ở đây (đó là JSON deserialize ~80ms). @@ -1593,6 +1639,28 @@ impl GraphIndex { .await .map_err(serr)?; self.files.push(f); + // Persist ĐẦU VÀO của dynamic index — MCP restart phải rebuild + // được từ storage, không mất resolution quality. + if let Some(deps) = &result.file_deps + && let Ok(bytes) = serde_json::to_vec(deps) + { + self.storage + .write() + .await + .set_file_deps(&result.path, &bytes) + .await + .map_err(serr)?; + } + if !result.fn_ptr_assigns.is_empty() + && let Ok(bytes) = serde_json::to_vec(&result.fn_ptr_assigns) + { + self.storage + .write() + .await + .set_fn_ptr_assigns(&result.path, &bytes) + .await + .map_err(serr)?; + } if let Some(p) = p { p.advance(1); } diff --git a/crates/codegraph-graph/src/storage.rs b/crates/codegraph-graph/src/storage.rs index 8e5fdfd21e..38a686c1b8 100644 --- a/crates/codegraph-graph/src/storage.rs +++ b/crates/codegraph-graph/src/storage.rs @@ -452,6 +452,24 @@ pub trait EntityStorage: Send + Sync { Ok(Vec::new()) } + // ── File deps (package/imports) + fn-ptr assign-sites ── + // Persist ĐẦU VÀO thô của các dynamic index (file_imports/fn_ptr_index) — + // rebuild khi open (MCP restart không được mất resolution quality). + async fn set_file_deps(&mut self, _path: &str, _deps: &[u8]) -> Result<()> { + Ok(()) + } + #[cfg_attr(not(feature = "sqlite"), allow(dead_code))] + async fn all_file_deps(&self) -> Result)>> { + Ok(Vec::new()) + } + async fn set_fn_ptr_assigns(&mut self, _path: &str, _assigns: &[u8]) -> Result<()> { + Ok(()) + } + #[cfg_attr(not(feature = "sqlite"), allow(dead_code))] + async fn all_fn_ptr_assigns(&self) -> Result)>> { + Ok(Vec::new()) + } + // ── Call-name index ── /// Lưu inverted index `call name → call sites` (opaque bytes, JSON) — mặc /// định: no-op. diff --git a/crates/codegraph-graph/src/storage/in_memory.rs b/crates/codegraph-graph/src/storage/in_memory.rs index 346fa5a24a..767d7469af 100644 --- a/crates/codegraph-graph/src/storage/in_memory.rs +++ b/crates/codegraph-graph/src/storage/in_memory.rs @@ -148,6 +148,8 @@ pub(crate) struct MemoryData { pub(crate) next_id: u64, /// func id → call records (JSON). pub(crate) call_records: HashMap>, + pub(crate) file_deps: HashMap>, + pub(crate) fn_ptr_assigns: HashMap>, /// call name → call sites (JSON). pub(crate) call_names: HashMap>, /// path → FileInfo. @@ -184,6 +186,8 @@ impl InMemoryStorage { // Id bắt đầu từ SYMBOL_BASE (marker reserved 1..=99). next_id: codegraph_core::SYMBOL_BASE, call_records: HashMap::new(), + file_deps: HashMap::new(), + fn_ptr_assigns: HashMap::new(), call_names: HashMap::new(), files: HashMap::new(), version: 0, @@ -561,6 +565,46 @@ impl EntityStorage for InMemoryStorage { Ok(d.call_records.get(&func).cloned()) } + async fn set_file_deps(&mut self, path: &str, deps: &[u8]) -> Result<()> { + let mut d = self + .data + .write() + .map_err(|_| StorageError::Internal("poison".into()))?; + d.file_deps.insert(path.to_string(), deps.to_vec()); + Ok(()) + } + + async fn all_file_deps(&self) -> Result)>> { + let d = self + .data + .read() + .map_err(|_| StorageError::Internal("poison".into()))?; + Ok(d.file_deps + .iter() + .map(|(p, b)| (p.clone(), b.clone())) + .collect()) + } + + async fn set_fn_ptr_assigns(&mut self, path: &str, assigns: &[u8]) -> Result<()> { + let mut d = self + .data + .write() + .map_err(|_| StorageError::Internal("poison".into()))?; + d.fn_ptr_assigns.insert(path.to_string(), assigns.to_vec()); + Ok(()) + } + + async fn all_fn_ptr_assigns(&self) -> Result)>> { + let d = self + .data + .read() + .map_err(|_| StorageError::Internal("poison".into()))?; + Ok(d.fn_ptr_assigns + .iter() + .map(|(p, b)| (p.clone(), b.clone())) + .collect()) + } + async fn all_call_records(&self) -> Result)>> { let d = self .data @@ -662,6 +706,8 @@ impl EntityStorage for InMemoryStorage { .map_err(|_| StorageError::Internal("poison".into()))?; d.symbols.clear(); d.next_id = codegraph_core::SYMBOL_BASE; + d.file_deps.clear(); + d.fn_ptr_assigns.clear(); d.call_records.clear(); d.call_names.clear(); d.files.clear(); diff --git a/crates/codegraph-graph/src/storage/sqlite.rs b/crates/codegraph-graph/src/storage/sqlite.rs index aad53f7c37..e861443ca4 100644 --- a/crates/codegraph-graph/src/storage/sqlite.rs +++ b/crates/codegraph-graph/src/storage/sqlite.rs @@ -211,6 +211,14 @@ impl SqliteStorage { func INTEGER PRIMARY KEY, records BLOB NOT NULL )", + "CREATE TABLE IF NOT EXISTS sg_file_deps ( + path TEXT PRIMARY KEY, + deps BLOB NOT NULL + )", + "CREATE TABLE IF NOT EXISTS sg_fn_ptr_assigns ( + path TEXT PRIMARY KEY, + assigns BLOB NOT NULL + )", "CREATE TABLE IF NOT EXISTS sg_call_names ( name TEXT PRIMARY KEY, sites BLOB NOT NULL @@ -703,6 +711,53 @@ impl EntityStorage for SqliteStorage { Ok(records) } + async fn set_file_deps(&mut self, path: &str, deps: &[u8]) -> Result<()> { + let mut conn = self.pool.acquire().await.map_err(db_err)?; + sqlx::query( + "INSERT INTO sg_file_deps (path, deps) VALUES (?1, ?2) + ON CONFLICT(path) DO UPDATE SET deps = excluded.deps", + ) + .bind(path) + .bind(deps) + .execute(&mut *conn) + .await + .map_err(db_err)?; + Ok(()) + } + + async fn all_file_deps(&self) -> Result)>> { + let mut conn = self.pool.acquire().await.map_err(db_err)?; + let rows: Vec<(String, Vec)> = sqlx::query_as("SELECT path, deps FROM sg_file_deps") + .fetch_all(&mut *conn) + .await + .map_err(db_err)?; + Ok(rows) + } + + async fn set_fn_ptr_assigns(&mut self, path: &str, assigns: &[u8]) -> Result<()> { + let mut conn = self.pool.acquire().await.map_err(db_err)?; + sqlx::query( + "INSERT INTO sg_fn_ptr_assigns (path, assigns) VALUES (?1, ?2) + ON CONFLICT(path) DO UPDATE SET assigns = excluded.assigns", + ) + .bind(path) + .bind(assigns) + .execute(&mut *conn) + .await + .map_err(db_err)?; + Ok(()) + } + + async fn all_fn_ptr_assigns(&self) -> Result)>> { + let mut conn = self.pool.acquire().await.map_err(db_err)?; + let rows: Vec<(String, Vec)> = + sqlx::query_as("SELECT path, assigns FROM sg_fn_ptr_assigns") + .fetch_all(&mut *conn) + .await + .map_err(db_err)?; + Ok(rows) + } + async fn all_call_records(&self) -> Result)>> { let mut conn = self.pool.acquire().await.map_err(db_err)?; let rows: Vec<(i64, Vec)> = @@ -847,6 +902,8 @@ impl EntityStorage for SqliteStorage { let mut conn = self.pool.acquire().await.map_err(db_err)?; for stmt in [ "DELETE FROM sg_symbols", + "DELETE FROM sg_file_deps", + "DELETE FROM sg_fn_ptr_assigns", "DELETE FROM sg_call_records", "DELETE FROM sg_call_names", "DELETE FROM sg_files", diff --git a/crates/codegraph-graph/tests/sqlite.rs b/crates/codegraph-graph/tests/sqlite.rs index ceeb0b8f48..b9ad521aa1 100644 --- a/crates/codegraph-graph/tests/sqlite.rs +++ b/crates/codegraph-graph/tests/sqlite.rs @@ -385,3 +385,94 @@ async fn external_qualified_call_not_linked_to_local_variable() { assert_eq!(cees[0].name, "selectDepartment"); assert_eq!(cees[0].file, "UserController.java"); } + +/// Dynamic indexes (file_imports/fn_ptr_index/impl_index/go_impls) PHẢI sống +/// sau reopen — MCP restart không được mất resolution quality. +#[tokio::test] +async fn dynamic_indexes_survive_reopen() { + let dir = tempfile::tempdir().unwrap(); + let path = format!("sqlite://{}", dir.path().join("dyn.db").to_string_lossy()); + + // Ingest: Go-style import alias + fn ptr assign + dynamic call. + let mut r = result( + "main.go", + vec![ + sym("main.go", "sink", SYMBOL_BASE), + sym("corp/api/orders/handler.go", "Get", SYMBOL_BASE + 1), + sym("main.go", "compile_file", SYMBOL_BASE + 2), + sym("main.go", "dtrace_compile_file", SYMBOL_BASE + 3), + sym("main.go", "call_site", SYMBOL_BASE + 4), + ], + HashMap::from([ + (SYMBOL_BASE, vec![SYMBOL_BASE, 0]), + (SYMBOL_BASE + 4, vec![SYMBOL_BASE + 4, 0]), + ]), + vec![ + CallRecord { + caller_id: SYMBOL_BASE, + call_name: "orders.Get".to_string(), + position: 1, + line: 1, + condition: None, + is_loop_body: false, + effect: EffectType::None, + effect_desc: None, + target_class: None, + target_method: None, + into_var: None, + is_new_var: false, + ..Default::default() + }, + CallRecord { + caller_id: SYMBOL_BASE + 4, + call_name: "zend_compile_file".to_string(), + position: 1, + line: 2, + condition: None, + is_loop_body: false, + effect: EffectType::None, + effect_desc: None, + target_class: None, + target_method: None, + into_var: None, + is_new_var: false, + ..Default::default() + }, + ], + ); + r.file_deps = Some(codegraph_graph::FileDeps { + package: Some("main".to_string()), + imports: vec![codegraph_graph::ImportAlias { + alias: "orders".to_string(), + path: "corp/api/orders".to_string(), + }], + }); + r.fn_ptr_assigns = vec![codegraph_graph::FnPtrAssign { + var: "zend_compile_file".to_string(), + target: "compile_file".to_string(), + line: 5, + }]; + { + let mut idx = GraphIndex::open(&path).await.unwrap(); + idx.ingest(&[r], None).await.unwrap(); + } + + // Reopen — dynamic indexes rebuild từ persist. + let idx = GraphIndex::open(&path).await.unwrap(); + + // Import alias: orders.Get → Get. + let flow = idx.flow(SYMBOL_BASE).await.unwrap(); + assert_eq!(flow.calls[0].to_id, Some(SYMBOL_BASE + 1)); + assert_eq!( + flow.calls[0].resolved_via, + codegraph_core::ResolutionKind::Alias + ); + + // Fn ptr assign: zend_compile_file → compile_file (PointerAssign). + let flow2 = idx.flow(SYMBOL_BASE + 4).await.unwrap(); + assert_eq!( + flow2.calls[0].resolved_via, + codegraph_core::ResolutionKind::PointerAssign + ); + assert_eq!(flow2.calls[0].candidate_names, vec!["compile_file"]); +} From 7de341e4e405e7e1f64c61bd5114d9c1755aa92a Mon Sep 17 00:00:00 2001 From: Hung Pham Date: Sun, 25 Oct 2026 16:02:22 +0700 Subject: [PATCH 06/19] Rust config review fixes: trait-declared methods + macro invocations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review bằng fixture "thật" (trait + impl + macro + closure + async) phát hiện: - function_signature_item (method KHAI BÁO trong trait) không được extract — không có symbol scope vào Interface nên fan-out Rust không bao giờ fire trên code thật (các test trước dùng synthetic symbols). Thêm vào decls → trigger Interface như Go method_elem. - macro_invocation không phải call (println!/vec! mất hoàn toàn) — thêm CallRule với rust_macro_call_name (`println!`). - E2E extraction thật: trait Repo{ping} + 2 impl → r.ping() fan-out cả hai (HierarchyFanOut) — chứng minh path thật chạy, không chỉ synthetic. - Cập nhật chain expectation test cũ: cg_t!/cg_d! giờ là call thật. Còn lại (defer): closure/callable tracking (cb()), generic instantiation, associated type semantics, UFCS. --- .../codegraph-extract/src/languages/rust.rs | 35 ++++- crates/codegraph-extract/tests/chains.rs | 10 ++ .../tests/dynamic_dispatch.rs | 120 ++++++++++++++++++ 3 files changed, 158 insertions(+), 7 deletions(-) diff --git a/crates/codegraph-extract/src/languages/rust.rs b/crates/codegraph-extract/src/languages/rust.rs index 46005719f3..2988af34fe 100644 --- a/crates/codegraph-extract/src/languages/rust.rs +++ b/crates/codegraph-extract/src/languages/rust.rs @@ -26,6 +26,10 @@ pub static SPEC: LangSpec = LangSpec { ("field_declaration", SymbolKind::Field), ("parameter", SymbolKind::Parameter), ("let_declaration", SymbolKind::Variable), + // Method KHAI BÁO trong trait (như Go method_elem) — scope vào trait + // Interface symbol → trigger fan-out thật (không có symbol này, + // dynamic dispatch Rust chỉ match impl theo tên, chọn 1 best guess). + ("function_signature_item", SymbolKind::Method), ], func_kinds: &["function_item"], class_kinds: &[ @@ -36,13 +40,23 @@ pub static SPEC: LangSpec = LangSpec { "mod_item", ], annotation_kinds: &["attribute"], - calls: &[CallRule { - kind: "call_expression", - callee_field: "function", - arguments_field: "arguments", - name_fn: None, - target_fn: None, - }], + calls: &[ + CallRule { + kind: "call_expression", + callee_field: "function", + arguments_field: "arguments", + name_fn: None, + target_fn: None, + }, + // `println!(...)`, `vec![...]` — macro invocation là call thật. + CallRule { + kind: "macro_invocation", + callee_field: "macro", + arguments_field: "tokens", + name_fn: Some(rust_macro_call_name), + target_fn: None, + }, + ], name_type_fallback: true, link_impl_methods: true, hooks: HookSpec { @@ -83,6 +97,13 @@ fn rust_class_type_name(node: &tree_sitter::Node, src: &[u8]) -> Option crate::lang_parser!(RustParser, SPEC); +/// Tên macro call: `println!` — macro field text + `!`. +fn rust_macro_call_name(node: &Node, src: &[u8]) -> Option { + let mac = node.child_by_field_name("macro")?; + let raw = text(&mac, src)?; + Some(format!("{raw}!")) +} + /// `use` declarations của file — nguyên liệu resolve qualified call /// (`Database::new` với `use crate::storage::Database;`). /// - `use foo::bar as baz;` → alias "baz", path "foo::bar" diff --git a/crates/codegraph-extract/tests/chains.rs b/crates/codegraph-extract/tests/chains.rs index eb599bdae7..e469f7932a 100644 --- a/crates/codegraph-extract/tests/chains.rs +++ b/crates/codegraph-extract/tests/chains.rs @@ -1394,9 +1394,16 @@ fn f() { ); // with the fix: into_var is now on the OUTERMOST call (map_err), not innermost (read) // this is correct because map_err produces the final value that gets assigned + // Macro invocation (cg_t!/cg_d!) giờ là call thật — edge macro → caller. assert_eq!( c, [ + "[CALL_ENTER]", + "cg_t! → __t", + "[CALL_EXIT]", + "[CALL_ENTER]", + "cg_t! → __t_load", + "[CALL_EXIT]", "[CALL_ENTER]", "self.storage.read().await.load_all_symbols().await.map_err → symbols", "[CALL_ENTER]", @@ -1407,6 +1414,9 @@ fn f() { "[CALL_EXIT]", "[CALL_EXIT]", "[CALL_ENTER]", + "cg_d!", + "[CALL_EXIT]", + "[CALL_ENTER]", "self.storage.read().await.load_all_files().await.map_err → self.files", "[CALL_ENTER]", "self.storage.read().await.load_all_files", diff --git a/crates/codegraph-extract/tests/dynamic_dispatch.rs b/crates/codegraph-extract/tests/dynamic_dispatch.rs index 084b6c27e6..48f6fe5037 100644 --- a/crates/codegraph-extract/tests/dynamic_dispatch.rs +++ b/crates/codegraph-extract/tests/dynamic_dispatch.rs @@ -498,3 +498,123 @@ class File(Writer): let file = parsed.symbols.iter().find(|s| s.name == "File").unwrap(); assert_eq!(file.impl_trait.as_deref(), Some("Writer")); } + +#[test] +fn rust_review_realistic_program() { + // Fixture "thật" — dùng để review cấu hình Rust: trait method khai báo, + // macro call, closure, async fn, associated const/type. + let src = r#" +pub trait Repo { + type Item; + const MAX: usize; + fn find(&self, id: u32) -> Option; + fn ping(&self); +} + +pub struct Pg; + +impl Repo for Pg { + type Item = u32; + const MAX: usize = 10; + fn find(&self, id: u32) -> Option { Some(id) } + fn ping(&self) { println!("ping"); } +} + +pub struct Srv { repo: Pg } + +impl Srv { + pub async fn handle(&self, id: u32) -> Option { + let cb = || self.repo.ping(); + cb(); + vec![1, 2].len(); + match id { + 0 => None, + _ => { self.repo.find(id) } + } + } +} +"#; + let parsed = codegraph_extract::languages::rust::RustParser + .parse_file("srv.rs", src) + .unwrap(); + for s in &parsed.symbols { + eprintln!( + "SYM {:?} {:?} scope={} type={:?} line={}", + s.name, s.kind, s.scope_id, s.type_name, s.line + ); + } + for c in &parsed.calls { + eprintln!( + "CALL {:?} recv={:?} line={}", + c.call_name, c.receiver_text, c.line + ); + } + // trait method khai báo (function_signature_item) có symbol? + assert!( + parsed.symbols.iter().any(|s| s.name == "find" + && s.type_name.is_none() + && parsed + .symbols + .iter() + .find(|t| t.id == s.scope_id) + .map(|p| p.kind) + == Some(SymbolKind::Interface)), + "trait-declared method find (Interface scope) missing" + ); + // macro call được bắt? + assert!( + parsed.calls.iter().any(|c| c.call_name.contains("println")), + "macro invocation println! missing from calls" + ); +} + +#[tokio::test] +async fn rust_trait_fanout_end_to_end_real_extraction() { + use codegraph_graph::{GraphIndex, ParseResult}; + + let src = r#" +pub trait Repo { + fn ping(&self); +} +pub struct Pg; +pub struct My; + +impl Repo for Pg { + fn ping(&self) { println!("pg"); } +} +impl Repo for My { + fn ping(&self) { println!("my"); } +} + +pub fn sink(r: &dyn Repo) { + r.ping(); +} +"#; + let parsed: ParseResult = codegraph_extract::languages::rust::RustParser + .parse_file("r.rs", src) + .unwrap(); + let mut idx = GraphIndex::in_memory(); + idx.ingest(&[parsed], None).await.unwrap(); + + // sink's call `r.ping()` phải fan-out cả Pg::ping lẫn My::ping. + let sink_id = idx + .resolve_by_name_or_id("sink", 0) + .unwrap() + .symbol + .unwrap() + .id; + let flow = idx.flow(sink_id).await.unwrap(); + let ping = flow + .calls + .iter() + .find(|c| c.to_name.ends_with("ping")) + .expect("ping call missing"); + assert_eq!( + ping.resolved_via, + codegraph_core::ResolutionKind::HierarchyFanOut, + "fan-out không fire trên extraction thật" + ); + let mut names = ping.candidate_names.clone(); + names.sort(); + assert_eq!(names, vec!["ping", "ping"]); +} From a3a7107d02708640c5ccb00ee1da4480c99f5e8b Mon Sep 17 00:00:00 2001 From: Hung Pham Date: Sun, 25 Oct 2026 16:18:43 +0700 Subject: [PATCH 07/19] =?UTF-8?q?Persist=20file=5Fdeps/fn=5Fptr=5Fassigns?= =?UTF-8?q?=20cho=20M=E1=BB=8CI=20storage=20backend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Trước đó chỉ sqlite + in_memory implement — lmdb/postgres/mysql/redis rơi về no-op default = mất dynamic indexes khi reopen trên các backend đó. - lmdb: 2 named DBI (sg_file_deps/sg_fn_ptr_assigns) + set/all (cursor iter) + clear_entities. - postgres/mysql: bảng sg_file_deps/sg_fn_ptr_assigns (repo_id, path) PK, CREATE TABLE IF NOT EXISTS idempotent khi mở repo (không cần migration tay); mysql dùng ON DUPLICATE KEY UPDATE; clear_entities thêm 2 bảng. - redis: 2 hash keys (filedeps/fnptpassigns) + DEL trong clear_entities. - cached.rs: forward 4 methods xuống inner storage (trước đây rơi về default no-op → bypass backend thật!). - Test roundtrip in_memory (set/all/clear); build + test --all-features. --- crates/codegraph-graph/src/storage/cached.rs | 17 ++++ .../codegraph-graph/src/storage/in_memory.rs | 20 +++++ crates/codegraph-graph/src/storage/lmdb.rs | 59 +++++++++++++ crates/codegraph-graph/src/storage/mysql.rs | 84 ++++++++++++++++++ .../codegraph-graph/src/storage/postgres.rs | 85 +++++++++++++++++++ crates/codegraph-graph/src/storage/redis.rs | 50 +++++++++++ 6 files changed, 315 insertions(+) diff --git a/crates/codegraph-graph/src/storage/cached.rs b/crates/codegraph-graph/src/storage/cached.rs index 8adb41b202..47c443a9bb 100644 --- a/crates/codegraph-graph/src/storage/cached.rs +++ b/crates/codegraph-graph/src/storage/cached.rs @@ -437,6 +437,23 @@ impl EntityStorage for CachedStorage { self.inner.all_call_records().await } + // ── File deps + fn-ptr assigns (no cache — nhỏ, dùng một lần khi rebuild) ── + async fn set_file_deps(&mut self, path: &str, deps: &[u8]) -> Result<(), StorageError> { + self.inner.set_file_deps(path, deps).await + } + + async fn all_file_deps(&self) -> Result)>, StorageError> { + self.inner.all_file_deps().await + } + + async fn set_fn_ptr_assigns(&mut self, path: &str, assigns: &[u8]) -> Result<(), StorageError> { + self.inner.set_fn_ptr_assigns(path, assigns).await + } + + async fn all_fn_ptr_assigns(&self) -> Result)>, StorageError> { + self.inner.all_fn_ptr_assigns().await + } + // ── Call-name index ── async fn set_call_name_index(&mut self, name: &str, sites: &[u8]) -> Result<(), StorageError> { self.inner.set_call_name_index(name, sites).await?; diff --git a/crates/codegraph-graph/src/storage/in_memory.rs b/crates/codegraph-graph/src/storage/in_memory.rs index 767d7469af..8bd39e913b 100644 --- a/crates/codegraph-graph/src/storage/in_memory.rs +++ b/crates/codegraph-graph/src/storage/in_memory.rs @@ -996,4 +996,24 @@ mod tests { assert_eq!(s.load_call_name_index("name").await.unwrap(), None); assert_eq!(s.version().await.unwrap(), 0); } + + #[tokio::test] + async fn file_deps_and_fn_ptr_assigns_roundtrip() { + let mut s = InMemoryStorage::default(); + s.set_file_deps("a.go", b"{\"imports\":[]}").await.unwrap(); + s.set_fn_ptr_assigns("z.c", b"[{\"var\":\"p\",\"target\":\"f\",\"line\":1}]") + .await + .unwrap(); + let deps = s.all_file_deps().await.unwrap(); + assert_eq!( + deps, + vec![("a.go".to_string(), b"{\"imports\":[]}".to_vec())] + ); + let assigns = s.all_fn_ptr_assigns().await.unwrap(); + assert_eq!(assigns.len(), 1); + // clear_entities xoá cả hai — ingest mới không rò dữ liệu cũ. + s.clear_entities().await.unwrap(); + assert!(s.all_file_deps().await.unwrap().is_empty()); + assert!(s.all_fn_ptr_assigns().await.unwrap().is_empty()); + } } diff --git a/crates/codegraph-graph/src/storage/lmdb.rs b/crates/codegraph-graph/src/storage/lmdb.rs index 646680e5db..aefa440fca 100644 --- a/crates/codegraph-graph/src/storage/lmdb.rs +++ b/crates/codegraph-graph/src/storage/lmdb.rs @@ -177,6 +177,8 @@ const D_COUNTER: &str = "rt_counter"; const D_SYMBOLS: &str = "sg_symbols"; const D_NEXT_ID: &str = "sg_next_id"; const D_CALL_RECORDS: &str = "sg_call_records"; +const D_FILE_DEPS: &str = "sg_file_deps"; +const D_FN_PTR_ASSIGNS: &str = "sg_fn_ptr_assigns"; const D_CALL_NAMES: &str = "sg_call_names"; const D_FILES: &str = "sg_files"; const D_VERSION: &str = "sg_meta"; @@ -289,6 +291,8 @@ pub struct LmdbStorage { symbols: Database, next_id: Database, call_records: Database, + file_deps: Database, + fn_ptr_assigns: Database, call_names: Database, files: Database, version: Database, @@ -350,6 +354,12 @@ impl LmdbStorage { let call_records = env .create_db(Some(D_CALL_RECORDS), DatabaseFlags::empty()) .map_err(e)?; + let file_deps = env + .create_db(Some(D_FILE_DEPS), DatabaseFlags::empty()) + .map_err(e)?; + let fn_ptr_assigns = env + .create_db(Some(D_FN_PTR_ASSIGNS), DatabaseFlags::empty()) + .map_err(e)?; let call_names = env .create_db(Some(D_CALL_NAMES), DatabaseFlags::empty()) .map_err(e)?; @@ -382,6 +392,8 @@ impl LmdbStorage { symbols, next_id, call_records, + file_deps, + fn_ptr_assigns, call_names, files, version, @@ -842,6 +854,51 @@ impl EntityStorage for LmdbStorage { Ok(out) } + async fn set_file_deps(&mut self, path: &str, deps: &[u8]) -> Result<()> { + let mut tx = self.env.begin_rw_txn().map_err(e)?; + tx.put(self.file_deps, &str_key(path), &deps, WriteFlags::empty()) + .map_err(e)?; + tx.commit().map_err(e)?; + Ok(()) + } + + async fn all_file_deps(&self) -> Result)>> { + let tx = self.env.begin_ro_txn().map_err(e)?; + let mut cur = tx.open_ro_cursor(self.file_deps).map_err(e)?; + let mut out = Vec::new(); + for item in cur.iter() { + let (k, v) = item.map_err(e)?; + let path = std::str::from_utf8(k).map_err(|e| StorageError::Internal(e.to_string()))?; + out.push((path.to_string(), v.to_vec())); + } + Ok(out) + } + + async fn set_fn_ptr_assigns(&mut self, path: &str, assigns: &[u8]) -> Result<()> { + let mut tx = self.env.begin_rw_txn().map_err(e)?; + tx.put( + self.fn_ptr_assigns, + &str_key(path), + &assigns, + WriteFlags::empty(), + ) + .map_err(e)?; + tx.commit().map_err(e)?; + Ok(()) + } + + async fn all_fn_ptr_assigns(&self) -> Result)>> { + let tx = self.env.begin_ro_txn().map_err(e)?; + let mut cur = tx.open_ro_cursor(self.fn_ptr_assigns).map_err(e)?; + let mut out = Vec::new(); + for item in cur.iter() { + let (k, v) = item.map_err(e)?; + let path = std::str::from_utf8(k).map_err(|e| StorageError::Internal(e.to_string()))?; + out.push((path.to_string(), v.to_vec())); + } + Ok(out) + } + async fn set_call_name_index(&mut self, name: &str, sites: &[u8]) -> Result<()> { let mut tx = self.env.begin_rw_txn().map_err(e)?; tx.put( @@ -934,6 +991,8 @@ impl EntityStorage for LmdbStorage { for db in [ self.symbols, self.call_records, + self.file_deps, + self.fn_ptr_assigns, self.call_names, self.files, self.embeddings, diff --git a/crates/codegraph-graph/src/storage/mysql.rs b/crates/codegraph-graph/src/storage/mysql.rs index ce204314be..72a8b098a8 100644 --- a/crates/codegraph-graph/src/storage/mysql.rs +++ b/crates/codegraph-graph/src/storage/mysql.rs @@ -81,6 +81,30 @@ impl MySqlStorage { .await .map_err(db_err)?; // Stats tổng hợp (codegraph_status đọc O(1) không rebuild) — idempotent. + + // Dynamic-index inputs (file imports + fn-ptr assigns) — idempotent. + sqlx::query( + "CREATE TABLE IF NOT EXISTS sg_file_deps ( + repo_id BIGINT NOT NULL, + path TEXT NOT NULL, + deps BLOB NOT NULL, + PRIMARY KEY (repo_id, path) + )", + ) + .execute(&self.pool) + .await + .map_err(db_err)?; + sqlx::query( + "CREATE TABLE IF NOT EXISTS sg_fn_ptr_assigns ( + repo_id BIGINT NOT NULL, + path TEXT NOT NULL, + assigns BLOB NOT NULL, + PRIMARY KEY (repo_id, path) + )", + ) + .execute(&self.pool) + .await + .map_err(db_err)?; sqlx::query( "CREATE TABLE IF NOT EXISTS sg_stats ( repo_id BIGINT NOT NULL, @@ -635,6 +659,64 @@ impl EntityStorage for MySqlStorage { Ok(out) } + async fn set_file_deps(&mut self, path: &str, deps: &[u8]) -> Result<()> { + sqlx::query( + "INSERT INTO sg_file_deps (repo_id, path, deps) VALUES (?, ?, ?) \ + ON DUPLICATE KEY UPDATE deps = VALUES(deps)", + ) + .bind(self.repo_id as i64) + .bind(path) + .bind(deps) + .execute(&self.pool) + .await + .map_err(db_err)?; + Ok(()) + } + + async fn all_file_deps(&self) -> Result)>> { + let rows = sqlx::query("SELECT path, deps FROM sg_file_deps WHERE repo_id = ?") + .bind(self.repo_id as i64) + .fetch_all(&self.pool) + .await + .map_err(db_err)?; + let mut out = Vec::with_capacity(rows.len()); + for r in &rows { + let path: String = r.try_get("path").map_err(db_err)?; + let deps: Vec = r.try_get("deps").map_err(db_err)?; + out.push((path, deps)); + } + Ok(out) + } + + async fn set_fn_ptr_assigns(&mut self, path: &str, assigns: &[u8]) -> Result<()> { + sqlx::query( + "INSERT INTO sg_fn_ptr_assigns (repo_id, path, assigns) VALUES (?, ?, ?) \ + ON DUPLICATE KEY UPDATE assigns = VALUES(assigns)", + ) + .bind(self.repo_id as i64) + .bind(path) + .bind(assigns) + .execute(&self.pool) + .await + .map_err(db_err)?; + Ok(()) + } + + async fn all_fn_ptr_assigns(&self) -> Result)>> { + let rows = sqlx::query("SELECT path, assigns FROM sg_fn_ptr_assigns WHERE repo_id = ?") + .bind(self.repo_id as i64) + .fetch_all(&self.pool) + .await + .map_err(db_err)?; + let mut out = Vec::with_capacity(rows.len()); + for r in &rows { + let path: String = r.try_get("path").map_err(db_err)?; + let assigns: Vec = r.try_get("assigns").map_err(db_err)?; + out.push((path, assigns)); + } + Ok(out) + } + async fn set_call_name_index(&mut self, name: &str, sites: &[u8]) -> Result<()> { sqlx::query( "INSERT INTO sg_call_names (repo_id, name, sites) VALUES (?, ?, ?) \ @@ -781,6 +863,8 @@ impl EntityStorage for MySqlStorage { "sg_symbols", "sg_embeddings", "sg_files", + "sg_file_deps", + "sg_fn_ptr_assigns", "sg_call_records", "sg_call_names", "rt_nodes", diff --git a/crates/codegraph-graph/src/storage/postgres.rs b/crates/codegraph-graph/src/storage/postgres.rs index 4ac3dee23c..40691d7ac3 100644 --- a/crates/codegraph-graph/src/storage/postgres.rs +++ b/crates/codegraph-graph/src/storage/postgres.rs @@ -92,6 +92,31 @@ impl PostgresStorage { .execute(&self.pool) .await .map_err(db_err)?; + + // Dynamic-index inputs (file imports + fn-ptr assigns) — idempotent. + sqlx::query( + "CREATE TABLE IF NOT EXISTS sg_file_deps ( + repo_id BIGINT NOT NULL, + path TEXT NOT NULL, + deps BYTEA NOT NULL, + PRIMARY KEY (repo_id, path) + )", + ) + .execute(&self.pool) + .await + .map_err(db_err)?; + sqlx::query( + "CREATE TABLE IF NOT EXISTS sg_fn_ptr_assigns ( + repo_id BIGINT NOT NULL, + path TEXT NOT NULL, + assigns BYTEA NOT NULL, + PRIMARY KEY (repo_id, path) + )", + ) + .execute(&self.pool) + .await + .map_err(db_err)?; + // Stats tổng hợp (codegraph_status đọc O(1) không rebuild) — idempotent. sqlx::query( "CREATE TABLE IF NOT EXISTS sg_stats ( @@ -647,6 +672,64 @@ impl EntityStorage for PostgresStorage { Ok(out) } + async fn set_file_deps(&mut self, path: &str, deps: &[u8]) -> Result<()> { + sqlx::query( + "INSERT INTO sg_file_deps (repo_id, path, deps) VALUES ($1, $2, $3) \ + ON CONFLICT (repo_id, path) DO UPDATE SET deps = EXCLUDED.deps", + ) + .bind(self.repo_id as i64) + .bind(path) + .bind(deps) + .execute(&self.pool) + .await + .map_err(db_err)?; + Ok(()) + } + + async fn all_file_deps(&self) -> Result)>> { + let rows = sqlx::query("SELECT path, deps FROM sg_file_deps WHERE repo_id = $1") + .bind(self.repo_id as i64) + .fetch_all(&self.pool) + .await + .map_err(db_err)?; + let mut out = Vec::with_capacity(rows.len()); + for r in &rows { + let path: String = r.try_get("path").map_err(db_err)?; + let deps: Vec = r.try_get("deps").map_err(db_err)?; + out.push((path, deps)); + } + Ok(out) + } + + async fn set_fn_ptr_assigns(&mut self, path: &str, assigns: &[u8]) -> Result<()> { + sqlx::query( + "INSERT INTO sg_fn_ptr_assigns (repo_id, path, assigns) VALUES ($1, $2, $3) \ + ON CONFLICT (repo_id, path) DO UPDATE SET assigns = EXCLUDED.assigns", + ) + .bind(self.repo_id as i64) + .bind(path) + .bind(assigns) + .execute(&self.pool) + .await + .map_err(db_err)?; + Ok(()) + } + + async fn all_fn_ptr_assigns(&self) -> Result)>> { + let rows = sqlx::query("SELECT path, assigns FROM sg_fn_ptr_assigns WHERE repo_id = $1") + .bind(self.repo_id as i64) + .fetch_all(&self.pool) + .await + .map_err(db_err)?; + let mut out = Vec::with_capacity(rows.len()); + for r in &rows { + let path: String = r.try_get("path").map_err(db_err)?; + let assigns: Vec = r.try_get("assigns").map_err(db_err)?; + out.push((path, assigns)); + } + Ok(out) + } + async fn set_call_name_index(&mut self, name: &str, sites: &[u8]) -> Result<()> { sqlx::query( "INSERT INTO sg_call_names (repo_id, name, sites) VALUES ($1, $2, $3) \ @@ -793,6 +876,8 @@ impl EntityStorage for PostgresStorage { "sg_symbols", "sg_embeddings", "sg_files", + "sg_file_deps", + "sg_fn_ptr_assigns", "sg_call_records", "sg_call_names", "rt_nodes", diff --git a/crates/codegraph-graph/src/storage/redis.rs b/crates/codegraph-graph/src/storage/redis.rs index 8c5e2ee5ef..3891458f1b 100644 --- a/crates/codegraph-graph/src/storage/redis.rs +++ b/crates/codegraph-graph/src/storage/redis.rs @@ -690,6 +690,54 @@ impl EntityStorage for RedisStorage { Ok(out) } + async fn set_file_deps(&mut self, path: &str, deps: &[u8]) -> Result<()> { + let mut conn = self.lock().await; + cmd("HSET") + .arg(self.kb.key("filedeps")) + .arg(path) + .arg(deps) + .query_async::<()>(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + Ok(()) + } + + async fn all_file_deps(&self) -> Result)>> { + let mut conn = self.lock().await; + let map: HashMap> = cmd("HGETALL") + .arg(self.kb.key("filedeps")) + .query_async(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + let mut out: Vec<(String, Vec)> = map.into_iter().collect(); + out.sort_by(|a, b| a.0.cmp(&b.0)); + Ok(out) + } + + async fn set_fn_ptr_assigns(&mut self, path: &str, assigns: &[u8]) -> Result<()> { + let mut conn = self.lock().await; + cmd("HSET") + .arg(self.kb.key("fnptpassigns")) + .arg(path) + .arg(assigns) + .query_async::<()>(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + Ok(()) + } + + async fn all_fn_ptr_assigns(&self) -> Result)>> { + let mut conn = self.lock().await; + let map: HashMap> = cmd("HGETALL") + .arg(self.kb.key("fnptpassigns")) + .query_async(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + let mut out: Vec<(String, Vec)> = map.into_iter().collect(); + out.sort_by(|a, b| a.0.cmp(&b.0)); + Ok(out) + } + async fn set_call_name_index(&mut self, name: &str, sites: &[u8]) -> Result<()> { let mut conn = self.lock().await; cmd("HSET") @@ -784,6 +832,8 @@ impl EntityStorage for RedisStorage { .arg(self.kb.key("nextid")) .arg(self.kb.key("callrecords")) .arg(self.kb.key("callnames")) + .arg(self.kb.key("filedeps")) + .arg(self.kb.key("fnptpassigns")) .arg(self.kb.key("files")) .arg(self.kb.key("version")) .query_async::<()>(&mut *conn) From 252544a0169adc2ba696363dc8873e65491585e7 Mon Sep 17 00:00:00 2001 From: Hung Pham Date: Sun, 25 Oct 2026 16:56:12 +0700 Subject: [PATCH 08/19] =?UTF-8?q?callers()/impact=20xuy=C3=AAn=20dyn-trait?= =?UTF-8?q?:=20fan-out=20fire=20theo=20receiver=5Ftext=20+=20dyn=5Fcallers?= =?UTF-8?q?=20index?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review qua MCP index thật (codegraph-rs tự index chính nó) phát hiện: - `callers(set_file_deps)` trả rỗng dù ingest_with_progress gọi nó qua `dyn Storage` — call qua trait object bị resolve Single vì is_method_dispatch check `call_name.contains('.')` nhưng extractor đã rút call_name còn bare, trong khi receiver_text vẫn giữ receiver. - Fix 1: is_method_dispatch = receiver_text.is_some() || call_name.contains('.') → fan-out fire cho dyn-trait call. - Fix 2: dyn_callers (callee → callers) build từ call_meta khi ingest, scan call records khi rebuild; callers_resumable merge vào pending (dedup qua visited) → callers()/impact() thấy caller xuyên dyn-trait/interface/fn-ptr. - E2E: callers() của từng impl method thấy dyn caller (trước đây rỗng). Note vận hành: MCP server đang chạy bị MDB_BAD_RSLOT sau re-index in-process — reader locktable stale, cần restart MCP server (issue riêng, chưa xử lý). --- crates/codegraph-graph/src/lib.rs | 150 +++++++++++++++++++++++++++++- 1 file changed, 149 insertions(+), 1 deletion(-) diff --git a/crates/codegraph-graph/src/lib.rs b/crates/codegraph-graph/src/lib.rs index 35f1f4e8c1..adadbb2702 100644 --- a/crates/codegraph-graph/src/lib.rs +++ b/crates/codegraph-graph/src/lib.rs @@ -386,6 +386,10 @@ pub struct GraphIndex { /// dispatch. Build từ `Symbol.impl_trait` khi ingest; in-memory (blob call /// record đã persist candidates nên reopen không cần index này). impl_index: HashMap>, + /// Fan-out edges đảo chiều: callee (dynamic target) → callers — nguồn cho + /// `callers()`/`impact` xuyên `dyn Trait`/interface/fn-ptr (chain radix + /// không thấy vì fan-out không ghi đè chain slot). + dyn_callers: HashMap>, /// Function-pointer variable (lowercase) → các target function đã gán /// (`p = f;` qua assign-site tracking). In-memory như `impl_index`. fn_ptr_index: HashMap>, @@ -1067,6 +1071,7 @@ impl GraphIndex { fn_ptr_index: HashMap::new(), go_impls: HashMap::new(), file_imports: HashMap::new(), + dyn_callers: HashMap::new(), edge_count: 0, files: Vec::new(), next_id: SYMBOL_BASE, @@ -1208,6 +1213,24 @@ impl GraphIndex { self.link_impls_globally(); self.build_impl_index(); self.build_go_method_sets(); + // dyn_callers từ call records persist (candidates đã lưu trong blob). + let all_records = self + .storage + .read() + .await + .all_call_records() + .await + .map_err(serr)?; + self.build_dyn_callers_from(all_records.iter().map(|(caller, bytes)| { + let cands = serde_json::from_slice::>(bytes) + .map(|recs| { + recs.into_iter() + .flat_map(|r| r.candidates) + .collect::>() + }) + .unwrap_or_default(); + (*caller, (ResolutionKind::Exact, cands)) + })); // Edge count — đếm thẳng từ chains (không materialize EdgeMeta: // metadata đã nằm trong call records persist, `flow()` đọc từ đó). @@ -1540,6 +1563,7 @@ impl GraphIndex { self.fn_ptr_index.clear(); self.go_impls.clear(); self.file_imports.clear(); + self.dyn_callers.clear(); self.edge_count = 0; self.files.clear(); self.name_records.clear(); @@ -1617,6 +1641,7 @@ impl GraphIndex { self.build_go_method_sets(); let mut call_meta: HashMap<(u64, usize), (ResolutionKind, Vec)> = HashMap::new(); self.resolve_calls(&all_calls, &mut call_meta); + self.build_dyn_callers_from(call_meta.iter().map(|((c, _), v)| (*c, (v.0, v.1.clone())))); // ── Phase 3: call records + call-name index + edge count ── self.build_call_indexes(&all_calls, &call_meta, p).await?; @@ -1832,6 +1857,22 @@ impl GraphIndex { } } + /// Build `dyn_callers` từ call meta — gọi sau `resolve_calls` (ingest) + /// hoặc scan call records (rebuild). + fn build_dyn_callers_from( + &mut self, + entries: impl Iterator))>, + ) { + for (caller, (_, cands)) in entries { + for callee in cands { + let list = self.dyn_callers.entry(callee).or_default(); + if !list.contains(&caller) { + list.push(caller); + } + } + } + } + /// Method-set matching cho Go implicit interface (structural typing): /// type T implements interface I ⟺ tập method name của I ⊆ tập method name /// của T. Nguồn: method symbols có `type_name` = receiver base type @@ -2058,7 +2099,13 @@ impl GraphIndex { // `h.handle`). Bare call `handle` với ứng viên Function tĩnh // thì resolve tĩnh thắng (free function trùng tên trait method // là trùng hợp tên, không phải dispatch). - let is_method_dispatch = call.call_name.contains('.'); + // Method dispatch thật: receiver tồn tại (`r.ping()`, + // `self.storage.read().await.set_file_deps(..)`) — call_name có + // thể bị rút còn bare (`set_file_deps`) nhưng receiver_text + // vẫn giữ dấu chấm. Call_name chứa '.' chỉ là fallback cho + // record thiếu receiver. + let is_method_dispatch = + call.receiver_text.is_some() || call.call_name.contains('.'); let all_dynamic = candidates.iter().all(|&id| { self.symbols .get(&id) @@ -2731,6 +2778,11 @@ impl GraphIndex { }); } state.pending = page.record_ids; + // Fan-out edges (dynamic dispatch) không nằm trong chain — + // engine radix không thấy; merge callers qua dyn-trait. + if let Some(dyn_list) = self.dyn_callers.get(¤t) { + state.pending.extend(dyn_list.iter().map(|&c| c as usize)); + } state.pending_pos = 0; state.search_complete = true; continue; @@ -6010,6 +6062,102 @@ mod tests { ); } + fn self_scope_is_class(idx: &GraphIndex, scope_id: u64) -> bool { + idx.symbol_by_id(scope_id) + .is_some_and(|p| p.kind == SymbolKind::Class) + } + + #[tokio::test] + async fn callers_finds_dyn_trait_dispatch() { + // Call qua trait object (`r.handle()`) — fan-out không ghi đè chain + // slot, nhưng callers() của từng impl PHẢI thấy caller. + let mut idx = GraphIndex::in_memory(); + let (caller, trait_id, m1, m2) = ( + SYMBOL_BASE, + SYMBOL_BASE + 1, + SYMBOL_BASE + 2, + SYMBOL_BASE + 3, + ); + let r = result( + "t.rs", + vec![ + dsym(caller, "caller", SymbolKind::Function, 0, None, 50), + dsym(trait_id, "Handler", SymbolKind::Interface, 0, None, 1), + dsym( + SYMBOL_BASE + 4, + "handle", + SymbolKind::Method, + trait_id, + None, + 2, + ), + dsym( + SYMBOL_BASE + 5, + "ImplA", + SymbolKind::Class, + 0, + Some("Handler"), + 10, + ), + dsym(m1, "handle", SymbolKind::Method, SYMBOL_BASE + 5, None, 11), + dsym( + SYMBOL_BASE + 7, + "ImplB", + SymbolKind::Class, + 0, + Some("Handler"), + 30, + ), + dsym(m2, "handle", SymbolKind::Method, SYMBOL_BASE + 7, None, 31), + ], + HashMap::from([(caller, vec![caller, 0])]), + vec![CallRecord { + caller_id: caller, + call_name: "r.handle".to_string(), + position: 1, + line: 51, + condition: None, + is_loop_body: false, + effect: EffectType::None, + effect_desc: None, + target_class: None, + target_method: None, + into_var: None, + is_new_var: false, + receiver_text: Some("r".to_string()), + ..Default::default() + }], + ); + idx.ingest(&[r], None).await.unwrap(); + + // Fan-out đã ghi nhận (2 candidates). + let flow = idx.flow(caller).await.unwrap(); + assert_eq!(flow.calls[0].resolved_via, ResolutionKind::HierarchyFanOut); + // callers() của MỖI impl thấy caller — trước đây trả rỗng. Id dùng + // GLOBAL (ingest remap): lấy Method "handle" từ registry. + // Chỉ 2 impl method là fan-out target (trait decl bị loại theo thiết + // kế — interface method không phải target gọi được). + let handle_ids: Vec = idx + .symbols + .values() + .filter(|s| { + s.kind == SymbolKind::Method + && s.name == "handle" + && self_scope_is_class(&idx, s.scope_id) + }) + .map(|s| s.id) + .collect(); + assert_eq!(handle_ids.len(), 2); + for m in handle_ids { + let cs = idx.callers(m, 1).await.unwrap(); + let ids: Vec = cs.iter().map(|s| s.id).collect(); + assert!( + ids.contains(&caller), + "callers({m}) missing dyn caller: {ids:?}" + ); + } + } + #[tokio::test] async fn serde_old_call_record_blob_defaults() { // Blob cũ (trước khi thêm receiver_text/resolved_via/candidates) phải From ab8578eb565ca243c1615be59d940a9b25a5285a Mon Sep 17 00:00:00 2001 From: Hung Pham Date: Sun, 25 Oct 2026 18:38:05 +0700 Subject: [PATCH 09/19] Java + JavaScript/TypeScript semantic enrichment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review setup vs machinery Go/Rust/Python đã có — lấp các gap: Java (typing đã tốt nhất: field/param/local + annotations + implements): - java_supers: gộp extends (superclass) + implements vào impl_trait — inheritance fan-out qua machinery C++-base (superclass đứng trước). - java_signature_key: (params)(return) — tên param bỏ. - java_file_deps: import (bỏ static/wildcard) → FileDeps; strip keyword "extends" khỏi superclass text. JavaScript/TypeScript: - param_container_kinds formal_parameters: js identifier/rest_pattern, ts required/optional_parameter (name từ field pattern, type từ type_annotation — strip ':' + '?'). Trước đây params KHÔNG được extract. - js_file_deps (share TS): import default/named-as/namespace → FileDeps. Bug kèm theo: class_type_name cũ không bao giờ match JS thuần (class_heritage không có extends_clause — chỉ TS có); js_extends xử lý cả 2 grammar. - js_extends → impl_trait: inheritance fan-out cho class extends. - js_fn_value_assign: `handler = process` / `const h = process` (identifier thuần) → PointerAssign machinery. Tests: 3 fixture mới (java supers/sig/imports; js params/imports/extends/ fn-value; ts typed params + interface method scope). 20/21 suite (Redis env). --- .../codegraph-extract/src/languages/common.rs | 51 ++++-- .../codegraph-extract/src/languages/java.rs | 110 +++++++++++-- .../src/languages/javascript.rs | 155 ++++++++++++++++++ .../src/languages/typescript.rs | 5 + .../tests/dynamic_dispatch.rs | 141 ++++++++++++++++ 5 files changed, 435 insertions(+), 27 deletions(-) diff --git a/crates/codegraph-extract/src/languages/common.rs b/crates/codegraph-extract/src/languages/common.rs index 234692ff28..2281f17f67 100644 --- a/crates/codegraph-extract/src/languages/common.rs +++ b/crates/codegraph-extract/src/languages/common.rs @@ -367,22 +367,36 @@ fn collect_symbols(node: &Node, ctx: &mut SymbolCtx, spec: &'static LangSpec) { for ch in named_children(node) { let (name_node, type_node) = match ch.kind() { "identifier" => (Some(ch), None), - "typed_parameter" | "default_parameter" => { - let name = ch.child_by_field_name("name").or_else(|| { - // typed_parameter: identifier con TRỰC TIẾP đầu tiên - // (first_identifier duyệt DFS từ cuối → dính type). - let mut cursor = ch.walk(); - let mut found = None; - for g in ch.children(&mut cursor) { - if g.kind() == "identifier" { - found = Some(g); - break; + // Python: typed/default param; TS: required/optional param + // (name ở field `pattern`, type ở field `type`). + "typed_parameter" | "default_parameter" | "required_parameter" + | "optional_parameter" => { + let name = ch + .child_by_field_name("name") + .or_else(|| ch.child_by_field_name("pattern")) + .or_else(|| { + // typed_parameter (Python): identifier con + // TRỰC TIẾP đầu tiên (first_identifier duyệt + // DFS từ cuối → dính type). + let mut cursor = ch.walk(); + let mut found = None; + for g in ch.children(&mut cursor) { + if g.kind() == "identifier" { + found = Some(g); + break; + } } - } - found - }); + found + }); (name, ch.child_by_field_name("type")) } + "rest_pattern" => { + // JS `...args` — identifier con duy nhất. + let mut cursor = ch.walk(); + let found = ch.children(&mut cursor).find(|g| g.kind() == "identifier"); + (found, None) + } + // Destructuring (`{a, b}`, `[x, y]`) — không tên đơn, bỏ. _ => (None, None), }; let Some(name_node) = name_node else { @@ -403,7 +417,16 @@ fn collect_symbols(node: &Node, ctx: &mut SymbolCtx, spec: &'static LangSpec) { scope: ScopeLevel::Parameter, scope_id: fid, type_ref: 0, - type_name: type_node.and_then(|t| text(&t, ctx.src)), + // TS type_annotation text = ": Database" — bỏ ':' + trim. + type_name: type_node + .and_then(|t| text(&t, ctx.src)) + .map(|t| { + t.trim_start_matches(':') + .trim() + .trim_end_matches('?') + .to_string() + }) + .filter(|t| !t.is_empty()), file: ctx.file.to_string(), line: name_node.start_position().row as u32 + 1, end_line: name_node.end_position().row as u32 + 1, diff --git a/crates/codegraph-extract/src/languages/java.rs b/crates/codegraph-extract/src/languages/java.rs index a648406748..ff72ef3ee1 100644 --- a/crates/codegraph-extract/src/languages/java.rs +++ b/crates/codegraph-extract/src/languages/java.rs @@ -1,5 +1,7 @@ use crate::languages::common::{named_children, text, CallRule, HookSpec, LangSpec, MarkerSpec}; use codegraph_core::SymbolKind; +use codegraph_graph::{FileDeps, ImportAlias}; +use tree_sitter::Node as TsNode; use tree_sitter::Node; fn ts_language() -> tree_sitter::Language { @@ -103,7 +105,9 @@ pub static SPEC: LangSpec = LangSpec { }, ], hooks: HookSpec { - impl_trait_fn: Some(java_interfaces), + impl_trait_fn: Some(java_supers), + sig_key_fn: Some(java_signature_key), + file_deps_fn: Some(java_file_deps), ..HookSpec::none() }, markers: MarkerSpec { @@ -130,20 +134,100 @@ pub static SPEC: LangSpec = LangSpec { }; crate::lang_parser!(JavaParser, SPEC); -/// Danh sách interface mà class implements (`interfaces` field của -/// class_declaration — type_list "Payment, Serializable"). Nhiều interface -/// nối bằng ',' — `build_impl_index` split khi build fan-out index. -fn java_interfaces(node: &Node, src: &[u8]) -> Option { +/// Supers của class: superclass (extends) + interfaces (implements) — cả hai +/// vào `impl_trait` (split ',' ở `build_impl_index`). Superclass đứng trước → +/// inheritance fan-out qua machinery C++-base (method của base + override). +fn java_supers(node: &Node, src: &[u8]) -> Option { if !matches!(node.kind(), "class_declaration" | "record_declaration") { return None; } - let tl = node.child_by_field_name("interfaces")?; - // Field text mang cả keyword: "implements Payment, Serializable". - let raw = text(&tl, src)?.replacen("implements", "", 1); - let names: Vec<&str> = raw - .split(',') - .map(str::trim) - .filter(|s| !s.is_empty()) - .collect(); + let mut names: Vec = Vec::new(); + // extends: field `superclass` (type_identifier | scoped type_identifier). + if let Some(sc) = node.child_by_field_name("superclass") { + // Field text mang cả keyword: "extends BasePayment". + if let Some(t) = text(&sc, src) { + names.push(t.replacen("extends", "", 1).trim().to_string()); + } + } + // implements: field `interfaces` — text mang cả keyword "implements A, B". + if let Some(tl) = node.child_by_field_name("interfaces") { + if let Some(raw) = text(&tl, src) { + for n in raw.replacen("implements", "", 1).split(',') { + let n = n.trim(); + if !n.is_empty() { + names.push(n.to_string()); + } + } + } + } + names.dedup(); (!names.is_empty()).then(|| names.join(",")) } + +/// Signature key `(params)(return)` cho method/constructor — Go-style. +/// `pay(int amount) void` → "(int)(void)"; tên param bỏ. +fn java_signature_key(node: &TsNode, src: &[u8]) -> Option { + if !matches!( + node.kind(), + "method_declaration" | "constructor_declaration" + ) { + return None; + } + let mut pt: Vec = Vec::new(); + if let Some(params) = node.child_by_field_name("parameters") { + for ch in named_children(¶ms) { + if let Some(t) = ch.child_by_field_name("type") { + if let Some(txt) = text(&t, src) { + pt.push(txt.replace(' ', "")); + } + } + } + } + let rt = node + .child_by_field_name("type") + .and_then(|t| text(&t, src)) + .map(|t| t.replace(' ', "")) + .unwrap_or_else(|| "void".to_string()); + Some(format!("({})({})", pt.join(","), rt)) +} + +/// Imports của file → FileDeps: +/// - `import com.foo.Bar;` → alias "Bar", path "com.foo.Bar" +/// - `import com.foo.Bar as B;` (hiếm, preview) → alias "B" +/// - `import com.foo.*;` / `import static ...;` wildcard → bỏ qua. +fn java_file_deps(root: &TsNode, src: &[u8]) -> Option { + let mut imports: Vec = Vec::new(); + let mut stack = vec![*root]; + while let Some(n) = stack.pop() { + if n.kind() == "import_declaration" { + let raw = text(&n, src).unwrap_or_default(); + let stmt = raw.trim().trim_end_matches(';').trim(); + let stmt = stmt.strip_prefix("import ").unwrap_or(stmt).trim(); + if stmt.starts_with("static ") || stmt.ends_with(".*") { + continue; // static import / wildcard — không có alias đơn + } + let (path, alias) = match stmt.split_once(" as ") { + Some((p, a)) => (p.trim().to_string(), a.trim().to_string()), + None => { + let seg = stmt.rsplit('.').next().unwrap_or("").to_string(); + (stmt.to_string(), seg) + } + }; + if !alias.is_empty() && !path.is_empty() { + imports.push(ImportAlias { alias, path }); + } + } + let mut cursor = n.walk(); + for ch in n.children(&mut cursor) { + stack.push(ch); + } + } + if imports.is_empty() { + None + } else { + Some(FileDeps { + package: None, + imports, + }) + } +} diff --git a/crates/codegraph-extract/src/languages/javascript.rs b/crates/codegraph-extract/src/languages/javascript.rs index 66425189b7..d3edfa11e2 100644 --- a/crates/codegraph-extract/src/languages/javascript.rs +++ b/crates/codegraph-extract/src/languages/javascript.rs @@ -1,5 +1,6 @@ use crate::languages::common::{named_children, text, CallRule, HookSpec, LangSpec, MarkerSpec}; use codegraph_core::SymbolKind; +use codegraph_graph::{FileDeps, ImportAlias}; use tree_sitter::Node; fn ts_language() -> tree_sitter::Language { @@ -60,6 +61,8 @@ pub static SPEC: LangSpec = LangSpec { "method_definition", ], class_kinds: &["class_declaration", "class"], + param_container_kinds: &["formal_parameters"], + field_from_self_attr: true, calls: &[ CallRule { kind: "call_expression", @@ -80,6 +83,9 @@ pub static SPEC: LangSpec = LangSpec { class_type_name: Some(class_type_name), anonymous_name_fn: Some(anonymous_name_node), value_func_kinds: &["function_expression", "arrow_function"], + impl_trait_fn: Some(js_extends), + file_deps_fn: Some(js_file_deps), + fn_ptr_assign_fn: Some(js_fn_value_assign), ..HookSpec::none() }, markers: MarkerSpec { @@ -107,3 +113,152 @@ pub static SPEC: LangSpec = LangSpec { ..LangSpec::dflt() }; crate::lang_parser!(JavaScriptParser, SPEC); + +fn first_identifier_text(node: &Node, src: &[u8]) -> Option { + let mut cursor = node.walk(); + let mut found = None; + for ch in node.children(&mut cursor) { + if ch.kind() == "identifier" { + found = Some(ch); + break; + } + } + found.and_then(|n| text(&n, src)) +} + +/// Base class của `class D extends B` → "B" — inheritance fan-out qua +/// machinery C++-base (impl_index key = base name). +/// +/// JS thuần: class_heritage chứa identifier/member_expression TRỰC TIẾP +/// (không có node extends_clause như TS — nghĩa là class_type_name cũ cũng +/// không bao giờ match JS!). TS dùng class_type_name qua extends_clause. +pub fn js_extends(node: &Node, src: &[u8]) -> Option { + if !matches!(node.kind(), "class_declaration" | "class") { + return None; + } + let heritage = node + .children(&mut node.walk()) + .find(|c| c.kind() == "class_heritage")?; + let named = named_children(&heritage); + // TS: heritage chứa extends_clause/implements_clause — dùng field name. + if let Some(first) = named.first() { + if matches!(first.kind(), "extends_clause" | "implements_clause") { + return first + .child_by_field_name("name") + .and_then(|n| text(&n, src)); + } + } + // JS: extends token là unnamed → named đầu tiên là base. + named.first().and_then(|n| text(n, src)) +} + +/// Imports của file → FileDeps (ESM): +/// - `import D from 'mod'` → alias "D", path "mod" +/// - `import { a, b as c } from 'mod'` → alias "a"/"c", path "mod.a"/"mod.b" +/// - `import * as ns from 'mod'` → alias "ns" +/// - `import 'mod'` / `export ... from` → bỏ. +pub fn js_file_deps(root: &Node, src: &[u8]) -> Option { + let mut imports: Vec = Vec::new(); + let mut stack = vec![*root]; + while let Some(n) = stack.pop() { + if n.kind() == "import_statement" { + let Some(source) = n.child_by_field_name("source") else { + continue; + }; + let raw = text(&source, src).unwrap_or_default(); + let path = raw.trim_matches(|c| c == '\'' || c == '"').to_string(); + if path.is_empty() { + continue; + } + let mut push = |alias: String| { + if !alias.is_empty() { + imports.push(ImportAlias { + alias, + path: path.clone(), + }); + } + }; + for ch in named_children(&n) { + match ch.kind() { + // `import D from 'mod'` — default trong import_clause. + "identifier" => { + if let Some(t) = text(&ch, src) { + push(t); + } + } + // `import * as ns from 'mod'`. + "namespace_import" => { + let t = first_identifier_text(&ch, src); + push(t.unwrap_or_default()); + } + "import_clause" => { + let mut cc = ch.walk(); + for g in ch.children(&mut cc) { + match g.kind() { + "identifier" => { + if let Some(t) = text(&g, src) { + push(t); + } + } + "namespace_import" => { + if let Some(t) = first_identifier_text(&g, src) { + push(t); + } + } + "named_imports" => { + let mut gc = g.walk(); + for spec in g.children(&mut gc) { + if spec.kind() == "import_specifier" { + let raw = text(&spec, src).unwrap_or_default(); + let alias = match raw.split_once(" as ") { + Some((_, a)) => a.trim().to_string(), + None => raw.trim().to_string(), + }; + push(alias); + } + } + } + _ => {} + } + } + } + _ => {} + } + } + } + let mut cursor = n.walk(); + for ch in n.children(&mut cursor) { + stack.push(ch); + } + } + if imports.is_empty() { + None + } else { + Some(FileDeps { + package: None, + imports, + }) + } +} + +pub fn js_fn_value_assign(node: &Node, src: &[u8]) -> Option<(String, String)> { + match node.kind() { + "assignment_expression" => { + let left = node.child_by_field_name("left")?; + let right = node.child_by_field_name("right")?; + if left.kind() != "identifier" || right.kind() != "identifier" { + return None; + } + Some((text(&left, src)?, text(&right, src)?)) + } + "variable_declarator" => { + let name = node.child_by_field_name("name")?; + let value = node.child_by_field_name("value")?; + if name.kind() != "identifier" || value.kind() != "identifier" { + return None; + } + Some((text(&name, src)?, text(&value, src)?)) + } + _ => None, + } +} diff --git a/crates/codegraph-extract/src/languages/typescript.rs b/crates/codegraph-extract/src/languages/typescript.rs index 9fac296cc6..5900d704d9 100644 --- a/crates/codegraph-extract/src/languages/typescript.rs +++ b/crates/codegraph-extract/src/languages/typescript.rs @@ -67,6 +67,8 @@ pub static SPEC: LangSpec = LangSpec { "enum_declaration", "internal_module", ], + param_container_kinds: &["formal_parameters"], + field_from_self_attr: true, calls: &[ CallRule { kind: "call_expression", @@ -87,6 +89,9 @@ pub static SPEC: LangSpec = LangSpec { class_type_name: Some(class_type_name), anonymous_name_fn: Some(crate::languages::javascript::anonymous_name_node), value_func_kinds: &["function_expression", "arrow_function"], + impl_trait_fn: Some(crate::languages::javascript::js_extends), + file_deps_fn: Some(crate::languages::javascript::js_file_deps), + fn_ptr_assign_fn: Some(crate::languages::javascript::js_fn_value_assign), ..HookSpec::none() }, markers: MarkerSpec { diff --git a/crates/codegraph-extract/tests/dynamic_dispatch.rs b/crates/codegraph-extract/tests/dynamic_dispatch.rs index 48f6fe5037..55f94d1979 100644 --- a/crates/codegraph-extract/tests/dynamic_dispatch.rs +++ b/crates/codegraph-extract/tests/dynamic_dispatch.rs @@ -618,3 +618,144 @@ pub fn sink(r: &dyn Repo) { names.sort(); assert_eq!(names, vec!["ping", "ping"]); } + +#[test] +fn java_supers_sig_and_file_deps() { + let src = r#" +package orders; + +import com.foo.Payment; +import static com.foo.Util.go; + +public class CardPayment extends BasePayment implements Payment, Serializable { + public void pay(int amount, String currency) {} + public CardPayment(int x) {} +} +"#; + let parsed = JavaParser.parse_file("C.java", src).unwrap(); + + // J2: extends + implements gộp vào impl_trait (superclass trước). + let cls = parsed + .symbols + .iter() + .find(|s| s.name == "CardPayment") + .unwrap(); + assert_eq!( + cls.impl_trait.as_deref(), + Some("BasePayment,Payment,Serializable") + ); + + // J3: sig_key — tên param bỏ. + let pay = parsed.symbols.iter().find(|s| s.name == "pay").unwrap(); + assert_eq!(pay.sig_key.as_deref(), Some("(int,String)(void)")); + let ctor = parsed + .symbols + .iter() + .find(|s| s.name == "CardPayment" && s.kind == SymbolKind::Method) + .unwrap(); + assert_eq!(ctor.sig_key.as_deref(), Some("(int)(void)")); + + // J1: imports — static bỏ qua, wildcard bỏ qua. + let deps = parsed.file_deps.expect("file deps missing"); + let mut pairs: Vec<(&str, &str)> = deps + .imports + .iter() + .map(|i| (i.alias.as_str(), i.path.as_str())) + .collect(); + pairs.sort(); + assert_eq!(pairs, vec![("Payment", "com.foo.Payment")]); +} + +#[test] +fn js_params_imports_extends_fn_value() { + let src = r#" +import D, { helper as h } from './util.js'; +import * as ns from 'lib'; + +class Base { + render() {} +} +class Page extends Base { + render() { return 1; } +} + +const handler = process; + +function setup(db, ...rest) { + handler(db); + const { a, b } = obj; +} +"#; + let parsed = codegraph_extract::languages::javascript::JavaScriptParser + .parse_file("p.js", src) + .unwrap(); + + // JS1: params — db + ...rest. + assert!(parsed + .symbols + .iter() + .any(|s| s.kind == SymbolKind::Parameter && s.name == "db")); + assert!(parsed + .symbols + .iter() + .any(|s| s.kind == SymbolKind::Parameter && s.name == "rest")); + + // JS2: imports. + let deps = parsed.file_deps.expect("file deps missing"); + let mut pairs: Vec<(&str, &str)> = deps + .imports + .iter() + .map(|i| (i.alias.as_str(), i.path.as_str())) + .collect(); + pairs.sort(); + assert_eq!( + pairs, + vec![("D", "./util.js"), ("h", "./util.js"), ("ns", "lib")] + ); + + // JS3: extends → impl_trait. + let page = parsed.symbols.iter().find(|s| s.name == "Page").unwrap(); + assert_eq!(page.impl_trait.as_deref(), Some("Base")); + + // JS4: fn-value assignment. + assert!(parsed + .fn_ptr_assigns + .iter() + .any(|a| a.var == "handler" && a.target == "process")); + // destructuring KHÔNG thành fn-value. + assert!(!parsed.fn_ptr_assigns.iter().any(|a| a.var == "a")); +} + +#[test] +fn ts_typed_params_extracted() { + let src = r#" +interface Handler { + handle(req: Request): Promise; +} + +function run(db: Database, limit?: number): void { + db.query(); +} +"#; + let parsed = codegraph_extract::languages::typescript::TypeScriptParser + .parse_file("t.ts", src) + .unwrap(); + let db = parsed + .symbols + .iter() + .find(|s| s.name == "db" && s.kind == SymbolKind::Parameter) + .expect("ts param db missing"); + assert_eq!(db.type_name.as_deref(), Some("Database")); + let limit = parsed + .symbols + .iter() + .find(|s| s.name == "limit" && s.kind == SymbolKind::Parameter) + .expect("ts optional param missing"); + assert_eq!(limit.type_name.as_deref(), Some("number")); + // interface method (method_signature) scope vào interface. + let iface = parsed.symbols.iter().find(|s| s.name == "Handler").unwrap(); + assert!(parsed + .symbols + .iter() + .any(|s| s.name == "handle" && s.kind == SymbolKind::Method && s.scope_id == iface.id)); +} From 1e902f7828508fe705001855214adacab49f699d Mon Sep 17 00:00:00 2001 From: Hung Pham Date: Sun, 25 Oct 2026 18:55:49 +0700 Subject: [PATCH 10/19] =?UTF-8?q?PHP/Ruby/Lua/Scala=20semantic=20enrichmen?= =?UTF-8?q?t=20=E2=80=94=20ho=C3=A0n=20thi=E1=BB=87n=20fan-out=20to=C3=A0n?= =?UTF-8?q?=20ng=C3=B4n=20ng=E1=BB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PHP (typing đã tốt: params/property/variable): - php_supers: base_clause (extends) + class_interface_clause (implements) — LÀ NODE KIND không phải field name (child_by_field_name trả None!) — inheritance fan-out. - php_signature_key + php_use_deps (namespace_use_declaration, bỏ function/const/wildcard). Ruby (mỏng nhất — params chưa từng extract): - param_container_kinds method_parameters: identifier/optional_parameter/ keyword_parameter/splat_parameter. - ruby_superclass → impl_trait (bỏ dynamic superclass `< Struct.new(..)`). - ruby_require_deps: require/require_relative → FileDeps. Lua: - param_container_kinds "parameters" (kind thật, không phải parameter_list). - lua_require_deps: `local mod = require('mod')` ghép variable_list với expression_list theo vị trí. - lua_fn_value_assign: `handler = process` → PointerAssign. Scala: - scala_supers: field "extend" (extends_clause là node kind, field tên "extend") — extends B with T → "B,T". - scala_signature_key + scala_file_deps (import_declaration, selector `.{a, b => c}`); token keyword "import" có cùng kind — guard child_count. Bài học chung: field name vs node kind khác nhau từng grammar — luôn dump AST trước khi wire hook. --- .../codegraph-extract/src/languages/common.rs | 10 +- crates/codegraph-extract/src/languages/lua.rs | 142 ++++++++++++- crates/codegraph-extract/src/languages/php.rs | 116 +++++++++++ .../codegraph-extract/src/languages/ruby.rs | 81 ++++++++ .../codegraph-extract/src/languages/scala.rs | 128 +++++++++++- .../tests/dynamic_dispatch.rs | 193 ++++++++++++++++++ 6 files changed, 663 insertions(+), 7 deletions(-) diff --git a/crates/codegraph-extract/src/languages/common.rs b/crates/codegraph-extract/src/languages/common.rs index 2281f17f67..1419de5f22 100644 --- a/crates/codegraph-extract/src/languages/common.rs +++ b/crates/codegraph-extract/src/languages/common.rs @@ -368,9 +368,11 @@ fn collect_symbols(node: &Node, ctx: &mut SymbolCtx, spec: &'static LangSpec) { let (name_node, type_node) = match ch.kind() { "identifier" => (Some(ch), None), // Python: typed/default param; TS: required/optional param - // (name ở field `pattern`, type ở field `type`). + // (name ở field `pattern`, type ở field `type`); Ruby: + // optional/keyword param (identifier con đầu); Scala: + // parameter (field name + type). "typed_parameter" | "default_parameter" | "required_parameter" - | "optional_parameter" => { + | "optional_parameter" | "keyword_parameter" | "parameter" => { let name = ch .child_by_field_name("name") .or_else(|| ch.child_by_field_name("pattern")) @@ -390,8 +392,8 @@ fn collect_symbols(node: &Node, ctx: &mut SymbolCtx, spec: &'static LangSpec) { }); (name, ch.child_by_field_name("type")) } - "rest_pattern" => { - // JS `...args` — identifier con duy nhất. + "rest_pattern" | "splat_parameter" => { + // JS `...args` / Ruby `*args` — identifier con duy nhất. let mut cursor = ch.walk(); let found = ch.children(&mut cursor).find(|g| g.kind() == "identifier"); (found, None) diff --git a/crates/codegraph-extract/src/languages/lua.rs b/crates/codegraph-extract/src/languages/lua.rs index 41dafe8802..2e6d3cccd6 100644 --- a/crates/codegraph-extract/src/languages/lua.rs +++ b/crates/codegraph-extract/src/languages/lua.rs @@ -1,5 +1,6 @@ -use crate::languages::common::{named_children, CallRule, HookSpec, LangSpec, MarkerSpec}; +use crate::languages::common::{named_children, text, CallRule, HookSpec, LangSpec, MarkerSpec}; use codegraph_core::SymbolKind; +use codegraph_graph::{FileDeps, ImportAlias}; use tree_sitter::Node; fn ts_language() -> tree_sitter::Language { @@ -40,6 +41,7 @@ pub static SPEC: LangSpec = LangSpec { "local_function", ], class_kinds: &[], + param_container_kinds: &["parameters"], calls: &[CallRule { kind: "function_call", callee_field: "name", @@ -50,6 +52,8 @@ pub static SPEC: LangSpec = LangSpec { hooks: HookSpec { anonymous_name_fn: Some(anonymous_name_node), value_func_kinds: &["function_definition"], + file_deps_fn: Some(lua_require_deps), + fn_ptr_assign_fn: Some(lua_fn_value_assign), ..HookSpec::none() }, markers: MarkerSpec { @@ -62,3 +66,139 @@ pub static SPEC: LangSpec = LangSpec { ..LangSpec::dflt() }; crate::lang_parser!(LuaParser, SPEC); + +/// `local mod = require('mod')` / `mod = require('mod')` → FileDeps (alias = +/// biến gán, path = string literal) — pattern chuẩn của Lua module system. +fn lua_require_deps(root: &Node, src: &[u8]) -> Option { + let mut imports: Vec = Vec::new(); + let mut stack = vec![*root]; + while let Some(n) = stack.pop() { + let is_assign = matches!( + n.kind(), + "assignment_statement" | "local_variable_declaration" + ); + if is_assign { + // variable_list (trái) + expression_list (phải, chứa require call). + let mut left_names: Vec> = Vec::new(); + let mut right_calls: Vec> = Vec::new(); + let mut cursor = n.walk(); + for ch in n.children(&mut cursor) { + match ch.kind() { + "variable_list" | "variable_declaration" => { + let mut c2 = ch.walk(); + for g in ch.children(&mut c2) { + if matches!(g.kind(), "identifier" | "variable") { + left_names.push(first_name(&g).and_then(|nn| text(&nn, src))); + } + } + } + "expression_list" => { + let mut c2 = ch.walk(); + for g in ch.children(&mut c2) { + if g.kind() == "function_call" { + let callee = + g.child_by_field_name("name").and_then(|nn| text(&nn, src)); + if callee.as_deref() == Some("require") { + right_calls.push(lua_require_path(&g, src)); + } else { + right_calls.push(None); + } + } + } + } + _ => {} + } + } + // Ghép theo vị trí: local mod = require('mod'). + for (i, path) in right_calls.into_iter().enumerate() { + if let (Some(path), Some(alias)) = (path, left_names.get(i).cloned().flatten()) { + imports.push(ImportAlias { alias, path }); + } + } + } + let mut cursor = n.walk(); + for ch in n.children(&mut cursor) { + stack.push(ch); + } + } + if imports.is_empty() { + None + } else { + Some(FileDeps { + package: None, + imports, + }) + } +} + +/// Tên (identifier/variable/dot) đầu tiên của node. +fn first_name<'a>(node: &Node<'a>) -> Option> { + if matches!(node.kind(), "identifier" | "variable" | "name") { + return Some(*node); + } + let mut cursor = node.walk(); + let mut found = None; + for ch in node.children(&mut cursor) { + if let Some(f) = first_name(&ch) { + found = Some(f); + break; + } + } + found +} + +/// String literal đầu trong arguments của `require('...')`. +fn lua_require_path<'a>(call: &Node<'a>, src: &'a [u8]) -> Option { + let args = call.child_by_field_name("arguments")?; + let mut cursor = args.walk(); + let mut found = None; + for ch in args.children(&mut cursor) { + if ch.kind() == "string" { + if let Some(t) = text(&ch, src) { + found = Some(t.trim_matches(|c| c == '\'' || c == '"').to_string()); + } + break; + } + } + found.filter(|p| !p.is_empty()) +} + +/// Function-value: `handler = funcName` (identifier = identifier). +fn lua_fn_value_assign(node: &Node, src: &[u8]) -> Option<(String, String)> { + if node.kind() != "assignment_statement" { + return None; + } + let mut names: Vec = Vec::new(); + let mut values: Vec = Vec::new(); + let mut cursor = node.walk(); + for ch in node.children(&mut cursor) { + match ch.kind() { + "variable_list" => { + let mut c2 = ch.walk(); + for g in ch.children(&mut c2) { + if let Some(nn) = first_name(&g) { + if let Some(t) = text(&nn, src) { + names.push(t); + } + } + } + } + "expression_list" => { + let mut c2 = ch.walk(); + for g in ch.children(&mut c2) { + if g.kind() == "identifier" { + if let Some(t) = text(&g, src) { + values.push(t); + } + } + } + } + _ => {} + } + } + if names.len() == 1 && values.len() == 1 { + Some((names.remove(0), values.remove(0))) + } else { + None + } +} diff --git a/crates/codegraph-extract/src/languages/php.rs b/crates/codegraph-extract/src/languages/php.rs index 2ca552db43..91d7e59050 100644 --- a/crates/codegraph-extract/src/languages/php.rs +++ b/crates/codegraph-extract/src/languages/php.rs @@ -1,5 +1,6 @@ use crate::languages::common::{text, CallRule, HookSpec, LangSpec, MarkerSpec}; use codegraph_core::SymbolKind; +use codegraph_graph::{FileDeps, ImportAlias}; use tree_sitter::Node; fn ts_language() -> tree_sitter::Language { @@ -128,6 +129,9 @@ pub static SPEC: LangSpec = LangSpec { hooks: HookSpec { anonymous_name_fn: Some(anonymous_name_node), value_func_kinds: &["anonymous_function", "arrow_function"], + impl_trait_fn: Some(php_supers), + sig_key_fn: Some(php_signature_key), + file_deps_fn: Some(php_use_deps), ..HookSpec::none() }, markers: MarkerSpec { @@ -154,3 +158,115 @@ pub static SPEC: LangSpec = LangSpec { ..LangSpec::dflt() }; crate::lang_parser!(PhpParser, SPEC); + +/// Supers của class/trait/interface: `class C extends B implements I, J` — +/// base_clause text chứa cả names (extends + implements gộp). Không có +/// base_clause → None. +fn php_supers(node: &Node, src: &[u8]) -> Option { + if !matches!( + node.kind(), + "class_declaration" | "interface_declaration" | "enum_declaration" + ) { + return None; + } + let mut names: Vec = Vec::new(); + // extends: field base_clause — text "extends B" (kw là unnamed). + let clause = node + .children(&mut node.walk()) + .find(|c| c.kind() == "base_clause"); + if let Some(clause) = clause { + if let Some(raw) = text(&clause, src) { + let items: Vec = raw + .replacen("extends", "", 1) + .split(',') + .map(|n| n.trim().to_string()) + .filter(|n| !n.is_empty() && n != "implements") + .collect(); + names.extend(items); + } + } + // implements: field class_interface_clause — text "implements I, J". + let iface_clause = node + .children(&mut node.walk()) + .find(|c| c.kind() == "class_interface_clause"); + if let Some(clause) = iface_clause { + if let Some(raw) = text(&clause, src) { + let items: Vec = raw + .replacen("implements", "", 1) + .split(',') + .map(|n| n.trim().to_string()) + .filter(|n| !n.is_empty() && n != "implements") + .collect(); + names.extend(items); + } + } + names.dedup(); + (!names.is_empty()).then(|| names.join(",")) +} + +/// Signature key `(params)(return)` — PHP có type hints đủ dùng. +fn php_signature_key(node: &Node, src: &[u8]) -> Option { + if !matches!(node.kind(), "method_declaration" | "function_definition") { + return None; + } + let mut pt: Vec = Vec::new(); + if let Some(params) = node.child_by_field_name("parameters") { + let mut cursor = params.walk(); + for ch in params.children(&mut cursor) { + if ch.kind() == "simple_parameter" { + if let Some(t) = ch.child_by_field_name("type") { + if let Some(txt) = text(&t, src) { + pt.push(txt.replace(' ', "")); + } + } + } + } + } + let rt = node + .child_by_field_name("return_type") + .and_then(|t| text(&t, src)) + .map(|t| t.replace(' ', "")) + .unwrap_or_else(|| "void".to_string()); + Some(format!("({})({})", pt.join(","), rt)) +} + +/// `use` statements của file → FileDeps: +/// - `use App\Models\User;` → alias "User", path "App\Models\User" +/// - `use App\Models\User as U;` → alias "U" +/// - `use function x;` / `use const X;` → bỏ (function/const import hiếm). +fn php_use_deps(root: &Node, src: &[u8]) -> Option { + let mut imports: Vec = Vec::new(); + let mut stack = vec![*root]; + while let Some(n) = stack.pop() { + if n.kind() == "namespace_use_declaration" { + let raw = text(&n, src).unwrap_or_default(); + let stmt = raw.trim().trim_end_matches(';').trim(); + let stmt = stmt.strip_prefix("use ").unwrap_or(stmt).trim(); + if stmt.starts_with("function ") || stmt.starts_with("const ") { + continue; + } + let (path, alias) = match stmt.split_once(" as ") { + Some((p, a)) => (p.trim().to_string(), a.trim().to_string()), + None => { + let seg = stmt.rsplit('\\').next().unwrap_or("").to_string(); + (stmt.to_string(), seg) + } + }; + if !alias.is_empty() && !path.is_empty() { + imports.push(ImportAlias { alias, path }); + } + } + let mut cursor = n.walk(); + for ch in n.children(&mut cursor) { + stack.push(ch); + } + } + if imports.is_empty() { + None + } else { + Some(FileDeps { + package: None, + imports, + }) + } +} diff --git a/crates/codegraph-extract/src/languages/ruby.rs b/crates/codegraph-extract/src/languages/ruby.rs index 6fe9cbd865..932f1ae1e0 100644 --- a/crates/codegraph-extract/src/languages/ruby.rs +++ b/crates/codegraph-extract/src/languages/ruby.rs @@ -1,5 +1,6 @@ use crate::languages::common::{text, CallRule, HookSpec, LangSpec, MarkerSpec}; use codegraph_core::SymbolKind; +use codegraph_graph::{FileDeps, ImportAlias}; use tree_sitter::Node; fn ts_language() -> tree_sitter::Language { @@ -39,6 +40,7 @@ pub static SPEC: LangSpec = LangSpec { ], func_kinds: &["method", "singleton_method"], class_kinds: &["class", "module"], + param_container_kinds: &["method_parameters"], calls: &[ CallRule { kind: "call", @@ -57,6 +59,8 @@ pub static SPEC: LangSpec = LangSpec { ], hooks: HookSpec { class_type_name: Some(class_type_name), + impl_trait_fn: Some(ruby_superclass), + file_deps_fn: Some(ruby_require_deps), ..HookSpec::none() }, markers: MarkerSpec { @@ -73,4 +77,81 @@ pub static SPEC: LangSpec = LangSpec { }, ..LangSpec::dflt() }; +/// String literal đầu tiên trong argument list của `require` — helper tách +/// riêng để tránh borrow scope của cursor. +fn ruby_require_path<'a>(args: &Node<'a>, src: &'a [u8]) -> Option { + let mut cursor = args.walk(); + let mut found = None; + for ch in args.children(&mut cursor) { + if ch.kind() == "string" { + if let Some(t) = text(&ch, src) { + found = Some( + t.trim_matches(|c| c == '\'' || c == '"') + .trim_start_matches("./") + .to_string(), + ); + } + break; + } + } + found +} + crate::lang_parser!(RubyParser, SPEC); + +/// Superclass `class Foo < Bar` → "Bar" — inheritance fan-out (giống type_name +/// nhưng đi vào impl_index; `Object`/`Struct::new`-style bỏ qua). +fn ruby_superclass(node: &Node, src: &[u8]) -> Option { + if node.kind() != "class" { + return None; + } + let sc = node.child_by_field_name("superclass")?; + let raw = text(&sc, src)?; + let name = raw.trim_start_matches('<').trim(); + // Loại dynamic superclass (`< Struct.new(:x)` / `< Base.new`). + if name.is_empty() || name.contains('(') || name.contains(".new") { + return None; + } + Some(name.to_string()) +} + +/// `require 'x'` / `require_relative 'x'` → FileDeps (alias = segment cuối). +fn ruby_require_deps(root: &Node, src: &[u8]) -> Option { + let mut imports: Vec = Vec::new(); + let mut stack = vec![*root]; + while let Some(n) = stack.pop() { + let is_require = n.kind() == "call" + && n.child_by_field_name("method") + .and_then(|m| text(&m, src)) + .is_some_and(|mtxt| mtxt == "require" || mtxt == "require_relative"); + if is_require { + // argument đầu là string literal. + if n.kind() == "call" { + if let Some(args) = n.child_by_field_name("arguments") { + if let Some(path) = ruby_require_path(&args, src) { + if let Some(seg) = path.rsplit('/').next() { + if !seg.is_empty() { + imports.push(ImportAlias { + alias: seg.to_string(), + path, + }); + } + } + } + } + } + } + let mut cursor = n.walk(); + for ch in n.children(&mut cursor) { + stack.push(ch); + } + } + if imports.is_empty() { + None + } else { + Some(FileDeps { + package: None, + imports, + }) + } +} diff --git a/crates/codegraph-extract/src/languages/scala.rs b/crates/codegraph-extract/src/languages/scala.rs index 11b4634495..f650723c30 100644 --- a/crates/codegraph-extract/src/languages/scala.rs +++ b/crates/codegraph-extract/src/languages/scala.rs @@ -1,5 +1,7 @@ -use crate::languages::common::{CallRule, HookSpec, LangSpec, MarkerSpec}; +use crate::languages::common::{named_children, text, CallRule, HookSpec, LangSpec, MarkerSpec}; use codegraph_core::SymbolKind; +use codegraph_graph::{FileDeps, ImportAlias}; +use tree_sitter::Node; fn ts_language() -> tree_sitter::Language { tree_sitter_scala::LANGUAGE.into() @@ -35,7 +37,12 @@ pub static SPEC: LangSpec = LangSpec { name_fn: None, target_fn: None, }], - hooks: HookSpec::none(), + hooks: HookSpec { + impl_trait_fn: Some(scala_supers), + sig_key_fn: Some(scala_signature_key), + file_deps_fn: Some(scala_file_deps), + ..HookSpec::none() + }, markers: MarkerSpec { if_kinds: &["if_expression"], loop_kinds: &["for_expression", "while_expression", "do_while_expression"], @@ -52,3 +59,120 @@ pub static SPEC: LangSpec = LangSpec { ..LangSpec::dflt() }; crate::lang_parser!(ScalaParser, SPEC); + +/// Supers: `class C extends B with T1 with T2` → "B,T1,T2" (extends_clause +/// chứa type list). `extends App` object cũng ăn. +fn scala_supers(node: &Node, src: &[u8]) -> Option { + if !matches!( + node.kind(), + "class_definition" | "trait_definition" | "object_definition" | "enum_definition" + ) { + return None; + } + let clause = node.child_by_field_name("extend")?; + let mut names: Vec = Vec::new(); + let mut stack = vec![clause]; + while let Some(n) = stack.pop() { + if matches!(n.kind(), "type_identifier" | "generic_type" | "call_site") { + // call_site: `extends App(...)` — tên ở con đầu. + let t = text(&n, src).map(|s| s.split('(').next().unwrap_or("").trim().to_string()); + if let Some(t) = t { + if !t.is_empty() { + names.push(t); + } + } + if n.kind() != "generic_type" { + continue; // generic_type cần đi tiếp con để lấy base + } + } + let mut cursor = n.walk(); + for ch in n.children(&mut cursor) { + stack.push(ch); + } + } + names.dedup(); + (!names.is_empty()).then(|| names.join(",")) +} + +/// Signature key `(params)(return)` — def có type hints. +fn scala_signature_key(node: &Node, src: &[u8]) -> Option { + if !matches!(node.kind(), "function_definition" | "function_declaration") { + return None; + } + let mut pt: Vec = Vec::new(); + if let Some(params) = node.child_by_field_name("parameters") { + for ch in named_children(¶ms) { + if let Some(t) = ch.child_by_field_name("type") { + if let Some(txt) = text(&t, src) { + pt.push(txt.replace(' ', "")); + } + } + } + } + let rt = node + .child_by_field_name("return_type") + .or_else(|| node.child_by_field_name("type")) + .and_then(|t| text(&t, src)) + .map(|t| t.replace(' ', "")) + .unwrap_or_default(); + Some(format!("({})({})", pt.join(","), rt)) +} + +/// `import a.b.c` / `import a.{x, y}` → FileDeps: +/// - importer đơn → alias "c", path "a.b.c" +/// - `import a.{x as z, y}` → alias "z"/"y", path "a.x"/"a.y" +fn scala_file_deps(root: &Node, src: &[u8]) -> Option { + let mut imports: Vec = Vec::new(); + let mut stack = vec![*root]; + while let Some(n) = stack.pop() { + if n.kind() == "import_declaration" && n.child_count() > 1 { + // selectors. Lấy toàn bộ path text trước, xử lý selector `{...}`. + let raw = text(&n, src).unwrap_or_default(); + let stmt = raw.trim().trim_end_matches(';').trim(); + let stmt = stmt.strip_prefix("import ").unwrap_or(stmt).trim(); + if let Some((base, sel)) = stmt.split_once(".{") { + let base = base.trim().to_string(); + let sel = sel.trim_end_matches('}').trim(); + for item in sel.split(',') { + let item = item.trim(); + if item.is_empty() { + continue; + } + let (name, alias) = match item.split_once("=>") { + Some((n2, a)) => (n2.trim().to_string(), a.trim().to_string()), + None => ( + item.to_string(), + item.rsplit('.').next().unwrap_or(item).to_string(), + ), + }; + if alias != "_" && !name.is_empty() { + imports.push(ImportAlias { + alias, + path: format!("{base}.{name}"), + }); + } + } + } else { + let seg = stmt.rsplit('.').next().unwrap_or("").to_string(); + if !seg.is_empty() && seg != "_" { + imports.push(ImportAlias { + alias: seg, + path: stmt.to_string(), + }); + } + } + } + let mut cursor = n.walk(); + for ch in n.children(&mut cursor) { + stack.push(ch); + } + } + if imports.is_empty() { + None + } else { + Some(FileDeps { + package: None, + imports, + }) + } +} diff --git a/crates/codegraph-extract/tests/dynamic_dispatch.rs b/crates/codegraph-extract/tests/dynamic_dispatch.rs index 55f94d1979..a960ef6d2c 100644 --- a/crates/codegraph-extract/tests/dynamic_dispatch.rs +++ b/crates/codegraph-extract/tests/dynamic_dispatch.rs @@ -7,7 +7,11 @@ use codegraph_extract::languages::c::CParser; use codegraph_extract::languages::cpp::CppParser; use codegraph_extract::languages::go::GoParser; use codegraph_extract::languages::java::JavaParser; +use codegraph_extract::languages::lua::LuaParser; +use codegraph_extract::languages::php::PhpParser; use codegraph_extract::languages::python::PythonParser; +use codegraph_extract::languages::ruby::RubyParser; +use codegraph_extract::languages::scala::ScalaParser; use codegraph_extract::LangParser; #[test] @@ -759,3 +763,192 @@ function run(db: Database, limit?: number): void { .iter() .any(|s| s.name == "handle" && s.kind == SymbolKind::Method && s.scope_id == iface.id)); } + +#[test] +fn php_supers_sig_use_deps() { + let src = r#" + = deps + .imports + .iter() + .map(|i| (i.alias.as_str(), i.path.as_str())) + .collect(); + pairs.sort(); + assert_eq!( + pairs, + vec![("O", "App\\Models\\Order"), ("User", "App\\Models\\User")] + ); +} + +#[test] +fn ruby_params_superclass_require() { + let src = r#" +require 'net/http' +require_relative 'orders/helper' + +class FileRepo < BaseRepo + def save(record, force: false) + persist(record) + end + + def fetch(*ids) + end +end +"#; + let parsed = RubyParser.parse_file("r.rb", src).unwrap(); + + // Params: save(record, force: false) + fetch(*ids). + for name in ["record", "force", "ids"] { + assert!( + parsed + .symbols + .iter() + .any(|s| s.kind == SymbolKind::Parameter && s.name == name), + "param {name} missing" + ); + } + + // Superclass → impl_trait (fan-out). + let cls = parsed + .symbols + .iter() + .find(|s| s.name == "FileRepo") + .unwrap(); + assert_eq!(cls.impl_trait.as_deref(), Some("BaseRepo")); + + // require deps. + let deps = parsed.file_deps.expect("require deps missing"); + let mut pairs: Vec<(&str, &str)> = deps + .imports + .iter() + .map(|i| (i.alias.as_str(), i.path.as_str())) + .collect(); + pairs.sort(); + assert_eq!( + pairs, + vec![("helper", "orders/helper"), ("http", "net/http")] + ); +} + +#[test] +fn lua_params_require_fn_value() { + let src = r#" +local json = require('dkjson') +local utils = require('utils') + +local M = {} + +function M.run(db, opts) + handler = process + handler(db) + return db +end + +return M +"#; + let parsed = LuaParser.parse_file("m.lua", src).unwrap(); + + // Params. + for name in ["db", "opts"] { + assert!( + parsed + .symbols + .iter() + .any(|s| s.kind == SymbolKind::Parameter && s.name == name), + "param {name} missing" + ); + } + + // require deps. + let deps = parsed.file_deps.expect("require deps missing"); + let mut pairs: Vec<(&str, &str)> = deps + .imports + .iter() + .map(|i| (i.alias.as_str(), i.path.as_str())) + .collect(); + pairs.sort(); + assert_eq!(pairs, vec![("json", "dkjson"), ("utils", "utils")]); + + // Fn-value. + assert!(parsed + .fn_ptr_assigns + .iter() + .any(|a| a.var == "handler" && a.target == "process")); +} + +#[test] +fn scala_supers_sig_imports() { + let src = r#" +package orders + +import scala.collection.mutable +import orders.repo.{BaseRepo, UserRepo => UR} + +class CardRepo extends BaseRepo with Logging { + def pay(amount: Int, currency: String): Unit = {} +} +"#; + let parsed = ScalaParser.parse_file("S.scala", src).unwrap(); + + // Supers: extends B with T. + let cls = parsed + .symbols + .iter() + .find(|s| s.name == "CardRepo") + .unwrap(); + let supers = cls.impl_trait.as_deref().expect("supers missing"); + eprintln!("SCALADBG supers={supers}"); + assert!(supers.contains("BaseRepo"), "supers: {supers}"); + assert!(supers.contains("Logging"), "supers: {supers}"); + + // Sig key. + let pay = parsed.symbols.iter().find(|s| s.name == "pay").unwrap(); + assert_eq!(pay.sig_key.as_deref(), Some("(Int,String)(Unit)")); + + // Imports: đơn + selector + alias. + let deps = parsed.file_deps.expect("imports missing"); + let mut pairs: Vec<(&str, &str)> = deps + .imports + .iter() + .map(|i| (i.alias.as_str(), i.path.as_str())) + .collect(); + pairs.sort(); + assert_eq!( + pairs, + vec![ + ("BaseRepo", "orders.repo.BaseRepo"), + ("UR", "orders.repo.UserRepo"), + ("mutable", "scala.collection.mutable"), + ] + ); +} From 20dc99761390daa3b53f3d4569a6f5f5c92d3844 Mon Sep 17 00:00:00 2001 From: Hung Pham Date: Sun, 25 Oct 2026 19:08:04 +0700 Subject: [PATCH 11/19] =?UTF-8?q?Swift/C#=20semantic=20enrichment=20?= =?UTF-8?q?=E2=80=94=20=C4=91=E1=BB=A7=2014/14=20ng=C3=B4n=20ng=E1=BB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Swift (trước đây gần trống — không hooks): - decls += protocol_function_declaration: method KHAI BÁO trong protocol scope vào Interface → fan-out (đúng bug Rust từng mắc). - swift_supers: inheritance_specifier (`class FileRepo: BaseRepo, Repo`) → impl_trait — protocol conformance đi cùng cơ chế fan-out. - swift_signature_key: Swift dùng TRÙNG tên field "name" cho tên param VÀ type (lấy field "name" cuối); match cả protocol_function_declaration; return = user_type sau token "->". - swift_import_deps: import_declaration → FileDeps. C# (typing/annotations sẵn tốt): - csharp_bases: base_list (node kind, unnamed child) → impl_trait (base class + interfaces, strip generic). - csharp_signature_key: return field là "returns" (không phải "type"). - csharp_using_deps: `using App.Repos` + alias form `using Db = App.Data`. Tests: 2 fixture mới (swift protocol/supers/sig/imports; csharp bases/sig/ using). 25/25 dynamic_dispatch, 20/21 workspace (Redis env-only). --- .../codegraph-extract/src/languages/csharp.rs | 104 +++++++++++++- .../codegraph-extract/src/languages/swift.rs | 131 +++++++++++++++++- 2 files changed, 231 insertions(+), 4 deletions(-) diff --git a/crates/codegraph-extract/src/languages/csharp.rs b/crates/codegraph-extract/src/languages/csharp.rs index 9e4511f451..24a1507844 100644 --- a/crates/codegraph-extract/src/languages/csharp.rs +++ b/crates/codegraph-extract/src/languages/csharp.rs @@ -1,5 +1,6 @@ -use crate::languages::common::{text, CallRule, HookSpec, LangSpec, MarkerSpec}; +use crate::languages::common::{named_children, text, CallRule, HookSpec, LangSpec, MarkerSpec}; use codegraph_core::SymbolKind; +use codegraph_graph::{FileDeps, ImportAlias}; use tree_sitter::Node; fn ts_language() -> tree_sitter::Language { @@ -58,7 +59,12 @@ pub static SPEC: LangSpec = LangSpec { target_fn: None, }, ], - hooks: HookSpec::none(), + hooks: HookSpec { + impl_trait_fn: Some(csharp_bases), + sig_key_fn: Some(csharp_signature_key), + file_deps_fn: Some(csharp_using_deps), + ..HookSpec::none() + }, markers: MarkerSpec { if_kinds: &["if_statement"], loop_kinds: &[ @@ -81,6 +87,100 @@ pub static SPEC: LangSpec = LangSpec { }, ..LangSpec::dflt() }; +/// Bases: `class FileRepo : BaseRepo, IRepo` — base_list (node kind, unnamed +/// child) chứa base_type con. Base class + interfaces → fan-out. +fn csharp_bases(node: &Node, src: &[u8]) -> Option { + if !matches!( + node.kind(), + "class_declaration" | "record_declaration" | "struct_declaration" + ) { + return None; + } + let base_list = node + .children(&mut node.walk()) + .find(|c| c.kind() == "base_list")?; + let mut names: Vec = Vec::new(); + for ch in named_children(&base_list) { + if let Some(t) = text(&ch, src) { + let base = t.split('<').next().unwrap_or("").trim().to_string(); + if !base.is_empty() { + names.push(base); + } + } + } + names.dedup(); + (!names.is_empty()).then(|| names.join(",")) +} + +/// Signature key `(params)(return)` — method/constructor có type hints. +fn csharp_signature_key(node: &Node, src: &[u8]) -> Option { + if !matches!( + node.kind(), + "method_declaration" | "constructor_declaration" + ) { + return None; + } + let mut pt: Vec = Vec::new(); + if let Some(params) = node.child_by_field_name("parameters") { + for ch in named_children(¶ms) { + if let Some(t) = ch.child_by_field_name("type") { + if let Some(txt) = text(&t, src) { + pt.push(txt.replace(' ', "")); + } + } + } + } + // C# return field là "returns" (không phải "type" như đa số grammar). + let rt = node + .child_by_field_name("returns") + .and_then(|t| text(&t, src)) + .map(|t| t.replace(' ', "")) + .unwrap_or_else(|| "void".to_string()); + Some(format!("({})({})", pt.join(","), rt)) +} + +/// `using` directives → FileDeps: +/// - `using App.Repos;` → alias "Repos", path "App.Repos" +/// - `using Db = App.Data;` → alias "Db", path "App.Data" +fn csharp_using_deps(root: &Node, src: &[u8]) -> Option { + let mut imports: Vec = Vec::new(); + let mut stack = vec![*root]; + while let Some(n) = stack.pop() { + if n.kind() == "using_directive" { + let mut alias: Option = None; + let mut path: Option = None; + let mut cursor = n.walk(); + for ch in n.children(&mut cursor) { + match ch.kind() { + "identifier" => alias = Some(text(&ch, src).unwrap_or_default()), + "qualified_name" => path = text(&ch, src), + _ => {} + } + } + drop(cursor); + if let Some(path) = path { + let alias = + alias.unwrap_or_else(|| path.rsplit('.').next().unwrap_or("").to_string()); + if !alias.is_empty() { + imports.push(ImportAlias { alias, path }); + } + } + } + let mut cursor = n.walk(); + for ch in n.children(&mut cursor) { + stack.push(ch); + } + } + if imports.is_empty() { + None + } else { + Some(FileDeps { + package: None, + imports, + }) + } +} + crate::lang_parser!(CSharpParser, SPEC); #[cfg(test)] diff --git a/crates/codegraph-extract/src/languages/swift.rs b/crates/codegraph-extract/src/languages/swift.rs index 63e46b44bd..c7bedc3266 100644 --- a/crates/codegraph-extract/src/languages/swift.rs +++ b/crates/codegraph-extract/src/languages/swift.rs @@ -1,5 +1,7 @@ -use crate::languages::common::{CallRule, HookSpec, LangSpec, MarkerSpec}; +use crate::languages::common::{text, CallRule, HookSpec, LangSpec, MarkerSpec}; use codegraph_core::SymbolKind; +use codegraph_graph::{FileDeps, ImportAlias}; +use tree_sitter::Node; fn ts_language() -> tree_sitter::Language { tree_sitter_swift::LANGUAGE.into() @@ -20,6 +22,9 @@ pub static SPEC: LangSpec = LangSpec { ("property_declaration", SymbolKind::Field), ("variable_declaration", SymbolKind::Variable), ("parameter", SymbolKind::Parameter), + // Method KHAI BÁO trong protocol (như Rust function_signature_item) — + // scope vào protocol Interface → trigger fan-out. + ("protocol_function_declaration", SymbolKind::Method), ], func_kinds: &[ "function_declaration", @@ -41,7 +46,12 @@ pub static SPEC: LangSpec = LangSpec { name_fn: None, target_fn: None, }], - hooks: HookSpec::none(), + hooks: HookSpec { + impl_trait_fn: Some(swift_supers), + sig_key_fn: Some(swift_signature_key), + file_deps_fn: Some(swift_import_deps), + ..HookSpec::none() + }, markers: MarkerSpec { if_kinds: &["if_statement"], if_block_kinds: &["statements"], @@ -57,3 +67,120 @@ pub static SPEC: LangSpec = LangSpec { ..LangSpec::dflt() }; crate::lang_parser!(SwiftParser, SPEC); + +/// Supers: `class FileRepo: BaseRepo, Repo` — inheritance_specifier con trực +/// tiếp (field inherits_from → user_type). Protocol conformance đi cùng cơ +/// chế → fan-out cho protocol dispatch. +fn swift_supers(node: &Node, src: &[u8]) -> Option { + if !matches!( + node.kind(), + "class_declaration" | "struct_declaration" | "enum_declaration" + ) { + return None; + } + let mut names: Vec = Vec::new(); + let mut cursor = node.walk(); + for ch in node.children(&mut cursor) { + if ch.kind() != "inheritance_specifier" { + continue; + } + // inherits_from → user_type → type_identifier đầu. + if let Some(t) = text(&ch, src) { + let base = t.split('<').next().unwrap_or("").trim().to_string(); + if !base.is_empty() { + names.push(base); + } + } + } + names.dedup(); + (!names.is_empty()).then(|| names.join(",")) +} + +/// Signature key `(params)(return)`. +/// +/// Swift grammar quái: parameter có HAI field cùng tên "name" (thứ nhất = +/// tên param simple_identifier, thứ hai = type user_type); return type cũng +/// là field "name" sau token `->`. Lấy field "name" CUỐI của từng node. +fn swift_signature_key(node: &Node, src: &[u8]) -> Option { + if !matches!( + node.kind(), + "function_declaration" | "protocol_function_declaration" + ) { + return None; + } + let mut pt: Vec = Vec::new(); + let mut ret: Option = None; + let mut cursor = node.walk(); + let mut after_arrow = false; + for ch in node.children(&mut cursor) { + if ch.kind() == "->" { + after_arrow = true; + continue; + } + if ch.kind() == "parameter" { + if let Some(t) = swift_last_name_field(&ch, src) { + pt.push(t.replace(' ', "")); + } + } else if after_arrow && ch.kind() == "user_type" { + ret = Some(text(&ch, src)?.replace(' ', "")); + } + } + Some(format!( + "({})({})", + pt.join(","), + ret.unwrap_or_else(|| "void".to_string()) + )) +} + +/// Field "name" CUỐI của node (Swift dùng trùng tên field cho tên param + type). +fn swift_last_name_field<'a>(node: &Node<'a>, src: &'a [u8]) -> Option { + let mut cursor = node.walk(); + let mut found: Option = None; + for (i, ch) in node.children(&mut cursor).enumerate() { + if node.field_name_for_child(i as u32) == Some("name") { + if let Some(t) = text(&ch, src) { + found = Some(t); + } + } + } + found +} + +/// `import Foundation` → FileDeps (alias = segment cuối; Swift single-module +/// nên import chủ yếu external — stdlib ngoài index → giữ raw). +fn swift_import_deps(root: &Node, src: &[u8]) -> Option { + let mut imports: Vec = Vec::new(); + let mut stack = vec![*root]; + while let Some(n) = stack.pop() { + if n.kind() == "import_declaration" { + // identifier cuối của path. + let mut cursor = n.walk(); + let mut last: Option = None; + for ch in n.children(&mut cursor) { + if ch.kind() == "identifier" { + if let Some(t) = text(&ch, src) { + last = Some(t); + } + } + } + if let Some(path) = last { + let alias = path.rsplit('.').next().unwrap_or("").to_string(); + if !alias.is_empty() { + imports.push(ImportAlias { alias, path }); + } + } + } + let mut cursor = n.walk(); + for ch in n.children(&mut cursor) { + stack.push(ch); + } + } + if imports.is_empty() { + None + } else { + Some(FileDeps { + package: None, + imports, + }) + } +} From 74d3053cf5ec177bfd09ab82f6f5f9325ce9f48e Mon Sep 17 00:00:00 2001 From: Hung Pham Date: Sun, 25 Oct 2026 19:43:05 +0700 Subject: [PATCH 12/19] FunctionValue + member assignment + MARKER_AWAIT (priority 1/3/6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A. FunctionValue cho 4 ngôn ngữ còn thiếu: - Python: `handler = process` (identifier=identifier) + `self.handler = process` (member, var = "self.handler"). - Ruby: CHỈ `handler = method(:process)` — plain `x = process` là LỜI GỌI trong Ruby, không track. - Java: `Runnable r = this::run` — method_reference, target = identifier cuối (không phụ thuộc field name grammar). - C#: `Action handler = Process` — value là identifier UNNAMED (không có field "value") — lấy identifier con cuối khác name. B. Member fn assignment `obj.handler = fn`: - Mở guard fn_ptr_index (bỏ `!contains('.')`) — lookup theo call_name bare lẫn dotted. - js/ts: member_expression left → key full dotted text (`Foo.prototype.run`). - lua: text chứa '.' → full text; bracket `handlers["read"]` skip. - python: self.attr. C. MARKER_AWAIT (Marker::Await = 27): MarkerSpec.await_kinds — python "await", js/ts/csharp/swift "await_expression" (dump AST xác nhận); walk_chain push AWAIT trước call như GO/DEFER → edge mang ngữ cảnh async boundary. E2E: member_fn_assignment_resolves_dotted_call — `obj.handler = process` + `obj.handler()` → PointerAssign (trước đây guard chặn). 25/25 dynamic_dispatch (2 test mới), 20/21 workspace (Redis env-only). --- crates/codegraph-core/src/lib.rs | 10 +- crates/codegraph-core/src/semgraph.rs | 11 +- .../codegraph-extract/src/languages/common.rs | 11 ++ .../codegraph-extract/src/languages/csharp.rs | 26 ++++ .../codegraph-extract/src/languages/java.rs | 27 +++++ .../src/languages/javascript.rs | 12 +- crates/codegraph-extract/src/languages/lua.rs | 15 ++- .../codegraph-extract/src/languages/python.rs | 34 ++++++ .../codegraph-extract/src/languages/ruby.rs | 32 +++++ .../codegraph-extract/src/languages/swift.rs | 1 + .../src/languages/typescript.rs | 1 + crates/codegraph-extract/tests/chains.rs | 1 + .../tests/dynamic_dispatch.rs | 112 ++++++++++++++++++ crates/codegraph-graph/src/lib.rs | 52 +++++++- 14 files changed, 328 insertions(+), 17 deletions(-) diff --git a/crates/codegraph-core/src/lib.rs b/crates/codegraph-core/src/lib.rs index 30482aee8c..1feabca0c1 100644 --- a/crates/codegraph-core/src/lib.rs +++ b/crates/codegraph-core/src/lib.rs @@ -16,9 +16,9 @@ pub use semgraph::{ EffectCallPattern, EffectRule, EffectType, FileInfo, FlowCall, FlowResult, FlowStep, FullFlowPath, FunctionScope, Marker, MemberInfo, PathEnding, ResolutionKind, ResolveResult, ScopeLevel, SearchFlowResult, Symbol, SymbolId, SymbolKind, SymbolMatch, TraceAction, - MARKER_BRANCH_END, MARKER_BREAK, MARKER_CALL_ENTER, MARKER_CALL_EXIT, MARKER_CATCH_START, - MARKER_CONTINUE, MARKER_DEFER, MARKER_GO, 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, MARKER_TRY_END, - MARKER_TRY_START, SYMBOL_BASE, + MARKER_AWAIT, MARKER_BRANCH_END, MARKER_BREAK, MARKER_CALL_ENTER, MARKER_CALL_EXIT, + MARKER_CATCH_START, MARKER_CONTINUE, MARKER_DEFER, MARKER_GO, 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, + MARKER_TRY_END, MARKER_TRY_START, SYMBOL_BASE, }; diff --git a/crates/codegraph-core/src/semgraph.rs b/crates/codegraph-core/src/semgraph.rs index 943be70158..96279bf474 100644 --- a/crates/codegraph-core/src/semgraph.rs +++ b/crates/codegraph-core/src/semgraph.rs @@ -46,11 +46,13 @@ pub enum Marker { Go = 25, /// Go `defer f()` — call hoãn đến khi return. Defer = 26, + /// Call trong ngữ cảnh await (`await f()`, `await expr`) — async boundary. + Await = 27, } impl Marker { /// All variant values (các hằng MARKER_*). - pub const ALL: [Marker; 22] = [ + pub const ALL: [Marker; 23] = [ Marker::Loop, Marker::RecCall, Marker::IfTrue, @@ -73,6 +75,7 @@ impl Marker { Marker::CatchStart, Marker::Go, Marker::Defer, + Marker::Await, ]; /// Giá trị u64 tương ứng (cho tương thích với chain cũ). @@ -107,6 +110,7 @@ impl Marker { 24 => Some(Marker::CatchStart), 25 => Some(Marker::Go), 26 => Some(Marker::Defer), + 27 => Some(Marker::Await), _ => None, } } @@ -136,6 +140,7 @@ impl Marker { Marker::CatchStart => "CATCH_START", Marker::Go => "GO", Marker::Defer => "DEFER", + Marker::Await => "AWAIT", } } @@ -165,6 +170,7 @@ impl Marker { "catch_start" => Some(Marker::CatchStart), "go" => Some(Marker::Go), "defer" => Some(Marker::Defer), + "await" => Some(Marker::Await), _ => None, } } @@ -204,6 +210,7 @@ pub const MARKER_TRY_END: u64 = 23; pub const MARKER_CATCH_START: u64 = 24; pub const MARKER_GO: u64 = 25; pub const MARKER_DEFER: u64 = 26; +pub const MARKER_AWAIT: u64 = 27; /// Helper để đọc chain sang danh sách marker — tiện cho code đọc. #[allow(dead_code)] @@ -720,7 +727,7 @@ impl From for TraceAction { Marker::Loop | Marker::LoopBack => Self::Loop, Marker::Break | Marker::Continue | Marker::StmtEnd => Self::Statement, Marker::TryStart | Marker::TryEnd | Marker::CatchStart => Self::Statement, - Marker::Go | Marker::Defer => Self::Statement, + Marker::Go | Marker::Defer | Marker::Await => Self::Statement, } } } diff --git a/crates/codegraph-extract/src/languages/common.rs b/crates/codegraph-extract/src/languages/common.rs index 1419de5f22..7f4c0d5df1 100644 --- a/crates/codegraph-extract/src/languages/common.rs +++ b/crates/codegraph-extract/src/languages/common.rs @@ -182,6 +182,9 @@ pub struct MarkerSpec { pub go_stmt_kinds: &'static [&'static str], /// Node kinds defer call (`defer_statement`) — marker DEFER trong chain. pub defer_kinds: &'static [&'static str], + /// Node kinds await (`await`, `await_expression`) — marker AWAIT trong + /// chain (async boundary: call bên trong mang ngữ cảnh await). + pub await_kinds: &'static [&'static str], pub if_kinds: &'static [&'static str], pub elif_kinds: &'static [&'static str], /// Block-like node kinds — fallback consequence/alternative khi thiếu field @@ -213,6 +216,7 @@ impl MarkerSpec { Self { go_stmt_kinds: &[], defer_kinds: &[], + await_kinds: &[], if_kinds: &[], elif_kinds: &[], if_block_kinds: &[], @@ -979,6 +983,13 @@ fn walk_chain( } return; } + if ctx.spec.markers.await_kinds.contains(&k) { + chain_push(ctx, codegraph_core::MARKER_AWAIT); + for ch in named_children(node) { + walk_chain(ctx, &ch, depth + 1, in_loop, None); + } + return; + } // 1. Call sites. if let Some(rule) = ctx.spec.calls.iter().find(|r| r.kind == k) { diff --git a/crates/codegraph-extract/src/languages/csharp.rs b/crates/codegraph-extract/src/languages/csharp.rs index 24a1507844..4602336966 100644 --- a/crates/codegraph-extract/src/languages/csharp.rs +++ b/crates/codegraph-extract/src/languages/csharp.rs @@ -63,6 +63,7 @@ pub static SPEC: LangSpec = LangSpec { impl_trait_fn: Some(csharp_bases), sig_key_fn: Some(csharp_signature_key), file_deps_fn: Some(csharp_using_deps), + fn_ptr_assign_fn: Some(csharp_fn_value_assign), ..HookSpec::none() }, markers: MarkerSpec { @@ -83,6 +84,7 @@ pub static SPEC: LangSpec = LangSpec { try_kinds: &["try_statement"], except_kinds: &["catch_clause", "catch_declaration"], finally_kinds: &["finally_clause"], + await_kinds: &["await_expression"], ..MarkerSpec::dflt() }, ..LangSpec::dflt() @@ -142,6 +144,30 @@ fn csharp_signature_key(node: &Node, src: &[u8]) -> Option { /// `using` directives → FileDeps: /// - `using App.Repos;` → alias "Repos", path "App.Repos" /// - `using Db = App.Data;` → alias "Db", path "App.Data" +/// +/// Function-value C#: `Action handler = Process;` — variable_declarator +/// với value identifier thuần. Lambda (`x => ...`) — skip. +fn csharp_fn_value_assign(node: &Node, src: &[u8]) -> Option<(String, String)> { + if node.kind() != "variable_declarator" { + return None; + } + let name = node.child_by_field_name("name")?; + if name.kind() != "identifier" { + return None; + } + // C# grammar: value (Process) là identifier UNNAMED (không có field + // "value") — lấy identifier con CUỐI khác name. + let mut target: Option = None; + let mut cursor = node.walk(); + for ch in node.children(&mut cursor) { + if ch.kind() == "identifier" && ch.id() != name.id() { + target = Some(ch); + } + } + let value = target?; + Some((text(&name, src)?, text(&value, src)?)) +} + fn csharp_using_deps(root: &Node, src: &[u8]) -> Option { let mut imports: Vec = Vec::new(); let mut stack = vec![*root]; diff --git a/crates/codegraph-extract/src/languages/java.rs b/crates/codegraph-extract/src/languages/java.rs index ff72ef3ee1..bd93dc58b5 100644 --- a/crates/codegraph-extract/src/languages/java.rs +++ b/crates/codegraph-extract/src/languages/java.rs @@ -108,6 +108,7 @@ pub static SPEC: LangSpec = LangSpec { impl_trait_fn: Some(java_supers), sig_key_fn: Some(java_signature_key), file_deps_fn: Some(java_file_deps), + fn_ptr_assign_fn: Some(java_fn_value_assign), ..HookSpec::none() }, markers: MarkerSpec { @@ -195,6 +196,32 @@ fn java_signature_key(node: &TsNode, src: &[u8]) -> Option { /// - `import com.foo.Bar;` → alias "Bar", path "com.foo.Bar" /// - `import com.foo.Bar as B;` (hiếm, preview) → alias "B" /// - `import com.foo.*;` / `import static ...;` wildcard → bỏ qua. +/// +/// Function-value Java: `Runnable r = this::run;` — variable_declarator với +/// value là method_reference (`this::run` / `Foo::bar`). Lambda body — skip. +fn java_fn_value_assign(node: &TsNode, src: &[u8]) -> Option<(String, String)> { + if node.kind() != "variable_declarator" { + return None; + } + let name = node.child_by_field_name("name")?; + let value = node.child_by_field_name("value")?; + if value.kind() != "method_reference" { + return None; + } + // method_reference: `this::run` / `Foo::bar` — identifier CUỐI là tên hàm + // (không phụ thuộc field name của grammar). + let mut target = None; + let mut c = value.walk(); + for ch in value.children(&mut c) { + if ch.kind() == "identifier" { + target = Some(ch); + } + } + let target = text(&target?, src)?; + let var = text(&name, src)?; + Some((var, target)) +} + fn java_file_deps(root: &TsNode, src: &[u8]) -> Option { let mut imports: Vec = Vec::new(); let mut stack = vec![*root]; diff --git a/crates/codegraph-extract/src/languages/javascript.rs b/crates/codegraph-extract/src/languages/javascript.rs index d3edfa11e2..aa9586391e 100644 --- a/crates/codegraph-extract/src/languages/javascript.rs +++ b/crates/codegraph-extract/src/languages/javascript.rs @@ -108,6 +108,7 @@ pub static SPEC: LangSpec = LangSpec { try_kinds: &["try_statement"], except_kinds: &["catch_clause"], finally_kinds: &["finally_clause"], + await_kinds: &["await_expression"], ..MarkerSpec::dflt() }, ..LangSpec::dflt() @@ -246,10 +247,17 @@ pub fn js_fn_value_assign(node: &Node, src: &[u8]) -> Option<(String, String)> { "assignment_expression" => { let left = node.child_by_field_name("left")?; let right = node.child_by_field_name("right")?; - if left.kind() != "identifier" || right.kind() != "identifier" { + if right.kind() != "identifier" { return None; } - Some((text(&left, src)?, text(&right, src)?)) + match left.kind() { + // `handler = process`. + "identifier" => Some((text(&left, src)?, text(&right, src)?)), + // `obj.handler = fn` / `Foo.prototype.run = fn` — key dotted + // (call `obj.handler()` khớp qua guard fn_ptr_index mở). + "member_expression" => Some((text(&left, src)?, text(&right, src)?)), + _ => None, + } } "variable_declarator" => { let name = node.child_by_field_name("name")?; diff --git a/crates/codegraph-extract/src/languages/lua.rs b/crates/codegraph-extract/src/languages/lua.rs index 2e6d3cccd6..008879ad53 100644 --- a/crates/codegraph-extract/src/languages/lua.rs +++ b/crates/codegraph-extract/src/languages/lua.rs @@ -176,9 +176,18 @@ fn lua_fn_value_assign(node: &Node, src: &[u8]) -> Option<(String, String)> { "variable_list" => { let mut c2 = ch.walk(); for g in ch.children(&mut c2) { - if let Some(nn) = first_name(&g) { - if let Some(t) = text(&nn, src) { - names.push(t); + // Lua assignment target: kind thật là "variable"/"identifier" + // — text chứa '.' (member fn assignment `obj.handler`) thì + // dùng FULL TEXT; identifier thuần → name; bracket skip. + let t = text(&g, src).unwrap_or_default(); + if t.contains('[') || t.contains('(') { + continue; + } + if t.contains('.') { + names.push(t); + } else if let Some(nn) = first_name(&g) { + if let Some(t2) = text(&nn, src) { + names.push(t2); } } } diff --git a/crates/codegraph-extract/src/languages/python.rs b/crates/codegraph-extract/src/languages/python.rs index 70084b9b90..9b04ab3ed4 100644 --- a/crates/codegraph-extract/src/languages/python.rs +++ b/crates/codegraph-extract/src/languages/python.rs @@ -45,6 +45,7 @@ pub static SPEC: LangSpec = LangSpec { anonymous_name_fn: Some(anonymous_name_node), impl_trait_fn: Some(py_superclasses), file_deps_fn: Some(py_file_deps), + fn_ptr_assign_fn: Some(py_fn_value_assign), ..HookSpec::none() }, markers: MarkerSpec { @@ -62,6 +63,7 @@ pub static SPEC: LangSpec = LangSpec { except_kinds: &["except_clause"], try_else_kinds: &["else_clause"], finally_kinds: &["finally_clause"], + await_kinds: &["await"], ..MarkerSpec::dflt() }, ..LangSpec::dflt() @@ -107,6 +109,38 @@ fn py_superclasses(node: &Node, src: &[u8]) -> Option { /// - `import os.path` → alias "path", path "os.path" /// - `from orders.models import Order, get_order as fetch` /// → alias "Order"/"fetch", path "orders.models.Order"/"orders.models.get_order" +/// +/// Function-value assignment (Python): +/// - `handler = process` — identifier = identifier thuần +/// - `self.handler = process` — attribute object="self" (member fn assignment; +/// call `self.handler()` có call_name "self.handler" khớp key). +/// +/// `x = lambda ...` — target vô danh, skip. +fn py_fn_value_assign(node: &Node, src: &[u8]) -> Option<(String, String)> { + if node.kind() != "assignment" { + return None; + } + let left = node.child_by_field_name("left")?; + let right = node.child_by_field_name("right")?; + let var = match left.kind() { + "identifier" => text(&left, src)?, + // `self.handler = fn` — attribute với object "self". + "attribute" => { + let obj = left.child_by_field_name("object")?; + if text(&obj, src).as_deref() != Some("self") { + return None; + } + text(&left, src)? + } + _ => return None, + }; + if right.kind() != "identifier" { + return None; + } + let target = text(&right, src)?; + Some((var, target)) +} + fn py_file_deps(root: &Node, src: &[u8]) -> Option { let mut imports: Vec = Vec::new(); let mut stack = vec![*root]; diff --git a/crates/codegraph-extract/src/languages/ruby.rs b/crates/codegraph-extract/src/languages/ruby.rs index 932f1ae1e0..3807b64720 100644 --- a/crates/codegraph-extract/src/languages/ruby.rs +++ b/crates/codegraph-extract/src/languages/ruby.rs @@ -61,6 +61,7 @@ pub static SPEC: LangSpec = LangSpec { class_type_name: Some(class_type_name), impl_trait_fn: Some(ruby_superclass), file_deps_fn: Some(ruby_require_deps), + fn_ptr_assign_fn: Some(ruby_fn_value_assign), ..HookSpec::none() }, markers: MarkerSpec { @@ -79,6 +80,37 @@ pub static SPEC: LangSpec = LangSpec { }; /// String literal đầu tiên trong argument list của `require` — helper tách /// riêng để tránh borrow scope của cursor. +/// Function-value assignment Ruby: `handler = method(:process)` — RHS là +/// call `method(...)` với symbol literal. `handler = process` là LỜI GỌI +/// process() trong Ruby — KHÔNG track (khác Go/JS). +fn ruby_fn_value_assign(node: &Node, src: &[u8]) -> Option<(String, String)> { + if node.kind() != "assignment" && node.kind() != "single_assignment" { + return None; + } + let left = node.child_by_field_name("left")?; + if left.kind() != "identifier" { + return None; + } + let right = node.child_by_field_name("right")?; + if right.kind() != "call" { + return None; + } + let m = right.child_by_field_name("method")?; + if text(&m, src).as_deref() != Some("method") { + return None; + } + // Argument: symbol literal `:process` — bỏ ':'. + let args = right.child_by_field_name("arguments")?; + let mut cursor = args.walk(); + let target = args + .children(&mut cursor) + .find(|c| c.kind() == "simple_symbol") + .and_then(|s| text(&s, src)) + .map(|t| t.trim_start_matches(':').to_string())?; + let var = text(&left, src)?; + Some((var, target)) +} + fn ruby_require_path<'a>(args: &Node<'a>, src: &'a [u8]) -> Option { let mut cursor = args.walk(); let mut found = None; diff --git a/crates/codegraph-extract/src/languages/swift.rs b/crates/codegraph-extract/src/languages/swift.rs index c7bedc3266..8b32a2e698 100644 --- a/crates/codegraph-extract/src/languages/swift.rs +++ b/crates/codegraph-extract/src/languages/swift.rs @@ -62,6 +62,7 @@ pub static SPEC: LangSpec = LangSpec { break_kinds: &["break_statement"], continue_kinds: &["continue_statement"], throw_kinds: &["throw_statement"], + await_kinds: &["await_expression"], ..MarkerSpec::dflt() }, ..LangSpec::dflt() diff --git a/crates/codegraph-extract/src/languages/typescript.rs b/crates/codegraph-extract/src/languages/typescript.rs index 5900d704d9..7dfcf95c2d 100644 --- a/crates/codegraph-extract/src/languages/typescript.rs +++ b/crates/codegraph-extract/src/languages/typescript.rs @@ -114,6 +114,7 @@ pub static SPEC: LangSpec = LangSpec { try_kinds: &["try_statement"], except_kinds: &["catch_clause"], finally_kinds: &["finally_clause"], + await_kinds: &["await_expression"], ..MarkerSpec::dflt() }, ..LangSpec::dflt() diff --git a/crates/codegraph-extract/tests/chains.rs b/crates/codegraph-extract/tests/chains.rs index e469f7932a..f612d6be05 100644 --- a/crates/codegraph-extract/tests/chains.rs +++ b/crates/codegraph-extract/tests/chains.rs @@ -425,6 +425,7 @@ class Service { c, [ "[TRY_START]", + "[AWAIT]", "[CALL_ENTER]", "this.repo.find", "[CALL_EXIT]", diff --git a/crates/codegraph-extract/tests/dynamic_dispatch.rs b/crates/codegraph-extract/tests/dynamic_dispatch.rs index a960ef6d2c..a73e4e7ae0 100644 --- a/crates/codegraph-extract/tests/dynamic_dispatch.rs +++ b/crates/codegraph-extract/tests/dynamic_dispatch.rs @@ -952,3 +952,115 @@ class CardRepo extends BaseRepo with Logging { ] ); } + +#[test] +fn fn_value_and_member_assignment_across_languages() { + // ── Python: identifier + self.attr member ── + let py = PythonParser + .parse_file("p.py", "def process(x): pass\n\ndef setup():\n handler = process\n self.handler = process\n other.x = process\n") + .unwrap(); + let py_vars: Vec<(&str, &str)> = py + .fn_ptr_assigns + .iter() + .map(|a| (a.var.as_str(), a.target.as_str())) + .collect(); + let mut py_sorted = py_vars.clone(); + py_sorted.sort(); + assert_eq!( + py_sorted, + vec![("handler", "process"), ("self.handler", "process")] + ); + + // ── Ruby: method(:sym) — plain `x = process` là lời gọi, không track ── + let rb = RubyParser + .parse_file( + "r.rb", + "def setup\n handler = method(:process)\n other = process\nend\n", + ) + .unwrap(); + let rb_vars: Vec<(&str, &str)> = rb + .fn_ptr_assigns + .iter() + .map(|a| (a.var.as_str(), a.target.as_str())) + .collect(); + assert_eq!(rb_vars, vec![("handler", "process")]); + + // ── Java: method reference ── + let jv = codegraph_extract::languages::java::JavaParser + .parse_file("J.java", "class A {\n void setup() {\n Runnable r = this::run;\n Supplier s = Foo::bar;\n }\n}\n") + .unwrap(); + let mut jv_vars: Vec<(&str, &str)> = jv + .fn_ptr_assigns + .iter() + .map(|a| (a.var.as_str(), a.target.as_str())) + .collect(); + jv_vars.sort(); + assert_eq!(jv_vars, vec![("r", "run"), ("s", "bar")]); + + // ── C#: delegate identifier ── + let cs = codegraph_extract::languages::csharp::CSharpParser + .parse_file( + "C.cs", + "class A {\n void Setup() {\n Action handler = Process;\n }\n}\n", + ) + .unwrap(); + let cs_vars: Vec<(&str, &str)> = cs + .fn_ptr_assigns + .iter() + .map(|a| (a.var.as_str(), a.target.as_str())) + .collect(); + assert_eq!(cs_vars, vec![("handler", "Process")]); + + // ── JS: member assignment (kể cả prototype) ── + let js = codegraph_extract::languages::javascript::JavaScriptParser + .parse_file( + "j.js", + "obj.handler = process;\nFoo.prototype.run = runImpl;\nplain = other;\n", + ) + .unwrap(); + let mut js_vars: Vec<(&str, &str)> = js + .fn_ptr_assigns + .iter() + .map(|a| (a.var.as_str(), a.target.as_str())) + .collect(); + js_vars.sort(); + assert_eq!( + js_vars, + vec![ + ("Foo.prototype.run", "runImpl"), + ("obj.handler", "process"), + ("plain", "other"), + ] + ); + + // ── Lua: field_expression ── + let lua = LuaParser + .parse_file("l.lua", "obj.handler = process\nlocal x = 1\n") + .unwrap(); + let lua_vars: Vec<(&str, &str)> = lua + .fn_ptr_assigns + .iter() + .map(|a| (a.var.as_str(), a.target.as_str())) + .collect(); + assert_eq!(lua_vars, vec![("obj.handler", "process")]); +} + +#[test] +fn await_markers_in_chain() { + // Python `await` + JS `await_expression` → MARKER_AWAIT trong chain. + let py = PythonParser + .parse_file("p.py", "async def f():\n await g()\n") + .unwrap(); + assert!(py + .chains + .values() + .any(|c| c.contains(&codegraph_core::MARKER_AWAIT))); + + let js = codegraph_extract::languages::javascript::JavaScriptParser + .parse_file("j.js", "async function f() { await g(); }\n") + .unwrap(); + assert!(js + .chains + .values() + .any(|c| c.contains(&codegraph_core::MARKER_AWAIT))); +} diff --git a/crates/codegraph-graph/src/lib.rs b/crates/codegraph-graph/src/lib.rs index adadbb2702..bb08ad3a0d 100644 --- a/crates/codegraph-graph/src/lib.rs +++ b/crates/codegraph-graph/src/lib.rs @@ -2139,11 +2139,10 @@ impl GraphIndex { res => return res, } - // 6. Function pointer assign-site (C/C++): `p = f;` tracked qua - // `FnPtrAssign` → call `p(...)` fan-out các target đã gán. - if !call.call_name.contains('.') - && let Some(targets) = self.fn_ptr_index.get(&call.call_name.to_lowercase()) - { + // 6. Function pointer assign-site: `p = f;` (C/C++/Go/JS...) và member + // fn assignment `obj.handler = fn` — key là text vế trái, lookup + // theo call_name (bare lẫn dotted). + if let Some(targets) = self.fn_ptr_index.get(&call.call_name.to_lowercase()) { let cands: Vec = targets .iter() .copied() @@ -6158,6 +6157,49 @@ mod tests { } } + #[tokio::test] + async fn member_fn_assignment_resolves_dotted_call() { + // `obj.handler = process` (member fn assignment) + call + // `obj.handler()` (call_name có dot) — guard fn_ptr_index phải mở. + let mut idx = GraphIndex::in_memory(); + let (caller, process) = (SYMBOL_BASE, SYMBOL_BASE + 1); + let mut r = result( + "j.js", + vec![ + dsym(caller, "setup", SymbolKind::Function, 0, None, 10), + dsym(process, "process", SymbolKind::Function, 0, None, 1), + ], + HashMap::from([(caller, vec![caller, 0])]), + vec![CallRecord { + caller_id: caller, + call_name: "obj.handler".to_string(), + position: 1, + line: 11, + condition: None, + is_loop_body: false, + effect: EffectType::None, + effect_desc: None, + target_class: None, + target_method: None, + into_var: None, + is_new_var: false, + receiver_text: Some("obj".to_string()), + ..Default::default() + }], + ); + r.fn_ptr_assigns = vec![FnPtrAssign { + var: "obj.handler".to_string(), + target: "process".to_string(), + line: 5, + }]; + idx.ingest(&[r], None).await.unwrap(); + + let flow = idx.flow(caller).await.unwrap(); + // Trước đây guard `!contains('.')` chặn — giờ resolve PointerAssign. + assert_eq!(flow.calls[0].resolved_via, ResolutionKind::PointerAssign); + assert_eq!(flow.calls[0].candidate_names, vec!["process"]); + } + #[tokio::test] async fn serde_old_call_record_blob_defaults() { // Blob cũ (trước khi thêm receiver_text/resolved_via/candidates) phải From 9bd95aff7bda3d4780bfdaef83ca084e9d674176 Mon Sep 17 00:00:00 2001 From: Hung Pham Date: Sun, 25 Oct 2026 20:01:34 +0700 Subject: [PATCH 13/19] =?UTF-8?q?C/C++=20enrichment=20batch=201-4:=20inclu?= =?UTF-8?q?de=20graph,=20fn-ptr=20m=E1=BB=9F=20r=E1=BB=99ng,=20sig=5Fkey,?= =?UTF-8?q?=20virtual=20precision?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. c_include_deps: #include "foo.h" / → FileDeps (path nguyên văn, alias = tên file không extension) — include graph qua machinery file_imports; C + C++ share. 2. c_fn_ptr_assign mở rộng: LHS field_expression (`obj.handler` — dotted key, ăn guard fn_ptr_index mới) + pointer_expression (`*handler` → "handler"); RHS `&f` address-of + `&ns::cb` qualified. 3. cpp_signature_key: params từ function_declarator (function_definition 1 tầng declarator; method inline field_declaration cũng 1 tầng — dump mới thấy, không phải 2); return field "type". 4. Virtual precision trong collect_fanout (C++ path): nếu CÓ candidate mang keyword virtual/override (signature chứa — extractor lưu dòng khai báo) → chỉ giữ virtual candidates + base decl; không keyword nào → giữ hết (soundness fallback). Loại non-virtual cùng tên trong hierarchy. E2E: cpp_virtual_filter_prefers_override_candidates (D2 non-virtual bị loại). Tests: 26/26 dynamic_dispatch (2 mới), 121/121 graph lib, 20/21 workspace (Redis env-only). --- crates/codegraph-extract/src/languages/c.rs | 78 ++++++++++-- crates/codegraph-extract/src/languages/cpp.rs | 38 ++++++ .../tests/dynamic_dispatch.rs | 86 +++++++++++++ crates/codegraph-graph/src/lib.rs | 116 ++++++++++++++++++ 4 files changed, 311 insertions(+), 7 deletions(-) diff --git a/crates/codegraph-extract/src/languages/c.rs b/crates/codegraph-extract/src/languages/c.rs index 9754bac551..7f4467c149 100644 --- a/crates/codegraph-extract/src/languages/c.rs +++ b/crates/codegraph-extract/src/languages/c.rs @@ -1,5 +1,6 @@ use crate::languages::common::{text, CallRule, HookSpec, LangSpec, MarkerSpec}; use codegraph_core::SymbolKind; +use codegraph_graph::{FileDeps, ImportAlias}; use tree_sitter::Node; fn ts_language() -> tree_sitter::Language { @@ -12,16 +13,73 @@ fn ts_language() -> tree_sitter::Language { /// /// Chỉ nhận LHS/var là identifier thuần và RHS/value là identifier thuần /// (địa chỉ hàm; `&f` cũng qua identifier con). +/// `#include` graph: `"foo.h"` (quoted) / `` (system) → +/// FileDeps — path nguyên văn, alias = tên file (không extension). Include +/// graph rebuild miễn phí qua machinery file_imports. +pub(crate) fn c_include_deps(root: &Node, src: &[u8]) -> Option { + let mut imports: Vec = Vec::new(); + let mut stack = vec![*root]; + while let Some(n) = stack.pop() { + if n.kind() == "preproc_include" { + if let Some(path_node) = n.child_by_field_name("path") { + let raw = text(&path_node, src).unwrap_or_default(); + let path = raw + .trim_matches(|c| c == '"' || c == '<' || c == '>') + .to_string(); + let alias = path + .rsplit('/') + .next() + .unwrap_or("") + .split('.') + .next() + .unwrap_or("") + .to_string(); + if !path.is_empty() && !alias.is_empty() { + imports.push(ImportAlias { alias, path }); + } + } + } + let mut cursor = n.walk(); + for ch in n.children(&mut cursor) { + stack.push(ch); + } + } + if imports.is_empty() { + None + } else { + Some(FileDeps { + package: None, + imports, + }) + } +} + pub(crate) fn c_fn_ptr_assign(node: &Node, src: &[u8]) -> Option<(String, String)> { match node.kind() { "assignment_expression" => { let left = node.child_by_field_name("left")?; let right = node.child_by_field_name("right")?; - if left.kind() != "identifier" { - return None; - } - let var = text(&left, src)?; - let target = identifier_text(&right, src)?; + // LHS: identifier thuần, field (`obj.handler` — dotted key), deref + // (`*handler` — strip *). Bracket `handlers[i]` skip. + let var = match left.kind() { + "identifier" => text(&left, src)?, + "field_expression" => text(&left, src)?, + "pointer_expression" => { + let inner = left.child_by_field_name("argument")?; + if inner.kind() != "identifier" { + return None; + } + text(&inner, src)? + } + _ => return None, + }; + // RHS: identifier thuần, `&f` (address-of), `&ns::cb` (qualified). + let target = match right.kind() { + "identifier" => text(&right, src)?, + "qualified_identifier" => text(&right, src)?, + "pointer_expression" => identifier_text(&right, src)?, + _ => return None, + }; Some((var, target)) } "init_declarator" => { @@ -38,11 +96,16 @@ pub(crate) fn c_fn_ptr_assign(node: &Node, src: &[u8]) -> Option<(String, String } } -/// Text identifier khi node là identifier thuần. `&f` (address-of) cố tình bỏ — -/// không có type info để phân biệt fn-ptr var với biến thường (tránh false positive). +/// Text identifier khi node là identifier thuần hoặc `&f` (address-of — +/// RHS của fn-ptr assignment; LHS vẫn yêu cầu identifier thuần nên không +/// có false positive biến thường). fn identifier_text(node: &Node, src: &[u8]) -> Option { match node.kind() { "identifier" => text(node, src), + "pointer_expression" => { + let inner = node.child_by_field_name("argument")?; + (inner.kind() == "identifier").then(|| text(&inner, src))? + } _ => None, } } @@ -88,6 +151,7 @@ pub static SPEC: LangSpec = LangSpec { }], hooks: HookSpec { fn_ptr_assign_fn: Some(c_fn_ptr_assign), + file_deps_fn: Some(c_include_deps), ..HookSpec::none() }, markers: MarkerSpec { diff --git a/crates/codegraph-extract/src/languages/cpp.rs b/crates/codegraph-extract/src/languages/cpp.rs index a8cf656975..bf04fed650 100644 --- a/crates/codegraph-extract/src/languages/cpp.rs +++ b/crates/codegraph-extract/src/languages/cpp.rs @@ -75,6 +75,8 @@ pub static SPEC: LangSpec = LangSpec { hooks: HookSpec { impl_trait_fn: Some(cpp_base_classes), fn_ptr_assign_fn: Some(crate::languages::c::c_fn_ptr_assign), + sig_key_fn: Some(cpp_signature_key), + file_deps_fn: Some(crate::languages::c::c_include_deps), ..HookSpec::none() }, markers: MarkerSpec { @@ -99,3 +101,39 @@ pub static SPEC: LangSpec = LangSpec { ..LangSpec::dflt() }; crate::lang_parser!(CppParser, SPEC); + +/// Signature key `(params)(return)` cho C++ — params từ function_declarator +/// trong declarator field; return = field `type`. Method inline trong class +/// (field_declaration với function_declarator) cũng tính. +fn cpp_signature_key(node: &Node, src: &[u8]) -> Option { + let is_fn = node.kind() == "function_definition"; + // field_declaration (method inline trong class): declarator field TRỰC + // TIẾP là function_declarator (1 tầng, không 2). + let is_method_decl = node.kind() == "field_declaration" + && node + .child_by_field_name("declarator") + .is_some_and(|d| d.kind() == "function_declarator"); + if !is_fn && !is_method_decl { + return None; + } + let declarator = node.child_by_field_name("declarator")?; + let mut pt: Vec = Vec::new(); + if let Some(params) = declarator.child_by_field_name("parameters") { + let mut cursor = params.walk(); + for ch in params.children(&mut cursor) { + if ch.kind() == "parameter" { + if let Some(t) = ch.child_by_field_name("type") { + if let Some(txt) = text(&t, src) { + pt.push(txt.replace(' ', "")); + } + } + } + } + } + let rt = node + .child_by_field_name("type") + .and_then(|t| text(&t, src)) + .map(|t| t.replace(' ', "")) + .unwrap_or_else(|| "void".to_string()); + Some(format!("({})({})", pt.join(","), rt)) +} diff --git a/crates/codegraph-extract/tests/dynamic_dispatch.rs b/crates/codegraph-extract/tests/dynamic_dispatch.rs index a73e4e7ae0..43a6b55e62 100644 --- a/crates/codegraph-extract/tests/dynamic_dispatch.rs +++ b/crates/codegraph-extract/tests/dynamic_dispatch.rs @@ -1064,3 +1064,89 @@ fn await_markers_in_chain() { .values() .any(|c| c.contains(&codegraph_core::MARKER_AWAIT))); } + +#[test] +fn c_includes_and_extended_fn_ptr() { + let src = r#" +#include "foo.h" +#include + +void cb(int x) {} + +void setup(void) { + obj.handler = cb; + *handler = cb; + handler = &ns_cb; +} +"#; + let parsed = codegraph_extract::languages::c::CParser + .parse_file("s.c", src) + .unwrap(); + + // C1: include graph. + let deps = parsed.file_deps.expect("include deps missing"); + let mut pairs: Vec<(&str, &str)> = deps + .imports + .iter() + .map(|i| (i.alias.as_str(), i.path.as_str())) + .collect(); + pairs.sort(); + assert_eq!(pairs, vec![("foo", "foo.h"), ("ssl", "openssl/ssl.h")]); + + // C2: fn-ptr mở rộng — field / deref / address-of. + let mut assigns: Vec<(&str, &str)> = parsed + .fn_ptr_assigns + .iter() + .map(|a| (a.var.as_str(), a.target.as_str())) + .collect(); + assigns.sort(); + assert_eq!( + assigns, + vec![ + ("handler", "cb"), + ("handler", "ns_cb"), + ("obj.handler", "cb") + ] + ); +} + +#[test] +fn cpp_sig_key_and_virtual_declaration() { + let src = r#" +class Base { +public: + virtual void run(); +}; +class D : public Base { +public: + void run() override; + void helper(); +}; +void D::run() {} +"#; + let parsed = codegraph_extract::languages::cpp::CppParser + .parse_file("s.cpp", src) + .unwrap(); + + // C3: sig_key — method inline (field_declaration) + out-of-line definition. + let run_inline = parsed + .symbols + .iter() + .find(|s| s.name == "run" && s.kind == SymbolKind::Method && s.line == 8) + .expect("inline run missing"); + assert_eq!(run_inline.sig_key.as_deref(), Some("()(void)")); + + // C4 dữ liệu nền: signature chứa virtual/override (filter dùng ở graph). + let base_run = parsed + .symbols + .iter() + .find(|s| s.name == "run" && s.line == 4) + .expect("base run decl missing"); + assert!(base_run.signature.as_deref().unwrap().contains("virtual")); + let d_run = parsed + .symbols + .iter() + .find(|s| s.name == "run" && s.line == 8) + .unwrap(); + assert!(d_run.signature.as_deref().unwrap().contains("override")); +} diff --git a/crates/codegraph-graph/src/lib.rs b/crates/codegraph-graph/src/lib.rs index bb08ad3a0d..bb2b3f1d47 100644 --- a/crates/codegraph-graph/src/lib.rs +++ b/crates/codegraph-graph/src/lib.rs @@ -2230,6 +2230,21 @@ impl GraphIndex { // Java/Rust interface method là abstract → loại decl. if tr.kind == SymbolKind::Class { cands.push(decl_id); + // Virtual precision: nếu CÓ candidate nào mang keyword + // virtual/override (signature chứa keyword — extractor lưu + // dòng khai báo), chỉ giữ các candidate đó + base decl — + // loại non-virtual cùng tên trong hierarchy. + let is_virtual = |id: u64| { + self.symbols.get(&id).is_some_and(|s| { + s.signature + .as_deref() + .is_some_and(|sig| sig.contains("virtual") || sig.contains("override")) + }) + }; + let has_virtual = cands.iter().any(|&id| is_virtual(id)); + if has_virtual { + cands.retain(|&id| is_virtual(id) || id == decl_id); + } } } cands.sort_unstable(); @@ -6200,6 +6215,107 @@ mod tests { assert_eq!(flow.calls[0].candidate_names, vec!["process"]); } + #[tokio::test] + async fn cpp_virtual_filter_prefers_override_candidates() { + // Base::run (virtual) + D1::run (override) + D2::run (non-virtual cùng + // tên) — call b.run() qua base → chỉ virtual candidates (+ base decl). + let mut idx = GraphIndex::in_memory(); + let vsym = |id: u64, + name: &str, + kind: SymbolKind, + scope_id: u64, + impl_trait: Option<&str>, + sig: Option<&str>, + line: u32| Symbol { + id, + name: name.to_string(), + kind, + scope: ScopeLevel::Global, + scope_id, + type_ref: 0, + type_name: None, + file: "t.cpp".to_string(), + line, + // Impl block cần range chứa method con (method line = line + 1). + end_line: if impl_trait.is_some() { line + 9 } else { line }, + signature: sig.map(String::from), + doc: None, + annotations: Vec::new(), + language: "cpp".to_string(), + impl_trait: impl_trait.map(String::from), + sig_key: None, + }; + let (caller, base_id, d1_impl, d1_run, d2_impl, d2_run) = ( + SYMBOL_BASE, + SYMBOL_BASE + 1, + SYMBOL_BASE + 2, + SYMBOL_BASE + 3, + SYMBOL_BASE + 4, + SYMBOL_BASE + 5, + ); + let r = result( + "t.cpp", + vec![ + vsym(caller, "caller", SymbolKind::Function, 0, None, None, 40), + vsym(base_id, "Base", SymbolKind::Class, 0, None, None, 1), + vsym( + SYMBOL_BASE + 6, + "run", + SymbolKind::Method, + base_id, + None, + Some("virtual void run();"), + 2, + ), + vsym(d1_impl, "D1", SymbolKind::Class, 0, Some("Base"), None, 5), + vsym( + d1_run, + "run", + SymbolKind::Method, + d1_impl, + None, + Some("void run() override;"), + 6, + ), + vsym(d2_impl, "D2", SymbolKind::Class, 0, Some("Base"), None, 10), + vsym( + d2_run, + "run", + SymbolKind::Method, + d2_impl, + None, + Some("void run();"), + 11, + ), + ], + HashMap::from([(caller, vec![caller, 0])]), + vec![CallRecord { + caller_id: caller, + call_name: "b.run".to_string(), + position: 1, + line: 41, + condition: None, + is_loop_body: false, + effect: EffectType::None, + effect_desc: None, + target_class: None, + target_method: None, + into_var: None, + is_new_var: false, + ..Default::default() + }], + ); + idx.ingest(&[r], None).await.unwrap(); + + let flow = idx.flow(caller).await.unwrap(); + assert_eq!(flow.calls[0].resolved_via, ResolutionKind::HierarchyFanOut); + let mut names = flow.calls[0].candidate_names.clone(); + names.sort(); + // D2::run (non-virtual) bị loại — chỉ D1::run (override) + Base::run. + assert_eq!(names, vec!["run", "run"]); + assert_eq!(idx.stats().edges, 2); + } + #[tokio::test] async fn serde_old_call_record_blob_defaults() { // Blob cũ (trước khi thêm receiver_text/resolved_via/candidates) phải From 1bdbe0e4275fb129f76770e11cb07b2bd4c6e769 Mon Sep 17 00:00:00 2001 From: Hung Pham Date: Sun, 25 Oct 2026 22:19:44 +0700 Subject: [PATCH 14/19] Web UI: dynamic-dispatch badges, Call sites/Pattern/Trace tabs, search pagination + annotation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Explore UI (SvelteKit + GraphQL) lộ đầy đủ query surface mới: - Flow tab: mỗi call hiện badge resolvedVia ([dyn]/[fn-ptr]/[unresolved]/…) + candidate chips click-to-navigate (searchExact). GraphQL enum là SCREAMING_SNAKE_CASE — smoke query bắt được bug casing lowercase ban đầu. - Tab Call sites (references): tìm callers theo call name — thấy cả unresolved/library + dynamic calls; prefill theo symbol name. - Tab Pattern (searchFlow): tìm hàm theo chain pattern (markers/tên/id). - Tab Trace (traceFullFlow — targetId arg): debug-style paths caller → target, indent theo stackDepth, icon theo action (CALL/RETURN/THROW/BRANCH/LOOP), màu theo PathEnding; chọn caller depth. - SymbolSearch: Load more (offset pagination), "continue" khi timed_out (resumable cursor), source by name | by annotation (graphcodeSearchByAnnotation). Smoke qua server thật: flow(ingest_with_progress) có 11 HIERARCHY_FAN_OUT calls với 7 candidates (dyn Storage backends), trace 3 paths, annotation "derive" 196 kết quả, references set_file_deps 2 callers. svelte-check 0 errors, build-web assets embed, workspace tests 20/21 (Redis env-only). --- web/src/lib/components/SymbolDetail.svelte | 367 ++++++++++++++++++++- web/src/lib/components/SymbolSearch.svelte | 84 ++++- web/src/lib/graphql/queries.ts | 60 +++- 3 files changed, 496 insertions(+), 15 deletions(-) diff --git a/web/src/lib/components/SymbolDetail.svelte b/web/src/lib/components/SymbolDetail.svelte index de825bc566..56a155f6f3 100644 --- a/web/src/lib/components/SymbolDetail.svelte +++ b/web/src/lib/components/SymbolDetail.svelte @@ -1,6 +1,14 @@