mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-03 02:21:44 +00:00
* 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>
1607 lines
43 KiB
TypeScript
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) = ⌖
|
|
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) = ⌖
|
|
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 = ⌖
|
|
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 = ⌖ 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) = ⌖
|
|
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);
|
|
});
|