More work on diffs
This commit is contained in:
Родитель
e9fbd6d430
Коммит
fceea64a08
|
@ -5,7 +5,7 @@ export const config: webpack.Configuration = {
|
|||
mode: 'development',
|
||||
entry: {
|
||||
resultsView: './src/view/results.tsx',
|
||||
compareView: './src/compare/view/compare.tsx',
|
||||
compareView: './src/compare/view/Compare.tsx',
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, '..', 'out'),
|
||||
|
|
|
@ -1,15 +1,30 @@
|
|||
import { DisposableObject } from "semmle-vscode-utils";
|
||||
import { WebviewPanel, ExtensionContext, window as Window, ViewColumn, Uri } from "vscode";
|
||||
import * as path from 'path';
|
||||
import {
|
||||
WebviewPanel,
|
||||
ExtensionContext,
|
||||
window as Window,
|
||||
ViewColumn,
|
||||
Uri,
|
||||
} from "vscode";
|
||||
import * as path from "path";
|
||||
|
||||
import { tmpDir } from "../run-queries";
|
||||
import { CompletedQuery } from "../query-results";
|
||||
import { CompareViewMessage } from "../interface-types";
|
||||
import {
|
||||
FromCompareViewMessage,
|
||||
ToCompareViewMessage,
|
||||
QueryCompareResult,
|
||||
} from "../interface-types";
|
||||
import { Logger } from "../logging";
|
||||
import { CodeQLCliServer } from "../cli";
|
||||
import { DatabaseManager } from "../databases";
|
||||
import { getHtmlForWebview, WebviewReveal } from "../webview-utils";
|
||||
import { showAndLogErrorMessage } from "../helpers";
|
||||
import {
|
||||
getHtmlForWebview,
|
||||
jumpToLocation,
|
||||
} from "../webview-utils";
|
||||
import { adaptSchema, adaptBqrs, RawResultSet } from "../adapt";
|
||||
import { BQRSInfo } from "../bqrs-cli-types";
|
||||
import resultsDiff from "./resultsDiff";
|
||||
|
||||
interface ComparePair {
|
||||
from: CompletedQuery;
|
||||
|
@ -19,6 +34,8 @@ interface ComparePair {
|
|||
export class CompareInterfaceManager extends DisposableObject {
|
||||
private comparePair: ComparePair | undefined;
|
||||
private panel: WebviewPanel | undefined;
|
||||
private panelLoaded = false;
|
||||
private panelLoadedCallBacks: (() => void)[] = [];
|
||||
|
||||
constructor(
|
||||
public ctx: ExtensionContext,
|
||||
|
@ -29,10 +46,49 @@ export class CompareInterfaceManager extends DisposableObject {
|
|||
super();
|
||||
}
|
||||
|
||||
showResults(from: CompletedQuery, to: CompletedQuery, forceReveal = WebviewReveal.NotForced) {
|
||||
async showResults(
|
||||
from: CompletedQuery,
|
||||
to: CompletedQuery,
|
||||
selectedResultSetName?: string
|
||||
) {
|
||||
this.comparePair = { from, to };
|
||||
if (forceReveal === WebviewReveal.Forced) {
|
||||
this.getPanel().reveal(undefined, true);
|
||||
this.getPanel().reveal(undefined, true);
|
||||
|
||||
await this.waitForPanelLoaded();
|
||||
const [
|
||||
commonResultSetNames,
|
||||
currentResultSetName,
|
||||
fromResultSet,
|
||||
toResultSet,
|
||||
] = await this.findCommonResultSetNames(from, to, selectedResultSetName);
|
||||
if (currentResultSetName) {
|
||||
await this.postMessage({
|
||||
t: "setComparisons",
|
||||
stats: {
|
||||
fromQuery: {
|
||||
// since we split the description into several rows
|
||||
// only run interpolation if the label is user-defined
|
||||
// otherwise we will wind up with duplicated rows
|
||||
name: from.options.label
|
||||
? from.interpolate(from.getLabel())
|
||||
: from.queryName,
|
||||
status: from.statusString,
|
||||
time: from.time,
|
||||
},
|
||||
toQuery: {
|
||||
name: to.options.label
|
||||
? to.interpolate(from.getLabel())
|
||||
: to.queryName,
|
||||
status: to.statusString,
|
||||
time: to.time,
|
||||
},
|
||||
},
|
||||
columns: fromResultSet.schema.columns,
|
||||
commonResultSetNames,
|
||||
currentResultSetName: currentResultSetName,
|
||||
rows: this.compareResults(fromResultSet, toResultSet),
|
||||
datebaseUri: to.database.databaseUri,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -40,9 +96,9 @@ export class CompareInterfaceManager extends DisposableObject {
|
|||
if (this.panel == undefined) {
|
||||
const { ctx } = this;
|
||||
const panel = (this.panel = Window.createWebviewPanel(
|
||||
"compareView", // internal name
|
||||
"Compare CodeQL Query Results", // user-visible name
|
||||
{ viewColumn: ViewColumn.Beside, preserveFocus: true },
|
||||
"compareView",
|
||||
"Compare CodeQL Query Results",
|
||||
{ viewColumn: ViewColumn.Active, preserveFocus: true },
|
||||
{
|
||||
enableScripts: true,
|
||||
enableFindWidget: true,
|
||||
|
@ -54,7 +110,10 @@ export class CompareInterfaceManager extends DisposableObject {
|
|||
}
|
||||
));
|
||||
this.panel.onDidDispose(
|
||||
() => this.panel = undefined,
|
||||
() => {
|
||||
this.panel = undefined;
|
||||
this.comparePair = undefined;
|
||||
},
|
||||
null,
|
||||
ctx.subscriptions
|
||||
);
|
||||
|
@ -64,10 +123,14 @@ export class CompareInterfaceManager extends DisposableObject {
|
|||
);
|
||||
|
||||
const stylesheetPathOnDisk = Uri.file(
|
||||
ctx.asAbsolutePath("out/compareView.css")
|
||||
ctx.asAbsolutePath("out/resultsView.css")
|
||||
);
|
||||
|
||||
panel.webview.html = getHtmlForWebview(panel.webview, scriptPathOnDisk, stylesheetPathOnDisk);
|
||||
panel.webview.html = getHtmlForWebview(
|
||||
panel.webview,
|
||||
scriptPathOnDisk,
|
||||
stylesheetPathOnDisk
|
||||
);
|
||||
panel.webview.onDidReceiveMessage(
|
||||
async (e) => this.handleMsgFromView(e),
|
||||
undefined,
|
||||
|
@ -77,9 +140,103 @@ export class CompareInterfaceManager extends DisposableObject {
|
|||
return this.panel;
|
||||
}
|
||||
|
||||
private async handleMsgFromView(msg: CompareViewMessage): Promise<void> {
|
||||
/** TODO */
|
||||
showAndLogErrorMessage(JSON.stringify(msg));
|
||||
showAndLogErrorMessage(JSON.stringify(this.comparePair));
|
||||
private waitForPanelLoaded(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
if (this.panelLoaded) {
|
||||
resolve();
|
||||
} else {
|
||||
this.panelLoadedCallBacks.push(resolve);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async handleMsgFromView(msg: FromCompareViewMessage): Promise<void> {
|
||||
switch (msg.t) {
|
||||
case "compareViewLoaded":
|
||||
this.panelLoaded = true;
|
||||
this.panelLoadedCallBacks.forEach((cb) => cb());
|
||||
this.panelLoadedCallBacks = [];
|
||||
break;
|
||||
|
||||
case "changeCompare":
|
||||
this.changeTable(msg.newResultSetName);
|
||||
break;
|
||||
|
||||
case "viewSourceFile":
|
||||
await jumpToLocation(msg, this.databaseManager, this.logger);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private postMessage(msg: ToCompareViewMessage): Thenable<boolean> {
|
||||
return this.getPanel().webview.postMessage(msg);
|
||||
}
|
||||
|
||||
private async findCommonResultSetNames(
|
||||
from: CompletedQuery,
|
||||
to: CompletedQuery,
|
||||
selectedResultSetName: string | undefined
|
||||
): Promise<[string[], string, RawResultSet, RawResultSet]> {
|
||||
const fromSchemas = await this.cliServer.bqrsInfo(
|
||||
from.query.resultsPaths.resultsPath
|
||||
);
|
||||
const toSchemas = await this.cliServer.bqrsInfo(
|
||||
to.query.resultsPaths.resultsPath
|
||||
);
|
||||
const fromSchemaNames = fromSchemas["result-sets"].map(
|
||||
(schema) => schema.name
|
||||
);
|
||||
const toSchemaNames = toSchemas["result-sets"].map((schema) => schema.name);
|
||||
const commonResultSetNames = fromSchemaNames.filter((name) =>
|
||||
toSchemaNames.includes(name)
|
||||
);
|
||||
const currentResultSetName = selectedResultSetName || commonResultSetNames[0];
|
||||
const fromResultSet = await this.getResultSet(
|
||||
fromSchemas,
|
||||
currentResultSetName,
|
||||
from.query.resultsPaths.resultsPath
|
||||
);
|
||||
const toResultSet = await this.getResultSet(
|
||||
toSchemas,
|
||||
currentResultSetName,
|
||||
to.query.resultsPaths.resultsPath
|
||||
);
|
||||
return [
|
||||
commonResultSetNames,
|
||||
currentResultSetName,
|
||||
fromResultSet,
|
||||
toResultSet,
|
||||
];
|
||||
}
|
||||
|
||||
private async changeTable(newResultSetName: string) {
|
||||
if (!this.comparePair?.from || !this.comparePair.to) {
|
||||
return;
|
||||
}
|
||||
await this.showResults(this.comparePair.from, this.comparePair.to, newResultSetName);
|
||||
}
|
||||
|
||||
private async getResultSet(
|
||||
bqrsInfo: BQRSInfo,
|
||||
resultSetName: string,
|
||||
resultsPath: string
|
||||
): Promise<RawResultSet> {
|
||||
const schema = bqrsInfo["result-sets"].find(
|
||||
(schema) => schema.name === resultSetName
|
||||
);
|
||||
if (!schema) {
|
||||
throw new Error(`Schema ${resultSetName} not found.`);
|
||||
}
|
||||
const chunk = await this.cliServer.bqrsDecode(resultsPath, resultSetName);
|
||||
const adaptedSchema = adaptSchema(schema);
|
||||
return adaptBqrs(adaptedSchema, chunk);
|
||||
}
|
||||
|
||||
private compareResults(
|
||||
fromResults: RawResultSet,
|
||||
toResults: RawResultSet
|
||||
): QueryCompareResult {
|
||||
// Only compare columns that have the same name
|
||||
return resultsDiff(fromResults, toResults);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
import { RawResultSet } from "../adapt";
|
||||
import { QueryCompareResult } from "../interface-types";
|
||||
|
||||
/**
|
||||
* Compare the rows of two queries. Use deep equality to determine if
|
||||
* rows have been added or removed across two invocations of a query.
|
||||
*
|
||||
* Assumptions:
|
||||
*
|
||||
* 1. Queries have the same sort order
|
||||
* 2. Queries have same number and order of columns
|
||||
* 3. Rows are not changed or re-ordered, they are only added or removed
|
||||
*
|
||||
* @param fromResults the source query
|
||||
* @param toResults the target query
|
||||
*
|
||||
* @throws Error when:
|
||||
* 1. number of columns do not match
|
||||
* 2. If either query is empty
|
||||
* 3. If the queries are 100% disjoint
|
||||
*/
|
||||
export default function resultsDiff(
|
||||
fromResults: RawResultSet,
|
||||
toResults: RawResultSet
|
||||
): QueryCompareResult {
|
||||
|
||||
if (fromResults.schema.columns.length !== toResults.schema.columns.length) {
|
||||
throw new Error("CodeQL Compare: Columns do not match.");
|
||||
}
|
||||
|
||||
if (!fromResults.rows.length) {
|
||||
throw new Error("CodeQL Compare: Source query has no results.");
|
||||
}
|
||||
|
||||
if (!toResults.rows.length) {
|
||||
throw new Error("CodeQL Compare: Target query has no results.");
|
||||
}
|
||||
|
||||
const results = {
|
||||
from: arrayDiff(fromResults.rows, toResults.rows),
|
||||
to: arrayDiff(toResults.rows, fromResults.rows),
|
||||
};
|
||||
|
||||
if (
|
||||
fromResults.rows.length === results.from.length &&
|
||||
toResults.rows.length === results.to.length
|
||||
) {
|
||||
throw new Error("CodeQL Compare: No overlap between the selected queries.");
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
function arrayDiff<T>(source: readonly T[], toRemove: readonly T[]): T[] {
|
||||
// Stringify the object so that we can compare hashes in the set
|
||||
const rest = new Set(toRemove.map((item) => JSON.stringify(item)));
|
||||
return source.filter((element) => !rest.has(JSON.stringify(element)));
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
import * as React from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import * as Rdom from "react-dom";
|
||||
|
||||
import RawTableHeader from "../../view/RawTableHeader";
|
||||
import {
|
||||
ToCompareViewMessage,
|
||||
SetComparisonsMessage,
|
||||
} from "../../interface-types";
|
||||
import CompareSelector from "./CompareSelector";
|
||||
import { vscode } from "../../view/vscode-api";
|
||||
import RawTableRow from "../../view/RawTableRow";
|
||||
import { ResultRow } from "../../adapt";
|
||||
import { className } from "../../view/result-table-utils";
|
||||
|
||||
const emptyComparison: SetComparisonsMessage = {
|
||||
t: "setComparisons",
|
||||
stats: {},
|
||||
rows: {
|
||||
from: [],
|
||||
to: [],
|
||||
},
|
||||
columns: [],
|
||||
commonResultSetNames: [],
|
||||
currentResultSetName: "",
|
||||
datebaseUri: "",
|
||||
};
|
||||
|
||||
export function Compare(props: {}): JSX.Element {
|
||||
const [comparison, setComparison] = useState<SetComparisonsMessage>(
|
||||
emptyComparison
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("message", (evt: MessageEvent) => {
|
||||
const msg: ToCompareViewMessage = evt.data;
|
||||
switch (msg.t) {
|
||||
case "setComparisons":
|
||||
setComparison(msg);
|
||||
}
|
||||
});
|
||||
});
|
||||
if (!comparison) {
|
||||
return <div>Waiting for results to load.</div>;
|
||||
}
|
||||
|
||||
try {
|
||||
return (
|
||||
<>
|
||||
<div className="vscode-codeql__compare-header">
|
||||
<div>Table to compare:</div>
|
||||
<CompareSelector
|
||||
availableResultSets={comparison.commonResultSetNames}
|
||||
currentResultSetName={comparison.currentResultSetName}
|
||||
updateResultSet={(newResultSetName: string) =>
|
||||
vscode.postMessage({ t: "changeCompare", newResultSetName })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<table className="vscode-codeql__compare-body">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>{comparison.stats.fromQuery?.name}</td>
|
||||
<td>{comparison.stats.toQuery?.name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{comparison.stats.fromQuery?.time}</td>
|
||||
<td>{comparison.stats.toQuery?.time}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{comparison.rows.from.length} rows removed</th>
|
||||
<th>{comparison.rows.to.length} rows added</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<table className={className}>
|
||||
<RawTableHeader
|
||||
columns={comparison.columns}
|
||||
schemaName={comparison.currentResultSetName}
|
||||
preventSort={true}
|
||||
/>
|
||||
{createRows(comparison.rows.from, comparison.datebaseUri)}
|
||||
</table>
|
||||
</td>
|
||||
<td>
|
||||
<table className={className}>
|
||||
<RawTableHeader
|
||||
columns={comparison.columns}
|
||||
schemaName={comparison.currentResultSetName}
|
||||
preventSort={true}
|
||||
/>
|
||||
{createRows(comparison.rows.to, comparison.datebaseUri)}
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return <div>Error!</div>;
|
||||
}
|
||||
}
|
||||
|
||||
function createRows(rows: ResultRow[], databaseUri: string) {
|
||||
return (
|
||||
<tbody>
|
||||
{rows.map((row, rowIndex) => (
|
||||
<RawTableRow
|
||||
key={rowIndex}
|
||||
rowIndex={rowIndex}
|
||||
row={row}
|
||||
databaseUri={databaseUri}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
);
|
||||
}
|
||||
|
||||
Rdom.render(
|
||||
<Compare />,
|
||||
document.getElementById("root"),
|
||||
// Post a message to the extension when fully loaded.
|
||||
() => vscode.postMessage({ t: "compareViewLoaded" })
|
||||
);
|
|
@ -0,0 +1,22 @@
|
|||
import * as React from "react";
|
||||
|
||||
interface Props {
|
||||
availableResultSets: string[];
|
||||
currentResultSetName: string;
|
||||
updateResultSet: (newResultSet: string) => void;
|
||||
}
|
||||
|
||||
export default function CompareSelector(props: Props) {
|
||||
return (
|
||||
<select
|
||||
value={props.currentResultSetName}
|
||||
onChange={(e) => props.updateResultSet(e.target.value)}
|
||||
>
|
||||
{props.availableResultSets.map((resultSet) => (
|
||||
<option key={resultSet} value={resultSet}>
|
||||
{resultSet}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import * as Rdom from 'react-dom';
|
||||
|
||||
interface Props {
|
||||
/**/
|
||||
}
|
||||
|
||||
export function App(props: Props): JSX.Element {
|
||||
return (
|
||||
<div>Compare View!</div>
|
||||
);
|
||||
}
|
||||
|
||||
Rdom.render(
|
||||
<App />,
|
||||
document.getElementById('root')
|
||||
);
|
|
@ -1,8 +0,0 @@
|
|||
.octicon {
|
||||
fill: var(--vscode-editor-foreground);
|
||||
margin-top: .25em;
|
||||
}
|
||||
|
||||
.octicon-light {
|
||||
opacity: 0.6;
|
||||
}
|
|
@ -10,14 +10,15 @@
|
|||
],
|
||||
"jsx": "react",
|
||||
"sourceMap": true,
|
||||
"rootDir": "..",
|
||||
"rootDir": "../..",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"experimentalDecorators": true
|
||||
"experimentalDecorators": true,
|
||||
"typeRoots" : ["./typings"]
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -306,7 +306,7 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu
|
|||
ctx,
|
||||
queryHistoryConfigurationListener,
|
||||
async item => showResultsForCompletedQuery(item, WebviewReveal.Forced),
|
||||
async (from: CompletedQuery, to: CompletedQuery) => showResultsForComparison(from, to, WebviewReveal.Forced),
|
||||
async (from: CompletedQuery, to: CompletedQuery) => showResultsForComparison(from, to),
|
||||
);
|
||||
logger.log('Initializing results panel interface.');
|
||||
const intm = new InterfaceManager(ctx, dbm, cliServer, queryServerLogger);
|
||||
|
@ -319,8 +319,12 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu
|
|||
logger.log('Initializing source archive filesystem provider.');
|
||||
archiveFilesystemProvider.activate(ctx);
|
||||
|
||||
async function showResultsForComparison(from: CompletedQuery, to: CompletedQuery, forceReveal: WebviewReveal): Promise<void> {
|
||||
await cmpm.showResults(from, to, forceReveal);
|
||||
async function showResultsForComparison(from: CompletedQuery, to: CompletedQuery): Promise<void> {
|
||||
try {
|
||||
await cmpm.showResults(from, to);
|
||||
} catch (e) {
|
||||
helpers.showAndLogErrorMessage(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function showResultsForCompletedQuery(query: CompletedQuery, forceReveal: WebviewReveal): Promise<void> {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as sarif from 'sarif';
|
||||
import { ResolvableLocationValue } from 'semmle-bqrs';
|
||||
import { ParsedResultSets } from './adapt';
|
||||
import { ResolvableLocationValue, ColumnSchema } from 'semmle-bqrs';
|
||||
import { ResultRow, ParsedResultSets } from './adapt';
|
||||
|
||||
/**
|
||||
* Only ever show this many results per run in interpreted results.
|
||||
|
@ -110,7 +110,7 @@ export type FromResultsViewMsg =
|
|||
| ResultViewLoaded
|
||||
| ChangePage;
|
||||
|
||||
interface ViewSourceFileMsg {
|
||||
export interface ViewSourceFileMsg {
|
||||
t: 'viewSourceFile';
|
||||
loc: ResolvableLocationValue;
|
||||
databaseUri: string;
|
||||
|
@ -171,8 +171,60 @@ interface ChangeInterpretedResultsSortMsg {
|
|||
sortState?: InterpretedResultsSortState;
|
||||
}
|
||||
|
||||
export interface CompareViewMessage {
|
||||
t: 'change-compare';
|
||||
export type FromCompareViewMessage =
|
||||
| CompareViewLoadedMessage
|
||||
| ChangeCompareMessage
|
||||
| ViewSourceFileMsg;
|
||||
|
||||
interface CompareViewLoadedMessage {
|
||||
t: 'compareViewLoaded';
|
||||
}
|
||||
|
||||
interface ChangeCompareMessage {
|
||||
t: 'changeCompare';
|
||||
newResultSetName: string;
|
||||
// TODO do we need to include the ids of the queries
|
||||
}
|
||||
|
||||
export type ToCompareViewMessage = SetComparisonsMessage;
|
||||
|
||||
export interface SetComparisonsMessage {
|
||||
readonly t: "setComparisons";
|
||||
readonly stats: {
|
||||
fromQuery?: {
|
||||
name: string;
|
||||
status: string;
|
||||
time: string;
|
||||
};
|
||||
toQuery?: {
|
||||
name: string;
|
||||
status: string;
|
||||
time: string;
|
||||
};
|
||||
};
|
||||
readonly columns: readonly ColumnSchema[];
|
||||
readonly commonResultSetNames: string[];
|
||||
readonly currentResultSetName: string;
|
||||
readonly rows: QueryCompareResult;
|
||||
readonly datebaseUri: string;
|
||||
}
|
||||
|
||||
export enum DiffKind {
|
||||
Add = 'Add',
|
||||
Remove = 'Remove',
|
||||
Change = 'Change'
|
||||
}
|
||||
|
||||
/**
|
||||
* from is the set of rows that have changes in the "from" query.
|
||||
* to is the set of rows that have changes in the "to" query.
|
||||
* They are in the same order, so element 1 in "from" corresponds to
|
||||
* element 1 in "to".
|
||||
*
|
||||
* If an array element is null, that means that the element was removed
|
||||
* (or added) in the comparison.
|
||||
*/
|
||||
export type QueryCompareResult = {
|
||||
from: ResultRow[];
|
||||
to: ResultRow[];
|
||||
};
|
||||
|
|
|
@ -2,21 +2,31 @@ import { RawResultSet } from './adapt';
|
|||
import { ResultSetSchema } from 'semmle-bqrs';
|
||||
import { Interpretation } from './interface-types';
|
||||
|
||||
export const SELECT_TABLE_NAME = '#select';
|
||||
export const ALERTS_TABLE_NAME = 'alerts';
|
||||
export const SELECT_TABLE_NAME = "#select";
|
||||
export const ALERTS_TABLE_NAME = "alerts";
|
||||
|
||||
export type RawTableResultSet = { t: 'RawResultSet' } & RawResultSet;
|
||||
export type PathTableResultSet = { t: 'SarifResultSet'; readonly schema: ResultSetSchema; name: string } & Interpretation;
|
||||
export type RawTableResultSet = { t: "RawResultSet" } & RawResultSet;
|
||||
export type PathTableResultSet = {
|
||||
t: "SarifResultSet";
|
||||
readonly schema: ResultSetSchema;
|
||||
name: string;
|
||||
} & Interpretation;
|
||||
|
||||
export type ResultSet =
|
||||
| RawTableResultSet
|
||||
| PathTableResultSet;
|
||||
export type ResultSet = RawTableResultSet | PathTableResultSet;
|
||||
|
||||
export function getDefaultResultSet(resultSets: readonly ResultSet[]): string {
|
||||
return getDefaultResultSetName(resultSets.map(resultSet => resultSet.schema.name));
|
||||
return getDefaultResultSetName(
|
||||
resultSets.map((resultSet) => resultSet.schema.name)
|
||||
);
|
||||
}
|
||||
|
||||
export function getDefaultResultSetName(resultSetNames: readonly string[]): string {
|
||||
export function getDefaultResultSetName(
|
||||
resultSetNames: readonly string[]
|
||||
): string {
|
||||
// Choose first available result set from the array
|
||||
return [ALERTS_TABLE_NAME, SELECT_TABLE_NAME, resultSetNames[0]].filter(resultSetName => resultSetNames.includes(resultSetName))[0];
|
||||
return [
|
||||
ALERTS_TABLE_NAME,
|
||||
SELECT_TABLE_NAME,
|
||||
resultSetNames[0],
|
||||
].filter((resultSetName) => resultSetNames.includes(resultSetName))[0];
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ import {
|
|||
getHtmlForWebview,
|
||||
shownLocationDecoration,
|
||||
shownLocationLineDecoration,
|
||||
showLocation,
|
||||
jumpToLocation,
|
||||
} from "./webview-utils";
|
||||
import { getDefaultResultSetName } from "./interface-utils";
|
||||
|
||||
|
@ -158,7 +158,10 @@ export class InterfaceManager extends DisposableObject {
|
|||
}
|
||||
));
|
||||
this._panel.onDidDispose(
|
||||
() => (this._panel = undefined),
|
||||
() => {
|
||||
this._panel = undefined;
|
||||
this._displayedQuery = undefined;
|
||||
},
|
||||
null,
|
||||
ctx.subscriptions
|
||||
);
|
||||
|
@ -199,27 +202,8 @@ export class InterfaceManager extends DisposableObject {
|
|||
|
||||
private async handleMsgFromView(msg: FromResultsViewMsg): Promise<void> {
|
||||
switch (msg.t) {
|
||||
case 'viewSourceFile': {
|
||||
const databaseItem = this.databaseManager.findDatabaseItem(
|
||||
Uri.parse(msg.databaseUri)
|
||||
);
|
||||
if (databaseItem !== undefined) {
|
||||
try {
|
||||
await showLocation(msg.loc, databaseItem);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
if (e.message.match(/File not found/)) {
|
||||
vscode.window.showErrorMessage(
|
||||
'Original file of this result is not in the database\'s source archive.'
|
||||
);
|
||||
} else {
|
||||
this.logger.log(`Unable to handleMsgFromView: ${e.message}`);
|
||||
}
|
||||
} else {
|
||||
this.logger.log(`Unable to handleMsgFromView: ${e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
case "viewSourceFile": {
|
||||
await jumpToLocation(msg, this.databaseManager, this.logger);
|
||||
break;
|
||||
}
|
||||
case 'toggleDiagnostics': {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { IndentAction, languages } from 'vscode';
|
||||
import { languages } from "vscode";
|
||||
|
||||
|
||||
/**
|
||||
|
@ -27,29 +27,29 @@ export function install() {
|
|||
languages.setLanguageConfiguration('dbscheme', langConfig);
|
||||
}
|
||||
|
||||
const onEnterRules = [
|
||||
{
|
||||
// e.g. /** | */
|
||||
beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/,
|
||||
afterText: /^\s*\*\/$/,
|
||||
action: { indentAction: IndentAction.IndentOutdent, appendText: ' * ' }
|
||||
}, {
|
||||
// e.g. /** ...|
|
||||
beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/,
|
||||
action: { indentAction: IndentAction.None, appendText: ' * ' }
|
||||
}, {
|
||||
// e.g. * ...|
|
||||
beforeText: /^(\t|[ ])*[ ]\*([ ]([^\*]|\*(?!\/))*)?$/,
|
||||
oneLineAboveText: /^(\s*(\/\*\*|\*)).*/,
|
||||
action: { indentAction: IndentAction.None, appendText: '* ' }
|
||||
}, {
|
||||
// e.g. */|
|
||||
beforeText: /^(\t|[ ])*[ ]\*\/\s*$/,
|
||||
action: { indentAction: IndentAction.None, removeText: 1 }
|
||||
},
|
||||
{
|
||||
// e.g. *-----*/|
|
||||
beforeText: /^(\t|[ ])*[ ]\*[^/]*\*\/\s*$/,
|
||||
action: { indentAction: IndentAction.None, removeText: 1 }
|
||||
}
|
||||
const onEnterRules: string[] = [
|
||||
// {
|
||||
// // e.g. /** | */
|
||||
// beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/,
|
||||
// afterText: /^\s*\*\/$/,
|
||||
// action: { indentAction: IndentAction.IndentOutdent, appendText: ' * ' }
|
||||
// }, {
|
||||
// // e.g. /** ...|
|
||||
// beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/,
|
||||
// action: { indentAction: IndentAction.None, appendText: ' * ' }
|
||||
// }, {
|
||||
// // e.g. * ...|
|
||||
// beforeText: /^(\t|[ ])*[ ]\*([ ]([^\*]|\*(?!\/))*)?$/,
|
||||
// oneLineAboveText: /^(\s*(\/\*\*|\*)).*/,
|
||||
// action: { indentAction: IndentAction.None, appendText: '* ' }
|
||||
// }, {
|
||||
// // e.g. */|
|
||||
// beforeText: /^(\t|[ ])*[ ]\*\/\s*$/,
|
||||
// action: { indentAction: IndentAction.None, removeText: 1 }
|
||||
// },
|
||||
// {
|
||||
// // e.g. *-----*/|
|
||||
// beforeText: /^(\t|[ ])*[ ]\*[^/]*\*\/\s*$/,
|
||||
// action: { indentAction: IndentAction.None, removeText: 1 }
|
||||
// }
|
||||
];
|
||||
|
|
|
@ -145,6 +145,10 @@ class HistoryTreeDataProvider implements vscode.TreeDataProvider<CompletedQuery>
|
|||
refresh() {
|
||||
this._onDidChangeTreeData.fire(undefined);
|
||||
}
|
||||
|
||||
find(queryId: number): CompletedQuery | undefined {
|
||||
return this.allHistory.find(query => query.query.queryID === queryId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -346,6 +350,10 @@ export class QueryHistoryManager {
|
|||
return item;
|
||||
}
|
||||
|
||||
find(queryId: number): CompletedQuery | undefined {
|
||||
return this.treeDataProvider.find(queryId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the tree view selection if the tree view is visible.
|
||||
*
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
import * as React from "react";
|
||||
|
||||
import { vscode } from "./vscode-api";
|
||||
import { RawResultsSortState, SortDirection } from "../interface-types";
|
||||
import { nextSortDirection } from "./result-table-utils";
|
||||
import { ColumnSchema } from "semmle-bqrs";
|
||||
|
||||
interface Props {
|
||||
readonly columns: readonly ColumnSchema[];
|
||||
readonly schemaName: string;
|
||||
readonly sortState?: RawResultsSortState;
|
||||
readonly preventSort?: boolean;
|
||||
}
|
||||
|
||||
function toggleSortStateForColumn(
|
||||
index: number,
|
||||
schemaName: string,
|
||||
sortState: RawResultsSortState | undefined,
|
||||
preventSort: boolean
|
||||
): void {
|
||||
if (preventSort) {
|
||||
return;
|
||||
}
|
||||
|
||||
const prevDirection =
|
||||
sortState && sortState.columnIndex === index
|
||||
? sortState.sortDirection
|
||||
: undefined;
|
||||
const nextDirection = nextSortDirection(prevDirection);
|
||||
const nextSortState =
|
||||
nextDirection === undefined
|
||||
? undefined
|
||||
: {
|
||||
columnIndex: index,
|
||||
sortDirection: nextDirection,
|
||||
};
|
||||
vscode.postMessage({
|
||||
t: "changeSort",
|
||||
resultSetName: schemaName,
|
||||
sortState: nextSortState,
|
||||
});
|
||||
}
|
||||
|
||||
export default function RawTableHeader(props: Props) {
|
||||
return (
|
||||
<thead>
|
||||
<tr>
|
||||
{[
|
||||
(
|
||||
<th key={-1}>
|
||||
<b>#</b>
|
||||
</th>
|
||||
),
|
||||
...props.columns.map((col, index) => {
|
||||
const displayName = col.name || `[${index}]`;
|
||||
const sortDirection =
|
||||
props.sortState && index === props.sortState.columnIndex
|
||||
? props.sortState.sortDirection
|
||||
: undefined;
|
||||
return (
|
||||
<th
|
||||
className={
|
||||
"sort-" +
|
||||
(sortDirection !== undefined
|
||||
? SortDirection[sortDirection]
|
||||
: "none")
|
||||
}
|
||||
key={index}
|
||||
onClick={() =>
|
||||
toggleSortStateForColumn(
|
||||
index,
|
||||
props.schemaName,
|
||||
props.sortState,
|
||||
!!props.preventSort
|
||||
)
|
||||
}
|
||||
>
|
||||
<b>{displayName}</b>
|
||||
</th>
|
||||
);
|
||||
}),
|
||||
]}
|
||||
</tr>
|
||||
</thead>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
import * as React from "react";
|
||||
import { ResultRow } from "../adapt";
|
||||
import { zebraStripe } from "./result-table-utils";
|
||||
import RawTableValue from "./RawTableValue";
|
||||
|
||||
interface Props {
|
||||
rowIndex: number;
|
||||
row: ResultRow;
|
||||
databaseUri: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function RawTableRow(props: Props) {
|
||||
return (
|
||||
<tr key={props.rowIndex} {...zebraStripe(props.rowIndex, props.className || '')}>
|
||||
<td key={-1}>{props.rowIndex + 1}</td>
|
||||
|
||||
{props.row.map((value, columnIndex) => (
|
||||
<td key={columnIndex}>
|
||||
<RawTableValue
|
||||
value={value}
|
||||
databaseUri={props.databaseUri}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import * as React from "react";
|
||||
|
||||
import { ResultValue } from "../adapt";
|
||||
import { renderLocation } from "./result-table-utils";
|
||||
|
||||
interface Props {
|
||||
value: ResultValue;
|
||||
databaseUri: string;
|
||||
}
|
||||
|
||||
export default function RawTableValue(props: Props): JSX.Element {
|
||||
const v = props.value;
|
||||
if (typeof v === 'string') {
|
||||
return <span>{v}</span>;
|
||||
}
|
||||
else if ('uri' in v) {
|
||||
return <a href={v.uri}>{v.uri}</a>;
|
||||
}
|
||||
else {
|
||||
return renderLocation(v.location, v.label, props.databaseUri);
|
||||
}
|
||||
}
|
|
@ -5,10 +5,11 @@ import * as Keys from '../result-keys';
|
|||
import { LocationStyle } from 'semmle-bqrs';
|
||||
import * as octicons from './octicons';
|
||||
import { className, renderLocation, ResultTableProps, zebraStripe, selectableZebraStripe, jumpToLocation, nextSortDirection } from './result-table-utils';
|
||||
import { onNavigation, NavigationEvent, vscode } from './results';
|
||||
import { onNavigation, NavigationEvent } from './results';
|
||||
import { PathTableResultSet } from '../interface-utils';
|
||||
import { parseSarifPlainTextMessage, parseSarifLocation } from '../sarif-utils';
|
||||
import { InterpretedResultsSortColumn, SortDirection, InterpretedResultsSortState } from '../interface-types';
|
||||
import { PathTableResultSet } from '../interface-utils';
|
||||
import { vscode } from './vscode-api';
|
||||
|
||||
export type PathTableProps = ResultTableProps & { resultSet: PathTableResultSet };
|
||||
export interface PathTableState {
|
||||
|
@ -188,11 +189,9 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
|
|||
|
||||
if (result.codeFlows === undefined) {
|
||||
rows.push(
|
||||
<tr {...zebraStripe(resultIndex)}>
|
||||
<tr key={resultIndex} {...zebraStripe(resultIndex)}>
|
||||
<td className="vscode-codeql__icon-cell">{octicons.info}</td>
|
||||
<td colSpan={3}>
|
||||
{msg}
|
||||
</td>
|
||||
<td colSpan={3}>{msg}</td>
|
||||
{locationCells}
|
||||
</tr>
|
||||
);
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import * as React from 'react';
|
||||
import { renderLocation, ResultTableProps, zebraStripe, className, nextSortDirection } from './result-table-utils';
|
||||
import { vscode } from './results';
|
||||
import { ResultValue } from '../adapt';
|
||||
import { SortDirection, RAW_RESULTS_LIMIT, RawResultsSortState } from '../interface-types';
|
||||
import { RawTableResultSet } from '../interface-utils';
|
||||
import * as React from "react";
|
||||
import { ResultTableProps, className } from "./result-table-utils";
|
||||
import { RAW_RESULTS_LIMIT, RawResultsSortState } from "../interface-types";
|
||||
import { RawTableResultSet } from "../interface-utils";
|
||||
import RawTableHeader from "./RawTableHeader";
|
||||
import RawTableRow from "./RawTableRow";
|
||||
|
||||
export type RawTableProps = ResultTableProps & {
|
||||
resultSet: RawTableResultSet;
|
||||
|
@ -19,7 +19,7 @@ export class RawTable extends React.Component<RawTableProps, {}> {
|
|||
render(): React.ReactNode {
|
||||
const { resultSet, databaseUri } = this.props;
|
||||
|
||||
let dataRows = this.props.resultSet.rows;
|
||||
let dataRows = resultSet.rows;
|
||||
let numTruncatedResults = 0;
|
||||
if (dataRows.length > RAW_RESULTS_LIMIT) {
|
||||
numTruncatedResults = dataRows.length - RAW_RESULTS_LIMIT;
|
||||
|
@ -27,19 +27,12 @@ export class RawTable extends React.Component<RawTableProps, {}> {
|
|||
}
|
||||
|
||||
const tableRows = dataRows.map((row, rowIndex) =>
|
||||
<tr key={rowIndex} {...zebraStripe(rowIndex)}>
|
||||
{
|
||||
[
|
||||
<td key={-1}>{rowIndex + 1 + this.props.offset}</td>,
|
||||
...row.map((value, columnIndex) =>
|
||||
<td key={columnIndex}>
|
||||
{
|
||||
renderTupleValue(value, databaseUri)
|
||||
}
|
||||
</td>)
|
||||
]
|
||||
}
|
||||
</tr>
|
||||
<RawTableRow
|
||||
key={rowIndex}
|
||||
rowIndex={rowIndex}
|
||||
row={row}
|
||||
databaseUri={databaseUri}
|
||||
/>
|
||||
);
|
||||
|
||||
if (numTruncatedResults > 0) {
|
||||
|
@ -50,53 +43,14 @@ export class RawTable extends React.Component<RawTableProps, {}> {
|
|||
}
|
||||
|
||||
return <table className={className}>
|
||||
<thead>
|
||||
<tr>
|
||||
{
|
||||
[
|
||||
<th key={-1}><b>#</b></th>,
|
||||
...resultSet.schema.columns.map((col, index) => {
|
||||
const displayName = col.name || `[${index}]`;
|
||||
const sortDirection = this.props.sortState && index === this.props.sortState.columnIndex ? this.props.sortState.sortDirection : undefined;
|
||||
return <th className={'sort-' + (sortDirection !== undefined ? SortDirection[sortDirection] : 'none')} key={index} onClick={() => this.toggleSortStateForColumn(index)}><b>{displayName}</b></th>;
|
||||
})
|
||||
]
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
<RawTableHeader
|
||||
columns={resultSet.schema.columns}
|
||||
schemaName={resultSet.schema.name}
|
||||
sortState={this.props.sortState}
|
||||
/>
|
||||
<tbody>
|
||||
{tableRows}
|
||||
</tbody>
|
||||
</table>;
|
||||
}
|
||||
|
||||
private toggleSortStateForColumn(index: number): void {
|
||||
const sortState = this.props.sortState;
|
||||
const prevDirection = sortState && sortState.columnIndex === index ? sortState.sortDirection : undefined;
|
||||
const nextDirection = nextSortDirection(prevDirection);
|
||||
const nextSortState = nextDirection === undefined ? undefined : {
|
||||
columnIndex: index,
|
||||
sortDirection: nextDirection
|
||||
};
|
||||
vscode.postMessage({
|
||||
t: 'changeSort',
|
||||
resultSetName: this.props.resultSet.schema.name,
|
||||
sortState: nextSortState
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render one column of a tuple.
|
||||
*/
|
||||
function renderTupleValue(v: ResultValue, databaseUri: string): JSX.Element {
|
||||
if (typeof v === 'string') {
|
||||
return <span>{v}</span>;
|
||||
}
|
||||
else if ('uri' in v) {
|
||||
return <a href={v.uri}>{v.uri}</a>;
|
||||
}
|
||||
else {
|
||||
return renderLocation(v.location, v.label, databaseUri);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import * as React from 'react';
|
||||
import { LocationValue, ResolvableLocationValue, tryGetResolvableLocation } from 'semmle-bqrs';
|
||||
import { RawResultsSortState, QueryMetadata, SortDirection } from '../interface-types';
|
||||
import { vscode } from './results';
|
||||
import { assertNever } from '../helpers-pure';
|
||||
import { ResultSet } from '../interface-utils';
|
||||
import { vscode } from './vscode-api';
|
||||
|
||||
export interface ResultTableProps {
|
||||
resultSet: ResultSet;
|
||||
|
|
|
@ -3,9 +3,9 @@ import { DatabaseInfo, Interpretation, RawResultsSortState, QueryMetadata, Resul
|
|||
import { PathTable } from './alert-table';
|
||||
import { RawTable } from './raw-results-table';
|
||||
import { ResultTableProps, tableSelectionHeaderClassName, toggleDiagnosticsClassName, alertExtrasClassName } from './result-table-utils';
|
||||
import { vscode } from './results';
|
||||
import { ParsedResultSets, ExtensionParsedResultSets } from '../adapt';
|
||||
import { ResultSet, ALERTS_TABLE_NAME, SELECT_TABLE_NAME, getDefaultResultSet } from '../interface-utils';
|
||||
import { vscode } from './vscode-api';
|
||||
|
||||
/**
|
||||
* Properties for the `ResultTables` component.
|
||||
|
|
|
@ -1,13 +1,32 @@
|
|||
import * as React from 'react';
|
||||
import * as Rdom from 'react-dom';
|
||||
import * as bqrs from 'semmle-bqrs';
|
||||
import { ElementBase, PrimitiveColumnValue, PrimitiveTypeKind, tryGetResolvableLocation } from 'semmle-bqrs';
|
||||
import { assertNever } from '../helpers-pure';
|
||||
import { DatabaseInfo, FromResultsViewMsg, Interpretation, IntoResultsViewMsg, SortedResultSetInfo, RawResultsSortState, NavigatePathMsg, QueryMetadata, ResultsPaths } from '../interface-types';
|
||||
import { EventHandlers as EventHandlerList } from './event-handler-list';
|
||||
import { ResultTables } from './result-tables';
|
||||
import { ResultValue, ResultRow, ParsedResultSets } from '../adapt';
|
||||
import { ResultSet } from '../interface-utils';
|
||||
import * as React from "react";
|
||||
import * as Rdom from "react-dom";
|
||||
import * as bqrs from "semmle-bqrs";
|
||||
import {
|
||||
ElementBase,
|
||||
PrimitiveColumnValue,
|
||||
PrimitiveTypeKind,
|
||||
tryGetResolvableLocation,
|
||||
} from "semmle-bqrs";
|
||||
import { assertNever } from "../helpers-pure";
|
||||
import {
|
||||
DatabaseInfo,
|
||||
Interpretation,
|
||||
IntoResultsViewMsg,
|
||||
SortedResultSetInfo,
|
||||
RawResultsSortState,
|
||||
NavigatePathMsg,
|
||||
QueryMetadata,
|
||||
ResultsPaths,
|
||||
} from "../interface-types";
|
||||
import { EventHandlers as EventHandlerList } from "./event-handler-list";
|
||||
import { ResultTables } from "./result-tables";
|
||||
import {
|
||||
ResultValue,
|
||||
ResultRow,
|
||||
ParsedResultSets,
|
||||
} from "../adapt";
|
||||
import { ResultSet } from "../interface-utils";
|
||||
import { vscode } from "./vscode-api";
|
||||
|
||||
/**
|
||||
* results.tsx
|
||||
|
@ -16,18 +35,13 @@ import { ResultSet } from '../interface-utils';
|
|||
* Displaying query results.
|
||||
*/
|
||||
|
||||
interface VsCodeApi {
|
||||
/**
|
||||
* Post message back to vscode extension.
|
||||
*/
|
||||
postMessage(msg: FromResultsViewMsg): void;
|
||||
}
|
||||
declare const acquireVsCodeApi: () => VsCodeApi;
|
||||
export const vscode = acquireVsCodeApi();
|
||||
|
||||
async function* getChunkIterator(response: Response): AsyncIterableIterator<Uint8Array> {
|
||||
async function* getChunkIterator(
|
||||
response: Response
|
||||
): AsyncIterableIterator<Uint8Array> {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load results: (${response.status}) ${response.statusText}`);
|
||||
throw new Error(
|
||||
`Failed to load results: (${response.status}) ${response.statusText}`
|
||||
);
|
||||
}
|
||||
const reader = response.body!.getReader();
|
||||
while (true) {
|
||||
|
@ -39,23 +53,28 @@ async function* getChunkIterator(response: Response): AsyncIterableIterator<Uint
|
|||
}
|
||||
}
|
||||
|
||||
function translatePrimitiveValue(value: PrimitiveColumnValue, type: PrimitiveTypeKind): ResultValue {
|
||||
function translatePrimitiveValue(
|
||||
value: PrimitiveColumnValue,
|
||||
type: PrimitiveTypeKind
|
||||
): ResultValue {
|
||||
switch (type) {
|
||||
case 'i':
|
||||
case 'f':
|
||||
case 's':
|
||||
case 'd':
|
||||
case 'b':
|
||||
case "i":
|
||||
case "f":
|
||||
case "s":
|
||||
case "d":
|
||||
case "b":
|
||||
return value.toString();
|
||||
|
||||
case 'u':
|
||||
case "u":
|
||||
return {
|
||||
uri: value as string
|
||||
uri: value as string,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function parseResultSets(response: Response): Promise<readonly ResultSet[]> {
|
||||
async function parseResultSets(
|
||||
response: Response
|
||||
): Promise<readonly ResultSet[]> {
|
||||
const chunks = getChunkIterator(response);
|
||||
|
||||
const resultSets: ResultSet[] = [];
|
||||
|
@ -64,32 +83,33 @@ async function parseResultSets(response: Response): Promise<readonly ResultSet[]
|
|||
const columnTypes = resultSetSchema.columns.map((column) => column.type);
|
||||
const rows: ResultRow[] = [];
|
||||
resultSets.push({
|
||||
t: 'RawResultSet',
|
||||
t: "RawResultSet",
|
||||
schema: resultSetSchema,
|
||||
rows: rows
|
||||
rows: rows,
|
||||
});
|
||||
|
||||
return (tuple) => {
|
||||
const row: ResultValue[] = [];
|
||||
tuple.forEach((value, index) => {
|
||||
const type = columnTypes[index];
|
||||
if (type.type === 'e') {
|
||||
if (type.type === "e") {
|
||||
const element: ElementBase = value as ElementBase;
|
||||
const label = (element.label !== undefined) ? element.label : element.id.toString(); //REVIEW: URLs?
|
||||
const label =
|
||||
element.label !== undefined ? element.label : element.id.toString(); //REVIEW: URLs?
|
||||
const resolvableLocation = tryGetResolvableLocation(element.location);
|
||||
if (resolvableLocation !== undefined) {
|
||||
row.push({
|
||||
label: label,
|
||||
location: resolvableLocation
|
||||
location: resolvableLocation,
|
||||
});
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// No location link.
|
||||
row.push(label);
|
||||
}
|
||||
}
|
||||
else {
|
||||
row.push(translatePrimitiveValue(value as PrimitiveColumnValue, type.type));
|
||||
} else {
|
||||
row.push(
|
||||
translatePrimitiveValue(value as PrimitiveColumnValue, type.type)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -151,16 +171,16 @@ class App extends React.Component<{}, ResultsViewState> {
|
|||
displayedResults: {
|
||||
resultsInfo: null,
|
||||
results: null,
|
||||
errorMessage: ''
|
||||
errorMessage: "",
|
||||
},
|
||||
nextResultsInfo: null,
|
||||
isExpectingResultsUpdate: true
|
||||
isExpectingResultsUpdate: true,
|
||||
};
|
||||
}
|
||||
|
||||
handleMessage(msg: IntoResultsViewMsg): void {
|
||||
switch (msg.t) {
|
||||
case 'setState':
|
||||
case "setState":
|
||||
this.updateStateWithNewResultsInfo({
|
||||
resultsPath: msg.resultsPath,
|
||||
parsedResultSets: msg.parsedResultSets,
|
||||
|
@ -168,18 +188,19 @@ class App extends React.Component<{}, ResultsViewState> {
|
|||
sortedResultsMap: new Map(Object.entries(msg.sortedResultsMap)),
|
||||
database: msg.database,
|
||||
interpretation: msg.interpretation,
|
||||
shouldKeepOldResultsWhileRendering: msg.shouldKeepOldResultsWhileRendering,
|
||||
metadata: msg.metadata
|
||||
shouldKeepOldResultsWhileRendering:
|
||||
msg.shouldKeepOldResultsWhileRendering,
|
||||
metadata: msg.metadata,
|
||||
});
|
||||
|
||||
this.loadResults();
|
||||
break;
|
||||
case 'resultsUpdating':
|
||||
case "resultsUpdating":
|
||||
this.setState({
|
||||
isExpectingResultsUpdate: true
|
||||
isExpectingResultsUpdate: true,
|
||||
});
|
||||
break;
|
||||
case 'navigatePath':
|
||||
case "navigatePath":
|
||||
onNavigation.fire(msg);
|
||||
break;
|
||||
default:
|
||||
|
@ -188,11 +209,13 @@ class App extends React.Component<{}, ResultsViewState> {
|
|||
}
|
||||
|
||||
private updateStateWithNewResultsInfo(resultsInfo: ResultsInfo): void {
|
||||
this.setState(prevState => {
|
||||
const stateWithDisplayedResults = (displayedResults: ResultsState): ResultsViewState => ({
|
||||
this.setState((prevState) => {
|
||||
const stateWithDisplayedResults = (
|
||||
displayedResults: ResultsState
|
||||
): ResultsViewState => ({
|
||||
displayedResults,
|
||||
isExpectingResultsUpdate: prevState.isExpectingResultsUpdate,
|
||||
nextResultsInfo: resultsInfo
|
||||
nextResultsInfo: resultsInfo,
|
||||
});
|
||||
|
||||
if (!prevState.isExpectingResultsUpdate && resultsInfo === null) {
|
||||
|
@ -200,7 +223,7 @@ class App extends React.Component<{}, ResultsViewState> {
|
|||
return stateWithDisplayedResults({
|
||||
resultsInfo: null,
|
||||
results: null,
|
||||
errorMessage: 'No results to display'
|
||||
errorMessage: "No results to display",
|
||||
});
|
||||
}
|
||||
if (!resultsInfo || !resultsInfo.shouldKeepOldResultsWhileRendering) {
|
||||
|
@ -208,19 +231,22 @@ class App extends React.Component<{}, ResultsViewState> {
|
|||
return stateWithDisplayedResults({
|
||||
resultsInfo: null,
|
||||
results: null,
|
||||
errorMessage: 'Loading results…'
|
||||
errorMessage: "Loading results…",
|
||||
});
|
||||
}
|
||||
return stateWithDisplayedResults(prevState.displayedResults);
|
||||
});
|
||||
}
|
||||
|
||||
private async getResultSets(resultsInfo: ResultsInfo): Promise<readonly ResultSet[]> {
|
||||
private async getResultSets(
|
||||
resultsInfo: ResultsInfo
|
||||
): Promise<readonly ResultSet[]> {
|
||||
const parsedResultSets = resultsInfo.parsedResultSets;
|
||||
switch (parsedResultSets.t) {
|
||||
case 'WebviewParsed': return await this.fetchResultSets(resultsInfo);
|
||||
case 'ExtensionParsed': {
|
||||
return [{ t: 'RawResultSet', ...parsedResultSets.resultSet }];
|
||||
case "WebviewParsed":
|
||||
return await this.fetchResultSets(resultsInfo);
|
||||
case "ExtensionParsed": {
|
||||
return [{ t: "RawResultSet", ...parsedResultSets.resultSet }];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -232,27 +258,26 @@ class App extends React.Component<{}, ResultsViewState> {
|
|||
}
|
||||
|
||||
let results: Results | null = null;
|
||||
let statusText = '';
|
||||
let statusText = "";
|
||||
try {
|
||||
const resultSets = await this.getResultSets(resultsInfo);
|
||||
results = {
|
||||
resultSets,
|
||||
database: resultsInfo.database,
|
||||
sortStates: this.getSortStates(resultsInfo)
|
||||
sortStates: this.getSortStates(resultsInfo),
|
||||
};
|
||||
}
|
||||
catch (e) {
|
||||
} catch (e) {
|
||||
let errorMessage: string;
|
||||
if (e instanceof Error) {
|
||||
errorMessage = e.message;
|
||||
} else {
|
||||
errorMessage = 'Unknown error';
|
||||
errorMessage = "Unknown error";
|
||||
}
|
||||
|
||||
statusText = `Error loading results: ${errorMessage}`;
|
||||
}
|
||||
|
||||
this.setState(prevState => {
|
||||
this.setState((prevState) => {
|
||||
// Only set state if this results info is still current.
|
||||
if (resultsInfo !== prevState.nextResultsInfo) {
|
||||
return null;
|
||||
|
@ -261,10 +286,10 @@ class App extends React.Component<{}, ResultsViewState> {
|
|||
displayedResults: {
|
||||
resultsInfo,
|
||||
results,
|
||||
errorMessage: statusText
|
||||
errorMessage: statusText,
|
||||
},
|
||||
nextResultsInfo: null,
|
||||
isExpectingResultsUpdate: false
|
||||
isExpectingResultsUpdate: false,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -273,67 +298,99 @@ class App extends React.Component<{}, ResultsViewState> {
|
|||
* This is deprecated, because it calls `fetch`. We are moving
|
||||
* towards doing all bqrs parsing in the extension.
|
||||
*/
|
||||
private async fetchResultSets(resultsInfo: ResultsInfo): Promise<readonly ResultSet[]> {
|
||||
private async fetchResultSets(
|
||||
resultsInfo: ResultsInfo
|
||||
): Promise<readonly ResultSet[]> {
|
||||
const unsortedResponse = await fetch(resultsInfo.resultsPath);
|
||||
const unsortedResultSets = await parseResultSets(unsortedResponse);
|
||||
return Promise.all(unsortedResultSets.map(async unsortedResultSet => {
|
||||
const sortedResultSetInfo = resultsInfo.sortedResultsMap.get(unsortedResultSet.schema.name);
|
||||
if (sortedResultSetInfo === undefined) {
|
||||
return unsortedResultSet;
|
||||
}
|
||||
const response = await fetch(sortedResultSetInfo.resultsPath);
|
||||
const resultSets = await parseResultSets(response);
|
||||
if (resultSets.length != 1) {
|
||||
throw new Error(`Expected sorted BQRS to contain a single result set, encountered ${resultSets.length} result sets.`);
|
||||
}
|
||||
return resultSets[0];
|
||||
}));
|
||||
return Promise.all(
|
||||
unsortedResultSets.map(async (unsortedResultSet) => {
|
||||
const sortedResultSetInfo = resultsInfo.sortedResultsMap.get(
|
||||
unsortedResultSet.schema.name
|
||||
);
|
||||
if (sortedResultSetInfo === undefined) {
|
||||
return unsortedResultSet;
|
||||
}
|
||||
const response = await fetch(sortedResultSetInfo.resultsPath);
|
||||
const resultSets = await parseResultSets(response);
|
||||
if (resultSets.length != 1) {
|
||||
throw new Error(
|
||||
`Expected sorted BQRS to contain a single result set, encountered ${resultSets.length} result sets.`
|
||||
);
|
||||
}
|
||||
return resultSets[0];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private getSortStates(resultsInfo: ResultsInfo): Map<string, RawResultsSortState> {
|
||||
private getSortStates(
|
||||
resultsInfo: ResultsInfo
|
||||
): Map<string, RawResultsSortState> {
|
||||
const entries = Array.from(resultsInfo.sortedResultsMap.entries());
|
||||
return new Map(entries.map(([key, sortedResultSetInfo]) =>
|
||||
[key, sortedResultSetInfo.sortState]));
|
||||
return new Map(
|
||||
entries.map(([key, sortedResultSetInfo]) => [
|
||||
key,
|
||||
sortedResultSetInfo.sortState,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
const displayedResults = this.state.displayedResults;
|
||||
if (displayedResults.results !== null && displayedResults.resultsInfo !== null) {
|
||||
if (
|
||||
displayedResults.results !== null &&
|
||||
displayedResults.resultsInfo !== null
|
||||
) {
|
||||
const parsedResultSets = displayedResults.resultsInfo.parsedResultSets;
|
||||
return <ResultTables
|
||||
parsedResultSets={parsedResultSets}
|
||||
rawResultSets={displayedResults.results.resultSets}
|
||||
interpretation={displayedResults.resultsInfo ? displayedResults.resultsInfo.interpretation : undefined}
|
||||
database={displayedResults.results.database}
|
||||
origResultsPaths={displayedResults.resultsInfo.origResultsPaths}
|
||||
resultsPath={displayedResults.resultsInfo.resultsPath}
|
||||
metadata={displayedResults.resultsInfo ? displayedResults.resultsInfo.metadata : undefined}
|
||||
sortStates={displayedResults.results.sortStates}
|
||||
interpretedSortState={displayedResults.resultsInfo.interpretation?.sortState}
|
||||
isLoadingNewResults={this.state.isExpectingResultsUpdate || this.state.nextResultsInfo !== null} />;
|
||||
}
|
||||
else {
|
||||
return (
|
||||
<ResultTables
|
||||
parsedResultSets={parsedResultSets}
|
||||
rawResultSets={displayedResults.results.resultSets}
|
||||
interpretation={
|
||||
displayedResults.resultsInfo
|
||||
? displayedResults.resultsInfo.interpretation
|
||||
: undefined
|
||||
}
|
||||
database={displayedResults.results.database}
|
||||
origResultsPaths={displayedResults.resultsInfo.origResultsPaths}
|
||||
resultsPath={displayedResults.resultsInfo.resultsPath}
|
||||
metadata={
|
||||
displayedResults.resultsInfo
|
||||
? displayedResults.resultsInfo.metadata
|
||||
: undefined
|
||||
}
|
||||
sortStates={displayedResults.results.sortStates}
|
||||
interpretedSortState={
|
||||
displayedResults.resultsInfo.interpretation?.sortState
|
||||
}
|
||||
isLoadingNewResults={
|
||||
this.state.isExpectingResultsUpdate ||
|
||||
this.state.nextResultsInfo !== null
|
||||
}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return <span>{displayedResults.errorMessage}</span>;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
this.vscodeMessageHandler = evt => this.handleMessage(evt.data as IntoResultsViewMsg);
|
||||
window.addEventListener('message', this.vscodeMessageHandler);
|
||||
this.vscodeMessageHandler = (evt) =>
|
||||
this.handleMessage(evt.data as IntoResultsViewMsg);
|
||||
window.addEventListener("message", this.vscodeMessageHandler);
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
if (this.vscodeMessageHandler) {
|
||||
window.removeEventListener('message', this.vscodeMessageHandler);
|
||||
window.removeEventListener("message", this.vscodeMessageHandler);
|
||||
}
|
||||
}
|
||||
|
||||
private vscodeMessageHandler: ((ev: MessageEvent) => void) | undefined = undefined;
|
||||
private vscodeMessageHandler:
|
||||
| ((ev: MessageEvent) => void)
|
||||
| undefined = undefined;
|
||||
}
|
||||
|
||||
Rdom.render(
|
||||
<App />,
|
||||
document.getElementById('root')
|
||||
);
|
||||
Rdom.render(<App />, document.getElementById("root"));
|
||||
|
||||
vscode.postMessage({ t: 'resultViewLoaded' });
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
margin-left: auto;
|
||||
}
|
||||
|
||||
.vscode-codeql__result-table-toggle-diagnostics {
|
||||
.vscode-codeql__result-table-toggle-diagnostics {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
|
@ -29,16 +29,17 @@
|
|||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.vscode-codeql__result-table-toggle-diagnostics input {
|
||||
margin: 3px 3px 1px 13px;
|
||||
}
|
||||
|
||||
|
||||
.vscode-codeql__result-table th {
|
||||
border-top: 1px solid rgba(88,96,105,0.25);
|
||||
border-bottom: 1px solid rgba(88,96,105,0.25);
|
||||
font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji;
|
||||
background: rgba(225,228,232, 0.25);
|
||||
border-top: 1px solid rgba(88, 96, 105, 0.25);
|
||||
border-bottom: 1px solid rgba(88, 96, 105, 0.25);
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji;
|
||||
background: rgba(225, 228, 232, 0.25);
|
||||
padding: 0.25em 0.5em;
|
||||
text-align: center;
|
||||
font-weight: normal;
|
||||
|
@ -48,8 +49,8 @@
|
|||
.vscode-codeql__result-table .sort-asc,
|
||||
.vscode-codeql__result-table .sort-desc,
|
||||
.vscode-codeql__result-table .sort-none {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.vscode-codeql__result-table .sort-none::after {
|
||||
|
@ -155,3 +156,12 @@ td.vscode-codeql__path-index-cell {
|
|||
.number-of-results {
|
||||
padding-left: 3em;
|
||||
}
|
||||
|
||||
.vscode-codeql__compare-header {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.vscode-codeql__compare-body {
|
||||
margin: 20px 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
import { FromCompareViewMessage, FromResultsViewMsg } from "../interface-types";
|
||||
|
||||
export interface VsCodeApi {
|
||||
/**
|
||||
* Post message back to vscode extension.
|
||||
*/
|
||||
postMessage(msg: FromResultsViewMsg | FromCompareViewMessage): void;
|
||||
}
|
||||
|
||||
declare const acquireVsCodeApi: () => VsCodeApi;
|
||||
export const vscode = acquireVsCodeApi();
|
|
@ -20,7 +20,9 @@ import {
|
|||
WholeFileLocation,
|
||||
ResolvableLocationValue,
|
||||
} from "semmle-bqrs";
|
||||
import { DatabaseItem } from "./databases";
|
||||
import { DatabaseItem, DatabaseManager } from "./databases";
|
||||
import { ViewSourceFileMsg } from "./interface-types";
|
||||
import { Logger } from "./logging";
|
||||
|
||||
/** Gets a nonce string created with 128 bits of entropy. */
|
||||
export function getNonce(): string {
|
||||
|
@ -197,3 +199,30 @@ export const shownLocationLineDecoration = Window.createTextEditorDecorationType
|
|||
isWholeLine: true,
|
||||
}
|
||||
);
|
||||
|
||||
export async function jumpToLocation(
|
||||
msg: ViewSourceFileMsg,
|
||||
databaseManager: DatabaseManager,
|
||||
logger: Logger
|
||||
) {
|
||||
const databaseItem = databaseManager.findDatabaseItem(
|
||||
Uri.parse(msg.databaseUri)
|
||||
);
|
||||
if (databaseItem !== undefined) {
|
||||
try {
|
||||
await showLocation(msg.loc, databaseItem);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
if (e.message.match(/File not found/)) {
|
||||
Window.showErrorMessage(
|
||||
`Original file of this result is not in the database's source archive.`
|
||||
);
|
||||
} else {
|
||||
logger.log(`Unable to handleMsgFromView: ${e.message}`);
|
||||
}
|
||||
} else {
|
||||
logger.log(`Unable to handleMsgFromView: ${e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче