value-reference-edges.test.ts 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  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. // Aggregate across ALL nodes of this name — a conditionally-defined module
  14. // const (`try: X=…; except: X=…`) has more than one, and the edge targets
  15. // whichever one ended up in the target map.
  16. const targets = cg.searchNodes(constName).map((r) => r.node).filter((n) => n.name === constName);
  17. const readers = new Set<string>();
  18. for (const t of targets) {
  19. for (const e of cg.getIncomingEdges(t.id)) {
  20. if (e.kind === 'references' && (e.metadata as { valueRef?: boolean } | undefined)?.valueRef) {
  21. const r = cg.getNode(e.source)?.name;
  22. if (r) readers.add(r);
  23. }
  24. }
  25. }
  26. return [...readers];
  27. }
  28. describe('value-reference edges', () => {
  29. let dir: string;
  30. let cg: CodeGraph | undefined;
  31. beforeEach(() => {
  32. dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-valueref-'));
  33. });
  34. afterEach(() => {
  35. cg?.destroy();
  36. cg = undefined;
  37. fs.rmSync(dir, { recursive: true, force: true });
  38. });
  39. function index(): CodeGraph {
  40. const g = CodeGraph.initSync(dir, { config: { include: ['**/*.ts', '**/*.tsx'], exclude: [] } });
  41. return g;
  42. }
  43. it('edges same-file readers to the file-scope const they read (default on)', async () => {
  44. fs.writeFileSync(
  45. path.join(dir, 'config.ts'),
  46. [
  47. 'export const TABLE_CONFIG = { rows: 10, cols: 4 };',
  48. 'export function rowCount() { return TABLE_CONFIG.rows; }',
  49. 'export function describeTable() { return `${TABLE_CONFIG.rows}x${TABLE_CONFIG.cols}`; }',
  50. 'export const HEADER = TABLE_CONFIG.cols;',
  51. ].join('\n'),
  52. );
  53. cg = index();
  54. await cg.indexAll();
  55. const readers = valueRefReaders(cg, 'TABLE_CONFIG');
  56. // rowCount, describeTable, and the HEADER const all read TABLE_CONFIG.
  57. expect(readers).toEqual(expect.arrayContaining(['rowCount', 'describeTable', 'HEADER']));
  58. });
  59. it('surfaces those readers in the impact radius of the const', async () => {
  60. fs.writeFileSync(
  61. path.join(dir, 'palette.ts'),
  62. [
  63. 'export const COLOR_PALETTE = { red: "#f00", blue: "#00f" };',
  64. 'export function pickRed() { return COLOR_PALETTE.red; }',
  65. ].join('\n'),
  66. );
  67. cg = index();
  68. await cg.indexAll();
  69. const target = cg.searchNodes('COLOR_PALETTE').map((r) => r.node).find((n) => n.name === 'COLOR_PALETTE')!;
  70. const impacted = [...cg.getImpactRadius(target.id).nodes.values()].map((n) => n.name);
  71. expect(impacted).toContain('pickRed');
  72. });
  73. it('does NOT edge a shadowed const — inner re-declaration makes the name ambiguous', async () => {
  74. // The Emscripten/bundled pattern: a file-scope `const Module` re-declared as
  75. // an inner `var Module` / param. Nested readers resolve to the INNER binding,
  76. // so a file-scope edge would be a false positive. The shadow guard drops it.
  77. fs.writeFileSync(
  78. path.join(dir, 'bundled.ts'),
  79. [
  80. 'const Module = (function () {',
  81. ' return function (Module) {',
  82. ' var Module = typeof Module !== "undefined" ? Module : {};',
  83. ' function locate() { return Module.path; }',
  84. ' function getFunc() { return Module.lookup; }',
  85. ' return { locate, getFunc };',
  86. ' };',
  87. '})();',
  88. 'export default Module;',
  89. ].join('\n'),
  90. );
  91. cg = index();
  92. await cg.indexAll();
  93. // No reader should be edged to the outer `const Module`.
  94. expect(valueRefReaders(cg, 'Module')).toEqual([]);
  95. });
  96. it('edges readers that use the const only inside JSX (.tsx)', async () => {
  97. // The tsx-specific path: the const is read ONLY inside JSX expressions, so
  98. // the reader-scan must descend into the JSX subtree to find it.
  99. fs.writeFileSync(
  100. path.join(dir, 'widget.tsx'),
  101. [
  102. 'export const THEME_TOKENS = { color: "red", size: 12 };',
  103. 'export function Label() {',
  104. ' return <span style={{ color: THEME_TOKENS.color }}>hi</span>;',
  105. '}',
  106. 'export const Box = () => <div data-size={THEME_TOKENS.size} />;',
  107. ].join('\n'),
  108. );
  109. cg = index();
  110. await cg.indexAll();
  111. expect(valueRefReaders(cg, 'THEME_TOKENS')).toEqual(expect.arrayContaining(['Label', 'Box']));
  112. });
  113. it('edges same-file readers to a package-level const/var (Go)', async () => {
  114. fs.writeFileSync(
  115. path.join(dir, 'main.go'),
  116. [
  117. 'package main',
  118. '',
  119. 'const MaxRetries = 3',
  120. 'var DefaultLabels = map[string]string{"env": "prod"}',
  121. '',
  122. 'func retry() int { return MaxRetries }',
  123. 'func labels() map[string]string { return DefaultLabels }',
  124. ].join('\n'),
  125. );
  126. cg = index();
  127. await cg.indexAll();
  128. expect(valueRefReaders(cg, 'MaxRetries')).toEqual(expect.arrayContaining(['retry']));
  129. expect(valueRefReaders(cg, 'DefaultLabels')).toEqual(expect.arrayContaining(['labels']));
  130. });
  131. it('does NOT edge a Go package const shadowed by a local := of the same name', async () => {
  132. // `Timeout` is a package const AND a local `:=` (short_var_declaration) in
  133. // shadows(). The local read resolves to the inner binding, so a file-scope
  134. // edge would be a false positive — the shadow prune drops the whole target.
  135. fs.writeFileSync(
  136. path.join(dir, 'shadow.go'),
  137. [
  138. 'package main',
  139. '',
  140. 'const Timeout = 30',
  141. '',
  142. 'func usesConst() int { return Timeout }',
  143. 'func shadows() int {',
  144. '\tTimeout := 5',
  145. '\treturn Timeout',
  146. '}',
  147. ].join('\n'),
  148. );
  149. cg = index();
  150. await cg.indexAll();
  151. expect(valueRefReaders(cg, 'Timeout')).toEqual([]);
  152. });
  153. it('keeps a conditionally-defined module const (try/except), not a shadow (Python)', async () => {
  154. // `HAS_SSL` is defined twice but BOTH at module scope (a conditional def, a
  155. // very common Python idiom). It is one logical const, not a shadow, so its
  156. // reader must stay edged — and the two halves must not edge each other.
  157. fs.writeFileSync(
  158. path.join(dir, 'cond.py'),
  159. [
  160. 'try:',
  161. '\tHAS_SSL = True',
  162. 'except ImportError:',
  163. '\tHAS_SSL = False',
  164. '',
  165. 'def uses_ssl():',
  166. '\treturn HAS_SSL',
  167. ].join('\n'),
  168. );
  169. cg = index();
  170. await cg.indexAll();
  171. expect(valueRefReaders(cg, 'HAS_SSL')).toEqual(['uses_ssl']);
  172. });
  173. it('emits nothing when CODEGRAPH_VALUE_REFS=0', async () => {
  174. const prev = process.env.CODEGRAPH_VALUE_REFS;
  175. process.env.CODEGRAPH_VALUE_REFS = '0';
  176. try {
  177. fs.writeFileSync(
  178. path.join(dir, 'config.ts'),
  179. ['export const TABLE_CONFIG = { rows: 10 };', 'export function rowCount() { return TABLE_CONFIG.rows; }'].join('\n'),
  180. );
  181. cg = index();
  182. await cg.indexAll();
  183. expect(valueRefReaders(cg, 'TABLE_CONFIG')).toEqual([]);
  184. } finally {
  185. if (prev === undefined) delete process.env.CODEGRAPH_VALUE_REFS;
  186. else process.env.CODEGRAPH_VALUE_REFS = prev;
  187. }
  188. });
  189. });