javascript

exercises

exercises.js⚔
/**
 * 20.5 Code Quality & Coverage - Exercises
 *
 * Practice building code quality tools
 */

// ============================================
// EXERCISE 1: Build a Lint Rule Engine
// ============================================

/**
 * Create a simple lint rule engine that:
 * - Supports custom rules
 * - Reports violations with line numbers
 * - Has fix suggestions
 * - Can auto-fix simple issues
 */

class LintEngine {
  // Your implementation here
}

/*
// SOLUTION:
class LintEngine {
    constructor() {
        this.rules = new Map();
        this.results = [];
    }
    
    // Add a rule
    addRule(name, config) {
        this.rules.set(name, {
            name,
            severity: config.severity || 'error',
            check: config.check,
            message: config.message,
            fix: config.fix || null
        });
    }
    
    // Parse code into lines
    parseCode(code) {
        return code.split('\n').map((content, index) => ({
            number: index + 1,
            content,
            trimmed: content.trim()
        }));
    }
    
    // Run all rules on code
    lint(code, filename = 'unknown') {
        this.results = [];
        const lines = this.parseCode(code);
        
        for (const [name, rule] of this.rules) {
            const violations = rule.check(code, lines);
            
            for (const violation of violations) {
                this.results.push({
                    rule: name,
                    severity: rule.severity,
                    line: violation.line,
                    column: violation.column || 0,
                    message: typeof rule.message === 'function' ? 
                        rule.message(violation) : 
                        rule.message,
                    fixable: !!rule.fix,
                    filename
                });
            }
        }
        
        return this.results.sort((a, b) => a.line - b.line);
    }
    
    // Auto-fix issues
    fix(code) {
        let fixed = code;
        const lines = this.parseCode(code);
        
        for (const [name, rule] of this.rules) {
            if (!rule.fix) continue;
            
            const violations = rule.check(fixed, this.parseCode(fixed));
            
            for (const violation of violations.reverse()) {
                fixed = rule.fix(fixed, violation);
            }
        }
        
        return fixed;
    }
    
    // Generate report
    report() {
        if (this.results.length === 0) {
            return 'No issues found!';
        }
        
        const byFile = {};
        for (const result of this.results) {
            if (!byFile[result.filename]) {
                byFile[result.filename] = [];
            }
            byFile[result.filename].push(result);
        }
        
        let output = '';
        
        for (const [file, issues] of Object.entries(byFile)) {
            output += `\n${file}\n`;
            
            for (const issue of issues) {
                const icon = issue.severity === 'error' ? 'āœ—' : '⚠';
                output += `  ${icon} ${issue.line}:${issue.column} ${issue.message} (${issue.rule})\n`;
            }
        }
        
        const errors = this.results.filter(r => r.severity === 'error').length;
        const warnings = this.results.filter(r => r.severity === 'warning').length;
        
        output += `\n${errors} errors, ${warnings} warnings\n`;
        
        return output;
    }
}

// Create common rules
function createCommonRules(engine) {
    // No console.log
    engine.addRule('no-console', {
        severity: 'warning',
        message: 'Unexpected console statement',
        check: (code, lines) => {
            return lines
                .filter(l => /\bconsole\.(log|warn|error|info)\s*\(/.test(l.content))
                .map(l => ({ line: l.number }));
        },
        fix: (code, violation) => {
            const lines = code.split('\n');
            lines[violation.line - 1] = '// ' + lines[violation.line - 1];
            return lines.join('\n');
        }
    });
    
    // No debugger
    engine.addRule('no-debugger', {
        severity: 'error',
        message: 'Unexpected debugger statement',
        check: (code, lines) => {
            return lines
                .filter(l => /\bdebugger\b/.test(l.content))
                .map(l => ({ line: l.number }));
        },
        fix: (code, violation) => {
            const lines = code.split('\n');
            lines.splice(violation.line - 1, 1);
            return lines.join('\n');
        }
    });
    
    // No var
    engine.addRule('no-var', {
        severity: 'error',
        message: "Use 'const' or 'let' instead of 'var'",
        check: (code, lines) => {
            return lines
                .filter(l => /\bvar\s+/.test(l.content))
                .map(l => ({ line: l.number }));
        },
        fix: (code, violation) => {
            const lines = code.split('\n');
            lines[violation.line - 1] = lines[violation.line - 1].replace(/\bvar\b/, 'let');
            return lines.join('\n');
        }
    });
    
    // No trailing spaces
    engine.addRule('no-trailing-spaces', {
        severity: 'warning',
        message: 'Trailing whitespace',
        check: (code, lines) => {
            return lines
                .filter(l => /\s+$/.test(l.content))
                .map(l => ({ line: l.number }));
        },
        fix: (code, violation) => {
            const lines = code.split('\n');
            lines[violation.line - 1] = lines[violation.line - 1].trimEnd();
            return lines.join('\n');
        }
    });
    
    // Max line length
    engine.addRule('max-len', {
        severity: 'warning',
        message: (v) => `Line exceeds ${v.maxLen} characters (${v.actualLen})`,
        check: (code, lines) => {
            const maxLen = 100;
            return lines
                .filter(l => l.content.length > maxLen)
                .map(l => ({ 
                    line: l.number, 
                    maxLen, 
                    actualLen: l.content.length 
                }));
        }
    });
    
    // Require semicolons
    engine.addRule('semi', {
        severity: 'error',
        message: 'Missing semicolon',
        check: (code, lines) => {
            return lines
                .filter(l => {
                    const t = l.trimmed;
                    // Skip if empty, comment, ends with { } or ends with ,
                    if (!t || t.startsWith('//') || t.startsWith('/*')) return false;
                    if (t.endsWith('{') || t.endsWith('}') || t.endsWith(',')) return false;
                    if (t.endsWith(';')) return false;
                    // Skip control structures
                    if (/^(if|else|for|while|switch|try|catch|finally)\b/.test(t)) return false;
                    // Skip function declarations
                    if (/^(function|class|const\s+\w+\s*=\s*\(|let\s+\w+\s*=\s*\()/.test(t) && t.endsWith(')')) return false;
                    return true;
                })
                .map(l => ({ line: l.number }));
        },
        fix: (code, violation) => {
            const lines = code.split('\n');
            lines[violation.line - 1] = lines[violation.line - 1].trimEnd() + ';';
            return lines.join('\n');
        }
    });
}

// Test the engine
function testLintEngine() {
    console.log('=== Lint Engine Tests ===\n');
    
    const engine = new LintEngine();
    createCommonRules(engine);
    
    const testCode = `
var name = 'test'
const value = 42;
console.log(name);
debugger;
let x = 'a very long line that exceeds the maximum line length limit of 100 characters which should trigger a warning';
function hello() {
    return 'world'   
}
`;
    
    const issues = engine.lint(testCode, 'test.js');
    console.log(engine.report());
    
    console.log('--- After Auto-fix ---\n');
    const fixed = engine.fix(testCode);
    console.log(fixed);
    
    console.log('\n=== Lint Engine Tests Complete ===\n');
}
*/

// ============================================
// EXERCISE 2: Build a Coverage Calculator
// ============================================

/**
 * Create a code coverage calculator that:
 * - Instruments code to track execution
 * - Tracks line, branch, and function coverage
 * - Generates coverage reports
 * - Highlights uncovered code
 */

class CoverageCalculator {
  // Your implementation here
}

/*
// SOLUTION:
class CoverageCalculator {
    constructor() {
        this.files = new Map();
        this._hits = new Map();
    }
    
    // Register a file for coverage
    registerFile(filename, structure) {
        this.files.set(filename, {
            lines: new Set(structure.lines || []),
            branches: structure.branches || [],
            functions: structure.functions || [],
            coveredLines: new Set(),
            coveredBranches: new Set(),
            coveredFunctions: new Set()
        });
        
        this._hits.set(filename, {
            lines: {},
            branches: {},
            functions: {}
        });
    }
    
    // Mark line as executed
    hitLine(filename, lineNumber) {
        const file = this.files.get(filename);
        if (!file) return;
        
        file.coveredLines.add(lineNumber);
        const hits = this._hits.get(filename);
        hits.lines[lineNumber] = (hits.lines[lineNumber] || 0) + 1;
    }
    
    // Mark branch as taken
    hitBranch(filename, branchId, taken) {
        const file = this.files.get(filename);
        if (!file) return;
        
        const key = `${branchId}-${taken ? 'true' : 'false'}`;
        file.coveredBranches.add(key);
        const hits = this._hits.get(filename);
        hits.branches[key] = (hits.branches[key] || 0) + 1;
    }
    
    // Mark function as called
    hitFunction(filename, functionName) {
        const file = this.files.get(filename);
        if (!file) return;
        
        file.coveredFunctions.add(functionName);
        const hits = this._hits.get(filename);
        hits.functions[functionName] = (hits.functions[functionName] || 0) + 1;
    }
    
    // Calculate coverage for a file
    getFileCoverage(filename) {
        const file = this.files.get(filename);
        if (!file) return null;
        
        const totalLines = file.lines.size;
        const coveredLines = file.coveredLines.size;
        
        // Each branch has true/false paths
        const totalBranches = file.branches.length * 2;
        const coveredBranches = file.coveredBranches.size;
        
        const totalFunctions = file.functions.length;
        const coveredFunctions = file.coveredFunctions.size;
        
        const uncoveredLines = [...file.lines].filter(l => !file.coveredLines.has(l));
        
        return {
            filename,
            lines: {
                total: totalLines,
                covered: coveredLines,
                pct: totalLines ? ((coveredLines / totalLines) * 100).toFixed(2) : '100.00',
                uncovered: uncoveredLines
            },
            branches: {
                total: totalBranches,
                covered: coveredBranches,
                pct: totalBranches ? ((coveredBranches / totalBranches) * 100).toFixed(2) : '100.00'
            },
            functions: {
                total: totalFunctions,
                covered: coveredFunctions,
                pct: totalFunctions ? ((coveredFunctions / totalFunctions) * 100).toFixed(2) : '100.00',
                uncovered: file.functions.filter(f => !file.coveredFunctions.has(f))
            }
        };
    }
    
    // Get overall coverage
    getSummary() {
        let totalLines = 0, coveredLines = 0;
        let totalBranches = 0, coveredBranches = 0;
        let totalFunctions = 0, coveredFunctions = 0;
        
        for (const file of this.files.values()) {
            totalLines += file.lines.size;
            coveredLines += file.coveredLines.size;
            totalBranches += file.branches.length * 2;
            coveredBranches += file.coveredBranches.size;
            totalFunctions += file.functions.length;
            coveredFunctions += file.coveredFunctions.size;
        }
        
        return {
            lines: {
                total: totalLines,
                covered: coveredLines,
                pct: totalLines ? ((coveredLines / totalLines) * 100).toFixed(2) : '100.00'
            },
            branches: {
                total: totalBranches,
                covered: coveredBranches,
                pct: totalBranches ? ((coveredBranches / totalBranches) * 100).toFixed(2) : '100.00'
            },
            functions: {
                total: totalFunctions,
                covered: coveredFunctions,
                pct: totalFunctions ? ((coveredFunctions / totalFunctions) * 100).toFixed(2) : '100.00'
            }
        };
    }
    
    // Generate detailed report
    generateReport() {
        const files = [];
        
        for (const filename of this.files.keys()) {
            files.push(this.getFileCoverage(filename));
        }
        
        return {
            files,
            summary: this.getSummary(),
            generatedAt: new Date().toISOString()
        };
    }
    
    // Check thresholds
    checkThresholds(thresholds) {
        const summary = this.getSummary();
        const failures = [];
        
        if (parseFloat(summary.lines.pct) < thresholds.lines) {
            failures.push(`Line coverage ${summary.lines.pct}% below ${thresholds.lines}%`);
        }
        if (parseFloat(summary.branches.pct) < thresholds.branches) {
            failures.push(`Branch coverage ${summary.branches.pct}% below ${thresholds.branches}%`);
        }
        if (parseFloat(summary.functions.pct) < thresholds.functions) {
            failures.push(`Function coverage ${summary.functions.pct}% below ${thresholds.functions}%`);
        }
        
        return {
            passed: failures.length === 0,
            failures
        };
    }
    
    // Print text report
    printReport() {
        const report = this.generateReport();
        
        console.log('\n' + '═'.repeat(65));
        console.log('                    Coverage Report');
        console.log('═'.repeat(65));
        
        for (const file of report.files) {
            console.log(`\nšŸ“ ${file.filename}`);
            console.log(`   Lines:     ${this.bar(file.lines.pct)} ${file.lines.pct}% (${file.lines.covered}/${file.lines.total})`);
            console.log(`   Branches:  ${this.bar(file.branches.pct)} ${file.branches.pct}% (${file.branches.covered}/${file.branches.total})`);
            console.log(`   Functions: ${this.bar(file.functions.pct)} ${file.functions.pct}% (${file.functions.covered}/${file.functions.total})`);
            
            if (file.lines.uncovered.length > 0) {
                console.log(`   Uncovered lines: ${file.lines.uncovered.join(', ')}`);
            }
            if (file.functions.uncovered.length > 0) {
                console.log(`   Uncovered functions: ${file.functions.uncovered.join(', ')}`);
            }
        }
        
        console.log('\n' + '─'.repeat(65));
        console.log('Summary');
        console.log('─'.repeat(65));
        console.log(`Lines:     ${this.bar(report.summary.lines.pct)} ${report.summary.lines.pct}%`);
        console.log(`Branches:  ${this.bar(report.summary.branches.pct)} ${report.summary.branches.pct}%`);
        console.log(`Functions: ${this.bar(report.summary.functions.pct)} ${report.summary.functions.pct}%`);
        console.log('═'.repeat(65) + '\n');
    }
    
    bar(pct) {
        const filled = Math.round(parseFloat(pct) / 5);
        return 'ā–ˆ'.repeat(filled) + 'ā–‘'.repeat(20 - filled);
    }
    
    reset() {
        for (const file of this.files.values()) {
            file.coveredLines.clear();
            file.coveredBranches.clear();
            file.coveredFunctions.clear();
        }
        this._hits.clear();
    }
}

// Test the calculator
function testCoverageCalculator() {
    console.log('=== Coverage Calculator Tests ===\n');
    
    const calc = new CoverageCalculator();
    
    // Register a file
    calc.registerFile('math.js', {
        lines: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
        branches: ['b1', 'b2'],
        functions: ['add', 'subtract', 'multiply', 'divide']
    });
    
    calc.registerFile('utils.js', {
        lines: [1, 2, 3, 4, 5],
        branches: ['b1'],
        functions: ['format', 'parse']
    });
    
    // Simulate test execution
    // math.js - partial coverage
    [1, 2, 3, 4, 5, 6].forEach(l => calc.hitLine('math.js', l));
    calc.hitBranch('math.js', 'b1', true);
    calc.hitBranch('math.js', 'b1', false);
    calc.hitBranch('math.js', 'b2', true);
    calc.hitFunction('math.js', 'add');
    calc.hitFunction('math.js', 'subtract');
    calc.hitFunction('math.js', 'multiply');
    
    // utils.js - full coverage
    [1, 2, 3, 4, 5].forEach(l => calc.hitLine('utils.js', l));
    calc.hitBranch('utils.js', 'b1', true);
    calc.hitBranch('utils.js', 'b1', false);
    calc.hitFunction('utils.js', 'format');
    calc.hitFunction('utils.js', 'parse');
    
    // Print report
    calc.printReport();
    
    // Check thresholds
    const check = calc.checkThresholds({
        lines: 80,
        branches: 75,
        functions: 80
    });
    
    console.log('Threshold check:', check);
    
    console.log('\n=== Coverage Calculator Tests Complete ===\n');
}
*/

// ============================================
// EXERCISE 3: Build a Complexity Reducer
// ============================================

/**
 * Create a tool that:
 * - Analyzes function complexity
 * - Suggests refactoring opportunities
 * - Recommends function splitting
 * - Tracks improvement over time
 */

class ComplexityReducer {
  // Your implementation here
}

/*
// SOLUTION:
class ComplexityReducer {
    constructor() {
        this.history = [];
    }
    
    // Analyze complexity
    analyze(source, functionName = 'anonymous') {
        const metrics = this.calculateMetrics(source);
        
        return {
            name: functionName,
            ...metrics,
            suggestions: this.generateSuggestions(metrics),
            score: this.calculateScore(metrics)
        };
    }
    
    calculateMetrics(source) {
        // Cyclomatic complexity
        const cyclomaticComplexity = this.calculateCyclomatic(source);
        
        // Cognitive complexity
        const cognitiveComplexity = this.calculateCognitive(source);
        
        // Lines of code
        const lines = source.split('\n').filter(l => l.trim() && !l.trim().startsWith('//')).length;
        
        // Nesting depth
        const maxNesting = this.calculateMaxNesting(source);
        
        // Parameter count (if function)
        const paramCount = this.countParameters(source);
        
        return {
            cyclomatic: cyclomaticComplexity,
            cognitive: cognitiveComplexity,
            lines,
            maxNesting,
            paramCount
        };
    }
    
    calculateCyclomatic(source) {
        let complexity = 1;
        
        const patterns = [
            /\bif\s*\(/g,
            /\belse\s+if\s*\(/g,
            /\bfor\s*\(/g,
            /\bwhile\s*\(/g,
            /\bcase\s+/g,
            /\bcatch\s*\(/g,
            /&&/g,
            /\|\|/g,
            /\?(?![\?.])/g  // Ternary, not optional chaining
        ];
        
        for (const pattern of patterns) {
            const matches = source.match(pattern);
            if (matches) complexity += matches.length;
        }
        
        return complexity;
    }
    
    calculateCognitive(source) {
        // Simplified cognitive complexity
        // Adds weight for nested structures
        let complexity = 0;
        let nesting = 0;
        
        const lines = source.split('\n');
        
        for (const line of lines) {
            const trimmed = line.trim();
            
            // Increment nesting
            if (/\b(if|for|while|switch|try)\s*[\(\{]/.test(trimmed)) {
                complexity += 1 + nesting;
                nesting++;
            }
            
            // Binary operators
            const andOr = (trimmed.match(/&&|\|\|/g) || []).length;
            complexity += andOr;
            
            // Decrement nesting
            if (trimmed === '}' || trimmed.endsWith('}')) {
                nesting = Math.max(0, nesting - 1);
            }
        }
        
        return complexity;
    }
    
    calculateMaxNesting(source) {
        let maxNesting = 0;
        let current = 0;
        
        for (const char of source) {
            if (char === '{') {
                current++;
                maxNesting = Math.max(maxNesting, current);
            } else if (char === '}') {
                current = Math.max(0, current - 1);
            }
        }
        
        return maxNesting;
    }
    
    countParameters(source) {
        const match = source.match(/function\s*\w*\s*\(([^)]*)\)/);
        if (!match) {
            const arrowMatch = source.match(/\(([^)]*)\)\s*=>/);
            if (!arrowMatch) return 0;
            return arrowMatch[1].split(',').filter(p => p.trim()).length;
        }
        return match[1].split(',').filter(p => p.trim()).length;
    }
    
    generateSuggestions(metrics) {
        const suggestions = [];
        
        if (metrics.cyclomatic > 10) {
            suggestions.push({
                type: 'split',
                severity: 'high',
                message: 'Function has high cyclomatic complexity. Consider splitting into smaller functions.',
                details: `Complexity: ${metrics.cyclomatic} (recommended: ≤10)`
            });
        } else if (metrics.cyclomatic > 7) {
            suggestions.push({
                type: 'simplify',
                severity: 'medium',
                message: 'Consider simplifying conditional logic.',
                details: `Complexity: ${metrics.cyclomatic} (recommended: ≤7 for easy maintenance)`
            });
        }
        
        if (metrics.cognitive > 15) {
            suggestions.push({
                type: 'restructure',
                severity: 'high',
                message: 'High cognitive complexity makes code hard to understand.',
                details: `Cognitive complexity: ${metrics.cognitive} (recommended: ≤15)`
            });
        }
        
        if (metrics.maxNesting > 4) {
            suggestions.push({
                type: 'flatten',
                severity: 'medium',
                message: 'Deep nesting detected. Consider early returns or guard clauses.',
                details: `Max nesting: ${metrics.maxNesting} (recommended: ≤4)`
            });
        }
        
        if (metrics.lines > 50) {
            suggestions.push({
                type: 'extract',
                severity: 'medium',
                message: 'Long function. Extract helper functions.',
                details: `Lines: ${metrics.lines} (recommended: ≤50)`
            });
        }
        
        if (metrics.paramCount > 4) {
            suggestions.push({
                type: 'refactor-params',
                severity: 'medium',
                message: 'Too many parameters. Consider using an options object.',
                details: `Parameters: ${metrics.paramCount} (recommended: ≤4)`
            });
        }
        
        return suggestions;
    }
    
    calculateScore(metrics) {
        // Higher is better (0-100)
        let score = 100;
        
        // Deduct for high cyclomatic
        if (metrics.cyclomatic > 10) score -= 30;
        else if (metrics.cyclomatic > 7) score -= 15;
        else if (metrics.cyclomatic > 5) score -= 5;
        
        // Deduct for cognitive complexity
        if (metrics.cognitive > 15) score -= 25;
        else if (metrics.cognitive > 10) score -= 10;
        
        // Deduct for deep nesting
        if (metrics.maxNesting > 4) score -= 15;
        else if (metrics.maxNesting > 3) score -= 5;
        
        // Deduct for length
        if (metrics.lines > 50) score -= 15;
        else if (metrics.lines > 30) score -= 5;
        
        return Math.max(0, score);
    }
    
    // Track improvement
    recordSnapshot(functionName, metrics) {
        this.history.push({
            timestamp: Date.now(),
            name: functionName,
            ...metrics
        });
    }
    
    getImprovement(functionName) {
        const records = this.history.filter(h => h.name === functionName);
        if (records.length < 2) return null;
        
        const first = records[0];
        const last = records[records.length - 1];
        
        return {
            cyclomatic: first.cyclomatic - last.cyclomatic,
            cognitive: first.cognitive - last.cognitive,
            lines: first.lines - last.lines,
            improved: last.score > first.score
        };
    }
    
    // Print analysis
    printAnalysis(analysis) {
        console.log(`\nFunction: ${analysis.name}`);
        console.log('─'.repeat(50));
        console.log(`  Cyclomatic Complexity: ${analysis.cyclomatic}`);
        console.log(`  Cognitive Complexity:  ${analysis.cognitive}`);
        console.log(`  Lines of Code:         ${analysis.lines}`);
        console.log(`  Max Nesting:           ${analysis.maxNesting}`);
        console.log(`  Parameters:            ${analysis.paramCount}`);
        console.log(`  Quality Score:         ${analysis.score}/100`);
        
        if (analysis.suggestions.length > 0) {
            console.log('\n  Suggestions:');
            for (const s of analysis.suggestions) {
                const icon = s.severity === 'high' ? 'āŒ' : 'āš ļø';
                console.log(`    ${icon} ${s.message}`);
                console.log(`       ${s.details}`);
            }
        } else {
            console.log('\n  āœ… No issues found!');
        }
    }
}

// Test the reducer
function testComplexityReducer() {
    console.log('=== Complexity Reducer Tests ===\n');
    
    const reducer = new ComplexityReducer();
    
    // Simple function
    const simpleCode = `
function add(a, b) {
    return a + b;
}`;
    
    const simple = reducer.analyze(simpleCode, 'add');
    reducer.printAnalysis(simple);
    
    // Complex function
    const complexCode = `
function processOrder(order, user, config, options, flags) {
    if (!order) {
        return null;
    }
    
    if (!user || !user.id) {
        throw new Error('Invalid user');
    }
    
    let total = 0;
    let discount = 0;
    
    for (const item of order.items) {
        if (item.quantity > 0) {
            if (item.price > 0) {
                total += item.quantity * item.price;
                
                if (item.onSale && config.enableSales) {
                    if (user.isPremium) {
                        discount += item.price * 0.2;
                    } else {
                        discount += item.price * 0.1;
                    }
                }
            }
        }
    }
    
    if (order.coupon) {
        switch (order.coupon.type) {
            case 'percent':
                discount += total * (order.coupon.value / 100);
                break;
            case 'fixed':
                discount += order.coupon.value;
                break;
            case 'bogo':
                // Complex BOGO logic
                for (const item of order.items) {
                    if (item.eligible && item.quantity >= 2) {
                        discount += Math.floor(item.quantity / 2) * item.price;
                    }
                }
                break;
        }
    }
    
    if (total - discount < 0) {
        discount = total;
    }
    
    return {
        subtotal: total,
        discount,
        total: total - discount
    };
}`;
    
    const complex = reducer.analyze(complexCode, 'processOrder');
    reducer.printAnalysis(complex);
    
    console.log('\n=== Complexity Reducer Tests Complete ===\n');
}
*/

