/** * Patch parsing utilities * Extracted from review.ts to improve modularity */ import { Project, SyntaxKind } from 'ts-morph' export interface HunkInfo { oldStart: number oldLength: number newStart: number newLength: number oldHunk: { startLine: number; endLine: number } newHunk: { startLine: number; endLine: number } } export interface ParsedPatch { oldHunk: string newHunk: string oldStart: number newStart: number } /** * Validate remedy code AST * Ensures the remedy doesn't create invalid TypeScript/JS */ export const validateRemedyAST = ( filename: string, remedy: string, project: Project ): boolean => { if (!filename.endsWith('.ts') && !filename.endsWith('.js')) return true try { const sf = project.createSourceFile(`test_${Date.now()}.ts`, remedy, { overwrite: true }) // Check for basic validity - no unexpected class declarations in remedy return !sf.getDescendantsOfKind(SyntaxKind.ClassDeclaration).length } catch (e) { return false } } /** * Split a patch into individual hunks */ export const splitPatch = (patch: string | null | undefined): string[] => { if (patch == null) return [] const pattern = /(^@@ -(\d+),(\d+) \+(\d+),(\d+) @@).*$/gm const result: string[] = [] let match = pattern.exec(patch) if (match != null) { const first = match.index + match[0].length let last = first while ((match = pattern.exec(patch)) !== null) { result.push(patch.substring(last, match.index).trim()) last = match.index + match[0].length } result.push(patch.substring(last).trim()) } return result } /** * Extract start/end line numbers from patch header * Exported for use by review.ts */ export const patchStartEndLine = (patch: string): HunkInfo | null => { const pattern = /(^@@ -(\d+),(\d+) \+(\d+),(\d+) @@).*$/gm const match = pattern.exec(patch) if (match != null) { const oldStart = parseInt(match[2]) const oldLength = parseInt(match[3]) const newStart = parseInt(match[4]) const newLength = parseInt(match[5]) return { oldStart, oldLength, newStart, newLength, oldHunk: { startLine: oldStart, endLine: oldStart + oldLength - 1 }, newHunk: { startLine: newStart, endLine: newStart + newLength - 1 } } } return null } /** * Parse a patch into old and new hunks */ export const parsePatch = (patch: string): ParsedPatch | null => { const hunkInfo = patchStartEndLine(patch) if (hunkInfo == null) return null const oldHunkLines: string[] = [] const newHunkLines: string[] = [] const lines = patch.split('\n') for (const line of lines) { if (line.startsWith('-')) { oldHunkLines.push(line.substring(1)) } else if (line.startsWith('+')) { newHunkLines.push(line.substring(1)) } else if (!line.startsWith('@@')) { oldHunkLines.push(line) newHunkLines.push(line) } } return { oldHunk: oldHunkLines.join('\n'), newHunk: newHunkLines.join('\n'), oldStart: hunkInfo.oldStart, newStart: hunkInfo.newStart } } /** * Check if a patch contains only documentation changes (comments) */ export const checkIfDocumentationOnly = (diff: string): boolean => { const content = diff .split('\n') .filter(l => l.startsWith('+') || l.startsWith('-')) .map(l => l.substring(1).trim()) .join('') return content.length > 0 && !/[a-zA-Z0-9]/.test(content.replace(/\/\/|\/\*|\*|#|"""|'''/g, '')) } /** * Check if a file should be included based on path filters */ export const shouldIncludeFile = ( filename: string, pathFilters: { check: (path: string) => boolean } | null ): boolean => { if (pathFilters == null) return true return pathFilters.check(filename) } /** * Check if content contains the ignore keyword */ export const shouldIgnoreContent = (content: string, ignoreKeyword: string): boolean => { return content.includes(ignoreKeyword) } /** * Parse a review response from AI into structured format */ export interface ReviewFinding { startLine: number endLine: number comment: string severity?: 'critical' | 'major' | 'minor' | 'info' confidence?: number remedy?: string verified?: boolean verificationFeedback?: string } export const parseReview = ( response: string, patches: any[], filename: string, debug = false ): ReviewFinding[] => { const reviews: ReviewFinding[] = [] // Match patterns like: // [LINE_START-LINE_END] | [SEVERITY] | [TYPE] message // or // [LINE] | [SEVERITY] message const pattern = /\[(\d+)(-(\d+))?\]\s*\|\s*(critical|major|minor|info)?\s*\|\s*(\w+)?\s*[:|-]?\s*(.+?)(?=\[(\d+)|$)/gis let match while ((match = pattern.exec(response)) !== null) { const startLine = parseInt(match[1]) const endLine = match[2] ? parseInt(match[2]) : startLine const severity = (match[4] || 'minor') as NonNullable const type = match[5] || 'general' const message = match[6].trim() // Validate line numbers against patch const validLine = patches.some( (p: any) => startLine >= p.newStart && startLine <= p.newStart + p.newLength ) if (validLine) { reviews.push({ startLine, endLine, comment: `[${severity.toUpperCase()}] | ${type.toUpperCase()}\n\n${message}`, severity, confidence: 80 }) } } // Also look for remedy suggestions in code blocks const remedyPattern = /```(?:\w+)?\s*\n([\s\S]*?)```/g let remedyMatch while ((remedyMatch = remedyPattern.exec(response)) !== null) { const remedy = remedyMatch[1].trim() // Associate remedy with last review if any if (reviews.length > 0) { reviews[reviews.length - 1].remedy = remedy } } return reviews } /** * Extract pattern type from a review message */ export const extractPatternType = (message: string): string => { const patternTypes = [ 'Security', 'Bug', 'Performance', 'Architecture', 'Style', 'Documentation', 'Testing', 'Refactoring' ] for (const type of patternTypes) { if (message.toLowerCase().includes(type.toLowerCase())) { return type.toLowerCase() } } return 'general' }