Skip to content

Commit 3a2cb48

Browse files
Copiloteleanorjboyd
andcommitted
Add validation for pythonProjects settings with user notifications
Co-authored-by: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com>
1 parent 51b67ed commit 3a2cb48

2 files changed

Lines changed: 212 additions & 0 deletions

File tree

src/features/projectManager.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
getDefaultEnvManagerSetting,
1717
getDefaultPkgManagerSetting,
1818
} from './settings/settingHelpers';
19+
import { validateAndNotifyPythonProjectsSettings } from './settings/settingsValidation';
1920

2021
type ProjectArray = PythonProject[];
2122

@@ -30,6 +31,12 @@ export class PythonProjectManagerImpl implements PythonProjectManager {
3031

3132
initialize(): void {
3233
this.add(this.getInitialProjects());
34+
35+
// Validate pythonProjects settings on initialization
36+
validateAndNotifyPythonProjectsSettings().catch(() => {
37+
// Error is already logged in the validation function
38+
});
39+
3340
this.disposables.push(
3441
this._onDidChangeProjects,
3542
new Disposable(() => this._projects.clear()),
@@ -43,6 +50,13 @@ export class PythonProjectManagerImpl implements PythonProjectManager {
4350
e.affectsConfiguration('python-envs.defaultPackageManager')
4451
) {
4552
this.updateDebounce.trigger();
53+
54+
// Validate settings when pythonProjects configuration changes
55+
if (e.affectsConfiguration('python-envs.pythonProjects')) {
56+
validateAndNotifyPythonProjectsSettings().catch(() => {
57+
// Error is already logged in the validation function
58+
});
59+
}
4660
}
4761
}),
4862
);
@@ -65,8 +79,19 @@ export class PythonProjectManagerImpl implements PythonProjectManager {
6579
newProjects.push(new PythonProjectsImpl(w.name, w.uri));
6680
}
6781

