#
tokens: 46965/50000 36/36 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .cursor
│   └── rules
│       └── 000_general.mdc
├── .github
│   └── workflows
│       ├── ci.yml
│       └── release.yml
├── .gitignore
├── .vscode
│   └── settings.json
├── biome.json
├── CLAUDE.md
├── lefthook.yaml
├── LICENSE
├── package.json
├── packages
│   └── sandbox
│       ├── package.json
│       ├── README.md
│       ├── src
│       │   ├── moduleA.ts
│       │   ├── moduleB.ts
│       │   └── utils.ts
│       └── tsconfig.json
├── pnpm-lock.yaml
├── README.md
├── scripts
│   └── mcp_launcher.js
├── src
│   ├── errors
│   │   └── timeout-error.ts
│   ├── index.ts
│   ├── mcp
│   │   ├── config.ts
│   │   ├── stdio.ts
│   │   └── tools
│   │       ├── integration.test.ts
│   │       ├── register-find-references-tool.ts
│   │       ├── register-move-symbol-to-file-tool.ts
│   │       ├── register-remove-path-alias-tool.ts
│   │       ├── register-rename-file-system-entry-tool.ts
│   │       ├── register-rename-symbol-tool.ts
│   │       └── ts-morph-tools.ts
│   ├── ts-morph
│   │   ├── _utils
│   │   │   ├── calculate-relative-path.test.ts
│   │   │   ├── calculate-relative-path.ts
│   │   │   ├── find-declarations-to-update.test.ts
│   │   │   ├── find-declarations-to-update.ts
│   │   │   └── ts-morph-project.ts
│   │   ├── find-references.test.ts
│   │   ├── find-references.ts
│   │   ├── move-symbol-to-file
│   │   │   ├── classify-dependencies.test.ts
│   │   │   ├── classify-dependencies.ts
│   │   │   ├── collect-external-imports.test.ts
│   │   │   ├── collect-external-imports.ts
│   │   │   ├── create-source-file-if-not-exists.test.ts
│   │   │   ├── create-source-file-if-not-exists.ts
│   │   │   ├── ensure-exports-in-original-file.test.ts
│   │   │   ├── ensure-exports-in-original-file.ts
│   │   │   ├── find-declaration.test.ts
│   │   │   ├── find-declaration.ts
│   │   │   ├── generate-content
│   │   │   │   ├── build-new-file-import-section.ts
│   │   │   │   ├── generate-new-source-file-content.test.ts
│   │   │   │   └── generate-new-source-file-content.ts
│   │   │   ├── get-declaration-identifier.ts
│   │   │   ├── internal-dependencies.test.ts
│   │   │   ├── internal-dependencies.ts
│   │   │   ├── move-symbol-to-file.dependencies.test.ts
│   │   │   ├── move-symbol-to-file.test.ts
│   │   │   ├── move-symbol-to-file.ts
│   │   │   ├── remove-original-symbol.test.ts
│   │   │   ├── remove-original-symbol.ts
│   │   │   ├── update-imports-in-referencing-files.test.ts
│   │   │   ├── update-imports-in-referencing-files.ts
│   │   │   ├── update-target-file.test.ts
│   │   │   └── update-target-file.ts
│   │   ├── remove-path-alias
│   │   │   ├── remove-path-alias.test.ts
│   │   │   └── remove-path-alias.ts
│   │   ├── rename-file-system
│   │   │   ├── _utils
│   │   │   │   ├── check-is-path-alias.ts
│   │   │   │   ├── find-declarations-for-rename-operation.ts
│   │   │   │   ├── find-referencing-declarations-for-identifier.ts
│   │   │   │   ├── get-identifier-node-from-declaration.test.ts
│   │   │   │   └── get-identifier-node-from-declaration.ts
│   │   │   ├── move-file-system-entries.ts
│   │   │   ├── prepare-renames.ts
│   │   │   ├── rename-file-system-entry.base.test.ts
│   │   │   ├── rename-file-system-entry.complex.test.ts
│   │   │   ├── rename-file-system-entry.errors.test.ts
│   │   │   ├── rename-file-system-entry.index.test.ts
│   │   │   ├── rename-file-system-entry.special.test.ts
│   │   │   ├── rename-file-system-entry.test.ts
│   │   │   ├── rename-file-system-entry.ts
│   │   │   └── update-module-specifiers.ts
│   │   ├── rename-symbol
│   │   │   ├── rename-symbol.test.ts
│   │   │   └── rename-symbol.ts
│   │   └── types.ts
│   └── utils
│       ├── logger-helpers.ts
│       └── logger.ts
├── tsconfig.json
└── vitest.config.ts
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
  1 | # Logs
  2 | logs
  3 | *.log
  4 | npm-debug.log*
  5 | yarn-debug.log*
  6 | yarn-error.log*
  7 | lerna-debug.log*
  8 | .pnpm-debug.log*
  9 | 
 10 | # Diagnostic reports (https://nodejs.org/api/report.html)
 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
 12 | 
 13 | # Runtime data
 14 | pids
 15 | *.pid
 16 | *.seed
 17 | *.pid.lock
 18 | 
 19 | # Directory for instrumented libs generated by jscoverage/JSCover
 20 | lib-cov
 21 | 
 22 | # Coverage directory used by tools like istanbul
 23 | coverage
 24 | *.lcov
 25 | 
 26 | # nyc test coverage
 27 | .nyc_output
 28 | 
 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
 30 | .grunt
 31 | 
 32 | # Bower dependency directory (https://bower.io/)
 33 | bower_components
 34 | 
 35 | # node-waf configuration
 36 | .lock-wscript
 37 | 
 38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
 39 | build/Release
 40 | 
 41 | # Dependency directories
 42 | node_modules/
 43 | jspm_packages/
 44 | 
 45 | # Snowpack dependency directory (https://snowpack.dev/)
 46 | web_modules/
 47 | 
 48 | # TypeScript cache
 49 | *.tsbuildinfo
 50 | 
 51 | # Optional npm cache directory
 52 | .npm
 53 | 
 54 | # Optional eslint cache
 55 | .eslintcache
 56 | 
 57 | # Optional stylelint cache
 58 | .stylelintcache
 59 | 
 60 | # Microbundle cache
 61 | .rpt2_cache/
 62 | .rts2_cache_cjs/
 63 | .rts2_cache_es/
 64 | .rts2_cache_umd/
 65 | 
 66 | # Optional REPL history
 67 | .node_repl_history
 68 | 
 69 | # Output of 'npm pack'
 70 | *.tgz
 71 | 
 72 | # Yarn Integrity file
 73 | .yarn-integrity
 74 | 
 75 | # dotenv environment variable files
 76 | .env
 77 | .env.development.local
 78 | .env.test.local
 79 | .env.production.local
 80 | .env.local
 81 | .env.cloud
 82 | 
 83 | # parcel-bundler cache (https://parceljs.org/)
 84 | .cache
 85 | .parcel-cache
 86 | 
 87 | # Next.js build output
 88 | .next
 89 | out
 90 | 
 91 | # Nuxt.js build / generate output
 92 | .nuxt
 93 | dist
 94 | 
 95 | # Gatsby files
 96 | .cache/
 97 | # Comment in the public line in if your project uses Gatsby and not Next.js
 98 | # https://nextjs.org/blog/next-9-1#public-directory-support
 99 | # public
100 | 
101 | # vuepress build output
102 | .vuepress/dist
103 | 
104 | # vuepress v2.x temp and cache directory
105 | .temp
106 | .cache
107 | 
108 | # vitepress build output
109 | **/.vitepress/dist
110 | 
111 | # vitepress cache directory
112 | **/.vitepress/cache
113 | 
114 | # Docusaurus cache and generated files
115 | .docusaurus
116 | 
117 | # Serverless directories
118 | .serverless/
119 | 
120 | # FuseBox cache
121 | .fusebox/
122 | 
123 | # DynamoDB Local files
124 | .dynamodb/
125 | 
126 | # TernJS port file
127 | .tern-port
128 | 
129 | # Stores VSCode versions used for testing VSCode extensions
130 | .vscode-test
131 | 
132 | # yarn v2
133 | .yarn/cache
134 | .yarn/unplugged
135 | .yarn/build-state.yml
136 | .yarn/install-state.gz
137 | .pnp.*
138 | 
139 | # Taskfile cache
140 | .task
141 | 
```

--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------

```json
1 | {}
2 | 
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
1 | #!/usr/bin/env node
2 | import { runStdioServer } from "./mcp/stdio";
3 | 
4 | // サーバー起動
5 | runStdioServer().catch((error: Error) => {
6 | 	process.stderr.write(JSON.stringify({ error: `Fatal error: ${error}` }));
7 | 	process.exit(1);
8 | });
9 | 
```

--------------------------------------------------------------------------------
/biome.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 | 	"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
 3 | 	"organizeImports": {
 4 | 		"enabled": false
 5 | 	},
 6 | 	"files": {
 7 | 		"ignore": ["dist/**/*", "coverage/**/*"]
 8 | 	},
 9 | 	"linter": {
10 | 		"enabled": true,
11 | 		"rules": {
12 | 			"recommended": true
13 | 		}
14 | 	}
15 | }
16 | 
```

--------------------------------------------------------------------------------
/packages/sandbox/src/moduleA.ts:
--------------------------------------------------------------------------------

```typescript
 1 | export const valueA = "Value from Module A";
 2 | 
 3 | export function funcA(): string {
 4 | 	console.log("Function A executed");
 5 | 	return "Result from Func A";
 6 | }
 7 | 
 8 | export interface InterfaceA {
 9 | 	id: number;
10 | 	name: string;
11 | }
12 | 
13 | // Add more complex scenarios later if needed
14 | 
```

--------------------------------------------------------------------------------
/src/errors/timeout-error.ts:
--------------------------------------------------------------------------------

```typescript
 1 | export class TimeoutError extends Error {
 2 | 	constructor(
 3 | 		message: string,
 4 | 		public readonly durationSeconds: number,
 5 | 	) {
 6 | 		super(message);
 7 | 		this.name = "TimeoutError";
 8 | 		// Set the prototype explicitly.
 9 | 		Object.setPrototypeOf(this, TimeoutError.prototype);
10 | 	}
11 | }
12 | 
```

--------------------------------------------------------------------------------
/src/mcp/stdio.ts:
--------------------------------------------------------------------------------

```typescript
1 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2 | import { createMcpServer } from "./config";
3 | 
4 | export async function runStdioServer() {
5 | 	const mcpServer = createMcpServer();
6 | 	const transport = new StdioServerTransport();
7 | 	await mcpServer.connect(transport);
8 | }
9 | 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 | 	"compilerOptions": {
 3 | 		"target": "ES2022",
 4 | 		"module": "NodeNext",
 5 | 		"moduleResolution": "NodeNext",
 6 | 		"outDir": "dist",
 7 | 		"rootDir": "src",
 8 | 		"strict": true,
 9 | 		"esModuleInterop": true,
10 | 		"skipLibCheck": true,
11 | 		"forceConsistentCasingInFileNames": true,
12 | 		"resolveJsonModule": true
13 | 	},
14 | 	"include": ["src/**/*"],
15 | 	"exclude": ["node_modules", "dist"]
16 | }
17 | 
```

--------------------------------------------------------------------------------
/packages/sandbox/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 | 	"compilerOptions": {
 3 | 		"target": "ESNext",
 4 | 		"module": "ESNext",
 5 | 		"moduleResolution": "node",
 6 | 		"strict": true,
 7 | 		"esModuleInterop": true,
 8 | 		"skipLibCheck": true,
 9 | 		"forceConsistentCasingInFileNames": true,
10 | 		"outDir": "./dist",
11 | 		"rootDir": "./src",
12 | 		"baseUrl": ".",
13 | 		"paths": {
14 | 			"@/*": ["src/*"]
15 | 		}
16 | 	},
17 | 	"include": ["src/**/*"],
18 | 	"exclude": ["node_modules", "dist"]
19 | }
20 | 
```

--------------------------------------------------------------------------------
/lefthook.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | pre-commit:
 2 |   parallel: true
 3 |   commands:
 4 |     format:
 5 |       glob: "**/*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}"
 6 |       run: pnpm biome check --write --no-errors-on-unmatched --files-ignore-unknown=true {staged_files} && git update-index --again
 7 | pre-push:
 8 |   parallel: true
 9 |   commands:
10 |     format:
11 |       glob: "**/*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}"
12 |       run: pnpm biome check --no-errors-on-unmatched --files-ignore-unknown=true {push_files}
13 |     test:
14 |       run: pnpm test
15 | 
```

--------------------------------------------------------------------------------
/packages/sandbox/src/moduleB.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { valueA, funcA, type InterfaceA } from "./moduleA";
 2 | import { utilFunc1, internalUtil } from "@/utils"; // Use path alias
 3 | 
 4 | export const valueB = `Value from Module B using ${valueA}`;
 5 | 
 6 | function privateHelperB() {
 7 | 	return `${internalUtil()} from B`;
 8 | }
 9 | 
10 | export function funcB(): InterfaceA {
11 | 	console.log("Function B executed");
12 | 	utilFunc1();
13 | 	const resultA = funcA();
14 | 	console.log("Result from funcA:", resultA);
15 | 	console.log(privateHelperB());
16 | 	return { id: 1, name: valueB };
17 | }
18 | 
19 | console.log(valueB);
20 | 
```

--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: CI
 2 | 
 3 | on:
 4 |   push:
 5 |     branches: [ main ]
 6 |   pull_request:
 7 |     branches: [ main ]
 8 | 
 9 | jobs:
10 |   build_and_test:
11 |     runs-on: ubuntu-latest
12 | 
13 |     steps:
14 |       - name: Checkout code
15 |         uses: actions/checkout@v4
16 | 
17 |       - name: Set up pnpm
18 |         uses: pnpm/action-setup@v4
19 | 
20 |       - name: Set up Node.js
21 |         uses: actions/setup-node@v4
22 |         with:
23 |           node-version-file: 'package.json'
24 |           cache: 'pnpm'
25 | 
26 |       - name: Install dependencies
27 |         run: pnpm install
28 | 
29 |       - name: Run type check
30 |         run: pnpm run check-types
31 | 
32 |       - name: Run lint
33 |         run: pnpm run lint
34 | 
35 |       - name: Run tests
36 |         run: pnpm run test
37 | 
```

--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { defineConfig } from "vitest/config";
 2 | 
 3 | // https://vitejs.dev/config/
 4 | export default defineConfig({
 5 | 	test: {
 6 | 		env: {
 7 | 			API_ADDRESS: "http://localhost:8080",
 8 | 		},
 9 | 		restoreMocks: true,
10 | 		mockReset: true,
11 | 		clearMocks: true,
12 | 		coverage: {
13 | 			provider: "v8",
14 | 			reporter: ["text", "json", "html", "lcov"],
15 | 			exclude: [
16 | 				"node_modules/**",
17 | 				"dist/**",
18 | 				"packages/sandbox/**",
19 | 				"**/*.test.ts",
20 | 				"**/*.spec.ts",
21 | 				"**/index.ts",
22 | 				"vitest.config.ts",
23 | 				"src/mcp/index.ts",
24 | 				"src/mcp/stdio.ts",
25 | 				"src/utils/logger.ts",
26 | 				"src/utils/logger-helpers.ts",
27 | 				"src/errors/**",
28 | 			],
29 | 			thresholds: {
30 | 				lines: 70,
31 | 				functions: 70,
32 | 				branches: 65,
33 | 				statements: 70,
34 | 			},
35 | 			clean: true,
36 | 			all: true,
37 | 		},
38 | 	},
39 | });
40 | 
```

--------------------------------------------------------------------------------
/src/mcp/tools/ts-morph-tools.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
 2 | 
 3 | import { registerRenameSymbolTool } from "./register-rename-symbol-tool";
 4 | import { registerRenameFileSystemEntryTool } from "./register-rename-file-system-entry-tool";
 5 | import { registerFindReferencesTool } from "./register-find-references-tool";
 6 | import { registerRemovePathAliasTool } from "./register-remove-path-alias-tool";
 7 | import { registerMoveSymbolToFileTool } from "./register-move-symbol-to-file-tool";
 8 | 
 9 | /**
10 |  * ts-morph を利用したリファクタリングツール群を MCP サーバーに登録する
11 |  */
12 | export function registerTsMorphTools(server: McpServer): void {
13 | 	registerRenameSymbolTool(server);
14 | 	registerRenameFileSystemEntryTool(server);
15 | 	registerFindReferencesTool(server);
16 | 	registerRemovePathAliasTool(server);
17 | 	registerMoveSymbolToFileTool(server);
18 | }
19 | 
```

--------------------------------------------------------------------------------
/src/ts-morph/rename-file-system/move-file-system-entries.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import logger from "../../utils/logger";
 2 | import type { RenameOperation } from "../types";
 3 | import { performance } from "node:perf_hooks";
 4 | 
 5 | export function moveFileSystemEntries(
 6 | 	renameOperations: RenameOperation[],
 7 | 	signal?: AbortSignal,
 8 | ) {
 9 | 	const startTime = performance.now();
10 | 	signal?.throwIfAborted();
11 | 	logger.debug(
12 | 		{ count: renameOperations.length },
13 | 		"Starting file system moves",
14 | 	);
15 | 	for (const { sourceFile, newPath, oldPath } of renameOperations) {
16 | 		signal?.throwIfAborted();
17 | 		logger.trace({ from: oldPath, to: newPath }, "Moving file");
18 | 		try {
19 | 			sourceFile.move(newPath);
20 | 		} catch (err) {
21 | 			logger.error(
22 | 				{ err, from: oldPath, to: newPath },
23 | 				"Error during sourceFile.move()",
24 | 			);
25 | 			throw err;
26 | 		}
27 | 	}
28 | 	const durationMs = (performance.now() - startTime).toFixed(2);
29 | 	logger.debug({ durationMs }, "Finished file system moves");
30 | }
31 | 
```

--------------------------------------------------------------------------------
/src/utils/logger.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import pino from "pino";
 2 | import {
 3 | 	configureTransport,
 4 | 	parseEnvVariables,
 5 | 	setupExitHandlers,
 6 | } from "./logger-helpers";
 7 | 
 8 | const env = parseEnvVariables();
 9 | 
10 | const isTestEnv = env.NODE_ENV === "test";
11 | 
12 | const pinoOptions: pino.LoggerOptions = {
13 | 	level: isTestEnv ? "silent" : env.LOG_LEVEL,
14 | 	base: { pid: process.pid },
15 | 	timestamp: pino.stdTimeFunctions.isoTime,
16 | 	formatters: {
17 | 		level: (label) => ({ level: label.toUpperCase() }),
18 | 	},
19 | };
20 | 
21 | const transport = !isTestEnv
22 | 	? configureTransport(env.NODE_ENV, env.LOG_OUTPUT, env.LOG_FILE_PATH)
23 | 	: undefined;
24 | 
25 | const baseLogger = transport
26 | 	? pino(pinoOptions, pino.transport(transport))
27 | 	: pino(pinoOptions);
28 | 
29 | setupExitHandlers(baseLogger);
30 | 
31 | // テスト環境では初期化ログを出力しない
32 | if (!isTestEnv) {
33 | 	baseLogger.info(
34 | 		{
35 | 			logLevel: env.LOG_LEVEL,
36 | 			logOutput: env.LOG_OUTPUT,
37 | 			logFilePath: env.LOG_OUTPUT === "file" ? env.LOG_FILE_PATH : undefined,
38 | 			nodeEnv: env.NODE_ENV,
39 | 		},
40 | 		"ロガー初期化完了",
41 | 	);
42 | }
43 | 
44 | export default baseLogger;
45 | 
```

--------------------------------------------------------------------------------
/src/ts-morph/rename-file-system/_utils/find-referencing-declarations-for-identifier.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import {
 2 | 	type ExportDeclaration,
 3 | 	type Identifier,
 4 | 	type ImportDeclaration,
 5 | 	SyntaxKind,
 6 | } from "ts-morph";
 7 | import logger from "../../../utils/logger";
 8 | 
 9 | export function findReferencingDeclarationsForIdentifier(
10 | 	identifierNode: Identifier,
11 | 	signal?: AbortSignal,
12 | ): Set<ImportDeclaration | ExportDeclaration> {
13 | 	const referencingDeclarations = new Set<
14 | 		ImportDeclaration | ExportDeclaration
15 | 	>();
16 | 
17 | 	logger.trace(
18 | 		{ identifierText: identifierNode.getText() },
19 | 		"Finding references for identifier",
20 | 	);
21 | 
22 | 	const references = identifierNode.findReferencesAsNodes();
23 | 
24 | 	for (const referenceNode of references) {
25 | 		signal?.throwIfAborted();
26 | 
27 | 		const importOrExportDecl =
28 | 			referenceNode.getFirstAncestorByKind(SyntaxKind.ImportDeclaration) ??
29 | 			referenceNode.getFirstAncestorByKind(SyntaxKind.ExportDeclaration);
30 | 
31 | 		if (importOrExportDecl?.getModuleSpecifier()) {
32 | 			referencingDeclarations.add(importOrExportDecl);
33 | 		}
34 | 	}
35 | 	logger.trace(
36 | 		{
37 | 			identifierText: identifierNode.getText(),
38 | 			count: referencingDeclarations.size,
39 | 		},
40 | 		"Found referencing declarations for identifier",
41 | 	);
42 | 	return referencingDeclarations;
43 | }
44 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 | 	"name": "@sirosuzume/mcp-tsmorph-refactor",
 3 | 	"version": "0.2.9",
 4 | 	"description": "ts-morph を利用した MCP リファクタリングサーバー",
 5 | 	"main": "dist/index.js",
 6 | 	"bin": {
 7 | 		"mcp-tsmorph-refactor": "dist/index.js"
 8 | 	},
 9 | 	"files": ["dist", "package.json", "README.md"],
