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/Cargo.lock b/Cargo.lock index b2b863fd44..acdc80a539 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -720,7 +720,7 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "codegraph" -version = "2.2.10" +version = "2.2.11" dependencies = [ "anyhow", "camino", @@ -743,7 +743,7 @@ dependencies = [ [[package]] name = "codegraph-api" -version = "2.2.10" +version = "2.2.11" dependencies = [ "anyhow", "camino", @@ -761,7 +761,7 @@ dependencies = [ [[package]] name = "codegraph-bench" -version = "2.2.10" +version = "2.2.11" dependencies = [ "anyhow", "camino", @@ -780,7 +780,7 @@ dependencies = [ [[package]] name = "codegraph-binary" -version = "2.2.10" +version = "2.2.11" dependencies = [ "camino", "codegraph-core", @@ -797,7 +797,7 @@ dependencies = [ [[package]] name = "codegraph-context" -version = "2.2.10" +version = "2.2.11" dependencies = [ "camino", "codegraph-core", @@ -812,7 +812,7 @@ dependencies = [ [[package]] name = "codegraph-core" -version = "2.2.10" +version = "2.2.11" dependencies = [ "async-graphql", "camino", @@ -823,7 +823,7 @@ dependencies = [ [[package]] name = "codegraph-docs" -version = "2.2.10" +version = "2.2.11" dependencies = [ "anyhow", "codegraph-core", @@ -841,7 +841,7 @@ dependencies = [ [[package]] name = "codegraph-extract" -version = "2.2.10" +version = "2.2.11" dependencies = [ "camino", "codegraph-binary", @@ -880,7 +880,7 @@ dependencies = [ [[package]] name = "codegraph-graph" -version = "2.2.10" +version = "2.2.11" dependencies = [ "async-trait", "bincode", @@ -910,7 +910,7 @@ dependencies = [ [[package]] name = "codegraph-graphql" -version = "2.2.10" +version = "2.2.11" dependencies = [ "anyhow", "async-graphql", @@ -936,7 +936,7 @@ dependencies = [ [[package]] name = "codegraph-installer" -version = "2.2.10" +version = "2.2.11" dependencies = [ "anyhow", "camino", @@ -952,7 +952,7 @@ dependencies = [ [[package]] name = "codegraph-mcp" -version = "2.2.10" +version = "2.2.11" dependencies = [ "anyhow", "axum", @@ -976,7 +976,7 @@ dependencies = [ [[package]] name = "codegraph-sboxes" -version = "2.2.10" +version = "2.2.11" dependencies = [ "camino", "codegraph-core", @@ -998,7 +998,7 @@ dependencies = [ [[package]] name = "codegraph-source" -version = "2.2.10" +version = "2.2.11" dependencies = [ "async-trait", "camino", @@ -1011,7 +1011,7 @@ dependencies = [ [[package]] name = "codegraph-web" -version = "2.2.10" +version = "2.2.11" dependencies = [ "axum", "mime_guess", diff --git a/Cargo.toml b/Cargo.toml index 0b4c7e2534..818f03165d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ members = [ ] [workspace.package] -version = "2.2.10" +version = "2.2.11" edition = "2021" rust-version = "1.80" license = "MIT" diff --git a/crates/codegraph-api/src/mermaid.rs b/crates/codegraph-api/src/mermaid.rs index 2041e47adf..4932a04504 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,8 @@ mod tests { doc: None, annotations: Vec::new(), language: "typescript".into(), + impl_trait: None, + sig_key: None, }, chain, chain_desc: desc.into_iter().map(String::from).collect(), @@ -779,6 +793,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..406722194b 100644 --- a/crates/codegraph-api/tests/api.rs +++ b/crates/codegraph-api/tests/api.rs @@ -22,6 +22,8 @@ fn sym(id: u64, name: &str) -> Symbol { doc: None, annotations: Vec::new(), language: "typescript".into(), + impl_trait: None, + sig_key: None, } } @@ -60,7 +62,10 @@ async fn seed_index(path: &str) -> (u64, u64, u64) { target_method: None, into_var: None, is_new_var: false, + ..Default::default() }], + fn_ptr_assigns: Vec::new(), + file_deps: None, }; idx.ingest(&[r], None).await.unwrap(); (caller, callee, helper) @@ -293,6 +298,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(); @@ -323,6 +330,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(); @@ -357,7 +366,10 @@ async fn seed_references(db: &str, count: usize) { target_method: None, into_var: None, 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 f10ce01b02..753d202146 100644 --- a/crates/codegraph-bench/benches/context.rs +++ b/crates/codegraph-bench/benches/context.rs @@ -35,6 +35,8 @@ fn fixture(fan_in: usize) -> ParseResult { doc: None, annotations: Vec::new(), language: "rust".into(), + impl_trait: None, + sig_key: None, }); let chain = if i == 0 { vec![id] @@ -53,6 +55,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 add64df42f..310802e420 100644 --- a/crates/codegraph-bench/src/bin/mem.rs +++ b/crates/codegraph-bench/src/bin/mem.rs @@ -333,6 +333,8 @@ 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, + sig_key: None, }); } @@ -366,6 +368,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(), }); } } @@ -382,6 +387,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 90674d9891..321038a232 100644 --- a/crates/codegraph-binary/src/extract.rs +++ b/crates/codegraph-binary/src/extract.rs @@ -154,6 +154,8 @@ fn do_extract( doc: None, annotations, language: "binary".to_string(), + impl_trait: None, + sig_key: None, }); } @@ -204,6 +206,8 @@ fn do_extract( doc: None, annotations, language: "binary".to_string(), + impl_trait: None, + sig_key: None, }); } @@ -247,6 +251,8 @@ fn do_extract( line: 0, }], language: "binary".to_string(), + impl_trait: None, + sig_key: None, }); } @@ -271,6 +277,8 @@ 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, + sig_key: None, }); } @@ -311,6 +319,8 @@ fn do_extract( symbols, chains, calls, + fn_ptr_assigns: Vec::new(), + file_deps: None, }) } @@ -552,6 +562,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 +648,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..ce255d872c 100644 --- a/crates/codegraph-context/src/lib.rs +++ b/crates/codegraph-context/src/lib.rs @@ -291,6 +291,8 @@ mod tests { doc: None, annotations: Vec::new(), language: "java".into(), + impl_trait: None, + sig_key: None, } } @@ -327,6 +329,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(); } @@ -365,6 +369,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(); } @@ -397,6 +403,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 f4a4ba1b5e..1feabca0c1 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_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 c50463ee0e..96279bf474 100644 --- a/crates/codegraph-core/src/semgraph.rs +++ b/crates/codegraph-core/src/semgraph.rs @@ -42,11 +42,17 @@ 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, + /// 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; 20] = [ + pub const ALL: [Marker; 23] = [ Marker::Loop, Marker::RecCall, Marker::IfTrue, @@ -67,6 +73,9 @@ impl Marker { Marker::TryStart, Marker::TryEnd, Marker::CatchStart, + Marker::Go, + Marker::Defer, + Marker::Await, ]; /// Giá trị u64 tương ứng (cho tương thích với chain cũ). @@ -99,6 +108,9 @@ impl Marker { 22 => Some(Marker::TryStart), 23 => Some(Marker::TryEnd), 24 => Some(Marker::CatchStart), + 25 => Some(Marker::Go), + 26 => Some(Marker::Defer), + 27 => Some(Marker::Await), _ => None, } } @@ -126,6 +138,9 @@ impl Marker { Marker::TryStart => "TRY_START", Marker::TryEnd => "TRY_END", Marker::CatchStart => "CATCH_START", + Marker::Go => "GO", + Marker::Defer => "DEFER", + Marker::Await => "AWAIT", } } @@ -153,6 +168,9 @@ 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), + "await" => Some(Marker::Await), _ => None, } } @@ -190,6 +208,9 @@ 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; +pub const MARKER_AWAIT: u64 = 27; /// Helper để đọc chain sang danh sách marker — tiện cho code đọc. #[allow(dead_code)] @@ -332,6 +353,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 +370,7 @@ impl EffectType { Self::FileRead => "file_read", Self::FileWrite => "file_write", Self::Log => "log", + Self::Panic => "panic", } } @@ -364,6 +388,7 @@ impl EffectType { "file_read" => Self::FileRead, "file_write" => Self::FileWrite, "log" => Self::Log, + "panic" => Self::Panic, _ => return None, }) } @@ -419,6 +444,14 @@ 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, + /// 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`). @@ -431,10 +464,35 @@ 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, + /// 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. /// /// 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 +515,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 +540,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 +609,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 @@ -646,6 +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 | Marker::Await => Self::Statement, } } } diff --git a/crates/codegraph-extract/src/bingraph.rs b/crates/codegraph-extract/src/bingraph.rs index a2615980ff..4fe2faa006 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,8 @@ mod tests { doc: None, annotations, language: "binary".to_string(), + impl_trait: None, + sig_key: None, } } @@ -985,6 +989,7 @@ mod tests { target_method: None, into_var: None, is_new_var: false, + ..Default::default() }, CallRecord { caller_id: sid(1), @@ -1000,8 +1005,11 @@ mod tests { target_method: None, into_var: None, is_new_var: false, + ..Default::default() }, ], + fn_ptr_assigns: Vec::new(), + file_deps: None, } } diff --git a/crates/codegraph-extract/src/config.rs b/crates/codegraph-extract/src/config.rs index 8363c36d3f..ddef8b7f08 100644 --- a/crates/codegraph-extract/src/config.rs +++ b/crates/codegraph-extract/src/config.rs @@ -201,6 +201,129 @@ struct EmbeddingSection { execution_provider: Option, } +// ── Effect-rule runtime config (MCP `codegraph_config_effects` + GraphQL) ── +// Macro/call semantics do LLM khai báo lúc runtime: đọc/ghi `[[effect_rules]]` +// trong .codegraph/config.toml bằng toml_edit (giữ nguyên mọi section khác). +// Orchestrator install_current lại config MỖI lần index → rule mới áp dụng +// từ lần index kế tiếp (đồng thời install ngay cho thread hiện tại). + +/// Một rule trong file — dạng raw để list trung thực cả rule không hợp lệ. +#[derive(Debug, Clone)] +pub struct EffectRuleView { + /// `contains` | `prefix` | `exact`. + pub matcher: String, + pub text: String, + pub effect: String, +} + +fn effect_config_path(root: &Utf8Path) -> Utf8PathBuf { + root.join(".codegraph").join("config.toml") +} + +/// Đọc `[[effect_rules]]` hiện có (raw — kể cả effect string không hợp lệ). +pub fn list_effect_rules(root: &Utf8Path) -> Vec { + let Ok(raw) = std::fs::read_to_string(effect_config_path(root)) else { + return Vec::new(); + }; + let Ok(doc) = raw.parse::() else { + return Vec::new(); + }; + let mut out = Vec::new(); + if let Some(arr) = doc.get("effect_rules").and_then(|i| i.as_array_of_tables()) { + for t in arr.iter() { + let (matcher, text) = t + .get("call") + .and_then(|c| c.as_table()) + .map(|c| { + for k in ["contains", "prefix", "exact"] { + if let Some(v) = c.get(k).and_then(|v| v.as_str()) { + return (k.to_string(), v.to_string()); + } + } + (String::new(), String::new()) + }) + .unwrap_or_default(); + let effect = t + .get("effect") + .and_then(|e| e.as_str()) + .unwrap_or_default() + .to_string(); + out.push(EffectRuleView { + matcher, + text, + effect, + }); + } + } + out +} + +/// Thêm rule (prepend vị trí cuối) + áp dụng ngay cho thread hiện tại. +/// Rule config được xét TRƯỚC bảng default (first-match-wins). +pub fn add_effect_rule( + root: &Utf8Path, + matcher: &str, + text: &str, + effect: &str, +) -> Result<(), String> { + if !matches!(matcher, "contains" | "prefix" | "exact") { + return Err(format!( + "matcher phải là contains|prefix|exact, nhận được {matcher:?}" + )); + } + if codegraph_core::EffectType::parse(effect).is_none() { + return Err(format!( + "effect không hợp lệ {effect:?} — dùng snake_case: panic, sql_query, http_call, …" + )); + } + if text.trim().is_empty() { + return Err("text matcher rỗng".into()); + } + let path = effect_config_path(root); + std::fs::create_dir_all(path.parent().unwrap()).map_err(|e| e.to_string())?; + let raw = std::fs::read_to_string(&path).unwrap_or_default(); + let mut doc = raw + .parse::() + .map_err(|e| format!("config.toml không parse được: {e}"))?; + if !doc.contains_key("effect_rules") { + doc["effect_rules"] = toml_edit::Item::ArrayOfTables(toml_edit::ArrayOfTables::new()); + } + let arr = doc["effect_rules"] + .as_array_of_tables_mut() + .ok_or("effect_rules phải là array-of-tables")?; + let mut call = toml_edit::Table::new(); + call.insert(matcher, toml_edit::value(text)); + let mut rule = toml_edit::Table::new(); + rule.insert("call", toml_edit::Item::Table(call)); + rule.insert("effect", toml_edit::value(effect)); + arr.push(rule); + std::fs::write(&path, doc.to_string()).map_err(|e| e.to_string())?; + // Áp dụng ngay cho thread hiện tại (orchestrator tự reload mỗi index). + let classifier = ExtractConfig::load(root).effect_classifier; + crate::languages::effects::install_current(Some(std::sync::Arc::new(classifier))); + Ok(()) +} + +/// Xoá rule theo index (0-based, theo thứ tự `list_effect_rules`). +pub fn remove_effect_rule(root: &Utf8Path, index: usize) -> Result { + let path = effect_config_path(root); + let raw = std::fs::read_to_string(&path).map_err(|e| e.to_string())?; + let mut doc = raw + .parse::() + .map_err(|e| format!("config.toml không parse được: {e}"))?; + let arr = doc["effect_rules"] + .as_array_of_tables_mut() + .ok_or("config không có [[effect_rules]]")?; + if index >= arr.len() { + return Ok(false); + } + arr.remove(index); + std::fs::write(&path, doc.to_string()).map_err(|e| e.to_string())?; + let classifier = ExtractConfig::load(root).effect_classifier; + crate::languages::effects::install_current(Some(std::sync::Arc::new(classifier))); + Ok(true) +} + /// Raw rule — `effect` để string để rule lỗi (unknown) bị skip + warn, không /// làm hỏng toàn bộ config; parse lại bằng `EffectType::parse`. #[derive(Debug, Deserialize)] @@ -724,6 +847,38 @@ pub fn is_cpp_header(source: &str) -> bool { MARKERS.iter().any(|m| sample.contains(m)) } +#[test] +fn effect_rule_crud_roundtrip() { + let dir = tempfile::tempdir().unwrap(); + let root = camino::Utf8PathBuf::from_path_buf(dir.path().to_path_buf()).unwrap(); + // init config mặc định + crate::project::init_project(&root).unwrap(); + assert!(list_effect_rules(&root).is_empty()); + // add: CHECK macro → panic (exception-like) + add_effect_rule(&root, "contains", "CHECK", "panic").unwrap(); + add_effect_rule(&root, "prefix", "db.", "sql_query").unwrap(); + let rules = list_effect_rules(&root); + assert_eq!(rules.len(), 2); + assert_eq!(rules[0].matcher, "contains"); + assert_eq!(rules[0].text, "CHECK"); + assert_eq!(rules[0].effect, "panic"); + // rule thực sự hiệu lực với classifier load từ file + let classifier = ExtractConfig::load(&root).effect_classifier; + assert_eq!(classifier.classify("CHECK_OK").0, EffectType::Panic); + assert_eq!(classifier.classify("db.Query").0, EffectType::SqlQuery); + // invalidate: effect sai bị chặn, file không đổi + assert!(add_effect_rule(&root, "contains", "x", "nonsense").is_err()); + assert_eq!(list_effect_rules(&root).len(), 2); + // remove theo index + assert!(remove_effect_rule(&root, 0).unwrap()); + assert_eq!(list_effect_rules(&root).len(), 1); + assert_eq!(list_effect_rules(&root)[0].text, "db."); + assert!(!remove_effect_rule(&root, 9).unwrap()); + // các section khác của config vẫn nguyên (storage etc. parse được) + let cfg = ExtractConfig::load(&root); + assert_eq!(cfg.storage.kind, StorageKind::Sqlite); +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/codegraph-extract/src/languages/c.rs b/crates/codegraph-extract/src/languages/c.rs index 7b808cd558..518a18287a 100644 --- a/crates/codegraph-extract/src/languages/c.rs +++ b/crates/codegraph-extract/src/languages/c.rs @@ -1,10 +1,150 @@ -use crate::languages::common::{CallRule, LangSpec}; +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_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). +/// `#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. +/// Signature cho typedef + macro (định nghĩa có sẵn): +/// - `typedef int (*cb)(int)` → `int (*cb)(int)` (underlying type đầy đủ). +/// - `#define CALL(x) x()` → `CALL(x) x()` (định nghĩa nguyên văn — macro +/// không resolve thành call edge; semantic do `[[effect_rules]]` config). +pub(crate) fn c_typedef_sig(node: &Node, src: &[u8]) -> Option { + let raw = text(node, src)? + .trim() + .trim_end_matches(';') + .trim() + .to_string(); + let sig = match node.kind() { + "type_definition" => raw.trim_start_matches("typedef").trim().to_string(), + "preproc_def" | "preproc_function_def" => { + raw.trim_start_matches("#define").trim().to_string() + } + _ => return None, + }; + (!sig.is_empty()).then_some(sig) +} + +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")?; + // 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" => { + 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 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, + } +} + +/// 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"], @@ -15,6 +155,13 @@ pub static SPEC: LangSpec = LangSpec { ("union_specifier", SymbolKind::Class), ("enum_specifier", SymbolKind::Enum), ("type_definition", SymbolKind::Constant), + // Macro `#define CALL(x) x()` / `#define MAX 10` — Constant symbol, + // signature = text định nghĩa (xem c_macro_sig). Invocation KHÔNG + // resolve thành edge (macro không phải function) — usage tra qua + // call-name index (references); hành vi semantic (exception-like…) + // khai báo qua config `[[effect_rules]]`. + ("preproc_function_def", SymbolKind::Constant), + ("preproc_def", SymbolKind::Constant), ("declaration", SymbolKind::Variable), ("field_declaration", SymbolKind::Field), ("parameter_declaration", SymbolKind::Parameter), @@ -22,12 +169,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", @@ -35,27 +176,23 @@ pub static SPEC: LangSpec = LangSpec { name_fn: None, target_fn: None, }], - class_type_name: None, - 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), + file_deps_fn: Some(c_include_deps), + typedef_sig_fn: Some(c_typedef_sig), + ..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 d9391a8b6d..84da77ec76 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>; @@ -51,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], @@ -65,12 +73,16 @@ 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. pub calls: &'static [CallRule], - /// Lấy type_name cho class symbol (TS `extends`/`implements` heritage). - pub class_type_name: 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). @@ -79,14 +91,104 @@ pub struct LangSpec { /// 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(){}`). + /// 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: &[], + param_container_kinds: &[], + field_from_self_attr: false, + 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 (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 (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; gán vào `Symbol.type_name`. + pub receiver_fn: Option, + /// Signature key chuẩn hóa `(param_types)(result_types)` cho + /// function/method — method-set matching so cả signature. + pub sig_key_fn: Option, + /// Signature text cho typedef/type_definition (C `typedef int (*cb)(int)` + /// — underlying type nằm trong declarator, field `type` chỉ giữ return). + pub typedef_sig_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 sinh từ `anonymous_name_fn`. + /// 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], - // ── marker rules ── +} + +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, + typedef_sig_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], + /// 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 @@ -113,6 +215,36 @@ pub struct LangSpec { pub body_field: &'static str, } +impl MarkerSpec { + pub const fn dflt() -> Self { + Self { + go_stmt_kinds: &[], + defer_kinds: &[], + await_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`). @@ -182,9 +314,36 @@ pub fn run_spec( symbols, chains, calls, + fn_ptr_assigns: collect_fn_ptr_assigns(&root, src, spec), + file_deps: spec.hooks.file_deps_fn.and_then(|f| f(&root, src)), }) } +/// 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.hooks.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 @@ -209,7 +368,173 @@ 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), + // Python: typed/default param; TS: required/optional param + // (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" | "keyword_parameter" | "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 + }); + (name, ch.child_by_field_name("type")) + } + "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) + } + // Destructuring (`{a, b}`, `[x, y]`) — không tên đơn, bỏ. + _ => (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, + // 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, + 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). + 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); @@ -254,6 +579,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") @@ -267,7 +595,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) } @@ -326,17 +654,43 @@ 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.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.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); + // Typedef/macro: signature = text định nghĩa (hook ƯU TIÊN — declarator + // chứa phần fn-ptr mà field `type` bỏ sót; extract_signature fallback). + let signature = (if kind == SymbolKind::Constant { + spec.hooks.typedef_sig_fn.and_then(|f| f(node, ctx.src)) + } else { + None + }) + .or_else(|| extract_signature(node, ctx.src, spec.markers.body_field)); let annotations = extract_annotations(node, ctx.src, spec.annotation_kinds); ctx.symbols.push(Symbol { @@ -354,6 +708,8 @@ fn push_symbol( doc: None, annotations, language: ctx.language.to_string(), + impl_trait, + sig_key, }); Some(id) } @@ -564,7 +920,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; @@ -594,7 +950,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); @@ -622,6 +978,30 @@ 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.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.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); + } + 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) { chain_push(ctx, MARKER_CALL_ENTER); @@ -634,9 +1014,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()); @@ -655,15 +1035,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); @@ -682,7 +1062,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. @@ -709,7 +1089,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()); @@ -724,7 +1104,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. @@ -759,7 +1139,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()); @@ -786,15 +1166,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. @@ -814,16 +1194,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) @@ -835,12 +1215,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()); } } @@ -908,9 +1288,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()); @@ -925,10 +1305,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); @@ -973,7 +1353,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); @@ -996,17 +1376,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. @@ -1028,13 +1408,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); @@ -1042,20 +1422,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); @@ -1109,6 +1490,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 +1583,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 +1650,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 +1667,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> { @@ -1408,7 +1814,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) @@ -1419,7 +1825,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 1ea4de59bd..a662bd2e87 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, HookSpec, LangSpec, MarkerSpec}; 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"], @@ -17,19 +58,23 @@ pub static SPEC: LangSpec = LangSpec { ("enum_specifier", SymbolKind::Enum), ("namespace_definition", SymbolKind::Module), ("type_definition", SymbolKind::Constant), + // Macro `#define CALL(x) x()` / `#define MAX 10` — Constant symbol, + // signature = text định nghĩa (xem c_macro_sig). Invocation KHÔNG + // resolve thành edge (macro không phải function) — usage tra qua + // call-name index (references); hành vi semantic (exception-like…) + // khai báo qua config `[[effect_rules]]`. + ("preproc_function_def", SymbolKind::Constant), + ("preproc_def", SymbolKind::Constant), ("declaration", SymbolKind::Variable), ("field_declaration", SymbolKind::Field), ("parameter_declaration", SymbolKind::Parameter), + // Lambda `[](x){...}` — Function symbol, mượn tên biến qua + // anonymous_name_fn (`auto f = [](){}` → f); chain riêng cho body. + ("lambda_expression", SymbolKind::Function), ], - func_kinds: &["function_definition"], + func_kinds: &["function_definition", "lambda_expression"], 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", @@ -37,32 +82,88 @@ pub static SPEC: LangSpec = LangSpec { name_fn: None, target_fn: None, }], - class_type_name: None, - 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), + sig_key_fn: Some(cpp_signature_key), + file_deps_fn: Some(crate::languages::c::c_include_deps), + typedef_sig_fn: Some(crate::languages::c::c_typedef_sig), + anonymous_name_fn: Some(cpp_lambda_name), + value_func_kinds: &["lambda_expression"], + ..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); + +/// `auto f = [](int x){ work(); };` — lambda mượn tên biến (pattern JS/PHP): +/// declaration → init_declarator có value là lambda_expression → field `name`. +/// Lambda truyền thẳng (`register([](){})`) không tên. +fn cpp_lambda_name<'a>(node: &Node<'a>) -> Option> { + let p = node.parent()?; + if p.kind() != "init_declarator" { + return None; + } + let value = p.child_by_field_name("value")?; + if value.id() != node.id() { + return None; + } + // C++ init_declarator: tên ở field `declarator` (JS variable_declarator + // dùng field `name` — grammar khác nhau). + p.child_by_field_name("declarator") +} + +/// 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/src/languages/csharp.rs b/crates/codegraph-extract/src/languages/csharp.rs index 1a95055e5c..4602336966 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, LangSpec}; +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 { @@ -42,11 +43,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,33 +59,153 @@ pub static SPEC: LangSpec = LangSpec { target_fn: None, }, ], - class_type_name: None, - 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 { + 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 { + 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"], + await_kinds: &["await_expression"], + ..MarkerSpec::dflt() + }, + ..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" +/// +/// 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]; + 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); 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 aab9a5e54b..9529716477 100644 --- a/crates/codegraph-extract/src/languages/go.rs +++ b/crates/codegraph-extract/src/languages/go.rs @@ -1,5 +1,8 @@ -use crate::languages::common::{first_identifier, 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; fn ts_language() -> tree_sitter::Language { @@ -33,6 +36,7 @@ pub static SPEC: LangSpec = LangSpec { decls: &[ ("function_declaration", SymbolKind::Function), ("method_declaration", SymbolKind::Method), + ("method_elem", SymbolKind::Method), ("type_spec", SymbolKind::Class), ("var_spec", SymbolKind::Variable), ("const_spec", SymbolKind::Constant), @@ -40,14 +44,8 @@ pub static SPEC: LangSpec = LangSpec { ("func_literal", SymbolKind::Function), ], func_kinds: &["function_declaration", "method_declaration", "func_literal"], - class_kinds: &[], + 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", @@ -55,27 +53,185 @@ pub static SPEC: LangSpec = LangSpec { name_fn: None, target_fn: None, }], - class_type_name: None, - if_kinds: &["if_statement"], - elif_kinds: &[], - if_block_kinds: &[], - loop_kinds: &["for_statement"], - switch_kinds: &["expression_switch_statement", "type_switch_statement"], - switch_block_kinds: &[], - switch_case_kinds: &["expression_case", "type_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 +/// (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) +} + +/// 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". +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 43b67a26e3..bd93dc58b5 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, LangSpec}; +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 { @@ -86,11 +88,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,32 +104,157 @@ pub static SPEC: LangSpec = LangSpec { target_fn: None, }, ], - class_type_name: None, - 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_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 { + 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); + +/// 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 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. +/// +/// 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]; + 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 77fc7b5783..aa9586391e 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, LangSpec}; +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,13 +61,8 @@ 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"], + param_container_kinds: &["formal_parameters"], + field_from_self_attr: true, calls: &[ CallRule { kind: "call_expression", @@ -83,33 +79,194 @@ pub static SPEC: LangSpec = LangSpec { target_fn: None, }, ], - class_type_name: Some(class_type_name), - 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"], + 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 { + 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"], + await_kinds: &["await_expression"], + ..MarkerSpec::dflt() + }, + ..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 right.kind() != "identifier" { + return None; + } + 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")?; + 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/lua.rs b/crates/codegraph-extract/src/languages/lua.rs index 8158c9c174..008879ad53 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, LangSpec}; +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,13 +41,7 @@ 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"], + param_container_kinds: &["parameters"], calls: &[CallRule { kind: "function_call", callee_field: "name", @@ -54,27 +49,165 @@ pub static SPEC: LangSpec = LangSpec { name_fn: None, target_fn: None, }], - class_type_name: None, - 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"], + file_deps_fn: Some(lua_require_deps), + fn_ptr_assign_fn: Some(lua_fn_value_assign), + ..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); + +/// `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) { + // 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); + } + } + } + } + "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 df2436abba..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, LangSpec}; +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 { @@ -88,11 +89,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,33 +126,147 @@ pub static SPEC: LangSpec = LangSpec { target_fn: None, }, ], - class_type_name: None, - 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"], + impl_trait_fn: Some(php_supers), + sig_key_fn: Some(php_signature_key), + file_deps_fn: Some(php_use_deps), + ..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); + +/// 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/python.rs b/crates/codegraph-extract/src/languages/python.rs index d66d57a0dc..9b04ab3ed4 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, LangSpec}; +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,13 +32,8 @@ 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: &[], + param_container_kinds: &["parameters"], + field_from_self_attr: true, calls: &[CallRule { kind: "call", callee_field: "function", @@ -45,28 +41,182 @@ pub static SPEC: LangSpec = LangSpec { name_fn: None, target_fn: None, }], - class_type_name: None, - 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), + 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 { + 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"], + await_kinds: &["await"], + ..MarkerSpec::dflt() + }, + ..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" +/// +/// 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]; + 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/src/languages/ruby.rs b/crates/codegraph-extract/src/languages/ruby.rs index c39ff31291..3807b64720 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, LangSpec}; +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,13 +40,7 @@ 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: &[], + param_container_kinds: &["method_parameters"], calls: &[ CallRule { kind: "call", @@ -62,27 +57,133 @@ pub static SPEC: LangSpec = LangSpec { target_fn: None, }, ], - class_type_name: Some(class_type_name), - 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), + 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 { + 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() }; +/// 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; + 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/rust.rs b/crates/codegraph-extract/src/languages/rust.rs index 689ae8bdc0..2988af34fe 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, LangSpec}; +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,15 @@ 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), + // 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: &[ @@ -28,49 +39,51 @@ 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`. + 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, - // 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", - arguments_field: "arguments", - name_fn: None, - target_fn: None, - }], - class_type_name: Some(rust_class_type_name), - 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", + 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 { + 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. @@ -84,6 +97,99 @@ 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" +/// - `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). +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 +282,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..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, LangSpec}; +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() @@ -28,12 +30,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,27 +37,142 @@ pub static SPEC: LangSpec = LangSpec { name_fn: None, target_fn: None, }], - class_type_name: None, - 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 { + 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"], + 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); + +/// 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/src/languages/swift.rs b/crates/codegraph-extract/src/languages/swift.rs index 1e774455e9..8b32a2e698 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, LangSpec}; +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", @@ -34,42 +39,149 @@ 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, - 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 { + 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"], + 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"], + await_kinds: &["await_expression"], + ..MarkerSpec::dflt() + }, + ..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, + }) + } +} diff --git a/crates/codegraph-extract/src/languages/typescript.rs b/crates/codegraph-extract/src/languages/typescript.rs index 9794995847..7dfcf95c2d 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,8 @@ 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"], + param_container_kinds: &["formal_parameters"], + field_from_self_attr: true, calls: &[ CallRule { kind: "call_expression", @@ -90,34 +85,39 @@ pub static SPEC: LangSpec = LangSpec { target_fn: None, }, ], - class_type_name: Some(class_type_name), - 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"], + 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 { + 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"], + await_kinds: &["await_expression"], + ..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/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/chains.rs b/crates/codegraph-extract/tests/chains.rs index eb599bdae7..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]", @@ -1394,9 +1395,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 +1415,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 new file mode 100644 index 0000000000..1c992f9f6d --- /dev/null +++ b/crates/codegraph-extract/tests/dynamic_dispatch.rs @@ -0,0 +1,1220 @@ +//! 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::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] +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. + 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); +} + +#[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 +} + +#[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(); + 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)")); +} + +#[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")); +} + +#[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"]); +} + +#[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)); +} + +#[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"); + 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"), + ] + ); +} + +#[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))); +} + +#[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")); +} + +#[test] +fn cpp_lambda_macro_typedef_batch() { + // ── C++: lambda mượn tên biến + chain riêng; macro là Function symbol; + // typedef fn-ptr giữ underlying signature ── + let cpp_src = r#" +#define CALL(x) x() +#define MAX 10 + +typedef int (*handler_t)(int); + +int work(int x) { return x; } + +void setup() { + auto f = [](int x) { return work(x); }; + f(1); + CALL(work); +} +"#; + let parsed = codegraph_extract::languages::cpp::CppParser + .parse_file("s.cpp", cpp_src) + .unwrap(); + + // #5: lambda → Function "f" với chain chứa work. + let f = parsed + .symbols + .iter() + .find(|s| s.name == "f" && s.kind == SymbolKind::Function) + .expect("lambda f symbol missing"); + let chain = parsed.chains.get(&f.id).expect("lambda chain missing"); + assert!( + chain.iter().any(|&e| e == f.id + 1 || e > 0), + "lambda chain phải có calls" + ); + // Call work() bên trong lambda ghi vào chain của f (không phải setup). + assert!(parsed + .calls + .iter() + .any(|c| c.caller_id == f.id && c.call_name.ends_with("work"))); + + // #9 (thiết kế mới): macro = Constant + signature là ĐỊNH NGHĨA nguyên + // văn — KHÔNG resolve thành call edge; usage tra qua call-name index. + let call_macro = parsed + .symbols + .iter() + .find(|s| s.name == "CALL") + .expect("CALL macro missing"); + assert_eq!(call_macro.kind, SymbolKind::Constant); + assert_eq!(call_macro.signature.as_deref(), Some("CALL(x) x()")); + let max_macro = parsed + .symbols + .iter() + .find(|s| s.name == "MAX") + .expect("MAX macro missing"); + assert_eq!(max_macro.kind, SymbolKind::Constant); + assert_eq!(max_macro.signature.as_deref(), Some("MAX 10")); + + // Invocation là call record RAW (không resolve) — usage tracking. + assert!(parsed.calls.iter().any(|c| c.call_name == "CALL")); + + // Hành vi semantic do config khai báo ("LLM tự định nghĩa"): rule + // Contains "CALL" → Panic (exception-like) — classifier xét trước default. + use codegraph_core::{EffectCallPattern, EffectRule, EffectType}; + let classifier = + codegraph_extract::languages::effects::EffectClassifier::with_config(vec![EffectRule { + call: EffectCallPattern::Contains { + contains: "CALL".to_string(), + }, + effect: EffectType::Panic, + }]); + let (eff, _) = classifier.classify("CALL"); + assert_eq!(eff, EffectType::Panic); + + // #4: typedef fn-ptr signature giữ underlying type. + let handler_t = parsed + .symbols + .iter() + .find(|s| s.name == "handler_t") + .expect("handler_t missing"); + assert_eq!( + handler_t.signature.as_deref(), + Some("int (*handler_t)(int)") + ); +} diff --git a/crates/codegraph-graph/src/diff.rs b/crates/codegraph-graph/src/diff.rs index 782fb1f9cc..4833a524b6 100644 --- a/crates/codegraph-graph/src/diff.rs +++ b/crates/codegraph-graph/src/diff.rs @@ -497,6 +497,8 @@ mod tests { doc: None, annotations: Vec::new(), language: "test".to_string(), + impl_trait: None, + sig_key: None, } } @@ -514,6 +516,8 @@ mod tests { symbols, chains, calls, + fn_ptr_assigns: Vec::new(), + file_deps: None, } } @@ -630,6 +634,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..570c81cfa6 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}; @@ -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` đã @@ -301,6 +337,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 +382,24 @@ 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>, + /// 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>, + /// 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ừ đó. @@ -1003,6 +1067,11 @@ impl GraphIndex { scope_index: HashMap::new(), 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(), + dyn_callers: HashMap::new(), edge_count: 0, files: Vec::new(), next_id: SYMBOL_BASE, @@ -1099,6 +1168,71 @@ 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.link_cpp_decl_def(); + 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ừ đó). // Không cần `all_call_records` ở đây (đó là JSON deserialize ~80ms). @@ -1426,6 +1560,11 @@ impl GraphIndex { self.scope_index.clear(); 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.dyn_callers.clear(); self.edge_count = 0; self.files.clear(); self.name_records.clear(); @@ -1473,12 +1612,41 @@ 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); + } + } + + self.link_impls_globally(); + self.link_cpp_decl_def(); // ── Phase 2: resolve placeholder 0 trong chains ── - self.resolve_calls(&all_calls); + 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); + 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, p).await?; + self.build_call_indexes(&all_calls, &call_meta, p).await?; // ── Phase 4: files ── if let Some(p) = p { @@ -1498,6 +1666,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); } @@ -1585,7 +1775,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 +1802,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 +1824,261 @@ 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 { + // 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() { + 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); + } + } + } + + /// C++ declaration–definition linking Ở MỨC GLOBAL: `void Service::run()` + /// trong .cpp (definition, có chain) nối với method khai báo trong .hpp + /// (declaration, không chain) — re-parent def về scope của decl (class/ + /// namespace) + reclassify theo kind của decl, để fan-out/flow thấy + /// definition. Điều kiện: cùng tên + cùng sig_key (khi cả hai có) + khác + /// file + một bên có chain một bên không. + fn link_cpp_decl_def(&mut self) { + let mut by_name: HashMap> = HashMap::new(); + for sym in self.symbols.values() { + if sym.language == "cpp" + && matches!(sym.kind, SymbolKind::Function | SymbolKind::Method) + { + by_name + .entry(sym.name.to_lowercase()) + .or_default() + .push(sym.id); + } + } + let mut links: Vec<(u64, u64)> = Vec::new(); + for ids in by_name.values() { + for &d in ids { + let Some(def) = self.symbols.get(&d) else { + continue; + }; + if !self.chains_map.contains_key(&d) { + continue; // def phải có body + } + for &c in ids { + if c == d { + continue; + } + let Some(decl) = self.symbols.get(&c) else { + continue; + }; + if self.chains_map.contains_key(&c) { + continue; // decl không có body + } + if decl.file == def.file { + continue; + } + // sig_key khớp khi cả hai có (tránh nối overload khác). + match (&decl.sig_key, &def.sig_key) { + (Some(a), Some(b)) if a != b => continue, + _ => {} + } + links.push((d, c)); + break; + } + } + } + for (def_id, decl_id) in links { + let Some(decl) = self.symbols.get(&decl_id) else { + continue; + }; + let (decl_scope, decl_kind) = (decl.scope_id, decl.kind); + if let Some(def) = self.symbols.get_mut(&def_id) { + def.scope_id = decl_scope; + def.kind = decl_kind; + } + // decl.type_ref → def: class_info/scope query đi tới definition. + if let Some(decl) = self.symbols.get_mut(&decl_id) { + decl.type_ref = def_id; + } + } + } + + /// 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 + /// (`go_receiver_type`) + method scope vào Interface symbol (language go). + fn build_go_method_sets(&mut self) { + // 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) + { + continue; + } + if let Some(recv) = &sym.type_name { + type_methods + .entry(recv.to_lowercase()) + .or_default() + .insert(sym.name.to_lowercase(), sym.sig_key.clone()); + } + 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(), sym.sig_key.clone())); + } + } + // 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, sk)| { + methods + .get(m) + .is_some_and(|type_sk| sig_matches(sk, type_sk)) + }) { + 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; + } + + /// 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 /// 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). @@ -1637,32 +2088,52 @@ impl GraphIndex { .cloned() .unwrap_or_default(); + // Namespace-qualified prefix (`orders::run`) — dùng cho preference + // lọc candidate bên dưới. + let ns_prefix = call + .call_name + .split_once("::") + .map(|(p, _)| p.trim().to_lowercase()) + .filter(|p| !p.is_empty()); + // 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); } - // 3b. Short name fallback (after last dot). + // 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 HOẶC `::` — qualified call + // C++ `orders::run` / Rust path không rơi vào import map). let short = call .call_name - .rsplit('.') - .next() + .rsplit(['.', ':']) + .find(|s| !s.is_empty()) .unwrap_or("") .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 +2146,251 @@ 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. + // 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). + // 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) + .and_then(|m| self.symbols.get(&m.scope_id)) + .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_dynamic { + let short = call.call_name.rsplit('.').next().unwrap_or(""); + return self.collect_fanout(&trait_hits, short, caller_id); + } + } + // Namespace-qualified (`orders::run`): nếu prefix khớp tổ tiên + // namespace hoặc file của MỘT subset candidate → chỉ giữ subset + // (tránh `foo::run` nhầm `bar::run`). + let mut candidates = candidates; + if let Some(prefix) = &ns_prefix + && candidates.len() > 1 + { + let preferred: Vec = candidates + .iter() + .copied() + .filter(|&id| self.namespace_matches(id, prefix)) + .collect(); + if !preferred.is_empty() && preferred.len() < candidates.len() { + candidates = preferred; + via_alias = true; + } + } + let kind = if via_alias { + ResolutionKind::Alias + } else { + ResolutionKind::Exact + }; + return CallResolution::Single( + self.pick_best_candidate(&candidates, caller_id, &call.call_name), + kind, + ); + } + + // 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). + match self.hierarchy_fanout(call, caller_id) { + CallResolution::None => {} + res => return res, + } + + // 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() + .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 từ các "interface source" (trait/interface Rust, + /// interface Java/Go, base class C++). + /// + /// - 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_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); + // 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(); + 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_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; + }; + if !matches!(m.kind, SymbolKind::Method | SymbolKind::Function) { + continue; + } + if let Some(tr) = self.symbols.get(&m.scope_id) + && tr.kind == SymbolKind::Interface + { + trait_hits.push((tr.id, mid)); + } + } + if trait_hits.is_empty() { + return CallResolution::None; } - Some(self.pick_best_candidate(&candidates, caller_id, &call.call_name)) + self.collect_fanout(&trait_hits, &short, caller_id) } /// Tìm method của class theo tên (scope_id == class id). @@ -1804,6 +2516,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 +2534,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 +2545,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 +2563,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 +2574,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 @@ -1900,6 +2639,78 @@ impl GraphIndex { Ok(()) } + /// Candidate có tổ tiên scope là Module (namespace) tên khớp prefix, + /// hoặc file chứa prefix (namespace thường trùng directory). + fn namespace_matches(&self, id: u64, prefix: &str) -> bool { + let mut cur = self.symbols.get(&id); + let mut hops = 0; + while let Some(sym) = cur + && hops < 8 + { + if sym.kind == SymbolKind::Module && sym.name.to_lowercase() == prefix { + return true; + } + cur = self.symbols.get(&sym.scope_id); + hops += 1; + } + self.symbols + .get(&id) + .is_some_and(|s| s.file.to_lowercase().contains(prefix)) + } + + /// 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 { + // 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(); + }; + 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 { + 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(""); + 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| { + 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) + } + }) + })); + } + } + 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 { @@ -2094,6 +2905,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; @@ -2221,6 +3037,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 +3064,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(), }); } @@ -2764,10 +3595,6 @@ impl GraphIndex { } } let mut handler_hit = None; - eprintln!( - "DBG3 cid={cid} cp={cp_pos} tries={open_tries:?} labels={:?} thrown={thrown_type:?}", - fr.branch_labels - ); for &ts in open_tries.iter().rev() { let te = matching_try_end(&fr.chain, ts) .unwrap_or(fr.chain.len()); @@ -4130,6 +4957,8 @@ mod tests { doc: None, annotations: Vec::new(), language: "test".to_string(), + impl_trait: None, + sig_key: None, } } @@ -4147,6 +4976,8 @@ mod tests { symbols, chains, calls, + fn_ptr_assigns: Vec::new(), + file_deps: None, } } @@ -4453,6 +5284,7 @@ mod tests { target_method: None, into_var: None, is_new_var: false, + ..Default::default() }, CallRecord { caller_id: SYMBOL_BASE, @@ -4468,6 +5300,7 @@ mod tests { target_method: None, into_var: None, is_new_var: false, + ..Default::default() }, ]; let r = result( @@ -4493,6 +5326,1316 @@ 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), + sig_key: None, + } + } + + #[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 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, + sig_key: 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, + sig_key: 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 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 + // 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 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" + ); + } + + 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 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 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 cpp_decl_def_linked_across_files() { + // .hpp: class Service { void run(); } (decl, không chain) + // .cpp: void Service::run() { work(); } (def, có chain) + // → def được re-parent về Service + reclassify Method; decl.type_ref → def. + let mut idx = GraphIndex::in_memory(); + let (svc, decl_id, work_id, def_id, caller_id) = ( + SYMBOL_BASE, + SYMBOL_BASE + 1, + SYMBOL_BASE + 2, + SYMBOL_BASE + 3, + SYMBOL_BASE + 4, + ); + let csym = + |id: u64, name: &str, kind: SymbolKind, scope_id: u64, file: &str, line: u32| Symbol { + id, + name: name.to_string(), + kind, + scope: ScopeLevel::Global, + scope_id, + type_ref: 0, + type_name: None, + file: file.to_string(), + line, + end_line: line, + signature: None, + doc: None, + annotations: Vec::new(), + language: "cpp".to_string(), + impl_trait: None, + sig_key: Some("()(void)".to_string()), + }; + let hpp = result( + "service.hpp", + vec![ + csym(svc, "Service", SymbolKind::Class, 0, "service.hpp", 1), + csym(decl_id, "run", SymbolKind::Method, svc, "service.hpp", 2), + ], + HashMap::new(), + vec![], + ); + let cpp = result( + "service.cpp", + vec![ + csym(def_id, "run", SymbolKind::Function, 0, "service.cpp", 5), + csym(work_id, "work", SymbolKind::Function, 0, "service.cpp", 1), + csym( + caller_id, + "caller", + SymbolKind::Function, + 0, + "service.cpp", + 10, + ), + ], + HashMap::from([ + (def_id, vec![def_id, work_id]), + (caller_id, vec![caller_id, 0]), + ]), + vec![CallRecord { + caller_id, + call_name: "svc.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(&[hpp, cpp], None).await.unwrap(); + + // Registry remap id theo thứ tự ingest — tra theo tên + chain thay vì id cứng. + let (def, decl) = { + let runs: Vec = idx + .symbols + .values() + .filter(|s| s.name == "run") + .cloned() + .collect(); + let def = runs + .iter() + .find(|s| idx.chains_map.contains_key(&s.id)) + .expect("def với chain"); + let decl = runs + .iter() + .find(|s| !idx.chains_map.contains_key(&s.id)) + .expect("decl"); + (def.clone(), decl.clone()) + }; + assert_eq!(def.kind, SymbolKind::Method, "def phải thành Method"); + assert_eq!(def.scope_id, svc, "def re-parent về Service"); + assert_eq!(decl.type_ref, def.id, "decl.type_ref phải trỏ def"); + // Call svc.run resolve về MỘT run method (Service không phải base — + // không phải virtual dispatch; điểm test là LINK, không phải fan-out). + // pick_best_candidate ưu tiên has-chain → chọn def (có body). + let flow = idx.flow(caller_id).await.unwrap(); + assert_eq!( + flow.calls[0].to_id, + Some(def.id), + "phải chọn def (has-chain)" + ); + } + + #[tokio::test] + async fn cpp_qualified_call_prefers_namespace() { + // 2 namespace cùng tên hàm `run` (orders/bar) — call `orders::run` + // phải ưu tiên candidate trong namespace orders. + let mut idx = GraphIndex::in_memory(); + let (ns_orders, run_orders, ns_bar, run_bar, caller_id) = ( + SYMBOL_BASE, + SYMBOL_BASE + 1, + SYMBOL_BASE + 2, + SYMBOL_BASE + 3, + SYMBOL_BASE + 4, + ); + let csym = + |id: u64, name: &str, kind: SymbolKind, scope_id: u64, file: &str, line: u32| Symbol { + id, + name: name.to_string(), + kind, + scope: ScopeLevel::Global, + scope_id, + type_ref: 0, + type_name: None, + file: file.to_string(), + line, + end_line: line, + signature: None, + doc: None, + annotations: Vec::new(), + language: "cpp".to_string(), + impl_trait: None, + sig_key: None, + }; + let r = result( + "main.cpp", + vec![ + csym( + ns_orders, + "orders", + SymbolKind::Module, + 0, + "orders/run.cpp", + 1, + ), + csym( + run_orders, + "run", + SymbolKind::Function, + ns_orders, + "orders/run.cpp", + 2, + ), + csym(ns_bar, "bar", SymbolKind::Module, 0, "bar/run.cpp", 1), + csym( + run_bar, + "run", + SymbolKind::Function, + ns_bar, + "bar/run.cpp", + 2, + ), + csym(caller_id, "main", SymbolKind::Function, 0, "main.cpp", 10), + ], + HashMap::from([(caller_id, vec![caller_id, 0])]), + vec![CallRecord { + caller_id, + call_name: "orders::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(); + + // Trước đây "::" không split được → unresolved. Giờ resolve vào orders.run. + let flow = idx.flow(caller_id).await.unwrap(); + assert_eq!( + flow.calls[0].to_id, + Some(run_orders), + "orders::run phải chọn run trong namespace orders, không phải bar" + ); + } + + #[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 +6654,7 @@ mod tests { target_method: None, into_var: None, is_new_var: false, + ..Default::default() }]; let r = result( "f.ts", @@ -4597,6 +6741,7 @@ mod tests { target_method: None, into_var: None, is_new_var: false, + ..Default::default() }]; let r = result( "f.ts", @@ -4634,6 +6779,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 +7423,7 @@ mod tests { target_method: None, into_var: None, is_new_var: false, + ..Default::default() }, CallRecord { caller_id: SYMBOL_BASE, @@ -5292,6 +7439,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 +7456,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 +7564,8 @@ mod tests { doc: None, annotations: Vec::new(), language: "java".into(), + impl_trait: None, + sig_key: None, }; let legacy_field = Symbol { id: 101, @@ -5431,6 +7582,8 @@ mod tests { doc: None, annotations: Vec::new(), language: "java".into(), + impl_trait: None, + sig_key: None, }; let legacy_do_work = Symbol { id: 102, @@ -5447,6 +7600,8 @@ mod tests { doc: None, annotations: Vec::new(), language: "java".into(), + impl_trait: None, + sig_key: None, }; let service = Symbol { id: 103, @@ -5463,6 +7618,8 @@ mod tests { doc: None, annotations: Vec::new(), language: "java".into(), + impl_trait: None, + sig_key: None, }; let service_do_work = Symbol { id: 104, @@ -5479,6 +7636,8 @@ mod tests { doc: None, 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. @@ -5498,6 +7657,8 @@ mod tests { doc: None, annotations: Vec::new(), language: "java".into(), + impl_trait: None, + sig_key: None, }; idx.ingest( &[ParseResult { @@ -5515,6 +7676,8 @@ mod tests { ], chains: HashMap::new(), calls: vec![], + fn_ptr_assigns: Vec::new(), + file_deps: None, }], None, ) diff --git a/crates/codegraph-graph/src/memtrack.rs b/crates/codegraph-graph/src/memtrack.rs index 3e486089b0..15f6ec2cf3 100644 --- a/crates/codegraph-graph/src/memtrack.rs +++ b/crates/codegraph-graph/src/memtrack.rs @@ -246,6 +246,8 @@ mod tests { doc: None, 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); @@ -265,6 +267,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..43a4204b98 100644 --- a/crates/codegraph-graph/src/shared.rs +++ b/crates/codegraph-graph/src/shared.rs @@ -341,6 +341,8 @@ mod tests { doc: None, annotations: Vec::new(), language: "ts".into(), + impl_trait: None, + sig_key: None, } } @@ -355,6 +357,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/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/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 624a5fcfd7..8bd39e913b 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(); @@ -930,6 +976,8 @@ mod tests { doc: None, 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(); @@ -948,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 0d5ac32f78..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", @@ -1061,5 +1145,7 @@ fn row_to_symbol(row: &MySqlRow) -> Result { doc, 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 02a89db89f..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", @@ -1078,5 +1163,7 @@ fn row_to_symbol(row: &PgRow) -> Result { doc, annotations, language, + impl_trait: None, + sig_key: None, }) } 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) 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/lmdb.rs b/crates/codegraph-graph/tests/lmdb.rs index bdabfe9e44..7978f7d11d 100644 --- a/crates/codegraph-graph/tests/lmdb.rs +++ b/crates/codegraph-graph/tests/lmdb.rs @@ -29,6 +29,8 @@ fn sym(file: &str, name: &str, id: u64) -> Symbol { doc: None, annotations: Vec::new(), language: "ts".to_string(), + impl_trait: None, + sig_key: None, } } @@ -46,6 +48,8 @@ fn result( symbols, chains, calls, + fn_ptr_assigns: Vec::new(), + file_deps: None, } } @@ -69,6 +73,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 +276,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..7f2e01bd7b 100644 --- a/crates/codegraph-graph/tests/rdbms.rs +++ b/crates/codegraph-graph/tests/rdbms.rs @@ -37,6 +37,8 @@ fn sym(file: &str, name: &str, id: u64) -> Symbol { doc: None, annotations: Vec::new(), language: "ts".to_string(), + impl_trait: None, + sig_key: None, } } @@ -54,6 +56,8 @@ fn result( symbols, chains, calls, + fn_ptr_assigns: Vec::new(), + file_deps: None, } } @@ -95,6 +99,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..1a9516c373 100644 --- a/crates/codegraph-graph/tests/redis.rs +++ b/crates/codegraph-graph/tests/redis.rs @@ -36,6 +36,8 @@ fn sym(file: &str, name: &str, id: u64) -> Symbol { doc: None, annotations: Vec::new(), language: "ts".to_string(), + impl_trait: None, + sig_key: None, } } @@ -53,6 +55,8 @@ fn result( symbols, chains, calls, + fn_ptr_assigns: Vec::new(), + file_deps: None, } } @@ -89,6 +93,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..b9ad521aa1 100644 --- a/crates/codegraph-graph/tests/sqlite.rs +++ b/crates/codegraph-graph/tests/sqlite.rs @@ -28,6 +28,8 @@ fn sym(file: &str, name: &str, id: u64) -> Symbol { doc: None, annotations: Vec::new(), language: "ts".to_string(), + impl_trait: None, + sig_key: None, } } @@ -52,6 +54,8 @@ fn result( symbols, chains, calls, + fn_ptr_assigns: Vec::new(), + file_deps: None, } } @@ -76,6 +80,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 +356,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", @@ -379,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"]); +} diff --git a/crates/codegraph-graph/tests/trace_flow_tests.rs b/crates/codegraph-graph/tests/trace_flow_tests.rs index afe6dbd388..bae00df6d4 100644 --- a/crates/codegraph-graph/tests/trace_flow_tests.rs +++ b/crates/codegraph-graph/tests/trace_flow_tests.rs @@ -30,6 +30,8 @@ fn sym(name: &str, id: u64) -> Symbol { file: "t.py".to_string(), line: 1, end_line: 1, + impl_trait: None, + sig_key: None, signature: None, doc: None, annotations: Vec::new(), @@ -51,6 +53,8 @@ fn result( symbols, chains, calls, + fn_ptr_assigns: Vec::new(), + file_deps: None, } } @@ -69,6 +73,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-graphql/src/mutation.rs b/crates/codegraph-graphql/src/mutation.rs index 2b3528fddb..1e9813689b 100644 --- a/crates/codegraph-graphql/src/mutation.rs +++ b/crates/codegraph-graphql/src/mutation.rs @@ -239,6 +239,63 @@ impl Mutation { }) } + /// Đọc/ghi `[[effect_rules]]` trong .codegraph/config.toml — khai báo + /// hành vi của call/macro (VD macro exception-like → panic). op: + /// list|add|remove; áp dụng từ lần index kế tiếp. + async fn config_effects( + &self, + ctx: &Context<'_>, + op: String, + matcher: Option, + text: Option, + effect: Option, + index: Option, + ) -> GqlResult { + use codegraph_extract::config; + let state = ctx.data::>()?; + let root = state + .session + .root() + .await + .ok_or_else(|| async_graphql::Error::new("session root unavailable"))?; + match op.as_str() { + "list" => { + let rules = config::list_effect_rules(&root); + if rules.is_empty() { + return Ok("No effect rules configured.".into()); + } + Ok(rules + .iter() + .enumerate() + .map(|(i, r)| format!("[{i}] {} {:?} → {}", r.matcher, r.text, r.effect)) + .collect::>() + .join("\n")) + } + "add" => config::add_effect_rule( + &root, + matcher.as_deref().unwrap_or(""), + text.as_deref().unwrap_or(""), + effect.as_deref().unwrap_or(""), + ) + .map_err(async_graphql::Error::new) + .map(|_| "Added. Applies from the next index.".to_string()), + "remove" => { + let idx = index.unwrap_or(-1); + if idx < 0 { + return Err(async_graphql::Error::new("remove requires index")); + } + match config::remove_effect_rule(&root, idx as usize) { + Ok(true) => Ok(format!("Removed effect rule [{idx}].")), + Ok(false) => Ok(format!("No rule at index {idx}.")), + Err(e) => Err(async_graphql::Error::new(e)), + } + } + other => Err(async_graphql::Error::new(format!( + "unknown op {other:?} — list|add|remove" + ))), + } + } + /// Xoá một document theo id. async fn graphdoc_remove(&self, ctx: &Context<'_>, id: ID) -> GqlResult { let state = ctx.data::>()?; diff --git a/crates/codegraph-mcp/src/callers_tests.rs b/crates/codegraph-mcp/src/callers_tests.rs index a4f74dbbbf..75488ff4c8 100644 --- a/crates/codegraph-mcp/src/callers_tests.rs +++ b/crates/codegraph-mcp/src/callers_tests.rs @@ -32,6 +32,8 @@ async fn callers_timeout_resume_dispatch() { doc: None, annotations: vec![], language: "rust".into(), + impl_trait: None, + sig_key: None, }; let mut idx = GraphIndex::open(&dsn).await.unwrap(); idx.ingest( @@ -43,6 +45,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-mcp/src/lib.rs b/crates/codegraph-mcp/src/lib.rs index fb45a87a4d..66d72a7477 100644 --- a/crates/codegraph-mcp/src/lib.rs +++ b/crates/codegraph-mcp/src/lib.rs @@ -275,6 +275,21 @@ impl CodegraphServer { let format = self.session.format().await; // Document tools — lazy doc graph (SharedDocGraph), không cần session // ready. Open giờ rẻ: `DocumentGraph::open` không materialize nodes. + // ── Effect-rule config (cần root, không cần index ready) ── + if name == "codegraph_config_effects" { + let root = self + .session + .root() + .await + .ok_or_else(|| McpError::invalid_params("session root unavailable", None))?; + return tools::dispatch_config_effects(&root, &args) + .map_err(|e| McpError::internal_error(e.to_string(), None)) + .map(|text| ToolOutput::Text { + text, + source_bytes: 0, + }); + } + if name.starts_with("codegraph_graphdoc_") { let doc_graph = self.doc_graph.clone(); return match name { diff --git a/crates/codegraph-mcp/src/tools.rs b/crates/codegraph-mcp/src/tools.rs index b2ef601c57..2d81dc508a 100644 --- a/crates/codegraph-mcp/src/tools.rs +++ b/crates/codegraph-mcp/src/tools.rs @@ -208,6 +208,17 @@ fn tool_defs() -> Vec { "Full re-index of the workspace into .codegraph/db.sqlite. Requires the workspace to be initialized (run codegraph_init first).", json!({ "type": "object", "properties": {} }), ), + tool( + "codegraph_config_effects", + "Read/write project effect rules in .codegraph/config.toml `[[effect_rules]]` — declare what a call/macro DOES (semantic classification), e.g. treat a macro like an exception. Rules are checked FIRST (before the built-in defaults, first-match-wins) at extraction time: each rule maps a call-name matcher to an effect. op: 'list' returns current rules (index order = removal index); 'add' appends {matcher: 'contains'|'prefix'|'exact', text, effect} — effect is snake_case (panic, sql_query, sql_write, cache_read, cache_write, http_call, event_emit, file_read, file_write, log, none); 'remove' deletes by index. Rules take effect from the NEXT codegraph_index (the re-index re-classifies call records); the current thread applies them immediately. Example: {op:'add', matcher:'contains', text:'CHECK', effect:'panic'} makes every CHECK_* macro invocation classify as an exception-like terminal call.", + json!({ "type": "object", "properties": { + "op": { "type": "string", "enum": ["list", "add", "remove"] }, + "matcher": { "type": "string", "enum": ["contains", "prefix", "exact"] }, + "text": { "type": "string" }, + "effect": { "type": "string" }, + "index": { "type": "integer" } + }, "required": ["op"] }), + ), // ── Shared symbol search (code + binary datasets gộp trong một call) ── tool( "codegraph_search_symbol", @@ -1851,6 +1862,56 @@ pub async fn dispatch_doc_ingest_dir( emit_value("", summary) } +/// codegraph_config_effects: đọc/ghi `[[effect_rules]]` cho root hiện tại. +pub fn dispatch_config_effects( + root: &camino::Utf8Path, + args: &serde_json::Value, +) -> codegraph_core::Result { + use codegraph_extract::config; + let op = args + .get("op") + .and_then(|v| v.as_str()) + .ok_or_else(|| codegraph_core::Error::Invalid("op is required".into()))?; + match op { + "list" => { + let rules = config::list_effect_rules(root); + if rules.is_empty() { + return Ok("No effect rules configured.".into()); + } + let lines: Vec = rules + .iter() + .enumerate() + .map(|(i, r)| format!("[{i}] {} {:?} → {}", r.matcher, r.text, r.effect)) + .collect(); + Ok(lines.join("\n")) + } + "add" => { + let matcher = args.get("matcher").and_then(|v| v.as_str()).unwrap_or(""); + let text = args.get("text").and_then(|v| v.as_str()).unwrap_or(""); + let effect = args.get("effect").and_then(|v| v.as_str()).unwrap_or(""); + config::add_effect_rule(root, matcher, text, effect) + .map_err(codegraph_core::Error::Invalid)?; + Ok(format!( + "Added effect rule: {matcher} {text:?} → {effect}. Applies from the next codegraph_index." + )) + } + "remove" => { + let idx = + args.get("index").and_then(|v| v.as_u64()).ok_or_else(|| { + codegraph_core::Error::Invalid("remove requires `index`".into()) + })? as usize; + match config::remove_effect_rule(root, idx) { + Ok(true) => Ok(format!("Removed effect rule [{idx}].")), + Ok(false) => Ok(format!("No rule at index {idx}.")), + Err(e) => Err(codegraph_core::Error::Invalid(e)), + } + } + other => Err(codegraph_core::Error::Invalid(format!( + "unknown op {other:?} — list|add|remove" + ))), + } +} + pub async fn dispatch_doc_remove( doc_graph: Arc, doc_id: u64, diff --git a/crates/codegraph-sboxes/tests/control_flow.rs b/crates/codegraph-sboxes/tests/control_flow.rs index 25766fe08e..0cdb39937a 100644 --- a/crates/codegraph-sboxes/tests/control_flow.rs +++ b/crates/codegraph-sboxes/tests/control_flow.rs @@ -30,6 +30,8 @@ fn sym(id: u64, name: &str) -> Symbol { doc: None, annotations: Vec::new(), language: "test".to_string(), + impl_trait: None, + sig_key: None, } } @@ -48,6 +50,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() } } @@ -65,6 +68,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 0e37219987..10909f874a 100644 --- a/crates/codegraph-sboxes/tests/end_to_end.rs +++ b/crates/codegraph-sboxes/tests/end_to_end.rs @@ -26,6 +26,8 @@ fn sym(id: u64, name: &str) -> Symbol { doc: None, annotations: Vec::new(), language: "test".to_string(), + impl_trait: None, + sig_key: None, } } @@ -44,6 +46,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() } } @@ -61,6 +64,8 @@ fn result( symbols, chains, calls, + fn_ptr_assigns: Vec::new(), + file_deps: None, } } diff --git a/packaging/aur/codegraph-rs-bin/PKGBUILD b/packaging/aur/codegraph-rs-bin/PKGBUILD index 23cefc14ab..0b7e1d14eb 100644 --- a/packaging/aur/codegraph-rs-bin/PKGBUILD +++ b/packaging/aur/codegraph-rs-bin/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: Hung Pham pkgname=codegraph-rs-bin -pkgver=2.2.10 +pkgver=2.2.11 pkgrel=1 pkgdesc="Local-first code intelligence: tree-sitter knowledge graph + MCP server (prebuilt binary)" arch=('x86_64' 'aarch64') diff --git a/packaging/choco/codegraph.nuspec b/packaging/choco/codegraph.nuspec index f0a82b44cf..aa1a39dc77 100644 --- a/packaging/choco/codegraph.nuspec +++ b/packaging/choco/codegraph.nuspec @@ -2,7 +2,7 @@ codegraph - 2.2.10 + 2.2.11 codegraph Hung Pham https://github.com/hungpham10/codegraph-rs diff --git a/packaging/winget/codegraph.yaml b/packaging/winget/codegraph.yaml index 853395ba4f..88eff5fb81 100644 --- a/packaging/winget/codegraph.yaml +++ b/packaging/winget/codegraph.yaml @@ -6,7 +6,7 @@ # release time (or automate it in the release pipeline before submitting to # microsoft/winget-pkgs). PackageIdentifier: hungpham10.codegraph -PackageVersion: 2.2.10 +PackageVersion: 2.2.11 PackageName: codegraph Publisher: Hung Pham PublisherUrl: https://github.com/hungpham10/codegraph-rs @@ -18,7 +18,7 @@ PackageUrl: https://github.com/hungpham10/codegraph-rs InstallerType: zip Installers: - Architecture: x64 - InstallerUrl: https://github.com/hungpham10/codegraph-rs/releases/download/v2.2.10/codegraph-x86_64-pc-windows-msvc.zip + InstallerUrl: https://github.com/hungpham10/codegraph-rs/releases/download/v2.2.11/codegraph-x86_64-pc-windows-msvc.zip InstallerSha256: 0000000000000000000000000000000000000000000000000000000000000000 InstallerType: zip ManifestType: singleton diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 8dad5dde37..f46312e556 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -5,11 +5,11 @@ # # Usage (pin a version / download the script first): # irm https://raw.githubusercontent.com/hungpham10/codegraph-rs/main/scripts/install.ps1 -OutFile install.ps1 -# .\install.ps1 -Version 2.2.10 +# .\install.ps1 -Version 2.2.11 [CmdletBinding()] param( - # Pin a specific version, e.g. "2.2.10". Empty = latest release. + # Pin a specific version, e.g. "2.2.11". Empty = latest release. [string]$Version ) 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 @@