mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 18:58:00 +00:00
Merge pull request #2274 from DeusData/distill/1245-c-pointer-return-types
fix(extract): a C-family function's return type keeps its pointer and qualifiers
This commit is contained in:
2 files changed
+253
-2
No files matched your search
+155
-2
@@ -3477,6 +3477,159 @@ static TSNode find_function_params(TSNode func_node, CBMLanguage lang) {
|
||||
return params;
|
||||
}
|
||||
|
||||
/* ── C-family declared return type ──────────────────────────────────
|
||||
* The C-family grammars split a declared return type three ways: the `type`
|
||||
* field (`char`), sibling type_qualifier nodes (`const`), and the
|
||||
* pointer/reference declarators wrapping the function declarator (`*`). Taking
|
||||
* only the `type` field published `const char *get_name(void)` as "char".
|
||||
*
|
||||
* Canonical spelling: leading cv-qualifiers in source order, the base type text
|
||||
* verbatim, then one space and the declarator markers outermost-first with no
|
||||
* space between them — `const char *`, `char **`, `Text &`, `Text *&`. A
|
||||
* qualifier on a pointer level follows its `*` and is separated from the next
|
||||
* marker by a space: `char *const *`. A qualifier written after the base type
|
||||
* (`char const *`) is normalized to the leading position. */
|
||||
|
||||
/* Output sink: measures when buf is NULL, writes otherwise. Rendering twice
|
||||
* sizes the arena allocation exactly without a second copy of the logic. */
|
||||
typedef struct {
|
||||
char *buf;
|
||||
size_t len;
|
||||
} c_rt_out_t;
|
||||
|
||||
static void c_rt_put(c_rt_out_t *out, const char *text, size_t n) {
|
||||
if (out->buf) {
|
||||
memcpy(out->buf + out->len, text, n);
|
||||
}
|
||||
out->len += n;
|
||||
}
|
||||
|
||||
static void c_rt_put_str(c_rt_out_t *out, const char *text) {
|
||||
c_rt_put(out, text, strlen(text));
|
||||
}
|
||||
|
||||
static void c_rt_put_node(c_rt_out_t *out, TSNode node, const char *source) {
|
||||
uint32_t start = ts_node_start_byte(node);
|
||||
uint32_t end = ts_node_end_byte(node);
|
||||
if (end > start) {
|
||||
c_rt_put(out, source + start, (size_t)(end - start));
|
||||
}
|
||||
}
|
||||
|
||||
/* type_qualifier also covers keywords that are not part of the type
|
||||
* (`constexpr`, `_Noreturn`, `mutable`, `__extension__`, …). Keep only the ones
|
||||
* that are, so `constexpr int f()` still returns "int". */
|
||||
static bool is_c_return_cv_qualifier(TSNode node, const char *source) {
|
||||
static const char *const kept[] = {"const", "volatile", "restrict", "__restrict",
|
||||
"__restrict__", "_Atomic", NULL};
|
||||
if (strcmp(ts_node_type(node), "type_qualifier") != 0) {
|
||||
return false;
|
||||
}
|
||||
uint32_t start = ts_node_start_byte(node);
|
||||
uint32_t end = ts_node_end_byte(node);
|
||||
size_t len = end > start ? (size_t)(end - start) : 0;
|
||||
for (const char *const *k = kept; *k; k++) {
|
||||
if (strlen(*k) == len && memcmp(source + start, *k, len) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool is_c_declarator_lang(CBMLanguage lang) {
|
||||
return lang == CBM_LANG_C || lang == CBM_LANG_CPP || lang == CBM_LANG_CUDA ||
|
||||
lang == CBM_LANG_GLSL || lang == CBM_LANG_HLSL || lang == CBM_LANG_ISPC ||
|
||||
lang == CBM_LANG_SLANG || lang == CBM_LANG_OBJC;
|
||||
}
|
||||
|
||||
/* Render the canonical return type into `out`; returns how many qualifiers and
|
||||
* markers were added around the base type (0 = the base text alone is already
|
||||
* the whole type). The declarator walk is one strict child chain, so it is
|
||||
* O(depth) with no recursion and needs no depth cap. It stops at the first node
|
||||
* that is neither a pointer nor a reference declarator: for a function returning
|
||||
* a function pointer (`int (*f(void))(int)`) that is the outer
|
||||
* function_declarator, which leaves the base type as it was. */
|
||||
static size_t c_rt_render(c_rt_out_t *out, TSNode func_node, TSNode type_node, TSNode declarator,
|
||||
const char *source) {
|
||||
size_t added = 0;
|
||||
uint32_t decl_start = ts_node_start_byte(declarator);
|
||||
uint32_t nc = ts_node_named_child_count(func_node);
|
||||
for (uint32_t i = 0; i < nc; i++) {
|
||||
TSNode ch = ts_node_named_child(func_node, i);
|
||||
if (ts_node_start_byte(ch) >= decl_start) {
|
||||
break;
|
||||
}
|
||||
if (is_c_return_cv_qualifier(ch, source)) {
|
||||
c_rt_put_node(out, ch, source);
|
||||
c_rt_put_str(out, " ");
|
||||
added++;
|
||||
}
|
||||
}
|
||||
c_rt_put_node(out, type_node, source);
|
||||
|
||||
bool need_space = true;
|
||||
TSNode decl = declarator;
|
||||
while (!ts_node_is_null(decl)) {
|
||||
const char *dk = ts_node_type(decl);
|
||||
bool is_ref = strcmp(dk, "reference_declarator") == 0;
|
||||
if (!is_ref && strcmp(dk, "pointer_declarator") != 0) {
|
||||
break;
|
||||
}
|
||||
if (need_space) {
|
||||
c_rt_put_str(out, " ");
|
||||
need_space = false;
|
||||
}
|
||||
/* A reference_declarator opens with its `&` / `&&` token. */
|
||||
TSNode marker = ts_node_child(decl, 0);
|
||||
if (is_ref && !ts_node_is_null(marker) && !ts_node_is_named(marker)) {
|
||||
c_rt_put_node(out, marker, source);
|
||||
} else {
|
||||
c_rt_put_str(out, is_ref ? "&" : "*");
|
||||
}
|
||||
added++;
|
||||
uint32_t dn = ts_node_named_child_count(decl);
|
||||
for (uint32_t i = 0; i < dn; i++) {
|
||||
TSNode q = ts_node_named_child(decl, i);
|
||||
if (is_c_return_cv_qualifier(q, source)) {
|
||||
c_rt_put_node(out, q, source);
|
||||
need_space = true;
|
||||
added++;
|
||||
}
|
||||
}
|
||||
/* tree-sitter-cpp/-cuda give a reference_declarator's inner declarator no
|
||||
* `declarator` field (see find_c_params); it is the one named child. */
|
||||
TSNode inner = ts_node_child_by_field_name(decl, TS_FIELD("declarator"));
|
||||
if (ts_node_is_null(inner) && is_ref && dn > 0) {
|
||||
inner = ts_node_named_child(decl, 0);
|
||||
}
|
||||
decl = inner;
|
||||
}
|
||||
return added;
|
||||
}
|
||||
|
||||
/* Declared return type of a C-family function/method node whose `type` field is
|
||||
* `type_node`. Any other language, and any type with nothing around its base
|
||||
* type, gets the base type text exactly as before. */
|
||||
static char *c_declared_return_type(CBMExtractCtx *ctx, TSNode func_node, TSNode type_node) {
|
||||
CBMArena *a = ctx->arena;
|
||||
TSNode declarator = ts_node_child_by_field_name(func_node, TS_FIELD("declarator"));
|
||||
if (!is_c_declarator_lang(ctx->language) || ts_node_is_null(declarator)) {
|
||||
return cbm_node_text(a, type_node, ctx->source);
|
||||
}
|
||||
c_rt_out_t out = {NULL, 0};
|
||||
if (c_rt_render(&out, func_node, type_node, declarator, ctx->source) == 0) {
|
||||
return cbm_node_text(a, type_node, ctx->source);
|
||||
}
|
||||
out.buf = (char *)cbm_arena_alloc(a, out.len + NULL_TERM);
|
||||
if (!out.buf) {
|
||||
return cbm_node_text(a, type_node, ctx->source);
|
||||
}
|
||||
out.len = 0;
|
||||
(void)c_rt_render(&out, func_node, type_node, declarator, ctx->source);
|
||||
out.buf[out.len] = '\0';
|
||||
return out.buf;
|
||||
}
|
||||
|
||||
// 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,
|
||||
@@ -3712,7 +3865,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_declared_return_type(ctx, func_node, rt);
|
||||
def.return_types = extract_return_types(a, rt, ctx->source, ctx->language);
|
||||
break;
|
||||
}
|
||||
@@ -4964,7 +5117,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_declared_return_type(ctx, child, rt);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -977,6 +977,65 @@ TEST(c_struct) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* return_type of the first definition named `name`; NULL when there is no such
|
||||
* definition or it carries no return type. */
|
||||
static const char *def_return_type(CBMFileResult *r, const char *name) {
|
||||
for (int i = 0; i < r->defs.count; i++) {
|
||||
if (strcmp(r->defs.items[i].name, name) == 0) {
|
||||
return r->defs.items[i].return_type;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* PR #1245: the C grammar splits a declared return type across the `type` node,
|
||||
* sibling type_qualifier nodes and the pointer_declarator chain wrapping the
|
||||
* function declarator. Taking only the `type` node published `const char *` as
|
||||
* "char". Canonical spelling: qualifiers, base type, one space, then the
|
||||
* declarator markers unspaced (`const char *`, `char **`). */
|
||||
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"
|
||||
"char const *east_const(void) { return 0; }\n"
|
||||
"const struct Point *find_point(void) { return 0; }\n"
|
||||
"volatile unsigned long *reg(void) { return 0; }\n"
|
||||
"char *const *frozen(void) { return 0; }\n",
|
||||
CBM_LANG_C, "t", "returns.c");
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_FALSE(r->has_error);
|
||||
ASSERT_STR_EQ(def_return_type(r, "text_end"), "const char *");
|
||||
ASSERT_STR_EQ(def_return_type(r, "table"), "char **");
|
||||
ASSERT_STR_EQ(def_return_type(r, "east_const"), "const char *");
|
||||
ASSERT_STR_EQ(def_return_type(r, "find_point"), "const struct Point *");
|
||||
ASSERT_STR_EQ(def_return_type(r, "reg"), "volatile unsigned long *");
|
||||
ASSERT_STR_EQ(def_return_type(r, "frozen"), "char *const *");
|
||||
cbm_free_result(r);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Guard for the fix above: a return type with no qualifier and no declarator
|
||||
* marker is already correct and must come out byte-identical. */
|
||||
TEST(c_function_return_type_plain_unchanged) {
|
||||
CBMFileResult *r = extract("int scalar(void) { return 0; }\n"
|
||||
"void nothing(void) {}\n"
|
||||
"unsigned long wide(void) { return 0; }\n"
|
||||
"struct Point make_point(void) { struct Point p; return p; }\n"
|
||||
"static inline size_t count(void) { return 0; }\n"
|
||||
"_Noreturn void die(void) { for (;;) {} }\n",
|
||||
CBM_LANG_C, "t", "plain.c");
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_FALSE(r->has_error);
|
||||
ASSERT_STR_EQ(def_return_type(r, "scalar"), "int");
|
||||
ASSERT_STR_EQ(def_return_type(r, "nothing"), "void");
|
||||
ASSERT_STR_EQ(def_return_type(r, "wide"), "unsigned long");
|
||||
ASSERT_STR_EQ(def_return_type(r, "make_point"), "struct Point");
|
||||
ASSERT_STR_EQ(def_return_type(r, "count"), "size_t");
|
||||
/* _Noreturn parses as a type_qualifier but is not part of the type. */
|
||||
ASSERT_STR_EQ(def_return_type(r, "die"), "void");
|
||||
cbm_free_result(r);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* --- C++ --- */
|
||||
TEST(cpp_class) {
|
||||
CBMFileResult *r = extract(
|
||||
@@ -990,6 +1049,42 @@ TEST(cpp_class) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* PR #1245, C++ side: in-class methods go through a separate extraction path
|
||||
* from free functions, and C++ adds reference markers (`&`, `&&`) whose
|
||||
* reference_declarator carries no `declarator` field. */
|
||||
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"
|
||||
" Text &self() { return *this; }\n"
|
||||
" const Text &cself() const { return *this; }\n"
|
||||
" Text *&slot() { return next_; }\n"
|
||||
" Text *next_;\n"
|
||||
" int width() const { return 0; }\n"
|
||||
" constexpr int square(int x) const { return x * x; }\n"
|
||||
"};\n"
|
||||
"const Text &shared() { static Text t; return t; }\n"
|
||||
"Text &&moved(Text &t) { return static_cast<Text &&>(t); }\n"
|
||||
"const char *Text::c_str() const { return nullptr; }\n",
|
||||
CBM_LANG_CPP, "t", "text.cpp");
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_FALSE(r->has_error);
|
||||
ASSERT_STR_EQ(def_return_type(r, "end"), "const char *");
|
||||
ASSERT_STR_EQ(def_return_type(r, "table"), "char **");
|
||||
ASSERT_STR_EQ(def_return_type(r, "self"), "Text &");
|
||||
ASSERT_STR_EQ(def_return_type(r, "cself"), "const Text &");
|
||||
ASSERT_STR_EQ(def_return_type(r, "slot"), "Text *&");
|
||||
ASSERT_STR_EQ(def_return_type(r, "width"), "int");
|
||||
/* constexpr parses as a type_qualifier but is not part of the type. */
|
||||
ASSERT_STR_EQ(def_return_type(r, "square"), "int");
|
||||
ASSERT_STR_EQ(def_return_type(r, "shared"), "const Text &");
|
||||
ASSERT_STR_EQ(def_return_type(r, "moved"), "Text &&");
|
||||
ASSERT_STR_EQ(def_return_type(r, "c_str"), "const char *");
|
||||
cbm_free_result(r);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
* Group C: Scripting / Dynamic Languages
|
||||
* ═══════════════════════════════════════════════════════════════════ */
|
||||
@@ -8271,8 +8366,11 @@ SUITE(extraction) {
|
||||
RUN_TEST(go_interface);
|
||||
RUN_TEST(zig_function);
|
||||
RUN_TEST(c_function);
|
||||
RUN_TEST(c_function_return_type_preserves_pointer_and_qualifier);
|
||||
RUN_TEST(c_function_return_type_plain_unchanged);
|
||||
RUN_TEST(c_struct);
|
||||
RUN_TEST(cpp_class);
|
||||
RUN_TEST(cpp_method_return_type_preserves_pointer_and_qualifier);
|
||||
|
||||
/* Scripting */
|
||||
RUN_TEST(python_function);
|
||||
|
||||
Reference in new issue
Block a user