diff --git a/extensions/ql-vscode/package.json b/extensions/ql-vscode/package.json index 49c891be5..f30e182a0 100644 --- a/extensions/ql-vscode/package.json +++ b/extensions/ql-vscode/package.json @@ -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" diff --git a/extensions/ql-vscode/src/extension.ts b/extensions/ql-vscode/src/extension.ts index 3b4dfeef0..ae91d9ea3 100644 --- a/extensions/ql-vscode/src/extension.ts +++ b/extensions/ql-vscode/src/extension.ts @@ -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', diff --git a/extensions/ql-vscode/src/files.ts b/extensions/ql-vscode/src/files.ts new file mode 100644 index 000000000..6b0189a2b --- /dev/null +++ b/extensions/ql-vscode/src/files.ts @@ -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 = 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]; +} diff --git a/extensions/ql-vscode/src/run-queries.ts b/extensions/ql-vscode/src/run-queries.ts index 69c1380f4..a629dd93a 100644 --- a/extensions/ql-vscode/src/run-queries.ts +++ b/extensions/ql-vscode/src/run-queries.ts @@ -419,7 +419,6 @@ export async function compileAndRunQueryAgainstDatabase( selectedQueryUri: vscode.Uri | undefined, templates?: messages.TemplateDefinitions, ): Promise { - 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. diff --git a/extensions/ql-vscode/test/data2/empty1.ql b/extensions/ql-vscode/test/data2/empty1.ql new file mode 100644 index 000000000..82198eaf8 --- /dev/null +++ b/extensions/ql-vscode/test/data2/empty1.ql @@ -0,0 +1 @@ +select 1 diff --git a/extensions/ql-vscode/test/data2/not-a-query.txt b/extensions/ql-vscode/test/data2/not-a-query.txt new file mode 100644 index 000000000..e69de29bb diff --git a/extensions/ql-vscode/test/data2/sub-folder/empty2.ql b/extensions/ql-vscode/test/data2/sub-folder/empty2.ql new file mode 100644 index 000000000..82198eaf8 --- /dev/null +++ b/extensions/ql-vscode/test/data2/sub-folder/empty2.ql @@ -0,0 +1 @@ +select 1 diff --git a/extensions/ql-vscode/test/pure-tests/files.test.ts b/extensions/ql-vscode/test/pure-tests/files.test.ts new file mode 100644 index 000000000..597c4cfce --- /dev/null +++ b/extensions/ql-vscode/test/pure-tests/files.test.ts @@ -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]); + }); +});