feat: implement core code review extension
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import type { CustomRule, Severity } from '../types';
|
||||
|
||||
interface RuleYamlItem {
|
||||
id: string;
|
||||
severity: string;
|
||||
description: string;
|
||||
message: string;
|
||||
languages?: string[];
|
||||
}
|
||||
|
||||
interface RuleConfig {
|
||||
enabled?: string[];
|
||||
rules?: Record<string, { enabled: boolean }>;
|
||||
}
|
||||
|
||||
function parseYamlSimple(content: string): object[] {
|
||||
const items: Array<Record<string, unknown>> = [];
|
||||
let current: Record<string, unknown> | null = null;
|
||||
|
||||
for (const line of content.split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) { continue; }
|
||||
|
||||
if (trimmed.startsWith('- ')) {
|
||||
if (current) { items.push(current); }
|
||||
current = {};
|
||||
const indentMatch = trimmed.match(/^- (\w[\w-]*)\s*:\s*(.*)$/);
|
||||
if (indentMatch) {
|
||||
const key = indentMatch[1];
|
||||
const raw = indentMatch[2].trim();
|
||||
if (raw.startsWith('[') && raw.endsWith(']')) {
|
||||
current[key] = raw.slice(1, -1).split(',').map(s =>
|
||||
s.trim().replace(/^['"]|['"]$/g, '')
|
||||
);
|
||||
} else {
|
||||
current[key] = raw;
|
||||
}
|
||||
}
|
||||
} else if (current) {
|
||||
const propMatch = trimmed.match(/^(\w[\w-]*)\s*:\s*(.*)$/);
|
||||
if (propMatch) {
|
||||
const key = propMatch[1];
|
||||
const raw = propMatch[2].trim();
|
||||
if (!raw || raw === '[]') {
|
||||
current[key] = [];
|
||||
} else if (raw.startsWith('[') && raw.endsWith(']')) {
|
||||
current[key] = raw.slice(1, -1).split(',').map(s =>
|
||||
s.trim().replace(/^['"]|['"]$/g, '')
|
||||
);
|
||||
} else {
|
||||
current[key] = raw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (current) { items.push(current); }
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
function parseConfigYaml(content: string): RuleConfig {
|
||||
const config: RuleConfig = { enabled: [], rules: {} };
|
||||
let section: string | null = null;
|
||||
let currentKey = '';
|
||||
|
||||
for (const line of content.split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) { continue; }
|
||||
|
||||
if (trimmed === 'enabled:') {
|
||||
section = 'enabled';
|
||||
continue;
|
||||
}
|
||||
if (trimmed === 'rules:') {
|
||||
section = 'rules';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (section === 'enabled' && trimmed.startsWith('- ')) {
|
||||
const name = trimmed.substring(2).trim();
|
||||
if (!config.enabled) { config.enabled = []; }
|
||||
config.enabled!.push(name);
|
||||
}
|
||||
|
||||
if (section === 'rules') {
|
||||
const ruleMatch = trimmed.match(/^(\w[\w-]*)\s*:\s*$/);
|
||||
if (ruleMatch) {
|
||||
currentKey = ruleMatch[1];
|
||||
if (!config.rules) { config.rules = {}; }
|
||||
config.rules[currentKey] = { enabled: true };
|
||||
} else if (currentKey) {
|
||||
const propMatch = trimmed.match(/^(\w+)\s*:\s*(.*)$/);
|
||||
if (propMatch) {
|
||||
const key = propMatch[1];
|
||||
const value = propMatch[2].trim();
|
||||
if (!config.rules) { config.rules = {}; }
|
||||
if (!config.rules[currentKey]) { config.rules[currentKey] = { enabled: true }; }
|
||||
(config.rules[currentKey] as Record<string, unknown>)[key] =
|
||||
value === 'false' ? false : value === 'true' ? true : value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
export function loadActiveRules(workspaceRoot: string): CustomRule[] {
|
||||
const rulesDir = path.join(workspaceRoot, '.code-review', 'rules');
|
||||
const configPath = path.join(workspaceRoot, '.code-review', 'config.yaml');
|
||||
|
||||
if (!fs.existsSync(rulesDir)) { return []; }
|
||||
|
||||
let ruleConfig: RuleConfig = {};
|
||||
if (fs.existsSync(configPath)) {
|
||||
const configContent = fs.readFileSync(configPath, 'utf-8');
|
||||
ruleConfig = parseConfigYaml(configContent);
|
||||
}
|
||||
|
||||
const enabledFiles = new Set(ruleConfig.enabled ?? []);
|
||||
const disabledRules = new Set(
|
||||
Object.entries(ruleConfig.rules ?? {})
|
||||
.filter(([, v]) => v.enabled === false)
|
||||
.map(([k]) => k)
|
||||
);
|
||||
|
||||
const allRules: CustomRule[] = [];
|
||||
|
||||
const files = fs.readdirSync(rulesDir).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'));
|
||||
for (const file of files) {
|
||||
if (enabledFiles.size > 0 && !enabledFiles.has(file)) { continue; }
|
||||
|
||||
const content = fs.readFileSync(path.join(rulesDir, file), 'utf-8');
|
||||
const items = parseYamlSimple(content) as RuleYamlItem[];
|
||||
|
||||
for (const item of items) {
|
||||
if (disabledRules.has(item.id)) { continue; }
|
||||
if (!item.id || !item.severity || !item.description || !item.message) { continue; }
|
||||
|
||||
const severity = (['error', 'warning', 'info'].includes(item.severity)
|
||||
? (item.severity as Severity)
|
||||
: 'warning');
|
||||
|
||||
allRules.push({
|
||||
id: item.id,
|
||||
severity,
|
||||
description: item.description,
|
||||
message: item.message,
|
||||
languages: item.languages,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return allRules;
|
||||
}
|
||||
Reference in New Issue
Block a user