Skip to content

Commit 78e3612

Browse files
committed
Use sarif for problems view over doing it manually.
1 parent c5d28c6 commit 78e3612

File tree

7 files changed

+134
-142
lines changed

7 files changed

+134
-142
lines changed

extensions/ql-vscode/src/cli.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import * as util from 'util';
66
import { Logger, ProgressReporter } from "./logging";
77
import { Disposable } from "vscode";
88
import { DistributionProvider } from "./distribution";
9-
import { SortDirection } from "./interface-types";
9+
import { SortDirection, QueryMetadata } from "./interface-types";
1010
import { assertNever } from "./helpers-pure";
1111

1212
/**
@@ -50,16 +50,6 @@ export interface UpgradesInfo {
5050
finalDbscheme: string;
5151
}
5252

53-
/**
54-
* The expected output of `codeql resolve metadata`.
55-
*/
56-
export interface QueryMetadata {
57-
name?: string,
58-
description?: string,
59-
id?: string,
60-
kind?: string
61-
}
62-
6353
// `codeql bqrs interpret` requires both of these to be present or
6454
// both absent.
6555
export interface SourceInfo {

extensions/ql-vscode/src/interface-types.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ export interface DatabaseInfo {
1616
databaseUri: string;
1717
}
1818

19+
/** Arbitrary query metadata */
20+
export interface QueryMetadata {
21+
name?: string,
22+
description?: string,
23+
id?: string,
24+
kind?: string
25+
}
26+
1927
export interface PreviousExecution {
2028
queryName: string;
2129
time: string;
@@ -31,7 +39,6 @@ export interface Interpretation {
3139

3240
export interface ResultsInfo {
3341
resultsPath: string;
34-
interpretedResultsPath: string;
3542
}
3643

3744
export interface SortedResultSetInfo {
@@ -56,7 +63,7 @@ export interface SetStateMsg {
5663
sortedResultsMap: SortedResultsMap;
5764
interpretation: undefined | Interpretation;
5865
database: DatabaseInfo;
59-
kind?: string;
66+
metadata?: QueryMetadata
6067
/**
6168
* Whether to keep displaying the old results while rendering the new results.
6269
*
@@ -86,6 +93,7 @@ interface ViewSourceFileMsg {
8693
interface ToggleDiagnostics {
8794
t: 'toggleDiagnostics';
8895
databaseUri: string;
96+
metadata?: QueryMetadata
8997
resultsPath: string;
9098
visible: boolean;
9199
kind?: string;

extensions/ql-vscode/src/interface.ts

Lines changed: 98 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import * as crypto from 'crypto';
22
import * as path from 'path';
3-
import * as bqrs from 'semmle-bqrs';
4-
import { CustomResultSets, FivePartLocation, LocationStyle, LocationValue, PathProblemQueryResults, ProblemQueryResults, ResolvableLocationValue, tryGetResolvableLocation, WholeFileLocation } from 'semmle-bqrs';
5-
import { FileReader } from 'semmle-io-node';
3+
import * as cli from './cli';
4+
import * as Sarif from 'sarif';
5+
import { parseSarifLocation, parseSarifPlainTextMessage } from './sarif-utils';
6+
import { FivePartLocation, LocationValue, ResolvableLocationValue, WholeFileLocation, tryGetResolvableLocation, LocationStyle } from 'semmle-bqrs';
67
import { DisposableObject } from 'semmle-vscode-utils';
78
import * as vscode from 'vscode';
89
import { Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, languages, Location, Position, Range, Uri, window as Window, workspace } from 'vscode';
@@ -11,7 +12,7 @@ import { DatabaseItem, DatabaseManager } from './databases';
1112
import * as helpers from './helpers';
1213
import { showAndLogErrorMessage } from './helpers';
1314
import { assertNever } from './helpers-pure';
14-
import { FromResultsViewMsg, Interpretation, IntoResultsViewMsg, ResultsInfo, SortedResultSetInfo, SortedResultsMap, INTERPRETED_RESULTS_PER_RUN_LIMIT } from './interface-types';
15+
import { FromResultsViewMsg, Interpretation, IntoResultsViewMsg, ResultsInfo, SortedResultSetInfo, SortedResultsMap, INTERPRETED_RESULTS_PER_RUN_LIMIT, QueryMetadata } from './interface-types';
1516
import { Logger } from './logging';
1617
import * as messages from './messages';
1718
import { EvaluationInfo, interpretResults, QueryInfo, tmpDir } from './queries';
@@ -165,7 +166,7 @@ export class InterfaceManager extends DisposableObject {
165166
if (msg.visible) {
166167
const databaseItem = this.databaseManager.findDatabaseItem(Uri.parse(msg.databaseUri));
167168
if (databaseItem !== undefined) {
168-
await this.showResultsAsDiagnostics(msg.resultsPath, msg.kind, databaseItem);
169+
await this.showResultsAsDiagnostics(msg.resultsPath, msg.metadata, databaseItem);
169170
}
170171
} else {
171172
// TODO: Only clear diagnostics on the same database.
@@ -262,10 +263,31 @@ export class InterfaceManager extends DisposableObject {
262263
sortedResultsMap,
263264
database: info.database,
264265
shouldKeepOldResultsWhileRendering,
265-
kind: info.query.metadata ? info.query.metadata.kind : undefined
266+
metadata: info.query.metadata
266267
});
267268
}
268269

270+
private async getTruncatedResults(metadata : QueryMetadata | undefined ,resultsPathOnDisk: string, sourceInfo : cli.SourceInfo | undefined, sourceLocationPrefix : string ) : Promise<Interpretation> {
271+
const sarif = await interpretResults(this.cliServer, metadata, resultsPathOnDisk, sourceInfo);
272+
// For performance reasons, limit the number of results we try
273+
// to serialize and send to the webview. TODO: possibly also
274+
// limit number of paths per result, number of steps per path,
275+
// or throw an error if we are in aggregate trying to send
276+
// massively too much data, as it can make the extension
277+
// unresponsive.
278+
let numTruncatedResults = 0;
279+
sarif.runs.forEach(run => {
280+
if (run.results !== undefined) {
281+
if (run.results.length > INTERPRETED_RESULTS_PER_RUN_LIMIT) {
282+
numTruncatedResults += run.results.length - INTERPRETED_RESULTS_PER_RUN_LIMIT;
283+
run.results = run.results.slice(0, INTERPRETED_RESULTS_PER_RUN_LIMIT);
284+
}
285+
}
286+
});
287+
return { sarif, sourceLocationPrefix, numTruncatedResults };
288+
;
289+
}
290+
269291
private async interpretResultsInfo(query: QueryInfo, resultsInfo: ResultsInfo): Promise<Interpretation | undefined> {
270292
let interpretation: Interpretation | undefined = undefined;
271293
if (query.hasInterpretedResults()
@@ -277,114 +299,107 @@ export class InterfaceManager extends DisposableObject {
277299
const sourceInfo = sourceArchiveUri === undefined ?
278300
undefined :
279301
{ sourceArchive: sourceArchiveUri.fsPath, sourceLocationPrefix };
280-
const sarif = await interpretResults(this.cliServer, query, resultsInfo, sourceInfo);
281-
// For performance reasons, limit the number of results we try
282-
// to serialize and send to the webview. TODO: possibly also
283-
// limit number of paths per result, number of steps per path,
284-
// or throw an error if we are in aggregate trying to send
285-
// massively too much data, as it can make the extension
286-
// unresponsive.
287-
let numTruncatedResults = 0;
288-
sarif.runs.forEach(run => {
289-
if (run.results !== undefined) {
290-
if (run.results.length > INTERPRETED_RESULTS_PER_RUN_LIMIT) {
291-
numTruncatedResults += run.results.length - INTERPRETED_RESULTS_PER_RUN_LIMIT;
292-
run.results = run.results.slice(0, INTERPRETED_RESULTS_PER_RUN_LIMIT);
293-
}
294-
}
295-
});
296-
interpretation = { sarif, sourceLocationPrefix, numTruncatedResults };
302+
interpretation = await this.getTruncatedResults(query.metadata, resultsInfo.resultsPath, sourceInfo, sourceLocationPrefix);
297303
}
298304
catch (e) {
299305
// If interpretation fails, accept the error and continue
300306
// trying to render uninterpreted results anyway.
301307
this.logger.log(`Exception during results interpretation: ${e.message}. Will show raw results instead.`);
302308
}
303309
}
304-
305310
return interpretation;
306311
}
307312

308-
private async showResultsAsDiagnostics(resultsPath: string, kind: string | undefined,
309-
database: DatabaseItem) {
310313

314+
private async showResultsAsDiagnostics(webviewResultsUri: string, metadata: QueryMetadata | undefined, database: DatabaseItem) {
311315
// URIs from the webview have the vscode-resource scheme, so convert into a filesystem URI first.
312-
const resultsPathOnDisk = webviewUriToFileUri(resultsPath).fsPath;
313-
const fileReader = await FileReader.open(resultsPathOnDisk);
314-
try {
315-
const resultSets = await bqrs.open(fileReader);
316-
try {
317-
switch (kind || 'problem') {
318-
case 'problem': {
319-
const customResults = bqrs.createCustomResultSets<ProblemQueryResults>(resultSets, ProblemQueryResults);
320-
await this.showProblemResultsAsDiagnostics(customResults, database);
321-
}
322-
break;
323-
324-
case 'path-problem': {
325-
const customResults = bqrs.createCustomResultSets<PathProblemQueryResults>(resultSets, PathProblemQueryResults);
326-
await this.showProblemResultsAsDiagnostics(customResults, database);
327-
}
328-
break;
316+
const resultsPathOnDisk = webviewUriToFileUri(webviewResultsUri).fsPath;
317+
const sourceLocationPrefix = await database.getSourceLocationPrefix(this.cliServer);
318+
const sourceArchiveUri = database.sourceArchive;
319+
const sourceInfo = sourceArchiveUri === undefined ?
320+
undefined :
321+
{ sourceArchive: sourceArchiveUri.fsPath, sourceLocationPrefix };
322+
const interpretation = await this.getTruncatedResults(metadata, resultsPathOnDisk, sourceInfo, sourceLocationPrefix);
329323

330-
default:
331-
throw new Error(`Unrecognized query kind '${kind}'.`);
332-
}
333-
}
334-
catch (e) {
335-
const msg = e instanceof Error ? e.message : e.toString();
336-
this.logger.log(`Exception while computing problem results as diagnostics: ${msg}`);
337-
this._diagnosticCollection.clear();
338-
}
324+
try {
325+
await this.showProblemResultsAsDiagnostics(interpretation, database);
339326
}
340-
finally {
341-
fileReader.dispose();
327+
catch (e) {
328+
const msg = e instanceof Error ? e.message : e.toString();
329+
this.logger.log(`Exception while computing problem results as diagnostics: ${msg}`);
330+
this._diagnosticCollection.clear();
342331
}
332+
343333
}
344334

345-
private async showProblemResultsAsDiagnostics(results: CustomResultSets<ProblemQueryResults>,
346-
databaseItem: DatabaseItem): Promise<void> {
335+
private async showProblemResultsAsDiagnostics(interpretation : Interpretation, databaseItem: DatabaseItem): Promise<void> {
336+
const { sarif, sourceLocationPrefix } = interpretation;
337+
338+
339+
if (!sarif.runs || !sarif.runs[0].results) {
340+
this.logger.log("Didn't find a run in the sarif results. Error processing sarif?")
341+
return;
342+
}
347343

348344
const diagnostics: [Uri, ReadonlyArray<Diagnostic>][] = [];
349-
for await (const problemRow of results.problems.readTuples()) {
350-
const codeLocation = resolveLocation(problemRow.element.location, databaseItem);
351-
let message: string;
352-
const references = problemRow.references;
353-
if (references) {
354-
let referenceIndex = 0;
355-
message = problemRow.message.replace(/\$\@/g, sub => {
356-
if (referenceIndex < references.length) {
357-
const replacement = references[referenceIndex].text;
358-
referenceIndex++;
359-
return replacement;
360-
}
361-
else {
362-
return sub;
363-
}
364-
});
345+
346+
for (const result of sarif.runs[0].results) {
347+
const message = result.message.text;
348+
if (message === undefined) {
349+
this.logger.log("Sarif had result without plaintext message")
350+
continue;
351+
}
352+
if (!result.locations) {
353+
this.logger.log("Sarif had result without location")
354+
continue;
355+
}
356+
357+
const sarifLoc = parseSarifLocation(result.locations[0], sourceLocationPrefix);
358+
if (sarifLoc.t == "NoLocation") {
359+
continue;
360+
}
361+
const resultLocation = tryResolveLocation(sarifLoc, databaseItem)
362+
if (!resultLocation) {
363+
this.logger.log("Sarif location was not resolvable " + sarifLoc)
364+
continue;
365365
}
366-
else {
367-
message = problemRow.message;
366+
const parsedMessage = parseSarifPlainTextMessage(message);
367+
const relatedInformation: DiagnosticRelatedInformation[] = [];
368+
const relatedLocationsById: { [k: number]: Sarif.Location } = {};
369+
370+
371+
for (let loc of result.relatedLocations || []) {
372+
relatedLocationsById[loc.id!] = loc;
368373
}
369-
const diagnostic = new Diagnostic(codeLocation.range, message, DiagnosticSeverity.Warning);
370-
if (problemRow.references) {
371-
const relatedInformation: DiagnosticRelatedInformation[] = [];
372-
for (const reference of problemRow.references) {
373-
const referenceLocation = tryResolveLocation(reference.element.location, databaseItem);
374+
let resultMessageChunks: string[] = [];
375+
for (const section of parsedMessage) {
376+
if (typeof section === "string") {
377+
resultMessageChunks.push(section);
378+
} else {
379+
resultMessageChunks.push(section.text);
380+
const sarifChunkLoc = parseSarifLocation(relatedLocationsById[section.dest], sourceLocationPrefix);
381+
if (sarifChunkLoc.t == "NoLocation") {
382+
continue;
383+
}
384+
const referenceLocation = tryResolveLocation(sarifChunkLoc, databaseItem);
385+
386+
374387
if (referenceLocation) {
375388
const related = new DiagnosticRelatedInformation(referenceLocation,
376-
reference.text);
389+
section.text);
377390
relatedInformation.push(related);
378391
}
379392
}
380-
diagnostic.relatedInformation = relatedInformation;
381393
}
394+
const diagnostic = new Diagnostic(resultLocation.range, resultMessageChunks.join(""), DiagnosticSeverity.Warning);
395+
diagnostic.relatedInformation = relatedInformation;
396+
382397
diagnostics.push([
383-
codeLocation.uri,
398+
resultLocation.uri,
384399
[diagnostic]
385400
]);
386-
}
387401

402+
}
388403
this._diagnosticCollection.set(diagnostics);
389404
}
390405

@@ -476,22 +491,6 @@ function resolveWholeFileLocation(loc: WholeFileLocation, databaseItem: Database
476491
return new Location(databaseItem.resolveSourceFile(loc.file), range);
477492
}
478493

479-
/**
480-
* Resolve the specified CodeQL location to a URI into the source archive.
481-
* @param loc CodeQL location to resolve
482-
* @param databaseItem Database in which to resolve the file location.
483-
*/
484-
function resolveLocation(loc: LocationValue | undefined, databaseItem: DatabaseItem): Location {
485-
const resolvedLocation = tryResolveLocation(loc, databaseItem);
486-
if (resolvedLocation) {
487-
return resolvedLocation;
488-
}
489-
else {
490-
// Return a fake position in the source archive directory itself.
491-
return new Location(databaseItem.resolveSourceFile(undefined), new Position(0, 0));
492-
}
493-
}
494-
495494
/**
496495
* Try to resolve the specified CodeQL location to a URI into the source archive. If no exact location
497496
* can be resolved, returns `undefined`.

0 commit comments

Comments
 (0)