Skip to content

Commit 577ce95

Browse files
committed
Use semver package for version comparison and precedence checking
1 parent 63c8afa commit 577ce95

File tree

7 files changed

+53
-224
lines changed

7 files changed

+53
-224
lines changed

common/config/rush/pnpm-lock.yaml

Lines changed: 26 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

extensions/ql-vscode/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -590,7 +590,9 @@
590590
"vscode-languageclient": "^6.1.3",
591591
"vscode-test-adapter-api": "~1.7.0",
592592
"vscode-test-adapter-util": "~0.7.0",
593-
"minimist": "~1.2.5"
593+
"minimist": "~1.2.5",
594+
"semver": "~7.3.2",
595+
"@types/semver": "~7.2.0"
594596
},
595597
"devDependencies": {
596598
"@types/chai": "^4.1.7",
Lines changed: 3 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,96 +1,17 @@
1+
import * as semver from "semver";
12
import { runCodeQlCliCommand } from "./cli";
23
import { Logger } from "./logging";
34

45
/**
56
* Get the version of a CodeQL CLI.
67
*/
7-
export async function getCodeQlCliVersion(codeQlPath: string, logger: Logger): Promise<Version | undefined> {
8+
export async function getCodeQlCliVersion(codeQlPath: string, logger: Logger): Promise<semver.SemVer | undefined> {
89
const output: string = await runCodeQlCliCommand(
910
codeQlPath,
1011
["version"],
1112
["--format=terse"],
1213
"Checking CodeQL version",
1314
logger
1415
);
15-
return tryParseVersionString(output.trim());
16-
}
17-
18-
/**
19-
* Try to parse a version string, returning undefined if we can't parse it.
20-
*
21-
* Version strings must contain a major, minor, and patch version. They may optionally
22-
* start with "v" and may optionally contain some "tail" string after the major, minor, and
23-
* patch versions, for example as in `v2.1.0+baf5bff`.
24-
*/
25-
export function tryParseVersionString(versionString: string): Version | undefined {
26-
const match = versionString.match(versionRegex);
27-
if (match === null) {
28-
return undefined;
29-
}
30-
return {
31-
buildMetadata: match[5],
32-
majorVersion: Number.parseInt(match[1], 10),
33-
minorVersion: Number.parseInt(match[2], 10),
34-
patchVersion: Number.parseInt(match[3], 10),
35-
prereleaseVersion: match[4],
36-
rawString: versionString,
37-
};
38-
}
39-
40-
/**
41-
* Regex for parsing semantic versions
42-
*
43-
* From the semver spec https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
44-
*/
45-
const versionRegex = new RegExp(String.raw`^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)` +
46-
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-]*))*))?` +
47-
String.raw`(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`);
48-
49-
/**
50-
* A version of the CodeQL CLI.
51-
*/
52-
export interface Version {
53-
/**
54-
* Build metadata
55-
*
56-
* For example, this will be `abcdef0` for version 2.1.0-alpha.1+abcdef0.
57-
* Build metadata must be ignored when comparing versions.
58-
*/
59-
buildMetadata: string | undefined;
60-
61-
/**
62-
* Major version number
63-
*
64-
* For example, this will be `2` for version 2.1.0-alpha.1+abcdef0.
65-
*/
66-
majorVersion: number;
67-
68-
/**
69-
* Minor version number
70-
*
71-
* For example, this will be `1` for version 2.1.0-alpha.1+abcdef0.
72-
*/
73-
minorVersion: number;
74-
75-
/**
76-
* Patch version number
77-
*
78-
* For example, this will be `0` for version 2.1.0-alpha.1+abcdef0.
79-
*/
80-
patchVersion: number;
81-
82-
/**
83-
* Prerelease version
84-
*
85-
* For example, this will be `alpha.1` for version 2.1.0-alpha.1+abcdef0.
86-
* The prerelease version must be considered when comparing versions.
87-
*/
88-
prereleaseVersion: string | undefined;
89-
90-
/**
91-
* Raw version string
92-
*
93-
* For example, this will be `2.1.0-alpha.1+abcdef0` for version 2.1.0-alpha.1+abcdef0.
94-
*/
95-
rawString: string;
16+
return semver.parse(output.trim()) || undefined;
9617
}

extensions/ql-vscode/src/distribution.ts

Lines changed: 14 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@ import * as fetch from "node-fetch";
22
import * as fs from "fs-extra";
33
import * as os from "os";
44
import * as path from "path";
5+
import * as semver from "semver";
56
import * as unzipper from "unzipper";
67
import * as url from "url";
78
import { ExtensionContext, Event } from "vscode";
89
import { DistributionConfig } from "./config";
910
import { InvocationRateLimiter, InvocationRateLimiterResultKind, showAndLogErrorMessage } from "./helpers";
1011
import { logger } from "./logging";
1112
import * as helpers from "./helpers";
12-
import { getCodeQlCliVersion, tryParseVersionString, Version } from "./cli-version";
13+
import { getCodeQlCliVersion } from "./cli-version";
1314

1415
/**
1516
* distribution.ts
@@ -41,9 +42,7 @@ const DEFAULT_DISTRIBUTION_REPOSITORY_NAME = "codeql-cli-binaries";
4142
*/
4243
export const DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT: VersionConstraint = {
4344
description: "2.*.*",
44-
isVersionCompatible: (v: Version) => {
45-
return v.majorVersion === 2 && v.minorVersion >= 0;
46-
}
45+
isVersionCompatible: (v: semver.SemVer) => semver.satisfies(v, "2.x")
4746
};
4847

4948
export interface DistributionProvider {
@@ -403,20 +402,16 @@ export class ReleasesApiConsumer {
403402
return false;
404403
}
405404

406-
const version = tryParseVersionString(release.tag_name);
407-
if (version === undefined || !versionConstraint.isVersionCompatible(version)) {
408-
return false;
409-
}
410-
411-
return true;
405+
const version = semver.parse(release.tag_name);
406+
return version !== null && versionConstraint.isVersionCompatible(version);
412407
});
413-
// tryParseVersionString must succeed due to the previous filtering step
408+
// Tag names must all be parsable to semvers due to the previous filtering step.
414409
const latestRelease = compatibleReleases.sort((a, b) => {
415-
const versionComparison = versionCompare(tryParseVersionString(b.tag_name)!, tryParseVersionString(a.tag_name)!);
416-
if (versionComparison === 0) {
417-
return b.created_at.localeCompare(a.created_at);
410+
const versionComparison = semver.compare(semver.parse(b.tag_name)!, semver.parse(a.tag_name)!);
411+
if (versionComparison !== 0) {
412+
return versionComparison;
418413
}
419-
return versionComparison;
414+
return b.created_at.localeCompare(a.created_at, "en-US");
420415
})[0];
421416
if (latestRelease === undefined) {
422417
throw new Error("No compatible CodeQL CLI releases were found. " +
@@ -516,29 +511,6 @@ export async function extractZipArchive(archivePath: string, outPath: string): P
516511
}));
517512
}
518513

519-
/**
520-
* Comparison of semantic versions.
521-
*
522-
* Returns a positive number if a is greater than b.
523-
* Returns 0 if a equals b.
524-
* Returns a negative number if a is less than b.
525-
*/
526-
export function versionCompare(a: Version, b: Version): number {
527-
if (a.majorVersion !== b.majorVersion) {
528-
return a.majorVersion - b.majorVersion;
529-
}
530-
if (a.minorVersion !== b.minorVersion) {
531-
return a.minorVersion - b.minorVersion;
532-
}
533-
if (a.patchVersion !== b.patchVersion) {
534-
return a.patchVersion - b.patchVersion;
535-
}
536-
if (a.prereleaseVersion !== undefined && b.prereleaseVersion !== undefined) {
537-
return a.prereleaseVersion.localeCompare(b.prereleaseVersion);
538-
}
539-
return 0;
540-
}
541-
542514
function codeQlLauncherName(): string {
543515
return (os.platform() === "win32") ? "codeql.exe" : "codeql";
544516
}
@@ -571,7 +543,7 @@ export type FindDistributionResult =
571543
interface CompatibleDistributionResult {
572544
codeQlPath: string;
573545
kind: FindDistributionResultKind.CompatibleDistribution;
574-
version: Version;
546+
version: semver.SemVer;
575547
}
576548

577549
interface UnknownCompatibilityDistributionResult {
@@ -582,7 +554,7 @@ interface UnknownCompatibilityDistributionResult {
582554
interface IncompatibleDistributionResult {
583555
codeQlPath: string;
584556
kind: FindDistributionResultKind.IncompatibleDistribution;
585-
version: Version;
557+
version: semver.SemVer;
586558
}
587559

588560
interface NoDistributionResult {
@@ -722,7 +694,7 @@ export interface GithubRelease {
722694
assets: GithubReleaseAsset[];
723695

724696
/**
725-
* The creation date of the release on GitHub.
697+
* The creation date of the release on GitHub, in ISO 8601 format.
726698
*/
727699
created_at: string;
728700

@@ -769,7 +741,7 @@ export interface GithubReleaseAsset {
769741

770742
interface VersionConstraint {
771743
description: string;
772-
isVersionCompatible(version: Version): boolean;
744+
isVersionCompatible(version: semver.SemVer): boolean;
773745
}
774746

775747
export class GithubApiError extends Error {

extensions/ql-vscode/src/extension.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
182182
const result = await distributionManager.getDistribution();
183183
switch (result.kind) {
184184
case FindDistributionResultKind.CompatibleDistribution:
185-
logger.log(`Found compatible version of CodeQL CLI (version ${result.version.rawString})`);
185+
logger.log(`Found compatible version of CodeQL CLI (version ${result.version.raw})`);
186186
break;
187187
case FindDistributionResultKind.IncompatibleDistribution:
188188
helpers.showAndLogWarningMessage("The current version of the CodeQL CLI is incompatible with this extension.");

0 commit comments

Comments
 (0)