Merge pull request #222 from jcreedcmu/jcreed/pr/184

Create diagnostics messages using sarif.
This commit is contained in:
jcreedcmu
2020-02-06 12:14:00 -05:00
committed by GitHub
17 changed files with 735 additions and 660 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

@@ -1,12 +1,13 @@
import * as path from 'path';
import { DisposableObject } from "semmle-vscode-utils";
import { commands, Event, EventEmitter, ExtensionContext, ProviderResult, TreeDataProvider, TreeItem, Uri, window } from "vscode";
import { DisposableObject } from 'semmle-vscode-utils';
import { commands, Event, EventEmitter, ExtensionContext, ProviderResult, TreeDataProvider, TreeItem, Uri, window } from 'vscode';
import * as cli from './cli';
import { DatabaseItem, DatabaseManager, getUpgradesDirectories } from "./databases";
import { logger } from "./logging";
import { clearCacheInDatabase, upgradeDatabase, UserCancellationException } from "./queries";
import { DatabaseItem, DatabaseManager, getUpgradesDirectories } from './databases';
import { getOnDiskWorkspaceFolders } from './helpers';
import { logger } from './logging';
import { clearCacheInDatabase, UserCancellationException } from './run-queries';
import * as qsClient from './queryserver-client';
import { getOnDiskWorkspaceFolders } from "./helpers";
import { upgradeDatabase } from './upgrades';
type ThemableIconPath = { light: string, dark: string } | string;

View File

