Skip to content

Commit 704dae6

Browse files
refactor resolvingWorkspaceVars and implement deprecation telemetry tracker (#841)
* Initial plan * Add comprehensive tests for resolveAndUpdatePythonPath prioritization logic Co-authored-by: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> * add tests for resolveAndUpdatePythonPath * refactor resolvingWorkspaceVars and implement deprecation telemetry tracker * refactor resolveAndUpdatePythonPath * fix item --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent 2322819 commit 704dae6

File tree

7 files changed

+170
-60
lines changed

7 files changed

+170
-60
lines changed

src/extension/debugger/configuration/providers/pyramidLaunch.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { EventName } from '../../../telemetry/constants';
1313
import { DebuggerTypeName } from '../../../constants';
1414
import { LaunchRequestArguments } from '../../../types';
1515
import { DebugConfigurationState, DebugConfigurationType } from '../../types';
16-
import { resolveVariables } from '../utils/common';
16+
import { resolveWorkspaceVariables } from '../utils/common';
1717

1818
const workspaceFolderToken = '${workspaceFolder}';
1919

@@ -73,7 +73,7 @@ export async function validateIniPath(
7373
if (!selected || selected.trim().length === 0) {
7474
return error;
7575
}
76-
const resolvedPath = resolveVariables(selected, undefined, folder);
76+
const resolvedPath = resolveWorkspaceVariables(selected, undefined, folder);
7777
if (resolvedPath) {
7878
if (selected !== defaultValue && !fs.pathExists(resolvedPath)) {
7979
return error;

src/extension/debugger/configuration/resolvers/base.ts

Lines changed: 99 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { getWorkspaceFolders, getWorkspaceFolder as getVSCodeWorkspaceFolder } f
1212
import { AttachRequestArguments, DebugOptions, LaunchRequestArguments, PathMapping } from '../../../types';
1313
import { PythonPathSource } from '../../types';
1414
import { IDebugConfigurationResolver } from '../types';
15-
import { resolveVariables } from '../utils/common';
15+
import { resolveWorkspaceVariables } from '../utils/common';
1616
import { getProgram } from './helper';
1717
import { getSettingsPythonPath, getInterpreterDetails } from '../../../common/python';
1818
import { getOSType, OSType } from '../../../common/platform';
@@ -80,6 +80,16 @@ export abstract class BaseConfigurationResolver<T extends DebugConfiguration>
8080
return undefined;
8181
}
8282

83+
/**
84+
* Resolves and updates file paths and Python interpreter paths in the debug configuration.
85+
*
86+
* This method performs two main operations:
87+
* 1. Resolves workspace variables in the envFile path (if specified)
88+
* 2. Resolves and updates Python interpreter paths, handling legacy pythonPath deprecation
89+
*
90+
* @param workspaceFolder The workspace folder URI for variable resolution
91+
* @param debugConfiguration The launch configuration to update
92+
*/
8393
protected async resolveAndUpdatePaths(
8494
workspaceFolder: Uri | undefined,
8595
debugConfiguration: LaunchRequestArguments,
@@ -88,76 +98,131 @@ export abstract class BaseConfigurationResolver<T extends DebugConfiguration>
8898
await this.resolveAndUpdatePythonPath(workspaceFolder, debugConfiguration);
8999
}
90100

101+
/**
102+
* Resolves workspace variables in the envFile path.
103+
*
104+
* Expands variables like ${workspaceFolder} in the envFile configuration using the
105+
* workspace folder path or current working directory as the base for resolution.
106+
*
107+
* @param workspaceFolder The workspace folder URI for variable resolution
108+
* @param debugConfiguration The launch configuration containing the envFile path
109+
*/
91110
protected static resolveAndUpdateEnvFilePath(
92111
workspaceFolder: Uri | undefined,
93112
debugConfiguration: LaunchRequestArguments,
94113
): void {
95-
if (!debugConfiguration) {
114+
// Early exit if no configuration or no envFile to resolve
115+
if (!debugConfiguration?.envFile) {
96116
return;
97117
}
98-
if (debugConfiguration.envFile && (workspaceFolder || debugConfiguration.cwd)) {
99-
debugConfiguration.envFile = resolveVariables(
100-
debugConfiguration.envFile,
101-
(workspaceFolder ? workspaceFolder.fsPath : undefined) || debugConfiguration.cwd,
102-
undefined,
103-
);
118+
119+
const basePath = workspaceFolder?.fsPath || debugConfiguration.cwd;
120+
121+
if (basePath) {
122+
// update envFile with resolved variables
123+
debugConfiguration.envFile = resolveWorkspaceVariables(debugConfiguration.envFile, basePath, undefined);
104124
}
105125
}
106126

127+
/**
128+
* Resolves Python interpreter paths and handles the legacy pythonPath deprecation.
129+
*
130+
* @param workspaceFolder The workspace folder URI for variable resolution and interpreter detection
131+
* @param debugConfiguration The launch configuration to update with resolved Python paths
132+
*/
107133
protected async resolveAndUpdatePythonPath(
108134
workspaceFolder: Uri | undefined,
109135
debugConfiguration: LaunchRequestArguments,
110136
): Promise<void> {
111137
if (!debugConfiguration) {
112138
return;
113139
}
140+
141+
// get the interpreter details in the context of the workspace folder
142+
const interpreterDetail = await getInterpreterDetails(workspaceFolder);
143+
const interpreterPath = interpreterDetail?.path ?? (await getSettingsPythonPath(workspaceFolder));
144+
const resolvedInterpreterPath = interpreterPath ? interpreterPath[0] : interpreterPath;
145+
146+
traceLog(
147+
`resolveAndUpdatePythonPath - Initial state: ` +
148+
`pythonPath='${debugConfiguration.pythonPath}', ` +
149+
`python='${debugConfiguration.python}', ` +
150+
`debugAdapterPython='${debugConfiguration.debugAdapterPython}', ` +
151+
`debugLauncherPython='${debugConfiguration.debugLauncherPython}', ` +
152+
`workspaceFolder='${workspaceFolder?.fsPath}'` +
153+
`resolvedInterpreterPath='${resolvedInterpreterPath}'`,
154+
);
155+
156+
// STEP 1: Resolve legacy pythonPath property (DEPRECATED)
157+
// pythonPath will equal user set value, or getInterpreterDetails if undefined or set to command
114158
if (debugConfiguration.pythonPath === '${command:python.interpreterPath}' || !debugConfiguration.pythonPath) {
115-
const interpreterDetail = await getInterpreterDetails(workspaceFolder);
116-
const interpreterPath = interpreterDetail
117-
? interpreterDetail.path
118-
: await getSettingsPythonPath(workspaceFolder);
119-
debugConfiguration.pythonPath = interpreterPath ? interpreterPath[0] : interpreterPath;
159+
this.pythonPathSource = PythonPathSource.settingsJson;
160+
debugConfiguration.pythonPath = resolvedInterpreterPath;
120161
} else {
121-
debugConfiguration.pythonPath = resolveVariables(
122-
debugConfiguration.pythonPath ? debugConfiguration.pythonPath : undefined,
162+
// User provided explicit pythonPath in launch.json
163+
debugConfiguration.pythonPath = resolveWorkspaceVariables(
164+
debugConfiguration.pythonPath,
123165
workspaceFolder?.fsPath,
124166
undefined,
125167
);
126168
}
127169

170+
// STEP 2: Resolve current python property (CURRENT STANDARD)
128171
if (debugConfiguration.python === '${command:python.interpreterPath}') {
172+
// if python is set to the command, resolve it
129173
this.pythonPathSource = PythonPathSource.settingsJson;
130-
const interpreterDetail = await getInterpreterDetails(workspaceFolder);
131-
const interpreterPath = interpreterDetail.path
132-
? interpreterDetail.path
133-
: await getSettingsPythonPath(workspaceFolder);
134-
debugConfiguration.python = interpreterPath ? interpreterPath[0] : interpreterPath;
135-
} else if (debugConfiguration.python === undefined) {
174+
debugConfiguration.python = resolvedInterpreterPath;
175+
} else if (!debugConfiguration.python) {
176+
// fallback to pythonPath if python undefined
136177
this.pythonPathSource = PythonPathSource.settingsJson;
137178
debugConfiguration.python = debugConfiguration.pythonPath;
138179
} else {
180+
// User provided explicit python path in launch.json
139181
this.pythonPathSource = PythonPathSource.launchJson;
140-
debugConfiguration.python = resolveVariables(
141-
debugConfiguration.python ?? debugConfiguration.pythonPath,
182+
debugConfiguration.python = resolveWorkspaceVariables(
183+
debugConfiguration.python,
142184
workspaceFolder?.fsPath,
143185
undefined,
144186
);
145187
}
146188

147-
if (
189+
// STEP 3: Set debug adapter and launcher Python paths (backwards compatible)
190+
this.setDebugComponentPythonPaths(debugConfiguration);
191+
192+
// STEP 4: Clean up - remove the deprecated pythonPath property
193+
delete debugConfiguration.pythonPath;
194+
}
195+
196+
/**
197+
* Sets debugAdapterPython and debugLauncherPython with backwards compatibility.
198+
* Prefers pythonPath over python for these internal properties.
199+
*
200+
* @param debugConfiguration The debug configuration to update
201+
*/
202+
private setDebugComponentPythonPaths(debugConfiguration: LaunchRequestArguments): void {
203+
const shouldSetDebugAdapter =
148204
debugConfiguration.debugAdapterPython === '${command:python.interpreterPath}' ||
149-
debugConfiguration.debugAdapterPython === undefined
150-
) {
151-
debugConfiguration.debugAdapterPython = debugConfiguration.pythonPath ?? debugConfiguration.python;
152-
}
153-
if (
205+
debugConfiguration.debugAdapterPython === undefined;
206+
207+
const shouldSetDebugLauncher =
154208
debugConfiguration.debugLauncherPython === '${command:python.interpreterPath}' ||
155-
debugConfiguration.debugLauncherPython === undefined
156-
) {
157-
debugConfiguration.debugLauncherPython = debugConfiguration.pythonPath ?? debugConfiguration.python;
209+
debugConfiguration.debugLauncherPython === undefined;
210+
211+
// Default fallback path (prefer pythonPath for backwards compatibility)
212+
const fallbackPath = debugConfiguration.pythonPath ?? debugConfiguration.python;
213+
214+
if (debugConfiguration.pythonPath !== debugConfiguration.python) {
215+
sendTelemetryEvent(EventName.DEPRECATED_CODE_PATH_USAGE, undefined, {
216+
codePath: 'different_python_paths_in_debug_config',
217+
});
158218
}
159219

160-
delete debugConfiguration.pythonPath;
220+
if (shouldSetDebugAdapter) {
221+
debugConfiguration.debugAdapterPython = fallbackPath;
222+
}
223+
if (shouldSetDebugLauncher) {
224+
debugConfiguration.debugLauncherPython = fallbackPath;
225+
}
161226
}
162227

163228
protected static debugOption(debugOptions: DebugOptions[], debugOption: DebugOptions): void {
@@ -194,7 +259,7 @@ export abstract class BaseConfigurationResolver<T extends DebugConfiguration>
194259
} else {
195260
// Expand ${workspaceFolder} variable first if necessary.
196261
pathMappings = pathMappings.map(({ localRoot: mappedLocalRoot, remoteRoot }) => {
197-
const resolvedLocalRoot = resolveVariables(mappedLocalRoot, defaultLocalRoot, undefined);
262+
const resolvedLocalRoot = resolveWorkspaceVariables(mappedLocalRoot, defaultLocalRoot, undefined);
198263
return {
199264
localRoot: resolvedLocalRoot || '',
200265
// TODO: Apply to remoteRoot too?

src/extension/debugger/configuration/resolvers/launch.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { getOSType, OSType } from '../../../common/platform';
88
import { getEnvFile } from '../../../common/settings';
99
import { DebuggerTypeName } from '../../../constants';
1010
import { DebugOptions, DebugPurpose, LaunchRequestArguments } from '../../../types';
11-
import { resolveVariables } from '../utils/common';
11+
import { resolveWorkspaceVariables } from '../utils/common';
1212
import { BaseConfigurationResolver } from './base';
1313
import { getDebugEnvironmentVariables, getProgram } from './helper';
1414
import { getConfiguration } from '../../../common/vscodeapi';
@@ -83,7 +83,7 @@ export class LaunchConfigurationResolver extends BaseConfigurationResolver<Launc
8383
debugConfiguration.cwd = workspaceFolder.fsPath;
8484
}
8585
if (typeof debugConfiguration.envFile !== 'string' && workspaceFolder) {
86-
debugConfiguration.envFile = resolveVariables(
86+
debugConfiguration.envFile = resolveWorkspaceVariables(
8787
getEnvFile('python', workspaceFolder),
8888
workspaceFolder.fsPath,
8989
undefined,

src/extension/debugger/configuration/utils/common.ts

Lines changed: 47 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,36 +8,63 @@
88

99
import { Uri, WorkspaceFolder } from 'vscode';
1010
import { getWorkspaceFolder } from '../../../common/vscodeapi';
11+
import { sendTelemetryEvent } from '../../../telemetry';
12+
import { EventName } from '../../../telemetry/constants';
1113

1214
/**
1315
* @returns whether the provided parameter is a JavaScript String or not.
1416
*/
1517
function isString(str: any): str is string {
16-
if (typeof str === 'string' || str instanceof String) {
17-
return true;
18-
}
19-
20-
return false;
18+
return typeof str === 'string' || str instanceof String;
2119
}
2220

23-
export function resolveVariables(
21+
/**
22+
* Resolves VS Code variable placeholders in a string value.
23+
*
24+
* Specifically handles:
25+
* - `${workspaceFolder}` - replaced with the workspace folder path
26+
* - `${env.VAR}` or `${env:VAR}` - replaced with empty string
27+
* - Unknown variables - left as-is in the original `${variable}` format
28+
*
29+
* @param value The string containing variable placeholders to resolve
30+
* @param rootFolder Fallback folder path to use if no workspace folder is available
31+
* @param folder The workspace folder context for variable resolution
32+
* @returns The string with variables resolved, or undefined if input was undefined
33+
*/
34+
export function resolveWorkspaceVariables(
2435
value: string | undefined,
2536
rootFolder: string | Uri | undefined,
2637
folder: WorkspaceFolder | undefined,
2738
): string | undefined {
28-
if (value) {
29-
const workspaceFolder = folder ? getWorkspaceFolder(folder.uri) : undefined;
30-
const variablesObject: { [key: string]: any } = {};
31-
variablesObject.workspaceFolder = workspaceFolder ? workspaceFolder.uri.fsPath : rootFolder;
32-
33-
const regexp = /\$\{(.*?)\}/g;
34-
return value.replace(regexp, (match: string, name: string) => {
35-
const newValue = variablesObject[name];
36-
if (isString(newValue)) {
37-
return newValue;
38-
}
39-
return match && (match.indexOf('env.') > 0 || match.indexOf('env:') > 0) ? '' : match;
40-
});
39+
if (!value) {
40+
return value;
4141
}
42-
return value;
42+
43+
// opt for folder with fallback to rootFolder
44+
const workspaceFolder = folder ? getWorkspaceFolder(folder.uri) : undefined;
45+
const workspaceFolderPath = workspaceFolder ? workspaceFolder.uri.fsPath : rootFolder;
46+
47+
// Replace all ${variable} patterns
48+
return value.replace(/\$\{([^}]+)\}/g, (match: string, variableName: string) => {
49+
// Handle workspaceFolder variable
50+
if (variableName === 'workspaceFolder' && isString(workspaceFolderPath)) {
51+
// Track usage of this potentially deprecated code path
52+
sendTelemetryEvent(EventName.DEPRECATED_CODE_PATH_USAGE, undefined, {
53+
codePath: 'workspaceFolder_substitution',
54+
});
55+
return workspaceFolderPath;
56+
}
57+
58+
// Replace environment variables with empty string
59+
if (variableName.startsWith('env.') || variableName.startsWith('env:')) {
60+
// Track usage of this potentially deprecated code path
61+
sendTelemetryEvent(EventName.DEPRECATED_CODE_PATH_USAGE, undefined, {
62+
codePath: 'env_variable_substitution',
63+
});
64+
return '';
65+
}
66+
67+
// Unknown variables are left unchanged
68+
return match;
69+
});
4370
}

src/extension/telemetry/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,5 @@ export enum EventName {
2424
USE_REPORT_ISSUE_COMMAND = 'USE_REPORT_ISSUE_COMMAND',
2525
DEBUGGER_PYTHON_37_DEPRECATED = 'DEBUGGER_PYTHON_37_DEPRECATED',
2626
DEBUGGER_SHOW_PYTHON_INLINE_VALUES = 'DEBUGGER_SHOW_PYTHON_INLINE_VALUES',
27+
DEPRECATED_CODE_PATH_USAGE = 'DEPRECATED_CODE_PATH_USAGE',
2728
}

src/extension/telemetry/index.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -691,4 +691,21 @@ export interface IEventNamePropertyMapping {
691691
"DEBUGGER_SHOW_PYTHON_INLINE_VALUES" : { "owner": "eleanorjboyd" }
692692
*/
693693
[EventName.DEBUGGER_SHOW_PYTHON_INLINE_VALUES]: never | undefined;
694+
/**
695+
* Telemetry event sent when potentially deprecated code paths are executed.
696+
*/
697+
/* __GDPR__
698+
"deprecated_code_path_usage" : {
699+
"codepath" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }
700+
}
701+
*/
702+
[EventName.DEPRECATED_CODE_PATH_USAGE]: {
703+
/**
704+
* Identifier for the specific deprecated code path that was executed.
705+
* Examples: 'workspaceFolder_substitution', 'env_variable_substitution'
706+
*
707+
* @type {string}
708+
*/
709+
codePath: string;
710+
};
694711
}

src/test/unittest/configuration/providers/pyramidLaunch.unit.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ import { Uri } from 'vscode';
1212
import { DebugConfigStrings } from '../../../../extension/common/utils/localize';
1313
import { MultiStepInput } from '../../../../extension/common/multiStepInput';
1414
import { DebuggerTypeName } from '../../../../extension/constants';
15-
import { resolveVariables } from '../../../../extension/debugger/configuration/utils/common';
1615
import * as pyramidLaunch from '../../../../extension/debugger/configuration/providers/pyramidLaunch';
1716
import { DebugConfigurationState } from '../../../../extension/debugger/types';
1817
import * as vscodeapi from '../../../../extension/common/vscodeapi';
18+
import { resolveWorkspaceVariables } from '../../../../extension/debugger/configuration/utils/common';
1919

2020
suite('Debugging - Configuration Provider Pyramid', () => {
2121
let input: MultiStepInput<DebugConfigurationState>;
@@ -52,7 +52,7 @@ suite('Debugging - Configuration Provider Pyramid', () => {
5252
test('Resolve variables (with resource)', async () => {
5353
const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 };
5454
workspaceStub.returns(folder);
55-
const resolvedPath = resolveVariables('${workspaceFolder}/one.py', undefined, folder);
55+
const resolvedPath = resolveWorkspaceVariables('${workspaceFolder}/one.py', undefined, folder);
5656

5757
expect(resolvedPath).to.be.equal(`${folder.uri.fsPath}/one.py`);
5858
});

0 commit comments

Comments
 (0)