GitNexus/gitnexus/test/integration/resolvers/callable-value-flow.test.ts
mengkaka 4154b63131
feat(indexing): add Objective-C semantic indexing support (#3179)
* docs: add Objective-C fork provider notes

* feat(objective-c): add deterministic provider and grammar

* feat(objective-c): finalize provider MVP

* fix(objective-c): harden provider integration

* fix(objective-c): normalize bare macro markers

* docs(objective-c): integrate provider documentation

* fix(objective-c): harden resolution and header classification

* fix(objective-c): complete provider follow-ups

* fix: address Objective-C review follow-ups

* chore: format Objective-C grammar sources

* fix(objective-c): harden review follow-ups

* Address PR review feedback (#3179)

Keep Objective-C chunking and macro recovery aligned with the grammar, and stop Community MEMBER_OF edges from leaking into symbol context.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address follow-up review on ObjC chunking and language fallback.

Keep preprocessor directive text from changing file-scope brace depth, group real ivar nodes, skip header modifiers, and restore Rakefile/Gemfile detection through getLanguageFromFilename.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Parse Objective-C headers with the objc grammar in embeddings.

ensureAndParse and structural extraction now use the same content classifier as ingest, including method snippets from .h files, so Protocol/Category/Class chunks are not re-parsed as C++.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3179)

Keep file-scope macro elision off C line splices and @interface/@protocol/@implementation bodies, and attach ivar attributes to the following instance variable when chunking.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(bench): rebaseline Objective-C CSV emit

* feat(objective-c): add workspace resolution and linear emit benches

Plain .h files are classified as C++, so the ObjC pass could not
resolve #import of those headers. Load a C/C#-style workspace once
per pass, and keep protocol-candidate USES linear.

Refs #3179

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3179)

- Compare LadybugDB labels() as a scalar when excluding Community MEMBER_OF edges.
- Walk superclass members, skip file-static C sibling defs, and ignore comments in ObjC header/macro scans.

Note: pre-existing failure in objective-c-provider integration (worker-pool ready timeout) not addressed by this PR.
Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3179)

Emit Objective-C declaration captures so compilation-unit siblings can share
header/implementation bindings, and keep class vs protocol visibility groups
distinct.

Note: pre-existing failure in worker-pool startup (GITNEXUS_WORKER_READY_TIMEOUT_MS) not addressed by this PR.
Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3179)

Emit every comma-separated property/ivar declarator, and count @interface
after a multiline block comment closes so in-declaration macros stay intact.

Note: pre-existing failure in worker-pool startup (GITNEXUS_WORKER_READY_TIMEOUT_MS) not addressed by this PR.
Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: ximengkai <ximengkai@soyoung.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-09 09:40:21 +00:00

