Implement MOVE operation with import management and basic dependency analysis. This phase focuses on operations that span multiple files and require careful coordination of imports and exports.
File: src/core/tools/refactor-code/utils/import-manager.ts
import {
Project,
SourceFile,
ImportDeclaration,
ExportDeclaration,
ImportSpecifier,
ExportSpecifier,
Node,
SyntaxKind,
} from "ts-morph"
import * as path from "path"
export interface ImportUpdate {
file: SourceFile
oldPath: string
newPath: string
symbolName: string
}
export class ImportManager {
private project: Project
private updatedFiles: Set<string> = new Set()
constructor(project: Project) {
this.project = project
}
/**
* Updates all imports after a symbol is moved to a new file
*/
async updateImportsAfterMove(symbolName: string, oldFilePath: string, newFilePath: string): Promise<void> {
this.updatedFiles.clear()
// Find all files that import from the old file
const importingFiles = this.findFilesImporting(oldFilePath)
for (const file of importingFiles) {
await this.updateImportPath(file, symbolName, oldFilePath, newFilePath)
this.updatedFiles.add(file.getFilePath())
}
// Update re-exports as well
const reExportingFiles = this.findFilesReExporting(oldFilePath)
for (const file of reExportingFiles) {
await this.updateReExportPath(file, symbolName, oldFilePath, newFilePath)
this.updatedFiles.add(file.getFilePath())
}
// Add necessary imports to the new file
const newFile = this.project.getSourceFile(newFilePath)
if (newFile) {
await this.addMissingImports(newFile, symbolName, oldFilePath)
}
}
/**
* Finds all files that import from the specified file
*/
private findFilesImporting(filePath: string): SourceFile[] {
const sourceFile = this.project.getSourceFile(filePath)
if (!sourceFile) return []
// Get all source files that reference this file
const referencingFiles = sourceFile.getReferencingSourceFiles()
// Filter to only those that actually import from this file
return referencingFiles.filter((file) => {
const imports = file.getImportDeclarations()
return imports.some((imp) => this.isImportFromFile(imp, filePath))
})
}
/**
* Finds all files that re-export from the specified file
*/
private findFilesReExporting(filePath: string): SourceFile[] {
const allFiles = this.project.getSourceFiles()
return allFiles.filter((file) => {
const exports = file.getExportDeclarations()
return exports.some((exp) => this.isExportFromFile(exp, filePath))
})
}
/**
* Updates import paths in a file
*/
private async updateImportPath(
file: SourceFile,
symbolName: string,
oldPath: string,
newPath: string,
): Promise<void> {
const imports = file.getImportDeclarations()
for (const importDecl of imports) {
if (!this.isImportFromFile(importDecl, oldPath)) {
continue
}
// Check if this import includes the moved symbol
const namedImports = importDecl.getNamedImports()
const hasSymbol = namedImports.some((imp) => imp.getName() === symbolName)
if (!hasSymbol) {
continue
}
// Calculate new relative path
const newRelativePath = this.calculateRelativePath(file.getFilePath(), newPath)
// Update the module specifier
importDecl.setModuleSpecifier(newRelativePath)
// Check if we need to keep the old import for other symbols
const otherImports = namedImports.filter((imp) => imp.getName() !== symbolName)
if (otherImports.length > 0) {
// Remove only the moved symbol from the import
const symbolImport = namedImports.find((imp) => imp.getName() === symbolName)
symbolImport?.remove()
// Add a new import for the moved symbol
this.addImport(file, symbolName, newRelativePath)
}
}
}
/**
* Updates re-export paths in a file
*/
private async updateReExportPath(
file: SourceFile,
symbolName: string,
oldPath: string,
newPath: string,
): Promise<void> {
const exports = file.getExportDeclarations()
for (const exportDecl of exports) {
if (!this.isExportFromFile(exportDecl, oldPath)) {
continue
}
// Check if this export includes the moved symbol
const namedExports = exportDecl.getNamedExports()
const hasSymbol = namedExports.some((exp) => exp.getName() === symbolName)
if (!hasSymbol) {
continue
}
// Calculate new relative path
const newRelativePath = this.calculateRelativePath(file.getFilePath(), newPath)
// Update the module specifier
exportDecl.setModuleSpecifier(newRelativePath)
// Check if we need to keep the old export for other symbols
const otherExports = namedExports.filter((exp) => exp.getName() !== symbolName)
if (otherExports.length > 0) {
// Remove only the moved symbol from the export
const symbolExport = namedExports.find((exp) => exp.getName() === symbolName)
symbolExport?.remove()
// Add a new export for the moved symbol
this.addReExport(file, symbolName, newRelativePath)
}
}
}
/**
* Adds missing imports to the new file
*/
private async addMissingImports(newFile: SourceFile, movedSymbolName: string, oldFilePath: string): Promise<void> {
// Get the moved symbol's dependencies from the old file
const oldFile = this.project.getSourceFile(oldFilePath)
if (!oldFile) return
// Find all symbols that the moved symbol depends on
const dependencies = this.findSymbolDependencies(oldFile, movedSymbolName)
for (const dep of dependencies) {
// Check if the dependency is already imported in the new file
if (!this.hasImport(newFile, dep.name)) {
// Add the import
if (dep.isLocal) {
// Local import from the old file
const relativePath = this.calculateRelativePath(newFile.getFilePath(), oldFilePath)
this.addImport(newFile, dep.name, relativePath)
} else {
// External import - copy the import statement
this.copyImport(newFile, oldFile, dep.name)
}
}
}
}
/**
* Checks if an import declaration is from the specified file
*/
private isImportFromFile(importDecl: ImportDeclaration, filePath: string): boolean {
const moduleSpecifier = importDecl.getModuleSpecifierValue()
const resolvedPath = this.resolveModulePath(importDecl.getSourceFile().getFilePath(), moduleSpecifier)
return this.pathsMatch(resolvedPath, filePath)
}
/**
* Checks if an export declaration is from the specified file
*/
private isExportFromFile(exportDecl: ExportDeclaration, filePath: string): boolean {
const moduleSpecifier = exportDecl.getModuleSpecifierValue()
if (!moduleSpecifier) return false
const resolvedPath = this.resolveModulePath(exportDecl.getSourceFile().getFilePath(), moduleSpecifier)
return this.pathsMatch(resolvedPath, filePath)
}
/**
* Calculates relative path between two files
*/
private calculateRelativePath(fromPath: string, toPath: string): string {
const fromDir = path.dirname(fromPath)
let relativePath = path.relative(fromDir, toPath)
// Remove file extension
relativePath = relativePath.replace(/\.(ts|tsx|js|jsx)$/, "")
// Ensure it starts with ./ or ../
if (!relativePath.startsWith(".")) {
relativePath = "./" + relativePath
}
return relativePath
}
/**
* Resolves a module path to an absolute path
*/
private resolveModulePath(fromPath: string, moduleSpecifier: string): string {
if (!moduleSpecifier.startsWith(".")) {
// External module
return moduleSpecifier
}
const fromDir = path.dirname(fromPath)
const resolved = path.resolve(fromDir, moduleSpecifier)
// Try with different extensions
const extensions = [".ts", ".tsx", ".js", ".jsx"]
for (const ext of extensions) {
if (resolved.endsWith(ext)) {
return resolved
}
const withExt = resolved + ext
if (this.project.getSourceFile(withExt)) {
return withExt
}
}
return resolved
}
/**
* Checks if two paths refer to the same file
*/
private pathsMatch(path1: string, path2: string): boolean {
// Normalize paths and remove extensions
const normalize = (p: string) => {
return p.replace(/\\/g, "/").replace(/\.(ts|tsx|js|jsx)$/, "")
}
return normalize(path1) === normalize(path2)
}
/**
* Finds dependencies of a symbol
*/
private findSymbolDependencies(file: SourceFile, symbolName: string): Array<{ name: string; isLocal: boolean }> {
// This is a simplified implementation
// In reality, we'd need to analyze the AST to find actual dependencies
const dependencies: Array<{ name: string; isLocal: boolean }> = []
// For now, return empty array
// Full implementation would analyze the symbol's body for references
return dependencies
}
/**
* Checks if a file already imports a symbol
*/
private hasImport(file: SourceFile, symbolName: string): boolean {
const imports = file.getImportDeclarations()
return imports.some((imp) => {
const namedImports = imp.getNamedImports()
return namedImports.some((ni) => ni.getName() === symbolName)
})
}
/**
* Adds an import to a file
*/
private addImport(file: SourceFile, symbolName: string, modulePath: string): void {
// Check if we already have an import from this module
const existingImport = file.getImportDeclaration((imp) => imp.getModuleSpecifierValue() === modulePath)
if (existingImport) {
// Add to existing import
existingImport.addNamedImport(symbolName)
} else {
// Create new import
file.addImportDeclaration({
moduleSpecifier: modulePath,
namedImports: [symbolName],
})
}
}
/**
* Adds a re-export to a file
*/
private addReExport(file: SourceFile, symbolName: string, modulePath: string): void {
file.addExportDeclaration({
moduleSpecifier: modulePath,
namedExports: [symbolName],
})
}
/**
* Copies an import from one file to another
*/
private copyImport(toFile: SourceFile, fromFile: SourceFile, symbolName: string): void {
const imports = fromFile.getImportDeclarations()
for (const imp of imports) {
const namedImports = imp.getNamedImports()
const hasSymbol = namedImports.some((ni) => ni.getName() === symbolName)
if (hasSymbol) {
// Copy this import
toFile.addImportDeclaration({
moduleSpecifier: imp.getModuleSpecifierValue(),
namedImports: [symbolName],
})
break
}
}
}
/**
* Gets list of files that were updated
*/
getUpdatedFiles(): string[] {
return Array.from(this.updatedFiles)
}
/**
* Removes unused imports from a file
*/
removeUnusedImports(file: SourceFile): void {
file.fixUnusedIdentifiers()
}
}
File: src/core/tools/refactor-code/operations/move.ts
import { Project, SourceFile, Node } from "ts-morph"
import { MoveOperation, OperationResult } from "../types"
import { SymbolFinder } from "../utils/symbol-finder"
import { ImportManager } from "../utils/import-manager"
import { RefactorTransaction } from "../transaction"
import * as path from "path"
export async function executeMoveOperation(
project: Project,
operation: MoveOperation,
transaction: RefactorTransaction,
): Promise<OperationResult> {
try {
// Validate inputs
if (!operation.targetFilePath) {
return {
success: false,
error: "Target file path is required for move operation",
operation,
}
}
// Check if moving to the same file
if (operation.selector.filePath === operation.targetFilePath) {
return {
success: false,
error: "Cannot move symbol to the same file",
operation,
}
}
// Get source file
const sourceFile = project.getSourceFile(operation.selector.filePath)
if (!sourceFile) {
return {
success: false,
error: `Source file not found: ${operation.selector.filePath}`,
operation,
}
}
// Find the symbol
const finder = new SymbolFinder(sourceFile)
const symbol = finder.findSymbol(operation.selector)
if (!symbol) {
return {
success: false,
error: `Symbol '${operation.selector.name}' not found in ${operation.selector.filePath}`,
operation,
}
}
// Check if symbol is moveable (only top-level symbols)
if (!isTopLevelSymbol(symbol)) {
return {
success: false,
error: `Symbol '${operation.selector.name}' is not a top-level symbol and cannot be moved`,
operation,
}
}
// Check if symbol is exported
const isExported = finder.isExported(symbol)
// Get or create target file
let targetFile = project.getSourceFile(operation.targetFilePath)
if (!targetFile) {
// Create the target file
targetFile = project.createSourceFile(operation.targetFilePath, "", {
overwrite: false,
})
}
// Snapshot both files
await transaction.snapshot(operation.selector.filePath)
await transaction.snapshot(operation.targetFilePath)
// Extract the symbol with its dependencies
const extracted = extractSymbolWithDependencies(symbol, sourceFile)
// Check for naming conflicts in target file
const conflictCheck = checkTargetFileConflicts(targetFile, operation.selector.name)
if (conflictCheck.hasConflict) {
return {
success: false,
error: `Naming conflict in target file: ${conflictCheck.message}`,
operation,
}
}
// Add to target file
addSymbolToFile(targetFile, extracted, isExported)
// Create import manager
const importManager = new ImportManager(project)
// Update all imports before removing the symbol
await importManager.updateImportsAfterMove(
operation.selector.name,
operation.selector.filePath,
operation.targetFilePath,
)
// Remove from source file
removeSymbolFromFile(symbol, sourceFile)
// Clean up empty imports in source file
importManager.removeUnusedImports(sourceFile)
// Format both files
sourceFile.formatText()
targetFile.formatText()
// Save all affected files
await project.save()
// Get all affected files
const affectedFiles = new Set<string>([
operation.selector.filePath,
operation.targetFilePath,
...importManager.getUpdatedFiles(),
])
// Record the operation
transaction.recordOperation({
id: operation.id || "move-" + Date.now(),
type: "move",
undo: () => {
// In a real implementation, we'd move the symbol back
},
})
return {
success: true,
operation,
affectedFiles: Array.from(affectedFiles),
message: `Successfully moved '${operation.selector.name}' to ${operation.targetFilePath}`,
}
} catch (error) {
return {
success: false,
error: `Move operation failed: ${error.message}`,
operation,
}
}
}
function isTopLevelSymbol(symbol: Node): boolean {
// Check if the symbol is at the top level of the file
const parent = symbol.getParent()
return parent?.getKind() === SyntaxKind.SourceFile
}
interface ExtractedSymbol {
text: string
imports: string[]
comments: string
}
function extractSymbolWithDependencies(symbol: Node, sourceFile: SourceFile): ExtractedSymbol {
// Get leading comments
const leadingComments = symbol
.getLeadingCommentRanges()
.map((range) => sourceFile.getFullText().slice(range.getPos(), range.getEnd()))
.join("\n")
// Get the symbol text
const symbolText = symbol.getFullText()
// Get required imports (simplified - in reality we'd analyze dependencies)
const imports: string[] = []
// If the symbol has type annotations, we might need type imports
// This is a simplified implementation
return {
text: symbolText.trim(),
imports,
comments: leadingComments.trim(),
}
}
function checkTargetFileConflicts(
targetFile: SourceFile,
symbolName: string,
): { hasConflict: boolean; message?: string } {
// Check for existing symbols with the same name
const finder = new SymbolFinder(targetFile)
const existingSymbol = finder.findSymbol({
type: "identifier",
name: symbolName,
filePath: targetFile.getFilePath(),
})
if (existingSymbol) {
return {
hasConflict: true,
message: `Symbol '${symbolName}' already exists in target file`,
}
}
return { hasConflict: false }
}
function addSymbolToFile(targetFile: SourceFile, extracted: ExtractedSymbol, isExported: boolean): void {
// Build the full text to add
let textToAdd = ""
// Add comments if any
if (extracted.comments) {
textToAdd += extracted.comments + "\n"
}
// Add export keyword if needed
if (isExported && !extracted.text.trim().startsWith("export")) {
textToAdd += "export "
}
// Add the symbol
textToAdd += extracted.text
// Add to the end of the file with proper spacing
const existingText = targetFile.getFullText()
if (existingText.trim()) {
textToAdd = "\n\n" + textToAdd
}
targetFile.addStatements(textToAdd)
}
function removeSymbolFromFile(symbol: Node, sourceFile: SourceFile): void {
// Remove the symbol and its leading comments
const leadingComments = symbol.getLeadingCommentRanges()
if (leadingComments.length > 0) {
// Get the start of the first comment
const firstCommentStart = leadingComments[0].getPos()
const symbolEnd = symbol.getEnd()
// Remove from first comment to end of symbol
sourceFile.removeText(firstCommentStart, symbolEnd - firstCommentStart)
} else {
// Just remove the symbol
symbol.remove()
}
// Clean up extra newlines
const text = sourceFile.getFullText()
const cleanedText = text.replace(/\n{3,}/g, "\n\n")
if (text !== cleanedText) {
sourceFile.replaceWithText(cleanedText)
}
}
File: src/core/tools/refactor-code/utils/dependency-analyzer.ts
import { RefactorOperation } from "../schema"
export interface OperationDependency {
operation: RefactorOperation
dependsOn: RefactorOperation[]
}
export interface DependencyGraph {
nodes: Map<string, RefactorOperation>
edges: Map<string, Set<string>> // operation id -> set of dependency ids
}
export class DependencyAnalyzer {
/**
* Analyzes dependencies between operations
*/
analyzeDependencies(operations: RefactorOperation[]): OperationDependency[] {
const dependencies: OperationDependency[] = []
for (let i = 0; i < operations.length; i++) {
const operation = operations[i]
const deps: RefactorOperation[] = []
for (let j = 0; j < i; j++) {
const earlierOp = operations[j]
if (this.operationsDependOn(operation, earlierOp)) {
deps.push(earlierOp)
}
}
dependencies.push({
operation,
dependsOn: deps,
})
}
return dependencies
}
/**
* Sorts operations based on dependencies
*/
sortOperations(operations: RefactorOperation[]): RefactorOperation[] {
const graph = this.buildDependencyGraph(operations)
const sorted = this.topologicalSort(graph)
if (!sorted) {
throw new Error("Circular dependency detected in refactoring operations")
}
return sorted
}
/**
* Checks if two operations have a dependency relationship
*/
private operationsDependOn(op1: RefactorOperation, op2: RefactorOperation): boolean {
// Check if operations affect the same file
if (this.affectsSameFile(op1, op2)) {
return true
}
// Check if one operation depends on the result of another
if (this.isDependentOn(op1, op2)) {
return true
}
return false
}
/**
* Checks if operations affect the same file
*/
private affectsSameFile(op1: RefactorOperation, op2: RefactorOperation): boolean {
const files1 = this.getAffectedFiles(op1)
const files2 = this.getAffectedFiles(op2)
// Check for intersection
return files1.some((f) => files2.includes(f))
}
/**
* Gets files affected by an operation
*/
private getAffectedFiles(op: RefactorOperation): string[] {
const files: string[] = []
// Add source file
if ("selector" in op && op.selector && "filePath" in op.selector) {
files.push(op.selector.filePath)
}
// Add target file for move operations
if (op.operation === "move" && "targetFilePath" in op) {
files.push(op.targetFilePath)
}
// Add target file for add operations
if (op.operation === "add" && "targetFilePath" in op) {
files.push(op.targetFilePath)
}
return files
}
/**
* Checks if one operation depends on another
*/
private isDependentOn(op1: RefactorOperation, op2: RefactorOperation): boolean {
// Example: If op2 renames a symbol that op1 tries to move
if (op2.operation === "rename" && op1.operation === "move") {
if ("selector" in op1 && "selector" in op2) {
// If op2 renamed the symbol that op1 is trying to move
if (op2.selector.name === op1.selector.name && op2.selector.filePath === op1.selector.filePath) {
return true
}
}
}
// Example: If op2 moves a symbol that op1 tries to rename
if (op2.operation === "move" && op1.operation === "rename") {
if ("selector" in op1 && "selector" in op2) {
// If op2 moved the symbol that op1 is trying to rename
if (op2.selector.name === op1.selector.name) {
// op1 should use the new file path
return true
}
}
}
return false
}
/**
* Builds a dependency graph
*/
private buildDependencyGraph(operations: RefactorOperation[]): DependencyGraph {
const nodes = new Map<string, RefactorOperation>()
const edges = new Map<string, Set<string>>()
// Ensure all operations have IDs
operations.forEach((op, index) => {
const id = op.id || `op-${index}`
nodes.set(id, { ...op, id })
edges.set(id, new Set())
})
// Build edges based on dependencies
const nodeArray = Array.from(nodes.entries())
for (let i = 0; i < nodeArray.length; i++) {
const [id1, op1] = nodeArray[i]
for (let j = i + 1; j < nodeArray.length; j++) {
const [id2, op2] = nodeArray[j]
if (this.operationsDependOn(op2, op1)) {
// op2 depends on op1
edges.get(id2)!.add(id1)
}
}
}
return { nodes, edges }
}
/**
* Performs topological sort on the dependency graph
*/
private topologicalSort(graph: DependencyGraph): RefactorOperation[] | null {
const { nodes, edges } = graph
const sorted: RefactorOperation[] = []
const visited = new Set<string>()
const visiting = new Set<string>()
// Helper function for DFS
const visit = (nodeId: string): boolean => {
if (visiting.has(nodeId)) {
// Circular dependency detected
return false
}
if (visited.has(nodeId)) {
return true
}
visiting.add(nodeId)
// Visit dependencies first
const dependencies = edges.get(nodeId) || new Set()
for (const depId of dependencies) {
if (!visit(depId)) {
return false
}
}
visiting.delete(nodeId)
visited.add(nodeId)
sorted.push(nodes.get(nodeId)!)
return true
}
// Visit all nodes
for (const nodeId of nodes.keys()) {
if (!visit(nodeId)) {
return null // Circular dependency
}
}
return sorted
}
/**
* Detects circular dependencies
*/
detectCircularDependencies(operations: RefactorOperation[]): string[][] {
const graph = this.buildDependencyGraph(operations)
const cycles: string[][] = []
// Use DFS to detect cycles
const visited = new Set<string>()
const recStack = new Set<string>()
const path: string[] = []
const findCycles = (nodeId: string): void => {
visited.add(nodeId)
recStack.add(nodeId)
path.push(nodeId)
const dependencies = graph.edges.get(nodeId) || new Set()
for (const depId of dependencies) {
if (!visited.has(depId)) {
findCycles(depId)
} else if (recStack.has(depId)) {
// Found a cycle
const cycleStart = path.indexOf(depId)
cycles.push(path.slice(cycleStart))
}
}
path.pop()
recStack.delete(nodeId)
}
for (const nodeId of graph.nodes.keys()) {
if (!visited.has(nodeId)) {
findCycles(nodeId)
}
}
return cycles
}
}
File: src/core/tools/refactor-code/engine.ts (updates)
import { executeMoveOperation } from './operations/move';
import { ImportManager } from './utils/import-manager';
import { DependencyAnalyzer } from './utils/dependency-analyzer';
// Add to the RefactorEngine class:
private importManager: ImportManager;
private dependencyAnalyzer: DependencyAnalyzer;
constructor(tsConfigPath?: string) {
// ... existing constructor code ...
this.importManager = new ImportManager(this.project);
this.dependencyAnalyzer = new DependencyAnalyzer();
}
async executeOperation(operation: RefactorOperation): Promise<OperationResult> {
// ... existing code ...
switch (operation.operation) {
// ... existing cases ...
case 'move':
result = await executeMoveOperation(
this.project,
operation,
this.transaction
);
break;
// ... rest of cases ...
}
// ... rest of method ...
}
async executeBatch(operations: RefactorOperation[]): Promise<BatchResult> {
// Sort operations based on dependencies
let sortedOperations: RefactorOperation[];
try {
sortedOperations = this.dependencyAnalyzer.sortOperations(operations);
} catch (error) {
if (error.message.includes('Circular dependency')) {
const cycles = this.dependencyAnalyzer.detectCircularDependencies(operations);
return {
success: false,
error: `Circular dependencies detected: ${JSON.stringify(cycles)}`,
operations: [],
totalOperations: operations.length,
successfulOperations: 0,
failedOperations: operations.length,
affectedFiles: [],
duration: 0
};
}
throw error;
}
// Execute sorted operations
const results: OperationResult[] = [];
const startTime = Date.now();
for (const operation of sortedOperations) {
const result = await this.executeOperation(operation);
results.push(result);
if (!result.success && operation.stopOnError !== false) {
break;
}
}
// ... rest of method ...
}
src/core/tools/refactor-code/__tests__/fixtures/move/
├── simple-function/
│ ├── input/
│ │ ├── tsconfig.json
│ │ └── src/
│ │ ├── source.ts
│ │ ├── target.ts (may not exist)
│ │ └── consumer.ts
│ ├── expected/
│ │ └── src/
│ │ ├── source.ts
│ │ ├── target.ts
│ │ └── consumer.ts
│ └── operation.json
├── with-imports/
│ ├── input/
│ ├── expected/
│ └── operation.json
├── create-new-file/
│ ├── input/
│ ├── expected/
│ └── operation.json
└── update-exports/
├── input/
├── expected/
└── operation.json
File: fixtures/move/simple-function/input/src/source.ts
import { helperFunction } from "./helpers"
export function functionToMove(value: string): string {
return helperFunction(value.toUpperCase())
}
export function stayingFunction(): void {
console.log("This stays")
}
File: fixtures/move/simple-function/input/src/consumer.ts
import { functionToMove, stayingFunction } from "./source"
const result = functionToMove("test")
stayingFunction()
File: fixtures/move/simple-function/operation.json
{
"operation": "move",
"selector": {
"type": "identifier",
"name": "functionToMove",
"kind": "function",
"filePath": "src/source.ts"
},
"targetFilePath": "src/target.ts",
"reason": "Better organization"
}
File: fixtures/move/simple-function/expected/src/source.ts
import { helperFunction } from "./helpers"
export function stayingFunction(): void {
console.log("This stays")
}
File: fixtures/move/simple-function/expected/src/target.ts
import { helperFunction } from "./helpers"
export function functionToMove(value: string): string {
return helperFunction(value.toUpperCase())
}
File: fixtures/move/simple-function/expected/src/consumer.ts
import { functionToMove } from "./target"
import { stayingFunction } from "./source"
const result = functionToMove("test")
stayingFunction()
File: src/core/tools/refactor-code/__tests__/operations/move.test.ts
import { RefactorEngine } from "../../engine"
import { runSnapshotTest } from "../helpers/snapshot-testing"
import * as path from "path"
import * as fs from "fs"
describe("MOVE Operation", () => {
const fixturesDir = path.join(__dirname, "../fixtures/move")
const testCases = fs
.readdirSync(fixturesDir)
.filter((dir) => fs.statSync(path.join(fixturesDir, dir)).isDirectory())
testCases.forEach((testCase) => {
test(`move: ${testCase}`, async () => {
await runSnapshotTest(path.join(fixturesDir, testCase))
})
})
test("should update imports in multiple files", async () => {
// Specific test for complex import scenarios
})
test("should handle circular imports", async () => {
// Test for circular import handling
})
test("should create target file if it does not exist", async () => {
// Test file creation
})
})
File: src/core/tools/refactor-code/__tests__/utils/import-manager.test.ts
import { ImportManager } from "../../utils/import-manager"
import { Project } from "ts-morph"
describe("ImportManager", () => {
let project: Project
let importManager: ImportManager
beforeEach(() => {
project = new Project({ useInMemoryFileSystem: true })
importManager = new ImportManager(project)
})
test("should update import paths after move", async () => {
// Create test files
const sourceFile = project.createSourceFile(
"src/source.ts",
`
export function myFunction() {}
`,
)
const consumerFile = project.createSourceFile(
"src/consumer.ts",
`
import { myFunction } from './source';
myFunction();
`,
)
// Move the function
await importManager.updateImportsAfterMove("myFunction", "src/source.ts", "src/utils/target.ts")
// Check the import was updated
const updatedConsumer = consumerFile.getFullText()
expect(updatedConsumer).toContain("from './utils/target'")
})
test("should handle re-exports", async () => {
// Test re-export handling
})
test("should preserve other imports when updating", async () => {
// Test that other imports are not affected
})
})
File: src/core/tools/refactor-code/__tests__/utils/dependency-analyzer.test.ts
import { DependencyAnalyzer } from "../../utils/dependency-analyzer"
import { RefactorOperation } from "../../schema"
describe("DependencyAnalyzer", () => {
const analyzer = new DependencyAnalyzer()
test("should detect file-based dependencies", () => {
const operations: RefactorOperation[] = [
{
operation: "rename",
selector: {
type: "identifier",
name: "oldName",
kind: "function",
filePath: "file1.ts",
},
newName: "newName",
reason: "Test",
},
{
operation: "move",
selector: {
type: "identifier",
name: "oldName", // Should depend on rename
kind: "function",
filePath: "file1.ts",
},
targetFilePath: "file2.ts",
reason: "Test",
},
]
const sorted = analyzer.sortOperations(operations)
// Rename should come before move
expect(sorted[0].operation).toBe("rename")
expect(sorted[1].operation).toBe("move")
})
test("should detect circular dependencies", () => {
const operations: RefactorOperation[] = [
{
id: "op1",
operation: "move",
selector: {
type: "identifier",
name: "func1",
kind: "function",
filePath: "file1.ts",
},
targetFilePath: "file2.ts",
reason: "Test",
},
{
id: "op2",
operation: "move",
selector: {
type: "identifier",
name: "func2",
kind: "function",
filePath: "file2.ts",
},
targetFilePath: "file1.ts",
reason: "Test",
},
]
expect(() => analyzer.sortOperations(operations)).toThrow("Circular dependency")
})
})
src/core/tools/refactor-code/utils/import-manager.tssrc/core/tools/refactor-code/operations/move.tssrc/core/tools/refactor-code/utils/dependency-analyzer.tsSolution: Ensure all files are loaded in the project and use proper path resolution.
Solution: Detect and warn about potential circular imports before executing.
Solution: Extract full text including leading/trailing trivia.
Solution: Use dependency analyzer to order operations correctly.
After completing Phase 3: