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

### Fixes carried by this fork

- C functions with conditional signatures and a shared body are now indexed, and conditional control statements no longer appear as functions or take calls away from their enclosing function.

- A TypeScript or JavaScript function wrapped in a string-named wrapper whose
result nothing binds — `Runtime.handler(cmd, Effect.fn("cli.api")(fn))`, an
array element, an arrow body, a return value — is now indexed under the
Expand Down
184 changes: 184 additions & 0 deletions __tests__/c-conditional-function-headers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import CodeGraph from '../src/index';
import { initGrammars, loadGrammarsForLanguages } from '../src/extraction/grammars';
import { extractFromSource } from '../src/extraction';
import { resetKernelForTests, tryKernelExtract } from '../src/extraction/kernel';
import { blankCAlternativeFunctionHeaders } from '../src/extraction/languages/c-cpp';
import type { ExtractionResult } from '../src/types';

const kernelAvailable = fs.existsSync(path.join(__dirname, '..', 'codegraph-kernel', 'prebuilds',
`${process.platform}-${process.arch}`, 'codegraph-kernel.node')) || process.env.CODEGRAPH_KERNEL_EXPECT === '1';
const envKeys = ['CODEGRAPH_KERNEL', 'CODEGRAPH_KERNEL_LANGS', 'CODEGRAPH_KERNEL_CCPP_ERROR_EXTRACT'] as const;

