ts-chained-receiver.test.ts 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. /**
  2. * A TS/JS member call reached through a host namespace — `chrome.storage.local
  3. * .get(k)`, `document.body.querySelector(s)` — ends in a platform API. Emitting
  4. * the bare method name for it let every such call exact-match whatever project
  5. * symbol shared the name, so a storage wrapper's `get` called itself (#1707).
  6. * Those calls stay unresolved, as do untyped identifier chains (#1566);
  7. * their qualified source references remain available for effect reporting. The existing
  8. * `window.MyNs.run()` and `this.<field>.m()` paths remain outside that guard.
  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. let dir: string;
  16. let cg: CodeGraph;
  17. beforeAll(async () => {
  18. dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-1707-'));
  19. const w = (rel: string, body: string) => fs.writeFileSync(path.join(dir, rel), body);
  20. w(
  21. 'storage.ts',
  22. 'declare const chrome: any;\n' +
  23. 'export const DraftHubStorage = {\n' +
  24. ' async get(key: string): Promise<unknown> {\n' +
  25. ' const result = await chrome.storage.local.get([key]);\n' +
  26. ' return result[key];\n' +
  27. ' },\n' +
  28. '};\n'
  29. );
  30. w(
  31. 'dom.ts',
  32. 'export function querySelector(sel: string): string { return sel; }\n' +
  33. 'export function findRow(): unknown {\n' +
  34. ' return document.body.querySelector("tr");\n' +
  35. '}\n'
  36. );
  37. w(
  38. 'service.ts',
  39. 'declare const window: any;\n' +
  40. 'export function ping(): string { return "pong"; }\n' +
  41. 'export function viaGlobal(): string {\n' +
  42. ' return window.MyNs.ping();\n' +
  43. '}\n' +
  44. 'export class PingService { ping(): string { return "service"; } }\n' +
  45. 'export class Runner {\n' +
  46. ' constructor(private svc: PingService) {}\n' +
  47. ' run(): string { return this.svc.ping(); }\n' +
  48. '}\n' +
  49. 'export class AnonymousRunner {\n' +
  50. ' constructor(private svc: { ping(): string }) {}\n' +
  51. ' run(): string { return this.svc.ping(); }\n' +
  52. '}\n'
  53. );
  54. cg = await CodeGraph.init(dir, { index: true });
  55. cg.resolveReferences();
  56. });
  57. afterAll(() => {
  58. cg.destroy();
  59. try {
  60. fs.rmSync(dir, { recursive: true, force: true });
  61. } catch {
  62. // Windows can still hold the SQLite handle for a moment; the OS temp dir is swept anyway.
  63. }
  64. });
  65. const fn = (name: string, file: string) =>
  66. cg.getNodesByKind('function').find((n) => n.name === name && n.filePath === file)!;
  67. const method = (qn: string) => cg.getNodesByKind('method').find((n) => n.qualifiedName === qn)!;
  68. const callTargets = (id: string) =>
  69. cg
  70. .getOutgoingEdges(id)
  71. .filter((e) => e.kind === 'calls')
  72. .map((e) => e.target);
  73. describe('TS/JS call through a host-global chain (#1707)', () => {
  74. it('does not make a storage wrapper call itself through chrome.storage.local.get', () => {
  75. const get = fn('get', 'storage.ts');
  76. expect(get).toBeDefined();
  77. expect(callTargets(get.id)).not.toContain(get.id);
  78. });
  79. it('does not bind document.body.querySelector to a same-named project function', () => {
  80. expect(callTargets(fn('findRow', 'dom.ts').id)).not.toContain(
  81. fn('querySelector', 'dom.ts').id
  82. );
  83. });
  84. it('keeps a chain rooted at a project value — window.MyNs.m() and this.<field>.m()', () => {
  85. const ping = fn('ping', 'service.ts').id;
  86. expect(callTargets(fn('viaGlobal', 'service.ts').id)).toContain(ping);
  87. expect(callTargets(method('Runner::run').id)).toEqual([method('PingService::ping').id]);
  88. });
  89. it('does not guess a same-named project target for an anonymous field type (#1496)', () => {
  90. // Neither the top-level ping nor PingService::ping establishes what svc is.
  91. expect(callTargets(method('AnonymousRunner::run').id)).toEqual([]);
  92. });
  93. });