Skip to content

Commit 554b171

Browse files
committed
fix(graph): precise Struct label for Rust/Swift/D/Go structs (Option B)
Struct definitions were labelled 'Class', conflating a distinct kind. Emit the precise 'Struct' label for Rust (struct_item), Swift/D (struct_declaration) and Go (type_spec struct_type); C/C++/Obj-C struct_specifier stays 'Class'. To avoid regressing type resolution, a struct must still behave as a type everywhere a class does. Added a centralized predicate cbm_label_is_type_like() (Class/Struct/Interface/Enum/Type/Trait) and routed every type-like consumer through it: registry seeding (pass_definitions/pass_parallel/pipeline_incremental kept in sync), class-resolution (pass_semantic/pass_parallel resolve_as_class), Go interface satisfaction (collects Struct nodes), the cross-file LSP label map, and the Go/Rust LSP type registrars. Class-only sites (protobuf services, K8s kinds, the C-family LSPs) are left unchanged. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
1 parent d97dee0 commit 554b171

13 files changed

Lines changed: 130 additions & 55 deletions

internal/cbm/cbm.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,4 +598,17 @@ void cbm_extract_unified(CBMExtractCtx *ctx);
598598
// K8s / Kustomize semantic extractor (called when language is CBM_LANG_K8S or CBM_LANG_KUSTOMIZE).
599599
void cbm_extract_k8s(CBMExtractCtx *ctx);
600600

601+
// --- Label predicates ---
602+
603+
// True when `label` names a TYPE-LIKE container definition — a node that can own
604+
// methods/fields, be a base/embedded type, satisfy/declare an interface, and be a
605+
// target of name→type resolution. The canonical set is:
606+
// Class, Struct, Interface, Enum, Type, Trait.
607+
// Single source of truth for every type-resolution / registry-seeding /
608+
// INHERITS·IMPLEMENTS / LSP-type-registrar consumer, so adding a new type-like
609+
// label (e.g. "Struct" for Rust/Go/Swift/D structs) updates them all at once
610+
// instead of scattering `|| strcmp(label,"Struct")==0` across the tree.
611+
// `label` may be NULL (returns false). Defined in helpers.c.
612+
bool cbm_label_is_type_like(const char *label);
613+
601614
#endif // CBM_H

internal/cbm/extract_defs.c

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3502,15 +3502,29 @@ static void extract_class_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
35023502

35033503
// Sway/WGSL: label struct defs as "Struct" and Sway `abi` blocks as
35043504
// "Interface". Scoped to these grammar-only languages so established
3505-
// struct-as-"Class" labeling (Rust/C++/Go/Cap'n Proto …) and the
3506-
// downstream type/IMPLEMENTS resolvers that depend on it are unaffected.
3505+
// struct-as-"Class" labeling (C++/Cap'n Proto …) and the downstream
3506+
// type/IMPLEMENTS resolvers that depend on it are unaffected.
35073507
if (ctx->language == CBM_LANG_SWAY || ctx->language == CBM_LANG_WGSL) {
35083508
if (strcmp(kind, "struct_item") == 0 || strcmp(kind, "struct_declaration") == 0) {
35093509
label = "Struct";
35103510
} else if (strcmp(kind, "abi_item") == 0) {
35113511
label = "Interface";
35123512
}
35133513
}
3514+
// Rust/Swift/D: a struct is a distinct kind from a class — emit the precise
3515+
// "Struct" label rather than collapsing it to "Class". Scoped to these three
3516+
// grammar/LSP languages whose struct node is `struct_item` (Rust) or
3517+
// `struct_declaration` (Swift, D). C/C++/Obj-C keep `struct_specifier` →
3518+
// "Class" (a C++ struct is class-like). "Struct" is a type-like container:
3519+
// every type-resolution / registry / IMPLEMENTS / LSP-registrar consumer
3520+
// routes through cbm_label_is_type_like(), so a struct still resolves as a
3521+
// type for its methods, fields, inheritance and impls.
3522+
if (ctx->language == CBM_LANG_RUST || ctx->language == CBM_LANG_SWIFT ||
3523+
ctx->language == CBM_LANG_DLANG) {
3524+
if (strcmp(kind, "struct_item") == 0 || strcmp(kind, "struct_declaration") == 0) {
3525+
label = "Struct";
3526+
}
3527+
}
35143528
// F#: a `type_definition` that has a primary constructor (`type Foo(...) =`)
35153529
// or an `inherit` clause is an OOP class, not a plain type alias. Label it
35163530
// "Class" so it is registered as a resolvable inheritance target (the graph
@@ -3525,15 +3539,18 @@ static void extract_class_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
35253539
}
35263540
}
35273541