@@ -12,7 +12,8 @@ import * as helpers from './helpers';
import { spawnIdeServer } from './ide-server';
import { InterfaceManager, WebviewReveal } from './interface';
import { ideServerLogger, logger, queryServerLogger } from './logging';
import { compileAndRunQueryAgainstDatabase, EvaluationInfo, tmpDirDisposal, UserCancellationException } from './queries';
import { compileAndRunQueryAgainstDatabase, tmpDirDisposal, UserCancellationException } from './run-queries';
import { CompletedQuery } from './query-results';
import { QueryHistoryManager } from './query-history';
import * as qsClient from './queryserver-client';
import { CodeQLCliServer } from './cli';
@@ -254,14 +255,14 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu
const qhm = new QueryHistoryManager(
ctx,
queryHistoryConfigurationListener,
async item => showResultsForInfo(item.info, WebviewReveal.Forced)
async item => showResultsForCompletedQuery(item, WebviewReveal.Forced)
);
const intm = new InterfaceManager(ctx, dbm, cliServer, queryServerLogger);
ctx.subscriptions.push(intm);
archiveFilesystemProvider.activate(ctx);
async function showResultsForInfo(info: EvaluationInfo, forceReveal: WebviewReveal): Promise<void> {
await intm.showResults(info, forceReveal, false);
async function showResultsForCompletedQuery(query: CompletedQuery, forceReveal: WebviewReveal): Promise<void> {
await intm.showResults(query, forceReveal, false);
}
async function compileAndRunQuery(quickEval: boolean, selectedQuery: Uri | undefined) {
@@ -272,8 +273,8 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu
throw new Error('Can\'t run query without a selected database');
}
const info = await compileAndRunQueryAgainstDatabase(cliServer, qs, dbItem, quickEval, selectedQuery);
await showResultsForInfo(info, WebviewReveal.NotForced);
qhm.push(info);
const item = qhm.addQuery(info);
await showResultsForCompletedQuery(item, WebviewReveal.NotForced);
}
catch (e) {
if (e instanceof UserCancellationException) {

View File

@@ -1,7 +1,7 @@
import * as path from 'path';
import { CancellationToken, ExtensionContext, ProgressOptions, window as Window, workspace } from 'vscode';
import { logger } from './logging';
import { EvaluationInfo } from './queries';
import { QueryInfo } from './run-queries';
export interface ProgressUpdate {
/**
@@ -121,17 +121,17 @@ export function getOnDiskWorkspaceFolders() {
* Gets a human-readable name for an evaluated query.
* Uses metadata if it exists, and defaults to the query file name.
*/
export function getQueryName(info: EvaluationInfo) {
export function getQueryName(query: QueryInfo) {
// Queries run through quick evaluation are not usually the entire query file.
// Label them differently and include the line numbers.
if (info.query.quickEvalPosition !== undefined) {
const { line, endLine, fileName } = info.query.quickEvalPosition;
if (query.quickEvalPosition !== undefined) {
const { line, endLine, fileName } = query.quickEvalPosition;
const lineInfo = line === endLine ? `${line}` : `${line}-${endLine}`;
return `Quick evaluation of ${path.basename(fileName)}:${lineInfo}`;
} else if (info.query.metadata && info.query.metadata.name) {
return info.query.metadata.name;
} else if (query.metadata && query.metadata.name) {
return query.metadata.name;
} else {
return path.basename(info.query.program.queryPath);
return path.basename(query.program.queryPath);
}
}

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;
@@ -29,7 +37,7 @@ export interface Interpretation {
sarif: sarif.Log;
}
export interface ResultsInfo {
export interface ResultsPaths {
resultsPath: string;
interpretedResultsPath: string;
}
@@ -53,10 +61,11 @@ export interface ResultsUpdatingMsg {
export interface SetStateMsg {
t: 'setState';
resultsPath: string;
origResultsPaths: ResultsPaths;
sortedResultsMap: SortedResultsMap;
interpretation: undefined | Interpretation;
database: DatabaseInfo;
kind?: string;
metadata?: QueryMetadata
/**
* Whether to keep displaying the old results while rendering the new results.
*
@@ -86,7 +95,8 @@ interface ViewSourceFileMsg {
interface ToggleDiagnostics {
t: 'toggleDiagnostics';
databaseUri: string;
resultsPath: string;
metadata?: QueryMetadata
origResultsPaths: ResultsPaths;
visible: boolean;
kind?: string;
};

View File

@@ -1,20 +1,21 @@
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 Sarif from 'sarif';
import { FivePartLocation, LocationStyle, LocationValue, ResolvableLocationValue, tryGetResolvableLocation, WholeFileLocation } 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';
import { Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, languages, Location, Range, Uri, window as Window, workspace } from 'vscode';
import * as cli from './cli';
import { CodeQLCliServer } from './cli';
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, INTERPRETED_RESULTS_PER_RUN_LIMIT, IntoResultsViewMsg, QueryMetadata, ResultsPaths, SortedResultSetInfo, SortedResultsMap } from './interface-types';
import { Logger } from './logging';
import * as messages from './messages';
import { EvaluationInfo, interpretResults, QueryInfo, tmpDir } from './queries';
import { CompletedQuery, interpretResults } from './query-results';
import { QueryInfo, tmpDir } from './run-queries';
import { parseSarifLocation, parseSarifPlainTextMessage } from './sarif-utils';
/**
* interface.ts
@@ -86,7 +87,7 @@ export function webviewUriToFileUri(webviewUri: string): Uri {
}
export class InterfaceManager extends DisposableObject {
private _displayedEvaluationInfo?: EvaluationInfo;
private _displayedQuery?: CompletedQuery;
private _panel: vscode.WebviewPanel | undefined;
private _panelLoaded = false;
private _panelLoadedCallBacks: (() => void)[] = [];
@@ -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.origResultsPaths, msg.metadata, databaseItem);
}
} else {
// TODO: Only clear diagnostics on the same database.
@@ -179,14 +180,14 @@ export class InterfaceManager extends DisposableObject {
this._panelLoadedCallBacks = [];
break;
case 'changeSort': {
if (this._displayedEvaluationInfo === undefined) {
if (this._displayedQuery === undefined) {
showAndLogErrorMessage("Failed to sort results since evaluation info was unknown.");
break;
}
// Notify the webview that it should expect new results.
await this.postMessage({ t: 'resultsUpdating' });
await this._displayedEvaluationInfo.query.updateSortState(this.cliServer, msg.resultSetName, msg.sortState);
await this.showResults(this._displayedEvaluationInfo, WebviewReveal.NotForced, true);
await this._displayedQuery.updateSortState(this.cliServer, msg.resultSetName, msg.sortState);
await this.showResults(this._displayedQuery, WebviewReveal.NotForced, true);
break;
}
default:
@@ -210,25 +211,25 @@ export class InterfaceManager extends DisposableObject {
/**
* Show query results in webview panel.
* @param info Evaluation info for the executed query.
* @param results Evaluation info for the executed query.
* @param shouldKeepOldResultsWhileRendering Should keep old results while rendering.
* @param forceReveal Force the webview panel to be visible and
* Appropriate when the user has just performed an explicit
* UI interaction requesting results, e.g. clicking on a query
* history entry.
*/
public async showResults(info: EvaluationInfo, forceReveal: WebviewReveal, shouldKeepOldResultsWhileRendering: boolean = false): Promise<void> {
if (info.result.resultType !== messages.QueryResultType.SUCCESS) {
public async showResults(results: CompletedQuery, forceReveal: WebviewReveal, shouldKeepOldResultsWhileRendering: boolean = false): Promise<void> {
if (results.result.resultType !== messages.QueryResultType.SUCCESS) {
return;
}
const interpretation = await this.interpretResultsInfo(info.query, info.query.resultsInfo);
const interpretation = await this.interpretResultsInfo(results.query);
const sortedResultsMap: SortedResultsMap = {};
info.query.sortedResultsInfo.forEach((v, k) =>
results.sortedResultsInfo.forEach((v, k) =>
sortedResultsMap[k] = this.convertPathPropertiesToWebviewUris(v));
this._displayedEvaluationInfo = info;
this._displayedQuery = results;
const panel = this.getPanel();
await this.waitForPanelLoaded();
@@ -241,7 +242,7 @@ export class InterfaceManager extends DisposableObject {
// more asynchronous message to not so abruptly interrupt
// user's workflow by immediately revealing the panel.
const showButton = 'View Results';
const queryName = helpers.getQueryName(info);
const queryName = results.queryName;
const resultPromise = vscode.window.showInformationMessage(
`Finished running query ${(queryName.length > 0) ? `${queryName}` : ''}.`,
showButton
@@ -258,17 +259,39 @@ export class InterfaceManager extends DisposableObject {
await this.postMessage({
t: 'setState',
interpretation,
resultsPath: this.convertPathToWebviewUri(info.query.resultsInfo.resultsPath),
origResultsPaths: results.query.resultsPaths,
resultsPath: this.convertPathToWebviewUri(results.query.resultsPaths.resultsPath),
sortedResultsMap,
database: info.database,
database: results.database,
shouldKeepOldResultsWhileRendering,
kind: info.query.metadata ? info.query.metadata.kind : undefined
metadata: results.query.metadata
});
}
private async interpretResultsInfo(query: QueryInfo, resultsInfo: ResultsInfo): Promise<Interpretation | undefined> {
private async getTruncatedResults(metadata: QueryMetadata | undefined, resultsPaths: ResultsPaths, sourceInfo: cli.SourceInfo | undefined, sourceLocationPrefix: string): Promise<Interpretation> {
const sarif = await interpretResults(this.cliServer, metadata, resultsPaths.interpretedResultsPath, 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): Promise<Interpretation | undefined> {
let interpretation: Interpretation | undefined = undefined;
if (query.hasInterpretedResults()
if (await query.hasInterpretedResults()
&& query.quickEvalPosition === undefined // never do results interpretation if quickEval
) {
try {
@@ -277,23 +300,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, query.resultsPaths, sourceInfo, sourceLocationPrefix);
}
catch (e) {
// If interpretation fails, accept the error and continue
@@ -301,90 +308,97 @@ 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) {
// 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);
private async showResultsAsDiagnostics(resultsInfo: ResultsPaths, metadata: QueryMetadata | undefined, database: DatabaseItem) {
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, resultsInfo, 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);
}
@@ -429,8 +443,8 @@ async function showLocation(loc: ResolvableLocationValue, databaseItem: Database
const doc = await workspace.openTextDocument(resolvedLocation.uri);
const editorsWithDoc = Window.visibleTextEditors.filter(e => e.document === doc);
const editor = editorsWithDoc.length > 0
? editorsWithDoc[0]
: await Window.showTextDocument(doc, vscode.ViewColumn.One);
? editorsWithDoc[0]
: await Window.showTextDocument(doc, vscode.ViewColumn.One);
let range = resolvedLocation.range;
// When highlighting the range, vscode's occurrence-match and bracket-match highlighting will
// trigger based on where we place the cursor/selection, and will compete for the user's attention.
@@ -443,8 +457,8 @@ async function showLocation(loc: ResolvableLocationValue, databaseItem: Database
// For multi-line ranges, place the cursor at the beginning to avoid visual artifacts from selected line-breaks.
// Multi-line ranges are usually large enough to overshadow the noise from bracket highlighting.
let selectionEnd = (range.start.line === range.end.line)
? range.end
: range.start;
? range.end
: range.start;
editor.selection = new vscode.Selection(range.start, selectionEnd);
editor.revealRange(range, vscode.TextEditorRevealType.InCenter);
editor.setDecorations(shownLocationDecoration, [range]);
@@ -479,22 +493,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

@@ -1,10 +1,10 @@
import * as path from 'path';
import * as vscode from 'vscode';
import { ExtensionContext, window as Window } from 'vscode';
import { EvaluationInfo } from './queries';
import * as helpers from './helpers';
import * as messages from './messages';
import { CompletedQuery } from './query-results';
import { QueryHistoryConfig } from './config';
import { QueryWithResults } from './run-queries';
/**
* query-history.ts
* ------------
@@ -24,76 +24,10 @@ export type QueryHistoryItemOptions = {
*/
const FAILED_QUERY_HISTORY_ITEM_ICON: string = 'media/red-x.svg';
/**
* One item in the user-displayed list of queries that have been run.
*/
export class QueryHistoryItem {
queryName: string;
time: string;
databaseName: string;
info: EvaluationInfo;
constructor(
info: EvaluationInfo,
public config: QueryHistoryConfig,
public options: QueryHistoryItemOptions = info.historyItemOptions,
) {
this.queryName = helpers.getQueryName(info);
this.databaseName = info.database.name;
this.info = info;
this.time = new Date().toLocaleString();
}
get statusString(): string {
switch (this.info.result.resultType) {
case messages.QueryResultType.CANCELLATION:
return `cancelled after ${this.info.result.evaluationTime / 1000} seconds`;
case messages.QueryResultType.OOM:
return `out of memory`;
case messages.QueryResultType.SUCCESS:
return `finished in ${this.info.result.evaluationTime / 1000} seconds`;
case messages.QueryResultType.TIMEOUT:
return `timed out after ${this.info.result.evaluationTime / 1000} seconds`;
case messages.QueryResultType.OTHER_ERROR:
default:
return `failed`;
}
}
interpolate(template: string): string {
const { databaseName, queryName, time, statusString } = this;
const replacements: { [k: string]: string } = {
t: time,
q: queryName,
d: databaseName,
s: statusString,
'%': '%',
};
return template.replace(/%(.)/g, (match, key) => {
const replacement = replacements[key];
return replacement !== undefined ? replacement : match;
});
}
getLabel(): string {
if (this.options.label !== undefined)
return this.options.label;
return this.config.format;
}
get didRunSuccessfully(): boolean {
return this.info.result.resultType === messages.QueryResultType.SUCCESS;
}
toString(): string {
return this.interpolate(this.getLabel());
}
}
/**
* Tree data provider for the query history view.
*/
class HistoryTreeDataProvider implements vscode.TreeDataProvider<QueryHistoryItem> {
class HistoryTreeDataProvider implements vscode.TreeDataProvider<CompletedQuery> {
/**
* XXX: This idiom for how to get a `.fire()`-able event emitter was
@@ -101,22 +35,20 @@ class HistoryTreeDataProvider implements vscode.TreeDataProvider<QueryHistoryIte
* involved and I hope there's something better that can be done
* instead.
*/
private _onDidChangeTreeData: vscode.EventEmitter<QueryHistoryItem | undefined> = new vscode.EventEmitter<QueryHistoryItem | undefined>();
readonly onDidChangeTreeData: vscode.Event<QueryHistoryItem | undefined> = this._onDidChangeTreeData.event;
private _onDidChangeTreeData: vscode.EventEmitter<CompletedQuery | undefined> = new vscode.EventEmitter<CompletedQuery | undefined>();
readonly onDidChangeTreeData: vscode.Event<CompletedQuery | undefined> = this._onDidChangeTreeData.event;
private ctx: ExtensionContext;
private history: QueryHistoryItem[] = [];
private history: CompletedQuery[] = [];
/**
* When not undefined, must be reference-equal to an item in `this.databases`.
*/
private current: QueryHistoryItem | undefined;
private current: CompletedQuery | undefined;
constructor(ctx: ExtensionContext) {
this.ctx = ctx;
constructor(private ctx: ExtensionContext) {
}
getTreeItem(element: QueryHistoryItem): vscode.TreeItem {
getTreeItem(element: CompletedQuery): vscode.TreeItem {
const it = new vscode.TreeItem(element.toString());
it.command = {
@@ -132,7 +64,7 @@ class HistoryTreeDataProvider implements vscode.TreeDataProvider<QueryHistoryIte
return it;
}
getChildren(element?: QueryHistoryItem): vscode.ProviderResult<QueryHistoryItem[]> {
getChildren(element?: CompletedQuery): vscode.ProviderResult<CompletedQuery[]> {
if (element == undefined) {
return this.history;
}
@@ -141,25 +73,25 @@ class HistoryTreeDataProvider implements vscode.TreeDataProvider<QueryHistoryIte
}
}
getParent(_element: QueryHistoryItem): vscode.ProviderResult<QueryHistoryItem> {
getParent(_element: CompletedQuery): vscode.ProviderResult<CompletedQuery> {
return null;
}
getCurrent(): QueryHistoryItem | undefined {
getCurrent(): CompletedQuery | undefined {
return this.current;
}
push(item: QueryHistoryItem): void {
push(item: CompletedQuery): void {
this.current = item;
this.history.push(item);
this.refresh();
}
setCurrentItem(item: QueryHistoryItem) {
setCurrentItem(item: CompletedQuery) {
this.current = item;
}
remove(item: QueryHistoryItem) {
remove(item: CompletedQuery) {
if (this.current === item)
this.current = undefined;
const index = this.history.findIndex(i => i === item);
@@ -188,19 +120,19 @@ const DOUBLE_CLICK_TIME = 500;
export class QueryHistoryManager {
treeDataProvider: HistoryTreeDataProvider;
ctx: ExtensionContext;
treeView: vscode.TreeView<QueryHistoryItem>;
selectedCallback: ((item: QueryHistoryItem) => void) | undefined;
lastItemClick: { time: Date, item: QueryHistoryItem } | undefined;
treeView: vscode.TreeView<CompletedQuery>;
selectedCallback: ((item: CompletedQuery) => void) | undefined;
lastItemClick: { time: Date, item: CompletedQuery } | undefined;
async invokeCallbackOn(queryHistoryItem: QueryHistoryItem) {
async invokeCallbackOn(queryHistoryItem: CompletedQuery) {
if (this.selectedCallback !== undefined) {
const sc = this.selectedCallback;
await sc(queryHistoryItem);
}
}
async handleOpenQuery(queryHistoryItem: QueryHistoryItem): Promise<void> {
const textDocument = await vscode.workspace.openTextDocument(vscode.Uri.file(queryHistoryItem.info.query.program.queryPath));
async handleOpenQuery(queryHistoryItem: CompletedQuery): Promise<void> {
const textDocument = await vscode.workspace.openTextDocument(vscode.Uri.file(queryHistoryItem.query.program.queryPath));
const editor = await vscode.window.showTextDocument(textDocument, vscode.ViewColumn.One);
const queryText = queryHistoryItem.options.queryText;
if (queryText !== undefined) {
@@ -210,7 +142,7 @@ export class QueryHistoryManager {
}
}
async handleRemoveHistoryItem(queryHistoryItem: QueryHistoryItem) {
async handleRemoveHistoryItem(queryHistoryItem: CompletedQuery) {
this.treeDataProvider.remove(queryHistoryItem);
const current = this.treeDataProvider.getCurrent();
if (current !== undefined) {
@@ -219,7 +151,7 @@ export class QueryHistoryManager {
}
}
async handleSetLabel(queryHistoryItem: QueryHistoryItem) {
async handleSetLabel(queryHistoryItem: CompletedQuery) {
const response = await vscode.window.showInputBox({
prompt: 'Label:',
placeHolder: '(use default)',
@@ -236,7 +168,7 @@ export class QueryHistoryManager {
}
}
async handleItemClicked(queryHistoryItem: QueryHistoryItem) {
async handleItemClicked(queryHistoryItem: CompletedQuery) {
this.treeDataProvider.setCurrentItem(queryHistoryItem);
const now = new Date();
@@ -258,7 +190,7 @@ export class QueryHistoryManager {
constructor(
ctx: ExtensionContext,
private queryHistoryConfigListener: QueryHistoryConfig,
selectedCallback?: (item: QueryHistoryItem) => Promise<void>
selectedCallback?: (item: CompletedQuery) => Promise<void>
) {
this.ctx = ctx;
this.selectedCallback = selectedCallback;
@@ -284,10 +216,11 @@ export class QueryHistoryManager {
});
}
push(evaluationInfo: EvaluationInfo) {
const item = new QueryHistoryItem(evaluationInfo, this.queryHistoryConfigListener);
addQuery(info: QueryWithResults): CompletedQuery {
const item = new CompletedQuery(info, this.queryHistoryConfigListener);
this.treeDataProvider.push(item);
this.updateTreeViewSelectionIfVisible();
return item;
}
/**

View File

@@ -0,0 +1,134 @@
import { QueryWithResults, tmpDir, QueryInfo } from "./run-queries";
import * as messages from './messages';
import * as helpers from './helpers';
import * as cli from './cli';
import * as sarif from 'sarif';
import * as fs from 'fs-extra';
import * as path from 'path';
import { SortState, SortedResultSetInfo, DatabaseInfo, QueryMetadata } from "./interface-types";
import { QueryHistoryConfig } from "./config";
import { QueryHistoryItemOptions } from "./query-history";
export class CompletedQuery implements QueryWithResults {
readonly time: string;
readonly query: QueryInfo;
readonly result: messages.EvaluationResult;
readonly database: DatabaseInfo;
options: QueryHistoryItemOptions;
/**
* Map from result set name to SortedResultSetInfo.
*/
sortedResultsInfo: Map<string, SortedResultSetInfo>;
constructor(
evalaution: QueryWithResults,
public config: QueryHistoryConfig,
) {
this.query = evalaution.query;
this.result = evalaution.result;
this.database = evalaution.database;
this.time = new Date().toLocaleString();
this.sortedResultsInfo = new Map();
this.options = evalaution.options;
}
get databaseName(): string {
return this.database.name;
}
get queryName(): string {
return helpers.getQueryName(this.query);
}
/**
* Holds if this query should produce interpreted results.
*/
canInterpretedResults(): Promise<boolean> {
return this.query.dbItem.hasMetadataFile();
}
get statusString(): string {
switch (this.result.resultType) {
case messages.QueryResultType.CANCELLATION:
return `cancelled after ${this.result.evaluationTime / 1000} seconds`;
case messages.QueryResultType.OOM:
return `out of memory`;
case messages.QueryResultType.SUCCESS:
return `finished in ${this.result.evaluationTime / 1000} seconds`;
case messages.QueryResultType.TIMEOUT:
return `timed out after ${this.result.evaluationTime / 1000} seconds`;
case messages.QueryResultType.OTHER_ERROR:
default:
return `failed`;
}
}
interpolate(template: string): string {
const { databaseName, queryName, time, statusString } = this;
const replacements: { [k: string]: string } = {
t: time,
q: queryName,
d: databaseName,
s: statusString,
'%': '%',
};
return template.replace(/%(.)/g, (match, key) => {
const replacement = replacements[key];
return replacement !== undefined ? replacement : match;
});
}
getLabel(): string {
if (this.options.label !== undefined)
return this.options.label;
return this.config.format;
}
get didRunSuccessfully(): boolean {
return this.result.resultType === messages.QueryResultType.SUCCESS;
}
toString(): string {
return this.interpolate(this.getLabel());
}
async updateSortState(server: cli.CodeQLCliServer, resultSetName: string, sortState: SortState | undefined): Promise<void> {
if (sortState === undefined) {
this.sortedResultsInfo.delete(resultSetName);
return;
}
const sortedResultSetInfo: SortedResultSetInfo = {
resultsPath: path.join(tmpDir.name, `sortedResults${this.query.queryID}-${resultSetName}.bqrs`),
sortState
};
await server.sortBqrs(this.query.resultsPaths.resultsPath, sortedResultSetInfo.resultsPath, resultSetName, [sortState.columnIndex], [sortState.direction]);
this.sortedResultsInfo.set(resultSetName, sortedResultSetInfo);
}
}
/**
* Call cli command to interpret results.
*/
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'));
}
if (metadata === undefined) {
throw new Error('Can\'t interpret results without query metadata');
}
let { kind, id } = metadata;
if (kind === undefined) {
throw new Error('Can\'t interpret results without query metadata including kind');
}
if (id === undefined) {
// Interpretation per se doesn't really require an id, but the
// SARIF format does, so in the absence of one, we use a dummy id.
id = "dummy-id";
}
return await server.interpretBqrs({ kind, id }, resultsPath, interpretedResultsPath, sourceInfo);
}

View File

@@ -8,7 +8,7 @@ import { CodeQLCliServer } from './cli';
import { DatabaseUI } from './databases-ui';
import * as helpers from './helpers';
import { logger } from './logging';
import { UserCancellationException } from './queries';
import { UserCancellationException } from './run-queries';
const QUICK_QUERIES_DIR_NAME = 'quick-queries';
const QUICK_QUERY_QUERY_NAME = 'quick-query.ql';

View File

@@ -1,29 +1,22 @@
import * as crypto from 'crypto';
import * as fs from 'fs-extra';
import * as path from 'path';
import * as sarif from 'sarif';
import * as tmp from 'tmp';
import { promisify } from 'util';
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, QueryMetadata, ResultsPaths } from './interface-types';
import { logger } from './logging';
import * as messages from './messages';
import * as qsClient from './queryserver-client';
import { promisify } from 'util';
import { QueryHistoryItemOptions } from './query-history';
import * as qsClient from './queryserver-client';
import { isQuickQueryPath } from './quick-query';
import { upgradeDatabase } from './upgrades';
/**
* Maximum number of lines to include from database upgrade message,
* to work around the fact that we can't guarantee a scrollable text
* box for it when displaying in dialog boxes.
*/
const MAX_UPGRADE_MESSAGE_LINES = 10;
/**
* queries.ts
* run-queries.ts
* -------------
*
* Compiling and running QL queries.
@@ -31,7 +24,7 @@ const MAX_UPGRADE_MESSAGE_LINES = 10;
// XXX: Tmp directory should be configuarble.
export const tmpDir = tmp.dirSync({ prefix: 'queries_', keep: false, unsafeCleanup: true });
const upgradesTmpDir = tmp.dirSync({ dir: tmpDir.name, prefix: 'upgrades_', keep: false, unsafeCleanup: true });
export const upgradesTmpDir = tmp.dirSync({ dir: tmpDir.name, prefix: 'upgrades_', keep: false, unsafeCleanup: true });
export const tmpDirDisposal = {
dispose: () => {
upgradesTmpDir.removeCallback();
@@ -39,7 +32,6 @@ export const tmpDirDisposal = {
}
};
export class UserCancellationException extends Error { }
/**
@@ -49,30 +41,26 @@ export class UserCancellationException extends Error { }
* output and results.
*/
export class QueryInfo {
compiledQueryPath: string;
resultsInfo: ResultsInfo;
private static nextQueryId = 0;
/**
* Map from result set name to SortedResultSetInfo.
*/
sortedResultsInfo: Map<string, SortedResultSetInfo>;
dataset: vscode.Uri; // guarantee the existence of a well-defined dataset dir at this point
queryId: number;
readonly compiledQueryPath: string;
readonly resultsPaths: ResultsPaths;
readonly dataset: vscode.Uri; // guarantee the existence of a well-defined dataset dir at this point
readonly queryID: number;
constructor(
public program: messages.QlProgram,
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 readonly program: messages.QlProgram,
public readonly dbItem: DatabaseItem,
public readonly queryDbscheme: string, // the dbscheme file the query expects, based on library path resolution
public readonly quickEvalPosition?: messages.Position,
public readonly 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.queryID = QueryInfo.nextQueryId++;
this.compiledQueryPath = path.join(tmpDir.name, `compiledQuery${this.queryID}.qlo`);
this.resultsPaths = {
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) {
throw new Error('Can\'t run query on invalid database.');
}
@@ -87,7 +75,7 @@ export class QueryInfo {
const callbackId = qs.registerCallback(res => { result = res });
const queryToRun: messages.QueryToRun = {
resultsPath: this.resultsInfo.resultsPath,
resultsPath: this.resultsPaths.resultsPath,
qlo: vscode.Uri.file(this.compiledQueryPath).toString(),
allowUnknownTemplates: true,
id: callbackId,
@@ -166,239 +154,13 @@ export class QueryInfo {
}
return hasMetadataFile;
}
async updateSortState(server: cli.CodeQLCliServer, resultSetName: string, sortState: SortState | undefined): Promise<void> {
if (sortState === undefined) {
this.sortedResultsInfo.delete(resultSetName);
return;
}
const sortedResultSetInfo: SortedResultSetInfo = {
resultsPath: path.join(tmpDir.name, `sortedResults${this.queryId}-${resultSetName}.bqrs`),
sortState
};
await server.sortBqrs(this.resultsInfo.resultsPath, sortedResultSetInfo.resultsPath, resultSetName, [sortState.columnIndex], [sortState.direction]);
this.sortedResultsInfo.set(resultSetName, sortedResultSetInfo);
}
}
/**
* 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'));
}
const { metadata } = queryInfo;
if (metadata == undefined) {
throw new Error('Can\'t interpret results without query metadata');
}
let { kind, id } = metadata;
if (kind == undefined) {
throw new Error('Can\'t interpret results without query metadata including kind');
}
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, '-');
}
return await server.interpretBqrs({ kind, id }, resultsInfo.resultsPath, resultsInfo.interpretedResultsPath, sourceInfo);
}
export interface EvaluationInfo {
query: QueryInfo;
result: messages.EvaluationResult;
database: DatabaseInfo;
historyItemOptions: QueryHistoryItemOptions;
}
/**
* Checks whether the given database can be upgraded to the given target DB scheme,
* and whether the user wants to proceed with the upgrade.
* Reports errors to both the user and the console.
* @returns the `UpgradeParams` needed to start the upgrade, if the upgrade is possible and was confirmed by the user, or `undefined` otherwise.
*/
async function checkAndConfirmDatabaseUpgrade(qs: qsClient.QueryServerClient, db: DatabaseItem, targetDbScheme: vscode.Uri, upgradesDirectories: vscode.Uri[]):
Promise<messages.UpgradeParams | undefined> {
if (db.contents === undefined || db.contents.dbSchemeUri === undefined) {
helpers.showAndLogErrorMessage("Database is invalid, and cannot be upgraded.");
return;
}
const params: messages.UpgradeParams = {
fromDbscheme: db.contents.dbSchemeUri.fsPath,
toDbscheme: targetDbScheme.fsPath,
additionalUpgrades: upgradesDirectories.map(uri => uri.fsPath)
};
let checkUpgradeResult: messages.CheckUpgradeResult;
try {
qs.logger.log('Checking database upgrade...');
checkUpgradeResult = await checkDatabaseUpgrade(qs, params);
}
catch (e) {
helpers.showAndLogErrorMessage(`Database cannot be upgraded: ${e}`);
return;
}
finally {
qs.logger.log('Done checking database upgrade.');
}
const checkedUpgrades = checkUpgradeResult.checkedUpgrades;
if (checkedUpgrades === undefined) {
const error = checkUpgradeResult.upgradeError || '[no error message available]';
await helpers.showAndLogErrorMessage(`Database cannot be upgraded: ${error}`);
return;
}
if (checkedUpgrades.scripts.length === 0) {
await helpers.showAndLogInformationMessage('Database is already up to date; nothing to do.');
return;
}
let curSha = checkedUpgrades.initialSha;
let descriptionMessage = '';
for (const script of checkedUpgrades.scripts) {
descriptionMessage += `Would perform upgrade: ${script.description}\n`;
descriptionMessage += `\t-> Compatibility: ${script.compatibility}\n`;
curSha = script.newSha;
}
const targetSha = checkedUpgrades.targetSha;
if (curSha != targetSha) {
// Newlines aren't rendered in notifications: https://github.com/microsoft/vscode/issues/48900
// A modal dialog would be rendered better, but is more intrusive.
await helpers.showAndLogErrorMessage(`Database cannot be upgraded to the target database scheme.
Can upgrade from ${checkedUpgrades.initialSha} (current) to ${curSha}, but cannot reach ${targetSha} (target).`);
// TODO: give a more informative message if we think the DB is ahead of the target DB scheme
return;
}
logger.log(descriptionMessage);
// Ask the user to confirm the upgrade.
const showLogItem: vscode.MessageItem = { title: 'No, Show Changes', isCloseAffordance: true };
const yesItem = { title: 'Yes', isCloseAffordance: false };
const noItem = { title: 'No', isCloseAffordance: true }
let dialogOptions: vscode.MessageItem[] = [yesItem, noItem];
let messageLines = descriptionMessage.split('\n');
if (messageLines.length > MAX_UPGRADE_MESSAGE_LINES) {
messageLines = messageLines.slice(0, MAX_UPGRADE_MESSAGE_LINES);
messageLines.push(`The list of upgrades was truncated, click "No, Show Changes" to see the full list.`);
dialogOptions.push(showLogItem);
}
const message = `Should the database ${db.databaseUri.fsPath} be upgraded?\n\n${messageLines.join("\n")}`;
const chosenItem = await vscode.window.showInformationMessage(message, { modal: true }, ...dialogOptions);
if (chosenItem === showLogItem) {
logger.outputChannel.show();
}
if (chosenItem === yesItem) {
return params;
}
else {
throw new UserCancellationException('User cancelled the database upgrade.');
}
}
/**
* Command handler for 'Upgrade Database'.
* Attempts to upgrade the given database to the given target DB scheme, using the given directory of upgrades.
* First performs a dry-run and prompts the user to confirm the upgrade.
* Reports errors during compilation and evaluation of upgrades to the user.
*/
export async function upgradeDatabase(qs: qsClient.QueryServerClient, db: DatabaseItem, targetDbScheme: vscode.Uri, upgradesDirectories: vscode.Uri[]):
Promise<messages.RunUpgradeResult | undefined> {
const upgradeParams = await checkAndConfirmDatabaseUpgrade(qs, db, targetDbScheme, upgradesDirectories);
if (upgradeParams === undefined) {
return;
}
let compileUpgradeResult: messages.CompileUpgradeResult;
try {
compileUpgradeResult = await compileDatabaseUpgrade(qs, upgradeParams);
}
catch (e) {
helpers.showAndLogErrorMessage(`Compilation of database upgrades failed: ${e}`);
return;
}
finally {
qs.logger.log('Done compiling database upgrade.')
}
if (compileUpgradeResult.compiledUpgrades === undefined) {
const error = compileUpgradeResult.error || '[no error message available]';
helpers.showAndLogErrorMessage(`Compilation of database upgrades failed: ${error}`);
return;
}
try {
qs.logger.log('Running the following database upgrade:');
qs.logger.log(compileUpgradeResult.compiledUpgrades.scripts.map(s => s.description.description).join('\n'));
return await runDatabaseUpgrade(qs, db, compileUpgradeResult.compiledUpgrades);
}
catch (e) {
helpers.showAndLogErrorMessage(`Database upgrade failed: ${e}`);
return;
}
finally {
qs.logger.log('Done running database upgrade.')
}
}
async function checkDatabaseUpgrade(qs: qsClient.QueryServerClient, upgradeParams: messages.UpgradeParams):
Promise<messages.CheckUpgradeResult> {
return helpers.withProgress({
location: vscode.ProgressLocation.Notification,
title: "Checking for database upgrades",
cancellable: true,
}, (progress, token) => qs.sendRequest(messages.checkUpgrade, upgradeParams, token, progress));
}
async function compileDatabaseUpgrade(qs: qsClient.QueryServerClient, upgradeParams: messages.UpgradeParams):
Promise<messages.CompileUpgradeResult> {
const params: messages.CompileUpgradeParams = {
upgrade: upgradeParams,
upgradeTempDir: upgradesTmpDir.name
}
return helpers.withProgress({
location: vscode.ProgressLocation.Notification,
title: "Compiling database upgrades",
cancellable: true,
}, (progress, token) => qs.sendRequest(messages.compileUpgrade, params, token, progress));
}
async function runDatabaseUpgrade(qs: qsClient.QueryServerClient, db: DatabaseItem, upgrades: messages.CompiledUpgrades):
Promise<messages.RunUpgradeResult> {
if (db.contents === undefined || db.contents.datasetUri === undefined) {
throw new Error('Can\'t upgrade an invalid database.');
}
const database: messages.Dataset = {
dbDir: db.contents.datasetUri.fsPath,
workingSet: 'default'
};
const params: messages.RunUpgradeParams = {
db: database,
timeoutSecs: qs.config.timeoutSecs,
toRun: upgrades
};
return helpers.withProgress({
location: vscode.ProgressLocation.Notification,
title: "Running database upgrades",
cancellable: true,
}, (progress, token) => qs.sendRequest(messages.runUpgrade, params, token, progress));
export interface QueryWithResults {
readonly query: QueryInfo;
readonly result: messages.EvaluationResult;
readonly database: DatabaseInfo;
readonly options: QueryHistoryItemOptions;
}
export async function clearCacheInDatabase(qs: qsClient.QueryServerClient, dbItem: DatabaseItem):
@@ -594,7 +356,7 @@ export async function compileAndRunQueryAgainstDatabase(
db: DatabaseItem,
quickEval: boolean,
selectedQueryUri: vscode.Uri | undefined
): Promise<EvaluationInfo> {
): Promise<QueryWithResults> {
if (!db.contents || !db.contents.dbSchemeUri) {
throw new Error(`Database ${db.databaseUri} does not have a CodeQL database scheme.`);
@@ -638,7 +400,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) {
@@ -660,7 +422,7 @@ export async function compileAndRunQueryAgainstDatabase(
name: db.name,
databaseUri: db.databaseUri.toString(true)
},
historyItemOptions
options: historyItemOptions
};
} else {
// Error dialogs are limited in size and scrollability,
@@ -699,7 +461,7 @@ export async function compileAndRunQueryAgainstDatabase(
name: db.name,
databaseUri: db.databaseUri.toString(true)
},
historyItemOptions,
options: historyItemOptions,
};
}
}

View File

@@ -0,0 +1,124 @@
import * as Sarif from "sarif"
import * as path from "path"
import { LocationStyle, ResolvableLocationValue } from "semmle-bqrs";
export interface SarifLink {
dest: number
text: string
}
type ParsedSarifLocation =
| ResolvableLocationValue
// Resolvable locations have a `file` field, but it will sometimes include
// a source location prefix, which contains build-specific information the user
// doesn't really need to see. We ensure that `userVisibleFile` will not contain
// that, and is appropriate for display in the UI.
& { userVisibleFile: string }
| { t: 'NoLocation', hint: string };
export type SarifMessageComponent = string | SarifLink
/**
* Unescape "[", "]" and "\\" like in sarif plain text messages
*/
export function unescapeSarifText(message: string): string {
return message.replace(/\\\[/g, "[").replace(/\\\]/g, "]").replace(/\\\\/, "\\");
}
export function parseSarifPlainTextMessage(message: string): SarifMessageComponent[] {
let results: SarifMessageComponent[] = [];
// We want something like "[linkText](4)", except that "[" and "]" may be escaped. The lookbehind asserts
// that the initial [ is not escaped. Then we parse a link text with "[" and "]" escaped. Then we parse the numerical target.
// Technically we could have any uri in the target but we don't output that yet.
// The possibility of escaping outside the link is not mentioned in the sarif spec but we always output sartif this way.
const linkRegex = /(?<=(?<!\\)(\\\\)*)\[(?<linkText>([^\\\]\[]|\\\\|\\\]|\\\[)*)\]\((?<linkTarget>[0-9]+)\)/g;
let result: RegExpExecArray | null;
let curIndex = 0;
while ((result = linkRegex.exec(message)) !== null) {
results.push(unescapeSarifText(message.substring(curIndex, result.index)));
const linkText = result.groups!["linkText"];
const linkTarget = +result.groups!["linkTarget"];
results.push({ dest: linkTarget, text: unescapeSarifText(linkText) });
curIndex = result.index + result[0].length;
}
results.push(unescapeSarifText(message.substring(curIndex, message.length)));
return results;
}
/**
* Computes a path normalized to reflect conventional normalization
* of windows paths into zip archive paths.
* @param sourceLocationPrefix The source location prefix of a database. May be
* unix style `/foo/bar/baz` or windows-style `C:\foo\bar\baz`.
* @param sarifRelativeUri A uri relative to sourceLocationPrefix.
* @returns A string that is valid for the `.file` field of a `FivePartLocation`:
* directory separators are normalized, but drive letters `C:` may appear.
*/
export function getPathRelativeToSourceLocationPrefix(sourceLocationPrefix: string, sarifRelativeUui: string) {
const normalizedSourceLocationPrefix = sourceLocationPrefix.replace(/\\/g, '/');
return path.join(normalizedSourceLocationPrefix, decodeURIComponent(sarifRelativeUui));
}
export function parseSarifLocation(loc: Sarif.Location, sourceLocationPrefix: string): ParsedSarifLocation {
const physicalLocation = loc.physicalLocation;
if (physicalLocation === undefined)
return { t: 'NoLocation', hint: 'no physical location' };
if (physicalLocation.artifactLocation === undefined)
return { t: 'NoLocation', hint: 'no artifact location' };
if (physicalLocation.artifactLocation.uri === undefined)
return { t: 'NoLocation', hint: 'artifact location has no uri' };
// This is not necessarily really an absolute uri; it could either be a
// file uri or a relative uri.
const uri = physicalLocation.artifactLocation.uri;
const fileUriRegex = /^file:/;
const effectiveLocation = uri.match(fileUriRegex) ?
decodeURIComponent(uri.replace(fileUriRegex, '')) :
getPathRelativeToSourceLocationPrefix(sourceLocationPrefix, uri);
const userVisibleFile = uri.match(fileUriRegex) ?
decodeURIComponent(uri.replace(fileUriRegex, '')) :
uri;
if (physicalLocation.region === undefined) {
// If the region property is absent, the physicalLocation object refers to the entire file.
// Source: https://docs.oasis-open.org/sarif/sarif/v2.1.0/cs01/sarif-v2.1.0-cs01.html#_Toc16012638.
// TODO: Do we get here if we provide a non-filesystem URL?
return {
t: LocationStyle.WholeFile,
file: effectiveLocation,
userVisibleFile,
};
} else {
const region = physicalLocation.region;
// We assume that the SARIF we're given always has startLine
// This is not mandated by the SARIF spec, but should be true of
// SARIF output by our own tools.
const lineStart = region.startLine!;
// These defaults are from SARIF 2.1.0 spec, section 3.30.2, "Text Regions"
// https://docs.oasis-open.org/sarif/sarif/v2.1.0/cs01/sarif-v2.1.0-cs01.html#_Ref493492556
const lineEnd = region.endLine === undefined ? lineStart : region.endLine;
const colStart = region.startColumn === undefined ? 1 : region.startColumn;
// We also assume that our tools will always supply `endColumn` field, which is
// fortunate, since the SARIF spec says that it defaults to the end of the line, whose
// length we don't know at this point in the code.
//
// It is off by one with respect to the way vscode counts columns in selections.
const colEnd = region.endColumn! - 1;
return {
t: LocationStyle.FivePart,
file: effectiveLocation,
userVisibleFile,
lineStart,
colStart,
lineEnd,
colEnd,
};
}
}

View File

@@ -0,0 +1,198 @@
import * as vscode from 'vscode';
import { DatabaseItem } from './databases';
import * as helpers from './helpers';
import { logger } from './logging';
import * as messages from './messages';
import * as qsClient from './queryserver-client';
import { upgradesTmpDir, UserCancellationException } from './run-queries';
/**
* Maximum number of lines to include from database upgrade message,
* to work around the fact that we can't guarantee a scrollable text
* box for it when displaying in dialog boxes.
*/
const MAX_UPGRADE_MESSAGE_LINES = 10;
/**
* Checks whether the given database can be upgraded to the given target DB scheme,
* and whether the user wants to proceed with the upgrade.
* Reports errors to both the user and the console.
* @returns the `UpgradeParams` needed to start the upgrade, if the upgrade is possible and was confirmed by the user, or `undefined` otherwise.
*/
async function checkAndConfirmDatabaseUpgrade(qs: qsClient.QueryServerClient, db: DatabaseItem, targetDbScheme: vscode.Uri, upgradesDirectories: vscode.Uri[]):
Promise<messages.UpgradeParams | undefined> {
if (db.contents === undefined || db.contents.dbSchemeUri === undefined) {
helpers.showAndLogErrorMessage("Database is invalid, and cannot be upgraded.");
return;
}
const params: messages.UpgradeParams = {
fromDbscheme: db.contents.dbSchemeUri.fsPath,
toDbscheme: targetDbScheme.fsPath,
additionalUpgrades: upgradesDirectories.map(uri => uri.fsPath)
};
let checkUpgradeResult: messages.CheckUpgradeResult;
try {
qs.logger.log('Checking database upgrade...');
checkUpgradeResult = await checkDatabaseUpgrade(qs, params);
}
catch (e) {
helpers.showAndLogErrorMessage(`Database cannot be upgraded: ${e}`);
return;
}
finally {
qs.logger.log('Done checking database upgrade.');
}
const checkedUpgrades = checkUpgradeResult.checkedUpgrades;
if (checkedUpgrades === undefined) {
const error = checkUpgradeResult.upgradeError || '[no error message available]';
await helpers.showAndLogErrorMessage(`Database cannot be upgraded: ${error}`);
return;
}
if (checkedUpgrades.scripts.length === 0) {
await helpers.showAndLogInformationMessage('Database is already up to date; nothing to do.');
return;
}
let curSha = checkedUpgrades.initialSha;
let descriptionMessage = '';
for (const script of checkedUpgrades.scripts) {
descriptionMessage += `Would perform upgrade: ${script.description}\n`;
descriptionMessage += `\t-> Compatibility: ${script.compatibility}\n`;
curSha = script.newSha;
}
const targetSha = checkedUpgrades.targetSha;
if (curSha != targetSha) {
// Newlines aren't rendered in notifications: https://github.com/microsoft/vscode/issues/48900
// A modal dialog would be rendered better, but is more intrusive.
await helpers.showAndLogErrorMessage(`Database cannot be upgraded to the target database scheme.
Can upgrade from ${checkedUpgrades.initialSha} (current) to ${curSha}, but cannot reach ${targetSha} (target).`);
// TODO: give a more informative message if we think the DB is ahead of the target DB scheme
return;
}
logger.log(descriptionMessage);
// Ask the user to confirm the upgrade.
const showLogItem: vscode.MessageItem = { title: 'No, Show Changes', isCloseAffordance: true };
const yesItem = { title: 'Yes', isCloseAffordance: false };
const noItem = { title: 'No', isCloseAffordance: true }
let dialogOptions: vscode.MessageItem[] = [yesItem, noItem];
let messageLines = descriptionMessage.split('\n');
if (messageLines.length > MAX_UPGRADE_MESSAGE_LINES) {
messageLines = messageLines.slice(0, MAX_UPGRADE_MESSAGE_LINES);
messageLines.push(`The list of upgrades was truncated, click "No, Show Changes" to see the full list.`);
dialogOptions.push(showLogItem);
}
const message = `Should the database ${db.databaseUri.fsPath} be upgraded?\n\n${messageLines.join("\n")}`;
const chosenItem = await vscode.window.showInformationMessage(message, { modal: true }, ...dialogOptions);
if (chosenItem === showLogItem) {
logger.outputChannel.show();
}
if (chosenItem === yesItem) {
return params;
}
else {
throw new UserCancellationException('User cancelled the database upgrade.');
}
}
/**
* Command handler for 'Upgrade Database'.
* Attempts to upgrade the given database to the given target DB scheme, using the given directory of upgrades.
* First performs a dry-run and prompts the user to confirm the upgrade.
* Reports errors during compilation and evaluation of upgrades to the user.
*/
export async function upgradeDatabase(qs: qsClient.QueryServerClient, db: DatabaseItem, targetDbScheme: vscode.Uri, upgradesDirectories: vscode.Uri[]):
Promise<messages.RunUpgradeResult | undefined> {
const upgradeParams = await checkAndConfirmDatabaseUpgrade(qs, db, targetDbScheme, upgradesDirectories);
if (upgradeParams === undefined) {
return;
}
let compileUpgradeResult: messages.CompileUpgradeResult;
try {
compileUpgradeResult = await compileDatabaseUpgrade(qs, upgradeParams);
}
catch (e) {
helpers.showAndLogErrorMessage(`Compilation of database upgrades failed: ${e}`);
return;
}
finally {
qs.logger.log('Done compiling database upgrade.')
}
if (compileUpgradeResult.compiledUpgrades === undefined) {
const error = compileUpgradeResult.error || '[no error message available]';
helpers.showAndLogErrorMessage(`Compilation of database upgrades failed: ${error}`);
return;
}
try {
qs.logger.log('Running the following database upgrade:');
qs.logger.log(compileUpgradeResult.compiledUpgrades.scripts.map(s => s.description.description).join('\n'));
return await runDatabaseUpgrade(qs, db, compileUpgradeResult.compiledUpgrades);
}
catch (e) {
helpers.showAndLogErrorMessage(`Database upgrade failed: ${e}`);
return;
}
finally {
qs.logger.log('Done running database upgrade.')
}
}
async function checkDatabaseUpgrade(qs: qsClient.QueryServerClient, upgradeParams: messages.UpgradeParams):
Promise<messages.CheckUpgradeResult> {
return helpers.withProgress({
location: vscode.ProgressLocation.Notification,
title: "Checking for database upgrades",
cancellable: true,
}, (progress, token) => qs.sendRequest(messages.checkUpgrade, upgradeParams, token, progress));
}
async function compileDatabaseUpgrade(qs: qsClient.QueryServerClient, upgradeParams: messages.UpgradeParams):
Promise<messages.CompileUpgradeResult> {
const params: messages.CompileUpgradeParams = {
upgrade: upgradeParams,
upgradeTempDir: upgradesTmpDir.name
}
return helpers.withProgress({
location: vscode.ProgressLocation.Notification,
title: "Compiling database upgrades",
cancellable: true,
}, (progress, token) => qs.sendRequest(messages.compileUpgrade, params, token, progress));
}
async function runDatabaseUpgrade(qs: qsClient.QueryServerClient, db: DatabaseItem, upgrades: messages.CompiledUpgrades):
Promise<messages.RunUpgradeResult> {
if (db.contents === undefined || db.contents.datasetUri === undefined) {
throw new Error('Can\'t upgrade an invalid database.');
}
const database: messages.Dataset = {
dbDir: db.contents.datasetUri.fsPath,
workingSet: 'default'
};
const params: messages.RunUpgradeParams = {
db: database,
timeoutSecs: qs.config.timeoutSecs,
toRun: upgrades
};
return helpers.withProgress({
location: vscode.ProgressLocation.Notification,
title: "Running database upgrades",
cancellable: true,
}, (progress, token) => qs.sendRequest(messages.runUpgrade, params, token, progress));
}

View File

@@ -2,10 +2,11 @@ import * as path from 'path';
import * as React from 'react';
import * as Sarif from 'sarif';
import * as Keys from '../result-keys';
import { LocationStyle, ResolvableLocationValue } from 'semmle-bqrs';
import { LocationStyle } from 'semmle-bqrs';
import * as octicons from './octicons';
import { className, renderLocation, ResultTableProps, zebraStripe, selectableZebraStripe, jumpToLocation } from './result-table-utils';
import { PathTableResultSet, onNavigation, NavigationEvent } from './results';
import { parseSarifPlainTextMessage, parseSarifLocation } from '../sarif-utils';
export type PathTableProps = ResultTableProps & { resultSet: PathTableResultSet };
export interface PathTableState {
@@ -13,64 +14,6 @@ export interface PathTableState {
selectedPathNode: undefined | Keys.PathNode;
}
interface SarifLink {
dest: number
text: string
}
type ParsedSarifLocation =
| ResolvableLocationValue
// Resolvable locations have a `file` field, but it will sometimes include
// a source location prefix, which contains build-specific information the user
// doesn't really need to see. We ensure that `userVisibleFile` will not contain
// that, and is appropriate for display in the UI.
& { userVisibleFile: string }
| { t: 'NoLocation', hint: string };
type SarifMessageComponent = string | SarifLink
/**
* Unescape "[", "]" and "\\" like in sarif plain text messages
*/
function unescapeSarifText(message: string): string {
return message.replace(/\\\[/g, "[").replace(/\\\]/g, "]").replace(/\\\\/, "\\");
}
function parseSarifPlainTextMessage(message: string): SarifMessageComponent[] {
let results: SarifMessageComponent[] = [];
// We want something like "[linkText](4)", except that "[" and "]" may be escaped. The lookbehind asserts
// that the initial [ is not escaped. Then we parse a link text with "[" and "]" escaped. Then we parse the numerical target.
// Technically we could have any uri in the target but we don't output that yet.
// The possibility of escaping outside the link is not mentioned in the sarif spec but we always output sartif this way.
const linkRegex = /(?<=(?<!\\)(\\\\)*)\[(?<linkText>([^\\\]\[]|\\\\|\\\]|\\\[)*)\]\((?<linkTarget>[0-9]+)\)/g;
let result: RegExpExecArray | null;
let curIndex = 0;
while ((result = linkRegex.exec(message)) !== null) {
results.push(unescapeSarifText(message.substring(curIndex, result.index)));
const linkText = result.groups!["linkText"];
const linkTarget = +result.groups!["linkTarget"];
results.push({ dest: linkTarget, text: unescapeSarifText(linkText) });
curIndex = result.index + result[0].length;
}
results.push(unescapeSarifText(message.substring(curIndex, message.length)));
return results;
}
/**
* Computes a path normalized to reflect conventional normalization
* of windows paths into zip archive paths.
* @param sourceLocationPrefix The source location prefix of a database. May be
* unix style `/foo/bar/baz` or windows-style `C:\foo\bar\baz`.
* @param sarifRelativeUri A uri relative to sourceLocationPrefix.
* @returns A string that is valid for the `.file` field of a `FivePartLocation`:
* directory separators are normalized, but drive letters `C:` may appear.
*/
export function getPathRelativeToSourceLocationPrefix(sourceLocationPrefix: string, sarifRelativeUui: string) {
const normalizedSourceLocationPrefix = sourceLocationPrefix.replace(/\\/g, '/');
return path.join(normalizedSourceLocationPrefix, decodeURIComponent(sarifRelativeUui));
}
export class PathTable extends React.Component<PathTableProps, PathTableState> {
constructor(props: PathTableProps) {
super(props);
@@ -323,64 +266,3 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
onNavigation.removeListener(this.handleNavigationEvent);
}
}
function parseSarifLocation(loc: Sarif.Location, sourceLocationPrefix: string): ParsedSarifLocation {
const physicalLocation = loc.physicalLocation;
if (physicalLocation === undefined)
return { t: 'NoLocation', hint: 'no physical location' };
if (physicalLocation.artifactLocation === undefined)
return { t: 'NoLocation', hint: 'no artifact location' };
if (physicalLocation.artifactLocation.uri === undefined)
return { t: 'NoLocation', hint: 'artifact location has no uri' };
// This is not necessarily really an absolute uri; it could either be a
// file uri or a relative uri.
const uri = physicalLocation.artifactLocation.uri;
const fileUriRegex = /^file:/;
const effectiveLocation = uri.match(fileUriRegex) ?
decodeURIComponent(uri.replace(fileUriRegex, '')) :
getPathRelativeToSourceLocationPrefix(sourceLocationPrefix, uri);
const userVisibleFile = uri.match(fileUriRegex) ?
decodeURIComponent(uri.replace(fileUriRegex, '')) :
uri;
if (physicalLocation.region === undefined) {
// If the region property is absent, the physicalLocation object refers to the entire file.
// Source: https://docs.oasis-open.org/sarif/sarif/v2.1.0/cs01/sarif-v2.1.0-cs01.html#_Toc16012638.
// TODO: Do we get here if we provide a non-filesystem URL?
return {
t: LocationStyle.WholeFile,
file: effectiveLocation,
userVisibleFile,
};
} else {
const region = physicalLocation.region;
// We assume that the SARIF we're given always has startLine
// This is not mandated by the SARIF spec, but should be true of
// SARIF output by our own tools.
const lineStart = region.startLine!;
// These defaults are from SARIF 2.1.0 spec, section 3.30.2, "Text Regions"
// https://docs.oasis-open.org/sarif/sarif/v2.1.0/cs01/sarif-v2.1.0-cs01.html#_Ref493492556
const lineEnd = region.endLine === undefined ? lineStart : region.endLine;
const colStart = region.startColumn === undefined ? 1 : region.startColumn;
// We also assume that our tools will always supply `endColumn` field, which is
// fortunate, since the SARIF spec says that it defaults to the end of the line, whose
// length we don't know at this point in the code.
//
// It is off by one with respect to the way vscode counts columns in selections.
const colEnd = region.endColumn! - 1;
return {
t: LocationStyle.FivePart,
file: effectiveLocation,
userVisibleFile,
lineStart,
colStart,
lineEnd,
colEnd,
};
}
}

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, ResultsPaths } 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,9 @@ export interface ResultTablesProps {
rawResultSets: readonly ResultSet[];
interpretation: Interpretation | undefined;
database: DatabaseInfo;
resultsPath: string | undefined;
kind: string | undefined;
metadata? : QueryMetadata
resultsPath: string ;
origResultsPaths: ResultsPaths;
sortStates: Map<string, SortState>;
isLoadingNewResults: boolean;
}
@@ -95,7 +96,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, origResultsPaths } = this.props;
// Only show the Problems view display checkbox for the alerts table.
const diagnosticsCheckBox = selectedTable === ALERTS_TABLE_NAME ?
@@ -104,10 +105,10 @@ export class ResultTables
if (resultsPath !== undefined) {
vscode.postMessage({
t: 'toggleDiagnostics',
resultsPath: resultsPath,
origResultsPaths: origResultsPaths,
databaseUri: database.databaseUri,
visible: e.target.checked,
kind: kind
metadata: metadata
});
}
}} />
@@ -157,11 +158,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, ResultsPaths } from '../interface-types';
import { ResultTables } from './result-tables';
import { EventHandlers as EventHandlerList } from './event-handler-list';
@@ -127,7 +127,7 @@ async function parseResultSets(response: Response): Promise<readonly ResultSet[]
interface ResultsInfo {
resultsPath: string;
kind: string | undefined;
origResultsPaths: ResultsPaths;
database: DatabaseInfo;
interpretation: Interpretation | undefined;
sortedResultsMap: Map<string, SortedResultSetInfo>;
@@ -135,6 +135,7 @@ interface ResultsInfo {
* See {@link SetStateMsg.shouldKeepOldResultsWhileRendering}.
*/
shouldKeepOldResultsWhileRendering: boolean;
metadata?: QueryMetadata
}
interface Results {
@@ -186,11 +187,12 @@ class App extends React.Component<{}, ResultsViewState> {
case 'setState':
this.updateStateWithNewResultsInfo({
resultsPath: msg.resultsPath,
kind: msg.kind,
origResultsPaths: msg.origResultsPaths,
sortedResultsMap: new Map(Object.entries(msg.sortedResultsMap)),
database: msg.database,
interpretation: msg.interpretation,
shouldKeepOldResultsWhileRendering: msg.shouldKeepOldResultsWhileRendering
shouldKeepOldResultsWhileRendering: msg.shouldKeepOldResultsWhileRendering,
metadata: msg.metadata
});
this.loadResults();
@@ -304,12 +306,13 @@ class App extends React.Component<{}, ResultsViewState> {
render() {
const displayedResults = this.state.displayedResults;
if (displayedResults.results !== null) {
if (displayedResults.results !== null && displayedResults.resultsInfo !== null) {
return <ResultTables rawResultSets={displayedResults.results.resultSets}
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}
origResultsPaths={displayedResults.resultsInfo.origResultsPaths}
resultsPath={displayedResults.resultsInfo.resultsPath}
metadata={displayedResults.resultsInfo ? displayedResults.resultsInfo.metadata : undefined}
sortStates={displayedResults.results.sortStates}
isLoadingNewResults={this.state.isExpectingResultsUpdate || this.state.nextResultsInfo !== null} />;
}

View File

@@ -0,0 +1,39 @@
import 'mocha';
import { expect } from "chai";
import { parseSarifPlainTextMessage } from '../../sarif-utils';
describe('parsing sarif', () => {
it('should be able to parse a simple message from the spec', async function() {
const message = "Tainted data was used. The data came from [here](3)."
const results = parseSarifPlainTextMessage(message);
expect(results).to.deep.equal([
"Tainted data was used. The data came from ",
{ dest: 3, text: "here" }, "."
]);
});
it('should be able to parse a complex message from the spec', async function() {
const message = "Prohibited term used in [para\\[0\\]\\\\spans\\[2\\]](1)."
const results = parseSarifPlainTextMessage(message);
expect(results).to.deep.equal([
"Prohibited term used in ",
{ dest: 1, text: "para[0]\\spans[2]" }, "."
]);
});
it('should be able to parse a broken complex message from the spec', async function() {
const message = "Prohibited term used in [para\\[0\\]\\\\spans\\[2\\](1)."
const results = parseSarifPlainTextMessage(message);
expect(results).to.deep.equal([
"Prohibited term used in [para[0]\\spans[2](1)."
]);
});
it('should be able to parse a message with extra escaping the spec', async function() {
const message = "Tainted data was used. The data came from \\[here](3)."
const results = parseSarifPlainTextMessage(message);
expect(results).to.deep.equal([
"Tainted data was used. The data came from [here](3)."
]);
});
});