Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 155 additions & 2 deletions internal/cbm/extract_defs.c
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}
}
Expand Down
98 changes: 98 additions & 0 deletions tests/test_extraction.c
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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
* ═══════════════════════════════════════════════════════════════════ */
Expand Down Expand Up @@ -8206,8 +8301,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);
Expand Down
Loading