Skip to content

Commit 3a23f05

Browse files
committed
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.
1 parent 52c6ee4 commit 3a23f05

File tree

8 files changed

+143
-6
lines changed

8 files changed

+143
-6
lines changed

extensions/ql-vscode/package.json

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,10 @@
170170
"command": "codeQL.runQuery",
171171
"title": "CodeQL: Run Query"
172172
},
173+
{
174+
"command": "codeQL.runQueries",
175+
"title": "CodeQL: Run Queries in Selected Files"
176+
},
173177
{
174178
"command": "codeQL.quickEval",
175179
"title": "CodeQL: Quick Evaluation"
@@ -443,16 +447,19 @@
443447
"when": "resourceScheme == codeql-zip-archive || explorerResourceIsFolder || resourceExtname == .zip"
444448
},
445449
{
446-
"command": "codeQL.runQuery",
447-
"group": "9_qlCommands",
448-
"when": "resourceLangId == ql && resourceExtname == .ql"
450+
"command": "codeQL.runQueries",
451+
"group": "9_qlCommands"
449452
}
450453
],
451454
"commandPalette": [
452455
{
453456
"command": "codeQL.runQuery",
454457
"when": "resourceLangId == ql && resourceExtname == .ql"
455458
},
459+
{
460+
"command": "codeQL.runQueries",
461+
"when": "false"
462+
},
456463
{
457464
"command": "codeQL.quickEval",
458465
"when": "editorLangId == ql"

extensions/ql-vscode/src/extension.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { commands, Disposable, ExtensionContext, extensions, languages, ProgressLocation, ProgressOptions, Uri, window as Window, env } from 'vscode';
22
import { LanguageClient } from 'vscode-languageclient';
3+
import * as path from 'path';
34
import { testExplorerExtensionId, TestHub } from 'vscode-test-adapter-api';
45
import * as archiveFilesystemProvider from './archive-filesystem-provider';
56
import { CodeQLCliServer } from './cli';
@@ -32,6 +33,7 @@ import { compileAndRunQueryAgainstDatabase, tmpDirDisposal, UserCancellationExce
3233
import { QLTestAdapterFactory } from './test-adapter';
3334
import { TestUIService } from './test-ui';
3435
import { CompareInterfaceManager } from './compare/compare-interface';
36+
import { gatherQlFiles } from './files';
3537

3638
/**
3739
* extension.ts
@@ -438,6 +440,27 @@ async function activateWithInstalledDistribution(
438440
async (uri: Uri | undefined) => await compileAndRunQuery(false, uri)
439441
)
440442
);
443+
ctx.subscriptions.push(
444+
commands.registerCommand(
445+
'codeQL.runQueries',
446+
async (_: Uri | undefined, multi: Uri[]) => {
447+
const [files, dirFound] = await gatherQlFiles(multi.map(uri => uri.fsPath));
448+
// warn user and display selected files when a directory is selected because some ql
449+
// files may be hidden from the user.
450+
if (dirFound) {
451+
const fileString = files.map(file => path.basename(file)).join(', ');
452+
const res = await helpers.showBinaryChoiceDialog(
453+
`You are about to run ${files.length} queries: ${fileString} Do you want to continue?`
454+
);
455+
if (!res) {
456+
return;
457+
}
458+
}
459+
const queryUris = files.map(path => Uri.parse(`file:${path}`, true));
460+
await Promise.all(queryUris.map(uri => compileAndRunQuery(false, uri)));
461+
}
462+
)
463+
);
441464
ctx.subscriptions.push(
442465
commands.registerCommand(
443466
'codeQL.quickEval',

extensions/ql-vscode/src/files.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import * as fs from 'fs-extra';
2+
import * as path from 'path';
3+
4+
5+
/**
6+
* Recursively finds all .ql files in this set of Uris.
7+
*
8+
* @param paths The list of Uris to search through
9+
*
10+
* @returns list of ql files and a boolean describing whether or not a directory was found/
11+
*/
12+
export async function gatherQlFiles(paths: string[]): Promise<[string[], boolean]> {
13+
const gatheredUris: Set<string> = new Set();
14+
let dirFound = false;
15+
for (const nextPath of paths) {
16+
if (
17+
(await fs.pathExists(nextPath)) &&
18+
(await fs.stat(nextPath)).isDirectory()
19+
) {
20+
dirFound = true;
21+
const subPaths = await fs.readdir(nextPath);
22+
const fullPaths = subPaths.map(p => path.join(nextPath, p));
23+
const nestedFiles = (await gatherQlFiles(fullPaths))[0];
24+
nestedFiles.forEach(nested => gatheredUris.add(nested));
25+
} else if (nextPath.endsWith('.ql')) {
26+
gatheredUris.add(nextPath);
27+
}
28+
}
29+
return [Array.from(gatheredUris), dirFound];
30+
}

extensions/ql-vscode/src/run-queries.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -419,20 +419,19 @@ export async function compileAndRunQueryAgainstDatabase(
419419
selectedQueryUri: vscode.Uri | undefined,
420420
templates?: messages.TemplateDefinitions,
421421
): Promise<QueryWithResults> {
422-
423422
if (!db.contents || !db.contents.dbSchemeUri) {
424423
throw new Error(`Database ${db.databaseUri} does not have a CodeQL database scheme.`);
425424
}
426425

427426
// Determine which query to run, based on the selection and the active editor.
428427
const { queryPath, quickEvalPosition, quickEvalText } = await determineSelectedQuery(selectedQueryUri, quickEval);
429428

430-
// If this is quick query, store the query text
431429
const historyItemOptions: QueryHistoryItemOptions = {};
432-
historyItemOptions.queryText = await fs.readFile(queryPath, 'utf8');
433430
historyItemOptions.isQuickQuery === isQuickQueryPath(queryPath);
434431
if (quickEval) {
435432
historyItemOptions.queryText = quickEvalText;
433+
} else {
434+
historyItemOptions.queryText = await fs.readFile(queryPath, 'utf8');
436435
}
437436

438437
// Get the workspace folder paths.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
select 1

extensions/ql-vscode/test/data2/not-a-query.txt

Whitespace-only changes.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
select 1
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import * as chai from 'chai';
2+
import 'chai/register-should';
3+
import * as sinonChai from 'sinon-chai';
4+
import 'mocha';
5+
import * as path from 'path';
6+
7+
import { gatherQlFiles } from '../../src/files';
8+
9+
chai.use(sinonChai);
10+
const expect = chai.expect;
11+
12+
describe('files', () => {
13+
const dataDir = path.join(path.dirname(__dirname), 'data');
14+
const data2Dir = path.join(path.dirname(__dirname), 'data2');
15+
16+
it('should pass', () => {
17+
expect(true).to.be.eq(true);
18+
});
19+
it('should find one file', async () => {
20+
const singleFile = path.join(dataDir, 'query.ql');
21+
const result = await gatherQlFiles([singleFile]);
22+
expect(result).to.deep.equal([[singleFile], false]);
23+
});
24+
25+
it('should find no files', async () => {
26+
const result = await gatherQlFiles([]);
27+
expect(result).to.deep.equal([[], false]);
28+
});
29+
30+
it('should find no files', async () => {
31+
const singleFile = path.join(dataDir, 'library.qll');
32+
const result = await gatherQlFiles([singleFile]);
33+
expect(result).to.deep.equal([[], false]);
34+
});
35+
36+
it('should handle invalid file', async () => {
37+
const singleFile = path.join(dataDir, 'xxx');
38+
const result = await gatherQlFiles([singleFile]);
39+
expect(result).to.deep.equal([[], false]);
40+
});
41+
42+
it('should find two files', async () => {
43+
const singleFile = path.join(dataDir, 'query.ql');
44+
const otherFile = path.join(dataDir, 'multiple-result-sets.ql');
45+
const notFile = path.join(dataDir, 'library.qll');
46+
const invalidFile = path.join(dataDir, 'xxx');
47+
48+
const result = await gatherQlFiles([singleFile, otherFile, notFile, invalidFile]);
49+
expect(result.sort()).to.deep.equal([[singleFile, otherFile], false]);
50+
});
51+
52+
it('should scan a directory', async () => {
53+
const singleFile = path.join(dataDir, 'query.ql');
54+
const otherFile = path.join(dataDir, 'multiple-result-sets.ql');
55+
56+
const result = await gatherQlFiles([dataDir]);
57+
expect(result.sort()).to.deep.equal([[otherFile, singleFile], true]);
58+
});
59+
60+
it('should scan a directory and some files', async () => {
61+
const singleFile = path.join(dataDir, 'query.ql');
62+
const empty1File = path.join(data2Dir, 'empty1.ql');
63+
const empty2File = path.join(data2Dir, 'sub-folder', 'empty2.ql');
64+
65+
const result = await gatherQlFiles([singleFile, data2Dir]);
66+
expect(result.sort()).to.deep.equal([[singleFile, empty1File, empty2File], true]);
67+
});
68+
69+
it('should avoid duplicates', async () => {
70+
const singleFile = path.join(dataDir, 'query.ql');
71+
const otherFile = path.join(dataDir, 'multiple-result-sets.ql');
72+
73+
const result = await gatherQlFiles([singleFile, dataDir, otherFile]);
74+
expect(result.sort()).to.deep.equal([[singleFile, otherFile], true]);
75+
});
76+
});

0 commit comments

Comments
 (0)