Skip to content

Commit 80c6ea6

Browse files
committed
Add the AST Viewer
This commit adds the AST Viewer for viewing the QL AST of a file in a database. The different components are as follows: 1. There is a new view `codeQLAstViewer`, which displays the AST 2. This view is backed by the `AstViewerDataProvider` and `AstViewer` classes in astView.ts 3. To generate an AST, we use contextual queries, similar to how Find references/declarations are implemented. In particular, in `definitions.ts` there is `TemplatePrintAstProvider` which provides an AST for a given source buffer. - Similar to the other queries, we first determine which database the buffer belongs to. - Based on that, we generate a synthetic qlpack and run the templatized `printAst.ql` query - We plug in the archive-relative path name of the source file. - After the query is run, we wrap the results in an `AstBuilder` instance. - When requested, the `AstBuilder` will generate the full AST of the file from the BQRS results. - The AST roots (all top-level elements, functions, variable declarations, etc, are roots) are passed to the `AstViewer` instance, which handles the display lifecycle and other VS Code-specific functions. There are a few unrelated pieces here, which can be pulled out to another PR if required: - The `codeQLQueryHistory` view now has a _welcome_ message to make it more obvious to users how to start. - `definitions.ts` is moved to the `contextual` subfolder. - `fileRangeFromURI` is extracted from `definitions.ts` to its own file so it can be reused. Also, note that this relies on github/codeql#3931 for the C/C++ query to be available in the QL sources. Other languages will need similar queries.
1 parent 2243c21 commit 80c6ea6

File tree

16 files changed

+1678
-68
lines changed

16 files changed

+1678
-68
lines changed
Lines changed: 7 additions & 0 deletions
Loading
Lines changed: 7 additions & 0 deletions
Loading

