Compare commits
16 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
02bf0c89c7 | 1 month ago |
|
|
b0565cd5b6 | 1 month ago |
|
|
7e0d804ccc | 1 month ago |
|
|
f6f0f71305 | 1 month ago |
|
|
f8807a0353 | 1 month ago |
|
|
0faf294fff | 1 month ago |
|
|
ea0852b7b8 | 1 month ago |
|
|
ff0e88ba39 | 1 month ago |
|
|
4cee1f479b | 1 month ago |
|
|
135d0f4569 | 1 month ago |
|
|
e3144b6e27 | 1 month ago |
|
|
e9acf78a4a | 1 month ago |
|
|
9841112827 | 2 months ago |
|
|
65aee1e35d | 2 months ago |
|
|
1d884069e3 | 2 months ago |
|
|
abd5d7ff2e | 2 months ago |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,248 +0,0 @@
|
||||
import { promises as fs } from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { spawn } from 'node:child_process';
|
||||
|
||||
export type SupportedLanguage = 'python' | 'c' | 'cpp';
|
||||
|
||||
export interface CodeQualityIssue {
|
||||
line: number;
|
||||
message: string;
|
||||
type: 'error' | 'warning' | 'info';
|
||||
rule?: string;
|
||||
}
|
||||
|
||||
export interface CodeQualityResult {
|
||||
language: SupportedLanguage;
|
||||
tool: string;
|
||||
score: number;
|
||||
quality: 'excellent' | 'good' | 'average' | 'poor';
|
||||
issues: CodeQualityIssue[];
|
||||
totals: {
|
||||
errors: number;
|
||||
warnings: number;
|
||||
infos: number;
|
||||
};
|
||||
runtimeMs: number;
|
||||
meta: {
|
||||
command: string;
|
||||
exitCode: number;
|
||||
};
|
||||
}
|
||||
|
||||
export class CodeQualityError extends Error {
|
||||
public readonly code: string;
|
||||
public readonly status: number;
|
||||
public readonly details?: Record<string, unknown>;
|
||||
|
||||
constructor(message: string, code: string, status = 500, details?: Record<string, unknown>) {
|
||||
super(message);
|
||||
this.name = 'CodeQualityError';
|
||||
this.code = code;
|
||||
this.status = status;
|
||||
this.details = details;
|
||||
}
|
||||
}
|
||||
|
||||
interface CommandResult {
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
exitCode: number;
|
||||
}
|
||||
|
||||
function runCommand(command: string, args: string[]): Promise<CommandResult> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(command, args, { shell: false });
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
child.stdout.on('data', (chunk) => {
|
||||
stdout += chunk.toString();
|
||||
});
|
||||
|
||||
child.stderr.on('data', (chunk) => {
|
||||
stderr += chunk.toString();
|
||||
});
|
||||
|
||||
child.on('error', (err) => {
|
||||
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||
reject(new CodeQualityError(`未找到命令 ${command},请确认已安装并添加到 PATH。`, 'tool_not_found', 400, { command }));
|
||||
return;
|
||||
}
|
||||
reject(new CodeQualityError(`执行命令失败: ${err.message}`, 'command_failed', 500, { command }));
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
resolve({ stdout, stderr, exitCode: typeof code === 'number' ? code : 0 });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function computeScore(totals: { errors: number; warnings: number; infos: number }): number {
|
||||
let score = 10;
|
||||
score -= totals.errors * 3;
|
||||
score -= totals.warnings * 1;
|
||||
score -= totals.infos * 0.5;
|
||||
score = Math.max(0, Math.min(10, score));
|
||||
return Math.round(score * 10) / 10;
|
||||
}
|
||||
|
||||
function mapScoreToQuality(score: number): 'excellent' | 'good' | 'average' | 'poor' {
|
||||
if (score >= 9) return 'excellent';
|
||||
if (score >= 7) return 'good';
|
||||
if (score >= 5) return 'average';
|
||||
return 'poor';
|
||||
}
|
||||
|
||||
function normalizeLanguage(language: string): SupportedLanguage {
|
||||
const lower = language.toLowerCase();
|
||||
if (lower === 'python' || lower === 'py') return 'python';
|
||||
if (lower === 'c' || lower === 'clang') return 'c';
|
||||
if (lower === 'c++' || lower === 'cpp' || lower === 'cxx') return 'cpp';
|
||||
throw new CodeQualityError(`暂不支持的语言: ${language}`, 'unsupported_language', 400);
|
||||
}
|
||||
|
||||
async function evaluatePython(filePath: string): Promise<{ tool: string; issues: CodeQualityIssue[]; totals: CodeQualityResult['totals']; meta: { exitCode: number; command: string } }> {
|
||||
const args = ['--output-format=json', '--score=yes', '--exit-zero', filePath];
|
||||
const result = await runCommand('pylint', args);
|
||||
|
||||
let parsed: Array<{ type: string; message: string; line: number; symbol?: string; messageId?: string }> = [];
|
||||
try {
|
||||
parsed = JSON.parse(result.stdout || '[]');
|
||||
} catch (err) {
|
||||
const error = err as Error;
|
||||
throw new CodeQualityError('无法解析 pylint 输出,请确认代码格式正确。', 'parse_failed', 500, {
|
||||
stdout: result.stdout,
|
||||
stderr: result.stderr,
|
||||
cause: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
const issues: CodeQualityIssue[] = parsed.map((item) => {
|
||||
const severity = (() => {
|
||||
switch (item.type) {
|
||||
case 'fatal':
|
||||
case 'error':
|
||||
return 'error';
|
||||
case 'warning':
|
||||
case 'convention':
|
||||
return 'warning';
|
||||
default:
|
||||
return 'info';
|
||||
}
|
||||
})();
|
||||
|
||||
return {
|
||||
line: item.line || 0,
|
||||
message: item.message,
|
||||
type: severity,
|
||||
rule: item.symbol || item.messageId,
|
||||
};
|
||||
});
|
||||
|
||||
const totals = issues.reduce((acc, issue) => {
|
||||
if (issue.type === 'error') acc.errors += 1;
|
||||
else if (issue.type === 'warning') acc.warnings += 1;
|
||||
else acc.infos += 1;
|
||||
return acc;
|
||||
}, { errors: 0, warnings: 0, infos: 0 });
|
||||
|
||||
return {
|
||||
tool: 'pylint',
|
||||
issues,
|
||||
totals,
|
||||
meta: { exitCode: result.exitCode, command: `pylint ${args.join(' ')}` },
|
||||
};
|
||||
}
|
||||
|
||||
async function evaluateCpp(filePath: string, language: SupportedLanguage): Promise<{ tool: string; issues: CodeQualityIssue[]; totals: CodeQualityResult['totals']; meta: { exitCode: number; command: string } }> {
|
||||
const template = '{file}|{line}|{severity}|{id}|{message}';
|
||||
const args = ['--enable=style,warning,performance,portability', `--template=${template}`, '--inline-suppr'];
|
||||
if (language === 'c') {
|
||||
args.push('--language=c');
|
||||
} else {
|
||||
args.push('--language=c++');
|
||||
}
|
||||
args.push(filePath);
|
||||
|
||||
const result = await runCommand('cppcheck', args);
|
||||
|
||||
const combinedOutput = `${result.stdout}\n${result.stderr}`;
|
||||
const lines = combinedOutput.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.includes('|'));
|
||||
|
||||
const issues: CodeQualityIssue[] = lines.map((line) => {
|
||||
const [ , lineNumber, severity, rule, message ] = line.split('|');
|
||||
let mappedSeverity: CodeQualityIssue['type'];
|
||||
if (severity === 'error') mappedSeverity = 'error';
|
||||
else if (severity === 'warning' || severity === 'style') mappedSeverity = 'warning';
|
||||
else mappedSeverity = 'info';
|
||||
return {
|
||||
line: Number(lineNumber) || 0,
|
||||
message: message?.trim() || '检测到潜在问题',
|
||||
type: mappedSeverity,
|
||||
rule: rule || undefined,
|
||||
};
|
||||
});
|
||||
|
||||
const totals = issues.reduce((acc, issue) => {
|
||||
if (issue.type === 'error') acc.errors += 1;
|
||||
else if (issue.type === 'warning') acc.warnings += 1;
|
||||
else acc.infos += 1;
|
||||
return acc;
|
||||
}, { errors: 0, warnings: 0, infos: 0 });
|
||||
|
||||
return {
|
||||
tool: 'cppcheck',
|
||||
issues,
|
||||
totals,
|
||||
meta: { exitCode: result.exitCode, command: `cppcheck ${args.join(' ')}` },
|
||||
};
|
||||
}
|
||||
|
||||
export async function evaluateCodeQuality(params: { language: string; code: string }): Promise<CodeQualityResult> {
|
||||
const language = normalizeLanguage(params.language);
|
||||
const code = (params.code || '').trim();
|
||||
if (!code) {
|
||||
throw new CodeQualityError('代码内容不能为空。', 'empty_code', 400);
|
||||
}
|
||||
|
||||
const tmpRoot = path.join(os.tmpdir(), 'openrank-code-quality');
|
||||
await fs.mkdir(tmpRoot, { recursive: true });
|
||||
const workingDir = path.join(tmpRoot, randomUUID());
|
||||
await fs.mkdir(workingDir, { recursive: true });
|
||||
|
||||
let extension: string;
|
||||
if (language === 'python') extension = 'py';
|
||||
else if (language === 'c') extension = 'c';
|
||||
else extension = 'cpp';
|
||||
const filePath = path.join(workingDir, `snippet.${extension}`);
|
||||
|
||||
const headerComment = language === 'python'
|
||||
? '# -*- coding: utf-8 -*-\n'
|
||||
: '/* OpenRank 临时代码质量检测文件 */\n';
|
||||
await fs.writeFile(filePath, `${headerComment}${code}`, 'utf8');
|
||||
|
||||
const start = Date.now();
|
||||
try {
|
||||
const analysis = language === 'python'
|
||||
? await evaluatePython(filePath)
|
||||
: await evaluateCpp(filePath, language);
|
||||
|
||||
const score = computeScore(analysis.totals);
|
||||
const quality = mapScoreToQuality(score);
|
||||
|
||||
return {
|
||||
language,
|
||||
tool: analysis.tool,
|
||||
score,
|
||||
quality,
|
||||
issues: analysis.issues,
|
||||
totals: analysis.totals,
|
||||
runtimeMs: Date.now() - start,
|
||||
meta: analysis.meta,
|
||||
};
|
||||
} finally {
|
||||
await fs.rm(workingDir, { recursive: true, force: true }).catch(() => undefined);
|
||||
}
|
||||
}
|
||||
@ -1,31 +0,0 @@
|
||||
import express, { Request, Response } from 'express';
|
||||
import app from './server';
|
||||
import { evaluateCodeQuality, CodeQualityError } from './codeQuality';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/evaluate', async (req: Request, res: Response) => {
|
||||
const { language, code } = req.body || {};
|
||||
|
||||
if (typeof language !== 'string' || !language.trim()) {
|
||||
return res.status(400).json({ error: 'missing_language', message: '请提供需要检测的语言类型。' });
|
||||
}
|
||||
|
||||
if (typeof code !== 'string' || !code.trim()) {
|
||||
return res.status(400).json({ error: 'missing_code', message: '请提供需要检测的代码内容。' });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await evaluateCodeQuality({ language, code });
|
||||
return res.json(result);
|
||||
} catch (err) {
|
||||
if (err instanceof CodeQualityError) {
|
||||
return res.status(err.status).json({ error: err.code, message: err.message, details: err.details });
|
||||
}
|
||||
console.error('[code-evaluate] unexpected error', err);
|
||||
const message = err instanceof Error ? err.message : '代码检测失败';
|
||||
return res.status(500).json({ error: 'code_analysis_failed', message });
|
||||
}
|
||||
});
|
||||
|
||||
app.use('/api/code', router);
|
||||
Loading…
Reference in new issue