Skip to content

Commit eb0a72f

Browse files
committed
Graph viewer support
1 parent 6ef287a commit eb0a72f

15 files changed

Lines changed: 1793 additions & 101 deletions

extensions/ql-vscode/package-lock.json

Lines changed: 1467 additions & 64 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

extensions/ql-vscode/package.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"onCommand:codeQLDatabases.chooseDatabaseLgtm",
4646
"onCommand:codeQL.setCurrentDatabase",
4747
"onCommand:codeQL.viewAst",
48+
"onCommand:codeQL.viewCfg",
4849
"onCommand:codeQL.openReferencedFile",
4950
"onCommand:codeQL.previewQueryHelp",
5051
"onCommand:codeQL.chooseDatabaseFolder",
@@ -356,6 +357,10 @@
356357
"command": "codeQL.viewAst",
357358
"title": "CodeQL: View AST"
358359
},
360+
{
361+
"command": "codeQL.viewCfg",
362+
"title": "CodeQL: View CFG"
363+
},
359364
{
360365
"command": "codeQL.upgradeCurrentDatabase",
361366
"title": "CodeQL: Upgrade Current Database"
@@ -713,6 +718,11 @@
713718
"group": "9_qlCommands",
714719
"when": "resourceScheme == codeql-zip-archive"
715720
},
721+
{
722+
"command": "codeQL.viewCfg",
723+
"group": "9_qlCommands",
724+
"when": "resourceScheme == codeql-zip-archive"
725+
},
716726
{
717727
"command": "codeQL.runQueries",
718728
"group": "9_qlCommands",
@@ -770,6 +780,10 @@
770780
"command": "codeQL.viewAst",
771781
"when": "resourceScheme == codeql-zip-archive"
772782
},
783+
{
784+
"command": "codeQL.viewCfg",
785+
"when": "resourceScheme == codeql-zip-archive"
786+
},
773787
{
774788
"command": "codeQLDatabases.setCurrentDatabase",
775789
"when": "false"
@@ -983,6 +997,8 @@
983997
"@octokit/rest": "^18.5.6",
984998
"child-process-promise": "^2.2.1",
985999
"classnames": "~2.2.6",
1000+
"d3": "^6.3.1",
1001+
"d3-graphviz": "^2.6.1",
9861002
"fs-extra": "^9.0.1",
9871003
"glob-promise": "^3.4.0",
9881004
"js-yaml": "^3.14.0",
@@ -1011,6 +1027,8 @@
10111027
"@types/chai-as-promised": "~7.1.2",
10121028
"@types/child-process-promise": "^2.2.1",
10131029
"@types/classnames": "~2.2.9",
1030+
"@types/d3": "^6.2.0",
1031+
"@types/d3-graphviz": "^2.6.6",
10141032
"@types/fs-extra": "^9.0.6",
10151033
"@types/glob": "^7.1.1",
10161034
"@types/google-protobuf": "^3.2.7",

extensions/ql-vscode/src/blob.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@
88
* succeeds.
99
*/
1010

11-
declare type Blob = string;
11+
//declare type Blob = string; // TODO: Check this

extensions/ql-vscode/src/cli.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as cpp from 'child-process-promise';
22
import * as child_process from 'child_process';
3+
import * as fs from 'fs-extra';
34
import * as path from 'path';
45
import * as sarif from 'sarif';
56
import { SemVer } from 'semver';
@@ -715,6 +716,26 @@ export class CodeQLCliServer implements Disposable {
715716
return await sarifParser(interpretedResultsPath);
716717
}
717718

719+
async readDotFiles(dir: string): Promise<string[]> {
720+
return Promise.all((await fs.readdir(dir))
721+
.filter(name => path.extname(name).toLowerCase() === '.dot')
722+
.map(file => fs.readFile(path.join(dir, file), 'utf8'))
723+
);
724+
}
725+
726+
async interpretBqrsGraph(metadata: QueryMetadata, resultsPath: string, interpretedResultsPath: string, sourceInfo?: SourceInfo): Promise<string[]> {
727+
const additionalArgs = sourceInfo ? ['--dot-location-url-format', 'file://' + sourceInfo.sourceLocationPrefix + '{path}:{start:line}:{start:column}:{end:line}:{end:column}'] : [];
728+
729+
await this.runInterpretCommand('dot', additionalArgs, metadata, resultsPath, interpretedResultsPath, sourceInfo);
730+
731+
try {
732+
const dot = await this.readDotFiles(interpretedResultsPath);
733+
return dot;
734+
} catch (err) {
735+
throw new Error(`Reading output of interpretation failed: ${err.stderr || err}`);
736+
}
737+
}
738+
718739
async generateResultsCsv(metadata: QueryMetadata, resultsPath: string, csvPath: string, sourceInfo?: SourceInfo): Promise<void> {
719740
await this.runInterpretCommand(CSV_FORMAT, [], metadata, resultsPath, csvPath, sourceInfo);
720741
}

extensions/ql-vscode/src/contextual/keyType.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export enum KeyType {
22
DefinitionQuery = 'DefinitionQuery',
33
ReferenceQuery = 'ReferenceQuery',
44
PrintAstQuery = 'PrintAstQuery',
5+
PrintCfgQuery = 'PrintCfgQuery',
56
}
67

78
export function tagOfKeyType(keyType: KeyType): string {
@@ -12,6 +13,8 @@ export function tagOfKeyType(keyType: KeyType): string {
1213
return 'ide-contextual-queries/local-references';
1314
case KeyType.PrintAstQuery:
1415
return 'ide-contextual-queries/print-ast';
16+
case KeyType.PrintCfgQuery:
17+
return 'ide-contextual-queries/print-cfg';
1518
}
1619
}
1720

@@ -23,6 +26,8 @@ export function nameOfKeyType(keyType: KeyType): string {
2326
return 'references';
2427
case KeyType.PrintAstQuery:
2528
return 'print AST';
29+
case KeyType.PrintCfgQuery:
30+
return 'print CFG';
2631
}
2732
}
2833

