More work on the graph viewer
The viewer is largely implemented now with the following features and limitations: 1. Any query with `@kind graph` will be opened as a graph 2. Queries that are `@kind graph` and `@tags ide-contextual-queries/print-cfg` will be used in the `CodeQL: View CFG` context command. This will be visible similar to how the AST viewer works. If there is not exactly 1 such query for a given language, then the extension will throw an error. 3. Unfortunately, the cg viewer assumes that the entire file will be added to the graph, so often this will be too big, That leads to the following limitation: 4. There is no size checking on the graph. Graphs that are too big will crash vscode. 5. Lastly, there is a small bug in how the `@id` is interpreted. Any `@id` with a `/` in it will place the `.dot` in a location that can't be found by vscode. So, just don't name your queries with any `/`. This feature is only available in canary mode.
This commit is contained in:
@@ -745,7 +745,7 @@
|
||||
{
|
||||
"command": "codeQL.viewCfg",
|
||||
"group": "9_qlCommands",
|
||||
"when": "resourceScheme == codeql-zip-archive"
|
||||
"when": "resourceScheme == codeql-zip-archive && config.codeQL.canary"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.runQueries",
|
||||
@@ -810,7 +810,7 @@
|
||||
},
|
||||
{
|
||||
"command": "codeQL.viewCfg",
|
||||
"when": "resourceScheme == codeql-zip-archive"
|
||||
"when": "resourceScheme == codeql-zip-archive && config.codeQL.canary"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.setCurrentDatabase",
|
||||
@@ -958,6 +958,10 @@
|
||||
"command": "codeQL.viewAst",
|
||||
"when": "resourceScheme == codeql-zip-archive"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.viewCfg",
|
||||
"when": "resourceScheme == codeql-zip-archive && config.codeQL.canary"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.quickEval",
|
||||
"when": "editorLangId == ql"
|
||||
|
||||
16
extensions/ql-vscode/src/additional-typings.d.ts
vendored
Normal file
16
extensions/ql-vscode/src/additional-typings.d.ts
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* The jszip and d3 libraries are designed to work in both the browser and
|
||||
* node. Consequently their typings files refer to both node
|
||||
* types like `Buffer` (which don't exist in the browser), and browser
|
||||
* types like `Blob` (which don't exist in node). Instead of sticking
|
||||
* all of `dom` in `compilerOptions.lib`, it suffices just to put in a
|
||||
* stub definition of the affected types so that compilation
|
||||
* succeeds.
|
||||
*/
|
||||
|
||||
declare type Blob = string;
|
||||
declare type RequestInit = Record<string, unknown>;
|
||||
declare type ElementTagNameMap = any;
|
||||
declare type NodeListOf<T> = Record<string, T>;
|
||||
declare type Node = Record<string, unknown>;
|
||||
declare type XMLDocument = Record<string, unknown>;
|
||||
11
extensions/ql-vscode/src/blob.d.ts
vendored
11
extensions/ql-vscode/src/blob.d.ts
vendored
@@ -1,11 +0,0 @@
|
||||
/**
|
||||
* The npm library jszip is designed to work in both the browser and
|
||||
* node. Consequently its typings @types/jszip refers to both node
|
||||
* types like `Buffer` (which don't exist in the browser), and browser
|
||||
* types like `Blob` (which don't exist in node). Instead of sticking
|
||||
* all of `dom` in `compilerOptions.lib`, it suffices just to put in a
|
||||
* stub definition of the type `Blob` here so that compilation
|
||||
* succeeds.
|
||||
*/
|
||||
|
||||
//declare type Blob = string; // TODO: Check this
|
||||
@@ -1061,11 +1061,11 @@ class SplitBuffer {
|
||||
|
||||
/**
|
||||
* A version of startsWith that isn't overriden by a broken version of ms-python.
|
||||
*
|
||||
*
|
||||
* The definition comes from
|
||||
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith
|
||||
* which is CC0/public domain
|
||||
*
|
||||
*
|
||||
* See https://github.com/github/vscode-codeql/issues/802 for more context as to why we need it.
|
||||
*/
|
||||
private static startsWith(s: string, searchString: string, position: number): boolean {
|
||||
|
||||
@@ -258,14 +258,14 @@ export class TemplatePrintCfgProvider {
|
||||
|
||||
const qlpack = await qlpackOfDatabase(this.cli, db);
|
||||
if (!qlpack) {
|
||||
throw new Error('Can\'t infer qlpack from database source archive');
|
||||
throw new Error('Can\'t infer qlpack from database source archive.');
|
||||
}
|
||||
const queries = await resolveQueries(this.cli, qlpack, KeyType.PrintCfgQuery);
|
||||
if (queries.length > 1) {
|
||||
throw new Error('Found multiple Print CFG queries. Can\'t continue');
|
||||
throw new Error(`Found multiple Print CFG queries. Can't continue. Make sure there is exacly one query with the tag ${KeyType.PrintCfgQuery}`);
|
||||
}
|
||||
if (queries.length === 0) {
|
||||
throw new Error('Did not find any Print CFG queries. Can\'t continue');
|
||||
throw new Error(`Did not find any Print CFG queries. Can't continue. Make sure there is exacly one query with the tag ${KeyType.PrintCfgQuery}`);
|
||||
}
|
||||
|
||||
const queryUri = Uri.file(queries[0]);
|
||||
|
||||
@@ -982,26 +982,6 @@ async function activateWithInstalledDistribution(
|
||||
})
|
||||
);
|
||||
|
||||
ctx.subscriptions.push(
|
||||
commandRunnerWithProgress(
|
||||
'codeQL.viewCfg',
|
||||
async (
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken
|
||||
) => {
|
||||
const res = await new TemplatePrintCfgProvider(cliServer, dbm)
|
||||
.provideCfgUri(window.activeTextEditor?.document);
|
||||
if (res) {
|
||||
await compileAndRunQuery(false, res[0], progress, token, undefined);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Calculate CFG',
|
||||
cancellable: true
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
void logger.log('Starting language server.');
|
||||
ctx.subscriptions.push(client.start());
|
||||
|
||||
@@ -1018,7 +998,8 @@ async function activateWithInstalledDistribution(
|
||||
);
|
||||
|
||||
const astViewer = new AstViewer();
|
||||
const templateProvider = new TemplatePrintAstProvider(cliServer, qs, dbm);
|
||||
const printAstTemplateProvider = new TemplatePrintAstProvider(cliServer, qs, dbm);
|
||||
const cfgTemplateProvider = new TemplatePrintCfgProvider(cliServer, dbm);
|
||||
|
||||
ctx.subscriptions.push(astViewer);
|
||||
ctx.subscriptions.push(commandRunnerWithProgress('codeQL.viewAst', async (
|
||||
@@ -1026,7 +1007,7 @@ async function activateWithInstalledDistribution(
|
||||
token: CancellationToken,
|
||||
selectedFile: Uri
|
||||
) => {
|
||||
const ast = await templateProvider.provideAst(
|
||||
const ast = await printAstTemplateProvider.provideAst(
|
||||
progress,
|
||||
token,
|
||||
selectedFile ?? window.activeTextEditor?.document.uri,
|
||||
@@ -1039,6 +1020,26 @@ async function activateWithInstalledDistribution(
|
||||
title: 'Calculate AST'
|
||||
}));
|
||||
|
||||
ctx.subscriptions.push(
|
||||
commandRunnerWithProgress(
|
||||
'codeQL.viewCfg',
|
||||
async (
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken
|
||||
) => {
|
||||
const res = await
|
||||
cfgTemplateProvider.provideCfgUri(window.activeTextEditor?.document);
|
||||
if (res) {
|
||||
await compileAndRunQuery(false, res[0], progress, token, undefined);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Calculating Control Flow Graph',
|
||||
cancellable: true
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
await commands.executeCommand('codeQLDatabases.removeOrphanedDatabases');
|
||||
|
||||
void logger.log('Successfully finished extension initialization.');
|
||||
|
||||
@@ -162,9 +162,9 @@ export class InterfaceManager extends DisposableObject {
|
||||
this._diagnosticCollection.clear();
|
||||
if (this.isShowingPanel()) {
|
||||
void this.postMessage({
|
||||
t: 'untoggleShowProblems'
|
||||
});
|
||||
}
|
||||
t: 'untoggleShowProblems'
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -347,8 +347,8 @@ export class InterfaceManager extends DisposableObject {
|
||||
break;
|
||||
default:
|
||||
assertNever(msg);
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
} catch (e) {
|
||||
void showAndLogErrorMessage(e.message, {
|
||||
fullMessage: e.stack
|
||||
});
|
||||
@@ -608,15 +608,14 @@ export class InterfaceManager extends DisposableObject {
|
||||
sourceInfo: cli.SourceInfo | undefined,
|
||||
sourceLocationPrefix: string,
|
||||
sortState: InterpretedResultsSortState | undefined
|
||||
): Promise<Interpretation | undefined> {
|
||||
): Promise<Interpretation | undefined> {
|
||||
if (!resultsPaths) {
|
||||
void this.logger.log('No results path. Cannot display interpreted results.');
|
||||
return undefined;
|
||||
}
|
||||
let data;
|
||||
let numTotalResults;
|
||||
if (metadata?.kind === 'graph')
|
||||
{
|
||||
if (metadata?.kind === GRAPH_TABLE_NAME) {
|
||||
data = await interpretGraphResults(
|
||||
this.cliServer,
|
||||
metadata,
|
||||
@@ -624,9 +623,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
sourceInfo
|
||||
);
|
||||
numTotalResults = data.dot.length;
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
const sarif = await interpretResultsSarif(
|
||||
this.cliServer,
|
||||
metadata,
|
||||
@@ -635,7 +632,7 @@ export class InterfaceManager extends DisposableObject {
|
||||
);
|
||||
|
||||
sarif.runs.forEach(run => {
|
||||
if (run.results !== undefined) {
|
||||
if (run.results) {
|
||||
sortInterpretedResults(run.results, sortState);
|
||||
}
|
||||
});
|
||||
@@ -644,9 +641,9 @@ export class InterfaceManager extends DisposableObject {
|
||||
data = sarif;
|
||||
|
||||
numTotalResults = (() => {
|
||||
if (sarif.runs.length === 0) return 0;
|
||||
if (sarif.runs[0].results === undefined) return 0;
|
||||
return sarif.runs[0].results.length;
|
||||
return sarif.runs?.[0]?.results
|
||||
? sarif.runs[0].results.length
|
||||
: 0;
|
||||
})();
|
||||
}
|
||||
|
||||
@@ -686,7 +683,10 @@ export class InterfaceManager extends DisposableObject {
|
||||
|
||||
return {
|
||||
...interp,
|
||||
data: { ...interp.data, runs: [getPageOfRun(interp.data.runs[0])] },
|
||||
data: {
|
||||
...interp.data,
|
||||
runs: [getPageOfRun(interp.data.runs[0])]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -281,7 +281,7 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
} else {
|
||||
this.treeDataProvider.setCurrentItem(ev.selection[0]);
|
||||
}
|
||||
this.updateCompareWith(ev.selection);
|
||||
this.updateCompareWith([...ev.selection]);
|
||||
})
|
||||
);
|
||||
|
||||
@@ -851,14 +851,14 @@ the file in the file explorer and dragging it into the workspace.`
|
||||
private determineSelection(
|
||||
singleItem: FullQueryInfo,
|
||||
multiSelect: FullQueryInfo[]
|
||||
): { finalSingleItem: FullQueryInfo; finalMultiSelect: FullQueryInfo[] } {
|
||||
): { readonly finalSingleItem: FullQueryInfo; readonly finalMultiSelect: FullQueryInfo[] } {
|
||||
if (!singleItem && !multiSelect?.[0]) {
|
||||
const selection = this.treeView.selection;
|
||||
const current = this.treeDataProvider.getCurrent();
|
||||
if (selection?.length) {
|
||||
return {
|
||||
finalSingleItem: selection[0],
|
||||
finalMultiSelect: selection
|
||||
finalMultiSelect: [...selection]
|
||||
};
|
||||
} else if (current) {
|
||||
return {
|
||||
|
||||
@@ -165,6 +165,25 @@ export async function interpretResultsSarif(
|
||||
return { ...res, t: 'SarifInterpretationData' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Call cli command to interpret graph results.
|
||||
*/
|
||||
export async function interpretGraphResults(
|
||||
cli: cli.CodeQLCliServer,
|
||||
metadata: QueryMetadata | undefined,
|
||||
resultsPaths: ResultsPaths,
|
||||
sourceInfo?: cli.SourceInfo
|
||||
): Promise<GraphInterpretationData> {
|
||||
const { resultsPath, interpretedResultsPath } = resultsPaths;
|
||||
if (await fs.pathExists(interpretedResultsPath)) {
|
||||
const dot = await cli.readDotFiles(interpretedResultsPath);
|
||||
return { dot, t: 'GraphInterpretationData' };
|
||||
}
|
||||
|
||||
const dot = await cli.interpretBqrsGraph(ensureMetadataIsComplete(metadata), resultsPath, interpretedResultsPath, sourceInfo);
|
||||
return { dot, t: 'GraphInterpretationData' };
|
||||
}
|
||||
|
||||
export function ensureMetadataIsComplete(metadata: QueryMetadata | undefined) {
|
||||
if (metadata === undefined) {
|
||||
throw new Error('Can\'t interpret results without query metadata');
|
||||
@@ -380,22 +399,3 @@ export class FullQueryInfo {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call cli command to interpret graph results.
|
||||
*/
|
||||
export async function interpretGraphResults(
|
||||
server: cli.CodeQLCliServer,
|
||||
metadata: QueryMetadata | undefined,
|
||||
resultsPaths: ResultsPaths,
|
||||
sourceInfo?: cli.SourceInfo
|
||||
): Promise<GraphInterpretationData> {
|
||||
const { resultsPath, interpretedResultsPath } = resultsPaths;
|
||||
if (await fs.pathExists(interpretedResultsPath)) {
|
||||
const dot = await server.readDotFiles(interpretedResultsPath);
|
||||
return { dot, t: 'GraphInterpretationData' };
|
||||
}
|
||||
|
||||
const dot = await server.interpretBqrsGraph(ensureMetadataIsComplete(metadata), resultsPath, interpretedResultsPath, sourceInfo);
|
||||
return { dot, t: 'GraphInterpretationData' };
|
||||
}
|
||||
|
||||
@@ -7,15 +7,26 @@ import { jumpToLocation } from './result-table-utils';
|
||||
import { tryGetLocationFromString } from '../pure/bqrs-utils';
|
||||
export type GraphProps = ResultTableProps & { resultSet: InterpretedResultSet<GraphInterpretationData> };
|
||||
|
||||
const className = 'vscode-codeql__result-tables-graph';
|
||||
|
||||
const graphClassName = 'vscode-codeql__result-tables-graph';
|
||||
const graphId = 'graph-results';
|
||||
export class Graph extends React.Component<GraphProps> {
|
||||
constructor(props: GraphProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
public render = (): JSX.Element => {
|
||||
return <div id={className} className={className} />;
|
||||
const { resultSet, offset } = this.props;
|
||||
const graphData = resultSet.interpretation?.data?.dot[offset];
|
||||
|
||||
if (!graphData) {
|
||||
return <>
|
||||
<div className={graphClassName}>Graph is not available.</div>
|
||||
</>;
|
||||
}
|
||||
|
||||
return <>
|
||||
<div id={graphId} className={graphClassName}><span>Rendering graph...</span></div>
|
||||
</>;
|
||||
};
|
||||
|
||||
public componentDidMount = () => {
|
||||
@@ -27,7 +38,13 @@ export class Graph extends React.Component<GraphProps> {
|
||||
};
|
||||
|
||||
private renderGraph = () => {
|
||||
const { databaseUri, resultSet } = this.props;
|
||||
const { databaseUri, resultSet, offset } = this.props;
|
||||
const graphData = resultSet.interpretation?.data?.dot[offset];
|
||||
|
||||
if (!graphData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const options = {
|
||||
fit: true,
|
||||
fade: false,
|
||||
@@ -35,13 +52,18 @@ export class Graph extends React.Component<GraphProps> {
|
||||
zoom: true,
|
||||
};
|
||||
|
||||
const element = document.querySelector(`.${className}`);
|
||||
const color = element ? getComputedStyle(element).color : 'black';
|
||||
const backgroundColor = element ? getComputedStyle(element).backgroundColor : 'transparent';
|
||||
const borderColor = element ? getComputedStyle(element).borderColor : 'black';
|
||||
const element = document.querySelector(`#${graphId}`);
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
element.firstChild?.remove();
|
||||
|
||||
const color = getComputedStyle(element).color;
|
||||
const backgroundColor = getComputedStyle(element).backgroundColor;
|
||||
const borderColor = getComputedStyle(element).borderColor;
|
||||
let firstPolygon = true;
|
||||
|
||||
graphviz(`#${className}`)
|
||||
graphviz(`#${graphId}`)
|
||||
.options(options)
|
||||
.attributer(function(d) {
|
||||
if (d.tag == 'a') {
|
||||
@@ -62,13 +84,14 @@ export class Graph extends React.Component<GraphProps> {
|
||||
// There is no proper way to identify the element containing the graph (which we
|
||||
// don't want a border around), as it is just has tag 'polygon'. Instead we assume
|
||||
// that the first polygon we see is that element
|
||||
if (d.tag != 'polygon' || !firstPolygon)
|
||||
if (d.tag != 'polygon' || !firstPolygon) {
|
||||
d.attributes.stroke = borderColor;
|
||||
else
|
||||
} else {
|
||||
firstPolygon = false;
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
.renderDot(resultSet.interpretation.data.dot[this.props.offset]);
|
||||
.renderDot(graphData);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -65,8 +65,6 @@ describe('queryResolver', () => {
|
||||
it('should throw an error when there are no queries found', async () => {
|
||||
mockCli.resolveQueriesInSuite.returns([]);
|
||||
|
||||
// TODO: Figure out why chai-as-promised isn't failing the test on an
|
||||
// unhandled rejection.
|
||||
try {
|
||||
await module.resolveQueries(mockCli, { dbschemePack: 'my-qlpack' }, KeyType.DefinitionQuery);
|
||||
// should reject
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"module": "commonjs",
|
||||
"target": "es2017",
|
||||
"outDir": "out",
|
||||
"lib": ["ES2020", "dom"], // No idea what I am doing; needed to avoid `Cannot find name 'RequestInit'` error
|
||||
"lib": ["ES2020"],
|
||||
"moduleResolution": "node",
|
||||
"sourceMap": true,
|
||||
"rootDir": "src",
|
||||
|
||||
Reference in New Issue
Block a user