Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd5da2b0f0 | ||
|
|
55c21888af | ||
|
|
edb1af09c4 | ||
|
|
ab3822d1cc | ||
|
|
69120e0799 | ||
|
|
b7dafc31bb | ||
|
|
2f5a306c2d | ||
|
|
0ef6b45b19 | ||
|
|
d9f33d34e3 | ||
|
|
5758e03a17 | ||
|
|
5d9f80cce8 | ||
|
|
867ee530b1 | ||
|
|
27e6a55756 | ||
|
|
b237bafa2f | ||
|
|
d0bde800f7 | ||
|
|
da0090aa99 | ||
|
|
66c9879ce3 | ||
|
|
9c2585116a | ||
|
|
e46c0e25e8 | ||
|
|
658b0ce243 | ||
|
|
c084e31416 | ||
|
|
9046844f0c | ||
|
|
5a9b49b9bb | ||
|
|
0672133bca | ||
|
|
c0de99bc42 | ||
|
|
6dbb1a27b9 | ||
|
|
dc1bace4c6 | ||
|
|
afe3c56ca8 | ||
|
|
a6f42e3eb3 | ||
|
|
9c2bd2a57b | ||
|
|
f42f474113 | ||
|
|
17c31e1539 | ||
|
|
b0fb4d6bc9 | ||
|
|
f8690bcebc | ||
|
|
b0410ec5de | ||
|
|
19e0058e61 |
1
.github/workflows/codeql.yml
vendored
1
.github/workflows/codeql.yml
vendored
@@ -26,6 +26,7 @@ jobs:
|
||||
with:
|
||||
languages: javascript
|
||||
config-file: ./.github/codeql/codeql-config.yml
|
||||
tools: latest
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@main
|
||||
|
||||
2
.github/workflows/main.yml
vendored
2
.github/workflows/main.yml
vendored
@@ -135,7 +135,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
version: ['v2.3.3', 'v2.4.6', 'v2.5.9', 'v2.6.3', 'v2.7.6', 'nightly']
|
||||
version: ['v2.3.3', 'v2.4.6', 'v2.5.9', 'v2.6.3', 'v2.7.6', 'v2.8.0', 'nightly']
|
||||
env:
|
||||
CLI_VERSION: ${{ matrix.version }}
|
||||
NIGHTLY_URL: ${{ needs.find-nightly.outputs.url }}
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
**/* @github/codeql-vscode-reviewers
|
||||
/extensions/ql-vscode/src/remote-queries/ @github/code-scanning-secexp-reviewers
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# CodeQL for Visual Studio Code: Changelog
|
||||
|
||||
## 1.5.11 - 10 February 2022
|
||||
|
||||
- Fix a bug where invoking _View AST_ from the file explorer would not view the selected file. Instead it would view the active editor. Also, prevent the _View AST_ from appearing if the current selection includes a directory or multiple files. [#1113](https://github.com/github/vscode-codeql/pull/1113)
|
||||
- Add query history items as soon as a query is run, including new icons for each history item. [#1094](https://github.com/github/vscode-codeql/pull/1094)
|
||||
- Allow in-progress query items to be cancelled from the query history view. [#1105](https://github.com/github/vscode-codeql/pull/1105)
|
||||
|
||||
## 1.5.10 - 25 January 2022
|
||||
|
||||
- Fix a bug where the results view moved column even when it was already visible. [#1070](https://github.com/github/vscode-codeql/pull/1070)
|
||||
|
||||
7
extensions/ql-vscode/media/drive.svg
Normal file
7
extensions/ql-vscode/media/drive.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.5 12.1952C15.5 12.9126 14.9137 13.4996 14.1957 13.4996H1.80435C1.08696 13.4996 0.5 12.9126 0.5 12.1952L0.5 9.80435C0.5 9.08696 1.08696 8.5 1.80435 8.5H14.1956C14.9137 8.5 15.5 9.08696 15.5 9.80435L15.5 12.1952Z" stroke="#959DA5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2.45654 11.5H13.5435" stroke="#959DA5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.5 9.5C13.224 9.5 13 9.725 13 10C13 10.275 13.224 10.5 13.5 10.5C13.776 10.5 14 10.275 14 10C14 9.725 13.776 9.5 13.5 9.5" fill="#959DA5"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.5 9.5C11.224 9.5 11 9.725 11 10C11 10.275 11.224 10.5 11.5 10.5C11.776 10.5 12 10.275 12 10C12 9.725 11.776 9.5 11.5 9.5" fill="#959DA5"/>
|
||||
<path d="M15.5 9.81464L13.8728 2.76261C13.6922 2.06804 12.9572 1.5 12.2391 1.5H3.76087C3.04348 1.5 2.30848 2.06804 2.12783 2.76261L0.5 9.8" stroke="#959DA5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
2231
extensions/ql-vscode/package-lock.json
generated
2231
extensions/ql-vscode/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@
|
||||
"description": "CodeQL for Visual Studio Code",
|
||||
"author": "GitHub",
|
||||
"private": true,
|
||||
"version": "1.5.10",
|
||||
"version": "1.5.11",
|
||||
"publisher": "GitHub",
|
||||
"license": "MIT",
|
||||
"icon": "media/VS-marketplace-CodeQL-icon.png",
|
||||
@@ -13,7 +13,9 @@
|
||||
"url": "https://github.com/github/vscode-codeql"
|
||||
},
|
||||
"engines": {
|
||||
"vscode": "^1.57.0"
|
||||
"vscode": "^1.59.0",
|
||||
"node": "^14.17.1",
|
||||
"npm": "^7.20.6"
|
||||
},
|
||||
"categories": [
|
||||
"Programming Languages"
|
||||
@@ -494,6 +496,10 @@
|
||||
"command": "codeQLQueryHistory.showQueryLog",
|
||||
"title": "Show Query Log"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.cancel",
|
||||
"title": "Cancel"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.showQueryText",
|
||||
"title": "Show Query Text"
|
||||
@@ -662,7 +668,7 @@
|
||||
{
|
||||
"command": "codeQLQueryHistory.removeHistoryItem",
|
||||
"group": "9_qlCommands",
|
||||
"when": "view == codeQLQueryHistory"
|
||||
"when": "viewItem == interpretedResultsItem || viewItem == rawResultsItem || viewItem == cancelledResultsItem"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.setLabel",
|
||||
@@ -672,12 +678,12 @@
|
||||
{
|
||||
"command": "codeQLQueryHistory.compareWith",
|
||||
"group": "9_qlCommands",
|
||||
"when": "view == codeQLQueryHistory"
|
||||
"when": "viewItem == rawResultsItem || viewItem == interpretedResultsItem"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.showQueryLog",
|
||||
"group": "9_qlCommands",
|
||||
"when": "view == codeQLQueryHistory"
|
||||
"when": "viewItem == rawResultsItem || viewItem == interpretedResultsItem"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.showQueryText",
|
||||
@@ -687,37 +693,37 @@
|
||||
{
|
||||
"command": "codeQLQueryHistory.viewCsvResults",
|
||||
"group": "9_qlCommands",
|
||||
"when": "view == codeQLQueryHistory && viewItem != interpretedResultsItem"
|
||||
"when": "viewItem == rawResultsItem"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.viewCsvAlerts",
|
||||
"group": "9_qlCommands",
|
||||
"when": "view == codeQLQueryHistory && viewItem == interpretedResultsItem"
|
||||
"when": "viewItem == interpretedResultsItem"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.viewSarifAlerts",
|
||||
"group": "9_qlCommands",
|
||||
"when": "view == codeQLQueryHistory && viewItem == interpretedResultsItem"
|
||||
"when": "viewItem == interpretedResultsItem"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.viewDil",
|
||||
"group": "9_qlCommands",
|
||||
"when": "view == codeQLQueryHistory"
|
||||
"when": "viewItem == rawResultsItem || viewItem == interpretedResultsItem"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.previewQueryHelp",
|
||||
"command": "codeQLQueryHistory.cancel",
|
||||
"group": "9_qlCommands",
|
||||
"when": "view == codeQLQueryHistory && resourceScheme == .qhelp && isWorkspaceTrusted"
|
||||
"when": "viewItem == inProgressResultsItem"
|
||||
},
|
||||
{
|
||||
"command": "codeQLTests.showOutputDifferences",
|
||||
"group": "qltest@1",
|
||||
"when": "view == test-explorer && viewItem == testWithSource"
|
||||
"when": "viewItem == testWithSource"
|
||||
},
|
||||
{
|
||||
"command": "codeQLTests.acceptOutput",
|
||||
"group": "qltest@2",
|
||||
"when": "view == test-explorer && viewItem == testWithSource"
|
||||
"when": "viewItem == testWithSource"
|
||||
}
|
||||
],
|
||||
"explorer/context": [
|
||||
@@ -729,7 +735,7 @@
|
||||
{
|
||||
"command": "codeQL.viewAst",
|
||||
"group": "9_qlCommands",
|
||||
"when": "resourceScheme == codeql-zip-archive"
|
||||
"when": "resourceScheme == codeql-zip-archive && !explorerResourceIsFolder && !listMultiSelection"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.runQueries",
|
||||
@@ -860,6 +866,10 @@
|
||||
"command": "codeQLQueryHistory.showQueryLog",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.cancel",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueryHistory.showQueryText",
|
||||
"when": "false"
|
||||
@@ -1003,20 +1013,24 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@octokit/rest": "^18.5.6",
|
||||
"@primer/octicons-react": "^16.3.0",
|
||||
"@primer/react": "^34.3.0",
|
||||
"child-process-promise": "^2.2.1",
|
||||
"classnames": "~2.2.6",
|
||||
"fs-extra": "^9.0.1",
|
||||
"glob-promise": "^3.4.0",
|
||||
"js-yaml": "^3.14.0",
|
||||
"minimist": "~1.2.5",
|
||||
"nanoid": "^3.2.0",
|
||||
"node-fetch": "~2.6.7",
|
||||
"path-browserify": "^1.0.1",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"semver": "~7.3.2",
|
||||
"stream": "^0.0.2",
|
||||
"stream-chain": "~2.2.4",
|
||||
"stream-json": "~1.7.3",
|
||||
"styled-components": "^5.3.3",
|
||||
"tmp": "^0.1.0",
|
||||
"tmp-promise": "~3.0.2",
|
||||
"tree-kill": "~1.2.2",
|
||||
@@ -1043,11 +1057,12 @@
|
||||
"@types/js-yaml": "^3.12.5",
|
||||
"@types/jszip": "~3.1.6",
|
||||
"@types/mocha": "^9.0.0",
|
||||
"@types/nanoid": "^3.0.0",
|
||||
"@types/node": "^12.14.1",
|
||||
"@types/node-fetch": "~2.5.2",
|
||||
"@types/proxyquire": "~1.3.28",
|
||||
"@types/react": "^16.8.17",
|
||||
"@types/react-dom": "^16.8.4",
|
||||
"@types/react": "^17.0.2",
|
||||
"@types/react-dom": "^17.0.2",
|
||||
"@types/sarif": "~2.1.2",
|
||||
"@types/semver": "~7.2.0",
|
||||
"@types/sinon": "~7.5.2",
|
||||
@@ -1057,7 +1072,7 @@
|
||||
"@types/through2": "^2.0.36",
|
||||
"@types/tmp": "^0.1.0",
|
||||
"@types/unzipper": "~0.10.1",
|
||||
"@types/vscode": "^1.57.0",
|
||||
"@types/vscode": "^1.59.0",
|
||||
"@types/webpack": "^4.32.1",
|
||||
"@types/xml2js": "~0.4.4",
|
||||
"@typescript-eslint/eslint-plugin": "^4.26.0",
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
import * as path from 'path';
|
||||
|
||||
import { tmpDir } from '../run-queries';
|
||||
import { CompletedQuery } from '../query-results';
|
||||
import {
|
||||
FromCompareViewMessage,
|
||||
ToCompareViewMessage,
|
||||
@@ -21,10 +20,11 @@ import { DatabaseManager } from '../databases';
|
||||
import { getHtmlForWebview, jumpToLocation } from '../interface-utils';
|
||||
import { transformBqrsResultSet, RawResultSet, BQRSInfo } from '../pure/bqrs-cli-types';
|
||||
import resultsDiff from './resultsDiff';
|
||||
import { FullCompletedQueryInfo } from '../query-results';
|
||||
|
||||
interface ComparePair {
|
||||
from: CompletedQuery;
|
||||
to: CompletedQuery;
|
||||
from: FullCompletedQueryInfo;
|
||||
to: FullCompletedQueryInfo;
|
||||
}
|
||||
|
||||
export class CompareInterfaceManager extends DisposableObject {
|
||||
@@ -39,15 +39,15 @@ export class CompareInterfaceManager extends DisposableObject {
|
||||
private cliServer: CodeQLCliServer,
|
||||
private logger: Logger,
|
||||
private showQueryResultsCallback: (
|
||||
item: CompletedQuery
|
||||
item: FullCompletedQueryInfo
|
||||
) => Promise<void>
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async showResults(
|
||||
from: CompletedQuery,
|
||||
to: CompletedQuery,
|
||||
from: FullCompletedQueryInfo,
|
||||
to: FullCompletedQueryInfo,
|
||||
selectedResultSetName?: string
|
||||
) {
|
||||
this.comparePair = { from, to };
|
||||
@@ -80,18 +80,14 @@ export class CompareInterfaceManager extends DisposableObject {
|
||||
// since we split the description into several rows
|
||||
// only run interpolation if the label is user-defined
|
||||
// otherwise we will wind up with duplicated rows
|
||||
name: from.options.label
|
||||
? from.interpolate(from.getLabel())
|
||||
: from.queryName,
|
||||
status: from.statusString,
|
||||
time: from.time,
|
||||
name: from.getShortLabel(),
|
||||
status: from.completedQuery.statusString,
|
||||
time: from.startTime,
|
||||
},
|
||||
toQuery: {
|
||||
name: to.options.label
|
||||
? to.interpolate(to.getLabel())
|
||||
: to.queryName,
|
||||
status: to.statusString,
|
||||
time: to.time,
|
||||
name: to.getShortLabel(),
|
||||
status: to.completedQuery.statusString,
|
||||
time: to.startTime,
|
||||
},
|
||||
},
|
||||
columns: fromResultSet.schema.columns,
|
||||
@@ -99,7 +95,7 @@ export class CompareInterfaceManager extends DisposableObject {
|
||||
currentResultSetName: currentResultSetName,
|
||||
rows,
|
||||
message,
|
||||
datebaseUri: to.database.databaseUri,
|
||||
databaseUri: to.initialInfo.databaseInfo.databaseUri,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -121,14 +117,14 @@ export class CompareInterfaceManager extends DisposableObject {
|
||||
],
|
||||
}
|
||||
));
|
||||
this.panel.onDidDispose(
|
||||
this.push(this.panel.onDidDispose(
|
||||
() => {
|
||||
this.panel = undefined;
|
||||
this.comparePair = undefined;
|
||||
},
|
||||
null,
|
||||
ctx.subscriptions
|
||||
);
|
||||
));
|
||||
|
||||
const scriptPathOnDisk = Uri.file(
|
||||
ctx.asAbsolutePath('out/compareView.js')
|
||||
@@ -141,13 +137,14 @@ export class CompareInterfaceManager extends DisposableObject {
|
||||
panel.webview.html = getHtmlForWebview(
|
||||
panel.webview,
|
||||
scriptPathOnDisk,
|
||||
[stylesheetPathOnDisk]
|
||||
[stylesheetPathOnDisk],
|
||||
false
|
||||
);
|
||||
panel.webview.onDidReceiveMessage(
|
||||
this.push(panel.webview.onDidReceiveMessage(
|
||||
async (e) => this.handleMsgFromView(e),
|
||||
undefined,
|
||||
ctx.subscriptions
|
||||
);
|
||||
));
|
||||
}
|
||||
return this.panel;
|
||||
}
|
||||
@@ -191,15 +188,15 @@ export class CompareInterfaceManager extends DisposableObject {
|
||||
}
|
||||
|
||||
private async findCommonResultSetNames(
|
||||
from: CompletedQuery,
|
||||
to: CompletedQuery,
|
||||
from: FullCompletedQueryInfo,
|
||||
to: FullCompletedQueryInfo,
|
||||
selectedResultSetName: string | undefined
|
||||
): Promise<[string[], string, RawResultSet, RawResultSet]> {
|
||||
const fromSchemas = await this.cliServer.bqrsInfo(
|
||||
from.query.resultsPaths.resultsPath
|
||||
from.completedQuery.query.resultsPaths.resultsPath
|
||||
);
|
||||
const toSchemas = await this.cliServer.bqrsInfo(
|
||||
to.query.resultsPaths.resultsPath
|
||||
to.completedQuery.query.resultsPaths.resultsPath
|
||||
);
|
||||
const fromSchemaNames = fromSchemas['result-sets'].map(
|
||||
(schema) => schema.name
|
||||
@@ -215,12 +212,12 @@ export class CompareInterfaceManager extends DisposableObject {
|
||||
const fromResultSet = await this.getResultSet(
|
||||
fromSchemas,
|
||||
currentResultSetName,
|
||||
from.query.resultsPaths.resultsPath
|
||||
from.completedQuery.query.resultsPaths.resultsPath
|
||||
);
|
||||
const toResultSet = await this.getResultSet(
|
||||
toSchemas,
|
||||
currentResultSetName,
|
||||
to.query.resultsPaths.resultsPath
|
||||
to.completedQuery.query.resultsPaths.resultsPath
|
||||
);
|
||||
return [
|
||||
commonResultSetNames,
|
||||
|
||||
@@ -17,7 +17,7 @@ const emptyComparison: SetComparisonsMessage = {
|
||||
columns: [],
|
||||
commonResultSetNames: [],
|
||||
currentResultSetName: '',
|
||||
datebaseUri: '',
|
||||
databaseUri: '',
|
||||
message: 'Empty comparison'
|
||||
};
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ export default function CompareTable(props: Props) {
|
||||
schemaName={comparison.currentResultSetName}
|
||||
preventSort={true}
|
||||
/>
|
||||
{createRows(rows.from, comparison.datebaseUri)}
|
||||
{createRows(rows.from, comparison.databaseUri)}
|
||||
</table>
|
||||
</td>
|
||||
<td>
|
||||
@@ -86,7 +86,7 @@ export default function CompareTable(props: Props) {
|
||||
schemaName={comparison.currentResultSetName}
|
||||
preventSort={true}
|
||||
/>
|
||||
{createRows(rows.to, comparison.datebaseUri)}
|
||||
{createRows(rows.to, comparison.databaseUri)}
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
import { decodeSourceArchiveUri, encodeArchiveBasePath } from '../archive-filesystem-provider';
|
||||
import { ColumnKindCode, EntityValue, getResultSetSchema, ResultSetSchema } from '../pure/bqrs-cli-types';
|
||||
import { CodeQLCliServer } from '../cli';
|
||||
@@ -7,16 +5,17 @@ import { DatabaseManager, DatabaseItem } from '../databases';
|
||||
import fileRangeFromURI from './fileRangeFromURI';
|
||||
import * as messages from '../pure/messages';
|
||||
import { QueryServerClient } from '../queryserver-client';
|
||||
import { QueryWithResults, compileAndRunQueryAgainstDatabase } from '../run-queries';
|
||||
import { QueryWithResults, compileAndRunQueryAgainstDatabase, createInitialQueryInfo } from '../run-queries';
|
||||
import { ProgressCallback } from '../commandRunner';
|
||||
import { KeyType } from './keyType';
|
||||
import { qlpackOfDatabase, resolveQueries } from './queryResolver';
|
||||
import { CancellationToken, LocationLink, Uri } from 'vscode';
|
||||
|
||||
export const SELECT_QUERY_NAME = '#select';
|
||||
export const TEMPLATE_NAME = 'selectedSourceFile';
|
||||
|
||||
export interface FullLocationLink extends vscode.LocationLink {
|
||||
originUri: vscode.Uri;
|
||||
export interface FullLocationLink extends LocationLink {
|
||||
originUri: Uri;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -40,10 +39,10 @@ export async function getLocationsForUriString(
|
||||
uriString: string,
|
||||
keyType: KeyType,
|
||||
progress: ProgressCallback,
|
||||
token: vscode.CancellationToken,
|
||||
token: CancellationToken,
|
||||
filter: (src: string, dest: string) => boolean
|
||||
): Promise<FullLocationLink[]> {
|
||||
const uri = decodeSourceArchiveUri(vscode.Uri.parse(uriString, true));
|
||||
const uri = decodeSourceArchiveUri(Uri.parse(uriString, true));
|
||||
const sourceArchiveUri = encodeArchiveBasePath(uri.sourceArchiveZipPath);
|
||||
|
||||
const db = dbm.findDatabaseItemBySourceArchive(sourceArchiveUri);
|
||||
@@ -56,12 +55,20 @@ export async function getLocationsForUriString(
|
||||
|
||||
const links: FullLocationLink[] = [];
|
||||
for (const query of await resolveQueries(cli, qlpack, keyType)) {
|
||||
const initialInfo = await createInitialQueryInfo(
|
||||
Uri.file(query),
|
||||
{
|
||||
name: db.name,
|
||||
databaseUri: db.databaseUri.toString(),
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
const results = await compileAndRunQueryAgainstDatabase(
|
||||
cli,
|
||||
qs,
|
||||
db,
|
||||
false,
|
||||
vscode.Uri.file(query),
|
||||
initialInfo,
|
||||
progress,
|
||||
token,
|
||||
templates
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
TextDocument,
|
||||
Uri
|
||||
} from 'vscode';
|
||||
import * as path from 'path';
|
||||
|
||||
import { decodeSourceArchiveUri, encodeArchiveBasePath, zipArchiveScheme } from '../archive-filesystem-provider';
|
||||
import { CodeQLCliServer } from '../cli';
|
||||
@@ -18,7 +19,7 @@ import { CachedOperation } from '../helpers';
|
||||
import { ProgressCallback, withProgress } from '../commandRunner';
|
||||
import * as messages from '../pure/messages';
|
||||
import { QueryServerClient } from '../queryserver-client';
|
||||
import { compileAndRunQueryAgainstDatabase, QueryWithResults } from '../run-queries';
|
||||
import { compileAndRunQueryAgainstDatabase, createInitialQueryInfo, QueryWithResults } from '../run-queries';
|
||||
import AstBuilder from './astBuilder';
|
||||
import {
|
||||
KeyType,
|
||||
@@ -123,33 +124,38 @@ export class TemplateQueryReferenceProvider implements ReferenceProvider {
|
||||
}
|
||||
}
|
||||
|
||||
type QueryWithDb = {
|
||||
query: QueryWithResults,
|
||||
dbUri: Uri
|
||||
};
|
||||
|
||||
export class TemplatePrintAstProvider {
|
||||
private cache: CachedOperation<QueryWithResults>;
|
||||
private cache: CachedOperation<QueryWithDb>;
|
||||
|
||||
constructor(
|
||||
private cli: CodeQLCliServer,
|
||||
private qs: QueryServerClient,
|
||||
private dbm: DatabaseManager,
|
||||
) {
|
||||
this.cache = new CachedOperation<QueryWithResults>(this.getAst.bind(this));
|
||||
this.cache = new CachedOperation<QueryWithDb>(this.getAst.bind(this));
|
||||
}
|
||||
|
||||
async provideAst(
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
document?: TextDocument
|
||||
fileUri?: Uri
|
||||
): Promise<AstBuilder | undefined> {
|
||||
if (!document) {
|
||||
if (!fileUri) {
|
||||
throw new Error('Cannot view the AST. Please select a valid source file inside a CodeQL database.');
|
||||
}
|
||||
const queryResults = this.shouldCache()
|
||||
? await this.cache.get(document.uri.toString(), progress, token)
|
||||
: await this.getAst(document.uri.toString(), progress, token);
|
||||
const { query, dbUri } = this.shouldCache()
|
||||
? await this.cache.get(fileUri.toString(), progress, token)
|
||||
: await this.getAst(fileUri.toString(), progress, token);
|
||||
|
||||
return new AstBuilder(
|
||||
queryResults, this.cli,
|
||||
this.dbm.findDatabaseItem(Uri.parse(queryResults.database.databaseUri!, true))!,
|
||||
document.fileName
|
||||
query, this.cli,
|
||||
this.dbm.findDatabaseItem(dbUri)!,
|
||||
path.basename(fileUri.fsPath),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -161,7 +167,7 @@ export class TemplatePrintAstProvider {
|
||||
uriString: string,
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken
|
||||
): Promise<QueryWithResults> {
|
||||
): Promise<QueryWithDb> {
|
||||
const uri = Uri.parse(uriString, true);
|
||||
if (uri.scheme !== zipArchiveScheme) {
|
||||
throw new Error('Cannot view the AST. Please select a valid source file inside a CodeQL database.');
|
||||
@@ -195,15 +201,26 @@ export class TemplatePrintAstProvider {
|
||||
}
|
||||
};
|
||||
|
||||
return await compileAndRunQueryAgainstDatabase(
|
||||
this.cli,
|
||||
this.qs,
|
||||
db,
|
||||
false,
|
||||
const initialInfo = await createInitialQueryInfo(
|
||||
Uri.file(query),
|
||||
progress,
|
||||
token,
|
||||
templates
|
||||
{
|
||||
name: db.name,
|
||||
databaseUri: db.databaseUri.toString(),
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
return {
|
||||
query: await compileAndRunQueryAgainstDatabase(
|
||||
this.cli,
|
||||
this.qs,
|
||||
db,
|
||||
initialInfo,
|
||||
progress,
|
||||
token,
|
||||
templates
|
||||
),
|
||||
dbUri: db.databaseUri
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
CancellationToken,
|
||||
CancellationTokenSource,
|
||||
commands,
|
||||
Disposable,
|
||||
ExtensionContext,
|
||||
@@ -59,10 +60,10 @@ import { InterfaceManager } from './interface';
|
||||
import { WebviewReveal } from './interface-utils';
|
||||
import { ideServerLogger, logger, queryServerLogger } from './logging';
|
||||
import { QueryHistoryManager } from './query-history';
|
||||
import { CompletedQuery } from './query-results';
|
||||
import { FullCompletedQueryInfo, FullQueryInfo } from './query-results';
|
||||
import * as qsClient from './queryserver-client';
|
||||
import { displayQuickQuery } from './quick-query';
|
||||
import { compileAndRunQueryAgainstDatabase, tmpDirDisposal } from './run-queries';
|
||||
import { compileAndRunQueryAgainstDatabase, createInitialQueryInfo, tmpDirDisposal } from './run-queries';
|
||||
import { QLTestAdapterFactory } from './test-adapter';
|
||||
import { TestUIService } from './test-ui';
|
||||
import { CompareInterfaceManager } from './compare/compare-interface';
|
||||
@@ -80,9 +81,10 @@ import { CodeQlStatusBarHandler } from './status-bar';
|
||||
import { Credentials } from './authentication';
|
||||
import { RemoteQueriesManager } from './remote-queries/remote-queries-manager';
|
||||
import { RemoteQuery } from './remote-queries/remote-query';
|
||||
import { RemoteQueryResult } from './remote-queries/remote-query-result';
|
||||
import { URLSearchParams } from 'url';
|
||||
import { RemoteQueriesInterfaceManager } from './remote-queries/remote-queries-interface';
|
||||
import { sampleRemoteQuery, sampleRemoteQueryResult } from './remote-queries/sample-data';
|
||||
import * as sampleData from './remote-queries/sample-data';
|
||||
import { handleDownloadPacks, handleInstallPackDependencies } from './packaging';
|
||||
import { AnalysesResultsManager } from './remote-queries/analyses-results-manager';
|
||||
|
||||
@@ -431,15 +433,16 @@ async function activateWithInstalledDistribution(
|
||||
void logger.log('Initializing query history manager.');
|
||||
const queryHistoryConfigurationListener = new QueryHistoryConfigListener();
|
||||
ctx.subscriptions.push(queryHistoryConfigurationListener);
|
||||
const showResults = async (item: CompletedQuery) =>
|
||||
const showResults = async (item: FullCompletedQueryInfo) =>
|
||||
showResultsForCompletedQuery(item, WebviewReveal.Forced);
|
||||
|
||||
const qhm = new QueryHistoryManager(
|
||||
qs,
|
||||
dbm,
|
||||
ctx.extensionPath,
|
||||
queryHistoryConfigurationListener,
|
||||
showResults,
|
||||
async (from: CompletedQuery, to: CompletedQuery) =>
|
||||
async (from: FullCompletedQueryInfo, to: FullCompletedQueryInfo) =>
|
||||
showResultsForComparison(from, to),
|
||||
);
|
||||
ctx.subscriptions.push(qhm);
|
||||
@@ -461,8 +464,8 @@ async function activateWithInstalledDistribution(
|
||||
archiveFilesystemProvider.activate(ctx);
|
||||
|
||||
async function showResultsForComparison(
|
||||
from: CompletedQuery,
|
||||
to: CompletedQuery
|
||||
from: FullCompletedQueryInfo,
|
||||
to: FullCompletedQueryInfo
|
||||
): Promise<void> {
|
||||
try {
|
||||
await cmpm.showResults(from, to);
|
||||
@@ -472,7 +475,7 @@ async function activateWithInstalledDistribution(
|
||||
}
|
||||
|
||||
async function showResultsForCompletedQuery(
|
||||
query: CompletedQuery,
|
||||
query: FullCompletedQueryInfo,
|
||||
forceReveal: WebviewReveal
|
||||
): Promise<void> {
|
||||
await intm.showResults(query, forceReveal, false);
|
||||
@@ -492,22 +495,38 @@ async function activateWithInstalledDistribution(
|
||||
if (databaseItem === undefined) {
|
||||
throw new Error('Can\'t run query without a selected database');
|
||||
}
|
||||
const info = await compileAndRunQueryAgainstDatabase(
|
||||
cliServer,
|
||||
qs,
|
||||
databaseItem,
|
||||
quickEval,
|
||||
selectedQuery,
|
||||
progress,
|
||||
token,
|
||||
undefined,
|
||||
range
|
||||
);
|
||||
const item = qhm.buildCompletedQuery(info);
|
||||
await showResultsForCompletedQuery(item, WebviewReveal.NotForced);
|
||||
// Note we must update the query history view after showing results as the
|
||||
// display and sorting might depend on the number of results
|
||||
await qhm.addCompletedQuery(item);
|
||||
const databaseInfo = {
|
||||
name: databaseItem.name,
|
||||
databaseUri: databaseItem.databaseUri.toString(),
|
||||
};
|
||||
|
||||
// handle cancellation from the history view.
|
||||
const source = new CancellationTokenSource();
|
||||
token.onCancellationRequested(() => source.cancel());
|
||||
|
||||
const initialInfo = await createInitialQueryInfo(selectedQuery, databaseInfo, quickEval, range);
|
||||
const item = new FullQueryInfo(initialInfo, queryHistoryConfigurationListener, source);
|
||||
qhm.addQuery(item);
|
||||
try {
|
||||
const completedQueryInfo = await compileAndRunQueryAgainstDatabase(
|
||||
cliServer,
|
||||
qs,
|
||||
databaseItem,
|
||||
initialInfo,
|
||||
progress,
|
||||
source.token,
|
||||
);
|
||||
item.completeThisQuery(completedQueryInfo);
|
||||
await showResultsForCompletedQuery(item as FullCompletedQueryInfo, WebviewReveal.NotForced);
|
||||
// Note we must update the query history view after showing results as the
|
||||
// display and sorting might depend on the number of results
|
||||
} catch (e) {
|
||||
item.failureReason = e.message;
|
||||
throw e;
|
||||
} finally {
|
||||
qhm.refreshTreeView();
|
||||
source.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -778,7 +797,7 @@ async function activateWithInstalledDistribution(
|
||||
);
|
||||
|
||||
void logger.log('Initializing remote queries interface.');
|
||||
const rqm = new RemoteQueriesManager(ctx, logger, cliServer);
|
||||
const rqm = new RemoteQueriesManager(ctx, cliServer, logger);
|
||||
|
||||
registerRemoteQueryTextProvider();
|
||||
|
||||
@@ -816,11 +835,22 @@ async function activateWithInstalledDistribution(
|
||||
await rqm.monitorRemoteQuery(query, token);
|
||||
}));
|
||||
|
||||
ctx.subscriptions.push(
|
||||
commandRunner('codeQL.autoDownloadRemoteQueryResults', async (
|
||||
queryResult: RemoteQueryResult,
|
||||
token: CancellationToken) => {
|
||||
await rqm.autoDownloadRemoteQueryResults(queryResult, token);
|
||||
}));
|
||||
|
||||
ctx.subscriptions.push(
|
||||
commandRunner('codeQL.showFakeRemoteQueryResults', async () => {
|
||||
const analysisResultsManager = new AnalysesResultsManager(ctx, logger);
|
||||
const rqim = new RemoteQueriesInterfaceManager(ctx, logger, analysisResultsManager);
|
||||
await rqim.showResults(sampleRemoteQuery, sampleRemoteQueryResult);
|
||||
await rqim.showResults(sampleData.sampleRemoteQuery, sampleData.sampleRemoteQueryResult);
|
||||
|
||||
await rqim.setAnalysisResults(sampleData.sampleAnalysesResultsStage1);
|
||||
await rqim.setAnalysisResults(sampleData.sampleAnalysesResultsStage2);
|
||||
await rqim.setAnalysisResults(sampleData.sampleAnalysesResultsStage3);
|
||||
}));
|
||||
|
||||
ctx.subscriptions.push(
|
||||
@@ -945,9 +975,11 @@ async function activateWithInstalledDistribution(
|
||||
}
|
||||
));
|
||||
|
||||
commands.registerCommand('codeQL.showLogs', () => {
|
||||
logger.show();
|
||||
});
|
||||
ctx.subscriptions.push(
|
||||
commandRunner('codeQL.showLogs', async () => {
|
||||
logger.show();
|
||||
})
|
||||
);
|
||||
|
||||
void logger.log('Starting language server.');
|
||||
ctx.subscriptions.push(client.start());
|
||||
@@ -970,12 +1002,13 @@ async function activateWithInstalledDistribution(
|
||||
ctx.subscriptions.push(astViewer);
|
||||
ctx.subscriptions.push(commandRunnerWithProgress('codeQL.viewAst', async (
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken
|
||||
token: CancellationToken,
|
||||
selectedFile: Uri
|
||||
) => {
|
||||
const ast = await templateProvider.provideAst(
|
||||
progress,
|
||||
token,
|
||||
window.activeTextEditor?.document,
|
||||
selectedFile ?? window.activeTextEditor?.document.uri,
|
||||
);
|
||||
if (ast) {
|
||||
astViewer.updateRoots(await ast.getRoots(), ast.db, ast.fileName);
|
||||
@@ -1019,7 +1052,7 @@ const checkForUpdatesCommand = 'codeQL.checkForUpdatesToCLI';
|
||||
|
||||
/**
|
||||
* This text provider lets us open readonly files in the editor.
|
||||
*
|
||||
*
|
||||
* TODO: Consolidate this with the 'codeql' text provider in query-history.ts.
|
||||
*/
|
||||
function registerRemoteQueryTextProvider() {
|
||||
|
||||
@@ -119,6 +119,7 @@ export function getHtmlForWebview(
|
||||
webview: Webview,
|
||||
scriptUriOnDisk: Uri,
|
||||
stylesheetUrisOnDisk: Uri[],
|
||||
allowInlineStyles: boolean
|
||||
): string {
|
||||
// Convert the on-disk URIs into webview URIs.
|
||||
const scriptWebviewUri = webview.asWebviewUri(scriptUriOnDisk);
|
||||
@@ -128,8 +129,13 @@ export function getHtmlForWebview(
|
||||
// Use a nonce in the content security policy to uniquely identify the above resources.
|
||||
const nonce = getNonce();
|
||||
|
||||
const stylesheetsHtmlLines = stylesheetWebviewUris.map(stylesheetWebviewUri =>
|
||||
`<link nonce="${nonce}" rel="stylesheet" href="${stylesheetWebviewUri}">`);
|
||||
const stylesheetsHtmlLines = allowInlineStyles
|
||||
? stylesheetWebviewUris.map(uri => createStylesLinkWithoutNonce(uri))
|
||||
: stylesheetWebviewUris.map(uri => createStylesLinkWithNonce(nonce, uri));
|
||||
|
||||
const styleSrc = allowInlineStyles
|
||||
? 'https://*.vscode-webview.net/ vscode-file: \'unsafe-inline\''
|
||||
: `'nonce-${nonce}'`;
|
||||
|
||||
/*
|
||||
* Content security policy:
|
||||
@@ -143,7 +149,7 @@ export function getHtmlForWebview(
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Security-Policy"
|
||||
content="default-src 'none'; script-src 'nonce-${nonce}'; style-src 'nonce-${nonce}'; connect-src ${webview.cspSource};">
|
||||
content="default-src 'none'; script-src 'nonce-${nonce}'; style-src ${styleSrc}; connect-src ${webview.cspSource};">
|
||||
${stylesheetsHtmlLines.join(` ${os.EOL}`)}
|
||||
</head>
|
||||
<body>
|
||||
@@ -243,3 +249,11 @@ export async function jumpToLocation(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createStylesLinkWithNonce(nonce: string, uri: Uri): string {
|
||||
return `<link nonce="${nonce}" rel="stylesheet" href="${uri}">`;
|
||||
}
|
||||
|
||||
function createStylesLinkWithoutNonce(uri: Uri): string {
|
||||
return `<link rel="stylesheet" href="${uri}">`;
|
||||
}
|
||||
|
||||
@@ -32,8 +32,8 @@ import {
|
||||
import { Logger } from './logging';
|
||||
import * as messages from './pure/messages';
|
||||
import { commandRunner } from './commandRunner';
|
||||
import { CompletedQuery, interpretResults } from './query-results';
|
||||
import { QueryInfo, tmpDir } from './run-queries';
|
||||
import { CompletedQueryInfo, interpretResults } from './query-results';
|
||||
import { QueryEvaluationInfo, tmpDir } from './run-queries';
|
||||
import { parseSarifLocation, parseSarifPlainTextMessage } from './pure/sarif-utils';
|
||||
import {
|
||||
WebviewReveal,
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
import { getDefaultResultSetName, ParsedResultSets } from './pure/interface-types';
|
||||
import { RawResultSet, transformBqrsResultSet, ResultSetSchema } from './pure/bqrs-cli-types';
|
||||
import { PAGE_SIZE } from './config';
|
||||
import { FullCompletedQueryInfo } from './query-results';
|
||||
|
||||
/**
|
||||
* interface.ts
|
||||
@@ -96,7 +97,7 @@ function numInterpretedPages(interpretation: Interpretation | undefined): number
|
||||
}
|
||||
|
||||
export class InterfaceManager extends DisposableObject {
|
||||
private _displayedQuery?: CompletedQuery;
|
||||
private _displayedQuery?: FullCompletedQueryInfo;
|
||||
private _interpretation?: Interpretation;
|
||||
private _panel: vscode.WebviewPanel | undefined;
|
||||
private _panelLoaded = false;
|
||||
@@ -176,14 +177,14 @@ export class InterfaceManager extends DisposableObject {
|
||||
}
|
||||
));
|
||||
|
||||
this._panel.onDidDispose(
|
||||
this.push(this._panel.onDidDispose(
|
||||
() => {
|
||||
this._panel = undefined;
|
||||
this._displayedQuery = undefined;
|
||||
},
|
||||
null,
|
||||
ctx.subscriptions
|
||||
);
|
||||
));
|
||||
const scriptPathOnDisk = vscode.Uri.file(
|
||||
ctx.asAbsolutePath('out/resultsView.js')
|
||||
);
|
||||
@@ -193,13 +194,14 @@ export class InterfaceManager extends DisposableObject {
|
||||
panel.webview.html = getHtmlForWebview(
|
||||
panel.webview,
|
||||
scriptPathOnDisk,
|
||||
[stylesheetPathOnDisk]
|
||||
[stylesheetPathOnDisk],
|
||||
false
|
||||
);
|
||||
panel.webview.onDidReceiveMessage(
|
||||
this.push(panel.webview.onDidReceiveMessage(
|
||||
async (e) => this.handleMsgFromView(e),
|
||||
undefined,
|
||||
ctx.subscriptions
|
||||
);
|
||||
));
|
||||
}
|
||||
return this._panel;
|
||||
}
|
||||
@@ -238,7 +240,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
}
|
||||
// Notify the webview that it should expect new results.
|
||||
await this.postMessage({ t: 'resultsUpdating' });
|
||||
await this._displayedQuery.updateInterpretedSortState(sortState);
|
||||
await this._displayedQuery.completedQuery.updateInterpretedSortState(sortState);
|
||||
await this.showResults(this._displayedQuery, WebviewReveal.NotForced, true);
|
||||
}
|
||||
|
||||
@@ -254,7 +256,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
}
|
||||
// Notify the webview that it should expect new results.
|
||||
await this.postMessage({ t: 'resultsUpdating' });
|
||||
await this._displayedQuery.updateSortState(
|
||||
await this._displayedQuery.completedQuery.updateSortState(
|
||||
this.cliServer,
|
||||
resultSetName,
|
||||
sortState
|
||||
@@ -314,7 +316,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
// sortedResultsInfo doesn't have an entry for the current
|
||||
// result set. Use this to determine whether or not we use
|
||||
// the sorted bqrs file.
|
||||
this._displayedQuery?.sortedResultsInfo.has(msg.selectedTable) || false
|
||||
!!this._displayedQuery?.completedQuery.sortedResultsInfo[msg.selectedTable]
|
||||
);
|
||||
}
|
||||
break;
|
||||
@@ -347,7 +349,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
|
||||
/**
|
||||
* Show query results in webview panel.
|
||||
* @param results Evaluation info for the executed query.
|
||||
* @param fullQuery Evaluation info for the executed query.
|
||||
* @param shouldKeepOldResultsWhileRendering Should keep old results while rendering.
|
||||
* @param forceReveal Force the webview panel to be visible and
|
||||
* Appropriate when the user has just performed an explicit
|
||||
@@ -355,27 +357,27 @@ export class InterfaceManager extends DisposableObject {
|
||||
* history entry.
|
||||
*/
|
||||
public async showResults(
|
||||
results: CompletedQuery,
|
||||
fullQuery: FullCompletedQueryInfo,
|
||||
forceReveal: WebviewReveal,
|
||||
shouldKeepOldResultsWhileRendering = false
|
||||
): Promise<void> {
|
||||
if (results.result.resultType !== messages.QueryResultType.SUCCESS) {
|
||||
if (fullQuery.completedQuery.result.resultType !== messages.QueryResultType.SUCCESS) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._interpretation = undefined;
|
||||
const interpretationPage = await this.interpretResultsInfo(
|
||||
results.query,
|
||||
results.interpretedResultsSortState
|
||||
fullQuery.completedQuery.query,
|
||||
fullQuery.completedQuery.interpretedResultsSortState
|
||||
);
|
||||
|
||||
const sortedResultsMap: SortedResultsMap = {};
|
||||
results.sortedResultsInfo.forEach(
|
||||
(v, k) =>
|
||||
Object.entries(fullQuery.completedQuery.sortedResultsInfo).forEach(
|
||||
([k, v]) =>
|
||||
(sortedResultsMap[k] = this.convertPathPropertiesToWebviewUris(v))
|
||||
);
|
||||
|
||||
this._displayedQuery = results;
|
||||
this._displayedQuery = fullQuery;
|
||||
|
||||
const panel = this.getPanel();
|
||||
await this.waitForPanelLoaded();
|
||||
@@ -388,7 +390,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
// more asynchronous message to not so abruptly interrupt
|
||||
// user's workflow by immediately revealing the panel.
|
||||
const showButton = 'View Results';
|
||||
const queryName = results.queryName;
|
||||
const queryName = fullQuery.getShortLabel();
|
||||
const resultPromise = vscode.window.showInformationMessage(
|
||||
`Finished running query ${queryName.length > 0 ? ` "${queryName}"` : ''
|
||||
}.`,
|
||||
@@ -407,7 +409,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
// Note that the resultSetSchemas will return offsets for the default (unsorted) page,
|
||||
// which may not be correct. However, in this case, it doesn't matter since we only
|
||||
// need the first offset, which will be the same no matter which sorting we use.
|
||||
const resultSetSchemas = await this.getResultSetSchemas(results);
|
||||
const resultSetSchemas = await this.getResultSetSchemas(fullQuery.completedQuery);
|
||||
const resultSetNames = resultSetSchemas.map(schema => schema.name);
|
||||
|
||||
const selectedTable = getDefaultResultSetName(resultSetNames);
|
||||
@@ -417,7 +419,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
|
||||
// Use sorted results path if it exists. This may happen if we are
|
||||
// reloading the results view after it has been sorted in the past.
|
||||
const resultsPath = results.getResultsPath(selectedTable);
|
||||
const resultsPath = fullQuery.completedQuery.getResultsPath(selectedTable);
|
||||
const pageSize = PAGE_SIZE.getValue<number>();
|
||||
const chunk = await this.cliServer.bqrsDecode(
|
||||
resultsPath,
|
||||
@@ -432,7 +434,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
}
|
||||
);
|
||||
const resultSet = transformBqrsResultSet(schema, chunk);
|
||||
results.setResultCount(interpretationPage?.numTotalResults || resultSet.schema.rows);
|
||||
fullQuery.completedQuery.setResultCount(interpretationPage?.numTotalResults || resultSet.schema.rows);
|
||||
const parsedResultSets: ParsedResultSets = {
|
||||
pageNumber: 0,
|
||||
pageSize,
|
||||
@@ -446,17 +448,17 @@ export class InterfaceManager extends DisposableObject {
|
||||
await this.postMessage({
|
||||
t: 'setState',
|
||||
interpretation: interpretationPage,
|
||||
origResultsPaths: results.query.resultsPaths,
|
||||
origResultsPaths: fullQuery.completedQuery.query.resultsPaths,
|
||||
resultsPath: this.convertPathToWebviewUri(
|
||||
results.query.resultsPaths.resultsPath
|
||||
fullQuery.completedQuery.query.resultsPaths.resultsPath
|
||||
),
|
||||
parsedResultSets,
|
||||
sortedResultsMap,
|
||||
database: results.database,
|
||||
database: fullQuery.initialInfo.databaseInfo,
|
||||
shouldKeepOldResultsWhileRendering,
|
||||
metadata: results.query.metadata,
|
||||
queryName: results.toString(),
|
||||
queryPath: results.query.program.queryPath
|
||||
metadata: fullQuery.completedQuery.query.metadata,
|
||||
queryName: fullQuery.label,
|
||||
queryPath: fullQuery.initialInfo.queryPath
|
||||
});
|
||||
}
|
||||
|
||||
@@ -476,25 +478,25 @@ export class InterfaceManager extends DisposableObject {
|
||||
throw new Error('Trying to show interpreted results but results were undefined');
|
||||
}
|
||||
|
||||
const resultSetSchemas = await this.getResultSetSchemas(this._displayedQuery);
|
||||
const resultSetSchemas = await this.getResultSetSchemas(this._displayedQuery.completedQuery);
|
||||
const resultSetNames = resultSetSchemas.map(schema => schema.name);
|
||||
|
||||
await this.postMessage({
|
||||
t: 'showInterpretedPage',
|
||||
interpretation: this.getPageOfInterpretedResults(pageNumber),
|
||||
database: this._displayedQuery.database,
|
||||
metadata: this._displayedQuery.query.metadata,
|
||||
database: this._displayedQuery.initialInfo.databaseInfo,
|
||||
metadata: this._displayedQuery.completedQuery.query.metadata,
|
||||
pageNumber,
|
||||
resultSetNames,
|
||||
pageSize: PAGE_SIZE.getValue(),
|
||||
numPages: numInterpretedPages(this._interpretation),
|
||||
queryName: this._displayedQuery.toString(),
|
||||
queryPath: this._displayedQuery.query.program.queryPath
|
||||
queryName: this._displayedQuery.label,
|
||||
queryPath: this._displayedQuery.initialInfo.queryPath
|
||||
});
|
||||
}
|
||||
|
||||
private async getResultSetSchemas(results: CompletedQuery, selectedTable = ''): Promise<ResultSetSchema[]> {
|
||||
const resultsPath = results.getResultsPath(selectedTable);
|
||||
private async getResultSetSchemas(completedQuery: CompletedQueryInfo, selectedTable = ''): Promise<ResultSetSchema[]> {
|
||||
const resultsPath = completedQuery.getResultsPath(selectedTable);
|
||||
const schemas = await this.cliServer.bqrsInfo(
|
||||
resultsPath,
|
||||
PAGE_SIZE.getValue()
|
||||
@@ -521,17 +523,17 @@ export class InterfaceManager extends DisposableObject {
|
||||
}
|
||||
|
||||
const sortedResultsMap: SortedResultsMap = {};
|
||||
results.sortedResultsInfo.forEach(
|
||||
(v, k) =>
|
||||
Object.entries(results.completedQuery.sortedResultsInfo).forEach(
|
||||
([k, v]) =>
|
||||
(sortedResultsMap[k] = this.convertPathPropertiesToWebviewUris(v))
|
||||
);
|
||||
|
||||
const resultSetSchemas = await this.getResultSetSchemas(results, sorted ? selectedTable : '');
|
||||
const resultSetSchemas = await this.getResultSetSchemas(results.completedQuery, sorted ? selectedTable : '');
|
||||
|
||||
// If there is a specific sorted table selected, a different bqrs file is loaded that doesn't have all the result set names.
|
||||
// Make sure that we load all result set names here.
|
||||
// See https://github.com/github/vscode-codeql/issues/1005
|
||||
const allResultSetSchemas = sorted ? await this.getResultSetSchemas(results, '') : resultSetSchemas;
|
||||
const allResultSetSchemas = sorted ? await this.getResultSetSchemas(results.completedQuery, '') : resultSetSchemas;
|
||||
const resultSetNames = allResultSetSchemas.map(schema => schema.name);
|
||||
|
||||
const schema = resultSetSchemas.find(
|
||||
@@ -542,7 +544,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
|
||||
const pageSize = PAGE_SIZE.getValue<number>();
|
||||
const chunk = await this.cliServer.bqrsDecode(
|
||||
results.getResultsPath(selectedTable, sorted),
|
||||
results.completedQuery.getResultsPath(selectedTable, sorted),
|
||||
schema.name,
|
||||
{
|
||||
offset: schema.pagination?.offsets[pageNumber],
|
||||
@@ -564,17 +566,17 @@ export class InterfaceManager extends DisposableObject {
|
||||
await this.postMessage({
|
||||
t: 'setState',
|
||||
interpretation: this._interpretation,
|
||||
origResultsPaths: results.query.resultsPaths,
|
||||
origResultsPaths: results.completedQuery.query.resultsPaths,
|
||||
resultsPath: this.convertPathToWebviewUri(
|
||||
results.query.resultsPaths.resultsPath
|
||||
results.completedQuery.query.resultsPaths.resultsPath
|
||||
),
|
||||
parsedResultSets,
|
||||
sortedResultsMap,
|
||||
database: results.database,
|
||||
database: results.initialInfo.databaseInfo,
|
||||
shouldKeepOldResultsWhileRendering: false,
|
||||
metadata: results.query.metadata,
|
||||
queryName: results.toString(),
|
||||
queryPath: results.query.program.queryPath
|
||||
metadata: results.completedQuery.query.metadata,
|
||||
queryName: results.label,
|
||||
queryPath: results.initialInfo.queryPath
|
||||
});
|
||||
}
|
||||
|
||||
@@ -643,18 +645,22 @@ export class InterfaceManager extends DisposableObject {
|
||||
}
|
||||
|
||||
private async interpretResultsInfo(
|
||||
query: QueryInfo,
|
||||
query: QueryEvaluationInfo,
|
||||
sortState: InterpretedResultsSortState | undefined
|
||||
): Promise<Interpretation | undefined> {
|
||||
if (
|
||||
(await query.canHaveInterpretedResults()) &&
|
||||
query.canHaveInterpretedResults() &&
|
||||
query.quickEvalPosition === undefined // never do results interpretation if quickEval
|
||||
) {
|
||||
try {
|
||||
const sourceLocationPrefix = await query.dbItem.getSourceLocationPrefix(
|
||||
const dbItem = this.databaseManager.findDatabaseItem(Uri.file(query.dbItemPath));
|
||||
if (!dbItem) {
|
||||
throw new Error(`Could not find database item for ${query.dbItemPath}`);
|
||||
}
|
||||
const sourceLocationPrefix = await dbItem.getSourceLocationPrefix(
|
||||
this.cliServer
|
||||
);
|
||||
const sourceArchiveUri = query.dbItem.sourceArchive;
|
||||
const sourceArchiveUri = dbItem.sourceArchive;
|
||||
const sourceInfo =
|
||||
sourceArchiveUri === undefined
|
||||
? undefined
|
||||
|
||||
@@ -74,31 +74,39 @@ export class OutputChannelLogger extends DisposableObject implements Logger {
|
||||
* continuing.
|
||||
*/
|
||||
async log(message: string, options = {} as LogOptions): Promise<void> {
|
||||
if (options.trailingNewline === undefined) {
|
||||
options.trailingNewline = true;
|
||||
}
|
||||
|
||||
if (options.trailingNewline) {
|
||||
this.outputChannel.appendLine(message);
|
||||
} else {
|
||||
this.outputChannel.append(message);
|
||||
}
|
||||
|
||||
if (this.additionalLogLocationPath && options.additionalLogLocation) {
|
||||
const logPath = path.join(this.additionalLogLocationPath, options.additionalLogLocation);
|
||||
let additional = this.additionalLocations.get(logPath);
|
||||
if (!additional) {
|
||||
const msg = `| Log being saved to ${logPath} |`;
|
||||
const separator = new Array(msg.length).fill('-').join('');
|
||||
this.outputChannel.appendLine(separator);
|
||||
this.outputChannel.appendLine(msg);
|
||||
this.outputChannel.appendLine(separator);
|
||||
additional = new AdditionalLogLocation(logPath, !this.isCustomLogDirectory);
|
||||
this.additionalLocations.set(logPath, additional);
|
||||
this.track(additional);
|
||||
try {
|
||||
if (options.trailingNewline === undefined) {
|
||||
options.trailingNewline = true;
|
||||
}
|
||||
if (options.trailingNewline) {
|
||||
this.outputChannel.appendLine(message);
|
||||
} else {
|
||||
this.outputChannel.append(message);
|
||||
}
|
||||
|
||||
await additional.log(message, options);
|
||||
if (this.additionalLogLocationPath && options.additionalLogLocation) {
|
||||
const logPath = path.join(this.additionalLogLocationPath, options.additionalLogLocation);
|
||||
let additional = this.additionalLocations.get(logPath);
|
||||
if (!additional) {
|
||||
const msg = `| Log being saved to ${logPath} |`;
|
||||
const separator = new Array(msg.length).fill('-').join('');
|
||||
this.outputChannel.appendLine(separator);
|
||||
this.outputChannel.appendLine(msg);
|
||||
this.outputChannel.appendLine(separator);
|
||||
additional = new AdditionalLogLocation(logPath, !this.isCustomLogDirectory);
|
||||
this.additionalLocations.set(logPath, additional);
|
||||
this.track(additional);
|
||||
}
|
||||
|
||||
await additional.log(message, options);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.message === 'Channel has been closed') {
|
||||
// Output channel is closed logging to console instead
|
||||
console.log('Output channel is closed logging to console instead:', message);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -316,7 +316,7 @@ export interface SetComparisonsMessage {
|
||||
readonly currentResultSetName: string;
|
||||
readonly rows: QueryCompareResult | undefined;
|
||||
readonly message: string | undefined;
|
||||
readonly datebaseUri: string;
|
||||
readonly databaseUri: string;
|
||||
}
|
||||
|
||||
export enum DiffKind {
|
||||
@@ -378,7 +378,8 @@ export type FromRemoteQueriesMessage =
|
||||
| OpenFileMsg
|
||||
| OpenVirtualFileMsg
|
||||
| RemoteQueryDownloadAnalysisResultsMessage
|
||||
| RemoteQueryDownloadAllAnalysesResultsMessage;
|
||||
| RemoteQueryDownloadAllAnalysesResultsMessage
|
||||
| RemoteQueryViewAnalysisResultsMessage;
|
||||
|
||||
export type ToRemoteQueriesMessage =
|
||||
| SetRemoteQueryResultMessage
|
||||
@@ -412,3 +413,8 @@ export interface RemoteQueryDownloadAllAnalysesResultsMessage {
|
||||
t: 'remoteQueryDownloadAllAnalysesResults';
|
||||
analysisSummaries: AnalysisSummary[];
|
||||
}
|
||||
|
||||
export interface RemoteQueryViewAnalysisResultsMessage {
|
||||
t: 'remoteQueryViewAnalysisResults';
|
||||
analysisSummary: AnalysisSummary
|
||||
}
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import { window as Window, env } from 'vscode';
|
||||
import { CompletedQuery } from './query-results';
|
||||
import {
|
||||
commands,
|
||||
env,
|
||||
Event,
|
||||
EventEmitter,
|
||||
ProviderResult,
|
||||
Range,
|
||||
ThemeIcon,
|
||||
TreeItem,
|
||||
TreeView,
|
||||
Uri,
|
||||
ViewColumn,
|
||||
window,
|
||||
workspace,
|
||||
} from 'vscode';
|
||||
import { QueryHistoryConfig } from './config';
|
||||
import { QueryWithResults } from './run-queries';
|
||||
import {
|
||||
showAndLogErrorMessage,
|
||||
showAndLogInformationMessage,
|
||||
@@ -16,6 +27,8 @@ import { QueryServerClient } from './queryserver-client';
|
||||
import { DisposableObject } from './pure/disposable-object';
|
||||
import { commandRunner } from './commandRunner';
|
||||
import { assertNever } from './pure/helpers-pure';
|
||||
import { FullCompletedQueryInfo, FullQueryInfo, QueryStatus } from './query-results';
|
||||
import { DatabaseManager } from './databases';
|
||||
|
||||
/**
|
||||
* query-history.ts
|
||||
@@ -26,12 +39,6 @@ import { assertNever } from './pure/helpers-pure';
|
||||
* `TreeDataProvider` subclass below.
|
||||
*/
|
||||
|
||||
export type QueryHistoryItemOptions = {
|
||||
label?: string; // user-settable label
|
||||
queryText?: string; // text of the selected file
|
||||
isQuickQuery?: boolean;
|
||||
};
|
||||
|
||||
export const SHOW_QUERY_TEXT_MSG = `\
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
// This is the text of the entire query file when it was executed for this query //
|
||||
@@ -59,7 +66,12 @@ const SHOW_QUERY_TEXT_QUICK_EVAL_MSG = `\
|
||||
*/
|
||||
const FAILED_QUERY_HISTORY_ITEM_ICON = 'media/red-x.svg';
|
||||
|
||||
enum SortOrder {
|
||||
/**
|
||||
* Path to icon to display next to a successful local run.
|
||||
*/
|
||||
const LOCAL_SUCCESS_QUERY_HISTORY_ITEM_ICON = 'media/drive.svg';
|
||||
|
||||
export enum SortOrder {
|
||||
NameAsc = 'NameAsc',
|
||||
NameDesc = 'NameDesc',
|
||||
DateAsc = 'DateAsc',
|
||||
@@ -74,19 +86,18 @@ enum SortOrder {
|
||||
export class HistoryTreeDataProvider extends DisposableObject {
|
||||
private _sortOrder = SortOrder.DateAsc;
|
||||
|
||||
private _onDidChangeTreeData = super.push(new vscode.EventEmitter<CompletedQuery | undefined>());
|
||||
private _onDidChangeTreeData = super.push(new EventEmitter<FullQueryInfo | undefined>());
|
||||
|
||||
readonly onDidChangeTreeData: vscode.Event<CompletedQuery | undefined> = this
|
||||
readonly onDidChangeTreeData: Event<FullQueryInfo | undefined> = this
|
||||
._onDidChangeTreeData.event;
|
||||
|
||||
private history: CompletedQuery[] = [];
|
||||
private history: FullQueryInfo[] = [];
|
||||
|
||||
private failedIconPath: string;
|
||||
|
||||
/**
|
||||
* When not undefined, must be reference-equal to an item in `this.databases`.
|
||||
*/
|
||||
private current: CompletedQuery | undefined;
|
||||
private localSuccessIconPath: string;
|
||||
|
||||
private current: FullQueryInfo | undefined;
|
||||
|
||||
constructor(extensionPath: string) {
|
||||
super();
|
||||
@@ -94,10 +105,14 @@ export class HistoryTreeDataProvider extends DisposableObject {
|
||||
extensionPath,
|
||||
FAILED_QUERY_HISTORY_ITEM_ICON
|
||||
);
|
||||
this.localSuccessIconPath = path.join(
|
||||
extensionPath,
|
||||
LOCAL_SUCCESS_QUERY_HISTORY_ITEM_ICON
|
||||
);
|
||||
}
|
||||
|
||||
async getTreeItem(element: CompletedQuery): Promise<vscode.TreeItem> {
|
||||
const treeItem = new vscode.TreeItem(element.toString());
|
||||
async getTreeItem(element: FullQueryInfo): Promise<TreeItem> {
|
||||
const treeItem = new TreeItem(element.label);
|
||||
|
||||
treeItem.command = {
|
||||
title: 'Query History Item',
|
||||
@@ -105,86 +120,105 @@ export class HistoryTreeDataProvider extends DisposableObject {
|
||||
arguments: [element],
|
||||
};
|
||||
|
||||
// Mark this query history item according to whether it has a
|
||||
// SARIF file so that we can make context menu items conditionally
|
||||
// available.
|
||||
const hasResults = await element.query.hasInterpretedResults();
|
||||
treeItem.contextValue = hasResults
|
||||
? 'interpretedResultsItem'
|
||||
: 'rawResultsItem';
|
||||
|
||||
if (!element.didRunSuccessfully) {
|
||||
treeItem.iconPath = this.failedIconPath;
|
||||
// Populate the icon and the context value. We use the context value to
|
||||
// control which commands are visible in the context menu.
|
||||
let hasResults;
|
||||
switch (element.status) {
|
||||
case QueryStatus.InProgress:
|
||||
treeItem.iconPath = new ThemeIcon('sync~spin');
|
||||
treeItem.contextValue = 'inProgressResultsItem';
|
||||
break;
|
||||
case QueryStatus.Completed:
|
||||
hasResults = await element.completedQuery?.query.hasInterpretedResults();
|
||||
treeItem.iconPath = this.localSuccessIconPath;
|
||||
treeItem.contextValue = hasResults
|
||||
? 'interpretedResultsItem'
|
||||
: 'rawResultsItem';
|
||||
break;
|
||||
case QueryStatus.Failed:
|
||||
treeItem.iconPath = this.failedIconPath;
|
||||
treeItem.contextValue = 'cancelledResultsItem';
|
||||
break;
|
||||
default:
|
||||
assertNever(element.status);
|
||||
}
|
||||
|
||||
return treeItem;
|
||||
}
|
||||
|
||||
getChildren(
|
||||
element?: CompletedQuery
|
||||
): vscode.ProviderResult<CompletedQuery[]> {
|
||||
return element ? [] : this.history.sort((q1, q2) => {
|
||||
element?: FullQueryInfo
|
||||
): ProviderResult<FullQueryInfo[]> {
|
||||
return element ? [] : this.history.sort((h1, h2) => {
|
||||
const resultCount1 = h1.completedQuery?.resultCount ?? -1;
|
||||
const resultCount2 = h2.completedQuery?.resultCount ?? -1;
|
||||
|
||||
switch (this.sortOrder) {
|
||||
case SortOrder.NameAsc:
|
||||
return q1.toString().localeCompare(q2.toString(), env.language);
|
||||
return h1.label.localeCompare(h2.label, env.language);
|
||||
case SortOrder.NameDesc:
|
||||
return q2.toString().localeCompare(q1.toString(), env.language);
|
||||
return h2.label.localeCompare(h1.label, env.language);
|
||||
case SortOrder.DateAsc:
|
||||
return q1.date.getTime() - q2.date.getTime();
|
||||
return h1.initialInfo.start.getTime() - h2.initialInfo.start.getTime();
|
||||
case SortOrder.DateDesc:
|
||||
return q2.date.getTime() - q1.date.getTime();
|
||||
return h2.initialInfo.start.getTime() - h1.initialInfo.start.getTime();
|
||||
case SortOrder.CountAsc:
|
||||
return q1.resultCount - q2.resultCount;
|
||||
// If the result counts are equal, sort by name.
|
||||
return resultCount1 - resultCount2 === 0
|
||||
? h1.label.localeCompare(h2.label, env.language)
|
||||
: resultCount1 - resultCount2;
|
||||
case SortOrder.CountDesc:
|
||||
return q2.resultCount - q1.resultCount;
|
||||
// If the result counts are equal, sort by name.
|
||||
return resultCount2 - resultCount1 === 0
|
||||
? h2.label.localeCompare(h1.label, env.language)
|
||||
: resultCount2 - resultCount1;
|
||||
default:
|
||||
assertNever(this.sortOrder);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getParent(_element: CompletedQuery): vscode.ProviderResult<CompletedQuery> {
|
||||
getParent(_element: FullQueryInfo): ProviderResult<FullQueryInfo> {
|
||||
return null;
|
||||
}
|
||||
|
||||
getCurrent(): CompletedQuery | undefined {
|
||||
getCurrent(): FullQueryInfo | undefined {
|
||||
return this.current;
|
||||
}
|
||||
|
||||
pushQuery(item: CompletedQuery): void {
|
||||
this.current = item;
|
||||
pushQuery(item: FullQueryInfo): void {
|
||||
this.history.push(item);
|
||||
this.setCurrentItem(item);
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
setCurrentItem(item: CompletedQuery) {
|
||||
setCurrentItem(item?: FullQueryInfo) {
|
||||
this.current = item;
|
||||
}
|
||||
|
||||
remove(item: CompletedQuery) {
|
||||
if (this.current === item) this.current = undefined;
|
||||
remove(item: FullQueryInfo) {
|
||||
const isCurrent = this.current === item;
|
||||
if (isCurrent) {
|
||||
this.setCurrentItem();
|
||||
}
|
||||
const index = this.history.findIndex((i) => i === item);
|
||||
if (index >= 0) {
|
||||
this.history.splice(index, 1);
|
||||
if (this.current === undefined && this.history.length > 0) {
|
||||
if (isCurrent && this.history.length > 0) {
|
||||
// Try to keep a current item, near the deleted item if there
|
||||
// are any available.
|
||||
this.current = this.history[Math.min(index, this.history.length - 1)];
|
||||
this.setCurrentItem(this.history[Math.min(index, this.history.length - 1)]);
|
||||
}
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
get allHistory(): CompletedQuery[] {
|
||||
get allHistory(): FullQueryInfo[] {
|
||||
return this.history;
|
||||
}
|
||||
|
||||
refresh(completedQuery?: CompletedQuery) {
|
||||
this._onDidChangeTreeData.fire(completedQuery);
|
||||
}
|
||||
|
||||
find(queryId: number): CompletedQuery | undefined {
|
||||
return this.allHistory.find((query) => query.query.queryID === queryId);
|
||||
refresh() {
|
||||
this._onDidChangeTreeData.fire(undefined);
|
||||
}
|
||||
|
||||
public get sortOrder() {
|
||||
@@ -204,33 +238,33 @@ export class HistoryTreeDataProvider extends DisposableObject {
|
||||
const DOUBLE_CLICK_TIME = 500;
|
||||
|
||||
const NO_QUERY_SELECTED = 'No query selected. Select a query history item you have already run and try again.';
|
||||
|
||||
export class QueryHistoryManager extends DisposableObject {
|
||||
treeDataProvider: HistoryTreeDataProvider;
|
||||
treeView: vscode.TreeView<CompletedQuery>;
|
||||
lastItemClick: { time: Date; item: CompletedQuery } | undefined;
|
||||
compareWithItem: CompletedQuery | undefined;
|
||||
treeView: TreeView<FullQueryInfo>;
|
||||
lastItemClick: { time: Date; item: FullQueryInfo } | undefined;
|
||||
compareWithItem: FullQueryInfo | undefined;
|
||||
|
||||
constructor(
|
||||
private qs: QueryServerClient,
|
||||
private dbm: DatabaseManager,
|
||||
extensionPath: string,
|
||||
private queryHistoryConfigListener: QueryHistoryConfig,
|
||||
private selectedCallback: (item: CompletedQuery) => Promise<void>,
|
||||
queryHistoryConfigListener: QueryHistoryConfig,
|
||||
private selectedCallback: (item: FullCompletedQueryInfo) => Promise<void>,
|
||||
private doCompareCallback: (
|
||||
from: CompletedQuery,
|
||||
to: CompletedQuery
|
||||
from: FullCompletedQueryInfo,
|
||||
to: FullCompletedQueryInfo
|
||||
) => Promise<void>
|
||||
) {
|
||||
super();
|
||||
|
||||
const treeDataProvider = (this.treeDataProvider = new HistoryTreeDataProvider(
|
||||
this.treeDataProvider = this.push(new HistoryTreeDataProvider(
|
||||
extensionPath
|
||||
));
|
||||
this.treeView = Window.createTreeView('codeQLQueryHistory', {
|
||||
treeDataProvider,
|
||||
this.treeView = this.push(window.createTreeView('codeQLQueryHistory', {
|
||||
treeDataProvider: this.treeDataProvider,
|
||||
canSelectMany: true,
|
||||
});
|
||||
this.push(this.treeView);
|
||||
this.push(treeDataProvider);
|
||||
}));
|
||||
|
||||
// Lazily update the tree view selection due to limitations of TreeView API (see
|
||||
// `updateTreeViewSelectionIfVisible` doc for details)
|
||||
@@ -239,11 +273,13 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
this.updateTreeViewSelectionIfVisible()
|
||||
)
|
||||
);
|
||||
// Don't allow the selection to become empty
|
||||
this.push(
|
||||
this.treeView.onDidChangeSelection(async (ev) => {
|
||||
if (ev.selection.length == 0) {
|
||||
if (ev.selection.length === 0) {
|
||||
// Don't allow the selection to become empty
|
||||
this.updateTreeViewSelectionIfVisible();
|
||||
} else {
|
||||
this.treeDataProvider.setCurrentItem(ev.selection[0]);
|
||||
}
|
||||
this.updateCompareWith(ev.selection);
|
||||
})
|
||||
@@ -298,6 +334,12 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
this.handleShowQueryLog.bind(this)
|
||||
)
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
'codeQLQueryHistory.cancel',
|
||||
this.handleCancel.bind(this)
|
||||
)
|
||||
);
|
||||
this.push(
|
||||
commandRunner(
|
||||
'codeQLQueryHistory.showQueryText',
|
||||
@@ -331,20 +373,22 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
this.push(
|
||||
commandRunner(
|
||||
'codeQLQueryHistory.itemClicked',
|
||||
async (item: CompletedQuery) => {
|
||||
async (item: FullQueryInfo) => {
|
||||
return this.handleItemClicked(item, [item]);
|
||||
}
|
||||
)
|
||||
);
|
||||
queryHistoryConfigListener.onDidChangeConfiguration(() => {
|
||||
this.treeDataProvider.refresh();
|
||||
});
|
||||
this.push(
|
||||
queryHistoryConfigListener.onDidChangeConfiguration(() => {
|
||||
this.treeDataProvider.refresh();
|
||||
})
|
||||
);
|
||||
|
||||
// displays query text in a read-only document
|
||||
vscode.workspace.registerTextDocumentContentProvider('codeql', {
|
||||
this.push(workspace.registerTextDocumentContentProvider('codeql', {
|
||||
provideTextDocumentContent(
|
||||
uri: vscode.Uri
|
||||
): vscode.ProviderResult<string> {
|
||||
uri: Uri
|
||||
): ProviderResult<string> {
|
||||
const params = new URLSearchParams(uri.query);
|
||||
|
||||
return (
|
||||
@@ -353,19 +397,19 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
: SHOW_QUERY_TEXT_MSG) + params.get('queryText')
|
||||
);
|
||||
},
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
async invokeCallbackOn(queryHistoryItem: CompletedQuery) {
|
||||
if (this.selectedCallback !== undefined) {
|
||||
async invokeCallbackOn(queryHistoryItem: FullQueryInfo) {
|
||||
if (this.selectedCallback && queryHistoryItem.isCompleted()) {
|
||||
const sc = this.selectedCallback;
|
||||
await sc(queryHistoryItem);
|
||||
await sc(queryHistoryItem as FullCompletedQueryInfo);
|
||||
}
|
||||
}
|
||||
|
||||
async handleOpenQuery(
|
||||
singleItem: CompletedQuery,
|
||||
multiSelect: CompletedQuery[]
|
||||
singleItem: FullQueryInfo,
|
||||
multiSelect: FullQueryInfo[]
|
||||
): Promise<void> {
|
||||
const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);
|
||||
if (!this.assertSingleQuery(finalMultiSelect)) {
|
||||
@@ -376,19 +420,19 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
throw new Error(NO_QUERY_SELECTED);
|
||||
}
|
||||
|
||||
const textDocument = await vscode.workspace.openTextDocument(
|
||||
vscode.Uri.file(finalSingleItem.query.program.queryPath)
|
||||
const textDocument = await workspace.openTextDocument(
|
||||
Uri.file(finalSingleItem.initialInfo.queryPath)
|
||||
);
|
||||
const editor = await vscode.window.showTextDocument(
|
||||
const editor = await window.showTextDocument(
|
||||
textDocument,
|
||||
vscode.ViewColumn.One
|
||||
ViewColumn.One
|
||||
);
|
||||
const queryText = finalSingleItem.options.queryText;
|
||||
if (queryText !== undefined && finalSingleItem.options.isQuickQuery) {
|
||||
const queryText = finalSingleItem.initialInfo.queryText;
|
||||
if (queryText !== undefined && finalSingleItem.initialInfo.isQuickQuery) {
|
||||
await editor.edit((edit) =>
|
||||
edit.replace(
|
||||
textDocument.validateRange(
|
||||
new vscode.Range(0, 0, textDocument.lineCount, 0)
|
||||
new Range(0, 0, textDocument.lineCount, 0)
|
||||
),
|
||||
queryText
|
||||
)
|
||||
@@ -397,18 +441,21 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
}
|
||||
|
||||
async handleRemoveHistoryItem(
|
||||
singleItem: CompletedQuery,
|
||||
multiSelect: CompletedQuery[]
|
||||
singleItem: FullQueryInfo,
|
||||
multiSelect: FullQueryInfo[]
|
||||
) {
|
||||
const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);
|
||||
|
||||
(finalMultiSelect || [finalSingleItem]).forEach((item) => {
|
||||
this.treeDataProvider.remove(item);
|
||||
item.dispose();
|
||||
// Removing in progress queries is not supported yet
|
||||
if (item.status !== QueryStatus.InProgress) {
|
||||
this.treeDataProvider.remove(item);
|
||||
item.completedQuery?.dispose();
|
||||
}
|
||||
});
|
||||
const current = this.treeDataProvider.getCurrent();
|
||||
if (current !== undefined) {
|
||||
await this.treeView.reveal(current);
|
||||
await this.treeView.reveal(current, { select: true });
|
||||
await this.invokeCallbackOn(current);
|
||||
}
|
||||
}
|
||||
@@ -438,45 +485,40 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
}
|
||||
|
||||
async handleSetLabel(
|
||||
singleItem: CompletedQuery,
|
||||
multiSelect: CompletedQuery[]
|
||||
singleItem: FullQueryInfo,
|
||||
multiSelect: FullQueryInfo[]
|
||||
): Promise<void> {
|
||||
if (!this.assertSingleQuery(multiSelect)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await vscode.window.showInputBox({
|
||||
const response = await window.showInputBox({
|
||||
prompt: 'Label:',
|
||||
placeHolder: '(use default)',
|
||||
value: singleItem.getLabel(),
|
||||
value: singleItem.label,
|
||||
});
|
||||
// undefined response means the user cancelled the dialog; don't change anything
|
||||
if (response !== undefined) {
|
||||
// Interpret empty string response as 'go back to using default'
|
||||
singleItem.options.label = response === '' ? undefined : response;
|
||||
if (this.treeDataProvider.sortOrder === SortOrder.NameAsc ||
|
||||
this.treeDataProvider.sortOrder === SortOrder.NameDesc) {
|
||||
this.treeDataProvider.refresh();
|
||||
} else {
|
||||
this.treeDataProvider.refresh(singleItem);
|
||||
}
|
||||
singleItem.initialInfo.userSpecifiedLabel = response === '' ? undefined : response;
|
||||
this.treeDataProvider.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
async handleCompareWith(
|
||||
singleItem: CompletedQuery,
|
||||
multiSelect: CompletedQuery[]
|
||||
singleItem: FullQueryInfo,
|
||||
multiSelect: FullQueryInfo[]
|
||||
) {
|
||||
try {
|
||||
if (!singleItem.didRunSuccessfully) {
|
||||
if (!singleItem.completedQuery?.didRunSuccessfully) {
|
||||
throw new Error('Please select a successful query.');
|
||||
}
|
||||
|
||||
const from = this.compareWithItem || singleItem;
|
||||
const to = await this.findOtherQueryToCompare(from, multiSelect);
|
||||
|
||||
if (from && to) {
|
||||
await this.doCompareCallback(from, to);
|
||||
if (from.isCompleted() && to?.isCompleted()) {
|
||||
await this.doCompareCallback(from as FullCompletedQueryInfo, to as FullCompletedQueryInfo);
|
||||
}
|
||||
} catch (e) {
|
||||
void showAndLogErrorMessage(e.message);
|
||||
@@ -484,8 +526,8 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
}
|
||||
|
||||
async handleItemClicked(
|
||||
singleItem: CompletedQuery,
|
||||
multiSelect: CompletedQuery[]
|
||||
singleItem: FullQueryInfo,
|
||||
multiSelect: FullQueryInfo[]
|
||||
) {
|
||||
const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);
|
||||
if (!this.assertSingleQuery(finalMultiSelect)) {
|
||||
@@ -516,23 +558,40 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
}
|
||||
|
||||
async handleShowQueryLog(
|
||||
singleItem: CompletedQuery,
|
||||
multiSelect: CompletedQuery[]
|
||||
singleItem: FullQueryInfo,
|
||||
multiSelect: FullQueryInfo[]
|
||||
) {
|
||||
if (!this.assertSingleQuery(multiSelect)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (singleItem.logFileLocation) {
|
||||
await this.tryOpenExternalFile(singleItem.logFileLocation);
|
||||
if (!singleItem.completedQuery) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (singleItem.completedQuery.logFileLocation) {
|
||||
await this.tryOpenExternalFile(singleItem.completedQuery.logFileLocation);
|
||||
} else {
|
||||
void showAndLogWarningMessage('No log file available');
|
||||
}
|
||||
}
|
||||
|
||||
async handleCancel(
|
||||
singleItem: FullQueryInfo,
|
||||
multiSelect: FullQueryInfo[]
|
||||
) {
|
||||
const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);
|
||||
|
||||
(finalMultiSelect || [finalSingleItem]).forEach((item) => {
|
||||
if (item.status === QueryStatus.InProgress) {
|
||||
item.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async handleShowQueryText(
|
||||
singleItem: CompletedQuery,
|
||||
multiSelect: CompletedQuery[]
|
||||
singleItem: FullQueryInfo,
|
||||
multiSelect: FullQueryInfo[]
|
||||
) {
|
||||
if (!this.assertSingleQuery(multiSelect)) {
|
||||
return;
|
||||
@@ -542,35 +601,32 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
throw new Error(NO_QUERY_SELECTED);
|
||||
}
|
||||
|
||||
const queryName = singleItem.queryName.endsWith('.ql')
|
||||
? singleItem.queryName
|
||||
: singleItem.queryName + '.ql';
|
||||
const params = new URLSearchParams({
|
||||
isQuickEval: String(!!singleItem.query.quickEvalPosition),
|
||||
isQuickEval: String(!!singleItem.initialInfo.quickEvalPosition),
|
||||
queryText: encodeURIComponent(await this.getQueryText(singleItem)),
|
||||
});
|
||||
const uri = vscode.Uri.parse(
|
||||
`codeql:${singleItem.query.queryID}-${queryName}?${params.toString()}`, true
|
||||
const uri = Uri.parse(
|
||||
`codeql:${singleItem.initialInfo.id}?${params.toString()}`, true
|
||||
);
|
||||
const doc = await vscode.workspace.openTextDocument(uri);
|
||||
await vscode.window.showTextDocument(doc, { preview: false });
|
||||
const doc = await workspace.openTextDocument(uri);
|
||||
await window.showTextDocument(doc, { preview: false });
|
||||
}
|
||||
|
||||
async handleViewSarifAlerts(
|
||||
singleItem: CompletedQuery,
|
||||
multiSelect: CompletedQuery[]
|
||||
singleItem: FullQueryInfo,
|
||||
multiSelect: FullQueryInfo[]
|
||||
) {
|
||||
if (!this.assertSingleQuery(multiSelect)) {
|
||||
if (!this.assertSingleQuery(multiSelect) || !singleItem.completedQuery) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasInterpretedResults = await singleItem.query.canHaveInterpretedResults();
|
||||
const query = singleItem.completedQuery.query;
|
||||
const hasInterpretedResults = query.canHaveInterpretedResults();
|
||||
if (hasInterpretedResults) {
|
||||
await this.tryOpenExternalFile(
|
||||
singleItem.query.resultsPaths.interpretedResultsPath
|
||||
query.resultsPaths.interpretedResultsPath
|
||||
);
|
||||
} else {
|
||||
const label = singleItem.getLabel();
|
||||
const label = singleItem.label;
|
||||
void showAndLogInformationMessage(
|
||||
`Query ${label} has no interpreted results.`
|
||||
);
|
||||
@@ -578,81 +634,65 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
}
|
||||
|
||||
async handleViewCsvResults(
|
||||
singleItem: CompletedQuery,
|
||||
multiSelect: CompletedQuery[]
|
||||
singleItem: FullQueryInfo,
|
||||
multiSelect: FullQueryInfo[]
|
||||
) {
|
||||
if (!this.assertSingleQuery(multiSelect)) {
|
||||
return;
|
||||
}
|
||||
if (await singleItem.query.hasCsv()) {
|
||||
void this.tryOpenExternalFile(singleItem.query.csvPath);
|
||||
if (!singleItem.completedQuery) {
|
||||
return;
|
||||
}
|
||||
await singleItem.query.exportCsvResults(this.qs, singleItem.query.csvPath, () => {
|
||||
const query = singleItem.completedQuery.query;
|
||||
if (await query.hasCsv()) {
|
||||
void this.tryOpenExternalFile(query.csvPath);
|
||||
return;
|
||||
}
|
||||
await query.exportCsvResults(this.qs, query.csvPath, () => {
|
||||
void this.tryOpenExternalFile(
|
||||
singleItem.query.csvPath
|
||||
query.csvPath
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async handleViewCsvAlerts(
|
||||
singleItem: CompletedQuery,
|
||||
multiSelect: CompletedQuery[]
|
||||
singleItem: FullQueryInfo,
|
||||
multiSelect: FullQueryInfo[]
|
||||
) {
|
||||
if (!this.assertSingleQuery(multiSelect)) {
|
||||
if (!this.assertSingleQuery(multiSelect) || !singleItem.completedQuery) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.tryOpenExternalFile(
|
||||
await singleItem.query.ensureCsvProduced(this.qs)
|
||||
await singleItem.completedQuery.query.ensureCsvProduced(this.qs, this.dbm)
|
||||
);
|
||||
}
|
||||
|
||||
async handleViewDil(
|
||||
singleItem: CompletedQuery,
|
||||
multiSelect: CompletedQuery[],
|
||||
singleItem: FullQueryInfo,
|
||||
multiSelect: FullQueryInfo[],
|
||||
) {
|
||||
if (!this.assertSingleQuery(multiSelect)) {
|
||||
return;
|
||||
}
|
||||
if (!singleItem.completedQuery) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.tryOpenExternalFile(
|
||||
await singleItem.query.ensureDilPath(this.qs)
|
||||
await singleItem.completedQuery.query.ensureDilPath(this.qs)
|
||||
);
|
||||
}
|
||||
|
||||
async getQueryText(queryHistoryItem: CompletedQuery): Promise<string> {
|
||||
if (queryHistoryItem.options.queryText) {
|
||||
return queryHistoryItem.options.queryText;
|
||||
} else if (queryHistoryItem.query.quickEvalPosition) {
|
||||
// capture all selected lines
|
||||
const startLine = queryHistoryItem.query.quickEvalPosition.line;
|
||||
const endLine = queryHistoryItem.query.quickEvalPosition.endLine;
|
||||
const textDocument = await vscode.workspace.openTextDocument(
|
||||
queryHistoryItem.query.quickEvalPosition.fileName
|
||||
);
|
||||
return textDocument.getText(
|
||||
new vscode.Range(startLine - 1, 0, endLine, 0)
|
||||
);
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
async getQueryText(queryHistoryItem: FullQueryInfo): Promise<string> {
|
||||
return queryHistoryItem.initialInfo.queryText;
|
||||
}
|
||||
|
||||
buildCompletedQuery(info: QueryWithResults): CompletedQuery {
|
||||
const item = new CompletedQuery(info, this.queryHistoryConfigListener);
|
||||
return item;
|
||||
}
|
||||
|
||||
addCompletedQuery(item: CompletedQuery) {
|
||||
addQuery(item: FullQueryInfo) {
|
||||
this.treeDataProvider.pushQuery(item);
|
||||
this.updateTreeViewSelectionIfVisible();
|
||||
}
|
||||
|
||||
find(queryId: number): CompletedQuery | undefined {
|
||||
return this.treeDataProvider.find(queryId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the tree view selection if the tree view is visible.
|
||||
*
|
||||
@@ -668,15 +708,15 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
// We must fire the onDidChangeTreeData event to ensure the current element can be selected
|
||||
// using `reveal` if the tree view was not visible when the current element was added.
|
||||
this.treeDataProvider.refresh();
|
||||
void this.treeView.reveal(current);
|
||||
void this.treeView.reveal(current, { select: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async tryOpenExternalFile(fileLocation: string) {
|
||||
const uri = vscode.Uri.file(fileLocation);
|
||||
const uri = Uri.file(fileLocation);
|
||||
try {
|
||||
await vscode.window.showTextDocument(uri, { preview: false });
|
||||
await window.showTextDocument(uri, { preview: false });
|
||||
} catch (e) {
|
||||
if (
|
||||
e.message.includes(
|
||||
@@ -693,7 +733,7 @@ the file in the file explorer and dragging it into the workspace.`
|
||||
);
|
||||
if (res) {
|
||||
try {
|
||||
await vscode.commands.executeCommand('revealFileInOS', uri);
|
||||
await commands.executeCommand('revealFileInOS', uri);
|
||||
} catch (e) {
|
||||
void showAndLogErrorMessage(e.message);
|
||||
}
|
||||
@@ -707,20 +747,26 @@ the file in the file explorer and dragging it into the workspace.`
|
||||
}
|
||||
|
||||
private async findOtherQueryToCompare(
|
||||
singleItem: CompletedQuery,
|
||||
multiSelect: CompletedQuery[]
|
||||
): Promise<CompletedQuery | undefined> {
|
||||
const dbName = singleItem.database.name;
|
||||
singleItem: FullQueryInfo,
|
||||
multiSelect: FullQueryInfo[]
|
||||
): Promise<FullQueryInfo | undefined> {
|
||||
if (!singleItem.completedQuery) {
|
||||
return undefined;
|
||||
}
|
||||
const dbName = singleItem.initialInfo.databaseInfo.name;
|
||||
|
||||
// if exactly 2 queries are selected, use those
|
||||
if (multiSelect?.length === 2) {
|
||||
// return the query that is not the first selected one
|
||||
const otherQuery =
|
||||
singleItem === multiSelect[0] ? multiSelect[1] : multiSelect[0];
|
||||
if (!otherQuery.didRunSuccessfully) {
|
||||
if (!otherQuery.completedQuery) {
|
||||
throw new Error('Please select a completed query.');
|
||||
}
|
||||
if (!otherQuery.completedQuery.didRunSuccessfully) {
|
||||
throw new Error('Please select a successful query.');
|
||||
}
|
||||
if (otherQuery.database.name !== dbName) {
|
||||
if (otherQuery.initialInfo.databaseInfo.name !== dbName) {
|
||||
throw new Error('Query databases must be the same.');
|
||||
}
|
||||
return otherQuery;
|
||||
@@ -735,23 +781,24 @@ the file in the file explorer and dragging it into the workspace.`
|
||||
.filter(
|
||||
(otherQuery) =>
|
||||
otherQuery !== singleItem &&
|
||||
otherQuery.didRunSuccessfully &&
|
||||
otherQuery.database.name === dbName
|
||||
otherQuery.completedQuery &&
|
||||
otherQuery.completedQuery.didRunSuccessfully &&
|
||||
otherQuery.initialInfo.databaseInfo.name === dbName
|
||||
)
|
||||
.map((otherQuery) => ({
|
||||
label: otherQuery.toString(),
|
||||
description: otherQuery.databaseName,
|
||||
detail: otherQuery.statusString,
|
||||
query: otherQuery,
|
||||
.map((item) => ({
|
||||
label: item.label,
|
||||
description: item.initialInfo.databaseInfo.name,
|
||||
detail: item.completedQuery!.statusString,
|
||||
query: item,
|
||||
}));
|
||||
if (comparableQueryLabels.length < 1) {
|
||||
throw new Error('No other queries available to compare with.');
|
||||
}
|
||||
const choice = await vscode.window.showQuickPick(comparableQueryLabels);
|
||||
const choice = await window.showQuickPick(comparableQueryLabels);
|
||||
return choice?.query;
|
||||
}
|
||||
|
||||
private assertSingleQuery(multiSelect: CompletedQuery[] = [], message = 'Please select a single query.') {
|
||||
private assertSingleQuery(multiSelect: FullQueryInfo[] = [], message = 'Please select a single query.') {
|
||||
if (multiSelect.length > 1) {
|
||||
void showAndLogErrorMessage(
|
||||
message
|
||||
@@ -778,7 +825,7 @@ the file in the file explorer and dragging it into the workspace.`
|
||||
*
|
||||
* @param newSelection the new selection after the most recent selection change
|
||||
*/
|
||||
private updateCompareWith(newSelection: CompletedQuery[]) {
|
||||
private updateCompareWith(newSelection: FullQueryInfo[]) {
|
||||
if (newSelection.length === 1) {
|
||||
this.compareWithItem = newSelection[0];
|
||||
} else if (
|
||||
@@ -792,6 +839,9 @@ the file in the file explorer and dragging it into the workspace.`
|
||||
|
||||
/**
|
||||
* If no items are selected, attempt to grab the selection from the treeview.
|
||||
* However, often the treeview itself does not have any selection. In this case,
|
||||
* grab the selection from the `treeDataProvider` current item.
|
||||
*
|
||||
* We need to use this method because when clicking on commands from the view title
|
||||
* bar, the selections are not passed in.
|
||||
*
|
||||
@@ -799,25 +849,38 @@ the file in the file explorer and dragging it into the workspace.`
|
||||
* @param multiSelect a multi-select or undefined if no items are selected
|
||||
*/
|
||||
private determineSelection(
|
||||
singleItem: CompletedQuery,
|
||||
multiSelect: CompletedQuery[]
|
||||
): { finalSingleItem: CompletedQuery; finalMultiSelect: CompletedQuery[] } {
|
||||
if (singleItem === undefined && (multiSelect === undefined || multiSelect.length === 0 || multiSelect[0] === undefined)) {
|
||||
singleItem: FullQueryInfo,
|
||||
multiSelect: FullQueryInfo[]
|
||||
): { finalSingleItem: FullQueryInfo; finalMultiSelect: FullQueryInfo[] } {
|
||||
if (!singleItem && !multiSelect?.[0]) {
|
||||
const selection = this.treeView.selection;
|
||||
if (selection) {
|
||||
const current = this.treeDataProvider.getCurrent();
|
||||
if (selection?.length) {
|
||||
return {
|
||||
finalSingleItem: selection[0],
|
||||
finalMultiSelect: selection
|
||||
};
|
||||
} else if (current) {
|
||||
return {
|
||||
finalSingleItem: current,
|
||||
finalMultiSelect: [current]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ensure we do not return undefined
|
||||
if (singleItem && !multiSelect?.[0]) {
|
||||
multiSelect = [singleItem];
|
||||
} else if (!singleItem && multiSelect?.[0]) {
|
||||
singleItem = multiSelect[0];
|
||||
}
|
||||
return {
|
||||
finalSingleItem: singleItem,
|
||||
finalMultiSelect: multiSelect
|
||||
};
|
||||
}
|
||||
|
||||
async refreshTreeView(completedQuery: CompletedQuery): Promise<void> {
|
||||
this.treeDataProvider.refresh(completedQuery);
|
||||
refreshTreeView(): void {
|
||||
this.treeDataProvider.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,59 @@
|
||||
import { env } from 'vscode';
|
||||
import { CancellationTokenSource, env } from 'vscode';
|
||||
|
||||
import { QueryWithResults, tmpDir, QueryInfo } from './run-queries';
|
||||
import { QueryWithResults, QueryEvaluationInfo } from './run-queries';
|
||||
import * as messages from './pure/messages';
|
||||
import * as cli from './cli';
|
||||
import * as sarif from 'sarif';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import { RawResultsSortState, SortedResultSetInfo, DatabaseInfo, QueryMetadata, InterpretedResultsSortState, ResultsPaths } from './pure/interface-types';
|
||||
import {
|
||||
RawResultsSortState,
|
||||
SortedResultSetInfo,
|
||||
QueryMetadata,
|
||||
InterpretedResultsSortState,
|
||||
ResultsPaths
|
||||
} from './pure/interface-types';
|
||||
import { QueryHistoryConfig } from './config';
|
||||
import { QueryHistoryItemOptions } from './query-history';
|
||||
import { DatabaseInfo } from './pure/interface-types';
|
||||
import { showAndLogErrorMessage } from './helpers';
|
||||
|
||||
export class CompletedQuery implements QueryWithResults {
|
||||
readonly date: Date;
|
||||
readonly time: string;
|
||||
readonly query: QueryInfo;
|
||||
/**
|
||||
* A description of the information about a query
|
||||
* that is available before results are populated.
|
||||
*/
|
||||
export interface InitialQueryInfo {
|
||||
userSpecifiedLabel?: string; // if missing, use a default label
|
||||
readonly queryText: string; // text of the selected file, or the selected text when doing quick eval
|
||||
readonly isQuickQuery: boolean;
|
||||
readonly isQuickEval: boolean;
|
||||
readonly quickEvalPosition?: messages.Position;
|
||||
readonly queryPath: string;
|
||||
readonly databaseInfo: DatabaseInfo
|
||||
readonly start: Date;
|
||||
readonly id: string; // unique id for this query.
|
||||
}
|
||||
|
||||
export enum QueryStatus {
|
||||
InProgress = 'InProgress',
|
||||
Completed = 'Completed',
|
||||
Failed = 'Failed',
|
||||
}
|
||||
|
||||
export class CompletedQueryInfo implements QueryWithResults {
|
||||
readonly query: QueryEvaluationInfo;
|
||||
readonly result: messages.EvaluationResult;
|
||||
readonly database: DatabaseInfo;
|
||||
readonly logFileLocation?: string;
|
||||
options: QueryHistoryItemOptions;
|
||||
resultCount: number;
|
||||
|
||||
/**
|
||||
* This dispose method is called when the query is removed from the history view.
|
||||
*/
|
||||
dispose: () => void;
|
||||
|
||||
/**
|
||||
* Map from result set name to SortedResultSetInfo.
|
||||
*/
|
||||
sortedResultsInfo: Map<string, SortedResultSetInfo>;
|
||||
sortedResultsInfo: Record<string, SortedResultSetInfo>;
|
||||
|
||||
/**
|
||||
* How we're currently sorting alerts. This is not mere interface
|
||||
@@ -35,20 +64,23 @@ export class CompletedQuery implements QueryWithResults {
|
||||
*/
|
||||
interpretedResultsSortState: InterpretedResultsSortState | undefined;
|
||||
|
||||
/**
|
||||
* Note that in the {@link FullQueryInfo.slurp} method, we create a CompletedQueryInfo instance
|
||||
* by explicitly setting the prototype in order to avoid calling this constructor.
|
||||
*/
|
||||
constructor(
|
||||
evaluation: QueryWithResults,
|
||||
public config: QueryHistoryConfig,
|
||||
) {
|
||||
this.query = evaluation.query;
|
||||
this.result = evaluation.result;
|
||||
this.database = evaluation.database;
|
||||
this.logFileLocation = evaluation.logFileLocation;
|
||||
this.options = evaluation.options;
|
||||
|
||||
// Use the dispose method from the evaluation.
|
||||
// The dispose will clean up any additional log locations that this
|
||||
// query may have created.
|
||||
this.dispose = evaluation.dispose;
|
||||
|
||||
this.date = new Date();
|
||||
this.time = this.date.toLocaleString(env.language);
|
||||
this.sortedResultsInfo = new Map();
|
||||
this.sortedResultsInfo = {};
|
||||
this.resultCount = 0;
|
||||
}
|
||||
|
||||
@@ -56,26 +88,16 @@ export class CompletedQuery implements QueryWithResults {
|
||||
this.resultCount = value;
|
||||
}
|
||||
|
||||
get databaseName(): string {
|
||||
return this.database.name;
|
||||
}
|
||||
get queryName(): string {
|
||||
return getQueryName(this.query);
|
||||
}
|
||||
get queryFileName(): string {
|
||||
return getQueryFileName(this.query);
|
||||
}
|
||||
|
||||
get statusString(): string {
|
||||
switch (this.result.resultType) {
|
||||
case messages.QueryResultType.CANCELLATION:
|
||||
return `cancelled after ${this.result.evaluationTime / 1000} seconds`;
|
||||
return `cancelled after ${Math.round(this.result.evaluationTime / 1000)} seconds`;
|
||||
case messages.QueryResultType.OOM:
|
||||
return 'out of memory';
|
||||
case messages.QueryResultType.SUCCESS:
|
||||
return `finished in ${this.result.evaluationTime / 1000} seconds`;
|
||||
return `finished in ${Math.round(this.result.evaluationTime / 1000)} seconds`;
|
||||
case messages.QueryResultType.TIMEOUT:
|
||||
return `timed out after ${this.result.evaluationTime / 1000} seconds`;
|
||||
return `timed out after ${Math.round(this.result.evaluationTime / 1000)} seconds`;
|
||||
case messages.QueryResultType.OTHER_ERROR:
|
||||
default:
|
||||
return this.result.message ? `failed: ${this.result.message}` : 'failed';
|
||||
@@ -86,52 +108,26 @@ export class CompletedQuery implements QueryWithResults {
|
||||
if (!useSorted) {
|
||||
return this.query.resultsPaths.resultsPath;
|
||||
}
|
||||
return this.sortedResultsInfo.get(selectedTable)?.resultsPath
|
||||
return this.sortedResultsInfo[selectedTable]?.resultsPath
|
||||
|| this.query.resultsPaths.resultsPath;
|
||||
}
|
||||
|
||||
interpolate(template: string): string {
|
||||
const { databaseName, queryName, time, resultCount, statusString, queryFileName } = this;
|
||||
const replacements: { [k: string]: string } = {
|
||||
t: time,
|
||||
q: queryName,
|
||||
d: databaseName,
|
||||
r: resultCount.toString(),
|
||||
s: statusString,
|
||||
f: queryFileName,
|
||||
'%': '%',
|
||||
};
|
||||
return template.replace(/%(.)/g, (match, key) => {
|
||||
const replacement = replacements[key];
|
||||
return replacement !== undefined ? replacement : match;
|
||||
});
|
||||
}
|
||||
|
||||
getLabel(): string {
|
||||
return this.options?.label
|
||||
|| this.config.format;
|
||||
}
|
||||
|
||||
get didRunSuccessfully(): boolean {
|
||||
return this.result.resultType === messages.QueryResultType.SUCCESS;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.interpolate(this.getLabel());
|
||||
}
|
||||
|
||||
async updateSortState(
|
||||
server: cli.CodeQLCliServer,
|
||||
resultSetName: string,
|
||||
sortState?: RawResultsSortState
|
||||
): Promise<void> {
|
||||
if (sortState === undefined) {
|
||||
this.sortedResultsInfo.delete(resultSetName);
|
||||
delete this.sortedResultsInfo[resultSetName];
|
||||
return;
|
||||
}
|
||||
|
||||
const sortedResultSetInfo: SortedResultSetInfo = {
|
||||
resultsPath: path.join(tmpDir.name, `sortedResults${this.query.queryID}-${resultSetName}.bqrs`),
|
||||
resultsPath: this.query.getSortedResultSetPath(resultSetName),
|
||||
sortState
|
||||
};
|
||||
|
||||
@@ -142,7 +138,7 @@ export class CompletedQuery implements QueryWithResults {
|
||||
[sortState.columnIndex],
|
||||
[sortState.sortDirection]
|
||||
);
|
||||
this.sortedResultsInfo.set(resultSetName, sortedResultSetInfo);
|
||||
this.sortedResultsInfo[resultSetName] = sortedResultSetInfo;
|
||||
}
|
||||
|
||||
async updateInterpretedSortState(sortState?: InterpretedResultsSortState): Promise<void> {
|
||||
@@ -151,36 +147,6 @@ export class CompletedQuery implements QueryWithResults {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets a human-readable name for an evaluated query.
|
||||
* Uses metadata if it exists, and defaults to the query file name.
|
||||
*/
|
||||
export function getQueryName(query: QueryInfo) {
|
||||
if (query.quickEvalPosition !== undefined) {
|
||||
return 'Quick evaluation of ' + getQueryFileName(query);
|
||||
} else if (query.metadata?.name) {
|
||||
return query.metadata.name;
|
||||
} else {
|
||||
return getQueryFileName(query);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the file name for an evaluated query.
|
||||
* Defaults to the query file name and may contain position information for quick eval queries.
|
||||
*/
|
||||
export function getQueryFileName(query: QueryInfo) {
|
||||
// Queries run through quick evaluation are not usually the entire query file.
|
||||
// Label them differently and include the line numbers.
|
||||
if (query.quickEvalPosition !== undefined) {
|
||||
const { line, endLine, fileName } = query.quickEvalPosition;
|
||||
const lineInfo = line === endLine ? `${line}` : `${line}-${endLine}`;
|
||||
return `${path.basename(fileName)}:${lineInfo}`;
|
||||
}
|
||||
return path.basename(query.program.queryPath);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Call cli command to interpret results.
|
||||
*/
|
||||
@@ -211,3 +177,205 @@ export function ensureMetadataIsComplete(metadata: QueryMetadata | undefined) {
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Used in Interface and Compare-Interface for queries that we know have been complated.
|
||||
*/
|
||||
export type FullCompletedQueryInfo = FullQueryInfo & {
|
||||
completedQuery: CompletedQueryInfo
|
||||
};
|
||||
|
||||
export class FullQueryInfo {
|
||||
|
||||
static async slurp(fsPath: string, config: QueryHistoryConfig): Promise<FullQueryInfo[]> {
|
||||
try {
|
||||
const data = await fs.readFile(fsPath, 'utf8');
|
||||
const queries = JSON.parse(data);
|
||||
return queries.map((q: FullQueryInfo) => {
|
||||
|
||||
// Need to explicitly set prototype since reading in from JSON will not
|
||||
// do this automatically. Note that we can't call the constructor here since
|
||||
// the constructor invokes extra logic that we don't want to do.
|
||||
Object.setPrototypeOf(q, FullQueryInfo.prototype);
|
||||
|
||||
// The config object is a global, se we need to set it explicitly
|
||||
// and ensure it is not serialized to JSON.
|
||||
q.setConfig(config);
|
||||
|
||||
// Date instances are serialized as strings. Need to
|
||||
// convert them back to Date instances.
|
||||
(q.initialInfo as any).start = new Date(q.initialInfo.start);
|
||||
if (q.completedQuery) {
|
||||
// Again, need to explicitly set prototypes.
|
||||
Object.setPrototypeOf(q.completedQuery, CompletedQueryInfo.prototype);
|
||||
Object.setPrototypeOf(q.completedQuery.query, QueryEvaluationInfo.prototype);
|
||||
// slurped queries do not need to be disposed
|
||||
q.completedQuery.dispose = () => { /**/ };
|
||||
}
|
||||
return q;
|
||||
});
|
||||
} catch (e) {
|
||||
void showAndLogErrorMessage('Error loading query history.', {
|
||||
fullMessage: ['Error loading query history.', e.stack].join('\n'),
|
||||
});
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the query history to disk. It is not necessary that the parent directory
|
||||
* exists, but if it does, it must be writable. An existing file will be overwritten.
|
||||
*
|
||||
* Any errors will be rethrown.
|
||||
*
|
||||
* @param queries the list of queries to save.
|
||||
* @param fsPath the path to save the queries to.
|
||||
*/
|
||||
static async splat(queries: FullQueryInfo[], fsPath: string): Promise<void> {
|
||||
try {
|
||||
const data = JSON.stringify(queries, null, 2);
|
||||
await fs.mkdirp(path.dirname(fsPath));
|
||||
await fs.writeFile(fsPath, data);
|
||||
} catch (e) {
|
||||
throw new Error(`Error saving query history to ${fsPath}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public failureReason: string | undefined;
|
||||
public completedQuery: CompletedQueryInfo | undefined;
|
||||
private config: QueryHistoryConfig | undefined;
|
||||
|
||||
/**
|
||||
* Note that in the {@link FullQueryInfo.slurp} method, we create a FullQueryInfo instance
|
||||
* by explicitly setting the prototype in order to avoid calling this constructor.
|
||||
*/
|
||||
constructor(
|
||||
public readonly initialInfo: InitialQueryInfo,
|
||||
config: QueryHistoryConfig,
|
||||
private readonly source?: CancellationTokenSource
|
||||
) {
|
||||
this.setConfig(config);
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.source?.cancel();
|
||||
}
|
||||
|
||||
get startTime() {
|
||||
return this.initialInfo.start.toLocaleString(env.language);
|
||||
}
|
||||
|
||||
interpolate(template: string): string {
|
||||
const { resultCount = 0, statusString = 'in progress' } = this.completedQuery || {};
|
||||
const replacements: { [k: string]: string } = {
|
||||
t: this.startTime,
|
||||
q: this.getQueryName(),
|
||||
d: this.initialInfo.databaseInfo.name,
|
||||
r: resultCount.toString(),
|
||||
s: statusString,
|
||||
f: this.getQueryFileName(),
|
||||
'%': '%',
|
||||
};
|
||||
return template.replace(/%(.)/g, (match, key) => {
|
||||
const replacement = replacements[key];
|
||||
return replacement !== undefined ? replacement : match;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a label for this query that includes interpolated values.
|
||||
*/
|
||||
get label(): string {
|
||||
return this.interpolate(
|
||||
this.initialInfo.userSpecifiedLabel ?? this.config?.format ?? ''
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Avoids getting the default label for the query.
|
||||
* If there is a custom label for this query, interpolate and use that.
|
||||
* Otherwise, use the name of the query.
|
||||
*
|
||||
* @returns the name of the query, unless there is a custom label for this query.
|
||||
*/
|
||||
getShortLabel(): string {
|
||||
return this.initialInfo.userSpecifiedLabel
|
||||
? this.interpolate(this.initialInfo.userSpecifiedLabel)
|
||||
: this.getQueryName();
|
||||
}
|
||||
|
||||
/**
|
||||
* The query's file name, unless it is a quick eval.
|
||||
* Queries run through quick evaluation are not usually the entire query file.
|
||||
* Label them differently and include the line numbers.
|
||||
*/
|
||||
getQueryFileName() {
|
||||
if (this.initialInfo.quickEvalPosition) {
|
||||
const { line, endLine, fileName } = this.initialInfo.quickEvalPosition;
|
||||
const lineInfo = line === endLine ? `${line}` : `${line}-${endLine}`;
|
||||
return `${path.basename(fileName)}:${lineInfo}`;
|
||||
}
|
||||
return path.basename(this.initialInfo.queryPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Three cases:
|
||||
*
|
||||
* - If this is a completed query, use the query name from the query metadata.
|
||||
* - If this is a quick eval, return the query name with a prefix
|
||||
* - Otherwise, return the query file name.
|
||||
*/
|
||||
getQueryName() {
|
||||
if (this.initialInfo.quickEvalPosition) {
|
||||
return 'Quick evaluation of ' + this.getQueryFileName();
|
||||
} else if (this.completedQuery?.query.metadata?.name) {
|
||||
return this.completedQuery?.query.metadata?.name;
|
||||
} else {
|
||||
return this.getQueryFileName();
|
||||
}
|
||||
}
|
||||
|
||||
isCompleted(): boolean {
|
||||
return !!this.completedQuery;
|
||||
}
|
||||
|
||||
completeThisQuery(info: QueryWithResults) {
|
||||
this.completedQuery = new CompletedQueryInfo(info);
|
||||
}
|
||||
|
||||
/**
|
||||
* If there is a failure reason, then this query has failed.
|
||||
* If there is no completed query, then this query is still running.
|
||||
* If there is a completed query, then check if didRunSuccessfully.
|
||||
* If true, then this query has completed successfully, otherwise it has failed.
|
||||
*/
|
||||
get status(): QueryStatus {
|
||||
if (this.failureReason) {
|
||||
return QueryStatus.Failed;
|
||||
} else if (!this.completedQuery) {
|
||||
return QueryStatus.InProgress;
|
||||
} else if (this.completedQuery.didRunSuccessfully) {
|
||||
return QueryStatus.Completed;
|
||||
} else {
|
||||
return QueryStatus.Failed;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The `config` property must not be serialized since it contains a listerner
|
||||
* for global configuration changes. Instead, It should be set when the query
|
||||
* is deserialized.
|
||||
*
|
||||
* @param config the global query history config object
|
||||
*/
|
||||
private setConfig(config: QueryHistoryConfig) {
|
||||
// avoid serializing config property
|
||||
Object.defineProperty(this, 'config', {
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
configurable: true,
|
||||
value: config
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { ExtensionContext } from 'vscode';
|
||||
import { CancellationToken, ExtensionContext } from 'vscode';
|
||||
import { Credentials } from '../authentication';
|
||||
import { Logger } from '../logging';
|
||||
import { downloadArtifactFromLink } from './gh-actions-api-client';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs-extra';
|
||||
import { AnalysisSummary } from './shared/remote-query-result';
|
||||
import * as sarif from 'sarif';
|
||||
import { AnalysisResults, QueryResult } from './shared/analysis-result';
|
||||
import { UserCancellationException } from '../commandRunner';
|
||||
import * as os from 'os';
|
||||
import { sarifParser } from '../sarif-parser';
|
||||
|
||||
export class AnalysesResultsManager {
|
||||
// Store for the results of various analyses for a single remote query.
|
||||
@@ -21,6 +22,7 @@ export class AnalysesResultsManager {
|
||||
|
||||
public async downloadAnalysisResults(
|
||||
analysisSummary: AnalysisSummary,
|
||||
publishResults: (analysesResults: AnalysisResults[]) => Promise<void>
|
||||
): Promise<void> {
|
||||
if (this.analysesResults.some(x => x.nwo === analysisSummary.nwo)) {
|
||||
// We already have the results for this analysis, don't download again.
|
||||
@@ -31,18 +33,44 @@ export class AnalysesResultsManager {
|
||||
|
||||
void this.logger.log(`Downloading and processing results for ${analysisSummary.nwo}`);
|
||||
|
||||
await this.downloadSingleAnalysisResults(analysisSummary, credentials);
|
||||
await this.downloadSingleAnalysisResults(analysisSummary, credentials, publishResults);
|
||||
}
|
||||
|
||||
public async downloadAllResults(
|
||||
analysisSummaries: AnalysisSummary[],
|
||||
public async downloadAnalysesResults(
|
||||
analysesToDownload: AnalysisSummary[],
|
||||
token: CancellationToken | undefined,
|
||||
publishResults: (analysesResults: AnalysisResults[]) => Promise<void>
|
||||
): Promise<void> {
|
||||
const credentials = await Credentials.initialize(this.ctx);
|
||||
|
||||
void this.logger.log('Downloading and processing all results');
|
||||
void this.logger.log('Downloading and processing analyses results');
|
||||
|
||||
for (const analysis of analysisSummaries) {
|
||||
await this.downloadSingleAnalysisResults(analysis, credentials);
|
||||
const batchSize = 3;
|
||||
const numOfBatches = Math.ceil(analysesToDownload.length / batchSize);
|
||||
const allFailures = [];
|
||||
|
||||
for (let i = 0; i < analysesToDownload.length; i += batchSize) {
|
||||
if (token?.isCancellationRequested) {
|
||||
throw new UserCancellationException('Downloading of analyses results has been cancelled', true);
|
||||
}
|
||||
|
||||
const batch = analysesToDownload.slice(i, i + batchSize);
|
||||
const batchTasks = batch.map(analysis => this.downloadSingleAnalysisResults(analysis, credentials, publishResults));
|
||||
|
||||
const nwos = batch.map(a => a.nwo).join(', ');
|
||||
void this.logger.log(`Downloading batch ${Math.floor(i / batchSize) + 1} of ${numOfBatches} (${nwos})`);
|
||||
|
||||
const taskResults = await Promise.allSettled(batchTasks);
|
||||
const failedTasks = taskResults.filter(x => x.status === 'rejected') as Array<PromiseRejectedResult>;
|
||||
if (failedTasks.length > 0) {
|
||||
const failures = failedTasks.map(t => t.reason.message);
|
||||
failures.forEach(f => void this.logger.log(f));
|
||||
allFailures.push(...failures);
|
||||
}
|
||||
}
|
||||
|
||||
if (allFailures.length > 0) {
|
||||
throw Error(allFailures.join(os.EOL));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,28 +80,42 @@ export class AnalysesResultsManager {
|
||||
|
||||
private async downloadSingleAnalysisResults(
|
||||
analysis: AnalysisSummary,
|
||||
credentials: Credentials
|
||||
credentials: Credentials,
|
||||
publishResults: (analysesResults: AnalysisResults[]) => Promise<void>
|
||||
): Promise<void> {
|
||||
const artifactPath = await downloadArtifactFromLink(credentials, analysis.downloadLink);
|
||||
const analysisResults: AnalysisResults = {
|
||||
nwo: analysis.nwo,
|
||||
status: 'InProgress',
|
||||
results: []
|
||||
};
|
||||
|
||||
let analysisResults: AnalysisResults;
|
||||
this.analysesResults.push(analysisResults);
|
||||
void publishResults(this.analysesResults);
|
||||
|
||||
let artifactPath;
|
||||
try {
|
||||
artifactPath = await downloadArtifactFromLink(credentials, analysis.downloadLink);
|
||||
}
|
||||
catch (e) {
|
||||
throw new Error(`Could not download the analysis results for ${analysis.nwo}: ${e.message}`);
|
||||
}
|
||||
|
||||
if (path.extname(artifactPath) === '.sarif') {
|
||||
const queryResults = await this.readResults(artifactPath);
|
||||
analysisResults = { nwo: analysis.nwo, results: queryResults };
|
||||
analysisResults.results = queryResults;
|
||||
analysisResults.status = 'Completed';
|
||||
} else {
|
||||
void this.logger.log('Cannot download results. Only alert and path queries are fully supported.');
|
||||
analysisResults = { nwo: analysis.nwo, results: [] };
|
||||
analysisResults.status = 'Failed';
|
||||
}
|
||||
|
||||
this.analysesResults.push(analysisResults);
|
||||
void publishResults(this.analysesResults);
|
||||
}
|
||||
|
||||
private async readResults(filePath: string): Promise<QueryResult[]> {
|
||||
const queryResults: QueryResult[] = [];
|
||||
|
||||
const sarifContents = await fs.readFile(filePath, 'utf8');
|
||||
const sarifLog = JSON.parse(sarifContents) as sarif.Log;
|
||||
const sarifLog = await sarifParser(filePath);
|
||||
|
||||
// Read the sarif file and extract information that we want to display
|
||||
// in the UI. For now we're only getting the message texts but we'll gradually
|
||||
|
||||
@@ -34,8 +34,6 @@ export async function getRemoteQueryIndex(
|
||||
const resultIndexArtifactId = getArtifactIDfromName('result-index', workflowUri, artifactList);
|
||||
const resultIndexItems = await getResultIndexItems(credentials, owner, repoName, resultIndexArtifactId);
|
||||
|
||||
const allResultsArtifactId = getArtifactIDfromName('all-results', workflowUri, artifactList);
|
||||
|
||||
const items = resultIndexItems.map(item => {
|
||||
const artifactId = getArtifactIDfromName(item.id, workflowUri, artifactList);
|
||||
|
||||
@@ -50,9 +48,8 @@ export async function getRemoteQueryIndex(
|
||||
});
|
||||
|
||||
return {
|
||||
allResultsArtifactId,
|
||||
artifactsUrlPath,
|
||||
items,
|
||||
items
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
ViewColumn,
|
||||
Uri,
|
||||
workspace,
|
||||
extensions,
|
||||
commands,
|
||||
} from 'vscode';
|
||||
import * as path from 'path';
|
||||
|
||||
@@ -14,6 +16,7 @@ import {
|
||||
FromRemoteQueriesMessage,
|
||||
RemoteQueryDownloadAnalysisResultsMessage,
|
||||
RemoteQueryDownloadAllAnalysesResultsMessage,
|
||||
RemoteQueryViewAnalysisResultsMessage,
|
||||
} from '../pure/interface-types';
|
||||
import { Logger } from '../logging';
|
||||
import { getHtmlForWebview } from '../interface-utils';
|
||||
@@ -51,12 +54,14 @@ export class RemoteQueriesInterfaceManager {
|
||||
t: 'setRemoteQueryResult',
|
||||
queryResult: this.buildViewModel(query, queryResult)
|
||||
});
|
||||
|
||||
await this.setAnalysisResults(this.analysesResultsManager.getAnalysesResults());
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds up a model tailored to the view based on the query and result domain entities.
|
||||
* The data is cleaned up, sorted where necessary, and transformed to a format that
|
||||
* the view model can use.
|
||||
* the view model can use.
|
||||
* @param query Information about the query that was run.
|
||||
* @param queryResult The result of the query.
|
||||
* @returns A fully created view model.
|
||||
@@ -78,7 +83,6 @@ export class RemoteQueriesInterfaceManager {
|
||||
totalResultCount: totalResultCount,
|
||||
executionTimestamp: this.formatDate(query.executionStartTime),
|
||||
executionDuration: executionDuration,
|
||||
downloadLink: queryResult.allResultsDownloadLink,
|
||||
analysisSummaries: analysisSummaries
|
||||
};
|
||||
}
|
||||
@@ -123,12 +127,15 @@ export class RemoteQueriesInterfaceManager {
|
||||
panel.webview.html = getHtmlForWebview(
|
||||
panel.webview,
|
||||
scriptPathOnDisk,
|
||||
[baseStylesheetUriOnDisk, stylesheetPathOnDisk]
|
||||
[baseStylesheetUriOnDisk, stylesheetPathOnDisk],
|
||||
true
|
||||
);
|
||||
panel.webview.onDidReceiveMessage(
|
||||
async (e) => this.handleMsgFromView(e),
|
||||
undefined,
|
||||
ctx.subscriptions
|
||||
ctx.subscriptions.push(
|
||||
panel.webview.onDidReceiveMessage(
|
||||
async (e) => this.handleMsgFromView(e),
|
||||
undefined,
|
||||
ctx.subscriptions
|
||||
)
|
||||
);
|
||||
}
|
||||
return this.panel;
|
||||
@@ -195,26 +202,56 @@ export class RemoteQueriesInterfaceManager {
|
||||
case 'remoteQueryDownloadAllAnalysesResults':
|
||||
await this.downloadAllAnalysesResults(msg);
|
||||
break;
|
||||
case 'remoteQueryViewAnalysisResults':
|
||||
await this.viewAnalysisResults(msg);
|
||||
break;
|
||||
default:
|
||||
assertNever(msg);
|
||||
}
|
||||
}
|
||||
|
||||
private async downloadAnalysisResults(msg: RemoteQueryDownloadAnalysisResultsMessage): Promise<void> {
|
||||
await this.analysesResultsManager.downloadAnalysisResults(msg.analysisSummary);
|
||||
await this.setAnalysisResults(this.analysesResultsManager.getAnalysesResults());
|
||||
await this.analysesResultsManager.downloadAnalysisResults(
|
||||
msg.analysisSummary,
|
||||
results => this.setAnalysisResults(results));
|
||||
}
|
||||
|
||||
private async downloadAllAnalysesResults(msg: RemoteQueryDownloadAllAnalysesResultsMessage): Promise<void> {
|
||||
await this.analysesResultsManager.downloadAllResults(msg.analysisSummaries);
|
||||
await this.setAnalysisResults(this.analysesResultsManager.getAnalysesResults());
|
||||
await this.analysesResultsManager.downloadAnalysesResults(
|
||||
msg.analysisSummaries,
|
||||
undefined,
|
||||
results => this.setAnalysisResults(results));
|
||||
}
|
||||
|
||||
private async setAnalysisResults(analysesResults: AnalysisResults[]): Promise<void> {
|
||||
await this.postMessage({
|
||||
t: 'setAnalysesResults',
|
||||
analysesResults: analysesResults
|
||||
});
|
||||
private async viewAnalysisResults(msg: RemoteQueryViewAnalysisResultsMessage): Promise<void> {
|
||||
const downloadLink = msg.analysisSummary.downloadLink;
|
||||
const filePath = path.join(tmpDir.name, downloadLink.id, downloadLink.innerFilePath || '');
|
||||
|
||||
const sarifViewerExtensionId = 'MS-SarifVSCode.sarif-viewer';
|
||||
|
||||
const sarifExt = extensions.getExtension(sarifViewerExtensionId);
|
||||
if (!sarifExt) {
|
||||
// Ask the user if they want to install the extension to view the results.
|
||||
void commands.executeCommand('workbench.extensions.installExtension', sarifViewerExtensionId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sarifExt.isActive) {
|
||||
await sarifExt.activate();
|
||||
}
|
||||
|
||||
await sarifExt.exports.openLogs([
|
||||
Uri.file(filePath),
|
||||
]);
|
||||
}
|
||||
|
||||
public async setAnalysisResults(analysesResults: AnalysisResults[]): Promise<void> {
|
||||
if (this.panel?.active) {
|
||||
await this.postMessage({
|
||||
t: 'setAnalysesResults',
|
||||
analysesResults: analysesResults
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private postMessage(msg: ToRemoteQueriesMessage): Thenable<boolean> {
|
||||
|
||||
@@ -14,16 +14,21 @@ import { RemoteQueryResult } from './remote-query-result';
|
||||
import { DownloadLink } from './download-link';
|
||||
import { AnalysesResultsManager } from './analyses-results-manager';
|
||||
|
||||
const autoDownloadMaxSize = 300 * 1024;
|
||||
const autoDownloadMaxCount = 100;
|
||||
|
||||
export class RemoteQueriesManager {
|
||||
private readonly remoteQueriesMonitor: RemoteQueriesMonitor;
|
||||
private readonly analysesResultsManager: AnalysesResultsManager;
|
||||
private readonly interfaceManager: RemoteQueriesInterfaceManager;
|
||||
|
||||
constructor(
|
||||
private readonly ctx: ExtensionContext,
|
||||
private readonly logger: Logger,
|
||||
private readonly cliServer: CodeQLCliServer
|
||||
private readonly cliServer: CodeQLCliServer,
|
||||
logger: Logger,
|
||||
) {
|
||||
this.analysesResultsManager = new AnalysesResultsManager(ctx, logger);
|
||||
this.interfaceManager = new RemoteQueriesInterfaceManager(ctx, logger, this.analysesResultsManager);
|
||||
this.remoteQueriesMonitor = new RemoteQueriesMonitor(ctx, logger);
|
||||
}
|
||||
|
||||
@@ -65,13 +70,16 @@ export class RemoteQueriesManager {
|
||||
|
||||
const queryResult = this.mapQueryResult(executionEndTime, resultIndex);
|
||||
|
||||
// Kick off auto-download of results.
|
||||
void commands.executeCommand('codeQL.autoDownloadRemoteQueryResults', queryResult);
|
||||
|
||||
const totalResultCount = queryResult.analysisSummaries.reduce((acc, cur) => acc + cur.resultCount, 0);
|
||||
const message = `Query "${query.queryName}" run on ${query.repositories.length} repositories and returned ${totalResultCount} results`;
|
||||
|
||||
const shouldOpenView = await showInformationMessageWithAction(message, 'View');
|
||||
if (shouldOpenView) {
|
||||
const rqim = new RemoteQueriesInterfaceManager(this.ctx, this.logger, this.analysesResultsManager);
|
||||
await rqim.showResults(query, queryResult);
|
||||
await this.interfaceManager.showResults(query, queryResult);
|
||||
|
||||
}
|
||||
} else if (queryResult.status === 'CompletedUnsuccessfully') {
|
||||
await showAndLogErrorMessage(`Remote query execution failed. Error: ${queryResult.error}`);
|
||||
@@ -81,6 +89,26 @@ export class RemoteQueriesManager {
|
||||
}
|
||||
}
|
||||
|
||||
public async autoDownloadRemoteQueryResults(
|
||||
queryResult: RemoteQueryResult,
|
||||
token: CancellationToken
|
||||
): Promise<void> {
|
||||
const analysesToDownload = queryResult.analysisSummaries
|
||||
.filter(a => a.fileSizeInBytes < autoDownloadMaxSize)
|
||||
.slice(0, autoDownloadMaxCount)
|
||||
.map(a => ({
|
||||
nwo: a.nwo,
|
||||
resultCount: a.resultCount,
|
||||
downloadLink: a.downloadLink,
|
||||
fileSize: String(a.fileSizeInBytes)
|
||||
}));
|
||||
|
||||
await this.analysesResultsManager.downloadAnalysesResults(
|
||||
analysesToDownload,
|
||||
token,
|
||||
results => this.interfaceManager.setAnalysisResults(results));
|
||||
}
|
||||
|
||||
private mapQueryResult(executionEndTime: Date, resultIndex: RemoteQueryResultIndex): RemoteQueryResult {
|
||||
const analysisSummaries = resultIndex.items.map(item => ({
|
||||
nwo: item.nwo,
|
||||
@@ -95,11 +123,7 @@ export class RemoteQueriesManager {
|
||||
|
||||
return {
|
||||
executionEndTime,
|
||||
analysisSummaries,
|
||||
allResultsDownloadLink: {
|
||||
id: resultIndex.allResultsArtifactId.toString(),
|
||||
urlPath: `${resultIndex.artifactsUrlPath}/${resultIndex.allResultsArtifactId}`
|
||||
}
|
||||
analysisSummaries
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
export interface RemoteQueryResultIndex {
|
||||
artifactsUrlPath: string;
|
||||
allResultsArtifactId: number;
|
||||
items: RemoteQueryResultIndexItem[];
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import { DownloadLink } from './download-link';
|
||||
export interface RemoteQueryResult {
|
||||
executionEndTime: Date;
|
||||
analysisSummaries: AnalysisSummary[];
|
||||
allResultsDownloadLink: DownloadLink;
|
||||
}
|
||||
|
||||
export interface AnalysisSummary {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { RemoteQuery } from './remote-query';
|
||||
import { RemoteQueryResult } from './remote-query-result';
|
||||
import { AnalysisResults } from './shared/analysis-result';
|
||||
|
||||
export const sampleRemoteQuery: RemoteQuery = {
|
||||
queryName: 'Inefficient regular expression',
|
||||
@@ -37,10 +38,6 @@ export const sampleRemoteQuery: RemoteQuery = {
|
||||
|
||||
export const sampleRemoteQueryResult: RemoteQueryResult = {
|
||||
executionEndTime: new Date('2022-01-06T17:04:37.026Z'),
|
||||
allResultsDownloadLink: {
|
||||
id: '137697018',
|
||||
urlPath: '/repos/big-corp/controller-repo/actions/artifacts/137697018'
|
||||
},
|
||||
analysisSummaries: [
|
||||
{
|
||||
nwo: 'big-corp/repo1',
|
||||
@@ -84,3 +81,95 @@ export const sampleRemoteQueryResult: RemoteQueryResult = {
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
const createAnalysisResults = (n: number) => Array(n).fill({ 'message': 'Sample text' });
|
||||
|
||||
export const sampleAnalysesResultsStage1: AnalysisResults[] = [
|
||||
{
|
||||
nwo: 'big-corp/repo1',
|
||||
status: 'InProgress',
|
||||
results: []
|
||||
},
|
||||
{
|
||||
nwo: 'big-corp/repo2',
|
||||
status: 'InProgress',
|
||||
results: []
|
||||
|
||||
},
|
||||
{
|
||||
nwo: 'big-corp/repo3',
|
||||
status: 'InProgress',
|
||||
results: []
|
||||
},
|
||||
// No entries for repo4
|
||||
];
|
||||
|
||||
export const sampleAnalysesResultsStage2: AnalysisResults[] = [
|
||||
{
|
||||
nwo: 'big-corp/repo1',
|
||||
status: 'Completed',
|
||||
results: createAnalysisResults(85)
|
||||
},
|
||||
{
|
||||
nwo: 'big-corp/repo2',
|
||||
status: 'Completed',
|
||||
results: createAnalysisResults(20)
|
||||
},
|
||||
{
|
||||
nwo: 'big-corp/repo3',
|
||||
status: 'InProgress',
|
||||
results: []
|
||||
},
|
||||
{
|
||||
nwo: 'big-corp/repo4',
|
||||
status: 'InProgress',
|
||||
results: []
|
||||
},
|
||||
];
|
||||
|
||||
export const sampleAnalysesResultsStage3: AnalysisResults[] = [
|
||||
{
|
||||
nwo: 'big-corp/repo1',
|
||||
status: 'Completed',
|
||||
results: createAnalysisResults(85)
|
||||
},
|
||||
{
|
||||
nwo: 'big-corp/repo2',
|
||||
status: 'Completed',
|
||||
results: createAnalysisResults(20)
|
||||
},
|
||||
{
|
||||
nwo: 'big-corp/repo3',
|
||||
status: 'Completed',
|
||||
results: createAnalysisResults(8)
|
||||
},
|
||||
{
|
||||
nwo: 'big-corp/repo4',
|
||||
status: 'Completed',
|
||||
results: createAnalysisResults(3)
|
||||
},
|
||||
];
|
||||
|
||||
export const sampleAnalysesResultsWithFailure: AnalysisResults[] = [
|
||||
{
|
||||
nwo: 'big-corp/repo1',
|
||||
status: 'Completed',
|
||||
results: createAnalysisResults(85)
|
||||
},
|
||||
{
|
||||
nwo: 'big-corp/repo2',
|
||||
status: 'Completed',
|
||||
results: createAnalysisResults(20)
|
||||
},
|
||||
{
|
||||
nwo: 'big-corp/repo3',
|
||||
status: 'Failed',
|
||||
results: []
|
||||
},
|
||||
{
|
||||
nwo: 'big-corp/repo4',
|
||||
status: 'Completed',
|
||||
results: createAnalysisResults(3)
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
export type AnalysisResultStatus = 'InProgress' | 'Completed' | 'Failed';
|
||||
|
||||
export interface AnalysisResults {
|
||||
nwo: string;
|
||||
status: AnalysisResultStatus;
|
||||
results: QueryResult[];
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ export interface RemoteQueryResult {
|
||||
totalResultCount: number;
|
||||
executionTimestamp: string;
|
||||
executionDuration: string;
|
||||
downloadLink: DownloadLink;
|
||||
analysisSummaries: AnalysisSummary[]
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,29 @@
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const BadgeContainer = styled.span`
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
padding-left: 0.2em;
|
||||
`;
|
||||
|
||||
const BadgeText = styled.span`
|
||||
display: inline-block;
|
||||
min-width: 1.5em;
|
||||
padding: 0.3em;
|
||||
border-radius: 35%;
|
||||
font-size: x-small;
|
||||
text-align: center;
|
||||
background: var(--vscode-badge-background);
|
||||
color: var(--vscode-badge-foreground);
|
||||
border-color: var(--vscode-badge-background);
|
||||
`;
|
||||
|
||||
const Badge = ({ text }: { text: string }) => (
|
||||
<span className="vscode-codeql__badge-container">
|
||||
<span className="vscode-codeql__badge">{text}</span>
|
||||
</span>
|
||||
<BadgeContainer>
|
||||
<BadgeText>{text}</BadgeText>
|
||||
</BadgeContainer>
|
||||
);
|
||||
|
||||
export default Badge;
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { ChevronDownIcon, ChevronRightIcon } from '@primer/octicons-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
const Container = styled.div`
|
||||
display: block;
|
||||
vertical-align: middle;
|
||||
`;
|
||||
|
||||
const TitleContainer = styled.span`
|
||||
display: inline-block;
|
||||
`;
|
||||
|
||||
const Button = styled.button`
|
||||
display: inline-block;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
padding-left: 0;
|
||||
padding-right: 0.1em;
|
||||
`;
|
||||
|
||||
const CollapsibleItem = ({
|
||||
title,
|
||||
children
|
||||
}: {
|
||||
title: React.ReactNode;
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
const [isExpanded, setExpanded] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<Container>
|
||||
<Button onClick={() => setExpanded(!isExpanded)}>
|
||||
{isExpanded
|
||||
? <ChevronDownIcon size={16} />
|
||||
: <ChevronRightIcon size={16} />
|
||||
}
|
||||
</Button>
|
||||
<TitleContainer>{title}</TitleContainer>
|
||||
</Container>
|
||||
{isExpanded && children}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CollapsibleItem;
|
||||
@@ -1,11 +1,23 @@
|
||||
import * as React from 'react';
|
||||
import * as octicons from '../../view/octicons';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const ButtonLink = styled.a`
|
||||
display: inline-block;
|
||||
font-size: x-small;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
|
||||
svg {
|
||||
fill: var(--vscode-textLink-foreground);
|
||||
}
|
||||
`;
|
||||
|
||||
const DownloadButton = ({ text, onClick }: { text: string, onClick: () => void }) => (
|
||||
<a className="vscode-codeql__download-button"
|
||||
onClick={onClick}>
|
||||
<ButtonLink onClick={onClick}>
|
||||
{octicons.download}{text}
|
||||
</a>
|
||||
</ButtonLink>
|
||||
);
|
||||
|
||||
export default DownloadButton;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Spinner } from '@primer/react';
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const SpinnerContainer = styled.span`
|
||||
vertical-align: middle;
|
||||
|
||||
svg {
|
||||
width: 0.8em;
|
||||
height: 0.8em;
|
||||
}
|
||||
`;
|
||||
|
||||
const DownloadSpinner = () => (
|
||||
<SpinnerContainer>
|
||||
<Spinner size="small" />
|
||||
</SpinnerContainer>
|
||||
);
|
||||
|
||||
export default DownloadSpinner;
|
||||
@@ -0,0 +1,53 @@
|
||||
import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import styled from 'styled-components';
|
||||
import { XCircleIcon } from '@primer/octicons-react';
|
||||
|
||||
const Container = styled.div`
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
opacity: 1;
|
||||
background-color: var(--vscode-editor-background);
|
||||
z-index: 5000;
|
||||
padding-top: 1em;
|
||||
`;
|
||||
|
||||
const CloseButton = styled.button`
|
||||
position: absolute;
|
||||
top: 1em;
|
||||
right: 1em;
|
||||
background-color: var(--vscode-editor-background);
|
||||
border: none;
|
||||
`;
|
||||
|
||||
const FullScreenModal = ({
|
||||
setOpen,
|
||||
containerElementId,
|
||||
children
|
||||
}: {
|
||||
setOpen: (open: boolean) => void;
|
||||
containerElementId: string;
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
const containerElement = document.getElementById(containerElementId);
|
||||
if (!containerElement) {
|
||||
throw Error(`Could not find container element. Id: ${containerElementId}`);
|
||||
}
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
<>
|
||||
<Container>
|
||||
<CloseButton onClick={() => setOpen(false)}>
|
||||
<XCircleIcon size={24} />
|
||||
</CloseButton>
|
||||
{children}
|
||||
</Container>
|
||||
</>,
|
||||
containerElement
|
||||
);
|
||||
};
|
||||
|
||||
export default FullScreenModal;
|
||||
@@ -0,0 +1,9 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const HorizontalSpace = styled.div<{ size: 1 | 2 | 3 }>`
|
||||
flex: 0 0 auto;
|
||||
display: inline-block;
|
||||
width: ${props => 0.2 * props.size}em;
|
||||
`;
|
||||
|
||||
export default HorizontalSpace;
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import * as Rdom from 'react-dom';
|
||||
import { ThemeProvider } from '@primer/react';
|
||||
import { ToRemoteQueriesMessage } from '../../pure/interface-types';
|
||||
import { AnalysisSummary, RemoteQueryResult } from '../shared/remote-query-result';
|
||||
import * as octicons from '../../view/octicons';
|
||||
@@ -9,10 +10,14 @@ import { vscode } from '../../view/vscode-api';
|
||||
|
||||
import SectionTitle from './SectionTitle';
|
||||
import VerticalSpace from './VerticalSpace';
|
||||
import HorizontalSpace from './HorizontalSpace';
|
||||
import Badge from './Badge';
|
||||
import ViewTitle from './ViewTitle';
|
||||
import DownloadButton from './DownloadButton';
|
||||
import { AnalysisResults } from '../shared/analysis-result';
|
||||
import DownloadSpinner from './DownloadSpinner';
|
||||
import CollapsibleItem from './CollapsibleItem';
|
||||
import { FileSymlinkFileIcon } from '@primer/octicons-react';
|
||||
|
||||
const numOfReposInContractedMode = 10;
|
||||
|
||||
@@ -26,10 +31,6 @@ const emptyQueryResult: RemoteQueryResult = {
|
||||
totalResultCount: 0,
|
||||
executionTimestamp: '',
|
||||
executionDuration: '',
|
||||
downloadLink: {
|
||||
id: '',
|
||||
urlPath: '',
|
||||
},
|
||||
analysisSummaries: []
|
||||
};
|
||||
|
||||
@@ -47,6 +48,13 @@ const downloadAllAnalysesResults = (query: RemoteQueryResult) => {
|
||||
});
|
||||
};
|
||||
|
||||
const viewAnalysisResults = (analysisSummary: AnalysisSummary) => {
|
||||
vscode.postMessage({
|
||||
t: 'remoteQueryViewAnalysisResults',
|
||||
analysisSummary
|
||||
});
|
||||
};
|
||||
|
||||
const openQueryFile = (queryResult: RemoteQueryResult) => {
|
||||
vscode.postMessage({
|
||||
t: 'openFile',
|
||||
@@ -61,12 +69,15 @@ const openQueryTextVirtualFile = (queryResult: RemoteQueryResult) => {
|
||||
});
|
||||
};
|
||||
|
||||
const sumAnalysesResults = (analysesResults: AnalysisResults[]) =>
|
||||
analysesResults.reduce((acc, curr) => acc + curr.results.length, 0);
|
||||
|
||||
const QueryInfo = (queryResult: RemoteQueryResult) => (
|
||||
<>
|
||||
<VerticalSpace />
|
||||
<VerticalSpace size={1} />
|
||||
{queryResult.totalResultCount} results in {queryResult.totalRepositoryCount} repositories
|
||||
({queryResult.executionDuration}), {queryResult.executionTimestamp}
|
||||
<VerticalSpace />
|
||||
<VerticalSpace size={1} />
|
||||
<span className="vscode-codeql__query-file">{octicons.file}
|
||||
<a className="vscode-codeql__query-file-link" href="#" onClick={() => openQueryFile(queryResult)}>
|
||||
{queryResult.queryFileName}
|
||||
@@ -80,34 +91,88 @@ const QueryInfo = (queryResult: RemoteQueryResult) => (
|
||||
</>
|
||||
);
|
||||
|
||||
const SummaryTitleWithResults = (queryResult: RemoteQueryResult) => (
|
||||
<div className="vscode-codeql__query-summary-container">
|
||||
<SectionTitle text={`Repositories with results (${queryResult.affectedRepositoryCount}):`} />
|
||||
<DownloadButton
|
||||
text="Download all"
|
||||
onClick={() => downloadAllAnalysesResults(queryResult)} />
|
||||
</div>
|
||||
);
|
||||
const SummaryTitleWithResults = ({
|
||||
queryResult,
|
||||
analysesResults
|
||||
}: {
|
||||
queryResult: RemoteQueryResult,
|
||||
analysesResults: AnalysisResults[]
|
||||
}) => {
|
||||
const showDownloadButton = queryResult.totalResultCount !== sumAnalysesResults(analysesResults);
|
||||
|
||||
return (
|
||||
<div className="vscode-codeql__query-summary-container">
|
||||
<SectionTitle>Repositories with results ({queryResult.affectedRepositoryCount}):</SectionTitle>
|
||||
{
|
||||
showDownloadButton && <DownloadButton
|
||||
text="Download all"
|
||||
onClick={() => downloadAllAnalysesResults(queryResult)} />
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SummaryTitleNoResults = () => (
|
||||
<div className="vscode-codeql__query-summary-container">
|
||||
<SectionTitle text="No results found" />
|
||||
<SectionTitle>No results found</SectionTitle>
|
||||
</div>
|
||||
);
|
||||
|
||||
const SummaryItem = (props: AnalysisSummary) => (
|
||||
const SummaryItemDownloadAndView = ({
|
||||
analysisSummary,
|
||||
analysisResults
|
||||
}: {
|
||||
analysisSummary: AnalysisSummary,
|
||||
analysisResults: AnalysisResults | undefined
|
||||
}) => {
|
||||
if (!analysisResults || analysisResults.status === 'Failed') {
|
||||
return <DownloadButton
|
||||
text={analysisSummary.fileSize}
|
||||
onClick={() => downloadAnalysisResults(analysisSummary)} />;
|
||||
}
|
||||
|
||||
if (analysisResults.status === 'InProgress') {
|
||||
return <>
|
||||
<HorizontalSpace size={2} />
|
||||
<DownloadSpinner />
|
||||
</>;
|
||||
}
|
||||
|
||||
return <>
|
||||
<HorizontalSpace size={2} />
|
||||
<a className="vscode-codeql__analysis-result-file-link"
|
||||
onClick={() => viewAnalysisResults(analysisSummary)} >
|
||||
<FileSymlinkFileIcon size={16} />
|
||||
</a>
|
||||
</>;
|
||||
};
|
||||
|
||||
const SummaryItem = ({
|
||||
analysisSummary,
|
||||
analysisResults
|
||||
}: {
|
||||
analysisSummary: AnalysisSummary,
|
||||
analysisResults: AnalysisResults | undefined
|
||||
}) => (
|
||||
<span>
|
||||
<span className="vscode-codeql__analysis-item">{octicons.repo}</span>
|
||||
<span className="vscode-codeql__analysis-item">{props.nwo}</span>
|
||||
<span className="vscode-codeql__analysis-item"><Badge text={props.resultCount.toString()} /></span>
|
||||
<span className="vscode-codeql__analysis-item">{analysisSummary.nwo}</span>
|
||||
<span className="vscode-codeql__analysis-item"><Badge text={analysisSummary.resultCount.toString()} /></span>
|
||||
<span className="vscode-codeql__analysis-item">
|
||||
<DownloadButton
|
||||
text={props.fileSize}
|
||||
onClick={() => downloadAnalysisResults(props)} />
|
||||
<SummaryItemDownloadAndView
|
||||
analysisSummary={analysisSummary}
|
||||
analysisResults={analysisResults} />
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
const Summary = (queryResult: RemoteQueryResult) => {
|
||||
|
||||
const Summary = ({
|
||||
queryResult,
|
||||
analysesResults
|
||||
}: {
|
||||
queryResult: RemoteQueryResult,
|
||||
analysesResults: AnalysisResults[]
|
||||
}) => {
|
||||
const [repoListExpanded, setRepoListExpanded] = useState(false);
|
||||
const numOfReposToShow = repoListExpanded ? queryResult.analysisSummaries.length : numOfReposInContractedMode;
|
||||
|
||||
@@ -116,13 +181,17 @@ const Summary = (queryResult: RemoteQueryResult) => {
|
||||
{
|
||||
queryResult.affectedRepositoryCount === 0
|
||||
? <SummaryTitleNoResults />
|
||||
: <SummaryTitleWithResults {...queryResult} />
|
||||
: <SummaryTitleWithResults
|
||||
queryResult={queryResult}
|
||||
analysesResults={analysesResults} />
|
||||
}
|
||||
|
||||
<ul className="vscode-codeql__analysis-summaries-list">
|
||||
{queryResult.analysisSummaries.slice(0, numOfReposToShow).map((summary, i) =>
|
||||
<li key={summary.nwo} className="vscode-codeql__analysis-summaries-list-item">
|
||||
<SummaryItem {...summary} />
|
||||
<SummaryItem
|
||||
analysisSummary={summary}
|
||||
analysisResults={analysesResults.find(a => a.nwo === summary.nwo)} />
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
@@ -138,16 +207,16 @@ const Summary = (queryResult: RemoteQueryResult) => {
|
||||
|
||||
const AnalysesResultsTitle = ({ totalAnalysesResults, totalResults }: { totalAnalysesResults: number, totalResults: number }) => {
|
||||
if (totalAnalysesResults === totalResults) {
|
||||
return <SectionTitle text={`${totalAnalysesResults} results`} />;
|
||||
return <SectionTitle>{totalAnalysesResults} results</SectionTitle>;
|
||||
}
|
||||
|
||||
return <SectionTitle text={`${totalAnalysesResults}/${totalResults} results`} />;
|
||||
return <SectionTitle>{totalAnalysesResults}/{totalResults} results</SectionTitle>;
|
||||
};
|
||||
|
||||
const AnalysesResultsDescription = ({ totalAnalysesResults, totalResults }: { totalAnalysesResults: number, totalResults: number }) => {
|
||||
if (totalAnalysesResults < totalResults) {
|
||||
return <>
|
||||
<VerticalSpace />
|
||||
<VerticalSpace size={1} />
|
||||
Some results haven't been downloaded automatically because of their size or because enough were downloaded already.
|
||||
Download them manually from the list above if you want to see them here.
|
||||
</>;
|
||||
@@ -156,8 +225,21 @@ const AnalysesResultsDescription = ({ totalAnalysesResults, totalResults }: { to
|
||||
return <></>;
|
||||
};
|
||||
|
||||
const RepoAnalysisResults = (analysisResults: AnalysisResults) => {
|
||||
const title = <>
|
||||
{analysisResults.nwo}
|
||||
<Badge text={analysisResults.results.length.toString()} />
|
||||
</>;
|
||||
|
||||
return (
|
||||
<CollapsibleItem title={title}>
|
||||
{analysisResults.results.map((r, i) => (<p key={i} >{r.message}</p>))}
|
||||
</CollapsibleItem>
|
||||
);
|
||||
};
|
||||
|
||||
const AnalysesResults = ({ analysesResults, totalResults }: { analysesResults: AnalysisResults[], totalResults: number }) => {
|
||||
const totalAnalysesResults = analysesResults.reduce((acc, curr) => acc + curr.results.length, 0);
|
||||
const totalAnalysesResults = sumAnalysesResults(analysesResults);
|
||||
|
||||
if (totalResults === 0) {
|
||||
return <></>;
|
||||
@@ -165,14 +247,19 @@ const AnalysesResults = ({ analysesResults, totalResults }: { analysesResults: A
|
||||
|
||||
return (
|
||||
<>
|
||||
<VerticalSpace />
|
||||
<VerticalSpace />
|
||||
<VerticalSpace size={2} />
|
||||
<AnalysesResultsTitle
|
||||
totalAnalysesResults={totalAnalysesResults}
|
||||
totalResults={totalResults} />
|
||||
<AnalysesResultsDescription
|
||||
totalAnalysesResults={totalAnalysesResults}
|
||||
totalResults={totalResults} />
|
||||
<ul className="vscode-codeql__analyses-results-list">
|
||||
{analysesResults.filter(a => a.results.length > 0).map(r =>
|
||||
<li key={r.nwo} className="vscode-codeql__analyses-results-list-item">
|
||||
<RepoAnalysisResults {...r} />
|
||||
</li>)}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -202,12 +289,16 @@ export function RemoteQueries(): JSX.Element {
|
||||
return <div>Waiting for results to load.</div>;
|
||||
}
|
||||
|
||||
const showAnalysesResults = false;
|
||||
|
||||
try {
|
||||
return <div>
|
||||
<ViewTitle title={queryResult.queryTitle} />
|
||||
<QueryInfo {...queryResult} />
|
||||
<Summary {...queryResult} />
|
||||
<AnalysesResults analysesResults={analysesResults} totalResults={queryResult.totalResultCount} />
|
||||
<ThemeProvider>
|
||||
<ViewTitle>{queryResult.queryTitle}</ViewTitle>
|
||||
<QueryInfo {...queryResult} />
|
||||
<Summary queryResult={queryResult} analysesResults={analysesResults} />
|
||||
{showAnalysesResults && <AnalysesResults analysesResults={analysesResults} totalResults={queryResult.totalResultCount} />}
|
||||
</ThemeProvider>
|
||||
</div>;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const SectionTitle = ({ text }: { text: string }) => (
|
||||
<h2 className="vscode-codeql__section-title">{text}</h2>
|
||||
);
|
||||
const SectionTitle = styled.h2`
|
||||
font-size: medium;
|
||||
font-weight: 500;
|
||||
padding: 0 0.5em 0 0;
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
`;
|
||||
|
||||
export default SectionTitle;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const VerticalSpace = () => (
|
||||
<div className="vscode-codeql__vertical-space" />
|
||||
);
|
||||
const VerticalSpace = styled.div<{ size: 1 | 2 | 3 }>`
|
||||
flex: 0 0 auto;
|
||||
height: ${props => 0.5 * props.size}em;
|
||||
`;
|
||||
|
||||
export default VerticalSpace;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const ViewTitle = ({ title }: { title: string }) => (
|
||||
<h1 className="vscode-codeql__view-title">{title}</h1>
|
||||
);
|
||||
const ViewTitle = styled.h1`
|
||||
font-size: large;
|
||||
margin-bottom: 0.5em;
|
||||
font-weight: 500;
|
||||
`;
|
||||
|
||||
export default ViewTitle;
|
||||
|
||||
@@ -2,74 +2,3 @@ body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial,
|
||||
sans-serif, Apple Color Emoji, Segoe UI Emoji;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* SectionTitle component */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
.vscode-codeql__section-title {
|
||||
font-size: medium;
|
||||
font-weight: 500;
|
||||
padding: 0 0.5em 0 0;
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* ViewTitle component */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
.vscode-codeql__view-title {
|
||||
font-size: large;
|
||||
margin-bottom: 0.5em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* VerticalSpace component */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
.vscode-codeql__vertical-space {
|
||||
flex: 0 0 auto;
|
||||
height: 0.5rem;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Badge component */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
.vscode-codeql__badge-container {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
padding-left: 0.2em;
|
||||
}
|
||||
|
||||
.vscode-codeql__badge {
|
||||
display: inline-block;
|
||||
min-width: 1.5em;
|
||||
padding: 0.3em;
|
||||
border-radius: 35%;
|
||||
font-size: x-small;
|
||||
text-align: center;
|
||||
background: var(--vscode-badge-background);
|
||||
color: var(--vscode-badge-foreground);
|
||||
border-color: var(--vscode-badge-background);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* DownloadButton component */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
.vscode-codeql__download-button {
|
||||
display: inline-block;
|
||||
font-size: x-small;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.vscode-codeql__download-button svg {
|
||||
fill: var(--vscode-textLink-foreground);
|
||||
}
|
||||
|
||||
@@ -38,6 +38,16 @@
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.vscode-codeql__analyses-results-list {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0.5em 0 0 0;
|
||||
}
|
||||
|
||||
.vscode-codeql__analyses-results-list-item {
|
||||
padding-top: 0.5em;
|
||||
}
|
||||
|
||||
.vscode-codeql__analysis-item {
|
||||
padding-right: 0.1em;
|
||||
}
|
||||
@@ -50,3 +60,7 @@
|
||||
padding-top: 1em;
|
||||
font-size: x-small;
|
||||
}
|
||||
|
||||
.vscode-codeql__analysis-result-file-link {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as crypto from 'crypto';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import * as tmp from 'tmp-promise';
|
||||
import { nanoid } from 'nanoid';
|
||||
import {
|
||||
CancellationToken,
|
||||
ConfigurationTarget,
|
||||
@@ -16,13 +17,13 @@ import { ErrorCodes, ResponseError } from 'vscode-languageclient';
|
||||
|
||||
import * as cli from './cli';
|
||||
import * as config from './config';
|
||||
import { DatabaseItem } from './databases';
|
||||
import { DatabaseItem, DatabaseManager } from './databases';
|
||||
import { getOnDiskWorkspaceFolders, showAndLogErrorMessage, tryGetQueryMetadata } from './helpers';
|
||||
import { ProgressCallback, UserCancellationException } from './commandRunner';
|
||||
import { DatabaseInfo, QueryMetadata, ResultsPaths } from './pure/interface-types';
|
||||
import { DatabaseInfo, QueryMetadata } from './pure/interface-types';
|
||||
import { logger } from './logging';
|
||||
import * as messages from './pure/messages';
|
||||
import { QueryHistoryItemOptions } from './query-history';
|
||||
import { InitialQueryInfo } from './query-results';
|
||||
import * as qsClient from './queryserver-client';
|
||||
import { isQuickQueryPath } from './quick-query';
|
||||
import { compileDatabaseUpgradeSequence, hasNondestructiveUpgradeCapabilities, upgradeDatabaseExplicit } from './upgrades';
|
||||
@@ -37,7 +38,6 @@ import { DecodedBqrsChunk } from './pure/bqrs-cli-types';
|
||||
* Compiling and running QL queries.
|
||||
*/
|
||||
|
||||
// XXX: Tmp directory should be configuarble.
|
||||
export const tmpDir = tmp.dirSync({ prefix: 'queries_', keep: false, unsafeCleanup: true });
|
||||
export const upgradesTmpDir = tmp.dirSync({ dir: tmpDir.name, prefix: 'upgrades_', keep: false, unsafeCleanup: true });
|
||||
export const tmpDirDisposal = {
|
||||
@@ -47,51 +47,69 @@ export const tmpDirDisposal = {
|
||||
}
|
||||
};
|
||||
|
||||
// exported for testing
|
||||
export const queriesDir = path.join(tmpDir.name, 'queries');
|
||||
|
||||
/**
|
||||
* A collection of evaluation-time information about a query,
|
||||
* including the query itself, and where we have decided to put
|
||||
* temporary files associated with it, such as the compiled query
|
||||
* output and results.
|
||||
*/
|
||||
export class QueryInfo {
|
||||
private static nextQueryId = 0;
|
||||
|
||||
readonly compiledQueryPath: string;
|
||||
readonly dilPath: string;
|
||||
readonly csvPath: string;
|
||||
readonly resultsPaths: ResultsPaths;
|
||||
readonly dataset: Uri; // guarantee the existence of a well-defined dataset dir at this point
|
||||
readonly queryID: number;
|
||||
export class QueryEvaluationInfo {
|
||||
readonly querySaveDir: string;
|
||||
|
||||
/**
|
||||
* Note that in the {@link FullQueryInfo.slurp} method, we create a QueryEvaluationInfo instance
|
||||
* by explicitly setting the prototype in order to avoid calling this constructor.
|
||||
*/
|
||||
constructor(
|
||||
public readonly program: messages.QlProgram,
|
||||
public readonly dbItem: DatabaseItem,
|
||||
public readonly id: string,
|
||||
public readonly dbItemPath: string,
|
||||
private readonly databaseHasMetadataFile: boolean,
|
||||
public readonly queryDbscheme: string, // the dbscheme file the query expects, based on library path resolution
|
||||
public readonly quickEvalPosition?: messages.Position,
|
||||
public readonly metadata?: QueryMetadata,
|
||||
public readonly templates?: messages.TemplateDefinitions,
|
||||
public readonly templates?: messages.TemplateDefinitions
|
||||
) {
|
||||
this.queryID = QueryInfo.nextQueryId++;
|
||||
this.compiledQueryPath = path.join(tmpDir.name, `compiledQuery${this.queryID}.qlo`);
|
||||
this.dilPath = path.join(tmpDir.name, `results${this.queryID}.dil`);
|
||||
this.csvPath = path.join(tmpDir.name, `results${this.queryID}.csv`);
|
||||
this.resultsPaths = {
|
||||
resultsPath: path.join(tmpDir.name, `results${this.queryID}.bqrs`),
|
||||
interpretedResultsPath: path.join(tmpDir.name, `interpretedResults${this.queryID}.sarif`)
|
||||
this.querySaveDir = path.join(queriesDir, this.id);
|
||||
}
|
||||
|
||||
get dilPath() {
|
||||
return path.join(this.querySaveDir, 'results.dil');
|
||||
}
|
||||
|
||||
get csvPath() {
|
||||
return path.join(this.querySaveDir, 'results.csv');
|
||||
}
|
||||
|
||||
get compiledQueryPath() {
|
||||
return path.join(this.querySaveDir, 'compiledQuery.qlo');
|
||||
}
|
||||
|
||||
get resultsPaths() {
|
||||
return {
|
||||
resultsPath: path.join(this.querySaveDir, 'results.bqrs'),
|
||||
interpretedResultsPath: path.join(this.querySaveDir, 'interpretedResults.sarif'),
|
||||
};
|
||||
if (dbItem.contents === undefined) {
|
||||
throw new Error('Can\'t run query on invalid database.');
|
||||
}
|
||||
this.dataset = dbItem.contents.datasetUri;
|
||||
}
|
||||
|
||||
getSortedResultSetPath(resultSetName: string) {
|
||||
return path.join(this.querySaveDir, `sortedResults-${resultSetName}.bqrs`);
|
||||
}
|
||||
|
||||
async run(
|
||||
qs: qsClient.QueryServerClient,
|
||||
upgradeQlo: string | undefined,
|
||||
availableMlModels: cli.MlModelInfo[],
|
||||
dbItem: DatabaseItem,
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
): Promise<messages.EvaluationResult> {
|
||||
if (!dbItem.contents || dbItem.error) {
|
||||
throw new Error('Can\'t run query on invalid database.');
|
||||
}
|
||||
|
||||
let result: messages.EvaluationResult | null = null;
|
||||
|
||||
const callbackId = qs.registerCallback(res => { result = res; });
|
||||
@@ -109,7 +127,7 @@ export class QueryInfo {
|
||||
timeoutSecs: qs.config.timeoutSecs,
|
||||
};
|
||||
const dataset: messages.Dataset = {
|
||||
dbDir: this.dataset.fsPath,
|
||||
dbDir: dbItem.contents.datasetUri.fsPath,
|
||||
workingSet: 'default'
|
||||
};
|
||||
const params: messages.EvaluateQueriesParams = {
|
||||
@@ -135,6 +153,7 @@ export class QueryInfo {
|
||||
|
||||
async compile(
|
||||
qs: qsClient.QueryServerClient,
|
||||
program: messages.QlProgram,
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
): Promise<messages.CompilationMessage[]> {
|
||||
@@ -157,7 +176,7 @@ export class QueryInfo {
|
||||
extraOptions: {
|
||||
timeoutSecs: qs.config.timeoutSecs
|
||||
},
|
||||
queryToCheck: this.program,
|
||||
queryToCheck: program,
|
||||
resultPath: this.compiledQueryPath,
|
||||
target,
|
||||
};
|
||||
@@ -172,20 +191,22 @@ export class QueryInfo {
|
||||
/**
|
||||
* Holds if this query can in principle produce interpreted results.
|
||||
*/
|
||||
async canHaveInterpretedResults(): Promise<boolean> {
|
||||
const hasMetadataFile = await this.dbItem.hasMetadataFile();
|
||||
if (!hasMetadataFile) {
|
||||
canHaveInterpretedResults(): boolean {
|
||||
if (!this.databaseHasMetadataFile) {
|
||||
void logger.log('Cannot produce interpreted results since the database does not have a .dbinfo or codeql-database.yml file.');
|
||||
return false;
|
||||
}
|
||||
|
||||
const hasKind = !!this.metadata?.kind;
|
||||
if (!hasKind) {
|
||||
void logger.log('Cannot produce interpreted results since the query does not have @kind metadata.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// table is the default query kind. It does not produce interpreted results.
|
||||
// any query kind that is not table can, in principle, produce interpreted results.
|
||||
const isTable = hasKind && this.metadata?.kind === 'table';
|
||||
|
||||
return hasMetadataFile && hasKind && !isTable;
|
||||
return !isTable;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -247,16 +268,21 @@ export class QueryInfo {
|
||||
out.end();
|
||||
}
|
||||
|
||||
async ensureCsvProduced(qs: qsClient.QueryServerClient): Promise<string> {
|
||||
async ensureCsvProduced(qs: qsClient.QueryServerClient, dbm: DatabaseManager): Promise<string> {
|
||||
if (await this.hasCsv()) {
|
||||
return this.csvPath;
|
||||
}
|
||||
|
||||
const dbItem = dbm.findDatabaseItem(Uri.file(this.dbItemPath));
|
||||
if (!dbItem) {
|
||||
throw new Error(`Cannot produce CSV results because database is missing. ${this.dbItemPath}`);
|
||||
}
|
||||
|
||||
let sourceInfo;
|
||||
if (this.dbItem.sourceArchive !== undefined) {
|
||||
if (dbItem.sourceArchive !== undefined) {
|
||||
sourceInfo = {
|
||||
sourceArchive: this.dbItem.sourceArchive.fsPath,
|
||||
sourceLocationPrefix: await this.dbItem.getSourceLocationPrefix(
|
||||
sourceArchive: dbItem.sourceArchive.fsPath,
|
||||
sourceLocationPrefix: await dbItem.getSourceLocationPrefix(
|
||||
qs.cliServer
|
||||
),
|
||||
};
|
||||
@@ -267,12 +293,9 @@ export class QueryInfo {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export interface QueryWithResults {
|
||||
readonly query: QueryInfo;
|
||||
readonly query: QueryEvaluationInfo;
|
||||
readonly result: messages.EvaluationResult;
|
||||
readonly database: DatabaseInfo;
|
||||
readonly options: QueryHistoryItemOptions;
|
||||
readonly logFileLocation?: string;
|
||||
readonly dispose: () => void;
|
||||
}
|
||||
@@ -356,33 +379,33 @@ async function getSelectedPosition(editor: TextEditor, range?: Range): Promise<m
|
||||
async function checkDbschemeCompatibility(
|
||||
cliServer: cli.CodeQLCliServer,
|
||||
qs: qsClient.QueryServerClient,
|
||||
query: QueryInfo,
|
||||
query: QueryEvaluationInfo,
|
||||
qlProgram: messages.QlProgram,
|
||||
dbItem: DatabaseItem,
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
): Promise<void> {
|
||||
const searchPath = getOnDiskWorkspaceFolders();
|
||||
|
||||
if (query.dbItem.contents !== undefined && query.dbItem.contents.dbSchemeUri !== undefined) {
|
||||
const { finalDbscheme } = await cliServer.resolveUpgrades(query.dbItem.contents.dbSchemeUri.fsPath, searchPath, false);
|
||||
if (dbItem.contents?.dbSchemeUri !== undefined) {
|
||||
const { finalDbscheme } = await cliServer.resolveUpgrades(dbItem.contents.dbSchemeUri.fsPath, searchPath, false);
|
||||
const hash = async function(filename: string): Promise<string> {
|
||||
return crypto.createHash('sha256').update(await fs.readFile(filename)).digest('hex');
|
||||
};
|
||||
|
||||
// At this point, we have learned about three dbschemes:
|
||||
|
||||
// query.program.dbschemePath is the dbscheme of the actual
|
||||
// database we're querying.
|
||||
const dbschemeOfDb = await hash(query.program.dbschemePath);
|
||||
// the dbscheme of the actual database we're querying.
|
||||
const dbschemeOfDb = await hash(dbItem.contents.dbSchemeUri.fsPath);
|
||||
|
||||
// query.queryDbScheme is the dbscheme of the query we're
|
||||
// running, including the library we've resolved it to use.
|
||||
// the dbscheme of the query we're running, including the library we've resolved it to use.
|
||||
const dbschemeOfLib = await hash(query.queryDbscheme);
|
||||
|
||||
// info.finalDbscheme is which database we're able to upgrade to
|
||||
// the database we're able to upgrade to
|
||||
const upgradableTo = await hash(finalDbscheme);
|
||||
|
||||
if (upgradableTo != dbschemeOfLib) {
|
||||
reportNoUpgradePath(query);
|
||||
reportNoUpgradePath(qlProgram, query);
|
||||
}
|
||||
|
||||
if (upgradableTo == dbschemeOfLib &&
|
||||
@@ -390,7 +413,7 @@ async function checkDbschemeCompatibility(
|
||||
// Try to upgrade the database
|
||||
await upgradeDatabaseExplicit(
|
||||
qs,
|
||||
query.dbItem,
|
||||
dbItem,
|
||||
progress,
|
||||
token
|
||||
);
|
||||
@@ -398,8 +421,8 @@ async function checkDbschemeCompatibility(
|
||||
}
|
||||
}
|
||||
|
||||
function reportNoUpgradePath(query: QueryInfo) {
|
||||
throw new Error(`Query ${query.program.queryPath} expects database scheme ${query.queryDbscheme}, but the current database has a different scheme, and no database upgrades are available. The current database scheme may be newer than the CodeQL query libraries in your workspace.\n\nPlease try using a newer version of the query libraries.`);
|
||||
function reportNoUpgradePath(qlProgram: messages.QlProgram, query: QueryEvaluationInfo): void {
|
||||
throw new Error(`Query ${qlProgram.queryPath} expects database scheme ${query.queryDbscheme}, but the current database has a different scheme, and no database upgrades are available. The current database scheme may be newer than the CodeQL query libraries in your workspace.\n\nPlease try using a newer version of the query libraries.`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -408,27 +431,29 @@ function reportNoUpgradePath(query: QueryInfo) {
|
||||
async function compileNonDestructiveUpgrade(
|
||||
qs: qsClient.QueryServerClient,
|
||||
upgradeTemp: tmp.DirectoryResult,
|
||||
query: QueryInfo,
|
||||
query: QueryEvaluationInfo,
|
||||
qlProgram: messages.QlProgram,
|
||||
dbItem: DatabaseItem,
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
): Promise<string> {
|
||||
const searchPath = getOnDiskWorkspaceFolders();
|
||||
|
||||
if (!query.dbItem?.contents?.dbSchemeUri) {
|
||||
if (!dbItem?.contents?.dbSchemeUri) {
|
||||
throw new Error('Database is invalid, and cannot be upgraded.');
|
||||
}
|
||||
const { scripts, matchesTarget } = await qs.cliServer.resolveUpgrades(query.dbItem.contents.dbSchemeUri.fsPath, searchPath, true, query.queryDbscheme);
|
||||
const { scripts, matchesTarget } = await qs.cliServer.resolveUpgrades(dbItem.contents.dbSchemeUri.fsPath, searchPath, true, query.queryDbscheme);
|
||||
|
||||
if (!matchesTarget) {
|
||||
reportNoUpgradePath(query);
|
||||
reportNoUpgradePath(qlProgram, query);
|
||||
}
|
||||
const result = await compileDatabaseUpgradeSequence(qs, query.dbItem, scripts, upgradeTemp, progress, token);
|
||||
const result = await compileDatabaseUpgradeSequence(qs, dbItem, scripts, upgradeTemp, progress, token);
|
||||
if (result.compiledUpgrade === undefined) {
|
||||
const error = result.error || '[no error message available]';
|
||||
throw new Error(error);
|
||||
}
|
||||
// We can upgrade to the actual target
|
||||
query.program.dbschemePath = query.queryDbscheme;
|
||||
qlProgram.dbschemePath = query.queryDbscheme;
|
||||
// We are new enough that we will always support single file upgrades.
|
||||
return result.compiledUpgrade;
|
||||
|
||||
@@ -513,14 +538,13 @@ export async function determineSelectedQuery(selectedResourceUri: Uri | undefine
|
||||
if (queryUri.scheme !== 'file') {
|
||||
throw new Error('Can only run queries that are on disk.');
|
||||
}
|
||||
const queryPath = queryUri.fsPath || '';
|
||||
const queryPath = queryUri.fsPath;
|
||||
|
||||
if (quickEval) {
|
||||
if (!(queryPath.endsWith('.ql') || queryPath.endsWith('.qll'))) {
|
||||
throw new Error('The selected resource is not a CodeQL file; It should have the extension ".ql" or ".qll".');
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
if (!(queryPath.endsWith('.ql'))) {
|
||||
throw new Error('The selected resource is not a CodeQL query file; It should have the extension ".ql".');
|
||||
}
|
||||
@@ -547,7 +571,13 @@ export async function determineSelectedQuery(selectedResourceUri: Uri | undefine
|
||||
throw new Error('The selected resource for quick evaluation should match the active editor.');
|
||||
}
|
||||
quickEvalPosition = await getSelectedPosition(editor, range);
|
||||
quickEvalText = editor.document.getText(editor.selection);
|
||||
if (!editor.selection?.isEmpty) {
|
||||
quickEvalText = editor.document.getText(editor.selection);
|
||||
} else {
|
||||
// capture the entire line if the user didn't select anything
|
||||
const line = editor.document.lineAt(editor.selection.active.line);
|
||||
quickEvalText = line.text.trim();
|
||||
}
|
||||
}
|
||||
|
||||
return { queryPath, quickEvalPosition, quickEvalText };
|
||||
@@ -556,33 +586,20 @@ export async function determineSelectedQuery(selectedResourceUri: Uri | undefine
|
||||
export async function compileAndRunQueryAgainstDatabase(
|
||||
cliServer: cli.CodeQLCliServer,
|
||||
qs: qsClient.QueryServerClient,
|
||||
db: DatabaseItem,
|
||||
quickEval: boolean,
|
||||
selectedQueryUri: Uri | undefined,
|
||||
dbItem: DatabaseItem,
|
||||
initialInfo: InitialQueryInfo,
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
templates?: messages.TemplateDefinitions,
|
||||
range?: Range
|
||||
): Promise<QueryWithResults> {
|
||||
if (!db.contents || !db.contents.dbSchemeUri) {
|
||||
throw new Error(`Database ${db.databaseUri} does not have a CodeQL database scheme.`);
|
||||
}
|
||||
|
||||
// Determine which query to run, based on the selection and the active editor.
|
||||
const { queryPath, quickEvalPosition, quickEvalText } = await determineSelectedQuery(selectedQueryUri, quickEval, range);
|
||||
|
||||
const historyItemOptions: QueryHistoryItemOptions = {};
|
||||
historyItemOptions.isQuickQuery === isQuickQueryPath(queryPath);
|
||||
if (quickEval) {
|
||||
historyItemOptions.queryText = quickEvalText;
|
||||
} else {
|
||||
historyItemOptions.queryText = await fs.readFile(queryPath, 'utf8');
|
||||
if (!dbItem.contents || !dbItem.contents.dbSchemeUri) {
|
||||
throw new Error(`Database ${dbItem.databaseUri} does not have a CodeQL database scheme.`);
|
||||
}
|
||||
|
||||
// Get the workspace folder paths.
|
||||
const diskWorkspaceFolders = getOnDiskWorkspaceFolders();
|
||||
// Figure out the library path for the query.
|
||||
const packConfig = await cliServer.resolveLibraryPath(diskWorkspaceFolders, queryPath);
|
||||
const packConfig = await cliServer.resolveLibraryPath(diskWorkspaceFolders, initialInfo.queryPath);
|
||||
|
||||
if (!packConfig.dbscheme) {
|
||||
throw new Error('Could not find a database scheme for this query. Please check that you have a valid qlpack.yml file for this query, which refers to a database scheme either in the `dbscheme` field or through one of its dependencies.');
|
||||
@@ -593,10 +610,10 @@ export async function compileAndRunQueryAgainstDatabase(
|
||||
// won't trigger this check)
|
||||
// This test will produce confusing results if we ever change the name of the database schema files.
|
||||
const querySchemaName = path.basename(packConfig.dbscheme);
|
||||
const dbSchemaName = path.basename(db.contents.dbSchemeUri.fsPath);
|
||||
const dbSchemaName = path.basename(dbItem.contents.dbSchemeUri.fsPath);
|
||||
if (querySchemaName != dbSchemaName) {
|
||||
void logger.log(`Query schema was ${querySchemaName}, but database schema was ${dbSchemaName}.`);
|
||||
throw new Error(`The query ${path.basename(queryPath)} cannot be run against the selected database (${db.name}): their target languages are different. Please select a different database and try again.`);
|
||||
throw new Error(`The query ${path.basename(initialInfo.queryPath)} cannot be run against the selected database (${dbItem.name}): their target languages are different. Please select a different database and try again.`);
|
||||
}
|
||||
|
||||
const qlProgram: messages.QlProgram = {
|
||||
@@ -607,8 +624,8 @@ export async function compileAndRunQueryAgainstDatabase(
|
||||
// Since we are compiling and running a query against a database,
|
||||
// we use the database's DB scheme here instead of the DB scheme
|
||||
// from the current document's project.
|
||||
dbschemePath: db.contents.dbSchemeUri.fsPath,
|
||||
queryPath: queryPath
|
||||
dbschemePath: dbItem.contents.dbSchemeUri.fsPath,
|
||||
queryPath: initialInfo.queryPath
|
||||
};
|
||||
|
||||
// Read the query metadata if possible, to use in the UI.
|
||||
@@ -632,29 +649,38 @@ export async function compileAndRunQueryAgainstDatabase(
|
||||
}
|
||||
}
|
||||
|
||||
const query = new QueryInfo(qlProgram, db, packConfig.dbscheme, quickEvalPosition, metadata, templates);
|
||||
const hasMetadataFile = (await dbItem.hasMetadataFile());
|
||||
const query = new QueryEvaluationInfo(
|
||||
initialInfo.id,
|
||||
dbItem.databaseUri.fsPath,
|
||||
hasMetadataFile,
|
||||
packConfig.dbscheme,
|
||||
initialInfo.quickEvalPosition,
|
||||
metadata,
|
||||
templates
|
||||
);
|
||||
|
||||
const upgradeDir = await tmp.dir({ dir: upgradesTmpDir.name, unsafeCleanup: true });
|
||||
try {
|
||||
let upgradeQlo;
|
||||
if (await hasNondestructiveUpgradeCapabilities(qs)) {
|
||||
upgradeQlo = await compileNonDestructiveUpgrade(qs, upgradeDir, query, progress, token);
|
||||
upgradeQlo = await compileNonDestructiveUpgrade(qs, upgradeDir, query, qlProgram, dbItem, progress, token);
|
||||
} else {
|
||||
await checkDbschemeCompatibility(cliServer, qs, query, progress, token);
|
||||
await checkDbschemeCompatibility(cliServer, qs, query, qlProgram, dbItem, progress, token);
|
||||
}
|
||||
let errors;
|
||||
try {
|
||||
errors = await query.compile(qs, progress, token);
|
||||
errors = await query.compile(qs, qlProgram, progress, token);
|
||||
} catch (e) {
|
||||
if (e instanceof ResponseError && e.code == ErrorCodes.RequestCancelled) {
|
||||
return createSyntheticResult(query, db, historyItemOptions, 'Query cancelled', messages.QueryResultType.CANCELLATION);
|
||||
return createSyntheticResult(query, 'Query cancelled', messages.QueryResultType.CANCELLATION);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length === 0) {
|
||||
const result = await query.run(qs, upgradeQlo, availableMlModels, progress, token);
|
||||
const result = await query.run(qs, upgradeQlo, availableMlModels, dbItem, progress, token);
|
||||
if (result.resultType !== messages.QueryResultType.SUCCESS) {
|
||||
const message = result.message || 'Failed to run query';
|
||||
void logger.log(message);
|
||||
@@ -663,11 +689,6 @@ export async function compileAndRunQueryAgainstDatabase(
|
||||
return {
|
||||
query,
|
||||
result,
|
||||
database: {
|
||||
name: db.name,
|
||||
databaseUri: db.databaseUri.toString(true)
|
||||
},
|
||||
options: historyItemOptions,
|
||||
logFileLocation: result.logFileLocation,
|
||||
dispose: () => {
|
||||
qs.logger.removeAdditionalLogLocation(result.logFileLocation);
|
||||
@@ -678,7 +699,7 @@ export async function compileAndRunQueryAgainstDatabase(
|
||||
// so we include a general description of the problem,
|
||||
// and direct the user to the output window for the detailed compilation messages.
|
||||
// However we don't show quick eval errors there so we need to display them anyway.
|
||||
void qs.logger.log(`Failed to compile query ${query.program.queryPath} against database scheme ${query.program.dbschemePath}:`);
|
||||
void qs.logger.log(`Failed to compile query ${initialInfo.queryPath} against database scheme ${qlProgram.dbschemePath}:`);
|
||||
|
||||
const formattedMessages: string[] = [];
|
||||
|
||||
@@ -688,16 +709,16 @@ export async function compileAndRunQueryAgainstDatabase(
|
||||
formattedMessages.push(formatted);
|
||||
void qs.logger.log(formatted);
|
||||
}
|
||||
if (quickEval && formattedMessages.length <= 2) {
|
||||
if (initialInfo.isQuickEval && formattedMessages.length <= 2) {
|
||||
// If there are more than 2 error messages, they will not be displayed well in a popup
|
||||
// and will be trimmed by the function displaying the error popup. Accordingly, we only
|
||||
// try to show the errors if there are 2 or less, otherwise we direct the user to the log.
|
||||
void showAndLogErrorMessage('Quick evaluation compilation failed: ' + formattedMessages.join('\n'));
|
||||
} else {
|
||||
void showAndLogErrorMessage((quickEval ? 'Quick evaluation' : 'Query') + compilationFailedErrorTail);
|
||||
void showAndLogErrorMessage((initialInfo.isQuickEval ? 'Quick evaluation' : 'Query') + compilationFailedErrorTail);
|
||||
}
|
||||
|
||||
return createSyntheticResult(query, db, historyItemOptions, 'Query had compilation errors', messages.QueryResultType.OTHER_ERROR);
|
||||
return createSyntheticResult(query, 'Query had compilation errors', messages.QueryResultType.OTHER_ERROR);
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
@@ -708,14 +729,38 @@ export async function compileAndRunQueryAgainstDatabase(
|
||||
}
|
||||
}
|
||||
|
||||
export async function createInitialQueryInfo(
|
||||
selectedQueryUri: Uri | undefined,
|
||||
databaseInfo: DatabaseInfo,
|
||||
isQuickEval: boolean,
|
||||
range?: Range
|
||||
): Promise<InitialQueryInfo> {
|
||||
// Determine which query to run, based on the selection and the active editor.
|
||||
const { queryPath, quickEvalPosition, quickEvalText } = await determineSelectedQuery(selectedQueryUri, isQuickEval, range);
|
||||
|
||||
return {
|
||||
queryPath,
|
||||
isQuickEval,
|
||||
isQuickQuery: isQuickQueryPath(queryPath),
|
||||
databaseInfo,
|
||||
id: `${path.basename(queryPath)}-${nanoid()}`,
|
||||
start: new Date(),
|
||||
... (isQuickEval ? {
|
||||
queryText: quickEvalText!, // if this query is quick eval, it must have quick eval text
|
||||
quickEvalPosition: quickEvalPosition
|
||||
} : {
|
||||
queryText: await fs.readFile(queryPath, 'utf8')
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
const compilationFailedErrorTail = ' compilation failed. Please make sure there are no errors in the query, the database is up to date,' +
|
||||
' and the query and database use the same target language. For more details on the error, go to View > Output,' +
|
||||
' and choose CodeQL Query Server from the dropdown.';
|
||||
|
||||
function createSyntheticResult(
|
||||
query: QueryInfo,
|
||||
db: DatabaseItem,
|
||||
historyItemOptions: QueryHistoryItemOptions,
|
||||
query: QueryEvaluationInfo,
|
||||
message: string,
|
||||
resultType: number
|
||||
): QueryWithResults {
|
||||
@@ -729,11 +774,6 @@ function createSyntheticResult(
|
||||
runId: -1,
|
||||
message
|
||||
},
|
||||
database: {
|
||||
name: db.name,
|
||||
databaseUri: db.databaseUri.toString(true)
|
||||
},
|
||||
options: historyItemOptions,
|
||||
dispose: () => { /**/ },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -35,13 +35,13 @@ export async function hasNondestructiveUpgradeCapabilities(qs: qsClient.QuerySer
|
||||
*/
|
||||
export async function compileDatabaseUpgradeSequence(
|
||||
qs: qsClient.QueryServerClient,
|
||||
db: DatabaseItem,
|
||||
dbItem: DatabaseItem,
|
||||
resolvedSequence: string[],
|
||||
currentUpgradeTmp: tmp.DirectoryResult,
|
||||
progress: ProgressCallback,
|
||||
token: vscode.CancellationToken
|
||||
): Promise<messages.CompileUpgradeSequenceResult> {
|
||||
if (db.contents === undefined || db.contents.dbSchemeUri === undefined) {
|
||||
if (dbItem.contents === undefined || dbItem.contents.dbSchemeUri === undefined) {
|
||||
throw new Error('Database is invalid, and cannot be upgraded.');
|
||||
}
|
||||
if (!await hasNondestructiveUpgradeCapabilities(qs)) {
|
||||
@@ -56,14 +56,14 @@ export async function compileDatabaseUpgradeSequence(
|
||||
|
||||
async function compileDatabaseUpgrade(
|
||||
qs: qsClient.QueryServerClient,
|
||||
db: DatabaseItem,
|
||||
dbItem: DatabaseItem,
|
||||
targetDbScheme: string,
|
||||
resolvedSequence: string[],
|
||||
currentUpgradeTmp: tmp.DirectoryResult,
|
||||
progress: ProgressCallback,
|
||||
token: vscode.CancellationToken
|
||||
): Promise<messages.CompileUpgradeResult> {
|
||||
if (!db.contents?.dbSchemeUri) {
|
||||
if (!dbItem.contents?.dbSchemeUri) {
|
||||
throw new Error('Database is invalid, and cannot be upgraded.');
|
||||
}
|
||||
// We have the upgrades we want but compileUpgrade
|
||||
@@ -78,7 +78,7 @@ async function compileDatabaseUpgrade(
|
||||
});
|
||||
return qs.sendRequest(messages.compileUpgrade, {
|
||||
upgrade: {
|
||||
fromDbscheme: db.contents.dbSchemeUri.fsPath,
|
||||
fromDbscheme: dbItem.contents.dbSchemeUri.fsPath,
|
||||
toDbscheme: targetDbScheme,
|
||||
additionalUpgrades: Array.from(uniqueParentDirs)
|
||||
},
|
||||
@@ -159,18 +159,18 @@ function getUpgradeDescriptions(compiled: messages.CompiledUpgrades): messages.U
|
||||
*/
|
||||
export async function upgradeDatabaseExplicit(
|
||||
qs: qsClient.QueryServerClient,
|
||||
db: DatabaseItem,
|
||||
dbItem: DatabaseItem,
|
||||
progress: ProgressCallback,
|
||||
token: vscode.CancellationToken,
|
||||
): Promise<messages.RunUpgradeResult | undefined> {
|
||||
|
||||
const searchPath: string[] = getOnDiskWorkspaceFolders();
|
||||
|
||||
if (!db?.contents?.dbSchemeUri) {
|
||||
if (!dbItem?.contents?.dbSchemeUri) {
|
||||
throw new Error('Database is invalid, and cannot be upgraded.');
|
||||
}
|
||||
const upgradeInfo = await qs.cliServer.resolveUpgrades(
|
||||
db.contents.dbSchemeUri.fsPath,
|
||||
dbItem.contents.dbSchemeUri.fsPath,
|
||||
searchPath,
|
||||
false
|
||||
);
|
||||
@@ -184,7 +184,7 @@ export async function upgradeDatabaseExplicit(
|
||||
try {
|
||||
let compileUpgradeResult: messages.CompileUpgradeResult;
|
||||
try {
|
||||
compileUpgradeResult = await compileDatabaseUpgrade(qs, db, finalDbscheme, scripts, currentUpgradeTmp, progress, token);
|
||||
compileUpgradeResult = await compileDatabaseUpgrade(qs, dbItem, finalDbscheme, scripts, currentUpgradeTmp, progress, token);
|
||||
}
|
||||
catch (e) {
|
||||
void showAndLogErrorMessage(`Compilation of database upgrades failed: ${e}`);
|
||||
@@ -200,13 +200,13 @@ export async function upgradeDatabaseExplicit(
|
||||
return;
|
||||
}
|
||||
|
||||
await checkAndConfirmDatabaseUpgrade(compileUpgradeResult.compiledUpgrades, db, qs.cliServer.quiet);
|
||||
await checkAndConfirmDatabaseUpgrade(compileUpgradeResult.compiledUpgrades, dbItem, qs.cliServer.quiet);
|
||||
|
||||
try {
|
||||
void qs.logger.log('Running the following database upgrade:');
|
||||
|
||||
getUpgradeDescriptions(compileUpgradeResult.compiledUpgrades).map(s => s.description).join('\n');
|
||||
return await runDatabaseUpgrade(qs, db, compileUpgradeResult.compiledUpgrades, progress, token);
|
||||
return await runDatabaseUpgrade(qs, dbItem, compileUpgradeResult.compiledUpgrades, progress, token);
|
||||
}
|
||||
catch (e) {
|
||||
void showAndLogErrorMessage(`Database upgrade failed: ${e}`);
|
||||
|
||||
@@ -11,7 +11,7 @@ import { DatabaseItem, DatabaseManager } from '../../databases';
|
||||
import { CodeQLExtensionInterface } from '../../extension';
|
||||
import { dbLoc, storagePath } from './global.helper';
|
||||
import { importArchiveDatabase } from '../../databaseFetcher';
|
||||
import { compileAndRunQueryAgainstDatabase } from '../../run-queries';
|
||||
import { compileAndRunQueryAgainstDatabase, createInitialQueryInfo } from '../../run-queries';
|
||||
import { CodeQLCliServer } from '../../cli';
|
||||
import { QueryServerClient } from '../../queryserver-client';
|
||||
import { skipIfNoCodeQL } from '../ensureCli';
|
||||
@@ -96,15 +96,12 @@ describe('Queries', function() {
|
||||
cli,
|
||||
qs,
|
||||
dbItem,
|
||||
false,
|
||||
Uri.file(queryPath),
|
||||
await mockInitialQueryInfo(queryPath),
|
||||
progress,
|
||||
token
|
||||
);
|
||||
|
||||
// just check that the query was successful
|
||||
expect(result.database.name).to.eq('db');
|
||||
expect(result.options.queryText).to.eq(fs.readFileSync(queryPath, 'utf8'));
|
||||
expect(result.result.resultType).to.eq(QueryResultType.SUCCESS);
|
||||
} catch (e) {
|
||||
console.error('Test Failed');
|
||||
@@ -121,15 +118,13 @@ describe('Queries', function() {
|
||||
cli,
|
||||
qs,
|
||||
dbItem,
|
||||
false,
|
||||
Uri.file(queryPath),
|
||||
await mockInitialQueryInfo(queryPath),
|
||||
progress,
|
||||
token
|
||||
);
|
||||
|
||||
// this message would indicate that the databases were not properly reregistered
|
||||
expect(result.result.message).not.to.eq('No result from server');
|
||||
expect(result.options.queryText).to.eq(fs.readFileSync(queryPath, 'utf8'));
|
||||
expect(result.result.resultType).to.eq(QueryResultType.SUCCESS);
|
||||
} catch (e) {
|
||||
console.error('Test Failed');
|
||||
@@ -174,4 +169,15 @@ describe('Queries', function() {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
async function mockInitialQueryInfo(queryPath: string) {
|
||||
return await createInitialQueryInfo(
|
||||
Uri.file(queryPath),
|
||||
{
|
||||
name: dbItem.name,
|
||||
databaseUri: dbItem.databaseUri.toString(),
|
||||
},
|
||||
false
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -44,7 +44,7 @@ const _10MB = _1MB * 10;
|
||||
|
||||
// CLI version to test. Hard code the latest as default. And be sure
|
||||
// to update the env if it is not otherwise set.
|
||||
const CLI_VERSION = process.env.CLI_VERSION || 'v2.7.6';
|
||||
const CLI_VERSION = process.env.CLI_VERSION || 'v2.8.0';
|
||||
process.env.CLI_VERSION = CLI_VERSION;
|
||||
|
||||
// Base dir where CLIs will be downloaded into
|
||||
|
||||
@@ -192,6 +192,9 @@ describe('helpers', () => {
|
||||
}
|
||||
|
||||
class MockMemento implements Memento {
|
||||
keys(): readonly string[] {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
map = new Map<any, any>();
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,9 +5,13 @@ import * as vscode from 'vscode';
|
||||
import * as sinon from 'sinon';
|
||||
import * as chaiAsPromised from 'chai-as-promised';
|
||||
import { logger } from '../../logging';
|
||||
import { QueryHistoryManager, HistoryTreeDataProvider } from '../../query-history';
|
||||
import { CompletedQuery } from '../../query-results';
|
||||
import { QueryInfo } from '../../run-queries';
|
||||
import { QueryHistoryManager, HistoryTreeDataProvider, SortOrder } from '../../query-history';
|
||||
import { QueryEvaluationInfo, QueryWithResults } from '../../run-queries';
|
||||
import { QueryHistoryConfigListener } from '../../config';
|
||||
import * as messages from '../../pure/messages';
|
||||
import { QueryServerClient } from '../../queryserver-client';
|
||||
import { FullQueryInfo, InitialQueryInfo } from '../../query-results';
|
||||
import { DatabaseManager } from '../../databases';
|
||||
|
||||
chai.use(chaiAsPromised);
|
||||
const expect = chai.expect;
|
||||
@@ -15,10 +19,14 @@ const assert = chai.assert;
|
||||
|
||||
|
||||
describe('query-history', () => {
|
||||
let configListener: QueryHistoryConfigListener;
|
||||
let showTextDocumentSpy: sinon.SinonStub;
|
||||
let showInformationMessageSpy: sinon.SinonStub;
|
||||
let executeCommandSpy: sinon.SinonStub;
|
||||
let showQuickPickSpy: sinon.SinonStub;
|
||||
let queryHistoryManager: QueryHistoryManager | undefined;
|
||||
let selectedCallback: sinon.SinonStub;
|
||||
let doCompareCallback: sinon.SinonStub;
|
||||
|
||||
let tryOpenExternalFile: Function;
|
||||
let sandbox: sinon.SinonSandbox;
|
||||
@@ -38,9 +46,16 @@ describe('query-history', () => {
|
||||
executeCommandSpy = sandbox.stub(vscode.commands, 'executeCommand');
|
||||
sandbox.stub(logger, 'log');
|
||||
tryOpenExternalFile = (QueryHistoryManager.prototype as any).tryOpenExternalFile;
|
||||
configListener = new QueryHistoryConfigListener();
|
||||
selectedCallback = sandbox.stub();
|
||||
doCompareCallback = sandbox.stub();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
afterEach(async () => {
|
||||
if (queryHistoryManager) {
|
||||
queryHistoryManager.dispose();
|
||||
queryHistoryManager = undefined;
|
||||
}
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
@@ -85,24 +100,24 @@ describe('query-history', () => {
|
||||
});
|
||||
});
|
||||
|
||||
let allHistory: FullQueryInfo[];
|
||||
|
||||
beforeEach(() => {
|
||||
allHistory = [
|
||||
createMockFullQueryInfo('a', createMockQueryWithResults(true)),
|
||||
createMockFullQueryInfo('b', createMockQueryWithResults(true)),
|
||||
createMockFullQueryInfo('a', createMockQueryWithResults(false)),
|
||||
createMockFullQueryInfo('a', createMockQueryWithResults(true)),
|
||||
];
|
||||
});
|
||||
|
||||
describe('findOtherQueryToCompare', () => {
|
||||
let allHistory: { database: { name: string }; didRunSuccessfully: boolean }[];
|
||||
|
||||
beforeEach(() => {
|
||||
allHistory = [
|
||||
{ didRunSuccessfully: true, database: { name: 'a' } },
|
||||
{ didRunSuccessfully: true, database: { name: 'b' } },
|
||||
{ didRunSuccessfully: false, database: { name: 'a' } },
|
||||
{ didRunSuccessfully: true, database: { name: 'a' } },
|
||||
];
|
||||
});
|
||||
|
||||
it('should find the second query to compare when one is selected', async () => {
|
||||
const thisQuery = allHistory[3];
|
||||
const queryHistory = createMockQueryHistory(allHistory);
|
||||
queryHistoryManager = await createMockQueryHistory(allHistory);
|
||||
showQuickPickSpy.returns({ query: allHistory[0] });
|
||||
|
||||
const otherQuery = await queryHistory.findOtherQueryToCompare(thisQuery, []);
|
||||
const otherQuery = await (queryHistoryManager as any).findOtherQueryToCompare(thisQuery, []);
|
||||
expect(otherQuery).to.eq(allHistory[0]);
|
||||
|
||||
// only called with first item, other items filtered out
|
||||
@@ -112,9 +127,9 @@ describe('query-history', () => {
|
||||
|
||||
it('should handle cancelling out of the quick select', async () => {
|
||||
const thisQuery = allHistory[3];
|
||||
const queryHistory = createMockQueryHistory(allHistory);
|
||||
queryHistoryManager = await createMockQueryHistory(allHistory);
|
||||
|
||||
const otherQuery = await queryHistory.findOtherQueryToCompare(thisQuery, []);
|
||||
const otherQuery = await (queryHistoryManager as any).findOtherQueryToCompare(thisQuery, []);
|
||||
expect(otherQuery).to.be.undefined;
|
||||
|
||||
// only called with first item, other items filtered out
|
||||
@@ -124,20 +139,20 @@ describe('query-history', () => {
|
||||
|
||||
it('should compare against 2 queries', async () => {
|
||||
const thisQuery = allHistory[3];
|
||||
const queryHistory = createMockQueryHistory(allHistory);
|
||||
queryHistoryManager = await createMockQueryHistory(allHistory);
|
||||
|
||||
const otherQuery = await queryHistory.findOtherQueryToCompare(thisQuery, [thisQuery, allHistory[0]]);
|
||||
const otherQuery = await (queryHistoryManager as any).findOtherQueryToCompare(thisQuery, [thisQuery, allHistory[0]]);
|
||||
expect(otherQuery).to.eq(allHistory[0]);
|
||||
expect(showQuickPickSpy).not.to.have.been.called;
|
||||
});
|
||||
|
||||
it('should throw an error when a query is not successful', async () => {
|
||||
const thisQuery = allHistory[3];
|
||||
const queryHistory = createMockQueryHistory(allHistory);
|
||||
allHistory[0].didRunSuccessfully = false;
|
||||
queryHistoryManager = await createMockQueryHistory(allHistory);
|
||||
allHistory[0] = createMockFullQueryInfo('a', createMockQueryWithResults(false));
|
||||
|
||||
try {
|
||||
await queryHistory.findOtherQueryToCompare(thisQuery, [thisQuery, allHistory[0]]);
|
||||
await (queryHistoryManager as any).findOtherQueryToCompare(thisQuery, [thisQuery, allHistory[0]]);
|
||||
assert(false, 'Should have thrown');
|
||||
} catch (e) {
|
||||
expect(e.message).to.eq('Please select a successful query.');
|
||||
@@ -145,12 +160,12 @@ describe('query-history', () => {
|
||||
});
|
||||
|
||||
it('should throw an error when a databases are not the same', async () => {
|
||||
const thisQuery = allHistory[3];
|
||||
const queryHistory = createMockQueryHistory(allHistory);
|
||||
allHistory[0].database.name = 'c';
|
||||
queryHistoryManager = await createMockQueryHistory(allHistory);
|
||||
|
||||
try {
|
||||
await queryHistory.findOtherQueryToCompare(thisQuery, [thisQuery, allHistory[0]]);
|
||||
// allHistory[0] is database a
|
||||
// allHistory[1] is database b
|
||||
await (queryHistoryManager as any).findOtherQueryToCompare(allHistory[0], [allHistory[0], allHistory[1]]);
|
||||
assert(false, 'Should have thrown');
|
||||
} catch (e) {
|
||||
expect(e.message).to.eq('Query databases must be the same.');
|
||||
@@ -159,10 +174,10 @@ describe('query-history', () => {
|
||||
|
||||
it('should throw an error when more than 2 queries selected', async () => {
|
||||
const thisQuery = allHistory[3];
|
||||
const queryHistory = createMockQueryHistory(allHistory);
|
||||
queryHistoryManager = await createMockQueryHistory(allHistory);
|
||||
|
||||
try {
|
||||
await queryHistory.findOtherQueryToCompare(thisQuery, [thisQuery, allHistory[0], allHistory[1]]);
|
||||
await (queryHistoryManager as any).findOtherQueryToCompare(thisQuery, [thisQuery, allHistory[0], allHistory[1]]);
|
||||
assert(false, 'Should have thrown');
|
||||
} catch (e) {
|
||||
expect(e.message).to.eq('Please select no more than 2 queries.');
|
||||
@@ -170,39 +185,127 @@ describe('query-history', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleItemClicked', () => {
|
||||
it('should call the selectedCallback when an item is clicked', async () => {
|
||||
queryHistoryManager = await createMockQueryHistory(allHistory);
|
||||
await queryHistoryManager.handleItemClicked(allHistory[0], [allHistory[0]]);
|
||||
expect(selectedCallback).to.have.been.calledOnceWith(allHistory[0]);
|
||||
expect(queryHistoryManager.treeDataProvider.getCurrent()).to.eq(allHistory[0]);
|
||||
});
|
||||
|
||||
it('should do nothing if there is a multi-selection', async () => {
|
||||
queryHistoryManager = await createMockQueryHistory(allHistory);
|
||||
await queryHistoryManager.handleItemClicked(allHistory[0], [allHistory[0], allHistory[1]]);
|
||||
expect(selectedCallback).not.to.have.been.called;
|
||||
expect(queryHistoryManager.treeDataProvider.getCurrent()).to.be.undefined;
|
||||
});
|
||||
|
||||
it('should throw if there is no selection', async () => {
|
||||
queryHistoryManager = await createMockQueryHistory(allHistory);
|
||||
try {
|
||||
await queryHistoryManager.handleItemClicked(undefined!, []);
|
||||
expect(true).to.be.false;
|
||||
} catch (e) {
|
||||
expect(selectedCallback).not.to.have.been.called;
|
||||
expect(e.message).to.contain('No query selected');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove an item and not select a new one', async () => {
|
||||
queryHistoryManager = await createMockQueryHistory(allHistory);
|
||||
// initialize the selection
|
||||
await queryHistoryManager.treeView.reveal(allHistory[0], { select: true });
|
||||
|
||||
// deleting the first item when a different item is selected
|
||||
// will not change the selection
|
||||
const toDelete = allHistory[1];
|
||||
const selected = allHistory[3];
|
||||
|
||||
// select the item we want
|
||||
await queryHistoryManager.treeView.reveal(selected, { select: true });
|
||||
|
||||
// should be selected
|
||||
expect(queryHistoryManager.treeDataProvider.getCurrent()).to.deep.eq(selected);
|
||||
|
||||
// remove an item
|
||||
await queryHistoryManager.handleRemoveHistoryItem(toDelete, [toDelete]);
|
||||
|
||||
expect(toDelete.completedQuery!.dispose).to.have.been.calledOnce;
|
||||
expect(queryHistoryManager.treeDataProvider.getCurrent()).to.deep.eq(selected);
|
||||
expect(queryHistoryManager.treeDataProvider.allHistory).not.to.contain(toDelete);
|
||||
|
||||
// the same item should be selected
|
||||
expect(selectedCallback).to.have.been.calledOnceWith(selected);
|
||||
});
|
||||
|
||||
it('should remove an item and select a new one', async () => {
|
||||
queryHistoryManager = await createMockQueryHistory(allHistory);
|
||||
|
||||
// deleting the selected item automatically selects next item
|
||||
const toDelete = allHistory[1];
|
||||
const newSelected = allHistory[2];
|
||||
// avoid triggering the callback by setting the field directly
|
||||
|
||||
// select the item we want
|
||||
await queryHistoryManager.treeView.reveal(toDelete, { select: true });
|
||||
await queryHistoryManager.handleRemoveHistoryItem(toDelete, [toDelete]);
|
||||
|
||||
expect(toDelete.completedQuery!.dispose).to.have.been.calledOnce;
|
||||
expect(queryHistoryManager.treeDataProvider.getCurrent()).to.eq(newSelected);
|
||||
expect(queryHistoryManager.treeDataProvider.allHistory).not.to.contain(toDelete);
|
||||
|
||||
// the current item should have been selected
|
||||
expect(selectedCallback).to.have.been.calledOnceWith(newSelected);
|
||||
});
|
||||
|
||||
describe('Compare callback', () => {
|
||||
it('should call the compare callback', async () => {
|
||||
queryHistoryManager = await createMockQueryHistory(allHistory);
|
||||
await queryHistoryManager.handleCompareWith(allHistory[0], [allHistory[0], allHistory[3]]);
|
||||
expect(doCompareCallback).to.have.been.calledOnceWith(allHistory[0], allHistory[3]);
|
||||
});
|
||||
|
||||
it('should avoid calling the compare callback when only one item is selected', async () => {
|
||||
queryHistoryManager = await createMockQueryHistory(allHistory);
|
||||
await queryHistoryManager.handleCompareWith(allHistory[0], [allHistory[0]]);
|
||||
expect(doCompareCallback).not.to.have.been.called;
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateCompareWith', () => {
|
||||
it('should update compareWithItem when there is a single item', () => {
|
||||
const queryHistory = createMockQueryHistory([]);
|
||||
queryHistory.updateCompareWith(['a']);
|
||||
expect(queryHistory.compareWithItem).to.be.eq('a');
|
||||
it('should update compareWithItem when there is a single item', async () => {
|
||||
queryHistoryManager = await createMockQueryHistory([]);
|
||||
(queryHistoryManager as any).updateCompareWith(['a']);
|
||||
expect(queryHistoryManager.compareWithItem).to.be.eq('a');
|
||||
});
|
||||
|
||||
it('should delete compareWithItem when there are 0 items', () => {
|
||||
const queryHistory = createMockQueryHistory([]);
|
||||
queryHistory.compareWithItem = 'a';
|
||||
queryHistory.updateCompareWith([]);
|
||||
expect(queryHistory.compareWithItem).to.be.undefined;
|
||||
it('should delete compareWithItem when there are 0 items', async () => {
|
||||
queryHistoryManager = await createMockQueryHistory([]);
|
||||
queryHistoryManager.compareWithItem = allHistory[0];
|
||||
(queryHistoryManager as any).updateCompareWith([]);
|
||||
expect(queryHistoryManager.compareWithItem).to.be.undefined;
|
||||
});
|
||||
|
||||
it('should delete compareWithItem when there are more than 2 items', () => {
|
||||
const queryHistory = createMockQueryHistory([]);
|
||||
queryHistory.compareWithItem = 'a';
|
||||
queryHistory.updateCompareWith(['a', 'b', 'c']);
|
||||
expect(queryHistory.compareWithItem).to.be.undefined;
|
||||
it('should delete compareWithItem when there are more than 2 items', async () => {
|
||||
queryHistoryManager = await createMockQueryHistory(allHistory);
|
||||
queryHistoryManager.compareWithItem = allHistory[0];
|
||||
(queryHistoryManager as any).updateCompareWith([allHistory[0], allHistory[1], allHistory[2]]);
|
||||
expect(queryHistoryManager.compareWithItem).to.be.undefined;
|
||||
});
|
||||
|
||||
it('should delete compareWithItem when there are 2 items and disjoint from compareWithItem', () => {
|
||||
const queryHistory = createMockQueryHistory([]);
|
||||
queryHistory.compareWithItem = 'a';
|
||||
queryHistory.updateCompareWith(['b', 'c']);
|
||||
expect(queryHistory.compareWithItem).to.be.undefined;
|
||||
it('should delete compareWithItem when there are 2 items and disjoint from compareWithItem', async () => {
|
||||
queryHistoryManager = await createMockQueryHistory([]);
|
||||
queryHistoryManager.compareWithItem = allHistory[0];
|
||||
(queryHistoryManager as any).updateCompareWith([allHistory[1], allHistory[2]]);
|
||||
expect(queryHistoryManager.compareWithItem).to.be.undefined;
|
||||
});
|
||||
|
||||
it('should do nothing when compareWithItem exists and exactly 2 items', () => {
|
||||
const queryHistory = createMockQueryHistory([]);
|
||||
queryHistory.compareWithItem = 'a';
|
||||
queryHistory.updateCompareWith(['a', 'b']);
|
||||
expect(queryHistory.compareWithItem).to.be.eq('a');
|
||||
it('should do nothing when compareWithItem exists and exactly 2 items', async () => {
|
||||
queryHistoryManager = await createMockQueryHistory([]);
|
||||
queryHistoryManager.compareWithItem = allHistory[0];
|
||||
(queryHistoryManager as any).updateCompareWith([allHistory[0], allHistory[1]]);
|
||||
expect(queryHistoryManager.compareWithItem).to.be.eq(allHistory[0]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -212,70 +315,254 @@ describe('query-history', () => {
|
||||
historyTreeDataProvider = new HistoryTreeDataProvider(vscode.Uri.file('/a/b/c').fsPath);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
historyTreeDataProvider.dispose();
|
||||
});
|
||||
|
||||
|
||||
it('should get a tree item with raw results', async () => {
|
||||
const mockQuery = {
|
||||
query: {
|
||||
hasInterpretedResults: () => Promise.resolve(false)
|
||||
} as QueryInfo,
|
||||
didRunSuccessfully: true,
|
||||
toString: () => 'mock label'
|
||||
} as CompletedQuery;
|
||||
const mockQuery = createMockFullQueryInfo('a', createMockQueryWithResults(true, /* raw results */ false));
|
||||
const treeItem = await historyTreeDataProvider.getTreeItem(mockQuery);
|
||||
expect(treeItem.command).to.deep.eq({
|
||||
title: 'Query History Item',
|
||||
command: 'codeQLQueryHistory.itemClicked',
|
||||
arguments: [mockQuery],
|
||||
});
|
||||
expect(treeItem.label).to.eq('mock label');
|
||||
expect(treeItem.label).to.contain('hucairz');
|
||||
expect(treeItem.contextValue).to.eq('rawResultsItem');
|
||||
expect(treeItem.iconPath).to.be.undefined;
|
||||
expect(treeItem.iconPath).to.deep.eq(vscode.Uri.file('/a/b/c/media/drive.svg').fsPath);
|
||||
});
|
||||
|
||||
it('should get a tree item with interpreted results', async () => {
|
||||
const mockQuery = {
|
||||
query: {
|
||||
// as above, except for this line
|
||||
hasInterpretedResults: () => Promise.resolve(true)
|
||||
} as QueryInfo,
|
||||
didRunSuccessfully: true,
|
||||
toString: () => 'mock label'
|
||||
} as CompletedQuery;
|
||||
const mockQuery = createMockFullQueryInfo('a', createMockQueryWithResults(true, /* interpreted results */ true));
|
||||
const treeItem = await historyTreeDataProvider.getTreeItem(mockQuery);
|
||||
expect(treeItem.contextValue).to.eq('interpretedResultsItem');
|
||||
expect(treeItem.iconPath).to.deep.eq(vscode.Uri.file('/a/b/c/media/drive.svg').fsPath);
|
||||
});
|
||||
|
||||
it('should get a tree item that did not complete successfully', async () => {
|
||||
const mockQuery = {
|
||||
query: {
|
||||
hasInterpretedResults: () => Promise.resolve(true)
|
||||
} as QueryInfo,
|
||||
// as above, except for this line
|
||||
didRunSuccessfully: false,
|
||||
toString: () => 'mock label'
|
||||
} as CompletedQuery;
|
||||
const mockQuery = createMockFullQueryInfo('a', createMockQueryWithResults(false), false);
|
||||
const treeItem = await historyTreeDataProvider.getTreeItem(mockQuery);
|
||||
expect(treeItem.iconPath).to.eq(vscode.Uri.file('/a/b/c/media/red-x.svg').fsPath);
|
||||
});
|
||||
|
||||
it('should get a tree item that failed before creating any results', async () => {
|
||||
const mockQuery = createMockFullQueryInfo('a', undefined, true);
|
||||
const treeItem = await historyTreeDataProvider.getTreeItem(mockQuery);
|
||||
expect(treeItem.iconPath).to.eq(vscode.Uri.file('/a/b/c/media/red-x.svg').fsPath);
|
||||
});
|
||||
|
||||
it('should get a tree item that is in progress', async () => {
|
||||
const mockQuery = createMockFullQueryInfo('a');
|
||||
const treeItem = await historyTreeDataProvider.getTreeItem(mockQuery);
|
||||
expect(treeItem.iconPath).to.deep.eq({
|
||||
id: 'sync~spin', color: undefined
|
||||
});
|
||||
});
|
||||
|
||||
it('should get children', () => {
|
||||
const mockQuery = {
|
||||
databaseName: 'abc'
|
||||
} as CompletedQuery;
|
||||
const mockQuery = createMockFullQueryInfo();
|
||||
historyTreeDataProvider.allHistory.push(mockQuery);
|
||||
expect(historyTreeDataProvider.getChildren()).to.deep.eq([mockQuery]);
|
||||
expect(historyTreeDataProvider.getChildren(mockQuery)).to.deep.eq([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createMockQueryHistory(allHistory: Record<string, unknown>[]) {
|
||||
return {
|
||||
assertSingleQuery: (QueryHistoryManager.prototype as any).assertSingleQuery,
|
||||
findOtherQueryToCompare: (QueryHistoryManager.prototype as any).findOtherQueryToCompare,
|
||||
treeDataProvider: {
|
||||
allHistory
|
||||
},
|
||||
updateCompareWith: (QueryHistoryManager.prototype as any).updateCompareWith,
|
||||
compareWithItem: undefined as undefined | string,
|
||||
};
|
||||
}
|
||||
describe('determineSelection', () => {
|
||||
const singleItem = 'a';
|
||||
const multipleItems = ['b', 'c', 'd'];
|
||||
|
||||
it('should get the selection from parameters', async () => {
|
||||
queryHistoryManager = await createMockQueryHistory(allHistory);
|
||||
const selection = (queryHistoryManager as any).determineSelection(singleItem, multipleItems);
|
||||
expect(selection).to.deep.eq({
|
||||
finalSingleItem: singleItem,
|
||||
finalMultiSelect: multipleItems
|
||||
});
|
||||
});
|
||||
|
||||
it('should get the selection when single selection is empty', async () => {
|
||||
queryHistoryManager = await createMockQueryHistory(allHistory);
|
||||
const selection = (queryHistoryManager as any).determineSelection(undefined, multipleItems);
|
||||
expect(selection).to.deep.eq({
|
||||
finalSingleItem: multipleItems[0],
|
||||
finalMultiSelect: multipleItems
|
||||
});
|
||||
});
|
||||
|
||||
it('should get the selection when multi-selection is empty', async () => {
|
||||
queryHistoryManager = await createMockQueryHistory(allHistory);
|
||||
const selection = (queryHistoryManager as any).determineSelection(singleItem, undefined);
|
||||
expect(selection).to.deep.eq({
|
||||
finalSingleItem: singleItem,
|
||||
finalMultiSelect: [singleItem]
|
||||
});
|
||||
});
|
||||
|
||||
it('should get the selection from the treeView when both selections are empty', async () => {
|
||||
queryHistoryManager = await createMockQueryHistory(allHistory);
|
||||
await queryHistoryManager.treeView.reveal(allHistory[1], { select: true });
|
||||
const selection = (queryHistoryManager as any).determineSelection(undefined, undefined);
|
||||
expect(selection).to.deep.eq({
|
||||
finalSingleItem: allHistory[1],
|
||||
finalMultiSelect: [allHistory[1]]
|
||||
});
|
||||
});
|
||||
|
||||
it('should get the selection from the treeDataProvider when both selections and the treeView are empty', async () => {
|
||||
queryHistoryManager = await createMockQueryHistory(allHistory);
|
||||
await queryHistoryManager.treeView.reveal(allHistory[1], { select: true });
|
||||
const selection = (queryHistoryManager as any).determineSelection(undefined, undefined);
|
||||
expect(selection).to.deep.eq({
|
||||
finalSingleItem: allHistory[1],
|
||||
finalMultiSelect: [allHistory[1]]
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getChildren', () => {
|
||||
const history = [
|
||||
item('a', 10, 20),
|
||||
item('b', 5, 30),
|
||||
item('c', 1, 25),
|
||||
];
|
||||
let treeDataProvider: HistoryTreeDataProvider;
|
||||
|
||||
beforeEach(async () => {
|
||||
queryHistoryManager = await createMockQueryHistory(allHistory);
|
||||
(queryHistoryManager.treeDataProvider as any).history = [...history];
|
||||
treeDataProvider = queryHistoryManager.treeDataProvider;
|
||||
});
|
||||
|
||||
it('should get children for name ascending', async () => {
|
||||
const expected = [...history];
|
||||
treeDataProvider.sortOrder = SortOrder.NameAsc;
|
||||
|
||||
const children = await treeDataProvider.getChildren();
|
||||
expect(children).to.deep.eq(expected);
|
||||
});
|
||||
|
||||
it('should get children for name descending', async () => {
|
||||
const expected = [...history].reverse();
|
||||
treeDataProvider.sortOrder = SortOrder.NameDesc;
|
||||
|
||||
const children = await treeDataProvider.getChildren();
|
||||
expect(children).to.deep.eq(expected);
|
||||
});
|
||||
|
||||
it('should get children for date ascending', async () => {
|
||||
const expected = [history[2], history[1], history[0]];
|
||||
treeDataProvider.sortOrder = SortOrder.DateAsc;
|
||||
|
||||
const children = await treeDataProvider.getChildren();
|
||||
expect(children).to.deep.eq(expected);
|
||||
});
|
||||
|
||||
it('should get children for date descending', async () => {
|
||||
const expected = [history[0], history[1], history[2]];
|
||||
treeDataProvider.sortOrder = SortOrder.DateDesc;
|
||||
|
||||
const children = await treeDataProvider.getChildren();
|
||||
expect(children).to.deep.eq(expected);
|
||||
});
|
||||
|
||||
it('should get children for result count ascending', async () => {
|
||||
const expected = [history[0], history[2], history[1]];
|
||||
treeDataProvider.sortOrder = SortOrder.CountAsc;
|
||||
|
||||
const children = await treeDataProvider.getChildren();
|
||||
expect(children).to.deep.eq(expected);
|
||||
});
|
||||
|
||||
it('should get children for result count descending', async () => {
|
||||
const expected = [history[1], history[2], history[0]];
|
||||
treeDataProvider.sortOrder = SortOrder.CountDesc;
|
||||
|
||||
const children = await treeDataProvider.getChildren();
|
||||
expect(children).to.deep.eq(expected);
|
||||
});
|
||||
|
||||
it('should get children for result count ascending when there are no results', async () => {
|
||||
// fall back to name
|
||||
const thisHistory = [item('a', 10), item('b', 50), item('c', 1)];
|
||||
(queryHistoryManager!.treeDataProvider as any).history = [...thisHistory];
|
||||
const expected = [...thisHistory];
|
||||
treeDataProvider.sortOrder = SortOrder.CountAsc;
|
||||
|
||||
const children = await treeDataProvider.getChildren();
|
||||
expect(children).to.deep.eq(expected);
|
||||
});
|
||||
|
||||
it('should get children for result count descending when there are no results', async () => {
|
||||
// fall back to name
|
||||
const thisHistory = [item('a', 10), item('b', 50), item('c', 1)];
|
||||
(queryHistoryManager!.treeDataProvider as any).history = [...thisHistory];
|
||||
const expected = [...thisHistory].reverse();
|
||||
treeDataProvider.sortOrder = SortOrder.CountDesc;
|
||||
|
||||
const children = await treeDataProvider.getChildren();
|
||||
expect(children).to.deep.eq(expected);
|
||||
});
|
||||
|
||||
function item(label: string, start: number, resultCount?: number) {
|
||||
return {
|
||||
label,
|
||||
initialInfo: {
|
||||
start: new Date(start),
|
||||
},
|
||||
completedQuery: {
|
||||
resultCount,
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
function createMockFullQueryInfo(dbName = 'a', queryWitbResults?: QueryWithResults, isFail = false): FullQueryInfo {
|
||||
const fqi = new FullQueryInfo(
|
||||
{
|
||||
databaseInfo: { name: dbName },
|
||||
start: new Date(),
|
||||
queryPath: 'hucairz'
|
||||
} as InitialQueryInfo,
|
||||
configListener,
|
||||
{} as vscode.CancellationTokenSource
|
||||
);
|
||||
|
||||
if (queryWitbResults) {
|
||||
fqi.completeThisQuery(queryWitbResults);
|
||||
}
|
||||
if (isFail) {
|
||||
fqi.failureReason = 'failure reason';
|
||||
}
|
||||
return fqi;
|
||||
}
|
||||
|
||||
function createMockQueryWithResults(didRunSuccessfully = true, hasInterpretedResults = true): QueryWithResults {
|
||||
return {
|
||||
query: {
|
||||
hasInterpretedResults: () => Promise.resolve(hasInterpretedResults)
|
||||
} as QueryEvaluationInfo,
|
||||
result: {
|
||||
resultType: didRunSuccessfully
|
||||
? messages.QueryResultType.SUCCESS
|
||||
: messages.QueryResultType.OTHER_ERROR
|
||||
} as messages.EvaluationResult,
|
||||
dispose: sandbox.spy(),
|
||||
};
|
||||
}
|
||||
|
||||
async function createMockQueryHistory(allHistory: FullQueryInfo[]) {
|
||||
const qhm = new QueryHistoryManager(
|
||||
{} as QueryServerClient,
|
||||
{} as DatabaseManager,
|
||||
'xxx',
|
||||
configListener,
|
||||
selectedCallback,
|
||||
doCompareCallback
|
||||
);
|
||||
(qhm.treeDataProvider as any).history = [...allHistory];
|
||||
await vscode.workspace.saveAll();
|
||||
qhm.refreshTreeView();
|
||||
return qhm;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,162 +3,188 @@ import * as path from 'path';
|
||||
import * as fs from 'fs-extra';
|
||||
import 'mocha';
|
||||
import 'sinon-chai';
|
||||
import * as Sinon from 'sinon';
|
||||
import * as sinon from 'sinon';
|
||||
import * as chaiAsPromised from 'chai-as-promised';
|
||||
import { CompletedQuery, interpretResults } from '../../query-results';
|
||||
import { QueryInfo, QueryWithResults, tmpDir } from '../../run-queries';
|
||||
import { FullQueryInfo, InitialQueryInfo, interpretResults } from '../../query-results';
|
||||
import { queriesDir, QueryEvaluationInfo, QueryWithResults, tmpDir } from '../../run-queries';
|
||||
import { QueryHistoryConfig } from '../../config';
|
||||
import { EvaluationResult, QueryResultType } from '../../pure/messages';
|
||||
import { SortDirection, SortedResultSetInfo } from '../../pure/interface-types';
|
||||
import { DatabaseInfo, SortDirection, SortedResultSetInfo } from '../../pure/interface-types';
|
||||
import { CodeQLCliServer, SourceInfo } from '../../cli';
|
||||
import { env } from 'process';
|
||||
import { CancellationTokenSource, Uri } from 'vscode';
|
||||
|
||||
chai.use(chaiAsPromised);
|
||||
const expect = chai.expect;
|
||||
|
||||
describe('CompletedQuery', () => {
|
||||
let disposeSpy: Sinon.SinonSpy;
|
||||
let onDidChangeQueryHistoryConfigurationSpy: Sinon.SinonSpy;
|
||||
describe('query-results', () => {
|
||||
let disposeSpy: sinon.SinonSpy;
|
||||
let onDidChangeQueryHistoryConfigurationSpy: sinon.SinonSpy;
|
||||
let mockConfig: QueryHistoryConfig;
|
||||
let sandbox: sinon.SinonSandbox;
|
||||
|
||||
beforeEach(() => {
|
||||
disposeSpy = Sinon.spy();
|
||||
onDidChangeQueryHistoryConfigurationSpy = Sinon.spy();
|
||||
sandbox = sinon.createSandbox();
|
||||
disposeSpy = sandbox.spy();
|
||||
onDidChangeQueryHistoryConfigurationSpy = sandbox.spy();
|
||||
mockConfig = mockQueryHistoryConfig();
|
||||
});
|
||||
|
||||
it('should construct a CompletedQuery', () => {
|
||||
const completedQuery = mockCompletedQuery();
|
||||
|
||||
expect(completedQuery.logFileLocation).to.eq('mno');
|
||||
expect(completedQuery.databaseName).to.eq('def');
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('should get the query name', () => {
|
||||
const completedQuery = mockCompletedQuery();
|
||||
|
||||
// from the query path
|
||||
expect(completedQuery.queryName).to.eq('stu');
|
||||
|
||||
// from the metadata
|
||||
(completedQuery.query as any).metadata = {
|
||||
name: 'vwx'
|
||||
};
|
||||
expect(completedQuery.queryName).to.eq('vwx');
|
||||
|
||||
// from quick eval position
|
||||
(completedQuery.query as any).quickEvalPosition = {
|
||||
line: 1,
|
||||
endLine: 2,
|
||||
fileName: '/home/users/yz'
|
||||
};
|
||||
expect(completedQuery.queryName).to.eq('Quick evaluation of yz:1-2');
|
||||
(completedQuery.query as any).quickEvalPosition.endLine = 1;
|
||||
expect(completedQuery.queryName).to.eq('Quick evaluation of yz:1');
|
||||
});
|
||||
|
||||
it('should get the query file name', () => {
|
||||
const completedQuery = mockCompletedQuery();
|
||||
|
||||
// from the query path
|
||||
expect(completedQuery.queryFileName).to.eq('stu');
|
||||
|
||||
// from quick eval position
|
||||
(completedQuery.query as any).quickEvalPosition = {
|
||||
line: 1,
|
||||
endLine: 2,
|
||||
fileName: '/home/users/yz'
|
||||
};
|
||||
expect(completedQuery.queryFileName).to.eq('yz:1-2');
|
||||
(completedQuery.query as any).quickEvalPosition.endLine = 1;
|
||||
expect(completedQuery.queryFileName).to.eq('yz:1');
|
||||
});
|
||||
|
||||
it('should get the label', () => {
|
||||
const completedQuery = mockCompletedQuery();
|
||||
expect(completedQuery.getLabel()).to.eq('ghi');
|
||||
completedQuery.options.label = '';
|
||||
expect(completedQuery.getLabel()).to.eq('pqr');
|
||||
});
|
||||
|
||||
it('should get the getResultsPath', () => {
|
||||
const completedQuery = mockCompletedQuery();
|
||||
// from results path
|
||||
expect(completedQuery.getResultsPath('zxa', false)).to.eq('axa');
|
||||
|
||||
completedQuery.sortedResultsInfo.set('zxa', {
|
||||
resultsPath: 'bxa'
|
||||
} as SortedResultSetInfo);
|
||||
|
||||
// still from results path
|
||||
expect(completedQuery.getResultsPath('zxa', false)).to.eq('axa');
|
||||
|
||||
// from sortedResultsInfo
|
||||
expect(completedQuery.getResultsPath('zxa')).to.eq('bxa');
|
||||
});
|
||||
|
||||
it('should get the statusString', () => {
|
||||
const completedQuery = mockCompletedQuery();
|
||||
expect(completedQuery.statusString).to.eq('failed');
|
||||
|
||||
completedQuery.result.message = 'Tremendously';
|
||||
expect(completedQuery.statusString).to.eq('failed: Tremendously');
|
||||
|
||||
completedQuery.result.resultType = QueryResultType.OTHER_ERROR;
|
||||
expect(completedQuery.statusString).to.eq('failed: Tremendously');
|
||||
|
||||
completedQuery.result.resultType = QueryResultType.CANCELLATION;
|
||||
completedQuery.result.evaluationTime = 2000;
|
||||
expect(completedQuery.statusString).to.eq('cancelled after 2 seconds');
|
||||
|
||||
completedQuery.result.resultType = QueryResultType.OOM;
|
||||
expect(completedQuery.statusString).to.eq('out of memory');
|
||||
|
||||
completedQuery.result.resultType = QueryResultType.SUCCESS;
|
||||
expect(completedQuery.statusString).to.eq('finished in 2 seconds');
|
||||
|
||||
completedQuery.result.resultType = QueryResultType.TIMEOUT;
|
||||
expect(completedQuery.statusString).to.eq('timed out after 2 seconds');
|
||||
|
||||
});
|
||||
|
||||
it('should updateSortState', async () => {
|
||||
const completedQuery = mockCompletedQuery();
|
||||
const spy = Sinon.spy();
|
||||
const mockServer = {
|
||||
sortBqrs: spy
|
||||
} as unknown as CodeQLCliServer;
|
||||
const sortState = {
|
||||
columnIndex: 1,
|
||||
sortDirection: SortDirection.desc
|
||||
};
|
||||
await completedQuery.updateSortState(mockServer, 'result-name', sortState);
|
||||
const expectedPath = path.join(tmpDir.name, 'sortedResults111-result-name.bqrs');
|
||||
expect(spy).to.have.been.calledWith(
|
||||
'axa',
|
||||
expectedPath,
|
||||
'result-name',
|
||||
[sortState.columnIndex],
|
||||
[sortState.sortDirection],
|
||||
);
|
||||
|
||||
expect(completedQuery.sortedResultsInfo.get('result-name')).to.deep.equal({
|
||||
resultsPath: expectedPath,
|
||||
sortState
|
||||
describe('FullQueryInfo', () => {
|
||||
it('should interpolate', () => {
|
||||
const fqi = createMockFullQueryInfo();
|
||||
const date = new Date('2022-01-01T00:00:00.000Z');
|
||||
const dateStr = date.toLocaleString(env.language);
|
||||
(fqi.initialInfo as any).start = date;
|
||||
expect(fqi.interpolate('xxx')).to.eq('xxx');
|
||||
expect(fqi.interpolate('%t %q %d %s %%')).to.eq(`${dateStr} hucairz a in progress %`);
|
||||
expect(fqi.interpolate('%t %q %d %s %%::%t %q %d %s %%')).to.eq(`${dateStr} hucairz a in progress %::${dateStr} hucairz a in progress %`);
|
||||
});
|
||||
|
||||
// delete the sort stae
|
||||
await completedQuery.updateSortState(mockServer, 'result-name');
|
||||
expect(completedQuery.sortedResultsInfo.size).to.eq(0);
|
||||
});
|
||||
it('should get the query name', () => {
|
||||
const fqi = createMockFullQueryInfo();
|
||||
|
||||
it('should interpolate', () => {
|
||||
const completedQuery = mockCompletedQuery();
|
||||
(completedQuery as any).time = '123';
|
||||
expect(completedQuery.interpolate('xxx')).to.eq('xxx');
|
||||
expect(completedQuery.interpolate('%t %q %d %s %%')).to.eq('123 stu def failed %');
|
||||
expect(completedQuery.interpolate('%t %q %d %s %%::%t %q %d %s %%')).to.eq('123 stu def failed %::123 stu def failed %');
|
||||
// from the query path
|
||||
expect(fqi.getQueryName()).to.eq('hucairz');
|
||||
|
||||
fqi.completeThisQuery(createMockQueryWithResults());
|
||||
|
||||
// from the metadata
|
||||
expect(fqi.getQueryName()).to.eq('vwx');
|
||||
|
||||
// from quick eval position
|
||||
(fqi.initialInfo as any).quickEvalPosition = {
|
||||
line: 1,
|
||||
endLine: 2,
|
||||
fileName: '/home/users/yz'
|
||||
};
|
||||
expect(fqi.getQueryName()).to.eq('Quick evaluation of yz:1-2');
|
||||
(fqi.initialInfo as any).quickEvalPosition.endLine = 1;
|
||||
expect(fqi.getQueryName()).to.eq('Quick evaluation of yz:1');
|
||||
});
|
||||
|
||||
it('should get the query file name', () => {
|
||||
const fqi = createMockFullQueryInfo();
|
||||
|
||||
// from the query path
|
||||
expect(fqi.getQueryFileName()).to.eq('hucairz');
|
||||
|
||||
// from quick eval position
|
||||
(fqi.initialInfo as any).quickEvalPosition = {
|
||||
line: 1,
|
||||
endLine: 2,
|
||||
fileName: '/home/users/yz'
|
||||
};
|
||||
expect(fqi.getQueryFileName()).to.eq('yz:1-2');
|
||||
(fqi.initialInfo as any).quickEvalPosition.endLine = 1;
|
||||
expect(fqi.getQueryFileName()).to.eq('yz:1');
|
||||
});
|
||||
|
||||
it('should get the label', () => {
|
||||
const fqi = createMockFullQueryInfo('db-name');
|
||||
|
||||
// the %q from the config is now replaced by the file name of the query
|
||||
expect(fqi.label).to.eq('from config hucairz');
|
||||
|
||||
// the %q from the config is now replaced by the name of the query
|
||||
// in the metadata
|
||||
fqi.completeThisQuery(createMockQueryWithResults());
|
||||
expect(fqi.label).to.eq('from config vwx');
|
||||
|
||||
// replace the config with a user specified label
|
||||
// must be interpolated
|
||||
fqi.initialInfo.userSpecifiedLabel = 'user specified label %d';
|
||||
expect(fqi.label).to.eq('user specified label db-name');
|
||||
});
|
||||
|
||||
it('should get the getResultsPath', () => {
|
||||
const fqi = createMockFullQueryInfo('a', createMockQueryWithResults());
|
||||
const completedQuery = fqi.completedQuery!;
|
||||
const expectedResultsPath = path.join(queriesDir, 'some-id/results.bqrs');
|
||||
|
||||
// from results path
|
||||
expect(completedQuery.getResultsPath('zxa', false)).to.eq(expectedResultsPath);
|
||||
|
||||
completedQuery.sortedResultsInfo['zxa'] = {
|
||||
resultsPath: 'bxa'
|
||||
} as SortedResultSetInfo;
|
||||
|
||||
// still from results path
|
||||
expect(completedQuery.getResultsPath('zxa', false)).to.eq(expectedResultsPath);
|
||||
|
||||
// from sortedResultsInfo
|
||||
expect(completedQuery.getResultsPath('zxa')).to.eq('bxa');
|
||||
});
|
||||
|
||||
it('should get the statusString', () => {
|
||||
const fqi = createMockFullQueryInfo('a', createMockQueryWithResults(false));
|
||||
const completedQuery = fqi.completedQuery!;
|
||||
|
||||
completedQuery.result.message = 'Tremendously';
|
||||
expect(completedQuery.statusString).to.eq('failed: Tremendously');
|
||||
|
||||
completedQuery.result.resultType = QueryResultType.OTHER_ERROR;
|
||||
expect(completedQuery.statusString).to.eq('failed: Tremendously');
|
||||
|
||||
completedQuery.result.resultType = QueryResultType.CANCELLATION;
|
||||
completedQuery.result.evaluationTime = 2345;
|
||||
expect(completedQuery.statusString).to.eq('cancelled after 2 seconds');
|
||||
|
||||
completedQuery.result.resultType = QueryResultType.OOM;
|
||||
expect(completedQuery.statusString).to.eq('out of memory');
|
||||
|
||||
completedQuery.result.resultType = QueryResultType.SUCCESS;
|
||||
expect(completedQuery.statusString).to.eq('finished in 2 seconds');
|
||||
|
||||
completedQuery.result.resultType = QueryResultType.TIMEOUT;
|
||||
expect(completedQuery.statusString).to.eq('timed out after 2 seconds');
|
||||
});
|
||||
|
||||
it('should updateSortState', async () => {
|
||||
// setup
|
||||
const fqi = createMockFullQueryInfo('a', createMockQueryWithResults());
|
||||
const completedQuery = fqi.completedQuery!;
|
||||
|
||||
const spy = sandbox.spy();
|
||||
const mockServer = {
|
||||
sortBqrs: spy
|
||||
} as unknown as CodeQLCliServer;
|
||||
const sortState = {
|
||||
columnIndex: 1,
|
||||
sortDirection: SortDirection.desc
|
||||
};
|
||||
|
||||
// test
|
||||
await completedQuery.updateSortState(mockServer, 'a-result-set-name', sortState);
|
||||
|
||||
// verify
|
||||
const expectedResultsPath = path.join(queriesDir, 'some-id/results.bqrs');
|
||||
const expectedSortedResultsPath = path.join(queriesDir, 'some-id/sortedResults-a-result-set-name.bqrs');
|
||||
expect(spy).to.have.been.calledWith(
|
||||
expectedResultsPath,
|
||||
expectedSortedResultsPath,
|
||||
'a-result-set-name',
|
||||
[sortState.columnIndex],
|
||||
[sortState.sortDirection],
|
||||
);
|
||||
|
||||
expect(completedQuery.sortedResultsInfo['a-result-set-name']).to.deep.equal({
|
||||
resultsPath: expectedSortedResultsPath,
|
||||
sortState
|
||||
});
|
||||
|
||||
// delete the sort state
|
||||
await completedQuery.updateSortState(mockServer, 'a-result-set-name');
|
||||
expect(Object.values(completedQuery.sortedResultsInfo).length).to.eq(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should interpretResults', async () => {
|
||||
const spy = Sinon.mock();
|
||||
const spy = sandbox.mock();
|
||||
spy.returns('1234');
|
||||
const mockServer = {
|
||||
interpretBqrs: spy
|
||||
@@ -221,43 +247,121 @@ describe('CompletedQuery', () => {
|
||||
expect(results3).to.deep.eq({ a: 6 });
|
||||
});
|
||||
|
||||
function mockCompletedQuery() {
|
||||
return new CompletedQuery(
|
||||
mockQueryWithResults(),
|
||||
mockQueryHistoryConfig()
|
||||
describe('splat and slurp', () => {
|
||||
// TODO also add a test for round trip starting from file
|
||||
it('should splat and slurp query history', async () => {
|
||||
const infoSuccessRaw = createMockFullQueryInfo('a', createMockQueryWithResults(false, false, '/a/b/c/a', false));
|
||||
const infoSuccessInterpreted = createMockFullQueryInfo('b', createMockQueryWithResults(true, true, '/a/b/c/b', false));
|
||||
const infoEarlyFailure = createMockFullQueryInfo('c', undefined, true);
|
||||
const infoLateFailure = createMockFullQueryInfo('d', createMockQueryWithResults(false, false, '/a/b/c/d', false));
|
||||
const infoInprogress = createMockFullQueryInfo('e');
|
||||
const allHistory = [
|
||||
infoSuccessRaw,
|
||||
infoSuccessInterpreted,
|
||||
infoEarlyFailure,
|
||||
infoLateFailure,
|
||||
infoInprogress
|
||||
];
|
||||
|
||||
const allHistoryPath = path.join(queriesDir, 'all-history.json');
|
||||
await FullQueryInfo.splat(allHistory, allHistoryPath);
|
||||
const allHistoryActual = await FullQueryInfo.slurp(allHistoryPath, mockConfig);
|
||||
|
||||
// the dispose methods will be different. Ignore them.
|
||||
allHistoryActual.forEach(info => {
|
||||
if (info.completedQuery) {
|
||||
const completedQuery = info.completedQuery;
|
||||
(completedQuery as any).dispose = undefined;
|
||||
|
||||
// these fields should be missing on the slurped value
|
||||
// but they are undefined on the original value
|
||||
if (!('logFileLocation' in completedQuery)) {
|
||||
(completedQuery as any).logFileLocation = undefined;
|
||||
}
|
||||
const query = completedQuery.query;
|
||||
if (!('quickEvalPosition' in query)) {
|
||||
(query as any).quickEvalPosition = undefined;
|
||||
}
|
||||
if (!('templates' in query)) {
|
||||
(query as any).templates = undefined;
|
||||
}
|
||||
}
|
||||
});
|
||||
allHistory.forEach(info => {
|
||||
if (info.completedQuery) {
|
||||
(info.completedQuery as any).dispose = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
// make the diffs somewhat sane by comparing each element directly
|
||||
for (let i = 0; i < allHistoryActual.length; i++) {
|
||||
expect(allHistoryActual[i]).to.deep.eq(allHistory[i]);
|
||||
}
|
||||
expect(allHistoryActual.length).to.deep.eq(allHistory.length);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
function createMockQueryWithResults(didRunSuccessfully = true, hasInterpretedResults = true, dbPath = '/a/b/c', includeSpies = true): QueryWithResults {
|
||||
const query = new QueryEvaluationInfo('some-id',
|
||||
Uri.file(dbPath).fsPath, // parse the Uri to make sure it is platform-independent
|
||||
true,
|
||||
'queryDbscheme',
|
||||
undefined,
|
||||
{
|
||||
name: 'vwx'
|
||||
},
|
||||
);
|
||||
|
||||
const result = {
|
||||
query,
|
||||
result: {
|
||||
evaluationTime: 12340,
|
||||
resultType: didRunSuccessfully
|
||||
? QueryResultType.SUCCESS
|
||||
: QueryResultType.OTHER_ERROR
|
||||
} as EvaluationResult,
|
||||
dispose: disposeSpy,
|
||||
};
|
||||
|
||||
if (includeSpies) {
|
||||
(query as any).hasInterpretedResults = () => Promise.resolve(hasInterpretedResults);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function mockQueryWithResults(): QueryWithResults {
|
||||
return {
|
||||
query: {
|
||||
program: {
|
||||
queryPath: 'stu'
|
||||
},
|
||||
resultsPaths: {
|
||||
resultsPath: 'axa'
|
||||
},
|
||||
queryID: 111
|
||||
} as never as QueryInfo,
|
||||
result: {} as never as EvaluationResult,
|
||||
database: {
|
||||
databaseUri: 'abc',
|
||||
name: 'def'
|
||||
},
|
||||
options: {
|
||||
label: 'ghi',
|
||||
queryText: 'jkl',
|
||||
isQuickQuery: false
|
||||
},
|
||||
logFileLocation: 'mno',
|
||||
dispose: disposeSpy
|
||||
};
|
||||
function createMockFullQueryInfo(dbName = 'a', queryWitbResults?: QueryWithResults, isFail = false): FullQueryInfo {
|
||||
const fqi = new FullQueryInfo(
|
||||
{
|
||||
databaseInfo: {
|
||||
name: dbName,
|
||||
databaseUri: Uri.parse(`/a/b/c/${dbName}`).fsPath
|
||||
} as unknown as DatabaseInfo,
|
||||
start: new Date(),
|
||||
queryPath: 'path/to/hucairz',
|
||||
queryText: 'some query',
|
||||
isQuickQuery: false,
|
||||
isQuickEval: false,
|
||||
id: `some-id-${dbName}`,
|
||||
} as InitialQueryInfo,
|
||||
mockQueryHistoryConfig(),
|
||||
{} as CancellationTokenSource
|
||||
);
|
||||
|
||||
if (queryWitbResults) {
|
||||
fqi.completeThisQuery(queryWitbResults);
|
||||
}
|
||||
if (isFail) {
|
||||
fqi.failureReason = 'failure reason';
|
||||
}
|
||||
return fqi;
|
||||
}
|
||||
|
||||
function mockQueryHistoryConfig(): QueryHistoryConfig {
|
||||
return {
|
||||
onDidChangeConfiguration: onDidChangeQueryHistoryConfigurationSpy,
|
||||
format: 'pqr'
|
||||
format: 'from config %q'
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,40 +5,39 @@ import 'sinon-chai';
|
||||
import * as sinon from 'sinon';
|
||||
import * as chaiAsPromised from 'chai-as-promised';
|
||||
|
||||
import { QueryInfo } from '../../run-queries';
|
||||
import { QlProgram, Severity, compileQuery } from '../../pure/messages';
|
||||
import { DatabaseItem } from '../../databases';
|
||||
import { QueryEvaluationInfo, queriesDir } from '../../run-queries';
|
||||
import { Severity, compileQuery } from '../../pure/messages';
|
||||
import { Uri } from 'vscode';
|
||||
|
||||
chai.use(chaiAsPromised);
|
||||
const expect = chai.expect;
|
||||
|
||||
describe('run-queries', () => {
|
||||
it('should create a QueryInfo', () => {
|
||||
it('should create a QueryEvaluationInfo', () => {
|
||||
const info = createMockQueryInfo();
|
||||
|
||||
const queryID = info.queryID;
|
||||
expect(path.basename(info.compiledQueryPath)).to.eq(`compiledQuery${queryID}.qlo`);
|
||||
expect(path.basename(info.dilPath)).to.eq(`results${queryID}.dil`);
|
||||
expect(path.basename(info.resultsPaths.resultsPath)).to.eq(`results${queryID}.bqrs`);
|
||||
expect(path.basename(info.resultsPaths.interpretedResultsPath)).to.eq(`interpretedResults${queryID}.sarif`);
|
||||
expect(info.dataset).to.eq('file:///abc');
|
||||
const queryId = info.id;
|
||||
expect(info.compiledQueryPath).to.eq(path.join(queriesDir, queryId, 'compiledQuery.qlo'));
|
||||
expect(info.dilPath).to.eq(path.join(queriesDir, queryId, 'results.dil'));
|
||||
expect(info.resultsPaths.resultsPath).to.eq(path.join(queriesDir, queryId, 'results.bqrs'));
|
||||
expect(info.resultsPaths.interpretedResultsPath).to.eq(path.join(queriesDir, queryId, 'interpretedResults.sarif'));
|
||||
expect(info.dbItemPath).to.eq(Uri.file('/abc').fsPath);
|
||||
});
|
||||
|
||||
it('should check if interpreted results can be created', async () => {
|
||||
const info = createMockQueryInfo();
|
||||
(info.dbItem.hasMetadataFile as sinon.SinonStub).returns(true);
|
||||
const info = createMockQueryInfo(true);
|
||||
|
||||
expect(await info.canHaveInterpretedResults()).to.eq(true);
|
||||
expect(info.canHaveInterpretedResults()).to.eq(true);
|
||||
|
||||
(info.dbItem.hasMetadataFile as sinon.SinonStub).returns(false);
|
||||
expect(await info.canHaveInterpretedResults()).to.eq(false);
|
||||
(info as any).databaseHasMetadataFile = false;
|
||||
expect(info.canHaveInterpretedResults()).to.eq(false);
|
||||
|
||||
(info.dbItem.hasMetadataFile as sinon.SinonStub).returns(true);
|
||||
(info as any).databaseHasMetadataFile = true;
|
||||
info.metadata!.kind = undefined;
|
||||
expect(await info.canHaveInterpretedResults()).to.eq(false);
|
||||
expect(info.canHaveInterpretedResults()).to.eq(false);
|
||||
|
||||
info.metadata!.kind = 'table';
|
||||
expect(await info.canHaveInterpretedResults()).to.eq(false);
|
||||
expect(info.canHaveInterpretedResults()).to.eq(false);
|
||||
});
|
||||
|
||||
describe('compile', () => {
|
||||
@@ -47,9 +46,15 @@ describe('run-queries', () => {
|
||||
const qs = createMockQueryServerClient();
|
||||
const mockProgress = 'progress-monitor';
|
||||
const mockCancel = 'cancel-token';
|
||||
const mockQlProgram = {
|
||||
dbschemePath: '',
|
||||
libraryPath: [],
|
||||
queryPath: ''
|
||||
};
|
||||
|
||||
const results = await info.compile(
|
||||
qs as any,
|
||||
mockQlProgram,
|
||||
mockProgress as any,
|
||||
mockCancel as any
|
||||
);
|
||||
@@ -74,7 +79,7 @@ describe('run-queries', () => {
|
||||
extraOptions: {
|
||||
timeoutSecs: 5
|
||||
},
|
||||
queryToCheck: 'my-program',
|
||||
queryToCheck: mockQlProgram,
|
||||
resultPath: info.compiledQueryPath,
|
||||
target: { query: {} }
|
||||
},
|
||||
@@ -84,15 +89,12 @@ describe('run-queries', () => {
|
||||
});
|
||||
});
|
||||
|
||||
function createMockQueryInfo() {
|
||||
return new QueryInfo(
|
||||
'my-program' as unknown as QlProgram,
|
||||
{
|
||||
contents: {
|
||||
datasetUri: 'file:///abc'
|
||||
},
|
||||
hasMetadataFile: sinon.stub()
|
||||
} as unknown as DatabaseItem,
|
||||
let queryNum = 0;
|
||||
function createMockQueryInfo(databaseHasMetadataFile = true) {
|
||||
return new QueryEvaluationInfo(
|
||||
`save-dir${queryNum++}`,
|
||||
Uri.parse('file:///abc').fsPath,
|
||||
databaseHasMetadataFile,
|
||||
'my-scheme', // queryDbscheme,
|
||||
undefined,
|
||||
{
|
||||
|
||||
@@ -367,9 +367,8 @@ describe('telemetry reporting', function() {
|
||||
);
|
||||
|
||||
// Need to wait some time since the onDidChangeConfiguration listeners fire
|
||||
// asynchronously and we sometimes need to wait for them to complete in
|
||||
// order to have as successful test.
|
||||
await wait(50);
|
||||
// asynchronously. Must ensure they to complete in order to have a successful test.
|
||||
await wait(100);
|
||||
}
|
||||
|
||||
async function wait(ms = 0) {
|
||||
|
||||
Reference in New Issue
Block a user