@@ -32,6 +37,7 @@ export function kindOfKeyType(keyType: KeyType): string {
3237
case KeyType.ReferenceQuery:
3338
return 'definitions';
3439
case KeyType.PrintAstQuery:
40+
case KeyType.PrintCfgQuery:
3541
return 'graph';
3642
}
3743
}

extensions/ql-vscode/src/contextual/templateProvider.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,3 +207,62 @@ export class TemplatePrintAstProvider {
207207
);
208208
}
209209
}
210+
211+
export class TemplatePrintCfgProvider {
212+
private cache: CachedOperation<[Uri, messages.TemplateDefinitions] | undefined>;
213+
214+
constructor(
215+
private cli: CodeQLCliServer,
216+
private dbm: DatabaseManager
217+
) {
218+
this.cache = new CachedOperation<[Uri, messages.TemplateDefinitions] | undefined>(this.getCfgUri.bind(this));
219+
}
220+
221+
async provideCfgUri(document?: TextDocument): Promise<[Uri, messages.TemplateDefinitions] | undefined> {
222+
if (!document) {
223+
return;
224+
}
225+
return await this.cache.get(document.uri.toString());
226+
}
227+
228+
private async getCfgUri(uriString: string): Promise<[Uri, messages.TemplateDefinitions]> {
229+
const uri = Uri.parse(uriString, true);
230+
if (uri.scheme !== zipArchiveScheme) {
231+
throw new Error('CFG Viewing is only available for databases with zipped source archives.');
232+
}
233+
234+
const zippedArchive = decodeSourceArchiveUri(uri);
235+
const sourceArchiveUri = encodeArchiveBasePath(zippedArchive.sourceArchiveZipPath);
236+
const db = this.dbm.findDatabaseItemBySourceArchive(sourceArchiveUri);
237+
238+
if (!db) {
239+
throw new Error('Can\'t infer database from the provided source.');
240+
}
241+
242+
const qlpack = await qlpackOfDatabase(this.cli, db);
243+
if (!qlpack) {
244+
throw new Error('Can\'t infer qlpack from database source archive');
245+
}
246+
const queries = await resolveQueries(this.cli, qlpack, KeyType.PrintCfgQuery);
247+
if (queries.length > 1) {
248+
throw new Error('Found multiple Print CFG queries. Can\'t continue');
249+
}
250+
if (queries.length === 0) {
251+
throw new Error('Did not find any Print CFG queries. Can\'t continue');
252+
}
253+
254+
const queryUri = Uri.file(queries[0]);
255+
256+
const templates: messages.TemplateDefinitions = {
257+
[TEMPLATE_NAME]: {
258+
values: {
259+
tuples: [[{
260+
stringValue: zippedArchive.pathWithinSourceArchive
261+
}]]
262+
}
263+
}
264+
};
265+
266+
return [queryUri, templates];
267+
}
268+
}

