mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-06 02:49:56 +00:00
fix(impact): signal the per-statement site cap on BasicBlock.callees
When a statement's call sites are truncated at DEFAULT_PDG_MAX_SITES_PER_STATEMENT
the recorded callee list is incomplete, so over-cap callees were silently absent
from BasicBlock.callees — making the impact bridge label a reachable-but-dropped
callee `unproven-bridge` with no signal. calleesOfBlock now emits a reserved
sentinel ('*', not a valid identifier leaf) for a capped block, and the bridge
treats a slice containing it as callee-unknown → keeps reach callgraph-equal
(proven) rather than under-proving. No fixture/real block hits the cap, so the
emit byte-identity fingerprint is unchanged (gate stays green; no re-index).
Addresses PR #2227 tri-review finding (P3, adversarial).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2a511ebd88
commit
64049b0c99
4 changed files with 109 additions and 1 deletions
|
|
@ -28,8 +28,20 @@ import {
|
|||
NO_IPDOM,
|
||||
} from './post-dominators.js';
|
||||
import { augmentForPostDom } from './synthetic-escape.js';
|
||||
import { DEFAULT_PDG_MAX_SITES_PER_STATEMENT } from './visitors/call-site-harvest.js';
|
||||
import type { BasicBlockData, BindingEntry, FunctionCfg } from './types.js';
|
||||
|
||||
/**
|
||||
* Reserved token placed in `BasicBlock.callees` when a statement's call sites
|
||||
* were truncated at {@link DEFAULT_PDG_MAX_SITES_PER_STATEMENT}: the recorded
|
||||
* callee list is then INCOMPLETE, so over-cap callees are absent. `*` is not a
|
||||
* valid identifier leaf, so it cannot collide with a real callee name. The
|
||||
* impact bridge treats a slice containing this sentinel as "callees unknown" and
|
||||
* keeps reach callgraph-equal (proven), rather than falsely labeling an
|
||||
* absent-but-real callee `unproven-bridge`.
|
||||
*/
|
||||
export const CALLEES_TRUNCATED_SENTINEL = '*';
|
||||
|
||||
/**
|
||||
* Default per-function CFG edge cap. A pathological generated function could
|
||||
* otherwise emit an unbounded edge set; the cap bounds graph growth and is
|
||||
|
|
@ -269,6 +281,13 @@ export const hasEmitSafeFacts = (cfg: FunctionCfg): boolean => {
|
|||
export function calleesOfBlock(block: BasicBlockData): string {
|
||||
const names = new Set<string>();
|
||||
for (const stmt of block.statements ?? []) {
|
||||
// A statement whose recorded sites reached the per-statement cap may have
|
||||
// dropped over-cap callees (the harvester stops at the cap). Flag the block
|
||||
// callee-unknown so the impact bridge keeps it callgraph-equal rather than
|
||||
// under-proving an absent-but-real callee.
|
||||
if ((stmt.sites?.length ?? 0) >= DEFAULT_PDG_MAX_SITES_PER_STATEMENT) {
|
||||
names.add(CALLEES_TRUNCATED_SENTINEL);
|
||||
}
|
||||
for (const site of stmt.sites ?? []) {
|
||||
if (site.kind === 'member-read') continue;
|
||||
const callee = site.callee;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
isLbugReady,
|
||||
} from '../../core/lbug/pool-adapter.js';
|
||||
import { isValidQueryParams } from '../../core/lbug/query-params.js';
|
||||
import { CALLEES_TRUNCATED_SENTINEL } from '../../core/ingestion/cfg/emit.js';
|
||||
import { isWalCorruptionError, WAL_RECOVERY_SUGGESTION } from '../../core/lbug/lbug-config.js';
|
||||
// Embedding imports are lazy (dynamic import) to avoid loading onnxruntime-node
|
||||
// at MCP server startup — crashes on unsupported Node ABI versions (#89)
|
||||
|
|
@ -271,7 +272,7 @@ interface PdgBridgeOptions {
|
|||
sliceCalleeNames?: ReadonlySet<string>;
|
||||
}
|
||||
|
||||
function pdgBridgeEvidenceForImpact(input: {
|
||||
export function pdgBridgeEvidenceForImpact(input: {
|
||||
bridge: PdgBridgeOptions;
|
||||
depth: number;
|
||||
calleeName: unknown;
|
||||
|
|
@ -295,6 +296,16 @@ function pdgBridgeEvidenceForImpact(input: {
|
|||
};
|
||||
}
|
||||
|
||||
// A slice block whose call sites were truncated at the per-statement cap has an
|
||||
// INCOMPLETE callee list, so absence from the set does not prove absence from
|
||||
// the slice. Keep such reach callgraph-equal rather than under-proving.
|
||||
if (sliceCalleeNames.has(CALLEES_TRUNCATED_SENTINEL)) {
|
||||
return {
|
||||
evidence: 'callgraph-bridge',
|
||||
basis: 'a slice block truncated its call sites — callee set is incomplete (callee-unknown)',
|
||||
};
|
||||
}
|
||||
|
||||
const name = typeof calleeName === 'string' ? calleeName : '';
|
||||
if (name && sliceCalleeNames.has(name)) {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -105,7 +105,9 @@ import {
|
|||
REPO_ID_HASH_LENGTH,
|
||||
parseListReposPagination,
|
||||
betterBridgeEvidence,
|
||||
pdgBridgeEvidenceForImpact,
|
||||
} from '../../src/mcp/local/local-backend.js';
|
||||
import { CALLEES_TRUNCATED_SENTINEL } from '../../src/core/ingestion/cfg/emit.js';
|
||||
import {
|
||||
listRegisteredRepos,
|
||||
cleanupOldKuzuFiles,
|
||||
|
|
@ -1718,6 +1720,25 @@ describe('LocalBackend impact mode (KTD1/KTD5/KTD12)', () => {
|
|||
expect(betterBridgeEvidence(unproven, unproven).evidence).toBe('unproven-bridge');
|
||||
});
|
||||
|
||||
it('pdgBridgeEvidenceForImpact treats a truncated-slice (sentinel) as callee-unknown → proven', () => {
|
||||
// A slice block that hit the per-statement site cap has an incomplete callee
|
||||
// list; the sentinel forces callgraph-equal so an absent-but-real callee is
|
||||
// not under-proven.
|
||||
const truncated = pdgBridgeEvidenceForImpact({
|
||||
bridge: { sliceCalleeNames: new Set([CALLEES_TRUNCATED_SENTINEL, 'foo']) },
|
||||
depth: 1,
|
||||
calleeName: 'unrelatedNotInSlice',
|
||||
});
|
||||
expect(truncated.evidence).toBe('callgraph-bridge');
|
||||
// Without the sentinel, a callee not in the slice is unproven.
|
||||
const notTruncated = pdgBridgeEvidenceForImpact({
|
||||
bridge: { sliceCalleeNames: new Set(['foo']) },
|
||||
depth: 1,
|
||||
calleeName: 'unrelatedNotInSlice',
|
||||
});
|
||||
expect(notTruncated.evidence).toBe('unproven-bridge');
|
||||
});
|
||||
|
||||
it("mode:'pdg' degrades gracefully when the slice-callees query fails (no bridge, no throw)", async () => {
|
||||
// calleesOfBlocks swallows a DB error and returns an empty set, so the bridge
|
||||
// is not built and the inter-procedural reach falls back to callgraph-equal —
|
||||
|
|
|
|||
57
gitnexus/test/unit/cfg-callees-of-block.test.ts
Normal file
57
gitnexus/test/unit/cfg-callees-of-block.test.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import { CALLEES_TRUNCATED_SENTINEL, calleesOfBlock } from '../../src/core/ingestion/cfg/emit.js';
|
||||
import { DEFAULT_PDG_MAX_SITES_PER_STATEMENT } from '../../src/core/ingestion/cfg/visitors/call-site-harvest.js';
|
||||
import type { BasicBlockData, SiteRecord } from '../../src/core/ingestion/cfg/types.js';
|
||||
|
||||
const callSite = (callee: string): SiteRecord => ({ kind: 'call', callee });
|
||||
|
||||
const block = (statements: BasicBlockData['statements']): BasicBlockData => ({
|
||||
index: 0,
|
||||
startLine: 1,
|
||||
endLine: 1,
|
||||
text: '',
|
||||
kind: 'normal',
|
||||
statements,
|
||||
});
|
||||
|
||||
describe('calleesOfBlock', () => {
|
||||
it('emits sorted, de-duplicated leaf callee names (dotted paths reduced to the leaf)', () => {
|
||||
const result = calleesOfBlock(
|
||||
block([
|
||||
{ line: 1, defs: [], uses: [], sites: [callSite('child_process.exec'), callSite('foo')] },
|
||||
{ line: 2, defs: [], uses: [], sites: [callSite('a.b.bar'), callSite('foo')] },
|
||||
]),
|
||||
);
|
||||
expect(result).toBe('bar exec foo');
|
||||
});
|
||||
|
||||
it('ignores member-read sites and sites without a callee', () => {
|
||||
const result = calleesOfBlock(
|
||||
block([
|
||||
{
|
||||
line: 1,
|
||||
defs: [],
|
||||
uses: [],
|
||||
sites: [{ kind: 'member-read', property: 'body' }, { kind: 'call' }, callSite('only')],
|
||||
},
|
||||
]),
|
||||
);
|
||||
expect(result).toBe('only');
|
||||
});
|
||||
|
||||
it('flags a block callee-unknown with the sentinel when a statement hits the site cap', () => {
|
||||
const cappedSites: SiteRecord[] = Array.from(
|
||||
{ length: DEFAULT_PDG_MAX_SITES_PER_STATEMENT },
|
||||
() => callSite('foo'),
|
||||
);
|
||||
const result = calleesOfBlock(block([{ line: 1, defs: [], uses: [], sites: cappedSites }]));
|
||||
// The sentinel sorts first ('*' < letters) and rides alongside the real names.
|
||||
expect(result.split(' ')).toContain(CALLEES_TRUNCATED_SENTINEL);
|
||||
expect(result.split(' ')).toContain('foo');
|
||||
});
|
||||
|
||||
it('returns an empty string for a block with no call sites', () => {
|
||||
expect(calleesOfBlock(block([{ line: 1, defs: [], uses: [] }]))).toBe('');
|
||||
expect(calleesOfBlock(block(undefined))).toBe('');
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue