mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-29 06:32:28 +00:00
129 lines
5.4 KiB
TypeScript
129 lines
5.4 KiB
TypeScript
/**
|
|
* The locality filter for value references, and the invariant triple it has to
|
|
* hold at once.
|
|
*
|
|
* A bare-identifier read (A2) makes module-scope constants answerable, but the
|
|
* same capture matches a read of a function-local `const`, and an edge to one of
|
|
* those retains exactly the inert symbols `pruneLocalSymbols` exists to drop.
|
|
* So references to `Const`/`Variable`/`Static` are filtered — and the shape of
|
|
* that filter is the whole story:
|
|
*
|
|
* 1. a function-local value MUST NOT keep an edge,
|
|
* 2. a module-scope value MUST keep one,
|
|
* 3. a CLASS MEMBER must keep one too — and this is the case a
|
|
* "module-level?" allowlist silently gets wrong.
|
|
*
|
|
* (3) is why the filter is a BLOCKLIST of function-local defs rather than an
|
|
* allowlist of module-level ones. A Java field, a C# field and a Python class
|
|
* attribute are none of module-level, and none of function-local. Under an
|
|
* allowlist they fall outside the allowed set and every one of their ACCESSES
|
|
* edges disappears — a whole edge class, in three languages, reported as
|
|
* "nothing reads this field".
|
|
*
|
|
* ── What each half of this file actually gates. Read before trusting it. ──
|
|
*
|
|
* The JS half gates the MECHANISM. Measured by instrumenting the bridge: for
|
|
* `javascript-const-references` it sees exactly two value-ACCESSES candidates,
|
|
* `DEFAULT_FETCH_LIMIT` (blocked=false) and `localScratchValue` (blocked=true).
|
|
* Inverting the filter's sense fails these tests.
|
|
*
|
|
* The Java half gates the OUTCOME, and deliberately not the mechanism, because
|
|
* the mechanism is not reachable from there: instrumenting the same bridge over
|
|
* `java-write-access` shows **zero** value-ACCESSES candidates — Java field
|
|
* references resolve to a `Property` target, and `isValueDefinitionLabel` covers
|
|
* only `Const`/`Static`/`Variable`, so the filter is never consulted. Those
|
|
* edges come from a different emitter entirely. So this half cannot fail when
|
|
* only the filter regresses, and saying otherwise would make it the kind of test
|
|
* that looks like a gate and is not one.
|
|
*
|
|
* It earns its place anyway: it asserts the user-visible answer ("does the graph
|
|
* know who reads this field?") by TARGET rather than by `reason`, which the
|
|
* per-language suites cannot do — they filter on `rel.reason === 'read'|'write'`
|
|
* while the bridge stamps `scope-resolution: read|write`, so a bridge-side
|
|
* change is invisible to them in either direction. If a future change ever makes
|
|
* the bridge the sole emitter for class members, this is the assertion that
|
|
* notices when they vanish.
|
|
*/
|
|
import { describe, it, expect, beforeAll } from 'vitest';
|
|
import path from 'node:path';
|
|
import { runPipelineFromRepo } from '../../../src/core/ingestion/pipeline.js';
|
|
import type { PipelineResult } from '../../../src/types/pipeline.js';
|
|
|
|
const LANG_FIXTURES = path.resolve(__dirname, '..', '..', 'fixtures', 'lang-resolution');
|
|
|
|
interface AccessEdge {
|
|
readonly source: string;
|
|
readonly target: string;
|
|
readonly targetLabel: string;
|
|
readonly reason: string;
|
|
}
|
|
|
|
const accessesOf = (result: PipelineResult): AccessEdge[] => {
|
|
const out: AccessEdge[] = [];
|
|
result.graph.forEachRelationship((r) => {
|
|
if (r.type !== 'ACCESSES') return;
|
|
const source = result.graph.getNode(r.sourceId);
|
|
const target = result.graph.getNode(r.targetId);
|
|
out.push({
|
|
source: String(source?.properties.name ?? ''),
|
|
target: String(target?.properties.name ?? ''),
|
|
targetLabel: String(target?.label ?? ''),
|
|
reason: String(r.reason ?? ''),
|
|
});
|
|
});
|
|
return out;
|
|
};
|
|
|
|
describe('value-reference locality filter', () => {
|
|
describe('class members survive it (the allowlist regression)', () => {
|
|
let java: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
java = await runPipelineFromRepo(path.join(LANG_FIXTURES, 'java-write-access'), () => {}, {});
|
|
}, 60_000);
|
|
|
|
it('keeps ACCESSES to Java instance fields', () => {
|
|
const targets = accessesOf(java).map((e) => e.target);
|
|
// Asserted as a non-empty set FIRST: every `toContain` below is vacuous
|
|
// if the fixture stopped producing ACCESSES entirely, which is precisely
|
|
// the regression this file exists to catch.
|
|
expect(targets.length).toBeGreaterThan(0);
|
|
expect(targets).toContain('name');
|
|
expect(targets).toContain('address');
|
|
});
|
|
|
|
it('resolves them to a member node, not to a stray local', () => {
|
|
const memberEdges = accessesOf(java).filter(
|
|
(e) => e.target === 'name' || e.target === 'address',
|
|
);
|
|
for (const edge of memberEdges) {
|
|
expect(edge.targetLabel).toBe('Property');
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('the two cases the filter exists for still hold', () => {
|
|
let js: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
js = await runPipelineFromRepo(
|
|
path.join(LANG_FIXTURES, 'javascript-const-references'),
|
|
() => {},
|
|
{},
|
|
);
|
|
}, 60_000);
|
|
|
|
it('keeps a module-scope const read', () => {
|
|
const targets = accessesOf(js).map((e) => e.target);
|
|
expect(targets.length).toBeGreaterThan(0);
|
|
expect(targets).toContain('DEFAULT_FETCH_LIMIT');
|
|
});
|
|
|
|
it('drops a function-local const read', () => {
|
|
// The whole reason the filter exists. `localScratchValue` is declared and
|
|
// read inside one function; an edge to it retains a symbol
|
|
// `pruneLocalSymbols` would otherwise remove.
|
|
expect(accessesOf(js).map((e) => e.target)).not.toContain('localScratchValue');
|
|
});
|
|
});
|
|
});
|