fix(extract,mcp): retain pointer returns and exact signature status

Both merge parents be89d496 and 97ce23f stored only the base type node for C/C++ functions and methods, so const char * and char ** were published as char. The feature parent also marked requested get_code signature output as truncated source and emitted repair-oriented clipping metadata.

Add c_declarator_return_type_text at internal/cbm/extract_defs.c:3154 to retain leading qualifiers and every pointer_declarator in O(D) traversal time with O(return-type bytes) arena storage and checked size arithmetic. Route free-function and method return metadata through it. In src/mcp/mcp.c:13543, treat signature mode as a complete requested representation: retain total_lines, omit truncated/source_clipped/clipped_at_lines, and reuse one signature_mode predicate.

Tests require const char *, char **, unchanged scalar int, C++ method parity, total_lines=300, and absence of false clipping fields. Verified extraction 329/329, C LSP 752/752, token_reduction 56/56, MCP 314/314 under ASan/UBSan; test-leak 1194/1194 with 0 leaks; explicit four-file test-syntax and source safety pass. Clang analyzer reports no diagnostics in changed production ranges.

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
This commit is contained in:
Andrew Hundt
2026-07-26 23:45:54 -04:00
parent 111d4267ec
commit c51e5cb01e
4 changed files with 199 additions and 8 deletions
+118 -2
View File
@@ -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;
}
}
+9 -4
View File
@@ -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) {
+66
View File
@@ -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);
+6 -2
View File
@@ -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);