extensions/ql-vscode/package.json

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,15 @@
2525
"onLanguage:ql",
2626
"onView:codeQLDatabases",
2727
"onView:codeQLQueryHistory",
28+
"onView:codeQLAstViewer",
2829
"onView:test-explorer",
2930
"onCommand:codeQL.checkForUpdatesToCLI",
3031
"onCommand:codeQLDatabases.chooseDatabaseFolder",
3132
"onCommand:codeQLDatabases.chooseDatabaseArchive",
3233
"onCommand:codeQLDatabases.chooseDatabaseInternet",
3334
"onCommand:codeQLDatabases.chooseDatabaseLgtm",
3435
"onCommand:codeQL.setCurrentDatabase",
36+
"onCommand:codeQL.viewAst",
3537
"onCommand:codeQL.chooseDatabaseFolder",
3638
"onCommand:codeQL.chooseDatabaseArchive",
3739
"onCommand:codeQL.chooseDatabaseInternet",
@@ -218,6 +220,10 @@
218220
"command": "codeQL.setCurrentDatabase",
219221
"title": "CodeQL: Set Current Database"
220222
},
223+
{
224+
"command": "codeQL.viewAst",
225+
"title": "CodeQL: View AST"
226+
},
221227
{
222228
"command": "codeQL.upgradeCurrentDatabase",
223229
"title": "CodeQL: Upgrade Current Database"
@@ -333,6 +339,18 @@
333339
{
334340
"command": "codeQLTests.acceptOutput",
335341
"title": "CodeQL: Accept Test Output"
342+
},
343+
{
344+
"command": "codeQLAstViewer.gotoCode",
345+
"title": "Go To Code"
346+
},
347+
{
348+
"command": "codeQLAstViewer.clear",
349+
"title": "Clear AST",
350+
"icon": {
351+
"light": "media/light/clear-all.svg",
352+
"dark": "media/dark/clear-all.svg"
353+
}
336354
}
337355
],
338356
"menus": {
@@ -366,6 +384,11 @@
366384
"command": "codeQLDatabases.chooseDatabaseLgtm",
367385
"when": "view == codeQLDatabases",
368386
"group": "navigation"
387+
},
388+
{
389+
"command": "codeQLAstViewer.clear",
390+
"when": "view == codeQLAstViewer",
391+
"group": "navigation"
369392
}
370393
],
371394
"view/item/context": [
@@ -446,6 +469,11 @@
446469
"group": "9_qlCommands",
447470
"when": "resourceScheme == codeql-zip-archive || explorerResourceIsFolder || resourceExtname == .zip"
448471
},
472+
{
473+
"command": "codeQL.viewAst",
474+
"group": "9_qlCommands",
475+
"when": "resourceScheme == codeql-zip-archive"
476+
},
449477
{
450478
"command": "codeQL.runQueries",
451479
"group": "9_qlCommands"
@@ -468,6 +496,10 @@
468496
"command": "codeQL.setCurrentDatabase",
469497
"when": "false"
470498
},
499+
{
500+
"command": "codeQL.viewAst",
501+
"when": "resourceScheme == codeql-zip-archive"
502+
},
471503
{
472504
"command": "codeQLDatabases.setCurrentDatabase",
473505
"when": "false"
@@ -543,6 +575,14 @@
543575
{
544576
"command": "codeQLQueryHistory.compareWith",
545577
"when": "false"
578+
},
579+
{
580+
"command": "codeQLAstViewer.gotoCode",
581+
"when": "false"
582+
},
583+
{
584+
"command": "codeQLAstViewer.clear",
585+
"when": "false"
546586
}
547587
],
548588
"editor/context": [
@@ -574,9 +614,23 @@
574614
{
575615
"id": "codeQLQueryHistory",
576616
"name": "Query History"
617+
},
618+
{
619+
"id": "codeQLAstViewer",
620+
"name": "AST Viewer"
577621
}
578622
]
579-
}
623+
},
624+
"viewsWelcome": [
625+
{
626+
"view": "codeQLAstViewer",
627+
"contents": "Run the 'CodeQL: View AST' command on an open source file from a Code QL database.\n[View AST](command:codeQL.viewAst)"
628+
},
629+
{
630+
"view": "codeQLQueryHistory",
631+
"contents": "Run the 'CodeQL: Run Query' command on QL query.\n[Run Query](command:codeQL.runQuery)"
632+
}
633+
]
580634
},
581635
"scripts": {
582636
"build": "gulp",
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import * as vscode from 'vscode';
2+
3+
import { DatabaseItem } from './databases';
4+
import { UrlValue, BqrsId } from './bqrs-cli-types';
5+
import fileRangeFromURI from './contextual/fileRangeFromURI';
6+
import { showLocation } from './interface-utils';
7+
8+
export interface AstItem {
9+
id: BqrsId;
10+
label?: string;
11+
location?: UrlValue;
12+
parent: AstItem | RootAstItem;
13+
children: AstItem[];
14+
order: number;
15+
}
16+
17+
export type RootAstItem = Omit<AstItem, 'parent'>;
18+
19+
class AstViewerDataProvider implements vscode.TreeDataProvider<AstItem | RootAstItem> {
20+
21+
public roots: RootAstItem[] = [];
22+
public db: DatabaseItem | undefined;
23+
24+
private _onDidChangeTreeData =
25+
new vscode.EventEmitter<AstItem | undefined>();
26+
readonly onDidChangeTreeData: vscode.Event<AstItem | undefined> =
27+
this._onDidChangeTreeData.event;
28+
29+
constructor() {
30+
vscode.commands.registerCommand('codeQLAstViewer.gotoCode',
31+
async (location: UrlValue, db: DatabaseItem) => {
32+
if (location) {
33+
await showLocation(fileRangeFromURI(location, db));
34+
}
35+
});
36+
}
37+
38+
refresh(): void {
39+
this._onDidChangeTreeData.fire();
40+
}
41+
getChildren(item?: AstItem): vscode.ProviderResult<(AstItem | RootAstItem)[]> {
42+
const children = item ? item.children : this.roots;
43+
return children.sort((c1, c2) => (c1.order - c2.order));
44+
}
45+
46+
getParent(item: AstItem): vscode.ProviderResult<AstItem> {
47+
return item.parent as AstItem;
48+
}
49+
50+
getTreeItem(item: AstItem): vscode.TreeItem {
51+
const line = typeof item.location === 'string'
52+
? item.location
53+
: item.location?.startLine;
54+
55+
const state = item.children.length
56+
? vscode.TreeItemCollapsibleState.Collapsed
57+
: vscode.TreeItemCollapsibleState.None;
58+
const treeItem = new vscode.TreeItem(item.label || '', state);
59+
treeItem.description = line ? `Line ${line}` : '';
60+
treeItem.id = String(item.id);
61+
treeItem.tooltip = `${treeItem.description} ${treeItem.label}`;
62+
treeItem.command = {
63+
command: 'codeQLAstViewer.gotoCode',
64+
title: 'Go To Code',
65+
tooltip: `Go To ${item.location}`,
66+
arguments: [item.location, this.db]
67+
};
68+
return treeItem;
69+
}
70+
}
71+
72+
export class AstViewer {
73+
private treeView: vscode.TreeView<AstItem | RootAstItem>;
74+
private treeDataProvider: AstViewerDataProvider;
75+
76+
constructor() {
77+
this.treeDataProvider = new AstViewerDataProvider();
78+
this.treeView = vscode.window.createTreeView('codeQLAstViewer', {
79+
treeDataProvider: this.treeDataProvider,
80+
showCollapseAll: true
81+
});
82+
83+
vscode.commands.registerCommand('codeQLAstViewer.clear', () => {
84+
this.clear();
85+
});
86+
}
87+
88+
updateRoots(roots: RootAstItem[], db: DatabaseItem, fileName: string) {
89+
this.treeDataProvider.roots = roots;
90+
this.treeDataProvider.db = db;
91+
this.treeDataProvider.refresh();
92+
this.treeView.message = `AST for ${fileName}`;
93+
this.treeView.reveal(roots[0], { focus: true });
94+
}
95+
96+
private clear() {
97+
this.treeDataProvider.roots = [];
98+
this.treeDataProvider.db = undefined;
99+
this.treeDataProvider.refresh();
100+
this.treeView.message = undefined;
101+
}
102+
}

extensions/ql-vscode/src/bqrs-cli-types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,12 @@ export interface BQRSInfo {
5353
'result-sets': ResultSetSchema[];
5454
}
5555

56+
export type BqrsId = number;
57+
5658
export interface EntityValue {
5759
url?: UrlValue;
5860
label?: string;
61+
id?: BqrsId;
5962
}
6063

6164
export interface LineColumnLocation {

extensions/ql-vscode/src/cli.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -499,7 +499,7 @@ export class CodeQLCliServer implements Disposable {
499499
*/
500500
async bqrsDecode(bqrsPath: string, resultSet: string, pageSize?: number, offset?: number): Promise<DecodedBqrsChunk> {
501501
const subcommandArgs = [
502-
'--entities=url,string',
502+
'--entities=id,url,string',
503503
'--result-set', resultSet,
504504
].concat(
505505
pageSize ? ['--rows', pageSize.toString()] : []
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { QueryWithResults } from '../run-queries';
2+
import { CodeQLCliServer } from '../cli';
3+
import { DecodedBqrsChunk, BqrsId, EntityValue } from '../bqrs-cli-types';
4+
import { DatabaseItem } from '../databases';
5+
import { AstItem, RootAstItem } from '../astViewer';
6+
7+
/**
8+
* A class that wraps a tree of QL results from a query that
9+
* has an @kind of graph
10+
*/
11+
// RENAME to ASTParser / ASTCreator
12+
export default class AstBuilder {
13+
14+
private roots: RootAstItem[] | undefined;
15+
private bqrsPath: string;
16+
constructor(
17+
queryResults: QueryWithResults,
18+
private cli: CodeQLCliServer,
19+
public db: DatabaseItem,
20+
public fileName: string
21+
) {
22+
this.bqrsPath = queryResults.query.resultsPaths.resultsPath;
23+
}
24+
25+
async getRoots(): Promise<RootAstItem[]> {
26+
if (!this.roots) {
27+
this.roots = await this.parseRoots();
28+
}
29+
return this.roots;
30+
}
31+
32+
private async parseRoots(): Promise<RootAstItem[]> {
33+
const [nodeTuples, edgeTuples, graphProperties] = await Promise.all([
34+
await this.cli.bqrsDecode(this.bqrsPath, 'nodes'),
35+
await this.cli.bqrsDecode(this.bqrsPath, 'edges'),
36+
await this.cli.bqrsDecode(this.bqrsPath, 'graphProperties'),
37+
]);
38+
39+
if (!this.isValidGraph(graphProperties)) {
40+
throw new Error('AST is invalid');
41+
}
42+
43+
const idToItem = new Map<BqrsId, AstItem | RootAstItem>();
44+
const parentToChildren = new Map<BqrsId, BqrsId[]>();
45+
const childToParent = new Map<BqrsId, BqrsId>();
46+
const astOrder = new Map<BqrsId, number>();
47+
const roots = [];
48+
49+
// Build up the parent-child relationships
50+
edgeTuples.tuples.forEach(tuple => {
51+
const from = tuple[0] as EntityValue;
52+
const to = tuple[1] as EntityValue;
53+
const toId = to.id!;
54+
const fromId = from.id!;
55+
56+
if (tuple[2] === 'semmle.order') {
57+
astOrder.set(toId, Number(tuple[3]));
58+
} else if (tuple[2] === 'semmle.label') {
59+
childToParent.set(toId, fromId);
60+
let children = parentToChildren.get(fromId);
61+
if (!children) {
62+
parentToChildren.set(fromId, children = []);
63+
}
64+
children.push(toId);
65+
}
66+
});
67+
68+
// populate parents and children
69+
nodeTuples.tuples.forEach(tuple => {
70+
const entity = tuple[0] as EntityValue;
71+
const id = entity.id!;
72+
73+
if (tuple[1] === 'semmle.order') {
74+
astOrder.set(id, Number(tuple[2]));
75+
76+
} else if (tuple[1] === 'semmle.label') {
77+
const item = {
78+
id,
79+
label: entity.label,
80+
location: entity.url,
81+
children: [] as AstItem[],
82+
order: Number.MAX_SAFE_INTEGER
83+
};
84+
85+
idToItem.set(id, item as RootAstItem);
86+
const parent = idToItem.get(childToParent.get(id) || -1);
87+
88+
if (parent) {
89+
const astItem = item as AstItem;
90+
astItem.parent = parent;
91+
parent.children.push(astItem);
92+
}
93+
const children = parentToChildren.get(id) || [];
94+
children.forEach(childId => {
95+
const child = idToItem.get(childId) as AstItem | undefined;
96+
if (child) {
97+
child.parent = item;
98+
item.children.push(child);
99+
}
100+
});
101+
}
102+
});
103+
104+
// find the roots and add the order
105+
for(const [, item] of idToItem) {
106+
item.order = astOrder.has(item.id)
107+
? astOrder.get(item.id)!
108+
: Number.MAX_SAFE_INTEGER;
109+
110+
if (!('parent' in item)) {
111+
roots.push(item);
112+
}
113+
}
114+
return roots;
115+
}
116+
117+
private isValidGraph(graphProperties: DecodedBqrsChunk) {
118+
const tuple = graphProperties?.tuples?.find(t => t[0] === 'semmle.graphKind');
119+
return tuple?.[1] === 'tree';
120+
}
121+
}

0 commit comments

Comments
 (0)