Merge remote-tracking branch 'origin/main' into dbartol/debug-adapter
This commit is contained in:
16
extensions/ql-vscode/.babelrc.json
Normal file
16
extensions/ql-vscode/.babelrc.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"sourceType": "unambiguous",
|
||||||
|
"presets": [
|
||||||
|
[
|
||||||
|
"@babel/preset-env",
|
||||||
|
{
|
||||||
|
"targets": {
|
||||||
|
"chrome": 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@babel/preset-typescript",
|
||||||
|
"@babel/preset-react"
|
||||||
|
],
|
||||||
|
"plugins": []
|
||||||
|
}
|
||||||
@@ -12,6 +12,9 @@ const config: StorybookConfig = {
|
|||||||
core: {
|
core: {
|
||||||
builder: "@storybook/builder-webpack5",
|
builder: "@storybook/builder-webpack5",
|
||||||
},
|
},
|
||||||
|
features: {
|
||||||
|
babelModeV7: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = config;
|
module.exports = config;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
## [UNRELEASED]
|
## [UNRELEASED]
|
||||||
|
|
||||||
|
- Fix bug that was causing code flows to not get updated when switching between results. [#2288](https://github.com/github/vscode-codeql/pull/2288)
|
||||||
- Restart the CodeQL language server whenever the _CodeQL: Restart Query Server_ command is invoked. This avoids bugs where the CLI version changes to support new language features, but the language server is not updated. [#2238](https://github.com/github/vscode-codeql/pull/2238)
|
- Restart the CodeQL language server whenever the _CodeQL: Restart Query Server_ command is invoked. This avoids bugs where the CLI version changes to support new language features, but the language server is not updated. [#2238](https://github.com/github/vscode-codeql/pull/2238)
|
||||||
|
|
||||||
## 1.8.1 - 23 March 2023
|
## 1.8.1 - 23 March 2023
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ import { redactableError } from "../pure/errors";
|
|||||||
import { QLPACK_FILENAMES } from "../pure/ql";
|
import { QLPACK_FILENAMES } from "../pure/ql";
|
||||||
|
|
||||||
export async function qlpackOfDatabase(
|
export async function qlpackOfDatabase(
|
||||||
cli: CodeQLCliServer,
|
cli: Pick<CodeQLCliServer, "resolveQlpacks">,
|
||||||
db: DatabaseItem,
|
db: Pick<DatabaseItem, "contents">,
|
||||||
): Promise<QlPacksForLanguage> {
|
): Promise<QlPacksForLanguage> {
|
||||||
if (db.contents === undefined) {
|
if (db.contents === undefined) {
|
||||||
throw new Error("Database is invalid and cannot infer QLPack.");
|
throw new Error("Database is invalid and cannot infer QLPack.");
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
import { ExtensionContext } from "vscode";
|
import { ExtensionContext } from "vscode";
|
||||||
import { DataExtensionsEditorView } from "./data-extensions-editor-view";
|
import { DataExtensionsEditorView } from "./data-extensions-editor-view";
|
||||||
import { DataExtensionsEditorCommands } from "../common/commands";
|
import { DataExtensionsEditorCommands } from "../common/commands";
|
||||||
import { CodeQLCliServer } from "../cli";
|
import { CliVersionConstraint, CodeQLCliServer } from "../cli";
|
||||||
import { QueryRunner } from "../queryRunner";
|
import { QueryRunner } from "../queryRunner";
|
||||||
import { DatabaseManager } from "../local-databases";
|
import { DatabaseManager } from "../local-databases";
|
||||||
import { extLogger } from "../common";
|
|
||||||
import { ensureDir } from "fs-extra";
|
import { ensureDir } from "fs-extra";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
|
import { App } from "../common/app";
|
||||||
|
import { showAndLogErrorMessage } from "../helpers";
|
||||||
|
|
||||||
export class DataExtensionsEditorModule {
|
export class DataExtensionsEditorModule {
|
||||||
private readonly queryStorageDir: string;
|
private readonly queryStorageDir: string;
|
||||||
|
|
||||||
private constructor(
|
private constructor(
|
||||||
private readonly ctx: ExtensionContext,
|
private readonly ctx: ExtensionContext,
|
||||||
|
private readonly app: App,
|
||||||
private readonly databaseManager: DatabaseManager,
|
private readonly databaseManager: DatabaseManager,
|
||||||
private readonly cliServer: CodeQLCliServer,
|
private readonly cliServer: CodeQLCliServer,
|
||||||
private readonly queryRunner: QueryRunner,
|
private readonly queryRunner: QueryRunner,
|
||||||
@@ -26,6 +28,7 @@ export class DataExtensionsEditorModule {
|
|||||||
|
|
||||||
public static async initialize(
|
public static async initialize(
|
||||||
ctx: ExtensionContext,
|
ctx: ExtensionContext,
|
||||||
|
app: App,
|
||||||
databaseManager: DatabaseManager,
|
databaseManager: DatabaseManager,
|
||||||
cliServer: CodeQLCliServer,
|
cliServer: CodeQLCliServer,
|
||||||
queryRunner: QueryRunner,
|
queryRunner: QueryRunner,
|
||||||
@@ -33,6 +36,7 @@ export class DataExtensionsEditorModule {
|
|||||||
): Promise<DataExtensionsEditorModule> {
|
): Promise<DataExtensionsEditorModule> {
|
||||||
const dataExtensionsEditorModule = new DataExtensionsEditorModule(
|
const dataExtensionsEditorModule = new DataExtensionsEditorModule(
|
||||||
ctx,
|
ctx,
|
||||||
|
app,
|
||||||
databaseManager,
|
databaseManager,
|
||||||
cliServer,
|
cliServer,
|
||||||
queryRunner,
|
queryRunner,
|
||||||
@@ -48,12 +52,21 @@ export class DataExtensionsEditorModule {
|
|||||||
"codeQL.openDataExtensionsEditor": async () => {
|
"codeQL.openDataExtensionsEditor": async () => {
|
||||||
const db = this.databaseManager.currentDatabaseItem;
|
const db = this.databaseManager.currentDatabaseItem;
|
||||||
if (!db) {
|
if (!db) {
|
||||||
void extLogger.log("No database selected");
|
void showAndLogErrorMessage("No database selected");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await this.cliServer.cliConstraints.supportsQlpacksKind())) {
|
||||||
|
void showAndLogErrorMessage(
|
||||||
|
`This feature requires CodeQL CLI version ${CliVersionConstraint.CLI_VERSION_WITH_QLPACKS_KIND.format()} or later.`,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const view = new DataExtensionsEditorView(
|
const view = new DataExtensionsEditorView(
|
||||||
this.ctx,
|
this.ctx,
|
||||||
|
this.app,
|
||||||
|
this.databaseManager,
|
||||||
this.cliServer,
|
this.cliServer,
|
||||||
this.queryRunner,
|
this.queryRunner,
|
||||||
this.queryStorageDir,
|
this.queryStorageDir,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
ViewColumn,
|
ViewColumn,
|
||||||
window,
|
window,
|
||||||
workspace,
|
workspace,
|
||||||
|
WorkspaceFolder,
|
||||||
} from "vscode";
|
} from "vscode";
|
||||||
import { AbstractWebview, WebviewPanelConfig } from "../abstract-webview";
|
import { AbstractWebview, WebviewPanelConfig } from "../abstract-webview";
|
||||||
import {
|
import {
|
||||||
@@ -12,34 +13,50 @@ import {
|
|||||||
ToDataExtensionsEditorMessage,
|
ToDataExtensionsEditorMessage,
|
||||||
} from "../pure/interface-types";
|
} from "../pure/interface-types";
|
||||||
import { ProgressUpdate } from "../progress";
|
import { ProgressUpdate } from "../progress";
|
||||||
import { extLogger, TeeLogger } from "../common";
|
import { QueryRunner } from "../queryRunner";
|
||||||
import { CoreCompletedQuery, QueryRunner } from "../queryRunner";
|
|
||||||
import { qlpackOfDatabase } from "../contextual/queryResolver";
|
|
||||||
import { file } from "tmp-promise";
|
|
||||||
import { readFile, writeFile } from "fs-extra";
|
|
||||||
import { dump as dumpYaml, load as loadYaml } from "js-yaml";
|
|
||||||
import {
|
import {
|
||||||
getOnDiskWorkspaceFolders,
|
|
||||||
showAndLogExceptionWithTelemetry,
|
showAndLogExceptionWithTelemetry,
|
||||||
showAndLogWarningMessage,
|
showAndLogWarningMessage,
|
||||||
} from "../helpers";
|
} from "../helpers";
|
||||||
import { DatabaseItem } from "../local-databases";
|
import { extLogger } from "../common";
|
||||||
|
import { readFile, writeFile } from "fs-extra";
|
||||||
|
import { load as loadYaml } from "js-yaml";
|
||||||
|
import { DatabaseItem, DatabaseManager } from "../local-databases";
|
||||||
import { CodeQLCliServer } from "../cli";
|
import { CodeQLCliServer } from "../cli";
|
||||||
import { asError, assertNever, getErrorMessage } from "../pure/helpers-pure";
|
import { asError, assertNever, getErrorMessage } from "../pure/helpers-pure";
|
||||||
|
import { generateFlowModel } from "./generate-flow-model";
|
||||||
|
import { promptImportGithubDatabase } from "../databaseFetcher";
|
||||||
|
import { App } from "../common/app";
|
||||||
import { ResolvableLocationValue } from "../pure/bqrs-cli-types";
|
import { ResolvableLocationValue } from "../pure/bqrs-cli-types";
|
||||||
import { showResolvableLocation } from "../interface-utils";
|
import { showResolvableLocation } from "../interface-utils";
|
||||||
import { decodeBqrsToExternalApiUsages } from "./bqrs";
|
import { decodeBqrsToExternalApiUsages } from "./bqrs";
|
||||||
import { redactableError } from "../pure/errors";
|
import { redactableError } from "../pure/errors";
|
||||||
|
import { readQueryResults, runQuery } from "./external-api-usage-query";
|
||||||
import { createDataExtensionYaml, loadDataExtensionYaml } from "./yaml";
|
import { createDataExtensionYaml, loadDataExtensionYaml } from "./yaml";
|
||||||
import { ExternalApiUsage } from "./external-api-usage";
|
import { ExternalApiUsage } from "./external-api-usage";
|
||||||
import { ModeledMethod } from "./modeled-method";
|
import { ModeledMethod } from "./modeled-method";
|
||||||
|
|
||||||
|
function getQlSubmoduleFolder(): WorkspaceFolder | undefined {
|
||||||
|
const workspaceFolder = workspace.workspaceFolders?.find(
|
||||||
|
(folder) => folder.name === "ql",
|
||||||
|
);
|
||||||
|
if (!workspaceFolder) {
|
||||||
|
void extLogger.log("No workspace folder 'ql' found");
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return workspaceFolder;
|
||||||
|
}
|
||||||
|
|
||||||
export class DataExtensionsEditorView extends AbstractWebview<
|
export class DataExtensionsEditorView extends AbstractWebview<
|
||||||
ToDataExtensionsEditorMessage,
|
ToDataExtensionsEditorMessage,
|
||||||
FromDataExtensionsEditorMessage
|
FromDataExtensionsEditorMessage
|
||||||
> {
|
> {
|
||||||
public constructor(
|
public constructor(
|
||||||
ctx: ExtensionContext,
|
ctx: ExtensionContext,
|
||||||
|
private readonly app: App,
|
||||||
|
private readonly databaseManager: DatabaseManager,
|
||||||
private readonly cliServer: CodeQLCliServer,
|
private readonly cliServer: CodeQLCliServer,
|
||||||
private readonly queryRunner: QueryRunner,
|
private readonly queryRunner: QueryRunner,
|
||||||
private readonly queryStorageDir: string,
|
private readonly queryStorageDir: string,
|
||||||
@@ -88,6 +105,10 @@ export class DataExtensionsEditorView extends AbstractWebview<
|
|||||||
);
|
);
|
||||||
await this.loadExternalApiUsages();
|
await this.loadExternalApiUsages();
|
||||||
|
|
||||||
|
break;
|
||||||
|
case "generateExternalApi":
|
||||||
|
await this.generateModeledMethods();
|
||||||
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
assertNever(msg);
|
assertNever(msg);
|
||||||
@@ -160,8 +181,8 @@ export class DataExtensionsEditorView extends AbstractWebview<
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.postMessage({
|
await this.postMessage({
|
||||||
t: "setExistingModeledMethods",
|
t: "addModeledMethods",
|
||||||
existingModeledMethods,
|
modeledMethods: existingModeledMethods,
|
||||||
});
|
});
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
void extLogger.log(`Unable to read data extension YAML: ${e}`);
|
void extLogger.log(`Unable to read data extension YAML: ${e}`);
|
||||||
@@ -169,22 +190,36 @@ export class DataExtensionsEditorView extends AbstractWebview<
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async loadExternalApiUsages(): Promise<void> {
|
protected async loadExternalApiUsages(): Promise<void> {
|
||||||
|
const cancellationTokenSource = new CancellationTokenSource();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const queryResult = await this.runQuery();
|
const queryResult = await runQuery({
|
||||||
|
cliServer: this.cliServer,
|
||||||
|
queryRunner: this.queryRunner,
|
||||||
|
databaseItem: this.databaseItem,
|
||||||
|
queryStorageDir: this.queryStorageDir,
|
||||||
|
logger: extLogger,
|
||||||
|
progress: (progressUpdate: ProgressUpdate) => {
|
||||||
|
void this.showProgress(progressUpdate, 1500);
|
||||||
|
},
|
||||||
|
token: cancellationTokenSource.token,
|
||||||
|
});
|
||||||
if (!queryResult) {
|
if (!queryResult) {
|
||||||
await this.clearProgress();
|
await this.clearProgress();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.showProgress({
|
await this.showProgress({
|
||||||
message: "Loading results",
|
message: "Decoding results",
|
||||||
step: 1100,
|
step: 1100,
|
||||||
maxStep: 1500,
|
maxStep: 1500,
|
||||||
});
|
});
|
||||||
|
|
||||||
const bqrsPath = queryResult.outputDir.bqrsPath;
|
const bqrsChunk = await readQueryResults({
|
||||||
|
cliServer: this.cliServer,
|
||||||
const bqrsChunk = await this.getResults(bqrsPath);
|
bqrsPath: queryResult.outputDir.bqrsPath,
|
||||||
|
logger: extLogger,
|
||||||
|
});
|
||||||
if (!bqrsChunk) {
|
if (!bqrsChunk) {
|
||||||
await this.clearProgress();
|
await this.clearProgress();
|
||||||
return;
|
return;
|
||||||
@@ -213,81 +248,90 @@ export class DataExtensionsEditorView extends AbstractWebview<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async runQuery(): Promise<CoreCompletedQuery | undefined> {
|
protected async generateModeledMethods(): Promise<void> {
|
||||||
const qlpacks = await qlpackOfDatabase(this.cliServer, this.databaseItem);
|
const tokenSource = new CancellationTokenSource();
|
||||||
|
|
||||||
const packsToSearch = [qlpacks.dbschemePack];
|
const selectedDatabase = this.databaseManager.currentDatabaseItem;
|
||||||
if (qlpacks.queryPack) {
|
|
||||||
packsToSearch.push(qlpacks.queryPack);
|
|
||||||
}
|
|
||||||
|
|
||||||
const suiteFile = (
|
// The external API methods are in the library source code, so we need to ask
|
||||||
await file({
|
// the user to import the library database. We need to have the database
|
||||||
postfix: ".qls",
|
// imported to the query server, so we need to register it to our workspace.
|
||||||
})
|
const database = await promptImportGithubDatabase(
|
||||||
).path;
|
this.app.commands,
|
||||||
const suiteYaml = [];
|
this.databaseManager,
|
||||||
for (const qlpack of packsToSearch) {
|
this.app.workspaceStoragePath ?? this.app.globalStoragePath,
|
||||||
suiteYaml.push({
|
this.app.credentials,
|
||||||
from: qlpack,
|
(update) => this.showProgress(update),
|
||||||
queries: ".",
|
tokenSource.token,
|
||||||
include: {
|
this.cliServer,
|
||||||
id: `${this.databaseItem.language}/telemetry/fetch-external-apis`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await writeFile(suiteFile, dumpYaml(suiteYaml), "utf8");
|
|
||||||
|
|
||||||
const queries = await this.cliServer.resolveQueriesInSuite(
|
|
||||||
suiteFile,
|
|
||||||
getOnDiskWorkspaceFolders(),
|
|
||||||
);
|
);
|
||||||
|
if (!database) {
|
||||||
|
await this.clearProgress();
|
||||||
|
void extLogger.log("No database chosen");
|
||||||
|
|
||||||
if (queries.length !== 1) {
|
|
||||||
void extLogger.log(`Expected exactly one query, got ${queries.length}`);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const query = queries[0];
|
// The library database was set as the current database by importing it,
|
||||||
|
// but we need to set it back to the originally selected database.
|
||||||
|
await this.databaseManager.setCurrentDatabaseItem(selectedDatabase);
|
||||||
|
|
||||||
const tokenSource = new CancellationTokenSource();
|
const workspaceFolder = getQlSubmoduleFolder();
|
||||||
|
if (!workspaceFolder) {
|
||||||
const queryRun = this.queryRunner.createQueryRun(
|
return;
|
||||||
this.databaseItem.databaseUri.fsPath,
|
|
||||||
{ queryPath: query, quickEvalPosition: undefined },
|
|
||||||
false,
|
|
||||||
getOnDiskWorkspaceFolders(),
|
|
||||||
undefined,
|
|
||||||
this.queryStorageDir,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
|
|
||||||
return queryRun.evaluate(
|
|
||||||
(update) => this.showProgress(update, 1500),
|
|
||||||
tokenSource.token,
|
|
||||||
new TeeLogger(this.queryRunner.logger, queryRun.outputDir.logPath),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getResults(bqrsPath: string) {
|
|
||||||
const bqrsInfo = await this.cliServer.bqrsInfo(bqrsPath);
|
|
||||||
if (bqrsInfo["result-sets"].length !== 1) {
|
|
||||||
void extLogger.log(
|
|
||||||
`Expected exactly one result set, got ${bqrsInfo["result-sets"].length}`,
|
|
||||||
);
|
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const resultSet = bqrsInfo["result-sets"][0];
|
|
||||||
|
|
||||||
await this.showProgress({
|
await this.showProgress({
|
||||||
message: "Decoding results",
|
step: 0,
|
||||||
step: 1200,
|
maxStep: 4000,
|
||||||
maxStep: 1500,
|
message: "Generating modeled methods for library",
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.cliServer.bqrsDecode(bqrsPath, resultSet.name);
|
try {
|
||||||
|
await generateFlowModel({
|
||||||
|
cliServer: this.cliServer,
|
||||||
|
queryRunner: this.queryRunner,
|
||||||
|
queryStorageDir: this.queryStorageDir,
|
||||||
|
qlDir: workspaceFolder.uri.fsPath,
|
||||||
|
databaseItem: database,
|
||||||
|
onResults: async (results) => {
|
||||||
|
const modeledMethodsByName: Record<string, ModeledMethod> = {};
|
||||||
|
|
||||||
|
for (const result of results) {
|
||||||
|
modeledMethodsByName[result.signature] = result.modeledMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.postMessage({
|
||||||
|
t: "addModeledMethods",
|
||||||
|
modeledMethods: modeledMethodsByName,
|
||||||
|
overrideNone: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
progress: (update) => this.showProgress(update),
|
||||||
|
token: tokenSource.token,
|
||||||
|
});
|
||||||
|
} catch (e: unknown) {
|
||||||
|
void showAndLogExceptionWithTelemetry(
|
||||||
|
redactableError(
|
||||||
|
asError(e),
|
||||||
|
)`Failed to generate flow model: ${getErrorMessage(e)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// After the flow model has been generated, we can remove the temporary database
|
||||||
|
// which we used for generating the flow model.
|
||||||
|
await this.databaseManager.removeDatabaseItem(
|
||||||
|
() =>
|
||||||
|
this.showProgress({
|
||||||
|
step: 3900,
|
||||||
|
maxStep: 4000,
|
||||||
|
message: "Removing temporary database",
|
||||||
|
}),
|
||||||
|
tokenSource.token,
|
||||||
|
database,
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.clearProgress();
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -297,6 +341,13 @@ export class DataExtensionsEditorView extends AbstractWebview<
|
|||||||
* that there's 1000 steps of the query progress since that takes the most time, and then
|
* that there's 1000 steps of the query progress since that takes the most time, and then
|
||||||
* an additional 500 steps for the rest of the work. The progress doesn't need to be 100%
|
* an additional 500 steps for the rest of the work. The progress doesn't need to be 100%
|
||||||
* accurate, so this is just a rough estimate.
|
* accurate, so this is just a rough estimate.
|
||||||
|
*
|
||||||
|
* For generating the modeled methods for an external library, the max step is 4000. This is
|
||||||
|
* based on the following steps:
|
||||||
|
* - 1000 for the summary model
|
||||||
|
* - 1000 for the sink model
|
||||||
|
* - 1000 for the source model
|
||||||
|
* - 1000 for the neutral model
|
||||||
*/
|
*/
|
||||||
private async showProgress(update: ProgressUpdate, maxStep?: number) {
|
private async showProgress(update: ProgressUpdate, maxStep?: number) {
|
||||||
await this.postMessage({
|
await this.postMessage({
|
||||||
@@ -316,12 +367,8 @@ export class DataExtensionsEditorView extends AbstractWebview<
|
|||||||
}
|
}
|
||||||
|
|
||||||
private calculateModelFilename(): string | undefined {
|
private calculateModelFilename(): string | undefined {
|
||||||
const workspaceFolder = workspace.workspaceFolders?.find(
|
const workspaceFolder = getQlSubmoduleFolder();
|
||||||
(folder) => folder.name === "ql",
|
|
||||||
);
|
|
||||||
if (!workspaceFolder) {
|
if (!workspaceFolder) {
|
||||||
void extLogger.log("No workspace folder 'ql' found");
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import { CoreCompletedQuery, QueryRunner } from "../queryRunner";
|
||||||
|
import { qlpackOfDatabase } from "../contextual/queryResolver";
|
||||||
|
import { file } from "tmp-promise";
|
||||||
|
import { writeFile } from "fs-extra";
|
||||||
|
import { dump as dumpYaml } from "js-yaml";
|
||||||
|
import { getOnDiskWorkspaceFolders } from "../helpers";
|
||||||
|
import { Logger, TeeLogger } from "../common";
|
||||||
|
import { CancellationToken } from "vscode";
|
||||||
|
import { CodeQLCliServer } from "../cli";
|
||||||
|
import { DatabaseItem } from "../local-databases";
|
||||||
|
import { ProgressCallback } from "../progress";
|
||||||
|
|
||||||
|
export type RunQueryOptions = {
|
||||||
|
cliServer: Pick<CodeQLCliServer, "resolveQlpacks" | "resolveQueriesInSuite">;
|
||||||
|
queryRunner: Pick<QueryRunner, "createQueryRun" | "logger">;
|
||||||
|
databaseItem: Pick<DatabaseItem, "contents" | "databaseUri" | "language">;
|
||||||
|
queryStorageDir: string;
|
||||||
|
logger: Logger;
|
||||||
|
|
||||||
|
progress: ProgressCallback;
|
||||||
|
token: CancellationToken;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function runQuery({
|
||||||
|
cliServer,
|
||||||
|
queryRunner,
|
||||||
|
databaseItem,
|
||||||
|
queryStorageDir,
|
||||||
|
logger,
|
||||||
|
progress,
|
||||||
|
token,
|
||||||
|
}: RunQueryOptions): Promise<CoreCompletedQuery | undefined> {
|
||||||
|
const qlpacks = await qlpackOfDatabase(cliServer, databaseItem);
|
||||||
|
|
||||||
|
const packsToSearch = [qlpacks.dbschemePack];
|
||||||
|
if (qlpacks.queryPack) {
|
||||||
|
packsToSearch.push(qlpacks.queryPack);
|
||||||
|
}
|
||||||
|
|
||||||
|
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`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await writeFile(suiteFile, dumpYaml(suiteYaml), "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 },
|
||||||
|
false,
|
||||||
|
getOnDiskWorkspaceFolders(),
|
||||||
|
extensionPacks,
|
||||||
|
queryStorageDir,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
return queryRun.evaluate(
|
||||||
|
progress,
|
||||||
|
token,
|
||||||
|
new TeeLogger(queryRunner.logger, queryRun.outputDir.logPath),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GetResultsOptions = {
|
||||||
|
cliServer: Pick<CodeQLCliServer, "bqrsInfo" | "bqrsDecode">;
|
||||||
|
bqrsPath: string;
|
||||||
|
logger: Logger;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function readQueryResults({
|
||||||
|
cliServer,
|
||||||
|
bqrsPath,
|
||||||
|
logger,
|
||||||
|
}: GetResultsOptions) {
|
||||||
|
const bqrsInfo = await cliServer.bqrsInfo(bqrsPath);
|
||||||
|
if (bqrsInfo["result-sets"].length !== 1) {
|
||||||
|
void logger.log(
|
||||||
|
`Expected exactly one result set, got ${bqrsInfo["result-sets"].length}`,
|
||||||
|
);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultSet = bqrsInfo["result-sets"][0];
|
||||||
|
|
||||||
|
return cliServer.bqrsDecode(bqrsPath, resultSet.name);
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import { CancellationToken } from "vscode";
|
||||||
|
import { DatabaseItem } from "../local-databases";
|
||||||
|
import { join } from "path";
|
||||||
|
import { QueryRunner } from "../queryRunner";
|
||||||
|
import { CodeQLCliServer } from "../cli";
|
||||||
|
import { TeeLogger } from "../common";
|
||||||
|
import { extensiblePredicateDefinitions } from "./yaml";
|
||||||
|
import { ProgressCallback } from "../progress";
|
||||||
|
import { getOnDiskWorkspaceFolders } from "../helpers";
|
||||||
|
import {
|
||||||
|
ModeledMethodType,
|
||||||
|
ModeledMethodWithSignature,
|
||||||
|
} from "./modeled-method";
|
||||||
|
|
||||||
|
type FlowModelOptions = {
|
||||||
|
cliServer: CodeQLCliServer;
|
||||||
|
queryRunner: QueryRunner;
|
||||||
|
queryStorageDir: string;
|
||||||
|
qlDir: string;
|
||||||
|
databaseItem: DatabaseItem;
|
||||||
|
progress: ProgressCallback;
|
||||||
|
token: CancellationToken;
|
||||||
|
onResults: (results: ModeledMethodWithSignature[]) => void | Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function getModeledMethodsFromFlow(
|
||||||
|
type: Exclude<ModeledMethodType, "none">,
|
||||||
|
queryName: string,
|
||||||
|
queryStep: number,
|
||||||
|
{
|
||||||
|
cliServer,
|
||||||
|
queryRunner,
|
||||||
|
queryStorageDir,
|
||||||
|
qlDir,
|
||||||
|
databaseItem,
|
||||||
|
progress,
|
||||||
|
token,
|
||||||
|
}: Omit<FlowModelOptions, "onResults">,
|
||||||
|
): Promise<ModeledMethodWithSignature[]> {
|
||||||
|
const definition = extensiblePredicateDefinitions[type];
|
||||||
|
|
||||||
|
const query = join(
|
||||||
|
qlDir,
|
||||||
|
databaseItem.language,
|
||||||
|
"ql/src/utils/modelgenerator",
|
||||||
|
queryName,
|
||||||
|
);
|
||||||
|
|
||||||
|
const queryRun = queryRunner.createQueryRun(
|
||||||
|
databaseItem.databaseUri.fsPath,
|
||||||
|
{ queryPath: query, quickEvalPosition: undefined },
|
||||||
|
false,
|
||||||
|
getOnDiskWorkspaceFolders(),
|
||||||
|
undefined,
|
||||||
|
queryStorageDir,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
const queryResult = await queryRun.evaluate(
|
||||||
|
({ step, message }) =>
|
||||||
|
progress({
|
||||||
|
message: `Generating ${type} model: ${message}`,
|
||||||
|
step: queryStep * 1000 + step,
|
||||||
|
maxStep: 4000,
|
||||||
|
}),
|
||||||
|
token,
|
||||||
|
new TeeLogger(queryRunner.logger, queryRun.outputDir.logPath),
|
||||||
|
);
|
||||||
|
|
||||||
|
const bqrsPath = queryResult.outputDir.bqrsPath;
|
||||||
|
|
||||||
|
const bqrsInfo = await cliServer.bqrsInfo(bqrsPath);
|
||||||
|
if (bqrsInfo["result-sets"].length !== 1) {
|
||||||
|
throw new Error(
|
||||||
|
`Expected exactly one result set, got ${bqrsInfo["result-sets"].length}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultSet = bqrsInfo["result-sets"][0];
|
||||||
|
|
||||||
|
const decodedResults = await cliServer.bqrsDecode(bqrsPath, resultSet.name);
|
||||||
|
|
||||||
|
const results = decodedResults.tuples;
|
||||||
|
|
||||||
|
return (
|
||||||
|
results
|
||||||
|
// This is just a sanity check. The query should only return strings.
|
||||||
|
.filter((result) => typeof result[0] === "string")
|
||||||
|
.map((result) => {
|
||||||
|
const row = result[0] as string;
|
||||||
|
|
||||||
|
return definition.readModeledMethod(row.split(";"));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateFlowModel({
|
||||||
|
onResults,
|
||||||
|
...options
|
||||||
|
}: FlowModelOptions) {
|
||||||
|
const summaryResults = await getModeledMethodsFromFlow(
|
||||||
|
"summary",
|
||||||
|
"CaptureSummaryModels.ql",
|
||||||
|
0,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
if (summaryResults) {
|
||||||
|
await onResults(summaryResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sinkResults = await getModeledMethodsFromFlow(
|
||||||
|
"sink",
|
||||||
|
"CaptureSinkModels.ql",
|
||||||
|
1,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
if (sinkResults) {
|
||||||
|
await onResults(sinkResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceResults = await getModeledMethodsFromFlow(
|
||||||
|
"source",
|
||||||
|
"CaptureSourceModels.ql",
|
||||||
|
2,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
if (sourceResults) {
|
||||||
|
await onResults(sourceResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
const neutralResults = await getModeledMethodsFromFlow(
|
||||||
|
"neutral",
|
||||||
|
"CaptureNeutralModels.ql",
|
||||||
|
3,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
if (neutralResults) {
|
||||||
|
await onResults(neutralResults);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,3 +11,8 @@ export type ModeledMethod = {
|
|||||||
output: string;
|
output: string;
|
||||||
kind: string;
|
kind: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ModeledMethodWithSignature = {
|
||||||
|
signature: string;
|
||||||
|
modeledMethod: ModeledMethod;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,27 +1,31 @@
|
|||||||
import { ExternalApiUsage } from "./external-api-usage";
|
import { ExternalApiUsage } from "./external-api-usage";
|
||||||
import { ModeledMethod, ModeledMethodType } from "./modeled-method";
|
import {
|
||||||
|
ModeledMethod,
|
||||||
|
ModeledMethodType,
|
||||||
|
ModeledMethodWithSignature,
|
||||||
|
} from "./modeled-method";
|
||||||
|
|
||||||
type ExternalApiUsageByType = {
|
type ExternalApiUsageByType = {
|
||||||
externalApiUsage: ExternalApiUsage;
|
externalApiUsage: ExternalApiUsage;
|
||||||
modeledMethod: ModeledMethod;
|
modeledMethod: ModeledMethod;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DataExtensionDefinition = {
|
type ExtensiblePredicateDefinition = {
|
||||||
extensible: string;
|
extensiblePredicate: string;
|
||||||
generateMethodDefinition: (method: ExternalApiUsageByType) => any[];
|
generateMethodDefinition: (method: ExternalApiUsageByType) => any[];
|
||||||
readModeledMethod: (row: any[]) => [string, ModeledMethod] | undefined;
|
readModeledMethod: (row: any[]) => ModeledMethodWithSignature;
|
||||||
};
|
};
|
||||||
|
|
||||||
function readRowToMethod(row: any[]): string {
|
function readRowToMethod(row: any[]): string {
|
||||||
return `${row[0]}.${row[1]}#${row[3]}${row[4]}`;
|
return `${row[0]}.${row[1]}#${row[3]}${row[4]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const definitions: Record<
|
export const extensiblePredicateDefinitions: Record<
|
||||||
Exclude<ModeledMethodType, "none">,
|
Exclude<ModeledMethodType, "none">,
|
||||||
DataExtensionDefinition
|
ExtensiblePredicateDefinition
|
||||||
> = {
|
> = {
|
||||||
source: {
|
source: {
|
||||||
extensible: "sourceModel",
|
extensiblePredicate: "sourceModel",
|
||||||
// extensible predicate sourceModel(
|
// extensible predicate sourceModel(
|
||||||
// string package, string type, boolean subtypes, string name, string signature, string ext,
|
// string package, string type, boolean subtypes, string name, string signature, string ext,
|
||||||
// string output, string kind, string provenance
|
// string output, string kind, string provenance
|
||||||
@@ -37,18 +41,18 @@ const definitions: Record<
|
|||||||
method.modeledMethod.kind,
|
method.modeledMethod.kind,
|
||||||
"manual",
|
"manual",
|
||||||
],
|
],
|
||||||
readModeledMethod: (row) => [
|
readModeledMethod: (row) => ({
|
||||||
readRowToMethod(row),
|
signature: readRowToMethod(row),
|
||||||
{
|
modeledMethod: {
|
||||||
type: "source",
|
type: "source",
|
||||||
input: "",
|
input: "",
|
||||||
output: row[6],
|
output: row[6],
|
||||||
kind: row[7],
|
kind: row[7],
|
||||||
},
|
},
|
||||||
],
|
}),
|
||||||
},
|
},
|
||||||
sink: {
|
sink: {
|
||||||
extensible: "sinkModel",
|
extensiblePredicate: "sinkModel",
|
||||||
// extensible predicate sinkModel(
|
// extensible predicate sinkModel(
|
||||||
// string package, string type, boolean subtypes, string name, string signature, string ext,
|
// string package, string type, boolean subtypes, string name, string signature, string ext,
|
||||||
// string input, string kind, string provenance
|
// string input, string kind, string provenance
|
||||||
@@ -64,18 +68,18 @@ const definitions: Record<
|
|||||||
method.modeledMethod.kind,
|
method.modeledMethod.kind,
|
||||||
"manual",
|
"manual",
|
||||||
],
|
],
|
||||||
readModeledMethod: (row) => [
|
readModeledMethod: (row) => ({
|
||||||
readRowToMethod(row),
|
signature: readRowToMethod(row),
|
||||||
{
|
modeledMethod: {
|
||||||
type: "sink",
|
type: "sink",
|
||||||
input: row[6],
|
input: row[6],
|
||||||
output: "",
|
output: "",
|
||||||
kind: row[7],
|
kind: row[7],
|
||||||
},
|
},
|
||||||
],
|
}),
|
||||||
},
|
},
|
||||||
summary: {
|
summary: {
|
||||||
extensible: "summaryModel",
|
extensiblePredicate: "summaryModel",
|
||||||
// extensible predicate summaryModel(
|
// extensible predicate summaryModel(
|
||||||
// string package, string type, boolean subtypes, string name, string signature, string ext,
|
// string package, string type, boolean subtypes, string name, string signature, string ext,
|
||||||
// string input, string output, string kind, string provenance
|
// string input, string output, string kind, string provenance
|
||||||
@@ -92,18 +96,18 @@ const definitions: Record<
|
|||||||
method.modeledMethod.kind,
|
method.modeledMethod.kind,
|
||||||
"manual",
|
"manual",
|
||||||
],
|
],
|
||||||
readModeledMethod: (row) => [
|
readModeledMethod: (row) => ({
|
||||||
readRowToMethod(row),
|
signature: readRowToMethod(row),
|
||||||
{
|
modeledMethod: {
|
||||||
type: "summary",
|
type: "summary",
|
||||||
input: row[6],
|
input: row[6],
|
||||||
output: row[7],
|
output: row[7],
|
||||||
kind: row[8],
|
kind: row[8],
|
||||||
},
|
},
|
||||||
],
|
}),
|
||||||
},
|
},
|
||||||
neutral: {
|
neutral: {
|
||||||
extensible: "neutralModel",
|
extensiblePredicate: "neutralModel",
|
||||||
// extensible predicate neutralModel(
|
// extensible predicate neutralModel(
|
||||||
// string package, string type, string name, string signature, string provenance
|
// string package, string type, string name, string signature, string provenance
|
||||||
// );
|
// );
|
||||||
@@ -114,21 +118,21 @@ const definitions: Record<
|
|||||||
method.externalApiUsage.methodParameters,
|
method.externalApiUsage.methodParameters,
|
||||||
"manual",
|
"manual",
|
||||||
],
|
],
|
||||||
readModeledMethod: (row) => [
|
readModeledMethod: (row) => ({
|
||||||
`${row[0]}.${row[1]}#${row[2]}${row[3]}`,
|
signature: `${row[0]}.${row[1]}#${row[2]}${row[3]}`,
|
||||||
{
|
modeledMethod: {
|
||||||
type: "neutral",
|
type: "neutral",
|
||||||
input: "",
|
input: "",
|
||||||
output: "",
|
output: "",
|
||||||
kind: "",
|
kind: "",
|
||||||
},
|
},
|
||||||
],
|
}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function createDataProperty(
|
function createDataProperty(
|
||||||
methods: ExternalApiUsageByType[],
|
methods: ExternalApiUsageByType[],
|
||||||
definition: DataExtensionDefinition,
|
definition: ExtensiblePredicateDefinition,
|
||||||
) {
|
) {
|
||||||
if (methods.length === 0) {
|
if (methods.length === 0) {
|
||||||
return " []";
|
return " []";
|
||||||
@@ -169,10 +173,10 @@ export function createDataExtensionYaml(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const extensions = Object.entries(definitions).map(
|
const extensions = Object.entries(extensiblePredicateDefinitions).map(
|
||||||
([type, definition]) => ` - addsTo:
|
([type, definition]) => ` - addsTo:
|
||||||
pack: codeql/java-all
|
pack: codeql/java-all
|
||||||
extensible: ${definition.extensible}
|
extensible: ${definition.extensiblePredicate}
|
||||||
data:${createDataProperty(
|
data:${createDataProperty(
|
||||||
methodsByType[type as Exclude<ModeledMethodType, "none">],
|
methodsByType[type as Exclude<ModeledMethodType, "none">],
|
||||||
definition,
|
definition,
|
||||||
@@ -214,8 +218,8 @@ export function loadDataExtensionYaml(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const definition = Object.values(definitions).find(
|
const definition = Object.values(extensiblePredicateDefinitions).find(
|
||||||
(definition) => definition.extensible === extensible,
|
(definition) => definition.extensiblePredicate === extensible,
|
||||||
);
|
);
|
||||||
if (!definition) {
|
if (!definition) {
|
||||||
continue;
|
continue;
|
||||||
@@ -227,9 +231,9 @@ export function loadDataExtensionYaml(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [apiInfo, modeledMethod] = result;
|
const { signature, modeledMethod } = result;
|
||||||
|
|
||||||
modeledMethods[apiInfo] = modeledMethod;
|
modeledMethods[signature] = modeledMethod;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -215,6 +215,9 @@ export class DistributionManager implements DistributionProvider {
|
|||||||
minSecondsSinceLastUpdateCheck: number,
|
minSecondsSinceLastUpdateCheck: number,
|
||||||
): Promise<DistributionUpdateCheckResult> {
|
): Promise<DistributionUpdateCheckResult> {
|
||||||
const distribution = await this.getDistributionWithoutVersionCheck();
|
const distribution = await this.getDistributionWithoutVersionCheck();
|
||||||
|
if (distribution === undefined) {
|
||||||
|
minSecondsSinceLastUpdateCheck = 0;
|
||||||
|
}
|
||||||
const extensionManagedCodeQlPath =
|
const extensionManagedCodeQlPath =
|
||||||
await this.extensionSpecificDistributionManager.getCodeQlPathWithoutVersionCheck();
|
await this.extensionSpecificDistributionManager.getCodeQlPathWithoutVersionCheck();
|
||||||
if (distribution?.codeQlPath !== extensionManagedCodeQlPath) {
|
if (distribution?.codeQlPath !== extensionManagedCodeQlPath) {
|
||||||
|
|||||||
@@ -884,6 +884,7 @@ async function activateWithInstalledDistribution(
|
|||||||
const dataExtensionsEditorModule =
|
const dataExtensionsEditorModule =
|
||||||
await DataExtensionsEditorModule.initialize(
|
await DataExtensionsEditorModule.initialize(
|
||||||
ctx,
|
ctx,
|
||||||
|
app,
|
||||||
dbm,
|
dbm,
|
||||||
cliServer,
|
cliServer,
|
||||||
qs,
|
qs,
|
||||||
|
|||||||
@@ -478,7 +478,7 @@ function findStandardQueryPack(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getQlPackForDbscheme(
|
export async function getQlPackForDbscheme(
|
||||||
cliServer: CodeQLCliServer,
|
cliServer: Pick<CodeQLCliServer, "resolveQlpacks">,
|
||||||
dbschemePath: string,
|
dbschemePath: string,
|
||||||
): Promise<QlPacksForLanguage> {
|
): Promise<QlPacksForLanguage> {
|
||||||
const qlpacks = await cliServer.resolveQlpacks(getOnDiskWorkspaceFolders());
|
const qlpacks = await cliServer.resolveQlpacks(getOnDiskWorkspaceFolders());
|
||||||
|
|||||||
@@ -493,6 +493,19 @@ export interface ShowProgressMessage {
|
|||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AddModeledMethodsMessage {
|
||||||
|
t: "addModeledMethods";
|
||||||
|
modeledMethods: Record<string, ModeledMethod>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If true, then any existing modeled methods set to "none" will be
|
||||||
|
* overwritten by the new modeled methods. Otherwise, the "none" modeled
|
||||||
|
* methods will not be overwritten, even if the new modeled methods
|
||||||
|
* contain a better model.
|
||||||
|
*/
|
||||||
|
overrideNone?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface JumpToUsageMessage {
|
export interface JumpToUsageMessage {
|
||||||
t: "jumpToUsage";
|
t: "jumpToUsage";
|
||||||
location: ResolvableLocationValue;
|
location: ResolvableLocationValue;
|
||||||
@@ -509,12 +522,17 @@ export interface SaveModeledMethods {
|
|||||||
modeledMethods: Record<string, ModeledMethod>;
|
modeledMethods: Record<string, ModeledMethod>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GenerateExternalApiMessage {
|
||||||
|
t: "generateExternalApi";
|
||||||
|
}
|
||||||
|
|
||||||
export type ToDataExtensionsEditorMessage =
|
export type ToDataExtensionsEditorMessage =
|
||||||
| SetExternalApiUsagesMessage
|
| SetExternalApiUsagesMessage
|
||||||
| ShowProgressMessage
|
| ShowProgressMessage
|
||||||
| SetExistingModeledMethods;
|
| AddModeledMethodsMessage;
|
||||||
|
|
||||||
export type FromDataExtensionsEditorMessage =
|
export type FromDataExtensionsEditorMessage =
|
||||||
| ViewLoadedMsg
|
| ViewLoadedMsg
|
||||||
| JumpToUsageMessage
|
| JumpToUsageMessage
|
||||||
| SaveModeledMethods;
|
| SaveModeledMethods
|
||||||
|
| GenerateExternalApiMessage;
|
||||||
|
|||||||
@@ -0,0 +1,223 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { ComponentMeta, ComponentStory } from "@storybook/react";
|
||||||
|
|
||||||
|
import { DataExtensionsEditor as DataExtensionsEditorComponent } from "../../view/data-extensions-editor/DataExtensionsEditor";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Data Extensions Editor/Data Extensions Editor",
|
||||||
|
component: DataExtensionsEditorComponent,
|
||||||
|
} as ComponentMeta<typeof DataExtensionsEditorComponent>;
|
||||||
|
|
||||||
|
const Template: ComponentStory<typeof DataExtensionsEditorComponent> = (
|
||||||
|
args,
|
||||||
|
) => <DataExtensionsEditorComponent {...args} />;
|
||||||
|
|
||||||
|
export const DataExtensionsEditor = Template.bind({});
|
||||||
|
DataExtensionsEditor.args = {
|
||||||
|
initialExternalApiUsages: [
|
||||||
|
{
|
||||||
|
signature: "org.sql2o.Connection#createQuery(String)",
|
||||||
|
packageName: "org.sql2o",
|
||||||
|
typeName: "Connection",
|
||||||
|
methodName: "createQuery",
|
||||||
|
methodParameters: "(String)",
|
||||||
|
supported: true,
|
||||||
|
usages: [
|
||||||
|
{
|
||||||
|
label: "createQuery(...)",
|
||||||
|
url: {
|
||||||
|
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||||
|
startLine: 15,
|
||||||
|
startColumn: 13,
|
||||||
|
endLine: 15,
|
||||||
|
endColumn: 56,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "createQuery(...)",
|
||||||
|
url: {
|
||||||
|
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||||
|
startLine: 26,
|
||||||
|
startColumn: 13,
|
||||||
|
endLine: 26,
|
||||||
|
endColumn: 39,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
signature: "org.sql2o.Query#executeScalar(Class)",
|
||||||
|
packageName: "org.sql2o",
|
||||||
|
typeName: "Query",
|
||||||
|
methodName: "executeScalar",
|
||||||
|
methodParameters: "(Class)",
|
||||||
|
supported: true,
|
||||||
|
usages: [
|
||||||
|
{
|
||||||
|
label: "executeScalar(...)",
|
||||||
|
url: {
|
||||||
|
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||||
|
startLine: 15,
|
||||||
|
startColumn: 13,
|
||||||
|
endLine: 15,
|
||||||
|
endColumn: 85,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "executeScalar(...)",
|
||||||
|
url: {
|
||||||
|
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||||
|
startLine: 26,
|
||||||
|
startColumn: 13,
|
||||||
|
endLine: 26,
|
||||||
|
endColumn: 68,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
signature: "org.sql2o.Sql2o#open()",
|
||||||
|
packageName: "org.sql2o",
|
||||||
|
typeName: "Sql2o",
|
||||||
|
methodName: "open",
|
||||||
|
methodParameters: "()",
|
||||||
|
supported: false,
|
||||||
|
usages: [
|
||||||
|
{
|
||||||
|
label: "open(...)",
|
||||||
|
url: {
|
||||||
|
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||||
|
startLine: 14,
|
||||||
|
startColumn: 24,
|
||||||
|
endLine: 14,
|
||||||
|
endColumn: 35,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "open(...)",
|
||||||
|
url: {
|
||||||
|
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||||
|
startLine: 25,
|
||||||
|
startColumn: 24,
|
||||||
|
endLine: 25,
|
||||||
|
endColumn: 35,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
signature: "java.io.PrintStream#println(String)",
|
||||||
|
packageName: "java.io",
|
||||||
|
typeName: "PrintStream",
|
||||||
|
methodName: "println",
|
||||||
|
methodParameters: "(String)",
|
||||||
|
supported: true,
|
||||||
|
usages: [
|
||||||
|
{
|
||||||
|
label: "println(...)",
|
||||||
|
url: {
|
||||||
|
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||||
|
startLine: 29,
|
||||||
|
startColumn: 9,
|
||||||
|
endLine: 29,
|
||||||
|
endColumn: 49,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
signature:
|
||||||
|
"org.springframework.boot.SpringApplication#run(Class,String[])",
|
||||||
|
packageName: "org.springframework.boot",
|
||||||
|
typeName: "SpringApplication",
|
||||||
|
methodName: "run",
|
||||||
|
methodParameters: "(Class,String[])",
|
||||||
|
supported: false,
|
||||||
|
usages: [
|
||||||
|
{
|
||||||
|
label: "run(...)",
|
||||||
|
url: {
|
||||||
|
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/Sql2oExampleApplication.java",
|
||||||
|
startLine: 9,
|
||||||
|
startColumn: 9,
|
||||||
|
endLine: 9,
|
||||||
|
endColumn: 66,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
signature: "org.sql2o.Sql2o#Sql2o(String,String,String)",
|
||||||
|
packageName: "org.sql2o",
|
||||||
|
typeName: "Sql2o",
|
||||||
|
methodName: "Sql2o",
|
||||||
|
methodParameters: "(String,String,String)",
|
||||||
|
supported: false,
|
||||||
|
usages: [
|
||||||
|
{
|
||||||
|
label: "new Sql2o(...)",
|
||||||
|
url: {
|
||||||
|
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||||
|
startLine: 10,
|
||||||
|
startColumn: 33,
|
||||||
|
endLine: 10,
|
||||||
|
endColumn: 88,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
signature: "org.sql2o.Sql2o#Sql2o(String)",
|
||||||
|
packageName: "org.sql2o",
|
||||||
|
typeName: "Sql2o",
|
||||||
|
methodName: "Sql2o",
|
||||||
|
methodParameters: "(String)",
|
||||||
|
supported: false,
|
||||||
|
usages: [
|
||||||
|
{
|
||||||
|
label: "new Sql2o(...)",
|
||||||
|
url: {
|
||||||
|
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||||
|
startLine: 23,
|
||||||
|
startColumn: 23,
|
||||||
|
endLine: 23,
|
||||||
|
endColumn: 36,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
initialModeledMethods: {
|
||||||
|
"org.sql2o.Sql2o#Sql2o(String)": {
|
||||||
|
type: "sink",
|
||||||
|
input: "Argument[0]",
|
||||||
|
output: "",
|
||||||
|
kind: "jndi-injection",
|
||||||
|
},
|
||||||
|
"org.sql2o.Connection#createQuery(String)": {
|
||||||
|
type: "summary",
|
||||||
|
input: "Argument[-1]",
|
||||||
|
output: "ReturnValue",
|
||||||
|
kind: "taint",
|
||||||
|
},
|
||||||
|
"org.sql2o.Sql2o#open()": {
|
||||||
|
type: "summary",
|
||||||
|
input: "Argument[-1]",
|
||||||
|
output: "ReturnValue",
|
||||||
|
kind: "taint",
|
||||||
|
},
|
||||||
|
"org.sql2o.Query#executeScalar(Class)": {
|
||||||
|
type: "neutral",
|
||||||
|
input: "",
|
||||||
|
output: "",
|
||||||
|
kind: "",
|
||||||
|
},
|
||||||
|
"org.sql2o.Sql2o#Sql2o(String,String,String)": {
|
||||||
|
type: "neutral",
|
||||||
|
input: "",
|
||||||
|
output: "",
|
||||||
|
kind: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { ComponentMeta, ComponentStory } from "@storybook/react";
|
||||||
|
|
||||||
|
import { MethodRow as MethodRowComponent } from "../../view/data-extensions-editor/MethodRow";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Data Extensions Editor/Method Row",
|
||||||
|
component: MethodRowComponent,
|
||||||
|
} as ComponentMeta<typeof MethodRowComponent>;
|
||||||
|
|
||||||
|
const Template: ComponentStory<typeof MethodRowComponent> = (args) => (
|
||||||
|
<MethodRowComponent {...args} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export const MethodRow = Template.bind({});
|
||||||
|
MethodRow.args = {
|
||||||
|
externalApiUsage: {
|
||||||
|
signature: "org.sql2o.Sql2o#open()",
|
||||||
|
packageName: "org.sql2o",
|
||||||
|
typeName: "Sql2o",
|
||||||
|
methodName: "open",
|
||||||
|
methodParameters: "()",
|
||||||
|
supported: true,
|
||||||
|
usages: [
|
||||||
|
{
|
||||||
|
label: "open(...)",
|
||||||
|
url: {
|
||||||
|
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||||
|
startLine: 14,
|
||||||
|
startColumn: 24,
|
||||||
|
endLine: 14,
|
||||||
|
endColumn: 35,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "open(...)",
|
||||||
|
url: {
|
||||||
|
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||||
|
startLine: 25,
|
||||||
|
startColumn: 24,
|
||||||
|
endLine: 25,
|
||||||
|
endColumn: 35,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
modeledMethod: {
|
||||||
|
type: "summary",
|
||||||
|
input: "Argument[-1]",
|
||||||
|
output: "ReturnValue",
|
||||||
|
kind: "taint",
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -33,13 +33,21 @@ const ProgressBar = styled.div<ProgressBarProps>`
|
|||||||
background-color: var(--vscode-progressBar-background);
|
background-color: var(--vscode-progressBar-background);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export function DataExtensionsEditor(): JSX.Element {
|
type Props = {
|
||||||
|
initialExternalApiUsages?: ExternalApiUsage[];
|
||||||
|
initialModeledMethods?: Record<string, ModeledMethod>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function DataExtensionsEditor({
|
||||||
|
initialExternalApiUsages = [],
|
||||||
|
initialModeledMethods = {},
|
||||||
|
}: Props): JSX.Element {
|
||||||
const [externalApiUsages, setExternalApiUsages] = useState<
|
const [externalApiUsages, setExternalApiUsages] = useState<
|
||||||
ExternalApiUsage[]
|
ExternalApiUsage[]
|
||||||
>([]);
|
>(initialExternalApiUsages);
|
||||||
const [modeledMethods, setModeledMethods] = useState<
|
const [modeledMethods, setModeledMethods] = useState<
|
||||||
Record<string, ModeledMethod>
|
Record<string, ModeledMethod>
|
||||||
>({});
|
>(initialModeledMethods);
|
||||||
const [progress, setProgress] = useState<Omit<ShowProgressMessage, "t">>({
|
const [progress, setProgress] = useState<Omit<ShowProgressMessage, "t">>({
|
||||||
step: 0,
|
step: 0,
|
||||||
maxStep: 0,
|
maxStep: 0,
|
||||||
@@ -57,14 +65,21 @@ export function DataExtensionsEditor(): JSX.Element {
|
|||||||
case "showProgress":
|
case "showProgress":
|
||||||
setProgress(msg);
|
setProgress(msg);
|
||||||
break;
|
break;
|
||||||
case "setExistingModeledMethods":
|
case "addModeledMethods":
|
||||||
setModeledMethods((oldModeledMethods) => {
|
setModeledMethods((oldModeledMethods) => {
|
||||||
|
const filteredOldModeledMethods = msg.overrideNone
|
||||||
|
? Object.fromEntries(
|
||||||
|
Object.entries(oldModeledMethods).filter(
|
||||||
|
([, value]) => value.type !== "none",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: oldModeledMethods;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...msg.existingModeledMethods,
|
...msg.modeledMethods,
|
||||||
...oldModeledMethods,
|
...filteredOldModeledMethods,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
assertNever(msg);
|
assertNever(msg);
|
||||||
@@ -107,6 +122,12 @@ export function DataExtensionsEditor(): JSX.Element {
|
|||||||
});
|
});
|
||||||
}, [externalApiUsages, modeledMethods]);
|
}, [externalApiUsages, modeledMethods]);
|
||||||
|
|
||||||
|
const onGenerateClick = useCallback(() => {
|
||||||
|
vscode.postMessage({
|
||||||
|
t: "generateExternalApi",
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataExtensionsEditorContainer>
|
<DataExtensionsEditorContainer>
|
||||||
{progress.maxStep > 0 && (
|
{progress.maxStep > 0 && (
|
||||||
@@ -128,6 +149,12 @@ export function DataExtensionsEditor(): JSX.Element {
|
|||||||
<div>
|
<div>
|
||||||
<h3>External API modelling</h3>
|
<h3>External API modelling</h3>
|
||||||
<VSCodeButton onClick={onApplyClick}>Apply</VSCodeButton>
|
<VSCodeButton onClick={onApplyClick}>Apply</VSCodeButton>
|
||||||
|
|
||||||
|
<VSCodeButton onClick={onGenerateClick}>
|
||||||
|
Download and generate
|
||||||
|
</VSCodeButton>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
<VSCodeDataGrid>
|
<VSCodeDataGrid>
|
||||||
<VSCodeDataGridRow rowType="header">
|
<VSCodeDataGridRow rowType="header">
|
||||||
<VSCodeDataGridCell cellType="columnheader" gridColumn={1}>
|
<VSCodeDataGridCell cellType="columnheader" gridColumn={1}>
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ export const DataFlowPaths = ({
|
|||||||
|
|
||||||
const { codeFlows, ruleDescription, message, severity } = dataFlowPaths;
|
const { codeFlows, ruleDescription, message, severity } = dataFlowPaths;
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
// Make sure to update the selected code flow if the data flow paths change
|
||||||
|
setSelectedCodeFlow(dataFlowPaths.codeFlows[0]);
|
||||||
|
}, [dataFlowPaths]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<VerticalSpace size={2} />
|
<VerticalSpace size={2} />
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ function renderResultCountString(resultSet: ResultSet): JSX.Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getInterpretedTableName(interpretation: Interpretation): string {
|
function getInterpretedTableName(interpretation: Interpretation): string {
|
||||||
return interpretation?.data.t === "GraphInterpretationData"
|
return interpretation.data.t === "GraphInterpretationData"
|
||||||
? GRAPH_TABLE_NAME
|
? GRAPH_TABLE_NAME
|
||||||
: ALERTS_TABLE_NAME;
|
: ALERTS_TABLE_NAME;
|
||||||
}
|
}
|
||||||
@@ -101,7 +101,7 @@ function getResultSets(
|
|||||||
): ResultSet[] {
|
): ResultSet[] {
|
||||||
const resultSets: ResultSet[] =
|
const resultSets: ResultSet[] =
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore 2783
|
// @ts-ignore 2783 Avoid compilation error for overwriting the t property
|
||||||
rawResultSets.map((rs) => ({ t: "RawResultSet", ...rs }));
|
rawResultSets.map((rs) => ({ t: "RawResultSet", ...rs }));
|
||||||
|
|
||||||
if (interpretation !== undefined) {
|
if (interpretation !== undefined) {
|
||||||
|
|||||||
@@ -0,0 +1,222 @@
|
|||||||
|
import {
|
||||||
|
readQueryResults,
|
||||||
|
runQuery,
|
||||||
|
} from "../../../../src/data-extensions-editor/external-api-usage-query";
|
||||||
|
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 { load } from "js-yaml";
|
||||||
|
|
||||||
|
function createMockUri(path = "/a/b/c/foo"): Uri {
|
||||||
|
return {
|
||||||
|
scheme: "file",
|
||||||
|
authority: "",
|
||||||
|
path,
|
||||||
|
query: "",
|
||||||
|
fragment: "",
|
||||||
|
fsPath: path,
|
||||||
|
with: jest.fn(),
|
||||||
|
toJSON: jest.fn(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("runQuery", () => {
|
||||||
|
it("runs the query", async () => {
|
||||||
|
jest.spyOn(queryResolver, "qlpackOfDatabase").mockResolvedValue({
|
||||||
|
dbschemePack: "codeql/java-all",
|
||||||
|
dbschemePackIsLibraryPack: false,
|
||||||
|
queryPack: "codeql/java-queries",
|
||||||
|
});
|
||||||
|
|
||||||
|
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,
|
||||||
|
}),
|
||||||
|
outputDir: {
|
||||||
|
logPath,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
logger: createMockLogger(),
|
||||||
|
},
|
||||||
|
logger: createMockLogger(),
|
||||||
|
databaseItem: {
|
||||||
|
databaseUri: createMockUri("/a/b/c/src.zip"),
|
||||||
|
contents: {
|
||||||
|
kind: DatabaseKind.Database,
|
||||||
|
name: "foo",
|
||||||
|
datasetUri: createMockUri(),
|
||||||
|
},
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: "codeql/java-queries",
|
||||||
|
queries: ".",
|
||||||
|
include: {
|
||||||
|
id: "java/telemetry/fetch-external-apis",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("readQueryResults", () => {
|
||||||
|
const options = {
|
||||||
|
cliServer: {
|
||||||
|
bqrsInfo: jest.fn(),
|
||||||
|
bqrsDecode: jest.fn(),
|
||||||
|
},
|
||||||
|
bqrsPath: "/tmp/results.bqrs",
|
||||||
|
logger: createMockLogger(),
|
||||||
|
};
|
||||||
|
|
||||||
|
it("returns undefined when there are no results", async () => {
|
||||||
|
options.cliServer.bqrsInfo.mockResolvedValue({
|
||||||
|
"result-sets": [],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await readQueryResults(options)).toBeUndefined();
|
||||||
|
expect(options.logger.log).toHaveBeenCalledWith(
|
||||||
|
expect.stringMatching(/Expected exactly one result set/),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns undefined when there are multiple result sets", async () => {
|
||||||
|
options.cliServer.bqrsInfo.mockResolvedValue({
|
||||||
|
"result-sets": [
|
||||||
|
{
|
||||||
|
name: "#select",
|
||||||
|
rows: 10,
|
||||||
|
columns: [
|
||||||
|
{ name: "apiName", kind: "s" },
|
||||||
|
{ name: "supported", kind: "b" },
|
||||||
|
{ name: "usage", kind: "e" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "#select2",
|
||||||
|
rows: 10,
|
||||||
|
columns: [
|
||||||
|
{ name: "apiName", kind: "s" },
|
||||||
|
{ name: "supported", kind: "b" },
|
||||||
|
{ name: "usage", kind: "e" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await readQueryResults(options)).toBeUndefined();
|
||||||
|
expect(options.logger.log).toHaveBeenCalledWith(
|
||||||
|
expect.stringMatching(/Expected exactly one result set/),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("gets the result set", async () => {
|
||||||
|
options.cliServer.bqrsInfo.mockResolvedValue({
|
||||||
|
"result-sets": [
|
||||||
|
{
|
||||||
|
name: "#select",
|
||||||
|
rows: 10,
|
||||||
|
columns: [
|
||||||
|
{ name: "apiName", kind: "s" },
|
||||||
|
{ name: "supported", kind: "b" },
|
||||||
|
{ name: "usage", kind: "e" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"compatible-query-kinds": ["Table", "Tree", "Graph"],
|
||||||
|
});
|
||||||
|
const decodedResultSet = {
|
||||||
|
columns: [
|
||||||
|
{ name: "apiName", kind: "String" },
|
||||||
|
{ name: "supported", kind: "Boolean" },
|
||||||
|
{ name: "usage", kind: "Entity" },
|
||||||
|
],
|
||||||
|
tuples: [
|
||||||
|
[
|
||||||
|
"java.io.PrintStream#println(String)",
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
label: "println(...)",
|
||||||
|
url: {
|
||||||
|
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||||
|
startLine: 29,
|
||||||
|
startColumn: 9,
|
||||||
|
endLine: 29,
|
||||||
|
endColumn: 49,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
options.cliServer.bqrsDecode.mockResolvedValue(decodedResultSet);
|
||||||
|
|
||||||
|
const result = await readQueryResults(options);
|
||||||
|
expect(result).toEqual(decodedResultSet);
|
||||||
|
expect(options.cliServer.bqrsInfo).toHaveBeenCalledWith(options.bqrsPath);
|
||||||
|
expect(options.cliServer.bqrsDecode).toHaveBeenCalledWith(
|
||||||
|
options.bqrsPath,
|
||||||
|
"#select",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user