Merge pull request #210 from jcreedcmu/jcreed/quick-query

Teach extension to do Quick Query
This commit is contained in:
Alexander Eyers-Taylor
2020-01-24 15:03:56 +00:00
committed by GitHub
8 changed files with 223 additions and 13 deletions

View File

@@ -3,6 +3,8 @@
## 1.0.4
- Disable word-based autocomplete by default.
- Add command `CodeQL: Quick Query` for easy query creation without
having to choose a place in the filesystem to store the query file.
## 1.0.3 - 13 January 2020

View File

@@ -27,6 +27,7 @@
"onCommand:codeQL.setCurrentDatabase",
"onCommand:codeQLDatabases.chooseDatabase",
"onCommand:codeQLDatabases.setCurrentDatabase",
"onCommand:codeQL.quickQuery",
"onWebviewPanel:resultsView",
"onFileSystem:codeql-zip-archive"
],
@@ -142,6 +143,10 @@
"command": "codeQL.quickEval",
"title": "CodeQL: Quick Evaluation"
},
{
"command": "codeQL.quickQuery",
"title": "CodeQL: Quick Query"
},
{
"command": "codeQL.chooseDatabase",
"title": "CodeQL: Choose Database",
@@ -341,6 +346,7 @@
"classnames": "~2.2.6",
"fs-extra": "^8.1.0",
"glob-promise": "^3.4.0",
"js-yaml": "^3.12.0",
"node-fetch": "~2.6.0",
"react": "^16.8.6",
"react-dom": "^16.8.6",
@@ -359,6 +365,7 @@
"@types/glob": "^7.1.1",
"@types/google-protobuf": "^3.2.7",
"@types/gulp": "^4.0.6",
"@types/js-yaml": "~3.12.1",
"@types/jszip": "~3.1.6",
"@types/mocha": "~5.2.7",
"@types/node": "^12.0.8",

View File

@@ -50,6 +50,11 @@ export interface UpgradesInfo {
finalDbscheme: string;
}
/**
* The expected output of `codeql resolve qlpacks`.
*/
export type QlpacksInfo = { [name: string]: string[] };
/**
* The expected output of `codeql resolve metadata`.
*/
@@ -396,7 +401,6 @@ export class CodeQLCliServer implements Disposable {
"Resolving database");
}
/**
* Gets information necessary for upgrading a database.
* @param dbScheme the path to the dbscheme of the database to be upgraded.
@@ -412,6 +416,21 @@ export class CodeQLCliServer implements Disposable {
"Resolving database upgrade scripts",
);
}
/**
* Gets information about available qlpacks
* @param searchPath A list of directories to search for qlpacks
* @returns A dictionary mapping qlpack name to the directory it comes from
*/
resolveQlpacks(searchPath: string[]): Promise<QlpacksInfo> {
const args = ['--additional-packs', searchPath.join(path.delimiter)];
return this.runJsonCodeQlCliCommand<QlpacksInfo>(
['resolve', 'qlpacks'],
args,
"Resolving qlpack information",
);
}
}
/**

View File

@@ -236,6 +236,11 @@ export interface DatabaseItem {
*/
getSourceLocationPrefix(server: cli.CodeQLCliServer): Promise<string>;
/**
* Returns dataset folder of exported database.
*/
getDatasetFolder(server: cli.CodeQLCliServer): Promise<string>;
/**
* Returns the root uri of the virtual filesystem for this database's source archive,
* as displayed in the filesystem explorer.
@@ -385,6 +390,14 @@ class DatabaseItemImpl implements DatabaseItem {
return dbInfo.sourceLocationPrefix;
}
/**
* Returns path to dataset folder of database.
*/
public async getDatasetFolder(server: cli.CodeQLCliServer): Promise<string> {
const dbInfo = await this.getDbInfo(server);
return dbInfo.datasetFolder;
}
/**
* Returns the root uri of the virtual filesystem for this database's source archive.
*/

View File

