| |
| |
| |
| |
| |
|
|
|
|
| import {ApiCheckout, FileInfo} from '../api-checkout'
|
| import {Project, SyntaxKind} from 'ts-morph'
|
|
|
| export interface IssueLocation {
|
| filename: string
|
| startLine: number
|
| endLine: number
|
| }
|
|
|
| export interface ExtractedContext {
|
| filename: string
|
| targetLines: string
|
| surroundingContext: string
|
| containingFunction?: {
|
| name: string
|
| startLine: number
|
| endLine: number
|
| code: string
|
| }
|
| imports?: string[]
|
| language: string
|
| }
|
|
|
|
|
| const CONTEXT_CONFIG = {
|
|
|
| SURROUNDING_LINES: 40,
|
|
|
| MAX_FUNCTION_LINES: 200,
|
|
|
| MAX_CONTEXT_CHARS: 4000
|
| }
|
|
|
| |
| |
| |
|
|
| export async function extractIssueContextAPI(
|
| apiCheckout: ApiCheckout,
|
| location: IssueLocation
|
| ): Promise<ExtractedContext | null> {
|
| try {
|
|
|
| const file = await apiCheckout.getFile(location.filename)
|
| const lines = file.content.split('\n')
|
|
|
|
|
| const language = location.filename.endsWith('.ts')
|
| ? 'typescript'
|
| : location.filename.endsWith('.js')
|
| ? 'javascript'
|
| : location.filename.endsWith('.py')
|
| ? 'python'
|
| : 'unknown'
|
|
|
|
|
| const startIdx = Math.max(0, location.startLine - 1)
|
| const endIdx = Math.min(lines.length, location.endLine)
|
| const targetLines = lines.slice(startIdx, endIdx).join('\n')
|
|
|
|
|
| const contextStart = Math.max(
|
| 0,
|
| location.startLine - CONTEXT_CONFIG.SURROUNDING_LINES - 1
|
| )
|
| const contextEnd = Math.min(
|
| lines.length,
|
| location.endLine + CONTEXT_CONFIG.SURROUNDING_LINES
|
| )
|
| const surroundingLines = lines.slice(contextStart, contextEnd)
|
| const surroundingContext = surroundingLines.join('\n')
|
|
|
|
|
| const importLines: string[] = []
|
| for (let i = 0; i < Math.min(50, lines.length); i++) {
|
| const line = lines[i].trim()
|
| if (
|
| line.startsWith('import ') ||
|
| line.startsWith('from ') ||
|
| line.startsWith('require(')
|
| ) {
|
| importLines.push(lines[i])
|
| }
|
| }
|
|
|
|
|
| let containingFunction: ExtractedContext['containingFunction'] = undefined
|
|
|
| if (language === 'typescript' || language === 'javascript') {
|
| containingFunction = await findContainingFunctionAPI(
|
| file.content,
|
| location.startLine
|
| )
|
| } else if (language === 'python') {
|
| containingFunction = findContainingFunctionPython(
|
| lines,
|
| location.startLine
|
| )
|
| }
|
|
|
|
|
| const context: ExtractedContext = {
|
| filename: location.filename,
|
| targetLines,
|
| surroundingContext,
|
| containingFunction,
|
| imports: importLines,
|
| language
|
| }
|
|
|
|
|
| if (surroundingContext.length > CONTEXT_CONFIG.MAX_CONTEXT_CHARS) {
|
| console.log(
|
| `[CONTEXT] Truncating context from ${surroundingContext.length} to ${CONTEXT_CONFIG.MAX_CONTEXT_CHARS} chars`
|
| )
|
| context.surroundingContext = surroundingContext.substring(
|
| 0,
|
| CONTEXT_CONFIG.MAX_CONTEXT_CHARS
|
| )
|
| }
|
|
|
| return context
|
| } catch (error) {
|
| console.error(
|
| `[CONTEXT] Failed to extract context for ${location.filename}:`,
|
| error
|
| )
|
| return null
|
| }
|
| }
|
|
|
| |
| |
|
|
| async function findContainingFunctionAPI(
|
| content: string,
|
| targetLine: number
|
| ): Promise<ExtractedContext['containingFunction']> {
|
| try {
|
| const project = new Project({useInMemoryFileSystem: true})
|
| const sourceFile = project.createSourceFile('temp.ts', content)
|
|
|
|
|
| const lineStartPos = sourceFile.compilerNode.getPositionOfLineAndCharacter(
|
| targetLine - 1,
|
| 0
|
| )
|
| const node = sourceFile.getDescendantAtPos(lineStartPos)
|
|
|
| if (!node) return undefined
|
|
|
|
|
| const parent = node.getFirstAncestor(
|
| a =>
|
| a.getKind() === SyntaxKind.FunctionDeclaration ||
|
| a.getKind() === SyntaxKind.MethodDeclaration ||
|
| a.getKind() === SyntaxKind.ClassDeclaration ||
|
| a.getKind() === SyntaxKind.InterfaceDeclaration ||
|
| a.getKind() === SyntaxKind.ArrowFunction ||
|
| a.getKind() === SyntaxKind.FunctionExpression
|
| )
|
|
|
| if (parent) {
|
| let name = 'anonymous'
|
| try {
|
| if ((parent as any).getName) {
|
| name = (parent as any).getName() || 'anonymous'
|
| }
|
| } catch {
|
|
|
| }
|
|
|
| const startLine = parent.getStartLineNumber()
|
| const endLine = parent.getEndLineNumber()
|
| const code = parent.getText()
|
|
|
|
|
| if (endLine - startLine > CONTEXT_CONFIG.MAX_FUNCTION_LINES) {
|
| return undefined
|
| }
|
|
|
| return {
|
| name,
|
| startLine,
|
| endLine,
|
| code
|
| }
|
| } else {
|
| console.log(`[CONTEXT] No function ancestor found at line ${targetLine}`)
|
| }
|
| } catch (e) {
|
| console.error(
|
| `[CONTEXT] ts-morph parsing failed for file (content length: ${content.length}): ${e}`
|
| )
|
| }
|
| return undefined
|
| }
|
|
|
| |
| |
| |
|
|
| function findContainingFunctionPython(
|
| lines: string[],
|
| targetLine: number
|
| ): ExtractedContext['containingFunction'] {
|
| const pyRegex = /^\s*(?:async\s+)?def\s+(\w+)/
|
| let functionStart = -1
|
| let functionName = ''
|
|
|
|
|
| for (let i = targetLine - 1; i >= 0; i--) {
|
| const match = lines[i].match(pyRegex)
|
| if (match) {
|
| functionStart = i
|
| functionName = match[1] || 'anonymous'
|
| break
|
| }
|
| }
|
|
|
| if (functionStart === -1) return undefined
|
|
|
|
|
| const baseIndent = lines[functionStart].match(/^(\s*)/)?.[1]?.length || 0
|
| const maxLines = Math.min(
|
| lines.length,
|
| functionStart + CONTEXT_CONFIG.MAX_FUNCTION_LINES
|
| )
|
|
|
| for (let j = functionStart + 1; j < maxLines; j++) {
|
| const checkLine = lines[j]
|
|
|
| if (checkLine.trim() === '' || checkLine.trim().startsWith('#')) continue
|
|
|
| const lineIndent = checkLine.match(/^(\s*)/)?.[1]?.length || 0
|
|
|
| if (lineIndent <= baseIndent && checkLine.trim()) {
|
| const functionCode = lines.slice(functionStart, j).join('\n')
|
| return {
|
| name: functionName,
|
| startLine: functionStart + 1,
|
| endLine: j,
|
| code: functionCode
|
| }
|
| }
|
| }
|
|
|
|
|
| const functionCode = lines.slice(functionStart).join('\n')
|
| return {
|
| name: functionName,
|
| startLine: functionStart + 1,
|
| endLine: lines.length,
|
| code: functionCode
|
| }
|
| }
|
|
|
| |
| |
|
|
| export function formatContextForLLMAPI(context: ExtractedContext): string {
|
| const parts: string[] = []
|
|
|
| parts.push(`FILE: ${context.filename}`)
|
| parts.push(`LANGUAGE: ${context.language}`)
|
| parts.push('')
|
|
|
| if (context.imports && context.imports.length > 0) {
|
| parts.push('IMPORTS:')
|
| context.imports.forEach(imp => parts.push(imp))
|
| parts.push('')
|
| }
|
|
|
| if (context.containingFunction) {
|
| parts.push('CONTAINING FUNCTION:')
|
| parts.push(`Name: ${context.containingFunction.name}`)
|
| parts.push(
|
| `Lines: ${context.containingFunction.startLine}-${context.containingFunction.endLine}`
|
| )
|
| parts.push('')
|
| parts.push('```' + context.language)
|
| parts.push(context.containingFunction.code)
|
| parts.push('```')
|
| parts.push('')
|
| }
|
|
|
| parts.push('TARGET CODE (lines with issue):')
|
| parts.push('```' + context.language)
|
| parts.push(context.targetLines)
|
| parts.push('```')
|
| parts.push('')
|
|
|
| parts.push('SURROUNDING CONTEXT:')
|
| parts.push('```' + context.language)
|
| parts.push(context.surroundingContext)
|
| parts.push('```')
|
|
|
| return parts.join('\n')
|
| }
|
|
|