Skip to content

Commit ec28e66

Browse files
committed
fix(parsing): recover export-macro-hidden type definitions (#1989)
Collect a bounded set of conventional export macro candidates and inject empty definitions into the existing C/C++ preprocessing second pass without overriding explicit caller definitions. Conservatively reconcile remapped definitions to recover hidden classes, structs, enums, free functions, and inline methods while suppressing matching base-class phantom callables. Add focused regression coverage for supported suffixes, ordinary all-caps negative controls, candidate limits, comments, strings, raw strings, overlong names, explicit define priority, and C/C++ extraction. Local MinGW extraction tests pass (344/344). ASan/UBSan were not available in the local MinGW toolchain and remain covered by upstream CI. Signed-off-by: XIYBHK <xiybhk@163.com>
1 parent 2f9828d commit ec28e66

4 files changed

Lines changed: 944 additions & 10 deletions

File tree

‎internal/cbm/cbm.c‎

Lines changed: 241 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1029,6 +1029,63 @@ static bool cbm_span_contains_callable_def(const char *src, int src_len, uint32_
10291029
return false;
10301030
}
10311031

1032+
/* #1989: type-def counterpart of cbm_span_contains_callable_def. A rescued
1033+
* class/struct/enum definition has its name followed by a base clause (':') or
1034+
* a body ('{'), not a parameter list, so the callable validator would reject
1035+
* every type def pulled from the expanded tree. */
1036+
static bool cbm_span_contains_type_def(const char *src, int src_len, uint32_t start_line,
1037+
uint32_t end_line, const char *name) {
1038+
if (!src || src_len <= 0 || !name || !name[0] || start_line == 0 || end_line < start_line) {
1039+
return false;
1040+
}
1041+
int span_start = 0;
1042+
uint32_t line = 1;
1043+
while (span_start < src_len && line < start_line) {
1044+
if (src[span_start++] == '\n') {
1045+
line++;
1046+
}
1047+
}
1048+
if (line != start_line) {
1049+
return false;
1050+
}
1051+
int span_end = span_start;
1052+
while (span_end < src_len && line <= end_line) {
1053+
if (src[span_end++] == '\n') {
1054+
line++;
1055+
}
1056+
}
1057+
size_t name_len = strlen(name);
1058+
for (int pos = span_start; pos + (int)name_len <= span_end; pos++) {
1059+
if (strncmp(src + pos, name, name_len) != 0 ||
1060+
(pos > 0 && cbm_identifier_char(src[pos - 1])) ||
1061+
(pos + (int)name_len < src_len && cbm_identifier_char(src[pos + name_len]))) {
1062+
continue;
1063+
}
1064+
int open = pos + (int)name_len;
1065+
while (open < span_end && isspace((unsigned char)src[open])) {
1066+
open++;
1067+
}
1068+
// Base clause (": public B"), body ("{"), or template args ("<...").
1069+
if (open < span_end && (src[open] == ':' || src[open] == '{' || src[open] == '<')) {
1070+
return true;
1071+
}
1072+
}
1073+
return false;
1074+
}
1075+
1076+
/* #1989: label buckets for the misparse-correction gates. A raw def extracted
1077+
* from a broken `class MOD_API Foo` shape lands under a callable label while
1078+
* the expanded tree yields a proper type label for the same name. */
1079+
static bool cbm_def_label_is_type(const char *label) {
1080+
return label != NULL && (strcmp(label, "Class") == 0 || strcmp(label, "Enum") == 0 ||
1081+
strcmp(label, "Interface") == 0 || strcmp(label, "Type") == 0 ||
1082+
strcmp(label, "Struct") == 0 || strcmp(label, "Union") == 0);
1083+
}
1084+
1085+
static bool cbm_def_label_is_callable(const char *label) {
1086+
return label != NULL && (strcmp(label, "Function") == 0 || strcmp(label, "Method") == 0);
1087+
}
1088+
10321089
/* Remap an expanded-source definition back to the original input file. Every
10331090
* line in the definition must be attributable to the main file; generated
10341091
* macro bodies, included headers, and ambiguous spans fail closed. */
@@ -1423,6 +1480,14 @@ static CBMFileResult *extract_file_ex_body(const char *source, int source_len, C
14231480
// Defs keep original-source line numbers; only CALLS are extracted from expanded source.
14241481
if (language == CBM_LANG_C || language == CBM_LANG_CPP || language == CBM_LANG_CUDA) {
14251482
uint64_t pp_start = now_ns();
1483+
/* #1989: collect build-system export-macro candidates from the raw
1484+
* source. They are predefined empty in the expanded buffer (see
1485+
* preprocessor.cpp) so `class MOD_API Foo` parses with its real name,
1486+
* and the rescue loop below can adopt corrected type defs. Bounded:
1487+
* at most CBM_EXPORT_MACRO_MAX names per file. */
1488+
char export_cands[CBM_EXPORT_MACRO_MAX][CBM_EXPORT_MACRO_NAME_MAX];
1489+
int export_cand_count =
1490+
cbm_export_macro_candidates(source, source_len, export_cands, CBM_EXPORT_MACRO_MAX);
14261491
CBMPreprocessedSource *preprocessed = cbm_preprocess_with_map(
14271492
source, source_len, rel_path, extra_defines, include_paths, language != CBM_LANG_C);
14281493
if (preprocessed && preprocessed->source) {
@@ -1503,44 +1568,211 @@ static CBMFileResult *extract_file_ex_body(const char *source, int source_len, C
15031568
* recover defs from it — adopting ONLY those that
15041569
* intersect a raw ERROR region, whose name is visible on
15051570
* the raw source line, and whose QN the raw pass did not
1506-
* already extract. */
1507-
if (ts_node_has_error(root)) {
1571+
* already extract.
1572+
*
1573+
* #1989: build-system export macros (<MOD>_API,
1574+
* <lib>_EXPORT, ...) are empty on the real compile line but
1575+
* opaque to tree-sitter, so `class MOD_API Foo` misparses
1576+
* with the macro as the type name — often with NO raw ERROR
1577+
* region at all (the class_specifier "succeeds" with the
1578+
* wrong name; enums and free functions do error out). When
1579+
* export-macro candidates were collected for this file, also
1580+
* adopt expanded defs that replace a raw def literally named
1581+
* as a candidate, or that correct a raw misparse label (the
1582+
* raw pass extracted the same name as Function/Method from
1583+
* the broken function_definition shape; the expanded tree
1584+
* yields a proper type def). Superseded raw defs are
1585+
* dropped so the corrected definition is not shadowed by
1586+
* the qualified-name dedup. With no candidates the block
1587+
* reduces to the original #961 behavior. */
1588+
if (ts_node_has_error(root) || export_cand_count > 0) {
15081589
cbm_error_regions_t raw_regs = {{0}, {0}, 0};
15091590
cbm_collect_error_regions(root, &raw_regs, source, source_len);
1510-
if (raw_regs.count > 0) {
1591+
if (raw_regs.count > 0 || export_cand_count > 0) {
15111592
int defs_before = result->defs.count;
15121593
cbm_extract_definitions(&pp_ctx);
15131594
int w = defs_before;
1595+
char *superseded = NULL;
1596+
if (export_cand_count > 0 && defs_before > 0) {
1597+
superseded = (char *)calloc((size_t)defs_before, 1);
1598+
}
1599+
/* #1989: spans of defs already adopted from the
1600+
* expanded tree. A def nested inside one of these
1601+
* (an inline method of a rescued class) was dropped
1602+
* by the raw pass — the misparsed
1603+
* `class MOD_API Foo {...}` parsed as a
1604+
* function_definition, and nested defs in its body
1605+
* are never walked. Bounded: one span per adopted
1606+
* def, capped at 64. */
1607+
struct {
1608+
uint32_t start;
1609+
uint32_t end;
1610+
} rescued_spans[64];
1611+
int rescued_count = 0;
15141612
for (int i = defs_before; i < result->defs.count; i++) {
15151613
CBMDefinition *d = &result->defs.items[i];
15161614
bool adopt = false;
1615+
int supersede_j = -1;
15171616
if (cbm_remap_preprocessed_def(d, preprocessed)) {
15181617
for (int rj = 0; rj < raw_regs.count && !adopt; rj++) {
15191618
if (d->start_line <= raw_regs.ends[rj] &&
15201619
d->end_line >= raw_regs.starts[rj]) {
15211620
adopt = true;
15221621
}
15231622
}
1623+
if (!adopt && export_cand_count > 0 && d->name) {
1624+
for (int ci = 0; ci < export_cand_count && !adopt; ci++) {
1625+
for (int j = 0; j < defs_before && !adopt; j++) {
1626+
CBMDefinition *r = &result->defs.items[j];
1627+
if (!r->name ||
1628+
strcmp(r->name, export_cands[ci]) != 0) {
1629+
continue;
1630+
}
1631+
if (r->start_line <= d->end_line &&
1632+
d->start_line <= r->end_line &&
1633+
cbm_span_contains_type_def(
1634+
source, source_len, d->start_line,
1635+
d->end_line, d->name)) {
1636+
adopt = true;
1637+
supersede_j = j;
1638+
}
1639+
}
1640+
}
1641+
if (!adopt && d->label && cbm_def_label_is_type(d->label)) {
1642+
for (int j = 0; j < defs_before && !adopt; j++) {
1643+
CBMDefinition *r = &result->defs.items[j];
1644+
/* Widened past callables (#1989): the
1645+
* misparse also mints value-label raw
1646+
* defs for types (`class X_API FEmpty {}`
1647+
* -> Variable FEmpty). Same name,
1648+
* overlapping span, raw label is NOT a
1649+
* type => raw is the misparse artifact. */
1650+
if (!r->name || !r->label || !d->name ||
1651+
strcmp(r->name, d->name) != 0 ||
1652+
cbm_def_label_is_type(r->label)) {
1653+
continue;
1654+
}
1655+
if (r->start_line <= d->end_line &&
1656+
d->start_line <= r->end_line) {
1657+
adopt = true;
1658+
supersede_j = j;
1659+
}
1660+
}
1661+
}
1662+
/* Nested rescue (#1989): a def nested inside
1663+
* an already-adopted def's span — the inline
1664+
* method of a rescued class, dropped by the
1665+
* raw pass because the class parsed as a
1666+
* function body. The same validation gates
1667+
* (name-on-line, shape, QN dedup) still run
1668+
* below before it lands. */
1669+
if (!adopt && rescued_count > 0) {
1670+
for (int k = 0; k < rescued_count && !adopt; k++) {
1671+
if (d->start_line >= rescued_spans[k].start &&
1672+
d->end_line <= rescued_spans[k].end) {
1673+
adopt = true;
1674+
}
1675+
}
1676+
}
1677+
}
15241678
}
1525-
if (adopt && (!d->name ||
1526-
!cbm_line_contains(source, source_len, d->start_line,
1527-
d->name) ||
1528-
!cbm_span_contains_callable_def(
1529-
source, source_len, d->start_line, d->end_line,
1530-
d->name))) {
1679+
if (adopt &&
1680+
(!d->name ||
1681+
!cbm_line_contains(source, source_len, d->start_line,
1682+
d->name) ||
1683+
(!cbm_span_contains_callable_def(source, source_len,
1684+
d->start_line, d->end_line,
1685+
d->name) &&
1686+
!cbm_span_contains_type_def(source, source_len, d->start_line,
1687+
d->end_line, d->name)))) {
15311688
adopt = false;
15321689
}
15331690
for (int j = 0; j < defs_before && adopt; j++) {
1691+
if (j == supersede_j || (superseded && superseded[j])) {
1692+
continue; // being replaced by this very def
1693+
}
15341694
const char *q = result->defs.items[j].qualified_name;
15351695
if (q && d->qualified_name &&
15361696
strcmp(q, d->qualified_name) == 0) {
1697+
/* #1989: a raw def holding the same QN is
1698+
* usually a reject — unless it is the
1699+
* misparse artifact this very def came to
1700+
* replace (raw "Function FCalc" from the
1701+
* broken `class MOD_API FCalc` shape vs the
1702+
* rescued "Class FCalc"). Same name, same
1703+
* span, raw label is not a type: supersede
1704+
* instead of dropping the correction. */
1705+
CBMDefinition *r = &result->defs.items[j];
1706+
if (d->label && cbm_def_label_is_type(d->label) &&
1707+
r->label && r->name && d->name &&
1708+
strcmp(r->name, d->name) == 0 &&
1709+
!cbm_def_label_is_type(r->label) &&
1710+
r->start_line == d->start_line &&
1711+
r->end_line == d->end_line && superseded) {
1712+
superseded[j] = 1;
1713+
continue;
1714+
}
15371715
adopt = false;
15381716
}
15391717
}
15401718
if (adopt) {
1719+
if (supersede_j >= 0 && supersede_j < defs_before &&
1720+
superseded) {
1721+
superseded[supersede_j] = 1;
1722+
}
1723+
/* Phantom-artifact suppression (#1989): on the
1724+
* raw tree `class MOD_API Foo : public Base
1725+
* {...}` parses as a function_definition whose
1726+
* declarator is the BASE-CLASS name, so the raw
1727+
* pass mints a bogus Function def named after
1728+
* the base spanning the whole class. Fingerprint:
1729+
* a raw callable def with an IDENTICAL span to
1730+
* the adopted type def whose name is one of the
1731+
* adopted def's base classes. A real method is
1732+
* a strict subset of the class span and never
1733+
* carries a base-class name. */
1734+
if (d->label && cbm_def_label_is_type(d->label) && superseded &&
1735+
d->base_classes) {
1736+
for (int j = 0; j < defs_before; j++) {
1737+
CBMDefinition *r = &result->defs.items[j];
1738+
if (!r->label || !r->name ||
1739+
!cbm_def_label_is_callable(r->label) ||
1740+
r->start_line != d->start_line ||
1741+
r->end_line != d->end_line) {
1742+
continue;
1743+
}
1744+
for (const char **b = d->base_classes; *b; b++) {
1745+
if (strcmp(*b, r->name) == 0) {
1746+
superseded[j] = 1;
1747+
break;
1748+
}
1749+
}
1750+
}
1751+
}
1752+
if (rescued_count < 64) {
1753+
rescued_spans[rescued_count].start = d->start_line;
1754+
rescued_spans[rescued_count].end = d->end_line;
1755+
rescued_count++;
1756+
}
15411757
result->defs.items[w++] = *d;
15421758
}
15431759
}
1760+
if (superseded) {
1761+
int rw = 0;
1762+
for (int j = 0; j < defs_before; j++) {
1763+
if (!superseded[j]) {
1764+
result->defs.items[rw++] = result->defs.items[j];
1765+
}
1766+
}
1767+
int shift = defs_before - rw;
1768+
if (shift > 0 && w > defs_before) {
1769+
memmove(&result->defs.items[rw],
1770+
&result->defs.items[defs_before],
1771+
(size_t)(w - defs_before) * sizeof(CBMDefinition));
1772+
}
1773+
w -= shift;
1774+
free(superseded);
1775+
}
15441776
result->defs.count = w;
15451777
}
15461778
}

0 commit comments

Comments
 (0)