refactorCodeTool.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. import * as path from "path"
  2. import { ClineSayTool } from "../../shared/ExtensionMessage"
  3. import { Task } from "../task/Task"
  4. import { ToolUse, RemoveClosingTag } from "../../shared/tools"
  5. import { formatResponse } from "../prompts/responses"
  6. import { AskApproval, HandleError, PushToolResult } from "../../shared/tools"
  7. import { fileExistsAtPath } from "../../utils/fs"
  8. import { RecordSource } from "../context-tracking/FileContextTrackerTypes"
  9. import { Project } from "ts-morph"
  10. import { RefactorEngine, RefactorEngineError } from "./refactor-code/engine"
  11. import { RobustLLMRefactorParser, RefactorParseError } from "./refactor-code/parser"
  12. import { BatchOperations } from "./refactor-code/schema"
  13. import { createDiagnostic } from "./refactor-code/utils/file-system"
  14. import { checkpointSave, checkpointRestore } from "../checkpoints"
  15. /**
  16. * Refactor code tool implementation
  17. *
  18. * This tool uses a powerful AST-based engine to perform code refactoring operations
  19. * with robust validation, error handling, and recovery mechanisms. It supports
  20. * batch operations for performing multiple refactorings as a single atomic unit.
  21. *
  22. * IMPORTANT GUIDELINES:
  23. * - ALL operations must be provided in an array format, even single operations
  24. * - Each operation in the batch is processed independently
  25. * - If any operation fails, the entire batch will be rolled back by default
  26. * - Path handling is consistent across all operations
  27. * - Import dependencies are automatically managed during move operations
  28. *
  29. * Supported operations:
  30. * 1. Move: Move code elements from one file to another with automatic import handling
  31. * 2. Rename: Rename symbols with proper reference handling across the project
  32. * 3. Remove: Remove code elements with safe validation and cleanup
  33. *
  34. * Error handling has been significantly improved with:
  35. * - Detailed validation before operations are executed
  36. * - Comprehensive error messages with actionable information
  37. * - Recovery mechanisms for common failure scenarios
  38. * - Path normalization to prevent file resolution issues
  39. */
  40. export async function refactorCodeTool(
  41. cline: Task,
  42. block: ToolUse,
  43. askApproval: AskApproval,
  44. handleError: HandleError,
  45. pushToolResult: PushToolResult,
  46. ) {
  47. // Extract operations from the parameters
  48. const operationsJson: string | undefined = block.params.operations
  49. // Tool message properties
  50. const sharedMessageProps: ClineSayTool = {
  51. tool: "refactorCode",
  52. path: "",
  53. content: "",
  54. }
  55. try {
  56. // Handle partial execution
  57. if (block.partial) {
  58. await cline.ask("tool", JSON.stringify(sharedMessageProps), block.partial).catch(() => {})
  59. return
  60. }
  61. // Verify required operations parameter
  62. if (!operationsJson) {
  63. cline.consecutiveMistakeCount++
  64. cline.recordToolError("refactor_code")
  65. pushToolResult(await cline.sayAndCreateMissingParamError("refactor_code", "operations"))
  66. return
  67. }
  68. // DIAGNOSTIC: Log current directories to help debug path issues
  69. console.log(`[DIAGNOSTIC] Working directory (cline.cwd): "${cline.cwd}"`)
  70. console.log(`[DIAGNOSTIC] Process directory (process.cwd()): "${process.cwd()}"`)
  71. // Create diagnostic function
  72. const diagnose = createDiagnostic(cline.cwd)
  73. // Initialize the RefactorEngine
  74. const engine = new RefactorEngine({
  75. projectRootPath: cline.cwd,
  76. })
  77. // Parse the operations
  78. let operations: BatchOperations
  79. try {
  80. // Parse the operations using the robust parser
  81. let parser = new RobustLLMRefactorParser()
  82. let parsedOperations: any[]
  83. try {
  84. // Attempt to parse the raw operations
  85. parsedOperations = parser.parseResponse(operationsJson)
  86. } catch (parseError) {
  87. // If parsing fails, try to directly parse as JSON without the parser's extra logic
  88. try {
  89. // The input might already be a JSON array
  90. const directJson = JSON.parse(operationsJson as string)
  91. parsedOperations = Array.isArray(directJson) ? directJson : [directJson]
  92. } catch (jsonError) {
  93. // If direct parsing also fails, throw the original error
  94. throw parseError
  95. }
  96. }
  97. // Create a batch operations object
  98. operations = {
  99. operations: parsedOperations,
  100. options: {
  101. stopOnError: true,
  102. },
  103. }
  104. } catch (error) {
  105. const err = error as Error
  106. cline.consecutiveMistakeCount++
  107. cline.recordToolError("refactor_code")
  108. const formattedError = `Failed to parse refactor operations: ${err.message}`
  109. await cline.say("error", formattedError)
  110. pushToolResult(formattedError)
  111. return
  112. }
  113. // Validate all file paths exist and are accessible
  114. const filesToCheck = new Set<string>()
  115. for (const op of operations.operations) {
  116. if ("filePath" in op.selector) {
  117. filesToCheck.add(op.selector.filePath)
  118. }
  119. // Check target file for move operations
  120. if (op.operation === "move") {
  121. filesToCheck.add(op.targetFilePath)
  122. }
  123. }
  124. for (const filePath of filesToCheck) {
  125. // Verify path is accessible
  126. const accessAllowed = cline.rooIgnoreController?.validateAccess(filePath)
  127. if (!accessAllowed) {
  128. await cline.say("rooignore_error", filePath)
  129. pushToolResult(formatResponse.toolError(formatResponse.rooIgnoreError(filePath)))
  130. return
  131. }
  132. // For source files, verify the file exists
  133. // (but don't check for target files in move operations, as they may not exist yet)
  134. const isTargetFile = operations.operations.some(
  135. (op) => op.operation === "move" && op.targetFilePath === filePath,
  136. )
  137. if (!isTargetFile) {
  138. // Verify file exists for source files
  139. const absolutePath = path.resolve(cline.cwd, filePath)
  140. const fileExists = await fileExistsAtPath(absolutePath)
  141. // DIAGNOSTIC: Log file existence check
  142. console.log(
  143. `[DIAGNOSTIC] File check - Path: "${filePath}", Absolute: "${absolutePath}", Exists: ${fileExists}`,
  144. )
  145. if (!fileExists) {
  146. // Run diagnostic on this file
  147. await diagnose(filePath, "File existence check")
  148. cline.consecutiveMistakeCount++
  149. cline.recordToolError("refactor_code")
  150. const formattedError = `File does not exist at path: ${filePath}\n\n<error_details>\nThe specified file could not be found. Please verify the file path is relative to the workspace directory: ${cline.cwd}\nResolved absolute path: ${absolutePath}\n</error_details>`
  151. await cline.say("error", formattedError)
  152. pushToolResult(formattedError)
  153. return
  154. }
  155. }
  156. }
  157. // Create human-readable operation description for approval with validation info
  158. let operationDescription = `Batch refactoring: ${operations.operations.length} operation${operations.operations.length > 1 ? "s" : ""}\n\n`
  159. for (let i = 0; i < operations.operations.length; i++) {
  160. const op = operations.operations[i]
  161. let description = `${i + 1}. `
  162. switch (op.operation) {
  163. case "rename":
  164. description += `Rename ${op.selector.name} to ${op.newName} in ${op.selector.filePath}`
  165. break
  166. case "move":
  167. description += `Move ${op.selector.name} from ${op.selector.filePath} to ${op.targetFilePath}`
  168. break
  169. case "remove":
  170. description += `Remove ${op.selector.name} from ${op.selector.filePath}`
  171. break
  172. default:
  173. description += `Unsupported operation: ${op.operation}`
  174. }
  175. if (op.reason) {
  176. description += ` (Reason: ${op.reason})`
  177. }
  178. operationDescription += `${description}\n`
  179. }
  180. // Add note about validation and rollback behavior
  181. operationDescription += `\n${operations.options?.stopOnError ? "NOTE: If any operation fails, the entire batch will be automatically rolled back to preserve file integrity." : "NOTE: Operations will continue executing even if some fail."}`
  182. operationDescription += `\nAll imports and references will be automatically updated.`
  183. // Ask for approval before performing refactoring
  184. const approvalMessage = JSON.stringify({
  185. ...sharedMessageProps,
  186. content: operationDescription,
  187. } satisfies ClineSayTool)
  188. const didApprove = await askApproval("tool", approvalMessage)
  189. if (!didApprove) {
  190. pushToolResult("Refactoring cancelled by user")
  191. return
  192. }
  193. // Create checkpoint before executing batch operations
  194. console.log("[REFACTOR] Creating checkpoint before batch operations")
  195. const checkpointTimestamp = Date.now()
  196. const checkpointResult = await checkpointSave(cline)
  197. // Execute the batch operations
  198. let result
  199. try {
  200. // DIAGNOSTIC: Log file state before operation
  201. for (const filePath of filesToCheck) {
  202. await diagnose(filePath, "Before refactoring")
  203. }
  204. result = await engine.executeBatch(operations)
  205. // Track all modified files
  206. const modifiedFiles = new Set<string>()
  207. if (result.success) {
  208. for (const opResult of result.results) {
  209. for (const file of opResult.affectedFiles) {
  210. const absoluteFilePath = path.resolve(cline.cwd, file)
  211. modifiedFiles.add(absoluteFilePath)
  212. await cline.fileContextTracker.trackFileContext(file, "roo_edited" as RecordSource)
  213. }
  214. }
  215. }
  216. // DIAGNOSTIC: Log file state after operation
  217. for (const filePath of modifiedFiles) {
  218. await diagnose(filePath, "After refactoring")
  219. }
  220. } catch (error) {
  221. // Handle errors in batch execution
  222. const errorMessage = `Batch refactoring failed with error: ${(error as Error).message}`
  223. console.error(`[ERROR] ${errorMessage}`)
  224. console.log("[REFACTOR] Batch operation failed - automatically restoring files from checkpoint")
  225. // Automatically restore from checkpoint if available
  226. if (checkpointResult && cline.enableCheckpoints) {
  227. try {
  228. // Get the most recent checkpoint (the one we just created)
  229. const latestMessage = cline.clineMessages[cline.clineMessages.length - 1]
  230. if (latestMessage && latestMessage.ts && latestMessage.ts >= checkpointTimestamp) {
  231. await checkpointRestore(cline, {
  232. ts: latestMessage.ts,
  233. commitHash: latestMessage.ts.toString(), // Use timestamp as commit hash for simplicity
  234. mode: "restore",
  235. })
  236. console.log("[REFACTOR] Files successfully restored to pre-operation state")
  237. }
  238. } catch (restoreError) {
  239. console.error("[REFACTOR] Failed to restore checkpoint:", restoreError)
  240. // Continue with error reporting even if restore fails
  241. }
  242. }
  243. cline.consecutiveMistakeCount++
  244. cline.recordToolError("refactor_code", errorMessage)
  245. await cline.say(
  246. "error",
  247. `${errorMessage}\n\nBatch operation aborted. Your files remain in their original state.`,
  248. )
  249. pushToolResult(`${errorMessage}\n\nBatch operation aborted. Your files remain in their original state.`)
  250. return
  251. }
  252. // Format results with detailed diagnostic information
  253. const resultMessages: string[] = []
  254. for (let i = 0; i < result.results.length; i++) {
  255. const opResult = result.results[i]
  256. const op = result.allOperations[i]
  257. if (opResult.success) {
  258. let message = ""
  259. switch (op.operation) {
  260. case "rename":
  261. message = `Renamed ${op.selector.name} to ${op.newName} in ${op.selector.filePath}`
  262. break
  263. case "move":
  264. message = `Moved ${op.selector.name} from ${op.selector.filePath} to ${op.targetFilePath}`
  265. break
  266. case "remove":
  267. message = `Removed ${op.selector.name} from ${op.selector.filePath}`
  268. break
  269. default:
  270. message = `Executed ${op.operation} operation successfully`
  271. }
  272. resultMessages.push(`✓ ${message}`)
  273. // Add warnings if present
  274. if (opResult.warnings && opResult.warnings.length > 0) {
  275. resultMessages.push(` Warnings: ${opResult.warnings.join(", ")}`)
  276. }
  277. } else {
  278. resultMessages.push(`✗ Operation failed: ${opResult.error}`)
  279. // Add any diagnostic information for failed operations
  280. if (opResult.warnings && opResult.warnings.length > 0) {
  281. resultMessages.push(` Additional info: ${opResult.warnings.join(", ")}`)
  282. }
  283. }
  284. }
  285. // Report results
  286. const finalResult = resultMessages.join("\n")
  287. if (result.success) {
  288. cline.consecutiveMistakeCount = 0
  289. cline.didEditFile = true
  290. // Create checkpoint after successful completion
  291. console.log("[REFACTOR] Creating checkpoint after successful batch operations")
  292. await checkpointSave(cline)
  293. pushToolResult(`Batch refactoring completed successfully:\n\n${finalResult}`)
  294. } else {
  295. // Batch operation failed - automatically restore from checkpoint
  296. console.log("[REFACTOR] Batch operation failed - automatically restoring files from checkpoint")
  297. // Automatically restore from checkpoint if available
  298. if (checkpointResult && cline.enableCheckpoints) {
  299. try {
  300. // Get the most recent checkpoint (the one we just created)
  301. const latestMessage = cline.clineMessages[cline.clineMessages.length - 1]
  302. if (latestMessage && latestMessage.ts && latestMessage.ts >= checkpointTimestamp) {
  303. await checkpointRestore(cline, {
  304. ts: latestMessage.ts,
  305. commitHash: latestMessage.ts.toString(), // Use timestamp as commit hash for simplicity
  306. mode: "restore",
  307. })
  308. console.log("[REFACTOR] Files successfully restored to pre-operation state")
  309. }
  310. } catch (restoreError) {
  311. console.error("[REFACTOR] Failed to restore checkpoint:", restoreError)
  312. // Continue with error reporting even if restore fails
  313. }
  314. }
  315. cline.consecutiveMistakeCount++
  316. cline.recordToolError("refactor_code", result.error || finalResult)
  317. const failureMessage = `Batch refactoring failed:\n\n${result.error || finalResult}\n\nBatch operation aborted. Your files remain in their original state.`
  318. await cline.say("error", failureMessage)
  319. pushToolResult(failureMessage)
  320. }
  321. } catch (error) {
  322. await handleError("refactoring code", error)
  323. }
  324. }