Fix CI lint: drop debug scaffolding, make lisp import walker iterative, apply clang-format

Remove leftover getenv(CBM_DBG_*) debug fprintf blocks in extract_calls.c/pass_calls.c (cppcheck always-true). Convert parse_lisp_imports to a stack-based walk (no recursion → no NOLINT, avoids deep-nesting overflow). Apply Homebrew clang-format to all touched sources. Local suite 5562/0, lint-ci clean.
This commit is contained in:
Martin Vogel committed 2026-06-08 22:55:23 +02:00
1 parent 54108092d8
commit 6f3b24df4f
25 files changed
+5606 -5048

No files matched your search

+5 -12
View File
@@ -885,9 +885,8 @@ static bool is_url_or_topic_keyword(const char *key) {
// arg (e.g. Go `SendMessageInput{QueueUrl: ...}`, `PublishInput{TopicArn: ...}`).
// Case-insensitive so QueueUrl/QueueURL/queue_url all match.
static bool is_queue_topic_field(const char *key) {
static const char *fields[] = {"QueueUrl", "QueueURL", "TopicArn", "TopicARN",
"QueueName", "TopicName", "QueueArn", "QueueARN",
"Destination", NULL};
static const char *fields[] = {"QueueUrl", "QueueURL", "TopicArn", "TopicARN", "QueueName",
"TopicName", "QueueArn", "QueueARN", "Destination", NULL};
if (!key || !key[0]) {
return false;
}
@@ -1080,8 +1079,7 @@ static const char *extract_handler_arg(CBMExtractCtx *ctx, TSNode args) {
for (uint32_t ai = HANDLER_START_IDX; ai < nc && ai < MAX_HANDLER_SCAN; ai++) {
TSNode arg2 = ts_node_named_child(args, ai);
/* PHP wraps each argument in an `argument` node — unwrap to the value. */
if (strcmp(ts_node_type(arg2), "argument") == 0 &&
ts_node_named_child_count(arg2) > 0) {
if (strcmp(ts_node_type(arg2), "argument") == 0 && ts_node_named_child_count(arg2) > 0) {
arg2 = ts_node_named_child(arg2, 0);
}
const char *ak2 = ts_node_type(arg2);
@@ -1093,8 +1091,8 @@ static const char *extract_handler_arg(CBMExtractCtx *ctx, TSNode args) {
return cbm_node_text(ctx->arena, arg2, ctx->source);
}
if (is_string_like(ak2)) {
const char *h = normalize_string_handler(ctx->arena,
cbm_node_text(ctx->arena, arg2, ctx->source));
const char *h =
normalize_string_handler(ctx->arena, cbm_node_text(ctx->arena, arg2, ctx->source));
if (h && h[0]) {
return h;
}
@@ -1138,11 +1136,6 @@ void handle_calls(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, Walk
call.start_line = (int)ts_node_start_point(node).row + TS_LINE_OFFSET;
TSNode args = ts_node_child_by_field_name(node, TS_FIELD("arguments"));
if (getenv("CBM_DBG_ARGS") && callee && (strstr(callee,"get")||strstr(callee,"Get")||strstr(callee,"client")||strstr(callee,"Route"))) {
fprintf(stderr, "[DBGARGS] callee=%s node=%s args_null=%d", callee, ts_node_type(node), ts_node_is_null(args));
if (!ts_node_is_null(args)) { uint32_t n=ts_node_named_child_count(args); fprintf(stderr, " nargs=%u:", n); for(uint32_t i=0;i<n&&i<4;i++) fprintf(stderr," [%s]",ts_node_type(ts_node_named_child(args,i))); }
fprintf(stderr, "\n");
}
if (!ts_node_is_null(args)) {
call.first_string_arg = extract_url_or_topic_arg(ctx, args);
if (call.first_string_arg && call.first_string_arg[0] == '/') {
+16 -21
View File
@@ -1607,8 +1607,8 @@ static const char **extract_csharp_base_list(CBMArena *a, TSNode node, const cha
}
// Append a base name (generic args stripped) to out[] if non-empty.
static void push_base_text(CBMArena *a, TSNode n, const char *source, const char **out,
int out_cap, int *count) {
static void push_base_text(CBMArena *a, TSNode n, const char *source, const char **out, int out_cap,
int *count) {
if (*count >= out_cap) {
return;
}
@@ -2977,8 +2977,7 @@ static void extract_class_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
if (!ts_node_is_null(inner) && strcmp(ts_node_type(inner), "binary_expression") == 0 &&
ts_node_named_child_count(inner) > 0) {
name_node = ts_node_named_child(inner, 0); /* LHS of `<:` */
} else if (!ts_node_is_null(inner) &&
strcmp(ts_node_type(inner), "identifier") == 0) {
} else if (!ts_node_is_null(inner) && strcmp(ts_node_type(inner), "identifier") == 0) {
name_node = inner;
} else {
name_node = cbm_find_child_by_kind(th, "identifier");
@@ -3078,8 +3077,8 @@ static void extract_class_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
}
break;
case CBM_LANG_GLEAM: // type_definition > type_name > type_identifier
name_node = find_first_descendant_by_kind(node, "type_identifier",
CBM_DESCENDANT_MAX_DEPTH);
name_node =
find_first_descendant_by_kind(node, "type_identifier", CBM_DESCENDANT_MAX_DEPTH);
break;
case CBM_LANG_RESCRIPT: { // type_declaration > type_binding(name=type_identifier)
TSNode binding = cbm_find_child_by_kind(node, "type_binding");
@@ -3092,8 +3091,7 @@ static void extract_class_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
break;
}
case CBM_LANG_FSHARP: { // type_definition > *_type_defn > type_name > identifier
TSNode tn = find_first_descendant_by_kind(node, "type_name",
CBM_DESCENDANT_MAX_DEPTH);
TSNode tn = find_first_descendant_by_kind(node, "type_name", CBM_DESCENDANT_MAX_DEPTH);
if (!ts_node_is_null(tn)) {
name_node = ts_node_child_by_field_name(tn, TS_FIELD("type_name"));
if (ts_node_is_null(name_node)) {
@@ -3130,8 +3128,7 @@ static void extract_class_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
}
case CBM_LANG_PASCAL: { // declClass is nested; name lives on the parent declType
TSNode parent = ts_node_parent(node);
if (!ts_node_is_null(parent) &&
strcmp(ts_node_type(parent), "declType") == 0) {
if (!ts_node_is_null(parent) && strcmp(ts_node_type(parent), "declType") == 0) {
name_node = ts_node_child_by_field_name(parent, TS_FIELD("name"));
}
break;
@@ -3175,9 +3172,8 @@ static void extract_class_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
// registry only indexes Function/Method/Class/Interface labels), letting
// `inherit Base` resolve into an INHERITS edge.
if (ctx->language == CBM_LANG_FSHARP && strcmp(label, "Type") == 0) {
if (!ts_node_is_null(
find_first_descendant_by_kind(node, "primary_constr_args",
CBM_DESCENDANT_MAX_DEPTH)) ||
if (!ts_node_is_null(find_first_descendant_by_kind(node, "primary_constr_args",
CBM_DESCENDANT_MAX_DEPTH)) ||
!ts_node_is_null(find_first_descendant_by_kind(node, "class_inherits_decl",
CBM_DESCENDANT_MAX_DEPTH))) {
label = "Class";
@@ -5182,8 +5178,9 @@ static void extract_lisp_def(CBMExtractCtx *ctx, TSNode node) {
* `ERROR` node, so the outer class is never recognized as a `class_declaration`
* and disappears from the graph:
*
* class MyClass { companion object : Factory<MyClass>() { ... } } // anon companion w/ delegation
* class Tree { inner class Node : BaseNode() { ... } } // inner + delegation
* class MyClass { companion object : Factory<MyClass>() { ... } } // anon companion w/
* delegation class Tree { inner class Node : BaseNode() { ... } } // inner +
* delegation
*
* Inside the ERROR node the tokens are still present as a flat child list:
* `class`/`object` keyword token → simple_identifier/type_identifier (name)
@@ -5243,16 +5240,14 @@ static void recover_kotlin_error_classes(CBMExtractCtx *ctx, TSNode err_node) {
/* delegation_specifier → user_type (directly or under
* constructor_invocation) → type_identifier; strip generic args. */
TSNode ut = ts_node_named_child(sib, 0);
if (!ts_node_is_null(ut) &&
strcmp(ts_node_type(ut), "constructor_invocation") == 0) {
if (!ts_node_is_null(ut) && strcmp(ts_node_type(ut), "constructor_invocation") == 0) {
ut = ts_node_named_child(ut, 0);
}
if (ts_node_is_null(ut)) {
continue;
}
TSNode ti = ut;
if (strcmp(ts_node_type(ut), "user_type") == 0 &&
ts_node_named_child_count(ut) > 0) {
if (strcmp(ts_node_type(ut), "user_type") == 0 && ts_node_named_child_count(ut) > 0) {
ti = ts_node_named_child(ut, 0);
}
push_base_text(a, ti, ctx->source, bases, MAX_BASES_MINUS_1, &bcount);
@@ -5268,8 +5263,8 @@ static void recover_kotlin_error_classes(CBMExtractCtx *ctx, TSNode err_node) {
def.end_line = ts_node_end_point(err_node).row + TS_LINE_OFFSET;
def.is_exported = cbm_is_exported(name, ctx->language);
if (bcount > 0) {
const char **result = (const char **)cbm_arena_alloc(
a, (size_t)(bcount + NULL_TERM) * sizeof(const char *));
const char **result = (const char **)cbm_arena_alloc(a, (size_t)(bcount + NULL_TERM) *
sizeof(const char *));
if (result) {
for (int k = 0; k < bcount; k++) {
result[k] = bases[k];
+23 -26
View File
@@ -1465,7 +1465,6 @@ static void parse_embedded_imports(CBMExtractCtx *ctx) {
}
}
// --- Namespace / package declaration capture ---
// Java/Kotlin/C#/PHP put the file's symbols inside a namespace/package whose
// name is NOT reflected in the path-based QN scheme. Capturing it lets the
@@ -1699,8 +1698,7 @@ static void lisp_push_clause_modules(CBMExtractCtx *ctx, TSNode clause) {
TSNode item = ts_node_named_child(clause, j);
const char *ik = ts_node_type(item);
/* `[app.util :as u]` — the module is the vector's first symbol. */
if (strcmp(ik, "vec_lit") == 0 || strcmp(ik, "list_lit") == 0 ||
strcmp(ik, "list") == 0) {
if (strcmp(ik, "vec_lit") == 0 || strcmp(ik, "list_lit") == 0 || strcmp(ik, "list") == 0) {
uint32_t vc = ts_node_named_child_count(item);
if (vc > 0) {
lisp_push_module(ctx, ts_node_named_child(item, 0));
@@ -1807,22 +1805,23 @@ static void lisp_process_list(CBMExtractCtx *ctx, TSNode node) {
}
}
/* Recursively walk lisp lists. Fennel binds requires inside other forms
/* Iteratively walk lisp lists. Fennel binds requires inside other forms
* (`(local util (require :util))`), so we must descend into every list, not
* just root children. */
static void lisp_walk(CBMExtractCtx *ctx, TSNode node) { // NOLINT(misc-no-recursion)
const char *nt = ts_node_type(node);
if (strcmp(nt, "list") == 0 || strcmp(nt, "list_lit") == 0) {
lisp_process_list(ctx, node);
}
uint32_t cc = ts_node_named_child_count(node);
for (uint32_t i = 0; i < cc; i++) {
lisp_walk(ctx, ts_node_named_child(node, i));
}
}
* just root children. Stack-based (not recursive) to avoid deep-nesting stack
* overflow, matching the other walkers in this file. */
static void parse_lisp_imports(CBMExtractCtx *ctx) {
lisp_walk(ctx, ctx->root);
CBMArena *a = ctx->arena;
TSNodeStack stack;
ts_nstack_init(&stack, a, CBM_SZ_512);
ts_nstack_push(&stack, a, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
const char *nt = ts_node_type(node);
if (strcmp(nt, "list") == 0 || strcmp(nt, "list_lit") == 0) {
lisp_process_list(ctx, node);
}
ts_nstack_push_children(&stack, a, node);
}
}
// --- Starlark imports ---
@@ -2089,11 +2088,10 @@ static void push_path_import(CBMExtractCtx *ctx, TSNode node) {
// back to text that still contains a directive keyword.
static void push_string_descendant_import(CBMExtractCtx *ctx, TSNode node) {
TSNode hit = node;
static const char *content_kinds[] = {"string_content", "str_literal",
"slStringLiteralPart", "include_path",
"attribute_value", "path_fragment",
"string_fragment", "literal_content",
NULL};
static const char *content_kinds[] = {
"string_content", "str_literal", "slStringLiteralPart",
"include_path", "attribute_value", "path_fragment",
"string_fragment", "literal_content", NULL};
for (const char **k = content_kinds; *k; k++) {
if (find_first_descendant_of(node, *k, &hit)) {
push_path_import(ctx, hit);
@@ -2101,7 +2099,7 @@ static void push_string_descendant_import(CBMExtractCtx *ctx, TSNode node) {
}
}
/* Quoted string nodes (Just/Pkl/Jsonnet variants): strip the quotes. */
static const char *string_kinds[] = {"string", "string_value", "string_literal",
static const char *string_kinds[] = {"string", "string_value", "string_literal",
"static_string", "stringConstant", NULL};
for (const char **k = string_kinds; *k; k++) {
if (find_first_descendant_of(node, *k, &hit)) {
@@ -2271,9 +2269,8 @@ static void parse_jsonnet_imports(CBMExtractCtx *ctx) {
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
const char *k = ts_node_type(node);
if (ts_node_is_named(node) &&
(strcmp(k, "import") == 0 || strcmp(k, "importstr") == 0 ||
strcmp(k, "importbin") == 0)) {
if (ts_node_is_named(node) && (strcmp(k, "import") == 0 || strcmp(k, "importstr") == 0 ||
strcmp(k, "importbin") == 0)) {
push_string_descendant_import(ctx, node);
}
ts_nstack_push_children(&stack, ctx->arena, node);
+6 -5
View File
@@ -204,8 +204,8 @@ static TSNode resolve_write_lhs_node(TSNode node) {
return left;
}
const char *nk = ts_node_type(node);
if (strcmp(nk, "postfix_unary_expression") == 0 ||
strcmp(nk, "prefix_unary_expression") == 0 || strcmp(nk, "update_expression") == 0) {
if (strcmp(nk, "postfix_unary_expression") == 0 || strcmp(nk, "prefix_unary_expression") == 0 ||
strcmp(nk, "update_expression") == 0) {
// Only ++/-- mutate their operand. Other unary postfix/prefix forms
// (C# null-forgiving `x!`, address-of `&x`, deref `*x`, logical `!x`)
// READ the operand — never treat them as writes.
@@ -230,9 +230,10 @@ static TSNode resolve_write_lhs_node(TSNode node) {
TSNode c = ts_node_named_child(node, i);
const char *ck = ts_node_type(c);
if (strcmp(ck, "identifier") == 0 || strcmp(ck, "simple_identifier") == 0 ||
strcmp(ck, "member_access_expression") == 0 || strcmp(ck, "field_expression") == 0 ||
strcmp(ck, "field_access") == 0 || strcmp(ck, "selector_expression") == 0 ||
strcmp(ck, "subscript_expression") == 0 || strcmp(ck, "index_expression") == 0) {
strcmp(ck, "member_access_expression") == 0 ||
strcmp(ck, "field_expression") == 0 || strcmp(ck, "field_access") == 0 ||
strcmp(ck, "selector_expression") == 0 || strcmp(ck, "subscript_expression") == 0 ||
strcmp(ck, "index_expression") == 0) {
return c;
}
}
+2 -1
View File
@@ -775,7 +775,8 @@ static bool is_export_of_declaration(TSNode node) {
for (uint32_t i = 0; i < n; i++) {
const char *ck = ts_node_type(ts_node_child(node, i));
if (strcmp(ck, "function_declaration") == 0 || strcmp(ck, "class_declaration") == 0 ||
strcmp(ck, "lexical_declaration") == 0 || strcmp(ck, "abstract_class_declaration") == 0 ||
strcmp(ck, "lexical_declaration") == 0 ||
strcmp(ck, "abstract_class_declaration") == 0 ||
strcmp(ck, "interface_declaration") == 0 || strcmp(ck, "enum_declaration") == 0 ||
strcmp(ck, "type_alias_declaration") == 0 || strcmp(ck, "variable_declaration") == 0 ||
strcmp(ck, "generator_function_declaration") == 0) {
+4 -4
View File
@@ -131,10 +131,10 @@ static const char *java_keywords[] = {
* (`data`, `open`, `sealed`, `suspend`, …) are context-sensitive and usable as
* identifiers, so they are intentionally omitted. */
static const char *kotlin_keywords[] = {
"as", "break", "class", "continue", "do", "else", "false", "for",
"fun", "if", "in", "interface", "is", "null", "object", "package",
"return", "super", "this", "throw", "true", "try", "typealias", "typeof",
"val", "var", "when", "while", NULL};
"as", "break", "class", "continue", "do", "else", "false", "for",
"fun", "if", "in", "interface", "is", "null", "object", "package",
"return", "super", "this", "throw", "true", "try", "typealias", "typeof",
"val", "var", "when", "while", NULL};
static const char *generic_keywords[] = {
"true", "false", "null", "nil", "None", "undefined", "void", "if",
+9 -10
View File
@@ -363,8 +363,7 @@ static const char *cs_class_types[] = {"class_declaration",
"type_declaration",
NULL};
static const char *cs_module_types[] = {"compilation_unit", NULL};
static const char *cs_call_types[] = {"invocation_expression", "object_creation_expression",
NULL};
static const char *cs_call_types[] = {"invocation_expression", "object_creation_expression", NULL};
static const char *cs_import_types[] = {"using_directive", "namespace_use_declaration",
"using_statement", "namespace_declaration", NULL};
static const char *cs_branch_types[] = {"if_statement", "for_statement", "foreach_statement",
@@ -389,10 +388,9 @@ static const char *php_class_types[] = {"trait_declaration", "enum_declaration",
static const char *php_import_types[] = {"extends", "include", "namespace_use_declaration",
"require", "use_declaration", NULL};
static const char *php_module_types[] = {"program", NULL};
static const char *php_call_types[] = {"member_call_expression", "scoped_call_expression",
"function_call_expression",
"object_creation_expression",
"nullsafe_member_call_expression", NULL};
static const char *php_call_types[] = {
"member_call_expression", "scoped_call_expression", "function_call_expression",
"object_creation_expression", "nullsafe_member_call_expression", NULL};
static const char *php_branch_types[] = {"if_statement", "for_statement", "foreach_statement",
"while_statement", "switch_statement", "case_statement",
"try_statement", "catch_clause", NULL};
@@ -418,8 +416,9 @@ static const char *scala_class_types[] = {"class_definition", "object_definition
"trait_definition", "enum_definition",
"type_definition", NULL};
static const char *scala_module_types[] = {"compilation_unit", NULL};
static const char *scala_call_types[] = {"call_expression", "generic_function", "field_expression",
"infix_expression", "instance_expression", NULL};
static const char *scala_call_types[] = {"call_expression", "generic_function",
"field_expression", "infix_expression",
"instance_expression", NULL};
static const char *scala_import_types[] = {"import_declaration", "extends", "import",
"using_directive", NULL};
static const char *scala_branch_types[] = {
@@ -1528,8 +1527,8 @@ static const char *smithy_class_types[] = {"structure_statement", "union_stateme
static const char *smithy_field_types[] = {"shape_member", NULL};
static const char *smithy_import_types[] = {"use_statement", NULL};
static const char *smithy_module_types[] = {"source_file", NULL};
static const char *wit_func_types[] = {"func_item", "resource_method", "export_item",
"import_item", NULL};
static const char *wit_func_types[] = {"func_item", "resource_method", "export_item", "import_item",
NULL};
static const char *wit_class_types[] = {"record_item", "resource_item", "enum_items",
"variant_items", "flags_items", NULL};
static const char *wit_field_types[] = {"record_field", NULL};
File diff suppressed because it is too large. Load diff
+2 -1
View File
@@ -4113,7 +4113,8 @@ static void kt_register_cross_def(CBMTypeRegistry *reg, CBMArena *arena, const C
n++;
}
}
const char **emb = (const char **)cbm_arena_alloc(arena, (size_t)(n + 1) * sizeof(*emb));
const char **emb =
(const char **)cbm_arena_alloc(arena, (size_t)(n + 1) * sizeof(*emb));
int idx = 0;
const char *start = d->embedded_types;
while (*start) {
+18 -19
View File
@@ -59,10 +59,10 @@
* `foo` is resolved when used as a bare identifier. */
typedef enum {
CBM_KT_USE_UNKNOWN = 0,
CBM_KT_USE_TYPE, /* class / interface / object / typealias */
CBM_KT_USE_FUNCTION, /* top-level fun, extension fun */
CBM_KT_USE_PROPERTY, /* top-level val/var */
CBM_KT_USE_WILDCARD, /* import a.b.* — local_name is a.b prefix */
CBM_KT_USE_TYPE, /* class / interface / object / typealias */
CBM_KT_USE_FUNCTION, /* top-level fun, extension fun */
CBM_KT_USE_PROPERTY, /* top-level val/var */
CBM_KT_USE_WILDCARD, /* import a.b.* — local_name is a.b prefix */
} CBMKotlinUseKind;
/* KotlinLSPContext — per-file state for Kotlin call resolution. */
@@ -74,27 +74,27 @@ typedef struct KotlinLSPContext {
CBMScope *current_scope;
/* Package context. Empty string for default package. */
const char *package_qn; /* dotted form, e.g. "com.example.foo" */
const char *module_qn; /* file-level QN, e.g. "<project>.com.example.foo.<File>" */
const char *project_name; /* project prefix (without trailing dot) */
const char *file_class_qn; /* JVM file-class QN, "<package>.<File>Kt" */
const char *rel_path; /* for diagnostics */
const char *package_qn; /* dotted form, e.g. "com.example.foo" */
const char *module_qn; /* file-level QN, e.g. "<project>.com.example.foo.<File>" */
const char *project_name; /* project prefix (without trailing dot) */
const char *file_class_qn; /* JVM file-class QN, "<package>.<File>Kt" */
const char *rel_path; /* for diagnostics */
/* Import map (parallel arrays). Kotlin imports are flat — no grouping
* — but each may have an `as` alias. Wildcard imports record the
* package prefix in `import_targets[i]` with `import_kinds[i] = WILDCARD`
* and `import_locals[i] = NULL`. */
const char **import_locals; /* alias name (or short name) used in code */
const char **import_targets; /* full FQN being imported */
const char **import_locals; /* alias name (or short name) used in code */
const char **import_targets; /* full FQN being imported */
CBMKotlinUseKind *import_kinds;
int import_count;
int import_cap;
/* Current declaration context. */
const char *enclosing_func_qn; /* function we are resolving inside */
const char *enclosing_class_qn; /* innermost class/interface/object QN, or NULL */
const char *enclosing_func_qn; /* function we are resolving inside */
const char *enclosing_class_qn; /* innermost class/interface/object QN, or NULL */
const char *enclosing_companion_qn; /* if inside a companion object body */
const char *enclosing_super_qn; /* primary super-class QN of current class, or NULL */
const char *enclosing_super_qn; /* primary super-class QN of current class, or NULL */
/* Current `this` and `super` types. */
const CBMType *this_type;
@@ -116,9 +116,8 @@ typedef struct KotlinLSPContext {
/* Initialize a KotlinLSPContext for processing one file. */
void kotlin_lsp_init(KotlinLSPContext *ctx, CBMArena *arena, const char *source, int source_len,
const CBMTypeRegistry *registry, const char *package_qn,
const char *module_qn, const char *project_name, const char *rel_path,
CBMResolvedCallArray *out);
const CBMTypeRegistry *registry, const char *package_qn, const char *module_qn,
const char *project_name, const char *rel_path, CBMResolvedCallArray *out);
/* Add an import mapping. local_name is the name used in code (alias or
* short name); target_qn is the full dotted FQN. For wildcard imports,
@@ -155,8 +154,8 @@ const CBMType *kotlin_lookup_property_type(KotlinLSPContext *ctx, const char *cl
/* Entry point: build registry from file defs + stdlib, then run resolution.
* Called from cbm_extract_file() after definitions and imports have been
* extracted. */
void cbm_run_kotlin_lsp(CBMArena *arena, CBMFileResult *result, const char *source,
int source_len, TSNode root);
void cbm_run_kotlin_lsp(CBMArena *arena, CBMFileResult *result, const char *source, int source_len,
TSNode root);
/* Cross-file LSP: build a registry from project-wide defs (local + cross-file)
* + stdlib, re-parse the source if no cached tree, walk and resolve calls.
+1240 -951
View File
File diff suppressed because it is too large. Load diff
+48 -56
View File
@@ -5,47 +5,47 @@
#include "scope.h"
#include "type_registry.h"
#include "../cbm.h"
#include "go_lsp.h" // CBMLSPDef, CBMResolvedCallArray reused across languages
#include "go_lsp.h" // CBMLSPDef, CBMResolvedCallArray reused across languages
// Lambda body record. When a lambda is assigned to a name (`fn =
// lambda x: x.method()`), we stash its parameter list + body so the
// next call site (`fn(arg)`) can do call-site driven inference.
typedef struct {
const char* name; // bound name, e.g. "fn"
TSNode lambda_node; // the lambda AST node
const char *name; // bound name, e.g. "fn"
TSNode lambda_node; // the lambda AST node
} CBMLambdaEntry;
// Function-as-dict-value entry. When `funcs = {"a": foo, "b": bar}` is
// seen, we record the per-key target QN so `funcs["a"]()` emits an
// edge to foo.
typedef struct {
const char* var_name; // e.g. "funcs"
const char* literal_key; // e.g. "a"
const char* target_qn; // e.g. "test.main.foo"
const char *var_name; // e.g. "funcs"
const char *literal_key; // e.g. "a"
const char *target_qn; // e.g. "test.main.foo"
} CBMDictLiteralEntry;
// PyLSPContext holds state for one Python file's type evaluation.
typedef struct {
CBMArena* arena;
const char* source;
CBMArena *arena;
const char *source;
int source_len;
const CBMTypeRegistry* registry;
CBMScope* current_scope;
const CBMTypeRegistry *registry;
CBMScope *current_scope;
// Import map: local_name -> module_qn (arena-allocated, NULL-terminated).
// Built from CBMFileResult.imports.
const char** import_local_names;
const char** import_module_qns;
const char **import_local_names;
const char **import_module_qns;
int import_count;
// Current function/class context for resolving `self`/`cls` and emitting
// caller QNs.
const char* enclosing_func_qn;
const char* enclosing_class_qn;
const char* module_qn;
const char *enclosing_func_qn;
const char *enclosing_class_qn;
const char *module_qn;
// Output: resolved calls accumulate here.
CBMResolvedCallArray* resolved_calls;
CBMResolvedCallArray *resolved_calls;
// Syntactic-call list (result->calls), borrowed from the per-file
// extraction result. The downstream pipeline only turns a resolved_call
@@ -57,15 +57,15 @@ typedef struct {
// non-NULL, py_emit_dunder_call injects a matching synthetic CBMCall so the
// recovered dunder call becomes a real CALLS edge. NULL in the cross-file
// path (no result->calls available there). Mirrors RustLSPContext.syn_calls.
CBMCallArray* syn_calls;
CBMCallArray *syn_calls;
// Lambda registry: simple linear list, looked up by name.
CBMLambdaEntry* lambdas;
CBMLambdaEntry *lambdas;
int lambda_count;
int lambda_cap;
// Dict-literal-as-dispatch-table tracking.
CBMDictLiteralEntry* dict_literals;
CBMDictLiteralEntry *dict_literals;
int dict_literal_count;
int dict_literal_cap;
@@ -74,77 +74,69 @@ typedef struct {
} PyLSPContext;
// Initialize a PyLSPContext for processing one file.
void py_lsp_init(PyLSPContext* ctx, CBMArena* arena, const char* source, int source_len,
const CBMTypeRegistry* registry, const char* module_qn, CBMResolvedCallArray* out);
void py_lsp_init(PyLSPContext *ctx, CBMArena *arena, const char *source, int source_len,
const CBMTypeRegistry *registry, const char *module_qn, CBMResolvedCallArray *out);
// Add an import mapping (call once per CBMImport entry).
void py_lsp_add_import(PyLSPContext* ctx, const char* local_name, const char* module_qn);
void py_lsp_add_import(PyLSPContext *ctx, const char *local_name, const char *module_qn);
// Bind every recorded import into the root scope. Idempotent. Called from
// py_lsp_process_file before AST traversal. Exposed for unit tests that
// don't need a tree-sitter parse.
void py_lsp_bind_imports(PyLSPContext* ctx);
void py_lsp_bind_imports(PyLSPContext *ctx);
// Test hook: lookup a name in the context's scope chain. Returns
// cbm_type_unknown() if not bound.
const CBMType* py_lsp_lookup_in_scope(const PyLSPContext* ctx, const char* name);
const CBMType *py_lsp_lookup_in_scope(const PyLSPContext *ctx, const char *name);
// Process all functions/classes in a file's AST, evaluating types and resolving calls.
// root must be the tree-sitter Python `module` node.
void py_lsp_process_file(PyLSPContext* ctx, TSNode root);
void py_lsp_process_file(PyLSPContext *ctx, TSNode root);
// Entry point: build registry from file's own defs + run LSP resolution.
// Mirrors cbm_run_go_lsp / cbm_run_c_lsp.
void cbm_run_py_lsp(CBMArena* arena, CBMFileResult* result,
const char* source, int source_len, TSNode root);
void cbm_run_py_lsp(CBMArena *arena, CBMFileResult *result, const char *source, int source_len,
TSNode root);
// Register Python stdlib types and functions into a registry.
// Auto-generated by scripts/gen-py-stdlib.py from typeshed.
// Phase 10 fills the body; Phase 2 ships a no-op so linkage works.
void cbm_python_stdlib_register(CBMTypeRegistry* reg, CBMArena* arena);
void cbm_python_stdlib_register(CBMTypeRegistry *reg, CBMArena *arena);
// --- Cross-file LSP resolution ---
void cbm_run_py_lsp_cross(
CBMArena* arena,
const char* source, int source_len,
const char* module_qn,
CBMLSPDef* defs, int def_count,
const char** import_names, const char** import_qns, int import_count,
TSTree* cached_tree, // NULL = parse internally
CBMResolvedCallArray* out);
void cbm_run_py_lsp_cross(CBMArena *arena, const char *source, int source_len,
const char *module_qn, CBMLSPDef *defs, int def_count,
const char **import_names, const char **import_qns, int import_count,
TSTree *cached_tree, // NULL = parse internally
CBMResolvedCallArray *out);
/* Tier 2: pre-built per-language registry (mirrors Go pilot).
* Filters all_defs to Python entries, builds + finalizes once. */
CBMTypeRegistry* cbm_py_build_cross_registry(
CBMArena* arena, CBMLSPDef* defs, int def_count);
CBMTypeRegistry *cbm_py_build_cross_registry(CBMArena *arena, CBMLSPDef *defs, int def_count);
void cbm_run_py_lsp_cross_with_registry(
CBMArena* arena,
const char* source, int source_len,
const char* module_qn,
CBMTypeRegistry* reg, // pre-built, finalized, READ-ONLY
const char** import_names, const char** import_qns, int import_count,
TSTree* cached_tree,
CBMResolvedCallArray* out);
void cbm_run_py_lsp_cross_with_registry(CBMArena *arena, const char *source, int source_len,
const char *module_qn,
CBMTypeRegistry *reg, // pre-built, finalized, READ-ONLY
const char **import_names, const char **import_qns,
int import_count, TSTree *cached_tree,
CBMResolvedCallArray *out);
// --- Batch cross-file LSP ---
typedef struct {
const char* source;
const char *source;
int source_len;
const char* module_qn;
TSTree* cached_tree; // from TSTree caching (NULL = parse internally)
CBMLSPDef* defs; // combined file-local + cross-file defs
const char *module_qn;
TSTree *cached_tree; // from TSTree caching (NULL = parse internally)
CBMLSPDef *defs; // combined file-local + cross-file defs
int def_count;
const char** import_names; // parallel arrays, import_count long
const char** import_qns;
const char **import_names; // parallel arrays, import_count long
const char **import_qns;
int import_count;
} CBMBatchPyLSPFile;
void cbm_batch_py_lsp_cross(
CBMArena* arena,
CBMBatchPyLSPFile* files, int file_count,
CBMResolvedCallArray* out);
void cbm_batch_py_lsp_cross(CBMArena *arena, CBMBatchPyLSPFile *files, int file_count,
CBMResolvedCallArray *out);
#endif // CBM_LSP_PY_LSP_H
+1591 -1307
View File
File diff suppressed because it is too large. Load diff
+71 -76
View File
@@ -38,49 +38,49 @@ struct CBMCargoManifest;
* shared override resolver (`pipeline/lsp_resolve.h`) only admits entries
* scoring >= CBM_LSP_CONFIDENCE_FLOOR (0.6). Numbers here mirror the Go
* LSP so the call-edge mix from a polyglot project stays comparable. */
#define CBM_RUST_CONF_DIRECT 0.95f /* path::to::func or alias hit */
#define CBM_RUST_CONF_METHOD 0.95f /* inherent method dispatch */
#define CBM_RUST_CONF_TRAIT_SOLE 0.92f /* trait method, single impl */
#define CBM_RUST_CONF_TRAIT_AMB 0.85f /* trait method, many impls */
#define CBM_RUST_CONF_UFCS 0.93f /* T::method() / Self::new() */
#define CBM_RUST_CONF_PROMOTED 0.90f /* through Deref / blanket impl */
#define CBM_RUST_CONF_MACRO_KNOWN 0.85f /* known std macro mapped to fn */
#define CBM_RUST_CONF_OPERATOR 0.88f /* a+b → T::add (operator trait) */
#define CBM_RUST_CONF_DIRECT 0.95f /* path::to::func or alias hit */
#define CBM_RUST_CONF_METHOD 0.95f /* inherent method dispatch */
#define CBM_RUST_CONF_TRAIT_SOLE 0.92f /* trait method, single impl */
#define CBM_RUST_CONF_TRAIT_AMB 0.85f /* trait method, many impls */
#define CBM_RUST_CONF_UFCS 0.93f /* T::method() / Self::new() */
#define CBM_RUST_CONF_PROMOTED 0.90f /* through Deref / blanket impl */
#define CBM_RUST_CONF_MACRO_KNOWN 0.85f /* known std macro mapped to fn */
#define CBM_RUST_CONF_OPERATOR 0.88f /* a+b → T::add (operator trait) */
/* Rust-flavoured LSP context: one per file, lifetime tied to a single
* `cbm_extract_file()` invocation (or the cross-file caller's arena). */
typedef struct {
CBMArena* arena;
const char* source;
CBMArena *arena;
const char *source;
int source_len;
const CBMTypeRegistry* registry;
CBMScope* current_scope;
const CBMTypeRegistry *registry;
CBMScope *current_scope;
/* `use` map: parallel arrays mapping a local-name (the last segment, or
* the `as` alias) to its full module path (`std::collections::HashMap`,
* `crate::foo::Bar`). Glob imports go in `glob_module_qns` instead. */
const char** use_local_names;
const char** use_module_paths;
const char **use_local_names;
const char **use_module_paths;
int use_count;
const char** glob_module_qns;
const char **glob_module_qns;
int glob_count;
/* Module-qualified name for this file (e.g. "<project>.<crate>.foo"). */
const char* module_qn;
const char *module_qn;
/* Enclosing function context. `enclosing_func_qn` is the QN we attach
* to every emitted CBMResolvedCall as `caller_qn`. */
const char* enclosing_func_qn;
const char *enclosing_func_qn;
/* `Self` resolution: when inside `impl T { ... }` or
* `impl Trait for T { ... }`, `self_type_qn` is `T`'s QN. NULL outside
* an impl. `self_trait_qn` is the trait QN (only set for trait impls)
* — used when emitting trait method calls so we can prefer concrete
* implementations over the trait method when only one impl exists. */
const char* self_type_qn;
const char* self_trait_qn;
const char *self_type_qn;
const char *self_trait_qn;
/* Closure parameter inference: when the call resolver descends into a
* call's argument list and the callee is a method like
@@ -88,7 +88,7 @@ typedef struct {
* of the closure's first param here. The closure_expression handler
* consumes it (and clears it) when binding params, so a chain like
* `vec.iter().map(|x| x.method())` resolves `x.method()` correctly. */
const CBMType* pending_closure_param_type;
const CBMType *pending_closure_param_type;
/* User-defined `macro_rules!` definitions collected during the
* pre-walk phase. Each rule stores its pattern + transcriber text;
@@ -96,7 +96,7 @@ typedef struct {
* against each rule and then substitutes/re-walks the expansion.
*
* The arrays are doubling-grown out of `arena`. */
struct RustMacroRule** macro_rules_arr;
struct RustMacroRule **macro_rules_arr;
int macro_rules_count;
/* Recursion guard for macro expansion. Real macro_rules! can be
@@ -119,7 +119,7 @@ typedef struct {
* falling through to module-prefix fallback. NULL when no manifest
* is available — the resolver still works, just without workspace
* awareness. Owned by the caller; the RustLSPContext only borrows. */
const struct CBMCargoManifest* cargo_manifest;
const struct CBMCargoManifest *cargo_manifest;
/* Chalk-lite trait-bound environment for the *currently-active*
* function or impl. Populated at function/impl entry from the
@@ -132,19 +132,19 @@ typedef struct {
* Arrays are arena-allocated and reset on every function entry.
*/
struct {
const char* param_name; /* "T", "U", "Item" */
const char* trait_qn; /* "core.clone.Clone" */
const char *param_name; /* "T", "U", "Item" */
const char *trait_qn; /* "core.clone.Clone" */
} *type_param_bounds;
int type_param_bound_count;
struct {
const char* alias_name; /* "U" */
const char* aliased_to; /* "T.Item" (representational) */
const char *alias_name; /* "U" */
const char *aliased_to; /* "T.Item" (representational) */
} *type_param_aliases;
int type_param_alias_count;
/* Output: resolved (and unresolved-with-reason) calls accumulate here. */
CBMResolvedCallArray* resolved_calls;
CBMResolvedCallArray *resolved_calls;
/* Syntactic-call list (result->calls), borrowed from the per-file
* extraction result. The downstream pipeline only turns a resolved_call
@@ -156,7 +156,7 @@ typedef struct {
* see them. When this is non-NULL the resolver injects matching synthetic
* CBMCall entries so those recovered calls become real edges. NULL in the
* cross-file path (no result available). */
CBMCallArray* syn_calls;
CBMCallArray *syn_calls;
/* While >0, rust_emit_resolved_call also injects a matching synthetic
* CBMCall into `syn_calls` so the recovered call becomes an edge. Set
@@ -169,23 +169,24 @@ typedef struct {
} RustLSPContext;
/* Initialise an empty context for processing one file. */
void rust_lsp_init(RustLSPContext* ctx, CBMArena* arena, const char* source, int source_len,
const CBMTypeRegistry* registry, const char* module_qn, CBMResolvedCallArray* out);
void rust_lsp_init(RustLSPContext *ctx, CBMArena *arena, const char *source, int source_len,
const CBMTypeRegistry *registry, const char *module_qn,
CBMResolvedCallArray *out);
/* Register a `use` alias. `local_name` may be the last segment of the path
* (`HashMap` for `use std::collections::HashMap`) or an `as` alias. The
* full `module_path` is stored verbatim (e.g. `std::collections::HashMap`).
* Glob imports (`use foo::*`) go through `rust_lsp_add_glob` instead. */
void rust_lsp_add_use(RustLSPContext* ctx, const char* local_name, const char* module_path);
void rust_lsp_add_glob(RustLSPContext* ctx, const char* module_qn);
void rust_lsp_add_use(RustLSPContext *ctx, const char *local_name, const char *module_path);
void rust_lsp_add_glob(RustLSPContext *ctx, const char *module_qn);
/* Process every function/method in the file, walking statements and
* evaluating expression types as we go. */
void rust_lsp_process_file(RustLSPContext* ctx, TSNode root);
void rust_lsp_process_file(RustLSPContext *ctx, TSNode root);
/* Evaluate the static type of a Rust expression node. Returns
* `cbm_type_unknown()` for anything we cannot type. */
const CBMType* rust_eval_expr_type(RustLSPContext* ctx, TSNode node);
const CBMType *rust_eval_expr_type(RustLSPContext *ctx, TSNode node);
/* Bidirectional variant: evaluate an expression with an `expected`
* hint available. The hint is used to:
@@ -199,27 +200,27 @@ const CBMType* rust_eval_expr_type(RustLSPContext* ctx, TSNode node);
* `expected` may be NULL (no hint). If the synthesised type matches
* the expected, returns it; if a hint resolves an ambiguity that the
* synthesis path cannot, returns the hint-substituted form. */
const CBMType* rust_eval_expr_typed(RustLSPContext* ctx, TSNode node, const CBMType* expected);
const CBMType *rust_eval_expr_typed(RustLSPContext *ctx, TSNode node, const CBMType *expected);
/* Convert a `*type*` AST node (type_identifier, scoped_type_identifier,
* reference_type, primitive_type, …) into a CBMType. */
const CBMType* rust_parse_type_node(RustLSPContext* ctx, TSNode node);
const CBMType *rust_parse_type_node(RustLSPContext *ctx, TSNode node);
/* Bind the bindings introduced by a let/for/match-pattern statement into
* the current scope. */
void rust_process_statement(RustLSPContext* ctx, TSNode node);
void rust_process_statement(RustLSPContext *ctx, TSNode node);
/* Look up an inherent method or field promoted through Deref / embedded
* trait blanket impls. Returns NULL if nothing matches. */
const CBMRegisteredFunc* rust_lookup_method(RustLSPContext* ctx,
const char* type_qn, const char* member_name);
const CBMRegisteredFunc *rust_lookup_method(RustLSPContext *ctx, const char *type_qn,
const char *member_name);
/* Entry point — called from `cbm_extract_file()` after the unified
* extractor has filled `result->defs`, `result->imports`, and
* `result->impl_traits`. Builds a per-file registry, parses the `use`
* graph, and walks every function body emitting CBMResolvedCall entries. */
void cbm_run_rust_lsp(CBMArena* arena, CBMFileResult* result,
const char* source, int source_len, TSNode root);
void cbm_run_rust_lsp(CBMArena *arena, CBMFileResult *result, const char *source, int source_len,
TSNode root);
/* Synthesize call edges for curated attribute proc-macros.
*
@@ -232,15 +233,15 @@ void cbm_run_rust_lsp(CBMArena* arena, CBMFileResult* result,
* direct call attributions.
*
* Called automatically from `cbm_run_rust_lsp_with_manifest`. */
void cbm_rust_synth_proc_macro_edges(CBMArena* arena, CBMFileResult* result);
void cbm_rust_synth_proc_macro_edges(CBMArena *arena, CBMFileResult *result);
/* Same as `cbm_run_rust_lsp`, but accepts an optional Cargo manifest
* so the resolver can route paths whose head is a workspace member /
* declared dependency. Pass NULL to fall back to the manifest-free
* behaviour. */
void cbm_run_rust_lsp_with_manifest(CBMArena* arena, CBMFileResult* result,
const char* source, int source_len, TSNode root,
const struct CBMCargoManifest* manifest);
void cbm_run_rust_lsp_with_manifest(CBMArena *arena, CBMFileResult *result, const char *source,
int source_len, TSNode root,
const struct CBMCargoManifest *manifest);
/* Register a curated subset of the Rust core/alloc/std prelude into the
* given registry. The seed is intentionally compact (~150 types, ~600
@@ -248,13 +249,13 @@ void cbm_run_rust_lsp_with_manifest(CBMArena* arena, CBMFileResult* result,
* plus the prelude trait method names (`clone`, `to_string`, `into`, …).
* Generated from `scripts/gen-rust-stdlib.py` if present, otherwise the
* hand-written `rust_stdlib_data.c` module is used. */
void cbm_rust_stdlib_register(CBMTypeRegistry* reg, CBMArena* arena);
void cbm_rust_stdlib_register(CBMTypeRegistry *reg, CBMArena *arena);
/* Register seeds for commonly-used external crates (serde, tokio,
* anyhow, clap, regex, log, futures, parking_lot, once_cell, chrono,
* uuid, reqwest, rayon). Best-effort curated entries — RUST_LSP_FOLLOWUP
* §A3. Calls into crates not in this seed remain `unresolved`. */
void cbm_rust_crates_register(CBMTypeRegistry* reg, CBMArena* arena);
void cbm_rust_crates_register(CBMTypeRegistry *reg, CBMArena *arena);
/* --- Cross-file LSP resolution --- */
@@ -263,46 +264,40 @@ void cbm_rust_crates_register(CBMTypeRegistry* reg, CBMArena* arena);
* — see that header for field semantics. We declare a separate typedef
* to allow Rust-specific fields without touching the Go ABI. */
typedef struct {
const char* qualified_name;
const char* short_name;
const char* label; /* "Function", "Method", "Type", "Trait" */
const char* receiver_type; /* for methods: receiver type QN (NULL for free fns) */
const char* def_module_qn; /* module QN where this def lives */
const char* return_types; /* "|"-separated return type texts */
const char* embedded_types; /* "|"-separated embedded type QNs */
const char* field_defs; /* "|"-separated "name:type" pairs */
const char* method_names_str;/* "|"-separated method names for traits */
const char* trait_qn; /* impl Trait for Type → set on Method defs */
bool is_interface; /* true for traits */
const char *qualified_name;
const char *short_name;
const char *label; /* "Function", "Method", "Type", "Trait" */
const char *receiver_type; /* for methods: receiver type QN (NULL for free fns) */
const char *def_module_qn; /* module QN where this def lives */
const char *return_types; /* "|"-separated return type texts */
const char *embedded_types; /* "|"-separated embedded type QNs */
const char *field_defs; /* "|"-separated "name:type" pairs */
const char *method_names_str; /* "|"-separated method names for traits */
const char *trait_qn; /* impl Trait for Type → set on Method defs */
bool is_interface; /* true for traits */
} CBMRustLSPDef;
/* Run cross-file resolution on a single file. */
void cbm_run_rust_lsp_cross(
CBMArena* arena,
const char* source, int source_len,
const char* module_qn,
CBMRustLSPDef* defs, int def_count,
const char** import_names, const char** import_qns, int import_count,
TSTree* cached_tree,
CBMResolvedCallArray* out);
void cbm_run_rust_lsp_cross(CBMArena *arena, const char *source, int source_len,
const char *module_qn, CBMRustLSPDef *defs, int def_count,
const char **import_names, const char **import_qns, int import_count,
TSTree *cached_tree, CBMResolvedCallArray *out);
/* Per-file input for batch cross-file Rust LSP processing. */
typedef struct {
const char* source;
const char *source;
int source_len;
const char* module_qn;
TSTree* cached_tree;
CBMRustLSPDef* defs;
const char *module_qn;
TSTree *cached_tree;
CBMRustLSPDef *defs;
int def_count;
const char** import_names;
const char** import_qns;
const char **import_names;
const char **import_qns;
int import_count;
} CBMBatchRustLSPFile;
/* Process several files in one CGo call (per-file arenas, result copy). */
void cbm_batch_rust_lsp_cross(
CBMArena* arena,
CBMBatchRustLSPFile* files, int file_count,
CBMResolvedCallArray* out);
void cbm_batch_rust_lsp_cross(CBMArena *arena, CBMBatchRustLSPFile *files, int file_count,
CBMResolvedCallArray *out);
#endif /* CBM_LSP_RUST_LSP_H */
-4
View File
@@ -198,11 +198,9 @@ static void handle_route_registration(cbm_pipeline_ctx_t *ctx, const CBMCall *ca
"{\"callee\":\"%s\",\"url_path\":\"%s\",\"via\":\"route_registration\"}",
call->callee_name, call->first_string_arg);
cbm_gbuf_insert_edge(ctx->gbuf, source_node->id, route_id, "CALLS", props);
if (getenv("CBM_DBG_ROUTE")) fprintf(stderr, "[DBG] route_reg callee=%s path=%s handler=%s\n", call->callee_name, call->first_string_arg, call->second_arg_name?call->second_arg_name:"(null)");
if (call->second_arg_name != NULL && call->second_arg_name[0] != '\0') {
cbm_resolution_t hres = cbm_registry_resolve(ctx->registry, call->second_arg_name,
module_qn, imp_keys, imp_vals, imp_count);
if (getenv("CBM_DBG_ROUTE")) fprintf(stderr, "[DBG] handler '%s' resolved=%s\n", call->second_arg_name, hres.qualified_name?hres.qualified_name:"(null)");
if (hres.qualified_name != NULL && hres.qualified_name[0] != '\0') {
const cbm_gbuf_node_t *handler = cbm_gbuf_find_by_qn(ctx->gbuf, hres.qualified_name);
if (handler != NULL) {
@@ -283,8 +281,6 @@ static void emit_classified_edge(cbm_pipeline_ctx_t *ctx, const CBMCall *call,
const cbm_resolution_t *res, const char *module_qn,
const char **imp_keys, const char **imp_vals, int imp_count) {
cbm_svc_kind_t svc = cbm_service_pattern_match(res->qualified_name);
if (getenv("CBM_DBG_SVC") && call->callee_name && (strstr(call->callee_name,"Route")||strstr(call->callee_name,"client")||strstr(call->callee_name,"Client")||strstr(call->callee_name,"RestClient")))
fprintf(stderr, "[DBGSVC] callee=%s resolved=%s svc=%d strarg=%s\n", call->callee_name, res->qualified_name?res->qualified_name:"(null)", svc, call->first_string_arg?call->first_string_arg:"(null)");
if (svc == CBM_SVC_ROUTE_REG && call->first_string_arg && call->first_string_arg[0] == '/') {
handle_route_registration(ctx, call, source, module_qn, imp_keys, imp_vals, imp_count);
return;
+6 -9
View File
@@ -275,11 +275,9 @@ static void k8s_scan_labels(const char *source, k8s_record_t *rec) {
char val[K8S_LABEL_LEN];
if (k8s_split_kv(trimmed, key, sizeof(key), val, sizeof(val))) {
/* Build the current dotted path for context decisions. */
bool under_selector =
(depth >= 1 && strcmp(stack[depth - 1].key, "selector") == 0);
bool under_selector = (depth >= 1 && strcmp(stack[depth - 1].key, "selector") == 0);
bool under_labels = (depth >= 1 && strcmp(stack[depth - 1].key, "labels") == 0);
bool under_metadata =
(depth >= 1 && strcmp(stack[depth - 1].key, "metadata") == 0);
bool under_metadata = (depth >= 1 && strcmp(stack[depth - 1].key, "metadata") == 0);
if (val[0] == '\0') {
/* Block-opening key: push onto the path stack. */
@@ -586,9 +584,9 @@ static int parse_requirements_deps(cbm_pipeline_ctx_t *ctx, const cbm_gbuf_node_
if (t[0] && t[0] != '#' && t[0] != '-' && strstr(t, "://") == NULL) {
char name[CBM_SZ_256];
size_t n = 0;
while (t[n] && t[n] != '=' && t[n] != '<' && t[n] != '>' && t[n] != '!' && t[n] != '~' &&
t[n] != '[' && t[n] != ';' && t[n] != ' ' && t[n] != '\t' && t[n] != '\r' &&
n + 1 < sizeof(name)) {
while (t[n] && t[n] != '=' && t[n] != '<' && t[n] != '>' && t[n] != '!' &&
t[n] != '~' && t[n] != '[' && t[n] != ';' && t[n] != ' ' && t[n] != '\t' &&
t[n] != '\r' && n + 1 < sizeof(name)) {
name[n] = t[n];
n++;
}
@@ -675,8 +673,7 @@ int cbm_pipeline_pass_k8s(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files,
* pass and contain no "Resource" definitions. Pass the already-
* read source buffer so handle_k8s_manifest does not re-read. */
(void)cached; /* cached YAML result intentionally discarded */
k8s_record_t *rec =
(recs.count < recs.cap) ? &recs.items[recs.count] : NULL;
k8s_record_t *rec = (recs.count < recs.cap) ? &recs.items[recs.count] : NULL;
handle_k8s_manifest(ctx, path, rel, source, src_len, rec);
if (rec && rec->node_id > 0) {
recs.count++;
+2 -2
View File
@@ -399,8 +399,8 @@ void cbm_pxc_run_one(CBMLanguage lang, CBMFileResult *r, const char *source, int
imp_qns, imp_count, tree, &out);
break;
case CBM_LANG_KOTLIN:
cbm_run_kotlin_lsp_cross(&scratch, source, source_len, module_qn, defs, def_count, imp_names,
imp_qns, imp_count, tree, &out);
cbm_run_kotlin_lsp_cross(&scratch, source, source_len, module_qn, defs, def_count,
imp_names, imp_qns, imp_count, tree, &out);
break;
default:
break;
+2 -2
View File
@@ -1941,8 +1941,8 @@ static void resolve_def_decorators(resolve_ctx_t *rc, resolve_worker_state_t *ws
* and the edge target IDs are remapped consistently. */
char syn_qn[CBM_SZ_512];
snprintf(syn_qn, sizeof(syn_qn), "<decorator:%s>", fn);
dn_id = cbm_gbuf_upsert_node(ws->local_edge_buf, "Decorator", fn, syn_qn, "", 0, 0,
"{}");
dn_id =
cbm_gbuf_upsert_node(ws->local_edge_buf, "Decorator", fn, syn_qn, "", 0, 0, "{}");
}
if (dn_id != 0 && node->id != dn_id) {
char dp[CBM_SZ_256];
+3 -2
View File
@@ -1275,8 +1275,9 @@ static bool import_targetable_label(const char *label) {
* File/Module-node QN (extension is stripped by fqn_module). Returns a borrowed
* node or NULL. Several filename conventions are tried in turn. */
static const cbm_gbuf_node_t *resolve_sibling_file(const cbm_pipeline_ctx_t *ctx,
const char *source_rel, const char *source_file_qn,
const char *module_path) {
const char *source_rel,
const char *source_file_qn,
const char *module_path) {
if (!module_path || !module_path[0]) {
return NULL;
}
+6 -5
View File
@@ -252,7 +252,8 @@ static int check_go_class_implements(cbm_pipeline_ctx_t *ctx, const cbm_gbuf_nod
* DEFINES_METHOD edges from the class. */
const cbm_gbuf_edge_t **cls_dm = NULL;
int cls_dm_count = 0;
cbm_gbuf_find_edges_by_source_type(ctx->gbuf, cls->id, "DEFINES_METHOD", &cls_dm, &cls_dm_count);
cbm_gbuf_find_edges_by_source_type(ctx->gbuf, cls->id, "DEFINES_METHOD", &cls_dm,
&cls_dm_count);
char prefix[CBM_SZ_512];
snprintf(prefix, sizeof(prefix), "%s.", cls->qualified_name);
@@ -365,8 +366,7 @@ static void resolve_decorator(cbm_pipeline_ctx_t *ctx, const cbm_gbuf_node_t *no
}
cbm_resolution_t res =
cbm_registry_resolve(ctx->registry, func_name, module_qn, imp_keys, imp_vals, imp_count);
if ((!res.qualified_name || res.qualified_name[0] == '\0') &&
!strchr(func_name, '.')) {
if ((!res.qualified_name || res.qualified_name[0] == '\0') && !strchr(func_name, '.')) {
/* C# attributes are referenced by their short name (`[Log]`) but declared
* with the conventional `Attribute` suffix (`class LogAttribute`). Retry
* with the suffix appended so the declaration resolves. */
@@ -432,8 +432,9 @@ static void sem_process_def_edges(cbm_pipeline_ctx_t *ctx, const CBMDefinition *
* (Java `implements`, C# `: IFace`, TS `implements`); a Class/
* Type/Enum base is plain INHERITS. */
const char *base_label = cbm_registry_label_of(ctx->registry, base_qn);
const char *edge_type =
(base_label && strcmp(base_label, "Interface") == 0) ? "IMPLEMENTS" : "INHERITS";
const char *edge_type = (base_label && strcmp(base_label, "Interface") == 0)
? "IMPLEMENTS"
: "INHERITS";
cbm_gbuf_insert_edge(ctx->gbuf, node->id, base_node->id, edge_type, "{}");
(*inherits_count)++;
}
+2 -3
View File
@@ -399,9 +399,8 @@ static int process_one_infra_binding(cbm_gbuf_t *gbuf, const CBMInfraBinding *ib
* upsert its Route node so the binding maps even when no code-side dispatch
* call created the node first (e.g. a standalone scheduler/subscription
* manifest). */
topic_route_id =
cbm_gbuf_upsert_node(gbuf, "Route", ib->source_name, topic_route_qn, rel_path, 0, 0,
ib->broker ? ib->broker : "async");
topic_route_id = cbm_gbuf_upsert_node(gbuf, "Route", ib->source_name, topic_route_qn,
rel_path, 0, 0, ib->broker ? ib->broker : "async");
if (topic_route_id <= 0) {
return 0;
}
File diff suppressed because it is too large. Load diff
+9 -7
View File
@@ -276,8 +276,9 @@ TEST(grammar_label_goldens) {
}
}
if (missing > 0) {
fprintf(stderr, " [LABEL] %d grammar(s) missing a golden — copy the [LABEL-CAPTURE] lines "
"into LABEL_GOLDENS\n",
fprintf(stderr,
" [LABEL] %d grammar(s) missing a golden — copy the [LABEL-CAPTURE] lines "
"into LABEL_GOLDENS\n",
missing);
}
ASSERT_EQ(failures, 0);
@@ -305,9 +306,9 @@ static int non_module_defs(CBMFileResult *r) {
* config / markup grammars (json/yaml/css/…) are intentionally excluded — a
* lone Module node is correct for them. */
static const char *MUST_EXTRACT_DEFS[] = {
"agda", "pony", "move", "cobol", "janet", "pine", "smali", "verilog",
"vhdl", "systemverilog", "protobuf", "graphql", "thrift", "capnp", "smithy",
"wit", "prisma", "cmake", "puppet", "tablegen", "assembly", "nasm", NULL};
"agda", "pony", "move", "cobol", "janet", "pine", "smali", "verilog",
"vhdl", "systemverilog", "protobuf", "graphql", "thrift", "capnp", "smithy", "wit",
"prisma", "cmake", "puppet", "tablegen", "assembly", "nasm", NULL};
TEST(grammar_code_extracts_defs) {
int failures = 0;
@@ -335,8 +336,9 @@ TEST(grammar_code_extracts_defs) {
int nd = non_module_defs(r);
cbm_free_result(r);
if (nd < 1) {
fprintf(stderr, " [CODE-DEFS] FAIL %-14s extracts 0 non-Module defs (under-extraction "
"gap — only a Module node)\n",
fprintf(stderr,
" [CODE-DEFS] FAIL %-14s extracts 0 non-Module defs (under-extraction "
"gap — only a Module node)\n",
name);
failures++;
}
+239 -281
View File
@@ -64,39 +64,45 @@ typedef struct {
} GpgProj;
typedef struct {
const char *name; /* relative filename, may include '/' for subdirs */
const char *name; /* relative filename, may include '/' for subdirs */
const char *content;
} GpgFile;
static void gpg_to_fwd_slashes(char *p) {
for (; *p; p++) {
if (*p == '\\') *p = '/';
if (*p == '\\')
*p = '/';
}
}
static cbm_store_t *gpg_open_indexed(GpgProj *lp) {
lp->project = cbm_project_name_from_path(lp->tmpdir);
if (!lp->project) return NULL;
if (!lp->project)
return NULL;
const char *home = getenv("HOME");
if (!home) home = "/tmp";
if (!home)
home = "/tmp";
char cache_dir[512];
snprintf(cache_dir, sizeof(cache_dir), "%s/.cache/codebase-memory-mcp", home);
cbm_mkdir(cache_dir);
snprintf(lp->dbpath, sizeof(lp->dbpath), "%s/%s.db", cache_dir, lp->project);
unlink(lp->dbpath);
lp->srv = cbm_mcp_server_new(NULL);
if (!lp->srv) return NULL;
if (!lp->srv)
return NULL;
char args[700];
snprintf(args, sizeof(args), "{\"repo_path\":\"%s\"}", lp->tmpdir);
char *resp = cbm_mcp_handle_tool(lp->srv, "index_repository", args);
if (resp) free(resp);
if (resp)
free(resp);
return cbm_store_open_path(lp->dbpath);
}
static cbm_store_t *gpg_index_files(GpgProj *lp, const GpgFile *files, int nfiles) {
memset(lp, 0, sizeof(*lp));
snprintf(lp->tmpdir, sizeof(lp->tmpdir), "/tmp/cbm_gpg_XXXXXX");
if (!cbm_mkdtemp(lp->tmpdir)) return NULL;
if (!cbm_mkdtemp(lp->tmpdir))
return NULL;
gpg_to_fwd_slashes(lp->tmpdir);
for (int i = 0; i < nfiles; i++) {
char path[700];
@@ -108,7 +114,8 @@ static cbm_store_t *gpg_index_files(GpgProj *lp, const GpgFile *files, int nfile
*slash = '/';
}
FILE *f = fopen(path, "wb");
if (!f) return NULL;
if (!f)
return NULL;
fputs(files[i].content, f);
fclose(f);
}
@@ -116,8 +123,12 @@ static cbm_store_t *gpg_index_files(GpgProj *lp, const GpgFile *files, int nfile
}
static void gpg_cleanup(GpgProj *lp, cbm_store_t *store) {
if (store) cbm_store_close(store);
if (lp->srv) { cbm_mcp_server_free(lp->srv); lp->srv = NULL; }
if (store)
cbm_store_close(store);
if (lp->srv) {
cbm_mcp_server_free(lp->srv);
lp->srv = NULL;
}
free(lp->project);
lp->project = NULL;
th_rmtree(lp->tmpdir);
@@ -147,8 +158,8 @@ typedef struct {
int classes;
int variables;
int sections;
int imports; /* IMPORTS edges */
int depends; /* DEPENDS_ON edges */
int imports; /* IMPORTS edges */
int depends; /* DEPENDS_ON edges */
} GpgMetrics;
static GpgMetrics gpg_metrics_files(const GpgFile *files, int nfiles) {
@@ -156,14 +167,14 @@ static GpgMetrics gpg_metrics_files(const GpgFile *files, int nfiles) {
cbm_store_t *store = gpg_index_files(&lp, files, nfiles);
GpgMetrics m = {0};
if (store) {
m.ok = 1;
m.ok = 1;
m.total_nodes = cbm_store_count_nodes(store, lp.project);
m.modules = gpg_count_label(store, lp.project, "Module");
m.classes = gpg_count_label(store, lp.project, "Class");
m.variables = gpg_count_label(store, lp.project, "Variable");
m.sections = gpg_count_label(store, lp.project, "Section");
m.imports = cbm_store_count_edges_by_type(store, lp.project, "IMPORTS");
m.depends = cbm_store_count_edges_by_type(store, lp.project, "DEPENDS_ON");
m.modules = gpg_count_label(store, lp.project, "Module");
m.classes = gpg_count_label(store, lp.project, "Class");
m.variables = gpg_count_label(store, lp.project, "Variable");
m.sections = gpg_count_label(store, lp.project, "Section");
m.imports = cbm_store_count_edges_by_type(store, lp.project, "IMPORTS");
m.depends = cbm_store_count_edges_by_type(store, lp.project, "DEPENDS_ON");
}
gpg_cleanup(&lp, store);
return m;
@@ -184,10 +195,9 @@ static GpgMetrics gpg_metrics(const char *filename, const char *content) {
/* HTML: minimal page → at least 1 Module node, no crash. */
TEST(probe_html_minimal_page) {
GpgMetrics m = gpg_metrics("index.html",
"<!DOCTYPE html>\n"
"<html><head><title>Test</title></head>\n"
"<body><h1>Hello</h1></body></html>\n");
GpgMetrics m = gpg_metrics("index.html", "<!DOCTYPE html>\n"
"<html><head><title>Test</title></head>\n"
"<body><h1>Hello</h1></body></html>\n");
ASSERT_TRUE(m.ok);
/* GREEN: HTML always produces at least a Module node. */
ASSERT_TRUE(m.modules >= 1);
@@ -198,15 +208,11 @@ TEST(probe_html_minimal_page) {
* RED: no HTML import resolver; script/link attributes are not walked into
* IMPORTS edges by the pipeline. */
TEST(probe_html_script_imports) {
static const GpgFile files[] = {
{"app.js",
"function init() { console.log('ready'); }\n"},
{"index.html",
"<!DOCTYPE html><html><head>\n"
"<link rel=\"stylesheet\" href=\"style.css\">\n"
"<script src=\"app.js\"></script>\n"
"</head><body></body></html>\n"}
};
static const GpgFile files[] = {{"app.js", "function init() { console.log('ready'); }\n"},
{"index.html", "<!DOCTYPE html><html><head>\n"
"<link rel=\"stylesheet\" href=\"style.css\">\n"
"<script src=\"app.js\"></script>\n"
"</head><body></body></html>\n"}};
GpgMetrics m = gpg_metrics_files(files, 2);
ASSERT_TRUE(m.ok);
/* RED: HTML <script src> / <link href> not resolved into IMPORTS edges. */
@@ -216,8 +222,7 @@ TEST(probe_html_script_imports) {
/* HTML: pure-data — no Class/Variable/Section nodes expected. */
TEST(probe_html_no_structural_nodes) {
GpgMetrics m = gpg_metrics("page.html",
"<html><body><p>text</p></body></html>\n");
GpgMetrics m = gpg_metrics("page.html", "<html><body><p>text</p></body></html>\n");
ASSERT_TRUE(m.ok);
/* GREEN: HTML produces zero Class/Variable/Section nodes (pure-data). */
ASSERT_TRUE(m.classes == 0);
@@ -236,12 +241,11 @@ TEST(probe_html_no_structural_nodes) {
/* XML: minimal document → Module node + at least 2 Class nodes. */
TEST(probe_xml_element_nodes) {
GpgMetrics m = gpg_metrics("config.xml",
"<?xml version=\"1.0\"?>\n"
"<config>\n"
" <server host=\"localhost\" port=\"8080\"/>\n"
" <database name=\"mydb\"/>\n"
"</config>\n");
GpgMetrics m = gpg_metrics("config.xml", "<?xml version=\"1.0\"?>\n"
"<config>\n"
" <server host=\"localhost\" port=\"8080\"/>\n"
" <database name=\"mydb\"/>\n"
"</config>\n");
ASSERT_TRUE(m.ok);
/* GREEN: XML extractor produces Class nodes for elements. */
ASSERT_TRUE(m.classes >= 2);
@@ -251,12 +255,11 @@ TEST(probe_xml_element_nodes) {
/* XML: more elements → more Class nodes (≥ 2). */
TEST(probe_xml_multiple_elements) {
GpgMetrics m = gpg_metrics("pom.xml",
"<project>\n"
" <groupId>com.example</groupId>\n"
" <artifactId>demo</artifactId>\n"
" <version>1.0</version>\n"
"</project>\n");
GpgMetrics m = gpg_metrics("pom.xml", "<project>\n"
" <groupId>com.example</groupId>\n"
" <artifactId>demo</artifactId>\n"
" <version>1.0</version>\n"
"</project>\n");
ASSERT_TRUE(m.ok);
/* GREEN: multiple element types must produce Class nodes. */
ASSERT_TRUE(m.classes >= 2);
@@ -265,8 +268,7 @@ TEST(probe_xml_multiple_elements) {
/* XML: no-crash guard for empty document. */
TEST(probe_xml_no_crash) {
GpgMetrics m = gpg_metrics("empty.xml",
"<?xml version=\"1.0\"?><root/>\n");
GpgMetrics m = gpg_metrics("empty.xml", "<?xml version=\"1.0\"?><root/>\n");
ASSERT_TRUE(m.ok);
ASSERT_TRUE(m.total_nodes >= 1);
PASS();
@@ -282,13 +284,12 @@ TEST(probe_xml_no_crash) {
/* CSS: rules file → only a Module node. */
TEST(probe_css_module_only) {
GpgMetrics m = gpg_metrics("styles.css",
"body {\n"
" margin: 0;\n"
" font-family: sans-serif;\n"
"}\n"
".container { max-width: 1200px; }\n"
"#header { background: #fff; }\n");
GpgMetrics m = gpg_metrics("styles.css", "body {\n"
" margin: 0;\n"
" font-family: sans-serif;\n"
"}\n"
".container { max-width: 1200px; }\n"
"#header { background: #fff; }\n");
ASSERT_TRUE(m.ok);
/* GREEN: CSS produces exactly 1 Module node; no Class/Variable/Section. */
ASSERT_TRUE(m.modules == 1);
@@ -300,13 +301,9 @@ TEST(probe_css_module_only) {
/* CSS: @import in two-file fixture → IMPORTS edge.
* RED: no CSS import resolver in the pipeline. */
TEST(probe_css_at_import) {
static const GpgFile files[] = {
{"base.css",
"* { box-sizing: border-box; }\n"},
{"main.css",
"@import \"base.css\";\n"
"body { margin: 0; }\n"}
};
static const GpgFile files[] = {{"base.css", "* { box-sizing: border-box; }\n"},
{"main.css", "@import \"base.css\";\n"
"body { margin: 0; }\n"}};
GpgMetrics m = gpg_metrics_files(files, 2);
ASSERT_TRUE(m.ok);
/* RED: CSS @import not resolved into IMPORTS edges. */
@@ -324,14 +321,13 @@ TEST(probe_css_at_import) {
/* SCSS: variables + rules → at least 1 Variable node. */
TEST(probe_scss_variable_node) {
GpgMetrics m = gpg_metrics("theme.scss",
"$primary: #3498db;\n"
"$secondary: #2ecc71;\n"
"\n"
".button {\n"
" background: $primary;\n"
" color: white;\n"
"}\n");
GpgMetrics m = gpg_metrics("theme.scss", "$primary: #3498db;\n"
"$secondary: #2ecc71;\n"
"\n"
".button {\n"
" background: $primary;\n"
" color: white;\n"
"}\n");
ASSERT_TRUE(m.ok);
/* GREEN: SCSS extracts at least 1 Variable node (histogram: Variable:1). */
ASSERT_TRUE(m.variables >= 1);
@@ -342,13 +338,9 @@ TEST(probe_scss_variable_node) {
/* SCSS: @import / @use in two-file fixture → IMPORTS edge.
* RED: no SCSS import resolver in the pipeline. */
TEST(probe_scss_at_use_imports) {
static const GpgFile files[] = {
{"_vars.scss",
"$brand: #e74c3c;\n"},
{"main.scss",
"@use 'vars';\n"
".hero { color: vars.$brand; }\n"}
};
static const GpgFile files[] = {{"_vars.scss", "$brand: #e74c3c;\n"},
{"main.scss", "@use 'vars';\n"
".hero { color: vars.$brand; }\n"}};
GpgMetrics m = gpg_metrics_files(files, 2);
ASSERT_TRUE(m.ok);
/* RED: SCSS @use / @import not resolved into IMPORTS edges. */
@@ -365,14 +357,13 @@ TEST(probe_scss_at_use_imports) {
/* Markdown: document with headings → at least 1 Section node. */
TEST(probe_markdown_section_node) {
GpgMetrics m = gpg_metrics("README.md",
"# Project Title\n"
"\n"
"Description text.\n"
"\n"
"## Getting Started\n"
"\n"
"Install with `npm install`.\n");
GpgMetrics m = gpg_metrics("README.md", "# Project Title\n"
"\n"
"Description text.\n"
"\n"
"## Getting Started\n"
"\n"
"Install with `npm install`.\n");
ASSERT_TRUE(m.ok);
/* GREEN: Markdown headings produce Section nodes. */
ASSERT_TRUE(m.sections >= 1);
@@ -383,11 +374,10 @@ TEST(probe_markdown_section_node) {
/* Markdown: multiple headings → at least 1 Section (histogram baseline: Section:1).
* The histogram fixture has 1 section; additional headings may or may not add more. */
TEST(probe_markdown_multi_heading) {
GpgMetrics m = gpg_metrics("docs.md",
"# Title\n"
"## Setup\n"
"### Install\n"
"## Usage\n");
GpgMetrics m = gpg_metrics("docs.md", "# Title\n"
"## Setup\n"
"### Install\n"
"## Usage\n");
ASSERT_TRUE(m.ok);
/* GREEN: at minimum 1 Section node. */
ASSERT_TRUE(m.sections >= 1);
@@ -396,8 +386,7 @@ TEST(probe_markdown_multi_heading) {
/* Markdown: no Class/Variable nodes (pure-data beyond Section). */
TEST(probe_markdown_no_class_variable) {
GpgMetrics m = gpg_metrics("notes.md",
"# Notes\n\nSome content.\n");
GpgMetrics m = gpg_metrics("notes.md", "# Notes\n\nSome content.\n");
ASSERT_TRUE(m.ok);
/* GREEN: Markdown does not produce Class or Variable nodes. */
ASSERT_TRUE(m.classes == 0);
@@ -414,16 +403,15 @@ TEST(probe_markdown_no_class_variable) {
/* RST: document with sections → Module:1, no Section nodes. */
TEST(probe_rst_module_only) {
GpgMetrics m = gpg_metrics("docs.rst",
"Project Title\n"
"=============\n"
"\n"
"Description text.\n"
"\n"
"Installation\n"
"------------\n"
"\n"
"Run ``pip install mypackage``.\n");
GpgMetrics m = gpg_metrics("docs.rst", "Project Title\n"
"=============\n"
"\n"
"Description text.\n"
"\n"
"Installation\n"
"------------\n"
"\n"
"Run ``pip install mypackage``.\n");
ASSERT_TRUE(m.ok);
/* GREEN: RST produces only a Module node (no Section nodes extracted). */
ASSERT_TRUE(m.modules == 1);
@@ -433,9 +421,8 @@ TEST(probe_rst_module_only) {
/* RST: no-crash guard. */
TEST(probe_rst_no_crash) {
GpgMetrics m = gpg_metrics("api.rst",
".. automodule:: mypackage.api\n"
" :members:\n");
GpgMetrics m = gpg_metrics("api.rst", ".. automodule:: mypackage.api\n"
" :members:\n");
ASSERT_TRUE(m.ok);
ASSERT_TRUE(m.total_nodes >= 1);
PASS();
@@ -450,11 +437,10 @@ TEST(probe_rst_no_crash) {
/* JSON: object with two top-level keys → 2 Variable nodes. */
TEST(probe_json_variable_nodes) {
GpgMetrics m = gpg_metrics("config.json",
"{\n"
" \"name\": \"myproject\",\n"
" \"version\": \"1.0.0\"\n"
"}\n");
GpgMetrics m = gpg_metrics("config.json", "{\n"
" \"name\": \"myproject\",\n"
" \"version\": \"1.0.0\"\n"
"}\n");
ASSERT_TRUE(m.ok);
/* GREEN: JSON top-level keys become Variable nodes. */
ASSERT_TRUE(m.variables >= 2);
@@ -468,12 +454,11 @@ TEST(probe_json_more_variables) {
* detect_file_language() returns CBM_LANG_COUNT and the file is never
* indexed (0 nodes). Use a generic .json filename so the JSON grammar
* actually runs (grammar_labels json=Variable:2). */
GpgMetrics m = gpg_metrics("data.json",
"{\n"
" \"name\": \"demo\",\n"
" \"version\": \"0.1.0\",\n"
" \"description\": \"A demo project\"\n"
"}\n");
GpgMetrics m = gpg_metrics("data.json", "{\n"
" \"name\": \"demo\",\n"
" \"version\": \"0.1.0\",\n"
" \"description\": \"A demo project\"\n"
"}\n");
ASSERT_TRUE(m.ok);
/* GREEN: at least 2 Variable nodes (histogram baseline). */
ASSERT_TRUE(m.variables >= 2);
@@ -489,12 +474,11 @@ TEST(probe_json_more_variables) {
/* JSON5: file with keys → only Module node. */
TEST(probe_json5_module_only) {
GpgMetrics m = gpg_metrics("config.json5",
"{\n"
" // comments allowed\n"
" name: 'myproject',\n"
" version: '1.0',\n"
"}\n");
GpgMetrics m = gpg_metrics("config.json5", "{\n"
" // comments allowed\n"
" name: 'myproject',\n"
" version: '1.0',\n"
"}\n");
ASSERT_TRUE(m.ok);
/* GREEN: JSON5 produces only a Module node (no Variable nodes unlike JSON). */
ASSERT_TRUE(m.modules == 1);
@@ -511,9 +495,8 @@ TEST(probe_json5_module_only) {
/* YAML: document with two top-level keys → 2 Variable nodes. */
TEST(probe_yaml_variable_nodes) {
GpgMetrics m = gpg_metrics("config.yaml",
"name: myproject\n"
"version: 1.0.0\n");
GpgMetrics m = gpg_metrics("config.yaml", "name: myproject\n"
"version: 1.0.0\n");
ASSERT_TRUE(m.ok);
/* GREEN: YAML top-level keys become Variable nodes. */
ASSERT_TRUE(m.variables >= 2);
@@ -523,10 +506,9 @@ TEST(probe_yaml_variable_nodes) {
/* YAML: three top-level keys → still ≥ 2 Variable nodes (baseline). */
TEST(probe_yaml_three_keys) {
GpgMetrics m = gpg_metrics("app.yaml",
"host: localhost\n"
"port: 8080\n"
"debug: true\n");
GpgMetrics m = gpg_metrics("app.yaml", "host: localhost\n"
"port: 8080\n"
"debug: true\n");
ASSERT_TRUE(m.ok);
/* GREEN: at least 2 Variable nodes. */
ASSERT_TRUE(m.variables >= 2);
@@ -542,10 +524,9 @@ TEST(probe_yaml_three_keys) {
/* TOML: file with table + key → Class:1 + Variable:1. */
TEST(probe_toml_table_and_key) {
GpgMetrics m = gpg_metrics("Cargo.toml",
"[package]\n"
"name = \"mylib\"\n"
"version = \"0.1.0\"\n");
GpgMetrics m = gpg_metrics("Cargo.toml", "[package]\n"
"name = \"mylib\"\n"
"version = \"0.1.0\"\n");
ASSERT_TRUE(m.ok);
/* GREEN: TOML table [package] becomes a Class node. */
ASSERT_TRUE(m.classes >= 1);
@@ -557,12 +538,11 @@ TEST(probe_toml_table_and_key) {
/* TOML: multiple tables → ≥ 1 Class node (may be exactly 1 per fixture). */
TEST(probe_toml_multiple_tables) {
GpgMetrics m = gpg_metrics("pyproject.toml",
"[tool.poetry]\n"
"name = \"demo\"\n"
"\n"
"[tool.poetry.dependencies]\n"
"python = \"^3.9\"\n");
GpgMetrics m = gpg_metrics("pyproject.toml", "[tool.poetry]\n"
"name = \"demo\"\n"
"\n"
"[tool.poetry.dependencies]\n"
"python = \"^3.9\"\n");
ASSERT_TRUE(m.ok);
/* GREEN: at least one Class node for tables. */
ASSERT_TRUE(m.classes >= 1);
@@ -578,10 +558,9 @@ TEST(probe_toml_multiple_tables) {
/* INI: file with a section and a key → Class + Variable. */
TEST(probe_ini_section_and_key) {
GpgMetrics m = gpg_metrics("app.ini",
"[database]\n"
"host = localhost\n"
"port = 5432\n");
GpgMetrics m = gpg_metrics("app.ini", "[database]\n"
"host = localhost\n"
"port = 5432\n");
ASSERT_TRUE(m.ok);
/* GREEN: [database] section becomes a Class node. */
ASSERT_TRUE(m.classes >= 1);
@@ -593,9 +572,8 @@ TEST(probe_ini_section_and_key) {
/* INI: .conf extension also maps to CBM_LANG_INI; must produce same structure. */
TEST(probe_ini_conf_extension) {
GpgMetrics m = gpg_metrics("server.conf",
"[server]\n"
"listen = 0.0.0.0\n");
GpgMetrics m = gpg_metrics("server.conf", "[server]\n"
"listen = 0.0.0.0\n");
ASSERT_TRUE(m.ok);
/* GREEN: .conf files index as INI and produce Class + Variable nodes. */
ASSERT_TRUE(m.classes >= 1);
@@ -612,11 +590,10 @@ TEST(probe_ini_conf_extension) {
/* CSV: file with header + rows → only Module node. */
TEST(probe_csv_module_only) {
GpgMetrics m = gpg_metrics("data.csv",
"id,name,score\n"
"1,Alice,95\n"
"2,Bob,87\n"
"3,Carol,92\n");
GpgMetrics m = gpg_metrics("data.csv", "id,name,score\n"
"1,Alice,95\n"
"2,Bob,87\n"
"3,Carol,92\n");
ASSERT_TRUE(m.ok);
/* GREEN: CSV produces only a Module node. */
ASSERT_TRUE(m.modules == 1);
@@ -634,13 +611,12 @@ TEST(probe_csv_module_only) {
/* SQL: CREATE TABLE + SELECT → at least 1 Variable node. */
TEST(probe_sql_variable_node) {
GpgMetrics m = gpg_metrics("schema.sql",
"CREATE TABLE users (\n"
" id INTEGER PRIMARY KEY,\n"
" name TEXT NOT NULL\n"
");\n"
"\n"
"SELECT id, name FROM users WHERE id = 1;\n");
GpgMetrics m = gpg_metrics("schema.sql", "CREATE TABLE users (\n"
" id INTEGER PRIMARY KEY,\n"
" name TEXT NOT NULL\n"
");\n"
"\n"
"SELECT id, name FROM users WHERE id = 1;\n");
ASSERT_TRUE(m.ok);
/* GREEN: SQL table reference produces at least 1 Variable node. */
ASSERT_TRUE(m.variables >= 1);
@@ -650,9 +626,9 @@ TEST(probe_sql_variable_node) {
/* SQL: multiple statements → still at least 1 Variable. */
TEST(probe_sql_insert_select) {
GpgMetrics m = gpg_metrics("queries.sql",
"INSERT INTO orders (user_id, total) VALUES (1, 99.99);\n"
"SELECT o.id, u.name FROM orders o JOIN users u ON o.user_id = u.id;\n");
GpgMetrics m = gpg_metrics(
"queries.sql", "INSERT INTO orders (user_id, total) VALUES (1, 99.99);\n"
"SELECT o.id, u.name FROM orders o JOIN users u ON o.user_id = u.id;\n");
ASSERT_TRUE(m.ok);
/* GREEN (fixture fix): SQL Variable nodes come ONLY from DDL
* create_table/create_view (lang_specs.c sql_var_types), NOT from
@@ -672,11 +648,10 @@ TEST(probe_sql_insert_select) {
/* SOQL: SELECT query → only Module node. */
TEST(probe_soql_module_only) {
GpgMetrics m = gpg_metrics("query.soql",
"SELECT Id, Name, Email\n"
"FROM Contact\n"
"WHERE AccountId = :accountId\n"
"ORDER BY Name\n");
GpgMetrics m = gpg_metrics("query.soql", "SELECT Id, Name, Email\n"
"FROM Contact\n"
"WHERE AccountId = :accountId\n"
"ORDER BY Name\n");
ASSERT_TRUE(m.ok);
/* GREEN: SOQL produces only a Module node. */
ASSERT_TRUE(m.modules == 1);
@@ -693,9 +668,8 @@ TEST(probe_soql_module_only) {
/* SOSL: FIND query → only Module node. */
TEST(probe_sosl_module_only) {
GpgMetrics m = gpg_metrics("search.sosl",
"FIND {Acme} IN ALL FIELDS\n"
"RETURNING Account(Id, Name), Contact(Id, Name)\n");
GpgMetrics m = gpg_metrics("search.sosl", "FIND {Acme} IN ALL FIELDS\n"
"RETURNING Account(Id, Name), Contact(Id, Name)\n");
ASSERT_TRUE(m.ok);
/* GREEN: SOSL produces only a Module node. */
ASSERT_TRUE(m.modules == 1);
@@ -713,10 +687,9 @@ TEST(probe_sosl_module_only) {
/* DotEnv: .env file → only Module node. */
TEST(probe_dotenv_module_only) {
GpgMetrics m = gpg_metrics(".env",
"DATABASE_URL=postgres://localhost/mydb\n"
"SECRET_KEY=supersecret\n"
"DEBUG=true\n");
GpgMetrics m = gpg_metrics(".env", "DATABASE_URL=postgres://localhost/mydb\n"
"SECRET_KEY=supersecret\n"
"DEBUG=true\n");
ASSERT_TRUE(m.ok);
/* REAL BUG (NEW class — file-index routing gap): the DotEnv grammar +
* histogram (grammar_labels dotenv=Module:1) exist and work via DIRECT
@@ -731,9 +704,8 @@ TEST(probe_dotenv_module_only) {
/* DotEnv: .env.local suffix variant → indexed as DotEnv. */
TEST(probe_dotenv_local_suffix) {
GpgMetrics m = gpg_metrics(".env.local",
"API_KEY=local-test-key\n"
"PORT=3001\n");
GpgMetrics m = gpg_metrics(".env.local", "API_KEY=local-test-key\n"
"PORT=3001\n");
ASSERT_TRUE(m.ok);
/* REAL BUG (NEW class — file-index routing gap): ".env.local" has no
* FILENAME_TABLE/EXT_TABLE entry, so file-based index_repository never
@@ -753,12 +725,11 @@ TEST(probe_dotenv_local_suffix) {
/* gitignore: pattern file → only Module node. */
TEST(probe_gitignore_module_only) {
GpgMetrics m = gpg_metrics(".gitignore",
"*.o\n"
"*.a\n"
"build/\n"
"dist/\n"
".DS_Store\n");
GpgMetrics m = gpg_metrics(".gitignore", "*.o\n"
"*.a\n"
"build/\n"
"dist/\n"
".DS_Store\n");
ASSERT_TRUE(m.ok);
/* CONTRACT (corrected): unlike .gitattributes, ".gitignore" is intentionally
* NOT indexed as a source file — the cbm_discover walker consumes it as
@@ -781,11 +752,10 @@ TEST(probe_gitignore_module_only) {
/* gitattributes: attribute file → only Module node. */
TEST(probe_gitattributes_module_only) {
GpgMetrics m = gpg_metrics(".gitattributes",
"*.c text eol=lf\n"
"*.h text eol=lf\n"
"*.png binary\n"
"*.jpg binary\n");
GpgMetrics m = gpg_metrics(".gitattributes", "*.c text eol=lf\n"
"*.h text eol=lf\n"
"*.png binary\n"
"*.jpg binary\n");
ASSERT_TRUE(m.ok);
/* REAL BUG (NEW class — file-index routing gap): gitattributes grammar +
* histogram (Module:1) work via DIRECT extraction, but ".gitattributes"
@@ -806,10 +776,9 @@ TEST(probe_gitattributes_module_only) {
/* Properties: key=value file → only Module node. */
TEST(probe_properties_module_only) {
GpgMetrics m = gpg_metrics("app.properties",
"server.host=localhost\n"
"server.port=8080\n"
"log.level=INFO\n");
GpgMetrics m = gpg_metrics("app.properties", "server.host=localhost\n"
"server.port=8080\n"
"log.level=INFO\n");
ASSERT_TRUE(m.ok);
/* GREEN: .properties produces only a Module node. */
ASSERT_TRUE(m.modules == 1);
@@ -829,10 +798,9 @@ TEST(probe_properties_module_only) {
/* Requirements: dep file → only Module node. */
TEST(probe_requirements_module_only) {
GpgMetrics m = gpg_metrics("requirements.txt",
"flask==2.3.0\n"
"requests>=2.28.0\n"
"sqlalchemy~=2.0\n");
GpgMetrics m = gpg_metrics("requirements.txt", "flask==2.3.0\n"
"requests>=2.28.0\n"
"sqlalchemy~=2.0\n");
ASSERT_TRUE(m.ok);
/* GREEN: requirements.txt produces only a Module node. */
ASSERT_TRUE(m.modules == 1);
@@ -842,10 +810,9 @@ TEST(probe_requirements_module_only) {
/* Requirements: DEPENDS_ON edges for package entries.
* RED: grammar-only requirements has no dependency-edge resolver. */
TEST(probe_requirements_depends_on_edge) {
GpgMetrics m = gpg_metrics("requirements.txt",
"numpy==1.24.0\n"
"pandas>=1.5.0\n"
"scipy>=1.10.0\n");
GpgMetrics m = gpg_metrics("requirements.txt", "numpy==1.24.0\n"
"pandas>=1.5.0\n"
"scipy>=1.10.0\n");
ASSERT_TRUE(m.ok);
/* RED: requirements deps not resolved into DEPENDS_ON edges. */
ASSERT_TRUE(m.depends >= 1); /* expected RED — no requirements edge resolver */
@@ -861,18 +828,17 @@ TEST(probe_requirements_depends_on_edge) {
/* Diff: unified diff → only Module node. */
TEST(probe_diff_module_only) {
GpgMetrics m = gpg_metrics("changes.diff",
"--- a/main.c\n"
"+++ b/main.c\n"
"@@ -1,5 +1,6 @@\n"
" #include <stdio.h>\n"
"+#include <stdlib.h>\n"
" \n"
" int main(void) {\n"
"- printf(\"hello\\n\");\n"
"+ printf(\"hello world\\n\");\n"
" return 0;\n"
" }\n");
GpgMetrics m = gpg_metrics("changes.diff", "--- a/main.c\n"
"+++ b/main.c\n"
"@@ -1,5 +1,6 @@\n"
" #include <stdio.h>\n"
"+#include <stdlib.h>\n"
" \n"
" int main(void) {\n"
"- printf(\"hello\\n\");\n"
"+ printf(\"hello world\\n\");\n"
" return 0;\n"
" }\n");
ASSERT_TRUE(m.ok);
/* GREEN: diff produces only a Module node. */
ASSERT_TRUE(m.modules == 1);
@@ -889,17 +855,16 @@ TEST(probe_diff_module_only) {
/* PO: translation file → only Module node. */
TEST(probe_po_module_only) {
GpgMetrics m = gpg_metrics("messages.po",
"# Translation file\n"
"msgid \"\"\n"
"msgstr \"\"\n"
"\"Content-Type: text/plain; charset=UTF-8\\n\"\n"
"\n"
"msgid \"Hello\"\n"
"msgstr \"Bonjour\"\n"
"\n"
"msgid \"Goodbye\"\n"
"msgstr \"Au revoir\"\n");
GpgMetrics m = gpg_metrics("messages.po", "# Translation file\n"
"msgid \"\"\n"
"msgstr \"\"\n"
"\"Content-Type: text/plain; charset=UTF-8\\n\"\n"
"\n"
"msgid \"Hello\"\n"
"msgstr \"Bonjour\"\n"
"\n"
"msgid \"Goodbye\"\n"
"msgstr \"Au revoir\"\n");
ASSERT_TRUE(m.ok);
/* GREEN: PO produces only a Module node. */
ASSERT_TRUE(m.modules == 1);
@@ -916,10 +881,9 @@ TEST(probe_po_module_only) {
/* Regex: pattern file → only Module node. */
TEST(probe_regex_module_only) {
GpgMetrics m = gpg_metrics("patterns.re",
"(foo|bar)+\n"
"^[a-zA-Z_][a-zA-Z0-9_]*$\n"
"\\d{4}-\\d{2}-\\d{2}\n");
GpgMetrics m = gpg_metrics("patterns.re", "(foo|bar)+\n"
"^[a-zA-Z_][a-zA-Z0-9_]*$\n"
"\\d{4}-\\d{2}-\\d{2}\n");
ASSERT_TRUE(m.ok);
/* REAL BUG (NEW class — file-index routing gap): the Regex grammar +
* histogram (grammar_labels regex=Module:1) work via DIRECT extraction
@@ -941,15 +905,14 @@ TEST(probe_regex_module_only) {
/* SSH Config: host file via ssh_config filename → only Module node. */
TEST(probe_sshconfig_module_only) {
GpgMetrics m = gpg_metrics("ssh_config",
"Host bastion\n"
" HostName 10.0.0.1\n"
" User ubuntu\n"
" IdentityFile ~/.ssh/id_rsa\n"
"\n"
"Host dev\n"
" HostName dev.example.com\n"
" User deploy\n");
GpgMetrics m = gpg_metrics("ssh_config", "Host bastion\n"
" HostName 10.0.0.1\n"
" User ubuntu\n"
" IdentityFile ~/.ssh/id_rsa\n"
"\n"
"Host dev\n"
" HostName dev.example.com\n"
" User deploy\n");
ASSERT_TRUE(m.ok);
/* GREEN: SSH config produces only a Module node. */
ASSERT_TRUE(m.modules == 1);
@@ -966,19 +929,18 @@ TEST(probe_sshconfig_module_only) {
/* BibTeX: bibliography file → only Module node. */
TEST(probe_bibtex_module_only) {
GpgMetrics m = gpg_metrics("refs.bib",
"@article{smith2023,\n"
" author = {Smith, John},\n"
" title = {A Study of Something},\n"
" year = {2023},\n"
" journal = {J. Examples}\n"
"}\n"
"\n"
"@book{jones2020,\n"
" author = {Jones, Alice},\n"
" title = {Reference Book},\n"
" year = {2020}\n"
"}\n");
GpgMetrics m = gpg_metrics("refs.bib", "@article{smith2023,\n"
" author = {Smith, John},\n"
" title = {A Study of Something},\n"
" year = {2023},\n"
" journal = {J. Examples}\n"
"}\n"
"\n"
"@book{jones2020,\n"
" author = {Jones, Alice},\n"
" title = {Reference Book},\n"
" year = {2020}\n"
"}\n");
ASSERT_TRUE(m.ok);
/* GREEN: BibTeX produces only a Module node. */
ASSERT_TRUE(m.modules == 1);
@@ -996,11 +958,10 @@ TEST(probe_bibtex_module_only) {
/* FORM: file with a variable declaration → at least 1 Variable node. */
TEST(probe_form_variable_node) {
GpgMetrics m = gpg_metrics("calc.frm",
"Symbol x, y, z;\n"
"Local F = x^2 + y^2 + z^2;\n"
"Print;\n"
".end\n");
GpgMetrics m = gpg_metrics("calc.frm", "Symbol x, y, z;\n"
"Local F = x^2 + y^2 + z^2;\n"
"Print;\n"
".end\n");
ASSERT_TRUE(m.ok);
/* GREEN: FORM produces at least 1 Variable node (histogram: Variable:1). */
ASSERT_TRUE(m.variables >= 1);
@@ -1010,14 +971,13 @@ TEST(probe_form_variable_node) {
/* FORM: no-crash guard with procedure definition. */
TEST(probe_form_procedure) {
GpgMetrics m = gpg_metrics("proc.frm",
"#procedure Square(x)\n"
" id `x'^2 = `x'^2;\n"
"#endprocedure\n"
"\n"
"Symbol a;\n"
"Local expr = a^2;\n"
".end\n");
GpgMetrics m = gpg_metrics("proc.frm", "#procedure Square(x)\n"
" id `x'^2 = `x'^2;\n"
"#endprocedure\n"
"\n"
"Symbol a;\n"
"Local expr = a^2;\n"
".end\n");
ASSERT_TRUE(m.ok);
/* GREEN: at least 1 node (Module). */
ASSERT_TRUE(m.total_nodes >= 1);
@@ -1033,14 +993,13 @@ TEST(probe_form_procedure) {
/* Linker script: SECTIONS directive → only Module node. */
TEST(probe_linkerscript_module_only) {
GpgMetrics m = gpg_metrics("link.ld",
"SECTIONS\n"
"{\n"
" . = 0x10000;\n"
" .text : { *(.text) }\n"
" .data : { *(.data) }\n"
" .bss : { *(.bss) }\n"
"}\n");
GpgMetrics m = gpg_metrics("link.ld", "SECTIONS\n"
"{\n"
" . = 0x10000;\n"
" .text : { *(.text) }\n"
" .data : { *(.data) }\n"
" .bss : { *(.bss) }\n"
"}\n");
ASSERT_TRUE(m.ok);
/* GREEN: linker script produces only a Module node. */
ASSERT_TRUE(m.modules == 1);
@@ -1050,18 +1009,17 @@ TEST(probe_linkerscript_module_only) {
/* Linker script: MEMORY + SECTIONS → still only Module node. */
TEST(probe_linkerscript_memory_sections) {
GpgMetrics m = gpg_metrics("stm32.ld",
"MEMORY\n"
"{\n"
" FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K\n"
" RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K\n"
"}\n"
"SECTIONS\n"
"{\n"
" .isr_vector : { *(.isr_vector) } > FLASH\n"
" .text : { *(.text*) } > FLASH\n"
" .bss : { *(.bss*) } > RAM\n"
"}\n");
GpgMetrics m = gpg_metrics("stm32.ld", "MEMORY\n"
"{\n"
" FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K\n"
" RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K\n"
"}\n"
"SECTIONS\n"
"{\n"
" .isr_vector : { *(.isr_vector) } > FLASH\n"
" .text : { *(.text*) } > FLASH\n"
" .bss : { *(.bss*) } > RAM\n"
"}\n");
ASSERT_TRUE(m.ok);
/* GREEN: linker script produces only a Module node. */
ASSERT_TRUE(m.modules >= 1);
File diff suppressed because it is too large. Load diff