Merge branch 'main' into selected-line-column
This commit is contained in:
2
.github/codeql/queries/assert-pure.ql
vendored
2
.github/codeql/queries/assert-pure.ql
vendored
@@ -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/.*")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
89
extensions/ql-vscode/src/common/invocation-rate-limiter.ts
Normal file
89
extensions/ql-vscode/src/common/invocation-rate-limiter.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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}]`,
|
||||
};
|
||||
|
||||
|
||||
@@ -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("#");
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -115,7 +115,7 @@ export type BqrsKind =
|
||||
| "Entity";
|
||||
|
||||
interface BqrsColumn {
|
||||
name: string;
|
||||
name?: string;
|
||||
kind: BqrsKind;
|
||||
}
|
||||
export interface DecodedBqrsChunk {
|
||||
|
||||
70
extensions/ql-vscode/src/pure/cached-operation.ts
Normal file
70
extensions/ql-vscode/src/pure/cached-operation.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" };
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[
|
||||
"v2.13.1",
|
||||
"v2.13.3",
|
||||
"v2.12.7",
|
||||
"v2.11.6",
|
||||
"v2.7.6",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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: [
|
||||
[
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user