Use semver package for version comparison and precedence checking

This commit is contained in:
Henry Mercer 2020-06-01 19:26:15 +01:00
Родитель e686b421ec
Коммит 034d8b7c68
7 изменённых файлов: 53 добавлений и 224 удалений

Просмотреть файл

@ -26,6 +26,7 @@ dependencies:
'@types/react': 16.9.23
'@types/react-dom': 16.9.5
'@types/sarif': 2.1.2
'@types/semver': 7.2.0
'@types/sinon': 7.5.2
'@types/sinon-chai': 3.2.3
'@types/through2': 2.0.34
@ -68,6 +69,7 @@ dependencies:
react: 16.13.0
react-dom: 16.13.0_react@16.13.0
reflect-metadata: 0.1.13
semver: 7.3.2
sinon: 9.0.1
sinon-chai: 3.5.0_chai@4.2.0+sinon@9.0.1
style-loader: 0.23.1
@ -508,6 +510,12 @@ packages:
dev: false
resolution:
integrity: sha512-TELZl5h48KaB6SFZqTuaMEw1hrGuusbBcH+yfMaaHdS2pwDr3RTH4CVN0LyY1kqSiDm9PPvAMx8FJ0LUZreOCQ==
/@types/semver/7.2.0:
dependencies:
'@types/node': 12.12.30
dev: false
resolution:
integrity: sha512-TbB0A8ACUWZt3Y6bQPstW9QNbhNeebdgLX4T/ZfkrswAfUzRiXrgd9seol+X379Wa589Pu4UEx9Uok0D4RjRCQ==
/@types/sinon-chai/3.2.3:
dependencies:
'@types/chai': 4.2.11
@ -6210,6 +6218,13 @@ packages:
hasBin: true
resolution:
integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
/semver/7.3.2:
dev: false
engines:
node: '>=10'
hasBin: true
resolution:
integrity: sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==
/serialize-javascript/2.1.2:
dev: false
resolution:
@ -7836,7 +7851,7 @@ packages:
peerDependencies:
glob: '*'
resolution:
integrity: sha512-NkoIMaJdASYX4NjcB+nsEk/8Ff/2RLvHwL0efNOny3no6aNuJ3EkpNK0ZdX7HQdmTdY3IJPmjoJ3Rn4pkbxgdA==
integrity: sha512-14DvfY6Fj3HXp2/CNJ2zNh9MA8zPw9mUcr8WqkSsYvJow7JMcIlJ//OOONwpoSWtfrk1bk6Cin7jj9H79ItHQQ==
tarball: 'file:projects/build-tasks.tgz'
version: 0.0.0
'file:projects/semmle-bqrs.tgz_typescript@3.8.3':
@ -7851,7 +7866,7 @@ packages:
peerDependencies:
typescript: '*'
resolution:
integrity: sha512-lE3FBYrOVF1JH0ZqvF4YA+bed3JPWYucsnFe+XL140a/YR19XD+TTHIfov7VpR9qdyWfARgvmR+gf2qsguXTKQ==
integrity: sha512-24GdnvMbGfQIWMfgDhift+kYJDnG7dX03NrpX4ajZ2rckteysvq2/K7XI1OXGvUuqrt3m0/+GRDHpSI9XKDJJA==
tarball: 'file:projects/semmle-bqrs.tgz'
version: 0.0.0
'file:projects/semmle-io-node.tgz_typescript@3.8.3':
@ -7866,7 +7881,7 @@ packages:
peerDependencies:
typescript: '*'
resolution:
integrity: sha512-MD9edC5HjrCfPmhktw6XmWotUmperj27/hDZiuMbuSlJ4jRKyiBtJ8Vk2Y4U41TrzsBlJfAwZW8tetPw5ujiLg==
integrity: sha512-Bj0ax/bASrHV7tamOuXZZdd3UOB4NBKdjdszIRaDvDRTu8RlEst+TVoUhkfy30qb2/6ePp3/juOJyyiBJN7u8Q==
tarball: 'file:projects/semmle-io-node.tgz'
version: 0.0.0
'file:projects/semmle-io.tgz_typescript@3.8.3':
@ -7880,7 +7895,7 @@ packages:
peerDependencies:
typescript: '*'
resolution:
integrity: sha512-ta1lLi1COIeFwpwH523cWheWx6OE8GTqguQmOA7G6CwRF41RYbbREf/4KlOLKO/uG2akhhl+3gcWY2c5/VDC/A==
integrity: sha512-NtyviDSevxbd+hj4J66LucOzo8LU2hJ1Jh0eHw0Qu3tRZPUT8HcQlseyy29AvZR8n8eppfEZiAm/JdiHfmRPMA==
tarball: 'file:projects/semmle-io.tgz'
version: 0.0.0
'file:projects/semmle-vscode-utils.tgz':
@ -7892,14 +7907,14 @@ packages:
dev: false
name: '@rush-temp/semmle-vscode-utils'
resolution:
integrity: sha512-Dbwt0/Wd0VNKkRZRjFQv3hmGy/UDt36HDtEDsNgZIcQACoY1j2+mJavpQ+ZzCg4Ftj06eHDVk+ptzUEd+8Ybzw==
integrity: sha512-5y5r8SDoN9Fp44naC9gUe8rOexeckXg2T0h9QCJAIcEgnFqOxzRc6Rv9gbMUStFKNh+rFlvmYmgPAdg5QkfgUg==
tarball: 'file:projects/semmle-vscode-utils.tgz'
version: 0.0.0
'file:projects/typescript-config.tgz':
dev: false
name: '@rush-temp/typescript-config'
resolution:
integrity: sha512-qJbtY2jvt6LKkmUt/seiYyXSEB6Oip/rW+SxofQEnpyplgIQv7whTZb6g5pwlSLGl8goTaQFm4NfazKhFmxXvQ==
integrity: sha512-XuUIySaNoooIduvehnlKYaHqZJmmQoCqB1RtKhNszjCYZaSSJAnKVucViWBf5oNLKSNP7NchrD7gcoBlQ3xYvw==
tarball: 'file:projects/typescript-config.tgz'
version: 0.0.0
'file:projects/vscode-codeql.tgz':
@ -7921,6 +7936,7 @@ packages:
'@types/react': 16.9.23
'@types/react-dom': 16.9.5
'@types/sarif': 2.1.2
'@types/semver': 7.2.0
'@types/sinon': 7.5.2
'@types/sinon-chai': 3.2.3
'@types/tmp': 0.1.0
@ -7955,6 +7971,7 @@ packages:
proxyquire: 2.1.3
react: 16.13.0
react-dom: 16.13.0_react@16.13.0
semver: 7.3.2
sinon: 9.0.1
sinon-chai: 3.5.0_chai@4.2.0+sinon@9.0.1
style-loader: 0.23.1
@ -7978,7 +7995,7 @@ packages:
dev: false
name: '@rush-temp/vscode-codeql'
resolution:
integrity: sha512-ClyrIRqnMYMmVHtHvW8MvS4GrRSt/dXY3lxBpxSv3wSJ67pEvWKea+DJyeVN2zaHz1/7gAOWQHhwBz6O3lEq6w==
integrity: sha512-bU6tGSUD6TzMa6XDiDymvfY28xtDKp6uYPVCwiy7zdsl5NYUxph5Yua0Snoam7oytdYMa2HieTn8Lh6Hkb5P/A==
tarball: 'file:projects/vscode-codeql.tgz'
version: 0.0.0
registry: ''
@ -8010,6 +8027,7 @@ specifiers:
'@types/react': ^16.8.17
'@types/react-dom': ^16.8.4
'@types/sarif': ~2.1.2
'@types/semver': ~7.2.0
'@types/sinon': ~7.5.2
'@types/sinon-chai': ~3.2.3
'@types/through2': ~2.0.34
@ -8052,6 +8070,7 @@ specifiers:
react: ^16.8.6
react-dom: ^16.8.6
reflect-metadata: ~0.1.13
semver: ~7.3.2
sinon: ~9.0.0
sinon-chai: ~3.5.0
style-loader: ~0.23.1

