Merge remote-tracking branch 'origin/main' into dbartol/save-before-start
This commit is contained in:
@@ -65,10 +65,6 @@ const baseConfig = {
|
||||
"import/no-namespace": "off",
|
||||
"import/no-unresolved": "off",
|
||||
"import/no-webpack-loader-syntax": "off",
|
||||
"jsx-a11y/anchor-is-valid": "off",
|
||||
"jsx-a11y/no-noninteractive-element-interactions": "off",
|
||||
"jsx-a11y/no-static-element-interactions": "off",
|
||||
"jsx-a11y/click-events-have-key-events": "off",
|
||||
"no-invalid-this": "off",
|
||||
"no-fallthrough": "off",
|
||||
"no-console": "off",
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
## [UNRELEASED]
|
||||
|
||||
- Add `CodeQL: Quick Evaluation Count` command to generate the count summary statistics of the results set
|
||||
without speding the time to compute locations and strings.
|
||||
|
||||
## 1.8.6 - 14 June 2023
|
||||
|
||||
- Add repositories to a variant analysis list with GitHub Code Search. [#2439](https://github.com/github/vscode-codeql/pull/2439) and [#2476](https://github.com/github/vscode-codeql/pull/2476)
|
||||
|
||||
71
extensions/ql-vscode/package-lock.json
generated
71
extensions/ql-vscode/package-lock.json
generated
@@ -34,7 +34,7 @@
|
||||
"path-browserify": "^1.0.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"semver": "~7.3.2",
|
||||
"semver": "~7.5.2",
|
||||
"source-map": "^0.7.4",
|
||||
"source-map-support": "^0.5.21",
|
||||
"stream": "^0.0.2",
|
||||
@@ -22961,18 +22961,6 @@
|
||||
"postcss": "^8.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/css-loader/node_modules/lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/css-loader/node_modules/picocolors": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||
@@ -23066,27 +23054,6 @@
|
||||
"postcss": "^8.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/css-loader/node_modules/semver": {
|
||||
"version": "7.5.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz",
|
||||
"integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lru-cache": "^6.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/css-loader/node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/css-select": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-4.1.3.tgz",
|
||||
@@ -41396,9 +41363,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.3.7",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
|
||||
"integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
|
||||
"version": "7.5.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz",
|
||||
"integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==",
|
||||
"dependencies": {
|
||||
"lru-cache": "^6.0.0"
|
||||
},
|
||||
@@ -63730,15 +63697,6 @@
|
||||
"integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==",
|
||||
"dev": true
|
||||
},
|
||||
"lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"yallist": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"picocolors": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||
@@ -63790,21 +63748,6 @@
|
||||
"requires": {
|
||||
"icss-utils": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "7.5.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz",
|
||||
"integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lru-cache": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -77755,9 +77698,9 @@
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "7.3.7",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
|
||||
"integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
|
||||
"version": "7.5.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz",
|
||||
"integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==",
|
||||
"requires": {
|
||||
"lru-cache": "^6.0.0"
|
||||
},
|
||||
|
||||
@@ -457,6 +457,10 @@
|
||||
"command": "codeQL.quickEval",
|
||||
"title": "CodeQL: Quick Evaluation"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.quickEvalCount",
|
||||
"title": "CodeQL: Quick Evaluation Count"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.quickEvalContextEditor",
|
||||
"title": "CodeQL: Quick Evaluation"
|
||||
@@ -501,6 +505,33 @@
|
||||
"command": "codeQL.copyVersion",
|
||||
"title": "CodeQL: Copy Version Information"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueries.runLocalQueryFromQueriesPanel",
|
||||
"title": "Run local query",
|
||||
"icon": "$(run)"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueries.runLocalQueriesFromPanel",
|
||||
"title": "Run local queries",
|
||||
"icon": "$(run-all)"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.runLocalQueryFromFileTab",
|
||||
"title": "CodeQL: Run local query",
|
||||
"icon": "$(run)"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueries.runLocalQueryContextMenu",
|
||||
"title": "Run against local database"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueries.runLocalQueriesContextMenu",
|
||||
"title": "Run against local database"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueries.runVariantAnalysisContextMenu",
|
||||
"title": "Run against variant analysis repositories"
|
||||
},
|
||||
{
|
||||
"command": "codeQLVariantAnalysisRepositories.openConfigFile",
|
||||
"title": "Open database configuration file",
|
||||
@@ -872,6 +903,13 @@
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
"editor/title": [
|
||||
{
|
||||
"command": "codeQL.runLocalQueryFromFileTab",
|
||||
"group": "navigation",
|
||||
"when": "resourceExtname == .ql && codeQL.currentDatabaseItem"
|
||||
}
|
||||
],
|
||||
"view/title": [
|
||||
{
|
||||
"command": "codeQLDatabases.sortByName",
|
||||
@@ -1095,6 +1133,31 @@
|
||||
"group": "1_queryHistory@1",
|
||||
"when": "viewItem == remoteResultsItem"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueries.runLocalQueryFromQueriesPanel",
|
||||
"group": "inline",
|
||||
"when": "view == codeQLQueries && viewItem == queryFile && codeQL.currentDatabaseItem"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueries.runLocalQueryContextMenu",
|
||||
"group": "queriesPanel@1",
|
||||
"when": "view == codeQLQueries && viewItem == queryFile && codeQL.currentDatabaseItem"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueries.runLocalQueriesContextMenu",
|
||||
"group": "queriesPanel@1",
|
||||
"when": "view == codeQLQueries && viewItem == queryFolder && codeQL.currentDatabaseItem"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueries.runVariantAnalysisContextMenu",
|
||||
"group": "queriesPanel@1",
|
||||
"when": "view == codeQLQueries && viewItem == queryFile"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueries.runLocalQueriesFromPanel",
|
||||
"group": "inline",
|
||||
"when": "view == codeQLQueries && viewItem == queryFolder && codeQL.currentDatabaseItem"
|
||||
},
|
||||
{
|
||||
"command": "codeQLTests.showOutputDifferences",
|
||||
"group": "qltest@1",
|
||||
@@ -1154,6 +1217,18 @@
|
||||
"command": "codeQL.runQuery",
|
||||
"when": "resourceLangId == ql && resourceExtname == .ql"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueries.runLocalQueryFromQueriesPanel",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueries.runLocalQueriesFromPanel",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.runLocalQueryFromFileTab",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.runQueryContextEditor",
|
||||
"when": "false"
|
||||
@@ -1206,6 +1281,10 @@
|
||||
"command": "codeQL.quickEval",
|
||||
"when": "editorLangId == ql"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.quickEvalCount",
|
||||
"when": "editorLangId == ql && codeql.supportsQuickEvalCount"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.quickEvalContextEditor",
|
||||
"when": "false"
|
||||
@@ -1274,6 +1353,18 @@
|
||||
"command": "codeQL.openDataExtensionsEditor",
|
||||
"when": "config.codeQL.canary && config.codeQL.dataExtensions.editor"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueries.runLocalQueryContextMenu",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueries.runLocalQueriesContextMenu",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLQueries.runVariantAnalysisContextMenu",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQLVariantAnalysisRepositories.openConfigFile",
|
||||
"when": "false"
|
||||
@@ -1673,7 +1764,7 @@
|
||||
"path-browserify": "^1.0.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"semver": "~7.3.2",
|
||||
"semver": "~7.5.2",
|
||||
"source-map": "^0.7.4",
|
||||
"source-map-support": "^0.5.21",
|
||||
"stream": "^0.0.2",
|
||||
|
||||
@@ -1483,6 +1483,13 @@ export class CodeQLCliServer implements Disposable {
|
||||
CliVersionConstraint.CLI_VERSION_WITH_PER_QUERY_EVAL_LOG,
|
||||
) >= 0,
|
||||
);
|
||||
await this.app.commands.execute(
|
||||
"setContext",
|
||||
"codeql.supportsQuickEvalCount",
|
||||
newVersion.compare(
|
||||
CliVersionConstraint.CLI_VERSION_WITH_QUICK_EVAL_COUNT,
|
||||
) >= 0,
|
||||
);
|
||||
} catch (e) {
|
||||
this._versionChangedListeners.forEach((listener) =>
|
||||
listener(undefined),
|
||||
@@ -1845,6 +1852,18 @@ export class CliVersionConstraint {
|
||||
|
||||
public static CLI_VERSION_GLOBAL_CACHE = new SemVer("2.12.4");
|
||||
|
||||
/**
|
||||
* CLI version where the query server supports quick-eval count mode.
|
||||
*/
|
||||
public static CLI_VERSION_WITH_QUICK_EVAL_COUNT = new SemVer("2.13.3");
|
||||
|
||||
/**
|
||||
* CLI version where the langauge server supports visisbility change notifications.
|
||||
*/
|
||||
public static CLI_VERSION_WITH_VISIBILITY_NOTIFICATIONS = new SemVer(
|
||||
"2.14.0",
|
||||
);
|
||||
|
||||
constructor(private readonly cli: CodeQLCliServer) {
|
||||
/**/
|
||||
}
|
||||
@@ -1918,4 +1937,16 @@ export class CliVersionConstraint {
|
||||
async usesGlobalCompilationCache() {
|
||||
return this.isVersionAtLeast(CliVersionConstraint.CLI_VERSION_GLOBAL_CACHE);
|
||||
}
|
||||
|
||||
async supportsVisibilityNotifications() {
|
||||
return this.isVersionAtLeast(
|
||||
CliVersionConstraint.CLI_VERSION_WITH_VISIBILITY_NOTIFICATIONS,
|
||||
);
|
||||
}
|
||||
|
||||
async supportsQuickEvalCount() {
|
||||
return this.isVersionAtLeast(
|
||||
CliVersionConstraint.CLI_VERSION_WITH_QUICK_EVAL_COUNT,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {
|
||||
VariantAnalysisScannedRepositoryResult,
|
||||
} from "../variant-analysis/shared/variant-analysis";
|
||||
import type { QLDebugConfiguration } from "../debugger/debug-configuration";
|
||||
import type { QueryTreeViewItem } from "../queries-panel/query-tree-view-item";
|
||||
|
||||
// A command function matching the signature that VS Code calls when
|
||||
// a command is invoked from a context menu on a TreeView with
|
||||
@@ -129,8 +130,14 @@ export type LocalQueryCommands = {
|
||||
"codeQL.runQueryOnMultipleDatabasesContextEditor": (
|
||||
uri?: Uri,
|
||||
) => Promise<void>;
|
||||
"codeQLQueries.runLocalQueryFromQueriesPanel": TreeViewContextSingleSelectionCommandFunction<QueryTreeViewItem>;
|
||||
"codeQLQueries.runLocalQueryContextMenu": TreeViewContextSingleSelectionCommandFunction<QueryTreeViewItem>;
|
||||
"codeQLQueries.runLocalQueriesContextMenu": TreeViewContextSingleSelectionCommandFunction<QueryTreeViewItem>;
|
||||
"codeQLQueries.runLocalQueriesFromPanel": TreeViewContextSingleSelectionCommandFunction<QueryTreeViewItem>;
|
||||
"codeQL.runLocalQueryFromFileTab": (uri: Uri) => Promise<void>;
|
||||
"codeQL.runQueries": ExplorerSelectionCommandFunction<Uri>;
|
||||
"codeQL.quickEval": (uri: Uri) => Promise<void>;
|
||||
"codeQL.quickEvalCount": (uri: Uri) => Promise<void>;
|
||||
"codeQL.quickEvalContextEditor": (uri: Uri) => Promise<void>;
|
||||
"codeQL.codeLensQuickEval": (uri: Uri, range: Range) => Promise<void>;
|
||||
"codeQL.quickQuery": () => Promise<void>;
|
||||
@@ -262,6 +269,7 @@ export type VariantAnalysisCommands = {
|
||||
) => Promise<void>;
|
||||
"codeQL.runVariantAnalysis": (uri?: Uri) => Promise<void>;
|
||||
"codeQL.runVariantAnalysisContextEditor": (uri?: Uri) => Promise<void>;
|
||||
"codeQLQueries.runVariantAnalysisContextMenu": TreeViewContextSingleSelectionCommandFunction<QueryTreeViewItem>;
|
||||
};
|
||||
|
||||
export type DatabasePanelCommands = {
|
||||
|
||||
@@ -20,6 +20,7 @@ import { DataFlowPaths } from "../variant-analysis/shared/data-flow-paths";
|
||||
import { ExternalApiUsage } from "../data-extensions-editor/external-api-usage";
|
||||
import { ModeledMethod } from "../data-extensions-editor/modeled-method";
|
||||
import { DataExtensionEditorViewState } from "../data-extensions-editor/shared/view-state";
|
||||
import { Mode } from "../data-extensions-editor/shared/mode";
|
||||
|
||||
/**
|
||||
* This module contains types and code that are shared between
|
||||
@@ -521,6 +522,11 @@ export interface AddModeledMethodsMessage {
|
||||
overrideNone?: boolean;
|
||||
}
|
||||
|
||||
export interface SwitchModeMessage {
|
||||
t: "switchMode";
|
||||
mode: Mode;
|
||||
}
|
||||
|
||||
export interface JumpToUsageMessage {
|
||||
t: "jumpToUsage";
|
||||
location: ResolvableLocationValue;
|
||||
@@ -530,8 +536,8 @@ export interface OpenExtensionPackMessage {
|
||||
t: "openExtensionPack";
|
||||
}
|
||||
|
||||
export interface OpenModelFileMessage {
|
||||
t: "openModelFile";
|
||||
export interface RefreshExternalApiUsages {
|
||||
t: "refreshExternalApiUsages";
|
||||
}
|
||||
|
||||
export interface SaveModeledMethods {
|
||||
@@ -558,7 +564,8 @@ export type ToDataExtensionsEditorMessage =
|
||||
|
||||
export type FromDataExtensionsEditorMessage =
|
||||
| ViewLoadedMsg
|
||||
| OpenModelFileMessage
|
||||
| SwitchModeMessage
|
||||
| RefreshExternalApiUsages
|
||||
| OpenExtensionPackMessage
|
||||
| JumpToUsageMessage
|
||||
| SaveModeledMethods
|
||||
|
||||
@@ -19,3 +19,11 @@ export const basename = (path: string): string => {
|
||||
const index = path.lastIndexOf("\\");
|
||||
return index === -1 ? path : path.slice(index + 1);
|
||||
};
|
||||
|
||||
// Returns the extension of a path, including the leading dot.
|
||||
export const extname = (path: string): string => {
|
||||
const name = basename(path);
|
||||
|
||||
const index = name.lastIndexOf(".");
|
||||
return index === -1 ? "" : name.slice(index);
|
||||
};
|
||||
@@ -24,11 +24,7 @@ export class Setting {
|
||||
parent?: Setting;
|
||||
private _hasChildren = false;
|
||||
|
||||
constructor(
|
||||
name: string,
|
||||
parent?: Setting,
|
||||
private readonly languageId?: string,
|
||||
) {
|
||||
constructor(name: string, parent?: Setting) {
|
||||
this.name = name;
|
||||
this.parent = parent;
|
||||
if (parent !== undefined) {
|
||||
@@ -49,22 +45,12 @@ export class Setting {
|
||||
}
|
||||
}
|
||||
|
||||
get scope(): ConfigurationScope | undefined {
|
||||
if (this.languageId !== undefined) {
|
||||
return {
|
||||
languageId: this.languageId,
|
||||
};
|
||||
} else {
|
||||
return this.parent?.scope;
|
||||
}
|
||||
}
|
||||
|
||||
getValue<T>(): T {
|
||||
getValue<T>(scope?: ConfigurationScope | null): T {
|
||||
if (this.parent === undefined) {
|
||||
throw new Error("Cannot get the value of a root setting.");
|
||||
}
|
||||
return workspace
|
||||
.getConfiguration(this.parent.qualifiedName, this.parent.scope)
|
||||
.getConfiguration(this.parent.qualifiedName, scope)
|
||||
.get<T>(this.name)!;
|
||||
}
|
||||
|
||||
@@ -73,7 +59,7 @@ export class Setting {
|
||||
throw new Error("Cannot update the value of a root setting.");
|
||||
}
|
||||
return workspace
|
||||
.getConfiguration(this.parent.qualifiedName, this.parent.scope)
|
||||
.getConfiguration(this.parent.qualifiedName)
|
||||
.update(this.name, value, target);
|
||||
}
|
||||
}
|
||||
@@ -84,7 +70,7 @@ export interface InspectionResult<T> {
|
||||
workspaceFolderValue?: T;
|
||||
}
|
||||
|
||||
const VSCODE_DEBUG_SETTING = new Setting("debug", undefined, "ql");
|
||||
const VSCODE_DEBUG_SETTING = new Setting("debug", undefined);
|
||||
export const VSCODE_SAVE_BEFORE_START_SETTING = new Setting(
|
||||
"saveBeforeStart",
|
||||
VSCODE_DEBUG_SETTING,
|
||||
@@ -731,15 +717,30 @@ export function showQueriesPanel(): boolean {
|
||||
|
||||
const DATA_EXTENSIONS = new Setting("dataExtensions", ROOT_SETTING);
|
||||
const LLM_GENERATION = new Setting("llmGeneration", DATA_EXTENSIONS);
|
||||
const FRAMEWORK_MODE = new Setting("frameworkMode", DATA_EXTENSIONS);
|
||||
const DISABLE_AUTO_NAME_EXTENSION_PACK = new Setting(
|
||||
"disableAutoNameExtensionPack",
|
||||
DATA_EXTENSIONS,
|
||||
);
|
||||
const EXTENSIONS_DIRECTORY = new Setting(
|
||||
"extensionsDirectory",
|
||||
DATA_EXTENSIONS,
|
||||
);
|
||||
|
||||
export function showLlmGeneration(): boolean {
|
||||
return !!LLM_GENERATION.getValue<boolean>();
|
||||
}
|
||||
|
||||
export function enableFrameworkMode(): boolean {
|
||||
return !!FRAMEWORK_MODE.getValue<boolean>();
|
||||
}
|
||||
|
||||
export function disableAutoNameExtensionPack(): boolean {
|
||||
return !!DISABLE_AUTO_NAME_EXTENSION_PACK.getValue<boolean>();
|
||||
}
|
||||
|
||||
export function getExtensionsDirectory(languageId: string): string | undefined {
|
||||
return EXTENSIONS_DIRECTORY.getValue<string>({
|
||||
languageId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ export async function getAutoModelUsages({
|
||||
// This will re-run the query that was already run when opening the data extensions editor. This
|
||||
// might be unnecessary, but this makes it really easy to get the path to the BQRS file which we
|
||||
// need to interpret the results.
|
||||
const queryResult = await runQuery({
|
||||
const queryResult = await runQuery("applicationModeQuery", {
|
||||
cliServer,
|
||||
queryRunner,
|
||||
queryStorageDir,
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
ModelRequest,
|
||||
} from "./auto-model-api";
|
||||
import type { UsageSnippetsBySignature } from "./auto-model-usages-query";
|
||||
import { groupMethods, sortGroupNames, sortMethods } from "./shared/sorting";
|
||||
import { Mode } from "./shared/mode";
|
||||
|
||||
// Soft limit on the number of candidates to send to the model.
|
||||
// Note that the model may return fewer than this number of candidates.
|
||||
@@ -19,6 +21,7 @@ export function createAutoModelRequest(
|
||||
externalApiUsages: ExternalApiUsage[],
|
||||
modeledMethods: Record<string, ModeledMethod>,
|
||||
usages: UsageSnippetsBySignature,
|
||||
mode: Mode,
|
||||
): ModelRequest {
|
||||
const request: ModelRequest = {
|
||||
language,
|
||||
@@ -26,11 +29,14 @@ export function createAutoModelRequest(
|
||||
candidates: [],
|
||||
};
|
||||
|
||||
// Sort by number of usages so we always send the most used methods first
|
||||
externalApiUsages = [...externalApiUsages];
|
||||
externalApiUsages.sort((a, b) => b.usages.length - a.usages.length);
|
||||
// Sort the same way as the UI so we send the first ones listed in the UI first
|
||||
const grouped = groupMethods(externalApiUsages, mode);
|
||||
const sortedGroupNames = sortGroupNames(grouped);
|
||||
const sortedExternalApiUsages = sortedGroupNames.flatMap((name) =>
|
||||
sortMethods(grouped[name]),
|
||||
);
|
||||
|
||||
for (const externalApiUsage of externalApiUsages) {
|
||||
for (const externalApiUsage of sortedExternalApiUsages) {
|
||||
const modeledMethod: ModeledMethod = modeledMethods[
|
||||
externalApiUsage.signature
|
||||
] ?? {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { ensureDir } from "fs-extra";
|
||||
import { join } from "path";
|
||||
import { App } from "../common/app";
|
||||
import { withProgress } from "../common/vscode/progress";
|
||||
import { pickExtensionPackModelFile } from "./extension-pack-picker";
|
||||
import { pickExtensionPack } from "./extension-pack-picker";
|
||||
import { showAndLogErrorMessage } from "../common/logging";
|
||||
|
||||
const SUPPORTED_LANGUAGES: string[] = ["java", "csharp"];
|
||||
@@ -78,7 +78,7 @@ export class DataExtensionsEditorModule {
|
||||
return;
|
||||
}
|
||||
|
||||
const modelFile = await pickExtensionPackModelFile(
|
||||
const modelFile = await pickExtensionPack(
|
||||
this.cliServer,
|
||||
db,
|
||||
this.app.logger,
|
||||
|
||||
@@ -4,8 +4,8 @@ import {
|
||||
Uri,
|
||||
ViewColumn,
|
||||
window,
|
||||
workspace,
|
||||
} from "vscode";
|
||||
import { join } from "path";
|
||||
import { RequestError } from "@octokit/request-error";
|
||||
import {
|
||||
AbstractWebview,
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
showAndLogExceptionWithTelemetry,
|
||||
showAndLogErrorMessage,
|
||||
} from "../common/logging";
|
||||
import { outputFile, pathExists, readFile } from "fs-extra";
|
||||
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";
|
||||
@@ -34,17 +34,23 @@ 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 { createDataExtensionYaml, loadDataExtensionYaml } from "./yaml";
|
||||
import {
|
||||
createDataExtensionYamlsForApplicationMode,
|
||||
createDataExtensionYamlsForFrameworkMode,
|
||||
loadDataExtensionYaml,
|
||||
} from "./yaml";
|
||||
import { ExternalApiUsage } from "./external-api-usage";
|
||||
import { ModeledMethod } from "./modeled-method";
|
||||
import { ExtensionPackModelFile } from "./shared/extension-pack";
|
||||
import { ExtensionPack } from "./shared/extension-pack";
|
||||
import { autoModel, ModelRequest, ModelResponse } from "./auto-model-api";
|
||||
import {
|
||||
createAutoModelRequest,
|
||||
parsePredictedClassifications,
|
||||
} from "./auto-model";
|
||||
import { showLlmGeneration } from "../config";
|
||||
import { enableFrameworkMode, showLlmGeneration } from "../config";
|
||||
import { getAutoModelUsages } from "./auto-model-usages-query";
|
||||
import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
|
||||
import { Mode } from "./shared/mode";
|
||||
|
||||
export class DataExtensionsEditorView extends AbstractWebview<
|
||||
ToDataExtensionsEditorMessage,
|
||||
@@ -58,7 +64,8 @@ export class DataExtensionsEditorView extends AbstractWebview<
|
||||
private readonly queryRunner: QueryRunner,
|
||||
private readonly queryStorageDir: string,
|
||||
private readonly databaseItem: DatabaseItem,
|
||||
private readonly modelFile: ExtensionPackModelFile,
|
||||
private readonly extensionPack: ExtensionPack,
|
||||
private mode: Mode = Mode.Application,
|
||||
) {
|
||||
super(ctx);
|
||||
}
|
||||
@@ -95,14 +102,12 @@ export class DataExtensionsEditorView extends AbstractWebview<
|
||||
case "openExtensionPack":
|
||||
await this.app.commands.execute(
|
||||
"revealInExplorer",
|
||||
Uri.file(this.modelFile.extensionPack.path),
|
||||
Uri.file(this.extensionPack.path),
|
||||
);
|
||||
|
||||
break;
|
||||
case "openModelFile":
|
||||
await window.showTextDocument(
|
||||
await workspace.openTextDocument(this.modelFile.filename),
|
||||
);
|
||||
case "refreshExternalApiUsages":
|
||||
await this.loadExternalApiUsages();
|
||||
|
||||
break;
|
||||
case "jumpToUsage":
|
||||
@@ -127,6 +132,12 @@ export class DataExtensionsEditorView extends AbstractWebview<
|
||||
msg.modeledMethods,
|
||||
);
|
||||
|
||||
break;
|
||||
case "switchMode":
|
||||
this.mode = msg.mode;
|
||||
|
||||
await Promise.all([this.setViewState(), this.loadExternalApiUsages()]);
|
||||
|
||||
break;
|
||||
default:
|
||||
assertNever(msg);
|
||||
@@ -147,9 +158,10 @@ export class DataExtensionsEditorView extends AbstractWebview<
|
||||
await this.postMessage({
|
||||
t: "setDataExtensionEditorViewState",
|
||||
viewState: {
|
||||
extensionPackModelFile: this.modelFile,
|
||||
modelFileExists: await pathExists(this.modelFile.filename),
|
||||
extensionPack: this.extensionPack,
|
||||
enableFrameworkMode: enableFrameworkMode(),
|
||||
showLlmButton: showLlmGeneration(),
|
||||
mode: this.mode,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -178,39 +190,70 @@ export class DataExtensionsEditorView extends AbstractWebview<
|
||||
externalApiUsages: ExternalApiUsage[],
|
||||
modeledMethods: Record<string, ModeledMethod>,
|
||||
): Promise<void> {
|
||||
const yaml = createDataExtensionYaml(
|
||||
this.databaseItem.language,
|
||||
externalApiUsages,
|
||||
modeledMethods,
|
||||
);
|
||||
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);
|
||||
}
|
||||
|
||||
await outputFile(this.modelFile.filename, yaml);
|
||||
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 to ${this.modelFile.filename}`,
|
||||
);
|
||||
void this.app.logger.log(`Saved data extension YAML`);
|
||||
}
|
||||
|
||||
protected async loadExistingModeledMethods(): Promise<void> {
|
||||
try {
|
||||
if (!(await pathExists(this.modelFile.filename))) {
|
||||
return;
|
||||
const extensions = await this.cliServer.resolveExtensions(
|
||||
this.extensionPack.path,
|
||||
getOnDiskWorkspaceFolders(),
|
||||
);
|
||||
|
||||
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 yaml = await readFile(this.modelFile.filename, "utf8");
|
||||
const existingModeledMethods: Record<string, ModeledMethod> = {};
|
||||
|
||||
const data = loadYaml(yaml, {
|
||||
filename: this.modelFile.filename,
|
||||
});
|
||||
for (const modelFile of modelFiles) {
|
||||
const yaml = await readFile(modelFile, "utf8");
|
||||
|
||||
const existingModeledMethods = loadDataExtensionYaml(data);
|
||||
const data = loadYaml(yaml, {
|
||||
filename: modelFile,
|
||||
});
|
||||
|
||||
if (!existingModeledMethods) {
|
||||
void showAndLogErrorMessage(
|
||||
this.app.logger,
|
||||
`Failed to parse data extension YAML ${this.modelFile.filename}.`,
|
||||
);
|
||||
return;
|
||||
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({
|
||||
@@ -220,9 +263,7 @@ export class DataExtensionsEditorView extends AbstractWebview<
|
||||
} catch (e: unknown) {
|
||||
void showAndLogErrorMessage(
|
||||
this.app.logger,
|
||||
`Unable to read data extension YAML ${
|
||||
this.modelFile.filename
|
||||
}: ${getErrorMessage(e)}`,
|
||||
`Unable to read data extension YAML: ${getErrorMessage(e)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -231,16 +272,21 @@ export class DataExtensionsEditorView extends AbstractWebview<
|
||||
const cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
try {
|
||||
const queryResult = await runQuery({
|
||||
cliServer: this.cliServer,
|
||||
queryRunner: this.queryRunner,
|
||||
databaseItem: this.databaseItem,
|
||||
queryStorageDir: this.queryStorageDir,
|
||||
progress: (progressUpdate: ProgressUpdate) => {
|
||||
void this.showProgress(progressUpdate, 1500);
|
||||
const queryResult = await runQuery(
|
||||
this.mode === Mode.Framework
|
||||
? "frameworkModeQuery"
|
||||
: "applicationModeQuery",
|
||||
{
|
||||
cliServer: this.cliServer,
|
||||
queryRunner: this.queryRunner,
|
||||
databaseItem: this.databaseItem,
|
||||
queryStorageDir: this.queryStorageDir,
|
||||
progress: (progressUpdate: ProgressUpdate) => {
|
||||
void this.showProgress(progressUpdate, 1500);
|
||||
},
|
||||
token: cancellationTokenSource.token,
|
||||
},
|
||||
token: cancellationTokenSource.token,
|
||||
});
|
||||
);
|
||||
if (!queryResult) {
|
||||
await this.clearProgress();
|
||||
return;
|
||||
@@ -289,30 +335,36 @@ export class DataExtensionsEditorView extends AbstractWebview<
|
||||
protected async generateModeledMethods(): Promise<void> {
|
||||
const tokenSource = new CancellationTokenSource();
|
||||
|
||||
const selectedDatabase = this.databaseManager.currentDatabaseItem;
|
||||
let addedDatabase: DatabaseItem | undefined;
|
||||
|
||||
// The external API methods are in the library source code, so we need to ask
|
||||
// the user to import the library database. We need to have the database
|
||||
// imported to the query server, so we need to register it to our workspace.
|
||||
const database = await promptImportGithubDatabase(
|
||||
this.app.commands,
|
||||
this.databaseManager,
|
||||
this.app.workspaceStoragePath ?? this.app.globalStoragePath,
|
||||
this.app.credentials,
|
||||
(update) => this.showProgress(update),
|
||||
this.cliServer,
|
||||
);
|
||||
if (!database) {
|
||||
await this.clearProgress();
|
||||
void this.app.logger.log("No database chosen");
|
||||
// In application mode, we need the database of a specific library to generate
|
||||
// the modeled methods. In framework mode, we'll use the current database.
|
||||
if (this.mode === Mode.Application) {
|
||||
const selectedDatabase = this.databaseManager.currentDatabaseItem;
|
||||
|
||||
return;
|
||||
// The external API methods are in the library source code, so we need to ask
|
||||
// the user to import the library database. We need to have the database
|
||||
// imported to the query server, so we need to register it to our workspace.
|
||||
addedDatabase = await promptImportGithubDatabase(
|
||||
this.app.commands,
|
||||
this.databaseManager,
|
||||
this.app.workspaceStoragePath ?? this.app.globalStoragePath,
|
||||
this.app.credentials,
|
||||
(update) => this.showProgress(update),
|
||||
this.cliServer,
|
||||
);
|
||||
if (!addedDatabase) {
|
||||
await this.clearProgress();
|
||||
void this.app.logger.log("No database chosen");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// The library database was set as the current database by importing it,
|
||||
// but we need to set it back to the originally selected database.
|
||||
await this.databaseManager.setCurrentDatabaseItem(selectedDatabase);
|
||||
}
|
||||
|
||||
// The library database was set as the current database by importing it,
|
||||
// but we need to set it back to the originally selected database.
|
||||
await this.databaseManager.setCurrentDatabaseItem(selectedDatabase);
|
||||
|
||||
await this.showProgress({
|
||||
step: 0,
|
||||
maxStep: 4000,
|
||||
@@ -324,7 +376,7 @@ export class DataExtensionsEditorView extends AbstractWebview<
|
||||
cliServer: this.cliServer,
|
||||
queryRunner: this.queryRunner,
|
||||
queryStorageDir: this.queryStorageDir,
|
||||
databaseItem: database,
|
||||
databaseItem: addedDatabase ?? this.databaseItem,
|
||||
onResults: async (results) => {
|
||||
const modeledMethodsByName: Record<string, ModeledMethod> = {};
|
||||
|
||||
@@ -351,14 +403,16 @@ export class DataExtensionsEditorView extends AbstractWebview<
|
||||
);
|
||||
}
|
||||
|
||||
// After the flow model has been generated, we can remove the temporary database
|
||||
// which we used for generating the flow model.
|
||||
await this.showProgress({
|
||||
step: 3900,
|
||||
maxStep: 4000,
|
||||
message: "Removing temporary database",
|
||||
});
|
||||
await this.databaseManager.removeDatabaseItem(database);
|
||||
if (addedDatabase) {
|
||||
// After the flow model has been generated, we can remove the temporary database
|
||||
// which we used for generating the flow model.
|
||||
await this.showProgress({
|
||||
step: 3900,
|
||||
maxStep: 4000,
|
||||
message: "Removing temporary database",
|
||||
});
|
||||
await this.databaseManager.removeDatabaseItem(addedDatabase);
|
||||
}
|
||||
|
||||
await this.clearProgress();
|
||||
}
|
||||
@@ -394,6 +448,7 @@ export class DataExtensionsEditorView extends AbstractWebview<
|
||||
externalApiUsages,
|
||||
modeledMethods,
|
||||
usages,
|
||||
this.mode,
|
||||
);
|
||||
|
||||
await this.showProgress({
|
||||
|
||||
@@ -37,7 +37,7 @@ export function autoNameExtensionPack(
|
||||
};
|
||||
}
|
||||
|
||||
function sanitizeExtensionPackName(name: string) {
|
||||
export function sanitizeExtensionPackName(name: string) {
|
||||
// Lowercase everything
|
||||
name = name.toLowerCase();
|
||||
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import { join, relative, resolve, sep } from "path";
|
||||
import { join } from "path";
|
||||
import { outputFile, pathExists, readFile } from "fs-extra";
|
||||
import { dump as dumpYaml, load as loadYaml } from "js-yaml";
|
||||
import { minimatch } from "minimatch";
|
||||
import { CancellationToken, window } from "vscode";
|
||||
import { CancellationToken, Uri, window } from "vscode";
|
||||
import { CodeQLCliServer, QlpacksInfo } from "../codeql-cli/cli";
|
||||
import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
|
||||
import { ProgressCallback } from "../common/vscode/progress";
|
||||
import { DatabaseItem } from "../databases/local-databases";
|
||||
import { getQlPackPath, QLPACK_FILENAMES } from "../common/ql";
|
||||
import { getErrorMessage } from "../common/helpers-pure";
|
||||
import { ExtensionPack, ExtensionPackModelFile } from "./shared/extension-pack";
|
||||
import { ExtensionPack } from "./shared/extension-pack";
|
||||
import { NotificationLogger, showAndLogErrorMessage } from "../common/logging";
|
||||
import { containsPath } from "../common/files";
|
||||
import { disableAutoNameExtensionPack } from "../config";
|
||||
import {
|
||||
disableAutoNameExtensionPack,
|
||||
getExtensionsDirectory,
|
||||
} from "../config";
|
||||
import {
|
||||
autoNameExtensionPack,
|
||||
ExtensionPackName,
|
||||
@@ -27,42 +28,7 @@ import {
|
||||
|
||||
const maxStep = 3;
|
||||
|
||||
export async function pickExtensionPackModelFile(
|
||||
cliServer: Pick<CodeQLCliServer, "resolveQlpacks" | "resolveExtensions">,
|
||||
databaseItem: Pick<DatabaseItem, "name" | "language">,
|
||||
logger: NotificationLogger,
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
): Promise<ExtensionPackModelFile | undefined> {
|
||||
const extensionPack = await pickExtensionPack(
|
||||
cliServer,
|
||||
databaseItem,
|
||||
logger,
|
||||
progress,
|
||||
token,
|
||||
);
|
||||
if (!extensionPack) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const modelFile = await pickModelFile(
|
||||
cliServer,
|
||||
databaseItem,
|
||||
extensionPack,
|
||||
progress,
|
||||
token,
|
||||
);
|
||||
if (!modelFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
filename: modelFile,
|
||||
extensionPack,
|
||||
};
|
||||
}
|
||||
|
||||
async function pickExtensionPack(
|
||||
export async function pickExtensionPack(
|
||||
cliServer: Pick<CodeQLCliServer, "resolveQlpacks">,
|
||||
databaseItem: Pick<DatabaseItem, "name" | "language">,
|
||||
logger: NotificationLogger,
|
||||
@@ -190,69 +156,6 @@ async function pickExtensionPack(
|
||||
return extensionPackOption.extensionPack;
|
||||
}
|
||||
|
||||
async function pickModelFile(
|
||||
cliServer: Pick<CodeQLCliServer, "resolveExtensions">,
|
||||
databaseItem: Pick<DatabaseItem, "name">,
|
||||
extensionPack: ExtensionPack,
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
): Promise<string | undefined> {
|
||||
// Find the existing model files in the extension pack
|
||||
const additionalPacks = getOnDiskWorkspaceFolders();
|
||||
const extensions = await cliServer.resolveExtensions(
|
||||
extensionPack.path,
|
||||
additionalPacks,
|
||||
);
|
||||
|
||||
const modelFiles = new Set<string>();
|
||||
|
||||
if (extensionPack.path in extensions.data) {
|
||||
for (const extension of extensions.data[extensionPack.path]) {
|
||||
modelFiles.add(extension.file);
|
||||
}
|
||||
}
|
||||
|
||||
if (modelFiles.size === 0) {
|
||||
return pickNewModelFile(databaseItem, extensionPack, token);
|
||||
}
|
||||
|
||||
const fileOptions: Array<{ label: string; file: string | null }> = [];
|
||||
for (const file of modelFiles) {
|
||||
fileOptions.push({
|
||||
label: relative(extensionPack.path, file).replaceAll(sep, "/"),
|
||||
file,
|
||||
});
|
||||
}
|
||||
fileOptions.push({
|
||||
label: "Create new model file",
|
||||
file: null,
|
||||
});
|
||||
|
||||
progress({
|
||||
message: "Choosing model file...",
|
||||
step: 3,
|
||||
maxStep,
|
||||
});
|
||||
|
||||
const fileOption = await window.showQuickPick(
|
||||
fileOptions,
|
||||
{
|
||||
title: "Select model file to use",
|
||||
},
|
||||
token,
|
||||
);
|
||||
|
||||
if (!fileOption) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (fileOption.file) {
|
||||
return fileOption.file;
|
||||
}
|
||||
|
||||
return pickNewModelFile(databaseItem, extensionPack, token);
|
||||
}
|
||||
|
||||
async function pickNewExtensionPack(
|
||||
databaseItem: Pick<DatabaseItem, "name" | "language">,
|
||||
token: CancellationToken,
|
||||
@@ -319,8 +222,14 @@ async function autoCreateExtensionPack(
|
||||
extensionPacksInfo: QlpacksInfo,
|
||||
logger: NotificationLogger,
|
||||
): Promise<ExtensionPack | undefined> {
|
||||
// Get the extensions directory to create the extension pack in
|
||||
const extensionsDirectory = await autoPickExtensionsDirectory();
|
||||
// Get the `codeQL.dataExtensions.extensionsDirectory` setting for the language
|
||||
const userExtensionsDirectory = getExtensionsDirectory(language);
|
||||
|
||||
// If the setting is not set, automatically pick a suitable directory
|
||||
const extensionsDirectory = userExtensionsDirectory
|
||||
? Uri.file(userExtensionsDirectory)
|
||||
: await autoPickExtensionsDirectory();
|
||||
|
||||
if (!extensionsDirectory) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -428,49 +337,6 @@ async function writeExtensionPack(
|
||||
return extensionPack;
|
||||
}
|
||||
|
||||
async function pickNewModelFile(
|
||||
databaseItem: Pick<DatabaseItem, "name">,
|
||||
extensionPack: ExtensionPack,
|
||||
token: CancellationToken,
|
||||
) {
|
||||
const filename = await window.showInputBox(
|
||||
{
|
||||
title: "Enter the name of the new model file",
|
||||
value: `models/${databaseItem.name.replaceAll("/", ".")}.model.yml`,
|
||||
validateInput: async (value: string): Promise<string | undefined> => {
|
||||
if (value === "") {
|
||||
return "File name must not be empty";
|
||||
}
|
||||
|
||||
const path = resolve(extensionPack.path, value);
|
||||
|
||||
if (await pathExists(path)) {
|
||||
return "File already exists";
|
||||
}
|
||||
|
||||
if (!containsPath(extensionPack.path, path)) {
|
||||
return "File must be in the extension pack";
|
||||
}
|
||||
|
||||
const matchesPattern = extensionPack.dataExtensions.some((pattern) =>
|
||||
minimatch(value, pattern, { matchBase: true }),
|
||||
);
|
||||
if (!matchesPattern) {
|
||||
return `File must match one of the patterns in 'dataExtensions' in ${extensionPack.yamlPath}`;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
token,
|
||||
);
|
||||
if (!filename) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return resolve(extensionPack.path, filename);
|
||||
}
|
||||
|
||||
async function readExtensionPack(path: string): Promise<ExtensionPack> {
|
||||
const qlpackPath = await getQlPackPath(path);
|
||||
if (!qlpackPath) {
|
||||
|
||||
@@ -15,6 +15,7 @@ import { QueryResultType } from "../query-server/new-messages";
|
||||
import { join } from "path";
|
||||
import { redactableError } from "../common/errors";
|
||||
import { telemetryListener } from "../common/vscode/telemetry";
|
||||
import { Query } from "./queries/query";
|
||||
|
||||
export type RunQueryOptions = {
|
||||
cliServer: Pick<CodeQLCliServer, "resolveQlpacks">;
|
||||
@@ -26,14 +27,17 @@ export type RunQueryOptions = {
|
||||
token: CancellationToken;
|
||||
};
|
||||
|
||||
export async function runQuery({
|
||||
cliServer,
|
||||
queryRunner,
|
||||
databaseItem,
|
||||
queryStorageDir,
|
||||
progress,
|
||||
token,
|
||||
}: RunQueryOptions): Promise<CoreCompletedQuery | undefined> {
|
||||
export async function runQuery(
|
||||
queryName: keyof Omit<Query, "dependencies">,
|
||||
{
|
||||
cliServer,
|
||||
queryRunner,
|
||||
databaseItem,
|
||||
queryStorageDir,
|
||||
progress,
|
||||
token,
|
||||
}: RunQueryOptions,
|
||||
): Promise<CoreCompletedQuery | undefined> {
|
||||
// The below code is temporary to allow for rapid prototyping of the queries. Once the queries are stabilized, we will
|
||||
// move these queries into the `github/codeql` repository and use them like any other contextual (e.g. AST) queries.
|
||||
// This is intentionally not pretty code, as it will be removed soon.
|
||||
@@ -61,7 +65,7 @@ export async function runQuery({
|
||||
|
||||
const queryDir = (await dir({ unsafeCleanup: true })).path;
|
||||
const queryFile = join(queryDir, "FetchExternalApis.ql");
|
||||
await writeFile(queryFile, query.mainQuery, "utf8");
|
||||
await writeFile(queryFile, query[queryName], "utf8");
|
||||
|
||||
if (query.dependencies) {
|
||||
for (const [filename, contents] of Object.entries(query.dependencies)) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Query } from "./query";
|
||||
|
||||
export const fetchExternalApisQuery: Query = {
|
||||
mainQuery: `/**
|
||||
applicationModeQuery: `/**
|
||||
* @name Usage of APIs coming from external libraries
|
||||
* @description A list of 3rd party APIs used in the codebase.
|
||||
* @tags telemetry
|
||||
@@ -9,27 +9,52 @@ export const fetchExternalApisQuery: Query = {
|
||||
* @id cs/telemetry/fetch-external-apis
|
||||
*/
|
||||
|
||||
import csharp
|
||||
import ExternalApi
|
||||
private import csharp
|
||||
private import AutomodelVsCode
|
||||
|
||||
class ExternalApi extends CallableMethod {
|
||||
ExternalApi() {
|
||||
this.isUnboundDeclaration() and
|
||||
this.fromLibrary() and
|
||||
this.(Modifiable).isEffectivelyPublic()
|
||||
}
|
||||
}
|
||||
|
||||
private Call aUsage(ExternalApi api) { result.getTarget().getUnboundDeclaration() = api }
|
||||
|
||||
private boolean isSupported(ExternalApi api) {
|
||||
api.isSupported() and result = true
|
||||
or
|
||||
not api.isSupported() and
|
||||
result = false
|
||||
}
|
||||
|
||||
from ExternalApi api, string apiName, boolean supported, Call usage
|
||||
where
|
||||
apiName = api.getApiName() and
|
||||
supported = isSupported(api) and
|
||||
usage = aUsage(api)
|
||||
select usage, apiName, supported.toString(), "supported", api.getFile().getBaseName(), "library"
|
||||
`,
|
||||
frameworkModeQuery: `/**
|
||||
* @name Public methods
|
||||
* @description A list of APIs callable by consumers. Excludes test and generated code.
|
||||
* @tags telemetry
|
||||
* @kind problem
|
||||
* @id cs/telemetry/fetch-public-methods
|
||||
*/
|
||||
|
||||
private import csharp
|
||||
private import dotnet
|
||||
private import semmle.code.csharp.frameworks.Test
|
||||
private import AutomodelVsCode
|
||||
|
||||
class PublicMethod extends CallableMethod {
|
||||
PublicMethod() { this.fromSource() and not this.getFile() instanceof TestFile }
|
||||
}
|
||||
|
||||
from PublicMethod publicMethod, string apiName, boolean supported
|
||||
where
|
||||
apiName = publicMethod.getApiName() and
|
||||
supported = isSupported(publicMethod)
|
||||
select publicMethod, apiName, supported.toString(), "supported",
|
||||
publicMethod.getFile().getBaseName(), "library"
|
||||
`,
|
||||
dependencies: {
|
||||
"ExternalApi.qll": `/** Provides classes and predicates related to handling APIs from external libraries. */
|
||||
"AutomodelVsCode.qll": `/** Provides classes and predicates related to handling APIs for the VS Code extension. */
|
||||
|
||||
private import csharp
|
||||
private import dotnet
|
||||
@@ -59,18 +84,17 @@ class TestLibrary extends RefType {
|
||||
}
|
||||
|
||||
/** Holds if the given callable is not worth supporting. */
|
||||
private predicate isUninteresting(DotNet::Callable c) {
|
||||
private predicate isUninteresting(DotNet::Declaration c) {
|
||||
c.getDeclaringType() instanceof TestLibrary or
|
||||
c.(Constructor).isParameterless()
|
||||
c.(Constructor).isParameterless() or
|
||||
c.getDeclaringType() instanceof AnonymousClass
|
||||
}
|
||||
|
||||
/**
|
||||
* An external API from either the C# Standard Library or a 3rd party library.
|
||||
* An callable method from either the C# Standard Library, a 3rd party library, or from the source.
|
||||
*/
|
||||
class ExternalApi extends DotNet::Callable {
|
||||
ExternalApi() {
|
||||
this.isUnboundDeclaration() and
|
||||
this.fromLibrary() and
|
||||
class CallableMethod extends DotNet::Declaration {
|
||||
CallableMethod() {
|
||||
this.(Modifiable).isEffectivelyPublic() and
|
||||
not isUninteresting(this)
|
||||
}
|
||||
@@ -81,7 +105,7 @@ class ExternalApi extends DotNet::Callable {
|
||||
bindingset[this]
|
||||
private string getSignature() {
|
||||
result =
|
||||
this.getDeclaringType().getUnboundDeclaration() + "." + this.getName() + "(" +
|
||||
nestedName(this.getDeclaringType().getUnboundDeclaration()) + "." + this.getName() + "(" +
|
||||
parameterQualifiedTypeNamesToString(this) + ")"
|
||||
}
|
||||
|
||||
@@ -149,47 +173,26 @@ class ExternalApi extends DotNet::Callable {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the limit for the number of results produced by a telemetry query.
|
||||
*/
|
||||
int resultLimit() { result = 1000 }
|
||||
boolean isSupported(CallableMethod callableMethod) {
|
||||
callableMethod.isSupported() and result = true
|
||||
or
|
||||
not callableMethod.isSupported() and
|
||||
result = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if it is relevant to count usages of \`api\`.
|
||||
* Gets the nested name of the declaration.
|
||||
*
|
||||
* If the declaration is not a nested type, the result is the same as \`getName()\`.
|
||||
* Otherwise the name of the nested type is prefixed with a \`+\` and appended to
|
||||
* the name of the enclosing type, which might be a nested type as well.
|
||||
*/
|
||||
signature predicate relevantApi(ExternalApi api);
|
||||
|
||||
/**
|
||||
* Given a predicate to count relevant API usages, this module provides a predicate
|
||||
* for restricting the number or returned results based on a certain limit.
|
||||
*/
|
||||
module Results<relevantApi/1 getRelevantUsages> {
|
||||
private int getUsages(string apiName) {
|
||||
result =
|
||||
strictcount(Call c, ExternalApi api |
|
||||
c.getTarget().getUnboundDeclaration() = api and
|
||||
apiName = api.getApiName() and
|
||||
getRelevantUsages(api)
|
||||
)
|
||||
}
|
||||
|
||||
private int getOrder(string apiName) {
|
||||
apiName =
|
||||
rank[result](string name, int usages |
|
||||
usages = getUsages(name)
|
||||
|
|
||||
name order by usages desc, name
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if there exists an API with \`apiName\` that is being used \`usages\` times
|
||||
* and if it is in the top results (guarded by resultLimit).
|
||||
*/
|
||||
predicate restrict(string apiName, int usages) {
|
||||
usages = getUsages(apiName) and
|
||||
getOrder(apiName) <= resultLimit()
|
||||
}
|
||||
private string nestedName(Declaration declaration) {
|
||||
not exists(declaration.getDeclaringType().getUnboundDeclaration()) and
|
||||
result = declaration.getName()
|
||||
or
|
||||
nestedName(declaration.getDeclaringType().getUnboundDeclaration()) + "+" + declaration.getName() =
|
||||
result
|
||||
}
|
||||
`,
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Query } from "./query";
|
||||
|
||||
export const fetchExternalApisQuery: Query = {
|
||||
mainQuery: `/**
|
||||
applicationModeQuery: `/**
|
||||
* @name Usage of APIs coming from external libraries
|
||||
* @description A list of 3rd party APIs used in the codebase. Excludes test and generated code.
|
||||
* @tags telemetry
|
||||
@@ -10,28 +10,46 @@ export const fetchExternalApisQuery: Query = {
|
||||
*/
|
||||
|
||||
import java
|
||||
import ExternalApi
|
||||
import AutomodelVsCode
|
||||
|
||||
class ExternalApi extends CallableMethod {
|
||||
ExternalApi() { not this.fromSource() }
|
||||
}
|
||||
|
||||
private Call aUsage(ExternalApi api) {
|
||||
result.getCallee().getSourceDeclaration() = api and
|
||||
not result.getFile() instanceof GeneratedFile
|
||||
}
|
||||
|
||||
private boolean isSupported(ExternalApi api) {
|
||||
api.isSupported() and result = true
|
||||
or
|
||||
not api.isSupported() and result = false
|
||||
}
|
||||
|
||||
from ExternalApi api, string apiName, boolean supported, Call usage
|
||||
from ExternalApi externalApi, string apiName, boolean supported, Call usage
|
||||
where
|
||||
apiName = api.getApiName() and
|
||||
supported = isSupported(api) and
|
||||
usage = aUsage(api)
|
||||
select usage, apiName, supported.toString(), "supported", api.jarContainer(), "library"
|
||||
apiName = externalApi.getApiName() and
|
||||
supported = isSupported(externalApi) and
|
||||
usage = aUsage(externalApi)
|
||||
select usage, apiName, supported.toString(), "supported", externalApi.jarContainer(), "library"
|
||||
`,
|
||||
frameworkModeQuery: `/**
|
||||
* @name Public methods
|
||||
* @description A list of APIs callable by consumers. Excludes test and generated code.
|
||||
* @tags telemetry
|
||||
* @kind problem
|
||||
* @id java/telemetry/fetch-public-methods
|
||||
*/
|
||||
|
||||
import java
|
||||
import AutomodelVsCode
|
||||
|
||||
class PublicMethodFromSource extends CallableMethod, ModelApi { }
|
||||
|
||||
from PublicMethodFromSource publicMethod, string apiName, boolean supported
|
||||
where
|
||||
apiName = publicMethod.getApiName() and
|
||||
supported = isSupported(publicMethod)
|
||||
select publicMethod, apiName, supported.toString(), "supported",
|
||||
publicMethod.getCompilationUnit().getParentContainer().getBaseName(), "library"
|
||||
`,
|
||||
dependencies: {
|
||||
"ExternalApi.qll": `/** Provides classes and predicates related to handling APIs from external libraries. */
|
||||
"AutomodelVsCode.qll": `/** Provides classes and predicates related to handling APIs for the VS Code extension. */
|
||||
|
||||
private import java
|
||||
private import semmle.code.java.dataflow.DataFlow
|
||||
@@ -43,25 +61,26 @@ private import semmle.code.java.dataflow.internal.FlowSummaryImpl as FlowSummary
|
||||
private import semmle.code.java.dataflow.TaintTracking
|
||||
private import semmle.code.java.dataflow.internal.ModelExclusions
|
||||
|
||||
/** Holds if the given callable is not worth supporting. */
|
||||
/** Holds if the given callable/method is not worth supporting. */
|
||||
private predicate isUninteresting(Callable c) {
|
||||
c.getDeclaringType() instanceof TestLibrary or
|
||||
c.(Constructor).isParameterless()
|
||||
c.(Constructor).isParameterless() or
|
||||
c.getDeclaringType() instanceof AnonymousClass
|
||||
}
|
||||
|
||||
/**
|
||||
* An external API from either the Standard Library or a 3rd party library.
|
||||
* A callable method from either the Standard Library, a 3rd party library or from the source.
|
||||
*/
|
||||
class ExternalApi extends Callable {
|
||||
ExternalApi() { not this.fromSource() and not isUninteresting(this) }
|
||||
class CallableMethod extends Method {
|
||||
CallableMethod() { not isUninteresting(this) }
|
||||
|
||||
/**
|
||||
* Gets information about the external API in the form expected by the MaD modeling framework.
|
||||
*/
|
||||
string getApiName() {
|
||||
result =
|
||||
this.getDeclaringType().getPackage() + "." + this.getDeclaringType().getSourceDeclaration() +
|
||||
"#" + this.getName() + paramsString(this)
|
||||
this.getDeclaringType().getPackage() + "." + this.getDeclaringType().nestedName() + "#" +
|
||||
this.getName() + paramsString(this)
|
||||
}
|
||||
|
||||
private string getJarName() {
|
||||
@@ -122,50 +141,85 @@ class ExternalApi extends Callable {
|
||||
}
|
||||
}
|
||||
|
||||
/** DEPRECATED: Alias for ExternalApi */
|
||||
deprecated class ExternalAPI = ExternalApi;
|
||||
boolean isSupported(CallableMethod method) {
|
||||
method.isSupported() and result = true
|
||||
or
|
||||
not method.isSupported() and result = false
|
||||
}
|
||||
|
||||
// The below is a copy of https://github.com/github/codeql/blob/249f9f863db1e94e3c46ca85b49fb0ec32f8ca92/java/ql/lib/semmle/code/java/dataflow/internal/ModelExclusions.qll
|
||||
// to avoid the use of internal modules.
|
||||
/** Holds if the given package \`p\` is a test package. */
|
||||
pragma[nomagic]
|
||||
private predicate isTestPackage(Package p) {
|
||||
p.getName()
|
||||
.matches([
|
||||
"org.junit%", "junit.%", "org.mockito%", "org.assertj%",
|
||||
"com.github.tomakehurst.wiremock%", "org.hamcrest%", "org.springframework.test.%",
|
||||
"org.springframework.mock.%", "org.springframework.boot.test.%", "reactor.test%",
|
||||
"org.xmlunit%", "org.testcontainers.%", "org.opentest4j%", "org.mockserver%",
|
||||
"org.powermock%", "org.skyscreamer.jsonassert%", "org.rnorth.visibleassertions",
|
||||
"org.openqa.selenium%", "com.gargoylesoftware.htmlunit%", "org.jboss.arquillian.testng%",
|
||||
"org.testng%"
|
||||
])
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the limit for the number of results produced by a telemetry query.
|
||||
* A test library.
|
||||
*/
|
||||
int resultLimit() { result = 1000 }
|
||||
class TestLibrary extends RefType {
|
||||
TestLibrary() { isTestPackage(this.getPackage()) }
|
||||
}
|
||||
|
||||
/** Holds if the given file is a test file. */
|
||||
private predicate isInTestFile(File file) {
|
||||
file.getAbsolutePath().matches(["%/test/%", "%/guava-tests/%", "%/guava-testlib/%"]) and
|
||||
not file.getAbsolutePath().matches("%/ql/test/%") // allows our test cases to work
|
||||
}
|
||||
|
||||
/** Holds if the given compilation unit's package is a JDK internal. */
|
||||
private predicate isJdkInternal(CompilationUnit cu) {
|
||||
cu.getPackage().getName().matches("org.graalvm%") or
|
||||
cu.getPackage().getName().matches("com.sun%") or
|
||||
cu.getPackage().getName().matches("sun%") or
|
||||
cu.getPackage().getName().matches("jdk%") or
|
||||
cu.getPackage().getName().matches("java2d%") or
|
||||
cu.getPackage().getName().matches("build.tools%") or
|
||||
cu.getPackage().getName().matches("propertiesparser%") or
|
||||
cu.getPackage().getName().matches("org.jcp%") or
|
||||
cu.getPackage().getName().matches("org.w3c%") or
|
||||
cu.getPackage().getName().matches("org.ietf.jgss%") or
|
||||
cu.getPackage().getName().matches("org.xml.sax%") or
|
||||
cu.getPackage().getName().matches("com.oracle%") or
|
||||
cu.getPackage().getName().matches("org.omg%") or
|
||||
cu.getPackage().getName().matches("org.relaxng%") or
|
||||
cu.getPackage().getName() = "compileproperties" or
|
||||
cu.getPackage().getName() = "transparentruler" or
|
||||
cu.getPackage().getName() = "genstubs" or
|
||||
cu.getPackage().getName() = "netscape.javascript" or
|
||||
cu.getPackage().getName() = ""
|
||||
}
|
||||
|
||||
/** Holds if the given callable is not worth modeling. */
|
||||
predicate isUninterestingForModels(Callable c) {
|
||||
isInTestFile(c.getCompilationUnit().getFile()) or
|
||||
isJdkInternal(c.getCompilationUnit()) or
|
||||
c instanceof MainMethod or
|
||||
c instanceof StaticInitializer or
|
||||
exists(FunctionalExpr funcExpr | c = funcExpr.asMethod()) or
|
||||
c.getDeclaringType() instanceof TestLibrary or
|
||||
c.(Constructor).isParameterless()
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if it is relevant to count usages of \`api\`.
|
||||
* A class that represents all callables for which we might be
|
||||
* interested in having a MaD model.
|
||||
*/
|
||||
signature predicate relevantApi(ExternalApi api);
|
||||
|
||||
/**
|
||||
* Given a predicate to count relevant API usages, this module provides a predicate
|
||||
* for restricting the number or returned results based on a certain limit.
|
||||
*/
|
||||
module Results<relevantApi/1 getRelevantUsages> {
|
||||
private int getUsages(string apiName) {
|
||||
result =
|
||||
strictcount(Call c, ExternalApi api |
|
||||
c.getCallee().getSourceDeclaration() = api and
|
||||
not c.getFile() instanceof GeneratedFile and
|
||||
apiName = api.getApiName() and
|
||||
getRelevantUsages(api)
|
||||
)
|
||||
}
|
||||
|
||||
private int getOrder(string apiInfo) {
|
||||
apiInfo =
|
||||
rank[result](string info, int usages |
|
||||
usages = getUsages(info)
|
||||
|
|
||||
info order by usages desc, info
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if there exists an API with \`apiName\` that is being used \`usages\` times
|
||||
* and if it is in the top results (guarded by resultLimit).
|
||||
*/
|
||||
predicate restrict(string apiName, int usages) {
|
||||
usages = getUsages(apiName) and
|
||||
getOrder(apiName) <= resultLimit()
|
||||
class ModelApi extends SrcCallable {
|
||||
ModelApi() {
|
||||
this.fromSource() and
|
||||
this.isEffectivelyPublic() and
|
||||
not isUninterestingForModels(this)
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -1,16 +1,29 @@
|
||||
export type Query = {
|
||||
/**
|
||||
* The main query.
|
||||
* The application query.
|
||||
*
|
||||
* It should select all usages of external APIs, and return the following result pattern:
|
||||
* - usage: the usage of the external API. This is an entity.
|
||||
* - apiName: the name of the external API. This is a string.
|
||||
* - supported: whether the external API is supported by the extension. This should be a string representation of a boolean to satify the result pattern for a problem query.
|
||||
* - supported: whether the external API is modeled. This should be a string representation of a boolean to satify the result pattern for a problem query.
|
||||
* - "supported": a string literal. This is required to make the query a valid problem query.
|
||||
* - libraryName: the name of the library that contains the external API. This is a string and usually the basename of a file.
|
||||
* - "library": a string literal. This is required to make the query a valid problem query.
|
||||
*/
|
||||
mainQuery: string;
|
||||
applicationModeQuery: string;
|
||||
/**
|
||||
* The framework query.
|
||||
*
|
||||
* It should select all methods that are callable by applications, which is usually all public methods (and constructors).
|
||||
* The result pattern should be as follows:
|
||||
* - method: the method that is callable by applications. This is an entity.
|
||||
* - apiName: the name of the external API. This is a string.
|
||||
* - supported: whether this method is modeled. This should be a string representation of a boolean to satify the result pattern for a problem query.
|
||||
* - "supported": a string literal. This is required to make the query a valid problem query.
|
||||
* - libraryName: an arbitrary string. This is required to make it match the structure of the application query.
|
||||
* - "library": a string literal. This is required to make the query a valid problem query.
|
||||
*/
|
||||
frameworkModeQuery: string;
|
||||
dependencies?: {
|
||||
[filename: string]: string;
|
||||
};
|
||||
|
||||
@@ -8,8 +8,3 @@ export interface ExtensionPack {
|
||||
extensionTargets: Record<string, string>;
|
||||
dataExtensions: string[];
|
||||
}
|
||||
|
||||
export interface ExtensionPackModelFile {
|
||||
filename: string;
|
||||
extensionPack: ExtensionPack;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum Mode {
|
||||
Application = "application",
|
||||
Framework = "framework",
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ExternalApiUsage } from "../../data-extensions-editor/external-api-usage";
|
||||
import { ExternalApiUsage } from "../external-api-usage";
|
||||
|
||||
export function calculateModeledPercentage(
|
||||
externalApiUsages: Array<Pick<ExternalApiUsage, "supported">>,
|
||||
@@ -0,0 +1,88 @@
|
||||
import { ExternalApiUsage } from "../external-api-usage";
|
||||
import { Mode } from "./mode";
|
||||
import { calculateModeledPercentage } from "./modeled-percentage";
|
||||
|
||||
export function groupMethods(
|
||||
externalApiUsages: ExternalApiUsage[],
|
||||
mode: Mode,
|
||||
): Record<string, ExternalApiUsage[]> {
|
||||
const groupedByLibrary: Record<string, ExternalApiUsage[]> = {};
|
||||
|
||||
for (const externalApiUsage of externalApiUsages) {
|
||||
// Group by package if using framework mode
|
||||
const key =
|
||||
mode === Mode.Framework
|
||||
? externalApiUsage.packageName
|
||||
: externalApiUsage.library;
|
||||
|
||||
groupedByLibrary[key] ??= [];
|
||||
groupedByLibrary[key].push(externalApiUsage);
|
||||
}
|
||||
|
||||
return groupedByLibrary;
|
||||
}
|
||||
|
||||
export function sortGroupNames(
|
||||
methods: Record<string, ExternalApiUsage[]>,
|
||||
): string[] {
|
||||
return Object.keys(methods).sort((a, b) =>
|
||||
compareGroups(methods[a], a, methods[b], b),
|
||||
);
|
||||
}
|
||||
|
||||
export function sortMethods(
|
||||
externalApiUsages: ExternalApiUsage[],
|
||||
): ExternalApiUsage[] {
|
||||
const sortedExternalApiUsages = [...externalApiUsages];
|
||||
sortedExternalApiUsages.sort((a, b) => compareMethod(a, b));
|
||||
return sortedExternalApiUsages;
|
||||
}
|
||||
|
||||
function compareGroups(
|
||||
a: ExternalApiUsage[],
|
||||
aName: string,
|
||||
b: ExternalApiUsage[],
|
||||
bName: string,
|
||||
): number {
|
||||
const supportedPercentageA = calculateModeledPercentage(a);
|
||||
const supportedPercentageB = calculateModeledPercentage(b);
|
||||
|
||||
// Sort first by supported percentage ascending
|
||||
if (supportedPercentageA > supportedPercentageB) {
|
||||
return 1;
|
||||
}
|
||||
if (supportedPercentageA < supportedPercentageB) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const numberOfUsagesA = a.reduce((acc, curr) => acc + curr.usages.length, 0);
|
||||
const numberOfUsagesB = b.reduce((acc, curr) => acc + curr.usages.length, 0);
|
||||
|
||||
// If the number of usages is equal, sort by number of methods descending
|
||||
if (numberOfUsagesA === numberOfUsagesB) {
|
||||
const numberOfMethodsA = a.length;
|
||||
const numberOfMethodsB = b.length;
|
||||
|
||||
// If the number of methods is equal, sort by library name ascending
|
||||
if (numberOfMethodsA === numberOfMethodsB) {
|
||||
return aName.localeCompare(bName);
|
||||
}
|
||||
|
||||
return numberOfMethodsB - numberOfMethodsA;
|
||||
}
|
||||
|
||||
// Then sort by number of usages descending
|
||||
return numberOfUsagesB - numberOfUsagesA;
|
||||
}
|
||||
|
||||
function compareMethod(a: ExternalApiUsage, b: ExternalApiUsage): number {
|
||||
// Sort first by supported, putting unmodeled methods first.
|
||||
if (a.supported && !b.supported) {
|
||||
return 1;
|
||||
}
|
||||
if (!a.supported && b.supported) {
|
||||
return -1;
|
||||
}
|
||||
// Then sort by number of usages descending
|
||||
return b.usages.length - a.usages.length;
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import { ExtensionPackModelFile } from "./extension-pack";
|
||||
import { ExtensionPack } from "./extension-pack";
|
||||
import { Mode } from "./mode";
|
||||
|
||||
export interface DataExtensionEditorViewState {
|
||||
extensionPackModelFile: ExtensionPackModelFile;
|
||||
modelFileExists: boolean;
|
||||
extensionPack: ExtensionPack;
|
||||
enableFrameworkMode: boolean;
|
||||
showLlmButton: boolean;
|
||||
mode: Mode;
|
||||
}
|
||||
|
||||
@@ -1,38 +1,39 @@
|
||||
import Ajv from "ajv";
|
||||
|
||||
import { basename, extname } from "../common/path";
|
||||
import { ExternalApiUsage } from "./external-api-usage";
|
||||
import { ModeledMethod, ModeledMethodType } from "./modeled-method";
|
||||
import {
|
||||
ModeledMethod,
|
||||
ModeledMethodType,
|
||||
ModeledMethodWithSignature,
|
||||
} from "./modeled-method";
|
||||
import { extensiblePredicateDefinitions } from "./predicates";
|
||||
ExtensiblePredicateDefinition,
|
||||
extensiblePredicateDefinitions,
|
||||
ExternalApiUsageByType,
|
||||
} from "./predicates";
|
||||
|
||||
import * as dataSchemaJson from "./data-schema.json";
|
||||
import { sanitizeExtensionPackName } from "./extension-pack-name";
|
||||
|
||||
const ajv = new Ajv({ allErrors: true });
|
||||
const dataSchemaValidate = ajv.compile(dataSchemaJson);
|
||||
|
||||
type ExternalApiUsageByType = {
|
||||
type ModeledExternalApiUsage = {
|
||||
externalApiUsage: ExternalApiUsage;
|
||||
modeledMethod: ModeledMethod;
|
||||
};
|
||||
|
||||
type ExtensiblePredicateDefinition = {
|
||||
extensiblePredicate: string;
|
||||
generateMethodDefinition: (method: ExternalApiUsageByType) => any[];
|
||||
readModeledMethod: (row: any[]) => ModeledMethodWithSignature;
|
||||
modeledMethod?: ModeledMethod;
|
||||
};
|
||||
|
||||
function createDataProperty(
|
||||
methods: ExternalApiUsageByType[],
|
||||
methods: ModeledExternalApiUsage[],
|
||||
definition: ExtensiblePredicateDefinition,
|
||||
) {
|
||||
if (methods.length === 0) {
|
||||
return " []";
|
||||
}
|
||||
|
||||
return `\n${methods
|
||||
const modeledMethods = methods.filter(
|
||||
(method): method is ExternalApiUsageByType =>
|
||||
method.modeledMethod !== undefined,
|
||||
);
|
||||
|
||||
return `\n${modeledMethods
|
||||
.map(
|
||||
(method) =>
|
||||
` - ${JSON.stringify(
|
||||
@@ -44,12 +45,11 @@ function createDataProperty(
|
||||
|
||||
export function createDataExtensionYaml(
|
||||
language: string,
|
||||
externalApiUsages: ExternalApiUsage[],
|
||||
modeledMethods: Record<string, ModeledMethod>,
|
||||
modeledUsages: ModeledExternalApiUsage[],
|
||||
) {
|
||||
const methodsByType: Record<
|
||||
Exclude<ModeledMethodType, "none">,
|
||||
ExternalApiUsageByType[]
|
||||
ModeledExternalApiUsage[]
|
||||
> = {
|
||||
source: [],
|
||||
sink: [],
|
||||
@@ -57,14 +57,11 @@ export function createDataExtensionYaml(
|
||||
neutral: [],
|
||||
};
|
||||
|
||||
for (const externalApiUsage of externalApiUsages) {
|
||||
const modeledMethod = modeledMethods[externalApiUsage.signature];
|
||||
for (const modeledUsage of modeledUsages) {
|
||||
const { modeledMethod } = modeledUsage;
|
||||
|
||||
if (modeledMethod?.type && modeledMethod.type !== "none") {
|
||||
methodsByType[modeledMethod.type].push({
|
||||
externalApiUsage,
|
||||
modeledMethod,
|
||||
});
|
||||
methodsByType[modeledMethod.type].push(modeledUsage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +80,114 @@ export function createDataExtensionYaml(
|
||||
${extensions.join("\n")}`;
|
||||
}
|
||||
|
||||
export function createDataExtensionYamlsForApplicationMode(
|
||||
language: string,
|
||||
externalApiUsages: ExternalApiUsage[],
|
||||
modeledMethods: Record<string, ModeledMethod>,
|
||||
): Record<string, string> {
|
||||
const methodsByLibraryFilename: Record<string, ModeledExternalApiUsage[]> =
|
||||
{};
|
||||
|
||||
for (const externalApiUsage of externalApiUsages) {
|
||||
const modeledMethod = modeledMethods[externalApiUsage.signature];
|
||||
|
||||
const filename = createFilenameForLibrary(externalApiUsage.library);
|
||||
|
||||
methodsByLibraryFilename[filename] =
|
||||
methodsByLibraryFilename[filename] || [];
|
||||
methodsByLibraryFilename[filename].push({
|
||||
externalApiUsage,
|
||||
modeledMethod,
|
||||
});
|
||||
}
|
||||
|
||||
const result: Record<string, string> = {};
|
||||
|
||||
for (const [filename, methods] of Object.entries(methodsByLibraryFilename)) {
|
||||
const hasModeledMethods = methods.some(
|
||||
(method) => method.modeledMethod !== undefined,
|
||||
);
|
||||
if (!hasModeledMethods) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result[filename] = createDataExtensionYaml(language, methods);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function createDataExtensionYamlsForFrameworkMode(
|
||||
databaseName: string,
|
||||
language: string,
|
||||
externalApiUsages: ExternalApiUsage[],
|
||||
modeledMethods: Record<string, ModeledMethod>,
|
||||
prefix = "models/",
|
||||
suffix = ".model",
|
||||
): Record<string, string> {
|
||||
const parts = databaseName.split("/");
|
||||
const libraryName = parts
|
||||
.slice(1)
|
||||
.map((part) => sanitizeExtensionPackName(part))
|
||||
.join("-");
|
||||
|
||||
const methods = externalApiUsages.map((externalApiUsage) => ({
|
||||
externalApiUsage,
|
||||
modeledMethod: modeledMethods[externalApiUsage.signature],
|
||||
}));
|
||||
|
||||
return {
|
||||
[`${prefix}${libraryName}${suffix}.yml`]: createDataExtensionYaml(
|
||||
language,
|
||||
methods,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
// From the semver package using
|
||||
// const { re, t } = require("semver/internal/re");
|
||||
// console.log(re[t.LOOSE]);
|
||||
// Modified to remove the ^ and $ anchors
|
||||
// This will match any semver string at the end of a larger string
|
||||
const semverRegex =
|
||||
/[v=\s]*([0-9]+)\.([0-9]+)\.([0-9]+)(?:-?((?:[0-9]+|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:[0-9]+|\d*[a-zA-Z-][a-zA-Z0-9-]*))*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?/;
|
||||
|
||||
export function createFilenameForLibrary(
|
||||
library: string,
|
||||
prefix = "models/",
|
||||
suffix = ".model",
|
||||
) {
|
||||
let libraryName = basename(library);
|
||||
const extension = extname(libraryName);
|
||||
libraryName = libraryName.slice(0, -extension.length);
|
||||
|
||||
const match = semverRegex.exec(libraryName);
|
||||
|
||||
if (match !== null) {
|
||||
// Remove everything after the start of the match
|
||||
libraryName = libraryName.slice(0, match.index);
|
||||
}
|
||||
|
||||
// Lowercase everything
|
||||
libraryName = libraryName.toLowerCase();
|
||||
|
||||
// Replace all spaces and underscores with hyphens
|
||||
libraryName = libraryName.replaceAll(/[\s_]+/g, "-");
|
||||
|
||||
// Replace all characters which are not allowed by empty strings
|
||||
libraryName = libraryName.replaceAll(/[^a-z0-9.-]/g, "");
|
||||
|
||||
// Remove any leading or trailing hyphens or dots
|
||||
libraryName = libraryName.replaceAll(/^[.-]+|[.-]+$/g, "");
|
||||
|
||||
// Remove any duplicate hyphens
|
||||
libraryName = libraryName.replaceAll(/-{2,}/g, "-");
|
||||
// Remove any duplicate dots
|
||||
libraryName = libraryName.replaceAll(/\.{2,}/g, ".");
|
||||
|
||||
return `${prefix}${libraryName}${suffix}.yml`;
|
||||
}
|
||||
|
||||
export function loadDataExtensionYaml(
|
||||
data: any,
|
||||
): Record<string, ModeledMethod> | undefined {
|
||||
|
||||
@@ -36,12 +36,12 @@ import {
|
||||
import {
|
||||
AstViewer,
|
||||
install,
|
||||
spawnIdeServer,
|
||||
getQueryEditorCommands,
|
||||
TemplatePrintAstProvider,
|
||||
TemplatePrintCfgProvider,
|
||||
TemplateQueryDefinitionProvider,
|
||||
TemplateQueryReferenceProvider,
|
||||
createIDEServer,
|
||||
} from "./language-support";
|
||||
import { DatabaseManager } from "./databases/local-databases";
|
||||
import { DatabaseUI } from "./databases/local-databases-ui";
|
||||
@@ -903,24 +903,7 @@ async function activateWithInstalledDistribution(
|
||||
ctx.subscriptions.push(tmpDirDisposal);
|
||||
|
||||
void extLogger.log("Initializing CodeQL language server.");
|
||||
const client = new LanguageClient(
|
||||
"codeQL.lsp",
|
||||
"CodeQL Language Server",
|
||||
() => spawnIdeServer(qlConfigurationListener),
|
||||
{
|
||||
documentSelector: [
|
||||
{ language: "ql", scheme: "file" },
|
||||
{ language: "yaml", scheme: "file", pattern: "**/qlpack.yml" },
|
||||
{ language: "yaml", scheme: "file", pattern: "**/codeql-pack.yml" },
|
||||
],
|
||||
synchronize: {
|
||||
configurationSection: "codeQL",
|
||||
},
|
||||
// Ensure that language server exceptions are logged to the same channel as its output.
|
||||
outputChannel: ideServerLogger.outputChannel,
|
||||
},
|
||||
true,
|
||||
);
|
||||
const ideServer = createIDEServer(qlConfigurationListener);
|
||||
|
||||
const localQueries = new LocalQueries(
|
||||
app,
|
||||
@@ -1002,7 +985,7 @@ async function activateWithInstalledDistribution(
|
||||
void extLogger.log("Registering top-level command palette commands.");
|
||||
|
||||
const allCommands: AllExtensionCommands = {
|
||||
...getCommands(app, cliServer, qs, client),
|
||||
...getCommands(app, cliServer, qs, ideServer),
|
||||
...getQueryEditorCommands({
|
||||
commandManager: app.commands,
|
||||
queryRunner: qs,
|
||||
@@ -1048,12 +1031,23 @@ async function activateWithInstalledDistribution(
|
||||
}
|
||||
|
||||
void extLogger.log("Starting language server.");
|
||||
await client.start();
|
||||
await ideServer.start();
|
||||
ctx.subscriptions.push({
|
||||
dispose: () => {
|
||||
void client.stop();
|
||||
void ideServer.stop();
|
||||
},
|
||||
});
|
||||
|
||||
// Handle visibility changes in the ideserver
|
||||
if (await cliServer.cliConstraints.supportsVisibilityNotifications()) {
|
||||
Window.onDidChangeVisibleTextEditors((editors) => {
|
||||
ideServer.notifyVisibilityChange(editors);
|
||||
});
|
||||
// Send an inital notification to the language server
|
||||
// to set the initial state of the visible editors.
|
||||
ideServer.notifyVisibilityChange(Window.visibleTextEditors);
|
||||
}
|
||||
|
||||
// Jump-to-definition and find-references
|
||||
void extLogger.log("Registering jump-to-definition handlers.");
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { ProgressLocation, window } from "vscode";
|
||||
import { StreamInfo } from "vscode-languageclient/node";
|
||||
import { ProgressLocation, TextEditor, window } from "vscode";
|
||||
import {
|
||||
LanguageClient,
|
||||
NotificationType,
|
||||
StreamInfo,
|
||||
} from "vscode-languageclient/node";
|
||||
import { shouldDebugIdeServer, spawnServer } from "../codeql-cli/cli";
|
||||
import { QueryServerConfig } from "../config";
|
||||
import { ideServerLogger } from "../common/logging/vscode";
|
||||
@@ -8,10 +12,52 @@ import { ideServerLogger } from "../common/logging/vscode";
|
||||
* Managing the language server for CodeQL.
|
||||
*/
|
||||
|
||||
/** Starts a new CodeQL language server process, sending progress messages to the status bar. */
|
||||
export async function spawnIdeServer(
|
||||
/**
|
||||
* Create a new CodeQL language server.
|
||||
*/
|
||||
export function createIDEServer(
|
||||
config: QueryServerConfig,
|
||||
): Promise<StreamInfo> {
|
||||
): CodeQLLanguageClient {
|
||||
return new CodeQLLanguageClient(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* CodeQL language server.
|
||||
*/
|
||||
export class CodeQLLanguageClient extends LanguageClient {
|
||||
constructor(config: QueryServerConfig) {
|
||||
super(
|
||||
"codeQL.lsp",
|
||||
"CodeQL Language Server",
|
||||
() => spawnIdeServer(config),
|
||||
{
|
||||
documentSelector: [
|
||||
{ language: "ql", scheme: "file" },
|
||||
{ language: "yaml", scheme: "file", pattern: "**/qlpack.yml" },
|
||||
{ language: "yaml", scheme: "file", pattern: "**/codeql-pack.yml" },
|
||||
],
|
||||
synchronize: {
|
||||
configurationSection: "codeQL",
|
||||
},
|
||||
// Ensure that language server exceptions are logged to the same channel as its output.
|
||||
outputChannel: ideServerLogger.outputChannel,
|
||||
},
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
notifyVisibilityChange(editors: readonly TextEditor[]) {
|
||||
const files = editors
|
||||
.filter((e) => e.document.uri.scheme === "file")
|
||||
.map((e) => e.document.uri.toString());
|
||||
void this.sendNotification(didChangeVisibileFiles, {
|
||||
visibleFiles: files,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Starts a new CodeQL language server process, sending progress messages to the status bar. */
|
||||
async function spawnIdeServer(config: QueryServerConfig): Promise<StreamInfo> {
|
||||
return window.withProgress(
|
||||
{ title: "CodeQL language server", location: ProgressLocation.Window },
|
||||
async (progressReporter, _) => {
|
||||
@@ -37,3 +83,13 @@ export async function spawnIdeServer(
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom notification type for when the set of visible files changes.
|
||||
*/
|
||||
interface DidChangeVisibileFilesParams {
|
||||
visibleFiles: string[];
|
||||
}
|
||||
|
||||
const didChangeVisibileFiles: NotificationType<DidChangeVisibileFilesParams> =
|
||||
new NotificationType("textDocument/codeQLDidChangeVisibleFiles");
|
||||
|
||||
@@ -39,7 +39,7 @@ import {
|
||||
import { CompletedLocalQueryInfo, LocalQueryInfo } from "../query-results";
|
||||
import { WebviewReveal } from "./webview";
|
||||
import { asError, getErrorMessage } from "../common/helpers-pure";
|
||||
import { CodeQLCliServer } from "../codeql-cli/cli";
|
||||
import { CliVersionConstraint, CodeQLCliServer } from "../codeql-cli/cli";
|
||||
import { LocalQueryCommands } from "../common/commands";
|
||||
import { App } from "../common/app";
|
||||
import { DisposableObject } from "../common/disposable-object";
|
||||
@@ -47,6 +47,7 @@ import { SkeletonQueryWizard } from "../skeleton-query-wizard";
|
||||
import { LocalQueryRun } from "./local-query-run";
|
||||
import { createMultiSelectionCommand } from "../common/vscode/selection-commands";
|
||||
import { findLanguage } from "../codeql-cli/query-language";
|
||||
import type { QueryTreeViewItem } from "../queries-panel/query-tree-view-item";
|
||||
|
||||
interface DatabaseQuickPickItem extends QuickPickItem {
|
||||
databaseItem: DatabaseItem;
|
||||
@@ -80,10 +81,20 @@ export class LocalQueries extends DisposableObject {
|
||||
this.runQueryOnMultipleDatabases.bind(this),
|
||||
"codeQL.runQueryOnMultipleDatabasesContextEditor":
|
||||
this.runQueryOnMultipleDatabases.bind(this),
|
||||
"codeQLQueries.runLocalQueryFromQueriesPanel":
|
||||
this.runQueryFromQueriesPanel.bind(this),
|
||||
"codeQLQueries.runLocalQueryContextMenu":
|
||||
this.runQueryFromQueriesPanel.bind(this),
|
||||
"codeQLQueries.runLocalQueriesContextMenu":
|
||||
this.runQueriesFromQueriesPanel.bind(this),
|
||||
"codeQLQueries.runLocalQueriesFromPanel":
|
||||
this.runQueriesFromQueriesPanel.bind(this),
|
||||
"codeQL.runLocalQueryFromFileTab": this.runQuery.bind(this),
|
||||
"codeQL.runQueries": createMultiSelectionCommand(
|
||||
this.runQueries.bind(this),
|
||||
),
|
||||
"codeQL.quickEval": this.quickEval.bind(this),
|
||||
"codeQL.quickEvalCount": this.quickEvalCount.bind(this),
|
||||
"codeQL.quickEvalContextEditor": this.quickEval.bind(this),
|
||||
"codeQL.codeLensQuickEval": this.codeLensQuickEval.bind(this),
|
||||
"codeQL.quickQuery": this.quickQuery.bind(this),
|
||||
@@ -98,6 +109,21 @@ export class LocalQueries extends DisposableObject {
|
||||
};
|
||||
}
|
||||
|
||||
private async runQueryFromQueriesPanel(
|
||||
queryTreeViewItem: QueryTreeViewItem,
|
||||
): Promise<void> {
|
||||
await this.runQuery(Uri.file(queryTreeViewItem.path));
|
||||
}
|
||||
|
||||
private async runQueriesFromQueriesPanel(
|
||||
queryTreeViewItem: QueryTreeViewItem,
|
||||
): Promise<void> {
|
||||
const uris = queryTreeViewItem.children.map((child) =>
|
||||
Uri.file(child.path),
|
||||
);
|
||||
await this.runQueries(uris);
|
||||
}
|
||||
|
||||
private async runQuery(uri: Uri | undefined): Promise<void> {
|
||||
await withProgress(
|
||||
async (progress, token) => {
|
||||
@@ -211,6 +237,29 @@ export class LocalQueries extends DisposableObject {
|
||||
);
|
||||
}
|
||||
|
||||
private async quickEvalCount(uri: Uri): Promise<void> {
|
||||
await withProgress(
|
||||
async (progress, token) => {
|
||||
if (!(await this.cliServer.cliConstraints.supportsQuickEvalCount())) {
|
||||
throw new Error(
|
||||
`Quick evaluation count is only supported by CodeQL CLI v${CliVersionConstraint.CLI_VERSION_WITH_QUICK_EVAL_COUNT} or later.`,
|
||||
);
|
||||
}
|
||||
await this.compileAndRunQuery(
|
||||
QuickEvalType.QuickEvalCount,
|
||||
uri,
|
||||
progress,
|
||||
token,
|
||||
undefined,
|
||||
);
|
||||
},
|
||||
{
|
||||
title: "Running query",
|
||||
cancellable: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private async codeLensQuickEval(uri: Uri, range: Range): Promise<void> {
|
||||
await withProgress(
|
||||
async (progress, token) =>
|
||||
@@ -249,7 +298,7 @@ export class LocalQueries extends DisposableObject {
|
||||
* Gets the current active query.
|
||||
*
|
||||
* For now, the "active query" is just whatever query is in the active text editor. Once we have a
|
||||
* propery "queries" panel, we can provide a way to select the current query there.
|
||||
* proper "queries" panel, we can provide a way to select the current query there.
|
||||
*/
|
||||
public async getCurrentQuery(allowLibraryFiles: boolean): Promise<string> {
|
||||
const editor = window.activeTextEditor;
|
||||
|
||||
@@ -8,14 +8,27 @@ import { QueryDiscovery } from "./query-discovery";
|
||||
import { QueryPackDiscovery } from "./query-pack-discovery";
|
||||
|
||||
export class QueriesModule extends DisposableObject {
|
||||
private queriesPanel: QueriesPanel | undefined;
|
||||
|
||||
private constructor(readonly app: App) {
|
||||
super();
|
||||
}
|
||||
|
||||
public static initialize(
|
||||
app: App,
|
||||
cliServer: CodeQLCliServer,
|
||||
): QueriesModule {
|
||||
const queriesModule = new QueriesModule(app);
|
||||
app.subscriptions.push(queriesModule);
|
||||
|
||||
queriesModule.initialize(app, cliServer);
|
||||
return queriesModule;
|
||||
}
|
||||
|
||||
private initialize(app: App, cliServer: CodeQLCliServer): void {
|
||||
// Currently, we only want to expose the new panel when we are in canary mode
|
||||
// and the user has enabled the "Show queries panel" flag.
|
||||
if (!isCanary() || !showQueriesPanel()) {
|
||||
// Currently, we only want to expose the new panel when we are in canary mode
|
||||
// and the user has enabled the "Show queries panel" flag.
|
||||
return;
|
||||
}
|
||||
void extLogger.log("Initializing queries panel.");
|
||||
@@ -31,18 +44,7 @@ export class QueriesModule extends DisposableObject {
|
||||
this.push(queryDiscovery);
|
||||
void queryDiscovery.initialRefresh();
|
||||
|
||||
const queriesPanel = new QueriesPanel(queryDiscovery);
|
||||
this.push(queriesPanel);
|
||||
}
|
||||
|
||||
public static initialize(
|
||||
app: App,
|
||||
cliServer: CodeQLCliServer,
|
||||
): QueriesModule {
|
||||
const queriesModule = new QueriesModule(app);
|
||||
app.subscriptions.push(queriesModule);
|
||||
|
||||
queriesModule.initialize(app, cliServer);
|
||||
return queriesModule;
|
||||
this.queriesPanel = new QueriesPanel(queryDiscovery);
|
||||
this.push(this.queriesPanel);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as vscode from "vscode";
|
||||
import { DisposableObject } from "../common/disposable-object";
|
||||
import { QueryTreeDataProvider } from "./query-tree-data-provider";
|
||||
import { QueryDiscovery } from "./query-discovery";
|
||||
import { window } from "vscode";
|
||||
|
||||
export class QueriesPanel extends DisposableObject {
|
||||
public constructor(queryDiscovery: QueryDiscovery) {
|
||||
@@ -9,7 +9,7 @@ export class QueriesPanel extends DisposableObject {
|
||||
|
||||
const dataProvider = new QueryTreeDataProvider(queryDiscovery);
|
||||
|
||||
const treeView = vscode.window.createTreeView("codeQLQueries", {
|
||||
const treeView = window.createTreeView("codeQLQueries", {
|
||||
treeDataProvider: dataProvider,
|
||||
});
|
||||
this.push(treeView);
|
||||
|
||||
@@ -3,22 +3,24 @@ import * as vscode from "vscode";
|
||||
export class QueryTreeViewItem extends vscode.TreeItem {
|
||||
constructor(
|
||||
name: string,
|
||||
path: string,
|
||||
public readonly path: string,
|
||||
language: string | undefined,
|
||||
public readonly children: QueryTreeViewItem[],
|
||||
) {
|
||||
super(name);
|
||||
this.tooltip = path;
|
||||
this.description = language;
|
||||
this.collapsibleState = this.children.length
|
||||
? vscode.TreeItemCollapsibleState.Collapsed
|
||||
: vscode.TreeItemCollapsibleState.None;
|
||||
if (this.children.length === 0) {
|
||||
this.description = language;
|
||||
this.collapsibleState = vscode.TreeItemCollapsibleState.None;
|
||||
this.contextValue = "queryFile";
|
||||
this.command = {
|
||||
title: "Open",
|
||||
command: "vscode.open",
|
||||
arguments: [vscode.Uri.file(path)],
|
||||
};
|
||||
} else {
|
||||
this.collapsibleState = vscode.TreeItemCollapsibleState.Collapsed;
|
||||
this.contextValue = "queryFolder";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ export interface InitialQueryInfo {
|
||||
readonly queryText: string; // text of the selected file, or the selected text when doing quick eval
|
||||
readonly isQuickQuery: boolean;
|
||||
readonly isQuickEval: boolean;
|
||||
readonly isQuickEvalCount?: boolean; // Missing is false for backwards compatibility
|
||||
readonly quickEvalPosition?: messages.Position;
|
||||
readonly queryPath: string;
|
||||
readonly databaseInfo: DatabaseInfo;
|
||||
@@ -270,7 +271,9 @@ export class LocalQueryInfo {
|
||||
* - Otherwise, return the query file name.
|
||||
*/
|
||||
getQueryName() {
|
||||
if (this.initialInfo.quickEvalPosition) {
|
||||
if (this.initialInfo.isQuickEvalCount) {
|
||||
return `Quick evaluation counts of ${this.getQueryFileName()}`;
|
||||
} else if (this.initialInfo.isQuickEval) {
|
||||
return `Quick evaluation of ${this.getQueryFileName()}`;
|
||||
} else if (this.completedQuery?.query.metadata?.name) {
|
||||
return this.completedQuery?.query.metadata?.name;
|
||||
|
||||
@@ -493,8 +493,9 @@ type SaveBeforeStartMode =
|
||||
*/
|
||||
export async function saveBeforeStart(): Promise<void> {
|
||||
const mode: SaveBeforeStartMode =
|
||||
(VSCODE_SAVE_BEFORE_START_SETTING.getValue<string>() as SaveBeforeStartMode) ??
|
||||
"nonUntitledEditorsInActiveGroup";
|
||||
(VSCODE_SAVE_BEFORE_START_SETTING.getValue<string>({
|
||||
languageId: "ql",
|
||||
}) as SaveBeforeStartMode) ?? "nonUntitledEditorsInActiveGroup";
|
||||
|
||||
// Despite the names of the modes, the VS Code implementation doesn't restrict itself to the
|
||||
// current tab group. It saves all dirty files in all groups. We'll do the same.
|
||||
@@ -562,9 +563,12 @@ export async function createInitialQueryInfo(
|
||||
databaseInfo: DatabaseInfo,
|
||||
): Promise<InitialQueryInfo> {
|
||||
const isQuickEval = selectedQuery.quickEval !== undefined;
|
||||
const isQuickEvalCount =
|
||||
selectedQuery.quickEval?.quickEvalCount !== undefined;
|
||||
return {
|
||||
queryPath: selectedQuery.queryPath,
|
||||
isQuickEval,
|
||||
isQuickEvalCount,
|
||||
isQuickQuery: isQuickQueryPath(selectedQuery.queryPath),
|
||||
databaseInfo,
|
||||
id: `${basename(selectedQuery.queryPath)}-${nanoid()}`,
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as React from "react";
|
||||
|
||||
import { ComponentMeta, ComponentStory } from "@storybook/react";
|
||||
|
||||
import { Mode } from "../../data-extensions-editor/shared/mode";
|
||||
import { DataExtensionsEditor as DataExtensionsEditorComponent } from "../../view/data-extensions-editor/DataExtensionsEditor";
|
||||
|
||||
export default {
|
||||
@@ -16,21 +17,18 @@ const Template: ComponentStory<typeof DataExtensionsEditorComponent> = (
|
||||
export const DataExtensionsEditor = Template.bind({});
|
||||
DataExtensionsEditor.args = {
|
||||
initialViewState: {
|
||||
extensionPackModelFile: {
|
||||
extensionPack: {
|
||||
path: "/home/user/vscode-codeql-starter/codeql-custom-queries-java/sql2o",
|
||||
yamlPath:
|
||||
"/home/user/vscode-codeql-starter/codeql-custom-queries-java/sql2o/codeql-pack.yml",
|
||||
name: "codeql/sql2o-models",
|
||||
version: "0.0.0",
|
||||
extensionTargets: {},
|
||||
dataExtensions: [],
|
||||
},
|
||||
filename:
|
||||
"/home/user/vscode-codeql-starter/codeql-custom-queries-java/sql2o/models/sql2o.yml",
|
||||
extensionPack: {
|
||||
path: "/home/user/vscode-codeql-starter/codeql-custom-queries-java/sql2o",
|
||||
yamlPath:
|
||||
"/home/user/vscode-codeql-starter/codeql-custom-queries-java/sql2o/codeql-pack.yml",
|
||||
name: "codeql/sql2o-models",
|
||||
version: "0.0.0",
|
||||
extensionTargets: {},
|
||||
dataExtensions: [],
|
||||
},
|
||||
modelFileExists: true,
|
||||
enableFrameworkMode: true,
|
||||
showLlmButton: true,
|
||||
mode: Mode.Application,
|
||||
},
|
||||
initialExternalApiUsages: [
|
||||
{
|
||||
|
||||
@@ -76,6 +76,7 @@ import {
|
||||
showAndLogInformationMessage,
|
||||
showAndLogWarningMessage,
|
||||
} from "../common/logging";
|
||||
import type { QueryTreeViewItem } from "../queries-panel/query-tree-view-item";
|
||||
|
||||
const maxRetryCount = 3;
|
||||
|
||||
@@ -163,6 +164,8 @@ export class VariantAnalysisManager
|
||||
// Since we are tracking extension usage through commands, this command mirrors the "codeQL.runVariantAnalysis" command
|
||||
"codeQL.runVariantAnalysisContextEditor":
|
||||
this.runVariantAnalysisFromCommand.bind(this),
|
||||
"codeQLQueries.runVariantAnalysisContextMenu":
|
||||
this.runVariantAnalysisFromQueriesPanel.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -185,6 +188,12 @@ export class VariantAnalysisManager
|
||||
);
|
||||
}
|
||||
|
||||
private async runVariantAnalysisFromQueriesPanel(
|
||||
queryTreeViewItem: QueryTreeViewItem,
|
||||
): Promise<void> {
|
||||
await this.runVariantAnalysisFromCommand(Uri.file(queryTreeViewItem.path));
|
||||
}
|
||||
|
||||
public async runVariantAnalysis(
|
||||
uri: Uri | undefined,
|
||||
progress: ProgressCallback,
|
||||
|
||||
@@ -46,6 +46,12 @@ export default function CompareTable(props: Props) {
|
||||
<thead>
|
||||
<tr>
|
||||
<td>
|
||||
{/*
|
||||
eslint-disable-next-line
|
||||
jsx-a11y/anchor-is-valid,
|
||||
jsx-a11y/click-events-have-key-events,
|
||||
jsx-a11y/no-static-element-interactions
|
||||
*/}
|
||||
<a
|
||||
onClick={() => openQuery("from")}
|
||||
className="vscode-codeql__compare-open"
|
||||
@@ -54,6 +60,12 @@ export default function CompareTable(props: Props) {
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
{/*
|
||||
eslint-disable-next-line
|
||||
jsx-a11y/anchor-is-valid,
|
||||
jsx-a11y/click-events-have-key-events,
|
||||
jsx-a11y/no-static-element-interactions
|
||||
*/}
|
||||
<a
|
||||
onClick={() => openQuery("to")}
|
||||
className="vscode-codeql__compare-open"
|
||||
|
||||
@@ -10,13 +10,13 @@ import { ExternalApiUsage } from "../../data-extensions-editor/external-api-usag
|
||||
import { ModeledMethod } from "../../data-extensions-editor/modeled-method";
|
||||
import { assertNever } from "../../common/helpers-pure";
|
||||
import { vscode } from "../vscode-api";
|
||||
import { calculateModeledPercentage } from "./modeled";
|
||||
import { calculateModeledPercentage } from "../../data-extensions-editor/shared/modeled-percentage";
|
||||
import { LinkIconButton } from "../variant-analysis/LinkIconButton";
|
||||
import { basename } from "../common/path";
|
||||
import { ViewTitle } from "../common";
|
||||
import { DataExtensionEditorViewState } from "../../data-extensions-editor/shared/view-state";
|
||||
import { ModeledMethodsList } from "./ModeledMethodsList";
|
||||
import { percentFormatter } from "./formatters";
|
||||
import { Mode } from "../../data-extensions-editor/shared/mode";
|
||||
|
||||
const DataExtensionsEditorContainer = styled.div`
|
||||
margin-top: 1rem;
|
||||
@@ -28,12 +28,6 @@ const DetailsContainer = styled.div`
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const NonExistingModelFileContainer = styled.div`
|
||||
display: flex;
|
||||
gap: 0.2em;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const EditorContainer = styled.div`
|
||||
margin-top: 1rem;
|
||||
`;
|
||||
@@ -145,6 +139,12 @@ export function DataExtensionsEditor({
|
||||
[],
|
||||
);
|
||||
|
||||
const onRefreshClick = useCallback(() => {
|
||||
vscode.postMessage({
|
||||
t: "refreshExternalApiUsages",
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onApplyClick = useCallback(() => {
|
||||
vscode.postMessage({
|
||||
t: "saveModeledMethods",
|
||||
@@ -173,11 +173,15 @@ export function DataExtensionsEditor({
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onOpenModelFileClick = useCallback(() => {
|
||||
const onSwitchModeClick = useCallback(() => {
|
||||
const newMode =
|
||||
viewState?.mode === Mode.Framework ? Mode.Application : Mode.Framework;
|
||||
|
||||
vscode.postMessage({
|
||||
t: "openModelFile",
|
||||
t: "switchMode",
|
||||
mode: newMode,
|
||||
});
|
||||
}, []);
|
||||
}, [viewState?.mode]);
|
||||
|
||||
return (
|
||||
<DataExtensionsEditorContainer>
|
||||
@@ -192,26 +196,12 @@ export function DataExtensionsEditor({
|
||||
<>
|
||||
<ViewTitle>Data extensions editor</ViewTitle>
|
||||
<DetailsContainer>
|
||||
{viewState?.extensionPackModelFile && (
|
||||
{viewState?.extensionPack && (
|
||||
<>
|
||||
<LinkIconButton onClick={onOpenExtensionPackClick}>
|
||||
<span slot="start" className="codicon codicon-package"></span>
|
||||
{viewState.extensionPackModelFile.extensionPack.name}
|
||||
{viewState.extensionPack.name}
|
||||
</LinkIconButton>
|
||||
{viewState.modelFileExists ? (
|
||||
<LinkIconButton onClick={onOpenModelFileClick}>
|
||||
<span
|
||||
slot="start"
|
||||
className="codicon codicon-file-code"
|
||||
></span>
|
||||
{basename(viewState.extensionPackModelFile.filename)}
|
||||
</LinkIconButton>
|
||||
) : (
|
||||
<NonExistingModelFileContainer>
|
||||
<span className="codicon codicon-file-code"></span>
|
||||
{basename(viewState.extensionPackModelFile.filename)}
|
||||
</NonExistingModelFileContainer>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<div>
|
||||
@@ -220,13 +210,39 @@ export function DataExtensionsEditor({
|
||||
<div>
|
||||
{percentFormatter.format(unModeledPercentage / 100)} unmodeled
|
||||
</div>
|
||||
{viewState?.enableFrameworkMode && (
|
||||
<>
|
||||
<div>
|
||||
Mode:{" "}
|
||||
{viewState?.mode === Mode.Framework
|
||||
? "Framework"
|
||||
: "Application"}
|
||||
</div>
|
||||
<div>
|
||||
<LinkIconButton onClick={onSwitchModeClick}>
|
||||
<span
|
||||
slot="start"
|
||||
className="codicon codicon-library"
|
||||
></span>
|
||||
Switch mode
|
||||
</LinkIconButton>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</DetailsContainer>
|
||||
|
||||
<EditorContainer>
|
||||
<ButtonsContainer>
|
||||
<VSCodeButton onClick={onApplyClick}>Apply</VSCodeButton>
|
||||
{viewState?.enableFrameworkMode && (
|
||||
<VSCodeButton appearance="secondary" onClick={onRefreshClick}>
|
||||
Refresh
|
||||
</VSCodeButton>
|
||||
)}
|
||||
<VSCodeButton onClick={onGenerateClick}>
|
||||
Download and generate
|
||||
{viewState?.mode === Mode.Framework
|
||||
? "Generate"
|
||||
: "Download and generate"}
|
||||
</VSCodeButton>
|
||||
{viewState?.showLlmButton && (
|
||||
<>
|
||||
@@ -239,6 +255,7 @@ export function DataExtensionsEditor({
|
||||
<ModeledMethodsList
|
||||
externalApiUsages={externalApiUsages}
|
||||
modeledMethods={modeledMethods}
|
||||
mode={viewState?.mode ?? Mode.Application}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</EditorContainer>
|
||||
|
||||
@@ -5,9 +5,10 @@ import { ExternalApiUsage } from "../../data-extensions-editor/external-api-usag
|
||||
import { ModeledMethod } from "../../data-extensions-editor/modeled-method";
|
||||
import { pluralize } from "../../common/word";
|
||||
import { ModeledMethodDataGrid } from "./ModeledMethodDataGrid";
|
||||
import { calculateModeledPercentage } from "./modeled";
|
||||
import { calculateModeledPercentage } from "../../data-extensions-editor/shared/modeled-percentage";
|
||||
import { decimalFormatter, percentFormatter } from "./formatters";
|
||||
import { Codicon } from "../common";
|
||||
import { Mode } from "../../data-extensions-editor/shared/mode";
|
||||
|
||||
const LibraryContainer = styled.div`
|
||||
margin-bottom: 1rem;
|
||||
@@ -38,9 +39,10 @@ const StatusContainer = styled.div`
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
libraryName: string;
|
||||
title: string;
|
||||
externalApiUsages: ExternalApiUsage[];
|
||||
modeledMethods: Record<string, ModeledMethod>;
|
||||
mode: Mode;
|
||||
onChange: (
|
||||
externalApiUsage: ExternalApiUsage,
|
||||
modeledMethod: ModeledMethod,
|
||||
@@ -48,9 +50,10 @@ type Props = {
|
||||
};
|
||||
|
||||
export const LibraryRow = ({
|
||||
libraryName,
|
||||
title,
|
||||
externalApiUsages,
|
||||
modeledMethods,
|
||||
mode,
|
||||
onChange,
|
||||
}: Props) => {
|
||||
const modeledPercentage = useMemo(() => {
|
||||
@@ -75,7 +78,7 @@ export const LibraryRow = ({
|
||||
) : (
|
||||
<Codicon name="chevron-right" label="Expand" />
|
||||
)}
|
||||
{libraryName}
|
||||
{title}
|
||||
{isExpanded ? null : (
|
||||
<>
|
||||
{" "}
|
||||
@@ -116,6 +119,7 @@ export const LibraryRow = ({
|
||||
<ModeledMethodDataGrid
|
||||
externalApiUsages={externalApiUsages}
|
||||
modeledMethods={modeledMethods}
|
||||
mode={mode}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from "../../data-extensions-editor/modeled-method";
|
||||
import { KindInput } from "./KindInput";
|
||||
import { extensiblePredicateDefinitions } from "../../data-extensions-editor/predicates";
|
||||
import { Mode } from "../../data-extensions-editor/shared/mode";
|
||||
|
||||
const Dropdown = styled(VSCodeDropdown)`
|
||||
width: 100%;
|
||||
@@ -37,6 +38,25 @@ const SupportSpan = styled.span<SupportedUnsupportedSpanProps>`
|
||||
}};
|
||||
`;
|
||||
|
||||
type SupportedUnsupportedLinkProps = {
|
||||
supported: boolean;
|
||||
modeled: ModeledMethod | undefined;
|
||||
};
|
||||
|
||||
const SupportLink = styled.button<SupportedUnsupportedLinkProps>`
|
||||
color: ${(props) => {
|
||||
if (!props.supported && props.modeled && props.modeled?.type !== "none") {
|
||||
return "orange";
|
||||
} else {
|
||||
return props.supported ? "green" : "red";
|
||||
}
|
||||
}};
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
`;
|
||||
|
||||
const UsagesButton = styled.button`
|
||||
color: var(--vscode-editor-foreground);
|
||||
background-color: transparent;
|
||||
@@ -47,6 +67,7 @@ const UsagesButton = styled.button`
|
||||
type Props = {
|
||||
externalApiUsage: ExternalApiUsage;
|
||||
modeledMethod: ModeledMethod | undefined;
|
||||
mode: Mode;
|
||||
onChange: (
|
||||
externalApiUsage: ExternalApiUsage,
|
||||
modeledMethod: ModeledMethod,
|
||||
@@ -56,6 +77,7 @@ type Props = {
|
||||
export const MethodRow = ({
|
||||
externalApiUsage,
|
||||
modeledMethod,
|
||||
mode,
|
||||
onChange,
|
||||
}: Props) => {
|
||||
const argumentsList = useMemo(() => {
|
||||
@@ -137,6 +159,7 @@ export const MethodRow = ({
|
||||
const jumpToUsage = useCallback(() => {
|
||||
vscode.postMessage({
|
||||
t: "jumpToUsage",
|
||||
// In framework mode, the first and only usage is the definition of the method
|
||||
location: externalApiUsage.usages[0].url,
|
||||
});
|
||||
}, [externalApiUsage]);
|
||||
@@ -157,19 +180,33 @@ export const MethodRow = ({
|
||||
</SupportSpan>
|
||||
</VSCodeDataGridCell>
|
||||
<VSCodeDataGridCell gridColumn={2}>
|
||||
<SupportSpan
|
||||
supported={externalApiUsage.supported}
|
||||
modeled={modeledMethod}
|
||||
>
|
||||
{externalApiUsage.methodName}
|
||||
{externalApiUsage.methodParameters}
|
||||
</SupportSpan>
|
||||
</VSCodeDataGridCell>
|
||||
<VSCodeDataGridCell gridColumn={3}>
|
||||
<UsagesButton onClick={jumpToUsage}>
|
||||
{externalApiUsage.usages.length}
|
||||
</UsagesButton>
|
||||
{mode === Mode.Application && (
|
||||
<SupportSpan
|
||||
supported={externalApiUsage.supported}
|
||||
modeled={modeledMethod}
|
||||
>
|
||||
{externalApiUsage.methodName}
|
||||
{externalApiUsage.methodParameters}
|
||||
</SupportSpan>
|
||||
)}
|
||||
{mode === Mode.Framework && (
|
||||
<SupportLink
|
||||
supported={externalApiUsage.supported}
|
||||
modeled={modeledMethod}
|
||||
onClick={jumpToUsage}
|
||||
>
|
||||
{externalApiUsage.methodName}
|
||||
{externalApiUsage.methodParameters}
|
||||
</SupportLink>
|
||||
)}
|
||||
</VSCodeDataGridCell>
|
||||
{mode === Mode.Application && (
|
||||
<VSCodeDataGridCell gridColumn={3}>
|
||||
<UsagesButton onClick={jumpToUsage}>
|
||||
{externalApiUsage.usages.length}
|
||||
</UsagesButton>
|
||||
</VSCodeDataGridCell>
|
||||
)}
|
||||
<VSCodeDataGridCell gridColumn={4}>
|
||||
{(!externalApiUsage.supported ||
|
||||
(modeledMethod && modeledMethod?.type !== "none")) && (
|
||||
|
||||
@@ -8,10 +8,13 @@ import { MethodRow } from "./MethodRow";
|
||||
import { ExternalApiUsage } from "../../data-extensions-editor/external-api-usage";
|
||||
import { ModeledMethod } from "../../data-extensions-editor/modeled-method";
|
||||
import { useMemo } from "react";
|
||||
import { Mode } from "../../data-extensions-editor/shared/mode";
|
||||
import { sortMethods } from "../../data-extensions-editor/shared/sorting";
|
||||
|
||||
type Props = {
|
||||
externalApiUsages: ExternalApiUsage[];
|
||||
modeledMethods: Record<string, ModeledMethod>;
|
||||
mode: Mode;
|
||||
onChange: (
|
||||
externalApiUsage: ExternalApiUsage,
|
||||
modeledMethod: ModeledMethod,
|
||||
@@ -21,23 +24,13 @@ type Props = {
|
||||
export const ModeledMethodDataGrid = ({
|
||||
externalApiUsages,
|
||||
modeledMethods,
|
||||
mode,
|
||||
onChange,
|
||||
}: Props) => {
|
||||
const sortedExternalApiUsages = useMemo(() => {
|
||||
const sortedExternalApiUsages = [...externalApiUsages];
|
||||
sortedExternalApiUsages.sort((a, b) => {
|
||||
// Sort first by supported, putting unmodeled methods first.
|
||||
if (a.supported && !b.supported) {
|
||||
return 1;
|
||||
}
|
||||
if (!a.supported && b.supported) {
|
||||
return -1;
|
||||
}
|
||||
// Then sort by number of usages descending
|
||||
return b.usages.length - a.usages.length;
|
||||
});
|
||||
return sortedExternalApiUsages;
|
||||
}, [externalApiUsages]);
|
||||
const sortedExternalApiUsages = useMemo(
|
||||
() => sortMethods(externalApiUsages),
|
||||
[externalApiUsages],
|
||||
);
|
||||
|
||||
return (
|
||||
<VSCodeDataGrid>
|
||||
@@ -48,9 +41,11 @@ export const ModeledMethodDataGrid = ({
|
||||
<VSCodeDataGridCell cellType="columnheader" gridColumn={2}>
|
||||
Method
|
||||
</VSCodeDataGridCell>
|
||||
<VSCodeDataGridCell cellType="columnheader" gridColumn={3}>
|
||||
Usages
|
||||
</VSCodeDataGridCell>
|
||||
{mode === Mode.Application && (
|
||||
<VSCodeDataGridCell cellType="columnheader" gridColumn={3}>
|
||||
Usages
|
||||
</VSCodeDataGridCell>
|
||||
)}
|
||||
<VSCodeDataGridCell cellType="columnheader" gridColumn={4}>
|
||||
Model type
|
||||
</VSCodeDataGridCell>
|
||||
@@ -69,6 +64,7 @@ export const ModeledMethodDataGrid = ({
|
||||
key={externalApiUsage.signature}
|
||||
externalApiUsage={externalApiUsage}
|
||||
modeledMethod={modeledMethods[externalApiUsage.signature]}
|
||||
mode={mode}
|
||||
onChange={onChange}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -2,12 +2,17 @@ import * as React from "react";
|
||||
import { useMemo } from "react";
|
||||
import { ExternalApiUsage } from "../../data-extensions-editor/external-api-usage";
|
||||
import { ModeledMethod } from "../../data-extensions-editor/modeled-method";
|
||||
import { calculateModeledPercentage } from "./modeled";
|
||||
import { LibraryRow } from "./LibraryRow";
|
||||
import { Mode } from "../../data-extensions-editor/shared/mode";
|
||||
import {
|
||||
groupMethods,
|
||||
sortGroupNames,
|
||||
} from "../../data-extensions-editor/shared/sorting";
|
||||
|
||||
type Props = {
|
||||
externalApiUsages: ExternalApiUsage[];
|
||||
modeledMethods: Record<string, ModeledMethod>;
|
||||
mode: Mode;
|
||||
onChange: (
|
||||
externalApiUsage: ExternalApiUsage,
|
||||
modeledMethod: ModeledMethod,
|
||||
@@ -17,71 +22,25 @@ type Props = {
|
||||
export const ModeledMethodsList = ({
|
||||
externalApiUsages,
|
||||
modeledMethods,
|
||||
mode,
|
||||
onChange,
|
||||
}: Props) => {
|
||||
const groupedByLibrary = useMemo(() => {
|
||||
const groupedByLibrary: Record<string, ExternalApiUsage[]> = {};
|
||||
const grouped = useMemo(
|
||||
() => groupMethods(externalApiUsages, mode),
|
||||
[externalApiUsages, mode],
|
||||
);
|
||||
|
||||
for (const externalApiUsage of externalApiUsages) {
|
||||
groupedByLibrary[externalApiUsage.library] ??= [];
|
||||
groupedByLibrary[externalApiUsage.library].push(externalApiUsage);
|
||||
}
|
||||
|
||||
return groupedByLibrary;
|
||||
}, [externalApiUsages]);
|
||||
|
||||
const sortedLibraryNames = useMemo(() => {
|
||||
return Object.keys(groupedByLibrary).sort((a, b) => {
|
||||
const supportedPercentageA = calculateModeledPercentage(
|
||||
groupedByLibrary[a],
|
||||
);
|
||||
const supportedPercentageB = calculateModeledPercentage(
|
||||
groupedByLibrary[b],
|
||||
);
|
||||
|
||||
// Sort first by supported percentage ascending
|
||||
if (supportedPercentageA > supportedPercentageB) {
|
||||
return 1;
|
||||
}
|
||||
if (supportedPercentageA < supportedPercentageB) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const numberOfUsagesA = groupedByLibrary[a].reduce(
|
||||
(acc, curr) => acc + curr.usages.length,
|
||||
0,
|
||||
);
|
||||
const numberOfUsagesB = groupedByLibrary[b].reduce(
|
||||
(acc, curr) => acc + curr.usages.length,
|
||||
0,
|
||||
);
|
||||
|
||||
// If the number of usages is equal, sort by number of methods descending
|
||||
if (numberOfUsagesA === numberOfUsagesB) {
|
||||
const numberOfMethodsA = groupedByLibrary[a].length;
|
||||
const numberOfMethodsB = groupedByLibrary[b].length;
|
||||
|
||||
// If the number of methods is equal, sort by library name ascending
|
||||
if (numberOfMethodsA === numberOfMethodsB) {
|
||||
return a.localeCompare(b);
|
||||
}
|
||||
|
||||
return numberOfMethodsB - numberOfMethodsA;
|
||||
}
|
||||
|
||||
// Then sort by number of usages descending
|
||||
return numberOfUsagesB - numberOfUsagesA;
|
||||
});
|
||||
}, [groupedByLibrary]);
|
||||
const sortedGroupNames = useMemo(() => sortGroupNames(grouped), [grouped]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{sortedLibraryNames.map((libraryName) => (
|
||||
{sortedGroupNames.map((libraryName) => (
|
||||
<LibraryRow
|
||||
key={libraryName}
|
||||
libraryName={libraryName}
|
||||
externalApiUsages={groupedByLibrary[libraryName]}
|
||||
title={libraryName}
|
||||
externalApiUsages={grouped[libraryName]}
|
||||
modeledMethods={modeledMethods}
|
||||
mode={mode}
|
||||
onChange={onChange}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -111,6 +111,10 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
|
||||
return (
|
||||
<span>
|
||||
No Alerts. See{" "}
|
||||
{/*
|
||||
eslint-disable-next-line
|
||||
jsx-a11y/anchor-is-valid,
|
||||
*/}
|
||||
<a href="#" onClick={this.props.showRawResults}>
|
||||
raw results
|
||||
</a>
|
||||
@@ -316,6 +320,10 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
|
||||
{...selectableZebraStripe(resultRowIsSelected, resultIndex)}
|
||||
key={resultIndex}
|
||||
>
|
||||
{/*
|
||||
eslint-disable-next-line
|
||||
jsx-a11y/no-noninteractive-element-interactions
|
||||
*/}
|
||||
<td
|
||||
className="vscode-codeql__icon-cell vscode-codeql__dropdown-cell"
|
||||
onMouseDown={toggler(indices)}
|
||||
@@ -353,6 +361,10 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
|
||||
<td className="vscode-codeql__icon-cell">
|
||||
<span className="vscode-codeql__vertical-rule"></span>
|
||||
</td>
|
||||
{/*
|
||||
eslint-disable-next-line
|
||||
jsx-a11y/no-noninteractive-element-interactions
|
||||
*/}
|
||||
<td
|
||||
className="vscode-codeql__icon-cell vscode-codeql__dropdown-cell"
|
||||
onMouseDown={toggler([pathKey])}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
ResultTableProps,
|
||||
className,
|
||||
emptyQueryResultsMessage,
|
||||
jumpToLocation,
|
||||
@@ -19,158 +19,158 @@ import { onNavigation } from "./results";
|
||||
import { tryGetResolvableLocation } from "../../common/bqrs-utils";
|
||||
import { ScrollIntoViewHelper } from "./scroll-into-view-helper";
|
||||
import { sendTelemetry } from "../common/telemetry";
|
||||
import { assertNever } from "../../common/helpers-pure";
|
||||
|
||||
export type RawTableProps = ResultTableProps & {
|
||||
export type RawTableProps = {
|
||||
databaseUri: string;
|
||||
resultSet: RawTableResultSet;
|
||||
sortState?: RawResultsSortState;
|
||||
offset: number;
|
||||
};
|
||||
|
||||
interface RawTableState {
|
||||
selectedItem?: { row: number; column: number };
|
||||
interface TableItem {
|
||||
readonly row: number;
|
||||
readonly column: number;
|
||||
}
|
||||
|
||||
export class RawTable extends React.Component<RawTableProps, RawTableState> {
|
||||
private scroller = new ScrollIntoViewHelper();
|
||||
export function RawTable({
|
||||
databaseUri,
|
||||
resultSet,
|
||||
sortState,
|
||||
offset,
|
||||
}: RawTableProps) {
|
||||
const [selectedItem, setSelectedItem] = useState<TableItem | undefined>();
|
||||
|
||||
constructor(props: RawTableProps) {
|
||||
super(props);
|
||||
this.setSelection = this.setSelection.bind(this);
|
||||
this.handleNavigationEvent = this.handleNavigationEvent.bind(this);
|
||||
this.state = {};
|
||||
const scroller = useRef<ScrollIntoViewHelper | undefined>(undefined);
|
||||
if (scroller.current === undefined) {
|
||||
scroller.current = new ScrollIntoViewHelper();
|
||||
}
|
||||
useEffect(() => scroller.current?.update());
|
||||
|
||||
private setSelection(row: number, column: number) {
|
||||
this.setState((prev) => ({
|
||||
...prev,
|
||||
selectedItem: { row, column },
|
||||
}));
|
||||
const setSelection = useCallback((row: number, column: number): void => {
|
||||
setSelectedItem({ row, column });
|
||||
sendTelemetry("local-results-raw-results-table-selected");
|
||||
}, []);
|
||||
|
||||
const navigateWithDelta = useCallback(
|
||||
(rowDelta: number, columnDelta: number): void => {
|
||||
setSelectedItem((prevSelectedItem) => {
|
||||
const numberOfAlerts = resultSet.rows.length;
|
||||
if (numberOfAlerts === 0) {
|
||||
return prevSelectedItem;
|
||||
}
|
||||
const currentRow = prevSelectedItem?.row;
|
||||
const nextRow = currentRow === undefined ? 0 : currentRow + rowDelta;
|
||||
if (nextRow < 0 || nextRow >= numberOfAlerts) {
|
||||
return prevSelectedItem;
|
||||
}
|
||||
const currentColumn = prevSelectedItem?.column;
|
||||
const nextColumn =
|
||||
currentColumn === undefined ? 0 : currentColumn + columnDelta;
|
||||
// Jump to the location of the new cell
|
||||
const rowData = resultSet.rows[nextRow];
|
||||
if (nextColumn < 0 || nextColumn >= rowData.length) {
|
||||
return prevSelectedItem;
|
||||
}
|
||||
const cellData = rowData[nextColumn];
|
||||
if (cellData != null && typeof cellData === "object") {
|
||||
const location = tryGetResolvableLocation(cellData.url);
|
||||
if (location !== undefined) {
|
||||
jumpToLocation(location, databaseUri);
|
||||
}
|
||||
}
|
||||
scroller.current?.scrollIntoViewOnNextUpdate();
|
||||
return { row: nextRow, column: nextColumn };
|
||||
});
|
||||
},
|
||||
[databaseUri, resultSet, scroller],
|
||||
);
|
||||
|
||||
const handleNavigationEvent = useCallback(
|
||||
(event: NavigateMsg) => {
|
||||
switch (event.direction) {
|
||||
case NavigationDirection.up: {
|
||||
navigateWithDelta(-1, 0);
|
||||
break;
|
||||
}
|
||||
case NavigationDirection.down: {
|
||||
navigateWithDelta(1, 0);
|
||||
break;
|
||||
}
|
||||
case NavigationDirection.left: {
|
||||
navigateWithDelta(0, -1);
|
||||
break;
|
||||
}
|
||||
case NavigationDirection.right: {
|
||||
navigateWithDelta(0, 1);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
assertNever(event.direction);
|
||||
}
|
||||
},
|
||||
[navigateWithDelta],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
onNavigation.addListener(handleNavigationEvent);
|
||||
return () => {
|
||||
onNavigation.removeListener(handleNavigationEvent);
|
||||
};
|
||||
}, [handleNavigationEvent]);
|
||||
|
||||
const [dataRows, numTruncatedResults] = useMemo(() => {
|
||||
if (resultSet.rows.length <= RAW_RESULTS_LIMIT) {
|
||||
return [resultSet.rows, 0];
|
||||
}
|
||||
return [
|
||||
resultSet.rows.slice(0, RAW_RESULTS_LIMIT),
|
||||
resultSet.rows.length - RAW_RESULTS_LIMIT,
|
||||
];
|
||||
}, [resultSet]);
|
||||
|
||||
if (dataRows.length === 0) {
|
||||
return emptyQueryResultsMessage();
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
const { resultSet, databaseUri } = this.props;
|
||||
const tableRows = dataRows.map((row: ResultRow, rowIndex: number) => (
|
||||
<RawTableRow
|
||||
key={rowIndex}
|
||||
rowIndex={rowIndex + offset}
|
||||
row={row}
|
||||
databaseUri={databaseUri}
|
||||
selectedColumn={
|
||||
selectedItem?.row === rowIndex ? selectedItem?.column : undefined
|
||||
}
|
||||
onSelected={setSelection}
|
||||
scroller={scroller.current}
|
||||
/>
|
||||
));
|
||||
|
||||
let dataRows = resultSet.rows;
|
||||
if (dataRows.length === 0) {
|
||||
return emptyQueryResultsMessage();
|
||||
}
|
||||
|
||||
let numTruncatedResults = 0;
|
||||
if (dataRows.length > RAW_RESULTS_LIMIT) {
|
||||
numTruncatedResults = dataRows.length - RAW_RESULTS_LIMIT;
|
||||
dataRows = dataRows.slice(0, RAW_RESULTS_LIMIT);
|
||||
}
|
||||
|
||||
const tableRows = dataRows.map((row: ResultRow, rowIndex: number) => (
|
||||
<RawTableRow
|
||||
key={rowIndex}
|
||||
rowIndex={rowIndex + this.props.offset}
|
||||
row={row}
|
||||
databaseUri={databaseUri}
|
||||
selectedColumn={
|
||||
this.state.selectedItem?.row === rowIndex
|
||||
? this.state.selectedItem?.column
|
||||
: undefined
|
||||
}
|
||||
onSelected={this.setSelection}
|
||||
scroller={this.scroller}
|
||||
/>
|
||||
));
|
||||
|
||||
if (numTruncatedResults > 0) {
|
||||
const colSpan = dataRows[0].length + 1; // one row for each data column, plus index column
|
||||
tableRows.push(
|
||||
<tr>
|
||||
<td
|
||||
key={"message"}
|
||||
colSpan={colSpan}
|
||||
style={{ textAlign: "center", fontStyle: "italic" }}
|
||||
>
|
||||
Too many results to show at once. {numTruncatedResults} result(s)
|
||||
omitted.
|
||||
</td>
|
||||
</tr>,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<table className={className}>
|
||||
<RawTableHeader
|
||||
columns={resultSet.schema.columns}
|
||||
schemaName={resultSet.schema.name}
|
||||
sortState={this.props.sortState}
|
||||
/>
|
||||
<tbody>{tableRows}</tbody>
|
||||
</table>
|
||||
if (numTruncatedResults > 0) {
|
||||
const colSpan = dataRows[0].length + 1; // one row for each data column, plus index column
|
||||
tableRows.push(
|
||||
<tr>
|
||||
<td
|
||||
key={"message"}
|
||||
colSpan={colSpan}
|
||||
style={{ textAlign: "center", fontStyle: "italic" }}
|
||||
>
|
||||
Too many results to show at once. {numTruncatedResults} result(s)
|
||||
omitted.
|
||||
</td>
|
||||
</tr>,
|
||||
);
|
||||
}
|
||||
|
||||
private handleNavigationEvent(event: NavigateMsg) {
|
||||
switch (event.direction) {
|
||||
case NavigationDirection.up: {
|
||||
this.navigateWithDelta(-1, 0);
|
||||
break;
|
||||
}
|
||||
case NavigationDirection.down: {
|
||||
this.navigateWithDelta(1, 0);
|
||||
break;
|
||||
}
|
||||
case NavigationDirection.left: {
|
||||
this.navigateWithDelta(0, -1);
|
||||
break;
|
||||
}
|
||||
case NavigationDirection.right: {
|
||||
this.navigateWithDelta(0, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private navigateWithDelta(rowDelta: number, columnDelta: number) {
|
||||
this.setState((prevState) => {
|
||||
const numberOfAlerts = this.props.resultSet.rows.length;
|
||||
if (numberOfAlerts === 0) {
|
||||
return prevState;
|
||||
}
|
||||
const currentRow = prevState.selectedItem?.row;
|
||||
const nextRow = currentRow === undefined ? 0 : currentRow + rowDelta;
|
||||
if (nextRow < 0 || nextRow >= numberOfAlerts) {
|
||||
return prevState;
|
||||
}
|
||||
const currentColumn = prevState.selectedItem?.column;
|
||||
const nextColumn =
|
||||
currentColumn === undefined ? 0 : currentColumn + columnDelta;
|
||||
// Jump to the location of the new cell
|
||||
const rowData = this.props.resultSet.rows[nextRow];
|
||||
if (nextColumn < 0 || nextColumn >= rowData.length) {
|
||||
return prevState;
|
||||
}
|
||||
const cellData = rowData[nextColumn];
|
||||
if (cellData != null && typeof cellData === "object") {
|
||||
const location = tryGetResolvableLocation(cellData.url);
|
||||
if (location !== undefined) {
|
||||
jumpToLocation(location, this.props.databaseUri);
|
||||
}
|
||||
}
|
||||
this.scroller.scrollIntoViewOnNextUpdate();
|
||||
return {
|
||||
...prevState,
|
||||
selectedItem: { row: nextRow, column: nextColumn },
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.scroller.update();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.scroller.update();
|
||||
onNavigation.addListener(this.handleNavigationEvent);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
onNavigation.removeListener(this.handleNavigationEvent);
|
||||
}
|
||||
return (
|
||||
<table className={className}>
|
||||
<RawTableHeader
|
||||
columns={resultSet.schema.columns}
|
||||
schemaName={resultSet.schema.name}
|
||||
sortState={sortState}
|
||||
/>
|
||||
<tbody>{tableRows}</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -98,14 +98,20 @@ export function renderLocation(
|
||||
const resolvableLoc = tryGetResolvableLocation(loc);
|
||||
if (databaseUri !== undefined && resolvableLoc !== undefined) {
|
||||
return (
|
||||
<a
|
||||
href="#"
|
||||
className="vscode-codeql__result-table-location-link"
|
||||
title={title}
|
||||
onClick={jumpToLocationHandler(resolvableLoc, databaseUri, callback)}
|
||||
>
|
||||
{displayLabel}
|
||||
</a>
|
||||
<>
|
||||
{/*
|
||||
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>;
|
||||
|
||||
@@ -327,6 +327,10 @@ export class ResultTables extends React.Component<
|
||||
</button>
|
||||
<div className={tableHeaderItemClassName}>{this.props.queryName}</div>
|
||||
<div className={tableHeaderItemClassName}>
|
||||
{/*
|
||||
eslint-disable-next-line
|
||||
jsx-a11y/anchor-is-valid
|
||||
*/}
|
||||
<a
|
||||
href="#"
|
||||
onClick={openQuery}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { QueryDetails } from "./QueryDetails";
|
||||
import { VariantAnalysisActions } from "./VariantAnalysisActions";
|
||||
import { VariantAnalysisStats } from "./VariantAnalysisStats";
|
||||
import { parseDate } from "../../common/date";
|
||||
import { basename } from "../common/path";
|
||||
import { basename } from "../../common/path";
|
||||
import {
|
||||
defaultFilterSortState,
|
||||
filterAndSortRepositoriesWithResults,
|
||||
|
||||
@@ -39,6 +39,7 @@ describe("commands declared in package.json", () => {
|
||||
commandTitles[command] = title!;
|
||||
} else if (
|
||||
command.match(/^codeQLDatabases\./) ||
|
||||
command.match(/^codeQLQueries\./) ||
|
||||
command.match(/^codeQLVariantAnalysisRepositories\./) ||
|
||||
command.match(/^codeQLQueryHistory\./) ||
|
||||
command.match(/^codeQLAstViewer\./) ||
|
||||
@@ -65,6 +66,12 @@ describe("commands declared in package.json", () => {
|
||||
contribContextMenuCmds.add(command);
|
||||
});
|
||||
|
||||
menus["editor/title"].forEach((commandDecl: CmdDecl) => {
|
||||
const { command } = commandDecl;
|
||||
paletteCmds.delete(command);
|
||||
contribContextMenuCmds.add(command);
|
||||
});
|
||||
|
||||
debuggers.forEach((debuggerDecl: DebuggerDecl) => {
|
||||
if (debuggerDecl.variables !== undefined) {
|
||||
for (const command of Object.values(debuggerDecl.variables)) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { basename } from "../path";
|
||||
import { basename, extname } from "../../../src/common/path";
|
||||
|
||||
describe(basename.name, () => {
|
||||
describe("basename", () => {
|
||||
const testCases = [
|
||||
{ path: "test.ql", expected: "test.ql" },
|
||||
{ path: "PLACEHOLDER/q0.ql", expected: "q0.ql" },
|
||||
@@ -41,3 +41,25 @@ describe(basename.name, () => {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("extname", () => {
|
||||
const testCases = [
|
||||
{ path: "test.ql", expected: ".ql" },
|
||||
{ path: "PLACEHOLDER/q0.ql", expected: ".ql" },
|
||||
{
|
||||
path: "/etc/hosts/",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
path: "/etc/hosts",
|
||||
expected: "",
|
||||
},
|
||||
];
|
||||
|
||||
test.each(testCases)(
|
||||
"extname of $path is $expected",
|
||||
({ path, expected }) => {
|
||||
expect(extname(path)).toEqual(expected);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
ClassificationType,
|
||||
Method,
|
||||
} from "../../../src/data-extensions-editor/auto-model-api";
|
||||
import { Mode } from "../../../src/data-extensions-editor/shared/mode";
|
||||
|
||||
describe("createAutoModelRequest", () => {
|
||||
const externalApiUsages: ExternalApiUsage[] = [
|
||||
@@ -259,7 +260,13 @@ describe("createAutoModelRequest", () => {
|
||||
|
||||
it("creates a matching request", () => {
|
||||
expect(
|
||||
createAutoModelRequest("java", externalApiUsages, modeledMethods, usages),
|
||||
createAutoModelRequest(
|
||||
"java",
|
||||
externalApiUsages,
|
||||
modeledMethods,
|
||||
usages,
|
||||
Mode.Application,
|
||||
),
|
||||
).toEqual({
|
||||
language: "java",
|
||||
samples: [
|
||||
@@ -340,60 +347,6 @@ describe("createAutoModelRequest", () => {
|
||||
input: "Argument[0]",
|
||||
classification: undefined,
|
||||
},
|
||||
{
|
||||
package: "org.springframework.boot",
|
||||
type: "SpringApplication",
|
||||
name: "run",
|
||||
signature: "(Class,String[])",
|
||||
usages:
|
||||
usages[
|
||||
"org.springframework.boot.SpringApplication#run(Class,String[])"
|
||||
],
|
||||
input: "Argument[this]",
|
||||
classification: undefined,
|
||||
},
|
||||
{
|
||||
package: "org.springframework.boot",
|
||||
type: "SpringApplication",
|
||||
name: "run",
|
||||
signature: "(Class,String[])",
|
||||
usages:
|
||||
usages[
|
||||
"org.springframework.boot.SpringApplication#run(Class,String[])"
|
||||
],
|
||||
input: "Argument[0]",
|
||||
classification: undefined,
|
||||
},
|
||||
{
|
||||
package: "org.springframework.boot",
|
||||
type: "SpringApplication",
|
||||
name: "run",
|
||||
signature: "(Class,String[])",
|
||||
usages:
|
||||
usages[
|
||||
"org.springframework.boot.SpringApplication#run(Class,String[])"
|
||||
],
|
||||
input: "Argument[1]",
|
||||
classification: undefined,
|
||||
},
|
||||
{
|
||||
package: "java.io",
|
||||
type: "PrintStream",
|
||||
name: "println",
|
||||
signature: "(String)",
|
||||
usages: usages["java.io.PrintStream#println(String)"],
|
||||
input: "Argument[this]",
|
||||
classification: undefined,
|
||||
},
|
||||
{
|
||||
package: "java.io",
|
||||
type: "PrintStream",
|
||||
name: "println",
|
||||
signature: "(String)",
|
||||
usages: usages["java.io.PrintStream#println(String)"],
|
||||
input: "Argument[0]",
|
||||
classification: undefined,
|
||||
},
|
||||
{
|
||||
package: "org.sql2o",
|
||||
type: "Sql2o",
|
||||
@@ -430,6 +383,60 @@ describe("createAutoModelRequest", () => {
|
||||
input: "Argument[2]",
|
||||
classification: undefined,
|
||||
},
|
||||
{
|
||||
package: "java.io",
|
||||
type: "PrintStream",
|
||||
name: "println",
|
||||
signature: "(String)",
|
||||
usages: usages["java.io.PrintStream#println(String)"],
|
||||
input: "Argument[this]",
|
||||
classification: undefined,
|
||||
},
|
||||
{
|
||||
package: "java.io",
|
||||
type: "PrintStream",
|
||||
name: "println",
|
||||
signature: "(String)",
|
||||
usages: usages["java.io.PrintStream#println(String)"],
|
||||
input: "Argument[0]",
|
||||
classification: undefined,
|
||||
},
|
||||
{
|
||||
package: "org.springframework.boot",
|
||||
type: "SpringApplication",
|
||||
name: "run",
|
||||
signature: "(Class,String[])",
|
||||
usages:
|
||||
usages[
|
||||
"org.springframework.boot.SpringApplication#run(Class,String[])"
|
||||
],
|
||||
input: "Argument[this]",
|
||||
classification: undefined,
|
||||
},
|
||||
{
|
||||
package: "org.springframework.boot",
|
||||
type: "SpringApplication",
|
||||
name: "run",
|
||||
signature: "(Class,String[])",
|
||||
usages:
|
||||
usages[
|
||||
"org.springframework.boot.SpringApplication#run(Class,String[])"
|
||||
],
|
||||
input: "Argument[0]",
|
||||
classification: undefined,
|
||||
},
|
||||
{
|
||||
package: "org.springframework.boot",
|
||||
type: "SpringApplication",
|
||||
name: "run",
|
||||
signature: "(Class,String[])",
|
||||
usages:
|
||||
usages[
|
||||
"org.springframework.boot.SpringApplication#run(Class,String[])"
|
||||
],
|
||||
input: "Argument[1]",
|
||||
classification: undefined,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { calculateModeledPercentage } from "../modeled";
|
||||
import { calculateModeledPercentage } from "../../../../src/data-extensions-editor/shared/modeled-percentage";
|
||||
|
||||
describe("calculateModeledPercentage", () => {
|
||||
it("when there are no external API usages", () => {
|
||||
@@ -1,11 +1,143 @@
|
||||
import {
|
||||
createDataExtensionYaml,
|
||||
createDataExtensionYamlsForApplicationMode,
|
||||
createDataExtensionYamlsForFrameworkMode,
|
||||
createFilenameForLibrary,
|
||||
loadDataExtensionYaml,
|
||||
} from "../../../src/data-extensions-editor/yaml";
|
||||
|
||||
describe("createDataExtensionYaml", () => {
|
||||
it("creates the correct YAML file", () => {
|
||||
const yaml = createDataExtensionYaml(
|
||||
const yaml = createDataExtensionYaml("java", [
|
||||
{
|
||||
externalApiUsage: {
|
||||
library: "sql2o-1.6.0.jar",
|
||||
signature: "org.sql2o.Connection#createQuery(String)",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Connection",
|
||||
methodName: "createQuery",
|
||||
methodParameters: "(String)",
|
||||
supported: true,
|
||||
usages: [
|
||||
{
|
||||
label: "createQuery(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 15,
|
||||
startColumn: 13,
|
||||
endLine: 15,
|
||||
endColumn: 56,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "createQuery(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 26,
|
||||
startColumn: 13,
|
||||
endLine: 26,
|
||||
endColumn: 39,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
modeledMethod: {
|
||||
type: "sink",
|
||||
input: "Argument[0]",
|
||||
output: "",
|
||||
kind: "sql",
|
||||
provenance: "df-generated",
|
||||
},
|
||||
},
|
||||
{
|
||||
externalApiUsage: {
|
||||
library: "sql2o-1.6.0.jar",
|
||||
signature: "org.sql2o.Query#executeScalar(Class)",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Query",
|
||||
methodName: "executeScalar",
|
||||
methodParameters: "(Class)",
|
||||
supported: true,
|
||||
usages: [
|
||||
{
|
||||
label: "executeScalar(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 15,
|
||||
startColumn: 13,
|
||||
endLine: 15,
|
||||
endColumn: 85,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "executeScalar(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 26,
|
||||
startColumn: 13,
|
||||
endLine: 26,
|
||||
endColumn: 68,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
expect(yaml).toEqual(`extensions:
|
||||
- addsTo:
|
||||
pack: codeql/java-all
|
||||
extensible: sourceModel
|
||||
data: []
|
||||
|
||||
- addsTo:
|
||||
pack: codeql/java-all
|
||||
extensible: sinkModel
|
||||
data:
|
||||
- ["org.sql2o","Connection",true,"createQuery","(String)","","Argument[0]","sql","df-generated"]
|
||||
|
||||
- addsTo:
|
||||
pack: codeql/java-all
|
||||
extensible: summaryModel
|
||||
data: []
|
||||
|
||||
- addsTo:
|
||||
pack: codeql/java-all
|
||||
extensible: neutralModel
|
||||
data: []
|
||||
`);
|
||||
});
|
||||
|
||||
it("includes the correct language", () => {
|
||||
const yaml = createDataExtensionYaml("csharp", []);
|
||||
|
||||
expect(yaml).toEqual(`extensions:
|
||||
- addsTo:
|
||||
pack: codeql/csharp-all
|
||||
extensible: sourceModel
|
||||
data: []
|
||||
|
||||
- addsTo:
|
||||
pack: codeql/csharp-all
|
||||
extensible: sinkModel
|
||||
data: []
|
||||
|
||||
- addsTo:
|
||||
pack: codeql/csharp-all
|
||||
extensible: summaryModel
|
||||
data: []
|
||||
|
||||
- addsTo:
|
||||
pack: codeql/csharp-all
|
||||
extensible: neutralModel
|
||||
data: []
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createDataExtensionYamlsForApplicationMode", () => {
|
||||
it("creates the correct YAML files", () => {
|
||||
const yaml = createDataExtensionYamlsForApplicationMode(
|
||||
"java",
|
||||
[
|
||||
{
|
||||
@@ -70,6 +202,70 @@ describe("createDataExtensionYaml", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
library: "sql2o-2.5.0-alpha1.jar",
|
||||
signature: "org.sql2o.Sql2o#Sql2o(String,String,String)",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Sql2o",
|
||||
methodName: "Sql2o",
|
||||
methodParameters: "(String,String,String)",
|
||||
supported: false,
|
||||
usages: [
|
||||
{
|
||||
label: "new Sql2o(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 10,
|
||||
startColumn: 33,
|
||||
endLine: 10,
|
||||
endColumn: 88,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
library: "spring-boot-3.0.2.jar",
|
||||
signature:
|
||||
"org.springframework.boot.SpringApplication#run(Class,String[])",
|
||||
packageName: "org.springframework.boot",
|
||||
typeName: "SpringApplication",
|
||||
methodName: "run",
|
||||
methodParameters: "(Class,String[])",
|
||||
supported: false,
|
||||
usages: [
|
||||
{
|
||||
label: "run(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/Sql2oExampleApplication.java",
|
||||
startLine: 9,
|
||||
startColumn: 9,
|
||||
endLine: 9,
|
||||
endColumn: 66,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
library: "rt.jar",
|
||||
signature: "java.io.PrintStream#println(String)",
|
||||
packageName: "java.io",
|
||||
typeName: "PrintStream",
|
||||
methodName: "println",
|
||||
methodParameters: "(String)",
|
||||
supported: true,
|
||||
usages: [
|
||||
{
|
||||
label: "println(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 29,
|
||||
startColumn: 9,
|
||||
endLine: 29,
|
||||
endColumn: 49,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
{
|
||||
"org.sql2o.Connection#createQuery(String)": {
|
||||
@@ -79,10 +275,25 @@ describe("createDataExtensionYaml", () => {
|
||||
kind: "sql",
|
||||
provenance: "df-generated",
|
||||
},
|
||||
"org.springframework.boot.SpringApplication#run(Class,String[])": {
|
||||
type: "neutral",
|
||||
input: "",
|
||||
output: "",
|
||||
kind: "summary",
|
||||
provenance: "manual",
|
||||
},
|
||||
"org.sql2o.Sql2o#Sql2o(String,String,String)": {
|
||||
type: "sink",
|
||||
input: "Argument[0]",
|
||||
output: "",
|
||||
kind: "jndi",
|
||||
provenance: "manual",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(yaml).toEqual(`extensions:
|
||||
expect(yaml).toEqual({
|
||||
"models/sql2o.model.yml": `extensions:
|
||||
- addsTo:
|
||||
pack: codeql/java-all
|
||||
extensible: sourceModel
|
||||
@@ -93,6 +304,7 @@ describe("createDataExtensionYaml", () => {
|
||||
extensible: sinkModel
|
||||
data:
|
||||
- ["org.sql2o","Connection",true,"createQuery","(String)","","Argument[0]","sql","df-generated"]
|
||||
- ["org.sql2o","Sql2o",true,"Sql2o","(String,String,String)","","Argument[0]","jndi","manual"]
|
||||
|
||||
- addsTo:
|
||||
pack: codeql/java-all
|
||||
@@ -103,33 +315,166 @@ describe("createDataExtensionYaml", () => {
|
||||
pack: codeql/java-all
|
||||
extensible: neutralModel
|
||||
data: []
|
||||
`);
|
||||
});
|
||||
|
||||
it("includes the correct language", () => {
|
||||
const yaml = createDataExtensionYaml("csharp", [], {});
|
||||
|
||||
expect(yaml).toEqual(`extensions:
|
||||
`,
|
||||
"models/spring-boot.model.yml": `extensions:
|
||||
- addsTo:
|
||||
pack: codeql/csharp-all
|
||||
pack: codeql/java-all
|
||||
extensible: sourceModel
|
||||
data: []
|
||||
|
||||
- addsTo:
|
||||
pack: codeql/csharp-all
|
||||
pack: codeql/java-all
|
||||
extensible: sinkModel
|
||||
data: []
|
||||
|
||||
- addsTo:
|
||||
pack: codeql/csharp-all
|
||||
pack: codeql/java-all
|
||||
extensible: summaryModel
|
||||
data: []
|
||||
|
||||
- addsTo:
|
||||
pack: codeql/csharp-all
|
||||
pack: codeql/java-all
|
||||
extensible: neutralModel
|
||||
data:
|
||||
- ["org.springframework.boot","SpringApplication","run","(Class,String[])","summary","manual"]
|
||||
`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("createDataExtensionYamlsForFrameworkMode", () => {
|
||||
it("creates the correct YAML files", () => {
|
||||
const yaml = createDataExtensionYamlsForFrameworkMode(
|
||||
"github/sql2o",
|
||||
"java",
|
||||
[
|
||||
{
|
||||
library: "sql2o",
|
||||
signature: "org.sql2o.Connection#createQuery(String)",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Connection",
|
||||
methodName: "createQuery",
|
||||
methodParameters: "(String)",
|
||||
supported: true,
|
||||
usages: [
|
||||
{
|
||||
label: "createQuery(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 15,
|
||||
startColumn: 13,
|
||||
endLine: 15,
|
||||
endColumn: 56,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "createQuery(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 26,
|
||||
startColumn: 13,
|
||||
endLine: 26,
|
||||
endColumn: 39,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
library: "sql2o",
|
||||
signature: "org.sql2o.Query#executeScalar(Class)",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Query",
|
||||
methodName: "executeScalar",
|
||||
methodParameters: "(Class)",
|
||||
supported: true,
|
||||
usages: [
|
||||
{
|
||||
label: "executeScalar(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 15,
|
||||
startColumn: 13,
|
||||
endLine: 15,
|
||||
endColumn: 85,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "executeScalar(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 26,
|
||||
startColumn: 13,
|
||||
endLine: 26,
|
||||
endColumn: 68,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
library: "sql2o",
|
||||
signature: "org.sql2o.Sql2o#Sql2o(String,String,String)",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Sql2o",
|
||||
methodName: "Sql2o",
|
||||
methodParameters: "(String,String,String)",
|
||||
supported: false,
|
||||
usages: [
|
||||
{
|
||||
label: "new Sql2o(...)",
|
||||
url: {
|
||||
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
|
||||
startLine: 10,
|
||||
startColumn: 33,
|
||||
endLine: 10,
|
||||
endColumn: 88,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
{
|
||||
"org.sql2o.Connection#createQuery(String)": {
|
||||
type: "sink",
|
||||
input: "Argument[0]",
|
||||
output: "",
|
||||
kind: "sql",
|
||||
provenance: "df-generated",
|
||||
},
|
||||
"org.sql2o.Sql2o#Sql2o(String,String,String)": {
|
||||
type: "sink",
|
||||
input: "Argument[0]",
|
||||
output: "",
|
||||
kind: "jndi",
|
||||
provenance: "manual",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(yaml).toEqual({
|
||||
"models/sql2o.model.yml": `extensions:
|
||||
- addsTo:
|
||||
pack: codeql/java-all
|
||||
extensible: sourceModel
|
||||
data: []
|
||||
|
||||
- addsTo:
|
||||
pack: codeql/java-all
|
||||
extensible: sinkModel
|
||||
data:
|
||||
- ["org.sql2o","Connection",true,"createQuery","(String)","","Argument[0]","sql","df-generated"]
|
||||
- ["org.sql2o","Sql2o",true,"Sql2o","(String,String,String)","","Argument[0]","jndi","manual"]
|
||||
|
||||
- addsTo:
|
||||
pack: codeql/java-all
|
||||
extensible: summaryModel
|
||||
data: []
|
||||
|
||||
- addsTo:
|
||||
pack: codeql/java-all
|
||||
extensible: neutralModel
|
||||
data: []
|
||||
`);
|
||||
`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -191,3 +536,48 @@ describe("loadDataExtensionYaml", () => {
|
||||
).toThrow("Invalid data extension YAML: must be object");
|
||||
});
|
||||
});
|
||||
|
||||
describe("createFilenameForLibrary", () => {
|
||||
const testCases = [
|
||||
{ library: "sql2o.jar", filename: "models/sql2o.model.yml" },
|
||||
{
|
||||
library: "sql2o-1.6.0.jar",
|
||||
filename: "models/sql2o.model.yml",
|
||||
},
|
||||
{
|
||||
library: "spring-boot-3.0.2.jar",
|
||||
filename: "models/spring-boot.model.yml",
|
||||
},
|
||||
{
|
||||
library: "spring-boot-v3.0.2.jar",
|
||||
filename: "models/spring-boot.model.yml",
|
||||
},
|
||||
{
|
||||
library: "spring-boot-3.0.2-alpha1.jar",
|
||||
filename: "models/spring-boot.model.yml",
|
||||
},
|
||||
{
|
||||
library: "spring-boot-3.0.2beta2.jar",
|
||||
filename: "models/spring-boot.model.yml",
|
||||
},
|
||||
{
|
||||
library: "rt.jar",
|
||||
filename: "models/rt.model.yml",
|
||||
},
|
||||
{
|
||||
library: "System.Runtime.dll",
|
||||
filename: "models/system.runtime.model.yml",
|
||||
},
|
||||
{
|
||||
library: "System.Runtime.1.5.0.dll",
|
||||
filename: "models/system.runtime.model.yml",
|
||||
},
|
||||
];
|
||||
|
||||
test.each(testCases)(
|
||||
"returns $filename if library name is $library",
|
||||
({ library, filename }) => {
|
||||
expect(createFilenameForLibrary(library)).toEqual(filename);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,8 @@ import { fetchExternalApiQueries } from "../../../../src/data-extensions-editor/
|
||||
import * as log from "../../../../src/common/logging/notifications";
|
||||
import { RedactableError } from "../../../../src/common/errors";
|
||||
import { showAndLogExceptionWithTelemetry } from "../../../../src/common/logging";
|
||||
import { QueryLanguage } from "../../../../src/common/query-language";
|
||||
import { Query } from "../../../../src/data-extensions-editor/queries/query";
|
||||
|
||||
function createMockUri(path = "/a/b/c/foo"): Uri {
|
||||
return {
|
||||
@@ -29,11 +31,31 @@ function createMockUri(path = "/a/b/c/foo"): Uri {
|
||||
}
|
||||
|
||||
describe("runQuery", () => {
|
||||
it("runs all queries", async () => {
|
||||
const logPath = (await file()).path;
|
||||
const cases = Object.keys(fetchExternalApiQueries).flatMap((lang) => {
|
||||
const query = fetchExternalApiQueries[lang as QueryLanguage];
|
||||
if (!query) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const keys = new Set(Object.keys(query));
|
||||
keys.delete("dependencies");
|
||||
|
||||
return Array.from(keys).map((name) => ({
|
||||
language: lang as QueryLanguage,
|
||||
queryName: name as keyof Omit<Query, "dependencies">,
|
||||
}));
|
||||
});
|
||||
|
||||
test.each(cases)(
|
||||
"should run $queryName for $language",
|
||||
async ({ language, queryName }) => {
|
||||
const logPath = (await file()).path;
|
||||
|
||||
const query = fetchExternalApiQueries[language];
|
||||
if (!query) {
|
||||
throw new Error(`No query found for language ${language}`);
|
||||
}
|
||||
|
||||
// Test all queries
|
||||
for (const [lang, query] of Object.entries(fetchExternalApiQueries)) {
|
||||
const options = {
|
||||
cliServer: {
|
||||
resolveQlpacks: jest.fn().mockResolvedValue({
|
||||
@@ -58,7 +80,7 @@ describe("runQuery", () => {
|
||||
name: "foo",
|
||||
datasetUri: createMockUri(),
|
||||
},
|
||||
language: lang,
|
||||
language,
|
||||
},
|
||||
queryStorageDir: "/tmp/queries",
|
||||
progress: jest.fn(),
|
||||
@@ -67,7 +89,8 @@ describe("runQuery", () => {
|
||||
onCancellationRequested: jest.fn(),
|
||||
},
|
||||
};
|
||||
const result = await runQuery(options);
|
||||
|
||||
const result = await runQuery(queryName, options);
|
||||
|
||||
expect(result?.resultType).toEqual(QueryResultType.SUCCESS);
|
||||
|
||||
@@ -94,7 +117,11 @@ describe("runQuery", () => {
|
||||
|
||||
const queryFiles = await readdir(queryDirectory);
|
||||
expect(queryFiles.sort()).toEqual(
|
||||
["codeql-pack.yml", "FetchExternalApis.ql", "ExternalApi.qll"].sort(),
|
||||
[
|
||||
"codeql-pack.yml",
|
||||
"FetchExternalApis.ql",
|
||||
"AutomodelVsCode.qll",
|
||||
].sort(),
|
||||
);
|
||||
|
||||
const suiteFileContents = await readFile(
|
||||
@@ -106,13 +133,13 @@ describe("runQuery", () => {
|
||||
name: "codeql/external-api-usage",
|
||||
version: "0.0.0",
|
||||
dependencies: {
|
||||
[`codeql/${lang}-all`]: "*",
|
||||
[`codeql/${language}-all`]: "*",
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
await readFile(join(queryDirectory, "FetchExternalApis.ql"), "utf8"),
|
||||
).toEqual(query.mainQuery);
|
||||
).toEqual(query[queryName]);
|
||||
|
||||
for (const [filename, contents] of Object.entries(
|
||||
query.dependencies ?? {},
|
||||
@@ -121,8 +148,8 @@ describe("runQuery", () => {
|
||||
contents,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("readQueryResults", () => {
|
||||
|
||||
@@ -58,6 +58,7 @@ describe("query-results", () => {
|
||||
endLine: 2,
|
||||
fileName: "/home/users/yz",
|
||||
};
|
||||
(fqi.initialInfo as any).isQuickEval = true;
|
||||
expect(fqi.getQueryName()).toBe("Quick evaluation of yz:1-2");
|
||||
(fqi.initialInfo as any).quickEvalPosition.endLine = 1;
|
||||
expect(fqi.getQueryName()).toBe("Quick evaluation of yz:1");
|
||||
|
||||
Reference in New Issue
Block a user