diff --git a/.eslintignore b/.eslintignore index dafa523c5629..5bd7c3feb1c2 100644 --- a/.eslintignore +++ b/.eslintignore @@ -63,7 +63,6 @@ src/test/interpreters/display/interpreterSelectionTip.unit.test.ts src/test/interpreters/display.unit.test.ts src/test/configuration/interpreterSelector/interpreterSelector.unit.test.ts -src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts src/test/install/channelManager.channels.test.ts src/test/install/channelManager.messages.test.ts @@ -322,7 +321,6 @@ src/test/workspaceSymbols/generator.unit.test.ts src/client/interpreter/interpreterService.ts src/client/interpreter/configuration/interpreterComparer.ts src/client/interpreter/configuration/interpreterSelector/commands/base.ts -src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts src/client/interpreter/configuration/interpreterSelector/commands/resetInterpreter.ts src/client/interpreter/configuration/interpreterSelector/commands/setShebangInterpreter.ts src/client/interpreter/configuration/interpreterSelector/interpreterSelector.ts @@ -570,7 +568,6 @@ src/client/common/utils/enum.ts src/client/common/utils/async.ts src/client/common/utils/localize.ts src/client/common/utils/platform.ts -src/client/common/utils/multiStepInput.ts src/client/common/utils/stopWatch.ts src/client/common/utils/random.ts src/client/common/utils/serializers.ts diff --git a/src/client/common/utils/multiStepInput.ts b/src/client/common/utils/multiStepInput.ts index 1f14f974b3cf..cbda46e9f2b0 100644 --- a/src/client/common/utils/multiStepInput.ts +++ b/src/client/common/utils/multiStepInput.ts @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +/* eslint-disable max-classes-per-file */ + 'use strict'; import { inject, injectable } from 'inversify'; @@ -12,11 +14,17 @@ import { IApplicationShell } from '../application/types'; export class InputFlowAction { public static back = new InputFlowAction(); + public static cancel = new InputFlowAction(); + public static resume = new InputFlowAction(); - private constructor() {} + + private constructor() { + /** No body. */ + } } +// eslint-disable-next-line @typescript-eslint/no-explicit-any export type InputStep = (input: MultiStepInput, state: T) => Promise | void>; export interface IQuickPickParameters { @@ -31,7 +39,6 @@ export interface IQuickPickParameters { matchOnDescription?: boolean; matchOnDetail?: boolean; acceptFilterBoxTextAsSelection?: boolean; - shouldResume?(): Promise; } export interface InputBoxParameters { @@ -43,11 +50,10 @@ export interface InputBoxParameters { prompt: string; buttons?: QuickInputButton[]; validate(value: string): Promise; - shouldResume?(): Promise; } -type MultiStepInputQuickPicResponseType = T | (P extends { buttons: (infer I)[] } ? I : never); -type MultiStepInputInputBoxResponseType

= string | (P extends { buttons: (infer I)[] } ? I : never); +type MultiStepInputQuickPicResponseType = T | (P extends { buttons: (infer I)[] } ? I : never) | undefined; +type MultiStepInputInputBoxResponseType

