Skip to content

Commit 92a9993

Browse files
authored
Add support for remote queries raw results (#1198)
1 parent ed61eb0 commit 92a9993

10 files changed

Lines changed: 247 additions & 24 deletions

File tree

extensions/ql-vscode/src/extension.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -890,7 +890,7 @@ async function activateWithInstalledDistribution(
890890

891891
ctx.subscriptions.push(
892892
commandRunner('codeQL.showFakeRemoteQueryResults', async () => {
893-
const analysisResultsManager = new AnalysesResultsManager(ctx, queryStorageDir, logger);
893+
const analysisResultsManager = new AnalysesResultsManager(ctx, cliServer, queryStorageDir, logger);
894894
const rqim = new RemoteQueriesInterfaceManager(ctx, logger, analysisResultsManager);
895895
await rqim.showResults(sampleData.sampleRemoteQuery, sampleData.sampleRemoteQueryResult);
896896

extensions/ql-vscode/src/remote-queries/analyses-results-manager.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ import { Credentials } from '../authentication';
66
import { Logger } from '../logging';
77
import { downloadArtifactFromLink } from './gh-actions-api-client';
88
import { AnalysisSummary } from './shared/remote-query-result';
9-
import { AnalysisResults, AnalysisAlert } from './shared/analysis-result';
9+
import { AnalysisResults, AnalysisAlert, AnalysisRawResults } from './shared/analysis-result';
1010
import { UserCancellationException } from '../commandRunner';
1111
import { sarifParser } from '../sarif-parser';
1212
import { extractAnalysisAlerts } from './sarif-processing';
13+
import { CodeQLCliServer } from '../cli';
14+
import { extractRawResults } from './bqrs-processing';
1315

1416
export class AnalysesResultsManager {
1517
// Store for the results of various analyses for each remote query.
@@ -18,6 +20,7 @@ export class AnalysesResultsManager {
1820

1921
constructor(
2022
private readonly ctx: ExtensionContext,
23+
private readonly cliServer: CodeQLCliServer,
2124
readonly storagePath: string,
2225
private readonly logger: Logger,
2326
) {
@@ -119,15 +122,23 @@ export class AnalysesResultsManager {
119122
}
120123

121124
let newAnaysisResults: AnalysisResults;
122-
if (path.extname(artifactPath) === '.sarif') {
123-
const queryResults = await this.readResults(artifactPath);
125+
const fileExtension = path.extname(artifactPath);
126+
if (fileExtension === '.sarif') {
127+
const queryResults = await this.readSarifResults(artifactPath);
124128
newAnaysisResults = {
125129
...analysisResults,
126130
interpretedResults: queryResults,
127131
status: 'Completed'
128132
};
133+
} else if (fileExtension === '.bqrs') {
134+
const queryResults = await this.readBqrsResults(artifactPath);
135+
newAnaysisResults = {
136+
...analysisResults,
137+
rawResults: queryResults,
138+
status: 'Completed'
139+
};
129140
} else {
130-
void this.logger.log('Cannot download results. Only alert and path queries are fully supported.');
141+
void this.logger.log(`Cannot download results. File type '${fileExtension}' not supported.`);
131142
newAnaysisResults = {
132143
...analysisResults,
133144
status: 'Failed'
@@ -137,7 +148,11 @@ export class AnalysesResultsManager {
137148
void publishResults([...resultsForQuery]);
138149
}
139150

140-
private async readResults(filePath: string): Promise<AnalysisAlert[]> {
151+
private async readBqrsResults(filePath: string): Promise<AnalysisRawResults> {
152+
return await extractRawResults(this.cliServer, this.logger, filePath);
153+
}
154+
155+
private async readSarifResults(filePath: string): Promise<AnalysisAlert[]> {
141156
const sarifLog = await sarifParser(filePath);
142157

143158
const processedSarif = extractAnalysisAlerts(sarifLog);
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { CodeQLCliServer } from '../cli';
2+
import { Logger } from '../logging';
3+
import { transformBqrsResultSet } from '../pure/bqrs-cli-types';
4+
import { AnalysisRawResults } from './shared/analysis-result';
5+
import { MAX_RAW_RESULTS } from './shared/result-limits';
6+
7+
export async function extractRawResults(
8+
cliServer: CodeQLCliServer,
9+
logger: Logger,
10+
filePath: string
11+
): Promise<AnalysisRawResults> {
12+
const bqrsInfo = await cliServer.bqrsInfo(filePath);
13+
const resultSets = bqrsInfo['result-sets'];
14+
15+
if (resultSets.length < 1) {
16+
throw new Error('No result sets found in results file.');
17+
}
18+
if (resultSets.length > 1) {
19+
void logger.log('Multiple result sets found in results file. Only the first one will be used.');
20+
}
21+
22+
const schema = resultSets[0];
23+
24+
const chunk = await cliServer.bqrsDecode(
25+
filePath,
26+
schema.name,
27+
{ pageSize: MAX_RAW_RESULTS });
28+
29+
const resultSet = transformBqrsResultSet(schema, chunk);
30+
31+
const capped = !!chunk.next;
32+
33+
return { schema, resultSet, capped };
34+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export class RemoteQueriesManager extends DisposableObject {
4141
logger: Logger,
4242
) {
4343
super();
44-
this.analysesResultsManager = new AnalysesResultsManager(ctx, storagePath, logger);
44+
this.analysesResultsManager = new AnalysesResultsManager(ctx, cliServer, storagePath, logger);
4545
this.interfaceManager = new RemoteQueriesInterfaceManager(ctx, logger, this.analysesResultsManager);
4646
this.remoteQueriesMonitor = new RemoteQueriesMonitor(ctx, logger);
4747

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
1+
import { RawResultSet, ResultSetSchema } from '../../pure/bqrs-cli-types';
2+
13
export type AnalysisResultStatus = 'InProgress' | 'Completed' | 'Failed';
24

35
export interface AnalysisResults {
46
nwo: string;
57
status: AnalysisResultStatus;
68
interpretedResults: AnalysisAlert[];
9+
rawResults?: AnalysisRawResults;
10+
}
11+
12+
export interface AnalysisRawResults {
13+
schema: ResultSetSchema,
14+
resultSet: RawResultSet,
15+
capped: boolean;
716
}
817

918
export interface AnalysisAlert {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// The maximum number of raw results to read from a BQRS file.
2+
// This is used to avoid reading the entire result set into memory
3+
// and trying to render it on screen. Users will be warned if the
4+
// results are capped.
5+
export const MAX_RAW_RESULTS = 500;
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import * as React from 'react';
2+
import { Box, Link } from '@primer/react';
3+
import { CellValue, RawResultSet, ResultSetSchema } from '../../pure/bqrs-cli-types';
4+
import { useState } from 'react';
5+
import TextButton from './TextButton';
6+
7+
const numOfResultsInContractedMode = 5;
8+
9+
const Row = ({
10+
row
11+
}: {
12+
row: CellValue[]
13+
}) => (
14+
<>
15+
{row.map((cell, cellIndex) => (
16+
<Box key={cellIndex}
17+
borderColor="border.default"
18+
borderStyle="solid"
19+
justifyContent="center"
20+
alignItems="center"
21+
p={2}>
22+
<Cell value={cell} />
23+
</Box>
24+
))}
25+
</>
26+
);
27+
28+
const Cell = ({
29+
value
30+
}: {
31+
value: CellValue
32+
}) => {
33+
switch (typeof value) {
34+
case 'string':
35+
case 'number':
36+
case 'boolean':
37+
return <span>{value.toString()}</span>;
38+
case 'object':
39+
// TODO: This will be converted to a proper link once there
40+
// is support for populating link URLs.
41+
return <Link>{value.label}</Link>;
42+
}
43+
};
44+
45+
const RawResultsTable = ({
46+
schema,
47+
results
48+
}: {
49+
schema: ResultSetSchema,
50+
results: RawResultSet
51+
}) => {
52+
const [tableExpanded, setTableExpanded] = useState(false);
53+
const numOfResultsToShow = tableExpanded ? results.rows.length : numOfResultsInContractedMode;
54+
const showButton = results.rows.length > numOfResultsInContractedMode;
55+
56+
// Create n equal size columns. We use minmax(0, 1fr) because the
57+
// minimum width of 1fr is auto, not 0.
58+
// https://css-tricks.com/equal-width-columns-in-css-grid-are-kinda-weird/
59+
const gridTemplateColumns = `repeat(${schema.columns.length}, minmax(0, 1fr))`;
60+
61+
return (
62+
<>
63+
<Box
64+
display="grid"
65+
gridTemplateColumns={gridTemplateColumns}
66+
maxWidth="45rem"
67+
p={2}>
68+
{results.rows.slice(0, numOfResultsToShow).map((row, rowIndex) => (
69+
<Row key={rowIndex} row={row} />
70+
))}
71+
</Box>
72+
{
73+
showButton &&
74+
<TextButton size='x-small' onClick={() => setTableExpanded(!tableExpanded)}>
75+
{tableExpanded ? (<span>View less</span>) : (<span>View all</span>)}
76+
</TextButton>
77+
}
78+
</>
79+
);
80+
};
81+
82+
export default RawResultsTable;

extensions/ql-vscode/src/remote-queries/view/RemoteQueries.tsx

Lines changed: 59 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import * as Rdom from 'react-dom';
44
import { Flash, ThemeProvider } from '@primer/react';
55
import { ToRemoteQueriesMessage } from '../../pure/interface-types';
66
import { AnalysisSummary, RemoteQueryResult } from '../shared/remote-query-result';
7-
7+
import { MAX_RAW_RESULTS } from '../shared/result-limits';
88
import { vscode } from '../../view/vscode-api';
99

1010
import SectionTitle from './SectionTitle';
@@ -18,6 +18,7 @@ import DownloadSpinner from './DownloadSpinner';
1818
import CollapsibleItem from './CollapsibleItem';
1919
import { AlertIcon, CodeSquareIcon, FileCodeIcon, FileSymlinkFileIcon, RepoIcon, TerminalIcon } from '@primer/octicons-react';
2020
import AnalysisAlertResult from './AnalysisAlertResult';
21+
import RawResultsTable from './RawResultsTable';
2122

2223
const numOfReposInContractedMode = 10;
2324

@@ -72,8 +73,13 @@ const openQueryTextVirtualFile = (queryResult: RemoteQueryResult) => {
7273
});
7374
};
7475

76+
const getAnalysisResultCount = (analysisResults: AnalysisResults): number => {
77+
const rawResultCount = analysisResults.rawResults?.resultSet.rows.length || 0;
78+
return analysisResults.interpretedResults.length + rawResultCount;
79+
};
80+
7581
const sumAnalysesResults = (analysesResults: AnalysisResults[]) =>
76-
analysesResults.reduce((acc, curr) => acc + curr.interpretedResults.length, 0);
82+
analysesResults.reduce((acc, curr) => acc + getAnalysisResultCount(curr), 0);
7783

7884
const QueryInfo = (queryResult: RemoteQueryResult) => (
7985
<>
@@ -249,22 +255,41 @@ const AnalysesResultsTitle = ({ totalAnalysesResults, totalResults }: { totalAna
249255
return <SectionTitle>{totalAnalysesResults}/{totalResults} results</SectionTitle>;
250256
};
251257

252-
const AnalysesResultsDescription = ({ totalAnalysesResults, totalResults }: { totalAnalysesResults: number, totalResults: number }) => {
253-
if (totalAnalysesResults < totalResults) {
254-
return <>
255-
<VerticalSpace size={1} />
256-
Some results haven&apos;t been downloaded automatically because of their size or because enough were downloaded already.
257-
Download them manually from the list above if you want to see them here.
258-
</>;
259-
}
258+
const AnalysesResultsDescription = ({
259+
queryResult,
260+
analysesResults,
261+
}: {
262+
queryResult: RemoteQueryResult
263+
analysesResults: AnalysisResults[],
264+
}) => {
265+
const showDownloadsMessage = queryResult.analysisSummaries.some(
266+
s => !analysesResults.some(a => a.nwo === s.nwo && a.status === 'Completed'));
267+
const downloadsMessage = <>
268+
<VerticalSpace size={1} />
269+
Some results haven&apos;t been downloaded automatically because of their size or because enough were downloaded already.
270+
Download them manually from the list above if you want to see them here.
271+
</>;
272+
273+
const showMaxResultsMessage = analysesResults.some(a => a.rawResults?.capped);
274+
const maxRawResultsMessage = <>
275+
<VerticalSpace size={1} />
276+
Some repositories have more than {MAX_RAW_RESULTS} results. We will only show you up to {MAX_RAW_RESULTS}
277+
results for each repository.
278+
</>;
260279

261-
return <></>;
280+
return (
281+
<>
282+
{showDownloadsMessage && downloadsMessage}
283+
{showMaxResultsMessage && maxRawResultsMessage}
284+
</>
285+
);
262286
};
263287

264288
const RepoAnalysisResults = (analysisResults: AnalysisResults) => {
289+
const numOfResults = getAnalysisResultCount(analysisResults);
265290
const title = <>
266291
{analysisResults.nwo}
267-
<Badge text={analysisResults.interpretedResults.length.toString()} />
292+
<Badge text={numOfResults.toString()} />
268293
</>;
269294

270295
return (
@@ -276,11 +301,24 @@ const RepoAnalysisResults = (analysisResults: AnalysisResults) => {
276301
<VerticalSpace size={2} />
277302
</li>)}
278303
</ul>
304+
{analysisResults.rawResults &&
305+
<RawResultsTable
306+
schema={analysisResults.rawResults.schema}
307+
results={analysisResults.rawResults.resultSet} />
308+
}
279309
</CollapsibleItem>
280310
);
281311
};
282312

283-
const AnalysesResults = ({ analysesResults, totalResults }: { analysesResults: AnalysisResults[], totalResults: number }) => {
313+
const AnalysesResults = ({
314+
queryResult,
315+
analysesResults,
316+
totalResults
317+
}: {
318+
queryResult: RemoteQueryResult,
319+
analysesResults: AnalysisResults[],
320+
totalResults: number
321+
}) => {
284322
const totalAnalysesResults = sumAnalysesResults(analysesResults);
285323

286324
if (totalResults === 0) {
@@ -294,10 +332,10 @@ const AnalysesResults = ({ analysesResults, totalResults }: { analysesResults: A
294332
totalAnalysesResults={totalAnalysesResults}
295333
totalResults={totalResults} />
296334
<AnalysesResultsDescription
297-
totalAnalysesResults={totalAnalysesResults}
298-
totalResults={totalResults} />
335+
queryResult={queryResult}
336+
analysesResults={analysesResults} />
299337
<ul className="vscode-codeql__flat-list">
300-
{analysesResults.filter(a => a.interpretedResults.length > 0).map(r =>
338+
{analysesResults.filter(a => a.interpretedResults.length > 0 || a.rawResults).map(r =>
301339
<li key={r.nwo} className="vscode-codeql__analyses-results-list-item">
302340
<RepoAnalysisResults {...r} />
303341
</li>)}
@@ -340,7 +378,11 @@ export function RemoteQueries(): JSX.Element {
340378
<QueryInfo {...queryResult} />
341379
<Failures {...queryResult} />
342380
<Summary queryResult={queryResult} analysesResults={analysesResults} />
343-
{showAnalysesResults && <AnalysesResults analysesResults={analysesResults} totalResults={queryResult.totalResultCount} />}
381+
{showAnalysesResults &&
382+
<AnalysesResults
383+
queryResult={queryResult}
384+
analysesResults={analysesResults}
385+
totalResults={queryResult.totalResultCount} />}
344386
</ThemeProvider>
345387
</div>;
346388
} catch (err) {
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import * as React from 'react';
2+
import styled from 'styled-components';
3+
4+
type Size = 'x-small' | 'small' | 'medium' | 'large' | 'x-large';
5+
6+
const StyledButton = styled.button<{ size: Size }>`
7+
background: none;
8+
color: var(--vscode-textLink-foreground);
9+
border: none;
10+
cursor: pointer;
11+
font-size: ${props => props.size};
12+
`;
13+
14+
const TextButton = ({
15+
size,
16+
onClick,
17+
children
18+
}: {
19+
size: Size,
20+
onClick: () => void,
21+
children: React.ReactNode
22+
}) => (
23+
<StyledButton
24+
size={size}
25+
onClick={onClick}>
26+
{children}
27+
</StyledButton>
28+
);
29+
30+
export default TextButton;

0 commit comments

Comments
 (0)