Skip to content

Commit 580832e

Browse files
hvitvedaeisenberg
authored andcommitted
Graph viewer support
1 parent ddca0bb commit 580832e

15 files changed

Lines changed: 1861 additions & 900 deletions

extensions/ql-vscode/package-lock.json

Lines changed: 1535 additions & 862 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
@@ -48,6 +48,7 @@
4848
"onCommand:codeQLDatabases.chooseDatabaseLgtm",
4949
"onCommand:codeQL.setCurrentDatabase",
5050
"onCommand:codeQL.viewAst",
51+
"onCommand:codeQL.viewCfg",
5152
"onCommand:codeQL.openReferencedFile",
5253
"onCommand:codeQL.previewQueryHelp",
5354
"onCommand:codeQL.chooseDatabaseFolder",
@@ -368,6 +369,10 @@
368369
"command": "codeQL.viewAst",
369370
"title": "CodeQL: View AST"
370371
},
372+
{
373+
"command": "codeQL.viewCfg",
374+
"title": "CodeQL: View CFG"
375+
},
371376
{
372377
"command": "codeQL.upgradeCurrentDatabase",
373378
"title": "CodeQL: Upgrade Current Database"
@@ -737,6 +742,11 @@
737742
"group": "9_qlCommands",
738743
"when": "resourceScheme == codeql-zip-archive && !explorerResourceIsFolder && !listMultiSelection"
739744
},
745+
{
746+
"command": "codeQL.viewCfg",
747+
"group": "9_qlCommands",
748+
"when": "resourceScheme == codeql-zip-archive"
749+
},
740750
{
741751
"command": "codeQL.runQueries",
742752
"group": "9_qlCommands",
@@ -798,6 +808,10 @@
798808
"command": "codeQL.viewAst",
799809
"when": "resourceScheme == codeql-zip-archive"
800810
},
811+
{
812+
"command": "codeQL.viewCfg",
813+
"when": "resourceScheme == codeql-zip-archive"
814+
},
801815
{
802816
"command": "codeQLDatabases.setCurrentDatabase",
803817
"when": "false"
@@ -1017,6 +1031,8 @@
10171031
"@primer/react": "^34.3.0",
10181032
"child-process-promise": "^2.2.1",
10191033
"classnames": "~2.2.6",
1034+
"d3": "^6.3.1",
1035+
"d3-graphviz": "^2.6.1",
10201036
"fs-extra": "^9.0.1",
10211037
"glob-promise": "^3.4.0",
10221038
"js-yaml": "^3.14.0",
@@ -1048,6 +1064,8 @@
10481064
"@types/child-process-promise": "^2.2.1",
10491065
"@types/classnames": "~2.2.9",
10501066
"@types/del": "^4.0.0",
1067+
"@types/d3": "^6.2.0",
1068+
"@types/d3-graphviz": "^2.6.6",
10511069
"@types/fs-extra": "^9.0.6",
10521070
"@types/glob": "^7.1.1",
10531071
"@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
@@ -224,3 +224,62 @@ export class TemplatePrintAstProvider {
224224
};
225225
}
226226
}
227+
228+
export class TemplatePrintCfgProvider {
229+
private cache: CachedOperation<[Uri, messages.TemplateDefinitions] | undefined>;
230+
231+
constructor(
232+
private cli: CodeQLCliServer,
233+
private dbm: DatabaseManager
234+
) {
235+
this.cache = new CachedOperation<[Uri, messages.TemplateDefinitions] | undefined>(this.getCfgUri.bind(this));
236+
}
237+
238+
async provideCfgUri(document?: TextDocument): Promise<[Uri, messages.TemplateDefinitions] | undefined> {
239+
if (!document) {
240+
return;
241+
}
242+
return await this.cache.get(document.uri.toString());
243+
}
244+
245+
private async getCfgUri(uriString: string): Promise<[Uri, messages.TemplateDefinitions]> {
246+
const uri = Uri.parse(uriString, true);
247+
if (uri.scheme !== zipArchiveScheme) {
248+
throw new Error('CFG Viewing is only available for databases with zipped source archives.');
249+
}
250+
251+
const zippedArchive = decodeSourceArchiveUri(uri);
252+
const sourceArchiveUri = encodeArchiveBasePath(zippedArchive.sourceArchiveZipPath);
253+
const db = this.dbm.findDatabaseItemBySourceArchive(sourceArchiveUri);
254+
255+
if (!db) {
256+
throw new Error('Can\'t infer database from the provided source.');
257+
}
258+
259+
const qlpack = await qlpackOfDatabase(this.cli, db);
260+
if (!qlpack) {
261+
throw new Error('Can\'t infer qlpack from database source archive');
262+
}
263+
const queries = await resolveQueries(this.cli, qlpack, KeyType.PrintCfgQuery);
264+
if (queries.length > 1) {
265+
throw new Error('Found multiple Print CFG queries. Can\'t continue');
266+
}
267+
if (queries.length === 0) {
268+
throw new Error('Did not find any Print CFG queries. Can\'t continue');
269+
}
270+
271+
const queryUri = Uri.file(queries[0]);
272+
273+
const templates: messages.TemplateDefinitions = {
274+
[TEMPLATE_NAME]: {
275+
values: {
276+
tuples: [[{
277+
stringValue: zippedArchive.pathWithinSourceArchive
278+
}]]
279+
}
280+
}
281+
};
282+
283+
return [queryUri, templates];
284+
}
285+
}

