Skip to content

Commit 63a5021

Browse files
authored
Use sarif parser for reopened results (#1457)
1 parent e891169 commit 63a5021

File tree

3 files changed

+232
-68
lines changed

3 files changed

+232
-68
lines changed

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { DatabaseInfo } from './pure/interface-types';
1818
import { QueryStatus } from './query-status';
1919
import { QueryEvaluationInfo, QueryWithResults } from './run-queries-shared';
2020
import { formatLegacyMessage } from './legacy-query-server/run-queries';
21+
import { sarifParser } from './sarif-parser';
2122

2223
/**
2324
* query-results.ts
@@ -158,10 +159,12 @@ export async function interpretResultsSarif(
158159
sourceInfo?: cli.SourceInfo
159160
): Promise<SarifInterpretationData> {
160161
const { resultsPath, interpretedResultsPath } = resultsPaths;
162+
let res;
161163
if (await fs.pathExists(interpretedResultsPath)) {
162-
return { ...JSON.parse(await fs.readFile(interpretedResultsPath, 'utf8')), t: 'SarifInterpretationData' };
164+
res = await sarifParser(interpretedResultsPath);
165+
} else {
166+
res = await cli.interpretBqrsSarif(ensureMetadataIsComplete(metadata), resultsPath, interpretedResultsPath, sourceInfo);
163167
}
164-
const res = await cli.interpretBqrsSarif(ensureMetadataIsComplete(metadata), resultsPath, interpretedResultsPath, sourceInfo);
165168
return { ...res, t: 'SarifInterpretationData' };
166169
}
167170

extensions/ql-vscode/src/sarif-parser.ts

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,34 @@
11
import * as Sarif from 'sarif';
22
import * as fs from 'fs-extra';
3-
import { parser } from 'stream-json';
4-
import { pick } from 'stream-json/filters/Pick';
5-
import Assembler = require('stream-json/Assembler');
6-
import { chain } from 'stream-chain';
3+
import { connectTo } from 'stream-json/Assembler';
74
import { getErrorMessage } from './pure/helpers-pure';
5+
import { withParser } from 'stream-json/filters/Pick';
86

97
const DUMMY_TOOL: Sarif.Tool = { driver: { name: '' } };
108

119
export async function sarifParser(interpretedResultsPath: string): Promise<Sarif.Log> {
1210
try {
1311
// Parse the SARIF file into token streams, filtering out only the results array.
14-
const p = parser();
15-
const pipeline = chain([
16-
fs.createReadStream(interpretedResultsPath),
17-
p,
18-
pick({ filter: 'runs.0.results' })
19-
]);
12+
const pipeline = fs.createReadStream(interpretedResultsPath).pipe(withParser({ filter: 'runs.0.results' }));
2013

2114
// Creates JavaScript objects from the token stream
22-
const asm = Assembler.connectTo(pipeline);
15+
const asm = connectTo(pipeline);
2316

24-
// Returns a constructed Log object with the results or an empty array if no results were found.
17+
// Returns a constructed Log object with the results of an empty array if no results were found.
2518
// If the parser fails for any reason, it will reject the promise.
2619
return await new Promise((resolve, reject) => {
20+
let alreadyDone = false;
2721
pipeline.on('error', (error) => {
2822
reject(error);
2923
});
3024

25+
// If the parser pipeline completes before the assembler, we've reached end of file and have not found any results.
26+
pipeline.on('end', () => {
27+
if (!alreadyDone) {
28+
reject(new Error('Invalid SARIF file: expecting at least one run with result.'));
29+
}
30+
});
31+
3132
asm.on('done', (asm) => {
3233

3334
const log: Sarif.Log = {
@@ -41,6 +42,7 @@ export async function sarifParser(interpretedResultsPath: string): Promise<Sarif
4142
};
4243

4344
resolve(log);
45+
alreadyDone = true;
4446
});
4547
});
4648
} catch (e) {

extensions/ql-vscode/src/vscode-tests/no-workspace/query-results.test.ts

Lines changed: 213 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { expect } from 'chai';
22
import * as path from 'path';
33
import * as fs from 'fs-extra';
4+
import * as os from 'os';
45
import * as sinon from 'sinon';
56
import { LocalQueryInfo, InitialQueryInfo, interpretResultsSarif } from '../../query-results';
67
import { QueryWithResults } from '../../run-queries-shared';
@@ -11,6 +12,7 @@ import { tmpDir } from '../../helpers';
1112
import { slurpQueryHistory, splatQueryHistory } from '../../query-serialization';
1213
import { formatLegacyMessage, QueryInProgress } from '../../legacy-query-server/run-queries';
1314
import { EvaluationResult, QueryResultType } from '../../pure/legacy-messages';
15+
import Sinon = require('sinon');
1416

1517
describe('query-results', () => {
1618
let disposeSpy: sinon.SinonSpy;
@@ -155,68 +157,213 @@ describe('query-results', () => {
155157
});
156158
});
157159

158-
it('should interpretResultsSarif', async () => {
159-
const spy = sandbox.mock();
160-
spy.returns({ a: '1234' });
161-
const mockServer = {
162-
interpretBqrsSarif: spy
163-
} as unknown as CodeQLCliServer;
164-
165-
const interpretedResultsPath = path.join(tmpDir.name, 'interpreted.json');
166-
const resultsPath = '123';
167-
const sourceInfo = {};
160+
describe('interpretResultsSarif', () => {
161+
let mockServer: CodeQLCliServer;
162+
let spy: Sinon.SinonExpectation;
168163
const metadata = {
169164
kind: 'my-kind',
170165
id: 'my-id' as string | undefined,
171166
scored: undefined
172167
};
173-
const results1 = await interpretResultsSarif(
174-
mockServer,
175-
metadata,
176-
{
177-
resultsPath, interpretedResultsPath
178-
},
179-
sourceInfo as SourceInfo
180-
);
168+
const resultsPath = '123';
169+
const interpretedResultsPath = path.join(tmpDir.name, 'interpreted.json');
170+
const sourceInfo = {};
181171

182-
expect(results1).to.deep.eq({ a: '1234', t: 'SarifInterpretationData' });
183-
expect(spy).to.have.been.calledWith(
184-
metadata,
185-
resultsPath, interpretedResultsPath, sourceInfo
186-
);
172+
beforeEach(() => {
173+
spy = sandbox.mock();
174+
spy.returns({ a: '1234' });
187175

188-
// Try again, but with no id
189-
spy.reset();
190-
spy.returns({ a: '1234' });
191-
delete metadata.id;
192-
const results2 = await interpretResultsSarif(
193-
mockServer,
194-
metadata,
195-
{
196-
resultsPath, interpretedResultsPath
197-
},
198-
sourceInfo as SourceInfo
199-
);
200-
expect(results2).to.deep.eq({ a: '1234', t: 'SarifInterpretationData' });
201-
expect(spy).to.have.been.calledWith(
202-
{ kind: 'my-kind', id: 'dummy-id', scored: undefined },
203-
resultsPath, interpretedResultsPath, sourceInfo
204-
);
176+
mockServer = {
177+
interpretBqrsSarif: spy
178+
} as unknown as CodeQLCliServer;
179+
});
205180

206-
// try a third time, but this time we get from file
207-
spy.reset();
208-
fs.writeFileSync(interpretedResultsPath, JSON.stringify({
209-
a: 6
210-
}), 'utf8');
211-
const results3 = await interpretResultsSarif(
212-
mockServer,
213-
metadata,
214-
{
215-
resultsPath, interpretedResultsPath
216-
},
217-
sourceInfo as SourceInfo
218-
);
219-
expect(results3).to.deep.eq({ a: 6, t: 'SarifInterpretationData' });
181+
afterEach(async () => {
182+
sandbox.restore();
183+
safeDel(interpretedResultsPath);
184+
});
185+
186+
it('should interpretResultsSarif', async function() {
187+
// up to 2 minutes per test
188+
this.timeout(2 * 60 * 1000);
189+
190+
const results = await interpretResultsSarif(
191+
mockServer,
192+
metadata,
193+
{
194+
resultsPath, interpretedResultsPath
195+
},
196+
sourceInfo as SourceInfo
197+
);
198+
199+
expect(results).to.deep.eq({ a: '1234', t: 'SarifInterpretationData' });
200+
expect(spy).to.have.been.calledWith(
201+
metadata,
202+
resultsPath, interpretedResultsPath, sourceInfo
203+
);
204+
});
205+
206+
it('should interpretBqrsSarif without ID', async function() {
207+
// up to 2 minutes per test
208+
this.timeout(2 * 60 * 1000);
209+
210+
delete metadata.id;
211+
const results = await interpretResultsSarif(
212+
mockServer,
213+
metadata,
214+
{
215+
resultsPath, interpretedResultsPath
216+
},
217+
sourceInfo as SourceInfo
218+
);
219+
expect(results).to.deep.eq({ a: '1234', t: 'SarifInterpretationData' });
220+
expect(spy).to.have.been.calledWith(
221+
{ kind: 'my-kind', id: 'dummy-id', scored: undefined },
222+
resultsPath, interpretedResultsPath, sourceInfo
223+
);
224+
});
225+
226+
it('should use sarifParser on a valid small SARIF file', async function() {
227+
// up to 2 minutes per test
228+
this.timeout(2 * 60 * 1000);
229+
230+
fs.writeFileSync(interpretedResultsPath, JSON.stringify({
231+
runs: [{ results: [] }] // A run needs results to succeed.
232+
}), 'utf8');
233+
const results = await interpretResultsSarif(
234+
mockServer,
235+
metadata,
236+
{
237+
resultsPath, interpretedResultsPath
238+
},
239+
sourceInfo as SourceInfo
240+
);
241+
// We do not re-interpret if we are reading from a SARIF file.
242+
expect(spy).to.not.have.been.called;
243+
244+
expect(results).to.have.property('t', 'SarifInterpretationData');
245+
expect(results).to.have.nested.property('runs[0].results');
246+
});
247+
248+
it('should throw an error on an invalid small SARIF file', async function() {
249+
// up to 2 minutes per test
250+
this.timeout(2 * 60 * 1000);
251+
252+
fs.writeFileSync(interpretedResultsPath, JSON.stringify({
253+
a: '6' // Invalid: no runs or results
254+
}), 'utf8');
255+
256+
await expect(
257+
interpretResultsSarif(
258+
mockServer,
259+
metadata,
260+
{
261+
resultsPath, interpretedResultsPath
262+
},
263+
sourceInfo as SourceInfo)
264+
).to.be.rejectedWith('Parsing output of interpretation failed: Invalid SARIF file: expecting at least one run with result.');
265+
266+
// We do not attempt to re-interpret if we are reading from a SARIF file.
267+
expect(spy).to.not.have.been.called;
268+
});
269+
270+
it('should use sarifParser on a valid large SARIF file', async function() {
271+
// up to 2 minutes per test
272+
this.timeout(2 * 60 * 1000);
273+
274+
const validSarifStream = fs.createWriteStream(interpretedResultsPath, { flags: 'w' });
275+
276+
const finished = new Promise((res, rej) => {
277+
validSarifStream.addListener('close', res);
278+
validSarifStream.addListener('error', rej);
279+
});
280+
281+
validSarifStream.write(JSON.stringify({
282+
runs: [{ results: [] }] // A run needs results to succeed.
283+
}), 'utf8');
284+
285+
validSarifStream.write('[', 'utf8');
286+
const iterations = 1_000_000;
287+
for (let i = 0; i < iterations; i++) {
288+
validSarifStream.write(JSON.stringify({
289+
a: '6'
290+
}), 'utf8');
291+
if (i < iterations - 1) {
292+
validSarifStream.write(',');
293+
}
294+
}
295+
validSarifStream.write(']', 'utf8');
296+
validSarifStream.end();
297+
await finished;
298+
299+
// We need to sleep to wait for MSFT Defender to scan the file
300+
// so that it can be read by our test.
301+
if (os.platform() === 'win32') {
302+
await sleep(10_000);
303+
}
304+
305+
const results = await interpretResultsSarif(
306+
mockServer,
307+
metadata,
308+
{
309+
resultsPath, interpretedResultsPath
310+
},
311+
sourceInfo as SourceInfo
312+
);
313+
// We do not re-interpret if we are reading from a SARIF file.
314+
expect(spy).to.not.have.been.called;
315+
316+
expect(results).to.have.property('t', 'SarifInterpretationData');
317+
expect(results).to.have.nested.property('runs[0].results');
318+
});
319+
320+
it('should throw an error on an invalid large SARIF file', async function() {
321+
// up to 2 minutes per test
322+
this.timeout(2 * 60 * 1000);
323+
324+
// There is a problem on Windows where the file at the prior path isn't able
325+
// to be deleted or written to, so we rename the path for this last test.
326+
const interpretedResultsPath = path.join(tmpDir.name, 'interpreted-invalid.json');
327+
const invalidSarifStream = fs.createWriteStream(interpretedResultsPath, { flags: 'w' });
328+
329+
const finished = new Promise((res, rej) => {
330+
invalidSarifStream.addListener('close', res);
331+
invalidSarifStream.addListener('error', rej);
332+
});
333+
334+
invalidSarifStream.write('[', 'utf8');
335+
const iterations = 1_000_000;
336+
for (let i = 0; i < iterations; i++) {
337+
invalidSarifStream.write(JSON.stringify({
338+
a: '6'
339+
}), 'utf8');
340+
if (i < iterations - 1) {
341+
invalidSarifStream.write(',');
342+
}
343+
}
344+
invalidSarifStream.write(']', 'utf8');
345+
invalidSarifStream.end();
346+
await finished;
347+
348+
// We need to sleep to wait for MSFT Defender to scan the file
349+
// so that it can be read by our test.
350+
if (os.platform() === 'win32') {
351+
await sleep(10_000);
352+
}
353+
354+
await expect(
355+
interpretResultsSarif(
356+
mockServer,
357+
metadata,
358+
{
359+
resultsPath, interpretedResultsPath
360+
},
361+
sourceInfo as SourceInfo)
362+
).to.be.rejectedWith('Parsing output of interpretation failed: Invalid SARIF file: expecting at least one run with result.');
363+
364+
// We do not attempt to re-interpret if we are reading from a SARIF file.
365+
expect(spy).to.not.have.been.called;
366+
});
220367
});
221368

222369
describe('splat and slurp', () => {
@@ -300,6 +447,18 @@ describe('query-results', () => {
300447
});
301448
});
302449

450+
function safeDel(file: string) {
451+
try {
452+
fs.unlinkSync(file);
453+
} catch (e) {
454+
// ignore
455+
}
456+
}
457+
458+
async function sleep(ms: number) {
459+
return new Promise(resolve => setTimeout(resolve, ms));
460+
}
461+
303462
function createMockQueryWithResults(
304463
queryPath: string,
305464
didRunSuccessfully = true,

0 commit comments

Comments
 (0)