#include "helpers.h" #include "arena.h" // CBMArena, cbm_arena_alloc/strdup/strndup/sprintf #include "cbm.h" // CBMExtractCtx, CBMLanguage, CBM_LANG_*, EFCEntry, EFC_SIZE #include "lang_specs.h" #include "tree_sitter/api.h" // TSNode, ts_node_* #include "foundation/constants.h" #include "foundation/compat.h" // CBM_TLS #include // calloc/free for the symbol-set cache enum { MIN_ROUTE_LEN = 3, MIN_SYS_PATH_LEN = 4, MAX_ROUTE_SCAN = 20, NOEXT_BUF = 256, MIN_HEX_LEN = 3, MAX_HEX_NAME_LEN = 64, INIT_FILE_LEN = 8, /* strlen("__init__") */ INDEX_FILE_LEN = 5, /* strlen("index") */ NOT_FOUND = -1, }; /* Prefix length helper for strncmp with string literals. */ #define SLEN(s) (sizeof(s) - SKIP_ONE) #include // uint32_t #include #include #include // --- Portable substring search --- // Hand-rolled memmem: does not rely on the system memmem (GNU/BSD-only; // msys2-clang on Windows lacks it), so it compiles identically everywhere. void *cbm_memmem(const void *haystack, size_t haystack_len, const void *needle, size_t needle_len) { if (needle_len == 0) { return (void *)haystack; } if (needle_len > haystack_len) { return NULL; } const char *h = (const char *)haystack; size_t last = haystack_len - needle_len; for (size_t i = 0; i <= last; i++) { if (memcmp(h + i, needle, needle_len) == 0) { return (void *)(h + i); } } return NULL; } // --- Node text extraction --- char *cbm_node_text(CBMArena *a, TSNode node, const char *source) { uint32_t start = ts_node_start_byte(node); uint32_t end = ts_node_end_byte(node); if (end <= start) { return cbm_arena_strdup(a, ""); } return cbm_arena_strndup(a, source + start, end - start); } // --- Keyword sets per language --- static const char *go_keywords[] = { "break", "case", "chan", "const", "continue", "default", "defer", "else", "fallthrough", "for", "func", "go", "goto", "if", "import", "interface", "map", "package", "range", "return", "select", "struct", "switch", "type", "var", "true", "false", "nil", "iota", "append", "cap", "close", "complex", "copy", "delete", "imag", "len", "make", "new", "panic", "print", "println", "real", "recover", NULL}; static const char *python_keywords[] = { "False", "None", "True", "and", "as", "assert", "async", "await", "break", "class", "continue", "def", "del", "elif", "else", "except", "finally", "for", "from", "global", "if", "import", "in", "is", "lambda", "nonlocal", "not", "or", "pass", "raise", "return", "try", "while", "with", "yield", "self", "cls", "__init__", "__name__", "__main__", "super", "print", "len", "range", "enumerate", "zip", "map", "filter", "type", "int", "str", "float", "bool", "list", "dict", "set", "tuple", "bytes", NULL}; static const char *js_keywords[] = { "break", "case", "catch", "class", "const", "continue", "debugger", "default", "delete", "do", "else", "export", "extends", "false", "finally", "for", "function", "if", "import", "in", "instanceof", "let", "new", "null", "return", "super", "switch", "this", "throw", "true", "try", "typeof", "undefined", "var", "void", "while", "with", "yield", "async", "await", "of", "static", "get", "set", "from", "as", "constructor", "prototype", "console", "window", "document", "process", "module", "exports", "require", "Array", "Object", "String", "Number", "Boolean", "Symbol", "Map", "Set", "Promise", "Error", "RegExp", "Date", "Math", "JSON", "parseInt", "parseFloat", "setTimeout", "setInterval", "clearTimeout", "clearInterval", NULL}; static const char *rust_keywords[] = { "as", "async", "await", "break", "const", "continue", "crate", "dyn", "else", "enum", "extern", "false", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref", "return", "self", "Self", "static", "struct", "super", "trait", "true", "type", "unsafe", "use", "where", "while", "abstract", "become", "box", "do", "final", "macro", "override", "priv", "try", "typeof", "unsized", "virtual", "yield", "Some", "None", "Ok", "Err", "Vec", "String", "Box", "Rc", "Arc", "Option", "Result", "println", "eprintln", "format", "write", "writeln", "print", "eprint", "panic", "assert", "assert_eq", "assert_ne", "debug_assert", "todo", "unimplemented", "cfg", "derive", "test", "allow", "deny", "warn", "forbid", "deprecated", NULL}; static const char *java_keywords[] = { "abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", "class", "const", "continue", "default", "do", "double", "else", "enum", "extends", "false", "final", "finally", "float", "for", "goto", "if", "implements", "import", "instanceof", "int", "interface", "long", "native", "new", "null", "package", "private", "protected", "public", "return", "short", "static", "strictfp", "super", "switch", "synchronized", "this", "throw", "throws", "transient", "true", "try", "void", "volatile", "while", "var", "record", "sealed", "permits", "yield", "System", "String", "Integer", "Long", "Double", "Float", "Boolean", "Object", "List", "Map", "Set", "Optional", "Stream", "Arrays", "Collections", NULL}; /* Kotlin hard keywords (those reserved everywhere). Kotlin does NOT reserve * primitive type names — `double`, `int`, `float`, `boolean` are ordinary * identifiers (the types are `Double`, `Int`, …), so a function named * `fun double()` is legal and must NOT be filtered as a keyword the way the * Java list (which lists Java primitives) would. Soft/modifier 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}; static const char *generic_keywords[] = { "true", "false", "null", "nil", "None", "undefined", "void", "if", "else", "for", "while", "do", "switch", "case", "default", "break", "continue", "return", "throw", "try", "catch", "finally", "class", "struct", "enum", "interface", "trait", "impl", "import", "export", "package", "module", "use", "require", "include", "new", "delete", "this", "self", "super", "public", "private", "protected", "static", "const", "var", "let", "function", "def", "fn", "func", "fun", "proc", "sub", "method", "async", "await", "yield", NULL}; /* Puppet reserves control-flow words but NOT `include`/`require`/`contain`, * which are ordinary built-in functions invoked as calls. Using the generic * list would wrongly drop `include`/`require` call edges, so Puppet gets its * own reserved-word set that omits them. */ static const char *puppet_keywords[] = {"true", "false", "undef", "if", "elsif", "else", "unless", "case", "and", "or", "in", "node", "class", "define", "inherits", "default", "return", NULL}; // True when `label` names a type-like container definition (see cbm.h). Single // source of truth for the type-resolution / registry / IMPLEMENTS / LSP-type // consumers — adding a label here updates them all. bool cbm_label_is_type_like(const char *label) { if (!label) { return false; } return strcmp(label, "Class") == 0 || strcmp(label, "Struct") == 0 || strcmp(label, "Interface") == 0 || strcmp(label, "Enum") == 0 || strcmp(label, "Type") == 0 || strcmp(label, "Trait") == 0; } bool cbm_is_keyword(const char *name, CBMLanguage lang) { if (!name || !name[0]) { return true; } const char **keywords; switch (lang) { case CBM_LANG_GO: keywords = go_keywords; break; case CBM_LANG_PYTHON: keywords = python_keywords; break; case CBM_LANG_JAVASCRIPT: case CBM_LANG_TYPESCRIPT: case CBM_LANG_TSX: keywords = js_keywords; break; case CBM_LANG_RUST: keywords = rust_keywords; break; case CBM_LANG_JAVA: case CBM_LANG_SCALA: keywords = java_keywords; break; case CBM_LANG_KOTLIN: keywords = kotlin_keywords; break; case CBM_LANG_PUPPET: keywords = puppet_keywords; break; default: keywords = generic_keywords; break; } for (const char **kw = keywords; *kw; kw++) { if (strcmp(name, *kw) == 0) { return true; } } return false; } // Builtins that appear in the keyword set above (so they are suppressed as bare // usages) but for which we mint a real graph node and an LSP resolution, so a // CALL to them must still be extracted. MUST stay in sync with kPyBuiltinNodes // in internal/cbm/lsp/py_builtins.c — every entry here has a "builtins." // node, so the resulting CALLS edge always has a target (never Module-sourced). static const char *python_resolvable_builtins[] = {"len", "print", "str", "int", "list", "dict", "range", NULL}; bool cbm_is_resolvable_builtin(const char *name, CBMLanguage lang) { if (!name || !name[0]) { return false; } if (lang != CBM_LANG_PYTHON) { return false; } for (const char **b = python_resolvable_builtins; *b; b++) { if (strcmp(name, *b) == 0) { return true; } } return false; } // --- Export detection --- bool cbm_is_exported(const char *name, CBMLanguage lang) { if (!name || !name[0]) { return false; } switch (lang) { case CBM_LANG_GO: return (name[0] >= 'A' && name[0] <= 'Z'); case CBM_LANG_PYTHON: return (name[0] != '_'); case CBM_LANG_JAVA: case CBM_LANG_CSHARP: case CBM_LANG_KOTLIN: return (name[0] >= 'A' && name[0] <= 'Z'); default: return true; } } // --- Test file detection --- static bool has_suffix(const char *str, const char *suffix) { size_t slen = strlen(str); size_t xlen = strlen(suffix); if (xlen > slen) { return false; } return strcmp(str + slen - xlen, suffix) == 0; } static bool has_prefix(const char *str, const char *prefix) { return strncmp(str, prefix, strlen(prefix)) == 0; } // Extract basename from path static const char *path_basename(const char *path) { const char *last = strrchr(path, '/'); return last ? last + SKIP_ONE : path; } // Strip extension from basename static void strip_ext(const char *base, char *buf, size_t buflen) { const char *dot = strrchr(base, '.'); if (dot && dot != base) { size_t len = (size_t)(dot - base); if (len >= buflen) { len = buflen - SKIP_ONE; } memcpy(buf, base, len); buf[len] = '\0'; } else { snprintf(buf, buflen, "%s", base); } } bool cbm_is_test_file(const char *rel_path, CBMLanguage lang) { if (!rel_path) { return false; } const char *base = path_basename(rel_path); switch (lang) { case CBM_LANG_GO: return has_suffix(base, "_test.go"); case CBM_LANG_PYTHON: return has_prefix(base, "test_") || has_suffix(base, "_test.py"); case CBM_LANG_JAVASCRIPT: case CBM_LANG_TYPESCRIPT: case CBM_LANG_TSX: { char noext[NOEXT_BUF]; strip_ext(base, noext, sizeof(noext)); return has_suffix(noext, ".test") || has_suffix(noext, ".spec") || has_suffix(noext, "_test") || has_suffix(noext, "_spec") || has_prefix(base, "test_"); } case CBM_LANG_JAVA: case CBM_LANG_KOTLIN: case CBM_LANG_SCALA: return has_suffix(base, "Test.java") || has_suffix(base, "Tests.java") || has_suffix(base, "Spec.java") || has_suffix(base, "Test.kt") || has_suffix(base, "Spec.kt") || has_suffix(base, "Test.scala") || has_suffix(base, "Spec.scala"); case CBM_LANG_RUST: // Rust tests are typically mod tests inside the file, but test files too return has_suffix(base, "_test.rs") || has_prefix(base, "test_"); case CBM_LANG_RUBY: return has_suffix(base, "_test.rb") || has_suffix(base, "_spec.rb") || has_prefix(base, "test_"); case CBM_LANG_PHP: return has_suffix(base, "Test.php"); case CBM_LANG_CSHARP: return has_suffix(base, "Tests.cs") || has_suffix(base, "Test.cs"); case CBM_LANG_CPP: case CBM_LANG_C: return has_suffix(base, "_test.c") || has_suffix(base, "_test.cc") || has_suffix(base, "_test.cpp") || has_prefix(base, "test_"); case CBM_LANG_MATLAB: return has_prefix(base, "test_") || has_prefix(base, "Test"); default: return false; } } // --- AST traversal helpers --- TSNode cbm_find_child_by_kind(TSNode parent, const char *kind) { uint32_t count = ts_node_child_count(parent); for (uint32_t i = 0; i < count; i++) { TSNode child = ts_node_child(parent, i); if (strcmp(ts_node_type(child), kind) == 0) { return child; } } TSNode null_node = {0}; return null_node; } /* ── Node-type classification: TSSymbol bitset acceleration ─────────────── * cbm_kind_in_set is called for nearly every AST node (function/class/call/ * import/branching sets), so a linear strcmp over the type-name array is a hot * path. tree-sitter already assigns each node type a small integer TSSymbol, so * we precompute — per (language, type-array) — a bitset of the matching symbol * ids and test ts_node_symbol() in O(1) with no string work. * * The cache is THREAD-LOCAL: extraction workers are independent pthreads, so a * per-thread cache needs no locking and is trivially correct. Bitsets are built * once per (lang, array) per thread from static spec arrays (bounded, stable). * Any type name that fails to resolve to a symbol disables the bitset for that * set (exact=false) and we fall back to the exact strcmp behavior — so the * result is always identical to the original, only faster. */ static bool kind_in_set_strcmp(TSNode node, const char *const *types) { const char *kind = ts_node_type(node); for (const char *const *t = types; *t; t++) { if (strcmp(kind, *t) == 0) { return true; } } return false; } typedef struct { const TSLanguage *lang; /* NULL = empty slot */ const char *const *types; /* identity key (static spec array pointer) */ uint64_t *bits; /* symbol bitset; NULL when exact==false */ uint32_t nsyms; /* ts_language_symbol_count(lang) */ bool exact; /* false → every name resolved; use strcmp fallback */ } ks_slot_t; enum { KS_SLOTS = 512, KS_SLOT_MASK = 511, KS_PROBE = 8 }; static CBM_TLS ks_slot_t ks_cache[KS_SLOTS]; static ks_slot_t *ks_build(const TSLanguage *lang, const char *const *types, ks_slot_t *s) { s->lang = lang; s->types = types; s->bits = NULL; s->nsyms = 0; s->exact = false; uint32_t nsyms = ts_language_symbol_count(lang); if (nsyms == 0) { return s; /* fall back to strcmp */ } uint64_t *bits = calloc(((size_t)nsyms + 63) / 64, sizeof(uint64_t)); if (!bits) { return s; } bool all_resolved = true; for (const char *const *t = types; *t; t++) { uint32_t len = (uint32_t)strlen(*t); /* A name may be a named node type or an anonymous token ("for", "&&"): * set whichever symbol(s) exist so ts_node_symbol matches either. */ TSSymbol sn = ts_language_symbol_for_name(lang, *t, len, true); TSSymbol sa = ts_language_symbol_for_name(lang, *t, len, false); bool any = false; if (sn != 0 && sn < nsyms) { bits[sn >> 6] |= (uint64_t)1 << (sn & 63); any = true; } if (sa != 0 && sa < nsyms) { bits[sa >> 6] |= (uint64_t)1 << (sa & 63); any = true; } if (!any) { all_resolved = false; /* unknown name → can't represent exactly */ } } if (!all_resolved) { free(bits); return s; /* exact stays false */ } s->bits = bits; s->nsyms = nsyms; s->exact = true; return s; } /* Find or build the cache slot for (lang, types). Returns NULL only if the * thread-local table is saturated at this hash (extremely rare → strcmp). */ static ks_slot_t *ks_get(const TSLanguage *lang, const char *const *types) { uintptr_t h = ((uintptr_t)types >> 4) ^ ((uintptr_t)lang >> 3) ^ ((uintptr_t)types >> 13); for (int probe = 0; probe < KS_PROBE; probe++) { ks_slot_t *s = &ks_cache[(size_t)(h + (uintptr_t)probe) & KS_SLOT_MASK]; if (s->lang == NULL) { return ks_build(lang, types, s); } if (s->lang == lang && s->types == types) { return s; } } return NULL; } bool cbm_kind_in_set(TSNode node, const char **types) { if (!types || !types[0]) { return false; } const TSLanguage *lang = ts_node_language(node); if (lang) { ks_slot_t *s = ks_get(lang, (const char *const *)types); if (s && s->exact && s->bits) { TSSymbol sym = ts_node_symbol(node); return sym < s->nsyms && (((s->bits[sym >> 6] >> (sym & 63)) & 1U) != 0); } } return kind_in_set_strcmp(node, (const char *const *)types); } /* Free the calling thread's node-type bitset cache (the calloc'd `bits` arrays * that cbm_kind_in_set builds lazily). The cache is thread-local, so each worker * thread and the main thread must call this at teardown (worker exit / process * exit) for LeakSanitizer to report no leak. Safe if no cache was ever built. */ void cbm_kind_in_set_free_cache(void) { for (int i = 0; i < KS_SLOTS; i++) { free(ks_cache[i].bits); ks_cache[i].bits = NULL; ks_cache[i].lang = NULL; ks_cache[i].types = NULL; ks_cache[i].nsyms = 0; ks_cache[i].exact = false; } } bool cbm_has_ancestor_kind(TSNode node, const char *kind, int max_depth) { TSNode cur = node; for (int i = 0; i < max_depth; i++) { TSNode parent = ts_node_parent(cur); if (ts_node_is_null(parent)) { return false; } if (strcmp(ts_node_type(parent), kind) == 0) { return true; } cur = parent; } return false; } // Recursive branching count #define BRANCHING_STACK_CAP 4096 static int count_branching_iter(TSNode root, const char **types) { TSNode stack[BRANCHING_STACK_CAP]; int top = 0; int count = 0; stack[top++] = root; while (top > 0) { TSNode node = stack[--top]; const char *kind = ts_node_type(node); for (const char **t = types; *t; t++) { if (strcmp(kind, *t) == 0) { count++; break; } } uint32_t n = ts_node_child_count(node); for (int i = (int)n - SKIP_ONE; i >= 0 && top < BRANCHING_STACK_CAP; i--) { stack[top++] = ts_node_child(node, (uint32_t)i); } } return count; } int cbm_count_branching(TSNode node, const char **branching_types) { if (!branching_types) { return 0; } return count_branching_iter(node, branching_types); } // Loop node-type names across tree-sitter grammars, for loop-nesting depth. bool cbm_is_loop_node_type(const char *kind) { static const char *const loops[] = {"for_statement", "while_statement", "do_statement", "do_while_statement", "for_in_statement", "for_of_statement", "for_each_statement", "foreach_statement", "enhanced_for_statement", "for_range_loop", "c_style_for_statement", "for_expression", "while_expression", "loop_expression", "while_let_expression", "repeat_statement", "repeat_while_statement", "until", "while_modifier", "until_modifier", "for", "while", NULL}; for (const char *const *l = loops; *l; l++) { if (strcmp(kind, *l) == 0) { return true; } } return false; } // Is `kind` a chained member/subscript access node? Language-agnostic generic // set covering the common grammars; used only for the structural "access depth" // smell, so unmatched grammars simply report 0 (never wrong, just silent). static bool is_member_access_node(const char *kind) { static const char *const access[] = {"member_expression", "field_expression", "selector_expression", "field_access", "member_access_expression", "navigation_expression", "attribute", "subscript_expression", "subscript", "index_expression", "element_access_expression", "scoped_identifier", NULL}; for (const char *const *a = access; *a; a++) { if (strcmp(kind, *a) == 0) { return true; } } return false; } // One traversal computing cyclomatic + cognitive + loop-nesting + access-depth // metrics. Each frame carries its branch-, loop- and access-nesting depth so // every metric (cognitive Campbell penalty, loop_depth polynomial-degree proxy, // max chained access depth) is produced in a single walk. void cbm_compute_complexity(TSNode node, const char **branching_types, cbm_complexity_t *out) { out->cyclomatic = 0; out->cognitive = 0; out->loop_count = 0; out->loop_depth = 0; out->max_access_depth = 0; if (!branching_types) { return; } struct cx_frame { TSNode node; int bdepth; int ldepth; int adepth; }; struct cx_frame stack[BRANCHING_STACK_CAP]; int top = 0; stack[top].node = node; stack[top].bdepth = 0; stack[top].ldepth = 0; stack[top].adepth = 0; top++; while (top > 0) { struct cx_frame f = stack[--top]; const char *kind = ts_node_type(f.node); bool is_branch = false; for (const char **t = branching_types; *t; t++) { if (strcmp(kind, *t) == 0) { is_branch = true; break; } } int child_b = f.bdepth; int child_l = f.ldepth; /* Chained member/subscript access: a.b.c.d nests as access(access(access(a))), * so each consecutive access node deepens the chain; non-access nodes reset it. */ int child_a = 0; if (ts_node_is_named(f.node) && is_member_access_node(kind)) { child_a = f.adepth + 1; if (child_a > out->max_access_depth) { out->max_access_depth = child_a; } } if (is_branch) { out->cyclomatic++; out->cognitive += 1 + f.bdepth; /* +1 plus nesting penalty (Campbell) */ child_b = f.bdepth + 1; } /* Only *named* nodes count as loops. In many grammars (Go, C, …) the * loop's `for`/`while` keyword is an anonymous child token whose node * type literally equals "for"/"while"; without this guard each loop is * counted twice and nesting depth is inflated by one. Named loop nodes * (e.g. Ruby's `while`/`until`/`for`) still match correctly. */ if (ts_node_is_named(f.node) && cbm_is_loop_node_type(kind)) { out->loop_count++; int d = f.ldepth + 1; if (d > out->loop_depth) { out->loop_depth = d; } child_l = d; } uint32_t n = ts_node_child_count(f.node); for (int i = (int)n - SKIP_ONE; i >= 0 && top < BRANCHING_STACK_CAP; i--) { stack[top].node = ts_node_child(f.node, (uint32_t)i); stack[top].bdepth = child_b; stack[top].ldepth = child_l; stack[top].adepth = child_a; top++; } } } // --- Enclosing function detection --- // Language-specific function node types for parent-chain walk static const char *func_kinds_go[] = {"function_declaration", "method_declaration", NULL}; static const char *func_kinds_python[] = {"function_definition", NULL}; static const char *func_kinds_js[] = {"function_declaration", "method_definition", "arrow_function", "function_expression", NULL}; static const char *func_kinds_rust[] = {"function_item", NULL}; static const char *func_kinds_java[] = {"method_declaration", "constructor_declaration", NULL}; static const char *func_kinds_cpp[] = {"function_definition", NULL}; static const char *func_kinds_ruby[] = {"method", "singleton_method", NULL}; static const char *func_kinds_php[] = {"function_definition", "method_declaration", NULL}; static const char *func_kinds_lua[] = {"function_declaration", "function_definition", NULL}; static const char *func_kinds_scala[] = {"function_definition", NULL}; static const char *func_kinds_kotlin[] = {"function_declaration", NULL}; static const char *func_kinds_elixir[] = {"call", NULL}; // def/defp are call nodes static const char *func_kinds_haskell[] = {"function", "value_definition", NULL}; static const char *func_kinds_ocaml[] = {"value_definition", "let_binding", NULL}; static const char *func_kinds_zig[] = {"function_declaration", "test_declaration", NULL}; static const char *func_kinds_bash[] = {"function_definition", NULL}; static const char *func_kinds_erlang[] = {"function_clause", NULL}; static const char *func_kinds_csharp[] = {"method_declaration", "constructor_declaration", NULL}; static const char *func_kinds_matlab[] = {"function_definition", NULL}; static const char *func_kinds_lean[] = {"def", "theorem", "instance", "abbrev", NULL}; static const char *func_kinds_form[] = {"procedure_definition", NULL}; static const char *func_kinds_magma[] = {"function_definition", "procedure_definition", "intrinsic_definition", NULL}; static const char *func_kinds_wolfram[] = {"set_delayed_top", "set_top", "set_delayed", "set", NULL}; static const char *func_kinds_generic[] = {"function_declaration", "function_definition", "method_declaration", "method_definition", NULL}; static const char **func_kinds_for_lang(CBMLanguage lang) { switch (lang) { case CBM_LANG_GO: return func_kinds_go; case CBM_LANG_PYTHON: return func_kinds_python; case CBM_LANG_JAVASCRIPT: case CBM_LANG_TYPESCRIPT: case CBM_LANG_TSX: return func_kinds_js; case CBM_LANG_RUST: return func_kinds_rust; case CBM_LANG_JAVA: return func_kinds_java; case CBM_LANG_CPP: case CBM_LANG_C: return func_kinds_cpp; case CBM_LANG_RUBY: return func_kinds_ruby; case CBM_LANG_PHP: return func_kinds_php; case CBM_LANG_LUA: return func_kinds_lua; case CBM_LANG_SCALA: return func_kinds_scala; case CBM_LANG_KOTLIN: return func_kinds_kotlin; case CBM_LANG_ELIXIR: return func_kinds_elixir; case CBM_LANG_HASKELL: return func_kinds_haskell; case CBM_LANG_OCAML: return func_kinds_ocaml; case CBM_LANG_ZIG: return func_kinds_zig; case CBM_LANG_BASH: return func_kinds_bash; case CBM_LANG_ERLANG: return func_kinds_erlang; case CBM_LANG_CSHARP: return func_kinds_csharp; case CBM_LANG_MATLAB: return func_kinds_matlab; case CBM_LANG_LEAN: return func_kinds_lean; case CBM_LANG_FORM: return func_kinds_form; case CBM_LANG_MAGMA: return func_kinds_magma; case CBM_LANG_WOLFRAM: return func_kinds_wolfram; default: { /* Enclosing-function drift fix (QUALITY_ANALYSIS gap #3): languages * without a curated func_kinds entry previously fell back to * func_kinds_generic, which misses their real function node types * (e.g. dart function_signature, perl subroutine_declaration_statement, * scss mixin_statement, nix function_expression, fortran subroutine, * cobol program_definition, verilog/vhdl, ...). The enclosing-function * walk then never found the parent function and attributed every * in-body call to the Module node. Use the language spec's * function_node_types (the single source of truth that extraction * already uses) when the curated switch has no entry. Curated languages * above are unchanged. */ const CBMLangSpec *spec = cbm_lang_spec(lang); if (spec && spec->function_node_types && spec->function_node_types[0]) return spec->function_node_types; return func_kinds_generic; } } } TSNode cbm_find_enclosing_func(TSNode node, CBMLanguage lang) { const char **kinds = func_kinds_for_lang(lang); TSNode cur = node; for (;;) { TSNode parent = ts_node_parent(cur); if (ts_node_is_null(parent)) { break; } const char *pk = ts_node_type(parent); for (const char **k = kinds; *k; k++) { if (strcmp(pk, *k) == 0) { return parent; } } cur = parent; } TSNode null_node = {0}; return null_node; } // Check if a node type is a terminal C declarator name. static bool is_c_terminal_name(const char *dk) { return strcmp(dk, "identifier") == 0 || strcmp(dk, "field_identifier") == 0 || strcmp(dk, "operator_name") == 0 || strcmp(dk, "operator_cast") == 0 || strcmp(dk, "destructor_name") == 0; } // Resolve name from a C++ qualified_identifier/scoped_identifier. static TSNode resolve_qualified_name(TSNode decl) { static const char *name_kinds[] = {"operator_name", "operator_cast", "destructor_name", "identifier", "field_identifier", NULL}; for (const char **k = name_kinds; *k; k++) { TSNode found = cbm_find_child_by_kind(decl, *k); if (!ts_node_is_null(found)) { return found; } } TSNode null_node = {0}; return null_node; } // Resolve function name from C/C++/CUDA/GLSL declarator chain. Shared canonical // implementation — see the header for the full rationale (#438). TSNode cbm_resolve_c_declarator_name_node(TSNode func_node) { TSNode decl = ts_node_child_by_field_name(func_node, TS_FIELD("declarator")); for (int depth = 0; depth < CBM_DECLARATOR_DEPTH_LIMIT && !ts_node_is_null(decl); depth++) { const char *dk = ts_node_type(decl); if (is_c_terminal_name(dk)) { return decl; } if (strcmp(dk, "qualified_identifier") == 0 || strcmp(dk, "scoped_identifier") == 0) { return resolve_qualified_name(decl); } TSNode inner = ts_node_child_by_field_name(decl, TS_FIELD("declarator")); if (ts_node_is_null(inner) && ts_node_named_child_count(decl) > 0) { inner = ts_node_named_child(decl, 0); } if (ts_node_is_null(inner)) { break; } decl = inner; } TSNode null_node = {0}; return null_node; } // Convert a resolved function/method name node to its name string. Most nodes // map directly to their text, but a C++ conversion-operator's `operator_cast` // node spans the full "operator bool() const" — this grammar folds the parameter // list and cv-qualifiers into the node. The method's name is only the // "operator " prefix, so truncate at the first '(' and trim trailing // space. Without this the conversion operator is indexed as "operator bool() // const", and a member lookup for "operator bool" (the implicit call in // `if (obj)`) misses. char *cbm_func_name_node_text(CBMArena *a, TSNode name_node, const char *source) { char *text = cbm_node_text(a, name_node, source); if (text && strcmp(ts_node_type(name_node), "operator_cast") == 0) { char *paren = strchr(text, '('); if (paren) { while (paren > text && (paren[-1] == ' ' || paren[-1] == '\t')) { paren--; } *paren = '\0'; } } return text; } static const char *func_node_name(CBMArena *a, TSNode func_node, const char *source, CBMLanguage lang) { // Wolfram: set_delayed_top/set_top/set_delayed/set — LHS is apply(user_symbol("f"), ...) if (lang == CBM_LANG_WOLFRAM) { const char *nk = ts_node_type(func_node); if (strcmp(nk, "set_delayed_top") == 0 || strcmp(nk, "set_top") == 0 || strcmp(nk, "set_delayed") == 0 || strcmp(nk, "set") == 0) { if (ts_node_named_child_count(func_node) > 0) { TSNode lhs = ts_node_named_child(func_node, 0); if (strcmp(ts_node_type(lhs), "apply") == 0 && ts_node_named_child_count(lhs) > 0) { TSNode head = ts_node_named_child(lhs, 0); if (strcmp(ts_node_type(head), "user_symbol") == 0) { return cbm_node_text(a, head, source); } } } return NULL; } } TSNode name_node = ts_node_child_by_field_name(func_node, TS_FIELD("name")); if (!ts_node_is_null(name_node)) { return cbm_node_text(a, name_node, source); } // Arrow functions: check parent variable_declarator if (strcmp(ts_node_type(func_node), "arrow_function") == 0) { TSNode parent = ts_node_parent(func_node); if (!ts_node_is_null(parent) && strcmp(ts_node_type(parent), "variable_declarator") == 0) { TSNode vname = ts_node_child_by_field_name(parent, TS_FIELD("name")); if (!ts_node_is_null(vname)) { return cbm_node_text(a, vname, source); } } } // C/C++/CUDA/GLSL: function_definition carries its name in the declarator chain. if (strcmp(ts_node_type(func_node), "function_definition") == 0) { TSNode dn = cbm_resolve_c_declarator_name_node(func_node); if (!ts_node_is_null(dn)) { return cbm_func_name_node_text(a, dn, source); } } return NULL; } const char *cbm_enclosing_func_qn(CBMArena *a, TSNode node, CBMLanguage lang, const char *source, const char *project, const char *rel_path, const char *module_qn) { TSNode func_node = cbm_find_enclosing_func(node, lang); if (ts_node_is_null(func_node)) { return module_qn; } const char *name = func_node_name(a, func_node, source, lang); if (!name || !name[0]) { return module_qn; } // Check if the function is inside a class — compute classQN.funcName. // For nested classes the class QN must carry the FULL nesting chain // (Outer.Inner, not just Inner) so it matches the class/method node QN the // def walk produces via compute_class_qn (extract_defs.c). Qualifying with // only the innermost class under-qualified the enclosing QN, so a call // inside a nested-class method sourced to the file node instead of its // method node and failed to join the LSP-resolved call by caller QN. const CBMLangSpec *spec = cbm_lang_spec(lang); if (spec && spec->class_node_types) { // Build the dotted class chain from the outermost enclosing class down // to the innermost. Walk parents collecting class names innermost-first, // then prepend each as we ascend so the result reads Outer.Inner. const char *class_chain = NULL; for (TSNode cur = ts_node_parent(func_node); !ts_node_is_null(cur); cur = ts_node_parent(cur)) { if (!cbm_kind_in_set(cur, spec->class_node_types)) { continue; } TSNode class_name = ts_node_child_by_field_name(cur, TS_FIELD("name")); if (ts_node_is_null(class_name)) { continue; } char *cname = cbm_node_text(a, class_name, source); if (!cname || !cname[0]) { continue; } class_chain = class_chain ? cbm_arena_sprintf(a, "%s.%s", cname, class_chain) : cname; } if (class_chain) { const char *class_qn = cbm_fqn_compute(a, project, rel_path, class_chain); return cbm_arena_sprintf(a, "%s.%s", class_qn, name); } } return cbm_fqn_compute(a, project, rel_path, name); } // --- Cached enclosing function QN --- const char *cbm_enclosing_func_qn_cached(CBMExtractCtx *ctx, TSNode node) { uint32_t pos = ts_node_start_byte(node); // Check cache: find a function range that contains this position. // Linear scan is fine for EFC_SIZE=CBM_SZ_64 (all entries fit in ~1 cache line). for (int i = 0; i < ctx->ef_cache.count; i++) { EFCEntry *e = &ctx->ef_cache.entries[i]; if (pos >= e->start_byte && pos < e->end_byte) { return e->qn; } } // Cache miss: compute via parent walk const char *qn = cbm_enclosing_func_qn(ctx->arena, node, ctx->language, ctx->source, ctx->project, ctx->rel_path, ctx->module_qn); // Cache the result: find the enclosing function's byte range TSNode func_node = cbm_find_enclosing_func(node, ctx->language); if (!ts_node_is_null(func_node) && ctx->ef_cache.count < EFC_SIZE) { EFCEntry *e = &ctx->ef_cache.entries[ctx->ef_cache.count++]; e->start_byte = ts_node_start_byte(func_node); e->end_byte = ts_node_end_byte(func_node); e->qn = qn; } return qn; } // --- Module-level detection --- // Module-level parent kind tables static const char *module_parents_go[] = {"source_file", NULL}; static const char *module_parents_rust[] = {"source_file", "mod_item", NULL}; static const char *module_parents_java[] = {"program", "class_body", NULL}; static const char *module_parents_kotlin[] = {"source_file", "class_body", NULL}; static const char *module_parents_scala[] = {"compilation_unit", "template_body", NULL}; static const char *module_parents_csharp[] = {"compilation_unit", "class_declaration", "namespace_declaration", NULL}; static const char *module_parents_php[] = {"program", NULL}; static const char *module_parents_ruby[] = {"program", "class", "module", NULL}; static const char *module_parents_c[] = {"translation_unit", NULL}; static const char *module_parents_zig[] = {"source_file", NULL}; static const char *module_parents_bash[] = {"program", NULL}; static const char *module_parents_erlang[] = {"source", "source_file", NULL}; static const char *module_parents_haskell[] = {"declarations", NULL}; static const char *module_parents_ocaml[] = {"compilation_unit", NULL}; static const char *module_parents_elixir[] = {"source", NULL}; static const char *module_parents_html[] = {"document", NULL}; static const char *module_parents_css[] = {"stylesheet", NULL}; static const char *module_parents_sql[] = {"source_file", "program", "statement", NULL}; static const char *module_parents_toml[] = {"document", "table", "table_array_element", NULL}; static const char *module_parents_config[] = { "document", "table", "table_array_element", "section", "object", "element", "array", NULL}; static const char *module_parents_hcl[] = {"config_file", NULL}; static const char *module_parents_makefile[] = {"makefile", NULL}; static const char *module_parents_commonlisp[] = {"source", NULL}; static const char *module_parents_matlab[] = {"source_file", NULL}; static const char *module_parents_form[] = {"source_file", NULL}; static const char *module_parents_magma[] = {"source_file", NULL}; /* tree-sitter-properties roots at `file`. */ static const char *module_parents_properties[] = {"file", "source_file", NULL}; // Check if parent node kind matches direct-or-grandparent for scripting languages. // Returns true if pk matches root_kind, or pk matches wrapper_kind and grandparent is root_kind. static bool check_script_module_level(TSNode parent, const char *pk, const char *root_kind, const char *wrapper_kind) { if (strcmp(pk, root_kind) == 0) { return true; } if (wrapper_kind && strcmp(pk, wrapper_kind) == 0) { TSNode gp = ts_node_parent(parent); return !ts_node_is_null(gp) && strcmp(ts_node_type(gp), root_kind) == 0; } return false; } // Get the module-level parent type list for table-driven languages. static const char **get_module_parents(CBMLanguage lang) { switch (lang) { case CBM_LANG_GO: return module_parents_go; case CBM_LANG_RUST: return module_parents_rust; case CBM_LANG_JAVA: return module_parents_java; case CBM_LANG_KOTLIN: return module_parents_kotlin; case CBM_LANG_SCALA: return module_parents_scala; case CBM_LANG_CSHARP: return module_parents_csharp; case CBM_LANG_PHP: return module_parents_php; case CBM_LANG_RUBY: return module_parents_ruby; case CBM_LANG_C: case CBM_LANG_CPP: case CBM_LANG_OBJC: return module_parents_c; case CBM_LANG_ZIG: return module_parents_zig; case CBM_LANG_BASH: return module_parents_bash; case CBM_LANG_ERLANG: return module_parents_erlang; case CBM_LANG_HASKELL: return module_parents_haskell; case CBM_LANG_OCAML: return module_parents_ocaml; case CBM_LANG_ELIXIR: return module_parents_elixir; case CBM_LANG_HTML: return module_parents_html; case CBM_LANG_CSS: case CBM_LANG_SCSS: return module_parents_css; case CBM_LANG_SQL: return module_parents_sql; case CBM_LANG_TOML: return module_parents_toml; case CBM_LANG_HCL: return module_parents_hcl; case CBM_LANG_JSON: case CBM_LANG_INI: case CBM_LANG_XML: case CBM_LANG_MARKDOWN: return module_parents_config; case CBM_LANG_SWIFT: return module_parents_zig; case CBM_LANG_DART: return module_parents_php; case CBM_LANG_PERL: case CBM_LANG_GROOVY: case CBM_LANG_DOCKERFILE: // top-level instructions are children of source_file return module_parents_zig; case CBM_LANG_R: return module_parents_php; case CBM_LANG_MAKEFILE: return module_parents_makefile; case CBM_LANG_COMMONLISP: return module_parents_commonlisp; case CBM_LANG_MATLAB: return module_parents_matlab; case CBM_LANG_LEAN: return module_parents_zig; case CBM_LANG_FORM: return module_parents_form; case CBM_LANG_MAGMA: return module_parents_magma; case CBM_LANG_PROPERTIES: return module_parents_properties; case CBM_LANG_GOMOD: // require_directive lives at source_file top level return module_parents_zig; default: return NULL; } } /* Variant that takes the node's parent DIRECTLY. The callers in * extract_defs.c iterate a known parent's children, so they already * have the parent — passing it here avoids ts_node_parent(node), which * is O(n) per call (tree-sitter nodes carry no parent pointer; the * parent is found by rescanning from the root). On a pathologically * large file (e.g. a 583k-line generated/fixture file with tens of * thousands of top-level statements) the old per-child ts_node_parent * made extraction O(n²) and effectively hung. */ bool cbm_is_module_level_p(TSNode parent, CBMLanguage lang) { if (ts_node_is_null(parent)) { return false; } const char *pk = ts_node_type(parent); // Languages with wrapper-pattern (expression_statement/export_statement/assignment_statement) if (lang == CBM_LANG_PYTHON) { return check_script_module_level(parent, pk, "module", "expression_statement"); } if (lang == CBM_LANG_JAVASCRIPT || lang == CBM_LANG_TYPESCRIPT || lang == CBM_LANG_TSX) { return check_script_module_level(parent, pk, "program", "export_statement"); } if (lang == CBM_LANG_LUA) { return check_script_module_level(parent, pk, "chunk", "assignment_statement"); } if (lang == CBM_LANG_YAML) { return strcmp(pk, "document") == 0 || strcmp(pk, "stream") == 0 || strcmp(pk, "block_mapping") == 0; } // Table lookup for the rest const char **parents = get_module_parents(lang); if (parents) { for (const char **p = parents; *p; p++) { if (strcmp(pk, *p) == 0) { return true; } } } return false; } /* Back-compat wrapper: computes the parent via ts_node_parent (O(n)). * Prefer cbm_is_module_level_p at call sites that already know the * parent (the common case — iterating a parent's children). */ bool cbm_is_module_level(TSNode node, CBMLanguage lang) { return cbm_is_module_level_p(ts_node_parent(node), lang); } // --- FQN computation --- // Mirrors Go's fqn.Compute(): project + path_parts_dotted + name // Internal helper: find extension start in basename (returns length without ext) static size_t strip_ext_len(const char *s, size_t len) { for (size_t i = len; i > 0; i--) { if (s[i - SKIP_ONE] == '.') { /* A dot at the very start of a filename segment (index 0, or right * after a '/') is a DOTFILE marker (".env", ".gitignore"), NOT an * extension separator. Stripping there leaves an empty stem whose * module QN collides with the parent directory/project root. Keep * the whole name as the stem; the leading dot is dropped later in * append_path_segments. */ if (i - SKIP_ONE == 0 || s[i - SKIP_ONE - SKIP_ONE] == '/') { return len; } return i - SKIP_ONE; } if (s[i - SKIP_ONE] == '/') { break; } } return len; } // Check if a path part should be skipped (Python __init__, JS/TS index). static bool should_skip_fqn_part(const char *part, size_t part_len, bool is_last, bool has_name) { if (!is_last || !has_name) { return false; } if (part_len == INIT_FILE_LEN && memcmp(part, "__init__", INIT_FILE_LEN) == 0) { return true; } if (part_len == INDEX_FILE_LEN && memcmp(part, "index", INDEX_FILE_LEN) == 0) { return true; } return false; } // Append dotted path segments from rel_path (extension-stripped) to output buffer. static char *append_path_segments(char *out, const char *rel_path, size_t plen, bool has_name) { const char *start = rel_path; const char *end_ptr = rel_path + plen; while (start < end_ptr) { const char *slash = (const char *)memchr(start, '/', end_ptr - start); const char *part_end = slash ? slash : end_ptr; size_t part_len = (size_t)(part_end - start); if (part_len > 0) { bool is_last = (part_end == end_ptr); if (!should_skip_fqn_part(start, part_len, is_last, has_name)) { /* Drop a leading '.' from a dotfile / hidden-dir segment * (".env" -> "env", ".github" -> "github"). Otherwise the QN * separator '.' plus the segment's own leading '.' produce a * malformed "proj..env" double-dot, and a root dotfile's empty * stem collides with the project QN. */ const char *seg = start; size_t seg_len = part_len; if (seg[0] == '.') { seg++; seg_len--; } if (seg_len > 0) { *out++ = '.'; memcpy(out, seg, seg_len); out += seg_len; } } } start = part_end + SKIP_ONE; } return out; } char *cbm_fqn_compute(CBMArena *a, const char *project, const char *rel_path, const char *name) { if (!project) project = ""; if (!rel_path) rel_path = ""; size_t proj_len = strlen(project); size_t path_len = strlen(rel_path); size_t name_len = name ? strlen(name) : 0; size_t max_len = proj_len + SKIP_ONE + path_len + SKIP_ONE + name_len + SKIP_ONE; char *buf = (char *)cbm_arena_alloc(a, max_len); if (!buf) { return NULL; } char *out = buf; memcpy(out, project, proj_len); out += proj_len; size_t plen = strip_ext_len(rel_path, path_len); out = append_path_segments(out, rel_path, plen, name && name_len > 0); if (name && name_len > 0) { *out++ = '.'; memcpy(out, name, name_len); out += name_len; } *out = '\0'; return buf; } char *cbm_fqn_module(CBMArena *a, const char *project, const char *rel_path) { return cbm_fqn_compute(a, project, rel_path, NULL); } // True when a language derives its module from the CONTAINING DIRECTORY (Java // package, Go package) rather than baking the filename stem into the module QN. // For these languages a sibling file in the same dir shares the module, and the // type/method name is appended once — so a class `Outer` in `Outer.java` is // `proj.Outer`, not `proj.Outer.Outer`, and a method in `myapp/db/conn.go` // belongs to module `proj.myapp.db`, not `proj.myapp.db.conn`. static bool cbm_lang_module_is_dir(CBMLanguage lang) { return lang == CBM_LANG_JAVA || lang == CBM_LANG_GO; } char *cbm_fqn_module_source_lang(CBMArena *a, const char *project, const char *rel_path, CBMLanguage lang) { if (!cbm_lang_module_is_dir(lang)) { // All other languages keep the legacy filename-stem module QN. return cbm_fqn_module(a, project, rel_path); } if (!rel_path) { rel_path = ""; } // Module is the CONTAINING DIRECTORY: strip the basename (last '/' segment). const char *last_slash = strrchr(rel_path, '/'); if (!last_slash) { // Root file: dir is empty → module is just the project. return cbm_fqn_folder(a, project, ""); } size_t dir_len = (size_t)(last_slash - rel_path); char *dir = (char *)cbm_arena_alloc(a, dir_len + SKIP_ONE); if (!dir) { return NULL; } memcpy(dir, rel_path, dir_len); dir[dir_len] = '\0'; return cbm_fqn_folder(a, project, dir); } char *cbm_fqn_compute_source_lang(CBMArena *a, const char *project, const char *rel_path, const char *name, CBMLanguage lang) { if (!cbm_lang_module_is_dir(lang)) { // All other languages keep the legacy filename-stem symbol QN. return cbm_fqn_compute(a, project, rel_path, name); } char *module = cbm_fqn_module_source_lang(a, project, rel_path, lang); if (!module) { return NULL; } if (!name || !name[0]) { return module; } return cbm_arena_sprintf(a, "%s.%s", module, name); } char *cbm_fqn_folder(CBMArena *a, const char *project, const char *rel_dir) { // project.dir1.dir2 size_t proj_len = strlen(project); size_t dir_len = strlen(rel_dir); size_t max_len = proj_len + SKIP_ONE + dir_len + SKIP_ONE; char *buf = (char *)cbm_arena_alloc(a, max_len); if (!buf) { return NULL; } char *out = buf; memcpy(out, project, proj_len); out += proj_len; if (dir_len > 0 && !(dir_len == SKIP_ONE && rel_dir[0] == '.')) { const char *start = rel_dir; const char *end_ptr = rel_dir + dir_len; while (start < end_ptr) { const char *slash = (const char *)memchr(start, '/', end_ptr - start); const char *part_end = slash ? slash : end_ptr; size_t part_len = (size_t)(part_end - start); if (part_len > 0) { *out++ = '.'; memcpy(out, start, part_len); out += part_len; } start = part_end + SKIP_ONE; } } *out = '\0'; return buf; } /* ── String literal classifier ──────────────────────────────────── */ // Check if a slash-prefixed string looks like a filesystem path. static bool is_filesystem_path(const char *s, int len) { if (len <= MIN_SYS_PATH_LEN) { return false; } return strncmp(s, "/usr/", SLEN("/usr/")) == 0 || strncmp(s, "/bin/", SLEN("/bin/")) == 0 || strncmp(s, "/etc/", SLEN("/etc/")) == 0 || strncmp(s, "/var/", SLEN("/var/")) == 0 || strncmp(s, "/tmp/", SLEN("/tmp/")) == 0 || strncmp(s, "/opt/", SLEN("/opt/")) == 0 || strncmp(s, "/home/", SLEN("/home/")) == 0 || strncmp(s, "/dev/", SLEN("/dev/")) == 0 || strncmp(s, "/sys/", SLEN("/sys/")) == 0 || strncmp(s, "/proc/", SLEN("/proc/")) == 0; } // Check if a slash-prefixed string looks like a REST API path. static bool is_rest_path(const char *s, int len) { if (is_filesystem_path(s, len)) { return false; } if (len > SKIP_ONE && s[len - SKIP_ONE] == '/') { return false; /* regex pattern */ } if (s[SKIP_ONE] == '^') { return false; /* regex */ } if (len == SKIP_ONE || (len == PAIR_LEN && s[SKIP_ONE] == '/')) { return false; /* bare / or // */ } if (s[SKIP_ONE] == '.') { return false; /* relative path */ } for (int i = SKIP_ONE; i < len && i < MAX_ROUTE_SCAN; i++) { char c = s[i]; if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) { return true; } } return false; } static bool is_url_like(const char *s, int len) { if (len < MIN_ROUTE_LEN) { return false; } if (strstr(s, "://")) { return true; } if (s[0] == '/') { return is_rest_path(s, len); } return false; } static bool has_config_extension(const char *s, int len) { static const char *exts[] = {".toml", ".yaml", ".yml", ".json", ".ini", ".env", ".cfg", ".conf", ".properties", NULL}; for (int i = 0; exts[i]; i++) { int elen = (int)strlen(exts[i]); if (len > elen && strcmp(s + len - elen, exts[i]) == 0) { return true; } } return false; } static bool is_env_var_pattern(const char *s, int len) { if (len < MIN_ROUTE_LEN || len > MAX_HEX_NAME_LEN) { return false; } bool has_upper = false; bool has_underscore = false; for (int i = 0; i < len; i++) { char c = s[i]; if (c >= 'A' && c <= 'Z') { has_upper = true; } else if (c == '_') { has_underscore = true; } else if (c >= '0' && c <= '9') { /* digits ok */ } else { return false; } } return has_upper && has_underscore; } int cbm_classify_string(const char *str, int len) { if (!str || len < PAIR_LEN) { return NOT_FOUND; } if (is_url_like(str, len)) { return CBM_STRREF_URL; } if (has_config_extension(str, len)) { return CBM_STRREF_CONFIG; } if (is_env_var_pattern(str, len)) { return CBM_STRREF_CONFIG; } return NOT_FOUND; }