diff --git a/package.json b/package.json index 8840c2573fa8..3569d86d89f8 100644 --- a/package.json +++ b/package.json @@ -231,6 +231,11 @@ "command": "python.enableLinting", "title": "%python.command.python.enableLinting.title%", "category": "Python" + }, + { + "command": "python.runLinting", + "title": "%python.command.python.runLinting.title%", + "category": "Python" } ], "menus": { @@ -1831,4 +1836,4 @@ "publisherDisplayName": "Microsoft", "publisherId": "998b010b-e2af-44a5-a6cd-0b5fd3b9b6f8" } -} +} \ No newline at end of file diff --git a/package.nls.json b/package.nls.json index dd6cedc42d68..b4f891c49a70 100644 --- a/package.nls.json +++ b/package.nls.json @@ -26,28 +26,29 @@ "python.command.python.goToPythonObject.title": "Go to Python Object", "python.command.python.setLinter.title": "Select Linter", "python.command.python.enableLinting.title": "Enable Linting", - "python.snippet.launch.standard.label": "Python", - "python.snippet.launch.standard.description": "Debug a Python program with standard output", - "python.snippet.launch.pyspark.label": "Python: PySpark", - "python.snippet.launch.pyspark.description": "Debug PySpark", - "python.snippet.launch.module.label": "Python: Module", - "python.snippet.launch.module.description": "Debug a Python Module", - "python.snippet.launch.terminal.label": "Python: Terminal (integrated)", - "python.snippet.launch.terminal.description": "Debug a Python program with Integrated Terminal/Console", - "python.snippet.launch.externalTerminal.label": "Python: Terminal (external)", - "python.snippet.launch.externalTerminal.description": "Debug a Python program with External Terminal/Console", - "python.snippet.launch.django.label": "Python: Django", - "python.snippet.launch.django.description": "Debug a Django Application", - "python.snippet.launch.flask.label": "Python: Flask (0.11.x or later)", - "python.snippet.launch.flask.description": "Debug a Flask Application", - "python.snippet.launch.flaskOld.label": "Python: Flask (0.10.x or earlier)", - "python.snippet.launch.flaskOld.description": "Debug an older styled Flask Application", - "python.snippet.launch.pyramid.label": "Python: Pyramid Application", - "python.snippet.launch.pyramid.description": "Debug a Pyramid Application", - "python.snippet.launch.watson.label": "Python: Watson Application", - "python.snippet.launch.watson.description": "Debug a Watson Application", - "python.snippet.launch.attach.label": "Python: Attach", - "python.snippet.launch.attach.description": "Attach the debugger for remote debugging", - "python.snippet.launch.scrapy.label": "Python: Scrapy", - "python.snippet.launch.scrapy.description": "Scrapy with Integrated Terminal/Console" -} + "python.command.python.runLinting.title": "Run Linting", + "python.snippet.launch.standard.label": "Python", + "python.snippet.launch.standard.description": "Debug a Python program with standard output", + "python.snippet.launch.pyspark.label": "Python: PySpark", + "python.snippet.launch.pyspark.description": "Debug PySpark", + "python.snippet.launch.module.label": "Python: Module", + "python.snippet.launch.module.description": "Debug a Python Module", + "python.snippet.launch.terminal.label": "Python: Terminal (integrated)", + "python.snippet.launch.terminal.description": "Debug a Python program with Integrated Terminal/Console", + "python.snippet.launch.externalTerminal.label": "Python: Terminal (external)", + "python.snippet.launch.externalTerminal.description": "Debug a Python program with External Terminal/Console", + "python.snippet.launch.django.label": "Python: Django", + "python.snippet.launch.django.description": "Debug a Django Application", + "python.snippet.launch.flask.label": "Python: Flask (0.11.x or later)", + "python.snippet.launch.flask.description": "Debug a Flask Application", + "python.snippet.launch.flaskOld.label": "Python: Flask (0.10.x or earlier)", + "python.snippet.launch.flaskOld.description": "Debug an older styled Flask Application", + "python.snippet.launch.pyramid.label": "Python: Pyramid Application", + "python.snippet.launch.pyramid.description": "Debug a Pyramid Application", + "python.snippet.launch.watson.label": "Python: Watson Application", + "python.snippet.launch.watson.description": "Debug a Watson Application", + "python.snippet.launch.attach.label": "Python: Attach", + "python.snippet.launch.attach.description": "Attach the debugger for remote debugging", + "python.snippet.launch.scrapy.label": "Python: Scrapy", + "python.snippet.launch.scrapy.description": "Scrapy with Integrated Terminal/Console" +} \ No newline at end of file diff --git a/package.nls.ru.json b/package.nls.ru.json index fccc142b15ae..457729ce463f 100644 --- a/package.nls.ru.json +++ b/package.nls.ru.json @@ -25,28 +25,29 @@ "python.command.python.goToPythonObject.title": "Перейти к объекту Python", "python.command.python.setLinter.title": "Выбрать анализатор кода", "python.command.python.enableLinting.title": "Включить анализатор кода", - "python.snippet.launch.standard.label": "Python", - "python.snippet.launch.standard.description": "Отладить программу Python со стандартным выводом", - "python.snippet.launch.pyspark.label": "Python: PySpark", - "python.snippet.launch.pyspark.description": "Отладка PySpark", - "python.snippet.launch.module.label": "Python: Модуль", - "python.snippet.launch.module.description": "Отладка модуля", - "python.snippet.launch.terminal.label": "Python: Интегрированная консоль", - "python.snippet.launch.terminal.description": "Отладка программы Python в интегрированной консоли", - "python.snippet.launch.externalTerminal.label": "Python: Внешний терминал", - "python.snippet.launch.externalTerminal.description": "Отладка программы Python во внешней консоли", - "python.snippet.launch.django.label": "Python: Django", - "python.snippet.launch.django.description": "Отладка приложения Django", - "python.snippet.launch.flask.label": "Python: Flask (0.11.x или новее)", - "python.snippet.launch.flask.description": "Отладка приложения Flask", - "python.snippet.launch.flaskOld.label": "Python: Flask (0.10.x или старее)", - "python.snippet.launch.flaskOld.description": "Отладка приложения Flask (старый стиль)", - "python.snippet.launch.pyramid.label": "Python: Приложение Pyramid", - "python.snippet.launch.pyramid.description": "Отладка приложения Pyramid", - "python.snippet.launch.watson.label": "Python: Приложение Watson", - "python.snippet.launch.watson.description": "Отладка приложения Watson", - "python.snippet.launch.attach.label": "Python: Подключить отладчик", - "python.snippet.launch.attach.description": "Подключить отладчик для удаленной отладки", - "python.snippet.launch.scrapy.label": "Python: Scrapy", - "python.snippet.launch.scrapy.description": "Scrapy в интегрированной консоли" -} + "python.command.python.runLinting.title": "Выполнить анализ кода", + "python.snippet.launch.standard.label": "Python", + "python.snippet.launch.standard.description": "Отладить программу Python со стандартным выводом", + "python.snippet.launch.pyspark.label": "Python: PySpark", + "python.snippet.launch.pyspark.description": "Отладка PySpark", + "python.snippet.launch.module.label": "Python: Модуль", + "python.snippet.launch.module.description": "Отладка модуля", + "python.snippet.launch.terminal.label": "Python: Интегрированная консоль", + "python.snippet.launch.terminal.description": "Отладка программы Python в интегрированной консоли", + "python.snippet.launch.externalTerminal.label": "Python: Внешний терминал", + "python.snippet.launch.externalTerminal.description": "Отладка программы Python во внешней консоли", + "python.snippet.launch.django.label": "Python: Django", + "python.snippet.launch.django.description": "Отладка приложения Django", + "python.snippet.launch.flask.label": "Python: Flask (0.11.x или новее)", + "python.snippet.launch.flask.description": "Отладка приложения Flask", + "python.snippet.launch.flaskOld.label": "Python: Flask (0.10.x или старее)", + "python.snippet.launch.flaskOld.description": "Отладка приложения Flask (старый стиль)", + "python.snippet.launch.pyramid.label": "Python: Приложение Pyramid", + "python.snippet.launch.pyramid.description": "Отладка приложения Pyramid", + "python.snippet.launch.watson.label": "Python: Приложение Watson", + "python.snippet.launch.watson.description": "Отладка приложения Watson", + "python.snippet.launch.attach.label": "Python: Подключить отладчик", + "python.snippet.launch.attach.description": "Подключить отладчик для удаленной отладки", + "python.snippet.launch.scrapy.label": "Python: Scrapy", + "python.snippet.launch.scrapy.description": "Scrapy в интегрированной консоли" +} \ No newline at end of file diff --git a/src/client/common/application/documentManager.ts b/src/client/common/application/documentManager.ts index 05b064ae1ccd..afd4577417eb 100644 --- a/src/client/common/application/documentManager.ts +++ b/src/client/common/application/documentManager.ts @@ -9,6 +9,9 @@ import { IDocumentManager } from './types'; @injectable() export class DocumentManager implements IDocumentManager { + public get textDocuments(): TextDocument[] { + return workspace.textDocuments; + } public get activeTextEditor(): TextEditor | undefined { return window.activeTextEditor; } @@ -30,6 +33,12 @@ export class DocumentManager implements IDocumentManager { public get onDidChangeTextEditorViewColumn(): Event { return window.onDidChangeTextEditorViewColumn; } + public get onDidOpenTextDocument(): Event { + return workspace.onDidOpenTextDocument; + } + public get onDidCloseTextDocument(): Event { + return workspace.onDidCloseTextDocument; + } public get onDidSaveTextDocument(): Event { return workspace.onDidSaveTextDocument; } diff --git a/src/client/common/application/types.ts b/src/client/common/application/types.ts index 9f012d1c3384..90da5554de81 100644 --- a/src/client/common/application/types.ts +++ b/src/client/common/application/types.ts @@ -312,6 +312,12 @@ export interface ICommandManager { export const IDocumentManager = Symbol('IDocumentManager'); export interface IDocumentManager { + /** + * All text documents currently known to the system. + * + * @readonly + */ + readonly textDocuments: TextDocument[]; /** * The currently active editor or `undefined`. The active editor is the one * that currently has focus or, when none has focus, the one that has changed @@ -352,6 +358,14 @@ export interface IDocumentManager { */ readonly onDidChangeTextEditorViewColumn: Event; + /** + * An event that is emitted when a [text document](#TextDocument) is opened. + */ + readonly onDidOpenTextDocument: Event; + /** + * An event that is emitted when a [text document](#TextDocument) is disposed. + */ + readonly onDidCloseTextDocument: Event; /** * An event that is emitted when a [text document](#TextDocument) is saved to disk. */ diff --git a/src/client/common/application/workspace.ts b/src/client/common/application/workspace.ts index 54f2c7dd2fa1..1d56349dc9ff 100644 --- a/src/client/common/application/workspace.ts +++ b/src/client/common/application/workspace.ts @@ -35,4 +35,7 @@ export class WorkspaceService implements IWorkspaceService { public findFiles(include: vscode.GlobPattern, exclude?: vscode.GlobPattern, maxResults?: number, token?: vscode.CancellationToken): Thenable { return vscode.workspace.findFiles(include, exclude, maxResults, token); } + public get onDidSaveTextDocument(): vscode.Event { + return vscode.workspace.onDidSaveTextDocument; + } } diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts index 8b30c2b5d3ed..cfbdd9584ac0 100644 --- a/src/client/common/constants.ts +++ b/src/client/common/constants.ts @@ -31,6 +31,7 @@ export namespace Commands { export const Create_Terminal = 'python.createTerminal'; export const Set_Linter = 'python.setLinter'; export const Enable_Linter = 'python.enableLinting'; + export const Run_Linter = 'python.runLinting'; } export namespace Octicons { export const Test_Pass = '$(check)'; diff --git a/src/client/extension.ts b/src/client/extension.ts index a8d15d4bbb7d..21657960b7ca 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -31,10 +31,10 @@ import { registerTypes as interpretersRegisterTypes } from './interpreter/servic import { ServiceContainer } from './ioc/container'; import { ServiceManager } from './ioc/serviceManager'; import { IServiceContainer } from './ioc/types'; -import { JupyterProvider } from './jupyter/provider'; import { JediFactory } from './languageServices/jediProxyFactory'; import { LinterCommands } from './linters/linterCommands'; import { registerTypes as lintersRegisterTypes } from './linters/serviceRegistry'; +import { ILintingEngine } from './linters/types'; import { PythonCompletionItemProvider } from './providers/completionProvider'; import { PythonDefinitionProvider } from './providers/definitionProvider'; import { PythonFormattingEditProvider } from './providers/formatProvider'; @@ -165,29 +165,16 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(vscode.languages.registerDocumentRangeFormattingEditProvider(PYTHON, formatProvider)); } - // tslint:disable-next-line:promise-function-async - const linterProvider = new LinterProvider(context, standardOutputChannel, (a, b) => Promise.resolve(false), serviceContainer); + const linterProvider = new LinterProvider(context, serviceContainer); context.subscriptions.push(linterProvider); - const jupyterExtInstalled = vscode.extensions.getExtension('donjayamanne.jupyter'); - if (jupyterExtInstalled) { - if (jupyterExtInstalled.isActive) { - // tslint:disable-next-line:no-unsafe-any - jupyterExtInstalled.exports.registerLanguageProvider(PYTHON.language, new JupyterProvider()); - // tslint:disable-next-line:no-unsafe-any - linterProvider.documentHasJupyterCodeCells = jupyterExtInstalled.exports.hasCodeCells; - } - - jupyterExtInstalled.activate().then(() => { - // tslint:disable-next-line:no-unsafe-any - jupyterExtInstalled.exports.registerLanguageProvider(PYTHON.language, new JupyterProvider()); - // tslint:disable-next-line:no-unsafe-any - linterProvider.documentHasJupyterCodeCells = jupyterExtInstalled.exports.hasCodeCells; - }); - } + + const jupyterExtension = vscode.extensions.getExtension('donjayamanne.jupyter'); + const lintingEngine = serviceContainer.get(ILintingEngine); + lintingEngine.linkJupiterExtension(jupyterExtension).ignoreErrors(); + tests.activate(context, unitTestOutChannel, symbolProvider, serviceContainer); context.subscriptions.push(new WorkspaceSymbols(serviceContainer)); - context.subscriptions.push(vscode.languages.registerOnTypeFormattingEditProvider(PYTHON, new BlockFormatProviders(), ':')); context.subscriptions.push(vscode.languages.registerOnTypeFormattingEditProvider(PYTHON, new OnEnterFormatter(), '\n')); @@ -199,9 +186,9 @@ export async function activate(context: vscode.ExtensionContext) { // tslint:disable-next-line:no-unused-expression new BannerService(persistentStateFactory); - const deprecationMgr = new FeatureDeprecationManager(persistentStateFactory, !!jupyterExtInstalled); + const deprecationMgr = new FeatureDeprecationManager(persistentStateFactory, !!jupyterExtension); deprecationMgr.initialize(); - context.subscriptions.push(new FeatureDeprecationManager(persistentStateFactory, !!jupyterExtInstalled)); + context.subscriptions.push(new FeatureDeprecationManager(persistentStateFactory, !!jupyterExtension)); } async function sendStartupTelemetry(activatedPromise: Promise, serviceContainer: IServiceContainer) { diff --git a/src/client/interpreter/contracts.ts b/src/client/interpreter/contracts.ts index 919ea8c46ab6..727415e00b0e 100644 --- a/src/client/interpreter/contracts.ts +++ b/src/client/interpreter/contracts.ts @@ -1,4 +1,4 @@ -import { CodeLensProvider, ConfigurationTarget, Disposable, TextDocument, Uri } from 'vscode'; +import { CodeLensProvider, ConfigurationTarget, Disposable, Event, TextDocument, Uri } from 'vscode'; import { Architecture } from '../common/platform/types'; export const INTERPRETER_LOCATOR_SERVICE = 'IInterpreterLocatorService'; @@ -75,8 +75,8 @@ export type WorkspacePythonPath = { }; export const IInterpreterService = Symbol('IInterpreterService'); - export interface IInterpreterService { + onDidChangeInterpreter: Event; getInterpreters(resource?: Uri): Promise; autoSetInterpreter(): Promise; getActiveInterpreter(resource?: Uri): Promise; diff --git a/src/client/interpreter/display/index.ts b/src/client/interpreter/display/index.ts index e728e60e78ef..6ddaf6edc13c 100644 --- a/src/client/interpreter/display/index.ts +++ b/src/client/interpreter/display/index.ts @@ -20,7 +20,7 @@ export class InterpreterDisplay implements IInterpreterDisplay { private readonly configurationService: IConfigurationService; private readonly helper: IInterpreterHelper; private readonly workspaceService: IWorkspaceService; - private currentWorkspaceInterpreter?: Uri; + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { this.interpreterService = serviceContainer.get(IInterpreterService); this.virtualEnvMgr = serviceContainer.get(IVirtualEnvironmentManager); @@ -48,14 +48,7 @@ export class InterpreterDisplay implements IInterpreterDisplay { } await this.updateDisplay(resource); } - private shouldRefresh(workspaceFolder?: Uri) { - if (!workspaceFolder || !this.currentWorkspaceInterpreter) { - return true; - } - return !this.fileSystem.arePathsSame(workspaceFolder.fsPath, this.currentWorkspaceInterpreter.fsPath); - } private async updateDisplay(workspaceFolder?: Uri) { - this.currentWorkspaceInterpreter = workspaceFolder; const interpreters = await this.interpreterService.getInterpreters(workspaceFolder); const interpreter = await this.interpreterService.getActiveInterpreter(workspaceFolder); const pythonPath = interpreter ? interpreter.path : this.configurationService.getSettings(workspaceFolder).pythonPath; diff --git a/src/client/interpreter/index.ts b/src/client/interpreter/index.ts index f6d048622f3c..3eddee641c28 100644 --- a/src/client/interpreter/index.ts +++ b/src/client/interpreter/index.ts @@ -1,6 +1,6 @@ import { inject, injectable } from 'inversify'; import * as path from 'path'; -import { ConfigurationTarget, Disposable, Uri } from 'vscode'; +import { ConfigurationTarget, Disposable, Event, EventEmitter, Uri } from 'vscode'; import { IDocumentManager, IWorkspaceService } from '../common/application/types'; import { PythonSettings } from '../common/configSettings'; import { IPythonExecutionFactory } from '../common/process/types'; @@ -8,7 +8,11 @@ import { IConfigurationService, IDisposableRegistry } from '../common/types'; import * as utils from '../common/utils'; import { IServiceContainer } from '../ioc/types'; import { IPythonPathUpdaterServiceManager } from './configuration/types'; -import { IInterpreterDisplay, IInterpreterHelper, IInterpreterLocatorService, IInterpreterService, IInterpreterVersionService, INTERPRETER_LOCATOR_SERVICE, InterpreterType, PythonInterpreter, WORKSPACE_VIRTUAL_ENV_SERVICE } from './contracts'; +import { + IInterpreterDisplay, IInterpreterHelper, IInterpreterLocatorService, + IInterpreterService, IInterpreterVersionService, INTERPRETER_LOCATOR_SERVICE, + InterpreterType, PythonInterpreter, WORKSPACE_VIRTUAL_ENV_SERVICE +} from './contracts'; import { IVirtualEnvironmentManager } from './virtualEnvs/types'; @injectable() @@ -16,16 +20,20 @@ export class InterpreterManager implements Disposable, IInterpreterService { private readonly interpreterProvider: IInterpreterLocatorService; private readonly pythonPathUpdaterService: IPythonPathUpdaterServiceManager; private readonly helper: IInterpreterHelper; + private readonly didChangeInterpreterEmitter = new EventEmitter(); + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { this.interpreterProvider = serviceContainer.get(IInterpreterLocatorService, INTERPRETER_LOCATOR_SERVICE); this.helper = serviceContainer.get(IInterpreterHelper); this.pythonPathUpdaterService = this.serviceContainer.get(IPythonPathUpdaterServiceManager); } + public async refresh(resource?: Uri) { const interpreterDisplay = this.serviceContainer.get(IInterpreterDisplay); return interpreterDisplay.refresh(resource); } + public initialize() { const disposables = this.serviceContainer.get(IDisposableRegistry); const documentManager = this.serviceContainer.get(IDocumentManager); @@ -33,9 +41,11 @@ export class InterpreterManager implements Disposable, IInterpreterService { const configService = this.serviceContainer.get(IConfigurationService); (configService.getSettings() as PythonSettings).addListener('change', this.onConfigChanged); } + public getInterpreters(resource?: Uri) { return this.interpreterProvider.getInterpreters(resource); } + public async autoSetInterpreter() { if (!this.shouldAutoSetInterpreter()) { return; @@ -63,10 +73,16 @@ export class InterpreterManager implements Disposable, IInterpreterService { await this.pythonPathUpdaterService.updatePythonPath(pythonPath, activeWorkspace.configTarget, 'load', activeWorkspace.folderUri); } } + public dispose(): void { this.interpreterProvider.dispose(); const configService = this.serviceContainer.get(IConfigurationService); (configService.getSettings() as PythonSettings).removeListener('change', this.onConfigChanged); + this.didChangeInterpreterEmitter.dispose(); + } + + public get onDidChangeInterpreter(): Event { + return this.didChangeInterpreterEmitter.event; } public async getActiveInterpreter(resource?: Uri): Promise { @@ -117,6 +133,7 @@ export class InterpreterManager implements Disposable, IInterpreterService { return false; } private onConfigChanged = () => { + this.didChangeInterpreterEmitter.fire(); const interpreterDisplay = this.serviceContainer.get(IInterpreterDisplay); interpreterDisplay.refresh() .catch(ex => console.error('Python Extension: display.refresh', ex)); diff --git a/src/client/linters/linterCommands.ts b/src/client/linters/linterCommands.ts index 613ea5a42c54..5ecde143c1cf 100644 --- a/src/client/linters/linterCommands.ts +++ b/src/client/linters/linterCommands.ts @@ -5,7 +5,7 @@ import * as vscode from 'vscode'; import { IApplicationShell, ICommandManager } from '../common/application/types'; import { Commands } from '../common/constants'; import { IServiceContainer } from '../ioc/types'; -import { ILinterManager } from './types'; +import { ILinterManager, ILintingEngine } from './types'; export class LinterCommands implements vscode.Disposable { private disposables: vscode.Disposable[] = []; @@ -19,6 +19,7 @@ export class LinterCommands implements vscode.Disposable { const commandManager = this.serviceContainer.get(ICommandManager); commandManager.registerCommand(Commands.Set_Linter, this.setLinterAsync.bind(this)); commandManager.registerCommand(Commands.Enable_Linter, this.enableLintingAsync.bind(this)); + commandManager.registerCommand(Commands.Run_Linter, this.runLinting.bind(this)); } public dispose() { this.disposables.forEach(disposable => disposable.dispose()); @@ -78,6 +79,11 @@ export class LinterCommands implements vscode.Disposable { } } + public runLinting(): void { + const engine = this.serviceContainer.get(ILintingEngine); + engine.lintOpenPythonFiles(); + } + private get settingsUri(): vscode.Uri | undefined { return vscode.window.activeTextEditor ? vscode.window.activeTextEditor.document.uri : undefined; } diff --git a/src/client/linters/linterInfo.ts b/src/client/linters/linterInfo.ts index f6af6a7cc0eb..10f18affe708 100644 --- a/src/client/linters/linterInfo.ts +++ b/src/client/linters/linterInfo.ts @@ -9,10 +9,12 @@ import { ILinterInfo, LinterId } from './types'; export class LinterInfo implements ILinterInfo { private _id: LinterId; private _product: Product; + private _configFileNames: string[]; - constructor(product: Product, id: LinterId, private configService: IConfigurationService) { + constructor(product: Product, id: LinterId, private configService: IConfigurationService, configFileNames: string[] = []) { this._product = product; this._id = id; + this._configFileNames = configFileNames; } public get id(): LinterId { @@ -31,6 +33,9 @@ export class LinterInfo implements ILinterInfo { public get enabledSettingName(): string { return `${this.id}Enabled`; } + public get configFileNames(): string[] { + return this._configFileNames; + } public async enableAsync(enabled: boolean, resource?: Uri): Promise { return this.configService.updateSettingAsync(`linting.${this.enabledSettingName}`, enabled, resource); diff --git a/src/client/linters/linterManager.ts b/src/client/linters/linterManager.ts index 2a809762ecd3..9b8a9a13d430 100644 --- a/src/client/linters/linterManager.ts +++ b/src/client/linters/linterManager.ts @@ -32,11 +32,11 @@ export class LinterManager implements ILinterManager { private configService: IConfigurationService; private disabledForCurrentSession = false; - constructor( @inject(IServiceContainer) serviceContainer: IServiceContainer) { + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { this.configService = serviceContainer.get(IConfigurationService); this.linters = [ new LinterInfo(Product.flake8, 'flake8', this.configService), - new LinterInfo(Product.pylint, 'pylint', this.configService), + new LinterInfo(Product.pylint, 'pylint', this.configService, ['.pylintrc', 'pylintrc']), new LinterInfo(Product.mypy, 'mypy', this.configService), new LinterInfo(Product.pep8, 'pep8', this.configService), new LinterInfo(Product.prospector, 'prospector', this.configService), diff --git a/src/client/linters/lintingEngine.ts b/src/client/linters/lintingEngine.ts new file mode 100644 index 000000000000..5c971483bc3a --- /dev/null +++ b/src/client/linters/lintingEngine.ts @@ -0,0 +1,178 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { Minimatch } from 'minimatch'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { IDocumentManager, IWorkspaceService } from '../common/application/types'; +import { LinterErrors, PythonLanguage, STANDARD_OUTPUT_CHANNEL } from '../common/constants'; +import { IConfigurationService, IOutputChannel } from '../common/types'; +import { IServiceContainer } from '../ioc/types'; +import { JupyterProvider } from '../jupyter/provider'; +import { sendTelemetryWhenDone } from '../telemetry'; +import { LINTING } from '../telemetry/constants'; +import { StopWatch } from '../telemetry/stopWatch'; +import { LinterTrigger, LintingTelemetry } from '../telemetry/types'; +import { ILinterInfo, ILinterManager, ILintingEngine, ILintMessage, LintMessageSeverity } from './types'; + +const PYTHON: vscode.DocumentFilter = { language: 'python' }; + +const lintSeverityToVSSeverity = new Map(); +lintSeverityToVSSeverity.set(LintMessageSeverity.Error, vscode.DiagnosticSeverity.Error); +lintSeverityToVSSeverity.set(LintMessageSeverity.Hint, vscode.DiagnosticSeverity.Hint); +lintSeverityToVSSeverity.set(LintMessageSeverity.Information, vscode.DiagnosticSeverity.Information); +lintSeverityToVSSeverity.set(LintMessageSeverity.Warning, vscode.DiagnosticSeverity.Warning); + +// tslint:disable-next-line:interface-name +interface DocumentHasJupyterCodeCells { + // tslint:disable-next-line:callable-types + (doc: vscode.TextDocument, token: vscode.CancellationToken): Promise; +} + +@injectable() +export class LintingEngine implements ILintingEngine { + private documentHasJupyterCodeCells: DocumentHasJupyterCodeCells; + private workspace: IWorkspaceService; + private documents: IDocumentManager; + private configurationService: IConfigurationService; + private linterManager: ILinterManager; + private diagnosticCollection: vscode.DiagnosticCollection; + private pendingLintings = new Map(); + private outputChannel: vscode.OutputChannel; + + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { + this.documentHasJupyterCodeCells = (a, b) => Promise.resolve(false); + this.documents = serviceContainer.get(IDocumentManager); + this.workspace = serviceContainer.get(IWorkspaceService); + this.configurationService = serviceContainer.get(IConfigurationService); + this.outputChannel = serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); + this.linterManager = serviceContainer.get(ILinterManager); + this.diagnosticCollection = vscode.languages.createDiagnosticCollection('python'); + } + + public lintOpenPythonFiles(): void { + this.documents.textDocuments.forEach(async document => { + if (document.languageId === PythonLanguage.language) { + await this.lintDocument(document, 'auto'); + } + }); + } + + public async lintDocument(document: vscode.TextDocument, trigger: LinterTrigger): Promise { + // Check if we need to lint this document + const workspaceFolder = this.workspace.getWorkspaceFolder(document.uri); + const workspaceRootPath = (workspaceFolder && typeof workspaceFolder.uri.fsPath === 'string') ? workspaceFolder.uri.fsPath : undefined; + const relativeFileName = typeof workspaceRootPath === 'string' ? path.relative(workspaceRootPath, document.fileName) : document.fileName; + const settings = this.configurationService.getSettings(document.uri); + if (document.languageId !== PythonLanguage.language) { + return; + } + if (!this.linterManager.isLintingEnabled()) { + this.diagnosticCollection.set(document.uri, []); + } + const ignoreMinmatches = settings.linting.ignorePatterns.map(pattern => { + return new Minimatch(pattern); + }); + + if (ignoreMinmatches.some(matcher => matcher.match(document.fileName) || matcher.match(relativeFileName))) { + return; + } + + if (this.pendingLintings.has(document.uri.fsPath)) { + this.pendingLintings.get(document.uri.fsPath)!.cancel(); + this.pendingLintings.delete(document.uri.fsPath); + } + + const cancelToken = new vscode.CancellationTokenSource(); + cancelToken.token.onCancellationRequested(() => { + if (this.pendingLintings.has(document.uri.fsPath)) { + this.pendingLintings.delete(document.uri.fsPath); + } + }); + + this.pendingLintings.set(document.uri.fsPath, cancelToken); + this.outputChannel.clear(); + + const promises: Promise[] = this.linterManager.getActiveLinters(document.uri) + .map(info => { + const stopWatch = new StopWatch(); + const linter = this.linterManager.createLinter(info.product, this.outputChannel, this.serviceContainer); + const promise = linter.lint(document, cancelToken.token); + this.sendLinterRunTelemetry(info, document.uri, promise, stopWatch, trigger); + return promise; + }); + + const hasJupyterCodeCells = await this.documentHasJupyterCodeCells(document, cancelToken.token); + // linters will resolve asynchronously - keep a track of all + // diagnostics reported as them come in. + let diagnostics: vscode.Diagnostic[] = []; + + for (const p of promises) { + const msgs = await p; + if (cancelToken.token.isCancellationRequested) { + break; + } + + diagnostics = []; + if (this.isDocumentOpen(document.uri)) { + // Build the message and suffix the message with the name of the linter used. + for (const m of msgs) { + // Ignore magic commands from jupyter. + if (hasJupyterCodeCells && document.lineAt(m.line - 1).text.trim().startsWith('%') && + (m.code === LinterErrors.pylint.InvalidSyntax || + m.code === LinterErrors.prospector.InvalidSyntax || + m.code === LinterErrors.flake8.InvalidSyntax)) { + return; + } + diagnostics.push(this.createDiagnostics(m, document)); + } + + // Limit the number of messages to the max value. + diagnostics = diagnostics.filter((value, index) => index <= settings.linting.maxNumberOfProblems); + } + // Set all diagnostics found in this pass, as this method always clears existing diagnostics. + this.diagnosticCollection.set(document.uri, diagnostics); + } + } + + // tslint:disable-next-line:no-any + public async linkJupiterExtension(jupiter: vscode.Extension | undefined): Promise { + if (!jupiter) { + return; + } + if (!jupiter.isActive) { + await jupiter.activate(); + } + // tslint:disable-next-line:no-unsafe-any + jupiter.exports.registerLanguageProvider(PYTHON.language, new JupyterProvider()); + // tslint:disable-next-line:no-unsafe-any + this.documentHasJupyterCodeCells = jupiter.exports.hasCodeCells; + } + + private sendLinterRunTelemetry(info: ILinterInfo, resource: vscode.Uri, promise: Promise, stopWatch: StopWatch, trigger: LinterTrigger): void { + const linterExecutablePathName = info.pathName(resource); + const properties: LintingTelemetry = { + tool: info.id, + hasCustomArgs: info.linterArgs(resource).length > 0, + trigger, + executableSpecified: linterExecutablePathName.length > 0 + }; + sendTelemetryWhenDone(LINTING, promise, stopWatch, properties); + } + + private isDocumentOpen(uri: vscode.Uri): boolean { + return this.documents.textDocuments.some(document => document.uri.fsPath === uri.fsPath); + } + + private createDiagnostics(message: ILintMessage, document: vscode.TextDocument): vscode.Diagnostic { + const position = new vscode.Position(message.line - 1, message.column); + const range = new vscode.Range(position, position); + + const severity = lintSeverityToVSSeverity.get(message.severity!)!; + const diagnostic = new vscode.Diagnostic(range, `${message.code}:${message.message}`, severity); + diagnostic.code = message.code; + diagnostic.source = message.provider; + return diagnostic; + } +} diff --git a/src/client/linters/serviceRegistry.ts b/src/client/linters/serviceRegistry.ts index 963b1b06d76a..89f7abb8e11f 100644 --- a/src/client/linters/serviceRegistry.ts +++ b/src/client/linters/serviceRegistry.ts @@ -3,8 +3,10 @@ import { IServiceManager } from '../ioc/types'; import { LinterManager } from './linterManager'; -import { ILinterManager } from './types'; +import { LintingEngine } from './lintingEngine'; +import { ILinterManager, ILintingEngine } from './types'; export function registerTypes(serviceManager: IServiceManager) { + serviceManager.addSingleton(ILintingEngine, LintingEngine); serviceManager.addSingleton(ILinterManager, LinterManager); } diff --git a/src/client/linters/types.ts b/src/client/linters/types.ts index 2c9ef1e8562f..21a63419ab39 100644 --- a/src/client/linters/types.ts +++ b/src/client/linters/types.ts @@ -4,6 +4,7 @@ import * as vscode from 'vscode'; import { ExecutionInfo, Product } from '../common/types'; import { IServiceContainer } from '../ioc/types'; +import { LinterTrigger } from '../telemetry/types'; export interface IErrorHandler { handleError(error: Error, resource: vscode.Uri, execInfo: ExecutionInfo): Promise; @@ -17,6 +18,7 @@ export interface ILinterInfo { readonly pathSettingName: string; readonly argsSettingName: string; readonly enabledSettingName: string; + readonly configFileNames: string[]; enableAsync(flag: boolean, resource?: vscode.Uri): Promise; isEnabled(resource?: vscode.Uri): boolean; pathName(resource?: vscode.Uri): string; @@ -56,3 +58,11 @@ export enum LintMessageSeverity { Warning, Information } + +export const ILintingEngine = Symbol('ILintingEngine'); +export interface ILintingEngine { + lintOpenPythonFiles(): void; + lintDocument(document: vscode.TextDocument, trigger: LinterTrigger): Promise; + // tslint:disable-next-line:no-any + linkJupiterExtension(jupiter: vscode.Extension | undefined): Promise; +} diff --git a/src/client/providers/linterProvider.ts b/src/client/providers/linterProvider.ts index 522f383cb5d5..fd44b9b457c8 100644 --- a/src/client/providers/linterProvider.ts +++ b/src/client/providers/linterProvider.ts @@ -1,238 +1,118 @@ -import * as fs from 'fs'; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + import * as path from 'path'; import * as vscode from 'vscode'; import { ConfigurationTarget, Uri, workspace } from 'vscode'; +import { IDocumentManager } from '../common/application/types'; import { ConfigSettingMonitor } from '../common/configSettingMonitor'; -import { PythonSettings } from '../common/configSettings'; -import { LinterErrors, PythonLanguage } from '../common/constants'; +import { IFileSystem } from '../common/platform/types'; +import { IConfigurationService } from '../common/types'; +import { IInterpreterService } from '../interpreter/contracts'; import { IServiceContainer } from '../ioc/types'; -import { ILinterInfo, ILinterManager, ILintMessage, LintMessageSeverity } from '../linters/types'; -import { sendTelemetryWhenDone } from '../telemetry'; -import { LINTING } from '../telemetry/constants'; -import { StopWatch } from '../telemetry/stopWatch'; -import { LinterTrigger, LintingTelemetry } from '../telemetry/types'; - -// tslint:disable-next-line:no-require-imports no-var-requires -const Minimatch = require('minimatch').Minimatch; +import { ILinterManager, ILintingEngine } from '../linters/types'; const uriSchemesToIgnore = ['git', 'showModifications', 'svn']; -const lintSeverityToVSSeverity = new Map(); -lintSeverityToVSSeverity.set(LintMessageSeverity.Error, vscode.DiagnosticSeverity.Error); -lintSeverityToVSSeverity.set(LintMessageSeverity.Hint, vscode.DiagnosticSeverity.Hint); -lintSeverityToVSSeverity.set(LintMessageSeverity.Information, vscode.DiagnosticSeverity.Information); -lintSeverityToVSSeverity.set(LintMessageSeverity.Warning, vscode.DiagnosticSeverity.Warning); - -function createDiagnostics(message: ILintMessage, document: vscode.TextDocument): vscode.Diagnostic { - const position = new vscode.Position(message.line - 1, message.column); - const range = new vscode.Range(position, position); - - const severity = lintSeverityToVSSeverity.get(message.severity!)!; - const diagnostic = new vscode.Diagnostic(range, `${message.code}:${message.message}`, severity); - diagnostic.code = message.code; - diagnostic.source = message.provider; - return diagnostic; -} -// tslint:disable-next-line:interface-name -interface DocumentHasJupyterCodeCells { - // tslint:disable-next-line:callable-types - (doc: vscode.TextDocument, token: vscode.CancellationToken): Promise; -} export class LinterProvider implements vscode.Disposable { - private linterManager: ILinterManager; private diagnosticCollection: vscode.DiagnosticCollection; - private pendingLintings = new Map(); - private outputChannel: vscode.OutputChannel; private context: vscode.ExtensionContext; private disposables: vscode.Disposable[]; private configMonitor: ConfigSettingMonitor; - public constructor( - context: vscode.ExtensionContext, - outputChannel: vscode.OutputChannel, - public documentHasJupyterCodeCells: DocumentHasJupyterCodeCells, - private serviceContainer: IServiceContainer) { + private interpreterService: IInterpreterService; + private documents: IDocumentManager; + private configuration: IConfigurationService; + private linterManager: ILinterManager; + private engine: ILintingEngine; + private fs: IFileSystem; - this.linterManager = serviceContainer.get(ILinterManager); - this.outputChannel = outputChannel; + public constructor(context: vscode.ExtensionContext, serviceContainer: IServiceContainer) { this.context = context; this.disposables = []; - this.initialize(); + + this.fs = serviceContainer.get(IFileSystem); + this.engine = serviceContainer.get(ILintingEngine); + this.linterManager = serviceContainer.get(ILinterManager); + this.interpreterService = serviceContainer.get(IInterpreterService); + this.documents = serviceContainer.get(IDocumentManager); + this.configuration = serviceContainer.get(IConfigurationService); + + this.diagnosticCollection = vscode.languages.createDiagnosticCollection('python'); + this.disposables.push(this.interpreterService.onDidChangeInterpreter(() => this.engine.lintOpenPythonFiles())); + + this.documents.onDidOpenTextDocument(e => this.onDocumentOpened(e), this.context.subscriptions); + this.documents.onDidCloseTextDocument(e => this.onDocumentClosed(e), this.context.subscriptions); + this.documents.onDidSaveTextDocument((e) => this.onDocumentSaved(e), this.context.subscriptions); + this.configMonitor = new ConfigSettingMonitor('linting'); this.configMonitor.on('change', this.lintSettingsChangedHandler.bind(this)); } + + public get diagnostics(): vscode.DiagnosticCollection { + return this.diagnosticCollection; + } + public dispose() { this.disposables.forEach(d => d.dispose()); this.configMonitor.dispose(); } - private isDocumentOpen(uri: vscode.Uri): boolean { - return vscode.workspace.textDocuments.some(document => document.uri.fsPath === uri.fsPath); - } - - private initialize() { - this.diagnosticCollection = vscode.languages.createDiagnosticCollection('python'); - - let disposable = vscode.workspace.onDidSaveTextDocument((e) => { - const settings = PythonSettings.getInstance(e.uri); - if (e.languageId !== 'python' || !settings.linting.enabled || !settings.linting.lintOnSave) { - return; - } - this.lintDocument(e, 100, 'save'); - }); - this.context.subscriptions.push(disposable); - - vscode.workspace.onDidOpenTextDocument((e) => { - const settings = PythonSettings.getInstance(e.uri); - if (e.languageId !== 'python' || !settings.linting.enabled) { - return; - } - // Exclude files opened by vscode when showing a diff view. - if (uriSchemesToIgnore.indexOf(e.uri.scheme) >= 0) { - return; - } - if (!e.uri.path || (path.basename(e.uri.path) === e.uri.path && !fs.existsSync(e.uri.path))) { - return; - } - this.lintDocument(e, 100, 'auto'); - }, this.context.subscriptions); - - disposable = vscode.workspace.onDidCloseTextDocument(textDocument => { - if (!textDocument || !textDocument.fileName || !textDocument.uri) { - return; - } - // Check if this document is still open as a duplicate editor. - if (!this.isDocumentOpen(textDocument.uri) && this.diagnosticCollection.has(textDocument.uri)) { - this.diagnosticCollection.set(textDocument.uri, []); - } - }); - this.context.subscriptions.push(disposable); - this.lintOpenPythonFiles(); + private isDocumentOpen(uri: vscode.Uri): boolean { + return this.documents.textDocuments.some(document => this.fs.arePathsSame(document.uri.fsPath, uri.fsPath)); } - private lintOpenPythonFiles() { - workspace.textDocuments.forEach(async document => { - if (document.languageId === PythonLanguage.language) { - await this.onLintDocument(document, 'auto'); - } - }); - } private lintSettingsChangedHandler(configTarget: ConfigurationTarget, wkspaceOrFolder: Uri) { if (configTarget === ConfigurationTarget.Workspace) { - this.lintOpenPythonFiles(); + this.engine.lintOpenPythonFiles(); return; } // Look for python files that belong to the specified workspace folder. workspace.textDocuments.forEach(async document => { const wkspaceFolder = workspace.getWorkspaceFolder(document.uri); if (wkspaceFolder && wkspaceFolder.uri.fsPath === wkspaceOrFolder.fsPath) { - await this.onLintDocument(document, 'auto'); + await this.engine.lintDocument(document, 'auto'); } }); } - // tslint:disable-next-line:member-ordering no-any - private lastTimeout: any; - private lintDocument(document: vscode.TextDocument, delay: number, trigger: LinterTrigger): void { - // Since this is a hack, lets wait for 2 seconds before linting. - // Give user to continue typing before we waste CPU time. - if (this.lastTimeout) { - clearTimeout(this.lastTimeout); - this.lastTimeout = 0; + private async onDocumentOpened(document: vscode.TextDocument): Promise { + const settings = this.configuration.getSettings(document.uri); + if (document.languageId !== 'python' || !settings.linting.enabled) { + return; } - - this.lastTimeout = setTimeout(async () => { - await this.onLintDocument(document, trigger); - }, delay); - } - private async onLintDocument(document: vscode.TextDocument, trigger: LinterTrigger): Promise { - // Check if we need to lint this document - const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri); - const workspaceRootPath = (workspaceFolder && typeof workspaceFolder.uri.fsPath === 'string') ? workspaceFolder.uri.fsPath : undefined; - const relativeFileName = typeof workspaceRootPath === 'string' ? path.relative(workspaceRootPath, document.fileName) : document.fileName; - const settings = PythonSettings.getInstance(document.uri); - if (document.languageId !== PythonLanguage.language) { + // Exclude files opened by vscode when showing a diff view. + if (uriSchemesToIgnore.indexOf(document.uri.scheme) >= 0) { return; } - if (!this.linterManager.isLintingEnabled()) { - this.diagnosticCollection.set(document.uri, []); + if (!document.uri.path || + (path.basename(document.uri.path) === document.uri.path && !await this.fs.fileExistsAsync(document.uri.path))) { + return; } - const ignoreMinmatches = settings.linting.ignorePatterns.map(pattern => { - return new Minimatch(pattern); - }); + this.engine.lintDocument(document, 'auto').ignoreErrors(); + } - if (ignoreMinmatches.some(matcher => matcher.match(document.fileName) || matcher.match(relativeFileName))) { + private onDocumentSaved(document: vscode.TextDocument) { + const settings = this.configuration.getSettings(document.uri); + if (document.languageId === 'python' && settings.linting.enabled && settings.linting.lintOnSave) { + this.engine.lintDocument(document, 'save').ignoreErrors(); return; } - if (this.pendingLintings.has(document.uri.fsPath)) { - this.pendingLintings.get(document.uri.fsPath)!.cancel(); - this.pendingLintings.delete(document.uri.fsPath); - } - const cancelToken = new vscode.CancellationTokenSource(); - cancelToken.token.onCancellationRequested(() => { - if (this.pendingLintings.has(document.uri.fsPath)) { - this.pendingLintings.delete(document.uri.fsPath); - } - }); - - this.pendingLintings.set(document.uri.fsPath, cancelToken); - this.outputChannel.clear(); - - const promises: Promise[] = this.linterManager.getActiveLinters(document.uri) - .map(info => { - const stopWatch = new StopWatch(); - const linter = this.linterManager.createLinter(info.product, this.outputChannel, this.serviceContainer); - const promise = linter.lint(document, cancelToken.token); - this.sendLinterRunTelemetry(info, document.uri, promise, stopWatch, trigger); - return promise; - }); - this.documentHasJupyterCodeCells(document, cancelToken.token) - .then(hasJupyterCodeCells => { - // linters will resolve asynchronously - keep a track of all - // diagnostics reported as them come in. - let diagnostics: vscode.Diagnostic[] = []; - - promises.forEach(p => { - p.then(msgs => { - if (cancelToken.token.isCancellationRequested) { - return; - } - - // Build the message and suffix the message with the name of the linter used. - msgs.forEach(d => { - // Ignore magic commands from jupyter. - if (hasJupyterCodeCells && document.lineAt(d.line - 1).text.trim().startsWith('%') && - (d.code === LinterErrors.pylint.InvalidSyntax || - d.code === LinterErrors.prospector.InvalidSyntax || - d.code === LinterErrors.flake8.InvalidSyntax)) { - return; - } - diagnostics.push(createDiagnostics(d, document)); - }); - - // Limit the number of messages to the max value. - diagnostics = diagnostics.filter((value, index) => index <= settings.linting.maxNumberOfProblems); - - if (!this.isDocumentOpen(document.uri)) { - diagnostics = []; - } - // Set all diagnostics found in this pass, as this method always clears existing diagnostics. - this.diagnosticCollection.set(document.uri, diagnostics); - }) - .catch(ex => console.error('Python Extension: documentHasJupyterCodeCells.promises', ex)); - }); - }) - .catch(ex => console.error('Python Extension: documentHasJupyterCodeCells', ex)); + const linters = this.linterManager.getActiveLinters(document.uri); + const fileName = path.basename(document.uri.fsPath).toLowerCase(); + const watchers = linters.filter((info) => info.configFileNames.indexOf(fileName) >= 0); + if (watchers.length > 0) { + setTimeout(() => this.engine.lintOpenPythonFiles(), 1000); + } } - private sendLinterRunTelemetry(info: ILinterInfo, resource: Uri, promise: Promise, stopWatch: StopWatch, trigger: LinterTrigger): void { - const linterExecutablePathName = info.pathName(resource); - const properties: LintingTelemetry = { - tool: info.id, - hasCustomArgs: info.linterArgs(resource).length > 0, - trigger, - executableSpecified: linterExecutablePathName.length > 0 - }; - sendTelemetryWhenDone(LINTING, promise, stopWatch, properties); + private onDocumentClosed(document: vscode.TextDocument) { + if (!document || !document.fileName || !document.uri) { + return; + } + // Check if this document is still open as a duplicate editor. + if (!this.isDocumentOpen(document.uri) && this.diagnosticCollection.has(document.uri)) { + this.diagnosticCollection.set(document.uri, []); + } } } diff --git a/src/test/linters/lint.commands.test.ts b/src/test/linters/lint.commands.test.ts index d46d65e6dbfc..d843c7128f4b 100644 --- a/src/test/linters/lint.commands.test.ts +++ b/src/test/linters/lint.commands.test.ts @@ -13,7 +13,7 @@ import { ServiceManager } from '../../client/ioc/serviceManager'; import { IServiceContainer } from '../../client/ioc/types'; import { LinterCommands } from '../../client/linters/linterCommands'; import { LinterManager } from '../../client/linters/linterManager'; -import { ILinterManager } from '../../client/linters/types'; +import { ILinterManager, ILintingEngine } from '../../client/linters/types'; import { closeActiveWindows, initialize, initializeTest } from '../initialize'; // tslint:disable-next-line:max-func-body-length @@ -22,6 +22,7 @@ suite('Linting - Linter Selector', () => { let appShell: TypeMoq.IMock; let commands: LinterCommands; let lm: ILinterManager; + let engine: TypeMoq.IMock; suiteSetup(initialize); setup(async () => { @@ -43,6 +44,9 @@ suite('Linting - Linter Selector', () => { serviceManager.addSingletonInstance(ICommandManager, commandManager.object); serviceManager.addSingletonInstance(IApplicationShell, appShell.object); + engine = TypeMoq.Mock.ofType(); + serviceManager.addSingletonInstance(ILintingEngine, engine.object); + lm = new LinterManager(serviceContainer); serviceManager.addSingletonInstance(ILinterManager, lm); @@ -69,6 +73,11 @@ suite('Linting - Linter Selector', () => { await selectLinterAsync([Product.flake8]); }); + test('Run linter command', async () => { + commands.runLinting(); + engine.verify(p => p.lintOpenPythonFiles(), TypeMoq.Times.once()); + }); + async function enableDisableLinterAsync(enable: boolean): Promise { let suggestions: string[] = []; let options: QuickPickOptions; diff --git a/src/test/linters/lint.manager.test.ts b/src/test/linters/lint.manager.test.ts index 26eb0c39a7b2..68f1e20e39c3 100644 --- a/src/test/linters/lint.manager.test.ts +++ b/src/test/linters/lint.manager.test.ts @@ -112,6 +112,13 @@ suite('Linting - Manager', () => { assert.equal(after![0].id, before![0].id, 'Should not be able to set unsupported linter'); }); + test('Pylint configuration file watch', async () => { + const pylint = lm.getLinterInfo(Product.pylint); + assert.equal(pylint.configFileNames.length, 2, 'Pylint configuration file count is incorrect.'); + assert.notEqual(pylint.configFileNames.indexOf('pylintrc'), -1, 'Pylint configuration files miss pylintrc.'); + assert.notEqual(pylint.configFileNames.indexOf('.pylintrc'), -1, 'Pylint configuration files miss .pylintrc.'); + }); + EnumEx.getValues(Product).forEach(product => { const linterIdMapping = new Map(); linterIdMapping.set(Product.flake8, 'flake8'); diff --git a/src/test/linters/lint.provider.test.ts b/src/test/linters/lint.provider.test.ts new file mode 100644 index 000000000000..53b0b56cedfa --- /dev/null +++ b/src/test/linters/lint.provider.test.ts @@ -0,0 +1,163 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import { Container } from 'inversify'; +import * as TypeMoq from 'typemoq'; +import * as vscode from 'vscode'; +import { IDocumentManager } from '../../client/common/application/types'; +import { createDeferred } from '../../client/common/helpers'; +import { IFileSystem } from '../../client/common/platform/types'; +import { IConfigurationService, ILintingSettings, IPythonSettings, Product } from '../../client/common/types'; +import { IInterpreterService } from '../../client/interpreter/contracts'; +import { ServiceContainer } from '../../client/ioc/container'; +import { ServiceManager } from '../../client/ioc/serviceManager'; +import { LinterManager } from '../../client/linters/linterManager'; +import { ILinterManager, ILintingEngine } from '../../client/linters/types'; +import { LinterProvider } from '../../client/providers/linterProvider'; +import { initialize } from '../initialize'; + +// tslint:disable-next-line:max-func-body-length +suite('Linting - Provider', () => { + let context: TypeMoq.IMock; + let interpreterService: TypeMoq.IMock; + let engine: TypeMoq.IMock; + let configService: TypeMoq.IMock; + let docManager: TypeMoq.IMock; + let settings: TypeMoq.IMock; + let lm: ILinterManager; + let serviceContainer: ServiceContainer; + let emitter: vscode.EventEmitter; + let document: TypeMoq.IMock; + let fs: TypeMoq.IMock; + + suiteSetup(initialize); + setup(async () => { + const cont = new Container(); + const serviceManager = new ServiceManager(cont); + + serviceContainer = new ServiceContainer(cont); + context = TypeMoq.Mock.ofType(); + + fs = TypeMoq.Mock.ofType(); + fs.setup(x => x.fileExistsAsync(TypeMoq.It.isAny())).returns(() => new Promise((resolve, reject) => resolve(true))); + fs.setup(x => x.arePathsSame(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())).returns(() => true); + serviceManager.addSingletonInstance(IFileSystem, fs.object); + + interpreterService = TypeMoq.Mock.ofType(); + serviceManager.addSingletonInstance(IInterpreterService, interpreterService.object); + + engine = TypeMoq.Mock.ofType(); + serviceManager.addSingletonInstance(ILintingEngine, engine.object); + + docManager = TypeMoq.Mock.ofType(); + serviceManager.addSingletonInstance(IDocumentManager, docManager.object); + + const lintSettings = TypeMoq.Mock.ofType(); + lintSettings.setup(x => x.enabled).returns(() => true); + lintSettings.setup(x => x.lintOnSave).returns(() => true); + + settings = TypeMoq.Mock.ofType(); + settings.setup(x => x.linting).returns(() => lintSettings.object); + + configService = TypeMoq.Mock.ofType(); + configService.setup(x => x.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); + serviceManager.addSingletonInstance(IConfigurationService, configService.object); + + lm = new LinterManager(serviceContainer); + serviceManager.addSingletonInstance(ILinterManager, lm); + emitter = new vscode.EventEmitter(); + document = TypeMoq.Mock.ofType(); + }); + + test('Lint on open file', () => { + docManager.setup(x => x.onDidOpenTextDocument).returns(() => emitter.event); + document.setup(x => x.uri).returns(() => vscode.Uri.file('test.py')); + document.setup(x => x.languageId).returns(() => 'python'); + + // tslint:disable-next-line:no-unused-variable + const provider = new LinterProvider(context.object, serviceContainer); + emitter.fire(document.object); + engine.verify(x => x.lintDocument(document.object, 'auto'), TypeMoq.Times.once()); + }); + + test('Lint on save file', () => { + docManager.setup(x => x.onDidSaveTextDocument).returns(() => emitter.event); + document.setup(x => x.uri).returns(() => vscode.Uri.file('test.py')); + document.setup(x => x.languageId).returns(() => 'python'); + + // tslint:disable-next-line:no-unused-variable + const provider = new LinterProvider(context.object, serviceContainer); + emitter.fire(document.object); + engine.verify(x => x.lintDocument(document.object, 'save'), TypeMoq.Times.once()); + }); + + test('No lint on open other files', () => { + docManager.setup(x => x.onDidOpenTextDocument).returns(() => emitter.event); + document.setup(x => x.uri).returns(() => vscode.Uri.file('test.cs')); + document.setup(x => x.languageId).returns(() => 'csharp'); + + // tslint:disable-next-line:no-unused-variable + const provider = new LinterProvider(context.object, serviceContainer); + emitter.fire(document.object); + engine.verify(x => x.lintDocument(document.object, 'save'), TypeMoq.Times.never()); + }); + + test('No lint on save other files', () => { + docManager.setup(x => x.onDidSaveTextDocument).returns(() => emitter.event); + document.setup(x => x.uri).returns(() => vscode.Uri.file('test.cs')); + document.setup(x => x.languageId).returns(() => 'csharp'); + + // tslint:disable-next-line:no-unused-variable + const provider = new LinterProvider(context.object, serviceContainer); + emitter.fire(document.object); + engine.verify(x => x.lintDocument(document.object, 'save'), TypeMoq.Times.never()); + }); + + test('Lint on change interpreters', () => { + const e = new vscode.EventEmitter(); + interpreterService.setup(x => x.onDidChangeInterpreter).returns(() => e.event); + + // tslint:disable-next-line:no-unused-variable + const provider = new LinterProvider(context.object, serviceContainer); + e.fire(); + engine.verify(x => x.lintOpenPythonFiles(), TypeMoq.Times.once()); + }); + + test('Lint on save pylintrc', async () => { + docManager.setup(x => x.onDidSaveTextDocument).returns(() => emitter.event); + document.setup(x => x.uri).returns(() => vscode.Uri.file('.pylintrc')); + + await lm.setActiveLintersAsync([Product.pylint]); + // tslint:disable-next-line:no-unused-variable + const provider = new LinterProvider(context.object, serviceContainer); + emitter.fire(document.object); + + const deferred = createDeferred(); + setTimeout(() => deferred.resolve(), 2000); + await deferred.promise; + engine.verify(x => x.lintOpenPythonFiles(), TypeMoq.Times.once()); + }); + + test('Diagnostic cleared on file close', () => testClearDiagnosticsOnClose(true)); + test('Diagnostic not cleared on file opened in another tab', () => testClearDiagnosticsOnClose(false)); + + function testClearDiagnosticsOnClose(closed: boolean) { + docManager.setup(x => x.onDidCloseTextDocument).returns(() => emitter.event); + + const uri = vscode.Uri.file('test.py'); + document.setup(x => x.uri).returns(() => uri); + document.setup(x => x.isClosed).returns(() => closed); + + docManager.setup(x => x.textDocuments).returns(() => closed ? [] : [document.object]); + + const provider = new LinterProvider(context.object, serviceContainer); + const diags: vscode.Diagnostic[] = []; + diags.push(new vscode.Diagnostic(new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 1)), 'error')); + provider.diagnostics.set(uri, diags); + + emitter.fire(document.object); + const d = provider.diagnostics.get(uri); + expect(d).to.be.lengthOf(closed ? 0 : 1, 'Diagnostic collection not of expected length after file close.'); + } +});