= string | (P extends { buttons: (infer I)[] } ? I : never) | undefined; export interface IMultiStepInput { run(start: InputStep, state: S): Promise; showQuickPick>({ @@ -58,7 +64,6 @@ export interface IMultiStepInput { activeItem, placeholder, buttons, - shouldResume, }: P): Promise>; showInputBox

({ title, @@ -68,15 +73,17 @@ export interface IMultiStepInput { prompt, validate, buttons, - shouldResume, }: P): Promise>; } export class MultiStepInput implements IMultiStepInput { private current?: QuickInput; + private steps: InputStep[] = []; + constructor(private readonly shell: IApplicationShell) {} - public run(start: InputStep, state: S) { + + public run(start: InputStep, state: S): Promise { return this.stepThrough(start, state); } @@ -88,7 +95,6 @@ export class MultiStepInput implements IMultiStepInput { activeItem, placeholder, buttons, - shouldResume, matchOnDescription, matchOnDetail, acceptFilterBoxTextAsSelection, @@ -116,24 +122,20 @@ export class MultiStepInput implements IMultiStepInput { if (item === QuickInputButtons.Back) { reject(InputFlowAction.back); } else { - resolve(item); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + resolve(item as any); } }), input.onDidChangeSelection((selectedItems) => resolve(selectedItems[0])), input.onDidHide(() => { - (async () => { - reject( - shouldResume && (await shouldResume()) - ? InputFlowAction.resume - : InputFlowAction.cancel, - ); - })().catch(reject); + resolve(undefined); }), ); if (acceptFilterBoxTextAsSelection) { disposables.push( input.onDidAccept(() => { - resolve(input.value); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + resolve(input.value as any); }), ); } @@ -157,7 +159,6 @@ export class MultiStepInput implements IMultiStepInput { validate, password, buttons, - shouldResume, }: P): Promise> { const disposables: Disposable[] = []; try { @@ -166,7 +167,7 @@ export class MultiStepInput implements IMultiStepInput { input.title = title; input.step = step; input.totalSteps = totalSteps; - input.password = password ? true : false; + input.password = !!password; input.value = value || ''; input.prompt = prompt; input.ignoreFocusOut = true; @@ -177,7 +178,8 @@ export class MultiStepInput implements IMultiStepInput { if (item === QuickInputButtons.Back) { reject(InputFlowAction.back); } else { - resolve(item); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + resolve(item as any); } }), input.onDidAccept(async () => { @@ -199,13 +201,7 @@ export class MultiStepInput implements IMultiStepInput { } }), input.onDidHide(() => { - (async () => { - reject( - shouldResume && (await shouldResume()) - ? InputFlowAction.resume - : InputFlowAction.cancel, - ); - })().catch(reject); + resolve(undefined); }), ); if (this.current) { @@ -254,6 +250,7 @@ export interface IMultiStepInputFactory { @injectable() export class MultiStepInputFactory { constructor(@inject(IApplicationShell) private readonly shell: IApplicationShell) {} + public create(): IMultiStepInput { return new MultiStepInput(this.shell); } diff --git a/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts b/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts index 8387e5230f38..e9301e699de5 100644 --- a/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts +++ b/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts @@ -39,7 +39,7 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand { super(pythonPathUpdaterService, commandManager, applicationShell, workspaceService); } - public async activate() { + public async activate(): Promise { this.disposables.push( this.commandManager.registerCommand(Commands.Set_Interpreter, this.setInterpreter.bind(this)), ); @@ -74,12 +74,15 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand { }); if (selection === undefined) { - return; + sendTelemetryEvent(EventName.SELECT_INTERPRETER_SELECTED, undefined, { action: 'escape' }); } else if (selection.label === enterInterpreterPathSuggestion.label) { return this._enterOrBrowseInterpreterPath(input, state); } else { + sendTelemetryEvent(EventName.SELECT_INTERPRETER_SELECTED, undefined, { action: 'selected' }); state.path = (selection as IInterpreterQuickPickItem).path; } + + return undefined; } @captureTelemetry(EventName.SELECT_INTERPRETER_ENTER_BUTTON) @@ -122,16 +125,18 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand { } @captureTelemetry(EventName.SELECT_INTERPRETER) - public async setInterpreter() { + public async setInterpreter(): Promise { const targetConfig = await this.getConfigTarget(); if (!targetConfig) { return; } - const configTarget = targetConfig.configTarget; + + const { configTarget } = targetConfig; const wkspace = targetConfig.folderUri; const interpreterState: InterpreterStateArgs = { path: undefined, workspace: wkspace }; const multiStep = this.multiStepFactory.create(); await multiStep.run((input, s) => this._pickInterpreter(input, s), interpreterState); + if (interpreterState.path !== undefined) { // User may choose to have an empty string stored, so variable `interpreterState.path` may be // an empty string, in which case we should update. diff --git a/src/client/telemetry/constants.ts b/src/client/telemetry/constants.ts index 752ce9228daf..6c641e1efd7e 100644 --- a/src/client/telemetry/constants.ts +++ b/src/client/telemetry/constants.ts @@ -24,6 +24,7 @@ export enum EventName { SELECT_INTERPRETER = 'SELECT_INTERPRETER', SELECT_INTERPRETER_ENTER_BUTTON = 'SELECT_INTERPRETER_ENTER_BUTTON', SELECT_INTERPRETER_ENTER_CHOICE = 'SELECT_INTERPRETER_ENTER_CHOICE', + SELECT_INTERPRETER_SELECTED = 'SELECT_INTERPRETER_SELECTED', PYTHON_INTERPRETER = 'PYTHON_INTERPRETER', PYTHON_INSTALL_PACKAGE = 'PYTHON_INSTALL_PACKAGE', PYTHON_INTERPRETER_DISCOVERY = 'PYTHON_INTERPRETER_DISCOVERY', diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index a1e649f2f0be..0eac0f22d3de 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -910,6 +910,17 @@ export interface IEventNamePropertyMapping { */ choice: 'enter' | 'browse'; }; + /** + * Telemetry event sent after an action has been taken while the interpreter quickpick was displayed, + * and if the action was not 'Enter interpreter path'. + */ + [EventName.SELECT_INTERPRETER_SELECTED]: { + /** + * 'escape' if the quickpick was dismissed. + * 'selected' if an interpreter was selected. + */ + action: 'escape' | 'selected'; + }; /** * Telemetry event sent with details after updating the python interpreter */ diff --git a/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts b/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts index 33ed5585e8a6..aa6602ab1ac9 100644 --- a/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts +++ b/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts @@ -6,13 +6,13 @@ import { expect } from 'chai'; import * as path from 'path'; import * as sinon from 'sinon'; import * as TypeMoq from 'typemoq'; -import { ConfigurationTarget, QuickPickItem, Uri } from 'vscode'; +import { ConfigurationTarget, OpenDialogOptions, QuickPickItem, Uri } from 'vscode'; import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../../../client/common/application/types'; import { PathUtils } from '../../../../client/common/platform/pathUtils'; import { IPlatformService } from '../../../../client/common/platform/types'; import { IConfigurationService, IPythonSettings } from '../../../../client/common/types'; import { InterpreterQuickPickList, Interpreters } from '../../../../client/common/utils/localize'; -import { IMultiStepInput, IMultiStepInputFactory } from '../../../../client/common/utils/multiStepInput'; +import { IMultiStepInput, IMultiStepInputFactory, InputStep } from '../../../../client/common/utils/multiStepInput'; import { InterpreterStateArgs, SetInterpreterCommand, @@ -22,6 +22,9 @@ import { IInterpreterSelector, IPythonPathUpdaterServiceManager, } from '../../../../client/interpreter/configuration/types'; +import { PythonEnvironment } from '../../../../client/pythonEnvironments/info'; +import { EventName } from '../../../../client/telemetry/constants'; +import * as Telemetry from '../../../../client/telemetry'; suite('Set Interpreter Command', () => { let workspace: TypeMoq.IMock; @@ -69,14 +72,16 @@ suite('Set Interpreter Command', () => { }); suite('Test method _pickInterpreter()', async () => { - let _enterOrBrowseInterpreterPath: sinon.SinonStub; + let _enterOrBrowseInterpreterPath: sinon.SinonStub; + let sendTelemetryStub: sinon.SinonStub; + let telemetryEvent: { eventName: EventName; properties: { userAction: string } } | undefined; + const item: IInterpreterQuickPickItem = { description: '', detail: '', label: '', path: 'This is the selected Python path', - - interpreter: {} as any, + interpreter: {} as PythonEnvironment, }; const expectedEnterInterpreterPathSuggestion = { label: InterpreterQuickPickList.enterPath.label(), @@ -84,12 +89,21 @@ suite('Set Interpreter Command', () => { alwaysShow: true, }; const currentPythonPath = 'python'; + setup(() => { _enterOrBrowseInterpreterPath = sinon.stub( SetInterpreterCommand.prototype, '_enterOrBrowseInterpreterPath', ); _enterOrBrowseInterpreterPath.resolves(); + sendTelemetryStub = sinon + .stub(Telemetry, 'sendTelemetryEvent') + .callsFake((eventName: EventName, _, properties: { userAction: string }) => { + telemetryEvent = { + eventName, + properties, + }; + }); interpreterSelector .setup((i) => i.getSuggestions(TypeMoq.It.isAny())) .returns(() => Promise.resolve([item])); @@ -107,7 +121,9 @@ suite('Set Interpreter Command', () => { ); }); teardown(() => { + telemetryEvent = undefined; sinon.restore(); + Telemetry._resetSharedProperties(); }); test('Existing state path must be removed before displaying picker', async () => { @@ -115,8 +131,7 @@ suite('Set Interpreter Command', () => { const multiStepInput = TypeMoq.Mock.ofType>(); multiStepInput .setup((i) => i.showQuickPick(TypeMoq.It.isAny())) - - .returns(() => Promise.resolve(undefined as any)); + .returns(() => Promise.resolve(undefined as unknown)); await setInterpreterCommand._pickInterpreter(multiStepInput.object, state); @@ -136,8 +151,7 @@ suite('Set Interpreter Command', () => { }; multiStepInput .setup((i) => i.showQuickPick(expectedParameters)) - - .returns(() => Promise.resolve(undefined as any)) + .returns(() => Promise.resolve((undefined as unknown) as QuickPickItem)) .verifiable(TypeMoq.Times.once()); await setInterpreterCommand._pickInterpreter(multiStepInput.object, state); @@ -148,23 +162,47 @@ suite('Set Interpreter Command', () => { test('If an item is selected, update state and return', async () => { const state: InterpreterStateArgs = { path: 'some path', workspace: undefined }; const multiStepInput = TypeMoq.Mock.ofType>(); - multiStepInput - .setup((i) => i.showQuickPick(TypeMoq.It.isAny())) - - .returns(() => Promise.resolve(item as any)); + multiStepInput.setup((i) => i.showQuickPick(TypeMoq.It.isAny())).returns(() => Promise.resolve(item)); await setInterpreterCommand._pickInterpreter(multiStepInput.object, state); expect(state.path).to.equal(item.path, ''); }); + test('If an item is selected, send SELECT_INTERPRETER_SELECTED telemetry with the "selected" property value', async () => { + const state: InterpreterStateArgs = { path: 'some path', workspace: undefined }; + const multiStepInput = TypeMoq.Mock.ofType>(); + multiStepInput.setup((i) => i.showQuickPick(TypeMoq.It.isAny())).returns(() => Promise.resolve(item)); + + await setInterpreterCommand._pickInterpreter(multiStepInput.object, state); + + sinon.assert.calledOnce(sendTelemetryStub); + assert.deepStrictEqual(telemetryEvent, { + eventName: EventName.SELECT_INTERPRETER_SELECTED, + properties: { action: 'selected' }, + }); + }); + + test('If the dropdown is dismissed, send SELECT_INTERPRETER_SELECTED telemetry with the "escape" property value', async () => { + const state: InterpreterStateArgs = { path: 'some path', workspace: undefined }; + const multiStepInput = TypeMoq.Mock.ofType>(); + multiStepInput.setup((i) => i.showQuickPick(TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); + + await setInterpreterCommand._pickInterpreter(multiStepInput.object, state); + + sinon.assert.calledOnce(sendTelemetryStub); + assert.deepStrictEqual(telemetryEvent, { + eventName: EventName.SELECT_INTERPRETER_SELECTED, + properties: { action: 'escape' }, + }); + }); + test('If `Enter or browse...` option is selected, call the corresponding method with correct arguments', async () => { const state: InterpreterStateArgs = { path: 'some path', workspace: undefined }; const multiStepInput = TypeMoq.Mock.ofType>(); multiStepInput .setup((i) => i.showQuickPick(TypeMoq.It.isAny())) - - .returns(() => Promise.resolve(expectedEnterInterpreterPathSuggestion as any)); + .returns(() => Promise.resolve(expectedEnterInterpreterPathSuggestion)); await setInterpreterCommand._pickInterpreter(multiStepInput.object, state); @@ -195,8 +233,7 @@ suite('Set Interpreter Command', () => { const multiStepInput = TypeMoq.Mock.ofType>(); multiStepInput .setup((i) => i.showQuickPick(expectedParameters)) - - .returns(() => Promise.resolve(undefined as any)) + .returns(() => Promise.resolve((undefined as unknown) as QuickPickItem)) .verifiable(TypeMoq.Times.once()); await setInterpreterCommand._enterOrBrowseInterpreterPath(multiStepInput.object, state); @@ -209,8 +246,7 @@ suite('Set Interpreter Command', () => { const multiStepInput = TypeMoq.Mock.ofType>(); multiStepInput .setup((i) => i.showQuickPick(TypeMoq.It.isAny())) - - .returns(() => Promise.resolve('enteredPath' as any)); + .returns(() => Promise.resolve('enteredPath')); await setInterpreterCommand._enterOrBrowseInterpreterPath(multiStepInput.object, state); @@ -221,10 +257,7 @@ suite('Set Interpreter Command', () => { const state: InterpreterStateArgs = { path: undefined, workspace: undefined }; const expectedPathUri = Uri.parse('browsed path'); const multiStepInput = TypeMoq.Mock.ofType>(); - multiStepInput - .setup((i) => i.showQuickPick(TypeMoq.It.isAny())) - - .returns(() => Promise.resolve(items[0] as any)); + multiStepInput.setup((i) => i.showQuickPick(TypeMoq.It.isAny())).returns(() => Promise.resolve(items[0])); appShell .setup((a) => a.showOpenDialog(TypeMoq.It.isAny())) .returns(() => Promise.resolve([expectedPathUri])); @@ -246,11 +279,10 @@ suite('Set Interpreter Command', () => { title: InterpreterQuickPickList.browsePath.title(), }; const multiStepInput = TypeMoq.Mock.ofType>(); - multiStepInput - .setup((i) => i.showQuickPick(TypeMoq.It.isAny())) - - .returns(() => Promise.resolve(items[0] as any)); - appShell.setup((a) => a.showOpenDialog(expectedParams as any)).verifiable(TypeMoq.Times.once()); + multiStepInput.setup((i) => i.showQuickPick(TypeMoq.It.isAny())).returns(() => Promise.resolve(items[0])); + appShell + .setup((a) => a.showOpenDialog(expectedParams as OpenDialogOptions)) + .verifiable(TypeMoq.Times.once()); platformService.setup((p) => p.isWindows).returns(() => true); await setInterpreterCommand._enterOrBrowseInterpreterPath(multiStepInput.object, state); @@ -267,10 +299,7 @@ suite('Set Interpreter Command', () => { canSelectMany: false, title: InterpreterQuickPickList.browsePath.title(), }; - multiStepInput - .setup((i) => i.showQuickPick(TypeMoq.It.isAny())) - - .returns(() => Promise.resolve(items[0] as any)); + multiStepInput.setup((i) => i.showQuickPick(TypeMoq.It.isAny())).returns(() => Promise.resolve(items[0])); appShell.setup((a) => a.showOpenDialog(expectedParams)).verifiable(TypeMoq.Times.once()); platformService.setup((p) => p.isWindows).returns(() => false); @@ -289,22 +318,19 @@ suite('Set Interpreter Command', () => { label: '', path: 'This is the selected Python path', - interpreter: {} as any, + interpreter: {} as PythonEnvironment, }; workspace.setup((w) => w.workspaceFolders).returns(() => undefined); interpreterSelector.setup((i) => i.getSuggestions(TypeMoq.It.isAny())).returns(() => Promise.resolve([])); const multiStepInput = { - run: (_: any, state: InterpreterStateArgs) => { + run: (_: unknown, state: InterpreterStateArgs) => { state.path = selectedItem.path; return Promise.resolve(); }, }; - multiStepInputFactory - .setup((f) => f.create()) - - .returns(() => multiStepInput as any); + multiStepInputFactory.setup((f) => f.create()).returns(() => multiStepInput as IMultiStepInput); pythonPathUpdater .setup((p) => p.updatePythonPath( @@ -331,7 +357,7 @@ suite('Set Interpreter Command', () => { label: '', path: 'This is the selected Python path', - interpreter: {} as any, + interpreter: {} as PythonEnvironment, }; const folder = { name: 'one', uri: Uri.parse('one'), index: 0 }; @@ -340,15 +366,12 @@ suite('Set Interpreter Command', () => { interpreterSelector.setup((i) => i.getSuggestions(TypeMoq.It.isAny())).returns(() => Promise.resolve([])); const multiStepInput = { - run: (_: any, state: InterpreterStateArgs) => { + run: (_: unknown, state: InterpreterStateArgs) => { state.path = selectedItem.path; return Promise.resolve(); }, }; - multiStepInputFactory - .setup((f) => f.create()) - - .returns(() => multiStepInput as any); + multiStepInputFactory.setup((f) => f.create()).returns(() => multiStepInput as IMultiStepInput); pythonPathUpdater .setup((p) => @@ -375,7 +398,7 @@ suite('Set Interpreter Command', () => { label: '', path: 'This is the selected Python path', - interpreter: {} as any, + interpreter: {} as PythonEnvironment, }; workspace.setup((w) => w.workspaceFolders).returns(() => [folder1, folder2]); @@ -399,15 +422,12 @@ suite('Set Interpreter Command', () => { interpreterSelector.setup((i) => i.getSuggestions(TypeMoq.It.isAny())).returns(() => Promise.resolve([])); const multiStepInput = { - run: (_: any, state: InterpreterStateArgs) => { + run: (_: unknown, state: InterpreterStateArgs) => { state.path = selectedItem.path; return Promise.resolve(); }, }; - multiStepInputFactory - .setup((f) => f.create()) - - .returns(() => multiStepInput as any); + multiStepInputFactory.setup((f) => f.create()).returns(() => multiStepInput as IMultiStepInput); appShell .setup((s) => s.showQuickPick(TypeMoq.It.isValue(expectedItems), TypeMoq.It.isAny())) .returns(() => @@ -444,7 +464,7 @@ suite('Set Interpreter Command', () => { label: '', path: 'This is the selected Python path', - interpreter: {} as any, + interpreter: {} as PythonEnvironment, }; workspace.setup((w) => w.workspaceFolders).returns(() => [folder1, folder2]); @@ -469,15 +489,12 @@ suite('Set Interpreter Command', () => { .setup((i) => i.getSuggestions(TypeMoq.It.isAny())) .returns(() => Promise.resolve([selectedItem])); const multiStepInput = { - run: (_: any, state: InterpreterStateArgs) => { + run: (_: unknown, state: InterpreterStateArgs) => { state.path = selectedItem.path; return Promise.resolve(); }, }; - multiStepInputFactory - .setup((f) => f.create()) - - .returns(() => multiStepInput as any); + multiStepInputFactory.setup((f) => f.create()).returns(() => multiStepInput as IMultiStepInput); appShell .setup((s) => s.showQuickPick(TypeMoq.It.isValue(expectedItems), TypeMoq.It.isAny())) .returns(() => @@ -509,10 +526,7 @@ suite('Set Interpreter Command', () => { workspace.setup((w) => w.workspaceFolders).returns(() => [folder1, folder2]); interpreterSelector.setup((i) => i.getSuggestions(TypeMoq.It.isAny())).returns(() => Promise.resolve([])); - multiStepInputFactory - .setup((f) => f.create()) - - .verifiable(TypeMoq.Times.never()); + multiStepInputFactory.setup((f) => f.create()).verifiable(TypeMoq.Times.never()); const expectedItems = [ { @@ -562,7 +576,8 @@ suite('Set Interpreter Command', () => { interpreterSelector.object, workspace.object, ); - let inputStep!: Function; + type InputStepType = () => Promise | void>; + let inputStep!: InputStepType; pythonSettings.setup((p) => p.pythonPath).returns(() => 'python'); const selectedItem: IInterpreterQuickPickItem = { description: '', @@ -570,23 +585,20 @@ suite('Set Interpreter Command', () => { label: '', path: 'This is the selected Python path', - interpreter: {} as any, + interpreter: {} as PythonEnvironment, }; workspace.setup((w) => w.workspaceFolders).returns(() => undefined); interpreterSelector.setup((i) => i.getSuggestions(TypeMoq.It.isAny())).returns(() => Promise.resolve([])); const multiStepInput = { - run: (inputStepArg: any, state: InterpreterStateArgs) => { + run: (inputStepArg: InputStepType, state: InterpreterStateArgs) => { inputStep = inputStepArg; state.path = selectedItem.path; return Promise.resolve(); }, }; - multiStepInputFactory - .setup((f) => f.create()) - - .returns(() => multiStepInput as any); + multiStepInputFactory.setup((f) => f.create()).returns(() => multiStepInput as IMultiStepInput); pythonPathUpdater .setup((p) => p.updatePythonPath(