Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
4 changes: 2 additions & 2 deletions Makefile.cbm
Original file line number Diff line number Diff line change
Expand Up @@ -217,8 +217,8 @@ MIMALLOC_CFLAGS_TEST = -std=c11 -g -O1 -w \

# sqlite3 (vendored amalgamation — compiled ourselves for ASan instrumentation)
SQLITE3_SRC = vendored/sqlite3/sqlite3.c
SQLITE3_CFLAGS = -std=c11 -O2 -w -DSQLITE_DQS=0 -DSQLITE_THREADSAFE=1
SQLITE3_CFLAGS_TEST = -std=c11 -g -O1 -w -DSQLITE_DQS=0 -DSQLITE_THREADSAFE=1
SQLITE3_CFLAGS = -std=c11 -O2 -w -DSQLITE_DQS=0 -DSQLITE_THREADSAFE=1 -DSQLITE_ENABLE_FTS5
SQLITE3_CFLAGS_TEST = -std=c11 -g -O1 -w -DSQLITE_DQS=0 -DSQLITE_THREADSAFE=1 -DSQLITE_ENABLE_FTS5

# TRE regex (vendored, Windows only — POSIX uses system <regex.h>)
TRE_SRC = vendored/tre/tre_all.c
Expand Down
97 changes: 97 additions & 0 deletions internal/cbm/extract_calls.c
Original file line number Diff line number Diff line change
Expand Up @@ -344,4 +344,101 @@ void handle_calls(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, Walk
}
}
}

// C# delegate/event patterns
if (ctx->language == CBM_LANG_CSHARP) {
// Fix 1: event += MethodName (bare method reference subscription)
// Creates a CALLS edge from the subscribing method to the handler method.
// e.g. _socket.OnConnected += SocketOnConnected;
if (strcmp(kind, "assignment_expression") == 0) {
TSNode op = ts_node_child_by_field_name(node, "operator", 8);
if (!ts_node_is_null(op)) {
char *op_text = cbm_node_text(ctx->arena, op, ctx->source);
if (op_text && strcmp(op_text, "+=") == 0) {
TSNode right = ts_node_child_by_field_name(node, "right", 5);
if (!ts_node_is_null(right)) {
const char *rk = ts_node_type(right);
if (strcmp(rk, "identifier") == 0 ||
strcmp(rk, "member_access_expression") == 0) {
char *callee = cbm_node_text(ctx->arena, right, ctx->source);
if (callee && callee[0] && !cbm_is_keyword(callee, ctx->language)) {
CBMCall call;
call.callee_name = callee;
call.enclosing_func_qn = state->enclosing_func_qn;
cbm_calls_push(&ctx->result->calls, ctx->arena, call);
}
}
}
}
}
}

// Fix 2: delegate?.Invoke() → resolve to receiver (delegate) name.
// C# delegates are invoked via .Invoke() or ?.Invoke() — the callee name
// "Invoke" resolves to nothing. Instead, extract the receiver (delegate property)
// name, which is more likely to match a registered symbol.
// e.g. OnConnected?.Invoke(this, e) → creates CALLS edge to "OnConnected"
//
// C# tree-sitter AST for "OnConnected?.Invoke(this, e)":
// invocation_expression
// function: conditional_access_expression
// expression: identifier "OnConnected" ← receiver
// member_binding_expression
// name: identifier "Invoke" ← method
// arguments: argument_list
if (cbm_kind_in_set(node, spec->call_node_types)) {
TSNode func_node2 = ts_node_child_by_field_name(node, "function", 8);
if (!ts_node_is_null(func_node2)) {
const char *fk2 = ts_node_type(func_node2);
bool is_invoke = false;
TSNode receiver2 = {0}; // NOLINT

if (strcmp(fk2, "conditional_access_expression") == 0) {
// ?. access: look for member_binding_expression child
uint32_t ncc = ts_node_named_child_count(func_node2);
for (uint32_t ci = 0; ci < ncc; ci++) {
TSNode child = ts_node_named_child(func_node2, ci);
const char *ck = ts_node_type(child);
if (strcmp(ck, "member_binding_expression") == 0) {
TSNode name_n = ts_node_child_by_field_name(child, "name", 4);
if (!ts_node_is_null(name_n)) {
char *nm = cbm_node_text(ctx->arena, name_n, ctx->source);
if (nm && strcmp(nm, "Invoke") == 0) {
is_invoke = true;
}
}
}
if (strcmp(ck, "identifier") == 0 ||
strcmp(ck, "member_access_expression") == 0) {
receiver2 = child;
}
}
} else if (strcmp(fk2, "member_access_expression") == 0) {
// Dot access: obj.Invoke(...)
TSNode name_n = ts_node_child_by_field_name(func_node2, "name", 4);
if (!ts_node_is_null(name_n)) {
char *nm = cbm_node_text(ctx->arena, name_n, ctx->source);
if (nm && strcmp(nm, "Invoke") == 0) {
is_invoke = true;
TSNode expr = ts_node_child_by_field_name(func_node2,
"expression", 10);
if (!ts_node_is_null(expr)) {
receiver2 = expr;
}
}
}
}

if (is_invoke && !ts_node_is_null(receiver2)) {
char *recv = cbm_node_text(ctx->arena, receiver2, ctx->source);
if (recv && recv[0] && !cbm_is_keyword(recv, ctx->language)) {
CBMCall call;
call.callee_name = recv;
call.enclosing_func_qn = state->enclosing_func_qn;
cbm_calls_push(&ctx->result->calls, ctx->arena, call);
}
}
}
}
}
}
50 changes: 49 additions & 1 deletion internal/cbm/extract_defs.c
Original file line number Diff line number Diff line change
Expand Up @@ -565,10 +565,58 @@ static const char **extract_base_classes(CBMArena *a, TSNode node, const char *s
}
}
}
// C/C++ specific: handle base_class_clause (contains access specifiers + type names)
// C# specific: handle base_list node (contains base types separated by commas)
{
uint32_t count = ts_node_child_count(node);
for (uint32_t i = 0; i < count; i++) {
TSNode child = ts_node_child(node, i);
if (strcmp(ts_node_type(child), "base_list") == 0) {
const char *bases[16];
int base_count = 0;
uint32_t bnc = ts_node_named_child_count(child);
for (uint32_t bi = 0; bi < bnc && base_count < MAX_BASES_MINUS_1; bi++) {
TSNode bc = ts_node_named_child(child, bi);
const char *bk = ts_node_type(bc);
// C# base types can be: identifier, generic_name, qualified_name,
// or wrapped in a simple_base_type / primary_constructor_base_type
char *text = NULL;
if (strcmp(bk, "identifier") == 0 || strcmp(bk, "generic_name") == 0 ||
strcmp(bk, "qualified_name") == 0) {
text = cbm_node_text(a, bc, source);
} else {
// For wrapper nodes (simple_base_type etc.), extract the first
// named child which should be the type identifier
TSNode inner = ts_node_named_child(bc, 0);
if (!ts_node_is_null(inner)) {
text = cbm_node_text(a, inner, source);
}
}
if (text && text[0]) {
// Strip generic args for resolution: "List<int>" → "List"
char *angle = strchr(text, '<');
if (angle) *angle = '\0';
bases[base_count++] = text;
}
}
if (base_count > 0) {
const char **result =
(const char **)cbm_arena_alloc(a, (base_count + 1) * sizeof(const char *));
if (result) {
for (int j = 0; j < base_count; j++) {
result[j] = bases[j];
}
result[base_count] = NULL;
return result;
}
}
}
}
}

