Skip to content

Commit a75f4b6

Browse files
committed
fix(extract): don't flag in-body function-like macro calls as parse gaps
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 the type in expression position), which cbm_collect_error_regions recorded as a parse_partial coverage gap. But the macro is #defined in the same file and the call sits inside an already-extracted function body, so nothing is actually missing from the graph — it's a benign call the grammar can't parse without the preprocessor (DeusData#1071, systematic across allocation-macro-style codebases). Subtract an error region only when it both (a) contains an invocation of a file-defined function-like macro and (b) is fully enclosed by an extracted Function/Method body. Condition (b) keeps a TOP-LEVEL macro invocation that expands to a definition still flagged (DeusData#949: the generated def isn't in the original span), and fails safe — a benign top-level call stays flagged rather than a real gap being hidden. Tests: a type-arg macro call inside a function no longer reports parse_partial; a real in-body syntax error still does; DeusData#949/DeusData#946 preserved. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
1 parent a688d85 commit a75f4b6

2 files changed

Lines changed: 149 additions & 0 deletions

File tree

‎internal/cbm/cbm.c‎

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -956,6 +956,101 @@ static void cbm_subtract_recovered_regions(cbm_error_regions_t *regs, const CBMD
956956
regs->count = kept;
957957
}
958958

959+
/* #1071: a function-like macro invocation whose argument is a type token
960+
* (e.g. ALLOC(int, n)) makes tree-sitter's C/C++ grammar emit an ERROR node — it
961+
* parses `int` in expression position — which would be recorded as a parse_partial
962+
* coverage gap. But the macro is #defined in THIS file, so nothing is actually
963+
* missing from the graph; it's a benign call the grammar can't parse without the
964+
* preprocessor. True if the [start_line, end_line] span contains a call `NAME(` to
965+
* a file-defined function-like macro (Macro label + a parameter signature). */
966+
static bool cbm_span_is_macro_invocation(const char *src, int src_len, uint32_t start_line,
967+
uint32_t end_line, const CBMDefArray *defs) {
968+
if (!src || src_len <= 0 || !defs || start_line == 0 || end_line < start_line) {
969+
return false;
970+
}
971+
int span_start = 0;
972+
uint32_t line = 1;
973+
while (span_start < src_len && line < start_line) {
974+
if (src[span_start++] == '\n') {
975+
line++;
976+
}
977+
}
978+
if (line != start_line) {
979+
return false;
980+
}
981+
int span_end = span_start;
982+
while (span_end < src_len && line <= end_line) {
983+
if (src[span_end++] == '\n') {
984+
line++;
985+
}
986+
}
987+
for (int di = 0; di < defs->count; di++) {
988+
const CBMDefinition *d = &defs->items[di];
989+
/* Function-like macros only: an object-like macro (#define PI 3.14) has no
990+
* parameter signature and can't be mistaken for a call. */
991+
if (!d->label || strcmp(d->label, "Macro") != 0 || !d->signature || !d->name ||
992+
!d->name[0]) {
993+
continue;
994+
}
995+
int nlen = (int)strlen(d->name);
996+
for (int pos = span_start; pos + nlen <= span_end; pos++) {
997+
if (strncmp(src + pos, d->name, (size_t)nlen) != 0 ||
998+
(pos > 0 && cbm_identifier_char(src[pos - 1])) ||
999+
(pos + nlen < src_len && cbm_identifier_char(src[pos + nlen]))) {
1000+
continue;
1001+
}
1002+
int open = pos + nlen;
1003+
while (open < span_end && isspace((unsigned char)src[open])) {
1004+
open++;
1005+
}
1006+
if (open < span_end && src[open] == '(') {
1007+
return true; /* NAME( ... ) — an invocation of this file's macro */
1008+
}
1009+
}
1010+
}
1011+
return false;
1012+
}
1013+
1014+
/* True if [rs, re] is fully enclosed by an extracted callable definition (a
1015+
* Function/Method body). A macro invocation INSIDE a real function body is an
1016+
* expression-level use where nothing is missing (#1071). A TOP-LEVEL invocation
1017+
* is different: the macro may itself expand to a definition that the original
1018+
* span doesn't contain (#949), which must stay flagged. Restricting the #1071
1019+
* suppression to in-body calls keeps that #949 gap honest and fails safe. */
1020+
static bool cbm_region_inside_callable(uint32_t rs, uint32_t re, const CBMDefArray *defs) {
1021+
for (int i = 0; i < defs->count; i++) {
1022+
const CBMDefinition *d = &defs->items[i];
1023+
if (!d->label) {
1024+
continue;
1025+
}
1026+
if (strcmp(d->label, "Function") != 0 && strcmp(d->label, "Method") != 0 &&
1027+
strcmp(d->label, "Constructor") != 0 && strcmp(d->label, "Destructor") != 0) {
1028+
continue;
1029+
}
1030+
if (d->start_line <= rs && d->end_line >= re && d->end_line > d->start_line) {
1031+
return true;
1032+
}
1033+
}
1034+
return false;
1035+
}
1036+
1037+
static void cbm_subtract_macro_invocation_regions(cbm_error_regions_t *regs,
1038+
const CBMDefArray *defs, const char *src,
1039+
int src_len) {
1040+
int kept = 0;
1041+
for (int i = 0; i < regs->count; i++) {
1042+
bool benign =
1043+
cbm_span_is_macro_invocation(src, src_len, regs->starts[i], regs->ends[i], defs) &&
1044+
cbm_region_inside_callable(regs->starts[i], regs->ends[i], defs);
1045+
if (!benign) {
1046+
regs->starts[kept] = regs->starts[i];
1047+
regs->ends[kept] = regs->ends[i];
1048+
kept++;
1049+
}
1050+
}
1051+
regs->count = kept;
1052+
}
1053+
9591054
/* Serialize collected regions as "start-end,start-end,..." into the arena. */
9601055
static const char *cbm_error_ranges_str(CBMArena *a, const cbm_error_regions_t *regs) {
9611056
if (regs->count <= 0) {
@@ -1410,6 +1505,9 @@ CBMFileResult *cbm_extract_file_ex(const char *source, int source_len, CBMLangua
14101505
cbm_collect_error_regions(root, &regs);
14111506
}
14121507
cbm_subtract_recovered_regions(&regs, &result->defs);
1508+
/* #1071: don't flag a benign function-like-macro call (defined in-file)
1509+
* that tree-sitter can't parse without the preprocessor. */
1510+
cbm_subtract_macro_invocation_regions(&regs, &result->defs, source, source_len);
14131511
if (regs.count > 0) {
14141512
result->parse_incomplete = true;
14151513
result->error_region_count = regs.count;

‎tests/test_extraction.c‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,55 @@ TEST(extract_cpp_macros_issue375) {
165165
PASS();
166166
}
167167

168+
/* #1071: a function-like macro invocation whose argument is a TYPE token
169+
* (SYNTH_ALLOC_ARRAY(char, n)) makes tree-sitter's C++ grammar emit an ERROR
170+
* node — it parses `char` in expression position — which cbm_collect_error_regions
171+
* records as a `parse_partial` coverage gap, even though the file is a valid,
172+
* in-file macro use with nothing actually missing from the graph. */
173+
TEST(extract_cpp_functionlike_macro_type_arg_no_false_parse_partial_issue1071) {
174+
CBMFileResult *r = extract("#include <cstddef>\n"
175+
"#include <cstdlib>\n"
176+
"\n"
177+
"#define SYNTH_ALLOC_ARRAY(Type, Count) \\\n"
178+
" ((Type*)std::malloc(sizeof(Type) * (Count)))\n"
179+
"\n"
180+
"struct Buffer {\n"
181+
" char* data;\n"
182+
" std::size_t size;\n"
183+
"};\n"
184+
"\n"
185+
"Buffer make_buffer(std::size_t n) {\n"
186+
" Buffer b;\n"
187+
" b.data = SYNTH_ALLOC_ARRAY(char, n);\n"
188+
" b.size = n;\n"
189+
" return b;\n"
190+
"}\n",
191+
CBM_LANG_CPP, "t", "alloc.cpp");
192+
ASSERT_NOT_NULL(r);
193+
ASSERT_FALSE(r->parse_incomplete); /* benign in-body macro call — not a coverage gap */
194+
ASSERT(has_def(r, "Function", "make_buffer"));
195+
ASSERT(has_def(r, "Macro", "SYNTH_ALLOC_ARRAY"));
196+
cbm_free_result(r);
197+
PASS();
198+
}
199+
200+
/* #1071 guard: the suppression must be tight. A REAL parse error inside a
201+
* function (not a macro call) must STILL be flagged, and a top-level macro
202+
* invocation is covered by extract_cpp_preproc_macro_generated_callable_skipped_issue949. */
203+
TEST(extract_cpp_real_in_body_error_still_flagged_issue1071) {
204+
/* `int x = ;` is a genuine syntax error inside foo()'s body — no macro
205+
* involved, so the coverage gap must not be suppressed. */
206+
CBMFileResult *r = extract("int foo() {\n"
207+
" int x = ;\n"
208+
" return x;\n"
209+
"}\n",
210+
CBM_LANG_CPP, "t", "broken.cpp");
211+
ASSERT_NOT_NULL(r);
212+
ASSERT_TRUE(r->parse_incomplete); /* real gap stays reported */
213+
cbm_free_result(r);
214+
PASS();
215+
}
216+
168217
/* --- GDScript: AST -> graph visitor (Godot, #186) --- */
169218
TEST(extract_gdscript_issue186) {
170219
CBMFileResult *r = extract("extends Node\n"
@@ -4674,6 +4723,8 @@ SUITE(extraction) {
46744723
RUN_TEST(extract_ts_factory_object_methods_issue341);
46754724
RUN_TEST(extract_c_macros_issue375);
46764725
RUN_TEST(extract_cpp_macros_issue375);
4726+
RUN_TEST(extract_cpp_functionlike_macro_type_arg_no_false_parse_partial_issue1071);
4727+
RUN_TEST(extract_cpp_real_in_body_error_still_flagged_issue1071);
46774728
RUN_TEST(extract_gdscript_issue186);
46784729
RUN_TEST(extract_powershell_issue35);
46794730
RUN_TEST(extract_luau_issue39);

0 commit comments

Comments
 (0)