describe('C conditional function headers and recovered control statements', () => {
let savedEnv: Record<string, string | undefined>;
beforeAll(async () => {
await initGrammars();
await loadGrammarsForLanguages(['c']);
});
beforeEach(() => {
savedEnv = Object.fromEntries(envKeys.map(key => [key, process.env[key]]));
resetKernelForTests();
});
afterEach(() => {
for (const key of envKeys) {
if (savedEnv[key] === undefined) delete process.env[key];
else process.env[key] = savedEnv[key];
}
resetKernelForTests();
});

function extract(backend: 'wasm' | 'kernel', source: string): ExtractionResult {
if (backend === 'wasm') {
process.env.CODEGRAPH_KERNEL = '0';
return extractFromSource('qp.c', source, 'c');
}
delete process.env.CODEGRAPH_KERNEL;
process.env.CODEGRAPH_KERNEL_LANGS = 'all';
process.env.CODEGRAPH_KERNEL_CCPP_ERROR_EXTRACT = '1';
const result = tryKernelExtract('qp.c', source, 'c');
expect(result).not.toBeNull();
return result!;
}

function expectCall(result: ExtractionResult, caller: string, callee: string) {
const owner = result.nodes.find(n => n.kind === 'function' && n.name === caller)!;
expect(owner).toBeDefined();
expect(result.unresolvedReferences.some(ref => ref.fromNodeId === owner.id
&& ref.referenceKind === 'calls' && ref.referenceName === callee)).toBe(true);
}

const backends = kernelAvailable ? ['wasm', 'kernel'] as const : ['wasm'] as const;
for (const crlf of [false, true]) {
const eol = (text: string) => crlf ? text.replace(/\n/g, '\r\n') : text;
const label = crlf ? 'CRLF' : 'LF';

it.each(backends)(`extracts shared bodies once, with original locations and calls (${label}, %s)`, backend => {
const source = eol(`#ifndef Q_SPY
bool QActive_post_(QActive * const me, QEvt const * const e,
uint_fast16_t const margin)
#else
bool QActive_post_(QActive * const me, QEvt const * const e,
uint_fast16_t const margin, void const * const sender)
#endif
{
QF_CRIT_STAT_
Q_REQUIRE_ID(100, e != (QEvt const *)0);
deliver(e);
return true;
}
void after(void) { QActive_post_(0, 0, 0); }
`);
const result = extract(backend, source);
expect(result.nodes.filter(n => n.kind === 'function').map(n => n.name))
.toEqual(['QActive_post_', 'after']);
const post = result.nodes.find(n => n.name === 'QActive_post_')!;
expect([post.startLine, post.endLine]).toEqual([2, 13]);
expectCall(result, 'QActive_post_', 'deliver');
expectCall(result, 'QActive_post_', 'Q_REQUIRE_ID');
expectCall(result, 'after', 'QActive_post_');
const normalized = blankCAlternativeFunctionHeaders(source);
expect(normalized.length).toBe(source.length);
expect([...normalized.matchAll(/[\r\n]/g)].map(m => m.index))
.toEqual([...source.matchAll(/[\r\n]/g)].map(m => m.index));
expect(normalized.indexOf('deliver')).toBe(source.indexOf('deliver'));
});

it.each(backends)(`keeps conditional else-if calls in the real caller (${label}, %s)`, backend => {
const result = extract(backend, eol(`void dispatch(int r) {
if (r == 0) { first(); }
#ifdef Q_SPY
/* tracing-only branches */
else if (r == 1) {
handled();
}
else if (r == 2) {
ignored();
}
#endif
else { last(); }
after();
}
void Q_REQUIRE_ID(int id) { legitimate(); }
`));
expect(result.nodes.filter(n => n.kind === 'function').map(n => n.name))
.toEqual(['dispatch', 'Q_REQUIRE_ID']);
for (const callee of ['first', 'handled', 'ignored', 'last', 'after']) {
expectCall(result, 'dispatch', callee);
}
expectCall(result, 'Q_REQUIRE_ID', 'legitimate');
});

it.each(backends)(`supports elif and function-pointer parameters (${label}, %s)`, backend => {
const result = extract(backend, eol(`#if SMALL
static char *pick(void (*cb)(int))
#elif MEDIUM
static char *pick(void (*cb)(int), int count)
#else
static char *pick(void (*cb)(int), int count, void *sender)
#endif
/* common body */
{ return lookup(); }
`));
expect(result.nodes.filter(n => n.kind === 'function').map(n => n.name)).toEqual(['pick']);
expectCall(result, 'pick', 'lookup');
});
}

it.each(backends)('resolves calls to recovered functions in an indexed project (%s)', async backend => {
if (backend === 'wasm') process.env.CODEGRAPH_KERNEL = '0';
else {
delete process.env.CODEGRAPH_KERNEL;
process.env.CODEGRAPH_KERNEL_LANGS = 'all';
process.env.CODEGRAPH_KERNEL_CCPP_ERROR_EXTRACT = '1';
}
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-c-conditional-'));
let cg: CodeGraph | undefined;
try {
fs.writeFileSync(path.join(root, 'post.c'), `#ifndef Q_SPY
int post(int x)
#else
int post(int x, void *sender)
#endif
{
if (x == 0) { first(); }
#ifdef Q_SPY
else if (x == 1) { handled(); }
#endif
return x;
}
`);
fs.writeFileSync(path.join(root, 'app.c'), `void first(void) {}
void handled(void) {}
void app(void) { post(0); }
`);
cg = await CodeGraph.init(root, { index: true, silent: true });
const nodes = [...cg.getNodesInFile('post.c'), ...cg.getNodesInFile('app.c')];
const byId = new Map(nodes.map(n => [n.id, n.name]));
const calls = cg.getOutgoingEdgesFrom(nodes.map(n => n.id)).filter(e => e.kind === 'calls')
.map(e => `${byId.get(e.source)} -> ${byId.get(e.target)}`);
expect(nodes.filter(n => n.kind === 'function').map(n => n.name).sort())
.toEqual(['app', 'first', 'handled', 'post']);
expect(calls.sort()).toEqual(['app -> post', 'post -> first', 'post -> handled']);
} finally {
cg?.close();
fs.rmSync(root, { recursive: true, force: true });
}
});

it.each([
'#if A\nvoid f(void) {}\n#else\nvoid f(int x) {}\n#endif\n',
'#if A\nvoid f(void)\n#else\nvoid g(void)\n#endif\n{}\n',
'#if A\nvoid f(void)\n#endif\n{}\n',
'#if A\nvoid f(\n#else\nvoid f(int x\n#endif\n) {}\n',
'#if A\n#if B\nvoid f(void)\n#endif\n#else\nvoid f(int x)\n#endif\n{}\n',
'#if A\n#define TYPE int\nTYPE f(void)\n#else\nint f(int x)\n#endif\n{}\n',
'/*\n#if A\nvoid f(void)\n#else\nvoid f(int x)\n#endif\n{}\n*/\n',
'#define HEADERS \\\n#if A \\\nvoid f(void) \\\n#else \\\nvoid f(int x) \\\n#endif\n{}\n',
])('leaves unrelated or ambiguous conditionals unchanged: %s', source => {
expect(blankCAlternativeFunctionHeaders(source)).toBe(source);
});
});
9 changes: 5 additions & 4 deletions codegraph-kernel/src/ccpp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1035,17 +1035,18 @@ impl<'t> Walker<'t> {
}


/// cppExtractor.isMisparsedFunction (languages/c-cpp.ts:811). cpp only.
/// C/C++ isMisparsedFunction: keep recovered control-statement bodies in
/// their enclosing caller rather than creating functions named `if`.
fn is_misparsed_function(&self, name: &str, node: Node) -> bool {
if matches!(name, "switch" | "if" | "for" | "while" | "do" | "case" | "return") {
return true;
}
if self.variant != Variant::Cpp {
return false;
}
if name.starts_with("namespace") {
return true;
}
if matches!(name, "switch" | "if" | "for" | "while" | "do" | "case" | "return") {
return true;
}
is_macro_misparsed_type_decl(node)
}

