Files
codebase-memory-mcp/internal/cbm/helpers.c
T
Martin Vogel 6b1e3a0d76 fix(calls): extract resolvable builtin calls so LSP builtin edges form
The call extractor dropped any callee matched by cbm_is_keyword(), and the
Python keyword set lists builtins (len, str, range, int, list, dict, print) to
suppress them as bare usages. That also dropped the textual CALL, so the LSP
resolution to the injected builtins.<name> node (py_builtins.c) had no call to
attach to and no CALLS edge formed (callable=0) for free-function and
constructor builtins. Method builtins (s.upper) were unaffected because method
names are not in the keyword set.

Add cbm_is_resolvable_builtin() — the builtins we mint a real graph node for —
and keep those callees in call extraction only; the keyword filter still
applies to usage and semantic extraction (its four other call sites are
unchanged). Every un-filtered builtin has a target node, so the new edge is
always callable-sourced, never Module-sourced.

Un-skips repro_lsp_py_builtin and repro_lsp_py_builtin_constructor.
Repro suite 311/0, unit suite 5693/0.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 18:47:39 +02:00

1446 lines
57 KiB
C

#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 <stdlib.h> // 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 <stdint.h> // uint32_t
#include <string.h>
#include <ctype.h>
#include <stdio.h>
// --- 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.<name>"
// 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 <type>" 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;
}