From 47116b8e198808399a791cb5130646951677ba23 Mon Sep 17 00:00:00 2001 From: Ilya Brykau Date: Sun, 30 Aug 2026 12:57:41 +0200 Subject: [PATCH 1/3] fix(extract): descend into Go struct field_declaration_list Signed-off-by: Ilya Brykau --- internal/cbm/extract_defs.c | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/internal/cbm/extract_defs.c b/internal/cbm/extract_defs.c index 15f96ba17..7bb6ec955 100644 --- a/internal/cbm/extract_defs.c +++ b/internal/cbm/extract_defs.c @@ -4780,6 +4780,20 @@ static TSNode find_class_member_body(TSNode class_node, CBMLanguage lang) { return ts_node_is_null(declarations) ? body : declarations; } +/* Go structs keep their field_declaration nodes one level below the body that + * find_class_body() returns: type_spec's `type` child is a struct_type whose + * only named child is a field_declaration_list. Interfaces need no such step -- + * interface_type holds its method specs directly, which is why interface members + * extracted correctly while every struct field was silently skipped. Normalize + * here, the same way the Java enum_body_declarations step above does. */ +static TSNode go_normalize_struct_body(TSNode body) { + if (ts_node_is_null(body) || strcmp(ts_node_type(body), "struct_type") != 0) { + return body; + } + TSNode list = cbm_find_child_by_kind(body, "field_declaration_list"); + return ts_node_is_null(list) ? body : list; +} + // Dart: resolve method name from method_signature/function_signature. static TSNode resolve_dart_method_name(TSNode child, const char *ck) { if (strcmp(ck, "method_signature") == 0) { @@ -6656,6 +6670,9 @@ static void extract_class_fields(CBMExtractCtx *ctx, TSNode class_node, const ch } TSNode body = find_class_member_body(class_node, ctx->language); + if (ctx->language == CBM_LANG_GO) { + body = go_normalize_struct_body(body); + } if (ts_node_is_null(body)) { return; } From cb7cb444126f8ec4542ecf5daef8188470e48327 Mon Sep 17 00:00:00 2001 From: Ilya Brykau Date: Sun, 30 Aug 2026 13:04:24 +0200 Subject: [PATCH 2/3] fix(extract): skip Go blank-identifier struct fields Signed-off-by: Ilya Brykau --- internal/cbm/extract_defs.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/internal/cbm/extract_defs.c b/internal/cbm/extract_defs.c index 7bb6ec955..2e359af67 100644 --- a/internal/cbm/extract_defs.c +++ b/internal/cbm/extract_defs.c @@ -6962,6 +6962,13 @@ static void extract_class_fields(CBMExtractCtx *ctx, TSNode class_node, const ch continue; } + /* Go: `_` is the blank identifier, used for explicit struct padding in + * generated code. It is not a referenceable field, and emitting it gives + * every `_` in the repository a same-named node to collide with. */ + if (ctx->language == CBM_LANG_GO && strcmp(name, "_") == 0) { + continue; + } + const char *field_qn = cbm_arena_sprintf(a, "%s.%s", class_qn, name); CBMDefinition def; From e07a12b6690980cddc64cab8990ddd3831d88997 Mon Sep 17 00:00:00 2001 From: Ilya Brykau Date: Sun, 30 Aug 2026 14:26:11 +0200 Subject: [PATCH 3/3] test(extract): pin Go struct field extraction and blank-identifier skip Reproduce-first probe for the two fixes on this branch: RED without the descend fix (count_defs_with_label(r, "Field") == 0, expected 3), GREEN with it. Asserts the three named fields with their declared types in return_type, the absence of the blank identifier, and that interface members keep extracting exactly as before. Signed-off-by: Ilya Brykau --- tests/test_extraction.c | 54 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/tests/test_extraction.c b/tests/test_extraction.c index 5b7e6a8f7..5b8d16f61 100644 --- a/tests/test_extraction.c +++ b/tests/test_extraction.c @@ -3015,6 +3015,59 @@ TEST(go_imports) { PASS(); } +/* #1935: Go struct fields were never extracted — find_class_body() returns the + * struct_type node, whose only named child is a field_declaration_list, so the + * member loop matched nothing and every field was silently skipped (0 Field + * nodes for ~5055 declarations on the measured repo). Interfaces hold their + * method specs directly and always worked. The blank identifier `_` is struct + * padding, not a referenceable field, and must stay out (241 collision edges + * on two generated structs otherwise). */ +TEST(extract_go_struct_fields_have_nodes) { + CBMFileResult *r = extract("package fxf\n\n" + "type Config struct {\n" + "\tName string\n" + "\tTimeout int\n" + "\tNested *Config\n" + "\t_ [8]byte\n" + "}\n\n" + "type Reader interface {\n" + "\tRead(p []byte) (int, error)\n" + "\tClose() error\n" + "}\n", + CBM_LANG_GO, "t", "cfg.go"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + /* RED before the descend fix: count is 0. The blank identifier must not + * bring it to 4. */ + ASSERT_EQ(count_defs_with_label(r, "Field"), 3); + ASSERT_TRUE(has_def(r, "Field", "Name")); + ASSERT_TRUE(has_def(r, "Field", "Timeout")); + ASSERT_TRUE(has_def(r, "Field", "Nested")); + ASSERT_FALSE(has_def(r, "Field", "_")); + /* Each field carries its declared type in return_type. */ + for (int i = 0; i < r->defs.count; i++) { + const CBMDefinition *d = &r->defs.items[i]; + if (!d->label || strcmp(d->label, "Field") != 0) { + continue; + } + ASSERT_NOT_NULL(d->return_type); + if (strcmp(d->name, "Name") == 0) { + ASSERT_TRUE(strcmp(d->return_type, "string") == 0); + } + if (strcmp(d->name, "Timeout") == 0) { + ASSERT_TRUE(strcmp(d->return_type, "int") == 0); + } + if (strcmp(d->name, "Nested") == 0) { + ASSERT_TRUE(strcmp(d->return_type, "*Config") == 0); + } + } + /* Interface members keep extracting exactly as before. */ + ASSERT_TRUE(has_def(r, "Method", "Read")); + ASSERT_TRUE(has_def(r, "Method", "Close")); + cbm_free_result(r); + PASS(); +} + TEST(java_imports) { CBMFileResult *r = extract( "import java.util.List;\nimport java.util.ArrayList;\nimport static java.lang.Math.PI;\n" @@ -6699,6 +6752,7 @@ SUITE(extraction) { RUN_TEST(python_imports); RUN_TEST(js_imports); RUN_TEST(go_imports); + RUN_TEST(extract_go_struct_fields_have_nodes); RUN_TEST(java_imports); RUN_TEST(rust_imports); RUN_TEST(c_imports);