extensions/ql-vscode/src/extension.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ import { DatabaseUI } from './databases-ui';
4040
import {
4141
TemplateQueryDefinitionProvider,
4242
TemplateQueryReferenceProvider,
43-
TemplatePrintAstProvider
43+
TemplatePrintAstProvider,
44+
TemplatePrintCfgProvider
4445
} from './contextual/templateProvider';
4546
import {
4647
DEFAULT_DISTRIBUTION_VERSION_RANGE,
@@ -918,6 +919,26 @@ async function activateWithInstalledDistribution(
918919
logger.show();
919920
});
920921

922+
ctx.subscriptions.push(
923+
commandRunnerWithProgress(
924+
'codeQL.viewCfg',
925+
async (
926+
progress: ProgressCallback,
927+
token: CancellationToken
928+
) => {
929+
const res = await new TemplatePrintCfgProvider(cliServer, dbm)
930+
.provideCfgUri(window.activeTextEditor?.document);
931+
if (res) {
932+
await compileAndRunQuery(false, res[0], progress, token, undefined);
933+
}
934+
},
935+
{
936+
title: 'Calculate CFG',
937+
cancellable: true
938+
}
939+
)
940+
);
941+
921942
void logger.log('Starting language server.');
922943
ctx.subscriptions.push(client.start());
923944

extensions/ql-vscode/src/interface.ts

Lines changed: 66 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,13 @@ import {
2727
InterpretedResultsSortState,
2828
SortDirection,
2929
ALERTS_TABLE_NAME,
30+
GRAPH_TABLE_NAME,
3031
RawResultsSortState,
3132
} from './pure/interface-types';
3233
import { Logger } from './logging';
3334
import * as messages from './pure/messages';
3435
import { commandRunner } from './commandRunner';
35-
import { CompletedQuery, interpretResultsSarif } from './query-results';
36+
import { CompletedQuery, interpretResultsSarif, interpretGraphResults } from './query-results';
3637
import { QueryInfo, tmpDir } from './run-queries';
3738
import { parseSarifLocation, parseSarifPlainTextMessage } from './pure/sarif-utils';
3839
import {
@@ -87,12 +88,33 @@ function sortInterpretedResults(
8788
}
8889
}
8990

