diff --git a/extensions/ql-vscode/CHANGELOG.md b/extensions/ql-vscode/CHANGELOG.md index 28094e7959b..f03c85c307d 100644 --- a/extensions/ql-vscode/CHANGELOG.md +++ b/extensions/ql-vscode/CHANGELOG.md @@ -3,6 +3,7 @@ ## [UNRELEASED] - Add support for filename pattern in history view. [#930](https://github.com/github/vscode-codeql/pull/930) +- Add an option _View Results (CSV)_ to view the results of a non-alert query. The existing options for alert queries have been renamed to _View Alerts_ to avoid confusion. [#929](https://github.com/github/vscode-codeql/pull/929) ## 1.5.3 - 18 August 2021 diff --git a/extensions/ql-vscode/package.json b/extensions/ql-vscode/package.json index dbf8730c6a5..80723894c61 100644 --- a/extensions/ql-vscode/package.json +++ b/extensions/ql-vscode/package.json @@ -466,8 +466,12 @@ "title": "View Results (CSV)" }, { - "command": "codeQLQueryHistory.viewSarifResults", - "title": "View Results (SARIF)" + "command": "codeQLQueryHistory.viewCsvAlerts", + "title": "View Alerts (CSV)" + }, + { + "command": "codeQLQueryHistory.viewSarifAlerts", + "title": "View Alerts (SARIF)" }, { "command": "codeQLQueryHistory.viewDil", @@ -646,10 +650,15 @@ { "command": "codeQLQueryHistory.viewCsvResults", "group": "9_qlCommands", + "when": "view == codeQLQueryHistory && viewItem != interpretedResultsItem" + }, + { + "command": "codeQLQueryHistory.viewCsvAlerts", + "group": "9_qlCommands", "when": "view == codeQLQueryHistory && viewItem == interpretedResultsItem" }, { - "command": "codeQLQueryHistory.viewSarifResults", + "command": "codeQLQueryHistory.viewSarifAlerts", "group": "9_qlCommands", "when": "view == codeQLQueryHistory && viewItem == interpretedResultsItem" }, @@ -805,7 +814,11 @@ "when": "false" }, { - "command": "codeQLQueryHistory.viewSarifResults", + "command": "codeQLQueryHistory.viewCsvAlerts", + "when": "false" + }, + { + "command": "codeQLQueryHistory.viewSarifAlerts", "when": "false" }, { diff --git a/extensions/ql-vscode/src/contextual/locationFinder.ts b/extensions/ql-vscode/src/contextual/locationFinder.ts index 1c6e95c458b..057b69813be 100644 --- a/extensions/ql-vscode/src/contextual/locationFinder.ts +++ b/extensions/ql-vscode/src/contextual/locationFinder.ts @@ -12,7 +12,7 @@ import { ProgressCallback } from '../commandRunner'; import { KeyType } from './keyType'; import { qlpackOfDatabase, resolveQueries } from './queryResolver'; -const SELECT_QUERY_NAME = '#select'; +export const SELECT_QUERY_NAME = '#select'; export const TEMPLATE_NAME = 'selectedSourceFile'; export interface FullLocationLink extends vscode.LocationLink { diff --git a/extensions/ql-vscode/src/query-history.ts b/extensions/ql-vscode/src/query-history.ts index ef607b95320..32323ce7bab 100644 --- a/extensions/ql-vscode/src/query-history.ts +++ b/extensions/ql-vscode/src/query-history.ts @@ -312,8 +312,14 @@ export class QueryHistoryManager extends DisposableObject { ); this.push( commandRunner( - 'codeQLQueryHistory.viewSarifResults', - this.handleViewSarifResults.bind(this) + 'codeQLQueryHistory.viewCsvAlerts', + this.handleViewCsvAlerts.bind(this) + ) + ); + this.push( + commandRunner( + 'codeQLQueryHistory.viewSarifAlerts', + this.handleViewSarifAlerts.bind(this) ) ); this.push( @@ -550,7 +556,7 @@ export class QueryHistoryManager extends DisposableObject { await vscode.window.showTextDocument(doc, { preview: false }); } - async handleViewSarifResults( + async handleViewSarifAlerts( singleItem: CompletedQuery, multiSelect: CompletedQuery[] ) { @@ -578,6 +584,24 @@ export class QueryHistoryManager extends DisposableObject { if (!this.assertSingleQuery(multiSelect)) { return; } + if (await singleItem.query.hasCsv()) { + void this.tryOpenExternalFile(singleItem.query.csvPath); + return; + } + await singleItem.query.exportCsvResults(this.qs, singleItem.query.csvPath, () => { + void this.tryOpenExternalFile( + singleItem.query.csvPath + ); + }); + } + + async handleViewCsvAlerts( + singleItem: CompletedQuery, + multiSelect: CompletedQuery[] + ) { + if (!this.assertSingleQuery(multiSelect)) { + return; + } await this.tryOpenExternalFile( await singleItem.query.ensureCsvProduced(this.qs) diff --git a/extensions/ql-vscode/src/run-queries.ts b/extensions/ql-vscode/src/run-queries.ts index cd9a5429dce..bed870de29c 100644 --- a/extensions/ql-vscode/src/run-queries.ts +++ b/extensions/ql-vscode/src/run-queries.ts @@ -25,6 +25,8 @@ import * as qsClient from './queryserver-client'; import { isQuickQueryPath } from './quick-query'; import { compileDatabaseUpgradeSequence, hasNondestructiveUpgradeCapabilities, upgradeDatabaseExplicit } from './upgrades'; import { ensureMetadataIsComplete } from './query-results'; +import { SELECT_QUERY_NAME } from './contextual/locationFinder'; +import { DecodedBqrsChunk } from './pure/bqrs-cli-types'; /** * run-queries.ts @@ -216,6 +218,29 @@ export class QueryInfo { return this.dilPath; } + async exportCsvResults(qs: qsClient.QueryServerClient, csvPath: string, onFinish: () => void): Promise { + let stopDecoding = false; + const out = fs.createWriteStream(csvPath); + out.on('finish', onFinish); + out.on('error', () => { + if (!stopDecoding) { + stopDecoding = true; + void showAndLogErrorMessage(`Failed to write CSV results to ${csvPath}`); + } + }); + let nextOffset: number | undefined = 0; + while (nextOffset !== undefined && !stopDecoding) { + const chunk: DecodedBqrsChunk = await qs.cliServer.bqrsDecode(this.resultsPaths.resultsPath, SELECT_QUERY_NAME, { + pageSize: 100, + offset: nextOffset, + }); + for (const tuple of chunk.tuples) + out.write(tuple.join(',') + '\n'); + nextOffset = chunk.next; + } + out.end(); + } + async ensureCsvProduced(qs: qsClient.QueryServerClient): Promise { if (await this.hasCsv()) { return this.csvPath;