10 | 	"publishConfig": {
11 | 		"access": "public"
12 | 	},
13 | 	"repository": {
14 | 		"type": "git",
15 | 		"url": "git+https://github.com/SiroSuzume/mcp-ts-morph.git"
16 | 	},
17 | 	"packageManager": "[email protected]",
18 | 	"scripts": {
19 | 		"preinstall": "npx only-allow pnpm",
20 | 		"clean": "shx rm -rf dist",
21 | 		"build": "pnpm run clean && tsc && shx chmod +x dist/index.js",
22 | 		"prepublishOnly": "pnpm run build",
23 | 		"inspector": "npx @modelcontextprotocol/inspector node build/index.js",
24 | 		"test": "vitest run --pool threads --poolOptions.threads.singleThread",
25 | 		"test:watch": "vitest",
26 | 		"test:coverage": "vitest run --coverage",
27 | 		"check-types": "tsc --noEmit",
28 | 		"lint": "biome lint ./",
29 | 		"lint:fix": "biome lint --write ./",
30 | 		"format": "biome check --write ./"
31 | 	},
32 | 	"keywords": ["mcp", "ts-morph", "refactoring"],
33 | 	"author": "SiroSuzume",
34 | 	"license": "MIT",
35 | 	"volta": {
36 | 		"node": "20.19.0"
37 | 	},
38 | 	"devDependencies": {
39 | 		"@biomejs/biome": "^1.9.4",
40 | 		"@types/node": "^22.14.0",
41 | 		"@vitest/coverage-v8": "3.1.2",
42 | 		"lefthook": "^1.11.8",
43 | 		"pino-pretty": "^13.0.0",
44 | 		"shx": "^0.4.0",
45 | 		"tsx": "^4.19.3",
46 | 		"vitest": "^3.1.1"
47 | 	},
48 | 	"dependencies": {
49 | 		"@modelcontextprotocol/sdk": "^1.17.5",
50 | 		"pino": "^9.6.0",
51 | 		"ts-morph": "^25.0.1",
52 | 		"typescript": "^5.8.3",
53 | 		"zod": "^3.24.2"
54 | 	}
55 | }
56 | 
```

--------------------------------------------------------------------------------
/src/ts-morph/rename-file-system/_utils/find-declarations-for-rename-operation.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import type { ExportDeclaration, ImportDeclaration } from "ts-morph";
 2 | import logger from "../../../utils/logger";
 3 | import type { RenameOperation } from "../../types";
 4 | import { findReferencingDeclarationsForIdentifier } from "./find-referencing-declarations-for-identifier";
 5 | import { getIdentifierNodeFromDeclaration } from "./get-identifier-node-from-declaration";
 6 | 
 7 | export function findDeclarationsForRenameOperation(
 8 | 	renameOperation: RenameOperation,
 9 | 	signal?: AbortSignal,
10 | ): Set<ImportDeclaration | ExportDeclaration> {
11 | 	const { sourceFile } = renameOperation;
12 | 	const declarationsForThisOperation = new Set<
13 | 		ImportDeclaration | ExportDeclaration
14 | 	>();
15 | 
16 | 	try {
17 | 		const exportSymbols = sourceFile.getExportSymbols();
18 | 		logger.trace(
19 | 			{ file: sourceFile.getFilePath(), count: exportSymbols.length },
20 | 			"Found export symbols for rename operation",
21 | 		);
22 | 
23 | 		for (const symbol of exportSymbols) {
24 | 			signal?.throwIfAborted();
25 | 			const symbolDeclarations = symbol.getDeclarations();
26 | 
27 | 			for (const symbolDeclaration of symbolDeclarations) {
28 | 				signal?.throwIfAborted();
29 | 				const identifierNode =
30 | 					getIdentifierNodeFromDeclaration(symbolDeclaration);
31 | 
32 | 				if (!identifierNode) {
33 | 					continue;
34 | 				}
35 | 
36 | 				const foundDecls = findReferencingDeclarationsForIdentifier(
37 | 					identifierNode,
38 | 					signal,
39 | 				);
40 | 
41 | 				for (const decl of foundDecls) {
42 | 					declarationsForThisOperation.add(decl);
43 | 				}
44 | 			}
45 | 		}
46 | 	} catch (error) {
47 | 		logger.warn(
48 | 			{ file: sourceFile.getFilePath(), err: error },
49 | 			"Error processing rename operation symbols",
50 | 		);
51 | 	}
52 | 	return declarationsForThisOperation;
53 | }
54 | 
```

--------------------------------------------------------------------------------
/src/ts-morph/_utils/ts-morph-project.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { Project, type SourceFile } from "ts-morph";
 2 | import * as path from "node:path";
 3 | import { NewLineKind } from "typescript";
 4 | import logger from "../../utils/logger";
 5 | 
 6 | export function initializeProject(tsconfigPath: string): Project {
 7 | 	const absoluteTsconfigPath = path.resolve(tsconfigPath);
 8 | 	return new Project({
 9 | 		tsConfigFilePath: absoluteTsconfigPath,
10 | 		manipulationSettings: {
11 | 			newLineKind: NewLineKind.LineFeed,
12 | 		},
13 | 	});
14 | }
15 | 
16 | export function getChangedFiles(project: Project): SourceFile[] {
17 | 	return project.getSourceFiles().filter((sf) => !sf.isSaved());
18 | }
19 | 
20 | export async function saveProjectChanges(
21 | 	project: Project,
22 | 	signal?: AbortSignal,
23 | ): Promise<void> {
24 | 	signal?.throwIfAborted();
25 | 	try {
26 | 		await project.save();
27 | 	} catch (error) {
28 | 		if (error instanceof Error && error.name === "AbortError") {
29 | 			throw error;
30 | 		}
31 | 		const message = error instanceof Error ? error.message : String(error);
32 | 		throw new Error(`ファイル保存中にエラーが発生しました: ${message}`);
33 | 	}
34 | }
35 | 
36 | export function getTsConfigPaths(
37 | 	project: Project,
38 | ): Record<string, string[]> | undefined {
39 | 	try {
40 | 		const options = project.compilerOptions.get();
41 | 		if (!options.paths) {
42 | 			return undefined;
43 | 		}
44 | 		if (typeof options.paths !== "object") {
45 | 			logger.warn(
46 | 				{ paths: options.paths },
47 | 				"Compiler options 'paths' is not an object.",
48 | 			);
49 | 			return undefined;
50 | 		}
51 | 
52 | 		const validPaths: Record<string, string[]> = {};
53 | 		for (const [key, value] of Object.entries(options.paths)) {
54 | 			if (
55 | 				Array.isArray(value) &&
56 | 				value.every((item) => typeof item === "string")
57 | 			) {
58 | 				validPaths[key] = value;
59 | 			} else {
60 | 				logger.warn(
61 | 					{ pathKey: key, pathValue: value },
62 | 					"Invalid format for paths entry, skipping.",
63 | 				);
64 | 			}
65 | 		}
66 | 		return validPaths;
67 | 	} catch (error) {
68 | 		logger.error({ err: error }, "Failed to get compiler options or paths");
69 | 		return undefined;
70 | 	}
71 | }
72 | 
73 | export function getTsConfigBaseUrl(project: Project): string | undefined {
74 | 	try {
75 | 		const options = project.compilerOptions.get();
76 | 		return options.baseUrl;
77 | 	} catch (error) {
78 | 		logger.error({ err: error }, "Failed to get compiler options baseUrl");
79 | 		return undefined;
80 | 	}
81 | }
82 | 
```

--------------------------------------------------------------------------------
/src/ts-morph/move-symbol-to-file/update-target-file.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { describe, it, expect } from "vitest";
 2 | import { Project } from "ts-morph";
 3 | import { updateTargetFile } from "./update-target-file";
 4 | import type { ImportMap } from "./generate-content/build-new-file-import-section";
 5 | 
 6 | describe("updateTargetFile", () => {
 7 | 	it("既存ファイルに新しい宣言と、それに必要な新しい名前付きインポートを追加・マージできる", () => {
 8 | 		const project = new Project({ useInMemoryFileSystem: true });
 9 | 		const targetFilePath = "/src/target.ts";
10 | 		project.createSourceFile(
11 | 			"/utils.ts",
12 | 			"export const foo = 1; export const bar = 2; export const qux = 3;",
13 | 		);
14 | 
15 | 		const initialContent = `import { foo, bar } from "../utils";
16 | 
17 | console.log(foo);
18 | console.log(bar);
19 | `;
20 | 		const targetSourceFile = project.createSourceFile(
21 | 			targetFilePath,
22 | 			initialContent,
23 | 		);
24 | 
25 | 		const requiredImportMap: ImportMap = new Map([
26 | 			[
27 | 				"../utils",
28 | 				{
29 | 					namedImports: new Set(["qux"]),
30 | 					isNamespaceImport: false,
31 | 				},
32 | 			],
33 | 		]);
34 | 
35 | 		const declarationStrings: string[] = [
36 | 			"export function baz() { return qux(); }",
37 | 		];
38 | 
39 | 		const expectedContent = `import { bar, foo, qux } from "../utils";
40 | 
41 | console.log(foo);
42 | console.log(bar);
43 | 
44 | export function baz() { return qux(); }
45 | `;
46 | 
47 | 		updateTargetFile(targetSourceFile, requiredImportMap, declarationStrings);
48 | 
49 | 		expect(targetSourceFile.getFullText().trim()).toBe(expectedContent.trim());
50 | 	});
51 | 
52 | 	it("requiredImportMap に自己参照パスが含まれていても、自己参照インポートは追加しない", () => {
53 | 		const project = new Project({ useInMemoryFileSystem: true });
54 | 		const targetFilePath = "/src/target.ts";
55 | 		const initialContent = `export type ExistingType = number;
56 | 
57 | console.log('hello');
58 | `;
59 | 		const targetSourceFile = project.createSourceFile(
60 | 			targetFilePath,
61 | 			initialContent,
62 | 		);
63 | 
64 | 		const requiredImportMap: ImportMap = new Map([
65 | 			[
66 | 				".",
67 | 				{
68 | 					namedImports: new Set(["ExistingType"]),
69 | 					isNamespaceImport: false,
70 | 				},
71 | 			],
72 | 		]);
73 | 
74 | 		const declarationStrings: string[] = [];
75 | 
76 | 		const expectedContent = initialContent;
77 | 
78 | 		updateTargetFile(targetSourceFile, requiredImportMap, declarationStrings);
79 | 
80 | 		expect(targetSourceFile.getFullText().trim()).toBe(expectedContent.trim());
81 | 	});
82 | 
83 | 	// TODO: Add more realistic test cases (e.g., default imports, different modules)
84 | });
85 | 
```

--------------------------------------------------------------------------------
/src/ts-morph/rename-file-system/prepare-renames.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import logger from "../../utils/logger";
 2 | import type { PathMapping, RenameOperation } from "../types";
 3 | import * as path from "node:path";
 4 | import { performance } from "node:perf_hooks";
 5 | import type { Project } from "ts-morph";
 6 | 
 7 | function checkDestinationExists(
 8 | 	project: Project,
 9 | 	pathToCheck: string,
10 | 	signal?: AbortSignal,
11 | ): void {
12 | 	signal?.throwIfAborted();
13 | 	if (project.getSourceFile(pathToCheck)) {
14 | 		throw new Error(`リネーム先パスに既にファイルが存在します: ${pathToCheck}`);
15 | 	}
16 | 	if (project.getDirectory(pathToCheck)) {
17 | 		throw new Error(
18 | 			`リネーム先パスに既にディレクトリが存在します: ${pathToCheck}`,
19 | 		);
20 | 	}
21 | }
22 | 
23 | export function prepareRenames(
24 | 	project: Project,
25 | 	renames: PathMapping[],
26 | 	signal?: AbortSignal,
27 | ): RenameOperation[] {
28 | 	const startTime = performance.now();
29 | 	signal?.throwIfAborted();
30 | 	const renameOperations: RenameOperation[] = [];
31 | 	const uniqueNewPaths = new Set<string>();
32 | 	logger.debug({ count: renames.length }, "Starting rename preparation");
33 | 
34 | 	for (const rename of renames) {
35 | 		signal?.throwIfAborted();
36 | 		const logRename = { old: rename.oldPath, new: rename.newPath };
37 | 		logger.trace({ rename: logRename }, "Processing rename request");
38 | 
39 | 		const absoluteOldPath = path.resolve(rename.oldPath);
40 | 		const absoluteNewPath = path.resolve(rename.newPath);
41 | 
42 | 		if (uniqueNewPaths.has(absoluteNewPath)) {
43 | 			throw new Error(`リネーム先のパスが重複しています: ${absoluteNewPath}`);
44 | 		}
45 | 		uniqueNewPaths.add(absoluteNewPath);
46 | 
47 | 		checkDestinationExists(project, absoluteNewPath, signal);
48 | 
49 | 		signal?.throwIfAborted();
50 | 		const sourceFile = project.getSourceFile(absoluteOldPath);
51 | 		const directory = project.getDirectory(absoluteOldPath);
52 | 
53 | 		if (sourceFile) {
54 | 			logger.trace({ path: absoluteOldPath }, "Identified as file rename");
55 | 			renameOperations.push({
56 | 				sourceFile,
57 | 				oldPath: absoluteOldPath,
58 | 				newPath: absoluteNewPath,
59 | 			});
60 | 		} else if (directory) {
61 | 			logger.trace({ path: absoluteOldPath }, "Identified as directory rename");
62 | 			signal?.throwIfAborted();
63 | 			const filesInDir = directory.getDescendantSourceFiles();
64 | 			logger.trace(
65 | 				{ path: absoluteOldPath, count: filesInDir.length },
66 | 				"Found files in directory to rename",
67 | 			);
68 | 			for (const sf of filesInDir) {
69 | 				const oldFilePath = sf.getFilePath();
70 | 				const relative = path.relative(absoluteOldPath, oldFilePath);
71 | 				const newFilePath = path.resolve(absoluteNewPath, relative);
72 | 				logger.trace(
73 | 					{ oldFile: oldFilePath, newFile: newFilePath },
74 | 					"Adding directory file to rename operations",
75 | 				);
76 | 				renameOperations.push({
77 | 					sourceFile: sf,
78 | 					oldPath: oldFilePath,
79 | 					newPath: newFilePath,
80 | 				});
81 | 			}
82 | 		} else {
83 | 			throw new Error(`リネーム対象が見つかりません: ${absoluteOldPath}`);
84 | 		}
85 | 	}
86 | 	const durationMs = (performance.now() - startTime).toFixed(2);
87 | 	logger.debug(
88 | 		{ operationCount: renameOperations.length, durationMs },
89 | 		"Finished rename preparation",
90 | 	);
91 | 	return renameOperations;
92 | }
93 | 
```

--------------------------------------------------------------------------------
/src/mcp/tools/register-remove-path-alias-tool.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
  2 | import { z } from "zod";
  3 | import { removePathAlias } from "../../ts-morph/remove-path-alias/remove-path-alias";
  4 | import { Project } from "ts-morph";
  5 | import * as path from "node:path"; // path モジュールが必要
  6 | import { performance } from "node:perf_hooks";
  7 | 
  8 | export function registerRemovePathAliasTool(server: McpServer): void {
  9 | 	server.tool(
 10 | 		"remove_path_alias_by_tsmorph",
 11 | 		`[Uses ts-morph] Replaces path aliases (e.g., '@/') with relative paths in import/export statements within the specified target path.
 12 | 
 13 | Analyzes the project based on \`tsconfig.json\` to resolve aliases and calculate relative paths.
 14 | 
 15 | ## Usage
 16 | 
 17 | Use this tool to convert alias paths like \`import Button from '@/components/Button'\` to relative paths like \`import Button from '../../components/Button'\`. This can be useful for improving portability or adhering to specific project conventions.
 18 | 
 19 | 1.  Specify the **absolute path** to the project\`tsconfig.json\`.
 20 | 2.  Specify the **absolute path** to the target file or directory where path aliases should be removed.
 21 | 3.  Optionally, run with \`dryRun: true\` to preview the changes without modifying files.
 22 | 
 23 | ## Parameters
 24 | 
 25 | - tsconfigPath (string, required): Absolute path to the project\`tsconfig.json\` file. **Must be an absolute path.**
 26 | - targetPath (string, required): The absolute path to the file or directory to process. **Must be an absolute path.**
 27 | - dryRun (boolean, optional): If true, only show intended changes without modifying files. Defaults to false.
 28 | 
 29 | ## Result
 30 | 
 31 | - On success: Returns a message containing the list of file paths modified (or scheduled to be modified if dryRun).
 32 | - On failure: Returns a message indicating the error.`,
 33 | 		{
 34 | 			tsconfigPath: z
 35 | 				.string()
 36 | 				.describe("Absolute path to the project's tsconfig.json file."),
 37 | 			targetPath: z
 38 | 				.string()
 39 | 				.describe("Absolute path to the target file or directory."),
 40 | 			dryRun: z
 41 | 				.boolean()
 42 | 				.optional()
 43 | 				.default(false)
 44 | 				.describe(
 45 | 					"If true, only show intended changes without modifying files.",
 46 | 				),
 47 | 		},
 48 | 		async (args) => {
 49 | 			const startTime = performance.now();
 50 | 			let message = "";
 51 | 			let isError = false;
 52 | 			let duration = "0.00";
 53 | 			const project = new Project({
 54 | 				tsConfigFilePath: args.tsconfigPath,
 55 | 			});
 56 | 
 57 | 			try {
 58 | 				const { tsconfigPath, targetPath, dryRun } = args;
 59 | 				const compilerOptions = project.compilerOptions.get();
 60 | 				const tsconfigDir = path.dirname(tsconfigPath);
 61 | 				const baseUrl = path.resolve(
 62 | 					tsconfigDir,
 63 | 					compilerOptions.baseUrl ?? ".",
 64 | 				);
 65 | 				const pathsOption = compilerOptions.paths ?? {};
 66 | 
 67 | 				const result = await removePathAlias({
 68 | 					project,
 69 | 					targetPath,
 70 | 					dryRun,
 71 | 					baseUrl,
 72 | 					paths: pathsOption,
 73 | 				});
 74 | 
 75 | 				if (!dryRun) {
 76 | 					await project.save();
 77 | 				}
 78 | 
 79 | 				const changedFilesList =
 80 | 					result.changedFiles.length > 0
 81 | 						? result.changedFiles.join("\n - ")
 82 | 						: "(No changes)";
 83 | 				const actionVerb = dryRun ? "scheduled for modification" : "modified";
 84 | 				message = `Path alias removal (${
 85 | 					dryRun ? "Dry run" : "Execute"
 86 | 				}): Within the specified path '${targetPath}', the following files were ${actionVerb}:\n - ${changedFilesList}`;
 87 | 			} catch (error) {
 88 | 				const errorMessage =
 89 | 					error instanceof Error ? error.message : String(error);
 90 | 				message = `Error during path alias removal process: ${errorMessage}`;
 91 | 				isError = true;
 92 | 			} finally {
 93 | 				const endTime = performance.now();
 94 | 				duration = ((endTime - startTime) / 1000).toFixed(2);
 95 | 			}
 96 | 
 97 | 			const finalMessage = `${message}\nStatus: ${
 98 | 				isError ? "Failure" : "Success"
 99 | 			}\nProcessing time: ${duration} seconds`;
100 | 
101 | 			return {
102 | 				content: [{ type: "text", text: finalMessage }],
103 | 				isError: isError,
104 | 			};
105 | 		},
106 | 	);
107 | }
108 | 
```

--------------------------------------------------------------------------------
/src/mcp/tools/register-find-references-tool.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
  2 | import { z } from "zod";
  3 | import { findSymbolReferences } from "../../ts-morph/find-references"; // 新しい関数と型をインポート
  4 | import { performance } from "node:perf_hooks";
  5 | 
  6 | export function registerFindReferencesTool(server: McpServer): void {
  7 | 	server.tool(
  8 | 		"find_references_by_tsmorph",
  9 | 		`[Uses ts-morph] Finds the definition and all references to a symbol at a given position throughout the project.
 10 | 
 11 | Analyzes the project based on \`tsconfig.json\` to locate the definition and all usages of the symbol (function, variable, class, etc.) specified by its position.
 12 | 
 13 | ## Usage
 14 | 
 15 | Use this tool before refactoring to understand the impact of changing a specific symbol. It helps identify where a function is called, where a variable is used, etc.
 16 | 
 17 | 1.  Specify the **absolute path** to the project's \`tsconfig.json\`.
 18 | 2.  Specify the **absolute path** to the file containing the symbol you want to investigate.
 19 | 3.  Specify the exact **position** (line and column) of the symbol within the file.
 20 | 
 21 | ## Parameters
 22 | 
 23 | - tsconfigPath (string, required): Absolute path to the project's root \`tsconfig.json\` file. Essential for ts-morph to parse the project. **Must be an absolute path.**
 24 | - targetFilePath (string, required): The absolute path to the file containing the symbol to find references for. **Must be an absolute path.**
 25 | - position (object, required): The exact position of the symbol to find references for.
 26 |   - line (number, required): 1-based line number.
 27 |   - column (number, required): 1-based column number.
 28 | 
 29 | ## Result
 30 | 
 31 | - On success: Returns a message containing the definition location (if found) and a list of reference locations (file path, line number, column number, and line text).
 32 | - On failure: Returns a message indicating the error.`,
 33 | 		{
 34 | 			tsconfigPath: z
 35 | 				.string()
 36 | 				.describe("Absolute path to the project's tsconfig.json file."),
 37 | 			targetFilePath: z
 38 | 				.string()
 39 | 				.describe("Absolute path to the file containing the symbol."),
 40 | 			position: z
 41 | 				.object({
 42 | 					line: z.number().describe("1-based line number."),
 43 | 					column: z.number().describe("1-based column number."),
 44 | 				})
 45 | 				.describe("The exact position of the symbol."),
 46 | 		},
 47 | 		async (args) => {
 48 | 			const startTime = performance.now();
 49 | 			let message = "";
 50 | 			let isError = false;
 51 | 			let duration = "0.00"; // duration を外で宣言・初期化
 52 | 
 53 | 			try {
 54 | 				const { tsconfigPath, targetFilePath, position } = args;
 55 | 				const { references, definition } = await findSymbolReferences({
 56 | 					tsconfigPath: tsconfigPath,
 57 | 					targetFilePath: targetFilePath,
 58 | 					position,
 59 | 				});
 60 | 
 61 | 				let resultText = "";
 62 | 
 63 | 				if (definition) {
 64 | 					resultText += "Definition:\n";
 65 | 					resultText += `- ${definition.filePath}:${definition.line}:${definition.column}\n`;
 66 | 					resultText += `  \`\`\`typescript\n  ${definition.text}\n  \`\`\`\n\n`;
 67 | 				} else {
 68 | 					resultText += "Definition not found.\n\n";
 69 | 				}
 70 | 
 71 | 				if (references.length > 0) {
 72 | 					resultText += `References (${references.length} found):\n`;
 73 | 					const formattedReferences = references
 74 | 						.map(
 75 | 							(ref) =>
 76 | 								`- ${ref.filePath}:${ref.line}:${ref.column}\n  \`\`\`typescript\n  ${ref.text}\n  \`\`\`\``,
 77 | 						)
 78 | 						.join("\n\n");
 79 | 					resultText += formattedReferences;
 80 | 				} else {
 81 | 					resultText += "References not found.";
 82 | 				}
 83 | 				message = resultText.trim();
 84 | 			} catch (error) {
 85 | 				const errorMessage =
 86 | 					error instanceof Error ? error.message : String(error);
 87 | 				message = `Error during reference search: ${errorMessage}`;
 88 | 				isError = true;
 89 | 			} finally {
 90 | 				const endTime = performance.now();
 91 | 				duration = ((endTime - startTime) / 1000).toFixed(2); // duration を更新
 92 | 			}
 93 | 
 94 | 			// finally の外で return する
 95 | 			const finalMessage = `${message}\nStatus: ${
 96 | 				isError ? "Failure" : "Success"
 97 | 			}\nProcessing time: ${duration} seconds`;
 98 | 
 99 | 			return {
100 | 				content: [{ type: "text", text: finalMessage }],
101 | 				isError: isError,
102 | 			};
103 | 		},
104 | 	);
105 | }
106 | 
```

--------------------------------------------------------------------------------
/src/mcp/tools/register-rename-symbol-tool.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
  2 | import { z } from "zod";
  3 | import { renameSymbol } from "../../ts-morph/rename-symbol/rename-symbol";
  4 | import { performance } from "node:perf_hooks";
  5 | 
  6 | export function registerRenameSymbolTool(server: McpServer): void {
  7 | 	server.tool(
  8 | 		"rename_symbol_by_tsmorph",
  9 | 		// Note for developers:
 10 | 		// The following English description is primarily intended for the LLM's understanding.
 11 | 		// Please refer to the JSDoc comment above for the original Japanese description.
 12 | 		`[Uses ts-morph] Renames TypeScript/JavaScript symbols across the project.
 13 | 
 14 | Analyzes the AST (Abstract Syntax Tree) to track and update references 
 15 | throughout the project, not just the definition site.
 16 | Useful for cross-file refactoring tasks during Vibe Coding.
 17 | 
 18 | ## Usage
 19 | 
 20 | Use this tool, for example, when you change a function name defined in one file 
 21 | and want to reflect that change in other files that import and use it.
 22 | ts-morph parses the project based on \`tsconfig.json\` to resolve symbol references 
 23 | and perform the rename.
 24 | 
 25 | 1.  Specify the exact location (file path, line, column) of the symbol 
 26 |     (function name, variable name, class name, etc.) you want to rename. 
 27 |     This is necessary for ts-morph to identify the target Identifier node in the AST.
 28 | 2.  Specify the current symbol name and the new symbol name.
 29 | 3.  It\'s recommended to first run with \`dryRun: true\` to check which files 
 30 |     ts-morph will modify.
 31 | 4.  If the preview looks correct, run with \`dryRun: false\` (or omit it) 
 32 |     to actually save the changes to the file system.
 33 | 
 34 | ## Parameters
 35 | 
 36 | - tsconfigPath (string, required): Path to the project\'s root \`tsconfig.json\` file. 
 37 |   Essential for ts-morph to correctly parse the project structure and file references. **Must be an absolute path (relative paths can be misinterpreted).**
 38 | - targetFilePath (string, required): Path to the file where the symbol to be renamed 
 39 |   is defined (or first appears). **Must be an absolute path (relative paths can be misinterpreted).**
 40 | - position (object, required): The exact position on the symbol to be renamed. 
 41 |   Serves as the starting point for ts-morph to locate the AST node.
 42 |   - line (number, required): 1-based line number, typically obtained from an editor.
 43 |   - column (number, required): 1-based column number (position of the first character 
 44 |     of the symbol name), typically obtained from an editor.
 45 | - symbolName (string, required): The current name of the symbol before renaming. 
 46 |   Used to verify against the node name found at the specified position.
 47 | - newName (string, required): The new name for the symbol after renaming.
 48 | - dryRun (boolean, optional): If set to true, prevents ts-morph from making and saving 
 49 |   file changes, returning only the list of files that would be affected. 
 50 |   Useful for verification. Defaults to false.
 51 | 
 52 | ## Result
 53 | 
 54 | - On success: Returns a message containing the list of file paths modified 
 55 |   (or scheduled to be modified if dryRun) by the rename.
 56 | - On failure: Returns a message indicating the error.`,
 57 | 		{
 58 | 			tsconfigPath: z
 59 | 				.string()
 60 | 				.describe("Path to the project's tsconfig.json file."),
 61 | 			targetFilePath: z
 62 | 				.string()
 63 | 				.describe("Path to the file containing the symbol to rename."),
 64 | 			position: z
 65 | 				.object({
 66 | 					line: z.number().describe("1-based line number."),
 67 | 					column: z.number().describe("1-based column number."),
 68 | 				})
 69 | 				.describe("The exact position of the symbol to rename."),
 70 | 			symbolName: z.string().describe("The current name of the symbol."),
 71 | 			newName: z.string().describe("The new name for the symbol."),
 72 | 			dryRun: z
 73 | 				.boolean()
 74 | 				.optional()
 75 | 				.default(false)
 76 | 				.describe(
 77 | 					"If true, only show intended changes without modifying files.",
 78 | 				),
 79 | 		},
 80 | 		async (args) => {
 81 | 			const startTime = performance.now();
 82 | 			let message = "";
 83 | 			let isError = false;
 84 | 			let duration = "0.00";
 85 | 
 86 | 			try {
 87 | 				const {
 88 | 					tsconfigPath,
 89 | 					targetFilePath,
 90 | 					position,
 91 | 					symbolName,
 92 | 					newName,
 93 | 					dryRun,
 94 | 				} = args;
 95 | 				const result = await renameSymbol({
 96 | 					tsconfigPath: tsconfigPath,
 97 | 					targetFilePath: targetFilePath,
 98 | 					position: position,
 99 | 					symbolName: symbolName,
100 | 					newName: newName,
101 | 					dryRun: dryRun,
102 | 				});
103 | 
104 | 				const changedFilesList =
105 | 					result.changedFiles.length > 0
106 | 						? result.changedFiles.join("\n - ")
107 | 						: "(No changes)";
108 | 
109 | 				if (dryRun) {
110 | 					message = `Dry run complete: Renaming symbol '${symbolName}' to '${newName}' would modify the following files:\n - ${changedFilesList}`;
111 | 				} else {
112 | 					message = `Rename successful: Renamed symbol '${symbolName}' to '${newName}'. The following files were modified:\n - ${changedFilesList}`;
113 | 				}
114 | 			} catch (error) {
115 | 				const errorMessage =
116 | 					error instanceof Error ? error.message : String(error);
117 | 				message = `Error during rename process: ${errorMessage}`;
118 | 				isError = true;
119 | 			} finally {
120 | 				const endTime = performance.now();
121 | 				duration = ((endTime - startTime) / 1000).toFixed(2);
122 | 			}
123 | 
124 | 			const finalMessage = `${message}\nStatus: ${
125 | 				isError ? "Failure" : "Success"
126 | 			}\nProcessing time: ${duration} seconds`;
127 | 
128 | 			return {
129 | 				content: [{ type: "text", text: finalMessage }],
130 | 				isError: isError,
131 | 			};
132 | 		},
133 | 	);
134 | }
135 | 
```

--------------------------------------------------------------------------------
/src/ts-morph/rename-file-system/rename-file-system-entry.complex.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect } from "vitest";
  2 | import * as path from "node:path";
  3 | import { Project } from "ts-morph";
  4 | import { renameFileSystemEntry } from "./rename-file-system-entry";
  5 | 
  6 | // --- Test Setup Helper ---
  7 | 
  8 | const setupProject = () => {
  9 | 	const project = new Project({
 10 | 		useInMemoryFileSystem: true,
 11 | 		compilerOptions: {
 12 | 			baseUrl: ".",
 13 | 			paths: {
 14 | 				"@/*": ["src/*"],
 15 | 			},
 16 | 			esModuleInterop: true,
 17 | 			allowJs: true,
 18 | 		},
 19 | 	});
 20 | 
 21 | 	project.createDirectory("/src");
 22 | 	project.createDirectory("/src/utils");
 23 | 	project.createDirectory("/src/components");
 24 | 	project.createDirectory("/src/internal-feature");
 25 | 
 26 | 	return project;
 27 | };
 28 | 
 29 | describe("renameFileSystemEntry Complex Cases", () => {
 30 | 	it("内部参照を持つフォルダをリネームする", async () => {
 31 | 		const project = setupProject();
 32 | 		const oldDirPath = "/src/internal-feature";
 33 | 		const newDirPath = "/src/cool-feature";
 34 | 		const file1Path = path.join(oldDirPath, "file1.ts");
 35 | 		const file2Path = path.join(oldDirPath, "file2.ts");
 36 | 
 37 | 		project.createSourceFile(
 38 | 			file1Path,
 39 | 			`import { value2 } from './file2'; export const value1 = value2 + 1;`,
 40 | 		);
 41 | 		project.createSourceFile(file2Path, "export const value2 = 100;");
 42 | 
 43 | 		await renameFileSystemEntry({
 44 | 			project,
 45 | 			renames: [{ oldPath: oldDirPath, newPath: newDirPath }],
 46 | 			dryRun: false,
 47 | 		});
 48 | 
 49 | 		expect(project.getDirectory(newDirPath)).toBeDefined();
 50 | 		const movedFile1 = project.getSourceFile(path.join(newDirPath, "file1.ts"));
 51 | 		expect(movedFile1).toBeDefined();
 52 | 		expect(movedFile1?.getFullText()).toContain(
 53 | 			"import { value2 } from './file2';",
 54 | 		);
 55 | 	});
 56 | 
 57 | 	it("複数のファイルを同時にリネームし、それぞれの参照が正しく更新される", async () => {
 58 | 		const project = setupProject();
 59 | 		const oldFile1 = "/src/utils/file1.ts";
 60 | 		const newFile1 = "/src/utils/renamed1.ts";
 61 | 		const oldFile2 = "/src/components/file2.ts";
 62 | 		const newFile2 = "/src/components/renamed2.ts";
 63 | 		const refFile = "/src/ref.ts";
 64 | 
 65 | 		project.createSourceFile(oldFile1, "export const val1 = 1;");
 66 | 		project.createSourceFile(oldFile2, "export const val2 = 2;");
 67 | 		project.createSourceFile(
 68 | 			refFile,
 69 | 			`import { val1 } from './utils/file1';\nimport { val2 } from './components/file2';`,
 70 | 		);
 71 | 
 72 | 		await renameFileSystemEntry({
 73 | 			project,
 74 | 			renames: [
 75 | 				{ oldPath: oldFile1, newPath: newFile1 },
 76 | 				{ oldPath: oldFile2, newPath: newFile2 },
 77 | 			],
 78 | 			dryRun: false,
 79 | 		});
 80 | 
 81 | 		expect(project.getSourceFile(oldFile1)).toBeUndefined();
 82 | 		expect(project.getSourceFile(newFile1)).toBeDefined();
 83 | 		expect(project.getSourceFile(oldFile2)).toBeUndefined();
 84 | 		expect(project.getSourceFile(newFile2)).toBeDefined();
 85 | 		const updatedRef = project.getSourceFileOrThrow(refFile).getFullText();
 86 | 		expect(updatedRef).toContain("import { val1 } from './utils/renamed1';");
 87 | 		expect(updatedRef).toContain(
 88 | 			"import { val2 } from './components/renamed2';",
 89 | 		);
 90 | 	});
 91 | 
 92 | 	it("ファイルとディレクトリを同時にリネームし、それぞれの参照が正しく更新される", async () => {
 93 | 		const project = setupProject();
 94 | 		const oldFile = "/src/utils/fileA.ts";
 95 | 		const newFile = "/src/utils/fileRenamed.ts";
 96 | 		const oldDir = "/src/components";
 97 | 		const newDir = "/src/widgets";
 98 | 		const compInDir = path.join(oldDir, "comp.ts");
 99 | 		const refFile = "/src/ref.ts";
100 | 
101 | 		project.createSourceFile(oldFile, "export const valA = 'A';");
102 | 		project.createSourceFile(compInDir, "export const valComp = 'Comp';");
103 | 		project.createSourceFile(
104 | 			refFile,
105 | 			`import { valA } from './utils/fileA';\nimport { valComp } from './components/comp';`,
106 | 		);
107 | 
108 | 		await renameFileSystemEntry({
109 | 			project,
110 | 			renames: [
111 | 				{ oldPath: oldFile, newPath: newFile },
112 | 				{ oldPath: oldDir, newPath: newDir },
113 | 			],
114 | 			dryRun: false,
115 | 		});
116 | 
117 | 		expect(project.getSourceFile(oldFile)).toBeUndefined();
118 | 		expect(project.getSourceFile(newFile)).toBeDefined();
119 | 		expect(project.getDirectory(newDir)).toBeDefined();
120 | 		expect(project.getSourceFile(path.join(newDir, "comp.ts"))).toBeDefined();
121 | 		const updatedRef = project.getSourceFileOrThrow(refFile).getFullText();
122 | 		expect(updatedRef).toContain("import { valA } from './utils/fileRenamed';");
123 | 		expect(updatedRef).toContain("import { valComp } from './widgets/comp';");
124 | 	});
125 | 
126 | 	it("ファイル名をスワップする(一時ファイル経由)", async () => {
127 | 		const project = setupProject();
128 | 		const fileA = "/src/fileA.ts";
129 | 		const fileB = "/src/fileB.ts";
130 | 		const tempFile = "/src/temp.ts";
131 | 		const refFile = "/src/ref.ts";
132 | 
133 | 		project.createSourceFile(fileA, "export const valA = 'A';");
134 | 		project.createSourceFile(fileB, "export const valB = 'B';");
135 | 		project.createSourceFile(
136 | 			refFile,
137 | 			`import { valA } from './fileA';\nimport { valB } from './fileB';`,
138 | 		);
139 | 
140 | 		await renameFileSystemEntry({
141 | 			project,
142 | 			renames: [{ oldPath: fileA, newPath: tempFile }],
143 | 			dryRun: false,
144 | 		});
145 | 		await renameFileSystemEntry({
146 | 			project,
147 | 			renames: [{ oldPath: fileB, newPath: fileA }],
148 | 			dryRun: false,
149 | 		});
150 | 		await renameFileSystemEntry({
151 | 			project,
152 | 			renames: [{ oldPath: tempFile, newPath: fileB }],
153 | 			dryRun: false,
154 | 		});
155 | 
156 | 		expect(project.getSourceFile(tempFile)).toBeUndefined();
157 | 		expect(project.getSourceFile(fileA)?.getFullText()).toContain(
158 | 			"export const valB = 'B';",
159 | 		);
160 | 		expect(project.getSourceFile(fileB)?.getFullText()).toContain(
161 | 			"export const valA = 'A';",
162 | 		);
163 | 		const updatedRef = project.getSourceFileOrThrow(refFile).getFullText();
164 | 		expect(updatedRef).toContain("import { valA } from './fileB';");
165 | 		expect(updatedRef).toContain("import { valB } from './fileA';");
166 | 	});
167 | });
168 | 
```

