Skip to content

Commit 25457d2

Browse files
committed
fix(registry): restrict same-module suffix resolution to self/namespace receivers
1 parent feadbe1 commit 25457d2

4 files changed

Lines changed: 230 additions & 58 deletions

File tree

internal/cbm/extract_defs.c

Lines changed: 54 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3554,8 +3554,9 @@ static void extract_class_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
35543554
}
35553555

35563556
TSNode name_node = ts_node_child_by_field_name(node, TS_FIELD("name"));
3557-
// ObjC: class name is first identifier child
3558-
if (ts_node_is_null(name_node) && ctx->language == CBM_LANG_OBJC) {
3557+
// ObjC/Java: class name is first identifier child (Java fallback for enum_declaration)
3558+
if (ts_node_is_null(name_node) &&
3559+
(ctx->language == CBM_LANG_OBJC || ctx->language == CBM_LANG_JAVA)) {
35593560
name_node = cbm_find_child_by_kind(node, "identifier");
35603561
}
35613562
// ObjectScript UDL: class name is a `class_name` child (no "name" field).
@@ -3969,77 +3970,93 @@ static void extract_class_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
39693970

39703971
// Find the body/members node inside a class node
39713972
static TSNode find_class_body(TSNode class_node, CBMLanguage lang) {
3973+
TSNode result = {0};
39723974
// Try field names first
39733975
static const char *body_fields[] = {"body", "members", "class_body", "declaration_list", NULL};
39743976
for (const char **f = body_fields; *f; f++) {
39753977
TSNode body = ts_node_child_by_field_name(class_node, *f, (uint32_t)strlen(*f));
39763978
if (!ts_node_is_null(body)) {
3977-
return body;
3979+
result = body;
3980+
break;
39783981
}
39793982
}
39803983
// Go: type_spec -> type field (interface_type or struct_type)
3981-
if (lang == CBM_LANG_GO) {
3984+
if (ts_node_is_null(result) && lang == CBM_LANG_GO) {
39823985
TSNode type_inner = ts_node_child_by_field_name(class_node, TS_FIELD("type"));
39833986
if (!ts_node_is_null(type_inner)) {
3984-
return type_inner;
3987+
result = type_inner;
39853988
}
39863989
}
39873990
// ObjC: class_implementation/class_interface has no single body node
39883991
// Methods are inside implementation_definition children directly
3989-
if (lang == CBM_LANG_OBJC) {
3992+
if (ts_node_is_null(result) && lang == CBM_LANG_OBJC) {
39903993
return class_node; // iterate children of the class node itself
39913994
}
39923995
// Squirrel: class_declaration has no body field — member_declaration nodes
39933996
// (each wrapping a function_declaration) are direct children of the class.
3994-
if (lang == CBM_LANG_SQUIRREL) {
3997+
if (ts_node_is_null(result) && lang == CBM_LANG_SQUIRREL) {
39953998
return class_node;
39963999
}
39974000
// Smali: field_definition nodes are direct children of class_definition (no
39984001
// dedicated body node) — iterate the class node itself.
3999-
if (lang == CBM_LANG_SMALI) {
4002+
if (ts_node_is_null(result) && lang == CBM_LANG_SMALI) {
40004003
return class_node;
40014004
}
40024005
// GraphQL: object/interface fields live in a fields_definition child.
4003-
if (lang == CBM_LANG_GRAPHQL) {
4006+
if (ts_node_is_null(result) && lang == CBM_LANG_GRAPHQL) {
40044007
TSNode b = cbm_find_child_by_kind(class_node, "fields_definition");
40054008
if (!ts_node_is_null(b)) {
4006-
return b;
4009+
result = b;
40074010
}
40084011
}
40094012
// Prisma: model columns live in a statement_block child. Gated to Prisma so
4010-
// the common "statement_block" kind can never hijack another language's
4011-
// class body via the generic fallback below.
4012-
if (lang == CBM_LANG_PRISMA) {
4013+
// general fallback behavior is unchanged.
4014+
if (ts_node_is_null(result) && lang == CBM_LANG_PRISMA) {
40134015
TSNode b = cbm_find_child_by_kind(class_node, "statement_block");
40144016
if (!ts_node_is_null(b)) {
4015-
return b;
4017+
result = b;
40164018
}
40174019
}
40184020
// Fallback: search children for known body node types
4019-
static const char *body_types[] = {"class_body",
4020-
"interface_body",
4021-
"enum_body",
4022-
"template_body",
4023-
"interface_type",
4024-
"struct_type",
4025-
"field_declaration_list",
4026-
"compound_statement",
4027-
"block",
4028-
"closure",
4029-
"implementation_definition",
4030-
NULL};
4031-
uint32_t count = ts_node_child_count(class_node);
4032-
for (uint32_t i = 0; i < count; i++) {
4033-
TSNode child = ts_node_child(class_node, i);
4034-
const char *ck = ts_node_type(child);
4035-
for (const char **t = body_types; *t; t++) {
4036-
if (strcmp(ck, *t) == 0) {
4037-
return child;
4021+
if (ts_node_is_null(result)) {
4022+
static const char *body_types[] = {"class_body",
4023+
"interface_body",
4024+
"enum_body",
4025+
"template_body",
4026+
"interface_type",
4027+
"struct_type",
4028+
"field_declaration_list",
4029+
"compound_statement",
4030+
"block",
4031+
"closure",
4032+
"implementation_definition",
4033+
NULL};
4034+
uint32_t count = ts_node_child_count(class_node);
4035+
for (uint32_t i = 0; i < count; i++) {
4036+
TSNode child = ts_node_child(class_node, i);
4037+
const char *ck = ts_node_type(child);
4038+
for (const char **t = body_types; *t; t++) {
4039+
if (strcmp(ck, *t) == 0) {
4040+
result = child;
4041+
break;
4042+
}
4043+
}
4044+
if (!ts_node_is_null(result)) {
4045+
break;
40384046
}
40394047
}
40404048
}
4041-
TSNode null_node = {0};
4042-
return null_node;
4049+
if (!ts_node_is_null(result) && strcmp(ts_node_type(result), "enum_body") == 0) {
4050+
TSNode decls = cbm_find_child_by_kind(result, "enum_body_declarations");
4051+
if (!ts_node_is_null(decls)) {
4052+
return decls;
4053+
}
4054+
}
4055+
if (ts_node_is_null(result)) {
4056+
TSNode null_node = {0};
4057+
return null_node;
4058+
}
4059+
return result;
40434060
}
40444061

40454062
// Dart: resolve method name from method_signature/function_signature.
@@ -6200,7 +6217,8 @@ static void push_class_body_children(TSNode node, const CBMLangSpec *spec, wd_st
62006217
const char *ck = ts_node_type(child);
62016218
if (strcmp(ck, "field_declaration_list") == 0 || strcmp(ck, "class_body") == 0 ||
62026219
strcmp(ck, "declaration_list") == 0 || strcmp(ck, "body") == 0 ||
6203-
strcmp(ck, "block") == 0 || strcmp(ck, "suite") == 0 ||
6220+
strcmp(ck, "block") == 0 || strcmp(ck, "suite") == 0 || strcmp(ck, "enum_body") == 0 ||
6221+
strcmp(ck, "enum_body_declarations") == 0 ||
62046222
// Groovy class bodies are a `closure` node; routing through the
62056223
// nested-class path keeps methods from being re-walked (and thus
62066224
// double-extracted) as top-level functions. Gated to Groovy so other

src/pipeline/registry.c

Lines changed: 127 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -673,20 +673,48 @@ static cbm_resolution_t resolve_import_map(const cbm_registry_t *r, const char *
673673
return empty_result();
674674
}
675675

676+
static bool is_same_module_receiver(const char *prefix, const char *module_qn) {
677+
if (!prefix || !prefix[0]) {
678+
return true; /* Bare names are always candidates for same-module resolution */
679+
}
680+
/* 1. Check known self-receivers */
681+
static const char *const self_receivers[] = {"self", "this", "cls", "@self", NULL};
682+
for (int i = 0; self_receivers[i]; i++) {
683+
if (strcmp(prefix, self_receivers[i]) == 0) {
684+
return true;
685+
}
686+
}
687+
/* 2. Check if prefix matches the module name or its last segment (namespace qualified) */
688+
size_t plen = strlen(prefix);
689+
size_t mlen = strlen(module_qn);
690+
if (mlen == plen && strcmp(module_qn, prefix) == 0) {
691+
return true;
692+
}
693+
if (mlen > plen && module_qn[mlen - plen - 1] == '.' &&
694+
strcmp(module_qn + (mlen - plen), prefix) == 0) {
695+
return true;
696+
}
697+
return false;
698+
}
699+
676700
/* Strategy 2: Same-module match */
677701
static cbm_resolution_t resolve_same_module(const cbm_registry_t *r, const char *callee_name,
678-
const char *suffix, const char *module_qn) {
702+
const char *prefix, const char *suffix,
703+
const char *module_qn) {
679704
char candidate[CBM_SZ_512];
680705
snprintf(candidate, sizeof(candidate), "%s.%s", module_qn, callee_name);
681706
const char *stored_key = cbm_ht_get_key(r->exact, candidate);
682707
if (stored_key) {
683708
return (cbm_resolution_t){stored_key, "same_module", CONF_SAME_MODULE, REG_RESOLVED};
684709
}
685710
if (suffix && suffix[0]) {
686-
snprintf(candidate, sizeof(candidate), "%s.%s", module_qn, suffix);
687-
stored_key = cbm_ht_get_key(r->exact, candidate);
688-
if (stored_key) {
689-
return (cbm_resolution_t){stored_key, "same_module", CONF_SAME_MODULE, REG_RESOLVED};
711+
if (is_same_module_receiver(prefix, module_qn)) {
712+
snprintf(candidate, sizeof(candidate), "%s.%s", module_qn, suffix);
713+
stored_key = cbm_ht_get_key(r->exact, candidate);
714+
if (stored_key) {
715+
return (cbm_resolution_t){stored_key, "same_module", CONF_SAME_MODULE,
716+
REG_RESOLVED};
717+
}
690718
}
691719
}
692720
return empty_result();
@@ -778,6 +806,66 @@ static const char *qualified_suffix_match(const qn_array_t *arr, const char *cal
778806
}
779807
return match;
780808
}
809+
static bool qn_ends_with_qualified(const char *qn, const char *callee_name) {
810+
char dotted[CBM_SZ_512];
811+
size_t w = 0;
812+
for (const char *s = callee_name; *s && w + 1 < sizeof(dotted);) {
813+
if (s[0] == ':' && s[1] == ':') {
814+
dotted[w++] = '.';
815+
s += 2;
816+
} else {
817+
dotted[w++] = *s++;
818+
}
819+
}
820+
dotted[w] = '\0';
821+
822+
size_t qlen = strlen(qn);
823+
if (qlen < w) {
824+
return false;
825+
}
826+
const char *tail = qn + (qlen - w);
827+
if (strcmp(tail, dotted) != 0) {
828+
return false;
829+
}
830+
if (tail != qn && tail[-1] != '.') {
831+
return false;
832+
}
833+
return true;
834+
}
835+
static bool is_type_like_label(const char *label) {
836+
if (!label) {
837+
return false;
838+
}
839+
return strcmp(label, "Class") == 0 || strcmp(label, "Struct") == 0 ||
840+
strcmp(label, "Interface") == 0 || strcmp(label, "Enum") == 0 ||
841+
strcmp(label, "Type") == 0 || strcmp(label, "Trait") == 0;
842+
}
843+
844+
static bool is_candidate_method(const cbm_registry_t *r, const char *qn) {
845+
const char *label = cbm_registry_label_of(r, qn);
846+
if (label && strcmp(label, "Method") == 0) {
847+
return true;
848+
}
849+
850+
char parent_qn[CBM_SZ_512];
851+
size_t len = strlen(qn);
852+
if (len >= sizeof(parent_qn)) {
853+
return false;
854+
}
855+
strcpy(parent_qn, qn);
856+
char *last_dot = strrchr(parent_qn, '.');
857+
if (!last_dot) {
858+
return false;
859+
}
860+
*last_dot = '\0';
861+
862+
const char *parent_label = cbm_registry_label_of(r, parent_qn);
863+
return is_type_like_label(parent_label);
864+
}
865+
866+
static bool is_qualified_callee(const char *callee_name) {
867+
return strchr(callee_name, '.') != NULL || strstr(callee_name, "::") != NULL;
868+
}
781869

782870
/* Strategy 3+4: Name lookup + suffix match */
783871
static cbm_resolution_t resolve_name_lookup(const cbm_registry_t *r, const char *callee_name,
@@ -792,36 +880,53 @@ static cbm_resolution_t resolve_name_lookup(const cbm_registry_t *r, const char
792880
return empty_result(); /* unresolvably ambiguous — see REG_MAX_CANDIDATES */
793881
}
794882

883+
cbm_resolution_t res = empty_result();
884+
795885
/* Strategy 3.5: a qualified callee disambiguates among multiple same-name
796886
* candidates by full qualified tail, before bare-name scoring collapses
797887
* them onto a single winner. */
798888
if (arr->count > 1) {
799889
const char *q = qualified_suffix_match(arr, callee_name);
800890
if (q) {
801-
return (cbm_resolution_t){q, "qualified_suffix", CONF_QUALIFIED_SUFFIX, REG_RESOLVED};
891+
res = (cbm_resolution_t){q, "qualified_suffix", CONF_QUALIFIED_SUFFIX, REG_RESOLVED};
802892
}
803893
}
804894

805-
/* Strategy 3: unique name */
806-
if (arr->count == SKIP_ONE) {
807-
double conf = CONF_UNIQUE_NAME;
808-
if (import_vals && import_count > 0 &&
809-
!is_import_reachable(arr->items[0], import_vals, import_count)) {
810-
conf *= DEFAULT_CONFIDENCE;
895+
if (!(res.qualified_name && res.qualified_name[0])) {
896+
/* Strategy 3: unique name */
897+
if (arr->count == 1) {
898+
double conf = CONF_UNIQUE_NAME;
899+
if (import_vals && import_count > 0 &&
900+
!is_import_reachable(arr->items[0], import_vals, import_count)) {
901+
conf *= DEFAULT_CONFIDENCE;
902+
}
903+
res = (cbm_resolution_t){arr->items[0], "unique_name", conf, REG_RESOLVED};
811904
}
812-
return (cbm_resolution_t){arr->items[0], "unique_name", conf, REG_RESOLVED};
813905
}
814906

815-
/* Strategy 4: multiple candidates */
816-
if (import_vals && import_count > 0) {
817-
return resolve_multi_with_imports(arr, module_qn, import_vals, import_count);
907+
if (!(res.qualified_name && res.qualified_name[0])) {
908+
/* Strategy 4: multiple candidates */
909+
if (import_vals && import_count > 0) {
910+
res = resolve_multi_with_imports(arr, module_qn, import_vals, import_count);
911+
} else {
912+
const char *best =
913+
best_by_import_distance((const char **)arr->items, arr->count, module_qn);
914+
if (best) {
915+
double conf = candidate_count_penalty(CONF_SUFFIX_MATCH, arr->count);
916+
res = (cbm_resolution_t){best, "suffix_match", conf, arr->count};
917+
}
918+
}
818919
}
819-
const char *best = best_by_import_distance((const char **)arr->items, arr->count, module_qn);
820-
if (best) {
821-
double conf = candidate_count_penalty(CONF_SUFFIX_MATCH, arr->count);
822-
return (cbm_resolution_t){best, "suffix_match", conf, arr->count};
920+
921+
if (res.qualified_name && is_qualified_callee(callee_name)) {
922+
if (!is_candidate_method(r, res.qualified_name)) {
923+
if (!qn_ends_with_qualified(res.qualified_name, callee_name)) {
924+
return empty_result();
925+
}
926+
}
823927
}
824-
return empty_result();
928+
929+
return res;
825930
}
826931

827932
cbm_resolution_t cbm_registry_resolve(const cbm_registry_t *r, const char *callee_name,
@@ -874,7 +979,7 @@ cbm_resolution_t cbm_registry_resolve(const cbm_registry_t *r, const char *calle
874979
resolve_import_map(r, prefix, suffix, import_map_keys, import_map_vals, import_map_count);
875980
if (!(res.qualified_name && res.qualified_name[0])) {
876981
/* Strategy 2: same module */
877-
res = resolve_same_module(r, callee_name, suffix, module_qn);
982+
res = resolve_same_module(r, callee_name, prefix, suffix, module_qn);
878983
}
879984
if (!(res.qualified_name && res.qualified_name[0])) {
880985
/* Strategy 3+4: name lookup */

tests/test_extraction.c

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,23 @@ TEST(java_class_extends_and_implements) {
418418
PASS();
419419
}
420420

421+
TEST(java_enum_method) {
422+
CBMFileResult *r = extract(
423+
"enum Day {\n"
424+
" MON, TUE, WED, THU, FRI, SAT, SUN;\n\n"
425+
" public boolean isWeekend() { return this == SAT || this == SUN; }\n"
426+
" public String label() { return name().toLowerCase(); }\n"
427+
"}\n",
428+
CBM_LANG_JAVA, "t", "Day.java");
429+
ASSERT_NOT_NULL(r);
430+
ASSERT_FALSE(r->has_error);
431+
ASSERT(has_def(r, "Enum", "Day"));
432+
ASSERT(has_def(r, "Method", "isWeekend"));
433+
ASSERT(has_def(r, "Method", "label"));
434+
cbm_free_result(r);
435+
PASS();
436+
}
437+
421438
/* REPRODUCTION (RED until fixed) — Python `class Animal(Base):` must extract the
422439
* BARE base name "Base", but extract_base_classes captures the whole
423440
* `superclasses` argument_list text "(Base)" instead: collect_bases_from_field
@@ -4429,6 +4446,7 @@ SUITE(extraction) {
44294446
RUN_TEST(java_method);
44304447
RUN_TEST(java_interface);
44314448
RUN_TEST(java_class_extends_and_implements);
4449+
RUN_TEST(java_enum_method);
44324450
RUN_TEST(python_class_base_extracted_bare);
44334451
RUN_TEST(php_class);
44344452
RUN_TEST(php_function);

0 commit comments

Comments
 (0)