call-receiver-no-fabrication.test.ts 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
  1. /**
  2. * A member call whose receiver is itself a call never fabricates an edge
  3. * (#1683, #1681). `d.setdefault(k, []).append(v)` used to lose its receiver at
  4. * extraction time, degrade to the bare `append`, and exact-match any top-level
  5. * project function of that name — a call edge from an unrelated function,
  6. * reproduced in Python and JavaScript alike. The receiver is now kept as
  7. * `<inner>().<method>`, which nothing name-matches; the inner call resolves
  8. * on its own as before.
  9. */
  10. import { describe, it, expect, beforeAll, afterAll } from 'vitest';
  11. import * as fs from 'fs';
  12. import * as os from 'os';
  13. import * as path from 'path';
  14. import { CodeGraph } from '../src';
  15. import { extractFromSource } from '../src/extraction';
  16. import { initGrammars, loadAllGrammars } from '../src/extraction/grammars';
  17. let dir: string;
  18. let cg: CodeGraph;
  19. beforeAll(async () => {
  20. await initGrammars();
  21. await loadAllGrammars();
  22. dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-1683-'));
  23. fs.mkdirSync(path.join(dir, 'py'));
  24. fs.mkdirSync(path.join(dir, 'js'));
  25. fs.writeFileSync(path.join(dir, 'py', '__init__.py'), '');
  26. fs.writeFileSync(
  27. path.join(dir, 'py', 'collect.py'),
  28. 'def append(item):\n return item\n\ndef get(key):\n return key\n\ndef make():\n return {}\n\n' +
  29. 'def bucket(d, k, v):\n d.setdefault(k, []).append(v)\n return d.items().get(k)\n\n' +
  30. 'def fresh():\n return make().get("x")\n'
  31. );
  32. fs.writeFileSync(
  33. path.join(dir, 'js', 'collect.js'),
  34. 'function append(item) { return item; }\nfunction run() { return 1; }\nfunction make() { return {}; }\n' +
  35. 'function bucket(d, k, v) { d.setdefault(k, []).append(v); make().run(); (0, make)().run(); }\n' +
  36. 'module.exports = { append, run, make, bucket };\n'
  37. );
  38. cg = CodeGraph.initSync(dir);
  39. await cg.indexAll();
  40. });
  41. afterAll(() => {
  42. cg.destroy();
  43. fs.rmSync(dir, { recursive: true, force: true });
  44. });
  45. const fn = (name: string, file: string) => cg.getNodesByName(name).find((n) => n.kind === 'function' && n.filePath.endsWith(file))!;
  46. const calleesOf = (name: string, file: string) =>
  47. cg.getCallees(fn(name, file).id).filter(({ edge }) => edge.kind === 'calls').map(({ node }) => node.name).sort();
  48. // Callers through `calls` edges only — a `module.exports = { run }` value reference is not a call.
  49. const callersOf = (name: string, file: string) =>
  50. cg.getCallers(fn(name, file).id).filter(({ edge }) => edge.kind === 'calls').map(({ node }) => node.name);
  51. describe('call-expression receivers (#1683)', () => {
  52. it('Python: no edge from a call-result receiver to a same-named top-level function', () => {
  53. expect(calleesOf('bucket', 'collect.py')).toEqual([]);
  54. expect(callersOf('append', 'collect.py')).toEqual([]);
  55. expect(callersOf('get', 'collect.py')).toEqual([]);
  56. // The inner call still resolves on its own; `.get` on its unknown product does not.
  57. expect(calleesOf('fresh', 'collect.py')).toEqual(['make']);
  58. });
  59. it('JavaScript: the same shape, and the inner call keeps its edge', () => {
  60. expect(callersOf('append', 'collect.js')).toEqual([]);
  61. // `make().run()` — what `make` returns is unknown, so `run` is not guessed.
  62. expect(callersOf('run', 'collect.js')).toEqual([]);
  63. expect(calleesOf('bucket', 'collect.js')).toEqual(['make']);
  64. });
  65. it('encodes the receiver as `<inner>().<method>` and drops a receiver with no static callee', () => {
  66. const r = extractFromSource('src/x.js', 'function f(d) { d.setdefault("k", []).append(1); make().run(); (0, make)().run(); arr[0]().go(); }');
  67. const names = r.unresolvedReferences.filter((u) => u.referenceKind === 'calls').map((u) => u.referenceName).sort();
  68. // `(0, make)` and `arr[0]` are the inner calls' own refs, unchanged; their chains are dropped.
  69. expect(names).toEqual(['(0, make)', 'arr[0]', 'd.setdefault', 'd.setdefault().append', 'make', 'make().run']);
  70. const py = extractFromSource('x.py', 'def f(d):\n d.setdefault("k", []).append(1)\n d.items().get(2)\n');
  71. expect(py.unresolvedReferences.filter((u) => u.referenceKind === 'calls').map((u) => u.referenceName).sort())
  72. .toEqual(['d.items', 'd.items().get', 'd.setdefault', 'd.setdefault().append']);
  73. });
  74. });