Files
vscode-codeql/extensions/ql-vscode/src/extension.ts
2021-11-18 16:10:26 +00:00

942 lines
33 KiB
TypeScript

import {
CancellationToken,
commands,
Disposable,
ExtensionContext,
extensions,
languages,
ProgressLocation,
ProgressOptions,
Uri,
window as Window,
env,
window,
QuickPickItem
} from 'vscode';
import { LanguageClient } from 'vscode-languageclient';
import * as os from 'os';
import * as path from 'path';
import * as tmp from 'tmp-promise';
import { testExplorerExtensionId, TestHub } from 'vscode-test-adapter-api';
import { AstViewer } from './astViewer';
import * as archiveFilesystemProvider from './archive-filesystem-provider';
import { CodeQLCliServer, CliVersionConstraint } from './cli';
import {
CliConfigListener,
DistributionConfigListener,
isCanary,
MAX_QUERIES,
QueryHistoryConfigListener,
QueryServerConfigListener
} from './config';
import * as languageSupport from './languageSupport';
import { DatabaseItem, DatabaseManager } from './databases';
import { DatabaseUI } from './databases-ui';
import {
TemplateQueryDefinitionProvider,
TemplateQueryReferenceProvider,
TemplatePrintAstProvider
} from './contextual/templateProvider';
import {
DEFAULT_DISTRIBUTION_VERSION_RANGE,
DistributionKind,
DistributionManager,
DistributionUpdateCheckResultKind,
FindDistributionResult,
FindDistributionResultKind,
GithubApiError,
GithubRateLimitedError
} from './distribution';
import * as helpers from './helpers';
import { assertNever } from './pure/helpers-pure';
import { spawnIdeServer } from './ide-server';
import { InterfaceManager } from './interface';
import { WebviewReveal } from './interface-utils';
import { ideServerLogger, logger, queryServerLogger } from './logging';
import { QueryHistoryManager } from './query-history';
import { CompletedQuery } from './query-results';
import * as qsClient from './queryserver-client';
import { displayQuickQuery } from './quick-query';
import { compileAndRunQueryAgainstDatabase, tmpDirDisposal } from './run-queries';
import { QLTestAdapterFactory } from './test-adapter';
import { TestUIService } from './test-ui';
import { CompareInterfaceManager } from './compare/compare-interface';
import { gatherQlFiles } from './pure/files';
import { initializeTelemetry } from './telemetry';
import {
commandRunner,
commandRunnerWithProgress,
ProgressCallback,
withProgress,
ProgressUpdate
} from './commandRunner';
import { CodeQlStatusBarHandler } from './status-bar';
import { Credentials } from './authentication';
import { runRemoteQuery } from './run-remote-query';
/**
* extension.ts
* ------------
*
* A vscode extension for CodeQL query development.
*/
/**
* Holds when we have proceeded past the initial phase of extension activation in which
* we are trying to ensure that a valid CodeQL distribution exists, and we're actually setting
* up the bulk of the extension.
*/
let beganMainExtensionActivation = false;
/**
* A list of vscode-registered-command disposables that contain
* temporary stub handlers for commands that exist package.json (hence
* are already connected to onscreen ui elements) but which will not
* have any useful effect if we haven't located a CodeQL distribution.
*/
const errorStubs: Disposable[] = [];
/**
* Holds when we are installing or checking for updates to the distribution.
*/
let isInstallingOrUpdatingDistribution = false;
const extensionId = 'GitHub.vscode-codeql';
const extension = extensions.getExtension(extensionId);
/**
* If the user tries to execute vscode commands after extension activation is failed, give
* a sensible error message.
*
* @param excludedCommands List of commands for which we should not register error stubs.
*/
function registerErrorStubs(excludedCommands: string[], stubGenerator: (command: string) => () => Promise<void>): void {
// Remove existing stubs
errorStubs.forEach(stub => stub.dispose());
if (extension === undefined) {
throw new Error(`Can't find extension ${extensionId}`);
}
const stubbedCommands: string[]
= extension.packageJSON.contributes.commands.map((entry: { command: string }) => entry.command);
stubbedCommands.forEach(command => {
if (excludedCommands.indexOf(command) === -1) {
errorStubs.push(commandRunner(command, stubGenerator(command)));
}
});
}
/**
* The publicly available interface for this extension. This is to
* be used in our tests.
*/
export interface CodeQLExtensionInterface {
readonly ctx: ExtensionContext;
readonly cliServer: CodeQLCliServer;
readonly qs: qsClient.QueryServerClient;
readonly distributionManager: DistributionManager;
readonly databaseManager: DatabaseManager;
readonly databaseUI: DatabaseUI;
readonly dispose: () => void;
}
/**
* Returns the CodeQLExtensionInterface, or an empty object if the interface is not
* available after activation is complete. This will happen if there is no cli
* installed when the extension starts. Downloading and installing the cli
* will happen at a later time.
*
* @param ctx The extension context
*
* @returns CodeQLExtensionInterface
*/
export async function activate(ctx: ExtensionContext): Promise<CodeQLExtensionInterface | Record<string, never>> {
void logger.log(`Starting ${extensionId} extension`);
if (extension === undefined) {
throw new Error(`Can't find extension ${extensionId}`);
}
const distributionConfigListener = new DistributionConfigListener();
await initializeLogging(ctx);
await initializeTelemetry(extension, ctx);
languageSupport.install();
ctx.subscriptions.push(distributionConfigListener);
const codeQlVersionRange = DEFAULT_DISTRIBUTION_VERSION_RANGE;
const distributionManager = new DistributionManager(distributionConfigListener, codeQlVersionRange, ctx);
const shouldUpdateOnNextActivationKey = 'shouldUpdateOnNextActivation';
registerErrorStubs([checkForUpdatesCommand], command => (async () => {
void helpers.showAndLogErrorMessage(`Can't execute ${command}: waiting to finish loading CodeQL CLI.`);
}));
interface DistributionUpdateConfig {
isUserInitiated: boolean;
shouldDisplayMessageWhenNoUpdates: boolean;
allowAutoUpdating: boolean;
}
async function installOrUpdateDistributionWithProgressTitle(progressTitle: string, config: DistributionUpdateConfig): Promise<void> {
const minSecondsSinceLastUpdateCheck = config.isUserInitiated ? 0 : 86400;
const noUpdatesLoggingFunc = config.shouldDisplayMessageWhenNoUpdates ?
helpers.showAndLogInformationMessage : async (message: string) => void logger.log(message);
const result = await distributionManager.checkForUpdatesToExtensionManagedDistribution(minSecondsSinceLastUpdateCheck);
// We do want to auto update if there is no distribution at all
const allowAutoUpdating = config.allowAutoUpdating || !await distributionManager.hasDistribution();
switch (result.kind) {
case DistributionUpdateCheckResultKind.AlreadyCheckedRecentlyResult:
void logger.log('Didn\'t perform CodeQL CLI update check since a check was already performed within the previous ' +
`${minSecondsSinceLastUpdateCheck} seconds.`);
break;
case DistributionUpdateCheckResultKind.AlreadyUpToDate:
await noUpdatesLoggingFunc('CodeQL CLI already up to date.');
break;
case DistributionUpdateCheckResultKind.InvalidLocation:
await noUpdatesLoggingFunc('CodeQL CLI is installed externally so could not be updated.');
break;
case DistributionUpdateCheckResultKind.UpdateAvailable:
if (beganMainExtensionActivation || !allowAutoUpdating) {
const updateAvailableMessage = `Version "${result.updatedRelease.name}" of the CodeQL CLI is now available. ` +
'Do you wish to upgrade?';
await ctx.globalState.update(shouldUpdateOnNextActivationKey, true);
if (await helpers.showInformationMessageWithAction(updateAvailableMessage, 'Restart and Upgrade')) {
await commands.executeCommand('workbench.action.reloadWindow');
}
} else {
const progressOptions: ProgressOptions = {
title: progressTitle,
location: ProgressLocation.Notification,
};
await withProgress(progressOptions, progress =>
distributionManager.installExtensionManagedDistributionRelease(result.updatedRelease, progress));
await ctx.globalState.update(shouldUpdateOnNextActivationKey, false);
void helpers.showAndLogInformationMessage(`CodeQL CLI updated to version "${result.updatedRelease.name}".`);
}
break;
default:
assertNever(result);
}
}
async function installOrUpdateDistribution(config: DistributionUpdateConfig): Promise<void> {
if (isInstallingOrUpdatingDistribution) {
throw new Error('Already installing or updating CodeQL CLI');
}
isInstallingOrUpdatingDistribution = true;
const codeQlInstalled = await distributionManager.getCodeQlPathWithoutVersionCheck() !== undefined;
const willUpdateCodeQl = ctx.globalState.get(shouldUpdateOnNextActivationKey);
const messageText = willUpdateCodeQl
? 'Updating CodeQL CLI'
: codeQlInstalled
? 'Checking for updates to CodeQL CLI'
: 'Installing CodeQL CLI';
try {
await installOrUpdateDistributionWithProgressTitle(messageText, config);
} catch (e) {
// Don't rethrow the exception, because if the config is changed, we want to be able to retry installing
// or updating the distribution.
const alertFunction = (codeQlInstalled && !config.isUserInitiated) ?
helpers.showAndLogWarningMessage : helpers.showAndLogErrorMessage;
const taskDescription = (willUpdateCodeQl ? 'update' :
codeQlInstalled ? 'check for updates to' : 'install') + ' CodeQL CLI';
if (e instanceof GithubRateLimitedError) {
void alertFunction(`Rate limited while trying to ${taskDescription}. Please try again after ` +
`your rate limit window resets at ${e.rateLimitResetDate.toLocaleString(env.language)}.`);
} else if (e instanceof GithubApiError) {
void alertFunction(`Encountered GitHub API error while trying to ${taskDescription}. ` + e);
}
void alertFunction(`Unable to ${taskDescription}. ` + e);
} finally {
isInstallingOrUpdatingDistribution = false;
}
}
async function getDistributionDisplayingDistributionWarnings(): Promise<FindDistributionResult> {
const result = await distributionManager.getDistribution();
switch (result.kind) {
case FindDistributionResultKind.CompatibleDistribution:
void logger.log(`Found compatible version of CodeQL CLI (version ${result.version.raw})`);
break;
case FindDistributionResultKind.IncompatibleDistribution: {
const fixGuidanceMessage = (() => {
switch (result.distribution.kind) {
case DistributionKind.ExtensionManaged:
return 'Please update the CodeQL CLI by running the "CodeQL: Check for CLI Updates" command.';
case DistributionKind.CustomPathConfig:
return `Please update the \"CodeQL CLI Executable Path\" setting to point to a CLI in the version range ${codeQlVersionRange}.`;
case DistributionKind.PathEnvironmentVariable:
return `Please update the CodeQL CLI on your PATH to a version compatible with ${codeQlVersionRange}, or ` +
`set the \"CodeQL CLI Executable Path\" setting to the path of a CLI version compatible with ${codeQlVersionRange}.`;
}
})();
void helpers.showAndLogWarningMessage(
`The current version of the CodeQL CLI (${result.version.raw}) ` +
`is incompatible with this extension. ${fixGuidanceMessage}`
);
break;
}
case FindDistributionResultKind.UnknownCompatibilityDistribution:
void helpers.showAndLogWarningMessage(
'Compatibility with the configured CodeQL CLI could not be determined. ' +
'You may experience problems using the extension.'
);
break;
case FindDistributionResultKind.NoDistribution:
void helpers.showAndLogErrorMessage('The CodeQL CLI could not be found.');
break;
default:
assertNever(result);
}
return result;
}
async function installOrUpdateThenTryActivate(
config: DistributionUpdateConfig
): Promise<CodeQLExtensionInterface | Record<string, never>> {
await installOrUpdateDistribution(config);
// Display the warnings even if the extension has already activated.
const distributionResult = await getDistributionDisplayingDistributionWarnings();
let extensionInterface: CodeQLExtensionInterface | Record<string, never> = {};
if (!beganMainExtensionActivation && distributionResult.kind !== FindDistributionResultKind.NoDistribution) {
extensionInterface = await activateWithInstalledDistribution(
ctx,
distributionManager,
distributionConfigListener
);
} else if (distributionResult.kind === FindDistributionResultKind.NoDistribution) {
registerErrorStubs([checkForUpdatesCommand], command => async () => {
const installActionName = 'Install CodeQL CLI';
const chosenAction = await void helpers.showAndLogErrorMessage(`Can't execute ${command}: missing CodeQL CLI.`, {
items: [installActionName]
});
if (chosenAction === installActionName) {
await installOrUpdateThenTryActivate({
isUserInitiated: true,
shouldDisplayMessageWhenNoUpdates: false,
allowAutoUpdating: true
});
}
});
}
return extensionInterface;
}
ctx.subscriptions.push(distributionConfigListener.onDidChangeConfiguration(() => installOrUpdateThenTryActivate({
isUserInitiated: true,
shouldDisplayMessageWhenNoUpdates: false,
allowAutoUpdating: true
})));
ctx.subscriptions.push(commandRunner(checkForUpdatesCommand, () => installOrUpdateThenTryActivate({
isUserInitiated: true,
shouldDisplayMessageWhenNoUpdates: true,
allowAutoUpdating: true
})));
return await installOrUpdateThenTryActivate({
isUserInitiated: !!ctx.globalState.get(shouldUpdateOnNextActivationKey),
shouldDisplayMessageWhenNoUpdates: false,
// only auto update on startup if the user has previously requested an update
// otherwise, ask user to accept the update
allowAutoUpdating: !!ctx.globalState.get(shouldUpdateOnNextActivationKey)
});
}
async function activateWithInstalledDistribution(
ctx: ExtensionContext,
distributionManager: DistributionManager,
distributionConfigListener: DistributionConfigListener
): Promise<CodeQLExtensionInterface> {
beganMainExtensionActivation = true;
// Remove any error stubs command handlers left over from first part
// of activation.
errorStubs.forEach((stub) => stub.dispose());
void logger.log('Initializing configuration listener...');
const qlConfigurationListener = await QueryServerConfigListener.createQueryServerConfigListener(
distributionManager
);
ctx.subscriptions.push(qlConfigurationListener);
void logger.log('Initializing CodeQL cli server...');
const cliServer = new CodeQLCliServer(
distributionManager,
new CliConfigListener(),
logger
);
ctx.subscriptions.push(cliServer);
const statusBar = new CodeQlStatusBarHandler(cliServer, distributionConfigListener);
ctx.subscriptions.push(statusBar);
void logger.log('Initializing query server client.');
const qs = new qsClient.QueryServerClient(
qlConfigurationListener,
cliServer,
{
logger: queryServerLogger,
contextStoragePath: getContextStoragePath(ctx),
},
(task) =>
Window.withProgress(
{ title: 'CodeQL query server', location: ProgressLocation.Window },
task
)
);
ctx.subscriptions.push(qs);
await qs.startQueryServer();
void logger.log('Initializing database manager.');
const dbm = new DatabaseManager(ctx, qs, cliServer, logger);
ctx.subscriptions.push(dbm);
void logger.log('Initializing database panel.');
const databaseUI = new DatabaseUI(
dbm,
qs,
getContextStoragePath(ctx),
ctx.extensionPath
);
databaseUI.init();
ctx.subscriptions.push(databaseUI);
void logger.log('Initializing query history manager.');
const queryHistoryConfigurationListener = new QueryHistoryConfigListener();
ctx.subscriptions.push(queryHistoryConfigurationListener);
const showResults = async (item: CompletedQuery) =>
showResultsForCompletedQuery(item, WebviewReveal.Forced);
const qhm = new QueryHistoryManager(
qs,
ctx.extensionPath,
queryHistoryConfigurationListener,
showResults,
async (from: CompletedQuery, to: CompletedQuery) =>
showResultsForComparison(from, to),
);
ctx.subscriptions.push(qhm);
void logger.log('Initializing results panel interface.');
const intm = new InterfaceManager(ctx, dbm, cliServer, queryServerLogger);
ctx.subscriptions.push(intm);
void logger.log('Initializing compare panel interface.');
const cmpm = new CompareInterfaceManager(
ctx,
dbm,
cliServer,
queryServerLogger,
showResults
);
ctx.subscriptions.push(cmpm);
void logger.log('Initializing source archive filesystem provider.');
archiveFilesystemProvider.activate(ctx);
async function showResultsForComparison(
from: CompletedQuery,
to: CompletedQuery
): Promise<void> {
try {
await cmpm.showResults(from, to);
} catch (e) {
void helpers.showAndLogErrorMessage(e.message);
}
}
async function showResultsForCompletedQuery(
query: CompletedQuery,
forceReveal: WebviewReveal
): Promise<void> {
await intm.showResults(query, forceReveal, false);
}
async function compileAndRunQuery(
quickEval: boolean,
selectedQuery: Uri | undefined,
progress: ProgressCallback,
token: CancellationToken,
databaseItem: DatabaseItem | undefined,
): Promise<void> {
if (qs !== undefined) {
// If no databaseItem is specified, use the database currently selected in the Databases UI
databaseItem = databaseItem || await databaseUI.getDatabaseItem(progress, token);
if (databaseItem === undefined) {
throw new Error('Can\'t run query without a selected database');
}
const info = await compileAndRunQueryAgainstDatabase(
cliServer,
qs,
databaseItem,
quickEval,
selectedQuery,
progress,
token
);
const item = qhm.buildCompletedQuery(info);
await showResultsForCompletedQuery(item, WebviewReveal.NotForced);
// Note we must update the query history view after showing results as the
// display and sorting might depend on the number of results
await qhm.addCompletedQuery(item);
}
}
const qhelpTmpDir = tmp.dirSync({ prefix: 'qhelp_', keep: false, unsafeCleanup: true });
ctx.subscriptions.push({ dispose: qhelpTmpDir.removeCallback });
async function previewQueryHelp(
selectedQuery: Uri
): Promise<void> {
// selectedQuery is unpopulated when executing through the command palette
const pathToQhelp = selectedQuery ? selectedQuery.fsPath : window.activeTextEditor?.document.uri.fsPath;
if(pathToQhelp) {
// Create temporary directory
const relativePathToMd = path.basename(pathToQhelp, '.qhelp') + '.md';
const absolutePathToMd = path.join(qhelpTmpDir.name, relativePathToMd);
const uri = Uri.file(absolutePathToMd);
try {
await cliServer.generateQueryHelp(pathToQhelp , absolutePathToMd);
await commands.executeCommand('markdown.showPreviewToSide', uri);
} catch (err) {
const errorMessage = err.message.includes('Generating qhelp in markdown') ? (
`Could not generate markdown from ${pathToQhelp}: Bad formatting in .qhelp file.`
) : `Could not open a preview of the generated file (${absolutePathToMd}).`;
void helpers.showAndLogErrorMessage(errorMessage, { fullMessage: `${errorMessage}\n${err}` });
}
}
}
async function openReferencedFile(
selectedQuery: Uri
): Promise<void> {
// If no file is selected, the path of the file in the editor is selected
const path = selectedQuery?.fsPath || window.activeTextEditor?.document.uri.fsPath;
if (qs !== undefined && path) {
if (await cliServer.cliConstraints.supportsResolveQlref()) {
const resolved = await cliServer.resolveQlref(path);
const uri = Uri.file(resolved.resolvedPath);
await window.showTextDocument(uri, { preview: false });
} else {
void helpers.showAndLogErrorMessage(
'Jumping from a .qlref file to the .ql file it references is not '
+ 'supported with the CLI version you are running.\n'
+ `Please upgrade your CLI to version ${CliVersionConstraint.CLI_VERSION_WITH_RESOLVE_QLREF
} or later to use this feature.`);
}
}
}
ctx.subscriptions.push(tmpDirDisposal);
void logger.log('Initializing CodeQL language server.');
const client = new LanguageClient(
'CodeQL Language Server',
() => spawnIdeServer(qlConfigurationListener),
{
documentSelector: [
{ language: 'ql', scheme: 'file' },
{ language: 'yaml', scheme: 'file', pattern: '**/qlpack.yml' },
],
synchronize: {
configurationSection: 'codeQL',
},
// Ensure that language server exceptions are logged to the same channel as its output.
outputChannel: ideServerLogger.outputChannel,
},
true
);
void logger.log('Initializing QLTest interface.');
const testExplorerExtension = extensions.getExtension<TestHub>(
testExplorerExtensionId
);
if (testExplorerExtension) {
const testHub = testExplorerExtension.exports;
const testAdapterFactory = new QLTestAdapterFactory(testHub, cliServer, dbm);
ctx.subscriptions.push(testAdapterFactory);
const testUIService = new TestUIService(testHub);
ctx.subscriptions.push(testUIService);
}
void logger.log('Registering top-level command palette commands.');
ctx.subscriptions.push(
commandRunnerWithProgress(
'codeQL.runQuery',
async (
progress: ProgressCallback,
token: CancellationToken,
uri: Uri | undefined
) => await compileAndRunQuery(false, uri, progress, token, undefined),
{
title: 'Running query',
cancellable: true
}
)
);
interface DatabaseQuickPickItem extends QuickPickItem {
databaseItem: DatabaseItem;
}
ctx.subscriptions.push(
commandRunnerWithProgress(
'codeQL.runQueryOnMultipleDatabases',
async (
progress: ProgressCallback,
token: CancellationToken,
uri: Uri | undefined
) => {
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 helpers.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<DatabaseQuickPickItem>(dbItem => (
{
databaseItem: dbItem,
label: dbItem.name,
description: dbItem.language,
}
));
/**
* Databases that were selected in the quick pick menu.
*/
const quickpick = await window.showQuickPick<DatabaseQuickPickItem>(
quickPickItems,
{ canPickMany: true, ignoreFocusOut: true }
);
if (quickpick !== undefined) {
// Collect all skipped databases and display them at the end (instead of popping up individual errors)
const skippedDatabases = [];
const errors = [];
for (const item of quickpick) {
try {
await compileAndRunQuery(false, uri, progress, token, item.databaseItem);
} catch (error) {
skippedDatabases.push(item.label);
errors.push(error.message);
}
}
if (skippedDatabases.length > 0) {
void logger.log(`Errors:\n${errors.join('\n')}`);
void helpers.showAndLogWarningMessage(
`The following databases were skipped:\n${skippedDatabases.join('\n')}.\nFor details about the errors, see the logs.`
);
}
} else {
void helpers.showAndLogErrorMessage('No databases selected.');
}
},
{
title: 'Running query on selected databases',
cancellable: true
}
)
);
ctx.subscriptions.push(
commandRunnerWithProgress(
'codeQL.runQueries',
async (
progress: ProgressCallback,
token: CancellationToken,
_: Uri | undefined,
multi: Uri[]
) => {
const maxQueryCount = MAX_QUERIES.getValue() as number;
const [files, dirFound] = await gatherQlFiles(multi.map(uri => uri.fsPath));
if (files.length > maxQueryCount) {
throw new Error(`You tried to run ${files.length} queries, but the maximum is ${maxQueryCount}. Try selecting fewer queries or changing the 'codeQL.runningQueries.maxQueries' setting.`);
}
// 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));
// Use a wrapped progress so that messages appear with the queries remaining in it.
let queriesRemaining = queryUris.length;
function wrappedProgress(update: ProgressUpdate) {
const message = queriesRemaining > 1
? `${queriesRemaining} remaining. ${update.message}`
: update.message;
progress({
...update,
message
});
}
if (queryUris.length > 1) {
// Try to upgrade the current database before running any queries
// so that the user isn't confronted with multiple upgrade
// requests for each query to run.
// Only do it if running multiple queries since this check is
// performed on each query run anyway.
await databaseUI.tryUpgradeCurrentDatabase(progress, token);
}
wrappedProgress({
maxStep: queryUris.length,
step: queryUris.length - queriesRemaining,
message: ''
});
await Promise.all(queryUris.map(async uri =>
compileAndRunQuery(false, uri, wrappedProgress, token, undefined)
.then(() => queriesRemaining--)
));
},
{
title: 'Running queries',
cancellable: true
})
);
ctx.subscriptions.push(
commandRunnerWithProgress(
'codeQL.quickEval',
async (
progress: ProgressCallback,
token: CancellationToken,
uri: Uri | undefined
) => await compileAndRunQuery(true, uri, progress, token, undefined),
{
title: 'Running query',
cancellable: true
})
);
ctx.subscriptions.push(
commandRunnerWithProgress('codeQL.quickQuery', async (
progress: ProgressCallback,
token: CancellationToken
) =>
displayQuickQuery(ctx, cliServer, databaseUI, progress, token),
{
title: 'Run Quick Query'
}
)
);
// The "runRemoteQuery" command is internal-only.
ctx.subscriptions.push(
commandRunnerWithProgress('codeQL.runRemoteQuery', async (
progress: ProgressCallback,
token: CancellationToken,
uri: Uri | undefined
) => {
if (isCanary()) {
progress({
maxStep: 5,
step: 0,
message: 'Getting credentials'
});
const credentials = await Credentials.initialize(ctx);
await runRemoteQuery(cliServer, credentials, uri || window.activeTextEditor?.document.uri, false, progress, token);
} else {
throw new Error('Remote queries require the CodeQL Canary version to run.');
}
}, {
title: 'Run Remote Query',
cancellable: true
})
);
ctx.subscriptions.push(
commandRunner(
'codeQL.openReferencedFile',
openReferencedFile
)
);
ctx.subscriptions.push(
commandRunner(
'codeQL.previewQueryHelp',
previewQueryHelp
)
);
ctx.subscriptions.push(
commandRunnerWithProgress('codeQL.restartQueryServer', async (
progress: ProgressCallback,
token: CancellationToken
) => {
await qs.restartQueryServer(progress, token);
void helpers.showAndLogInformationMessage('CodeQL Query Server restarted.', {
outputLogger: queryServerLogger,
});
}, {
title: 'Restarting Query Server'
})
);
ctx.subscriptions.push(
commandRunnerWithProgress('codeQL.chooseDatabaseFolder', (
progress: ProgressCallback,
token: CancellationToken
) =>
databaseUI.handleChooseDatabaseFolder(progress, token), {
title: 'Choose a Database from a Folder'
})
);
ctx.subscriptions.push(
commandRunnerWithProgress('codeQL.chooseDatabaseArchive', (
progress: ProgressCallback,
token: CancellationToken
) =>
databaseUI.handleChooseDatabaseArchive(progress, token), {
title: 'Choose a Database from an Archive'
})
);
ctx.subscriptions.push(
commandRunnerWithProgress('codeQL.chooseDatabaseLgtm', (
progress: ProgressCallback,
token: CancellationToken
) =>
databaseUI.handleChooseDatabaseLgtm(progress, token),
{
title: 'Adding database from LGTM',
})
);
ctx.subscriptions.push(
commandRunnerWithProgress('codeQL.chooseDatabaseInternet', (
progress: ProgressCallback,
token: CancellationToken
) =>
databaseUI.handleChooseDatabaseInternet(progress, token),
{
title: 'Adding database from URL',
})
);
ctx.subscriptions.push(
commandRunner('codeQL.openDocumentation', async () =>
env.openExternal(Uri.parse('https://codeql.github.com/docs/'))));
ctx.subscriptions.push(
commandRunner('codeQL.copyVersion', async () => {
const text = `CodeQL extension version: ${extension?.packageJSON.version} \nCodeQL CLI version: ${await getCliVersion()} \nPlatform: ${os.platform()} ${os.arch()}`;
await env.clipboard.writeText(text);
void helpers.showAndLogInformationMessage(text);
}));
const getCliVersion = async () => {
try {
return await cliServer.getVersion();
} catch {
return '<missing>';
}
};
// The "authenticateToGitHub" command is internal-only.
ctx.subscriptions.push(
commandRunner('codeQL.authenticateToGitHub', async () => {
if (isCanary()) {
/**
* Credentials for authenticating to GitHub.
* These are used when making API calls.
*/
const credentials = await Credentials.initialize(ctx);
const octokit = await credentials.getOctokit();
const userInfo = await octokit.users.getAuthenticated();
void helpers.showAndLogInformationMessage(`Authenticated to GitHub as user: ${userInfo.data.login}`);
}
}));
commands.registerCommand('codeQL.showLogs', () => {
logger.show();
});
void logger.log('Starting language server.');
ctx.subscriptions.push(client.start());
// Jump-to-definition and find-references
void logger.log('Registering jump-to-definition handlers.');
languages.registerDefinitionProvider(
{ scheme: archiveFilesystemProvider.zipArchiveScheme },
new TemplateQueryDefinitionProvider(cliServer, qs, dbm)
);
languages.registerReferenceProvider(
{ scheme: archiveFilesystemProvider.zipArchiveScheme },
new TemplateQueryReferenceProvider(cliServer, qs, dbm)
);
const astViewer = new AstViewer();
const templateProvider = new TemplatePrintAstProvider(cliServer, qs, dbm);
ctx.subscriptions.push(astViewer);
ctx.subscriptions.push(commandRunnerWithProgress('codeQL.viewAst', async (
progress: ProgressCallback,
token: CancellationToken
) => {
const ast = await templateProvider.provideAst(
progress,
token,
window.activeTextEditor?.document,
);
if (ast) {
astViewer.updateRoots(await ast.getRoots(), ast.db, ast.fileName);
}
}, {
cancellable: true,
title: 'Calculate AST'
}));
await commands.executeCommand('codeQLDatabases.removeOrphanedDatabases');
void logger.log('Successfully finished extension initialization.');
return {
ctx,
cliServer,
qs,
distributionManager,
databaseManager: dbm,
databaseUI,
dispose: () => {
ctx.subscriptions.forEach(d => d.dispose());
}
};
}
function getContextStoragePath(ctx: ExtensionContext) {
return ctx.storagePath || ctx.globalStoragePath;
}
async function initializeLogging(ctx: ExtensionContext): Promise<void> {
const storagePath = getContextStoragePath(ctx);
await logger.setLogStoragePath(storagePath, false);
await ideServerLogger.setLogStoragePath(storagePath, false);
ctx.subscriptions.push(logger);
ctx.subscriptions.push(queryServerLogger);
ctx.subscriptions.push(ideServerLogger);
}
const checkForUpdatesCommand = 'codeQL.checkForUpdatesToCLI';