Skip to content

Commit fb63ec7

Browse files
author
Dave Bartolomeo
committed
Consume codeql version JSON output for feature capabilities
1 parent b67efee commit fb63ec7

5 files changed

Lines changed: 119 additions & 40 deletions

File tree

extensions/ql-vscode/src/codeql-cli/cli-command.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,38 @@ import { promisify } from "util";
33

44
import type { BaseLogger } from "../common/logging";
55
import type { ProgressReporter } from "../common/logging/vscode";
6-
import { getChildProcessErrorMessage } from "../common/helpers-pure";
6+
import {
7+
getChildProcessErrorMessage,
8+
getErrorMessage,
9+
} from "../common/helpers-pure";
710

811
/**
912
* Flags to pass to all cli commands.
1013
*/
1114
export const LOGGING_FLAGS = ["-v", "--log-to-stderr"];
1215

1316
/**
14-
* Runs a CodeQL CLI command without invoking the CLI server, returning the output as a string.
17+
* Runs a CodeQL CLI command without invoking the CLI server, deserializing the output as JSON.
1518
* @param codeQlPath The path to the CLI.
1619
* @param command The `codeql` command to be run, provided as an array of command/subcommand names.
1720
* @param commandArgs The arguments to pass to the `codeql` command.
1821
* @param description Description of the action being run, to be shown in log and error messages.
1922
* @param logger Logger to write command log messages, e.g. to an output channel.
2023
* @param progressReporter Used to output progress messages, e.g. to the status bar.
21-
* @returns The contents of the command's stdout, if the command succeeded.
24+
* @returns A JSON object parsed from the contents of the command's stdout, if the command succeeded.
2225
*/
23-
export async function runCodeQlCliCommand(
26+
export async function runJsonCodeQlCliCommand<OutputType>(
2427
codeQlPath: string,
2528
command: string[],
2629
commandArgs: string[],
2730
description: string,
2831
logger: BaseLogger,
2932
progressReporter?: ProgressReporter,
30-
): Promise<string> {
33+
): Promise<OutputType> {
3134
// Add logging arguments first, in case commandArgs contains positional parameters.
3235
const args = command.concat(LOGGING_FLAGS).concat(commandArgs);
3336
const argsString = args.join(" ");
37+
let stdout: string;
3438
try {
3539
if (progressReporter !== undefined) {
3640
progressReporter.report({ message: description });
@@ -41,10 +45,18 @@ export async function runCodeQlCliCommand(
4145
const result = await promisify(execFile)(codeQlPath, args);
4246
void logger.log(result.stderr);
4347
void logger.log("CLI command succeeded.");
44-
return result.stdout;
48+
stdout = result.stdout;
4549
} catch (err) {
4650
throw new Error(
4751
`${description} failed: ${getChildProcessErrorMessage(err)}`,
4852
);
4953
}
54+
55+
try {
56+
return JSON.parse(stdout) as OutputType;
57+
} catch (err) {
58+
throw new Error(
59+
`Parsing output of ${description} failed: ${getErrorMessage(err)}`,
60+
);
61+
}
5062
}

extensions/ql-vscode/src/codeql-cli/cli-version.ts

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,49 @@
11
import type { SemVer } from "semver";
22
import { parse } from "semver";
3-
import { runCodeQlCliCommand } from "./cli-command";
3+
import { runJsonCodeQlCliCommand } from "./cli-command";
44
import type { Logger } from "../common/logging";
55
import { getErrorMessage } from "../common/helpers-pure";
66

7+
interface VersionResult {
8+
version: string;
9+
features: CliFeatures | undefined;
10+
}
11+
12+
export interface CliFeatures {
13+
featuresInVersionResult?: boolean;
14+
mrvaPackCreate?: boolean;
15+
}
16+
17+
export interface VersionAndFeatures {
18+
version: SemVer;
19+
features: CliFeatures;
20+
}
21+
722
/**
823
* Get the version of a CodeQL CLI.
924
*/
1025
export async function getCodeQlCliVersion(
1126
codeQlPath: string,
1227
logger: Logger,
13-
): Promise<SemVer | undefined> {
28+
): Promise<VersionAndFeatures | undefined> {
1429
try {
15-
const output: string = await runCodeQlCliCommand(
30+
const output: VersionResult = await runJsonCodeQlCliCommand<VersionResult>(
1631
codeQlPath,
1732
["version"],
18-
["--format=terse"],
33+
["--format=json"],
1934
"Checking CodeQL version",
2035
logger,
2136
);
22-
return parse(output.trim()) || undefined;
37+
38+
const version = parse(output.version.trim()) || undefined;
39+
if (version === undefined) {
40+
return undefined;
41+
}
42+
43+
return {
44+
version,
45+
features: output.features ?? {},
46+
};
2347
} catch (e) {
2448
// Failed to run the version command. This might happen if the cli version is _really_ old, or it is corrupted.
2549
// Either way, we can't determine compatibility.

extensions/ql-vscode/src/codeql-cli/cli.ts

Lines changed: 52 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { QueryLanguage } from "../common/query-language";
3434
import { LINE_ENDINGS, splitStreamAtSeparators } from "../common/split-stream";
3535
import type { Position } from "../query-server/messages";
3636
import { LOGGING_FLAGS } from "./cli-command";
37+
import type { CliFeatures, VersionAndFeatures } from "./cli-version";
3738

3839
/**
3940
* The version of the SARIF format that we are using.
@@ -193,7 +194,9 @@ type OnLineCallback = (
193194
line: string,
194195
) => Promise<string | undefined> | string | undefined;
195196

196-
type VersionChangedListener = (newVersion: SemVer | undefined) => void;
197+
type VersionChangedListener = (
198+
newVersionAndFeatures: VersionAndFeatures | undefined,
199+
) => void;
197200

198201
/**
199202
* This class manages a cli server started by `codeql execute cli-server` to
@@ -211,8 +214,8 @@ export class CodeQLCliServer implements Disposable {
211214
/** A buffer with a single null byte. */
212215
nullBuffer: Buffer;
213216

214-
/** Version of current cli, lazily computed by the `getVersion()` method */
215-
private _version: SemVer | undefined;
217+
/** Version of current cli and its supported features, lazily computed by the `getVersion()` method */
218+
private _versionAndFeatures: VersionAndFeatures | undefined;
216219

217220
private _versionChangedListeners: VersionChangedListener[] = [];
218221

@@ -288,7 +291,7 @@ export class CodeQLCliServer implements Disposable {
288291
const callback = (): void => {
289292
try {
290293
this.killProcessIfRunning();
291-
this._version = undefined;
294+
this._versionAndFeatures = undefined;
292295
this._supportedLanguages = undefined;
293296
} finally {
294297
this.runNext();
@@ -1417,6 +1420,27 @@ export class CodeQLCliServer implements Disposable {
14171420
);
14181421
}
14191422

1423+
public async packCreate(
1424+
dir: string,
1425+
workspaceFolders: string[],
1426+
outputPath: string,
1427+
moreOptions: string[],
1428+
): Promise<void> {
1429+
const args = [
1430+
"--output",
1431+
outputPath,
1432+
dir,
1433+
...moreOptions,
1434+
...this.getAdditionalPacksArg(workspaceFolders),
1435+
];
1436+
1437+
return this.runJsonCodeQlCliCommandWithAuthentication(
1438+
["pack", "create"],
1439+
args,
1440+
"Creating pack",
1441+
);
1442+
}
1443+
14201444
async packBundle(
14211445
dir: string,
14221446
workspaceFolders: string[],
@@ -1481,27 +1505,35 @@ export class CodeQLCliServer implements Disposable {
14811505
);
14821506
}
14831507

1484-
public async getVersion() {
1485-
if (!this._version) {
1508+
public async getVersion(): Promise<SemVer> {
1509+
return (await this.getVersionAndFeatures()).version;
1510+
}
1511+
1512+
public async getFeatures(): Promise<CliFeatures> {
1513+
return (await this.getVersionAndFeatures()).features;
1514+
}
1515+
1516+
public async getVersionAndFeatures(): Promise<VersionAndFeatures> {
1517+
if (!this._versionAndFeatures) {
14861518
try {
1487-
const newVersion = await this.refreshVersion();
1488-
this._version = newVersion;
1519+
const newVersionAndFeatures = await this.refreshVersion();
1520+
this._versionAndFeatures = newVersionAndFeatures;
14891521
this._versionChangedListeners.forEach((listener) =>
1490-
listener(newVersion),
1522+
listener(newVersionAndFeatures),
14911523
);
14921524

14931525
// this._version is only undefined upon config change, so we reset CLI-based context key only when necessary.
14941526
await this.app.commands.execute(
14951527
"setContext",
14961528
"codeql.supportsQuickEvalCount",
1497-
newVersion.compare(
1529+
newVersionAndFeatures.version.compare(
14981530
CliVersionConstraint.CLI_VERSION_WITH_QUICK_EVAL_COUNT,
14991531
) >= 0,
15001532
);
15011533
await this.app.commands.execute(
15021534
"setContext",
15031535
"codeql.supportsTrimCache",
1504-
newVersion.compare(
1536+
newVersionAndFeatures.version.compare(
15051537
CliVersionConstraint.CLI_VERSION_WITH_TRIM_CACHE,
15061538
) >= 0,
15071539
);
@@ -1512,23 +1544,23 @@ export class CodeQLCliServer implements Disposable {
15121544
throw e;
15131545
}
15141546
}
1515-
return this._version;
1547+
return this._versionAndFeatures;
15161548
}
15171549

15181550
public addVersionChangedListener(listener: VersionChangedListener) {
1519-
if (this._version) {
1520-
listener(this._version);
1551+
if (this._versionAndFeatures) {
1552+
listener(this._versionAndFeatures);
15211553
}
15221554
this._versionChangedListeners.push(listener);
15231555
}
15241556

1525-
private async refreshVersion() {
1557+
private async refreshVersion(): Promise<VersionAndFeatures> {
15261558
const distribution = await this.distributionProvider.getDistribution();
15271559
switch (distribution.kind) {
15281560
case FindDistributionResultKind.CompatibleDistribution:
15291561
// eslint-disable-next-line no-fallthrough -- Intentional fallthrough
15301562
case FindDistributionResultKind.IncompatibleDistribution:
1531-
return distribution.version;
1563+
return distribution.versionAndFeatures;
15321564

15331565
default:
15341566
// We should not get here because if no distributions are available, then
@@ -1745,4 +1777,8 @@ export class CliVersionConstraint {
17451777
CliVersionConstraint.CLI_VERSION_WITH_EXTENSIBLE_PREDICATE_METADATA,
17461778
);
17471779
}
1780+
1781+
async supportsMrvaPackCreate(): Promise<boolean> {
1782+
return (await this.cli.getFeatures()).mrvaPackCreate === true;
1783+
}
17481784
}

extensions/ql-vscode/src/codeql-cli/distribution.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { createWriteStream, mkdtemp, pathExists, remove } from "fs-extra";
22
import { tmpdir } from "os";
33
import { delimiter, dirname, join } from "path";
4-
import type { SemVer } from "semver";
54
import { Range, satisfies } from "semver";
65
import type { Event, ExtensionContext } from "vscode";
76
import type { DistributionConfig } from "../config";
87
import { extLogger } from "../common/logging/vscode";
8+
import type { VersionAndFeatures } from "./cli-version";
99
import { getCodeQlCliVersion } from "./cli-version";
1010
import type { ProgressCallback } from "../common/vscode/progress";
1111
import { reportStreamProgress } from "../common/vscode/progress";
@@ -88,11 +88,11 @@ export class DistributionManager implements DistributionProvider {
8888
kind: FindDistributionResultKind.NoDistribution,
8989
};
9090
}
91-
const version = await getCodeQlCliVersion(
91+
const versionAndFeatures = await getCodeQlCliVersion(
9292
distribution.codeQlPath,
9393
extLogger,
9494
);
95-
if (version === undefined) {
95+
if (versionAndFeatures === undefined) {
9696
return {
9797
distribution,
9898
kind: FindDistributionResultKind.UnknownCompatibilityDistribution,
@@ -119,17 +119,21 @@ export class DistributionManager implements DistributionProvider {
119119
distribution.kind !== DistributionKind.ExtensionManaged ||
120120
this.config.includePrerelease;
121121

122-
if (!satisfies(version, this.versionRange, { includePrerelease })) {
122+
if (
123+
!satisfies(versionAndFeatures.version, this.versionRange, {
124+
includePrerelease,
125+
})
126+
) {
123127
return {
124128
distribution,
125129
kind: FindDistributionResultKind.IncompatibleDistribution,
126-
version,
130+
versionAndFeatures,
127131
};
128132
}
129133
return {
130134
distribution,
131135
kind: FindDistributionResultKind.CompatibleDistribution,
132-
version,
136+
versionAndFeatures,
133137
};
134138
}
135139

@@ -599,7 +603,7 @@ interface DistributionResult {
599603

600604
interface CompatibleDistributionResult extends DistributionResult {
601605
kind: FindDistributionResultKind.CompatibleDistribution;
602-
version: SemVer;
606+
versionAndFeatures: VersionAndFeatures;
603607
}
604608

605609
interface UnknownCompatibilityDistributionResult extends DistributionResult {
@@ -608,7 +612,7 @@ interface UnknownCompatibilityDistributionResult extends DistributionResult {
608612

609613
interface IncompatibleDistributionResult extends DistributionResult {
610614
kind: FindDistributionResultKind.IncompatibleDistribution;
611-
version: SemVer;
615+
versionAndFeatures: VersionAndFeatures;
612616
}
613617

614618
interface NoDistributionResult {

extensions/ql-vscode/src/extension.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,7 @@ export async function activate(
418418
codeQlExtension.variantAnalysisManager,
419419
);
420420
codeQlExtension.cliServer.addVersionChangedListener((ver) => {
421-
telemetryListener.cliVersion = ver;
421+
telemetryListener.cliVersion = ver?.version;
422422
});
423423

424424
let unsupportedWarningShown = false;
@@ -431,13 +431,16 @@ export async function activate(
431431
return;
432432
}
433433

434-
if (CliVersionConstraint.OLDEST_SUPPORTED_CLI_VERSION.compare(ver) < 0) {
434+
if (
435+
CliVersionConstraint.OLDEST_SUPPORTED_CLI_VERSION.compare(ver.version) <
436+
0
437+
) {
435438
return;
436439
}
437440

438441
void showAndLogWarningMessage(
439442
extLogger,
440-
`You are using an unsupported version of the CodeQL CLI (${ver}). ` +
443+
`You are using an unsupported version of the CodeQL CLI (${ver.version}). ` +
441444
`The minimum supported version is ${CliVersionConstraint.OLDEST_SUPPORTED_CLI_VERSION}. ` +
442445
`Please upgrade to a newer version of the CodeQL CLI.`,
443446
);
@@ -592,7 +595,7 @@ async function getDistributionDisplayingDistributionWarnings(
592595
switch (result.kind) {
593596
case FindDistributionResultKind.CompatibleDistribution:
594597
void extLogger.log(
595-
`Found compatible version of CodeQL CLI (version ${result.version.raw})`,
598+
`Found compatible version of CodeQL CLI (version ${result.versionAndFeatures.version.raw})`,
596599
);
597600
break;
598601
case FindDistributionResultKind.IncompatibleDistribution: {
@@ -612,7 +615,7 @@ async function getDistributionDisplayingDistributionWarnings(
612615

613616
void showAndLogWarningMessage(
614617
extLogger,
615-
`The current version of the CodeQL CLI (${result.version.raw}) ` +
618+
`The current version of the CodeQL CLI (${result.versionAndFeatures.version.raw}) ` +
616619
`is incompatible with this extension. ${fixGuidanceMessage}`,
617620
);
618621
break;

0 commit comments

Comments
 (0)