Skip to content

Commit 29a0cae

Browse files
author
Mikhail Arkhipov
authored
Fix backslash escape, multiple linters output, .pyenv/versions folder search (#920)
* Basic tokenizer * Fixed property names * Tests, round I * Tests, round II * tokenizer test * Remove temorary change * Fix merge issue * Merge conflict * Merge conflict * Completion test * Fix last line * Fix javascript math * Make test await for results * Add license headers * Rename definitions to types * License headers * Fix typo in completion details (typo) * Fix hover test * Russian translations * Update to better translation * Fix typo * #70 How to get all parameter info when filling in a function param list * Fix #70 How to get all parameter info when filling in a function param list * Clean up * Clean imports * CR feedback * Trim whitespace for test stability * More tests * Better handle no-parameters documentation * Better handle ellipsis and Python3 * #385 Auto-Indentation doesn't work after comment * #141 Auto indentation broken when return keyword involved * Undo changes * #627 Docstrings for builtin methods are not parsed correctly * reStructuredText converter * Fix: period is not an operator * Minor fixes * Restructure * Tests * Tests * Code heuristics * Baselines * HTML handling * Lists * State machine * Baselines * Squash * no message * Whitespace difference * Update Jedi to 0.11.1 * Enable Travis * Test fixes * Undo change * Jedi 0.11 with parser * Undo changes * Undo changes * Test fixes * More tests * Tests * Fix pylint search * Handle quote escapes in strings * Escapes in strings * CR feedback * Discover pylintrc better + tests * Fix .pyenv/versions search * Fix multiple linters output * Better handle markdown underscore * Test * Fix 916: PyLint checks wrong files * Test stability * Try increase timeout * Make sure linting is enabled in tests * Try another way of waiting * Simplify * Fix clear diags on close tests * Try writing settings directly * Increase timeout * Measure test time * Measure time * Simplify * Set timeout
1 parent b77d68a commit 29a0cae

File tree

11 files changed

+176
-82
lines changed

11 files changed

+176
-82
lines changed

src/client/common/markdown/restTextConverter.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ export class RestTextConverter {
3636
return text
3737
.replace(/\#/g, '\\#')
3838
.replace(/\*/g, '\\*')
39-
.replace(/\_/g, '\\_');
39+
.replace(/\ _/g, ' \\_')
40+
.replace(/^_/, '\\_');
4041
}
4142

4243
private transformLines(docstring: string): string {

src/client/interpreter/locators/services/globalVirtualEnvService.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,11 @@ export class GlobalVirtualEnvironmentsSearchPathProvider implements IVirtualEnvi
4242
folders.push(pyenvRoot);
4343
folders.push(path.join(pyenvRoot, 'versions'));
4444
} else {
45+
// Check if .pyenv/versions is in the list
4546
const pyenvVersions = path.join('.pyenv', 'versions');
4647
if (venvFolders.indexOf('.pyenv') >= 0 && venvFolders.indexOf(pyenvVersions) < 0) {
47-
folders.push(pyenvVersions);
48+
// if .pyenv is in the list, but .pyenv/versions is not, add it.
49+
folders.push(path.join(homedir, pyenvVersions));
4850
}
4951
}
5052
return folders;

src/client/linters/linterCommands.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,9 @@ export class LinterCommands implements vscode.Disposable {
7979
}
8080
}
8181

82-
public runLinting(): void {
82+
public runLinting(): Promise<vscode.DiagnosticCollection> {
8383
const engine = this.serviceContainer.get<ILintingEngine>(ILintingEngine);
84-
engine.lintOpenPythonFiles();
84+
return engine.lintOpenPythonFiles();
8585
}
8686

8787
private get settingsUri(): vscode.Uri | undefined {

src/client/linters/lintingEngine.ts

Lines changed: 51 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import { Minimatch } from 'minimatch';
66
import * as path from 'path';
77
import * as vscode from 'vscode';
88
import { IDocumentManager, IWorkspaceService } from '../common/application/types';
9-
import { LinterErrors, PythonLanguage, STANDARD_OUTPUT_CHANNEL } from '../common/constants';
9+
import { LinterErrors, STANDARD_OUTPUT_CHANNEL } from '../common/constants';
10+
import { IFileSystem } from '../common/platform/types';
1011
import { IConfigurationService, IOutputChannel } from '../common/types';
1112
import { IServiceContainer } from '../ioc/types';
1213
import { JupyterProvider } from '../jupyter/provider';
@@ -40,6 +41,7 @@ export class LintingEngine implements ILintingEngine {
4041
private diagnosticCollection: vscode.DiagnosticCollection;
4142
private pendingLintings = new Map<string, vscode.CancellationTokenSource>();
4243
private outputChannel: vscode.OutputChannel;
44+
private fileSystem: IFileSystem;
4345

4446
constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) {
4547
this.documentHasJupyterCodeCells = (a, b) => Promise.resolve(false);
@@ -48,34 +50,32 @@ export class LintingEngine implements ILintingEngine {
4850
this.configurationService = serviceContainer.get<IConfigurationService>(IConfigurationService);
4951
this.outputChannel = serviceContainer.get<vscode.OutputChannel>(IOutputChannel, STANDARD_OUTPUT_CHANNEL);
5052
this.linterManager = serviceContainer.get<ILinterManager>(ILinterManager);
53+
this.fileSystem = serviceContainer.get<IFileSystem>(IFileSystem);
5154
this.diagnosticCollection = vscode.languages.createDiagnosticCollection('python');
5255
}
5356

54-
public lintOpenPythonFiles(): void {
55-
this.documents.textDocuments.forEach(async document => {
56-
if (document.languageId === PythonLanguage.language) {
57-
await this.lintDocument(document, 'auto');
58-
}
59-
});
57+
public get diagnostics(): vscode.DiagnosticCollection {
58+
return this.diagnosticCollection;
6059
}
6160

62-
public async lintDocument(document: vscode.TextDocument, trigger: LinterTrigger): Promise<void> {
63-
// Check if we need to lint this document
64-
const workspaceFolder = this.workspace.getWorkspaceFolder(document.uri);
65-
const workspaceRootPath = (workspaceFolder && typeof workspaceFolder.uri.fsPath === 'string') ? workspaceFolder.uri.fsPath : undefined;
66-
const relativeFileName = typeof workspaceRootPath === 'string' ? path.relative(workspaceRootPath, document.fileName) : document.fileName;
67-
const settings = this.configurationService.getSettings(document.uri);
68-
if (document.languageId !== PythonLanguage.language) {
69-
return;
61+
public clearDiagnostics(document: vscode.TextDocument): void {
62+
if (this.diagnosticCollection.has(document.uri)) {
63+
this.diagnosticCollection.delete(document.uri);
7064
}
71-
if (!this.linterManager.isLintingEnabled(document.uri)) {
72-
this.diagnosticCollection.set(document.uri, []);
73-
}
74-
const ignoreMinmatches = settings.linting.ignorePatterns.map(pattern => {
75-
return new Minimatch(pattern);
76-
});
65+
}
7766

78-
if (ignoreMinmatches.some(matcher => matcher.match(document.fileName) || matcher.match(relativeFileName))) {
67+
public async lintOpenPythonFiles(): Promise<vscode.DiagnosticCollection> {
68+
this.diagnosticCollection.clear();
69+
const promises = this.documents.textDocuments.map(async document => await this.lintDocument(document, 'auto'));
70+
await Promise.all(promises);
71+
return this.diagnosticCollection;
72+
}
73+
74+
public async lintDocument(document: vscode.TextDocument, trigger: LinterTrigger): Promise<void> {
75+
this.diagnosticCollection.set(document.uri, []);
76+
77+
// Check if we need to lint this document
78+
if (!await this.shouldLintDocument(document)) {
7979
return;
8080
}
8181

@@ -107,14 +107,14 @@ export class LintingEngine implements ILintingEngine {
107107
// linters will resolve asynchronously - keep a track of all
108108
// diagnostics reported as them come in.
109109
let diagnostics: vscode.Diagnostic[] = [];
110+
const settings = this.configurationService.getSettings(document.uri);
110111

111112
for (const p of promises) {
112113
const msgs = await p;
113114
if (cancelToken.token.isCancellationRequested) {
114115
break;
115116
}
116117

117-
diagnostics = [];
118118
if (this.isDocumentOpen(document.uri)) {
119119
// Build the message and suffix the message with the name of the linter used.
120120
for (const m of msgs) {
@@ -123,17 +123,16 @@ export class LintingEngine implements ILintingEngine {
123123
(m.code === LinterErrors.pylint.InvalidSyntax ||
124124
m.code === LinterErrors.prospector.InvalidSyntax ||
125125
m.code === LinterErrors.flake8.InvalidSyntax)) {
126-
return;
126+
continue;
127127
}
128128
diagnostics.push(this.createDiagnostics(m, document));
129129
}
130-
131130
// Limit the number of messages to the max value.
132131
diagnostics = diagnostics.filter((value, index) => index <= settings.linting.maxNumberOfProblems);
133132
}
134-
// Set all diagnostics found in this pass, as this method always clears existing diagnostics.
135-
this.diagnosticCollection.set(document.uri, diagnostics);
136133
}
134+
// Set all diagnostics found in this pass, as this method always clears existing diagnostics.
135+
this.diagnosticCollection.set(document.uri, diagnostics);
137136
}
138137

139138
// tslint:disable-next-line:no-any
@@ -175,4 +174,29 @@ export class LintingEngine implements ILintingEngine {
175174
diagnostic.source = message.provider;
176175
return diagnostic;
177176
}
177+
178+
private async shouldLintDocument(document: vscode.TextDocument): Promise<boolean> {
179+
if (!this.linterManager.isLintingEnabled(document.uri)) {
180+
this.diagnosticCollection.set(document.uri, []);
181+
return false;
182+
}
183+
184+
if (document.languageId !== PYTHON.language) {
185+
return false;
186+
}
187+
188+
const workspaceFolder = this.workspace.getWorkspaceFolder(document.uri);
189+
const workspaceRootPath = (workspaceFolder && typeof workspaceFolder.uri.fsPath === 'string') ? workspaceFolder.uri.fsPath : undefined;
190+
const relativeFileName = typeof workspaceRootPath === 'string' ? path.relative(workspaceRootPath, document.fileName) : document.fileName;
191+
192+
const settings = this.configurationService.getSettings(document.uri);
193+
const ignoreMinmatches = settings.linting.ignorePatterns.map(pattern => new Minimatch(pattern));
194+
if (ignoreMinmatches.some(matcher => matcher.match(document.fileName) || matcher.match(relativeFileName))) {
195+
return false;
196+
}
197+
if (document.uri.scheme !== 'file' || !document.uri.fsPath) {
198+
return false;
199+
}
200+
return await this.fileSystem.fileExistsAsync(document.uri.fsPath);
201+
}
178202
}

src/client/linters/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,10 @@ export enum LintMessageSeverity {
6161

6262
export const ILintingEngine = Symbol('ILintingEngine');
6363
export interface ILintingEngine {
64-
lintOpenPythonFiles(): void;
64+
readonly diagnostics: vscode.DiagnosticCollection;
65+
lintOpenPythonFiles(): Promise<vscode.DiagnosticCollection>;
6566
lintDocument(document: vscode.TextDocument, trigger: LinterTrigger): Promise<void>;
6667
// tslint:disable-next-line:no-any
6768
linkJupiterExtension(jupiter: vscode.Extension<any> | undefined): Promise<void>;
69+
clearDiagnostics(document: vscode.TextDocument): void;
6870
}

src/client/providers/linterProvider.ts

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,14 @@ import * as vscode from 'vscode';
66
import { ConfigurationTarget, Uri, workspace } from 'vscode';
77
import { IDocumentManager } from '../common/application/types';
88
import { ConfigSettingMonitor } from '../common/configSettingMonitor';
9+
import { isTestExecution } from '../common/constants';
910
import { IFileSystem } from '../common/platform/types';
1011
import { IConfigurationService } from '../common/types';
1112
import { IInterpreterService } from '../interpreter/contracts';
1213
import { IServiceContainer } from '../ioc/types';
1314
import { ILinterManager, ILintingEngine } from '../linters/types';
1415

15-
const uriSchemesToIgnore = ['git', 'showModifications', 'svn'];
16-
1716
export class LinterProvider implements vscode.Disposable {
18-
private diagnosticCollection: vscode.DiagnosticCollection;
1917
private context: vscode.ExtensionContext;
2018
private disposables: vscode.Disposable[];
2119
private configMonitor: ConfigSettingMonitor;
@@ -37,7 +35,6 @@ export class LinterProvider implements vscode.Disposable {
3735
this.documents = serviceContainer.get<IDocumentManager>(IDocumentManager);
3836
this.configuration = serviceContainer.get<IConfigurationService>(IConfigurationService);
3937

40-
this.diagnosticCollection = vscode.languages.createDiagnosticCollection('python');
4138
this.disposables.push(this.interpreterService.onDidChangeInterpreter(() => this.engine.lintOpenPythonFiles()));
4239

4340
this.documents.onDidOpenTextDocument(e => this.onDocumentOpened(e), this.context.subscriptions);
@@ -46,10 +43,12 @@ export class LinterProvider implements vscode.Disposable {
4643

4744
this.configMonitor = new ConfigSettingMonitor('linting');
4845
this.configMonitor.on('change', this.lintSettingsChangedHandler.bind(this));
49-
}
5046

51-
public get diagnostics(): vscode.DiagnosticCollection {
52-
return this.diagnosticCollection;
47+
// On workspace reopen we don't get `onDocumentOpened` since it is first opened
48+
// and then the extension is activated. So schedule linting pass now.
49+
if (!isTestExecution()) {
50+
setTimeout(() => this.engine.lintOpenPythonFiles().ignoreErrors(), 1200);
51+
}
5352
}
5453

5554
public dispose() {
@@ -63,35 +62,23 @@ export class LinterProvider implements vscode.Disposable {
6362

6463
private lintSettingsChangedHandler(configTarget: ConfigurationTarget, wkspaceOrFolder: Uri) {
6564
if (configTarget === ConfigurationTarget.Workspace) {
66-
this.engine.lintOpenPythonFiles();
65+
this.engine.lintOpenPythonFiles().ignoreErrors();
6766
return;
6867
}
6968
// Look for python files that belong to the specified workspace folder.
7069
workspace.textDocuments.forEach(async document => {
7170
const wkspaceFolder = workspace.getWorkspaceFolder(document.uri);
7271
if (wkspaceFolder && wkspaceFolder.uri.fsPath === wkspaceOrFolder.fsPath) {
73-
await this.engine.lintDocument(document, 'auto');
72+
this.engine.lintDocument(document, 'auto').ignoreErrors();
7473
}
7574
});
7675
}
7776

78-
private async onDocumentOpened(document: vscode.TextDocument): Promise<void> {
79-
const settings = this.configuration.getSettings(document.uri);
80-
if (document.languageId !== 'python' || !settings.linting.enabled) {
81-
return;
82-
}
83-
// Exclude files opened by vscode when showing a diff view.
84-
if (uriSchemesToIgnore.indexOf(document.uri.scheme) >= 0) {
85-
return;
86-
}
87-
if (!document.uri.path ||
88-
(path.basename(document.uri.path) === document.uri.path && !await this.fs.fileExistsAsync(document.uri.path))) {
89-
return;
90-
}
77+
private onDocumentOpened(document: vscode.TextDocument): void {
9178
this.engine.lintDocument(document, 'auto').ignoreErrors();
9279
}
9380

94-
private onDocumentSaved(document: vscode.TextDocument) {
81+
private onDocumentSaved(document: vscode.TextDocument): void {
9582
const settings = this.configuration.getSettings(document.uri);
9683
if (document.languageId === 'python' && settings.linting.enabled && settings.linting.lintOnSave) {
9784
this.engine.lintDocument(document, 'save').ignoreErrors();
@@ -111,8 +98,8 @@ export class LinterProvider implements vscode.Disposable {
11198
return;
11299
}
113100
// Check if this document is still open as a duplicate editor.
114-
if (!this.isDocumentOpen(document.uri) && this.diagnosticCollection.has(document.uri)) {
115-
this.diagnosticCollection.set(document.uri, []);
101+
if (!this.isDocumentOpen(document.uri)) {
102+
this.engine.clearDiagnostics(document);
116103
}
117104
}
118105
}

src/test/interpreters/venv.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ suite('Virtual environments', () => {
4949

5050
let paths = pathProvider.getSearchPaths();
5151
let expected = folders.map(item => path.join(homedir, item));
52-
expected.push(path.join('.pyenv', 'versions'));
52+
expected.push(path.join(homedir, '.pyenv', 'versions'));
5353

5454
expect(paths).to.deep.equal(expected, 'Global search folder list is incorrect.');
5555

src/test/linters/lint.provider.test.ts

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

4-
import { expect } from 'chai';
54
import { Container } from 'inversify';
65
import * as TypeMoq from 'typemoq';
76
import * as vscode from 'vscode';
@@ -150,14 +149,11 @@ suite('Linting - Provider', () => {
150149
document.setup(x => x.isClosed).returns(() => closed);
151150

152151
docManager.setup(x => x.textDocuments).returns(() => closed ? [] : [document.object]);
153-
152+
// tslint:disable-next-line:prefer-const no-unused-variable
154153
const provider = new LinterProvider(context.object, serviceContainer);
155-
const diags: vscode.Diagnostic[] = [];
156-
diags.push(new vscode.Diagnostic(new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 1)), 'error'));
157-
provider.diagnostics.set(uri, diags);
158154

159155
emitter.fire(document.object);
160-
const d = provider.diagnostics.get(uri);
161-
expect(d).to.be.lengthOf(closed ? 0 : 1, 'Diagnostic collection not of expected length after file close.');
156+
const timesExpected = closed ? TypeMoq.Times.once() : TypeMoq.Times.never();
157+
engine.verify(x => x.clearDiagnostics(TypeMoq.It.isAny()), timesExpected);
162158
}
163159
});

src/test/linters/lint.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as fs from 'fs-extra';
33
import * as path from 'path';
44
import { Uri } from 'vscode';
55
import * as vscode from 'vscode';
6+
import { ICommandManager } from '../../client/common/application/types';
67
import { STANDARD_OUTPUT_CHANNEL } from '../../client/common/constants';
78
import { Product } from '../../client/common/installer/productInstaller';
89
import { IConfigurationService, IOutputChannel } from '../../client/common/types';
@@ -250,4 +251,26 @@ suite('Linting', () => {
250251
await configService.updateSettingAsync('linting.pylintUseMinimalCheckers', false, workspaceUri);
251252
await testEnablingDisablingOfLinter(Product.pylint, true, file);
252253
});
254+
// tslint:disable-next-line:no-function-expression
255+
test('Multiple linters', async function () {
256+
// tslint:disable-next-line:no-invalid-this
257+
this.timeout(40000);
258+
259+
await closeActiveWindows();
260+
const document = await vscode.workspace.openTextDocument(path.join(pythoFilesPath, 'print.py'));
261+
await vscode.window.showTextDocument(document);
262+
await configService.updateSettingAsync('linting.enabled', true, workspaceUri);
263+
await configService.updateSettingAsync('linting.pylintUseMinimalCheckers', false, workspaceUri);
264+
await configService.updateSettingAsync('linting.pylintEnabled', true, workspaceUri);
265+
await configService.updateSettingAsync('linting.flake8Enabled', true, workspaceUri);
266+
267+
const commands = ioc.serviceContainer.get<ICommandManager>(ICommandManager);
268+
const collection = await commands.executeCommand('python.runLinting') as vscode.DiagnosticCollection;
269+
assert.notEqual(collection, undefined, 'python.runLinting did not return valid diagnostics collection.');
270+
271+
const messages = collection!.get(document.uri);
272+
assert.notEqual(messages!.length, 0, 'No diagnostic messages.');
273+
assert.notEqual(messages!.filter(x => x.source === 'pylint').length, 0, 'No pylint messages.');
274+
assert.notEqual(messages!.filter(x => x.source === 'flake8').length, 0, 'No flake8 messages.');
275+
});
253276
});

0 commit comments

Comments
 (0)