Skip to content

Commit 98c42a9

Browse files
committed
Add basic "quick query" functionality
1 parent 542470a commit 98c42a9

File tree

3 files changed

+145
-0
lines changed

3 files changed

+145
-0
lines changed

extensions/ql-vscode/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,10 @@
142142
"command": "codeQL.quickEval",
143143
"title": "CodeQL: Quick Evaluation"
144144
},
145+
{
146+
"command": "codeQL.quickQuery",
147+
"title": "CodeQL: Quick Query"
148+
},
145149
{
146150
"command": "codeQL.chooseDatabase",
147151
"title": "CodeQL: Choose Database",

extensions/ql-vscode/src/extension.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { QueryHistoryManager } from './query-history';
1717
import * as qsClient from './queryserver-client';
1818
import { CodeQLCliServer } from './cli';
1919
import { assertNever } from './helpers-pure';
20+
import { displayQuickQuery } from './quick-query';
2021

2122
/**
2223
* extension.ts
@@ -305,6 +306,7 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu
305306

306307
ctx.subscriptions.push(commands.registerCommand('codeQL.runQuery', async (uri: Uri | undefined) => await compileAndRunQuery(false, uri)));
307308
ctx.subscriptions.push(commands.registerCommand('codeQL.quickEval', async (uri: Uri | undefined) => await compileAndRunQuery(true, uri)));
309+
ctx.subscriptions.push(commands.registerCommand('codeQL.quickQuery', async () => displayQuickQuery(ctx, cliServer, databaseUI)));
308310

309311
ctx.subscriptions.push(client.start());
310312
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import * as fs from 'fs-extra';
2+
import * as glob from 'glob-promise';
3+
import * as yaml from 'js-yaml';
4+
import * as path from 'path';
5+
import { ExtensionContext, window as Window, workspace, Uri } from 'vscode';
6+
import { ErrorCodes, ResponseError } from 'vscode-languageclient';
7+
import { CodeQLCliServer } from './cli';
8+
import { DatabaseUI } from './databases-ui';
9+
import * as helpers from './helpers';
10+
import { logger } from './logging';
11+
import { UserCancellationException } from './queries';
12+
13+
const QUICK_QUERIES_DIR_NAME = 'quick-queries';
14+
const QUICK_QUERY_QUERY_NAME = 'quick-query.ql';
15+
16+
async function getQlPackFor(cliServer: CodeQLCliServer, dbschemePath: string): Promise<string> {
17+
const qlpacks = await cliServer.resolveQlpacks(helpers.getOnDiskWorkspaceFolders());
18+
const packs: { packDir: string | undefined, packName: string }[] =
19+
Object.entries(qlpacks).map(([packName, dirs]) => {
20+
if (dirs.length < 1) {
21+
logger.log(`In getQlPackFor ${dbschemePath}, qlpack ${packName} has no directories`);
22+
return { packName, packDir: undefined };
23+
}
24+
if (dirs.length > 1) {
25+
logger.log(`In getQlPackFor ${dbschemePath}, qlpack ${packName} has more than one directory; arbitrarily choosing the first`);
26+
}
27+
return {
28+
packName,
29+
packDir: dirs[0]
30+
}
31+
});
32+
for (const { packDir, packName } of packs) {
33+
if (packDir !== undefined) {
34+
const qlpack = yaml.safeLoad(await fs.readFile(path.join(packDir, 'qlpack.yml'), 'utf8'));
35+
if (qlpack.dbscheme !== undefined && path.basename(qlpack.dbscheme) === path.basename(dbschemePath)) {
36+
return packName;
37+
}
38+
}
39+
}
40+
throw new Error(`Could not find qlpack file for dbscheme ${dbschemePath}`);
41+
}
42+
43+
/**
44+
* `getBaseText` heuristically returns an appropriate import
45+
* statement prelude based on the filename of the dbscheme file
46+
* given. This information might be more appropriately provided by
47+
* the qlpack itself.
48+
*/
49+
function getBaseText(dbschemeBase: string) {
50+
if (dbschemeBase == 'semmlecode.javascript.dbscheme') return 'import javascript\n\nselect ""';
51+
if (dbschemeBase == 'semmlecode.cpp.dbscheme') return 'import cpp\n\nselect ""';
52+
if (dbschemeBase == 'semmlecode.java.dbscheme') return 'import java\n\nselect ""';
53+
if (dbschemeBase == 'semmlecode.python.dbscheme') return 'import python\n\nselect ""';
54+
return '';
55+
}
56+
57+
async function getQuickQueriesDir(ctx: ExtensionContext): Promise<string> {
58+
const storagePath = ctx.storagePath;
59+
if (storagePath === undefined) {
60+
throw new Error('Workspace storage path is undefined');
61+
}
62+
const queriesPath = path.join(storagePath, QUICK_QUERIES_DIR_NAME);
63+
fs.ensureDir(queriesPath, { mode: 0o700 });
64+
return queriesPath;
65+
}
66+
67+
/**
68+
* Show a buffer the user can enter a simple query into.
69+
*/
70+
export async function displayQuickQuery(ctx: ExtensionContext, cliServer: CodeQLCliServer, databaseUI: DatabaseUI) {
71+
try {
72+
73+
// If there is already a quick query open, don't clobber it, just
74+
// show it.
75+
const existing = workspace.textDocuments.find(doc => path.basename(doc.uri.fsPath) === QUICK_QUERY_QUERY_NAME);
76+
if (existing !== undefined) {
77+
Window.showTextDocument(existing);
78+
return;
79+
}
80+
81+
const queriesDir = await getQuickQueriesDir(ctx);
82+
83+
// We need this folder in workspace folders so the language server
84+
// knows how to find its qlpack.yml
85+
if (workspace.workspaceFolders === undefined
86+
|| !workspace.workspaceFolders.some(folder => folder.uri.fsPath === queriesDir)) {
87+
workspace.updateWorkspaceFolders(
88+
(workspace.workspaceFolders || []).length,
89+
0,
90+
{ uri: Uri.file(queriesDir), name: "Quick Queries" }
91+
);
92+
}
93+
94+
// We're going to infer which qlpack to use from the current database
95+
const dbItem = await databaseUI.getDatabaseItem();
96+
if (dbItem === undefined) {
97+
throw new Error('Can\'t start quick query without a selected database');
98+
}
99+
100+
const datasetFolder = await dbItem.getDatasetFolder(cliServer);
101+
const dbschemes = await glob(path.join(datasetFolder, '*.dbscheme'))
102+
103+
if (dbschemes.length < 1) {
104+
throw new Error(`Can't find dbscheme for current database in ${datasetFolder}`);
105+
}
106+
107+
const dbscheme = dbschemes[0];
108+
if (dbschemes.length > 1) {
109+
logger.log(`Found multiple dbschemes in ${datasetFolder}; arbitrarily choosing the first, ${dbscheme}`);
110+
}
111+
112+
const qlpack = await getQlPackFor(cliServer, dbscheme);
113+
const quickQueryQlpackYaml: any = {
114+
name: "quick-query",
115+
version: "1.0.0",
116+
libraryPathDependencies: [qlpack]
117+
};
118+
119+
const qlFile = path.join(queriesDir, QUICK_QUERY_QUERY_NAME);
120+
const qlPackFile = path.join(queriesDir, 'qlpack.yml');
121+
await fs.writeFile(qlFile, getBaseText(path.basename(dbscheme)), 'utf8');
122+
await fs.writeFile(qlPackFile, yaml.safeDump(quickQueryQlpackYaml), 'utf8');
123+
Window.showTextDocument(await workspace.openTextDocument(qlFile));
124+
}
125+
126+
// TODO: clean up error handling for top-level commands like this
127+
catch (e) {
128+
if (e instanceof UserCancellationException) {
129+
logger.log(e.message);
130+
}
131+
else if (e instanceof ResponseError && e.code == ErrorCodes.RequestCancelled) {
132+
logger.log(e.message);
133+
}
134+
else if (e instanceof Error)
135+
helpers.showAndLogErrorMessage(e.message);
136+
else
137+
throw e;
138+
}
139+
}

0 commit comments

Comments
 (0)