PRININIT / lib /src /autofix /patch-applicator.js
Rachit-Tw's picture
Upload 183 files
272a04f verified
Raw
History Blame Contribute Delete
15.4 kB
"use strict";
/**
* Safe Patch Applicator
*
* Applies LLM-generated code patches SAFELY.
* Uses function-level replacement, NOT regex hacks.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.applyPatch = applyPatch;
exports.validatePatchSemantically = validatePatchSemantically;
exports.rollbackPatch = rollbackPatch;
exports.cleanupOldBackups = cleanupOldBackups;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const child_process_1 = require("child_process");
/**
* Apply a patch safely using the most appropriate strategy:
* 1. Function-level AST-aware replacement (preferred)
* 2. Line-range replacement with verification (fallback)
*/
function applyPatch(workingDir, request) {
const absPath = path.resolve(workingDir, request.filename);
if (!fs.existsSync(absPath)) {
return {
success: false,
error: `File not found: ${absPath}`,
originalCode: '',
patchedCode: ''
};
}
try {
const originalCode = fs.readFileSync(absPath, 'utf8');
const lines = originalCode.split('\n');
// Strategy 1: Function-level replacement (preferred)
if (request.functionName &&
request.functionStartLine &&
request.functionEndLine) {
const result = applyFunctionLevelPatch(lines, request);
if (result.success) {
// Write backup first
const backupPath = `${absPath}.backup.${Date.now()}`;
fs.writeFileSync(backupPath, originalCode);
// Write patched code
fs.writeFileSync(absPath, result.patchedCode);
return {
...result,
originalCode,
backupPath
};
}
// Fall through to line-range if function-level fails
}
// Strategy 2: Line-range replacement
if (request.lineStart && request.lineEnd) {
const result = applyLineRangePatch(lines, request);
if (result.success) {
const backupPath = `${absPath}.backup.${Date.now()}`;
fs.writeFileSync(backupPath, originalCode);
fs.writeFileSync(absPath, result.patchedCode);
return {
...result,
originalCode,
backupPath
};
}
return result;
}
return {
success: false,
error: 'No valid patch strategy available',
originalCode,
patchedCode: originalCode
};
}
catch (err) {
return {
success: false,
error: `Patch application failed: ${err.message}`,
originalCode: '',
patchedCode: ''
};
}
}
/**
* Apply function-level replacement
* Replaces the entire function with the new implementation
*/
function applyFunctionLevelPatch(lines, request) {
const originalCode = lines.join('\n');
if (!request.functionStartLine || !request.functionEndLine) {
return {
success: false,
error: 'Function boundaries not specified',
originalCode,
patchedCode: originalCode
};
}
// Convert to 0-indexed
const startIdx = request.functionStartLine - 1;
const endIdx = request.functionEndLine;
if (startIdx < 0 || endIdx > lines.length || startIdx >= endIdx) {
return {
success: false,
error: `Invalid function boundaries: ${request.functionStartLine}-${request.functionEndLine}`,
originalCode,
patchedCode: originalCode
};
}
// Verify the function exists at expected location (heuristic check)
const originalFunction = lines.slice(startIdx, endIdx).join('\n');
const functionName = request.functionName || 'unknown';
// Simple heuristic: check if function name appears in the first line
const firstLine = lines[startIdx].toLowerCase();
const expectedName = functionName.toLowerCase();
// Check for function patterns
const hasFunctionPattern = firstLine.includes('function') ||
firstLine.includes('=>') ||
firstLine.includes('def ') || // Python
firstLine.includes(`${expectedName}`);
if (!hasFunctionPattern) {
return {
success: false,
error: `Function "${functionName}" not found at expected location. Heuristic check failed.`,
originalCode,
patchedCode: originalCode
};
}
// Normalize new code indentation
const newCode = normalizeIndentation(request.newCode, lines[startIdx]);
// Build the patched file
const beforeFunction = lines.slice(0, startIdx);
const afterFunction = lines.slice(endIdx);
const patchedLines = [...beforeFunction, newCode, ...afterFunction];
const patchedCode = patchedLines.join('\n');
// Safety check: basic syntax validation
const syntaxError = quickSyntaxCheck(patchedCode, request.language);
if (syntaxError) {
return {
success: false,
error: `Syntax check failed: ${syntaxError}`,
originalCode,
patchedCode: originalCode
};
}
return {
success: true,
originalCode,
patchedCode
};
}
/**
* Apply line-range replacement (fallback)
* Replaces specific lines with new code
*/
function applyLineRangePatch(lines, request) {
const originalCode = lines.join('\n');
if (!request.lineStart || !request.lineEnd) {
return {
success: false,
error: 'Line range not specified',
originalCode,
patchedCode: originalCode
};
}
// Convert to 0-indexed
const startIdx = request.lineStart - 1;
const endIdx = request.lineEnd;
if (startIdx < 0 || endIdx > lines.length || startIdx >= endIdx) {
return {
success: false,
error: `Invalid line range: ${request.lineStart}-${request.lineEnd}`,
originalCode,
patchedCode: originalCode
};
}
// Get base indentation from the first line being replaced
const baseIndent = getIndentation(lines[startIdx]);
// Normalize and apply new code indentation
const newCode = normalizeIndentation(request.newCode, lines[startIdx]);
const newCodeLines = newCode.split('\n');
// Build the patched file
const beforeRange = lines.slice(0, startIdx);
const afterRange = lines.slice(endIdx);
const patchedLines = [...beforeRange, ...newCodeLines, ...afterRange];
const patchedCode = patchedLines.join('\n');
// Safety check
const syntaxError = quickSyntaxCheck(patchedCode, request.language);
if (syntaxError) {
return {
success: false,
error: `Syntax check failed: ${syntaxError}`,
originalCode,
patchedCode: originalCode
};
}
return {
success: true,
originalCode,
patchedCode
};
}
/**
* Get indentation of a line
*/
function getIndentation(line) {
const match = line.match(/^(\s*)/);
return match ? match[1] : '';
}
/**
* Normalize indentation of new code to match original context
*/
function normalizeIndentation(newCode, referenceLine) {
const baseIndent = getIndentation(referenceLine);
const lines = newCode.split('\n');
// Find minimum indentation in new code
let minIndent = Infinity;
for (const line of lines) {
if (line.trim()) {
const indent = getIndentation(line).length;
minIndent = Math.min(minIndent, indent);
}
}
if (minIndent === Infinity)
return newCode;
// Remove min indent from all lines and add base indent
const normalized = lines.map(line => {
if (!line.trim())
return '';
const currentIndent = getIndentation(line).length;
const relativeIndent = currentIndent - minIndent;
return baseIndent + ' '.repeat(relativeIndent) + line.trimStart();
});
return normalized.join('\n');
}
async function validatePatchSemantically(workingDir, filename, language) {
// Try validation commands in order of preference
const validationCommands = [];
if (language === 'typescript') {
// Check for TypeScript compiler
validationCommands.push({ cmd: `npx tsc --noEmit ${filename}`, label: 'TypeScript compiler' }, { cmd: 'npx tsc --noEmit', label: 'Project-wide TypeScript check' });
}
// Check for ESLint
validationCommands.push({ cmd: `npx eslint ${filename} --max-warnings=0`, label: 'ESLint' }, { cmd: 'npx eslint . --max-warnings=0', label: 'Project-wide ESLint' });
// Check for npm test (if tests exist)
validationCommands.push({
cmd: 'npm test -- --passWithNoTests --testPathPattern=' + filename,
label: 'npm test'
});
for (const { cmd, label } of validationCommands) {
try {
console.log(`[VALIDATION] Running ${label}...`);
const stdout = (0, child_process_1.execSync)(cmd, {
cwd: workingDir,
encoding: 'utf8',
timeout: 60000,
stdio: ['pipe', 'pipe', 'pipe']
});
console.log(`[VALIDATION] ✓ ${label} passed`);
return {
passed: true,
command: cmd,
stdout,
stderr: '',
exitCode: 0
};
}
catch (err) {
// Command failed - this might be expected for missing tools
if (err.status === 127 ||
err.message?.includes('not found') ||
err.message?.includes('ENOENT')) {
console.log(`[VALIDATION] ${label} not available, skipping...`);
continue; // Try next command
}
// Validation actually failed
console.log(`[VALIDATION] ✗ ${label} failed (exit ${err.status})`);
return {
passed: false,
command: cmd,
stdout: err.stdout || '',
stderr: err.stderr || '',
exitCode: err.status || 1
};
}
}
// No validation tools available
console.log(`[VALIDATION] ⚠ No semantic validation tools available`);
return {
passed: true, // Allow patch if we can't validate
command: 'none',
stdout: '',
stderr: 'No validation tools available',
exitCode: 0
};
}
/**
* Quick syntax validation
* Not comprehensive, catches obvious issues
*/
function quickSyntaxCheck(code, language) {
// Basic brace/parenthesis balance check
let braceCount = 0;
let parenCount = 0;
let bracketCount = 0;
let inString = false;
let stringChar = '';
for (let i = 0; i < code.length; i++) {
const char = code[i];
const prevChar = i > 0 ? code[i - 1] : '';
// Handle strings
if (!inString && (char === '"' || char === "'" || char === '`')) {
inString = true;
stringChar = char;
continue;
}
if (inString && char === stringChar && prevChar !== '\\') {
inString = false;
continue;
}
if (inString)
continue;
// Count braces
if (char === '{')
braceCount++;
else if (char === '}')
braceCount--;
else if (char === '(')
parenCount++;
else if (char === ')')
parenCount--;
else if (char === '[')
bracketCount++;
else if (char === ']')
bracketCount--;
// Early termination on severe imbalance
if (braceCount < -2 || parenCount < -2 || bracketCount < -2) {
return `Unbalanced ${braceCount < 0 ? 'braces' : parenCount < 0 ? 'parentheses' : 'brackets'}`;
}
}
if (braceCount !== 0)
return `Unbalanced braces: ${braceCount > 0 ? 'missing' : 'extra'} ${Math.abs(braceCount)} }`;
if (parenCount !== 0)
return `Unbalanced parentheses: ${parenCount > 0 ? 'missing' : 'extra'} ${Math.abs(parenCount)} )`;
if (bracketCount !== 0)
return `Unbalanced brackets: ${bracketCount > 0 ? 'missing' : 'extra'} ${Math.abs(bracketCount)} ]`;
// Language-specific checks
if (language === 'typescript' || language === 'javascript') {
// Check for common syntax issues
if (/\bfunction\s*\([^)]*\)\s*[^=>{]/.test(code)) {
return 'Function without body';
}
}
return null;
}
/**
* Rollback a patch using backup file
*/
function rollbackPatch(backupPath, originalPath) {
try {
if (!fs.existsSync(backupPath)) {
console.error(`[PATCH] Backup not found: ${backupPath}`);
return false;
}
fs.copyFileSync(backupPath, originalPath);
console.log(`[PATCH] Rolled back ${originalPath} from ${backupPath}`);
return true;
}
catch (err) {
console.error(`[PATCH] Rollback failed: ${err.message}`);
return false;
}
}
/**
* Clean up backup files older than specified age
*/
function cleanupOldBackups(workingDir, maxAgeMs = 24 * 60 * 60 * 1000) {
try {
const files = fs.readdirSync(workingDir);
const now = Date.now();
for (const file of files) {
if (file.includes('.backup.')) {
const backupPath = path.join(workingDir, file);
const stats = fs.statSync(backupPath);
const age = now - stats.mtimeMs;
if (age > maxAgeMs) {
fs.unlinkSync(backupPath);
console.log(`[PATCH] Cleaned up old backup: ${file}`);
}
}
}
}
catch (err) {
console.error(`[PATCH] Cleanup failed: ${err.message}`);
}
}
//# sourceMappingURL=patch-applicator.js.map