mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-22 09:35:22 +08:00
Single-argument function macros were indexed under parentheses in C or the macro name in C++, breaking caller resolution. Recover both parser shapes only when a preceding local macro definition establishes the function name, and mirror the recovery in the Rust kernel. Preserve existing multi-argument recovery and ordinary declarations; add extraction, resolution, explore-flow, and parity regressions. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4001,6 +4001,52 @@ enum class EDenseMode : uint8
|
||||
});
|
||||
});
|
||||
|
||||
describe('C/C++ single-argument function macros (#1373)', () => {
|
||||
it.each(['c', 'cpp'] as const)('recovers single-argument function macros in %s (#1373)', (language) => {
|
||||
const code = '#define NATIVE_FN(name) int name(void)\n'
|
||||
+ 'NATIVE_FN(get_version) { return helper(); }\n'
|
||||
+ 'int use_it(void) { return get_version(); }\n';
|
||||
const result = extractFromSource(`main.${language}`, code, language);
|
||||
const functions = result.nodes.filter((n) => n.kind === 'function');
|
||||
expect(functions.map((n) => n.name)).toEqual(['get_version', 'use_it']);
|
||||
expect(functions[0]).toMatchObject({ qualifiedName: 'get_version', startLine: 2, endLine: 2, startColumn: 0 });
|
||||
expect(result.unresolvedReferences).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ fromNodeId: functions[0].id, referenceName: 'helper', referenceKind: 'calls' }),
|
||||
expect.objectContaining({ fromNodeId: functions[1].id, referenceName: 'get_version', referenceKind: 'calls' }),
|
||||
]));
|
||||
});
|
||||
|
||||
it.each(['c', 'cpp'] as const)('does not guess single-argument macro names in %s (#1373)', (language) => {
|
||||
for (const prefix of [
|
||||
'',
|
||||
'// #define NATIVE_FN(name) int name(void)\n',
|
||||
'#define NATIVE_FN(name) int fixed(name)\n',
|
||||
'#define NATIVE_FN(name) int test_ ## name(void)\n',
|
||||
'#define NATIVE_FN(name) register_test(name)\n',
|
||||
'#define NATIVE_FN(name) typedef int name(void)\n',
|
||||
'#define NATIVE_FN(name) int name(void)\n#define NATIVE_FN int\n',
|
||||
'#define NATIVE_FN(name) int name(void)\n#ifdef OTHER\n#undef NATIVE_FN\n#endif\n',
|
||||
'#define NATIVE_FN(name) int name(void)\n#undef NATIVE_FN\n',
|
||||
'#define NATIVE_FN(name) int name(void)\n#define NATIVE_FN(name) int fixed(name)\n',
|
||||
]) {
|
||||
const result = extractFromSource(`main.${language}`, prefix + 'NATIVE_FN(candidate) { return 1; }\n', language);
|
||||
expect(result.nodes.filter((n) => n.kind === 'function').map((n) => n.name)).not.toContain('candidate');
|
||||
}
|
||||
const alternate = extractFromSource(`main.${language}`, [
|
||||
'#ifdef OTHER', '#define NATIVE_FN(name) int name(void)', '#else',
|
||||
'NATIVE_FN(candidate) { return 1; }', '#endif', '',
|
||||
].join('\n'), language);
|
||||
expect(alternate.nodes.filter((n) => n.kind === 'function').map((n) => n.name)).not.toContain('candidate');
|
||||
const ordinary = extractFromSource(`main.${language}`, 'int (parenthesized)(void) { return 1; }\n', language);
|
||||
expect(ordinary.nodes.find((n) => n.kind === 'function')?.name).toBe('(parenthesized)');
|
||||
if (language === 'c') {
|
||||
const knr = extractFromSource('knr.c', 'int old_style(arg) int arg; { return arg; }\n', 'c');
|
||||
expect(knr.nodes.find((n) => n.kind === 'function')?.name).toBe('old_style');
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('CUDA extraction (#387)', () => {
|
||||
// CUDA parses with the C++ grammar. Three CUDA-only shapes misparse:
|
||||
// execution-space specifiers (`__global__ void f(…)`) shunt the real return
|
||||
|
||||
@@ -95,6 +95,33 @@ describe.skipIf(!kernelBuilt)('kernel C/C++ extraction parity', () => {
|
||||
expect(viaWasm.nodes.length).toBeGreaterThanOrEqual(minNodes);
|
||||
}
|
||||
|
||||
it.each(['c', 'cpp'] as const)('single-argument function macros and negative controls: %s (#1373)', (language) => {
|
||||
const source = [
|
||||
'#define NATIVE_FN(name) int name(void)',
|
||||
'NATIVE_FN(get_version) { return helper(); }',
|
||||
'int use_it(void) { return get_version(); }',
|
||||
'#define FN(name) int name(void)',
|
||||
'FN(short_macro) { return 0; }',
|
||||
'#define POINTER_FN(name) const char *name(void)',
|
||||
'POINTER_FN(get_text) { return 0; }',
|
||||
'#define TEST_CASE(name) int test_ ## name(void)',
|
||||
'TEST_CASE(candidate) { return 0; }',
|
||||
'#define REGISTER_FN(name) register_test(name)',
|
||||
'REGISTER_FN(registration) { return 0; }',
|
||||
'int (parenthesized)(void) { return 1; }',
|
||||
'',
|
||||
].join('\n');
|
||||
assertParity(`fixtures/macros.${language}`, source, language);
|
||||
assertParity(`fixtures/macros-crlf.${language}`, source.replace(/\n/g, '\r\n'), language);
|
||||
const result = tryKernelExtract(`fixtures/macros.${language}`, source, language)!;
|
||||
const names = result.nodes.filter((n) => n.kind === 'function').map((n) => n.name);
|
||||
expect(names).toContain('get_version');
|
||||
expect(names).toContain('get_text');
|
||||
expect(names).toContain('short_macro');
|
||||
expect(names).not.toContain('candidate');
|
||||
expect(names).not.toContain('registration');
|
||||
});
|
||||
|
||||
it('torture fixture (c): fn-ptr tables, typedefs, file-scope consts, value-refs', () => {
|
||||
const file = path.join(FIXTURE_DIR, 'torture.c');
|
||||
assertParity('fixtures/torture.c', fs.readFileSync(file, 'utf8'), 'c');
|
||||
|
||||
@@ -39,6 +39,34 @@ describe('Resolution Module', () => {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true, maxRetries: 5 });
|
||||
});
|
||||
|
||||
it.each(['c', 'cpp'] as const)('connects single-argument function macros in %s (#1373)', async (language) => {
|
||||
const file = `main.${language}`;
|
||||
fs.writeFileSync(path.join(tempDir, file), [
|
||||
'#define NATIVE_FN(name) int name(void)',
|
||||
'int helper(void) { return 1; }',
|
||||
'NATIVE_FN(get_version) { return helper(); }',
|
||||
'int use_it(void) { return get_version(); }',
|
||||
'int plain_func(void) { return 42; }',
|
||||
'',
|
||||
].join('\n'));
|
||||
cg = await CodeGraph.init(tempDir, { index: true });
|
||||
const functions = cg.getNodesByKind('function');
|
||||
const recovered = functions.find((n) => n.name === 'get_version');
|
||||
expect(recovered).toBeDefined();
|
||||
expect(cg.getCallers(recovered!.id).map((c) => c.node.name)).toContain('use_it');
|
||||
expect(cg.getCallees(recovered!.id).map((c) => c.node.name)).toContain('helper');
|
||||
const caller = functions.find((n) => n.name === 'use_it')!;
|
||||
expect(cg.getCallees(caller.id).map((c) => c.node.id)).toContain(recovered!.id);
|
||||
expect(functions.map((n) => n.name)).toContain('plain_func');
|
||||
const { ToolHandler } = await import('../src/mcp/tools');
|
||||
const result = await new ToolHandler(cg).execute('codegraph_explore', { query: 'use_it get_version helper' });
|
||||
expect(result.isError).toBeUndefined();
|
||||
const text = result.content[0]!.text!;
|
||||
expect(text).toContain('Flow');
|
||||
expect(text).toMatch(/use_it[^\n]*get_version[^\n]*helper/);
|
||||
|
||||
});
|
||||
|
||||
describe('Name Matcher', () => {
|
||||
it('should match exact name references', () => {
|
||||
// Create a mock context
|
||||
|
||||
@@ -93,6 +93,12 @@ fn has_lower_re() -> &'static Regex {
|
||||
static RE: OnceLock<Regex> = OnceLock::new();
|
||||
RE.get_or_init(|| Regex::new(r"[a-z]").unwrap())
|
||||
}
|
||||
fn single_arg_macro_replacement_re() -> &'static Regex {
|
||||
static RE: OnceLock<Regex> = OnceLock::new();
|
||||
RE.get_or_init(|| Regex::new(
|
||||
r"^(?:[A-Za-z_][A-Za-z0-9_:]*[ \t\r\n]+)+[*& \t\r\n]*([A-Za-z_][A-Za-z0-9_]*)[ \t\r\n]*\([^(){};#]*\)[ \t\r\n]*$"
|
||||
).unwrap())
|
||||
}
|
||||
/// normalizeCppReturnType: smart-pointer/optional unwrap.
|
||||
fn ret_wrapper_re() -> &'static Regex {
|
||||
static RE: OnceLock<Regex> = OnceLock::new();
|
||||
@@ -593,6 +599,9 @@ impl<'t> Walker<'t> {
|
||||
/// extractNameRaw for the c/cpp extractor configs (nameField 'declarator';
|
||||
/// cpp resolveName = extractCppQualifiedMethodName).
|
||||
fn extract_name_raw(&self, node: Node) -> String {
|
||||
if let Some(name) = self.recover_single_arg_macro_defined_name(node) {
|
||||
return name;
|
||||
}
|
||||
if self.variant == Variant::Cpp {
|
||||
if let Some(hook) = self.extract_cpp_qualified_method_name(node) {
|
||||
return hook;
|
||||
@@ -650,6 +659,84 @@ impl<'t> Walker<'t> {
|
||||
parts.last().map(|s| s.to_string())
|
||||
}
|
||||
|
||||
/// recoverSingleArgMacroDefinedName: only a local macro definition proves
|
||||
/// that its argument is the function name, in either parser shape (#1373).
|
||||
fn recover_single_arg_macro_defined_name(&self, node: Node) -> Option<String> {
|
||||
if node.kind() != "function_definition" {
|
||||
return None;
|
||||
}
|
||||
let declarator = node.child_by_field_name("declarator")?;
|
||||
let (macro_node, argument) = if declarator.kind() == "parenthesized_declarator"
|
||||
&& declarator.named_child_count() == 1
|
||||
{
|
||||
let m = node.child_by_field_name("type")?;
|
||||
let a = declarator.named_child(0)?;
|
||||
if m.kind() != "type_identifier" || a.kind() != "identifier" {
|
||||
return None;
|
||||
}
|
||||
(m, a)
|
||||
} else if declarator.kind() == "function_declarator"
|
||||
&& node.child_by_field_name("type").is_none()
|
||||
{
|
||||
let m = declarator.child_by_field_name("declarator")?;
|
||||
let params = declarator.child_by_field_name("parameters")?;
|
||||
let param = params.named_child(0)?;
|
||||
if m.kind() != "identifier" || params.named_child_count() != 1
|
||||
|| param.kind() != "parameter_declaration" || param.named_child_count() != 1
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let a = param.named_child(0)?;
|
||||
if a.kind() != "type_identifier" {
|
||||
return None;
|
||||
}
|
||||
(m, a)
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
let macro_name = self.text(macro_node);
|
||||
let mut scope = Some(node);
|
||||
while let Some(current) = scope {
|
||||
if current.kind() == "preproc_else" || current.kind().starts_with("preproc_elif") {
|
||||
return None;
|
||||
}
|
||||
let mut previous = current.prev_named_sibling();
|
||||
while let Some(prev) = previous {
|
||||
previous = prev.prev_named_sibling();
|
||||
if prev.kind().starts_with("preproc_if") {
|
||||
return None;
|
||||
}
|
||||
if prev.kind() == "preproc_call"
|
||||
&& prev.child_by_field_name("directive").map(|n| self.text(n)) == Some("#undef")
|
||||
&& prev.child_by_field_name("argument").map(|n| self.text(n).trim()) == Some(macro_name)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
if !matches!(prev.kind(), "preproc_function_def" | "preproc_def")
|
||||
|| prev.child_by_field_name("name").map(|n| self.text(n)) != Some(macro_name)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let params = prev.child_by_field_name("parameters")?;
|
||||
let param = params.named_child(0)?;
|
||||
let value = prev.child_by_field_name("value")?;
|
||||
if params.named_child_count() != 1 || param.kind() != "identifier" {
|
||||
return None;
|
||||
}
|
||||
let replacement = self.text(value).replace("\\\r\n", " ").replace("\\\n", " ");
|
||||
let captures = single_arg_macro_replacement_re().captures(replacement.trim())?;
|
||||
if replacement.split(|c: char| !c.is_ascii_alphanumeric() && c != '_').any(|s| s == "typedef")
|
||||
|| captures.get(1)?.as_str() != self.text(param)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
return Some(self.text(argument).to_string());
|
||||
}
|
||||
scope = current.parent();
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// recoverCppMacroDefinedName (languages/c-cpp.ts:49).
|
||||
fn recover_cpp_macro_defined_name(&self, node: Node) -> Option<String> {
|
||||
if node.kind() != "function_definition" {
|
||||
|
||||
@@ -27,6 +27,55 @@ function findDeclaratorQualifiedId(declarator: SyntaxNode): SyntaxNode | undefin
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Single-argument macros are ambiguous without their definition (#1373).
|
||||
* Require a preceding local #define whose replacement uses its sole parameter
|
||||
* as the function declarator; registration, token-pasting, and K&R forms stay
|
||||
* untouched. Walk enclosing scopes, but never guess across conditional macros.
|
||||
*/
|
||||
function recoverSingleArgMacroDefinedName(node: SyntaxNode, source: string): string | undefined {
|
||||
if (node.type !== 'function_definition') return undefined;
|
||||
const declarator = getChildByField(node, 'declarator');
|
||||
let macro: SyntaxNode | null = null;
|
||||
let argument: SyntaxNode | null = null;
|
||||
if (declarator?.type === 'parenthesized_declarator' && declarator.namedChildCount === 1) {
|
||||
// C: NATIVE_FN is parsed as the return type, (name) as the declarator.
|
||||
macro = getChildByField(node, 'type');
|
||||
argument = declarator.namedChild(0);
|
||||
if (macro?.type !== 'type_identifier' || argument?.type !== 'identifier') return undefined;
|
||||
} else if (declarator?.type === 'function_declarator' && !getChildByField(node, 'type')) {
|
||||
// C++: NATIVE_FN(name) is parsed as an implicit-return-type function.
|
||||
macro = getChildByField(declarator, 'declarator');
|
||||
const params = getChildByField(declarator, 'parameters');
|
||||
const param = params?.namedChild(0);
|
||||
if (macro?.type !== 'identifier' || params?.namedChildCount !== 1 ||
|
||||
param?.type !== 'parameter_declaration' || param.namedChildCount !== 1) return undefined;
|
||||
argument = param.namedChild(0);
|
||||
if (argument?.type !== 'type_identifier') return undefined;
|
||||
}
|
||||
if (!macro || !argument) return undefined;
|
||||
const macroName = getNodeText(macro, source);
|
||||
for (let scope: SyntaxNode | null = node; scope; scope = scope.parent) {
|
||||
if (scope.type === 'preproc_else' || scope.type.startsWith('preproc_elif')) return undefined;
|
||||
for (let prev = scope.previousNamedSibling; prev; prev = prev.previousNamedSibling) {
|
||||
if (prev.type.startsWith('preproc_if')) return undefined;
|
||||
if (prev.type === 'preproc_call' && getChildByField(prev, 'directive')?.text === '#undef' &&
|
||||
getChildByField(prev, 'argument')?.text.trim() === macroName) return undefined;
|
||||
if (prev.type !== 'preproc_function_def' && prev.type !== 'preproc_def') continue;
|
||||
if (getChildByField(prev, 'name')?.text !== macroName) continue;
|
||||
const params = getChildByField(prev, 'parameters');
|
||||
const param = params?.namedChild(0);
|
||||
const value = getChildByField(prev, 'value');
|
||||
if (params?.namedChildCount !== 1 || param?.type !== 'identifier' || !value) return undefined;
|
||||
const replacement = getNodeText(value, source).replace(/\\\r?\n/g, ' ').trim();
|
||||
const match = replacement.match(/^(?:[A-Za-z_][A-Za-z0-9_:]*\s+)+[*&\s]*([A-Za-z_]\w*)\s*\([^(){};#]*\)\s*$/);
|
||||
if (!match || /\btypedef\b/.test(replacement) || match[1] !== param.text) return undefined;
|
||||
return getNodeText(argument, source);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recover the real function name from the macro-definition idiom
|
||||
* `MACRO_NAME(real_name, typed args…) { body }` — flash-attention's
|
||||
@@ -73,7 +122,7 @@ function recoverCppMacroDefinedName(node: SyntaxNode, source: string): string |
|
||||
}
|
||||
|
||||
function extractCppQualifiedMethodName(node: SyntaxNode, source: string): string | undefined {
|
||||
const macroDefined = recoverCppMacroDefinedName(node, source);
|
||||
const macroDefined = recoverSingleArgMacroDefinedName(node, source) ?? recoverCppMacroDefinedName(node, source);
|
||||
if (macroDefined) return macroDefined;
|
||||
const declarator = getChildByField(node, 'declarator');
|
||||
if (!declarator) return undefined;
|
||||
@@ -207,6 +256,7 @@ function extractCppReturnType(node: SyntaxNode, source: string): string | undefi
|
||||
}
|
||||
|
||||
export const cExtractor: LanguageExtractor = {
|
||||
resolveName: recoverSingleArgMacroDefinedName,
|
||||
// CUDA in C-detected headers (content-gated blank; see preParseCSource).
|
||||
preParse: preParseCSource,
|
||||
// Universal net: recover a real name from any macro-mangled function name.
|
||||
|
||||
Reference in New Issue
Block a user