bare-call-no-method.test.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. /**
  2. * In JS/TS a receiver-less call can never bind to a class method: `serialize(x)`
  3. * inside `Record.serialize` means the module-scope function, and the method
  4. * itself — which the same-file proximity term used to pick, producing a
  5. * self-edge — is not a candidate (#1714). `this.serialize(x)` still is.
  6. */
  7. import { describe, it, expect, afterEach } from 'vitest';
  8. import * as fs from 'fs';
  9. import * as os from 'os';
  10. import * as path from 'path';
  11. import CodeGraph from '../src/index';
  12. let tempDir: string;
  13. let cg: CodeGraph | null = null;
  14. async function callsFromMethod(source: string, methodName: string): Promise<string[]> {
  15. tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-1714-'));
  16. fs.writeFileSync(path.join(tempDir, 'record.ts'), source);
  17. cg = await CodeGraph.init(tempDir, { index: true });
  18. cg.resolveReferences();
  19. const from = cg.getNodesByKind('method').find((n) => n.name === methodName)!;
  20. expect(from).toBeDefined();
  21. return cg
  22. .getOutgoingEdges(from.id)
  23. .filter((e) => e.kind === 'calls')
  24. .map((e) => cg!.getNode(e.target))
  25. .filter((n): n is NonNullable<typeof n> => !!n)
  26. .map((n) => `${n.kind}:${n.qualifiedName ?? n.name}`);
  27. }
  28. afterEach(() => {
  29. cg?.close();
  30. cg = null;
  31. fs.rmSync(tempDir, { recursive: true, force: true });
  32. });
  33. describe('a receiver-less JS/TS call never binds to a method (#1714)', () => {
  34. it('resolves the bare call onto the module-scope function, not the enclosing method', async () => {
  35. const callees = await callsFromMethod(
  36. [
  37. 'function serialize(value: string): string {',
  38. ' return value.trim();',
  39. '}',
  40. '',
  41. 'export class Record {',
  42. ' constructor(private readonly raw: string) {}',
  43. ' serialize(): string {',
  44. ' return serialize(this.raw);',
  45. ' }',
  46. '}',
  47. '',
  48. ].join('\n'),
  49. 'serialize'
  50. );
  51. expect(callees).toContain('function:serialize');
  52. expect(callees).not.toContain('method:Record::serialize');
  53. });
  54. it('keeps `this.serialize()` — a real recursive self-call', async () => {
  55. const callees = await callsFromMethod(
  56. [
  57. 'function serialize(value: string): string {',
  58. ' return value.trim();',
  59. '}',
  60. '',
  61. 'export class Record {',
  62. ' constructor(private readonly raw: string, private depth = 0) {}',
  63. ' serialize(): string {',
  64. ' if (this.depth > 0) return this.serialize();',
  65. ' return this.raw;',
  66. ' }',
  67. '}',
  68. '',
  69. ].join('\n'),
  70. 'serialize'
  71. );
  72. expect(callees).toContain('method:Record::serialize');
  73. });
  74. it('a bare call to a name the file binds itself has no cross-file candidate', async () => {
  75. tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-1714-'));
  76. fs.writeFileSync(path.join(tempDir, 'config.ts'), 'export function resolve(p: string) { return p; }\nexport function transform(c: string) { return c; }\nexport function now() { return 0; }\n');
  77. fs.writeFileSync(
  78. path.join(tempDir, 'client.ts'),
  79. [
  80. 'const transform = makeTransform();',
  81. 'export function ping(): Promise<void> {',
  82. ' return new Promise((resolve, reject) => {',
  83. ' setTimeout(() => resolve(), 10);',
  84. ' });',
  85. '}',
  86. 'export function run(options: { now?: () => number }) {',
  87. ' const now = options.now || (() => Date.now());',
  88. ' return now() + transform("x").length;',
  89. '}',
  90. '',
  91. ].join('\n')
  92. );
  93. cg = await CodeGraph.init(tempDir, { index: true });
  94. cg.resolveReferences();
  95. const targets = cg.getNodesByKind('function').filter((n) => n.filePath === 'config.ts').map((n) => n.id);
  96. const callers = cg.getNodesByKind('function').filter((n) => n.filePath === 'client.ts');
  97. const crossFile = callers.flatMap((c) => cg!.getOutgoingEdges(c.id)).filter((e) => e.kind === 'calls' && targets.includes(e.target));
  98. expect(crossFile).toEqual([]);
  99. });
  100. it('a destructured require or a string mentioning the name is not a local binding', async () => {
  101. tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-1714-'));
  102. fs.writeFileSync(path.join(tempDir, 'public-ip.js'), 'function lookupPublicIPv4() { return "1.2.3.4"; }\nfunction test(name, fn) { return fn(); }\nmodule.exports = { lookupPublicIPv4, test };\n');
  103. fs.writeFileSync(
  104. path.join(tempDir, 'main.js'),
  105. [
  106. 'const { lookupPublicIPv4 } = require("./public-ip");',
  107. 'const { test } = require("./public-ip");',
  108. 'async function prepare() {',
  109. ' const ip = await lookupPublicIPv4();',
  110. ' test("a test of the thing", () => {});',
  111. ' return ip;',
  112. '}',
  113. 'module.exports = { prepare };',
  114. '',
  115. ].join('\n')
  116. );
  117. cg = await CodeGraph.init(tempDir, { index: true });
  118. cg.resolveReferences();
  119. const prepare = cg.getNodesByKind('function').find((n) => n.name === 'prepare')!;
  120. const names = cg.getOutgoingEdges(prepare.id).filter((e) => e.kind === 'calls').map((e) => cg!.getNode(e.target)?.name);
  121. expect(names).toContain('lookupPublicIPv4');
  122. expect(names).toContain('test');
  123. });
  124. it('keeps `other.serialize()` — a call through a receiver', async () => {
  125. const callees = await callsFromMethod(
  126. [
  127. 'export class Record {',
  128. ' serialize(): string { return ""; }',
  129. ' copyOf(other: Record): string {',
  130. ' return other.serialize();',
  131. ' }',
  132. '}',
  133. '',
  134. ].join('\n'),
  135. 'copyOf'
  136. );
  137. expect(callees).toContain('method:Record::serialize');
  138. });
  139. });