src/vs/platform/extensions/common/extensionValidator.ts
391 LOC · 310 covered · 81 uncovered · 83 ranges · 97 concepts · 20 introducers · 65 tests
File neighbourhood
The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.
Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file
In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.
Graph controls are ready.
Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.
/*---------------------------------------------------------------------------------------------
extensionValidator.ts ×9
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { isEqualOrParent, joinPath } from '../../../base/common/resources.js';
import Severity from '../../../base/common/severity.js';
import { URI } from '../../../base/common/uri.js';
import * as nls from '../../../nls.js';
import * as semver from '../../../base/common/semver/semver.js';
import { IExtensionManifest } from './extensions.js';
export interface IParsedVersion {
hasCaret: boolean;
hasGreaterEquals: boolean;
majorBase: number;
majorMustEqual: boolean;
minorBase: number;
minorMustEqual: boolean;
patchBase: number;
patchMustEqual: boolean;
preRelease: string | null;
}
export interface INormalizedVersion {
majorBase: number;
majorMustEqual: boolean;
minorBase: number;
minorMustEqual: boolean;
patchBase: number;
patchMustEqual: boolean;
notBefore: number; /* milliseconds timestamp, or 0 */
isMinimum: boolean;
}
const VERSION_REGEXP = /^(\^|>=)?((\d+)|x)\.((\d+)|x)\.((\d+)|x)(\-.*)?$/;
const NOT_BEFORE_REGEXP = /^-(\d{4})(\d{2})(\d{2})(\d{2})?(\d{2})?$/;
export function isValidVersionStr(version: string): boolean {
return (version === '*' || VERSION_REGEXP.test(version));
}
export function parseVersion(version: string): IParsedVersion | null {
return null;
}
version = version.trim();
if (version === '*') {
hasCaret: false,
hasGreaterEquals: false,
majorBase: 0,
majorMustEqual: false,
minorBase: 0,
minorMustEqual: false,
patchBase: 0,
patchMustEqual: false,
preRelease: null
};
}
const m = version.match(VERSION_REGEXP);
if (!m) {
return null;
}
hasCaret: m[1] === '^',
hasGreaterEquals: m[1] === '>=',
majorBase: m[2] === 'x' ? 0 : parseInt(m[2], 10),
majorMustEqual: (m[2] === 'x' ? false : true),
minorBase: m[4] === 'x' ? 0 : parseInt(m[4], 10),
minorMustEqual: (m[4] === 'x' ? false : true),
patchBase: m[6] === 'x' ? 0 : parseInt(m[6], 10),
patchMustEqual: (m[6] === 'x' ? false : true),
preRelease: m[8] || null
};
}
export function normalizeVersion(version: IParsedVersion | null): INormalizedVersion | null {
return null;
}
const majorBase = version.majorBase;
const majorMustEqual = version.majorMustEqual;
const minorBase = version.minorBase;
let minorMustEqual = version.minorMustEqual;
const patchBase = version.patchBase;
let patchMustEqual = version.patchMustEqual;
if (version.hasCaret) {
if (majorBase === 0) {
minorMustEqual = false;
patchMustEqual = false;
}
}
let notBefore = 0;
if (version.preRelease) {
if (match) {
notBefore = Date.UTC(Number(year), Number(month) - 1, Number(day), Number(hours) || 0, Number(minutes) || 0);
}
return {
majorBase: majorBase,
majorMustEqual: majorMustEqual,
minorBase: minorBase,
minorMustEqual: minorMustEqual,
patchBase: patchBase,
patchMustEqual: patchMustEqual,
isMinimum: version.hasGreaterEquals,
notBefore,
};
}
export function isValidVersion(_inputVersion: string | INormalizedVersion, _inputDate: ProductDate, _desiredVersion: string | INormalizedVersion): boolean {
if (typeof _inputVersion === 'string') {
version = normalizeVersion(parseVersion(_inputVersion));
} else {
version = _inputVersion;
}
let productTs: number | undefined;
if (_inputDate instanceof Date) {
productTs = _inputDate.getTime();
}
let desiredVersion: INormalizedVersion | null;
if (typeof _desiredVersion === 'string') {
}
if (!version || !desiredVersion) {
return false;
}
const majorBase = version.majorBase;
const minorBase = version.minorBase;
const patchBase = version.patchBase;
let desiredMajorBase = desiredVersion.majorBase;
let desiredMinorBase = desiredVersion.minorBase;
let desiredPatchBase = desiredVersion.patchBase;
const desiredNotBefore = desiredVersion.notBefore;
let majorMustEqual = desiredVersion.majorMustEqual;
let minorMustEqual = desiredVersion.minorMustEqual;
let patchMustEqual = desiredVersion.patchMustEqual;
if (desiredVersion.isMinimum) {
return true;
}
if (majorBase < desiredMajorBase) {
return false;
}
if (minorBase > desiredMinorBase) {
return true;
}
if (minorBase < desiredMinorBase) {
return false;
}
if (productTs && productTs < desiredNotBefore) {
return false;
}
return patchBase >= desiredPatchBase;
}
// Anything < 1.0.0 is compatible with >= 1.0.0, except exact matches
if (majorBase === 1 && desiredMajorBase === 0 && (!majorMustEqual || !minorMustEqual || !patchMustEqual)) {
desiredMinorBase = 0;
desiredPatchBase = 0;
majorMustEqual = true;
minorMustEqual = false;
patchMustEqual = false;
}
if (majorBase < desiredMajorBase) {
return false;
}
if (majorBase > desiredMajorBase) {
return (!majorMustEqual);
}
// at this point, majorBase are equal
if (minorBase < desiredMinorBase) {
return false;
}
if (minorBase > desiredMinorBase) {
return (!minorMustEqual);
}
// at this point, minorBase are equal
if (patchBase < desiredPatchBase) {
return false;
}
if (patchBase > desiredPatchBase) {
return (!patchMustEqual);
}
// at this point, patchBase are equal
}
return true;
}
type ProductDate = string | Date | undefined;
export function validateExtensionManifest(productVersion: string, productDate: ProductDate, extensionLocation: URI, extensionManifest: IExtensionManifest, extensionIsBuiltin: boolean): readonly [Severity, string][] {
if (typeof extensionManifest.publisher !== 'undefined' && typeof extensionManifest.publisher !== 'string') {
validations.push([Severity.Error, nls.localize('extensionDescription.publisher', "property publisher must be of type `string`.")]);
return validations;
}
validations.push([Severity.Error, nls.localize('extensionDescription.name', "property `{0}` is mandatory and must be of type `string`", 'name')]);
return validations;
}
validations.push([Severity.Error, nls.localize('extensionDescription.version', "property `{0}` is mandatory and must be of type `string`", 'version')]);
return validations;
}
validations.push([Severity.Error, nls.localize('extensionDescription.engines', "property `{0}` is mandatory and must be of type `object`", 'engines')]);
return validations;
}
validations.push([Severity.Error, nls.localize('extensionDescription.engines.vscode', "property `{0}` is mandatory and must be of type `string`", 'engines.vscode')]);
return validations;
}
if (typeof extensionManifest.extensionDependencies !== 'undefined') {
extensionsScannerService.ts ×30
if (!isStringArray(extensionManifest.extensionDependencies)) {
validations.push([Severity.Error, nls.localize('extensionDescription.extensionDependencies', "property `{0}` can be omitted or must be of type `string[]`", 'extensionDependencies')]);
return validations;
}
}
if (typeof extensionManifest.extensionAffinity !== 'undefined') {
extensionsScannerService.ts ×30
if (!isStringArray(extensionManifest.extensionAffinity)) {
validations.push([Severity.Error, nls.localize('extensionDescription.extensionAffinity', "property `{0}` can be omitted or must be of type `string[]`", 'extensionAffinity')]);
return validations;
}
}
if (typeof extensionManifest.activationEvents !== 'undefined') {
extensionsScannerService.ts ×30
if (!isStringArray(extensionManifest.activationEvents)) {
validations.push([Severity.Error, nls.localize('extensionDescription.activationEvents1', "property `{0}` can be omitted or must be of type `string[]`", 'activationEvents')]);
return validations;
}
if (typeof extensionManifest.main === 'undefined' && typeof extensionManifest.browser === 'undefined') {
extensionsScannerService.ts ×30
validations.push([Severity.Error, nls.localize('extensionDescription.activationEvents2', "property `{0}` should be omitted if the extension doesn't have a `{1}` or `{2}` property.", 'activationEvents', 'main', 'browser')]);
return validations;
}
if (typeof extensionManifest.extensionKind !== 'undefined') {
if (typeof extensionManifest.main === 'undefined') {
validations.push([Severity.Warning, nls.localize('extensionDescription.extensionKind', "property `{0}` can be defined only if property `main` is also defined.", 'extensionKind')]);
// not a failure case
}
}
if (typeof extensionManifest.main !== 'string') {
validations.push([Severity.Error, nls.localize('extensionDescription.main1', "property `{0}` can be omitted or must be of type `string`", 'main')]);
return validations;
const mainLocation = joinPath(extensionLocation, extensionManifest.main);
if (!isEqualOrParent(mainLocation, extensionLocation)) {
validations.push([Severity.Warning, nls.localize('extensionDescription.main2', "Expected `main` ({0}) to be included inside extension's folder ({1}). This might make the extension non-portable.", mainLocation.path, extensionLocation.path)]);
// not a failure case
}
}
if (typeof extensionManifest.browser !== 'undefined') {
if (typeof extensionManifest.browser !== 'string') {
validations.push([Severity.Error, nls.localize('extensionDescription.browser1', "property `{0}` can be omitted or must be of type `string`", 'browser')]);
return validations;
} else {
const browserLocation = joinPath(extensionLocation, extensionManifest.browser);
if (!isEqualOrParent(browserLocation, extensionLocation)) {
validations.push([Severity.Warning, nls.localize('extensionDescription.browser2', "Expected `browser` ({0}) to be included inside extension's folder ({1}). This might make the extension non-portable.", browserLocation.path, extensionLocation.path)]);
// not a failure case
}
}
}
if (!semver.valid(extensionManifest.version)) {
validations.push([Severity.Error, nls.localize('notSemver', "Extension version is not semver compatible.")]);
return validations;
}
const notices: string[] = [];
const validExtensionVersion = isValidExtensionVersion(productVersion, productDate, extensionManifest, extensionIsBuiltin, notices);
if (!validExtensionVersion) {
validations.push([Severity.Error, notice]);
}
}
return validations;
}
export function isValidExtensionVersion(productVersion: string, productDate: ProductDate, extensionManifest: IExtensionManifest, extensionIsBuiltin: boolean, notices: string[]): boolean {
if (extensionIsBuiltin || (typeof extensionManifest.main === 'undefined' && typeof extensionManifest.browser === 'undefined')) {
return true;
}
return isVersionValid(productVersion, productDate, extensionManifest.engines.vscode, notices);
}
export function isEngineValid(engine: string, version: string, date: ProductDate): boolean {
// TODO@joao: discuss with alex '*' doesn't seem to be a valid engine version
return engine === '*' || isVersionValid(version, date, engine);
}
function isVersionValid(currentVersion: string, date: ProductDate, requestedVersion: string, notices: string[] = []): boolean {
extensionValidator.ts ×6
const desiredVersion = normalizeVersion(parseVersion(requestedVersion));
if (!desiredVersion) {
notices.push(nls.localize('versionSyntax', "Could not parse `engines.vscode` value {0}. Please use, for example: ^1.22.0, ^1.22.x, etc.", requestedVersion));
return false;
}
// enforce that a breaking API version is specified.
// for 0.X.Y, that means up to 0.X must be specified
// otherwise for Z.X.Y, that means Z must be specified
if (desiredVersion.majorBase === 0) {
if (!desiredVersion.majorMustEqual || !desiredVersion.minorMustEqual) {
notices.push(nls.localize('versionSpecificity1', "Version specified in `engines.vscode` ({0}) is not specific enough. For vscode versions before 1.0.0, please define at a minimum the major and minor desired version. E.g. ^0.10.0, 0.10.x, 0.11.0, etc.", requestedVersion));
return false;
}
// force that major must be specific
if (!desiredVersion.majorMustEqual) {
notices.push(nls.localize('versionSpecificity2', "Version specified in `engines.vscode` ({0}) is not specific enough. For vscode versions after 1.0.0, please define at a minimum the major desired version. E.g. ^1.10.0, 1.10.x, 1.x.x, 2.x.x, etc.", requestedVersion));
return false;
}
if (!isValidVersion(currentVersion, date, desiredVersion)) {
notices.push(nls.localize('versionMismatch', "Extension is not compatible with Code {0}. Extension requires: {1}.", currentVersion, requestedVersion));
extensionValidator.ts ×1
return false;
}
return true;
}
if (!Array.isArray(arr)) {
return false;
}
if (typeof arr[i] !== 'string') {
return false;
}
return true;
}