* fix #214294

* fix version message

* finetune message
This commit is contained in:
Sandeep Somavarapu 2024-06-19 23:57:39 +02:00 committed by GitHub
parent 0cce95eac9
commit 7717059b2e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 164 additions and 11 deletions

View file

@ -23,6 +23,7 @@ import {
} from 'vs/platform/extensionManagement/common/extensionManagement';
import { areSameExtensions, ExtensionKey, getGalleryExtensionId, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { ExtensionType, IExtensionManifest, isApplicationScopedExtension, TargetPlatform } from 'vs/platform/extensions/common/extensions';
import { areApiProposalsCompatible } from 'vs/platform/extensions/common/extensionValidator';
import { ILogService } from 'vs/platform/log/common/log';
import { IProductService } from 'vs/platform/product/common/productService';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
@ -548,6 +549,10 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
compatibleExtension = await this.getCompatibleVersion(extension, sameVersion, installPreRelease, productVersion);
if (!compatibleExtension) {
const incompatibleApiProposalsMessages: string[] = [];
if (!areApiProposalsCompatible(extension.properties.enabledApiProposals ?? [], incompatibleApiProposalsMessages)) {
throw new ExtensionManagementError(nls.localize('incompatibleAPI', "Can't install '{0}' extension. {1}", extension.displayName ?? extension.identifier.id, incompatibleApiProposalsMessages[0]), ExtensionManagementErrorCode.IncompatibleApi);
}
/** If no compatible release version is found, check if the extension has a release version or not and throw relevant error */
if (!installPreRelease && extension.properties.isPreReleaseVersion && (await this.galleryService.getExtensions([extension.identifier], CancellationToken.None))[0]) {
throw new ExtensionManagementError(nls.localize('notFoundReleaseExtension', "Can't install release version of '{0}' extension because it has no release version.", extension.displayName ?? extension.identifier.id), ExtensionManagementErrorCode.ReleaseVersionNotFound);

View file

@ -18,7 +18,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment'
import { getTargetPlatform, IExtensionGalleryService, IExtensionIdentifier, IExtensionInfo, IGalleryExtension, IGalleryExtensionAsset, IGalleryExtensionAssets, IGalleryExtensionVersion, InstallOperation, IQueryOptions, IExtensionsControlManifest, isNotWebExtensionInWebTargetPlatform, isTargetPlatformCompatible, ITranslation, SortBy, SortOrder, StatisticType, toTargetPlatform, WEB_EXTENSION_TAG, IExtensionQueryOptions, IDeprecationInfo, ISearchPrefferedResults, ExtensionGalleryError, ExtensionGalleryErrorCode, IProductVersion } from 'vs/platform/extensionManagement/common/extensionManagement';
import { adoptToGalleryExtensionId, areSameExtensions, getGalleryExtensionId, getGalleryExtensionTelemetryData } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions';
import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator';
import { areApiProposalsCompatible, isEngineValid } from 'vs/platform/extensions/common/extensionValidator';
import { IFileService } from 'vs/platform/files/common/files';
import { ILogService } from 'vs/platform/log/common/log';
import { IProductService } from 'vs/platform/product/common/productService';
@ -209,6 +209,7 @@ const PropertyType = {
ExtensionPack: 'Microsoft.VisualStudio.Code.ExtensionPack',
Engine: 'Microsoft.VisualStudio.Code.Engine',
PreRelease: 'Microsoft.VisualStudio.Code.PreRelease',
EnabledApiProposals: 'Microsoft.VisualStudio.Code.EnabledApiProposals',
LocalizedLanguages: 'Microsoft.VisualStudio.Code.LocalizedLanguages',
WebExtension: 'Microsoft.VisualStudio.Code.WebExtension',
SponsorLink: 'Microsoft.VisualStudio.Code.SponsorLink',
@ -430,6 +431,12 @@ function isPreReleaseVersion(version: IRawGalleryExtensionVersion): boolean {
return values.length > 0 && values[0].value === 'true';
}
function getEnabledApiProposals(version: IRawGalleryExtensionVersion): string[] {
const values = version.properties ? version.properties.filter(p => p.key === PropertyType.EnabledApiProposals) : [];
const value = (values.length > 0 && values[0].value) || '';
return value ? value.split(',') : [];
}
function getLocalizedLanguages(version: IRawGalleryExtensionVersion): string[] {
const values = version.properties ? version.properties.filter(p => p.key === PropertyType.LocalizedLanguages) : [];
const value = (values.length > 0 && values[0].value) || '';
@ -548,6 +555,7 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller
dependencies: getExtensions(version, PropertyType.Dependency),
extensionPack: getExtensions(version, PropertyType.ExtensionPack),
engine: getEngine(version),
enabledApiProposals: getEnabledApiProposals(version),
localizedLanguages: getLocalizedLanguages(version),
targetPlatform: getTargetPlatformForExtensionVersion(version),
isPreReleaseVersion: isPreReleaseVersion(version)
@ -704,7 +712,16 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
}
engine = manifest.engines.vscode;
}
return isEngineValid(engine, productVersion.version, productVersion.date);
if (!isEngineValid(engine, productVersion.version, productVersion.date)) {
return false;
}
if (!areApiProposalsCompatible(extension.properties.enabledApiProposals ?? [])) {
return false;
}
return true;
}
private async isValidVersion(extension: string, rawGalleryExtensionVersion: IRawGalleryExtensionVersion, versionType: 'release' | 'prerelease' | 'any', compatible: boolean, allTargetPlatforms: TargetPlatform[], targetPlatform: TargetPlatform, productVersion: IProductVersion = { version: this.productService.version, date: this.productService.date }): Promise<boolean> {
@ -915,7 +932,18 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
continue;
}
// Allow any version if includePreRelease flag is set otherwise only release versions are allowed
if (await this.isValidVersion(getGalleryExtensionId(rawGalleryExtension.publisher.publisherName, rawGalleryExtension.extensionName), rawGalleryExtensionVersion, includePreRelease ? 'any' : 'release', criteria.compatible, allTargetPlatforms, criteria.targetPlatform, criteria.productVersion)) {
if (await this.isValidVersion(
getGalleryExtensionId(rawGalleryExtension.publisher.publisherName, rawGalleryExtension.extensionName),
rawGalleryExtensionVersion,
includePreRelease ? 'any' : 'release',
criteria.compatible,
allTargetPlatforms,
criteria.targetPlatform,
criteria.productVersion)
) {
if (criteria.compatible && !areApiProposalsCompatible(getEnabledApiProposals(rawGalleryExtensionVersion))) {
return null;
}
return toExtension(rawGalleryExtension, rawGalleryExtensionVersion, allTargetPlatforms, queryContext);
}
if (version && rawGalleryExtensionVersion.version === version) {
@ -1161,7 +1189,15 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
const validVersions: IRawGalleryExtensionVersion[] = [];
await Promise.all(galleryExtensions[0].versions.map(async (version) => {
try {
if (await this.isValidVersion(extension.identifier.id, version, includePreRelease ? 'any' : 'release', true, allTargetPlatforms, targetPlatform)) {
if (
(await this.isValidVersion(
extension.identifier.id,
version, includePreRelease ? 'any' : 'release',
true,
allTargetPlatforms,
targetPlatform))
&& areApiProposalsCompatible(getEnabledApiProposals(version))
) {
validVersions.push(version);
}
} catch (error) { /* Ignore error and skip version */ }

View file

@ -159,6 +159,7 @@ export interface IGalleryExtensionProperties {
dependencies?: string[];
extensionPack?: string[];
engine?: string;
enabledApiProposals?: string[];
localizedLanguages?: string[];
targetPlatform: TargetPlatform;
isPreReleaseVersion: boolean;
@ -437,6 +438,7 @@ export const enum ExtensionManagementErrorCode {
Deprecated = 'Deprecated',
Malicious = 'Malicious',
Incompatible = 'Incompatible',
IncompatibleApi = 'IncompatibleApi',
IncompatibleTargetPlatform = 'IncompatibleTargetPlatform',
ReleaseVersionNotFound = 'ReleaseVersionNotFound',
Invalid = 'Invalid',

View file

@ -8,7 +8,8 @@ import Severity from 'vs/base/common/severity';
import { URI } from 'vs/base/common/uri';
import * as nls from 'vs/nls';
import * as semver from 'vs/base/common/semver/semver';
import { IExtensionManifest } from 'vs/platform/extensions/common/extensions';
import { IExtensionManifest, parseApiProposals } from 'vs/platform/extensions/common/extensions';
import { allApiProposals } from 'vs/platform/extensions/common/extensionsApiProposals';
export interface IParsedVersion {
hasCaret: boolean;
@ -314,12 +315,22 @@ export function validateExtensionManifest(productVersion: string, productDate: P
}
const notices: string[] = [];
const isValid = isValidExtensionVersion(productVersion, productDate, extensionManifest, extensionIsBuiltin, notices);
if (!isValid) {
const validExtensionVersion = isValidExtensionVersion(productVersion, productDate, extensionManifest, extensionIsBuiltin, notices);
if (!validExtensionVersion) {
for (const notice of notices) {
validations.push([Severity.Error, notice]);
}
}
if (extensionManifest.enabledApiProposals?.length) {
const incompatibleNotices: string[] = [];
if (!areApiProposalsCompatible([...extensionManifest.enabledApiProposals], incompatibleNotices)) {
for (const notice of incompatibleNotices) {
validations.push([Severity.Error, notice]);
}
}
}
return validations;
}
@ -338,6 +349,38 @@ export function isEngineValid(engine: string, version: string, date: ProductDate
return engine === '*' || isVersionValid(version, date, engine);
}
export function areApiProposalsCompatible(apiProposals: string[]): boolean;
export function areApiProposalsCompatible(apiProposals: string[], notices: string[]): boolean;
export function areApiProposalsCompatible(apiProposals: string[], productApiProposals: Readonly<{ [proposalName: string]: Readonly<{ proposal: string; version?: number }> }>): boolean;
export function areApiProposalsCompatible(apiProposals: string[], arg1?: any): boolean {
if (apiProposals.length === 0) {
return true;
}
const notices: string[] | undefined = Array.isArray(arg1) ? arg1 : undefined;
const productApiProposals: Readonly<{ [proposalName: string]: Readonly<{ proposal: string; version?: number }> }> = (notices ? undefined : arg1) ?? allApiProposals;
const incompatibleNotices: string[] = [];
const parsedProposals = parseApiProposals(apiProposals);
for (const { proposalName, version } of parsedProposals) {
const existingProposal = productApiProposals[proposalName];
if (!existingProposal) {
continue;
}
if (!version) {
continue;
}
if (existingProposal.version !== version) {
if (existingProposal.version) {
incompatibleNotices.push(nls.localize('apiProposalMismatch', "Extension is not compatible with API proposal {0}. Extension requires version {1} but product has version {2}.", proposalName, version, existingProposal.version));
} else {
incompatibleNotices.push(nls.localize('apiProposalMismatchNoVersion', "Extension is not compatible with API proposal {0}. Extension requires version {1} but product has no version defined.", proposalName, version));
}
}
}
notices?.push(...incompatibleNotices);
return incompatibleNotices.length === 0;
}
function isVersionValid(currentVersion: string, date: ProductDate, requestedVersion: string, notices: string[] = []): boolean {
const desiredVersion = normalizeVersion(parseVersion(requestedVersion));

View file

@ -494,6 +494,13 @@ export function isResolverExtension(manifest: IExtensionManifest, remoteAuthorit
return false;
}
export function parseApiProposals(enabledApiProposals: string[]): { proposalName: string; version?: number }[] {
return enabledApiProposals.map(proposal => {
const [proposalName, version] = proposal.split('@');
return { proposalName, version: version ? parseInt(version) : undefined };
});
}
export function parseEnabledApiProposalNames(enabledApiProposals: string[]): string[] {
return enabledApiProposals.map(proposal => proposal.split('@')[0]);
}

View file

@ -5,7 +5,7 @@
import assert from 'assert';
import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils';
import { IExtensionManifest } from 'vs/platform/extensions/common/extensions';
import { INormalizedVersion, IParsedVersion, isValidExtensionVersion, isValidVersion, isValidVersionStr, normalizeVersion, parseVersion } from 'vs/platform/extensions/common/extensionValidator';
import { areApiProposalsCompatible, INormalizedVersion, IParsedVersion, isValidExtensionVersion, isValidVersion, isValidVersionStr, normalizeVersion, parseVersion } from 'vs/platform/extensions/common/extensionValidator';
suite('Extension Version Validator', () => {
@ -423,4 +423,21 @@ suite('Extension Version Validator', () => {
};
assert.strictEqual(isValidExtensionVersion('1.44.0', undefined, manifest, false, []), false);
});
test('areApiProposalsCompatible', () => {
assert.strictEqual(areApiProposalsCompatible([]), true);
assert.strictEqual(areApiProposalsCompatible([], ['hello']), true);
assert.strictEqual(areApiProposalsCompatible([], {}), true);
assert.strictEqual(areApiProposalsCompatible(['proposal1'], {}), true);
assert.strictEqual(areApiProposalsCompatible(['proposal1'], { 'proposal1': { proposal: '' } }), true);
assert.strictEqual(areApiProposalsCompatible(['proposal1'], { 'proposal1': { proposal: '', version: 1 } }), true);
assert.strictEqual(areApiProposalsCompatible(['proposal1@1'], { 'proposal1': { proposal: '', version: 1 } }), true);
assert.strictEqual(areApiProposalsCompatible(['proposal1'], { 'proposal2': { proposal: '' } }), true);
assert.strictEqual(areApiProposalsCompatible(['proposal1', 'proposal2'], {}), true);
assert.strictEqual(areApiProposalsCompatible(['proposal1', 'proposal2'], { 'proposal1': { proposal: '' } }), true);
assert.strictEqual(areApiProposalsCompatible(['proposal1@1'], { 'proposal1': { proposal: '', version: 2 } }), false);
assert.strictEqual(areApiProposalsCompatible(['proposal1@1'], { 'proposal1': { proposal: '' } }), false);
});
});

View file

@ -533,7 +533,7 @@ export class LocalExtensionsProvider {
addToSkipped.push(e);
this.logService.info(`${syncResourceLogLabel}: Skipped synchronizing extension`, gallery.displayName || gallery.identifier.id);
}
if (error instanceof ExtensionManagementError && [ExtensionManagementErrorCode.Incompatible, ExtensionManagementErrorCode.IncompatibleTargetPlatform].includes(error.code)) {
if (error instanceof ExtensionManagementError && [ExtensionManagementErrorCode.Incompatible, ExtensionManagementErrorCode.IncompatibleApi, ExtensionManagementErrorCode.IncompatibleTargetPlatform].includes(error.code)) {
this.logService.info(`${syncResourceLogLabel}: Skipped synchronizing extension because the compatible extension is not found.`, gallery.displayName || gallery.identifier.id);
} else if (error) {
this.logService.error(error);

View file

@ -134,7 +134,7 @@ export class PromptExtensionInstallFailureAction extends Action {
return;
}
if ([ExtensionManagementErrorCode.Incompatible, ExtensionManagementErrorCode.IncompatibleTargetPlatform, ExtensionManagementErrorCode.Malicious, ExtensionManagementErrorCode.Deprecated].includes(<ExtensionManagementErrorCode>this.error.name)) {
if ([ExtensionManagementErrorCode.Incompatible, ExtensionManagementErrorCode.IncompatibleApi, ExtensionManagementErrorCode.IncompatibleTargetPlatform, ExtensionManagementErrorCode.Malicious, ExtensionManagementErrorCode.Deprecated].includes(<ExtensionManagementErrorCode>this.error.name)) {
await this.dialogService.info(getErrorMessage(this.error));
return;
}

View file

@ -24,6 +24,7 @@ import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/use
import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile';
import { IRemoteUserDataProfilesService } from 'vs/workbench/services/userDataProfile/common/remoteUserDataProfiles';
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
import { areApiProposalsCompatible } from 'vs/platform/extensions/common/extensionValidator';
export class NativeRemoteExtensionManagementService extends RemoteExtensionManagementService {
@ -134,6 +135,10 @@ export class NativeRemoteExtensionManagementService extends RemoteExtensionManag
}
if (!compatibleExtension) {
const incompatibleApiProposalsMessages: string[] = [];
if (!areApiProposalsCompatible(extension.properties.enabledApiProposals ?? [], incompatibleApiProposalsMessages)) {
throw new ExtensionManagementError(localize('incompatibleAPI', "Can't install '{0}' extension. {1}", extension.displayName ?? extension.identifier.id, incompatibleApiProposalsMessages[0]), ExtensionManagementErrorCode.IncompatibleApi);
}
/** If no compatible release version is found, check if the extension has a release version or not and throw relevant error */
if (!includePreRelease && extension.properties.isPreReleaseVersion && (await this.galleryService.getExtensions([extension.identifier], CancellationToken.None))[0]) {
throw new ExtensionManagementError(localize('notFoundReleaseExtension', "Can't install release version of '{0}' extension because it has no release version.", extension.identifier.id), ExtensionManagementErrorCode.ReleaseVersionNotFound);

View file

@ -4,11 +4,17 @@
*--------------------------------------------------------------------------------------------*/
import { isNonEmptyArray } from 'vs/base/common/arrays';
import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { localize } from 'vs/nls';
import { Disposable } from 'vs/base/common/lifecycle';
import { ExtensionIdentifier, IExtensionDescription, IExtensionManifest } from 'vs/platform/extensions/common/extensions';
import { allApiProposals, ApiProposalName } from 'vs/platform/extensions/common/extensionsApiProposals';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { ILogService } from 'vs/platform/log/common/log';
import { IProductService } from 'vs/platform/product/common/productService';
import { Registry } from 'vs/platform/registry/common/platform';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { Extensions, IExtensionFeatureMarkdownRenderer, IExtensionFeaturesRegistry, IRenderedData } from 'vs/workbench/services/extensionManagement/common/extensionFeatures';
import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent';
export class ExtensionsProposedApi {
@ -107,3 +113,35 @@ export class ExtensionsProposedApi {
}
}
}
class ApiProposalsMarkdowneRenderer extends Disposable implements IExtensionFeatureMarkdownRenderer {
readonly type = 'markdown';
shouldRender(manifest: IExtensionManifest): boolean {
return !!manifest.enabledApiProposals?.length;
}
render(manifest: IExtensionManifest): IRenderedData<IMarkdownString> {
const enabledApiProposals = manifest.enabledApiProposals || [];
const data = new MarkdownString();
if (enabledApiProposals.length) {
for (const proposal of enabledApiProposals) {
data.appendMarkdown(`- \`${proposal}\`\n`);
}
}
return {
data,
dispose: () => { }
};
}
}
Registry.as<IExtensionFeaturesRegistry>(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({
id: 'enabledApiProposals',
label: localize('enabledProposedAPIs', "API Proposals"),
access: {
canToggle: false
},
renderer: new SyncDescriptor(ApiProposalsMarkdowneRenderer),
});