Просмотреть файл

@ -590,7 +590,9 @@
"vscode-languageclient": "^6.1.3",
"vscode-test-adapter-api": "~1.7.0",
"vscode-test-adapter-util": "~0.7.0",
"minimist": "~1.2.5"
"minimist": "~1.2.5",
"semver": "~7.3.2",
"@types/semver": "~7.2.0"
},
"devDependencies": {
"@types/chai": "^4.1.7",

Просмотреть файл

@ -1,10 +1,11 @@
import * as semver from "semver";
import { runCodeQlCliCommand } from "./cli";
import { Logger } from "./logging";
/**
* Get the version of a CodeQL CLI.
*/
export async function getCodeQlCliVersion(codeQlPath: string, logger: Logger): Promise<Version | undefined> {
export async function getCodeQlCliVersion(codeQlPath: string, logger: Logger): Promise<semver.SemVer | undefined> {
const output: string = await runCodeQlCliCommand(
codeQlPath,
["version"],
@ -12,85 +13,5 @@ export async function getCodeQlCliVersion(codeQlPath: string, logger: Logger): P
"Checking CodeQL version",
logger
);
return tryParseVersionString(output.trim());
}
/**
* Try to parse a version string, returning undefined if we can't parse it.
*
* Version strings must contain a major, minor, and patch version. They may optionally
* start with "v" and may optionally contain some "tail" string after the major, minor, and
* patch versions, for example as in `v2.1.0+baf5bff`.
*/
export function tryParseVersionString(versionString: string): Version | undefined {
const match = versionString.match(versionRegex);
if (match === null) {
return undefined;
}
return {
buildMetadata: match[5],
majorVersion: Number.parseInt(match[1], 10),
minorVersion: Number.parseInt(match[2], 10),
patchVersion: Number.parseInt(match[3], 10),
prereleaseVersion: match[4],
rawString: versionString,
};
}
/**
* Regex for parsing semantic versions
*
* From the semver spec https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
*/
const versionRegex = new RegExp(String.raw`^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)` +
String.raw`(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?` +
String.raw`(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`);
/**
* A version of the CodeQL CLI.
*/
export interface Version {
/**
* Build metadata
*
* For example, this will be `abcdef0` for version 2.1.0-alpha.1+abcdef0.
* Build metadata must be ignored when comparing versions.
*/
buildMetadata: string | undefined;
/**
* Major version number
*
* For example, this will be `2` for version 2.1.0-alpha.1+abcdef0.
*/
majorVersion: number;
/**
* Minor version number
*
* For example, this will be `1` for version 2.1.0-alpha.1+abcdef0.
*/
minorVersion: number;
/**
* Patch version number
*
* For example, this will be `0` for version 2.1.0-alpha.1+abcdef0.
*/
patchVersion: number;
/**
* Prerelease version
*
* For example, this will be `alpha.1` for version 2.1.0-alpha.1+abcdef0.
* The prerelease version must be considered when comparing versions.
*/
prereleaseVersion: string | undefined;
/**
* Raw version string
*
* For example, this will be `2.1.0-alpha.1+abcdef0` for version 2.1.0-alpha.1+abcdef0.
*/
rawString: string;
return semver.parse(output.trim()) || undefined;
}

Просмотреть файл

@ -2,6 +2,7 @@ import * as fetch from "node-fetch";
import * as fs from "fs-extra";
import * as os from "os";
import * as path from "path";
import * as semver from "semver";
import * as unzipper from "unzipper";
import * as url from "url";
import { ExtensionContext, Event } from "vscode";
@ -9,7 +10,7 @@ import { DistributionConfig } from "./config";
import { InvocationRateLimiter, InvocationRateLimiterResultKind, showAndLogErrorMessage } from "./helpers";
import { logger } from "./logging";
import * as helpers from "./helpers";
import { getCodeQlCliVersion, tryParseVersionString, Version } from "./cli-version";
import { getCodeQlCliVersion } from "./cli-version";
/**
* distribution.ts
@ -41,9 +42,7 @@ const DEFAULT_DISTRIBUTION_REPOSITORY_NAME = "codeql-cli-binaries";
*/
export const DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT: VersionConstraint = {
description: "2.*.*",
isVersionCompatible: (v: Version) => {
return v.majorVersion === 2 && v.minorVersion >= 0;
}
isVersionCompatible: (v: semver.SemVer) => semver.satisfies(v, "2.x")
};
export interface DistributionProvider {
@ -403,20 +402,16 @@ export class ReleasesApiConsumer {
return false;
}
const version = tryParseVersionString(release.tag_name);
if (version === undefined || !versionConstraint.isVersionCompatible(version)) {
return false;
}
return true;
const version = semver.parse(release.tag_name);
return version !== null && versionConstraint.isVersionCompatible(version);
});
// tryParseVersionString must succeed due to the previous filtering step
// Tag names must all be parsable to semvers due to the previous filtering step.
const latestRelease = compatibleReleases.sort((a, b) => {
const versionComparison = versionCompare(tryParseVersionString(b.tag_name)!, tryParseVersionString(a.tag_name)!);
if (versionComparison === 0) {
return b.created_at.localeCompare(a.created_at);
const versionComparison = semver.compare(semver.parse(b.tag_name)!, semver.parse(a.tag_name)!);
if (versionComparison !== 0) {
return versionComparison;
}
return versionComparison;
return b.created_at.localeCompare(a.created_at, "en-US");
})[0];
if (latestRelease === undefined) {
throw new Error("No compatible CodeQL CLI releases were found. " +
@ -516,29 +511,6 @@ export async function extractZipArchive(archivePath: string, outPath: string): P
}));
}
/**
* Comparison of semantic versions.
*
* Returns a positive number if a is greater than b.
* Returns 0 if a equals b.
* Returns a negative number if a is less than b.
*/
export function versionCompare(a: Version, b: Version): number {
if (a.majorVersion !== b.majorVersion) {
return a.majorVersion - b.majorVersion;
}
if (a.minorVersion !== b.minorVersion) {
return a.minorVersion - b.minorVersion;
}
if (a.patchVersion !== b.patchVersion) {
return a.patchVersion - b.patchVersion;
}
if (a.prereleaseVersion !== undefined && b.prereleaseVersion !== undefined) {
return a.prereleaseVersion.localeCompare(b.prereleaseVersion);
}
return 0;
}
function codeQlLauncherName(): string {
return (os.platform() === "win32") ? "codeql.exe" : "codeql";
}
@ -571,7 +543,7 @@ export type FindDistributionResult =
interface CompatibleDistributionResult {
codeQlPath: string;
kind: FindDistributionResultKind.CompatibleDistribution;
version: Version;
version: semver.SemVer;
}
interface UnknownCompatibilityDistributionResult {
@ -582,7 +554,7 @@ interface UnknownCompatibilityDistributionResult {
interface IncompatibleDistributionResult {
codeQlPath: string;
kind: FindDistributionResultKind.IncompatibleDistribution;
version: Version;
version: semver.SemVer;
}
interface NoDistributionResult {
@ -722,7 +694,7 @@ export interface GithubRelease {
assets: GithubReleaseAsset[];
/**
* The creation date of the release on GitHub.
* The creation date of the release on GitHub, in ISO 8601 format.
*/
created_at: string;
@ -769,7 +741,7 @@ export interface GithubReleaseAsset {
interface VersionConstraint {
description: string;
isVersionCompatible(version: Version): boolean;
isVersionCompatible(version: semver.SemVer): boolean;
}
export class GithubApiError extends Error {

Просмотреть файл

@ -182,7 +182,7 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
const result = await distributionManager.getDistribution();
switch (result.kind) {
case FindDistributionResultKind.CompatibleDistribution:
logger.log(`Found compatible version of CodeQL CLI (version ${result.version.rawString})`);
logger.log(`Found compatible version of CodeQL CLI (version ${result.version.raw})`);
break;
case FindDistributionResultKind.IncompatibleDistribution:
helpers.showAndLogWarningMessage("The current version of the CodeQL CLI is incompatible with this extension.");

Просмотреть файл

@ -1,50 +0,0 @@
import { expect } from "chai";
import "mocha";
import { tryParseVersionString } from "../../cli-version";
describe("Version parsing", () => {
it("should accept version without prerelease and build metadata", () => {
const v = tryParseVersionString("3.2.4")!;
expect(v.majorVersion).to.equal(3);
expect(v.minorVersion).to.equal(2);
expect(v.patchVersion).to.equal(4);
expect(v.prereleaseVersion).to.be.undefined;
expect(v.buildMetadata).to.be.undefined;
});
it("should accept v at the beginning of the version", () => {
const v = tryParseVersionString("v3.2.4")!;
expect(v.majorVersion).to.equal(3);
expect(v.minorVersion).to.equal(2);
expect(v.patchVersion).to.equal(4);
expect(v.prereleaseVersion).to.be.undefined;
expect(v.buildMetadata).to.be.undefined;
});
it("should accept version with prerelease", () => {
const v = tryParseVersionString("v3.2.4-alpha.0")!;
expect(v.majorVersion).to.equal(3);
expect(v.minorVersion).to.equal(2);
expect(v.patchVersion).to.equal(4);
expect(v.prereleaseVersion).to.equal("alpha.0");
expect(v.buildMetadata).to.be.undefined;
});
it("should accept version with prerelease and build metadata", () => {
const v = tryParseVersionString("v3.2.4-alpha.0+abcdef0")!;
expect(v.majorVersion).to.equal(3);
expect(v.minorVersion).to.equal(2);
expect(v.patchVersion).to.equal(4);
expect(v.prereleaseVersion).to.equal("alpha.0");
expect(v.buildMetadata).to.equal("abcdef0");
});
it("should accept version with build metadata", () => {
const v = tryParseVersionString("v3.2.4+abcdef0")!;
expect(v.majorVersion).to.equal(3);
expect(v.minorVersion).to.equal(2);
expect(v.patchVersion).to.equal(4);
expect(v.prereleaseVersion).to.be.undefined;
expect(v.buildMetadata).to.equal("abcdef0");
});
});

Просмотреть файл

@ -1,14 +1,14 @@
import * as chai from "chai";
import * as path from "path";
import * as fetch from "node-fetch";
import 'chai/register-should';
import * as sinonChai from 'sinon-chai';
import * as sinon from 'sinon';
import "chai/register-should";
import * as semver from "semver";
import * as sinonChai from "sinon-chai";
import * as sinon from "sinon";
import * as pq from "proxyquire";
import "mocha";
import { Version } from "../../cli-version";
import { GithubRelease, GithubReleaseAsset, ReleasesApiConsumer, versionCompare } from "../../distribution";
import { GithubRelease, GithubReleaseAsset, ReleasesApiConsumer } from "../../distribution";
const proxyquire = pq.noPreserveCache();
chai.use(sinonChai);
@ -86,7 +86,7 @@ describe("Releases API consumer", () => {
const latestRelease = await consumer.getLatestRelease({
description: "2.*.*",
isVersionCompatible: version => version.majorVersion === 2
isVersionCompatible: version => semver.satisfies(version, "2.x")
});
expect(latestRelease.id).to.equal(1);
});
@ -152,41 +152,6 @@ describe("Releases API consumer", () => {
});
});
describe("Release version ordering", () => {
function createVersion(majorVersion: number, minorVersion: number, patchVersion: number, prereleaseVersion?: string, buildMetadata?: string): Version {
return {
buildMetadata,
majorVersion,
minorVersion,
patchVersion,
prereleaseVersion,
rawString: `${majorVersion}.${minorVersion}.${patchVersion}` +
prereleaseVersion ? `-${prereleaseVersion}` : "" +
buildMetadata ? `+${buildMetadata}` : ""
};
}
it("major versions compare correctly", () => {
expect(versionCompare(createVersion(3, 0, 0), createVersion(2, 9, 9)) > 0).to.be.true;
});
it("minor versions compare correctly", () => {
expect(versionCompare(createVersion(2, 1, 0), createVersion(2, 0, 9)) > 0).to.be.true;
});
it("patch versions compare correctly", () => {
expect(versionCompare(createVersion(2, 1, 2), createVersion(2, 1, 1)) > 0).to.be.true;
});
it("prerelease versions compare correctly", () => {
expect(versionCompare(createVersion(2, 1, 0, "alpha.2"), createVersion(2, 1, 0, "alpha.1")) > 0).to.true;
});
it("build metadata compares correctly", () => {
expect(versionCompare(createVersion(2, 1, 0, "alpha.1", "abcdef0"), createVersion(2, 1, 0, "alpha.1", "bcdef01"))).to.equal(0);
});
});
describe('Launcher path', () => {
const pathToCmd = `abc${path.sep}codeql.cmd`;
const pathToExe = `abc${path.sep}codeql.exe`;