UNPKG

@digitalnodecom/node-red-contrib-analyzer

Version:

A Node-RED global service that monitors function nodes for debugging artifacts and performance issues. Features real-time quality metrics, Vue.js dashboard, and comprehensive code analysis.

389 lines (331 loc) 16.3 kB
const { detectDebuggingTraits } = require('./detector'); class QualityMetrics { constructor() { // Quality scoring weights - STRICT SCORING this.weights = { // Critical issues (Level 1) - SEVERELY PENALIZED 'top-level-return': 60, // Critical: worst possible score impact 'debugger-statement': 50, // Critical: debug code in production // Important issues (Level 2) - HEAVILY PENALIZED 'console-log': 25, // Major: logging clutter 'node-warn': 30, // Major: debug output 'todo-comment': 15, // Important: unfinished work 'unused-variable': 10, // Important: code quality // Minor issues (Level 3) - MODERATELY PENALIZED 'hardcoded-test': 12, // Moderate: test artifacts 'multiple-empty-lines': 3 // Minor: formatting }; // Complexity factors this.complexityFactors = { linesOfCode: 0.1, cyclomaticComplexity: 2.0, nestingDepth: 1.5, functionCount: 0.5 }; } // Calculate quality score for a single node - STRICT SCORING calculateNodeQualityScore(issues, linesOfCode = 0) { let baseScore = 100; let totalDeduction = 0; let hasCriticalIssues = false; // Deduct points based on issues found issues.forEach(issue => { const weight = this.weights[issue.type] || 1; const severity = this.getIssueSeverity(issue.type); if (severity.level === 'critical') { hasCriticalIssues = true; // Critical issues get exponential penalty totalDeduction += weight * 1.5; // 1.5x multiplier for critical } else { totalDeduction += weight; } }); // Critical penalty: Any critical issue caps score at 40 maximum if (hasCriticalIssues) { baseScore = Math.min(baseScore, 40); } // Apply penalty scaling based on lines of code const sizeMultiplier = Math.min(1 + (linesOfCode / 150), 2.5); // More aggressive scaling totalDeduction *= sizeMultiplier; // Multiple critical issues = near-zero score const criticalCount = issues.filter(issue => this.getIssueSeverity(issue.type).level === 'critical').length; if (criticalCount >= 2) { baseScore = Math.min(baseScore, 15); // Maximum 15 points with 2+ critical issues } const qualityScore = Math.max(0, baseScore - totalDeduction); return Math.round(qualityScore * 100) / 100; } // Calculate complexity score for code calculateComplexityScore(code) { if (!code || typeof code !== 'string') { return 0; } let complexity = 0; const lines = code.split('\n'); const linesOfCode = lines.filter(line => line.trim() && !line.trim().startsWith('//')).length; // Lines of code factor complexity += linesOfCode * this.complexityFactors.linesOfCode; // Cyclomatic complexity indicators const cyclomaticKeywords = [ /\bif\s*\(/g, /\bwhile\s*\(/g, /\bfor\s*\(/g, /\bcatch\s*\(/g, /\bswitch\s*\(/g, /\bcase\s+/g, /&&|\|\|/g, /\?.*:/g // Logical operators and ternary ]; cyclomaticKeywords.forEach(pattern => { const matches = (code.match(pattern) || []).length; complexity += matches * this.complexityFactors.cyclomaticComplexity; }); // Nesting depth (approximate by counting braces) let maxNesting = 0; let currentNesting = 0; for (const char of code) { if (char === '{') { currentNesting++; maxNesting = Math.max(maxNesting, currentNesting); } else if (char === '}') { currentNesting--; } } complexity += maxNesting * this.complexityFactors.nestingDepth; // Function count const functionCount = (code.match(/function\s+\w+\s*\(/g) || []).length; complexity += functionCount * this.complexityFactors.functionCount; return Math.round(complexity * 100) / 100; } // Calculate flow-level quality metrics - STRICT FAULTY NODE WEIGHTING calculateFlowQualityMetrics(nodeConfigs, detectionLevel = 2) { let totalIssues = 0; let nodesWithIssues = 0; let nodesWithCriticalIssues = 0; let totalFunctionNodes = 0; let totalComplexity = 0; const issueTypes = new Set(); const nodeMetrics = []; let totalQualityScore = 0; nodeConfigs.forEach(nodeConfig => { if (nodeConfig.type === 'function' && nodeConfig.func) { totalFunctionNodes++; const issues = detectDebuggingTraits(nodeConfig.func, detectionLevel); const linesOfCode = nodeConfig.func.split('\n').length; const complexityScore = this.calculateComplexityScore(nodeConfig.func); const qualityScore = this.calculateNodeQualityScore(issues, linesOfCode); totalComplexity += complexityScore; totalIssues += issues.length; totalQualityScore += qualityScore; const hasCriticalIssues = issues.some(issue => this.getIssueSeverity(issue.type).level === 'critical'); if (issues.length > 0) { nodesWithIssues++; if (hasCriticalIssues) { nodesWithCriticalIssues++; } issues.forEach(issue => issueTypes.add(issue.type)); } nodeMetrics.push({ nodeId: nodeConfig.id, nodeName: nodeConfig.name || `Function Node ${nodeConfig.id.substring(0, 8)}`, issuesCount: issues.length, issueDetails: issues, complexityScore: complexityScore, linesOfCode: linesOfCode, qualityScore: qualityScore, hasCriticalIssues: hasCriticalIssues }); } }); // STRICT FLOW QUALITY CALCULATION - based on faulty nodes with critical weighting let flowQualityScore = 100; if (totalFunctionNodes > 0) { // Use weighted average of individual node scores const avgNodeQuality = totalQualityScore / totalFunctionNodes; flowQualityScore = avgNodeQuality; // HEAVY PENALTY for nodes with critical issues const criticalNodeRatio = nodesWithCriticalIssues / totalFunctionNodes; flowQualityScore -= (criticalNodeRatio * 60); // Up to 60 point deduction // MODERATE PENALTY for general faulty nodes const faultyNodeRatio = nodesWithIssues / totalFunctionNodes; flowQualityScore -= (faultyNodeRatio * 25); // Additional penalty // COMPLEXITY PENALTY (only apply if there are actual issues) if (nodesWithIssues > 0) { const avgComplexityScore = totalComplexity / totalFunctionNodes; const complexityPenalty = Math.min(avgComplexityScore / 1.5, 40); // More aggressive flowQualityScore -= complexityPenalty; } } // Any flow with critical issues cannot exceed 50 points if (nodesWithCriticalIssues > 0) { flowQualityScore = Math.min(flowQualityScore, 50); } // Flows with >50% faulty nodes cannot exceed 30 points if (totalFunctionNodes > 0 && (nodesWithIssues / totalFunctionNodes) > 0.5) { flowQualityScore = Math.min(flowQualityScore, 30); } return { totalIssues, nodesWithIssues, nodesWithCriticalIssues, totalFunctionNodes, issueTypes: Array.from(issueTypes), qualityScore: Math.max(0, Math.round(flowQualityScore * 100) / 100), complexityScore: Math.round((totalFunctionNodes > 0 ? totalComplexity / totalFunctionNodes : 0) * 100) / 100, nodeMetrics }; } // Calculate overall system quality trends - STRICT CRITICAL WEIGHTING calculateSystemQualityTrends(allFlowMetrics) { if (!allFlowMetrics || allFlowMetrics.length === 0) { return { overallQuality: 100, technicalDebt: 0, complexity: 0, flowCount: 0, affectedNodes: 0, criticalNodes: 0 }; } const totalFlows = allFlowMetrics.length; const totalNodes = allFlowMetrics.reduce((sum, flow) => sum + flow.totalFunctionNodes, 0); const totalIssues = allFlowMetrics.reduce((sum, flow) => sum + flow.totalIssues, 0); const totalAffectedNodes = allFlowMetrics.reduce((sum, flow) => sum + flow.nodesWithIssues, 0); const totalCriticalNodes = allFlowMetrics.reduce((sum, flow) => sum + (flow.nodesWithCriticalIssues || 0), 0); // Overall quality (weighted average of flow qualities) const totalWeightedQuality = allFlowMetrics.reduce((sum, flow) => { return sum + (flow.qualityScore * flow.totalFunctionNodes); }, 0); let overallQuality = totalNodes > 0 ? totalWeightedQuality / totalNodes : 100; // CRITICAL SYSTEM PENALTIES if (totalNodes > 0) { // Any critical nodes in system severely impact overall quality const criticalNodeRatio = totalCriticalNodes / totalNodes; overallQuality -= (criticalNodeRatio * 70); // Up to 70 point penalty // Systems with >25% faulty nodes get capped const faultyNodeRatio = totalAffectedNodes / totalNodes; if (faultyNodeRatio > 0.25) { overallQuality = Math.min(overallQuality, 60); } // Systems with any critical nodes cannot exceed 65 points if (totalCriticalNodes > 0) { overallQuality = Math.min(overallQuality, 65); } } // Technical debt metric - MORE AGGRESSIVE SCALING const technicalDebt = totalNodes > 0 ? Math.min((totalIssues / totalNodes) * 35, 100) : 0; // Add critical node weighting to technical debt const criticalDebtBonus = totalNodes > 0 ? (totalCriticalNodes / totalNodes) * 40 : 0; const finalTechnicalDebt = Math.min(technicalDebt + criticalDebtBonus, 100); // Overall complexity (weighted average) const totalWeightedComplexity = allFlowMetrics.reduce((sum, flow) => { return sum + (flow.complexityScore * flow.totalFunctionNodes); }, 0); const overallComplexity = totalNodes > 0 ? totalWeightedComplexity / totalNodes : 0; return { overallQuality: Math.max(0, Math.round(overallQuality * 100) / 100), technicalDebt: Math.round(finalTechnicalDebt * 100) / 100, complexity: Math.round(overallComplexity * 100) / 100, flowCount: totalFlows, affectedNodes: totalAffectedNodes, criticalNodes: totalCriticalNodes }; } // Get quality grade based on score - STRICT GRADING getQualityGrade(score) { if (score >= 98) return { grade: 'A+', color: '#22c55e', description: 'Excellent' }; if (score >= 95) return { grade: 'A', color: '#16a34a', description: 'Very Good' }; if (score >= 90) return { grade: 'A-', color: '#65a30d', description: 'Good' }; if (score >= 85) return { grade: 'B+', color: '#84cc16', description: 'Above Average' }; if (score >= 80) return { grade: 'B', color: '#eab308', description: 'Average' }; if (score >= 70) return { grade: 'B-', color: '#f59e0b', description: 'Below Average' }; if (score >= 60) return { grade: 'C+', color: '#f97316', description: 'Fair' }; if (score >= 50) return { grade: 'C', color: '#ea580c', description: 'Poor' }; if (score >= 35) return { grade: 'D', color: '#dc2626', description: 'Very Poor' }; if (score >= 20) return { grade: 'D-', color: '#b91c1c', description: 'Critical' }; return { grade: 'F', color: '#991b1b', description: 'Failing' }; } // Get issue severity classification getIssueSeverity(issueType) { if (['top-level-return', 'debugger-statement'].includes(issueType)) { return { level: 'critical', color: '#dc2626', priority: 1 }; } if (['console-log', 'node-warn', 'unused-variable', 'todo-comment'].includes(issueType)) { return { level: 'warning', color: '#f59e0b', priority: 2 }; } return { level: 'info', color: '#3b82f6', priority: 3 }; } // Generate quality report for a flow generateFlowQualityReport(flowMetrics) { const grade = this.getQualityGrade(flowMetrics.qualityScore); const criticalIssues = flowMetrics.issueTypes.filter(type => this.getIssueSeverity(type).level === 'critical' ).length; const warningIssues = flowMetrics.issueTypes.filter(type => this.getIssueSeverity(type).level === 'warning' ).length; return { ...flowMetrics, grade: grade, criticalIssues, warningIssues, healthPercentage: Math.round((flowMetrics.totalFunctionNodes - flowMetrics.nodesWithIssues) / Math.max(1, flowMetrics.totalFunctionNodes) * 100), recommendations: this.generateRecommendations(flowMetrics) }; } // Generate improvement recommendations generateRecommendations(flowMetrics) { const recommendations = []; if (flowMetrics.totalIssues === 0) { recommendations.push({ type: 'success', message: 'Excellent! No code quality issues detected.', action: 'Maintain current coding standards' }); return recommendations; } // Critical issues if (flowMetrics.issueTypes.includes('top-level-return')) { recommendations.push({ type: 'critical', message: 'Remove top-level return statements', action: 'Refactor functions to use proper control flow' }); } if (flowMetrics.issueTypes.includes('debugger-statement')) { recommendations.push({ type: 'critical', message: 'Remove debugger statements', action: 'Clean up debugging code before deployment' }); } // Warning issues if (flowMetrics.issueTypes.includes('console-log')) { recommendations.push({ type: 'warning', message: 'Replace console.log with proper logging', action: 'Use node.log() or remove debugging statements' }); } if (flowMetrics.issueTypes.includes('unused-variable')) { recommendations.push({ type: 'info', message: 'Remove unused variables', action: 'Clean up variable declarations to improve readability' }); } if (flowMetrics.complexityScore > 20) { recommendations.push({ type: 'warning', message: 'High code complexity detected', action: 'Consider breaking down complex functions into smaller ones' }); } if (flowMetrics.nodesWithIssues / flowMetrics.totalFunctionNodes > 0.5) { recommendations.push({ type: 'warning', message: 'Many nodes have quality issues', action: 'Focus on systematic code review and refactoring' }); } return recommendations; } } module.exports = QualityMetrics;