value-reference-edges.test.ts 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724
  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 module-level const/static (Rust)', async () => {
  114. fs.writeFileSync(
  115. path.join(dir, 'lib.rs'),
  116. [
  117. 'const MAX_RETRIES: u32 = 3;',
  118. 'static DEFAULT_LABEL: &str = "prod";',
  119. '',
  120. 'fn retry() -> u32 { MAX_RETRIES }',
  121. "fn label() -> &'static str { DEFAULT_LABEL }",
  122. ].join('\n'),
  123. );
  124. cg = index();
  125. await cg.indexAll();
  126. expect(valueRefReaders(cg, 'MAX_RETRIES')).toEqual(expect.arrayContaining(['retry']));
  127. expect(valueRefReaders(cg, 'DEFAULT_LABEL')).toEqual(expect.arrayContaining(['label']));
  128. });
  129. it('does NOT edge a Rust const shadowed by a local let of the same name', async () => {
  130. fs.writeFileSync(
  131. path.join(dir, 'shadow.rs'),
  132. [
  133. 'const TIMEOUT: u32 = 30;',
  134. '',
  135. 'fn uses_const() -> u32 { TIMEOUT }',
  136. 'fn shadows() -> u32 {',
  137. ' let TIMEOUT = 5;',
  138. ' TIMEOUT',
  139. '}',
  140. ].join('\n'),
  141. );
  142. cg = index();
  143. await cg.indexAll();
  144. expect(valueRefReaders(cg, 'TIMEOUT')).toEqual([]);
  145. });
  146. it('edges same-file readers to a package-level const/var (Go)', async () => {
  147. fs.writeFileSync(
  148. path.join(dir, 'main.go'),
  149. [
  150. 'package main',
  151. '',
  152. 'const MaxRetries = 3',
  153. 'var DefaultLabels = map[string]string{"env": "prod"}',
  154. '',
  155. 'func retry() int { return MaxRetries }',
  156. 'func labels() map[string]string { return DefaultLabels }',
  157. ].join('\n'),
  158. );
  159. cg = index();
  160. await cg.indexAll();
  161. expect(valueRefReaders(cg, 'MaxRetries')).toEqual(expect.arrayContaining(['retry']));
  162. expect(valueRefReaders(cg, 'DefaultLabels')).toEqual(expect.arrayContaining(['labels']));
  163. });
  164. it('does NOT edge a Go package const shadowed by a local := of the same name', async () => {
  165. // `Timeout` is a package const AND a local `:=` (short_var_declaration) in
  166. // shadows(). The local read resolves to the inner binding, so a file-scope
  167. // edge would be a false positive — the shadow prune drops the whole target.
  168. fs.writeFileSync(
  169. path.join(dir, 'shadow.go'),
  170. [
  171. 'package main',
  172. '',
  173. 'const Timeout = 30',
  174. '',
  175. 'func usesConst() int { return Timeout }',
  176. 'func shadows() int {',
  177. '\tTimeout := 5',
  178. '\treturn Timeout',
  179. '}',
  180. ].join('\n'),
  181. );
  182. cg = index();
  183. await cg.indexAll();
  184. expect(valueRefReaders(cg, 'Timeout')).toEqual([]);
  185. });
  186. it('keeps a conditionally-defined module const (try/except), not a shadow (Python)', async () => {
  187. // `HAS_SSL` is defined twice but BOTH at module scope (a conditional def, a
  188. // very common Python idiom). It is one logical const, not a shadow, so its
  189. // reader must stay edged — and the two halves must not edge each other.
  190. fs.writeFileSync(
  191. path.join(dir, 'cond.py'),
  192. [
  193. 'try:',
  194. '\tHAS_SSL = True',
  195. 'except ImportError:',
  196. '\tHAS_SSL = False',
  197. '',
  198. 'def uses_ssl():',
  199. '\treturn HAS_SSL',
  200. ].join('\n'),
  201. );
  202. cg = index();
  203. await cg.indexAll();
  204. expect(valueRefReaders(cg, 'HAS_SSL')).toEqual(['uses_ssl']);
  205. });
  206. it('edges readers to a top-level AND a class-internal constant (Ruby)', async () => {
  207. // Ruby keeps almost all constants inside a class/module. Both the top-level
  208. // `MAX_RETRIES` and the class-internal `Config::TIMEOUT` must be targets, and
  209. // their same-file readers edged (TIMEOUT is read by two methods of Config).
  210. fs.writeFileSync(
  211. path.join(dir, 'app.rb'),
  212. [
  213. 'MAX_RETRIES = 3',
  214. '',
  215. 'def retry_count',
  216. ' MAX_RETRIES',
  217. 'end',
  218. '',
  219. 'class Config',
  220. ' TIMEOUT = 30',
  221. ' def self.get_timeout',
  222. ' TIMEOUT',
  223. ' end',
  224. ' def describe',
  225. ' "timeout=#{TIMEOUT}"',
  226. ' end',
  227. 'end',
  228. ].join('\n'),
  229. );
  230. cg = index();
  231. await cg.indexAll();
  232. expect(valueRefReaders(cg, 'MAX_RETRIES')).toEqual(expect.arrayContaining(['retry_count']));
  233. expect(valueRefReaders(cg, 'TIMEOUT')).toEqual(expect.arrayContaining(['get_timeout', 'describe']));
  234. });
  235. it('edges same-file readers to a file-scope const/table (C)', async () => {
  236. // C keeps shareable values at file scope as `static const` — scalars and,
  237. // very commonly, pointer/array lookup tables. Both must be extracted as
  238. // nodes (the generic fallback misses C's nested init_declarator name) and
  239. // their same-file readers edged.
  240. fs.writeFileSync(
  241. path.join(dir, 'config.c'),
  242. [
  243. 'static const int MAX_ITEMS = 100;',
  244. 'static const char *const STATUS_NAMES[] = { "ok", "fail", "pending" };',
  245. '',
  246. 'int capped(int n) { return n > MAX_ITEMS ? MAX_ITEMS : n; }',
  247. 'const char *label(int i) { return STATUS_NAMES[i]; }',
  248. ].join('\n'),
  249. );
  250. cg = index();
  251. await cg.indexAll();
  252. expect(valueRefReaders(cg, 'MAX_ITEMS')).toEqual(expect.arrayContaining(['capped']));
  253. expect(valueRefReaders(cg, 'STATUS_NAMES')).toEqual(expect.arrayContaining(['label']));
  254. });
  255. it('does NOT edge a C file const shadowed by a function-local of the same name', async () => {
  256. // `TIMEOUT` is a file const AND a local `int TIMEOUT = 5` (init_declarator)
  257. // in shadows(). The local read resolves to the inner binding, so a
  258. // file-scope edge would be a false positive — the shadow prune drops it.
  259. fs.writeFileSync(
  260. path.join(dir, 'shadow.c'),
  261. [
  262. 'static const int TIMEOUT = 30;',
  263. '',
  264. 'int uses_const(void) { return TIMEOUT; }',
  265. 'int shadows(void) {',
  266. ' int TIMEOUT = 5;',
  267. ' return TIMEOUT;',
  268. '}',
  269. ].join('\n'),
  270. );
  271. cg = index();
  272. await cg.indexAll();
  273. expect(valueRefReaders(cg, 'TIMEOUT')).toEqual([]);
  274. });
  275. it('does NOT mint a value target from a macro-prefixed C prototype (return-type misparse)', async () => {
  276. // A prototype led by an unknown macro (`CURL_EXTERN CURLcode fn(args);`)
  277. // makes tree-sitter-c misparse it as a declaration whose "variable" is the
  278. // bare return-type identifier — which would mint a spurious `CURLcode`
  279. // value target read by every function of that type. The bare-identifier
  280. // skip prevents it, while real file-scope consts still edge their readers.
  281. fs.writeFileSync(
  282. path.join(dir, 'api.c'),
  283. [
  284. 'typedef enum { CURLE_OK, CURLE_FAIL } CURLcode;',
  285. 'CURL_EXTERN CURLcode curl_easy_init(int x);',
  286. 'CURL_EXTERN CURLcode curl_easy_setopt(int y);',
  287. '',
  288. 'static const int REAL_LIMIT = 42;',
  289. 'int use_real(void) { return REAL_LIMIT; }',
  290. ].join('\n'),
  291. );
  292. cg = index();
  293. await cg.indexAll();
  294. // The return-type name is never extracted as a const/var, so it is not a
  295. // value-ref target at all.
  296. const curlcodeValues = cg
  297. .searchNodes('CURLcode')
  298. .map((r) => r.node)
  299. .filter((n) => n.name === 'CURLcode' && (n.kind === 'constant' || n.kind === 'variable'));
  300. expect(curlcodeValues).toEqual([]);
  301. // Real file-scope consts alongside the misparse-prone prototypes still work.
  302. expect(valueRefReaders(cg, 'REAL_LIMIT')).toEqual(expect.arrayContaining(['use_real']));
  303. });
  304. it('edges same-file methods to a class-scope static final constant (Java)', async () => {
  305. // Java keeps constants as `static final` fields inside a class. They extract
  306. // as `constant` kind (not `field`) so the value-ref gate targets them; a
  307. // plain instance `final` field is NOT a constant and must not be a target.
  308. fs.writeFileSync(
  309. path.join(dir, 'Limits.java'),
  310. [
  311. 'class Limits {',
  312. ' public static final int MAX_ITEMS = 100;',
  313. ' static final String[] STATUS_NAMES = { "ok", "fail" };',
  314. ' final int instanceId = 1;',
  315. ' int capped(int n) { return n > MAX_ITEMS ? MAX_ITEMS : n; }',
  316. ' String label(int i) { return STATUS_NAMES[i]; }',
  317. ' int id() { return instanceId; }',
  318. '}',
  319. ].join('\n'),
  320. );
  321. cg = index();
  322. await cg.indexAll();
  323. expect(valueRefReaders(cg, 'MAX_ITEMS')).toEqual(expect.arrayContaining(['capped']));
  324. expect(valueRefReaders(cg, 'STATUS_NAMES')).toEqual(expect.arrayContaining(['label']));
  325. // An instance `final` field is mutable per-object state, not a shared
  326. // constant — it stays `field` kind and is never a value-ref target.
  327. expect(valueRefReaders(cg, 'instanceId')).toEqual([]);
  328. });
  329. it('does NOT edge a Java class const shadowed by a method-local of the same name', async () => {
  330. fs.writeFileSync(
  331. path.join(dir, 'Shadow.java'),
  332. [
  333. 'class Shadow {',
  334. ' static final int TIMEOUT = 30;',
  335. ' int usesConst() { return TIMEOUT; }',
  336. ' int shadows() { int TIMEOUT = 5; return TIMEOUT; }',
  337. '}',
  338. ].join('\n'),
  339. );
  340. cg = index();
  341. await cg.indexAll();
  342. expect(valueRefReaders(cg, 'TIMEOUT')).toEqual([]);
  343. });
  344. it('edges same-file methods to a class const / static readonly (C#)', async () => {
  345. // C# constants are `const` (compile-time) or `static readonly` (runtime);
  346. // both extract as `constant`. An instance `readonly` field is per-object and
  347. // stays `field`.
  348. fs.writeFileSync(
  349. path.join(dir, 'Limits.cs'),
  350. [
  351. 'class Limits {',
  352. ' const int MAX_ITEMS = 100;',
  353. ' static readonly string[] STATUS_NAMES = { "ok", "fail" };',
  354. ' readonly int instanceId = 1;',
  355. ' int Capped(int n) { return n > MAX_ITEMS ? MAX_ITEMS : n; }',
  356. ' string Label(int i) { return STATUS_NAMES[i]; }',
  357. ' int Id() { return instanceId; }',
  358. '}',
  359. ].join('\n'),
  360. );
  361. cg = index();
  362. await cg.indexAll();
  363. expect(valueRefReaders(cg, 'MAX_ITEMS')).toEqual(expect.arrayContaining(['Capped']));
  364. expect(valueRefReaders(cg, 'STATUS_NAMES')).toEqual(expect.arrayContaining(['Label']));
  365. expect(valueRefReaders(cg, 'instanceId')).toEqual([]);
  366. });
  367. it('does NOT edge a C# class const shadowed by a method-local of the same name', async () => {
  368. fs.writeFileSync(
  369. path.join(dir, 'Shadow.cs'),
  370. [
  371. 'class Shadow {',
  372. ' const int TIMEOUT = 30;',
  373. ' int UsesConst() { return TIMEOUT; }',
  374. ' int Shadows() { int TIMEOUT = 5; return TIMEOUT; }',
  375. '}',
  376. ].join('\n'),
  377. );
  378. cg = index();
  379. await cg.indexAll();
  380. expect(valueRefReaders(cg, 'TIMEOUT')).toEqual([]);
  381. });
  382. it('edges same-file readers to a top-level and class const, incl. self:: / Class:: (PHP)', async () => {
  383. // PHP keeps constants at file scope (`const X`) and inside classes (`const
  384. // X`), both extracted as `constant`. A constant *reference* is a `name` node
  385. // (bare `X`, or the const half of `self::X` / `Foo::X`), so the reader-scan
  386. // must match `name`. A `$var` local is a different namespace and can never
  387. // shadow a bare constant — so there is nothing to prune.
  388. fs.writeFileSync(
  389. path.join(dir, 'Config.php'),
  390. [
  391. '<?php',
  392. 'const APP_VERSION = "1.0";',
  393. 'class Config {',
  394. ' const MAX_ITEMS = 100;',
  395. ' const STATUS_NAMES = ["ok", "fail"];',
  396. ' public static $counter = 0;',
  397. ' function capped($n) { return $n > self::MAX_ITEMS ? self::MAX_ITEMS : $n; }',
  398. ' function label($i) { return Config::STATUS_NAMES[$i]; }',
  399. ' function version() { return APP_VERSION; }',
  400. '}',
  401. ].join('\n'),
  402. );
  403. cg = index();
  404. await cg.indexAll();
  405. expect(valueRefReaders(cg, 'MAX_ITEMS')).toEqual(expect.arrayContaining(['capped']));
  406. expect(valueRefReaders(cg, 'STATUS_NAMES')).toEqual(expect.arrayContaining(['label']));
  407. expect(valueRefReaders(cg, 'APP_VERSION')).toEqual(expect.arrayContaining(['version']));
  408. // A static property is mutable class state, not a constant — never a target.
  409. expect(valueRefReaders(cg, 'counter')).toEqual([]);
  410. });
  411. it('edges readers to a top-level and object-scope val, not a class instance val (Scala)', async () => {
  412. // Scala has no `static`: an `object` is a singleton, so its `val`s are the
  413. // shared-constant idiom (extracted as `constant`, like a top-level val). A
  414. // `class` val is a per-instance immutable field (`field`, never a target).
  415. fs.writeFileSync(
  416. path.join(dir, 'Demo.scala'),
  417. [
  418. 'val AppVersion = "1.0"',
  419. 'object Config {',
  420. ' val TIMEOUT_MS = 30',
  421. ' val STATUS_NAMES = List("ok", "fail")',
  422. ' def capped(n: Int): Int = if (n > TIMEOUT_MS) TIMEOUT_MS else n',
  423. ' def label(i: Int): String = STATUS_NAMES(i)',
  424. '}',
  425. 'class Widget {',
  426. ' val MaxItems = 100',
  427. ' def within(n: Int): Int = if (n < MaxItems) n else MaxItems',
  428. '}',
  429. ].join('\n'),
  430. );
  431. cg = index();
  432. await cg.indexAll();
  433. expect(valueRefReaders(cg, 'TIMEOUT_MS')).toEqual(expect.arrayContaining(['capped']));
  434. expect(valueRefReaders(cg, 'STATUS_NAMES')).toEqual(expect.arrayContaining(['label']));
  435. // A class instance `val` is per-object state (kind `field`), not a shared
  436. // constant — never a value-ref target even though `within` reads it.
  437. expect(valueRefReaders(cg, 'MaxItems')).toEqual([]);
  438. });
  439. it('does NOT edge a Scala object val shadowed by a method-local val of the same name', async () => {
  440. fs.writeFileSync(
  441. path.join(dir, 'Shadow.scala'),
  442. [
  443. 'object Config {',
  444. ' val TIMEOUT = 30',
  445. ' def usesConst(): Int = TIMEOUT',
  446. ' def shadows(): Int = { val TIMEOUT = 5; TIMEOUT }',
  447. '}',
  448. ].join('\n'),
  449. );
  450. cg = index();
  451. await cg.indexAll();
  452. expect(valueRefReaders(cg, 'TIMEOUT')).toEqual([]);
  453. });
  454. it('edges readers to top-level, object, and companion-object constants, not a class val (Kotlin)', async () => {
  455. // Kotlin has no `static`: a top-level property, an `object` (singleton), and a
  456. // class's `companion object` all hold shared constants (`val`→constant). A
  457. // class instance `val` is per-object state (`field`, never a target). The
  458. // property name nests as variable_declaration→simple_identifier, and a const
  459. // reference is a `simple_identifier`.
  460. fs.writeFileSync(
  461. path.join(dir, 'Demo.kt'),
  462. [
  463. 'const val TOP_LEVEL_MAX = 100',
  464. 'object Config {',
  465. ' const val TIMEOUT_MS = 30',
  466. ' val STATUS_NAMES = listOf("ok", "fail")',
  467. ' fun capped(n: Int): Int = if (n > TIMEOUT_MS) TIMEOUT_MS else n',
  468. ' fun label(i: Int): String = STATUS_NAMES[i]',
  469. '}',
  470. 'class Widget {',
  471. ' companion object { const val MAX_RETRIES = 3 }',
  472. ' val instanceField = 1',
  473. ' fun retries(): Int = MAX_RETRIES',
  474. ' fun within(n: Int): Int = if (n < TOP_LEVEL_MAX) n else TOP_LEVEL_MAX',
  475. '}',
  476. ].join('\n'),
  477. );
  478. cg = index();
  479. await cg.indexAll();
  480. expect(valueRefReaders(cg, 'STATUS_NAMES')).toEqual(expect.arrayContaining(['label']));
  481. expect(valueRefReaders(cg, 'MAX_RETRIES')).toEqual(expect.arrayContaining(['retries']));
  482. expect(valueRefReaders(cg, 'TOP_LEVEL_MAX')).toEqual(expect.arrayContaining(['within']));
  483. // A class instance `val` is per-object state (kind `field`), never a target.
  484. expect(valueRefReaders(cg, 'instanceField')).toEqual([]);
  485. });
  486. it('does NOT edge a Kotlin object const shadowed by a method-local val of the same name', async () => {
  487. fs.writeFileSync(
  488. path.join(dir, 'Shadow.kt'),
  489. [
  490. 'object Config {',
  491. ' const val TIMEOUT = 30',
  492. ' fun usesConst(): Int = TIMEOUT',
  493. ' fun shadows(): Int { val TIMEOUT = 5; return TIMEOUT }',
  494. '}',
  495. ].join('\n'),
  496. );
  497. cg = index();
  498. await cg.indexAll();
  499. expect(valueRefReaders(cg, 'TIMEOUT')).toEqual([]);
  500. });
  501. it('edges readers to a top-level let and static let in enum/struct, not an instance let (Swift)', async () => {
  502. // Swift has no `static` keyword for globals; the shared-constant idiom is a
  503. // top-level `let` or a `static let` inside a type — Swift namespaces these in
  504. // `enum`/`struct`. Those extract as `constant`; an instance stored `let` is
  505. // per-object (`field`, never a target); a *computed* property is skipped.
  506. fs.writeFileSync(
  507. path.join(dir, 'Demo.swift'),
  508. [
  509. 'let topLevelMax = 100',
  510. 'enum Constants {',
  511. ' static let TIMEOUT_MS = 30',
  512. ' static let STATUS_NAMES = ["ok", "fail"]',
  513. '}',
  514. 'struct Widget {',
  515. ' static let MAX_RETRIES = 3',
  516. ' let instanceField = 1',
  517. ' func retries() -> Int { return Widget.MAX_RETRIES }',
  518. ' func within(_ n: Int) -> Int { return n < topLevelMax ? n : topLevelMax }',
  519. '}',
  520. 'func labels(_ i: Int) -> String { return Constants.STATUS_NAMES[i] }',
  521. ].join('\n'),
  522. );
  523. cg = index();
  524. await cg.indexAll();
  525. expect(valueRefReaders(cg, 'STATUS_NAMES')).toEqual(expect.arrayContaining(['labels']));
  526. expect(valueRefReaders(cg, 'MAX_RETRIES')).toEqual(expect.arrayContaining(['retries']));
  527. expect(valueRefReaders(cg, 'topLevelMax')).toEqual(expect.arrayContaining(['within']));
  528. // An instance `let` is per-object state (kind `field`), never a target.
  529. expect(valueRefReaders(cg, 'instanceField')).toEqual([]);
  530. });
  531. it('does NOT edge a Swift static const shadowed by a function-local let of the same name', async () => {
  532. fs.writeFileSync(
  533. path.join(dir, 'Shadow.swift'),
  534. [
  535. 'enum Config {',
  536. ' static let TIMEOUT = 30',
  537. ' static func usesConst() -> Int { return TIMEOUT }',
  538. ' static func shadows() -> Int { let TIMEOUT = 5; return TIMEOUT }',
  539. '}',
  540. ].join('\n'),
  541. );
  542. cg = index();
  543. await cg.indexAll();
  544. expect(valueRefReaders(cg, 'TIMEOUT')).toEqual([]);
  545. });
  546. it('edges readers to a top-level const and a class static const/final (Dart)', async () => {
  547. // Dart's grammar uses `static_final_declaration` for exactly the top-level
  548. // `const`/`final` and class `static const`/`static final` — the shared
  549. // constants — so those extract as `constant`. Instance fields and `var`
  550. // (`initialized_identifier`) and locals (`initialized_variable_definition`)
  551. // are NOT this node, so they never become targets. Dart attaches a method
  552. // body as a sibling of the signature, so the reader-scan pulls that in.
  553. fs.writeFileSync(
  554. path.join(dir, 'demo.dart'),
  555. [
  556. 'const TOP_LEVEL_MAX = 100;',
  557. 'class Config {',
  558. ' static const TIMEOUT_MS = 30;',
  559. ' static final STATUS_NAMES = ["ok", "fail"];',
  560. ' final int instanceField = 1;',
  561. ' int capped(int n) => n > TIMEOUT_MS ? TIMEOUT_MS : n;',
  562. ' String label(int i) { return STATUS_NAMES[i]; }',
  563. ' int withinLimit(int n) => n < TOP_LEVEL_MAX ? n : TOP_LEVEL_MAX;',
  564. '}',
  565. ].join('\n'),
  566. );
  567. cg = index();
  568. await cg.indexAll();
  569. expect(valueRefReaders(cg, 'TIMEOUT_MS')).toEqual(expect.arrayContaining(['capped']));
  570. expect(valueRefReaders(cg, 'STATUS_NAMES')).toEqual(expect.arrayContaining(['label']));
  571. expect(valueRefReaders(cg, 'TOP_LEVEL_MAX')).toEqual(expect.arrayContaining(['withinLimit']));
  572. // An instance field is per-object state, never a value-ref target.
  573. expect(valueRefReaders(cg, 'instanceField')).toEqual([]);
  574. });
  575. it('does NOT edge a Dart const shadowed by a method-local const of the same name', async () => {
  576. fs.writeFileSync(
  577. path.join(dir, 'shadow.dart'),
  578. [
  579. 'const TIMEOUT = 30;',
  580. 'class C {',
  581. ' int usesConst() => TIMEOUT;',
  582. ' int shadows() { const TIMEOUT = 5; return TIMEOUT; }',
  583. '}',
  584. ].join('\n'),
  585. );
  586. cg = index();
  587. await cg.indexAll();
  588. expect(valueRefReaders(cg, 'TIMEOUT')).toEqual([]);
  589. });
  590. it('edges same-file functions to a unit-scope const (Pascal)', async () => {
  591. // Pascal keeps shareable constants in a `const` section at unit (file) scope
  592. // (and class scope). They already extract as `constant`. A const reference is
  593. // an `identifier`; the catch is that Pascal attaches a proc body (`block`) as
  594. // a sibling of the proc header (`declProc`, the reader scope), so the
  595. // reader-scan pulls in that sibling.
  596. fs.writeFileSync(
  597. path.join(dir, 'demo.pas'),
  598. [
  599. 'unit Demo;',
  600. 'interface',
  601. 'const',
  602. ' MAX_ITEMS = 100;',
  603. " APP_NAME = 'MyApp';",
  604. 'implementation',
  605. 'function Capped(n: Integer): Integer;',
  606. 'begin',
  607. ' if n > MAX_ITEMS then Capped := MAX_ITEMS else Capped := n;',
  608. 'end;',
  609. 'function AppLabel: string;',
  610. 'begin',
  611. ' AppLabel := APP_NAME;',
  612. 'end;',
  613. 'end.',
  614. ].join('\n'),
  615. );
  616. cg = index();
  617. await cg.indexAll();
  618. expect(valueRefReaders(cg, 'MAX_ITEMS')).toEqual(expect.arrayContaining(['Capped']));
  619. expect(valueRefReaders(cg, 'APP_NAME')).toEqual(expect.arrayContaining(['AppLabel']));
  620. });
  621. it('does NOT edge a Pascal unit const shadowed by a function-local const of the same name', async () => {
  622. fs.writeFileSync(
  623. path.join(dir, 'shadow.pas'),
  624. [
  625. 'unit Shadow;',
  626. 'interface',
  627. 'const',
  628. ' TIMEOUT = 30;',
  629. 'implementation',
  630. 'function UsesConst: Integer;',
  631. 'begin',
  632. ' UsesConst := TIMEOUT;',
  633. 'end;',
  634. 'function Shadows: Integer;',
  635. 'const TIMEOUT = 5;',
  636. 'begin',
  637. ' Shadows := TIMEOUT;',
  638. 'end;',
  639. 'end.',
  640. ].join('\n'),
  641. );
  642. cg = index();
  643. await cg.indexAll();
  644. expect(valueRefReaders(cg, 'TIMEOUT')).toEqual([]);
  645. });
  646. it('emits nothing when CODEGRAPH_VALUE_REFS=0', async () => {
  647. const prev = process.env.CODEGRAPH_VALUE_REFS;
  648. process.env.CODEGRAPH_VALUE_REFS = '0';
  649. try {
  650. fs.writeFileSync(
  651. path.join(dir, 'config.ts'),
  652. ['export const TABLE_CONFIG = { rows: 10 };', 'export function rowCount() { return TABLE_CONFIG.rows; }'].join('\n'),
  653. );
  654. cg = index();
  655. await cg.indexAll();
  656. expect(valueRefReaders(cg, 'TABLE_CONFIG')).toEqual([]);
  657. } finally {
  658. if (prev === undefined) delete process.env.CODEGRAPH_VALUE_REFS;
  659. else process.env.CODEGRAPH_VALUE_REFS = prev;
  660. }
  661. });
  662. });