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
86 changes: 73 additions & 13 deletions crates/codegraph-core/src/extractors/c.rs
Original file line number Diff line number Diff line change
Expand Up @@ -294,15 +294,62 @@ fn match_c_node(node: &Node, source: &[u8], symbols: &mut FileSymbols, _depth: u

"call_expression" => {
if let Some(fn_node) = node.child_by_field_name("function") {
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,
..Default::default()
});
let fn_name = node_text(&fn_node, source);
// dlsym(handle, "symbol") — dynamic symbol loading
if fn_name == "dlsym" || fn_name == "dlvsym" {
// Get second arg (index 1) — the symbol name
let args = node.child_by_field_name("arguments")
.or_else(|| find_child(node, "argument_list"));
let mut arg_idx = 0usize;
let mut second_arg: Option<String> = None;
if let Some(args) = args {
for i in 0..args.child_count() {
if let Some(child) = args.child(i) {
match child.kind() {
"(" | ")" | "," => continue,
"string_literal" | "string_content" if arg_idx == 1 => {
second_arg = Some(node_text(&child, source)
.replace(&['"', '\''][..], ""));
break;
}
_ => { arg_idx += 1; }
}
}
}
}
match second_arg {
Some(sym) if !sym.is_empty() => {
symbols.calls.push(Call {
name: sym.clone(),
line: call_line,
dynamic: Some(true),
dynamic_kind: Some("reflection".to_string()),
key_expr: Some(sym),
..Default::default()
});
}
_ => {
symbols.calls.push(Call {
name: "<dynamic:unresolved>".to_string(),
line: call_line,
dynamic: Some(true),
dynamic_kind: Some("unresolved-dynamic".to_string()),
..Default::default()
});
}
}
} else {
symbols.calls.push(Call {
name: fn_name.to_string(),
line: call_line,
dynamic: None,
receiver: None,
..Default::default()
});
}
}
"field_expression" => {
let name = named_child_text(&fn_node, "field", source)
Expand All @@ -312,21 +359,34 @@ fn match_c_node(node: &Node, source: &[u8], symbols: &mut FileSymbols, _depth: u
.map(|s| s.to_string());
symbols.calls.push(Call {
name,
line: start_line(node),
line: call_line,
dynamic: None,
receiver,
..Default::default()
});
}
_ => {
// (*fp)(args) — function pointer call; unresolvable
"parenthesized_expression" | "pointer_expression" => {
symbols.calls.push(Call {
name: node_text(&fn_node, source).to_string(),
line: start_line(node),
dynamic: None,
receiver: None,
name: "<dynamic:unresolved>".to_string(),
line: call_line,
dynamic: Some(true),
dynamic_kind: Some("unresolved-dynamic".to_string()),
..Default::default()
});
}
_ => {
let name = node_text(&fn_node, source);
if !name.is_empty() {
symbols.calls.push(Call {
name: name.to_string(),
line: call_line,
dynamic: None,
receiver: None,
..Default::default()
});
}
}
}
}
}
Expand Down
60 changes: 59 additions & 1 deletion crates/codegraph-core/src/extractors/cpp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -323,9 +323,57 @@ fn handle_cpp_preproc_include(node: &Node, source: &[u8], symbols: &mut FileSymb

fn handle_cpp_call_expression(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
if let Some(fn_node) = node.child_by_field_name("function") {
let call_line = start_line(node);
match fn_node.kind() {
"identifier" | "qualified_identifier" | "scoped_identifier" => {
push_simple_call(symbols, node, node_text(&fn_node, source).to_string());
let fn_name = node_text(&fn_node, source);
// dlsym(handle, "symbol") — dynamic symbol loading via C ABI.
// String-literal argument: resolves as reflection (extern "C" symbols are not mangled).
// Variable argument: flagged as unresolved-dynamic.
if fn_name == "dlsym" || fn_name == "dlvsym" {
let args = node.child_by_field_name("arguments")
.or_else(|| find_child(node, "argument_list"));
let mut arg_idx = 0usize;
let mut second_arg: Option<String> = None;
if let Some(args) = args {
for i in 0..args.child_count() {
if let Some(child) = args.child(i) {
match child.kind() {
"(" | ")" | "," => continue,
"string_literal" | "string_content" if arg_idx == 1 => {
second_arg = Some(node_text(&child, source)
.replace(&['"', '\''][..], ""));
break;
}
_ => { arg_idx += 1; }
}
}
}
}
match second_arg {
Some(sym) if !sym.is_empty() => {
symbols.calls.push(Call {
name: sym.clone(),
line: call_line,
dynamic: Some(true),
dynamic_kind: Some("reflection".to_string()),
key_expr: Some(sym),
..Default::default()
});
}
_ => {
symbols.calls.push(Call {
name: "<dynamic:unresolved>".to_string(),
line: call_line,
dynamic: Some(true),
dynamic_kind: Some("unresolved-dynamic".to_string()),
..Default::default()
});
}
}
} else {
push_simple_call(symbols, node, fn_name.to_string());
}
}
"field_expression" => {
let name = named_child_text(&fn_node, "field", source)
Expand All @@ -335,6 +383,16 @@ fn handle_cpp_call_expression(node: &Node, source: &[u8], symbols: &mut FileSymb
.map(|s| s.to_string());
push_call(symbols, node, name, receiver, None);
}
// (*fp)(args) — function pointer call through dereference; unresolvable statically
"parenthesized_expression" | "pointer_expression" => {
symbols.calls.push(Call {
name: "<dynamic:unresolved>".to_string(),
line: call_line,
dynamic: Some(true),
dynamic_kind: Some("unresolved-dynamic".to_string()),
..Default::default()
});
}
_ => {
push_simple_call(symbols, node, node_text(&fn_node, source).to_string());
}
Expand Down
64 changes: 61 additions & 3 deletions crates/codegraph-core/src/extractors/go.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,25 +195,83 @@ fn handle_import_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
}
}

/// Get the Nth non-punctuation argument from a Go call_expression.
fn get_go_arg(node: &Node, index: usize, source: &[u8]) -> Option<(String, String)> {
let args = node.child_by_field_name("arguments")?;
let mut count = 0usize;
for i in 0..args.child_count() {
let child = args.child(i)?;
match child.kind() {
"(" | ")" | "," => continue,
kind => {
if count == index {
return Some((node_text(&child, source).to_string(), kind.to_string()));
}
count += 1;
}
}
}
None
}

fn handle_call_expr(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
let Some(fn_node) = node.child_by_field_name("function") 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),
line: call_line,
dynamic: None,
receiver: None,
..Default::default()
});
}
"selector_expression" => {
if let Some(field) = fn_node.child_by_field_name("field") {
let field_name = node_text(&field, source);
let receiver = named_child_text(&fn_node, "operand", source)
.map(|s| s.to_string());

// v.MethodByName("name") — reflect-based dynamic dispatch
if field_name == "MethodByName" {
if let Some((arg_text, arg_kind)) = get_go_arg(node, 0, source) {
match arg_kind.as_str() {
"interpreted_string_literal" | "raw_string_literal" => {
let method = arg_text.replace(&['"', '`'][..], "");
if !method.is_empty() {
symbols.calls.push(Call {
name: method,
line: call_line,
dynamic: Some(true),
dynamic_kind: Some("reflection".to_string()),
key_expr: Some(arg_text),
receiver,
..Default::default()
});
return;
}
}
_ => {
symbols.calls.push(Call {
name: "<dynamic:computed-key>".to_string(),
line: call_line,
dynamic: Some(true),
dynamic_kind: Some("computed-key".to_string()),
key_expr: Some(arg_text),
receiver,
..Default::default()
});
return;
}
}
}
}

symbols.calls.push(Call {
name: node_text(&field, source).to_string(),
line: start_line(node),
name: field_name.to_string(),
line: call_line,
dynamic: None,
receiver,
..Default::default()
Expand Down
82 changes: 69 additions & 13 deletions src/extractors/c.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
import type {
Call,
ExtractorOutput,
SubDeclaration,
TreeSitterNode,
TreeSitterTree,
} from '../types.js';
import type { ExtractorOutput, SubDeclaration, TreeSitterNode, TreeSitterTree } from '../types.js';
import { findChild, nodeEndLine } from './helpers.js';

/**
Expand Down Expand Up @@ -142,19 +136,81 @@ function handleCInclude(node: TreeSitterNode, ctx: ExtractorOutput): void {
});
}

/** Get the Nth non-punctuation argument from a C call_expression. */
function getCArg(node: TreeSitterNode, index: number): TreeSitterNode | null {
const args = node.childForFieldName('arguments');
if (!args) return null;
let count = 0;
for (let i = 0; i < args.childCount; i++) {
const child = args.child(i);
if (!child) continue;
const t = child.type;
if (t === '(' || t === ')' || t === ',' || t === 'comment') continue;
if (count === index) return child;
count++;
}
return null;
}

function handleCCallExpression(node: TreeSitterNode, ctx: ExtractorOutput): void {
const funcNode = node.childForFieldName('function');
if (!funcNode) return;
const call: Call = { name: '', line: node.startPosition.row + 1 };
const callLine = node.startPosition.row + 1;

if (funcNode.type === 'field_expression') {
const field = funcNode.childForFieldName('field');
const argument = funcNode.childForFieldName('argument');
if (field) call.name = field.text;
if (argument) call.receiver = argument.text;
} else {
call.name = funcNode.text;
if (field) {
ctx.calls.push({
name: field.text,
line: callLine,
...(argument ? { receiver: argument.text } : {}),
});
}
return;
}

// (*fp)(args) — function pointer call through dereference; unresolvable statically
if (funcNode.type === 'parenthesized_expression' || funcNode.type === 'pointer_expression') {
ctx.calls.push({
name: '<dynamic:unresolved>',
line: callLine,
dynamic: true,
dynamicKind: 'unresolved-dynamic',
});
return;
}

const fnName = funcNode.text;

// dlsym(handle, "symbol") — dynamic symbol loading
if (fnName === 'dlsym' || fnName === 'dlvsym') {
const nameArg = getCArg(node, 1); // second arg is the symbol name
if (nameArg && (nameArg.type === 'string_literal' || nameArg.type === 'string_content')) {
const sym = nameArg.text.replace(/['"]/g, '');
if (sym) {
ctx.calls.push({
name: sym,
line: callLine,
dynamic: true,
dynamicKind: 'reflection',
keyExpr: nameArg.text,
});
return;
}
}
ctx.calls.push({
name: '<dynamic:unresolved>',
line: callLine,
dynamic: true,
dynamicKind: 'unresolved-dynamic',
});
return;
}

if (fnName) {
ctx.calls.push({ name: fnName, line: callLine });
}
if (call.name) ctx.calls.push(call);
}

// ── Child extraction helpers ────────────────────────────────────────────────
Expand Down
Loading
Loading