--------------------------------------------------------------------------------
/src/ts-morph/rename-file-system/rename-file-system-entry.base.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect } from "vitest";
  2 | import { Project } from "ts-morph";
  3 | import * as path from "node:path";
  4 | import { renameFileSystemEntry } from "./rename-file-system-entry";
  5 | 
  6 | // --- Test Setup Helper ---
  7 | 
  8 | const setupProject = () => {
  9 | 	const project = new Project({
 10 | 		useInMemoryFileSystem: true,
 11 | 		compilerOptions: {
 12 | 			baseUrl: ".",
 13 | 			paths: {
 14 | 				"@/*": ["src/*"],
 15 | 			},
 16 | 			esModuleInterop: true,
 17 | 			allowJs: true,
 18 | 		},
 19 | 	});
 20 | 
 21 | 	// 共通のディレクトリ構造をメモリ上に作成
 22 | 	project.createDirectory("/src");
 23 | 	project.createDirectory("/src/utils");
 24 | 	project.createDirectory("/src/components");
 25 | 	project.createDirectory("/src/old-feature");
 26 | 	project.createDirectory("/src/myFeature");
 27 | 	project.createDirectory("/src/anotherFeature");
 28 | 	project.createDirectory("/src/dirA");
 29 | 	project.createDirectory("/src/dirB");
 30 | 	project.createDirectory("/src/dirC");
 31 | 	project.createDirectory("/src/core");
 32 | 	project.createDirectory("/src/widgets");
 33 | 
 34 | 	return project;
 35 | };
 36 | 
 37 | describe("renameFileSystemEntry Base Cases", () => {
 38 | 	it("ファイルリネーム時に相対パスとエイリアスパスのimport文を正しく更新する", async () => {
 39 | 		const project = setupProject();
 40 | 		const oldUtilPath = "/src/utils/old-util.ts";
 41 | 		const newUtilPath = "/src/utils/new-util.ts";
 42 | 		const componentPath = "/src/components/MyComponent.ts";
 43 | 		const utilIndexPath = "/src/utils/index.ts";
 44 | 
 45 | 		project.createSourceFile(
 46 | 			oldUtilPath,
 47 | 			'export const oldUtil = () => "old";',
 48 | 		);
 49 | 		project.createSourceFile(utilIndexPath, 'export * from "./old-util";');
 50 | 		project.createSourceFile(
 51 | 			componentPath,
 52 | 			`import { oldUtil as relativeImport } from '../utils/old-util';
 53 | import { oldUtil as aliasImport } from '@/utils/old-util';
 54 | import { oldUtil as indexImport } from '../utils';
 55 | 
 56 | console.log(relativeImport(), aliasImport(), indexImport());
 57 | `,
 58 | 		);
 59 | 
 60 | 		await renameFileSystemEntry({
 61 | 			project,
 62 | 			renames: [{ oldPath: oldUtilPath, newPath: newUtilPath }],
 63 | 			dryRun: false,
 64 | 		});
 65 | 
 66 | 		const updatedComponentContent = project
 67 | 			.getSourceFileOrThrow(componentPath)
 68 | 			.getFullText();
 69 | 
 70 | 		expect(updatedComponentContent).toBe(
 71 | 			`import { oldUtil as relativeImport } from '../utils/new-util';
 72 | import { oldUtil as aliasImport } from '../utils/new-util';
 73 | import { oldUtil as indexImport } from '../utils';
 74 | 
 75 | console.log(relativeImport(), aliasImport(), indexImport());
 76 | `,
 77 | 		);
 78 | 		expect(project.getSourceFile(oldUtilPath)).toBeUndefined();
 79 | 		expect(project.getSourceFile(newUtilPath)).toBeDefined();
 80 | 	});
 81 | 
 82 | 	it("フォルダリネーム時に相対パスとエイリアスパスのimport文を正しく更新する", async () => {
 83 | 		const project = setupProject();
 84 | 		const oldFeatureDir = "/src/old-feature";
 85 | 		const newFeatureDir = "/src/new-feature";
 86 | 		const featureFilePath = path.join(oldFeatureDir, "feature.ts");
 87 | 		const componentPath = "/src/components/AnotherComponent.ts";
 88 | 		const featureIndexPath = path.join(oldFeatureDir, "index.ts");
 89 | 
 90 | 		project.createSourceFile(
 91 | 			featureFilePath,
 92 | 			'export const feature = () => "feature";',
 93 | 		);
 94 | 		project.createSourceFile(featureIndexPath, 'export * from "./feature";');
 95 | 		project.createSourceFile(
 96 | 			componentPath,
 97 | 			`import { feature as relativeImport } from '../old-feature/feature';
 98 | import { feature as aliasImport } from '@/old-feature/feature';
 99 | import { feature as indexImport } from '../old-feature';
100 | 
101 | console.log(relativeImport(), aliasImport(), indexImport());
102 | `,
103 | 		);
104 | 
105 | 		await renameFileSystemEntry({
106 | 			project,
107 | 			renames: [{ oldPath: oldFeatureDir, newPath: newFeatureDir }],
108 | 			dryRun: false,
109 | 		});
110 | 
111 | 		const updatedComponentContent = project
112 | 			.getSourceFileOrThrow(componentPath)
113 | 			.getFullText();
114 | 		expect(
115 | 			updatedComponentContent,
116 | 		).toBe(`import { feature as relativeImport } from '../new-feature/feature';
117 | import { feature as aliasImport } from '../new-feature/feature';
118 | import { feature as indexImport } from '../new-feature/index';
119 | 
120 | console.log(relativeImport(), aliasImport(), indexImport());
121 | `);
122 | 
123 | 		expect(project.getDirectory(newFeatureDir)).toBeDefined();
124 | 		expect(
125 | 			project.getSourceFile(path.join(newFeatureDir, "feature.ts")),
126 | 		).toBeDefined();
127 | 		expect(
128 | 			project.getSourceFile(path.join(newFeatureDir, "index.ts")),
129 | 		).toBeDefined();
130 | 	});
131 | 
132 | 	it("同階層(.)や親階層(..)への相対パスimport文を持つファイルをリネームした際に、参照元のパスが正しく更新される", async () => {
133 | 		const project = setupProject();
134 | 		const dirA = "/src/dirA";
135 | 		const dirB = "/src/dirB";
136 | 
137 | 		const fileA1Path = path.join(dirA, "fileA1.ts");
138 | 		const fileA2Path = path.join(dirA, "fileA2.ts");
139 | 		const fileBPath = path.join(dirB, "fileB.ts");
140 | 		const fileA3Path = path.join(dirA, "fileA3.ts");
141 | 
142 | 		project.createSourceFile(fileA1Path, "export const valA1 = 1;");
143 | 		project.createSourceFile(fileA2Path, "export const valA2 = 2;");
144 | 		project.createSourceFile(
145 | 			fileBPath,
146 | 			`
147 | import { valA2 } from '../dirA/fileA2';
148 | import { valA1 } from '../dirA/fileA1';
149 | console.log(valA2, valA1);
150 |         `,
151 | 		);
152 | 		project.createSourceFile(
153 | 			fileA3Path,
154 | 			`
155 | import { valA2 } from './fileA2';
156 | console.log(valA2);
157 | `,
158 | 		);
159 | 
160 | 		const newFileA2Path = path.join(dirA, "renamedA2.ts");
161 | 
162 | 		await renameFileSystemEntry({
163 | 			project,
164 | 			renames: [{ oldPath: fileA2Path, newPath: newFileA2Path }],
165 | 			dryRun: false,
166 | 		});
167 | 
168 | 		const updatedFileBContent = project
169 | 			.getSourceFileOrThrow(fileBPath)
170 | 			.getFullText();
171 | 		const updatedFileA3Content = project
172 | 			.getSourceFileOrThrow(fileA3Path)
173 | 			.getFullText();
174 | 
175 | 		expect(updatedFileBContent).toContain(
176 | 			"import { valA2 } from '../dirA/renamedA2';",
177 | 		);
178 | 		expect(updatedFileBContent).toContain(
179 | 			"import { valA1 } from '../dirA/fileA1';",
180 | 		);
181 | 		expect(updatedFileA3Content).toContain(
182 | 			"import { valA2 } from './renamedA2';",
183 | 		);
184 | 
185 | 		expect(project.getSourceFile(fileA2Path)).toBeUndefined();
186 | 		expect(project.getSourceFile(newFileA2Path)).toBeDefined();
187 | 	});
188 | 
189 | 	it("親階層(..)への相対パスimport文を持つファイルを、別のディレクトリに移動(リネーム)した際に、参照元のパスが正しく更新される", async () => {
190 | 		const project = setupProject();
191 | 		const dirA = "/src/dirA";
192 | 		const dirC = "/src/dirC";
193 | 
194 | 		const fileA1Path = path.join(dirA, "fileA1.ts");
195 | 		const fileA2Path = path.join(dirA, "fileA2.ts");
196 | 
197 | 		project.createSourceFile(fileA1Path, "export const valA1 = 1;");
198 | 		project.createSourceFile(
199 | 			fileA2Path,
200 | 			`
201 | import { valA1 } from './fileA1';
202 | console.log(valA1);
203 | `,
204 | 		);
205 | 
206 | 		const newFileA1Path = path.join(dirC, "movedA1.ts");
207 | 
208 | 		await renameFileSystemEntry({
209 | 			project,
210 | 			renames: [{ oldPath: fileA1Path, newPath: newFileA1Path }],
211 | 			dryRun: false,
212 | 		});
213 | 
214 | 		const updatedFileA2Content = project
215 | 			.getSourceFileOrThrow(fileA2Path)
216 | 			.getFullText();
217 | 		expect(updatedFileA2Content).toContain(
218 | 			"import { valA1 } from '../dirC/movedA1';",
219 | 		);
220 | 
221 | 		expect(project.getSourceFile(fileA1Path)).toBeUndefined();
222 | 		expect(project.getSourceFile(newFileA1Path)).toBeDefined();
223 | 	});
224 | });
225 | 
```

--------------------------------------------------------------------------------
/src/ts-morph/move-symbol-to-file/generate-content/build-new-file-import-section.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import logger from "../../../utils/logger";
  2 | import { calculateRelativePath } from "../../_utils/calculate-relative-path";
  3 | import type {
  4 | 	DependencyClassification,
  5 | 	NeededExternalImports,
  6 | } from "../../types";
  7 | 
  8 | type ExtendedImportInfo = {
  9 | 	defaultName?: string;
 10 | 	namedImports: Set<string>;
 11 | 	isNamespaceImport: boolean;
 12 | 	namespaceImportName?: string;
 13 | };
 14 | 
 15 | export type ImportMap = Map<string, ExtendedImportInfo>;
 16 | 
 17 | function aggregateImports(
 18 | 	importMap: ImportMap,
 19 | 	relativePath: string,
 20 | 	importName: string,
 21 | 	isDefault: boolean,
 22 | ) {
 23 | 	if (isDefault) {
 24 | 		const actualDefaultName = importName;
 25 | 		if (!importMap.has(relativePath)) {
 26 | 			importMap.set(relativePath, {
 27 | 				namedImports: new Set(),
 28 | 				isNamespaceImport: false,
 29 | 			});
 30 | 		}
 31 | 		const entry = importMap.get(relativePath);
 32 | 		if (!entry || entry.isNamespaceImport) {
 33 | 			logger.warn(
 34 | 				`Skipping default import aggregation for ${relativePath} due to existing namespace import or missing entry.`,
 35 | 			);
 36 | 			return;
 37 | 		}
 38 | 		entry.defaultName = actualDefaultName;
 39 | 		logger.debug(
 40 | 			`Aggregated default import: ${actualDefaultName} for path: ${relativePath}`,
 41 | 		);
 42 | 		return;
 43 | 	}
 44 | 	const nameToAdd = importName;
 45 | 	if (!importMap.has(relativePath)) {
 46 | 		importMap.set(relativePath, {
 47 | 			namedImports: new Set(),
 48 | 			isNamespaceImport: false,
 49 | 		});
 50 | 	}
 51 | 	const entry = importMap.get(relativePath);
 52 | 	if (!entry || entry.isNamespaceImport) {
 53 | 		logger.warn(
 54 | 			`Skipping named import aggregation for ${relativePath} due to existing namespace import or missing entry.`,
 55 | 		);
 56 | 		return;
 57 | 	}
 58 | 	entry.namedImports.add(nameToAdd);
 59 | 	logger.debug(
 60 | 		`Aggregated named import: ${nameToAdd} for path: ${relativePath}`,
 61 | 	);
 62 | }
 63 | 
 64 | function processExternalImports(
 65 | 	importMap: ImportMap,
 66 | 	neededExternalImports: NeededExternalImports,
 67 | 	newFilePath: string,
 68 | ): void {
 69 | 	logger.debug("Processing external imports...");
 70 | 	for (const [
 71 | 		originalModuleSpecifier,
 72 | 		{ names, declaration, isNamespaceImport, namespaceImportName },
 73 | 	] of neededExternalImports.entries()) {
 74 | 		const moduleSourceFile = declaration?.getModuleSpecifierSourceFile();
 75 | 		let relativePath = "";
 76 | 		let isSelfReference = false;
 77 | 
 78 | 		if (
 79 | 			moduleSourceFile &&
 80 | 			!moduleSourceFile.getFilePath().includes("/node_modules/")
 81 | 		) {
 82 | 			const absoluteModulePath = moduleSourceFile.getFilePath();
 83 | 			if (absoluteModulePath === newFilePath) {
 84 | 				isSelfReference = true;
 85 | 			} else {
 86 | 				relativePath = calculateRelativePath(newFilePath, absoluteModulePath);
 87 | 				logger.debug(
 88 | 					`Calculated relative path for NON-node_modules import: ${relativePath} (from ${absoluteModulePath})`,
 89 | 				);
 90 | 			}
 91 | 		} else {
 92 | 			relativePath = originalModuleSpecifier;
 93 | 			logger.debug(
 94 | 				`Using original module specifier for node_modules or unresolved import: ${relativePath}`,
 95 | 			);
 96 | 		}
 97 | 
 98 | 		if (isSelfReference) {
 99 | 			logger.debug(`Skipping self-reference import for path: ${newFilePath}`);
100 | 			continue;
101 | 		}
102 | 
103 | 		if (isNamespaceImport && namespaceImportName) {
104 | 			if (!importMap.has(relativePath)) {
105 | 				importMap.set(relativePath, {
106 | 					namedImports: new Set(),
107 | 					isNamespaceImport: true,
108 | 					namespaceImportName: namespaceImportName,
109 | 				});
110 | 				logger.debug(
111 | 					`Added namespace import: ${namespaceImportName} for path: ${relativePath}`,
112 | 				);
113 | 			} else {
114 | 				logger.warn(
115 | 					`Namespace import for ${relativePath} conflicts with existing non-namespace imports. Skipping.`,
116 | 				);
117 | 			}
118 | 			continue;
119 | 		}
120 | 
121 | 		const defaultImportNode = declaration?.getDefaultImport();
122 | 		const actualDefaultName = defaultImportNode?.getText();
123 | 
124 | 		for (const name of names) {
125 | 			const isDefaultFlag = name === "default" && !!actualDefaultName;
126 | 			if (isDefaultFlag) {
127 | 				if (!actualDefaultName) {
128 | 					logger.warn(
129 | 						`Default import name was expected but not found for ${relativePath}. Skipping default import.`,
130 | 					);
131 | 					continue;
132 | 				}
133 | 				aggregateImports(importMap, relativePath, actualDefaultName, true);
134 | 			} else {
135 | 				aggregateImports(importMap, relativePath, name, false);
136 | 			}
137 | 		}
138 | 	}
139 | }
140 | 
141 | function processInternalDependencies(
142 | 	importMap: ImportMap,
143 | 	classifiedDependencies: DependencyClassification[],
144 | 	newFilePath: string,
145 | 	originalFilePath: string,
146 | ): void {
147 | 	logger.debug("Processing internal dependencies for import map...");
148 | 
149 | 	if (newFilePath === originalFilePath) {
150 | 		logger.debug(
151 | 			"Skipping internal dependency processing as source and target files are the same.",
152 | 		);
153 | 		return;
154 | 	}
155 | 
156 | 	const dependenciesToImportNames = new Set<string>();
157 | 
158 | 	for (const dep of classifiedDependencies) {
159 | 		if (dep.type === "importFromOriginal" || dep.type === "addExport") {
160 | 			logger.debug(`Internal dependency to import from original: ${dep.name}`);
161 | 			dependenciesToImportNames.add(dep.name);
162 | 		}
163 | 	}
164 | 
165 | 	if (dependenciesToImportNames.size === 0) {
166 | 		logger.debug("No internal dependencies need importing from original file.");
167 | 		return;
168 | 	}
169 | 
170 | 	const internalImportPath = calculateRelativePath(
171 | 		newFilePath,
172 | 		originalFilePath,
173 | 	);
174 | 	logger.debug(
175 | 		`Calculated relative path for internal import: ${internalImportPath}`,
176 | 	);
177 | 
178 | 	if (internalImportPath !== "." && internalImportPath !== "./") {
179 | 		for (const name of dependenciesToImportNames) {
180 | 			aggregateImports(importMap, internalImportPath, name, false);
181 | 		}
182 | 	} else {
183 | 		logger.debug("Skipping aggregation for self-referencing internal path.");
184 | 	}
185 | }
186 | 
187 | function buildImportStatementString(
188 | 	defaultImportName: string | undefined,
189 | 	namedImportSpecifiers: string,
190 | 	relativePath: string,
191 | 	isNamespaceImport: boolean,
192 | 	namespaceImportName?: string,
193 | ): string {
194 | 	const fromPart = `from "${relativePath}";`;
195 | 	if (isNamespaceImport && namespaceImportName) {
196 | 		return `import * as ${namespaceImportName} ${fromPart}`;
197 | 	}
198 | 	if (!defaultImportName && !namedImportSpecifiers) {
199 | 		logger.debug(`Building side-effect import for ${relativePath}`);
200 | 		return `import ${fromPart}`;
201 | 	}
202 | 	const defaultPart = defaultImportName ? `${defaultImportName}` : "";
203 | 	const namedPart = namedImportSpecifiers ? `{ ${namedImportSpecifiers} }` : "";
204 | 	const separator = defaultPart && namedPart ? ", " : "";
205 | 	return `import ${defaultPart}${separator}${namedPart} ${fromPart}`;
206 | }
207 | 
208 | export function calculateRequiredImportMap(
209 | 	neededExternalImports: NeededExternalImports,
210 | 	classifiedDependencies: DependencyClassification[],
211 | 	newFilePath: string,
212 | 	originalFilePath: string,
213 | ): ImportMap {
214 | 	const importMap: ImportMap = new Map();
215 | 	processExternalImports(importMap, neededExternalImports, newFilePath);
216 | 	processInternalDependencies(
217 | 		importMap,
218 | 		classifiedDependencies,
219 | 		newFilePath,
220 | 		originalFilePath,
221 | 	);
222 | 	return importMap;
223 | }
224 | 
225 | export function buildImportSectionStringFromMap(importMap: ImportMap): string {
226 | 	logger.debug("Generating import section string...");
227 | 	let importSection = "";
228 | 	const sortedPaths = [...importMap.keys()].sort();
229 | 	for (const path of sortedPaths) {
230 | 		const importData = importMap.get(path);
231 | 		if (!importData) {
232 | 			logger.warn(`Import data not found for path ${path} during generation.`);
233 | 			continue;
234 | 		}
235 | 		const {
236 | 			defaultName,
237 | 			namedImports,
238 | 			isNamespaceImport,
239 | 			namespaceImportName,
240 | 		} = importData;
241 | 		const sortedNamedImports = [...namedImports].sort().join(", ");
242 | 		const importStatement = buildImportStatementString(
243 | 			defaultName,
244 | 			sortedNamedImports,
245 | 			path,
246 | 			isNamespaceImport,
247 | 			namespaceImportName,
248 | 		);
249 | 		if (importStatement) {
250 | 			importSection += `${importStatement}\n`;
251 | 		}
252 | 	}
253 | 	if (importSection) {
254 | 		importSection += "\n";
255 | 	}
256 | 	logger.debug(`Generated Import Section String:
257 | ${importSection}`);
258 | 	return importSection;
259 | }
260 | 
```

