Merge branch 'main' into robertbrignull/no-nested-functions

This commit is contained in:
Robert
2023-03-15 13:37:35 +00:00
23 changed files with 544 additions and 100 deletions

View File

@@ -142,7 +142,7 @@
"ts-node": "^10.7.0",
"ts-protoc-gen": "^0.9.0",
"typescript": "^4.5.5",
"webpack": "^5.62.2",
"webpack": "^5.76.0",
"webpack-cli": "^4.6.0"
},
"engines": {
@@ -40359,9 +40359,9 @@
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE="
},
"node_modules/webpack": {
"version": "5.73.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.73.0.tgz",
"integrity": "sha512-svjudQRPPa0YiOYa2lM/Gacw0r6PvxptHj4FuEKQ2kX05ZLkjbVc5MnPs6its5j7IZljnIqSVo/OsY2X0IpHGA==",
"version": "5.76.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.0.tgz",
"integrity": "sha512-l5sOdYBDunyf72HW8dF23rFtWq/7Zgvt/9ftMof71E/yUb1YLOBmTgA2K4vQthB3kotMrSj609txVE0dnr2fjA==",
"dev": true,
"dependencies": {
"@types/eslint-scope": "^3.7.3",
@@ -40369,11 +40369,11 @@
"@webassemblyjs/ast": "1.11.1",
"@webassemblyjs/wasm-edit": "1.11.1",
"@webassemblyjs/wasm-parser": "1.11.1",
"acorn": "^8.4.1",
"acorn": "^8.7.1",
"acorn-import-assertions": "^1.7.6",
"browserslist": "^4.14.5",
"chrome-trace-event": "^1.0.2",
"enhanced-resolve": "^5.9.3",
"enhanced-resolve": "^5.10.0",
"es-module-lexer": "^0.9.0",
"eslint-scope": "5.1.1",
"events": "^3.2.0",
@@ -40386,7 +40386,7 @@
"schema-utils": "^3.1.0",
"tapable": "^2.1.1",
"terser-webpack-plugin": "^5.1.3",
"watchpack": "^2.3.1",
"watchpack": "^2.4.0",
"webpack-sources": "^3.2.3"
},
"bin": {
@@ -40758,9 +40758,9 @@
}
},
"node_modules/webpack/node_modules/enhanced-resolve": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.3.tgz",
"integrity": "sha512-Bq9VSor+kjvW3f9/MiiR4eE3XYgOl7/rS8lnSxbRbF3kS0B2r+Y9w5krBWxZgDxASVZbdYrn5wT4j/Wb0J9qow==",
"version": "5.12.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz",
"integrity": "sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==",
"dev": true,
"dependencies": {
"graceful-fs": "^4.2.4",
@@ -72205,9 +72205,9 @@
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE="
},
"webpack": {
"version": "5.73.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.73.0.tgz",
"integrity": "sha512-svjudQRPPa0YiOYa2lM/Gacw0r6PvxptHj4FuEKQ2kX05ZLkjbVc5MnPs6its5j7IZljnIqSVo/OsY2X0IpHGA==",
"version": "5.76.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.0.tgz",
"integrity": "sha512-l5sOdYBDunyf72HW8dF23rFtWq/7Zgvt/9ftMof71E/yUb1YLOBmTgA2K4vQthB3kotMrSj609txVE0dnr2fjA==",
"dev": true,
"requires": {
"@types/eslint-scope": "^3.7.3",
@@ -72215,11 +72215,11 @@
"@webassemblyjs/ast": "1.11.1",
"@webassemblyjs/wasm-edit": "1.11.1",
"@webassemblyjs/wasm-parser": "1.11.1",
"acorn": "^8.4.1",
"acorn": "^8.7.1",
"acorn-import-assertions": "^1.7.6",
"browserslist": "^4.14.5",
"chrome-trace-event": "^1.0.2",
"enhanced-resolve": "^5.9.3",
"enhanced-resolve": "^5.10.0",
"es-module-lexer": "^0.9.0",
"eslint-scope": "5.1.1",
"events": "^3.2.0",
@@ -72232,7 +72232,7 @@
"schema-utils": "^3.1.0",
"tapable": "^2.1.1",
"terser-webpack-plugin": "^5.1.3",
"watchpack": "^2.3.1",
"watchpack": "^2.4.0",
"webpack-sources": "^3.2.3"
},
"dependencies": {
@@ -72261,9 +72261,9 @@
}
},
"enhanced-resolve": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.3.tgz",
"integrity": "sha512-Bq9VSor+kjvW3f9/MiiR4eE3XYgOl7/rS8lnSxbRbF3kS0B2r+Y9w5krBWxZgDxASVZbdYrn5wT4j/Wb0J9qow==",
"version": "5.12.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz",
"integrity": "sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==",
"dev": true,
"requires": {
"graceful-fs": "^4.2.4",

View File

@@ -1535,7 +1535,7 @@
"ts-node": "^10.7.0",
"ts-protoc-gen": "^0.9.0",
"typescript": "^4.5.5",
"webpack": "^5.62.2",
"webpack": "^5.76.0",
"webpack-cli": "^4.6.0"
},
"lint-staged": {

View File

@@ -1284,11 +1284,25 @@ export class CodeQLCliServer implements Disposable {
);
}
async packInstall(dir: string, forceUpdate = false) {
async packInstall(
dir: string,
{ forceUpdate = false, workspaceFolders = [] as string[] } = {},
) {
const args = [dir];
if (forceUpdate) {
args.push("--mode", "update");
}
if (workspaceFolders?.length > 0) {
if (await this.cliConstraints.supportsAdditionalPacksInstall()) {
args.push(
// Allow prerelease packs from the ql submodule.
"--allow-prerelease",
// Allow the use of --additional-packs argument without issueing a warning
"--no-strict-mode",
...this.getAdditionalPacksArg(workspaceFolders),
);
}
}
return this.runJsonCodeQlCliCommandWithAuthentication(
["pack", "install"],
args,
@@ -1692,6 +1706,13 @@ export class CliVersionConstraint {
*/
public static CLI_VERSION_WITH_QLPACKS_KIND = new SemVer("2.12.3");
/**
* CLI version that supports the `--additional-packs` option for the `pack install` command.
*/
public static CLI_VERSION_WITH_ADDITIONAL_PACKS_INSTALL = new SemVer(
"2.12.4",
);
constructor(private readonly cli: CodeQLCliServer) {
/**/
}
@@ -1755,4 +1776,10 @@ export class CliVersionConstraint {
CliVersionConstraint.CLI_VERSION_WITH_QLPACKS_KIND,
);
}
async supportsAdditionalPacksInstall() {
return this.isVersionAtLeast(
CliVersionConstraint.CLI_VERSION_WITH_ADDITIONAL_PACKS_INSTALL,
);
}
}

View File

@@ -3,6 +3,7 @@ import { Disposable } from "../pure/disposable-object";
import { AppEventEmitter } from "./events";
import { Logger } from "./logging";
import { Memento } from "./memento";
import { AppCommandManager } from "./commands";
export interface App {
createEventEmitter<T>(): AppEventEmitter<T>;
@@ -15,6 +16,7 @@ export interface App {
readonly workspaceStoragePath?: string;
readonly workspaceState: Memento;
readonly credentials: Credentials;
readonly commands: AppCommandManager;
}
export enum AppMode {

View File

@@ -0,0 +1,24 @@
import { CommandManager } from "../packages/commands";
/**
* Contains type definitions for all commands used by the extension.
*
* To add a new command first define its type here, then provide
* the implementation in the corresponding `getCommands` function.
*/
// Base commands not tied directly to a module like e.g. variant analysis.
export type BaseCommands = {
"codeQL.openDocumentation": () => Promise<void>;
};
// Commands tied to variant analysis
export type VariantAnalysisCommands = {
"codeQL.openVariantAnalysisLogs": (
variantAnalysisId: number,
) => Promise<void>;
};
export type AllCommands = BaseCommands & VariantAnalysisCommands;
export type AppCommandManager = CommandManager<AllCommands>;

View File

@@ -0,0 +1,32 @@
import { commands } from "vscode";
import { commandRunner } from "../../commandRunner";
import { CommandFunction, CommandManager } from "../../packages/commands";
/**
* Create a command manager for VSCode, wrapping the commandRunner
* and vscode.executeCommand.
*/
export function createVSCodeCommandManager<
Commands extends Record<string, CommandFunction>,
>(): CommandManager<Commands> {
return new CommandManager(commandRunner, wrapExecuteCommand);
}
/**
* wrapExecuteCommand wraps commands.executeCommand to satisfy that the
* type is a Promise. Type script does not seem to be smart enough
* to figure out that `ReturnType<Commands[CommandName]>` is actually
* a Promise, so we need to add a second layer of wrapping and unwrapping
* (The `Promise<Awaited<` part) to get the right types.
*/
async function wrapExecuteCommand<
Commands extends Record<string, CommandFunction>,
CommandName extends keyof Commands & string = keyof Commands & string,
>(
commandName: CommandName,
...args: Parameters<Commands[CommandName]>
): Promise<Awaited<ReturnType<Commands[CommandName]>>> {
return await commands.executeCommand<
Awaited<ReturnType<Commands[CommandName]>>
>(commandName, ...args);
}

View File

@@ -6,14 +6,19 @@ import { AppEventEmitter } from "../events";
import { extLogger, Logger } from "../logging";
import { Memento } from "../memento";
import { VSCodeAppEventEmitter } from "./events";
import { AppCommandManager } from "../commands";
import { createVSCodeCommandManager } from "./commands";
export class ExtensionApp implements App {
public readonly credentials: VSCodeCredentials;
public readonly commands: AppCommandManager;
public constructor(
public readonly extensionContext: vscode.ExtensionContext,
) {
this.credentials = new VSCodeCredentials();
this.commands = createVSCodeCommandManager();
extensionContext.subscriptions.push(this.commands);
}
public get extensionPath(): string {

View File

@@ -137,6 +137,7 @@ import { DbModule } from "./databases/db-module";
import { redactableError } from "./pure/errors";
import { QueryHistoryDirs } from "./query-history/query-history-dirs";
import { DirResult } from "tmp";
import { AllCommands, BaseCommands } from "./common/commands";
/**
* extension.ts
@@ -168,6 +169,17 @@ let isInstallingOrUpdatingDistribution = false;
const extensionId = "GitHub.vscode-codeql";
const extension = extensions.getExtension(extensionId);
/**
* Return all commands that are not tied to the more specific managers.
*/
function getCommands(): BaseCommands {
return {
"codeQL.openDocumentation": async () => {
await env.openExternal(Uri.parse("https://codeql.github.com/docs/"));
},
};
}
/**
* If the user tries to execute vscode commands after extension activation is failed, give
* a sensible error message.
@@ -1113,14 +1125,14 @@ async function activateWithInstalledDistribution(
),
);
ctx.subscriptions.push(
commandRunner(
"codeQL.openVariantAnalysisLogs",
async (variantAnalysisId: number) => {
await variantAnalysisManager.openVariantAnalysisLogs(variantAnalysisId);
},
),
);
const allCommands: AllCommands = {
...getCommands(),
...variantAnalysisManager.getCommands(),
};
for (const [commandName, command] of Object.entries(allCommands)) {
app.commands.register(commandName as keyof AllCommands, command);
}
ctx.subscriptions.push(
commandRunner(
@@ -1343,12 +1355,6 @@ async function activateWithInstalledDistribution(
),
);
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: ${

View File

@@ -1 +1,70 @@
export class CommandManager {}
/**
* Contains a generic implementation of typed commands.
*
* This allows different parts of the extension to register commands with a certain type,
* and then allow other parts to call those commands in a well-typed manner.
*/
import { Disposable } from "./Disposable";
/**
* A command function is a completely untyped command.
*/
export type CommandFunction = (...args: any[]) => Promise<unknown>;
/**
* The command manager basically takes a single input, the type
* of all the known commands. The second parameter is provided by
* default (and should not be needed by the caller) it is a
* technicality to allow the type system to look up commands.
*/
export class CommandManager<
Commands extends Record<string, CommandFunction>,
CommandName extends keyof Commands & string = keyof Commands & string,
> implements Disposable
{
// TODO: should this be a map?
// TODO: handle multiple command names
private commands: Disposable[] = [];
constructor(
private readonly commandRegister: <T extends CommandName>(
commandName: T,
fn: Commands[T],
) => Disposable,
private readonly commandExecute: <T extends CommandName>(
commandName: T,
...args: Parameters<Commands[T]>
) => Promise<Awaited<ReturnType<Commands[T]>>>,
) {}
/**
* Register a command with the specified name and implementation.
*/
register<T extends CommandName>(
commandName: T,
definition: Commands[T],
): void {
this.commands.push(this.commandRegister(commandName, definition));
}
/**
* Execute a command with the specified name and the provided arguments.
*/
execute<T extends CommandName>(
commandName: T,
...args: Parameters<Commands[T]>
): Promise<Awaited<ReturnType<Commands[T]>>> {
return this.commandExecute(commandName, ...args);
}
/**
* Dispose the manager, disposing all the registered commands.
*/
dispose(): void {
this.commands.forEach((cmd) => {
cmd.dispose();
});
this.commands = [];
}
}

View File

@@ -0,0 +1,7 @@
/**
* This interface mirrors the vscode.Disaposable class, so that
* the command manager does not depend on vscode directly.
*/
export interface Disposable {
dispose(): void;
}

View File

@@ -2,6 +2,10 @@ import { join } from "path";
import { pathExists } from "fs-extra";
export const QLPACK_FILENAMES = ["qlpack.yml", "codeql-pack.yml"];
export const QLPACK_LOCK_FILENAMES = [
"qlpack.lock.yml",
"codeql-pack.lock.yml",
];
export const FALLBACK_QLPACK_FILENAME = QLPACK_FILENAMES[0];
export async function getQlPackPath(

View File

@@ -143,7 +143,7 @@ export async function displayQuickQuery(
if (shouldRewrite) {
await cliServer.clearCache();
await cliServer.packInstall(queriesDir, true);
await cliServer.packInstall(queriesDir, { forceUpdate: true });
}
await Window.showTextDocument(await workspace.openTextDocument(qlFile));

View File

@@ -34,6 +34,7 @@ import {
getQlPackPath,
FALLBACK_QLPACK_FILENAME,
QLPACK_FILENAMES,
QLPACK_LOCK_FILENAMES,
} from "../pure/ql";
export interface QlPack {
@@ -70,42 +71,23 @@ async function generateQueryPack(
const originalPackRoot = await findPackRoot(queryFile);
const packRelativePath = relative(originalPackRoot, queryFile);
const targetQueryFileName = join(queryPackDir, packRelativePath);
const workspaceFolders = getOnDiskWorkspaceFolders();
let language: string | undefined;
// Check if the query is already in a query pack.
// If so, copy the entire query pack to the temporary directory.
// Otherwise, copy only the query file to the temporary directory
// and generate a synthetic query pack.
if (await getQlPackPath(originalPackRoot)) {
// don't include ql files. We only want the queryFile to be copied.
const toCopy = await cliServer.packPacklist(originalPackRoot, false);
// also copy the lock file (either new name or old name) and the query file itself. These are not included in the packlist.
[
join(originalPackRoot, "qlpack.lock.yml"),
join(originalPackRoot, "codeql-pack.lock.yml"),
await copyExistingQueryPack(
cliServer,
originalPackRoot,
queryFile,
].forEach((absolutePath) => {
if (absolutePath) {
toCopy.push(absolutePath);
}
});
let copiedCount = 0;
await copy(originalPackRoot, queryPackDir, {
filter: (file: string) =>
// copy file if it is in the packlist, or it is a parent directory of a file in the packlist
!!toCopy.find((f) => {
// Normalized paths ensure that Windows drive letters are capitalized consistently.
const normalizedPath = Uri.file(f).fsPath;
const matches =
normalizedPath === file || normalizedPath.startsWith(file + sep);
if (matches) {
copiedCount++;
}
return matches;
}),
});
void extLogger.log(`Copied ${copiedCount} files to ${queryPackDir}`);
await fixPackFile(queryPackDir, packRelativePath);
queryPackDir,
packRelativePath,
);
language = await findLanguage(cliServer, Uri.file(targetQueryFileName));
} else {
@@ -114,20 +96,12 @@ async function generateQueryPack(
// copy only the query file to the query pack directory
// and generate a synthetic query pack
void extLogger.log(`Copying ${queryFile} to ${queryPackDir}`);
await copy(queryFile, targetQueryFileName);
void extLogger.log("Generating synthetic query pack");
const syntheticQueryPack = {
name: QUERY_PACK_NAME,
version: "0.0.0",
dependencies: {
[`codeql/${language}-all`]: "*",
},
defaultSuite: generateDefaultSuite(packRelativePath),
};
await writeFile(
join(queryPackDir, FALLBACK_QLPACK_FILENAME),
dump(syntheticQueryPack),
await createNewQueryPack(
queryFile,
queryPackDir,
targetQueryFileName,
language,
packRelativePath,
);
}
if (!language) {
@@ -149,12 +123,21 @@ async function generateQueryPack(
precompilationOpts = ["--no-precompile"];
}
if (await cliServer.useExtensionPacks()) {
await injectExtensionPacks(cliServer, queryPackDir, workspaceFolders);
}
await cliServer.packInstall(queryPackDir, {
workspaceFolders,
});
// Clear the CLI cache so that the most recent qlpack lock file is used.
await cliServer.clearCache();
const bundlePath = await getPackedBundlePath(queryPackDir);
void extLogger.log(
`Compiling and bundling query pack from ${queryPackDir} to ${bundlePath}. (This may take a while.)`,
);
await cliServer.packInstall(queryPackDir);
const workspaceFolders = getOnDiskWorkspaceFolders();
await cliServer.packBundle(
queryPackDir,
workspaceFolders,
@@ -168,6 +151,70 @@ async function generateQueryPack(
};
}
async function createNewQueryPack(
queryFile: string,
queryPackDir: string,
targetQueryFileName: string,
language: string | undefined,
packRelativePath: string,
) {
void extLogger.log(`Copying ${queryFile} to ${queryPackDir}`);
await copy(queryFile, targetQueryFileName);
void extLogger.log("Generating synthetic query pack");
const syntheticQueryPack = {
name: QUERY_PACK_NAME,
version: "0.0.0",
dependencies: {
[`codeql/${language}-all`]: "*",
},
defaultSuite: generateDefaultSuite(packRelativePath),
};
await writeFile(
join(queryPackDir, FALLBACK_QLPACK_FILENAME),
dump(syntheticQueryPack),
);
}
async function copyExistingQueryPack(
cliServer: cli.CodeQLCliServer,
originalPackRoot: string,
queryFile: string,
queryPackDir: string,
packRelativePath: string,
) {
const toCopy = await cliServer.packPacklist(originalPackRoot, false);
[
// also copy the lock file (either new name or old name) and the query file itself. These are not included in the packlist.
...QLPACK_LOCK_FILENAMES.map((f) => join(originalPackRoot, f)),
queryFile,
].forEach((absolutePath) => {
if (absolutePath) {
toCopy.push(absolutePath);
}
});
let copiedCount = 0;
await copy(originalPackRoot, queryPackDir, {
filter: (file: string) =>
// copy file if it is in the packlist, or it is a parent directory of a file in the packlist
!!toCopy.find((f) => {
// Normalized paths ensure that Windows drive letters are capitalized consistently.
const normalizedPath = Uri.file(f).fsPath;
const matches =
normalizedPath === file || normalizedPath.startsWith(file + sep);
if (matches) {
copiedCount++;
}
return matches;
}),
});
void extLogger.log(`Copied ${copiedCount} files to ${queryPackDir}`);
await fixPackFile(queryPackDir, packRelativePath);
}
async function findPackRoot(queryFile: string): Promise<string> {
// recursively find the directory containing qlpack.yml or codeql-pack.yml
let dir = dirname(queryFile);
@@ -329,19 +376,54 @@ async function fixPackFile(
}
const qlpack = load(await readFile(packPath, "utf8")) as QlPack;
// update pack name
qlpack.name = QUERY_PACK_NAME;
// update default suite
delete qlpack.defaultSuiteFile;
qlpack.defaultSuite = generateDefaultSuite(packRelativePath);
// remove any ${workspace} version references
updateDefaultSuite(qlpack, packRelativePath);
removeWorkspaceRefs(qlpack);
await writeFile(packPath, dump(qlpack));
}
async function injectExtensionPacks(
cliServer: cli.CodeQLCliServer,
queryPackDir: string,
workspaceFolders: string[],
) {
const qlpackFile = await getQlPackPath(queryPackDir);
if (!qlpackFile) {
throw new Error(
`Could not find ${QLPACK_FILENAMES.join(
" or ",
)} file in '${queryPackDir}'`,
);
}
const syntheticQueryPack = load(await readFile(qlpackFile, "utf8")) as QlPack;
const extensionPacks = await cliServer.resolveQlpacks(workspaceFolders, true);
Object.entries(extensionPacks).forEach(([name, paths]) => {
// We are guaranteed that there is at least one path found for each extension pack.
// If there are multiple paths, then we have a problem. This means that there is
// ambiguity in which path to use. This is an error.
if (paths.length > 1) {
throw new Error(
`Multiple versions of extension pack '${name}' found: ${paths.join(
", ",
)}`,
);
}
// Add this extension pack as a dependency. It doesn't matter which
// version we specify, since we are guaranteed that the extension pack
// is resolved from source at the given path.
syntheticQueryPack.dependencies[name] = "*";
});
await writeFile(qlpackFile, dump(syntheticQueryPack));
await cliServer.clearCache();
}
function updateDefaultSuite(qlpack: QlPack, packRelativePath: string) {
delete qlpack.defaultSuiteFile;
qlpack.defaultSuite = generateDefaultSuite(packRelativePath);
}
function generateDefaultSuite(packRelativePath: string) {
return [
{

View File

@@ -62,6 +62,7 @@ import { URLSearchParams } from "url";
import { DbManager } from "../databases/db-manager";
import { App } from "../common/app";
import { redactableError } from "../pure/errors";
import { AppCommandManager, VariantAnalysisCommands } from "../common/commands";
export class VariantAnalysisManager
extends DisposableObject
@@ -123,6 +124,18 @@ export class VariantAnalysisManager
);
}
getCommands(): VariantAnalysisCommands {
return {
"codeQL.openVariantAnalysisLogs": async (variantAnalysisId: number) => {
await this.openVariantAnalysisLogs(variantAnalysisId);
},
};
}
get commandManager(): AppCommandManager {
return this.app.commands;
}
public async runVariantAnalysis(
uri: Uri | undefined,
progress: ProgressCallback,

View File

@@ -2,6 +2,7 @@ import {
VariantAnalysis,
VariantAnalysisScannedRepositoryState,
} from "./shared/variant-analysis";
import { AppCommandManager } from "../common/commands";
export interface VariantAnalysisViewInterface {
variantAnalysisId: number;
@@ -11,6 +12,8 @@ export interface VariantAnalysisViewInterface {
export interface VariantAnalysisViewManager<
T extends VariantAnalysisViewInterface,
> {
commandManager: AppCommandManager;
registerView(view: T): void;
unregisterView(view: T): void;
getView(variantAnalysisId: number): T | undefined;

View File

@@ -145,7 +145,7 @@ export class VariantAnalysisView
);
break;
case "openLogs":
await commands.executeCommand(
await this.manager.commandManager.execute(
"codeQL.openVariantAnalysisLogs",
this.variantAnalysisId,
);

View File

@@ -6,6 +6,8 @@ import { createMockLogger } from "./loggerMock";
import { createMockMemento } from "../mock-memento";
import { testCredentialsWithStub } from "../factories/authentication";
import { Credentials } from "../../src/common/authentication";
import { AppCommandManager } from "../../src/common/commands";
import { createMockCommandManager } from "./commandsMock";
export function createMockApp({
extensionPath = "/mock/extension/path",
@@ -15,6 +17,7 @@ export function createMockApp({
executeCommand = jest.fn(() => Promise.resolve()),
workspaceState = createMockMemento(),
credentials = testCredentialsWithStub(),
commands = createMockCommandManager(),
}: {
extensionPath?: string;
workspaceStoragePath?: string;
@@ -23,6 +26,7 @@ export function createMockApp({
executeCommand?: () => Promise<void>;
workspaceState?: Memento;
credentials?: Credentials;
commands?: AppCommandManager;
}): App {
return {
mode: AppMode.Test,
@@ -35,6 +39,7 @@ export function createMockApp({
createEventEmitter,
executeCommand,
credentials,
commands,
};
}

View File

@@ -0,0 +1,13 @@
import { AppCommandManager } from "../../src/common/commands";
import { CommandFunction, CommandManager } from "../../src/packages/commands";
import { Disposable } from "../../src/packages/commands/Disposable";
export function createMockCommandManager({
registerCommand = jest.fn(),
executeCommand = jest.fn(),
}: {
registerCommand?: (commandName: string, fn: CommandFunction) => Disposable;
executeCommand?: (commandName: string, ...args: any[]) => Promise<any>;
} = {}): AppCommandManager {
return new CommandManager(registerCommand, executeCommand);
}

View File

@@ -19,5 +19,6 @@ export function createMockExtensionContext({
globalStorageUri: vscode.Uri.file(globalStoragePath),
storageUri: vscode.Uri.file(workspaceStoragePath),
workspaceState: createMockMemento(),
subscriptions: [],
} as any as vscode.ExtensionContext;
}

View File

@@ -1,8 +1,111 @@
import { CommandManager } from "../../../../src/packages/commands";
import {
CommandFunction,
CommandManager,
} from "../../../../src/packages/commands";
describe(CommandManager.name, () => {
it("can create a command manager", () => {
const commandManager = new CommandManager();
expect(commandManager).not.toBeUndefined();
describe("CommandManager", () => {
it("can register a command", () => {
const commandRegister = jest.fn();
const commandManager = new CommandManager<Record<string, CommandFunction>>(
commandRegister,
jest.fn(),
);
const myCommand = jest.fn();
commandManager.register("abc", myCommand);
expect(commandRegister).toHaveBeenCalledTimes(1);
expect(commandRegister).toHaveBeenCalledWith("abc", myCommand);
});
it("can register typed commands", async () => {
const commands = {
"codeQL.openVariantAnalysisLogs": async (variantAnalysisId: number) => {
return variantAnalysisId * 10;
},
};
const commandManager = new CommandManager<typeof commands>(
jest.fn(),
jest.fn(),
);
// @ts-expect-error wrong command name should give a type error
commandManager.register("abc", jest.fn());
commandManager.register(
"codeQL.openVariantAnalysisLogs",
// @ts-expect-error wrong function parameter type should give a type error
async (variantAnalysisId: string): Promise<number> => 10,
);
commandManager.register(
"codeQL.openVariantAnalysisLogs",
// @ts-expect-error wrong function return type should give a type error
async (variantAnalysisId: number): Promise<string> => "hello",
);
// Working types
commandManager.register(
"codeQL.openVariantAnalysisLogs",
async (variantAnalysisId: number): Promise<number> =>
variantAnalysisId * 10,
);
});
it("can dispose of its commands", () => {
const dispose1 = jest.fn();
const dispose2 = jest.fn();
const commandRegister = jest
.fn()
.mockReturnValueOnce({ dispose: dispose1 })
.mockReturnValueOnce({ dispose: dispose2 });
const commandManager = new CommandManager<Record<string, CommandFunction>>(
commandRegister,
jest.fn(),
);
commandManager.register("abc", jest.fn());
commandManager.register("def", jest.fn());
expect(dispose1).not.toHaveBeenCalled();
expect(dispose2).not.toHaveBeenCalled();
commandManager.dispose();
expect(dispose1).toHaveBeenCalledTimes(1);
expect(dispose2).toHaveBeenCalledTimes(1);
});
it("can execute a command", async () => {
const commandExecute = jest.fn().mockReturnValue(7);
const commandManager = new CommandManager<Record<string, CommandFunction>>(
jest.fn(),
commandExecute,
);
const result = await commandManager.execute("abc", "hello", true);
expect(result).toEqual(7);
expect(commandExecute).toHaveBeenCalledTimes(1);
expect(commandExecute).toHaveBeenCalledWith("abc", "hello", true);
});
it("can execute typed commands", async () => {
const commands = {
"codeQL.openVariantAnalysisLogs": async (variantAnalysisId: number) => {
return variantAnalysisId * 10;
},
};
const commandManager = new CommandManager<typeof commands>(
jest.fn(),
jest.fn(),
);
// @ts-expect-error wrong command name should give a type error
await commandManager.execute("abc", 4);
await commandManager.execute(
"codeQL.openVariantAnalysisLogs",
// @ts-expect-error wrong argument type should give a type error
"xyz",
);
// @ts-expect-error wrong number of arguments should give a type error
await commandManager.execute("codeQL.openVariantAnalysisLogs", 2, 3);
// Working types
await commandManager.execute("codeQL.openVariantAnalysisLogs", 7);
});
});

View File

@@ -0,0 +1,12 @@
extensions:
- addsTo:
pack: codeql/javascript-all
extensible: sourceModel
data:
- [ "@example/read-write-user-data", "Member[readUserData].ReturnValue.Awaited", "remote" ]
- addsTo:
pack: codeql/javascript-all
extensible: sinkModel
data:
- [ "@example/read-write-user-data", "Member[writeUserData].Argument[0]", "command-line-injection" ]

View File

@@ -0,0 +1,8 @@
name: github/extension-pack-for-testing
version: 0.0.0
library: true
extensionTargets:
github/remote-query-pack: "*"
dataExtensions:
- extension-file.yml

View File

@@ -12,7 +12,7 @@ import * as ghApiClient from "../../../../src/variant-analysis/gh-api/gh-api-cli
import { join } from "path";
import { VariantAnalysisManager } from "../../../../src/variant-analysis/variant-analysis-manager";
import { CodeQLCliServer } from "../../../../src/cli";
import { CliVersionConstraint, CodeQLCliServer } from "../../../../src/cli";
import {
fixWorkspaceReferences,
restoreWorkspaceReferences,
@@ -255,6 +255,30 @@ describe("Variant Analysis Manager", () => {
qlxFilesThatExist: ["subfolder/in-pack.qlx"],
});
});
it("should run a remote query with extension packs inside a qlpack", async () => {
if (!(await cli.cliConstraints.supportsQlpacksKind())) {
console.log(
`Skipping test because qlpacks kind is only suppported in CLI version ${CliVersionConstraint.CLI_VERSION_WITH_QLPACKS_KIND} or later.`,
);
return;
}
await cli.setUseExtensionPacks(true);
await doVariantAnalysisTest({
queryPath: "data-remote-qlpack-nested/subfolder/in-pack.ql",
filesThatExist: [
"subfolder/in-pack.ql",
"otherfolder/lib.qll",
".codeql/libraries/semmle/targets-extension/0.0.0/ext/extension.yml",
],
filesThatDoNotExist: ["subfolder/not-in-pack.ql"],
qlxFilesThatExist: ["subfolder/in-pack.qlx"],
dependenciesToCheck: [
"codeql/javascript-all",
"semmle/targets-extension",
],
});
});
});
async function doVariantAnalysisTest({
@@ -262,11 +286,13 @@ describe("Variant Analysis Manager", () => {
filesThatExist,
qlxFilesThatExist,
filesThatDoNotExist,
dependenciesToCheck = ["codeql/javascript-all"],
}: {
queryPath: string;
filesThatExist: string[];
qlxFilesThatExist: string[];
filesThatDoNotExist: string[];
dependenciesToCheck?: string[];
}) {
const fileUri = getFile(queryPath);
await variantAnalysisManager.runVariantAnalysis(
@@ -328,8 +354,10 @@ describe("Variant Analysis Manager", () => {
const actualLockKeys = Object.keys(qlpackLockContents.dependencies);
// The lock file should contain at least codeql/javascript-all.
expect(actualLockKeys).toContain("codeql/javascript-all");
// The lock file should contain at least the specified dependencies.
dependenciesToCheck.forEach((dep) =>
expect(actualLockKeys).toContain(dep),
);
}
function getFile(file: string): Uri {