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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,10 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

#### Symbols, tests and the viewer

- **A C macro call written with designated initializers no longer swallows every function after it.** Betaflight resets each config struct with `RESET_CONFIG(type, dst, .field = value, …)`, a shape the C grammar cannot parse; past a hundred or so fields its error recovery ran the enclosing function to the end of the file, the next function vanished from the index and every later one was filed under the first, where name matching then treated it as an unreachable closure. The argument list of such a call is now blanked before parsing, offsets kept, so the file's functions come out with their real extents. On that tree 45 functions in `pid.c` alone moved back to top level and their 117 callers resolve at exact-match confidence. Re-index after upgrading. (#1729)

- **A C `#if` group whose branches are not whole statements no longer produces phantom functions.** An `else if (…) { … }` arm kept behind `#ifdef`, an `if (…)` header whose body sits after the `#endif` (the ST HAL's per-device latency tables), or a function signature that differs per configuration all read to the C grammar as a function *named* `if` — and every real function after it in the file was then filed underneath it, or dropped. Such a group is now collapsed to its first live branch before parsing, offsets kept, and block macros written in capitals (`ATOMIC_BLOCK(…) { … }`) are recognized like their lowercase cousins. On a betaflight tree 265 phantom nested functions became 5, and whole functions that had been missing (`spiInternalStartDMA`, `processSmartPortTelemetry`, the CMSIS matrix routines) are back with their callers at exact-match confidence. Re-index after upgrading.

- **Files under an `e2e/` directory count as tests.** Their calls no longer appear as production callers in Steps, dead-code and test badges.

- **Production code under a `samples` or `examples` package path is no longer treated as test code.** A Kotlin or Java project whose package path runs through `com/google/samples/…` (Now in Android, for one) had nearly every file counted as a fixture, so the Map opened on `build-logic`, the entry points hid the app, and dead-code and test badges were wrong. Only the project layout above a `src/` folder decides now; the package path below it never does.
Expand Down
165 changes: 165 additions & 0 deletions __tests__/extraction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11671,6 +11671,171 @@ describe('C/C++ kernel-port preParse blanks (R7a)', () => {
expect(blankLoneMacroLines(bare)).toBe(bare);
});

it('blankCDesignatedMacroArgs empties a designated-initializer macro call, offsets kept (#1729)', async () => {
const { blankCDesignatedMacroArgs } = await import('../src/extraction/languages/c-cpp');
const src = [
'void resetProfile(profile_t *p)',
'{',
' RESET_CONFIG(profile_t, p,',
' .pid = { [PID_ROLL] = PID_ROLL_DEFAULT, [PID_YAW] = { 50, 75 } },',
' .limit = 500, // trailing comma follows',
' );',
' log(.5);',
' OTHER_MACRO(a == b, c);',
'}',
].join('\n');
const out = blankCDesignatedMacroArgs(src);
expect(out.length).toBe(src.length);
expect(out.split('\n').length).toBe(src.split('\n').length);
expect(out).toContain('RESET_CONFIG(');
expect(out).not.toContain('.pid');
expect(out).not.toContain('PID_ROLL');
// The closing `);` keeps its column; the argument lines are spaces.
expect(out.split('\n')[5]).toBe(' );');
expect(out.split('\n')[3]).toBe(' '.repeat(src.split('\n')[3].length));
// A numeric literal and a comparison are not designators.
expect(out).toContain('log(.5);');
expect(out).toContain('OTHER_MACRO(a == b, c);');
});

it('a designated-initializer macro call no longer swallows the functions after it (#1729)', async () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-1729-'));
try {
// 120 `.field = value` arguments: past the point where tree-sitter-c's
// error recovery ran the enclosing function to the end of the file.
const fields = Array.from({ length: 120 }, (_, i) => ` .field${i} = ${i},`).join('\n');
fs.writeFileSync(
path.join(dir, 'pid.c'),
`void resetProfile(profile_t *p)\n{\n RESET_CONFIG(profile_t, p,\n${fields}\n );\n}\n\nvoid g(void)\n{\n}\n\nint h(void)\n{\n return 1;\n}\n`
);
const cg = await CodeGraph.init(dir, { index: true });
try {
const fns = cg.getNodesByKind('function').filter((n) => n.filePath === 'pid.c');
const byName = Object.fromEntries(fns.map((n) => [n.name, n]));
expect(Object.keys(byName).sort()).toEqual(['g', 'h', 'resetProfile']);
expect(byName.resetProfile!.endLine).toBe(125);
expect(byName.g!.qualifiedName).toBe('g');
expect(byName.h!.qualifiedName).toBe('h');
} finally {
cg.close();
}
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});

it('blankCUnbalancedConditionalBranches collapses a damaged #if group to its first branch, offsets kept', async () => {
const { blankCUnbalancedConditionalBranches } = await import('../src/extraction/languages/c-cpp');
const src = [
'void f(int id)',
'{',
' if (id == 1) {',
' a(id);',
' }',
'#if defined(USE_V) || \\',
' defined(USE_W)',
' else if (id == 2) { // "{" in a comment',
' b(id);',
' }',
'#else',
' else if (id == 3) {',
' c(id);',
' }',
'#endif',
'#ifdef USE_X',
' d(id);',
'#else',
' e(id);',
'#endif',
'}',
'#if 0',
'void dead(void) {',
'#else',
'void live(void) {',
'#endif',
'}',
].join('\n');
const out = blankCUnbalancedConditionalBranches(src);
expect(out.length).toBe(src.length);
expect(out.split('\n').length).toBe(src.split('\n').length);
const lines = out.split('\n');
// The `else`-led group: directive lines (continuation included) and the
// second branch are spaces; the first branch is verbatim.
expect(lines[5]).toBe(' '.repeat(src.split('\n')[5]!.length));
expect(lines[6]).toBe(' '.repeat(src.split('\n')[6]!.length));
expect(lines[7]).toBe(' else if (id == 2) { // "{" in a comment');
expect(lines[8]).toBe(' b(id);');
expect(lines[10]).toBe(' ');
expect(lines[11]).toBe(' '.repeat(src.split('\n')[11]!.length));
expect(out).not.toContain('c(id)');
expect(lines[14]).toBe(' ');
// A balanced group is untouched, directives included.
expect(lines[15]).toBe('#ifdef USE_X');
expect(lines[16]).toBe(' d(id);');
expect(lines[18]).toBe(' e(id);');
expect(lines[19]).toBe('#endif');
// `#if 0` keeps the live branch instead.
expect(out).not.toContain('dead');
expect(lines[24]).toBe('void live(void) {');
// No conditional group at all: identity.
const plain = 'int g(void) {\n#define X 1\n return X;\n}\n';
expect(blankCUnbalancedConditionalBranches(plain)).toBe(plain);
});

it('a #if branch beginning with `else` no longer files the rest of the file under the enclosing function', async () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-c-ifelse-'));
try {
fs.writeFileSync(
path.join(dir, 'current.c'),
[
'void currentMeterRead(int id, meter_t *meter)',
'{',
' if (id == 1) {',
' adcRead(meter);',
' }',
'#ifdef USE_VIRTUAL',
' else if (id == 2) {',
' virtualRead(meter);',
' }',
'#endif',
' else {',
' resetMeter(meter);',
' }',
'}',
'',
'static bool markBusy(void)',
'{',
' ATOMIC_BLOCK(NVIC_PRIO_MAX) {',
' busy = true;',
' }',
' return true;',
'}',
'',
'int after(void)',
'{',
' return 1;',
'}',
'',
].join('\n')
);
const cg = await CodeGraph.init(dir, { index: true });
try {
const fns = cg.getNodesByKind('function').filter((n) => n.filePath === 'current.c');
const byName = Object.fromEntries(fns.map((n) => [n.name, n]));
expect(Object.keys(byName).sort()).toEqual(['after', 'currentMeterRead', 'markBusy']);
expect(byName.currentMeterRead!.endLine).toBe(14);
expect(byName.markBusy!.qualifiedName).toBe('markBusy');
expect(byName.markBusy!.endLine).toBe(22);
expect(byName.after!.qualifiedName).toBe('after');
expect(byName.after!.startLine).toBe(24);
} finally {
cg.close();
}
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});

it('blankCStatementMacroCalls blanks indented iterator macros, keeps the block', async () => {
const { blankCStatementMacroCalls } = await import('../src/extraction/languages/c-cpp');
const src = [
Expand Down
167 changes: 162 additions & 5 deletions src/extraction/languages/c-cpp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -946,7 +946,10 @@ export function blankCStatementMacroCalls(source: string): string {
const content = (l: string): string => l.replace(/\r$/, '').trim();
for (let i = 0; i < lines.length; i++) {
const line = lines[i] as string;
const m = /^[ \t]+([a-z_][a-z0-9_]*)[ \t]*\(/.exec(line);
// Lowercase iterator macros, or an all-caps block macro
// (`ATOMIC_BLOCK(NVIC_PRIO_MAX) {`, `PG_FOREACH(reg) {` — betaflight);
// a PascalCase name is left alone (a constructor, if the file is C++).
const m = /^[ \t]+([a-z_][a-z0-9_]*|[A-Z_][A-Z0-9_]*)[ \t]*\(/.exec(line);
if (!m || C_STMT_MACRO_KEYWORDS.has(m[1] as string)) continue;
const open = line.indexOf('(', m[0].length - 1);
let depth = 0;
Expand Down Expand Up @@ -1515,8 +1518,159 @@ export function blankCNamedVariadicDefineDots(source: string): string {
* C-detected headers in CUDA projects (llm.c keeps `__device__` helpers and
* kernel prototypes in plain `.h`) — the same content-gated CUDA blank as
* C++. Offset-preserving. */
/**
* Blank the argument list of a statement-level `MACRO( … );` call whose
* arguments are designated initializers — betaflight's
*
* RESET_CONFIG(pidProfile_t, pidProfile,
* .pid = { [PID_ROLL] = PID_ROLL_DEFAULT, … },
* .pidSumLimit = PIDSUM_LIMIT,
* …
* );
*
* tree-sitter-c has no rule for `.field = value` as a call argument, and past
* roughly a hundred such arguments its error recovery gives up on the
* enclosing function: the `function_definition` runs to the end of the file,
* the next function vanishes and every one after it is nested under the first
* (#1729 — 310 functions in 73 files on that tree, which name matching then
* treated as unreachable closures). Emptying the argument list to spaces,
* newlines kept, leaves `RESET_CONFIG(\n\n…\n);` — a call the grammar parses
* cleanly — at the cost of the references inside the initializer, which the
* broken parse was not yielding either. Statement-level only (`);` follows),
* macro-cased name only, offsets preserved.
*/
export function blankCDesignatedMacroArgs(source: string): string {
if (source.indexOf('=') === -1) return source;
const out = source.split('');
const re = /^[ \t]*([A-Z_][A-Z0-9_]*)\s*\(/gm;
let m: RegExpExecArray | null;
while ((m = re.exec(source))) {
const open = m.index + m[0].length - 1;
let depth = 1;
let i = open + 1;
for (; i < source.length && depth > 0; i++) {
const c = source[i];
if (c === '(') depth++;
else if (c === ')') depth--;
}
if (depth !== 0) continue;
const close = i - 1;
const args = source.slice(open + 1, close);
// A designator at argument depth: `.name =` or `[index] =`.
if (!/(^|[,{(\s])(\.[A-Za-z_]\w*|\[[^\]]+\])\s*=[^=]/.test(args)) continue;
if (!/^\s*;/.test(source.slice(close + 1))) continue;
for (let k = open + 1; k < close; k++) if (out[k] !== '\n') out[k] = ' ';
re.lastIndex = close;
}
return out.join('');
}

/**
* Collapse a `#if … #elif … #else … #endif` group whose branches are not
* self-contained statements to its first live branch. tree-sitter-c keeps
* EVERY branch of a conditional group and parses each as a run of block
* items, so a group is only harmless when each branch is complete on its
* own. Three shapes in real C are not, and each ends in a phantom function
* (K&R `if(cond) { … }` reads as an implicit-int function definition named
* `if`, and the extractor then files it — and every symbol after it — under
* the enclosing function; 265 on a betaflight tree, #1729 follow-up):
*
* - a branch that BEGINS with `else` — `} #ifdef X else if (…) { … } #endif`
* (a conditional arm of an if-chain);
* - a branch that ENDS with a bare `if (…)` / `else if (…)` header whose
* body sits after the `#endif` (the ST HAL's per-device latency tables);
* - a branch whose braces do not balance — a function signature or a `{`
* that differs per configuration, with the body shared.
*
* The first branch not written `#if 0` is kept verbatim; every other branch
* AND the group's directive lines are blanked to spaces (newlines and `\r`
* kept, so offsets survive), which is exactly what the preprocessor would
* hand a compiler for that configuration. The symbols the other branches
* held were not extracting cleanly anyway. Balanced groups — the vast
* majority — are untouched. This pass edits directive lines on purpose, so
* it runs AFTER `restoreDirectiveLines` (like the named-variadic pass).
* Groups are handled innermost-first; an inner group inside a blanked branch
* simply disappears with it.
*/
const C_COND_OPEN_RE = /^[ \t]*#[ \t]*(if|ifdef|ifndef)\b[ \t]*(.*)$/;
const C_COND_NEXT_RE = /^[ \t]*#[ \t]*(elif|else)\b/;
const C_COND_ENDIF_RE = /^[ \t]*#[ \t]*endif\b/;
export function blankCUnbalancedConditionalBranches(source: string): string {
if (source.indexOf('#') === -1) return source;
const lines = source.split('\n');
const stripCr = (l: string): string => (l.endsWith('\r') ? l.slice(0, -1) : l);
// Code content of a line: comments and string / char literals removed.
const code = (l: string): string =>
stripCr(l)
.replace(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g, '')
.replace(/\/\*.*?\*\//g, '')
.replace(/\/\/.*$/, '')
.trim();
const blank = (l: string): string => l.replace(/[^\r]/g, ' ');
// Directive line indices of each open group, innermost last.
const stack: Array<{ marks: number[]; keep: number }> = [];
const directive: boolean[] = [];
let changed = false;
let continuation = false;
for (let i = 0; i < lines.length; i++) {
const line = stripCr(lines[i] as string);
const isDirective: boolean = continuation || /^[ \t]*#/.test(line);
directive[i] = isDirective;
const wasContinuation = continuation;
continuation = isDirective && /\\\s*$/.test(line);
if (!isDirective || wasContinuation) continue;
let m: RegExpExecArray | null;
if ((m = C_COND_OPEN_RE.exec(line))) {
const dead = m[1] === 'if' && /^0\b/.test(m[2] as string);
stack.push({ marks: [i], keep: dead ? 1 : 0 });
continue;
}
const top = stack[stack.length - 1];
if (!top) continue;
if (C_COND_NEXT_RE.test(line)) {
top.marks.push(i);
continue;
}
if (!C_COND_ENDIF_RE.test(line)) continue;
stack.pop();
top.marks.push(i);
if (top.keep >= top.marks.length - 1) continue; // `#if 0` with nothing to keep
let damaged = false;
for (let b = 0; b < top.marks.length - 1 && !damaged; b++) {
const body: string[] = [];
for (let k = (top.marks[b] as number) + 1; k < (top.marks[b + 1] as number); k++) {
if (directive[k]) continue; // a `\`-continued directive line
const c = code(lines[k] as string);
if (c) body.push(c);
}
if (body.length === 0) continue;
const first = body[0] as string;
const last = body[body.length - 1] as string;
let depth = 0;
for (const l of body) for (const ch of l) depth += ch === '{' ? 1 : ch === '}' ? -1 : 0;
damaged =
depth !== 0 ||
/^else\b/.test(first) ||
(/^(else[ \t]+)?if[ \t]*\(/.test(last) && /\)$/.test(last));
}
if (!damaged) continue;
for (let b = 0; b < top.marks.length - 1; b++) {
const from = top.marks[b] as number;
const to = top.marks[b + 1] as number;
// The directive line (with its `\`-continuations), then — unless this
// is the kept branch — the branch body.
for (let k = from; k < to && (k === from || directive[k]); k++) lines[k] = blank(lines[k] as string);
if (b === top.keep) continue;
for (let k = from + 1; k < to; k++) lines[k] = blank(lines[k] as string);
}
lines[i] = blank(lines[i] as string);
changed = true;
}
return changed ? lines.join('\n') : source;
}

function preParseCSource(source: string): string {
const inner = blankCKernelAnnotations(blankCCplusplusGuardBodies(source));
const inner = blankCDesignatedMacroArgs(blankCKernelAnnotations(blankCCplusplusGuardBodies(source)));
let blanked = blankCLeadingAttrMacros(
blankLoneMacroLines(
blankCStatementMacroCalls(
Expand All @@ -1539,9 +1693,12 @@ function preParseCSource(source: string): string {
)
);
if (looksLikeCudaSource(blanked)) blanked = blankCudaConstructs(blanked);
// The named-variadic `#define` pass runs AFTER the directive restore — it
// deliberately edits directive lines (see its doc comment).
return blankCNamedVariadicDefineDots(restoreDirectiveLines(source, blanked));
// The named-variadic `#define` pass and the conditional-group collapse run
// AFTER the directive restore — they deliberately edit directive lines (see
// their doc comments).
return blankCUnbalancedConditionalBranches(
blankCNamedVariadicDefineDots(restoreDirectiveLines(source, blanked))
);
}

export const cppExtractor: LanguageExtractor = {
Expand Down