import { PolicyV2 } from '@nihal1983/core'; import { Checker, PRContext, Violation } from '../types'; /** * CodeChecker validates code patterns and anti-patterns */ export class CodeChecker implements Checker { name = 'CodeChecker'; constructor(private policy: PolicyV2) {} async check(context: PRContext): Promise { const violations: Violation[] = []; const codeRules = this.policy.code_rules; if (!codeRules) { return violations; } // Check anti-patterns if (codeRules.anti_patterns) { for (const antiPattern of codeRules.anti_patterns) { violations.push(...await this.checkAntiPattern(context, antiPattern)); } } // Check required patterns (future implementation) if (codeRules.required_patterns) { // TODO: Implement required pattern checking } return violations; } /** * Check for anti-patterns in code */ private async checkAntiPattern( context: PRContext, antiPattern: { name: string; pattern: string; files?: string; context?: string; severity: 'info' | 'warn' | 'block'; message: string; reference?: string; } ): Promise { const violations: Violation[] = []; // Determine which files to check let filesToCheck = context.files; if (antiPattern.files) { // Convert glob pattern to regex: ** -> .*, then * -> [^/]* const pattern = antiPattern.files.replace(/\*\*/g, '.*').replace(/\*/g, '[^/]*'); const filePattern = new RegExp(pattern); filesToCheck = filesToCheck.filter(f => filePattern.test(f.path)); } // Check each file for (const file of filesToCheck) { if (!file.content) { continue; // Skip if content not available } // Check if pattern matches const regex = new RegExp(antiPattern.pattern, 'gm'); const matchesArray = Array.from(file.content.matchAll(regex)); for (const match of matchesArray) { // If context pattern specified, validate context if (antiPattern.context) { const contextRegex = new RegExp(antiPattern.context); // Get surrounding context (100 chars before and after) const matchIndex = match.index || 0; const contextStart = Math.max(0, matchIndex - 100); const contextEnd = Math.min(file.content.length, matchIndex + match[0].length + 100); const surroundingContext = file.content.substring(contextStart, contextEnd); if (!contextRegex.test(surroundingContext)) { continue; // Context doesn't match, skip this occurrence } } // Find line number const lineNumber = this.getLineNumber(file.content, match.index || 0); violations.push({ ruleId: antiPattern.name, severity: antiPattern.severity, message: antiPattern.message, location: `${file.path}:${lineNumber}`, suggestion: this.getSuggestionForAntiPattern(antiPattern.name), reference: antiPattern.reference }); } } return violations; } /** * Get line number from character index */ private getLineNumber(content: string, index: number): number { const lines = content.substring(0, index).split('\n'); return lines.length; } /** * Get fix suggestion for specific anti-patterns */ private getSuggestionForAntiPattern(patternName: string): string { const suggestions: Record = { 'no-sql-injection': 'Use parameterized queries: db.query("SELECT * FROM users WHERE id = $1", [id])', 'no-password-exposure': 'Remove password_hash from SELECT query or filter it before returning', 'no-n-plus-one-queries': 'Use JOIN instead of queries in loops, or use batch loading (e.g., DataLoader)', 'no-hardcoded-secrets': 'Move secrets to environment variables (.env file)', }; return suggestions[patternName] || 'Fix the code to comply with policy'; } }