pi-lens
Version:
Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo
92 lines (91 loc) • 3.37 kB
JavaScript
import * as fs from "node:fs";
import * as path from "node:path";
export class AstGrepRuleManager {
ruleDir;
log;
ruleDescriptions = null;
constructor(ruleDir, log) {
this.ruleDir = ruleDir;
this.log = log;
}
loadRuleDescriptions() {
if (this.ruleDescriptions !== null)
return this.ruleDescriptions;
const descriptions = new Map();
const possiblePaths = [
path.join(this.ruleDir, "ast-grep-rules", "rules"),
path.join(this.ruleDir, "rules"),
this.ruleDir,
];
const rulesPath = possiblePaths.find((p) => fs.existsSync(p));
if (!rulesPath) {
this.log(`Rule descriptions: no rules directory found in ${possiblePaths.join(", ")}`);
this.ruleDescriptions = descriptions;
return descriptions;
}
try {
const files = fs.readdirSync(rulesPath).filter((f) => f.endsWith(".yml"));
this.log(`Loaded ${files.length} rule descriptions from ${rulesPath}`);
for (const file of files) {
const filePath = path.join(rulesPath, file);
const content = fs.readFileSync(filePath, "utf-8");
const rule = this.parseRuleYaml(content);
if (rule) {
descriptions.set(rule.id, rule);
}
}
}
catch (err) {
this.log(`Failed to load rule descriptions: ${err.message}`);
}
this.ruleDescriptions = descriptions;
return descriptions;
}
parseRuleYaml(content) {
const result = {};
const idMatch = content.match(/^id:\s*(.+)$/m);
if (idMatch)
result.id = idMatch[1].trim();
const msgMatch = content.match(/^message:\s*"([^"]+)"/m) ||
content.match(/^message:\s*'([^']+)'/m) ||
content.match(/^message:\s*(.+)$/m);
if (msgMatch)
result.message = (msgMatch[3] || msgMatch[2] || msgMatch[1]).trim();
const noteMatch = content.match(/^note:\s*\|([\s\S]*?)(?=^\w|\n\n|\nrule:)/m);
if (noteMatch) {
result.note = noteMatch[1]
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line.length > 0)
.join(" ");
}
const sevMatch = content.match(/^severity:\s*(.+)$/m);
if (sevMatch)
result.severity = this.mapSeverity(sevMatch[1].trim());
const gradeMatch = content.match(/Grade\s+(\d+\.\d+)/i);
if (gradeMatch)
result.grade = parseFloat(gradeMatch[1]);
const fixMatch = content.match(/^fix:\s*\|?([\s\S]*?)(?=^\w|^rule:|Z)/m);
if (fixMatch) {
result.fix = fixMatch[1]
.split(/\r?\n/)
.map((line) => line.replace(/^\s*\|?\s*/, ""))
.filter((line) => line.length > 0)
.join("\n");
}
if (result.id && result.message) {
return result;
}
return null;
}
mapSeverity(severity) {
const lower = severity.toLowerCase();
if (lower === "error")
return "error";
if (lower === "warning")
return "warning";
if (lower === "info")
return "info";
return "hint";
}
}