sicua
Version:
A tool for analyzing project structure and dependencies
493 lines (492 loc) • 22.5 kB
JavaScript
;
/**
* Updated component scoring analyzer with enhanced pattern detection
* and support for new metrics (accessibility, performance)
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ComponentScoringAnalyzer = void 0;
const ScoringCriteria_1 = require("./ScoringCriteria");
const ScoringAggregator_1 = require("./ScoringAggregator");
const ComponentFilter_1 = require("../seo/utils/ComponentFilter");
class ComponentScoringAnalyzer {
constructor(options = {}) {
this.options = {
weights: ScoringCriteria_1.DEFAULT_SCORING_WEIGHTS,
multipliers: ScoringCriteria_1.DEFAULT_SCORING_MULTIPLIERS,
includeScoreBreakdown: false,
minimumScore: 0,
excludeFileTypes: [],
...options,
};
this.scoringAggregator = new ScoringAggregator_1.ScoringAggregator(this.options.weights, this.options.multipliers);
}
/**
* Calculate top scoring (most problematic) components with component filtering
*/
async calculateTopScoringComponents(components, analyzerResults, topCount = 20) {
const report = await this.generateScoringReport(components, analyzerResults);
return report.topComponents.slice(0, topCount);
}
/**
* Generate a comprehensive scoring report with relative normalization
*/
async generateScoringReport(components, analyzerResults) {
// Filter to only include actual React components
const actualComponents = ComponentFilter_1.ComponentFilter.filterComponents(components);
const rawScoredComponents = [];
const excludedComponents = {
constants: 0,
configs: 0,
types: 0,
total: 0,
};
// Count filtered out components
const filteredOut = components.length - actualComponents.length;
excludedComponents.total = filteredOut;
// First pass: Calculate raw scores for all components
for (const component of actualComponents) {
const fileType = (0, ScoringCriteria_1.getFileType)(component.name, component.fullPath);
// Skip if file type is in exclusion list
if (this.options.excludeFileTypes?.includes(fileType)) {
excludedComponents[fileType]++;
excludedComponents.total++;
continue;
}
const context = {
component,
allComponents: actualComponents,
...analyzerResults,
};
const scoringResult = this.scoringAggregator.calculateScore(context);
// Only include components meeting minimum score threshold
if (scoringResult.finalScore >= (this.options.minimumScore || 0)) {
rawScoredComponents.push({
component,
rawScore: scoringResult.finalScore,
scoringResult,
});
}
}
// Apply relative normalization
const normalizedComponents = this.applyRelativeNormalization(rawScoredComponents);
// Build final scored components with file type stats
const fileTypeStats = {};
const detailedResults = new Map();
const scoredComponents = normalizedComponents.map(({ component, normalizedScore, scoringResult }) => {
const fileType = (0, ScoringCriteria_1.getFileType)(component.name, component.fullPath);
// Track file type statistics using normalized scores
if (!fileTypeStats[fileType]) {
fileTypeStats[fileType] = { count: 0, totalScore: 0 };
}
fileTypeStats[fileType].count++;
fileTypeStats[fileType].totalScore += normalizedScore;
if (this.options.includeScoreBreakdown) {
detailedResults.set(component.name, {
...scoringResult,
finalScore: normalizedScore, // Use normalized score in breakdown
});
}
return {
...component,
score: normalizedScore,
};
});
// Sort by normalized score (highest first - most problematic)
scoredComponents.sort((a, b) => b.score - a.score);
// Calculate statistics with normalized scores
const statistics = this.calculateStatistics(scoredComponents, fileTypeStats);
const categoryBreakdown = this.calculateCategoryBreakdown(scoredComponents);
return {
topComponents: scoredComponents,
scoringStatistics: statistics,
categoryBreakdown,
detailedResults: this.options.includeScoreBreakdown
? detailedResults
: undefined,
excludedComponents,
};
}
/**
* Apply relative normalization to create meaningful comparative scores
*/
applyRelativeNormalization(rawScoredComponents) {
if (rawScoredComponents.length === 0) {
return [];
}
const rawScores = rawScoredComponents.map((item) => item.rawScore);
const maxScore = Math.max(...rawScores);
const minScore = Math.min(...rawScores);
const scoreRange = maxScore - minScore;
// Calculate percentiles for better distribution
const sortedScores = [...rawScores].sort((a, b) => a - b);
const q1Index = Math.floor(sortedScores.length * 0.25);
const q3Index = Math.floor(sortedScores.length * 0.75);
const q1Score = sortedScores[q1Index];
const q3Score = sortedScores[q3Index];
return rawScoredComponents.map(({ component, rawScore, scoringResult }) => {
let normalizedScore = 0;
if (scoreRange > 0) {
// Method 1: Min-max normalization to 0-100 scale
const minMaxNormalized = ((rawScore - minScore) / scoreRange) * 100;
// Method 2: Percentile-based normalization for better distribution
let percentileScore = 0;
if (rawScore <= q1Score) {
// Bottom 25% -> 0-25 range
percentileScore = (rawScore / q1Score) * 25;
}
else if (rawScore <= q3Score) {
// Middle 50% -> 25-75 range
percentileScore =
25 + ((rawScore - q1Score) / (q3Score - q1Score)) * 50;
}
else {
// Top 25% -> 75-100 range
percentileScore =
75 + ((rawScore - q3Score) / (maxScore - q3Score)) * 25;
}
// Blend the two approaches: 70% min-max, 30% percentile
normalizedScore = minMaxNormalized * 0.7 + percentileScore * 0.3;
// Apply logarithmic scaling for better spread in the upper range
if (normalizedScore > 50) {
const upperRange = normalizedScore - 50;
const logScaled = 50 + (upperRange * Math.log10(upperRange + 1)) / Math.log10(51);
normalizedScore = Math.min(logScaled, 100);
}
}
else {
// All components have the same score - give them middle scores
normalizedScore = 50;
}
return {
component,
normalizedScore: Math.round(normalizedScore * 100) / 100,
scoringResult,
};
});
}
/**
* Enhanced summary report that explains the normalization
*/
generateSummaryReport(report) {
const summary = [];
summary.push("Component Scoring Analysis Summary (Relative Scoring)");
summary.push("=====================================================");
summary.push("");
summary.push(`Total Components Analyzed: ${report.scoringStatistics.totalComponents}`);
summary.push(`Average Score: ${report.scoringStatistics.averageScore}/100 (normalized)`);
summary.push(`Highest Score: ${report.scoringStatistics.highestScore}/100 (most problematic)`);
summary.push(`Lowest Score: ${report.scoringStatistics.lowestScore}/100 (least problematic)`);
summary.push("");
summary.push("📊 Scoring Method: Relative normalization within project context");
summary.push(" • Scores are normalized to 0-100 based on component comparison");
summary.push(" • Higher scores indicate more problematic components relative to your project");
summary.push(" • A score of 80+ means this component is among the most complex in your codebase");
summary.push("");
// File type distribution
if (Object.keys(report.scoringStatistics.fileTypeDistribution).length > 0) {
summary.push("Score by File Type (normalized):");
Object.entries(report.scoringStatistics.fileTypeDistribution)
.sort(([, a], [, b]) => b.averageScore - a.averageScore)
.forEach(([fileType, stats]) => {
summary.push(` ${fileType}: ${stats.averageScore}/100 (${stats.count} files)`);
});
summary.push("");
}
// Excluded components
if (report.excludedComponents && report.excludedComponents.total > 0) {
summary.push("Excluded Components:");
if (report.excludedComponents.constants > 0) {
summary.push(` Constants files: ${report.excludedComponents.constants}`);
}
if (report.excludedComponents.configs > 0) {
summary.push(` Config files: ${report.excludedComponents.configs}`);
}
if (report.excludedComponents.types > 0) {
summary.push(` Type files: ${report.excludedComponents.types}`);
}
summary.push(` Total excluded: ${report.excludedComponents.total}`);
summary.push("");
}
summary.push("Score Distribution (relative to your project):");
summary.push(` Critical (80-100): ${report.scoringStatistics.scoreDistribution.CRITICAL} components - Highest complexity in your project`);
summary.push(` High (60-79): ${report.scoringStatistics.scoreDistribution.HIGH} components - Above average complexity`);
summary.push(` Medium (40-59): ${report.scoringStatistics.scoreDistribution.MEDIUM} components - Average complexity`);
summary.push(` Low (20-39): ${report.scoringStatistics.scoreDistribution.LOW} components - Below average complexity`);
summary.push(` Minimal (0-19): ${report.scoringStatistics.scoreDistribution.MINIMAL} components - Lowest complexity in your project`);
summary.push("");
summary.push("Top 10 Most Problematic Components (relative to your project):");
const top10 = report.topComponents.slice(0, 10);
top10.forEach((component, index) => {
const fileType = (0, ScoringCriteria_1.getFileType)(component.name, component.fullPath);
summary.push(` ${index + 1}. ${component.name} (${component.score}/100) [${fileType}]`);
});
return summary.join("\n");
}
/**
* Get detailed scoring breakdown for a specific component with filtering
*/
async getComponentScoreBreakdown(component, analyzerResults) {
// Check if this is an actual component
if (!ComponentFilter_1.ComponentFilter.isActualComponent(component)) {
return `Component "${component.name}" is not a React component (utility function, hook, or handler) and was excluded from scoring.`;
}
const context = {
component,
allComponents: [component], // Minimal context for single component analysis
...analyzerResults,
};
const scoringResult = this.scoringAggregator.calculateScore(context);
return this.scoringAggregator.getScoreBreakdown(scoringResult);
}
/**
* Calculate scoring statistics with file type breakdown
*/
calculateStatistics(scoredComponents, fileTypeStats) {
if (scoredComponents.length === 0) {
return {
totalComponents: 0,
averageScore: 0,
scoreDistribution: {
CRITICAL: 0,
HIGH: 0,
MEDIUM: 0,
LOW: 0,
MINIMAL: 0,
},
highestScore: 0,
lowestScore: 0,
fileTypeDistribution: {},
};
}
const scores = scoredComponents.map((c) => c.score);
const totalComponents = scoredComponents.length;
const averageScore = scores.reduce((sum, score) => sum + score, 0) / totalComponents;
const highestScore = Math.max(...scores);
const lowestScore = Math.min(...scores);
// Calculate score distribution
const scoreDistribution = {
CRITICAL: 0,
HIGH: 0,
MEDIUM: 0,
LOW: 0,
MINIMAL: 0,
};
for (const component of scoredComponents) {
const score = component.score;
if (score >= ScoringCriteria_1.SCORE_RANGES.CRITICAL.min) {
scoreDistribution.CRITICAL++;
}
else if (score >= ScoringCriteria_1.SCORE_RANGES.HIGH.min) {
scoreDistribution.HIGH++;
}
else if (score >= ScoringCriteria_1.SCORE_RANGES.MEDIUM.min) {
scoreDistribution.MEDIUM++;
}
else if (score >= ScoringCriteria_1.SCORE_RANGES.LOW.min) {
scoreDistribution.LOW++;
}
else {
scoreDistribution.MINIMAL++;
}
}
// Calculate file type distribution
const fileTypeDistribution = {};
for (const [fileType, stats] of Object.entries(fileTypeStats)) {
fileTypeDistribution[fileType] = {
count: stats.count,
averageScore: Math.round((stats.totalScore / stats.count) * 100) / 100,
};
}
return {
totalComponents,
averageScore: Math.round(averageScore * 100) / 100,
scoreDistribution,
highestScore: Math.round(highestScore * 100) / 100,
lowestScore: Math.round(lowestScore * 100) / 100,
fileTypeDistribution,
};
}
/**
* Calculate category breakdown for reporting
*/
calculateCategoryBreakdown(scoredComponents) {
const breakdown = {
criticalIssues: 0,
highPriorityIssues: 0,
mediumPriorityIssues: 0,
lowPriorityIssues: 0,
minimalIssues: 0,
};
for (const component of scoredComponents) {
const score = component.score;
if (score >= ScoringCriteria_1.SCORE_RANGES.CRITICAL.min) {
breakdown.criticalIssues++;
}
else if (score >= ScoringCriteria_1.SCORE_RANGES.HIGH.min) {
breakdown.highPriorityIssues++;
}
else if (score >= ScoringCriteria_1.SCORE_RANGES.MEDIUM.min) {
breakdown.mediumPriorityIssues++;
}
else if (score >= ScoringCriteria_1.SCORE_RANGES.LOW.min) {
breakdown.lowPriorityIssues++;
}
else {
breakdown.minimalIssues++;
}
}
return breakdown;
}
/**
* Filter components by score range
*/
filterComponentsByScoreRange(scoredComponents, range) {
const scoreRange = ScoringCriteria_1.SCORE_RANGES[range];
return scoredComponents.filter((component) => component.score >= scoreRange.min && component.score <= scoreRange.max);
}
/**
* Get components with specific problematic patterns - ENHANCED
*/
getComponentsWithPattern(scoredComponents, analyzerResults, pattern) {
return scoredComponents.filter((component) => {
switch (pattern) {
case "circular-dependencies":
return this.hasCircularDependencies(component, analyzerResults);
case "missing-error-handling":
return this.hasMissingErrorHandling(component, analyzerResults);
case "high-complexity":
return this.hasHighComplexity(component, analyzerResults);
case "type-issues":
return this.hasTypeIssues(component, analyzerResults);
case "seo-issues":
return this.hasSEOIssues(component, analyzerResults);
case "accessibility-issues":
return this.hasAccessibilityIssues(component, analyzerResults);
case "performance-issues":
return this.hasPerformanceIssues(component, analyzerResults);
default:
return false;
}
});
}
/**
* Check if component has circular dependencies
*/
hasCircularDependencies(component, results) {
if (!results.dependencyAnalysis?.circularDependencies)
return false;
return results.dependencyAnalysis.circularDependencies.circularGroups.some((group) => group.components.includes(component.fullPath));
}
/**
* Check if component has missing error handling
*/
hasMissingErrorHandling(component, results) {
if (!results.errorHandlingAnalysis)
return false;
const componentResult = results.errorHandlingAnalysis.componentResults[component.name];
if (!componentResult)
return false;
return componentResult.functionErrorHandling.some((func) => func.riskAnalysis.shouldHaveErrorHandling && !func.hasErrorHandling);
}
/**
* Check if component has high complexity
*/
hasHighComplexity(component, results) {
if (!results.complexityAnalysis)
return false;
const cyclomaticComplexity = results.complexityAnalysis.cyclomaticComplexity[component.name] || 0;
const cognitiveComplexity = results.complexityAnalysis.cognitiveComplexity[component.name] || 0;
return cyclomaticComplexity > 10 || cognitiveComplexity > 15;
}
/**
* Check if component has type issues
*/
hasTypeIssues(component, results) {
if (!results.typeAnalysis)
return false;
return (results.typeAnalysis.componentsWithoutPropTypes.includes(component.name) ||
results.typeAnalysis.complexTypes.some((ct) => ct.fileName === component.fullPath));
}
/**
* NEW: Check if component has SEO issues
*/
hasSEOIssues(component, results) {
if (!results.seoAnalysis)
return false;
const componentPath = component.fullPath;
// Check if it's a page with SEO issues
const isPage = componentPath.includes("/pages/") || componentPath.includes("/app/");
if (isPage) {
const pageMetaTags = results.seoAnalysis.metaTags.pages[componentPath];
if (!pageMetaTags ||
!pageMetaTags.title.present ||
!pageMetaTags.description.present) {
return true;
}
}
// Check for heading hierarchy issues
const headingIssues = results.seoAnalysis.semanticStructure.headingHierarchy.hierarchyIssues.filter((issue) => issue.path === componentPath);
return headingIssues.length > 0;
}
/**
* NEW: Check if component has accessibility issues
*/
hasAccessibilityIssues(component, results) {
if (!results.seoAnalysis)
return false;
const componentPath = component.fullPath;
// Check for images without alt text
const imageIssues = results.seoAnalysis.imageOptimization.images.filter((img) => img.usedInPages.includes(componentPath));
const hasImageAltIssues = imageIssues.some((img) => !img.attributes.alt ||
img.issues.some((issue) => issue.type === "missing-alt"));
// Check for ARIA issues
const ariaIssues = results.seoAnalysis.semanticStructure.accessibility.aria.potentialMisuse
.length > 0;
// Check for form label issues
const formIssues = results.seoAnalysis.semanticStructure.accessibility.forms.inputs;
const hasFormIssues = formIssues.total > 0 &&
(formIssues.missingLabels > 0 || formIssues.missingAriaLabels > 0);
return hasImageAltIssues || ariaIssues || hasFormIssues;
}
/**
* NEW: Check if component has performance issues
*/
hasPerformanceIssues(component, results) {
if (!results.seoAnalysis?.performance)
return false;
const componentPath = component.fullPath;
// Check for missing lazy loading
const lazyLoadingAnalysis = results.seoAnalysis.performance.lazyLoading;
if (lazyLoadingAnalysis) {
const shouldBeLazyLoaded = lazyLoadingAnalysis.components.some((comp) => comp.path === componentPath &&
comp.shouldBeLazyLoaded &&
!comp.isLazyLoaded);
if (shouldBeLazyLoaded)
return true;
}
// Check for heavy imports
const bundleAnalysis = results.seoAnalysis.performance.bundleOptimization;
if (bundleAnalysis) {
const hasHeavyImports = bundleAnalysis.heavyImports.some((imp) => imp.importedBy.includes(componentPath) &&
imp.potentialImpact === "high" &&
!imp.isDynamic);
if (hasHeavyImports)
return true;
}
// Check for Core Web Vitals issues
const coreWebVitals = results.seoAnalysis.performance.coreWebVitals;
if (coreWebVitals) {
const hasVitalsIssues = coreWebVitals.potentialIssues.some((issue) => issue.location === componentPath && issue.severity === "high");
if (hasVitalsIssues)
return true;
}
return false;
}
/**
* Get components by file type
*/
getComponentsByFileType(scoredComponents, fileType) {
return scoredComponents.filter((component) => (0, ScoringCriteria_1.getFileType)(component.name, component.fullPath) === fileType);
}
}
exports.ComponentScoringAnalyzer = ComponentScoringAnalyzer;