UNPKG

sicua

Version:

A tool for analyzing project structure and dependencies

575 lines (574 loc) 25.2 kB
"use strict"; /** * Updated individual metric calculation functions for component scoring * Focus on "easily overlooked" issues that developers miss * Each function returns a score from 0-100 where higher = more problematic */ Object.defineProperty(exports, "__esModule", { value: true }); exports.calculateCircularDependencyScore = calculateCircularDependencyScore; exports.calculateErrorHandlingScore = calculateErrorHandlingScore; exports.calculateComplexityScore = calculateComplexityScore; exports.calculateMaintainabilityScore = calculateMaintainabilityScore; exports.calculateCouplingScore = calculateCouplingScore; exports.calculateTypeIssuesScore = calculateTypeIssuesScore; exports.calculateAccessibilityScore = calculateAccessibilityScore; exports.calculatePerformanceScore = calculatePerformanceScore; exports.calculateSEOScore = calculateSEOScore; exports.calculateZombieCodeScore = calculateZombieCodeScore; exports.calculateTranslationScore = calculateTranslationScore; exports.calculateMagicNumbersScore = calculateMagicNumbersScore; exports.calculateCodeMetricsScore = calculateCodeMetricsScore; exports.calculateDeduplicationScore = calculateDeduplicationScore; exports.calculateComponentFlowScore = calculateComponentFlowScore; exports.calculateReactComplexityScore = calculateReactComplexityScore; exports.calculateContainerComplexityScore = calculateContainerComplexityScore; const ScoringCriteria_1 = require("./ScoringCriteria"); /** * Calculate circular dependency score * Returns 100 if component is in circular dependency, 0 otherwise */ function calculateCircularDependencyScore(context) { if (!context.dependencyAnalysis?.circularDependencies) return 0; const { circularGroups } = context.dependencyAnalysis.circularDependencies; const componentPath = context.component.fullPath; for (const group of circularGroups) { if (group.components.some((comp) => comp === componentPath)) { // More critical if it's a larger circular group or marked as critical const severityMultiplier = group.isCritical ? 1.0 : 0.8; const sizeMultiplier = Math.min(group.size / 5, 1.0); return Math.round(100 * severityMultiplier * sizeMultiplier); } } return 0; } /** * Calculate error handling gaps score - ENHANCED * Higher score for components with risky operations but no error handling */ function calculateErrorHandlingScore(context) { if (!context.errorHandlingAnalysis) return 0; const componentName = context.component.name; const componentResult = context.errorHandlingAnalysis.componentResults[componentName]; if (!componentResult) return 0; let score = 0; let totalRiskyFunctions = 0; let functionsWithProperHandling = 0; let highRiskFunctions = 0; // Analyze function-level error handling with enhanced risk detection for (const funcHandling of componentResult.functionErrorHandling) { if (funcHandling.riskAnalysis.shouldHaveErrorHandling) { totalRiskyFunctions++; // Count high-risk functions (async operations, network calls, etc.) const riskIndicators = funcHandling.riskAnalysis.riskIndicators; if (riskIndicators.hasAsyncOperations || riskIndicators.hasNetworkCalls || riskIndicators.hasFileOperations || riskIndicators.hasDatabaseOperations) { highRiskFunctions++; } if (funcHandling.hasErrorHandling) { functionsWithProperHandling++; } else { // Higher penalty for high-risk operations without error handling const riskMultiplier = riskIndicators.hasAsyncOperations || riskIndicators.hasNetworkCalls ? 2.0 : 1.0; score += funcHandling.riskAnalysis.riskScore * 25 * riskMultiplier; } } } // Additional penalties for missing error boundaries in components with fallback elements if (componentResult.fallbackElements.length > 0 && componentResult.errorBoundaries.length === 0) { score += 40; // Increased penalty } // Higher penalty for high-risk functions without any error handling if (highRiskFunctions > 0 && functionsWithProperHandling === 0) { score += 50; // Critical penalty for async/network operations without error handling } // Calculate percentage of risky functions without error handling if (totalRiskyFunctions > 0) { const uncoveredPercentage = (totalRiskyFunctions - functionsWithProperHandling) / totalRiskyFunctions; score += uncoveredPercentage * 60; // Increased from 50 } return Math.min(Math.round(score), 100); } /** * Calculate complexity score based on cyclomatic and cognitive complexity */ function calculateComplexityScore(context) { if (!context.complexityAnalysis) return 0; const componentName = context.component.name; const cyclomaticComplexity = context.complexityAnalysis.cyclomaticComplexity[componentName] || 0; const cognitiveComplexity = context.complexityAnalysis.cognitiveComplexity[componentName] || 0; // Normalize complexity scores const cyclomaticScore = Math.min((cyclomaticComplexity / ScoringCriteria_1.DEFAULT_SCORING_THRESHOLDS.highCyclomaticComplexity) * 50, 50); const cognitiveScore = Math.min((cognitiveComplexity / ScoringCriteria_1.DEFAULT_SCORING_THRESHOLDS.highCognitiveComplexity) * 50, 50); return Math.round(cyclomaticScore + cognitiveScore); } /** * Calculate maintainability index score (inverted - lower maintainability = higher score) */ function calculateMaintainabilityScore(context) { if (!context.complexityAnalysis) return 0; const componentName = context.component.name; const maintainabilityIndex = context.complexityAnalysis.maintainabilityIndex[componentName]; if (maintainabilityIndex === undefined) return 0; // Lower maintainability index = higher problematic score if (maintainabilityIndex < ScoringCriteria_1.DEFAULT_SCORING_THRESHOLDS.lowMaintainabilityIndex) { const score = ((ScoringCriteria_1.DEFAULT_SCORING_THRESHOLDS.lowMaintainabilityIndex - maintainabilityIndex) / ScoringCriteria_1.DEFAULT_SCORING_THRESHOLDS.lowMaintainabilityIndex) * 100; return Math.round(score); } return 0; } /** * Calculate coupling degree score */ function calculateCouplingScore(context) { if (!context.complexityAnalysis) return 0; const componentName = context.component.name; const couplingDegree = context.complexityAnalysis.couplingDegree[componentName] || 0; const score = Math.min((couplingDegree / ScoringCriteria_1.DEFAULT_SCORING_THRESHOLDS.highCouplingDegree) * 100, 100); return Math.round(score); } /** * Calculate type issues score - ENHANCED * Focus on critical type safety gaps that developers miss */ function calculateTypeIssuesScore(context) { if (!context.typeAnalysis) return 0; const componentName = context.component.name; const componentPath = context.component.fullPath; let score = 0; // Higher penalty for components missing prop types (critical for React components) if (context.typeAnalysis.componentsWithoutPropTypes.includes(componentName)) { score += 50; // Increased from 40 } // Check for any usage - more aggressive detection const anyUsageInComponent = context.typeAnalysis.anyUsageCount || 0; if (anyUsageInComponent > ScoringCriteria_1.DEFAULT_SCORING_THRESHOLDS.highAnyUsage) { score += Math.min(anyUsageInComponent * 15, 40); // Increased penalty } // Check complex types that might indicate over-engineering or poor type design const componentComplexTypes = context.typeAnalysis.complexTypes.filter((ct) => ct.fileName === componentPath); if (componentComplexTypes.length > ScoringCriteria_1.DEFAULT_SCORING_THRESHOLDS.highComplexTypeCount) { score += 25; // Reduced penalty - complex types aren't always bad } // Check for untyped function parameters and returns const functionsWithoutTypes = context.typeAnalysis.regularFunctionsWithoutReturnType || 0; if (functionsWithoutTypes > 0) { score += Math.min(functionsWithoutTypes * 10, 30); } // Bonus reduction for well-typed components with interfaces/types if (context.typeAnalysis.interfacesCount > 0 && !context.typeAnalysis.componentsWithoutPropTypes.includes(componentName)) { score = Math.max(score - 15, 0); // Reward good type practices } return Math.round(score); } /** * NEW: Calculate accessibility issues score * Focus on missing accessibility features that developers often forget */ function calculateAccessibilityScore(context) { if (!context.seoAnalysis) return 0; let score = 0; const componentPath = context.component.fullPath; // Check for missing alt texts on images const imageIssues = context.seoAnalysis.imageOptimization.images.filter((img) => img.usedInPages.includes(componentPath)); let missingAltCount = 0; let totalImages = 0; for (const image of imageIssues) { totalImages++; const hasAltIssue = image.issues.some((issue) => issue.type === "missing-alt"); if (hasAltIssue || !image.attributes.alt) { missingAltCount++; } } // Penalty for missing alt texts if (missingAltCount > 0) { score += Math.min((missingAltCount / ScoringCriteria_1.DEFAULT_SCORING_THRESHOLDS.missingAltTexts) * 40, 40); } // Check for missing ARIA attributes (approximate based on static analysis) const ariaAnalysis = context.seoAnalysis.semanticStructure.accessibility.aria; if (ariaAnalysis.potentialMisuse.length > 0) { score += Math.min(ariaAnalysis.potentialMisuse.length * 10, 30); } // Check for missing labels on form inputs const formIssues = context.seoAnalysis.semanticStructure.accessibility.forms.inputs; const missingLabelsRatio = formIssues.total > 0 ? (formIssues.missingLabels + formIssues.missingAriaLabels) / formIssues.total : 0; if (missingLabelsRatio > 0.1) { // More than 10% missing labels score += missingLabelsRatio * 50; } // Check for missing semantic structure const landmarkUsage = context.seoAnalysis.semanticStructure.landmarkElements; if (componentPath.includes("/pages/") || componentPath.includes("layout")) { // Pages and layouts should have proper landmarks if (landmarkUsage.elements.main === 0) score += 20; if (landmarkUsage.elements.header === 0) score += 15; if (landmarkUsage.elements.nav === 0) score += 10; } return Math.min(Math.round(score), 100); } /** * NEW: Calculate performance issues score * Focus on bundle optimization and loading performance */ function calculatePerformanceScore(context) { if (!context.seoAnalysis) return 0; let score = 0; const componentPath = context.component.fullPath; // Check for missing lazy loading opportunities const lazyLoadingAnalysis = context.seoAnalysis.performance?.lazyLoading; if (lazyLoadingAnalysis) { const componentLazyInfo = lazyLoadingAnalysis.components.filter((comp) => comp.path === componentPath); for (const comp of componentLazyInfo) { if (comp.shouldBeLazyLoaded && !comp.isLazyLoaded) { score += 25; // Penalty for missing lazy loading } } // Check for images that should be lazy loaded const imageLazyInfo = lazyLoadingAnalysis.images.filter((img) => img.usedInPages.includes(componentPath)); let imagesToLazyLoad = 0; for (const img of imageLazyInfo) { if (!img.hasLazyLoading && !img.isAboveFold) { imagesToLazyLoad++; } } if (imagesToLazyLoad > ScoringCriteria_1.DEFAULT_SCORING_THRESHOLDS.missingLazyLoading) { score += Math.min(imagesToLazyLoad * 10, 30); } } // Check for heavy imports (bundle optimization) const bundleAnalysis = context.seoAnalysis.performance?.bundleOptimization; if (bundleAnalysis) { const heavyImports = bundleAnalysis.heavyImports.filter((imp) => imp.importedBy.includes(componentPath)); const highImpactImports = heavyImports.filter((imp) => imp.potentialImpact === "high" && !imp.isDynamic); if (highImpactImports.length > 0) { score += Math.min(highImpactImports.length * 20, 40); } } // Check Core Web Vitals issues const coreWebVitals = context.seoAnalysis.performance?.coreWebVitals; if (coreWebVitals) { const componentIssues = coreWebVitals.potentialIssues.filter((issue) => issue.location === componentPath); const highSeverityIssues = componentIssues.filter((issue) => issue.severity === "high"); score += highSeverityIssues.length * 15; } return Math.min(Math.round(score), 100); } /** * Enhanced SEO scoring that doesn't penalize simple pages */ function calculateSEOScore(context) { if (!context.seoAnalysis) return 0; let score = 0; const componentName = context.component.name; const componentPath = context.component.fullPath; // Check if this is a page component const isPage = componentPath.includes("/pages/") || componentPath.includes("/app/") || componentPath.includes("page.") || componentName.toLowerCase().includes("page"); // For simple pages that just render a client component (like StrMapPage), don't penalize if (isPage && context.component.content) { const content = context.component.content; const lineCount = content.split('\n').length; // If it's a simple page (< 50 lines) with proper metadata, don't penalize if (lineCount < 50 && content.includes('generateMetadata')) { return 0; // Simple pages with metadata are good } } if (isPage) { const pageMetaTags = context.seoAnalysis.metaTags.pages[componentPath]; if (pageMetaTags) { // Only penalize missing critical SEO for complex pages if (!pageMetaTags.title.present) score += 30; // Reduced penalty if (!pageMetaTags.description.present) score += 30; // Reduced penalty // Don't penalize length issues as heavily if (pageMetaTags.title.present && (pageMetaTags.title.length > 70 || pageMetaTags.title.length < 20)) { score += 10; // Reduced penalty } if (pageMetaTags.description.present && (pageMetaTags.description.length > 180 || pageMetaTags.description.length < 100)) { score += 10; // Reduced penalty } // Missing Open Graph - less critical if (!pageMetaTags.openGraph.present) score += 15; // Reduced penalty } } // Reduce other SEO penalties const headingIssues = context.seoAnalysis.semanticStructure.headingHierarchy.hierarchyIssues.filter((issue) => issue.path === componentPath); score += headingIssues.length * 8; // Reduced penalty return Math.min(Math.round(score), 100); } /** * Calculate zombie code score */ function calculateZombieCodeScore(context) { if (!context.dependencyAnalysis?.zombieClusters) return 0; const componentPath = context.component.fullPath; const { clusters } = context.dependencyAnalysis.zombieClusters; for (const cluster of clusters) { if (cluster.components.includes(componentPath)) { // Score based on risk level and cluster size const riskMultiplier = cluster.risk === "high" ? 1.0 : cluster.risk === "medium" ? 0.7 : 0.4; const sizeMultiplier = Math.min(cluster.size / 10, 1.0); return Math.round(100 * riskMultiplier * sizeMultiplier); } } return 0; } /** * Calculate translation issues score */ function calculateTranslationScore(context) { if (!context.translationAnalysis) return 0; const componentPath = context.component.fullPath; let score = 0; // Count missing translations in this component const missingTranslationsInComponent = context.translationAnalysis.missingTranslations.filter((mt) => mt.key.filePath === componentPath).length; if (missingTranslationsInComponent > ScoringCriteria_1.DEFAULT_SCORING_THRESHOLDS.highMissingTranslations) { score += 60; } else { score += (missingTranslationsInComponent / ScoringCriteria_1.DEFAULT_SCORING_THRESHOLDS.highMissingTranslations) * 60; } // Check for duplicate translations used in this component const duplicatesInComponent = context.translationAnalysis.duplicateTranslations.filter((dt) => dt.usages.some((usage) => usage.filePath === componentPath)).length; score += duplicatesInComponent * 5; return Math.min(Math.round(score), 100); } /** * Calculate magic numbers score - REDUCED SENSITIVITY * Less aggressive since magic numbers are often obvious to developers */ function calculateMagicNumbersScore(context) { if (!context.generalAnalysis) return 0; const componentPath = context.component.fullPath; const magicNumbersInComponent = context.generalAnalysis.codeMetrics.magicNumbers.filter((mn) => mn.filePath === componentPath).length; if (magicNumbersInComponent === 0) return 0; // Higher threshold, less sensitive const score = Math.min((magicNumbersInComponent / ScoringCriteria_1.DEFAULT_SCORING_THRESHOLDS.highMagicNumberCount) * 100, 100); return Math.round(score); } /** * Calculate code metrics score (line metrics, comment ratio) - REDUCED IMPORTANCE */ function calculateCodeMetricsScore(context) { if (!context.generalAnalysis) return 0; let score = 0; // Less aggressive scoring for code metrics const codeToCommentRatio = context.generalAnalysis.codeMetrics.codeToCommentRatio; if (codeToCommentRatio < ScoringCriteria_1.DEFAULT_SCORING_THRESHOLDS.lowCodeToCommentRatio) { score += 30; // Reduced from 50 } return Math.round(score); } /** * Calculate deduplication opportunities score */ function calculateDeduplicationScore(context) { if (!context.deduplicationAnalysis) return 0; const componentName = context.component.name; for (const similarity of context.deduplicationAnalysis) { if (similarity.components.includes(componentName)) { if (similarity.similarityScore > ScoringCriteria_1.DEFAULT_SCORING_THRESHOLDS.highSimilarityScore) { return Math.round(similarity.similarityScore * 100); } } } return 0; } /** * Calculate component flow complexity score */ function calculateComponentFlowScore(context) { if (!context.componentFlowAnalysis) return 0; const componentName = context.component.name; let score = 0; // Find this component in the flow analysis for (const route of context.componentFlowAnalysis.routes) { const findComponentInFlow = (node) => { if (node.componentName === componentName) return node; for (const child of node.children || []) { const found = findComponentInFlow(child); if (found) return found; } return null; }; const componentNode = findComponentInFlow(route.pageComponent); if (componentNode) { // Score based on conditional renders const conditionalRenderCount = componentNode.conditionalRenders?.length || 0; if (conditionalRenderCount > ScoringCriteria_1.DEFAULT_SCORING_THRESHOLDS.highConditionalRenderCount) { score += 40; } else { score += (conditionalRenderCount / ScoringCriteria_1.DEFAULT_SCORING_THRESHOLDS.highConditionalRenderCount) * 40; } // Score based on nesting depth const calculateDepth = (node, depth = 0) => { if (!node.children || node.children.length === 0) return depth; return Math.max(...node.children.map((child) => calculateDepth(child, depth + 1))); }; const maxDepth = calculateDepth(componentNode); if (maxDepth > ScoringCriteria_1.DEFAULT_SCORING_THRESHOLDS.deepComponentNesting) { score += 30; } break; } } return Math.min(Math.round(score), 100); } /** * NEW: Calculate React-specific complexity patterns that make components problematic */ function calculateReactComplexityScore(context) { if (!context.component.content) return 0; const content = context.component.content; let score = 0; // Multiple useEffect hooks (like ReportCreationContainer) const useEffectMatches = content.match(/useEffect\s*\(/g); const useEffectCount = useEffectMatches ? useEffectMatches.length : 0; if (useEffectCount > 3) { score += (useEffectCount - 3) * 15; // Heavy penalty for many useEffects } // Multiple useState hooks indicating complex state management const useStateMatches = content.match(/useState\s*[<(]/g); const useStateCount = useStateMatches ? useStateMatches.length : 0; if (useStateCount > 5) { score += (useStateCount - 5) * 8; // Penalty for complex state } // Complex conditional rendering (nested ternary, multiple conditions) const ternaryMatches = content.match(/\?\s*[^:]*:/g); const ternaryCount = ternaryMatches ? ternaryMatches.length : 0; if (ternaryCount > 3) { score += (ternaryCount - 3) * 10; } // Nested JSX expressions indicating complex rendering logic const jsxExpressionMatches = content.match(/{\s*[^}]*{[^}]*}[^}]*}/g); const nestedJsxCount = jsxExpressionMatches ? jsxExpressionMatches.length : 0; if (nestedJsxCount > 2) { score += nestedJsxCount * 8; } // Long functions/components (lines of code) const lineCount = content.split('\n').length; if (lineCount > 150) { score += (lineCount - 150) * 0.3; // Penalty for very long components } // Multiple async operations without proper error handling const asyncMatches = content.match(/async\s+/g); const awaitMatches = content.match(/await\s+/g); const asyncCount = asyncMatches ? asyncMatches.length : 0; const awaitCount = awaitMatches ? awaitMatches.length : 0; const tryMatches = content.match(/try\s*{/g); const tryCount = tryMatches ? tryMatches.length : 0; if ((asyncCount > 2 || awaitCount > 3) && tryCount === 0) { score += 25; // Heavy penalty for async operations without error handling } // Store/context coupling (multiple store imports like ReportCreationContainer) const storeMatches = content.match(/use\w*Store\s*\(/g); const storeCount = storeMatches ? storeMatches.length : 0; if (storeCount > 3) { score += (storeCount - 3) * 12; // Penalty for high store coupling } return Math.min(Math.round(score), 100); } /** * NEW: Calculate container component penalty * Container components should be simple - if they're complex, they're problematic */ function calculateContainerComplexityScore(context) { if (!context.component.content) return 0; const content = context.component.content; const componentName = context.component.name; // Identify if this is likely a container component const containerPatterns = [ /Container$/, /Provider$/, /Wrapper$/, /Manager$/, /Controller$/, ]; const isContainer = containerPatterns.some(pattern => pattern.test(componentName)) || context.component.fullPath.includes('container'); if (!isContainer) return 0; let score = 0; // Containers should delegate, not implement complex logic const implementationPatterns = [ /useEffect\s*\(/g, /useState\s*[<(]/g, /async\s+/g, /setInterval\s*\(/g, /setTimeout\s*\(/g, /fetch\s*\(/g, /axios\./g, ]; implementationPatterns.forEach(pattern => { const matches = content.match(pattern); if (matches && matches.length > 2) { score += matches.length * 8; // Heavy penalty for implementation in containers } }); // Containers with complex JSX are doing too much const jsxComplexity = (content.match(/<\w/g) || []).length; if (jsxComplexity > 10) { score += (jsxComplexity - 10) * 2; } return Math.min(Math.round(score), 100); }