Merge branch 'main' into robertbrignull/data_new_header
This commit is contained in:
@@ -318,7 +318,6 @@ This requires running a MRVA query and seeing the results view.
|
|||||||
1. Alphabetically
|
1. Alphabetically
|
||||||
2. By number of results
|
2. By number of results
|
||||||
3. By popularity
|
3. By popularity
|
||||||
4. By most recent commit
|
|
||||||
9. Can filter repos
|
9. Can filter repos
|
||||||
10. Shows correct statistics
|
10. Shows correct statistics
|
||||||
1. Total number of results
|
1. Total number of results
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
## [UNRELEASED]
|
## [UNRELEASED]
|
||||||
|
|
||||||
|
- Remove "last updated" information and sorting from variant analysis results view. [#2637](https://github.com/github/vscode-codeql/pull/2637)
|
||||||
- Links to code on GitHub now include column numbers as well as line numbers. [#2406](https://github.com/github/vscode-codeql/pull/2406)
|
- Links to code on GitHub now include column numbers as well as line numbers. [#2406](https://github.com/github/vscode-codeql/pull/2406)
|
||||||
- No longer highlight trailing commas for jump to definition. [#2615](https://github.com/github/vscode-codeql/pull/2615)
|
- No longer highlight trailing commas for jump to definition. [#2615](https://github.com/github/vscode-codeql/pull/2615)
|
||||||
|
|
||||||
|
|||||||
@@ -350,13 +350,11 @@
|
|||||||
"enum": [
|
"enum": [
|
||||||
"alphabetically",
|
"alphabetically",
|
||||||
"popularity",
|
"popularity",
|
||||||
"mostRecentCommit",
|
|
||||||
"numberOfResults"
|
"numberOfResults"
|
||||||
],
|
],
|
||||||
"enumDescriptions": [
|
"enumDescriptions": [
|
||||||
"Sort repositories alphabetically in the results view.",
|
"Sort repositories alphabetically in the results view.",
|
||||||
"Sort repositories by popularity in the results view.",
|
"Sort repositories by popularity in the results view.",
|
||||||
"Sort repositories by most recent commit in the results view.",
|
|
||||||
"Sort repositories by number of results in the results view."
|
"Sort repositories by number of results in the results view."
|
||||||
],
|
],
|
||||||
"description": "The default sorting order for repositories in the variant analysis results view."
|
"description": "The default sorting order for repositories in the variant analysis results view."
|
||||||
|
|||||||
@@ -500,13 +500,6 @@ interface SetExternalApiUsagesMessage {
|
|||||||
externalApiUsages: ExternalApiUsage[];
|
externalApiUsages: ExternalApiUsage[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ShowProgressMessage {
|
|
||||||
t: "showProgress";
|
|
||||||
step: number;
|
|
||||||
maxStep: number;
|
|
||||||
message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LoadModeledMethodsMessage {
|
interface LoadModeledMethodsMessage {
|
||||||
t: "loadModeledMethods";
|
t: "loadModeledMethods";
|
||||||
modeledMethods: Record<string, ModeledMethod>;
|
modeledMethods: Record<string, ModeledMethod>;
|
||||||
@@ -562,7 +555,6 @@ interface ModelDependencyMessage {
|
|||||||
export type ToDataExtensionsEditorMessage =
|
export type ToDataExtensionsEditorMessage =
|
||||||
| SetExtensionPackStateMessage
|
| SetExtensionPackStateMessage
|
||||||
| SetExternalApiUsagesMessage
|
| SetExternalApiUsagesMessage
|
||||||
| ShowProgressMessage
|
|
||||||
| LoadModeledMethodsMessage
|
| LoadModeledMethodsMessage
|
||||||
| AddModeledMethodsMessage;
|
| AddModeledMethodsMessage;
|
||||||
|
|
||||||
|
|||||||
@@ -14,11 +14,7 @@ import {
|
|||||||
FromDataExtensionsEditorMessage,
|
FromDataExtensionsEditorMessage,
|
||||||
ToDataExtensionsEditorMessage,
|
ToDataExtensionsEditorMessage,
|
||||||
} from "../common/interface-types";
|
} from "../common/interface-types";
|
||||||
import {
|
import { ProgressCallback, withProgress } from "../common/vscode/progress";
|
||||||
ProgressCallback,
|
|
||||||
ProgressUpdate,
|
|
||||||
withProgress,
|
|
||||||
} from "../common/vscode/progress";
|
|
||||||
import { QueryRunner } from "../query-server";
|
import { QueryRunner } from "../query-server";
|
||||||
import {
|
import {
|
||||||
showAndLogExceptionWithTelemetry,
|
showAndLogExceptionWithTelemetry,
|
||||||
@@ -232,203 +228,205 @@ export class DataExtensionsEditorView extends AbstractWebview<
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async loadExternalApiUsages(): Promise<void> {
|
protected async loadExternalApiUsages(): Promise<void> {
|
||||||
const cancellationTokenSource = new CancellationTokenSource();
|
await withProgress(
|
||||||
|
async (progress) => {
|
||||||
|
try {
|
||||||
|
const cancellationTokenSource = new CancellationTokenSource();
|
||||||
|
const queryResult = await runQuery(
|
||||||
|
this.mode === Mode.Framework
|
||||||
|
? "frameworkModeQuery"
|
||||||
|
: "applicationModeQuery",
|
||||||
|
{
|
||||||
|
cliServer: this.cliServer,
|
||||||
|
queryRunner: this.queryRunner,
|
||||||
|
databaseItem: this.databaseItem,
|
||||||
|
queryStorageDir: this.queryStorageDir,
|
||||||
|
progress: (update) => progress({ ...update, maxStep: 1500 }),
|
||||||
|
token: cancellationTokenSource.token,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!queryResult) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
progress({
|
||||||
const queryResult = await runQuery(
|
message: "Decoding results",
|
||||||
this.mode === Mode.Framework
|
step: 1100,
|
||||||
? "frameworkModeQuery"
|
maxStep: 1500,
|
||||||
: "applicationModeQuery",
|
});
|
||||||
{
|
|
||||||
cliServer: this.cliServer,
|
|
||||||
queryRunner: this.queryRunner,
|
|
||||||
databaseItem: this.databaseItem,
|
|
||||||
queryStorageDir: this.queryStorageDir,
|
|
||||||
progress: (progressUpdate: ProgressUpdate) => {
|
|
||||||
void this.showProgress(progressUpdate, 1500);
|
|
||||||
},
|
|
||||||
token: cancellationTokenSource.token,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (!queryResult) {
|
|
||||||
await this.clearProgress();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.showProgress({
|
const bqrsChunk = await readQueryResults({
|
||||||
message: "Decoding results",
|
cliServer: this.cliServer,
|
||||||
step: 1100,
|
bqrsPath: queryResult.outputDir.bqrsPath,
|
||||||
maxStep: 1500,
|
});
|
||||||
});
|
if (!bqrsChunk) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const bqrsChunk = await readQueryResults({
|
progress({
|
||||||
cliServer: this.cliServer,
|
message: "Finalizing results",
|
||||||
bqrsPath: queryResult.outputDir.bqrsPath,
|
step: 1450,
|
||||||
});
|
maxStep: 1500,
|
||||||
if (!bqrsChunk) {
|
});
|
||||||
await this.clearProgress();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.showProgress({
|
const externalApiUsages = decodeBqrsToExternalApiUsages(bqrsChunk);
|
||||||
message: "Finalizing results",
|
|
||||||
step: 1450,
|
|
||||||
maxStep: 1500,
|
|
||||||
});
|
|
||||||
|
|
||||||
const externalApiUsages = decodeBqrsToExternalApiUsages(bqrsChunk);
|
await this.postMessage({
|
||||||
|
t: "setExternalApiUsages",
|
||||||
await this.postMessage({
|
externalApiUsages,
|
||||||
t: "setExternalApiUsages",
|
});
|
||||||
externalApiUsages,
|
} catch (err) {
|
||||||
});
|
void showAndLogExceptionWithTelemetry(
|
||||||
|
this.app.logger,
|
||||||
await this.clearProgress();
|
this.app.telemetry,
|
||||||
} catch (err) {
|
redactableError(
|
||||||
void showAndLogExceptionWithTelemetry(
|
asError(err),
|
||||||
this.app.logger,
|
)`Failed to load external API usages: ${getErrorMessage(err)}`,
|
||||||
this.app.telemetry,
|
);
|
||||||
redactableError(
|
}
|
||||||
asError(err),
|
},
|
||||||
)`Failed to load external API usages: ${getErrorMessage(err)}`,
|
{ cancellable: false },
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async generateModeledMethods(): Promise<void> {
|
protected async generateModeledMethods(): Promise<void> {
|
||||||
const tokenSource = new CancellationTokenSource();
|
await withProgress(
|
||||||
|
async (progress) => {
|
||||||
|
const tokenSource = new CancellationTokenSource();
|
||||||
|
|
||||||
let addedDatabase: DatabaseItem | undefined;
|
let addedDatabase: DatabaseItem | undefined;
|
||||||
|
|
||||||
// In application mode, we need the database of a specific library to generate
|
// In application mode, we need the database of a specific library to generate
|
||||||
// the modeled methods. In framework mode, we'll use the current database.
|
// the modeled methods. In framework mode, we'll use the current database.
|
||||||
if (this.mode === Mode.Application) {
|
if (this.mode === Mode.Application) {
|
||||||
addedDatabase = await this.promptImportDatabase((update) =>
|
addedDatabase = await this.promptImportDatabase(progress);
|
||||||
this.showProgress(update),
|
if (!addedDatabase) {
|
||||||
);
|
return;
|
||||||
if (!addedDatabase) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.showProgress({
|
|
||||||
step: 0,
|
|
||||||
maxStep: 4000,
|
|
||||||
message: "Generating modeled methods for library",
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
await generateFlowModel({
|
|
||||||
cliServer: this.cliServer,
|
|
||||||
queryRunner: this.queryRunner,
|
|
||||||
queryStorageDir: this.queryStorageDir,
|
|
||||||
databaseItem: addedDatabase ?? this.databaseItem,
|
|
||||||
onResults: async (modeledMethods) => {
|
|
||||||
const modeledMethodsByName: Record<string, ModeledMethod> = {};
|
|
||||||
|
|
||||||
for (const modeledMethod of modeledMethods) {
|
|
||||||
modeledMethodsByName[modeledMethod.signature] = modeledMethod;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await this.postMessage({
|
progress({
|
||||||
t: "addModeledMethods",
|
step: 0,
|
||||||
modeledMethods: modeledMethodsByName,
|
maxStep: 4000,
|
||||||
|
message: "Generating modeled methods for library",
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await generateFlowModel({
|
||||||
|
cliServer: this.cliServer,
|
||||||
|
queryRunner: this.queryRunner,
|
||||||
|
queryStorageDir: this.queryStorageDir,
|
||||||
|
databaseItem: addedDatabase ?? this.databaseItem,
|
||||||
|
onResults: async (modeledMethods) => {
|
||||||
|
const modeledMethodsByName: Record<string, ModeledMethod> = {};
|
||||||
|
|
||||||
|
for (const modeledMethod of modeledMethods) {
|
||||||
|
modeledMethodsByName[modeledMethod.signature] = modeledMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.postMessage({
|
||||||
|
t: "addModeledMethods",
|
||||||
|
modeledMethods: modeledMethodsByName,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
progress,
|
||||||
|
token: tokenSource.token,
|
||||||
});
|
});
|
||||||
},
|
} catch (e: unknown) {
|
||||||
progress: (update) => this.showProgress(update),
|
void showAndLogExceptionWithTelemetry(
|
||||||
token: tokenSource.token,
|
this.app.logger,
|
||||||
});
|
this.app.telemetry,
|
||||||
} catch (e: unknown) {
|
redactableError(
|
||||||
void showAndLogExceptionWithTelemetry(
|
asError(e),
|
||||||
this.app.logger,
|
)`Failed to generate flow model: ${getErrorMessage(e)}`,
|
||||||
this.app.telemetry,
|
);
|
||||||
redactableError(
|
}
|
||||||
asError(e),
|
|
||||||
)`Failed to generate flow model: ${getErrorMessage(e)}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addedDatabase) {
|
if (addedDatabase) {
|
||||||
// After the flow model has been generated, we can remove the temporary database
|
// After the flow model has been generated, we can remove the temporary database
|
||||||
// which we used for generating the flow model.
|
// which we used for generating the flow model.
|
||||||
await this.showProgress({
|
progress({
|
||||||
step: 3900,
|
step: 3900,
|
||||||
maxStep: 4000,
|
maxStep: 4000,
|
||||||
message: "Removing temporary database",
|
message: "Removing temporary database",
|
||||||
});
|
});
|
||||||
await this.databaseManager.removeDatabaseItem(addedDatabase);
|
await this.databaseManager.removeDatabaseItem(addedDatabase);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
await this.clearProgress();
|
{ cancellable: false },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async generateModeledMethodsFromLlm(
|
private async generateModeledMethodsFromLlm(
|
||||||
externalApiUsages: ExternalApiUsage[],
|
externalApiUsages: ExternalApiUsage[],
|
||||||
modeledMethods: Record<string, ModeledMethod>,
|
modeledMethods: Record<string, ModeledMethod>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const maxStep = 3000;
|
await withProgress(
|
||||||
|
async (progress) => {
|
||||||
|
const maxStep = 3000;
|
||||||
|
|
||||||
await this.showProgress({
|
progress({
|
||||||
step: 0,
|
step: 0,
|
||||||
maxStep,
|
maxStep,
|
||||||
message: "Retrieving usages",
|
message: "Retrieving usages",
|
||||||
});
|
});
|
||||||
|
|
||||||
const usages = await getAutoModelUsages({
|
const usages = await getAutoModelUsages({
|
||||||
cliServer: this.cliServer,
|
cliServer: this.cliServer,
|
||||||
queryRunner: this.queryRunner,
|
queryRunner: this.queryRunner,
|
||||||
queryStorageDir: this.queryStorageDir,
|
queryStorageDir: this.queryStorageDir,
|
||||||
databaseItem: this.databaseItem,
|
databaseItem: this.databaseItem,
|
||||||
progress: (update) => this.showProgress(update, maxStep),
|
progress: (update) => progress({ ...update, maxStep }),
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.showProgress({
|
progress({
|
||||||
step: 1800,
|
step: 1800,
|
||||||
maxStep,
|
maxStep,
|
||||||
message: "Creating request",
|
message: "Creating request",
|
||||||
});
|
});
|
||||||
|
|
||||||
const request = createAutoModelRequest(
|
const request = createAutoModelRequest(
|
||||||
this.databaseItem.language,
|
this.databaseItem.language,
|
||||||
externalApiUsages,
|
externalApiUsages,
|
||||||
modeledMethods,
|
modeledMethods,
|
||||||
usages,
|
usages,
|
||||||
this.mode,
|
this.mode,
|
||||||
|
);
|
||||||
|
|
||||||
|
progress({
|
||||||
|
step: 2000,
|
||||||
|
maxStep,
|
||||||
|
message: "Sending request",
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await this.callAutoModelApi(request);
|
||||||
|
if (!response) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
progress({
|
||||||
|
step: 2500,
|
||||||
|
maxStep,
|
||||||
|
message: "Parsing response",
|
||||||
|
});
|
||||||
|
|
||||||
|
const predictedModeledMethods = parsePredictedClassifications(
|
||||||
|
response.predicted || [],
|
||||||
|
);
|
||||||
|
|
||||||
|
progress({
|
||||||
|
step: 2800,
|
||||||
|
maxStep,
|
||||||
|
message: "Applying results",
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.postMessage({
|
||||||
|
t: "addModeledMethods",
|
||||||
|
modeledMethods: predictedModeledMethods,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ cancellable: false },
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.showProgress({
|
|
||||||
step: 2000,
|
|
||||||
maxStep,
|
|
||||||
message: "Sending request",
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await this.callAutoModelApi(request);
|
|
||||||
if (!response) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.showProgress({
|
|
||||||
step: 2500,
|
|
||||||
maxStep,
|
|
||||||
message: "Parsing response",
|
|
||||||
});
|
|
||||||
|
|
||||||
const predictedModeledMethods = parsePredictedClassifications(
|
|
||||||
response.predicted || [],
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.showProgress({
|
|
||||||
step: 2800,
|
|
||||||
maxStep,
|
|
||||||
message: "Applying results",
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.postMessage({
|
|
||||||
t: "addModeledMethods",
|
|
||||||
modeledMethods: predictedModeledMethods,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.clearProgress();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async modelDependency(): Promise<void> {
|
private async modelDependency(): Promise<void> {
|
||||||
@@ -489,46 +487,12 @@ export class DataExtensionsEditorView extends AbstractWebview<
|
|||||||
return addedDatabase;
|
return addedDatabase;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Progress in this class is a bit weird. Most of the progress is based on running the query.
|
|
||||||
* Query progress is always between 0 and 1000. However, we still have some steps that need
|
|
||||||
* to be done after the query has finished. Therefore, the maximum step is 1500. This captures
|
|
||||||
* 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%
|
|
||||||
* 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) {
|
|
||||||
await this.postMessage({
|
|
||||||
t: "showProgress",
|
|
||||||
step: update.step,
|
|
||||||
maxStep: maxStep ?? update.maxStep,
|
|
||||||
message: update.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async clearProgress() {
|
|
||||||
await this.showProgress({
|
|
||||||
step: 0,
|
|
||||||
maxStep: 0,
|
|
||||||
message: "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async callAutoModelApi(
|
private async callAutoModelApi(
|
||||||
request: ModelRequest,
|
request: ModelRequest,
|
||||||
): Promise<ModelResponse | null> {
|
): Promise<ModelResponse | null> {
|
||||||
try {
|
try {
|
||||||
return await autoModel(this.app.credentials, request);
|
return await autoModel(this.app.credentials, request);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await this.clearProgress();
|
|
||||||
|
|
||||||
if (e instanceof RequestError && e.status === 429) {
|
if (e instanceof RequestError && e.status === 429) {
|
||||||
void showAndLogExceptionWithTelemetry(
|
void showAndLogExceptionWithTelemetry(
|
||||||
this.app.logger,
|
this.app.logger,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { QueryResultType } from "../query-server/new-messages";
|
|||||||
import { file } from "tmp-promise";
|
import { file } from "tmp-promise";
|
||||||
import { writeFile } from "fs-extra";
|
import { writeFile } from "fs-extra";
|
||||||
import { dump } from "js-yaml";
|
import { dump } from "js-yaml";
|
||||||
import { qlpackOfDatabase } from "../language-support";
|
import { qlpackOfDatabase } from "../local-queries";
|
||||||
import { telemetryListener } from "../common/vscode/telemetry";
|
import { telemetryListener } from "../common/vscode/telemetry";
|
||||||
|
|
||||||
type FlowModelOptions = {
|
type FlowModelOptions = {
|
||||||
|
|||||||
@@ -12,16 +12,13 @@ import { CodeQLCliServer } from "../../codeql-cli/cli";
|
|||||||
import { DatabaseManager, DatabaseItem } from "../../databases/local-databases";
|
import { DatabaseManager, DatabaseItem } from "../../databases/local-databases";
|
||||||
import { ProgressCallback } from "../../common/vscode/progress";
|
import { ProgressCallback } from "../../common/vscode/progress";
|
||||||
import { KeyType } from "./key-type";
|
import { KeyType } from "./key-type";
|
||||||
import {
|
import { resolveQueries, runContextualQuery } from "./query-resolver";
|
||||||
qlpackOfDatabase,
|
|
||||||
resolveQueries,
|
|
||||||
runContextualQuery,
|
|
||||||
} from "./query-resolver";
|
|
||||||
import { CancellationToken, LocationLink, Uri } from "vscode";
|
import { CancellationToken, LocationLink, Uri } from "vscode";
|
||||||
import { QueryOutputDir } from "../../run-queries-shared";
|
import { QueryOutputDir } from "../../run-queries-shared";
|
||||||
import { QueryRunner } from "../../query-server";
|
import { QueryRunner } from "../../query-server";
|
||||||
import { QueryResultType } from "../../query-server/new-messages";
|
import { QueryResultType } from "../../query-server/new-messages";
|
||||||
import { fileRangeFromURI } from "./file-range-from-uri";
|
import { fileRangeFromURI } from "./file-range-from-uri";
|
||||||
|
import { qlpackOfDatabase } from "../../local-queries";
|
||||||
|
|
||||||
export const SELECT_QUERY_NAME = "#select";
|
export const SELECT_QUERY_NAME = "#select";
|
||||||
export const SELECTED_SOURCE_FILE = "selectedSourceFile";
|
export const SELECTED_SOURCE_FILE = "selectedSourceFile";
|
||||||
|
|||||||
@@ -1,14 +1,5 @@
|
|||||||
import { writeFile, promises } from "fs-extra";
|
|
||||||
import { dump } from "js-yaml";
|
|
||||||
import { file } from "tmp-promise";
|
|
||||||
import { basename, dirname, resolve } from "path";
|
|
||||||
|
|
||||||
import { getOnDiskWorkspaceFolders } from "../../common/vscode/workspace-folders";
|
import { getOnDiskWorkspaceFolders } from "../../common/vscode/workspace-folders";
|
||||||
import {
|
import { QlPacksForLanguage } from "../../databases/qlpack";
|
||||||
getPrimaryDbscheme,
|
|
||||||
getQlPackForDbscheme,
|
|
||||||
QlPacksForLanguage,
|
|
||||||
} from "../../databases/qlpack";
|
|
||||||
import {
|
import {
|
||||||
KeyType,
|
KeyType,
|
||||||
kindOfKeyType,
|
kindOfKeyType,
|
||||||
@@ -17,154 +8,22 @@ import {
|
|||||||
} from "./key-type";
|
} from "./key-type";
|
||||||
import { CodeQLCliServer } from "../../codeql-cli/cli";
|
import { CodeQLCliServer } from "../../codeql-cli/cli";
|
||||||
import { DatabaseItem } from "../../databases/local-databases";
|
import { DatabaseItem } from "../../databases/local-databases";
|
||||||
|
import { resolveQueries as resolveLocalQueries } from "../../local-queries/query-resolver";
|
||||||
import { extLogger } from "../../common/logging/vscode";
|
import { extLogger } from "../../common/logging/vscode";
|
||||||
import {
|
import { TeeLogger } from "../../common/logging";
|
||||||
showAndLogExceptionWithTelemetry,
|
|
||||||
TeeLogger,
|
|
||||||
} from "../../common/logging";
|
|
||||||
import { CancellationToken } from "vscode";
|
import { CancellationToken } from "vscode";
|
||||||
import { ProgressCallback } from "../../common/vscode/progress";
|
import { ProgressCallback } from "../../common/vscode/progress";
|
||||||
import { CoreCompletedQuery, QueryRunner } from "../../query-server";
|
import { CoreCompletedQuery, QueryRunner } from "../../query-server";
|
||||||
import { redactableError } from "../../common/errors";
|
import { createLockFileForStandardQuery } from "../../local-queries/standard-queries";
|
||||||
import { QLPACK_FILENAMES } from "../../common/ql";
|
|
||||||
import { telemetryListener } from "../../common/vscode/telemetry";
|
|
||||||
|
|
||||||
export async function qlpackOfDatabase(
|
|
||||||
cli: Pick<CodeQLCliServer, "resolveQlpacks">,
|
|
||||||
db: Pick<DatabaseItem, "contents">,
|
|
||||||
): Promise<QlPacksForLanguage> {
|
|
||||||
if (db.contents === undefined) {
|
|
||||||
throw new Error("Database is invalid and cannot infer QLPack.");
|
|
||||||
}
|
|
||||||
const datasetPath = db.contents.datasetUri.fsPath;
|
|
||||||
const dbscheme = await getPrimaryDbscheme(datasetPath);
|
|
||||||
return await getQlPackForDbscheme(cli, dbscheme);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds the contextual queries with the specified key in a list of CodeQL packs.
|
|
||||||
*
|
|
||||||
* @param cli The CLI instance to use.
|
|
||||||
* @param qlpacks The list of packs to search.
|
|
||||||
* @param keyType The contextual query key of the query to search for.
|
|
||||||
* @returns The found queries from the first pack in which any matching queries were found.
|
|
||||||
*/
|
|
||||||
async function resolveQueriesFromPacks(
|
|
||||||
cli: CodeQLCliServer,
|
|
||||||
qlpacks: string[],
|
|
||||||
keyType: KeyType,
|
|
||||||
): Promise<string[]> {
|
|
||||||
const suiteFile = (
|
|
||||||
await file({
|
|
||||||
postfix: ".qls",
|
|
||||||
})
|
|
||||||
).path;
|
|
||||||
const suiteYaml = [];
|
|
||||||
for (const qlpack of qlpacks) {
|
|
||||||
suiteYaml.push({
|
|
||||||
from: qlpack,
|
|
||||||
queries: ".",
|
|
||||||
include: {
|
|
||||||
kind: kindOfKeyType(keyType),
|
|
||||||
"tags contain": tagOfKeyType(keyType),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await writeFile(suiteFile, dump(suiteYaml), "utf8");
|
|
||||||
|
|
||||||
const queries = await cli.resolveQueriesInSuite(
|
|
||||||
suiteFile,
|
|
||||||
getOnDiskWorkspaceFolders(),
|
|
||||||
);
|
|
||||||
return queries;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function resolveQueries(
|
export async function resolveQueries(
|
||||||
cli: CodeQLCliServer,
|
cli: CodeQLCliServer,
|
||||||
qlpacks: QlPacksForLanguage,
|
qlpacks: QlPacksForLanguage,
|
||||||
keyType: KeyType,
|
keyType: KeyType,
|
||||||
): Promise<string[]> {
|
): Promise<string[]> {
|
||||||
const packsToSearch: string[] = [];
|
return resolveLocalQueries(cli, qlpacks, nameOfKeyType(keyType), {
|
||||||
|
kind: kindOfKeyType(keyType),
|
||||||
// The CLI can handle both library packs and query packs, so search both packs in order.
|
"tags contain": [tagOfKeyType(keyType)],
|
||||||
packsToSearch.push(qlpacks.dbschemePack);
|
|
||||||
if (qlpacks.queryPack !== undefined) {
|
|
||||||
packsToSearch.push(qlpacks.queryPack);
|
|
||||||
}
|
|
||||||
|
|
||||||
const queries = await resolveQueriesFromPacks(cli, packsToSearch, keyType);
|
|
||||||
if (queries.length > 0) {
|
|
||||||
return queries;
|
|
||||||
}
|
|
||||||
|
|
||||||
// No queries found. Determine the correct error message for the various scenarios.
|
|
||||||
const keyTypeName = nameOfKeyType(keyType);
|
|
||||||
const keyTypeTag = tagOfKeyType(keyType);
|
|
||||||
const joinedPacksToSearch = packsToSearch.join(", ");
|
|
||||||
const error = redactableError`No ${keyTypeName} queries (tagged "${keyTypeTag}") could be found in the \
|
|
||||||
current library path (tried searching the following packs: ${joinedPacksToSearch}). \
|
|
||||||
Try upgrading the CodeQL libraries. If that doesn't work, then ${keyTypeName} queries are not yet available \
|
|
||||||
for this language.`;
|
|
||||||
|
|
||||||
void showAndLogExceptionWithTelemetry(extLogger, telemetryListener, error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function resolveContextualQuery(
|
|
||||||
cli: CodeQLCliServer,
|
|
||||||
query: string,
|
|
||||||
): Promise<{ packPath: string; createdTempLockFile: boolean }> {
|
|
||||||
// Contextual queries now live within the standard library packs.
|
|
||||||
// This simplifies distribution (you don't need the standard query pack to use the AST viewer),
|
|
||||||
// but if the library pack doesn't have a lockfile, we won't be able to find
|
|
||||||
// other pack dependencies of the library pack.
|
|
||||||
|
|
||||||
// Work out the enclosing pack.
|
|
||||||
const packContents = await cli.packPacklist(query, false);
|
|
||||||
const packFilePath = packContents.find((p) =>
|
|
||||||
QLPACK_FILENAMES.includes(basename(p)),
|
|
||||||
);
|
|
||||||
if (packFilePath === undefined) {
|
|
||||||
// Should not happen; we already resolved this query.
|
|
||||||
throw new Error(
|
|
||||||
`Could not find a CodeQL pack file for the pack enclosing the contextual query ${query}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const packPath = dirname(packFilePath);
|
|
||||||
const lockFilePath = packContents.find((p) =>
|
|
||||||
["codeql-pack.lock.yml", "qlpack.lock.yml"].includes(basename(p)),
|
|
||||||
);
|
|
||||||
let createdTempLockFile = false;
|
|
||||||
if (!lockFilePath) {
|
|
||||||
// No lock file, likely because this library pack is in the package cache.
|
|
||||||
// Create a lock file so that we can resolve dependencies and library path
|
|
||||||
// for the contextual query.
|
|
||||||
void extLogger.log(
|
|
||||||
`Library pack ${packPath} is missing a lock file; creating a temporary lock file`,
|
|
||||||
);
|
|
||||||
await cli.packResolveDependencies(packPath);
|
|
||||||
createdTempLockFile = true;
|
|
||||||
// Clear CLI server pack cache before installing dependencies,
|
|
||||||
// so that it picks up the new lock file, not the previously cached pack.
|
|
||||||
void extLogger.log("Clearing the CodeQL CLI server's pack cache");
|
|
||||||
await cli.clearCache();
|
|
||||||
// Install dependencies.
|
|
||||||
void extLogger.log(
|
|
||||||
`Installing package dependencies for library pack ${packPath}`,
|
|
||||||
);
|
|
||||||
await cli.packInstall(packPath);
|
|
||||||
}
|
|
||||||
return { packPath, createdTempLockFile };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function removeTemporaryLockFile(packPath: string) {
|
|
||||||
const tempLockFilePath = resolve(packPath, "codeql-pack.lock.yml");
|
|
||||||
void extLogger.log(
|
|
||||||
`Deleting temporary package lock file at ${tempLockFilePath}`,
|
|
||||||
);
|
|
||||||
// It's fine if the file doesn't exist.
|
|
||||||
await promises.rm(resolve(packPath, "codeql-pack.lock.yml"), {
|
|
||||||
force: true,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,10 +37,7 @@ export async function runContextualQuery(
|
|||||||
token: CancellationToken,
|
token: CancellationToken,
|
||||||
templates: Record<string, string>,
|
templates: Record<string, string>,
|
||||||
): Promise<CoreCompletedQuery> {
|
): Promise<CoreCompletedQuery> {
|
||||||
const { packPath, createdTempLockFile } = await resolveContextualQuery(
|
const { cleanup } = await createLockFileForStandardQuery(cli, query);
|
||||||
cli,
|
|
||||||
query,
|
|
||||||
);
|
|
||||||
const queryRun = qs.createQueryRun(
|
const queryRun = qs.createQueryRun(
|
||||||
db.databaseUri.fsPath,
|
db.databaseUri.fsPath,
|
||||||
{ queryPath: query, quickEvalPosition: undefined },
|
{ queryPath: query, quickEvalPosition: undefined },
|
||||||
@@ -200,8 +56,6 @@ export async function runContextualQuery(
|
|||||||
token,
|
token,
|
||||||
new TeeLogger(qs.logger, queryRun.outputDir.logPath),
|
new TeeLogger(qs.logger, queryRun.outputDir.logPath),
|
||||||
);
|
);
|
||||||
if (createdTempLockFile) {
|
await cleanup?.();
|
||||||
await removeTemporaryLockFile(packPath);
|
|
||||||
}
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,11 +27,7 @@ import {
|
|||||||
SELECTED_SOURCE_LINE,
|
SELECTED_SOURCE_LINE,
|
||||||
SELECTED_SOURCE_COLUMN,
|
SELECTED_SOURCE_COLUMN,
|
||||||
} from "./location-finder";
|
} from "./location-finder";
|
||||||
import {
|
import { resolveQueries, runContextualQuery } from "./query-resolver";
|
||||||
qlpackOfDatabase,
|
|
||||||
resolveQueries,
|
|
||||||
runContextualQuery,
|
|
||||||
} from "./query-resolver";
|
|
||||||
import {
|
import {
|
||||||
isCanary,
|
isCanary,
|
||||||
NO_CACHE_AST_VIEWER,
|
NO_CACHE_AST_VIEWER,
|
||||||
@@ -39,6 +35,7 @@ import {
|
|||||||
} from "../../config";
|
} from "../../config";
|
||||||
import { CoreCompletedQuery, QueryRunner } from "../../query-server";
|
import { CoreCompletedQuery, QueryRunner } from "../../query-server";
|
||||||
import { AstBuilder } from "../ast-viewer/ast-builder";
|
import { AstBuilder } from "../ast-viewer/ast-builder";
|
||||||
|
import { qlpackOfDatabase } from "../../local-queries";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs templated CodeQL queries to find definitions in
|
* Runs templated CodeQL queries to find definitions in
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export * from "./local-queries";
|
export * from "./local-queries";
|
||||||
export * from "./local-query-run";
|
export * from "./local-query-run";
|
||||||
|
export * from "./query-resolver";
|
||||||
export * from "./quick-eval-code-lens-provider";
|
export * from "./quick-eval-code-lens-provider";
|
||||||
export * from "./quick-query";
|
export * from "./quick-query";
|
||||||
export * from "./results-view";
|
export * from "./results-view";
|
||||||
|
|||||||
131
extensions/ql-vscode/src/local-queries/query-resolver.ts
Normal file
131
extensions/ql-vscode/src/local-queries/query-resolver.ts
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import { CodeQLCliServer } from "../codeql-cli/cli";
|
||||||
|
import { DatabaseItem } from "../databases/local-databases";
|
||||||
|
import {
|
||||||
|
getPrimaryDbscheme,
|
||||||
|
getQlPackForDbscheme,
|
||||||
|
QlPacksForLanguage,
|
||||||
|
} from "../databases/qlpack";
|
||||||
|
import { file } from "tmp-promise";
|
||||||
|
import { writeFile } from "fs-extra";
|
||||||
|
import { dump } from "js-yaml";
|
||||||
|
import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
|
||||||
|
import { redactableError } from "../common/errors";
|
||||||
|
import { showAndLogExceptionWithTelemetry } from "../common/logging";
|
||||||
|
import { extLogger } from "../common/logging/vscode";
|
||||||
|
import { telemetryListener } from "../common/vscode/telemetry";
|
||||||
|
|
||||||
|
export async function qlpackOfDatabase(
|
||||||
|
cli: Pick<CodeQLCliServer, "resolveQlpacks">,
|
||||||
|
db: Pick<DatabaseItem, "contents">,
|
||||||
|
): Promise<QlPacksForLanguage> {
|
||||||
|
if (db.contents === undefined) {
|
||||||
|
throw new Error("Database is invalid and cannot infer QLPack.");
|
||||||
|
}
|
||||||
|
const datasetPath = db.contents.datasetUri.fsPath;
|
||||||
|
const dbscheme = await getPrimaryDbscheme(datasetPath);
|
||||||
|
return await getQlPackForDbscheme(cli, dbscheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueryConstraints {
|
||||||
|
kind?: string;
|
||||||
|
"tags contain"?: string[];
|
||||||
|
"tags contain all"?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the queries with the specified kind and tags in a list of CodeQL packs.
|
||||||
|
*
|
||||||
|
* @param cli The CLI instance to use.
|
||||||
|
* @param qlpacks The list of packs to search.
|
||||||
|
* @param constraints Constraints on the queries to search for.
|
||||||
|
* @returns The found queries from the first pack in which any matching queries were found.
|
||||||
|
*/
|
||||||
|
async function resolveQueriesFromPacks(
|
||||||
|
cli: CodeQLCliServer,
|
||||||
|
qlpacks: string[],
|
||||||
|
constraints: QueryConstraints,
|
||||||
|
): Promise<string[]> {
|
||||||
|
const suiteFile = (
|
||||||
|
await file({
|
||||||
|
postfix: ".qls",
|
||||||
|
})
|
||||||
|
).path;
|
||||||
|
const suiteYaml = [];
|
||||||
|
for (const qlpack of qlpacks) {
|
||||||
|
suiteYaml.push({
|
||||||
|
from: qlpack,
|
||||||
|
queries: ".",
|
||||||
|
include: constraints,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await writeFile(
|
||||||
|
suiteFile,
|
||||||
|
dump(suiteYaml, {
|
||||||
|
noRefs: true, // CodeQL doesn't really support refs
|
||||||
|
}),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
|
||||||
|
return await cli.resolveQueriesInSuite(
|
||||||
|
suiteFile,
|
||||||
|
getOnDiskWorkspaceFolders(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the queries with the specified kind and tags in a QLPack.
|
||||||
|
*
|
||||||
|
* @param cli The CLI instance to use.
|
||||||
|
* @param qlpacks The list of packs to search.
|
||||||
|
* @param name The name of the query to use in error messages.
|
||||||
|
* @param constraints Constraints on the queries to search for.
|
||||||
|
* @returns The found queries from the first pack in which any matching queries were found.
|
||||||
|
*/
|
||||||
|
export async function resolveQueries(
|
||||||
|
cli: CodeQLCliServer,
|
||||||
|
qlpacks: QlPacksForLanguage,
|
||||||
|
name: string,
|
||||||
|
constraints: QueryConstraints,
|
||||||
|
): Promise<string[]> {
|
||||||
|
const packsToSearch: string[] = [];
|
||||||
|
|
||||||
|
// The CLI can handle both library packs and query packs, so search both packs in order.
|
||||||
|
packsToSearch.push(qlpacks.dbschemePack);
|
||||||
|
if (qlpacks.queryPack !== undefined) {
|
||||||
|
packsToSearch.push(qlpacks.queryPack);
|
||||||
|
}
|
||||||
|
|
||||||
|
const queries = await resolveQueriesFromPacks(
|
||||||
|
cli,
|
||||||
|
packsToSearch,
|
||||||
|
constraints,
|
||||||
|
);
|
||||||
|
if (queries.length > 0) {
|
||||||
|
return queries;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No queries found. Determine the correct error message for the various scenarios.
|
||||||
|
const humanConstraints = [];
|
||||||
|
if (constraints.kind !== undefined) {
|
||||||
|
humanConstraints.push(`kind "${constraints.kind}"`);
|
||||||
|
}
|
||||||
|
if (constraints["tags contain"] !== undefined) {
|
||||||
|
humanConstraints.push(`tagged "${constraints["tags contain"].join(" ")}"`);
|
||||||
|
}
|
||||||
|
if (constraints["tags contain all"] !== undefined) {
|
||||||
|
humanConstraints.push(
|
||||||
|
`tagged all of "${constraints["tags contain all"].join(" ")}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const joinedPacksToSearch = packsToSearch.join(", ");
|
||||||
|
const error = redactableError`No ${name} queries (${humanConstraints.join(
|
||||||
|
", ",
|
||||||
|
)}) could be found in the \
|
||||||
|
current library path (tried searching the following packs: ${joinedPacksToSearch}). \
|
||||||
|
Try upgrading the CodeQL libraries. If that doesn't work, then ${name} queries are not yet available \
|
||||||
|
for this language.`;
|
||||||
|
|
||||||
|
void showAndLogExceptionWithTelemetry(extLogger, telemetryListener, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
77
extensions/ql-vscode/src/local-queries/standard-queries.ts
Normal file
77
extensions/ql-vscode/src/local-queries/standard-queries.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { CodeQLCliServer } from "../codeql-cli/cli";
|
||||||
|
import { QLPACK_FILENAMES, QLPACK_LOCK_FILENAMES } from "../common/ql";
|
||||||
|
import { basename, dirname, resolve } from "path";
|
||||||
|
import { extLogger } from "../common/logging/vscode";
|
||||||
|
import { promises } from "fs-extra";
|
||||||
|
import { BaseLogger } from "../common/logging";
|
||||||
|
|
||||||
|
type LockFileForStandardQueryResult = {
|
||||||
|
cleanup?: () => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a temporary query suite for a given query living within the standard library packs.
|
||||||
|
*
|
||||||
|
* This will create a lock file so the CLI can run the query without having the ql submodule.
|
||||||
|
*/
|
||||||
|
export async function createLockFileForStandardQuery(
|
||||||
|
cli: CodeQLCliServer,
|
||||||
|
queryPath: string,
|
||||||
|
logger: BaseLogger = extLogger,
|
||||||
|
): Promise<LockFileForStandardQueryResult> {
|
||||||
|
// These queries live within the standard library packs.
|
||||||
|
// This simplifies distribution (you don't need the standard query pack to use the AST viewer),
|
||||||
|
// but if the library pack doesn't have a lockfile, we won't be able to find
|
||||||
|
// other pack dependencies of the library pack.
|
||||||
|
|
||||||
|
// Work out the enclosing pack.
|
||||||
|
const packContents = await cli.packPacklist(queryPath, false);
|
||||||
|
const packFilePath = packContents.find((p) =>
|
||||||
|
QLPACK_FILENAMES.includes(basename(p)),
|
||||||
|
);
|
||||||
|
if (packFilePath === undefined) {
|
||||||
|
// Should not happen; we already resolved this query.
|
||||||
|
throw new Error(
|
||||||
|
`Could not find a CodeQL pack file for the pack enclosing the contextual query ${queryPath}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const packPath = dirname(packFilePath);
|
||||||
|
const lockFilePath = packContents.find((p) =>
|
||||||
|
QLPACK_LOCK_FILENAMES.includes(basename(p)),
|
||||||
|
);
|
||||||
|
|
||||||
|
let cleanup: (() => Promise<void>) | undefined = undefined;
|
||||||
|
|
||||||
|
if (!lockFilePath) {
|
||||||
|
// No lock file, likely because this library pack is in the package cache.
|
||||||
|
// Create a lock file so that we can resolve dependencies and library path
|
||||||
|
// for the contextual query.
|
||||||
|
void logger.log(
|
||||||
|
`Library pack ${packPath} is missing a lock file; creating a temporary lock file`,
|
||||||
|
);
|
||||||
|
await cli.packResolveDependencies(packPath);
|
||||||
|
|
||||||
|
cleanup = async () => {
|
||||||
|
const tempLockFilePath = resolve(packPath, "codeql-pack.lock.yml");
|
||||||
|
void logger.log(
|
||||||
|
`Deleting temporary package lock file at ${tempLockFilePath}`,
|
||||||
|
);
|
||||||
|
// It's fine if the file doesn't exist.
|
||||||
|
await promises.rm(resolve(packPath, "codeql-pack.lock.yml"), {
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Clear CLI server pack cache before installing dependencies,
|
||||||
|
// so that it picks up the new lock file, not the previously cached pack.
|
||||||
|
void logger.log("Clearing the CodeQL CLI server's pack cache");
|
||||||
|
await cli.clearCache();
|
||||||
|
// Install dependencies.
|
||||||
|
void logger.log(
|
||||||
|
`Installing package dependencies for library pack ${packPath}`,
|
||||||
|
);
|
||||||
|
await cli.packInstall(packPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { cleanup };
|
||||||
|
}
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
|
|
||||||
import { Meta, StoryFn } from "@storybook/react";
|
|
||||||
|
|
||||||
import { LastUpdated as LastUpdatedComponent } from "../../view/common/LastUpdated";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
title: "Last Updated",
|
|
||||||
component: LastUpdatedComponent,
|
|
||||||
} as Meta<typeof LastUpdatedComponent>;
|
|
||||||
|
|
||||||
const Template: StoryFn<typeof LastUpdatedComponent> = (args) => (
|
|
||||||
<LastUpdatedComponent {...args} />
|
|
||||||
);
|
|
||||||
|
|
||||||
export const LastUpdated = Template.bind({});
|
|
||||||
|
|
||||||
LastUpdated.args = {
|
|
||||||
lastUpdated: new Date(Date.now() - 3_600_000).toISOString(), // 1 hour ago
|
|
||||||
};
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Repository, RepositoryWithMetadata } from "./repository";
|
import { Repository, RepositoryWithMetadata } from "./repository";
|
||||||
import { parseDate } from "../../common/date";
|
|
||||||
import { assertNever } from "../../common/helpers-pure";
|
import { assertNever } from "../../common/helpers-pure";
|
||||||
|
|
||||||
export enum FilterKey {
|
export enum FilterKey {
|
||||||
@@ -10,7 +9,6 @@ export enum FilterKey {
|
|||||||
export enum SortKey {
|
export enum SortKey {
|
||||||
Alphabetically = "alphabetically",
|
Alphabetically = "alphabetically",
|
||||||
Popularity = "popularity",
|
Popularity = "popularity",
|
||||||
MostRecentCommit = "mostRecentCommit",
|
|
||||||
NumberOfResults = "numberOfResults",
|
NumberOfResults = "numberOfResults",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,16 +79,6 @@ export function compareRepository(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Newest to oldest
|
|
||||||
if (filterSortState?.sortKey === SortKey.MostRecentCommit) {
|
|
||||||
const lastUpdated =
|
|
||||||
(parseDate(right.updatedAt)?.getTime() ?? 0) -
|
|
||||||
(parseDate(left.updatedAt)?.getTime() ?? 0);
|
|
||||||
if (lastUpdated !== 0) {
|
|
||||||
return lastUpdated;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fall back on name compare. Use en-US because the repository name does not contain
|
// Fall back on name compare. Use en-US because the repository name does not contain
|
||||||
// special characters due to restrictions in GitHub owner/repository names.
|
// special characters due to restrictions in GitHub owner/repository names.
|
||||||
return left.fullName.localeCompare(right.fullName, "en-US", {
|
return left.fullName.localeCompare(right.fullName, "en-US", {
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import { useMemo } from "react";
|
|
||||||
import styled from "styled-components";
|
|
||||||
|
|
||||||
import { parseDate } from "../../common/date";
|
|
||||||
import { humanizeRelativeTime } from "../../common/time";
|
|
||||||
|
|
||||||
import { Codicon } from "./icon";
|
|
||||||
|
|
||||||
const IconContainer = styled.span`
|
|
||||||
flex-grow: 0;
|
|
||||||
text-align: right;
|
|
||||||
margin-right: 0;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Duration = styled.span`
|
|
||||||
display: inline-block;
|
|
||||||
text-align: left;
|
|
||||||
width: 8em;
|
|
||||||
margin-left: 0.5em;
|
|
||||||
`;
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
lastUpdated?: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const LastUpdated = ({ lastUpdated }: Props) => {
|
|
||||||
const date = useMemo(() => parseDate(lastUpdated), [lastUpdated]);
|
|
||||||
|
|
||||||
if (!date) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<IconContainer>
|
|
||||||
<Codicon name="repo-push" label="Most recent commit" />
|
|
||||||
</IconContainer>
|
|
||||||
<Duration>{humanizeRelativeTime(date.getTime() - Date.now())}</Duration>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import {
|
import { ToDataExtensionsEditorMessage } from "../../common/interface-types";
|
||||||
ShowProgressMessage,
|
|
||||||
ToDataExtensionsEditorMessage,
|
|
||||||
} from "../../common/interface-types";
|
|
||||||
import {
|
import {
|
||||||
VSCodeButton,
|
VSCodeButton,
|
||||||
VSCodeCheckbox,
|
VSCodeCheckbox,
|
||||||
@@ -72,17 +69,6 @@ const ButtonsContainer = styled.div`
|
|||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type ProgressBarProps = {
|
|
||||||
completion: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ProgressBar = styled.div<ProgressBarProps>`
|
|
||||||
height: 10px;
|
|
||||||
width: ${(props) => props.completion * 100}%;
|
|
||||||
|
|
||||||
background-color: var(--vscode-progressBar-background);
|
|
||||||
`;
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
initialViewState?: DataExtensionEditorViewState;
|
initialViewState?: DataExtensionEditorViewState;
|
||||||
initialExternalApiUsages?: ExternalApiUsage[];
|
initialExternalApiUsages?: ExternalApiUsage[];
|
||||||
@@ -108,11 +94,6 @@ export function DataExtensionsEditor({
|
|||||||
const [modeledMethods, setModeledMethods] = useState<
|
const [modeledMethods, setModeledMethods] = useState<
|
||||||
Record<string, ModeledMethod>
|
Record<string, ModeledMethod>
|
||||||
>(initialModeledMethods);
|
>(initialModeledMethods);
|
||||||
const [progress, setProgress] = useState<Omit<ShowProgressMessage, "t">>({
|
|
||||||
step: 0,
|
|
||||||
maxStep: 0,
|
|
||||||
message: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const listener = (evt: MessageEvent) => {
|
const listener = (evt: MessageEvent) => {
|
||||||
@@ -125,9 +106,6 @@ export function DataExtensionsEditor({
|
|||||||
case "setExternalApiUsages":
|
case "setExternalApiUsages":
|
||||||
setExternalApiUsages(msg.externalApiUsages);
|
setExternalApiUsages(msg.externalApiUsages);
|
||||||
break;
|
break;
|
||||||
case "showProgress":
|
|
||||||
setProgress(msg);
|
|
||||||
break;
|
|
||||||
case "loadModeledMethods":
|
case "loadModeledMethods":
|
||||||
setModeledMethods((oldModeledMethods) => {
|
setModeledMethods((oldModeledMethods) => {
|
||||||
return {
|
return {
|
||||||
@@ -274,95 +252,81 @@ export function DataExtensionsEditor({
|
|||||||
});
|
});
|
||||||
}, [viewState?.mode]);
|
}, [viewState?.mode]);
|
||||||
|
|
||||||
if (viewState === undefined) {
|
if (viewState === undefined || externalApiUsages.length === 0) {
|
||||||
return <LoadingContainer>Loading...</LoadingContainer>;
|
return <LoadingContainer>Loading...</LoadingContainer>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataExtensionsEditorContainer>
|
<DataExtensionsEditorContainer>
|
||||||
{progress.maxStep > 0 && (
|
<HeaderContainer>
|
||||||
<p>
|
<HeaderColumn>
|
||||||
<ProgressBar completion={progress.step / progress.maxStep} />{" "}
|
<HeaderRow>
|
||||||
{progress.message}
|
<ViewTitle>
|
||||||
</p>
|
{getLanguageDisplayName(viewState.extensionPack.language)}
|
||||||
)}
|
</ViewTitle>
|
||||||
|
<VSCodeTag>
|
||||||
|
{percentFormatter.format(modeledPercentage / 100)} modeled
|
||||||
|
</VSCodeTag>
|
||||||
|
</HeaderRow>
|
||||||
|
<HeaderRow>
|
||||||
|
<>{viewState.extensionPack.name}</>
|
||||||
|
</HeaderRow>
|
||||||
|
<HeaderRow>
|
||||||
|
<LinkIconButton onClick={onOpenDatabaseClick}>
|
||||||
|
<span slot="start" className="codicon codicon-package"></span>
|
||||||
|
Open database
|
||||||
|
</LinkIconButton>
|
||||||
|
<LinkIconButton onClick={onOpenExtensionPackClick}>
|
||||||
|
<span slot="start" className="codicon codicon-package"></span>
|
||||||
|
Open extension pack
|
||||||
|
</LinkIconButton>
|
||||||
|
{viewState.enableFrameworkMode && (
|
||||||
|
<LinkIconButton onClick={onSwitchModeClick}>
|
||||||
|
<span slot="start" className="codicon codicon-library"></span>
|
||||||
|
{viewState.mode === Mode.Framework
|
||||||
|
? "Model as application"
|
||||||
|
: "Model as dependency"}
|
||||||
|
</LinkIconButton>
|
||||||
|
)}
|
||||||
|
</HeaderRow>
|
||||||
|
</HeaderColumn>
|
||||||
|
<HeaderSpacer />
|
||||||
|
<HeaderColumn>
|
||||||
|
<VSCodeCheckbox>Hide modeled APIs</VSCodeCheckbox>
|
||||||
|
</HeaderColumn>
|
||||||
|
</HeaderContainer>
|
||||||
|
|
||||||
{externalApiUsages.length > 0 && (
|
<EditorContainer>
|
||||||
<>
|
<ButtonsContainer>
|
||||||
<HeaderContainer>
|
<VSCodeButton
|
||||||
<HeaderColumn>
|
onClick={onSaveAllClick}
|
||||||
<HeaderRow>
|
disabled={modifiedSignatures.size === 0}
|
||||||
<ViewTitle>
|
>
|
||||||
{getLanguageDisplayName(viewState.extensionPack.language)}
|
Save all
|
||||||
</ViewTitle>
|
</VSCodeButton>
|
||||||
<VSCodeTag>
|
{viewState.enableFrameworkMode && (
|
||||||
{percentFormatter.format(modeledPercentage / 100)} modeled
|
<VSCodeButton appearance="secondary" onClick={onRefreshClick}>
|
||||||
</VSCodeTag>
|
Refresh
|
||||||
</HeaderRow>
|
</VSCodeButton>
|
||||||
<HeaderRow>
|
)}
|
||||||
<>{viewState.extensionPack.name}</>
|
{viewState.mode === Mode.Framework && (
|
||||||
</HeaderRow>
|
<VSCodeButton onClick={onGenerateFromSourceClick}>
|
||||||
<HeaderRow>
|
Generate
|
||||||
<LinkIconButton onClick={onOpenDatabaseClick}>
|
</VSCodeButton>
|
||||||
<span slot="start" className="codicon codicon-package"></span>
|
)}
|
||||||
Open database
|
</ButtonsContainer>
|
||||||
</LinkIconButton>
|
<ModeledMethodsList
|
||||||
<LinkIconButton onClick={onOpenExtensionPackClick}>
|
externalApiUsages={externalApiUsages}
|
||||||
<span slot="start" className="codicon codicon-package"></span>
|
modeledMethods={modeledMethods}
|
||||||
Open extension pack
|
modifiedSignatures={modifiedSignatures}
|
||||||
</LinkIconButton>
|
viewState={viewState}
|
||||||
{viewState.enableFrameworkMode && (
|
onChange={onChange}
|
||||||
<LinkIconButton onClick={onSwitchModeClick}>
|
onSaveModelClick={onSaveModelClick}
|
||||||
<span
|
onGenerateFromLlmClick={onGenerateFromLlmClick}
|
||||||
slot="start"
|
onGenerateFromSourceClick={onGenerateFromSourceClick}
|
||||||
className="codicon codicon-library"
|
onModelDependencyClick={onModelDependencyClick}
|
||||||
></span>
|
/>
|
||||||
{viewState.mode === Mode.Framework
|
</EditorContainer>
|
||||||
? "Model as application"
|
|
||||||
: "Model as dependency"}
|
|
||||||
</LinkIconButton>
|
|
||||||
)}
|
|
||||||
</HeaderRow>
|
|
||||||
</HeaderColumn>
|
|
||||||
<HeaderSpacer />
|
|
||||||
<HeaderColumn>
|
|
||||||
<VSCodeCheckbox>Hide modeled APIs</VSCodeCheckbox>
|
|
||||||
</HeaderColumn>
|
|
||||||
</HeaderContainer>
|
|
||||||
|
|
||||||
<EditorContainer>
|
|
||||||
<ButtonsContainer>
|
|
||||||
<VSCodeButton
|
|
||||||
onClick={onSaveAllClick}
|
|
||||||
disabled={modifiedSignatures.size === 0}
|
|
||||||
>
|
|
||||||
Save all
|
|
||||||
</VSCodeButton>
|
|
||||||
{viewState.enableFrameworkMode && (
|
|
||||||
<VSCodeButton appearance="secondary" onClick={onRefreshClick}>
|
|
||||||
Refresh
|
|
||||||
</VSCodeButton>
|
|
||||||
)}
|
|
||||||
{viewState.mode === Mode.Framework && (
|
|
||||||
<VSCodeButton onClick={onGenerateFromSourceClick}>
|
|
||||||
Generate
|
|
||||||
</VSCodeButton>
|
|
||||||
)}
|
|
||||||
</ButtonsContainer>
|
|
||||||
<ModeledMethodsList
|
|
||||||
externalApiUsages={externalApiUsages}
|
|
||||||
modeledMethods={modeledMethods}
|
|
||||||
modifiedSignatures={modifiedSignatures}
|
|
||||||
viewState={viewState}
|
|
||||||
onChange={onChange}
|
|
||||||
onSaveModelClick={onSaveModelClick}
|
|
||||||
onGenerateFromLlmClick={onGenerateFromLlmClick}
|
|
||||||
onGenerateFromSourceClick={onGenerateFromSourceClick}
|
|
||||||
onModelDependencyClick={onModelDependencyClick}
|
|
||||||
/>
|
|
||||||
</EditorContainer>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</DataExtensionsEditorContainer>
|
</DataExtensionsEditorContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import {
|
|||||||
import { vscode } from "../vscode-api";
|
import { vscode } from "../vscode-api";
|
||||||
import { AnalyzedRepoItemContent } from "./AnalyzedRepoItemContent";
|
import { AnalyzedRepoItemContent } from "./AnalyzedRepoItemContent";
|
||||||
import StarCount from "../common/StarCount";
|
import StarCount from "../common/StarCount";
|
||||||
import { LastUpdated } from "../common/LastUpdated";
|
|
||||||
import { useTelemetryOnChange } from "../common/telemetry";
|
import { useTelemetryOnChange } from "../common/telemetry";
|
||||||
import { DeterminateProgressRing } from "../common/DeterminateProgressRing";
|
import { DeterminateProgressRing } from "../common/DeterminateProgressRing";
|
||||||
|
|
||||||
@@ -297,7 +296,6 @@ export const RepoRow = ({
|
|||||||
<div>
|
<div>
|
||||||
<StarCount starCount={repository.stargazersCount} />
|
<StarCount starCount={repository.stargazersCount} />
|
||||||
</div>
|
</div>
|
||||||
<LastUpdated lastUpdated={repository.updatedAt} />
|
|
||||||
</MetadataContainer>
|
</MetadataContainer>
|
||||||
</TitleContainer>
|
</TitleContainer>
|
||||||
{isExpanded && expandableContentLoaded && (
|
{isExpanded && expandableContentLoaded && (
|
||||||
|
|||||||
@@ -34,9 +34,6 @@ export const RepositoriesSort = ({ value, onChange, className }: Props) => {
|
|||||||
Number of results
|
Number of results
|
||||||
</VSCodeOption>
|
</VSCodeOption>
|
||||||
<VSCodeOption value={SortKey.Popularity}>Popularity</VSCodeOption>
|
<VSCodeOption value={SortKey.Popularity}>Popularity</VSCodeOption>
|
||||||
<VSCodeOption value={SortKey.MostRecentCommit}>
|
|
||||||
Most recent commit
|
|
||||||
</VSCodeOption>
|
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -38,10 +38,7 @@ describe(RepoRow.name, () => {
|
|||||||
expect(
|
expect(
|
||||||
screen.queryByRole("img", {
|
screen.queryByRole("img", {
|
||||||
// There should not be any icons, except for the icons which are always shown
|
// There should not be any icons, except for the icons which are always shown
|
||||||
name: (name) =>
|
name: (name) => !["expand", "stars count"].includes(name.toLowerCase()),
|
||||||
!["expand", "stars count", "most recent commit"].includes(
|
|
||||||
name.toLowerCase(),
|
|
||||||
),
|
|
||||||
}),
|
}),
|
||||||
).not.toBeInTheDocument();
|
).not.toBeInTheDocument();
|
||||||
|
|
||||||
@@ -279,26 +276,7 @@ describe(RepoRow.name, () => {
|
|||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows updated at", () => {
|
it("does not show star count when unknown", () => {
|
||||||
render({
|
|
||||||
repository: {
|
|
||||||
...createMockRepositoryWithMetadata(),
|
|
||||||
// 1 month ago
|
|
||||||
updatedAt: new Date(
|
|
||||||
Date.now() - 1000 * 60 * 60 * 24 * 30,
|
|
||||||
).toISOString(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(screen.getByText("last month")).toBeInTheDocument();
|
|
||||||
expect(
|
|
||||||
screen.getByRole("img", {
|
|
||||||
name: "Most recent commit",
|
|
||||||
}),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not show star count and updated at when unknown", () => {
|
|
||||||
render({
|
render({
|
||||||
repository: {
|
repository: {
|
||||||
id: undefined,
|
id: undefined,
|
||||||
@@ -312,11 +290,6 @@ describe(RepoRow.name, () => {
|
|||||||
name: "Stars count",
|
name: "Stars count",
|
||||||
}),
|
}),
|
||||||
).not.toBeInTheDocument();
|
).not.toBeInTheDocument();
|
||||||
expect(
|
|
||||||
screen.queryByRole("img", {
|
|
||||||
name: "Most recent commit",
|
|
||||||
}),
|
|
||||||
).not.toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can expand the repo item", async () => {
|
it("can expand the repo item", async () => {
|
||||||
|
|||||||
@@ -204,55 +204,6 @@ describe(compareRepository.name, () => {
|
|||||||
).toBeLessThan(0);
|
).toBeLessThan(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("when sort key is 'Most recent commit'", () => {
|
|
||||||
const sorter = compareRepository({
|
|
||||||
...permissiveFilterSortState,
|
|
||||||
sortKey: SortKey.MostRecentCommit,
|
|
||||||
});
|
|
||||||
|
|
||||||
const left = {
|
|
||||||
fullName: "github/galaxy",
|
|
||||||
updatedAt: "2020-01-01T00:00:00Z",
|
|
||||||
};
|
|
||||||
const right = {
|
|
||||||
fullName: "github/world",
|
|
||||||
updatedAt: "2021-01-01T00:00:00Z",
|
|
||||||
};
|
|
||||||
|
|
||||||
it("compares correctly", () => {
|
|
||||||
expect(sorter(left, right)).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("compares the inverse correctly", () => {
|
|
||||||
expect(sorter(right, left)).toBeLessThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("compares equal values correctly", () => {
|
|
||||||
expect(sorter(left, left)).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("compares equal single values correctly", () => {
|
|
||||||
expect(
|
|
||||||
sorter(left, {
|
|
||||||
...right,
|
|
||||||
updatedAt: left.updatedAt,
|
|
||||||
}),
|
|
||||||
).toBeLessThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("compares missing single values correctly", () => {
|
|
||||||
expect(
|
|
||||||
sorter(
|
|
||||||
{
|
|
||||||
...left,
|
|
||||||
updatedAt: undefined,
|
|
||||||
},
|
|
||||||
right,
|
|
||||||
),
|
|
||||||
).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe(compareWithResults.name, () => {
|
describe(compareWithResults.name, () => {
|
||||||
@@ -303,32 +254,6 @@ describe(compareWithResults.name, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("when sort key is 'Most recent commit'", () => {
|
|
||||||
const sorter = compareWithResults({
|
|
||||||
...permissiveFilterSortState,
|
|
||||||
sortKey: SortKey.MostRecentCommit,
|
|
||||||
});
|
|
||||||
|
|
||||||
const left = {
|
|
||||||
repository: {
|
|
||||||
id: 11,
|
|
||||||
fullName: "github/galaxy",
|
|
||||||
updatedAt: "2020-01-01T00:00:00Z",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const right = {
|
|
||||||
repository: {
|
|
||||||
id: 12,
|
|
||||||
fullName: "github/world",
|
|
||||||
updatedAt: "2021-01-01T00:00:00Z",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
it("compares correctly", () => {
|
|
||||||
expect(sorter(left, right)).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when sort key is results count", () => {
|
describe("when sort key is results count", () => {
|
||||||
const sorter = compareWithResults({
|
const sorter = compareWithResults({
|
||||||
...permissiveFilterSortState,
|
...permissiveFilterSortState,
|
||||||
|
|||||||
@@ -5,23 +5,11 @@ import { getErrorMessage } from "../../../../../src/common/helpers-pure";
|
|||||||
|
|
||||||
import * as log from "../../../../../src/common/logging/notifications";
|
import * as log from "../../../../../src/common/logging/notifications";
|
||||||
import * as workspaceFolders from "../../../../../src/common/vscode/workspace-folders";
|
import * as workspaceFolders from "../../../../../src/common/vscode/workspace-folders";
|
||||||
import * as qlpack from "../../../../../src/databases/qlpack";
|
import { KeyType, resolveQueries } from "../../../../../src/language-support";
|
||||||
import {
|
|
||||||
KeyType,
|
|
||||||
qlpackOfDatabase,
|
|
||||||
resolveQueries,
|
|
||||||
} from "../../../../../src/language-support";
|
|
||||||
import { CodeQLCliServer } from "../../../../../src/codeql-cli/cli";
|
import { CodeQLCliServer } from "../../../../../src/codeql-cli/cli";
|
||||||
import { mockDatabaseItem, mockedObject } from "../../../utils/mocking.helpers";
|
import { mockedObject } from "../../../utils/mocking.helpers";
|
||||||
|
|
||||||
describe("queryResolver", () => {
|
describe("queryResolver", () => {
|
||||||
let getQlPackForDbschemeSpy: jest.SpiedFunction<
|
|
||||||
typeof qlpack.getQlPackForDbscheme
|
|
||||||
>;
|
|
||||||
let getPrimaryDbschemeSpy: jest.SpiedFunction<
|
|
||||||
typeof qlpack.getPrimaryDbscheme
|
|
||||||
>;
|
|
||||||
|
|
||||||
const resolveQueriesInSuite = jest.fn();
|
const resolveQueriesInSuite = jest.fn();
|
||||||
|
|
||||||
const mockCli = mockedObject<CodeQLCliServer>({
|
const mockCli = mockedObject<CodeQLCliServer>({
|
||||||
@@ -29,16 +17,6 @@ describe("queryResolver", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
getQlPackForDbschemeSpy = jest
|
|
||||||
.spyOn(qlpack, "getQlPackForDbscheme")
|
|
||||||
.mockResolvedValue({
|
|
||||||
dbschemePack: "dbschemePack",
|
|
||||||
dbschemePackIsLibraryPack: false,
|
|
||||||
});
|
|
||||||
getPrimaryDbschemeSpy = jest
|
|
||||||
.spyOn(qlpack, "getPrimaryDbscheme")
|
|
||||||
.mockResolvedValue("primaryDbscheme");
|
|
||||||
|
|
||||||
jest
|
jest
|
||||||
.spyOn(workspaceFolders, "getOnDiskWorkspaceFolders")
|
.spyOn(workspaceFolders, "getOnDiskWorkspaceFolders")
|
||||||
.mockReturnValue([]);
|
.mockReturnValue([]);
|
||||||
@@ -68,7 +46,7 @@ describe("queryResolver", () => {
|
|||||||
queries: ".",
|
queries: ".",
|
||||||
include: {
|
include: {
|
||||||
kind: "definitions",
|
kind: "definitions",
|
||||||
"tags contain": "ide-contextual-queries/local-definitions",
|
"tags contain": ["ide-contextual-queries/local-definitions"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@@ -87,31 +65,9 @@ describe("queryResolver", () => {
|
|||||||
expect(true).toBe(false);
|
expect(true).toBe(false);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
expect(getErrorMessage(e)).toBe(
|
expect(getErrorMessage(e)).toBe(
|
||||||
'No definitions queries (tagged "ide-contextual-queries/local-definitions") could be found in the current library path (tried searching the following packs: my-qlpack). Try upgrading the CodeQL libraries. If that doesn\'t work, then definitions queries are not yet available for this language.',
|
'No definitions queries (kind "definitions", tagged "ide-contextual-queries/local-definitions") could be found in the current library path (tried searching the following packs: my-qlpack). Try upgrading the CodeQL libraries. If that doesn\'t work, then definitions queries are not yet available for this language.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("qlpackOfDatabase", () => {
|
|
||||||
it("should get the qlpack of a database", async () => {
|
|
||||||
getQlPackForDbschemeSpy.mockResolvedValue({
|
|
||||||
dbschemePack: "my-qlpack",
|
|
||||||
dbschemePackIsLibraryPack: false,
|
|
||||||
});
|
|
||||||
const db = mockDatabaseItem({
|
|
||||||
contents: {
|
|
||||||
datasetUri: {
|
|
||||||
fsPath: "/path/to/database",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const result = await qlpackOfDatabase(mockCli, db);
|
|
||||||
expect(result).toEqual({
|
|
||||||
dbschemePack: "my-qlpack",
|
|
||||||
dbschemePackIsLibraryPack: false,
|
|
||||||
});
|
|
||||||
expect(getPrimaryDbschemeSpy).toBeCalledWith("/path/to/database");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import {
|
||||||
|
qlpackOfDatabase,
|
||||||
|
resolveQueries,
|
||||||
|
} from "../../../../src/local-queries";
|
||||||
|
import { mockDatabaseItem, mockedObject } from "../../utils/mocking.helpers";
|
||||||
|
import { CodeQLCliServer } from "../../../../src/codeql-cli/cli";
|
||||||
|
import * as qlpack from "../../../../src/databases/qlpack";
|
||||||
|
import * as workspaceFolders from "../../../../src/common/vscode/workspace-folders";
|
||||||
|
import * as log from "../../../../src/common/logging/notifications";
|
||||||
|
import { load } from "js-yaml";
|
||||||
|
import * as fs from "fs-extra";
|
||||||
|
|
||||||
|
describe("qlpackOfDatabase", () => {
|
||||||
|
let getQlPackForDbschemeSpy: jest.SpiedFunction<
|
||||||
|
typeof qlpack.getQlPackForDbscheme
|
||||||
|
>;
|
||||||
|
let getPrimaryDbschemeSpy: jest.SpiedFunction<
|
||||||
|
typeof qlpack.getPrimaryDbscheme
|
||||||
|
>;
|
||||||
|
|
||||||
|
const mockCli = mockedObject<CodeQLCliServer>({});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
getQlPackForDbschemeSpy = jest
|
||||||
|
.spyOn(qlpack, "getQlPackForDbscheme")
|
||||||
|
.mockResolvedValue({
|
||||||
|
dbschemePack: "dbschemePack",
|
||||||
|
dbschemePackIsLibraryPack: false,
|
||||||
|
});
|
||||||
|
getPrimaryDbschemeSpy = jest
|
||||||
|
.spyOn(qlpack, "getPrimaryDbscheme")
|
||||||
|
.mockResolvedValue("primaryDbscheme");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should get the qlpack of a database", async () => {
|
||||||
|
getQlPackForDbschemeSpy.mockResolvedValue({
|
||||||
|
dbschemePack: "my-qlpack",
|
||||||
|
dbschemePackIsLibraryPack: false,
|
||||||
|
});
|
||||||
|
const db = mockDatabaseItem({
|
||||||
|
contents: {
|
||||||
|
datasetUri: {
|
||||||
|
fsPath: "/path/to/database",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const result = await qlpackOfDatabase(mockCli, db);
|
||||||
|
expect(result).toEqual({
|
||||||
|
dbschemePack: "my-qlpack",
|
||||||
|
dbschemePackIsLibraryPack: false,
|
||||||
|
});
|
||||||
|
expect(getPrimaryDbschemeSpy).toBeCalledWith("/path/to/database");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("resolveQueries", () => {
|
||||||
|
const resolveQueriesInSuite = jest.fn();
|
||||||
|
|
||||||
|
const mockCli = mockedObject<CodeQLCliServer>({
|
||||||
|
resolveQueriesInSuite,
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest
|
||||||
|
.spyOn(workspaceFolders, "getOnDiskWorkspaceFolders")
|
||||||
|
.mockReturnValue([]);
|
||||||
|
jest.spyOn(log, "showAndLogErrorMessage").mockResolvedValue(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should resolve a query", async () => {
|
||||||
|
resolveQueriesInSuite.mockReturnValue(["a", "b"]);
|
||||||
|
const result = await resolveQueries(
|
||||||
|
mockCli,
|
||||||
|
{ dbschemePack: "my-qlpack", dbschemePackIsLibraryPack: false },
|
||||||
|
"my query",
|
||||||
|
{
|
||||||
|
kind: "graph",
|
||||||
|
"tags contain": ["ide-contextual-queries/print-ast"],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect(result).toEqual(["a", "b"]);
|
||||||
|
|
||||||
|
expect(resolveQueriesInSuite).toHaveBeenCalledWith(
|
||||||
|
expect.stringMatching(/\.qls$/),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const fileName = resolveQueriesInSuite.mock.calls[0][0];
|
||||||
|
|
||||||
|
expect(load(await fs.readFile(fileName, "utf-8"))).toEqual([
|
||||||
|
{
|
||||||
|
from: "my-qlpack",
|
||||||
|
queries: ".",
|
||||||
|
include: {
|
||||||
|
kind: "graph",
|
||||||
|
"tags contain": ["ide-contextual-queries/print-ast"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw an error when there are no queries found", async () => {
|
||||||
|
resolveQueriesInSuite.mockReturnValue([]);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
resolveQueries(
|
||||||
|
mockCli,
|
||||||
|
{ dbschemePack: "my-qlpack", dbschemePackIsLibraryPack: false },
|
||||||
|
"my query",
|
||||||
|
{
|
||||||
|
kind: "graph",
|
||||||
|
"tags contain": ["ide-contextual-queries/print-ast"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).rejects.toThrowError(
|
||||||
|
'No my query queries (kind "graph", tagged "ide-contextual-queries/print-ast") could be found in the current library path (tried searching the following packs: my-qlpack). Try upgrading the CodeQL libraries. If that doesn\'t work, then my query queries are not yet available for this language.',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import { mockedObject } from "../../utils/mocking.helpers";
|
||||||
|
import { CodeQLCliServer } from "../../../../src/codeql-cli/cli";
|
||||||
|
import { dir, DirectoryResult } from "tmp-promise";
|
||||||
|
import { join } from "path";
|
||||||
|
import { createLockFileForStandardQuery } from "../../../../src/local-queries/standard-queries";
|
||||||
|
import { outputFile, pathExists } from "fs-extra";
|
||||||
|
|
||||||
|
describe("createLockFileForStandardQuery", () => {
|
||||||
|
let tmpDir: DirectoryResult;
|
||||||
|
let packPath: string;
|
||||||
|
let qlpackPath: string;
|
||||||
|
let queryPath: string;
|
||||||
|
|
||||||
|
const packPacklist = jest.fn();
|
||||||
|
const packResolveDependencies = jest.fn();
|
||||||
|
const clearCache = jest.fn();
|
||||||
|
const packInstall = jest.fn();
|
||||||
|
|
||||||
|
const mockCli = mockedObject<CodeQLCliServer>({
|
||||||
|
packPacklist,
|
||||||
|
packResolveDependencies,
|
||||||
|
clearCache,
|
||||||
|
packInstall,
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
tmpDir = await dir({
|
||||||
|
unsafeCleanup: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
packPath = join(tmpDir.path, "a", "b");
|
||||||
|
qlpackPath = join(packPath, "qlpack.yml");
|
||||||
|
queryPath = join(packPath, "d", "e", "query.ql");
|
||||||
|
|
||||||
|
packPacklist.mockResolvedValue([qlpackPath, queryPath]);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await tmpDir.cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when the lock file exists", () => {
|
||||||
|
let lockfilePath: string;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
lockfilePath = join(packPath, "qlpack.lock.yml");
|
||||||
|
|
||||||
|
packPacklist.mockResolvedValue([qlpackPath, lockfilePath, queryPath]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not resolve or install dependencies", async () => {
|
||||||
|
expect(await createLockFileForStandardQuery(mockCli, queryPath)).toEqual({
|
||||||
|
cleanup: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(packResolveDependencies).not.toHaveBeenCalled();
|
||||||
|
expect(clearCache).not.toHaveBeenCalled();
|
||||||
|
expect(packInstall).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not resolve or install dependencies with a codeql-pack.lock.yml", async () => {
|
||||||
|
lockfilePath = join(packPath, "codeql-pack.lock.yml");
|
||||||
|
|
||||||
|
packPacklist.mockResolvedValue([qlpackPath, lockfilePath, queryPath]);
|
||||||
|
|
||||||
|
expect(await createLockFileForStandardQuery(mockCli, queryPath)).toEqual({
|
||||||
|
cleanup: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(packResolveDependencies).not.toHaveBeenCalled();
|
||||||
|
expect(clearCache).not.toHaveBeenCalled();
|
||||||
|
expect(packInstall).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when the lock file does not exist", () => {
|
||||||
|
it("resolves and installs dependencies", async () => {
|
||||||
|
expect(await createLockFileForStandardQuery(mockCli, queryPath)).toEqual({
|
||||||
|
cleanup: expect.any(Function),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(packResolveDependencies).toHaveBeenCalledWith(packPath);
|
||||||
|
expect(clearCache).toHaveBeenCalledWith();
|
||||||
|
expect(packInstall).toHaveBeenCalledWith(packPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cleans up the lock file using the cleanup function", async () => {
|
||||||
|
const { cleanup } = await createLockFileForStandardQuery(
|
||||||
|
mockCli,
|
||||||
|
queryPath,
|
||||||
|
);
|
||||||
|
expect(cleanup).not.toBeUndefined();
|
||||||
|
|
||||||
|
const lockfilePath = join(packPath, "codeql-pack.lock.yml");
|
||||||
|
|
||||||
|
await outputFile(lockfilePath, "lock file contents");
|
||||||
|
|
||||||
|
await cleanup?.();
|
||||||
|
|
||||||
|
expect(await pathExists(lockfilePath)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not fail when cleaning up a non-existing lock file", async () => {
|
||||||
|
const { cleanup } = await createLockFileForStandardQuery(
|
||||||
|
mockCli,
|
||||||
|
queryPath,
|
||||||
|
);
|
||||||
|
expect(cleanup).not.toBeUndefined();
|
||||||
|
|
||||||
|
await cleanup?.();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user