1607 lines
43 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { SupportedLanguages } from 'gitnexus-shared';
import { providers } from '../../../src/core/ingestion/languages/index.js';
import { getRelationships, runPipelineFromRepo, type PipelineOptions } from './helpers.js';
import {
loadParseCache,
PARSE_CACHE_VERSION,
saveParseCache,
type ParseCache,
} from '../../../src/storage/parse-cache.js';
import {
getDurableParsedFileDir,
pruneAndSaveDurableParsedFileStore,
} from '../../../src/storage/parsedfile-store.js';
type CallableFlowCoverage = 'matrix' | 'dedicated' | 'not-applicable';
const CALLABLE_FLOW_PROVIDER_COVERAGE = {
[SupportedLanguages.JavaScript]: 'matrix',
[SupportedLanguages.TypeScript]: 'dedicated',
[SupportedLanguages.Python]: 'matrix',
[SupportedLanguages.Java]: 'matrix',
[SupportedLanguages.C]: 'dedicated',
[SupportedLanguages.CPlusPlus]: 'dedicated',
[SupportedLanguages.CSharp]: 'matrix',
[SupportedLanguages.Go]: 'matrix',
[SupportedLanguages.Ruby]: 'matrix',
[SupportedLanguages.Rust]: 'matrix',
[SupportedLanguages.PHP]: 'matrix',
[SupportedLanguages.Kotlin]: 'matrix',
[SupportedLanguages.Swift]: 'matrix',
[SupportedLanguages.Dart]: 'matrix',
[SupportedLanguages.Vue]: 'matrix',
[SupportedLanguages.Cobol]: 'matrix',
[SupportedLanguages.Zig]: 'matrix',
// Objective-C message sends are resolved by its ScopeResolver. The provider
// does not emit callable-value-flow captures in this MVP.
[SupportedLanguages.ObjectiveC]: 'not-applicable',
} as const satisfies Record<SupportedLanguages, CallableFlowCoverage>;
const PROVIDER_FLOW_CASES = [
{
language: SupportedLanguages.JavaScript,
extension: 'js',
caller: 'invoke',
target: 'target',
source: `
function target() {}
function invoke(callback) { callback(); }
const first = target;
const second = first;
invoke(second);
`,
},
{
language: SupportedLanguages.Python,
extension: 'py',
caller: 'invoke',
target: 'target',
source: `
def target():
pass
def invoke(callback):
callback()
first = target
second = first
invoke(second)
`,
},
{
language: SupportedLanguages.Java,
extension: 'java',
caller: 'invoke',
target: 'target',
source: `
class Demo {
static void target() {}
static void invoke(Runnable callback) { callback.run(); }
static void main(String[] args) {
Runnable first = Demo::target;
Runnable second = first;
invoke(second);
}
}
`,
},
{
language: SupportedLanguages.Kotlin,
extension: 'kt',
caller: 'invoke',
target: 'target',
source: `
fun target() {}
fun invoke(callback: () -> Unit) { callback() }
fun main() {
val first = ::target
val second = first
invoke(second)
}
`,
},
{
language: SupportedLanguages.Go,
extension: 'go',
caller: 'invoke',
target: 'target',
source: `
package main
func target() {}
func invoke(callback func()) { callback() }
func main() {
first := target
second := first
invoke(second)
}
`,
},
{
language: SupportedLanguages.Rust,
extension: 'rs',
caller: 'invoke',
target: 'target',
source: `
fn target() {}
fn invoke(callback: fn()) { callback(); }
fn main() {
let first = target;
let second = first;
invoke(second);
}
`,
},
{
language: SupportedLanguages.CSharp,
extension: 'cs',
caller: 'Invoke',
target: 'Target',
source: `
using System;
class Demo {
static void Target() {}
static void Invoke(Action callback) { callback(); }
static void Main() {
Action first = Target;
Action second = first;
Invoke(second);
}
}
`,
},
{
language: SupportedLanguages.PHP,
extension: 'php',
caller: 'invoke',
target: 'target',
source: `<?php
function target() {}
function invoke($callback) { $callback(); }
$first = target(...);
$second = $first;
invoke($second);
`,
},
{
language: SupportedLanguages.Ruby,
extension: 'rb',
caller: 'invoke',
target: 'target',
source: `
def target; end
def invoke(callback); callback.call; end
first = method(:target)
second = first
invoke(second)
`,
},
{
language: SupportedLanguages.Swift,
extension: 'swift',
caller: 'invoke',
target: 'target',
source: `
func target() {}
func invoke(_ callback: () -> Void) { callback() }
func main() {
let first = target
let second = first
invoke(second)
}
`,
},
{
language: SupportedLanguages.Dart,
extension: 'dart',
caller: 'invoke',
target: 'target',
source: `
void target() {}
void invoke(void Function() callback) { callback(); }
void main() {
final first = target;
final second = first;
invoke(second);
}
`,
},
{
language: SupportedLanguages.Vue,
extension: 'vue',
caller: 'invoke',
target: 'target',
source: `
<script setup lang="ts">
function target(): void {}
function invoke(callback: () => void): void { callback(); }
const first = target;
const second = first;
invoke(second);
</script>
`,
},
{
language: SupportedLanguages.Cobol,
extension: 'cbl',
caller: 'INVOKE',
target: 'TARGET',
source: `
>>SOURCE FORMAT FREE
IDENTIFICATION DIVISION.
PROGRAM-ID. MAIN.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 CALLBACK USAGE PROCEDURE-POINTER.
PROCEDURE DIVISION.
SET CALLBACK TO ENTRY "TARGET".
CALL "INVOKE" USING CALLBACK.
STOP RUN.
END PROGRAM MAIN.
IDENTIFICATION DIVISION.
PROGRAM-ID. INVOKE.
DATA DIVISION.
LINKAGE SECTION.
01 CB USAGE PROCEDURE-POINTER.
PROCEDURE DIVISION USING CB.
CALL CB.
GOBACK.
END PROGRAM INVOKE.
IDENTIFICATION DIVISION.
PROGRAM-ID. TARGET.
PROCEDURE DIVISION.
GOBACK.
END PROGRAM TARGET.
`,
},
{
language: SupportedLanguages.Zig,
extension: 'zig',
caller: 'invoke',
target: 'target',
source: `
fn target() void {}
fn invoke(callback: *const fn () void) void { callback(); }
pub fn main() void {
const first = target;
const second = first;
invoke(second);
}
`,
},
] as const;
async function runSource(extension: string, source: string, options: PipelineOptions = {}) {
return runSources({ [`main.${extension}`]: source }, options);
}
async function runSources(files: Record<string, string>, options: PipelineOptions = {}) {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-callable-flow-'));
try {
for (const [filePath, source] of Object.entries(files)) {
const fullPath = path.join(root, filePath);
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
fs.writeFileSync(fullPath, source, 'utf8');
}
return await runPipelineFromRepo(root, () => {}, { skipGraphPhases: true, ...options });
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
}
function callableCallSiteLines(
result: Awaited<ReturnType<typeof runSource>>,
source: string,
target: string,
): number[] {
return getRelationships(result, 'CALLS')
.filter(
(edge) =>
edge.source === source &&
edge.target === target &&
edge.rel.reason === 'callable-value-flow',
)
.map((edge) => Number(edge.rel.id.match(/:(\d+):(\d+)$/)?.[1]))
.sort((left, right) => left - right);
}
function callsFrom(
result: Awaited<ReturnType<typeof runSource>>,
source: string,
): Array<{ target: string; reason: string }> {
return getRelationships(result, 'CALLS')
.filter((edge) => edge.source === source)
.map((edge) => ({ target: edge.target, reason: edge.rel.reason ?? '' }));
}
function callableTargetQualifiedNames(
result: Awaited<ReturnType<typeof runSource>>,
source: string,
): string[] {
return getRelationships(result, 'CALLS')
.filter((edge) => edge.source === source && edge.rel.reason === 'callable-value-flow')
.map((edge) => result.graph.getNode(edge.rel.targetId)?.properties.qualifiedName ?? edge.target)
.sort();
}
function callableTargetIds(
result: Awaited<ReturnType<typeof runSource>>,
source: string,
): string[] {
return getRelationships(result, 'CALLS')
.filter((edge) => edge.source === source && edge.rel.reason === 'callable-value-flow')
.map((edge) => edge.rel.targetId)
.sort();
}
describe('callable value flow', () => {
it('governs every registered language provider', () => {
expect(Object.keys(CALLABLE_FLOW_PROVIDER_COVERAGE).sort()).toEqual(
Object.keys(providers).sort(),
);
});
it('does not overstate Objective-C callable-value-flow coverage', () => {
expect(CALLABLE_FLOW_PROVIDER_COVERAGE[SupportedLanguages.ObjectiveC]).toBe('not-applicable');
expect(
PROVIDER_FLOW_CASES.some(({ language }) => language === SupportedLanguages.ObjectiveC),
).toBe(false);
});
it.each(PROVIDER_FLOW_CASES)(
'resolves assign → copy → argument → invoke for $language',
async ({ extension, source, caller, target }) => {
const result = await runSource(extension, source);
expect(callsFrom(result, caller)).toContainEqual({
target,
reason: 'callable-value-flow',
});
},
90_000,
);
it('resolves C function pointers, pointer copies, pointer-to-pointer loads, and two wrappers', async () => {
const result = await runSource(
'c',
`
void target(void) {}
void callback(void) {}
void installed_target(void) {}
void invoke(void (*callback)(void)) { callback(); }
void outer(void (*cb)(void)) { invoke(cb); }
void install(void (**slot)(void)) { *slot = &installed_target; }
int entry(void) {
void (*fp)(void) = &target;
void (*fp2)(void) = fp;
void (**slot)(void) = &fp2;
fp();
(*fp2)();
(*slot)();
(**slot)();
invoke(*slot);
outer(target);
void (*installed)(void) = &callback;
install(&installed);
installed();
return 0;
}
`,
);
expect(callsFrom(result, 'entry')).toEqual(
expect.arrayContaining([
{ target: 'target', reason: 'callable-value-flow' },
{ target: 'installed_target', reason: 'callable-value-flow' },
expect.objectContaining({ target: 'invoke' }),
expect.objectContaining({ target: 'outer' }),
expect.objectContaining({ target: 'install' }),
]),
);
expect(callsFrom(result, 'invoke')).toContainEqual({
target: 'target',
reason: 'callable-value-flow',
});
expect(callsFrom(result, 'invoke').some((edge) => edge.target === 'callback')).toBe(false);
expect(callableCallSiteLines(result, 'entry', 'target')).toEqual([13, 14, 15, 16]);
expect(callableCallSiteLines(result, 'entry', 'installed_target')).toEqual([21]);
expect(callableCallSiteLines(result, 'invoke', 'target')).toEqual([5]);
}, 60_000);
it('joins C prototypes to cross-file definitions without leaking a static decoy', async () => {
const result = await runSources({
'target.c': 'void target(void) {}\n',
'decoy.c': 'static void target(void) {}\n',
'wrapper.c': 'void invoke(void (*callback)(void)) { callback(); }\n',
'main.c': `
void target(void);
void invoke(void (*callback)(void));
int main(void) {
void (*assigned)(void) = target;
invoke(assigned);
return 0;
}
`,
});
const callbackEdges = getRelationships(result, 'CALLS').filter(
(edge) =>
edge.source === 'invoke' &&
edge.target === 'target' &&
edge.rel.reason === 'callable-value-flow',
);
expect(callbackEdges).toHaveLength(1);
expect(callbackEdges[0]!.sourceFilePath).toBe('wrapper.c');
expect(callbackEdges[0]!.targetFilePath).toBe('target.c');
}, 60_000);
it('uses a C++ function-pointer signature to select one overload and suppresses untyped overload sets', async () => {
const result = await runSource(
'cpp',
`
void target(int) {}
void target(double) {}
int entry() {
void (*typed)(int) = &target;
auto unresolved = target;
typed(1);
unresolved(1);
return 0;
}
`,
);
expect(callableTargetQualifiedNames(result, 'entry')).toEqual(['target']);
expect(callableCallSiteLines(result, 'entry', 'target')).toEqual([8]);
const targetEdge = getRelationships(result, 'CALLS').find(
(edge) =>
edge.source === 'entry' &&
edge.target === 'target' &&
edge.rel.reason === 'callable-value-flow',
);
expect(result.graph.getNode(targetEdge!.rel.targetId)?.properties.parameterTypes).toEqual([
'int',
]);
}, 60_000);
it('retains C++ pointer and member-pointer signatures across split declaration and assignment', async () => {
const result = await runSource(
'cpp',
`
void target(int) {}
void target(double) {}
struct Base {
virtual void run() const {}
virtual void run() {}
};
struct Derived : Base {
void run() const override {}
void run() override {}
};
void invoke(void (*callback)(int), Derived& value, void (Base::*member)() const) {
callback(1);
(value.*member)();
}
int entry() {
void (*callback)(int);
callback = &target;
void (Base::*member)() const;
member = &Base::run;
Derived value;
invoke(callback, value, member);
return 0;
}
`,
{ skipGraphPhases: false },
);
const targets = getRelationships(result, 'CALLS')
.filter((edge) => edge.source === 'invoke' && edge.rel.reason === 'callable-value-flow')
.map((edge) => ({
id: edge.rel.targetId,
properties: result.graph.getNode(edge.rel.targetId)?.properties,
}));
expect(targets).toHaveLength(2);
expect(
targets.some(
({ id, properties }) =>
id.includes('target') &&
JSON.stringify(properties?.parameterTypes) === JSON.stringify(['int']),
),
).toBe(true);
expect(
targets.some(
({ id, properties }) => id.includes('Derived.run') && properties?.isConst === true,
),
).toBe(true);
expect(
targets.some(
({ id, properties }) =>
JSON.stringify(properties?.parameterTypes) === JSON.stringify(['double']) ||
(id.includes('Derived.run') && properties?.isConst !== true),
),
).toBe(false);
}, 90_000);
it('resolves an imported C++ function assigned through an auto pointer', async () => {
const result = await runSources({
'target.hpp': 'void target();\n',
'target.cpp': '#include "target.hpp"\nvoid target() {}\n',
'main.cpp': '#include "target.hpp"\nint entry() { auto callback = &target; callback(); }\n',
});
const targetEdges = getRelationships(result, 'CALLS').filter(
(edge) =>
edge.source === 'entry' &&
edge.target === 'target' &&
edge.rel.reason === 'callable-value-flow',
);
expect(targetEdges).toHaveLength(1);
expect(result.graph.getNode(targetEdges[0]!.rel.targetId)?.properties.filePath).toBe(
'target.cpp',
);
}, 90_000);
it('dispatches C++ member pointers through object/reference and pointer receivers to the virtual override', async () => {
const result = await runSource(
'cpp',
`
struct Base {
virtual void run() {}
};
struct Derived : Base {
void run() override {}
};
void invoke(Derived& value, void (Base::*member)()) {
(value.*member)();
Derived* pointer = &value;
(pointer->*member)();
}
int entry() {
Derived value;
auto member = &Base::run;
invoke(value, member);
return 0;
}
`,
{ skipGraphPhases: false },
);
const targets = callableTargetIds(result, 'invoke');
expect(targets).toHaveLength(2);
expect(new Set(targets)).toEqual(new Set(['Method:main.cpp:Derived.run#0']));
}, 90_000);
it('resolves (obj->*ptr)() regardless of tree-sitter ERROR-recovery grouping (#2522 review)', async () => {
const result = await runSource(
'cpp',
`
struct Base {
virtual void run() {}
};
struct Derived : Base {
void run() override {}
};
void invoke(Derived& v, void (Base::*ptr)()) {
Derived* obj = &v;
(obj->*ptr)();
}
int entry() {
Derived v;
auto ptr = &Base::run;
invoke(v, ptr);
return 0;
}
`,
{ skipGraphPhases: false },
);
const targets = callableTargetIds(result, 'invoke');
expect(targets).toHaveLength(1);
expect(new Set(targets)).toEqual(new Set(['Method:main.cpp:Derived.run#0']));
}, 90_000);
it('keeps non-virtual C++ member pointers exact and dispatches virtual overload/cv signatures precisely', async () => {
const result = await runSource(
'cpp',
`
struct Base {
virtual void run(int) {}
virtual void run(double) {}
void fixed() {}
virtual void cv() const {}
virtual void cv() {}
};
struct Derived : Base {
void run(int) override {}
void run(double) override {}
void fixed() {}
void cv() const override {}
void cv() override {}
};
void invoke(
Derived& value,
void (Base::*runMember)(int),
void (Base::*fixedMember)(),
void (Base::*constMember)() const,
void (Base::*mutableMember)()
) {
(value.*runMember)(1);
(value.*fixedMember)();
(value.*constMember)();
(value.*mutableMember)();
}
int entry() {
Derived value;
void (Base::*runMember)(int) = &Base::run;
void (Base::*fixedMember)() = &Base::fixed;
void (Base::*constMember)() const = &Base::cv;
void (Base::*mutableMember)() = &Base::cv;
invoke(value, runMember, fixedMember, constMember, mutableMember);
return 0;
}
`,
{ skipGraphPhases: false },
);
const targets = getRelationships(result, 'CALLS')
.filter((edge) => edge.source === 'invoke' && edge.rel.reason === 'callable-value-flow')
.map((edge) => ({
id: edge.rel.targetId,
properties: result.graph.getNode(edge.rel.targetId)?.properties,
}));
expect(targets).toHaveLength(4);
expect(
targets.some(
({ id, properties }) =>
id.includes('Derived.run') &&
JSON.stringify(properties?.parameterTypes) === JSON.stringify(['int']),
),
).toBe(true);
expect(
targets.some(
({ id, properties }) =>
id.includes('Derived.run') &&
JSON.stringify(properties?.parameterTypes) === JSON.stringify(['double']),
),
).toBe(false);
expect(targets.some(({ id }) => id.includes('Base.fixed'))).toBe(true);
expect(targets.some(({ id }) => id.includes('Derived.fixed'))).toBe(false);
expect(
targets.some(
({ id, properties }) => id.includes('Derived.cv') && properties?.isConst === true,
),
).toBe(true);
expect(
targets.some(
({ id, properties }) => id.includes('Derived.cv') && properties?.isConst !== true,
),
).toBe(true);
}, 90_000);
it('resolves C++ function references and references to pointer variables', async () => {
const result = await runSource(
'cpp',
`
void target() {}
void invoke(void (&cb)()) { cb(); }
int entry() {
void (*fp)(void) = &target;
void (&fr)(void) = target;
void (*&fpr)(void) = fp;
fr();
fpr();
invoke(fr);
return 0;
}
`,
);
expect(callsFrom(result, 'entry')).toEqual(
expect.arrayContaining([
{ target: 'target', reason: 'callable-value-flow' },
expect.objectContaining({ target: 'invoke' }),
]),
);
expect(callsFrom(result, 'invoke')).toContainEqual({
target: 'target',
reason: 'callable-value-flow',
});
expect(callableCallSiteLines(result, 'entry', 'target')).toEqual([9, 10]);
expect(callableCallSiteLines(result, 'invoke', 'target')).toEqual([3]);
}, 60_000);
it('propagates a C++ function reference through a cross-file prototype and formal', async () => {
const result = await runSources({
'target.cpp': 'void target() {}\n',
'decoy.cpp': 'static void target() {}\n',
'wrapper.cpp': `
using Callback = void (*)();
void invoke(Callback callback) { callback(); }
`,
'main.cpp': `
using Callback = void (*)();
void target();
void invoke(Callback callback);
int main() {
void (&reference)() = target;
invoke(reference);
return 0;
}
`,
});
const callbackEdges = getRelationships(result, 'CALLS').filter(
(edge) =>
edge.source === 'invoke' &&
edge.target === 'target' &&
edge.rel.reason === 'callable-value-flow',
);
expect(callbackEdges).toHaveLength(1);
expect(callbackEdges[0]!.sourceFilePath).toBe('wrapper.cpp');
expect(callbackEdges[0]!.targetFilePath).toBe('target.cpp');
}, 60_000);
it('resolves TypeScript callable assignment, copy, and actual-to-formal invocation', async () => {
const result = await runSource(
'ts',
`
function target(): void {}
function invoke(callback: () => void): void { callback(); }
function outer(cb: () => void): void { invoke(cb); }
const first = target;
const second = first;
second();
outer(second);
`,
);
expect(callsFrom(result, 'main.ts')).toEqual(
expect.arrayContaining([
{ target: 'target', reason: 'callable-value-flow' },
expect.objectContaining({ target: 'outer' }),
]),
);
expect(callsFrom(result, 'invoke')).toContainEqual({
target: 'target',
reason: 'callable-value-flow',
});
}, 60_000);
it('propagates an imported callable through a cross-file wrapper without selecting a decoy', async () => {
const result = await runSources({
'target.ts': `export function target(): void {}`,
'decoy.ts': `export function target(): void {}`,
'wrapper.ts': `
export function invoke(callback: () => void): void {
callback();
}
`,
'main.ts': `
import { target } from './target';
import { invoke } from './wrapper';
const assigned = target;
invoke(assigned);
`,
});
const edge = getRelationships(result, 'CALLS').find(
(candidate) =>
candidate.source === 'invoke' &&
candidate.target === 'target' &&
candidate.rel.reason === 'callable-value-flow',
);
expect(edge).toBeDefined();
expect(edge!.sourceFilePath).toBe('wrapper.ts');
expect(edge!.targetFilePath).toBe('target.ts');
expect(
getRelationships(result, 'CALLS').some(
(candidate) => candidate.source === 'invoke' && candidate.targetFilePath === 'decoy.ts',
),
).toBe(false);
}, 60_000);
it('resolves a bound method assigned to a variable and passed into a wrapper', async () => {
const result = await runSource(
'ts',
`
class Worker {
run(): void {}
}
class Decoy {
run(): void {}
}
function invoke(callback: () => void): void { callback(); }
const worker = new Worker();
const assigned = worker.run;
invoke(assigned);
`,
);
const edge = getRelationships(result, 'CALLS').find(
(candidate) =>
candidate.source === 'invoke' &&
candidate.target === 'run' &&
candidate.rel.reason === 'callable-value-flow',
);
expect(edge).toBeDefined();
expect(edge!.targetLabel).toBe('Method');
expect(edge!.rel.targetId).toContain('Worker.run');
}, 60_000);
it('does not reinterpret call results passed as arguments as callable designators', async () => {
const result = await runSource(
'ts',
`
function target(): void {}
function factory(): () => void { return target; }
function invoke(callback: () => void): void { callback(); }
invoke(factory());
`,
);
expect(callableCallSiteLines(result, 'invoke', 'target')).toEqual([]);
expect(callableCallSiteLines(result, 'invoke', 'factory')).toEqual([]);
}, 60_000);
it('resolves COBOL procedure pointers in sequence-numbered fixed format (#2522 review)', async () => {
const result = await runSource(
'cbl',
`
000100 IDENTIFICATION DIVISION.
000200 PROGRAM-ID. MAIN.
000300 DATA DIVISION.
000400 WORKING-STORAGE SECTION.
000500 01 CALLBACK USAGE IS PROCEDURE-POINTER.
000600 PROCEDURE DIVISION.
000700 SET CALLBACK TO ENTRY "TARGET".
000800 CALL "INVOKE" USING CALLBACK.
000900 STOP RUN.
001000 END PROGRAM MAIN.
001100 IDENTIFICATION DIVISION.
001200 PROGRAM-ID. INVOKE.
001300 DATA DIVISION.
001400 LINKAGE SECTION.
001500 01 CB USAGE IS PROCEDURE-POINTER.
001600 PROCEDURE DIVISION USING CB.
001700 CALL CB.
001800 GOBACK.
001900 END PROGRAM INVOKE.
002000 IDENTIFICATION DIVISION.
002100 PROGRAM-ID. TARGET.
002200 PROCEDURE DIVISION.
002300 GOBACK.
002400 END PROGRAM TARGET.
`,
);
expect(callableCallSiteLines(result, 'INVOKE', 'TARGET')).toEqual([18]);
}, 60_000);
it('propagates COBOL procedure pointers through SET x TO y copies (#2522 review)', async () => {
const result = await runSource(
'cbl',
`
>>SOURCE FORMAT FREE
IDENTIFICATION DIVISION.
PROGRAM-ID. MAIN.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 P1 USAGE PROCEDURE-POINTER.
01 P2 USAGE PROCEDURE-POINTER.
PROCEDURE DIVISION.
SET P1 TO ENTRY "TARGET".
SET P2 TO P1.
CALL P2.
STOP RUN.
END PROGRAM MAIN.
IDENTIFICATION DIVISION.
PROGRAM-ID. TARGET.
PROCEDURE DIVISION.
GOBACK.
END PROGRAM TARGET.
`,
);
expect(callableCallSiteLines(result, 'MAIN', 'TARGET')).toEqual([12]);
}, 60_000);
it('ignores commented-out COBOL SET statements (#2522 review)', async () => {
const result = await runSource(
'cbl',
`
000100 IDENTIFICATION DIVISION.
000200 PROGRAM-ID. MAIN.
000300 DATA DIVISION.
000400 WORKING-STORAGE SECTION.
000500 01 CALLBACK USAGE IS PROCEDURE-POINTER.
000600 PROCEDURE DIVISION.
000700 SET CALLBACK TO ENTRY "TARGET".
000750* SET CALLBACK TO ENTRY "MAIN".
000800 CALL "INVOKE" USING CALLBACK.
000900 STOP RUN.
001000 END PROGRAM MAIN.
001100 IDENTIFICATION DIVISION.
001200 PROGRAM-ID. INVOKE.
001300 DATA DIVISION.
001400 LINKAGE SECTION.
001500 01 CB USAGE IS PROCEDURE-POINTER.
001600 PROCEDURE DIVISION USING CB.
001700 CALL CB.
001800 GOBACK.
001900 END PROGRAM INVOKE.
002000 IDENTIFICATION DIVISION.
002100 PROGRAM-ID. TARGET.
002200 PROCEDURE DIVISION.
002300 GOBACK.
002400 END PROGRAM TARGET.
`,
);
// The dead SET in the comment must not add MAIN to the callee set.
expect(callableCallSiteLines(result, 'INVOKE', 'MAIN')).toEqual([]);
expect(callableCallSiteLines(result, 'INVOKE', 'TARGET')).toEqual([19]);
}, 60_000);
it('does not resolve a Rust unit enum variant seeded as a callable value (#2522 review)', async () => {
const result = await runSource(
'rs',
`
enum Shape {
Square,
}
fn dispatch(cb: fn()) {
cb();
}
fn entry() {
let x = Shape::Square;
let _ = x;
}
fn main() {
entry();
dispatch(main);
}
`,
);
// Shape::Square is a value, not a callable — the qualified-name guard
// must keep the over-captured seed edge-free.
expect(
getRelationships(result, 'CALLS').filter(
(edge) => edge.rel.reason === 'callable-value-flow' && edge.target === 'Square',
),
).toEqual([]);
}, 60_000);
it('pairs Go multi-value := positionally instead of cross-wiring (#2522 review)', async () => {
const result = await runSource(
'go',
`package main
func targetFunc() {}
func otherFunc() {}
func entry() {
a, b := targetFunc, otherFunc
a()
b()
}
`,
);
expect(callableCallSiteLines(result, 'entry', 'targetFunc')).toEqual([8]);
expect(callableCallSiteLines(result, 'entry', 'otherFunc')).toEqual([9]);
}, 60_000);
it('does not treat a bare Ruby identifier as a callable reference — it is a call (#2522 review)', async () => {
const result = await runSource(
'rb',
`
def process
'done'
end
def dispatch(cb)
cb.call
end
action = process
dispatch(action)
`,
);
// `action` holds process's RETURN VALUE (a String); a CALLS edge from
// dispatch to process here is over-linking.
expect(callableCallSiteLines(result, 'dispatch', 'process')).toEqual([]);
expect(
getRelationships(result, 'CALLS').filter(
(edge) => edge.rel.reason === 'callable-value-flow' && edge.target === 'process',
),
).toEqual([]);
}, 60_000);
it('resolves the C ops-vtable pattern: struct-field pointer stored then called (#2522 review)', async () => {
const result = await runSource(
'c',
`
void handler(int x) {}
struct ops { void (*run)(int); };
int entry(struct ops *o) {
o->run = handler;
o->run(1);
return 0;
}
`,
);
expect(callableCallSiteLines(result, 'entry', 'handler')).toEqual([6]);
}, 60_000);
it('resolves a file-scope function pointer assigned in one function and called in another (#2522 review)', async () => {
const result = await runSource(
'c',
`
void handler(int x) {}
static void (*fp)(int);
void init(void) { fp = handler; }
void run(void) { fp(1); }
`,
);
expect(callableCallSiteLines(result, 'run', 'handler')).toEqual([5]);
}, 60_000);
it('binds variable-indexed function-pointer array cells to the array, not the index (#2522 review)', async () => {
const result = await runSource(
'c',
`
void handler(int x) {}
int entry(void) {
void (*tbl[2])(int);
int i = 0;
tbl[i] = handler;
tbl[i](7);
return 0;
}
`,
);
expect(callableCallSiteLines(result, 'entry', 'handler')).toEqual([7]);
}, 60_000);
it('does not store a factory function itself through a C pointer assignment', async () => {
const result = await runSource(
'c',
`
void initial(void) {}
void produced(void) {}
void (*factory(void))(void) { return &produced; }
void install(void (**slot)(void)) { *slot = factory(); }
int entry(void) {
void (*callback)(void) = &initial;
install(&callback);
callback();
return 0;
}
`,
);
expect(callableCallSiteLines(result, 'entry', 'factory')).toEqual([]);
expect(callableCallSiteLines(result, 'entry', 'initial')).toEqual([9]);
}, 60_000);
it('keeps a direct call outside a nested same-name callable binding on ordinary resolution', async () => {
const result = await runSource(
'ts',
`
function target(): void {}
function replacement(): void {}
function entry(): void {
target();
{
const target = replacement;
target();
}
}
`,
);
expect(
getRelationships(result, 'CALLS').some(
(edge) => edge.source === 'entry' && edge.target === 'target',
),
).toBe(true);
expect(callableCallSiteLines(result, 'entry', 'replacement')).toEqual([8]);
}, 60_000);
it('keeps the declaration as a target when a function is reassigned through its own name and the RHS is unresolvable (#2522 review)', async () => {
const result = await runSource(
'js',
`
import { fancyGreet } from 'external-pkg';
function greet() {}
function entry() {
greet = fancyGreet;
greet();
}
`,
);
expect(callableCallSiteLines(result, 'entry', 'greet')).toEqual([6]);
}, 60_000);
it('unions the declaration with a resolvable reassignment target (inclusion semantics)', async () => {
const result = await runSource(
'js',
`
function greet() {}
function fancy() {}
function entry() {
greet = fancy;
greet();
}
`,
);
expect(callableCallSiteLines(result, 'entry', 'greet')).toEqual([6]);
expect(callableCallSiteLines(result, 'entry', 'fancy')).toEqual([6]);
}, 60_000);
it.each([
{
language: 'Python',
extension: 'py',
source: `
def target():
pass
def entry(enabled):
if enabled:
callback = target
callback()
`,
},
{
language: 'PHP',
extension: 'php',
source: `<?php
function target() {}
function entry($enabled) {
if ($enabled) {
$callback = target(...);
}
$callback();
}
`,
},
{
language: 'Ruby',
extension: 'rb',
source: `
def target; end
def entry(enabled)
if enabled
callback = method(:target)
end
callback.call
end
`,
},
{
language: 'Kotlin',
extension: 'kt',
caller: 'invoke',
source: `
fun target() {}
fun other() {}
fun invoke(callback: () -> Unit) { callback() }
fun entry(enabled: Boolean) {
var chosen = ::other
if (enabled) {
chosen = ::target
}
invoke(chosen)
}
`,
},
{
language: 'C#',
extension: 'cs',
source: `
class Demo {
static void target() {}
static void entry(bool enabled) {
System.Action callback = () => {};
if (enabled) {
callback = target;
}
callback();
}
}
`,
},
{
language: 'Swift',
extension: 'swift',
caller: 'invoke',
source: `
func target() {}
func other() {}
func invoke(_ callback: () -> Void) { callback() }
func entry(enabled: Bool) {
var chosen = other
if enabled {
chosen = target
}
invoke(chosen)
}
`,
},
{
language: 'Dart',
extension: 'dart',
source: `
void target() {}
void entry(bool enabled) {
var callback = () {};
if (enabled) {
callback = target;
}
callback();
}
`,
},
])(
'keeps function-scoped callable assignments visible outside nested $language blocks',
async ({ extension, source, caller = 'entry' }) => {
const result = await runSource(extension, source);
expect(callsFrom(result, caller)).toContainEqual({
target: 'target',
reason: 'callable-value-flow',
});
},
60_000,
);
it('does not leak actuals between distinct C++ wrapper overloads', async () => {
const result = await runSource(
'cpp',
`
void target() {}
void invoke(void (*callback)()) { callback(); }
void invoke(void (*callback)(), int) { callback(); }
int entry() {
invoke(target);
return 0;
}
`,
);
const sourceParameterCounts = getRelationships(result, 'CALLS')
.filter((edge) => edge.target === 'target' && edge.rel.reason === 'callable-value-flow')
.map((edge) => result.graph.getNode(edge.rel.sourceId)?.properties.parameterCount)
.sort();
expect(sourceParameterCounts).toEqual([1]);
}, 90_000);
it('terminates copy cycles and preserves the reachable callable target', async () => {
const result = await runSource(
'ts',
`
function target(): void {}
let first = target;
let second = first;
first = second;
second = first;
second();
`,
);
expect(callsFrom(result, 'main.ts')).toContainEqual({
target: 'target',
reason: 'callable-value-flow',
});
}, 60_000);
it('suppresses rather than partially emitting an over-cap candidate set', async () => {
const targets = Array.from({ length: 33 }, (_, index) => `target${index}`);
const result = await runSource(
'js',
[
...targets.map((name) => `function ${name}() {}`),
`let callback = ${targets[0]};`,
...targets.slice(1).map((name) => `callback = ${name};`),
'callback();',
].join('\n'),
);
expect(callableTargetIds(result, 'main.js')).toEqual([]);
}, 60_000);
it('does not steal an ordinary Java method whose name matches a callable protocol', async () => {
const result = await runSource(
'java',
`
class Worker {
void run() {}
}
class Demo {
static void entry() {
Worker worker = new Worker();
worker.run();
}
}
`,
);
expect(callsFrom(result, 'entry')).toContainEqual(expect.objectContaining({ target: 'run' }));
expect(
callsFrom(result, 'entry').some(
(edge) => edge.target === 'Worker' && edge.reason === 'callable-value-flow',
),
).toBe(false);
}, 60_000);
it('does not treat a callable invocation result as the callable itself', async () => {
const result = await runSource(
'ts',
`
function target(): number { return 1; }
const value = target();
value();
`,
);
expect(callsFrom(result, 'main.ts')).not.toContainEqual({
target: 'target',
reason: 'callable-value-flow',
});
}, 60_000);
it('replays identical callable-flow semantics from the durable warm parse cache', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-callable-warm-repo-'));
const storage = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-callable-warm-store-'));
try {
fs.writeFileSync(
path.join(root, 'main.ts'),
`
function target(): void {}
function invoke(callback: () => void): void { callback(); }
const assigned = target;
invoke(assigned);
`,
'utf8',
);
const coldCache: ParseCache = {
version: PARSE_CACHE_VERSION,
entries: new Map(),
usedKeys: new Set(),
storagePath: storage,
onDiskKeys: new Set(),
};
const cold = await runPipelineFromRepo(root, () => {}, {
skipGraphPhases: true,
parseCache: coldCache,
});
const savedKeys = await saveParseCache(storage, coldCache);
await pruneAndSaveDurableParsedFileStore(
getDurableParsedFileDir(storage),
PARSE_CACHE_VERSION,
new Set(savedKeys),
);
const warmCache = await loadParseCache(storage);
const warm = await runPipelineFromRepo(root, () => {}, {
skipGraphPhases: true,
parseCache: warmCache,
});
const project = (result: Awaited<ReturnType<typeof runSource>>) =>
getRelationships(result, 'CALLS')
.filter((edge) => edge.rel.reason === 'callable-value-flow')
.map(
(edge) =>
`${edge.sourceFilePath}:${edge.source}->${edge.targetFilePath}:${edge.target}`,
)
.sort();
expect(project(warm)).toEqual(project(cold));
expect(project(warm)).toEqual(['main.ts:invoke->main.ts:target']);
} finally {
fs.rmSync(root, { recursive: true, force: true });
fs.rmSync(storage, { recursive: true, force: true });
}
}, 120_000);
it('replays closure-binding resolution from the durable warm parse cache (#2693)', async () => {
// The #2693 captures are provider-synthesized (Dart's fieldless
// `initialized_identifier` seed) and the function-local closure `Function`
// node is minted at parse time — both are replayed VERBATIM from the parse
// cache, so a serialization change would surface only on the SECOND
// analyze. Every other test in this PR runs cold and would stay green.
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-closure-warm-repo-'));
const storage = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-closure-warm-store-'));
try {
fs.writeFileSync(
path.join(root, 'app.dart'),
'var handler = (int x) => x;\n\nint caller() {\n return handler(1);\n}\n',
'utf8',
);
fs.writeFileSync(
path.join(root, 'App.kt'),
'val handler = { x: Int -> x }\n\nfun caller(): Int {\n return handler(1)\n}\n',
'utf8',
);
const coldCache: ParseCache = {
version: PARSE_CACHE_VERSION,
entries: new Map(),
usedKeys: new Set(),
storagePath: storage,
onDiskKeys: new Set(),
};
const cold = await runPipelineFromRepo(root, () => {}, {
skipGraphPhases: true,
parseCache: coldCache,
});
const savedKeys = await saveParseCache(storage, coldCache);
await pruneAndSaveDurableParsedFileStore(
getDurableParsedFileDir(storage),
PARSE_CACHE_VERSION,
new Set(savedKeys),
);
const warmCache = await loadParseCache(storage);
const warm = await runPipelineFromRepo(root, () => {}, {
skipGraphPhases: true,
parseCache: warmCache,
});
const project = (result: Awaited<ReturnType<typeof runSource>>) =>
getRelationships(result, 'CALLS')
.map(
(edge) =>
`${edge.sourceFilePath}:${edge.source}->${edge.targetFilePath}:${edge.target}`,
)
.sort();
expect(project(warm)).toEqual(project(cold));
expect(project(warm)).toEqual([
'App.kt:caller->App.kt:handler',
'app.dart:caller->app.dart:handler',
]);
} finally {
fs.rmSync(root, { recursive: true, force: true });
fs.rmSync(storage, { recursive: true, force: true });
}
}, 120_000);
it('keeps normal/PDG targets identical and stamps calleeIds at the indirect invocation', async () => {
const source = `
function target(): void {}
function invoke(callback: () => void): void { callback(); }
const assigned = target;
invoke(assigned);
`;
const normal = await runSource('ts', source);
const pdg = await runSource('ts', source, { pdg: true });
const project = (result: Awaited<ReturnType<typeof runSource>>) =>
callsFrom(result, 'invoke')
.filter((edge) => edge.reason === 'callable-value-flow')
.map((edge) => edge.target)
.sort();
expect(project(pdg)).toEqual(project(normal));
expect(project(pdg)).toEqual(['target']);
const matchingBlocks: Array<Record<string, unknown>> = [];
pdg.graph.forEachNode((node) => {
if (
node.label === 'BasicBlock' &&
typeof node.properties.text === 'string' &&
node.properties.text.includes('callback()')
) {
matchingBlocks.push(node.properties);
}
});
expect(matchingBlocks).not.toHaveLength(0);
expect(
matchingBlocks.some(
(properties) =>
typeof properties.calleeIds === 'string' && properties.calleeIds.includes('target'),
),
).toBe(true);
}, 90_000);
it('does not read a Zig member call `x.f(arg)` as a direct call named `f` (Zig PR review, F2)', async () => {
// tree-sitter-zig spells `field_expression` as `object:`/`member:`. Read as
// a direct call, `self.slot.release()` next to the container alias
// `pub const release = deinit;` minted a `deinit → deinit` self-loop, and
// every `.init(...)` fanned its arguments out to all same-named callables
// (4,761 cap warnings on Lightpanda). A receiver-typed member call must
// still carry its actual into the formal AFTER the implicit `self`.
const result = await runSource(
'zig',
`
pub const Slot = struct {
n: u32 = 0,
pub fn release(self: *Slot) void { self.n = 0; }
};
pub const Global = struct {
slot: *Slot,
pub fn deinit(self: Global) void {
self.slot.release();
}
pub const release = deinit;
};
fn target(x: u32) void { _ = x; }
fn explicitTarget(x: u32) void { _ = x; }
fn namespaceTarget(x: u32) void { _ = x; }
pub const Runner = struct {
pub fn run(self: *Runner, cb: *const fn (u32) void) void { _ = self; cb(2); }
pub fn init(cb: *const fn (u32) void) Runner { cb(3); return .{}; }
};
pub const Decoy = struct {
pub fn init(cb: *const fn (u32) void) Decoy { cb(4); return .{}; }
};
pub const helpers = struct {
pub fn apply(cb: *const fn (u32) void) void { cb(5); }
};
pub fn main() void {
var r: Runner = undefined;
r.run(target);
Runner.run(&r, explicitTarget);
helpers.apply(namespaceTarget);
const made: Runner = .init(target);
_ = made;
}
`,
);
// (i) no self-loop through the alias: `self.slot.release()` is a call on
// Slot, not an invoke through Global's `release` name-cell.
expect(callsFrom(result, 'deinit')).not.toContainEqual({
target: 'deinit',
reason: 'callable-value-flow',
});
// (ii) both spellings of a method call reach formal `cb` (index 1, after
// the explicit `self`): the implicit receiver of `r.run(target)` is
// prepended as actual 0, the explicit `Runner.run(&r, explicitTarget)`
// already carries it. Dropping `self` from the formals instead lined up
// only the implicit form (PR #1432 review).
expect(callsFrom(result, 'run')).toContainEqual({
target: 'target',
reason: 'callable-value-flow',
});
expect(callsFrom(result, 'run')).toContainEqual({
target: 'explicitTarget',
reason: 'callable-value-flow',
});
// A namespace receiver passes nothing implicitly: `helpers.apply(cb)` must
// not shift its actual off formal `cb@0`.
expect(callsFrom(result, 'apply')).toContainEqual({
target: 'namespaceTarget',
reason: 'callable-value-flow',
});
// (iii) decl literal `.init(target)` names no callee by simple name — the
// unrelated `Decoy.init` must not gain a flow edge to `target`.
expect(
getRelationships(result, 'CALLS').filter(
(edge) =>
edge.rel.sourceId.includes('Decoy.init') &&
edge.target === 'target' &&
edge.rel.reason === 'callable-value-flow',
),
).toEqual([]);
}, 60_000);
});