Skip to content
34 changes: 28 additions & 6 deletions internal/cbm/extract_defs.c
Original file line number Diff line number Diff line change
Expand Up @@ -3554,8 +3554,9 @@ 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");
}
// ObjectScript UDL: class name is a `class_name` child (no "name" field).
Expand Down Expand Up @@ -3974,6 +3975,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 @@ -4029,17 +4036,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 @@ -6200,7 +6221,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 @@ -673,20 +673,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 @@ -778,6 +806,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 @@ -792,36 +880,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 @@ -874,7 +979,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
18 changes: 18 additions & 0 deletions tests/test_extraction.c
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,23 @@ TEST(java_class_extends_and_implements) {
PASS();
}

TEST(java_enum_method) {
CBMFileResult *r = extract(
"enum Day {\n"
" MON, TUE, WED, THU, FRI, SAT, SUN;\n\n"
" public boolean isWeekend() { return this == SAT || this == SUN; }\n"
" public String label() { return name().toLowerCase(); }\n"
"}\n",
CBM_LANG_JAVA, "t", "Day.java");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
ASSERT(has_def(r, "Enum", "Day"));
ASSERT(has_def(r, "Method", "isWeekend"));
ASSERT(has_def(r, "Method", "label"));
cbm_free_result(r);
PASS();
}

/* REPRODUCTION (RED until fixed) — Python `class Animal(Base):` must extract the
* BARE base name "Base", but extract_base_classes captures the whole
* `superclasses` argument_list text "(Base)" instead: collect_bases_from_field
Expand Down Expand Up @@ -4429,6 +4446,7 @@ SUITE(extraction) {
RUN_TEST(java_method);
RUN_TEST(java_interface);
RUN_TEST(java_class_extends_and_implements);
RUN_TEST(java_enum_method);
RUN_TEST(python_class_base_extracted_bare);
RUN_TEST(php_class);
RUN_TEST(php_function);
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 @@ -855,6 +885,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