mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-06 02:49:56 +00:00
test(resolvers): add class-instance + factory-pattern singleton coverage for TS/JS (issue #1358)
Closes the remaining sub-cases of issue #1358 surfaced by PR #1718's adversarial review (Finding 4). PR #1718 fixed object-literal-shorthand singletons (`export const fooService = { getUser() {} }`); this commit adds parallel coverage for the two other singleton shapes that resolve through the existing scope-resolution chain: // Pattern 1 — class-instance singleton export class FooService { getUser(id) { ... } } export const fooService = new FooService(); // Pattern 2 — factory-pattern singleton export class FooService { getUser(id) { ... } } export function makeFooService() { return new FooService(); } export const fooService = makeFooService(); Pre-plan investigation (per local plan docs/plans/2026-05-21-002 § "Pre-Plan Investigation Task (T1)") confirmed Outcome A — both patterns already resolve end-to-end through: - `@type-binding.constructor` capture (languages/{typescript,javascript}/ query.ts) seeds `fooService → FooService` at parse time - `propagateImportedReturnTypes` (scope-resolution/passes/ imported-return-types.ts:114) mirrors the typeBinding cross-file - Receiver-bound Case 4 simple typeBinding lookup (scope-resolution/passes/receiver-bound-calls.ts:625) MRO-walks FooService and emits the CALLS edge to getUser Tests added per language × pattern (5 each, 10 total): - node existence (Class, Method, Function, Const, plus Function for the factory pattern's `makeFooService`) - HAS_METHOD edge from class to method (class-instance variant) - CALLS edge from caller to `getUser` with `targetFilePath: 'src/service.{ts,js}'`, `reason: 'import-resolved'`, `confidence: 0.85` — exact `.toEqual([{...}])` shape pinning so a regression that emits at lower confidence or drops the cross-file reason fails loudly Fixtures placed under the existing `test/fixtures/lang-resolution/` convention. Tests appended to `test/integration/resolvers/{typescript,javascript}.test.ts`, matching the in-file pattern of every other resolver scenario. Also supersedes and removes the standalone `test/integration/class-instance-and-factory-singleton-resolution.test.ts` introduced earlier in this PR session (`0df91b77`) — the proper home for language-resolver scenarios is the per-language resolver test file alongside similar fixtures (`javascript-self-this-resolution`, `javascript-cross-file`, `typescript-tsconfig-paths`, etc.). One canonical location for the scenario, not two. Verification: 10/10 new singleton tests pass; 297/297 full TS+JS resolver suite pass (no regression in any existing resolver test).
This commit is contained in:
parent
e89773213f
commit
c8e573bc35
11 changed files with 270 additions and 181 deletions
9
gitnexus/test/fixtures/lang-resolution/javascript-class-instance-singleton/src/consumer.js
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/javascript-class-instance-singleton/src/consumer.js
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { fooService } from './service.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} id
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function caller(id) {
|
||||||
|
return fooService.getUser(id);
|
||||||
|
}
|
||||||
11
gitnexus/test/fixtures/lang-resolution/javascript-class-instance-singleton/src/service.js
vendored
Normal file
11
gitnexus/test/fixtures/lang-resolution/javascript-class-instance-singleton/src/service.js
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
export class FooService {
|
||||||
|
/**
|
||||||
|
* @param {string} id
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
getUser(id) {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fooService = new FooService();
|
||||||
9
gitnexus/test/fixtures/lang-resolution/javascript-factory-singleton/src/consumer.js
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/javascript-factory-singleton/src/consumer.js
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { fooService } from './service.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} id
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function caller(id) {
|
||||||
|
return fooService.getUser(id);
|
||||||
|
}
|
||||||
18
gitnexus/test/fixtures/lang-resolution/javascript-factory-singleton/src/service.js
vendored
Normal file
18
gitnexus/test/fixtures/lang-resolution/javascript-factory-singleton/src/service.js
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
export class FooService {
|
||||||
|
/**
|
||||||
|
* @param {string} id
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
getUser(id) {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {FooService}
|
||||||
|
*/
|
||||||
|
export function makeFooService() {
|
||||||
|
return new FooService();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fooService = makeFooService();
|
||||||
5
gitnexus/test/fixtures/lang-resolution/typescript-class-instance-singleton/src/consumer.ts
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/typescript-class-instance-singleton/src/consumer.ts
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { fooService } from './service';
|
||||||
|
|
||||||
|
export function caller(id: string) {
|
||||||
|
return fooService.getUser(id);
|
||||||
|
}
|
||||||
7
gitnexus/test/fixtures/lang-resolution/typescript-class-instance-singleton/src/service.ts
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/typescript-class-instance-singleton/src/service.ts
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
export class FooService {
|
||||||
|
getUser(id: string) {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fooService = new FooService();
|
||||||
5
gitnexus/test/fixtures/lang-resolution/typescript-factory-singleton/src/consumer.ts
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/typescript-factory-singleton/src/consumer.ts
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { fooService } from './service';
|
||||||
|
|
||||||
|
export function caller(id: string) {
|
||||||
|
return fooService.getUser(id);
|
||||||
|
}
|
||||||
11
gitnexus/test/fixtures/lang-resolution/typescript-factory-singleton/src/service.ts
vendored
Normal file
11
gitnexus/test/fixtures/lang-resolution/typescript-factory-singleton/src/service.ts
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
export class FooService {
|
||||||
|
getUser(id: string) {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeFooService(): FooService {
|
||||||
|
return new FooService();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fooService = makeFooService();
|
||||||
|
|
@ -1,181 +0,0 @@
|
||||||
/**
|
|
||||||
* Regression coverage for issue #1358's class-instance and factory-pattern
|
|
||||||
* singleton sub-cases. PR #1718 closed the object-literal-shorthand case
|
|
||||||
* (`export const fooService = { getUser() {} }`). This file covers the
|
|
||||||
* other two singleton shapes:
|
|
||||||
*
|
|
||||||
* // Pattern 1 — class-instance singleton
|
|
||||||
* export class FooService { getUser(id) {...} }
|
|
||||||
* export const fooService = new FooService();
|
|
||||||
*
|
|
||||||
* // Pattern 2 — factory-pattern singleton
|
|
||||||
* export class FooService { getUser(id) {...} }
|
|
||||||
* export function makeFooService() { return new FooService(); }
|
|
||||||
* export const fooService = makeFooService();
|
|
||||||
*
|
|
||||||
* Both already resolve end-to-end via scope-resolution's
|
|
||||||
* `@type-binding.constructor` capture (TS query) +
|
|
||||||
* `propagateImportedReturnTypes` chain-follow (cross-file mirror) +
|
|
||||||
* receiver-bound Case 4 (simple typeBinding lookup). This test pins that
|
|
||||||
* behavior so a future refactor of any of those three mechanisms cannot
|
|
||||||
* silently regress either pattern.
|
|
||||||
*
|
|
||||||
* Origin: PR #1718 review Finding 4 (NOTED, deferred). T1 pre-plan
|
|
||||||
* investigation per docs/plans/2026-05-21-002-feat-pr1718-followups-class-
|
|
||||||
* instance-and-label-normalization-plan.md confirmed Outcome A — both
|
|
||||||
* patterns already work; this is the regression-net.
|
|
||||||
*/
|
|
||||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
||||||
import fs from 'node:fs';
|
|
||||||
import os from 'node:os';
|
|
||||||
import path from 'node:path';
|
|
||||||
import {
|
|
||||||
getRelationships,
|
|
||||||
getNodesByLabel,
|
|
||||||
runPipelineFromRepo,
|
|
||||||
type PipelineResult,
|
|
||||||
} from './resolvers/helpers.js';
|
|
||||||
import { generateId } from '../../src/lib/utils.js';
|
|
||||||
|
|
||||||
function writeFixture(files: Record<string, string>): string {
|
|
||||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gnx-singleton-'));
|
|
||||||
for (const [rel, content] of Object.entries(files)) {
|
|
||||||
const full = path.join(root, rel);
|
|
||||||
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
||||||
fs.writeFileSync(full, content);
|
|
||||||
}
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeFixture(root: string): void {
|
|
||||||
fs.rmSync(root, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
const CONSUMER_TS = `import { fooService } from './service';
|
|
||||||
|
|
||||||
export function caller(id: string) {
|
|
||||||
return fooService.getUser(id);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Class methods carry a class-qualified node id (e.g. `FooService.getUser`)
|
|
||||||
// to distinguish them from same-name methods on other classes. Object-literal
|
|
||||||
// methods (per PR #1718) use the bare name because they have no class owner.
|
|
||||||
const EXPECTED_METHOD_NODE_ID = generateId('Method', 'src/service.ts:FooService.getUser#1');
|
|
||||||
|
|
||||||
// ── Pattern 1: class-instance singleton ─────────────────────────────────────
|
|
||||||
|
|
||||||
describe('class-instance singleton resolution (issue #1358 sub-case 2)', () => {
|
|
||||||
let repoRoot: string;
|
|
||||||
let result: PipelineResult;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
repoRoot = writeFixture({
|
|
||||||
'src/service.ts': `export class FooService {
|
|
||||||
getUser(id: string) {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const fooService = new FooService();
|
|
||||||
`,
|
|
||||||
'src/consumer.ts': CONSUMER_TS,
|
|
||||||
});
|
|
||||||
result = await runPipelineFromRepo(repoRoot, () => undefined, {
|
|
||||||
skipGraphPhases: true,
|
|
||||||
skipWorkers: true,
|
|
||||||
});
|
|
||||||
}, 60000);
|
|
||||||
|
|
||||||
afterAll(() => removeFixture(repoRoot));
|
|
||||||
|
|
||||||
it('emits Class:FooService, Method:getUser, Function:caller, Const:fooService exactly once', () => {
|
|
||||||
expect(getNodesByLabel(result, 'Class').filter((n) => n === 'FooService').length).toBe(1);
|
|
||||||
expect(getNodesByLabel(result, 'Method').filter((n) => n === 'getUser').length).toBe(1);
|
|
||||||
expect(getNodesByLabel(result, 'Function').filter((n) => n === 'caller').length).toBe(1);
|
|
||||||
expect(getNodesByLabel(result, 'Const').filter((n) => n === 'fooService').length).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('emits HAS_METHOD edge from FooService class to getUser', () => {
|
|
||||||
const hasMethod = getRelationships(result, 'HAS_METHOD');
|
|
||||||
const fromClass = hasMethod.filter((e) => e.source === 'FooService').map((e) => e.target);
|
|
||||||
expect(fromClass).toEqual(['getUser']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('emits CALLS edge caller → FooService.getUser with confidence 0.85 and reason import-resolved', () => {
|
|
||||||
const calls = getRelationships(result, 'CALLS');
|
|
||||||
const projected = calls
|
|
||||||
.filter((e) => e.source === 'caller' && e.target === 'getUser')
|
|
||||||
.map((e) => ({
|
|
||||||
targetId: e.rel.targetId,
|
|
||||||
confidence: e.rel.confidence,
|
|
||||||
reason: e.rel.reason,
|
|
||||||
}));
|
|
||||||
|
|
||||||
expect(projected).toEqual([
|
|
||||||
{
|
|
||||||
targetId: EXPECTED_METHOD_NODE_ID,
|
|
||||||
confidence: 0.85,
|
|
||||||
reason: 'import-resolved',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── Pattern 2: factory-pattern singleton ────────────────────────────────────
|
|
||||||
|
|
||||||
describe('factory-pattern singleton resolution (issue #1358 sub-case 3)', () => {
|
|
||||||
let repoRoot: string;
|
|
||||||
let result: PipelineResult;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
repoRoot = writeFixture({
|
|
||||||
'src/service.ts': `export class FooService {
|
|
||||||
getUser(id: string) {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function makeFooService(): FooService {
|
|
||||||
return new FooService();
|
|
||||||
}
|
|
||||||
|
|
||||||
export const fooService = makeFooService();
|
|
||||||
`,
|
|
||||||
'src/consumer.ts': CONSUMER_TS,
|
|
||||||
});
|
|
||||||
result = await runPipelineFromRepo(repoRoot, () => undefined, {
|
|
||||||
skipGraphPhases: true,
|
|
||||||
skipWorkers: true,
|
|
||||||
});
|
|
||||||
}, 60000);
|
|
||||||
|
|
||||||
afterAll(() => removeFixture(repoRoot));
|
|
||||||
|
|
||||||
it('emits Function:makeFooService alongside the class and consumer nodes', () => {
|
|
||||||
expect(getNodesByLabel(result, 'Function').filter((n) => n === 'makeFooService').length).toBe(
|
|
||||||
1,
|
|
||||||
);
|
|
||||||
expect(getNodesByLabel(result, 'Function').filter((n) => n === 'caller').length).toBe(1);
|
|
||||||
expect(getNodesByLabel(result, 'Const').filter((n) => n === 'fooService').length).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('resolves caller.fooService.getUser to FooService.getUser via factory chain-follow', () => {
|
|
||||||
const calls = getRelationships(result, 'CALLS');
|
|
||||||
const projected = calls
|
|
||||||
.filter((e) => e.source === 'caller' && e.target === 'getUser')
|
|
||||||
.map((e) => ({
|
|
||||||
targetId: e.rel.targetId,
|
|
||||||
confidence: e.rel.confidence,
|
|
||||||
reason: e.rel.reason,
|
|
||||||
}));
|
|
||||||
|
|
||||||
expect(projected).toEqual([
|
|
||||||
{
|
|
||||||
targetId: EXPECTED_METHOD_NODE_ID,
|
|
||||||
confidence: 0.85,
|
|
||||||
reason: 'import-resolved',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -541,3 +541,101 @@ describe('JavaScript Child extends Parent — inherited method resolution (SM-9)
|
||||||
expect(parentMethodCall!.source).toBe('run');
|
expect(parentMethodCall!.source).toBe('run');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Issue #1358: class-instance singleton (`export const x = new C()`)
|
||||||
|
// PR #1718 closed the object-literal-shorthand sub-case; this fixture covers
|
||||||
|
// the class-instance sub-case for JavaScript. Same resolution chain as TS but
|
||||||
|
// the receiver type comes from the `new ClassName()` initializer (no JSDoc
|
||||||
|
// annotation needed — the @type-binding.constructor capture handles it).
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('JavaScript class-instance singleton resolution (issue #1358 sub-case)', () => {
|
||||||
|
let result: PipelineResult;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
result = await runPipelineFromRepo(
|
||||||
|
path.join(FIXTURES, 'javascript-class-instance-singleton'),
|
||||||
|
() => {},
|
||||||
|
{ skipGraphPhases: true },
|
||||||
|
);
|
||||||
|
}, 60000);
|
||||||
|
|
||||||
|
it('detects FooService class, getUser method, caller function, fooService Const', () => {
|
||||||
|
expect(getNodesByLabel(result, 'Class')).toContain('FooService');
|
||||||
|
expect(getNodesByLabel(result, 'Method')).toContain('getUser');
|
||||||
|
expect(getNodesByLabel(result, 'Function')).toContain('caller');
|
||||||
|
expect(getNodesByLabel(result, 'Const')).toContain('fooService');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits HAS_METHOD edge from FooService to getUser', () => {
|
||||||
|
const hasMethod = getRelationships(result, 'HAS_METHOD');
|
||||||
|
const fromClass = hasMethod.filter((e) => e.source === 'FooService').map((e) => e.target);
|
||||||
|
expect(fromClass).toEqual(['getUser']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves caller.fooService.getUser() to FooService.getUser via constructor-inferred typeBinding', () => {
|
||||||
|
const calls = getRelationships(result, 'CALLS');
|
||||||
|
const projected = calls
|
||||||
|
.filter((e) => e.source === 'caller' && e.target === 'getUser')
|
||||||
|
.map((e) => ({
|
||||||
|
targetFilePath: e.targetFilePath,
|
||||||
|
reason: e.rel.reason,
|
||||||
|
confidence: e.rel.confidence,
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(projected).toEqual([
|
||||||
|
{
|
||||||
|
targetFilePath: 'src/service.js',
|
||||||
|
reason: 'import-resolved',
|
||||||
|
confidence: 0.85,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Issue #1358: factory-pattern singleton (`export const x = makeC()`)
|
||||||
|
// Tests the @type-binding.alias chain-follow for JS — fooService aliases the
|
||||||
|
// return of makeFooService(), whose JSDoc @returns {FooService} ties the chain
|
||||||
|
// back to the class. Resolution propagates cross-file via
|
||||||
|
// propagateImportedReturnTypes followChainPostFinalize.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('JavaScript factory-pattern singleton resolution (issue #1358 sub-case)', () => {
|
||||||
|
let result: PipelineResult;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
result = await runPipelineFromRepo(
|
||||||
|
path.join(FIXTURES, 'javascript-factory-singleton'),
|
||||||
|
() => {},
|
||||||
|
{ skipGraphPhases: true },
|
||||||
|
);
|
||||||
|
}, 60000);
|
||||||
|
|
||||||
|
it('detects FooService class, makeFooService function, fooService Const, caller function', () => {
|
||||||
|
expect(getNodesByLabel(result, 'Class')).toContain('FooService');
|
||||||
|
expect(getNodesByLabel(result, 'Function')).toContain('makeFooService');
|
||||||
|
expect(getNodesByLabel(result, 'Function')).toContain('caller');
|
||||||
|
expect(getNodesByLabel(result, 'Const')).toContain('fooService');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves caller.fooService.getUser() through the factory chain to FooService.getUser', () => {
|
||||||
|
const calls = getRelationships(result, 'CALLS');
|
||||||
|
const projected = calls
|
||||||
|
.filter((e) => e.source === 'caller' && e.target === 'getUser')
|
||||||
|
.map((e) => ({
|
||||||
|
targetFilePath: e.targetFilePath,
|
||||||
|
reason: e.rel.reason,
|
||||||
|
confidence: e.rel.confidence,
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(projected).toEqual([
|
||||||
|
{
|
||||||
|
targetFilePath: 'src/service.js',
|
||||||
|
reason: 'import-resolved',
|
||||||
|
confidence: 0.85,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -2936,3 +2936,100 @@ export function createUtf8User(): void {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Issue #1358: class-instance singleton (`export const x = new C()`)
|
||||||
|
// PR #1718 closed the object-literal-shorthand sub-case; this fixture covers
|
||||||
|
// the class-instance sub-case. Resolution chain: @type-binding.constructor
|
||||||
|
// (TS query) → propagateImportedReturnTypes (cross-file mirror) →
|
||||||
|
// receiver-bound Case 4 (simple typeBinding) → MRO walk.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('TypeScript class-instance singleton resolution (issue #1358 sub-case)', () => {
|
||||||
|
let result: PipelineResult;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
result = await runPipelineFromRepo(
|
||||||
|
path.join(FIXTURES, 'typescript-class-instance-singleton'),
|
||||||
|
() => {},
|
||||||
|
{ skipGraphPhases: true },
|
||||||
|
);
|
||||||
|
}, 60000);
|
||||||
|
|
||||||
|
it('detects FooService class, getUser method, caller function, fooService Const', () => {
|
||||||
|
expect(getNodesByLabel(result, 'Class')).toContain('FooService');
|
||||||
|
expect(getNodesByLabel(result, 'Method')).toContain('getUser');
|
||||||
|
expect(getNodesByLabel(result, 'Function')).toContain('caller');
|
||||||
|
expect(getNodesByLabel(result, 'Const')).toContain('fooService');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits HAS_METHOD edge from FooService to getUser', () => {
|
||||||
|
const hasMethod = getRelationships(result, 'HAS_METHOD');
|
||||||
|
const fromClass = hasMethod.filter((e) => e.source === 'FooService').map((e) => e.target);
|
||||||
|
expect(fromClass).toEqual(['getUser']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves caller.fooService.getUser() to FooService.getUser via constructor-inferred typeBinding', () => {
|
||||||
|
const calls = getRelationships(result, 'CALLS');
|
||||||
|
const projected = calls
|
||||||
|
.filter((e) => e.source === 'caller' && e.target === 'getUser')
|
||||||
|
.map((e) => ({
|
||||||
|
targetFilePath: e.targetFilePath,
|
||||||
|
reason: e.rel.reason,
|
||||||
|
confidence: e.rel.confidence,
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(projected).toEqual([
|
||||||
|
{
|
||||||
|
targetFilePath: 'src/service.ts',
|
||||||
|
reason: 'import-resolved',
|
||||||
|
confidence: 0.85,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Issue #1358: factory-pattern singleton (`export const x = makeC()`)
|
||||||
|
// Tests the @type-binding.alias chain-follow path through
|
||||||
|
// propagateImportedReturnTypes (followChainPostFinalize) — fooService aliases
|
||||||
|
// makeFooService's return type, which the constructor seeds as FooService.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('TypeScript factory-pattern singleton resolution (issue #1358 sub-case)', () => {
|
||||||
|
let result: PipelineResult;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
result = await runPipelineFromRepo(
|
||||||
|
path.join(FIXTURES, 'typescript-factory-singleton'),
|
||||||
|
() => {},
|
||||||
|
{ skipGraphPhases: true },
|
||||||
|
);
|
||||||
|
}, 60000);
|
||||||
|
|
||||||
|
it('detects FooService class, makeFooService function, fooService Const, caller function', () => {
|
||||||
|
expect(getNodesByLabel(result, 'Class')).toContain('FooService');
|
||||||
|
expect(getNodesByLabel(result, 'Function')).toContain('makeFooService');
|
||||||
|
expect(getNodesByLabel(result, 'Function')).toContain('caller');
|
||||||
|
expect(getNodesByLabel(result, 'Const')).toContain('fooService');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves caller.fooService.getUser() through the factory chain to FooService.getUser', () => {
|
||||||
|
const calls = getRelationships(result, 'CALLS');
|
||||||
|
const projected = calls
|
||||||
|
.filter((e) => e.source === 'caller' && e.target === 'getUser')
|
||||||
|
.map((e) => ({
|
||||||
|
targetFilePath: e.targetFilePath,
|
||||||
|
reason: e.rel.reason,
|
||||||
|
confidence: e.rel.confidence,
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(projected).toEqual([
|
||||||
|
{
|
||||||
|
targetFilePath: 'src/service.ts',
|
||||||
|
reason: 'import-resolved',
|
||||||
|
confidence: 0.85,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue