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
JavaScript
;
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