diff --git a/news/1 Enhancements/15170.md b/news/1 Enhancements/15170.md new file mode 100644 index 000000000000..fde246e92891 --- /dev/null +++ b/news/1 Enhancements/15170.md @@ -0,0 +1 @@ +Added `python.linting.cwd` to change the working directory of the linters (thanks [Matthew Shirley](https://github.com/matthewshirley)) \ No newline at end of file diff --git a/package.json b/package.json index 1fd3eccc220f..078c8850fe92 100644 --- a/package.json +++ b/package.json @@ -1269,6 +1269,12 @@ "description": "Whether to lint Python files.", "scope": "resource" }, + "python.linting.cwd": { + "type": "string", + "default": null, + "description": "Optional working directory for linters.", + "scope": "resource" + }, "python.linting.flake8Args": { "type": "array", "description": "Arguments passed in. Each argument is a separate item in the array.", diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index 779b081363a4..d44ea117e7cb 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -339,6 +339,7 @@ export class PythonSettings implements IPythonSettings { ? this.linting : { enabled: false, + cwd: undefined, ignorePatterns: [], flake8Args: [], flake8Enabled: false, @@ -409,6 +410,10 @@ export class PythonSettings implements IPythonSettings { this.linting.mypyPath = getAbsolutePath(systemVariables.resolveAny(this.linting.mypyPath), workspaceRoot); this.linting.banditPath = getAbsolutePath(systemVariables.resolveAny(this.linting.banditPath), workspaceRoot); + if (this.linting.cwd) { + this.linting.cwd = getAbsolutePath(systemVariables.resolveAny(this.linting.cwd), workspaceRoot); + } + const formattingSettings = systemVariables.resolveAny(pythonSettings.get('formatting'))!; if (this.formatting) { Object.assign(this.formatting, formattingSettings); diff --git a/src/client/common/types.ts b/src/client/common/types.ts index 52f153ba163e..1070197d7722 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -258,6 +258,7 @@ export interface ILintingSettings { readonly pycodestyleCategorySeverity: IPycodestyleCategorySeverity; readonly flake8CategorySeverity: Flake8CategorySeverity; readonly mypyCategorySeverity: IMypyCategorySeverity; + cwd?: string; prospectorPath: string; pylintPath: string; pycodestylePath: string; diff --git a/src/client/linters/baseLinter.ts b/src/client/linters/baseLinter.ts index 565bdd5a9103..37fa11c843d3 100644 --- a/src/client/linters/baseLinter.ts +++ b/src/client/linters/baseLinter.ts @@ -101,6 +101,10 @@ export abstract class BaseLinter implements ILinter { workspaceFolder && typeof workspaceFolder.uri.fsPath === 'string' ? workspaceFolder.uri.fsPath : undefined; return typeof workspaceRootPath === 'string' ? workspaceRootPath : path.dirname(document.uri.fsPath); } + + protected getWorkingDirectoryPath(document: vscode.TextDocument): string { + return this._pythonSettings.linting.cwd || this.getWorkspaceRootPath(document); + } protected abstract runLinter( document: vscode.TextDocument, cancellation: vscode.CancellationToken, @@ -138,7 +142,7 @@ export abstract class BaseLinter implements ILinter { return []; } const executionInfo = this.info.getExecutionInfo(args, document.uri); - const cwd = this.getWorkspaceRootPath(document); + const cwd = this.getWorkingDirectoryPath(document); const pythonToolsExecutionService = this.serviceContainer.get( IPythonToolExecutionService, ); diff --git a/src/client/linters/prospector.ts b/src/client/linters/prospector.ts index 106f3bfe9104..50524b2e3b33 100644 --- a/src/client/linters/prospector.ts +++ b/src/client/linters/prospector.ts @@ -30,7 +30,7 @@ export class Prospector extends BaseLinter { } protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise { - const cwd = this.getWorkspaceRootPath(document); + const cwd = this.getWorkingDirectoryPath(document); const relativePath = path.relative(cwd, document.uri.fsPath); return this.run(['--absolute-paths', '--output-format=json', relativePath], document, cancellation); } diff --git a/src/client/linters/pylint.ts b/src/client/linters/pylint.ts index f235d986fa90..495787568509 100644 --- a/src/client/linters/pylint.ts +++ b/src/client/linters/pylint.ts @@ -39,10 +39,10 @@ export class Pylint extends BaseLinter { this.info.linterArgs(uri).length === 0 && // Check pylintrc next to the file or above up to and including the workspace root !(await Pylint.hasConfigurationFileInWorkspace(this.fileSystem, path.dirname(uri.fsPath), workspaceRoot)) && - // Check for pylintrc at the root and above + // Check for pylintrc at the cwd and above !(await Pylint.hasConfigurationFile( this.fileSystem, - this.getWorkspaceRootPath(document), + this.getWorkingDirectoryPath(document), this.platformService, )) ) { diff --git a/src/test/linters/common.ts b/src/test/linters/common.ts index 0f8d9bf79e81..87a489cbbb31 100644 --- a/src/test/linters/common.ts +++ b/src/test/linters/common.ts @@ -70,6 +70,7 @@ export function throwUnknownProduct(product: Product) { export class LintingSettings { public enabled: boolean; + public cwd?: string; public ignorePatterns: string[]; public prospectorEnabled: boolean; public prospectorArgs: string[]; @@ -107,6 +108,7 @@ export class LintingSettings { // mostly from configSettings.ts this.enabled = true; + this.cwd = undefined; this.ignorePatterns = []; this.lintOnSave = false; this.maxNumberOfProblems = 100; diff --git a/src/test/linters/lint.args.test.ts b/src/test/linters/lint.args.test.ts index 579d991749da..0a86ca34253c 100644 --- a/src/test/linters/lint.args.test.ts +++ b/src/test/linters/lint.args.test.ts @@ -102,6 +102,7 @@ suite('Linting - Arguments', () => { const lintSettings = TypeMoq.Mock.ofType(); lintSettings.setup((x) => x.enabled).returns(() => true); lintSettings.setup((x) => x.lintOnSave).returns(() => true); + lintSettings.setup((x) => x.cwd).returns(() => undefined); settings = TypeMoq.Mock.ofType(); settings.setup((x) => x.linting).returns(() => lintSettings.object); diff --git a/src/test/linters/lint.functional.test.ts b/src/test/linters/lint.functional.test.ts index 9263d4571aef..bf0c2f78a390 100644 --- a/src/test/linters/lint.functional.test.ts +++ b/src/test/linters/lint.functional.test.ts @@ -846,4 +846,17 @@ suite('Linting Functional Tests', () => { maxErrors, ); }); + + test('Linters use config in cwd directory', async () => { + const maxErrors = 0; + const fixture = new TestFixture(); + fixture.lintingSettings.cwd = path.join(pythonFilesDir, 'pylintcwd'); + + await testLinterMessageCount( + fixture, + Product.pylint, + path.join(pythonFilesDir, 'threeLineLints.py'), + maxErrors, + ); + }); }); diff --git a/src/test/linters/pylint.unit.test.ts b/src/test/linters/pylint.unit.test.ts index 8764286da897..a642d7400f00 100644 --- a/src/test/linters/pylint.unit.test.ts +++ b/src/test/linters/pylint.unit.test.ts @@ -354,7 +354,7 @@ suite('Pylint - Function runLinter()', () => { ): Promise { return super.runLinter(document, cancellation); } - public getWorkspaceRootPath(_document: vscode.TextDocument): string { + public getWorkingDirectoryPath(_document: vscode.TextDocument): string { return 'path/to/workspaceRoot'; } } diff --git a/src/test/mockClasses.ts b/src/test/mockClasses.ts index c5fa011b27cc..0404f18910e8 100644 --- a/src/test/mockClasses.ts +++ b/src/test/mockClasses.ts @@ -54,6 +54,7 @@ export class MockStatusBarItem implements vscode.StatusBarItem { export class MockLintingSettings implements ILintingSettings { public enabled!: boolean; + public cwd?: string; public ignorePatterns!: string[]; public prospectorEnabled!: boolean; public prospectorArgs!: string[]; diff --git a/src/test/pythonFiles/linting/cwd/.pylintrc b/src/test/pythonFiles/linting/cwd/.pylintrc new file mode 100644 index 000000000000..8530187c095f --- /dev/null +++ b/src/test/pythonFiles/linting/cwd/.pylintrc @@ -0,0 +1,2 @@ +[MESSAGES CONTROL] +disable=C0326,I0011,I0012,C0304,C0103,W0613,E0001,E1101