Skip to content
Merged
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
98 changes: 98 additions & 0 deletions internal/cbm/cbm.c
Original file line number Diff line number Diff line change
Expand Up @@ -956,6 +956,101 @@ static void cbm_subtract_recovered_regions(cbm_error_regions_t *regs, const CBMD
regs->count = kept;
}

/* #1071: a function-like macro invocation whose argument is a type token
* (e.g. ALLOC(int, n)) makes tree-sitter's C/C++ grammar emit an ERROR node — it
* parses `int` in expression position — which would be recorded as a parse_partial
* coverage gap. But the macro is #defined in THIS file, so nothing is actually
* missing from the graph; it's a benign call the grammar can't parse without the
* preprocessor. True if the [start_line, end_line] span contains a call `NAME(` to
* a file-defined function-like macro (Macro label + a parameter signature). */
static bool cbm_span_is_macro_invocation(const char *src, int src_len, uint32_t start_line,
uint32_t end_line, const CBMDefArray *defs) {
if (!src || src_len <= 0 || !defs || start_line == 0 || end_line < start_line) {
return false;
}
int span_start = 0;
uint32_t line = 1;
while (span_start < src_len && line < start_line) {
if (src[span_start++] == '\n') {
line++;
}
}
if (line != start_line) {
return false;
}
int span_end = span_start;
while (span_end < src_len && line <= end_line) {
if (src[span_end++] == '\n') {
line++;
}
}
for (int di = 0; di < defs->count; di++) {
const CBMDefinition *d = &defs->items[di];
/* Function-like macros only: an object-like macro (#define PI 3.14) has no
* parameter signature and can't be mistaken for a call. */
if (!d->label || strcmp(d->label, "Macro") != 0 || !d->signature || !d->name ||
!d->name[0]) {
continue;
}
int nlen = (int)strlen(d->name);
for (int pos = span_start; pos + nlen <= span_end; pos++) {
if (strncmp(src + pos, d->name, (size_t)nlen) != 0 ||
(pos > 0 && cbm_identifier_char(src[pos - 1])) ||
(pos + nlen < src_len && cbm_identifier_char(src[pos + nlen]))) {
continue;
}
int open = pos + nlen;
while (open < span_end && isspace((unsigned char)src[open])) {
open++;
}
if (open < span_end && src[open] == '(') {
return true; /* NAME( ... ) — an invocation of this file's macro */
}
}
}
return false;
}

/* True if [rs, re] is fully enclosed by an extracted callable definition (a
* Function/Method body). A macro invocation INSIDE a real function body is an
* expression-level use where nothing is missing (#1071). A TOP-LEVEL invocation
* is different: the macro may itself expand to a definition that the original
* span doesn't contain (#949), which must stay flagged. Restricting the #1071
* suppression to in-body calls keeps that #949 gap honest and fails safe. */
static bool cbm_region_inside_callable(uint32_t rs, uint32_t re, const CBMDefArray *defs) {
for (int i = 0; i < defs->count; i++) {
const CBMDefinition *d = &defs->items[i];
if (!d->label) {
continue;
}
if (strcmp(d->label, "Function") != 0 && strcmp(d->label, "Method") != 0 &&
strcmp(d->label, "Constructor") != 0 && strcmp(d->label, "Destructor") != 0) {
continue;
}
if (d->start_line <= rs && d->end_line >= re && d->end_line > d->start_line) {
return true;
}
}
return false;
}

static void cbm_subtract_macro_invocation_regions(cbm_error_regions_t *regs,
const CBMDefArray *defs, const char *src,
int src_len) {
int kept = 0;
for (int i = 0; i < regs->count; i++) {
bool benign =
cbm_span_is_macro_invocation(src, src_len, regs->starts[i], regs->ends[i], defs) &&
cbm_region_inside_callable(regs->starts[i], regs->ends[i], defs);
if (!benign) {
regs->starts[kept] = regs->starts[i];
regs->ends[kept] = regs->ends[i];
kept++;
}
}
regs->count = kept;
}

