Skip to content

Commit b4fbfb6

Browse files
authored
Merge pull request #1570 from github/koesie10/variant-analysis-results-manager
Add variant analysis results manager
2 parents 86d10b4 + a0fb3b4 commit b4fbfb6

File tree

9 files changed

+289
-24
lines changed

9 files changed

+289
-24
lines changed

extensions/ql-vscode/src/extension.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -467,10 +467,16 @@ async function activateWithInstalledDistribution(
467467
const localQueryResultsView = new ResultsView(ctx, dbm, cliServer, queryServerLogger, labelProvider);
468468
ctx.subscriptions.push(localQueryResultsView);
469469

470-
void logger.log('Initializing variant analysis manager.');
470+
void logger.log('Initializing remote queries manager.');
471471
const rqm = new RemoteQueriesManager(ctx, cliServer, queryStorageDir, logger);
472472
ctx.subscriptions.push(rqm);
473473

474+
void logger.log('Initializing variant analysis manager.');
475+
const variantAnalysisStorageDir = path.join(ctx.globalStorageUri.fsPath, 'variant-analyses');
476+
await fs.ensureDir(variantAnalysisStorageDir);
477+
const variantAnalysisManager = new VariantAnalysisManager(ctx, cliServer, variantAnalysisStorageDir, logger);
478+
ctx.subscriptions.push(variantAnalysisManager);
479+
474480
void logger.log('Initializing query history.');
475481
const qhm = new QueryHistoryManager(
476482
qs,
@@ -909,7 +915,6 @@ async function activateWithInstalledDistribution(
909915
})
910916
);
911917

