awaited-receiver.test.ts 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. import { afterEach, beforeEach, expect, it } from 'vitest';
  2. import * as fs from 'node:fs';
  3. import * as os from 'node:os';
  4. import * as path from 'node:path';
  5. import { CodeGraph } from '../src';
  6. let root: string;
  7. let cg: CodeGraph | undefined;
  8. beforeEach(() => { root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-awaited-')); });
  9. afterEach(() => { cg?.close(); cg = undefined; fs.rmSync(root, { recursive: true, force: true }); });
  10. async function index(files: Record<string, string>) {
  11. for (const [name, text] of Object.entries(files)) fs.writeFileSync(path.join(root, name), text);
  12. cg = await CodeGraph.init(root, { index: true });
  13. }
  14. function calls(name: string, file = 'caller.ts') {
  15. const node = cg!.getNodesByKind('function').find(n => n.name === name && n.filePath === file);
  16. expect(node).toBeDefined();
  17. return cg!.getCallees(node!.id).filter(({ edge }) => edge.kind === 'calls')
  18. .map(({ node, edge }) => ({ target: `${node.filePath}:${node.qualifiedName}`, line: edge.line }));
  19. }
  20. const engine = 'export class Engine { run() {} }\nexport async function makeEngine(): Promise<Engine> { return new Engine(); }';
  21. it('follows an imported factory alias rather than an unrelated namesake (#1840)', async () => {
  22. await index({
  23. 'engine.ts': engine,
  24. 'decoy.ts': 'export async function load(): Promise<string> { return ""; }',
  25. 'caller.ts': 'import { makeEngine as load } from "./engine";\nexport async function drive() { const handle = await load(); handle.run(); }',
  26. });
  27. expect(calls('drive').map(c => c.target).sort()).toEqual(['engine.ts:Engine::run', 'engine.ts:makeEngine']);
  28. });
  29. it('resolves return-type aliases in the factory module, not a caller-local decoy (#1840)', async () => {
  30. await index({
  31. 'engine.ts': engine,
  32. 'factory.ts': 'import { Engine as Service } from "./engine";\nexport async function load(): Promise<Service> { return new Service(); }',
  33. 'caller.ts': 'import { load } from "./factory";\nclass Service { run() {} }\nexport async function drive() { const handle = await load(); handle.run(); }',
  34. });
  35. expect(calls('drive').map(c => c.target).sort()).toEqual(['engine.ts:Engine::run', 'factory.ts:load']);
  36. });
  37. it('does not infer from a factory hidden by a parameter (#1840)', async () => {
  38. await index({
  39. 'engine.ts': engine,
  40. 'caller.ts': 'import { makeEngine } from "./engine";\nexport async function drive(makeEngine: () => Promise<string>) { const handle = await makeEngine(); handle.run(); }',
  41. });
  42. expect(calls('drive').map(c => c.target)).not.toContain('engine.ts:Engine::run');
  43. });
  44. it('distinguishes same-named awaited receivers in sibling blocks (#1840)', async () => {
  45. await index({ 'caller.ts': `class PaneManager { split() {} }
  46. async function text(): Promise<string> { return ""; }
  47. async function pane(): Promise<PaneManager> { return new PaneManager(); }
  48. export async function drive() {
  49. { const value = await text(); value.split(); }
  50. { const value = await pane(); value.split(); }
  51. }` });
  52. expect(calls('drive').filter(c => c.target.endsWith('PaneManager::split'))).toEqual([
  53. { target: 'caller.ts:PaneManager::split', line: 6 },
  54. ]);
  55. });
  56. it('keeps captured awaited bindings but rejects shadowing parameters (#1840)', async () => {
  57. await index({ 'caller.ts': `${engine}
  58. export async function outer() {
  59. const handle = await makeEngine();
  60. function captured() { handle.run(); }
  61. function shadow(handle: any) { handle.run(); }
  62. return { captured, shadow };
  63. }` });
  64. expect(calls('captured').map(c => c.target)).toContain('caller.ts:Engine::run');
  65. expect(calls('shadow').map(c => c.target)).not.toContain('caller.ts:Engine::run');
  66. });
  67. it('invalidates an awaited return type after edits in the callee file (#1840)', async () => {
  68. const caller = 'import { load } from "./factory";\nexport async function drive() { const value = await load(); value.split(); }';
  69. const primitive = 'export async function load(): Promise<string> { return ""; }';
  70. const project = 'export class Pane { split() {} }\nexport async function load(): Promise<Pane> { return new Pane(); }';
  71. await index({ 'caller.ts': caller, 'factory.ts': primitive, 'decoy.ts': 'export class Other { split() {} }' });
  72. expect(calls('drive').map(c => c.target)).toEqual(['factory.ts:load']);
  73. fs.writeFileSync(path.join(root, 'factory.ts'), project);
  74. await cg!.sync();
  75. expect(calls('drive').map(c => c.target).sort()).toEqual(['factory.ts:Pane::split', 'factory.ts:load']);
  76. fs.writeFileSync(path.join(root, 'factory.ts'), primitive);
  77. await cg!.sync();
  78. expect(calls('drive').map(c => c.target)).toEqual(['factory.ts:load']);
  79. });
  80. it('reads a multiline factory annotation and preserves ordinary typed receivers (#1840)', async () => {
  81. await index({ 'caller.ts': `class Engine { run() {} }
  82. class Decoy { run() {} }
  83. async function load(
  84. input: string
  85. ): Promise<Engine> { return new Engine(); }
  86. export async function drive() { const value = await load(''); value.run(); }
  87. export function ordinary() { const engine = new Engine(); engine.run(); }` });
  88. expect(calls('drive').map(c => c.target).sort()).toEqual(['caller.ts:Engine::run', 'caller.ts:load']);
  89. expect(calls('ordinary').map(c => c.target)).toContain('caller.ts:Engine::run');
  90. });
  91. it('supports newline-terminated awaited declarations without treating a chained result as the factory type (#1840)', async () => {
  92. await index({ 'caller.ts': `${engine}
  93. export async function drive() {
  94. const value = await makeEngine()
  95. value.run()
  96. }
  97. export async function chained() {
  98. const value = await makeEngine().toString();
  99. value.run();
  100. }` });
  101. expect(calls('drive').map(c => c.target)).toContain('caller.ts:Engine::run');
  102. expect(calls('chained').map(c => c.target)).not.toContain('caller.ts:Engine::run');
  103. });
  104. it('does not borrow a local factory annotation through a nearer variable binding (#1840)', async () => {
  105. await index({ 'caller.ts': `${engine}
  106. export async function drive(other: () => Promise<string>) {
  107. const makeEngine = other;
  108. const value = await makeEngine();
  109. value.run();
  110. }` });
  111. expect(calls('drive').map(c => c.target)).not.toContain('caller.ts:Engine::run');
  112. });
  113. it('preserves a real awaited member-factory call outside the bare-callee inference path (#1840)', async () => {
  114. await index({ 'caller.ts': `class Engine { run() {} }
  115. class Factory { async create(): Promise<Engine> { return new Engine(); } }
  116. export async function drive() {
  117. const handle = await new Factory().create();
  118. handle.run();
  119. }` });
  120. expect(calls('drive').map(c => c.target)).toContain('caller.ts:Engine::run');
  121. });
  122. it('invalidates negative and positive file eligibility when caller edits add and remove await (#1840)', async () => {
  123. const plain = 'import { load } from "./factory";\nfunction opaque() { return null; }\nexport async function drive() { const value = opaque(); value.split(); }';
  124. const awaited = 'import { load } from "./factory";\nexport async function drive() { const value = await load(); value.split(); }';
  125. await index({
  126. 'caller.ts': plain,
  127. 'factory.ts': 'export class Pane { split() {} }\nexport class Other { split() {} }\nexport async function load(): Promise<Pane> { return new Pane(); }',
  128. });
  129. expect(calls('drive').filter(c => c.target.endsWith('Pane::split'))).toEqual([]);
  130. fs.writeFileSync(path.join(root, 'caller.ts'), awaited);
  131. await cg!.sync();
  132. expect(calls('drive').map(c => c.target)).toContain('factory.ts:Pane::split');
  133. fs.writeFileSync(path.join(root, 'caller.ts'), plain);
  134. await cg!.sync();
  135. expect(calls('drive').filter(c => c.target.endsWith('Pane::split'))).toEqual([]);
  136. });