@@ -17,6 +17,7 @@ import { QueryHistoryManager } from './query-history';
import * as qsClient from './queryserver-client';
import { CodeQLCliServer } from './cli';
import { assertNever } from './helpers-pure';
import { displayQuickQuery } from './quick-query';
/**
* extension.ts
@@ -305,6 +306,7 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu
ctx.subscriptions.push(commands.registerCommand('codeQL.runQuery', async (uri: Uri | undefined) => await compileAndRunQuery(false, uri)));
ctx.subscriptions.push(commands.registerCommand('codeQL.quickEval', async (uri: Uri | undefined) => await compileAndRunQuery(true, uri)));
ctx.subscriptions.push(commands.registerCommand('codeQL.quickQuery', async () => displayQuickQuery(ctx, cliServer, databaseUI)));
ctx.subscriptions.push(client.start());
}

View File

@@ -12,6 +12,8 @@ import { logger } from './logging';
import * as messages from './messages';
import * as qsClient from './queryserver-client';
import { promisify } from 'util';
import { QueryHistoryItemOptions } from './query-history';
import { isQuickQueryPath } from './quick-query';
/**
* queries.ts
@@ -205,6 +207,7 @@ export interface EvaluationInfo {
query: QueryInfo;
result: messages.EvaluationResult;
database: DatabaseInfo;
historyItemOptions: QueryHistoryItemOptions;
}
/**
@@ -393,7 +396,7 @@ export async function clearCacheInDatabase(qs: qsClient.QueryServerClient, dbIte
title: "Clearing Cache",
cancellable: false,
}, (progress, token) =>
qs.sendRequest(messages.clearCache, params, token, progress)
qs.sendRequest(messages.clearCache, params, token, progress)
);
}
@@ -574,6 +577,12 @@ export async function compileAndRunQueryAgainstDatabase(
// Determine which query to run, based on the selection and the active editor.
const { queryPath, quickEvalPosition } = await determineSelectedQuery(selectedQueryUri, quickEval);
// If this is quick query, store the query text
const historyItemOptions: QueryHistoryItemOptions = {};
if (isQuickQueryPath(queryPath)) {
historyItemOptions.queryText = await fs.readFile(queryPath, 'utf8');
}
// Get the workspace folder paths.
const diskWorkspaceFolders = helpers.getOnDiskWorkspaceFolders();
// Figure out the library path for the query.
@@ -616,7 +625,6 @@ export async function compileAndRunQueryAgainstDatabase(
const errors = await query.compile(qs);
if (errors.length == 0) {
const result = await query.run(qs);
return {
@@ -625,7 +633,8 @@ export async function compileAndRunQueryAgainstDatabase(
database: {
name: db.name,
databaseUri: db.databaseUri.toString(true)
}
},
historyItemOptions
};
} else {
// Error dialogs are limited in size and scrollability,
@@ -650,6 +659,7 @@ export async function compileAndRunQueryAgainstDatabase(
" and the query and database use the same target language. For more details on the error, go to View > Output," +
" and choose CodeQL Query Server from the dropdown.");
}
return {
query,
result: {
@@ -662,7 +672,8 @@ export async function compileAndRunQueryAgainstDatabase(
database: {
name: db.name,
databaseUri: db.databaseUri.toString(true)
}
},
historyItemOptions,
};
}
}

View File

@@ -13,6 +13,11 @@ import { QueryHistoryConfig } from './config';
* `TreeDataProvider` subclass below.
*/
export type QueryHistoryItemOptions = {
label?: string, // user-settable label
queryText?: string, // stored query for quick query
}
/**
* One item in the user-displayed list of queries that have been run.
*/
@@ -25,7 +30,7 @@ export class QueryHistoryItem {
constructor(
info: EvaluationInfo,
public config: QueryHistoryConfig,
public label?: string, // user-settable label
public options: QueryHistoryItemOptions = info.historyItemOptions,
) {
this.queryName = helpers.getQueryName(info);
this.databaseName = info.database.name;
@@ -65,8 +70,8 @@ export class QueryHistoryItem {
}
getLabel(): string {
if (this.label !== undefined)
return this.label;
if (this.options.label !== undefined)
return this.options.label;
return this.config.format;
}
@@ -179,9 +184,15 @@ export class QueryHistoryManager {
}
}
async handleOpenQuery(queryHistoryItem: QueryHistoryItem) {
async handleOpenQuery(queryHistoryItem: QueryHistoryItem): Promise<void> {
const textDocument = await vscode.workspace.openTextDocument(vscode.Uri.file(queryHistoryItem.info.query.program.queryPath));
await vscode.window.showTextDocument(textDocument, vscode.ViewColumn.One);
const editor = await vscode.window.showTextDocument(textDocument, vscode.ViewColumn.One);
const queryText = queryHistoryItem.options.queryText;
if (queryText !== undefined) {
await editor.edit(edit => edit.replace(textDocument.validateRange(
new vscode.Range(0, 0, textDocument.lineCount, 0)), queryText)
);
}
}
async handleRemoveHistoryItem(queryHistoryItem: QueryHistoryItem) {
@@ -203,9 +214,9 @@ export class QueryHistoryManager {
if (response !== undefined) {
if (response === '')
// Interpret empty string response as "go back to using default"
queryHistoryItem.label = undefined;
queryHistoryItem.options.label = undefined;
else
queryHistoryItem.label = response;
queryHistoryItem.options.label = response;
this.treeDataProvider.refresh();
}
}
@@ -277,7 +288,7 @@ export class QueryHistoryManager {
const current = this.treeDataProvider.getCurrent();
if (current != undefined) {
// We must fire the onDidChangeTreeData event to ensure the current element can be selected
// using `reveal` if the tree view was not visible when the current element was added.
// using `reveal` if the tree view was not visible when the current element was added.
this.treeDataProvider.refresh();
this.treeView.reveal(current);
}

View File

@@ -0,0 +1,145 @@
import * as fs from 'fs-extra';
import * as glob from 'glob-promise';
import * as yaml from 'js-yaml';
import * as path from 'path';
import { ExtensionContext, window as Window, workspace, Uri } from 'vscode';
import { ErrorCodes, ResponseError } from 'vscode-languageclient';
import { CodeQLCliServer } from './cli';
import { DatabaseUI } from './databases-ui';
import * as helpers from './helpers';
import { logger } from './logging';
import { UserCancellationException } from './queries';
const QUICK_QUERIES_DIR_NAME = 'quick-queries';
const QUICK_QUERY_QUERY_NAME = 'quick-query.ql';
export function isQuickQueryPath(queryPath: string): boolean {
return path.basename(queryPath) === QUICK_QUERY_QUERY_NAME;
}
async function getQlPackFor(cliServer: CodeQLCliServer, dbschemePath: string): Promise<string> {
const qlpacks = await cliServer.resolveQlpacks(helpers.getOnDiskWorkspaceFolders());
const packs: { packDir: string | undefined, packName: string }[] =
Object.entries(qlpacks).map(([packName, dirs]) => {
if (dirs.length < 1) {
logger.log(`In getQlPackFor ${dbschemePath}, qlpack ${packName} has no directories`);
return { packName, packDir: undefined };
}
if (dirs.length > 1) {
logger.log(`In getQlPackFor ${dbschemePath}, qlpack ${packName} has more than one directory; arbitrarily choosing the first`);
}
return {
packName,
packDir: dirs[0]
}
});
for (const { packDir, packName } of packs) {
if (packDir !== undefined) {
const qlpack = yaml.safeLoad(await fs.readFile(path.join(packDir, 'qlpack.yml'), 'utf8'));
if (qlpack.dbscheme !== undefined && path.basename(qlpack.dbscheme) === path.basename(dbschemePath)) {
return packName;
}
}
}
throw new Error(`Could not find qlpack file for dbscheme ${dbschemePath}`);
}
/**
* `getBaseText` heuristically returns an appropriate import statement
* prelude based on the filename of the dbscheme file given. TODO: add
* a 'default import' field to the qlpack itself, and use that.
*/
function getBaseText(dbschemeBase: string) {
if (dbschemeBase == 'semmlecode.javascript.dbscheme') return 'import javascript\n\nselect ""';
if (dbschemeBase == 'semmlecode.cpp.dbscheme') return 'import cpp\n\nselect ""';
if (dbschemeBase == 'semmlecode.dbscheme') return 'import java\n\nselect ""';
if (dbschemeBase == 'semmlecode.python.dbscheme') return 'import python\n\nselect ""';
if (dbschemeBase == 'semmlecode.csharp.dbscheme') return 'import csharp\n\nselect ""';
if (dbschemeBase == 'go.dbscheme') return 'import go\n\nselect ""';
return 'select ""';
}
async function getQuickQueriesDir(ctx: ExtensionContext): Promise<string> {
const storagePath = ctx.storagePath;
if (storagePath === undefined) {
throw new Error('Workspace storage path is undefined');
}
const queriesPath = path.join(storagePath, QUICK_QUERIES_DIR_NAME);
fs.ensureDir(queriesPath, { mode: 0o700 });
return queriesPath;
}
/**
* Show a buffer the user can enter a simple query into.
*/
export async function displayQuickQuery(ctx: ExtensionContext, cliServer: CodeQLCliServer, databaseUI: DatabaseUI) {
try {
// If there is already a quick query open, don't clobber it, just
// show it.
const existing = workspace.textDocuments.find(doc => path.basename(doc.uri.fsPath) === QUICK_QUERY_QUERY_NAME);
if (existing !== undefined) {
Window.showTextDocument(existing);
return;
}
const queriesDir = await getQuickQueriesDir(ctx);
// We need this folder in workspace folders so the language server
// knows how to find its qlpack.yml
if (workspace.workspaceFolders === undefined
|| !workspace.workspaceFolders.some(folder => folder.uri.fsPath === queriesDir)) {
workspace.updateWorkspaceFolders(
(workspace.workspaceFolders || []).length,
0,
{ uri: Uri.file(queriesDir), name: "Quick Queries" }
);
}
// We're going to infer which qlpack to use from the current database
const dbItem = await databaseUI.getDatabaseItem();
if (dbItem === undefined) {
throw new Error('Can\'t start quick query without a selected database');
}
const datasetFolder = await dbItem.getDatasetFolder(cliServer);
const dbschemes = await glob(path.join(datasetFolder, '*.dbscheme'))
if (dbschemes.length < 1) {
throw new Error(`Can't find dbscheme for current database in ${datasetFolder}`);
}
dbschemes.sort();
const dbscheme = dbschemes[0];
if (dbschemes.length > 1) {
Window.showErrorMessage(`Found multiple dbschemes in ${datasetFolder} during quick query; arbitrarily choosing the first, ${dbscheme}, to decide what library to use.`);
}
const qlpack = await getQlPackFor(cliServer, dbscheme);
const quickQueryQlpackYaml: any = {
name: "quick-query",
version: "1.0.0",
libraryPathDependencies: [qlpack]
};
const qlFile = path.join(queriesDir, QUICK_QUERY_QUERY_NAME);
const qlPackFile = path.join(queriesDir, 'qlpack.yml');
await fs.writeFile(qlFile, getBaseText(path.basename(dbscheme)), 'utf8');
await fs.writeFile(qlPackFile, yaml.safeDump(quickQueryQlpackYaml), 'utf8');
Window.showTextDocument(await workspace.openTextDocument(qlFile));
}
// TODO: clean up error handling for top-level commands like this
catch (e) {
if (e instanceof UserCancellationException) {
logger.log(e.message);
}
else if (e instanceof ResponseError && e.code == ErrorCodes.RequestCancelled) {
logger.log(e.message);
}
else if (e instanceof Error)
helpers.showAndLogErrorMessage(e.message);
else
throw e;
}
}