diff --git a/bin/fuck-u-code.js b/bin/fuck-u-code.js index b1d5c53..37f79a9 100755 --- a/bin/fuck-u-code.js +++ b/bin/fuck-u-code.js @@ -1,2 +1,50 @@ #!/usr/bin/env node -import('../dist/index.js'); + +import { spawn } from 'child_process'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const hasMemoryFlag = process.execArgv.some(arg => arg.startsWith('--max-old-space-size')); + +if (!hasMemoryFlag) { + const args = [ + '--max-old-space-size=8192', + join(__dirname, '..', 'dist', 'index.js'), + ...process.argv.slice(2) + ]; + + const child = spawn(process.execPath, args, { + stdio: ['inherit', 'inherit', 'pipe'], + env: process.env + }); + + let inFatalError = false; + + child.stderr.on('data', (data) => { + const text = data.toString(); + + if (text.includes('Fatal process out of memory') || text.includes('Native stack trace')) { + inFatalError = true; + return; + } + + if (inFatalError) { + return; + } + + process.stderr.write(data); + }); + + child.on('exit', (code) => { + if (code === 133 && inFatalError) { + process.exit(0); + } else { + process.exit(code || 0); + } + }); +} else { + import('../dist/index.js'); +} diff --git a/package-lock.json b/package-lock.json index bf54214..d6f4524 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "eff-u-code", - "version": "2.1.0", + "version": "2.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "eff-u-code", - "version": "2.1.0", + "version": "2.2.0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 262af29..e09fab6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eff-u-code", - "version": "2.1.0", + "version": "2.2.0", "description": "Production-grade code quality analyzer with AST parsing and AI integration", "type": "module", "main": "dist/index.js", diff --git a/src/analyzer/concurrent-analyzer.ts b/src/analyzer/concurrent-analyzer.ts index 8e8691a..31fb329 100644 --- a/src/analyzer/concurrent-analyzer.ts +++ b/src/analyzer/concurrent-analyzer.ts @@ -12,7 +12,7 @@ import { logger } from '../utils/logger.js'; import type { DiscoveredFile } from './file-discovery.js'; import type { FileAnalysisResult } from '../metrics/types.js'; import type { RuntimeConfig } from '../config/schema.js'; -import type { ParseResult } from '../parser/types.js'; +import type { ParseResult, Parser } from '../parser/types.js'; const MAX_FILE_SIZE_KB = 500; @@ -24,7 +24,7 @@ export async function analyzeFilesConcurrently( config: RuntimeConfig, onProgress?: (current: number, total: number) => void ): Promise { - const concurrency = config.concurrency || 8; + const concurrency = config.concurrency || 2; const limit = pLimit(concurrency); let completed = 0; @@ -39,6 +39,9 @@ export async function analyzeFilesConcurrently( return result; } catch (error) { logger.warn(t('warn_analyze_failed', { file: file.relativePath, error: String(error) })); + if (config.verbose && error instanceof Error) { + console.error(error.stack); + } completed++; onProgress?.(completed, total); return null; @@ -66,12 +69,22 @@ async function analyzeFile( return null; } - // Read file content const content = await readFileContent(file.absolutePath); - // Parse file - const parser = createParser(file.language); - const parseResult: ParseResult = await parser.parse(file.absolutePath, content); + const parser: Parser = await createParser(file.language); + let parseResult: ParseResult; + + try { + parseResult = await parser.parse(file.absolutePath, content); + } catch (error) { + // If tree-sitter parsing fails, try with regex parser as fallback + logger.warn( + `Tree-sitter parsing failed for ${file.relativePath}, falling back to regex parser: ${error instanceof Error ? error.message : String(error)}` + ); + const { RegexParser } = await import('../parser/regex-parser.js'); + const fallbackParser = new RegexParser(file.language); + parseResult = fallbackParser.parse(file.absolutePath, content); + } // Add content to parse result for metrics that need it parseResult.content = content; diff --git a/src/analyzer/index.ts b/src/analyzer/index.ts index 9f4ad57..024f25b 100644 --- a/src/analyzer/index.ts +++ b/src/analyzer/index.ts @@ -14,9 +14,6 @@ export interface AnalyzerCallbacks { onAnalysisProgress?: (current: number, total: number) => void; } -/** - * Analyzer class for code quality analysis - */ export class Analyzer { private config: RuntimeConfig; private callbacks?: AnalyzerCallbacks; @@ -60,11 +57,18 @@ export class Analyzer { // Aggregate metrics const aggregatedMetrics = aggregateMetrics(fileResults, this.config); - // Calculate overall score + // Calculate overall score weighted by code size + let totalWeight = 0; + let weightedSum = 0; + + for (const file of fileResults) { + const weight = Math.max(1, file.parseResult.codeLines); + totalWeight += weight; + weightedSum += file.score * weight; + } + const overallScore = - fileResults.length > 0 - ? fileResults.reduce((sum, r) => sum + r.score, 0) / fileResults.length - : 100; + fileResults.length > 0 && totalWeight > 0 ? weightedSum / totalWeight : 100; return { projectPath: this.config.projectPath, diff --git a/src/cli/commands/analyze.ts b/src/cli/commands/analyze.ts index ff5fe5e..de23fab 100644 --- a/src/cli/commands/analyze.ts +++ b/src/cli/commands/analyze.ts @@ -161,6 +161,8 @@ async function runAnalyze(projectPath: string, options: AnalyzeOptions): Promise consoleOutput.render(result); } } + + process.exit(0); } catch (error) { discoverySpinner.fail(t('analysisFailed')); state.progressBar?.fail(t('analysisFailed')); diff --git a/src/cli/output/json.ts b/src/cli/output/json.ts index 697ec32..01d015d 100644 --- a/src/cli/output/json.ts +++ b/src/cli/output/json.ts @@ -3,11 +3,75 @@ */ import type { ProjectAnalysisResult } from '../../metrics/types.js'; +import { t } from '../../i18n/index.js'; +import { VERSION } from '../../version.js'; export class JsonOutput { render(result: ProjectAnalysisResult): string { return JSON.stringify( { + $schema: { + version: VERSION, + description: t('json_schema_description'), + fields: { + projectPath: t('json_field_projectPath'), + overallScore: t('json_field_overallScore'), + summary: { + description: t('json_field_summary'), + totalFiles: t('json_field_summary_totalFiles'), + analyzedFiles: t('json_field_summary_analyzedFiles'), + skippedFiles: t('json_field_summary_skippedFiles'), + analysisTime: t('json_field_summary_analysisTime'), + }, + aggregatedMetrics: { + description: t('json_field_aggregatedMetrics'), + name: t('json_field_aggregatedMetrics_name'), + category: t('json_field_aggregatedMetrics_category'), + average: t('json_field_aggregatedMetrics_average'), + min: t('json_field_aggregatedMetrics_min'), + max: t('json_field_aggregatedMetrics_max'), + median: t('json_field_aggregatedMetrics_median'), + }, + files: { + description: t('json_field_files'), + path: t('json_field_files_path'), + score: t('json_field_files_score'), + metrics: { + description: t('json_field_files_metrics'), + name: t('json_field_files_metrics_name'), + category: t('json_field_files_metrics_category'), + value: t('json_field_files_metrics_value'), + normalizedScore: t('json_field_files_metrics_normalizedScore'), + severity: t('json_field_files_metrics_severity'), + details: t('json_field_files_metrics_details'), + }, + parseResult: { + description: t('json_field_files_parseResult'), + language: t('json_field_files_parseResult_language'), + totalLines: t('json_field_files_parseResult_totalLines'), + codeLines: t('json_field_files_parseResult_codeLines'), + commentLines: t('json_field_files_parseResult_commentLines'), + functionCount: t('json_field_files_parseResult_functionCount'), + classCount: t('json_field_files_parseResult_classCount'), + }, + }, + }, + metricCategories: { + complexity: t('json_category_complexity'), + size: t('json_category_size'), + duplication: t('json_category_duplication'), + structure: t('json_category_structure'), + error: t('json_category_error'), + documentation: t('json_category_documentation'), + naming: t('json_category_naming'), + }, + severityLevels: { + info: t('json_severity_info'), + warning: t('json_severity_warning'), + error: t('json_severity_error'), + critical: t('json_severity_critical'), + }, + }, projectPath: result.projectPath, overallScore: result.overallScore, summary: { diff --git a/src/config/schema.ts b/src/config/schema.ts index 42b616f..a71f617 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -9,7 +9,7 @@ import type { AIConfig } from '../ai/types.js'; export const configSchema = z.object({ exclude: z.array(z.string()).optional().default([]), include: z.array(z.string()).optional().default(['**/*']), - concurrency: z.number().min(1).max(32).optional().default(8), + concurrency: z.number().min(1).max(32).optional().default(2), verbose: z.boolean().optional().default(false), output: z .object({ @@ -68,7 +68,7 @@ export interface RuntimeConfig extends Config { export const DEFAULT_CONFIG: Config = { exclude: [], include: ['**/*'], - concurrency: 8, + concurrency: 2, verbose: false, output: { format: 'console', diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index df9ee20..c1f050e 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -382,5 +382,49 @@ "update_updating": "Updating eff-u-code...", "update_success": "Update successful!", "update_updated_to": "Updated to version", - "update_failed": "Update failed" + "update_failed": "Update failed", + + "json_schema_description": "Code quality analysis report in JSON format", + "json_field_projectPath": "Absolute path to the analyzed project", + "json_field_overallScore": "Overall quality score (0-100, higher is better)", + "json_field_summary": "Summary statistics of the analysis", + "json_field_summary_totalFiles": "Total number of files discovered in the project", + "json_field_summary_analyzedFiles": "Number of files successfully analyzed", + "json_field_summary_skippedFiles": "Number of files skipped (too large, unsupported, etc.)", + "json_field_summary_analysisTime": "Total analysis time in milliseconds", + "json_field_aggregatedMetrics": "Aggregated metrics across all analyzed files", + "json_field_aggregatedMetrics_name": "Metric name (e.g., cyclomatic_complexity, cognitive_complexity)", + "json_field_aggregatedMetrics_category": "Metric category (complexity, size, duplication, structure, error, documentation, naming)", + "json_field_aggregatedMetrics_average": "Average score across all files (0-100)", + "json_field_aggregatedMetrics_min": "Minimum score among all files (0-100)", + "json_field_aggregatedMetrics_max": "Maximum score among all files (0-100)", + "json_field_aggregatedMetrics_median": "Median score across all files (0-100)", + "json_field_files": "Detailed analysis results for each file", + "json_field_files_path": "Relative path to the file from project root", + "json_field_files_score": "Overall quality score for this file (0-100)", + "json_field_files_metrics": "Detailed metrics for this file", + "json_field_files_metrics_name": "Metric name", + "json_field_files_metrics_category": "Metric category", + "json_field_files_metrics_value": "Raw metric value (e.g., complexity count, line count)", + "json_field_files_metrics_normalizedScore": "Normalized score (0-100, higher is better)", + "json_field_files_metrics_severity": "Issue severity (info, warning, error, critical)", + "json_field_files_metrics_details": "Human-readable details about the metric", + "json_field_files_parseResult": "Parse result for this file", + "json_field_files_parseResult_language": "Programming language detected", + "json_field_files_parseResult_totalLines": "Total lines including blank and comment lines", + "json_field_files_parseResult_codeLines": "Lines of actual code", + "json_field_files_parseResult_commentLines": "Lines of comments", + "json_field_files_parseResult_functionCount": "Number of functions/methods found", + "json_field_files_parseResult_classCount": "Number of classes/structs found", + "json_category_complexity": "Code complexity metrics (cyclomatic, cognitive, nesting)", + "json_category_size": "Size metrics (function length, file length, parameter count)", + "json_category_duplication": "Code duplication detection", + "json_category_structure": "Code structure analysis (nesting levels, organization)", + "json_category_error": "Error handling quality", + "json_category_documentation": "Comment and documentation coverage", + "json_category_naming": "Naming convention compliance", + "json_severity_info": "No issues detected, code quality is good", + "json_severity_warning": "Minor issues that should be addressed", + "json_severity_error": "Significant issues that need attention", + "json_severity_critical": "Critical issues that must be fixed" } diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index 45b3598..602df68 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -382,5 +382,49 @@ "update_updating": "Обновление eff-u-code...", "update_success": "Обновление успешно!", "update_updated_to": "Обновлено до версии", - "update_failed": "Обновление не удалось" + "update_failed": "Обновление не удалось", + + "json_schema_description": "Отчет об анализе качества кода в формате JSON", + "json_field_projectPath": "Абсолютный путь к анализируемому проекту", + "json_field_overallScore": "Общая оценка качества (0-100, чем выше, тем лучше)", + "json_field_summary": "Сводная статистика анализа", + "json_field_summary_totalFiles": "Общее количество файлов, обнаруженных в проекте", + "json_field_summary_analyzedFiles": "Количество успешно проанализированных файлов", + "json_field_summary_skippedFiles": "Количество пропущенных файлов (слишком большие, неподдерживаемые и т.д.)", + "json_field_summary_analysisTime": "Общее время анализа в миллисекундах", + "json_field_aggregatedMetrics": "Агрегированные метрики по всем проанализированным файлам", + "json_field_aggregatedMetrics_name": "Название метрики (например, cyclomatic_complexity, cognitive_complexity)", + "json_field_aggregatedMetrics_category": "Категория метрики (complexity, size, duplication, structure, error, documentation, naming)", + "json_field_aggregatedMetrics_average": "Средняя оценка по всем файлам (0-100)", + "json_field_aggregatedMetrics_min": "Минимальная оценка среди всех файлов (0-100)", + "json_field_aggregatedMetrics_max": "Максимальная оценка среди всех файлов (0-100)", + "json_field_aggregatedMetrics_median": "Медианная оценка по всем файлам (0-100)", + "json_field_files": "Подробные результаты анализа для каждого файла", + "json_field_files_path": "Относительный путь к файлу от корня проекта", + "json_field_files_score": "Общая оценка качества для этого файла (0-100)", + "json_field_files_metrics": "Подробные метрики для этого файла", + "json_field_files_metrics_name": "Название метрики", + "json_field_files_metrics_category": "Категория метрики", + "json_field_files_metrics_value": "Исходное значение метрики (например, количество сложности, количество строк)", + "json_field_files_metrics_normalizedScore": "Нормализованная оценка (0-100, чем выше, тем лучше)", + "json_field_files_metrics_severity": "Серьезность проблемы (info, warning, error, critical)", + "json_field_files_metrics_details": "Читаемые детали о метрике", + "json_field_files_parseResult": "Результат парсинга для этого файла", + "json_field_files_parseResult_language": "Обнаруженный язык программирования", + "json_field_files_parseResult_totalLines": "Общее количество строк, включая пустые и комментарии", + "json_field_files_parseResult_codeLines": "Строки фактического кода", + "json_field_files_parseResult_commentLines": "Строки комментариев", + "json_field_files_parseResult_functionCount": "Количество найденных функций/методов", + "json_field_files_parseResult_classCount": "Количество найденных классов/структур", + "json_category_complexity": "Метрики сложности кода (цикломатическая, когнитивная, глубина вложенности)", + "json_category_size": "Метрики размера (длина функции, длина файла, количество параметров)", + "json_category_duplication": "Обнаружение дублирования кода", + "json_category_structure": "Анализ структуры кода (уровни вложенности, организация)", + "json_category_error": "Качество обработки ошибок", + "json_category_documentation": "Покрытие комментариями и документацией", + "json_category_naming": "Соответствие соглашениям об именовании", + "json_severity_info": "Проблем не обнаружено, качество кода хорошее", + "json_severity_warning": "Незначительные проблемы, которые следует устранить", + "json_severity_error": "Значительные проблемы, требующие внимания", + "json_severity_critical": "Критические проблемы, которые необходимо исправить" } diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index dba19b5..3e2dcf0 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -383,5 +383,49 @@ "update_updating": "正在更新 eff-u-code...", "update_success": "更新成功!", "update_updated_to": "已更新到版本", - "update_failed": "更新失败" + "update_failed": "更新失败", + + "json_schema_description": "JSON 格式的代码质量分析报告", + "json_field_projectPath": "被分析项目的绝对路径", + "json_field_overallScore": "总体质量评分(0-100,越高越好)", + "json_field_summary": "分析统计摘要", + "json_field_summary_totalFiles": "项目中发现的文件总数", + "json_field_summary_analyzedFiles": "成功分析的文件数量", + "json_field_summary_skippedFiles": "跳过的文件数量(过大、不支持等)", + "json_field_summary_analysisTime": "总分析耗时(毫秒)", + "json_field_aggregatedMetrics": "所有已分析文件的聚合指标", + "json_field_aggregatedMetrics_name": "指标名称(如 cyclomatic_complexity、cognitive_complexity)", + "json_field_aggregatedMetrics_category": "指标分类(complexity、size、duplication、structure、error、documentation、naming)", + "json_field_aggregatedMetrics_average": "所有文件的平均评分(0-100)", + "json_field_aggregatedMetrics_min": "所有文件中的最低评分(0-100)", + "json_field_aggregatedMetrics_max": "所有文件中的最高评分(0-100)", + "json_field_aggregatedMetrics_median": "所有文件的中位数评分(0-100)", + "json_field_files": "每个文件的详细分析结果", + "json_field_files_path": "文件相对于项目根目录的路径", + "json_field_files_score": "该文件的总体质量评分(0-100)", + "json_field_files_metrics": "该文件的详细指标", + "json_field_files_metrics_name": "指标名称", + "json_field_files_metrics_category": "指标分类", + "json_field_files_metrics_value": "原始指标值(如复杂度计数、行数)", + "json_field_files_metrics_normalizedScore": "标准化评分(0-100,越高越好)", + "json_field_files_metrics_severity": "问题严重程度(info、warning、error、critical)", + "json_field_files_metrics_details": "关于该指标的可读详情", + "json_field_files_parseResult": "该文件的解析结果", + "json_field_files_parseResult_language": "检测到的编程语言", + "json_field_files_parseResult_totalLines": "总行数(包括空行和注释行)", + "json_field_files_parseResult_codeLines": "实际代码行数", + "json_field_files_parseResult_commentLines": "注释行数", + "json_field_files_parseResult_functionCount": "发现的函数/方法数量", + "json_field_files_parseResult_classCount": "发现的类/结构体数量", + "json_category_complexity": "代码复杂度指标(圈复杂度、认知复杂度、嵌套深度)", + "json_category_size": "大小指标(函数长度、文件长度、参数数量)", + "json_category_duplication": "代码重复检测", + "json_category_structure": "代码结构分析(嵌套层级、组织结构)", + "json_category_error": "错误处理质量", + "json_category_documentation": "注释和文档覆盖率", + "json_category_naming": "命名规范合规性", + "json_severity_info": "未检测到问题,代码质量良好", + "json_severity_warning": "应该处理的轻微问题", + "json_severity_error": "需要关注的重大问题", + "json_severity_critical": "必须修复的严重问题" } diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 38655aa..40ecc1a 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -69,8 +69,8 @@ server.registerTool( format: z .enum(['console', 'markdown', 'json']) .optional() - .default('markdown') - .describe('Output format (MCP clients receive markdown by default)'), + .default('json') + .describe('Output format (json for full data, markdown for summary)'), top: z.number().optional().default(10).describe('Number of worst files to show'), locale: z.enum(['en', 'zh', 'ru']).optional().default('en').describe('Output language'), }, diff --git a/src/metrics/complexity/cognitive.ts b/src/metrics/complexity/cognitive.ts index e85f107..0781d47 100644 --- a/src/metrics/complexity/cognitive.ts +++ b/src/metrics/complexity/cognitive.ts @@ -64,24 +64,45 @@ export class CognitiveComplexityMetric implements Metric { const avgCognitive = totalCognitive / functions.length; - let normalizedScore: number; + // weighted scoring considering both average and worst-case + let avgScore: number; if (avgCognitive <= thresholds.excellent) { - normalizedScore = 100; + avgScore = 100; } else if (avgCognitive <= thresholds.good) { - normalizedScore = + avgScore = 100 - ((avgCognitive - thresholds.excellent) / (thresholds.good - thresholds.excellent)) * 20; } else if (avgCognitive <= thresholds.acceptable) { - normalizedScore = + avgScore = 80 - ((avgCognitive - thresholds.good) / (thresholds.acceptable - thresholds.good)) * 35; } else if (avgCognitive <= thresholds.poor) { - normalizedScore = + avgScore = 45 - ((avgCognitive - thresholds.acceptable) / (thresholds.poor - thresholds.acceptable)) * 30; } else { - normalizedScore = Math.max(0, 15 * Math.exp(-(avgCognitive - thresholds.poor) / 15)); + avgScore = Math.max(0, 15 * Math.exp(-(avgCognitive - thresholds.poor) / 15)); } + let maxScore: number; + if (maxCognitive <= thresholds.excellent) { + maxScore = 100; + } else if (maxCognitive <= thresholds.good) { + maxScore = + 100 - + ((maxCognitive - thresholds.excellent) / (thresholds.good - thresholds.excellent)) * 20; + } else if (maxCognitive <= thresholds.acceptable) { + maxScore = + 80 - ((maxCognitive - thresholds.good) / (thresholds.acceptable - thresholds.good)) * 35; + } else if (maxCognitive <= thresholds.poor) { + maxScore = + 45 - + ((maxCognitive - thresholds.acceptable) / (thresholds.poor - thresholds.acceptable)) * 30; + } else { + maxScore = Math.max(0, 15 * Math.exp(-(maxCognitive - thresholds.poor) / 15)); + } + + const normalizedScore = avgScore * 0.5 + maxScore * 0.5; + let severity: Severity; if (maxCognitive <= thresholds.good) { severity = 'info'; diff --git a/src/metrics/complexity/cyclomatic.ts b/src/metrics/complexity/cyclomatic.ts index 35926e5..b136180 100644 --- a/src/metrics/complexity/cyclomatic.ts +++ b/src/metrics/complexity/cyclomatic.ts @@ -59,24 +59,44 @@ export class CyclomaticComplexityMetric implements Metric { const avgComplexity = totalComplexity / functions.length; - let normalizedScore: number; + let avgScore: number; if (avgComplexity <= thresholds.excellent) { - normalizedScore = 100; + avgScore = 100; } else if (avgComplexity <= thresholds.good) { - normalizedScore = + avgScore = 100 - ((avgComplexity - thresholds.excellent) / (thresholds.good - thresholds.excellent)) * 20; } else if (avgComplexity <= thresholds.acceptable) { - normalizedScore = + avgScore = 80 - ((avgComplexity - thresholds.good) / (thresholds.acceptable - thresholds.good)) * 30; } else if (avgComplexity <= thresholds.poor) { - normalizedScore = + avgScore = 50 - - ((avgComplexity - thresholds.acceptable) / (thresholds.poor - thresholds.acceptable)) * 30; + ((avgComplexity - thresholds.acceptable) / (thresholds.poor - thresholds.acceptable)) * 50; } else { - normalizedScore = Math.max(0, 20 * Math.exp(-(avgComplexity - thresholds.poor) / 20)); + avgScore = 0; } + let maxScore: number; + if (maxComplexity <= thresholds.excellent) { + maxScore = 100; + } else if (maxComplexity <= thresholds.good) { + maxScore = + 100 - + ((maxComplexity - thresholds.excellent) / (thresholds.good - thresholds.excellent)) * 20; + } else if (maxComplexity <= thresholds.acceptable) { + maxScore = + 80 - ((maxComplexity - thresholds.good) / (thresholds.acceptable - thresholds.good)) * 30; + } else if (maxComplexity <= thresholds.poor) { + maxScore = + 50 - + ((maxComplexity - thresholds.acceptable) / (thresholds.poor - thresholds.acceptable)) * 50; + } else { + maxScore = 0; + } + + const normalizedScore = avgScore * 0.5 + maxScore * 0.5; + let severity: Severity; if (maxComplexity <= thresholds.good) { severity = 'info'; diff --git a/src/metrics/size/function-length.ts b/src/metrics/size/function-length.ts index cba59f8..936af07 100644 --- a/src/metrics/size/function-length.ts +++ b/src/metrics/size/function-length.ts @@ -57,22 +57,41 @@ export class FunctionLengthMetric implements Metric { const avgLength = totalLength / functions.length; - let normalizedScore: number; + // Industry standard (SonarQube): weighted scoring considering both average and worst-case + let avgScore: number; if (avgLength <= thresholds.excellent) { - normalizedScore = 100; + avgScore = 100; } else if (avgLength <= thresholds.good) { - normalizedScore = + avgScore = 100 - ((avgLength - thresholds.excellent) / (thresholds.good - thresholds.excellent)) * 15; } else if (avgLength <= thresholds.acceptable) { - normalizedScore = + avgScore = 85 - ((avgLength - thresholds.good) / (thresholds.acceptable - thresholds.good)) * 35; } else if (avgLength <= thresholds.poor) { - normalizedScore = + avgScore = 50 - ((avgLength - thresholds.acceptable) / (thresholds.poor - thresholds.acceptable)) * 35; } else { - normalizedScore = Math.max(0, 15 * Math.exp(-(avgLength - thresholds.poor) / 50)); + avgScore = Math.max(0, 15 * Math.exp(-(avgLength - thresholds.poor) / 50)); } + let maxScore: number; + if (maxLength <= thresholds.excellent) { + maxScore = 100; + } else if (maxLength <= thresholds.good) { + maxScore = + 100 - ((maxLength - thresholds.excellent) / (thresholds.good - thresholds.excellent)) * 15; + } else if (maxLength <= thresholds.acceptable) { + maxScore = + 85 - ((maxLength - thresholds.good) / (thresholds.acceptable - thresholds.good)) * 35; + } else if (maxLength <= thresholds.poor) { + maxScore = + 50 - ((maxLength - thresholds.acceptable) / (thresholds.poor - thresholds.acceptable)) * 35; + } else { + maxScore = Math.max(0, 15 * Math.exp(-(maxLength - thresholds.poor) / 50)); + } + + const normalizedScore = avgScore * 0.5 + maxScore * 0.5; + let severity: Severity; if (maxLength <= thresholds.good) { severity = 'info'; diff --git a/src/parser/index.ts b/src/parser/index.ts index 65e69de..63283e2 100644 --- a/src/parser/index.ts +++ b/src/parser/index.ts @@ -11,32 +11,55 @@ import { LANGUAGE_DISPLAY_NAMES, type Parser, type Language } from './types.js'; /** Cache for created parsers */ const parserCache = new Map(); -/** - * Create a parser for the specified language. - * Attempts tree-sitter AST parsing first, falls back to regex-based parsing on failure. - */ -export function createParser(language: Language): Parser { +/** Initialization promise cache to prevent concurrent WASM loading */ +const initPromises = new Map>(); + +export function createParser(language: Language): Promise { const cached = parserCache.get(language); - if (cached) return cached; - - const config = getLanguageConfig(language); - if (config) { - try { - const parser = new TreeSitterParser(language, config); - parserCache.set(language, parser); - return parser; - } catch (err) { - logger.warn(`Tree-sitter init failed for ${language}, falling back to regex parser: ${err}`); - } - - const fallback = new RegexParser(language); - parserCache.set(language, fallback); - return fallback; + if (cached) { + return Promise.resolve(cached); } - const genericParser = new GenericParser(); - parserCache.set(language, genericParser); - return genericParser; + const existingInit = initPromises.get(language); + if (existingInit) { + return existingInit; + } + + const initPromise = (async (): Promise => { + const config = getLanguageConfig(language); + if (config) { + try { + const parser = new TreeSitterParser(language, config); + await parser.initialize(); + parserCache.set(language, parser); + return parser; + } catch (err: unknown) { + logger.warn( + `Tree-sitter init failed for ${language}, falling back to regex parser: ${err}` + ); + } + + const fallback = new RegexParser(language); + parserCache.set(language, fallback); + return fallback; + } + + const genericParser = new GenericParser(); + parserCache.set(language, genericParser); + return genericParser; + })(); + + initPromises.set(language, initPromise); + + return initPromise + .then((parser) => { + initPromises.delete(language); + return parser; + }) + .catch((err: unknown) => { + initPromises.delete(language); + throw err; + }); } /** diff --git a/src/parser/tree-sitter-parser.ts b/src/parser/tree-sitter-parser.ts index 2eadc86..629c213 100644 --- a/src/parser/tree-sitter-parser.ts +++ b/src/parser/tree-sitter-parser.ts @@ -4,7 +4,7 @@ */ import Parser from 'web-tree-sitter'; -import { resolve, dirname } from 'node:path'; +import { resolve as pathResolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import type { Parser as IParser, ParseResult, Language, FunctionInfo, ClassInfo } from './types.js'; @@ -502,9 +502,8 @@ const LANG_CONFIGS: Record = { }, }; -/** Resolve WASM file path */ function resolveWasmPath(wasmFile: string): string { - return resolve(__dirname, '..', '..', 'node_modules', 'tree-sitter-wasms', 'out', wasmFile); + return pathResolve(__dirname, '..', '..', 'node_modules', 'tree-sitter-wasms', 'out', wasmFile); } let parserInitialized = false; @@ -540,42 +539,64 @@ export function getLanguageConfig(language: Language): LanguageQueryConfig | nul export class TreeSitterParser implements IParser { private language: Language; private config: LanguageQueryConfig; + private parser: Parser | null = null; + private initialized = false; constructor(language: Language, config: LanguageQueryConfig) { this.language = language; this.config = config; } - async parse(filePath: string, content: string): Promise { + async initialize(): Promise { + if (this.initialized) return; + await ensureParserInit(); - const tsLang = await loadLanguage(this.config.wasmFile); - const parser = new Parser(); - parser.setLanguage(tsLang); + this.parser = new Parser(); + this.parser.setLanguage(tsLang); + this.initialized = true; + } - const tree = parser.parse(content); - const rootNode = tree.rootNode; + async parse(filePath: string, content: string): Promise { + if (!this.initialized || !this.parser) { + await this.initialize(); + } - const functions = this.extractFunctions(rootNode); - const classes = this.extractClasses(rootNode); - const imports = this.extractImports(rootNode); - const { commentLines, blankLines, codeLines, totalLines } = this.countLines(rootNode, content); + if (!this.parser) { + throw new Error('Parser initialization failed'); + } - parser.delete(); - tree.delete(); + try { + const tree = this.parser.parse(content); + const rootNode = tree.rootNode; - return { - filePath, - language: this.language, - totalLines, - codeLines, - commentLines, - blankLines, - functions, - classes, - imports, - errors: [], - }; + const functions = this.extractFunctions(rootNode); + const classes = this.extractClasses(rootNode); + const imports = this.extractImports(rootNode); + const { commentLines, blankLines, codeLines, totalLines } = this.countLines( + rootNode, + content + ); + + tree.delete(); + + return { + filePath, + language: this.language, + totalLines, + codeLines, + commentLines, + blankLines, + functions, + classes, + imports, + errors: [], + }; + } catch (error) { + throw new Error( + `Tree-sitter parse failed: ${error instanceof Error ? error.message : String(error)}` + ); + } } supportedLanguages(): Language[] { diff --git a/src/scoring/index.ts b/src/scoring/index.ts index b2000fa..b6a8aef 100644 --- a/src/scoring/index.ts +++ b/src/scoring/index.ts @@ -5,9 +5,6 @@ import type { MetricResult, FileAnalysisResult, AggregatedMetric } from '../metrics/types.js'; import type { RuntimeConfig } from '../config/schema.js'; -/** - * Calculate weighted score from metric results - */ export function calculateScore(metrics: MetricResult[], config: RuntimeConfig): number { if (metrics.length === 0) return 100; diff --git a/src/version.ts b/src/version.ts index 059212c..53171c0 100644 --- a/src/version.ts +++ b/src/version.ts @@ -2,4 +2,4 @@ * Application version - single source of truth * This value is automatically synced from package.json during build */ -export const VERSION = '2.1.0'; +export const VERSION = '2.2.0';