90-
function numPagesOfResultSet(resultSet: RawResultSet): number {
91-
return Math.ceil(resultSet.schema.rows / PAGE_SIZE.getValue<number>());
91+
function interpretedPageSize(interpretation: Interpretation | undefined): number {
92+
if (interpretation && interpretation.data.t == 'GraphInterpretationData')
93+
return 1;
94+
return PAGE_SIZE.getValue<number>();
95+
}
96+
97+
function numPagesOfResultSet(resultSet: RawResultSet, interpretation?: Interpretation): number {
98+
const pageSize = interpretedPageSize(interpretation);
99+
100+
const n = interpretation && interpretation.data.t == 'GraphInterpretationData'
101+
? interpretation.data.dot.length
102+
: resultSet.schema.rows;
103+
104+
return Math.ceil(n / pageSize);
92105
}
93106

94107
function numInterpretedPages(interpretation: Interpretation | undefined): number {
95-
return Math.ceil((interpretation?.data.runs[0].results?.length || 0) / PAGE_SIZE.getValue<number>());
108+
if (!interpretation)
109+
return 0;
110+
111+
const pageSize = interpretedPageSize(interpretation);
112+
113+
const n = interpretation.data.t == 'GraphInterpretationData'
114+
? interpretation.data.dot.length
115+
: interpretation.data.runs[0].results?.length || 0;
116+
117+
return Math.ceil(n / pageSize);
96118
}
97119

98120
export class InterfaceManager extends DisposableObject {
@@ -303,7 +325,7 @@ export class InterfaceManager extends DisposableObject {
303325
await this.changeInterpretedSortState(msg.sortState);
304326
break;
305327
case 'changePage':
306-
if (msg.selectedTable === ALERTS_TABLE_NAME) {
328+
if (msg.selectedTable === ALERTS_TABLE_NAME || msg.selectedTable === GRAPH_TABLE_NAME) {
307329
await this.showPageOfInterpretedResults(msg.pageNumber);
308330
}
309331
else {
@@ -323,8 +345,8 @@ export class InterfaceManager extends DisposableObject {
323345
break;
324346
default:
325347
assertNever(msg);
326-
}
327-
} catch (e) {
348+
}
349+
} catch (e) {
328350
void showAndLogErrorMessage(e.message, {
329351
fullMessage: e.stack
330352
});
@@ -434,7 +456,7 @@ export class InterfaceManager extends DisposableObject {
434456
const parsedResultSets: ParsedResultSets = {
435457
pageNumber: 0,
436458
pageSize,
437-
numPages: numPagesOfResultSet(resultSet),
459+
numPages: numPagesOfResultSet(resultSet, this._interpretation),
438460
numInterpretedPages: numInterpretedPages(this._interpretation),
439461
resultSet: { ...resultSet, t: 'RawResultSet' },
440462
selectedTable: undefined,
@@ -484,7 +506,7 @@ export class InterfaceManager extends DisposableObject {
484506
metadata: this._displayedQuery.query.metadata,
485507
pageNumber,
486508
resultSetNames,
487-
pageSize: PAGE_SIZE.getValue(),
509+
pageSize: interpretedPageSize(this._interpretation),
488510
numPages: numInterpretedPages(this._interpretation),
489511
queryName: this._displayedQuery.toString(),
490512
queryPath: this._displayedQuery.query.program.queryPath
@@ -582,30 +604,50 @@ export class InterfaceManager extends DisposableObject {
582604
sourceInfo: cli.SourceInfo | undefined,
583605
sourceLocationPrefix: string,
584606
sortState: InterpretedResultsSortState | undefined
585-
): Promise<Interpretation | undefined> {
607+
): Promise<Interpretation | undefined> {
586608
if (!resultsPaths) {
587609
void this.logger.log('No results path. Cannot display interpreted results.');
588610
return undefined;
589611
}
612+
let data;
613+
let numTotalResults;
614+
if (metadata?.kind === 'graph')
615+
{
616+
data = await interpretGraphResults(
617+
this.cliServer,
618+
metadata,
619+
resultsPaths,
620+
sourceInfo
621+
);
622+
numTotalResults = data.dot.length;
623+
}
624+
else
625+
{
626+
const sarif = await interpretResultsSarif(
627+
this.cliServer,
628+
metadata,
629+
resultsPaths,
630+
sourceInfo
631+
);
590632

591-
const sarif = await interpretResultsSarif(
592-
this.cliServer,
593-
metadata,
594-
resultsPaths,
595-
sourceInfo
596-
);
633+
sarif.runs.forEach(run => {
634+
if (run.results !== undefined) {
635+
sortInterpretedResults(run.results, sortState);
636+
}
637+
});
597638

598-
sarif.runs.forEach(run => {
599-
if (run.results !== undefined) {
600-
sortInterpretedResults(run.results, sortState);
601-
}
602-
});
639+
sarif.sortState = sortState;
640+
data = sarif;
603641

604-
const numTotalResults = sarif.runs[0]?.results?.length || 0;
642+
numTotalResults = (() => {
643+
if (sarif.runs.length === 0) return 0;
644+
if (sarif.runs[0].results === undefined) return 0;
645+
return sarif.runs[0].results.length;
646+
})();
647+
}
605648

606-
sarif.sortState = sortState;
607649
const interpretation: Interpretation = {
608-
data: sarif,
650+
data,
609651
sourceLocationPrefix,
610652
numTruncatedResults: 0,
611653
numTotalResults

0 commit comments

Comments
 (0)