UNPKG

ripbug-ai-detector

Version:

🔥 RipBug AI Bug Detector - Built by an AI that rips its own bugs. Destroy AI-generated bugs before you commit.

452 lines • 20.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.FunctionSignatureDetector = void 0; const ast_parser_enhanced_1 = require("../analysis/ast-parser-enhanced"); const feature_flags_1 = require("../config/feature-flags"); const file_utils_1 = require("../utils/file-utils"); class FunctionSignatureDetector { parser; constructor() { // Initialize with feature flags for smart tree-sitter rollout const useTreeSitter = feature_flags_1.FeatureFlags.shouldUseTreeSitter('default'); this.parser = new ast_parser_enhanced_1.EnhancedASTParser({ enableTreeSitter: useTreeSitter, fallbackToRegex: true, debugMode: false }); } // Main detection method async detect(files) { const issues = []; for (const file of files) { try { const fileIssues = await this.analyzeFile(file, files); issues.push(...fileIssues); } catch (error) { // Skip files that can't be parsed console.warn(`Failed to analyze ${file}: ${error}`); } } return issues; } // Analyze a single file for function signature changes async analyzeFile(filePath, allFiles) { const issues = []; // Read current file content const fileInfo = await file_utils_1.FileUtils.getFileInfo(filePath); if (!fileInfo.isJavaScript) { return issues; } // Parse current version const currentFunctions = this.parser.extractFunctions(fileInfo.content, filePath); // For MVP, we'll simulate "old" version by checking git diff // In a full implementation, we'd compare with git HEAD const changes = await this.detectFunctionChanges(currentFunctions, filePath); // For each changed function, find call sites for (const change of changes) { const affectedFiles = await this.findCallSites(change.newFunction, allFiles); if (affectedFiles.length > 0) { const issue = { id: `func-sig-${change.newFunction.name}-${Date.now()}`, type: 'function-signature-change', severity: 'error', message: this.createIssueMessage(change), file: filePath, line: change.newFunction.line, column: change.newFunction.column, details: { functionName: change.newFunction.name, oldSignature: this.getFunctionSignature(change.oldFunction), newSignature: this.getFunctionSignature(change.newFunction), affectedFiles, context: change.details }, suggestions: this.generateSuggestions(change, affectedFiles), confidence: 0.9 // High confidence for function signature changes }; issues.push(issue); } } // STEP 4: Basic cross-file signature validation const crossFileIssues = await this.validateCrossFileSignatures(currentFunctions, allFiles); issues.push(...crossFileIssues); return issues; } // STEP 4: Basic cross-file signature validation - SIMPLE APPROACH async validateCrossFileSignatures(functions, allFiles) { const issues = []; for (const func of functions) { // TODO: Fix export detection and re-enable this check // Temporarily disabled to keep Step 4 working // if (!func.isExported) continue; // Find calls to this function in other files const callSites = await this.findCallSites(func, allFiles); for (const callSite of callSites) { // Simple comparison: required parameters vs provided arguments const requiredParams = func.parameters.filter(p => !p.optional && !p.defaultValue); const providedArgs = this.getArgumentCount(callSite.context); // Breaking change: missing required parameters if (providedArgs < requiredParams.length) { const missing = requiredParams.length - providedArgs; issues.push({ id: `cross-file-${func.name}-${callSite.line}`, type: 'function-signature-change', severity: 'error', message: `Function call missing ${missing} required parameter(s): ${func.name}()`, file: func.file, line: func.line, column: func.column, details: { functionName: func.name, oldSignature: `${func.name}(${this.getSimpleSignature(providedArgs)})`, newSignature: `${func.name}(${this.getSimpleSignature(requiredParams.length)})`, affectedFiles: [callSite], context: `Breaking call: ${callSite.context}` }, suggestions: [ `Add ${missing} missing parameter(s) to call in ${callSite.path}:${callSite.line}`, 'Check if function signature was changed without updating callers' ], confidence: 0.9 }); } } } return issues; } // Enhanced call site detection using tree-sitter's superior parsing async findCallSitesWithTreeSitter(func, allFiles) { const affectedFiles = []; for (const file of allFiles) { if (file === func.file) continue; // Skip the file where function is defined try { const fileInfo = await file_utils_1.FileUtils.getFileInfo(file); if (!fileInfo.isJavaScript) continue; // Use enhanced parser with tree-sitter for superior call detection const calls = this.parser.extractFunctionCalls(fileInfo.content, file); // Find calls to our function with enhanced matching const matchingCalls = calls.filter(call => { // Enhanced matching: handle method calls, destructured imports, etc. return call.name === func.name || call.name.endsWith(`.${func.name}`) || call.context.includes(`${func.name}(`); }); for (const call of matchingCalls) { affectedFiles.push({ path: file, line: call.line, column: call.column, context: call.context, suggestion: this.generateEnhancedCallSiteSuggestion(func, call) }); } } catch (error) { // Fallback to original method if tree-sitter fails const fallbackCalls = await this.findCallSites(func, [file]); affectedFiles.push(...fallbackCalls); } } return affectedFiles; } // Enhanced signature compatibility analysis analyzeSignatureCompatibility(func, callSite) { // Count required parameters (non-optional) const requiredParams = func.parameters.filter(p => !p.optional).length; // Get provided arguments using enhanced analysis const providedArgs = this.getArgumentCountFromContext(callSite.context); // Analyze the type of breaking change if (providedArgs < requiredParams) { return { hasBreakingChange: true, severity: 'error', message: `Function call missing ${requiredParams - providedArgs} required parameter(s): ${func.name}()`, expectedSignature: `${func.name}(${this.getSimpleSignature(providedArgs)})`, changeType: 'missing-parameters', suggestions: [ `Add ${requiredParams - providedArgs} missing parameter(s) to call in ${callSite.path}:${callSite.line}`, 'Check if function signature was changed by AI without updating callers', 'Consider making new parameters optional with default values' ], confidence: 0.95 // High confidence for missing parameters }; } // Check for complex parameter types that might cause issues const hasComplexTypes = func.parameters.some(p => p.type && (p.type.includes('{') || p.type.includes('<') || p.type.includes('|'))); if (hasComplexTypes && providedArgs === requiredParams) { return { hasBreakingChange: true, severity: 'warning', message: `Function call may have type compatibility issues: ${func.name}()`, expectedSignature: `${func.name}(${this.getParameterSignature(func.parameters)})`, changeType: 'type-compatibility', suggestions: [ 'Verify argument types match the updated function signature', 'Check for TypeScript compilation errors', 'Review parameter types that may have been changed by AI' ], confidence: 0.75 // Medium confidence for type issues }; } return { hasBreakingChange: false, severity: 'warning', message: '', expectedSignature: '', changeType: 'none', suggestions: [], confidence: 0 }; } // Get accurate argument count using enhanced parser async getAccurateArgumentCount(functionFile, callSite) { try { const fileInfo = await file_utils_1.FileUtils.getFileInfo(callSite.path); if (!fileInfo.isJavaScript) return 0; // Use enhanced parser to get accurate call information const calls = this.parser.extractFunctionCalls(fileInfo.content, callSite.path); // Find the specific call at this line const matchingCall = calls.find(call => call.line === callSite.line); if (matchingCall) { return matchingCall.argumentCount || matchingCall.arguments?.length || 0; } // Fallback to context analysis return this.getArgumentCountFromContext(callSite.context); } catch (error) { // Fallback to simple counting on error return this.getArgumentCountFromContext(callSite.context); } } // Enhanced argument counting from call context getArgumentCountFromContext(callContext) { const match = callContext.match(/\(([^)]*)\)/); if (!match || !match[1].trim()) return 0; const argsString = match[1]; // Handle complex arguments with nested objects, arrays, etc. let depth = 0; let argCount = 0; let currentArg = ''; for (let i = 0; i < argsString.length; i++) { const char = argsString[i]; if (char === '(' || char === '[' || char === '{') { depth++; } else if (char === ')' || char === ']' || char === '}') { depth--; } else if (char === ',' && depth === 0) { if (currentArg.trim()) argCount++; currentArg = ''; continue; } currentArg += char; } // Count the last argument if (currentArg.trim()) argCount++; return argCount; } // Enhanced call site suggestion generation generateEnhancedCallSiteSuggestion(func, call) { const requiredParams = func.parameters.filter(p => !p.optional); const providedArgs = call.arguments?.length || this.getArgumentCountFromContext(call.context); if (providedArgs < requiredParams.length) { const missingParams = requiredParams.slice(providedArgs); const suggestions = missingParams.map(p => { if (p.defaultValue) return p.defaultValue; if (p.type === 'string') return "''"; if (p.type === 'number') return '0'; if (p.type === 'boolean') return 'false'; if (p.type && p.type.includes('{}')) return '{}'; if (p.type && p.type.includes('[]')) return '[]'; return 'undefined'; }); return `Add missing parameters: ${suggestions.join(', ')}`; } return 'Verify parameter types match the function signature'; } // Generate sophisticated suggestions based on AI patterns generateSophisticatedSuggestions(change) { const suggestions = []; // Base suggestions suggestions.push(`Update ${change.breakingCalls.length} call site(s) to match new signature`); // Pattern-specific suggestions if (change.aiPatterns.includes('options-parameter')) { suggestions.push('Consider making the options parameter optional with default value {}'); suggestions.push('This appears to be an AI-generated options pattern - verify it\'s necessary'); } if (change.aiPatterns.includes('complex-types')) { suggestions.push('Simplify complex TypeScript types if possible for better maintainability'); suggestions.push('Consider using interfaces instead of inline object types'); } if (change.aiPatterns.includes('high-parameter-count')) { suggestions.push('Consider refactoring to use an options object to reduce parameter count'); } if (change.aiPatterns.includes('generic-parameters')) { suggestions.push('Verify generic types are actually needed and properly constrained'); } // Confidence-based suggestions if (change.confidence > 0.8) { suggestions.push('High confidence AI-generated change detected - review carefully'); } return suggestions; } // Simple argument counting from call context getArgumentCount(callContext) { const match = callContext.match(/\(([^)]*)\)/); if (!match || !match[1].trim()) return 0; // Simple split by comma and count non-empty parts return match[1].split(',').filter(arg => arg.trim()).length; } // Generate simple signature representation getSimpleSignature(argCount) { if (argCount === 0) return ''; return Array(argCount).fill('arg').map((_, i) => `arg${i + 1}`).join(', '); } // Get parameter signature string getParameterSignature(parameters) { return parameters.map(p => { let sig = p.name; if (p.type) sig += `: ${p.type}`; if (p.optional) sig += '?'; if (p.defaultValue) sig += ` = ${p.defaultValue}`; return sig; }).join(', '); } // Detect function changes (simplified for MVP) async detectFunctionChanges(currentFunctions, filePath) { const changes = []; // For MVP, we'll use heuristics to detect likely AI changes // This is where we'd normally compare with git HEAD for (const func of currentFunctions) { // Detect common AI patterns that indicate function signature changes if (this.looksLikeAIChange(func)) { // Simulate an "old" version for demo purposes const oldFunction = this.simulateOldFunction(func); changes.push({ oldFunction, newFunction: func, changeType: 'parameters', details: 'Function parameters appear to have been modified' }); } } return changes; } // Heuristic to detect if function looks like it was changed by AI looksLikeAIChange(func) { // AI commonly adds options/config parameters const hasOptionsParam = func.parameters.some(p => p.name.toLowerCase().includes('option') || p.name.toLowerCase().includes('config') || p.name.toLowerCase().includes('setting')); // AI often adds optional parameters at the end const hasOptionalParams = func.parameters.some(p => p.optional); // AI tends to add type annotations const hasTypeAnnotations = func.parameters.some(p => p.type); // For MVP demo, trigger on any of these patterns return hasOptionsParam || (hasOptionalParams && hasTypeAnnotations); } // Simulate old function for demo (in real implementation, get from git) simulateOldFunction(newFunc) { // Remove the last parameter (common AI pattern) const oldParameters = newFunc.parameters.slice(0, -1); return { ...newFunc, parameters: oldParameters }; } // Find all call sites of a function across files async findCallSites(func, allFiles) { const affectedFiles = []; for (const file of allFiles) { if (file === func.file) continue; // Skip the file where function is defined try { const fileInfo = await file_utils_1.FileUtils.getFileInfo(file); if (!fileInfo.isJavaScript) continue; const calls = this.parser.extractFunctionCalls(fileInfo.content, file); // Find calls to our function const matchingCalls = calls.filter(call => call.name === func.name); for (const call of matchingCalls) { affectedFiles.push({ path: file, line: call.line, column: call.column, context: call.context, suggestion: this.generateCallSiteSuggestion(func, call.context) }); } } catch (error) { // Skip files that can't be parsed } } return affectedFiles; } // Create human-readable issue message createIssueMessage(change) { const oldSig = this.getFunctionSignature(change.oldFunction); const newSig = this.getFunctionSignature(change.newFunction); return `Function signature changed without updating callers: ${oldSig} → ${newSig}`; } // Get function signature string getFunctionSignature(func) { const params = func.parameters.map(p => { let param = p.name; if (p.type) param += `: ${p.type}`; if (p.optional) param += '?'; if (p.defaultValue) param += ` = ${p.defaultValue}`; return param; }).join(', '); return `${func.name}(${params})`; } // Generate suggestions for fixing the issue generateSuggestions(change, affectedFiles) { const suggestions = []; suggestions.push(`Update ${affectedFiles.length} call sites to match new signature`); suggestions.push(`Add default values to new parameters to maintain compatibility`); suggestions.push(`Consider creating a wrapper function for backward compatibility`); return suggestions; } // Generate suggestion for specific call site generateCallSiteSuggestion(func, context) { const newParams = func.parameters.map(p => { if (p.defaultValue) return p.defaultValue; if (p.optional) return 'undefined'; if (p.type === 'string') return "''"; if (p.type === 'number') return '0'; if (p.type === 'boolean') return 'false'; return 'null'; }); return `Add missing parameters: ${newParams.slice(-1).join(', ')}`; } } exports.FunctionSignatureDetector = FunctionSignatureDetector; //# sourceMappingURL=function-signature-detector.js.map