import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import * as fs from 'node:fs'; import * as path from 'node:path'; import * as os from 'node:os'; import { CodeGraph } from '../src'; /** * #841 — React components declared via an HOC wrapper * (`const Button = forwardRef(...)`, `memo(...)`, `styled.x\`…\``) were indexed * as plain `constant` nodes, so their JSX usages (`; } ` ); const db = await index(); // The render edge exists and is the synthesized jsx-render kind. const edgeRows = db .prepare( `SELECT s.name caller FROM edges e JOIN nodes s ON s.id = e.source JOIN nodes t ON t.id = e.target WHERE json_extract(e.metadata, '$.synthesizedBy') = 'jsx-render' AND t.kind = 'component' AND t.name = 'Button'` ) .all(); expect(edgeRows.map((r: any) => r.caller)).toContain('Page'); // ...and it surfaces through the public callers API (the issue's symptom: // "No callers found" before the fix). const buttonId = db .prepare("SELECT id FROM nodes WHERE name='Button' AND kind='component'") .get().id as string; const callers = cg.getCallers(buttonId).map((c: any) => c.node.name); expect(callers).toContain('Page'); }); it('captures the inner render-fn body callees under the component', async () => { fs.writeFileSync( path.join(dir, 'widget.tsx'), `import * as React from 'react'; function useThing() { return 1; } export const Widget = React.forwardRef((props, ref) => { const v = useThing(); return
{v}
; }); ` ); const db = await index(); const rows = db .prepare( `SELECT t.name FROM edges e JOIN nodes s ON s.id = e.source JOIN nodes t ON t.id = e.target WHERE s.name = 'Widget' AND s.kind = 'component' AND e.kind = 'calls' AND t.name = 'useThing'` ) .all(); expect(rows.length).toBeGreaterThanOrEqual(1); }); it('does not misclassify non-component PascalCase consts (precision)', async () => { fs.writeFileSync( path.join(dir, 'controls.tsx'), `import * as React from 'react'; const cache = memo(expensiveFn); export const Config = loadConfig(); export const Client = new ApiClient(); export const Styles = styledHelper(); export const Total = [1, 2].reduce((a, b) => a + b, 0); export const Theme = { color: 'red' }; ` ); const db = await index(); for (const name of ['Config', 'Client', 'Styles', 'Total', 'Theme']) { expect(kindsOf(db, name), `${name} must stay a constant`).toContain('constant'); expect(kindsOf(db, name), `${name} must not be a component`).not.toContain('component'); } // A lowercase-named memo() result is a memoization util, not a component. expect(kindsOf(db, 'cache')).not.toContain('component'); }); });