Merge branch 'main' into robertbrignull/multiple-models-method-row
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
**/* @github/codeql-vscode-reviewers
|
||||
**/variant-analysis/ @github/code-scanning-secexp-reviewers
|
||||
**/databases/ @github/code-scanning-secexp-reviewers
|
||||
**/method-modeling/ @github/code-scanning-secexp-reviewers
|
||||
**/model-editor/ @github/code-scanning-secexp-reviewers
|
||||
**/queries-panel/ @github/code-scanning-secexp-reviewers
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
## [UNRELEASED]
|
||||
|
||||
- Fix a bug where the query to Find Definitions in database source files would not be cancelled appropriately. [#2885](https://github.com/github/vscode-codeql/pull/2885)
|
||||
- It is now possible to show the language of query history items using the `%l` specifier in the `codeQL.queryHistory.format` setting. Note that this only works for queries run after this upgrade, and older items will show `unknown` as a language. [#2892](https://github.com/github/vscode-codeql/pull/2892)
|
||||
- Increase the required version of VS Code to 1.82.0. [#2877](https://github.com/github/vscode-codeql/pull/2877)
|
||||
- Fix a bug where the query server was restarted twice after configuration changes. [#2884](https://github.com/github/vscode-codeql/pull/2884).
|
||||
- Add support for the `telemetry.telemetryLevel` setting. For more information, see the [telemetry documentation](https://codeql.github.com/docs/codeql-for-visual-studio-code/about-telemetry-in-codeql-for-visual-studio-code). [#2824](https://github.com/github/vscode-codeql/pull/2824).
|
||||
|
||||
## 1.9.1 - 29 September 2023
|
||||
|
||||
|
||||
@@ -446,13 +446,20 @@
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"scope": "application",
|
||||
"markdownDescription": "Specifies whether to send CodeQL usage telemetry. This setting AND the global `#telemetry.enableTelemetry#` setting must be checked for telemetry to be sent to GitHub. For more information, see the [telemetry documentation](https://codeql.github.com/docs/codeql-for-visual-studio-code/about-telemetry-in-codeql-for-visual-studio-code)"
|
||||
"markdownDescription": "Specifies whether to send CodeQL usage telemetry. This setting AND the one of the global telemetry settings (`#telemetry.enableTelemetry#` or `#telemetry.telemetryLevel#`) must be enabled for telemetry to be sent to GitHub. For more information, see the [telemetry documentation](https://codeql.github.com/docs/codeql-for-visual-studio-code/about-telemetry-in-codeql-for-visual-studio-code)",
|
||||
"tags": [
|
||||
"telemetry",
|
||||
"usesOnlineServices"
|
||||
]
|
||||
},
|
||||
"codeQL.telemetry.logTelemetry": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"scope": "application",
|
||||
"description": "Specifies whether or not to write telemetry events to the extension log."
|
||||
"description": "Specifies whether or not to write telemetry events to the extension log.",
|
||||
"tags": [
|
||||
"telemetry"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1996,7 +2003,7 @@
|
||||
"id": "codeQLMethodModeling",
|
||||
"type": "webview",
|
||||
"name": "CodeQL Method Modeling",
|
||||
"when": "config.codeQL.canary && config.codeQL.model.methodModelingView && codeql.modelEditorOpen && !codeql.modelEditorActive"
|
||||
"when": "config.codeQL.canary"
|
||||
}
|
||||
],
|
||||
"codeql-methods-usage": [
|
||||
|
||||
@@ -323,6 +323,7 @@ export type PackagingCommands = {
|
||||
|
||||
export type ModelEditorCommands = {
|
||||
"codeQL.openModelEditor": () => Promise<void>;
|
||||
"codeQL.openModelEditorFromModelingPanel": () => Promise<void>;
|
||||
"codeQLModelEditor.jumpToUsageLocation": (
|
||||
method: Method,
|
||||
usage: Usage,
|
||||
|
||||
@@ -9,10 +9,16 @@ export type DisposeHandler = (disposable: Disposable) => void;
|
||||
/**
|
||||
* Base class to make it easier to implement a `Disposable` that owns other disposable object.
|
||||
*/
|
||||
export abstract class DisposableObject implements Disposable {
|
||||
export class DisposableObject implements Disposable {
|
||||
private disposables: Disposable[] = [];
|
||||
private tracked?: Set<Disposable> = undefined;
|
||||
|
||||
constructor(...dispoables: Disposable[]) {
|
||||
for (const d of dispoables) {
|
||||
this.push(d);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds `obj` to a list of objects to dispose when `this` is disposed. Objects added by `push` are
|
||||
* disposed in reverse order of being added.
|
||||
|
||||
@@ -19,7 +19,10 @@ import { ErrorLike } from "../common/errors";
|
||||
import { DataFlowPaths } from "../variant-analysis/shared/data-flow-paths";
|
||||
import { Method, Usage } from "../model-editor/method";
|
||||
import { ModeledMethod } from "../model-editor/modeled-method";
|
||||
import { ModelEditorViewState } from "../model-editor/shared/view-state";
|
||||
import {
|
||||
MethodModelingPanelViewState,
|
||||
ModelEditorViewState,
|
||||
} from "../model-editor/shared/view-state";
|
||||
import { Mode } from "../model-editor/shared/mode";
|
||||
import { QueryLanguage } from "./query-language";
|
||||
|
||||
@@ -543,8 +546,7 @@ interface RefreshMethods {
|
||||
|
||||
interface SaveModeledMethods {
|
||||
t: "saveModeledMethods";
|
||||
methods: Method[];
|
||||
modeledMethods: Record<string, ModeledMethod>;
|
||||
methodSignatures?: string[];
|
||||
}
|
||||
|
||||
interface GenerateMethodMessage {
|
||||
@@ -577,9 +579,14 @@ interface SetModeledMethodMessage {
|
||||
method: ModeledMethod;
|
||||
}
|
||||
|
||||
interface SetInModelingModeMessage {
|
||||
t: "setInModelingMode";
|
||||
inModelingMode: boolean;
|
||||
}
|
||||
|
||||
interface RevealMethodMessage {
|
||||
t: "revealMethod";
|
||||
method: Method;
|
||||
methodSignature: string;
|
||||
}
|
||||
|
||||
export type ToModelEditorMessage =
|
||||
@@ -610,10 +617,20 @@ interface RevealInEditorMessage {
|
||||
method: Method;
|
||||
}
|
||||
|
||||
interface StartModelingMessage {
|
||||
t: "startModeling";
|
||||
}
|
||||
|
||||
export type FromMethodModelingMessage =
|
||||
| CommonFromViewMessages
|
||||
| SetModeledMethodMessage
|
||||
| RevealInEditorMessage;
|
||||
| RevealInEditorMessage
|
||||
| StartModelingMessage;
|
||||
|
||||
interface SetMethodModelingPanelViewStateMessage {
|
||||
t: "setMethodModelingPanelViewState";
|
||||
viewState: MethodModelingPanelViewState;
|
||||
}
|
||||
|
||||
interface SetMethodMessage {
|
||||
t: "setMethod";
|
||||
@@ -633,7 +650,9 @@ interface SetSelectedMethodMessage {
|
||||
}
|
||||
|
||||
export type ToMethodModelingMessage =
|
||||
| SetMethodModelingPanelViewStateMessage
|
||||
| SetMethodMessage
|
||||
| SetModeledMethodMessage
|
||||
| SetMethodModifiedMessage
|
||||
| SetSelectedMethodMessage;
|
||||
| SetSelectedMethodMessage
|
||||
| SetInModelingModeMessage;
|
||||
|
||||
@@ -40,10 +40,7 @@ export const PACKS_BY_QUERY_LANGUAGE = {
|
||||
],
|
||||
[QueryLanguage.Go]: ["codeql/go-queries"],
|
||||
[QueryLanguage.Java]: ["codeql/java-queries"],
|
||||
[QueryLanguage.Javascript]: [
|
||||
"codeql/javascript-queries",
|
||||
"codeql/javascript-experimental-atm-queries",
|
||||
],
|
||||
[QueryLanguage.Javascript]: ["codeql/javascript-queries"],
|
||||
[QueryLanguage.Python]: ["codeql/python-queries"],
|
||||
[QueryLanguage.Ruby]: ["codeql/ruby-queries"],
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ export abstract class AbstractWebviewViewProvider<
|
||||
private disposables: Disposable[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly app: App,
|
||||
protected readonly app: App,
|
||||
private readonly webviewKind: WebviewKind,
|
||||
) {}
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { CancellationToken, Disposable } from "vscode";
|
||||
import { DisposableObject } from "../disposable-object";
|
||||
|
||||
/**
|
||||
* A cancellation token that cancels when any of its constituent
|
||||
* cancellation tokens are cancelled.
|
||||
*/
|
||||
export class MultiCancellationToken implements CancellationToken {
|
||||
private readonly tokens: CancellationToken[];
|
||||
|
||||
constructor(...tokens: CancellationToken[]) {
|
||||
this.tokens = tokens;
|
||||
}
|
||||
|
||||
get isCancellationRequested(): boolean {
|
||||
return this.tokens.some((t) => t.isCancellationRequested);
|
||||
}
|
||||
|
||||
onCancellationRequested<T>(listener: (e: T) => any): Disposable {
|
||||
return new DisposableObject(
|
||||
...this.tokens.map((t) => t.onCancellationRequested(listener)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,13 @@ import {
|
||||
Extension,
|
||||
ExtensionContext,
|
||||
ConfigurationChangeEvent,
|
||||
env,
|
||||
} from "vscode";
|
||||
import TelemetryReporter from "vscode-extension-telemetry";
|
||||
import {
|
||||
ConfigListener,
|
||||
CANARY_FEATURES,
|
||||
ENABLE_TELEMETRY,
|
||||
GLOBAL_ENABLE_TELEMETRY,
|
||||
LOG_TELEMETRY,
|
||||
isIntegrationTestMode,
|
||||
isCanary,
|
||||
@@ -59,8 +59,6 @@ export class ExtensionTelemetryListener
|
||||
extends ConfigListener
|
||||
implements AppTelemetry
|
||||
{
|
||||
static relevantSettings = [ENABLE_TELEMETRY, CANARY_FEATURES];
|
||||
|
||||
private reporter?: TelemetryReporter;
|
||||
|
||||
private cliVersionStr = NOT_SET_CLI_VERSION;
|
||||
@@ -72,6 +70,10 @@ export class ExtensionTelemetryListener
|
||||
private readonly ctx: ExtensionContext,
|
||||
) {
|
||||
super();
|
||||
|
||||
env.onDidChangeTelemetryEnabled(async () => {
|
||||
await this.initialize();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,10 +93,7 @@ export class ExtensionTelemetryListener
|
||||
async handleDidChangeConfiguration(
|
||||
e: ConfigurationChangeEvent,
|
||||
): Promise<void> {
|
||||
if (
|
||||
e.affectsConfiguration("codeQL.telemetry.enableTelemetry") ||
|
||||
e.affectsConfiguration("telemetry.enableTelemetry")
|
||||
) {
|
||||
if (e.affectsConfiguration(ENABLE_TELEMETRY.qualifiedName)) {
|
||||
await this.initialize();
|
||||
}
|
||||
|
||||
@@ -102,7 +101,7 @@ export class ExtensionTelemetryListener
|
||||
// Re-request if codeQL.canary is being set to `true` and telemetry
|
||||
// is not currently enabled.
|
||||
if (
|
||||
e.affectsConfiguration("codeQL.canary") &&
|
||||
e.affectsConfiguration(CANARY_FEATURES.qualifiedName) &&
|
||||
CANARY_FEATURES.getValue() &&
|
||||
!ENABLE_TELEMETRY.getValue()
|
||||
) {
|
||||
@@ -212,7 +211,7 @@ export class ExtensionTelemetryListener
|
||||
properties.stack = error.stack;
|
||||
}
|
||||
|
||||
this.reporter.sendTelemetryEvent("error", properties, {});
|
||||
this.reporter.sendTelemetryErrorEvent("error", properties, {});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -224,7 +223,7 @@ export class ExtensionTelemetryListener
|
||||
// if global telemetry is disabled, avoid showing the dialog or making any changes
|
||||
let result = undefined;
|
||||
if (
|
||||
GLOBAL_ENABLE_TELEMETRY.getValue() &&
|
||||
env.isTelemetryEnabled &&
|
||||
// Avoid showing the dialog if we are in integration test mode.
|
||||
!isIntegrationTestMode()
|
||||
) {
|
||||
|
||||
@@ -72,15 +72,8 @@ export const VSCODE_SAVE_BEFORE_START_SETTING = new Setting(
|
||||
|
||||
const ROOT_SETTING = new Setting("codeQL");
|
||||
|
||||
// Global configuration
|
||||
// Telemetry configuration
|
||||
const TELEMETRY_SETTING = new Setting("telemetry", ROOT_SETTING);
|
||||
const AST_VIEWER_SETTING = new Setting("astViewer", ROOT_SETTING);
|
||||
const CONTEXTUAL_QUERIES_SETTINGS = new Setting(
|
||||
"contextualQueries",
|
||||
ROOT_SETTING,
|
||||
);
|
||||
const GLOBAL_TELEMETRY_SETTING = new Setting("telemetry");
|
||||
const LOG_INSIGHTS_SETTING = new Setting("logInsights", ROOT_SETTING);
|
||||
|
||||
export const LOG_TELEMETRY = new Setting("logTelemetry", TELEMETRY_SETTING);
|
||||
export const ENABLE_TELEMETRY = new Setting(
|
||||
@@ -88,11 +81,6 @@ export const ENABLE_TELEMETRY = new Setting(
|
||||
TELEMETRY_SETTING,
|
||||
);
|
||||
|
||||
export const GLOBAL_ENABLE_TELEMETRY = new Setting(
|
||||
"enableTelemetry",
|
||||
GLOBAL_TELEMETRY_SETTING,
|
||||
);
|
||||
|
||||
// Distribution configuration
|
||||
const DISTRIBUTION_SETTING = new Setting("cli", ROOT_SETTING);
|
||||
export const CUSTOM_CODEQL_PATH_SETTING = new Setting(
|
||||
@@ -475,6 +463,7 @@ export function allowCanaryQueryServer() {
|
||||
return value === undefined ? true : !!value;
|
||||
}
|
||||
|
||||
const LOG_INSIGHTS_SETTING = new Setting("logInsights", ROOT_SETTING);
|
||||
export const JOIN_ORDER_WARNING_THRESHOLD = new Setting(
|
||||
"joinOrderWarningThreshold",
|
||||
LOG_INSIGHTS_SETTING,
|
||||
@@ -484,6 +473,7 @@ export function joinOrderWarningThreshold(): number {
|
||||
return JOIN_ORDER_WARNING_THRESHOLD.getValue<number>();
|
||||
}
|
||||
|
||||
const AST_VIEWER_SETTING = new Setting("astViewer", ROOT_SETTING);
|
||||
/**
|
||||
* Hidden setting: Avoids caching in the AST viewer if the user is also a canary user.
|
||||
*/
|
||||
@@ -492,6 +482,10 @@ export const NO_CACHE_AST_VIEWER = new Setting(
|
||||
AST_VIEWER_SETTING,
|
||||
);
|
||||
|
||||
const CONTEXTUAL_QUERIES_SETTINGS = new Setting(
|
||||
"contextualQueries",
|
||||
ROOT_SETTING,
|
||||
);
|
||||
/**
|
||||
* Hidden setting: Avoids caching in jump to def and find refs contextual queries if the user is also a canary user.
|
||||
*/
|
||||
@@ -711,20 +705,33 @@ const LLM_GENERATION = new Setting("llmGeneration", MODEL_SETTING);
|
||||
const EXTENSIONS_DIRECTORY = new Setting("extensionsDirectory", MODEL_SETTING);
|
||||
const SHOW_MULTIPLE_MODELS = new Setting("showMultipleModels", MODEL_SETTING);
|
||||
|
||||
export function showFlowGeneration(): boolean {
|
||||
return !!FLOW_GENERATION.getValue<boolean>();
|
||||
export interface ModelConfig {
|
||||
flowGeneration: boolean;
|
||||
llmGeneration: boolean;
|
||||
getExtensionsDirectory(languageId: string): string | undefined;
|
||||
showMultipleModels: boolean;
|
||||
}
|
||||
|
||||
export function showLlmGeneration(): boolean {
|
||||
return !!LLM_GENERATION.getValue<boolean>();
|
||||
}
|
||||
export class ModelConfigListener extends ConfigListener implements ModelConfig {
|
||||
protected handleDidChangeConfiguration(e: ConfigurationChangeEvent): void {
|
||||
this.handleDidChangeConfigurationForRelevantSettings([MODEL_SETTING], e);
|
||||
}
|
||||
|
||||
export function getExtensionsDirectory(languageId: string): string | undefined {
|
||||
return EXTENSIONS_DIRECTORY.getValue<string>({
|
||||
languageId,
|
||||
});
|
||||
}
|
||||
public get flowGeneration(): boolean {
|
||||
return !!FLOW_GENERATION.getValue<boolean>();
|
||||
}
|
||||
|
||||
export function showMultipleModels(): boolean {
|
||||
return !!SHOW_MULTIPLE_MODELS.getValue<boolean>();
|
||||
public get llmGeneration(): boolean {
|
||||
return !!LLM_GENERATION.getValue<boolean>();
|
||||
}
|
||||
|
||||
public getExtensionsDirectory(languageId: string): string | undefined {
|
||||
return EXTENSIONS_DIRECTORY.getValue<string>({
|
||||
languageId,
|
||||
});
|
||||
}
|
||||
|
||||
public get showMultipleModels(): boolean {
|
||||
return !!SHOW_MULTIPLE_MODELS.getValue<boolean>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
import { CoreCompletedQuery, QueryRunner } from "../../query-server";
|
||||
import { AstBuilder } from "../ast-viewer/ast-builder";
|
||||
import { qlpackOfDatabase } from "../../local-queries";
|
||||
import { MultiCancellationToken } from "../../common/vscode/multi-cancellation-token";
|
||||
|
||||
/**
|
||||
* Runs templated CodeQL queries to find definitions in
|
||||
@@ -43,6 +44,7 @@ import { qlpackOfDatabase } from "../../local-queries";
|
||||
* generalize this to other custom queries, e.g. showing dataflow to
|
||||
* or from a selected identifier.
|
||||
*/
|
||||
|
||||
export class TemplateQueryDefinitionProvider implements DefinitionProvider {
|
||||
private cache: CachedOperation<LocationLink[]>;
|
||||
|
||||
@@ -60,11 +62,11 @@ export class TemplateQueryDefinitionProvider implements DefinitionProvider {
|
||||
async provideDefinition(
|
||||
document: TextDocument,
|
||||
position: Position,
|
||||
_token: CancellationToken,
|
||||
token: CancellationToken,
|
||||
): Promise<LocationLink[]> {
|
||||
const fileLinks = this.shouldUseCache()
|
||||
? await this.cache.get(document.uri.toString())
|
||||
: await this.getDefinitions(document.uri.toString());
|
||||
? await this.cache.get(document.uri.toString(), token)
|
||||
: await this.getDefinitions(document.uri.toString(), token);
|
||||
|
||||
const locLinks: LocationLink[] = [];
|
||||
for (const link of fileLinks) {
|
||||
@@ -79,9 +81,13 @@ export class TemplateQueryDefinitionProvider implements DefinitionProvider {
|
||||
return !(isCanary() && NO_CACHE_CONTEXTUAL_QUERIES.getValue<boolean>());
|
||||
}
|
||||
|
||||
private async getDefinitions(uriString: string): Promise<LocationLink[]> {
|
||||
private async getDefinitions(
|
||||
uriString: string,
|
||||
token: CancellationToken,
|
||||
): Promise<LocationLink[]> {
|
||||
return withProgress(
|
||||
async (progress, token) => {
|
||||
async (progress, tokenInner) => {
|
||||
const multiToken = new MultiCancellationToken(token, tokenInner);
|
||||
return getLocationsForUriString(
|
||||
this.cli,
|
||||
this.qs,
|
||||
@@ -90,7 +96,7 @@ export class TemplateQueryDefinitionProvider implements DefinitionProvider {
|
||||
KeyType.DefinitionQuery,
|
||||
this.queryStorageDir,
|
||||
progress,
|
||||
token,
|
||||
multiToken,
|
||||
(src, _dest) => src === uriString,
|
||||
);
|
||||
},
|
||||
@@ -126,11 +132,11 @@ export class TemplateQueryReferenceProvider implements ReferenceProvider {
|
||||
document: TextDocument,
|
||||
position: Position,
|
||||
_context: ReferenceContext,
|
||||
_token: CancellationToken,
|
||||
token: CancellationToken,
|
||||
): Promise<Location[]> {
|
||||
const fileLinks = this.shouldUseCache()
|
||||
? await this.cache.get(document.uri.toString())
|
||||
: await this.getReferences(document.uri.toString());
|
||||
? await this.cache.get(document.uri.toString(), token)
|
||||
: await this.getReferences(document.uri.toString(), token);
|
||||
|
||||
const locLinks: Location[] = [];
|
||||
for (const link of fileLinks) {
|
||||
@@ -148,9 +154,14 @@ export class TemplateQueryReferenceProvider implements ReferenceProvider {
|
||||
return !(isCanary() && NO_CACHE_CONTEXTUAL_QUERIES.getValue<boolean>());
|
||||
}
|
||||
|
||||
private async getReferences(uriString: string): Promise<FullLocationLink[]> {
|
||||
private async getReferences(
|
||||
uriString: string,
|
||||
token: CancellationToken,
|
||||
): Promise<FullLocationLink[]> {
|
||||
return withProgress(
|
||||
async (progress, token) => {
|
||||
async (progress, tokenInner) => {
|
||||
const multiToken = new MultiCancellationToken(token, tokenInner);
|
||||
|
||||
return getLocationsForUriString(
|
||||
this.cli,
|
||||
this.qs,
|
||||
@@ -159,7 +170,7 @@ export class TemplateQueryReferenceProvider implements ReferenceProvider {
|
||||
KeyType.DefinitionQuery,
|
||||
this.queryStorageDir,
|
||||
progress,
|
||||
token,
|
||||
multiToken,
|
||||
(src, _dest) => src === uriString,
|
||||
);
|
||||
},
|
||||
|
||||
@@ -11,7 +11,7 @@ import { getQlPackPath, QLPACK_FILENAMES } from "../common/ql";
|
||||
import { getErrorMessage } from "../common/helpers-pure";
|
||||
import { ExtensionPack } from "./shared/extension-pack";
|
||||
import { NotificationLogger, showAndLogErrorMessage } from "../common/logging";
|
||||
import { getExtensionsDirectory } from "../config";
|
||||
import { ModelConfig } from "../config";
|
||||
import {
|
||||
autoNameExtensionPack,
|
||||
ExtensionPackName,
|
||||
@@ -28,6 +28,7 @@ const extensionPackValidate = ajv.compile(extensionPackMetadataSchemaJson);
|
||||
export async function pickExtensionPack(
|
||||
cliServer: Pick<CodeQLCliServer, "resolveQlpacks">,
|
||||
databaseItem: Pick<DatabaseItem, "name" | "language">,
|
||||
modelConfig: ModelConfig,
|
||||
logger: NotificationLogger,
|
||||
progress: ProgressCallback,
|
||||
maxStep: number,
|
||||
@@ -56,7 +57,9 @@ export async function pickExtensionPack(
|
||||
});
|
||||
|
||||
// Get the `codeQL.model.extensionsDirectory` setting for the language
|
||||
const userExtensionsDirectory = getExtensionsDirectory(databaseItem.language);
|
||||
const userExtensionsDirectory = modelConfig.getExtensionsDirectory(
|
||||
databaseItem.language,
|
||||
);
|
||||
|
||||
// If the setting is not set, automatically pick a suitable directory
|
||||
const extensionsDirectory = userExtensionsDirectory
|
||||
|
||||
@@ -5,6 +5,7 @@ import { MethodModelingViewProvider } from "./method-modeling-view-provider";
|
||||
import { Method } from "../method";
|
||||
import { ModelingStore } from "../modeling-store";
|
||||
import { ModelEditorViewTracker } from "../model-editor-view-tracker";
|
||||
import { ModelConfigListener } from "../../config";
|
||||
|
||||
export class MethodModelingPanel extends DisposableObject {
|
||||
private readonly provider: MethodModelingViewProvider;
|
||||
@@ -16,10 +17,16 @@ export class MethodModelingPanel extends DisposableObject {
|
||||
) {
|
||||
super();
|
||||
|
||||
// This is here instead of in MethodModelingViewProvider because we need to
|
||||
// dispose this when the extension gets disposed, not when the webview gets
|
||||
// disposed.
|
||||
const modelConfig = this.push(new ModelConfigListener());
|
||||
|
||||
this.provider = new MethodModelingViewProvider(
|
||||
app,
|
||||
modelingStore,
|
||||
editorViewTracker,
|
||||
modelConfig,
|
||||
);
|
||||
this.push(
|
||||
window.registerWebviewViewProvider(
|
||||
|
||||
@@ -12,6 +12,7 @@ import { DbModelingState, ModelingStore } from "../modeling-store";
|
||||
import { AbstractWebviewViewProvider } from "../../common/vscode/abstract-webview-view-provider";
|
||||
import { assertNever } from "../../common/helpers-pure";
|
||||
import { ModelEditorViewTracker } from "../model-editor-view-tracker";
|
||||
import { ModelConfigListener } from "../../config";
|
||||
|
||||
export class MethodModelingViewProvider extends AbstractWebviewViewProvider<
|
||||
ToMethodModelingMessage,
|
||||
@@ -25,13 +26,24 @@ export class MethodModelingViewProvider extends AbstractWebviewViewProvider<
|
||||
app: App,
|
||||
private readonly modelingStore: ModelingStore,
|
||||
private readonly editorViewTracker: ModelEditorViewTracker,
|
||||
private readonly modelConfig: ModelConfigListener,
|
||||
) {
|
||||
super(app, "method-modeling");
|
||||
}
|
||||
|
||||
protected override onWebViewLoaded(): void {
|
||||
this.setInitialState();
|
||||
protected override async onWebViewLoaded(): Promise<void> {
|
||||
await Promise.all([this.setViewState(), this.setInitialState()]);
|
||||
this.registerToModelingStoreEvents();
|
||||
this.registerToModelConfigEvents();
|
||||
}
|
||||
|
||||
private async setViewState(): Promise<void> {
|
||||
await this.postMessage({
|
||||
t: "setMethodModelingPanelViewState",
|
||||
viewState: {
|
||||
showMultipleModels: this.modelConfig.showMultipleModels,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async setMethod(method: Method): Promise<void> {
|
||||
@@ -45,15 +57,17 @@ export class MethodModelingViewProvider extends AbstractWebviewViewProvider<
|
||||
}
|
||||
}
|
||||
|
||||
private setInitialState(): void {
|
||||
const selectedMethod = this.modelingStore.getSelectedMethodDetails();
|
||||
if (selectedMethod) {
|
||||
void this.postMessage({
|
||||
t: "setSelectedMethod",
|
||||
method: selectedMethod.method,
|
||||
modeledMethod: selectedMethod.modeledMethod,
|
||||
isModified: selectedMethod.isModified,
|
||||
});
|
||||
private async setInitialState(): Promise<void> {
|
||||
if (this.modelingStore.hasStateForActiveDb()) {
|
||||
const selectedMethod = this.modelingStore.getSelectedMethodDetails();
|
||||
if (selectedMethod) {
|
||||
await this.postMessage({
|
||||
t: "setSelectedMethod",
|
||||
method: selectedMethod.method,
|
||||
modeledMethod: selectedMethod.modeledMethod,
|
||||
isModified: selectedMethod.isModified,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +76,7 @@ export class MethodModelingViewProvider extends AbstractWebviewViewProvider<
|
||||
): Promise<void> {
|
||||
switch (msg.t) {
|
||||
case "viewLoaded":
|
||||
this.onWebViewLoaded();
|
||||
await this.onWebViewLoaded();
|
||||
break;
|
||||
|
||||
case "telemetry":
|
||||
@@ -92,6 +106,12 @@ export class MethodModelingViewProvider extends AbstractWebviewViewProvider<
|
||||
await this.revealInModelEditor(msg.method);
|
||||
|
||||
break;
|
||||
|
||||
case "startModeling":
|
||||
await this.app.commands.execute(
|
||||
"codeQL.openModelEditorFromModelingPanel",
|
||||
);
|
||||
break;
|
||||
default:
|
||||
assertNever(msg);
|
||||
}
|
||||
@@ -159,5 +179,33 @@ export class MethodModelingViewProvider extends AbstractWebviewViewProvider<
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
this.push(
|
||||
this.modelingStore.onDbOpened(async () => {
|
||||
await this.postMessage({
|
||||
t: "setInModelingMode",
|
||||
inModelingMode: true,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
this.push(
|
||||
this.modelingStore.onDbClosed(async () => {
|
||||
if (!this.modelingStore.anyDbsBeingModeled()) {
|
||||
await this.postMessage({
|
||||
t: "setInModelingMode",
|
||||
inModelingMode: false,
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private registerToModelConfigEvents(): void {
|
||||
this.push(
|
||||
this.modelConfig.onDidChangeConfiguration(() => {
|
||||
void this.setViewState();
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import { MethodModelingPanel } from "./method-modeling/method-modeling-panel";
|
||||
import { ModelingStore } from "./modeling-store";
|
||||
import { showResolvableLocation } from "../databases/local-databases/locations";
|
||||
import { ModelEditorViewTracker } from "./model-editor-view-tracker";
|
||||
import { ModelConfigListener } from "../config";
|
||||
|
||||
const SUPPORTED_LANGUAGES: string[] = ["java", "csharp"];
|
||||
|
||||
@@ -73,115 +74,9 @@ export class ModelEditorModule extends DisposableObject {
|
||||
|
||||
public getCommands(): ModelEditorCommands {
|
||||
return {
|
||||
"codeQL.openModelEditor": async () => {
|
||||
const db = this.databaseManager.currentDatabaseItem;
|
||||
if (!db) {
|
||||
void showAndLogErrorMessage(this.app.logger, "No database selected");
|
||||
return;
|
||||
}
|
||||
|
||||
const language = db.language;
|
||||
if (
|
||||
!SUPPORTED_LANGUAGES.includes(language) ||
|
||||
!isQueryLanguage(language)
|
||||
) {
|
||||
void showAndLogErrorMessage(
|
||||
this.app.logger,
|
||||
`The CodeQL Model Editor is not supported for ${language} databases.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
return withProgress(
|
||||
async (progress) => {
|
||||
const maxStep = 4;
|
||||
|
||||
if (!(await this.cliServer.cliConstraints.supportsQlpacksKind())) {
|
||||
void showAndLogErrorMessage(
|
||||
this.app.logger,
|
||||
`This feature requires CodeQL CLI version ${CliVersionConstraint.CLI_VERSION_WITH_QLPACKS_KIND.format()} or later.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!(await this.cliServer.cliConstraints.supportsResolveExtensions())
|
||||
) {
|
||||
void showAndLogErrorMessage(
|
||||
this.app.logger,
|
||||
`This feature requires CodeQL CLI version ${CliVersionConstraint.CLI_VERSION_WITH_RESOLVE_EXTENSIONS.format()} or later.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const modelFile = await pickExtensionPack(
|
||||
this.cliServer,
|
||||
db,
|
||||
this.app.logger,
|
||||
progress,
|
||||
maxStep,
|
||||
);
|
||||
if (!modelFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
progress({
|
||||
message: "Installing dependencies...",
|
||||
step: 3,
|
||||
maxStep,
|
||||
});
|
||||
|
||||
// Create new temporary directory for query files and pack dependencies
|
||||
const { path: queryDir, cleanup: cleanupQueryDir } = await dir({
|
||||
unsafeCleanup: true,
|
||||
});
|
||||
|
||||
const success = await setUpPack(this.cliServer, queryDir, language);
|
||||
if (!success) {
|
||||
await cleanupQueryDir();
|
||||
return;
|
||||
}
|
||||
|
||||
progress({
|
||||
message: "Opening editor...",
|
||||
step: 4,
|
||||
maxStep,
|
||||
});
|
||||
|
||||
const view = new ModelEditorView(
|
||||
this.app,
|
||||
this.modelingStore,
|
||||
this.editorViewTracker,
|
||||
this.databaseManager,
|
||||
this.cliServer,
|
||||
this.queryRunner,
|
||||
this.queryStorageDir,
|
||||
queryDir,
|
||||
db,
|
||||
modelFile,
|
||||
Mode.Application,
|
||||
);
|
||||
|
||||
this.modelingStore.onDbClosed(async (dbUri) => {
|
||||
if (dbUri === db.databaseUri.toString()) {
|
||||
await cleanupQueryDir();
|
||||
}
|
||||
});
|
||||
|
||||
this.push(view);
|
||||
this.push({
|
||||
dispose(): void {
|
||||
void cleanupQueryDir();
|
||||
},
|
||||
});
|
||||
|
||||
await view.openView();
|
||||
},
|
||||
{
|
||||
title: "Opening CodeQL Model Editor",
|
||||
},
|
||||
);
|
||||
},
|
||||
"codeQL.openModelEditor": this.openModelEditor.bind(this),
|
||||
"codeQL.openModelEditorFromModelingPanel":
|
||||
this.openModelEditor.bind(this),
|
||||
"codeQLModelEditor.jumpToUsageLocation": async (
|
||||
method: Method,
|
||||
usage: Usage,
|
||||
@@ -213,4 +108,125 @@ export class ModelEditorModule extends DisposableObject {
|
||||
await this.methodModelingPanel.setMethod(method);
|
||||
await showResolvableLocation(usage.url, databaseItem, this.app.logger);
|
||||
}
|
||||
|
||||
private async openModelEditor(): Promise<void> {
|
||||
{
|
||||
const db = this.databaseManager.currentDatabaseItem;
|
||||
if (!db) {
|
||||
void showAndLogErrorMessage(this.app.logger, "No database selected");
|
||||
return;
|
||||
}
|
||||
|
||||
const language = db.language;
|
||||
if (
|
||||
!SUPPORTED_LANGUAGES.includes(language) ||
|
||||
!isQueryLanguage(language)
|
||||
) {
|
||||
void showAndLogErrorMessage(
|
||||
this.app.logger,
|
||||
`The CodeQL Model Editor is not supported for ${language} databases.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
return withProgress(
|
||||
async (progress) => {
|
||||
const maxStep = 4;
|
||||
|
||||
if (!(await this.cliServer.cliConstraints.supportsQlpacksKind())) {
|
||||
void showAndLogErrorMessage(
|
||||
this.app.logger,
|
||||
`This feature requires CodeQL CLI version ${CliVersionConstraint.CLI_VERSION_WITH_QLPACKS_KIND.format()} or later.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!(await this.cliServer.cliConstraints.supportsResolveExtensions())
|
||||
) {
|
||||
void showAndLogErrorMessage(
|
||||
this.app.logger,
|
||||
`This feature requires CodeQL CLI version ${CliVersionConstraint.CLI_VERSION_WITH_RESOLVE_EXTENSIONS.format()} or later.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const modelConfig = this.push(new ModelConfigListener());
|
||||
|
||||
const modelFile = await pickExtensionPack(
|
||||
this.cliServer,
|
||||
db,
|
||||
modelConfig,
|
||||
this.app.logger,
|
||||
progress,
|
||||
maxStep,
|
||||
);
|
||||
if (!modelFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
progress({
|
||||
message: "Installing dependencies...",
|
||||
step: 3,
|
||||
maxStep,
|
||||
});
|
||||
|
||||
// Create new temporary directory for query files and pack dependencies
|
||||
const { path: queryDir, cleanup: cleanupQueryDir } = await dir({
|
||||
unsafeCleanup: true,
|
||||
});
|
||||
|
||||
const success = await setUpPack(
|
||||
this.cliServer,
|
||||
queryDir,
|
||||
language,
|
||||
modelConfig,
|
||||
);
|
||||
if (!success) {
|
||||
await cleanupQueryDir();
|
||||
return;
|
||||
}
|
||||
|
||||
progress({
|
||||
message: "Opening editor...",
|
||||
step: 4,
|
||||
maxStep,
|
||||
});
|
||||
|
||||
const view = new ModelEditorView(
|
||||
this.app,
|
||||
this.modelingStore,
|
||||
this.editorViewTracker,
|
||||
modelConfig,
|
||||
this.databaseManager,
|
||||
this.cliServer,
|
||||
this.queryRunner,
|
||||
this.queryStorageDir,
|
||||
queryDir,
|
||||
db,
|
||||
modelFile,
|
||||
Mode.Application,
|
||||
);
|
||||
|
||||
this.modelingStore.onDbClosed(async (dbUri) => {
|
||||
if (dbUri === db.databaseUri.toString()) {
|
||||
await cleanupQueryDir();
|
||||
}
|
||||
});
|
||||
|
||||
this.push(view);
|
||||
this.push({
|
||||
dispose(): void {
|
||||
void cleanupQueryDir();
|
||||
},
|
||||
});
|
||||
|
||||
await view.openView();
|
||||
},
|
||||
{
|
||||
title: "Opening CodeQL Model Editor",
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { writeFile } from "fs-extra";
|
||||
import { dump } from "js-yaml";
|
||||
import { prepareExternalApiQuery } from "./external-api-usage-queries";
|
||||
import { CodeQLCliServer } from "../codeql-cli/cli";
|
||||
import { showLlmGeneration } from "../config";
|
||||
import { ModelConfig } from "../config";
|
||||
import { Mode } from "./shared/mode";
|
||||
import { resolveQueriesFromPacks } from "../local-queries";
|
||||
import { modeTag } from "./mode-tag";
|
||||
@@ -28,12 +28,14 @@ export const syntheticQueryPackName = "codeql/external-api-usage";
|
||||
* @param cliServer The CodeQL CLI server to use.
|
||||
* @param queryDir The directory to set up.
|
||||
* @param language The language to use for the queries.
|
||||
* @param modelConfig The model config to use.
|
||||
* @returns true if the setup was successful, false otherwise.
|
||||
*/
|
||||
export async function setUpPack(
|
||||
cliServer: CodeQLCliServer,
|
||||
queryDir: string,
|
||||
language: QueryLanguage,
|
||||
modelConfig: ModelConfig,
|
||||
): Promise<boolean> {
|
||||
// Download the required query packs
|
||||
await cliServer.packDownload([`codeql/${language}-queries`]);
|
||||
@@ -84,7 +86,7 @@ export async function setUpPack(
|
||||
}
|
||||
|
||||
// Download any other required packs
|
||||
if (language === "java" && showLlmGeneration()) {
|
||||
if (language === "java" && modelConfig.llmGeneration) {
|
||||
await cliServer.packDownload([`codeql/${language}-automodel-queries`]);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,8 +17,8 @@ import {
|
||||
import { ProgressCallback, withProgress } from "../common/vscode/progress";
|
||||
import { QueryRunner } from "../query-server";
|
||||
import {
|
||||
showAndLogExceptionWithTelemetry,
|
||||
showAndLogErrorMessage,
|
||||
showAndLogExceptionWithTelemetry,
|
||||
} from "../common/logging";
|
||||
import { DatabaseItem, DatabaseManager } from "../databases/local-databases";
|
||||
import { CodeQLCliServer } from "../codeql-cli/cli";
|
||||
@@ -34,11 +34,7 @@ import {
|
||||
import { Method, Usage } from "./method";
|
||||
import { ModeledMethod } from "./modeled-method";
|
||||
import { ExtensionPack } from "./shared/extension-pack";
|
||||
import {
|
||||
showFlowGeneration,
|
||||
showLlmGeneration,
|
||||
showMultipleModels,
|
||||
} from "../config";
|
||||
import { ModelConfigListener } from "../config";
|
||||
import { Mode } from "./shared/mode";
|
||||
import { loadModeledMethods, saveModeledMethods } from "./modeled-method-fs";
|
||||
import { pickExtensionPack } from "./extension-pack-picker";
|
||||
@@ -47,6 +43,10 @@ import { AutoModeler } from "./auto-modeler";
|
||||
import { telemetryListener } from "../common/vscode/telemetry";
|
||||
import { ModelingStore } from "./modeling-store";
|
||||
import { ModelEditorViewTracker } from "./model-editor-view-tracker";
|
||||
import {
|
||||
convertFromLegacyModeledMethods,
|
||||
convertToLegacyModeledMethods,
|
||||
} from "./modeled-methods-legacy";
|
||||
|
||||
export class ModelEditorView extends AbstractWebview<
|
||||
ToModelEditorMessage,
|
||||
@@ -58,6 +58,7 @@ export class ModelEditorView extends AbstractWebview<
|
||||
protected readonly app: App,
|
||||
private readonly modelingStore: ModelingStore,
|
||||
private readonly viewTracker: ModelEditorViewTracker<ModelEditorView>,
|
||||
private readonly modelConfig: ModelConfigListener,
|
||||
private readonly databaseManager: DatabaseManager,
|
||||
private readonly cliServer: CodeQLCliServer,
|
||||
private readonly queryRunner: QueryRunner,
|
||||
@@ -71,6 +72,7 @@ export class ModelEditorView extends AbstractWebview<
|
||||
|
||||
this.modelingStore.initializeStateForDb(databaseItem);
|
||||
this.registerToModelingStoreEvents();
|
||||
this.registerToModelConfigEvents();
|
||||
|
||||
this.viewTracker.registerView(this);
|
||||
|
||||
@@ -100,9 +102,6 @@ export class ModelEditorView extends AbstractWebview<
|
||||
panel.onDidChangeViewState(async () => {
|
||||
if (panel.active) {
|
||||
this.modelingStore.setActiveDb(this.databaseItem);
|
||||
await this.markModelEditorAsActive();
|
||||
} else {
|
||||
await this.updateModelEditorActiveContext();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -126,36 +125,12 @@ export class ModelEditorView extends AbstractWebview<
|
||||
);
|
||||
}
|
||||
|
||||
private async markModelEditorAsActive(): Promise<void> {
|
||||
void this.app.commands.execute(
|
||||
"setContext",
|
||||
"codeql.modelEditorActive",
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
private async updateModelEditorActiveContext(): Promise<void> {
|
||||
await this.app.commands.execute(
|
||||
"setContext",
|
||||
"codeql.modelEditorActive",
|
||||
this.isAModelEditorActive(),
|
||||
);
|
||||
}
|
||||
|
||||
private isAModelEditorOpen(): boolean {
|
||||
return window.tabGroups.all.some((tabGroup) =>
|
||||
tabGroup.tabs.some((tab) => this.isTabModelEditorView(tab)),
|
||||
);
|
||||
}
|
||||
|
||||
private isAModelEditorActive(): boolean {
|
||||
return window.tabGroups.all.some((tabGroup) =>
|
||||
tabGroup.tabs.some(
|
||||
(tab) => this.isTabModelEditorView(tab) && tab.isActive,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private isTabModelEditorView(tab: Tab): boolean {
|
||||
if (!(tab.input instanceof TabInputWebview)) {
|
||||
return false;
|
||||
@@ -228,47 +203,58 @@ export class ModelEditorView extends AbstractWebview<
|
||||
|
||||
break;
|
||||
case "saveModeledMethods":
|
||||
await withProgress(
|
||||
async (progress) => {
|
||||
progress({
|
||||
step: 1,
|
||||
maxStep: 500 + externalApiQueriesProgressMaxStep,
|
||||
message: "Writing model files",
|
||||
});
|
||||
await saveModeledMethods(
|
||||
this.extensionPack,
|
||||
this.databaseItem.language,
|
||||
msg.methods,
|
||||
msg.modeledMethods,
|
||||
this.mode,
|
||||
this.cliServer,
|
||||
this.app.logger,
|
||||
);
|
||||
{
|
||||
const methods = this.modelingStore.getMethods(
|
||||
this.databaseItem,
|
||||
msg.methodSignatures,
|
||||
);
|
||||
const modeledMethods = this.modelingStore.getModeledMethods(
|
||||
this.databaseItem,
|
||||
msg.methodSignatures,
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
this.setViewState(),
|
||||
this.loadMethods((update) =>
|
||||
progress({
|
||||
...update,
|
||||
step: update.step + 500,
|
||||
maxStep: 500 + externalApiQueriesProgressMaxStep,
|
||||
}),
|
||||
),
|
||||
]);
|
||||
},
|
||||
{
|
||||
cancellable: false,
|
||||
},
|
||||
);
|
||||
await withProgress(
|
||||
async (progress) => {
|
||||
progress({
|
||||
step: 1,
|
||||
maxStep: 500 + externalApiQueriesProgressMaxStep,
|
||||
message: "Writing model files",
|
||||
});
|
||||
await saveModeledMethods(
|
||||
this.extensionPack,
|
||||
this.databaseItem.language,
|
||||
methods,
|
||||
convertFromLegacyModeledMethods(modeledMethods),
|
||||
this.mode,
|
||||
this.cliServer,
|
||||
this.app.logger,
|
||||
);
|
||||
|
||||
this.modelingStore.removeModifiedMethods(
|
||||
this.databaseItem,
|
||||
Object.keys(msg.modeledMethods),
|
||||
);
|
||||
await Promise.all([
|
||||
this.setViewState(),
|
||||
this.loadMethods((update) =>
|
||||
progress({
|
||||
...update,
|
||||
step: update.step + 500,
|
||||
maxStep: 500 + externalApiQueriesProgressMaxStep,
|
||||
}),
|
||||
),
|
||||
]);
|
||||
},
|
||||
{
|
||||
cancellable: false,
|
||||
},
|
||||
);
|
||||
|
||||
void telemetryListener?.sendUIInteraction(
|
||||
"model-editor-save-modeled-methods",
|
||||
);
|
||||
this.modelingStore.removeModifiedMethods(
|
||||
this.databaseItem,
|
||||
Object.keys(modeledMethods),
|
||||
);
|
||||
|
||||
void telemetryListener?.sendUIInteraction(
|
||||
"model-editor-save-modeled-methods",
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
case "generateMethod":
|
||||
@@ -355,21 +341,21 @@ export class ModelEditorView extends AbstractWebview<
|
||||
|
||||
await this.postMessage({
|
||||
t: "revealMethod",
|
||||
method,
|
||||
methodSignature: method.signature,
|
||||
});
|
||||
}
|
||||
|
||||
private async setViewState(): Promise<void> {
|
||||
const showLlmButton =
|
||||
this.databaseItem.language === "java" && showLlmGeneration();
|
||||
this.databaseItem.language === "java" && this.modelConfig.llmGeneration;
|
||||
|
||||
await this.postMessage({
|
||||
t: "setModelEditorViewState",
|
||||
viewState: {
|
||||
extensionPack: this.extensionPack,
|
||||
showFlowGeneration: showFlowGeneration(),
|
||||
showFlowGeneration: this.modelConfig.flowGeneration,
|
||||
showLlmButton,
|
||||
showMultipleModels: showMultipleModels(),
|
||||
showMultipleModels: this.modelConfig.showMultipleModels,
|
||||
mode: this.mode,
|
||||
},
|
||||
});
|
||||
@@ -386,7 +372,10 @@ export class ModelEditorView extends AbstractWebview<
|
||||
this.cliServer,
|
||||
this.app.logger,
|
||||
);
|
||||
this.modelingStore.setModeledMethods(this.databaseItem, modeledMethods);
|
||||
this.modelingStore.setModeledMethods(
|
||||
this.databaseItem,
|
||||
convertToLegacyModeledMethods(modeledMethods),
|
||||
);
|
||||
} catch (e: unknown) {
|
||||
void showAndLogErrorMessage(
|
||||
this.app.logger,
|
||||
@@ -508,6 +497,7 @@ export class ModelEditorView extends AbstractWebview<
|
||||
const modelFile = await pickExtensionPack(
|
||||
this.cliServer,
|
||||
addedDatabase,
|
||||
this.modelConfig,
|
||||
this.app.logger,
|
||||
progress,
|
||||
3,
|
||||
@@ -520,6 +510,7 @@ export class ModelEditorView extends AbstractWebview<
|
||||
this.app,
|
||||
this.modelingStore,
|
||||
this.viewTracker,
|
||||
this.modelConfig,
|
||||
this.databaseManager,
|
||||
this.cliServer,
|
||||
this.queryRunner,
|
||||
@@ -641,6 +632,14 @@ export class ModelEditorView extends AbstractWebview<
|
||||
);
|
||||
}
|
||||
|
||||
private registerToModelConfigEvents() {
|
||||
this.push(
|
||||
this.modelConfig.onDidChangeConfiguration(() => {
|
||||
void this.setViewState();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private addModeledMethods(modeledMethods: Record<string, ModeledMethod>) {
|
||||
this.modelingStore.addModeledMethods(this.databaseItem, modeledMethods);
|
||||
|
||||
|
||||
@@ -10,17 +10,12 @@ import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
|
||||
import { load as loadYaml } from "js-yaml";
|
||||
import { CodeQLCliServer } from "../codeql-cli/cli";
|
||||
import { pathsEqual } from "../common/files";
|
||||
import {
|
||||
convertFromLegacyModeledMethods,
|
||||
convertFromLegacyModeledMethodsFiles,
|
||||
convertToLegacyModeledMethods,
|
||||
} from "./modeled-methods-legacy";
|
||||
|
||||
export async function saveModeledMethods(
|
||||
extensionPack: ExtensionPack,
|
||||
language: string,
|
||||
methods: Method[],
|
||||
modeledMethods: Record<string, ModeledMethod>,
|
||||
modeledMethods: Record<string, ModeledMethod[]>,
|
||||
mode: Mode,
|
||||
cliServer: CodeQLCliServer,
|
||||
logger: NotificationLogger,
|
||||
@@ -34,8 +29,8 @@ export async function saveModeledMethods(
|
||||
const yamls = createDataExtensionYamls(
|
||||
language,
|
||||
methods,
|
||||
convertFromLegacyModeledMethods(modeledMethods),
|
||||
convertFromLegacyModeledMethodsFiles(existingModeledMethods),
|
||||
modeledMethods,
|
||||
existingModeledMethods,
|
||||
mode,
|
||||
);
|
||||
|
||||
@@ -50,12 +45,12 @@ async function loadModeledMethodFiles(
|
||||
extensionPack: ExtensionPack,
|
||||
cliServer: CodeQLCliServer,
|
||||
logger: NotificationLogger,
|
||||
): Promise<Record<string, Record<string, ModeledMethod>>> {
|
||||
): Promise<Record<string, Record<string, ModeledMethod[]>>> {
|
||||
const modelFiles = await listModelFiles(extensionPack.path, cliServer);
|
||||
|
||||
const modeledMethodsByFile: Record<
|
||||
string,
|
||||
Record<string, ModeledMethod>
|
||||
Record<string, ModeledMethod[]>
|
||||
> = {};
|
||||
|
||||
for (const modelFile of modelFiles) {
|
||||
@@ -73,8 +68,7 @@ async function loadModeledMethodFiles(
|
||||
);
|
||||
continue;
|
||||
}
|
||||
modeledMethodsByFile[modelFile] =
|
||||
convertToLegacyModeledMethods(modeledMethods);
|
||||
modeledMethodsByFile[modelFile] = modeledMethods;
|
||||
}
|
||||
|
||||
return modeledMethodsByFile;
|
||||
@@ -84,8 +78,8 @@ export async function loadModeledMethods(
|
||||
extensionPack: ExtensionPack,
|
||||
cliServer: CodeQLCliServer,
|
||||
logger: NotificationLogger,
|
||||
): Promise<Record<string, ModeledMethod>> {
|
||||
const existingModeledMethods: Record<string, ModeledMethod> = {};
|
||||
): Promise<Record<string, ModeledMethod[]>> {
|
||||
const existingModeledMethods: Record<string, ModeledMethod[]> = {};
|
||||
|
||||
const modeledMethodsByFile = await loadModeledMethodFiles(
|
||||
extensionPack,
|
||||
@@ -94,7 +88,11 @@ export async function loadModeledMethods(
|
||||
);
|
||||
for (const modeledMethods of Object.values(modeledMethodsByFile)) {
|
||||
for (const [key, value] of Object.entries(modeledMethods)) {
|
||||
existingModeledMethods[key] = value;
|
||||
if (!(key in existingModeledMethods)) {
|
||||
existingModeledMethods[key] = [];
|
||||
}
|
||||
|
||||
existingModeledMethods[key].push(...value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,13 +21,3 @@ export function convertToLegacyModeledMethods(
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function convertFromLegacyModeledMethodsFiles(
|
||||
modeledMethods: Record<string, Record<string, ModeledMethod>>,
|
||||
): Record<string, Record<string, ModeledMethod[]>> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(modeledMethods).map(([filename, modeledMethods]) => {
|
||||
return [filename, convertFromLegacyModeledMethods(modeledMethods)];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ interface SelectedMethodChangedEvent {
|
||||
|
||||
export class ModelingStore extends DisposableObject {
|
||||
public readonly onActiveDbChanged: AppEvent<void>;
|
||||
public readonly onDbOpened: AppEvent<string>;
|
||||
public readonly onDbClosed: AppEvent<string>;
|
||||
public readonly onMethodsChanged: AppEvent<MethodsChangedEvent>;
|
||||
public readonly onHideModeledMethodsChanged: AppEvent<HideModeledMethodsChangedEvent>;
|
||||
@@ -60,6 +61,7 @@ export class ModelingStore extends DisposableObject {
|
||||
private activeDb: string | undefined;
|
||||
|
||||
private readonly onActiveDbChangedEventEmitter: AppEventEmitter<void>;
|
||||
private readonly onDbOpenedEventEmitter: AppEventEmitter<string>;
|
||||
private readonly onDbClosedEventEmitter: AppEventEmitter<string>;
|
||||
private readonly onMethodsChangedEventEmitter: AppEventEmitter<MethodsChangedEvent>;
|
||||
private readonly onHideModeledMethodsChangedEventEmitter: AppEventEmitter<HideModeledMethodsChangedEvent>;
|
||||
@@ -79,6 +81,9 @@ export class ModelingStore extends DisposableObject {
|
||||
);
|
||||
this.onActiveDbChanged = this.onActiveDbChangedEventEmitter.event;
|
||||
|
||||
this.onDbOpenedEventEmitter = this.push(app.createEventEmitter<string>());
|
||||
this.onDbOpened = this.onDbOpenedEventEmitter.event;
|
||||
|
||||
this.onDbClosedEventEmitter = this.push(app.createEventEmitter<string>());
|
||||
this.onDbClosed = this.onDbClosedEventEmitter.event;
|
||||
|
||||
@@ -123,6 +128,8 @@ export class ModelingStore extends DisposableObject {
|
||||
selectedMethod: undefined,
|
||||
selectedUsage: undefined,
|
||||
});
|
||||
|
||||
this.onDbOpenedEventEmitter.fire(dbUri);
|
||||
}
|
||||
|
||||
public setActiveDb(databaseItem: DatabaseItem) {
|
||||
@@ -154,6 +161,31 @@ export class ModelingStore extends DisposableObject {
|
||||
return this.state.get(this.activeDb);
|
||||
}
|
||||
|
||||
public hasStateForActiveDb(): boolean {
|
||||
return !!this.getStateForActiveDb();
|
||||
}
|
||||
|
||||
public anyDbsBeingModeled(): boolean {
|
||||
return this.state.size > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the methods for the given database item and method signatures.
|
||||
* If the `methodSignatures` argument is not provided or is undefined, returns all methods.
|
||||
*/
|
||||
public getMethods(
|
||||
dbItem: DatabaseItem,
|
||||
methodSignatures?: string[],
|
||||
): Method[] {
|
||||
const methods = this.getState(dbItem).methods;
|
||||
if (!methodSignatures) {
|
||||
return methods;
|
||||
}
|
||||
return methods.filter((method) =>
|
||||
methodSignatures.includes(method.signature),
|
||||
);
|
||||
}
|
||||
|
||||
public setMethods(dbItem: DatabaseItem, methods: Method[]) {
|
||||
const dbState = this.getState(dbItem);
|
||||
const dbUri = dbItem.databaseUri.toString();
|
||||
@@ -182,6 +214,25 @@ export class ModelingStore extends DisposableObject {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the modeled methods for the given database item and method signatures.
|
||||
* If the `methodSignatures` argument is not provided or is undefined, returns all modeled methods.
|
||||
*/
|
||||
public getModeledMethods(
|
||||
dbItem: DatabaseItem,
|
||||
methodSignatures?: string[],
|
||||
): Record<string, ModeledMethod> {
|
||||
const modeledMethods = this.getState(dbItem).modeledMethods;
|
||||
if (!methodSignatures) {
|
||||
return modeledMethods;
|
||||
}
|
||||
return Object.fromEntries(
|
||||
Object.entries(modeledMethods).filter(([key]) =>
|
||||
methodSignatures.includes(key),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public addModeledMethods(
|
||||
dbItem: DatabaseItem,
|
||||
methods: Record<string, ModeledMethod>,
|
||||
|
||||
@@ -8,3 +8,7 @@ export interface ModelEditorViewState {
|
||||
showMultipleModels: boolean;
|
||||
mode: Mode;
|
||||
}
|
||||
|
||||
export interface MethodModelingPanelViewState {
|
||||
showMultipleModels: boolean;
|
||||
}
|
||||
|
||||
@@ -23,8 +23,6 @@ export class CommandManager<
|
||||
CommandName extends keyof Commands & string = keyof Commands & string,
|
||||
> implements Disposable
|
||||
{
|
||||
// TODO: should this be a map?
|
||||
// TODO: handle multiple command names
|
||||
private commands: Disposable[] = [];
|
||||
|
||||
constructor(
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { Meta, StoryFn } from "@storybook/react";
|
||||
|
||||
import { ResponsiveContainer as ResponsiveContainerComponent } from "../../view/common/ResponsiveContainer";
|
||||
|
||||
export default {
|
||||
title: "Responsive Container",
|
||||
component: ResponsiveContainerComponent,
|
||||
} as Meta<typeof ResponsiveContainerComponent>;
|
||||
|
||||
const Template: StoryFn<typeof ResponsiveContainerComponent> = (args) => (
|
||||
<ResponsiveContainerComponent>
|
||||
<span>Hello</span>
|
||||
</ResponsiveContainerComponent>
|
||||
);
|
||||
|
||||
export const ResponsiveContainer = Template.bind({});
|
||||
@@ -4,6 +4,7 @@ import { Meta, StoryFn } from "@storybook/react";
|
||||
|
||||
import { MethodModeling as MethodModelingComponent } from "../../view/method-modeling/MethodModeling";
|
||||
import { createMethod } from "../../../test/factories/model-editor/method-factories";
|
||||
import { createModeledMethod } from "../../../test/factories/model-editor/modeled-method-factories";
|
||||
export default {
|
||||
title: "Method Modeling/Method Modeling",
|
||||
component: MethodModelingComponent,
|
||||
@@ -18,18 +19,53 @@ const method = createMethod();
|
||||
export const MethodUnmodeled = Template.bind({});
|
||||
MethodUnmodeled.args = {
|
||||
method,
|
||||
modeledMethods: [],
|
||||
modelingStatus: "unmodeled",
|
||||
};
|
||||
|
||||
export const MethodModeled = Template.bind({});
|
||||
MethodModeled.args = {
|
||||
method,
|
||||
|
||||
modeledMethods: [],
|
||||
modelingStatus: "unsaved",
|
||||
};
|
||||
|
||||
export const MethodSaved = Template.bind({});
|
||||
MethodSaved.args = {
|
||||
method,
|
||||
modeledMethods: [],
|
||||
modelingStatus: "saved",
|
||||
};
|
||||
|
||||
export const MultipleModelingsUnmodeled = Template.bind({});
|
||||
MultipleModelingsUnmodeled.args = {
|
||||
method,
|
||||
modeledMethods: [],
|
||||
showMultipleModels: true,
|
||||
modelingStatus: "saved",
|
||||
};
|
||||
|
||||
export const MultipleModelingsModeledSingle = Template.bind({});
|
||||
MultipleModelingsModeledSingle.args = {
|
||||
method,
|
||||
modeledMethods: [createModeledMethod(method)],
|
||||
showMultipleModels: true,
|
||||
modelingStatus: "saved",
|
||||
};
|
||||
|
||||
export const MultipleModelingsModeledMultiple = Template.bind({});
|
||||
MultipleModelingsModeledMultiple.args = {
|
||||
method,
|
||||
modeledMethods: [
|
||||
createModeledMethod(method),
|
||||
createModeledMethod({
|
||||
...method,
|
||||
type: "source",
|
||||
input: "",
|
||||
output: "ReturnValue",
|
||||
kind: "remote",
|
||||
}),
|
||||
],
|
||||
showMultipleModels: true,
|
||||
modelingStatus: "saved",
|
||||
};
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { Meta, StoryFn } from "@storybook/react";
|
||||
|
||||
import { NoMethodSelected as NoMethodSelectedComponent } from "../../view/method-modeling/NoMethodSelected";
|
||||
|
||||
export default {
|
||||
title: "Method Modeling/No Method Selected",
|
||||
component: NoMethodSelectedComponent,
|
||||
} as Meta<typeof NoMethodSelectedComponent>;
|
||||
|
||||
const Template: StoryFn<typeof NoMethodSelectedComponent> = () => (
|
||||
<NoMethodSelectedComponent />
|
||||
);
|
||||
|
||||
export const NoMethodSelected = Template.bind({});
|
||||
@@ -0,0 +1,16 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { Meta, StoryFn } from "@storybook/react";
|
||||
|
||||
import { NotInModelingMode as NotInModelingModeComponent } from "../../view/method-modeling/NotInModelingMode";
|
||||
|
||||
export default {
|
||||
title: "Method Modeling/Not In Modeling Mode",
|
||||
component: NotInModelingModeComponent,
|
||||
} as Meta<typeof NotInModelingModeComponent>;
|
||||
|
||||
const Template: StoryFn<typeof NotInModelingModeComponent> = () => (
|
||||
<NotInModelingModeComponent />
|
||||
);
|
||||
|
||||
export const NotInModelingMode = Template.bind({});
|
||||
15
extensions/ql-vscode/src/view/common/ResponsiveContainer.tsx
Normal file
15
extensions/ql-vscode/src/view/common/ResponsiveContainer.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { styled } from "styled-components";
|
||||
|
||||
export const ResponsiveContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
height: 100vh;
|
||||
|
||||
@media (min-height: 300px) {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
`;
|
||||
@@ -4,7 +4,7 @@ import classNames from "classnames";
|
||||
|
||||
type Props = {
|
||||
name: string;
|
||||
label: string;
|
||||
label?: string;
|
||||
className?: string;
|
||||
slot?: string;
|
||||
};
|
||||
|
||||
@@ -5,22 +5,23 @@ import { ModelingStatusIndicator } from "../model-editor/ModelingStatusIndicator
|
||||
import { Method } from "../../model-editor/method";
|
||||
import { MethodName } from "../model-editor/MethodName";
|
||||
import { ModeledMethod } from "../../model-editor/modeled-method";
|
||||
import { MethodModelingInputs } from "./MethodModelingInputs";
|
||||
import { VSCodeTag } from "@vscode/webview-ui-toolkit/react";
|
||||
import { ReviewInEditorButton } from "./ReviewInEditorButton";
|
||||
import { ModeledMethodsPanel } from "./ModeledMethodsPanel";
|
||||
|
||||
const Container = styled.div`
|
||||
padding: 0.3rem;
|
||||
padding-top: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const Title = styled.div`
|
||||
padding-bottom: 0.3rem;
|
||||
font-size: 0.7rem;
|
||||
padding-bottom: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
text-transform: uppercase;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const DependencyContainer = styled.div`
|
||||
@@ -34,19 +35,32 @@ const DependencyContainer = styled.div`
|
||||
padding: 0.5rem;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
margin-bottom: 0.8rem;
|
||||
`;
|
||||
|
||||
const StyledVSCodeTag = styled(VSCodeTag)<{ visible: boolean }>`
|
||||
visibility: ${(props) => (props.visible ? "visible" : "hidden")};
|
||||
`;
|
||||
|
||||
const UnsavedTag = ({ modelingStatus }: { modelingStatus: ModelingStatus }) => (
|
||||
<StyledVSCodeTag visible={modelingStatus === "unsaved"}>
|
||||
Unsaved
|
||||
</StyledVSCodeTag>
|
||||
);
|
||||
|
||||
export type MethodModelingProps = {
|
||||
modelingStatus: ModelingStatus;
|
||||
method: Method;
|
||||
modeledMethod: ModeledMethod | undefined;
|
||||
modeledMethods: ModeledMethod[];
|
||||
showMultipleModels?: boolean;
|
||||
onChange: (modeledMethod: ModeledMethod) => void;
|
||||
};
|
||||
|
||||
export const MethodModeling = ({
|
||||
modelingStatus,
|
||||
modeledMethod,
|
||||
modeledMethods,
|
||||
method,
|
||||
showMultipleModels = false,
|
||||
onChange,
|
||||
}: MethodModelingProps): JSX.Element => {
|
||||
return (
|
||||
@@ -54,15 +68,16 @@ export const MethodModeling = ({
|
||||
<Title>
|
||||
{method.packageName}
|
||||
{method.libraryVersion && <>@{method.libraryVersion}</>}
|
||||
{modelingStatus === "unsaved" ? <VSCodeTag>Unsaved</VSCodeTag> : null}
|
||||
<UnsavedTag modelingStatus={modelingStatus} />
|
||||
</Title>
|
||||
<DependencyContainer>
|
||||
<ModelingStatusIndicator status={modelingStatus} />
|
||||
<MethodName {...method} />
|
||||
</DependencyContainer>
|
||||
<MethodModelingInputs
|
||||
<ModeledMethodsPanel
|
||||
method={method}
|
||||
modeledMethod={modeledMethod}
|
||||
modeledMethods={modeledMethods}
|
||||
showMultipleModels={showMultipleModels}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<ReviewInEditorButton method={method} />
|
||||
|
||||
@@ -11,11 +11,14 @@ const Container = styled.div`
|
||||
padding-top: 0.5rem;
|
||||
`;
|
||||
|
||||
const Input = styled.label``;
|
||||
const Input = styled.label`
|
||||
display: block;
|
||||
padding-bottom: 0.3rem;
|
||||
`;
|
||||
|
||||
const Name = styled.span`
|
||||
display: block;
|
||||
padding-bottom: 0.3rem;
|
||||
padding-bottom: 0.5rem;
|
||||
`;
|
||||
|
||||
export type MethodModelingInputsProps = {
|
||||
|
||||
@@ -7,8 +7,20 @@ import { ToMethodModelingMessage } from "../../common/interface-types";
|
||||
import { assertNever } from "../../common/helpers-pure";
|
||||
import { ModeledMethod } from "../../model-editor/modeled-method";
|
||||
import { vscode } from "../vscode-api";
|
||||
import { NotInModelingMode } from "./NotInModelingMode";
|
||||
import { NoMethodSelected } from "./NoMethodSelected";
|
||||
import { MethodModelingPanelViewState } from "../../model-editor/shared/view-state";
|
||||
|
||||
type Props = {
|
||||
initialViewState?: MethodModelingPanelViewState;
|
||||
};
|
||||
|
||||
export function MethodModelingView({ initialViewState }: Props): JSX.Element {
|
||||
const [viewState, setViewState] = useState<
|
||||
MethodModelingPanelViewState | undefined
|
||||
>(initialViewState);
|
||||
const [inModelingMode, setInModelingMode] = useState<boolean>(false);
|
||||
|
||||
export function MethodModelingView(): JSX.Element {
|
||||
const [method, setMethod] = useState<Method | undefined>(undefined);
|
||||
|
||||
const [modeledMethod, setModeledMethod] = React.useState<
|
||||
@@ -28,6 +40,12 @@ export function MethodModelingView(): JSX.Element {
|
||||
if (evt.origin === window.origin) {
|
||||
const msg: ToMethodModelingMessage = evt.data;
|
||||
switch (msg.t) {
|
||||
case "setMethodModelingPanelViewState":
|
||||
setViewState(msg.viewState);
|
||||
break;
|
||||
case "setInModelingMode":
|
||||
setInModelingMode(msg.inModelingMode);
|
||||
break;
|
||||
case "setMethod":
|
||||
setMethod(msg.method);
|
||||
break;
|
||||
@@ -58,8 +76,12 @@ export function MethodModelingView(): JSX.Element {
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!inModelingMode) {
|
||||
return <NotInModelingMode />;
|
||||
}
|
||||
|
||||
if (!method) {
|
||||
return <>Select method to model</>;
|
||||
return <NoMethodSelected />;
|
||||
}
|
||||
|
||||
const onChange = (modeledMethod: ModeledMethod) => {
|
||||
@@ -73,7 +95,8 @@ export function MethodModelingView(): JSX.Element {
|
||||
<MethodModeling
|
||||
modelingStatus={modelingStatus}
|
||||
method={method}
|
||||
modeledMethod={modeledMethod}
|
||||
modeledMethods={modeledMethod ? [modeledMethod] : []}
|
||||
showMultipleModels={viewState?.showMultipleModels}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import * as React from "react";
|
||||
import { ModeledMethod } from "../../model-editor/modeled-method";
|
||||
import { MethodModelingInputs } from "./MethodModelingInputs";
|
||||
import { Method } from "../../model-editor/method";
|
||||
import { styled } from "styled-components";
|
||||
import { MultipleModeledMethodsPanel } from "./MultipleModeledMethodsPanel";
|
||||
|
||||
export type ModeledMethodsPanelProps = {
|
||||
method: Method;
|
||||
modeledMethods: ModeledMethod[];
|
||||
showMultipleModels: boolean;
|
||||
onChange: (modeledMethod: ModeledMethod) => void;
|
||||
};
|
||||
|
||||
const SingleMethodModelingInputs = styled(MethodModelingInputs)`
|
||||
padding-bottom: 0.5rem;
|
||||
`;
|
||||
|
||||
export const ModeledMethodsPanel = ({
|
||||
method,
|
||||
modeledMethods,
|
||||
showMultipleModels,
|
||||
onChange,
|
||||
}: ModeledMethodsPanelProps) => {
|
||||
if (!showMultipleModels) {
|
||||
return (
|
||||
<SingleMethodModelingInputs
|
||||
method={method}
|
||||
modeledMethod={
|
||||
modeledMethods.length > 0 ? modeledMethods[0] : undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MultipleModeledMethodsPanel
|
||||
method={method}
|
||||
modeledMethods={modeledMethods}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
import * as React from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { Method } from "../../model-editor/method";
|
||||
import { ModeledMethod } from "../../model-editor/modeled-method";
|
||||
import { styled } from "styled-components";
|
||||
import { MethodModelingInputs } from "./MethodModelingInputs";
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react";
|
||||
import { Codicon } from "../common";
|
||||
|
||||
export type MultipleModeledMethodsPanelProps = {
|
||||
method: Method;
|
||||
modeledMethods: ModeledMethod[];
|
||||
onChange: (modeledMethod: ModeledMethod) => void;
|
||||
};
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 0.05rem solid var(--vscode-panelSection-border);
|
||||
`;
|
||||
|
||||
const Footer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
`;
|
||||
|
||||
const PaginationActions = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5rem;
|
||||
`;
|
||||
|
||||
export const MultipleModeledMethodsPanel = ({
|
||||
method,
|
||||
modeledMethods,
|
||||
onChange,
|
||||
}: MultipleModeledMethodsPanelProps) => {
|
||||
const [selectedIndex, setSelectedIndex] = useState<number>(0);
|
||||
|
||||
const handlePreviousClick = useCallback(() => {
|
||||
setSelectedIndex((previousIndex) => previousIndex - 1);
|
||||
}, []);
|
||||
const handleNextClick = useCallback(() => {
|
||||
setSelectedIndex((previousIndex) => previousIndex + 1);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{modeledMethods.length > 0 ? (
|
||||
<MethodModelingInputs
|
||||
method={method}
|
||||
modeledMethod={modeledMethods[selectedIndex]}
|
||||
onChange={onChange}
|
||||
/>
|
||||
) : (
|
||||
<MethodModelingInputs
|
||||
method={method}
|
||||
modeledMethod={undefined}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
<Footer>
|
||||
<PaginationActions>
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
aria-label="Previous modeling"
|
||||
onClick={handlePreviousClick}
|
||||
disabled={modeledMethods.length < 2 || selectedIndex === 0}
|
||||
>
|
||||
<Codicon name="chevron-left" />
|
||||
</VSCodeButton>
|
||||
{modeledMethods.length > 1 && (
|
||||
<div>
|
||||
{selectedIndex + 1}/{modeledMethods.length}
|
||||
</div>
|
||||
)}
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
aria-label="Next modeling"
|
||||
onClick={handleNextClick}
|
||||
disabled={
|
||||
modeledMethods.length < 2 ||
|
||||
selectedIndex === modeledMethods.length - 1
|
||||
}
|
||||
>
|
||||
<Codicon name="chevron-right" />
|
||||
</VSCodeButton>
|
||||
</PaginationActions>
|
||||
</Footer>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import * as React from "react";
|
||||
import { ResponsiveContainer } from "../common/ResponsiveContainer";
|
||||
|
||||
export const NoMethodSelected = () => {
|
||||
return (
|
||||
<ResponsiveContainer>Select an API or method to model</ResponsiveContainer>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import * as React from "react";
|
||||
import { useCallback } from "react";
|
||||
import { vscode } from "../vscode-api";
|
||||
import { styled } from "styled-components";
|
||||
import TextButton from "../common/TextButton";
|
||||
import { ResponsiveContainer } from "../common/ResponsiveContainer";
|
||||
|
||||
const Button = styled(TextButton)`
|
||||
margin-top: 0.2rem;
|
||||
`;
|
||||
|
||||
export const NotInModelingMode = () => {
|
||||
const handleClick = useCallback(() => {
|
||||
vscode.postMessage({
|
||||
t: "startModeling",
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ResponsiveContainer>
|
||||
<span>Not in modeling mode</span>
|
||||
<Button onClick={handleClick}>Start modeling</Button>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
};
|
||||
@@ -6,7 +6,7 @@ import TextButton from "../common/TextButton";
|
||||
import { Method } from "../../model-editor/method";
|
||||
|
||||
const Button = styled(TextButton)`
|
||||
margin-top: 0.5rem;
|
||||
margin-top: 0.7rem;
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -16,7 +16,7 @@ describe(MethodModeling.name, () => {
|
||||
render({
|
||||
modelingStatus: "saved",
|
||||
method,
|
||||
modeledMethod,
|
||||
modeledMethods: [modeledMethod],
|
||||
onChange,
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import * as React from "react";
|
||||
import { render as reactRender, screen } from "@testing-library/react";
|
||||
import { createMethod } from "../../../../test/factories/model-editor/method-factories";
|
||||
import { createModeledMethod } from "../../../../test/factories/model-editor/modeled-method-factories";
|
||||
import {
|
||||
ModeledMethodsPanel,
|
||||
ModeledMethodsPanelProps,
|
||||
} from "../ModeledMethodsPanel";
|
||||
|
||||
describe(ModeledMethodsPanel.name, () => {
|
||||
const render = (props: ModeledMethodsPanelProps) =>
|
||||
reactRender(<ModeledMethodsPanel {...props} />);
|
||||
|
||||
const method = createMethod();
|
||||
const modeledMethods = [createModeledMethod(), createModeledMethod()];
|
||||
const onChange = jest.fn();
|
||||
|
||||
describe("when show multiple models is disabled", () => {
|
||||
const showMultipleModels = false;
|
||||
|
||||
it("renders the method modeling inputs", () => {
|
||||
render({
|
||||
method,
|
||||
modeledMethods,
|
||||
onChange,
|
||||
showMultipleModels,
|
||||
});
|
||||
|
||||
expect(screen.getAllByRole("combobox")).toHaveLength(4);
|
||||
});
|
||||
|
||||
it("does not render the pagination", () => {
|
||||
render({
|
||||
method,
|
||||
modeledMethods,
|
||||
onChange,
|
||||
showMultipleModels,
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.queryByLabelText("Previous modeling"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.queryByLabelText("Next modeling")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when show multiple models is enabled", () => {
|
||||
const showMultipleModels = true;
|
||||
|
||||
it("renders the method modeling inputs once", () => {
|
||||
render({
|
||||
method,
|
||||
modeledMethods,
|
||||
onChange,
|
||||
showMultipleModels,
|
||||
});
|
||||
|
||||
expect(screen.getAllByRole("combobox")).toHaveLength(4);
|
||||
});
|
||||
|
||||
it("renders the pagination", () => {
|
||||
render({
|
||||
method,
|
||||
modeledMethods,
|
||||
onChange,
|
||||
showMultipleModels,
|
||||
});
|
||||
|
||||
expect(screen.getByLabelText("Previous modeling")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Next modeling")).toBeInTheDocument();
|
||||
expect(screen.getByText("1/2")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,312 @@
|
||||
import * as React from "react";
|
||||
import { render as reactRender, screen, waitFor } from "@testing-library/react";
|
||||
import { createMethod } from "../../../../test/factories/model-editor/method-factories";
|
||||
import { createModeledMethod } from "../../../../test/factories/model-editor/modeled-method-factories";
|
||||
import {
|
||||
MultipleModeledMethodsPanel,
|
||||
MultipleModeledMethodsPanelProps,
|
||||
} from "../MultipleModeledMethodsPanel";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { ModeledMethod } from "../../../model-editor/modeled-method";
|
||||
|
||||
describe(MultipleModeledMethodsPanel.name, () => {
|
||||
const render = (props: MultipleModeledMethodsPanelProps) =>
|
||||
reactRender(<MultipleModeledMethodsPanel {...props} />);
|
||||
|
||||
const method = createMethod();
|
||||
const onChange = jest.fn();
|
||||
|
||||
describe("with no modeled methods", () => {
|
||||
const modeledMethods: ModeledMethod[] = [];
|
||||
|
||||
it("renders the method modeling inputs once", () => {
|
||||
render({
|
||||
method,
|
||||
modeledMethods,
|
||||
onChange,
|
||||
});
|
||||
|
||||
expect(screen.getAllByRole("combobox")).toHaveLength(4);
|
||||
expect(
|
||||
screen.getByRole("combobox", {
|
||||
name: "Model type",
|
||||
}),
|
||||
).toHaveValue("none");
|
||||
});
|
||||
|
||||
it("disables all pagination", () => {
|
||||
render({
|
||||
method,
|
||||
modeledMethods,
|
||||
onChange,
|
||||
});
|
||||
|
||||
expect(
|
||||
screen
|
||||
.getByLabelText("Previous modeling")
|
||||
.getElementsByTagName("input")[0],
|
||||
).toBeDisabled();
|
||||
expect(
|
||||
screen.getByLabelText("Next modeling").getElementsByTagName("input")[0],
|
||||
).toBeDisabled();
|
||||
expect(screen.queryByText("0/0")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("1/0")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("with one modeled method", () => {
|
||||
const modeledMethods = [
|
||||
createModeledMethod({
|
||||
...method,
|
||||
type: "sink",
|
||||
input: "Argument[this]",
|
||||
output: "",
|
||||
kind: "path-injection",
|
||||
}),
|
||||
];
|
||||
|
||||
it("renders the method modeling inputs once", () => {
|
||||
render({
|
||||
method,
|
||||
modeledMethods,
|
||||
onChange,
|
||||
});
|
||||
|
||||
expect(screen.getAllByRole("combobox")).toHaveLength(4);
|
||||
expect(
|
||||
screen.getByRole("combobox", {
|
||||
name: "Model type",
|
||||
}),
|
||||
).toHaveValue("sink");
|
||||
});
|
||||
|
||||
it("disables all pagination", () => {
|
||||
render({
|
||||
method,
|
||||
modeledMethods,
|
||||
onChange,
|
||||
});
|
||||
|
||||
expect(
|
||||
screen
|
||||
.getByLabelText("Previous modeling")
|
||||
.getElementsByTagName("input")[0],
|
||||
).toBeDisabled();
|
||||
expect(
|
||||
screen.getByLabelText("Next modeling").getElementsByTagName("input")[0],
|
||||
).toBeDisabled();
|
||||
expect(screen.queryByText("1/1")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("with two modeled methods", () => {
|
||||
const modeledMethods = [
|
||||
createModeledMethod({
|
||||
...method,
|
||||
type: "sink",
|
||||
input: "Argument[this]",
|
||||
output: "",
|
||||
kind: "path-injection",
|
||||
}),
|
||||
createModeledMethod({
|
||||
...method,
|
||||
type: "source",
|
||||
input: "",
|
||||
output: "ReturnValue",
|
||||
kind: "remote",
|
||||
}),
|
||||
];
|
||||
|
||||
it("renders the method modeling inputs once", () => {
|
||||
render({
|
||||
method,
|
||||
modeledMethods,
|
||||
onChange,
|
||||
});
|
||||
|
||||
expect(screen.getAllByRole("combobox")).toHaveLength(4);
|
||||
expect(
|
||||
screen.getByRole("combobox", {
|
||||
name: "Model type",
|
||||
}),
|
||||
).toHaveValue("sink");
|
||||
});
|
||||
|
||||
it("renders the pagination", () => {
|
||||
render({
|
||||
method,
|
||||
modeledMethods,
|
||||
onChange,
|
||||
});
|
||||
|
||||
expect(screen.getByLabelText("Previous modeling")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Next modeling")).toBeInTheDocument();
|
||||
expect(screen.getByText("1/2")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("disables the correct pagination", async () => {
|
||||
render({
|
||||
method,
|
||||
modeledMethods,
|
||||
onChange,
|
||||
});
|
||||
|
||||
expect(
|
||||
screen
|
||||
.getByLabelText("Previous modeling")
|
||||
.getElementsByTagName("input")[0],
|
||||
).toBeDisabled();
|
||||
expect(
|
||||
screen.getByLabelText("Next modeling").getElementsByTagName("input")[0],
|
||||
).toBeEnabled();
|
||||
});
|
||||
|
||||
it("can use the pagination", async () => {
|
||||
render({
|
||||
method,
|
||||
modeledMethods,
|
||||
onChange,
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByLabelText("Next modeling"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen
|
||||
.getByLabelText("Previous modeling")
|
||||
.getElementsByTagName("input")[0],
|
||||
).toBeEnabled();
|
||||
});
|
||||
|
||||
expect(
|
||||
screen
|
||||
.getByLabelText("Previous modeling")
|
||||
.getElementsByTagName("input")[0],
|
||||
).toBeEnabled();
|
||||
expect(
|
||||
screen.getByLabelText("Next modeling").getElementsByTagName("input")[0],
|
||||
).toBeDisabled();
|
||||
expect(screen.getByText("2/2")).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.getByRole("combobox", {
|
||||
name: "Model type",
|
||||
}),
|
||||
).toHaveValue("source");
|
||||
});
|
||||
});
|
||||
|
||||
describe("with three modeled methods", () => {
|
||||
const modeledMethods = [
|
||||
createModeledMethod({
|
||||
...method,
|
||||
type: "sink",
|
||||
input: "Argument[this]",
|
||||
output: "",
|
||||
kind: "path-injection",
|
||||
}),
|
||||
createModeledMethod({
|
||||
...method,
|
||||
type: "source",
|
||||
input: "",
|
||||
output: "ReturnValue",
|
||||
kind: "remote",
|
||||
}),
|
||||
createModeledMethod({
|
||||
...method,
|
||||
type: "source",
|
||||
input: "",
|
||||
output: "ReturnValue",
|
||||
kind: "local",
|
||||
}),
|
||||
];
|
||||
|
||||
it("can use the pagination", async () => {
|
||||
render({
|
||||
method,
|
||||
modeledMethods,
|
||||
onChange,
|
||||
});
|
||||
|
||||
expect(
|
||||
screen
|
||||
.getByLabelText("Previous modeling")
|
||||
.getElementsByTagName("input")[0],
|
||||
).toBeDisabled();
|
||||
expect(
|
||||
screen.getByLabelText("Next modeling").getElementsByTagName("input")[0],
|
||||
).toBeEnabled();
|
||||
expect(screen.getByText("1/3")).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(screen.getByLabelText("Next modeling"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen
|
||||
.getByLabelText("Previous modeling")
|
||||
.getElementsByTagName("input")[0],
|
||||
).toBeEnabled();
|
||||
});
|
||||
|
||||
expect(
|
||||
screen
|
||||
.getByLabelText("Previous modeling")
|
||||
.getElementsByTagName("input")[0],
|
||||
).toBeEnabled();
|
||||
expect(
|
||||
screen.getByLabelText("Next modeling").getElementsByTagName("input")[0],
|
||||
).toBeEnabled();
|
||||
expect(screen.getByText("2/3")).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.getByRole("combobox", {
|
||||
name: "Model type",
|
||||
}),
|
||||
).toHaveValue("source");
|
||||
|
||||
await userEvent.click(screen.getByLabelText("Next modeling"));
|
||||
|
||||
expect(
|
||||
screen
|
||||
.getByLabelText("Previous modeling")
|
||||
.getElementsByTagName("input")[0],
|
||||
).toBeEnabled();
|
||||
expect(
|
||||
screen.getByLabelText("Next modeling").getElementsByTagName("input")[0],
|
||||
).toBeDisabled();
|
||||
expect(screen.getByText("3/3")).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.getByRole("combobox", {
|
||||
name: "Kind",
|
||||
}),
|
||||
).toHaveValue("local");
|
||||
|
||||
await userEvent.click(screen.getByLabelText("Previous modeling"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen
|
||||
.getByLabelText("Next modeling")
|
||||
.getElementsByTagName("input")[0],
|
||||
).toBeEnabled();
|
||||
});
|
||||
|
||||
expect(
|
||||
screen
|
||||
.getByLabelText("Previous modeling")
|
||||
.getElementsByTagName("input")[0],
|
||||
).toBeEnabled();
|
||||
expect(
|
||||
screen.getByLabelText("Next modeling").getElementsByTagName("input")[0],
|
||||
).toBeEnabled();
|
||||
expect(screen.getByText("2/3")).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.getByRole("combobox", {
|
||||
name: "Kind",
|
||||
}),
|
||||
).toHaveValue("remote");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -78,10 +78,7 @@ export type LibraryRowProps = {
|
||||
hideModeledMethods: boolean;
|
||||
revealedMethodSignature: string | null;
|
||||
onChange: (modeledMethod: ModeledMethod) => void;
|
||||
onSaveModelClick: (
|
||||
methods: Method[],
|
||||
modeledMethods: Record<string, ModeledMethod>,
|
||||
) => void;
|
||||
onSaveModelClick: (methodSignatures: string[]) => void;
|
||||
onGenerateFromLlmClick: (
|
||||
dependencyName: string,
|
||||
methods: Method[],
|
||||
@@ -165,11 +162,11 @@ export const LibraryRow = ({
|
||||
|
||||
const handleSave = useCallback(
|
||||
async (e: React.MouseEvent) => {
|
||||
onSaveModelClick(methods, modeledMethods);
|
||||
onSaveModelClick(methods.map((m) => m.signature));
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
},
|
||||
[methods, modeledMethods, onSaveModelClick],
|
||||
[methods, onSaveModelClick],
|
||||
);
|
||||
|
||||
const hasUnsavedChanges = useMemo(() => {
|
||||
|
||||
@@ -142,7 +142,7 @@ export function ModelEditor({
|
||||
);
|
||||
break;
|
||||
case "revealMethod":
|
||||
setRevealedMethodSignature(msg.method.signature);
|
||||
setRevealedMethodSignature(msg.methodSignature);
|
||||
|
||||
break;
|
||||
default:
|
||||
@@ -196,21 +196,15 @@ export function ModelEditor({
|
||||
const onSaveAllClick = useCallback(() => {
|
||||
vscode.postMessage({
|
||||
t: "saveModeledMethods",
|
||||
methods,
|
||||
modeledMethods,
|
||||
});
|
||||
}, [methods, modeledMethods]);
|
||||
}, []);
|
||||
|
||||
const onSaveModelClick = useCallback(
|
||||
(methods: Method[], modeledMethods: Record<string, ModeledMethod>) => {
|
||||
vscode.postMessage({
|
||||
t: "saveModeledMethods",
|
||||
methods,
|
||||
modeledMethods,
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
const onSaveModelClick = useCallback((methodSignatures: string[]) => {
|
||||
vscode.postMessage({
|
||||
t: "saveModeledMethods",
|
||||
methodSignatures,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onGenerateFromSourceClick = useCallback(() => {
|
||||
vscode.postMessage({
|
||||
|
||||
@@ -20,10 +20,7 @@ export type ModeledMethodsListProps = {
|
||||
viewState: ModelEditorViewState;
|
||||
hideModeledMethods: boolean;
|
||||
onChange: (modeledMethod: ModeledMethod) => void;
|
||||
onSaveModelClick: (
|
||||
methods: Method[],
|
||||
modeledMethods: Record<string, ModeledMethod>,
|
||||
) => void;
|
||||
onSaveModelClick: (methodSignatures: string[]) => void;
|
||||
onGenerateFromLlmClick: (
|
||||
packageName: string,
|
||||
methods: Method[],
|
||||
|
||||
@@ -57,6 +57,7 @@ describe(ModelKindDropdown.name, () => {
|
||||
// Changing the type to sink should update the supported kinds
|
||||
const updatedModeledMethod = createModeledMethod({
|
||||
type: "sink",
|
||||
kind: "local",
|
||||
});
|
||||
|
||||
rerender(
|
||||
|
||||
@@ -13,7 +13,7 @@ export function createModeledMethod(
|
||||
type: "sink",
|
||||
input: "Argument[0]",
|
||||
output: "",
|
||||
kind: "jndi-injection",
|
||||
kind: "path-injection",
|
||||
provenance: "manual",
|
||||
...data,
|
||||
};
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import {
|
||||
ConfigurationScope,
|
||||
Uri,
|
||||
workspace,
|
||||
WorkspaceConfiguration as VSCodeWorkspaceConfiguration,
|
||||
WorkspaceFolder,
|
||||
} from "vscode";
|
||||
import { Uri, workspace, WorkspaceFolder } from "vscode";
|
||||
import { dump as dumpYaml, load as loadYaml } from "js-yaml";
|
||||
import { outputFile, readFile } from "fs-extra";
|
||||
import { join } from "path";
|
||||
@@ -14,7 +8,8 @@ import { QlpacksInfo } from "../../../../src/codeql-cli/cli";
|
||||
import { pickExtensionPack } from "../../../../src/model-editor/extension-pack-picker";
|
||||
import { ExtensionPack } from "../../../../src/model-editor/shared/extension-pack";
|
||||
import { createMockLogger } from "../../../__mocks__/loggerMock";
|
||||
import { vscodeGetConfigurationMock } from "../../test-config";
|
||||
import { ModelConfig } from "../../../../src/config";
|
||||
import { mockedObject } from "../../utils/mocking.helpers";
|
||||
|
||||
describe("pickExtensionPack", () => {
|
||||
let tmpDir: string;
|
||||
@@ -32,6 +27,7 @@ describe("pickExtensionPack", () => {
|
||||
let workspaceFoldersSpy: jest.SpyInstance;
|
||||
let additionalPacks: string[];
|
||||
let workspaceFolder: WorkspaceFolder;
|
||||
let modelConfig: ModelConfig;
|
||||
|
||||
const logger = createMockLogger();
|
||||
const maxStep = 4;
|
||||
@@ -67,41 +63,20 @@ describe("pickExtensionPack", () => {
|
||||
workspaceFoldersSpy = jest
|
||||
.spyOn(workspace, "workspaceFolders", "get")
|
||||
.mockReturnValue([workspaceFolder]);
|
||||
|
||||
modelConfig = mockedObject<ModelConfig>({
|
||||
getExtensionsDirectory: jest.fn().mockReturnValue(undefined),
|
||||
});
|
||||
});
|
||||
|
||||
it("selects an existing extension pack", async () => {
|
||||
vscodeGetConfigurationMock.mockImplementation(
|
||||
(
|
||||
section?: string,
|
||||
scope?: ConfigurationScope | null,
|
||||
): VSCodeWorkspaceConfiguration => {
|
||||
expect(section).toEqual("codeQL.model");
|
||||
expect((scope as any)?.languageId).toEqual("java");
|
||||
|
||||
return {
|
||||
get: (key: string) => {
|
||||
expect(key).toEqual("extensionsDirectory");
|
||||
return undefined;
|
||||
},
|
||||
has: (key: string) => {
|
||||
return key === "extensionsDirectory";
|
||||
},
|
||||
inspect: () => {
|
||||
throw new Error("inspect not implemented");
|
||||
},
|
||||
update: () => {
|
||||
throw new Error("update not implemented");
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const cliServer = mockCliServer(qlPacks);
|
||||
|
||||
expect(
|
||||
await pickExtensionPack(
|
||||
cliServer,
|
||||
databaseItem,
|
||||
modelConfig,
|
||||
logger,
|
||||
progress,
|
||||
maxStep,
|
||||
@@ -112,35 +87,10 @@ describe("pickExtensionPack", () => {
|
||||
additionalPacks,
|
||||
true,
|
||||
);
|
||||
expect(modelConfig.getExtensionsDirectory).toHaveBeenCalledWith("java");
|
||||
});
|
||||
|
||||
it("creates a new extension pack using default extensions directory", async () => {
|
||||
vscodeGetConfigurationMock.mockImplementation(
|
||||
(
|
||||
section?: string,
|
||||
scope?: ConfigurationScope | null,
|
||||
): VSCodeWorkspaceConfiguration => {
|
||||
expect(section).toEqual("codeQL.model");
|
||||
expect((scope as any)?.languageId).toEqual("java");
|
||||
|
||||
return {
|
||||
get: (key: string) => {
|
||||
expect(key).toEqual("extensionsDirectory");
|
||||
return undefined;
|
||||
},
|
||||
has: (key: string) => {
|
||||
return key === "extensionsDirectory";
|
||||
},
|
||||
inspect: () => {
|
||||
throw new Error("inspect not implemented");
|
||||
},
|
||||
update: () => {
|
||||
throw new Error("update not implemented");
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const tmpDir = await dir({
|
||||
unsafeCleanup: true,
|
||||
});
|
||||
@@ -183,6 +133,7 @@ describe("pickExtensionPack", () => {
|
||||
await pickExtensionPack(
|
||||
cliServer,
|
||||
databaseItem,
|
||||
modelConfig,
|
||||
logger,
|
||||
progress,
|
||||
maxStep,
|
||||
@@ -199,6 +150,7 @@ describe("pickExtensionPack", () => {
|
||||
dataExtensions: ["models/**/*.yml"],
|
||||
});
|
||||
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
|
||||
expect(modelConfig.getExtensionsDirectory).toHaveBeenCalledWith("java");
|
||||
|
||||
expect(
|
||||
loadYaml(await readFile(join(newPackDir, "codeql-pack.yml"), "utf8")),
|
||||
@@ -223,31 +175,9 @@ describe("pickExtensionPack", () => {
|
||||
"my-custom-extensions-directory",
|
||||
);
|
||||
|
||||
vscodeGetConfigurationMock.mockImplementation(
|
||||
(
|
||||
section?: string,
|
||||
scope?: ConfigurationScope | null,
|
||||
): VSCodeWorkspaceConfiguration => {
|
||||
expect(section).toEqual("codeQL.model");
|
||||
expect((scope as any)?.languageId).toEqual("java");
|
||||
|
||||
return {
|
||||
get: (key: string) => {
|
||||
expect(key).toEqual("extensionsDirectory");
|
||||
return configExtensionsDir;
|
||||
},
|
||||
has: (key: string) => {
|
||||
return key === "extensionsDirectory";
|
||||
},
|
||||
inspect: () => {
|
||||
throw new Error("inspect not implemented");
|
||||
},
|
||||
update: () => {
|
||||
throw new Error("update not implemented");
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
const modelConfig = mockedObject<ModelConfig>({
|
||||
getExtensionsDirectory: jest.fn().mockReturnValue(configExtensionsDir),
|
||||
});
|
||||
|
||||
const newPackDir = join(configExtensionsDir, "vscode-codeql-java");
|
||||
|
||||
@@ -257,6 +187,7 @@ describe("pickExtensionPack", () => {
|
||||
await pickExtensionPack(
|
||||
cliServer,
|
||||
databaseItem,
|
||||
modelConfig,
|
||||
logger,
|
||||
progress,
|
||||
maxStep,
|
||||
@@ -273,6 +204,7 @@ describe("pickExtensionPack", () => {
|
||||
dataExtensions: ["models/**/*.yml"],
|
||||
});
|
||||
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
|
||||
expect(modelConfig.getExtensionsDirectory).toHaveBeenCalledWith("java");
|
||||
|
||||
expect(
|
||||
loadYaml(await readFile(join(newPackDir, "codeql-pack.yml"), "utf8")),
|
||||
@@ -299,6 +231,7 @@ describe("pickExtensionPack", () => {
|
||||
await pickExtensionPack(
|
||||
cliServer,
|
||||
databaseItem,
|
||||
modelConfig,
|
||||
logger,
|
||||
progress,
|
||||
maxStep,
|
||||
@@ -324,6 +257,7 @@ describe("pickExtensionPack", () => {
|
||||
await pickExtensionPack(
|
||||
cliServer,
|
||||
databaseItem,
|
||||
modelConfig,
|
||||
logger,
|
||||
progress,
|
||||
maxStep,
|
||||
@@ -351,6 +285,7 @@ describe("pickExtensionPack", () => {
|
||||
await pickExtensionPack(
|
||||
cliServer,
|
||||
databaseItem,
|
||||
modelConfig,
|
||||
logger,
|
||||
progress,
|
||||
maxStep,
|
||||
@@ -388,6 +323,7 @@ describe("pickExtensionPack", () => {
|
||||
await pickExtensionPack(
|
||||
cliServer,
|
||||
databaseItem,
|
||||
modelConfig,
|
||||
logger,
|
||||
progress,
|
||||
maxStep,
|
||||
@@ -425,6 +361,7 @@ describe("pickExtensionPack", () => {
|
||||
await pickExtensionPack(
|
||||
cliServer,
|
||||
databaseItem,
|
||||
modelConfig,
|
||||
logger,
|
||||
progress,
|
||||
maxStep,
|
||||
@@ -465,6 +402,7 @@ describe("pickExtensionPack", () => {
|
||||
await pickExtensionPack(
|
||||
cliServer,
|
||||
databaseItem,
|
||||
modelConfig,
|
||||
logger,
|
||||
progress,
|
||||
maxStep,
|
||||
@@ -522,6 +460,7 @@ describe("pickExtensionPack", () => {
|
||||
await pickExtensionPack(
|
||||
cliServer,
|
||||
databaseItem,
|
||||
modelConfig,
|
||||
logger,
|
||||
progress,
|
||||
maxStep,
|
||||
|
||||
@@ -8,6 +8,7 @@ import { QueryLanguage } from "../../../../src/common/query-language";
|
||||
import { Mode } from "../../../../src/model-editor/shared/mode";
|
||||
import { mockedObject } from "../../utils/mocking.helpers";
|
||||
import { CodeQLCliServer } from "../../../../src/codeql-cli/cli";
|
||||
import { ModelConfig } from "../../../../src/config";
|
||||
|
||||
describe("setUpPack", () => {
|
||||
let queryDir: string;
|
||||
@@ -32,8 +33,11 @@ describe("setUpPack", () => {
|
||||
packInstall: jest.fn(),
|
||||
resolveQueriesInSuite: jest.fn().mockResolvedValue([]),
|
||||
});
|
||||
const modelConfig = mockedObject<ModelConfig>({
|
||||
llmGeneration: false,
|
||||
});
|
||||
|
||||
await setUpPack(cliServer, queryDir, language);
|
||||
await setUpPack(cliServer, queryDir, language, modelConfig);
|
||||
|
||||
const queryFiles = await readdir(queryDir);
|
||||
expect(queryFiles.sort()).toEqual(
|
||||
@@ -89,8 +93,11 @@ describe("setUpPack", () => {
|
||||
.fn()
|
||||
.mockResolvedValue(["/a/b/c/ApplicationModeEndpoints.ql"]),
|
||||
});
|
||||
const modelConfig = mockedObject<ModelConfig>({
|
||||
llmGeneration: false,
|
||||
});
|
||||
|
||||
await setUpPack(cliServer, queryDir, language);
|
||||
await setUpPack(cliServer, queryDir, language, modelConfig);
|
||||
|
||||
const queryFiles = await readdir(queryDir);
|
||||
expect(queryFiles.sort()).toEqual(["codeql-pack.yml"].sort());
|
||||
|
||||
@@ -10,11 +10,15 @@ import { QueryRunner } from "../../../../src/query-server";
|
||||
import { ExtensionPack } from "../../../../src/model-editor/shared/extension-pack";
|
||||
import { createMockModelingStore } from "../../../__mocks__/model-editor/modelingStoreMock";
|
||||
import { createMockModelEditorViewTracker } from "../../../__mocks__/model-editor/modelEditorViewTrackerMock";
|
||||
import { ModelConfigListener } from "../../../../src/config";
|
||||
|
||||
describe("ModelEditorView", () => {
|
||||
const app = createMockApp({});
|
||||
const modelingStore = createMockModelingStore();
|
||||
const viewTracker = createMockModelEditorViewTracker();
|
||||
const modelConfig = mockedObject<ModelConfigListener>({
|
||||
onDidChangeConfiguration: jest.fn(),
|
||||
});
|
||||
const databaseManager = mockEmptyDatabaseManager();
|
||||
const cliServer = mockedObject<CodeQLCliServer>({});
|
||||
const queryRunner = mockedObject<QueryRunner>({});
|
||||
@@ -41,6 +45,7 @@ describe("ModelEditorView", () => {
|
||||
app,
|
||||
modelingStore,
|
||||
viewTracker,
|
||||
modelConfig,
|
||||
databaseManager,
|
||||
cliServer,
|
||||
queryRunner,
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
workspace,
|
||||
ConfigurationTarget,
|
||||
window,
|
||||
env,
|
||||
} from "vscode";
|
||||
import {
|
||||
ExtensionTelemetryListener,
|
||||
@@ -30,13 +31,18 @@ describe("telemetry reporting", () => {
|
||||
let sendTelemetryEventSpy: jest.SpiedFunction<
|
||||
typeof TelemetryReporter.prototype.sendTelemetryEvent
|
||||
>;
|
||||
let sendTelemetryExceptionSpy: jest.SpiedFunction<
|
||||
typeof TelemetryReporter.prototype.sendTelemetryException
|
||||
let sendTelemetryErrorEventSpy: jest.SpiedFunction<
|
||||
typeof TelemetryReporter.prototype.sendTelemetryErrorEvent
|
||||
>;
|
||||
let disposeSpy: jest.SpiedFunction<
|
||||
typeof TelemetryReporter.prototype.dispose
|
||||
>;
|
||||
|
||||
let isTelemetryEnabledSpy: jest.SpyInstance<
|
||||
typeof env.isTelemetryEnabled,
|
||||
[]
|
||||
>;
|
||||
|
||||
let showInformationMessageSpy: jest.SpiedFunction<
|
||||
typeof window.showInformationMessage
|
||||
>;
|
||||
@@ -56,8 +62,8 @@ describe("telemetry reporting", () => {
|
||||
sendTelemetryEventSpy = jest
|
||||
.spyOn(TelemetryReporter.prototype, "sendTelemetryEvent")
|
||||
.mockReturnValue(undefined);
|
||||
sendTelemetryExceptionSpy = jest
|
||||
.spyOn(TelemetryReporter.prototype, "sendTelemetryException")
|
||||
sendTelemetryErrorEventSpy = jest
|
||||
.spyOn(TelemetryReporter.prototype, "sendTelemetryErrorEvent")
|
||||
.mockReturnValue(undefined);
|
||||
disposeSpy = jest
|
||||
.spyOn(TelemetryReporter.prototype, "dispose")
|
||||
@@ -78,6 +84,9 @@ describe("telemetry reporting", () => {
|
||||
.get<boolean>("codeQL.canary")).toString();
|
||||
|
||||
// each test will default to telemetry being enabled
|
||||
isTelemetryEnabledSpy = jest
|
||||
.spyOn(env, "isTelemetryEnabled", "get")
|
||||
.mockReturnValue(true);
|
||||
await enableTelemetry("telemetry", true);
|
||||
await enableTelemetry("codeQL.telemetry", true);
|
||||
|
||||
@@ -116,6 +125,7 @@ describe("telemetry reporting", () => {
|
||||
});
|
||||
|
||||
it("should initialize telemetry when global option disabled", async () => {
|
||||
isTelemetryEnabledSpy.mockReturnValue(false);
|
||||
await enableTelemetry("telemetry", false);
|
||||
await telemetryListener.initialize();
|
||||
expect(telemetryListener._reporter).toBeDefined();
|
||||
@@ -133,6 +143,7 @@ describe("telemetry reporting", () => {
|
||||
|
||||
it("should not initialize telemetry when both options disabled", async () => {
|
||||
await enableTelemetry("codeQL.telemetry", false);
|
||||
isTelemetryEnabledSpy.mockReturnValue(false);
|
||||
await enableTelemetry("telemetry", false);
|
||||
await telemetryListener.initialize();
|
||||
expect(telemetryListener._reporter).toBeUndefined();
|
||||
@@ -179,6 +190,7 @@ describe("telemetry reporting", () => {
|
||||
const reporter: any = telemetryListener._reporter;
|
||||
expect(reporter.userOptIn).toBe(true); // enabled
|
||||
|
||||
isTelemetryEnabledSpy.mockReturnValue(false);
|
||||
await enableTelemetry("telemetry", false);
|
||||
expect(reporter.userOptIn).toBe(false); // disabled
|
||||
});
|
||||
@@ -198,8 +210,7 @@ describe("telemetry reporting", () => {
|
||||
},
|
||||
{ executionTime: 1234 },
|
||||
);
|
||||
|
||||
expect(sendTelemetryExceptionSpy).not.toBeCalled();
|
||||
expect(sendTelemetryErrorEventSpy).not.toBeCalled();
|
||||
});
|
||||
|
||||
it("should send a command usage event with an error", async () => {
|
||||
@@ -221,8 +232,7 @@ describe("telemetry reporting", () => {
|
||||
},
|
||||
{ executionTime: 1234 },
|
||||
);
|
||||
|
||||
expect(sendTelemetryExceptionSpy).not.toBeCalled();
|
||||
expect(sendTelemetryErrorEventSpy).not.toBeCalled();
|
||||
});
|
||||
|
||||
it("should send a command usage event with a cli version", async () => {
|
||||
@@ -245,8 +255,7 @@ describe("telemetry reporting", () => {
|
||||
},
|
||||
{ executionTime: 1234 },
|
||||
);
|
||||
|
||||
expect(sendTelemetryExceptionSpy).not.toBeCalled();
|
||||
expect(sendTelemetryErrorEventSpy).not.toBeCalled();
|
||||
|
||||
// Verify that if the cli version is not set, then the telemetry falls back to "not-set"
|
||||
sendTelemetryEventSpy.mockClear();
|
||||
@@ -268,6 +277,7 @@ describe("telemetry reporting", () => {
|
||||
},
|
||||
{ executionTime: 5678 },
|
||||
);
|
||||
expect(sendTelemetryErrorEventSpy).not.toBeCalled();
|
||||
});
|
||||
|
||||
it("should avoid sending an event when telemetry is disabled", async () => {
|
||||
@@ -278,7 +288,7 @@ describe("telemetry reporting", () => {
|
||||
telemetryListener.sendCommandUsage("command-id", 1234, new Error());
|
||||
|
||||
expect(sendTelemetryEventSpy).not.toBeCalled();
|
||||
expect(sendTelemetryExceptionSpy).not.toBeCalled();
|
||||
expect(sendTelemetryErrorEventSpy).not.toBeCalled();
|
||||
});
|
||||
|
||||
it("should send an event when telemetry is re-enabled", async () => {
|
||||
@@ -298,6 +308,7 @@ describe("telemetry reporting", () => {
|
||||
},
|
||||
{ executionTime: 1234 },
|
||||
);
|
||||
expect(sendTelemetryErrorEventSpy).not.toBeCalled();
|
||||
});
|
||||
|
||||
it("should filter undesired properties from telemetry payload", async () => {
|
||||
@@ -345,6 +356,8 @@ describe("telemetry reporting", () => {
|
||||
resolveArg(3 /* "yes" item */),
|
||||
);
|
||||
await ctx.globalState.update("telemetry-request-viewed", false);
|
||||
expect(env.isTelemetryEnabled).toBe(true);
|
||||
|
||||
await enableTelemetry("codeQL.telemetry", false);
|
||||
|
||||
await telemetryListener.initialize();
|
||||
@@ -411,6 +424,7 @@ describe("telemetry reporting", () => {
|
||||
// If the user ever turns global telemetry back on, then we can
|
||||
// show the dialog.
|
||||
|
||||
isTelemetryEnabledSpy.mockReturnValue(false);
|
||||
await enableTelemetry("telemetry", false);
|
||||
await ctx.globalState.update("telemetry-request-viewed", false);
|
||||
|
||||
@@ -455,6 +469,7 @@ describe("telemetry reporting", () => {
|
||||
},
|
||||
{},
|
||||
);
|
||||
expect(sendTelemetryErrorEventSpy).not.toBeCalled();
|
||||
});
|
||||
|
||||
it("should send a ui-interaction telementry event with a cli version", async () => {
|
||||
@@ -472,6 +487,7 @@ describe("telemetry reporting", () => {
|
||||
},
|
||||
{},
|
||||
);
|
||||
expect(sendTelemetryErrorEventSpy).not.toBeCalled();
|
||||
});
|
||||
|
||||
it("should send an error telementry event", async () => {
|
||||
@@ -479,7 +495,8 @@ describe("telemetry reporting", () => {
|
||||
|
||||
telemetryListener.sendError(redactableError`test`);
|
||||
|
||||
expect(sendTelemetryEventSpy).toHaveBeenCalledWith(
|
||||
expect(sendTelemetryEventSpy).not.toBeCalled();
|
||||
expect(sendTelemetryErrorEventSpy).toHaveBeenCalledWith(
|
||||
"error",
|
||||
{
|
||||
message: "test",
|
||||
@@ -497,7 +514,8 @@ describe("telemetry reporting", () => {
|
||||
|
||||
telemetryListener.sendError(redactableError`test`);
|
||||
|
||||
expect(sendTelemetryEventSpy).toHaveBeenCalledWith(
|
||||
expect(sendTelemetryEventSpy).not.toBeCalled();
|
||||
expect(sendTelemetryErrorEventSpy).toHaveBeenCalledWith(
|
||||
"error",
|
||||
{
|
||||
message: "test",
|
||||
@@ -516,7 +534,8 @@ describe("telemetry reporting", () => {
|
||||
redactableError`test message with secret information: ${42} and more ${"secret"} parts`,
|
||||
);
|
||||
|
||||
expect(sendTelemetryEventSpy).toHaveBeenCalledWith(
|
||||
expect(sendTelemetryEventSpy).not.toBeCalled();
|
||||
expect(sendTelemetryErrorEventSpy).toHaveBeenCalledWith(
|
||||
"error",
|
||||
{
|
||||
message:
|
||||
|
||||
Reference in New Issue
Block a user