Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4bb48879ec | ||
|
|
46e7382832 | ||
|
|
91bd7f5971 | ||
|
|
109c8755c3 | ||
|
|
218a14a4a1 | ||
|
|
71efe355f0 | ||
|
|
f7eee72b93 | ||
|
|
3bc884f45d | ||
|
|
ddf382d690 | ||
|
|
b84c429882 | ||
|
|
73a0bcacc8 | ||
|
|
60f47e8ee3 | ||
|
|
c29f4d4c79 | ||
|
|
71f74cb620 | ||
|
|
c4766e464b | ||
|
|
eba67f8f4f | ||
|
|
b7a97d34e5 | ||
|
|
18a9e2794e | ||
|
|
8208940532 | ||
|
|
71d4038744 | ||
|
|
034d8b7c68 | ||
|
|
e686b421ec | ||
|
|
9191873eb1 | ||
|
|
d924e9f649 | ||
|
|
e911bf4854 | ||
|
|
7b9e540332 | ||
|
|
577ce95cb1 | ||
|
|
63c8afab44 | ||
|
|
7777f9d643 | ||
|
|
6505e97b98 | ||
|
|
a6fc0d5493 | ||
|
|
572e74e079 | ||
|
|
c2de5fc9b6 | ||
|
|
c90dae89c1 | ||
|
|
110cf0ddc0 |
33
common/config/rush/pnpm-lock.yaml
generated
33
common/config/rush/pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# CodeQL for Visual Studio Code: Changelog
|
||||
|
||||
## 1.2.2 - 8 June 2020
|
||||
|
||||
- Fix auto-indentation rules.
|
||||
- Add ability to download platform-specific releases of the CodeQL CLI if they are available.
|
||||
- Fix handling of downloading prerelease versions of the CodeQL CLI.
|
||||
- Add pagination for displaying non-interpreted results.
|
||||
|
||||
## 1.2.1 - 29 May 2020
|
||||
|
||||
- Better formatting and autoindentation when adding QLDoc comments to `.ql` and `.qll` files.
|
||||
|
||||
@@ -30,9 +30,5 @@
|
||||
"end": "^\\s*//\\s*#?endregion\\b"
|
||||
}
|
||||
},
|
||||
"wordPattern": "(-?\\d*\\.\\d\\w*)|([^\\~\\!\\@\\#\\%\\^\\&\\*\\(\\)\\-\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\'\\\"\\.\\<\\>\\/\\?\\s]+)",
|
||||
"indentationRules": {
|
||||
"increaseIndentPattern": "^((?!.*?\\/\\*).*\\*/)?\\s*[\\}\\]].*$",
|
||||
"decreaseIndentPattern": "^((?!\\/\\/).)*(\\{[^}\"']*|\\([^)\"']*|\\[[^\\]\"']*)$"
|
||||
}
|
||||
"wordPattern": "(-?\\d*\\.\\d\\w*)|([^\\~\\!\\@\\#\\%\\^\\&\\*\\(\\)\\-\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\'\\\"\\.\\<\\>\\/\\?\\s]+)"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"description": "CodeQL for Visual Studio Code",
|
||||
"author": "GitHub",
|
||||
"private": true,
|
||||
"version": "1.2.1",
|
||||
"version": "1.2.2",
|
||||
"publisher": "GitHub",
|
||||
"license": "MIT",
|
||||
"icon": "media/VS-marketplace-CodeQL-icon.png",
|
||||
@@ -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",
|
||||
|
||||
@@ -101,3 +101,34 @@ export function adaptBqrs(schema: AdaptedSchema, page: DecodedBqrsChunk): RawRes
|
||||
rows: page.tuples.map(adaptRow),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* This type has two branches; we are in the process of changing from
|
||||
* one to the other. The old way is to parse them inside the webview,
|
||||
* the new way is to parse them in the extension. The main motivation
|
||||
* for this transition is to make pagination possible in such a way
|
||||
* that only one page needs to be sent from the extension to the webview.
|
||||
*/
|
||||
export type ParsedResultSets = ExtensionParsedResultSets | WebviewParsedResultSets;
|
||||
|
||||
/**
|
||||
* The old method doesn't require any nontrivial information to be included here,
|
||||
* just a tag to indicate that it is being used.
|
||||
*/
|
||||
export interface WebviewParsedResultSets {
|
||||
t: 'WebviewParsed';
|
||||
selectedTable?: string; // when undefined, means 'show default table'
|
||||
}
|
||||
|
||||
/**
|
||||
* The new method includes which bqrs page is being sent, and the
|
||||
* actual results parsed on the extension side.
|
||||
*/
|
||||
export interface ExtensionParsedResultSets {
|
||||
t: 'ExtensionParsed';
|
||||
pageNumber: number;
|
||||
numPages: number;
|
||||
selectedTable?: string; // when undefined, means 'show default table'
|
||||
resultSetNames: string[];
|
||||
resultSet: RawResultSet;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -35,16 +36,11 @@ const DEFAULT_DISTRIBUTION_OWNER_NAME = "github";
|
||||
const DEFAULT_DISTRIBUTION_REPOSITORY_NAME = "codeql-cli-binaries";
|
||||
|
||||
/**
|
||||
* Version constraint for the CLI.
|
||||
* Range of versions of the CLI that are compatible with the extension.
|
||||
*
|
||||
* This applies to both extension-managed and CLI distributions.
|
||||
*/
|
||||
export const DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT: VersionConstraint = {
|
||||
description: "2.*.*",
|
||||
isVersionCompatible: (v: Version) => {
|
||||
return v.majorVersion === 2 && v.minorVersion >= 0;
|
||||
}
|
||||
};
|
||||
export const DEFAULT_DISTRIBUTION_VERSION_RANGE: semver.Range = new semver.Range("2.x");
|
||||
|
||||
export interface DistributionProvider {
|
||||
getCodeQlPathWithoutVersionCheck(): Promise<string | undefined>;
|
||||
@@ -52,44 +48,63 @@ export interface DistributionProvider {
|
||||
}
|
||||
|
||||
export class DistributionManager implements DistributionProvider {
|
||||
constructor(extensionContext: ExtensionContext, config: DistributionConfig, versionConstraint: VersionConstraint) {
|
||||
constructor(extensionContext: ExtensionContext, config: DistributionConfig, versionRange: semver.Range) {
|
||||
this._config = config;
|
||||
this._extensionSpecificDistributionManager = new ExtensionSpecificDistributionManager(extensionContext, config, versionConstraint);
|
||||
this._extensionSpecificDistributionManager = new ExtensionSpecificDistributionManager(extensionContext, config, versionRange);
|
||||
this._onDidChangeDistribution = config.onDidChangeDistributionConfiguration;
|
||||
this._updateCheckRateLimiter = new InvocationRateLimiter(
|
||||
extensionContext,
|
||||
"extensionSpecificDistributionUpdateCheck",
|
||||
() => this._extensionSpecificDistributionManager.checkForUpdatesToDistribution()
|
||||
);
|
||||
this._versionConstraint = versionConstraint;
|
||||
this._versionRange = versionRange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up a CodeQL launcher binary.
|
||||
*/
|
||||
public async getDistribution(): Promise<FindDistributionResult> {
|
||||
const codeQlPath = await this.getCodeQlPathWithoutVersionCheck();
|
||||
if (codeQlPath === undefined) {
|
||||
const distribution = await this.getDistributionWithoutVersionCheck();
|
||||
if (distribution === undefined) {
|
||||
return {
|
||||
kind: FindDistributionResultKind.NoDistribution,
|
||||
};
|
||||
}
|
||||
const version = await getCodeQlCliVersion(codeQlPath, logger);
|
||||
if (version !== undefined && !this._versionConstraint.isVersionCompatible(version)) {
|
||||
const version = await getCodeQlCliVersion(distribution.codeQlPath, logger);
|
||||
if (version === undefined) {
|
||||
return {
|
||||
codeQlPath,
|
||||
distribution,
|
||||
kind: FindDistributionResultKind.UnknownCompatibilityDistribution,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies whether prerelease versions of the CodeQL CLI should be accepted.
|
||||
*
|
||||
* Suppose a user sets the includePrerelease config option, obtains a prerelease, then decides
|
||||
* they no longer want a prerelease, so unsets the includePrerelease config option.
|
||||
* Unsetting the includePrerelease config option should trigger an update check, and this
|
||||
* update check should present them an update that returns them back to a non-prerelease
|
||||
* version.
|
||||
*
|
||||
* Therefore, we adopt the following:
|
||||
*
|
||||
* - If the user is managing their own CLI, they can use a prerelease without specifying the
|
||||
* includePrerelease option.
|
||||
* - If the user is using an extension-managed CLI, then prereleases are only accepted when the
|
||||
* includePrerelease config option is set.
|
||||
*/
|
||||
const includePrerelease = distribution.kind !== DistributionKind.ExtensionManaged || this._config.includePrerelease;
|
||||
|
||||
if (!semver.satisfies(version, this._versionRange, { includePrerelease })) {
|
||||
return {
|
||||
distribution,
|
||||
kind: FindDistributionResultKind.IncompatibleDistribution,
|
||||
version,
|
||||
};
|
||||
}
|
||||
if (version === undefined) {
|
||||
return {
|
||||
codeQlPath,
|
||||
kind: FindDistributionResultKind.UnknownCompatibilityDistribution,
|
||||
};
|
||||
}
|
||||
return {
|
||||
codeQlPath,
|
||||
distribution,
|
||||
kind: FindDistributionResultKind.CompatibleDistribution,
|
||||
version
|
||||
};
|
||||
@@ -100,10 +115,15 @@ export class DistributionManager implements DistributionProvider {
|
||||
return result.kind !== FindDistributionResultKind.NoDistribution;
|
||||
}
|
||||
|
||||
public async getCodeQlPathWithoutVersionCheck(): Promise<string | undefined> {
|
||||
const distribution = await this.getDistributionWithoutVersionCheck();
|
||||
return distribution?.codeQlPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to a possibly-compatible CodeQL launcher binary, or undefined if a binary not be found.
|
||||
*/
|
||||
public async getCodeQlPathWithoutVersionCheck(): Promise<string | undefined> {
|
||||
async getDistributionWithoutVersionCheck(): Promise<Distribution | undefined> {
|
||||
// Check config setting, then extension specific distribution, then PATH.
|
||||
if (this._config.customCodeQlPath) {
|
||||
if (!await fs.pathExists(this._config.customCodeQlPath)) {
|
||||
@@ -121,19 +141,28 @@ export class DistributionManager implements DistributionProvider {
|
||||
) {
|
||||
warnDeprecatedLauncher();
|
||||
}
|
||||
return this._config.customCodeQlPath;
|
||||
return {
|
||||
codeQlPath: this._config.customCodeQlPath,
|
||||
kind: DistributionKind.CustomPathConfig
|
||||
};
|
||||
}
|
||||
|
||||
const extensionSpecificCodeQlPath = await this._extensionSpecificDistributionManager.getCodeQlPathWithoutVersionCheck();
|
||||
if (extensionSpecificCodeQlPath !== undefined) {
|
||||
return extensionSpecificCodeQlPath;
|
||||
return {
|
||||
codeQlPath: extensionSpecificCodeQlPath,
|
||||
kind: DistributionKind.ExtensionManaged
|
||||
};
|
||||
}
|
||||
|
||||
if (process.env.PATH) {
|
||||
for (const searchDirectory of process.env.PATH.split(path.delimiter)) {
|
||||
const expectedLauncherPath = await getExecutableFromDirectory(searchDirectory);
|
||||
if (expectedLauncherPath) {
|
||||
return expectedLauncherPath;
|
||||
return {
|
||||
codeQlPath: expectedLauncherPath,
|
||||
kind: DistributionKind.PathEnvironmentVariable
|
||||
};
|
||||
}
|
||||
}
|
||||
logger.log("INFO: Could not find CodeQL on path.");
|
||||
@@ -150,9 +179,9 @@ export class DistributionManager implements DistributionProvider {
|
||||
*/
|
||||
public async checkForUpdatesToExtensionManagedDistribution(
|
||||
minSecondsSinceLastUpdateCheck: number): Promise<DistributionUpdateCheckResult> {
|
||||
const codeQlPath = await this.getCodeQlPathWithoutVersionCheck();
|
||||
const distribution = await this.getDistributionWithoutVersionCheck();
|
||||
const extensionManagedCodeQlPath = await this._extensionSpecificDistributionManager.getCodeQlPathWithoutVersionCheck();
|
||||
if (codeQlPath !== undefined && codeQlPath !== extensionManagedCodeQlPath) {
|
||||
if (distribution?.codeQlPath !== extensionManagedCodeQlPath) {
|
||||
// A distribution is present but it isn't managed by the extension.
|
||||
return createInvalidLocationResult();
|
||||
}
|
||||
@@ -198,14 +227,14 @@ export class DistributionManager implements DistributionProvider {
|
||||
private readonly _extensionSpecificDistributionManager: ExtensionSpecificDistributionManager;
|
||||
private readonly _updateCheckRateLimiter: InvocationRateLimiter<DistributionUpdateCheckResult>;
|
||||
private readonly _onDidChangeDistribution: Event<void> | undefined;
|
||||
private readonly _versionConstraint: VersionConstraint;
|
||||
private readonly _versionRange: semver.Range;
|
||||
}
|
||||
|
||||
class ExtensionSpecificDistributionManager {
|
||||
constructor(extensionContext: ExtensionContext, config: DistributionConfig, versionConstraint: VersionConstraint) {
|
||||
constructor(extensionContext: ExtensionContext, config: DistributionConfig, versionRange: semver.Range) {
|
||||
this._extensionContext = extensionContext;
|
||||
this._config = config;
|
||||
this._versionConstraint = versionConstraint;
|
||||
this._versionRange = versionRange;
|
||||
}
|
||||
|
||||
public async getCodeQlPathWithoutVersionCheck(): Promise<string | undefined> {
|
||||
@@ -268,7 +297,18 @@ class ExtensionSpecificDistributionManager {
|
||||
`but encountered an error: ${e}.`);
|
||||
}
|
||||
|
||||
const assetStream = await this.createReleasesApiConsumer().streamBinaryContentOfAsset(release.assets[0]);
|
||||
// Filter assets to the unique one that we require.
|
||||
const requiredAssetName = this.getRequiredAssetName();
|
||||
const assets = release.assets.filter(asset => asset.name === requiredAssetName);
|
||||
if (assets.length === 0) {
|
||||
throw new Error(`Invariant violation: chose a release to install that didn't have ${requiredAssetName}`);
|
||||
}
|
||||
if (assets.length > 1) {
|
||||
logger.log('WARNING: chose a release with more than one asset to install, found ' +
|
||||
assets.map(asset => asset.name).join(', '));
|
||||
}
|
||||
|
||||
const assetStream = await this.createReleasesApiConsumer().streamBinaryContentOfAsset(assets[0]);
|
||||
const tmpDirectory = await fs.mkdtemp(path.join(os.tmpdir(), "vscode-codeql"));
|
||||
|
||||
try {
|
||||
@@ -325,12 +365,36 @@ class ExtensionSpecificDistributionManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the codeql cli installation we prefer to install, based on our current platform.
|
||||
*/
|
||||
private getRequiredAssetName(): string {
|
||||
if (os.platform() === 'linux') return 'codeql-linux64.zip';
|
||||
if (os.platform() === 'darwin') return 'codeql-osx64.zip';
|
||||
if (os.platform() === 'win32') return 'codeql-win64.zip';
|
||||
return 'codeql.zip';
|
||||
}
|
||||
|
||||
private async getLatestRelease(): Promise<Release> {
|
||||
const release = await this.createReleasesApiConsumer().getLatestRelease(this._versionConstraint, this._config.includePrerelease);
|
||||
if (release.assets.length !== 1) {
|
||||
throw new Error("Release had an unexpected number of assets");
|
||||
}
|
||||
return release;
|
||||
const requiredAssetName = this.getRequiredAssetName();
|
||||
logger.log(`Searching for latest release including ${requiredAssetName}.`);
|
||||
return this.createReleasesApiConsumer().getLatestRelease(
|
||||
this._versionRange,
|
||||
this._config.includePrerelease,
|
||||
release => {
|
||||
const matchingAssets = release.assets.filter(asset => asset.name === requiredAssetName);
|
||||
if (matchingAssets.length === 0) {
|
||||
// For example, this could be a release with no platform-specific assets.
|
||||
logger.log(`INFO: Ignoring a release with no assets named ${requiredAssetName}`);
|
||||
return false;
|
||||
}
|
||||
if (matchingAssets.length > 1) {
|
||||
logger.log(`WARNING: Ignoring a release with more than one asset named ${requiredAssetName}`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private createReleasesApiConsumer(): ReleasesApiConsumer {
|
||||
@@ -369,7 +433,7 @@ class ExtensionSpecificDistributionManager {
|
||||
|
||||
private readonly _config: DistributionConfig;
|
||||
private readonly _extensionContext: ExtensionContext;
|
||||
private readonly _versionConstraint: VersionConstraint;
|
||||
private readonly _versionRange: semver.Range;
|
||||
|
||||
private static readonly _currentDistributionFolderBaseName = "distribution";
|
||||
private static readonly _currentDistributionFolderIndexStateKey = "distributionFolderIndex";
|
||||
@@ -390,7 +454,7 @@ export class ReleasesApiConsumer {
|
||||
this._repoName = repoName;
|
||||
}
|
||||
|
||||
public async getLatestRelease(versionConstraint: VersionConstraint, includePrerelease = false): Promise<Release> {
|
||||
public async getLatestRelease(versionRange: semver.Range, includePrerelease = false, additionalCompatibilityCheck?: (release: GithubRelease) => boolean): Promise<Release> {
|
||||
const apiPath = `/repos/${this._ownerName}/${this._repoName}/releases`;
|
||||
const allReleases: GithubRelease[] = await (await this.makeApiCall(apiPath)).json();
|
||||
const compatibleReleases = allReleases.filter(release => {
|
||||
@@ -398,20 +462,20 @@ export class ReleasesApiConsumer {
|
||||
return false;
|
||||
}
|
||||
|
||||
const version = tryParseVersionString(release.tag_name);
|
||||
if (version === undefined || !versionConstraint.isVersionCompatible(version)) {
|
||||
const version = semver.parse(release.tag_name);
|
||||
if (version === null || !semver.satisfies(version, versionRange, { includePrerelease })) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return !additionalCompatibilityCheck || additionalCompatibilityCheck(release);
|
||||
});
|
||||
// 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. " +
|
||||
@@ -511,29 +575,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";
|
||||
}
|
||||
@@ -550,6 +591,17 @@ function isRedirectStatusCode(statusCode: number): boolean {
|
||||
* Types and helper functions relating to those types.
|
||||
*/
|
||||
|
||||
export enum DistributionKind {
|
||||
CustomPathConfig,
|
||||
ExtensionManaged,
|
||||
PathEnvironmentVariable
|
||||
}
|
||||
|
||||
export interface Distribution {
|
||||
codeQlPath: string;
|
||||
kind: DistributionKind;
|
||||
}
|
||||
|
||||
export enum FindDistributionResultKind {
|
||||
CompatibleDistribution,
|
||||
UnknownCompatibilityDistribution,
|
||||
@@ -563,21 +615,27 @@ export type FindDistributionResult =
|
||||
| IncompatibleDistributionResult
|
||||
| NoDistributionResult;
|
||||
|
||||
interface CompatibleDistributionResult {
|
||||
codeQlPath: string;
|
||||
kind: FindDistributionResultKind.CompatibleDistribution;
|
||||
version: Version;
|
||||
/**
|
||||
* A result representing a distribution of the CodeQL CLI that may or may not be compatible with
|
||||
* the extension.
|
||||
*/
|
||||
interface DistributionResult {
|
||||
distribution: Distribution;
|
||||
kind: FindDistributionResultKind;
|
||||
}
|
||||
|
||||
interface UnknownCompatibilityDistributionResult {
|
||||
codeQlPath: string;
|
||||
interface CompatibleDistributionResult extends DistributionResult {
|
||||
kind: FindDistributionResultKind.CompatibleDistribution;
|
||||
version: semver.SemVer;
|
||||
}
|
||||
|
||||
interface UnknownCompatibilityDistributionResult extends DistributionResult {
|
||||
kind: FindDistributionResultKind.UnknownCompatibilityDistribution;
|
||||
}
|
||||
|
||||
interface IncompatibleDistributionResult {
|
||||
codeQlPath: string;
|
||||
interface IncompatibleDistributionResult extends DistributionResult {
|
||||
kind: FindDistributionResultKind.IncompatibleDistribution;
|
||||
version: Version;
|
||||
version: semver.SemVer;
|
||||
}
|
||||
|
||||
interface NoDistributionResult {
|
||||
@@ -717,7 +775,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;
|
||||
|
||||
@@ -762,11 +820,6 @@ export interface GithubReleaseAsset {
|
||||
size: number;
|
||||
}
|
||||
|
||||
interface VersionConstraint {
|
||||
description: string;
|
||||
isVersionCompatible(version: Version): boolean;
|
||||
}
|
||||
|
||||
export class GithubApiError extends Error {
|
||||
constructor(public status: number, public body: string) {
|
||||
super(`API call failed with status code ${status}, body: ${body}`);
|
||||
|
||||
@@ -8,7 +8,16 @@ import * as languageSupport from './languageSupport';
|
||||
import { DatabaseManager } from './databases';
|
||||
import { DatabaseUI } from './databases-ui';
|
||||
import { TemplateQueryDefinitionProvider, TemplateQueryReferenceProvider } from './definitions';
|
||||
import { DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT, DistributionManager, DistributionUpdateCheckResultKind, FindDistributionResult, FindDistributionResultKind, GithubApiError, GithubRateLimitedError } from './distribution';
|
||||
import {
|
||||
DEFAULT_DISTRIBUTION_VERSION_RANGE,
|
||||
DistributionKind,
|
||||
DistributionManager,
|
||||
DistributionUpdateCheckResultKind,
|
||||
FindDistributionResult,
|
||||
FindDistributionResultKind,
|
||||
GithubApiError,
|
||||
GithubRateLimitedError
|
||||
} from './distribution';
|
||||
import * as helpers from './helpers';
|
||||
import { assertNever } from './helpers-pure';
|
||||
import { spawnIdeServer } from './ide-server';
|
||||
@@ -83,7 +92,8 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
|
||||
|
||||
const distributionConfigListener = new DistributionConfigListener();
|
||||
ctx.subscriptions.push(distributionConfigListener);
|
||||
const distributionManager = new DistributionManager(ctx, distributionConfigListener, DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT);
|
||||
const codeQlVersionRange = DEFAULT_DISTRIBUTION_VERSION_RANGE;
|
||||
const distributionManager = new DistributionManager(ctx, distributionConfigListener, codeQlVersionRange);
|
||||
|
||||
const shouldUpdateOnNextActivationKey = "shouldUpdateOnNextActivation";
|
||||
|
||||
@@ -182,11 +192,25 @@ 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.");
|
||||
case FindDistributionResultKind.IncompatibleDistribution: {
|
||||
const fixGuidanceMessage = (() => {
|
||||
switch (result.distribution.kind) {
|
||||
case DistributionKind.ExtensionManaged:
|
||||
return "Please update the CodeQL CLI by running the \"CodeQL: Check for CLI Updates\" command.";
|
||||
case DistributionKind.CustomPathConfig:
|
||||
return `Please update the \"CodeQL CLI Executable Path\" setting to point to a CLI in the version range ${codeQlVersionRange}.`;
|
||||
case DistributionKind.PathEnvironmentVariable:
|
||||
return `Please update the CodeQL CLI on your PATH to a version compatible with ${codeQlVersionRange}, or ` +
|
||||
`set the \"CodeQL CLI Executable Path\" setting to the path of a CLI version compatible with ${codeQlVersionRange}.`;
|
||||
}
|
||||
})();
|
||||
|
||||
helpers.showAndLogWarningMessage(`The current version of the CodeQL CLI (${result.version.raw}) ` +
|
||||
"is incompatible with this extension. " + fixGuidanceMessage);
|
||||
break;
|
||||
}
|
||||
case FindDistributionResultKind.UnknownCompatibilityDistribution:
|
||||
helpers.showAndLogWarningMessage("Compatibility with the configured CodeQL CLI could not be determined. " +
|
||||
"You may experience problems using the extension.");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as sarif from 'sarif';
|
||||
import { ResolvableLocationValue } from 'semmle-bqrs';
|
||||
import { RawResultSet } from './adapt';
|
||||
import { ParsedResultSets } from './adapt';
|
||||
|
||||
/**
|
||||
* Only ever show this many results per run in interpreted results.
|
||||
@@ -12,6 +12,11 @@ export const INTERPRETED_RESULTS_PER_RUN_LIMIT = 100;
|
||||
*/
|
||||
export const RAW_RESULTS_LIMIT = 10000;
|
||||
|
||||
/**
|
||||
* Show this many rows in a raw result table at a time.
|
||||
*/
|
||||
export const RAW_RESULTS_PAGE_SIZE = 100;
|
||||
|
||||
export interface DatabaseInfo {
|
||||
name: string;
|
||||
databaseUri: string;
|
||||
@@ -81,9 +86,10 @@ export interface SetStateMsg {
|
||||
|
||||
/**
|
||||
* An experimental way of providing results from the extension.
|
||||
* Should be undefined unless config.EXPERIMENTAL_BQRS_SETTING is set to true.
|
||||
* Should be in the WebviewParsedResultSets branch of the type
|
||||
* unless config.EXPERIMENTAL_BQRS_SETTING is set to true.
|
||||
*/
|
||||
resultSets?: RawResultSet[];
|
||||
parsedResultSets: ParsedResultSets;
|
||||
}
|
||||
|
||||
/** Advance to the next or previous path no in the path viewer */
|
||||
@@ -101,7 +107,8 @@ export type FromResultsViewMsg =
|
||||
| ToggleDiagnostics
|
||||
| ChangeRawResultsSortMsg
|
||||
| ChangeInterpretedResultsSortMsg
|
||||
| ResultViewLoaded;
|
||||
| ResultViewLoaded
|
||||
| ChangePage;
|
||||
|
||||
interface ViewSourceFileMsg {
|
||||
t: 'viewSourceFile';
|
||||
@@ -122,6 +129,12 @@ interface ResultViewLoaded {
|
||||
t: 'resultViewLoaded';
|
||||
}
|
||||
|
||||
interface ChangePage {
|
||||
t: 'changePage';
|
||||
pageNumber: number; // 0-indexed, displayed to the user as 1-indexed
|
||||
selectedTable: string;
|
||||
}
|
||||
|
||||
export enum SortDirection {
|
||||
asc, desc
|
||||
}
|
||||
|
||||
22
extensions/ql-vscode/src/interface-utils.ts
Normal file
22
extensions/ql-vscode/src/interface-utils.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { RawResultSet } from "./adapt";
|
||||
import { ResultSetSchema } from "semmle-bqrs";
|
||||
import { Interpretation } from "./interface-types";
|
||||
|
||||
export const SELECT_TABLE_NAME = '#select';
|
||||
export const ALERTS_TABLE_NAME = 'alerts';
|
||||
|
||||
export type RawTableResultSet = { t: 'RawResultSet' } & RawResultSet;
|
||||
export type PathTableResultSet = { t: 'SarifResultSet'; readonly schema: ResultSetSchema; name: string } & Interpretation;
|
||||
|
||||
export type ResultSet =
|
||||
| RawTableResultSet
|
||||
| PathTableResultSet;
|
||||
|
||||
export function getDefaultResultSet(resultSets: readonly ResultSet[]): string {
|
||||
return getDefaultResultSetName(resultSets.map(resultSet => resultSet.schema.name));
|
||||
}
|
||||
|
||||
export function getDefaultResultSetName(resultSetNames: readonly string[]): string {
|
||||
// Choose first available result set from the array
|
||||
return [ALERTS_TABLE_NAME, SELECT_TABLE_NAME, resultSetNames[0]].filter(resultSetName => resultSetNames.includes(resultSetName))[0];
|
||||
}
|
||||
@@ -10,14 +10,15 @@ import { CodeQLCliServer } from './cli';
|
||||
import { DatabaseItem, DatabaseManager } from './databases';
|
||||
import { showAndLogErrorMessage } from './helpers';
|
||||
import { assertNever } from './helpers-pure';
|
||||
import { FromResultsViewMsg, Interpretation, INTERPRETED_RESULTS_PER_RUN_LIMIT, IntoResultsViewMsg, QueryMetadata, ResultsPaths, SortedResultSetInfo, SortedResultsMap, InterpretedResultsSortState, SortDirection } from './interface-types';
|
||||
import { FromResultsViewMsg, Interpretation, INTERPRETED_RESULTS_PER_RUN_LIMIT, IntoResultsViewMsg, QueryMetadata, ResultsPaths, SortedResultSetInfo, SortedResultsMap, InterpretedResultsSortState, SortDirection, RAW_RESULTS_PAGE_SIZE } from './interface-types';
|
||||
import { Logger } from './logging';
|
||||
import * as messages from './messages';
|
||||
import { CompletedQuery, interpretResults } from './query-results';
|
||||
import { QueryInfo, tmpDir } from './run-queries';
|
||||
import { parseSarifLocation, parseSarifPlainTextMessage } from './sarif-utils';
|
||||
import { adaptSchema, adaptBqrs, RawResultSet } from './adapt';
|
||||
import { adaptSchema, adaptBqrs, RawResultSet, ParsedResultSets } from './adapt';
|
||||
import { EXPERIMENTAL_BQRS_SETTING } from './config';
|
||||
import { getDefaultResultSetName } from './interface-utils';
|
||||
|
||||
/**
|
||||
* interface.ts
|
||||
@@ -115,8 +116,13 @@ function sortInterpretedResults(results: Sarif.Result[], sortState: InterpretedR
|
||||
}
|
||||
}
|
||||
|
||||
function numPagesOfResultSet(resultSet: RawResultSet): number {
|
||||
return Math.ceil(resultSet.schema.tupleCount / RAW_RESULTS_PAGE_SIZE);
|
||||
}
|
||||
|
||||
export class InterfaceManager extends DisposableObject {
|
||||
private _displayedQuery?: CompletedQuery;
|
||||
private _interpretation?: Interpretation;
|
||||
private _panel: vscode.WebviewPanel | undefined;
|
||||
private _panelLoaded = false;
|
||||
private _panelLoadedCallBacks: (() => void)[] = [];
|
||||
@@ -288,6 +294,9 @@ export class InterfaceManager extends DisposableObject {
|
||||
query.updateInterpretedSortState(this.cliServer, msg.sortState)
|
||||
);
|
||||
break;
|
||||
case "changePage":
|
||||
await this.showPageOfResults(msg.selectedTable, msg.pageNumber);
|
||||
break;
|
||||
default:
|
||||
assertNever(msg);
|
||||
}
|
||||
@@ -339,6 +348,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
);
|
||||
|
||||
this._displayedQuery = results;
|
||||
this._interpretation = interpretation;
|
||||
|
||||
const panel = this.getPanel();
|
||||
await this.waitForPanelLoaded();
|
||||
@@ -364,18 +374,37 @@ export class InterfaceManager extends DisposableObject {
|
||||
});
|
||||
}
|
||||
|
||||
let resultSets: RawResultSet[] | undefined;
|
||||
const getParsedResultSets = async (): Promise<ParsedResultSets> => {
|
||||
if (EXPERIMENTAL_BQRS_SETTING.getValue()) {
|
||||
const schemas = await this.cliServer.bqrsInfo(results.query.resultsPaths.resultsPath, RAW_RESULTS_PAGE_SIZE);
|
||||
|
||||
if (EXPERIMENTAL_BQRS_SETTING.getValue()) {
|
||||
resultSets = [];
|
||||
const schemas = await this.cliServer.bqrsInfo(results.query.resultsPaths.resultsPath);
|
||||
for (const schema of schemas["result-sets"]) {
|
||||
const chunk = await this.cliServer.bqrsDecode(results.query.resultsPaths.resultsPath, schema.name);
|
||||
const resultSetNames = schemas["result-sets"].map(resultSet => resultSet.name);
|
||||
|
||||
// This may not wind up being the page we actually show, if there are interpreted results,
|
||||
// but speculatively send it anyway.
|
||||
const selectedTable = getDefaultResultSetName(resultSetNames);
|
||||
const schema = schemas["result-sets"].find(resultSet => resultSet.name == selectedTable)!;
|
||||
if (schema === undefined) {
|
||||
return { t: 'WebviewParsed' };
|
||||
}
|
||||
|
||||
const chunk = await this.cliServer.bqrsDecode(results.query.resultsPaths.resultsPath, schema.name, RAW_RESULTS_PAGE_SIZE, schema.pagination?.offsets[0]);
|
||||
const adaptedSchema = adaptSchema(schema);
|
||||
const resultSet = adaptBqrs(adaptedSchema, chunk);
|
||||
resultSets.push(resultSet);
|
||||
|
||||
return {
|
||||
t: 'ExtensionParsed',
|
||||
pageNumber: 0,
|
||||
numPages: numPagesOfResultSet(resultSet),
|
||||
resultSet,
|
||||
selectedTable: undefined,
|
||||
resultSetNames
|
||||
};
|
||||
}
|
||||
}
|
||||
else {
|
||||
return { t: 'WebviewParsed' };
|
||||
}
|
||||
};
|
||||
|
||||
await this.postMessage({
|
||||
t: "setState",
|
||||
@@ -384,7 +413,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
resultsPath: this.convertPathToWebviewUri(
|
||||
results.query.resultsPaths.resultsPath
|
||||
),
|
||||
resultSets,
|
||||
parsedResultSets: await getParsedResultSets(),
|
||||
sortedResultsMap,
|
||||
database: results.database,
|
||||
shouldKeepOldResultsWhileRendering,
|
||||
@@ -392,6 +421,59 @@ export class InterfaceManager extends DisposableObject {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a page of raw results from the chosen table.
|
||||
*/
|
||||
public async showPageOfResults(selectedTable: string, pageNumber: number): Promise<void> {
|
||||
const results = this._displayedQuery;
|
||||
if (results === undefined) {
|
||||
throw new Error('trying to view a page of a query that is not loaded');
|
||||
}
|
||||
|
||||
const sortedResultsMap: SortedResultsMap = {};
|
||||
results.sortedResultsInfo.forEach(
|
||||
(v, k) =>
|
||||
(sortedResultsMap[k] = this.convertPathPropertiesToWebviewUris(
|
||||
v
|
||||
))
|
||||
);
|
||||
|
||||
const schemas = await this.cliServer.bqrsInfo(results.query.resultsPaths.resultsPath, RAW_RESULTS_PAGE_SIZE);
|
||||
|
||||
const resultSetNames = schemas["result-sets"].map(resultSet => resultSet.name);
|
||||
|
||||
const schema = schemas["result-sets"].find(resultSet => resultSet.name == selectedTable)!;
|
||||
if (schema === undefined)
|
||||
throw new Error(`Query result set '${selectedTable}' not found.`);
|
||||
|
||||
const chunk = await this.cliServer.bqrsDecode(results.query.resultsPaths.resultsPath, schema.name, RAW_RESULTS_PAGE_SIZE, schema.pagination?.offsets[pageNumber]);
|
||||
const adaptedSchema = adaptSchema(schema);
|
||||
const resultSet = adaptBqrs(adaptedSchema, chunk);
|
||||
|
||||
const parsedResultSets: ParsedResultSets = {
|
||||
t: 'ExtensionParsed',
|
||||
pageNumber,
|
||||
resultSet,
|
||||
numPages: numPagesOfResultSet(resultSet),
|
||||
selectedTable: selectedTable,
|
||||
resultSetNames
|
||||
};
|
||||
|
||||
await this.postMessage({
|
||||
t: "setState",
|
||||
interpretation: this._interpretation,
|
||||
origResultsPaths: results.query.resultsPaths,
|
||||
resultsPath: this.convertPathToWebviewUri(
|
||||
results.query.resultsPaths.resultsPath
|
||||
),
|
||||
parsedResultSets,
|
||||
sortedResultsMap,
|
||||
database: results.database,
|
||||
shouldKeepOldResultsWhileRendering: false,
|
||||
metadata: results.query.metadata
|
||||
});
|
||||
}
|
||||
|
||||
private async getTruncatedResults(
|
||||
metadata: QueryMetadata | undefined,
|
||||
resultsPaths: ResultsPaths,
|
||||
|
||||
@@ -17,11 +17,14 @@ export function install() {
|
||||
// setLanguageConfiguration requires a regexp for the wordpattern, not a string
|
||||
langConfig.wordPattern = new RegExp(langConfig.wordPattern);
|
||||
langConfig.onEnterRules = onEnterRules;
|
||||
langConfig.indentationRules = {
|
||||
decreaseIndentPattern: /^((?!.*?\/\*).*\*\/)?\s*[\}\]].*$/,
|
||||
increaseIndentPattern: /^((?!\/\/).)*(\{[^}"'`]*|\([^)"'`]*|\[[^\]"'`]*)$/
|
||||
};
|
||||
|
||||
languages.setLanguageConfiguration('ql', langConfig);
|
||||
languages.setLanguageConfiguration('qll', langConfig);
|
||||
languages.setLanguageConfiguration('dbscheme', langConfig);
|
||||
|
||||
}
|
||||
|
||||
const onEnterRules = [
|
||||
|
||||
@@ -223,11 +223,16 @@ export class QueryHistoryManager {
|
||||
if (queryHistoryItem.logFileLocation) {
|
||||
const uri = vscode.Uri.file(queryHistoryItem.logFileLocation);
|
||||
try {
|
||||
await vscode.window.showTextDocument(uri, {
|
||||
});
|
||||
await vscode.window.showTextDocument(uri);
|
||||
} catch (e) {
|
||||
if (e.message.includes('Files above 50MB cannot be synchronized with extensions')) {
|
||||
const res = await helpers.showBinaryChoiceDialog('File is too large to open in the editor, do you want to open it externally?');
|
||||
const res = await helpers.showBinaryChoiceDialog(
|
||||
`VS Code does not allow extensions to open files >50MB. This file
|
||||
exceeds that limit. Do you want to open it outside of VS Code?
|
||||
|
||||
You can also try manually opening it inside VS Code by selecting
|
||||
the file in the file explorer and dragging it into the workspace.`
|
||||
);
|
||||
if (res) {
|
||||
try {
|
||||
await vscode.commands.executeCommand('revealFileInOS', uri);
|
||||
|
||||
@@ -5,9 +5,10 @@ import * as Keys from '../result-keys';
|
||||
import { LocationStyle } from 'semmle-bqrs';
|
||||
import * as octicons from './octicons';
|
||||
import { className, renderLocation, ResultTableProps, zebraStripe, selectableZebraStripe, jumpToLocation, nextSortDirection } from './result-table-utils';
|
||||
import { PathTableResultSet, onNavigation, NavigationEvent, vscode } from './results';
|
||||
import { onNavigation, NavigationEvent, vscode } from './results';
|
||||
import { parseSarifPlainTextMessage, parseSarifLocation } from '../sarif-utils';
|
||||
import { InterpretedResultsSortColumn, SortDirection, InterpretedResultsSortState } from '../interface-types';
|
||||
import { PathTableResultSet } from '../interface-utils';
|
||||
|
||||
export type PathTableProps = ResultTableProps & { resultSet: PathTableResultSet };
|
||||
export interface PathTableState {
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import * as React from "react";
|
||||
import { renderLocation, ResultTableProps, zebraStripe, className, nextSortDirection } from "./result-table-utils";
|
||||
import { RawTableResultSet, vscode } from "./results";
|
||||
import { vscode } from "./results";
|
||||
import { ResultValue } from "../adapt";
|
||||
import { SortDirection, RAW_RESULTS_LIMIT, RawResultsSortState } from "../interface-types";
|
||||
import { RawTableResultSet } from "../interface-utils";
|
||||
|
||||
export type RawTableProps = ResultTableProps & {
|
||||
resultSet: RawTableResultSet;
|
||||
sortState?: RawResultsSortState;
|
||||
offset: number;
|
||||
};
|
||||
|
||||
export class RawTable extends React.Component<RawTableProps, {}> {
|
||||
@@ -28,7 +30,7 @@ export class RawTable extends React.Component<RawTableProps, {}> {
|
||||
<tr key={rowIndex} {...zebraStripe(rowIndex)}>
|
||||
{
|
||||
[
|
||||
<td key={-1}>{rowIndex + 1}</td>,
|
||||
<td key={-1}>{rowIndex + 1 + this.props.offset}</td>,
|
||||
...row.map((value, columnIndex) =>
|
||||
<td key={columnIndex}>
|
||||
{
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import * as React from 'react';
|
||||
import { LocationValue, ResolvableLocationValue, tryGetResolvableLocation } from 'semmle-bqrs';
|
||||
import { RawResultsSortState, QueryMetadata, SortDirection } from '../interface-types';
|
||||
import { ResultSet, vscode } from './results';
|
||||
import { vscode } from './results';
|
||||
import { assertNever } from '../helpers-pure';
|
||||
import { ResultSet } from '../interface-utils';
|
||||
|
||||
export interface ResultTableProps {
|
||||
resultSet: ResultSet;
|
||||
@@ -10,6 +11,7 @@ export interface ResultTableProps {
|
||||
metadata?: QueryMetadata;
|
||||
resultsPath: string | undefined;
|
||||
sortState?: RawResultsSortState;
|
||||
offset: number;
|
||||
|
||||
/**
|
||||
* Holds if there are any raw results. When that is the case, we
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import * as React from 'react';
|
||||
import { DatabaseInfo, Interpretation, RawResultsSortState, QueryMetadata, ResultsPaths, InterpretedResultsSortState } from '../interface-types';
|
||||
import { DatabaseInfo, Interpretation, RawResultsSortState, QueryMetadata, ResultsPaths, InterpretedResultsSortState, RAW_RESULTS_PAGE_SIZE } from '../interface-types';
|
||||
import { PathTable } from './alert-table';
|
||||
import { RawTable } from './raw-results-table';
|
||||
import { ResultTableProps, tableSelectionHeaderClassName, toggleDiagnosticsClassName, alertExtrasClassName } from './result-table-utils';
|
||||
import { ResultSet, vscode } from './results';
|
||||
import { vscode } from './results';
|
||||
import { ParsedResultSets, ExtensionParsedResultSets } from '../adapt';
|
||||
import { ResultSet, ALERTS_TABLE_NAME, SELECT_TABLE_NAME, getDefaultResultSet } from '../interface-utils';
|
||||
|
||||
/**
|
||||
* Properties for the `ResultTables` component.
|
||||
*/
|
||||
export interface ResultTablesProps {
|
||||
parsedResultSets: ParsedResultSets;
|
||||
rawResultSets: readonly ResultSet[];
|
||||
interpretation: Interpretation | undefined;
|
||||
database: DatabaseInfo;
|
||||
@@ -25,10 +28,9 @@ export interface ResultTablesProps {
|
||||
*/
|
||||
interface ResultTablesState {
|
||||
selectedTable: string; // name of selected result set
|
||||
selectedPage: string; // stringified selected page
|
||||
}
|
||||
|
||||
const ALERTS_TABLE_NAME = 'alerts';
|
||||
const SELECT_TABLE_NAME = '#select';
|
||||
const UPDATING_RESULTS_TEXT_CLASS_NAME = "vscode-codeql__result-tables-updating-text";
|
||||
|
||||
function getResultCount(resultSet: ResultSet): number {
|
||||
@@ -75,23 +77,66 @@ export class ResultTables
|
||||
return resultSets;
|
||||
}
|
||||
|
||||
private getResultSetNames(resultSets: ResultSet[]): string[] {
|
||||
if (this.props.parsedResultSets.t === 'ExtensionParsed') {
|
||||
return this.props.parsedResultSets.resultSetNames.concat([ALERTS_TABLE_NAME]);
|
||||
}
|
||||
else {
|
||||
return resultSets.map(resultSet => resultSet.schema.name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if we have a result set obtained from the extension that came
|
||||
* from the ExtensionParsed branch of ParsedResultSets. This is evidence
|
||||
* that the user has the experimental flag turned on that allows extension-side
|
||||
* bqrs parsing.
|
||||
*/
|
||||
paginationAllowed(): boolean {
|
||||
return this.props.parsedResultSets.t === 'ExtensionParsed';
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if we actually should show pagination interface right now. This is
|
||||
* still false for the time being when we're viewing alerts.
|
||||
*/
|
||||
paginationEnabled(): boolean {
|
||||
return this.paginationAllowed() &&
|
||||
this.props.parsedResultSets.selectedTable !== ALERTS_TABLE_NAME &&
|
||||
this.state.selectedTable !== ALERTS_TABLE_NAME;
|
||||
}
|
||||
|
||||
constructor(props: ResultTablesProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
// Get the result set that should be displayed by default
|
||||
selectedTable: ResultTables.getDefaultResultSet(this.getResultSets())
|
||||
};
|
||||
}
|
||||
const selectedTable = props.parsedResultSets.selectedTable || getDefaultResultSet(this.getResultSets());
|
||||
|
||||
private static getDefaultResultSet(resultSets: readonly ResultSet[]): string {
|
||||
const resultSetNames = resultSets.map(resultSet => resultSet.schema.name);
|
||||
// Choose first available result set from the array
|
||||
return [ALERTS_TABLE_NAME, SELECT_TABLE_NAME, resultSets[0].schema.name].filter(resultSetName => resultSetNames.includes(resultSetName))[0];
|
||||
let selectedPage: string;
|
||||
switch (props.parsedResultSets.t) {
|
||||
case 'ExtensionParsed':
|
||||
selectedPage = (props.parsedResultSets.pageNumber + 1) + '';
|
||||
break;
|
||||
case 'WebviewParsed':
|
||||
selectedPage = '';
|
||||
break;
|
||||
}
|
||||
|
||||
this.state = { selectedTable, selectedPage };
|
||||
}
|
||||
|
||||
private onTableSelectionChange = (event: React.ChangeEvent<HTMLSelectElement>): void => {
|
||||
this.setState({ selectedTable: event.target.value });
|
||||
const selectedTable = event.target.value;
|
||||
const fetchPageFromExtension = this.paginationAllowed() && selectedTable !== ALERTS_TABLE_NAME;
|
||||
|
||||
if (fetchPageFromExtension) {
|
||||
vscode.postMessage({
|
||||
t: 'changePage',
|
||||
pageNumber: 0,
|
||||
selectedTable
|
||||
});
|
||||
}
|
||||
else
|
||||
this.setState({ selectedTable });
|
||||
}
|
||||
|
||||
private alertTableExtras(): JSX.Element | undefined {
|
||||
@@ -118,24 +163,81 @@ export class ResultTables
|
||||
</div>;
|
||||
}
|
||||
|
||||
getOffset(): number {
|
||||
const { parsedResultSets } = this.props;
|
||||
switch (parsedResultSets.t) {
|
||||
case 'ExtensionParsed':
|
||||
return parsedResultSets.pageNumber * RAW_RESULTS_PAGE_SIZE;
|
||||
case 'WebviewParsed':
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
renderPageButtons(resultSets: ExtensionParsedResultSets): JSX.Element {
|
||||
const selectedTable = this.state.selectedTable;
|
||||
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ selectedPage: e.target.value });
|
||||
};
|
||||
const choosePage = (input: string) => {
|
||||
const pageNumber = parseInt(input);
|
||||
if (pageNumber !== undefined && !isNaN(pageNumber)) {
|
||||
const actualPageNumber = Math.max(0, Math.min(pageNumber - 1, resultSets.numPages - 1));
|
||||
vscode.postMessage({
|
||||
t: 'changePage',
|
||||
pageNumber: actualPageNumber,
|
||||
selectedTable,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const prevPage = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
vscode.postMessage({
|
||||
t: 'changePage',
|
||||
pageNumber: Math.max(resultSets.pageNumber - 1, 0),
|
||||
selectedTable,
|
||||
});
|
||||
};
|
||||
const nextPage = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
vscode.postMessage({
|
||||
t: 'changePage',
|
||||
pageNumber: Math.min(resultSets.pageNumber + 1, resultSets.numPages - 1),
|
||||
selectedTable,
|
||||
});
|
||||
};
|
||||
return <span>
|
||||
<button onClick={prevPage} ><</button>
|
||||
<input value={this.state.selectedPage} onChange={onChange}
|
||||
onBlur={e => choosePage(e.target.value)}
|
||||
onKeyDown={e => { if (e.keyCode === 13) choosePage((e.target as HTMLInputElement).value); }}
|
||||
/>
|
||||
<button value=">" onClick={nextPage} >></button>
|
||||
</span>;
|
||||
}
|
||||
|
||||
renderButtons(): JSX.Element {
|
||||
if (this.props.parsedResultSets.t === 'ExtensionParsed' && this.paginationEnabled())
|
||||
return this.renderPageButtons(this.props.parsedResultSets);
|
||||
else
|
||||
return <span />;
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
const { selectedTable } = this.state;
|
||||
const resultSets = this.getResultSets();
|
||||
const resultSetNames = this.getResultSetNames(resultSets);
|
||||
|
||||
const resultSet = resultSets.find(resultSet => resultSet.schema.name == selectedTable);
|
||||
const nonemptyRawResults = resultSets.some(resultSet => resultSet.t == 'RawResultSet' && resultSet.rows.length > 0);
|
||||
const numberOfResults = resultSet && renderResultCountString(resultSet);
|
||||
|
||||
const resultSetOptions =
|
||||
resultSetNames.map(name => <option key={name} value={name}>{name}</option>);
|
||||
|
||||
return <div>
|
||||
{this.renderButtons()}
|
||||
<div className={tableSelectionHeaderClassName}>
|
||||
<select value={selectedTable} onChange={this.onTableSelectionChange}>
|
||||
{
|
||||
resultSets.map(resultSet =>
|
||||
<option key={resultSet.schema.name} value={resultSet.schema.name}>
|
||||
{resultSet.schema.name}
|
||||
</option>
|
||||
)
|
||||
}
|
||||
{resultSetOptions}
|
||||
</select>
|
||||
{numberOfResults}
|
||||
{selectedTable === ALERTS_TABLE_NAME ? this.alertTableExtras() : undefined}
|
||||
@@ -152,7 +254,8 @@ export class ResultTables
|
||||
resultsPath={this.props.resultsPath}
|
||||
sortState={this.props.sortStates.get(resultSet.schema.name)}
|
||||
nonemptyRawResults={nonemptyRawResults}
|
||||
showRawResults={() => { this.setState({ selectedTable: SELECT_TABLE_NAME }); }} />
|
||||
showRawResults={() => { this.setState({ selectedTable: SELECT_TABLE_NAME }); }}
|
||||
offset={this.getOffset()} />
|
||||
}
|
||||
</div>;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import * as React from 'react';
|
||||
import * as Rdom from 'react-dom';
|
||||
import * as bqrs from 'semmle-bqrs';
|
||||
import { ElementBase, PrimitiveColumnValue, PrimitiveTypeKind, ResultSetSchema, tryGetResolvableLocation } from 'semmle-bqrs';
|
||||
import { ElementBase, PrimitiveColumnValue, PrimitiveTypeKind, tryGetResolvableLocation } from 'semmle-bqrs';
|
||||
import { assertNever } from '../helpers-pure';
|
||||
import { DatabaseInfo, FromResultsViewMsg, Interpretation, IntoResultsViewMsg, SortedResultSetInfo, RawResultsSortState, NavigatePathMsg, QueryMetadata, ResultsPaths } from '../interface-types';
|
||||
import { EventHandlers as EventHandlerList } from './event-handler-list';
|
||||
import { ResultTables } from './result-tables';
|
||||
import { RawResultSet, ResultValue, ResultRow } from '../adapt';
|
||||
import { ResultValue, ResultRow, ParsedResultSets } from '../adapt';
|
||||
import { ResultSet } from '../interface-utils';
|
||||
|
||||
/**
|
||||
* results.tsx
|
||||
@@ -24,13 +25,6 @@ interface VsCodeApi {
|
||||
declare const acquireVsCodeApi: () => VsCodeApi;
|
||||
export const vscode = acquireVsCodeApi();
|
||||
|
||||
export type RawTableResultSet = { t: 'RawResultSet' } & RawResultSet;
|
||||
export type PathTableResultSet = { t: 'SarifResultSet'; readonly schema: ResultSetSchema; name: string } & Interpretation;
|
||||
|
||||
export type ResultSet =
|
||||
| RawTableResultSet
|
||||
| PathTableResultSet;
|
||||
|
||||
async function* getChunkIterator(response: Response): AsyncIterableIterator<Uint8Array> {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load results: (${response.status}) ${response.statusText}`);
|
||||
@@ -107,8 +101,8 @@ async function parseResultSets(response: Response): Promise<readonly ResultSet[]
|
||||
}
|
||||
|
||||
interface ResultsInfo {
|
||||
parsedResultSets: ParsedResultSets;
|
||||
resultsPath: string;
|
||||
resultSets: ResultSet[] | undefined;
|
||||
origResultsPaths: ResultsPaths;
|
||||
database: DatabaseInfo;
|
||||
interpretation: Interpretation | undefined;
|
||||
@@ -169,7 +163,7 @@ class App extends React.Component<{}, ResultsViewState> {
|
||||
case 'setState':
|
||||
this.updateStateWithNewResultsInfo({
|
||||
resultsPath: msg.resultsPath,
|
||||
resultSets: msg.resultSets?.map(x => ({ t: 'RawResultSet', ...x })),
|
||||
parsedResultSets: msg.parsedResultSets,
|
||||
origResultsPaths: msg.origResultsPaths,
|
||||
sortedResultsMap: new Map(Object.entries(msg.sortedResultsMap)),
|
||||
database: msg.database,
|
||||
@@ -221,6 +215,16 @@ class App extends React.Component<{}, ResultsViewState> {
|
||||
});
|
||||
}
|
||||
|
||||
private async getResultSets(resultsInfo: ResultsInfo): Promise<readonly ResultSet[]> {
|
||||
const parsedResultSets = resultsInfo.parsedResultSets;
|
||||
switch (parsedResultSets.t) {
|
||||
case 'WebviewParsed': return await this.fetchResultSets(resultsInfo);
|
||||
case 'ExtensionParsed': {
|
||||
return [{ t: 'RawResultSet', ...parsedResultSets.resultSet }];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async loadResults(): Promise<void> {
|
||||
const resultsInfo = this.state.nextResultsInfo;
|
||||
if (resultsInfo === null) {
|
||||
@@ -230,7 +234,7 @@ class App extends React.Component<{}, ResultsViewState> {
|
||||
let results: Results | null = null;
|
||||
let statusText = '';
|
||||
try {
|
||||
const resultSets = resultsInfo.resultSets || await this.getResultSets(resultsInfo);
|
||||
const resultSets = await this.getResultSets(resultsInfo);
|
||||
results = {
|
||||
resultSets,
|
||||
database: resultsInfo.database,
|
||||
@@ -265,7 +269,11 @@ class App extends React.Component<{}, ResultsViewState> {
|
||||
});
|
||||
}
|
||||
|
||||
private async getResultSets(resultsInfo: ResultsInfo): Promise<readonly ResultSet[]> {
|
||||
/**
|
||||
* This is deprecated, because it calls `fetch`. We are moving
|
||||
* towards doing all bqrs parsing in the extension.
|
||||
*/
|
||||
private async fetchResultSets(resultsInfo: ResultsInfo): Promise<readonly ResultSet[]> {
|
||||
const unsortedResponse = await fetch(resultsInfo.resultsPath);
|
||||
const unsortedResultSets = await parseResultSets(unsortedResponse);
|
||||
return Promise.all(unsortedResultSets.map(async unsortedResultSet => {
|
||||
@@ -291,7 +299,10 @@ class App extends React.Component<{}, ResultsViewState> {
|
||||
render(): JSX.Element {
|
||||
const displayedResults = this.state.displayedResults;
|
||||
if (displayedResults.results !== null && displayedResults.resultsInfo !== null) {
|
||||
return <ResultTables rawResultSets={displayedResults.results.resultSets}
|
||||
const parsedResultSets = displayedResults.resultsInfo.parsedResultSets;
|
||||
return <ResultTables
|
||||
parsedResultSets={parsedResultSets}
|
||||
rawResultSets={displayedResults.results.resultSets}
|
||||
interpretation={displayedResults.resultsInfo ? displayedResults.resultsInfo.interpretation : undefined}
|
||||
database={displayedResults.results.database}
|
||||
origResultsPaths={displayedResults.resultsInfo.origResultsPaths}
|
||||
|
||||
@@ -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);
|
||||
@@ -17,46 +17,58 @@ const expect = chai.expect;
|
||||
describe("Releases API consumer", () => {
|
||||
const owner = "someowner";
|
||||
const repo = "somerepo";
|
||||
const sampleReleaseResponse: GithubRelease[] = [
|
||||
{
|
||||
"assets": [],
|
||||
"created_at": "2019-09-01T00:00:00Z",
|
||||
"id": 1,
|
||||
"name": "",
|
||||
"prerelease": false,
|
||||
"tag_name": "v2.1.0"
|
||||
},
|
||||
{
|
||||
"assets": [],
|
||||
"created_at": "2019-08-10T00:00:00Z",
|
||||
"id": 2,
|
||||
"name": "",
|
||||
"prerelease": false,
|
||||
"tag_name": "v3.1.1"
|
||||
},
|
||||
{
|
||||
"assets": [],
|
||||
"created_at": "2019-09-05T00:00:00Z",
|
||||
"id": 3,
|
||||
"name": "",
|
||||
"prerelease": false,
|
||||
"tag_name": "v2.0.0"
|
||||
},
|
||||
{
|
||||
"assets": [],
|
||||
"created_at": "2019-08-11T00:00:00Z",
|
||||
"id": 4,
|
||||
"name": "",
|
||||
"prerelease": true,
|
||||
"tag_name": "v3.1.2-pre"
|
||||
},
|
||||
];
|
||||
const unconstrainedVersionConstraint = {
|
||||
description: "*",
|
||||
isVersionCompatible: () => true
|
||||
};
|
||||
const unconstrainedVersionRange = new semver.Range("*");
|
||||
|
||||
describe("picking the latest release", () => {
|
||||
const sampleReleaseResponse: GithubRelease[] = [
|
||||
{
|
||||
"assets": [],
|
||||
"created_at": "2019-09-01T00:00:00Z",
|
||||
"id": 1,
|
||||
"name": "",
|
||||
"prerelease": false,
|
||||
"tag_name": "v2.1.0"
|
||||
},
|
||||
{
|
||||
"assets": [],
|
||||
"created_at": "2019-08-10T00:00:00Z",
|
||||
"id": 2,
|
||||
"name": "",
|
||||
"prerelease": false,
|
||||
"tag_name": "v3.1.1"
|
||||
},
|
||||
{
|
||||
"assets": [{
|
||||
id: 1,
|
||||
name: "exampleAsset.txt",
|
||||
size: 1
|
||||
}],
|
||||
"created_at": "2019-09-05T00:00:00Z",
|
||||
"id": 3,
|
||||
"name": "",
|
||||
"prerelease": false,
|
||||
"tag_name": "v2.0.0"
|
||||
},
|
||||
{
|
||||
"assets": [],
|
||||
"created_at": "2019-08-11T00:00:00Z",
|
||||
"id": 4,
|
||||
"name": "",
|
||||
"prerelease": true,
|
||||
"tag_name": "v3.1.2-pre-1.1"
|
||||
},
|
||||
// Release ID 5 is older than release ID 4 but its version has a higher precedence, so release
|
||||
// ID 5 should be picked over release ID 4.
|
||||
{
|
||||
"assets": [],
|
||||
"created_at": "2019-08-09T00:00:00Z",
|
||||
"id": 5,
|
||||
"name": "",
|
||||
"prerelease": true,
|
||||
"tag_name": "v3.1.2-pre-2.0"
|
||||
},
|
||||
];
|
||||
|
||||
it("picking latest release: is based on version", async () => {
|
||||
class MockReleasesApiConsumer extends ReleasesApiConsumer {
|
||||
protected async makeApiCall(apiPath: string): Promise<fetch.Response> {
|
||||
if (apiPath === `/repos/${owner}/${repo}/releases`) {
|
||||
@@ -66,45 +78,55 @@ describe("Releases API consumer", () => {
|
||||
}
|
||||
}
|
||||
|
||||
const consumer = new MockReleasesApiConsumer(owner, repo);
|
||||
it("picked release has version with the highest precedence", async () => {
|
||||
const consumer = new MockReleasesApiConsumer(owner, repo);
|
||||
|
||||
const latestRelease = await consumer.getLatestRelease(unconstrainedVersionConstraint);
|
||||
expect(latestRelease.id).to.equal(2);
|
||||
});
|
||||
|
||||
it("picking latest release: obeys version constraints", async () => {
|
||||
class MockReleasesApiConsumer extends ReleasesApiConsumer {
|
||||
protected async makeApiCall(apiPath: string): Promise<fetch.Response> {
|
||||
if (apiPath === `/repos/${owner}/${repo}/releases`) {
|
||||
return Promise.resolve(new fetch.Response(JSON.stringify(sampleReleaseResponse)));
|
||||
}
|
||||
return Promise.reject(new Error(`Unknown API path: ${apiPath}`));
|
||||
}
|
||||
}
|
||||
|
||||
const consumer = new MockReleasesApiConsumer(owner, repo);
|
||||
|
||||
const latestRelease = await consumer.getLatestRelease({
|
||||
description: "2.*.*",
|
||||
isVersionCompatible: version => version.majorVersion === 2
|
||||
const latestRelease = await consumer.getLatestRelease(unconstrainedVersionRange);
|
||||
expect(latestRelease.id).to.equal(2);
|
||||
});
|
||||
expect(latestRelease.id).to.equal(1);
|
||||
});
|
||||
|
||||
it("picking latest release: includes prereleases when option set", async () => {
|
||||
class MockReleasesApiConsumer extends ReleasesApiConsumer {
|
||||
protected async makeApiCall(apiPath: string): Promise<fetch.Response> {
|
||||
if (apiPath === `/repos/${owner}/${repo}/releases`) {
|
||||
return Promise.resolve(new fetch.Response(JSON.stringify(sampleReleaseResponse)));
|
||||
}
|
||||
return Promise.reject(new Error(`Unknown API path: ${apiPath}`));
|
||||
}
|
||||
}
|
||||
it("version of picked release is within the version range", async () => {
|
||||
const consumer = new MockReleasesApiConsumer(owner, repo);
|
||||
|
||||
const consumer = new MockReleasesApiConsumer(owner, repo);
|
||||
const latestRelease = await consumer.getLatestRelease(new semver.Range("2.*.*"));
|
||||
expect(latestRelease.id).to.equal(1);
|
||||
});
|
||||
|
||||
const latestRelease = await consumer.getLatestRelease(unconstrainedVersionConstraint, true);
|
||||
expect(latestRelease.id).to.equal(4);
|
||||
it("fails if none of the releases are within the version range", async () => {
|
||||
const consumer = new MockReleasesApiConsumer(owner, repo);
|
||||
|
||||
await chai.expect(
|
||||
consumer.getLatestRelease(new semver.Range("5.*.*"))
|
||||
).to.be.rejectedWith(Error);
|
||||
});
|
||||
|
||||
it("picked release passes additional compatibility test if an additional compatibility test is specified", async () => {
|
||||
const consumer = new MockReleasesApiConsumer(owner, repo);
|
||||
|
||||
const latestRelease = await consumer.getLatestRelease(
|
||||
new semver.Range("2.*.*"),
|
||||
true,
|
||||
release => release.assets.some(asset => asset.name === "exampleAsset.txt")
|
||||
);
|
||||
expect(latestRelease.id).to.equal(3);
|
||||
});
|
||||
|
||||
it("fails if none of the releases pass the additional compatibility test", async () => {
|
||||
const consumer = new MockReleasesApiConsumer(owner, repo);
|
||||
|
||||
await chai.expect(consumer.getLatestRelease(
|
||||
new semver.Range("2.*.*"),
|
||||
true,
|
||||
release => release.assets.some(asset => asset.name === "otherExampleAsset.txt")
|
||||
)).to.be.rejectedWith(Error);
|
||||
});
|
||||
|
||||
it("picked release is the most recent prerelease when includePrereleases is set", async () => {
|
||||
const consumer = new MockReleasesApiConsumer(owner, repo);
|
||||
|
||||
const latestRelease = await consumer.getLatestRelease(unconstrainedVersionRange, true);
|
||||
expect(latestRelease.id).to.equal(5);
|
||||
});
|
||||
});
|
||||
|
||||
it("gets correct assets for a release", async () => {
|
||||
@@ -141,7 +163,7 @@ describe("Releases API consumer", () => {
|
||||
|
||||
const consumer = new MockReleasesApiConsumer(owner, repo);
|
||||
|
||||
const assets = (await consumer.getLatestRelease(unconstrainedVersionConstraint)).assets;
|
||||
const assets = (await consumer.getLatestRelease(unconstrainedVersionRange)).assets;
|
||||
|
||||
expect(assets.length).to.equal(expectedAssets.length);
|
||||
expectedAssets.map((expectedAsset, index) => {
|
||||
@@ -152,41 +174,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`;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { runTests } from 'vscode-test';
|
||||
|
||||
// A subset of the fields in TestOptions from vscode-test, which we
|
||||
@@ -26,6 +27,11 @@ async function runTestsWithRetryOnSegfault(suite: Suite, tries: number): Promise
|
||||
if (t < tries - 1)
|
||||
console.error('Retrying...');
|
||||
}
|
||||
else if (os.platform() === 'win32') {
|
||||
console.error(`Test runner caught exception (${err})`);
|
||||
if (t < tries - 1)
|
||||
console.error('Retrying...');
|
||||
}
|
||||
else {
|
||||
throw err;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user