Repository: microsoft/vscode (TypeScript)
Date: 2023-02-15 15:43:40+00:00
Task type: Bug Fix

Issue:
Extensions are not loaded if relativeLocation is empty string
- Install some extensions
- Open extensions folder and open extensions.json file
- Update the `relativeLocation` property to empty string
- Restart VS Code

🐛 No extensions are shown


Issue discussion:

Solution patch:
diff --git a/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts
--- a/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts
+++ b/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts
@@ -248,7 +248,7 @@ export abstract class AbstractExtensionsProfileScannerService extends Disposable
 						this.reportAndThrowInvalidConentError(file);
 					}
 					let location: URI;
-					if (isString(e.relativeLocation)) {
+					if (isString(e.relativeLocation) && e.relativeLocation) {
 						// Extension in new format. No migration needed.
 						location = this.resolveExtensionLocation(e.relativeLocation);
 					} else if (isString(e.location)) {
@@ -396,7 +396,7 @@ export abstract class AbstractExtensionsProfileScannerService extends Disposable
 function isStoredProfileExtension(candidate: any): candidate is IStoredProfileExtension {
 	return isObject(candidate)
 		&& isIExtensionIdentifier(candidate.identifier)
-		&& (isUriComponents(candidate.location) || isString(candidate.location))
+		&& (isUriComponents(candidate.location) || (isString(candidate.location) && candidate.location))
 		&& (isUndefined(candidate.relativeLocation) || isString(candidate.relativeLocation))
 		&& candidate.version && isString(candidate.version);
 }


Test patch:
diff --git a/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts b/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts
--- a/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts
+++ b/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts
@@ -414,7 +414,7 @@ suite('ExtensionsProfileScannerService', () => {
 
 	test('read extension when manifest has empty lines and spaces', async () => {
 		const extensionsManifest = joinPath(extensionsLocation, 'extensions.json');
-		await instantiationService.get(IFileService).writeFile(extensionsManifest, VSBuffer.fromString(`   
+		await instantiationService.get(IFileService).writeFile(extensionsManifest, VSBuffer.fromString(`
 
 
 		`));
@@ -423,6 +423,25 @@ suite('ExtensionsProfileScannerService', () => {
 		assert.deepStrictEqual(actual, []);
 	});
 
+	test('read extension when the relative location is empty', async () => {
+		const extensionsManifest = joinPath(extensionsLocation, 'extensions.json');
+		const extension = aExtension('pub.a', joinPath(extensionsLocation, 'pub.a-1.0.0'));
+		await instantiationService.get(IFileService).writeFile(extensionsManifest, VSBuffer.fromString(JSON.stringify([{
+			identifier: extension.identifier,
+			location: extension.location.toJSON(),
+			relativeLocation: '',
+			version: extension.manifest.version,
+		}])));
+
+		const testObject = instantiationService.createInstance(TestObject, extensionsLocation);
+
+		const actual = await testObject.scanProfileExtensions(extensionsManifest);
+		assert.deepStrictEqual(actual.map(a => ({ ...a, location: a.location.toJSON() })), [{ identifier: extension.identifier, location: extension.location.toJSON(), version: extension.manifest.version, metadata: undefined }]);
+
+		const manifestContent = JSON.parse((await instantiationService.get(IFileService).readFile(extensionsManifest)).value.toString());
+		assert.deepStrictEqual(manifestContent, [{ identifier: extension.identifier, location: extension.location.toJSON(), relativeLocation: 'pub.a-1.0.0', version: extension.manifest.version }]);
+	});
+
 	function aExtension(id: string, location: URI, e?: Partial<IExtension>): IExtension {
 		return {
 			identifier: { id },