From 8347d3b4a24b40770f31660a146be965f8715770 Mon Sep 17 00:00:00 2001 From: Hung Pham Date: Sun, 6 Sep 2026 22:59:57 +0700 Subject: [PATCH] Implement lambda detection into graph --- crates/codegraph-extract/src/languages/c.rs | 2 + .../codegraph-extract/src/languages/common.rs | 59 ++++- crates/codegraph-extract/src/languages/cpp.rs | 2 + .../codegraph-extract/src/languages/csharp.rs | 2 + crates/codegraph-extract/src/languages/go.rs | 28 ++- .../codegraph-extract/src/languages/java.rs | 2 + .../src/languages/javascript.rs | 20 ++ crates/codegraph-extract/src/languages/lua.rs | 22 +- crates/codegraph-extract/src/languages/php.rs | 28 ++- .../codegraph-extract/src/languages/python.rs | 20 +- .../codegraph-extract/src/languages/ruby.rs | 2 + .../codegraph-extract/src/languages/rust.rs | 2 + .../codegraph-extract/src/languages/scala.rs | 2 + .../codegraph-extract/src/languages/swift.rs | 2 + .../src/languages/typescript.rs | 2 + crates/codegraph-extract/tests/chains.rs | 207 ++++++++++++++++++ 16 files changed, 393 insertions(+), 9 deletions(-) diff --git a/crates/codegraph-extract/src/languages/c.rs b/crates/codegraph-extract/src/languages/c.rs index 935353abed..7b808cd558 100644 --- a/crates/codegraph-extract/src/languages/c.rs +++ b/crates/codegraph-extract/src/languages/c.rs @@ -26,6 +26,8 @@ pub static SPEC: LangSpec = LangSpec { name_type_fallback: false, link_impl_methods: false, + anonymous_name_fn: None, + value_func_kinds: &[], calls: &[CallRule { kind: "call_expression", callee_field: "function", diff --git a/crates/codegraph-extract/src/languages/common.rs b/crates/codegraph-extract/src/languages/common.rs index e317d02f82..fa30e415c8 100644 --- a/crates/codegraph-extract/src/languages/common.rs +++ b/crates/codegraph-extract/src/languages/common.rs @@ -33,6 +33,10 @@ 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; +/// 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>; + /// Một call-site rule: node kind nào là call + callee field + cách lấy tên. #[derive(Clone, Copy)] pub struct CallRule { @@ -73,6 +77,13 @@ 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(){}`). + pub anonymous_name_fn: Option, + /// Node kind của giá trị gán là hàm anonymous — decl Variable/Constant chứa + /// nó bị bỏ để tránh trùng tên với Function sinh từ `anonymous_name_fn`. + pub value_func_kinds: &'static [&'static str], // ── marker rules ── pub if_kinds: &'static [&'static str], pub elif_kinds: &'static [&'static str], @@ -224,6 +235,13 @@ fn push_symbol( kind: SymbolKind, node_kind: &str, ) -> Option { + // Declarator gán hàm anonymous (`const a = () => {}`, `var h = func(){}`): + // bỏ symbol Variable — hàm bên trong sẽ được đặt tên qua anonymous_name_fn, + // tránh 2 symbol trùng tên (Variable + Function). + if matches!(kind, SymbolKind::Variable | SymbolKind::Constant) && decl_value_is_func(node, spec) + { + return None; + } // C/C++: macro attribute trước qualified ctor (`_CUSTOM_ATTRIBUTE // CustomWidget::CustomWidget(...)`) làm tree-sitter đánh ERROR — field // `declarator` chỉ vào init_declarator sai; tên ctor nằm trong function_declarator @@ -244,8 +262,10 @@ fn push_symbol( .or_else(|| { // Anonymous function/class (JS `export default function() {}`, // C anonymous struct) — first_identifier trong body là nhiễu, bỏ qua. + // 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) { - None + spec.anonymous_name_fn.and_then(|f| f(node)) } else { first_identifier(node) } @@ -513,7 +533,7 @@ fn collect_chains( calls: &mut Vec, ) { if spec.func_kinds.contains(&root.kind()) { - if let Some(id) = func_id_of(root, src, func_index) { + if let Some(id) = func_id_of(root, src, spec, func_index) { let (chain, mut cs) = build_chain(root, src, spec, id); chains.insert(id, chain); calls.append(&mut cs); @@ -531,10 +551,18 @@ fn collect_chains( } } -fn func_id_of(node: &Node, src: &[u8], func_index: &HashMap<(String, u32), u64>) -> Option { +fn func_id_of<'a>( + node: &Node<'a>, + src: &[u8], + spec: &'static LangSpec, + func_index: &HashMap<(String, u32), u64>, +) -> Option { + // Phải khớp push_symbol về (name, line): name field → declarator → + // anonymous_name_fn (hàm gán qua biến) → first_identifier. 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(|| first_identifier(node))?; let name = text(&name_node, src)?; let line = name_node.start_position().row as u32 + 1; @@ -1155,8 +1183,31 @@ fn is_conversion_declarator(n: &Node) -> bool { .unwrap_or(false) } +/// Decl Variable/Constant có giá trị là hàm anonymous? Chỉ đi qua các wrapper +/// 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() { + return false; + } + decl_value_is_func_at(node, spec, 0) +} + +fn decl_value_is_func_at(node: &Node, spec: &LangSpec, depth: u32) -> bool { + if depth > 4 { + return false; + } + named_children(node).into_iter().any(|ch| { + spec.value_func_kinds.contains(&ch.kind()) + || (matches!( + ch.kind(), + "expression_list" | "assignment_statement" | "variable_list" + ) && decl_value_is_func_at(&ch, spec, depth + 1)) + }) +} + /// DFS tìm identifier đầu tiên trong subtree. -fn first_identifier<'a>(n: &Node<'a>) -> Option> { +pub fn first_identifier<'a>(n: &Node<'a>) -> Option> { let mut stack = vec![*n]; while let Some(node) = stack.pop() { if matches!( diff --git a/crates/codegraph-extract/src/languages/cpp.rs b/crates/codegraph-extract/src/languages/cpp.rs index 6d0c531060..1ea4de59bd 100644 --- a/crates/codegraph-extract/src/languages/cpp.rs +++ b/crates/codegraph-extract/src/languages/cpp.rs @@ -28,6 +28,8 @@ pub static SPEC: LangSpec = LangSpec { name_type_fallback: false, link_impl_methods: false, + anonymous_name_fn: None, + value_func_kinds: &[], calls: &[CallRule { kind: "call_expression", callee_field: "function", diff --git a/crates/codegraph-extract/src/languages/csharp.rs b/crates/codegraph-extract/src/languages/csharp.rs index 2beb3384bb..63bcf7ca36 100644 --- a/crates/codegraph-extract/src/languages/csharp.rs +++ b/crates/codegraph-extract/src/languages/csharp.rs @@ -45,6 +45,8 @@ pub static SPEC: LangSpec = LangSpec { name_type_fallback: false, link_impl_methods: false, + anonymous_name_fn: None, + value_func_kinds: &[], calls: &[ CallRule { kind: "invocation_expression", diff --git a/crates/codegraph-extract/src/languages/go.rs b/crates/codegraph-extract/src/languages/go.rs index 6ed9c1fe95..aab9a5e54b 100644 --- a/crates/codegraph-extract/src/languages/go.rs +++ b/crates/codegraph-extract/src/languages/go.rs @@ -1,10 +1,31 @@ -use crate::languages::common::{CallRule, LangSpec}; +use crate::languages::common::{first_identifier, CallRule, LangSpec}; use codegraph_core::SymbolKind; +use tree_sitter::Node; fn ts_language() -> tree_sitter::Language { tree_sitter_go::LANGUAGE.into() } +/// `f := func(){}` / `var h = func(){}` — func_literal mượn tên biến. +/// func_literal truyền thẳng (`go func(){ }()`) không được đặt tên. +fn anonymous_name_node<'a>(node: &Node<'a>) -> Option> { + let el = node.parent()?; + if el.kind() != "expression_list" { + return None; + } + let p = el.parent()?; + match p.kind() { + // `var h = func(){ }` — tên ở field `name` của var_spec. + "var_spec" => p.child_by_field_name("name"), + // `f := func(){ }` — tên là identifier đầu của expression_list left. + "short_var_declaration" => { + let left = p.child_by_field_name("left")?; + first_identifier(&left) + } + _ => None, + } +} + pub static SPEC: LangSpec = LangSpec { language_name: "go", extensions: &["go"], @@ -16,14 +37,17 @@ pub static SPEC: LangSpec = LangSpec { ("var_spec", SymbolKind::Variable), ("const_spec", SymbolKind::Constant), ("parameter_declaration", SymbolKind::Parameter), + ("func_literal", SymbolKind::Function), ], - func_kinds: &["function_declaration", "method_declaration"], + func_kinds: &["function_declaration", "method_declaration", "func_literal"], class_kinds: &[], 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", diff --git a/crates/codegraph-extract/src/languages/java.rs b/crates/codegraph-extract/src/languages/java.rs index 2aeecdba49..43b67a26e3 100644 --- a/crates/codegraph-extract/src/languages/java.rs +++ b/crates/codegraph-extract/src/languages/java.rs @@ -89,6 +89,8 @@ pub static SPEC: LangSpec = LangSpec { name_type_fallback: false, link_impl_methods: false, + anonymous_name_fn: None, + value_func_kinds: &[], calls: &[ CallRule { kind: "method_invocation", diff --git a/crates/codegraph-extract/src/languages/javascript.rs b/crates/codegraph-extract/src/languages/javascript.rs index d8ce7db4ba..77fc7b5783 100644 --- a/crates/codegraph-extract/src/languages/javascript.rs +++ b/crates/codegraph-extract/src/languages/javascript.rs @@ -20,6 +20,24 @@ pub fn class_type_name(node: &Node, src: &[u8]) -> Option { None } +/// Hàm anonymous gán qua ngữ cảnh — mượn tên từ nơi gán: +/// `var a = function(){}` / `const f = () => {}` (declarator name), +/// `obj.foo = function(){}` (left), `{ foo: function(){} }` (pair key). +pub fn anonymous_name_node<'a>(node: &Node<'a>) -> Option> { + let p = node.parent()?; + let (value_field, name_field) = match p.kind() { + "variable_declarator" => ("value", "name"), + "assignment_expression" => ("right", "left"), + "pair" | "property" => ("value", "key"), + _ => return None, + }; + let value = p.child_by_field_name(value_field)?; + if value.id() != node.id() { + return None; + } + p.child_by_field_name(name_field) +} + pub static SPEC: LangSpec = LangSpec { language_name: "javascript", extensions: &["js", "jsx", "mjs", "cjs"], @@ -47,6 +65,8 @@ pub static SPEC: LangSpec = LangSpec { name_type_fallback: false, link_impl_methods: false, + anonymous_name_fn: Some(anonymous_name_node), + value_func_kinds: &["function_expression", "arrow_function"], calls: &[ CallRule { kind: "call_expression", diff --git a/crates/codegraph-extract/src/languages/lua.rs b/crates/codegraph-extract/src/languages/lua.rs index 527672640a..8158c9c174 100644 --- a/crates/codegraph-extract/src/languages/lua.rs +++ b/crates/codegraph-extract/src/languages/lua.rs @@ -1,10 +1,28 @@ -use crate::languages::common::{CallRule, LangSpec}; +use crate::languages::common::{named_children, CallRule, LangSpec}; use codegraph_core::SymbolKind; +use tree_sitter::Node; fn ts_language() -> tree_sitter::Language { tree_sitter_lua::LANGUAGE.into() } +/// `f = function() end` / `local f = function() end` — function_definition +/// anonymous mượn tên từ variable_list của assignment_statement. +fn anonymous_name_node<'a>(node: &Node<'a>) -> Option> { + let el = node.parent()?; + if el.kind() != "expression_list" { + return None; + } + let stmt = el.parent()?; + if stmt.kind() != "assignment_statement" { + return None; + } + named_children(&stmt) + .into_iter() + .find(|c| c.kind() == "variable_list") + .and_then(|vl| vl.child_by_field_name("name")) +} + pub static SPEC: LangSpec = LangSpec { language_name: "lua", extensions: &["lua"], @@ -27,6 +45,8 @@ pub static SPEC: LangSpec = LangSpec { name_type_fallback: false, link_impl_methods: false, + anonymous_name_fn: Some(anonymous_name_node), + value_func_kinds: &["function_definition"], calls: &[CallRule { kind: "function_call", callee_field: "name", diff --git a/crates/codegraph-extract/src/languages/php.rs b/crates/codegraph-extract/src/languages/php.rs index ceb2b125ef..df2436abba 100644 --- a/crates/codegraph-extract/src/languages/php.rs +++ b/crates/codegraph-extract/src/languages/php.rs @@ -37,6 +37,23 @@ fn member_call_name(node: &Node, src: &[u8]) -> Option { Some(name) } +/// `$f = function() {}` / `$h = fn() => ...` — hàm anonymous mượn tên biến +/// (node `name` bên trong variable_name, bỏ `$` prefix). +fn anonymous_name_node<'a>(node: &Node<'a>) -> Option> { + let p = node.parent()?; + if p.kind() != "assignment_expression" { + return None; + } + let right = p.child_by_field_name("right")?; + if right.id() != node.id() { + return None; + } + let left = p.child_by_field_name("left")?; + let mut cursor = left.walk(); + let name_node = left.children(&mut cursor).find(|c| c.kind() == "name"); + name_node.or(Some(left)) +} + pub static SPEC: LangSpec = LangSpec { language_name: "php", extensions: &["php"], @@ -54,8 +71,15 @@ pub static SPEC: LangSpec = LangSpec { ("const_declaration", SymbolKind::Constant), ("simple_parameter", SymbolKind::Parameter), ("property_promotion_parameter", SymbolKind::Parameter), + ("anonymous_function", SymbolKind::Function), + ("arrow_function", SymbolKind::Function), + ], + func_kinds: &[ + "function_definition", + "method_declaration", + "anonymous_function", + "arrow_function", ], - func_kinds: &["function_definition", "method_declaration"], class_kinds: &[ "class_declaration", "interface_declaration", @@ -67,6 +91,8 @@ pub static SPEC: LangSpec = LangSpec { 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", diff --git a/crates/codegraph-extract/src/languages/python.rs b/crates/codegraph-extract/src/languages/python.rs index 0cb4cb0932..d66d57a0dc 100644 --- a/crates/codegraph-extract/src/languages/python.rs +++ b/crates/codegraph-extract/src/languages/python.rs @@ -1,10 +1,25 @@ use crate::languages::common::{CallRule, LangSpec}; use codegraph_core::SymbolKind; +use tree_sitter::Node; fn ts_language() -> tree_sitter::Language { tree_sitter_python::LANGUAGE.into() } +/// `f = lambda: ...` — lambda mượn tên biến ở vế trái assignment. +/// Lambda truyền thẳng (vd `map(lambda: 1, ...)`) không được đặt tên. +fn anonymous_name_node<'a>(node: &Node<'a>) -> Option> { + let p = node.parent()?; + if p.kind() != "assignment" { + return None; + } + let right = p.child_by_field_name("right")?; + if right.id() != node.id() { + return None; + } + p.child_by_field_name("left") +} + pub static SPEC: LangSpec = LangSpec { language_name: "python", extensions: &["py", "pyi"], @@ -12,14 +27,17 @@ pub static SPEC: LangSpec = LangSpec { decls: &[ ("function_definition", SymbolKind::Function), ("class_definition", SymbolKind::Class), + ("lambda", SymbolKind::Function), ], - func_kinds: &["function_definition"], + func_kinds: &["function_definition", "lambda"], class_kinds: &["class_definition"], param_kinds: &[], annotation_kinds: &[], name_type_fallback: false, link_impl_methods: false, + anonymous_name_fn: Some(anonymous_name_node), + value_func_kinds: &[], calls: &[CallRule { kind: "call", callee_field: "function", diff --git a/crates/codegraph-extract/src/languages/ruby.rs b/crates/codegraph-extract/src/languages/ruby.rs index f2407edd44..c39ff31291 100644 --- a/crates/codegraph-extract/src/languages/ruby.rs +++ b/crates/codegraph-extract/src/languages/ruby.rs @@ -44,6 +44,8 @@ pub static SPEC: LangSpec = LangSpec { name_type_fallback: false, link_impl_methods: false, + anonymous_name_fn: None, + value_func_kinds: &[], calls: &[ CallRule { kind: "call", diff --git a/crates/codegraph-extract/src/languages/rust.rs b/crates/codegraph-extract/src/languages/rust.rs index 240f46ef75..689ae8bdc0 100644 --- a/crates/codegraph-extract/src/languages/rust.rs +++ b/crates/codegraph-extract/src/languages/rust.rs @@ -34,6 +34,8 @@ pub static SPEC: LangSpec = LangSpec { 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", diff --git a/crates/codegraph-extract/src/languages/scala.rs b/crates/codegraph-extract/src/languages/scala.rs index 581e9173df..be1c3f26ba 100644 --- a/crates/codegraph-extract/src/languages/scala.rs +++ b/crates/codegraph-extract/src/languages/scala.rs @@ -32,6 +32,8 @@ pub static SPEC: LangSpec = LangSpec { name_type_fallback: false, link_impl_methods: false, + anonymous_name_fn: None, + value_func_kinds: &[], calls: &[CallRule { kind: "call_expression", callee_field: "function", diff --git a/crates/codegraph-extract/src/languages/swift.rs b/crates/codegraph-extract/src/languages/swift.rs index e90d63a54d..1e774455e9 100644 --- a/crates/codegraph-extract/src/languages/swift.rs +++ b/crates/codegraph-extract/src/languages/swift.rs @@ -37,6 +37,8 @@ pub static SPEC: LangSpec = LangSpec { 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). diff --git a/crates/codegraph-extract/src/languages/typescript.rs b/crates/codegraph-extract/src/languages/typescript.rs index 647fc0c2af..9794995847 100644 --- a/crates/codegraph-extract/src/languages/typescript.rs +++ b/crates/codegraph-extract/src/languages/typescript.rs @@ -72,6 +72,8 @@ pub static SPEC: LangSpec = LangSpec { name_type_fallback: false, link_impl_methods: false, + anonymous_name_fn: Some(crate::languages::javascript::anonymous_name_node), + value_func_kinds: &["function_expression", "arrow_function"], calls: &[ CallRule { kind: "call_expression", diff --git a/crates/codegraph-extract/tests/chains.rs b/crates/codegraph-extract/tests/chains.rs index c4a52665bb..d6608c53e1 100644 --- a/crates/codegraph-extract/tests/chains.rs +++ b/crates/codegraph-extract/tests/chains.rs @@ -917,3 +917,210 @@ function runStorage(op: string, s: Store): void { ] ); } + +// ==================== Hàm anonymous gán qua biến (lambda) ==================== + +fn parse(lang: &str, src: &str) -> codegraph_graph::ParseResult { + let parser = registry() + .into_iter() + .find(|p| p.name() == lang) + .unwrap_or_else(|| panic!("no parser {lang}")); + parser.parse_file("anon.test", src).expect("parse") +} + +fn find<'a>(res: &'a codegraph_graph::ParseResult, name: &str) -> &'a codegraph_core::Symbol { + res.symbols + .iter() + .find(|s| s.name == name) + .unwrap_or_else(|| panic!("symbol `{name}` không tồn tại")) +} + +#[test] +fn js_var_assigned_function_expression() { + let res = parse( + "javascript", + r#" +var a = function(){ b(); }; +function b(){ c(); } +a(); +"#, + ); + let a = find(&res, "a"); + assert!( + matches!(a.kind, SymbolKind::Function), + "kind = {:?}", + a.kind + ); + // Declarator không còn push Variable `a` trùng tên với Function. + assert!(res + .symbols + .iter() + .all(|s| s.name != "a" || s.kind == SymbolKind::Function)); + // Chain của `a` chứa call `b`. + assert!(res + .calls + .iter() + .any(|c| c.caller_id == a.id && c.call_name == "b")); +} + +#[test] +fn js_const_arrow_chain() { + let c = walk("javascript", "const f = () => g();\n"); + assert_eq!(c, ["g"]); +} + +#[test] +fn ts_const_arrow_chain() { + let c = walk("typescript", "const f = (): void => g();\n"); + assert_eq!(c, ["g"]); +} + +#[test] +fn js_assignment_and_object_literal_functions() { + let res = parse( + "javascript", + r#" +obj.foo = function(){ helper(); }; +const conf = { setup: function(){ init(); } }; +"#, + ); + for name in ["obj.foo", "setup"] { + let s = find(&res, name); + assert!( + matches!(s.kind, SymbolKind::Function), + "{name}: kind = {:?}", + s.kind + ); + } + // Object literal không phải hàm — `conf` vẫn là Variable. + assert!(matches!(find(&res, "conf").kind, SymbolKind::Variable)); +} + +#[test] +fn js_regression_plain_variable_and_inline_arrow() { + let res = parse("javascript", "const x = 5;\nsetTimeout(() => {});\n"); + // `x` vẫn là Variable. + assert!(matches!(find(&res, "x").kind, SymbolKind::Variable)); + // Lambda truyền thẳng không sinh symbol Function rác. + assert!(!res + .symbols + .iter() + .any(|s| matches!(s.kind, SymbolKind::Function))); +} + +#[tokio::test] +async fn js_var_assigned_function_resolves_through_ingest() { + let res = parse( + "javascript", + r#" +var a = function(){ b(); }; +function b(){ c(); } +a(); +"#, + ); + let a_id = find(&res, "a").id; + let mut idx = codegraph_graph::GraphIndex::in_memory(); + idx.ingest(&[res]).await.unwrap(); + let callees = idx.callees(a_id).await.unwrap(); + assert!( + callees.iter().any(|s| s.name == "b"), + "call `a()` phải resolve tới `b`" + ); +} + +#[test] +fn python_assigned_lambda() { + let res = parse("python", "f = lambda: g()\n"); + let f = find(&res, "f"); + assert!( + matches!(f.kind, SymbolKind::Function), + "kind = {:?}", + f.kind + ); + assert!(res + .calls + .iter() + .any(|c| c.caller_id == f.id && c.call_name == "g")); +} + +#[test] +fn python_regression_inline_lambda_and_plain_assign() { + let res = parse("python", "x = 5\nmap(lambda: 1, [])\n"); + // Không có decl → không symbol nào; lambda inline không được đặt tên. + assert!(res.symbols.is_empty()); +} + +#[test] +fn go_func_literal_assigned() { + let res = parse( + "go", + r#" +package main +var h = func(){ } +func main() { + f := func(){ g() } + go func(){ }() +} +"#, + ); + // h (var) + f (:=) + main — func_literal goroutine inline không được đặt tên. + let funcs: Vec<&str> = res + .symbols + .iter() + .filter(|s| matches!(s.kind, SymbolKind::Function)) + .map(|s| s.name.as_str()) + .collect(); + assert_eq!(funcs, ["h", "main", "f"]); + // `var h` không còn Variable trùng tên. + assert!(!res + .symbols + .iter() + .any(|s| s.name == "h" && s.kind == SymbolKind::Variable)); + let f = find(&res, "f"); + assert!(res + .calls + .iter() + .any(|c| c.caller_id == f.id && c.call_name == "g")); +} + +#[test] +fn lua_assigned_anonymous_function() { + let res = parse("lua", "local f = function() g() end\nh = function() end\n"); + for name in ["f", "h"] { + let s = find(&res, name); + assert!( + matches!(s.kind, SymbolKind::Function), + "{name}: kind = {:?}", + s.kind + ); + } + let f = find(&res, "f"); + assert!(res + .calls + .iter() + .any(|c| c.caller_id == f.id && c.call_name == "g")); + assert!(!res + .symbols + .iter() + .any(|s| s.name == "f" && s.kind == SymbolKind::Variable)); +} + +#[test] +fn php_assigned_anonymous_and_arrow() { + let res = parse( + "php", + " h2();\n", + ); + let f = find(&res, "f"); + let h = find(&res, "h"); + assert!(matches!(f.kind, SymbolKind::Function)); + assert!(matches!(h.kind, SymbolKind::Function)); + assert!(res + .calls + .iter() + .any(|c| c.caller_id == f.id && c.call_name == "g")); + assert!(res + .calls + .iter() + .any(|c| c.caller_id == h.id && c.call_name == "h2")); +}