GitNexus/gitnexus/test/unit/ts-receiver-chain-capture.test.ts
Gergő Magyar 911151e230
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
fix(resolution): resolve Go pointer-receiver calls, and report the program boundary instead of hedging (#2766) (#2782)
2026-08-01 22:42:18 +01:00

105 lines
4.2 KiB
TypeScript

/**
* U5 — TypeScript emits `@reference.receiver-chain` for a receiver that is
* itself an expression.
*
* Capture-level, not resolution-level: nothing consumes the field yet, so these
* assert what is written, and that nothing extra is.
*/
import { describe, it, expect } from 'vitest';
import { emitTsScopeCaptures } from '../../src/core/ingestion/languages/typescript/index.js';
/** The chain captures produced for a source, keyed by the member being called. */
function chainsFor(src: string): Record<string, string> {
const out: Record<string, string> = {};
for (const match of emitTsScopeCaptures(src, 'main.ts')) {
const chain = match['@reference.receiver-chain'];
const name = match['@reference.name'];
if (chain === undefined || name === undefined) continue;
out[name.text] = chain.text;
}
return out;
}
const MODELS = `class Address { save(): void {} }
class User { name = ''; address = new Address(); save(): void {} }
class Service {
getUser(): User { return new User(); }
getTyped<T>(): User { return new User(); }
}
declare const svc: Service;
declare const repos: User[];
`;
describe('TypeScript receiver-chain capture', () => {
it('emits a chain for a plain call-chain receiver', () => {
expect(chainsFor(`${MODELS}\nsvc.getUser().save();\n`)).toMatchObject({
save: '2|svc|cgetUser',
});
});
it('emits a mixed call/field chain base-first', () => {
expect(chainsFor(`${MODELS}\nsvc.getUser().address.save();\n`)).toMatchObject({
save: '2|svc|cgetUser|faddress',
});
});
it('emits a chain for an optional-chained receiver — one of the shapes that resolves to nothing today', () => {
expect(chainsFor(`${MODELS}\nsvc?.getUser().save();\n`)).toMatchObject({
save: '2|svc|cgetUser',
});
});
it('emits a chain for an explicit-type-argument receiver', () => {
expect(chainsFor(`${MODELS}\nsvc.getTyped<User>().save();\n`)).toMatchObject({
save: '2|svc|cgetTyped',
});
});
it('emits a name-free index step for a subscript receiver', () => {
// The declaration `repos: User[]` at the top of MODELS exists for this.
// A subscript receiver contains neither `.` nor `(`, so Case 0's old
// punctuation gate never fired and this call was INVISIBLE — no edge and no
// recorded drop. The chain is what makes it visible; the `i` step carries no
// name because a subscript key is a value, not a member.
expect(chainsFor(`${MODELS}\nrepos[0].save();\n`)).toMatchObject({ save: '2|repos|i' });
});
it('emits one index step per subscript, so a nested container is not flattened', () => {
// `User[][]` reduces to the same `User` a single `User[]` does, so the STEP
// COUNT is the only thing that distinguishes `nested[0]` (still a container)
// from `nested[0][1]` (an element).
const src = `${MODELS}\ndeclare const nested: User[][];\nnested[0][1].save();\n`;
expect(chainsFor(src)).toMatchObject({ save: '2|nested|i|i' });
});
it('emits no chain for a bare-name receiver — there is nothing to fold', () => {
expect(chainsFor(`${MODELS}\nconst u = new User();\nu.save();\n`)).toEqual({});
});
it('emits no chain for a property read or write receiver (CALL_TAGS gate)', () => {
// Without the gate these pay the walk and the bytes for a field no
// call-site resolver reads.
const chains = chainsFor(
`${MODELS}\nsvc.getUser().name = 'x';\nconst n = svc.getUser().name;\n`,
);
expect(chains).not.toHaveProperty('name');
});
it('emits a plain string payload, so the capture stays structured-clone safe', () => {
const chains = chainsFor(`${MODELS}\nsvc.getUser().save();\n`);
expect(typeof chains.save).toBe('string');
});
it('emits NO chain past MAX_CHAIN_DEPTH rather than a truncated one', () => {
// `extractMixedChain` reports no base receiver when it stops early, and a
// partial chain is missing exactly the head that decides the final type —
// encoding it would turn an untypeable receiver into a wrongly-typed one.
const deep = `${MODELS}
class C5 { e(): C5 { return this; } f(): void {} }
declare const a: C5;
a.e().e().e().e().f();
`;
expect(chainsFor(deep)).not.toHaveProperty('f');
});
});