/* Serialize collected regions as "start-end,start-end,..." into the arena. */
static const char *cbm_error_ranges_str(CBMArena *a, const cbm_error_regions_t *regs) {
if (regs->count <= 0) {
Expand Down Expand Up @@ -1410,6 +1505,9 @@ CBMFileResult *cbm_extract_file_ex(const char *source, int source_len, CBMLangua
cbm_collect_error_regions(root, &regs);
}
cbm_subtract_recovered_regions(&regs, &result->defs);
/* #1071: don't flag a benign function-like-macro call (defined in-file)
* that tree-sitter can't parse without the preprocessor. */
cbm_subtract_macro_invocation_regions(&regs, &result->defs, source, source_len);
if (regs.count > 0) {
result->parse_incomplete = true;
result->error_region_count = regs.count;
Expand Down
51 changes: 51 additions & 0 deletions tests/test_extraction.c
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,55 @@ TEST(extract_cpp_macros_issue375) {
PASS();
}

/* #1071: a function-like macro invocation whose argument is a TYPE token
* (SYNTH_ALLOC_ARRAY(char, n)) makes tree-sitter's C++ grammar emit an ERROR
* node — it parses `char` in expression position — which cbm_collect_error_regions
* records as a `parse_partial` coverage gap, even though the file is a valid,
* in-file macro use with nothing actually missing from the graph. */
TEST(extract_cpp_functionlike_macro_type_arg_no_false_parse_partial_issue1071) {
CBMFileResult *r = extract("#include <cstddef>\n"
"#include <cstdlib>\n"
"\n"
"#define SYNTH_ALLOC_ARRAY(Type, Count) \\\n"
" ((Type*)std::malloc(sizeof(Type) * (Count)))\n"
"\n"
"struct Buffer {\n"
" char* data;\n"
" std::size_t size;\n"
"};\n"
"\n"
"Buffer make_buffer(std::size_t n) {\n"
" Buffer b;\n"
" b.data = SYNTH_ALLOC_ARRAY(char, n);\n"
" b.size = n;\n"
" return b;\n"
"}\n",
CBM_LANG_CPP, "t", "alloc.cpp");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->parse_incomplete); /* benign in-body macro call — not a coverage gap */
ASSERT(has_def(r, "Function", "make_buffer"));
ASSERT(has_def(r, "Macro", "SYNTH_ALLOC_ARRAY"));
cbm_free_result(r);
PASS();
}

/* #1071 guard: the suppression must be tight. A REAL parse error inside a
* function (not a macro call) must STILL be flagged, and a top-level macro
* invocation is covered by extract_cpp_preproc_macro_generated_callable_skipped_issue949. */
TEST(extract_cpp_real_in_body_error_still_flagged_issue1071) {
/* `int x = ;` is a genuine syntax error inside foo()'s body — no macro
* involved, so the coverage gap must not be suppressed. */
CBMFileResult *r = extract("int foo() {\n"
" int x = ;\n"
" return x;\n"
"}\n",
CBM_LANG_CPP, "t", "broken.cpp");
ASSERT_NOT_NULL(r);
ASSERT_TRUE(r->parse_incomplete); /* real gap stays reported */
cbm_free_result(r);
PASS();
}

/* --- GDScript: AST -> graph visitor (Godot, #186) --- */
TEST(extract_gdscript_issue186) {
CBMFileResult *r = extract("extends Node\n"
Expand Down Expand Up @@ -4674,6 +4723,8 @@ SUITE(extraction) {
RUN_TEST(extract_ts_factory_object_methods_issue341);
RUN_TEST(extract_c_macros_issue375);
RUN_TEST(extract_cpp_macros_issue375);
RUN_TEST(extract_cpp_functionlike_macro_type_arg_no_false_parse_partial_issue1071);
RUN_TEST(extract_cpp_real_in_body_error_still_flagged_issue1071);
RUN_TEST(extract_gdscript_issue186);
RUN_TEST(extract_powershell_issue35);
RUN_TEST(extract_luau_issue39);
Expand Down
Loading