Use sarif for problems view over doing it manually.

This commit is contained in:
alexet
2019-11-25 18:06:15 +00:00
committed by Jason Reed
parent 752c7b2d6b
commit 29f6ec9996
7 changed files with 134 additions and 142 deletions

View File

@@ -6,7 +6,7 @@ import * as util from 'util';
import { Logger, ProgressReporter } from "./logging";
import { Disposable } from "vscode";
import { DistributionProvider } from "./distribution";
import { SortDirection } from "./interface-types";
import { SortDirection, QueryMetadata } from "./interface-types";
import { assertNever } from "./helpers-pure";
/**
@@ -55,16 +55,6 @@ export interface UpgradesInfo {
*/
export type QlpacksInfo = { [name: string]: string[] };
/**
* The expected output of `codeql resolve metadata`.
*/
export interface QueryMetadata {
name?: string,
description?: string,
id?: string,
kind?: string
}
// `codeql bqrs interpret` requires both of these to be present or
// both absent.
export interface SourceInfo {
@@ -159,7 +149,7 @@ export class CodeQLCliServer implements Disposable {
if (!config) {
throw new Error("Failed to find codeql distribution")
}
return spawnServer(config, "CodeQL CLI Server", ["execute", "cli-server"], [], this.logger, _data => {})
return spawnServer(config, "CodeQL CLI Server", ["execute", "cli-server"], [], this.logger, _data => { })
}
private async runCodeQlCliInternal(command: string[], commandArgs: string[], description: string): Promise<string> {

View File

@@ -16,6 +16,14 @@ export interface DatabaseInfo {
databaseUri: string;
}
/** Arbitrary query metadata */
export interface QueryMetadata {
name?: string,
description?: string,
id?: string,
kind?: string
}
export interface PreviousExecution {
queryName: string;
time: string;
@@ -31,7 +39,6 @@ export interface Interpretation {
export interface ResultsInfo {
resultsPath: string;
interpretedResultsPath: string;
}
export interface SortedResultSetInfo {
@@ -56,7 +63,7 @@ export interface SetStateMsg {
sortedResultsMap: SortedResultsMap;
interpretation: undefined | Interpretation;
database: DatabaseInfo;
kind?: string;
metadata?: QueryMetadata
/**
* Whether to keep displaying the old results while rendering the new results.
*
@@ -86,6 +93,7 @@ interface ViewSourceFileMsg {
interface ToggleDiagnostics {
t: 'toggleDiagnostics';
databaseUri: string;
metadata?: QueryMetadata
resultsPath: string;
visible: boolean;
kind?: string;

View File

@@ -1,8 +1,9 @@
import * as crypto from 'crypto';
import * as path from 'path';
import * as bqrs from 'semmle-bqrs';
import { CustomResultSets, FivePartLocation, LocationStyle, LocationValue, PathProblemQueryResults, ProblemQueryResults, ResolvableLocationValue, tryGetResolvableLocation, WholeFileLocation } from 'semmle-bqrs';
import { FileReader } from 'semmle-io-node';
import * as cli from './cli';
import * as Sarif from 'sarif';
import { parseSarifLocation, parseSarifPlainTextMessage } from './sarif-utils';
import { FivePartLocation, LocationValue, ResolvableLocationValue, WholeFileLocation, tryGetResolvableLocation, LocationStyle } from 'semmle-bqrs';
import { DisposableObject } from 'semmle-vscode-utils';
import * as vscode from 'vscode';
import { Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, languages, Location, Position, Range, Uri, window as Window, workspace } from 'vscode';
@@ -11,7 +12,7 @@ import { DatabaseItem, DatabaseManager } from './databases';
import * as helpers from './helpers';
import { showAndLogErrorMessage } from './helpers';
import { assertNever } from './helpers-pure';
import { FromResultsViewMsg, Interpretation, IntoResultsViewMsg, ResultsInfo, SortedResultSetInfo, SortedResultsMap, INTERPRETED_RESULTS_PER_RUN_LIMIT } from './interface-types';
import { FromResultsViewMsg, Interpretation, IntoResultsViewMsg, ResultsInfo, SortedResultSetInfo, SortedResultsMap, INTERPRETED_RESULTS_PER_RUN_LIMIT, QueryMetadata } from './interface-types';
import { Logger } from './logging';
import * as messages from './messages';
import { EvaluationInfo, interpretResults, QueryInfo, tmpDir } from './queries';
@@ -165,7 +166,7 @@ export class InterfaceManager extends DisposableObject {
if (msg.visible) {
const databaseItem = this.databaseManager.findDatabaseItem(Uri.parse(msg.databaseUri));
if (databaseItem !== undefined) {
await this.showResultsAsDiagnostics(msg.resultsPath, msg.kind, databaseItem);
await this.showResultsAsDiagnostics(msg.resultsPath, msg.metadata, databaseItem);
}
} else {
// TODO: Only clear diagnostics on the same database.
@@ -262,10 +263,31 @@ export class InterfaceManager extends DisposableObject {
sortedResultsMap,
database: info.database,
shouldKeepOldResultsWhileRendering,
kind: info.query.metadata ? info.query.metadata.kind : undefined
metadata: info.query.metadata
});
}
private async getTruncatedResults(metadata : QueryMetadata | undefined ,resultsPathOnDisk: string, sourceInfo : cli.SourceInfo | undefined, sourceLocationPrefix : string ) : Promise<Interpretation> {
const sarif = await interpretResults(this.cliServer, metadata, resultsPathOnDisk, sourceInfo);
// For performance reasons, limit the number of results we try
// to serialize and send to the webview. TODO: possibly also
// limit number of paths per result, number of steps per path,
// or throw an error if we are in aggregate trying to send
// massively too much data, as it can make the extension
// unresponsive.
let numTruncatedResults = 0;
sarif.runs.forEach(run => {
if (run.results !== undefined) {
if (run.results.length > INTERPRETED_RESULTS_PER_RUN_LIMIT) {
numTruncatedResults += run.results.length - INTERPRETED_RESULTS_PER_RUN_LIMIT;
run.results = run.results.slice(0, INTERPRETED_RESULTS_PER_RUN_LIMIT);
}
}
});
return { sarif, sourceLocationPrefix, numTruncatedResults };
;
}
private async interpretResultsInfo(query: QueryInfo, resultsInfo: ResultsInfo): Promise<Interpretation | undefined> {
let interpretation: Interpretation | undefined = undefined;
if (query.hasInterpretedResults()
@@ -277,23 +299,7 @@ export class InterfaceManager extends DisposableObject {
const sourceInfo = sourceArchiveUri === undefined ?
undefined :
{ sourceArchive: sourceArchiveUri.fsPath, sourceLocationPrefix };
const sarif = await interpretResults(this.cliServer, query, resultsInfo, sourceInfo);
// For performance reasons, limit the number of results we try
// to serialize and send to the webview. TODO: possibly also
// limit number of paths per result, number of steps per path,
// or throw an error if we are in aggregate trying to send
// massively too much data, as it can make the extension
// unresponsive.
let numTruncatedResults = 0;
sarif.runs.forEach(run => {
if (run.results !== undefined) {
if (run.results.length > INTERPRETED_RESULTS_PER_RUN_LIMIT) {
numTruncatedResults += run.results.length - INTERPRETED_RESULTS_PER_RUN_LIMIT;
run.results = run.results.slice(0, INTERPRETED_RESULTS_PER_RUN_LIMIT);
}
}
});
interpretation = { sarif, sourceLocationPrefix, numTruncatedResults };
interpretation = await this.getTruncatedResults(query.metadata, resultsInfo.resultsPath, sourceInfo, sourceLocationPrefix);
}
catch (e) {
// If interpretation fails, accept the error and continue
@@ -301,90 +307,99 @@ export class InterfaceManager extends DisposableObject {
this.logger.log(`Exception during results interpretation: ${e.message}. Will show raw results instead.`);
}
}
return interpretation;
}
private async showResultsAsDiagnostics(resultsPath: string, kind: string | undefined,
database: DatabaseItem) {
private async showResultsAsDiagnostics(webviewResultsUri: string, metadata: QueryMetadata | undefined, database: DatabaseItem) {
// URIs from the webview have the vscode-resource scheme, so convert into a filesystem URI first.
const resultsPathOnDisk = webviewUriToFileUri(resultsPath).fsPath;
const fileReader = await FileReader.open(resultsPathOnDisk);
const resultsPathOnDisk = webviewUriToFileUri(webviewResultsUri).fsPath;
const sourceLocationPrefix = await database.getSourceLocationPrefix(this.cliServer);
const sourceArchiveUri = database.sourceArchive;
const sourceInfo = sourceArchiveUri === undefined ?
undefined :
{ sourceArchive: sourceArchiveUri.fsPath, sourceLocationPrefix };
const interpretation = await this.getTruncatedResults(metadata, resultsPathOnDisk, sourceInfo, sourceLocationPrefix);
try {
const resultSets = await bqrs.open(fileReader);
try {
switch (kind || 'problem') {
case 'problem': {
const customResults = bqrs.createCustomResultSets<ProblemQueryResults>(resultSets, ProblemQueryResults);
await this.showProblemResultsAsDiagnostics(customResults, database);
}
break;
case 'path-problem': {
const customResults = bqrs.createCustomResultSets<PathProblemQueryResults>(resultSets, PathProblemQueryResults);
await this.showProblemResultsAsDiagnostics(customResults, database);
}
break;
default:
throw new Error(`Unrecognized query kind '${kind}'.`);
}
}
catch (e) {
const msg = e instanceof Error ? e.message : e.toString();
this.logger.log(`Exception while computing problem results as diagnostics: ${msg}`);
this._diagnosticCollection.clear();
}
await this.showProblemResultsAsDiagnostics(interpretation, database);
}
finally {
fileReader.dispose();
catch (e) {
const msg = e instanceof Error ? e.message : e.toString();
this.logger.log(`Exception while computing problem results as diagnostics: ${msg}`);
this._diagnosticCollection.clear();
}
}
private async showProblemResultsAsDiagnostics(results: CustomResultSets<ProblemQueryResults>,
databaseItem: DatabaseItem): Promise<void> {
private async showProblemResultsAsDiagnostics(interpretation : Interpretation, databaseItem: DatabaseItem): Promise<void> {
const { sarif, sourceLocationPrefix } = interpretation;
if (!sarif.runs || !sarif.runs[0].results) {
this.logger.log("Didn't find a run in the sarif results. Error processing sarif?")
return;
}
const diagnostics: [Uri, ReadonlyArray<Diagnostic>][] = [];
for await (const problemRow of results.problems.readTuples()) {
const codeLocation = resolveLocation(problemRow.element.location, databaseItem);
let message: string;
const references = problemRow.references;
if (references) {
let referenceIndex = 0;
message = problemRow.message.replace(/\$\@/g, sub => {
if (referenceIndex < references.length) {
const replacement = references[referenceIndex].text;
referenceIndex++;
return replacement;
}
else {
return sub;
}
});
for (const result of sarif.runs[0].results) {
const message = result.message.text;
if (message === undefined) {
this.logger.log("Sarif had result without plaintext message")
continue;
}
else {
message = problemRow.message;
if (!result.locations) {
this.logger.log("Sarif had result without location")
continue;
}
const diagnostic = new Diagnostic(codeLocation.range, message, DiagnosticSeverity.Warning);
if (problemRow.references) {
const relatedInformation: DiagnosticRelatedInformation[] = [];
for (const reference of problemRow.references) {
const referenceLocation = tryResolveLocation(reference.element.location, databaseItem);
const sarifLoc = parseSarifLocation(result.locations[0], sourceLocationPrefix);
if (sarifLoc.t == "NoLocation") {
continue;
}
const resultLocation = tryResolveLocation(sarifLoc, databaseItem)
if (!resultLocation) {
this.logger.log("Sarif location was not resolvable " + sarifLoc)
continue;
}
const parsedMessage = parseSarifPlainTextMessage(message);
const relatedInformation: DiagnosticRelatedInformation[] = [];
const relatedLocationsById: { [k: number]: Sarif.Location } = {};
for (let loc of result.relatedLocations || []) {
relatedLocationsById[loc.id!] = loc;
}
let resultMessageChunks: string[] = [];
for (const section of parsedMessage) {
if (typeof section === "string") {
resultMessageChunks.push(section);
} else {
resultMessageChunks.push(section.text);
const sarifChunkLoc = parseSarifLocation(relatedLocationsById[section.dest], sourceLocationPrefix);
if (sarifChunkLoc.t == "NoLocation") {
continue;
}
const referenceLocation = tryResolveLocation(sarifChunkLoc, databaseItem);
if (referenceLocation) {
const related = new DiagnosticRelatedInformation(referenceLocation,
reference.text);
section.text);
relatedInformation.push(related);
}
}
diagnostic.relatedInformation = relatedInformation;
}
const diagnostic = new Diagnostic(resultLocation.range, resultMessageChunks.join(""), DiagnosticSeverity.Warning);
diagnostic.relatedInformation = relatedInformation;
diagnostics.push([
codeLocation.uri,
resultLocation.uri,
[diagnostic]
]);
}
}
this._diagnosticCollection.set(diagnostics);
}
@@ -479,22 +494,6 @@ function resolveWholeFileLocation(loc: WholeFileLocation, databaseItem: Database
return new Location(databaseItem.resolveSourceFile(loc.file), range);
}
/**
* Resolve the specified CodeQL location to a URI into the source archive.
* @param loc CodeQL location to resolve
* @param databaseItem Database in which to resolve the file location.
*/
function resolveLocation(loc: LocationValue | undefined, databaseItem: DatabaseItem): Location {
const resolvedLocation = tryResolveLocation(loc, databaseItem);
if (resolvedLocation) {
return resolvedLocation;
}
else {
// Return a fake position in the source archive directory itself.
return new Location(databaseItem.resolveSourceFile(undefined), new Position(0, 0));
}
}
/**
* Try to resolve the specified CodeQL location to a URI into the source archive. If no exact location
* can be resolved, returns `undefined`.

View File

@@ -7,7 +7,7 @@ import * as vscode from 'vscode';
import * as cli from './cli';
import { DatabaseItem, getUpgradesDirectories } from './databases';
import * as helpers from './helpers';
import { DatabaseInfo, SortState, ResultsInfo, SortedResultSetInfo } from './interface-types';
import { DatabaseInfo, SortState, ResultsInfo, SortedResultSetInfo, QueryMetadata } from './interface-types';
import { logger } from './logging';
import * as messages from './messages';
import * as qsClient from './queryserver-client';
@@ -64,13 +64,12 @@ export class QueryInfo {
public dbItem: DatabaseItem,
public queryDbscheme: string, // the dbscheme file the query expects, based on library path resolution
public quickEvalPosition?: messages.Position,
public metadata?: cli.QueryMetadata,
public metadata?: QueryMetadata,
) {
this.queryId = QueryInfo.nextQueryId++;
this.compiledQueryPath = path.join(tmpDir.name, `compiledQuery${this.queryId}.qlo`);
this.resultsInfo = {
resultsPath: path.join(tmpDir.name, `results${this.queryId}.bqrs`),
interpretedResultsPath: path.join(tmpDir.name, `interpretedResults${this.queryId}.sarif`)
};
this.sortedResultsInfo = new Map();
if (dbItem.contents === undefined) {
@@ -186,11 +185,12 @@ export class QueryInfo {
/**
* Call cli command to interpret results.
*/
export async function interpretResults(server: cli.CodeQLCliServer, queryInfo: QueryInfo, resultsInfo: ResultsInfo, sourceInfo?: cli.SourceInfo): Promise<sarif.Log> {
if (await fs.pathExists(resultsInfo.interpretedResultsPath)) {
return JSON.parse(await fs.readFile(resultsInfo.interpretedResultsPath, 'utf8'));
export async function interpretResults(server: cli.CodeQLCliServer, metadata: QueryMetadata | undefined, resultsPath: string, sourceInfo?: cli.SourceInfo): Promise<sarif.Log> {
const interpretedResultsPath = resultsPath + ".interpreted.sarif"
if (await fs.pathExists(interpretedResultsPath)) {
return JSON.parse(await fs.readFile(interpretedResultsPath, 'utf8'));
}
const { metadata } = queryInfo;
if (metadata == undefined) {
throw new Error('Can\'t interpret results without query metadata');
}
@@ -200,14 +200,10 @@ export async function interpretResults(server: cli.CodeQLCliServer, queryInfo: Q
}
if (id == undefined) {
// Interpretation per se doesn't really require an id, but the
// SARIF format does, so in the absence of one, we invent one
// based on the query path.
//
// Just to be careful, sanitize to remove '/' since SARIF (section
// 3.27.5 "ruleId property") says that it has special meaning.
id = queryInfo.program.queryPath.replace(/\//g, '-');
// SARIF format does, so in the absence of one, we use a dummy id.
id = "dummy-id";
}
return await server.interpretBqrs({ kind, id }, resultsInfo.resultsPath, resultsInfo.interpretedResultsPath, sourceInfo);
return await server.interpretBqrs( { kind, id }, resultsPath, interpretedResultsPath, sourceInfo);
}
export interface EvaluationInfo {
@@ -638,7 +634,7 @@ export async function compileAndRunQueryAgainstDatabase(
};
// Read the query metadata if possible, to use in the UI.
let metadata: cli.QueryMetadata | undefined;
let metadata: QueryMetadata | undefined;
try {
metadata = await cliServer.resolveMetadata(qlProgram.queryPath);
} catch (e) {

View File

@@ -1,11 +1,12 @@
import * as React from 'react';
import { LocationValue, ResolvableLocationValue, tryGetResolvableLocation } from 'semmle-bqrs';
import { SortState } from '../interface-types';
import { SortState, QueryMetadata } from '../interface-types';
import { ResultSet, vscode } from './results';
export interface ResultTableProps {
resultSet: ResultSet;
databaseUri: string;
metadata?: QueryMetadata
resultsPath: string | undefined;
sortState?: SortState;
}

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import { DatabaseInfo, Interpretation, SortState } from '../interface-types';
import { DatabaseInfo, Interpretation, SortState, QueryMetadata } from '../interface-types';
import { PathTable } from './alert-table';
import { RawTable } from './raw-results-table';
import { ResultTableProps, tableSelectionHeaderClassName, toggleDiagnosticsClassName } from './result-table-utils';
@@ -12,8 +12,8 @@ export interface ResultTablesProps {
rawResultSets: readonly ResultSet[];
interpretation: Interpretation | undefined;
database: DatabaseInfo;
metadata? : QueryMetadata
resultsPath: string | undefined;
kind: string | undefined;
sortStates: Map<string, SortState>;
isLoadingNewResults: boolean;
}
@@ -95,7 +95,7 @@ export class ResultTables
render(): React.ReactNode {
const { selectedTable } = this.state;
const resultSets = this.getResultSets();
const { database, resultsPath, kind } = this.props;
const { database, resultsPath, metadata } = this.props;
// Only show the Problems view display checkbox for the alerts table.
const diagnosticsCheckBox = selectedTable === ALERTS_TABLE_NAME ?
@@ -107,7 +107,7 @@ export class ResultTables
resultsPath: resultsPath,
databaseUri: database.databaseUri,
visible: e.target.checked,
kind: kind
metadata: metadata
});
}
}} />
@@ -157,11 +157,9 @@ class ResultTable extends React.Component<ResultTableProps, {}> {
const { resultSet } = this.props;
switch (resultSet.t) {
case 'RawResultSet': return <RawTable
resultSet={resultSet} databaseUri={this.props.databaseUri}
resultsPath={this.props.resultsPath} sortState={this.props.sortState} />;
{...this.props} resultSet={resultSet} />;
case 'SarifResultSet': return <PathTable
resultSet={resultSet} databaseUri={this.props.databaseUri}
resultsPath={this.props.resultsPath} />;
{...this.props} resultSet={resultSet} />;
}
}
}

View File

@@ -3,7 +3,7 @@ import * as Rdom from 'react-dom';
import * as bqrs from 'semmle-bqrs';
import { ElementBase, LocationValue, PrimitiveColumnValue, PrimitiveTypeKind, ResultSetSchema, tryGetResolvableLocation } from 'semmle-bqrs';
import { assertNever } from '../helpers-pure';
import { DatabaseInfo, FromResultsViewMsg, Interpretation, IntoResultsViewMsg, SortedResultSetInfo, SortState, NavigatePathMsg } from '../interface-types';
import { DatabaseInfo, FromResultsViewMsg, Interpretation, IntoResultsViewMsg, SortedResultSetInfo, SortState, NavigatePathMsg, QueryMetadata } from '../interface-types';
import { ResultTables } from './result-tables';
import { EventHandlers as EventHandlerList } from './event-handler-list';
@@ -127,7 +127,6 @@ async function parseResultSets(response: Response): Promise<readonly ResultSet[]
interface ResultsInfo {
resultsPath: string;
kind: string | undefined;
database: DatabaseInfo;
interpretation: Interpretation | undefined;
sortedResultsMap: Map<string, SortedResultSetInfo>;
@@ -135,6 +134,7 @@ interface ResultsInfo {
* See {@link SetStateMsg.shouldKeepOldResultsWhileRendering}.
*/
shouldKeepOldResultsWhileRendering: boolean;
metadata?: QueryMetadata
}
interface Results {
@@ -186,11 +186,11 @@ class App extends React.Component<{}, ResultsViewState> {
case 'setState':
this.updateStateWithNewResultsInfo({
resultsPath: msg.resultsPath,
kind: msg.kind,
sortedResultsMap: new Map(Object.entries(msg.sortedResultsMap)),
database: msg.database,
interpretation: msg.interpretation,
shouldKeepOldResultsWhileRendering: msg.shouldKeepOldResultsWhileRendering
shouldKeepOldResultsWhileRendering: msg.shouldKeepOldResultsWhileRendering,
metadata: msg.metadata
});
this.loadResults();
@@ -309,7 +309,7 @@ class App extends React.Component<{}, ResultsViewState> {
interpretation={displayedResults.resultsInfo ? displayedResults.resultsInfo.interpretation : undefined}
database={displayedResults.results.database}
resultsPath={displayedResults.resultsInfo ? displayedResults.resultsInfo.resultsPath : undefined}
kind={displayedResults.resultsInfo ? displayedResults.resultsInfo.kind : undefined}
metadata={displayedResults.resultsInfo ? displayedResults.resultsInfo.metadata : undefined}
sortStates={displayedResults.results.sortStates}
isLoadingNewResults={this.state.isExpectingResultsUpdate || this.state.nextResultsInfo !== null} />;
}