Skip to content

Commit 42c642d

Browse files
committed
Add exporting of variant analysis results
This adds the export of variant analysis results. This is unfortunately a larger change than I would have liked because there are many differences in the types and I think further unification of the code might make it less clear and would actually make this code harder to read when the remote queries code is removed. In general, the idea for the export of a variant analysis follows the same process as the export of remote queries, with the difference being that variant analysis results are loaded on-the-fly from dis, rather than only loading from memory. This means it should use less memory, but it also means that the export is slower.
1 parent 62453d1 commit 42c642d

File tree

7 files changed

+299
-56
lines changed

7 files changed

+299
-56
lines changed

extensions/ql-vscode/src/extension.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,11 @@ import { RemoteQueryResult } from './remote-queries/remote-query-result';
9797
import { URLSearchParams } from 'url';
9898
import { handleDownloadPacks, handleInstallPackDependencies } from './packaging';
9999
import { HistoryItemLabelProvider } from './history-item-label-provider';
100-
import { exportRemoteQueryResults, exportSelectedRemoteQueryResults } from './remote-queries/export-results';
100+
import {
101+
exportRemoteQueryResults,
102+
exportSelectedRemoteQueryResults,
103+
exportVariantAnalysisResults
104+
} from './remote-queries/export-results';
101105
import { RemoteQuery } from './remote-queries/remote-query';
102106
import { EvalLogViewer } from './eval-log-viewer';
103107
import { SummaryLanguageSupport } from './log-insights/summary-language-support';
@@ -986,6 +990,12 @@ async function activateWithInstalledDistribution(
986990
})
987991
);
988992