Expand Down
71 changes: 70 additions & 1 deletion src/extraction/languages/c-cpp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,10 @@ function extractCppReturnType(node: SyntaxNode, source: string): string | undefi
}

export const cExtractor: LanguageExtractor = {
// Error recovery can read `else if (...) { ... }` across a preprocessor
// boundary as a function with return type `else`. Keep its body's calls
// in the enclosing function through the existing misparse-body walker.
isMisparsedFunction: (name) => ['switch', 'if', 'for', 'while', 'do', 'case', 'return'].includes(name),
resolveName: (node, source) =>
recoverSingleArgMacroDefinedName(node, source) ?? unnamedTypeVariableName(node, source),
// A struct, union or enum defined in a declaration's type is documented by
Expand Down Expand Up @@ -2395,9 +2399,74 @@ export function blankCDesignatedMacroArgs(source: string): string {
return rawStrings.restore(out.join(''));
}

/**
* Alternative C function signatures can share a body after #endif (QP's
* tracing-enabled signatures add a sender parameter). Neither signature is
* a definition to tree-sitter in that shape. Keep the first signature only
* when EVERY branch is a complete header for the same name, with an #else
* and a shared opening brace. This chooses a representative signature, not
* a build configuration. Complete definitions and nested/partial headers
* stay untouched. All offsets, including CRLF, remain unchanged.
*/
export function blankCAlternativeFunctionHeaders(source: string): string {
if (!source.includes('#')) return source;
const code = maskCppCommentsAndLiterals(source);
type Region = { start: number; branch: number; headers: [number, number][]; valid: boolean; hasElse: boolean };
const stack: Region[] = [];
const spans: [number, number][] = [];
const headerName = (header: string): string | undefined => {
const match = /^\s*(?:[A-Za-z_]\w*[\s*]+)+([A-Za-z_]\w*)\s*\(/.exec(header);
if (!match || /[;{}#=]/.test(header)) return undefined;
let depth = 1;
for (let i = match[0].length; i < header.length; i++) {
if (header[i] === '(') depth++;
else if (header[i] === ')' && --depth === 0) {
return header.slice(i + 1).trim() === '' ? match[1] : undefined;
}
}
return undefined;
};
// Consume continued directives together: a #if spelled inside a macro
// replacement must never open a region of its own.
const directives = /^[ \t]*#(?:\\\r?\n|[^\r\n])*/gm;
for (const match of code.matchAll(directives)) {
const start = match.index!;
const end = start + match[0].length;
const directive = /^[ \t]*#[ \t]*(\w+)/.exec(match[0])?.[1];
const current = stack[stack.length - 1];
if (directive === 'if' || directive === 'ifdef' || directive === 'ifndef') {
if (current) current.valid = false;
stack.push({ start, branch: end, headers: [], valid: true, hasElse: false });
} else if (current && (directive === 'else' || directive === 'elif')) {
if (current.hasElse) current.valid = false;
current.headers.push([current.branch, start]);
current.branch = end;
if (directive === 'else') current.hasElse = true;
} else if (current && directive === 'endif') {
stack.pop();
current.headers.push([current.branch, start]);
if (!current.valid || !current.hasElse || !/^\s*\{/.test(code.slice(end))) continue;
const names = current.headers.map(([from, to]) => headerName(code.slice(from, to)));
if (!names[0] || names.some((name) => name !== names[0])) continue;
const [firstStart, firstEnd] = current.headers[0]!;
spans.push([current.start, firstStart], [firstEnd, end]);
} else if (current) {
current.valid = false;
}
}
if (spans.length === 0) return source;
const out = source.split('');
for (const [from, to] of spans) {
for (let i = from; i < to; i++) if (out[i] !== '\n' && out[i] !== '\r') out[i] = ' ';
}
return out.join('');
}

function preParseCSource(source: string): string {
const rawStrings = maskCppRawStrings(source);
source = rawStrings.source;
// Run before the directive snapshot so restoreDirectiveLines does not
// reintroduce the conditional that split the function header from its body.
source = blankCAlternativeFunctionHeaders(rawStrings.source);
const inner = blankCDesignatedMacroArgs(blankCKernelAnnotations(blankCCplusplusGuardBodies(source)));
let blanked = blankCLeadingAttrMacros(
blankLoneMacroLines(
Expand Down