Skip to main content
Glama
SiroSuzume

MCP ts-morph Refactoring Tools

by SiroSuzume
find-references.test.ts13.7 kB
import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { findSymbolReferences } from "./find-references"; import * as fs from "node:fs"; import * as path from "node:path"; import * as os from "node:os"; /** * テスト用の一時ディレクトリを作成 */ function createTempDir(): string { return fs.mkdtempSync(path.join(os.tmpdir(), "find-references-test-")); } /** * ディレクトリを再帰的に削除 */ function removeTempDir(dir: string): void { if (fs.existsSync(dir)) { fs.rmSync(dir, { recursive: true, force: true }); } } describe("findSymbolReferences", () => { let tempDir: string; beforeEach(() => { tempDir = createTempDir(); }); afterEach(() => { removeTempDir(tempDir); }); it("基本的な変数の参照を見つけることができる", async () => { // ファイルシステムにテストプロジェクトを作成 const tsconfigPath = path.join(tempDir, "tsconfig.json"); const srcDir = path.join(tempDir, "src"); fs.mkdirSync(srcDir, { recursive: true }); // tsconfig.json を作成 fs.writeFileSync( tsconfigPath, JSON.stringify( { compilerOptions: { rootDir: "./src", outDir: "./dist", module: "commonjs", target: "es2020", strict: true, }, include: ["src/**/*"], }, null, 2, ), ); // テストファイルを作成 const utilsPath = path.join(srcDir, "utils.ts"); const mainPath = path.join(srcDir, "main.ts"); fs.writeFileSync( utilsPath, `export const myVariable = "test value"; export function helperFunction() { return myVariable; } `, ); fs.writeFileSync( mainPath, `import { myVariable, helperFunction } from "./utils"; console.log(myVariable); const result = helperFunction(); `, ); // myVariable の参照を検索(定義位置) const result = await findSymbolReferences({ tsconfigPath, targetFilePath: utilsPath, position: { line: 1, column: 14 }, // "myVariable" の位置 }); // 定義位置の確認 expect(result.definition).toBeTruthy(); expect(result.definition?.filePath).toBe(utilsPath); expect(result.definition?.line).toBe(1); expect(result.definition?.text).toContain("myVariable"); // 参照箇所の確認(定義箇所は除外される) // インポート文での参照も含まれる expect(result.references.length).toBeGreaterThanOrEqual(2); // utils.ts内での参照 const utilsRef = result.references.find( (ref) => ref.filePath === utilsPath && ref.line === 4, ); expect(utilsRef).toBeTruthy(); // main.ts内での参照(インポート文とconsole.log) const mainRefs = result.references.filter( (ref) => ref.filePath === mainPath, ); expect(mainRefs.length).toBeGreaterThanOrEqual(1); // console.logでの参照が含まれていることを確認 const consoleLogRef = mainRefs.find((ref) => ref.line === 3); expect(consoleLogRef).toBeTruthy(); }); it("関数の参照を見つけることができる", async () => { const tsconfigPath = path.join(tempDir, "tsconfig.json"); const srcDir = path.join(tempDir, "src"); fs.mkdirSync(srcDir, { recursive: true }); fs.writeFileSync( tsconfigPath, JSON.stringify( { compilerOptions: { rootDir: "./src", outDir: "./dist", module: "commonjs", target: "es2020", strict: true, }, include: ["src/**/*"], }, null, 2, ), ); const functionsPath = path.join(srcDir, "functions.ts"); const usagePath = path.join(srcDir, "usage.ts"); fs.writeFileSync( functionsPath, `export function calculate(a: number, b: number): number { return a + b; } export function processData() { const result = calculate(10, 20); return result; } `, ); fs.writeFileSync( usagePath, `import { calculate, processData } from "./functions"; const sum = calculate(5, 3); console.log(sum); processData(); `, ); // calculate 関数の参照を検索 const result = await findSymbolReferences({ tsconfigPath, targetFilePath: functionsPath, position: { line: 1, column: 17 }, // "calculate" の位置 }); expect(result.definition).toBeTruthy(); expect(result.definition?.filePath).toBe(functionsPath); // 参照箇所(定義を除く) // インポート文での参照も含まれる expect(result.references.length).toBeGreaterThanOrEqual(2); // functions.ts内での参照 const internalRef = result.references.find( (ref) => ref.filePath === functionsPath && ref.line === 6, ); expect(internalRef).toBeTruthy(); // usage.ts内での参照 const externalRefs = result.references.filter( (ref) => ref.filePath === usagePath, ); expect(externalRefs.length).toBeGreaterThanOrEqual(1); // calculate(5, 3)の呼び出しが含まれていることを確認 const callRef = externalRefs.find((ref) => ref.line === 3); expect(callRef).toBeTruthy(); }); it("クラスの参照を見つけることができる", async () => { const tsconfigPath = path.join(tempDir, "tsconfig.json"); const srcDir = path.join(tempDir, "src"); fs.mkdirSync(srcDir, { recursive: true }); fs.writeFileSync( tsconfigPath, JSON.stringify( { compilerOptions: { rootDir: "./src", outDir: "./dist", module: "commonjs", target: "es2020", strict: true, }, include: ["src/**/*"], }, null, 2, ), ); const modelsPath = path.join(srcDir, "models.ts"); const appPath = path.join(srcDir, "app.ts"); fs.writeFileSync( modelsPath, `export class User { constructor(public name: string, public age: number) {} greet(): string { return \`Hello, I'm \${this.name}\`; } } export class Admin extends User { constructor(name: string, age: number, public role: string) { super(name, age); } } `, ); fs.writeFileSync( appPath, `import { User, Admin } from "./models"; const user = new User("John", 30); const admin = new Admin("Jane", 25, "super-admin"); console.log(user.greet()); `, ); // User クラスの参照を検索 const result = await findSymbolReferences({ tsconfigPath, targetFilePath: modelsPath, position: { line: 1, column: 14 }, // "User" の位置 }); expect(result.definition).toBeTruthy(); expect(result.definition?.filePath).toBe(modelsPath); // 参照箇所 expect(result.references.length).toBeGreaterThanOrEqual(2); // Admin クラスでの継承 const extendsRef = result.references.find( (ref) => ref.filePath === modelsPath && ref.text.includes("extends"), ); expect(extendsRef).toBeTruthy(); // app.tsでのインスタンス化 const instantiationRef = result.references.find( (ref) => ref.filePath === appPath && ref.text.includes("new User"), ); expect(instantiationRef).toBeTruthy(); }); it("存在しないシンボルに対してエラーをスローする", async () => { const tsconfigPath = path.join(tempDir, "tsconfig.json"); const srcDir = path.join(tempDir, "src"); fs.mkdirSync(srcDir, { recursive: true }); fs.writeFileSync( tsconfigPath, JSON.stringify( { compilerOptions: { rootDir: "./src", outDir: "./dist", module: "commonjs", target: "es2020", strict: true, }, include: ["src/**/*"], }, null, 2, ), ); const testPath = path.join(srcDir, "test.ts"); fs.writeFileSync( testPath, `const someVariable = "test"; `, ); // 存在しない位置を指定 await expect( findSymbolReferences({ tsconfigPath, targetFilePath: testPath, position: { line: 10, column: 1 }, // 存在しない行 }), ).rejects.toThrow(); }); it("re-exportされたシンボルの参照を見つけることができる", async () => { const tsconfigPath = path.join(tempDir, "tsconfig.json"); const srcDir = path.join(tempDir, "src"); fs.mkdirSync(srcDir, { recursive: true }); fs.writeFileSync( tsconfigPath, JSON.stringify( { compilerOptions: { rootDir: "./src", outDir: "./dist", module: "commonjs", target: "es2020", strict: true, }, include: ["src/**/*"], }, null, 2, ), ); const utilsPath = path.join(srcDir, "utils.ts"); const indexPath = path.join(srcDir, "index.ts"); const appPath = path.join(srcDir, "app.ts"); // utils.ts - オリジナルの定義 fs.writeFileSync( utilsPath, `export function helper() { return "helper function"; } export const CONSTANT = 42; `, ); // index.ts - re-export fs.writeFileSync( indexPath, `export { helper, CONSTANT } from "./utils"; export { helper as utilHelper } from "./utils"; // 別名でのre-export `, ); // app.ts - re-export経由での使用 fs.writeFileSync( appPath, `import { helper, CONSTANT, utilHelper } from "./index"; console.log(helper()); console.log(CONSTANT); console.log(utilHelper()); `, ); // helper関数の参照を検索 const result = await findSymbolReferences({ tsconfigPath, targetFilePath: utilsPath, position: { line: 1, column: 17 }, // "helper" の位置 }); expect(result.definition).toBeTruthy(); expect(result.definition?.filePath).toBe(utilsPath); // re-export文とインポート文、使用箇所での参照を含む expect(result.references.length).toBeGreaterThanOrEqual(3); // index.tsでのre-export const reExportRefs = result.references.filter( (ref) => ref.filePath === indexPath, ); expect(reExportRefs.length).toBeGreaterThanOrEqual(2); // 通常のre-exportと別名でのre-export // app.tsでの使用 const appRefs = result.references.filter((ref) => ref.filePath === appPath); expect(appRefs.length).toBeGreaterThanOrEqual(1); }); it("循環参照があるファイル間での参照を見つけることができる", async () => { const tsconfigPath = path.join(tempDir, "tsconfig.json"); const srcDir = path.join(tempDir, "src"); fs.mkdirSync(srcDir, { recursive: true }); fs.writeFileSync( tsconfigPath, JSON.stringify( { compilerOptions: { rootDir: "./src", outDir: "./dist", module: "commonjs", target: "es2020", strict: true, }, include: ["src/**/*"], }, null, 2, ), ); const moduleAPath = path.join(srcDir, "moduleA.ts"); const moduleBPath = path.join(srcDir, "moduleB.ts"); // moduleA.ts - moduleBを参照 fs.writeFileSync( moduleAPath, `import { functionB } from "./moduleB"; export function functionA() { return "A"; } export function useB() { return functionB(); } `, ); // moduleB.ts - moduleAを参照(循環参照) fs.writeFileSync( moduleBPath, `import { functionA } from "./moduleA"; export function functionB() { return "B"; } export function useA() { return functionA(); } `, ); // functionAの参照を検索 const result = await findSymbolReferences({ tsconfigPath, targetFilePath: moduleAPath, position: { line: 3, column: 17 }, // "functionA" の位置 }); expect(result.definition).toBeTruthy(); expect(result.definition?.filePath).toBe(moduleAPath); // moduleBからの参照を確認 const moduleBRefs = result.references.filter( (ref) => ref.filePath === moduleBPath, ); expect(moduleBRefs.length).toBeGreaterThanOrEqual(1); // useA関数内での使用を確認 const useARef = moduleBRefs.find((ref) => ref.text.includes("functionA()")); expect(useARef).toBeTruthy(); }); it("インターフェースの参照を見つけることができる", async () => { const tsconfigPath = path.join(tempDir, "tsconfig.json"); const srcDir = path.join(tempDir, "src"); fs.mkdirSync(srcDir, { recursive: true }); fs.writeFileSync( tsconfigPath, JSON.stringify( { compilerOptions: { rootDir: "./src", outDir: "./dist", module: "commonjs", target: "es2020", strict: true, }, include: ["src/**/*"], }, null, 2, ), ); const typesPath = path.join(srcDir, "types.ts"); const implementationPath = path.join(srcDir, "implementation.ts"); fs.writeFileSync( typesPath, `export interface UserData { id: number; name: string; email: string; } export interface AdminData extends UserData { role: string; } `, ); fs.writeFileSync( implementationPath, `import { UserData, AdminData } from "./types"; function processUser(user: UserData): void { console.log(user.name); } const userData: UserData = { id: 1, name: "John", email: "john@example.com" }; const adminData: AdminData = { id: 2, name: "Jane", email: "jane@example.com", role: "admin" }; processUser(userData); processUser(adminData); `, ); // UserData インターフェースの参照を検索 const result = await findSymbolReferences({ tsconfigPath, targetFilePath: typesPath, position: { line: 1, column: 18 }, // "UserData" の位置 }); expect(result.definition).toBeTruthy(); expect(result.definition?.filePath).toBe(typesPath); // 参照箇所を確認 expect(result.references.length).toBeGreaterThanOrEqual(3); // types.ts内での継承での参照 const extendsRef = result.references.find( (ref) => ref.filePath === typesPath && ref.text.includes("extends"), ); expect(extendsRef).toBeTruthy(); // implementation.ts内での型注釈での参照 const typeAnnotationRefs = result.references.filter( (ref) => ref.filePath === implementationPath, ); expect(typeAnnotationRefs.length).toBeGreaterThanOrEqual(2); // 関数パラメータと変数宣言 }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/SiroSuzume/mcp-ts-morph'

If you have feedback or need assistance with the MCP directory API, please join our Discord server