Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 29 additions & 6 deletions internal/cbm/extract_defs.c
Original file line number Diff line number Diff line change
Expand Up @@ -3502,10 +3502,12 @@ static void extract_class_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
}

TSNode name_node = ts_node_child_by_field_name(node, TS_FIELD("name"));
// ObjC: class name is first identifier child
if (ts_node_is_null(name_node) && ctx->language == CBM_LANG_OBJC) {
// ObjC/Java: class name is first identifier child (Java fallback for enum_declaration)
if (ts_node_is_null(name_node) &&
(ctx->language == CBM_LANG_OBJC || ctx->language == CBM_LANG_JAVA)) {
name_node = cbm_find_child_by_kind(node, "identifier");
}

// Swift and newer tree-sitter-kotlin: class/object name is a type_identifier
// child (no "name" field).
if (ts_node_is_null(name_node) &&
Expand Down Expand Up @@ -3918,6 +3920,12 @@ static TSNode find_class_body(TSNode class_node, CBMLanguage lang) {
for (const char **f = body_fields; *f; f++) {
TSNode body = ts_node_child_by_field_name(class_node, *f, (uint32_t)strlen(*f));
if (!ts_node_is_null(body)) {
if (strcmp(ts_node_type(body), "enum_body") == 0) {
TSNode decls = cbm_find_child_by_kind(body, "enum_body_declarations");
if (!ts_node_is_null(decls)) {
return decls;
}
}
return body;
}
}
Expand Down Expand Up @@ -3973,17 +3981,31 @@ static TSNode find_class_body(TSNode class_node, CBMLanguage lang) {
"implementation_definition",
NULL};
uint32_t count = ts_node_child_count(class_node);
TSNode result = {0};
for (uint32_t i = 0; i < count; i++) {
TSNode child = ts_node_child(class_node, i);
const char *ck = ts_node_type(child);
for (const char **t = body_types; *t; t++) {
if (strcmp(ck, *t) == 0) {
return child;
result = child;
break;
}
}
if (!ts_node_is_null(result)) {
break;
}
}
TSNode null_node = {0};
return null_node;
if (!ts_node_is_null(result) && strcmp(ts_node_type(result), "enum_body") == 0) {
TSNode decls = cbm_find_child_by_kind(result, "enum_body_declarations");
if (!ts_node_is_null(decls)) {
return decls;
}
}
if (ts_node_is_null(result)) {
TSNode null_node = {0};
return null_node;
}
return result;
}

// Dart: resolve method name from method_signature/function_signature.
Expand Down Expand Up @@ -5836,7 +5858,8 @@ static void push_class_body_children(TSNode node, const CBMLangSpec *spec, wd_st
const char *ck = ts_node_type(child);
if (strcmp(ck, "field_declaration_list") == 0 || strcmp(ck, "class_body") == 0 ||
strcmp(ck, "declaration_list") == 0 || strcmp(ck, "body") == 0 ||
strcmp(ck, "block") == 0 || strcmp(ck, "suite") == 0 ||
strcmp(ck, "block") == 0 || strcmp(ck, "suite") == 0 || strcmp(ck, "enum_body") == 0 ||
strcmp(ck, "enum_body_declarations") == 0 ||
// Groovy class bodies are a `closure` node; routing through the
// nested-class path keeps methods from being re-walked (and thus
// double-extracted) as top-level functions. Gated to Groovy so other
Expand Down
149 changes: 127 additions & 22 deletions src/pipeline/registry.c
Original file line number Diff line number Diff line change
Expand Up @@ -659,20 +659,48 @@ static cbm_resolution_t resolve_import_map(const cbm_registry_t *r, const char *
return empty_result();
}

static bool is_same_module_receiver(const char *prefix, const char *module_qn) {
if (!prefix || !prefix[0]) {
return true; /* Bare names are always candidates for same-module resolution */
}
/* 1. Check known self-receivers */
static const char *const self_receivers[] = {"self", "this", "cls", "@self", NULL};
for (int i = 0; self_receivers[i]; i++) {
if (strcmp(prefix, self_receivers[i]) == 0) {
return true;
}
}
/* 2. Check if prefix matches the module name or its last segment (namespace qualified) */
size_t plen = strlen(prefix);
size_t mlen = strlen(module_qn);
if (mlen == plen && strcmp(module_qn, prefix) == 0) {
return true;
}
if (mlen > plen && module_qn[mlen - plen - 1] == '.' &&
strcmp(module_qn + (mlen - plen), prefix) == 0) {
return true;
}
return false;
}

/* Strategy 2: Same-module match */
static cbm_resolution_t resolve_same_module(const cbm_registry_t *r, const char *callee_name,
const char *suffix, const char *module_qn) {
const char *prefix, const char *suffix,
const char *module_qn) {
char candidate[CBM_SZ_512];
snprintf(candidate, sizeof(candidate), "%s.%s", module_qn, callee_name);
const char *stored_key = cbm_ht_get_key(r->exact, candidate);
if (stored_key) {
return (cbm_resolution_t){stored_key, "same_module", CONF_SAME_MODULE, REG_RESOLVED};
}
if (suffix && suffix[0]) {
snprintf(candidate, sizeof(candidate), "%s.%s", module_qn, suffix);
stored_key = cbm_ht_get_key(r->exact, candidate);
if (stored_key) {
return (cbm_resolution_t){stored_key, "same_module", CONF_SAME_MODULE, REG_RESOLVED};
if (is_same_module_receiver(prefix, module_qn)) {
snprintf(candidate, sizeof(candidate), "%s.%s", module_qn, suffix);
stored_key = cbm_ht_get_key(r->exact, candidate);
if (stored_key) {
return (cbm_resolution_t){stored_key, "same_module", CONF_SAME_MODULE,
REG_RESOLVED};
}
}
}
return empty_result();
Expand Down Expand Up @@ -764,6 +792,66 @@ static const char *qualified_suffix_match(const qn_array_t *arr, const char *cal
}
return match;
}
static bool qn_ends_with_qualified(const char *qn, const char *callee_name) {
char dotted[CBM_SZ_512];
size_t w = 0;
for (const char *s = callee_name; *s && w + 1 < sizeof(dotted);) {
if (s[0] == ':' && s[1] == ':') {
dotted[w++] = '.';
s += 2;
} else {
dotted[w++] = *s++;
}
}
dotted[w] = '\0';

size_t qlen = strlen(qn);
if (qlen < w) {
return false;
}
const char *tail = qn + (qlen - w);
if (strcmp(tail, dotted) != 0) {
return false;
}
if (tail != qn && tail[-1] != '.') {
return false;
}
return true;
}
static bool is_type_like_label(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;
}

static bool is_candidate_method(const cbm_registry_t *r, const char *qn) {
const char *label = cbm_registry_label_of(r, qn);
if (label && strcmp(label, "Method") == 0) {
return true;
}

char parent_qn[CBM_SZ_512];
size_t len = strlen(qn);
if (len >= sizeof(parent_qn)) {
return false;
}
strcpy(parent_qn, qn);
char *last_dot = strrchr(parent_qn, '.');
if (!last_dot) {
return false;
}
*last_dot = '\0';

const char *parent_label = cbm_registry_label_of(r, parent_qn);
return is_type_like_label(parent_label);
}

static bool is_qualified_callee(const char *callee_name) {
return strchr(callee_name, '.') != NULL || strstr(callee_name, "::") != NULL;
}

/* Strategy 3+4: Name lookup + suffix match */
static cbm_resolution_t resolve_name_lookup(const cbm_registry_t *r, const char *callee_name,
Expand All @@ -778,36 +866,53 @@ static cbm_resolution_t resolve_name_lookup(const cbm_registry_t *r, const char
return empty_result(); /* unresolvably ambiguous — see REG_MAX_CANDIDATES */
}

cbm_resolution_t res = empty_result();

/* Strategy 3.5: a qualified callee disambiguates among multiple same-name
* candidates by full qualified tail, before bare-name scoring collapses
* them onto a single winner. */
if (arr->count > 1) {
const char *q = qualified_suffix_match(arr, callee_name);
if (q) {
return (cbm_resolution_t){q, "qualified_suffix", CONF_QUALIFIED_SUFFIX, REG_RESOLVED};
res = (cbm_resolution_t){q, "qualified_suffix", CONF_QUALIFIED_SUFFIX, REG_RESOLVED};
}
}

/* Strategy 3: unique name */
if (arr->count == SKIP_ONE) {
double conf = CONF_UNIQUE_NAME;
if (import_vals && import_count > 0 &&
!is_import_reachable(arr->items[0], import_vals, import_count)) {
conf *= DEFAULT_CONFIDENCE;
if (!(res.qualified_name && res.qualified_name[0])) {
/* Strategy 3: unique name */
if (arr->count == 1) {
double conf = CONF_UNIQUE_NAME;
if (import_vals && import_count > 0 &&
!is_import_reachable(arr->items[0], import_vals, import_count)) {
conf *= DEFAULT_CONFIDENCE;
}
res = (cbm_resolution_t){arr->items[0], "unique_name", conf, REG_RESOLVED};
}
return (cbm_resolution_t){arr->items[0], "unique_name", conf, REG_RESOLVED};
}

/* Strategy 4: multiple candidates */
if (import_vals && import_count > 0) {
return resolve_multi_with_imports(arr, module_qn, import_vals, import_count);
if (!(res.qualified_name && res.qualified_name[0])) {
/* Strategy 4: multiple candidates */
if (import_vals && import_count > 0) {
res = resolve_multi_with_imports(arr, module_qn, import_vals, import_count);
} else {
const char *best =
best_by_import_distance((const char **)arr->items, arr->count, module_qn);
if (best) {
double conf = candidate_count_penalty(CONF_SUFFIX_MATCH, arr->count);
res = (cbm_resolution_t){best, "suffix_match", conf, arr->count};
}
}
}
const char *best = best_by_import_distance((const char **)arr->items, arr->count, module_qn);
if (best) {
double conf = candidate_count_penalty(CONF_SUFFIX_MATCH, arr->count);
return (cbm_resolution_t){best, "suffix_match", conf, arr->count};

if (res.qualified_name && is_qualified_callee(callee_name)) {
if (!is_candidate_method(r, res.qualified_name)) {
if (!qn_ends_with_qualified(res.qualified_name, callee_name)) {
return empty_result();
}
}
}
return empty_result();

return res;
}

cbm_resolution_t cbm_registry_resolve(const cbm_registry_t *r, const char *callee_name,
Expand Down Expand Up @@ -860,7 +965,7 @@ cbm_resolution_t cbm_registry_resolve(const cbm_registry_t *r, const char *calle
resolve_import_map(r, prefix, suffix, import_map_keys, import_map_vals, import_map_count);
if (!(res.qualified_name && res.qualified_name[0])) {
/* Strategy 2: same module */
res = resolve_same_module(r, callee_name, suffix, module_qn);
res = resolve_same_module(r, callee_name, prefix, suffix, module_qn);
}
if (!(res.qualified_name && res.qualified_name[0])) {
/* Strategy 3+4: name lookup */
Expand Down
31 changes: 31 additions & 0 deletions tests/test_registry.c
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,36 @@ TEST(resolve_same_module) {
PASS();
}

TEST(resolve_same_module_only_on_self_receiver) {
cbm_registry_t *r = cbm_registry_new();
cbm_registry_add(r, "get", "proj.pkg.service.get", "Function");

/* Dotted call with unrelated receiver (e.g. "axios.get") -> should NOT resolve */
cbm_resolution_t res1 = cbm_registry_resolve(r, "axios.get", "proj.pkg.service", NULL, NULL, 0);
ASSERT_TRUE(res1.qualified_name == NULL || res1.qualified_name[0] == '\0');

/* Dotted call with delegation pattern (e.g. "_get_store().get") -> should NOT resolve */
cbm_resolution_t res2 = cbm_registry_resolve(r, "_get_store().get", "proj.pkg.service", NULL, NULL, 0);
ASSERT_TRUE(res2.qualified_name == NULL || res2.qualified_name[0] == '\0');

/* Dotted call with self-receiver (e.g. "self.get") -> should resolve */
cbm_resolution_t res3 = cbm_registry_resolve(r, "self.get", "proj.pkg.service", NULL, NULL, 0);
ASSERT_STR_EQ(res3.qualified_name, "proj.pkg.service.get");
ASSERT_STR_EQ(res3.strategy, "same_module");

/* Dotted call with exact module name prefix (e.g. "proj.pkg.service.get") -> should resolve */
cbm_resolution_t res4 = cbm_registry_resolve(r, "proj.pkg.service.get", "proj.pkg.service", NULL, NULL, 0);
ASSERT_STR_EQ(res4.qualified_name, "proj.pkg.service.get");

/* Dotted call with namespace/module last segment (e.g. "service.get") -> should resolve */
cbm_resolution_t res5 = cbm_registry_resolve(r, "service.get", "proj.pkg.service", NULL, NULL, 0);
ASSERT_STR_EQ(res5.qualified_name, "proj.pkg.service.get");
ASSERT_STR_EQ(res5.strategy, "same_module");

cbm_registry_free(r);
PASS();
}

/* A package/namespace-qualified callee whose bare name is defined in several
* places must resolve to the package named in the call — not collapse onto a
* single winner. Regression for qualified cross-file calls (e.g. Perl
Expand Down Expand Up @@ -814,6 +844,7 @@ SUITE(registry) {
RUN_TEST(registry_no_duplicates);
/* Resolution */
RUN_TEST(resolve_same_module);
RUN_TEST(resolve_same_module_only_on_self_receiver);
RUN_TEST(resolve_qualified_disambiguates_same_name);
RUN_TEST(resolve_qualified_ambiguous_tail_falls_through);
RUN_TEST(resolve_import_map);
Expand Down
Loading