Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 72 additions & 12 deletions crates/codegraph-core/src/extractors/csharp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
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: "<dynamic:unresolved>".to_string(),
line: call_line,
dynamic: Some(true),
dynamic_kind: Some("unresolved-dynamic".to_string()),
receiver,
..Default::default()
});
return;
}
Comment on lines +267 to 277

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 All .Invoke() calls classified as unresolved-dynamic

The current check catches every receiver.Invoke(...) call, not just MethodInfo.Invoke. Standard delegate and lambda invocations (myFunc.Invoke(arg), Action.Invoke(), LINQ delegate chains) are all statically knowable but will now be flagged as unresolved-dynamic and emit sink edges. This is parity with the TS extractor, but worth verifying that C# code heavy on delegate .Invoke() patterns doesn't accumulate noisy sink edges that inflate unresolved-dynamic counts.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code


// 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: "<dynamic:computed-key>".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()
});
}
Expand Down
67 changes: 67 additions & 0 deletions crates/codegraph-core/src/extractors/elixir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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<Node> = 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: "<dynamic:computed-key>".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");
Expand Down
51 changes: 51 additions & 0 deletions crates/codegraph-core/src/extractors/lua.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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: "<dynamic:eval>".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") {
Expand Down Expand Up @@ -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<Node> = 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: "<dynamic:computed-key>".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(),
Expand Down
105 changes: 79 additions & 26 deletions crates/codegraph-core/src/extractors/swift.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
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" => {
Expand Down Expand Up @@ -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
Expand All @@ -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: "<dynamic:unresolved>".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(|| "<dynamic:unresolved>".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()
});
}
}

Expand Down
Loading