Compare commits

...

36 Commits

Author SHA1 Message Date
Andrew Eisenberg
bd5da2b0f0 Release preparation for v1.5.11
Some checks failed
Release / Release (push) Has been cancelled
Release / Publish to VS Code Marketplace (push) Has been cancelled
Release / Publish to Open VSX Registry (push) Has been cancelled
2022-02-10 12:26:24 -08:00
Henry Mercer
55c21888af Update Code Scanning workflow now that we no longer need tools: latest
The Actions VM image containing v2.7.6 of the CodeQL CLI has now fully
rolled out, so we no longer need to download the latest CodeQL bundle to
use this CLI and include this repo in the ML-powered queries beta.
2022-02-10 18:15:52 +00:00
Charis Kyriakou
edb1af09c4 Hide analyses results until view is complete (#1126) 2022-02-10 08:13:59 +00:00
Charis Kyriakou
ab3822d1cc Use SARIF viewer extension for analysis results (#1125) 2022-02-10 08:13:31 +00:00
Andrew Eisenberg
69120e0799 Add extra delay in telemetry test
Some of our internal integration tests are failing occasionally. I
think extending the wait time here will fix.
2022-02-09 15:10:59 -08:00
Andrew Eisenberg
b7dafc31bb Better comments around splat and slurp functions
Also, address other small PR comments.
2022-02-08 12:43:38 -08:00
Andrew Eisenberg
2f5a306c2d Simplify the query history objects to make them serializable
The goal with this change is to simplify the query history to make it
possible to serialize and de serialize.

This change adds serialization support. Since query history objects are
complex, the de-serialization requires manipulation of the 
de serialized object prototypes.
2022-02-08 12:43:38 -08:00
Charis Kyriakou
0ef6b45b19 Remove use of all-results artifact (#1120) 2022-02-08 08:28:54 +00:00
dependabot[bot]
d9f33d34e3 Bump copy-props from 2.0.4 to 2.0.5 in /extensions/ql-vscode
Bumps [copy-props](https://github.com/gulpjs/copy-props) from 2.0.4 to 2.0.5.
- [Release notes](https://github.com/gulpjs/copy-props/releases)
- [Changelog](https://github.com/gulpjs/copy-props/blob/master/CHANGELOG.md)
- [Commits](https://github.com/gulpjs/copy-props/compare/2.0.4...2.0.5)

---
updated-dependencies:
- dependency-name: copy-props
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-07 15:39:23 -08:00
dependabot[bot]
5758e03a17 Bump nth-check from 2.0.0 to 2.0.1 in /extensions/ql-vscode
Bumps [nth-check](https://github.com/fb55/nth-check) from 2.0.0 to 2.0.1.
- [Release notes](https://github.com/fb55/nth-check/releases)
- [Commits](https://github.com/fb55/nth-check/compare/v2.0.0...v2.0.1)

---
updated-dependencies:
- dependency-name: nth-check
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-07 15:38:18 -08:00
Andrew Eisenberg
5d9f80cce8 Update ensureCli.ts 2022-02-07 13:30:15 -08:00
Andrew Eisenberg
867ee530b1 Update CLI test version 2022-02-07 13:30:15 -08:00
Charis Kyriakou
27e6a55756 Add full screen modal component (#1117) 2022-02-07 08:24:29 +00:00
Andrew Eisenberg
b237bafa2f Avoid AST Viewer for invalid selections
When a directory is selected or there are multiple selections, do not
show the command.
2022-02-04 11:54:11 -08:00
Andrew Eisenberg
d0bde800f7 Update changelog 2022-02-04 11:54:11 -08:00
Andrew Eisenberg
da0090aa99 Fix ast view and command registration
Two small bugs:

1. The AST view command was viewing the wrong ast when the command was
   selected from the context menu. It was always selecting the active
   editor instead of the item selected in the file menu.
2. The `codeql.showLogs` command was not being registered properly.
   With this change, there is uniform error handling, telemetry,
   and disposal.
2022-02-04 11:54:11 -08:00
Charis Kyriakou
66c9879ce3 Fix package versions for react typings (#1118) 2022-02-04 13:39:29 +00:00
Charis Kyriakou
9c2585116a Show collapsible analyses results (#1116) 2022-02-04 08:02:21 +00:00
Andrew Eisenberg
e46c0e25e8 Update CODEOWNERS
code-scanning security experiences team should be helping to review the remote queries part of the extension.
2022-02-03 08:14:25 -08:00
Charis Kyriakou
658b0ce243 Convert re-usable components to styled-components (#1112) 2022-02-03 08:34:24 +00:00
Andrew Eisenberg
c084e31416 Simplify command expressions
Use only `||` and clearly specify when each item should be visible.
2022-02-02 13:39:12 -08:00
Andrew Eisenberg
9046844f0c Add cancellation from query history view
And tweak the commands visible from the view.
2022-02-02 13:39:12 -08:00
Charis Kyriakou
5a9b49b9bb Show remote analyses results status (#1108) 2022-02-01 17:55:10 +00:00
Andrew Eisenberg
0672133bca Ensure query text shows for empty selections
Fixes a bug where quick eval was showing empty query text.

Previously, `getQueryText` was looking up the query text when it was
called if the specified text was empty. This was removed with the
recent changes to query history. It was also a bug since the query file
could have changed after the query was run.

This change ensures that if the quick eval position is empty, the
entire line is returned as the quick eval location.
2022-02-01 06:34:48 -08:00
Andrew Eisenberg
c0de99bc42 Add tests for sort order and selection 2022-02-01 06:34:48 -08:00
Andrew Eisenberg
6dbb1a27b9 Fix sort order and selection
This commit fixes two related issues with the
history view.

1. Sort order was changing after a query item completed. The fix is a
   change in how we fire off the `onDidChangeTreeData` event. When the
   event is fired with a single item, that item is pushed to the top of
   the list. I'm not exactly sure why this wasn't happening before, but
   I suspect it was because we were refreshing the list at the same time
   as we were inserting the new item.

   The solution here is to always refresh the entire list, instead of
   single items. This is fine since re building the list is a trivial
   operation. See the `refreshTreeView()` method.

   With this change, the sort order is now stable.

2. Originally reported here: #1093
   The problem is that the internal treeView selection was not being
   updated when a new item was being added. Due to some oddities with
   the way selection works in the tree view (ie- the visible selection
   does not always match the internal selection).

   The solution is to use the current item from the `treeDataProvider`
   in `determineSelection`.

Also, this change makes the sorting more precise and fixes some typos.
2022-02-01 06:34:48 -08:00
Andrew Eisenberg
dc1bace4c6 Ensure Open Query Text works for in progress queries
Same with "Open query that produced these results".

In order to do this, needed to move the query id generation into the
InitialQueryInfo.
2022-02-01 06:34:48 -08:00
Andrew Eisenberg
afe3c56ca8 Update changelog 2022-02-01 06:34:48 -08:00
Andrew Eisenberg
a6f42e3eb3 Add query items immediately
This is a large commit and includes all the changes to add query
history items immediately. This also includes some smaller related 
changes that were hit while cleaning this area up.

The major part of this change is a refactoring of what we store in
the query history list. Previously, the `CompletedQuery` was stored.
Previously, objects of this type include all information about a query that was run
including:

- Its source file and text range (if a quick eval)
- Its database
- Its label
- The query results itself
- Metrics about the query run
- Metadata about the query itself

Now, the item stored is called a `FullQueryInfo`, which has two
properties:

- InitialQueryInfo: all the data about the query that we know _before_
  the query completes, eg- its source file and text range, database, and
  label
- CompletedQueryInfo: all the data about the query that we can only
  learn _after_ the query completes. This is an optional property.

There is also a `failureReason` property, which is an optional string
describing why the query failed.


There is also a `FullCompletedQueryInfo` type, which only exists to 
help with stronger typing. It is a `FullQueryInfo` with a non-optional
`CompletedQueryInfo`.

Most of the changes are around changing how the query history accesses
its history list.

There are some other smaller changes included here:

- New icon for completed query (previously, completed queries had no
  icons).
- New spinning icon for in progress queries.
- Better error handling in the logger to handle log messages when the
  extension is shutting down. This mostly helps clean up the output
  during tests.
- Add more disposables to subscriptions to be disposed of when the
  extension shuts down.
2022-02-01 06:34:48 -08:00
Charis Kyriakou
9c2bd2a57b Use streaming SARIF parser (#1109) 2022-01-31 16:39:20 +00:00
Charis Kyriakou
f42f474113 Use 'engines' to define required node and npm versions (#1106) 2022-01-28 15:37:04 +00:00
Henry Mercer
17c31e1539 Run CodeQL analysis with latest CLI to opt into ML-powered queries beta 2022-01-28 14:14:00 +00:00
Charis Kyriakou
b0fb4d6bc9 Upgrade React version (#1103) 2022-01-28 10:37:59 +00:00
Charis Kyriakou
f8690bcebc Auto-download analyses results (#1098) 2022-01-27 10:16:13 +00:00
shati-patel
b0410ec5de Update to VS Code 1.59.0 2022-01-26 12:31:09 +00:00
shati-patel
19e0058e61 Bump version to v1.5.11 2022-01-25 16:41:03 +00:00
49 changed files with 4320 additions and 1247 deletions

View File

@@ -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

View File

@@ -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 }}

View File

@@ -1 +1,2 @@
**/* @github/codeql-vscode-reviewers
/extensions/ql-vscode/src/remote-queries/ @github/code-scanning-secexp-reviewers

View File

@@ -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)

View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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",

View File

@@ -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,

View File

@@ -17,7 +17,7 @@ const emptyComparison: SetComparisonsMessage = {
columns: [],
commonResultSetNames: [],
currentResultSetName: '',
datebaseUri: '',
databaseUri: '',
message: 'Empty comparison'
};

View File

@@ -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>

View File

@@ -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

View File

@@ -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
};
}
}

View File

@@ -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() {

View File

@@ -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}">`;
}

View File

@@ -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

View File

@@ -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;
}
}
}

View File

@@ -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
}

View File

@@ -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();
}
}

View File

@@ -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
});
}
}

View File

@@ -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

View File

@@ -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
};
}

View File

@@ -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> {

View File

@@ -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
};
}
}

View File

@@ -1,6 +1,5 @@
export interface RemoteQueryResultIndex {
artifactsUrlPath: string;
allResultsArtifactId: number;
items: RemoteQueryResultIndexItem[];
}

View File

@@ -3,7 +3,6 @@ import { DownloadLink } from './download-link';
export interface RemoteQueryResult {
executionEndTime: Date;
analysisSummaries: AnalysisSummary[];
allResultsDownloadLink: DownloadLink;
}
export interface AnalysisSummary {

View File

@@ -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)
},
];

View File

@@ -1,5 +1,8 @@
export type AnalysisResultStatus = 'InProgress' | 'Completed' | 'Failed';
export interface AnalysisResults {
nwo: string;
status: AnalysisResultStatus;
results: QueryResult[];
}

View File

@@ -10,7 +10,6 @@ export interface RemoteQueryResult {
totalResultCount: number;
executionTimestamp: string;
executionDuration: string;
downloadLink: DownloadLink;
analysisSummaries: AnalysisSummary[]
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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&apos;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);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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: () => { /**/ },
};
}

View File

@@ -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}`);

View File

@@ -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
);
}
});

View File

@@ -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

View File

@@ -192,6 +192,9 @@ describe('helpers', () => {
}
class MockMemento implements Memento {
keys(): readonly string[] {
throw new Error('Method not implemented.');
}
map = new Map<any, any>();
/**

View File

@@ -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;
}
});

View File

@@ -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'
};
}
});

View File

@@ -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,
{

View File

@@ -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) {