--------------------------------------------------------------------------------
/src/ts-morph/move-symbol-to-file/update-imports-in-referencing-files.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect } from "vitest";
  2 | import { Project, IndentationText, QuoteKind } from "ts-morph";
  3 | import { updateImportsInReferencingFiles } from "./update-imports-in-referencing-files";
  4 | 
  5 | describe("updateImportsInReferencingFiles", () => {
  6 | 	const oldDirPath = "/src/moduleA";
  7 | 	const oldFilePath = `${oldDirPath}/old-location.ts`;
  8 | 	const moduleAIndexPath = `${oldDirPath}/index.ts`;
  9 | 	const newFilePath = "/src/moduleC/new-location.ts";
 10 | 
 11 | 	// --- Setup Helper Function ---
 12 | 	const setupTestProject = () => {
 13 | 		const project = new Project({
 14 | 			manipulationSettings: {
 15 | 				indentationText: IndentationText.TwoSpaces,
 16 | 				quoteKind: QuoteKind.Single,
 17 | 			},
 18 | 			useInMemoryFileSystem: true,
 19 | 			compilerOptions: {
 20 | 				baseUrl: ".",
 21 | 				paths: {
 22 | 					"@/*": ["src/*"],
 23 | 				},
 24 | 				typeRoots: [],
 25 | 			},
 26 | 		});
 27 | 
 28 | 		project.createDirectory("/src");
 29 | 		project.createDirectory(oldDirPath);
 30 | 		project.createDirectory("/src/moduleB");
 31 | 		project.createDirectory("/src/moduleC");
 32 | 		project.createDirectory("/src/moduleD");
 33 | 		project.createDirectory("/src/moduleE");
 34 | 		project.createDirectory("/src/moduleF");
 35 | 		project.createDirectory("/src/moduleG");
 36 | 
 37 | 		// Use literal strings for symbols in setup
 38 | 		project.createSourceFile(
 39 | 			oldFilePath,
 40 | 			`export const exportedSymbol = 123;
 41 | export const anotherSymbol = 456;
 42 | export type MyType = { id: number };
 43 | `,
 44 | 		);
 45 | 
 46 | 		project.createSourceFile(
 47 | 			moduleAIndexPath,
 48 | 			`export { exportedSymbol, anotherSymbol } from './old-location';
 49 | export type { MyType } from './old-location';
 50 | `,
 51 | 		);
 52 | 
 53 | 		const importerRel = project.createSourceFile(
 54 | 			"/src/moduleB/importer-relative.ts",
 55 | 			`import { exportedSymbol } from '../moduleA/old-location';\nconsole.log(exportedSymbol);`,
 56 | 		);
 57 | 
 58 | 		const importerAlias = project.createSourceFile(
 59 | 			"/src/moduleD/importer-alias.ts",
 60 | 			`import { anotherSymbol } from '@/moduleA/old-location';\nconsole.log(anotherSymbol);`,
 61 | 		);
 62 | 
 63 | 		const importerIndex = project.createSourceFile(
 64 | 			"/src/moduleE/importer-index.ts",
 65 | 			`import { exportedSymbol } from '../moduleA';\nconsole.log(exportedSymbol);`,
 66 | 		);
 67 | 
 68 | 		const importerMulti = project.createSourceFile(
 69 | 			"/src/moduleF/importer-multi.ts",
 70 | 			`import { exportedSymbol, anotherSymbol } from '../moduleA/old-location';\nconsole.log(exportedSymbol, anotherSymbol);`,
 71 | 		);
 72 | 
 73 | 		const importerType = project.createSourceFile(
 74 | 			"/src/moduleG/importer-type.ts",
 75 | 			`import type { MyType } from '../moduleA/old-location';\nlet val: MyType;`,
 76 | 		);
 77 | 
 78 | 		const noRefFile = project.createSourceFile(
 79 | 			"/src/no-ref.ts",
 80 | 			'console.log("hello");',
 81 | 		);
 82 | 
 83 | 		return {
 84 | 			project,
 85 | 			importerRelPath: "/src/moduleB/importer-relative.ts",
 86 | 			importerAliasPath: "/src/moduleD/importer-alias.ts",
 87 | 			importerIndexPath: "/src/moduleE/importer-index.ts",
 88 | 			importerMultiPath: "/src/moduleF/importer-multi.ts",
 89 | 			importerTypePath: "/src/moduleG/importer-type.ts",
 90 | 			noRefFilePath: "/src/no-ref.ts",
 91 | 			oldFilePath,
 92 | 			newFilePath,
 93 | 		};
 94 | 	};
 95 | 
 96 | 	it("相対パスでインポートしているファイルのパスを正しく更新する", async () => {
 97 | 		const { project, oldFilePath, newFilePath, importerRelPath } =
 98 | 			setupTestProject();
 99 | 		await updateImportsInReferencingFiles(
100 | 			project,
101 | 			oldFilePath,
102 | 			newFilePath,
103 | 			"exportedSymbol",
104 | 		);
105 | 		const expected = `import { exportedSymbol } from '../moduleC/new-location';
106 | console.log(exportedSymbol);`;
107 | 		expect(project.getSourceFile(importerRelPath)?.getText()).toBe(expected);
108 | 	});
109 | 
110 | 	it("エイリアスパスでインポートしているファイルのパスを正しく更新する (相対パスになる)", async () => {
111 | 		const { project, oldFilePath, newFilePath, importerAliasPath } =
112 | 			setupTestProject();
113 | 		await updateImportsInReferencingFiles(
114 | 			project,
115 | 			oldFilePath,
116 | 			newFilePath,
117 | 			"anotherSymbol",
118 | 		);
119 | 		const expected = `import { anotherSymbol } from '../moduleC/new-location';
120 | console.log(anotherSymbol);`;
121 | 		expect(project.getSourceFile(importerAliasPath)?.getText()).toBe(expected);
122 | 	});
123 | 
124 | 	it("複数のファイルから参照されている場合、指定したシンボルのパスのみ更新する", async () => {
125 | 		const {
126 | 			project,
127 | 			oldFilePath,
128 | 			newFilePath,
129 | 			importerRelPath,
130 | 			importerAliasPath,
131 | 		} = setupTestProject();
132 | 		await updateImportsInReferencingFiles(
133 | 			project,
134 | 			oldFilePath,
135 | 			newFilePath,
136 | 			"exportedSymbol",
137 | 		);
138 | 
139 | 		const expectedRel = `import { exportedSymbol } from '../moduleC/new-location';
140 | console.log(exportedSymbol);`;
141 | 		expect(project.getSourceFile(importerRelPath)?.getText()).toBe(expectedRel);
142 | 
143 | 		const expectedAlias = `import { anotherSymbol } from '@/moduleA/old-location';
144 | console.log(anotherSymbol);`;
145 | 		expect(project.getSourceFile(importerAliasPath)?.getText()).toBe(
146 | 			expectedAlias,
147 | 		);
148 | 	});
149 | 
150 | 	it("複数の名前付きインポートを持つファイルのパスを、指定したシンボルのみ更新する", async () => {
151 | 		const { project, oldFilePath, newFilePath, importerMultiPath } =
152 | 			setupTestProject();
153 | 		const symbolToMove = "exportedSymbol";
154 | 
155 | 		await updateImportsInReferencingFiles(
156 | 			project,
157 | 			oldFilePath,
158 | 			newFilePath,
159 | 			symbolToMove,
160 | 		);
161 | 
162 | 		const expected = `import { anotherSymbol } from '../moduleA/old-location';
163 | import { exportedSymbol } from '../moduleC/new-location';
164 | 
165 | console.log(exportedSymbol, anotherSymbol);`;
166 | 		expect(project.getSourceFile(importerMultiPath)?.getText()).toBe(expected);
167 | 	});
168 | 
169 | 	it("Typeインポートを持つファイルのパスを正しく更新する", async () => {
170 | 		const { project, oldFilePath, newFilePath, importerTypePath } =
171 | 			setupTestProject();
172 | 		await updateImportsInReferencingFiles(
173 | 			project,
174 | 			oldFilePath,
175 | 			newFilePath,
176 | 			"MyType",
177 | 		);
178 | 		const expected = `import type { MyType } from '../moduleC/new-location';
179 | let val: MyType;`;
180 | 		expect(project.getSourceFile(importerTypePath)?.getText()).toBe(expected);
181 | 	});
182 | 
183 | 	it("移動元ファイルへの参照がない場合、エラーなく完了し、他のファイルは変更されない", async () => {
184 | 		const { project, oldFilePath, newFilePath, noRefFilePath } =
185 | 			setupTestProject();
186 | 		const originalContent =
187 | 			project.getSourceFile(noRefFilePath)?.getText() ?? "";
188 | 
189 | 		await expect(
190 | 			updateImportsInReferencingFiles(
191 | 				project,
192 | 				oldFilePath,
193 | 				newFilePath,
194 | 				"exportedSymbol",
195 | 			),
196 | 		).resolves.toBeUndefined();
197 | 
198 | 		expect(project.getSourceFile(noRefFilePath)?.getText()).toBe(
199 | 			originalContent,
200 | 		);
201 | 	});
202 | 
203 | 	it("移動先ファイルが元々移動元シンボルをインポートしていた場合、そのインポート指定子/宣言を削除する", async () => {
204 | 		const project = new Project({ useInMemoryFileSystem: true });
205 | 		const oldPath = "/src/old.ts";
206 | 		const newPath = "/src/new.ts";
207 | 
208 | 		project.createSourceFile(
209 | 			oldPath,
210 | 			"export const symbolToMove = 1; export const keepSymbol = 2;",
211 | 		);
212 | 		const referencingFile = project.createSourceFile(
213 | 			newPath,
214 | 			`import { symbolToMove, keepSymbol } from './old';
215 | console.log(symbolToMove, keepSymbol);`,
216 | 		);
217 | 
218 | 		await updateImportsInReferencingFiles(
219 | 			project,
220 | 			oldPath,
221 | 			newPath,
222 | 			"symbolToMove",
223 | 		);
224 | 
225 | 		const expected = `import { keepSymbol } from './old';
226 | console.log(symbolToMove, keepSymbol);`;
227 | 		expect(referencingFile.getText()).toBe(expected);
228 | 
229 | 		// --- ケース2: 移動対象シンボルのみインポートしていた場合 ---
230 | 		const project2 = new Project({ useInMemoryFileSystem: true });
231 | 		project2.createSourceFile(oldPath, "export const symbolToMove = 1;");
232 | 		const referencingFile2 = project2.createSourceFile(
233 | 			newPath,
234 | 			`import { symbolToMove } from './old';
235 | console.log(symbolToMove);`,
236 | 		);
237 | 
238 | 		await updateImportsInReferencingFiles(
239 | 			project2,
240 | 			oldPath,
241 | 			newPath,
242 | 			"symbolToMove",
243 | 		);
244 | 
245 | 		expect(referencingFile2.getText()).toBe("console.log(symbolToMove);");
246 | 	});
247 | 
248 | 	// --- 【制限事項確認】将来的に対応したいケース ---
249 | 	it.skip("【制限事項】バレルファイル経由でインポートしているファイルのパスは更新される", async () => {
250 | 		const { project, oldFilePath, newFilePath, importerIndexPath } =
251 | 			setupTestProject();
252 | 		await updateImportsInReferencingFiles(
253 | 			project,
254 | 			oldFilePath,
255 | 			newFilePath,
256 | 			"exportedSymbol",
257 | 		);
258 | 		const updatedContent =
259 | 			project.getSourceFile(importerIndexPath)?.getText() ?? "";
260 | 		const expectedImportPath = "../../moduleC/new-location";
261 | 		const expected = `import { exportedSymbol } from '${expectedImportPath}';
262 | console.log(exportedSymbol);`;
263 | 		expect(updatedContent).toBe(expected);
264 | 	});
265 | });
266 | 
```

--------------------------------------------------------------------------------
/src/ts-morph/_utils/find-declarations-to-update.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect } from "vitest";
  2 | import { Project, IndentationText, QuoteKind } from "ts-morph";
  3 | import { findDeclarationsReferencingFile } from "./find-declarations-to-update";
  4 | 
  5 | // --- Setup Helper Function ---
  6 | const setupTestProject = () => {
  7 | 	const project = new Project({
  8 | 		manipulationSettings: {
  9 | 			indentationText: IndentationText.TwoSpaces,
 10 | 			quoteKind: QuoteKind.Single,
 11 | 		},
 12 | 		useInMemoryFileSystem: true,
 13 | 		compilerOptions: {
 14 | 			baseUrl: ".",
 15 | 			paths: {
 16 | 				"@/*": ["src/*"],
 17 | 				"@utils/*": ["src/utils/*"],
 18 | 			},
 19 | 			// typeRoots: [], // Avoids errors on potentially missing node types if not installed
 20 | 		},
 21 | 	});
 22 | 
 23 | 	// Target file
 24 | 	const targetFilePath = "/src/target.ts";
 25 | 	const targetFile = project.createSourceFile(
 26 | 		targetFilePath,
 27 | 		`export const targetSymbol = 'target';
 28 | export type TargetType = number;`,
 29 | 	);
 30 | 
 31 | 	// File importing with relative path
 32 | 	const importerRelPath = "/src/importer-relative.ts";
 33 | 	project.createSourceFile(
 34 | 		importerRelPath,
 35 | 		`import { targetSymbol } from './target';
 36 | import type { TargetType } from './target';
 37 | console.log(targetSymbol);`,
 38 | 	);
 39 | 
 40 | 	// File importing with alias path
 41 | 	const importerAliasPath = "/src/importer-alias.ts";
 42 | 	project.createSourceFile(
 43 | 		importerAliasPath,
 44 | 		`import { targetSymbol } from '@/target';
 45 | console.log(targetSymbol);`,
 46 | 	);
 47 | 
 48 | 	// Barrel file re-exporting from target
 49 | 	const barrelFilePath = "/src/index.ts";
 50 | 	project.createSourceFile(
 51 | 		barrelFilePath,
 52 | 		`export { targetSymbol } from './target'; // 値を再エクスポート
 53 | export type { TargetType } from './target'; // 型を再エクスポート`,
 54 | 	);
 55 | 
 56 | 	// File importing from barrel file
 57 | 	const importerBarrelPath = "/src/importer-barrel.ts";
 58 | 	project.createSourceFile(
 59 | 		importerBarrelPath,
 60 | 		`import { targetSymbol } from './index'; // バレルファイルからインポート
 61 | console.log(targetSymbol);`,
 62 | 	);
 63 | 
 64 | 	// File with no reference
 65 | 	const noRefFilePath = "/src/no-ref.ts";
 66 | 	project.createSourceFile(noRefFilePath, "const unrelated = 1;");
 67 | 
 68 | 	return {
 69 | 		project,
 70 | 		targetFile,
 71 | 		targetFilePath,
 72 | 		importerRelPath,
 73 | 		importerAliasPath,
 74 | 		barrelFilePath,
 75 | 		importerBarrelPath,
 76 | 		noRefFilePath,
 77 | 	};
 78 | };
 79 | 
 80 | describe("findDeclarationsReferencingFile", () => {
 81 | 	it("target.ts を直接参照している全ての宣言 (Import/Export) を見つける", async () => {
 82 | 		const {
 83 | 			project,
 84 | 			targetFile,
 85 | 			targetFilePath,
 86 | 			importerRelPath,
 87 | 			importerAliasPath,
 88 | 			barrelFilePath,
 89 | 		} = setupTestProject();
 90 | 		const results = await findDeclarationsReferencingFile(targetFile);
 91 | 
 92 | 		// 期待値: 5つの宣言 (相対パスインポートx2, エイリアスパスインポートx1, バレルエクスポートx2)
 93 | 		expect(results).toHaveLength(5);
 94 | 
 95 | 		// --- 相対パスインポートの検証 ---
 96 | 		const relativeImports = results.filter(
 97 | 			(r) =>
 98 | 				r.referencingFilePath === importerRelPath &&
 99 | 				r.declaration.getKindName() === "ImportDeclaration",
100 | 		);
101 | 		expect(relativeImports).toHaveLength(2);
102 | 		const valueRelImport = relativeImports.find((r) =>
103 | 			r.declaration.getText().includes("targetSymbol"),
104 | 		);
105 | 		expect(valueRelImport?.originalSpecifierText).toBe("./target");
106 | 		const typeRelImport = relativeImports.find((r) =>
107 | 			r.declaration.getText().includes("TargetType"),
108 | 		);
109 | 		expect(typeRelImport?.originalSpecifierText).toBe("./target");
110 | 
111 | 		// --- エイリアスパスインポートの検証 ---
112 | 		const aliasImports = results.filter(
113 | 			(r) =>
114 | 				r.referencingFilePath === importerAliasPath &&
115 | 				r.declaration.getKindName() === "ImportDeclaration",
116 | 		);
117 | 		expect(aliasImports).toHaveLength(1);
118 | 		expect(aliasImports[0].originalSpecifierText).toBe("@/target");
119 | 		expect(aliasImports[0].wasPathAlias).toBe(true);
120 | 
121 | 		// --- バレルエクスポートの検証 ---
122 | 		const barrelExports = results.filter(
123 | 			(r) =>
124 | 				r.referencingFilePath === barrelFilePath &&
125 | 				r.declaration.getKindName() === "ExportDeclaration",
126 | 		);
127 | 		expect(barrelExports).toHaveLength(2);
128 | 		const valueBarrelExport = barrelExports.find((r) =>
129 | 			r.declaration.getText().includes("targetSymbol"),
130 | 		);
131 | 		expect(valueBarrelExport?.originalSpecifierText).toBe("./target");
132 | 		const typeBarrelExport = barrelExports.find((r) =>
133 | 			r.declaration.getText().includes("TargetType"),
134 | 		);
135 | 		expect(typeBarrelExport?.originalSpecifierText).toBe("./target");
136 | 	});
137 | 
138 | 	it("エイリアスパスでインポートしている ImportDeclaration を見つけ、wasPathAlias が true になる", async () => {
139 | 		const { project, targetFile, targetFilePath, importerAliasPath } =
140 | 			setupTestProject();
141 | 		const results = await findDeclarationsReferencingFile(targetFile);
142 | 
143 | 		// エイリアスパスによるインポートを特定する
144 | 		const aliasImports = results.filter(
145 | 			(r) => r.referencingFilePath === importerAliasPath,
146 | 		);
147 | 		expect(aliasImports).toHaveLength(1);
148 | 		const aliasImport = aliasImports[0];
149 | 
150 | 		expect(aliasImport).toBeDefined();
151 | 		expect(aliasImport.referencingFilePath).toBe(importerAliasPath);
152 | 		expect(aliasImport.resolvedPath).toBe(targetFilePath);
153 | 		expect(aliasImport.originalSpecifierText).toBe("@/target");
154 | 		expect(aliasImport.declaration.getKindName()).toBe("ImportDeclaration");
155 | 		expect(aliasImport.wasPathAlias).toBe(true); // エイリアスが検出されるべき
156 | 	});
157 | 
158 | 	it("バレルファイルで再エクスポートしている ExportDeclaration を見つける", async () => {
159 | 		const { project, targetFile, targetFilePath, barrelFilePath } =
160 | 			setupTestProject();
161 | 		const results = await findDeclarationsReferencingFile(targetFile);
162 | 
163 | 		// バレルファイルからのエクスポートを特定する
164 | 		const exportDeclarations = results.filter(
165 | 			(r) => r.referencingFilePath === barrelFilePath,
166 | 		);
167 | 		expect(exportDeclarations).toHaveLength(2);
168 | 
169 | 		const valueExport = exportDeclarations.find((r) =>
170 | 			r.declaration.getText().includes("targetSymbol"),
171 | 		);
172 | 		expect(valueExport).toBeDefined();
173 | 		expect(valueExport?.referencingFilePath).toBe(barrelFilePath);
174 | 		expect(valueExport?.resolvedPath).toBe(targetFilePath);
175 | 		expect(valueExport?.originalSpecifierText).toBe("./target");
176 | 		expect(valueExport?.declaration.getKindName()).toBe("ExportDeclaration");
177 | 		expect(valueExport?.wasPathAlias).toBe(false);
178 | 
179 | 		const typeExport = exportDeclarations.find((r) =>
180 | 			r.declaration.getText().includes("TargetType"),
181 | 		);
182 | 		expect(typeExport).toBeDefined();
183 | 		expect(typeExport?.referencingFilePath).toBe(barrelFilePath);
184 | 		expect(typeExport?.resolvedPath).toBe(targetFilePath);
185 | 		expect(typeExport?.originalSpecifierText).toBe("./target");
186 | 		expect(typeExport?.declaration.getKindName()).toBe("ExportDeclaration");
187 | 		expect(typeExport?.wasPathAlias).toBe(false);
188 | 	});
189 | 
190 | 	// findDeclarationsReferencingFile は getReferencingSourceFiles を使うため、
191 | 	// バレルファイルを経由した参照は見つけられない (これは想定される動作)
192 | 	it("バレルファイル経由のインポートは見つけられない (getReferencingSourceFiles の仕様)", async () => {
193 | 		const { project, targetFile, importerBarrelPath } = setupTestProject();
194 | 		const results = await findDeclarationsReferencingFile(targetFile);
195 | 
196 | 		// 結果に importerBarrelPath からのインポートが含まれないことを確認
197 | 		const barrelImport = results.find(
198 | 			(r) => r.referencingFilePath === importerBarrelPath,
199 | 		);
200 | 		expect(barrelImport).toBeUndefined();
201 | 	});
202 | 
203 | 	it("対象ファイルへの参照がない場合は空の配列を返す", async () => {
204 | 		const { project } = setupTestProject();
205 | 		// 参照されていないファイルを作成
206 | 		const unreferencedFile = project.createSourceFile(
207 | 			"/src/unreferenced.ts",
208 | 			"export const x = 1;",
209 | 		);
210 | 		const results = await findDeclarationsReferencingFile(unreferencedFile);
211 | 		expect(results).toHaveLength(0);
212 | 	});
213 | 
214 | 	it("Import と Export が混在する場合、両方を見つけられる", async () => {
215 | 		const { project, targetFile, targetFilePath } = setupTestProject();
216 | 		// target からインポートとエクスポートの両方を行う別のファイルを追加
217 | 		const mixedRefPath = "/src/mixed-ref.ts";
218 | 		project.createSourceFile(
219 | 			mixedRefPath,
220 | 			`
221 | 			import { targetSymbol } from './target';
222 | 			export { TargetType } from './target';
223 | 			console.log(targetSymbol);
224 | 		`,
225 | 		);
226 | 		const results = await findDeclarationsReferencingFile(targetFile);
227 | 
228 | 		// mixedRefPath からの2つの宣言 + セットアップからの他の宣言を期待
229 | 		const mixedRefs = results.filter(
230 | 			(r) => r.referencingFilePath === mixedRefPath,
231 | 		);
232 | 		expect(mixedRefs).toHaveLength(2);
233 | 
234 | 		const importDecl = mixedRefs.find(
235 | 			(d) => d.declaration.getKindName() === "ImportDeclaration",
236 | 		);
237 | 		const exportDecl = mixedRefs.find(
238 | 			(d) => d.declaration.getKindName() === "ExportDeclaration",
239 | 		);
240 | 		expect(importDecl).toBeDefined();
241 | 		expect(exportDecl).toBeDefined();
242 | 	});
243 | });
244 | 
```

--------------------------------------------------------------------------------
/src/ts-morph/remove-path-alias/remove-path-alias.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { Project } from "ts-morph";
  2 | import { describe, it, expect } from "vitest";
  3 | import * as path from "node:path";
  4 | import { removePathAlias } from "./remove-path-alias";
  5 | 
  6 | const TEST_TSCONFIG_PATH = "/tsconfig.json";
  7 | const TEST_BASE_URL = "/src";
  8 | const TEST_PATHS = {
  9 | 	"@/*": ["*"],
 10 | 	"@components/*": ["components/*"],
 11 | 	"@utils/helpers": ["utils/helpers.ts"],
 12 | };
 13 | 
 14 | const setupProject = () => {
 15 | 	const project = new Project({
 16 | 		useInMemoryFileSystem: true,
 17 | 		compilerOptions: {
 18 | 			baseUrl: path.relative(path.dirname(TEST_TSCONFIG_PATH), TEST_BASE_URL),
 19 | 			paths: TEST_PATHS,
 20 | 			allowJs: true,
 21 | 		},
 22 | 	});
 23 | 	project.createSourceFile(
 24 | 		TEST_TSCONFIG_PATH,
 25 | 		JSON.stringify({
 26 | 			compilerOptions: { baseUrl: "./src", paths: TEST_PATHS },
 27 | 		}),
 28 | 	);
 29 | 	return project;
 30 | };
 31 | 
 32 | describe("removePathAlias", () => {
 33 | 	it("単純なワイルドカードエイリアス (@/*) を相対パスに変換できること", async () => {
 34 | 		const project = setupProject();
 35 | 		const importerPath = "/src/features/featureA/index.ts";
 36 | 		const componentPath = "/src/components/Button.ts";
 37 | 		project.createSourceFile(componentPath, "export const Button = {};");
 38 | 		const importerContent = `import { Button } from '@/components/Button';`;
 39 | 		project.createSourceFile(importerPath, importerContent);
 40 | 
 41 | 		const result = await removePathAlias({
 42 | 			project,
 43 | 			targetPath: importerPath,
 44 | 			baseUrl: TEST_BASE_URL,
 45 | 			paths: TEST_PATHS,
 46 | 			dryRun: false,
 47 | 		});
 48 | 
 49 | 		const sourceFile = project.getSourceFileOrThrow(importerPath);
 50 | 		const importDeclaration = sourceFile.getImportDeclarations()[0];
 51 | 		expect(importDeclaration?.getModuleSpecifierValue()).toBe(
 52 | 			"../../components/Button",
 53 | 		);
 54 | 		expect(result.changedFiles).toEqual([importerPath]);
 55 | 	});
 56 | 
 57 | 	it("特定のパスエイリアス (@components/*) を相対パスに変換できること", async () => {
 58 | 		const project = setupProject();
 59 | 		const importerPath = "/src/index.ts";
 60 | 		const componentPath = "/src/components/Input/index.ts";
 61 | 		project.createSourceFile(componentPath, "export const Input = {};");
 62 | 		const importerContent = `import { Input } from '@components/Input';`;
 63 | 		project.createSourceFile(importerPath, importerContent);
 64 | 
 65 | 		const result = await removePathAlias({
 66 | 			project,
 67 | 			targetPath: importerPath,
 68 | 			baseUrl: TEST_BASE_URL,
 69 | 			paths: TEST_PATHS,
 70 | 			dryRun: false,
 71 | 		});
 72 | 
 73 | 		const sourceFile = project.getSourceFileOrThrow(importerPath);
 74 | 		expect(
 75 | 			sourceFile.getImportDeclarations()[0]?.getModuleSpecifierValue(),
 76 | 		).toBe("./components/Input/index");
 77 | 		expect(result.changedFiles).toEqual([importerPath]);
 78 | 	});
 79 | 
 80 | 	it("ファイルへの直接エイリアス (@utils/helpers) を相対パスに変換できること", async () => {
 81 | 		const project = setupProject();
 82 | 		const importerPath = "/src/features/featureB/utils.ts";
 83 | 		const helperPath = "/src/utils/helpers.ts";
 84 | 		project.createSourceFile(helperPath, "export const helperFunc = () => {};");
 85 | 		const importerContent = `import { helperFunc } from '@utils/helpers';`;
 86 | 		project.createSourceFile(importerPath, importerContent);
 87 | 
 88 | 		const result = await removePathAlias({
 89 | 			project,
 90 | 			targetPath: importerPath,
 91 | 			baseUrl: TEST_BASE_URL,
 92 | 			paths: TEST_PATHS,
 93 | 			dryRun: false,
 94 | 		});
 95 | 
 96 | 		const sourceFile = project.getSourceFileOrThrow(importerPath);
 97 | 		expect(
 98 | 			sourceFile.getImportDeclarations()[0]?.getModuleSpecifierValue(),
 99 | 		).toBe("../../utils/helpers");
100 | 		expect(result.changedFiles).toEqual([importerPath]);
101 | 	});
102 | 
103 | 	it("エイリアスでない通常の相対パスは変更しないこと", async () => {
104 | 		const project = setupProject();
105 | 		const importerPath = "/src/features/featureA/index.ts";
106 | 		const servicePath = "/src/features/featureA/service.ts";
107 | 		project.createSourceFile(servicePath, "export class Service {}");
108 | 		const importerContent = `import { Service } from './service';`;
109 | 		const sourceFile = project.createSourceFile(importerPath, importerContent);
110 | 		const originalContent = sourceFile.getFullText();
111 | 
112 | 		const result = await removePathAlias({
113 | 			project,
114 | 			targetPath: importerPath,
115 | 			baseUrl: TEST_BASE_URL,
116 | 			paths: TEST_PATHS,
117 | 			dryRun: false,
118 | 		});
119 | 
120 | 		expect(sourceFile.getFullText()).toBe(originalContent);
121 | 		expect(result.changedFiles).toEqual([]);
122 | 	});
123 | 
124 | 	it("エイリアスでない node_modules パスは変更しないこと", async () => {
125 | 		const project = setupProject();
126 | 		const importerPath = "/src/index.ts";
127 | 		const importerContent = `import * as fs from 'fs';`;
128 | 		const sourceFile = project.createSourceFile(importerPath, importerContent);
129 | 		const originalContent = sourceFile.getFullText();
130 | 
131 | 		const result = await removePathAlias({
132 | 			project,
133 | 			targetPath: importerPath,
134 | 			baseUrl: TEST_BASE_URL,
135 | 			paths: TEST_PATHS,
136 | 			dryRun: false,
137 | 		});
138 | 
139 | 		expect(sourceFile.getFullText()).toBe(originalContent);
140 | 		expect(result.changedFiles).toEqual([]);
141 | 	});
142 | 
143 | 	it("dryRun モードではファイルを変更せず、変更予定リストを返すこと", async () => {
144 | 		const project = setupProject();
145 | 		const importerPath = "/src/features/featureA/index.ts";
146 | 		const componentPath = "/src/components/Button.ts";
147 | 		project.createSourceFile(componentPath, "export const Button = {};");
148 | 		const importerContent = `import { Button } from '@/components/Button';`;
149 | 		const sourceFile = project.createSourceFile(importerPath, importerContent);
150 | 		const originalContent = sourceFile.getFullText();
151 | 
152 | 		const result = await removePathAlias({
153 | 			project,
154 | 			targetPath: importerPath,
155 | 			baseUrl: TEST_BASE_URL,
156 | 			paths: TEST_PATHS,
157 | 			dryRun: true,
158 | 		});
159 | 
160 | 		expect(sourceFile.getFullText()).toBe(originalContent);
161 | 		expect(result.changedFiles).toEqual([importerPath]);
162 | 	});
163 | 
164 | 	it("ディレクトリを対象とした場合に、内部の複数ファイルのエイリアスを変換できること", async () => {
165 | 		const project = setupProject();
166 | 		const dirPath = "/src/features/multi";
167 | 		const file1Path = path.join(dirPath, "file1.ts");
168 | 		const file2Path = path.join(dirPath, "sub/file2.ts");
169 | 		const buttonPath = "/src/components/Button.ts";
170 | 		const inputPath = "/src/components/Input.ts";
171 | 
172 | 		project.createSourceFile(buttonPath, "export const Button = {};");
173 | 		project.createSourceFile(inputPath, "export const Input = {};");
174 | 		project.createSourceFile(
175 | 			file1Path,
176 | 			"import { Button } from '@/components/Button';",
177 | 		);
178 | 		project.createSourceFile(
179 | 			file2Path,
180 | 			"import { Input } from '@components/Input';",
181 | 		);
182 | 
183 | 		const result = await removePathAlias({
184 | 			project,
185 | 			targetPath: dirPath,
186 | 			baseUrl: TEST_BASE_URL,
187 | 			paths: TEST_PATHS,
188 | 			dryRun: false,
189 | 		});
190 | 
191 | 		const file1 = project.getSourceFileOrThrow(file1Path);
192 | 		const file2 = project.getSourceFileOrThrow(file2Path);
193 | 
194 | 		expect(file1.getImportDeclarations()[0]?.getModuleSpecifierValue()).toBe(
195 | 			"../../components/Button",
196 | 		);
197 | 		expect(file2.getImportDeclarations()[0]?.getModuleSpecifierValue()).toBe(
198 | 			"../../../components/Input",
199 | 		);
200 | 		expect(result.changedFiles.sort()).toEqual([file1Path, file2Path].sort());
201 | 	});
202 | 
203 | 	it("解決できないエイリアスパスを変更しないこと", async () => {
204 | 		const project = setupProject();
205 | 		const importerPath = "/src/index.ts";
206 | 		const importerContent = `import { Something } from '@unknown/package';`;
207 | 		const sourceFile = project.createSourceFile(importerPath, importerContent);
208 | 		const originalContent = sourceFile.getFullText();
209 | 
210 | 		const result = await removePathAlias({
211 | 			project,
212 | 			targetPath: importerPath,
213 | 			baseUrl: TEST_BASE_URL,
214 | 			paths: TEST_PATHS,
215 | 			dryRun: false,
216 | 		});
217 | 
218 | 		expect(sourceFile.getFullText()).toBe(originalContent);
219 | 		expect(result.changedFiles).toEqual([]);
220 | 	});
221 | 
222 | 	it("エイリアスが index.ts を指す場合、結果は /index で終わる (省略されない)", async () => {
223 | 		const project = setupProject();
224 | 		const importerPath = "/src/features/featureA/component.ts";
225 | 		const indexPath = "/src/components/index.ts";
226 | 
227 | 		project.createSourceFile(indexPath, "export const CompIndex = 1;");
228 | 		project.createSourceFile(
229 | 			importerPath,
230 | 			"import { CompIndex } from '@/components';",
231 | 		);
232 | 
233 | 		const result = await removePathAlias({
234 | 			project,
235 | 			targetPath: importerPath,
236 | 			baseUrl: "/",
237 | 			paths: { "@/*": ["src/*"] },
238 | 			dryRun: false,
239 | 		});
240 | 
241 | 		const sourceFile = project.getSourceFileOrThrow(importerPath);
242 | 		expect(
243 | 			sourceFile.getImportDeclarations()[0]?.getModuleSpecifierValue(),
244 | 		).toBe("../../components/index");
245 | 		expect(result.changedFiles).toEqual([importerPath]);
246 | 	});
247 | 
248 | 	it("エイリアスが .js ファイルを指す場合、結果から拡張子は削除される", async () => {
249 | 		const project = setupProject();
250 | 		const importerPath = "/src/app.ts";
251 | 		const jsPath = "/src/utils/legacy.js";
252 | 
253 | 		project.createSourceFile(jsPath, "export const legacyFunc = () => {};");
254 | 		project.createSourceFile(
255 | 			importerPath,
256 | 			"import { legacyFunc } from '@/utils/legacy.js';",
257 | 		);
258 | 
259 | 		const result = await removePathAlias({
260 | 			project,
261 | 			targetPath: importerPath,
262 | 			baseUrl: "/",
263 | 			paths: { "@/*": ["src/*"] },
264 | 			dryRun: false,
265 | 		});
266 | 
267 | 		const sourceFile = project.getSourceFileOrThrow(importerPath);
268 | 		expect(
269 | 			sourceFile.getImportDeclarations()[0]?.getModuleSpecifierValue(),
270 | 		).toBe("./utils/legacy");
271 | 		expect(result.changedFiles).toEqual([importerPath]);
272 | 	});
273 | });
274 | 
```

