pi-lens
Version:
Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo
160 lines (159 loc) • 5.66 kB
JavaScript
import * as fs from "node:fs";
import * as path from "node:path";
import { safeSpawnAsync } from "../../safe-spawn.js";
import { PRIORITY } from "../priorities.js";
import { createAvailabilityChecker } from "./utils/runner-helpers.js";
const dotnet = createAvailabilityChecker("dotnet", ".exe");
function findProjectTarget(cwd) {
const entries = fs.readdirSync(cwd);
const solution = entries.find((entry) => /\.(sln|slnx)$/i.test(entry));
if (solution)
return solution;
return entries.find((entry) => /\.(csproj)$/i.test(entry));
}
function isDotnetRuleId(value) {
if (value.length < 2)
return false;
let sawDigit = false;
for (const char of value) {
const code = char.charCodeAt(0);
const isLetter = (code >= 65 && code <= 90) || (code >= 97 && code <= 122);
const isDigit = code >= 48 && code <= 57;
if (isDigit) {
sawDigit = true;
continue;
}
if (!isLetter || sawDigit)
return false;
}
return sawDigit;
}
function parseDotnetDiagnosticLine(line) {
const lower = line.toLowerCase();
const fileEnd = lower.lastIndexOf(".cs(");
if (fileEnd < 0)
return null;
const reportedFile = line.slice(0, fileEnd + 3).trim();
let cursor = fileEnd + 4;
const lineStart = cursor;
while (cursor < line.length &&
line.charCodeAt(cursor) >= 48 &&
line.charCodeAt(cursor) <= 57) {
cursor += 1;
}
if (cursor === lineStart || line[cursor] !== ",")
return null;
const lineStr = line.slice(lineStart, cursor);
cursor += 1;
const colStart = cursor;
while (cursor < line.length &&
line.charCodeAt(cursor) >= 48 &&
line.charCodeAt(cursor) <= 57) {
cursor += 1;
}
if (cursor === colStart || line.slice(cursor, cursor + 3) !== "): ")
return null;
const colStr = line.slice(colStart, cursor);
cursor += 3;
const tailLower = line.slice(cursor).toLowerCase();
let severityLabel;
if (tailLower.startsWith("error ")) {
severityLabel = "error";
cursor += "error ".length;
}
else if (tailLower.startsWith("warning ")) {
severityLabel = "warning";
cursor += "warning ".length;
}
else {
return null;
}
const colon = line.indexOf(":", cursor);
if (colon < 0)
return null;
const rule = line.slice(cursor, colon).trim();
if (!isDotnetRuleId(rule))
return null;
let message = line.slice(colon + 1).trim();
if (message.endsWith("]")) {
const projectSuffix = message.lastIndexOf(" [");
if (projectSuffix >= 0)
message = message.slice(0, projectSuffix).trimEnd();
}
return { reportedFile, lineStr, colStr, severityLabel, rule, message };
}
function parseDotnetBuildOutput(raw, filePath) {
const diagnostics = [];
for (const line of raw.split(/\r?\n/)) {
const parsed = parseDotnetDiagnosticLine(line);
if (!parsed)
continue;
const { reportedFile, lineStr, colStr, severityLabel, rule, message } = parsed;
const resolvedReported = path.resolve(reportedFile);
const resolvedTarget = path.resolve(filePath);
if (resolvedReported !== resolvedTarget)
continue;
const severity = severityLabel.toLowerCase() === "error" ? "error" : "warning";
const lineNum = Number.parseInt(lineStr, 10) || 1;
const colNum = Number.parseInt(colStr, 10) || 1;
diagnostics.push({
id: `dotnet-build-${rule}-${lineNum}-${colNum}`,
message: `[${rule}] ${message.trim()}`,
filePath,
line: lineNum,
column: colNum,
severity,
semantic: severity === "error" ? "blocking" : "warning",
tool: "dotnet-build",
rule,
fixable: false,
});
}
return diagnostics;
}
const dotnetBuildRunner = {
id: "dotnet-build",
appliesTo: ["csharp"],
priority: PRIORITY.GENERAL_ANALYSIS,
enabledByDefault: true,
timeoutMs: 90_000,
skipTestFiles: false,
async run(ctx) {
const cwd = ctx.cwd || process.cwd();
if (!(await (dotnet.isAvailableAsync(cwd)))) {
return { status: "skipped", diagnostics: [], semantic: "none" };
}
const cmd = dotnet.getCommand(cwd);
if (!cmd) {
return { status: "skipped", diagnostics: [], semantic: "none" };
}
const target = findProjectTarget(cwd);
if (!target) {
return { status: "skipped", diagnostics: [], semantic: "none" };
}
const result = await safeSpawnAsync(cmd, ["build", target, "--nologo", "--verbosity", "minimal"], {
cwd,
timeout: 60000,
});
const raw = `${result.stdout ?? ""}\n${result.stderr ?? ""}`.trim();
if (result.status === 0 && !raw) {
return { status: "succeeded", diagnostics: [], semantic: "none" };
}
const diagnostics = parseDotnetBuildOutput(raw, ctx.filePath);
if (diagnostics.length === 0) {
return {
status: result.status === 0 ? "succeeded" : "failed",
diagnostics: [],
semantic: "warning",
rawOutput: raw,
};
}
const hasErrors = diagnostics.some((d) => d.severity === "error");
return {
status: hasErrors ? "failed" : "succeeded",
diagnostics,
semantic: hasErrors ? "blocking" : "warning",
};
},
};
export default dotnetBuildRunner;