mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-03 02:21:44 +00:00
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
124 lines
6.9 KiB
TypeScript
124 lines
6.9 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import type { FunctionCfg, SiteRecord } from '../../src/core/ingestion/cfg/types.js';
|
|
import { cfgOf } from '../helpers/ts-cfg-harness.js';
|
|
import { allSites } from '../helpers/cfg-harness.js';
|
|
|
|
/**
|
|
* U1 (#2227 follow-up plan) — `SiteRecord.at` call-site anchor position.
|
|
*
|
|
* Each call/new `SiteRecord` is stamped with `at: [line (1-based), col
|
|
* (0-based)]`, recorded by the worker harvester at the call/new node where it
|
|
* reads the callee. A downstream unit joins each site to its resolved callee id
|
|
* by EXACT position, so `at` MUST be the SAME anchor the CALLS-edge resolution
|
|
* keys on (plan KTD7).
|
|
*
|
|
* ANCHOR ALIGNMENT (verified against the scope-extractor): the CALLS `atRange`
|
|
* is `nodeToCapture('@reference.call.*', node).range` where the `@reference.call`
|
|
* anchor is the WHOLE call/new EXPRESSION node (the callee identifier / member
|
|
* property is the `@reference.name` SUB-tag, excluded from the anchor by
|
|
* `anchorCaptureFor` + `KNOWN_SUB_TAGS`; `atRange: anchor.range` at
|
|
* scope-extractor.ts:1030). The harvester's `visitCall`/`visitNew` receives that
|
|
* exact `call_expression`/`new_expression` node and records its `startPosition`,
|
|
* so `at` == the CALLS atRange for every call shape:
|
|
* - bare call `foo(x)` → `at` == the bare callee/call-expr start.
|
|
* - member call `arr.map(x)` → `at` == the call-expr start, i.e. the receiver
|
|
* `arr`'s position (where `@reference.call.member`
|
|
* anchors), NOT the `.map` property token.
|
|
* - chained call `a.b.c(x)` → `at` == the outer call-expr start (`a`).
|
|
*
|
|
* Member-read sites carry no `at` (the resolved-id join only consumes call/new).
|
|
*/
|
|
|
|
/** The call/new sites of `cfg`, in (block, statement, site) order. */
|
|
function callSites(cfg: FunctionCfg): SiteRecord[] {
|
|
return allSites(cfg).filter((s) => s.kind === 'call' || s.kind === 'new');
|
|
}
|
|
|
|
/** The single call/new site whose dotted callee path is `callee` (throws otherwise). */
|
|
function siteByCallee(cfg: FunctionCfg, callee: string): SiteRecord {
|
|
const matches = callSites(cfg).filter((s) => s.callee === callee);
|
|
if (matches.length !== 1) {
|
|
throw new Error(`expected exactly 1 call site with callee ${callee}, got ${matches.length}`);
|
|
}
|
|
return matches[0];
|
|
}
|
|
|
|
describe('SiteRecord.at — call-site anchor position (U1)', () => {
|
|
it('a bare call `foo(x)` carries `at` of the callee/call-expr anchor', () => {
|
|
// `function f(x) { foo(x); }` — `foo(` starts at column 16 on line 1; for a
|
|
// bare call the call-expression start and the callee identifier coincide, so
|
|
// this is the anchor the CALLS `@reference.call.free` resolution keys on.
|
|
const cfg = cfgOf(`function f(x) { foo(x); }`);
|
|
expect(siteByCallee(cfg, 'foo')).toMatchObject({ kind: 'call', at: [1, 16] });
|
|
});
|
|
|
|
it('a member call `arr.map(...)` carries `at` of the resolved-reference anchor (the call-expr / receiver start)', () => {
|
|
// `function f(arr) { arr.map(x => x); }` — `arr.map(...)` is one
|
|
// `call_expression` starting at `arr` (column 18). The CALLS
|
|
// `@reference.call.member` anchor is that whole call_expression, so `at`
|
|
// records the receiver/call-expr start, NOT the `.map` property token (which
|
|
// is only the `@reference.name` sub-tag — plan KTD7).
|
|
const cfg = cfgOf(`function f(arr) { arr.map(x => x); }`);
|
|
expect(siteByCallee(cfg, 'arr.map')).toMatchObject({ kind: 'call', at: [1, 18] });
|
|
});
|
|
|
|
it('a chained call `a.b.c(x)` carries `at` of the outer call-expr start (the chain root)', () => {
|
|
// `a.b.c(x)` is one call_expression starting at the chain root `a` (column
|
|
// 19); the `@reference.call.member` anchor and the harvested `at` both land
|
|
// there — the outer call-expr start, not the `.c` property token.
|
|
const cfg = cfgOf(`function f(a, x) { a.b.c(x); }`);
|
|
expect(siteByCallee(cfg, 'a.b.c')).toMatchObject({ kind: 'call', at: [1, 19] });
|
|
});
|
|
|
|
it('an argument-position call on a LATER line carries that call`s own line, not the statement head line', () => {
|
|
// line 2: outer(
|
|
// line 3: inner(a)
|
|
// line 4: );
|
|
// The whole call statement begins on line 2, but the inner argument-position
|
|
// call begins on line 3 — its `SiteRecord.at` line MUST be 3 (the inner
|
|
// call's line). This is the core bug the position field fixes: a
|
|
// statement-line join would mis-attribute `inner`'s resolved id to line 2.
|
|
const cfg = cfgOf(`function f(a, b) {\n outer(\n inner(a)\n );\n}`);
|
|
const outer = siteByCallee(cfg, 'outer');
|
|
const inner = siteByCallee(cfg, 'inner');
|
|
expect(outer).toMatchObject({ kind: 'call', at: [2, 2] });
|
|
expect(inner).toMatchObject({ kind: 'call', at: [3, 4] });
|
|
// The inner call's `at` line is the INNER call's line, distinct from the
|
|
// statement head — assert the inequality unconditionally.
|
|
expect(inner.at?.[0]).toBe(3);
|
|
expect(inner.at?.[0]).not.toBe(outer.at?.[0]);
|
|
});
|
|
|
|
it('a `new` site carries `at` of the new-expression anchor', () => {
|
|
// `new User(x)` — `new` starts at column 16; the harvester records the
|
|
// new_expression start, matching `@reference.call.constructor`'s anchor.
|
|
const cfg = cfgOf(`function f(x) { const u = new User(x); return u; }`);
|
|
expect(siteByCallee(cfg, 'User')).toMatchObject({ kind: 'new', at: [1, 26] });
|
|
});
|
|
|
|
it('a nested closure `arr.map(x => foo(x))` records ONLY the outer `arr.map` site — `foo` is excluded', () => {
|
|
// The harvester does not record sites inside nested functions (types.ts:150),
|
|
// so `foo(x)` inside the arrow produces NO top-level SiteRecord here. The
|
|
// block's only call site is the outer `arr.map`, carrying its own `at`.
|
|
const cfg = cfgOf(`function f(arr) { arr.map(x => foo(x)); }`);
|
|
const sites = callSites(cfg);
|
|
// Exactly one call site, and it is the outer member call — not `foo`.
|
|
expect(sites).toHaveLength(1);
|
|
expect(sites.map((s) => s.callee)).toEqual(['arr.map']);
|
|
expect(siteByCallee(cfg, 'arr.map')).toMatchObject({ kind: 'call', at: [1, 18] });
|
|
// `foo` produces no site at this level (nested-fn exclusion preserved).
|
|
expect(sites.some((s) => s.callee === 'foo')).toBe(false);
|
|
});
|
|
|
|
it('member-read sites carry no `at` (the id join only consumes call/new)', () => {
|
|
// `sink(req.body)` — `req.body` is a value-position member-read site; only
|
|
// the `sink(...)` call site needs an anchor for the resolved-id join.
|
|
const cfg = cfgOf(`function f(req) { sink(req.body); }`);
|
|
const memberReads = allSites(cfg).filter((s) => s.kind === 'member-read');
|
|
expect(memberReads.length).toBeGreaterThan(0);
|
|
// No member-read site carries `at` (omit-when-absent for non-call sites).
|
|
expect(memberReads.every((s) => s.at === undefined)).toBe(true);
|
|
// The enclosing call site does carry one.
|
|
expect(siteByCallee(cfg, 'sink')).toMatchObject({ kind: 'call', at: [1, 18] });
|
|
});
|
|
});
|