Make type signature of groupBy more accurate (#272395)

See #272263, it currently assumes that all keys of K will be defined. This still isn't perfect, because it says that the returned object might contain a key with value `undefined`, but in reality, if a key is defined, it will have a value. So now usages of this with Object.entries and so on are slighly wrong, but that's preferable IMO.
This commit is contained in:
Rob Lourens
2025-10-20 21:13:56 -07:00
committed by GitHub
parent 0809a8ebdb
commit b9480e9d7d
9 changed files with 24 additions and 16 deletions

View File

@@ -19,8 +19,8 @@ export type INumberDictionary<V> = Record<number, V>;
* Groups the collection into a dictionary based on the provided * Groups the collection into a dictionary based on the provided
* group function. * group function.
*/ */
export function groupBy<K extends string | number | symbol, V>(data: readonly V[], groupFn: (element: V) => K): Record<K, V[]> { export function groupBy<K extends string | number | symbol, V>(data: readonly V[], groupFn: (element: V) => K): Partial<Record<K, V[]>> {
const result: Record<K, V[]> = Object.create(null); const result: Partial<Record<K, V[]>> = Object.create(null);
for (const element of data) { for (const element of data) {
const key = groupFn(element); const key = groupFn(element);
let target = result[key]; let target = result[key];

View File

@@ -206,7 +206,9 @@ export class DisposableTracker implements IDisposableTracker {
const continuations = groupBy([...prevStarts].map(d => getStackTracePath(d)[i]), v => v); const continuations = groupBy([...prevStarts].map(d => getStackTracePath(d)[i]), v => v);
delete continuations[stackTracePath[i]]; delete continuations[stackTracePath[i]];
for (const [cont, set] of Object.entries(continuations)) { for (const [cont, set] of Object.entries(continuations)) {
stackTraceFormattedLines.unshift(` - stacktraces of ${set.length} other leaks continue with ${cont}`); if (set) {
stackTraceFormattedLines.unshift(` - stacktraces of ${set.length} other leaks continue with ${cont}`);
}
} }
stackTraceFormattedLines.unshift(line); stackTraceFormattedLines.unshift(line);

View File

@@ -24,12 +24,12 @@ suite('Collections', () => {
const grouped = collections.groupBy(source, x => x.key); const grouped = collections.groupBy(source, x => x.key);
// Group 1 // Group 1
assert.strictEqual(grouped[group1].length, 2); assert.strictEqual(grouped[group1]?.length, 2);
assert.strictEqual(grouped[group1][0].value, value1); assert.strictEqual(grouped[group1][0].value, value1);
assert.strictEqual(grouped[group1][1].value, value2); assert.strictEqual(grouped[group1][1].value, value2);
// Group 2 // Group 2
assert.strictEqual(grouped[group2].length, 1); assert.strictEqual(grouped[group2]?.length, 1);
assert.strictEqual(grouped[group2][0].value, value3); assert.strictEqual(grouped[group2][0].value, value3);
}); });

View File

@@ -79,11 +79,11 @@ export class ModePickerActionItem extends ActionWidgetDropdownActionViewItem {
mode => mode.source?.storage === PromptsStorage.extension && mode.source.extensionId.value === productService.defaultChatAgent?.chatExtensionId ? mode => mode.source?.storage === PromptsStorage.extension && mode.source.extensionId.value === productService.defaultChatAgent?.chatExtensionId ?
'builtin' : 'custom'); 'builtin' : 'custom');
const customBuiltinModeActions = customModes.builtin.map(mode => { const customBuiltinModeActions = customModes.builtin?.map(mode => {
const action = makeActionFromCustomMode(mode, currentMode); const action = makeActionFromCustomMode(mode, currentMode);
action.category = builtInCategory; action.category = builtInCategory;
return action; return action;
}); }) ?? [];
const orderedModes = coalesce([ const orderedModes = coalesce([
agentMode && makeAction(agentMode, currentMode), agentMode && makeAction(agentMode, currentMode),

View File

@@ -410,9 +410,9 @@ export class BaseIssueReporterService extends Disposable {
return ext.isTheme ? 'themes' : 'nonThemes'; return ext.isTheme ? 'themes' : 'nonThemes';
}); });
const numberOfThemeExtesions = themes && themes.length; const numberOfThemeExtesions = (themes && themes.length) ?? 0;
this.issueReporterModel.update({ numberOfThemeExtesions, enabledNonThemeExtesions: nonThemes, allExtensions: installedExtensions }); this.issueReporterModel.update({ numberOfThemeExtesions, enabledNonThemeExtesions: nonThemes, allExtensions: installedExtensions });
this.updateExtensionTable(nonThemes, numberOfThemeExtesions); this.updateExtensionTable(nonThemes ?? [], numberOfThemeExtesions);
if (this.disableExtensions || installedExtensions.length === 0) { if (this.disableExtensions || installedExtensions.length === 0) {
(<HTMLButtonElement>this.getElementById('disableExtensions')).disabled = true; (<HTMLButtonElement>this.getElementById('disableExtensions')).disabled = true;
} }

View File

@@ -19,9 +19,11 @@ class TestMarkersModel extends MarkersModel {
Object.keys(byResource).forEach(key => { Object.keys(byResource).forEach(key => {
const markers = byResource[key]; const markers = byResource[key];
const resource = markers[0].resource; if (markers) {
const resource = markers[0].resource;
this.setResourceMarkers([[resource, markers]]); this.setResourceMarkers([[resource, markers]]);
}
}); });
} }
} }

View File

@@ -117,9 +117,9 @@ export class ListMcpServerCommand extends Action2 {
const firstRun = pick.items.length === 0; const firstRun = pick.items.length === 0;
pick.items = [ pick.items = [
{ id: '$add', label: localize('mcp.addServer', 'Add Server'), description: localize('mcp.addServer.description', 'Add a new server configuration'), alwaysShow: true, iconClass: ThemeIcon.asClassName(Codicon.add) }, { id: '$add', label: localize('mcp.addServer', 'Add Server'), description: localize('mcp.addServer.description', 'Add a new server configuration'), alwaysShow: true, iconClass: ThemeIcon.asClassName(Codicon.add) },
...Object.values(servers).filter(s => s.length).flatMap((servers): (ItemType | IQuickPickSeparator)[] => [ ...Object.values(servers).filter(s => s!.length).flatMap((servers): (ItemType | IQuickPickSeparator)[] => [
{ type: 'separator', label: servers[0].collection.label, id: servers[0].collection.id }, { type: 'separator', label: servers![0].collection.label, id: servers![0].collection.id },
...servers.map(server => ({ ...servers!.map(server => ({
id: server.definition.id, id: server.definition.id,
label: server.definition.label, label: server.definition.label,
description: McpConnectionState.toString(server.connectionState.read(reader)), description: McpConnectionState.toString(server.connectionState.read(reader)),

View File

@@ -778,7 +778,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD
for (const _handle in deletesByHandle) { for (const _handle in deletesByHandle) {
const handle = parseInt(_handle); const handle = parseInt(_handle);
const ids = deletesByHandle[handle]; const ids = deletesByHandle[handle]!;
const cell = this.getCellByHandle(handle); const cell = this.getCellByHandle(handle);
cell?.deltaCellStatusBarItems(ids, []); cell?.deltaCellStatusBarItems(ids, []);
ids.forEach(id => this._statusBarItemIdToCellMap.delete(id)); ids.forEach(id => this._statusBarItemIdToCellMap.delete(id));

View File

@@ -504,11 +504,15 @@ class HistoryItemRenderer implements ICompressibleTreeRenderer<SCMHistoryItemVie
continue; continue;
} }
if (!historyItemRefs) {
continue;
}
// Group history item references by icon // Group history item references by icon
const historyItemRefByIconId = groupBy2(historyItemRefs, ref => ThemeIcon.isThemeIcon(ref.icon) ? ref.icon.id : ''); const historyItemRefByIconId = groupBy2(historyItemRefs, ref => ThemeIcon.isThemeIcon(ref.icon) ? ref.icon.id : '');
for (const [key, historyItemRefs] of Object.entries(historyItemRefByIconId)) { for (const [key, historyItemRefs] of Object.entries(historyItemRefByIconId)) {
// Skip badges without an icon // Skip badges without an icon
if (key === '') { if (key === '' || !historyItemRefs) {
continue; continue;
} }