Merge remote-tracking branch 'origin/main' into dbartol/save-before-start

This commit is contained in:
Dave Bartolomeo
2023-06-28 20:07:12 -04:00
54 changed files with 2022 additions and 1474 deletions

View File

@@ -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",

View File

@@ -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)

View File

@@ -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"
},

View File

@@ -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",

View File

@@ -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,
);
}
}

View File

@@ -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 = {

View File

@@ -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

View File

@@ -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);
};

View File

@@ -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,
});
}

View File

@@ -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,

View File

@@ -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
] ?? {

View File

@@ -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,

View File

@@ -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({

View File

@@ -37,7 +37,7 @@ export function autoNameExtensionPack(
};
}
function sanitizeExtensionPackName(name: string) {
export function sanitizeExtensionPackName(name: string) {
// Lowercase everything
name = name.toLowerCase();

View File

@@ -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) {

View File

@@ -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)) {

View File

@@ -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
}
`,
},

View File

@@ -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)
}
}
`,

View File

@@ -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;
};

View File

@@ -8,8 +8,3 @@ export interface ExtensionPack {
extensionTargets: Record<string, string>;
dataExtensions: string[];
}
export interface ExtensionPackModelFile {
filename: string;
extensionPack: ExtensionPack;
}

View File

@@ -0,0 +1,4 @@
export enum Mode {
Application = "application",
Framework = "framework",
}

View File

@@ -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">>,

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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 {

View File

@@ -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.");

View File

@@ -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");

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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";
}
}
}

View File

@@ -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;

View File

@@ -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()}`,

View File

@@ -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: [
{

View File

@@ -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,

View File

@@ -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"

View File

@@ -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>

View File

@@ -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}
/>
</>

View File

@@ -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")) && (

View File

@@ -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}
/>
))}

View File

@@ -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}
/>
))}

View File

@@ -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])}

View File

@@ -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>
);
}

View File

@@ -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>;

View File

@@ -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}

View File

@@ -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,

View File

@@ -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)) {

View File

@@ -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);
},
);
});

View File

@@ -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,
},
],
});
});

View File

@@ -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", () => {

View File

@@ -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);
},
);
});

View File

@@ -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", () => {

View File

@@ -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");