993+
ctx.subscriptions.push(
994+
commandRunner('codeQL.exportVariantAnalysisResults', async (variantAnalysisId: number) => {
995+
await exportVariantAnalysisResults(ctx, variantAnalysisManager, variantAnalysisId);
996+
})
997+
);
998+
989999
ctx.subscriptions.push(
9901000
commandRunner('codeQL.loadVariantAnalysisRepoResults', async (variantAnalysisId: number, repositoryFullName: string) => {
9911001
await variantAnalysisManager.loadResults(variantAnalysisId, repositoryFullName);

extensions/ql-vscode/src/query-history.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1273,9 +1273,11 @@ export class QueryHistoryManager extends DisposableObject {
12731273
return;
12741274
}
12751275

1276-
// Remote queries only
1276+
// Remote queries and variant analysis only
12771277
if (finalSingleItem.t === 'remote') {
12781278
await commands.executeCommand('codeQL.exportRemoteQueryResults', finalSingleItem.queryId);
1279+
} else if (finalSingleItem.t === 'variant-analysis') {
1280+
await commands.executeCommand('codeQL.exportVariantAnalysisResults', finalSingleItem.variantAnalysis.id);
12791281
}
12801282
}
12811283

extensions/ql-vscode/src/remote-queries/export-results.ts

Lines changed: 160 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,50 @@
11
import * as path from 'path';
22
import * as fs from 'fs-extra';
33

4-
import { window, commands, Uri, ExtensionContext, QuickPickItem, workspace, ViewColumn } from 'vscode';
4+
import { window, commands, Uri, ExtensionContext, workspace, ViewColumn } from 'vscode';
55
import { Credentials } from '../authentication';
66
import { UserCancellationException } from '../commandRunner';
77
import { showInformationMessageWithAction } from '../helpers';
88
import { logger } from '../logging';
99
import { QueryHistoryManager } from '../query-history';
1010
import { createGist } from './gh-api/gh-api-client';
1111
import { RemoteQueriesManager } from './remote-queries-manager';
12-
import { generateMarkdown } from './remote-queries-markdown-generation';
12+
import {
13+
generateMarkdown,
14+
generateVariantAnalysisMarkdown,
15+
MarkdownFile,
16+
} from './remote-queries-markdown-generation';
1317
import { RemoteQuery } from './remote-query';
1418
import { AnalysisResults, sumAnalysesResults } from './shared/analysis-result';
1519
import { pluralize } from '../pure/word';
20+
import { VariantAnalysisManager } from './variant-analysis-manager';
21+
import { assertNever } from '../pure/helpers-pure';
22+
import {
23+
VariantAnalysis,
24+
VariantAnalysisScannedRepository,
25+
VariantAnalysisScannedRepositoryResult
26+
} from './shared/variant-analysis';
1627

1728
/**
18-
* Exports the results of the currently-selected remote query.
29+
* Exports the results of the currently-selected remote query or variant analysis.
1930
*/
2031
export async function exportSelectedRemoteQueryResults(queryHistoryManager: QueryHistoryManager): Promise<void> {
2132
const queryHistoryItem = queryHistoryManager.getCurrentQueryHistoryItem();
2233
if (!queryHistoryItem || queryHistoryItem.t === 'local') {
2334
throw new Error('No variant analysis results currently open. To open results, click an item in the query history view.');
2435
}
2536

26-
if (!queryHistoryItem.completed) {
27-
throw new Error('Variant analysis results are not yet available.');
28-
}
29-
3037
if (queryHistoryItem.t === 'remote') {
3138
return commands.executeCommand('codeQL.exportRemoteQueryResults', queryHistoryItem.queryId);
39+
} else if (queryHistoryItem.t === 'variant-analysis') {
40+
return commands.executeCommand('codeQL.exportVariantAnalysisResults', queryHistoryItem.variantAnalysis.id);
3241
} else {
33-
throw new Error('No variant analysis results currently open. To open results, click an item in the query history view.');
42+
assertNever(queryHistoryItem);
3443
}
3544
}
3645

3746
/**
38-
* Exports the results of the given or currently-selected remote query.
47+
* Exports the results of the given remote query.
3948
* The user is prompted to select the export format.
4049
*/
4150
export async function exportRemoteQueryResults(
@@ -58,32 +67,111 @@ export async function exportRemoteQueryResults(
5867
const query = queryHistoryItem.remoteQuery;
5968
const analysesResults = remoteQueriesManager.getAnalysesResults(queryHistoryItem.queryId);
6069

61-
const gistOption = {
62-
label: '$(ports-open-browser-icon) Create Gist (GitHub)',
63-
};
64-
const localMarkdownOption = {
65-
label: '$(markdown) Save as markdown',
66-
};
67-
const exportFormat = await determineExportFormat(gistOption, localMarkdownOption);
70+
const exportFormat = await determineExportFormat();
71+
if (!exportFormat) {
72+
return;
73+
}
6874

69-
if (exportFormat === gistOption) {
70-
await exportResultsToGist(ctx, query, analysesResults);
71-
} else if (exportFormat === localMarkdownOption) {
72-
const queryDirectoryPath = await queryHistoryManager.getQueryHistoryItemDirectory(
73-
queryHistoryItem
74-
);
75-
await exportResultsToLocalMarkdown(queryDirectoryPath, query, analysesResults);
75+
const exportDirectory = await queryHistoryManager.getQueryHistoryItemDirectory(queryHistoryItem);
76+
77+
await exportRemoteQueryAnalysisResults(ctx, exportDirectory, query, analysesResults, exportFormat);
78+
}
79+
80+
export async function exportRemoteQueryAnalysisResults(
81+
ctx: ExtensionContext,
82+
exportDirectory: string,
83+
query: RemoteQuery,
84+
analysesResults: AnalysisResults[],
85+
exportFormat: 'gist' | 'local',
86+
) {
87+
const description = buildGistDescription(query, analysesResults);
88+
const markdownFiles = generateMarkdown(query, analysesResults, exportFormat);
89+
90+
await exportResults(ctx, exportDirectory, description, markdownFiles, exportFormat);
91+
}
92+
93+
/**
94+
* Exports the results of the given or currently-selected remote query.
95+
* The user is prompted to select the export format.
96+
*/
97+
export async function exportVariantAnalysisResults(
98+
ctx: ExtensionContext,
99+
variantAnalysisManager: VariantAnalysisManager,
100+
variantAnalysisId: number,
101+
): Promise<void> {
102+
const variantAnalysis = await variantAnalysisManager.getVariantAnalysis(variantAnalysisId);
103+
if (!variantAnalysis) {
104+
void logger.log(`Could not find variant analysis with id ${variantAnalysisId}`);
105+
throw new Error('There was an error when trying to retrieve variant analysis information');
76106
}
107+
108+
void logger.log(`Exporting variant analysis results for variant analysis with id ${variantAnalysis.id}`);
109+
110+
const exportFormat = await determineExportFormat();
111+
if (!exportFormat) {
112+
return;
113+
}
114+
115+
async function* getAnalysesResults(): AsyncGenerator<[VariantAnalysisScannedRepository, VariantAnalysisScannedRepositoryResult]> {
116+
if (!variantAnalysis?.scannedRepos) {
117+
return;
118+
}
119+
120+
for (const repo of variantAnalysis.scannedRepos) {
121+
if (repo.resultCount == 0) {
122+
yield [repo, {
123+
variantAnalysisId: variantAnalysis.id,
124+
repositoryId: repo.repository.id,
125+
}];
126+
continue;
127+
}
128+
129+
let result: VariantAnalysisScannedRepositoryResult;
130+
131+
if (!variantAnalysisManager.areResultsLoaded(variantAnalysis.id, repo.repository.fullName)) {
132+
result = await variantAnalysisManager.loadResultsFromStorage(variantAnalysis.id, repo.repository.fullName);
133+
} else {
134+
result = await variantAnalysisManager.loadResults(variantAnalysis.id, repo.repository.fullName);
135+
}
136+
137+
yield [repo, result];
138+
}
139+
}
140+
141+
const exportDirectory = variantAnalysisManager.getVariantAnalysisStorageLocation(variantAnalysis.id);
142+
143+
await exportVariantAnalysisAnalysisResults(ctx, exportDirectory, variantAnalysis, getAnalysesResults(), exportFormat);
144+
}
145+
146+
export async function exportVariantAnalysisAnalysisResults(
147+
ctx: ExtensionContext,
148+
exportDirectory: string,
149+
variantAnalysis: VariantAnalysis,
150+
analysesResults: AsyncIterable<[VariantAnalysisScannedRepository, VariantAnalysisScannedRepositoryResult]>,
151+
exportFormat: 'gist' | 'local',
152+
) {
153+
const description = buildVariantAnalysisGistDescription(variantAnalysis);
154+
const markdownFiles = await generateVariantAnalysisMarkdown(variantAnalysis, analysesResults, 'gist');
155+
156+
await exportResults(ctx, exportDirectory, description, markdownFiles, exportFormat);
77157
}
78158

79159
/**
80160
* Determines the format in which to export the results, from the given export options.
81161
*/
82-
async function determineExportFormat(
83-
...options: { label: string }[]
84-
): Promise<QuickPickItem> {
162+
async function determineExportFormat(): Promise<'gist' | 'local' | undefined> {
163+
const gistOption = {
164+
label: '$(ports-open-browser-icon) Create Gist (GitHub)',
165+
};
166+
const localMarkdownOption = {
167+
label: '$(markdown) Save as markdown',
168+
};
169+
85170
const exportFormat = await window.showQuickPick(
86-
options,
171+
[
172+
gistOption,
173+
localMarkdownOption,
174+
],
87175
{
88176
placeHolder: 'Select export format',
89177
canPickMany: false,
@@ -93,20 +181,38 @@ async function determineExportFormat(
93181
if (!exportFormat || !exportFormat.label) {
94182
throw new UserCancellationException('No export format selected', true);
95183
}
96-
return exportFormat;
184+
185+
if (exportFormat === gistOption) {
186+
return 'gist';
187+
}
188+
if (exportFormat === localMarkdownOption) {
189+
return 'local';
190+
}
191+
192+
return undefined;
97193
}
98194

99-
/**
100-
* Converts the results of a remote query to markdown and uploads the files as a secret gist.
101-
*/
102-
export async function exportResultsToGist(
195+
export async function exportResults(
103196
ctx: ExtensionContext,
104-
query: RemoteQuery,
105-
analysesResults: AnalysisResults[]
106-
): Promise<void> {
197+
exportDirectory: string,
198+
description: string,
199+
markdownFiles: MarkdownFile[],
200+
exportFormat: 'gist' | 'local',
201+
) {
202+
if (exportFormat === 'gist') {
203+
await exportToGist(ctx, description, markdownFiles);
204+
} else if (exportFormat === 'local') {
205+
await exportToLocalMarkdown(exportDirectory, markdownFiles);
206+
}
207+
}
208+
209+
export async function exportToGist(
210+
ctx: ExtensionContext,
211+
description: string,
212+
markdownFiles: MarkdownFile[]
213+
) {
107214
const credentials = await Credentials.initialize(ctx);
108-
const description = buildGistDescription(query, analysesResults);
109-
const markdownFiles = generateMarkdown(query, analysesResults, 'gist');
215+
110216
// Convert markdownFiles to the appropriate format for uploading to gist
111217
const gistFiles = markdownFiles.reduce((acc, cur) => {
112218
acc[`${cur.fileName}.md`] = { content: cur.content.join('\n') };
@@ -137,16 +243,25 @@ const buildGistDescription = (query: RemoteQuery, analysesResults: AnalysisResul
137243
};
138244

139245
/**
140-
* Converts the results of a remote query to markdown and saves the files locally
141-
* in the query directory (where query results and metadata are also saved).
246+
* Builds Gist description
247+
* Ex: Empty Block (Go) x results (y repositories)
142248
*/
143-
async function exportResultsToLocalMarkdown(
144-
queryDirectoryPath: string,
145-
query: RemoteQuery,
146-
analysesResults: AnalysisResults[]
249+
const buildVariantAnalysisGistDescription = (variantAnalysis: VariantAnalysis) => {
250+
const resultCount = variantAnalysis.scannedRepos?.reduce((acc, item) => acc + (item.resultCount ?? 0), 0) ?? 0;
251+
const resultLabel = pluralize(resultCount, 'result', 'results');
252+
253+
const repositoryLabel = variantAnalysis.scannedRepos?.length ? `(${pluralize(variantAnalysis.scannedRepos.length, 'repository', 'repositories')})` : '';
254+
return `${variantAnalysis.query.name} (${variantAnalysis.query.language}) ${resultLabel} ${repositoryLabel}`;
255+
};
256+
257+
/**
258+
* Saves the results of an exported query to local markdown files.
259+
*/
260+
async function exportToLocalMarkdown(
261+
exportDirectory: string,
262+
markdownFiles: MarkdownFile[],
147263
) {
148-
const markdownFiles = generateMarkdown(query, analysesResults, 'local');
149-
const exportedResultsPath = path.join(queryDirectoryPath, 'exported-results');
264+
const exportedResultsPath = path.join(exportDirectory, 'exported-results');
150265
await fs.ensureDir(exportedResultsPath);
151266
for (const markdownFile of markdownFiles) {
152267
const filePath = path.join(exportedResultsPath, `${markdownFile.fileName}.md`);

0 commit comments

Comments
 (0)