ripple-ai-detector
Version:
🌊 Ripple AI Bug Detector - Built by an AI that knows its flaws. Catch AI-generated bugs before you commit.
220 lines (177 loc) • 7.35 kB
text/typescript
import { SimpleParser } from '../analysis/simple-parser';
import { FileUtils } from '../utils/file-utils';
import { Issue, FunctionInfo, AffectedFile } from '../types/analysis';
import path from 'path';
interface FunctionChange {
oldFunction: FunctionInfo;
newFunction: FunctionInfo;
changeType: 'parameters' | 'name' | 'signature';
details: string;
}
export class FunctionSignatureDetector {
private parser: SimpleParser;
constructor() {
this.parser = new SimpleParser();
}
// Main detection method
async detect(files: string[]): Promise<Issue[]> {
const issues: Issue[] = [];
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
private async analyzeFile(filePath: string, allFiles: string[]): Promise<Issue[]> {
const issues: Issue[] = [];
// Read current file content
const fileInfo = await 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: 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);
}
}
return issues;
}
// Detect function changes (simplified for MVP)
private async detectFunctionChanges(currentFunctions: FunctionInfo[], filePath: string): Promise<FunctionChange[]> {
const changes: FunctionChange[] = [];
// 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
private looksLikeAIChange(func: FunctionInfo): boolean {
// 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)
private simulateOldFunction(newFunc: FunctionInfo): FunctionInfo {
// 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
private async findCallSites(func: FunctionInfo, allFiles: string[]): Promise<AffectedFile[]> {
const affectedFiles: AffectedFile[] = [];
for (const file of allFiles) {
if (file === func.file) continue; // Skip the file where function is defined
try {
const fileInfo = await 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
private createIssueMessage(change: FunctionChange): string {
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
private getFunctionSignature(func: FunctionInfo): string {
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
private generateSuggestions(change: FunctionChange, affectedFiles: AffectedFile[]): string[] {
const suggestions: string[] = [];
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
private generateCallSiteSuggestion(func: FunctionInfo, context: string): string {
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(', ')}`;
}
}