From 9658fa0e21d1ebcb58ff1bb63a982e648d3da441 Mon Sep 17 00:00:00 2001 From: jiangyq9 Date: Sat, 27 Jun 2026 10:15:13 +0800 Subject: [PATCH] feat(cli): expose circular dependency detection --- __tests__/cli-cycles.test.ts | 62 ++++++++++++++++++++++++++++++++++++ src/bin/codegraph.ts | 40 +++++++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 __tests__/cli-cycles.test.ts diff --git a/__tests__/cli-cycles.test.ts b/__tests__/cli-cycles.test.ts new file mode 100644 index 000000000..9f2da2020 --- /dev/null +++ b/__tests__/cli-cycles.test.ts @@ -0,0 +1,62 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { execFileSync } from 'child_process'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { CodeGraph } from '../src'; + +const BIN = path.resolve(__dirname, '../dist/bin/codegraph.js'); + +function runCycles(cwd: string, args: string[] = []): string { + return execFileSync(process.execPath, [BIN, 'cycles', cwd, ...args], { + encoding: 'utf-8', + env: { ...process.env, CODEGRAPH_NO_DAEMON: '1', CODEGRAPH_WASM_RELAUNCHED: '1' }, + stdio: ['ignore', 'pipe', 'pipe'], + }); +} + +describe('codegraph cycles CLI', () => { + let tempDir: string; + + beforeEach(() => { + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cycles-cli-')); + fs.mkdirSync(path.join(tempDir, 'src')); + }); + + afterEach(() => { + fs.rmSync(tempDir, { recursive: true, force: true }); + }); + + it('prints circular file dependencies as JSON', async () => { + fs.writeFileSync( + path.join(tempDir, 'src/a.ts'), + "import { b } from './b';\nexport function a(){ return b(); }\n", + ); + fs.writeFileSync( + path.join(tempDir, 'src/b.ts'), + "import { a } from './a';\nexport function b(){ return a(); }\n", + ); + + const cg = CodeGraph.initSync(tempDir); + await cg.indexAll(); + cg.close(); + + const parsed = JSON.parse(runCycles(tempDir, ['--json'])) as { count: number; cycles: string[][] }; + expect(parsed.count).toBeGreaterThan(0); + expect(parsed.cycles.some((cycle) => cycle.includes('src/a.ts') && cycle.includes('src/b.ts'))).toBe(true); + }); + + it('prints a friendly message when no cycles are found', async () => { + fs.writeFileSync(path.join(tempDir, 'src/a.ts'), 'export function a(){ return 1; }\n'); + fs.writeFileSync( + path.join(tempDir, 'src/b.ts'), + "import { a } from './a';\nexport function b(){ return a(); }\n", + ); + + const cg = CodeGraph.initSync(tempDir); + await cg.indexAll(); + cg.close(); + + expect(runCycles(tempDir)).toContain('No circular dependencies found'); + }); +}); diff --git a/src/bin/codegraph.ts b/src/bin/codegraph.ts index 3033bc476..d66264f58 100644 --- a/src/bin/codegraph.ts +++ b/src/bin/codegraph.ts @@ -20,6 +20,7 @@ * codegraph callees Find what a function/method calls * codegraph impact Analyze what code is affected by changing a symbol * codegraph affected [files] Find test files affected by changes + * codegraph cycles [path] Find circular file dependencies * codegraph upgrade [version] Update CodeGraph to the latest release */ @@ -976,6 +977,45 @@ program } }); +/** + * codegraph cycles [path] + */ +program + .command('cycles [path]') + .description('Find circular file dependencies in the indexed project') + .option('-j, --json', 'Output as JSON') + .action(async (pathArg: string | undefined, options: { json?: boolean }) => { + const projectPath = resolveProjectPath(pathArg); + + try { + if (!isInitialized(projectPath)) { + error(`CodeGraph not initialized in ${projectPath}`); + process.exit(1); + } + + const { default: CodeGraph } = await loadCodeGraph(); + const cg = await CodeGraph.open(projectPath); + const cycles = cg.findCircularDependencies(); + + if (options.json) { + console.log(JSON.stringify({ cycles, count: cycles.length }, null, 2)); + } else if (cycles.length === 0) { + success('No circular dependencies found'); + } else { + console.log(chalk.bold(`\nCircular Dependencies (${cycles.length}):\n`)); + cycles.forEach((cycle, index) => { + const closed = cycle.length > 0 ? [...cycle, cycle[0]] : cycle; + console.log(chalk.cyan(`${index + 1}. `) + closed.join(' -> ')); + }); + } + + cg.destroy(); + } catch (err) { + error(`Cycle detection failed: ${err instanceof Error ? err.message : String(err)}`); + process.exit(1); + } + }); + /** * codegraph explore *