// C/C++ specific: handle base_class_clause (contains access specifiers + type names)
{
uint32_t count2 = ts_node_child_count(node);
for (uint32_t i = 0; i < count2; i++) {
TSNode child = ts_node_child(node, i);
if (strcmp(ts_node_type(child), "base_class_clause") == 0) {
// Extract type identifiers from base_class_clause, skipping access specifiers
Expand Down
29 changes: 26 additions & 3 deletions internal/cbm/extract_unified.c
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,32 @@ void cbm_extract_unified(CBMExtractCtx *ctx) {

// 4. Push scope markers for boundary nodes
if (spec->function_node_types && cbm_kind_in_set(node, spec->function_node_types)) {
const char *fqn = compute_func_qn(ctx, node, spec, &state);
if (fqn) {
push_scope(&state, SCOPE_FUNC, depth, fqn);
// Fix 3: C# lambda_expression inside += assignment should NOT create
// a new scope boundary. Calls inside the lambda body should be attributed
// to the outer method that subscribes the event handler, not to an
// anonymous lambda. This matches the semantic intent: the subscribing
// method IS responsible for what runs when the event fires.
bool skip_scope = false;
if (ctx->language == CBM_LANG_CSHARP &&
strcmp(ts_node_type(node), "lambda_expression") == 0) {
TSNode parent = ts_node_parent(node);
if (!ts_node_is_null(parent) &&
strcmp(ts_node_type(parent), "assignment_expression") == 0) {
TSNode op = ts_node_child_by_field_name(parent, "operator", 8);
if (!ts_node_is_null(op)) {
char *op_text = cbm_node_text(ctx->arena, op, ctx->source);
if (op_text && (strcmp(op_text, "+=") == 0 ||
strcmp(op_text, "-=") == 0)) {
skip_scope = true;
}
}
}
}
if (!skip_scope) {
const char *fqn = compute_func_qn(ctx, node, spec, &state);
if (fqn) {
push_scope(&state, SCOPE_FUNC, depth, fqn);
}
}
} else if (spec->class_node_types && cbm_kind_in_set(node, spec->class_node_types)) {
const char *cqn = compute_class_qn(ctx, node);
Expand Down
21 changes: 21 additions & 0 deletions src/cypher/cypher.c
Original file line number Diff line number Diff line change
Expand Up @@ -1561,6 +1561,9 @@ typedef struct {
} binding_t;

/* Get node property by name */
/* Forward declaration — full implementation below */
static const char *json_extract_prop(const char *json, const char *key, char *buf, size_t buf_sz);

static const char *node_prop(const cbm_node_t *n, const char *prop) {
if (!n || !prop) {
return "";
Expand Down Expand Up @@ -1588,6 +1591,24 @@ static const char *node_prop(const cbm_node_t *n, const char *prop) {
snprintf(buf, sizeof(buf), "%d", n->end_line);
return buf;
}
if (strcmp(prop, "file") == 0) {
return n->file_path ? n->file_path : "";
}
if (strcmp(prop, "id") == 0) {
static char buf[32];
snprintf(buf, sizeof(buf), "%lld", (long long)n->id);
return buf;
}
/* Fall through to JSON properties for unknown fields.
* This enables queries like WHERE n.is_entry_point = true
* or WHERE n.confidence > 0.5 on properties stored in properties_json. */
if (n->properties_json) {
static char json_buf[1024];
const char *val = json_extract_prop(n->properties_json, prop, json_buf, sizeof(json_buf));
if (val && val[0]) {
return val;
}
}
return "";
}

Expand Down
Loading