82+
// Validate that overrides is an array
83+
if (!Array.isArray(overrides)) {
84+
// Skip processing if the setting is invalid
85+
continue;
86+
}
87+
6888
// For each override, resolve its path and add as a project if not already present
6989
for (const o of overrides) {
90+
// Skip invalid entries (missing required fields)
91+
if (!o || typeof o !== 'object' || !o.path || !o.envManager || !o.packageManager) {
92+
continue;
93+
}
94+
7095
let uriFromWorkspace: Uri | undefined = undefined;
7196
// if override has a workspace property, resolve the path relative to that workspace
7297
if (o.workspace) {
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { ConfigurationTarget, l10n, Uri, workspace } from 'vscode';
5+
import { traceError, traceWarn } from '../../common/logging';
6+
import { getGlobalPersistentState } from '../../common/persistentState';
7+
import { showWarningMessage } from '../../common/window.apis';
8+
import { getConfiguration, getWorkspaceFolders } from '../../common/workspace.apis';
9+
import { PythonProjectSettings } from '../../internal.api';
10+
11+
const DONT_SHOW_INVALID_SETTINGS_KEY = 'dontShowInvalidPythonProjectSettings';
12+
13+
interface InvalidProjectEntry {
14+
entry: PythonProjectSettings;
15+
workspaceUri: Uri;
16+
reason: string;
17+
}
18+
19+
/**
20+
* Validates a single PythonProjectSettings entry
21+
* @param entry The settings entry to validate
22+
* @returns An error message if invalid, undefined if valid
23+
*/
24+
function validateProjectEntry(entry: PythonProjectSettings): string | undefined {
25+
// Check if required fields exist
26+
if (!entry.path) {
27+
return l10n.t('Missing required field: path');
28+
}
29+
30+
if (!entry.envManager) {
31+
return l10n.t('Missing required field: envManager');
32+
}
33+
34+
if (!entry.packageManager) {
35+
return l10n.t('Missing required field: packageManager');
36+
}
37+
38+
// Check if entry is a valid object (not a string or other primitive)
39+
if (typeof entry !== 'object') {
40+
return l10n.t('Invalid entry format: expected object');
41+
}
42+
43+
return undefined;
44+
}
45+
46+
/**
47+
* Validates all pythonProjects settings across all workspaces
48+
* @returns Array of invalid entries with their details
49+
*/
50+
export function validatePythonProjectsSettings(): InvalidProjectEntry[] {
51+
const invalidEntries: InvalidProjectEntry[] = [];
52+
const workspaces = getWorkspaceFolders() ?? [];
53+
54+
for (const workspace of workspaces) {
55+
const config = getConfiguration('python-envs', workspace.uri);
56+
const projectSettings = config.get<PythonProjectSettings[]>('pythonProjects', []);
57+
58+
// Check if the setting is valid (should be an array)
59+
if (!Array.isArray(projectSettings)) {
60+
traceError(
61+
`Invalid pythonProjects setting in workspace ${workspace.name}: expected array, got ${typeof projectSettings}`,
62+
);
63+
invalidEntries.push({
64+
entry: projectSettings as unknown as PythonProjectSettings,
65+
workspaceUri: workspace.uri,
66+
reason: l10n.t('Invalid format: pythonProjects must be an array'),
67+
});
68+
continue;
69+
}
70+
71+
// Validate each entry
72+
for (const entry of projectSettings) {
73+
const error = validateProjectEntry(entry);
74+
if (error) {
75+
traceWarn(`Invalid pythonProjects entry in workspace ${workspace.name}:`, entry, error);
76+
invalidEntries.push({
77+
entry,
78+
workspaceUri: workspace.uri,
79+
reason: error,
80+
});
81+
}
82+
}
83+
}
84+
85+
return invalidEntries;
86+
}
87+
88+
/**
89+
* Shows a warning notification about invalid pythonProjects settings
90+
* Provides options to remove the invalid entry, open settings, or don't show again
91+
* @param invalidEntries Array of invalid entries found
92+
*/
93+
export async function notifyInvalidPythonProjectsSettings(invalidEntries: InvalidProjectEntry[]): Promise<void> {
94+
if (invalidEntries.length === 0) {
95+
return;
96+
}
97+
98+
// Check if user has chosen to not show this warning again
99+
const persistentState = await getGlobalPersistentState();
100+
const dontShowAgain = await persistentState.get<boolean>(DONT_SHOW_INVALID_SETTINGS_KEY, false);
101+
102+
if (dontShowAgain) {
103+
traceWarn('Not showing invalid pythonProjects settings warning due to user preference');
104+
return;
105+
}
106+
107+
// Create a descriptive message
108+
const count = invalidEntries.length;
109+
const message =
110+
count === 1
111+
? l10n.t(
112+
'Found an invalid entry in python-envs.pythonProjects settings. This may cause issues with project detection.',
113+
)
114+
: l10n.t(
115+
'Found {0} invalid entries in python-envs.pythonProjects settings. This may cause issues with project detection.',
116+
count,
117+
);
118+
119+
// Show warning with options
120+
const removeOption = l10n.t('Remove Invalid Entry');
121+
const openSettingsOption = l10n.t('Open Settings');
122+
const dontShowOption = l10n.t("Don't Show Again");
123+
124+
const choice = await showWarningMessage(message, removeOption, openSettingsOption, dontShowOption);
125+
126+
if (choice === removeOption) {
127+
// Remove the first invalid entry (or all if multiple)
128+
try {
129+
// Group by workspace to batch updates
130+
const byWorkspace = new Map<string, InvalidProjectEntry[]>();
131+
for (const invalid of invalidEntries) {
132+
const key = invalid.workspaceUri.toString();
133+
if (!byWorkspace.has(key)) {
134+
byWorkspace.set(key, []);
135+
}
136+
byWorkspace.get(key)!.push(invalid);
137+
}
138+
139+
// Remove invalid entries from each workspace
140+
for (const [workspaceKey, entries] of byWorkspace) {
141+
const workspaceUri = Uri.parse(workspaceKey);
142+
const config = getConfiguration('python-envs', workspaceUri);
143+
let projectSettings = config.get<PythonProjectSettings[]>('pythonProjects', []);
144+
145+
if (!Array.isArray(projectSettings)) {
146+
await config.update('pythonProjects', [], ConfigurationTarget.Workspace);
147+
continue;
148+
}
149+
150+
// Filter out all invalid entries
151+
const invalidJsonStrings = new Set(entries.map((e) => JSON.stringify(e.entry)));
152+
projectSettings = projectSettings.filter((e) => !invalidJsonStrings.has(JSON.stringify(e)));
153+
154+
await config.update('pythonProjects', projectSettings, ConfigurationTarget.Workspace);
155+
}
156+
157+
traceWarn(`Removed ${invalidEntries.length} invalid pythonProjects entries`);
158+
} catch (error) {
159+
traceError('Failed to remove invalid entries:', error);
160+
await showWarningMessage(l10n.t('Failed to remove invalid entries. Please check the logs for details.'));
161+
}
162+
} else if (choice === openSettingsOption) {
163+
// Open settings for the user to manually fix
164+
await workspace
165+
.getConfiguration('python-envs')
166+
.update('pythonProjects', undefined, ConfigurationTarget.WorkspaceFolder, false);
167+
} else if (choice === dontShowOption) {
168+
// Save preference to not show again
169+
await persistentState.set(DONT_SHOW_INVALID_SETTINGS_KEY, true);
170+
traceWarn('User chose to not show invalid pythonProjects settings warning again');
171+
}
172+
}
173+
174+
/**
175+
* Validates pythonProjects settings and shows a notification if invalid entries are found
176+
* Should be called when the extension initializes or when settings change
177+
*/
178+
export async function validateAndNotifyPythonProjectsSettings(): Promise<void> {
179+
try {
180+
const invalidEntries = validatePythonProjectsSettings();
181+
if (invalidEntries.length > 0) {
182+
await notifyInvalidPythonProjectsSettings(invalidEntries);
183+
}
184+
} catch (error) {
185+
traceError('Error validating pythonProjects settings:', error);
186+
}
187+
}

0 commit comments

Comments
 (0)