diff --git a/extensions/ql-vscode/CHANGELOG.md b/extensions/ql-vscode/CHANGELOG.md index e420de7a165..3333ef44e78 100644 --- a/extensions/ql-vscode/CHANGELOG.md +++ b/extensions/ql-vscode/CHANGELOG.md @@ -3,6 +3,7 @@ ## [UNRELEASED] - Add a command _CodeQL: Run Query on Multiple Databases_, which lets users select multiple databases to run a query on. [#898](https://github.com/github/vscode-codeql/pull/898) +- Autodetect what language a query targets. This refines the _CodeQL: Run Query on Multiple Databases_ command to only show relevant databases. [#915](https://github.com/github/vscode-codeql/pull/915) ## 1.5.2 - 13 July 2021 diff --git a/extensions/ql-vscode/src/cli.ts b/extensions/ql-vscode/src/cli.ts index 99381fb5178..5319d333360 100644 --- a/extensions/ql-vscode/src/cli.ts +++ b/extensions/ql-vscode/src/cli.ts @@ -8,7 +8,7 @@ import { Readable } from 'stream'; import { StringDecoder } from 'string_decoder'; import * as tk from 'tree-kill'; import { promisify } from 'util'; -import { CancellationToken, Disposable } from 'vscode'; +import { CancellationToken, Disposable, Uri } from 'vscode'; import { BQRSInfo, DecodedBqrsChunk } from './pure/bqrs-cli-types'; import { CliConfig } from './config'; @@ -43,6 +43,16 @@ export interface QuerySetup { compilationCache?: string; } +/** + * The expected output of `codeql resolve queries --format bylanguage`. + */ +export interface QueryInfoByLanguage { + // Using `unknown` as a placeholder. For now, the value is only ever an empty object. + byLanguage: Record>; + noDeclaredLanguage: Record; + multipleDeclaredLanguages: Record; +} + /** * The expected output of `codeql resolve database`. */ @@ -71,6 +81,11 @@ export interface UpgradesInfo { */ export type QlpacksInfo = { [name: string]: string[] }; +/** + * The expected output of `codeql resolve languages`. + */ +export type LanguagesInfo = { [name: string]: string[] }; + /** * The expected output of `codeql resolve qlref`. */ @@ -482,6 +497,20 @@ export class CodeQLCliServer implements Disposable { return await this.runJsonCodeQlCliCommand(['resolve', 'library-path'], subcommandArgs, 'Resolving library paths'); } + /** + * Resolves the language for a query. + * @param queryUri The URI of the query + */ + async resolveQueryByLanguage(workspaces: string[], queryUri: Uri): Promise { + const subcommandArgs = [ + '--format', 'bylanguage', + queryUri.fsPath, + '--additional-packs', + workspaces.join(path.delimiter) + ]; + return JSON.parse(await this.runCodeQlCliCommand(['resolve', 'queries'], subcommandArgs, 'Resolving query by language')); + } + /** * Finds all available QL tests in a given directory. * @param testPath Root of directory tree to search for tests. @@ -724,6 +753,14 @@ export class CodeQLCliServer implements Disposable { ); } + /** + * Gets information about the available languages. + * @returns A dictionary mapping language name to the directory it comes from + */ + async resolveLanguages(): Promise { + return await this.runJsonCodeQlCliCommand(['resolve', 'languages'], [], 'Resolving languages'); + } + /** * Gets information about queries in a query suite. * @param suite The suite to resolve. diff --git a/extensions/ql-vscode/src/extension.ts b/extensions/ql-vscode/src/extension.ts index 0f2f2f47be6..b760d923ad0 100644 --- a/extensions/ql-vscode/src/extension.ts +++ b/extensions/ql-vscode/src/extension.ts @@ -73,7 +73,7 @@ import { import { CodeQlStatusBarHandler } from './status-bar'; import { Credentials } from './authentication'; -import runRemoteQuery from './run-remote-query'; +import { runRemoteQuery, findLanguage } from './run-remote-query'; /** * extension.ts @@ -570,7 +570,21 @@ async function activateWithInstalledDistribution( token: CancellationToken, uri: Uri | undefined ) => { - const quickPickItems = dbm.databaseItems.map(dbItem => ( + let filteredDBs = dbm.databaseItems; + if (filteredDBs.length === 0) { + void helpers.showAndLogErrorMessage('No databases found. Please add a suitable database to your workspace.'); + return; + } + // If possible, only show databases with the right language (otherwise show all databases). + const queryLanguage = await findLanguage(cliServer, uri); + if (queryLanguage) { + filteredDBs = dbm.databaseItems.filter(db => db.language === queryLanguage); + if (filteredDBs.length === 0) { + void helpers.showAndLogErrorMessage(`No databases found for language ${queryLanguage}. Please add a suitable database to your workspace.`); + return; + } + } + const quickPickItems = filteredDBs.map(dbItem => ( { databaseItem: dbItem, label: dbItem.name, @@ -582,7 +596,7 @@ async function activateWithInstalledDistribution( */ const quickpick = await window.showQuickPick( quickPickItems, - { canPickMany: true } + { canPickMany: true, ignoreFocusOut: true } ); if (quickpick !== undefined) { // Collect all skipped databases and display them at the end (instead of popping up individual errors) @@ -707,7 +721,7 @@ async function activateWithInstalledDistribution( ) => { if (isCanary()) { const credentials = await Credentials.initialize(ctx); - await runRemoteQuery(credentials, uri || window.activeTextEditor?.document.uri); + await runRemoteQuery(cliServer, credentials, uri || window.activeTextEditor?.document.uri); } }) ); diff --git a/extensions/ql-vscode/src/run-remote-query.ts b/extensions/ql-vscode/src/run-remote-query.ts index d46ca3748ec..443c37d20e9 100644 --- a/extensions/ql-vscode/src/run-remote-query.ts +++ b/extensions/ql-vscode/src/run-remote-query.ts @@ -1,20 +1,53 @@ -import { Uri } from 'vscode'; +import { Uri, window } from 'vscode'; import * as yaml from 'js-yaml'; import * as fs from 'fs-extra'; -import { showAndLogErrorMessage, showAndLogInformationMessage } from './helpers'; +import { getOnDiskWorkspaceFolders, showAndLogErrorMessage, showAndLogInformationMessage } from './helpers'; import { Credentials } from './authentication'; +import * as cli from './cli'; +import { logger } from './logging'; interface Config { repositories: string[]; ref?: string; - language: string; + language?: string; } // Test "controller" repository and workflow. const OWNER = 'dsp-testing'; const REPO = 'qc-controller'; -export default async function runRemoteQuery(credentials: Credentials, uri?: Uri) { +/** + * Finds the language that a query targets. + * If it can't be autodetected, prompt the user to specify the language manually. + */ +export async function findLanguage( + cliServer: cli.CodeQLCliServer, + queryUri: Uri | undefined +): Promise { + const uri = queryUri || window.activeTextEditor?.document.uri; + if (uri !== undefined) { + try { + const queryInfo = await cliServer.resolveQueryByLanguage(getOnDiskWorkspaceFolders(), uri); + const language = (Object.keys(queryInfo.byLanguage))[0]; + void logger.log(`Detected query language: ${language}`); + return language; + } catch (e) { + void logger.log('Could not autodetect query language. Select language manually.'); + } + } + const availableLanguages = Object.keys(await cliServer.resolveLanguages()); + const language = await window.showQuickPick( + availableLanguages, + { placeHolder: 'Select target language for your query', ignoreFocusOut: true } + ); + if (!language) { + // This only happens if the user cancels the quick pick. + void showAndLogErrorMessage('Language not found. Language must be specified manually.'); + } + return language; +} + +export async function runRemoteQuery(cliServer: cli.CodeQLCliServer, credentials: Credentials, uri?: Uri) { if (!uri?.fsPath.endsWith('.ql')) { return; } @@ -34,9 +67,13 @@ export default async function runRemoteQuery(credentials: Credentials, uri?: Uri const config = yaml.safeLoad(await fs.readFile(repositoriesFile, 'utf8')) as Config; const ref = config.ref || 'main'; - const language = config.language; + const language = config.language || await findLanguage(cliServer, uri); const repositories = config.repositories; + if (!language) { + return; // No error message needed, since `findlanguage` already displays one. + } + try { await octokit.request( 'POST /repos/:owner/:repo/code-scanning/codeql/queries',