Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6928d3159 | ||
|
|
fd26e02ed3 | ||
|
|
de381804f6 | ||
|
|
2f92477bd9 | ||
|
|
926ab92dfe | ||
|
|
36484fcea6 | ||
|
|
89e7b03d4a | ||
|
|
c3e3390647 | ||
|
|
010ae64da3 | ||
|
|
bd3702121f | ||
|
|
043d17d454 | ||
|
|
1c7cad0151 | ||
|
|
e0383b3f9a | ||
|
|
0d972d7916 | ||
|
|
ab020f24ae | ||
|
|
81cbf26910 | ||
|
|
2e2f101131 | ||
|
|
610d40c99c | ||
|
|
adf6f66517 | ||
|
|
8f84989d98 | ||
|
|
22c9386123 | ||
|
|
53e1794b50 | ||
|
|
307d6d7c7f | ||
|
|
a0e60fb154 | ||
|
|
8b5bdbb6ef | ||
|
|
0ad9cdd5ac | ||
|
|
c3b2e9d478 | ||
|
|
c20bbd9606 | ||
|
|
6080a0d585 | ||
|
|
9fda320589 | ||
|
|
143b51ef82 | ||
|
|
51d4c87af4 | ||
|
|
be5efc01ee | ||
|
|
08a30c454a | ||
|
|
1377969213 | ||
|
|
41f1aae71d | ||
|
|
62cae6ead1 | ||
|
|
39e3627e06 | ||
|
|
43586c91d9 | ||
|
|
31414b7506 |
3
.vscode/launch.json
vendored
3
.vscode/launch.json
vendored
@@ -8,8 +8,7 @@
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "${execPath}",
|
||||
"args": [
|
||||
"--extensionDevelopmentPath=${workspaceRoot}/dist/vscode-codeql",
|
||||
"${workspaceRoot}/../vscode-codeql-starter/vscode-codeql-starter.code-workspace"
|
||||
"--extensionDevelopmentPath=${workspaceRoot}/dist/vscode-codeql"
|
||||
],
|
||||
"stopOnEntry": false,
|
||||
"sourceMaps": true,
|
||||
|
||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -32,5 +32,7 @@
|
||||
"eslint.options": {
|
||||
// This is necessary so that eslint can properly resolve its plugins
|
||||
"resolvePluginsRelativeTo": "./extensions/ql-vscode"
|
||||
}
|
||||
},
|
||||
// Force this to false since this will cause too many changes on each commit
|
||||
"editor.formatOnSave": false
|
||||
}
|
||||
|
||||
578
common/config/rush/pnpm-lock.yaml
generated
578
common/config/rush/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,18 @@
|
||||
# CodeQL for Visual Studio Code: Changelog
|
||||
|
||||
## 1.1.2
|
||||
## 1.1.4 - 13 May 2020
|
||||
|
||||
- Add the ability to download and install databases archives from the internet.
|
||||
|
||||
## 1.1.3 - 8 May 2020
|
||||
|
||||
- Add a suggestion in alerts view to view raw results, when there are
|
||||
raw results but no alerts.
|
||||
- Add the ability to rename databases in the database view.
|
||||
- Add the ability to open the directory in the filesystem
|
||||
of a database.
|
||||
|
||||
## 1.1.2 - 28 April 2020
|
||||
|
||||
- Implement syntax highlighting for the new `unique` aggregate.
|
||||
- Implement XML syntax highlighting for `.qhelp` files.
|
||||
@@ -49,7 +61,7 @@
|
||||
## 1.0.3 - 13 January 2020
|
||||
|
||||
- Reduce the frequency of CodeQL CLI update checks to help avoid hitting GitHub API limits of 60 requests per
|
||||
hour for unauthenticated IPs.
|
||||
hour for unauthenticated IPs.
|
||||
- Fix sorting of result sets with names containing special characters.
|
||||
|
||||
## 1.0.2 - 13 December 2019
|
||||
@@ -58,8 +70,7 @@ hour for unauthenticated IPs.
|
||||
- Allow customization of query history labels from settings and from
|
||||
query history view context menu.
|
||||
- Show number of results in results view.
|
||||
- Add commands `CodeQL: Show Next Step on Path` and `CodeQL: Show
|
||||
Previous Step on Path` for navigating the steps on the currently
|
||||
- Add commands `CodeQL: Show Next Step on Path` and `CodeQL: Show Previous Step on Path` for navigating the steps on the currently
|
||||
shown path result.
|
||||
|
||||
## 1.0.1 - 21 November 2019
|
||||
|
||||
@@ -10,7 +10,7 @@ export const config: webpack.Configuration = {
|
||||
path: path.resolve(__dirname, '..', 'out'),
|
||||
filename: "[name].js"
|
||||
},
|
||||
devtool: 'source-map',
|
||||
devtool: "inline-source-map",
|
||||
resolve: {
|
||||
extensions: ['.js', '.ts', '.tsx', '.json']
|
||||
},
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"description": "CodeQL for Visual Studio Code",
|
||||
"author": "GitHub",
|
||||
"private": true,
|
||||
"version": "1.1.2",
|
||||
"version": "1.1.4",
|
||||
"publisher": "GitHub",
|
||||
"license": "MIT",
|
||||
"icon": "media/VS-marketplace-CodeQL-icon.png",
|
||||
@@ -29,6 +29,7 @@
|
||||
"onCommand:codeQL.checkForUpdatesToCLI",
|
||||
"onCommand:codeQL.chooseDatabase",
|
||||
"onCommand:codeQL.setCurrentDatabase",
|
||||
"onCommand:codeQL.downloadDatabase",
|
||||
"onCommand:codeQLDatabases.chooseDatabase",
|
||||
"onCommand:codeQLDatabases.setCurrentDatabase",
|
||||
"onCommand:codeQL.quickQuery",
|
||||
@@ -80,9 +81,6 @@
|
||||
},
|
||||
{
|
||||
"id": "xml",
|
||||
"aliases": [
|
||||
"qhelp"
|
||||
],
|
||||
"extensions": [
|
||||
".qhelp"
|
||||
]
|
||||
@@ -206,6 +204,18 @@
|
||||
"command": "codeQLDatabases.upgradeDatabase",
|
||||
"title": "Upgrade Database"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.renameDatabase",
|
||||
"title": "Rename Database"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.openDatabaseFolder",
|
||||
"title": "Show Database Directory"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.downloadDatabase",
|
||||
"title": "CodeQL: Download database"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.sortByName",
|
||||
"title": "Sort by Name",
|
||||
@@ -305,6 +315,16 @@
|
||||
"group": "9_qlCommands",
|
||||
"when": "view == codeQLDatabases"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.renameDatabase",
|
||||
"group": "9_qlCommands",
|
||||
"when": "view == codeQLDatabases"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.openDatabaseFolder",
|
||||
"group": "9_qlCommands",
|
||||
"when": "view == codeQLDatabases"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.openQuery",
|
||||
"group": "9_qlCommands",
|
||||
@@ -358,6 +378,10 @@
|
||||
"command": "codeQL.runQuery",
|
||||
"when": "resourceLangId == ql && resourceExtname == .ql"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.downloadDatabase",
|
||||
"when": "true"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.quickEval",
|
||||
"when": "editorLangId == ql"
|
||||
@@ -370,6 +394,14 @@
|
||||
"command": "codeQLDatabases.setCurrentDatabase",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.renameDatabase",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.openDatabaseFolder",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.sortByName",
|
||||
"when": "false"
|
||||
@@ -450,7 +482,8 @@
|
||||
"update-vscode": "node ./node_modules/vscode/bin/install",
|
||||
"postinstall": "node ./node_modules/vscode/bin/install",
|
||||
"format": "tsfmt -r",
|
||||
"lint": "eslint src test --ext .ts,.tsx"
|
||||
"lint": "eslint src test --ext .ts,.tsx",
|
||||
"lint-staged": "lint-staged"
|
||||
},
|
||||
"dependencies": {
|
||||
"child-process-promise": "^2.2.1",
|
||||
@@ -467,8 +500,8 @@
|
||||
"tmp": "^0.1.0",
|
||||
"tree-kill": "~1.2.2",
|
||||
"unzipper": "~0.10.5",
|
||||
"vscode-jsonrpc": "^4.0.0",
|
||||
"vscode-languageclient": "^5.2.1",
|
||||
"vscode-jsonrpc": "^5.0.1",
|
||||
"vscode-languageclient": "^6.1.3",
|
||||
"vscode-test-adapter-api": "~1.7.0",
|
||||
"vscode-test-adapter-util": "~0.7.0",
|
||||
"minimist": "~1.2.5"
|
||||
@@ -527,6 +560,23 @@
|
||||
"@types/sinon-chai": "~3.2.3",
|
||||
"proxyquire": "~2.1.3",
|
||||
"@types/proxyquire": "~1.3.28",
|
||||
"eslint-plugin-react": "~7.19.0"
|
||||
"eslint-plugin-react": "~7.19.0",
|
||||
"husky": "~4.2.5",
|
||||
"lint-staged": "~10.2.2",
|
||||
"prettier": "~2.0.5"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "npm run lint-staged"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"./**/*.{json,css,scss,md}": [
|
||||
"prettier --write"
|
||||
],
|
||||
"./**/*.{ts,tsx}": [
|
||||
"eslint --fix --debug",
|
||||
"tsfmt -r"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
103
extensions/ql-vscode/src/adapt.ts
Normal file
103
extensions/ql-vscode/src/adapt.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { DecodedBqrsChunk, ResultSetSchema, ColumnKind, Column, ColumnValue } from "./bqrs-cli-types";
|
||||
import { LocationValue, ResultSetSchema as AdaptedSchema, ColumnSchema, ColumnType, LocationStyle } from 'semmle-bqrs';
|
||||
|
||||
// FIXME: This is a temporary bit of impedance matching to convert
|
||||
// from the types provided by ./bqrs-cli-types, to the types used by
|
||||
// the view layer.
|
||||
//
|
||||
// The reason that it is benign for now is that it is only used by
|
||||
// feature-flag-guarded codepaths that won't be encountered by normal
|
||||
// users. It is not yet guaranteed to produce correct output for raw
|
||||
// results.
|
||||
//
|
||||
// Eventually, the view layer should be refactored to directly accept data
|
||||
// of types coming from bqrs-cli-types, and this file can be deleted.
|
||||
|
||||
export type ResultRow = ResultValue[];
|
||||
|
||||
export interface ResultElement {
|
||||
label: string;
|
||||
location?: LocationValue;
|
||||
}
|
||||
|
||||
export interface ResultUri {
|
||||
uri: string;
|
||||
}
|
||||
|
||||
export type ResultValue = ResultElement | ResultUri | string;
|
||||
|
||||
export interface RawResultSet {
|
||||
readonly schema: AdaptedSchema;
|
||||
readonly rows: readonly ResultRow[];
|
||||
}
|
||||
|
||||
function adaptKind(kind: ColumnKind): ColumnType {
|
||||
// XXX what about 'u'?
|
||||
if (kind === 'e') {
|
||||
return { type: 'e', primitiveType: 's', locationStyle: LocationStyle.FivePart, hasLabel: true }
|
||||
}
|
||||
else {
|
||||
return { type: kind };
|
||||
}
|
||||
}
|
||||
|
||||
function adaptColumn(col: Column): ColumnSchema {
|
||||
return { name: col.name!, type: adaptKind(col.kind) };
|
||||
}
|
||||
|
||||
export function adaptSchema(schema: ResultSetSchema): AdaptedSchema {
|
||||
return {
|
||||
columns: schema.columns.map(adaptColumn),
|
||||
name: schema.name,
|
||||
tupleCount: schema.rows,
|
||||
version: 0,
|
||||
}
|
||||
}
|
||||
|
||||
export function adaptValue(val: ColumnValue): ResultValue {
|
||||
// XXX taking a lot of incorrect shortcuts here
|
||||
|
||||
if (typeof val === 'string') {
|
||||
return val;
|
||||
}
|
||||
|
||||
if (typeof val === 'number' || typeof val === 'boolean') {
|
||||
return val + '';
|
||||
}
|
||||
|
||||
const url = val.url;
|
||||
|
||||
if (typeof url === 'string') {
|
||||
return url;
|
||||
}
|
||||
|
||||
if (url === undefined) {
|
||||
return 'none';
|
||||
}
|
||||
|
||||
return {
|
||||
label: val.label || '',
|
||||
location: {
|
||||
t: LocationStyle.FivePart,
|
||||
lineStart: url.startLine,
|
||||
lineEnd: url.endLine,
|
||||
colStart: url.startColumn,
|
||||
colEnd: url.endColumn,
|
||||
// FIXME: This seems definitely wrong. Should we be using
|
||||
// something like the code in sarif-utils.ts?
|
||||
file: url.uri.replace(/file:/, ''),
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export function adaptRow(row: ColumnValue[]): ResultRow {
|
||||
return row.map(adaptValue);
|
||||
}
|
||||
|
||||
export function adaptBqrs(schema: AdaptedSchema, page: DecodedBqrsChunk): RawResultSet {
|
||||
return {
|
||||
schema,
|
||||
rows: page.tuples.map(adaptRow),
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,9 @@ const ROOT_SETTING = new Setting('codeQL');
|
||||
*/
|
||||
export const EXPERIMENTAL_FEATURES_SETTING = new Setting('experimentalFeatures', ROOT_SETTING);
|
||||
|
||||
/* Advanced setting: used to enable bqrs parsing in the cli instead of in the webview. */
|
||||
export const EXPERIMENTAL_BQRS_SETTING = new Setting('experimentalBqrsParsing', ROOT_SETTING);
|
||||
|
||||
// Distribution configuration
|
||||
|
||||
const DISTRIBUTION_SETTING = new Setting('cli', ROOT_SETTING);
|
||||
|
||||
111
extensions/ql-vscode/src/databaseFetcher.ts
Normal file
111
extensions/ql-vscode/src/databaseFetcher.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import * as fetch from "node-fetch";
|
||||
import * as unzipper from "unzipper";
|
||||
import { Uri, ProgressOptions, ProgressLocation, commands, window } from "vscode";
|
||||
import * as fs from "fs-extra";
|
||||
import * as path from "path";
|
||||
import { DatabaseManager } from "./databases";
|
||||
import { ProgressCallback, showAndLogErrorMessage, withProgress } from "./helpers";
|
||||
|
||||
export default async function promptFetchDatabase(dbm: DatabaseManager, storagePath: string) {
|
||||
try {
|
||||
const databaseUrl = await window.showInputBox({
|
||||
prompt: 'Enter URL of zipfile of database to download'
|
||||
});
|
||||
|
||||
if (databaseUrl) {
|
||||
validateUrl(databaseUrl);
|
||||
|
||||
const progressOptions: ProgressOptions = {
|
||||
location: ProgressLocation.Notification,
|
||||
title: 'Adding database from URL',
|
||||
cancellable: false,
|
||||
};
|
||||
await withProgress(progressOptions, async progress => await databaseFetcher(databaseUrl, dbm, storagePath, progress));
|
||||
commands.executeCommand('codeQLDatabases.focus');
|
||||
}
|
||||
} catch (e) {
|
||||
showAndLogErrorMessage(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function databaseFetcher(
|
||||
databaseUrl: string,
|
||||
databasesManager: DatabaseManager,
|
||||
storagePath: string,
|
||||
progressCallback: ProgressCallback
|
||||
): Promise<void> {
|
||||
progressCallback({
|
||||
maxStep: 3,
|
||||
message: 'Downloading database',
|
||||
step: 1
|
||||
});
|
||||
if (!storagePath) {
|
||||
throw new Error("No storage path specified.");
|
||||
}
|
||||
const unzipPath = await getStorageFolder(storagePath, databaseUrl);
|
||||
|
||||
const response = await fetch.default(databaseUrl);
|
||||
const unzipStream = unzipper.Extract({
|
||||
path: unzipPath
|
||||
});
|
||||
progressCallback({
|
||||
maxStep: 3,
|
||||
message: 'Unzipping database',
|
||||
step: 2
|
||||
});
|
||||
await new Promise((resolve, reject) => {
|
||||
response.body.on('error', reject);
|
||||
unzipStream.on('error', reject);
|
||||
unzipStream.on('close', resolve);
|
||||
response.body.pipe(unzipStream);
|
||||
});
|
||||
progressCallback({
|
||||
maxStep: 3,
|
||||
message: 'Opening database',
|
||||
step: 3
|
||||
});
|
||||
|
||||
// if there is a single directory inside, then assume that's what we want to import
|
||||
const dirs = await fs.readdir(unzipPath);
|
||||
const dbPath = dirs?.length === 1 && (await fs.stat(path.join(unzipPath, dirs[0]))).isDirectory
|
||||
? path.join(unzipPath, dirs[0])
|
||||
: unzipPath;
|
||||
|
||||
// might need to upgrade before importing...
|
||||
const item = await databasesManager.openDatabase(Uri.parse(dbPath));
|
||||
databasesManager.setCurrentDatabaseItem(item);
|
||||
}
|
||||
|
||||
async function getStorageFolder(storagePath: string, urlStr: string) {
|
||||
const url = Uri.parse(urlStr);
|
||||
let lastName = path.basename(url.path).substring(0, 255);
|
||||
if (lastName.endsWith(".zip")) {
|
||||
lastName = lastName.substring(0, lastName.length - 4);
|
||||
}
|
||||
|
||||
const realpath = await fs.realpath(storagePath);
|
||||
let folderName = path.join(realpath, lastName);
|
||||
let counter = 0;
|
||||
while (await fs.pathExists(folderName)) {
|
||||
counter++;
|
||||
folderName = path.join(realpath, `${lastName}-${counter}`);
|
||||
if (counter > 100) {
|
||||
throw new Error("Could not find a unique name for downloaded database.");
|
||||
}
|
||||
}
|
||||
return folderName;
|
||||
}
|
||||
|
||||
|
||||
function validateUrl(databaseUrl: string) {
|
||||
let uri;
|
||||
try {
|
||||
uri = Uri.parse(databaseUrl, true);
|
||||
} catch (e) {
|
||||
throw new Error(`Invalid url: ${databaseUrl}`);
|
||||
}
|
||||
|
||||
if (uri.scheme !== 'https') {
|
||||
throw new Error('Must use https for downloading a database.');
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import * as path from 'path';
|
||||
import { DisposableObject } from 'semmle-vscode-utils';
|
||||
import { commands, Event, EventEmitter, ExtensionContext, ProviderResult, TreeDataProvider, TreeItem, Uri, window } from 'vscode';
|
||||
import { commands, Event, EventEmitter, ExtensionContext, ProviderResult, TreeDataProvider, TreeItem, Uri, window, env } from 'vscode';
|
||||
import * as cli from './cli';
|
||||
import { DatabaseItem, DatabaseManager, getUpgradesDirectories } from './databases';
|
||||
import { getOnDiskWorkspaceFolders } from './helpers';
|
||||
import { getOnDiskWorkspaceFolders, showAndLogErrorMessage } from './helpers';
|
||||
import { logger } from './logging';
|
||||
import { clearCacheInDatabase, UserCancellationException } from './run-queries';
|
||||
import * as qsClient from './queryserver-client';
|
||||
@@ -94,7 +94,7 @@ class DatabaseTreeDataProvider extends DisposableObject
|
||||
public getChildren(element?: DatabaseItem): ProviderResult<DatabaseItem[]> {
|
||||
if (element === undefined) {
|
||||
return this.databaseManager.databaseItems.slice(0).sort((db1, db2) => {
|
||||
switch(this.sortOrder) {
|
||||
switch (this.sortOrder) {
|
||||
case SortOrder.NameAsc:
|
||||
return db1.name.localeCompare(db2.name);
|
||||
case SortOrder.NameDesc:
|
||||
@@ -180,6 +180,8 @@ export class DatabaseUI extends DisposableObject {
|
||||
ctx.subscriptions.push(commands.registerCommand('codeQLDatabases.sortByDateAdded', this.handleSortByDateAdded));
|
||||
ctx.subscriptions.push(commands.registerCommand('codeQLDatabases.removeDatabase', this.handleRemoveDatabase));
|
||||
ctx.subscriptions.push(commands.registerCommand('codeQLDatabases.upgradeDatabase', this.handleUpgradeDatabase));
|
||||
ctx.subscriptions.push(commands.registerCommand('codeQLDatabases.renameDatabase', this.handleRenameDatabase));
|
||||
ctx.subscriptions.push(commands.registerCommand('codeQLDatabases.openDatabaseFolder', this.handleOpenFolder));
|
||||
}
|
||||
|
||||
private handleMakeCurrentDatabase = async (databaseItem: DatabaseItem): Promise<void> => {
|
||||
@@ -272,6 +274,29 @@ export class DatabaseUI extends DisposableObject {
|
||||
this.databaseManager.removeDatabaseItem(databaseItem);
|
||||
}
|
||||
|
||||
private handleRenameDatabase = async (databaseItem: DatabaseItem): Promise<void> => {
|
||||
try {
|
||||
const newName = await window.showInputBox({
|
||||
prompt: 'Choose new database name',
|
||||
value: databaseItem.name
|
||||
});
|
||||
|
||||
if (newName) {
|
||||
this.databaseManager.renameDatabaseItem(databaseItem, newName);
|
||||
}
|
||||
} catch (e) {
|
||||
showAndLogErrorMessage(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
private handleOpenFolder = async (databaseItem: DatabaseItem): Promise<void> => {
|
||||
try {
|
||||
await env.openExternal(databaseItem.databaseUri);
|
||||
} catch (e) {
|
||||
showAndLogErrorMessage(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current database directory. If we don't already have a
|
||||
* current database, ask the user for one, and return that, or
|
||||
|
||||
@@ -109,8 +109,9 @@ async function findDataset(parentDirectory: string): Promise<vscode.Uri> {
|
||||
return vscode.Uri.file(dbAbsolutePath);
|
||||
}
|
||||
|
||||
async function findSourceArchive(databasePath: string, silent = false):
|
||||
Promise<vscode.Uri | undefined> {
|
||||
async function findSourceArchive(
|
||||
databasePath: string, silent = false
|
||||
): Promise<vscode.Uri | undefined> {
|
||||
|
||||
const relativePaths = ['src', 'output/src_archive']
|
||||
|
||||
@@ -203,7 +204,7 @@ export interface DatabaseItem {
|
||||
/** The URI of the database */
|
||||
readonly databaseUri: vscode.Uri;
|
||||
/** The name of the database to be displayed in the UI */
|
||||
readonly name: string;
|
||||
name: string;
|
||||
/** The URI of the database's source archive, or `undefined` if no source archive is to be used. */
|
||||
readonly sourceArchive: vscode.Uri | undefined;
|
||||
/**
|
||||
@@ -287,6 +288,10 @@ class DatabaseItemImpl implements DatabaseItem {
|
||||
}
|
||||
}
|
||||
|
||||
public set name(newName: string) {
|
||||
this.options.displayName = newName;
|
||||
}
|
||||
|
||||
public get sourceArchive(): vscode.Uri | undefined {
|
||||
if (this.options.ignoreSourceArchive || (this._contents === undefined)) {
|
||||
return undefined;
|
||||
@@ -459,12 +464,11 @@ function eventFired<T>(event: vscode.Event<T>, timeoutMs = 1000): Promise<T | un
|
||||
}
|
||||
|
||||
export class DatabaseManager extends DisposableObject {
|
||||
private readonly _onDidChangeDatabaseItem =
|
||||
this.push(new vscode.EventEmitter<DatabaseItem | undefined>());
|
||||
private readonly _onDidChangeDatabaseItem = this.push(new vscode.EventEmitter<DatabaseItem | undefined>());
|
||||
|
||||
readonly onDidChangeDatabaseItem = this._onDidChangeDatabaseItem.event;
|
||||
|
||||
private readonly _onDidChangeCurrentDatabaseItem =
|
||||
this.push(new vscode.EventEmitter<DatabaseItem | undefined>());
|
||||
private readonly _onDidChangeCurrentDatabaseItem = this.push(new vscode.EventEmitter<DatabaseItem | undefined>());
|
||||
readonly onDidChangeCurrentDatabaseItem = this._onDidChangeCurrentDatabaseItem.event;
|
||||
|
||||
private readonly _databaseItems: DatabaseItemImpl[] = [];
|
||||
@@ -642,6 +646,12 @@ export class DatabaseManager extends DisposableObject {
|
||||
this._onDidChangeDatabaseItem.fire(undefined);
|
||||
}
|
||||
|
||||
public async renameDatabaseItem(item: DatabaseItem, newName: string) {
|
||||
item.name = newName;
|
||||
this.updatePersistedDatabaseList();
|
||||
this._onDidChangeDatabaseItem.fire(item);
|
||||
}
|
||||
|
||||
public removeDatabaseItem(item: DatabaseItem) {
|
||||
if (this._currentDatabaseItem == item)
|
||||
this._currentDatabaseItem = undefined;
|
||||
@@ -658,6 +668,14 @@ export class DatabaseManager extends DisposableObject {
|
||||
vscode.workspace.updateWorkspaceFolders(folderIndex, 1);
|
||||
}
|
||||
|
||||
// Delete folder from file system only if it is controlled by the extension
|
||||
if (this.isExtensionControlledLocation(item.databaseUri)) {
|
||||
logger.log(`Deleting database from filesystem.`);
|
||||
fs.remove(item.databaseUri.path).then(
|
||||
() => logger.log(`Deleted '${item.databaseUri.path}'`),
|
||||
e => logger.log(`Failed to delete '${item.databaseUri.path}'. Reason: ${e.message}`));
|
||||
}
|
||||
|
||||
this._onDidChangeDatabaseItem.fire(undefined);
|
||||
}
|
||||
|
||||
@@ -669,6 +687,11 @@ export class DatabaseManager extends DisposableObject {
|
||||
private updatePersistedDatabaseList(): void {
|
||||
this.ctx.workspaceState.update(DB_LIST, this._databaseItems.map(item => item.getPersistedState()));
|
||||
}
|
||||
|
||||
private isExtensionControlledLocation(uri: vscode.Uri) {
|
||||
const storagePath = this.ctx.storagePath || this.ctx.globalStoragePath;
|
||||
return uri.path.startsWith(storagePath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -70,7 +70,7 @@ export class TemplateQueryDefinitionProvider implements vscode.DefinitionProvide
|
||||
}
|
||||
|
||||
async getDefinitions(uriString: string): Promise<vscode.LocationLink[]> {
|
||||
return getLinksForUriString(this.cli, this.qs, this.dbm, uriString, (src, _dest) => src === uriString);
|
||||
return getLinksForUriString(this.cli, this.qs, this.dbm, uriString, KeyType.DefinitionQuery, (src, _dest) => src === uriString);
|
||||
}
|
||||
|
||||
async provideDefinition(document: vscode.TextDocument, position: vscode.Position, _token: vscode.CancellationToken): Promise<vscode.LocationLink[]> {
|
||||
@@ -97,7 +97,7 @@ export class TemplateQueryReferenceProvider implements vscode.ReferenceProvider
|
||||
}
|
||||
|
||||
async getReferences(uriString: string): Promise<FullLocationLink[]> {
|
||||
return getLinksForUriString(this.cli, this.qs, this.dbm, uriString, (_src, dest) => dest === uriString);
|
||||
return getLinksForUriString(this.cli, this.qs, this.dbm, uriString, KeyType.ReferenceQuery, (_src, dest) => dest === uriString);
|
||||
}
|
||||
|
||||
async provideReferences(document: vscode.TextDocument, position: vscode.Position, _context: vscode.ReferenceContext, _token: vscode.CancellationToken): Promise<vscode.Location[]> {
|
||||
@@ -146,6 +146,7 @@ async function getLinksForUriString(
|
||||
qs: QueryServerClient,
|
||||
dbm: DatabaseManager,
|
||||
uriString: string,
|
||||
keyType: KeyType,
|
||||
filter: (src: string, dest: string) => boolean
|
||||
) {
|
||||
const uri = decodeSourceArchiveUri(vscode.Uri.parse(uriString));
|
||||
@@ -158,7 +159,7 @@ async function getLinksForUriString(
|
||||
throw new Error("Can't infer qlpack from database source archive");
|
||||
}
|
||||
const links: FullLocationLink[] = []
|
||||
for (const query of await resolveQueries(cli, qlpack, KeyType.ReferenceQuery)) {
|
||||
for (const query of await resolveQueries(cli, qlpack, keyType)) {
|
||||
const templates: messages.TemplateDefinitions = {
|
||||
[TEMPLATE_NAME]: {
|
||||
values: {
|
||||
|
||||
@@ -6,7 +6,7 @@ import * as unzipper from "unzipper";
|
||||
import * as url from "url";
|
||||
import { ExtensionContext, Event } from "vscode";
|
||||
import { DistributionConfig } from "./config";
|
||||
import { InvocationRateLimiter, InvocationRateLimiterResultKind, ProgressUpdate, showAndLogErrorMessage } from "./helpers";
|
||||
import { InvocationRateLimiter, InvocationRateLimiterResultKind, showAndLogErrorMessage } from "./helpers";
|
||||
import { logger } from "./logging";
|
||||
import * as helpers from "./helpers";
|
||||
import { getCodeQlCliVersion, tryParseVersionString, Version } from "./cli-version";
|
||||
@@ -171,7 +171,7 @@ export class DistributionManager implements DistributionProvider {
|
||||
* Returns a failed promise if an unexpected error occurs during installation.
|
||||
*/
|
||||
public installExtensionManagedDistributionRelease(release: Release,
|
||||
progressCallback?: (p: ProgressUpdate) => void): Promise<void> {
|
||||
progressCallback?: helpers.ProgressCallback): Promise<void> {
|
||||
return this._extensionSpecificDistributionManager.installDistributionRelease(release, progressCallback);
|
||||
}
|
||||
|
||||
@@ -253,14 +253,14 @@ class ExtensionSpecificDistributionManager {
|
||||
* Returns a failed promise if an unexpected error occurs during installation.
|
||||
*/
|
||||
public async installDistributionRelease(release: Release,
|
||||
progressCallback?: (p: ProgressUpdate) => void): Promise<void> {
|
||||
progressCallback?: helpers.ProgressCallback): Promise<void> {
|
||||
await this.downloadDistribution(release, progressCallback);
|
||||
// Store the installed release within the global extension state.
|
||||
this.storeInstalledRelease(release);
|
||||
}
|
||||
|
||||
private async downloadDistribution(release: Release,
|
||||
progressCallback?: (p: ProgressUpdate) => void): Promise<void> {
|
||||
progressCallback?: helpers.ProgressCallback): Promise<void> {
|
||||
try {
|
||||
await this.removeDistribution();
|
||||
} catch (e) {
|
||||
|
||||
@@ -20,6 +20,7 @@ import { displayQuickQuery } from './quick-query';
|
||||
import { compileAndRunQueryAgainstDatabase, tmpDirDisposal, UserCancellationException } from './run-queries';
|
||||
import { QLTestAdapterFactory } from './test-adapter';
|
||||
import { TestUIService } from './test-ui';
|
||||
import promptFetchDatabase from './databaseFetcher';
|
||||
|
||||
/**
|
||||
* extension.ts
|
||||
@@ -60,8 +61,9 @@ function registerErrorStubs(excludedCommands: string[], stubGenerator: (command:
|
||||
|
||||
const extensionId = 'GitHub.vscode-codeql'; // TODO: Is there a better way of obtaining this?
|
||||
const extension = extensions.getExtension(extensionId);
|
||||
if (extension === undefined)
|
||||
if (extension === undefined) {
|
||||
throw new Error(`Can't find extension ${extensionId}`);
|
||||
}
|
||||
|
||||
const stubbedCommands: string[]
|
||||
= extension.packageJSON.contributes.commands.map((entry: { command: string }) => entry.command);
|
||||
@@ -333,6 +335,7 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu
|
||||
await qs.restartQueryServer();
|
||||
helpers.showAndLogInformationMessage('CodeQL Query Server restarted.', { outputLogger: queryServerLogger });
|
||||
}));
|
||||
ctx.subscriptions.push(commands.registerCommand('codeQL.downloadDatabase', () => promptFetchDatabase(dbm, getContextStoragePath(ctx))));
|
||||
|
||||
ctx.subscriptions.push(client.start());
|
||||
|
||||
@@ -348,10 +351,15 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu
|
||||
}
|
||||
}
|
||||
|
||||
function getContextStoragePath(ctx: ExtensionContext) {
|
||||
return ctx.storagePath || ctx.globalStoragePath;
|
||||
}
|
||||
|
||||
function initializeLogging(ctx: ExtensionContext): void {
|
||||
logger.init(ctx);
|
||||
queryServerLogger.init(ctx);
|
||||
ideServerLogger.init(ctx);
|
||||
const storagePath = getContextStoragePath(ctx);
|
||||
logger.init(storagePath);
|
||||
queryServerLogger.init(storagePath);
|
||||
ideServerLogger.init(storagePath);
|
||||
ctx.subscriptions.push(logger);
|
||||
ctx.subscriptions.push(queryServerLogger);
|
||||
ctx.subscriptions.push(ideServerLogger);
|
||||
|
||||
@@ -22,6 +22,8 @@ export interface ProgressUpdate {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export type ProgressCallback = (p: ProgressUpdate) => void;
|
||||
|
||||
/**
|
||||
* This mediates between the kind of progress callbacks we want to
|
||||
* write (where we *set* current progress position and give
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as sarif from 'sarif';
|
||||
import { ResolvableLocationValue } from 'semmle-bqrs';
|
||||
import { RawResultSet } from './adapt';
|
||||
|
||||
/**
|
||||
* Only ever show this many results per run in interpreted results.
|
||||
@@ -77,6 +78,12 @@ export interface SetStateMsg {
|
||||
* This is useful to prevent properties like scroll state being lost when rendering the sorted results after sorting a column.
|
||||
*/
|
||||
shouldKeepOldResultsWhileRendering: boolean;
|
||||
|
||||
/**
|
||||
* An experimental way of providing results from the extension.
|
||||
* Should be undefined unless config.EXPERIMENTAL_BQRS_SETTING is set to true.
|
||||
*/
|
||||
resultSets?: RawResultSet[];
|
||||
}
|
||||
|
||||
/** Advance to the next or previous path no in the path viewer */
|
||||
|
||||
@@ -16,6 +16,8 @@ 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 { EXPERIMENTAL_BQRS_SETTING } from './config';
|
||||
|
||||
/**
|
||||
* interface.ts
|
||||
@@ -349,9 +351,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
const showButton = "View Results";
|
||||
const queryName = results.queryName;
|
||||
const resultPromise = vscode.window.showInformationMessage(
|
||||
`Finished running query ${
|
||||
queryName.length > 0 ? ` “${queryName}”` : ""
|
||||
}.`,
|
||||
`Finished running query ${queryName.length > 0 ? ` "${queryName}"` : ""}.`,
|
||||
showButton
|
||||
);
|
||||
// Address this click asynchronously so we still update the
|
||||
@@ -363,6 +363,19 @@ export class InterfaceManager extends DisposableObject {
|
||||
});
|
||||
}
|
||||
|
||||
let resultSets: RawResultSet[] | undefined;
|
||||
|
||||
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 adaptedSchema = adaptSchema(schema);
|
||||
const resultSet = adaptBqrs(adaptedSchema, chunk);
|
||||
resultSets.push(resultSet);
|
||||
}
|
||||
}
|
||||
|
||||
await this.postMessage({
|
||||
t: "setState",
|
||||
interpretation,
|
||||
@@ -370,6 +383,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
resultsPath: this.convertPathToWebviewUri(
|
||||
results.query.resultsPaths.resultsPath
|
||||
),
|
||||
resultSets,
|
||||
sortedResultsMap,
|
||||
database: results.database,
|
||||
shouldKeepOldResultsWhileRendering,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { window as Window, OutputChannel, Progress, ExtensionContext, Disposable } from 'vscode';
|
||||
import { window as Window, OutputChannel, Progress, Disposable } from 'vscode';
|
||||
import { DisposableObject } from 'semmle-vscode-utils';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
@@ -47,8 +47,8 @@ export class OutputChannelLogger extends DisposableObject implements Logger {
|
||||
this.push(this.outputChannel);
|
||||
}
|
||||
|
||||
init(ctx: ExtensionContext): void {
|
||||
this.additionalLogLocationPath = path.join(ctx.storagePath || ctx.globalStoragePath, this.title);
|
||||
init(storagePath: string): void {
|
||||
this.additionalLogLocationPath = path.join(storagePath, this.title);
|
||||
|
||||
// clear out any old state from previous runs
|
||||
fs.remove(this.additionalLogLocationPath);
|
||||
|
||||
@@ -87,8 +87,7 @@ export class QLTestAdapter extends DisposableObject implements TestAdapter {
|
||||
private readonly _tests = this.push(
|
||||
new EventEmitter<TestLoadStartedEvent | TestLoadFinishedEvent>());
|
||||
private readonly _testStates = this.push(
|
||||
new EventEmitter<TestRunStartedEvent | TestRunFinishedEvent | TestSuiteEvent |
|
||||
TestEvent>());
|
||||
new EventEmitter<TestRunStartedEvent | TestRunFinishedEvent | TestSuiteEvent | TestEvent>());
|
||||
private readonly _autorun = this.push(new EventEmitter<void>());
|
||||
private runningTask?: vscode.CancellationTokenSource = undefined;
|
||||
|
||||
@@ -108,9 +107,7 @@ export class QLTestAdapter extends DisposableObject implements TestAdapter {
|
||||
return this._tests.event;
|
||||
}
|
||||
|
||||
public get testStates(): Event<TestRunStartedEvent | TestRunFinishedEvent | TestSuiteEvent |
|
||||
TestEvent> {
|
||||
|
||||
public get testStates(): Event<TestRunStartedEvent | TestRunFinishedEvent | TestSuiteEvent | TestEvent> {
|
||||
return this._testStates.event;
|
||||
}
|
||||
|
||||
@@ -118,9 +115,7 @@ export class QLTestAdapter extends DisposableObject implements TestAdapter {
|
||||
return this._autorun.event;
|
||||
}
|
||||
|
||||
private static createTestOrSuiteInfos(testNodes: readonly QLTestNode[]):
|
||||
(TestSuiteInfo | TestInfo)[] {
|
||||
|
||||
private static createTestOrSuiteInfos(testNodes: readonly QLTestNode[]): (TestSuiteInfo | TestInfo)[] {
|
||||
return testNodes.map((childNode) => {
|
||||
return QLTestAdapter.createTestOrSuiteInfo(childNode);
|
||||
});
|
||||
@@ -129,11 +124,9 @@ export class QLTestAdapter extends DisposableObject implements TestAdapter {
|
||||
private static createTestOrSuiteInfo(testNode: QLTestNode): TestSuiteInfo | TestInfo {
|
||||
if (testNode instanceof QLTestFile) {
|
||||
return QLTestAdapter.createTestInfo(testNode);
|
||||
}
|
||||
else if (testNode instanceof QLTestDirectory) {
|
||||
} else if (testNode instanceof QLTestDirectory) {
|
||||
return QLTestAdapter.createTestSuiteInfo(testNode, testNode.name);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
throw new Error('Unexpected test type.');
|
||||
}
|
||||
}
|
||||
@@ -148,9 +141,7 @@ export class QLTestAdapter extends DisposableObject implements TestAdapter {
|
||||
};
|
||||
}
|
||||
|
||||
private static createTestSuiteInfo(testDirectory: QLTestDirectory, label: string):
|
||||
TestSuiteInfo {
|
||||
|
||||
private static createTestSuiteInfo(testDirectory: QLTestDirectory, label: string): TestSuiteInfo {
|
||||
return {
|
||||
type: 'suite',
|
||||
id: testDirectory.path,
|
||||
|
||||
@@ -69,6 +69,14 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
|
||||
});
|
||||
}
|
||||
|
||||
renderNoResults(): JSX.Element {
|
||||
if (this.props.nonemptyRawResults) {
|
||||
return <span>No Alerts. See <a href='#' onClick={this.props.showRawResults}>raw results</a>.</span>;
|
||||
} else {
|
||||
return <span>No Alerts</span>;
|
||||
}
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
const { databaseUri, resultSet } = this.props;
|
||||
|
||||
@@ -156,13 +164,14 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
|
||||
return (e) => this.toggle(e, indices);
|
||||
};
|
||||
|
||||
const noResults = <span>No Results</span>; // TODO: Maybe make this look nicer
|
||||
if (resultSet.sarif.runs.length === 0 ||
|
||||
resultSet.sarif.runs[0].results === undefined ||
|
||||
resultSet.sarif.runs[0].results.length === 0) {
|
||||
return this.renderNoResults();
|
||||
}
|
||||
|
||||
let expansionIndex = 0;
|
||||
|
||||
if (resultSet.sarif.runs.length === 0) return noResults;
|
||||
if (resultSet.sarif.runs[0].results === undefined) return noResults;
|
||||
|
||||
resultSet.sarif.runs[0].results.forEach((result, resultIndex) => {
|
||||
const text = result.message.text || '[no text]';
|
||||
const msg: JSX.Element[] =
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as React from "react";
|
||||
import { renderLocation, ResultTableProps, zebraStripe, className, nextSortDirection } from "./result-table-utils";
|
||||
import { RawTableResultSet, ResultValue, vscode } from "./results";
|
||||
import { RawTableResultSet, vscode } from "./results";
|
||||
import { ResultValue } from "../adapt";
|
||||
import { SortDirection, RAW_RESULTS_LIMIT, RawResultsSortState } from "../interface-types";
|
||||
|
||||
export type RawTableProps = ResultTableProps & {
|
||||
|
||||
@@ -10,6 +10,18 @@ export interface ResultTableProps {
|
||||
metadata?: QueryMetadata;
|
||||
resultsPath: string | undefined;
|
||||
sortState?: RawResultsSortState;
|
||||
|
||||
/**
|
||||
* Holds if there are any raw results. When that is the case, we
|
||||
* want to direct users to pay attention to raw results if
|
||||
* interpreted results are empty.
|
||||
*/
|
||||
nonemptyRawResults: boolean;
|
||||
|
||||
/**
|
||||
* Callback to show raw results.
|
||||
*/
|
||||
showRawResults: () => void;
|
||||
}
|
||||
|
||||
export const className = 'vscode-codeql__result-table';
|
||||
|
||||
@@ -123,6 +123,7 @@ export class ResultTables
|
||||
const resultSets = this.getResultSets();
|
||||
|
||||
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);
|
||||
|
||||
return <div>
|
||||
@@ -149,7 +150,9 @@ export class ResultTables
|
||||
<ResultTable key={resultSet.schema.name} resultSet={resultSet}
|
||||
databaseUri={this.props.database.databaseUri}
|
||||
resultsPath={this.props.resultsPath}
|
||||
sortState={this.props.sortStates.get(resultSet.schema.name)} />
|
||||
sortState={this.props.sortStates.get(resultSet.schema.name)}
|
||||
nonemptyRawResults={nonemptyRawResults}
|
||||
showRawResults={() => { this.setState({ selectedTable: SELECT_TABLE_NAME }) }} />
|
||||
}
|
||||
</div>;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import * as React from 'react';
|
||||
import * as Rdom from 'react-dom';
|
||||
import * as bqrs from 'semmle-bqrs';
|
||||
import { ElementBase, LocationValue, PrimitiveColumnValue, PrimitiveTypeKind, ResultSetSchema, tryGetResolvableLocation } from 'semmle-bqrs';
|
||||
import { ElementBase, PrimitiveColumnValue, PrimitiveTypeKind, ResultSetSchema, 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';
|
||||
|
||||
/**
|
||||
* results.tsx
|
||||
@@ -23,19 +24,6 @@ interface VsCodeApi {
|
||||
declare const acquireVsCodeApi: () => VsCodeApi;
|
||||
export const vscode = acquireVsCodeApi();
|
||||
|
||||
export interface ResultElement {
|
||||
label: string;
|
||||
location?: LocationValue;
|
||||
}
|
||||
|
||||
export interface ResultUri {
|
||||
uri: string;
|
||||
}
|
||||
|
||||
export type ResultValue = ResultElement | ResultUri | string;
|
||||
|
||||
export type ResultRow = ResultValue[];
|
||||
|
||||
export type RawTableResultSet = { t: 'RawResultSet' } & RawResultSet;
|
||||
export type PathTableResultSet = { t: 'SarifResultSet'; readonly schema: ResultSetSchema; name: string } & Interpretation;
|
||||
|
||||
@@ -43,11 +31,6 @@ export type ResultSet =
|
||||
| RawTableResultSet
|
||||
| PathTableResultSet;
|
||||
|
||||
export interface RawResultSet {
|
||||
readonly schema: ResultSetSchema;
|
||||
readonly rows: readonly ResultRow[];
|
||||
}
|
||||
|
||||
async function* getChunkIterator(response: Response): AsyncIterableIterator<Uint8Array> {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load results: (${response.status}) ${response.statusText}`);
|
||||
@@ -62,9 +45,7 @@ async function* getChunkIterator(response: Response): AsyncIterableIterator<Uint
|
||||
}
|
||||
}
|
||||
|
||||
function translatePrimitiveValue(value: PrimitiveColumnValue, type: PrimitiveTypeKind):
|
||||
ResultValue {
|
||||
|
||||
function translatePrimitiveValue(value: PrimitiveColumnValue, type: PrimitiveTypeKind): ResultValue {
|
||||
switch (type) {
|
||||
case 'i':
|
||||
case 'f':
|
||||
@@ -127,6 +108,7 @@ async function parseResultSets(response: Response): Promise<readonly ResultSet[]
|
||||
|
||||
interface ResultsInfo {
|
||||
resultsPath: string;
|
||||
resultSets: ResultSet[] | undefined;
|
||||
origResultsPaths: ResultsPaths;
|
||||
database: DatabaseInfo;
|
||||
interpretation: Interpretation | undefined;
|
||||
@@ -187,6 +169,7 @@ class App extends React.Component<{}, ResultsViewState> {
|
||||
case 'setState':
|
||||
this.updateStateWithNewResultsInfo({
|
||||
resultsPath: msg.resultsPath,
|
||||
resultSets: msg.resultSets?.map(x => ({ t: 'RawResultSet', ...x })),
|
||||
origResultsPaths: msg.origResultsPaths,
|
||||
sortedResultsMap: new Map(Object.entries(msg.sortedResultsMap)),
|
||||
database: msg.database,
|
||||
@@ -247,8 +230,9 @@ class App extends React.Component<{}, ResultsViewState> {
|
||||
let results: Results | null = null;
|
||||
let statusText = '';
|
||||
try {
|
||||
const resultSets = resultsInfo.resultSets || await this.getResultSets(resultsInfo);
|
||||
results = {
|
||||
resultSets: await this.getResultSets(resultsInfo),
|
||||
resultSets,
|
||||
database: resultsInfo.database,
|
||||
sortStates: this.getSortStates(resultsInfo)
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from 'chai';
|
||||
import 'mocha';
|
||||
import { LocationStyle, StringLocation, tryGetWholeFileLocation } from 'semmle-bqrs';
|
||||
import { LocationStyle, StringLocation, tryGetResolvableLocation } from 'semmle-bqrs';
|
||||
|
||||
describe('processing string locations', function () {
|
||||
it('should detect Windows whole-file locations', function () {
|
||||
@@ -8,7 +8,7 @@ describe('processing string locations', function () {
|
||||
t: LocationStyle.String,
|
||||
loc: 'file://C:/path/to/file.ext:0:0:0:0'
|
||||
};
|
||||
const wholeFileLoc = tryGetWholeFileLocation(loc);
|
||||
const wholeFileLoc = tryGetResolvableLocation(loc);
|
||||
expect(wholeFileLoc).to.eql({t: LocationStyle.WholeFile, file: 'C:/path/to/file.ext'});
|
||||
});
|
||||
it('should detect Unix whole-file locations', function () {
|
||||
@@ -16,12 +16,27 @@ describe('processing string locations', function () {
|
||||
t: LocationStyle.String,
|
||||
loc: 'file:///path/to/file.ext:0:0:0:0'
|
||||
};
|
||||
const wholeFileLoc = tryGetWholeFileLocation(loc);
|
||||
const wholeFileLoc = tryGetResolvableLocation(loc);
|
||||
expect(wholeFileLoc).to.eql({t: LocationStyle.WholeFile, file: '/path/to/file.ext'});
|
||||
});
|
||||
it('should detect Unix 5-part locations', function () {
|
||||
const loc: StringLocation = {
|
||||
t: LocationStyle.String,
|
||||
loc: 'file:///path/to/file.ext:1:2:3:4'
|
||||
};
|
||||
const wholeFileLoc = tryGetResolvableLocation(loc);
|
||||
expect(wholeFileLoc).to.eql({
|
||||
t: LocationStyle.FivePart,
|
||||
file: '/path/to/file.ext',
|
||||
lineStart: 1,
|
||||
colStart: 2,
|
||||
lineEnd: 3,
|
||||
colEnd: 4
|
||||
});
|
||||
});
|
||||
it('should ignore other string locations', function () {
|
||||
for (const loc of ['file:///path/to/file.ext', 'I am not a location']) {
|
||||
const wholeFileLoc = tryGetWholeFileLocation({
|
||||
const wholeFileLoc = tryGetResolvableLocation({
|
||||
t: LocationStyle.String,
|
||||
loc: loc
|
||||
});
|
||||
|
||||
@@ -48,18 +48,7 @@ describe('OutputChannelLogger tests', () => {
|
||||
});
|
||||
|
||||
it('should create a side log in the workspace area', async () => {
|
||||
await sideLogTest('storagePath', 'globalStoragePath');
|
||||
});
|
||||
|
||||
it('should create a side log in the global area', async () => {
|
||||
await sideLogTest('globalStoragePath', 'storagePath');
|
||||
});
|
||||
|
||||
async function sideLogTest(expectedArea: string, otherArea: string): Promise<void> {
|
||||
logger.init({
|
||||
[expectedArea]: tempFolders[expectedArea].name,
|
||||
[otherArea]: undefined
|
||||
});
|
||||
logger.init(tempFolders.storagePath.name);
|
||||
|
||||
await logger.log('xxx', { additionalLogLocation: 'first' });
|
||||
await logger.log('yyy', { additionalLogLocation: 'second' });
|
||||
@@ -67,19 +56,16 @@ describe('OutputChannelLogger tests', () => {
|
||||
await logger.log('aaa');
|
||||
|
||||
// expect 2 side logs
|
||||
const testLoggerFolder = path.join(tempFolders[expectedArea].name, 'test-logger');
|
||||
const testLoggerFolder = path.join(tempFolders.storagePath.name, 'test-logger');
|
||||
expect(fs.readdirSync(testLoggerFolder).length).to.equal(2);
|
||||
expect(fs.readdirSync(tempFolders[otherArea].name).length).to.equal(0);
|
||||
|
||||
// contents
|
||||
expect(fs.readFileSync(path.join(testLoggerFolder, 'first'), 'utf8')).to.equal('xxx\nzzz');
|
||||
expect(fs.readFileSync(path.join(testLoggerFolder, 'second'), 'utf8')).to.equal('yyy\n');
|
||||
}
|
||||
});
|
||||
|
||||
it('should delete side logs on dispose', async () => {
|
||||
logger.init({
|
||||
storagePath: tempFolders.storagePath.name
|
||||
});
|
||||
logger.init(tempFolders.storagePath.name);
|
||||
await logger.log('xxx', { additionalLogLocation: 'first' });
|
||||
await logger.log('yyy', { additionalLogLocation: 'second' });
|
||||
|
||||
@@ -94,9 +80,7 @@ describe('OutputChannelLogger tests', () => {
|
||||
});
|
||||
|
||||
it('should remove an additional log location', async () => {
|
||||
logger.init({
|
||||
storagePath: tempFolders.storagePath.name
|
||||
});
|
||||
logger.init(tempFolders.storagePath.name);
|
||||
await logger.log('xxx', { additionalLogLocation: 'first' });
|
||||
await logger.log('yyy', { additionalLogLocation: 'second' });
|
||||
|
||||
@@ -112,9 +96,7 @@ describe('OutputChannelLogger tests', () => {
|
||||
|
||||
it('should delete an existing folder on init', async () => {
|
||||
fs.createFileSync(path.join(tempFolders.storagePath.name, 'test-logger', 'xxx'));
|
||||
logger.init({
|
||||
storagePath: tempFolders.storagePath.name
|
||||
});
|
||||
logger.init(tempFolders.storagePath.name);
|
||||
// should be empty dir
|
||||
|
||||
const testLoggerFolder = path.join(tempFolders.storagePath.name, 'test-logger');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LocationStyle } from './bqrs-schema';
|
||||
import { LocationStyle } from "./bqrs-schema";
|
||||
|
||||
// See https://help.semmle.com/QL/learn-ql/ql/locations.html for how these are used.
|
||||
export interface FivePartLocation {
|
||||
@@ -31,54 +31,69 @@ export type LocationValue = RawLocationValue | WholeFileLocation;
|
||||
/** A location that may be resolved to a source code element. */
|
||||
export type ResolvableLocationValue = FivePartLocation | WholeFileLocation;
|
||||
|
||||
|
||||
/**
|
||||
* The CodeQL filesystem libraries use this pattern in `getURL()` predicates
|
||||
* to describe the location of an entire filesystem resource.
|
||||
* Such locations appear as `StringLocation`s instead of `FivePartLocation`s.
|
||||
*
|
||||
*
|
||||
* Folder resources also get similar URLs, but with the `folder` scheme.
|
||||
* They are deliberately ignored here, since there is no suitable location to show the user.
|
||||
*/
|
||||
const WHOLE_FILE_LOCATION_REGEX = /file:\/\/(.+):0:0:0:0/;
|
||||
|
||||
const FILE_LOCATION_REGEX = /file:\/\/(.+):([0-9]+):([0-9]+):([0-9]+):([0-9]+)/;
|
||||
/**
|
||||
* Gets a resolvable source file location for the specified `LocationValue`, if possible.
|
||||
* @param loc The location to test.
|
||||
*/
|
||||
export function tryGetResolvableLocation(loc: LocationValue | undefined): ResolvableLocationValue | undefined {
|
||||
export function tryGetResolvableLocation(
|
||||
loc: LocationValue | undefined
|
||||
): ResolvableLocationValue | undefined {
|
||||
if (loc === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
else if ((loc.t === LocationStyle.FivePart) && loc.file) {
|
||||
} else if (loc.t === LocationStyle.FivePart && loc.file) {
|
||||
return loc;
|
||||
}
|
||||
else if ((loc.t === LocationStyle.WholeFile) && loc.file) {
|
||||
} else if (loc.t === LocationStyle.WholeFile && loc.file) {
|
||||
return loc;
|
||||
}
|
||||
else if ((loc.t === LocationStyle.String) && loc.loc) {
|
||||
return tryGetWholeFileLocation(loc);
|
||||
}
|
||||
else {
|
||||
} else if (loc.t === LocationStyle.String && loc.loc) {
|
||||
return tryGetLocationFromString(loc);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function tryGetWholeFileLocation(loc: StringLocation): WholeFileLocation | undefined {
|
||||
const matches = WHOLE_FILE_LOCATION_REGEX.exec(loc.loc);
|
||||
export function tryGetLocationFromString(
|
||||
loc: StringLocation
|
||||
): ResolvableLocationValue | undefined {
|
||||
const matches = FILE_LOCATION_REGEX.exec(loc.loc);
|
||||
if (matches && matches.length > 1 && matches[1]) {
|
||||
// Whole-file location.
|
||||
// We could represent this as a FivePartLocation with all numeric fields set to zero,
|
||||
// but that would be a deliberate misuse as those fields are intended to be 1-based.
|
||||
return {
|
||||
t: LocationStyle.WholeFile,
|
||||
file: matches[1]
|
||||
};
|
||||
if (isWholeFileMatch(matches)) {
|
||||
return {
|
||||
t: LocationStyle.WholeFile,
|
||||
file: matches[1],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
t: LocationStyle.FivePart,
|
||||
file: matches[1],
|
||||
lineStart: Number(matches[2]),
|
||||
colStart: Number(matches[3]),
|
||||
lineEnd: Number(matches[4]),
|
||||
colEnd: Number(matches[5]),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function isWholeFileMatch(matches: RegExpExecArray): boolean {
|
||||
return (
|
||||
matches[2] === "0" &&
|
||||
matches[3] === "0" &&
|
||||
matches[4] === "0" &&
|
||||
matches[5] === "0"
|
||||
);
|
||||
}
|
||||
|
||||
export interface ElementBase {
|
||||
id: PrimitiveColumnValue;
|
||||
label?: string;
|
||||
@@ -93,8 +108,7 @@ export interface ElementWithLocation extends ElementBase {
|
||||
location: LocationValue;
|
||||
}
|
||||
|
||||
export interface Element extends Required<ElementBase> {
|
||||
}
|
||||
export interface Element extends Required<ElementBase> {}
|
||||
|
||||
export type PrimitiveColumnValue = string | boolean | number | Date;
|
||||
export type ColumnValue = PrimitiveColumnValue | ElementBase;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"newLineCharacter": "\n",
|
||||
"convertTabsToSpaces": true,
|
||||
"indentStyle": 2,
|
||||
"insertSpaceAfterCommaDelimiter": true,
|
||||
"insertSpaceAfterSemicolonInForStatements": true,
|
||||
|
||||
Reference in New Issue
Block a user