Skip to content

Commit 724bcce

Browse files
authored
Pylint not enabled by default, can be easily enabled for workspaces that have it available. (#2913)
Prepare Pylint to no longer be enabled by default. Fix for #974 - Pylint is easily enabled for workspaces that have it available - Relocate logic that checks for and enables available linter.
1 parent cae8a13 commit 724bcce

18 files changed

+834
-80
lines changed

news/1 Enhancements/974.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Pylint is no longer enabled by default. Users that have not configured pylint but who have installed it in their workspace will be asked if they'd like to enable it.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1690,4 +1690,4 @@
16901690
"publisherDisplayName": "Microsoft",
16911691
"publisherId": "998b010b-e2af-44a5-a6cd-0b5fd3b9b6f8"
16921692
}
1693-
}
1693+
}

src/client/linters/baseLinter.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,6 @@ export abstract class BaseLinter implements ILinter {
8686
return this._info;
8787
}
8888

89-
public isLinterExecutableSpecified(resource: vscode.Uri) {
90-
const executablePath = this.info.pathName(resource);
91-
return path.basename(executablePath).length > 0 && path.basename(executablePath) !== executablePath;
92-
}
9389
public async lint(document: vscode.TextDocument, cancellation: vscode.CancellationToken): Promise<ILintMessage[]> {
9490
this._pythonSettings = this.configService.getSettings(document.uri);
9591
return this.runLinter(document, cancellation);
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { inject, injectable } from 'inversify';
7+
import { Uri } from 'vscode';
8+
import { IApplicationShell, IWorkspaceService } from '../common/application/types';
9+
import { traceError } from '../common/logger';
10+
import { IConfigurationService, IInstaller, Product } from '../common/types';
11+
import { IAvailableLinterActivator, ILinterInfo } from './types';
12+
13+
@injectable()
14+
export class AvailableLinterActivator implements IAvailableLinterActivator {
15+
constructor(
16+
@inject(IApplicationShell) private appShell: IApplicationShell,
17+
@inject(IInstaller) private installer: IInstaller,
18+
@inject(IWorkspaceService) private workspaceConfig: IWorkspaceService,
19+
@inject(IConfigurationService) private configService: IConfigurationService
20+
) { }
21+
22+
/**
23+
* Check if it is possible to enable an otherwise-unconfigured linter in
24+
* the current workspace, and if so ask the user if they want that linter
25+
* configured explicitly.
26+
*
27+
* @param linterInfo The linter to check installation status.
28+
* @param resource Context for the operation (required when in multi-root workspaces).
29+
*
30+
* @returns true if configuration was updated in any way, false otherwise.
31+
*/
32+
public async promptIfLinterAvailable(linterInfo: ILinterInfo, resource?: Uri): Promise<boolean> {
33+
// Has the feature been enabled yet?
34+
if (!this.isFeatureEnabled) {
35+
return false;
36+
}
37+
38+
// Has the linter in question has been configured explicitly? If so, no need to continue.
39+
if (!this.isLinterUsingDefaultConfiguration(linterInfo, resource)) {
40+
return false;
41+
}
42+
43+
// Is the linter available in the current workspace?
44+
if (await this.isLinterAvailable(linterInfo.product, resource)) {
45+
46+
// great, it is - ask the user if they'd like to enable it.
47+
return this.promptToConfigureAvailableLinter(linterInfo);
48+
}
49+
return false;
50+
}
51+
52+
/**
53+
* Raise a dialog asking the user if they would like to explicitly configure a
54+
* linter or not in their current workspace.
55+
*
56+
* @param linterInfo The linter to ask the user to enable or not.
57+
*
58+
* @returns true if the user requested a configuration change, false otherwise.
59+
*/
60+
public async promptToConfigureAvailableLinter(linterInfo: ILinterInfo): Promise<boolean> {
61+
type ConfigureLinterMessage = {
62+
enabled: boolean;
63+
title: string;
64+
};
65+
66+
const optButtons: ConfigureLinterMessage[] = [
67+
{
68+
title: `Enable ${linterInfo.id}`,
69+
enabled: true
70+
},
71+
{
72+
title: `Disable ${linterInfo.id}`,
73+
enabled: false
74+
}
75+
];
76+
const pick = await this.appShell.showInformationMessage(`Linter ${linterInfo.id} is available but not enabled.`, ...optButtons);
77+
if (pick) {
78+
await linterInfo.enableAsync(pick.enabled);
79+
return true;
80+
}
81+
82+
return false;
83+
}
84+
85+
/**
86+
* Check if the linter itself is available in the workspace's Python environment or
87+
* not.
88+
*
89+
* @param linterProduct Linter to check in the current workspace environment.
90+
* @param resource Context information for workspace.
91+
*/
92+
public async isLinterAvailable(linterProduct: Product, resource?: Uri): Promise<boolean | undefined> {
93+
return this.installer.isInstalled(linterProduct, resource)
94+
.catch((reason) => {
95+
// report and continue, assume the linter is unavailable.
96+
traceError(`[WARNING]: Failed to discover if linter ${linterProduct} is installed.`, reason);
97+
return false;
98+
});
99+
}
100+
101+
/**
102+
* Check if the given linter has been configured by the user in this workspace or not.
103+
*
104+
* @param linterInfo Linter to check for configuration status.
105+
* @param resource Context information.
106+
*
107+
* @returns true if the linter has not been configured at the user, workspace, or workspace-folder scope. false otherwise.
108+
*/
109+
public isLinterUsingDefaultConfiguration(linterInfo: ILinterInfo, resource?: Uri): boolean {
110+
const ws = this.workspaceConfig.getConfiguration('python.linting', resource);
111+
const pe = ws!.inspect(linterInfo.enabledSettingName);
112+
return (pe!.globalValue === undefined && pe!.workspaceValue === undefined && pe!.workspaceFolderValue === undefined);
113+
}
114+
115+
/**
116+
* Check if this feature is enabled yet.
117+
*
118+
* This is a feature of the vscode-python extension that will become enabled once the
119+
* Python Language Server becomes the default, replacing Jedi as the default. Testing
120+
* the global default setting for `"python.jediEnabled": false` enables it.
121+
*
122+
* @returns true if the global default for python.jediEnabled is false.
123+
*/
124+
public get isFeatureEnabled(): boolean {
125+
return !this.configService.getSettings().jediEnabled;
126+
}
127+
}

src/client/linters/linterCommands.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export class LinterCommands implements vscode.Disposable {
2828
public async setLinterAsync(): Promise<void> {
2929
const linters = this.linterManager.getAllLinterInfos();
3030
const suggestions = linters.map(x => x.id).sort();
31-
const activeLinters = this.linterManager.getActiveLinters(this.settingsUri);
31+
const activeLinters = await this.linterManager.getActiveLinters(true, this.settingsUri);
3232

3333
let current: string;
3434
switch (activeLinters.length) {
@@ -64,7 +64,7 @@ export class LinterCommands implements vscode.Disposable {
6464

6565
public async enableLintingAsync(): Promise<void> {
6666
const options = ['on', 'off'];
67-
const current = this.linterManager.isLintingEnabled(this.settingsUri) ? options[0] : options[1];
67+
const current = await this.linterManager.isLintingEnabled(true, this.settingsUri) ? options[0] : options[1];
6868

6969
const quickPickOptions: vscode.QuickPickOptions = {
7070
matchOnDetail: true,

src/client/linters/linterManager.ts

Lines changed: 61 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4+
'use strict';
5+
46
import { inject, injectable } from 'inversify';
5-
import { CancellationToken, OutputChannel, TextDocument, Uri } from 'vscode';
6-
import { IConfigurationService, ILogger, Product } from '../common/types';
7+
import {
8+
CancellationToken, OutputChannel, TextDocument, Uri
9+
} from 'vscode';
10+
import {
11+
IConfigurationService, ILogger, Product
12+
} from '../common/types';
713
import { IServiceContainer } from '../ioc/types';
814
import { Bandit } from './bandit';
915
import { Flake8 } from './flake8';
@@ -14,7 +20,9 @@ import { Prospector } from './prospector';
1420
import { PyDocStyle } from './pydocstyle';
1521
import { PyLama } from './pylama';
1622
import { Pylint } from './pylint';
17-
import { ILinter, ILinterInfo, ILinterManager, ILintMessage } from './types';
23+
import {
24+
IAvailableLinterActivator, ILinter, ILinterInfo, ILinterManager, ILintMessage
25+
} from './types';
1826

1927
class DisabledLinter implements ILinter {
2028
constructor(private configService: IConfigurationService) { }
@@ -31,8 +39,9 @@ export class LinterManager implements ILinterManager {
3139
private lintingEnabledSettingName = 'enabled';
3240
private linters: ILinterInfo[];
3341
private configService: IConfigurationService;
42+
private checkedForInstalledLinters: boolean = false;
3443

35-
constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) {
44+
constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) {
3645
this.configService = serviceContainer.get<IConfigurationService>(IConfigurationService);
3746
this.linters = [
3847
new LinterInfo(Product.bandit, 'bandit', this.configService),
@@ -58,40 +67,49 @@ export class LinterManager implements ILinterManager {
5867
throw new Error('Invalid linter');
5968
}
6069

61-
public isLintingEnabled(resource?: Uri): boolean {
70+
public async isLintingEnabled(silent: boolean, resource?: Uri): Promise<boolean> {
6271
const settings = this.configService.getSettings(resource);
63-
return (settings.linting[this.lintingEnabledSettingName] as boolean) && this.getActiveLinters(resource).length > 0;
72+
const activeLintersPresent = await this.getActiveLinters(silent, resource);
73+
return (settings.linting[this.lintingEnabledSettingName] as boolean) && activeLintersPresent.length > 0;
6474
}
6575

6676
public async enableLintingAsync(enable: boolean, resource?: Uri): Promise<void> {
6777
await this.configService.updateSetting(`linting.${this.lintingEnabledSettingName}`, enable, resource);
68-
69-
// If nothing is enabled, fix it up to PyLint (default).
70-
if (enable && this.getActiveLinters(resource).length === 0) {
71-
await this.setActiveLintersAsync([Product.pylint], resource);
72-
}
7378
}
7479

75-
public getActiveLinters(resource?: Uri): ILinterInfo[] {
80+
public async getActiveLinters(silent: boolean, resource?: Uri): Promise<ILinterInfo[]> {
81+
if (!silent) {
82+
await this.enableUnconfiguredLinters(resource);
83+
}
7684
return this.linters.filter(x => x.isEnabled(resource));
7785
}
7886

7987
public async setActiveLintersAsync(products: Product[], resource?: Uri): Promise<void> {
80-
const active = this.getActiveLinters(resource);
81-
for (const x of active) {
82-
await x.enableAsync(false, resource);
83-
}
84-
if (products.length > 0) {
85-
const toActivate = this.linters.filter(x => products.findIndex(p => x.product === p) >= 0);
86-
for (const x of toActivate) {
87-
await x.enableAsync(true, resource);
88+
// ensure we only allow valid linters to be set, otherwise leave things alone.
89+
// filter out any invalid products:
90+
const validProducts = products.filter(product => {
91+
const foundIndex = this.linters.findIndex(validLinter => validLinter.product === product);
92+
return foundIndex !== -1;
93+
});
94+
95+
// if we have valid linter product(s), enable only those
96+
if (validProducts.length > 0) {
97+
const active = await this.getActiveLinters(true, resource);
98+
for (const x of active) {
99+
await x.enableAsync(false, resource);
100+
}
101+
if (products.length > 0) {
102+
const toActivate = this.linters.filter(x => products.findIndex(p => x.product === p) >= 0);
103+
for (const x of toActivate) {
104+
await x.enableAsync(true, resource);
105+
}
106+
await this.enableLintingAsync(true, resource);
88107
}
89-
await this.enableLintingAsync(true, resource);
90108
}
91109
}
92110

93-
public createLinter(product: Product, outputChannel: OutputChannel, serviceContainer: IServiceContainer, resource?: Uri): ILinter {
94-
if (!this.isLintingEnabled(resource)) {
111+
public async createLinter(product: Product, outputChannel: OutputChannel, serviceContainer: IServiceContainer, resource?: Uri): Promise<ILinter> {
112+
if (!await this.isLintingEnabled(true, resource)) {
95113
return new DisabledLinter(this.configService);
96114
}
97115
const error = 'Linter manager: Unknown linter';
@@ -118,4 +136,24 @@ export class LinterManager implements ILinterManager {
118136
}
119137
throw new Error(error);
120138
}
139+
140+
protected async enableUnconfiguredLinters(resource?: Uri): Promise<boolean> {
141+
// if we've already checked during this session, don't bother again
142+
if (this.checkedForInstalledLinters) {
143+
return false;
144+
}
145+
this.checkedForInstalledLinters = true;
146+
147+
// only check & ask the user if they'd like to enable pylint
148+
const pylintInfo = this.linters.find(
149+
(linter: ILinterInfo) => linter.id === 'pylint'
150+
);
151+
152+
// If linting is disabled, don't bother checking further.
153+
if (pylintInfo && await this.isLintingEnabled(true, resource)) {
154+
const activator = this.serviceContainer.get<IAvailableLinterActivator>(IAvailableLinterActivator);
155+
return activator.promptIfLinterAvailable(pylintInfo, resource);
156+
}
157+
return false;
158+
}
121159
}

src/client/linters/lintingEngine.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4+
'use strict';
5+
46
import { inject, injectable } from 'inversify';
57
import { Minimatch } from 'minimatch';
68
import * as path from 'path';
@@ -93,10 +95,16 @@ export class LintingEngine implements ILintingEngine {
9395

9496
this.pendingLintings.set(document.uri.fsPath, cancelToken);
9597

96-
const promises: Promise<ILintMessage[]>[] = this.linterManager.getActiveLinters(document.uri)
97-
.map(info => {
98+
const activeLinters = await this.linterManager.getActiveLinters(false, document.uri);
99+
const promises: Promise<ILintMessage[]>[] = activeLinters
100+
.map(async (info: ILinterInfo) => {
98101
const stopWatch = new StopWatch();
99-
const linter = this.linterManager.createLinter(info.product, this.outputChannel, this.serviceContainer, document.uri);
102+
const linter = await this.linterManager.createLinter(
103+
info.product,
104+
this.outputChannel,
105+
this.serviceContainer,
106+
document.uri
107+
);
100108
const promise = linter.lint(document, cancelToken.token);
101109
this.sendLinterRunTelemetry(info, document.uri, promise, stopWatch, trigger);
102110
return promise;
@@ -175,7 +183,7 @@ export class LintingEngine implements ILintingEngine {
175183
}
176184

177185
private async shouldLintDocument(document: vscode.TextDocument): Promise<boolean> {
178-
if (!this.linterManager.isLintingEnabled(document.uri)) {
186+
if (!await this.linterManager.isLintingEnabled(false, document.uri)) {
179187
this.diagnosticCollection.set(document.uri, []);
180188
return false;
181189
}

src/client/linters/serviceRegistry.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@
22
// Licensed under the MIT License.
33

44
import { IServiceManager } from '../ioc/types';
5+
import { AvailableLinterActivator } from './linterAvailability';
56
import { LinterManager } from './linterManager';
67
import { LintingEngine } from './lintingEngine';
7-
import { ILinterManager, ILintingEngine } from './types';
8+
import {
9+
IAvailableLinterActivator, ILinterManager, ILintingEngine
10+
} from './types';
811

912
export function registerTypes(serviceManager: IServiceManager) {
1013
serviceManager.addSingleton<ILintingEngine>(ILintingEngine, LintingEngine);
1114
serviceManager.addSingleton<ILinterManager>(ILinterManager, LinterManager);
15+
serviceManager.add<IAvailableLinterActivator>(IAvailableLinterActivator, AvailableLinterActivator);
1216
}

src/client/linters/types.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export interface ILinterInfo {
1919
readonly argsSettingName: string;
2020
readonly enabledSettingName: string;
2121
readonly configFileNames: string[];
22-
enableAsync(flag: boolean, resource?: vscode.Uri): Promise<void>;
22+
enableAsync(enabled: boolean, resource?: vscode.Uri): Promise<void>;
2323
isEnabled(resource?: vscode.Uri): boolean;
2424
pathName(resource?: vscode.Uri): string;
2525
linterArgs(resource?: vscode.Uri): string[];
@@ -31,15 +31,20 @@ export interface ILinter {
3131
lint(document: vscode.TextDocument, cancellation: vscode.CancellationToken): Promise<ILintMessage[]>;
3232
}
3333

34+
export const IAvailableLinterActivator = Symbol('IAvailableLinterActivator');
35+
export interface IAvailableLinterActivator {
36+
promptIfLinterAvailable(linter: ILinterInfo, resource?: vscode.Uri): Promise<boolean>;
37+
}
38+
3439
export const ILinterManager = Symbol('ILinterManager');
3540
export interface ILinterManager {
3641
getAllLinterInfos(): ILinterInfo[];
3742
getLinterInfo(product: Product): ILinterInfo;
38-
getActiveLinters(resource?: vscode.Uri): ILinterInfo[];
39-
isLintingEnabled(resource?: vscode.Uri): boolean;
43+
getActiveLinters(silent: boolean, resource?: vscode.Uri): Promise<ILinterInfo[]>;
44+
isLintingEnabled(silent: boolean, resource?: vscode.Uri): Promise<boolean>;
4045
enableLintingAsync(enable: boolean, resource?: vscode.Uri): Promise<void>;
4146
setActiveLintersAsync(products: Product[], resource?: vscode.Uri): Promise<void>;
42-
createLinter(product: Product, outputChannel: vscode.OutputChannel, serviceContainer: IServiceContainer, resource?: vscode.Uri): ILinter;
47+
createLinter(product: Product, outputChannel: vscode.OutputChannel, serviceContainer: IServiceContainer, resource?: vscode.Uri): Promise<ILinter>;
4348
}
4449

4550
export interface ILintMessage {

0 commit comments

Comments
 (0)