Merge branch 'main' into location-url-column

This commit is contained in:
Robert
2023-07-19 09:58:46 +01:00
committed by GitHub
62 changed files with 14484 additions and 32491 deletions

View File

@@ -44,21 +44,21 @@ choose to go through some of the Optional Test Cases.
#### Test case 2: Running a problem query and viewing results
1. Open the [javascript UnsafeJQueryPlugin query](https://github.com/github/codeql/blob/main/javascript/ql/src/Security/CWE-079/UnsafeJQueryPlugin.ql).
1. Open the [javascript ReDoS query](https://github.com/github/codeql/blob/main/javascript/ql/src/Performance/ReDoS.ql).
2. Select the `babel/babel` database (or download it if you don't have one already)
3. Run a local query.
4. Once the query completes:
- Check that the result messages are rendered
- Check that alert locations can be clicked on
#### Test case 3: Running a non-probem query and viewing results
#### Test case 3: Running a non-problem query and viewing results
1. Open the [cpp FunLinesOfCode query](https://github.com/github/codeql/blob/main/cpp/ql/src/Metrics/Functions/FunLinesOfCode.ql).
2. Select the `google/brotli` database (or download it if you don't have one already)
3. Run a local query.
4. Once the query completes:
- Check that the results table is rendered
- Check that alert locations can be clicked on
- Check that result locations can be clicked on
#### Test case 3: Can use AST viewer

View File

@@ -1,4 +1,4 @@
import type { StorybookConfig } from "@storybook/core-common";
import type { StorybookConfig } from "@storybook/react-webpack5";
const config: StorybookConfig = {
stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
@@ -8,13 +8,13 @@ const config: StorybookConfig = {
"@storybook/addon-interactions",
"./vscode-theme-addon/preset.ts",
],
framework: "@storybook/react",
core: {
builder: "@storybook/builder-webpack5",
framework: {
name: "@storybook/react-webpack5",
options: {},
},
features: {
babelModeV7: true,
docs: {
autodocs: "tag",
},
};
module.exports = config;
export default config;

View File

@@ -1,4 +1,4 @@
import { addons } from "@storybook/addons";
import { addons } from "@storybook/manager-api";
import { themes } from "@storybook/theming";
addons.setConfig({

View File

@@ -1,31 +1,36 @@
import { Preview } from "@storybook/react";
import { themes } from "@storybook/theming";
import { action } from "@storybook/addon-actions";
// Allow all stories/components to use Codicons
import "@vscode/codicons/dist/codicon.css";
// https://storybook.js.org/docs/react/configure/overview#configure-story-rendering
export const parameters = {
// All props starting with `on` will automatically receive an action as a prop
actions: { argTypesRegex: "^on[A-Z].*" },
// All props matching these names will automatically get the correct control
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
// Use a dark theme to be aligned with VSCode
docs: {
theme: themes.dark,
},
backgrounds: {
// The background is injected by our theme CSS files
disable: true,
},
};
(window as any).acquireVsCodeApi = () => ({
postMessage: action("post-vscode-message"),
setState: action("set-vscode-state"),
});
// https://storybook.js.org/docs/react/configure/overview#configure-story-rendering
const preview: Preview = {
parameters: {
// All props starting with `on` will automatically receive an action as a prop
actions: { argTypesRegex: "^on[A-Z].*" },
// All props matching these names will automatically get the correct control
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
// Use a dark theme to be aligned with VSCode
docs: {
theme: themes.dark,
},
backgrounds: {
// The background is injected by our theme CSS files
disable: true,
},
},
};
export default preview;

View File

@@ -1,14 +1,12 @@
import * as React from "react";
import { FunctionComponent, useCallback } from "react";
import { useGlobals } from "@storybook/api";
import { useGlobals } from "@storybook/manager-api";
import {
IconButton,
Icons,
WithTooltip,
TooltipLinkList,
Link,
WithHideFn,
WithTooltip,
} from "@storybook/components";
import { themeNames, VSCodeTheme } from "./theme";
@@ -26,7 +24,7 @@ export const ThemeSelector: FunctionComponent = () => {
);
const createLinks = useCallback(
(onHide: () => void): Link[] =>
(onHide: () => void) =>
Object.values(VSCodeTheme).map((theme) => ({
id: theme,
onClick() {
@@ -44,8 +42,8 @@ export const ThemeSelector: FunctionComponent = () => {
<WithTooltip
placement="top"
trigger="click"
closeOnClick
tooltip={({ onHide }: WithHideFn) => (
closeOnOutsideClick
tooltip={({ onHide }: { onHide: () => void }) => (
<TooltipLinkList links={createLinks(onHide)} />
)}
>

View File

@@ -1,5 +1,5 @@
import * as React from "react";
import { addons, types } from "@storybook/addons";
import { addons, types } from "@storybook/manager-api";
import { ThemeSelector } from "./ThemeSelector";
const ADDON_ID = "vscode-theme-addon";

View File

@@ -1,4 +1,4 @@
export function config(entry = []) {
export function previewAnnotations(entry = []) {
return [...entry, require.resolve("./preview.ts")];
}

View File

@@ -1,6 +1,5 @@
import { useEffect, useGlobals } from "@storybook/addons";
import { useEffect } from "react";
import type {
AnyFramework,
PartialStoryFn as StoryFunction,
StoryContext,
} from "@storybook/csf";
@@ -34,11 +33,8 @@ const themeFiles: { [key in VSCodeTheme]: string } = {
.default,
};
export const withTheme = (
StoryFn: StoryFunction<AnyFramework>,
context: StoryContext<AnyFramework>,
) => {
const [{ vscodeTheme }] = useGlobals();
export const withTheme = (StoryFn: StoryFunction, context: StoryContext) => {
const { vscodeTheme } = context.globals;
useEffect(() => {
const styleSelectorId =

View File

@@ -3,14 +3,18 @@
## [UNRELEASED]
- Links to code on GitHub now include column numbers as well as line numbers. [#2406](https://github.com/github/vscode-codeql/pull/2406)
- No longer highlight trailing commas for jump to definition. [#2615](https://github.com/github/vscode-codeql/pull/2615)
## 1.8.8 - 17 July 2023
- Remove support for CodeQL CLI versions older than 2.9.4. [#2610](https://github.com/github/vscode-codeql/pull/2610)
- Implement syntax highlighting for the `additional` and `default` keywords. [#2609](https://github.com/github/vscode-codeql/pull/2609)
## 1.8.7 - 29 June 2023
- Show a run button on the file tab for query files, that will start a local query. This button will only show when a local database is selected in the extension. [#2544](https://github.com/github/vscode-codeql/pull/2544)
- Add `CodeQL: Quick Evaluation Count` command to generate the count summary statistics of the results set
without spending the time to compute locations and strings.
- Add a `CodeQL: Quick Evaluation Count` command to generate the count summary statistics of the results set
without spending the time to compute locations and strings. [#2475](https://github.com/github/vscode-codeql/pull/2475)
## 1.8.6 - 14 June 2023

View File

@@ -30,5 +30,5 @@
"end": "^\\s*//\\s*#?endregion\\b"
}
},
"wordPattern": "(-?\\d*\\.\\d\\w*)|([^\\~\\!\\@\\#\\%\\^\\&\\*\\(\\)\\-\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\'\\\"\\.\\<\\>\\/\\?\\s]+)"
"wordPattern": "(-?\\d*\\.\\d\\w*)|([^\\~\\!\\@\\#\\%\\^\\&\\*\\(\\)\\-\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\'\\\"\\.\\<\\>\\/\\?\\s\\,]+)"
}

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@
"description": "CodeQL for Visual Studio Code",
"author": "GitHub",
"private": true,
"version": "1.8.8",
"version": "1.8.9",
"publisher": "GitHub",
"license": "MIT",
"icon": "media/VS-marketplace-CodeQL-icon.png",
@@ -1733,8 +1733,8 @@
"lint": "eslint . --ext .js,.ts,.tsx --max-warnings=0",
"lint:markdown": "markdownlint-cli2 \"../../**/*.{md,mdx}\" \"!**/node_modules/**\" \"!**/.vscode-test/**\" \"!**/build/cli/v*/**\"",
"format-staged": "lint-staged",
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"lint:scenarios": "ts-node scripts/lint-scenarios.ts",
"check-types": "find . -type f -name \"tsconfig.json\" -not -path \"./node_modules/*\" | sed -r 's|/[^/]+$||' | sort | uniq | xargs -I {} sh -c \"echo Checking types in {} && cd {} && npx tsc --noEmit\"",
"postinstall": "patch-package",
@@ -1786,17 +1786,22 @@
"devDependencies": {
"@babel/core": "^7.18.13",
"@babel/plugin-transform-modules-commonjs": "^7.18.6",
"@babel/preset-env": "^7.21.4",
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.21.4",
"@faker-js/faker": "^8.0.2",
"@github/markdownlint-github": "^0.3.0",
"@octokit/plugin-throttling": "^5.0.1",
"@storybook/addon-actions": "^6.5.17-alpha.0",
"@storybook/addon-essentials": "^6.5.17-alpha.0",
"@storybook/addon-interactions": "^6.5.17-alpha.0",
"@storybook/addon-links": "^6.5.17-alpha.0",
"@storybook/builder-webpack5": "^6.5.17-alpha.0",
"@storybook/manager-webpack5": "^6.5.17-alpha.0",
"@storybook/react": "^6.5.17-alpha.0",
"@storybook/testing-library": "^0.0.13",
"@storybook/addon-actions": "^7.1.0",
"@storybook/addon-essentials": "^7.1.0",
"@storybook/addon-interactions": "^7.1.0",
"@storybook/addon-links": "^7.1.0",
"@storybook/components": "^7.1.0",
"@storybook/csf": "^0.1.1",
"@storybook/manager-api": "^7.1.0",
"@storybook/react": "^7.1.0",
"@storybook/react-webpack5": "^7.1.0",
"@storybook/theming": "^7.1.0",
"@testing-library/dom": "^9.3.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
@@ -1837,6 +1842,7 @@
"@vscode/vsce": "^2.19.0",
"ansi-colors": "^4.1.1",
"applicationinsights": "^2.3.5",
"cosmiconfig": "^7.1.0",
"cross-env": "^7.0.3",
"css-loader": "~6.8.1",
"del": "^6.0.0",
@@ -1868,6 +1874,7 @@
"npm-run-all": "^4.1.5",
"patch-package": "^7.0.0",
"prettier": "^3.0.0",
"storybook": "^7.1.0",
"tar-stream": "^3.0.0",
"through2": "^4.0.2",
"ts-jest": "^29.0.1",

View File

@@ -1806,6 +1806,11 @@ export class CliVersionConstraint {
"2.10.0",
);
/**
* CLI version where the `resolve extensions` subcommand exists.
*/
public static CLI_VERSION_WITH_RESOLVE_EXTENSIONS = new SemVer("2.10.2");
/**
* CLI version where the `--evaluator-log` and related options to the query server were introduced,
* on a per-query server basis.
@@ -1882,6 +1887,12 @@ export class CliVersionConstraint {
);
}
async supportsResolveExtensions() {
return this.isVersionAtLeast(
CliVersionConstraint.CLI_VERSION_WITH_RESOLVE_EXTENSIONS,
);
}
async supportsStructuredEvalLog() {
return this.isVersionAtLeast(
CliVersionConstraint.CLI_VERSION_WITH_STRUCTURED_EVAL_LOG,

View File

@@ -78,6 +78,16 @@ export class DataExtensionsEditorModule {
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,

View File

@@ -5,7 +5,6 @@ import {
ViewColumn,
window,
} from "vscode";
import { join } from "path";
import { RequestError } from "@octokit/request-error";
import {
AbstractWebview,
@@ -21,8 +20,6 @@ import {
showAndLogExceptionWithTelemetry,
showAndLogErrorMessage,
} from "../common/logging";
import { outputFile, readFile } from "fs-extra";
import { load as loadYaml } from "js-yaml";
import { DatabaseItem, DatabaseManager } from "../databases/local-databases";
import { CodeQLCliServer } from "../codeql-cli/cli";
import { asError, assertNever, getErrorMessage } from "../common/helpers-pure";
@@ -34,11 +31,6 @@ import { showResolvableLocation } from "../databases/local-databases/locations";
import { decodeBqrsToExternalApiUsages } from "./bqrs";
import { redactableError } from "../common/errors";
import { readQueryResults, runQuery } from "./external-api-usage-query";
import {
createDataExtensionYamlsForApplicationMode,
createDataExtensionYamlsForFrameworkMode,
loadDataExtensionYaml,
} from "./yaml";
import { ExternalApiUsage } from "./external-api-usage";
import { ModeledMethod } from "./modeled-method";
import { ExtensionPack } from "./shared/extension-pack";
@@ -49,8 +41,9 @@ import {
} from "./auto-model";
import { enableFrameworkMode, showLlmGeneration } from "../config";
import { getAutoModelUsages } from "./auto-model-usages-query";
import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
import { Mode } from "./shared/mode";
import { loadModeledMethods, saveModeledMethods } from "./modeled-method-fs";
import { join } from "path";
export class DataExtensionsEditorView extends AbstractWebview<
ToDataExtensionsEditorMessage,
@@ -123,9 +116,14 @@ export class DataExtensionsEditorView extends AbstractWebview<
break;
case "saveModeledMethods":
await this.saveModeledMethods(
await saveModeledMethods(
this.extensionPack,
this.databaseItem.name,
this.databaseItem.language,
msg.externalApiUsages,
msg.modeledMethods,
this.mode,
this.app.logger,
);
await Promise.all([this.setViewState(), this.loadExternalApiUsages()]);
@@ -194,79 +192,16 @@ export class DataExtensionsEditorView extends AbstractWebview<
}
}
protected async saveModeledMethods(
externalApiUsages: ExternalApiUsage[],
modeledMethods: Record<string, ModeledMethod>,
): Promise<void> {
let yamls: Record<string, string>;
switch (this.mode) {
case Mode.Application:
yamls = createDataExtensionYamlsForApplicationMode(
this.databaseItem.language,
externalApiUsages,
modeledMethods,
);
break;
case Mode.Framework:
yamls = createDataExtensionYamlsForFrameworkMode(
this.databaseItem.name,
this.databaseItem.language,
externalApiUsages,
modeledMethods,
);
break;
default:
assertNever(this.mode);
}
for (const [filename, yaml] of Object.entries(yamls)) {
await outputFile(join(this.extensionPack.path, filename), yaml);
}
void this.app.logger.log(`Saved data extension YAML`);
}
protected async loadExistingModeledMethods(): Promise<void> {
try {
const extensions = await this.cliServer.resolveExtensions(
this.extensionPack.path,
getOnDiskWorkspaceFolders(),
const modeledMethods = await loadModeledMethods(
this.extensionPack,
this.cliServer,
this.app.logger,
);
const modelFiles = new Set<string>();
if (this.extensionPack.path in extensions.data) {
for (const extension of extensions.data[this.extensionPack.path]) {
modelFiles.add(extension.file);
}
}
const existingModeledMethods: Record<string, ModeledMethod> = {};
for (const modelFile of modelFiles) {
const yaml = await readFile(modelFile, "utf8");
const data = loadYaml(yaml, {
filename: modelFile,
});
const modeledMethods = loadDataExtensionYaml(data);
if (!modeledMethods) {
void showAndLogErrorMessage(
this.app.logger,
`Failed to parse data extension YAML ${modelFile}.`,
);
continue;
}
for (const [key, value] of Object.entries(modeledMethods)) {
existingModeledMethods[key] = value;
}
}
await this.postMessage({
t: "loadModeledMethods",
modeledMethods: existingModeledMethods,
modeledMethods,
});
} catch (e: unknown) {
void showAndLogErrorMessage(

View File

@@ -0,0 +1,93 @@
import { outputFile, readFile } from "fs-extra";
import { ExternalApiUsage } from "./external-api-usage";
import { ModeledMethod } from "./modeled-method";
import { Mode } from "./shared/mode";
import { createDataExtensionYamls, loadDataExtensionYaml } from "./yaml";
import { join } from "path";
import { ExtensionPack } from "./shared/extension-pack";
import {
Logger,
NotificationLogger,
showAndLogErrorMessage,
} from "../common/logging";
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";
export async function saveModeledMethods(
extensionPack: ExtensionPack,
databaseName: string,
language: string,
externalApiUsages: ExternalApiUsage[],
modeledMethods: Record<string, ModeledMethod>,
mode: Mode,
logger: Logger,
): Promise<void> {
const yamls = createDataExtensionYamls(
databaseName,
language,
externalApiUsages,
modeledMethods,
mode,
);
for (const [filename, yaml] of Object.entries(yamls)) {
await outputFile(join(extensionPack.path, filename), yaml);
}
void logger.log(`Saved data extension YAML`);
}
export async function loadModeledMethods(
extensionPack: ExtensionPack,
cliServer: CodeQLCliServer,
logger: NotificationLogger,
): Promise<Record<string, ModeledMethod>> {
const modelFiles = await listModelFiles(extensionPack.path, cliServer);
const existingModeledMethods: Record<string, ModeledMethod> = {};
for (const modelFile of modelFiles) {
const yaml = await readFile(modelFile, "utf8");
const data = loadYaml(yaml, {
filename: modelFile,
});
const modeledMethods = loadDataExtensionYaml(data);
if (!modeledMethods) {
void showAndLogErrorMessage(
logger,
`Failed to parse data extension YAML ${modelFile}.`,
);
continue;
}
for (const [key, value] of Object.entries(modeledMethods)) {
existingModeledMethods[key] = value;
}
}
return existingModeledMethods;
}
export async function listModelFiles(
extensionPackPath: string,
cliServer: CodeQLCliServer,
): Promise<Set<string>> {
const result = await cliServer.resolveExtensions(
extensionPackPath,
getOnDiskWorkspaceFolders(),
);
const modelFiles = new Set<string>();
for (const [path, extensions] of Object.entries(result.data)) {
if (pathsEqual(path, extensionPackPath)) {
for (const extension of extensions) {
modelFiles.add(extension.file);
}
}
}
return modelFiles;
}

View File

@@ -9,6 +9,8 @@ import {
import * as dataSchemaJson from "./data-schema.json";
import { sanitizeExtensionPackName } from "./extension-pack-name";
import { Mode } from "./shared/mode";
import { assertNever } from "../common/helpers-pure";
const ajv = new Ajv({ allErrors: true });
const dataSchemaValidate = ajv.compile(dataSchemaJson);
@@ -66,6 +68,32 @@ export function createDataExtensionYaml(
${extensions.join("\n")}`;
}
export function createDataExtensionYamls(
databaseName: string,
language: string,
externalApiUsages: ExternalApiUsage[],
modeledMethods: Record<string, ModeledMethod>,
mode: Mode,
) {
switch (mode) {
case Mode.Application:
return createDataExtensionYamlsForApplicationMode(
language,
externalApiUsages,
modeledMethods,
);
case Mode.Framework:
return createDataExtensionYamlsForFrameworkMode(
databaseName,
language,
externalApiUsages,
modeledMethods,
);
default:
assertNever(mode);
}
}
export function createDataExtensionYamlsForApplicationMode(
language: string,
externalApiUsages: ExternalApiUsage[],

View File

@@ -1,6 +1,6 @@
import * as React from "react";
import { ComponentStory, ComponentMeta } from "@storybook/react";
import { Meta, StoryFn } from "@storybook/react";
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react";
import { VariantAnalysisContainer } from "../../view/variant-analysis/VariantAnalysisContainer";
@@ -16,9 +16,9 @@ export default {
</VariantAnalysisContainer>
),
],
} as ComponentMeta<typeof Alert>;
} as Meta<typeof Alert>;
const Template: ComponentStory<typeof Alert> = (args) => <Alert {...args} />;
const Template: StoryFn<typeof Alert> = (args) => <Alert {...args} />;
export const Warning = Template.bind({});
Warning.args = {

View File

@@ -1,6 +1,6 @@
import * as React from "react";
import { ComponentStory, ComponentMeta } from "@storybook/react";
import { Meta, StoryFn } from "@storybook/react";
import { CodePaths } from "../../view/common";
import type { CodeFlow } from "../../variant-analysis/shared/analysis-result";
@@ -9,11 +9,9 @@ export default {
title: "Code Paths",
component: CodePaths,
decorators: [(Story) => <Story />],
} as ComponentMeta<typeof CodePaths>;
} as Meta<typeof CodePaths>;
const Template: ComponentStory<typeof CodePaths> = (args) => (
<CodePaths {...args} />
);
const Template: StoryFn<typeof CodePaths> = (args) => <CodePaths {...args} />;
export const PowerShell = Template.bind({});

View File

@@ -1,15 +1,15 @@
import * as React from "react";
import { ComponentStory, ComponentMeta } from "@storybook/react";
import { Meta, StoryFn } from "@storybook/react";
import { FileCodeSnippet } from "../../view/common";
export default {
title: "File Code Snippet",
component: FileCodeSnippet,
} as ComponentMeta<typeof FileCodeSnippet>;
} as Meta<typeof FileCodeSnippet>;
const Template: ComponentStory<typeof FileCodeSnippet> = (args) => (
const Template: StoryFn<typeof FileCodeSnippet> = (args) => (
<FileCodeSnippet {...args} />
);

View File

@@ -1,15 +1,15 @@
import * as React from "react";
import { ComponentStory, ComponentMeta } from "@storybook/react";
import { Meta, StoryFn } from "@storybook/react";
import { LastUpdated as LastUpdatedComponent } from "../../view/common/LastUpdated";
export default {
title: "Last Updated",
component: LastUpdatedComponent,
} as ComponentMeta<typeof LastUpdatedComponent>;
} as Meta<typeof LastUpdatedComponent>;
const Template: ComponentStory<typeof LastUpdatedComponent> = (args) => (
const Template: StoryFn<typeof LastUpdatedComponent> = (args) => (
<LastUpdatedComponent {...args} />
);

View File

@@ -1,15 +1,15 @@
import * as React from "react";
import { ComponentStory, ComponentMeta } from "@storybook/react";
import { Meta, StoryFn } from "@storybook/react";
import StarCountComponent from "../../view/common/StarCount";
export default {
title: "Star Count",
component: StarCountComponent,
} as ComponentMeta<typeof StarCountComponent>;
} as Meta<typeof StarCountComponent>;
const Template: ComponentStory<typeof StarCountComponent> = (args) => (
const Template: StoryFn<typeof StarCountComponent> = (args) => (
<StarCountComponent {...args} />
);

View File

@@ -1,6 +1,6 @@
import * as React from "react";
import { ComponentStory, ComponentMeta } from "@storybook/react";
import { Meta, StoryFn } from "@storybook/react";
import TextButtonComponent from "../../view/common/TextButton";
@@ -15,9 +15,9 @@ export default {
},
},
},
} as ComponentMeta<typeof TextButtonComponent>;
} as Meta<typeof TextButtonComponent>;
const Template: ComponentStory<typeof TextButtonComponent> = (args) => (
const Template: StoryFn<typeof TextButtonComponent> = (args) => (
<TextButtonComponent {...args} />
);

View File

@@ -1,8 +1,8 @@
import * as React from "react";
import { ComponentStory, ComponentMeta } from "@storybook/react";
import { Meta, StoryFn } from "@storybook/react";
import { CodePaths, Codicon as CodiconComponent } from "../../../view/common";
import { Codicon as CodiconComponent } from "../../../view/common";
// To regenerate the icons, use the following command from the `extensions/ql-vscode` directory:
// jq -R '[inputs | [splits(", *")] as $row | $row[0]]' < node_modules/@vscode/codicons/dist/codicon.csv > src/stories/common/icon/vscode-icons.json
@@ -17,9 +17,9 @@ export default {
options: icons,
},
},
} as ComponentMeta<typeof CodePaths>;
} as Meta<typeof CodiconComponent>;
const Template: ComponentStory<typeof CodiconComponent> = (args) => (
const Template: StoryFn<typeof CodiconComponent> = (args) => (
<CodiconComponent {...args} />
);

View File

@@ -1,6 +1,6 @@
import * as React from "react";
import { ComponentStory, ComponentMeta } from "@storybook/react";
import { Meta, StoryFn } from "@storybook/react";
import {
CodePaths,
@@ -10,9 +10,9 @@ import {
export default {
title: "Icon/Error Icon",
component: ErrorIconComponent,
} as ComponentMeta<typeof CodePaths>;
} as Meta<typeof CodePaths>;
const Template: ComponentStory<typeof ErrorIconComponent> = (args) => (
const Template: StoryFn<typeof ErrorIconComponent> = (args) => (
<ErrorIconComponent {...args} />
);

View File

@@ -1,6 +1,6 @@
import * as React from "react";
import { ComponentStory, ComponentMeta } from "@storybook/react";
import { Meta, StoryFn } from "@storybook/react";
import {
CodePaths,
@@ -10,9 +10,9 @@ import {
export default {
title: "Icon/Success Icon",
component: SuccessIconComponent,
} as ComponentMeta<typeof CodePaths>;
} as Meta<typeof CodePaths>;
const Template: ComponentStory<typeof SuccessIconComponent> = (args) => (
const Template: StoryFn<typeof SuccessIconComponent> = (args) => (
<SuccessIconComponent {...args} />
);

View File

@@ -1,6 +1,6 @@
import * as React from "react";
import { ComponentStory, ComponentMeta } from "@storybook/react";
import { Meta, StoryFn } from "@storybook/react";
import {
CodePaths,
@@ -10,9 +10,9 @@ import {
export default {
title: "Icon/Warning Icon",
component: WarningIconComponent,
} as ComponentMeta<typeof CodePaths>;
} as Meta<typeof CodePaths>;
const Template: ComponentStory<typeof WarningIconComponent> = (args) => (
const Template: StoryFn<typeof WarningIconComponent> = (args) => (
<WarningIconComponent {...args} />
);

View File

@@ -1,6 +1,6 @@
import * as React from "react";
import { ComponentMeta, ComponentStory } from "@storybook/react";
import { Meta, StoryFn } from "@storybook/react";
import { Mode } from "../../data-extensions-editor/shared/mode";
import { DataExtensionsEditor as DataExtensionsEditorComponent } from "../../view/data-extensions-editor/DataExtensionsEditor";
@@ -9,11 +9,11 @@ import { CallClassification } from "../../data-extensions-editor/external-api-us
export default {
title: "Data Extensions Editor/Data Extensions Editor",
component: DataExtensionsEditorComponent,
} as ComponentMeta<typeof DataExtensionsEditorComponent>;
} as Meta<typeof DataExtensionsEditorComponent>;
const Template: ComponentStory<typeof DataExtensionsEditorComponent> = (
args,
) => <DataExtensionsEditorComponent {...args} />;
const Template: StoryFn<typeof DataExtensionsEditorComponent> = (args) => (
<DataExtensionsEditorComponent {...args} />
);
export const DataExtensionsEditor = Template.bind({});
DataExtensionsEditor.args = {

View File

@@ -1,6 +1,6 @@
import * as React from "react";
import { ComponentMeta, ComponentStory } from "@storybook/react";
import { Meta, StoryFn } from "@storybook/react";
import { MethodRow as MethodRowComponent } from "../../view/data-extensions-editor/MethodRow";
import { CallClassification } from "../../data-extensions-editor/external-api-usage";
@@ -8,9 +8,9 @@ import { CallClassification } from "../../data-extensions-editor/external-api-us
export default {
title: "Data Extensions Editor/Method Row",
component: MethodRowComponent,
} as ComponentMeta<typeof MethodRowComponent>;
} as Meta<typeof MethodRowComponent>;
const Template: ComponentStory<typeof MethodRowComponent> = (args) => (
const Template: StoryFn<typeof MethodRowComponent> = (args) => (
<MethodRowComponent {...args} />
);

View File

@@ -1,15 +1,15 @@
import * as React from "react";
import { ComponentMeta, ComponentStory } from "@storybook/react";
import { Meta, StoryFn } from "@storybook/react";
import { DataFlowPaths as DataFlowPathsComponent } from "../../view/data-flow-paths/DataFlowPaths";
import { createMockDataFlowPaths } from "../../../test/factories/variant-analysis/shared/data-flow-paths";
export default {
title: "Data Flow Paths/Data Flow Paths",
component: DataFlowPathsComponent,
} as ComponentMeta<typeof DataFlowPathsComponent>;
} as Meta<typeof DataFlowPathsComponent>;
const Template: ComponentStory<typeof DataFlowPathsComponent> = (args) => (
const Template: StoryFn<typeof DataFlowPathsComponent> = (args) => (
<DataFlowPathsComponent {...args} />
);

View File

@@ -1,6 +1,6 @@
import * as React from "react";
import { ComponentStory, ComponentMeta } from "@storybook/react";
import { Meta, StoryFn } from "@storybook/react";
import AnalysisAlertResult from "../../view/variant-analysis/AnalysisAlertResult";
import type { AnalysisAlert } from "../../variant-analysis/shared/analysis-result";
@@ -8,9 +8,9 @@ import type { AnalysisAlert } from "../../variant-analysis/shared/analysis-resul
export default {
title: "Variant Analysis/Analysis Alert Result",
component: AnalysisAlertResult,
} as ComponentMeta<typeof AnalysisAlertResult>;
} as Meta<typeof AnalysisAlertResult>;
const Template: ComponentStory<typeof AnalysisAlertResult> = (args) => (
const Template: StoryFn<typeof AnalysisAlertResult> = (args) => (
<AnalysisAlertResult {...args} />
);

View File

@@ -1,15 +1,15 @@
import * as React from "react";
import { ComponentMeta, ComponentStory } from "@storybook/react";
import { Meta, StoryFn } from "@storybook/react";
import { VariantAnalysisFailureReason } from "../../variant-analysis/shared/variant-analysis";
import { FailureReasonAlert } from "../../view/variant-analysis/FailureReasonAlert";
export default {
title: "Variant Analysis/Failure reason alert",
component: FailureReasonAlert,
} as ComponentMeta<typeof FailureReasonAlert>;
} as Meta<typeof FailureReasonAlert>;
const Template: ComponentStory<typeof FailureReasonAlert> = (args) => (
const Template: StoryFn<typeof FailureReasonAlert> = (args) => (
<FailureReasonAlert {...args} />
);

View File

@@ -1,6 +1,6 @@
import * as React from "react";
import { ComponentMeta, ComponentStory } from "@storybook/react";
import { Meta, StoryFn } from "@storybook/react";
import { VariantAnalysisContainer } from "../../view/variant-analysis/VariantAnalysisContainer";
import { QueryDetails as QueryDetailsComponent } from "../../view/variant-analysis/QueryDetails";
@@ -29,9 +29,9 @@ export default {
},
},
},
} as ComponentMeta<typeof QueryDetailsComponent>;
} as Meta<typeof QueryDetailsComponent>;
const Template: ComponentStory<typeof QueryDetailsComponent> = (args) => (
const Template: StoryFn<typeof QueryDetailsComponent> = (args) => (
<QueryDetailsComponent {...args} />
);

View File

@@ -1,6 +1,6 @@
import * as React from "react";
import { ComponentMeta, ComponentStory } from "@storybook/react";
import { Meta, StoryFn } from "@storybook/react";
import { VariantAnalysisContainer } from "../../view/variant-analysis/VariantAnalysisContainer";
import {
@@ -27,9 +27,9 @@ export default {
</VariantAnalysisContainer>
),
],
} as ComponentMeta<typeof RepoRow>;
} as Meta<typeof RepoRow>;
const Template: ComponentStory<typeof RepoRow> = (args: RepoRowProps) => (
const Template: StoryFn<typeof RepoRow> = (args: RepoRowProps) => (
<RepoRow {...args} />
);

View File

@@ -1,7 +1,7 @@
import * as React from "react";
import { useState } from "react";
import { ComponentMeta } from "@storybook/react";
import { Meta } from "@storybook/react";
import { RepositoriesFilter as RepositoriesFilterComponent } from "../../view/variant-analysis/RepositoriesFilter";
import { FilterKey } from "../../variant-analysis/shared/variant-analysis-filter-sort";
@@ -16,7 +16,7 @@ export default {
},
},
},
} as ComponentMeta<typeof RepositoriesFilterComponent>;
} as Meta<typeof RepositoriesFilterComponent>;
export const RepositoriesFilter = () => {
const [value, setValue] = useState(FilterKey.All);

View File

@@ -1,7 +1,7 @@
import * as React from "react";
import { useState } from "react";
import { ComponentMeta } from "@storybook/react";
import { Meta } from "@storybook/react";
import { RepositoriesSearch as RepositoriesSearchComponent } from "../../view/variant-analysis/RepositoriesSearch";
@@ -15,7 +15,7 @@ export default {
},
},
},
} as ComponentMeta<typeof RepositoriesSearchComponent>;
} as Meta<typeof RepositoriesSearchComponent>;
export const RepositoriesSearch = () => {
const [value, setValue] = useState("");

View File

@@ -1,7 +1,7 @@
import * as React from "react";
import { useState } from "react";
import { ComponentMeta } from "@storybook/react";
import { Meta } from "@storybook/react";
import { RepositoriesSearchSortRow as RepositoriesSearchSortRowComponent } from "../../view/variant-analysis/RepositoriesSearchSortRow";
import { defaultFilterSortState } from "../../variant-analysis/shared/variant-analysis-filter-sort";
@@ -16,7 +16,7 @@ export default {
},
},
},
} as ComponentMeta<typeof RepositoriesSearchSortRowComponent>;
} as Meta<typeof RepositoriesSearchSortRowComponent>;
export const RepositoriesSearchSortRow = () => {
const [value, setValue] = useState(defaultFilterSortState);

View File

@@ -1,7 +1,7 @@
import * as React from "react";
import { useState } from "react";
import { ComponentMeta } from "@storybook/react";
import { Meta } from "@storybook/react";
import { RepositoriesSort as RepositoriesSortComponent } from "../../view/variant-analysis/RepositoriesSort";
import { SortKey } from "../../variant-analysis/shared/variant-analysis-filter-sort";
@@ -16,7 +16,7 @@ export default {
},
},
},
} as ComponentMeta<typeof RepositoriesSortComponent>;
} as Meta<typeof RepositoriesSortComponent>;
export const RepositoriesSort = () => {
const [value, setValue] = useState(SortKey.Alphabetically);

View File

@@ -1,6 +1,6 @@
import * as React from "react";
import { ComponentMeta, ComponentStory } from "@storybook/react";
import { Meta, StoryFn } from "@storybook/react";
import { VariantAnalysis as VariantAnalysisComponent } from "../../view/variant-analysis/VariantAnalysis";
import {
@@ -18,9 +18,9 @@ import { createMockRepositoryWithMetadata } from "../../../test/factories/varian
export default {
title: "Variant Analysis/Variant Analysis",
component: VariantAnalysisComponent,
} as ComponentMeta<typeof VariantAnalysisComponent>;
} as Meta<typeof VariantAnalysisComponent>;
const Template: ComponentStory<typeof VariantAnalysisComponent> = (args) => (
const Template: StoryFn<typeof VariantAnalysisComponent> = (args) => (
<VariantAnalysisComponent {...args} />
);

View File

@@ -1,6 +1,6 @@
import * as React from "react";
import { ComponentMeta, ComponentStory } from "@storybook/react";
import { Meta, StoryFn } from "@storybook/react";
import { VariantAnalysisContainer } from "../../view/variant-analysis/VariantAnalysisContainer";
import { VariantAnalysisStatus } from "../../variant-analysis/shared/variant-analysis";
@@ -36,9 +36,9 @@ export default {
},
},
},
} as ComponentMeta<typeof VariantAnalysisActions>;
} as Meta<typeof VariantAnalysisActions>;
const Template: ComponentStory<typeof VariantAnalysisActions> = (args) => (
const Template: StoryFn<typeof VariantAnalysisActions> = (args) => (
<VariantAnalysisActions {...args} />
);

View File

@@ -1,6 +1,6 @@
import * as React from "react";
import { ComponentMeta, ComponentStory } from "@storybook/react";
import { Meta, StoryFn } from "@storybook/react";
import { faker } from "@faker-js/faker";
@@ -28,11 +28,11 @@ export default {
</VariantAnalysisContainer>
),
],
} as ComponentMeta<typeof VariantAnalysisAnalyzedRepos>;
} as Meta<typeof VariantAnalysisAnalyzedRepos>;
const Template: ComponentStory<typeof VariantAnalysisAnalyzedRepos> = (
args,
) => <VariantAnalysisAnalyzedRepos {...args} />;
const Template: StoryFn<typeof VariantAnalysisAnalyzedRepos> = (args) => (
<VariantAnalysisAnalyzedRepos {...args} />
);
const interpretedResultsForRepo = (
nwo: string,

View File

@@ -1,6 +1,6 @@
import * as React from "react";
import { ComponentMeta, ComponentStory } from "@storybook/react";
import { Meta, StoryFn } from "@storybook/react";
import { VariantAnalysisContainer } from "../../view/variant-analysis/VariantAnalysisContainer";
import { VariantAnalysisHeader } from "../../view/variant-analysis/VariantAnalysisHeader";
@@ -59,9 +59,9 @@ export default {
},
},
},
} as ComponentMeta<typeof VariantAnalysisHeader>;
} as Meta<typeof VariantAnalysisHeader>;
const Template: ComponentStory<typeof VariantAnalysisHeader> = (args) => (
const Template: StoryFn<typeof VariantAnalysisHeader> = (args) => (
<VariantAnalysisHeader {...args} />
);

View File

@@ -1,6 +1,6 @@
import * as React from "react";
import { ComponentMeta, ComponentStory } from "@storybook/react";
import { Meta, StoryFn } from "@storybook/react";
import { VariantAnalysisContainer } from "../../view/variant-analysis/VariantAnalysisContainer";
import { VariantAnalysisLoading as VariantAnalysisLoadingComponent } from "../../view/variant-analysis/VariantAnalysisLoading";
@@ -16,9 +16,9 @@ export default {
),
],
argTypes: {},
} as ComponentMeta<typeof VariantAnalysisLoadingComponent>;
} as Meta<typeof VariantAnalysisLoadingComponent>;
const Template: ComponentStory<typeof VariantAnalysisLoadingComponent> = () => (
const Template: StoryFn<typeof VariantAnalysisLoadingComponent> = () => (
<VariantAnalysisLoadingComponent />
);

View File

@@ -1,7 +1,7 @@
import * as React from "react";
import { useState } from "react";
import { ComponentMeta, ComponentStory } from "@storybook/react";
import { Meta, StoryFn } from "@storybook/react";
import { VariantAnalysisContainer } from "../../view/variant-analysis/VariantAnalysisContainer";
import { VariantAnalysisOutcomePanels } from "../../view/variant-analysis/VariantAnalysisOutcomePanels";
@@ -27,11 +27,9 @@ export default {
</VariantAnalysisContainer>
),
],
} as ComponentMeta<typeof VariantAnalysisOutcomePanels>;
} as Meta<typeof VariantAnalysisOutcomePanels>;
const Template: ComponentStory<typeof VariantAnalysisOutcomePanels> = (
args,
) => {
const Template: StoryFn<typeof VariantAnalysisOutcomePanels> = (args) => {
const [filterSortState, setFilterSortState] =
useState<RepositoriesFilterSortState>(defaultFilterSortState);

View File

@@ -1,6 +1,6 @@
import * as React from "react";
import { ComponentMeta, ComponentStory } from "@storybook/react";
import { Meta, StoryFn } from "@storybook/react";
import { VariantAnalysisContainer } from "../../view/variant-analysis/VariantAnalysisContainer";
import { VariantAnalysisSkippedRepositoriesTab } from "../../view/variant-analysis/VariantAnalysisSkippedRepositoriesTab";
@@ -16,9 +16,9 @@ export default {
</VariantAnalysisContainer>
),
],
} as ComponentMeta<typeof VariantAnalysisSkippedRepositoriesTab>;
} as Meta<typeof VariantAnalysisSkippedRepositoriesTab>;
const Template: ComponentStory<typeof VariantAnalysisSkippedRepositoriesTab> = (
const Template: StoryFn<typeof VariantAnalysisSkippedRepositoriesTab> = (
args,
) => <VariantAnalysisSkippedRepositoriesTab {...args} />;

View File

@@ -1,6 +1,6 @@
import * as React from "react";
import { ComponentStory, ComponentMeta } from "@storybook/react";
import { Meta, StoryFn } from "@storybook/react";
import { VariantAnalysisContainer } from "../../view/variant-analysis/VariantAnalysisContainer";
import { VariantAnalysisStats } from "../../view/variant-analysis/VariantAnalysisStats";
@@ -24,9 +24,9 @@ export default {
},
},
},
} as ComponentMeta<typeof VariantAnalysisStats>;
} as Meta<typeof VariantAnalysisStats>;
const Template: ComponentStory<typeof VariantAnalysisStats> = (args) => (
const Template: StoryFn<typeof VariantAnalysisStats> = (args) => (
<VariantAnalysisStats {...args} />
);

View File

@@ -17,7 +17,6 @@ import { DataExtensionEditorViewState } from "../../data-extensions-editor/share
import { ModeledMethodsList } from "./ModeledMethodsList";
import { percentFormatter } from "./formatters";
import { Mode } from "../../data-extensions-editor/shared/mode";
import { groupMethods } from "../../data-extensions-editor/shared/sorting";
const LoadingContainer = styled.div`
text-align: center;
@@ -75,7 +74,9 @@ export function DataExtensionsEditor({
const [externalApiUsages, setExternalApiUsages] = useState<
ExternalApiUsage[]
>(initialExternalApiUsages);
const [unsavedModels, setUnsavedModels] = useState<Set<string>>(new Set());
const [modifiedSignatures, setModifiedSignatures] = useState<Set<string>>(
new Set(),
);
const [modeledMethods, setModeledMethods] = useState<
Record<string, ModeledMethod>
@@ -119,15 +120,11 @@ export function DataExtensionsEditor({
),
};
});
setUnsavedModels(
(oldUnsavedModels) =>
setModifiedSignatures(
(oldModifiedSignatures) =>
new Set([
...oldUnsavedModels,
...modelsAffectedByNewModeledMethods(
msg.modeledMethods,
externalApiUsages,
viewState?.mode ?? Mode.Application,
),
...oldModifiedSignatures,
...Object.keys(msg.modeledMethods),
]),
);
break;
@@ -145,7 +142,7 @@ export function DataExtensionsEditor({
return () => {
window.removeEventListener("message", listener);
};
}, [externalApiUsages, viewState?.mode]);
}, []);
const modeledPercentage = useMemo(
() => calculateModeledPercentage(externalApiUsages),
@@ -160,8 +157,9 @@ export function DataExtensionsEditor({
...oldModeledMethods,
[method.signature]: model,
}));
setUnsavedModels(
(oldUnsavedModels) => new Set([...oldUnsavedModels, modelName]),
setModifiedSignatures(
(oldModifiedSignatures) =>
new Set([...oldModifiedSignatures, method.signature]),
);
},
[],
@@ -179,12 +177,11 @@ export function DataExtensionsEditor({
externalApiUsages,
modeledMethods,
});
setUnsavedModels(new Set());
setModifiedSignatures(new Set());
}, [externalApiUsages, modeledMethods]);
const onSaveModelClick = useCallback(
(
modelName: string,
externalApiUsages: ExternalApiUsage[],
modeledMethods: Record<string, ModeledMethod>,
) => {
@@ -193,10 +190,12 @@ export function DataExtensionsEditor({
externalApiUsages,
modeledMethods,
});
setUnsavedModels((oldUnsavedModels) => {
const newUnsavedModels = new Set(oldUnsavedModels);
newUnsavedModels.delete(modelName);
return newUnsavedModels;
setModifiedSignatures((oldModifiedSignatures) => {
const newModifiedSignatures = new Set([...oldModifiedSignatures]);
for (const externalApiUsage of externalApiUsages) {
newModifiedSignatures.delete(externalApiUsage.signature);
}
return newModifiedSignatures;
});
},
[],
@@ -317,8 +316,8 @@ export function DataExtensionsEditor({
</ButtonsContainer>
<ModeledMethodsList
externalApiUsages={externalApiUsages}
unsavedModels={unsavedModels}
modeledMethods={modeledMethods}
modifiedSignatures={modifiedSignatures}
viewState={viewState}
onChange={onChange}
onSaveModelClick={onSaveModelClick}
@@ -331,15 +330,3 @@ export function DataExtensionsEditor({
</DataExtensionsEditorContainer>
);
}
function modelsAffectedByNewModeledMethods(
modeledMethods: Record<string, ModeledMethod>,
externalApiUsages: ExternalApiUsage[],
mode: Mode,
): string[] {
const signatures = new Set(Object.keys(modeledMethods));
const affectedExternalApiUsages = externalApiUsages.filter(
(externalApiUsage) => signatures.has(externalApiUsage.signature),
);
return Object.keys(groupMethods(affectedExternalApiUsages, mode));
}

View File

@@ -71,15 +71,14 @@ type Props = {
libraryVersion?: string;
externalApiUsages: ExternalApiUsage[];
modeledMethods: Record<string, ModeledMethod>;
modifiedSignatures: Set<string>;
viewState: DataExtensionEditorViewState;
hasUnsavedChanges: boolean;
onChange: (
modelName: string,
externalApiUsage: ExternalApiUsage,
modeledMethod: ModeledMethod,
) => void;
onSaveModelClick: (
modelName: string,
externalApiUsages: ExternalApiUsage[],
modeledMethods: Record<string, ModeledMethod>,
) => void;
@@ -95,8 +94,8 @@ export const LibraryRow = ({
libraryVersion,
externalApiUsages,
modeledMethods,
modifiedSignatures,
viewState,
hasUnsavedChanges,
onChange,
onSaveModelClick,
onGenerateFromLlmClick,
@@ -137,11 +136,11 @@ export const LibraryRow = ({
const handleSave = useCallback(
async (e: React.MouseEvent) => {
onSaveModelClick(title, externalApiUsages, modeledMethods);
onSaveModelClick(externalApiUsages, modeledMethods);
e.stopPropagation();
e.preventDefault();
},
[title, externalApiUsages, modeledMethods, onSaveModelClick],
[externalApiUsages, modeledMethods, onSaveModelClick],
);
const onChangeWithModelName = useCallback(
@@ -151,6 +150,12 @@ export const LibraryRow = ({
[onChange, title],
);
const hasUnsavedChanges = useMemo(() => {
return externalApiUsages.some((externalApiUsage) =>
modifiedSignatures.has(externalApiUsage.signature),
);
}, [externalApiUsages, modifiedSignatures]);
return (
<LibraryContainer>
<TitleContainer onClick={toggleExpanded} aria-expanded={isExpanded}>
@@ -195,6 +200,7 @@ export const LibraryRow = ({
<ModeledMethodDataGrid
externalApiUsages={externalApiUsages}
modeledMethods={modeledMethods}
modifiedSignatures={modifiedSignatures}
mode={viewState.mode}
onChange={onChangeWithModelName}
/>

View File

@@ -1,5 +1,4 @@
import {
VSCodeCheckbox,
VSCodeDataGridCell,
VSCodeDataGridRow,
VSCodeLink,
@@ -20,6 +19,10 @@ import { extensiblePredicateDefinitions } from "../../data-extensions-editor/pre
import { Mode } from "../../data-extensions-editor/shared/mode";
import { Dropdown } from "../common/Dropdown";
import { MethodClassifications } from "./MethodClassifications";
import {
ModelingStatus,
ModelingStatusIndicator,
} from "./ModelingStatusIndicator";
const ApiOrMethodCell = styled(VSCodeDataGridCell)`
display: flex;
@@ -51,6 +54,7 @@ const modelTypeOptions: Array<{ value: ModeledMethodType; label: string }> = [
type Props = {
externalApiUsage: ExternalApiUsage;
modeledMethod: ModeledMethod | undefined;
methodIsUnsaved: boolean;
mode: Mode;
onChange: (
externalApiUsage: ExternalApiUsage,
@@ -59,11 +63,12 @@ type Props = {
};
export const MethodRow = (props: Props) => {
const { externalApiUsage, modeledMethod } = props;
const { externalApiUsage, modeledMethod, methodIsUnsaved } = props;
const methodCanBeModeled =
!externalApiUsage.supported ||
(modeledMethod && modeledMethod?.type !== "none");
(modeledMethod && modeledMethod?.type !== "none") ||
methodIsUnsaved;
if (methodCanBeModeled) {
return <ModelableMethodRow {...props} />;
@@ -73,7 +78,8 @@ export const MethodRow = (props: Props) => {
};
function ModelableMethodRow(props: Props) {
const { externalApiUsage, modeledMethod, mode, onChange } = props;
const { externalApiUsage, modeledMethod, methodIsUnsaved, mode, onChange } =
props;
const argumentsList = useMemo(() => {
if (externalApiUsage.methodParameters === "()") {
@@ -192,10 +198,12 @@ function ModelableMethodRow(props: Props) {
: undefined;
const showKindCell = predicate?.supportedKinds;
const modelingStatus = getModelingStatus(modeledMethod, methodIsUnsaved);
return (
<VSCodeDataGridRow>
<ApiOrMethodCell gridColumn={1}>
<VSCodeCheckbox />
<ModelingStatusIndicator status={modelingStatus} />
<ExternalApiUsageName {...props} />
{mode === Mode.Application && (
<UsagesButton onClick={jumpToUsage}>
@@ -251,7 +259,7 @@ function UnmodelableMethodRow(props: Props) {
return (
<VSCodeDataGridRow>
<ApiOrMethodCell gridColumn={1}>
<VSCodeCheckbox />
<ModelingStatusIndicator status="saved" />
<ExternalApiUsageName {...props} />
{mode === Mode.Application && (
<UsagesButton onClick={jumpToUsage}>
@@ -287,3 +295,17 @@ function sendJumpToUsageMessage(externalApiUsage: ExternalApiUsage) {
location: externalApiUsage.usages[0].url,
});
}
function getModelingStatus(
modeledMethod: ModeledMethod | undefined,
methodIsUnsaved: boolean,
): ModelingStatus {
if (modeledMethod) {
if (methodIsUnsaved) {
return "unsaved";
} else if (modeledMethod.type !== "none") {
return "saved";
}
}
return "unmodeled";
}

View File

@@ -14,6 +14,7 @@ import { sortMethods } from "../../data-extensions-editor/shared/sorting";
type Props = {
externalApiUsages: ExternalApiUsage[];
modeledMethods: Record<string, ModeledMethod>;
modifiedSignatures: Set<string>;
mode: Mode;
onChange: (
externalApiUsage: ExternalApiUsage,
@@ -24,6 +25,7 @@ type Props = {
export const ModeledMethodDataGrid = ({
externalApiUsages,
modeledMethods,
modifiedSignatures,
mode,
onChange,
}: Props) => {
@@ -56,6 +58,7 @@ export const ModeledMethodDataGrid = ({
key={externalApiUsage.signature}
externalApiUsage={externalApiUsage}
modeledMethod={modeledMethods[externalApiUsage.signature]}
methodIsUnsaved={modifiedSignatures.has(externalApiUsage.signature)}
mode={mode}
onChange={onChange}
/>

View File

@@ -12,8 +12,8 @@ import { DataExtensionEditorViewState } from "../../data-extensions-editor/share
type Props = {
externalApiUsages: ExternalApiUsage[];
unsavedModels: Set<string>;
modeledMethods: Record<string, ModeledMethod>;
modifiedSignatures: Set<string>;
viewState: DataExtensionEditorViewState;
onChange: (
modelName: string,
@@ -21,7 +21,6 @@ type Props = {
modeledMethod: ModeledMethod,
) => void;
onSaveModelClick: (
modelName: string,
externalApiUsages: ExternalApiUsage[],
modeledMethods: Record<string, ModeledMethod>,
) => void;
@@ -38,8 +37,8 @@ const libraryNameOverrides: Record<string, string> = {
export const ModeledMethodsList = ({
externalApiUsages,
unsavedModels,
modeledMethods,
modifiedSignatures,
viewState,
onChange,
onSaveModelClick,
@@ -79,8 +78,8 @@ export const ModeledMethodsList = ({
title={libraryNameOverrides[libraryName] ?? libraryName}
libraryVersion={libraryVersions[libraryName]}
externalApiUsages={grouped[libraryName]}
hasUnsavedChanges={unsavedModels.has(libraryName)}
modeledMethods={modeledMethods}
modifiedSignatures={modifiedSignatures}
viewState={viewState}
onChange={onChange}
onSaveModelClick={onSaveModelClick}

View File

@@ -0,0 +1,22 @@
import * as React from "react";
import { assertNever } from "../../common/helpers-pure";
import { Codicon } from "../common/icon/Codicon";
export type ModelingStatus = "unmodeled" | "unsaved" | "saved";
interface Props {
status: ModelingStatus;
}
export function ModelingStatusIndicator({ status }: Props) {
switch (status) {
case "unmodeled":
return <Codicon name="circle-large-outline" label="Method not modeled" />;
case "unsaved":
return <Codicon name="pass" label="Changes have not been saved" />;
case "saved":
return <Codicon name="pass-filled" label="Method modeled" />;
default:
assertNever(status);
}
}

View File

@@ -1,6 +1,6 @@
import * as React from "react";
import { renderLocation } from "./result-table-utils";
import { Location } from "./locations/Location";
import { CellValue } from "../../common/bqrs-cli-types";
interface Props {
@@ -16,14 +16,15 @@ export default function RawTableValue(props: Props): JSX.Element {
typeof rawValue === "number" ||
typeof rawValue === "boolean"
) {
return <span>{renderLocation(undefined, rawValue.toString())}</span>;
return <Location label={rawValue.toString()} />;
}
return renderLocation(
rawValue.url,
rawValue.label,
props.databaseUri,
undefined,
props.onSelected,
return (
<Location
loc={rawValue.url}
label={rawValue.label}
databaseUri={props.databaseUri}
onClick={props.onSelected}
/>
);
}

View File

@@ -1,11 +1,9 @@
import { basename } from "path";
import * as React from "react";
import * as Sarif from "sarif";
import * as Keys from "./result-keys";
import { chevronDown, chevronRight, info, listUnordered } from "./octicons";
import {
className,
renderLocation,
ResultTableProps,
selectableZebraStripe,
jumpToLocation,
@@ -18,15 +16,12 @@ import {
NavigationDirection,
SarifInterpretationData,
} from "../../common/interface-types";
import {
parseSarifPlainTextMessage,
parseSarifLocation,
isNoLocation,
} from "../../common/sarif-utils";
import { isWholeFileLoc, isLineColumnLoc } from "../../common/bqrs-utils";
import { parseSarifLocation, isNoLocation } from "../../common/sarif-utils";
import { ScrollIntoViewHelper } from "./scroll-into-view-helper";
import { sendTelemetry } from "../common/telemetry";
import { AlertTableHeader } from "./alert-table-header";
import { SarifMessageWithLocations } from "./locations/SarifMessageWithLocations";
import { SarifLocation } from "./locations/SarifLocation";
export type AlertTableProps = ResultTableProps & {
resultSet: InterpretedResultSet<SarifInterpretationData>;
@@ -100,41 +95,6 @@ export class AlertTable extends React.Component<
const { numTruncatedResults, sourceLocationPrefix } =
resultSet.interpretation;
function renderRelatedLocations(
msg: string,
relatedLocations: Sarif.Location[],
resultKey: Keys.PathNode | Keys.Result | undefined,
): JSX.Element[] {
const relatedLocationsById: { [k: string]: Sarif.Location } = {};
for (const loc of relatedLocations) {
relatedLocationsById[loc.id!] = loc;
}
// match things like `[link-text](related-location-id)`
const parts = parseSarifPlainTextMessage(msg);
return parts.map((part, i) => {
if (typeof part === "string") {
return <span key={i}>{part}</span>;
} else {
const renderedLocation = renderSarifLocationWithText(
part.text,
relatedLocationsById[part.dest],
resultKey,
);
return <span key={i}>{renderedLocation}</span>;
}
});
}
function renderNonLocation(
msg: string | undefined,
locationHint: string,
): JSX.Element | undefined {
if (msg === undefined) return undefined;
return <span title={locationHint}>{msg}</span>;
}
const updateSelectionCallback = (
resultKey: Keys.PathNode | Keys.Result | undefined,
) => {
@@ -147,65 +107,6 @@ export class AlertTable extends React.Component<
};
};
function renderSarifLocationWithText(
text: string | undefined,
loc: Sarif.Location,
resultKey: Keys.PathNode | Keys.Result | undefined,
): JSX.Element | undefined {
const parsedLoc = parseSarifLocation(loc, sourceLocationPrefix);
if ("hint" in parsedLoc) {
return renderNonLocation(text, parsedLoc.hint);
} else if (isWholeFileLoc(parsedLoc) || isLineColumnLoc(parsedLoc)) {
return renderLocation(
parsedLoc,
text,
databaseUri,
undefined,
updateSelectionCallback(resultKey),
);
} else {
return undefined;
}
}
/**
* Render sarif location as a link with the text being simply a
* human-readable form of the location itself.
*/
function renderSarifLocation(
loc: Sarif.Location,
pathNodeKey: Keys.PathNode | Keys.Result | undefined,
): JSX.Element | undefined {
const parsedLoc = parseSarifLocation(loc, sourceLocationPrefix);
if ("hint" in parsedLoc) {
return renderNonLocation("[no location]", parsedLoc.hint);
} else if (isWholeFileLoc(parsedLoc)) {
const shortLocation = `${basename(parsedLoc.userVisibleFile)}`;
const longLocation = `${parsedLoc.userVisibleFile}`;
return renderLocation(
parsedLoc,
shortLocation,
databaseUri,
longLocation,
updateSelectionCallback(pathNodeKey),
);
} else if (isLineColumnLoc(parsedLoc)) {
const shortLocation = `${basename(parsedLoc.userVisibleFile)}:${
parsedLoc.startLine
}:${parsedLoc.startColumn}`;
const longLocation = `${parsedLoc.userVisibleFile}`;
return renderLocation(
parsedLoc,
shortLocation,
databaseUri,
longLocation,
updateSelectionCallback(pathNodeKey),
);
} else {
return undefined;
}
}
const toggler: (keys: Keys.ResultKey[]) => (e: React.MouseEvent) => void = (
indices,
) => {
@@ -220,19 +121,32 @@ export class AlertTable extends React.Component<
(result, resultIndex) => {
const resultKey: Keys.Result = { resultIndex };
const text = result.message.text || "[no text]";
const msg: JSX.Element[] =
result.relatedLocations === undefined
? [<span key="0">{text}</span>]
: renderRelatedLocations(text, result.relatedLocations, resultKey);
const msg =
result.relatedLocations === undefined ? (
<span key="0">{text}</span>
) : (
<SarifMessageWithLocations
msg={text}
relatedLocations={result.relatedLocations}
sourceLocationPrefix={sourceLocationPrefix}
databaseUri={databaseUri}
onClick={updateSelectionCallback(resultKey)}
/>
);
const currentResultExpanded = this.state.expanded.has(
Keys.keyToString(resultKey),
);
const indicator = currentResultExpanded ? chevronDown : chevronRight;
const location =
result.locations !== undefined &&
result.locations.length > 0 &&
renderSarifLocation(result.locations[0], resultKey);
const location = result.locations !== undefined &&
result.locations.length > 0 && (
<SarifLocation
loc={result.locations[0]}
sourceLocationPrefix={sourceLocationPrefix}
databaseUri={databaseUri}
onClick={updateSelectionCallback(resultKey)}
/>
);
const locationCells = (
<td className="vscode-codeql__location-cell">{location}</td>
);
@@ -342,17 +256,28 @@ export class AlertTable extends React.Component<
const step = pathNodes[pathNodeIndex];
const msg =
step.location !== undefined &&
step.location.message !== undefined
? renderSarifLocationWithText(
step.location.message.text,
step.location,
pathNodeKey,
)
: "[no location]";
step.location.message !== undefined ? (
<SarifLocation
text={step.location.message.text}
loc={step.location}
sourceLocationPrefix={sourceLocationPrefix}
databaseUri={databaseUri}
onClick={updateSelectionCallback(pathNodeKey)}
/>
) : (
"[no location]"
);
const additionalMsg =
step.location !== undefined
? renderSarifLocation(step.location, pathNodeKey)
: "";
step.location !== undefined ? (
<SarifLocation
loc={step.location}
sourceLocationPrefix={sourceLocationPrefix}
databaseUri={databaseUri}
onClick={updateSelectionCallback(pathNodeKey)}
/>
) : (
""
);
const isSelected = Keys.equalsNotUndefined(
this.state.selectedItem,
pathNodeKey,

View File

@@ -0,0 +1,50 @@
import * as React from "react";
import { useCallback } from "react";
import { ResolvableLocationValue } from "../../../common/bqrs-cli-types";
import { jumpToLocation } from "../result-table-utils";
interface Props {
loc: ResolvableLocationValue;
label: string;
databaseUri: string;
title?: string;
onClick?: () => void;
}
/**
* A clickable location link.
*/
export function ClickableLocation({
loc,
label,
databaseUri,
title,
onClick: onClick,
}: Props): JSX.Element {
const handleClick = useCallback(
(e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
jumpToLocation(loc, databaseUri);
onClick?.();
},
[loc, databaseUri, onClick],
);
return (
<>
{/*
eslint-disable-next-line
jsx-a11y/anchor-is-valid,
*/}
<a
href="#"
className="vscode-codeql__result-table-location-link"
title={title}
onClick={handleClick}
>
{label}
</a>
</>
);
}

View File

@@ -0,0 +1,55 @@
import * as React from "react";
import { useMemo } from "react";
import { UrlValue } from "../../../common/bqrs-cli-types";
import {
isStringLoc,
tryGetResolvableLocation,
} from "../../../common/bqrs-utils";
import { convertNonPrintableChars } from "../../../common/text-utils";
import { NonClickableLocation } from "./NonClickableLocation";
import { ClickableLocation } from "./ClickableLocation";
interface Props {
loc?: UrlValue;
label?: string;
databaseUri?: string;
title?: string;
onClick?: () => void;
}
/**
* A location link. Will be clickable if a location URL and database URI are provided.
*/
export function Location({
loc,
label,
databaseUri,
title,
onClick,
}: Props): JSX.Element {
const resolvableLoc = useMemo(() => tryGetResolvableLocation(loc), [loc]);
const displayLabel = useMemo(() => convertNonPrintableChars(label), [label]);
if (loc === undefined) {
return <NonClickableLocation msg={displayLabel} />;
}
if (isStringLoc(loc)) {
return <a href={loc}>{loc}</a>;
}
if (databaseUri === undefined || resolvableLoc === undefined) {
return <NonClickableLocation msg={displayLabel} locationHint={title} />;
}
return (
<ClickableLocation
loc={resolvableLoc}
label={displayLabel}
databaseUri={databaseUri}
title={title}
onClick={onClick}
/>
);
}

View File

@@ -0,0 +1,15 @@
import * as React from "react";
interface Props {
msg?: string;
locationHint?: string;
}
/**
* A non-clickable location for when there isn't a valid link.
* Designed to fit in with the other types of location components.
*/
export function NonClickableLocation({ msg, locationHint }: Props) {
if (msg === undefined) return null;
return <span title={locationHint}>{msg}</span>;
}

View File

@@ -0,0 +1,68 @@
import * as React from "react";
import * as Sarif from "sarif";
import { isLineColumnLoc, isWholeFileLoc } from "../../../common/bqrs-utils";
import { parseSarifLocation } from "../../../common/sarif-utils";
import { basename } from "path";
import { useMemo } from "react";
import { Location } from "./Location";
interface Props {
text?: string;
loc?: Sarif.Location;
sourceLocationPrefix: string;
databaseUri: string;
onClick: () => void;
}
/**
* A clickable SARIF location link.
*
* Custom text can be provided, but otherwise the text will be
* a human-readable form of the location itself.
*/
export function SarifLocation({
text,
loc,
sourceLocationPrefix,
databaseUri,
onClick,
}: Props) {
const parsedLoc = useMemo(
() => loc && parseSarifLocation(loc, sourceLocationPrefix),
[loc, sourceLocationPrefix],
);
if (parsedLoc === undefined || "hint" in parsedLoc) {
return <Location label={text || "[no location]"} title={parsedLoc?.hint} />;
}
if (isWholeFileLoc(parsedLoc)) {
return (
<Location
loc={parsedLoc}
label={text || `${basename(parsedLoc.userVisibleFile)}`}
databaseUri={databaseUri}
title={text ? undefined : `${parsedLoc.userVisibleFile}`}
onClick={onClick}
/>
);
}
if (isLineColumnLoc(parsedLoc)) {
return (
<Location
loc={parsedLoc}
label={
text ||
`${basename(parsedLoc.userVisibleFile)}:${parsedLoc.startLine}:${
parsedLoc.startColumn
}`
}
databaseUri={databaseUri}
title={text ? undefined : `${parsedLoc.userVisibleFile}`}
onClick={onClick}
/>
);
}
return null;
}

View File

@@ -0,0 +1,51 @@
import * as React from "react";
import * as Sarif from "sarif";
import { parseSarifPlainTextMessage } from "../../../common/sarif-utils";
import { SarifLocation } from "./SarifLocation";
interface Props {
msg: string;
relatedLocations: Sarif.Location[];
sourceLocationPrefix: string;
databaseUri: string;
onClick: () => void;
}
/**
* Parses a SARIF message and populates clickable locations.
*/
export function SarifMessageWithLocations({
msg,
relatedLocations,
sourceLocationPrefix,
databaseUri,
onClick,
}: Props) {
const relatedLocationsById: Map<number, Sarif.Location> = new Map();
for (const loc of relatedLocations) {
if (loc.id !== undefined) {
relatedLocationsById.set(loc.id, loc);
}
}
return (
<>
{parseSarifPlainTextMessage(msg).map((part, i) => {
if (typeof part === "string") {
return <span key={i}>{part}</span>;
} else {
return (
<SarifLocation
key={i}
text={part.text}
loc={relatedLocationsById.get(part.dest)}
sourceLocationPrefix={sourceLocationPrefix}
databaseUri={databaseUri}
onClick={onClick}
/>
);
}
})}
</>
);
}

View File

@@ -1,6 +1,5 @@
import * as React from "react";
import { UrlValue, ResolvableLocationValue } from "../../common/bqrs-cli-types";
import { isStringLoc, tryGetResolvableLocation } from "../../common/bqrs-utils";
import { ResolvableLocationValue } from "../../common/bqrs-cli-types";
import {
RawResultsSortState,
QueryMetadata,
@@ -9,7 +8,6 @@ import {
} from "../../common/interface-types";
import { assertNever } from "../../common/helpers-pure";
import { vscode } from "../vscode-api";
import { convertNonPrintableChars } from "../../common/text-utils";
import { sendTelemetry } from "../common/telemetry";
export interface ResultTableProps {
@@ -44,21 +42,6 @@ export const oddRowClassName = "vscode-codeql__result-table-row--odd";
export const pathRowClassName = "vscode-codeql__result-table-row--path";
export const selectedRowClassName = "vscode-codeql__result-table-row--selected";
export function jumpToLocationHandler(
loc: ResolvableLocationValue,
databaseUri: string,
callback?: () => void,
): (e: React.MouseEvent) => void {
return (e) => {
jumpToLocation(loc, databaseUri);
e.preventDefault();
e.stopPropagation();
if (callback) {
callback();
}
};
}
export function jumpToLocation(
loc: ResolvableLocationValue,
databaseUri: string,
@@ -77,47 +60,6 @@ export function openFile(filePath: string): void {
});
}
/**
* Render a location as a link which when clicked displays the original location.
*/
export function renderLocation(
loc?: UrlValue,
label?: string,
databaseUri?: string,
title?: string,
callback?: () => void,
): JSX.Element {
const displayLabel = convertNonPrintableChars(label!);
if (loc === undefined) {
return <span>{displayLabel}</span>;
} else if (isStringLoc(loc)) {
return <a href={loc}>{loc}</a>;
}
const resolvableLoc = tryGetResolvableLocation(loc);
if (databaseUri !== undefined && resolvableLoc !== undefined) {
return (
<>
{/*
eslint-disable-next-line
jsx-a11y/anchor-is-valid,
*/}
<a
href="#"
className="vscode-codeql__result-table-location-link"
title={title}
onClick={jumpToLocationHandler(resolvableLoc, databaseUri, callback)}
>
{displayLabel}
</a>
</>
);
} else {
return <span title={title}>{displayLabel}</span>;
}
}
/**
* Returns the attributes for a zebra-striped table row at position `index`.
*/

View File

@@ -5,7 +5,7 @@ import {
VariantAnalysisStats,
VariantAnalysisStatsProps,
} from "../VariantAnalysisStats";
import { userEvent } from "@storybook/testing-library";
import userEvent from "@testing-library/user-event";
describe(VariantAnalysisStats.name, () => {
const onViewLogsClick = jest.fn();
@@ -141,13 +141,13 @@ describe(VariantAnalysisStats.name, () => {
).not.toBeInTheDocument();
});
it("renders 'View logs' link when the variant analysis status is succeeded", () => {
it("renders 'View logs' link when the variant analysis status is succeeded", async () => {
render({
variantAnalysisStatus: VariantAnalysisStatus.Succeeded,
completedAt: new Date(),
});
userEvent.click(screen.getByText("View logs"));
await userEvent.click(screen.getByText("View logs"));
expect(onViewLogsClick).toHaveBeenCalledTimes(1);
});

View File

@@ -0,0 +1,190 @@
import { Uri, workspace } from "vscode";
import * as tmp from "tmp";
import { CodeQLCliServer } from "../../../../src/codeql-cli/cli";
import { getActivatedExtension } from "../../global.helper";
import { mkdirSync, writeFileSync } from "fs";
import {
listModelFiles,
loadModeledMethods,
} from "../../../../src/data-extensions-editor/modeled-method-fs";
import { ExtensionPack } from "../../../../src/data-extensions-editor/shared/extension-pack";
import { join } from "path";
import { extLogger } from "../../../../src/common/logging/vscode";
import { homedir } from "os";
const dummyExtensionPackContents = `
name: dummy/pack
version: 0.0.0
library: true
extensionTargets:
codeql/java-all: '*'
dataExtensions:
- models/**/*.yml
`;
const dummyModelContents = `
extensions:
- addsTo:
pack: codeql/java-all
extensible: sourceModel
data: []
- addsTo:
pack: codeql/java-all
extensible: sinkModel
data:
- ["org.eclipse.jetty.server","Server",true,"getConnectors","()","","Argument[this]","sql","manual"]
- addsTo:
pack: codeql/java-all
extensible: summaryModel
data: []
- addsTo:
pack: codeql/java-all
extensible: neutralModel
data: []
`;
describe("modeled-method-fs", () => {
let tmpDir: string;
let tmpDirRemoveCallback: (() => void) | undefined;
let workspacePath: string;
let cli: CodeQLCliServer;
beforeEach(async () => {
// On windows, make sure to use a temp directory that isn't an alias and therefore won't be canonicalised by CodeQL.
// See https://github.com/github/vscode-codeql/pull/2605 for more context.
const t = tmp.dirSync({
dir:
process.platform === "win32"
? join(homedir(), "AppData", "Local", "Temp")
: undefined,
});
tmpDir = t.name;
tmpDirRemoveCallback = t.removeCallback;
const workspaceFolder = {
uri: Uri.file(join(tmpDir, "workspace")),
name: "workspace",
index: 0,
};
workspacePath = workspaceFolder.uri.fsPath;
mkdirSync(workspacePath);
jest
.spyOn(workspace, "workspaceFolders", "get")
.mockReturnValue([workspaceFolder]);
const extension = await getActivatedExtension();
cli = extension.cliServer;
});
afterEach(() => {
tmpDirRemoveCallback?.();
});
function writeExtensionPackFiles(
extensionPackName: string,
modelFileNames: string[],
): string {
const extensionPackPath = join(workspacePath, extensionPackName);
mkdirSync(extensionPackPath);
writeFileSync(
join(extensionPackPath, "codeql-pack.yml"),
dummyExtensionPackContents,
);
mkdirSync(join(extensionPackPath, "models"));
for (const filename of modelFileNames) {
writeFileSync(
join(extensionPackPath, "models", filename),
dummyModelContents,
);
}
return extensionPackPath;
}
function makeExtensionPack(path: string): ExtensionPack {
return {
path,
yamlPath: path,
name: "dummy/pack",
version: "0.0.1",
extensionTargets: {},
dataExtensions: [],
};
}
describe("listModelFiles", () => {
it("should return the empty set when the extension pack is empty", async () => {
if (!(await cli.cliConstraints.supportsResolveExtensions())) {
return;
}
const extensionPackPath = writeExtensionPackFiles("extension-pack", []);
const modelFiles = await listModelFiles(extensionPackPath, cli);
expect(modelFiles).toEqual(new Set());
});
it("should find all model files", async () => {
if (!(await cli.cliConstraints.supportsResolveExtensions())) {
return;
}
const extensionPackPath = writeExtensionPackFiles("extension-pack", [
"library1.model.yml",
"library2.model.yml",
]);
const modelFiles = await listModelFiles(extensionPackPath, cli);
expect(modelFiles).toEqual(
new Set([
join(extensionPackPath, "models", "library1.model.yml"),
join(extensionPackPath, "models", "library2.model.yml"),
]),
);
});
it("should ignore model files from other extension packs", async () => {
if (!(await cli.cliConstraints.supportsResolveExtensions())) {
return;
}
const extensionPackPath = writeExtensionPackFiles("extension-pack", [
"library1.model.yml",
]);
writeExtensionPackFiles("another-extension-pack", ["library2.model.yml"]);
const modelFiles = await listModelFiles(extensionPackPath, cli);
expect(modelFiles).toEqual(
new Set([join(extensionPackPath, "models", "library1.model.yml")]),
);
});
});
describe("loadModeledMethods", () => {
it("should load modeled methods", async () => {
if (!(await cli.cliConstraints.supportsResolveExtensions())) {
return;
}
const extensionPackPath = writeExtensionPackFiles("extension-pack", [
"library.model.yml",
]);
const modeledMethods = await loadModeledMethods(
makeExtensionPack(extensionPackPath),
cli,
extLogger,
);
expect(Object.keys(modeledMethods).length).toEqual(1);
expect(Object.keys(modeledMethods)[0]).toEqual(
"org.eclipse.jetty.server.Server#getConnectors()",
);
});
});
});