diff --git a/packages/pyright-internal/src/common/envVarUtils.ts b/packages/pyright-internal/src/common/envVarUtils.ts index 324017499ccc..5ae1ff88479a 100644 --- a/packages/pyright-internal/src/common/envVarUtils.ts +++ b/packages/pyright-internal/src/common/envVarUtils.ts @@ -8,12 +8,13 @@ import * as os from 'os'; +import { Workspace } from '../workspaceFactory'; import { Uri } from './uri/uri'; // Expands certain predefined variables supported within VS Code settings. // Ideally, VS Code would provide an API for doing this expansion, but // it doesn't. We'll handle the most common variables here as a convenience. -export function expandPathVariables(rootPath: Uri, path: string): string { +export function expandPathVariables(path: string, rootPath: Uri, workspaces: Workspace[]): string { // Make sure the pathStr looks like a URI path. let pathStr = path.replace(/\\/g, '/'); @@ -24,6 +25,10 @@ export function expandPathVariables(rootPath: Uri, path: string): string { // Replace everything inline. pathStr = pathStr.replace(/\$\{workspaceFolder\}/g, rootPath.getPath()); + for (const workspace of workspaces) { + const ws_regexp = RegExp(`\\$\\{workspaceFolder:${workspace.workspaceName}\\}`, 'g'); + pathStr = pathStr.replace(ws_regexp, workspace.rootUri.getPath()); + } if (process.env.HOME !== undefined) { replace(/\$\{env:HOME\}/g, process.env.HOME || ''); } diff --git a/packages/pyright-internal/src/server.ts b/packages/pyright-internal/src/server.ts index ab2d749beb9b..2af110012ef7 100644 --- a/packages/pyright-internal/src/server.ts +++ b/packages/pyright-internal/src/server.ts @@ -39,7 +39,7 @@ import { getRootUri } from './common/uri/uriUtils'; import { LanguageServerBase, ServerSettings } from './languageServerBase'; import { CodeActionProvider } from './languageService/codeActionProvider'; import { PyrightFileSystem } from './pyrightFileSystem'; -import { Workspace } from './workspaceFactory'; +import { WellKnownWorkspaceKinds, Workspace } from './workspaceFactory'; const maxAnalysisTimeInForeground = { openFilesTimeInMs: 50, noOpenFilesTimeInMs: 200 }; @@ -101,12 +101,14 @@ export class PyrightServer extends LanguageServerBase { }; try { + const workspaces = this.workspaceFactory.getNonDefaultWorkspaces(WellKnownWorkspaceKinds.Regular); + const pythonSection = await this.getConfiguration(workspace.rootUri, 'python'); if (pythonSection) { const pythonPath = pythonSection.pythonPath; if (pythonPath && isString(pythonPath) && !isPythonBinary(pythonPath)) { serverSettings.pythonPath = workspace.rootUri.resolvePaths( - expandPathVariables(workspace.rootUri, pythonPath) + expandPathVariables(pythonPath, workspace.rootUri, workspaces) ); } @@ -114,7 +116,7 @@ export class PyrightServer extends LanguageServerBase { if (venvPath && isString(venvPath)) { serverSettings.venvPath = workspace.rootUri.resolvePaths( - expandPathVariables(workspace.rootUri, venvPath) + expandPathVariables(venvPath, workspace.rootUri, workspaces) ); } } @@ -126,7 +128,7 @@ export class PyrightServer extends LanguageServerBase { const typeshedPath = typeshedPaths[0]; if (typeshedPath && isString(typeshedPath)) { serverSettings.typeshedPath = workspace.rootUri.resolvePaths( - expandPathVariables(workspace.rootUri, typeshedPath) + expandPathVariables(typeshedPath, workspace.rootUri, workspaces) ); } } @@ -134,7 +136,7 @@ export class PyrightServer extends LanguageServerBase { const stubPath = pythonAnalysisSection.stubPath; if (stubPath && isString(stubPath)) { serverSettings.stubPath = workspace.rootUri.resolvePaths( - expandPathVariables(workspace.rootUri, stubPath) + expandPathVariables(stubPath, workspace.rootUri, workspaces) ); } @@ -166,7 +168,9 @@ export class PyrightServer extends LanguageServerBase { if (extraPaths && Array.isArray(extraPaths) && extraPaths.length > 0) { serverSettings.extraPaths = extraPaths .filter((p) => p && isString(p)) - .map((p) => workspace.rootUri.resolvePaths(expandPathVariables(workspace.rootUri, p))); + .map((p) => + workspace.rootUri.resolvePaths(expandPathVariables(p, workspace.rootUri, workspaces)) + ); } serverSettings.includeFileSpecs = this._getStringValues(pythonAnalysisSection.include); diff --git a/packages/pyright-internal/src/tests/envVarUtils.test.ts b/packages/pyright-internal/src/tests/envVarUtils.test.ts new file mode 100644 index 000000000000..f368025251df --- /dev/null +++ b/packages/pyright-internal/src/tests/envVarUtils.test.ts @@ -0,0 +1,120 @@ +/* + * envVarUtils.test.ts + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * + * Unit tests for functions in envVarUtils. + */ + +import * as os from 'os'; + +import assert from 'assert'; + +import { expandPathVariables } from '../common/envVarUtils'; +import { Uri } from '../common/uri/uri'; +import { Workspace } from '../workspaceFactory'; + +jest.mock('os', () => ({ __esModule: true, ...jest.requireActual('os') })); + +test('expands ${workspaceFolder}', () => { + const workspaceFolderUri = Uri.parse('/src', true); + const test_path = '${workspaceFolder}/foo'; + const path = `${workspaceFolderUri.getPath()}/foo`; + assert.equal(expandPathVariables(test_path, workspaceFolderUri, []), path); +}); + +test('expands ${workspaceFolder:sibling}', () => { + const workspaceFolderUri = Uri.parse('/src', true); + const workspace = { workspaceName: 'sibling', rootUri: workspaceFolderUri } as Workspace; + const test_path = `\${workspaceFolder:${workspace.workspaceName}}/foo`; + const path = `${workspaceFolderUri.getPath()}/foo`; + assert.equal(expandPathVariables(test_path, workspaceFolderUri, [workspace]), path); +}); + +describe('expandPathVariables', () => { + const OLD_ENV = process.env; + + beforeEach(() => { + jest.resetModules(); + process.env = { ...OLD_ENV }; + }); + + afterAll(() => { + process.env = OLD_ENV; + }); + + test('expands ${env:HOME}', () => { + process.env.HOME = 'file:///home/foo'; + const test_path = '${env:HOME}/bar'; + const path = `${process.env.HOME}/bar`; + assert.equal(expandPathVariables(test_path, Uri.empty(), []), path); + }); + + test('expands ${env:USERNAME}', () => { + process.env.USERNAME = 'foo'; + const test_path = 'file:///home/${env:USERNAME}/bar'; + const path = `file:///home/${process.env.USERNAME}/bar`; + assert.equal(expandPathVariables(test_path, Uri.empty(), []), path); + }); + + test('expands ${env:VIRTUAL_ENV}', () => { + process.env.VIRTUAL_ENV = 'file:///home/foo/.venv/path'; + const test_path = '${env:VIRTUAL_ENV}/bar'; + const path = `${process.env.VIRTUAL_ENV}/bar`; + assert.equal(expandPathVariables(test_path, Uri.empty(), []), path); + }); + + test('expands ~ with os.homedir()', () => { + jest.spyOn(os, 'homedir').mockReturnValue('file:///home/foo'); + process.env.HOME = ''; + process.env.USERPROFILE = ''; + const test_path = '~/bar'; + const path = `${os.homedir()}/bar`; + assert.equal(expandPathVariables(test_path, Uri.empty(), []), path); + }); + + test('expands ~ with env:HOME', () => { + jest.spyOn(os, 'homedir').mockReturnValue(''); + process.env.HOME = 'file:///home/foo'; + process.env.USERPROFILE = ''; + const test_path = '~/bar'; + const path = `${process.env.HOME}/bar`; + assert.equal(expandPathVariables(test_path, Uri.empty(), []), path); + }); + + test('expands ~ with env:USERPROFILE', () => { + jest.spyOn(os, 'homedir').mockReturnValue(''); + process.env.HOME = ''; + process.env.USERPROFILE = 'file:///home/foo'; + const test_path = '~/bar'; + const path = `${process.env.USERPROFILE}/bar`; + assert.equal(expandPathVariables(test_path, Uri.empty(), []), path); + }); + + test('expands /~ with os.homedir()', () => { + jest.spyOn(os, 'homedir').mockReturnValue('file:///home/foo'); + process.env.HOME = ''; + process.env.USERPROFILE = ''; + const test_path = '/~/bar'; + const path = `${os.homedir()}/bar`; + assert.equal(expandPathVariables(test_path, Uri.empty(), []), path); + }); + + test('expands /~ with env:HOME', () => { + jest.spyOn(os, 'homedir').mockReturnValue(''); + process.env.HOME = 'file:///home/foo'; + process.env.USERPROFILE = ''; + const test_path = '/~/bar'; + const path = `${process.env.HOME}/bar`; + assert.equal(expandPathVariables(test_path, Uri.empty(), []), path); + }); + + test('expands /~ with env:USERPROFILE', () => { + jest.spyOn(os, 'homedir').mockReturnValue(''); + process.env.HOME = ''; + process.env.USERPROFILE = 'file:///home/foo'; + const test_path = '/~/bar'; + const path = `${process.env.USERPROFILE}/bar`; + assert.equal(expandPathVariables(test_path, Uri.empty(), []), path); + }); +}); diff --git a/packages/pyright-internal/src/tests/uri.test.ts b/packages/pyright-internal/src/tests/uri.test.ts index d44466dc571e..4b97d738fcf5 100644 --- a/packages/pyright-internal/src/tests/uri.test.ts +++ b/packages/pyright-internal/src/tests/uri.test.ts @@ -643,14 +643,14 @@ function getHomeDirUri() { test('resolvePath3 ~ escape', () => { assert.equal( - resolvePaths(expandPathVariables(Uri.empty(), '~/path'), 'to', '..', 'from', 'file.ext/'), + resolvePaths(expandPathVariables('~/path', Uri.empty(), []), 'to', '..', 'from', 'file.ext/'), `${getHomeDirUri().toString()}/path/from/file.ext` ); }); test('resolvePath4 ~ escape in middle', () => { assert.equal( - resolvePaths('/path', expandPathVariables(Uri.empty(), '~/file.ext/')), + resolvePaths('/path', expandPathVariables('~/file.ext/', Uri.empty(), [])), `${getHomeDirUri().toString()}/file.ext` ); }); @@ -661,12 +661,12 @@ function combinePaths(uri: string, ...paths: string[]) { test('invalid ~ without root', () => { const path = combinePaths('Library', 'Mobile Documents', 'com~apple~CloudDocs', 'Development', 'mysuperproject'); - assert.equal(resolvePaths(expandPathVariables(Uri.parse('foo:///src', true), path)), path); + assert.equal(resolvePaths(expandPathVariables(path, Uri.parse('foo:///src', true), [])), path); }); test('invalid ~ with root', () => { const path = combinePaths('/', 'Library', 'com~apple~CloudDocs', 'Development', 'mysuperproject'); - assert.equal(resolvePaths(expandPathVariables(Uri.parse('foo:///src', true), path)), path); + assert.equal(resolvePaths(expandPathVariables(path, Uri.parse('foo:///src', true), [])), path); }); function containsPath(uri: string, child: string) {