Merge branch 'main' into selected-line-column

This commit is contained in:
Andrew Eisenberg
2023-06-01 08:07:18 -07:00
committed by GitHub
48 changed files with 1213 additions and 634 deletions

View File

@@ -19,7 +19,7 @@ class PureFile extends File {
this.getRelativePath().regexpMatch(".*/src/pure/.*") or
this.getRelativePath().regexpMatch(".*/src/common/.*")
) and
not this.getRelativePath().regexpMatch(".*/src/common/vscode/.*")
not this.getRelativePath().regexpMatch(".*/vscode/.*")
}
}

View File

@@ -516,6 +516,10 @@
"title": "Add new list",
"icon": "$(new-folder)"
},
{
"command": "codeQLVariantAnalysisRepositories.importFromCodeSearch",
"title": "Add repositories with GitHub Code Search"
},
{
"command": "codeQLVariantAnalysisRepositories.setSelectedItem",
"title": "Select"
@@ -961,6 +965,11 @@
"when": "view == codeQLVariantAnalysisRepositories && viewItem =~ /canBeOpenedOnGitHub/",
"group": "2_qlContextMenu@1"
},
{
"command": "codeQLVariantAnalysisRepositories.importFromCodeSearch",
"when": "view == codeQLVariantAnalysisRepositories && viewItem =~ /canImportCodeSearch/",
"group": "2_qlContextMenu@1"
},
{
"command": "codeQLDatabases.setCurrentDatabase",
"group": "inline",
@@ -1297,6 +1306,10 @@
"command": "codeQLVariantAnalysisRepositories.removeItemContextMenu",
"when": "false"
},
{
"command": "codeQLVariantAnalysisRepositories.importFromCodeSearch",
"when": "false"
},
{
"command": "codeQLDatabases.setCurrentDatabase",
"when": "false"

View File

@@ -218,7 +218,7 @@ export class CodeQLCliServer implements Disposable {
private readonly app: App,
private distributionProvider: DistributionProvider,
private cliConfig: CliConfig,
private logger: Logger,
public readonly logger: Logger,
) {
this.commandQueue = [];
this.commandInProcess = false;
@@ -330,6 +330,7 @@ export class CodeQLCliServer implements Disposable {
commandArgs: string[],
description: string,
onLine?: OnLineCallback,
silent?: boolean,
): Promise<string> {
const stderrBuffers: Buffer[] = [];
if (this.commandInProcess) {
@@ -349,7 +350,12 @@ export class CodeQLCliServer implements Disposable {
// Compute the full args array
const args = command.concat(LOGGING_FLAGS).concat(commandArgs);
const argsString = args.join(" ");
void this.logger.log(`${description} using CodeQL CLI: ${argsString}...`);
// If we are running silently, we don't want to print anything to the console.
if (!silent) {
void this.logger.log(
`${description} using CodeQL CLI: ${argsString}...`,
);
}
try {
await new Promise<void>((resolve, reject) => {
// Start listening to stdout
@@ -395,24 +401,30 @@ export class CodeQLCliServer implements Disposable {
const fullBuffer = Buffer.concat(stdoutBuffers);
// Make sure we remove the terminator;
const data = fullBuffer.toString("utf8", 0, fullBuffer.length - 1);
void this.logger.log("CLI command succeeded.");
if (!silent) {
void this.logger.log("CLI command succeeded.");
}
return data;
} catch (err) {
// Kill the process if it isn't already dead.
this.killProcessIfRunning();
// Report the error (if there is a stderr then use that otherwise just report the error cod or nodejs error)
// Report the error (if there is a stderr then use that otherwise just report the error code or nodejs error)
const newError =
stderrBuffers.length === 0
? new Error(`${description} failed: ${err}`)
? new Error(
`${description} failed with args:${EOL} ${argsString}${EOL}${err}`,
)
: new Error(
`${description} failed: ${Buffer.concat(stderrBuffers).toString(
"utf8",
)}`,
`${description} failed with args:${EOL} ${argsString}${EOL}${Buffer.concat(
stderrBuffers,
).toString("utf8")}`,
);
newError.stack += getErrorStack(err);
throw newError;
} finally {
void this.logger.log(Buffer.concat(stderrBuffers).toString("utf8"));
if (!silent) {
void this.logger.log(Buffer.concat(stderrBuffers).toString("utf8"));
}
// Remove the listeners we set up.
process.stdout.removeAllListeners("data");
process.stderr.removeAllListeners("data");
@@ -549,9 +561,11 @@ export class CodeQLCliServer implements Disposable {
{
progressReporter,
onLine,
silent = false,
}: {
progressReporter?: ProgressReporter;
onLine?: OnLineCallback;
silent?: boolean;
} = {},
): Promise<string> {
if (progressReporter) {
@@ -567,6 +581,7 @@ export class CodeQLCliServer implements Disposable {
commandArgs,
description,
onLine,
silent,
).then(resolve, reject);
} catch (err) {
reject(err);
@@ -600,10 +615,12 @@ export class CodeQLCliServer implements Disposable {
addFormat = true,
progressReporter,
onLine,
silent = false,
}: {
addFormat?: boolean;
progressReporter?: ProgressReporter;
onLine?: OnLineCallback;
silent?: boolean;
} = {},
): Promise<OutputType> {
let args: string[] = [];
@@ -614,6 +631,7 @@ export class CodeQLCliServer implements Disposable {
const result = await this.runCodeQlCliCommand(command, args, description, {
progressReporter,
onLine,
silent,
});
try {
return JSON.parse(result) as OutputType;
@@ -739,14 +757,19 @@ export class CodeQLCliServer implements Disposable {
/**
* Finds all available queries in a given directory.
* @param queryDir Root of directory tree to search for queries.
* @param silent If true, don't print logs to the CodeQL extension log.
* @returns The list of queries that were found.
*/
public async resolveQueries(queryDir: string): Promise<ResolvedQueries> {
public async resolveQueries(
queryDir: string,
silent?: boolean,
): Promise<ResolvedQueries> {
const subcommandArgs = [queryDir];
return await this.runJsonCodeQlCliCommand<ResolvedQueries>(
["resolve", "queries"],
subcommandArgs,
"Resolving queries",
{ silent },
);
}
@@ -1050,6 +1073,7 @@ export class CodeQLCliServer implements Disposable {
resultsPath: string,
interpretedResultsPath: string,
sourceInfo?: SourceInfo,
args?: string[],
): Promise<sarif.Log> {
const additionalArgs = [
// TODO: This flag means that we don't group interpreted results
@@ -1057,6 +1081,7 @@ export class CodeQLCliServer implements Disposable {
// interpretation with and without this flag, or do some
// grouping client-side.
"--no-group-results",
...(args ?? []),
];
await this.runInterpretCommand(

View File

@@ -6,12 +6,7 @@ import * as semver from "semver";
import { URL } from "url";
import { ExtensionContext, Event } from "vscode";
import { DistributionConfig } from "../config";
import {
InvocationRateLimiter,
InvocationRateLimiterResultKind,
showAndLogErrorMessage,
showAndLogWarningMessage,
} from "../helpers";
import { showAndLogErrorMessage, showAndLogWarningMessage } from "../helpers";
import { extLogger } from "../common";
import { getCodeQlCliVersion } from "./cli-version";
import {
@@ -24,6 +19,10 @@ import {
extractZipArchive,
getRequiredAssetName,
} from "../pure/distribution";
import {
InvocationRateLimiter,
InvocationRateLimiterResultKind,
} from "../common/invocation-rate-limiter";
/**
* distribution.ts
@@ -76,7 +75,7 @@ export class DistributionManager implements DistributionProvider {
extensionContext,
);
this.updateCheckRateLimiter = new InvocationRateLimiter(
extensionContext,
extensionContext.globalState,
"extensionSpecificDistributionUpdateCheck",
() =>
this.extensionSpecificDistributionManager.checkForUpdatesToDistribution(),

View File

@@ -275,6 +275,7 @@ export type DatabasePanelCommands = {
"codeQLVariantAnalysisRepositories.openOnGitHubContextMenu": TreeViewContextSingleSelectionCommandFunction<DbTreeViewItem>;
"codeQLVariantAnalysisRepositories.renameItemContextMenu": TreeViewContextSingleSelectionCommandFunction<DbTreeViewItem>;
"codeQLVariantAnalysisRepositories.removeItemContextMenu": TreeViewContextSingleSelectionCommandFunction<DbTreeViewItem>;
"codeQLVariantAnalysisRepositories.importFromCodeSearch": TreeViewContextSingleSelectionCommandFunction<DbTreeViewItem>;
};
export type AstCfgCommands = {

View File

@@ -1,6 +1,6 @@
import { DisposableObject } from "../pure/disposable-object";
import { extLogger } from "./logging/vscode/loggers";
import { getErrorMessage } from "../pure/helpers-pure";
import { Logger } from "./logging";
/**
* Base class for "discovery" operations, which scan the file system to find specific kinds of
@@ -8,18 +8,28 @@ import { getErrorMessage } from "../pure/helpers-pure";
* same time.
*/
export abstract class Discovery<T> extends DisposableObject {
private retry = false;
private discoveryInProgress = false;
private restartWhenFinished = false;
private currentDiscoveryPromise: Promise<void> | undefined;
constructor(private readonly name: string) {
constructor(private readonly name: string, private readonly logger: Logger) {
super();
}
/**
* Returns the promise of the currently running refresh operation, if one is in progress.
* Otherwise returns a promise that resolves immediately.
*/
public waitForCurrentRefresh(): Promise<void> {
return this.currentDiscoveryPromise ?? Promise.resolve();
}
/**
* Force the discovery process to run. Normally invoked by the derived class when a relevant file
* system change is detected.
*
* Returns a promise that resolves when the refresh is complete, including any retries.
*/
public refresh(): void {
public refresh(): Promise<void> {
// We avoid having multiple discovery operations in progress at the same time. Otherwise, if we
// got a storm of refresh requests due to, say, the copying or deletion of a large directory
// tree, we could potentially spawn a separate simultaneous discovery operation for each
@@ -36,14 +46,16 @@ export abstract class Discovery<T> extends DisposableObject {
// other change notifications that might be coming along. However, this would create more
// latency in the common case, in order to save a bit of latency in the uncommon case.
if (this.discoveryInProgress) {
if (this.currentDiscoveryPromise !== undefined) {
// There's already a discovery operation in progress. Tell it to restart when it's done.
this.retry = true;
this.restartWhenFinished = true;
} else {
// No discovery in progress, so start one now.
this.discoveryInProgress = true;
this.launchDiscovery();
this.currentDiscoveryPromise = this.launchDiscovery().finally(() => {
this.currentDiscoveryPromise = undefined;
});
}
return this.currentDiscoveryPromise;
}
/**
@@ -51,34 +63,31 @@ export abstract class Discovery<T> extends DisposableObject {
* discovery operation completes, the `update` function will be invoked with the results of the
* discovery.
*/
private launchDiscovery(): void {
const discoveryPromise = this.discover();
discoveryPromise
.then((results) => {
if (!this.retry) {
// Update any listeners with the results of the discovery.
this.discoveryInProgress = false;
this.update(results);
}
})
private async launchDiscovery(): Promise<void> {
let results: T | undefined;
try {
results = await this.discover();
} catch (err) {
void this.logger.log(
`${this.name} failed. Reason: ${getErrorMessage(err)}`,
);
results = undefined;
}
.catch((err: unknown) => {
void extLogger.log(
`${this.name} failed. Reason: ${getErrorMessage(err)}`,
);
})
.finally(() => {
if (this.retry) {
// Another refresh request came in while we were still running a previous discovery
// operation. Since the discovery results we just computed are now stale, we'll launch
// another discovery operation instead of updating.
// Note that by doing this inside of `finally`, we will relaunch discovery even if the
// initial discovery operation failed.
this.retry = false;
this.launchDiscovery();
}
});
if (this.restartWhenFinished) {
// Another refresh request came in while we were still running a previous discovery
// operation. Since the discovery results we just computed are now stale, we'll launch
// another discovery operation instead of updating.
// We want to relaunch discovery regardless of if the initial discovery operation
// succeeded or failed.
this.restartWhenFinished = false;
await this.launchDiscovery();
} else {
// If the discovery was successful, then update any listeners with the results.
if (results !== undefined) {
this.update(results);
}
}
}
/**

View File

@@ -0,0 +1,89 @@
import { Memento } from "./memento";
/**
* Provides a utility method to invoke a function only if a minimum time interval has elapsed since
* the last invocation of that function.
*/
export class InvocationRateLimiter<T> {
constructor(
private readonly globalState: Memento,
private readonly funcIdentifier: string,
private readonly func: () => Promise<T>,
private readonly createDate: (dateString?: string) => Date = (s) =>
s ? new Date(s) : new Date(),
) {}
/**
* Invoke the function if `minSecondsSinceLastInvocation` seconds have elapsed since the last invocation.
*/
public async invokeFunctionIfIntervalElapsed(
minSecondsSinceLastInvocation: number,
): Promise<InvocationRateLimiterResult<T>> {
const updateCheckStartDate = this.createDate();
const lastInvocationDate = this.getLastInvocationDate();
if (
minSecondsSinceLastInvocation &&
lastInvocationDate &&
lastInvocationDate <= updateCheckStartDate &&
lastInvocationDate.getTime() + minSecondsSinceLastInvocation * 1000 >
updateCheckStartDate.getTime()
) {
return createRateLimitedResult();
}
const result = await this.func();
await this.setLastInvocationDate(updateCheckStartDate);
return createInvokedResult(result);
}
private getLastInvocationDate(): Date | undefined {
const maybeDateString: string | undefined = this.globalState.get(
InvocationRateLimiter._invocationRateLimiterPrefix + this.funcIdentifier,
);
return maybeDateString ? this.createDate(maybeDateString) : undefined;
}
private async setLastInvocationDate(date: Date): Promise<void> {
return await this.globalState.update(
InvocationRateLimiter._invocationRateLimiterPrefix + this.funcIdentifier,
date,
);
}
private static readonly _invocationRateLimiterPrefix =
"invocationRateLimiter_lastInvocationDate_";
}
export enum InvocationRateLimiterResultKind {
Invoked,
RateLimited,
}
/**
* The function was invoked and returned the value `result`.
*/
interface InvokedResult<T> {
kind: InvocationRateLimiterResultKind.Invoked;
result: T;
}
/**
* The function was not invoked as the minimum interval since the last invocation had not elapsed.
*/
interface RateLimitedResult {
kind: InvocationRateLimiterResultKind.RateLimited;
}
type InvocationRateLimiterResult<T> = InvokedResult<T> | RateLimitedResult;
function createInvokedResult<T>(result: T): InvokedResult<T> {
return {
kind: InvocationRateLimiterResultKind.Invoked,
result,
};
}
function createRateLimitedResult(): RateLimitedResult {
return {
kind: InvocationRateLimiterResultKind.RateLimited,
};
}

View File

@@ -1,9 +1,9 @@
import { showAndLogErrorMessage } from "../helpers";
import { showAndLogErrorMessage } from "../../helpers";
import {
ExplorerSelectionCommandFunction,
TreeViewContextMultiSelectionCommandFunction,
TreeViewContextSingleSelectionCommandFunction,
} from "./commands";
} from "../commands";
// A hack to match types that are not an array, which is useful to help avoid
// misusing createSingleSelectionCommand, e.g. where T accidentally gets instantiated

View File

@@ -0,0 +1,136 @@
import { CancellationTokenSource } from "vscode";
import { join } from "path";
import { runQuery } from "./external-api-usage-query";
import { CodeQLCliServer } from "../codeql-cli/cli";
import { QueryRunner } from "../query-server";
import { DatabaseItem } from "../databases/local-databases";
import { interpretResultsSarif } from "../query-results";
import { ProgressCallback } from "../common/vscode/progress";
type Options = {
cliServer: CodeQLCliServer;
queryRunner: QueryRunner;
databaseItem: DatabaseItem;
queryStorageDir: string;
progress: ProgressCallback;
};
export type UsageSnippetsBySignature = Record<string, string[]>;
export async function getAutoModelUsages({
cliServer,
queryRunner,
databaseItem,
queryStorageDir,
progress,
}: Options): Promise<UsageSnippetsBySignature> {
const maxStep = 1500;
const cancellationTokenSource = new CancellationTokenSource();
// This will re-run the query that was already run when opening the data extensions editor. This
// might be unnecessary, but this makes it really easy to get the path to the BQRS file which we
// need to interpret the results.
const queryResult = await runQuery({
cliServer,
queryRunner,
queryStorageDir,
databaseItem,
progress: (update) =>
progress({
maxStep,
step: update.step,
message: update.message,
}),
token: cancellationTokenSource.token,
});
if (!queryResult) {
throw new Error("Query failed");
}
progress({
maxStep,
step: 1100,
message: "Retrieving source location prefix",
});
// CodeQL needs to have access to the database to be able to retrieve the
// snippets from it. The source location prefix is used to determine the
// base path of the database.
const sourceLocationPrefix = await databaseItem.getSourceLocationPrefix(
cliServer,
);
const sourceArchiveUri = databaseItem.sourceArchive;
const sourceInfo =
sourceArchiveUri === undefined
? undefined
: {
sourceArchive: sourceArchiveUri.fsPath,
sourceLocationPrefix,
};
progress({
maxStep,
step: 1200,
message: "Interpreting results",
});
// Convert the results to SARIF so that Codeql will retrieve the snippets
// from the datababe. This means we don't need to do that in the extension
// and everything is handled by the CodeQL CLI.
const sarif = await interpretResultsSarif(
cliServer,
{
// To interpret the results we need to provide metadata about the query. We could do this using
// `resolveMetadata` but that would be an extra call to the CodeQL CLI server and would require
// us to know the path to the query on the filesystem. Since we know what the metadata should
// look like and the only metadata that the CodeQL CLI requires is an ID and the kind, we can
// simply use constants here.
kind: "problem",
id: "usage",
},
{
resultsPath: queryResult.outputDir.bqrsPath,
interpretedResultsPath: join(
queryStorageDir,
"interpreted-results.sarif",
),
},
sourceInfo,
["--sarif-add-snippets"],
);
progress({
maxStep,
step: 1400,
message: "Parsing results",
});
const snippets: UsageSnippetsBySignature = {};
const results = sarif.runs[0]?.results;
if (!results) {
throw new Error("No results");
}
// This will group the snippets by the method signature.
for (const result of results) {
const signature = result.message.text;
const snippet =
result.locations?.[0]?.physicalLocation?.contextRegion?.snippet?.text;
if (!signature || !snippet) {
continue;
}
if (!(signature in snippets)) {
snippets[signature] = [];
}
snippets[signature].push(snippet);
}
return snippets;
}

View File

@@ -6,11 +6,13 @@ import {
Method,
ModelRequest,
} from "./auto-model-api";
import type { UsageSnippetsBySignature } from "./auto-model-usages-query";
export function createAutoModelRequest(
language: string,
externalApiUsages: ExternalApiUsage[],
modeledMethods: Record<string, ModeledMethod>,
usages: UsageSnippetsBySignature,
): ModelRequest {
const request: ModelRequest = {
language,
@@ -29,6 +31,10 @@ export function createAutoModelRequest(
type: "none",
};
const usagesForMethod =
usages[externalApiUsage.signature] ??
externalApiUsage.usages.map((usage) => usage.label);
const numberOfArguments =
externalApiUsage.methodParameters === "()"
? 0
@@ -48,9 +54,7 @@ export function createAutoModelRequest(
modeledMethod.type === "none"
? undefined
: toMethodClassification(modeledMethod),
usages: externalApiUsage.usages
.slice(0, 10)
.map((usage) => usage.label),
usages: usagesForMethod.slice(0, 10),
input: `Argument[${argumentIndex}]`,
};

View File

@@ -7,9 +7,9 @@ export function decodeBqrsToExternalApiUsages(
const methodsByApiName = new Map<string, ExternalApiUsage>();
chunk?.tuples.forEach((tuple) => {
const signature = tuple[0] as string;
const supported = tuple[1] as boolean;
const usage = tuple[2] as Call;
const usage = tuple[0] as Call;
const signature = tuple[1] as string;
const supported = (tuple[2] as string) === "true";
const [packageWithType, methodDeclaration] = signature.split("#");

View File

@@ -45,6 +45,7 @@ import {
parsePredictedClassifications,
} from "./auto-model";
import { showLlmGeneration } from "../config";
import { getAutoModelUsages } from "./auto-model-usages-query";
function getQlSubmoduleFolder(): WorkspaceFolder | undefined {
const workspaceFolder = workspace.workspaceFolders?.find(
@@ -385,23 +386,66 @@ export class DataExtensionsEditorView extends AbstractWebview<
externalApiUsages: ExternalApiUsage[],
modeledMethods: Record<string, ModeledMethod>,
): Promise<void> {
const maxStep = 3000;
await this.showProgress({
step: 0,
maxStep,
message: "Retrieving usages",
});
const usages = await getAutoModelUsages({
cliServer: this.cliServer,
queryRunner: this.queryRunner,
queryStorageDir: this.queryStorageDir,
databaseItem: this.databaseItem,
progress: (update) => this.showProgress(update, maxStep),
});
await this.showProgress({
step: 1800,
maxStep,
message: "Creating request",
});
const request = createAutoModelRequest(
this.databaseItem.language,
externalApiUsages,
modeledMethods,
usages,
);
await this.showProgress({
step: 2000,
maxStep,
message: "Sending request",
});
const response = await autoModel(this.app.credentials, request);
await this.showProgress({
step: 2500,
maxStep,
message: "Parsing response",
});
const predictedModeledMethods = parsePredictedClassifications(
response.predicted,
);
await this.showProgress({
step: 2800,
maxStep,
message: "Applying results",
});
await this.postMessage({
t: "addModeledMethods",
modeledMethods: predictedModeledMethods,
overrideNone: true,
});
await this.clearProgress();
}
/*

View File

@@ -5,30 +5,28 @@ export const fetchExternalApisQuery: Query = {
* @name Usage of APIs coming from external libraries
* @description A list of 3rd party APIs used in the codebase.
* @tags telemetry
* @kind problem
* @id cs/telemetry/fetch-external-apis
*/
import csharp
import semmle.code.csharp.dataflow.internal.FlowSummaryImpl as FlowSummaryImpl
import ExternalApi
private Call aUsage(ExternalApi api) {
result.getTarget().getUnboundDeclaration() = api
}
private boolean isSupported(ExternalApi api) {
api.isSupported() and result = true
or
not api.isSupported() and
result = false
}
from ExternalApi api, string apiName, boolean supported, Call usage
where
apiName = api.getApiName() and
supported = isSupported(api) and
usage = aUsage(api)
select apiName, supported, usage
import csharp
import ExternalApi
private Call aUsage(ExternalApi api) { result.getTarget().getUnboundDeclaration() = api }
private boolean isSupported(ExternalApi api) {
api.isSupported() and result = true
or
not api.isSupported() and
result = false
}
from ExternalApi api, string apiName, boolean supported, Call usage
where
apiName = api.getApiName() and
supported = isSupported(api) and
usage = aUsage(api)
select usage, apiName, supported.toString(), "supported"
`,
dependencies: {
"ExternalApi.qll": `/** Provides classes and predicates related to handling APIs from external libraries. */

View File

@@ -5,11 +5,11 @@ export const fetchExternalApisQuery: Query = {
* @name Usage of APIs coming from external libraries
* @description A list of 3rd party APIs used in the codebase. Excludes test and generated code.
* @tags telemetry
* @kind problem
* @id java/telemetry/fetch-external-apis
*/
import java
import semmle.code.java.dataflow.internal.FlowSummaryImpl as FlowSummaryImpl
import ExternalApi
private Call aUsage(ExternalApi api) {
@@ -28,7 +28,7 @@ where
apiName = api.getApiName() and
supported = isSupported(api) and
usage = aUsage(api)
select apiName, supported, usage
select usage, apiName, supported.toString(), "supported"
`,
dependencies: {
"ExternalApi.qll": `/** Provides classes and predicates related to handling APIs from external libraries. */

View File

@@ -1,4 +1,13 @@
export type Query = {
/**
* The main query.
*
* It should select all usages of external APIs, and return the following result pattern:
* - usage: the usage of the external API. This is an entity.
* - apiName: the name of the external API. This is a string.
* - supported: whether the external API is supported by the extension. This should be a string representation of a boolean to satify the result pattern for a problem query.
* - "supported": a string literal. This is required to make the query a valid problem query.
*/
mainQuery: string;
dependencies?: {
[filename: string]: string;

View File

@@ -147,10 +147,46 @@ export class DbConfigStore extends DisposableObject {
await this.writeConfig(config);
}
/**
* Adds a list of remote repositories to an existing repository list and removes duplicates.
* @returns a list of repositories that were not added because the list reached 1000 entries.
*/
public async addRemoteReposToList(
repoNwoList: string[],
parentList: string,
): Promise<string[]> {
if (!this.config) {
throw Error("Cannot add variant analysis repos if config is not loaded");
}
const config = cloneDbConfig(this.config);
const parent = config.databases.variantAnalysis.repositoryLists.find(
(list) => list.name === parentList,
);
if (!parent) {
throw Error(`Cannot find parent list '${parentList}'`);
}
// Remove duplicates from the list of repositories.
const newRepositoriesList = [
...new Set([...parent.repositories, ...repoNwoList]),
];
parent.repositories = newRepositoriesList.slice(0, 1000);
const truncatedRepositories = newRepositoriesList.slice(1000);
await this.writeConfig(config);
return truncatedRepositories;
}
/**
* Adds one remote repository
* @returns either nothing, or, if a parentList is given AND the number of repos on that list reaches 1000 returns the repo that was not added.
*/
public async addRemoteRepo(
repoNwo: string,
parentList?: string,
): Promise<void> {
): Promise<string[]> {
if (!this.config) {
throw Error("Cannot add variant analysis repo if config is not loaded");
}
@@ -165,6 +201,7 @@ export class DbConfigStore extends DisposableObject {
);
}
const truncatedRepositories = [];
const config = cloneDbConfig(this.config);
if (parentList) {
const parent = config.databases.variantAnalysis.repositoryLists.find(
@@ -173,12 +210,15 @@ export class DbConfigStore extends DisposableObject {
if (!parent) {
throw Error(`Cannot find parent list '${parentList}'`);
} else {
parent.repositories.push(repoNwo);
const newRepositories = [...parent.repositories, repoNwo];
parent.repositories = newRepositories.slice(0, 1000);
truncatedRepositories.push(...newRepositories.slice(1000));
}
} else {
config.databases.variantAnalysis.repositories.push(repoNwo);
}
await this.writeConfig(config);
return truncatedRepositories;
}
public async addRemoteOwner(owner: string): Promise<void> {

View File

@@ -101,8 +101,15 @@ export class DbManager extends DisposableObject {
public async addNewRemoteRepo(
nwo: string,
parentList?: string,
): Promise<void> {
await this.dbConfigStore.addRemoteRepo(nwo, parentList);
): Promise<string[]> {
return await this.dbConfigStore.addRemoteRepo(nwo, parentList);
}
public async addNewRemoteReposToList(
nwoList: string[],
parentList: string,
): Promise<string[]> {
return await this.dbConfigStore.addRemoteReposToList(nwoList, parentList);
}
public async addNewRemoteOwner(owner: string): Promise<void> {

View File

@@ -49,7 +49,7 @@ import { LocalDatabasesCommands } from "../common/commands";
import {
createMultiSelectionCommand,
createSingleSelectionCommand,
} from "../common/selection-commands";
} from "../common/vscode/selection-commands";
enum SortOrder {
NameAsc = "NameAsc",

View File

@@ -1,4 +1,5 @@
import {
ProgressLocation,
QuickPickItem,
TreeView,
TreeViewExpansionEvent,
@@ -13,7 +14,10 @@ import {
getOwnerFromGitHubUrl,
isValidGitHubOwner,
} from "../../common/github-url-identifier-helper";
import { showAndLogErrorMessage } from "../../helpers";
import {
showAndLogErrorMessage,
showAndLogInformationMessage,
} from "../../helpers";
import { DisposableObject } from "../../pure/disposable-object";
import {
DbItem,
@@ -32,6 +36,8 @@ import { getControllerRepo } from "../../variant-analysis/run-remote-query";
import { getErrorMessage } from "../../pure/helpers-pure";
import { DatabasePanelCommands } from "../../common/commands";
import { App } from "../../common/app";
import { getCodeSearchRepositories } from "../../variant-analysis/gh-api/gh-api-client";
import { QueryLanguage } from "../../common/query-language";
export interface RemoteDatabaseQuickPickItem extends QuickPickItem {
remoteDatabaseKind: string;
@@ -41,6 +47,10 @@ export interface AddListQuickPickItem extends QuickPickItem {
databaseKind: DbListKind;
}
export interface CodeSearchQuickPickItem extends QuickPickItem {
language: string;
}
export class DbPanel extends DisposableObject {
private readonly dataProvider: DbTreeDataProvider;
private readonly treeView: TreeView<DbTreeViewItem>;
@@ -93,6 +103,8 @@ export class DbPanel extends DisposableObject {
this.renameItem.bind(this),
"codeQLVariantAnalysisRepositories.removeItemContextMenu":
this.removeItem.bind(this),
"codeQLVariantAnalysisRepositories.importFromCodeSearch":
this.importFromCodeSearch.bind(this),
};
}
@@ -171,7 +183,14 @@ export class DbPanel extends DisposableObject {
return;
}
await this.dbManager.addNewRemoteRepo(nwo, parentList);
const truncatedRepositories = await this.dbManager.addNewRemoteRepo(
nwo,
parentList,
);
if (parentList) {
this.reportAnyTruncatedRepos(truncatedRepositories, parentList);
}
}
private async addNewRemoteOwner(): Promise<void> {
@@ -323,6 +342,89 @@ export class DbPanel extends DisposableObject {
await this.dbManager.removeDbItem(treeViewItem.dbItem);
}
private async importFromCodeSearch(
treeViewItem: DbTreeViewItem,
): Promise<void> {
if (treeViewItem.dbItem?.kind !== DbItemKind.RemoteUserDefinedList) {
throw new Error("Please select a valid list to add code search results.");
}
const listName = treeViewItem.dbItem.listName;
const languageQuickPickItems: CodeSearchQuickPickItem[] = Object.values(
QueryLanguage,
).map((language) => ({
label: language.toString(),
alwaysShow: true,
language: language.toString(),
}));
const codeSearchLanguage =
await window.showQuickPick<CodeSearchQuickPickItem>(
languageQuickPickItems,
{
title: "Select a language for your search",
placeHolder: "Select an option",
ignoreFocusOut: true,
},
);
if (!codeSearchLanguage) {
return;
}
const codeSearchQuery = await window.showInputBox({
title: "GitHub Code Search",
prompt:
"Use [GitHub's Code Search syntax](https://docs.github.com/en/search-github/github-code-search/understanding-github-code-search-syntax), including code qualifiers, regular expressions, and boolean operations, to search for repositories.",
placeHolder: "org:github",
});
if (codeSearchQuery === undefined || codeSearchQuery === "") {
return;
}
void window.withProgress(
{
location: ProgressLocation.Notification,
title: "Searching for repositories... This might take a while",
cancellable: true,
},
async (progress, token) => {
progress.report({ increment: 10 });
const repositories = await getCodeSearchRepositories(
this.app.credentials,
`${codeSearchQuery} language:${codeSearchLanguage.language}`,
progress,
token,
);
token.onCancellationRequested(() => {
void showAndLogInformationMessage("Code search cancelled");
return;
});
progress.report({ increment: 10, message: "Processing results..." });
const truncatedRepositories =
await this.dbManager.addNewRemoteReposToList(repositories, listName);
this.reportAnyTruncatedRepos(truncatedRepositories, listName);
},
);
}
private reportAnyTruncatedRepos(
truncatedRepositories: string[],
listName: string,
) {
if (truncatedRepositories.length > 0) {
void showAndLogErrorMessage(
`Some repositories were not added to '${listName}' because a list can only have 1000 entries. Excluded repositories: ${truncatedRepositories.join(
", ",
)}`,
);
}
}
private async onDidCollapseElement(
event: TreeViewExpansionEvent<DbTreeViewItem>,
): Promise<void> {

View File

@@ -4,7 +4,8 @@ export type DbTreeViewItemAction =
| "canBeSelected"
| "canBeRemoved"
| "canBeRenamed"
| "canBeOpenedOnGitHub";
| "canBeOpenedOnGitHub"
| "canImportCodeSearch";
export function getDbItemActions(dbItem: DbItem): DbTreeViewItemAction[] {
const actions: DbTreeViewItemAction[] = [];
@@ -21,7 +22,9 @@ export function getDbItemActions(dbItem: DbItem): DbTreeViewItemAction[] {
if (canBeOpenedOnGitHub(dbItem)) {
actions.push("canBeOpenedOnGitHub");
}
if (canImportCodeSearch(dbItem)) {
actions.push("canImportCodeSearch");
}
return actions;
}
@@ -60,6 +63,10 @@ function canBeOpenedOnGitHub(dbItem: DbItem): boolean {
return dbItemKindsThatCanBeOpenedOnGitHub.includes(dbItem.kind);
}
function canImportCodeSearch(dbItem: DbItem): boolean {
return DbItemKind.RemoteUserDefinedList === dbItem.kind;
}
export function getGitHubUrl(dbItem: DbItem): string | undefined {
switch (dbItem.kind) {
case DbItemKind.RemoteOwner:

View File

@@ -10,14 +10,7 @@ import { glob } from "glob";
import { load } from "js-yaml";
import { join, basename, dirname } from "path";
import { dirSync } from "tmp-promise";
import {
ExtensionContext,
Uri,
window as Window,
workspace,
env,
WorkspaceFolder,
} from "vscode";
import { Uri, window as Window, workspace, env, WorkspaceFolder } from "vscode";
import { CodeQLCliServer, QlpacksInfo } from "./codeql-cli/cli";
import { UserCancellationException } from "./common/vscode/progress";
import { extLogger, OutputChannelLogger } from "./common";
@@ -363,106 +356,6 @@ export async function prepareCodeTour(
}
}
/**
* Provides a utility method to invoke a function only if a minimum time interval has elapsed since
* the last invocation of that function.
*/
export class InvocationRateLimiter<T> {
constructor(
extensionContext: ExtensionContext,
funcIdentifier: string,
func: () => Promise<T>,
createDate: (dateString?: string) => Date = (s) =>
s ? new Date(s) : new Date(),
) {
this._createDate = createDate;
this._extensionContext = extensionContext;
this._func = func;
this._funcIdentifier = funcIdentifier;
}
/**
* Invoke the function if `minSecondsSinceLastInvocation` seconds have elapsed since the last invocation.
*/
public async invokeFunctionIfIntervalElapsed(
minSecondsSinceLastInvocation: number,
): Promise<InvocationRateLimiterResult<T>> {
const updateCheckStartDate = this._createDate();
const lastInvocationDate = this.getLastInvocationDate();
if (
minSecondsSinceLastInvocation &&
lastInvocationDate &&
lastInvocationDate <= updateCheckStartDate &&
lastInvocationDate.getTime() + minSecondsSinceLastInvocation * 1000 >
updateCheckStartDate.getTime()
) {
return createRateLimitedResult();
}
const result = await this._func();
await this.setLastInvocationDate(updateCheckStartDate);
return createInvokedResult(result);
}
private getLastInvocationDate(): Date | undefined {
const maybeDateString: string | undefined =
this._extensionContext.globalState.get(
InvocationRateLimiter._invocationRateLimiterPrefix +
this._funcIdentifier,
);
return maybeDateString ? this._createDate(maybeDateString) : undefined;
}
private async setLastInvocationDate(date: Date): Promise<void> {
return await this._extensionContext.globalState.update(
InvocationRateLimiter._invocationRateLimiterPrefix + this._funcIdentifier,
date,
);
}
private readonly _createDate: (dateString?: string) => Date;
private readonly _extensionContext: ExtensionContext;
private readonly _func: () => Promise<T>;
private readonly _funcIdentifier: string;
private static readonly _invocationRateLimiterPrefix =
"invocationRateLimiter_lastInvocationDate_";
}
export enum InvocationRateLimiterResultKind {
Invoked,
RateLimited,
}
/**
* The function was invoked and returned the value `result`.
*/
interface InvokedResult<T> {
kind: InvocationRateLimiterResultKind.Invoked;
result: T;
}
/**
* The function was not invoked as the minimum interval since the last invocation had not elapsed.
*/
interface RateLimitedResult {
kind: InvocationRateLimiterResultKind.RateLimited;
}
type InvocationRateLimiterResult<T> = InvokedResult<T> | RateLimitedResult;
function createInvokedResult<T>(result: T): InvokedResult<T> {
return {
kind: InvocationRateLimiterResultKind.Invoked,
result,
};
}
function createRateLimitedResult(): RateLimitedResult {
return {
kind: InvocationRateLimiterResultKind.RateLimited,
};
}
export interface QlPacksForLanguage {
/** The name of the pack containing the dbscheme. */
dbschemePack: string;
@@ -584,77 +477,6 @@ export async function getPrimaryDbscheme(
return dbscheme;
}
/**
* A cached mapping from strings to value of type U.
*/
export class CachedOperation<U> {
private readonly operation: (t: string, ...args: any[]) => Promise<U>;
private readonly cached: Map<string, U>;
private readonly lru: string[];
private readonly inProgressCallbacks: Map<
string,
Array<[(u: U) => void, (reason?: any) => void]>
>;
constructor(
operation: (t: string, ...args: any[]) => Promise<U>,
private cacheSize = 100,
) {
this.operation = operation;
this.lru = [];
this.inProgressCallbacks = new Map<
string,
Array<[(u: U) => void, (reason?: any) => void]>
>();
this.cached = new Map<string, U>();
}
async get(t: string, ...args: any[]): Promise<U> {
// Try and retrieve from the cache
const fromCache = this.cached.get(t);
if (fromCache !== undefined) {
// Move to end of lru list
this.lru.push(
this.lru.splice(
this.lru.findIndex((v) => v === t),
1,
)[0],
);
return fromCache;
}
// Otherwise check if in progress
const inProgressCallback = this.inProgressCallbacks.get(t);
if (inProgressCallback !== undefined) {
// If so wait for it to resolve
return await new Promise((resolve, reject) => {
inProgressCallback.push([resolve, reject]);
});
}
// Otherwise compute the new value, but leave a callback to allow sharing work
const callbacks: Array<[(u: U) => void, (reason?: any) => void]> = [];
this.inProgressCallbacks.set(t, callbacks);
try {
const result = await this.operation(t, ...args);
callbacks.forEach((f) => f[0](result));
this.inProgressCallbacks.delete(t);
if (this.lru.length > this.cacheSize) {
const toRemove = this.lru.shift()!;
this.cached.delete(toRemove);
}
this.lru.push(t);
this.cached.set(t, result);
return result;
} catch (e) {
// Rethrow error on all callbacks
callbacks.forEach((f) => f[1](e));
throw e;
} finally {
this.inProgressCallbacks.delete(t);
}
}
}
/**
* The following functions al heuristically determine metadata about databases.
*/

View File

@@ -17,7 +17,7 @@ import {
} from "../../common/vscode/archive-filesystem-provider";
import { CodeQLCliServer } from "../../codeql-cli/cli";
import { DatabaseManager } from "../../databases/local-databases";
import { CachedOperation } from "../../helpers";
import { CachedOperation } from "../../pure/cached-operation";
import { ProgressCallback, withProgress } from "../../common/vscode/progress";
import { KeyType } from "./key-type";
import {

View File

@@ -47,7 +47,7 @@ import { App } from "../common/app";
import { DisposableObject } from "../pure/disposable-object";
import { SkeletonQueryWizard } from "../skeleton-query-wizard";
import { LocalQueryRun } from "./local-query-run";
import { createMultiSelectionCommand } from "../common/selection-commands";
import { createMultiSelectionCommand } from "../common/vscode/selection-commands";
interface DatabaseQuickPickItem extends QuickPickItem {
databaseItem: DatabaseItem;

View File

@@ -115,7 +115,7 @@ export type BqrsKind =
| "Entity";
interface BqrsColumn {
name: string;
name?: string;
kind: BqrsKind;
}
export interface DecodedBqrsChunk {

View File

@@ -0,0 +1,70 @@
/**
* A cached mapping from strings to value of type U.
*/
export class CachedOperation<U> {
private readonly operation: (t: string, ...args: any[]) => Promise<U>;
private readonly cached: Map<string, U>;
private readonly lru: string[];
private readonly inProgressCallbacks: Map<
string,
Array<[(u: U) => void, (reason?: any) => void]>
>;
constructor(
operation: (t: string, ...args: any[]) => Promise<U>,
private cacheSize = 100,
) {
this.operation = operation;
this.lru = [];
this.inProgressCallbacks = new Map<
string,
Array<[(u: U) => void, (reason?: any) => void]>
>();
this.cached = new Map<string, U>();
}
async get(t: string, ...args: any[]): Promise<U> {
// Try and retrieve from the cache
const fromCache = this.cached.get(t);
if (fromCache !== undefined) {
// Move to end of lru list
this.lru.push(
this.lru.splice(
this.lru.findIndex((v) => v === t),
1,
)[0],
);
return fromCache;
}
// Otherwise check if in progress
const inProgressCallback = this.inProgressCallbacks.get(t);
if (inProgressCallback !== undefined) {
// If so wait for it to resolve
return await new Promise((resolve, reject) => {
inProgressCallback.push([resolve, reject]);
});
}
// Otherwise compute the new value, but leave a callback to allow sharing work
const callbacks: Array<[(u: U) => void, (reason?: any) => void]> = [];
this.inProgressCallbacks.set(t, callbacks);
try {
const result = await this.operation(t, ...args);
callbacks.forEach((f) => f[0](result));
this.inProgressCallbacks.delete(t);
if (this.lru.length > this.cacheSize) {
const toRemove = this.lru.shift()!;
this.cached.delete(toRemove);
}
this.lru.push(t);
this.cached.set(t, result);
return result;
} catch (e) {
// Rethrow error on all callbacks
callbacks.forEach((f) => f[1](e));
throw e;
} finally {
this.inProgressCallbacks.delete(t);
}
}
}

View File

@@ -21,7 +21,7 @@ export class QueriesModule extends DisposableObject {
const queryDiscovery = new QueryDiscovery(app, cliServer);
this.push(queryDiscovery);
queryDiscovery.refresh();
void queryDiscovery.refresh();
const queriesPanel = new QueriesPanel(queryDiscovery);
this.push(queriesPanel);

View File

@@ -8,6 +8,7 @@ import { FileTreeDirectory, FileTreeLeaf } from "../common/file-tree-nodes";
import { getOnDiskWorkspaceFoldersObjects } from "../helpers";
import { AppEventEmitter } from "../common/events";
import { QueryDiscoverer } from "./query-tree-data-provider";
import { extLogger } from "../common";
/**
* The results of discovering queries.
@@ -41,7 +42,7 @@ export class QueryDiscovery
);
constructor(app: App, private readonly cliServer: CodeQLCliServer) {
super("Query Discovery");
super("Query Discovery", extLogger);
this.onDidChangeQueriesEmitter = this.push(app.createEventEmitter<void>());
this.push(app.onDidChangeWorkspaceFolders(this.refresh.bind(this)));
@@ -113,7 +114,12 @@ export class QueryDiscovery
const fullPath = workspaceFolder.uri.fsPath;
const name = workspaceFolder.name;
const resolvedQueries = await this.cliServer.resolveQueries(fullPath);
// We don't want to log each invocation of resolveQueries, since it clutters up the log.
const silent = true;
const resolvedQueries = await this.cliServer.resolveQueries(
fullPath,
silent,
);
if (resolvedQueries.length === 0) {
return undefined;
}

View File

@@ -59,7 +59,7 @@ import { tryOpenExternalFile } from "../common/vscode/external-files";
import {
createMultiSelectionCommand,
createSingleSelectionCommand,
} from "../common/selection-commands";
} from "../common/vscode/selection-commands";
/**
* query-history-manager.ts

View File

@@ -139,6 +139,7 @@ export async function interpretResultsSarif(
metadata: QueryMetadata | undefined,
resultsPaths: ResultsPaths,
sourceInfo?: cli.SourceInfo,
args?: string[],
): Promise<SarifInterpretationData> {
const { resultsPath, interpretedResultsPath } = resultsPaths;
let res;
@@ -150,6 +151,7 @@ export async function interpretResultsSarif(
resultsPath,
interpretedResultsPath,
sourceInfo,
args,
);
}
return { ...res, t: "SarifInterpretationData" };

View File

@@ -11,6 +11,7 @@ import { MultiFileSystemWatcher } from "../common/vscode/multi-file-system-watch
import { CodeQLCliServer } from "../codeql-cli/cli";
import { pathExists } from "fs-extra";
import { FileTreeDirectory, FileTreeLeaf } from "../common/file-tree-nodes";
import { extLogger } from "../common";
/**
* The results of discovering QL tests.
@@ -42,7 +43,7 @@ export class QLTestDiscovery extends Discovery<QLTestDiscoveryResults> {
private readonly workspaceFolder: WorkspaceFolder,
private readonly cliServer: CodeQLCliServer,
) {
super("QL Test Discovery");
super("QL Test Discovery", extLogger);
this.push(this.watcher.onDidChange(this.handleDidChange, this));
}
@@ -64,7 +65,7 @@ export class QLTestDiscovery extends Discovery<QLTestDiscoveryResults> {
private handleDidChange(uri: Uri): void {
if (!QLTestDiscovery.ignoreTestPath(uri.fsPath)) {
this.refresh();
void this.refresh();
}
}
protected async discover(): Promise<QLTestDiscoveryResults> {

View File

@@ -115,7 +115,7 @@ export class QLTestAdapter extends DisposableObject implements TestAdapter {
this.qlTestDiscovery = this.push(
new QLTestDiscovery(workspaceFolder, cliServer),
);
this.qlTestDiscovery.refresh();
void this.qlTestDiscovery.refresh();
this.push(this.qlTestDiscovery.onDidChangeTests(this.discoverTests, this));
}

View File

@@ -92,7 +92,7 @@ class WorkspaceFolderHandler extends DisposableObject {
this.push(
this.testDiscovery.onDidChangeTests(this.handleDidChangeTests, this),
);
this.testDiscovery.refresh();
void this.testDiscovery.refresh();
}
private handleDidChangeTests(): void {

View File

@@ -7,6 +7,43 @@ import {
VariantAnalysisSubmissionRequest,
} from "./variant-analysis";
import { Repository } from "./repository";
import { Progress } from "vscode";
import { CancellationToken } from "vscode-jsonrpc";
export async function getCodeSearchRepositories(
credentials: Credentials,
query: string,
progress: Progress<{
message?: string | undefined;
increment?: number | undefined;
}>,
token: CancellationToken,
): Promise<string[]> {
let nwos: string[] = [];
const octokit = await credentials.getOctokit();
for await (const response of octokit.paginate.iterator(
octokit.rest.search.repos,
{
q: query,
per_page: 100,
},
)) {
nwos.push(...response.data.map((item) => item.full_name));
// calculate progress bar: 80% of the progress bar is used for the code search
const totalNumberOfRequests = Math.ceil(response.data.total_count / 100);
// Since we have a maximum 10 of requests, we use a fixed increment whenever the totalNumberOfRequests is greater than 10
const increment =
totalNumberOfRequests < 10 ? 80 / totalNumberOfRequests : 8;
progress.report({ increment });
if (token.isCancellationRequested) {
nwos = [];
break;
}
}
return [...new Set(nwos)];
}
export async function submitVariantAnalysis(
credentials: Credentials,

View File

@@ -1,5 +1,5 @@
[
"v2.13.1",
"v2.13.3",
"v2.12.7",
"v2.11.6",
"v2.7.6",

View File

@@ -0,0 +1,143 @@
import type { Memento } from "vscode";
import { InvocationRateLimiter } from "../../../src/common/invocation-rate-limiter";
describe("Invocation rate limiter", () => {
// 1 January 2020
let currentUnixTime = 1577836800;
function createDate(dateString?: string): Date {
if (dateString) {
return new Date(dateString);
}
const numMillisecondsPerSecond = 1000;
return new Date(currentUnixTime * numMillisecondsPerSecond);
}
function createInvocationRateLimiter<T>(
funcIdentifier: string,
func: () => Promise<T>,
): InvocationRateLimiter<T> {
return new InvocationRateLimiter(
new MockMemento(),
funcIdentifier,
func,
(s) => createDate(s),
);
}
class MockMemento implements Memento {
keys(): readonly string[] {
throw new Error("Method not implemented.");
}
map = new Map<any, any>();
/**
* Return a value.
*
* @param key A string.
* @param defaultValue A value that should be returned when there is no
* value (`undefined`) with the given key.
* @return The stored value or the defaultValue.
*/
get<T>(key: string, defaultValue?: T): T {
return this.map.has(key) ? this.map.get(key) : defaultValue;
}
/**
* Store a value. The value must be JSON-stringifyable.
*
* @param key A string.
* @param value A value. MUST not contain cyclic references.
*/
async update(key: string, value: any): Promise<void> {
this.map.set(key, value);
}
}
it("initially invokes function", async () => {
let numTimesFuncCalled = 0;
const invocationRateLimiter = createInvocationRateLimiter(
"funcid",
async () => {
numTimesFuncCalled++;
},
);
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(100);
expect(numTimesFuncCalled).toBe(1);
});
it("doesn't invoke function again if no time has passed", async () => {
let numTimesFuncCalled = 0;
const invocationRateLimiter = createInvocationRateLimiter(
"funcid",
async () => {
numTimesFuncCalled++;
},
);
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(100);
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(100);
expect(numTimesFuncCalled).toBe(1);
});
it("doesn't invoke function again if requested time since last invocation hasn't passed", async () => {
let numTimesFuncCalled = 0;
const invocationRateLimiter = createInvocationRateLimiter(
"funcid",
async () => {
numTimesFuncCalled++;
},
);
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(100);
currentUnixTime += 1;
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(2);
expect(numTimesFuncCalled).toBe(1);
});
it("invokes function again immediately if requested time since last invocation is 0 seconds", async () => {
let numTimesFuncCalled = 0;
const invocationRateLimiter = createInvocationRateLimiter(
"funcid",
async () => {
numTimesFuncCalled++;
},
);
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(0);
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(0);
expect(numTimesFuncCalled).toBe(2);
});
it("invokes function again after requested time since last invocation has elapsed", async () => {
let numTimesFuncCalled = 0;
const invocationRateLimiter = createInvocationRateLimiter(
"funcid",
async () => {
numTimesFuncCalled++;
},
);
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(1);
currentUnixTime += 1;
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(1);
expect(numTimesFuncCalled).toBe(2);
});
it("invokes functions with different rate limiters", async () => {
let numTimesFuncACalled = 0;
const invocationRateLimiterA = createInvocationRateLimiter(
"funcid",
async () => {
numTimesFuncACalled++;
},
);
let numTimesFuncBCalled = 0;
const invocationRateLimiterB = createInvocationRateLimiter(
"funcid",
async () => {
numTimesFuncBCalled++;
},
);
await invocationRateLimiterA.invokeFunctionIfIntervalElapsed(100);
await invocationRateLimiterB.invokeFunctionIfIntervalElapsed(100);
expect(numTimesFuncACalled).toBe(1);
expect(numTimesFuncBCalled).toBe(1);
});
});

View File

@@ -200,9 +200,36 @@ describe("createAutoModelRequest", () => {
},
};
const usages: Record<string, string[]> = {
"org.springframework.boot.SpringApplication#run(Class,String[])": [
"public class Sql2oExampleApplication {\n public static void main(String[] args) {\n SpringApplication.run(Sql2oExampleApplication.class, args);\n }\n}",
],
"org.sql2o.Connection#createQuery(String)": [
' public String index(@RequestParam("id") String id) {\n try (var con = sql2o.open()) {\n con.createQuery("select 1 where id = " + id).executeScalar(Integer.class);\n }\n\n',
'\n try (var con = sql2o.open()) {\n con.createQuery("select 1").executeScalar(Integer.class);\n }\n\n',
],
"org.sql2o.Query#executeScalar(Class)": [
' public String index(@RequestParam("id") String id) {\n try (var con = sql2o.open()) {\n con.createQuery("select 1 where id = " + id).executeScalar(Integer.class);\n }\n\n',
'\n try (var con = sql2o.open()) {\n con.createQuery("select 1").executeScalar(Integer.class);\n }\n\n',
],
"org.sql2o.Sql2o#open()": [
' @GetMapping("/")\n public String index(@RequestParam("id") String id) {\n try (var con = sql2o.open()) {\n con.createQuery("select 1 where id = " + id).executeScalar(Integer.class);\n }\n',
' Sql2o sql2o = new Sql2o(url);\n\n try (var con = sql2o.open()) {\n con.createQuery("select 1").executeScalar(Integer.class);\n }\n',
],
"java.io.PrintStream#println(String)": [
' }\n\n System.out.println("Connected to " + url);\n\n return "Greetings from Spring Boot!";\n',
],
"org.sql2o.Sql2o#Sql2o(String,String,String)": [
'@RestController\npublic class HelloController {\n private final Sql2o sql2o = new Sql2o("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1","sa", "");\n\n @GetMapping("/")\n',
],
"org.sql2o.Sql2o#Sql2o(String)": [
' @GetMapping("/connect")\n public String connect(@RequestParam("url") String url) {\n Sql2o sql2o = new Sql2o(url);\n\n try (var con = sql2o.open()) {\n',
],
};
it("creates a matching request", () => {
expect(
createAutoModelRequest("java", externalApiUsages, modeledMethods),
createAutoModelRequest("java", externalApiUsages, modeledMethods, usages),
).toEqual({
language: "java",
samples: [
@@ -216,7 +243,7 @@ describe("createAutoModelRequest", () => {
kind: "jndi-injection",
explanation: "",
},
usages: ["new Sql2o(...)"],
usages: usages["org.sql2o.Sql2o#Sql2o(String)"],
input: "Argument[0]",
},
],
@@ -226,64 +253,78 @@ describe("createAutoModelRequest", () => {
type: "Connection",
name: "createQuery",
signature: "(String)",
usages: ["createQuery(...)", "createQuery(...)"],
usages: usages["org.sql2o.Connection#createQuery(String)"],
input: "Argument[0]",
classification: undefined,
},
{
package: "org.sql2o",
type: "Query",
name: "executeScalar",
signature: "(Class)",
usages: ["executeScalar(...)", "executeScalar(...)"],
usages: usages["org.sql2o.Query#executeScalar(Class)"],
input: "Argument[0]",
classification: undefined,
},
{
package: "org.springframework.boot",
type: "SpringApplication",
name: "run",
signature: "(Class,String[])",
usages: ["run(...)"],
usages:
usages[
"org.springframework.boot.SpringApplication#run(Class,String[])"
],
input: "Argument[0]",
classification: undefined,
},
{
package: "org.springframework.boot",
type: "SpringApplication",
name: "run",
signature: "(Class,String[])",
usages: ["run(...)"],
usages:
usages[
"org.springframework.boot.SpringApplication#run(Class,String[])"
],
input: "Argument[1]",
classification: undefined,
},
{
package: "java.io",
type: "PrintStream",
name: "println",
signature: "(String)",
usages: ["println(...)"],
usages: usages["java.io.PrintStream#println(String)"],
input: "Argument[0]",
classification: undefined,
},
{
package: "org.sql2o",
type: "Sql2o",
name: "Sql2o",
signature: "(String,String,String)",
usages: ["new Sql2o(...)"],
usages: usages["org.sql2o.Sql2o#Sql2o(String,String,String)"],
input: "Argument[0]",
classification: undefined,
},
{
package: "org.sql2o",
type: "Sql2o",
name: "Sql2o",
signature: "(String,String,String)",
usages: ["new Sql2o(...)"],
usages: usages["org.sql2o.Sql2o#Sql2o(String,String,String)"],
input: "Argument[1]",
classification: undefined,
},
{
package: "org.sql2o",
type: "Sql2o",
name: "Sql2o",
signature: "(String,String,String)",
usages: ["new Sql2o(...)"],
usages: usages["org.sql2o.Sql2o#Sql2o(String,String,String)"],
input: "Argument[2]",
classification: undefined,
},
],
});

View File

@@ -4,14 +4,13 @@ import { DecodedBqrsChunk } from "../../../src/pure/bqrs-cli-types";
describe("decodeBqrsToExternalApiUsages", () => {
const chunk: DecodedBqrsChunk = {
columns: [
{ name: "apiName", kind: "String" },
{ name: "supported", kind: "Boolean" },
{ name: "usage", kind: "Entity" },
{ name: "apiName", kind: "String" },
{ kind: "String" },
{ kind: "String" },
],
tuples: [
[
"java.io.PrintStream#println(String)",
true,
{
label: "println(...)",
url: {
@@ -22,10 +21,11 @@ describe("decodeBqrsToExternalApiUsages", () => {
endColumn: 49,
},
},
"java.io.PrintStream#println(String)",
"true",
"supported",
],
[
"org.springframework.boot.SpringApplication#run(Class,String[])",
false,
{
label: "run(...)",
url: {
@@ -36,10 +36,11 @@ describe("decodeBqrsToExternalApiUsages", () => {
endColumn: 66,
},
},
"org.springframework.boot.SpringApplication#run(Class,String[])",
"false",
"supported",
],
[
"org.sql2o.Connection#createQuery(String)",
true,
{
label: "createQuery(...)",
url: {
@@ -50,10 +51,11 @@ describe("decodeBqrsToExternalApiUsages", () => {
endColumn: 56,
},
},
"org.sql2o.Connection#createQuery(String)",
"true",
"supported",
],
[
"org.sql2o.Connection#createQuery(String)",
true,
{
label: "createQuery(...)",
url: {
@@ -64,10 +66,11 @@ describe("decodeBqrsToExternalApiUsages", () => {
endColumn: 39,
},
},
"org.sql2o.Connection#createQuery(String)",
"true",
"supported",
],
[
"org.sql2o.Query#executeScalar(Class)",
true,
{
label: "executeScalar(...)",
url: {
@@ -78,10 +81,11 @@ describe("decodeBqrsToExternalApiUsages", () => {
endColumn: 85,
},
},
"org.sql2o.Query#executeScalar(Class)",
"true",
"supported",
],
[
"org.sql2o.Query#executeScalar(Class)",
true,
{
label: "executeScalar(...)",
url: {
@@ -92,10 +96,11 @@ describe("decodeBqrsToExternalApiUsages", () => {
endColumn: 68,
},
},
"org.sql2o.Query#executeScalar(Class)",
"true",
"supported",
],
[
"org.sql2o.Sql2o#open()",
true,
{
label: "open(...)",
url: {
@@ -106,10 +111,11 @@ describe("decodeBqrsToExternalApiUsages", () => {
endColumn: 35,
},
},
"org.sql2o.Sql2o#open()",
"true",
"supported",
],
[
"org.sql2o.Sql2o#open()",
true,
{
label: "open(...)",
url: {
@@ -120,10 +126,11 @@ describe("decodeBqrsToExternalApiUsages", () => {
endColumn: 35,
},
},
"org.sql2o.Sql2o#open()",
"true",
"supported",
],
[
"org.sql2o.Sql2o#Sql2o(String,String,String)",
true,
{
label: "new Sql2o(...)",
url: {
@@ -134,10 +141,11 @@ describe("decodeBqrsToExternalApiUsages", () => {
endColumn: 88,
},
},
"org.sql2o.Sql2o#Sql2o(String,String,String)",
"true",
"supported",
],
[
"org.sql2o.Sql2o#Sql2o(String)",
true,
{
label: "new Sql2o(...)",
url: {
@@ -148,6 +156,9 @@ describe("decodeBqrsToExternalApiUsages", () => {
endColumn: 36,
},
},
"org.sql2o.Sql2o#Sql2o(String)",
"true",
"supported",
],
],
};

View File

@@ -241,6 +241,113 @@ describe("db config store", () => {
configStore.dispose();
});
it("should add unique remote repositories to the correct list", async () => {
// Initial set up
const dbConfig = createDbConfig({
remoteLists: [
{
name: "list1",
repositories: ["owner/repo1"],
},
],
});
const configStore = await initializeConfig(dbConfig, configPath, app);
expect(
configStore.getConfig().value.databases.variantAnalysis
.repositoryLists[0],
).toEqual({
name: "list1",
repositories: ["owner/repo1"],
});
// Add
const response = await configStore.addRemoteReposToList(
["owner/repo1", "owner/repo2"],
"list1",
);
// Read the config file
const updatedDbConfig = (await readJSON(configPath)) as DbConfig;
// Check that the config file has been updated
const updatedRemoteDbs = updatedDbConfig.databases.variantAnalysis;
expect(updatedRemoteDbs.repositories).toHaveLength(0);
expect(updatedRemoteDbs.repositoryLists).toHaveLength(1);
expect(updatedRemoteDbs.repositoryLists[0]).toEqual({
name: "list1",
repositories: ["owner/repo1", "owner/repo2"],
});
expect(response).toEqual([]);
configStore.dispose();
});
it("should add no more than 1000 repositories to a remote list when adding multiple repos", async () => {
// Initial set up
const dbConfig = createDbConfig({
remoteLists: [
{
name: "list1",
repositories: [],
},
],
});
const configStore = await initializeConfig(dbConfig, configPath, app);
// Add
const response = await configStore.addRemoteReposToList(
[...Array(1001).keys()].map((i) => `owner/db${i}`),
"list1",
);
// Read the config file
const updatedDbConfig = (await readJSON(configPath)) as DbConfig;
// Check that the config file has been updated
const updatedRemoteDbs = updatedDbConfig.databases.variantAnalysis;
expect(updatedRemoteDbs.repositories).toHaveLength(0);
expect(updatedRemoteDbs.repositoryLists).toHaveLength(1);
expect(updatedRemoteDbs.repositoryLists[0].repositories).toHaveLength(
1000,
);
expect(response).toEqual(["owner/db1000"]);
configStore.dispose();
});
it("should add no more than 1000 repositories to a remote list when adding one repo", async () => {
// Initial set up
const dbConfig = createDbConfig({
remoteLists: [
{
name: "list1",
repositories: [...Array(1000).keys()].map((i) => `owner/db${i}`),
},
],
});
const configStore = await initializeConfig(dbConfig, configPath, app);
// Add
const reponse = await configStore.addRemoteRepo("owner/db1000", "list1");
// Read the config file
const updatedDbConfig = (await readJSON(configPath)) as DbConfig;
// Check that the config file has been updated
const updatedRemoteDbs = updatedDbConfig.databases.variantAnalysis;
expect(updatedRemoteDbs.repositories).toHaveLength(0);
expect(updatedRemoteDbs.repositoryLists).toHaveLength(1);
expect(updatedRemoteDbs.repositoryLists[0].repositories).toHaveLength(
1000,
);
expect(reponse).toEqual(["owner/db1000"]);
configStore.dispose();
});
it("should add a remote owner", async () => {
// Initial set up
const dbConfig = createDbConfig();

View File

@@ -88,6 +88,73 @@ describe("db manager", () => {
).toEqual("owner2/repo2");
});
it("should add new remote repos to a user defined list", async () => {
const dbConfig: DbConfig = createDbConfig({
remoteLists: [
{
name: "my-list-1",
repositories: ["owner1/repo1"],
},
],
});
await saveDbConfig(dbConfig);
await dbManager.addNewRemoteReposToList(["owner2/repo2"], "my-list-1");
const dbConfigFileContents = await readDbConfigDirectly();
expect(
dbConfigFileContents.databases.variantAnalysis.repositoryLists.length,
).toBe(1);
expect(
dbConfigFileContents.databases.variantAnalysis.repositoryLists[0],
).toEqual({
name: "my-list-1",
repositories: ["owner1/repo1", "owner2/repo2"],
});
});
it("should return truncated repos when adding multiple repos to a user defined list", async () => {
const dbConfig: DbConfig = createDbConfig({
remoteLists: [
{
name: "my-list-1",
repositories: [...Array(1000).keys()].map((i) => `owner/db${i}`),
},
],
});
await saveDbConfig(dbConfig);
const response = await dbManager.addNewRemoteReposToList(
["owner2/repo2"],
"my-list-1",
);
expect(response).toEqual(["owner2/repo2"]);
});
it("should return truncated repos when adding one repo to a user defined list", async () => {
const dbConfig: DbConfig = createDbConfig({
remoteLists: [
{
name: "my-list-1",
repositories: [...Array(1000).keys()].map((i) => `owner/db${i}`),
},
],
});
await saveDbConfig(dbConfig);
const response = await dbManager.addNewRemoteRepo(
"owner2/repo2",
"my-list-1",
);
expect(response).toEqual(["owner2/repo2"]);
});
it("should add a new remote repo to a user defined list", async () => {
const dbConfig: DbConfig = createDbConfig({
remoteLists: [

View File

@@ -62,12 +62,17 @@ describe("getDbItemActions", () => {
expect(actions.length).toEqual(0);
});
it("should set canBeSelected, canBeRemoved and canBeRenamed for remote user defined db list", () => {
it("should set canBeSelected, canBeRemoved, canBeRenamed and canImportCodeSearch for remote user defined db list", () => {
const dbItem = createRemoteUserDefinedListDbItem();
const actions = getDbItemActions(dbItem);
expect(actions).toEqual(["canBeSelected", "canBeRemoved", "canBeRenamed"]);
expect(actions).toEqual([
"canBeSelected",
"canBeRemoved",
"canBeRenamed",
"canImportCodeSearch",
]);
});
it("should not set canBeSelected for remote user defined db list that is already selected", () => {

View File

@@ -15,6 +15,7 @@ import {
import { KeyType, resolveQueries } from "../../../src/language-support";
import { faker } from "@faker-js/faker";
import { getActivatedExtension } from "../global.helper";
import { BaseLogger } from "../../../src/common";
/**
* Perform proper integration tests by running the CLI
@@ -23,10 +24,14 @@ describe("Use cli", () => {
let cli: CodeQLCliServer;
let supportedLanguages: string[];
let logSpy: jest.SpiedFunction<BaseLogger["log"]>;
beforeEach(async () => {
const extension = await getActivatedExtension();
cli = extension.cliServer;
supportedLanguages = await cli.getSupportedLanguages();
logSpy = jest.spyOn(cli.logger, "log");
});
if (process.env.CLI_VERSION && process.env.CLI_VERSION !== "nightly") {
@@ -42,6 +47,23 @@ describe("Use cli", () => {
expect(result).toEqual(["-J-Xmx4096M", "--off-heap-ram=4096"]);
});
describe("silent logging", () => {
it("should log command output", async () => {
const queryDir = getOnDiskWorkspaceFolders()[0];
await cli.resolveQueries(queryDir);
expect(logSpy).toHaveBeenCalled();
});
it("shouldn't log command output if the `silent` flag is set", async () => {
const queryDir = getOnDiskWorkspaceFolders()[0];
const silent = true;
await cli.resolveQueries(queryDir, silent);
expect(logSpy).not.toHaveBeenCalled();
});
});
itWithCodeQL()("should resolve query packs", async () => {
const qlpacks = await cli.resolveQlpacks(getOnDiskWorkspaceFolders());
// Depending on the version of the CLI, the qlpacks may have different names

View File

@@ -349,7 +349,12 @@ describe("db panel rendering nodes", () => {
expect(item.tooltip).toBeUndefined();
expect(item.iconPath).toBeUndefined();
expect(item.collapsibleState).toBe(TreeItemCollapsibleState.Collapsed);
checkDbItemActions(item, ["canBeSelected", "canBeRenamed", "canBeRemoved"]);
checkDbItemActions(item, [
"canBeSelected",
"canBeRenamed",
"canBeRemoved",
"canImportCodeSearch",
]);
expect(item.children).toBeTruthy();
expect(item.children.length).toBe(repos.length);

View File

@@ -6,14 +6,10 @@ import {
workspace,
} from "vscode";
import { CodeQLCliServer } from "../../../../src/codeql-cli/cli";
import {
QueryDiscovery,
QueryDiscoveryResults,
} from "../../../../src/queries-panel/query-discovery";
import { QueryDiscovery } from "../../../../src/queries-panel/query-discovery";
import { createMockApp } from "../../../__mocks__/appMock";
import { mockedObject } from "../../utils/mocking.helpers";
import { basename, join, sep } from "path";
import { sleep } from "../../../../src/pure/time";
describe("QueryDiscovery", () => {
beforeEach(() => {
@@ -28,11 +24,10 @@ describe("QueryDiscovery", () => {
});
const discovery = new QueryDiscovery(createMockApp({}), cli);
const results: QueryDiscoveryResults = await (
discovery as any
).discover();
await discovery.refresh();
const queries = discovery.queries;
expect(results.queries).toEqual([]);
expect(queries).toEqual([]);
expect(resolveQueries).toHaveBeenCalledTimes(1);
});
@@ -49,22 +44,18 @@ describe("QueryDiscovery", () => {
});
const discovery = new QueryDiscovery(createMockApp({}), cli);
const results: QueryDiscoveryResults = await (
discovery as any
).discover();
await discovery.refresh();
const queries = discovery.queries;
expect(queries).toBeDefined();
expect(results.queries[0].children.length).toEqual(3);
expect(results.queries[0].children[0].name).toEqual("dir1");
expect(results.queries[0].children[0].children.length).toEqual(1);
expect(results.queries[0].children[0].children[0].name).toEqual(
"query1.ql",
);
expect(results.queries[0].children[1].name).toEqual("dir2");
expect(results.queries[0].children[1].children.length).toEqual(1);
expect(results.queries[0].children[1].children[0].name).toEqual(
"query2.ql",
);
expect(results.queries[0].children[2].name).toEqual("query3.ql");
expect(queries![0].children.length).toEqual(3);
expect(queries![0].children[0].name).toEqual("dir1");
expect(queries![0].children[0].children.length).toEqual(1);
expect(queries![0].children[0].children[0].name).toEqual("query1.ql");
expect(queries![0].children[1].name).toEqual("dir2");
expect(queries![0].children[1].children.length).toEqual(1);
expect(queries![0].children[1].children[0].name).toEqual("query2.ql");
expect(queries![0].children[2].name).toEqual("query3.ql");
});
it("should collapse directories containing only a single element", async () => {
@@ -79,25 +70,21 @@ describe("QueryDiscovery", () => {
});
const discovery = new QueryDiscovery(createMockApp({}), cli);
const results: QueryDiscoveryResults = await (
discovery as any
).discover();
await discovery.refresh();
const queries = discovery.queries;
expect(queries).toBeDefined();
expect(results.queries[0].children.length).toEqual(1);
expect(results.queries[0].children[0].name).toEqual("dir1");
expect(results.queries[0].children[0].children.length).toEqual(2);
expect(results.queries[0].children[0].children[0].name).toEqual(
expect(queries![0].children.length).toEqual(1);
expect(queries![0].children[0].name).toEqual("dir1");
expect(queries![0].children[0].children.length).toEqual(2);
expect(queries![0].children[0].children[0].name).toEqual(
"dir2 / dir3 / dir3",
);
expect(
results.queries[0].children[0].children[0].children.length,
).toEqual(1);
expect(
results.queries[0].children[0].children[0].children[0].name,
).toEqual("query2.ql");
expect(results.queries[0].children[0].children[1].name).toEqual(
"query1.ql",
expect(queries![0].children[0].children[0].children.length).toEqual(1);
expect(queries![0].children[0].children[0].children[0].name).toEqual(
"query2.ql",
);
expect(queries![0].children[0].children[1].name).toEqual("query1.ql");
});
it("calls resolveQueries once for each workspace folder", async () => {
@@ -128,14 +115,14 @@ describe("QueryDiscovery", () => {
});
const discovery = new QueryDiscovery(createMockApp({}), cli);
const results: QueryDiscoveryResults = await (
discovery as any
).discover();
await discovery.refresh();
const queries = discovery.queries;
expect(queries).toBeDefined();
expect(results.queries.length).toEqual(3);
expect(results.queries[0].children[0].name).toEqual("query1.ql");
expect(results.queries[1].children[0].name).toEqual("query2.ql");
expect(results.queries[2].children[0].name).toEqual("query3.ql");
expect(queries!.length).toEqual(3);
expect(queries![0].children[0].name).toEqual("query1.ql");
expect(queries![1].children[0].name).toEqual("query2.ql");
expect(queries![2].children[0].name).toEqual("query3.ql");
expect(resolveQueries).toHaveBeenCalledTimes(3);
});
@@ -176,16 +163,14 @@ describe("QueryDiscovery", () => {
const onDidChangeQueriesSpy = jest.fn();
discovery.onDidChangeQueries(onDidChangeQueriesSpy);
const results = await (discovery as any).discover();
(discovery as any).update(results);
await discovery.refresh();
expect(createFileSystemWatcherSpy).toHaveBeenCalledTimes(2);
expect(onDidChangeQueriesSpy).toHaveBeenCalledTimes(1);
onWatcherDidChangeEvent.fire(workspace.workspaceFolders![0].uri);
// Wait for refresh to finish
await sleep(100);
await discovery.waitForCurrentRefresh();
expect(onDidChangeQueriesSpy).toHaveBeenCalledTimes(2);
});
@@ -209,15 +194,13 @@ describe("QueryDiscovery", () => {
const onDidChangeQueriesSpy = jest.fn();
discovery.onDidChangeQueries(onDidChangeQueriesSpy);
const results = await (discovery as any).discover();
(discovery as any).update(results);
await discovery.refresh();
expect(onDidChangeQueriesSpy).toHaveBeenCalledTimes(1);
onDidChangeWorkspaceFoldersEvent.fire({ added: [], removed: [] });
// Wait for refresh to finish
await sleep(100);
await discovery.waitForCurrentRefresh();
expect(onDidChangeQueriesSpy).toHaveBeenCalledTimes(2);
});

View File

@@ -52,12 +52,14 @@ describe("qltest-discovery", () => {
});
it("should run discovery", async () => {
const result = await (qlTestDiscover as any).discover();
expect(result.watchPath).toEqualPath(baseDir);
expect(result.testDirectory.path).toEqualPath(baseDir);
expect(result.testDirectory.name).toBe("My tests");
await qlTestDiscover.refresh();
const testDirectory = qlTestDiscover.testDirectory;
expect(testDirectory).toBeDefined();
let children = result.testDirectory.children;
expect(testDirectory!.path).toEqualPath(baseDir);
expect(testDirectory!.name).toBe("My tests");
let children = testDirectory!.children;
expect(children.length).toBe(1);
expect(children[0].path).toEqualPath(cDir);
@@ -83,12 +85,14 @@ describe("qltest-discovery", () => {
it("should avoid discovery if a folder does not exist", async () => {
await fs.remove(baseDir);
const result = await (qlTestDiscover as any).discover();
expect(result.watchPath).toEqualPath(baseDir);
expect(result.testDirectory.path).toEqualPath(baseDir);
expect(result.testDirectory.name).toBe("My tests");
await qlTestDiscover.refresh();
const testDirectory = qlTestDiscover.testDirectory;
expect(testDirectory).toBeDefined();
expect(result.testDirectory.children.length).toBe(0);
expect(testDirectory!.path).toEqualPath(baseDir);
expect(testDirectory!.name).toBe("My tests");
expect(testDirectory!.children.length).toBe(0);
});
});
});

View File

@@ -286,7 +286,7 @@ describe("Launcher path", () => {
const manager = new DistributionManager(
{ customCodeQlPath: pathToCmd } as any,
{} as any,
undefined as any,
{} as any,
);
const result = await manager.getCodeQlPathWithoutVersionCheck();
@@ -304,7 +304,7 @@ describe("Launcher path", () => {
const manager = new DistributionManager(
{ customCodeQlPath: pathToCmd } as any,
{} as any,
undefined as any,
{} as any,
);
const result = await manager.getCodeQlPathWithoutVersionCheck();
@@ -319,7 +319,7 @@ describe("Launcher path", () => {
const manager = new DistributionManager(
{ customCodeQlPath: pathToCmd } as any,
{} as any,
undefined as any,
{} as any,
);
const result = await manager.getCodeQlPathWithoutVersionCheck();

View File

@@ -10,7 +10,7 @@ import { QueryResultType } from "../../../../src/pure/new-messages";
import { readdir, readFile } from "fs-extra";
import { load } from "js-yaml";
import { dirname, join } from "path";
import { fetchExternalApiQueries } from "../../../../src/data-extensions-editor/queries/index";
import { fetchExternalApiQueries } from "../../../../src/data-extensions-editor/queries";
import * as helpers from "../../../../src/helpers";
import { RedactableError } from "../../../../src/pure/errors";
@@ -162,18 +162,20 @@ describe("readQueryResults", () => {
name: "#select",
rows: 10,
columns: [
{ name: "apiName", kind: "s" },
{ name: "supported", kind: "b" },
{ name: "usage", kind: "e" },
{ name: "apiName", kind: "s" },
{ kind: "s" },
{ kind: "s" },
],
},
{
name: "#select2",
rows: 10,
columns: [
{ name: "apiName", kind: "s" },
{ name: "supported", kind: "b" },
{ name: "usage", kind: "e" },
{ name: "apiName", kind: "s" },
{ kind: "s" },
{ kind: "s" },
],
},
],
@@ -192,9 +194,10 @@ describe("readQueryResults", () => {
name: "#select",
rows: 10,
columns: [
{ name: "apiName", kind: "s" },
{ name: "supported", kind: "b" },
{ name: "usage", kind: "e" },
{ name: "apiName", kind: "s" },
{ kind: "s" },
{ kind: "s" },
],
},
],
@@ -202,9 +205,10 @@ describe("readQueryResults", () => {
});
const decodedResultSet = {
columns: [
{ name: "apiName", kind: "String" },
{ name: "supported", kind: "Boolean" },
{ name: "usage", kind: "Entity" },
{ name: "usage", kind: "e" },
{ name: "apiName", kind: "s" },
{ kind: "s" },
{ kind: "s" },
],
tuples: [
[

View File

@@ -1,17 +1,4 @@
import {
EnvironmentVariableCollection,
EnvironmentVariableMutator,
Event,
ExtensionContext,
ExtensionMode,
Memento,
SecretStorage,
SecretStorageChangeEvent,
Uri,
window,
workspace,
WorkspaceFolder,
} from "vscode";
import { Uri, window, workspace, WorkspaceFolder } from "vscode";
import { dump } from "js-yaml";
import * as tmp from "tmp";
import { join } from "path";
@@ -28,7 +15,6 @@ import { DirResult } from "tmp";
import {
getFirstWorkspaceFolder,
getInitialQueryContents,
InvocationRateLimiter,
isFolderAlreadyInWorkspace,
isLikelyDatabaseRoot,
isLikelyDbLanguageFolder,
@@ -45,118 +31,6 @@ import { Setting } from "../../../src/config";
import { createMockCommandManager } from "../../__mocks__/commandsMock";
describe("helpers", () => {
describe("Invocation rate limiter", () => {
// 1 January 2020
let currentUnixTime = 1577836800;
function createDate(dateString?: string): Date {
if (dateString) {
return new Date(dateString);
}
const numMillisecondsPerSecond = 1000;
return new Date(currentUnixTime * numMillisecondsPerSecond);
}
function createInvocationRateLimiter<T>(
funcIdentifier: string,
func: () => Promise<T>,
): InvocationRateLimiter<T> {
return new InvocationRateLimiter(
new MockExtensionContext(),
funcIdentifier,
func,
(s) => createDate(s),
);
}
it("initially invokes function", async () => {
let numTimesFuncCalled = 0;
const invocationRateLimiter = createInvocationRateLimiter(
"funcid",
async () => {
numTimesFuncCalled++;
},
);
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(100);
expect(numTimesFuncCalled).toBe(1);
});
it("doesn't invoke function again if no time has passed", async () => {
let numTimesFuncCalled = 0;
const invocationRateLimiter = createInvocationRateLimiter(
"funcid",
async () => {
numTimesFuncCalled++;
},
);
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(100);
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(100);
expect(numTimesFuncCalled).toBe(1);
});
it("doesn't invoke function again if requested time since last invocation hasn't passed", async () => {
let numTimesFuncCalled = 0;
const invocationRateLimiter = createInvocationRateLimiter(
"funcid",
async () => {
numTimesFuncCalled++;
},
);
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(100);
currentUnixTime += 1;
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(2);
expect(numTimesFuncCalled).toBe(1);
});
it("invokes function again immediately if requested time since last invocation is 0 seconds", async () => {
let numTimesFuncCalled = 0;
const invocationRateLimiter = createInvocationRateLimiter(
"funcid",
async () => {
numTimesFuncCalled++;
},
);
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(0);
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(0);
expect(numTimesFuncCalled).toBe(2);
});
it("invokes function again after requested time since last invocation has elapsed", async () => {
let numTimesFuncCalled = 0;
const invocationRateLimiter = createInvocationRateLimiter(
"funcid",
async () => {
numTimesFuncCalled++;
},
);
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(1);
currentUnixTime += 1;
await invocationRateLimiter.invokeFunctionIfIntervalElapsed(1);
expect(numTimesFuncCalled).toBe(2);
});
it("invokes functions with different rate limiters", async () => {
let numTimesFuncACalled = 0;
const invocationRateLimiterA = createInvocationRateLimiter(
"funcid",
async () => {
numTimesFuncACalled++;
},
);
let numTimesFuncBCalled = 0;
const invocationRateLimiterB = createInvocationRateLimiter(
"funcid",
async () => {
numTimesFuncBCalled++;
},
);
await invocationRateLimiterA.invokeFunctionIfIntervalElapsed(100);
await invocationRateLimiterB.invokeFunctionIfIntervalElapsed(100);
expect(numTimesFuncACalled).toBe(1);
expect(numTimesFuncBCalled).toBe(1);
});
});
describe("codeql-database.yml tests", () => {
let dir: tmp.DirResult;
let language: QueryLanguage;
@@ -250,116 +124,6 @@ describe("helpers", () => {
});
});
class MockExtensionContext implements ExtensionContext {
extensionMode: ExtensionMode = 3;
subscriptions: Array<{ dispose(): unknown }> = [];
workspaceState: Memento = new MockMemento();
globalState = new MockGlobalStorage();
extensionPath = "";
asAbsolutePath(_relativePath: string): string {
throw new Error("Method not implemented.");
}
storagePath = "";
globalStoragePath = "";
logPath = "";
extensionUri = Uri.parse("");
environmentVariableCollection = new MockEnvironmentVariableCollection();
secrets = new MockSecretStorage();
storageUri = Uri.parse("");
globalStorageUri = Uri.parse("");
logUri = Uri.parse("");
extension: any;
}
class MockEnvironmentVariableCollection
implements EnvironmentVariableCollection
{
[Symbol.iterator](): Iterator<
[variable: string, mutator: EnvironmentVariableMutator],
any,
undefined
> {
throw new Error("Method not implemented.");
}
persistent = false;
replace(_variable: string, _value: string): void {
throw new Error("Method not implemented.");
}
append(_variable: string, _value: string): void {
throw new Error("Method not implemented.");
}
prepend(_variable: string, _value: string): void {
throw new Error("Method not implemented.");
}
get(_variable: string): EnvironmentVariableMutator | undefined {
throw new Error("Method not implemented.");
}
forEach(
_callback: (
variable: string,
mutator: EnvironmentVariableMutator,
collection: EnvironmentVariableCollection,
) => any,
_thisArg?: any,
): void {
throw new Error("Method not implemented.");
}
delete(_variable: string): void {
throw new Error("Method not implemented.");
}
clear(): void {
throw new Error("Method not implemented.");
}
}
class MockMemento implements Memento {
keys(): readonly string[] {
throw new Error("Method not implemented.");
}
map = new Map<any, any>();
/**
* Return a value.
*
* @param key A string.
* @param defaultValue A value that should be returned when there is no
* value (`undefined`) with the given key.
* @return The stored value or the defaultValue.
*/
get<T>(key: string, defaultValue?: T): T {
return this.map.has(key) ? this.map.get(key) : defaultValue;
}
/**
* Store a value. The value must be JSON-stringifyable.
*
* @param key A string.
* @param value A value. MUST not contain cyclic references.
*/
async update(key: string, value: any): Promise<void> {
this.map.set(key, value);
}
}
class MockGlobalStorage extends MockMemento {
public setKeysForSync(_keys: string[]): void {
return;
}
}
class MockSecretStorage implements SecretStorage {
get(_key: string): Thenable<string | undefined> {
throw new Error("Method not implemented.");
}
store(_key: string, _value: string): Thenable<void> {
throw new Error("Method not implemented.");
}
delete(_key: string): Thenable<void> {
throw new Error("Method not implemented.");
}
onDidChange!: Event<SecretStorageChangeEvent>;
}
it("should report stream progress", () => {
const progressSpy = jest.fn();
const mockReadable = {

View File

@@ -225,6 +225,7 @@ describe("query-results", () => {
resultsPath,
interpretedResultsPath,
sourceInfo,
undefined,
);
},
2 * 60 * 1000, // up to 2 minutes per test
@@ -249,6 +250,7 @@ describe("query-results", () => {
resultsPath,
interpretedResultsPath,
sourceInfo,
undefined,
);
},
2 * 60 * 1000, // up to 2 minutes per test