754 lines
34 KiB
JavaScript
754 lines
34 KiB
JavaScript
"use strict";
|
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
});
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.deactivate = exports.activate = void 0;
|
|
const vscode = require("vscode");
|
|
const path = require("path");
|
|
const crypto = require("crypto");
|
|
const child_process_promise_1 = require("child-process-promise");
|
|
const fs = require("fs");
|
|
const lookpath_1 = require("lookpath");
|
|
const untildify = require("untildify");
|
|
const shlex_1 = require("shlex");
|
|
const AsyncLock = require("async-lock");
|
|
const allSettled = require("promise.allsettled");
|
|
const mypy_1 = require("./mypy");
|
|
const diagnostics = new Map();
|
|
const outputChannel = vscode.window.createOutputChannel('Mypy');
|
|
let _context;
|
|
let lock = new AsyncLock();
|
|
let statusBarItem;
|
|
let activeChecks = 0;
|
|
let checkIndex = 1;
|
|
let activated = false;
|
|
let logFile;
|
|
function activate(context) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
activated = true;
|
|
_context = context;
|
|
context.subscriptions.push(outputChannel);
|
|
const extension = vscode.extensions.getExtension('matangover.mypy');
|
|
const currentVersion = extension === null || extension === void 0 ? void 0 : extension.packageJSON.version;
|
|
context.globalState.update('extensionVersion', currentVersion);
|
|
initDebugLog(context);
|
|
output(`Mypy extension activated, version ${currentVersion}`);
|
|
if ((extension === null || extension === void 0 ? void 0 : extension.extensionKind) === vscode.ExtensionKind.Workspace) {
|
|
output('Running remotely');
|
|
}
|
|
if (logFile) {
|
|
output(`Saving debug log to: ${logFile}`);
|
|
}
|
|
statusBarItem = vscode.window.createStatusBarItem();
|
|
context.subscriptions.push(statusBarItem);
|
|
statusBarItem.text = "$(gear~spin) mypy";
|
|
output('Registering listener for interpreter changed event');
|
|
const pythonExtensionAPI = yield getPythonExtensionAPI(undefined);
|
|
if (pythonExtensionAPI !== undefined) {
|
|
const handler = pythonExtensionAPI.environments.onDidChangeActiveEnvironmentPath(activeInterpreterChanged);
|
|
context.subscriptions.push(handler);
|
|
output('Listener registered');
|
|
}
|
|
context.subscriptions.push(vscode.workspace.onDidChangeWorkspaceFolders(workspaceFoldersChanged), vscode.workspace.onDidSaveTextDocument(documentSaved), vscode.workspace.onDidDeleteFiles(filesDeleted), vscode.workspace.onDidRenameFiles(filesRenamed), vscode.workspace.onDidCreateFiles(filesCreated), vscode.workspace.onDidChangeConfiguration(configurationChanged), vscode.commands.registerCommand("mypy.recheckWorkspace", recheckWorkspace), vscode.commands.registerCommand("mypy.restartAndRecheckWorkspace", restartAndRecheckWorkspace));
|
|
// This is used to show the custom commands only when the extension is active.
|
|
vscode.commands.executeCommand("setContext", "mypy.activated", true);
|
|
// Do _not_ await this call on purpose, so that extension activation finishes quickly. This is
|
|
// important because if VS Code is closed before the checks are done, deactivate will only be
|
|
// called if activate has already finished.
|
|
forEachFolder(vscode.workspace.workspaceFolders, folder => checkWorkspace(folder.uri));
|
|
output('Activation complete');
|
|
});
|
|
}
|
|
exports.activate = activate;
|
|
function initDebugLog(context) {
|
|
const mypyConfig = vscode.workspace.getConfiguration('mypy');
|
|
const debug = mypyConfig.get('debugLogging', false);
|
|
if (debug) {
|
|
try {
|
|
const storageDir = context.globalStorageUri.fsPath;
|
|
fs.mkdirSync(storageDir, { recursive: true });
|
|
logFile = path.join(storageDir, "mypy_extension.log");
|
|
}
|
|
catch (e) {
|
|
output(`Failed to create extension storage directory: ${e}`);
|
|
}
|
|
}
|
|
}
|
|
function deactivate() {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
activated = false;
|
|
output(`Mypy extension deactivating, shutting down daemons...`);
|
|
yield forEachFolder(vscode.workspace.workspaceFolders, folder => stopDaemon(folder.uri));
|
|
output(`Mypy daemons stopped, extension deactivated`);
|
|
vscode.commands.executeCommand("setContext", "mypy.activated", false);
|
|
});
|
|
}
|
|
exports.deactivate = deactivate;
|
|
function workspaceFoldersChanged(e) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
const format = (folders) => folders.map(f => f.name).join(", ") || "none";
|
|
output(`Workspace folders changed. Added: ${format(e.added)}. Removed: ${format(e.removed)}.`);
|
|
yield forEachFolder(e.removed, (folder) => __awaiter(this, void 0, void 0, function* () {
|
|
var _a;
|
|
yield stopDaemon(folder.uri);
|
|
(_a = diagnostics.get(folder.uri)) === null || _a === void 0 ? void 0 : _a.dispose();
|
|
diagnostics.delete(folder.uri);
|
|
}));
|
|
yield forEachFolder(e.added, folder => checkWorkspace(folder.uri));
|
|
});
|
|
}
|
|
function forEachFolder(folders, func, ignoreErrors = true) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
if (folders === undefined) {
|
|
return;
|
|
}
|
|
// Run the function for each callback, and catch errors if any.
|
|
// Use allSettled instead of Promise.all to always await all Promises, even if one rejects.
|
|
const promises = folders.map(func);
|
|
const results = yield allSettled(promises);
|
|
const rejections = [];
|
|
for (const [index, result] of results.entries()) {
|
|
if (result.status === "rejected") {
|
|
const folder = folders[index];
|
|
const folderUri = folder instanceof vscode.Uri ? folder : folder.uri;
|
|
rejections.push({
|
|
folder: folderUri.fsPath,
|
|
error: result.reason
|
|
});
|
|
}
|
|
}
|
|
if (rejections.length > 0) {
|
|
if (ignoreErrors) {
|
|
const errorString = rejections.map(r => `${r.folder}: ${errorToString(r.error)}`).join("\n");
|
|
output("forEachFolder ignored errors in the following folders:\n" + errorString);
|
|
}
|
|
else {
|
|
throw rejections;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
function errorToString(error) {
|
|
if (error instanceof Error && error.stack) {
|
|
return error.stack;
|
|
}
|
|
else {
|
|
return String(error);
|
|
}
|
|
}
|
|
function stopDaemon(folder, retry = true) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
output(`Stop daemon: ${folder.fsPath}`);
|
|
const result = yield runDmypy(folder, 'stop');
|
|
if (result.success) {
|
|
output(`Stopped daemon: ${folder.fsPath}`);
|
|
}
|
|
else {
|
|
if (retry) {
|
|
// Daemon stopping can fail with 'Status file not found' if the daemon has been started
|
|
// very recently and hasn't written the status file yet. In that case, retry, otherwise
|
|
// we might leave a zombie daemon running. This happened due to the following events:
|
|
// 1. Open folder in VS Code, and then add another workspace folder
|
|
// 2. VS Code fires onDidChangeWorkspaceFolders and onDidChangeConfiguration, which
|
|
// causes us to queue two checks. (This is probably a bug in VS Code.)
|
|
// 3. VS Code immediately restarts the Extension Host process, which causes our
|
|
// extension to deactivate.
|
|
// 4. We try to stop the daemon but it is not yet running. We then start the daemon
|
|
// because of the queued check(s), which results in a zombie daemon.
|
|
// This simple retry solves the issue.
|
|
output(`Daemon stopping failed, retrying in 1 second: ${folder.fsPath}`);
|
|
yield sleep(1000);
|
|
yield stopDaemon(folder, false);
|
|
}
|
|
else {
|
|
output(`Daemon stopping failed again, giving up: ${folder.fsPath}`);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
function runDmypy(folder, dmypyCommand, mypyArgs = [], warnIfFailed = false, successfulExitCodes, addPythonExecutableArgument = false, currentCheck, retryIfDaemonStuck = true) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
let dmypyGlobalArgs = [];
|
|
let dmypyCommandArgs = [];
|
|
let statusFilePath = null;
|
|
// Store the dmypy status file in the extension's workspace storage folder, instead of the
|
|
// default location which is .dmypy.json in the cwd.
|
|
if ((_context === null || _context === void 0 ? void 0 : _context.storageUri) !== undefined) {
|
|
fs.mkdirSync(_context.storageUri.fsPath, { recursive: true });
|
|
const folderHash = crypto.createHash('sha1').update(folder.toString()).digest('hex');
|
|
const statusFileName = `dmypy-${folderHash}-${process.pid}.json`;
|
|
statusFilePath = path.join(_context.storageUri.fsPath, statusFileName);
|
|
dmypyGlobalArgs = ["--status-file", statusFilePath];
|
|
const commandsSupportingLog = ["start", "restart", "run"];
|
|
if (commandsSupportingLog.includes(dmypyCommand)) {
|
|
const logFileName = `dmypy-${folderHash}.log`;
|
|
const logFilePath = path.join(_context.storageUri.fsPath, logFileName);
|
|
dmypyCommandArgs = ['--log-file', logFilePath];
|
|
}
|
|
}
|
|
const activeInterpreter = yield getActiveInterpreter(folder, currentCheck);
|
|
const mypyConfig = vscode.workspace.getConfiguration('mypy', folder);
|
|
let executable;
|
|
const runUsingActiveInterpreter = mypyConfig.get('runUsingActiveInterpreter');
|
|
let executionArgs = [];
|
|
if (runUsingActiveInterpreter) {
|
|
executable = activeInterpreter;
|
|
executionArgs = ["-m", "mypy.dmypy"];
|
|
if (executable === undefined) {
|
|
warn("Could not run mypy: no active interpreter. Please activate an interpreter in the " +
|
|
"Python extension or switch off the mypy.runUsingActiveInterpreter setting.", warnIfFailed, currentCheck);
|
|
}
|
|
}
|
|
else {
|
|
executable = yield getDmypyExecutable(folder, warnIfFailed, currentCheck);
|
|
}
|
|
if (executable === undefined) {
|
|
return { success: false, stdout: null };
|
|
}
|
|
if (addPythonExecutableArgument && activeInterpreter) {
|
|
mypyArgs = ['--python-executable', activeInterpreter, ...mypyArgs];
|
|
}
|
|
const args = [...executionArgs, ...dmypyGlobalArgs, dmypyCommand, ...dmypyCommandArgs];
|
|
if (mypyArgs.length > 0) {
|
|
args.push("--", ...mypyArgs);
|
|
}
|
|
const command = [executable, ...args].map(shlex_1.quote).join(" ");
|
|
output(`Running dmypy in folder ${folder.fsPath}\n${command}`, currentCheck);
|
|
try {
|
|
const result = yield child_process_promise_1.spawn(executable, args, {
|
|
cwd: folder.fsPath,
|
|
capture: ['stdout', 'stderr'],
|
|
successfulExitCodes
|
|
});
|
|
if (result.code == 1 && result.stderr) {
|
|
// This might happen when running using `python -m mypy.dmypy` and some error in the
|
|
// interpreter occurs, such as import error when mypy is not installed.
|
|
let error = '';
|
|
if (runUsingActiveInterpreter && result.stderr.includes('ModuleNotFoundError')) {
|
|
error = 'Probably mypy is not installed in the active interpreter ' +
|
|
`(${activeInterpreter}). Either install mypy in this interpreter or switch ` +
|
|
'off the mypy.runUsingActiveInterpreter setting. ';
|
|
}
|
|
warn(`Error running mypy in ${folder.fsPath}. ${error}See Output panel for details.`, warnIfFailed, currentCheck, true);
|
|
if (result.stdout) {
|
|
output(`stdout:\n${result.stdout}`, currentCheck);
|
|
}
|
|
output(`stderr:\n${result.stderr}`, currentCheck);
|
|
return { success: false, stdout: result.stdout };
|
|
}
|
|
return { success: true, stdout: result.stdout };
|
|
}
|
|
catch (exception) {
|
|
let error = exception.toString();
|
|
let showDetailsButton = false;
|
|
if (exception.name === 'ChildProcessError') {
|
|
const ex = exception;
|
|
if (ex.code !== undefined) {
|
|
let errorString;
|
|
if (ex.stderr) {
|
|
// Show only first line of error to user because Newlines are stripped in VSCode
|
|
// warning messages and it can appear confusing.
|
|
let mypyError = ex.stderr.split("\n")[0];
|
|
if (mypyError.length > 300) {
|
|
mypyError = mypyError.slice(0, 300) + " [...]";
|
|
}
|
|
errorString = `error: "${mypyError}"`;
|
|
}
|
|
else {
|
|
errorString = `exit code ${ex.code}`;
|
|
}
|
|
error = `mypy failed with ${errorString}. See Output panel for details.`;
|
|
showDetailsButton = true;
|
|
}
|
|
if (ex.stdout) {
|
|
if (ex.code == 2 && !ex.stderr) {
|
|
// Mypy considers syntax errors as fatal errors (exit code 2). The daemon's
|
|
// exit code is inconsistent in this case (e.g. for syntax errors it can return
|
|
// either 1 or 2).
|
|
return { success: true, stdout: ex.stdout };
|
|
}
|
|
output(`stdout:\n${ex.stdout}`, currentCheck);
|
|
}
|
|
if (ex.stderr) {
|
|
output(`stderr:\n${ex.stderr}`, currentCheck);
|
|
if (ex.stderr.indexOf('Daemon crashed!') != -1) {
|
|
error = 'the mypy daemon crashed. This is probably a bug in mypy itself, ' +
|
|
'see Output panel for details. The daemon will be restarted automatically.';
|
|
showDetailsButton = true;
|
|
}
|
|
else if (ex.stderr.indexOf('There are no .py[i] files in directory') != -1) {
|
|
// Swallow this error. This may happen if one workspace folder contains
|
|
// Python files and another folder doesn't, or if a workspace contains Python
|
|
// files that are not reachable from the target directory.
|
|
return { success: true, stdout: '' };
|
|
}
|
|
else if (ex.stderr.indexOf('Connection refused') != -1 ||
|
|
ex.stderr.indexOf('[Errno 2] No such file') != -1 ||
|
|
ex.stderr.indexOf('Socket operation on non-socket') != -1) {
|
|
// This can happen if the daemon is stuck, or if the status file is stale due to
|
|
// e.g. a previous daemon that hasn't been stopped properly. See:
|
|
// https://github.com/matangover/mypy-vscode/issues/37
|
|
// https://github.com/matangover/mypy-vscode/issues/45
|
|
// To reproduce the above exceptions:
|
|
// 1. 'Connection refused': kill daemon process (so that it stops listening on
|
|
// the socket), and change the pid in status file to any running process.
|
|
// 2. 'No such file': change connection_name in status file to a non-existent
|
|
// file.
|
|
// 3. 'Socket operation on non-socket': change connection_name in status file
|
|
// to an existing file which is not a socket
|
|
if (retryIfDaemonStuck) {
|
|
// Kill the daemon.
|
|
output("Daemon is stuck or status file is stale. Killing daemon", currentCheck);
|
|
yield killDaemon(folder, currentCheck, statusFilePath);
|
|
// Run the same command again, but this time don't retry if it fails.
|
|
yield sleep(1000);
|
|
output("Retrying command", currentCheck);
|
|
return yield runDmypy(folder, dmypyCommand, mypyArgs, warnIfFailed, successfulExitCodes, addPythonExecutableArgument, currentCheck, false);
|
|
}
|
|
else {
|
|
error = 'the mypy daemon is stuck. An attempt to kill it and retry failed. ' +
|
|
'This is probably a bug in mypy itself, see Output panel for details.';
|
|
showDetailsButton = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
warn(`Error running mypy in ${folder.fsPath}: ${error}`, warnIfFailed, currentCheck, showDetailsButton);
|
|
return { success: false, stdout: null };
|
|
}
|
|
});
|
|
}
|
|
function killDaemon(folder, currentCheck, statusFilePath) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
const killResult = yield runDmypy(folder, "kill", undefined, undefined, undefined, undefined, currentCheck, false);
|
|
output(`Ran dmypy kill, stdout: ${killResult.stdout}`, currentCheck);
|
|
if (killResult.success) {
|
|
output("Daemon killed successfully", currentCheck);
|
|
return;
|
|
}
|
|
output("Error killing daemon, attempt to delete status file", currentCheck);
|
|
if (statusFilePath) {
|
|
try {
|
|
fs.unlinkSync(statusFilePath);
|
|
}
|
|
catch (e) {
|
|
output(`Error deleting status file: ${errorToString(e)}`, currentCheck);
|
|
}
|
|
}
|
|
else {
|
|
output("No status file to delete", currentCheck);
|
|
}
|
|
});
|
|
}
|
|
function recheckWorkspace() {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
output("Rechecking workspace");
|
|
yield forEachFolder(vscode.workspace.workspaceFolders, folder => checkWorkspace(folder.uri));
|
|
output("Recheck complete");
|
|
});
|
|
}
|
|
function restartAndRecheckWorkspace() {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
output("Stopping daemons");
|
|
yield forEachFolder(vscode.workspace.workspaceFolders, folder => stopDaemon(folder.uri));
|
|
yield recheckWorkspace();
|
|
});
|
|
}
|
|
function getDmypyExecutable(folder, warnIfFailed, currentCheck) {
|
|
var _a;
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
const mypyConfig = vscode.workspace.getConfiguration('mypy', folder);
|
|
let dmypyExecutable = (_a = mypyConfig.get('dmypyExecutable')) !== null && _a !== void 0 ? _a : 'dmypy';
|
|
let helpURL = "https://github.com/matangover/mypy-vscode#installing-mypy";
|
|
const isCommand = path.parse(dmypyExecutable).dir === '';
|
|
if (isCommand) {
|
|
const executable = yield lookpath_1.lookpath(dmypyExecutable);
|
|
if (executable === undefined) {
|
|
warn(`The mypy daemon executable ('${dmypyExecutable}') was not found on your PATH. ` +
|
|
`Please install mypy or adjust the mypy.dmypyExecutable setting.`, warnIfFailed, currentCheck, undefined, helpURL);
|
|
return undefined;
|
|
}
|
|
dmypyExecutable = executable;
|
|
}
|
|
else {
|
|
dmypyExecutable = untildify(dmypyExecutable).replace('${workspaceFolder}', folder.fsPath);
|
|
if (!fs.existsSync(dmypyExecutable)) {
|
|
warn(`The mypy daemon executable ('${dmypyExecutable}') was not found. ` +
|
|
`Please install mypy or adjust the mypy.dmypyExecutable setting.`, warnIfFailed, currentCheck, undefined, helpURL);
|
|
return undefined;
|
|
}
|
|
}
|
|
return dmypyExecutable;
|
|
});
|
|
}
|
|
function documentSaved(document) {
|
|
const folder = vscode.workspace.getWorkspaceFolder(document.uri);
|
|
if (!folder) {
|
|
return;
|
|
}
|
|
if (document.languageId == "python" || isMaybeConfigFile(folder, document.fileName)) {
|
|
output(`Document saved: ${document.uri.fsPath}`);
|
|
checkWorkspace(folder.uri);
|
|
}
|
|
}
|
|
function isMaybeConfigFile(folder, file) {
|
|
if (isConfigFileName(file)) {
|
|
return true;
|
|
}
|
|
let configFile = vscode.workspace.getConfiguration("mypy", folder).get("configFile");
|
|
if (configFile === undefined) {
|
|
return false;
|
|
}
|
|
if (!path.isAbsolute(configFile)) {
|
|
configFile = path.join(folder.uri.fsPath, configFile);
|
|
}
|
|
return path.normalize(configFile) == path.normalize(file);
|
|
}
|
|
function isConfigFileName(file) {
|
|
const name = path.basename(file);
|
|
return name == "mypy.ini" || name == ".mypy.ini" || name == "setup.cfg" || name == "config";
|
|
}
|
|
function configurationChanged(event) {
|
|
var _a;
|
|
const folders = (_a = vscode.workspace.workspaceFolders) !== null && _a !== void 0 ? _a : [];
|
|
const affectedFolders = folders.filter(folder => event.affectsConfiguration("mypy", folder));
|
|
if (affectedFolders.length === 0) {
|
|
return;
|
|
}
|
|
const affectedFoldersString = affectedFolders.map(f => f.uri.fsPath).join(", ");
|
|
output(`Mypy settings changed: ${affectedFoldersString}`);
|
|
forEachFolder(affectedFolders, folder => checkWorkspace(folder.uri));
|
|
}
|
|
function checkWorkspace(folder) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
// Don't check the same workspace folder more than once at the same time.
|
|
yield lock.acquire(folder.fsPath, () => checkWorkspaceInternal(folder));
|
|
});
|
|
}
|
|
function checkWorkspaceInternal(folder) {
|
|
var _a;
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
if (!activated) {
|
|
// This can happen if a check was queued right before the extension was deactivated.
|
|
// We don't want to check in that case since it would cause a zombie daemon.
|
|
output(`Extension is not activated, not checking: ${folder.fsPath}`);
|
|
return;
|
|
}
|
|
const mypyConfig = vscode.workspace.getConfiguration("mypy", folder);
|
|
if (!mypyConfig.get("enabled", true)) {
|
|
output(`Mypy disabled for folder: ${folder.fsPath}`);
|
|
const folderDiagnostics = diagnostics.get(folder);
|
|
if (folderDiagnostics) {
|
|
folderDiagnostics.clear();
|
|
}
|
|
return;
|
|
}
|
|
statusBarItem.show();
|
|
activeChecks++;
|
|
const currentCheck = checkIndex;
|
|
checkIndex++;
|
|
output(`Check folder: ${folder.fsPath}`, currentCheck);
|
|
let targets = mypyConfig.get("targets", []);
|
|
const mypyArgs = [...targets, '--show-error-end', '--no-error-summary', '--no-pretty', '--no-color-output'];
|
|
const configFile = mypyConfig.get("configFile");
|
|
if (configFile) {
|
|
output(`Using config file: ${configFile}`, currentCheck);
|
|
mypyArgs.push('--config-file', configFile);
|
|
}
|
|
const extraArguments = mypyConfig.get("extraArguments");
|
|
if (extraArguments !== undefined && extraArguments.length > 0) {
|
|
output(`Using extra arguments: ${extraArguments}`, currentCheck);
|
|
mypyArgs.push(...extraArguments);
|
|
}
|
|
const result = yield runDmypy(folder, 'run', mypyArgs, true, [0, 1], true, currentCheck);
|
|
activeChecks--;
|
|
if (activeChecks == 0) {
|
|
statusBarItem.hide();
|
|
}
|
|
if (result.stdout !== null) {
|
|
output(`Mypy output:\n${(_a = result.stdout) !== null && _a !== void 0 ? _a : "\n"}`, currentCheck);
|
|
}
|
|
const folderDiagnostics = getWorkspaceDiagnostics(folder);
|
|
folderDiagnostics.clear();
|
|
if (result.success && result.stdout) {
|
|
const fileDiagnostics = parseMypyOutput(result.stdout, folder);
|
|
folderDiagnostics.set(Array.from(fileDiagnostics.entries()));
|
|
}
|
|
});
|
|
}
|
|
function parseMypyOutput(stdout, folder) {
|
|
const outputLines = [];
|
|
stdout.split(/\r?\n/).forEach(line => {
|
|
const match = mypy_1.mypyOutputPattern.exec(line);
|
|
if (match !== null) {
|
|
const line = match.groups;
|
|
const previousLine = outputLines[outputLines.length - 1];
|
|
if (previousLine && line.type == "note" && previousLine.type == "note" && line.location == previousLine.location) {
|
|
// This line continues the note on the previous line, merge them.
|
|
previousLine.message += "\n" + line.message;
|
|
}
|
|
else {
|
|
outputLines.push(line);
|
|
}
|
|
}
|
|
});
|
|
let fileDiagnostics = new Map();
|
|
for (const line of outputLines) {
|
|
const diagnostic = createDiagnostic(line);
|
|
const fileUri = getFileUri(line.file, folder);
|
|
if (!fileDiagnostics.has(fileUri)) {
|
|
fileDiagnostics.set(fileUri, []);
|
|
}
|
|
const thisFileDiagnostics = fileDiagnostics.get(fileUri);
|
|
thisFileDiagnostics.push(diagnostic);
|
|
}
|
|
return fileDiagnostics;
|
|
}
|
|
function getLinkUrl(line) {
|
|
if (line.type == "note") {
|
|
const seeLines = line.message.split(/\r?\n/).filter(l => l.startsWith("See https://"));
|
|
if (seeLines.length > 0) {
|
|
return seeLines[0].slice(4);
|
|
}
|
|
}
|
|
else {
|
|
if (line.code) {
|
|
return `https://mypy.readthedocs.io/en/stable/_refs.html#code-${line.code}`;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
function getFileUri(filePath, folder) {
|
|
// By default mypy outputs paths relative to the checked folder. If the user specifies
|
|
// `show_absolute_path = True` in the config file, mypy outputs absolute paths.
|
|
if (!path.isAbsolute(filePath)) {
|
|
filePath = path.join(folder.fsPath, filePath);
|
|
}
|
|
const fileUri = vscode.Uri.file(filePath);
|
|
return fileUri;
|
|
}
|
|
function createDiagnostic(line) {
|
|
var _a;
|
|
// Mypy output is 1-based, VS Code is 0-based.
|
|
const lineNo = parseInt(line.line) - 1;
|
|
let column = 0;
|
|
if (line.column !== undefined) {
|
|
column = parseInt(line.column) - 1;
|
|
}
|
|
let endLineNo = lineNo;
|
|
let endColumn = column;
|
|
if (line.endLine !== undefined && line.endColumn !== undefined) {
|
|
endLineNo = parseInt(line.endLine) - 1;
|
|
// Mypy's endColumn is inclusive, VS Code's is exclusive.
|
|
endColumn = parseInt(line.endColumn);
|
|
if (lineNo == endLineNo && column == endColumn - 1) {
|
|
// Mypy gave a zero-length range, give a zero-length range for VS Code as well, so that
|
|
// the error squiggle marks the entire word at that position.
|
|
endColumn = column;
|
|
}
|
|
}
|
|
const range = new vscode.Range(lineNo, column, endLineNo, endColumn);
|
|
const diagnostic = new vscode.Diagnostic(range, line.message, line.type === "error"
|
|
? vscode.DiagnosticSeverity.Error
|
|
: vscode.DiagnosticSeverity.Information);
|
|
diagnostic.source = "mypy";
|
|
const errorCode = (_a = line.code) !== null && _a !== void 0 ? _a : "note";
|
|
const url = getLinkUrl(line);
|
|
if (url === undefined) {
|
|
diagnostic.code = errorCode;
|
|
}
|
|
else {
|
|
diagnostic.code = {
|
|
value: errorCode,
|
|
target: vscode.Uri.parse(url),
|
|
};
|
|
}
|
|
return diagnostic;
|
|
}
|
|
function getWorkspaceDiagnostics(folder) {
|
|
let workspaceDiagnostics = diagnostics.get(folder);
|
|
if (workspaceDiagnostics) {
|
|
return workspaceDiagnostics;
|
|
}
|
|
else {
|
|
const workspaceDiagnostics = vscode.languages.createDiagnosticCollection('mypy');
|
|
diagnostics.set(folder, workspaceDiagnostics);
|
|
_context.subscriptions.push(workspaceDiagnostics);
|
|
return workspaceDiagnostics;
|
|
}
|
|
}
|
|
function getActiveInterpreter(folder, currentCheck) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
const path = yield getPythonPathFromPythonExtension(folder, currentCheck);
|
|
if (path === undefined) {
|
|
return undefined;
|
|
}
|
|
if (!fs.existsSync(path)) {
|
|
warn(`The selected Python interpreter does not exist: ${path}`, false, currentCheck);
|
|
return undefined;
|
|
}
|
|
return path;
|
|
});
|
|
}
|
|
// The VS Code Python extension manages its own internal Python interpreter configuration. This
|
|
// function was originally taken from pyright but modified to work with the new environments API:
|
|
// https://github.com/microsoft/vscode-python/wiki/Python-Environment-APIs
|
|
function getPythonPathFromPythonExtension(scopeUri, currentCheck) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
try {
|
|
const api = yield getPythonExtensionAPI(currentCheck);
|
|
if (api === undefined) {
|
|
return;
|
|
}
|
|
const environmentPath = api.environments.getActiveEnvironmentPath(scopeUri);
|
|
const environment = yield api.environments.resolveEnvironment(environmentPath);
|
|
if (environment === undefined) {
|
|
output('Invalid Python environment returned by Python extension', currentCheck);
|
|
return;
|
|
}
|
|
if (environment.executable.uri === undefined) {
|
|
output('Invalid Python executable path returned by Python extension', currentCheck);
|
|
return;
|
|
}
|
|
const result = environment.executable.uri.fsPath;
|
|
output(`Received Python path from Python extension: ${result}`, currentCheck);
|
|
return result;
|
|
}
|
|
catch (error) {
|
|
output(`Exception when reading Python path from Python extension: ${errorToString(error)}`, currentCheck);
|
|
}
|
|
return undefined;
|
|
});
|
|
}
|
|
function activeInterpreterChanged(e) {
|
|
var _a;
|
|
const resource = e.resource;
|
|
if (resource === undefined) {
|
|
output(`Active interpreter changed for resource: unknown`);
|
|
(_a = vscode.workspace.workspaceFolders) === null || _a === void 0 ? void 0 : _a.map(folder => checkWorkspace(folder.uri));
|
|
}
|
|
else {
|
|
output(`Active interpreter changed for resource: ${resource.uri.fsPath}`);
|
|
checkWorkspace(resource.uri);
|
|
}
|
|
}
|
|
function getPythonExtensionAPI(currentCheck) {
|
|
var _a;
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
const extension = vscode.extensions.getExtension('ms-python.python');
|
|
if (!extension) {
|
|
output('Python extension not found', currentCheck);
|
|
return undefined;
|
|
}
|
|
if (!extension.isActive) {
|
|
output('Waiting for Python extension to load', currentCheck);
|
|
yield extension.activate();
|
|
output('Python extension loaded', currentCheck);
|
|
}
|
|
const environmentsAPI = (_a = extension.exports) === null || _a === void 0 ? void 0 : _a.environments;
|
|
if (!environmentsAPI) {
|
|
output('Python extension version is too old (it does not expose the environments API). ' +
|
|
'Please upgrade the Python extension to the latest version.', currentCheck);
|
|
return undefined;
|
|
}
|
|
return extension.exports;
|
|
});
|
|
}
|
|
function warn(warning, show = false, currentCheck, detailsButton = false, helpURL) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
output(warning, currentCheck);
|
|
if (show) {
|
|
const items = [];
|
|
if (detailsButton) {
|
|
items.push("Details");
|
|
}
|
|
if (helpURL) {
|
|
items.push("Help");
|
|
}
|
|
const result = yield vscode.window.showWarningMessage(warning, ...items);
|
|
if (result === "Details") {
|
|
outputChannel.show();
|
|
}
|
|
else if (result === "Help") {
|
|
vscode.env.openExternal(vscode.Uri.parse(helpURL));
|
|
}
|
|
}
|
|
});
|
|
}
|
|
function filesDeleted(e) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
yield filesChanged(e.files);
|
|
});
|
|
}
|
|
function filesRenamed(e) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
const changedUris = e.files.map(f => f.oldUri).concat(...e.files.map(f => f.newUri));
|
|
yield filesChanged(changedUris);
|
|
});
|
|
}
|
|
function filesCreated(e) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
yield filesChanged(e.files, true);
|
|
});
|
|
}
|
|
function filesChanged(files, created = false) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
const folders = new Set();
|
|
for (let file of files) {
|
|
const folder = vscode.workspace.getWorkspaceFolder(file);
|
|
if (folder === undefined)
|
|
continue;
|
|
const path = file.fsPath;
|
|
if (path.endsWith(".py") || path.endsWith(".pyi")) {
|
|
folders.add(folder.uri);
|
|
}
|
|
else if (isMaybeConfigFile(folder, path)) {
|
|
// Don't trigger mypy run if config file has just been created and is empty, because
|
|
// mypy would error. Give the user a chance to edit the file.
|
|
const justCreatedAndEmpty = created && fs.statSync(path).size === 0;
|
|
if (!justCreatedAndEmpty) {
|
|
folders.add(folder.uri);
|
|
}
|
|
}
|
|
}
|
|
if (folders.size === 0) {
|
|
return;
|
|
}
|
|
const foldersString = Array.from(folders).map(f => f.fsPath).join(", ");
|
|
output(`Files changed in folders: ${foldersString}`);
|
|
yield forEachFolder(Array.from(folders), folder => checkWorkspace(folder));
|
|
});
|
|
}
|
|
function output(line, currentCheck) {
|
|
if (currentCheck !== undefined) {
|
|
line = `[${currentCheck}] ${line}`;
|
|
}
|
|
if (logFile) {
|
|
try {
|
|
var tzoffset = (new Date()).getTimezoneOffset() * 60000;
|
|
var localISOTime = (new Date(Date.now() - tzoffset)).toISOString().slice(0, -1);
|
|
fs.appendFileSync(logFile, `${localISOTime} [${process.pid}] ${line}\n`);
|
|
}
|
|
catch (e) {
|
|
// Ignore
|
|
}
|
|
}
|
|
try {
|
|
outputChannel.appendLine(line);
|
|
}
|
|
catch (e) {
|
|
// Ignore error. This can happen when VS Code is closing and it calls our deactivate
|
|
// function, and the output channel is already closed.
|
|
}
|
|
}
|
|
function sleep(ms) {
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|
|
//# sourceMappingURL=extension.js.map
|