Skip to content

Commit 50fee61

Browse files
committed
Use sarif for problems view over doing it manually.
1 parent 42417bc commit 50fee61

File tree

7 files changed

+107
-110
lines changed

7 files changed

+107
-110
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: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ export interface DatabaseInfo {
1616
databaseUri: string;
1717
}
1818

19+
/** Arbitrary query metadata */
20+
export type QueryMetadata = {[key: string]: string};
21+
1922
export interface PreviousExecution {
2023
queryName: string;
2124
time: string;
@@ -31,7 +34,6 @@ export interface Interpretation {
3134

3235
export interface ResultsInfo {
3336
resultsPath: string;
34-
interpretedResultsPath: string;
3537
}
3638

3739
export interface SortedResultSetInfo {
@@ -56,7 +58,7 @@ export interface SetStateMsg {
5658
sortedResultsMap: SortedResultsMap;
5759
interpretation: undefined | Interpretation;
5860
database: DatabaseInfo;
59-
kind?: string;
61+
metadata?: QueryMetadata
6062
/**
6163
* Whether to keep displaying the old results while rendering the new results.
6264
*
@@ -78,6 +80,7 @@ interface ViewSourceFileMsg {
7880
interface ToggleDiagnostics {
7981
t: 'toggleDiagnostics';
8082
databaseUri: string;
83+
metadata?: QueryMetadata
8184
resultsPath: string;
8285
visible: boolean;
8386
kind?: string;

extensions/ql-vscode/src/interface.ts

Lines changed: 76 additions & 67 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';
@@ -159,7 +160,7 @@ export class InterfaceManager extends DisposableObject {
159160
if (msg.visible) {
160161
const databaseItem = this.databaseManager.findDatabaseItem(Uri.parse(msg.databaseUri));
161162
if (databaseItem !== undefined) {
162-
await this.showResultsAsDiagnostics(msg.resultsPath, msg.kind, databaseItem);
163+
await this.showResultsAsDiagnostics(msg.resultsPath, msg.metadata, databaseItem);
163164
}
164165
} else {
165166
// TODO: Only clear diagnostics on the same database.
@@ -256,7 +257,7 @@ export class InterfaceManager extends DisposableObject {
256257
sortedResultsMap,
257258
database: info.database,
258259
shouldKeepOldResultsWhileRendering,
259-
kind: info.query.metadata ? info.query.metadata.kind : undefined
260+
metadata: info.query.metadata
260261
});
261262
}
262263

@@ -271,7 +272,7 @@ export class InterfaceManager extends DisposableObject {
271272
const sourceInfo = sourceArchiveUri === undefined ?
272273
undefined :
273274
{ sourceArchive: sourceArchiveUri.fsPath, sourceLocationPrefix };
274-
const sarif = await interpretResults(this.cliServer, query, resultsInfo, sourceInfo);
275+
const sarif = await interpretResults(this.cliServer, query.metadata, query.resultsInfo.resultsPath, sourceInfo);
275276
// For performance reasons, limit the number of results we try
276277
// to serialize and send to the webview. TODO: possibly also
277278
// limit number of paths per result, number of steps per path,
@@ -295,90 +296,98 @@ export class InterfaceManager extends DisposableObject {
295296
this.logger.log(`Exception during results interpretation: ${e.message}. Will show raw results instead.`);
296297
}
297298
}
298-
299299
return interpretation;
300300
}
301301

302-
private async showResultsAsDiagnostics(resultsPath: string, kind: string | undefined,
303-
database: DatabaseItem) {
304-
302+
private async showResultsAsDiagnostics(resultsPath: string, metadata: QueryMetadata | undefined, database: DatabaseItem) {
305303
// URIs from the webview have the vscode-resource scheme, so convert into a filesystem URI first.
306304
const resultsPathOnDisk = webviewUriToFileUri(resultsPath).fsPath;
307-
const fileReader = await FileReader.open(resultsPathOnDisk);
308-
try {
309-
const resultSets = await bqrs.open(fileReader);
310-
try {
311-
switch (kind || 'problem') {
312-
case 'problem': {
313-
const customResults = bqrs.createCustomResultSets<ProblemQueryResults>(resultSets, ProblemQueryResults);
314-
await this.showProblemResultsAsDiagnostics(customResults, database);
315-
}
316-
break;
305+
const sourceLocationPrefix = await database.getSourceLocationPrefix(this.cliServer);
306+
const sourceArchiveUri = database.sourceArchive;
307+
const sourceInfo = sourceArchiveUri === undefined ?
308+
undefined :
309+
{ sourceArchive: sourceArchiveUri.fsPath, sourceLocationPrefix };
310+
const sarif = await interpretResults(this.cliServer, metadata, resultsPathOnDisk, sourceInfo);
317311

318-
case 'path-problem': {
319-
const customResults = bqrs.createCustomResultSets<PathProblemQueryResults>(resultSets, PathProblemQueryResults);
320-
await this.showProblemResultsAsDiagnostics(customResults, database);
321-
}
322-
break;
323-
324-
default:
325-
throw new Error(`Unrecognized query kind '${kind}'.`);
326-
}
327-
}
328-
catch (e) {
329-
const msg = e instanceof Error ? e.message : e.toString();
330-
this.logger.log(`Exception while computing problem results as diagnostics: ${msg}`);
331-
this._diagnosticCollection.clear();
332-
}
312+
try {
313+
await this.showProblemResultsAsDiagnostics(sarif, database);
333314
}
334-
finally {
335-
fileReader.dispose();
315+
catch (e) {
316+
const msg = e instanceof Error ? e.message : e.toString();
317+
this.logger.log(`Exception while computing problem results as diagnostics: ${msg}`);
318+
this._diagnosticCollection.clear();
336319
}
320+
337321
}
338322

339-
private async showProblemResultsAsDiagnostics(results: CustomResultSets<ProblemQueryResults>,
340-
databaseItem: DatabaseItem): Promise<void> {
323+
private async showProblemResultsAsDiagnostics(results: Sarif.Log, databaseItem: DatabaseItem): Promise<void> {
324+
const sourceLocationPrefix = await databaseItem.getSourceLocationPrefix(this.cliServer);
325+
326+
327+
if (!results.runs || !results.runs[0].results) {
328+
this.logger.log("Didn't find a run in the sarif results. Error processing sarif?")
329+
return;
330+
}
341331

342332
const diagnostics: [Uri, ReadonlyArray<Diagnostic>][] = [];
343-
for await (const problemRow of results.problems.readTuples()) {
344-
const codeLocation = resolveLocation(problemRow.element.location, databaseItem);
345-
let message: string;
346-
const references = problemRow.references;
347-
if (references) {
348-
let referenceIndex = 0;
349-
message = problemRow.message.replace(/\$\@/g, sub => {
350-
if (referenceIndex < references.length) {
351-
const replacement = references[referenceIndex].text;
352-
referenceIndex++;
353-
return replacement;
354-
}
355-
else {
356-
return sub;
357-
}
358-
});
333+
334+
for (const result of results.runs[0].results) {
335+
const message = result.message.text;
336+
if (message === undefined) {
337+
this.logger.log("Sarif had result without plaintext message")
338+
continue;
359339
}
360-
else {
361-
message = problemRow.message;
340+
if (!result.locations) {
341+
this.logger.log("Sarif had result without location")
342+
continue;
362343
}
363-
const diagnostic = new Diagnostic(codeLocation.range, message, DiagnosticSeverity.Warning);
364-
if (problemRow.references) {
365-
const relatedInformation: DiagnosticRelatedInformation[] = [];
366-
for (const reference of problemRow.references) {
367-
const referenceLocation = tryResolveLocation(reference.element.location, databaseItem);
344+
345+
const sarifLoc = parseSarifLocation(result.locations[0], sourceLocationPrefix);
346+
if (sarifLoc.t == "NoLocation") {
347+
continue;
348+
}
349+
const resultLocation = tryResolveLocation(sarifLoc, databaseItem)
350+
if (!resultLocation) {
351+
this.logger.log("Sarif location was not resolvable " + sarifLoc)
352+
continue;
353+
}
354+
const parsedMessage = parseSarifPlainTextMessage(message);
355+
const relatedInformation: DiagnosticRelatedInformation[] = [];
356+
const relatedLocationsById: { [k: number]: Sarif.Location } = {};
357+
358+
359+
for (let loc of result.relatedLocations || []) {
360+
relatedLocationsById[loc.id!] = loc;
361+
}
362+
let resultMessageChunks: string[] = [];
363+
for (const section of parsedMessage) {
364+
if (typeof section === "string") {
365+
resultMessageChunks.push(section);
366+
} else {
367+
resultMessageChunks.push(section.text);
368+
const sarifChunkLoc = parseSarifLocation(relatedLocationsById[section.dest], sourceLocationPrefix);
369+
if (sarifChunkLoc.t == "NoLocation") {
370+
continue;
371+
}
372+
const referenceLocation = tryResolveLocation(sarifChunkLoc, databaseItem);
373+
374+
368375
if (referenceLocation) {
369376
const related = new DiagnosticRelatedInformation(referenceLocation,
370-
reference.text);
377+
section.text);
371378
relatedInformation.push(related);
372379
}
373380
}
374-
diagnostic.relatedInformation = relatedInformation;
375381
}
382+
const diagnostic = new Diagnostic(resultLocation.range, resultMessageChunks.join(""), DiagnosticSeverity.Warning);
383+
diagnostic.relatedInformation = relatedInformation;
384+
376385
diagnostics.push([
377-
codeLocation.uri,
386+
resultLocation.uri,
378387
[diagnostic]
379388
]);
380-
}
381389

390+
}
382391
this._diagnosticCollection.set(diagnostics);
383392
}
384393

extensions/ql-vscode/src/queries.ts

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import * as vscode from 'vscode';
77
import * as cli from './cli';
88
import { DatabaseItem, getUpgradesDirectories } from './databases';
99
import * as helpers from './helpers';
10-
import { DatabaseInfo, SortState, ResultsInfo, SortedResultSetInfo } from './interface-types';
10+
import { DatabaseInfo, SortState, ResultsInfo, SortedResultSetInfo, QueryMetadata } from './interface-types';
1111
import { logger } from './logging';
1212
import * as messages from './messages';
1313
import * as qsClient from './queryserver-client';
@@ -55,13 +55,12 @@ export class QueryInfo {
5555
public dbItem: DatabaseItem,
5656
public queryDbscheme: string, // the dbscheme file the query expects, based on library path resolution
5757
public quickEvalPosition?: messages.Position,
58-
public metadata?: cli.QueryMetadata,
58+
public metadata?: QueryMetadata,
5959
) {
6060
this.queryId = QueryInfo.nextQueryId++;
6161
this.compiledQueryPath = path.join(tmpDir.name, `compiledQuery${this.queryId}.qlo`);
6262
this.resultsInfo = {
6363
resultsPath: path.join(tmpDir.name, `results${this.queryId}.bqrs`),
64-
interpretedResultsPath: path.join(tmpDir.name, `interpretedResults${this.queryId}.sarif`)
6564
};
6665
this.sortedResultsInfo = new Map();
6766
if (dbItem.contents === undefined) {
@@ -173,11 +172,12 @@ export class QueryInfo {
173172
/**
174173
* Call cli command to interpret results.
175174
*/
176-
export async function interpretResults(server: cli.CodeQLCliServer, queryInfo: QueryInfo, resultsInfo: ResultsInfo, sourceInfo?: cli.SourceInfo): Promise<sarif.Log> {
177-
if (await fs.pathExists(resultsInfo.interpretedResultsPath)) {
178-
return JSON.parse(await fs.readFile(resultsInfo.interpretedResultsPath, 'utf8'));
175+
export async function interpretResults(server: cli.CodeQLCliServer, metadata: QueryMetadata | undefined, resultsPath: string, sourceInfo?: cli.SourceInfo): Promise<sarif.Log> {
176+
const interpretedResultsPath = resultsPath + ".interpreted.sarif"
177+
178+
if (await fs.pathExists(interpretedResultsPath)) {
179+
return JSON.parse(await fs.readFile(interpretedResultsPath, 'utf8'));
179180
}
180-
const { metadata } = queryInfo;
181181
if (metadata == undefined) {
182182
throw new Error('Can\'t interpret results without query metadata');
183183
}
@@ -187,14 +187,10 @@ export async function interpretResults(server: cli.CodeQLCliServer, queryInfo: Q
187187
}
188188
if (id == undefined) {
189189
// Interpretation per se doesn't really require an id, but the
190-
// SARIF format does, so in the absence of one, we invent one
191-
// based on the query path.
192-
//
193-
// Just to be careful, sanitize to remove '/' since SARIF (section
194-
// 3.27.5 "ruleId property") says that it has special meaning.
195-
id = queryInfo.program.queryPath.replace(/\//g, '-');
196-
}
197-
return await server.interpretBqrs({ kind, id }, resultsInfo.resultsPath, resultsInfo.interpretedResultsPath, sourceInfo);
190+
// SARIF format does, so in the absence of one, we use a dummy id.
191+
id = "dummy-id";
192+
}
193+
return await server.interpretBqrs( { kind, id }, resultsPath, interpretedResultsPath, sourceInfo);
198194
}
199195

200196
export interface EvaluationInfo {
@@ -599,7 +595,7 @@ export async function compileAndRunQueryAgainstDatabase(
599595
};
600596

601597
// Read the query metadata if possible, to use in the UI.
602-
let metadata: cli.QueryMetadata | undefined;
598+
let metadata: QueryMetadata | undefined;
603599
try {
604600
metadata = await cliServer.resolveMetadata(qlProgram.queryPath);
605601
} catch (e) {

extensions/ql-vscode/src/view/result-table-utils.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import * as React from 'react';
22
import { LocationValue, ResolvableLocationValue, tryGetResolvableLocation } from 'semmle-bqrs';
3-
import { SortState } from '../interface-types';
3+
import { SortState, QueryMetadata } from '../interface-types';
44
import { ResultSet, vscode } from './results';
55

66
export interface ResultTableProps {
77
resultSet: ResultSet;
88
databaseUri: string;
9+
metadata?: QueryMetadata
910
resultsPath: string | undefined;
1011
sortState?: SortState;
1112
}

0 commit comments

Comments
 (0)