Repository: microsoft/vscode (TypeScript)
Date: 2022-05-28 19:59:28+00:00
Task type: Feature

Issue:
Improve setting sync experience with more error handing
<!-- ⚠️⚠️ Do Not Delete This! feature_request_template ⚠️⚠️ -->
<!-- Please read our Rules of Conduct: https://opensource.microsoft.com/codeofconduct/ -->
<!-- Please search existing issues to avoid creating duplicates. -->

<!-- Describe the feature you'd like. -->
Syncing setting will failed and no hint while keybindings.json has an error, for example:
```json
{}
```
> It should be an array, such as []

There is no hint told you need to fix your keybindings, it only says "o is not iterable", after seeing [this](https://github.com/microsoft/vscode/issues/103839#issuecomment-670012275) I found what iccur such an error, but most time people may think that is a bug.


Issue discussion:
Yeah
I don't have any **User tasks** (no file `AppData\Roaming\Code\User\tasks.json`). VS Code tries to synchronize them and does it every minute! The servers says "I'm a spammer" and denied everything.
Three weeks later, I find out that the synchronization just disabled...... >:(
When connecting, I was asked only three options
- do you want to replace your own with a cloud?
- want to merge?
- cancel.

Of course, I don't want to replace my data with old cloud data, and I clicked the only one not fatal "merge" and **a lot of garbage returned to my work environment**.

Well great experience 👍
@TIMONz1535 Can you please file a separate issue for your scenario and please provide us steps to reproduce.

Solution patch:
diff --git a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts
--- a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts
+++ b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts
@@ -14,7 +14,7 @@ import { FormattingOptions } from 'vs/base/common/jsonFormatter';
 import { Disposable } from 'vs/base/common/lifecycle';
 import { IExtUri } from 'vs/base/common/resources';
 import { uppercaseFirstLetter } from 'vs/base/common/strings';
-import { isString } from 'vs/base/common/types';
+import { isString, isUndefined } from 'vs/base/common/types';
 import { URI } from 'vs/base/common/uri';
 import { IHeaders } from 'vs/base/parts/request/common/request';
 import { localize } from 'vs/nls';
@@ -798,10 +798,10 @@ export abstract class AbstractJsonFileSynchroniser extends AbstractFileSynchroni
 		super(file, resource, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService, uriIdentityService);
 	}
 
-	protected hasErrors(content: string): boolean {
+	protected hasErrors(content: string, isArray: boolean): boolean {
 		const parseErrors: ParseError[] = [];
-		parse(content, parseErrors, { allowEmptyContent: true, allowTrailingComma: true });
-		return parseErrors.length > 0;
+		const result = parse(content, parseErrors, { allowEmptyContent: true, allowTrailingComma: true });
+		return parseErrors.length > 0 || (!isUndefined(result) && isArray !== Array.isArray(result));
 	}
 
 	private _formattingOptions: Promise<FormattingOptions> | undefined = undefined;
diff --git a/src/vs/platform/userDataSync/common/keybindingsSync.ts b/src/vs/platform/userDataSync/common/keybindingsSync.ts
--- a/src/vs/platform/userDataSync/common/keybindingsSync.ts
+++ b/src/vs/platform/userDataSync/common/keybindingsSync.ts
@@ -103,7 +103,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
 		if (remoteContent) {
 			let localContent: string = fileContent ? fileContent.value.toString() : '[]';
 			localContent = localContent || '[]';
-			if (this.hasErrors(localContent)) {
+			if (this.hasErrors(localContent, true)) {
 				throw new UserDataSyncError(localize('errorInvalidSettings', "Unable to sync keybindings because the content in the file is not valid. Please open the file and correct it."), UserDataSyncErrorCode.LocalInvalidContent, this.resource);
 			}
 
@@ -222,7 +222,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
 		if (content !== null) {
 			content = content.trim();
 			content = content || '[]';
-			if (this.hasErrors(content)) {
+			if (this.hasErrors(content, true)) {
 				throw new UserDataSyncError(localize('errorInvalidSettings', "Unable to sync keybindings because the content in the file is not valid. Please open the file and correct it."), UserDataSyncErrorCode.LocalInvalidContent, this.resource);
 			}
 		}
diff --git a/src/vs/platform/userDataSync/common/settingsSync.ts b/src/vs/platform/userDataSync/common/settingsSync.ts
--- a/src/vs/platform/userDataSync/common/settingsSync.ts
+++ b/src/vs/platform/userDataSync/common/settingsSync.ts
@@ -107,7 +107,8 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement
 		// First time syncing to remote
 		else if (fileContent) {
 			this.logService.trace(`${this.syncResourceLogLabel}: Remote settings does not exist. Synchronizing settings for the first time.`);
-			mergedContent = fileContent.value.toString();
+			mergedContent = fileContent.value.toString().trim() || '{}';
+			this.validateContent(mergedContent);
 			hasRemoteChanged = true;
 		}
 
@@ -340,7 +341,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement
 	}
 
 	private validateContent(content: string): void {
-		if (this.hasErrors(content)) {
+		if (this.hasErrors(content, false)) {
 			throw new UserDataSyncError(localize('errorInvalidSettings', "Unable to sync settings as there are errors/warning in settings file."), UserDataSyncErrorCode.LocalInvalidContent, this.resource);
 		}
 	}


Test patch:
diff --git a/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts b/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts
--- a/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts
+++ b/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts
@@ -10,7 +10,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment'
 import { IFileService } from 'vs/platform/files/common/files';
 import { ILogService } from 'vs/platform/log/common/log';
 import { getKeybindingsContentFromSyncContent, KeybindingsSynchroniser } from 'vs/platform/userDataSync/common/keybindingsSync';
-import { IUserDataSyncStoreService, SyncResource } from 'vs/platform/userDataSync/common/userDataSync';
+import { IUserDataSyncStoreService, SyncResource, UserDataSyncError, UserDataSyncErrorCode } from 'vs/platform/userDataSync/common/userDataSync';
 import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient';
 
 suite('KeybindingsSync', () => {
@@ -202,4 +202,15 @@ suite('KeybindingsSync', () => {
 		assert.deepStrictEqual(server.requests, []);
 	});
 
+	test('sync throws invalid content error - content is an object', async () => {
+		await client.instantiationService.get(IFileService).writeFile(client.instantiationService.get(IEnvironmentService).keybindingsResource, VSBuffer.fromString('{}'));
+		try {
+			await testObject.sync(await client.manifest());
+			assert.fail('should fail with invalid content error');
+		} catch (e) {
+			assert.ok(e instanceof UserDataSyncError);
+			assert.deepStrictEqual((<UserDataSyncError>e).code, UserDataSyncErrorCode.LocalInvalidContent);
+		}
+	});
+
 });
diff --git a/src/vs/platform/userDataSync/test/common/settingsSync.test.ts b/src/vs/platform/userDataSync/test/common/settingsSync.test.ts
--- a/src/vs/platform/userDataSync/test/common/settingsSync.test.ts
+++ b/src/vs/platform/userDataSync/test/common/settingsSync.test.ts
@@ -465,6 +465,17 @@ suite('SettingsSync - Auto', () => {
 		}
 	});
 
+	test('sync throws invalid content error - content is an array', async () => {
+		await updateSettings('[]', client);
+		try {
+			await testObject.sync(await client.manifest());
+			assert.fail('should fail with invalid content error');
+		} catch (e) {
+			assert.ok(e instanceof UserDataSyncError);
+			assert.deepStrictEqual((<UserDataSyncError>e).code, UserDataSyncErrorCode.LocalInvalidContent);
+		}
+	});
+
 	test('sync when there are conflicts', async () => {
 		const client2 = disposableStore.add(new UserDataSyncClient(server));
 		await client2.setUp(true);