Skip to content

Commit 3fe37a2

Browse files
author
Mikhail Arkhipov
authored
Fix pylintrc search (#805)
* Fix pylint search * Handle quote escapes in strings * Escapes in strings * CR feedback * Discover pylintrc better + tests
1 parent 9aea5f1 commit 3fe37a2

File tree

6 files changed

+221
-25
lines changed

6 files changed

+221
-25
lines changed

src/client/language/tokenizer.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,14 @@ export class Tokenizer implements ITokenizer {
359359
}
360360

361361
private skipToSingleEndQuote(quote: number): void {
362-
while (!this.cs.isEndOfStream() && this.cs.currentChar !== quote) {
362+
while (!this.cs.isEndOfStream()) {
363+
if (this.cs.currentChar === Char.Backslash && this.cs.nextChar === quote) {
364+
this.cs.advance(2);
365+
continue;
366+
}
367+
if (this.cs.currentChar === quote) {
368+
break;
369+
}
363370
this.cs.moveNext();
364371
}
365372
this.cs.moveNext();

src/client/linters/baseLinter.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import * as path from 'path';
22
import * as vscode from 'vscode';
3-
import { CancellationToken, OutputChannel, TextDocument, Uri } from 'vscode';
3+
import { IWorkspaceService } from '../common/application/types';
44
import '../common/extensions';
55
import { IPythonToolExecutionService } from '../common/process/types';
6-
import { ExecutionInfo, ILogger, Product } from '../common/types';
76
import { IConfigurationService, IPythonSettings } from '../common/types';
7+
import { ExecutionInfo, ILogger, Product } from '../common/types';
88
import { IServiceContainer } from '../ioc/types';
99
import { ErrorHandler } from './errorHandlers/errorHandler';
1010
import { ILinter, ILinterInfo, ILinterManager, ILintMessage, LintMessageSeverity } from './types';
@@ -38,25 +38,27 @@ export abstract class BaseLinter implements ILinter {
3838
private errorHandler: ErrorHandler;
3939
private _pythonSettings: IPythonSettings;
4040
private _info: ILinterInfo;
41+
private workspace: IWorkspaceService;
4142

4243
protected get pythonSettings(): IPythonSettings {
4344
return this._pythonSettings;
4445
}
4546

4647
constructor(product: Product,
47-
protected readonly outputChannel: OutputChannel,
48+
protected readonly outputChannel: vscode.OutputChannel,
4849
protected readonly serviceContainer: IServiceContainer,
4950
protected readonly columnOffset = 0) {
5051
this._info = serviceContainer.get<ILinterManager>(ILinterManager).getLinterInfo(product);
5152
this.errorHandler = new ErrorHandler(this.info.product, outputChannel, serviceContainer);
5253
this.configService = serviceContainer.get<IConfigurationService>(IConfigurationService);
54+
this.workspace = serviceContainer.get<IWorkspaceService>(IWorkspaceService);
5355
}
5456

5557
public get info(): ILinterInfo {
5658
return this._info;
5759
}
5860

59-
public isLinterExecutableSpecified(resource: Uri) {
61+
public isLinterExecutableSpecified(resource: vscode.Uri) {
6062
const executablePath = this.info.pathName(resource);
6163
return path.basename(executablePath).length > 0 && path.basename(executablePath) !== executablePath;
6264
}
@@ -66,7 +68,7 @@ export abstract class BaseLinter implements ILinter {
6668
}
6769

6870
protected getWorkspaceRootPath(document: vscode.TextDocument): string {
69-
const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri);
71+
const workspaceFolder = this.workspace.getWorkspaceFolder(document.uri);
7072
const workspaceRootPath = (workspaceFolder && typeof workspaceFolder.uri.fsPath === 'string') ? workspaceFolder.uri.fsPath : undefined;
7173
return typeof workspaceRootPath === 'string' ? workspaceRootPath : __dirname;
7274
}
@@ -107,7 +109,7 @@ export abstract class BaseLinter implements ILinter {
107109
const cwd = this.getWorkspaceRootPath(document);
108110
const pythonToolsExecutionService = this.serviceContainer.get<IPythonToolExecutionService>(IPythonToolExecutionService);
109111
try {
110-
const result = await pythonToolsExecutionService.exec(executionInfo, {cwd, token: cancellation, mergeStdOutErr: true}, document.uri);
112+
const result = await pythonToolsExecutionService.exec(executionInfo, { cwd, token: cancellation, mergeStdOutErr: true }, document.uri);
111113
this.displayLinterResultHeader(result.stdout);
112114
return await this.parseMessages(result.stdout, document, cancellation, regEx);
113115
} catch (error) {
@@ -116,12 +118,12 @@ export abstract class BaseLinter implements ILinter {
116118
}
117119
}
118120

119-
protected async parseMessages(output: string, document: TextDocument, token: CancellationToken, regEx: string) {
121+
protected async parseMessages(output: string, document: vscode.TextDocument, token: vscode.CancellationToken, regEx: string) {
120122
const outputLines = output.splitLines({ removeEmptyEntries: false, trim: false });
121123
return this.parseLines(outputLines, regEx);
122124
}
123125

124-
protected handleError(error: Error, resource: Uri, execInfo: ExecutionInfo) {
126+
protected handleError(error: Error, resource: vscode.Uri, execInfo: ExecutionInfo) {
125127
this.errorHandler.handleError(error, resource, execInfo)
126128
.catch(this.logger.logError.bind(this, 'Error in errorHandler.handleError'));
127129
}

src/client/linters/pylint.ts

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import { IServiceContainer } from '../ioc/types';
1111
import { BaseLinter } from './baseLinter';
1212
import { ILintMessage } from './types';
1313

14+
const pylintrc = 'pylintrc';
15+
const dotPylintrc = '.pylintrc';
16+
1417
export class Pylint extends BaseLinter {
1518
private fileSystem: IFileSystem;
1619
private platformService: IPlatformService;
@@ -25,12 +28,16 @@ export class Pylint extends BaseLinter {
2528
let minArgs: string[] = [];
2629
// Only use minimal checkers if
2730
// a) there are no custom arguments and
28-
// b) there is no pylintrc file
31+
// b) there is no pylintrc file next to the file or at the workspace root
2932
const uri = document.uri;
33+
const workspaceRoot = this.getWorkspaceRootPath(document);
3034
const settings = this.configService.getSettings(uri);
3135
if (settings.linting.pylintUseMinimalCheckers
3236
&& this.info.linterArgs(uri).length === 0
33-
&& !await Pylint.hasConfigurationFile(this.fileSystem, uri.fsPath, this.platformService)) {
37+
// Check pylintrc next to the file or above up to and including the workspace root
38+
&& !await Pylint.hasConfigrationFileInWorkspace(this.fileSystem, path.dirname(uri.fsPath), workspaceRoot)
39+
// Check for pylintrc at the root and above
40+
&& !await Pylint.hasConfigurationFile(this.fileSystem, this.getWorkspaceRootPath(document), this.platformService)) {
3441
minArgs = [
3542
'--disable=all',
3643
'--enable=F,E,unreachable,duplicate-key,unnecessary-semicolon,global-variable-not-assigned,unused-variable,unused-wildcard-import,binary-op-exception,bad-format-string,anomalous-backslash-in-string,bad-open-mode'
@@ -51,7 +58,7 @@ export class Pylint extends BaseLinter {
5158
}
5259

5360
// tslint:disable-next-line:member-ordering
54-
public static async hasConfigurationFile(fs: IFileSystem, filePath: string, platformService: IPlatformService): Promise<boolean> {
61+
public static async hasConfigurationFile(fs: IFileSystem, folder: string, platformService: IPlatformService): Promise<boolean> {
5562
// https://pylint.readthedocs.io/en/latest/user_guide/run.html
5663
// https://github.com/PyCQA/pylint/blob/975e08148c0faa79958b459303c47be1a2e1500a/pylint/config.py
5764
// 1. pylintrc in the current working directory
@@ -69,9 +76,7 @@ export class Pylint extends BaseLinter {
6976
return true;
7077
}
7178

72-
let dir = path.dirname(filePath);
73-
const pylintrc = 'pylintrc';
74-
const dotPylintrc = '.pylintrc';
79+
let dir = folder;
7580
if (await fs.fileExistsAsync(path.join(dir, pylintrc)) || await fs.fileExistsAsync(path.join(dir, dotPylintrc))) {
7681
return true;
7782
}
@@ -87,7 +92,7 @@ export class Pylint extends BaseLinter {
8792
}
8893
current = above;
8994
above = path.dirname(above);
90-
} while (current !== above);
95+
} while (!fs.arePathsSame(current, above));
9196

9297
dir = path.resolve('~');
9398
if (await fs.fileExistsAsync(path.join(dir, dotPylintrc))) {
@@ -104,4 +109,19 @@ export class Pylint extends BaseLinter {
104109
}
105110
return false;
106111
}
112+
113+
// tslint:disable-next-line:member-ordering
114+
public static async hasConfigrationFileInWorkspace(fs: IFileSystem, folder: string, root: string): Promise<boolean> {
115+
// Search up from file location to the workspace root
116+
let current = folder;
117+
let above = path.dirname(current);
118+
do {
119+
if (await fs.fileExistsAsync(path.join(current, pylintrc)) || await fs.fileExistsAsync(path.join(current, dotPylintrc))) {
120+
return true;
121+
}
122+
current = above;
123+
above = path.dirname(above);
124+
} while (!fs.arePathsSame(current, root) && !fs.arePathsSame(current, above));
125+
return false;
126+
}
107127
}

src/test/language/tokenizer.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,21 @@ suite('Language.Tokenizer', () => {
6464
assert.equal(tokens.getItemAt(i).type, TokenType.String);
6565
}
6666
});
67+
test('Strings: single quote escape', async () => {
68+
const t = new Tokenizer();
69+
// tslint:disable-next-line:quotemark
70+
const tokens = t.tokenize("'\\'quoted\\''");
71+
assert.equal(tokens.count, 1);
72+
assert.equal(tokens.getItemAt(0).type, TokenType.String);
73+
assert.equal(tokens.getItemAt(0).length, 12);
74+
});
75+
test('Strings: double quote escape', async () => {
76+
const t = new Tokenizer();
77+
const tokens = t.tokenize('"\\"quoted\\""');
78+
assert.equal(tokens.count, 1);
79+
assert.equal(tokens.getItemAt(0).type, TokenType.String);
80+
assert.equal(tokens.getItemAt(0).length, 12);
81+
});
6782
test('Comments', async () => {
6883
const t = new Tokenizer();
6984
const tokens = t.tokenize(' #co"""mment1\n\t\n#comm\'ent2 ');

0 commit comments

Comments
 (0)