Add command to run multiple queries at once from file explorer

New command called `codeQL.runQueries`.

When invoked, gather all selected files and folders, and recursively
search for ql files to run. Warn the user if a directory is selected.
See comment inline for reason.
This commit is contained in:
Andrew Eisenberg 2020-06-19 11:52:03 -07:00
Родитель 52c6ee4477
Коммит 3a23f05a0a
8 изменённых файлов: 143 добавлений и 6 удалений

Просмотреть файл

@ -170,6 +170,10 @@
"command": "codeQL.runQuery",
"title": "CodeQL: Run Query"
},
{
"command": "codeQL.runQueries",
"title": "CodeQL: Run Queries in Selected Files"
},
{
"command": "codeQL.quickEval",
"title": "CodeQL: Quick Evaluation"
@ -443,9 +447,8 @@
"when": "resourceScheme == codeql-zip-archive || explorerResourceIsFolder || resourceExtname == .zip"
},
{
"command": "codeQL.runQuery",
"group": "9_qlCommands",
"when": "resourceLangId == ql && resourceExtname == .ql"
"command": "codeQL.runQueries",
"group": "9_qlCommands"
}
],
"commandPalette": [
@ -453,6 +456,10 @@
"command": "codeQL.runQuery",
"when": "resourceLangId == ql && resourceExtname == .ql"
},
{
"command": "codeQL.runQueries",
"when": "false"
},
{
"command": "codeQL.quickEval",
"when": "editorLangId == ql"

Просмотреть файл

@ -1,5 +1,6 @@
import { commands, Disposable, ExtensionContext, extensions, languages, ProgressLocation, ProgressOptions, Uri, window as Window, env } from 'vscode';
import { LanguageClient } from 'vscode-languageclient';
import * as path from 'path';
import { testExplorerExtensionId, TestHub } from 'vscode-test-adapter-api';
import * as archiveFilesystemProvider from './archive-filesystem-provider';
import { CodeQLCliServer } from './cli';
@ -32,6 +33,7 @@ import { compileAndRunQueryAgainstDatabase, tmpDirDisposal, UserCancellationExce
import { QLTestAdapterFactory } from './test-adapter';
import { TestUIService } from './test-ui';
import { CompareInterfaceManager } from './compare/compare-interface';
import { gatherQlFiles } from './files';
/**
* extension.ts
@ -438,6 +440,27 @@ async function activateWithInstalledDistribution(
async (uri: Uri | undefined) => await compileAndRunQuery(false, uri)
)
);
ctx.subscriptions.push(
commands.registerCommand(
'codeQL.runQueries',
async (_: Uri | undefined, multi: Uri[]) => {
const [files, dirFound] = await gatherQlFiles(multi.map(uri => uri.fsPath));
// warn user and display selected files when a directory is selected because some ql
// files may be hidden from the user.
if (dirFound) {
const fileString = files.map(file => path.basename(file)).join(', ');
const res = await helpers.showBinaryChoiceDialog(
`You are about to run ${files.length} queries: ${fileString} Do you want to continue?`
);
if (!res) {
return;
}
}
const queryUris = files.map(path => Uri.parse(`file:${path}`, true));
await Promise.all(queryUris.map(uri => compileAndRunQuery(false, uri)));
}
)
);
ctx.subscriptions.push(
commands.registerCommand(
'codeQL.quickEval',

Просмотреть файл

@ -0,0 +1,30 @@
import * as fs from 'fs-extra';
import * as path from 'path';
/**
* Recursively finds all .ql files in this set of Uris.
*
* @param paths The list of Uris to search through
*
* @returns list of ql files and a boolean describing whether or not a directory was found/
*/
export async function gatherQlFiles(paths: string[]): Promise<[string[], boolean]> {
const gatheredUris: Set<string> = new Set();
let dirFound = false;
for (const nextPath of paths) {
if (
(await fs.pathExists(nextPath)) &&
(await fs.stat(nextPath)).isDirectory()
) {
dirFound = true;
const subPaths = await fs.readdir(nextPath);
const fullPaths = subPaths.map(p => path.join(nextPath, p));
const nestedFiles = (await gatherQlFiles(fullPaths))[0];
nestedFiles.forEach(nested => gatheredUris.add(nested));
} else if (nextPath.endsWith('.ql')) {
gatheredUris.add(nextPath);
}
}
return [Array.from(gatheredUris), dirFound];
}

Просмотреть файл

@ -419,7 +419,6 @@ export async function compileAndRunQueryAgainstDatabase(
selectedQueryUri: vscode.Uri | undefined,
templates?: messages.TemplateDefinitions,
): Promise<QueryWithResults> {
if (!db.contents || !db.contents.dbSchemeUri) {
throw new Error(`Database ${db.databaseUri} does not have a CodeQL database scheme.`);
}
@ -427,12 +426,12 @@ export async function compileAndRunQueryAgainstDatabase(
// Determine which query to run, based on the selection and the active editor.
const { queryPath, quickEvalPosition, quickEvalText } = await determineSelectedQuery(selectedQueryUri, quickEval);
// If this is quick query, store the query text
const historyItemOptions: QueryHistoryItemOptions = {};
historyItemOptions.queryText = await fs.readFile(queryPath, 'utf8');
historyItemOptions.isQuickQuery === isQuickQueryPath(queryPath);
if (quickEval) {
historyItemOptions.queryText = quickEvalText;
} else {
historyItemOptions.queryText = await fs.readFile(queryPath, 'utf8');
}
// Get the workspace folder paths.

Просмотреть файл

@ -0,0 +1 @@
select 1

Просмотреть файл

Просмотреть файл

@ -0,0 +1 @@
select 1

Просмотреть файл

@ -0,0 +1,76 @@
import * as chai from 'chai';
import 'chai/register-should';
import * as sinonChai from 'sinon-chai';
import 'mocha';
import * as path from 'path';
import { gatherQlFiles } from '../../src/files';
chai.use(sinonChai);
const expect = chai.expect;
describe('files', () => {
const dataDir = path.join(path.dirname(__dirname), 'data');
const data2Dir = path.join(path.dirname(__dirname), 'data2');
it('should pass', () => {
expect(true).to.be.eq(true);
});
it('should find one file', async () => {
const singleFile = path.join(dataDir, 'query.ql');
const result = await gatherQlFiles([singleFile]);
expect(result).to.deep.equal([[singleFile], false]);
});
it('should find no files', async () => {
const result = await gatherQlFiles([]);
expect(result).to.deep.equal([[], false]);
});
it('should find no files', async () => {
const singleFile = path.join(dataDir, 'library.qll');
const result = await gatherQlFiles([singleFile]);
expect(result).to.deep.equal([[], false]);
});
it('should handle invalid file', async () => {
const singleFile = path.join(dataDir, 'xxx');
const result = await gatherQlFiles([singleFile]);
expect(result).to.deep.equal([[], false]);
});
it('should find two files', async () => {
const singleFile = path.join(dataDir, 'query.ql');
const otherFile = path.join(dataDir, 'multiple-result-sets.ql');
const notFile = path.join(dataDir, 'library.qll');
const invalidFile = path.join(dataDir, 'xxx');
const result = await gatherQlFiles([singleFile, otherFile, notFile, invalidFile]);
expect(result.sort()).to.deep.equal([[singleFile, otherFile], false]);
});
it('should scan a directory', async () => {
const singleFile = path.join(dataDir, 'query.ql');
const otherFile = path.join(dataDir, 'multiple-result-sets.ql');
const result = await gatherQlFiles([dataDir]);
expect(result.sort()).to.deep.equal([[otherFile, singleFile], true]);
});
it('should scan a directory and some files', async () => {
const singleFile = path.join(dataDir, 'query.ql');
const empty1File = path.join(data2Dir, 'empty1.ql');
const empty2File = path.join(data2Dir, 'sub-folder', 'empty2.ql');
const result = await gatherQlFiles([singleFile, data2Dir]);
expect(result.sort()).to.deep.equal([[singleFile, empty1File, empty2File], true]);
});
it('should avoid duplicates', async () => {
const singleFile = path.join(dataDir, 'query.ql');
const otherFile = path.join(dataDir, 'multiple-result-sets.ql');
const result = await gatherQlFiles([singleFile, dataDir, otherFile]);
expect(result.sort()).to.deep.equal([[singleFile, otherFile], true]);
});
});