javascript
exercises
exercises.jsā”javascript
/**
* 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,
};
}