// ============================================
// EXERCISE 4: Quality Dashboard
// ============================================

/**
 * Build a quality metrics dashboard that:
 * - Tracks multiple quality metrics
 * - Shows trends over time
 * - Generates visual reports
 * - Alerts on degradation
 */

class QualityDashboard {
  // Your implementation here
}

/*
// SOLUTION:
class QualityDashboard {
    constructor(config = {}) {
        this.snapshots = [];
        this.thresholds = {
            coverage: config.coverageThreshold || 80,
            complexity: config.complexityThreshold || 10,
            issues: config.issueThreshold || 0,
            techDebt: config.techDebtThreshold || 60 // minutes
        };
        this.alerts = [];
    }
    
    // Record a snapshot
    recordSnapshot(metrics) {
        const snapshot = {
            timestamp: Date.now(),
            date: new Date().toISOString(),
            coverage: metrics.coverage,
            complexity: metrics.complexity,
            issues: metrics.issues || { errors: 0, warnings: 0 },
            techDebt: metrics.techDebt || 0,
            tests: metrics.tests || { passed: 0, failed: 0, skipped: 0 }
        };
        
        this.snapshots.push(snapshot);
        this.checkAlerts(snapshot);
        
        return snapshot;
    }
    
    // Check for alerts
    checkAlerts(snapshot) {
        const newAlerts = [];
        
        if (snapshot.coverage.lines < this.thresholds.coverage) {
            newAlerts.push({
                type: 'coverage',
                severity: 'error',
                message: `Coverage dropped to ${snapshot.coverage.lines}%`,
                threshold: this.thresholds.coverage
            });
        }
        
        if (snapshot.complexity.avg > this.thresholds.complexity) {
            newAlerts.push({
                type: 'complexity',
                severity: 'warning',
                message: `Average complexity is ${snapshot.complexity.avg}`,
                threshold: this.thresholds.complexity
            });
        }
        
        if (snapshot.issues.errors > this.thresholds.issues) {
            newAlerts.push({
                type: 'issues',
                severity: 'error',
                message: `${snapshot.issues.errors} lint errors found`,
                threshold: this.thresholds.issues
            });
        }
        
        // Check for degradation
        if (this.snapshots.length >= 2) {
            const prev = this.snapshots[this.snapshots.length - 2];
            
            if (snapshot.coverage.lines < prev.coverage.lines - 5) {
                newAlerts.push({
                    type: 'degradation',
                    severity: 'warning',
                    message: `Coverage dropped ${(prev.coverage.lines - snapshot.coverage.lines).toFixed(1)}%`
                });
            }
            
            if (snapshot.issues.errors > prev.issues.errors) {
                newAlerts.push({
                    type: 'degradation',
                    severity: 'warning',
                    message: `${snapshot.issues.errors - prev.issues.errors} new lint errors`
                });
            }
        }
        
        this.alerts.push(...newAlerts);
        return newAlerts;
    }
    
    // Get trends
    getTrends(days = 7) {
        const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
        const recent = this.snapshots.filter(s => s.timestamp >= cutoff);
        
        if (recent.length < 2) return { insufficient: true };
        
        const first = recent[0];
        const last = recent[recent.length - 1];
        
        return {
            coverage: {
                change: last.coverage.lines - first.coverage.lines,
                direction: last.coverage.lines > first.coverage.lines ? 'up' : 'down',
                values: recent.map(s => s.coverage.lines)
            },
            complexity: {
                change: last.complexity.avg - first.complexity.avg,
                direction: last.complexity.avg < first.complexity.avg ? 'improving' : 'degrading',
                values: recent.map(s => s.complexity.avg)
            },
            issues: {
                change: last.issues.errors - first.issues.errors,
                direction: last.issues.errors < first.issues.errors ? 'improving' : 'degrading',
                values: recent.map(s => s.issues.errors)
            },
            period: {
                start: first.date,
                end: last.date,
                snapshots: recent.length
            }
        };
    }
    
    // Get current status
    getStatus() {
        if (this.snapshots.length === 0) {
            return { status: 'no-data' };
        }
        
        const latest = this.snapshots[this.snapshots.length - 1];
        
        let status = 'passing';
        
        if (latest.coverage.lines < this.thresholds.coverage) {
            status = 'failing';
        } else if (latest.issues.errors > 0) {
            status = 'failing';
        } else if (latest.complexity.avg > this.thresholds.complexity) {
            status = 'warning';
        }
        
        return {
            status,
            latest,
            thresholds: this.thresholds
        };
    }
    
    // Generate ASCII dashboard
    generateDashboard() {
        const status = this.getStatus();
        const trends = this.getTrends();
        
        let dashboard = '';
        
        // Header
        dashboard += '╔══════════════════════════════════════════════════════════════╗\n';
        dashboard += 'ā•‘                    Quality Dashboard                          ā•‘\n';
        dashboard += '╠══════════════════════════════════════════════════════════════╣\n';
        
        if (status.status === 'no-data') {
            dashboard += 'ā•‘  No data available. Record some snapshots first.            ā•‘\n';
            dashboard += 'ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•\n';
            return dashboard;
        }
        
        // Status
        const statusIcon = status.status === 'passing' ? 'āœ…' : status.status === 'warning' ? 'āš ļø' : 'āŒ';
        dashboard += `ā•‘  Status: ${statusIcon} ${status.status.toUpperCase().padEnd(47)} ā•‘\n`;
        dashboard += '╠══════════════════════════════════════════════════════════════╣\n';
        
        // Metrics
        const latest = status.latest;
        
        dashboard += 'ā•‘  Metrics                                                      ā•‘\n';
        dashboard += `ā•‘    Coverage:    ${this.miniBar(latest.coverage.lines)} ${latest.coverage.lines.toFixed(1).padStart(5)}%   ā•‘\n`;
        dashboard += `ā•‘    Complexity:  ${this.miniBar(100 - latest.complexity.avg * 5)} ${latest.complexity.avg.toFixed(1).padStart(5)}     ā•‘\n`;
        dashboard += `ā•‘    Lint Errors: ${latest.issues.errors.toString().padEnd(43)} ā•‘\n`;
        dashboard += `ā•‘    Warnings:    ${latest.issues.warnings.toString().padEnd(43)} ā•‘\n`;
        
        // Trends
        if (!trends.insufficient) {
            dashboard += '╠══════════════════════════════════════════════════════════════╣\n';
            dashboard += 'ā•‘  Trends (7 days)                                             ā•‘\n';
            
            const covArrow = trends.coverage.direction === 'up' ? '↑' : '↓';
            const compArrow = trends.complexity.direction === 'improving' ? '↓' : '↑';
            const issueArrow = trends.issues.direction === 'improving' ? '↓' : '↑';
            
            dashboard += `ā•‘    Coverage:    ${covArrow} ${trends.coverage.change > 0 ? '+' : ''}${trends.coverage.change.toFixed(1)}%                                    ā•‘\n`;
            dashboard += `ā•‘    Complexity:  ${compArrow} ${trends.complexity.change > 0 ? '+' : ''}${trends.complexity.change.toFixed(2)}                                   ā•‘\n`;
            dashboard += `ā•‘    Issues:      ${issueArrow} ${trends.issues.change > 0 ? '+' : ''}${trends.issues.change}                                       ā•‘\n`;
        }
        
        // Alerts
        const recentAlerts = this.alerts.slice(-3);
        if (recentAlerts.length > 0) {
            dashboard += '╠══════════════════════════════════════════════════════════════╣\n';
            dashboard += 'ā•‘  Recent Alerts                                               ā•‘\n';
            
            for (const alert of recentAlerts) {
                const icon = alert.severity === 'error' ? 'āŒ' : 'āš ļø';
                const msg = `${icon} ${alert.message}`.substring(0, 58).padEnd(58);
                dashboard += `ā•‘  ${msg} ā•‘\n`;
            }
        }
        
        dashboard += 'ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•\n';
        
        return dashboard;
    }
    
    miniBar(pct) {
        const filled = Math.round(Math.min(100, Math.max(0, pct)) / 5);
        return 'ā–ˆ'.repeat(filled) + 'ā–‘'.repeat(20 - filled);
    }
    
    // Print dashboard
    printDashboard() {
        console.log(this.generateDashboard());
    }
    
    // Export data
    export() {
        return {
            snapshots: this.snapshots,
            alerts: this.alerts,
            thresholds: this.thresholds,
            currentStatus: this.getStatus(),
            trends: this.getTrends()
        };
    }
}

// Test the dashboard
function testQualityDashboard() {
    console.log('=== Quality Dashboard Tests ===\n');
    
    const dashboard = new QualityDashboard({
        coverageThreshold: 80,
        complexityThreshold: 8
    });
    
    // Simulate snapshots over time
    const baseDate = Date.now() - 7 * 24 * 60 * 60 * 1000;
    
    const snapshots = [
        { coverage: { lines: 75, branches: 70, functions: 80 }, complexity: { avg: 6.5, max: 12 }, issues: { errors: 5, warnings: 10 } },
        { coverage: { lines: 78, branches: 72, functions: 82 }, complexity: { avg: 6.2, max: 11 }, issues: { errors: 3, warnings: 8 } },
        { coverage: { lines: 80, branches: 75, functions: 85 }, complexity: { avg: 5.8, max: 10 }, issues: { errors: 1, warnings: 5 } },
        { coverage: { lines: 82, branches: 78, functions: 88 }, complexity: { avg: 5.5, max: 9 }, issues: { errors: 0, warnings: 3 } },
        { coverage: { lines: 85, branches: 80, functions: 90 }, complexity: { avg: 5.2, max: 8 }, issues: { errors: 0, warnings: 2 } }
    ];
    
    // Record snapshots with simulated dates
    snapshots.forEach((snapshot, i) => {
        const fakeSnapshot = dashboard.recordSnapshot(snapshot);
        fakeSnapshot.timestamp = baseDate + i * 24 * 60 * 60 * 1000;
    });
    
    // Print dashboard
    dashboard.printDashboard();
    
    // Show trends
    console.log('\nTrends:', JSON.stringify(dashboard.getTrends(), null, 2));
    
    console.log('\n=== Quality Dashboard Tests Complete ===\n');
}
*/

// ============================================
// RUN EXERCISES
// ============================================

console.log('=== Code Quality & Coverage Exercises ===\n');
console.log('Implement the following exercises:');
console.log('1. LintEngine - Custom lint rule engine');
console.log('2. CoverageCalculator - Code coverage tracking');
console.log('3. ComplexityReducer - Complexity analysis & suggestions');
console.log('4. QualityDashboard - Quality metrics dashboard');
console.log('');
console.log('Uncomment solutions to verify your implementations.');

// Export
if (typeof module !== 'undefined' && module.exports) {
  module.exports = {
    LintEngine,
    CoverageCalculator,
    ComplexityReducer,
    QualityDashboard,
  };
}
Exercises - JavaScript Tutorial | DeepML