Skip to content

Commit 09af48a

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

File tree

7 files changed

+151
-154
lines changed

7 files changed

+151
-154
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: 13 additions & 3 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;
@@ -29,7 +37,7 @@ export interface Interpretation {
2937
sarif: sarif.Log;
3038
}
3139

32-
export interface ResultsInfo {
40+
export interface ResultsPaths {
3341
resultsPath: string;
3442
interpretedResultsPath: string;
3543
}
@@ -53,10 +61,11 @@ export interface ResultsUpdatingMsg {
5361
export interface SetStateMsg {
5462
t: 'setState';
5563
resultsPath: string;
64+
origResultsPaths: ResultsPaths;
5665
sortedResultsMap: SortedResultsMap;
5766
interpretation: undefined | Interpretation;
5867
database: DatabaseInfo;
59-
kind?: string;
68+
metadata?: QueryMetadata
6069
/**
6170
* Whether to keep displaying the old results while rendering the new results.
6271
*
@@ -86,7 +95,8 @@ interface ViewSourceFileMsg {
8695
interface ToggleDiagnostics {
8796
t: 'toggleDiagnostics';
8897
databaseUri: string;
89-
resultsPath: string;
98+
metadata?: QueryMetadata
99+
origResultsPaths: ResultsPaths;
90100
visible: boolean;
91101
kind?: string;
92102
};

extensions/ql-vscode/src/interface.ts

Lines changed: 102 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
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';
8-
import { Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, languages, Location, Position, Range, Uri, window as Window, workspace } from 'vscode';
9+
import { Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, languages, Location, Range, Uri, window as Window, workspace } from 'vscode';
910
import { CodeQLCliServer } from './cli';
1011
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, ResultsPaths, 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.origResultsPaths, msg.metadata, databaseItem);
169170
}
170171
} else {
171172
// TODO: Only clear diagnostics on the same database.
@@ -222,7 +223,7 @@ export class InterfaceManager extends DisposableObject {
222223
return;
223224
}
224225

225-
const interpretation = await this.interpretResultsInfo(info.query, info.query.resultsInfo);
226+
const interpretation = await this.interpretResultsInfo(info.query, info.query.resultsPaths);
226227

227228
const sortedResultsMap: SortedResultsMap = {};
228229
info.query.sortedResultsInfo.forEach((v, k) =>
@@ -258,15 +259,37 @@ export class InterfaceManager extends DisposableObject {
258259
await this.postMessage({
259260
t: 'setState',
260261
interpretation,
261-
resultsPath: this.convertPathToWebviewUri(info.query.resultsInfo.resultsPath),
262+
origResultsPaths: info.query.resultsPaths,
263+
resultsPath: this.convertPathToWebviewUri(info.query.resultsPaths.resultsPath),
262264
sortedResultsMap,
263265
database: info.database,
264266
shouldKeepOldResultsWhileRendering,
265-
kind: info.query.metadata ? info.query.metadata.kind : undefined
267+
metadata: info.query.metadata
266268
});
267269
}
268270

269-
private async interpretResultsInfo(query: QueryInfo, resultsInfo: ResultsInfo): Promise<Interpretation | undefined> {
271+
private async getTruncatedResults(metadata : QueryMetadata | undefined ,resultsInfo: ResultsPaths, sourceInfo : cli.SourceInfo | undefined, sourceLocationPrefix : string ) : Promise<Interpretation> {
272+
const sarif = await interpretResults(this.cliServer, metadata, resultsInfo, sourceInfo);
273+
// For performance reasons, limit the number of results we try
274+
// to serialize and send to the webview. TODO: possibly also
275+
// limit number of paths per result, number of steps per path,
276+
// or throw an error if we are in aggregate trying to send
277+
// massively too much data, as it can make the extension
278+
// unresponsive.
279+
let numTruncatedResults = 0;
280+
sarif.runs.forEach(run => {
281+
if (run.results !== undefined) {
282+
if (run.results.length > INTERPRETED_RESULTS_PER_RUN_LIMIT) {
283+
numTruncatedResults += run.results.length - INTERPRETED_RESULTS_PER_RUN_LIMIT;
284+
run.results = run.results.slice(0, INTERPRETED_RESULTS_PER_RUN_LIMIT);
285+
}
286+
}
287+
});
288+
return { sarif, sourceLocationPrefix, numTruncatedResults };
289+
;
290+
}
291+
292+
private async interpretResultsInfo(query: QueryInfo, resultsInfo: ResultsPaths): Promise<Interpretation | undefined> {
270293
let interpretation: Interpretation | undefined = undefined;
271294
if (query.hasInterpretedResults()
272295
&& query.quickEvalPosition === undefined // never do results interpretation if quickEval
@@ -277,114 +300,105 @@ export class InterfaceManager extends DisposableObject {
277300
const sourceInfo = sourceArchiveUri === undefined ?
278301
undefined :
279302
{ 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 };
303+
interpretation = await this.getTruncatedResults(query.metadata, resultsInfo, sourceInfo, sourceLocationPrefix);
297304
}
298305
catch (e) {
299306
// If interpretation fails, accept the error and continue
300307
// trying to render uninterpreted results anyway.
301308
this.logger.log(`Exception during results interpretation: ${e.message}. Will show raw results instead.`);
302309
}
303310
}
304-
305311
return interpretation;
306312
}
307313

308-
private async showResultsAsDiagnostics(resultsPath: string, kind: string | undefined,
309-
database: DatabaseItem) {
310-
311-
// 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;
323314

324-
case 'path-problem': {
325-
const customResults = bqrs.createCustomResultSets<PathProblemQueryResults>(resultSets, PathProblemQueryResults);
326-
await this.showProblemResultsAsDiagnostics(customResults, database);
327-
}
328-
break;
315+
private async showResultsAsDiagnostics(resultsInfo: ResultsPaths, metadata: QueryMetadata | undefined, database: DatabaseItem) {
316+
const sourceLocationPrefix = await database.getSourceLocationPrefix(this.cliServer);
317+
const sourceArchiveUri = database.sourceArchive;
318+
const sourceInfo = sourceArchiveUri === undefined ?
319+
undefined :
320+
{ sourceArchive: sourceArchiveUri.fsPath, sourceLocationPrefix };
321+
const interpretation = await this.getTruncatedResults(metadata, resultsInfo, sourceInfo, sourceLocationPrefix);
329322

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-
}
323+
try {
324+
await this.showProblemResultsAsDiagnostics(interpretation, database);
339325
}
340-
finally {
341-
fileReader.dispose();
326+
catch (e) {
327+
const msg = e instanceof Error ? e.message : e.toString();
328+
this.logger.log(`Exception while computing problem results as diagnostics: ${msg}`);
329+
this._diagnosticCollection.clear();
342330
}
331+
343332
}
344333

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

348343
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-
});
344+
345+
for (const result of sarif.runs[0].results) {
346+
const message = result.message.text;
347+
if (message === undefined) {
348+
this.logger.log("Sarif had result without plaintext message")
349+
continue;
350+
}
351+
if (!result.locations) {
352+
this.logger.log("Sarif had result without location")
353+
continue;
354+
}
355+
356+
const sarifLoc = parseSarifLocation(result.locations[0], sourceLocationPrefix);
357+
if (sarifLoc.t == "NoLocation") {
358+
continue;
359+
}
360+
const resultLocation = tryResolveLocation(sarifLoc, databaseItem)
361+
if (!resultLocation) {
362+
this.logger.log("Sarif location was not resolvable " + sarifLoc)
363+
continue;
365364
}
366-
else {
367-
message = problemRow.message;
365+
const parsedMessage = parseSarifPlainTextMessage(message);
366+
const relatedInformation: DiagnosticRelatedInformation[] = [];
367+
const relatedLocationsById: { [k: number]: Sarif.Location } = {};
368+
369+
370+
for (let loc of result.relatedLocations || []) {
371+
relatedLocationsById[loc.id!] = loc;
368372
}
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);
373+
let resultMessageChunks: string[] = [];
374+
for (const section of parsedMessage) {
375+
if (typeof section === "string") {
376+
resultMessageChunks.push(section);
377+
} else {
378+
resultMessageChunks.push(section.text);
379+
const sarifChunkLoc = parseSarifLocation(relatedLocationsById[section.dest], sourceLocationPrefix);
380+
if (sarifChunkLoc.t == "NoLocation") {
381+
continue;
382+
}
383+
const referenceLocation = tryResolveLocation(sarifChunkLoc, databaseItem);
384+
385+
374386
if (referenceLocation) {
375387
const related = new DiagnosticRelatedInformation(referenceLocation,
376-
reference.text);
388+
section.text);
377389
relatedInformation.push(related);
378390
}
379391
}
380-
diagnostic.relatedInformation = relatedInformation;
381392
}
393+
const diagnostic = new Diagnostic(resultLocation.range, resultMessageChunks.join(""), DiagnosticSeverity.Warning);
394+
diagnostic.relatedInformation = relatedInformation;
395+
382396
diagnostics.push([
383-
codeLocation.uri,
397+
resultLocation.uri,
384398
[diagnostic]
385399
]);
386-
}
387400

401+
}
388402
this._diagnosticCollection.set(diagnostics);
389403
}
390404

@@ -476,22 +490,6 @@ function resolveWholeFileLocation(loc: WholeFileLocation, databaseItem: Database
476490
return new Location(databaseItem.resolveSourceFile(loc.file), range);
477491
}
478492

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-
495493
/**
496494
* Try to resolve the specified CodeQL location to a URI into the source archive. If no exact location
497495
* can be resolved, returns `undefined`.

0 commit comments

Comments
 (0)