mirror of
https://github.com/Done-0/fuck-u-code.git
synced 2026-06-07 22:53:34 +08:00
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:
@@ -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
4
package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "Критические проблемы, которые необходимо исправить"
|
||||
}
|
||||
|
||||
@@ -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": "必须修复的严重问题"
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
},
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user