debug: fix unexpected behaviors with duplicate `name`s in `launch.json` (#236513)

Suffix duplicated launch configs with a config at the time they're read. In debug we assume the names are unique, so this should fix #231377 and probably other hidden issues as well.
pull/235672/head
Connor Peet 2024-12-18 12:39:57 -08:00 committed by GitHub
parent 1147139fbb
commit e6e5856995
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 39 additions and 22 deletions

View File

@ -10,7 +10,6 @@ import { Emitter, Event } from '../../../../base/common/event.js';
import * as json from '../../../../base/common/json.js';
import { IJSONSchema } from '../../../../base/common/jsonSchema.js';
import { DisposableStore, IDisposable, dispose } from '../../../../base/common/lifecycle.js';
import * as objects from '../../../../base/common/objects.js';
import * as resources from '../../../../base/common/resources.js';
import { ThemeIcon } from '../../../../base/common/themables.js';
import { URI as uri } from '../../../../base/common/uri.js';
@ -27,16 +26,16 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo
import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';
import { IWorkspaceContextService, IWorkspaceFolder, IWorkspaceFoldersChangeEvent, WorkbenchState } from '../../../../platform/workspace/common/workspace.js';
import { IEditorPane } from '../../../common/editor.js';
import { debugConfigure } from './debugIcons.js';
import { CONTEXT_DEBUG_CONFIGURATION_TYPE, DebugConfigurationProviderTriggerKind, IAdapterManager, ICompound, IConfig, IConfigPresentation, IConfigurationManager, IDebugConfigurationProvider, IGlobalConfig, IGuessedDebugger, ILaunch } from '../common/debug.js';
import { launchSchema } from '../common/debugSchemas.js';
import { getVisibleAndSorted } from '../common/debugUtils.js';
import { launchSchemaId } from '../../../services/configuration/common/configuration.js';
import { ACTIVE_GROUP, IEditorService } from '../../../services/editor/common/editorService.js';
import { IExtensionService } from '../../../services/extensions/common/extensions.js';
import { IHistoryService } from '../../../services/history/common/history.js';
import { IPreferencesService } from '../../../services/preferences/common/preferences.js';
import { ITextFileService } from '../../../services/textfile/common/textfiles.js';
import { CONTEXT_DEBUG_CONFIGURATION_TYPE, DebugConfigurationProviderTriggerKind, IAdapterManager, ICompound, IConfig, IConfigPresentation, IConfigurationManager, IDebugConfigurationProvider, IGlobalConfig, IGuessedDebugger, ILaunch } from '../common/debug.js';
import { launchSchema } from '../common/debugSchemas.js';
import { getVisibleAndSorted } from '../common/debugUtils.js';
import { debugConfigure } from './debugIcons.js';
const jsonRegistry = Registry.as<IJSONContributionRegistry>(JSONExtensions.JSONContribution);
jsonRegistry.registerSchema(launchSchemaId, launchSchema);
@ -509,7 +508,7 @@ abstract class AbstractLaunch implements ILaunch {
) { }
getCompound(name: string): ICompound | undefined {
const config = this.getConfig();
const config = this.getDeduplicatedConfig();
if (!config || !config.compounds) {
return undefined;
}
@ -518,7 +517,7 @@ abstract class AbstractLaunch implements ILaunch {
}
getConfigurationNames(ignoreCompoundsAndPresentation = false): string[] {
const config = this.getConfig();
const config = this.getDeduplicatedConfig();
if (!config || (!Array.isArray(config.configurations) && !Array.isArray(config.compounds))) {
return [];
} else {
@ -540,21 +539,22 @@ abstract class AbstractLaunch implements ILaunch {
getConfiguration(name: string): IConfig | undefined {
// We need to clone the configuration in order to be able to make changes to it #42198
const config = objects.deepClone(this.getConfig());
const config = this.getDeduplicatedConfig();
if (!config || !config.configurations) {
return undefined;
}
const configuration = config.configurations.find(config => config && config.name === name);
if (configuration) {
if (this instanceof UserLaunch) {
configuration.__configurationTarget = ConfigurationTarget.USER;
} else if (this instanceof WorkspaceLaunch) {
configuration.__configurationTarget = ConfigurationTarget.WORKSPACE;
} else {
configuration.__configurationTarget = ConfigurationTarget.WORKSPACE_FOLDER;
}
if (!configuration) {
return;
}
if (this instanceof UserLaunch) {
return { ...configuration, __configurationTarget: ConfigurationTarget.USER };
} else if (this instanceof WorkspaceLaunch) {
return { ...configuration, __configurationTarget: ConfigurationTarget.WORKSPACE };
} else {
return { ...configuration, __configurationTarget: ConfigurationTarget.WORKSPACE_FOLDER };
}
return configuration;
}
async getInitialConfigurationContent(folderUri?: uri, type?: string, useInitialConfigs?: boolean, token?: CancellationToken): Promise<string> {
@ -575,9 +575,28 @@ abstract class AbstractLaunch implements ILaunch {
return content;
}
get hidden(): boolean {
return false;
}
private getDeduplicatedConfig(): IGlobalConfig | undefined {
const original = this.getConfig();
return original && {
version: original.version,
compounds: original.compounds && distinguishConfigsByName(original.compounds),
configurations: original.configurations && distinguishConfigsByName(original.configurations),
};
}
}
function distinguishConfigsByName<T extends { name: string }>(things: readonly T[]): T[] {
const seen = new Map<string, number>();
return things.map(thing => {
const no = seen.get(thing.name) || 0;
seen.set(thing.name, no + 1);
return no === 0 ? thing : { ...thing, name: `${thing.name} (${no})` };
});
}
class Launch extends AbstractLaunch implements ILaunch {
@ -655,11 +674,9 @@ class Launch extends AbstractLaunch implements ILaunch {
}
async writeConfiguration(configuration: IConfig): Promise<void> {
const fullConfig = objects.deepClone(this.getConfig()!);
if (!fullConfig.configurations) {
fullConfig.configurations = [];
}
fullConfig.configurations.push(configuration);
// note: we don't get the deduplicated config since we don't want that to 'leak' into the file
const fullConfig: Partial<IGlobalConfig> = this.getConfig() || {};
fullConfig.configurations = [...fullConfig.configurations || [], configuration];
await this.configurationService.updateValue('launch', fullConfig, { resource: this.workspace.uri }, ConfigurationTarget.WORKSPACE_FOLDER);
}
}