Lint redundant activation events if extension is targeting ^1.75 (#173345)

This commit is contained in:
Joyce Er 2023-02-03 09:53:00 -08:00 committed by GitHub
parent 19014d03d6
commit 3ab0fb3aa9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 127 additions and 3 deletions

View File

@ -0,0 +1,117 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// https://github.com/microsoft/vscode/blob/6cb34eb22385bc93ab25aa2e5113f59c7a2299ac/src/vs/platform/extensions/common/extensionValidator.ts
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})$/;
export function isValidVersionStr(version: string): boolean {
version = version.trim();
return (version === '*' || VERSION_REGEXP.test(version));
}
export function parseVersion(version: string): IParsedVersion | null {
if (!isValidVersionStr(version)) {
return null;
}
version = version.trim();
if (version === '*') {
return {
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;
}
return {
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 {
if (!version) {
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) {
patchMustEqual = false;
} else {
minorMustEqual = false;
patchMustEqual = false;
}
}
let notBefore = 0;
if (version.preRelease) {
const match = NOT_BEFORE_REGEXP.exec(version.preRelease);
if (match) {
const [, year, month, day] = match;
notBefore = Date.UTC(Number(year), Number(month) - 1, Number(day));
}
}
return {
majorBase: majorBase,
majorMustEqual: majorMustEqual,
minorBase: minorBase,
minorMustEqual: minorMustEqual,
patchBase: patchBase,
patchMustEqual: patchMustEqual,
isMinimum: version.hasGreaterEquals,
notBefore,
};
}

View File

@ -11,6 +11,7 @@ import { parseTree, findNodeAtLocation, Node as JsonNode, getNodeValue } from 'j
import * as MarkdownItType from 'markdown-it';
import { languages, workspace, Disposable, TextDocument, Uri, Diagnostic, Range, DiagnosticSeverity, Position, env, l10n } from 'vscode';
import { INormalizedVersion, normalizeVersion, parseVersion } from './extensionEngineValidation';
const product = JSON.parse(fs.readFileSync(path.join(env.appRoot, 'product.json'), { encoding: 'utf-8' }));
const allowedBadgeProviders: string[] = (product.extensionAllowedBadgeProviders || []).map((s: string) => s.toLowerCase());
@ -33,6 +34,7 @@ const relativeBadgeUrlRequiresHttpsRepository = l10n.t("Relative badge URLs requ
const apiProposalNotListed = l10n.t("This proposal cannot be used because for this extension the product defines a fixed set of API proposals. You can test your extension but before publishing you MUST reach out to the VS Code team.");
const implicitActivationEvent = l10n.t("This activation event cannot be explicitly listed by your extension.");
const redundantImplicitActivationEvent = l10n.t("This activation event can be removed as VS Code generates these automatically from your package.json contribution declarations.");
const bumpEngineForImplicitActivationEvents = l10n.t("This activation event can be removed for extensions targeting engine version ^1.75 as VS Code will generate these automatically from your package.json contribution declarations.");
const starActivation = l10n.t("Using '*' activation is usually a bad idea as it impacts performance.");
enum Context {
@ -52,6 +54,7 @@ interface PackageJsonInfo {
hasHttpsRepository: boolean;
repository: Uri;
implicitActivationEvents: Set<string> | undefined;
engineVersion: INormalizedVersion | null;
}
export class ExtensionLinter {
@ -156,16 +159,18 @@ export class ExtensionLinter {
if (activationEventsNode?.type === 'array' && activationEventsNode.children) {
for (const activationEventNode of activationEventsNode.children) {
const activationEvent = getNodeValue(activationEventNode);
const isImplicitActivationSupported = info.engineVersion && info.engineVersion?.majorBase >= 1 && info.engineVersion?.minorBase >= 75;
// Redundant Implicit Activation
if (info.implicitActivationEvents?.has(activationEvent) && redundantImplicitActivationEventPrefixes.some((prefix) => activationEvent.startsWith(prefix))) {
const start = document.positionAt(activationEventNode.offset);
const end = document.positionAt(activationEventNode.offset + activationEventNode.length);
diagnostics.push(new Diagnostic(new Range(start, end), redundantImplicitActivationEvent, DiagnosticSeverity.Warning));
const message = isImplicitActivationSupported ? redundantImplicitActivationEvent : bumpEngineForImplicitActivationEvents;
diagnostics.push(new Diagnostic(new Range(start, end), message, isImplicitActivationSupported ? DiagnosticSeverity.Warning : DiagnosticSeverity.Information));
}
// Reserved Implicit Activation
for (const implicitActivationEventPrefix of reservedImplicitActivationEventPrefixes) {
if (activationEvent.startsWith(implicitActivationEventPrefix)) {
if (isImplicitActivationSupported && activationEvent.startsWith(implicitActivationEventPrefix)) {
const start = document.positionAt(activationEventNode.offset);
const end = document.positionAt(activationEventNode.offset + activationEventNode.length);
diagnostics.push(new Diagnostic(new Range(start, end), implicitActivationEvent, DiagnosticSeverity.Error));
@ -314,6 +319,7 @@ export class ExtensionLinter {
private readPackageJsonInfo(folder: Uri, tree: JsonNode | undefined) {
const engine = tree && findNodeAtLocation(tree, ['engines', 'vscode']);
const parsedEngineVersion = engine?.type === 'string' ? normalizeVersion(parseVersion(engine.value)) : null;
const repo = tree && findNodeAtLocation(tree, ['repository', 'url']);
const uri = repo && parseUri(repo.value);
const activationEvents = tree && parseImplicitActivationEvents(tree);
@ -322,7 +328,8 @@ export class ExtensionLinter {
isExtension: !!(engine && engine.type === 'string'),
hasHttpsRepository: !!(repo && repo.type === 'string' && repo.value && uri && uri.scheme.toLowerCase() === 'https'),
repository: uri!,
implicitActivationEvents: activationEvents
implicitActivationEvents: activationEvents,
engineVersion: parsedEngineVersion
};
const str = folder.toString();
const oldInfo = this.folderToPackageJsonInfo[str];