--------------------------------------------------------------------------------
/src/ts-morph/move-symbol-to-file/internal-dependencies.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect } from "vitest";
  2 | import {
  3 | 	type FunctionDeclaration,
  4 | 	Project,
  5 | 	SyntaxKind,
  6 | 	type VariableStatement,
  7 | } from "ts-morph";
  8 | import { findTopLevelDeclarationByName } from "./find-declaration";
  9 | import { getInternalDependencies } from "./internal-dependencies";
 10 | // --- Test Setup Helper ---
 11 | const setupProject = () => {
 12 | 	const project = new Project({
 13 | 		useInMemoryFileSystem: true,
 14 | 		compilerOptions: { target: 99, module: 99 },
 15 | 	});
 16 | 	project.createDirectory("/src");
 17 | 	return project;
 18 | };
 19 | 
 20 | describe("getInternalDependencies", () => {
 21 | 	it("関数宣言が依存する内部関数と内部変数を特定できる", () => {
 22 | 		const project = setupProject();
 23 | 		const filePath = "/src/internal-deps-advanced.ts";
 24 | 		const sourceFile = project.createSourceFile(
 25 | 			filePath,
 26 | 			`
 27 | 			const configValue = 10;
 28 | 			const calculatedValue = configValue * 2;
 29 | 			function helperFunc(n: number): number { return n + calculatedValue; }
 30 | 			export function mainFunc(x: number): void { const result = helperFunc(x); console.log(result); }
 31 | 		`,
 32 | 		);
 33 | 		const mainFuncDecl = findTopLevelDeclarationByName(
 34 | 			sourceFile,
 35 | 			"mainFunc",
 36 | 			SyntaxKind.FunctionDeclaration,
 37 | 		) as FunctionDeclaration;
 38 | 		const helperFuncDecl = findTopLevelDeclarationByName(
 39 | 			sourceFile,
 40 | 			"helperFunc",
 41 | 			SyntaxKind.FunctionDeclaration,
 42 | 		) as FunctionDeclaration;
 43 | 		const calculatedValueStmt = findTopLevelDeclarationByName(
 44 | 			sourceFile,
 45 | 			"calculatedValue",
 46 | 			SyntaxKind.VariableStatement,
 47 | 		) as VariableStatement;
 48 | 		const configValueStmt = findTopLevelDeclarationByName(
 49 | 			sourceFile,
 50 | 			"configValue",
 51 | 			SyntaxKind.VariableStatement,
 52 | 		) as VariableStatement;
 53 | 
 54 | 		expect(mainFuncDecl).toBeDefined();
 55 | 		expect(helperFuncDecl).toBeDefined();
 56 | 		expect(calculatedValueStmt).toBeDefined();
 57 | 		expect(configValueStmt).toBeDefined();
 58 | 
 59 | 		const dependencies = getInternalDependencies(mainFuncDecl);
 60 | 
 61 | 		expect(dependencies).toBeInstanceOf(Array);
 62 | 		expect(dependencies).toHaveLength(3); // helperFunc, calculatedValue, configValue
 63 | 		expect(dependencies).toEqual(
 64 | 			expect.arrayContaining([
 65 | 				helperFuncDecl,
 66 | 				calculatedValueStmt,
 67 | 				configValueStmt,
 68 | 			]),
 69 | 		);
 70 | 	});
 71 | 
 72 | 	it("関数宣言が依存する内部変数を特定できる (間接依存)", () => {
 73 | 		const project = setupProject();
 74 | 		const filePath = "/src/internal-deps-advanced.ts";
 75 | 		const sourceFile = project.createSourceFile(
 76 | 			filePath,
 77 | 			`
 78 | 			const configValue = 10; // <- さらに依存
 79 | 			const calculatedValue = configValue * 2; // <- 依存先
 80 | 			function helperFunc(n: number): number { return n + calculatedValue; } // <- これを対象
 81 | 		`,
 82 | 		);
 83 | 		const helperFuncDecl = findTopLevelDeclarationByName(
 84 | 			sourceFile,
 85 | 			"helperFunc",
 86 | 			SyntaxKind.FunctionDeclaration,
 87 | 		) as FunctionDeclaration;
 88 | 		const calculatedValueStmt = findTopLevelDeclarationByName(
 89 | 			sourceFile,
 90 | 			"calculatedValue",
 91 | 			SyntaxKind.VariableStatement,
 92 | 		) as VariableStatement;
 93 | 		const configValueStmt = findTopLevelDeclarationByName(
 94 | 			sourceFile,
 95 | 			"configValue",
 96 | 			SyntaxKind.VariableStatement,
 97 | 		) as VariableStatement;
 98 | 
 99 | 		expect(helperFuncDecl).toBeDefined();
100 | 		expect(calculatedValueStmt).toBeDefined();
101 | 		expect(configValueStmt).toBeDefined();
102 | 
103 | 		const dependencies = getInternalDependencies(helperFuncDecl);
104 | 
105 | 		expect(dependencies).toBeInstanceOf(Array);
106 | 		expect(dependencies).toHaveLength(2); // calculatedValue, configValue
107 | 		expect(dependencies).toEqual(
108 | 			expect.arrayContaining([calculatedValueStmt, configValueStmt]),
109 | 		);
110 | 	});
111 | 
112 | 	it("変数宣言が依存する内部変数を特定できる", () => {
113 | 		const project = setupProject();
114 | 		const filePath = "/src/internal-deps-advanced.ts";
115 | 		const sourceFile = project.createSourceFile(
116 | 			filePath,
117 | 			`
118 | 			const configValue = 10; // <- さらに依存
119 | 			const calculatedValue = configValue * 2; // <- 依存先
120 | 			export const derivedConst = calculatedValue + 5; // <- これを対象
121 | 		`,
122 | 		);
123 | 		const derivedConstStmt = findTopLevelDeclarationByName(
124 | 			sourceFile,
125 | 			"derivedConst",
126 | 			SyntaxKind.VariableStatement,
127 | 		) as VariableStatement;
128 | 		const calculatedValueStmt = findTopLevelDeclarationByName(
129 | 			sourceFile,
130 | 			"calculatedValue",
131 | 			SyntaxKind.VariableStatement,
132 | 		) as VariableStatement;
133 | 		const configValueStmt = findTopLevelDeclarationByName(
134 | 			sourceFile,
135 | 			"configValue",
136 | 			SyntaxKind.VariableStatement,
137 | 		) as VariableStatement;
138 | 
139 | 		expect(derivedConstStmt).toBeDefined();
140 | 		expect(calculatedValueStmt).toBeDefined();
141 | 		expect(configValueStmt).toBeDefined();
142 | 
143 | 		const dependencies = getInternalDependencies(derivedConstStmt);
144 | 
145 | 		expect(dependencies).toBeInstanceOf(Array);
146 | 		expect(dependencies).toHaveLength(2); // calculatedValue, configValue
147 | 		expect(dependencies).toEqual(
148 | 			expect.arrayContaining([calculatedValueStmt, configValueStmt]),
149 | 		);
150 | 	});
151 | 
152 | 	it("変数宣言が依存する内部変数を特定できる (直接依存)", () => {
153 | 		const project = setupProject();
154 | 		const filePath = "/src/internal-deps-advanced.ts";
155 | 		const sourceFile = project.createSourceFile(
156 | 			filePath,
157 | 			`
158 | 				const configValue = 10; // <- 依存先
159 | 				const calculatedValue = configValue * 2; // <- これを対象
160 | 			`,
161 | 		);
162 | 		const calculatedValueStmt = findTopLevelDeclarationByName(
163 | 			sourceFile,
164 | 			"calculatedValue",
165 | 			SyntaxKind.VariableStatement,
166 | 		) as VariableStatement;
167 | 		const configValueStmt = findTopLevelDeclarationByName(
168 | 			sourceFile,
169 | 			"configValue",
170 | 			SyntaxKind.VariableStatement,
171 | 		) as VariableStatement;
172 | 
173 | 		expect(
174 | 			calculatedValueStmt,
175 | 			"Test setup failed: calculatedValue not found",
176 | 		).toBeDefined();
177 | 		expect(
178 | 			configValueStmt,
179 | 			"Test setup failed: configValue not found",
180 | 		).toBeDefined();
181 | 
182 | 		const dependencies = getInternalDependencies(calculatedValueStmt);
183 | 
184 | 		expect(dependencies).toBeInstanceOf(Array);
185 | 		expect(dependencies).toHaveLength(1);
186 | 		expect(dependencies[0]).toBe(configValueStmt);
187 | 	});
188 | 
189 | 	it("依存関係がない場合は空配列を返す", () => {
190 | 		const project = setupProject();
191 | 		const filePath = "/src/internal-deps-advanced.ts";
192 | 		const sourceFile = project.createSourceFile(
193 | 			filePath,
194 | 			`
195 | 			const configValue = 10;
196 | 			function unusedFunc() {}
197 | 		`,
198 | 		);
199 | 		const configValueStmt = findTopLevelDeclarationByName(
200 | 			sourceFile,
201 | 			"configValue",
202 | 			SyntaxKind.VariableStatement,
203 | 		) as VariableStatement;
204 | 		const unusedFuncDecl = findTopLevelDeclarationByName(
205 | 			sourceFile,
206 | 			"unusedFunc",
207 | 			SyntaxKind.FunctionDeclaration,
208 | 		) as FunctionDeclaration;
209 | 
210 | 		expect(
211 | 			configValueStmt,
212 | 			"Test setup failed: configValue not found",
213 | 		).toBeDefined();
214 | 		expect(
215 | 			unusedFuncDecl,
216 | 			"Test setup failed: unusedFunc not found",
217 | 		).toBeDefined();
218 | 
219 | 		const configDeps = getInternalDependencies(configValueStmt);
220 | 		const unusedDeps = getInternalDependencies(unusedFuncDecl);
221 | 
222 | 		expect(configDeps).toEqual([]);
223 | 		expect(unusedDeps).toEqual([]);
224 | 	});
225 | 
226 | 	it("関数宣言が依存する非エクスポートのアロー関数を特定できる", () => {
227 | 		const project = setupProject();
228 | 		const filePath = "/src/arrow-func-dep.ts";
229 | 		const sourceFile = project.createSourceFile(
230 | 			filePath,
231 | 			`
232 | 			const arrowHelper = (n: number): number => n * n;
233 | 			export function mainFunc(x: number): number { return arrowHelper(x); }
234 | 		`,
235 | 		);
236 | 		const mainFuncDecl = findTopLevelDeclarationByName(
237 | 			sourceFile,
238 | 			"mainFunc",
239 | 			SyntaxKind.FunctionDeclaration,
240 | 		) as FunctionDeclaration;
241 | 		const arrowHelperStmt = findTopLevelDeclarationByName(
242 | 			sourceFile,
243 | 			"arrowHelper",
244 | 			SyntaxKind.VariableStatement,
245 | 		) as VariableStatement;
246 | 
247 | 		expect(mainFuncDecl).toBeDefined();
248 | 		expect(arrowHelperStmt).toBeDefined();
249 | 
250 | 		const dependencies = getInternalDependencies(mainFuncDecl);
251 | 
252 | 		expect(dependencies.length).toBe(1);
253 | 		expect(dependencies[0]).toBe(arrowHelperStmt);
254 | 	});
255 | 
256 | 	it("複数の間接的な内部依存関係を再帰的に特定できる", () => {
257 | 		const project = setupProject();
258 | 		const filePath = "/src/recursive-deps.ts";
259 | 		const sourceFile = project.createSourceFile(
260 | 			filePath,
261 | 			`
262 | 			const d = 4;
263 | 			const c = () => d;
264 | 			const b = () => c();
265 | 			export const a = () => b(); // a -> b -> c -> d
266 | 			const e = () => d; // d は a 以外からも参照されるが、ここでは a の依存のみ見る
267 | 		`,
268 | 		);
269 | 		const aStmt = findTopLevelDeclarationByName(
270 | 			sourceFile,
271 | 			"a",
272 | 			SyntaxKind.VariableStatement,
273 | 		) as VariableStatement;
274 | 		const bStmt = findTopLevelDeclarationByName(
275 | 			sourceFile,
276 | 			"b",
277 | 			SyntaxKind.VariableStatement,
278 | 		) as VariableStatement;
279 | 		const cStmt = findTopLevelDeclarationByName(
280 | 			sourceFile,
281 | 			"c",
282 | 			SyntaxKind.VariableStatement,
283 | 		) as VariableStatement;
284 | 		const dStmt = findTopLevelDeclarationByName(
285 | 			sourceFile,
286 | 			"d",
287 | 			SyntaxKind.VariableStatement,
288 | 		) as VariableStatement;
289 | 
290 | 		expect(aStmt).toBeDefined();
291 | 		expect(bStmt).toBeDefined();
292 | 		expect(cStmt).toBeDefined();
293 | 		expect(dStmt).toBeDefined();
294 | 
295 | 		const dependencies = getInternalDependencies(aStmt);
296 | 
297 | 		expect(dependencies).toBeInstanceOf(Array);
298 | 		expect(dependencies).toHaveLength(3); // b, c, d が含まれるはず
299 | 		expect(dependencies).toEqual(expect.arrayContaining([bStmt, cStmt, dStmt]));
300 | 	});
301 | });
302 | 
```

--------------------------------------------------------------------------------
/src/mcp/tools/register-rename-file-system-entry-tool.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
  2 | import { z } from "zod";
  3 | import { renameFileSystemEntry } from "../../ts-morph/rename-file-system/rename-file-system-entry";
  4 | import { initializeProject } from "../../ts-morph/_utils/ts-morph-project";
  5 | import * as path from "node:path";
  6 | import { performance } from "node:perf_hooks";
  7 | import { TimeoutError } from "../../errors/timeout-error";
  8 | import logger from "../../utils/logger";
  9 | 
 10 | const renameSchema = z.object({
 11 | 	tsconfigPath: z
 12 | 		.string()
 13 | 		.describe("Absolute path to the project's tsconfig.json file."),
 14 | 	renames: z
 15 | 		.array(
 16 | 			z.object({
 17 | 				oldPath: z
 18 | 					.string()
 19 | 					.describe(
 20 | 						"The current absolute path of the file or folder to rename.",
 21 | 					),
 22 | 				newPath: z
 23 | 					.string()
 24 | 					.describe("The new desired absolute path for the file or folder."),
 25 | 			}),
 26 | 		)
 27 | 		.nonempty()
 28 | 		.describe("An array of rename operations, each with oldPath and newPath."),
 29 | 	dryRun: z
 30 | 		.boolean()
 31 | 		.optional()
 32 | 		.default(false)
 33 | 		.describe("If true, only show intended changes without modifying files."),
 34 | 	timeoutSeconds: z
 35 | 		.number()
 36 | 		.int()
 37 | 		.positive()
 38 | 		.optional()
 39 | 		.default(120)
 40 | 		.describe(
 41 | 			"Maximum time in seconds allowed for the operation before it times out. Defaults to 120.",
 42 | 		),
 43 | });
 44 | 
 45 | type RenameArgs = z.infer<typeof renameSchema>;
 46 | 
 47 | export function registerRenameFileSystemEntryTool(server: McpServer): void {
 48 | 	server.tool(
 49 | 		"rename_filesystem_entry_by_tsmorph",
 50 | 		`[Uses ts-morph] Renames **one or more** TypeScript/JavaScript files **and/or folders** and updates all import/export paths referencing them throughout the project.
 51 | 
 52 | Analyzes the project based on \`tsconfig.json\` to find all references to the items being renamed and automatically corrects their paths. **Handles various path types, including relative paths, path aliases (e.g., @/), and imports referencing a directory\'s index.ts (\`from \'.\'\` or \`from \'..\'\`).** Checks for conflicts before applying changes.
 53 | 
 54 | ## Usage
 55 | 
 56 | Use this tool when you want to rename/move multiple files or folders simultaneously (e.g., renaming \`util.ts\` to \`helper.ts\` and moving \`src/data\` to \`src/coreData\` in one operation) and need all the \`import\`/\`export\` statements referencing them to be updated automatically.
 57 | 
 58 | 1.  Specify the path to the project's \`tsconfig.json\` file. **Must be an absolute path.**
 59 | 2.  Provide an array of rename operations. Each object in the array must contain:
 60 |     - \`oldPath\`: The current **absolute path** of the file or folder to rename.
 61 |     - \`newPath\`: The new desired **absolute path** for the file or folder.
 62 | 3.  It\'s recommended to first run with \`dryRun: true\` to check which files will be affected.
 63 | 4.  If the preview looks correct, run with \`dryRun: false\` (or omit it) to actually save the changes to the file system.
 64 | 
 65 | ## Parameters
 66 | 
 67 | - tsconfigPath (string, required): Absolute path to the project's root \`tsconfig.json\` file. **Must be an absolute path.**
 68 | - renames (array of objects, required): An array where each object specifies a rename operation with:
 69 |     - oldPath (string, required): The current absolute path of the file or folder. **Must be an absolute path.**
 70 |     - newPath (string, required): The new desired absolute path for the file or folder. **Must be an absolute path.**
 71 | - dryRun (boolean, optional): If set to true, prevents making and saving file changes, returning only the list of files that would be affected. Defaults to false.
 72 | - timeoutSeconds (number, optional): Maximum time in seconds allowed for the operation before it times out. Defaults to 120 seconds.
 73 | 
 74 | ## Result
 75 | 
 76 | - On success: Returns a message listing the file paths modified or scheduled to be modified.
 77 | - On failure: Returns a message indicating the error (e.g., path conflict, file not found, timeout).
 78 | 
 79 | ## Remarks
 80 | - **Symbol-based Reference Finding:** This tool now primarily uses symbol analysis (identifying exported functions, classes, variables, etc.) to find references across the project, rather than solely relying on path matching.
 81 | - **Path Alias Handling:** Path aliases (e.g., \`@/\`) in import/export statements *are* updated, but they will be **converted to relative paths**. If preserving path aliases is crucial, consider using the \`remove_path_alias_by_tsmorph\` tool *before* renaming to convert them to relative paths preemptively.
 82 | - **Index File Imports:** Imports referencing a directory's \`index.ts\` or \`index.tsx\` (e.g., \`import Component from '../components'\`) will be updated to reference the specific index file directly (e.g., \`import Component from '../components/index.tsx'\`).
 83 | - **Known Limitation (Default Exports):** Currently, this tool may not correctly update references for default exports declared using an identifier (e.g., \`export default MyIdentifier;\`). Default exports using function or class declarations (e.g., \`export default function myFunction() {}\`) are generally handled.
 84 | - **Performance:** Renaming numerous files/folders or operating in a very large project can take significant time due to the detailed symbol analysis and reference updates.
 85 | - **Conflicts:** The tool checks for conflicts (e.g., renaming to an existing path, duplicate targets) before applying changes.
 86 | - **Timeout:** Operations exceeding the specified \`timeoutSeconds\` will be canceled.`,
 87 | 		renameSchema.shape,
 88 | 		async (args: RenameArgs) => {
 89 | 			const startTime = performance.now();
 90 | 			let message = "";
 91 | 			let isError = false;
 92 | 			let changedFilesCount = 0;
 93 | 			const { tsconfigPath, renames, dryRun, timeoutSeconds } = args;
 94 | 			const TIMEOUT_MS = timeoutSeconds * 1000;
 95 | 
 96 | 			let resultPayload: {
 97 | 				content: { type: "text"; text: string }[];
 98 | 				isError: boolean;
 99 | 			} = {
100 | 				content: [{ type: "text", text: "An unexpected error occurred." }],
101 | 				isError: true,
102 | 			};
103 | 
104 | 			const controller = new AbortController();
105 | 			let timeoutId: NodeJS.Timeout | undefined = undefined;
106 | 			const logArgs = {
107 | 				tsconfigPath,
108 | 				renames: renames.map((r) => ({
109 | 					old: path.basename(r.oldPath),
110 | 					new: path.basename(r.newPath),
111 | 				})),
112 | 				dryRun,
113 | 				timeoutSeconds,
114 | 			};
115 | 
116 | 			try {
117 | 				timeoutId = setTimeout(() => {
118 | 					const errorMessage = `Operation timed out after ${timeoutSeconds} seconds`;
119 | 					logger.error(
120 | 						{ toolArgs: logArgs, durationSeconds: timeoutSeconds },
121 | 						errorMessage,
122 | 					);
123 | 					controller.abort(new TimeoutError(errorMessage, timeoutSeconds));
124 | 				}, TIMEOUT_MS);
125 | 
126 | 				const project = initializeProject(tsconfigPath);
127 | 				const result = await renameFileSystemEntry({
128 | 					project,
129 | 					renames,
130 | 					dryRun,
131 | 					signal: controller.signal,
132 | 				});
133 | 
134 | 				changedFilesCount = result.changedFiles.length;
135 | 
136 | 				const changedFilesList =
137 | 					result.changedFiles.length > 0
138 | 						? result.changedFiles.join("\n - ")
139 | 						: "(No changes)";
140 | 				const renameSummary = renames
141 | 					.map(
142 | 						(r) =>
143 | 							`'${path.basename(r.oldPath)}' -> '${path.basename(r.newPath)}'`,
144 | 					)
145 | 					.join(", ");
146 | 
147 | 				if (dryRun) {
148 | 					message = `Dry run complete: Renaming [${renameSummary}] would modify the following files:\n - ${changedFilesList}`;
149 | 				} else {
150 | 					message = `Rename successful: Renamed [${renameSummary}]. The following files were modified:\n - ${changedFilesList}`;
151 | 				}
152 | 				isError = false;
153 | 			} catch (error) {
154 | 				logger.error(
155 | 					{ err: error, toolArgs: logArgs },
156 | 					"Error executing rename_filesystem_entry_by_tsmorph",
157 | 				);
158 | 
159 | 				if (error instanceof TimeoutError) {
160 | 					message = `処理が ${error.durationSeconds} 秒以内に完了しなかったため、タイムアウトしました。操作はキャンセルされました.\nプロジェクトの規模が大きいか、変更箇所が多い可能性があります.`;
161 | 				} else if (error instanceof Error && error.name === "AbortError") {
162 | 					message = `操作がキャンセルされました: ${error.message}`;
163 | 				} else {
164 | 					const errorMessage =
165 | 						error instanceof Error ? error.message : String(error);
166 | 					message = `Error during rename process: ${errorMessage}`;
167 | 				}
168 | 				isError = true;
169 | 			} finally {
170 | 				if (timeoutId) {
171 | 					clearTimeout(timeoutId);
172 | 				}
173 | 				const endTime = performance.now();
174 | 				const durationMs = endTime - startTime;
175 | 
176 | 				logger.info(
177 | 					{
178 | 						status: isError ? "Failure" : "Success",
179 | 						durationMs: Number.parseFloat(durationMs.toFixed(2)),
180 | 						changedFilesCount,
181 | 						dryRun,
182 | 					},
183 | 					"rename_filesystem_entry_by_tsmorph tool finished",
184 | 				);
185 | 				try {
186 | 					logger.flush();
187 | 					logger.trace("Logs flushed after tool execution.");
188 | 				} catch (flushErr) {
189 | 					console.error("Failed to flush logs:", flushErr);
190 | 				}
191 | 			}
192 | 
193 | 			const endTime = performance.now();
194 | 			const durationMs = endTime - startTime;
195 | 			const durationSec = (durationMs / 1000).toFixed(2);
196 | 			const finalMessage = `${message}\nStatus: ${isError ? "Failure" : "Success"}\nProcessing time: ${durationSec} seconds`;
197 | 			resultPayload = {
198 | 				content: [{ type: "text", text: finalMessage }],
199 | 				isError: isError,
200 | 			};
201 | 
202 | 			return resultPayload;
203 | 		},
204 | 	);
205 | }
206 | 
```

--------------------------------------------------------------------------------
/src/mcp/tools/register-move-symbol-to-file-tool.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
  2 | import { z } from "zod";
  3 | import { moveSymbolToFile } from "../../ts-morph/move-symbol-to-file/move-symbol-to-file";
  4 | import { initializeProject } from "../../ts-morph/_utils/ts-morph-project";
  5 | import { getChangedFiles } from "../../ts-morph/_utils/ts-morph-project";
  6 | import { SyntaxKind } from "ts-morph";
  7 | import { performance } from "node:perf_hooks";
  8 | import logger from "../../utils/logger";
  9 | import * as path from "node:path";
 10 | 
 11 | const syntaxKindMapping: { [key: string]: SyntaxKind } = {
 12 | 	FunctionDeclaration: SyntaxKind.FunctionDeclaration,
 13 | 	VariableStatement: SyntaxKind.VariableStatement,
 14 | 	ClassDeclaration: SyntaxKind.ClassDeclaration,
 15 | 	InterfaceDeclaration: SyntaxKind.InterfaceDeclaration,
 16 | 	TypeAliasDeclaration: SyntaxKind.TypeAliasDeclaration,
 17 | 	EnumDeclaration: SyntaxKind.EnumDeclaration,
 18 | };
 19 | const moveSymbolSchema = z.object({
 20 | 	tsconfigPath: z
 21 | 		.string()
 22 | 		.describe(
 23 | 			"Absolute path to the project's tsconfig.json file. Essential for ts-morph.",
 24 | 		),
 25 | 	originalFilePath: z
 26 | 		.string()
 27 | 		.describe("Absolute path to the file containing the symbol to move."),
 28 | 	targetFilePath: z
 29 | 		.string()
 30 | 		.describe(
 31 | 			"Absolute path to the destination file. Can be an existing file; if the path does not exist, a new file will be created.",
 32 | 		),
 33 | 	symbolToMove: z.string().describe("The name of the symbol to move."),
 34 | 	declarationKindString: z
 35 | 		.string()
 36 | 		.optional()
 37 | 		.describe(
 38 | 			"Optional. The kind of the declaration as a string (e.g., 'VariableStatement', 'FunctionDeclaration', 'ClassDeclaration', 'InterfaceDeclaration', 'TypeAliasDeclaration', 'EnumDeclaration'). Providing this helps resolve ambiguity if multiple symbols share the same name.",
 39 | 		),
 40 | 	dryRun: z
 41 | 		.boolean()
 42 | 		.optional()
 43 | 		.default(false)
 44 | 		.describe("If true, only show intended changes without modifying files."),
 45 | });
 46 | 
 47 | type MoveSymbolArgs = z.infer<typeof moveSymbolSchema>;
 48 | 
 49 | /**
 50 |  * MCPサーバーに 'move_symbol_to_file_by_tsmorph' ツールを登録します。
 51 |  * このツールは、指定されたシンボルをファイル間で移動し、関連する参照を更新します。
 52 |  *
 53 |  * @param server McpServer インスタンス
 54 |  */
 55 | export function registerMoveSymbolToFileTool(server: McpServer): void {
 56 | 	server.tool(
 57 | 		"move_symbol_to_file_by_tsmorph",
 58 | 		`[Uses ts-morph] Moves a specified symbol (function, variable, class, etc.) and its internal-only dependencies to a new file, automatically updating all references across the project. Aids refactoring tasks like file splitting and improving modularity.
 59 | 
 60 | Analyzes the AST (Abstract Syntax Tree) to identify usages of the symbol and corrects import/export paths based on the new file location. It also handles moving necessary internal dependencies (those used only by the symbol being moved).
 61 | 
 62 | ## Usage
 63 | 
 64 | Use this tool for various code reorganization tasks:
 65 | 
 66 | 1.  **Moving a specific function/class/variable:** Relocate a specific piece of logic to a more appropriate file (e.g., moving a helper function from a general \`utils.ts\` to a feature-specific \`feature-utils.ts\`). **This tool moves the specified symbol and its internal-only dependencies.**
 67 | 2.  **Extracting or Moving related logic (File Splitting/Reorganization):** To split a large file or reorganize logic, move related functions, classes, types, or variables to a **different file (new or existing)** one by one using this tool. **You will need to run this tool multiple times, once for each top-level symbol you want to move.**
 68 | 3.  **Improving modularity:** Group related functionalities together by moving multiple symbols (functions, types, etc.) into separate, more focused files. **Run this tool for each symbol you wish to relocate.**
 69 | 
 70 | ts-morph parses the project based on \`tsconfig.json\` to resolve references and perform the move safely, updating imports/exports automatically.
 71 | 
 72 | ## Parameters
 73 | 
 74 | - tsconfigPath (string, required): Absolute path to the project\'s root \`tsconfig.json\`
 75 | - originalFilePath (string, required): Absolute path to the file currently containing the symbol to move.
 76 | - targetFilePath (string, required): Absolute path to the destination file. Can be an existing file; if the path does not exist, a new file will be created.
 77 | - symbolToMove (string, required): The name of the **single top-level symbol** you want to move in this execution.
 78 | - declarationKindString (string, optional): The kind of the declaration (e.g., \'VariableStatement\', \'FunctionDeclaration\'). Recommended to resolve ambiguity if multiple symbols share the same name.
 79 | - dryRun (boolean, optional): If true, only show intended changes without modifying files. Defaults to false.
 80 | 
 81 | ## Result
 82 | 
 83 | - On success: Returns a message confirming the move and reference updates, including a list of modified files (or files that would be modified if dryRun is true).
 84 | - On failure: Returns an error message (e.g., symbol not found, default export, AST errors).
 85 | 
 86 | ## Remarks
 87 | 
 88 | - **Moves one top-level symbol per execution:** This tool is designed to move a single specified top-level symbol (and its internal-only dependencies) in each run. To move multiple related top-level symbols (e.g., several functions and types for file splitting), you need to invoke this tool multiple times, once for each symbol.
 89 | - **Default exports cannot be moved.**
 90 | - **Internal dependency handling:** Dependencies (functions, variables, types, etc.) used *only* by the moved symbol within the original file are moved along with it. Dependencies that are also used by other symbols remaining in the original file will stay, might gain an \`export\` keyword if they didn't have one, and will be imported by the new file where the symbol was moved. Symbols in the original file that are *not* dependencies of the moved symbol will remain untouched unless explicitly moved in a separate execution of this tool.
 91 | - **Performance:** Moving symbols with many references in large projects might take time.`,
 92 | 		moveSymbolSchema.extend({
 93 | 			symbolToMove: z
 94 | 				.string()
 95 | 				.describe(
 96 | 					"The name of the single top-level symbol you want to move in this execution.",
 97 | 				),
 98 | 		}).shape,
 99 | 		async (args: MoveSymbolArgs) => {
100 | 			const startTime = performance.now();
101 | 			let message = "";
102 | 			let isError = false;
103 | 			let changedFilesCount = 0;
104 | 			let changedFiles: string[] = [];
105 | 			const {
106 | 				tsconfigPath,
107 | 				originalFilePath,
108 | 				targetFilePath,
109 | 				symbolToMove,
110 | 				declarationKindString,
111 | 				dryRun,
112 | 			} = args;
113 | 
114 | 			const declarationKind: SyntaxKind | undefined =
115 | 				declarationKindString && syntaxKindMapping[declarationKindString]
116 | 					? syntaxKindMapping[declarationKindString]
117 | 					: undefined;
118 | 
119 | 			if (declarationKindString && declarationKind === undefined) {
120 | 				logger.warn(
121 | 					`Invalid declarationKindString provided: '${declarationKindString}'. Proceeding without kind specification.`,
122 | 				);
123 | 			}
124 | 
125 | 			const logArgs = {
126 | 				tsconfigPath,
127 | 				originalFilePath: path.basename(originalFilePath),
128 | 				targetFilePath: path.basename(targetFilePath),
129 | 				symbolToMove,
130 | 				declarationKindString,
131 | 				dryRun,
132 | 			};
133 | 
134 | 			try {
135 | 				const project = initializeProject(tsconfigPath);
136 | 				await moveSymbolToFile(
137 | 					project,
138 | 					originalFilePath,
139 | 					targetFilePath,
140 | 					symbolToMove,
141 | 					declarationKind,
142 | 				);
143 | 
144 | 				changedFiles = getChangedFiles(project).map((sf) => sf.getFilePath());
145 | 				changedFilesCount = changedFiles.length;
146 | 
147 | 				const baseMessage = `Moved symbol \"${symbolToMove}\" from ${originalFilePath} to ${targetFilePath}.`;
148 | 				const changedFilesList =
149 | 					changedFiles.length > 0 ? changedFiles.join("\n - ") : "(No changes)";
150 | 
151 | 				if (dryRun) {
152 | 					message = `Dry run: ${baseMessage}\nFiles that would be modified:\n - ${changedFilesList}`;
153 | 					logger.info({ changedFiles }, "Dry run: Skipping save.");
154 | 				} else {
155 | 					await project.save();
156 | 					logger.debug("Project changes saved after symbol move.");
157 | 					message = `${baseMessage}\nThe following files were modified:\n - ${changedFilesList}`;
158 | 				}
159 | 				isError = false;
160 | 			} catch (error) {
161 | 				logger.error(
162 | 					{ err: error, toolArgs: logArgs },
163 | 					"Error executing move_symbol_to_file_by_tsmorph",
164 | 				);
165 | 				const errorMessage =
166 | 					error instanceof Error ? error.message : String(error);
167 | 				message = `Error moving symbol: ${errorMessage}`;
168 | 				isError = true;
169 | 			} finally {
170 | 				const endTime = performance.now();
171 | 				const durationMs = endTime - startTime;
172 | 
173 | 				logger.info(
174 | 					{
175 | 						status: isError ? "Failure" : "Success",
176 | 						durationMs: Number.parseFloat(durationMs.toFixed(2)),
177 | 						changedFilesCount,
178 | 						dryRun,
179 | 					},
180 | 					"move_symbol_to_file_by_tsmorph tool finished",
181 | 				);
182 | 				try {
183 | 					logger.flush();
184 | 				} catch (flushErr) {
185 | 					console.error("Failed to flush logs:", flushErr);
186 | 				}
187 | 			}
188 | 
189 | 			const endTime = performance.now();
190 | 			const durationMs = endTime - startTime;
191 | 			const durationSec = (durationMs / 1000).toFixed(2);
192 | 			const finalMessage = `${message}\nStatus: ${isError ? "Failure" : "Success"}\nProcessing time: ${durationSec} seconds`;
193 | 
194 | 			return {
195 | 				content: [{ type: "text", text: finalMessage }],
196 | 				isError: isError,
197 | 			};
198 | 		},
199 | 	);
200 | }
201 | 
```

--------------------------------------------------------------------------------
/src/ts-morph/move-symbol-to-file/generate-content/generate-new-source-file-content.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect } from "vitest";
  2 | import { Project, SyntaxKind, ts } from "ts-morph";
  3 | import { findTopLevelDeclarationByName } from "../find-declaration";
  4 | import { generateNewSourceFileContent } from "./generate-new-source-file-content";
  5 | import type {
  6 | 	DependencyClassification,
  7 | 	NeededExternalImports,
  8 | } from "../../types";
  9 | 
 10 | // テストプロジェクト設定用ヘルパー
 11 | const setupProjectWithCode = (
 12 | 	code: string,
 13 | 	filePath = "/src/original.ts",
 14 | 	project?: Project,
 15 | ) => {
 16 | 	const proj = project ?? new Project({ useInMemoryFileSystem: true });
 17 | 	proj.compilerOptions.set({ jsx: ts.JsxEmit.ReactJSX });
 18 | 	const originalSourceFile = proj.createSourceFile(filePath, code);
 19 | 	return { project: proj, originalSourceFile };
 20 | };
 21 | 
 22 | describe("generateNewSourceFileContent", () => {
 23 | 	it("依存関係のない VariableDeclaration から新しいファイルの内容を生成できる", () => {
 24 | 		const code = "const myVar = 123;";
 25 | 		const { originalSourceFile } = setupProjectWithCode(code);
 26 | 		const targetSymbolName = "myVar";
 27 | 
 28 | 		const declarationStatement = findTopLevelDeclarationByName(
 29 | 			originalSourceFile,
 30 | 			targetSymbolName,
 31 | 			SyntaxKind.VariableStatement,
 32 | 		);
 33 | 		expect(declarationStatement).toBeDefined();
 34 | 		if (!declarationStatement) return;
 35 | 
 36 | 		const classifiedDependencies: DependencyClassification[] = [];
 37 | 		const neededExternalImports: NeededExternalImports = new Map();
 38 | 
 39 | 		const newFileContent = generateNewSourceFileContent(
 40 | 			declarationStatement,
 41 | 			classifiedDependencies,
 42 | 			originalSourceFile.getFilePath(),
 43 | 			"/src/newLocation.ts",
 44 | 			neededExternalImports,
 45 | 		);
 46 | 
 47 | 		const expectedContent = "export const myVar = 123;\n";
 48 | 		expect(newFileContent.trim()).toBe(expectedContent.trim());
 49 | 	});
 50 | 
 51 | 	it("内部依存関係 (moveToNewFile) を持つ VariableDeclaration から新しいファイル内容を生成できる", () => {
 52 | 		const code = `
 53 | 			function helperFunc(n: number): number {
 54 | 				return n * 2;
 55 | 			}
 56 | 			const myVar = helperFunc(10);
 57 | 		`;
 58 | 		const { originalSourceFile } = setupProjectWithCode(code);
 59 | 		const targetSymbolName = "myVar";
 60 | 		const dependencyName = "helperFunc";
 61 | 
 62 | 		const declarationStatement = findTopLevelDeclarationByName(
 63 | 			originalSourceFile,
 64 | 			targetSymbolName,
 65 | 			SyntaxKind.VariableStatement,
 66 | 		);
 67 | 		const dependencyStatement = findTopLevelDeclarationByName(
 68 | 			originalSourceFile,
 69 | 			dependencyName,
 70 | 			SyntaxKind.FunctionDeclaration,
 71 | 		);
 72 | 
 73 | 		expect(declarationStatement).toBeDefined();
 74 | 		expect(dependencyStatement).toBeDefined();
 75 | 		if (!declarationStatement || !dependencyStatement) return;
 76 | 
 77 | 		const classifiedDependencies: DependencyClassification[] = [
 78 | 			{ type: "moveToNewFile", statement: dependencyStatement },
 79 | 		];
 80 | 		const neededExternalImports: NeededExternalImports = new Map();
 81 | 
 82 | 		const newFileContent = generateNewSourceFileContent(
 83 | 			declarationStatement,
 84 | 			classifiedDependencies,
 85 | 			originalSourceFile.getFilePath(),
 86 | 			"/src/newLocation.ts",
 87 | 			neededExternalImports,
 88 | 		);
 89 | 
 90 | 		const expectedContent = `
 91 | 			/* export なし */ function helperFunc(n: number): number {
 92 | 				return n * 2;
 93 | 			}
 94 | 
 95 | 			export const myVar = helperFunc(10);
 96 | 		`;
 97 | 		const normalize = (str: string) => str.replace(/\s+/g, " ").trim();
 98 | 		expect(normalize(newFileContent)).toBe(
 99 | 			normalize(expectedContent.replace("/* export なし */ ", "")),
100 | 		);
101 | 		expect(newFileContent).not.toContain("export function helperFunc");
102 | 		expect(newFileContent).toContain("function helperFunc");
103 | 	});
104 | 
105 | 	it("外部依存関係 (import) を持つ VariableDeclaration から新しいファイル内容を生成できる", () => {
106 | 		const externalCode =
107 | 			"export function externalFunc(n: number): number { return n + 1; }";
108 | 		const originalCode = `
109 | 			import { externalFunc } from './external';
110 | 			const myVar = externalFunc(99);
111 | 		`;
112 | 		const { project, originalSourceFile } = setupProjectWithCode(
113 | 			originalCode,
114 | 			"/src/moduleA/main.ts",
115 | 		);
116 | 		project.createSourceFile("/src/moduleA/external.ts", externalCode);
117 | 		const targetSymbolName = "myVar";
118 | 		const newFilePath = "/src/moduleB/newFile.ts";
119 | 
120 | 		const declarationStatement = findTopLevelDeclarationByName(
121 | 			originalSourceFile,
122 | 			targetSymbolName,
123 | 			SyntaxKind.VariableStatement,
124 | 		);
125 | 		expect(declarationStatement).toBeDefined();
126 | 		if (!declarationStatement) return;
127 | 
128 | 		const classifiedDependencies: DependencyClassification[] = [];
129 | 		const neededExternalImports: NeededExternalImports = new Map();
130 | 		const importDecl = originalSourceFile.getImportDeclaration("./external");
131 | 		expect(importDecl).toBeDefined();
132 | 		if (importDecl) {
133 | 			const moduleSourceFile = importDecl.getModuleSpecifierSourceFile();
134 | 			const key = moduleSourceFile
135 | 				? moduleSourceFile.getFilePath()
136 | 				: importDecl.getModuleSpecifierValue();
137 | 			neededExternalImports.set(key, {
138 | 				names: new Set(["externalFunc"]),
139 | 				declaration: importDecl,
140 | 			});
141 | 		}
142 | 
143 | 		const newFileContent = generateNewSourceFileContent(
144 | 			declarationStatement,
145 | 			classifiedDependencies,
146 | 			originalSourceFile.getFilePath(),
147 | 			newFilePath,
148 | 			neededExternalImports,
149 | 		);
150 | 
151 | 		const expectedContent = `
152 | import { externalFunc } from "../moduleA/external";
153 | export const myVar = externalFunc(99);
154 | 		`.trim();
155 | 		const normalize = (str: string) => str.replace(/\s+/g, " ").trim();
156 | 		expect(normalize(newFileContent)).toBe(normalize(expectedContent));
157 | 	});
158 | 
159 | 	it("node_modulesからの外部依存を持つシンボルを移動する際、インポートパスが維持される", () => {
160 | 		const originalCode = `
161 | import { useState } from 'react';
162 | 
163 | const CounterComponent = () => {
164 |   const [count, setCount] = useState(0);
165 |   return \`Count: \${count}\`;
166 | };
167 | `;
168 | 		const originalFilePath = "/src/components/Counter.tsx";
169 | 		const newFilePath = "/src/features/NewCounter.tsx";
170 | 		const targetSymbolName = "CounterComponent";
171 | 
172 | 		const { project, originalSourceFile } = setupProjectWithCode(
173 | 			originalCode,
174 | 			originalFilePath,
175 | 		);
176 | 
177 | 		const declarationStatement = findTopLevelDeclarationByName(
178 | 			originalSourceFile,
179 | 			targetSymbolName,
180 | 			SyntaxKind.VariableStatement,
181 | 		);
182 | 		expect(declarationStatement).toBeDefined();
183 | 		if (!declarationStatement) return;
184 | 
185 | 		const neededExternalImports: NeededExternalImports = new Map();
186 | 		const reactImportDecl = originalSourceFile.getImportDeclaration("react");
187 | 		expect(reactImportDecl).toBeDefined();
188 | 		if (reactImportDecl) {
189 | 			expect(reactImportDecl.getModuleSpecifierSourceFile()).toBeUndefined();
190 | 			const key = reactImportDecl.getModuleSpecifierValue();
191 | 			neededExternalImports.set(key, {
192 | 				names: new Set(["useState"]),
193 | 				declaration: reactImportDecl,
194 | 			});
195 | 		}
196 | 
197 | 		const classifiedDependencies: DependencyClassification[] = [];
198 | 
199 | 		const newFileContent = generateNewSourceFileContent(
200 | 			declarationStatement,
201 | 			classifiedDependencies,
202 | 			originalFilePath,
203 | 			newFilePath,
204 | 			neededExternalImports,
205 | 		);
206 | 
207 | 		const expectedImportStatement = 'import { useState } from "react";';
208 | 		const expectedContent = `
209 | import { useState } from "react";
210 | 
211 | export const CounterComponent = () => {
212 |   const [count, setCount] = useState(0);
213 |   return \`Count: \${count}\`;
214 | };
215 |   `.trim();
216 | 		const normalize = (str: string) => str.replace(/\s+/g, " ").trim();
217 | 
218 | 		expect(newFileContent.trim()).toContain(expectedImportStatement);
219 | 
220 | 		expect(newFileContent).not.toContain("node_modules/react");
221 | 		expect(newFileContent).not.toContain("../");
222 | 
223 | 		expect(normalize(newFileContent)).toBe(normalize(expectedContent));
224 | 	});
225 | 
226 | 	it("名前空間インポート (import * as) を持つシンボルから新しいファイル内容を生成できる", () => {
227 | 		const originalCode = `
228 | import * as path from 'node:path';
229 | 
230 | const resolveFullPath = (dir: string, file: string): string => {
231 |   return path.resolve(dir, file);
232 | };
233 | `;
234 | 		const originalFilePath = "/src/utils/pathHelper.ts";
235 | 		const newFilePath = "/src/core/newPathHelper.ts";
236 | 		const targetSymbolName = "resolveFullPath";
237 | 
238 | 		const { project, originalSourceFile } = setupProjectWithCode(
239 | 			originalCode,
240 | 			originalFilePath,
241 | 		);
242 | 
243 | 		const declarationStatement = findTopLevelDeclarationByName(
244 | 			originalSourceFile,
245 | 			targetSymbolName,
246 | 			SyntaxKind.VariableStatement,
247 | 		);
248 | 		expect(declarationStatement).toBeDefined();
249 | 		if (!declarationStatement) return;
250 | 
251 | 		const neededExternalImports: NeededExternalImports = new Map();
252 | 		const pathImportDecl = originalSourceFile.getImportDeclaration("node:path");
253 | 		expect(pathImportDecl).toBeDefined();
254 | 		if (pathImportDecl) {
255 | 			const key = pathImportDecl.getModuleSpecifierValue();
256 | 			neededExternalImports.set(key, {
257 | 				names: new Set(),
258 | 				declaration: pathImportDecl,
259 | 				isNamespaceImport: true,
260 | 				namespaceImportName: "path",
261 | 			});
262 | 		}
263 | 
264 | 		const classifiedDependencies: DependencyClassification[] = [];
265 | 
266 | 		const newFileContent = generateNewSourceFileContent(
267 | 			declarationStatement,
268 | 			classifiedDependencies,
269 | 			originalFilePath,
270 | 			newFilePath,
271 | 			neededExternalImports,
272 | 		);
273 | 
274 | 		const expectedImportStatement = 'import * as path from "node:path";';
275 | 		const expectedContent = `
276 | ${expectedImportStatement}
277 | 
278 | export const resolveFullPath = (dir: string, file: string): string => {
279 |   return path.resolve(dir, file);
280 | };
281 |   `.trim();
282 | 		const normalize = (str: string) => str.replace(/\s+/g, " ").trim();
283 | 
284 | 		expect(newFileContent.trim()).toContain(expectedImportStatement);
285 | 		expect(normalize(newFileContent)).toBe(normalize(expectedContent));
286 | 	});
287 | 
288 | 	it("デフォルトインポートに依存するシンボルから新しいファイル内容を生成できる", () => {
289 | 		const loggerCode = `
290 | 			export default function logger(message: string) {
291 | 				console.log(message);
292 | 			}
293 | 		`;
294 | 		const originalCode = `
295 | 			import myLogger from './logger';
296 | 
297 | 			function functionThatUsesLogger(msg: string) {
298 | 				myLogger(\`LOG: \${msg}\`);
299 | 			}
300 | 		`;
301 | 		const originalFilePath = "/src/module/main.ts";
302 | 		const loggerFilePath = "/src/module/logger.ts";
303 | 		const newFilePath = "/src/feature/newLoggerUser.ts";
304 | 		const targetSymbolName = "functionThatUsesLogger";
305 | 
306 | 		const { project, originalSourceFile } = setupProjectWithCode(
307 | 			originalCode,
308 | 			originalFilePath,
309 | 		);
310 | 		project.createSourceFile(loggerFilePath, loggerCode);
311 | 
312 | 		// 移動対象の宣言を取得
313 | 		const declarationStatement = findTopLevelDeclarationByName(
314 | 			originalSourceFile,
315 | 			targetSymbolName,
316 | 			SyntaxKind.FunctionDeclaration,
317 | 		);
318 | 		expect(declarationStatement).toBeDefined();
319 | 		if (!declarationStatement) return;
320 | 
321 | 		// 必要な外部インポート情報を手動で設定 (デフォルトインポート)
322 | 		const neededExternalImports: NeededExternalImports = new Map();
323 | 		const loggerImportDecl =
324 | 			originalSourceFile.getImportDeclaration("./logger");
325 | 		expect(loggerImportDecl).toBeDefined();
326 | 		if (loggerImportDecl) {
327 | 			const moduleSourceFile = loggerImportDecl.getModuleSpecifierSourceFile();
328 | 			expect(moduleSourceFile).toBeDefined();
329 | 			if (moduleSourceFile) {
330 | 				const key = moduleSourceFile.getFilePath();
331 | 				neededExternalImports.set(key, {
332 | 					names: new Set(["default"]),
333 | 					declaration: loggerImportDecl,
334 | 				});
335 | 			}
336 | 		}
337 | 
338 | 		const classifiedDependencies: DependencyClassification[] = [];
339 | 
340 | 		const newFileContent = generateNewSourceFileContent(
341 | 			declarationStatement,
342 | 			classifiedDependencies,
343 | 			originalFilePath,
344 | 			newFilePath,
345 | 			neededExternalImports,
346 | 		);
347 | 
348 | 		const expectedImportStatement = 'import myLogger from "../module/logger";';
349 | 		const incorrectImport1 = 'import { default } from "../module/logger";';
350 | 		const incorrectImport2 =
351 | 			'import { default as myLogger } from "../module/logger";';
352 | 
353 | 		expect(newFileContent).not.toContain(incorrectImport1);
354 | 		expect(newFileContent).not.toContain(incorrectImport2);
355 | 
356 | 		expect(newFileContent).toContain(expectedImportStatement);
357 | 
358 | 		expect(newFileContent).toContain("export function functionThatUsesLogger");
359 | 	});
360 | });
361 | 
```

--------------------------------------------------------------------------------
/src/ts-morph/rename-file-system/rename-file-system-entry.special.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect } from "vitest";
  2 | import { Project } from "ts-morph";
  3 | import { renameFileSystemEntry } from "./rename-file-system-entry";
  4 | 
  5 | // --- Test Setup Helper ---
  6 | 
  7 | const setupProject = () => {
  8 | 	const project = new Project({
  9 | 		useInMemoryFileSystem: true,
 10 | 		compilerOptions: {
 11 | 			baseUrl: ".",
 12 | 			paths: {
 13 | 				"@/*": ["src/*"],
 14 | 			},
 15 | 			esModuleInterop: true,
 16 | 			allowJs: true,
 17 | 		},
 18 | 	});
 19 | 
 20 | 	project.createDirectory("/src");
 21 | 	project.createDirectory("/src/utils");
 22 | 	project.createDirectory("/src/components");
 23 | 
 24 | 	return project;
 25 | };
 26 | 
 27 | describe("renameFileSystemEntry Special Cases", () => {
 28 | 	it("dryRun: true の場合、ファイルシステム(メモリ上)の変更を行わず、変更予定リストを返す", async () => {
 29 | 		const project = setupProject();
 30 | 		const oldUtilPath = "/src/utils/old-util.ts";
 31 | 		const newUtilPath = "/src/utils/new-util.ts";
 32 | 		const componentPath = "/src/components/MyComponent.ts";
 33 | 
 34 | 		project.createSourceFile(
 35 | 			oldUtilPath,
 36 | 			'export const oldUtil = () => "old";',
 37 | 		);
 38 | 		project.createSourceFile(
 39 | 			componentPath,
 40 | 			`import { oldUtil } from '../utils/old-util';`,
 41 | 		);
 42 | 
 43 | 		const result = await renameFileSystemEntry({
 44 | 			project,
 45 | 			renames: [{ oldPath: oldUtilPath, newPath: newUtilPath }],
 46 | 			dryRun: true,
 47 | 		});
 48 | 
 49 | 		expect(project.getSourceFile(oldUtilPath)).toBeUndefined();
 50 | 		expect(project.getSourceFile(newUtilPath)).toBeDefined();
 51 | 
 52 | 		expect(result.changedFiles).toContain(newUtilPath);
 53 | 		expect(result.changedFiles).toContain(componentPath);
 54 | 		expect(result.changedFiles).not.toContain(oldUtilPath);
 55 | 	});
 56 | 
 57 | 	it("どのファイルからも参照されていないファイルをリネームする", async () => {
 58 | 		const project = setupProject();
 59 | 		const oldPath = "/src/utils/unreferenced.ts";
 60 | 		const newPath = "/src/utils/renamed-unreferenced.ts";
 61 | 		project.createSourceFile(oldPath, "export const lonely = true;");
 62 | 
 63 | 		const result = await renameFileSystemEntry({
 64 | 			project,
 65 | 			renames: [{ oldPath, newPath }],
 66 | 			dryRun: false,
 67 | 		});
 68 | 
 69 | 		expect(project.getSourceFile(oldPath)).toBeUndefined();
 70 | 		expect(project.getSourceFile(newPath)).toBeDefined();
 71 | 		expect(project.getSourceFileOrThrow(newPath).getFullText()).toContain(
 72 | 			"export const lonely = true;",
 73 | 		);
 74 | 		expect(result.changedFiles).toEqual([newPath]);
 75 | 	});
 76 | 
 77 | 	it("デフォルトインポートのパスが正しく更新される", async () => {
 78 | 		const project = setupProject();
 79 | 		const oldDefaultPath = "/src/utils/defaultExport.ts";
 80 | 		const newDefaultPath = "/src/utils/renamedDefaultExport.ts";
 81 | 		const importerPath = "/src/importer.ts";
 82 | 
 83 | 		project.createSourceFile(
 84 | 			oldDefaultPath,
 85 | 			"export default function myDefaultFunction() { return 'default'; }",
 86 | 		);
 87 | 		project.createSourceFile(
 88 | 			importerPath,
 89 | 			"import MyDefaultImport from './utils/defaultExport';\nconsole.log(MyDefaultImport());",
 90 | 		);
 91 | 
 92 | 		await renameFileSystemEntry({
 93 | 			project,
 94 | 			renames: [{ oldPath: oldDefaultPath, newPath: newDefaultPath }],
 95 | 			dryRun: false,
 96 | 		});
 97 | 
 98 | 		const updatedImporterContent = project
 99 | 			.getSourceFileOrThrow(importerPath)
100 | 			.getFullText();
101 | 		expect(project.getSourceFile(oldDefaultPath)).toBeUndefined();
102 | 		expect(project.getSourceFile(newDefaultPath)).toBeDefined();
103 | 		expect(updatedImporterContent).toContain(
104 | 			"import MyDefaultImport from './utils/renamedDefaultExport';",
105 | 		);
106 | 	});
107 | 
108 | 	it("デフォルトエクスポートされた変数 (export default variableName) のパスが正しく更新される", async () => {
109 | 		const project = setupProject();
110 | 		const oldVarDefaultPath = "/src/utils/variableDefaultExport.ts";
111 | 		const newVarDefaultPath = "/src/utils/renamedVariableDefaultExport.ts";
112 | 		const importerPath = "/src/importerVar.ts";
113 | 
114 | 		project.createSourceFile(
115 | 			oldVarDefaultPath,
116 | 			"const myVar = { value: 'default var' };\nexport default myVar;",
117 | 		);
118 | 		project.createSourceFile(
119 | 			importerPath,
120 | 			"import MyVarImport from './utils/variableDefaultExport';\nconsole.log(MyVarImport.value);",
121 | 		);
122 | 
123 | 		await renameFileSystemEntry({
124 | 			project,
125 | 			renames: [{ oldPath: oldVarDefaultPath, newPath: newVarDefaultPath }],
126 | 			dryRun: false,
127 | 		});
128 | 
129 | 		const updatedImporterContent = project
130 | 			.getSourceFileOrThrow(importerPath)
131 | 			.getFullText();
132 | 		expect(project.getSourceFile(oldVarDefaultPath)).toBeUndefined();
133 | 		expect(project.getSourceFile(newVarDefaultPath)).toBeDefined();
134 | 		expect(updatedImporterContent).toContain(
135 | 			"import MyVarImport from './utils/renamedVariableDefaultExport';",
136 | 		);
137 | 	});
138 | });
139 | 
140 | describe("renameFileSystemEntry Extension Preservation", () => {
141 | 	it("import文のパスに .js 拡張子が含まれている場合、リネーム後も維持される", async () => {
142 | 		const project = setupProject();
143 | 		const oldJsPath = "/src/utils/legacy-util.js";
144 | 		const newJsPath = "/src/utils/modern-util.js";
145 | 		const importerPath = "/src/components/MyComponent.ts";
146 | 		const otherTsPath = "/src/utils/helper.ts";
147 | 		const newOtherTsPath = "/src/utils/renamed-helper.ts";
148 | 
149 | 		project.createSourceFile(oldJsPath, "export const legacyValue = 1;");
150 | 		project.createSourceFile(otherTsPath, "export const helperValue = 2;");
151 | 		project.createSourceFile(
152 | 			importerPath,
153 | 			`import { legacyValue } from '../utils/legacy-util.js';
154 | import { helperValue } from '../utils/helper';
155 | 
156 | console.log(legacyValue, helperValue);
157 | `,
158 | 		);
159 | 
160 | 		await renameFileSystemEntry({
161 | 			project,
162 | 			renames: [
163 | 				{ oldPath: oldJsPath, newPath: newJsPath },
164 | 				{ oldPath: otherTsPath, newPath: newOtherTsPath },
165 | 			],
166 | 			dryRun: false,
167 | 		});
168 | 
169 | 		const updatedImporterContent = project
170 | 			.getSourceFileOrThrow(importerPath)
171 | 			.getFullText();
172 | 
173 | 		expect(updatedImporterContent).toContain(
174 | 			"import { legacyValue } from '../utils/modern-util.js';",
175 | 		);
176 | 		expect(updatedImporterContent).toContain(
177 | 			"import { helperValue } from '../utils/renamed-helper';",
178 | 		);
179 | 
180 | 		expect(project.getSourceFile(oldJsPath)).toBeUndefined();
181 | 		expect(project.getSourceFile(newJsPath)).toBeDefined();
182 | 		expect(project.getSourceFile(otherTsPath)).toBeUndefined();
183 | 		expect(project.getSourceFile(newOtherTsPath)).toBeDefined();
184 | 	});
185 | });
186 | 
187 | describe("renameFileSystemEntry with index.ts re-exports", () => {
188 | 	it("index.ts が 'export * from \"./moduleB\"' 形式で moduleB.ts を再エクスポートし、moduleB.ts をリネームした場合", async () => {
189 | 		const project = setupProject();
190 | 		const utilsDir = "/src/utils";
191 | 		const moduleBOriginalPath = `${utilsDir}/moduleB.ts`;
192 | 		const moduleBRenamedPath = `${utilsDir}/moduleBRenamed.ts`;
193 | 		const indexTsPath = `${utilsDir}/index.ts`;
194 | 		const componentPath = "/src/components/MyComponent.ts";
195 | 
196 | 		project.createSourceFile(
197 | 			moduleBOriginalPath,
198 | 			"export const importantValue = 'Hello from B';",
199 | 		);
200 | 		project.createSourceFile(indexTsPath, 'export * from "./moduleB";');
201 | 		project.createSourceFile(
202 | 			componentPath,
203 | 			"import { importantValue } from '@/utils';\\nconsole.log(importantValue);",
204 | 		);
205 | 
206 | 		const result = await renameFileSystemEntry({
207 | 			project,
208 | 			renames: [{ oldPath: moduleBOriginalPath, newPath: moduleBRenamedPath }],
209 | 			dryRun: false,
210 | 		});
211 | 
212 | 		expect(project.getSourceFile(moduleBOriginalPath)).toBeUndefined();
213 | 		expect(project.getSourceFile(moduleBRenamedPath)).toBeDefined();
214 | 		expect(project.getSourceFileOrThrow(moduleBRenamedPath).getFullText()).toBe(
215 | 			"export const importantValue = 'Hello from B';",
216 | 		);
217 | 
218 | 		const indexTsContent = project
219 | 			.getSourceFileOrThrow(indexTsPath)
220 | 			.getFullText();
221 | 		expect(indexTsContent).toContain('export * from "./moduleBRenamed";');
222 | 		expect(indexTsContent).not.toContain('export * from "./moduleB";');
223 | 
224 | 		const componentContent = project
225 | 			.getSourceFileOrThrow(componentPath)
226 | 			.getFullText();
227 | 		expect(componentContent).toContain(
228 | 			"import { importantValue } from '@/utils';",
229 | 		);
230 | 
231 | 		expect(result.changedFiles).toHaveLength(3);
232 | 		expect(result.changedFiles).toEqual(
233 | 			expect.arrayContaining([moduleBRenamedPath, indexTsPath, componentPath]),
234 | 		);
235 | 	});
236 | 
237 | 	it("index.ts が 'export { specificExport } from \"./moduleC\"' 形式で moduleC.ts を再エクスポートし、moduleC.ts をリネームした場合", async () => {
238 | 		const project = setupProject();
239 | 		const utilsDir = "/src/utils";
240 | 		const moduleCOriginalPath = `${utilsDir}/moduleC.ts`;
241 | 		const moduleCRenamedPath = `${utilsDir}/moduleCRenamed.ts`;
242 | 		const indexTsPath = `${utilsDir}/index.ts`;
243 | 		const componentPath = "/src/components/MyComponentForC.ts";
244 | 
245 | 		project.createSourceFile(
246 | 			moduleCOriginalPath,
247 | 			"export const specificExport = 'Hello from C';",
248 | 		);
249 | 		project.createSourceFile(
250 | 			indexTsPath,
251 | 			'export { specificExport } from "./moduleC";',
252 | 		);
253 | 		project.createSourceFile(
254 | 			componentPath,
255 | 			"import { specificExport } from '@/utils';\\nconsole.log(specificExport);",
256 | 		);
257 | 
258 | 		const result = await renameFileSystemEntry({
259 | 			project,
260 | 			renames: [{ oldPath: moduleCOriginalPath, newPath: moduleCRenamedPath }],
261 | 			dryRun: false,
262 | 		});
263 | 
264 | 		expect(project.getSourceFile(moduleCOriginalPath)).toBeUndefined();
265 | 		expect(project.getSourceFile(moduleCRenamedPath)).toBeDefined();
266 | 		expect(project.getSourceFileOrThrow(moduleCRenamedPath).getFullText()).toBe(
267 | 			"export const specificExport = 'Hello from C';",
268 | 		);
269 | 
270 | 		const indexTsContent = project
271 | 			.getSourceFileOrThrow(indexTsPath)
272 | 			.getFullText();
273 | 		expect(indexTsContent).toContain(
274 | 			'export { specificExport } from "./moduleCRenamed";',
275 | 		);
276 | 		expect(indexTsContent).not.toContain(
277 | 			'export { specificExport } from "./moduleC";',
278 | 		);
279 | 
280 | 		const componentContent = project
281 | 			.getSourceFileOrThrow(componentPath)
282 | 			.getFullText();
283 | 		expect(componentContent).toContain(
284 | 			"import { specificExport } from '@/utils';",
285 | 		);
286 | 
287 | 		expect(result.changedFiles).toHaveLength(3);
288 | 		expect(result.changedFiles).toEqual(
289 | 			expect.arrayContaining([moduleCRenamedPath, indexTsPath, componentPath]),
290 | 		);
291 | 	});
292 | 
293 | 	it("index.ts が再エクスポートを行い、その utils ディレクトリ全体をリネームした場合", async () => {
294 | 		const project = setupProject();
295 | 		const oldUtilsDir = "/src/utils";
296 | 		const newUtilsDir = "/src/newUtils";
297 | 
298 | 		const moduleDOriginalPath = `${oldUtilsDir}/moduleD.ts`;
299 | 		const indexTsOriginalPath = `${oldUtilsDir}/index.ts`;
300 | 		const componentPath = "/src/components/MyComponentForD.ts";
301 | 
302 | 		project.createSourceFile(
303 | 			moduleDOriginalPath,
304 | 			"export const valueFromD = 'Hello from D';",
305 | 		);
306 | 		project.createSourceFile(indexTsOriginalPath, 'export * from "./moduleD";');
307 | 		project.createSourceFile(
308 | 			componentPath,
309 | 			"import { valueFromD } from '@/utils';\\nconsole.log(valueFromD);",
310 | 		);
311 | 
312 | 		const result = await renameFileSystemEntry({
313 | 			project,
314 | 			renames: [{ oldPath: oldUtilsDir, newPath: newUtilsDir }],
315 | 			dryRun: false,
316 | 		});
317 | 
318 | 		const moduleDRenamedPath = `${newUtilsDir}/moduleD.ts`;
319 | 		const indexTsRenamedPath = `${newUtilsDir}/index.ts`;
320 | 
321 | 		expect(project.getSourceFile(moduleDOriginalPath)).toBeUndefined();
322 | 		expect(project.getSourceFile(indexTsOriginalPath)).toBeUndefined();
323 | 		// expect(project.getDirectory(oldUtilsDir)).toBeUndefined(); // ユーザーの指示によりコメントアウト
324 | 
325 | 		expect(project.getDirectory(newUtilsDir)).toBeDefined();
326 | 		expect(project.getSourceFile(moduleDRenamedPath)).toBeDefined();
327 | 		expect(project.getSourceFile(indexTsRenamedPath)).toBeDefined();
328 | 
329 | 		expect(project.getSourceFileOrThrow(moduleDRenamedPath).getFullText()).toBe(
330 | 			"export const valueFromD = 'Hello from D';",
331 | 		);
332 | 		expect(project.getSourceFileOrThrow(indexTsRenamedPath).getFullText()).toBe(
333 | 			'export * from "./moduleD";',
334 | 		);
335 | 
336 | 		const componentContent = project
337 | 			.getSourceFileOrThrow(componentPath)
338 | 			.getFullText();
339 | 		expect(componentContent).toContain(
340 | 			"import { valueFromD } from '../newUtils/index';",
341 | 		);
342 | 
343 | 		expect(result.changedFiles).toHaveLength(3);
344 | 		expect(result.changedFiles).toEqual(
345 | 			expect.arrayContaining([
346 | 				moduleDRenamedPath,
347 | 				indexTsRenamedPath,
348 | 				componentPath,
349 | 			]),
350 | 		);
351 | 	});
352 | });
353 | 
354 | describe("renameFileSystemEntry with index.ts re-exports (actual bug reproduction)", () => {
355 | 	it("index.tsが複数のモジュールを再エクスポートし、そのうちの1つをリネームした際、インポート元のパスがindex.tsを指し続けること", async () => {
356 | 		const project = setupProject();
357 | 		const utilsDir = "/src/utils";
358 | 		const moduleAOriginalPath = `${utilsDir}/moduleA.ts`;
359 | 		const moduleARenamedPath = `${utilsDir}/moduleARenamed.ts`;
360 | 		const moduleBPath = `${utilsDir}/moduleB.ts`;
361 | 		const indexTsPath = `${utilsDir}/index.ts`;
362 | 		const componentPath = "/src/components/MyComponent.ts";
363 | 
364 | 		project.createSourceFile(
365 | 			moduleAOriginalPath,
366 | 			"export const funcA = () => 'original_A';",
367 | 		);
368 | 		project.createSourceFile(moduleBPath, "export const funcB = () => 'B';");
369 | 		project.createSourceFile(
370 | 			indexTsPath,
371 | 			'export * from "./moduleA";\nexport * from "./moduleB";',
372 | 		);
373 | 		project.createSourceFile(
374 | 			componentPath,
375 | 			"import { funcA, funcB } from '@/utils';\nconsole.log(funcA(), funcB());",
376 | 		);
377 | 
378 | 		const originalComponentContent = project
379 | 			.getSourceFileOrThrow(componentPath)
380 | 			.getFullText();
381 | 
382 | 		await renameFileSystemEntry({
383 | 			project,
384 | 			renames: [{ oldPath: moduleAOriginalPath, newPath: moduleARenamedPath }],
385 | 			dryRun: false,
386 | 		});
387 | 
388 | 		// 1. moduleA.ts がリネームされていること
389 | 		expect(project.getSourceFile(moduleAOriginalPath)).toBeUndefined();
390 | 		expect(project.getSourceFile(moduleARenamedPath)).toBeDefined();
391 | 		expect(project.getSourceFileOrThrow(moduleARenamedPath).getFullText()).toBe(
392 | 			"export const funcA = () => 'original_A';",
393 | 		);
394 | 
395 | 		// 2. index.ts が正しく更新されていること
396 | 		const indexTsContent = project
397 | 			.getSourceFileOrThrow(indexTsPath)
398 | 			.getFullText();
399 | 		expect(indexTsContent).toContain('export * from "./moduleARenamed";');
400 | 		expect(indexTsContent).toContain('export * from "./moduleB";');
401 | 		expect(indexTsContent).not.toContain('export * from "./moduleA";');
402 | 
403 | 		// 3. MyComponent.ts のインポートパスが変更されていないこと
404 | 		const updatedComponentContent = project
405 | 			.getSourceFileOrThrow(componentPath)
406 | 			.getFullText();
407 | 		expect(updatedComponentContent).toBe(originalComponentContent);
408 | 		// さらに具体的に確認
409 | 		expect(updatedComponentContent).toContain(
410 | 			"import { funcA, funcB } from '@/utils';",
411 | 		);
412 | 	});
413 | });
414 | 
```