UNPKG

perlnavigator-server

Version:

Perl language server

334 lines 14.7 kB
"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