pury
Version:
🛡️ AI-powered security scanner with advanced threat detection, dual reporting system (detailed & summary), and comprehensive code analysis
405 lines • 17.8 kB
JavaScript
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