diff --git a/extensions/ql-vscode/src/history-item-label-provider.ts b/extensions/ql-vscode/src/history-item-label-provider.ts index dc17c2b5aef..d7c460e3839 100644 --- a/extensions/ql-vscode/src/history-item-label-provider.ts +++ b/extensions/ql-vscode/src/history-item-label-provider.ts @@ -4,6 +4,8 @@ import { QueryHistoryConfig } from './config'; import { LocalQueryInfo, QueryHistoryInfo } from './query-results'; import { RemoteQueryHistoryItem } from './remote-queries/remote-query-history-item'; import { pluralize } from './helpers'; +import { VariantAnalysisHistoryItem } from './remote-queries/variant-analysis-history-item'; +import { assertNever } from './pure/helpers-pure'; interface InterpolateReplacements { t: string; // Start time @@ -21,9 +23,20 @@ export class HistoryItemLabelProvider { } getLabel(item: QueryHistoryInfo) { - const replacements = item.t === 'local' - ? this.getLocalInterpolateReplacements(item) - : this.getRemoteInterpolateReplacements(item); + let replacements: InterpolateReplacements; + switch (item.t) { + case 'local': + replacements = this.getLocalInterpolateReplacements(item); + break; + case 'remote': + replacements = this.getRemoteInterpolateReplacements(item); + break; + case 'variant-analysis': + replacements = this.getVariantAnalysisInterpolateReplacements(item); + break; + default: + assertNever(item); + } const rawLabel = item.userSpecifiedLabel ?? (this.config.format || '%q'); @@ -37,11 +50,20 @@ export class HistoryItemLabelProvider { * @returns the name of the query, unless there is a custom label for this query. */ getShortLabel(item: QueryHistoryInfo): string { - return item.userSpecifiedLabel - ? this.getLabel(item) - : item.t === 'local' - ? item.getQueryName() - : item.remoteQuery.queryName; + if (item.userSpecifiedLabel) { + return this.getLabel(item); + } else { + switch (item.t) { + case 'local': + return item.getQueryName(); + case 'remote': + return item.remoteQuery.queryName; + case 'variant-analysis': + return item.variantAnalysis.query.name; + default: + assertNever(item); + } + } } @@ -90,4 +112,17 @@ export class HistoryItemLabelProvider { '%': '%' }; } + + private getVariantAnalysisInterpolateReplacements(item: VariantAnalysisHistoryItem): InterpolateReplacements { + const resultCount = item.resultCount ? `(${pluralize(item.resultCount, 'result', 'results')})` : ''; + return { + t: new Date(item.variantAnalysis.executionStartTime).toLocaleString(env.language), + q: `${item.variantAnalysis.query.name} (${item.variantAnalysis.query.language})`, + d: 'TODO', + r: resultCount, + s: item.status, + f: path.basename(item.variantAnalysis.query.filePath), + '%': '%', + }; + } } diff --git a/extensions/ql-vscode/src/query-history.ts b/extensions/ql-vscode/src/query-history.ts index 5b09f73393b..4a61eb2358f 100644 --- a/extensions/ql-vscode/src/query-history.ts +++ b/extensions/ql-vscode/src/query-history.ts @@ -206,13 +206,9 @@ export class HistoryTreeDataProvider extends DisposableObject implements TreeDat const h1Label = this.labelProvider.getLabel(h1).toLowerCase(); const h2Label = this.labelProvider.getLabel(h2).toLowerCase(); - const h1Date = h1.t === 'local' - ? h1.initialInfo.start.getTime() - : h1.remoteQuery?.executionStartTime; + const h1Date = this.getItemDate(h1); - const h2Date = h2.t === 'local' - ? h2.initialInfo.start.getTime() - : h2.remoteQuery?.executionStartTime; + const h2Date = this.getItemDate(h2); const resultCount1 = h1.t === 'local' ? h1.completedQuery?.resultCount ?? -1 @@ -311,6 +307,19 @@ export class HistoryTreeDataProvider extends DisposableObject implements TreeDat this._sortOrder = newSortOrder; this._onDidChangeTreeData.fire(undefined); } + + private getItemDate(item: QueryHistoryInfo) { + switch (item.t) { + case 'local': + return item.initialInfo.start.getTime(); + case 'remote': + return item.remoteQuery.executionStartTime; + case 'variant-analysis': + return item.variantAnalysis.executionStartTime; + default: + assertNever(item); + } + } } export class QueryHistoryManager extends DisposableObject { @@ -649,10 +658,20 @@ export class QueryHistoryManager extends DisposableObject { return; } - const queryPath = finalSingleItem.t === 'local' - ? finalSingleItem.initialInfo.queryPath - : finalSingleItem.remoteQuery.queryFilePath; - + let queryPath: string; + switch (finalSingleItem.t) { + case 'local': + queryPath = finalSingleItem.initialInfo.queryPath; + break; + case 'remote': + queryPath = finalSingleItem.remoteQuery.queryFilePath; + break; + case 'variant-analysis': + queryPath = finalSingleItem.variantAnalysis.query.filePath; + break; + default: + assertNever(finalSingleItem); + } const textDocument = await workspace.openTextDocument( Uri.file(queryPath) ); @@ -710,8 +729,12 @@ export class QueryHistoryManager extends DisposableObject { // We need to delete it from disk as well. await item.completedQuery?.query.deleteQuery(); } - } else { + } else if (item.t === 'remote') { await this.removeRemoteQuery(item); + } else if (item.t === 'variant-analysis') { + // TODO + } else { + assertNever(item); } })); @@ -1025,15 +1048,20 @@ export class QueryHistoryManager extends DisposableObject { isQuickEval: String(!!(finalSingleItem.t === 'local' && finalSingleItem.initialInfo.quickEvalPosition)), queryText: encodeURIComponent(await this.getQueryText(finalSingleItem)), }); - const queryId = finalSingleItem.t === 'local' - ? finalSingleItem.initialInfo.id - : finalSingleItem.queryId; - const uri = Uri.parse( - `codeql:${queryId}?${params.toString()}`, true - ); - const doc = await workspace.openTextDocument(uri); - await window.showTextDocument(doc, { preview: false }); + if (finalSingleItem.t === 'variant-analysis') { + // TODO + } else { + const queryId = finalSingleItem.t === 'local' + ? finalSingleItem.initialInfo.id + : finalSingleItem.queryId; + + const uri = Uri.parse( + `codeql:${queryId}?${params.toString()}`, true + ); + const doc = await workspace.openTextDocument(uri); + await window.showTextDocument(doc, { preview: false }); + } } async handleViewSarifAlerts( @@ -1149,9 +1177,16 @@ export class QueryHistoryManager extends DisposableObject { } async getQueryText(item: QueryHistoryInfo): Promise { - return item.t === 'local' - ? item.initialInfo.queryText - : item.remoteQuery.queryText; + switch (item.t) { + case 'local': + return item.initialInfo.queryText; + case 'remote': + return item.remoteQuery.queryText; + case 'variant-analysis': + return 'TODO'; + default: + assertNever(item); + } } async handleExportResults(): Promise { diff --git a/extensions/ql-vscode/src/query-results.ts b/extensions/ql-vscode/src/query-results.ts index 8b643cefd67..43b13a02a1d 100644 --- a/extensions/ql-vscode/src/query-results.ts +++ b/extensions/ql-vscode/src/query-results.ts @@ -19,6 +19,7 @@ import { QueryStatus } from './query-status'; import { RemoteQueryHistoryItem } from './remote-queries/remote-query-history-item'; import { QueryEvaluationInfo, QueryWithResults } from './run-queries-shared'; import { formatLegacyMessage } from './legacy-query-server/run-queries'; +import { VariantAnalysisHistoryItem } from './remote-queries/variant-analysis-history-item'; /** * query-results.ts @@ -201,13 +202,13 @@ export function ensureMetadataIsComplete(metadata: QueryMetadata | undefined) { } /** - * Used in Interface and Compare-Interface for queries that we know have been complated. + * Used in Interface and Compare-Interface for queries that we know have been completed. */ export type CompletedLocalQueryInfo = LocalQueryInfo & { completedQuery: CompletedQueryInfo }; -export type QueryHistoryInfo = LocalQueryInfo | RemoteQueryHistoryItem; +export type QueryHistoryInfo = LocalQueryInfo | RemoteQueryHistoryItem | VariantAnalysisHistoryItem; export class LocalQueryInfo { readonly t = 'local'; diff --git a/extensions/ql-vscode/src/query-serialization.ts b/extensions/ql-vscode/src/query-serialization.ts index 521e02f78ec..8b6a6d259c6 100644 --- a/extensions/ql-vscode/src/query-serialization.ts +++ b/extensions/ql-vscode/src/query-serialization.ts @@ -15,8 +15,8 @@ export async function slurpQueryHistory(fsPath: string): Promise { - if (q.t === 'remote') { + if (q.t === 'remote' || q.t === 'variant-analysis') { // the slurper doesn't know where the remote queries are stored // so we need to assume here that they exist. Later, we check to // see if they exist on disk. @@ -90,7 +90,7 @@ export async function splatQueryHistory(queries: QueryHistoryInfo[], fsPath: str // remove incomplete local queries since they cannot be recreated on restart const filteredQueries = queries.filter(q => q.t === 'local' ? q.completedQuery !== undefined : true); const data = JSON.stringify({ - version: 1, + version: 2, // version 2 adds the `variant-analysis` type. queries: filteredQueries }, null, 2); await fs.writeFile(fsPath, data); diff --git a/extensions/ql-vscode/src/remote-queries/variant-analysis-history-item.ts b/extensions/ql-vscode/src/remote-queries/variant-analysis-history-item.ts new file mode 100644 index 00000000000..49ce619cdea --- /dev/null +++ b/extensions/ql-vscode/src/remote-queries/variant-analysis-history-item.ts @@ -0,0 +1,15 @@ +import { QueryStatus } from '../query-status'; +import { VariantAnalysis } from './shared/variant-analysis'; + +/** + * Information about a variant analysis. + */ +export interface VariantAnalysisHistoryItem { + readonly t: 'variant-analysis'; + failureReason?: string; + resultCount?: number; + status: QueryStatus; + completed: boolean; + variantAnalysis: VariantAnalysis; + userSpecifiedLabel?: string; +} 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 12272345f30..3c9396cd36e 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 @@ -290,7 +290,7 @@ describe('query-results', () => { it('should handle an invalid query history version', async () => { const badPath = path.join(tmpDir.name, 'bad-query-history.json'); fs.writeFileSync(badPath, JSON.stringify({ - version: 2, + version: 3, queries: allHistory }), 'utf8'); diff --git a/extensions/ql-vscode/src/vscode-tests/no-workspace/remote-queries/remote-query-history.test.ts b/extensions/ql-vscode/src/vscode-tests/no-workspace/remote-queries/remote-query-history.test.ts index 88efae7b239..2fa9e1d64d7 100644 --- a/extensions/ql-vscode/src/vscode-tests/no-workspace/remote-queries/remote-query-history.test.ts +++ b/extensions/ql-vscode/src/vscode-tests/no-workspace/remote-queries/remote-query-history.test.ts @@ -164,7 +164,7 @@ describe('Remote queries and query history manager', function() { // also, both queries should be removed from on disk storage expect(fs.readJSONSync(path.join(STORAGE_DIR, 'workspace-query-history.json'))).to.deep.eq({ - version: 1, + version: 2, queries: [] }); });