feat: add JSON schema documentation and improve parser error handling

- Add comprehensive JSON schema field descriptions with i18n support (en, zh, ru)
- Fix shell script parsing by adding graceful fallback to regex parser when tree-sitter fails
- Change MCP server default output format from markdown to JSON for better AI integration
- Improve parser error handling with automatic fallback mechanism
- Add detailed field explanations in $schema for better AI comprehension
- Bump version to 2.2.0

Breaking changes:
- MCP server now returns JSON format by default instead of markdown
This commit is contained in:
Done-0
2026-02-16 11:05:23 +08:00
parent 2bf5220b7f
commit cd55ca0935
19 changed files with 461 additions and 97 deletions

View File

@@ -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');
}

4
package-lock.json generated
View File

@@ -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": {

View File

@@ -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",

View File

@@ -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<FileAnalysisResult[]> {
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;

View File

@@ -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,

View File

@@ -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'));

View File

@@ -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: {

View File

@@ -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',

View File

@@ -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"
}

View File

@@ -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": "Критические проблемы, которые необходимо исправить"
}

View File

@@ -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": "必须修复的严重问题"
}

View File

@@ -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'),
},

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -11,32 +11,55 @@ import { LANGUAGE_DISPLAY_NAMES, type Parser, type Language } from './types.js';
/** Cache for created parsers */
const parserCache = new Map<Language, Parser>();
/**
* 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<Language, Promise<Parser>>();
export function createParser(language: Language): Promise<Parser> {
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<Parser> => {
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;
});
}
/**

View File

@@ -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<string, LanguageQueryConfig> = {
},
};
/** 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<ParseResult> {
async initialize(): Promise<void> {
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<ParseResult> {
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[] {

View File

@@ -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;

View File

@@ -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';