Implement ADD and INLINE operations with code generation utilities. These operations enable adding new code elements (functions, classes, methods) and inlining existing code for optimization or refactoring purposes.
File: src/core/tools/refactor-code/utils/code-generator.ts
import {
Project,
SourceFile,
ClassDeclaration,
InterfaceDeclaration,
MethodDeclarationStructure,
PropertyDeclarationStructure,
FunctionDeclarationStructure,
VariableStatementStructure,
Scope,
} from "ts-morph"
export interface FunctionSpec {
name: string
parameters: ParameterSpec[]
returnType: string
body: string
isAsync?: boolean
isExported?: boolean
jsDoc?: string
}
export interface ClassSpec {
name: string
properties: PropertySpec[]
methods: MethodSpec[]
constructor?: ConstructorSpec
extends?: string
implements?: string[]
isExported?: boolean
isAbstract?: boolean
jsDoc?: string
}
export interface InterfaceSpec {
name: string
properties: PropertySpec[]
methods: MethodSpec[]
extends?: string[]
isExported?: boolean
jsDoc?: string
}
export interface PropertySpec {
name: string
type: string
accessibility?: "public" | "private" | "protected"
isReadonly?: boolean
isStatic?: boolean
isOptional?: boolean
initializer?: string
jsDoc?: string
}
export interface MethodSpec {
name: string
parameters: ParameterSpec[]
returnType: string
body?: string
accessibility?: "public" | "private" | "protected"
isAsync?: boolean
isStatic?: boolean
isAbstract?: boolean
jsDoc?: string
}
export interface ParameterSpec {
name: string
type: string
isOptional?: boolean
defaultValue?: string
isRest?: boolean
}
export interface ConstructorSpec {
parameters: ParameterSpec[]
body: string
accessibility?: "public" | "private" | "protected"
}
export class CodeGenerator {
private indentSize: number = 2
/**
* Generates a function declaration
*/
generateFunction(spec: FunctionSpec): string {
const parts: string[] = []
// Add JSDoc if provided
if (spec.jsDoc) {
parts.push(spec.jsDoc)
}
// Build function signature
const modifiers: string[] = []
if (spec.isExported) modifiers.push("export")
if (spec.isAsync) modifiers.push("async")
const params = this.generateParameters(spec.parameters)
const signature = `${modifiers.join(" ")} function ${spec.name}(${params}): ${spec.returnType}`
// Add body
const body = this.formatBody(spec.body)
parts.push(`${signature} {${body}}`)
return parts.join("\n")
}
/**
* Generates a class declaration
*/
generateClass(spec: ClassSpec): string {
const parts: string[] = []
// Add JSDoc if provided
if (spec.jsDoc) {
parts.push(spec.jsDoc)
}
// Build class signature
const modifiers: string[] = []
if (spec.isExported) modifiers.push("export")
if (spec.isAbstract) modifiers.push("abstract")
let signature = `${modifiers.join(" ")} class ${spec.name}`
if (spec.extends) {
signature += ` extends ${spec.extends}`
}
if (spec.implements && spec.implements.length > 0) {
signature += ` implements ${spec.implements.join(", ")}`
}
parts.push(signature + " {")
// Add properties
const classBody: string[] = []
for (const prop of spec.properties) {
classBody.push(this.generateProperty(prop))
}
if (spec.properties.length > 0 && (spec.constructor || spec.methods.length > 0)) {
classBody.push("") // Empty line
}
// Add constructor
if (spec.constructor) {
classBody.push(this.generateConstructor(spec.constructor))
if (spec.methods.length > 0) {
classBody.push("") // Empty line
}
}
// Add methods
for (let i = 0; i < spec.methods.length; i++) {
classBody.push(this.generateMethod(spec.methods[i]))
if (i < spec.methods.length - 1) {
classBody.push("") // Empty line between methods
}
}
// Indent class body
const indentedBody = classBody.map((line) => (line ? this.indent(line) : line)).join("\n")
parts.push(indentedBody)
parts.push("}")
return parts.join("\n")
}
/**
* Generates an interface declaration
*/
generateInterface(spec: InterfaceSpec): string {
const parts: string[] = []
// Add JSDoc if provided
if (spec.jsDoc) {
parts.push(spec.jsDoc)
}
// Build interface signature
const modifiers: string[] = []
if (spec.isExported) modifiers.push("export")
let signature = `${modifiers.join(" ")} interface ${spec.name}`
if (spec.extends && spec.extends.length > 0) {
signature += ` extends ${spec.extends.join(", ")}`
}
parts.push(signature + " {")
// Add properties and methods
const interfaceBody: string[] = []
for (const prop of spec.properties) {
const optional = prop.isOptional ? "?" : ""
const readonly = prop.isReadonly ? "readonly " : ""
interfaceBody.push(`${readonly}${prop.name}${optional}: ${prop.type};`)
}
if (spec.properties.length > 0 && spec.methods.length > 0) {
interfaceBody.push("") // Empty line
}
for (const method of spec.methods) {
const params = this.generateParameters(method.parameters)
interfaceBody.push(`${method.name}(${params}): ${method.returnType};`)
}
// Indent interface body
const indentedBody = interfaceBody.map((line) => (line ? this.indent(line) : line)).join("\n")
parts.push(indentedBody)
parts.push("}")
return parts.join("\n")
}
/**
* Generates a property declaration
*/
private generateProperty(spec: PropertySpec): string {
const parts: string[] = []
if (spec.jsDoc) {
parts.push(spec.jsDoc)
}
const modifiers: string[] = []
if (spec.accessibility) modifiers.push(spec.accessibility)
if (spec.isStatic) modifiers.push("static")
if (spec.isReadonly) modifiers.push("readonly")
const optional = spec.isOptional ? "?" : ""
const initializer = spec.initializer ? ` = ${spec.initializer}` : ""
const declaration = `${modifiers.join(" ")} ${spec.name}${optional}: ${spec.type}${initializer};`
parts.push(declaration)
return parts.join("\n")
}
/**
* Generates a method declaration
*/
private generateMethod(spec: MethodSpec): string {
const parts: string[] = []
if (spec.jsDoc) {
parts.push(spec.jsDoc)
}
const modifiers: string[] = []
if (spec.accessibility) modifiers.push(spec.accessibility)
if (spec.isStatic) modifiers.push("static")
if (spec.isAsync) modifiers.push("async")
if (spec.isAbstract) modifiers.push("abstract")
const params = this.generateParameters(spec.parameters)
const signature = `${modifiers.join(" ")} ${spec.name}(${params}): ${spec.returnType}`
if (spec.isAbstract || !spec.body) {
parts.push(signature + ";")
} else {
const body = this.formatBody(spec.body)
parts.push(`${signature} {${body}}`)
}
return parts.join("\n")
}
/**
* Generates a constructor declaration
*/
private generateConstructor(spec: ConstructorSpec): string {
const modifiers: string[] = []
if (spec.accessibility) modifiers.push(spec.accessibility)
const params = this.generateParameters(spec.parameters)
const signature = `${modifiers.join(" ")} constructor(${params})`
const body = this.formatBody(spec.body)
return `${signature} {${body}}`
}
/**
* Generates parameter list
*/
private generateParameters(parameters: ParameterSpec[]): string {
return parameters
.map((param) => {
const optional = param.isOptional ? "?" : ""
const rest = param.isRest ? "..." : ""
const defaultValue = param.defaultValue ? ` = ${param.defaultValue}` : ""
return `${rest}${param.name}${optional}: ${param.type}${defaultValue}`
})
.join(", ")
}
/**
* Formats a function/method body
*/
private formatBody(body: string): string {
if (!body.trim()) {
return "\n" + this.indent("// TODO: Implement") + "\n"
}
const lines = body.trim().split("\n")
const indented = lines.map((line) => this.indent(line)).join("\n")
return "\n" + indented + "\n"
}
/**
* Indents a string
*/
private indent(str: string, level: number = 1): string {
const spaces = " ".repeat(this.indentSize * level)
return spaces + str
}
/**
* Generates JSDoc comment
*/
generateJsDoc(
description: string,
params?: Array<{ name: string; description: string }>,
returns?: string,
): string {
const lines: string[] = ["/**", ` * ${description}`]
if (params && params.length > 0) {
lines.push(" *")
for (const param of params) {
lines.push(` * @param ${param.name} ${param.description}`)
}
}
if (returns) {
if (params && params.length > 0) {
lines.push(" *")
}
lines.push(` * @returns ${returns}`)
}
lines.push(" */")
return lines.join("\n")
}
}
File: src/core/tools/refactor-code/operations/add.ts
import { Project, SourceFile, ClassDeclaration } from "ts-morph"
import { AddOperation, OperationResult } from "../types"
import { CodeGenerator } from "../utils/code-generator"
import { RefactorTransaction } from "../transaction"
import { HumanReviewSystem } from "../human-review"
export async function executeAddOperation(
project: Project,
operation: AddOperation,
transaction: RefactorTransaction,
codeGenerator: CodeGenerator,
reviewSystem: HumanReviewSystem,
): Promise<OperationResult> {
try {
// Always generate review guide for add operations
const reviewGuide = reviewSystem.generateReviewGuide([operation])
// Validate inputs
if (!operation.symbolName) {
return {
success: false,
error: "Symbol name is required for add operation",
operation,
requiresReview: true,
reviewGuide: JSON.stringify(reviewGuide),
}
}
if (!operation.code && !operation.spec) {
return {
success: false,
error: "Either code or spec is required for add operation",
operation,
requiresReview: true,
reviewGuide: JSON.stringify(reviewGuide),
}
}
// Get or create target file
let targetFile = project.getSourceFile(operation.targetFilePath)
if (!targetFile) {
targetFile = project.createSourceFile(operation.targetFilePath, "", {
overwrite: false,
})
}
// Snapshot the file
await transaction.snapshot(operation.targetFilePath)
// Check for naming conflicts
const conflictCheck = checkNamingConflict(targetFile, operation.symbolName, operation.symbolType)
if (conflictCheck.hasConflict) {
return {
success: false,
error: conflictCheck.message!,
operation,
requiresReview: true,
reviewGuide: JSON.stringify(reviewGuide),
}
}
// Generate or use provided code
let codeToAdd: string
if (operation.code) {
codeToAdd = operation.code
} else if (operation.spec) {
codeToAdd = generateCodeFromSpec(codeGenerator, operation)
} else {
return {
success: false,
error: "No code or spec provided",
operation,
requiresReview: true,
reviewGuide: JSON.stringify(reviewGuide),
}
}
// Add the code based on symbol type and position
const result = await addCodeToFile(targetFile, operation, codeToAdd, transaction)
if (result.success) {
// Format the file
targetFile.formatText()
// Save the file
await targetFile.save()
}
return {
...result,
requiresReview: true,
reviewGuide: JSON.stringify(reviewGuide),
}
} catch (error) {
return {
success: false,
error: `Add operation failed: ${error.message}`,
operation,
requiresReview: true,
}
}
}
function checkNamingConflict(
file: SourceFile,
symbolName: string,
symbolType: string,
): { hasConflict: boolean; message?: string } {
switch (symbolType) {
case "function":
if (file.getFunction(symbolName)) {
return {
hasConflict: true,
message: `Function '${symbolName}' already exists in the file`,
}
}
break
case "class":
if (file.getClass(symbolName)) {
return {
hasConflict: true,
message: `Class '${symbolName}' already exists in the file`,
}
}
break
case "interface":
if (file.getInterface(symbolName)) {
return {
hasConflict: true,
message: `Interface '${symbolName}' already exists in the file`,
}
}
break
case "variable":
const varStatements = file.getVariableStatements()
for (const statement of varStatements) {
if (statement.getDeclarations().some((d) => d.getName() === symbolName)) {
return {
hasConflict: true,
message: `Variable '${symbolName}' already exists in the file`,
}
}
}
break
}
return { hasConflict: false }
}
function generateCodeFromSpec(codeGenerator: CodeGenerator, operation: AddOperation): string {
const spec = operation.spec!
switch (operation.symbolType) {
case "function":
return codeGenerator.generateFunction({
name: operation.symbolName,
parameters: spec.parameters || [],
returnType: spec.returnType || "void",
body: spec.body || "",
isAsync: spec.isAsync,
isExported: spec.isExported,
jsDoc: spec.jsDoc,
})
case "class":
return codeGenerator.generateClass({
name: operation.symbolName,
properties: spec.properties || [],
methods: spec.methods || [],
constructor: spec.constructor,
extends: spec.extends,
implements: spec.implements,
isExported: spec.isExported,
isAbstract: spec.isAbstract,
jsDoc: spec.jsDoc,
})
case "interface":
return codeGenerator.generateInterface({
name: operation.symbolName,
properties: spec.properties || [],
methods: spec.methods || [],
extends: spec.extends,
isExported: spec.isExported,
jsDoc: spec.jsDoc,
})
default:
return operation.code || ""
}
}
async function addCodeToFile(
file: SourceFile,
operation: AddOperation,
code: string,
transaction: RefactorTransaction,
): Promise<OperationResult> {
try {
if (operation.symbolType === "method" || operation.symbolType === "property") {
// Need to add to a class
if (!operation.parentSymbol) {
return {
success: false,
error: "Parent symbol is required for method/property",
operation,
}
}
const parentClass = file.getClass(operation.parentSymbol)
if (!parentClass) {
return {
success: false,
error: `Parent class '${operation.parentSymbol}' not found`,
operation,
}
}
if (operation.symbolType === "method") {
// Parse the method code and add it
// This is simplified - real implementation would parse the code properly
parentClass.addMethod({
name: operation.symbolName,
statements: code,
})
} else {
// Add property
parentClass.addProperty({
name: operation.symbolName,
type: "any", // Would be parsed from code
initializer: code,
})
}
} else {
// Add to file at specified position
const position = operation.position || "end"
switch (position) {
case "start":
file.insertText(0, code + "\n\n")
break
case "end":
file.addStatements(code)
break
case "before":
case "after":
// Would need additional logic to find the reference symbol
file.addStatements(code)
break
default:
file.addStatements(code)
}
}
// Record the operation
transaction.recordOperation({
id: operation.id || "add-" + Date.now(),
type: "add",
undo: () => {
// In a real implementation, we'd remove the added code
},
})
return {
success: true,
operation,
affectedFiles: [operation.targetFilePath],
message: `Successfully added ${operation.symbolType} '${operation.symbolName}'`,
}
} catch (error) {
return {
success: false,
error: `Failed to add code: ${error.message}`,
operation,
}
}
}
File: src/core/tools/refactor-code/operations/inline.ts
import { Project, SourceFile, Node, CallExpression, Identifier } from "ts-morph"
import { InlineOperation, OperationResult } from "../types"
import { SymbolFinder } from "../utils/symbol-finder"
import { RefactorTransaction } from "../transaction"
export async function executeInlineOperation(
project: Project,
operation: InlineOperation,
transaction: RefactorTransaction,
): Promise<OperationResult> {
try {
// 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 to inline
const finder = new SymbolFinder(sourceFile)
const symbol = finder.findSymbol(operation.selector)
if (!symbol) {
return {
success: false,
error: `Symbol '${operation.selector.name}' not found`,
operation,
}
}
// Get the value/implementation to inline
const inlineValue = extractInlineValue(symbol)
if (!inlineValue) {
return {
success: false,
error: `Cannot extract inline value for '${operation.selector.name}'`,
operation,
}
}
// Find all references to inline
const references = finder.getReferences(symbol)
if (references.length === 0) {
return {
success: false,
error: `No references found for '${operation.selector.name}'`,
operation,
}
}
// Snapshot all affected files
const affectedFiles = new Set<string>()
affectedFiles.add(operation.selector.filePath)
for (const ref of references) {
const refFile = ref.getSourceFile().getFilePath()
affectedFiles.add(refFile)
await transaction.snapshot(refFile)
}
// Perform the inlining
const inlineResults = performInlining(references, inlineValue)
if (!inlineResults.success) {
return {
success: false,
error: inlineResults.error,
operation,
}
}
// Remove the original declaration
removeOriginalDeclaration(symbol)
// Save all affected files
await project.save()
// Record the operation
transaction.recordOperation({
id: operation.id || "inline-" + Date.now(),
type: "inline",
undo: () => {
// In a real implementation, we'd restore the original symbol
},
})
return {
success: true,
operation,
affectedFiles: Array.from(affectedFiles),
message: `Successfully inlined '${operation.selector.name}' at ${references.length} location(s)`,
}
} catch (error) {
return {
success: false,
error: `Inline operation failed: ${error.message}`,
operation,
}
}
}
interface InlineValue {
type: "constant" | "function" | "expression"
value: string
isAsync?: boolean
parameters?: string[]
}
function extractInlineValue(symbol: Node): InlineValue | null {
// Handle variable declarations
if (Node.isVariableDeclaration(symbol)) {
const initializer = symbol.getInitializer()
if (!initializer) {
return null
}
// Check if it's a simple constant
if (
Node.isLiteralExpression(initializer) ||
Node.isIdentifier(initializer) ||
Node.isObjectLiteralExpression(initializer) ||
Node.isArrayLiteralExpression(initializer)
) {
return {
type: "constant",
value: initializer.getText(),
}
}
// Check if it's a function expression
if (Node.isFunctionExpression(initializer) || Node.isArrowFunction(initializer)) {
return {
type: "function",
value: initializer.getBody()?.getText() || "",
isAsync: initializer.isAsync(),
parameters: initializer.getParameters().map((p) => p.getName()),
}
}
// Other expressions
return {
type: "expression",
value: initializer.getText(),
}
}
// Handle function declarations
if (Node.isFunctionDeclaration(symbol)) {
const body = symbol.getBody()
if (!body) {
return null
}
return {
type: "function",
value: body.getText(),
isAsync: symbol.isAsync(),
parameters: symbol.getParameters().map((p) => p.getName()),
}
}
// Handle other symbol types
// This is simplified - real implementation would handle more cases
return null
}
interface InlineResult {
success: boolean
error?: string
}
function performInlining(references: Identifier[], inlineValue: InlineValue): InlineResult {
for (const ref of references) {
try {
const parent = ref.getParent()
if (inlineValue.type === "constant") {
// Simple replacement
ref.replaceWithText(inlineValue.value)
} else if (inlineValue.type === "function" && Node.isCallExpression(parent)) {
// Inline function call
const result = inlineFunctionCall(parent, inlineValue)
if (!result.success) {
return result
}
} else if (inlineValue.type === "expression") {
// Inline expression (may need parentheses)
const needsParens = shouldWrapInParentheses(ref)
const replacement = needsParens ? `(${inlineValue.value})` : inlineValue.value
ref.replaceWithText(replacement)
} else {
return {
success: false,
error: `Cannot inline ${inlineValue.type} at this location`,
}
}
} catch (error) {
return {
success: false,
error: `Failed to inline at reference: ${error.message}`,
}
}
}
return { success: true }
}
function inlineFunctionCall(callExpr: CallExpression, inlineValue: InlineValue): InlineResult {
if (!inlineValue.parameters || !inlineValue.value) {
return {
success: false,
error: "Invalid function inline value",
}
}
// Get the arguments from the call
const args = callExpr.getArguments()
// Build parameter mapping
const paramMapping = new Map<string, string>()
inlineValue.parameters.forEach((param, index) => {
if (args[index]) {
paramMapping.set(param, args[index].getText())
}
})
// Replace parameters in the function body
let inlinedBody = inlineValue.value
// Remove braces if it's a single expression
if (inlinedBody.startsWith("{") && inlinedBody.endsWith("}")) {
inlinedBody = inlinedBody.slice(1, -1).trim()
// Handle return statements
if (inlinedBody.startsWith("return ")) {
inlinedBody = inlinedBody.substring(7).trim()
if (inlinedBody.endsWith(";")) {
inlinedBody = inlinedBody.slice(0, -1)
}
}
}
// Replace parameters with arguments
for (const [param, arg] of paramMapping) {
// This is simplified - real implementation would use proper AST manipulation
const paramRegex = new RegExp(`\\b${param}\\b`, "g")
inlinedBody = inlinedBody.replace(paramRegex, arg)
}
// Handle async functions
if (inlineValue.isAsync) {
// Check if we're in an async context
const containingFunction = callExpr
.getAncestors()
.find((a) => Node.isFunctionDeclaration(a) || Node.isMethodDeclaration(a) || Node.isArrowFunction(a))
if (containingFunction && !containingFunction.isAsync()) {
return {
success: false,
error: "Cannot inline async function in non-async context",
}
}
inlinedBody = `await (${inlinedBody})`
}
// Replace the call expression
callExpr.replaceWithText(inlinedBody)
return { success: true }
}
function shouldWrapInParentheses(node: Node): boolean {
const parent = node.getParent()
if (!parent) return false
// Check if the parent is an operation that requires parentheses
if (Node.isBinaryExpression(parent) || Node.isConditionalExpression(parent) || Node.isCallExpression(parent)) {
return true
}
return false
}
function removeOriginalDeclaration(symbol: Node): void {
if (Node.isVariableDeclaration(symbol)) {
const statement = symbol.getVariableStatement()
if (statement) {
const declarations = statement.getDeclarations()
if (declarations.length === 1) {
// Remove the entire statement
statement.remove()
} else {
// Just remove this declaration
symbol.remove()
}
}
} else if (Node.isFunctionDeclaration(symbol)) {
symbol.remove()
} else {
// Handle other declaration types
symbol.remove()
}
}
src/core/tools/refactor-code/__tests__/fixtures/add/
├── add-function/
│ ├── input/
│ │ └── src/
│ │ └── utils.ts
│ ├── expected/
│ │ └── src/
│ │ └── utils.ts
│ └── operation.json
├── add-class/
│ ├── input/
│ ├── expected/
│ └── operation.json
├── add-method/
│ ├── input/
│ ├── expected/
│ └── operation.json
└── add-interface/
├── input/
├── expected/
└── operation.json
File: fixtures/add/add-function/operation.json
{
"operation": "add",
"symbolType": "function",
"symbolName": "calculateAverage",
"targetFilePath": "src/utils.ts",
"spec": {
"parameters": [
{
"name": "numbers",
"type": "number[]"
}
],
"returnType": "number",
"body": "const sum = numbers.reduce((acc, num) => acc + num, 0);\nreturn sum / numbers.length;",
"isExported": true,
"jsDoc": "/**\n * Calculates the average of an array of numbers\n * @param numbers The numbers to average\n * @returns The average value\n */"
},
"reason": "Add utility function for calculating averages"
}
src/core/tools/refactor-code/__tests__/fixtures/inline/
├── inline-constant/
│ ├── input/
│ ├── expected/
│ └── operation.json
├── inline-function/
│ ├── input/
│ ├── expected/
│ └── operation.json
└── inline-expression/
├── input/
├── expected/
└── operation.json
src/core/tools/refactor-code/utils/code-generator.tssrc/core/tools/refactor-code/operations/add.tssrc/core/tools/refactor-code/operations/inline.tsAfter completing Phase 5: