Spaces:
Paused
Paused
| /** | |
| * 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<ReviewFinding['severity']> | |
| 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' | |
| } | |