Skip to content

Commit f214544

Browse files
authored
Remove prompt to install a linter (#15646)
* Remove prompt to install a linter * Forgot to remove the experiment group lol * Skip installing linter in single-workspace tests * Forgot the other test lol
1 parent 488aaa2 commit f214544

8 files changed

Lines changed: 9 additions & 550 deletions

File tree

news/1 Enhancements/15465.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Remove prompt to install a linter when none are available.

src/client/common/experiments/groups.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,6 @@ export enum NativeTensorBoard {
3535
experiment = 'pythonTensorboardExperiment',
3636
}
3737

38-
// Experiment to show a prompt asking users to install or select linter
39-
export enum LinterInstallationPromptVariants {
40-
pylintFirst = 'pythonInstallPylintButtonFirst',
41-
flake8First = 'pythonInstallFlake8ButtonFirst',
42-
noPrompt = 'pythonNotDisplayLinterPrompt',
43-
}
44-
4538
// Experiment to control which environment discovery mechanism can be used
4639
export enum DiscoveryVariants {
4740
discoverWithFileWatching = 'pythonDiscoveryModule',

src/client/common/installer/productInstaller.ts

Lines changed: 3 additions & 249 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,24 @@ import { CancellationToken, OutputChannel, Uri } from 'vscode';
55
import '../../common/extensions';
66
import { IInterpreterService } from '../../interpreter/contracts';
77
import { IServiceContainer } from '../../ioc/types';
8-
import { ILinterManager, LinterId } from '../../linters/types';
98
import { EnvironmentType, PythonEnvironment } from '../../pythonEnvironments/info';
10-
import { sendTelemetryEvent } from '../../telemetry';
11-
import { EventName } from '../../telemetry/constants';
12-
import { IApplicationShell, ICommandManager, IWorkspaceService } from '../application/types';
13-
import { Commands, STANDARD_OUTPUT_CHANNEL } from '../constants';
14-
import { LinterInstallationPromptVariants } from '../experiments/groups';
9+
import { IApplicationShell, IWorkspaceService } from '../application/types';
10+
import { STANDARD_OUTPUT_CHANNEL } from '../constants';
1511
import { traceError, traceInfo } from '../logger';
1612
import { IPlatformService } from '../platform/types';
1713
import { IProcessServiceFactory, IPythonExecutionFactory } from '../process/types';
1814
import { ITerminalServiceFactory } from '../terminal/types';
1915
import {
2016
IConfigurationService,
21-
IExperimentService,
2217
IInstaller,
2318
InstallerResponse,
2419
IOutputChannel,
25-
IPersistentStateFactory,
2620
ProductInstallStatus,
2721
ModuleNamePurpose,
2822
Product,
2923
ProductType,
3024
} from '../types';
31-
import { Common, Installer, Linters } from '../utils/localize';
25+
import { Installer } from '../utils/localize';
3226
import { isResource, noop } from '../utils/misc';
3327
import { ProductNames } from './productNames';
3428
import {
@@ -294,244 +288,6 @@ export class FormatterInstaller extends BaseInstaller {
294288
return InstallerResponse.Ignore;
295289
}
296290
}
297-
298-
export class LinterInstaller extends BaseInstaller {
299-
// This is a hack, really we should be handling this in a service that
300-
// controls the prompts we show. The issue here was that if we show
301-
// a prompt to install pylint and flake8, and user selects flake8
302-
// we immediately show this prompt again saying install flake8, while the
303-
// installation is on going.
304-
private static promptSeen: boolean = false;
305-
private readonly experimentsManager: IExperimentService;
306-
private readonly linterManager: ILinterManager;
307-
308-
constructor(protected serviceContainer: IServiceContainer, protected outputChannel: OutputChannel) {
309-
super(serviceContainer, outputChannel);
310-
this.experimentsManager = serviceContainer.get<IExperimentService>(IExperimentService);
311-
this.linterManager = serviceContainer.get<ILinterManager>(ILinterManager);
312-
}
313-
314-
public static reset() {
315-
// Read notes where this is defined.
316-
LinterInstaller.promptSeen = false;
317-
}
318-
319-
protected async promptToInstallImplementation(
320-
product: Product,
321-
resource?: Uri,
322-
cancel?: CancellationToken,
323-
): Promise<InstallerResponse> {
324-
// This is a hack, really we should be handling this in a service that
325-
// controls the prompts we show. The issue here was that if we show
326-
// a prompt to install pylint and flake8, and user selects flake8
327-
// we immediately show this prompt again saying install flake8, while the
328-
// installation is on going.
329-
if (LinterInstaller.promptSeen) {
330-
return InstallerResponse.Ignore;
331-
}
332-
333-
LinterInstaller.promptSeen = true;
334-
335-
// Conditions to use experiment prompt:
336-
// 1. There should be no linter set in any scope
337-
// 2. The default linter should be pylint
338-
339-
if (!this.isLinterSetInAnyScope() && product === Product.pylint) {
340-
if (await this.experimentsManager.inExperiment(LinterInstallationPromptVariants.noPrompt)) {
341-
// We won't show a prompt, so tell the extension to treat as though user
342-
// ignored the prompt.
343-
sendTelemetryEvent(EventName.LINTER_INSTALL_PROMPT, undefined, {
344-
prompt: 'noPrompt',
345-
});
346-
347-
const productName = ProductNames.get(product)!;
348-
traceInfo(`Linter ${productName} is not installed.`);
349-
350-
return InstallerResponse.Ignore;
351-
} else if (await this.experimentsManager.inExperiment(LinterInstallationPromptVariants.pylintFirst)) {
352-
return this.newPromptForInstallation(true, resource, cancel);
353-
} else if (await this.experimentsManager.inExperiment(LinterInstallationPromptVariants.flake8First)) {
354-
return this.newPromptForInstallation(false, resource, cancel);
355-
}
356-
}
357-
358-
sendTelemetryEvent(EventName.LINTER_INSTALL_PROMPT, undefined, {
359-
prompt: 'old',
360-
});
361-
return this.oldPromptForInstallation(product, resource, cancel);
362-
}
363-
364-
/**
365-
* For installers that want to avoid prompting the user over and over, they can make use of a
366-
* persisted true/false value representing user responses to 'stop showing this prompt'. This method
367-
* gets the persisted value given the installer-defined key.
368-
*
369-
* @param key Key to use to get a persisted response value, each installer must define this for themselves.
370-
* @returns Boolean: The current state of the stored response key given.
371-
*/
372-
protected getStoredResponse(key: string): boolean {
373-
const factory = this.serviceContainer.get<IPersistentStateFactory>(IPersistentStateFactory);
374-
const state = factory.createGlobalPersistentState<boolean | undefined>(key, undefined);
375-
return state.value === true;
376-
}
377-
378-
private async newPromptForInstallation(pylintFirst: boolean, resource?: Uri, cancel?: CancellationToken) {
379-
const productName = ProductNames.get(Product.pylint)!;
380-
381-
// User has already set to ignore this prompt
382-
const disableLinterInstallPromptKey = `${productName}_DisableLinterInstallPrompt`;
383-
if (this.getStoredResponse(disableLinterInstallPromptKey) === true) {
384-
return InstallerResponse.Ignore;
385-
}
386-
387-
// Check if the linter settings has Pylint or flake8 pointing to executables.
388-
// If the settings point to executables then we can't install. Defer to old Prompt.
389-
if (
390-
!this.isExecutableAModule(Product.pylint, resource) ||
391-
!this.isExecutableAModule(Product.flake8, resource)
392-
) {
393-
return this.oldPromptForInstallation(Product.pylint, resource, cancel);
394-
}
395-
396-
const installPylint = Linters.installPylint();
397-
const installFlake8 = Linters.installFlake8();
398-
const doNotShowAgain = Common.doNotShowAgain();
399-
400-
const options = pylintFirst
401-
? [installPylint, installFlake8, doNotShowAgain]
402-
: [installFlake8, installPylint, doNotShowAgain];
403-
const message = Linters.installMessage();
404-
const prompt = pylintFirst ? 'pylintFirst' : 'flake8first';
405-
406-
sendTelemetryEvent(EventName.LINTER_INSTALL_PROMPT, undefined, {
407-
prompt,
408-
});
409-
410-
const response = await this.appShell.showInformationMessage(message, ...options);
411-
412-
if (response === installPylint) {
413-
sendTelemetryEvent(EventName.LINTER_INSTALL_PROMPT_ACTION, undefined, {
414-
prompt,
415-
action: 'installPylint',
416-
});
417-
return this.install(Product.pylint, resource, cancel);
418-
} else if (response === installFlake8) {
419-
sendTelemetryEvent(EventName.LINTER_INSTALL_PROMPT_ACTION, undefined, {
420-
prompt,
421-
action: 'installFlake8',
422-
});
423-
await this.linterManager.setActiveLintersAsync([Product.flake8], resource);
424-
return this.install(Product.flake8, resource, cancel);
425-
} else if (response === doNotShowAgain) {
426-
sendTelemetryEvent(EventName.LINTER_INSTALL_PROMPT_ACTION, undefined, {
427-
prompt,
428-
action: 'disablePrompt',
429-
});
430-
await this.setStoredResponse(disableLinterInstallPromptKey, true);
431-
return InstallerResponse.Ignore;
432-
}
433-
434-
sendTelemetryEvent(EventName.LINTER_INSTALL_PROMPT_ACTION, undefined, {
435-
prompt,
436-
action: 'close',
437-
});
438-
return InstallerResponse.Ignore;
439-
}
440-
441-
private async oldPromptForInstallation(product: Product, resource?: Uri, cancel?: CancellationToken) {
442-
const isPylint = product === Product.pylint;
443-
444-
const productName = ProductNames.get(product)!;
445-
const install = Common.install();
446-
const doNotShowAgain = Common.doNotShowAgain();
447-
const disableLinterInstallPromptKey = `${productName}_DisableLinterInstallPrompt`;
448-
const selectLinter = Linters.selectLinter();
449-
450-
if (isPylint && this.getStoredResponse(disableLinterInstallPromptKey) === true) {
451-
return InstallerResponse.Ignore;
452-
}
453-
454-
const options = isPylint ? [selectLinter, doNotShowAgain] : [selectLinter];
455-
456-
let message = `Linter ${productName} is not installed.`;
457-
if (this.isExecutableAModule(product, resource)) {
458-
options.splice(0, 0, install);
459-
} else {
460-
const executable = this.getExecutableNameFromSettings(product, resource);
461-
message = `Path to the ${productName} linter is invalid (${executable})`;
462-
}
463-
const response = await this.appShell.showErrorMessage(message, ...options);
464-
if (response === install) {
465-
sendTelemetryEvent(EventName.LINTER_NOT_INSTALLED_PROMPT, undefined, {
466-
tool: productName as LinterId,
467-
action: 'install',
468-
});
469-
return this.install(product, resource, cancel);
470-
} else if (response === doNotShowAgain) {
471-
await this.setStoredResponse(disableLinterInstallPromptKey, true);
472-
sendTelemetryEvent(EventName.LINTER_NOT_INSTALLED_PROMPT, undefined, {
473-
tool: productName as LinterId,
474-
action: 'disablePrompt',
475-
});
476-
return InstallerResponse.Ignore;
477-
}
478-
479-
if (response === selectLinter) {
480-
sendTelemetryEvent(EventName.LINTER_NOT_INSTALLED_PROMPT, undefined, { action: 'select' });
481-
const commandManager = this.serviceContainer.get<ICommandManager>(ICommandManager);
482-
await commandManager.executeCommand(Commands.Set_Linter);
483-
}
484-
return InstallerResponse.Ignore;
485-
}
486-
487-
private isLinterSetInAnyScope() {
488-
const config = this.workspaceService.getConfiguration('python');
489-
if (config) {
490-
const keys = [
491-
'linting.pylintEnabled',
492-
'linting.flake8Enabled',
493-
'linting.banditEnabled',
494-
'linting.mypyEnabled',
495-
'linting.pycodestyleEnabled',
496-
'linting.prospectorEnabled',
497-
'linting.pydocstyleEnabled',
498-
'linting.pylamaEnabled',
499-
];
500-
501-
const values = keys.map((key) => {
502-
const value = config.inspect<boolean>(key);
503-
if (value) {
504-
if (value.globalValue || value.workspaceValue || value.workspaceFolderValue) {
505-
return 'linter set';
506-
}
507-
}
508-
return 'no info';
509-
});
510-
511-
return values.includes('linter set');
512-
}
513-
514-
return false;
515-
}
516-
517-
/**
518-
* For installers that want to avoid prompting the user over and over, they can make use of a
519-
* persisted true/false value representing user responses to 'stop showing this prompt'. This
520-
* method will set that persisted value given the installer-defined key.
521-
*
522-
* @param key Key to use to get a persisted response value, each installer must define this for themselves.
523-
* @param value Boolean value to store for the user - if they choose to not be prompted again for instance.
524-
* @returns Boolean: The current state of the stored response key given.
525-
*/
526-
private async setStoredResponse(key: string, value: boolean): Promise<void> {
527-
const factory = this.serviceContainer.get<IPersistentStateFactory>(IPersistentStateFactory);
528-
const state = factory.createGlobalPersistentState<boolean | undefined>(key, undefined);
529-
if (state && state.value !== value) {
530-
await state.updateValue(value);
531-
}
532-
}
533-
}
534-
535291
export class TestFrameworkInstaller extends BaseInstaller {
536292
protected async promptToInstallImplementation(
537293
product: Product,
@@ -695,8 +451,6 @@ export class ProductInstaller implements IInstaller {
695451
switch (productType) {
696452
case ProductType.Formatter:
697453
return new FormatterInstaller(this.serviceContainer, this.outputChannel);
698-
case ProductType.Linter:
699-
return new LinterInstaller(this.serviceContainer, this.outputChannel);
700454
case ProductType.WorkspaceSymbols:
701455
return new CTagsInstaller(this.serviceContainer, this.outputChannel);
702456
case ProductType.TestFramework:

src/client/telemetry/constants.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,6 @@ export enum EventName {
113113

114114
SELECT_LINTER = 'LINTING.SELECT',
115115

116-
LINTER_NOT_INSTALLED_PROMPT = 'LINTER_NOT_INSTALLED_PROMPT',
117-
LINTER_INSTALL_PROMPT = 'LINTER_INSTALL_PROMPT',
118-
LINTER_INSTALL_PROMPT_ACTION = 'LINTER_INSTALL_PROMPT_ACTION',
119116
CONFIGURE_AVAILABLE_LINTER_PROMPT = 'CONFIGURE_AVAILABLE_LINTER_PROMPT',
120117
HASHED_PACKAGE_NAME = 'HASHED_PACKAGE_NAME',
121118
HASHED_PACKAGE_PERF = 'HASHED_PACKAGE_PERF',

src/client/telemetry/index.ts

Lines changed: 0 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -796,59 +796,7 @@ export interface IEventNamePropertyMapping {
796796
hashedName: string;
797797
};
798798
[EventName.HASHED_PACKAGE_PERF]: never | undefined;
799-
/**
800-
* Telemetry event sent with details of selection in prompt
801-
* `Prompt message` :- 'Linter ${productName} is not installed'
802-
*/
803-
[EventName.LINTER_NOT_INSTALLED_PROMPT]: {
804-
/**
805-
* Name of the linter
806-
*
807-
* @type {LinterId}
808-
*/
809-
tool?: LinterId;
810-
/**
811-
* `select` When 'Select linter' option is selected
812-
* `disablePrompt` When 'Do not show again' option is selected
813-
* `install` When 'Install' option is selected
814-
*
815-
* @type {('select' | 'disablePrompt' | 'install')}
816-
*/
817-
action: 'select' | 'disablePrompt' | 'install';
818-
};
819-
820-
/**
821-
* Telemetry event sent before showing the linter prompt to install
822-
* pylint or flake8.
823-
*/
824-
[EventName.LINTER_INSTALL_PROMPT]: {
825-
/**
826-
* Identify which prompt was shown.
827-
*
828-
* @type {('old' | 'noPrompt' | 'pylintFirst' | 'flake8first')}
829-
*/
830-
prompt: 'old' | 'noPrompt' | 'pylintFirst' | 'flake8first';
831-
};
832-
833-
/**
834-
* Telemetry event sent after user had selected one of the options
835-
* provided by the linter prompt.
836-
*/
837-
[EventName.LINTER_INSTALL_PROMPT_ACTION]: {
838-
/**
839-
* Identify which prompt was shown.
840-
*
841-
* @type {('pylintFirst' | 'flake8first')}
842-
*/
843-
prompt: 'pylintFirst' | 'flake8first';
844799

845-
/**
846-
* Which of the following actions did user select
847-
*
848-
* @type {('pylint' | 'flake8' | 'disablePrompt' | 'close')}
849-
*/
850-
action: 'installPylint' | 'installFlake8' | 'disablePrompt' | 'close';
851-
};
852800
/**
853801
* Telemetry event sent when installing modules
854802
*/

src/test/common/installer.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -328,9 +328,11 @@ suite('Installer', () => {
328328
}
329329
getNamesAndValues<Product>(Product).forEach((prod) => {
330330
test(`Ensure isInstalled for Product: '${prod.name}' executes the right command`, async function () {
331-
if (new ProductService().getProductType(prod.value) === ProductType.DataScience) {
331+
const productType = new ProductService().getProductType(prod.value);
332+
if (productType === ProductType.DataScience || productType === ProductType.Linter) {
332333
return this.skip();
333334
}
335+
334336
ioc.serviceManager.addSingletonInstance<IModuleInstaller>(
335337
IModuleInstaller,
336338
new MockModuleInstaller('one', false),
@@ -365,7 +367,7 @@ suite('Installer', () => {
365367
getNamesAndValues<Product>(Product).forEach((prod) => {
366368
test(`Ensure install for Product: '${prod.name}' executes the right command in IModuleInstaller`, async function () {
367369
const productType = new ProductService().getProductType(prod.value);
368-
if (productType === ProductType.DataScience) {
370+
if (productType === ProductType.DataScience || productType === ProductType.Linter) {
369371
return this.skip();
370372
}
371373
ioc.serviceManager.addSingletonInstance<IModuleInstaller>(

0 commit comments

Comments
 (0)