perlnavigator-server
Version:
Perl language server
334 lines • 14.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.perlimports = exports.perlcritic = exports.perlcompile = void 0;
const node_1 = require("vscode-languageserver/node");
const types_1 = require("./types");
const path_1 = require("path");
const vscode_uri_1 = require("vscode-uri");
const utils_1 = require("./utils");
const parseTags_1 = require("./parseTags");
const assets_1 = require("./assets");
const parser_1 = require("./parser");
async function perlcompile(textDocument, workspaceFolders, settings) {
const parsingPromise = (0, parser_1.parseDocument)(textDocument, types_1.ParseType.selfNavigation);
if (!settings.perlCompileEnabled) {
const parsedDoc = await parsingPromise;
return { diags: [], perlDoc: parsedDoc };
}
let perlParams = [...settings.perlParams, "-c"];
let perlEnv = settings.perlEnv;
let perlEnvAdd = settings.perlEnvAdd;
const filePath = vscode_uri_1.default.parse(textDocument.uri).fsPath;
if (settings.enableWarnings)
perlParams = perlParams.concat(["-Mwarnings", "-M-warnings=redefine"]); // Force enable some warnings.
perlParams = perlParams.concat((0, utils_1.getIncPaths)(workspaceFolders, settings));
perlParams = perlParams.concat(await getInquisitor());
(0, utils_1.nLog)("Starting perl compilation check with the equivalent of: " + settings.perlPath + " " + perlParams.join(" ") + " " + filePath, settings);
let output;
let stdout;
let severity;
const diagnostics = [];
const code = getAdjustedPerlCode(textDocument, filePath);
try {
let options = { timeout: 10000, maxBuffer: 20 * 1024 * 1024 };
if (perlEnv) {
if (perlEnvAdd) {
options.env = { ...process.env, ...perlEnv };
}
else {
options.env = perlEnv;
}
}
const perlProcess = (0, utils_1.async_execFile)(settings.perlPath, perlParams, options);
perlProcess?.child?.stdin?.on("error", (error) => {
(0, utils_1.nLog)("Perl Compilation Error Caught: ", settings);
(0, utils_1.nLog)(error, settings);
});
perlProcess?.child?.stdin?.write(code);
perlProcess?.child?.stdin?.end();
const out = await perlProcess;
output = out.stderr.toString();
stdout = out.stdout.toString();
severity = node_1.DiagnosticSeverity.Warning;
}
catch (error) {
// TODO: Check if we overflowed the buffer.
if ("stderr" in error && "stdout" in error) {
output = error.stderr.toString();
stdout = error.stdout.toString();
severity = node_1.DiagnosticSeverity.Error;
}
else {
(0, utils_1.nLog)("Perlcompile failed with unknown error", settings);
(0, utils_1.nLog)(error, settings);
return;
}
}
const compiledDoc = (0, parseTags_1.buildNav)(stdout, filePath, textDocument.uri);
const parsedDoc = await parsingPromise;
const mergedDoc = mergeDocs(parsedDoc, compiledDoc);
output.split("\n").forEach((violation) => {
maybeAddCompDiag(violation, severity, diagnostics, filePath, mergedDoc);
});
// If a base object throws a warning multiple times, we want to deduplicate it to declutter the problems tab.
const uniq_diagnostics = Array.from(new Set(diagnostics.map((diag) => JSON.stringify(diag)))).map((str) => JSON.parse(str));
return { diags: uniq_diagnostics, perlDoc: mergedDoc };
}
exports.perlcompile = perlcompile;
async function getInquisitor() {
const inq_path = await (0, assets_1.getPerlAssetsPath)();
let inq = ["-I", inq_path, "-MInquisitor"];
return inq;
}
function getAdjustedPerlCode(textDocument, filePath) {
let code = textDocument.getText();
// module name regex stolen from https://metacpan.org/pod/Module::Runtime#$module_name_rx
const module_name_rx = /^\s*package[\s\n]+([A-Z_a-z][0-9A-Z_a-z]*(?:::[0-9A-Z_a-z]+)*)/gm;
let register_inc_path = "";
let module_name_match = module_name_rx.exec(code);
while (module_name_match != null) {
const module_name = module_name_match[1];
const inc_filename = module_name.replaceAll("::", "/") + ".pm";
// make sure the package found actually matches the filename
if (filePath.indexOf(inc_filename) != -1) {
register_inc_path = `\$INC{'${inc_filename}'} = '${filePath}';`;
break;
}
else {
module_name_match = module_name_rx.exec(code);
}
}
code =
`local \$0; use lib_bs22::SourceStash; BEGIN { \$0 = '${filePath}'; if (\$INC{'FindBin.pm'}) { FindBin->again(); }; \$lib_bs22::SourceStash::filename = '${filePath}'; print "Setting file" . __FILE__; ${register_inc_path} }\n# line 0 \"${filePath}\"\ndie('Not needed, but die for safety');\n` +
code;
return code;
}
function maybeAddCompDiag(violation, severity, diagnostics, filePath, perlDoc) {
violation = violation.replaceAll("\r", ""); // Clean up for Windows
violation = violation.replace(/, <STDIN> line 1\.$/g, ""); // Remove our stdin nonsense
let output = localizeErrors(violation, filePath, perlDoc);
if (typeof output == "undefined")
return;
const lineNum = output.lineNum;
violation = output.violation;
if (violation.indexOf("=PerlWarning=") != -1) {
// Downgrade severity for explicitly marked severities
severity = node_1.DiagnosticSeverity.Warning;
violation = violation.replaceAll("=PerlWarning=", ""); // Don't display the PerlWarnings
}
diagnostics.push({
severity: severity,
range: {
start: { line: lineNum, character: 0 },
end: { line: lineNum, character: 500 },
},
message: "Syntax: " + violation,
source: "perlnavigator",
});
}
function localizeErrors(violation, filePath, perlDoc) {
if (violation.indexOf("Too late to run CHECK block") != -1)
return;
let match = /^(.+)at\s+(.+?)\s+line\s+(\d+)/i.exec(violation);
if (match) {
if (match[2] == filePath) {
violation = match[1];
const lineNum = +match[3] - 1;
return { violation, lineNum };
}
else {
// The error/warnings must be in an imported library (possibly indirectly imported).
let lineNum = 0; // If indirectly imported
const importFileName = match[2].replace(".pm", "").replace(/[\\\/]/g, "::");
perlDoc.imported.forEach((line, mod) => {
// importFileName could be something like usr::lib::perl::dir::Foo::Bar
if (importFileName.endsWith(mod))
lineNum = line;
});
return { violation, lineNum };
}
}
match = /\s+is not exported by the ([\w:]+) module$/i.exec(violation);
if (match) {
let lineNum = perlDoc.imported.get(match[1]);
if (typeof lineNum == "undefined")
lineNum = 0;
return { violation, lineNum };
}
return;
}
async function perlcritic(textDocument, workspaceFolders, settings) {
if (!settings.perlcriticEnabled)
return [];
const critic_path = (0, path_1.join)(await (0, assets_1.getPerlAssetsPath)(), "criticWrapper.pl");
let criticParams = [...settings.perlParams, critic_path].concat(getCriticProfile(workspaceFolders, settings));
criticParams = criticParams.concat(["--file", vscode_uri_1.default.parse(textDocument.uri).fsPath]);
// Add any extra params from settings
if (settings.perlcriticSeverity)
criticParams = criticParams.concat(["--severity", settings.perlcriticSeverity.toString()]);
if (settings.perlcriticTheme)
criticParams = criticParams.concat(["--theme", settings.perlcriticTheme]);
if (settings.perlcriticExclude)
criticParams = criticParams.concat(["--exclude", settings.perlcriticExclude]);
if (settings.perlcriticInclude)
criticParams = criticParams.concat(["--include", settings.perlcriticInclude]);
(0, utils_1.nLog)("Now starting perlcritic with: " + criticParams.join(" "), settings);
const code = textDocument.getText();
const diagnostics = [];
let output;
try {
const process = (0, utils_1.async_execFile)(settings.perlPath, criticParams, { timeout: 25000 });
process?.child?.stdin?.on("error", (error) => {
(0, utils_1.nLog)("Perl Critic Error Caught: ", settings);
(0, utils_1.nLog)(error, settings);
});
process?.child?.stdin?.write(code);
process?.child?.stdin?.end();
const out = await process;
output = out.stdout;
}
catch (error) {
(0, utils_1.nLog)("Perlcritic failed with unknown error", settings);
(0, utils_1.nLog)(error, settings);
return diagnostics;
}
(0, utils_1.nLog)("Critic output: " + output, settings);
output.split("\n").forEach((violation) => {
maybeAddCriticDiag(violation, diagnostics, settings);
});
return diagnostics;
}
exports.perlcritic = perlcritic;
async function perlimports(textDocument, workspaceFolders, settings) {
if (!settings.perlimportsLintEnabled)
return [];
const importsPath = (0, path_1.join)(await (0, assets_1.getPerlAssetsPath)(), "perlimportsWrapper.pl");
const cliParams = [...settings.perlParams, importsPath, ...(0, utils_1.getPerlimportsProfile)(settings), "--lint", "--json", "--filename", vscode_uri_1.default.parse(textDocument.uri).fsPath];
(0, utils_1.nLog)("Now starting perlimports with: " + cliParams.join(" "), settings);
const code = textDocument.getText();
const diagnostics = [];
let output;
try {
const process = (0, utils_1.async_execFile)(settings.perlPath, cliParams, { timeout: 25000 });
process?.child?.stdin?.on("error", (error) => {
(0, utils_1.nLog)("perlimports Error Caught: " + error, settings);
});
process?.child?.stdin?.write(code);
process?.child?.stdin?.end();
const out = await process;
output = out.stdout;
}
catch (error) {
(0, utils_1.nLog)("Attempted to run perlimports lint: " + error.stdout, settings);
output = error.message;
}
// The first line will be an error message about perlimports failing.
// The last line may be blank.
output
.split("\n")
.filter((v) => v.startsWith("{"))
.forEach((violation) => {
maybeAddPerlImportsDiag(violation, diagnostics, settings);
});
return diagnostics;
}
exports.perlimports = perlimports;
function getCriticProfile(workspaceFolders, settings) {
let profileCmd = [];
if (settings.perlcriticProfile) {
let profile = settings.perlcriticProfile;
if (profile.indexOf("$workspaceFolder") != -1) {
if (workspaceFolders) {
// TODO: Fix this too. Only uses the first workspace folder
const workspaceUri = vscode_uri_1.default.parse(workspaceFolders[0].uri).fsPath;
profileCmd.push("--profile");
profileCmd.push(profile.replaceAll("$workspaceFolder", workspaceUri));
}
else {
(0, utils_1.nLog)("You specified $workspaceFolder in your perlcritic path, but didn't include any workspace folders. Ignoring profile.", settings);
}
}
else {
profileCmd.push("--profile");
profileCmd.push(profile);
}
}
return profileCmd;
}
function maybeAddCriticDiag(violation, diagnostics, settings) {
// Severity ~|~ Line ~|~ Column ~|~ Description ~|~ Policy ~||~ Newline
const tokens = violation.replace("~||~", "").replaceAll("\r", "").split("~|~");
if (tokens.length != 5) {
return;
}
const line_num = +tokens[1] - 1;
const col_num = +tokens[2] - 1;
const message = tokens[3] + " (" + tokens[4] + ", Severity: " + tokens[0] + ")";
const severity = getCriticDiagnosticSeverity(tokens[0], settings);
if (!severity) {
return;
}
diagnostics.push({
severity: severity,
range: {
start: { line: line_num, character: col_num },
end: { line: line_num, character: col_num + 500 }, // Arbitrarily large
},
message: "Critic: " + message,
source: "perlnavigator",
});
}
function maybeAddPerlImportsDiag(violation, diagnostics, settings) {
try {
const diag = JSON.parse(violation);
const loc = diag.location;
diagnostics.push({
message: `perlimports: ${diag.reason} \n\n ${diag.diff}`,
range: {
start: { line: Number(loc.start.line) - 1, character: Number(loc.start.column) - 1 },
end: { line: Number(loc.end.line) - 1, character: Number(loc.end.column) - 1 },
},
severity: node_1.DiagnosticSeverity.Warning,
source: "perlnavigator",
});
}
catch (error) {
(0, utils_1.nLog)(`Could not parse JSON violation ${error}`, settings);
}
}
function getCriticDiagnosticSeverity(severity_num, settings) {
// Unknown severity gets max (should never happen)
const severity_config = severity_num == "1"
? settings.severity1
: severity_num == "2"
? settings.severity2
: severity_num == "3"
? settings.severity3
: severity_num == "4"
? settings.severity4
: settings.severity5;
switch (severity_config) {
case "none":
return undefined;
case "hint":
return node_1.DiagnosticSeverity.Hint;
case "info":
return node_1.DiagnosticSeverity.Information;
case "warning":
return node_1.DiagnosticSeverity.Warning;
default:
return node_1.DiagnosticSeverity.Error;
}
}
function mergeDocs(doc1, doc2) {
// TODO: Redo this code. Instead of merging sources, you should keep track of where symbols came from
doc1.autoloads = new Map([...doc1.autoloads, ...doc2.autoloads]);
doc1.canonicalElems = new Map([...doc1.canonicalElems, ...doc2.canonicalElems]);
// TODO: Should elems be merged? Probably. Or tagged doc and compilation results are totally split
doc1.elems = new Map([...doc2.elems, ...doc1.elems]); // Tagged docs have priority?
doc1.imported = new Map([...doc1.imported, ...doc2.imported]);
doc1.parents = new Map([...doc1.parents, ...doc2.parents]);
doc1.uri = doc2.uri;
return doc1;
}
//# sourceMappingURL=diagnostics.js.map