/** * A function passed as a value whose name the file imports from its own * project names what that import binds. When the import resolver binds it to * something that is not a function, the function-as-value matcher still fell * back to the one project function of that name elsewhere: * - outline's `import isCloudHosted from '~/utils/isCloudHosted'` (a boolean * constant) landed on the server's `isCloudHosted()` policy helper, and its * `Star` model class on the `` icon component; * - outline's `import * as Slack from '../slack'` (a module object) landed on * the Slack plugin's settings component; * - topcoder's `import TCO21 from './themes/TCO21.scss'` landed on the TCO21 * community's routes, and cord-field's `.graphql` document on a component. * * A wrapper declared beside the function it wraps (`export default * styled(Disclosure)`) and a lazy loader (`lazy(() => import('../pages/Home'))`) * still lead to the namesake, as does an import the import resolver can't * follow at all (a barrel it doesn't read). */ import { describe, it, expect, afterAll } from 'vitest'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { CodeGraph } from '../src'; const roots: string[] = []; afterAll(() => { for (const r of roots.splice(0)) fs.rmSync(r, { recursive: true, force: true }); }); async function indexProject(files: Record): Promise { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-fnref-project-')); roots.push(root); for (const [rel, content] of Object.entries(files)) { fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true }); fs.writeFileSync(path.join(root, rel), content); } return CodeGraph.init(root, { index: true }); } /** ` -> :` for each function-as-value edge leaving `file`. */ function fnRefsFrom(cg: CodeGraph, file: string): string[] { const ids = cg.getNodesInFile(file).map((n) => n.id); return [...new Set(cg .getOutgoingEdgesFrom(ids) .filter((e) => e.metadata?.fnRef === true) .map((e) => { const target = cg.getNode(e.target); return `${String(e.metadata?.refName)} -> ${target?.filePath}:${target?.qualifiedName}`; }))] .sort(); } const PACKAGE_JSON = JSON.stringify({ name: 'wiki', dependencies: { react: '^18.2.0', 'styled-components': '^6.1.0' } }); const TSCONFIG = JSON.stringify({ compilerOptions: { baseUrl: '.', jsx: 'react-jsx' } }); describe('a function value the file imports from its own project', () => { it('names the constant, class or module the import binds, not a function elsewhere', async () => { const cg = await indexProject({ 'package.json': PACKAGE_JSON, 'tsconfig.json': TSCONFIG, // What the imports bind: a constant, a model class and a module. 'app/utils/isCloudHosted.ts': `export const isCloudHosted = window.location.host.endsWith('.example.com'); `, 'app/models/Star.ts': `export default class Star { id = ''; } `, 'plugins/slack/server/slack.ts': `export function post(text: string): string { return text; } `, // The project's only functions of those names, somewhere else. 'server/policies/utils.ts': `export function isCloudHosted(): boolean { return true; } `, 'app/components/Star.tsx': `export function Star() { return ; } `, 'plugins/slack/client/Settings.tsx': `export function Slack() { return
; } `, 'app/scenes/Settings.ts': `import { isCloudHosted } from '../utils/isCloudHosted'; import Star from '../models/Star'; export function settings(register: (value: unknown) => void): void { register(isCloudHosted); register(Star); } `, 'plugins/slack/server/hooks.test.ts': `import * as Slack from './slack'; export function spyOn(spy: (module: unknown) => void): void { spy(Slack); } `, }); try { expect({ settings: fnRefsFrom(cg, 'app/scenes/Settings.ts'), hooks: fnRefsFrom(cg, 'plugins/slack/server/hooks.test.ts'), }).toEqual({ settings: [], hooks: [] }); } finally { cg.close(); } }, 60_000); it('never names a function when it imports a stylesheet, an image or a GraphQL document', async () => { const cg = await indexProject({ 'package.json': PACKAGE_JSON, 'tsconfig.json': TSCONFIG, 'src/components/Countdown/themes/TCO21.scss': `.countdown { color: red; } `, 'src/assets/img-QA.png': '', 'src/assets/visibility.svg': ` `, 'src/scenes/Story/Story.graphql': `mutation CreateStory { createStory { id } } `, // The project's only functions of those names. 'src/routes/TCO21/Routes.tsx': `export function TCO21() { return
; } `, 'src/routes/QA/Routes.tsx': `export function QA() { return
; } `, 'src/components/VisibilityIcon.tsx': `export function VisibilityIcon() { return ; } `, 'src/scenes/Story/CreateStory.tsx': `export function CreateStory() { return
; } `, 'src/components/Countdown/index.tsx': `import TCO21 from './themes/TCO21.scss'; import QA from '../../assets/img-QA.png'; import VisibilityIcon from '../../assets/visibility.svg?react'; import { CreateStoryDocument as CreateStory } from '../../scenes/Story/Story.graphql'; export function themes(pick: (value: unknown) => void): void { pick(TCO21); pick(QA); pick(VisibilityIcon); pick(CreateStory); } `, }); try { expect(fnRefsFrom(cg, 'src/components/Countdown/index.tsx')).toEqual([]); } finally { cg.close(); } }, 60_000); it('keeps a wrapper beside its function, a lazy loader, a barrel and a plain import', async () => { const cg = await indexProject({ 'package.json': PACKAGE_JSON, 'tsconfig.json': TSCONFIG, // The default export wraps the function declared beside it. 'app/components/Disclosure.tsx': `import styled from 'styled-components'; function Disclosure(props: { open: boolean }) { return