diff --git a/crates/codegraph-core/src/extractors/csharp.rs b/crates/codegraph-core/src/extractors/csharp.rs index 5851420c..8d3f45ae 100644 --- a/crates/codegraph-core/src/extractors/csharp.rs +++ b/crates/codegraph-core/src/extractors/csharp.rs @@ -221,40 +221,100 @@ fn handle_using_directive(node: &Node, source: &[u8], symbols: &mut FileSymbols) } } +/// Get the first string-literal argument text from a C# invocation node. +fn get_cs_first_string_arg(node: &Node, source: &[u8]) -> Option { + let args = node.child_by_field_name("argument_list") + .or_else(|| find_child(node, "argument_list"))?; + for i in 0..args.child_count() { + let Some(child) = args.child(i) else { continue }; + let t = child.kind(); + if matches!(t, "(" | ")" | ",") { continue; } + // argument node may wrap the literal + let target = if t == "argument" { + child.child(0).unwrap_or(child) + } else { + child + }; + let tt = target.kind(); + if tt == "string_literal" || tt == "verbatim_string_literal" { + let raw = node_text(&target, source); + return Some(raw.trim_matches(|c| c == '"' || c == '@').to_string()); + } + break; + } + None +} + fn handle_invocation_expr(node: &Node, source: &[u8], symbols: &mut FileSymbols) { let fn_node = node.child_by_field_name("function").or_else(|| node.child(0)); let Some(fn_node) = fn_node else { return }; + let call_line = start_line(node); match fn_node.kind() { "identifier" => { symbols.calls.push(Call { name: node_text(&fn_node, source).to_string(), - line: start_line(node), - dynamic: None, - receiver: None, + line: call_line, ..Default::default() }); } "member_access_expression" => { - if let Some(name) = fn_node.child_by_field_name("name") { - let receiver = named_child_text(&fn_node, "expression", source) - .map(|s| s.to_string()); + let Some(name) = fn_node.child_by_field_name("name") else { return }; + let method_name = node_text(&name, source); + let receiver = named_child_text(&fn_node, "expression", source) + .map(|s| s.to_string()); + + // method.Invoke(target, args) — runtime reflection; target unknown + if method_name == "Invoke" { symbols.calls.push(Call { - name: node_text(&name, source).to_string(), - line: start_line(node), - dynamic: None, + name: "".to_string(), + line: call_line, + dynamic: Some(true), + dynamic_kind: Some("unresolved-dynamic".to_string()), receiver, ..Default::default() }); + return; } + + // type.GetMethod("name") / GetRuntimeMethod / GetDeclaredMethod — resolvable if literal + if matches!(method_name, "GetMethod" | "GetRuntimeMethod" | "GetDeclaredMethod") { + let literal = get_cs_first_string_arg(node, source); + if let Some(lit) = literal { + symbols.calls.push(Call { + name: lit.clone(), + line: call_line, + dynamic: Some(true), + dynamic_kind: Some("reflection".to_string()), + key_expr: Some(lit), + receiver, + ..Default::default() + }); + } else { + symbols.calls.push(Call { + name: "".to_string(), + line: call_line, + dynamic: Some(true), + dynamic_kind: Some("computed-key".to_string()), + receiver, + ..Default::default() + }); + } + return; + } + + symbols.calls.push(Call { + name: method_name.to_string(), + line: call_line, + receiver, + ..Default::default() + }); } "generic_name" | "member_binding_expression" => { let name = fn_node.child_by_field_name("name").or_else(|| fn_node.child(0)); if let Some(name) = name { symbols.calls.push(Call { name: node_text(&name, source).to_string(), - line: start_line(node), - dynamic: None, - receiver: None, + line: call_line, ..Default::default() }); } diff --git a/crates/codegraph-core/src/extractors/elixir.rs b/crates/codegraph-core/src/extractors/elixir.rs index f6650053..237d6244 100644 --- a/crates/codegraph-core/src/extractors/elixir.rs +++ b/crates/codegraph-core/src/extractors/elixir.rs @@ -34,6 +34,7 @@ fn match_elixir_node(node: &Node, source: &[u8], symbols: &mut FileSymbols, _dep "defprotocol" => handle_defprotocol(node, source, symbols), "defimpl" => handle_defimpl(node, source, symbols), "import" | "use" | "require" | "alias" => handle_elixir_import(node, source, symbols, keyword), + "apply" => handle_elixir_apply(node, source, symbols), _ => { symbols.calls.push(Call { name: keyword.to_string(), @@ -366,6 +367,72 @@ fn handle_elixir_import(node: &Node, source: &[u8], symbols: &mut FileSymbols, k )); } +/// apply(module, :function, args) — Elixir dynamic dispatch. +/// Second argument is an atom literal → reflection; variable → computed-key. +fn handle_elixir_apply(node: &Node, source: &[u8], symbols: &mut FileSymbols) { + let args = match find_child(node, "arguments") { + Some(a) => a, + None => { + symbols.calls.push(Call { + name: "apply".to_string(), + line: start_line(node), + ..Default::default() + }); + return; + } + }; + + // Locate the second argument (skip punctuation and the first arg) + let mut arg_idx = 0u32; + let mut second_arg: Option = None; + for i in 0..args.child_count() { + let Some(child) = args.child(i) else { continue }; + let t = child.kind(); + if matches!(t, "(" | ")" | ",") { continue; } + if arg_idx == 1 { + second_arg = Some(child); + break; + } + arg_idx += 1; + } + + let Some(second) = second_arg else { + symbols.calls.push(Call { + name: "apply".to_string(), + line: start_line(node), + ..Default::default() + }); + return; + }; + + let t = second.kind(); + if t == "atom" || t == "atom_literal" { + // :atom — strip leading colon to get function name + let raw = node_text(&second, source); + let fn_name = raw.trim_start_matches(':').to_string(); + let key_expr = raw.to_string(); + symbols.calls.push(Call { + name: fn_name, + line: start_line(node), + dynamic: Some(true), + dynamic_kind: Some("reflection".to_string()), + key_expr: Some(key_expr), + ..Default::default() + }); + } else { + // Variable function name — computed-key + let key_expr = node_text(&second, source).to_string(); + symbols.calls.push(Call { + name: "".to_string(), + line: start_line(node), + dynamic: Some(true), + dynamic_kind: Some("computed-key".to_string()), + key_expr: Some(key_expr), + ..Default::default() + }); + } +} + fn handle_dot_call(node: &Node, dot_node: &Node, source: &[u8], symbols: &mut FileSymbols) { let right = find_child(dot_node, "identifier"); let left = find_child(dot_node, "alias"); diff --git a/crates/codegraph-core/src/extractors/lua.rs b/crates/codegraph-core/src/extractors/lua.rs index 01964bde..5629d09e 100644 --- a/crates/codegraph-core/src/extractors/lua.rs +++ b/crates/codegraph-core/src/extractors/lua.rs @@ -94,6 +94,21 @@ fn handle_lua_function_call(node: &Node, source: &[u8], symbols: &mut FileSymbol None => return, }; + // load(chunk) / loadstring(chunk) / dofile — dynamic code execution; always undecidable + if name_node.kind() == "identifier" { + let ident = node_text(&name_node, source); + if matches!(ident, "load" | "loadstring" | "dofile") { + symbols.calls.push(Call { + name: "".to_string(), + line: start_line(node), + dynamic: Some(true), + dynamic_kind: Some("eval".to_string()), + ..Default::default() + }); + return; + } + } + // Check for require() as import if name_node.kind() == "identifier" && node_text(&name_node, source) == "require" { if let Some(args) = node.child_by_field_name("arguments") { @@ -137,6 +152,42 @@ fn handle_lua_function_call(node: &Node, source: &[u8], symbols: &mut FileSymbol }); } } + "bracket_index_expression" => { + // t[k]() — bracket-index call; key may be variable. + let table = name_node.child_by_field_name("table"); + let table_id = table.as_ref().map(|n| n.id()); + let mut key: Option = None; + for i in 0..name_node.child_count() { + let Some(ch) = name_node.child(i) else { continue }; + if matches!(ch.kind(), "[" | "]") { continue; } + if table_id == Some(ch.id()) { continue; } + key = Some(ch); + break; + } + if let Some(k) = key { + if k.kind() == "string" || k.kind() == "string_literal" { + let raw = node_text(&k, source); + let call_name = raw.trim_matches(|c| c == '\'' || c == '"').to_string(); + symbols.calls.push(Call { + name: call_name, + line: start_line(node), + receiver: table.map(|t| node_text(&t, source).to_string()), + ..Default::default() + }); + } else { + let key_expr = node_text(&k, source).to_string(); + symbols.calls.push(Call { + name: "".to_string(), + line: start_line(node), + dynamic: Some(true), + dynamic_kind: Some("computed-key".to_string()), + key_expr: Some(key_expr), + receiver: table.map(|t| node_text(&t, source).to_string()), + ..Default::default() + }); + } + } + } _ => { symbols.calls.push(Call { name: node_text(&name_node, source).to_string(), diff --git a/crates/codegraph-core/src/extractors/swift.rs b/crates/codegraph-core/src/extractors/swift.rs index dc44aed7..51a57d0a 100644 --- a/crates/codegraph-core/src/extractors/swift.rs +++ b/crates/codegraph-core/src/extractors/swift.rs @@ -189,6 +189,39 @@ fn extract_swift_inheritance(node: &Node, source: &[u8], class_name: &str, symbo } } +/// Extract the text content of the first string literal argument in a Swift call_expression. +/// Inspects call_suffix → value_arguments → value_argument → line_string_literal. +fn get_swift_first_string_literal(call_node: &Node, source: &[u8]) -> Option { + let call_suffix = find_child(call_node, "call_suffix")?; + let value_args = find_child(&call_suffix, "value_arguments") + .unwrap_or(call_suffix); + for i in 0..value_args.child_count() { + let Some(arg) = value_args.child(i) else { continue }; + let t = arg.kind(); + if matches!(t, "(" | ")" | ",") { continue; } + let value_node = if t == "value_argument" { + arg.child_by_field_name("value") + .or_else(|| arg.child(arg.child_count().saturating_sub(1))) + .unwrap_or(arg) + } else { + arg + }; + let vt = value_node.kind(); + if vt == "line_string_literal" || vt == "string_literal" { + for j in 0..value_node.child_count() { + let Some(ch) = value_node.child(j) else { continue }; + if ch.kind() == "line_str_text" || ch.kind() == "string_content" { + return Some(node_text(&ch, source).to_string()); + } + } + let raw = node_text(&value_node, source); + return Some(raw.trim_matches(|c| c == '"' || c == '\'').to_string()); + } + break; + } + None +} + fn match_swift_node(node: &Node, source: &[u8], symbols: &mut FileSymbols, _depth: usize) { match node.kind() { "class_declaration" => { @@ -288,16 +321,8 @@ fn match_swift_node(node: &Node, source: &[u8], symbols: &mut FileSymbols, _dept "call_expression" => { if let Some(fn_node) = node.child(0) { - match fn_node.kind() { - "simple_identifier" => { - symbols.calls.push(Call { - name: node_text(&fn_node, source).to_string(), - line: start_line(node), - dynamic: None, - receiver: None, - ..Default::default() - }); - } + let call_line = start_line(node); + let (call_name, call_receiver) = match fn_node.kind() { "navigation_expression" => { let last = fn_node.child(fn_node.child_count().saturating_sub(1)); // Swift's grammar wraps the method name in a `navigation_suffix` node @@ -318,24 +343,52 @@ fn match_swift_node(node: &Node, source: &[u8], symbols: &mut FileSymbols, _dept }).unwrap_or_else(|| node_text(&fn_node, source).to_string()); let receiver = fn_node.child(0) .map(|n| node_text(&n, source).to_string()); - symbols.calls.push(Call { - name, - line: start_line(node), - dynamic: None, - receiver, - ..Default::default() - }); - } - _ => { - symbols.calls.push(Call { - name: node_text(&fn_node, source).to_string(), - line: start_line(node), - dynamic: None, - receiver: None, - ..Default::default() - }); + (name, receiver) } + _ => (node_text(&fn_node, source).to_string(), None), + }; + + if call_name.is_empty() { + return; + } + + // performSelector — ObjC-style dynamic dispatch; selector not statically knowable + if call_name == "performSelector" { + symbols.calls.push(Call { + name: "".to_string(), + line: call_line, + dynamic: Some(true), + dynamic_kind: Some("unresolved-dynamic".to_string()), + receiver: call_receiver, + ..Default::default() + }); + return; } + + // NSSelectorFromString("name") — selector from literal string + if call_name == "NSSelectorFromString" { + let literal = get_swift_first_string_literal(node, source); + symbols.calls.push(Call { + name: literal.clone().unwrap_or_else(|| "".to_string()), + line: call_line, + dynamic: Some(true), + dynamic_kind: Some(if literal.is_some() { + "reflection".to_string() + } else { + "unresolved-dynamic".to_string() + }), + key_expr: literal, + ..Default::default() + }); + return; + } + + symbols.calls.push(Call { + name: call_name, + line: call_line, + receiver: call_receiver, + ..Default::default() + }); } }