value-reference-edges.test.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. /**
  2. * Value-reference edges (TS/JS): same-file `references` edges from a reader
  3. * symbol to the file-scope const/var it reads, so impact analysis catches
  4. * "change this constant, affect its readers". Default on; CODEGRAPH_VALUE_REFS=0
  5. * disables. See TreeSitterExtractor.flushValueRefs.
  6. */
  7. import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  8. import * as fs from 'fs';
  9. import * as path from 'path';
  10. import * as os from 'os';
  11. import CodeGraph from '../src';
  12. function valueRefReaders(cg: CodeGraph, constName: string): string[] {
  13. const target = cg.searchNodes(constName).map((r) => r.node).find((n) => n.name === constName);
  14. if (!target) return [];
  15. return cg
  16. .getIncomingEdges(target.id)
  17. .filter((e) => e.kind === 'references' && (e.metadata as { valueRef?: boolean } | undefined)?.valueRef)
  18. .map((e) => cg.getNode(e.source)?.name)
  19. .filter((n): n is string => Boolean(n));
  20. }
  21. describe('value-reference edges', () => {
  22. let dir: string;
  23. let cg: CodeGraph | undefined;
  24. beforeEach(() => {
  25. dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-valueref-'));
  26. });
  27. afterEach(() => {
  28. cg?.destroy();
  29. cg = undefined;
  30. fs.rmSync(dir, { recursive: true, force: true });
  31. });
  32. function index(): CodeGraph {
  33. const g = CodeGraph.initSync(dir, { config: { include: ['**/*.ts', '**/*.tsx'], exclude: [] } });
  34. return g;
  35. }
  36. it('edges same-file readers to the file-scope const they read (default on)', async () => {
  37. fs.writeFileSync(
  38. path.join(dir, 'config.ts'),
  39. [
  40. 'export const TABLE_CONFIG = { rows: 10, cols: 4 };',
  41. 'export function rowCount() { return TABLE_CONFIG.rows; }',
  42. 'export function describeTable() { return `${TABLE_CONFIG.rows}x${TABLE_CONFIG.cols}`; }',
  43. 'export const HEADER = TABLE_CONFIG.cols;',
  44. ].join('\n'),
  45. );
  46. cg = index();
  47. await cg.indexAll();
  48. const readers = valueRefReaders(cg, 'TABLE_CONFIG');
  49. // rowCount, describeTable, and the HEADER const all read TABLE_CONFIG.
  50. expect(readers).toEqual(expect.arrayContaining(['rowCount', 'describeTable', 'HEADER']));
  51. });
  52. it('surfaces those readers in the impact radius of the const', async () => {
  53. fs.writeFileSync(
  54. path.join(dir, 'palette.ts'),
  55. [
  56. 'export const COLOR_PALETTE = { red: "#f00", blue: "#00f" };',
  57. 'export function pickRed() { return COLOR_PALETTE.red; }',
  58. ].join('\n'),
  59. );
  60. cg = index();
  61. await cg.indexAll();
  62. const target = cg.searchNodes('COLOR_PALETTE').map((r) => r.node).find((n) => n.name === 'COLOR_PALETTE')!;
  63. const impacted = [...cg.getImpactRadius(target.id).nodes.values()].map((n) => n.name);
  64. expect(impacted).toContain('pickRed');
  65. });
  66. it('does NOT edge a shadowed const — inner re-declaration makes the name ambiguous', async () => {
  67. // The Emscripten/bundled pattern: a file-scope `const Module` re-declared as
  68. // an inner `var Module` / param. Nested readers resolve to the INNER binding,
  69. // so a file-scope edge would be a false positive. The shadow guard drops it.
  70. fs.writeFileSync(
  71. path.join(dir, 'bundled.ts'),
  72. [
  73. 'const Module = (function () {',
  74. ' return function (Module) {',
  75. ' var Module = typeof Module !== "undefined" ? Module : {};',
  76. ' function locate() { return Module.path; }',
  77. ' function getFunc() { return Module.lookup; }',
  78. ' return { locate, getFunc };',
  79. ' };',
  80. '})();',
  81. 'export default Module;',
  82. ].join('\n'),
  83. );
  84. cg = index();
  85. await cg.indexAll();
  86. // No reader should be edged to the outer `const Module`.
  87. expect(valueRefReaders(cg, 'Module')).toEqual([]);
  88. });
  89. it('emits nothing when CODEGRAPH_VALUE_REFS=0', async () => {
  90. const prev = process.env.CODEGRAPH_VALUE_REFS;
  91. process.env.CODEGRAPH_VALUE_REFS = '0';
  92. try {
  93. fs.writeFileSync(
  94. path.join(dir, 'config.ts'),
  95. ['export const TABLE_CONFIG = { rows: 10 };', 'export function rowCount() { return TABLE_CONFIG.rows; }'].join('\n'),
  96. );
  97. cg = index();
  98. await cg.indexAll();
  99. expect(valueRefReaders(cg, 'TABLE_CONFIG')).toEqual([]);
  100. } finally {
  101. if (prev === undefined) delete process.env.CODEGRAPH_VALUE_REFS;
  102. else process.env.CODEGRAPH_VALUE_REFS = prev;
  103. }
  104. });
  105. });