912-
const variantAnalysisManager = new VariantAnalysisManager(ctx, logger);
913918
ctx.subscriptions.push(
914919
commandRunner('codeQL.monitorVariantAnalysis', async (
915920
variantAnalysis: VariantAnalysis,

extensions/ql-vscode/src/remote-queries/shared/variant-analysis.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ export interface VariantAnalysisScannedRepositoryState {
9595
}
9696

9797
export interface VariantAnalysisScannedRepositoryResult {
98+
variantAnalysisId: number;
9899
repositoryId: number;
99100
interpretedResults?: AnalysisAlert[];
100101
rawResults?: AnalysisRawResults;

extensions/ql-vscode/src/remote-queries/variant-analysis-manager.ts

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
import * as ghApiClient from './gh-api/gh-api-client';
2-
import * as path from 'path';
3-
import * as fs from 'fs-extra';
42
import { CancellationToken, ExtensionContext } from 'vscode';
53
import { DisposableObject } from '../pure/disposable-object';
64
import { Logger } from '../logging';
@@ -19,18 +17,25 @@ import {
1917
import { getErrorMessage } from '../pure/helpers-pure';
2018
import { VariantAnalysisView } from './variant-analysis-view';
2119
import { VariantAnalysisViewManager } from './variant-analysis-view-manager';
20+
import { VariantAnalysisResultsManager } from './variant-analysis-results-manager';
21+
import { CodeQLCliServer } from '../cli';
2222

2323
export class VariantAnalysisManager extends DisposableObject implements VariantAnalysisViewManager<VariantAnalysisView> {
2424
private readonly variantAnalysisMonitor: VariantAnalysisMonitor;
25+
private readonly variantAnalysisResultsManager: VariantAnalysisResultsManager;
2526
private readonly views = new Map<number, VariantAnalysisView>();
2627

2728
constructor(
2829
private readonly ctx: ExtensionContext,
30+
cliServer: CodeQLCliServer,
31+
storagePath: string,
2932
logger: Logger,
3033
) {
3134
super();
3235
this.variantAnalysisMonitor = this.push(new VariantAnalysisMonitor(ctx, logger));
3336
this.variantAnalysisMonitor.onVariantAnalysisChange(this.onVariantAnalysisUpdated.bind(this));
37+
38+
this.variantAnalysisResultsManager = this.push(new VariantAnalysisResultsManager(cliServer, storagePath, logger));
3439
}
3540

3641
public async showView(variantAnalysisId: number): Promise<void> {
@@ -118,25 +123,7 @@ export class VariantAnalysisManager extends DisposableObject implements VariantA
118123
repoState.downloadStatus = VariantAnalysisScannedRepositoryDownloadStatus.InProgress;
119124
await this.onRepoStateUpdated(variantAnalysisSummary.id, repoState);
120125

121-
const resultDirectory = path.join(
122-
this.ctx.globalStorageUri.fsPath,
123-
'variant-analyses',
124-
`${variantAnalysisSummary.id}`,
125-
scannedRepo.repository.full_name
126-
);
127-
128-
const storagePath = path.join(
129-
resultDirectory,
130-
scannedRepo.repository.full_name
131-
);
132-
133-
const result = await ghApiClient.getVariantAnalysisRepoResult(
134-
credentials,
135-
repoTask.artifact_url
136-
);
137-
138-
fs.mkdirSync(resultDirectory, { recursive: true });
139-
await fs.writeFile(storagePath, JSON.stringify(result, null, 2), 'utf8');
126+
await this.variantAnalysisResultsManager.download(credentials, variantAnalysisSummary.id, repoTask);
140127
}
141128

142129
repoState.downloadStatus = VariantAnalysisScannedRepositoryDownloadStatus.Succeeded;
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import * as fs from 'fs-extra';
2+
import * as os from 'os';
3+
import * as path from 'path';
4+
5+
import { Credentials } from '../authentication';
6+
import { Logger } from '../logging';
7+
import { AnalysisAlert, AnalysisRawResults } from './shared/analysis-result';
8+
import { sarifParser } from '../sarif-parser';
9+
import { extractAnalysisAlerts } from './sarif-processing';
10+
import { CodeQLCliServer } from '../cli';
11+
import { extractRawResults } from './bqrs-processing';
12+
import { VariantAnalysisScannedRepositoryResult } from './shared/variant-analysis';
13+
import { DisposableObject, DisposeHandler } from '../pure/disposable-object';
14+
import { VariantAnalysisRepoTask } from './gh-api/variant-analysis';
15+
import * as ghApiClient from './gh-api/gh-api-client';
16+
import { EventEmitter } from 'vscode';
17+
18+
type CacheKey = `${number}/${string}`;
19+
20+
const createCacheKey = (variantAnalysisId: number, repoTask: VariantAnalysisRepoTask): CacheKey => `${variantAnalysisId}/${repoTask.repository.full_name}`;
21+
22+
export type ResultDownloadedEvent = {
23+
variantAnalysisId: number;
24+
repoTask: VariantAnalysisRepoTask;
25+
}
26+
27+
export class VariantAnalysisResultsManager extends DisposableObject {
28+
private readonly cachedResults: Map<CacheKey, VariantAnalysisScannedRepositoryResult>;
29+
30+
private readonly _onResultDownloaded = this.push(new EventEmitter<ResultDownloadedEvent>());
31+
readonly onResultDownloaded = this._onResultDownloaded.event;
32+
33+
private readonly _onResultLoaded = this.push(new EventEmitter<VariantAnalysisScannedRepositoryResult>());
34+
readonly onResultLoaded = this._onResultLoaded.event;
35+
36+
constructor(
37+
private readonly cliServer: CodeQLCliServer,
38+
private readonly storagePath: string,
39+
private readonly logger: Logger,
40+
) {
41+
super();
42+
this.cachedResults = new Map();
43+
}
44+
45+
public async download(
46+
credentials: Credentials,
47+
variantAnalysisId: number,
48+
repoTask: VariantAnalysisRepoTask,
49+
): Promise<void> {
50+
if (!repoTask.artifact_url) {
51+
throw new Error('Missing artifact URL');
52+
}
53+
54+
const resultDirectory = this.getRepoStorageDirectory(variantAnalysisId, repoTask.repository.full_name);
55+
56+
const result = await ghApiClient.getVariantAnalysisRepoResult(
57+
credentials,
58+
repoTask.artifact_url
59+
);
60+
61+
fs.mkdirSync(resultDirectory, { recursive: true });
62+
await fs.writeFile(path.join(resultDirectory, 'results.zip'), JSON.stringify(result, null, 2), 'utf8');
63+
64+
this._onResultDownloaded.fire({
65+
variantAnalysisId,
66+
repoTask,
67+
});
68+
}
69+
70+
public async loadResults(
71+
variantAnalysisId: number,
72+
repoTask: VariantAnalysisRepoTask
73+
): Promise<VariantAnalysisScannedRepositoryResult> {
74+
const result = this.cachedResults.get(createCacheKey(variantAnalysisId, repoTask));
75+
76+
return result ?? await this.loadResultsIntoMemory(variantAnalysisId, repoTask);
77+
}
78+
79+
private async loadResultsIntoMemory(
80+
variantAnalysisId: number,
81+
repoTask: VariantAnalysisRepoTask,
82+
): Promise<VariantAnalysisScannedRepositoryResult> {
83+
const result = await this.loadResultsFromStorage(variantAnalysisId, repoTask);
84+
this.cachedResults.set(createCacheKey(variantAnalysisId, repoTask), result);
85+
this._onResultLoaded.fire(result);
86+
return result;
87+
}
88+
89+
private async loadResultsFromStorage(
90+
variantAnalysisId: number,
91+
repoTask: VariantAnalysisRepoTask,
92+
): Promise<VariantAnalysisScannedRepositoryResult> {
93+
if (!(await this.isVariantAnalysisRepoDownloaded(variantAnalysisId, repoTask))) {
94+
throw new Error('Variant analysis results not downloaded');
95+
}
96+
97+
if (!repoTask.database_commit_sha || !repoTask.source_location_prefix) {
98+
throw new Error('Missing database commit SHA');
99+
}
100+
101+
const fileLinkPrefix = this.createGitHubDotcomFileLinkPrefix(repoTask.repository.full_name, repoTask.database_commit_sha);
102+
103+
const storageDirectory = this.getRepoStorageDirectory(variantAnalysisId, repoTask.repository.full_name);
104+
105+
const sarifPath = path.join(storageDirectory, 'results.sarif');
106+
const bqrsPath = path.join(storageDirectory, 'results.bqrs');
107+
if (await fs.pathExists(sarifPath)) {
108+
const interpretedResults = await this.readSarifResults(sarifPath, fileLinkPrefix);
109+
110+
return {
111+
variantAnalysisId,
112+
repositoryId: repoTask.repository.id,
113+
interpretedResults,
114+
};
115+
}
116+
117+
if (await fs.pathExists(bqrsPath)) {
118+
const rawResults = await this.readBqrsResults(bqrsPath, fileLinkPrefix, repoTask.source_location_prefix);
119+
120+
return {
121+
variantAnalysisId,
122+
repositoryId: repoTask.repository.id,
123+
rawResults,
124+
};
125+
}
126+
127+
throw new Error('Missing results file');
128+
}
129+
130+
private async isVariantAnalysisRepoDownloaded(
131+
variantAnalysisId: number,
132+
repoTask: VariantAnalysisRepoTask,
133+
): Promise<boolean> {
134+
return await fs.pathExists(this.getRepoStorageDirectory(variantAnalysisId, repoTask.repository.full_name));
135+
}
136+
137+
private async readBqrsResults(filePath: string, fileLinkPrefix: string, sourceLocationPrefix: string): Promise<AnalysisRawResults> {
138+
return await extractRawResults(this.cliServer, this.logger, filePath, fileLinkPrefix, sourceLocationPrefix);
139+
}
140+
141+
private async readSarifResults(filePath: string, fileLinkPrefix: string): Promise<AnalysisAlert[]> {
142+
const sarifLog = await sarifParser(filePath);
143+
144+
const processedSarif = extractAnalysisAlerts(sarifLog, fileLinkPrefix);
145+
if (processedSarif.errors.length) {
146+
void this.logger.log(`Error processing SARIF file: ${os.EOL}${processedSarif.errors.join(os.EOL)}`);
147+
}
148+
149+
return processedSarif.alerts;
150+
}
151+
152+
private getStorageDirectory(variantAnalysisId: number): string {
153+
return path.join(
154+
this.storagePath,
155+
`${variantAnalysisId}`
156+
);
157+
}
158+
159+
private getRepoStorageDirectory(variantAnalysisId: number, fullName: string): string {
160+
return path.join(
161+
this.getStorageDirectory(variantAnalysisId),
162+
fullName
163+
);
164+
}
165+
166+
private createGitHubDotcomFileLinkPrefix(fullName: string, sha: string): string {
167+
return `https://github.com/${fullName}/blob/${sha}`;
168+
}
169+
170+
public dispose(disposeHandler?: DisposeHandler) {
171+
super.dispose(disposeHandler);
172+
173+
this.cachedResults.clear();
174+
}
175+
}

extensions/ql-vscode/src/stories/variant-analysis/VariantAnalysisAnalyzedRepos.stories.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,14 +100,17 @@ Example.args = {
100100
},
101101
repositoryResults: [
102102
{
103+
variantAnalysisId: 1,
103104
repositoryId: 63537249,
104105
interpretedResults: interpretedResultsForRepo('facebook/create-react-app'),
105106
},
106107
{
108+
variantAnalysisId: 1,
107109
repositoryId: 167174,
108110
interpretedResults: interpretedResultsForRepo('jquery/jquery'),
109111
},
110112
{
113+
variantAnalysisId: 1,
111114
repositoryId: 237159,
112115
interpretedResults: interpretedResultsForRepo('expressjs/express'),
113116
}

extensions/ql-vscode/src/view/variant-analysis/VariantAnalysis.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { vscode } from '../vscode-api';
1414

1515
const repositoryResults: VariantAnalysisScannedRepositoryResult[] = [
1616
{
17+
variantAnalysisId: 1,
1718
repositoryId: 1,
1819
rawResults: {
1920
schema: {

extensions/ql-vscode/src/view/variant-analysis/__tests__/VariantAnalysisAnalyzedRepos.spec.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ describe(VariantAnalysisAnalyzedRepos.name, () => {
7878
render({
7979
repositoryResults: [
8080
{
81+
variantAnalysisId: 1,
8182
repositoryId: 2,
8283
interpretedResults: [
8384
{

extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/variant-analysis-manager.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@ import {
1616
import { createMockApiResponse } from '../../factories/remote-queries/gh-api/variant-analysis-api-response';
1717
import { createMockScannedRepos } from '../../factories/remote-queries/gh-api/scanned-repositories';
1818
import { createMockVariantAnalysisRepoTask } from '../../factories/remote-queries/gh-api/variant-analysis-repo-task';
19+
import { CodeQLCliServer } from '../../../cli';
20+
import { storagePath } from '../global.helper';
1921

2022
describe('Variant Analysis Manager', async function() {
2123
let sandbox: sinon.SinonSandbox;
24+
let cli: CodeQLCliServer;
2225
let cancellationTokenSource: CancellationTokenSource;
2326
let variantAnalysisManager: VariantAnalysisManager;
2427
let variantAnalysis: VariantAnalysisApiResponse;
@@ -47,7 +50,8 @@ describe('Variant Analysis Manager', async function() {
4750

4851
try {
4952
const extension = await extensions.getExtension<CodeQLExtensionInterface | Record<string, never>>('GitHub.vscode-codeql')!.activate();
50-
variantAnalysisManager = new VariantAnalysisManager(extension.ctx, logger);
53+
cli = extension.cliServer;
54+
variantAnalysisManager = new VariantAnalysisManager(extension.ctx, cli, storagePath, logger);
5155
} catch (e) {
5256
fail(e as Error);
5357
}

0 commit comments

Comments
 (0)