diff --git a/extensions/ql-vscode/media/drive.svg b/extensions/ql-vscode/media/drive.svg
new file mode 100644
index 000000000..48bd917dd
--- /dev/null
+++ b/extensions/ql-vscode/media/drive.svg
@@ -0,0 +1,7 @@
+
diff --git a/extensions/ql-vscode/src/compare/compare-interface.ts b/extensions/ql-vscode/src/compare/compare-interface.ts
index 50597e7d9..4cb14a21f 100644
--- a/extensions/ql-vscode/src/compare/compare-interface.ts
+++ b/extensions/ql-vscode/src/compare/compare-interface.ts
@@ -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
) {
super();
}
async showResults(
- from: CompletedQuery,
- to: CompletedQuery,
+ from: FullCompletedQueryInfo,
+ to: FullCompletedQueryInfo,
selectedResultSetName?: string
) {
this.comparePair = { from, to };
@@ -80,17 +80,13 @@ 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,
+ name: from.getShortLabel(),
+ status: from.completedQuery.statusString,
time: from.time,
},
toQuery: {
- name: to.options.label
- ? to.interpolate(to.getLabel())
- : to.queryName,
- status: to.statusString,
+ name: to.getShortLabel(),
+ status: to.completedQuery.statusString,
time: to.time,
},
},
@@ -99,7 +95,7 @@ export class CompareInterfaceManager extends DisposableObject {
currentResultSetName: currentResultSetName,
rows,
message,
- datebaseUri: to.database.databaseUri,
+ datebaseUri: 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')
@@ -143,11 +139,11 @@ export class CompareInterfaceManager extends DisposableObject {
scriptPathOnDisk,
[stylesheetPathOnDisk]
);
- panel.webview.onDidReceiveMessage(
+ this.push(panel.webview.onDidReceiveMessage(
async (e) => this.handleMsgFromView(e),
undefined,
ctx.subscriptions
- );
+ ));
}
return this.panel;
}
@@ -191,15 +187,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 +211,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,
diff --git a/extensions/ql-vscode/src/contextual/locationFinder.ts b/extensions/ql-vscode/src/contextual/locationFinder.ts
index 057b69813..a331ac97f 100644
--- a/extensions/ql-vscode/src/contextual/locationFinder.ts
+++ b/extensions/ql-vscode/src/contextual/locationFinder.ts
@@ -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 {
- 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
diff --git a/extensions/ql-vscode/src/contextual/templateProvider.ts b/extensions/ql-vscode/src/contextual/templateProvider.ts
index 8da041f46..69ba4c49d 100644
--- a/extensions/ql-vscode/src/contextual/templateProvider.ts
+++ b/extensions/ql-vscode/src/contextual/templateProvider.ts
@@ -18,7 +18,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,15 +123,20 @@ export class TemplateQueryReferenceProvider implements ReferenceProvider {
}
}
+type QueryWithDb = {
+ query: QueryWithResults,
+ dbUri: Uri
+};
+
export class TemplatePrintAstProvider {
- private cache: CachedOperation;
+ private cache: CachedOperation;
constructor(
private cli: CodeQLCliServer,
private qs: QueryServerClient,
private dbm: DatabaseManager,
) {
- this.cache = new CachedOperation(this.getAst.bind(this));
+ this.cache = new CachedOperation(this.getAst.bind(this));
}
async provideAst(
@@ -142,13 +147,13 @@ export class TemplatePrintAstProvider {
if (!document) {
throw new Error('Cannot view the AST. Please select a valid source file inside a CodeQL database.');
}
- const queryResults = this.shouldCache()
+ const { query, dbUri } = this.shouldCache()
? await this.cache.get(document.uri.toString(), progress, token)
: await this.getAst(document.uri.toString(), progress, token);
return new AstBuilder(
- queryResults, this.cli,
- this.dbm.findDatabaseItem(Uri.parse(queryResults.database.databaseUri!, true))!,
+ query, this.cli,
+ this.dbm.findDatabaseItem(dbUri)!,
document.fileName
);
}
@@ -161,7 +166,7 @@ export class TemplatePrintAstProvider {
uriString: string,
progress: ProgressCallback,
token: CancellationToken
- ): Promise {
+ ): Promise {
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 +200,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
+ };
}
}
diff --git a/extensions/ql-vscode/src/extension.ts b/extensions/ql-vscode/src/extension.ts
index c1277a864..14080c718 100644
--- a/extensions/ql-vscode/src/extension.ts
+++ b/extensions/ql-vscode/src/extension.ts
@@ -59,10 +59,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';
@@ -432,7 +432,7 @@ 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(
@@ -440,7 +440,7 @@ async function activateWithInstalledDistribution(
ctx.extensionPath,
queryHistoryConfigurationListener,
showResults,
- async (from: CompletedQuery, to: CompletedQuery) =>
+ async (from: FullCompletedQueryInfo, to: FullCompletedQueryInfo) =>
showResultsForComparison(from, to),
);
ctx.subscriptions.push(qhm);
@@ -462,8 +462,8 @@ async function activateWithInstalledDistribution(
archiveFilesystemProvider.activate(ctx);
async function showResultsForComparison(
- from: CompletedQuery,
- to: CompletedQuery
+ from: FullCompletedQueryInfo,
+ to: FullCompletedQueryInfo
): Promise {
try {
await cmpm.showResults(from, to);
@@ -473,7 +473,7 @@ async function activateWithInstalledDistribution(
}
async function showResultsForCompletedQuery(
- query: CompletedQuery,
+ query: FullCompletedQueryInfo,
forceReveal: WebviewReveal
): Promise {
await intm.showResults(query, forceReveal, false);
@@ -493,22 +493,35 @@ 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(),
+ };
+
+ const initialInfo = await createInitialQueryInfo(selectedQuery, databaseInfo, quickEval, range);
+ const item = new FullQueryInfo(initialInfo, queryHistoryConfigurationListener);
+ qhm.addCompletedQuery(item);
+ await qhm.refreshTreeView(item);
+
+ try {
+ const info = await compileAndRunQueryAgainstDatabase(
+ cliServer,
+ qs,
+ databaseItem,
+ initialInfo,
+ progress,
+ token,
+ );
+ item.completeThisQuery(info);
+ 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 {
+ await qhm.refreshTreeView(item);
+ }
}
}
@@ -1027,7 +1040,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() {
diff --git a/extensions/ql-vscode/src/interface.ts b/extensions/ql-vscode/src/interface.ts
index eb40ee9b0..16b2a5a2f 100644
--- a/extensions/ql-vscode/src/interface.ts
+++ b/extensions/ql-vscode/src/interface.ts
@@ -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 { QueryEvaluatonInfo, 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')
);
@@ -195,11 +196,11 @@ export class InterfaceManager extends DisposableObject {
scriptPathOnDisk,
[stylesheetPathOnDisk]
);
- panel.webview.onDidReceiveMessage(
+ this.push(panel.webview.onDidReceiveMessage(
async (e) => this.handleMsgFromView(e),
undefined,
ctx.subscriptions
- );
+ ));
}
return this._panel;
}
@@ -238,7 +239,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 +255,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 +315,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.has(msg.selectedTable) || false
);
}
break;
@@ -347,7 +348,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 +356,27 @@ export class InterfaceManager extends DisposableObject {
* history entry.
*/
public async showResults(
- results: CompletedQuery,
+ fullQuery: FullCompletedQueryInfo,
forceReveal: WebviewReveal,
shouldKeepOldResultsWhileRendering = false
): Promise {
- 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(
+ fullQuery.completedQuery.sortedResultsInfo.forEach(
(v, k) =>
(sortedResultsMap[k] = this.convertPathPropertiesToWebviewUris(v))
);
- this._displayedQuery = results;
+ this._displayedQuery = fullQuery;
const panel = this.getPanel();
await this.waitForPanelLoaded();
@@ -388,7 +389,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 +408,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 +418,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();
const chunk = await this.cliServer.bqrsDecode(
resultsPath,
@@ -432,7 +433,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 +447,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.completedQuery.query.program.queryPath
});
}
@@ -476,25 +477,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.completedQuery.query.program.queryPath
});
}
- private async getResultSetSchemas(results: CompletedQuery, selectedTable = ''): Promise {
- const resultsPath = results.getResultsPath(selectedTable);
+ private async getResultSetSchemas(completedQuery: CompletedQueryInfo, selectedTable = ''): Promise {
+ const resultsPath = completedQuery.getResultsPath(selectedTable);
const schemas = await this.cliServer.bqrsInfo(
resultsPath,
PAGE_SIZE.getValue()
@@ -521,17 +522,17 @@ export class InterfaceManager extends DisposableObject {
}
const sortedResultsMap: SortedResultsMap = {};
- results.sortedResultsInfo.forEach(
+ results.completedQuery.sortedResultsInfo.forEach(
(v, k) =>
(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 +543,7 @@ export class InterfaceManager extends DisposableObject {
const pageSize = PAGE_SIZE.getValue();
const chunk = await this.cliServer.bqrsDecode(
- results.getResultsPath(selectedTable, sorted),
+ results.completedQuery.getResultsPath(selectedTable, sorted),
schema.name,
{
offset: schema.pagination?.offsets[pageNumber],
@@ -564,17 +565,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.completedQuery.query.program.queryPath
});
}
@@ -643,7 +644,7 @@ export class InterfaceManager extends DisposableObject {
}
private async interpretResultsInfo(
- query: QueryInfo,
+ query: QueryEvaluatonInfo,
sortState: InterpretedResultsSortState | undefined
): Promise {
if (
diff --git a/extensions/ql-vscode/src/logging.ts b/extensions/ql-vscode/src/logging.ts
index c1b31bfc3..70d8ad8bb 100644
--- a/extensions/ql-vscode/src/logging.ts
+++ b/extensions/ql-vscode/src/logging.ts
@@ -74,31 +74,39 @@ export class OutputChannelLogger extends DisposableObject implements Logger {
* continuing.
*/
async log(message: string, options = {} as LogOptions): Promise {
- 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;
+ }
}
}
diff --git a/extensions/ql-vscode/src/query-history.ts b/extensions/ql-vscode/src/query-history.ts
index cd097ffa8..cc016cd19 100644
--- a/extensions/ql-vscode/src/query-history.ts
+++ b/extensions/ql-vscode/src/query-history.ts
@@ -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,7 @@ 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';
/**
* query-history.ts
@@ -26,12 +38,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,6 +65,11 @@ const SHOW_QUERY_TEXT_QUICK_EVAL_MSG = `\
*/
const FAILED_QUERY_HISTORY_ITEM_ICON = 'media/red-x.svg';
+/**
+ * Path to icon to display next to a successful local run.
+ */
+const LOCAL_SUCCESS_QUERY_HISTORY_ITEM_ICON = 'media/drive.svg';
+
enum SortOrder {
NameAsc = 'NameAsc',
NameDesc = 'NameDesc',
@@ -74,19 +85,21 @@ enum SortOrder {
export class HistoryTreeDataProvider extends DisposableObject {
private _sortOrder = SortOrder.DateAsc;
- private _onDidChangeTreeData = super.push(new vscode.EventEmitter());
+ private _onDidChangeTreeData = super.push(new EventEmitter());
- readonly onDidChangeTreeData: vscode.Event = this
+ readonly onDidChangeTreeData: Event = this
._onDidChangeTreeData.event;
- private history: CompletedQuery[] = [];
+ private history: FullQueryInfo[] = [];
private failedIconPath: string;
+ private localSuccessIconPath: string;
+
/**
* When not undefined, must be reference-equal to an item in `this.databases`.
*/
- private current: CompletedQuery | undefined;
+ private current: FullQueryInfo | undefined;
constructor(extensionPath: string) {
super();
@@ -94,10 +107,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 {
- const treeItem = new vscode.TreeItem(element.toString());
+ async getTreeItem(element: FullQueryInfo): Promise {
+ const treeItem = new TreeItem(element.label);
treeItem.command = {
title: 'Query History Item',
@@ -108,61 +125,77 @@ export class HistoryTreeDataProvider extends DisposableObject {
// 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();
+ const hasResults = await element.completedQuery?.query.hasInterpretedResults();
treeItem.contextValue = hasResults
? 'interpretedResultsItem'
: 'rawResultsItem';
- if (!element.didRunSuccessfully) {
- treeItem.iconPath = this.failedIconPath;
+ switch (element.status) {
+ case QueryStatus.InProgress:
+ // TODO this is not a good icon.
+ treeItem.iconPath = new ThemeIcon('sync~spin');
+ break;
+ case QueryStatus.Completed:
+ treeItem.iconPath = this.localSuccessIconPath;
+ break;
+ case QueryStatus.Failed:
+ treeItem.iconPath = this.failedIconPath;
+ break;
+ default:
+ assertNever(element.status);
}
return treeItem;
}
getChildren(
- element?: CompletedQuery
- ): vscode.ProviderResult {
- return element ? [] : this.history.sort((q1, q2) => {
+ element?: FullQueryInfo
+ ): ProviderResult {
+ return element ? [] : this.history.sort((h1, h2) => {
+ const q1 = h1.completedQuery;
+ const q2 = h2.completedQuery;
+
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;
+ return (!q1 || !q2) ? 0 : q1.resultCount - q2.resultCount;
case SortOrder.CountDesc:
- return q2.resultCount - q1.resultCount;
+ return (!q1 || !q2) ? 0 : q2.resultCount - q1.resultCount;
default:
assertNever(this.sortOrder);
}
});
}
- getParent(_element: CompletedQuery): vscode.ProviderResult {
+ getParent(_element: FullQueryInfo): ProviderResult {
return null;
}
- getCurrent(): CompletedQuery | undefined {
+ getCurrent(): FullQueryInfo | undefined {
return this.current;
}
- pushQuery(item: CompletedQuery): void {
+ pushQuery(item: FullQueryInfo): void {
this.current = item;
this.history.push(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) {
+ if (this.current === item) {
+ this.current = undefined;
+ }
const index = this.history.findIndex((i) => i === item);
if (index >= 0) {
this.history.splice(index, 1);
@@ -175,16 +208,12 @@ export class HistoryTreeDataProvider extends DisposableObject {
}
}
- 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(item?: FullQueryInfo) {
+ this._onDidChangeTreeData.fire(item);
}
public get sortOrder() {
@@ -204,33 +233,32 @@ 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;
- lastItemClick: { time: Date; item: CompletedQuery } | undefined;
- compareWithItem: CompletedQuery | undefined;
+ treeView: TreeView;
+ lastItemClick: { time: Date; item: FullQueryInfo } | undefined;
+ compareWithItem: FullQueryInfo | undefined;
constructor(
private qs: QueryServerClient,
extensionPath: string,
- private queryHistoryConfigListener: QueryHistoryConfig,
- private selectedCallback: (item: CompletedQuery) => Promise,
+ queryHistoryConfigListener: QueryHistoryConfig,
+ private selectedCallback: (item: FullCompletedQueryInfo) => Promise,
private doCompareCallback: (
- from: CompletedQuery,
- to: CompletedQuery
+ from: FullCompletedQueryInfo,
+ to: FullCompletedQueryInfo
) => Promise
) {
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)
@@ -331,20 +359,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 {
+ uri: Uri
+ ): ProviderResult {
const params = new URLSearchParams(uri.query);
return (
@@ -353,19 +383,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 {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);
if (!this.assertSingleQuery(finalMultiSelect)) {
@@ -376,19 +406,23 @@ export class QueryHistoryManager extends DisposableObject {
throw new Error(NO_QUERY_SELECTED);
}
- const textDocument = await vscode.workspace.openTextDocument(
- vscode.Uri.file(finalSingleItem.query.program.queryPath)
+ if (!finalSingleItem.completedQuery) {
+ throw new Error('Select a completed query.');
+ }
+
+ const textDocument = await workspace.openTextDocument(
+ Uri.file(finalSingleItem.completedQuery.query.program.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,14 +431,14 @@ 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();
+ item.completedQuery?.dispose();
});
const current = this.treeDataProvider.getCurrent();
if (current !== undefined) {
@@ -438,22 +472,22 @@ export class QueryHistoryManager extends DisposableObject {
}
async handleSetLabel(
- singleItem: CompletedQuery,
- multiSelect: CompletedQuery[]
+ singleItem: FullQueryInfo,
+ multiSelect: FullQueryInfo[]
): Promise {
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;
+ singleItem.initialInfo.userSpecifiedLabel = response === '' ? undefined : response;
if (this.treeDataProvider.sortOrder === SortOrder.NameAsc ||
this.treeDataProvider.sortOrder === SortOrder.NameDesc) {
this.treeDataProvider.refresh();
@@ -464,19 +498,19 @@ export class QueryHistoryManager extends DisposableObject {
}
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 +518,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 +550,27 @@ 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 handleShowQueryText(
- singleItem: CompletedQuery,
- multiSelect: CompletedQuery[]
+ singleItem: FullQueryInfo,
+ multiSelect: FullQueryInfo[]
) {
if (!this.assertSingleQuery(multiSelect)) {
return;
@@ -542,35 +580,38 @@ 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),
- queryText: encodeURIComponent(await this.getQueryText(singleItem)),
- });
- const uri = vscode.Uri.parse(
- `codeql:${singleItem.query.queryID}-${queryName}?${params.toString()}`, true
- );
- const doc = await vscode.workspace.openTextDocument(uri);
- await vscode.window.showTextDocument(doc, { preview: false });
- }
-
- async handleViewSarifAlerts(
- singleItem: CompletedQuery,
- multiSelect: CompletedQuery[]
- ) {
- if (!this.assertSingleQuery(multiSelect)) {
+ if (!singleItem.completedQuery) {
return;
}
- const hasInterpretedResults = await singleItem.query.canHaveInterpretedResults();
+ const rawQueryName = singleItem.getQueryName();
+ const queryName = rawQueryName.endsWith('.ql') ? rawQueryName : rawQueryName + '.ql';
+ const params = new URLSearchParams({
+ isQuickEval: String(!!singleItem.completedQuery.query.quickEvalPosition),
+ queryText: encodeURIComponent(await this.getQueryText(singleItem)),
+ });
+ const uri = Uri.parse(
+ `codeql:${singleItem.completedQuery.query.queryID}-${queryName}?${params.toString()}`, true
+ );
+ const doc = await workspace.openTextDocument(uri);
+ await window.showTextDocument(doc, { preview: false });
+ }
+
+ async handleViewSarifAlerts(
+ singleItem: FullQueryInfo,
+ multiSelect: FullQueryInfo[]
+ ) {
+ if (!this.assertSingleQuery(multiSelect) || !singleItem.completedQuery) {
+ return;
+ }
+ const query = singleItem.completedQuery.query;
+ const hasInterpretedResults = await 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 +619,87 @@ 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)
);
}
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 {
- if (queryHistoryItem.options.queryText) {
- return queryHistoryItem.options.queryText;
- } else if (queryHistoryItem.query.quickEvalPosition) {
+ async getQueryText(queryHistoryItem: FullQueryInfo): Promise {
+ if (queryHistoryItem.initialInfo.queryText) {
+ return queryHistoryItem.initialInfo.queryText;
+ }
+
+ if (!queryHistoryItem.completedQuery) {
+ return '';
+ }
+
+ const query = queryHistoryItem.completedQuery.query;
+
+ if (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
+ const startLine = query.quickEvalPosition.line;
+ const endLine = query.quickEvalPosition.endLine;
+ const textDocument = await workspace.openTextDocument(
+ query.quickEvalPosition.fileName
);
return textDocument.getText(
- new vscode.Range(startLine - 1, 0, endLine, 0)
+ new Range(startLine - 1, 0, endLine, 0)
);
} else {
return '';
}
}
- buildCompletedQuery(info: QueryWithResults): CompletedQuery {
- const item = new CompletedQuery(info, this.queryHistoryConfigListener);
- return item;
- }
-
- addCompletedQuery(item: CompletedQuery) {
+ addCompletedQuery(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.
*
@@ -674,9 +721,9 @@ export class QueryHistoryManager extends DisposableObject {
}
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 +740,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 +754,26 @@ the file in the file explorer and dragging it into the workspace.`
}
private async findOtherQueryToCompare(
- singleItem: CompletedQuery,
- multiSelect: CompletedQuery[]
- ): Promise {
- const dbName = singleItem.database.name;
+ singleItem: FullQueryInfo,
+ multiSelect: FullQueryInfo[]
+ ): Promise {
+ 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 +788,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 +832,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 (
@@ -799,9 +853,9 @@ 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[] } {
+ singleItem: FullQueryInfo,
+ multiSelect: FullQueryInfo[]
+ ): { finalSingleItem: FullQueryInfo; finalMultiSelect: FullQueryInfo[] } {
if (singleItem === undefined && (multiSelect === undefined || multiSelect.length === 0 || multiSelect[0] === undefined)) {
const selection = this.treeView.selection;
if (selection) {
@@ -817,7 +871,7 @@ the file in the file explorer and dragging it into the workspace.`
};
}
- async refreshTreeView(completedQuery: CompletedQuery): Promise {
- this.treeDataProvider.refresh(completedQuery);
+ async refreshTreeView(item: FullQueryInfo): Promise {
+ this.treeDataProvider.refresh(item);
}
}
diff --git a/extensions/ql-vscode/src/query-results.ts b/extensions/ql-vscode/src/query-results.ts
index c2a5922f9..8752b1be7 100644
--- a/extensions/ql-vscode/src/query-results.ts
+++ b/extensions/ql-vscode/src/query-results.ts
@@ -1,23 +1,46 @@
import { env } from 'vscode';
-import { QueryWithResults, tmpDir, QueryInfo } from './run-queries';
+import { QueryWithResults, tmpDir, QueryEvaluatonInfo } 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';
-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
+ readonly isQuickQuery: boolean;
+ readonly isQuickEval: boolean;
+ readonly quickEvalPosition?: messages.Position;
+ readonly queryPath: string;
+ readonly databaseInfo: DatabaseInfo
+ readonly start: Date;
+}
+
+export enum QueryStatus {
+ InProgress = 'InProgress',
+ Completed = 'Completed',
+ Failed = 'Failed',
+}
+
+export class CompletedQueryInfo implements QueryWithResults {
+ readonly query: QueryEvaluatonInfo;
readonly result: messages.EvaluationResult;
- readonly database: DatabaseInfo;
readonly logFileLocation?: string;
- options: QueryHistoryItemOptions;
resultCount: number;
dispose: () => void;
@@ -37,17 +60,12 @@ export class CompletedQuery implements QueryWithResults {
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;
this.dispose = evaluation.dispose;
- this.date = new Date();
- this.time = this.date.toLocaleString(env.language);
this.sortedResultsInfo = new Map();
this.resultCount = 0;
}
@@ -56,26 +74,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';
@@ -90,36 +98,10 @@ export class CompletedQuery implements QueryWithResults {
|| 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,
@@ -151,36 +133,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 +163,121 @@ 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 {
+ public failureReason: string | undefined;
+ public completedQuery: CompletedQueryInfo | undefined;
+
+ constructor(
+ public readonly initialInfo: InitialQueryInfo,
+ private readonly config: QueryHistoryConfig,
+ ) {
+ /**/
+ }
+
+ get time() {
+ 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.time,
+ 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;
+ }
+ }
+}
diff --git a/extensions/ql-vscode/src/remote-queries/remote-queries-interface.ts b/extensions/ql-vscode/src/remote-queries/remote-queries-interface.ts
index 45a13910a..cbdd28980 100644
--- a/extensions/ql-vscode/src/remote-queries/remote-queries-interface.ts
+++ b/extensions/ql-vscode/src/remote-queries/remote-queries-interface.ts
@@ -58,7 +58,7 @@ export class RemoteQueriesInterfaceManager {
/**
* 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.
@@ -127,10 +127,12 @@ export class RemoteQueriesInterfaceManager {
scriptPathOnDisk,
[baseStylesheetUriOnDisk, stylesheetPathOnDisk]
);
- 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;
diff --git a/extensions/ql-vscode/src/run-queries.ts b/extensions/ql-vscode/src/run-queries.ts
index 1154241d8..7dcb60e84 100644
--- a/extensions/ql-vscode/src/run-queries.ts
+++ b/extensions/ql-vscode/src/run-queries.ts
@@ -22,7 +22,7 @@ import { ProgressCallback, UserCancellationException } from './commandRunner';
import { DatabaseInfo, QueryMetadata, ResultsPaths } 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';
@@ -53,7 +53,7 @@ export const tmpDirDisposal = {
* temporary files associated with it, such as the compiled query
* output and results.
*/
-export class QueryInfo {
+export class QueryEvaluatonInfo {
private static nextQueryId = 0;
readonly compiledQueryPath: string;
@@ -71,7 +71,7 @@ export class QueryInfo {
public readonly metadata?: QueryMetadata,
public readonly templates?: messages.TemplateDefinitions,
) {
- this.queryID = QueryInfo.nextQueryId++;
+ this.queryID = QueryEvaluatonInfo.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`);
@@ -269,10 +269,8 @@ export class QueryInfo {
export interface QueryWithResults {
- readonly query: QueryInfo;
+ readonly query: QueryEvaluatonInfo;
readonly result: messages.EvaluationResult;
- readonly database: DatabaseInfo;
- readonly options: QueryHistoryItemOptions;
readonly logFileLocation?: string;
readonly dispose: () => void;
}
@@ -356,7 +354,7 @@ async function getSelectedPosition(editor: TextEditor, range?: Range): Promise {
@@ -398,7 +396,7 @@ async function checkDbschemeCompatibility(
}
}
-function reportNoUpgradePath(query: QueryInfo) {
+function reportNoUpgradePath(query: QueryEvaluatonInfo) {
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.`);
}
@@ -408,7 +406,7 @@ function reportNoUpgradePath(query: QueryInfo) {
async function compileNonDestructiveUpgrade(
qs: qsClient.QueryServerClient,
upgradeTemp: tmp.DirectoryResult,
- query: QueryInfo,
+ query: QueryEvaluatonInfo,
progress: ProgressCallback,
token: CancellationToken,
): Promise {
@@ -557,32 +555,19 @@ export async function compileAndRunQueryAgainstDatabase(
cliServer: cli.CodeQLCliServer,
qs: qsClient.QueryServerClient,
db: DatabaseItem,
- quickEval: boolean,
- selectedQueryUri: Uri | undefined,
+ initialInfo: InitialQueryInfo,
progress: ProgressCallback,
token: CancellationToken,
templates?: messages.TemplateDefinitions,
- range?: Range
): Promise {
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');
- }
-
// 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.');
@@ -596,7 +581,7 @@ export async function compileAndRunQueryAgainstDatabase(
const dbSchemaName = path.basename(db.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 (${db.name}): their target languages are different. Please select a different database and try again.`);
}
const qlProgram: messages.QlProgram = {
@@ -608,7 +593,7 @@ export async function compileAndRunQueryAgainstDatabase(
// 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
+ queryPath: initialInfo.queryPath
};
// Read the query metadata if possible, to use in the UI.
@@ -632,7 +617,7 @@ export async function compileAndRunQueryAgainstDatabase(
}
}
- const query = new QueryInfo(qlProgram, db, packConfig.dbscheme, quickEvalPosition, metadata, templates);
+ const query = new QueryEvaluatonInfo(qlProgram, db, packConfig.dbscheme, initialInfo.quickEvalPosition, metadata, templates);
const upgradeDir = await tmp.dir({ dir: upgradesTmpDir.name, unsafeCleanup: true });
try {
@@ -647,7 +632,7 @@ export async function compileAndRunQueryAgainstDatabase(
errors = await query.compile(qs, 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;
}
@@ -663,11 +648,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);
@@ -688,16 +668,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 +688,36 @@ export async function compileAndRunQueryAgainstDatabase(
}
}
+export async function createInitialQueryInfo(
+ selectedQueryUri: Uri | undefined,
+ databaseInfo: DatabaseInfo,
+ isQuickEval: boolean, range?: Range
+): Promise {
+ // 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,
+ start: new Date(),
+ ... (isQuickEval ? {
+ queryText: quickEvalText,
+ 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: QueryEvaluatonInfo,
message: string,
resultType: number
): QueryWithResults {
@@ -729,11 +731,6 @@ function createSyntheticResult(
runId: -1,
message
},
- database: {
- name: db.name,
- databaseUri: db.databaseUri.toString(true)
- },
- options: historyItemOptions,
dispose: () => { /**/ },
};
}
diff --git a/extensions/ql-vscode/src/vscode-tests/cli-integration/queries.test.ts b/extensions/ql-vscode/src/vscode-tests/cli-integration/queries.test.ts
index 826a23fd3..20a7c7932 100644
--- a/extensions/ql-vscode/src/vscode-tests/cli-integration/queries.test.ts
+++ b/extensions/ql-vscode/src/vscode-tests/cli-integration/queries.test.ts
@@ -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
+ );
+ }
});
diff --git a/extensions/ql-vscode/src/vscode-tests/no-workspace/query-history.test.ts b/extensions/ql-vscode/src/vscode-tests/no-workspace/query-history.test.ts
index e7f36f21b..9513967fe 100644
--- a/extensions/ql-vscode/src/vscode-tests/no-workspace/query-history.test.ts
+++ b/extensions/ql-vscode/src/vscode-tests/no-workspace/query-history.test.ts
@@ -6,8 +6,11 @@ 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 { QueryEvaluatonInfo, 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';
chai.use(chaiAsPromised);
const expect = chai.expect;
@@ -15,10 +18,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 +45,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 +99,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 +126,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 +138,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 +159,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 +173,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 +184,116 @@ 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 function() {
+ queryHistoryManager = await createMockQueryHistory(allHistory);
+ // deleting the first item when a different item is selected
+ // will not change the selection
+ const toDelete = allHistory[1];
+ const selected = allHistory[3];
+ // avoid triggering the callback by setting the field directly
+ (queryHistoryManager.treeDataProvider as any).current = selected;
+ await queryHistoryManager.handleRemoveHistoryItem(toDelete, [toDelete]);
+
+ expect(toDelete.completedQuery!.dispose).to.have.been.calledOnce;
+ expect(queryHistoryManager.treeDataProvider.getCurrent()).to.eq(selected);
+ expect(allHistory).not.to.contain(toDelete);
+
+ // the current item should have been re-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
+ (queryHistoryManager.treeDataProvider as any).current = toDelete;
+ await queryHistoryManager.handleRemoveHistoryItem(toDelete, [toDelete]);
+
+ expect(toDelete.completedQuery!.dispose).to.have.been.calledOnce;
+ expect(queryHistoryManager.treeDataProvider.getCurrent()).to.eq(newSelected);
+ expect(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 +303,107 @@ 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({
+ id: 'check', color: undefined
+ });
});
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({
+ id: 'check', color: undefined
+ });
});
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: 'search-refresh', 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[]) {
- 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,
- };
-}
+ function createMockFullQueryInfo(dbName = 'a', queryWitbResults?: QueryWithResults, isFail = false): FullQueryInfo {
+ const fqi = new FullQueryInfo(
+ {
+ databaseInfo: { name: dbName },
+ start: new Date(),
+ queryPath: 'hucairz'
+ } as InitialQueryInfo,
+ configListener
+ );
+
+ 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 QueryEvaluatonInfo,
+ 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,
+ 'xxx',
+ configListener,
+ selectedCallback,
+ doCompareCallback
+ );
+ (qhm.treeDataProvider as any).history = allHistory;
+ await vscode.workspace.saveAll();
+
+ return qhm;
+ }
+});
diff --git a/extensions/ql-vscode/src/vscode-tests/no-workspace/query-results.test.ts b/extensions/ql-vscode/src/vscode-tests/no-workspace/query-results.test.ts
index dedf26d80..444014aa2 100644
--- a/extensions/ql-vscode/src/vscode-tests/no-workspace/query-results.test.ts
+++ b/extensions/ql-vscode/src/vscode-tests/no-workspace/query-results.test.ts
@@ -3,162 +3,186 @@ 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 { QueryEvaluatonInfo, QueryWithResults, tmpDir } from '../../run-queries';
import { QueryHistoryConfig } from '../../config';
import { EvaluationResult, QueryResultType } from '../../pure/messages';
import { SortDirection, SortedResultSetInfo } from '../../pure/interface-types';
import { CodeQLCliServer, SourceInfo } from '../../cli';
+import { env } from 'process';
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 sandbox: sinon.SinonSandbox;
beforeEach(() => {
- disposeSpy = Sinon.spy();
- onDidChangeQueryHistoryConfigurationSpy = Sinon.spy();
+ sandbox = sinon.createSandbox();
+ disposeSpy = sandbox.spy();
+ onDidChangeQueryHistoryConfigurationSpy = sandbox.spy();
});
- 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!;
+ // from results path
+ expect(completedQuery.getResultsPath('zxa', false)).to.eq('/a/b/c');
+
+ completedQuery.sortedResultsInfo.set('zxa', {
+ resultsPath: 'bxa'
+ } as SortedResultSetInfo);
+
+ // still from results path
+ expect(completedQuery.getResultsPath('zxa', false)).to.eq('/a/b/c');
+
+ // 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 () => {
+ 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
+ };
+ await completedQuery.updateSortState(mockServer, 'result-name', sortState);
+ const expectedPath = path.join(tmpDir.name, 'sortedResults6789-result-name.bqrs');
+ expect(spy).to.have.been.calledWith(
+ '/a/b/c',
+ expectedPath,
+ 'result-name',
+ [sortState.columnIndex],
+ [sortState.sortDirection],
+ );
+
+ expect(completedQuery.sortedResultsInfo.get('result-name')).to.deep.equal({
+ resultsPath: expectedPath,
+ sortState
+ });
+
+ // delete the sort stae
+ await completedQuery.updateSortState(mockServer, 'result-name');
+ expect(completedQuery.sortedResultsInfo.size).to.eq(0);
+ });
+
+ // interpolate
+ // time
+ // label
+ // getShortLabel
+ // getQueryFileName
+ // getQueryName
+
+ // status
});
it('should interpretResults', async () => {
- const spy = Sinon.mock();
+ const spy = sandbox.mock();
spy.returns('1234');
const mockServer = {
interpretBqrs: spy
@@ -221,43 +245,52 @@ describe('CompletedQuery', () => {
expect(results3).to.deep.eq({ a: 6 });
});
- function mockCompletedQuery() {
- return new CompletedQuery(
- mockQueryWithResults(),
- mockQueryHistoryConfig()
- );
- }
-
- function mockQueryWithResults(): QueryWithResults {
+ function createMockQueryWithResults(didRunSuccessfully = true, hasInterpretedResults = true): QueryWithResults {
return {
query: {
- program: {
- queryPath: 'stu'
+ hasInterpretedResults: () => Promise.resolve(hasInterpretedResults),
+ queryID: 6789,
+ metadata: {
+ name: 'vwx'
},
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
+ resultsPath: '/a/b/c',
+ interpretedResultsPath: '/d/e/f'
+ }
+ } as QueryEvaluatonInfo,
+ result: {
+ evaluationTime: 12340,
+ resultType: didRunSuccessfully
+ ? QueryResultType.SUCCESS
+ : QueryResultType.OTHER_ERROR
+ } as EvaluationResult,
+ dispose: disposeSpy,
};
}
+ function createMockFullQueryInfo(dbName = 'a', queryWitbResults?: QueryWithResults, isFail = false): FullQueryInfo {
+ const fqi = new FullQueryInfo(
+ {
+ databaseInfo: { name: dbName },
+ start: new Date(),
+ queryPath: 'path/to/hucairz'
+ } as InitialQueryInfo,
+ mockQueryHistoryConfig()
+ );
+
+ 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'
};
}
});
diff --git a/extensions/ql-vscode/src/vscode-tests/no-workspace/run-queries.test.ts b/extensions/ql-vscode/src/vscode-tests/no-workspace/run-queries.test.ts
index 14847bf77..064b02932 100644
--- a/extensions/ql-vscode/src/vscode-tests/no-workspace/run-queries.test.ts
+++ b/extensions/ql-vscode/src/vscode-tests/no-workspace/run-queries.test.ts
@@ -5,7 +5,7 @@ import 'sinon-chai';
import * as sinon from 'sinon';
import * as chaiAsPromised from 'chai-as-promised';
-import { QueryInfo } from '../../run-queries';
+import { QueryEvaluatonInfo } from '../../run-queries';
import { QlProgram, Severity, compileQuery } from '../../pure/messages';
import { DatabaseItem } from '../../databases';
@@ -13,7 +13,7 @@ chai.use(chaiAsPromised);
const expect = chai.expect;
describe('run-queries', () => {
- it('should create a QueryInfo', () => {
+ it('should create a QueryEvaluatonInfo', () => {
const info = createMockQueryInfo();
const queryID = info.queryID;
@@ -85,7 +85,7 @@ describe('run-queries', () => {
});
function createMockQueryInfo() {
- return new QueryInfo(
+ return new QueryEvaluatonInfo(
'my-program' as unknown as QlProgram,
{
contents: {