ts-this-field-call.test.ts 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. /**
  2. * A TS/JS call through a field of the enclosing class resolves on the field's
  3. * declared type, never by bare name (#1496).
  4. *
  5. * `this.mailer.send(msg)` inside `Notifier.send()` used to be emitted as the
  6. * bare `send`, which exact-matched the nearest same-named method — the
  7. * calling method itself. The stored self-edge `Notifier::send → Notifier::send`
  8. * made callers, callees, impact and trace silently wrong on exactly the
  9. * shape a delegating wrapper takes. The identical call resolved correctly
  10. * whenever the wrapper had any other name.
  11. */
  12. import { describe, it, expect, beforeAll, afterAll } from 'vitest';
  13. import * as fs from 'fs';
  14. import * as os from 'os';
  15. import * as path from 'path';
  16. import { CodeGraph } from '../src';
  17. import { initGrammars, loadAllGrammars } from '../src/extraction/grammars';
  18. let dir: string;
  19. let cg: CodeGraph;
  20. beforeAll(async () => {
  21. await initGrammars();
  22. await loadAllGrammars();
  23. dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-1496-'));
  24. fs.mkdirSync(path.join(dir, 'src'));
  25. const w = (rel: string, body: string) => fs.writeFileSync(path.join(dir, 'src', rel), body);
  26. w('mailer.ts', 'export class Mailer {\n send(msg: string): string { return msg; }\n}\n');
  27. w(
  28. 'notifier.ts',
  29. "import { Mailer } from './mailer';\n" +
  30. 'export class Notifier {\n' +
  31. ' constructor(private readonly mailer: Mailer, private items: string[]) {}\n' +
  32. ' send(msg: string): string { return this.mailer.send(msg); }\n' +
  33. ' other(msg: string): string { return this.mailer.send(msg); }\n' +
  34. ' push(msg: string): void { this.items.push(msg); }\n' +
  35. '}\n'
  36. );
  37. // Plain JS: the field's type is only known from its `new` initializer.
  38. // (resolveMethodOnType matches within one language, so the JS wrapper gets a JS Mailer.)
  39. w('legacy-mailer.js', 'class LegacyMailer {\n send(msg) { return msg; }\n}\nmodule.exports = { LegacyMailer };\n');
  40. w(
  41. 'legacy.js',
  42. "const { LegacyMailer } = require('./legacy-mailer');\n" +
  43. 'class LegacyNotifier {\n' +
  44. ' constructor() { this.mailer = new LegacyMailer(); }\n' +
  45. ' send(msg) { return this.mailer.send(msg); }\n' +
  46. '}\n' +
  47. 'module.exports = { LegacyNotifier };\n'
  48. );
  49. // A field typed as the type OF a value: an object literal used as a namespace.
  50. w(
  51. 'storage.ts',
  52. 'export const DraftHubStorage = {\n' +
  53. ' async get(key: string): Promise<string> { return key; },\n' +
  54. ' async getSettings(): Promise<object> { return {}; },\n' +
  55. '};\n'
  56. );
  57. w(
  58. 'keeper.ts',
  59. "import { DraftHubStorage } from './storage';\n" +
  60. 'export class Keeper {\n' +
  61. ' constructor(private readonly storage: typeof DraftHubStorage) {}\n' +
  62. ' async get(key: string): Promise<string> { return this.storage.get(key); }\n' +
  63. ' async settings(): Promise<object> { return this.storage.getSettings(); }\n' +
  64. '}\n'
  65. );
  66. cg = CodeGraph.initSync(dir);
  67. await cg.indexAll();
  68. });
  69. afterAll(() => {
  70. cg.destroy();
  71. fs.rmSync(dir, { recursive: true, force: true });
  72. });
  73. const method = (qn: string) => cg.getNodesByKind('method').find((n) => n.qualifiedName === qn)!;
  74. const calleesOf = (qn: string) => cg.getCallees(method(qn).id).map(({ node }) => node.qualifiedName).sort();
  75. describe('this.<field>.<method>() (#1496)', () => {
  76. it('resolves on the field\'s declared type even when the wrapper shares the method name', () => {
  77. expect(calleesOf('Notifier::send')).toEqual(['Mailer::send']);
  78. expect(calleesOf('Notifier::other')).toEqual(['Mailer::send']);
  79. // No self-edge anywhere.
  80. const self = cg.getCallers(method('Notifier::send').id).some(({ node }) => node.id === method('Notifier::send').id);
  81. expect(self).toBe(false);
  82. });
  83. it('reads a JS field initialized in the constructor', () => {
  84. expect(calleesOf('LegacyNotifier::send')).toEqual(['LegacyMailer::send']);
  85. });
  86. it('leaves a builtin-typed field unresolved rather than guessing a same-named method', () => {
  87. // `this.items.push()` — `string[]` names no project type; the wrapper `push`
  88. // must not become its own callee.
  89. expect(calleesOf('Notifier::push')).toEqual([]);
  90. });
  91. it('resolves a field typed `typeof <objectLiteral>` onto the literal\'s member', () => {
  92. // The members are bare-named functions inside the constant's extent (#1573).
  93. expect(calleesOf('Keeper::settings')).toEqual(['getSettings']);
  94. expect(calleesOf('Keeper::get')).toEqual(['get']);
  95. const self = cg.getCallers(method('Keeper::get').id).some(({ node }) => node.id === method('Keeper::get').id);
  96. expect(self).toBe(false);
  97. });
  98. });