Merge branch 'main' into aeisenberg/cli-version-telemetry

This commit is contained in:
Andrew Eisenberg
2023-04-17 09:18:38 -07:00
committed by GitHub
53 changed files with 4272 additions and 1050 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1461,6 +1461,7 @@
"fs-extra": "^11.1.1",
"immutable": "^4.0.0",
"js-yaml": "^4.1.0",
"minimatch": "^9.0.0",
"minimist": "~1.2.6",
"msw": "^1.2.0",
"nanoid": "^3.2.0",
@@ -1531,7 +1532,7 @@
"@types/through2": "^2.0.36",
"@types/tmp": "^0.1.0",
"@types/unzipper": "~0.10.1",
"@types/vscode": "^1.59.0",
"@types/vscode": "^1.67.0",
"@types/webpack": "^5.28.0",
"@types/webpack-env": "^1.18.0",
"@types/xml2js": "~0.4.4",
@@ -1555,7 +1556,7 @@
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-storybook": "^0.6.4",
"file-loader": "^6.2.0",
"glob": "^9.3.2",
"glob": "^10.0.0",
"gulp": "^4.0.2",
"gulp-esbuild": "^0.10.5",
"gulp-replace": "^1.1.3",

View File

@@ -11,6 +11,8 @@ import { showAndLogErrorMessage } from "../helpers";
import { withProgress } from "../progress";
import { pickExtensionPackModelFile } from "./extension-pack-picker";
const SUPPORTED_LANGUAGES: string[] = ["java", "csharp"];
export class DataExtensionsEditorModule {
private readonly queryStorageDir: string;
@@ -51,15 +53,22 @@ export class DataExtensionsEditorModule {
public getCommands(): DataExtensionsEditorCommands {
return {
"codeQL.openDataExtensionsEditor": async () =>
withProgress(
async (progress) => {
const db = this.databaseManager.currentDatabaseItem;
if (!db) {
void showAndLogErrorMessage("No database selected");
return;
}
"codeQL.openDataExtensionsEditor": async () => {
const db = this.databaseManager.currentDatabaseItem;
if (!db) {
void showAndLogErrorMessage("No database selected");
return;
}
if (!SUPPORTED_LANGUAGES.includes(db.language)) {
void showAndLogErrorMessage(
`The data extensions editor is not supported for ${db.language} databases.`,
);
return;
}
return withProgress(
async (progress, token) => {
if (!(await this.cliServer.cliConstraints.supportsQlpacksKind())) {
void showAndLogErrorMessage(
`This feature requires CodeQL CLI version ${CliVersionConstraint.CLI_VERSION_WITH_QLPACKS_KIND.format()} or later.`,
@@ -69,7 +78,9 @@ export class DataExtensionsEditorModule {
const modelFile = await pickExtensionPackModelFile(
this.cliServer,
db,
progress,
token,
);
if (!modelFile) {
return;
@@ -90,7 +101,8 @@ export class DataExtensionsEditorModule {
{
title: "Opening Data Extensions Editor",
},
),
);
},
};
}

View File

@@ -18,7 +18,7 @@ import {
showAndLogExceptionWithTelemetry,
} from "../helpers";
import { extLogger } from "../common";
import { readFile, writeFile } from "fs-extra";
import { outputFile, readFile } from "fs-extra";
import { load as loadYaml } from "js-yaml";
import { DatabaseItem, DatabaseManager } from "../local-databases";
import { CodeQLCliServer } from "../cli";
@@ -148,9 +148,13 @@ export class DataExtensionsEditorView extends AbstractWebview<
externalApiUsages: ExternalApiUsage[],
modeledMethods: Record<string, ModeledMethod>,
): Promise<void> {
const yaml = createDataExtensionYaml(externalApiUsages, modeledMethods);
const yaml = createDataExtensionYaml(
this.databaseItem.language,
externalApiUsages,
modeledMethods,
);
await writeFile(this.modelFilename, yaml);
await outputFile(this.modelFilename, yaml);
void extLogger.log(`Saved data extension YAML to ${this.modelFilename}`);
}
@@ -194,7 +198,6 @@ export class DataExtensionsEditorView extends AbstractWebview<
queryRunner: this.queryRunner,
databaseItem: this.databaseItem,
queryStorageDir: this.queryStorageDir,
logger: extLogger,
progress: (progressUpdate: ProgressUpdate) => {
void this.showProgress(progressUpdate, 1500);
},

View File

@@ -1,21 +1,49 @@
import { relative, sep } from "path";
import { window } from "vscode";
import { join, relative, resolve, sep } from "path";
import { outputFile, pathExists, readFile } from "fs-extra";
import { dump as dumpYaml, load as loadYaml } from "js-yaml";
import { minimatch } from "minimatch";
import { CancellationToken, window } from "vscode";
import { CodeQLCliServer } from "../cli";
import { getOnDiskWorkspaceFolders, showAndLogErrorMessage } from "../helpers";
import {
getOnDiskWorkspaceFolders,
getOnDiskWorkspaceFoldersObjects,
showAndLogErrorMessage,
} from "../helpers";
import { ProgressCallback } from "../progress";
import { DatabaseItem } from "../local-databases";
import { getQlPackPath, QLPACK_FILENAMES } from "../pure/ql";
const maxStep = 3;
const packNamePartRegex = /[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/;
const packNameRegex = new RegExp(
`^(?:(?<scope>${packNamePartRegex.source})/)?(?<name>${packNamePartRegex.source})$`,
);
const packNameLength = 128;
export async function pickExtensionPackModelFile(
cliServer: Pick<CodeQLCliServer, "resolveQlpacks" | "resolveExtensions">,
databaseItem: Pick<DatabaseItem, "name" | "language">,
progress: ProgressCallback,
token: CancellationToken,
): Promise<string | undefined> {
const extensionPackPath = await pickExtensionPack(cliServer, progress);
const extensionPackPath = await pickExtensionPack(
cliServer,
databaseItem,
progress,
token,
);
if (!extensionPackPath) {
return;
}
const modelFile = await pickModelFile(cliServer, progress, extensionPackPath);
const modelFile = await pickModelFile(
cliServer,
databaseItem,
extensionPackPath,
progress,
token,
);
if (!modelFile) {
return;
}
@@ -25,7 +53,9 @@ export async function pickExtensionPackModelFile(
async function pickExtensionPack(
cliServer: Pick<CodeQLCliServer, "resolveQlpacks">,
databaseItem: Pick<DatabaseItem, "name" | "language">,
progress: ProgressCallback,
token: CancellationToken,
): Promise<string | undefined> {
progress({
message: "Resolving extension packs...",
@@ -36,10 +66,20 @@ async function pickExtensionPack(
// Get all existing extension packs in the workspace
const additionalPacks = getOnDiskWorkspaceFolders();
const extensionPacks = await cliServer.resolveQlpacks(additionalPacks, true);
const options = Object.keys(extensionPacks).map((pack) => ({
label: pack,
extensionPack: pack,
}));
if (Object.keys(extensionPacks).length === 0) {
return pickNewExtensionPack(databaseItem, token);
}
const options: Array<{ label: string; extensionPack: string | null }> =
Object.keys(extensionPacks).map((pack) => ({
label: pack,
extensionPack: pack,
}));
options.push({
label: "Create new extension pack",
extensionPack: null,
});
progress({
message: "Choosing extension pack...",
@@ -47,13 +87,21 @@ async function pickExtensionPack(
maxStep,
});
const extensionPackOption = await window.showQuickPick(options, {
title: "Select extension pack to use",
});
const extensionPackOption = await window.showQuickPick(
options,
{
title: "Select extension pack to use",
},
token,
);
if (!extensionPackOption) {
return undefined;
}
if (!extensionPackOption.extensionPack) {
return pickNewExtensionPack(databaseItem, token);
}
const extensionPackPaths = extensionPacks[extensionPackOption.extensionPack];
if (extensionPackPaths.length !== 1) {
void showAndLogErrorMessage(
@@ -74,8 +122,10 @@ async function pickExtensionPack(
async function pickModelFile(
cliServer: Pick<CodeQLCliServer, "resolveExtensions">,
progress: ProgressCallback,
databaseItem: Pick<DatabaseItem, "name">,
extensionPackPath: string,
progress: ProgressCallback,
token: CancellationToken,
): Promise<string | undefined> {
// Find the existing model files in the extension pack
const additionalPacks = getOnDiskWorkspaceFolders();
@@ -92,13 +142,21 @@ async function pickModelFile(
}
}
const fileOptions: Array<{ label: string; file: string }> = [];
if (modelFiles.size === 0) {
return pickNewModelFile(databaseItem, extensionPackPath, token);
}
const fileOptions: Array<{ label: string; file: string | null }> = [];
for (const file of modelFiles) {
fileOptions.push({
label: relative(extensionPackPath, file).replaceAll(sep, "/"),
file,
});
}
fileOptions.push({
label: "Create new model file",
file: null,
});
progress({
message: "Choosing model file...",
@@ -106,13 +164,186 @@ async function pickModelFile(
maxStep,
});
const fileOption = await window.showQuickPick(fileOptions, {
title: "Select model file to use",
});
const fileOption = await window.showQuickPick(
fileOptions,
{
title: "Select model file to use",
},
token,
);
if (!fileOption) {
return undefined;
}
if (fileOption.file) {
return fileOption.file;
}
return pickNewModelFile(databaseItem, extensionPackPath, token);
}
async function pickNewExtensionPack(
databaseItem: Pick<DatabaseItem, "name" | "language">,
token: CancellationToken,
): Promise<string | undefined> {
const workspaceFolders = getOnDiskWorkspaceFoldersObjects();
const workspaceFolderOptions = workspaceFolders.map((folder) => ({
label: folder.name,
detail: folder.uri.fsPath,
path: folder.uri.fsPath,
}));
// We're not using window.showWorkspaceFolderPick because that also includes the database source folders while
// we only want to include on-disk workspace folders.
const workspaceFolder = await window.showQuickPick(workspaceFolderOptions, {
title: "Select workspace folder to create extension pack in",
});
if (!workspaceFolder) {
return undefined;
}
const packName = await window.showInputBox(
{
title: "Create new extension pack",
prompt: "Enter name of extension pack",
placeHolder: `e.g. ${databaseItem.name}-extensions`,
validateInput: async (value: string): Promise<string | undefined> => {
if (!value) {
return "Pack name must not be empty";
}
if (value.length > packNameLength) {
return `Pack name must be no longer than ${packNameLength} characters`;
}
const matches = packNameRegex.exec(value);
if (!matches?.groups) {
return "Invalid package name: a pack name must contain only lowercase ASCII letters, ASCII digits, and hyphens";
}
const packPath = join(workspaceFolder.path, matches.groups.name);
if (await pathExists(packPath)) {
return `A pack already exists at ${packPath}`;
}
return undefined;
},
},
token,
);
if (!packName) {
return undefined;
}
const matches = packNameRegex.exec(packName);
if (!matches?.groups) {
return;
}
return fileOption.file;
const name = matches.groups.name;
const packPath = join(workspaceFolder.path, name);
if (await pathExists(packPath)) {
return undefined;
}
const packYamlPath = join(packPath, "codeql-pack.yml");
await outputFile(
packYamlPath,
dumpYaml({
name,
version: "0.0.0",
library: true,
extensionTargets: {
[`codeql/${databaseItem.language}-all`]: "*",
},
dataExtensions: ["models/**/*.yml"],
}),
);
return packPath;
}
async function pickNewModelFile(
databaseItem: Pick<DatabaseItem, "name">,
extensionPackPath: string,
token: CancellationToken,
) {
const qlpackPath = await getQlPackPath(extensionPackPath);
if (!qlpackPath) {
void showAndLogErrorMessage(
`Could not find any of ${QLPACK_FILENAMES.join(
", ",
)} in ${extensionPackPath}`,
);
return undefined;
}
const qlpack = await loadYaml(await readFile(qlpackPath, "utf8"), {
filename: qlpackPath,
});
if (typeof qlpack !== "object" || qlpack === null) {
void showAndLogErrorMessage(`Could not parse ${qlpackPath}`);
return undefined;
}
const dataExtensionPatternsValue = qlpack.dataExtensions;
if (
!(
Array.isArray(dataExtensionPatternsValue) ||
typeof dataExtensionPatternsValue === "string"
)
) {
void showAndLogErrorMessage(
`Expected 'dataExtensions' to be a string or an array in ${qlpackPath}`,
);
return undefined;
}
// The YAML allows either a string or an array of strings
const dataExtensionPatterns = Array.isArray(dataExtensionPatternsValue)
? dataExtensionPatternsValue
: [dataExtensionPatternsValue];
const filename = await window.showInputBox(
{
title: "Enter the name of the new model file",
value: `models/${databaseItem.name.replaceAll("/", ".")}.model.yml`,
validateInput: async (value: string): Promise<string | undefined> => {
if (value === "") {
return "File name must not be empty";
}
const path = resolve(extensionPackPath, value);
if (await pathExists(path)) {
return "File already exists";
}
const notInExtensionPack = relative(extensionPackPath, path).startsWith(
"..",
);
if (notInExtensionPack) {
return "File must be in the extension pack";
}
const matchesPattern = dataExtensionPatterns.some((pattern) =>
minimatch(value, pattern, { matchBase: true }),
);
if (!matchesPattern) {
return `File must match one of the patterns in 'dataExtensions' in ${qlpackPath}`;
}
return undefined;
},
},
token,
);
if (!filename) {
return undefined;
}
return resolve(extensionPackPath, filename);
}

View File

@@ -1,25 +1,27 @@
import { CoreCompletedQuery, QueryRunner } from "../queryRunner";
import { qlpackOfDatabase } from "../contextual/queryResolver";
import { file } from "tmp-promise";
import { dir } from "tmp-promise";
import { writeFile } from "fs-extra";
import { dump as dumpYaml } from "js-yaml";
import {
getOnDiskWorkspaceFolders,
showAndLogExceptionWithTelemetry,
} from "../helpers";
import { Logger, TeeLogger } from "../common";
import { TeeLogger } from "../common";
import { CancellationToken } from "vscode";
import { CodeQLCliServer } from "../cli";
import { DatabaseItem } from "../local-databases";
import { ProgressCallback } from "../progress";
import { fetchExternalApiQueries } from "./queries";
import { QueryResultType } from "../pure/new-messages";
import { join } from "path";
import { redactableError } from "../pure/errors";
import { QueryLanguage } from "../common/query-language";
export type RunQueryOptions = {
cliServer: Pick<CodeQLCliServer, "resolveQlpacks" | "resolveQueriesInSuite">;
cliServer: Pick<CodeQLCliServer, "resolveQlpacks">;
queryRunner: Pick<QueryRunner, "createQueryRun" | "logger">;
databaseItem: Pick<DatabaseItem, "contents" | "databaseUri" | "language">;
queryStorageDir: string;
logger: Logger;
progress: ProgressCallback;
token: CancellationToken;
@@ -30,54 +32,53 @@ export async function runQuery({
queryRunner,
databaseItem,
queryStorageDir,
logger,
progress,
token,
}: RunQueryOptions): Promise<CoreCompletedQuery | undefined> {
const qlpacks = await qlpackOfDatabase(cliServer, databaseItem);
// The below code is temporary to allow for rapid prototyping of the queries. Once the queries are stabilized, we will
// move these queries into the `github/codeql` repository and use them like any other contextual (e.g. AST) queries.
// This is intentionally not pretty code, as it will be removed soon.
// For a reference of what this should do in the future, see the previous implementation in
// https://github.com/github/vscode-codeql/blob/089d3566ef0bc67d9b7cc66e8fd6740b31c1c0b0/extensions/ql-vscode/src/data-extensions-editor/external-api-usage-query.ts#L33-L72
const packsToSearch = [qlpacks.dbschemePack];
if (qlpacks.queryPack) {
packsToSearch.push(qlpacks.queryPack);
const query = fetchExternalApiQueries[databaseItem.language as QueryLanguage];
if (!query) {
void showAndLogExceptionWithTelemetry(
redactableError`No external API usage query found for language ${databaseItem.language}`,
);
return;
}
const suiteFile = (
await file({
postfix: ".qls",
})
).path;
const suiteYaml = [];
for (const qlpack of packsToSearch) {
suiteYaml.push({
from: qlpack,
queries: ".",
include: {
id: `${databaseItem.language}/telemetry/fetch-external-apis`,
},
});
const queryDir = (await dir({ unsafeCleanup: true })).path;
const queryFile = join(queryDir, "FetchExternalApis.ql");
await writeFile(queryFile, query.mainQuery, "utf8");
if (query.dependencies) {
for (const [filename, contents] of Object.entries(query.dependencies)) {
const dependencyFile = join(queryDir, filename);
await writeFile(dependencyFile, contents, "utf8");
}
}
await writeFile(suiteFile, dumpYaml(suiteYaml), "utf8");
const syntheticQueryPack = {
name: "codeql/external-api-usage",
version: "0.0.0",
dependencies: {
[`codeql/${databaseItem.language}-all`]: "*",
},
};
const qlpackFile = join(queryDir, "codeql-pack.yml");
await writeFile(qlpackFile, dumpYaml(syntheticQueryPack), "utf8");
const additionalPacks = getOnDiskWorkspaceFolders();
const extensionPacks = Object.keys(
await cliServer.resolveQlpacks(additionalPacks, true),
);
const queries = await cliServer.resolveQueriesInSuite(
suiteFile,
getOnDiskWorkspaceFolders(),
);
if (queries.length !== 1) {
void logger.log(`Expected exactly one query, got ${queries.length}`);
return;
}
const query = queries[0];
const queryRun = queryRunner.createQueryRun(
databaseItem.databaseUri.fsPath,
{ queryPath: query, quickEvalPosition: undefined },
{ queryPath: queryFile, quickEvalPosition: undefined },
false,
getOnDiskWorkspaceFolders(),
extensionPacks,
@@ -86,11 +87,22 @@ export async function runQuery({
undefined,
);
return queryRun.evaluate(
const completedQuery = await queryRun.evaluate(
progress,
token,
new TeeLogger(queryRunner.logger, queryRun.outputDir.logPath),
);
if (completedQuery.resultType !== QueryResultType.SUCCESS) {
void showAndLogExceptionWithTelemetry(
redactableError`External API usage query failed: ${
completedQuery.message ?? "No message"
}`,
);
return;
}
return completedQuery;
}
export type GetResultsOptions = {

View File

@@ -4,7 +4,7 @@ import { join } from "path";
import { QueryRunner } from "../queryRunner";
import { CodeQLCliServer } from "../cli";
import { TeeLogger } from "../common";
import { extensiblePredicateDefinitions } from "./yaml";
import { extensiblePredicateDefinitions } from "./predicates";
import { ProgressCallback } from "../progress";
import {
getOnDiskWorkspaceFolders,

View File

@@ -0,0 +1,138 @@
import { ExternalApiUsage } from "./external-api-usage";
import {
ModeledMethod,
ModeledMethodType,
ModeledMethodWithSignature,
} from "./modeled-method";
export type ExternalApiUsageByType = {
externalApiUsage: ExternalApiUsage;
modeledMethod: ModeledMethod;
};
export type ExtensiblePredicateDefinition = {
extensiblePredicate: string;
generateMethodDefinition: (method: ExternalApiUsageByType) => Tuple[];
readModeledMethod: (row: Tuple[]) => ModeledMethodWithSignature;
supportedKinds?: string[];
};
type Tuple = boolean | number | string;
function readRowToMethod(row: Tuple[]): string {
return `${row[0]}.${row[1]}#${row[3]}${row[4]}`;
}
export const extensiblePredicateDefinitions: Record<
Exclude<ModeledMethodType, "none">,
ExtensiblePredicateDefinition
> = {
source: {
extensiblePredicate: "sourceModel",
// extensible predicate sourceModel(
// string package, string type, boolean subtypes, string name, string signature, string ext,
// string output, string kind, string provenance
// );
generateMethodDefinition: (method) => [
method.externalApiUsage.packageName,
method.externalApiUsage.typeName,
true,
method.externalApiUsage.methodName,
method.externalApiUsage.methodParameters,
"",
method.modeledMethod.output,
method.modeledMethod.kind,
"manual",
],
readModeledMethod: (row) => ({
signature: readRowToMethod(row),
modeledMethod: {
type: "source",
input: "",
output: row[6] as string,
kind: row[7] as string,
},
}),
supportedKinds: ["remote"],
},
sink: {
extensiblePredicate: "sinkModel",
// extensible predicate sinkModel(
// string package, string type, boolean subtypes, string name, string signature, string ext,
// string input, string kind, string provenance
// );
generateMethodDefinition: (method) => [
method.externalApiUsage.packageName,
method.externalApiUsage.typeName,
true,
method.externalApiUsage.methodName,
method.externalApiUsage.methodParameters,
"",
method.modeledMethod.input,
method.modeledMethod.kind,
"manual",
],
readModeledMethod: (row) => ({
signature: readRowToMethod(row),
modeledMethod: {
type: "sink",
input: row[6] as string,
output: "",
kind: row[7] as string,
},
}),
supportedKinds: ["sql", "xss", "logging"],
},
summary: {
extensiblePredicate: "summaryModel",
// extensible predicate summaryModel(
// string package, string type, boolean subtypes, string name, string signature, string ext,
// string input, string output, string kind, string provenance
// );
generateMethodDefinition: (method) => [
method.externalApiUsage.packageName,
method.externalApiUsage.typeName,
true,
method.externalApiUsage.methodName,
method.externalApiUsage.methodParameters,
"",
method.modeledMethod.input,
method.modeledMethod.output,
method.modeledMethod.kind,
"manual",
],
readModeledMethod: (row) => ({
signature: readRowToMethod(row),
modeledMethod: {
type: "summary",
input: row[6] as string,
output: row[7] as string,
kind: row[8] as string,
},
}),
supportedKinds: ["taint", "value"],
},
neutral: {
extensiblePredicate: "neutralModel",
// extensible predicate neutralModel(
// string package, string type, string name, string signature, string provenance
// );
generateMethodDefinition: (method) => [
method.externalApiUsage.packageName,
method.externalApiUsage.typeName,
method.externalApiUsage.methodName,
method.externalApiUsage.methodParameters,
"manual",
],
readModeledMethod: (row) => ({
signature: `${row[0]}.${row[1]}#${row[2]}${row[3]}`,
modeledMethod: {
type: "neutral",
input: "",
output: "",
kind: "",
},
}),
},
};

View File

@@ -0,0 +1,198 @@
import { Query } from "./query";
export const fetchExternalApisQuery: Query = {
mainQuery: `/**
* @name Usage of APIs coming from external libraries
* @description A list of 3rd party APIs used in the codebase.
* @tags telemetry
* @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
`,
dependencies: {
"ExternalApi.qll": `/** Provides classes and predicates related to handling APIs from external libraries. */
private import csharp
private import dotnet
private import semmle.code.csharp.dispatch.Dispatch
private import semmle.code.csharp.dataflow.ExternalFlow
private import semmle.code.csharp.dataflow.FlowSummary
private import semmle.code.csharp.dataflow.internal.DataFlowImplCommon as DataFlowImplCommon
private import semmle.code.csharp.dataflow.internal.DataFlowPrivate
private import semmle.code.csharp.dataflow.internal.DataFlowDispatch as DataFlowDispatch
private import semmle.code.csharp.dataflow.internal.FlowSummaryImpl as FlowSummaryImpl
private import semmle.code.csharp.dataflow.internal.TaintTrackingPrivate
private import semmle.code.csharp.security.dataflow.flowsources.Remote
pragma[nomagic]
private predicate isTestNamespace(Namespace ns) {
ns.getFullName()
.matches([
"NUnit.Framework%", "Xunit%", "Microsoft.VisualStudio.TestTools.UnitTesting%", "Moq%"
])
}
/**
* A test library.
*/
class TestLibrary extends RefType {
TestLibrary() { isTestNamespace(this.getNamespace()) }
}
/** Holds if the given callable is not worth supporting. */
private predicate isUninteresting(DotNet::Callable c) {
c.getDeclaringType() instanceof TestLibrary or
c.(Constructor).isParameterless()
}
/**
* An external API from either the C# Standard Library or a 3rd party library.
*/
class ExternalApi extends DotNet::Callable {
ExternalApi() {
this.isUnboundDeclaration() and
this.fromLibrary() and
this.(Modifiable).isEffectivelyPublic() and
not isUninteresting(this)
}
/**
* Gets the unbound type, name and parameter types of this API.
*/
bindingset[this]
private string getSignature() {
result =
this.getDeclaringType().getUnboundDeclaration() + "." + this.getName() + "(" +
parameterQualifiedTypeNamesToString(this) + ")"
}
/**
* Gets the namespace of this API.
*/
bindingset[this]
string getNamespace() { this.getDeclaringType().hasQualifiedName(result, _) }
/**
* Gets the namespace and signature of this API.
*/
bindingset[this]
string getApiName() { result = this.getNamespace() + "#" + this.getSignature() }
/** Gets a node that is an input to a call to this API. */
private ArgumentNode getAnInput() {
result
.getCall()
.(DataFlowDispatch::NonDelegateDataFlowCall)
.getATarget(_)
.getUnboundDeclaration() = this
}
/** Gets a node that is an output from a call to this API. */
private DataFlow::Node getAnOutput() {
exists(
Call c, DataFlowDispatch::NonDelegateDataFlowCall dc, DataFlowImplCommon::ReturnKindExt ret
|
dc.getDispatchCall().getCall() = c and
c.getTarget().getUnboundDeclaration() = this
|
result = ret.getAnOutNode(dc)
)
}
/** Holds if this API has a supported summary. */
pragma[nomagic]
predicate hasSummary() {
this instanceof SummarizedCallable
or
defaultAdditionalTaintStep(this.getAnInput(), _)
}
/** Holds if this API is a known source. */
pragma[nomagic]
predicate isSource() {
this.getAnOutput() instanceof RemoteFlowSource or sourceNode(this.getAnOutput(), _)
}
/** Holds if this API is a known sink. */
pragma[nomagic]
predicate isSink() { sinkNode(this.getAnInput(), _) }
/** Holds if this API is a known neutral. */
pragma[nomagic]
predicate isNeutral() { this instanceof FlowSummaryImpl::Public::NeutralCallable }
/**
* Holds if this API is supported by existing CodeQL libraries, that is, it is either a
* recognized source, sink or neutral or it has a flow summary.
*/
predicate isSupported() {
this.hasSummary() or this.isSource() or this.isSink() or this.isNeutral()
}
}
/**
* Gets the limit for the number of results produced by a telemetry query.
*/
int resultLimit() { result = 1000 }
/**
* Holds if it is relevant to count usages of "api".
*/
signature predicate relevantApi(ExternalApi api);
/**
* Given a predicate to count relevant API usages, this module provides a predicate
* for restricting the number or returned results based on a certain limit.
*/
module Results<relevantApi/1 getRelevantUsages> {
private int getUsages(string apiName) {
result =
strictcount(Call c, ExternalApi api |
c.getTarget().getUnboundDeclaration() = api and
apiName = api.getApiName() and
getRelevantUsages(api)
)
}
private int getOrder(string apiName) {
apiName =
rank[result](string name, int usages |
usages = getUsages(name)
|
name order by usages desc, name
)
}
/**
* Holds if there exists an API with "apiName" that is being used "usages" times
* and if it is in the top results (guarded by resultLimit).
*/
predicate restrict(string apiName, int usages) {
usages = getUsages(apiName) and
getOrder(apiName) <= resultLimit()
}
}
`,
},
};

View File

@@ -0,0 +1,9 @@
import { fetchExternalApisQuery as csharpFetchExternalApisQuery } from "./csharp";
import { fetchExternalApisQuery as javaFetchExternalApisQuery } from "./java";
import { Query } from "./query";
import { QueryLanguage } from "../../common/query-language";
export const fetchExternalApiQueries: Partial<Record<QueryLanguage, Query>> = {
[QueryLanguage.CSharp]: csharpFetchExternalApisQuery,
[QueryLanguage.Java]: javaFetchExternalApisQuery,
};

View File

@@ -0,0 +1,179 @@
import { Query } from "./query";
export const fetchExternalApisQuery: Query = {
mainQuery: `/**
* @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
* @id java/telemetry/fetch-external-apis
*/
import java
import semmle.code.java.dataflow.internal.FlowSummaryImpl as FlowSummaryImpl
import ExternalApi
private Call aUsage(ExternalApi api) {
result.getCallee().getSourceDeclaration() = api and
not result.getFile() instanceof GeneratedFile
}
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
`,
dependencies: {
"ExternalApi.qll": `/** Provides classes and predicates related to handling APIs from external libraries. */
private import java
private import semmle.code.java.dataflow.DataFlow
private import semmle.code.java.dataflow.ExternalFlow
private import semmle.code.java.dataflow.FlowSources
private import semmle.code.java.dataflow.FlowSummary
private import semmle.code.java.dataflow.internal.DataFlowPrivate
private import semmle.code.java.dataflow.TaintTracking
pragma[nomagic]
private predicate isTestPackage(Package p) {
p.getName()
.matches([
"org.junit%", "junit.%", "org.mockito%", "org.assertj%",
"com.github.tomakehurst.wiremock%", "org.hamcrest%", "org.springframework.test.%",
"org.springframework.mock.%", "org.springframework.boot.test.%", "reactor.test%",
"org.xmlunit%", "org.testcontainers.%", "org.opentest4j%", "org.mockserver%",
"org.powermock%", "org.skyscreamer.jsonassert%", "org.rnorth.visibleassertions",
"org.openqa.selenium%", "com.gargoylesoftware.htmlunit%", "org.jboss.arquillian.testng%",
"org.testng%"
])
}
/**
* A test library.
*/
private class TestLibrary extends RefType {
TestLibrary() { isTestPackage(this.getPackage()) }
}
private string containerAsJar(Container container) {
if container instanceof JarFile then result = container.getBaseName() else result = "rt.jar"
}
/** Holds if the given callable is not worth supporting. */
private predicate isUninteresting(Callable c) {
c.getDeclaringType() instanceof TestLibrary or
c.(Constructor).isParameterless()
}
/**
* An external API from either the Standard Library or a 3rd party library.
*/
class ExternalApi extends Callable {
ExternalApi() { not this.fromSource() and not isUninteresting(this) }
/**
* Gets information about the external API in the form expected by the MaD modeling framework.
*/
string getApiName() {
result =
this.getDeclaringType().getPackage() + "." + this.getDeclaringType().getSourceDeclaration() +
"#" + this.getName() + paramsString(this)
}
/**
* Gets the jar file containing this API. Normalizes the Java Runtime to "rt.jar" despite the presence of modules.
*/
string jarContainer() { result = containerAsJar(this.getCompilationUnit().getParentContainer*()) }
/** Gets a node that is an input to a call to this API. */
private DataFlow::Node getAnInput() {
exists(Call call | call.getCallee().getSourceDeclaration() = this |
result.asExpr().(Argument).getCall() = call or
result.(ArgumentNode).getCall().asCall() = call
)
}
/** Gets a node that is an output from a call to this API. */
private DataFlow::Node getAnOutput() {
exists(Call call | call.getCallee().getSourceDeclaration() = this |
result.asExpr() = call or
result.(DataFlow::PostUpdateNode).getPreUpdateNode().(ArgumentNode).getCall().asCall() = call
)
}
/** Holds if this API has a supported summary. */
pragma[nomagic]
predicate hasSummary() {
this = any(SummarizedCallable sc).asCallable() or
TaintTracking::localAdditionalTaintStep(this.getAnInput(), _)
}
pragma[nomagic]
predicate isSource() {
this.getAnOutput() instanceof RemoteFlowSource or sourceNode(this.getAnOutput(), _)
}
/** Holds if this API is a known sink. */
pragma[nomagic]
predicate isSink() { sinkNode(this.getAnInput(), _) }
/** Holds if this API is supported by existing CodeQL libraries, that is, it is either a recognized source or sink or has a flow summary. */
predicate isSupported() { this.hasSummary() or this.isSource() or this.isSink() }
}
/** DEPRECATED: Alias for ExternalApi */
deprecated class ExternalAPI = ExternalApi;
/**
* Gets the limit for the number of results produced by a telemetry query.
*/
int resultLimit() { result = 1000 }
/**
* Holds if it is relevant to count usages of \`api\`.
*/
signature predicate relevantApi(ExternalApi api);
/**
* Given a predicate to count relevant API usages, this module provides a predicate
* for restricting the number or returned results based on a certain limit.
*/
module Results<relevantApi/1 getRelevantUsages> {
private int getUsages(string apiName) {
result =
strictcount(Call c, ExternalApi api |
c.getCallee().getSourceDeclaration() = api and
not c.getFile() instanceof GeneratedFile and
apiName = api.getApiName() and
getRelevantUsages(api)
)
}
private int getOrder(string apiInfo) {
apiInfo =
rank[result](string info, int usages |
usages = getUsages(info)
|
info order by usages desc, info
)
}
/**
* Holds if there exists an API with \`apiName\` that is being used \`usages\` times
* and if it is in the top results (guarded by resultLimit).
*/
predicate restrict(string apiName, int usages) {
usages = getUsages(apiName) and
getOrder(apiName) <= resultLimit()
}
}
`,
},
};

View File

@@ -0,0 +1,6 @@
export type Query = {
mainQuery: string;
dependencies?: {
[filename: string]: string;
};
};

View File

@@ -6,6 +6,7 @@ import {
ModeledMethodType,
ModeledMethodWithSignature,
} from "./modeled-method";
import { extensiblePredicateDefinitions } from "./predicates";
import * as dataSchemaJson from "./data-schema.json";
@@ -23,120 +24,6 @@ type ExtensiblePredicateDefinition = {
readModeledMethod: (row: any[]) => ModeledMethodWithSignature;
};
function readRowToMethod(row: any[]): string {
return `${row[0]}.${row[1]}#${row[3]}${row[4]}`;
}
export const extensiblePredicateDefinitions: Record<
Exclude<ModeledMethodType, "none">,
ExtensiblePredicateDefinition
> = {
source: {
extensiblePredicate: "sourceModel",
// extensible predicate sourceModel(
// string package, string type, boolean subtypes, string name, string signature, string ext,
// string output, string kind, string provenance
// );
generateMethodDefinition: (method) => [
method.externalApiUsage.packageName,
method.externalApiUsage.typeName,
true,
method.externalApiUsage.methodName,
method.externalApiUsage.methodParameters,
"",
method.modeledMethod.output,
method.modeledMethod.kind,
"manual",
],
readModeledMethod: (row) => ({
signature: readRowToMethod(row),
modeledMethod: {
type: "source",
input: "",
output: row[6],
kind: row[7],
},
}),
},
sink: {
extensiblePredicate: "sinkModel",
// extensible predicate sinkModel(
// string package, string type, boolean subtypes, string name, string signature, string ext,
// string input, string kind, string provenance
// );
generateMethodDefinition: (method) => [
method.externalApiUsage.packageName,
method.externalApiUsage.typeName,
true,
method.externalApiUsage.methodName,
method.externalApiUsage.methodParameters,
"",
method.modeledMethod.input,
method.modeledMethod.kind,
"manual",
],
readModeledMethod: (row) => ({
signature: readRowToMethod(row),
modeledMethod: {
type: "sink",
input: row[6],
output: "",
kind: row[7],
},
}),
},
summary: {
extensiblePredicate: "summaryModel",
// extensible predicate summaryModel(
// string package, string type, boolean subtypes, string name, string signature, string ext,
// string input, string output, string kind, string provenance
// );
generateMethodDefinition: (method) => [
method.externalApiUsage.packageName,
method.externalApiUsage.typeName,
true,
method.externalApiUsage.methodName,
method.externalApiUsage.methodParameters,
"",
method.modeledMethod.input,
method.modeledMethod.output,
method.modeledMethod.kind,
"manual",
],
readModeledMethod: (row) => ({
signature: readRowToMethod(row),
modeledMethod: {
type: "summary",
input: row[6],
output: row[7],
kind: row[8],
},
}),
},
neutral: {
extensiblePredicate: "neutralModel",
// extensible predicate neutralModel(
// string package, string type, string name, string signature, string provenance
// );
generateMethodDefinition: (method) => [
method.externalApiUsage.packageName,
method.externalApiUsage.typeName,
method.externalApiUsage.methodName,
method.externalApiUsage.methodParameters,
"manual",
],
readModeledMethod: (row) => ({
signature: `${row[0]}.${row[1]}#${row[2]}${row[3]}`,
modeledMethod: {
type: "neutral",
input: "",
output: "",
kind: "",
},
}),
},
};
function createDataProperty(
methods: ExternalApiUsageByType[],
definition: ExtensiblePredicateDefinition,
@@ -156,6 +43,7 @@ function createDataProperty(
}
export function createDataExtensionYaml(
language: string,
externalApiUsages: ExternalApiUsage[],
modeledMethods: Record<string, ModeledMethod>,
) {
@@ -182,7 +70,7 @@ export function createDataExtensionYaml(
const extensions = Object.entries(extensiblePredicateDefinitions).map(
([type, definition]) => ` - addsTo:
pack: codeql/java-all
pack: codeql/${language}-all
extensible: ${definition.extensiblePredicate}
data:${createDataProperty(
methodsByType[type as Exclude<ModeledMethodType, "none">],

View File

@@ -34,11 +34,11 @@ import { DatabasePanelCommands } from "../../common/commands";
import { App } from "../../common/app";
export interface RemoteDatabaseQuickPickItem extends QuickPickItem {
kind: string;
remoteDatabaseKind: string;
}
export interface AddListQuickPickItem extends QuickPickItem {
kind: DbListKind;
databaseKind: DbListKind;
}
export class DbPanel extends DisposableObject {
@@ -113,19 +113,19 @@ export class DbPanel extends DisposableObject {
) {
await this.addNewRemoteRepo(highlightedItem.parentListName);
} else {
const quickPickItems = [
const quickPickItems: RemoteDatabaseQuickPickItem[] = [
{
label: "$(repo) From a GitHub repository",
detail: "Add a variant analysis repository from GitHub",
alwaysShow: true,
kind: "repo",
remoteDatabaseKind: "repo",
},
{
label: "$(organization) All repositories of a GitHub org or owner",
detail:
"Add a variant analysis list of repositories from a GitHub organization/owner",
alwaysShow: true,
kind: "owner",
remoteDatabaseKind: "owner",
},
];
const databaseKind =
@@ -142,9 +142,9 @@ export class DbPanel extends DisposableObject {
// We set 'true' to make this a silent exception.
throw new UserCancellationException("No repository selected", true);
}
if (databaseKind.kind === "repo") {
if (databaseKind.remoteDatabaseKind === "repo") {
await this.addNewRemoteRepo();
} else if (databaseKind.kind === "owner") {
} else if (databaseKind.remoteDatabaseKind === "owner") {
await this.addNewRemoteOwner();
}
}

View File

@@ -177,7 +177,13 @@ function getCommands(
cliServer.restartCliServer();
await Promise.all([
queryRunner.restartQueryServer(progress, token),
ideServer.restart(),
async () => {
if (ideServer.isRunning()) {
await ideServer.restart();
} else {
await ideServer.start();
}
},
]);
void showAndLogInformationMessage("CodeQL Query Server restarted.", {
outputLogger: queryServerLogger,

View File

@@ -8,7 +8,7 @@ import {
} from "fs-extra";
import { glob } from "glob";
import { load } from "js-yaml";
import { join, basename } from "path";
import { join, basename, dirname } from "path";
import { dirSync } from "tmp-promise";
import {
ExtensionContext,
@@ -16,6 +16,7 @@ import {
window as Window,
workspace,
env,
WorkspaceFolder,
} from "vscode";
import { CodeQLCliServer, QlpacksInfo } from "./cli";
import { UserCancellationException } from "./progress";
@@ -249,16 +250,21 @@ export async function showInformationMessageWithAction(
}
/** Gets all active workspace folders that are on the filesystem. */
export function getOnDiskWorkspaceFolders() {
export function getOnDiskWorkspaceFoldersObjects() {
const workspaceFolders = workspace.workspaceFolders || [];
const diskWorkspaceFolders: string[] = [];
const diskWorkspaceFolders: WorkspaceFolder[] = [];
for (const workspaceFolder of workspaceFolders) {
if (workspaceFolder.uri.scheme === "file")
diskWorkspaceFolders.push(workspaceFolder.uri.fsPath);
diskWorkspaceFolders.push(workspaceFolder);
}
return diskWorkspaceFolders;
}
/** Gets all active workspace folders that are on the filesystem. */
export function getOnDiskWorkspaceFolders() {
return getOnDiskWorkspaceFoldersObjects().map((folder) => folder.uri.fsPath);
}
/** Check if folder is already present in workspace */
export function isFolderAlreadyInWorkspace(folderName: string) {
const workspaceFolders = workspace.workspaceFolders || [];
@@ -785,3 +791,39 @@ export async function* walkDirectory(
}
}
}
/**
* Returns the path of the first folder in the workspace.
* This is used to decide where to create skeleton QL packs.
*
* If the first folder is a QL pack, then the parent folder is returned.
* This is because the vscode-codeql-starter repo contains a ql pack in
* the first folder.
*
* This is a temporary workaround until we can retire the
* vscode-codeql-starter repo.
*/
export function getFirstWorkspaceFolder() {
const workspaceFolders = getOnDiskWorkspaceFolders();
if (!workspaceFolders || workspaceFolders.length === 0) {
throw new Error("No workspace folders found");
}
const firstFolderFsPath = workspaceFolders[0];
// For the vscode-codeql-starter repo, the first folder will be a ql pack
// so we need to get the parent folder
if (
firstFolderFsPath.includes(
join("vscode-codeql-starter", "codeql-custom-queries"),
)
) {
// return the parent folder
return dirname(firstFolderFsPath);
} else {
// if the first folder is not a ql pack, then we are in a normal workspace
return firstFolderFsPath;
}
}

View File

@@ -23,6 +23,7 @@ export class ServerProcess implements Disposable {
dispose(): void {
void this.logger.log(`Stopping ${this.name}...`);
this.connection.dispose();
this.connection.end();
this.child.stdin!.end();
this.child.stderr!.destroy();
// TODO kill the process if it doesn't terminate after a certain time limit.

View File

@@ -11,6 +11,7 @@ import {
showAndLogExceptionWithTelemetry,
isFolderAlreadyInWorkspace,
showBinaryChoiceDialog,
getFirstWorkspaceFolder,
} from "./helpers";
import { ProgressCallback, withProgress } from "./progress";
import {
@@ -29,6 +30,7 @@ import { isCodespacesTemplate } from "./config";
import { QlPackGenerator } from "./qlpack-generator";
import { QueryLanguage } from "./common/query-language";
import { App } from "./common/app";
import { existsSync } from "fs";
/**
* databases.ts
@@ -662,8 +664,13 @@ export class DatabaseManager extends DisposableObject {
return;
}
const firstWorkspaceFolder = getFirstWorkspaceFolder();
const folderName = `codeql-custom-queries-${databaseItem.language}`;
if (isFolderAlreadyInWorkspace(folderName)) {
if (
existsSync(join(firstWorkspaceFolder, folderName)) ||
isFolderAlreadyInWorkspace(folderName)
) {
return;
}
@@ -680,7 +687,7 @@ export class DatabaseManager extends DisposableObject {
folderName,
databaseItem.language as QueryLanguage,
this.cli,
this.ctx.storageUri?.fsPath,
firstWorkspaceFolder,
);
await qlPackGenerator.generate();
} catch (e: unknown) {
@@ -1022,7 +1029,19 @@ export class DatabaseManager extends DisposableObject {
token: vscode.CancellationToken,
dbItem: DatabaseItem,
) {
await this.qs.deregisterDatabase(progress, token, dbItem);
try {
await this.qs.deregisterDatabase(progress, token, dbItem);
} catch (e) {
const message = getErrorMessage(e);
if (message === "Connection is disposed.") {
// This is expected if the query server is not running.
void extLogger.log(
`Could not de-register database '${dbItem.name}' because query server is not running.`,
);
return;
}
throw e;
}
}
private async registerDatabase(
progress: ProgressCallback,

View File

@@ -1,7 +1,7 @@
import { writeFile } from "fs-extra";
import { mkdir, writeFile } from "fs-extra";
import { dump } from "js-yaml";
import { join } from "path";
import { Uri, workspace } from "vscode";
import { Uri } from "vscode";
import { CodeQLCliServer } from "./cli";
import { QueryLanguage } from "./common/query-language";
@@ -44,14 +44,7 @@ export class QlPackGenerator {
}
private async createWorkspaceFolder() {
await workspace.fs.createDirectory(this.folderUri);
const end = (workspace.workspaceFolders || []).length;
workspace.updateWorkspaceFolders(end, 0, {
name: this.folderName,
uri: this.folderUri,
});
await mkdir(this.folderUri.fsPath);
}
private async createQlPackYaml() {

View File

@@ -1,34 +1,15 @@
import { assertNever } from "../../pure/helpers-pure";
import {
LocalQueryInfo,
InitialQueryInfo,
CompletedQueryInfo,
} from "../../query-results";
import { QueryEvaluationInfo } from "../../run-queries-shared";
import { QueryHistoryInfo } from "../query-history-info";
import {
QueryHistoryLocalQueryDto,
InitialQueryInfoDto,
QueryEvaluationInfoDto,
CompletedQueryInfoDto,
SortedResultSetInfoDto,
SortDirectionDto,
} from "./query-history-local-query-dto";
import { mapLocalQueryInfoToDto } from "./query-history-local-query-domain-mapper";
import { QueryHistoryItemDto } from "./query-history-dto";
import { QueryHistoryVariantAnalysisDto } from "./query-history-variant-analysis-dto";
import {
RawResultsSortState,
SortDirection,
SortedResultSetInfo,
} from "../../pure/interface-types";
import { mapQueryHistoryVariantAnalysisToDto } from "./query-history-variant-analysis-domain-mapper";
export function mapQueryHistoryToDto(
queries: QueryHistoryInfo[],
): QueryHistoryItemDto[] {
return queries.map((q) => {
if (q.t === "variant-analysis") {
const query: QueryHistoryVariantAnalysisDto = q;
return query;
return mapQueryHistoryVariantAnalysisToDto(q);
} else if (q.t === "local") {
return mapLocalQueryInfoToDto(q);
} else {
@@ -36,105 +17,3 @@ export function mapQueryHistoryToDto(
}
});
}
function mapLocalQueryInfoToDto(
query: LocalQueryInfo,
): QueryHistoryLocalQueryDto {
return {
initialInfo: mapInitialQueryInfoToDto(query.initialInfo),
t: "local",
evalLogLocation: query.evalLogLocation,
evalLogSummaryLocation: query.evalLogSummaryLocation,
jsonEvalLogSummaryLocation: query.jsonEvalLogSummaryLocation,
evalLogSummarySymbolsLocation: query.evalLogSummarySymbolsLocation,
failureReason: query.failureReason,
completedQuery:
query.completedQuery && mapCompletedQueryToDto(query.completedQuery),
};
}
function mapCompletedQueryToDto(
query: CompletedQueryInfo,
): CompletedQueryInfoDto {
const sortedResults = Object.fromEntries(
Object.entries(query.sortedResultsInfo).map(([key, value]) => {
return [key, mapSortedResultSetInfoToDto(value)];
}),
);
return {
query: mapQueryEvaluationInfoToDto(query.query),
result: {
runId: query.result.runId,
queryId: query.result.queryId,
resultType: query.result.resultType,
evaluationTime: query.result.evaluationTime,
message: query.result.message,
logFileLocation: query.result.logFileLocation,
},
logFileLocation: query.logFileLocation,
successful: query.successful,
message: query.message,
resultCount: query.resultCount,
sortedResultsInfo: sortedResults,
};
}
function mapSortDirectionToDto(sortDirection: SortDirection): SortDirectionDto {
switch (sortDirection) {
case SortDirection.asc:
return SortDirectionDto.asc;
case SortDirection.desc:
return SortDirectionDto.desc;
}
}
function mapRawResultsSortStateToDto(
sortState: RawResultsSortState,
): SortedResultSetInfoDto["sortState"] {
return {
columnIndex: sortState.columnIndex,
sortDirection: mapSortDirectionToDto(sortState.sortDirection),
};
}
function mapSortedResultSetInfoToDto(
resultSet: SortedResultSetInfo,
): SortedResultSetInfoDto {
return {
resultsPath: resultSet.resultsPath,
sortState: mapRawResultsSortStateToDto(resultSet.sortState),
};
}
function mapInitialQueryInfoToDto(
localQueryInitialInfo: InitialQueryInfo,
): InitialQueryInfoDto {
return {
userSpecifiedLabel: localQueryInitialInfo.userSpecifiedLabel,
queryText: localQueryInitialInfo.queryText,
isQuickQuery: localQueryInitialInfo.isQuickQuery,
isQuickEval: localQueryInitialInfo.isQuickEval,
quickEvalPosition: localQueryInitialInfo.quickEvalPosition,
queryPath: localQueryInitialInfo.queryPath,
databaseInfo: {
databaseUri: localQueryInitialInfo.databaseInfo.databaseUri,
name: localQueryInitialInfo.databaseInfo.name,
},
start: localQueryInitialInfo.start,
id: localQueryInitialInfo.id,
};
}
function mapQueryEvaluationInfoToDto(
queryEvaluationInfo: QueryEvaluationInfo,
): QueryEvaluationInfoDto {
return {
querySaveDir: queryEvaluationInfo.querySaveDir,
dbItemPath: queryEvaluationInfo.dbItemPath,
databaseHasMetadataFile: queryEvaluationInfo.databaseHasMetadataFile,
quickEvalPosition: queryEvaluationInfo.quickEvalPosition,
metadata: queryEvaluationInfo.metadata,
resultsPaths: queryEvaluationInfo.resultsPaths,
};
}

View File

@@ -1,36 +1,14 @@
import {
LocalQueryInfo,
CompletedQueryInfo,
InitialQueryInfo,
} from "../../query-results";
import { QueryEvaluationInfo } from "../../run-queries-shared";
import { QueryHistoryInfo } from "../query-history-info";
import { VariantAnalysisHistoryItem } from "../variant-analysis-history-item";
import {
CompletedQueryInfoDto,
QueryEvaluationInfoDto,
InitialQueryInfoDto,
QueryHistoryLocalQueryDto,
SortDirectionDto,
InterpretedResultsSortStateDto,
SortedResultSetInfoDto,
RawResultsSortStateDto,
} from "./query-history-local-query-dto";
import { QueryHistoryItemDto } from "./query-history-dto";
import {
InterpretedResultsSortState,
RawResultsSortState,
SortDirection,
SortedResultSetInfo,
} from "../../pure/interface-types";
import { mapQueryHistoryVariantAnalysisToDomainModel } from "./query-history-variant-analysis-dto-mapper";
import { mapLocalQueryItemToDomainModel } from "./query-history-local-query-dto-mapper";
export function mapQueryHistoryToDomainModel(
queries: QueryHistoryItemDto[],
): QueryHistoryInfo[] {
return queries.map((d) => {
if (d.t === "variant-analysis") {
const query: VariantAnalysisHistoryItem = d;
return query;
return mapQueryHistoryVariantAnalysisToDomainModel(d);
} else if (d.t === "local") {
return mapLocalQueryItemToDomainModel(d);
}
@@ -42,122 +20,3 @@ export function mapQueryHistoryToDomainModel(
);
});
}
function mapLocalQueryItemToDomainModel(
localQuery: QueryHistoryLocalQueryDto,
): LocalQueryInfo {
return new LocalQueryInfo(
mapInitialQueryInfoToDomainModel(localQuery.initialInfo),
undefined,
localQuery.failureReason,
localQuery.completedQuery &&
mapCompletedQueryInfoToDomainModel(localQuery.completedQuery),
localQuery.evalLogLocation,
localQuery.evalLogSummaryLocation,
localQuery.jsonEvalLogSummaryLocation,
localQuery.evalLogSummarySymbolsLocation,
);
}
function mapCompletedQueryInfoToDomainModel(
completedQuery: CompletedQueryInfoDto,
): CompletedQueryInfo {
const sortState =
completedQuery.interpretedResultsSortState &&
mapSortStateToDomainModel(completedQuery.interpretedResultsSortState);
const sortedResults = Object.fromEntries(
Object.entries(completedQuery.sortedResultsInfo).map(([key, value]) => {
return [key, mapSortedResultSetInfoToDomainModel(value)];
}),
);
return new CompletedQueryInfo(
mapQueryEvaluationInfoToDomainModel(completedQuery.query),
{
runId: completedQuery.result.runId,
queryId: completedQuery.result.queryId,
resultType: completedQuery.result.resultType,
evaluationTime: completedQuery.result.evaluationTime,
message: completedQuery.result.message,
logFileLocation: completedQuery.result.logFileLocation,
},
completedQuery.logFileLocation,
completedQuery.successful ?? completedQuery.sucessful,
completedQuery.message,
sortState,
completedQuery.resultCount,
sortedResults,
);
}
function mapInitialQueryInfoToDomainModel(
initialInfo: InitialQueryInfoDto,
): InitialQueryInfo {
return {
userSpecifiedLabel: initialInfo.userSpecifiedLabel,
queryText: initialInfo.queryText,
isQuickQuery: initialInfo.isQuickQuery,
isQuickEval: initialInfo.isQuickEval,
quickEvalPosition: initialInfo.quickEvalPosition,
queryPath: initialInfo.queryPath,
databaseInfo: {
databaseUri: initialInfo.databaseInfo.databaseUri,
name: initialInfo.databaseInfo.name,
},
start: new Date(initialInfo.start),
id: initialInfo.id,
};
}
function mapQueryEvaluationInfoToDomainModel(
evaluationInfo: QueryEvaluationInfoDto,
): QueryEvaluationInfo {
return new QueryEvaluationInfo(
evaluationInfo.querySaveDir,
evaluationInfo.dbItemPath,
evaluationInfo.databaseHasMetadataFile,
evaluationInfo.quickEvalPosition,
evaluationInfo.metadata,
);
}
function mapSortDirectionToDomainModel(
sortDirection: SortDirectionDto,
): SortDirection {
switch (sortDirection) {
case SortDirectionDto.asc:
return SortDirection.asc;
case SortDirectionDto.desc:
return SortDirection.desc;
}
}
function mapSortStateToDomainModel(
sortState: InterpretedResultsSortStateDto,
): InterpretedResultsSortState {
return {
sortBy: sortState.sortBy,
sortDirection: mapSortDirectionToDomainModel(sortState.sortDirection),
};
}
function mapSortedResultSetInfoToDomainModel(
sortedResultSetInfo: SortedResultSetInfoDto,
): SortedResultSetInfo {
return {
resultsPath: sortedResultSetInfo.resultsPath,
sortState: mapRawResultsSortStateToDomainModel(
sortedResultSetInfo.sortState,
),
};
}
function mapRawResultsSortStateToDomainModel(
sortState: RawResultsSortStateDto,
): RawResultsSortState {
return {
columnIndex: sortState.columnIndex,
sortDirection: mapSortDirectionToDomainModel(sortState.sortDirection),
};
}

View File

@@ -0,0 +1,121 @@
import {
LocalQueryInfo,
InitialQueryInfo,
CompletedQueryInfo,
} from "../../query-results";
import { QueryEvaluationInfo } from "../../run-queries-shared";
import {
QueryHistoryLocalQueryDto,
InitialQueryInfoDto,
QueryEvaluationInfoDto,
CompletedQueryInfoDto,
SortedResultSetInfoDto,
SortDirectionDto,
} from "./query-history-local-query-dto";
import {
RawResultsSortState,
SortDirection,
SortedResultSetInfo,
} from "../../pure/interface-types";
export function mapLocalQueryInfoToDto(
query: LocalQueryInfo,
): QueryHistoryLocalQueryDto {
return {
initialInfo: mapInitialQueryInfoToDto(query.initialInfo),
t: "local",
evalLogLocation: query.evalLogLocation,
evalLogSummaryLocation: query.evalLogSummaryLocation,
jsonEvalLogSummaryLocation: query.jsonEvalLogSummaryLocation,
evalLogSummarySymbolsLocation: query.evalLogSummarySymbolsLocation,
failureReason: query.failureReason,
completedQuery:
query.completedQuery && mapCompletedQueryToDto(query.completedQuery),
};
}
function mapCompletedQueryToDto(
query: CompletedQueryInfo,
): CompletedQueryInfoDto {
const sortedResults = Object.fromEntries(
Object.entries(query.sortedResultsInfo).map(([key, value]) => {
return [key, mapSortedResultSetInfoToDto(value)];
}),
);
return {
query: mapQueryEvaluationInfoToDto(query.query),
result: {
runId: query.result.runId,
queryId: query.result.queryId,
resultType: query.result.resultType,
evaluationTime: query.result.evaluationTime,
message: query.result.message,
logFileLocation: query.result.logFileLocation,
},
logFileLocation: query.logFileLocation,
successful: query.successful,
message: query.message,
resultCount: query.resultCount,
sortedResultsInfo: sortedResults,
};
}
function mapSortDirectionToDto(sortDirection: SortDirection): SortDirectionDto {
switch (sortDirection) {
case SortDirection.asc:
return SortDirectionDto.asc;
case SortDirection.desc:
return SortDirectionDto.desc;
}
}
function mapRawResultsSortStateToDto(
sortState: RawResultsSortState,
): SortedResultSetInfoDto["sortState"] {
return {
columnIndex: sortState.columnIndex,
sortDirection: mapSortDirectionToDto(sortState.sortDirection),
};
}
function mapSortedResultSetInfoToDto(
resultSet: SortedResultSetInfo,
): SortedResultSetInfoDto {
return {
resultsPath: resultSet.resultsPath,
sortState: mapRawResultsSortStateToDto(resultSet.sortState),
};
}
function mapInitialQueryInfoToDto(
localQueryInitialInfo: InitialQueryInfo,
): InitialQueryInfoDto {
return {
userSpecifiedLabel: localQueryInitialInfo.userSpecifiedLabel,
queryText: localQueryInitialInfo.queryText,
isQuickQuery: localQueryInitialInfo.isQuickQuery,
isQuickEval: localQueryInitialInfo.isQuickEval,
quickEvalPosition: localQueryInitialInfo.quickEvalPosition,
queryPath: localQueryInitialInfo.queryPath,
databaseInfo: {
databaseUri: localQueryInitialInfo.databaseInfo.databaseUri,
name: localQueryInitialInfo.databaseInfo.name,
},
start: localQueryInitialInfo.start,
id: localQueryInitialInfo.id,
};
}
function mapQueryEvaluationInfoToDto(
queryEvaluationInfo: QueryEvaluationInfo,
): QueryEvaluationInfoDto {
return {
querySaveDir: queryEvaluationInfo.querySaveDir,
dbItemPath: queryEvaluationInfo.dbItemPath,
databaseHasMetadataFile: queryEvaluationInfo.databaseHasMetadataFile,
quickEvalPosition: queryEvaluationInfo.quickEvalPosition,
metadata: queryEvaluationInfo.metadata,
resultsPaths: queryEvaluationInfo.resultsPaths,
};
}

View File

@@ -0,0 +1,141 @@
import {
LocalQueryInfo,
CompletedQueryInfo,
InitialQueryInfo,
} from "../../query-results";
import { QueryEvaluationInfo } from "../../run-queries-shared";
import {
CompletedQueryInfoDto,
QueryEvaluationInfoDto,
InitialQueryInfoDto,
QueryHistoryLocalQueryDto,
SortDirectionDto,
InterpretedResultsSortStateDto,
SortedResultSetInfoDto,
RawResultsSortStateDto,
} from "./query-history-local-query-dto";
import {
InterpretedResultsSortState,
RawResultsSortState,
SortDirection,
SortedResultSetInfo,
} from "../../pure/interface-types";
export function mapLocalQueryItemToDomainModel(
localQuery: QueryHistoryLocalQueryDto,
): LocalQueryInfo {
return new LocalQueryInfo(
mapInitialQueryInfoToDomainModel(localQuery.initialInfo),
undefined,
localQuery.failureReason,
localQuery.completedQuery &&
mapCompletedQueryInfoToDomainModel(localQuery.completedQuery),
localQuery.evalLogLocation,
localQuery.evalLogSummaryLocation,
localQuery.jsonEvalLogSummaryLocation,
localQuery.evalLogSummarySymbolsLocation,
);
}
function mapCompletedQueryInfoToDomainModel(
completedQuery: CompletedQueryInfoDto,
): CompletedQueryInfo {
const sortState =
completedQuery.interpretedResultsSortState &&
mapSortStateToDomainModel(completedQuery.interpretedResultsSortState);
const sortedResults = Object.fromEntries(
Object.entries(completedQuery.sortedResultsInfo).map(([key, value]) => {
return [key, mapSortedResultSetInfoToDomainModel(value)];
}),
);
return new CompletedQueryInfo(
mapQueryEvaluationInfoToDomainModel(completedQuery.query),
{
runId: completedQuery.result.runId,
queryId: completedQuery.result.queryId,
resultType: completedQuery.result.resultType,
evaluationTime: completedQuery.result.evaluationTime,
message: completedQuery.result.message,
logFileLocation: completedQuery.result.logFileLocation,
},
completedQuery.logFileLocation,
completedQuery.successful ?? completedQuery.sucessful,
completedQuery.message,
sortState,
completedQuery.resultCount,
sortedResults,
);
}
function mapInitialQueryInfoToDomainModel(
initialInfo: InitialQueryInfoDto,
): InitialQueryInfo {
return {
userSpecifiedLabel: initialInfo.userSpecifiedLabel,
queryText: initialInfo.queryText,
isQuickQuery: initialInfo.isQuickQuery,
isQuickEval: initialInfo.isQuickEval,
quickEvalPosition: initialInfo.quickEvalPosition,
queryPath: initialInfo.queryPath,
databaseInfo: {
databaseUri: initialInfo.databaseInfo.databaseUri,
name: initialInfo.databaseInfo.name,
},
start: new Date(initialInfo.start),
id: initialInfo.id,
};
}
function mapQueryEvaluationInfoToDomainModel(
evaluationInfo: QueryEvaluationInfoDto,
): QueryEvaluationInfo {
return new QueryEvaluationInfo(
evaluationInfo.querySaveDir,
evaluationInfo.dbItemPath,
evaluationInfo.databaseHasMetadataFile,
evaluationInfo.quickEvalPosition,
evaluationInfo.metadata,
);
}
function mapSortDirectionToDomainModel(
sortDirection: SortDirectionDto,
): SortDirection {
switch (sortDirection) {
case SortDirectionDto.asc:
return SortDirection.asc;
case SortDirectionDto.desc:
return SortDirection.desc;
}
}
function mapSortStateToDomainModel(
sortState: InterpretedResultsSortStateDto,
): InterpretedResultsSortState {
return {
sortBy: sortState.sortBy,
sortDirection: mapSortDirectionToDomainModel(sortState.sortDirection),
};
}
function mapSortedResultSetInfoToDomainModel(
sortedResultSetInfo: SortedResultSetInfoDto,
): SortedResultSetInfo {
return {
resultsPath: sortedResultSetInfo.resultsPath,
sortState: mapRawResultsSortStateToDomainModel(
sortedResultSetInfo.sortState,
),
};
}
function mapRawResultsSortStateToDomainModel(
sortState: RawResultsSortStateDto,
): RawResultsSortState {
return {
columnIndex: sortState.columnIndex,
sortDirection: mapSortDirectionToDomainModel(sortState.sortDirection),
};
}

View File

@@ -0,0 +1,235 @@
import {
QueryHistoryVariantAnalysisDto,
QueryLanguageDto,
QueryStatusDto,
VariantAnalysisDto,
VariantAnalysisFailureReasonDto,
VariantAnalysisRepoStatusDto,
VariantAnalysisScannedRepositoryDto,
VariantAnalysisSkippedRepositoriesDto,
VariantAnalysisSkippedRepositoryDto,
VariantAnalysisSkippedRepositoryGroupDto,
VariantAnalysisStatusDto,
} from "./query-history-variant-analysis-dto";
import {
VariantAnalysis,
VariantAnalysisFailureReason,
VariantAnalysisRepoStatus,
VariantAnalysisScannedRepository,
VariantAnalysisSkippedRepositories,
VariantAnalysisSkippedRepository,
VariantAnalysisSkippedRepositoryGroup,
VariantAnalysisStatus,
} from "../../variant-analysis/shared/variant-analysis";
import { assertNever } from "../../pure/helpers-pure";
import { QueryLanguage } from "../../common/query-language";
import { QueryStatus } from "../../query-status";
import { VariantAnalysisHistoryItem } from "../variant-analysis-history-item";
export function mapQueryHistoryVariantAnalysisToDto(
item: VariantAnalysisHistoryItem,
): QueryHistoryVariantAnalysisDto {
return {
t: "variant-analysis",
failureReason: item.failureReason,
resultCount: item.resultCount,
status: mapQueryStatusToDto(item.status),
completed: item.completed,
variantAnalysis: mapVariantAnalysisDtoToDto(item.variantAnalysis),
userSpecifiedLabel: item.userSpecifiedLabel,
};
}
function mapVariantAnalysisDtoToDto(
variantAnalysis: VariantAnalysis,
): VariantAnalysisDto {
return {
id: variantAnalysis.id,
controllerRepo: {
id: variantAnalysis.controllerRepo.id,
fullName: variantAnalysis.controllerRepo.fullName,
private: variantAnalysis.controllerRepo.private,
},
query: {
name: variantAnalysis.query.name,
filePath: variantAnalysis.query.filePath,
language: mapQueryLanguageToDto(variantAnalysis.query.language),
text: variantAnalysis.query.text,
},
databases: {
repositories: variantAnalysis.databases.repositories,
repositoryLists: variantAnalysis.databases.repositoryLists,
repositoryOwners: variantAnalysis.databases.repositoryOwners,
},
createdAt: variantAnalysis.createdAt,
updatedAt: variantAnalysis.updatedAt,
executionStartTime: variantAnalysis.executionStartTime,
status: mapVariantAnalysisStatusToDto(variantAnalysis.status),
completedAt: variantAnalysis.completedAt,
actionsWorkflowRunId: variantAnalysis.actionsWorkflowRunId,
failureReason:
variantAnalysis.failureReason &&
mapVariantAnalysisFailureReasonToDto(variantAnalysis.failureReason),
scannedRepos:
variantAnalysis.scannedRepos &&
mapVariantAnalysisScannedRepositoriesToDto(variantAnalysis.scannedRepos),
skippedRepos:
variantAnalysis.skippedRepos &&
mapVariantAnalysisSkippedRepositoriesToDto(variantAnalysis.skippedRepos),
};
}
function mapVariantAnalysisScannedRepositoriesToDto(
repos: VariantAnalysisScannedRepository[],
): VariantAnalysisScannedRepositoryDto[] {
return repos.map(mapVariantAnalysisScannedRepositoryToDto);
}
function mapVariantAnalysisScannedRepositoryToDto(
repo: VariantAnalysisScannedRepository,
): VariantAnalysisScannedRepositoryDto {
return {
repository: {
id: repo.repository.id,
fullName: repo.repository.fullName,
private: repo.repository.private,
stargazersCount: repo.repository.stargazersCount,
updatedAt: repo.repository.updatedAt,
},
analysisStatus: mapVariantAnalysisRepoStatusToDto(repo.analysisStatus),
resultCount: repo.resultCount,
artifactSizeInBytes: repo.artifactSizeInBytes,
failureMessage: repo.failureMessage,
};
}
function mapVariantAnalysisSkippedRepositoriesToDto(
repos: VariantAnalysisSkippedRepositories,
): VariantAnalysisSkippedRepositoriesDto {
return {
accessMismatchRepos:
repos.accessMismatchRepos &&
mapVariantAnalysisSkippedRepositoryGroupToDto(repos.accessMismatchRepos),
notFoundRepos:
repos.notFoundRepos &&
mapVariantAnalysisSkippedRepositoryGroupToDto(repos.notFoundRepos),
noCodeqlDbRepos:
repos.noCodeqlDbRepos &&
mapVariantAnalysisSkippedRepositoryGroupToDto(repos.noCodeqlDbRepos),
overLimitRepos:
repos.overLimitRepos &&
mapVariantAnalysisSkippedRepositoryGroupToDto(repos.overLimitRepos),
};
}
function mapVariantAnalysisSkippedRepositoryGroupToDto(
repoGroup: VariantAnalysisSkippedRepositoryGroup,
): VariantAnalysisSkippedRepositoryGroupDto {
return {
repositoryCount: repoGroup.repositoryCount,
repositories: repoGroup.repositories.map(
mapVariantAnalysisSkippedRepositoryToDto,
),
};
}
function mapVariantAnalysisSkippedRepositoryToDto(
repo: VariantAnalysisSkippedRepository,
): VariantAnalysisSkippedRepositoryDto {
return {
id: repo.id,
fullName: repo.fullName,
private: repo.private,
stargazersCount: repo.stargazersCount,
updatedAt: repo.updatedAt,
};
}
function mapVariantAnalysisFailureReasonToDto(
failureReason: VariantAnalysisFailureReason,
): VariantAnalysisFailureReasonDto {
switch (failureReason) {
case VariantAnalysisFailureReason.NoReposQueried:
return VariantAnalysisFailureReasonDto.NoReposQueried;
case VariantAnalysisFailureReason.ActionsWorkflowRunFailed:
return VariantAnalysisFailureReasonDto.ActionsWorkflowRunFailed;
case VariantAnalysisFailureReason.InternalError:
return VariantAnalysisFailureReasonDto.InternalError;
default:
assertNever(failureReason);
}
}
function mapVariantAnalysisRepoStatusToDto(
status: VariantAnalysisRepoStatus,
): VariantAnalysisRepoStatusDto {
switch (status) {
case VariantAnalysisRepoStatus.Pending:
return VariantAnalysisRepoStatusDto.Pending;
case VariantAnalysisRepoStatus.InProgress:
return VariantAnalysisRepoStatusDto.InProgress;
case VariantAnalysisRepoStatus.Succeeded:
return VariantAnalysisRepoStatusDto.Succeeded;
case VariantAnalysisRepoStatus.Failed:
return VariantAnalysisRepoStatusDto.Failed;
case VariantAnalysisRepoStatus.Canceled:
return VariantAnalysisRepoStatusDto.Canceled;
case VariantAnalysisRepoStatus.TimedOut:
return VariantAnalysisRepoStatusDto.TimedOut;
default:
assertNever(status);
}
}
function mapVariantAnalysisStatusToDto(
status: VariantAnalysisStatus,
): VariantAnalysisStatusDto {
switch (status) {
case VariantAnalysisStatus.InProgress:
return VariantAnalysisStatusDto.InProgress;
case VariantAnalysisStatus.Succeeded:
return VariantAnalysisStatusDto.Succeeded;
case VariantAnalysisStatus.Failed:
return VariantAnalysisStatusDto.Failed;
case VariantAnalysisStatus.Canceled:
return VariantAnalysisStatusDto.Canceled;
default:
assertNever(status);
}
}
function mapQueryLanguageToDto(language: QueryLanguage): QueryLanguageDto {
switch (language) {
case QueryLanguage.CSharp:
return QueryLanguageDto.CSharp;
case QueryLanguage.Cpp:
return QueryLanguageDto.Cpp;
case QueryLanguage.Go:
return QueryLanguageDto.Go;
case QueryLanguage.Java:
return QueryLanguageDto.Java;
case QueryLanguage.Javascript:
return QueryLanguageDto.Javascript;
case QueryLanguage.Python:
return QueryLanguageDto.Python;
case QueryLanguage.Ruby:
return QueryLanguageDto.Ruby;
case QueryLanguage.Swift:
return QueryLanguageDto.Swift;
default:
assertNever(language);
}
}
function mapQueryStatusToDto(status: QueryStatus): QueryStatusDto {
switch (status) {
case QueryStatus.InProgress:
return QueryStatusDto.InProgress;
case QueryStatus.Completed:
return QueryStatusDto.Completed;
case QueryStatus.Failed:
return QueryStatusDto.Failed;
default:
assertNever(status);
}
}

View File

@@ -0,0 +1,253 @@
import {
QueryHistoryVariantAnalysisDto,
QueryLanguageDto,
QueryStatusDto,
VariantAnalysisDto,
VariantAnalysisFailureReasonDto,
VariantAnalysisRepoStatusDto,
VariantAnalysisScannedRepositoryDto,
VariantAnalysisSkippedRepositoriesDto,
VariantAnalysisSkippedRepositoryDto,
VariantAnalysisSkippedRepositoryGroupDto,
VariantAnalysisStatusDto,
} from "./query-history-variant-analysis-dto";
import {
VariantAnalysis,
VariantAnalysisFailureReason,
VariantAnalysisRepoStatus,
VariantAnalysisScannedRepository,
VariantAnalysisSkippedRepositories,
VariantAnalysisSkippedRepository,
VariantAnalysisSkippedRepositoryGroup,
VariantAnalysisStatus,
} from "../../variant-analysis/shared/variant-analysis";
import { assertNever } from "../../pure/helpers-pure";
import { QueryLanguage } from "../../common/query-language";
import { QueryStatus } from "../../query-status";
import { VariantAnalysisHistoryItem } from "../variant-analysis-history-item";
export function mapQueryHistoryVariantAnalysisToDomainModel(
item: QueryHistoryVariantAnalysisDto,
): VariantAnalysisHistoryItem {
return {
t: "variant-analysis",
failureReason: item.failureReason,
resultCount: item.resultCount,
status: mapQueryStatusToDomainModel(item.status),
completed: item.completed,
variantAnalysis: mapVariantAnalysisToDomainModel(item.variantAnalysis),
userSpecifiedLabel: item.userSpecifiedLabel,
};
}
function mapVariantAnalysisToDomainModel(
variantAnalysis: VariantAnalysisDto,
): VariantAnalysis {
return {
id: variantAnalysis.id,
controllerRepo: {
id: variantAnalysis.controllerRepo.id,
fullName: variantAnalysis.controllerRepo.fullName,
private: variantAnalysis.controllerRepo.private,
},
query: {
name: variantAnalysis.query.name,
filePath: variantAnalysis.query.filePath,
language: mapQueryLanguageToDomainModel(variantAnalysis.query.language),
text: variantAnalysis.query.text,
},
databases: {
repositories: variantAnalysis.databases.repositories,
repositoryLists: variantAnalysis.databases.repositoryLists,
repositoryOwners: variantAnalysis.databases.repositoryOwners,
},
createdAt: variantAnalysis.createdAt,
updatedAt: variantAnalysis.updatedAt,
executionStartTime: variantAnalysis.executionStartTime,
status: mapVariantAnalysisStatusToDomainModel(variantAnalysis.status),
completedAt: variantAnalysis.completedAt,
actionsWorkflowRunId: variantAnalysis.actionsWorkflowRunId,
failureReason:
variantAnalysis.failureReason &&
mapVariantAnalysisFailureReasonToDomainModel(
variantAnalysis.failureReason,
),
scannedRepos:
variantAnalysis.scannedRepos &&
mapVariantAnalysisScannedRepositoriesToDomainModel(
variantAnalysis.scannedRepos,
),
skippedRepos:
variantAnalysis.skippedRepos &&
mapVariantAnalysisSkippedRepositoriesToDomainModel(
variantAnalysis.skippedRepos,
),
};
}
function mapVariantAnalysisScannedRepositoriesToDomainModel(
repos: VariantAnalysisScannedRepositoryDto[],
): VariantAnalysisScannedRepository[] {
return repos.map(mapVariantAnalysisScannedRepositoryToDomainModel);
}
function mapVariantAnalysisScannedRepositoryToDomainModel(
repo: VariantAnalysisScannedRepositoryDto,
): VariantAnalysisScannedRepository {
return {
repository: {
id: repo.repository.id,
fullName: repo.repository.fullName,
private: repo.repository.private,
stargazersCount: repo.repository.stargazersCount,
updatedAt: repo.repository.updatedAt,
},
analysisStatus: mapVariantAnalysisRepoStatusToDomainModel(
repo.analysisStatus,
),
resultCount: repo.resultCount,
artifactSizeInBytes: repo.artifactSizeInBytes,
failureMessage: repo.failureMessage,
};
}
function mapVariantAnalysisSkippedRepositoriesToDomainModel(
repos: VariantAnalysisSkippedRepositoriesDto,
): VariantAnalysisSkippedRepositories {
return {
accessMismatchRepos:
repos.accessMismatchRepos &&
mapVariantAnalysisSkippedRepositoryGroupToDomainModel(
repos.accessMismatchRepos,
),
notFoundRepos:
repos.notFoundRepos &&
mapVariantAnalysisSkippedRepositoryGroupToDomainModel(
repos.notFoundRepos,
),
noCodeqlDbRepos:
repos.noCodeqlDbRepos &&
mapVariantAnalysisSkippedRepositoryGroupToDomainModel(
repos.noCodeqlDbRepos,
),
overLimitRepos:
repos.overLimitRepos &&
mapVariantAnalysisSkippedRepositoryGroupToDomainModel(
repos.overLimitRepos,
),
};
}
function mapVariantAnalysisSkippedRepositoryGroupToDomainModel(
repoGroup: VariantAnalysisSkippedRepositoryGroupDto,
): VariantAnalysisSkippedRepositoryGroup {
return {
repositoryCount: repoGroup.repositoryCount,
repositories: repoGroup.repositories.map(
mapVariantAnalysisSkippedRepositoryToDomainModel,
),
};
}
function mapVariantAnalysisSkippedRepositoryToDomainModel(
repo: VariantAnalysisSkippedRepositoryDto,
): VariantAnalysisSkippedRepository {
return {
id: repo.id,
fullName: repo.fullName,
private: repo.private,
stargazersCount: repo.stargazersCount,
updatedAt: repo.updatedAt,
};
}
function mapVariantAnalysisFailureReasonToDomainModel(
failureReason: VariantAnalysisFailureReasonDto,
): VariantAnalysisFailureReason {
switch (failureReason) {
case VariantAnalysisFailureReasonDto.NoReposQueried:
return VariantAnalysisFailureReason.NoReposQueried;
case VariantAnalysisFailureReasonDto.ActionsWorkflowRunFailed:
return VariantAnalysisFailureReason.ActionsWorkflowRunFailed;
case VariantAnalysisFailureReasonDto.InternalError:
return VariantAnalysisFailureReason.InternalError;
default:
assertNever(failureReason);
}
}
function mapVariantAnalysisRepoStatusToDomainModel(
status: VariantAnalysisRepoStatusDto,
): VariantAnalysisRepoStatus {
switch (status) {
case VariantAnalysisRepoStatusDto.Pending:
return VariantAnalysisRepoStatus.Pending;
case VariantAnalysisRepoStatusDto.InProgress:
return VariantAnalysisRepoStatus.InProgress;
case VariantAnalysisRepoStatusDto.Succeeded:
return VariantAnalysisRepoStatus.Succeeded;
case VariantAnalysisRepoStatusDto.Failed:
return VariantAnalysisRepoStatus.Failed;
case VariantAnalysisRepoStatusDto.Canceled:
return VariantAnalysisRepoStatus.Canceled;
case VariantAnalysisRepoStatusDto.TimedOut:
return VariantAnalysisRepoStatus.TimedOut;
default:
assertNever(status);
}
}
function mapVariantAnalysisStatusToDomainModel(
status: VariantAnalysisStatusDto,
): VariantAnalysisStatus {
switch (status) {
case VariantAnalysisStatusDto.InProgress:
return VariantAnalysisStatus.InProgress;
case VariantAnalysisStatusDto.Succeeded:
return VariantAnalysisStatus.Succeeded;
case VariantAnalysisStatusDto.Failed:
return VariantAnalysisStatus.Failed;
case VariantAnalysisStatusDto.Canceled:
return VariantAnalysisStatus.Canceled;
default:
assertNever(status);
}
}
function mapQueryLanguageToDomainModel(
language: QueryLanguageDto,
): QueryLanguage {
switch (language) {
case QueryLanguageDto.CSharp:
return QueryLanguage.CSharp;
case QueryLanguageDto.Cpp:
return QueryLanguage.Cpp;
case QueryLanguageDto.Go:
return QueryLanguage.Go;
case QueryLanguageDto.Java:
return QueryLanguage.Java;
case QueryLanguageDto.Javascript:
return QueryLanguage.Javascript;
case QueryLanguageDto.Python:
return QueryLanguage.Python;
case QueryLanguageDto.Ruby:
return QueryLanguage.Ruby;
case QueryLanguageDto.Swift:
return QueryLanguage.Swift;
default:
assertNever(language);
}
}
function mapQueryStatusToDomainModel(status: QueryStatusDto): QueryStatus {
switch (status) {
case QueryStatusDto.InProgress:
return QueryStatus.InProgress;
case QueryStatusDto.Completed:
return QueryStatus.Completed;
case QueryStatusDto.Failed:
return QueryStatus.Failed;
default:
assertNever(status);
}
}

View File

@@ -1,27 +1,17 @@
// Contains models and consts for the data we want to store in the query history store.
// Changes to these models should be done carefully and account for backwards compatibility of data.
import { QueryLanguage } from "../../common/query-language";
import { QueryStatus } from "../../query-status";
import {
VariantAnalysisFailureReason,
VariantAnalysisRepoStatus,
VariantAnalysisStatus,
} from "../../variant-analysis/shared/variant-analysis";
// All data points are modelled, except enums.
export interface QueryHistoryVariantAnalysisDto {
readonly t: "variant-analysis";
failureReason?: string;
resultCount?: number;
status: QueryStatus;
status: QueryStatusDto;
completed: boolean;
variantAnalysis: VariantAnalysisQueryHistoryDto;
variantAnalysis: VariantAnalysisDto;
userSpecifiedLabel?: string;
}
export interface VariantAnalysisQueryHistoryDto {
export interface VariantAnalysisDto {
id: number;
controllerRepo: {
id: number;
@@ -31,7 +21,7 @@ export interface VariantAnalysisQueryHistoryDto {
query: {
name: string;
filePath: string;
language: QueryLanguage;
language: QueryLanguageDto;
text: string;
};
databases: {
@@ -42,10 +32,10 @@ export interface VariantAnalysisQueryHistoryDto {
createdAt: string;
updatedAt: string;
executionStartTime: number;
status: VariantAnalysisStatus;
status: VariantAnalysisStatusDto;
completedAt?: string;
actionsWorkflowRunId?: number;
failureReason?: VariantAnalysisFailureReason;
failureReason?: VariantAnalysisFailureReasonDto;
scannedRepos?: VariantAnalysisScannedRepositoryDto[];
skippedRepos?: VariantAnalysisSkippedRepositoriesDto;
}
@@ -58,7 +48,7 @@ export interface VariantAnalysisScannedRepositoryDto {
stargazersCount: number;
updatedAt: string | null;
};
analysisStatus: VariantAnalysisRepoStatus;
analysisStatus: VariantAnalysisRepoStatusDto;
resultCount?: number;
artifactSizeInBytes?: number;
failureMessage?: string;
@@ -83,3 +73,42 @@ export interface VariantAnalysisSkippedRepositoryDto {
stargazersCount?: number;
updatedAt?: string | null;
}
export enum VariantAnalysisFailureReasonDto {
NoReposQueried = "noReposQueried",
ActionsWorkflowRunFailed = "actionsWorkflowRunFailed",
InternalError = "internalError",
}
export enum VariantAnalysisRepoStatusDto {
Pending = "pending",
InProgress = "inProgress",
Succeeded = "succeeded",
Failed = "failed",
Canceled = "canceled",
TimedOut = "timedOut",
}
export enum VariantAnalysisStatusDto {
InProgress = "inProgress",
Succeeded = "succeeded",
Failed = "failed",
Canceled = "canceled",
}
export enum QueryLanguageDto {
CSharp = "csharp",
Cpp = "cpp",
Go = "go",
Java = "java",
Javascript = "javascript",
Python = "python",
Ruby = "ruby",
Swift = "swift",
}
export enum QueryStatusDto {
InProgress = "InProgress",
Completed = "Completed",
Failed = "Failed",
}

View File

@@ -1,6 +1,6 @@
import { ensureFile } from "fs-extra";
import { DisposableObject } from "../pure/disposable-object";
import { DisposableObject, DisposeHandler } from "../pure/disposable-object";
import { CancellationToken } from "vscode";
import { createMessageConnection, RequestType } from "vscode-jsonrpc/node";
import * as cli from "../cli";
@@ -224,4 +224,10 @@ export class QueryServerClient extends DisposableObject {
delete this.progressCallbacks[id];
}
}
public dispose(disposeHandler?: DisposeHandler | undefined): void {
this.progressCallbacks = {};
this.stopQueryServer();
super.dispose(disposeHandler);
}
}

View File

@@ -1,15 +1,20 @@
import { join, dirname } from "path";
import { join } from "path";
import { CancellationToken, Uri, workspace, window as Window } from "vscode";
import { CodeQLCliServer } from "./cli";
import { OutputChannelLogger } from "./common";
import { Credentials } from "./common/authentication";
import { QueryLanguage } from "./common/query-language";
import { askForLanguage, isFolderAlreadyInWorkspace } from "./helpers";
import {
askForLanguage,
getFirstWorkspaceFolder,
isFolderAlreadyInWorkspace,
} from "./helpers";
import { getErrorMessage } from "./pure/helpers-pure";
import { QlPackGenerator } from "./qlpack-generator";
import { DatabaseItem, DatabaseManager } from "./local-databases";
import { ProgressCallback, UserCancellationException } from "./progress";
import { askForGitHubRepo, downloadGitHubDatabase } from "./databaseFetcher";
import { existsSync } from "fs";
type QueryLanguagesToDatabaseMap = Record<string, string>;
@@ -50,11 +55,11 @@ export class SkeletonQueryWizard {
return;
}
this.qlPackStoragePath = this.getFirstStoragePath();
this.qlPackStoragePath = getFirstWorkspaceFolder();
const skeletonPackAlreadyExists = isFolderAlreadyInWorkspace(
this.folderName,
);
const skeletonPackAlreadyExists =
existsSync(join(this.qlPackStoragePath, this.folderName)) ||
isFolderAlreadyInWorkspace(this.folderName);
if (skeletonPackAlreadyExists) {
// just create a new example query file in skeleton QL pack
@@ -93,27 +98,6 @@ export class SkeletonQueryWizard {
});
}
public getFirstStoragePath() {
const workspaceFolders = workspace.workspaceFolders;
if (!workspaceFolders || workspaceFolders.length === 0) {
throw new Error("No workspace folders found");
}
const firstFolder = workspaceFolders[0];
const firstFolderFsPath = firstFolder.uri.fsPath;
// For the vscode-codeql-starter repo, the first folder will be a ql pack
// so we need to get the parent folder
if (firstFolderFsPath.includes("codeql-custom-queries")) {
// return the parent folder
return dirname(firstFolderFsPath);
} else {
// if the first folder is not a ql pack, then we are in a normal workspace
return firstFolderFsPath;
}
}
private async chooseLanguage() {
this.progress({
message: "Choose language",

View File

@@ -16,7 +16,7 @@ import { ModeledMethod } from "../../data-extensions-editor/modeled-method";
import { MethodRow } from "./MethodRow";
import { assertNever } from "../../pure/helpers-pure";
import { vscode } from "../vscode-api";
import { calculateSupportedPercentage } from "./supported";
import { calculateModeledPercentage } from "./modeled";
export const DataExtensionsEditorContainer = styled.div`
margin-top: 1rem;
@@ -97,12 +97,12 @@ export function DataExtensionsEditor({
};
}, []);
const supportedPercentage = useMemo(
() => calculateSupportedPercentage(externalApiUsages),
const modeledPercentage = useMemo(
() => calculateModeledPercentage(externalApiUsages),
[externalApiUsages],
);
const unsupportedPercentage = 100 - supportedPercentage;
const unModeledPercentage = 100 - modeledPercentage;
const onChange = useCallback(
(method: ExternalApiUsage, model: ModeledMethod) => {
@@ -140,10 +140,10 @@ export function DataExtensionsEditor({
{externalApiUsages.length > 0 && (
<>
<div>
<h3>External API support stats</h3>
<h3>External API model stats</h3>
<ul>
<li>Supported: {supportedPercentage.toFixed(2)}%</li>
<li>Unsupported: {unsupportedPercentage.toFixed(2)}%</li>
<li>Modeled: {modeledPercentage.toFixed(2)}%</li>
<li>Unmodeled: {unModeledPercentage.toFixed(2)}%</li>
</ul>
</div>
<div>

View File

@@ -0,0 +1,48 @@
import * as React from "react";
import { useCallback, useEffect } from "react";
import styled from "styled-components";
import { VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react";
import type { ModeledMethod } from "../../data-extensions-editor/modeled-method";
const Dropdown = styled(VSCodeDropdown)`
width: 100%;
`;
type Props = {
kinds: Array<ModeledMethod["kind"]>;
value: ModeledMethod["kind"] | undefined;
onChange: (value: ModeledMethod["kind"]) => void;
};
export const KindInput = ({ kinds, value, onChange }: Props) => {
const handleInput = useCallback(
(e: InputEvent) => {
const target = e.target as HTMLSelectElement;
onChange(target.value as ModeledMethod["kind"]);
},
[onChange],
);
useEffect(() => {
if (value === undefined && kinds.length > 0) {
onChange(kinds[0]);
}
if (value !== undefined && !kinds.includes(value)) {
onChange(kinds[0]);
}
}, [value, kinds, onChange]);
return (
<Dropdown value={value} onInput={handleInput}>
{kinds.map((kind) => (
<VSCodeOption key={kind} value={kind}>
{kind}
</VSCodeOption>
))}
</Dropdown>
);
};

View File

@@ -3,7 +3,6 @@ import {
VSCodeDataGridRow,
VSCodeDropdown,
VSCodeOption,
VSCodeTextField,
} from "@vscode/webview-ui-toolkit/react";
import * as React from "react";
import { useCallback, useMemo } from "react";
@@ -15,15 +14,13 @@ import {
ModeledMethod,
ModeledMethodType,
} from "../../data-extensions-editor/modeled-method";
import { KindInput } from "./KindInput";
import { extensiblePredicateDefinitions } from "../../data-extensions-editor/predicates";
const Dropdown = styled(VSCodeDropdown)`
width: 100%;
`;
const TextField = styled(VSCodeTextField)`
width: 100%;
`;
type SupportedUnsupportedSpanProps = {
supported: boolean;
};
@@ -107,17 +104,15 @@ export const MethodRow = ({
},
[onChange, externalApiUsage, modeledMethod],
);
const handleKindInput = useCallback(
(e: InputEvent) => {
const handleKindChange = useCallback(
(kind: string) => {
if (!modeledMethod) {
return;
}
const target = e.target as HTMLSelectElement;
onChange(externalApiUsage, {
...modeledMethod,
kind: target.value as ModeledMethod["kind"],
kind,
});
},
[onChange, externalApiUsage, modeledMethod],
@@ -130,6 +125,11 @@ export const MethodRow = ({
});
}, [externalApiUsage]);
const predicate =
modeledMethod?.type && modeledMethod.type !== "none"
? extensiblePredicateDefinitions[modeledMethod.type]
: undefined;
return (
<VSCodeDataGridRow>
<VSCodeDataGridCell gridColumn={1}>
@@ -155,7 +155,7 @@ export const MethodRow = ({
value={modeledMethod?.type ?? "none"}
onInput={handleTypeInput}
>
<VSCodeOption value="none">Unmodelled</VSCodeOption>
<VSCodeOption value="none">Unmodeled</VSCodeOption>
<VSCodeOption value="source">Source</VSCodeOption>
<VSCodeOption value="sink">Sink</VSCodeOption>
<VSCodeOption value="summary">Flow summary</VSCodeOption>
@@ -195,10 +195,13 @@ export const MethodRow = ({
)}
</VSCodeDataGridCell>
<VSCodeDataGridCell gridColumn={7}>
{modeledMethod?.type &&
["source", "sink", "summary"].includes(modeledMethod?.type) && (
<TextField value={modeledMethod?.kind} onInput={handleKindInput} />
)}
{predicate?.supportedKinds && (
<KindInput
kinds={predicate.supportedKinds}
value={modeledMethod?.kind}
onChange={handleKindChange}
/>
)}
</VSCodeDataGridCell>
</VSCodeDataGridRow>
);

View File

@@ -1,13 +1,13 @@
import { calculateSupportedPercentage } from "../supported";
import { calculateModeledPercentage } from "../modeled";
describe("calculateSupportedPercentage", () => {
describe("calculateModeledPercentage", () => {
it("when there are no external API usages", () => {
expect(calculateSupportedPercentage([])).toBe(0);
expect(calculateModeledPercentage([])).toBe(0);
});
it("when there are is 1 supported external API usage", () => {
it("when there are is 1 modeled external API usage", () => {
expect(
calculateSupportedPercentage([
calculateModeledPercentage([
{
supported: true,
},
@@ -15,9 +15,9 @@ describe("calculateSupportedPercentage", () => {
).toBe(100);
});
it("when there are is 1 unsupported external API usage", () => {
it("when there are is 1 unmodeled external API usage", () => {
expect(
calculateSupportedPercentage([
calculateModeledPercentage([
{
supported: false,
},
@@ -25,9 +25,9 @@ describe("calculateSupportedPercentage", () => {
).toBe(0);
});
it("when there are multiple supporte and unsupported external API usage", () => {
it("when there are multiple modeled and unmodeled external API usage", () => {
expect(
calculateSupportedPercentage([
calculateModeledPercentage([
{
supported: false,
},

View File

@@ -0,0 +1,15 @@
import { ExternalApiUsage } from "../../data-extensions-editor/external-api-usage";
export function calculateModeledPercentage(
externalApiUsages: Array<Pick<ExternalApiUsage, "supported">>,
): number {
if (externalApiUsages.length === 0) {
return 0;
}
const modeledExternalApiUsages = externalApiUsages.filter((m) => m.supported);
const modeledRatio =
modeledExternalApiUsages.length / externalApiUsages.length;
return modeledRatio * 100;
}

View File

@@ -1,17 +0,0 @@
import { ExternalApiUsage } from "../../data-extensions-editor/external-api-usage";
export function calculateSupportedPercentage(
externalApiUsages: Array<Pick<ExternalApiUsage, "supported">>,
): number {
if (externalApiUsages.length === 0) {
return 0;
}
const supportedExternalApiUsages = externalApiUsages.filter(
(m) => m.supported,
);
const supportedRatio =
supportedExternalApiUsages.length / externalApiUsages.length;
return supportedRatio * 100;
}

View File

@@ -6,6 +6,7 @@ import {
describe("createDataExtensionYaml", () => {
it("creates the correct YAML file", () => {
const yaml = createDataExtensionYaml(
"java",
[
{
signature: "org.sql2o.Connection#createQuery(String)",
@@ -99,6 +100,32 @@ describe("createDataExtensionYaml", () => {
pack: codeql/java-all
extensible: neutralModel
data: []
`);
});
it("includes the correct language", () => {
const yaml = createDataExtensionYaml("csharp", [], {});
expect(yaml).toEqual(`extensions:
- addsTo:
pack: codeql/csharp-all
extensible: sourceModel
data: []
- addsTo:
pack: codeql/csharp-all
extensible: sinkModel
data: []
- addsTo:
pack: codeql/csharp-all
extensible: summaryModel
data: []
- addsTo:
pack: codeql/csharp-all
extensible: neutralModel
data: []
`);
});
});

View File

@@ -53,7 +53,7 @@ describe("Db panel UI commands", () => {
it.skip("should add new local db list", async () => {
// Add db list
jest.spyOn(window, "showQuickPick").mockResolvedValue({
kind: DbListKind.Local,
databaseKind: DbListKind.Local,
} as AddListQuickPickItem);
jest.spyOn(window, "showInputBox").mockResolvedValue("my-list-1");
await commandManager.execute(
@@ -73,7 +73,7 @@ describe("Db panel UI commands", () => {
it("should add new remote repository", async () => {
// Add db
jest.spyOn(window, "showQuickPick").mockResolvedValue({
kind: "repo",
remoteDatabaseKind: "repo",
} as RemoteDatabaseQuickPickItem);
jest.spyOn(window, "showInputBox").mockResolvedValue("owner1/repo1");
@@ -96,7 +96,7 @@ describe("Db panel UI commands", () => {
it("should add new remote owner", async () => {
// Add owner
jest.spyOn(window, "showQuickPick").mockResolvedValue({
kind: "owner",
remoteDatabaseKind: "owner",
} as RemoteDatabaseQuickPickItem);
jest.spyOn(window, "showInputBox").mockResolvedValue("owner1");

View File

@@ -16,8 +16,6 @@ import {
} from "../global.helper";
import { createMockCommandManager } from "../../__mocks__/commandsMock";
jest.setTimeout(60_000);
/**
* Run various integration tests for databases
*/

View File

@@ -4,9 +4,6 @@ import { CodeQLCliServer } from "../../../src/cli";
import { tryGetQueryMetadata } from "../../../src/helpers";
import { getActivatedExtension } from "../global.helper";
// up to 3 minutes per test
jest.setTimeout(3 * 60 * 1000);
describe("helpers (with CLI)", () => {
const baseDir = __dirname;

View File

@@ -6,6 +6,9 @@ const config: Config = {
...baseConfig,
runner: "<rootDir>/../jest-runner-installed-extensions.ts",
setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"],
// CLI integration tests call into the CLI and execute queries, so these are expected to take a lot longer
// than the default 5 seconds.
testTimeout: 180_000, // 3 minutes
};
export default config;

View File

@@ -98,8 +98,6 @@ const db: messages.Dataset = {
workingSet: "default",
};
jest.setTimeout(60_000);
describeWithCodeQL()("using the legacy query server", () => {
const nullProgressReporter: ProgressReporter = {
report: () => {

View File

@@ -104,8 +104,6 @@ const nullProgressReporter: ProgressReporter = {
},
};
jest.setTimeout(20_000);
describeWithCodeQL()("using the new query server", () => {
let qs: qsClient.QueryServerClient;
let cliServer: cli.CodeQLCliServer;

View File

@@ -12,9 +12,6 @@ import {
import { mockedQuickPickItem } from "../utils/mocking.helpers";
import { getActivatedExtension } from "../global.helper";
// up to 3 minutes per test
jest.setTimeout(3 * 60 * 1000);
describe("Packaging commands", () => {
let cli: CodeQLCliServer;
const progress = jest.fn();

View File

@@ -27,8 +27,6 @@ import { QueryResultType } from "../../../src/pure/new-messages";
import { createVSCodeCommandManager } from "../../../src/common/vscode/commands";
import { AllCommands, QueryServerCommands } from "../../../src/common/commands";
jest.setTimeout(20_000);
/**
* Integration tests for queries
*/

View File

@@ -14,8 +14,6 @@ import { KeyType } from "../../../src/contextual/keyType";
import { faker } from "@faker-js/faker";
import { getActivatedExtension } from "../global.helper";
jest.setTimeout(60_000);
/**
* Perform proper integration tests by running the CLI
*/

View File

@@ -22,8 +22,6 @@ import * as databaseFetcher from "../../../src/databaseFetcher";
import { createMockDB } from "../../factories/databases/databases";
import { asError } from "../../../src/pure/helpers-pure";
jest.setTimeout(80_000);
describe("SkeletonQueryWizard", () => {
let mockCli: CodeQLCliServer;
let wizard: SkeletonQueryWizard;
@@ -84,11 +82,11 @@ describe("SkeletonQueryWizard", () => {
jest.spyOn(workspace, "workspaceFolders", "get").mockReturnValue([
{
name: `codespaces-codeql`,
uri: { fsPath: storagePath },
uri: { fsPath: storagePath, scheme: "file" },
},
{
name: "/second/folder/path",
uri: { fsPath: storagePath },
uri: { fsPath: storagePath, scheme: "file" },
},
] as WorkspaceFolder[]);
@@ -304,66 +302,6 @@ describe("SkeletonQueryWizard", () => {
});
});
describe("getFirstStoragePath", () => {
it("should return the first workspace folder", async () => {
jest.spyOn(workspace, "workspaceFolders", "get").mockReturnValue([
{
name: "codespaces-codeql",
uri: { fsPath: "codespaces-codeql" },
},
] as WorkspaceFolder[]);
wizard = new SkeletonQueryWizard(
mockCli,
jest.fn(),
credentials,
extLogger,
mockDatabaseManager,
token,
storagePath,
);
expect(wizard.getFirstStoragePath()).toEqual("codespaces-codeql");
});
describe("if user is in vscode-codeql-starter workspace", () => {
it("should set storage path to parent folder", async () => {
jest.spyOn(workspace, "workspaceFolders", "get").mockReturnValue([
{
name: "codeql-custom-queries-cpp",
uri: {
fsPath: join(
"vscode-codeql-starter",
"codeql-custom-queries-cpp",
),
},
},
{
name: "codeql-custom-queries-csharp",
uri: {
fsPath: join(
"vscode-codeql-starter",
"codeql-custom-queries-csharp",
),
},
},
] as WorkspaceFolder[]);
wizard = new SkeletonQueryWizard(
mockCli,
jest.fn(),
credentials,
extLogger,
mockDatabaseManager,
token,
storagePath,
);
expect(wizard.getFirstStoragePath()).toEqual("vscode-codeql-starter");
});
});
});
describe("findDatabaseItemByNwo", () => {
describe("when the item exists", () => {
it("should return the database item", async () => {

View File

@@ -5,8 +5,6 @@ import { readFile, writeFile, ensureDir, copy } from "fs-extra";
import { createVSCodeCommandManager } from "../../../src/common/vscode/commands";
import { AllCommands } from "../../../src/common/commands";
jest.setTimeout(20_000);
/**
* Integration tests for queries
*/

View File

@@ -29,9 +29,6 @@ import { QueryLanguage } from "../../../../src/common/query-language";
import { readBundledPack } from "../../utils/bundled-pack-helpers";
import { load } from "js-yaml";
// up to 3 minutes per test
jest.setTimeout(3 * 60 * 1000);
describe("Variant Analysis Manager", () => {
let cli: CodeQLCliServer;
let cancellationTokenSource: CancellationTokenSource;

View File

@@ -15,8 +15,6 @@ import { getActivatedExtension } from "../../global.helper";
import { createVSCodeCommandManager } from "../../../../src/common/vscode/commands";
import { AllCommands } from "../../../../src/common/commands";
jest.setTimeout(30_000);
const mockServer = new MockGitHubApiServer();
beforeAll(() => mockServer.startServer());
afterEach(() => mockServer.unloadScenario());

View File

@@ -687,6 +687,22 @@ describe("local databases", () => {
);
});
});
describe("when the QL pack already exists", () => {
beforeEach(() => {
fs.mkdirSync(join(dir.name, `codeql-custom-queries-${language}`));
});
it("should exit early", async () => {
showBinaryChoiceDialogSpy = jest
.spyOn(helpers, "showBinaryChoiceDialog")
.mockResolvedValue(false);
await (databaseManager as any).createSkeletonPacks(mockDbItem);
expect(generateSpy).not.toBeCalled();
});
});
});
describe("openDatabase", () => {

View File

@@ -1,4 +1,8 @@
import { QuickPickItem, window } from "vscode";
import { CancellationTokenSource, QuickPickItem, window } from "vscode";
import { dump as dumpYaml, load as loadYaml } from "js-yaml";
import { outputFile, readFile } from "fs-extra";
import { join } from "path";
import { dir } from "tmp-promise";
import { pickExtensionPackModelFile } from "../../../../src/data-extensions-editor/extension-pack-picker";
import { QlpacksInfo, ResolveExtensionsResult } from "../../../../src/cli";
@@ -21,14 +25,33 @@ describe("pickExtensionPackModelFile", () => {
],
},
};
const databaseItem = {
name: "github/vscode-codeql",
language: "java",
};
const cancellationTokenSource = new CancellationTokenSource();
const token = cancellationTokenSource.token;
const progress = jest.fn();
let showQuickPickSpy: jest.SpiedFunction<typeof window.showQuickPick>;
let showInputBoxSpy: jest.SpiedFunction<typeof window.showInputBox>;
let showAndLogErrorMessageSpy: jest.SpiedFunction<
typeof helpers.showAndLogErrorMessage
>;
beforeEach(() => {
showQuickPickSpy = jest
.spyOn(window, "showQuickPick")
.mockRejectedValue(new Error("Unexpected call to showQuickPick"));
showInputBoxSpy = jest
.spyOn(window, "showInputBox")
.mockRejectedValue(new Error("Unexpected call to showInputBox"));
showAndLogErrorMessageSpy = jest
.spyOn(helpers, "showAndLogErrorMessage")
.mockImplementation((msg) => {
throw new Error(`Unexpected call to showAndLogErrorMessage: ${msg}`);
});
});
it("allows choosing an existing extension pack and model file", async () => {
@@ -43,9 +66,14 @@ describe("pickExtensionPackModelFile", () => {
file: "/a/b/c/my-extension-pack/models/model.yml",
} as QuickPickItem);
expect(await pickExtensionPackModelFile(cliServer, progress)).toEqual(
"/a/b/c/my-extension-pack/models/model.yml",
);
expect(
await pickExtensionPackModelFile(
cliServer,
databaseItem,
progress,
token,
),
).toEqual("/a/b/c/my-extension-pack/models/model.yml");
expect(showQuickPickSpy).toHaveBeenCalledTimes(2);
expect(showQuickPickSpy).toHaveBeenCalledWith(
[
@@ -57,10 +85,15 @@ describe("pickExtensionPackModelFile", () => {
label: "another-extension-pack",
extensionPack: "another-extension-pack",
},
{
label: expect.stringMatching(/create/i),
extensionPack: null,
},
],
{
title: expect.any(String),
},
token,
);
expect(showQuickPickSpy).toHaveBeenCalledWith(
[
@@ -68,10 +101,15 @@ describe("pickExtensionPackModelFile", () => {
label: "models/model.yml",
file: "/a/b/c/my-extension-pack/models/model.yml",
},
{
label: expect.stringMatching(/create/i),
file: null,
},
],
{
title: expect.any(String),
},
token,
);
expect(cliServer.resolveQlpacks).toHaveBeenCalledTimes(1);
expect(cliServer.resolveQlpacks).toHaveBeenCalledWith([], true);
@@ -82,39 +120,300 @@ describe("pickExtensionPackModelFile", () => {
);
});
it("allows choosing an existing extension pack and creating a new model file", async () => {
const tmpDir = await dir({
unsafeCleanup: true,
});
const cliServer = mockCliServer(
{
...qlPacks,
"my-extension-pack": [tmpDir.path],
},
{
models: extensions.models,
data: {
[tmpDir.path]: [
{
file: join(tmpDir.path, "models/model.yml"),
index: 0,
predicate: "sinkModel",
},
],
},
},
);
showQuickPickSpy.mockResolvedValueOnce({
label: "my-extension-pack",
extensionPack: "my-extension-pack",
} as QuickPickItem);
showQuickPickSpy.mockResolvedValueOnce({
label: "create",
file: null,
} as QuickPickItem);
showInputBoxSpy.mockResolvedValue("models/my-model.yml");
await outputFile(
join(tmpDir.path, "codeql-pack.yml"),
dumpYaml({
name: "my-extension-pack",
version: "0.0.0",
library: true,
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: ["models/**/*.yml"],
}),
);
expect(
await pickExtensionPackModelFile(
cliServer,
databaseItem,
progress,
token,
),
).toEqual(join(tmpDir.path, "models/my-model.yml"));
expect(showQuickPickSpy).toHaveBeenCalledTimes(2);
expect(showQuickPickSpy).toHaveBeenCalledWith(
[
{
label: "my-extension-pack",
extensionPack: "my-extension-pack",
},
{
label: "another-extension-pack",
extensionPack: "another-extension-pack",
},
{
label: expect.stringMatching(/create/i),
extensionPack: null,
},
],
{
title: expect.any(String),
},
token,
);
expect(showQuickPickSpy).toHaveBeenCalledWith(
[
{
label: "models/model.yml",
file: join(tmpDir.path, "models/model.yml"),
},
{
label: expect.stringMatching(/create/i),
file: null,
},
],
{
title: expect.any(String),
},
token,
);
expect(showInputBoxSpy).toHaveBeenCalledWith(
{
title: expect.any(String),
value: "models/github.vscode-codeql.model.yml",
validateInput: expect.any(Function),
},
token,
);
expect(cliServer.resolveQlpacks).toHaveBeenCalledTimes(1);
expect(cliServer.resolveQlpacks).toHaveBeenCalledWith([], true);
expect(cliServer.resolveExtensions).toHaveBeenCalledTimes(1);
expect(cliServer.resolveExtensions).toHaveBeenCalledWith(tmpDir.path, []);
});
it("allows cancelling the extension pack prompt", async () => {
const cliServer = mockCliServer(qlPacks, extensions);
showQuickPickSpy.mockResolvedValueOnce(undefined);
expect(await pickExtensionPackModelFile(cliServer, progress)).toEqual(
undefined,
);
expect(
await pickExtensionPackModelFile(
cliServer,
databaseItem,
progress,
token,
),
).toEqual(undefined);
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
expect(cliServer.resolveExtensions).not.toHaveBeenCalled();
});
it("does not show any options when there are no extension packs", async () => {
it("allows user to create an extension pack when there are no extension packs", async () => {
const cliServer = mockCliServer({}, { models: [], data: {} });
const tmpDir = await dir({
unsafeCleanup: true,
});
showQuickPickSpy.mockResolvedValueOnce({
label: "codeql-custom-queries-java",
path: tmpDir.path,
} as QuickPickItem);
showInputBoxSpy.mockResolvedValueOnce("my-extension-pack");
showInputBoxSpy.mockResolvedValue("models/my-model.yml");
expect(
await pickExtensionPackModelFile(
cliServer,
databaseItem,
progress,
token,
),
).toEqual(join(tmpDir.path, "my-extension-pack", "models", "my-model.yml"));
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
expect(showInputBoxSpy).toHaveBeenCalledTimes(2);
expect(showInputBoxSpy).toHaveBeenCalledWith(
{
title: expect.stringMatching(/extension pack/i),
prompt: expect.stringMatching(/extension pack/i),
placeHolder: expect.stringMatching(/github\/vscode-codeql-extensions/),
validateInput: expect.any(Function),
},
token,
);
expect(showInputBoxSpy).toHaveBeenCalledWith(
{
title: expect.stringMatching(/model file/),
value: "models/github.vscode-codeql.model.yml",
validateInput: expect.any(Function),
},
token,
);
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
expect(cliServer.resolveExtensions).toHaveBeenCalled();
expect(
loadYaml(
await readFile(
join(tmpDir.path, "my-extension-pack", "codeql-pack.yml"),
"utf8",
),
),
).toEqual({
name: "my-extension-pack",
version: "0.0.0",
library: true,
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: ["models/**/*.yml"],
});
});
it("allows user to create an extension pack when there are no extension packs with a different language", async () => {
const cliServer = mockCliServer({}, { models: [], data: {} });
const tmpDir = await dir({
unsafeCleanup: true,
});
showQuickPickSpy.mockResolvedValueOnce({
label: "codeql-custom-queries-java",
path: tmpDir.path,
} as QuickPickItem);
showInputBoxSpy.mockResolvedValueOnce("my-extension-pack");
showInputBoxSpy.mockResolvedValue("models/my-model.yml");
expect(
await pickExtensionPackModelFile(
cliServer,
{
...databaseItem,
language: "csharp",
},
progress,
token,
),
).toEqual(join(tmpDir.path, "my-extension-pack", "models", "my-model.yml"));
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
expect(showInputBoxSpy).toHaveBeenCalledTimes(2);
expect(showInputBoxSpy).toHaveBeenCalledWith(
{
title: expect.stringMatching(/extension pack/i),
prompt: expect.stringMatching(/extension pack/i),
placeHolder: expect.stringMatching(/github\/vscode-codeql-extensions/),
validateInput: expect.any(Function),
},
token,
);
expect(showInputBoxSpy).toHaveBeenCalledWith(
{
title: expect.stringMatching(/model file/),
value: "models/github.vscode-codeql.model.yml",
validateInput: expect.any(Function),
},
token,
);
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
expect(cliServer.resolveExtensions).toHaveBeenCalled();
expect(
loadYaml(
await readFile(
join(tmpDir.path, "my-extension-pack", "codeql-pack.yml"),
"utf8",
),
),
).toEqual({
name: "my-extension-pack",
version: "0.0.0",
library: true,
extensionTargets: {
"codeql/csharp-all": "*",
},
dataExtensions: ["models/**/*.yml"],
});
});
it("allows cancelling the workspace folder selection", async () => {
const cliServer = mockCliServer({}, { models: [], data: {} });
showQuickPickSpy.mockResolvedValueOnce(undefined);
expect(await pickExtensionPackModelFile(cliServer, progress)).toEqual(
undefined,
);
expect(
await pickExtensionPackModelFile(
cliServer,
databaseItem,
progress,
token,
),
).toEqual(undefined);
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
expect(showQuickPickSpy).toHaveBeenCalledWith([], {
title: expect.any(String),
});
expect(showInputBoxSpy).toHaveBeenCalledTimes(0);
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
expect(cliServer.resolveExtensions).not.toHaveBeenCalled();
});
it("allows cancelling the extension pack name input", async () => {
const cliServer = mockCliServer({}, { models: [], data: {} });
showQuickPickSpy.mockResolvedValueOnce({
label: "codeql-custom-queries-java",
path: "/a/b/c",
} as QuickPickItem);
showInputBoxSpy.mockResolvedValueOnce(undefined);
expect(
await pickExtensionPackModelFile(
cliServer,
databaseItem,
progress,
token,
),
).toEqual(undefined);
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
expect(showInputBoxSpy).toHaveBeenCalledTimes(1);
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
expect(cliServer.resolveExtensions).not.toHaveBeenCalled();
});
it("shows an error when an extension pack resolves to more than 1 location", async () => {
const showAndLogErrorMessageSpy = jest.spyOn(
helpers,
"showAndLogErrorMessage",
);
showAndLogErrorMessageSpy.mockResolvedValue(undefined);
const cliServer = mockCliServer(
{
@@ -131,9 +430,14 @@ describe("pickExtensionPackModelFile", () => {
extensionPack: "my-extension-pack",
} as QuickPickItem);
expect(await pickExtensionPackModelFile(cliServer, progress)).toEqual(
undefined,
);
expect(
await pickExtensionPackModelFile(
cliServer,
databaseItem,
progress,
token,
),
).toEqual(undefined);
expect(showAndLogErrorMessageSpy).toHaveBeenCalledTimes(1);
expect(showAndLogErrorMessageSpy).toHaveBeenCalledWith(
expect.stringMatching(/could not be resolved to a single location/),
@@ -153,47 +457,468 @@ describe("pickExtensionPackModelFile", () => {
} as QuickPickItem);
showQuickPickSpy.mockResolvedValueOnce(undefined);
expect(await pickExtensionPackModelFile(cliServer, progress)).toEqual(
undefined,
);
expect(
await pickExtensionPackModelFile(
cliServer,
databaseItem,
progress,
token,
),
).toEqual(undefined);
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
expect(cliServer.resolveExtensions).toHaveBeenCalled();
});
it("does not show any options when there are no model files", async () => {
const cliServer = mockCliServer(qlPacks, { models: [], data: {} });
it("shows create input box when there are no model files", async () => {
const tmpDir = await dir({
unsafeCleanup: true,
});
const cliServer = mockCliServer(
{
"my-extension-pack": [tmpDir.path],
},
{ models: [], data: {} },
);
await outputFile(
join(tmpDir.path, "codeql-pack.yml"),
dumpYaml({
name: "my-extension-pack",
version: "0.0.0",
library: true,
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: ["models/**/*.yml"],
}),
);
showQuickPickSpy.mockResolvedValueOnce({
label: "my-extension-pack",
extensionPack: "my-extension-pack",
} as QuickPickItem);
showQuickPickSpy.mockResolvedValueOnce(undefined);
showInputBoxSpy.mockResolvedValue("models/my-model.yml");
expect(await pickExtensionPackModelFile(cliServer, progress)).toEqual(
undefined,
);
expect(showQuickPickSpy).toHaveBeenCalledTimes(2);
expect(showQuickPickSpy).toHaveBeenCalledWith(
[
{
label: "my-extension-pack",
extensionPack: "my-extension-pack",
},
{
label: "another-extension-pack",
extensionPack: "another-extension-pack",
},
],
expect(
await pickExtensionPackModelFile(
cliServer,
databaseItem,
progress,
token,
),
).toEqual(join(tmpDir.path, "models/my-model.yml"));
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
expect(showInputBoxSpy).toHaveBeenCalledWith(
{
title: expect.any(String),
value: "models/github.vscode-codeql.model.yml",
validateInput: expect.any(Function),
},
token,
);
expect(showQuickPickSpy).toHaveBeenCalledWith([], {
title: expect.any(String),
});
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
expect(cliServer.resolveExtensions).toHaveBeenCalled();
});
it("shows an error when there is no pack YAML file", async () => {
const tmpDir = await dir({
unsafeCleanup: true,
});
const cliServer = mockCliServer(
{
"my-extension-pack": [tmpDir.path],
},
{ models: [], data: {} },
);
showQuickPickSpy.mockResolvedValueOnce({
label: "my-extension-pack",
extensionPack: "my-extension-pack",
} as QuickPickItem);
showQuickPickSpy.mockResolvedValueOnce(undefined);
showAndLogErrorMessageSpy.mockResolvedValue(undefined);
expect(
await pickExtensionPackModelFile(
cliServer,
databaseItem,
progress,
token,
),
).toEqual(undefined);
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
expect(showInputBoxSpy).not.toHaveBeenCalled();
expect(showAndLogErrorMessageSpy).toHaveBeenCalledTimes(1);
expect(showAndLogErrorMessageSpy).toHaveBeenCalledWith(
expect.stringMatching(/codeql-pack\.yml/),
);
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
expect(cliServer.resolveExtensions).toHaveBeenCalled();
});
it("shows an error when the pack YAML file is invalid", async () => {
const tmpDir = await dir({
unsafeCleanup: true,
});
const cliServer = mockCliServer(
{
"my-extension-pack": [tmpDir.path],
},
{ models: [], data: {} },
);
await outputFile(join(tmpDir.path, "codeql-pack.yml"), dumpYaml("java"));
showQuickPickSpy.mockResolvedValueOnce({
label: "my-extension-pack",
extensionPack: "my-extension-pack",
} as QuickPickItem);
showQuickPickSpy.mockResolvedValueOnce(undefined);
showAndLogErrorMessageSpy.mockResolvedValue(undefined);
expect(
await pickExtensionPackModelFile(
cliServer,
databaseItem,
progress,
token,
),
).toEqual(undefined);
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
expect(showInputBoxSpy).not.toHaveBeenCalled();
expect(showAndLogErrorMessageSpy).toHaveBeenCalledTimes(1);
expect(showAndLogErrorMessageSpy).toHaveBeenCalledWith(
expect.stringMatching(/Could not parse/),
);
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
expect(cliServer.resolveExtensions).toHaveBeenCalled();
});
it("shows an error when the pack YAML does not contain dataExtensions", async () => {
const tmpDir = await dir({
unsafeCleanup: true,
});
const cliServer = mockCliServer(
{
"my-extension-pack": [tmpDir.path],
},
{ models: [], data: {} },
);
await outputFile(
join(tmpDir.path, "codeql-pack.yml"),
dumpYaml({
name: "my-extension-pack",
version: "0.0.0",
library: true,
extensionTargets: {
"codeql/java-all": "*",
},
}),
);
showQuickPickSpy.mockResolvedValueOnce({
label: "my-extension-pack",
extensionPack: "my-extension-pack",
} as QuickPickItem);
showQuickPickSpy.mockResolvedValueOnce(undefined);
showAndLogErrorMessageSpy.mockResolvedValue(undefined);
expect(
await pickExtensionPackModelFile(
cliServer,
databaseItem,
progress,
token,
),
).toEqual(undefined);
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
expect(showInputBoxSpy).not.toHaveBeenCalled();
expect(showAndLogErrorMessageSpy).toHaveBeenCalledTimes(1);
expect(showAndLogErrorMessageSpy).toHaveBeenCalledWith(
expect.stringMatching(/Expected 'dataExtensions' to be/),
);
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
expect(cliServer.resolveExtensions).toHaveBeenCalled();
});
it("shows an error when the pack YAML dataExtensions is invalid", async () => {
const tmpDir = await dir({
unsafeCleanup: true,
});
const cliServer = mockCliServer(
{
"my-extension-pack": [tmpDir.path],
},
{ models: [], data: {} },
);
await outputFile(
join(tmpDir.path, "codeql-pack.yml"),
dumpYaml({
name: "my-extension-pack",
version: "0.0.0",
library: true,
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: {
"codeql/java-all": "invalid",
},
}),
);
showQuickPickSpy.mockResolvedValueOnce({
label: "my-extension-pack",
extensionPack: "my-extension-pack",
} as QuickPickItem);
showQuickPickSpy.mockResolvedValueOnce(undefined);
showAndLogErrorMessageSpy.mockResolvedValue(undefined);
expect(
await pickExtensionPackModelFile(
cliServer,
databaseItem,
progress,
token,
),
).toEqual(undefined);
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
expect(showInputBoxSpy).not.toHaveBeenCalled();
expect(showAndLogErrorMessageSpy).toHaveBeenCalledTimes(1);
expect(showAndLogErrorMessageSpy).toHaveBeenCalledWith(
expect.stringMatching(/Expected 'dataExtensions' to be/),
);
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
expect(cliServer.resolveExtensions).toHaveBeenCalled();
});
it("allows cancelling the new file input box", async () => {
const tmpDir = await dir({
unsafeCleanup: true,
});
const cliServer = mockCliServer(
{
"my-extension-pack": [tmpDir.path],
},
{ models: [], data: {} },
);
await outputFile(
join(tmpDir.path, "codeql-pack.yml"),
dumpYaml({
name: "my-extension-pack",
version: "0.0.0",
library: true,
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: ["models/**/*.yml"],
}),
);
showQuickPickSpy.mockResolvedValueOnce({
label: "my-extension-pack",
extensionPack: "my-extension-pack",
} as QuickPickItem);
showQuickPickSpy.mockResolvedValueOnce(undefined);
showInputBoxSpy.mockResolvedValue(undefined);
expect(
await pickExtensionPackModelFile(
cliServer,
databaseItem,
progress,
token,
),
).toEqual(undefined);
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
expect(showInputBoxSpy).toHaveBeenCalledTimes(1);
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
expect(cliServer.resolveExtensions).toHaveBeenCalled();
});
it("validates the pack name input", async () => {
const cliServer = mockCliServer({}, { models: [], data: {} });
showQuickPickSpy.mockResolvedValueOnce({
label: "a",
path: "/a/b/c",
} as QuickPickItem);
showInputBoxSpy.mockResolvedValue(undefined);
expect(
await pickExtensionPackModelFile(
cliServer,
databaseItem,
progress,
token,
),
).toEqual(undefined);
const validateFile = showInputBoxSpy.mock.calls[0][0]?.validateInput;
expect(validateFile).toBeDefined();
if (!validateFile) {
return;
}
expect(await validateFile("")).toEqual("Pack name must not be empty");
expect(await validateFile("a".repeat(129))).toEqual(
"Pack name must be no longer than 128 characters",
);
expect(await validateFile("github/vscode-codeql/extensions")).toEqual(
"Invalid package name: a pack name must contain only lowercase ASCII letters, ASCII digits, and hyphens",
);
expect(await validateFile("VSCODE")).toEqual(
"Invalid package name: a pack name must contain only lowercase ASCII letters, ASCII digits, and hyphens",
);
expect(await validateFile("github/vscode-codeql-")).toEqual(
"Invalid package name: a pack name must contain only lowercase ASCII letters, ASCII digits, and hyphens",
);
expect(
await validateFile("github/vscode-codeql-extensions"),
).toBeUndefined();
expect(await validateFile("vscode-codeql-extensions")).toBeUndefined();
});
it("validates the file input", async () => {
const tmpDir = await dir({
unsafeCleanup: true,
});
const cliServer = mockCliServer(
{
"my-extension-pack": [tmpDir.path],
},
{ models: [], data: {} },
);
const qlpackPath = join(tmpDir.path, "codeql-pack.yml");
await outputFile(
qlpackPath,
dumpYaml({
name: "my-extension-pack",
version: "0.0.0",
library: true,
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: ["models/**/*.yml", "data/**/*.yml"],
}),
);
await outputFile(
join(tmpDir.path, "models", "model.yml"),
dumpYaml({
extensions: [],
}),
);
showQuickPickSpy.mockResolvedValueOnce({
label: "my-extension-pack",
extensionPack: "my-extension-pack",
} as QuickPickItem);
showQuickPickSpy.mockResolvedValueOnce(undefined);
showInputBoxSpy.mockResolvedValue(undefined);
expect(
await pickExtensionPackModelFile(
cliServer,
databaseItem,
progress,
token,
),
).toEqual(undefined);
const validateFile = showInputBoxSpy.mock.calls[0][0]?.validateInput;
expect(validateFile).toBeDefined();
if (!validateFile) {
return;
}
expect(await validateFile("")).toEqual("File name must not be empty");
expect(await validateFile("models/model.yml")).toEqual(
"File already exists",
);
expect(await validateFile("../model.yml")).toEqual(
"File must be in the extension pack",
);
expect(await validateFile("/home/user/model.yml")).toEqual(
"File must be in the extension pack",
);
expect(await validateFile("model.yml")).toEqual(
`File must match one of the patterns in 'dataExtensions' in ${qlpackPath}`,
);
expect(await validateFile("models/model.yaml")).toEqual(
`File must match one of the patterns in 'dataExtensions' in ${qlpackPath}`,
);
expect(await validateFile("models/my-model.yml")).toBeUndefined();
expect(await validateFile("models/nested/model.yml")).toBeUndefined();
expect(await validateFile("data/model.yml")).toBeUndefined();
});
it("allows the dataExtensions to be a string", async () => {
const tmpDir = await dir({
unsafeCleanup: true,
});
const cliServer = mockCliServer(
{
"my-extension-pack": [tmpDir.path],
},
{ models: [], data: {} },
);
const qlpackPath = join(tmpDir.path, "codeql-pack.yml");
await outputFile(
qlpackPath,
dumpYaml({
name: "my-extension-pack",
version: "0.0.0",
library: true,
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: "models/**/*.yml",
}),
);
await outputFile(
join(tmpDir.path, "models", "model.yml"),
dumpYaml({
extensions: [],
}),
);
showQuickPickSpy.mockResolvedValueOnce({
label: "my-extension-pack",
extensionPack: "my-extension-pack",
} as QuickPickItem);
showQuickPickSpy.mockResolvedValueOnce(undefined);
showInputBoxSpy.mockResolvedValue(undefined);
expect(
await pickExtensionPackModelFile(
cliServer,
databaseItem,
progress,
token,
),
).toEqual(undefined);
const validateFile = showInputBoxSpy.mock.calls[0][0]?.validateInput;
expect(validateFile).toBeDefined();
if (!validateFile) {
return;
}
expect(await validateFile("models/my-model.yml")).toBeUndefined();
});
});
function mockCliServer(

View File

@@ -5,11 +5,12 @@ import {
import { createMockLogger } from "../../../__mocks__/loggerMock";
import type { Uri } from "vscode";
import { DatabaseKind } from "../../../../src/local-databases";
import * as queryResolver from "../../../../src/contextual/queryResolver";
import { file } from "tmp-promise";
import { QueryResultType } from "../../../../src/pure/new-messages";
import { readFile } from "fs-extra";
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 * as helpers from "../../../../src/helpers";
import { RedactableError } from "../../../../src/pure/errors";
@@ -27,98 +28,98 @@ function createMockUri(path = "/a/b/c/foo"): Uri {
}
describe("runQuery", () => {
it("runs the query", async () => {
jest.spyOn(queryResolver, "qlpackOfDatabase").mockResolvedValue({
dbschemePack: "codeql/java-all",
dbschemePackIsLibraryPack: false,
queryPack: "codeql/java-queries",
});
it("runs all queries", async () => {
const logPath = (await file()).path;
const options = {
cliServer: {
resolveQlpacks: jest.fn().mockResolvedValue({
"my/java-extensions": "/a/b/c/",
}),
resolveQueriesInSuite: jest
.fn()
.mockResolvedValue([
"/home/github/codeql/java/ql/src/Telemetry/FetchExternalAPIs.ql",
]),
},
queryRunner: {
createQueryRun: jest.fn().mockReturnValue({
evaluate: jest.fn().mockResolvedValue({
resultType: QueryResultType.SUCCESS,
// Test all queries
for (const [lang, query] of Object.entries(fetchExternalApiQueries)) {
const options = {
cliServer: {
resolveQlpacks: jest.fn().mockResolvedValue({
"my/extensions": "/a/b/c/",
}),
outputDir: {
logPath,
},
queryRunner: {
createQueryRun: jest.fn().mockReturnValue({
evaluate: jest.fn().mockResolvedValue({
resultType: QueryResultType.SUCCESS,
}),
outputDir: {
logPath,
},
}),
logger: createMockLogger(),
},
databaseItem: {
databaseUri: createMockUri("/a/b/c/src.zip"),
contents: {
kind: DatabaseKind.Database,
name: "foo",
datasetUri: createMockUri(),
},
}),
logger: createMockLogger(),
},
logger: createMockLogger(),
databaseItem: {
databaseUri: createMockUri("/a/b/c/src.zip"),
contents: {
kind: DatabaseKind.Database,
name: "foo",
datasetUri: createMockUri(),
language: lang,
},
language: "java",
},
queryStorageDir: "/tmp/queries",
progress: jest.fn(),
token: {
isCancellationRequested: false,
onCancellationRequested: jest.fn(),
},
};
const result = await runQuery(options);
expect(result?.resultType).toEqual(QueryResultType.SUCCESS);
expect(options.cliServer.resolveQueriesInSuite).toHaveBeenCalledWith(
expect.anything(),
[],
);
const suiteFile = options.cliServer.resolveQueriesInSuite.mock.calls[0][0];
const suiteFileContents = await readFile(suiteFile, "utf8");
const suiteYaml = load(suiteFileContents);
expect(suiteYaml).toEqual([
{
from: "codeql/java-all",
queries: ".",
include: {
id: "java/telemetry/fetch-external-apis",
queryStorageDir: "/tmp/queries",
progress: jest.fn(),
token: {
isCancellationRequested: false,
onCancellationRequested: jest.fn(),
},
},
{
from: "codeql/java-queries",
queries: ".",
include: {
id: "java/telemetry/fetch-external-apis",
},
},
]);
};
const result = await runQuery(options);
expect(options.cliServer.resolveQlpacks).toHaveBeenCalledTimes(1);
expect(options.cliServer.resolveQlpacks).toHaveBeenCalledWith([], true);
expect(options.queryRunner.createQueryRun).toHaveBeenCalledWith(
"/a/b/c/src.zip",
{
queryPath:
"/home/github/codeql/java/ql/src/Telemetry/FetchExternalAPIs.ql",
quickEvalPosition: undefined,
},
false,
[],
["my/java-extensions"],
"/tmp/queries",
undefined,
undefined,
);
expect(result?.resultType).toEqual(QueryResultType.SUCCESS);
expect(options.cliServer.resolveQlpacks).toHaveBeenCalledTimes(1);
expect(options.cliServer.resolveQlpacks).toHaveBeenCalledWith([], true);
expect(options.queryRunner.createQueryRun).toHaveBeenCalledWith(
"/a/b/c/src.zip",
{
queryPath: expect.stringMatching(/FetchExternalApis\.ql/),
quickEvalPosition: undefined,
},
false,
[],
["my/extensions"],
"/tmp/queries",
undefined,
undefined,
);
const queryPath =
options.queryRunner.createQueryRun.mock.calls[0][1].queryPath;
const queryDirectory = dirname(queryPath);
const queryFiles = await readdir(queryDirectory);
expect(queryFiles.sort()).toEqual(
["codeql-pack.yml", "FetchExternalApis.ql", "ExternalApi.qll"].sort(),
);
const suiteFileContents = await readFile(
join(queryDirectory, "codeql-pack.yml"),
"utf8",
);
const suiteYaml = load(suiteFileContents);
expect(suiteYaml).toEqual({
name: "codeql/external-api-usage",
version: "0.0.0",
dependencies: {
[`codeql/${lang}-all`]: "*",
},
});
expect(
await readFile(join(queryDirectory, "FetchExternalApis.ql"), "utf8"),
).toEqual(query.mainQuery);
for (const [filename, contents] of Object.entries(
query.dependencies ?? {},
)) {
expect(await readFile(join(queryDirectory, filename), "utf8")).toEqual(
contents,
);
}
}
});
});

View File

@@ -7,7 +7,11 @@
"completed": true,
"variantAnalysis": {
"id": 98574321397,
"controllerRepoId": 128321,
"controllerRepo": {
"id": 128321,
"fullName": "github/codeql",
"private": false
},
"query": {
"name": "Variant Analysis Integration Test 1",
"filePath": "PLACEHOLDER/q2.ql",
@@ -30,7 +34,11 @@
"completed": true,
"variantAnalysis": {
"id": 98574321397,
"controllerRepoId": 128321,
"controllerRepo": {
"id": 128321,
"fullName": "github/codeql",
"private": false
},
"query": {
"name": "Variant Analysis Integration Test 2",
"filePath": "PLACEHOLDER/q2.ql",

View File

@@ -26,6 +26,7 @@ import {
import { DirResult } from "tmp";
import {
getFirstWorkspaceFolder,
getInitialQueryContents,
InvocationRateLimiter,
isFolderAlreadyInWorkspace,
@@ -272,6 +273,13 @@ describe("helpers", () => {
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.");
@@ -671,3 +679,42 @@ describe("prepareCodeTour", () => {
});
});
});
describe("getFirstWorkspaceFolder", () => {
it("should return the first workspace folder", async () => {
jest.spyOn(workspace, "workspaceFolders", "get").mockReturnValue([
{
name: "codespaces-codeql",
uri: { fsPath: "codespaces-codeql", scheme: "file" },
},
] as WorkspaceFolder[]);
expect(getFirstWorkspaceFolder()).toEqual("codespaces-codeql");
});
describe("if user is in vscode-codeql-starter workspace", () => {
it("should set storage path to parent folder", async () => {
jest.spyOn(workspace, "workspaceFolders", "get").mockReturnValue([
{
name: "codeql-custom-queries-cpp",
uri: {
fsPath: join("vscode-codeql-starter", "codeql-custom-queries-cpp"),
scheme: "file",
},
},
{
name: "codeql-custom-queries-csharp",
uri: {
fsPath: join(
"vscode-codeql-starter",
"codeql-custom-queries-csharp",
),
scheme: "file",
},
},
] as WorkspaceFolder[]);
expect(getFirstWorkspaceFolder()).toEqual("vscode-codeql-starter");
});
});
});