PRIX / src /utils /patch-utils.ts
yamxxx1's picture
Upload 139 files
7da7e2c verified
Raw
History Blame
6.24 kB
/**
* 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'
}