/** * Code Review Engine - Engine * * Main code review engine with rule matching and multi-pass analysis. */ import { randomBytes } from "node:crypto"; import type { CodeIssue, IssueCategory, ReportOptions, ReviewFile, ReviewRequest, ReviewResult, ReviewRule, ReviewStats, RuleSet, } from "./types.js"; import { TextReporter } from "./reports/text.js"; import { HtmlReporter } from "./reports/html.js"; import { MarkdownReporter } from "./reports/markdown.js"; // ─── Default Rules ──────────────────────────────────────────────────────── const DEFAULT_RULES: ReviewRule[] = [ // Security rules { id: "security/no-eval", name: "No eval()", severity: "error", category: "security", description: "Avoid using eval() as it can execute arbitrary code", pattern: "\\beval\\s*\\(", message: "Use of eval() is a security risk", suggestion: "Use Function() constructor or JSON.parse() instead", }, { id: "security/no-inner-html", name: "No innerHTML", severity: "warning", category: "security", description: "Avoid innerHTML to prevent XSS attacks", pattern: "\\.innerHTML\\s*=", message: "innerHTML can lead to XSS vulnerabilities", suggestion: "Use textContent or sanitize HTML before insertion", }, { id: "security/no-dynamic-selector", name: "No dynamic SQL selectors", severity: "error", category: "security", description: "Avoid dynamic SQL selectors to prevent injection", pattern: "(SELECT|INSERT|UPDATE|DELETE)\\s+.*\\$\\{", message: "Dynamic SQL can lead to injection attacks", suggestion: "Use parameterized queries instead", }, // Performance rules { id: "performance/no-inner-loop", name: "No nested loops", severity: "info", category: "performance", description: "Nested loops can lead to O(n²) complexity", pattern: "for\\s*\\([^)]*\\)\\s*\\{[^}]*for\\s*\\(", message: "Nested loop detected - consider optimizing", suggestion: "Use a hash map or set for O(1) lookups", }, { id: "performance/no-sync", name: "No synchronous file I/O", severity: "warning", category: "performance", description: "Avoid synchronous I/O in async code", pattern: "(readFileSync|writeFileSync|readSync)\\s*\\(", message: "Synchronous I/O blocks the event loop", suggestion: "Use async/await with fs/promises", }, // Best practice rules { id: "best-practice/no-console", name: "No console statements", severity: "info", category: "best-practice", description: "Remove console statements before production", pattern: "\\bconsole\\.(log|debug|info|warn|error)\\s*\\(", message: "Console statement found", suggestion: "Use a proper logging library in production", }, { id: "best-practice/no-any", name: "No any type", severity: "warning", category: "typescript", description: "Avoid using 'any' type", pattern: ":\\s*any\\b", message: "Use of 'any' defeats TypeScript type checking", suggestion: "Use 'unknown' and narrow the type safely", }, { id: "best-practice/no-deprecated", name: "No deprecated APIs", severity: "warning", category: "best-practice", description: "Check for deprecated API usage", pattern: "/@(deprecated|deprecated|@deprecated)/i", message: "Deprecated API usage detected", }, // Style rules { id: "style/no-magic-numbers", name: "No magic numbers", severity: "info", category: "style", description: "Avoid magic numbers in code", pattern: "(? = new Map(); private customMatchers: Map CodeIssue[]> = new Map(); constructor() { // Load default rules this.rules = [...DEFAULT_RULES]; } /** * Add a rule */ addRule(rule: ReviewRule): void { const existingIndex = this.rules.findIndex((r) => r.id === rule.id); if (existingIndex >= 0) { this.rules[existingIndex] = rule; } else { this.rules.push(rule); } } /** * Add multiple rules */ addRules(rules: ReviewRule[]): void { for (const rule of rules) { this.addRule(rule); } } /** * Add a rule set */ addRuleSet(set: RuleSet): void { this.ruleSets.set(set.name, set); if (set.extends) { for (const parentName of set.extends) { const parent = this.ruleSets.get(parentName); if (parent) { this.rules.push(...parent.rules); } } } this.rules.push(...set.rules); } /** * Register custom matcher */ registerCustomMatcher( name: string, matcher: (file: ReviewFile) => CodeIssue[], ): void { this.customMatchers.set(name, matcher); } /** * Get enabled rules */ getRules(): ReviewRule[] { return this.rules.filter((r) => r.enabled !== false); } /** * Review files */ async review(request: ReviewRequest): Promise { const startTime = Date.now(); const files = request.files ?? []; const issues: CodeIssue[] = []; // Get rules to apply const rules = request.rules?.length ? request.rules.filter((r) => r.enabled !== false) : this.getRules().filter((r) => r.enabled !== false); // Apply rules to each file for (const file of files) { const fileIssues = this.applyRules(file, rules); issues.push(...fileIssues); } // Apply custom matchers for (const [, matcher] of this.customMatchers) { for (const file of files) { const matcherIssues = matcher(file); issues.push(...matcherIssues); } } // Generate summary const summary = this.generateSummary(issues); const result: ReviewResult = { id: this.generateId(), timestamp: new Date().toISOString(), durationMs: Date.now() - startTime, issues, summary, stats: this.generateStats(files, issues), }; return result; } /** * Apply rules to a file */ private applyRules(file: ReviewFile, rules: ReviewRule[]): CodeIssue[] { const issues: CodeIssue[] = []; for (const rule of rules) { if (rule.pattern) { const pattern = rule.pattern instanceof RegExp ? rule.pattern : new RegExp(rule.pattern, rule.patternFlags ?? "g"); // Find all matches let match: RegExpExecArray | null; while ((match = pattern.exec(file.content)) !== null) { const lineInfo = this.getLineInfo(file.content, match.index); issues.push(this.createIssue(rule, file.path, lineInfo, match[0])); } } } return issues; } /** * Get line and column info for a position */ private getLineInfo( content: string, position: number, ): { line: number; column: number; lineContent: string } { const lines = content.substring(0, position).split("\n"); const line = lines.length; const column = lines[lines.length - 1].length + 1; const allLines = content.split("\n"); return { line, column, lineContent: allLines[line - 1] ?? "", }; } /** * Create an issue from a rule match */ private createIssue( rule: ReviewRule, file: string, lineInfo: { line: number; column: number; lineContent: string }, _matchedCode: string, ): CodeIssue { return { id: `${rule.id}-${this.generateId()}`, severity: rule.severity, category: rule.category, title: rule.name, message: rule.message ?? rule.description, file, line: lineInfo.line, column: lineInfo.column, rule: rule.id, ruleUrl: rule.url, suggestion: rule.suggestion, code: lineInfo.lineContent.trim(), fix: rule.suggestion, }; } /** * Generate issue summary */ private generateSummary(issues: CodeIssue[]): { total: number; errors: number; warnings: number; info: number; hints: number; byCategory: Record; byFile: Record; } { const summary = { total: issues.length, errors: 0, warnings: 0, info: 0, hints: 0, byCategory: {} as Record, byFile: {} as Record, }; for (const issue of issues) { switch (issue.severity) { case "error": summary.errors++; break; case "warning": summary.warnings++; break; case "info": summary.info++; break; case "hint": summary.hints++; break; } summary.byCategory[issue.category] = (summary.byCategory[issue.category] ?? 0) + 1; summary.byFile[issue.file] = (summary.byFile[issue.file] ?? 0) + 1; } return summary; } /** * Generate review statistics */ private generateStats(files: ReviewFile[], issues: CodeIssue[]): ReviewStats { const linesReviewed = files.reduce( (sum, f) => sum + f.content.split("\n").length, 0, ); return { filesReviewed: files.length, linesReviewed, issuesFound: issues.length, aiSuggestions: 0, rulesMatched: this.rules.filter((r) => r.enabled !== false).length, }; } /** * Generate unique ID */ private generateId(): string { return randomBytes(8).toString("hex"); } /** * Generate report */ generateReport(result: ReviewResult, options: ReportOptions): string { switch (options.format) { case "text": return new TextReporter().generate(result, options); case "html": return new HtmlReporter().generate(result, options); case "markdown": return new MarkdownReporter().generate(result, options); case "json": default: return JSON.stringify(result, null, 2); } } } // ─── Factory Function ──────────────────────────────────────────────────── /** * Create a code review engine */ export function createCodeReviewEngine(): CodeReviewEngine { return new CodeReviewEngine(); }