UNPKG

pury

Version:

🛡️ AI-powered security scanner with advanced threat detection, dual reporting system (detailed & summary), and comprehensive code analysis

405 lines 17.8 kB
import { FindingType, Severity } from '../types/index.js'; import { logger } from '../utils/logger.js'; import { extractLineContext } from '../utils/file-utils.js'; export class QualityAnalyzer { patterns; sensitivity; constructor(sensitivity = 'medium') { this.sensitivity = sensitivity; this.patterns = this.loadPatterns(); } async analyze(files, onProgress) { const startTime = Date.now(); const findings = []; for (let i = 0; i < files.length; i++) { const file = files[i]; onProgress?.(i + 1, files.length, file.path); try { const fileFindings = await this.analyzeFile(file); findings.push(...fileFindings); } catch (error) { logger.warn(`Failed to analyze file ${file.path}: ${error.message}`); } } const processingTime = Date.now() - startTime; logger.debug(`Quality analysis completed in ${processingTime}ms`); return { findings, processingTime, filesAnalyzed: files.length }; } async analyzeFile(file) { const findings = []; const lines = file.content.split('\n'); // Skip certain file types that shouldn't be analyzed for code quality if (this.shouldSkipFile(file)) { return findings; } for (const pattern of this.patterns) { if (!this.shouldApplyPattern(pattern)) { continue; } const matches = this.findPatternMatches(file.content, lines, pattern); findings.push(...matches.map(match => this.createFinding(match, file, pattern))); } // Additional analysis methods findings.push(...this.analyzeCodeComplexity(file, lines)); findings.push(...this.analyzeLocalization(file, lines)); findings.push(...this.analyzePerformance(file, lines)); return findings; } shouldSkipFile(file) { const skipExtensions = ['.json', '.md', '.txt', '.xml', '.svg', '.css']; const skipPatterns = [ '.min.js', '.min.css', 'node_modules', 'dist/', 'build/', 'coverage/', '.test.', '.spec.' ]; return (skipExtensions.includes(file.extension.toLowerCase()) || skipPatterns.some(pattern => file.path.includes(pattern))); } shouldApplyPattern(pattern) { switch (this.sensitivity) { case 'low': return pattern.severity === Severity.HIGH || pattern.severity === Severity.CRITICAL; case 'medium': return pattern.severity !== Severity.LOW; case 'high': return true; default: return true; } } findPatternMatches(content, lines, pattern) { const matches = []; lines.forEach((line, index) => { const match = line.match(pattern.pattern); if (match) { matches.push({ line: index + 1, match: match[0], context: extractLineContext(content, index + 1, 1) }); } }); return matches; } analyzeCodeComplexity(file, lines) { const findings = []; // Analyze function length let currentFunction = null; let braceDepth = 0; lines.forEach((line, index) => { const trimmedLine = line.trim(); // Simple function detection (works for most JS/TS functions) const functionMatch = /(?:function\s+(\w+)|(\w+)\s*:\s*function|(\w+)\s*=\s*function|(\w+)\s*=\s*\([^)]*\)\s*=>)/.exec(trimmedLine); if (functionMatch && !currentFunction) { const functionName = functionMatch[1] || functionMatch[2] || functionMatch[3] || functionMatch[4] || 'anonymous'; currentFunction = { name: functionName, startLine: index + 1, lineCount: 0 }; braceDepth = 0; } if (currentFunction) { currentFunction.lineCount++; // Count braces to determine function end braceDepth += (line.match(/\{/g) || []).length; braceDepth -= (line.match(/\}/g) || []).length; if (braceDepth <= 0 && currentFunction.lineCount > 1) { if (currentFunction.lineCount > 50) { findings.push({ id: `complexity-length-${Date.now()}-${index}`, type: FindingType.CODE_QUALITY, severity: currentFunction.lineCount > 100 ? Severity.HIGH : Severity.MEDIUM, title: 'Long Function', description: `Function "${currentFunction.name}" is ${currentFunction.lineCount} lines long`, file: file.path, line: currentFunction.startLine, suggestion: 'Consider breaking this function into smaller, more focused functions' }); } currentFunction = null; } } }); // Analyze nested loops/conditions let nestingLevel = 0; let maxNesting = 0; let maxNestingLine = 0; lines.forEach((line, index) => { const trimmedLine = line.trim(); // Count nesting level increases if (/^\s*(if|for|while|switch|try|function)\s*\(/.test(trimmedLine) || /^\s*\w+\s*=>\s*\{/.test(trimmedLine)) { nestingLevel++; if (nestingLevel > maxNesting) { maxNesting = nestingLevel; maxNestingLine = index + 1; } } // Count nesting level decreases const braceCloses = (line.match(/\}/g) || []).length; nestingLevel = Math.max(0, nestingLevel - braceCloses); }); if (maxNesting > 4) { findings.push({ id: `complexity-nesting-${Date.now()}`, type: FindingType.CODE_QUALITY, severity: maxNesting > 6 ? Severity.HIGH : Severity.MEDIUM, title: 'High Nesting Level', description: `Maximum nesting level of ${maxNesting} detected`, file: file.path, line: maxNestingLine, suggestion: 'Consider refactoring to reduce nesting levels using early returns or guard clauses' }); } return findings; } analyzeLocalization(file, lines) { const findings = []; // Check for hardcoded strings that might need localization const localizationPatterns = [ /['"`][^'"`]*[а-яА-Я][^'"`]*['"`]/g, // Cyrillic characters /['"`][^'"`]*[中日韓][^'"`]*['"`]/g, // CJK characters /['"`][^'"`]*[àáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ][^'"`]*['"`]/g // Extended Latin ]; lines.forEach((line, index) => { for (const pattern of localizationPatterns) { const matches = line.match(pattern); if (matches) { for (const match of matches) { // Skip if it's in a comment if (line.indexOf(match) > line.indexOf('//') && line.includes('//')) { continue; } findings.push({ id: `localization-${Date.now()}-${index}`, type: FindingType.LOCALIZATION, severity: Severity.LOW, title: 'Non-English Text in Code', description: 'Detected non-English text that may need localization', file: file.path, line: index + 1, evidence: match, suggestion: 'Consider externalizing this text to a localization file or translating to English for international collaboration' }); } } } }); return findings; } analyzePerformance(file, lines) { const findings = []; lines.forEach((line, index) => { const trimmedLine = line.trim(); // Check for synchronous file operations in Node.js if (/fs\.(readFileSync|writeFileSync|existsSync|statSync)/.test(trimmedLine)) { findings.push({ id: `perf-sync-fs-${Date.now()}-${index}`, type: FindingType.PERFORMANCE, severity: Severity.MEDIUM, title: 'Synchronous File Operation', description: 'Synchronous file system operation can block the event loop', file: file.path, line: index + 1, evidence: trimmedLine, suggestion: 'Consider using asynchronous alternatives (fs.promises) to avoid blocking the event loop' }); } // Check for inefficient array operations if (/\.forEach\s*\([^)]*\.push\s*\(/.test(trimmedLine)) { findings.push({ id: `perf-foreach-push-${Date.now()}-${index}`, type: FindingType.PERFORMANCE, severity: Severity.LOW, title: 'Inefficient Array Operation', description: 'Using forEach with push - consider using map() instead', file: file.path, line: index + 1, evidence: trimmedLine, suggestion: 'Use array.map() instead of forEach with push for better performance and readability' }); } // Check for regex in loops if (/for\s*\(.*\{[\s\S]*new\s+RegExp\s*\(/.test(`${line}\n${lines[index + 1] || ''}`)) { findings.push({ id: `perf-regex-loop-${Date.now()}-${index}`, type: FindingType.PERFORMANCE, severity: Severity.MEDIUM, title: 'RegExp Creation in Loop', description: 'Creating RegExp objects inside loops is inefficient', file: file.path, line: index + 1, suggestion: 'Move RegExp creation outside the loop or use literal regex patterns' }); } }); return findings; } createFinding(match, file, pattern) { return { id: `quality-${pattern.id}-${Date.now()}-${match.line}`, type: FindingType.CODE_QUALITY, severity: pattern.severity, title: pattern.name, description: pattern.description, file: file.path, line: match.line, evidence: match.match.trim(), suggestion: pattern.suggestion, references: [`Pattern ID: ${pattern.id}`, `Category: ${pattern.category}`] }; } loadPatterns() { return [ // Debug statements { id: 'debug-001', name: 'Console.log Statement', pattern: /console\.(log|info|warn|error|debug|trace)\s*\(/g, severity: Severity.LOW, description: 'Console logging statement found - should be removed in production', category: 'debug', suggestion: 'Remove console statements or replace with proper logging framework' }, { id: 'debug-002', name: 'Debugger Statement', pattern: /\bdebugger\b/g, severity: Severity.MEDIUM, description: 'Debugger statement found - will pause execution in debugging environments', category: 'debug', suggestion: 'Remove debugger statements before deploying to production' }, { id: 'debug-003', name: 'Alert Statement', pattern: /\balert\s*\(/g, severity: Severity.MEDIUM, description: 'Alert statement found - poor user experience and debugging remnant', category: 'debug', suggestion: 'Replace alert with proper user notification system' }, // Performance issues { id: 'perf-001', name: 'setTimeout with 0 delay', pattern: /setTimeout\s*\([^,]+,\s*0\s*\)/g, severity: Severity.LOW, description: 'setTimeout with 0 delay - consider using setImmediate or process.nextTick', category: 'performance', suggestion: 'Use setImmediate() or process.nextTick() for better performance' }, { id: 'perf-002', name: 'Expensive Operation in Loop', pattern: /for\s*\([^}]*\{[^}]*(?:getElementById|querySelector|querySelectorAll)/g, severity: Severity.MEDIUM, description: 'DOM query inside loop - cache the result outside the loop', category: 'performance', suggestion: 'Cache DOM queries outside loops to improve performance' }, // Style and maintainability { id: 'style-001', name: 'TODO Comment', pattern: /\/\/\s*TODO|\/\*\s*TODO|\#\s*TODO/gi, severity: Severity.LOW, description: 'TODO comment found - consider addressing or tracking in issue tracker', category: 'style', suggestion: 'Address TODO items or move them to your issue tracking system' }, { id: 'style-002', name: 'FIXME Comment', pattern: /\/\/\s*FIXME|\/\*\s*FIXME|\#\s*FIXME/gi, severity: Severity.MEDIUM, description: 'FIXME comment found - indicates known issues that need attention', category: 'style', suggestion: 'Address FIXME comments as they indicate known problems' }, { id: 'style-003', name: 'Magic Number', pattern: /(?<![a-zA-Z0-9_])[0-9]{3,}(?![a-zA-Z0-9_])/g, severity: Severity.LOW, description: 'Magic number detected - consider using named constants', category: 'style', suggestion: 'Replace magic numbers with named constants for better maintainability' }, // Security-related code quality { id: 'security-001', name: 'Commented Out Code', pattern: /\/\/.*(?:function|var|let|const|if|for|while)/g, severity: Severity.LOW, description: 'Commented out code found - clean up or document why it is kept', category: 'style', suggestion: 'Remove commented out code or add explanation for why it is preserved' }, { id: 'security-002', name: 'Empty Catch Block', pattern: /catch\s*\([^)]*\)\s*\{\s*\}/g, severity: Severity.MEDIUM, description: 'Empty catch block - errors are being silently ignored', category: 'security', suggestion: 'Handle errors appropriately or at least log them for debugging' } ]; } addCustomPattern(pattern) { this.patterns.push(pattern); } getPatternCount() { return this.patterns.length; } setSensitivity(level) { this.sensitivity = level; } // Method to clean console.log statements async cleanConsoleStatements(files) { let filesModified = 0; let statementsRemoved = 0; const changes = []; for (const file of files) { const lines = file.content.split('\n'); let fileModified = false; lines.map((line, index) => { const consolePatter = /console\.(log|info|warn|error|debug|trace)\s*\([^;]*\);?/g; const match = line.match(consolePatter); if (match) { const original = line; const modified = line.replace(consolePatter, '').trim(); changes.push({ file: file.path, line: index + 1, original, modified: modified || '// console statement removed' }); statementsRemoved += match.length; fileModified = true; return modified || '// console statement removed'; } return line; }); if (fileModified) { filesModified++; // In a real implementation, you would write the modified content back to the file // file.content = modifiedLines.join('\n'); } } return { filesModified, statementsRemoved, changes }; } } //# sourceMappingURL=quality-analyzer.js.map