diff --git a/internal/cbm/extract_defs.c b/internal/cbm/extract_defs.c index eab85b8ff..9f9ffcf10 100644 --- a/internal/cbm/extract_defs.c +++ b/internal/cbm/extract_defs.c @@ -3151,6 +3151,122 @@ static TSNode find_c_params(TSNode func_node) { return null_node; } +static bool c_return_type_size_add(size_t *total, size_t addition) { + if (addition > SIZE_MAX - *total) { + return false; + } + *total += addition; + return true; +} + +/* C-family grammars split a function's declared return type across sibling + * type/type_qualifier nodes and the declarator chain. Preserve the exact base + * spelling, leading qualifiers, and every pointer layer without imposing a + * depth cap: the walk follows one strict child chain, so runtime is O(D) and + * temporary memory is O(return-type bytes), where D is declarator depth. */ +static char *c_declarator_return_type_text(CBMExtractCtx *ctx, TSNode func_node, TSNode type_node) { + uint32_t base_start = ts_node_start_byte(type_node); + uint32_t base_end = ts_node_end_byte(type_node); + if (base_end <= base_start) { + return cbm_node_text(ctx->arena, type_node, ctx->source); + } + + TSNode declarator = ts_node_child_by_field_name(func_node, TS_FIELD("declarator")); + if (ts_node_is_null(declarator)) { + return cbm_node_text(ctx->arena, type_node, ctx->source); + } + + size_t pointer_count = 0; + for (TSNode node = declarator; !ts_node_is_null(node); + node = ts_node_child_by_field_name(node, TS_FIELD("declarator"))) { + if (strcmp(ts_node_type(node), "pointer_declarator") == 0) { + if (pointer_count == SIZE_MAX) { + ctx->result->has_error = true; + ctx->result->error_msg = + cbm_arena_strdup(ctx->arena, "C return-type pointer depth exceeds size limit"); + return cbm_node_text(ctx->arena, type_node, ctx->source); + } + pointer_count++; + } + } + + uint32_t declarator_start = ts_node_start_byte(declarator); + size_t qualifier_bytes = 0; + size_t qualifier_count = 0; + uint32_t child_count = ts_node_named_child_count(func_node); + for (uint32_t i = 0; i < child_count; i++) { + TSNode child = ts_node_named_child(func_node, i); + if (strcmp(ts_node_type(child), "type_qualifier") != 0 || + ts_node_end_byte(child) > declarator_start) { + continue; + } + uint32_t start = ts_node_start_byte(child); + uint32_t end = ts_node_end_byte(child); + if (end > start) { + size_t len = (size_t)(end - start); + if (len > SIZE_MAX - qualifier_bytes || qualifier_count == SIZE_MAX) { + ctx->result->has_error = true; + ctx->result->error_msg = + cbm_arena_strdup(ctx->arena, "C return-type qualifier size exceeds limit"); + return cbm_node_text(ctx->arena, type_node, ctx->source); + } + qualifier_bytes += len; + qualifier_count++; + } + } + + if (pointer_count == 0 && qualifier_count == 0) { + return cbm_node_text(ctx->arena, type_node, ctx->source); + } + + size_t base_len = (size_t)(base_end - base_start); + size_t result_len = base_len; + if (!c_return_type_size_add(&result_len, qualifier_bytes) || + !c_return_type_size_add(&result_len, qualifier_count) || + (pointer_count > 0 && !c_return_type_size_add(&result_len, SKIP_ONE)) || + !c_return_type_size_add(&result_len, pointer_count) || + !c_return_type_size_add(&result_len, NULL_TERM)) { + ctx->result->has_error = true; + ctx->result->error_msg = + cbm_arena_strdup(ctx->arena, "C return-type metadata exceeds addressable size"); + return cbm_node_text(ctx->arena, type_node, ctx->source); + } + char *result = cbm_arena_alloc(ctx->arena, result_len); + if (!result) { + ctx->result->has_error = true; + ctx->result->error_msg = + cbm_arena_strdup(ctx->arena, "C return-type metadata allocation failed"); + return cbm_node_text(ctx->arena, type_node, ctx->source); + } + + size_t pos = 0; + for (uint32_t i = 0; i < child_count; i++) { + TSNode child = ts_node_named_child(func_node, i); + if (strcmp(ts_node_type(child), "type_qualifier") != 0 || + ts_node_end_byte(child) > declarator_start) { + continue; + } + uint32_t start = ts_node_start_byte(child); + uint32_t end = ts_node_end_byte(child); + if (end <= start) { + continue; + } + size_t len = (size_t)(end - start); + memcpy(result + pos, ctx->source + start, len); + pos += len; + result[pos++] = ' '; + } + memcpy(result + pos, ctx->source + base_start, base_len); + pos += base_len; + if (pointer_count > 0) { + result[pos++] = ' '; + memset(result + pos, '*', pointer_count); + pos += pointer_count; + } + result[pos] = '\0'; + return result; +} + // C++: resolve trailing return type (auto f() -> Type) on a declarator node. // Updates def->return_type and def->return_types if trailing type found. static void resolve_cpp_trailing_return(CBMArena *a, TSNode func_node, const char *source, @@ -3304,7 +3420,7 @@ static void extract_func_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec for (const char **f = rt_fields; *f; f++) { TSNode rt = ts_node_child_by_field_name(func_node, *f, (uint32_t)strlen(*f)); if (!ts_node_is_null(rt)) { - def.return_type = cbm_node_text(a, rt, ctx->source); + def.return_type = c_declarator_return_type_text(ctx, func_node, rt); def.return_types = extract_return_types(ctx, rt); break; } @@ -4311,7 +4427,7 @@ static void push_method_def(CBMExtractCtx *ctx, TSNode child, TSNode class_node, for (const char **f = rt_fields; *f; f++) { TSNode rt = ts_node_child_by_field_name(child, *f, (uint32_t)strlen(*f)); if (!ts_node_is_null(rt)) { - def.return_type = cbm_node_text(a, rt, ctx->source); + def.return_type = c_declarator_return_type_text(ctx, child, rt); break; } } diff --git a/src/mcp/mcp.c b/src/mcp/mcp.c index ba5a0aa6f..24d0fc852 100644 --- a/src/mcp/mcp.c +++ b/src/mcp/mcp.c @@ -13540,6 +13540,7 @@ static char *build_snippet_response(cbm_mcp_server_t *srv, cbm_node_t *node, int end = node->end_line > start ? node->end_line : start + SNIPPET_DEFAULT_LINES; int total_lines = end - start + 1; bool truncated = false; + bool signature_mode = mode && strcmp(mode, "signature") == 0; char *source = NULL; char *source_tail = NULL; @@ -13555,8 +13556,8 @@ static char *build_snippet_response(cbm_mcp_server_t *srv, cbm_node_t *node, bool path_ok = path_len >= 0 && (size_t)path_len < apsz && cbm_path_within_root(root_path, abs_path); if (path_ok) { - if (mode && strcmp(mode, "signature") == 0) { - truncated = true; + if (signature_mode) { + /* Source omission is the requested representation, not truncation. */ } else if (mode && strcmp(mode, "head_tail") == 0 && max_lines > 0 && total_lines > max_lines) { int head_count = (max_lines * CBM_SNIPPET_HEAD_PERCENT) / @@ -13600,7 +13601,7 @@ static char *build_snippet_response(cbm_mcp_server_t *srv, cbm_node_t *node, yyjson_mut_obj_add_int(doc, root_obj, "start_line", start); yyjson_mut_obj_add_int(doc, root_obj, "end_line", end); - if (mode && strcmp(mode, "signature") == 0) { + if (signature_mode) { /* Signature mode: source omitted; signature comes from properties below */ } else if (mode && strcmp(mode, "head_tail") == 0 && source && source_tail) { /* Combine head + marker + tail */ @@ -13641,6 +13642,10 @@ static char *build_snippet_response(cbm_mcp_server_t *srv, cbm_node_t *node, yyjson_mut_obj_add_bool(doc, root_obj, "source_clipped", true); yyjson_mut_obj_add_int(doc, root_obj, "clipped_at_lines", max_lines); yyjson_mut_obj_add_int(doc, root_obj, "total_lines", total_lines); + } else if (signature_mode) { + /* Preserve useful size context without claiming requested source was + * clipped or suggesting max_lines=0 is needed to repair the result. */ + yyjson_mut_obj_add_int(doc, root_obj, "total_lines", total_lines); } /* match_method — omitted for exact matches */ @@ -13652,7 +13657,7 @@ static char *build_snippet_response(cbm_mcp_server_t *srv, cbm_node_t *node, * props_doc is freed AFTER serialization since yyjson_mut_obj_add_str * stores pointers into it (zero-copy). */ yyjson_doc *props_doc = NULL; - bool include_properties = !compact || (mode && strcmp(mode, "signature") == 0); + bool include_properties = !compact || signature_mode; if (include_properties && node->properties_json && node->properties_json[0] != '\0') { props_doc = yyjson_read(node->properties_json, strlen(node->properties_json), 0); if (props_doc) { diff --git a/tests/test_extraction.c b/tests/test_extraction.c index 1448d9b63..abbe8cbcc 100644 --- a/tests/test_extraction.c +++ b/tests/test_extraction.c @@ -908,6 +908,40 @@ TEST(c_function) { PASS(); } +TEST(c_function_return_type_preserves_pointer_and_qualifier) { + CBMFileResult *r = + extract("static const char *text_end(const char *text) { return text; }\n" + "char **table(void) { return 0; }\n" + "int scalar(void) { return 0; }\n", + CBM_LANG_C, "t", "returns.c"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + + const CBMDefinition *text_end = NULL; + const CBMDefinition *table = NULL; + const CBMDefinition *scalar = NULL; + for (int i = 0; i < r->defs.count; i++) { + const CBMDefinition *def = &r->defs.items[i]; + if (strcmp(def->name, "text_end") == 0) { + text_end = def; + } else if (strcmp(def->name, "table") == 0) { + table = def; + } else if (strcmp(def->name, "scalar") == 0) { + scalar = def; + } + } + + ASSERT_NOT_NULL(text_end); + ASSERT_NOT_NULL(table); + ASSERT_NOT_NULL(scalar); + ASSERT_STR_EQ(text_end->return_type, "const char *"); + ASSERT_STR_EQ(table->return_type, "char **"); + ASSERT_STR_EQ(scalar->return_type, "int"); + + cbm_free_result(r); + PASS(); +} + TEST(c_struct) { CBMFileResult *r = extract("struct Point { int x; int y; };\nvoid init_point(struct Point *p) { p->x = 0; }\n", @@ -932,6 +966,36 @@ TEST(cpp_class) { PASS(); } +TEST(cpp_method_return_type_preserves_pointer_and_qualifier) { + CBMFileResult *r = + extract("class Text {\n" + "public:\n" + " const char *end() { return nullptr; }\n" + " char **table() { return nullptr; }\n" + "};\n", + CBM_LANG_CPP, "t", "text.cpp"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + + const CBMDefinition *end = NULL; + const CBMDefinition *table = NULL; + for (int i = 0; i < r->defs.count; i++) { + const CBMDefinition *def = &r->defs.items[i]; + if (strcmp(def->name, "end") == 0) { + end = def; + } else if (strcmp(def->name, "table") == 0) { + table = def; + } + } + ASSERT_NOT_NULL(end); + ASSERT_NOT_NULL(table); + ASSERT_STR_EQ(end->return_type, "const char *"); + ASSERT_STR_EQ(table->return_type, "char **"); + + cbm_free_result(r); + PASS(); +} + /* ═══════════════════════════════════════════════════════════════════ * Group C: Scripting / Dynamic Languages * ═══════════════════════════════════════════════════════════════════ */ @@ -5643,8 +5707,10 @@ SUITE(extraction) { RUN_TEST(dlang_struct); RUN_TEST(zig_function); RUN_TEST(c_function); + RUN_TEST(c_function_return_type_preserves_pointer_and_qualifier); RUN_TEST(c_struct); RUN_TEST(cpp_class); + RUN_TEST(cpp_method_return_type_preserves_pointer_and_qualifier); /* Scripting */ RUN_TEST(python_function); diff --git a/tests/test_token_reduction.c b/tests/test_token_reduction.c index bcb77de11..913cdf093 100644 --- a/tests/test_token_reduction.c +++ b/tests/test_token_reduction.c @@ -387,8 +387,12 @@ TEST(snippet_signature_mode) { ASSERT_NOT_NULL(strstr(resp, "def large_function(arg1, arg2, arg3)")); /* Should NOT contain full source body */ ASSERT_NULL(strstr(resp, "step_050")); - /* Should indicate total size */ - ASSERT_NOT_NULL(strstr(resp, "\"total_lines\"")); + /* Signature is a complete requested representation, not clipped source. + * Retain size context without telling callers to repair a non-problem. */ + ASSERT_NOT_NULL(strstr(resp, "\"total_lines\":300")); + ASSERT_NULL(strstr(resp, "\"truncated\":true")); + ASSERT_NULL(strstr(resp, "\"source_clipped\":true")); + ASSERT_NULL(strstr(resp, "\"clipped_at_lines\"")); free(resp); cbm_mcp_server_free(srv);