3528-
// Go type_spec: check inner type for interface/struct
3542+
// Go type_spec: check inner type for interface/struct. A Go `type T struct
3543+
// {...}` is a struct → emit the precise "Struct" label (a type-like container;
3544+
// its methods/fields/embedding resolve through cbm_label_is_type_like(), and
3545+
// cbm_pipeline_implements_go() collects Struct nodes too).
35293546
if (strcmp(kind, "type_spec") == 0) {
35303547
TSNode type_inner = ts_node_child_by_field_name(node, TS_FIELD("type"));
35313548
if (!ts_node_is_null(type_inner)) {
35323549
const char *inner_kind = ts_node_type(type_inner);
35333550
if (strcmp(inner_kind, "interface_type") == 0) {
35343551
label = "Interface";
35353552
} else if (strcmp(inner_kind, "struct_type") == 0) {
3536-
label = "Class";
3553+
label = "Struct";
35373554
}
35383555
}
35393556
}

internal/cbm/helpers.c

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,18 @@ static const char *puppet_keywords[] = {
155155
"and", "or", "in", "node", "class", "define", "inherits", "default",
156156
"return", NULL};
157157

158+
// True when `label` names a type-like container definition (see cbm.h). Single
159+
// source of truth for the type-resolution / registry / IMPLEMENTS / LSP-type
160+
// consumers — adding a label here updates them all.
161+
bool cbm_label_is_type_like(const char *label) {
162+
if (!label) {
163+
return false;
164+
}
165+
return strcmp(label, "Class") == 0 || strcmp(label, "Struct") == 0 ||
166+
strcmp(label, "Interface") == 0 || strcmp(label, "Enum") == 0 ||
167+
strcmp(label, "Type") == 0 || strcmp(label, "Trait") == 0;
168+
}
169+
158170
bool cbm_is_keyword(const char *name, CBMLanguage lang) {
159171
if (!name || !name[0]) {
160172
return true;

internal/cbm/lsp/go_lsp.c

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1713,9 +1713,10 @@ void cbm_run_go_lsp(CBMArena* arena, CBMFileResult* result,
17131713
CBMDefinition* d = &result->defs.items[i];
17141714
if (!d->qualified_name || !d->name) continue;
17151715

1716-
// Register Class/Type nodes
1717-
if (d->label && (strcmp(d->label, "Class") == 0 || strcmp(d->label, "Type") == 0 ||
1718-
strcmp(d->label, "Interface") == 0)) {
1716+
// Register every type-like container (Class/Struct/Type/Interface/Enum/
1717+
// Trait). Struct included so a Go `type T struct {...}` (now labelled
1718+
// "Struct") is registered as a type and its methods/embedding resolve.
1719+
if (cbm_label_is_type_like(d->label)) {
17191720
CBMRegisteredType rt;
17201721
memset(&rt, 0, sizeof(rt));
17211722
rt.qualified_name = d->qualified_name;
@@ -2534,9 +2535,9 @@ void cbm_run_go_lsp_cross(
25342535

25352536
const char* def_mod = d->def_module_qn ? d->def_module_qn : module_qn;
25362537

2537-
// Type/Interface/Class
2538-
if (strcmp(d->label, "Type") == 0 || strcmp(d->label, "Class") == 0 ||
2539-
strcmp(d->label, "Interface") == 0) {
2538+
// Every type-like container (Type/Class/Struct/Interface/Enum/Trait).
2539+
// Struct included so Go structs (now labelled "Struct") register as types.
2540+
if (cbm_label_is_type_like(d->label)) {
25402541
CBMRegisteredType rt;
25412542
memset(&rt, 0, sizeof(rt));
25422543
rt.qualified_name = d->qualified_name; // borrowed
@@ -2787,8 +2788,9 @@ CBMTypeRegistry* cbm_go_build_cross_registry(
27872788
* fall back to — this registry is project-wide, not per-file. */
27882789
const char* def_mod = d->def_module_qn ? d->def_module_qn : "";
27892790

2790-
if (strcmp(d->label, "Type") == 0 || strcmp(d->label, "Class") == 0 ||
2791-
strcmp(d->label, "Interface") == 0) {
2791+
// Every type-like container (Type/Class/Struct/Interface/Enum/Trait).
2792+
// Struct included so Go structs (now labelled "Struct") register as types.
2793+
if (cbm_label_is_type_like(d->label)) {
27922794
CBMRegisteredType rt;
27932795
memset(&rt, 0, sizeof(rt));
27942796
rt.qualified_name = d->qualified_name; /* borrowed */

internal/cbm/lsp/rust_lsp.c

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4530,8 +4530,10 @@ static void rust_build_registry_from_defs(CBMArena *arena, CBMTypeRegistry *reg,
45304530
if (!d->qualified_name || !d->name)
45314531
continue;
45324532

4533-
if (d->label && (strcmp(d->label, "Class") == 0 || strcmp(d->label, "Type") == 0 ||
4534-
strcmp(d->label, "Interface") == 0 || strcmp(d->label, "Trait") == 0)) {
4533+
// Every type-like container (Class/Struct/Type/Interface/Trait/Enum).
4534+
// Struct included so a Rust `struct Foo` (now labelled "Struct") registers
4535+
// as a type and its `impl Foo` methods/fields resolve.
4536+
if (cbm_label_is_type_like(d->label)) {
45354537
CBMRegisteredType rt;
45364538
memset(&rt, 0, sizeof(rt));
45374539
rt.qualified_name = d->qualified_name;
@@ -4839,7 +4841,10 @@ static void rust_build_registry_from_defs(CBMArena *arena, CBMTypeRegistry *reg,
48394841
CBMDefinition *d = &result->defs.items[i];
48404842
if (!d->qualified_name || !d->name)
48414843
continue;
4842-
if (!d->label || (strcmp(d->label, "Class") != 0 && strcmp(d->label, "Type") != 0))
4844+
/* `#[derive(...)]` rides on type-like defs — most often a struct or
4845+
* enum (now labelled "Struct"/"Enum"), also type aliases. Accept the
4846+
* whole type-like set so a derive on a struct is not dropped. */
4847+
if (!cbm_label_is_type_like(d->label))
48434848
continue;
48444849
if (!d->decorators)
48454850
continue;
@@ -5151,8 +5156,9 @@ void cbm_run_rust_lsp_cross(CBMArena *arena, const char *source, int source_len,
51515156
continue;
51525157
const char *def_mod = d->def_module_qn ? d->def_module_qn : module_qn;
51535158

5154-
if (strcmp(d->label, "Type") == 0 || strcmp(d->label, "Class") == 0 ||
5155-
strcmp(d->label, "Interface") == 0 || strcmp(d->label, "Trait") == 0) {
5159+
// Every type-like container (Type/Class/Struct/Interface/Trait/Enum).
5160+
// Struct included so Rust structs (now labelled "Struct") register here.
5161+
if (cbm_label_is_type_like(d->label)) {
51565162
CBMRegisteredType rt;
51575163
memset(&rt, 0, sizeof(rt));
51585164
rt.qualified_name = cbm_arena_strdup(arena, d->qualified_name);

src/pipeline/pass_configlink.c

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,15 +105,17 @@ static int collect_config_entries(const cbm_gbuf_node_t *const *vars, int var_co
105105
return n;
106106
}
107107

108-
/* Collect code nodes (Function/Variable/Class) not from config files. */
108+
/* Collect code nodes (Function/Variable/Class/Struct) not from config files. */
109109
typedef struct {
110110
int64_t node_id;
111111
char normalized[CBM_SZ_256];
112112
} code_entry_t;
113113

114114
static int collect_code_entries(cbm_gbuf_t *gb, code_entry_t *out, int max_out) {
115115
int n = 0;
116-
static const char *labels[] = {"Function", "Variable", "Class", NULL};
116+
/* "Struct" alongside "Class": a config key may name a Go/Rust/Swift/D struct
117+
* type, which is now labelled "Struct" — keep it linkable. */
118+
static const char *labels[] = {"Function", "Variable", "Class", "Struct", NULL};
117119

118120
for (int li = 0; labels[li] && n < max_out; li++) {
119121
const cbm_gbuf_node_t **nodes = NULL;

src/pipeline/pass_definitions.c

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -295,15 +295,18 @@ static void process_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *def, const
295295
int64_t node_id = cbm_gbuf_upsert_node(
296296
ctx->gbuf, def->label ? def->label : "Function", def->name, def->qualified_name,
297297
def->file_path ? def->file_path : rel, (int)def->start_line, (int)def->end_line, props);
298-
/* Register callable symbols + Interface. Interface must be in the registry
299-
* so C#/Java `class Foo : IBar` / `class Foo implements IBar` can resolve
300-
* `IBar` to an INHERITS edge target during the enrichment phase.
301-
* Variable/Field defs are also registered so pass_usages.c can resolve
302-
* READS/WRITES accesses (rw->var_name) to a Variable/Field node QN. */
298+
/* Register callable symbols + every type-like container (Class/Struct/
299+
* Interface/Enum/Type/Trait). Type-like defs must be in the registry so
300+
* `class Foo : IBar` (INHERITS), `impl Trait for S` (IMPLEMENTS), and method/
301+
* field resolution can reach them — Struct included so Rust/Go/Swift/D structs
302+
* resolve as type targets just as a Class did. Variable/Field defs are also
303+
* registered so pass_usages.c can resolve READS/WRITES accesses (rw->var_name)
304+
* to a Variable/Field node QN.
305+
* KEEP IN SYNC with pass_parallel.c and pipeline_incremental.c's seed sets. */
303306
if (node_id > 0 && def->label &&
304307
(strcmp(def->label, "Function") == 0 || strcmp(def->label, "Method") == 0 ||
305-
strcmp(def->label, "Class") == 0 || strcmp(def->label, "Interface") == 0 ||
306-
strcmp(def->label, "Variable") == 0 || strcmp(def->label, "Field") == 0)) {
308+
cbm_label_is_type_like(def->label) || strcmp(def->label, "Variable") == 0 ||
309+
strcmp(def->label, "Field") == 0)) {
307310
cbm_registry_add(ctx->registry, def->name, def->qualified_name, def->label);
308311
}
309312
char *file_qn = cbm_pipeline_fqn_compute(ctx->project_name, rel, "__file__");

src/pipeline/pass_enrichment.c

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -292,8 +292,11 @@ static void free_tagged_nodes(tagged_node_t *nodes, int count) {
292292
/* Phase 1: Collect decorated nodes and count word frequency. */
293293
static int collect_decorated_nodes(cbm_gbuf_t *gbuf, tagged_node_t **out_nodes,
294294
CBMHashTable *word_counts) {
295-
static const char *labels[] = {"Function", "Method", "Class"};
296-
static const int nlabels = 3;
295+
/* "Struct" alongside "Class" so Go/Rust/Swift/D struct names keep
296+
* contributing to / receiving auto-tags as they did when structs were
297+
* labelled "Class". */
298+
static const char *labels[] = {"Function", "Method", "Class", "Struct"};
299+
static const int nlabels = 4;
297300
tagged_node_t *nodes = NULL;
298301
int node_count = 0;
299302
int node_cap = 0;

src/pipeline/pass_lsp_cross.c

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -82,16 +82,16 @@ static char *pxc_read_file(const char *path, int *out_len) {
8282
return buf;
8383
}
8484

85-
/* Map a CBMDefinition.label to a CBMLSPDef.label. Per-language LSP
86-
* registrars only care about Class/Interface/Trait/Enum/Type/Protocol/
87-
* Function/Method — variables, modules, decorators, etc. are skipped. */
85+
/* Map a CBMDefinition.label to a CBMLSPDef.label. Per-language LSP registrars
86+
* only care about type-like containers (Class/Struct/Interface/Trait/Enum/Type)
87+
* plus Protocol/Function/Method — variables, modules, decorators, etc. are
88+
* skipped. Struct passes through so Rust/Go struct type-registration via the
89+
* cross-file LSP path is not dropped. */
8890
static const char *pxc_map_label(const char *label) {
8991
if (!label)
9092
return NULL;
91-
if (strcmp(label, "Class") == 0 || strcmp(label, "Interface") == 0 ||
92-
strcmp(label, "Trait") == 0 || strcmp(label, "Enum") == 0 || strcmp(label, "Type") == 0 ||
93-
strcmp(label, "Protocol") == 0 || strcmp(label, "Function") == 0 ||
94-
strcmp(label, "Method") == 0) {
93+
if (cbm_label_is_type_like(label) || strcmp(label, "Protocol") == 0 ||
94+
strcmp(label, "Function") == 0 || strcmp(label, "Method") == 0) {
9595
return label;
9696
}
9797
return NULL;

src/pipeline/pass_parallel.c

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -410,12 +410,12 @@ static const char *resolve_as_class(const cbm_registry_t *reg, const char *name,
410410
if (!res.qualified_name || res.qualified_name[0] == '\0') {
411411
return NULL;
412412
}
413+
/* Accept any type-like container (Class/Struct/Interface/Enum/Type/Trait):
414+
* base classes, Rust `impl Trait for S` struct receivers, and Go struct
415+
* embedding all resolve through here. Struct included so the struct receiver
416+
* of an IMPLEMENTS edge is not dropped. */
413417
const char *label = cbm_registry_label_of(reg, res.qualified_name);
414-
if (!label) {
415-
return NULL;
416-
}
417-
if (strcmp(label, "Class") != 0 && strcmp(label, "Interface") != 0 &&
418-
strcmp(label, "Type") != 0 && strcmp(label, "Enum") != 0) {
418+
if (!cbm_label_is_type_like(label)) {
419419
return NULL;
420420
}
421421
return res.qualified_name;
@@ -822,11 +822,14 @@ static int register_and_link_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *d
822822
if (!def->name || !def->qualified_name || !def->label) {
823823
return 0;
824824
}
825-
/* Register callable symbols + Interface — see pass_definitions.c for rationale.
826-
* Variable/Field defs are registered too so READS/WRITES can resolve. */
825+
/* Register callable symbols + every type-like container (Class/Struct/
826+
* Interface/Enum/Type/Trait) — see pass_definitions.c for rationale. Struct
827+
* included so Rust/Go/Swift/D structs resolve as type targets. Variable/Field
828+
* defs are registered too so READS/WRITES can resolve.
829+
* KEEP IN SYNC with pass_definitions.c and pipeline_incremental.c. */
827830
if (strcmp(def->label, "Function") == 0 || strcmp(def->label, "Method") == 0 ||
828-
strcmp(def->label, "Class") == 0 || strcmp(def->label, "Interface") == 0 ||
829-
strcmp(def->label, "Variable") == 0 || strcmp(def->label, "Field") == 0) {
831+
cbm_label_is_type_like(def->label) || strcmp(def->label, "Variable") == 0 ||
832+
strcmp(def->label, "Field") == 0) {
830833
cbm_registry_add(ctx->registry, def->name, def->qualified_name, def->label);
831834
(*reg_entries)++;
832835
}

0 commit comments

Comments
 (0)