extensions/ql-vscode/src/extension.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ import { DatabaseUI } from './databases-ui';
4141
import {
4242
TemplateQueryDefinitionProvider,
4343
TemplateQueryReferenceProvider,
44-
TemplatePrintAstProvider
44+
TemplatePrintAstProvider,
45+
TemplatePrintCfgProvider
4546
} from './contextual/templateProvider';
4647
import {
4748
DEFAULT_DISTRIBUTION_VERSION_RANGE,
@@ -981,6 +982,26 @@ async function activateWithInstalledDistribution(
981982
})
982983
);
983984

985+
ctx.subscriptions.push(
986+
commandRunnerWithProgress(
987+
'codeQL.viewCfg',
988+
async (
989+
progress: ProgressCallback,
990+
token: CancellationToken
991+
) => {
992+
const res = await new TemplatePrintCfgProvider(cliServer, dbm)
993+
.provideCfgUri(window.activeTextEditor?.document);
994+
if (res) {
995+
await compileAndRunQuery(false, res[0], progress, token, undefined);
996+
}
997+
},
998+
{
999+
title: 'Calculate CFG',
1000+
cancellable: true
1001+
}
1002+
)
1003+
);
1004+
9841005
void logger.log('Starting language server.');
9851006
ctx.subscriptions.push(client.start());
9861007

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 { CompletedQueryInfo, interpretResultsSarif } from './query-results';
36+
import { CompletedQueryInfo, interpretResultsSarif, interpretGraphResults } from './query-results';
3637
import { QueryEvaluationInfo, tmpDir } from './run-queries';
3738
import { parseSarifLocation, parseSarifPlainTextMessage } from './pure/sarif-utils';
3839
import {
@@ -88,12 +89,33 @@ function sortInterpretedResults(
8889
}
8990
}
9091

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

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

99121
export class InterfaceManager extends DisposableObject {
@@ -305,7 +327,7 @@ export class InterfaceManager extends DisposableObject {
305327
await this.changeInterpretedSortState(msg.sortState);
306328
break;
307329
case 'changePage':
308-
if (msg.selectedTable === ALERTS_TABLE_NAME) {
330+
if (msg.selectedTable === ALERTS_TABLE_NAME || msg.selectedTable === GRAPH_TABLE_NAME) {
309331
await this.showPageOfInterpretedResults(msg.pageNumber);
310332
}
311333
else {
@@ -325,8 +347,8 @@ export class InterfaceManager extends DisposableObject {
325347
break;
326348
default:
327349
assertNever(msg);
328-
}
329-
} catch (e) {
350+
}
351+
} catch (e) {
330352
void showAndLogErrorMessage(e.message, {
331353
fullMessage: e.stack
332354
});
@@ -438,7 +460,7 @@ export class InterfaceManager extends DisposableObject {
438460
const parsedResultSets: ParsedResultSets = {
439461
pageNumber: 0,
440462
pageSize,
441-
numPages: numPagesOfResultSet(resultSet),
463+
numPages: numPagesOfResultSet(resultSet, this._interpretation),
442464
numInterpretedPages: numInterpretedPages(this._interpretation),
443465
resultSet: { ...resultSet, t: 'RawResultSet' },
444466
selectedTable: undefined,
@@ -488,7 +510,7 @@ export class InterfaceManager extends DisposableObject {
488510
metadata: this._displayedQuery.completedQuery.query.metadata,
489511
pageNumber,
490512
resultSetNames,
491-
pageSize: PAGE_SIZE.getValue(),
513+
pageSize: interpretedPageSize(this._interpretation),
492514
numPages: numInterpretedPages(this._interpretation),
493515
queryName: this._displayedQuery.label,
494516
queryPath: this._displayedQuery.initialInfo.queryPath
@@ -586,30 +608,50 @@ export class InterfaceManager extends DisposableObject {
586608
sourceInfo: cli.SourceInfo | undefined,
587609
sourceLocationPrefix: string,
588610
sortState: InterpretedResultsSortState | undefined
589-
): Promise<Interpretation | undefined> {
611+
): Promise<Interpretation | undefined> {
590612
if (!resultsPaths) {
591613
void this.logger.log('No results path. Cannot display interpreted results.');
592614
return undefined;
593615
}
616+
let data;
617+
let numTotalResults;
618+
if (metadata?.kind === 'graph')
619+
{
620+
data = await interpretGraphResults(
621+
this.cliServer,
622+
metadata,
623+
resultsPaths,
624+
sourceInfo
625+
);
626+
numTotalResults = data.dot.length;
627+
}
628+
else
629+
{
630+
const sarif = await interpretResultsSarif(
631+
this.cliServer,
632+
metadata,
633+
resultsPaths,
634+
sourceInfo
635+
);
594636

595-
const sarif = await interpretResultsSarif(
596-
this.cliServer,
597-
metadata,
598-
resultsPaths,
599-
sourceInfo
600-
);
637+
sarif.runs.forEach(run => {
638+
if (run.results !== undefined) {
639+
sortInterpretedResults(run.results, sortState);
640+
}
641+
});
601642

602-
sarif.runs.forEach(run => {
603-
if (run.results !== undefined) {
604-
sortInterpretedResults(run.results, sortState);
605-
}
606-
});
643+
sarif.sortState = sortState;
644+
data = sarif;
607645

608-
const numTotalResults = sarif.runs[0]?.results?.length || 0;
646+
numTotalResults = (() => {
647+
if (sarif.runs.length === 0) return 0;
648+
if (sarif.runs[0].results === undefined) return 0;
649+
return sarif.runs[0].results.length;
650+
})();
651+
}
609652

610-
sarif.sortState = sortState;
611653
const interpretation: Interpretation = {
612-
data: sarif,
654+
data,
613655
sourceLocationPrefix,
614656
numTruncatedResults: 0,
615657
numTotalResults

0 commit comments

Comments
 (0)