Merge remote-tracking branch 'origin/main' into koesie10/compare-sarif-view

This commit is contained in:
Koen Vlaswinkel
2023-12-14 09:54:50 +01:00
151 changed files with 11300 additions and 37854 deletions

View File

@@ -7,9 +7,6 @@ updates:
day: "thursday" # Thursday is arbitrary
labels:
- "Update dependencies"
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-minor", "version-update:semver-patch"]
- package-ecosystem: "github-actions"
directory: "/"
schedule:
@@ -17,6 +14,3 @@ updates:
day: "thursday" # Thursday is arbitrary
labels:
- "Update dependencies"
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-minor", "version-update:semver-patch"]

View File

@@ -2,18 +2,26 @@
## [UNRELEASED]
- Avoid showing a popup when hovering over source elements in database source files. [#3125](https://github.com/github/vscode-codeql/pull/3125)
## 1.11.0 - 13 December 2023
- Add a new method modeling panel to classify methods as sources/sinks/summaries while in the context of the source code. [#3128](https://github.com/github/vscode-codeql/pull/3128)
- Adds the ability to add multiple classifications per method in the CodeQL Model Editor. [#3128](https://github.com/github/vscode-codeql/pull/3128)
- Switch add and delete button positions in the CodeQL Model Editor. [#3123](https://github.com/github/vscode-codeql/pull/3123)
- Add a prompt to the "Quick query" command to encourage users in single-folder workspaces to use "Create query" instead. [#3082](https://github.com/github/vscode-codeql/pull/3082)
- Remove support for CodeQL CLI versions older than 2.11.6. [#3087](https://github.com/github/vscode-codeql/pull/3087)
- Preserve focus on results viewer when showing a location in a file. [#3088](https://github.com/github/vscode-codeql/pull/3088)
- The `dataflowtracking` and `tainttracking` snippets expand to the new module-based interface. [#3091](https://github.com/github/vscode-codeql/pull/3091)
- The compare view will now show a loading message while the results are loading. [#3107](https://github.com/github/vscode-codeql/pull/3107)
- Make top-banner of the model editor sticky [#3120](https://github.com/github/vscode-codeql/pull/3120)
## 1.10.0 - 16 November 2023
- Add new CodeQL views for managing databases and queries:
1. A queries panel that shows all queries in your workspace. It allows you to view, create, and run queries in one place.
2. A language selector, which allows you to quickly filter databases and queries by language.
For more information, see the [documentation](https://codeql.github.com/docs/codeql-for-visual-studio-code/analyzing-your-projects/#filtering-databases-and-queries-by-language).
- When adding a CodeQL database, we no longer add the database source folder to the workspace by default (since this caused bugs in single-folder workspaces). [#3047](https://github.com/github/vscode-codeql/pull/3047)
- You can manually add individual database source folders to the workspace with the "Add Database Source to Workspace" right-click command in the databases view.

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@
"description": "CodeQL for Visual Studio Code",
"author": "GitHub",
"private": true,
"version": "1.10.1",
"version": "1.11.1",
"publisher": "GitHub",
"license": "MIT",
"icon": "media/VS-marketplace-CodeQL-icon.png",
@@ -1840,15 +1840,14 @@
{
"id": "codeQLMethodModeling",
"type": "webview",
"name": "CodeQL Method Modeling",
"when": "config.codeQL.canary"
"name": "CodeQL Method Modeling"
}
],
"codeql-methods-usage": [
{
"id": "codeQLMethodsUsage",
"name": "CodeQL Methods Usage",
"when": "config.codeQL.canary && codeql.modelEditorOpen"
"when": "codeql.modelEditorOpen"
}
]
},
@@ -1975,7 +1974,6 @@
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.4.3",
"@types/child-process-promise": "^2.2.1",
"@types/classnames": "^2.2.9",
"@types/d3": "^7.4.0",
"@types/d3-graphviz": "^2.6.6",
"@types/del": "^4.0.0",
@@ -1993,7 +1991,7 @@
"@types/semver": "^7.2.0",
"@types/stream-json": "^1.7.1",
"@types/styled-components": "^5.1.11",
"@types/tar-stream": "^2.2.2",
"@types/tar-stream": "^3.1.3",
"@types/through2": "^2.0.36",
"@types/tmp": "^0.1.0",
"@types/unzipper": "^0.10.1",
@@ -2001,7 +1999,7 @@
"@types/webpack": "^5.28.0",
"@types/webpack-env": "^1.18.0",
"@typescript-eslint/eslint-plugin": "^6.2.1",
"@typescript-eslint/parser": "^6.2.1",
"@typescript-eslint/parser": "^6.14.0",
"@vscode/test-electron": "^2.2.0",
"@vscode/vsce": "^2.19.0",
"ansi-colors": "^4.1.1",
@@ -2032,7 +2030,7 @@
"jest-runner-vscode": "^3.0.1",
"lint-staged": "^15.0.2",
"markdownlint-cli2": "^0.6.0",
"markdownlint-cli2-formatter-pretty": "^0.0.4",
"markdownlint-cli2-formatter-pretty": "^0.0.5",
"mini-css-extract-plugin": "^2.6.1",
"npm-run-all": "^4.1.5",
"patch-package": "^8.0.0",

View File

@@ -11,7 +11,7 @@ import { promisify } from "util";
import { CancellationToken, Disposable, Uri } from "vscode";
import {
BQRSInfo,
BqrsInfo,
DecodedBqrs,
DecodedBqrsChunk,
} from "../common/bqrs-cli-types";
@@ -928,11 +928,11 @@ export class CodeQLCliServer implements Disposable {
* @param bqrsPath The path to the bqrs.
* @param pageSize The page size to precompute offsets into the binary file for.
*/
async bqrsInfo(bqrsPath: string, pageSize?: number): Promise<BQRSInfo> {
async bqrsInfo(bqrsPath: string, pageSize?: number): Promise<BqrsInfo> {
const subcommandArgs = (
pageSize ? ["--paginate-rows", pageSize.toString()] : []
).concat(bqrsPath);
return await this.runJsonCodeQlCliCommand<BQRSInfo>(
return await this.runJsonCodeQlCliCommand<BqrsInfo>(
["bqrs", "info"],
subcommandArgs,
"Reading bqrs header",

View File

@@ -195,9 +195,8 @@ export class DistributionManager implements DistributionProvider {
if (process.env.PATH) {
for (const searchDirectory of process.env.PATH.split(delimiter)) {
const expectedLauncherPath = await getExecutableFromDirectory(
searchDirectory,
);
const expectedLauncherPath =
await getExecutableFromDirectory(searchDirectory);
if (expectedLauncherPath) {
return {
codeQlPath: expectedLauncherPath,

View File

@@ -4,7 +4,7 @@
* the "for the sake of extensibility" comment in messages.ts.
*/
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace ColumnKindCode {
export namespace BqrsColumnKindCode {
export const FLOAT = "f";
export const INTEGER = "i";
export const STRING = "s";
@@ -13,55 +13,44 @@ export namespace ColumnKindCode {
export const ENTITY = "e";
}
type ColumnKind =
| typeof ColumnKindCode.FLOAT
| typeof ColumnKindCode.INTEGER
| typeof ColumnKindCode.STRING
| typeof ColumnKindCode.BOOLEAN
| typeof ColumnKindCode.DATE
| typeof ColumnKindCode.ENTITY;
export type BqrsColumnKind =
| typeof BqrsColumnKindCode.FLOAT
| typeof BqrsColumnKindCode.INTEGER
| typeof BqrsColumnKindCode.STRING
| typeof BqrsColumnKindCode.BOOLEAN
| typeof BqrsColumnKindCode.DATE
| typeof BqrsColumnKindCode.ENTITY;
interface Column {
export interface BqrsSchemaColumn {
name?: string;
kind: ColumnKind;
kind: BqrsColumnKind;
}
export interface ResultSetSchema {
export interface BqrsResultSetSchema {
name: string;
rows: number;
columns: Column[];
pagination?: PaginationInfo;
columns: BqrsSchemaColumn[];
pagination?: BqrsPaginationInfo;
}
export function getResultSetSchema(
resultSetName: string,
resultSets: BQRSInfo,
): ResultSetSchema | undefined {
for (const schema of resultSets["result-sets"]) {
if (schema.name === resultSetName) {
return schema;
}
}
return undefined;
}
interface PaginationInfo {
interface BqrsPaginationInfo {
"step-size": number;
offsets: number[];
}
export interface BQRSInfo {
"result-sets": ResultSetSchema[];
export interface BqrsInfo {
"result-sets": BqrsResultSetSchema[];
}
export type BqrsId = number;
export interface EntityValue {
url?: UrlValue;
export interface BqrsEntityValue {
url?: BqrsUrlValue;
label?: string;
id?: BqrsId;
}
export interface LineColumnLocation {
export interface BqrsLineColumnLocation {
uri: string;
startLine: number;
startColumn: number;
@@ -69,7 +58,7 @@ export interface LineColumnLocation {
endColumn: number;
}
export interface WholeFileLocation {
export interface BqrsWholeFileLocation {
uri: string;
startLine: never;
startColumn: never;
@@ -77,47 +66,28 @@ export interface WholeFileLocation {
endColumn: never;
}
export type ResolvableLocationValue = WholeFileLocation | LineColumnLocation;
export type BqrsUrlValue =
| BqrsWholeFileLocation
| BqrsLineColumnLocation
| string;
export type UrlValue = ResolvableLocationValue | string;
export type CellValue = EntityValue | number | string | boolean;
export type ResultRow = CellValue[];
export interface RawResultSet {
readonly schema: ResultSetSchema;
readonly rows: readonly ResultRow[];
}
// TODO: This function is not necessary. It generates a tuple that is slightly easier
// to handle than the ResultSetSchema and DecodedBqrsChunk. But perhaps it is unnecessary
// boilerplate.
export function transformBqrsResultSet(
schema: ResultSetSchema,
page: DecodedBqrsChunk,
): RawResultSet {
return {
schema,
rows: Array.from(page.tuples),
};
}
export type BqrsCellValue = BqrsEntityValue | number | string | boolean;
export type BqrsKind =
| "String"
| "Float"
| "Integer"
| "String"
| "Boolean"
| "Date"
| "Entity";
export interface BqrsColumn {
interface BqrsColumn {
name?: string;
kind: BqrsKind;
}
export interface DecodedBqrsChunk {
tuples: CellValue[][];
tuples: BqrsCellValue[][];
next?: number;
columns: BqrsColumn[];
}

View File

@@ -0,0 +1,216 @@
import {
BqrsCellValue as BqrsCellValue,
BqrsColumnKind as BqrsColumnKind,
BqrsColumnKindCode,
DecodedBqrsChunk,
BqrsEntityValue as BqrsEntityValue,
BqrsLineColumnLocation,
BqrsResultSetSchema,
BqrsUrlValue as BqrsUrlValue,
BqrsWholeFileLocation,
BqrsSchemaColumn,
} from "./bqrs-cli-types";
import {
CellValue,
Column,
ColumnKind,
EntityValue,
RawResultSet,
Row,
UrlValue,
UrlValueResolvable,
} from "./raw-result-types";
import { assertNever } from "./helpers-pure";
import { isEmptyPath } from "./bqrs-utils";
export function bqrsToResultSet(
schema: BqrsResultSetSchema,
chunk: DecodedBqrsChunk,
): RawResultSet {
const name = schema.name;
const totalRowCount = schema.rows;
const columns = schema.columns.map(mapColumn);
const rows = chunk.tuples.map(
(tuple): Row => tuple.map((cell): CellValue => mapCellValue(cell)),
);
const resultSet: RawResultSet = {
name,
totalRowCount,
columns,
rows,
};
if (chunk.next) {
resultSet.nextPageOffset = chunk.next;
}
return resultSet;
}
function mapColumn(column: BqrsSchemaColumn): Column {
const result: Column = {
kind: mapColumnKind(column.kind),
};
if (column.name) {
result.name = column.name;
}
return result;
}
function mapColumnKind(kind: BqrsColumnKind): ColumnKind {
switch (kind) {
case BqrsColumnKindCode.STRING:
return ColumnKind.String;
case BqrsColumnKindCode.FLOAT:
return ColumnKind.Float;
case BqrsColumnKindCode.INTEGER:
return ColumnKind.Integer;
case BqrsColumnKindCode.BOOLEAN:
return ColumnKind.Boolean;
case BqrsColumnKindCode.DATE:
return ColumnKind.Date;
case BqrsColumnKindCode.ENTITY:
return ColumnKind.Entity;
default:
assertNever(kind);
}
}
function mapCellValue(cellValue: BqrsCellValue): CellValue {
switch (typeof cellValue) {
case "string":
return {
type: "string",
value: cellValue,
};
case "number":
return {
type: "number",
value: cellValue,
};
case "boolean":
return {
type: "boolean",
value: cellValue,
};
case "object":
return {
type: "entity",
value: mapEntityValue(cellValue),
};
}
}
function mapEntityValue(cellValue: BqrsEntityValue): EntityValue {
const result: EntityValue = {};
if (cellValue.id) {
result.id = cellValue.id;
}
if (cellValue.label) {
result.label = cellValue.label;
}
if (cellValue.url) {
result.url = mapUrlValue(cellValue.url);
}
return result;
}
export function mapUrlValue(urlValue: BqrsUrlValue): UrlValue | undefined {
if (typeof urlValue === "string") {
const location = tryGetLocationFromString(urlValue);
if (location !== undefined) {
return location;
}
return {
type: "string",
value: urlValue,
};
}
if (isWholeFileLoc(urlValue)) {
return {
type: "wholeFileLocation",
uri: urlValue.uri,
};
}
if (isLineColumnLoc(urlValue)) {
return {
type: "lineColumnLocation",
uri: urlValue.uri,
startLine: urlValue.startLine,
startColumn: urlValue.startColumn,
endLine: urlValue.endLine,
endColumn: urlValue.endColumn,
};
}
return undefined;
}
function isLineColumnLoc(loc: BqrsUrlValue): loc is BqrsLineColumnLocation {
return (
typeof loc !== "string" &&
!isEmptyPath(loc.uri) &&
"startLine" in loc &&
"startColumn" in loc &&
"endLine" in loc &&
"endColumn" in loc
);
}
function isWholeFileLoc(loc: BqrsUrlValue): loc is BqrsWholeFileLocation {
return (
typeof loc !== "string" && !isEmptyPath(loc.uri) && !isLineColumnLoc(loc)
);
}
/**
* The CodeQL filesystem libraries use this pattern in `getURL()` predicates
* to describe the location of an entire filesystem resource.
* Such locations appear as `StringLocation`s instead of `FivePartLocation`s.
*
* Folder resources also get similar URLs, but with the `folder` scheme.
* They are deliberately ignored here, since there is no suitable location to show the user.
*/
const FILE_LOCATION_REGEX = /file:\/\/(.+):([0-9]+):([0-9]+):([0-9]+):([0-9]+)/;
function tryGetLocationFromString(loc: string): UrlValueResolvable | undefined {
const matches = FILE_LOCATION_REGEX.exec(loc);
if (matches && matches.length > 1 && matches[1]) {
if (isWholeFileMatch(matches)) {
return {
type: "wholeFileLocation",
uri: matches[1],
};
} else {
return {
type: "lineColumnLocation",
uri: matches[1],
startLine: Number(matches[2]),
startColumn: Number(matches[3]),
endLine: Number(matches[4]),
endColumn: Number(matches[5]),
};
}
}
return undefined;
}
function isWholeFileMatch(matches: RegExpExecArray): boolean {
return (
matches[2] === "0" &&
matches[3] === "0" &&
matches[4] === "0" &&
matches[5] === "0"
);
}

View File

@@ -1,111 +1,20 @@
import {
UrlValue,
ResolvableLocationValue,
LineColumnLocation,
WholeFileLocation,
} from "./bqrs-cli-types";
import { createRemoteFileRef } from "../common/location-link-utils";
/**
* The CodeQL filesystem libraries use this pattern in `getURL()` predicates
* to describe the location of an entire filesystem resource.
* Such locations appear as `StringLocation`s instead of `FivePartLocation`s.
*
* Folder resources also get similar URLs, but with the `folder` scheme.
* They are deliberately ignored here, since there is no suitable location to show the user.
*/
const FILE_LOCATION_REGEX = /file:\/\/(.+):([0-9]+):([0-9]+):([0-9]+):([0-9]+)/;
/**
* Gets a resolvable source file location for the specified `LocationValue`, if possible.
* @param loc The location to test.
*/
export function tryGetResolvableLocation(
loc: UrlValue | undefined,
): ResolvableLocationValue | undefined {
let resolvedLoc;
if (loc === undefined) {
resolvedLoc = undefined;
} else if (isWholeFileLoc(loc) || isLineColumnLoc(loc)) {
resolvedLoc = loc as ResolvableLocationValue;
} else if (isStringLoc(loc)) {
resolvedLoc = tryGetLocationFromString(loc);
} else {
resolvedLoc = undefined;
}
return resolvedLoc;
}
export function tryGetLocationFromString(
loc: string,
): ResolvableLocationValue | undefined {
const matches = FILE_LOCATION_REGEX.exec(loc);
if (matches && matches.length > 1 && matches[1]) {
if (isWholeFileMatch(matches)) {
return {
uri: matches[1],
} as WholeFileLocation;
} else {
return {
uri: matches[1],
startLine: Number(matches[2]),
startColumn: Number(matches[3]),
endLine: Number(matches[4]),
endColumn: Number(matches[5]),
};
}
} else {
return undefined;
}
}
function isWholeFileMatch(matches: RegExpExecArray): boolean {
return (
matches[2] === "0" &&
matches[3] === "0" &&
matches[4] === "0" &&
matches[5] === "0"
);
}
import { isUrlValueResolvable, UrlValue } from "./raw-result-types";
/**
* Checks whether the file path is empty. If so, we do not want to render this location
* as a link.
*
* @param uri A file uri
*/
export function isEmptyPath(uriStr: string) {
return !uriStr || uriStr === "file:/";
}
export function isLineColumnLoc(loc: UrlValue): loc is LineColumnLocation {
return (
typeof loc !== "string" &&
!isEmptyPath(loc.uri) &&
"startLine" in loc &&
"startColumn" in loc &&
"endLine" in loc &&
"endColumn" in loc
);
}
export function isWholeFileLoc(loc: UrlValue): loc is WholeFileLocation {
return (
typeof loc !== "string" && !isEmptyPath(loc.uri) && !isLineColumnLoc(loc)
);
}
export function isStringLoc(loc: UrlValue): loc is string {
return typeof loc === "string";
}
export function tryGetRemoteLocation(
loc: UrlValue | undefined,
fileLinkPrefix: string,
sourceLocationPrefix: string | undefined,
): string | undefined {
const resolvableLocation = tryGetResolvableLocation(loc);
if (!resolvableLocation) {
if (!loc || !isUrlValueResolvable(loc)) {
return undefined;
}
@@ -115,22 +24,19 @@ export function tryGetRemoteLocation(
// "file:${sourceLocationPrefix}/relative/path/to/file"
// So we need to strip off the first part to get the relative path.
if (sourceLocationPrefix) {
if (!resolvableLocation.uri.startsWith(`file:${sourceLocationPrefix}/`)) {
if (!loc.uri.startsWith(`file:${sourceLocationPrefix}/`)) {
return undefined;
}
trimmedLocation = resolvableLocation.uri.replace(
`file:${sourceLocationPrefix}/`,
"",
);
trimmedLocation = loc.uri.replace(`file:${sourceLocationPrefix}/`, "");
} else {
// If the source location prefix is empty (e.g. for older remote queries), we assume that the database
// was created on a Linux actions runner and has the format:
// "file:/home/runner/work/<repo>/<repo>/relative/path/to/file"
// So we need to drop the first 6 parts of the path.
if (!resolvableLocation.uri.startsWith("file:/home/runner/work/")) {
if (!loc.uri.startsWith("file:/home/runner/work/")) {
return undefined;
}
const locationParts = resolvableLocation.uri.split("/");
const locationParts = loc.uri.split("/");
trimmedLocation = locationParts.slice(6, locationParts.length).join("/");
}
@@ -138,11 +44,16 @@ export function tryGetRemoteLocation(
fileLinkPrefix,
filePath: trimmedLocation,
};
if (loc.type === "wholeFileLocation") {
return createRemoteFileRef(fileLink);
}
return createRemoteFileRef(
fileLink,
resolvableLocation.startLine,
resolvableLocation.endLine,
resolvableLocation.startColumn,
resolvableLocation.endColumn,
loc.startLine,
loc.endLine,
loc.startColumn,
loc.endColumn,
);
}

View File

@@ -1,11 +1,4 @@
import * as sarif from "sarif";
import {
RawResultSet,
ResultRow,
ResultSetSchema,
ResolvableLocationValue,
BqrsColumn,
} from "../common/bqrs-cli-types";
import {
VariantAnalysis,
VariantAnalysisScannedRepositoryResult,
@@ -25,6 +18,12 @@ import {
} from "../model-editor/shared/view-state";
import { Mode } from "../model-editor/shared/mode";
import { QueryLanguage } from "./query-language";
import {
Column,
RawResultSet,
Row,
UrlValueResolvable,
} from "./raw-result-types";
/**
* This module contains types and code that are shared between
@@ -35,10 +34,13 @@ export const SELECT_TABLE_NAME = "#select";
export const ALERTS_TABLE_NAME = "alerts";
export const GRAPH_TABLE_NAME = "graph";
export type RawTableResultSet = { t: "RawResultSet" } & RawResultSet;
type RawTableResultSet = {
t: "RawResultSet";
resultSet: RawResultSet;
};
type InterpretedResultSet<T> = {
t: "InterpretedResultSet";
readonly schema: ResultSetSchema;
name: string;
interpretation: InterpretationT<T>;
};
@@ -208,7 +210,7 @@ export type FromResultsViewMsg =
*/
interface ViewSourceFileMsg {
t: "viewSourceFile";
loc: ResolvableLocationValue;
loc: UrlValueResolvable;
databaseUri: string;
}
@@ -377,9 +379,9 @@ type QueryCompareResult = RawQueryCompareResult | InterpretedQueryCompareResult;
*/
export type RawQueryCompareResult = {
kind: "raw";
columns: readonly BqrsColumn[];
from: ResultRow[];
to: ResultRow[];
columns: readonly Column[];
from: Row[];
to: Row[];
};
/**

View File

@@ -0,0 +1,90 @@
export enum ColumnKind {
String = "string",
Float = "float",
Integer = "integer",
Boolean = "boolean",
Date = "date",
Entity = "entity",
}
export type Column = {
name?: string;
kind: ColumnKind;
};
type UrlValueString = {
type: "string";
value: string;
};
export type UrlValueWholeFileLocation = {
type: "wholeFileLocation";
uri: string;
};
export type UrlValueLineColumnLocation = {
type: "lineColumnLocation";
uri: string;
startLine: number;
startColumn: number;
endLine: number;
endColumn: number;
};
export type UrlValueResolvable =
| UrlValueWholeFileLocation
| UrlValueLineColumnLocation;
export function isUrlValueResolvable(
value: UrlValue,
): value is UrlValueResolvable {
return (
value.type === "wholeFileLocation" || value.type === "lineColumnLocation"
);
}
export type UrlValue = UrlValueString | UrlValueResolvable;
export type EntityValue = {
url?: UrlValue;
label?: string;
id?: number;
};
type CellValueEntity = {
type: "entity";
value: EntityValue;
};
type CellValueNumber = {
type: "number";
value: number;
};
type CellValueString = {
type: "string";
value: string;
};
type CellValueBoolean = {
type: "boolean";
value: boolean;
};
export type CellValue =
| CellValueEntity
| CellValueNumber
| CellValueString
| CellValueBoolean;
export type Row = CellValue[];
export type RawResultSet = {
name: string;
totalRowCount: number;
columns: Column[];
rows: Row[];
nextPageOffset?: number;
};

View File

@@ -1,11 +1,11 @@
export type DeepReadonly<T> = T extends Array<infer R>
? DeepReadonlyArray<R>
: // eslint-disable-next-line @typescript-eslint/ban-types
T extends Function
? T
: T extends object
? DeepReadonlyObject<T>
: T;
T extends Function
? T
: T extends object
? DeepReadonlyObject<T>
: T;
interface DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {}

View File

@@ -1,6 +1,6 @@
import * as Sarif from "sarif";
import type { HighlightedRegion } from "../variant-analysis/shared/analysis-result";
import { ResolvableLocationValue } from "../common/bqrs-cli-types";
import { UrlValueResolvable } from "./raw-result-types";
import { isEmptyPath } from "./bqrs-utils";
export interface SarifLink {
@@ -16,7 +16,7 @@ interface NoLocation {
}
type ParsedSarifLocation =
| (ResolvableLocationValue & {
| (UrlValueResolvable & {
userVisibleFile: string;
})
// Resolvable locations have a `uri` field, but it will sometimes include
@@ -137,6 +137,7 @@ export function parseSarifLocation(
// If the region property is absent, the physicalLocation object refers to the entire file.
// Source: https://docs.oasis-open.org/sarif/sarif/v2.1.0/cs01/sarif-v2.1.0-cs01.html#_Toc16012638.
return {
type: "wholeFileLocation",
uri: effectiveLocation,
userVisibleFile,
} as ParsedSarifLocation;
@@ -144,6 +145,7 @@ export function parseSarifLocation(
const region = parseSarifRegion(physicalLocation.region);
return {
type: "lineColumnLocation",
uri: effectiveLocation,
userVisibleFile,
...region,
@@ -232,14 +234,14 @@ export function parseHighlightedLine(
const highlightStartColumn = isSingleLineHighlight
? highlightedRegion.startColumn
: isFirstHighlightedLine
? highlightedRegion.startColumn
: 0;
? highlightedRegion.startColumn
: 0;
const highlightEndColumn = isSingleLineHighlight
? highlightedRegion.endColumn
: isLastHighlightedLine
? highlightedRegion.endColumn
: line.length + 1;
? highlightedRegion.endColumn
: line.length + 1;
const plainSection1 = line.substring(0, highlightStartColumn - 1);
const highlightedSection = line.substring(

View File

@@ -162,8 +162,8 @@ export class ExtensionTelemetryListener
const status = !error
? CommandCompletion.Success
: error instanceof UserCancellationException
? CommandCompletion.Cancelled
: CommandCompletion.Failed;
? CommandCompletion.Cancelled
: CommandCompletion.Failed;
this.reporter.sendTelemetryEvent(
"command-usage",

View File

@@ -10,7 +10,7 @@ import { extLogger } from "../common/logging/vscode";
import { CodeQLCliServer } from "../codeql-cli/cli";
import { DatabaseManager } from "../databases/local-databases";
import { jumpToLocation } from "../databases/local-databases/locations";
import { BQRSInfo, DecodedBqrsChunk } from "../common/bqrs-cli-types";
import { BqrsInfo } from "../common/bqrs-cli-types";
import resultsDiff from "./resultsDiff";
import { CompletedLocalQueryInfo } from "../query-results";
import { assertNever, getErrorMessage } from "../common/helpers-pure";
@@ -22,6 +22,8 @@ import {
import { telemetryListener } from "../common/vscode/telemetry";
import { redactableError } from "../common/errors";
import { App } from "../common/app";
import { bqrsToResultSet } from "../common/bqrs-raw-results-mapper";
import { RawResultSet } from "../common/raw-result-types";
import {
findCommonResultSetNames,
findResultSetNames,
@@ -29,9 +31,9 @@ import {
interface ComparePair {
from: CompletedLocalQueryInfo;
fromSchemas: BQRSInfo;
fromSchemas: BqrsInfo;
to: CompletedLocalQueryInfo;
toSchemas: BQRSInfo;
toSchemas: BqrsInfo;
commonResultSetNames: readonly string[];
}
@@ -236,22 +238,23 @@ export class CompareView extends AbstractWebview<
}
private async getResultSet(
bqrsInfo: BQRSInfo,
bqrsInfo: BqrsInfo,
resultSetName: string,
resultsPath: string,
): Promise<DecodedBqrsChunk> {
): Promise<RawResultSet> {
const schema = bqrsInfo["result-sets"].find(
(schema) => schema.name === resultSetName,
);
if (!schema) {
throw new Error(`Schema ${resultSetName} not found.`);
}
return await this.cliServer.bqrsDecode(resultsPath, resultSetName);
const chunk = await this.cliServer.bqrsDecode(resultsPath, resultSetName);
return bqrsToResultSet(schema, chunk);
}
private compareResults(
fromResults: DecodedBqrsChunk,
toResults: DecodedBqrsChunk,
fromResults: RawResultSet,
toResults: RawResultSet,
): RawQueryCompareResult {
// Only compare columns that have the same name
return resultsDiff(fromResults, toResults);

View File

@@ -1,9 +1,9 @@
import { BQRSInfo } from "../common/bqrs-cli-types";
import { BqrsInfo } from "../common/bqrs-cli-types";
import { getDefaultResultSetName } from "../common/interface-types";
export async function findCommonResultSetNames(
fromSchemas: BQRSInfo,
toSchemas: BQRSInfo,
fromSchemas: BqrsInfo,
toSchemas: BqrsInfo,
): Promise<string[]> {
const fromSchemaNames = fromSchemas["result-sets"].map(
(schema) => schema.name,
@@ -14,8 +14,8 @@ export async function findCommonResultSetNames(
}
export async function findResultSetNames(
fromSchemas: BQRSInfo,
toSchemas: BQRSInfo,
fromSchemas: BqrsInfo,
toSchemas: BqrsInfo,
commonResultSetNames: readonly string[],
selectedResultSetName: string | undefined,
) {

View File

@@ -1,5 +1,5 @@
import { DecodedBqrsChunk } from "../common/bqrs-cli-types";
import { RawQueryCompareResult } from "../common/interface-types";
import { RawResultSet } from "../common/raw-result-types";
/**
* Compare the rows of two queries. Use deep equality to determine if
@@ -20,31 +20,31 @@ import { RawQueryCompareResult } from "../common/interface-types";
* 3. If the queries are 100% disjoint
*/
export default function resultsDiff(
fromResults: DecodedBqrsChunk,
toResults: DecodedBqrsChunk,
fromResults: RawResultSet,
toResults: RawResultSet,
): RawQueryCompareResult {
if (fromResults.columns.length !== toResults.columns.length) {
throw new Error("CodeQL Compare: Columns do not match.");
}
if (!fromResults.tuples.length) {
if (!fromResults.rows.length) {
throw new Error("CodeQL Compare: Source query has no results.");
}
if (!toResults.tuples.length) {
if (!toResults.rows.length) {
throw new Error("CodeQL Compare: Target query has no results.");
}
const results: RawQueryCompareResult = {
kind: "raw",
columns: fromResults.columns,
from: arrayDiff(fromResults.tuples, toResults.tuples),
to: arrayDiff(toResults.tuples, fromResults.tuples),
from: arrayDiff(fromResults.rows, toResults.rows),
to: arrayDiff(toResults.rows, fromResults.rows),
};
if (
fromResults.tuples.length === results.from.length &&
toResults.tuples.length === results.to.length
fromResults.rows.length === results.from.length &&
toResults.rows.length === results.to.length
) {
throw new Error("CodeQL Compare: No overlap between the selected queries.");
}

View File

@@ -726,7 +726,6 @@ export interface ModelConfig {
flowGeneration: boolean;
llmGeneration: boolean;
getExtensionsDirectory(languageId: string): string | undefined;
showMultipleModels: boolean;
enableRuby: boolean;
}
@@ -765,10 +764,6 @@ export class ModelConfigListener extends ConfigListener implements ModelConfig {
});
}
public get showMultipleModels(): boolean {
return isCanary();
}
public get enableRuby(): boolean {
return !!ENABLE_RUBY.getValue<boolean>();
}

View File

@@ -1,17 +1,11 @@
import { pathExists, outputJSON, readJSON, readJSONSync } from "fs-extra";
import { join } from "path";
import {
clearLocalDbConfig,
cloneDbConfig,
DbConfig,
initializeLocalDbConfig,
removeLocalDb,
removeLocalList,
removeRemoteList,
removeRemoteOwner,
removeRemoteRepo,
renameLocalDb,
renameLocalList,
renameRemoteList,
SelectedDbItem,
DB_CONFIG_VERSION,
@@ -30,13 +24,7 @@ import {
DbConfigValidationErrorKind,
} from "../db-validation-errors";
import { ValueResult } from "../../common/value-result";
import {
LocalDatabaseDbItem,
LocalListDbItem,
RemoteUserDefinedListDbItem,
DbItem,
DbItemKind,
} from "../db-item";
import { RemoteUserDefinedListDbItem, DbItem, DbItemKind } from "../db-item";
export class DbConfigStore extends DisposableObject {
public static readonly databaseConfigFileName = "databases.json";
@@ -119,20 +107,9 @@ export class DbConfigStore extends DisposableObject {
let config: DbConfig;
switch (dbItem.kind) {
case DbItemKind.LocalList:
config = removeLocalList(this.config, dbItem.listName);
break;
case DbItemKind.RemoteUserDefinedList:
config = removeRemoteList(this.config, dbItem.listName);
break;
case DbItemKind.LocalDatabase:
// When we start using local databases these need to be removed from disk as well.
config = removeLocalDb(
this.config,
dbItem.databaseName,
dbItem.parentListName,
);
break;
case DbItemKind.RemoteRepo:
config = removeRemoteRepo(
this.config,
@@ -229,22 +206,6 @@ export class DbConfigStore extends DisposableObject {
await this.writeConfig(config);
}
public async addLocalList(listName: string): Promise<void> {
if (!this.config) {
throw Error("Cannot add local list if config is not loaded");
}
this.validateLocalListName(listName);
const config = cloneDbConfig(this.config);
config.databases.local.lists.push({
name: listName,
databases: [],
});
await this.writeConfig(config);
}
public async addRemoteList(listName: string): Promise<void> {
if (!this.config) {
throw Error("Cannot add variant analysis list if config is not loaded");
@@ -261,25 +222,6 @@ export class DbConfigStore extends DisposableObject {
await this.writeConfig(config);
}
public async renameLocalList(
currentDbItem: LocalListDbItem,
newName: string,
) {
if (!this.config) {
throw Error("Cannot rename local list if config is not loaded");
}
this.validateLocalListName(newName);
const updatedConfig = renameLocalList(
this.config,
currentDbItem.listName,
newName,
);
await this.writeConfig(updatedConfig);
}
public async renameRemoteList(
currentDbItem: RemoteUserDefinedListDbItem,
newName: string,
@@ -301,27 +243,6 @@ export class DbConfigStore extends DisposableObject {
await this.writeConfig(updatedConfig);
}
public async renameLocalDb(
currentDbItem: LocalDatabaseDbItem,
newName: string,
parentListName?: string,
): Promise<void> {
if (!this.config) {
throw Error("Cannot rename local db if config is not loaded");
}
this.validateLocalDbName(newName);
const updatedConfig = renameLocalDb(
this.config,
currentDbItem.databaseName,
newName,
parentListName,
);
await this.writeConfig(updatedConfig);
}
public doesRemoteListExist(listName: string): boolean {
if (!this.config) {
throw Error(
@@ -334,31 +255,6 @@ export class DbConfigStore extends DisposableObject {
);
}
public doesLocalListExist(listName: string): boolean {
if (!this.config) {
throw Error("Cannot check local list existence if config is not loaded");
}
return this.config.databases.local.lists.some((l) => l.name === listName);
}
public doesLocalDbExist(dbName: string, listName?: string): boolean {
if (!this.config) {
throw Error(
"Cannot check variant analysis repository existence if config is not loaded",
);
}
if (listName) {
return this.config.databases.local.lists.some(
(l) =>
l.name === listName && l.databases.some((d) => d.name === dbName),
);
}
return this.config.databases.local.databases.some((d) => d.name === dbName);
}
public doesRemoteDbExist(dbName: string, listName?: string): boolean {
if (!this.config) {
throw Error(
@@ -384,7 +280,6 @@ export class DbConfigStore extends DisposableObject {
}
private async writeConfig(config: DbConfig): Promise<void> {
clearLocalDbConfig(config);
await outputJSON(this.configPath, config, {
spaces: 2,
});
@@ -416,7 +311,6 @@ export class DbConfigStore extends DisposableObject {
}
if (newConfig) {
initializeLocalDbConfig(newConfig);
this.configErrors = this.configValidator.validate(newConfig);
}
@@ -451,7 +345,6 @@ export class DbConfigStore extends DisposableObject {
}
if (newConfig) {
initializeLocalDbConfig(newConfig);
this.configErrors = this.configValidator.validate(newConfig);
}
@@ -499,10 +392,6 @@ export class DbConfigStore extends DisposableObject {
owners: [],
repositories: [],
},
local: {
lists: [],
databases: [],
},
},
selected: {
kind: SelectedDbItemKind.VariantAnalysisSystemDefinedList,
@@ -511,16 +400,6 @@ export class DbConfigStore extends DisposableObject {
};
}
private validateLocalListName(listName: string): void {
if (listName === "") {
throw Error("List name cannot be empty");
}
if (this.doesLocalListExist(listName)) {
throw Error(`A local list with the name '${listName}' already exists`);
}
}
private validateRemoteListName(listName: string): void {
if (listName === "") {
throw Error("List name cannot be empty");
@@ -532,14 +411,4 @@ export class DbConfigStore extends DisposableObject {
);
}
}
private validateLocalDbName(dbName: string): void {
if (dbName === "") {
throw Error("Database name cannot be empty");
}
if (this.doesLocalDbExist(dbName)) {
throw Error(`A local database with the name '${dbName}' already exists`);
}
}
}

View File

@@ -1,7 +1,7 @@
import { readJsonSync } from "fs-extra";
import { resolve } from "path";
import Ajv, { ValidateFunction } from "ajv";
import { clearLocalDbConfig, DbConfig } from "./db-config";
import { DbConfig } from "./db-config";
import { findDuplicateStrings } from "../../common/text-utils";
import {
DbConfigValidationError,
@@ -19,8 +19,6 @@ export class DbConfigValidator {
}
public validate(dbConfig: DbConfig): DbConfigValidationError[] {
const localDbs = clearLocalDbConfig(dbConfig);
this.validateSchemaFn(dbConfig);
if (this.validateSchemaFn.errors) {
@@ -30,13 +28,6 @@ export class DbConfigValidator {
}));
}
// Add any local db config back so that we have a config
// object that respects its type and validation can happen
// as normal.
if (localDbs) {
dbConfig.databases.local = localDbs;
}
return [
...this.validateDbListNames(dbConfig),
...this.validateDbNames(dbConfig),
@@ -55,14 +46,6 @@ export class DbConfigValidator {
)}`,
});
const duplicateLocalDbLists = findDuplicateStrings(
dbConfig.databases.local.lists.map((n) => n.name),
);
if (duplicateLocalDbLists.length > 0) {
errors.push(buildError(duplicateLocalDbLists));
}
const duplicateRemoteDbLists = findDuplicateStrings(
dbConfig.databases.variantAnalysis.repositoryLists.map((n) => n.name),
);
@@ -81,14 +64,6 @@ export class DbConfigValidator {
message: `There are databases with the same name: ${dups.join(", ")}`,
});
const duplicateLocalDbs = findDuplicateStrings(
dbConfig.databases.local.databases.map((d) => d.name),
);
if (duplicateLocalDbs.length > 0) {
errors.push(buildError(duplicateLocalDbs));
}
const duplicateRemoteDbs = findDuplicateStrings(
dbConfig.databases.variantAnalysis.repositories,
);
@@ -111,13 +86,6 @@ export class DbConfigValidator {
)}`,
});
for (const list of dbConfig.databases.local.lists) {
const dups = findDuplicateStrings(list.databases.map((d) => d.name));
if (dups.length > 0) {
errors.push(buildError(list.name, dups));
}
}
for (const list of dbConfig.databases.variantAnalysis.repositoryLists) {
const dups = findDuplicateStrings(list.repositories);
if (dups.length > 0) {

View File

@@ -1,8 +1,6 @@
// Contains models and consts for the data we want to store in the database config.
// Changes to these models should be done carefully and account for backwards compatibility of data.
import { DatabaseOrigin } from "../local-databases/database-origin";
export const DB_CONFIG_VERSION = 1;
export interface DbConfig {
@@ -13,37 +11,21 @@ export interface DbConfig {
interface DbConfigDatabases {
variantAnalysis: RemoteDbConfig;
local: LocalDbConfig;
}
export type SelectedDbItem =
| SelectedLocalUserDefinedList
| SelectedLocalDatabase
| SelectedRemoteSystemDefinedList
| SelectedVariantAnalysisUserDefinedList
| SelectedRemoteOwner
| SelectedRemoteRepository;
export enum SelectedDbItemKind {
LocalUserDefinedList = "localUserDefinedList",
LocalDatabase = "localDatabase",
VariantAnalysisSystemDefinedList = "variantAnalysisSystemDefinedList",
VariantAnalysisUserDefinedList = "variantAnalysisUserDefinedList",
VariantAnalysisOwner = "variantAnalysisOwner",
VariantAnalysisRepository = "variantAnalysisRepository",
}
interface SelectedLocalUserDefinedList {
kind: SelectedDbItemKind.LocalUserDefinedList;
listName: string;
}
interface SelectedLocalDatabase {
kind: SelectedDbItemKind.LocalDatabase;
databaseName: string;
listName?: string;
}
interface SelectedRemoteSystemDefinedList {
kind: SelectedDbItemKind.VariantAnalysisSystemDefinedList;
listName: string;
@@ -76,24 +58,6 @@ export interface RemoteRepositoryList {
repositories: string[];
}
interface LocalDbConfig {
lists: LocalList[];
databases: LocalDatabase[];
}
export interface LocalList {
name: string;
databases: LocalDatabase[];
}
export interface LocalDatabase {
name: string;
dateAdded: number;
language: string;
origin: DatabaseOrigin;
storagePath: string;
}
export function cloneDbConfig(config: DbConfig): DbConfig {
return {
version: config.version,
@@ -108,13 +72,6 @@ export function cloneDbConfig(config: DbConfig): DbConfig {
owners: [...config.databases.variantAnalysis.owners],
repositories: [...config.databases.variantAnalysis.repositories],
},
local: {
lists: config.databases.local.lists.map((list) => ({
name: list.name,
databases: list.databases.map((db) => ({ ...db })),
})),
databases: config.databases.local.databases.map((db) => ({ ...db })),
},
},
selected: config.selected
? cloneDbConfigSelectedItem(config.selected)
@@ -122,28 +79,6 @@ export function cloneDbConfig(config: DbConfig): DbConfig {
};
}
export function renameLocalList(
originalConfig: DbConfig,
currentListName: string,
newListName: string,
): DbConfig {
const config = cloneDbConfig(originalConfig);
const list = getLocalList(config, currentListName);
list.name = newListName;
if (
config.selected?.kind === SelectedDbItemKind.LocalUserDefinedList ||
config.selected?.kind === SelectedDbItemKind.LocalDatabase
) {
if (config.selected.listName === currentListName) {
config.selected.listName = newListName;
}
}
return config;
}
export function renameRemoteList(
originalConfig: DbConfig,
currentListName: string,
@@ -167,67 +102,6 @@ export function renameRemoteList(
return config;
}
export function renameLocalDb(
originalConfig: DbConfig,
currentDbName: string,
newDbName: string,
parentListName?: string,
): DbConfig {
const config = cloneDbConfig(originalConfig);
if (parentListName) {
const list = getLocalList(config, parentListName);
const dbIndex = list.databases.findIndex((db) => db.name === currentDbName);
if (dbIndex === -1) {
throw Error(
`Cannot find database '${currentDbName}' in list '${parentListName}'`,
);
}
list.databases[dbIndex].name = newDbName;
} else {
const dbIndex = config.databases.local.databases.findIndex(
(db) => db.name === currentDbName,
);
if (dbIndex === -1) {
throw Error(`Cannot find database '${currentDbName}' in local databases`);
}
config.databases.local.databases[dbIndex].name = newDbName;
}
if (
config.selected?.kind === SelectedDbItemKind.LocalDatabase &&
config.selected.databaseName === currentDbName
) {
config.selected.databaseName = newDbName;
}
return config;
}
export function removeLocalList(
originalConfig: DbConfig,
listName: string,
): DbConfig {
const config = cloneDbConfig(originalConfig);
config.databases.local.lists = config.databases.local.lists.filter(
(list) => list.name !== listName,
);
if (config.selected?.kind === SelectedDbItemKind.LocalUserDefinedList) {
config.selected = undefined;
}
if (
config.selected?.kind === SelectedDbItemKind.LocalDatabase &&
config.selected?.listName === listName
) {
config.selected = undefined;
}
return config;
}
export function removeRemoteList(
originalConfig: DbConfig,
listName: string,
@@ -255,35 +129,6 @@ export function removeRemoteList(
return config;
}
export function removeLocalDb(
originalConfig: DbConfig,
databaseName: string,
parentListName?: string,
): DbConfig {
const config = cloneDbConfig(originalConfig);
if (parentListName) {
const parentList = getLocalList(config, parentListName);
parentList.databases = parentList.databases.filter(
(db) => db.name !== databaseName,
);
} else {
config.databases.local.databases = config.databases.local.databases.filter(
(db) => db.name !== databaseName,
);
}
if (
config.selected?.kind === SelectedDbItemKind.LocalDatabase &&
config.selected?.databaseName === databaseName &&
config.selected?.listName === parentListName
) {
config.selected = undefined;
}
return config;
}
export function removeRemoteRepo(
originalConfig: DbConfig,
repoFullName: string,
@@ -333,51 +178,8 @@ export function removeRemoteOwner(
return config;
}
/**
* Removes local db config from a db config object, if one is set.
* We do this because we don't want to expose this feature to users
* yet (since it's only partially implemented), but we also don't want
* to remove all the code we've already implemented.
* @param config The config object to change.
* @returns Any removed local db config.
*/
export function clearLocalDbConfig(
config: DbConfig,
): LocalDbConfig | undefined {
let localDbs = undefined;
if (config && config.databases && config.databases.local) {
localDbs = config.databases.local;
delete (config.databases as any).local;
}
return localDbs;
}
/**
* Initializes the local db config, if the config object contains
* database configuration.
* @param config The config object to change.
*/
export function initializeLocalDbConfig(config: DbConfig): void {
if (config.databases) {
config.databases.local = { lists: [], databases: [] };
}
}
function cloneDbConfigSelectedItem(selected: SelectedDbItem): SelectedDbItem {
switch (selected.kind) {
case SelectedDbItemKind.LocalUserDefinedList:
return {
kind: SelectedDbItemKind.LocalUserDefinedList,
listName: selected.listName,
};
case SelectedDbItemKind.LocalDatabase:
return {
kind: SelectedDbItemKind.LocalDatabase,
databaseName: selected.databaseName,
listName: selected.listName,
};
case SelectedDbItemKind.VariantAnalysisSystemDefinedList:
return {
kind: SelectedDbItemKind.VariantAnalysisSystemDefinedList,
@@ -402,16 +204,6 @@ function cloneDbConfigSelectedItem(selected: SelectedDbItem): SelectedDbItem {
}
}
function getLocalList(config: DbConfig, listName: string): LocalList {
const list = config.databases.local.lists.find((l) => l.name === listName);
if (!list) {
throw Error(`Cannot find local list '${listName}'`);
}
return list;
}
function getRemoteList(
config: DbConfig,
listName: string,

View File

@@ -1,27 +1,14 @@
import { DbItem, DbItemKind, flattenDbItems } from "./db-item";
export type ExpandedDbItem =
| RootLocalExpandedDbItem
| LocalUserDefinedListExpandedDbItem
| RootRemoteExpandedDbItem
| RemoteUserDefinedListExpandedDbItem;
export enum ExpandedDbItemKind {
RootLocal = "rootLocal",
LocalUserDefinedList = "localUserDefinedList",
RootRemote = "rootRemote",
RemoteUserDefinedList = "remoteUserDefinedList",
}
interface RootLocalExpandedDbItem {
kind: ExpandedDbItemKind.RootLocal;
}
interface LocalUserDefinedListExpandedDbItem {
kind: ExpandedDbItemKind.LocalUserDefinedList;
listName: string;
}
interface RootRemoteExpandedDbItem {
kind: ExpandedDbItemKind.RootRemote;
}
@@ -80,13 +67,6 @@ export function cleanNonExistentExpandedItems(
function mapDbItemToExpandedDbItem(dbItem: DbItem): ExpandedDbItem {
switch (dbItem.kind) {
case DbItemKind.RootLocal:
return { kind: ExpandedDbItemKind.RootLocal };
case DbItemKind.LocalList:
return {
kind: ExpandedDbItemKind.LocalUserDefinedList,
listName: dbItem.listName,
};
case DbItemKind.RootRemote:
return { kind: ExpandedDbItemKind.RootRemote };
case DbItemKind.RemoteUserDefinedList:
@@ -104,13 +84,6 @@ function isDbItemEqualToExpandedDbItem(
expandedDbItem: ExpandedDbItem,
) {
switch (dbItem.kind) {
case DbItemKind.RootLocal:
return expandedDbItem.kind === ExpandedDbItemKind.RootLocal;
case DbItemKind.LocalList:
return (
expandedDbItem.kind === ExpandedDbItemKind.LocalUserDefinedList &&
expandedDbItem.listName === dbItem.listName
);
case DbItemKind.RootRemote:
return expandedDbItem.kind === ExpandedDbItemKind.RootRemote;
case DbItemKind.RemoteUserDefinedList:
@@ -118,7 +91,6 @@ function isDbItemEqualToExpandedDbItem(
expandedDbItem.kind === ExpandedDbItemKind.RemoteUserDefinedList &&
expandedDbItem.listName === dbItem.listName
);
case DbItemKind.LocalDatabase:
case DbItemKind.RemoteSystemDefinedList:
case DbItemKind.RemoteOwner:
case DbItemKind.RemoteRepo:

View File

@@ -2,17 +2,13 @@ import { DbItem, DbItemKind } from "./db-item";
export function getDbItemName(dbItem: DbItem): string | undefined {
switch (dbItem.kind) {
case DbItemKind.RootLocal:
case DbItemKind.RootRemote:
return undefined;
case DbItemKind.LocalList:
case DbItemKind.RemoteUserDefinedList:
case DbItemKind.RemoteSystemDefinedList:
return dbItem.listName;
case DbItemKind.RemoteOwner:
return dbItem.ownerName;
case DbItemKind.LocalDatabase:
return dbItem.databaseName;
case DbItemKind.RemoteRepo:
return dbItem.repoFullName;
}

View File

@@ -1,12 +1,9 @@
import { DbItem, DbItemKind, LocalDbItem, RemoteDbItem } from "./db-item";
import { DbItem, DbItemKind, RemoteDbItem } from "./db-item";
import { SelectedDbItem, SelectedDbItemKind } from "./config/db-config";
export function getSelectedDbItem(dbItems: DbItem[]): DbItem | undefined {
for (const dbItem of dbItems) {
if (
dbItem.kind === DbItemKind.RootRemote ||
dbItem.kind === DbItemKind.RootLocal
) {
if (dbItem.kind === DbItemKind.RootRemote) {
for (const child of dbItem.children) {
const selectedItem = extractSelected(child);
if (selectedItem) {
@@ -23,20 +20,11 @@ export function getSelectedDbItem(dbItems: DbItem[]): DbItem | undefined {
return undefined;
}
function extractSelected(
dbItem: RemoteDbItem | LocalDbItem,
): DbItem | undefined {
function extractSelected(dbItem: RemoteDbItem): DbItem | undefined {
if (dbItem.selected) {
return dbItem;
}
switch (dbItem.kind) {
case DbItemKind.LocalList:
for (const database of dbItem.databases) {
if (database.selected) {
return database;
}
}
break;
case DbItemKind.RemoteUserDefinedList:
for (const repo of dbItem.repos) {
if (repo.selected) {
@@ -52,17 +40,10 @@ export function mapDbItemToSelectedDbItem(
dbItem: DbItem,
): SelectedDbItem | undefined {
switch (dbItem.kind) {
case DbItemKind.RootLocal:
case DbItemKind.RootRemote:
// Root items are not selectable.
return undefined;
case DbItemKind.LocalList:
return {
kind: SelectedDbItemKind.LocalUserDefinedList,
listName: dbItem.listName,
};
case DbItemKind.RemoteUserDefinedList:
return {
kind: SelectedDbItemKind.VariantAnalysisUserDefinedList,
@@ -81,13 +62,6 @@ export function mapDbItemToSelectedDbItem(
ownerName: dbItem.ownerName,
};
case DbItemKind.LocalDatabase:
return {
kind: SelectedDbItemKind.LocalDatabase,
databaseName: dbItem.databaseName,
listName: dbItem?.parentListName,
};
case DbItemKind.RemoteRepo:
return {
kind: SelectedDbItemKind.VariantAnalysisRepository,

View File

@@ -1,11 +1,6 @@
// This file contains models that are used to represent the databases.
import { DatabaseOrigin } from "./local-databases/database-origin";
export enum DbItemKind {
RootLocal = "RootLocal",
LocalList = "LocalList",
LocalDatabase = "LocalDatabase",
RootRemote = "RootRemote",
RemoteSystemDefinedList = "RemoteSystemDefinedList",
RemoteUserDefinedList = "RemoteUserDefinedList",
@@ -13,49 +8,13 @@ export enum DbItemKind {
RemoteRepo = "RemoteRepo",
}
export enum DbListKind {
Local = "Local",
Remote = "Remote",
}
export interface RootLocalDbItem {
kind: DbItemKind.RootLocal;
expanded: boolean;
children: LocalDbItem[];
}
export type LocalDbItem = LocalListDbItem | LocalDatabaseDbItem;
export interface LocalListDbItem {
kind: DbItemKind.LocalList;
expanded: boolean;
selected: boolean;
listName: string;
databases: LocalDatabaseDbItem[];
}
export interface LocalDatabaseDbItem {
kind: DbItemKind.LocalDatabase;
selected: boolean;
databaseName: string;
dateAdded: number;
language: string;
origin: DatabaseOrigin;
storagePath: string;
parentListName?: string;
}
export interface RootRemoteDbItem {
kind: DbItemKind.RootRemote;
expanded: boolean;
children: RemoteDbItem[];
}
export type DbItem =
| RootLocalDbItem
| RootRemoteDbItem
| RemoteDbItem
| LocalDbItem;
export type DbItem = RootRemoteDbItem | RemoteDbItem;
export type RemoteDbItem =
| RemoteSystemDefinedListDbItem
@@ -108,25 +67,13 @@ export function isRemoteRepoDbItem(dbItem: DbItem): dbItem is RemoteRepoDbItem {
return dbItem.kind === DbItemKind.RemoteRepo;
}
export function isLocalListDbItem(dbItem: DbItem): dbItem is LocalListDbItem {
return dbItem.kind === DbItemKind.LocalList;
}
export function isLocalDatabaseDbItem(
dbItem: DbItem,
): dbItem is LocalDatabaseDbItem {
return dbItem.kind === DbItemKind.LocalDatabase;
}
type SelectableDbItem = RemoteDbItem | LocalDbItem;
type SelectableDbItem = RemoteDbItem;
export function isSelectableDbItem(dbItem: DbItem): dbItem is SelectableDbItem {
return SelectableDbItemKinds.includes(dbItem.kind);
}
const SelectableDbItemKinds = [
DbItemKind.LocalList,
DbItemKind.LocalDatabase,
DbItemKind.RemoteSystemDefinedList,
DbItemKind.RemoteUserDefinedList,
DbItemKind.RemoteOwner,
@@ -139,19 +86,12 @@ export function flattenDbItems(dbItems: DbItem[]): DbItem[] {
for (const dbItem of dbItems) {
allItems.push(dbItem);
switch (dbItem.kind) {
case DbItemKind.RootLocal:
allItems.push(...flattenDbItems(dbItem.children));
break;
case DbItemKind.LocalList:
allItems.push(...flattenDbItems(dbItem.databases));
break;
case DbItemKind.RootRemote:
allItems.push(...flattenDbItems(dbItem.children));
break;
case DbItemKind.RemoteUserDefinedList:
allItems.push(...dbItem.repos);
break;
case DbItemKind.LocalDatabase:
case DbItemKind.RemoteSystemDefinedList:
case DbItemKind.RemoteOwner:
case DbItemKind.RemoteRepo:

View File

@@ -3,14 +3,7 @@ import { AppEvent, AppEventEmitter } from "../common/events";
import { ValueResult } from "../common/value-result";
import { DisposableObject } from "../common/disposable-object";
import { DbConfigStore } from "./config/db-config-store";
import {
DbItem,
DbItemKind,
DbListKind,
LocalDatabaseDbItem,
LocalListDbItem,
RemoteUserDefinedListDbItem,
} from "./db-item";
import { DbItem, RemoteUserDefinedListDbItem } from "./db-item";
import {
updateExpandedItem,
replaceExpandedItem,
@@ -116,31 +109,15 @@ export class DbManager extends DisposableObject {
await this.dbConfigStore.addRemoteOwner(owner);
}
public async addNewList(
listKind: DbListKind,
listName: string,
): Promise<void> {
switch (listKind) {
case DbListKind.Local:
await this.dbConfigStore.addLocalList(listName);
break;
case DbListKind.Remote:
await this.dbConfigStore.addRemoteList(listName);
break;
default:
throw Error(`Unknown list kind '${listKind}'`);
}
public async addNewList(listName: string): Promise<void> {
await this.dbConfigStore.addRemoteList(listName);
}
public async renameList(
currentDbItem: LocalListDbItem | RemoteUserDefinedListDbItem,
currentDbItem: RemoteUserDefinedListDbItem,
newName: string,
): Promise<void> {
if (currentDbItem.kind === DbItemKind.LocalList) {
await this.dbConfigStore.renameLocalList(currentDbItem, newName);
} else if (currentDbItem.kind === DbItemKind.RemoteUserDefinedList) {
await this.dbConfigStore.renameRemoteList(currentDbItem, newName);
}
await this.dbConfigStore.renameRemoteList(currentDbItem, newName);
const newDbItem = { ...currentDbItem, listName: newName };
const newExpandedItems = replaceExpandedItem(
@@ -152,26 +129,8 @@ export class DbManager extends DisposableObject {
await this.setExpandedItems(newExpandedItems);
}
public async renameLocalDb(
currentDbItem: LocalDatabaseDbItem,
newName: string,
): Promise<void> {
await this.dbConfigStore.renameLocalDb(
currentDbItem,
newName,
currentDbItem.parentListName,
);
}
public doesListExist(listKind: DbListKind, listName: string): boolean {
switch (listKind) {
case DbListKind.Local:
return this.dbConfigStore.doesLocalListExist(listName);
case DbListKind.Remote:
return this.dbConfigStore.doesRemoteListExist(listName);
default:
throw Error(`Unknown list kind '${listKind}'`);
}
public doesListExist(listName: string): boolean {
return this.dbConfigStore.doesRemoteListExist(listName);
}
public doesRemoteOwnerExist(owner: string): boolean {
@@ -182,10 +141,6 @@ export class DbManager extends DisposableObject {
return this.dbConfigStore.doesRemoteDbExist(nwo, listName);
}
public doesLocalDbExist(dbName: string, listName?: string): boolean {
return this.dbConfigStore.doesLocalDbExist(dbName, listName);
}
private getExpandedItems(): ExpandedDbItem[] {
const items = this.app.workspaceState.get<ExpandedDbItem[]>(
DbManager.DB_EXPANDED_STATE_KEY,

View File

@@ -1,19 +1,14 @@
import {
DbConfig,
LocalDatabase,
LocalList,
RemoteRepositoryList,
SelectedDbItemKind,
} from "./config/db-config";
import {
DbItemKind,
LocalDatabaseDbItem,
LocalListDbItem,
RemoteOwnerDbItem,
RemoteRepoDbItem,
RemoteSystemDefinedListDbItem,
RemoteUserDefinedListDbItem,
RootLocalDbItem,
RootRemoteDbItem,
} from "./db-item";
import { ExpandedDbItem, ExpandedDbItemKind } from "./db-item-expansion";
@@ -55,28 +50,6 @@ export function createRemoteTree(
};
}
export function createLocalTree(
dbConfig: DbConfig,
expandedItems: ExpandedDbItem[],
): RootLocalDbItem {
const localLists = dbConfig.databases.local.lists.map((l) =>
createLocalList(l, dbConfig, expandedItems),
);
const localDbs = dbConfig.databases.local.databases.map((l) =>
createLocalDb(l, dbConfig),
);
const expanded = expandedItems.some(
(e) => e.kind === ExpandedDbItemKind.RootLocal,
);
return {
kind: DbItemKind.RootLocal,
children: [...localLists, ...localDbs],
expanded: !!expanded,
};
}
function createSystemDefinedList(
n: number,
dbConfig: DbConfig,
@@ -155,51 +128,3 @@ function createRepoItem(
parentListName: listName,
};
}
function createLocalList(
list: LocalList,
dbConfig: DbConfig,
expandedItems: ExpandedDbItem[],
): LocalListDbItem {
const selected =
dbConfig.selected &&
dbConfig.selected.kind === SelectedDbItemKind.LocalUserDefinedList &&
dbConfig.selected.listName === list.name;
const expanded = expandedItems.some(
(e) =>
e.kind === ExpandedDbItemKind.LocalUserDefinedList &&
e.listName === list.name,
);
return {
kind: DbItemKind.LocalList,
listName: list.name,
databases: list.databases.map((d) => createLocalDb(d, dbConfig, list.name)),
selected: !!selected,
expanded: !!expanded,
};
}
function createLocalDb(
db: LocalDatabase,
dbConfig: DbConfig,
listName?: string,
): LocalDatabaseDbItem {
const selected =
dbConfig.selected &&
dbConfig.selected.kind === SelectedDbItemKind.LocalDatabase &&
dbConfig.selected.databaseName === db.name &&
dbConfig.selected.listName === listName;
return {
kind: DbItemKind.LocalDatabase,
databaseName: db.name,
dateAdded: db.dateAdded,
language: db.language,
origin: db.origin,
storagePath: db.storagePath,
selected: !!selected,
parentListName: listName,
};
}

View File

@@ -424,9 +424,8 @@ export class DatabaseManager extends DisposableObject {
step: ++step,
});
const databaseItem = await this.createDatabaseItemFromPersistedState(
database,
);
const databaseItem =
await this.createDatabaseItemFromPersistedState(database);
try {
await this.refreshDatabase(databaseItem);
await this.registerDatabase(databaseItem);

View File

@@ -9,20 +9,15 @@ import {
window as Window,
workspace,
} from "vscode";
import {
LineColumnLocation,
ResolvableLocationValue,
UrlValue,
WholeFileLocation,
} from "../../common/bqrs-cli-types";
import {
isLineColumnLoc,
tryGetResolvableLocation,
} from "../../common/bqrs-utils";
import { getErrorMessage } from "../../common/helpers-pure";
import { assertNever, getErrorMessage } from "../../common/helpers-pure";
import { Logger } from "../../common/logging";
import { DatabaseItem } from "./database-item";
import { DatabaseManager } from "./database-manager";
import {
UrlValueLineColumnLocation,
UrlValueResolvable,
UrlValueWholeFileLocation,
} from "../../common/raw-result-types";
const findMatchBackground = new ThemeColor("editor.findMatchBackground");
const findRangeHighlightBackground = new ThemeColor(
@@ -45,7 +40,7 @@ export const shownLocationLineDecoration =
* @param databaseItem Database in which to resolve the file location.
*/
function resolveFivePartLocation(
loc: LineColumnLocation,
loc: UrlValueLineColumnLocation,
databaseItem: DatabaseItem,
): Location {
// `Range` is a half-open interval, and is zero-based. CodeQL locations are closed intervals, and
@@ -66,7 +61,7 @@ function resolveFivePartLocation(
* @param databaseItem Database in which to resolve the filesystem resource location.
*/
function resolveWholeFileLocation(
loc: WholeFileLocation,
loc: UrlValueWholeFileLocation,
databaseItem: DatabaseItem,
): Location {
// A location corresponding to the start of the file.
@@ -81,21 +76,25 @@ function resolveWholeFileLocation(
* @param databaseItem Database in which to resolve the file location.
*/
export function tryResolveLocation(
loc: UrlValue | undefined,
loc: UrlValueResolvable | undefined,
databaseItem: DatabaseItem,
): Location | undefined {
const resolvableLoc = tryGetResolvableLocation(loc);
if (!resolvableLoc || typeof resolvableLoc === "string") {
if (!loc) {
return;
} else if (isLineColumnLoc(resolvableLoc)) {
return resolveFivePartLocation(resolvableLoc, databaseItem);
} else {
return resolveWholeFileLocation(resolvableLoc, databaseItem);
}
switch (loc.type) {
case "wholeFileLocation":
return resolveWholeFileLocation(loc, databaseItem);
case "lineColumnLocation":
return resolveFivePartLocation(loc, databaseItem);
default:
assertNever(loc);
}
}
export async function showResolvableLocation(
loc: ResolvableLocationValue,
loc: UrlValueResolvable,
databaseItem: DatabaseItem,
logger: Logger,
): Promise<void> {
@@ -153,7 +152,7 @@ export async function showLocation(location?: Location) {
export async function jumpToLocation(
databaseUri: string,
loc: ResolvableLocationValue,
loc: UrlValueResolvable,
databaseManager: DatabaseManager,
logger: Logger,
) {

View File

@@ -1,6 +1,5 @@
import { DbItem, DbItemKind } from "../db-item";
import {
createDbTreeViewItemLocalDatabase,
createDbTreeViewItemOwner,
createDbTreeViewItemRepo,
createDbTreeViewItemRoot,
@@ -11,14 +10,6 @@ import {
export function mapDbItemToTreeViewItem(dbItem: DbItem): DbTreeViewItem {
switch (dbItem.kind) {
case DbItemKind.RootLocal:
return createDbTreeViewItemRoot(
dbItem,
"local",
"Local databases",
dbItem.children.map((c) => mapDbItemToTreeViewItem(c)),
);
case DbItemKind.RootRemote:
return createDbTreeViewItemRoot(
dbItem,
@@ -46,19 +37,5 @@ export function mapDbItemToTreeViewItem(dbItem: DbItem): DbTreeViewItem {
case DbItemKind.RemoteRepo:
return createDbTreeViewItemRepo(dbItem, dbItem.repoFullName);
case DbItemKind.LocalList:
return createDbTreeViewItemUserDefinedList(
dbItem,
dbItem.listName,
dbItem.databases.map(mapDbItemToTreeViewItem),
);
case DbItemKind.LocalDatabase:
return createDbTreeViewItemLocalDatabase(
dbItem,
dbItem.databaseName,
dbItem.language,
);
}
}

View File

@@ -17,14 +17,7 @@ import {
isValidGitHubOwner,
} from "../../common/github-url-identifier-helper";
import { DisposableObject } from "../../common/disposable-object";
import {
DbItem,
DbItemKind,
DbListKind,
LocalDatabaseDbItem,
LocalListDbItem,
RemoteUserDefinedListDbItem,
} from "../db-item";
import { DbItem, DbItemKind, RemoteUserDefinedListDbItem } from "../db-item";
import { getDbItemName } from "../db-item-naming";
import { DbManager } from "../db-manager";
import { DbTreeDataProvider } from "./db-tree-data-provider";
@@ -42,10 +35,6 @@ export interface RemoteDatabaseQuickPickItem extends QuickPickItem {
remoteDatabaseKind: string;
}
export interface AddListQuickPickItem extends QuickPickItem {
databaseKind: DbListKind;
}
interface CodeSearchQuickPickItem extends QuickPickItem {
language: string;
}
@@ -223,8 +212,6 @@ export class DbPanel extends DisposableObject {
}
private async addNewList(): Promise<void> {
const listKind = DbListKind.Remote;
const listName = await window.showInputBox({
prompt: "Enter a name for the new list",
placeHolder: "example-list",
@@ -233,7 +220,7 @@ export class DbPanel extends DisposableObject {
return;
}
if (this.dbManager.doesListExist(listKind, listName)) {
if (this.dbManager.doesListExist(listName)) {
void showAndLogErrorMessage(
this.app.logger,
`The list '${listName}' already exists`,
@@ -241,7 +228,7 @@ export class DbPanel extends DisposableObject {
return;
}
await this.dbManager.addNewList(listKind, listName);
await this.dbManager.addNewList(listName);
}
private async setSelectedItem(treeViewItem: DbTreeViewItem): Promise<void> {
@@ -277,59 +264,13 @@ export class DbPanel extends DisposableObject {
return;
}
switch (dbItem.kind) {
case DbItemKind.LocalList:
await this.renameLocalListItem(dbItem, newName);
break;
case DbItemKind.LocalDatabase:
await this.renameLocalDatabaseItem(dbItem, newName);
break;
case DbItemKind.RemoteUserDefinedList:
await this.renameVariantAnalysisUserDefinedListItem(dbItem, newName);
break;
default:
throw Error(`Action not allowed for the '${dbItem.kind}' db item kind`);
if (dbItem.kind === DbItemKind.RemoteUserDefinedList) {
await this.renameVariantAnalysisUserDefinedListItem(dbItem, newName);
} else {
throw Error(`Action not allowed for the '${dbItem.kind}' db item kind`);
}
}
private async renameLocalListItem(
dbItem: LocalListDbItem,
newName: string,
): Promise<void> {
if (dbItem.listName === newName) {
return;
}
if (this.dbManager.doesListExist(DbListKind.Local, newName)) {
void showAndLogErrorMessage(
this.app.logger,
`The list '${newName}' already exists`,
);
return;
}
await this.dbManager.renameList(dbItem, newName);
}
private async renameLocalDatabaseItem(
dbItem: LocalDatabaseDbItem,
newName: string,
): Promise<void> {
if (dbItem.databaseName === newName) {
return;
}
if (this.dbManager.doesLocalDbExist(newName, dbItem.parentListName)) {
void showAndLogErrorMessage(
this.app.logger,
`The database '${newName}' already exists`,
);
return;
}
await this.dbManager.renameLocalDb(dbItem, newName);
}
private async renameVariantAnalysisUserDefinedListItem(
dbItem: RemoteUserDefinedListDbItem,
newName: string,
@@ -338,7 +279,7 @@ export class DbPanel extends DisposableObject {
return;
}
if (this.dbManager.doesListExist(DbListKind.Remote, newName)) {
if (this.dbManager.doesListExist(newName)) {
void showAndLogErrorMessage(
this.app.logger,
`The list '${newName}' already exists`,

View File

@@ -29,18 +29,12 @@ export function getDbItemActions(dbItem: DbItem): DbTreeViewItemAction[] {
}
const dbItemKindsThatCanBeRemoved = [
DbItemKind.LocalList,
DbItemKind.RemoteUserDefinedList,
DbItemKind.LocalDatabase,
DbItemKind.RemoteRepo,
DbItemKind.RemoteOwner,
];
const dbItemKindsThatCanBeRenamed = [
DbItemKind.LocalList,
DbItemKind.RemoteUserDefinedList,
DbItemKind.LocalDatabase,
];
const dbItemKindsThatCanBeRenamed = [DbItemKind.RemoteUserDefinedList];
const dbItemKindsThatCanBeOpenedOnGitHub = [
DbItemKind.RemoteOwner,

View File

@@ -2,13 +2,10 @@ import * as vscode from "vscode";
import {
DbItem,
isSelectableDbItem,
LocalDatabaseDbItem,
LocalListDbItem,
RemoteOwnerDbItem,
RemoteRepoDbItem,
RemoteSystemDefinedListDbItem,
RemoteUserDefinedListDbItem,
RootLocalDbItem,
RootRemoteDbItem,
} from "../db-item";
import { getDbItemActions } from "./db-tree-view-item-action";
@@ -74,7 +71,7 @@ export function createDbTreeViewItemError(
}
export function createDbTreeViewItemRoot(
dbItem: RootLocalDbItem | RootRemoteDbItem,
dbItem: RootRemoteDbItem,
label: string,
tooltip: string,
children: DbTreeViewItem[],
@@ -105,7 +102,7 @@ export function createDbTreeViewItemSystemDefinedList(
}
export function createDbTreeViewItemUserDefinedList(
dbItem: LocalListDbItem | RemoteUserDefinedListDbItem,
dbItem: RemoteUserDefinedListDbItem,
listName: string,
children: DbTreeViewItem[],
): DbTreeViewItem {
@@ -147,21 +144,6 @@ export function createDbTreeViewItemRepo(
);
}
export function createDbTreeViewItemLocalDatabase(
dbItem: LocalDatabaseDbItem,
databaseName: string,
language: string,
): DbTreeViewItem {
return new DbTreeViewItem(
dbItem,
new vscode.ThemeIcon("database"),
databaseName,
`Language: ${language}`,
vscode.TreeItemCollapsibleState.None,
[],
);
}
function getCollapsibleState(
expanded: boolean,
): vscode.TreeItemCollapsibleState {

View File

@@ -542,8 +542,8 @@ async function installOrUpdateDistribution(
const messageText = willUpdateCodeQl
? "Updating CodeQL CLI"
: codeQlInstalled
? "Checking for updates to CodeQL CLI"
: "Installing CodeQL CLI";
? "Checking for updates to CodeQL CLI"
: "Installing CodeQL CLI";
try {
await installOrUpdateDistributionWithProgressTitle(
@@ -564,8 +564,8 @@ async function installOrUpdateDistribution(
willUpdateCodeQl
? "update"
: codeQlInstalled
? "check for updates to"
: "install"
? "check for updates to"
: "install"
} CodeQL CLI`;
if (e instanceof GithubRateLimitedError) {
@@ -1086,23 +1086,27 @@ async function activateWithInstalledDistribution(
// Jump-to-definition and find-references
void extLogger.log("Registering jump-to-definition handlers.");
languages.registerDefinitionProvider(
{ scheme: zipArchiveScheme },
new TemplateQueryDefinitionProvider(
cliServer,
qs,
dbm,
contextualQueryStorageDir,
ctx.subscriptions.push(
languages.registerDefinitionProvider(
{ scheme: zipArchiveScheme },
new TemplateQueryDefinitionProvider(
cliServer,
qs,
dbm,
contextualQueryStorageDir,
),
),
);
languages.registerReferenceProvider(
{ scheme: zipArchiveScheme },
new TemplateQueryReferenceProvider(
cliServer,
qs,
dbm,
contextualQueryStorageDir,
ctx.subscriptions.push(
languages.registerReferenceProvider(
{ scheme: zipArchiveScheme },
new TemplateQueryReferenceProvider(
cliServer,
qs,
dbm,
contextualQueryStorageDir,
),
),
);

View File

@@ -2,13 +2,14 @@ import { CodeQLCliServer } from "../../codeql-cli/cli";
import {
DecodedBqrsChunk,
BqrsId,
EntityValue,
BqrsEntityValue,
} from "../../common/bqrs-cli-types";
import { DatabaseItem } from "../../databases/local-databases";
import { ChildAstItem, AstItem } from "./ast-viewer";
import { Uri } from "vscode";
import { QueryOutputDir } from "../../run-queries-shared";
import { fileRangeFromURI } from "../contextual/file-range-from-uri";
import { mapUrlValue } from "../../common/bqrs-raw-results-mapper";
/**
* A class that wraps a tree of QL results from a query that
@@ -55,8 +56,8 @@ export class AstBuilder {
// Build up the parent-child relationships
edgeTuples.tuples.forEach((tuple) => {
const [source, target, tupleType, value] = tuple as [
EntityValue,
EntityValue,
BqrsEntityValue,
BqrsEntityValue,
string,
string,
];
@@ -90,7 +91,11 @@ export class AstBuilder {
// populate parents and children
nodeTuples.tuples.forEach((tuple) => {
const [entity, tupleType, value] = tuple as [EntityValue, string, string];
const [entity, tupleType, value] = tuple as [
BqrsEntityValue,
string,
string,
];
const id = entity.id!;
switch (tupleType) {
@@ -106,7 +111,7 @@ export class AstBuilder {
const item = {
id,
label,
location: entity.url,
location: entity.url ? mapUrlValue(entity.url) : undefined,
fileLocation: fileRangeFromURI(entity.url, this.db),
children: [] as ChildAstItem[],
order: Number.MAX_SAFE_INTEGER,

View File

@@ -16,20 +16,20 @@ import {
import { basename } from "path";
import { DatabaseItem } from "../../databases/local-databases";
import { UrlValue, BqrsId } from "../../common/bqrs-cli-types";
import { BqrsId } from "../../common/bqrs-cli-types";
import { showLocation } from "../../databases/local-databases/locations";
import {
isStringLoc,
isWholeFileLoc,
isLineColumnLoc,
} from "../../common/bqrs-utils";
import { DisposableObject } from "../../common/disposable-object";
import { asError, getErrorMessage } from "../../common/helpers-pure";
import {
asError,
assertNever,
getErrorMessage,
} from "../../common/helpers-pure";
import { redactableError } from "../../common/errors";
import { AstViewerCommands } from "../../common/commands";
import { extLogger } from "../../common/logging/vscode";
import { showAndLogExceptionWithTelemetry } from "../../common/logging";
import { telemetryListener } from "../../common/vscode/telemetry";
import { UrlValue } from "../../common/raw-result-types";
export interface AstItem {
id: BqrsId;
@@ -90,15 +90,18 @@ class AstViewerDataProvider
private extractLineInfo(loc?: UrlValue) {
if (!loc) {
return "";
} else if (isStringLoc(loc)) {
return loc;
} else if (isWholeFileLoc(loc)) {
return loc.uri;
} else if (isLineColumnLoc(loc)) {
return loc.startLine;
} else {
return "";
return;
}
switch (loc.type) {
case "string":
return loc.value;
case "wholeFileLocation":
return loc.uri;
case "lineColumnLocation":
return loc.startLine;
default:
assertNever(loc);
}
}
}

View File

@@ -1,11 +1,14 @@
import * as vscode from "vscode";
import { UrlValue, LineColumnLocation } from "../../common/bqrs-cli-types";
import {
BqrsUrlValue,
BqrsLineColumnLocation,
} from "../../common/bqrs-cli-types";
import { isEmptyPath } from "../../common/bqrs-utils";
import { DatabaseItem } from "../../databases/local-databases";
export function fileRangeFromURI(
uri: UrlValue | undefined,
uri: BqrsUrlValue | undefined,
db: DatabaseItem,
): vscode.Location | undefined {
if (!uri || typeof uri === "string") {
@@ -13,7 +16,7 @@ export function fileRangeFromURI(
} else if ("startOffset" in uri) {
return undefined;
} else {
const loc = uri as LineColumnLocation;
const loc = uri as BqrsLineColumnLocation;
if (isEmptyPath(loc.uri)) {
return undefined;
}

View File

@@ -3,10 +3,9 @@ import {
encodeArchiveBasePath,
} from "../../common/vscode/archive-filesystem-provider";
import {
ColumnKindCode,
EntityValue,
getResultSetSchema,
ResultSetSchema,
BqrsColumnKindCode,
BqrsEntityValue,
BqrsResultSetSchema,
} from "../../common/bqrs-cli-types";
import { CodeQLCliServer } from "../../codeql-cli/cli";
import { DatabaseItem, DatabaseManager } from "../../databases/local-databases";
@@ -99,12 +98,14 @@ async function getLinksFromResults(
const localLinks: FullLocationLink[] = [];
const bqrsPath = outputDir.bqrsPath;
const info = await cli.bqrsInfo(bqrsPath);
const selectInfo = getResultSetSchema(SELECT_QUERY_NAME, info);
const selectInfo = info["result-sets"].find(
(schema) => schema.name === SELECT_QUERY_NAME,
);
if (isValidSelect(selectInfo)) {
// TODO: Page this
const allTuples = await cli.bqrsDecode(bqrsPath, SELECT_QUERY_NAME);
for (const tuple of allTuples.tuples) {
const [src, dest] = tuple as [EntityValue, EntityValue];
const [src, dest] = tuple as [BqrsEntityValue, BqrsEntityValue];
const srcFile = src.url && fileRangeFromURI(src.url, db);
const destFile = dest.url && fileRangeFromURI(dest.url, db);
if (
@@ -130,12 +131,12 @@ function createTemplates(path: string): Record<string, string> {
};
}
function isValidSelect(selectInfo: ResultSetSchema | undefined) {
function isValidSelect(selectInfo: BqrsResultSetSchema | undefined) {
return (
selectInfo &&
selectInfo.columns.length === 3 &&
selectInfo.columns[0].kind === ColumnKindCode.ENTITY &&
selectInfo.columns[1].kind === ColumnKindCode.ENTITY &&
selectInfo.columns[2].kind === ColumnKindCode.STRING
selectInfo.columns[0].kind === BqrsColumnKindCode.ENTITY &&
selectInfo.columns[1].kind === BqrsColumnKindCode.ENTITY &&
selectInfo.columns[2].kind === BqrsColumnKindCode.STRING
);
}

View File

@@ -88,25 +88,18 @@ export class TemplateQueryDefinitionProvider implements DefinitionProvider {
uriString: string,
token: CancellationToken,
): Promise<LocationLink[]> {
return withProgress(
async (progress, tokenInner) => {
const multiToken = new MultiCancellationToken(token, tokenInner);
return getLocationsForUriString(
this.cli,
this.qs,
this.dbm,
uriString,
KeyType.DefinitionQuery,
this.queryStorageDir,
progress,
multiToken,
(src, _dest) => src === uriString,
);
},
{
cancellable: true,
title: "Finding definitions",
},
// Do not create a multitoken here. There will be no popup and users cannot click on anything to cancel this operation.
// This is because finding definitions can be triggered by a hover, which should not have a popup.
return getLocationsForUriString(
this.cli,
this.qs,
this.dbm,
uriString,
KeyType.DefinitionQuery,
this.queryStorageDir,
() => {}, // noop
token,
(src, _dest) => src === uriString,
);
}
}
@@ -161,6 +154,7 @@ export class TemplateQueryReferenceProvider implements ReferenceProvider {
uriString: string,
token: CancellationToken,
): Promise<FullLocationLink[]> {
// Create a multitoken here. There will be a popup and users can click on it to cancel this operation.
return withProgress(
async (progress, tokenInner) => {
const multiToken = new MultiCancellationToken(token, tokenInner);

View File

@@ -60,11 +60,7 @@ import {
shownLocationLineDecoration,
jumpToLocation,
} from "../databases/local-databases/locations";
import {
RawResultSet,
transformBqrsResultSet,
ResultSetSchema,
} from "../common/bqrs-cli-types";
import { bqrsToResultSet } from "../common/bqrs-raw-results-mapper";
import {
AbstractWebview,
WebviewPanelConfig,
@@ -76,6 +72,8 @@ import { redactableError } from "../common/errors";
import { ResultsViewCommands } from "../common/commands";
import { App } from "../common/app";
import { Disposable } from "../common/disposable-object";
import { RawResultSet } from "../common/raw-result-types";
import { BqrsResultSetSchema } from "../common/bqrs-cli-types";
/**
* results-view.ts
@@ -106,9 +104,9 @@ function sortInterpretedResults(
a.message.text === undefined
? 0
: b.message.text === undefined
? 0
: multiplier *
a.message.text?.localeCompare(b.message.text, env.language),
? 0
: multiplier *
a.message.text?.localeCompare(b.message.text, env.language),
);
break;
default:
@@ -136,7 +134,7 @@ function numPagesOfResultSet(
const n =
interpretation?.data.t === "GraphInterpretationData"
? interpretation.data.dot.length
: resultSet.schema.rows;
: resultSet.totalRowCount;
return Math.ceil(n / pageSize);
}
@@ -524,16 +522,16 @@ export class ResultsView extends AbstractWebview<
offset: schema.pagination?.offsets[0],
pageSize,
});
const resultSet = transformBqrsResultSet(schema, chunk);
const resultSet = bqrsToResultSet(schema, chunk);
fullQuery.completedQuery.setResultCount(
interpretationPage?.numTotalResults || resultSet.schema.rows,
interpretationPage?.numTotalResults || resultSet.totalRowCount,
);
const parsedResultSets: ParsedResultSets = {
pageNumber: 0,
pageSize,
numPages: numPagesOfResultSet(resultSet, this._interpretation),
numInterpretedPages: numInterpretedPages(this._interpretation),
resultSet: { ...resultSet, t: "RawResultSet" },
resultSet: { t: "RawResultSet", resultSet },
selectedTable: undefined,
resultSetNames,
};
@@ -601,7 +599,7 @@ export class ResultsView extends AbstractWebview<
private async getResultSetSchemas(
completedQuery: CompletedQueryInfo,
selectedTable = "",
): Promise<ResultSetSchema[]> {
): Promise<BqrsResultSetSchema[]> {
const resultsPath = completedQuery.getResultsPath(selectedTable);
const schemas = await this.cliServer.bqrsInfo(
resultsPath,
@@ -668,12 +666,12 @@ export class ResultsView extends AbstractWebview<
pageSize,
},
);
const resultSet = transformBqrsResultSet(schema, chunk);
const resultSet = bqrsToResultSet(schema, chunk);
const parsedResultSets: ParsedResultSets = {
pageNumber,
pageSize,
resultSet: { t: "RawResultSet", ...resultSet },
resultSet: { t: "RawResultSet", resultSet },
numPages: numPagesOfResultSet(resultSet),
numInterpretedPages: numInterpretedPages(this._interpretation),
selectedTable,

View File

@@ -507,9 +507,8 @@ export class SkeletonQueryWizard {
): Promise<DatabaseItem | undefined> {
const defaultDatabaseNwo = QUERY_LANGUAGE_TO_DATABASE_REPO[language];
const dbItems = await SkeletonQueryWizard.sortDatabaseItemsByDateAdded(
databaseItems,
);
const dbItems =
await SkeletonQueryWizard.sortDatabaseItemsByDateAdded(databaseItems);
const defaultDatabaseItem = await SkeletonQueryWizard.findDatabaseItemByNwo(
language,

View File

@@ -37,45 +37,49 @@ function makeKey(
return `${queryCausingWork}:${predicate}${suffix ? ` ${suffix}` : ""}`;
}
const DEPENDENT_PREDICATES_REGEXP = (() => {
function getDependentPredicates(operations: string[]): I.List<string> {
const id = String.raw`[0-9a-zA-Z:#_\./]+`;
const idWithAngleBrackets = String.raw`[0-9a-zA-Z:#_<>\./]+`;
const quotedId = String.raw`\`[^\`\r\n]*\``;
const regexps = [
// SCAN id
String.raw`SCAN\s+([0-9a-zA-Z:#_]+|\`[^\`\r\n]*\`)\s`,
String.raw`SCAN\s+(${id}|${quotedId})\s`,
// JOIN id WITH id
String.raw`JOIN\s+([0-9a-zA-Z:#_]+|\`[^\`\r\n]*\`)\s+WITH\s+([0-9a-zA-Z:#_]+|\`[^\`\r\n]*\`)\s`,
String.raw`JOIN\s+(${id}|${quotedId})\s+WITH\s+(${id}|${quotedId})\s`,
// JOIN WITH id
String.raw`JOIN\s+WITH\s+(${id}|${quotedId})\s`,
// AGGREGATE id, id
String.raw`AGGREGATE\s+([0-9a-zA-Z:#_]+|\`[^\`\r\n]*\`)\s*,\s+([0-9a-zA-Z:#_]+|\`[^\`\r\n]*\`)`,
String.raw`AGGREGATE\s+(${id}|${quotedId})\s*,\s+(${id}|${quotedId})`,
// id AND NOT id
String.raw`([0-9a-zA-Z:#_]+|\`[^\`\r\n]*\`)\s+AND\s+NOT\s+([0-9a-zA-Z:#_]+|\`[^\`\r\n]*\`)`,
String.raw`(${id}|${quotedId})\s+AND\s+NOT\s+(${id}|${quotedId})`,
// AND NOT id
String.raw`AND\s+NOT\s+(${id}|${quotedId})`,
// INVOKE HIGHER-ORDER RELATION rel ON <id, ..., id>
String.raw`INVOKE\s+HIGHER-ORDER\s+RELATION\s[^\s]+\sON\s+<([0-9a-zA-Z:#_<>]+|\`[^\`\r\n]*\`)((?:,[0-9a-zA-Z:#_<>]+|,\`[^\`\r\n]*\`)*)>`,
String.raw`INVOKE\s+HIGHER-ORDER\s+RELATION\s[^\s]+\sON\s+<(${idWithAngleBrackets}|${quotedId})((?:,${idWithAngleBrackets}|,${quotedId})*)>`,
// SELECT id
String.raw`SELECT\s+([0-9a-zA-Z:#_]+|\`[^\`\r\n]*\`)`,
String.raw`SELECT\s+(${id}|${quotedId})`,
// REWRITE id WITH
String.raw`REWRITE\s+([0-9a-zA-Z:#_]+|\`[^\`\r\n]*\`)\s+WITH\s`,
String.raw`REWRITE\s+(${id}|${quotedId})\s+WITH\s`,
// id UNION id UNION ... UNION id
String.raw`(${id}|${quotedId})((?:\s+UNION\s+${id}|${quotedId})+)`,
];
return new RegExp(
`${String.raw`\{[0-9]+\}\s+[0-9a-zA-Z]+\s=\s(?:` + regexps.join("|")})`,
const r = new RegExp(
`${
String.raw`\{[0-9]+\}\s+(?:[0-9a-zA-Z]+\s=|\|)\s(?:` + regexps.join("|")
})`,
);
})();
function getDependentPredicates(operations: string[]): I.List<string> {
return I.List(operations).flatMap((operation) => {
const matches = DEPENDENT_PREDICATES_REGEXP.exec(operation.trim());
if (matches !== null) {
return I.List(matches)
.rest() // Skip the first group as it's just the entire string
.filter((x) => !!x && !x.match("r[0-9]+|PRIMITIVE")) // Only keep the references to predicates.
.flatMap((x) => x.split(",")) // Group 2 in the INVOKE HIGHER_ORDER RELATION case is a comma-separated list of identifiers.
.filter((x) => !!x) // Remove empty strings
.map((x) =>
x.startsWith("`") && x.endsWith("`")
? x.substring(1, x.length - 1)
: x,
); // Remove quotes from quoted identifiers
} else {
return I.List();
}
const matches = r.exec(operation.trim()) || [];
return I.List(matches)
.rest() // Skip the first group as it's just the entire string
.filter((x) => !!x)
.flatMap((x) => x.split(",")) // Group 2 in the INVOKE HIGHER_ORDER RELATION case is a comma-separated list of identifiers.
.flatMap((x) => x.split(" UNION ")) // Split n-ary unions into individual arguments.
.filter((x) => !x.match("r[0-9]+|PRIMITIVE")) // Only keep the references to predicates.
.filter((x) => !!x) // Remove empty strings
.map((x) =>
x.startsWith("`") && x.endsWith("`") ? x.substring(1, x.length - 1) : x,
); // Remove quotes from quoted identifiers
});
}

View File

@@ -85,9 +85,8 @@ export async function runAutoModelQueries({
// CodeQL needs to have access to the database to be able to retrieve the
// snippets from it. The source location prefix is used to determine the
// base path of the database.
const sourceLocationPrefix = await databaseItem.getSourceLocationPrefix(
cliServer,
);
const sourceLocationPrefix =
await databaseItem.getSourceLocationPrefix(cliServer);
const sourceArchiveUri = databaseItem.sourceArchive;
const sourceInfo =
sourceArchiveUri === undefined

View File

@@ -1,11 +1,13 @@
import { DecodedBqrsChunk } from "../common/bqrs-cli-types";
import { Call, CallClassification, Method } from "./method";
import { DecodedBqrsChunk, BqrsEntityValue } from "../common/bqrs-cli-types";
import { CallClassification, Method, Usage } from "./method";
import { ModeledMethodType } from "./modeled-method";
import { parseLibraryFilename } from "./library";
import { Mode } from "./shared/mode";
import { ApplicationModeTuple, FrameworkModeTuple } from "./queries/query";
import { QueryLanguage } from "../common/query-language";
import { getModelsAsDataLanguage } from "./languages";
import { mapUrlValue } from "../common/bqrs-raw-results-mapper";
import { isUrlValueResolvable } from "../common/raw-result-types";
export function decodeBqrsToMethods(
chunk: DecodedBqrsChunk,
@@ -17,7 +19,7 @@ export function decodeBqrsToMethods(
const definition = getModelsAsDataLanguage(language);
chunk?.tuples.forEach((tuple) => {
let usage: Call;
let usageEntityValue: BqrsEntityValue;
let packageName: string;
let typeName: string;
let methodName: string;
@@ -30,7 +32,7 @@ export function decodeBqrsToMethods(
if (mode === Mode.Application) {
[
usage,
usageEntityValue,
packageName,
typeName,
methodName,
@@ -43,7 +45,7 @@ export function decodeBqrsToMethods(
] = tuple as ApplicationModeTuple;
} else {
[
usage,
usageEntityValue,
packageName,
typeName,
methodName,
@@ -97,11 +99,25 @@ export function decodeBqrsToMethods(
});
}
if (usageEntityValue.url === undefined) {
return;
}
const usageUrl = mapUrlValue(usageEntityValue.url);
if (!usageUrl || !isUrlValueResolvable(usageUrl)) {
return;
}
if (!usageEntityValue.label) {
return;
}
const method = methodsByApiName.get(signature)!;
const usages = [
const usages: Usage[] = [
...method.usages,
{
...usage,
label: usageEntityValue.label,
url: usageUrl,
classification,
},
];

View File

@@ -29,6 +29,23 @@ function rubyMethodSignature(typeName: string, methodName: string) {
return `${typeName}#${methodName}`;
}
function rubyMethodPath(methodName: string) {
if (methodName === "") {
return "";
}
return `Method[${methodName}]`;
}
function rubyPath(methodName: string, path: string) {
const methodPath = rubyMethodPath(methodName);
if (methodPath === "") {
return path;
}
return `${methodPath}.${path}`;
}
export const ruby: ModelsAsDataLanguage = {
availableModes: [Mode.Framework],
createMethodSignature: ({ typeName, methodName }) =>
@@ -42,7 +59,7 @@ export const ruby: ModelsAsDataLanguage = {
// );
generateMethodDefinition: (method) => [
method.typeName,
`Method[${method.methodName}].${method.output}`,
rubyPath(method.methodName, method.output),
method.kind,
],
readModeledMethod: (row) => {
@@ -71,8 +88,11 @@ export const ruby: ModelsAsDataLanguage = {
// string type, string path, string kind
// );
generateMethodDefinition: (method) => {
const path = `Method[${method.methodName}].${method.input}`;
return [method.typeName, path, method.kind];
return [
method.typeName,
rubyPath(method.methodName, method.input),
method.kind,
];
},
readModeledMethod: (row) => {
const typeName = row[0] as string;
@@ -101,7 +121,7 @@ export const ruby: ModelsAsDataLanguage = {
// );
generateMethodDefinition: (method) => [
method.typeName,
`Method[${method.methodName}]`,
rubyMethodPath(method.methodName),
method.input,
method.output,
method.kind,
@@ -131,7 +151,7 @@ export const ruby: ModelsAsDataLanguage = {
// );
generateMethodDefinition: (method) => [
method.typeName,
`Method[${method.methodName}]`,
rubyMethodPath(method.methodName),
method.kind,
],
readModeledMethod: (row) => {
@@ -157,7 +177,7 @@ export const ruby: ModelsAsDataLanguage = {
generateMethodDefinition: (method) => [
method.relatedTypeName,
method.typeName,
`Method[${method.methodName}].${method.path}`,
rubyPath(method.methodName, method.path),
],
readModeledMethod: (row) => {
const typeName = row[1] as string;

View File

@@ -48,7 +48,6 @@ export class MethodModelingViewProvider extends AbstractWebviewViewProvider<
t: "setMethodModelingPanelViewState",
viewState: {
language: this.language,
showMultipleModels: this.modelConfig.showMultipleModels,
},
});
}

View File

@@ -1,9 +1,9 @@
import { ResolvableLocationValue } from "../common/bqrs-cli-types";
import { ModeledMethod, ModeledMethodType } from "./modeled-method";
import { UrlValueResolvable } from "../common/raw-result-types";
export type Call = {
type Call = {
readonly label: string;
readonly url: Readonly<ResolvableLocationValue>;
readonly url: Readonly<UrlValueResolvable>;
};
export enum CallClassification {

View File

@@ -19,6 +19,7 @@ import { assertNever } from "../../common/helpers-pure";
import { ModeledMethod } from "../modeled-method";
import { groupMethods, sortGroupNames, sortMethods } from "../shared/sorting";
import { INITIAL_MODE, Mode } from "../shared/mode";
import { UrlValueResolvable } from "../../common/raw-result-types";
export class MethodsUsageDataProvider
extends DisposableObject
@@ -99,11 +100,16 @@ export class MethodsUsageDataProvider
} else {
const { method, usage } = item;
const description =
usage.url.type === "wholeFileLocation"
? this.relativePathWithinDatabase(usage.url.uri)
: `${this.relativePathWithinDatabase(usage.url.uri)} [${
usage.url.startLine
}, ${usage.url.endLine}]`;
return {
label: usage.label,
description: `${this.relativePathWithinDatabase(usage.url.uri)} [${
usage.url.startLine
}, ${usage.url.endLine}]`,
description,
collapsibleState: TreeItemCollapsibleState.None,
command: {
title: "Show usage",
@@ -211,14 +217,35 @@ function usagesAreEqual(u1: Usage, u2: Usage): boolean {
return (
u1.label === u2.label &&
u1.classification === u2.classification &&
u1.url.uri === u2.url.uri &&
u1.url.startLine === u2.url.startLine &&
u1.url.startColumn === u2.url.startColumn &&
u1.url.endLine === u2.url.endLine &&
u1.url.endColumn === u2.url.endColumn
urlValueResolvablesAreEqual(u1.url, u2.url)
);
}
function urlValueResolvablesAreEqual(
u1: UrlValueResolvable,
u2: UrlValueResolvable,
): boolean {
if (u1.type !== u2.type) {
return false;
}
if (u1.type === "wholeFileLocation" && u2.type === "wholeFileLocation") {
return u1.uri === u2.uri;
}
if (u1.type === "lineColumnLocation" && u2.type === "lineColumnLocation") {
return (
u1.uri === u2.uri &&
u1.startLine === u2.startLine &&
u1.startColumn === u2.startColumn &&
u1.endLine === u2.endLine &&
u1.endColumn === u2.endColumn
);
}
return false;
}
function sortMethodsInGroups(methods: readonly Method[], mode: Mode): Method[] {
const grouped = groupMethods(methods, mode);

View File

@@ -385,7 +385,6 @@ export class ModelEditorView extends AbstractWebview<
language: this.language,
showGenerateButton,
showLlmButton,
showMultipleModels: this.modelConfig.showMultipleModels,
mode: this.modelingStore.getMode(this.databaseItem),
showModeSwitchButton,
sourceArchiveAvailable,
@@ -482,9 +481,8 @@ export class ModelEditorView extends AbstractWebview<
// 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 (mode === Mode.Application) {
addedDatabase = await this.promptChooseNewOrExistingDatabase(
progress,
);
addedDatabase =
await this.promptChooseNewOrExistingDatabase(progress);
if (!addedDatabase) {
return;
}
@@ -562,9 +560,8 @@ export class ModelEditorView extends AbstractWebview<
private async modelDependency(): Promise<void> {
return withProgress(async (progress, token) => {
const addedDatabase = await this.promptChooseNewOrExistingDatabase(
progress,
);
const addedDatabase =
await this.promptChooseNewOrExistingDatabase(progress);
if (!addedDatabase || token.isCancellationRequested) {
return;
}

View File

@@ -1,5 +1,6 @@
import { Call, CallClassification } from "../method";
import { CallClassification } from "../method";
import { ModeledMethodType } from "../modeled-method";
import { BqrsEntityValue } from "../../common/bqrs-cli-types";
export type Query = {
/**
@@ -39,7 +40,7 @@ export type Query = {
};
export type ApplicationModeTuple = [
Call,
BqrsEntityValue,
string,
string,
string,
@@ -52,7 +53,7 @@ export type ApplicationModeTuple = [
];
export type FrameworkModeTuple = [
Call,
BqrsEntityValue,
string,
string,
string,

View File

@@ -9,9 +9,15 @@ export const fetchExternalApisQuery: Query = {
* @tags modeleditor endpoints application-mode
*/
import ruby
import codeql.ruby.AST
select "todo", "todo", "todo", "todo", "todo", false, "todo", "todo", "todo", "todo"
// This query is empty as Application Mode is not yet supported for Ruby.
from
Call usage, string package, string type, string name, string parameters, boolean supported,
string namespace, string version, string supportedType, string classification
where none()
select usage, package, namespace, type, name, parameters, supported, namespace, version,
supportedType, classification
`,
frameworkModeQuery: `/**
* @name Fetch endpoints for use in the model editor (framework mode)
@@ -22,34 +28,14 @@ select "todo", "todo", "todo", "todo", "todo", false, "todo", "todo", "todo", "t
*/
import ruby
import FrameworkModeEndpointsQuery
import ModelEditor
from PublicEndpointFromSource endpoint, boolean supported, string type
where
supported = isSupported(endpoint) and
type = supportedType(endpoint)
from PublicEndpointFromSource endpoint
select endpoint, endpoint.getNamespace(), endpoint.getTypeName(), endpoint.getName(),
endpoint.getParameterTypes(), supported, endpoint.getFile().getBaseName(), type
endpoint.getParameterTypes(), endpoint.getSupportedStatus(), endpoint.getFile().getBaseName(),
endpoint.getSupportedType()
`,
dependencies: {
"FrameworkModeEndpointsQuery.qll": `private import ruby
private import ModelEditor
private import modeling.internal.Util as Util
/**
* A class of effectively public callables from source code.
*/
class PublicEndpointFromSource extends Endpoint {
PublicEndpointFromSource() {
this.getFile() instanceof Util::RelevantFile
}
override predicate isSource() { this instanceof SourceCallable }
override predicate isSink() { this instanceof SinkCallable }
}
`,
"ModelEditor.qll": `/** Provides classes and predicates related to handling APIs for the VS Code extension. */
private import ruby
@@ -57,22 +43,31 @@ private import codeql.ruby.dataflow.FlowSummary
private import codeql.ruby.dataflow.internal.DataFlowPrivate
private import codeql.ruby.dataflow.internal.FlowSummaryImpl as FlowSummaryImpl
private import codeql.ruby.dataflow.internal.FlowSummaryImplSpecific
private import modeling.internal.Util as Util
private import modeling.internal.Types
private import codeql.ruby.frameworks.core.Gem
private import codeql.ruby.frameworks.data.ModelsAsData
private import codeql.ruby.frameworks.data.internal.ApiGraphModelsExtensions
private import queries.modeling.internal.Util as Util
/** Holds if the given callable is not worth supporting. */
private predicate isUninteresting(DataFlow::MethodNode c) {
c.getLocation().getFile().getRelativePath().regexpMatch(".*(test|spec).*")
c.getLocation().getFile() instanceof TestFile
}
private predicate gemFileStep(Gem::GemSpec gem, Folder folder, int n) {
n = 0 and folder.getAFile() = gem.(File)
or
exists(Folder parent, int m |
gemFileStep(gem, parent, m) and
parent.getAFolder() = folder and
n = m + 1
)
}
/**
* A callable method or accessor from either the Ruby Standard Library, a 3rd party library, or from the source.
*/
class Endpoint extends DataFlow::MethodNode {
Endpoint() {
this.isPublic() and not isUninteresting(this)
}
Endpoint() { this.isPublic() and not isUninteresting(this) }
File getFile() { result = this.getLocation().getFile() }
@@ -83,9 +78,13 @@ class Endpoint extends DataFlow::MethodNode {
*/
bindingset[this]
string getNamespace() {
// Return the name of any gemspec file in the database.
// TODO: make this work for projects with multiple gems (and hence multiple gemspec files)
result = any(Gem::GemSpec g).getName()
exists(Folder folder | folder = this.getFile().getParentContainer() |
// The nearest gemspec to this endpoint, if one exists
result = min(Gem::GemSpec g, int n | gemFileStep(g, folder, n) | g order by n).getName()
or
not gemFileStep(_, folder, _) and
result = ""
)
}
/**
@@ -93,9 +92,12 @@ class Endpoint extends DataFlow::MethodNode {
*/
bindingset[this]
string getTypeName() {
// result = nestedName(this.getDeclaringType().getUnboundDeclaration())
// result = any(DataFlow::ClassNode c | Types::methodReturnsType(this, c) | c).getQualifiedName()
result = Util::getAnAccessPathPrefixWithoutSuffix(this)
result =
any(DataFlow::ModuleNode m | m.getOwnInstanceMethod(this.getMethodName()) = this)
.getQualifiedName() or
result =
any(DataFlow::ModuleNode m | m.getOwnSingletonMethod(this.getMethodName()) = this)
.getQualifiedName() + "!"
}
/**
@@ -103,24 +105,26 @@ class Endpoint extends DataFlow::MethodNode {
*/
bindingset[this]
string getParameterTypes() {
// For now, return the names of postional parameters. We don't always have type information, so we can't return type names.
// We don't yet handle keyword params, splat params or block params.
// result = "(" + parameterQualifiedTypeNamesToString(this) + ")"
// For now, return the names of postional and keyword parameters. We don't always have type information, so we can't return type names.
// We don't yet handle splat params or block params.
result =
"(" +
concat(DataFlow::ParameterNode p, int i |
p = this.asCallable().getParameter(i)
concat(string key, string value |
value = any(int i | i.toString() = key | this.asCallable().getParameter(i)).getName()
or
exists(DataFlow::ParameterNode param |
param = this.asCallable().getKeywordParameter(key)
|
value = key + ":"
)
|
p.getName(), "," order by i
value, "," order by key
) + ")"
}
/** Holds if this API has a supported summary. */
pragma[nomagic]
predicate hasSummary() {
// this instanceof SummarizedCallable
none()
}
predicate hasSummary() { none() }
/** Holds if this API is a known source. */
pragma[nomagic]
@@ -132,10 +136,7 @@ class Endpoint extends DataFlow::MethodNode {
/** Holds if this API is a known neutral. */
pragma[nomagic]
predicate isNeutral() {
// this instanceof FlowSummaryImpl::Public::NeutralCallable
none()
}
predicate isNeutral() { none() }
/**
* Holds if this API is supported by existing CodeQL libraries, that is, it is either a
@@ -144,261 +145,207 @@ class Endpoint extends DataFlow::MethodNode {
predicate isSupported() {
this.hasSummary() or this.isSource() or this.isSink() or this.isNeutral()
}
}
boolean isSupported(Endpoint endpoint) {
if endpoint.isSupported() then result = true else result = false
}
boolean getSupportedStatus() { if this.isSupported() then result = true else result = false }
string supportedType(Endpoint endpoint) {
endpoint.isSink() and result = "sink"
or
endpoint.isSource() and result = "source"
or
endpoint.hasSummary() and result = "summary"
or
endpoint.isNeutral() and result = "neutral"
or
not endpoint.isSupported() and result = ""
string getSupportedType() {
this.isSink() and result = "sink"
or
this.isSource() and result = "source"
or
this.hasSummary() and result = "summary"
or
this.isNeutral() and result = "neutral"
or
not this.isSupported() and result = ""
}
}
string methodClassification(Call method) {
method.getFile() instanceof TestFile and result = "test"
or
not method.getFile() instanceof TestFile and
result = "source"
}
class TestFile extends File {
TestFile() {
this.getRelativePath().regexpMatch(".*(test|spec).+") and
not this.getAbsolutePath().matches("%/ql/test/%") // allows our test cases to work
}
}
/**
* A callable where there exists a MaD sink model that applies to it.
*/
class SinkCallable extends DataFlow::CallableNode {
SinkCallable() { sinkElement(this.asExpr().getExpr(), _, _, _) }
class SinkCallable extends DataFlow::MethodNode {
SinkCallable() {
exists(string type, string path, string method |
method = path.regexpCapture("(Method\\\\[[^\\\\]]+\\\\]).*", 1) and
Util::pathToMethod(this, type, method) and
sinkModel(type, path, _)
)
}
}
/**
* A callable where there exists a MaD source model that applies to it.
*/
class SourceCallable extends DataFlow::CallableNode {
SourceCallable() { sourceElement(this.asExpr().getExpr(), _, _, _) }
}`,
"modeling/internal/Util.qll": `private import ruby
// \`SomeClass#initialize\` methods are usually called indirectly via
// \`SomeClass.new\`, so we need to account for this when generating access paths
private string getNormalizedMethodName(DataFlow::MethodNode methodNode) {
exists(string actualMethodName | actualMethodName = methodNode.getMethodName() |
if actualMethodName = "initialize" then result = "new" else result = actualMethodName
)
SourceCallable() {
exists(string type, string path, string method |
method = path.regexpCapture("(Method\\\\[[^\\\\]]+\\\\]).*", 1) and
Util::pathToMethod(this, type, method) and
sourceModel(type, path, _)
)
}
}
private string getAccessPathSuffix(Ast::MethodBase method) {
if method instanceof Ast::SingletonMethod or method.getName() = "initialize"
then result = "!"
else result = ""
}
/**
* A class of effectively public callables from source code.
*/
class PublicEndpointFromSource extends Endpoint {
override predicate isSource() { this instanceof SourceCallable }
string getAnAccessPathPrefix(DataFlow::MethodNode methodNode) {
result =
getAnAccessPathPrefixWithoutSuffix(methodNode) +
getAccessPathSuffix(methodNode.asExpr().getExpr())
override predicate isSink() { this instanceof SinkCallable }
}
`,
"queries/modeling/internal/Util.qll": `/**
* Contains utility methods and classes to assist with generating data extensions models.
*/
string getAnAccessPathPrefixWithoutSuffix(DataFlow::MethodNode methodNode) {
result =
methodNode
.asExpr()
.getExpr()
.getEnclosingModule()
.(Ast::ConstantWriteAccess)
.getAQualifiedName()
}
private import ruby
private import codeql.ruby.ApiGraphs
/**
* A file that is relevant in the context of library modeling.
*
* In practice, this means a file that is not part of test code.
*/
class RelevantFile extends File {
RelevantFile() { not this.getRelativePath().regexpMatch(".*/?test(case)?s?/.*") }
}
string getMethodPath(DataFlow::MethodNode methodNode) {
result = "Method[" + getNormalizedMethodName(methodNode) + "]"
}
private string getParameterPath(DataFlow::ParameterNode paramNode) {
exists(Ast::Parameter param, string paramSpec |
param = paramNode.asParameter() and
(
paramSpec = param.getPosition().toString()
or
paramSpec = param.(Ast::KeywordParameter).getName() + ":"
or
param instanceof Ast::BlockParameter and
paramSpec = "block"
/**
* Gets an access path of an argument corresponding to the given \`paramNode\`.
*/
string getArgumentPath(DataFlow::ParameterNode paramNode) {
paramNode.getLocation().getFile() instanceof RelevantFile and
exists(string paramSpecifier |
exists(Ast::Parameter param |
param = paramNode.asParameter() and
(
paramSpecifier = param.getPosition().toString()
or
paramSpecifier = param.(Ast::KeywordParameter).getName() + ":"
or
param instanceof Ast::BlockParameter and
paramSpecifier = "block"
)
)
or
paramNode instanceof DataFlow::SelfParameterNode and paramSpecifier = "self"
|
result = "Parameter[" + paramSpec + "]"
result = "Argument[" + paramSpecifier + "]"
)
}
string getMethodParameterPath(DataFlow::MethodNode methodNode, DataFlow::ParameterNode paramNode) {
result = getMethodPath(methodNode) + "." + getParameterPath(paramNode)
/**
* Holds if \`(type,path)\` evaluates to the given method, when evalauted from a client of the current library.
*/
predicate pathToMethod(DataFlow::MethodNode method, string type, string path) {
method.getLocation().getFile() instanceof RelevantFile and
exists(DataFlow::ModuleNode mod, string methodName |
method = mod.getOwnInstanceMethod(methodName) and
if methodName = "initialize"
then (
type = mod.getQualifiedName() + "!" and
path = "Method[new]"
) else (
type = mod.getQualifiedName() and
path = "Method[" + methodName + "]"
)
or
method = mod.getOwnSingletonMethod(methodName) and
type = mod.getQualifiedName() + "!" and
path = "Method[" + methodName + "]"
)
}
`,
"modeling/internal/Types.qll": `private import ruby
private import codeql.ruby.ApiGraphs
private import Util as Util
module Types {
private module Config implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) {
// TODO: construction of type values not using a "new" call
source.(DataFlow::CallNode).getMethodName() = "new"
}
predicate isSink(DataFlow::Node sink) { sink = any(DataFlow::MethodNode m).getAReturnNode() }
}
private import DataFlow::Global<Config>
predicate methodReturnsType(DataFlow::MethodNode methodNode, DataFlow::ClassNode classNode) {
// ignore cases of initializing instance of self
not methodNode.getMethodName() = "initialize" and
exists(DataFlow::CallNode initCall |
flow(initCall, methodNode.getAReturnNode()) and
classNode.getAnImmediateReference().getAMethodCall() = initCall and
// constructed object does not have a type declared in test code
/*
* TODO: this may be too restrictive, e.g.
* - if a type is declared in both production and test code
* - if a built-in type is extended in test code
*/
forall(Ast::ModuleBase classDecl | classDecl = classNode.getADeclaration() |
classDecl.getLocation().getFile() instanceof Util::RelevantFile
)
)
}
// \`exprNode\` is an instance of \`classNode\`
private predicate exprHasType(DataFlow::ExprNode exprNode, DataFlow::ClassNode classNode) {
exists(DataFlow::MethodNode methodNode, DataFlow::CallNode callNode |
methodReturnsType(methodNode, classNode) and
callNode.getATarget() = methodNode
|
exprNode.getALocalSource() = callNode
)
or
exists(DataFlow::MethodNode containingMethod |
classNode.getInstanceMethod(containingMethod.getMethodName()) = containingMethod
|
exprNode.getALocalSource() = containingMethod.getSelfParameter()
)
}
// extensible predicate typeModel(string type1, string type2, string path);
// the method node in type2 constructs an instance of classNode
private predicate typeModelReturns(string type1, string type2, string path) {
exists(DataFlow::MethodNode methodNode, DataFlow::ClassNode classNode |
methodNode.getLocation().getFile() instanceof Util::RelevantFile and
methodReturnsType(methodNode, classNode)
|
type1 = classNode.getQualifiedName() and
type2 = Util::getAnAccessPathPrefix(methodNode) and
path = Util::getMethodPath(methodNode) + ".ReturnValue"
)
}
predicate methodTakesParameterOfType(
DataFlow::MethodNode methodNode, DataFlow::ClassNode classNode,
DataFlow::ParameterNode parameterNode
) {
exists(DataFlow::CallNode callToMethodNode, DataFlow::LocalSourceNode argumentNode |
callToMethodNode.getATarget() = methodNode and
// positional parameter
exists(int paramIndex |
argumentNode.flowsTo(callToMethodNode.getArgument(paramIndex)) and
parameterNode = methodNode.getParameter(paramIndex)
)
or
// keyword parameter
exists(string kwName |
argumentNode.flowsTo(callToMethodNode.getKeywordArgument(kwName)) and
parameterNode = methodNode.getKeywordParameter(kwName)
)
or
// block parameter
argumentNode.flowsTo(callToMethodNode.getBlock()) and
parameterNode = methodNode.getBlockParameter()
|
// parameter directly from new call
argumentNode.(DataFlow::CallNode).getMethodName() = "new" and
classNode.getAnImmediateReference().getAMethodCall() = argumentNode
or
// parameter from indirect new call
exists(DataFlow::ExprNode argExpr |
exprHasType(argExpr, classNode) and
argumentNode.(DataFlow::CallNode).getATarget() = argExpr
)
)
}
private predicate typeModelParameters(string type1, string type2, string path) {
exists(
DataFlow::MethodNode methodNode, DataFlow::ClassNode classNode,
DataFlow::ParameterNode parameterNode
|
methodNode.getLocation().getFile() instanceof Util::RelevantFile and
methodTakesParameterOfType(methodNode, classNode, parameterNode)
|
type1 = classNode.getQualifiedName() and
type2 = Util::getAnAccessPathPrefix(methodNode) and
path = Util::getMethodParameterPath(methodNode, parameterNode)
)
}
// TODO: non-positional params for block arg parameters
private predicate methodYieldsType(
DataFlow::CallableNode callableNode, int argIdx, DataFlow::ClassNode classNode
) {
exprHasType(callableNode.getABlockCall().getArgument(argIdx), classNode)
}
/*
* e.g. for
* \`\`\`rb
* class Foo
* def initialize
* // do some stuff...
* if block_given?
* yield self
* end
* end
*
* def do_something
* // do something else
* end
* end
*
* Foo.new do |foo| foo.do_something end
* \`\`\`
*
* the parameter foo to the block is an instance of Foo.
*/
private predicate typeModelBlockArgumentParameters(string type1, string type2, string path) {
exists(DataFlow::MethodNode methodNode, DataFlow::ClassNode classNode, int argIdx |
methodNode.getLocation().getFile() instanceof Util::RelevantFile and
methodYieldsType(methodNode, argIdx, classNode)
|
type1 = classNode.getQualifiedName() and
type2 = Util::getAnAccessPathPrefix(methodNode) and
path = Util::getMethodPath(methodNode) + ".Argument[block].Parameter[" + argIdx + "]"
)
}
predicate typeModel(string type1, string type2, string path) {
typeModelReturns(type1, type2, path)
or
typeModelParameters(type1, type2, path)
or
typeModelBlockArgumentParameters(type1, type2, path)
}
/**
* Gets any parameter to \`methodNode\`. This may be a positional, keyword,
* block, or self parameter.
*/
DataFlow::ParameterNode getAnyParameter(DataFlow::MethodNode methodNode) {
result =
[
methodNode.getParameter(_), methodNode.getKeywordParameter(_), methodNode.getBlockParameter(),
methodNode.getSelfParameter()
]
}
`,
private predicate pathToNodeBase(API::Node node, string type, string path, boolean isOutput) {
exists(DataFlow::MethodNode method, string prevPath | pathToMethod(method, type, prevPath) |
isOutput = true and
node = method.getAReturnNode().backtrack() and
path = prevPath + ".ReturnValue" and
not method.getMethodName() = "initialize" // ignore return value of initialize method
or
isOutput = false and
exists(DataFlow::ParameterNode paramNode |
paramNode = getAnyParameter(method) and
node = paramNode.track()
|
path = prevPath + "." + getArgumentPath(paramNode)
)
)
}
private predicate pathToNodeRec(
API::Node node, string type, string path, boolean isOutput, int pathLength
) {
pathLength < 8 and
(
pathToNodeBase(node, type, path, isOutput) and
pathLength = 1
or
exists(API::Node prevNode, string prevPath, boolean prevIsOutput, int prevPathLength |
pathToNodeRec(prevNode, type, prevPath, prevIsOutput, prevPathLength) and
pathLength = prevPathLength + 1
|
node = prevNode.getAnElement() and
path = prevPath + ".Element" and
isOutput = prevIsOutput
or
node = prevNode.getReturn() and
path = prevPath + ".ReturnValue" and
isOutput = prevIsOutput
or
prevIsOutput = false and
isOutput = true and
(
exists(int n |
node = prevNode.getParameter(n) and
path = prevPath + ".Parameter[" + n + "]"
)
or
exists(string name |
node = prevNode.getKeywordParameter(name) and
path = prevPath + ".Parameter[" + name + ":]"
)
or
node = prevNode.getBlock() and
path = prevPath + ".Parameter[block]"
)
)
)
}
/**
* Holds if \`(type,path)\` evaluates to a value corresponding to \`node\`, when evaluated from a client of the current library.
*/
predicate pathToNode(API::Node node, string type, string path, boolean isOutput) {
pathToNodeRec(node, type, path, isOutput, _)
}`,
},
};

View File

@@ -1,16 +0,0 @@
import { ModeledMethod } from "../modeled-method";
/**
* Converts a ModeledMethod[] to a single ModeledMethod for legacy usage. This function should always be used instead
* of the trivial conversion to track usages of this conversion.
*
* This method should only be called inside a `postMessage` call. If it's used anywhere else, consider whether the
* boundary is correct: the boundary should as close as possible to the extension host -> webview boundary.
*
* @param modeledMethods The ModeledMethod[]
*/
export function convertToLegacyModeledMethod(
modeledMethods: ModeledMethod[],
): ModeledMethod | undefined {
return modeledMethods[0];
}

View File

@@ -7,7 +7,6 @@ export interface ModelEditorViewState {
language: QueryLanguage;
showGenerateButton: boolean;
showLlmButton: boolean;
showMultipleModels: boolean;
mode: Mode;
showModeSwitchButton: boolean;
sourceArchiveAvailable: boolean;
@@ -15,5 +14,4 @@ export interface ModelEditorViewState {
export interface MethodModelingPanelViewState {
language: QueryLanguage | undefined;
showMultipleModels: boolean;
}

View File

@@ -239,8 +239,8 @@ export class QLTestAdapter extends DisposableObject implements TestAdapter {
const state = event.pass
? "passed"
: event.messages?.length
? "errored"
: "failed";
? "errored"
: "failed";
let message: string | undefined;
if (event.failureDescription || event.diff?.length) {
message =

View File

@@ -19,7 +19,7 @@ import { nanoid } from "nanoid";
import { CodeQLCliServer } from "./codeql-cli/cli";
import { SELECT_QUERY_NAME } from "./language-support";
import { DatabaseManager } from "./databases/local-databases";
import { DecodedBqrsChunk, EntityValue } from "./common/bqrs-cli-types";
import { DecodedBqrsChunk, BqrsEntityValue } from "./common/bqrs-cli-types";
import { BaseLogger, showAndLogWarningMessage } from "./common/logging";
import { extLogger } from "./common/logging/vscode";
import { generateSummarySymbolsFile } from "./log-insights/summary-parser";
@@ -287,7 +287,7 @@ export class QueryEvaluationInfo extends QueryOutputDir {
typeof v === "string" ? v.replaceAll('"', '""') : v
}"`;
} else if (chunk.columns[i].kind === "Entity") {
return (v as EntityValue).label;
return (v as BqrsEntityValue).label;
} else {
return v;
}

View File

@@ -5,6 +5,7 @@ import { Meta, StoryFn } from "@storybook/react";
import CompareTableComponent from "../../view/compare/CompareTable";
import "../../view/results/resultsView.css";
import { ColumnKind } from "../../common/raw-result-types";
export default {
title: "Compare/Compare Table",
@@ -40,30 +41,38 @@ CompareTable.args = {
result: {
kind: "raw",
columns: [
{ name: "a", kind: "Entity" },
{ name: "b", kind: "Entity" },
{ name: "a", kind: ColumnKind.Entity },
{ name: "b", kind: ColumnKind.Entity },
],
from: [],
to: [
[
{
label: "url : String",
url: {
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
startLine: 22,
startColumn: 27,
endLine: 22,
endColumn: 57,
type: "entity",
value: {
label: "url : String",
url: {
type: "lineColumnLocation",
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
startLine: 22,
startColumn: 27,
endLine: 22,
endColumn: 57,
},
},
},
{
label: "url",
url: {
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
startLine: 23,
startColumn: 33,
endLine: 23,
endColumn: 35,
type: "entity",
value: {
label: "url",
url: {
type: "lineColumnLocation",
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
startLine: 23,
startColumn: 33,
endLine: 23,
endColumn: 35,
},
},
},
],

View File

@@ -1,24 +1,20 @@
{
"schema": {
"resultSet": {
"name": "#select",
"rows": 1,
"totalRowCount": 1,
"columns": [
{
"kind": "i"
"kind": "integer"
}
]
},
"resultSet": {
"schema": {
"name": "#select",
"rows": 1,
"columns": [
],
"rows": [
[
{
"kind": "i"
"type": "number",
"value": 60688
}
]
},
"rows": [[60688]]
]
},
"fileLinkPrefix": "https://github.com/facebook/create-react-app/blob/d960b9e38c062584ff6cfb1a70e1512509a966e7",
"sourceLocationPrefix": "/home/runner/work/bulk-builder/bulk-builder",

View File

@@ -47,26 +47,16 @@ MethodSaved.args = {
modelingStatus: "saved",
};
export const MultipleModelingsUnmodeled = Template.bind({});
MultipleModelingsUnmodeled.args = {
language,
method,
modeledMethods: [],
modelingStatus: "saved",
showMultipleModels: true,
};
export const MultipleModelingsModeledSingle = Template.bind({});
MultipleModelingsModeledSingle.args = {
export const ModeledSingle = Template.bind({});
ModeledSingle.args = {
language,
method,
modeledMethods: [createSinkModeledMethod(method)],
modelingStatus: "saved",
showMultipleModels: true,
};
export const MultipleModelingsModeledMultiple = Template.bind({});
MultipleModelingsModeledMultiple.args = {
export const ModeledMultiple = Template.bind({});
ModeledMultiple.args = {
language,
method,
modeledMethods: [
@@ -79,11 +69,10 @@ MultipleModelingsModeledMultiple.args = {
}),
],
modelingStatus: "saved",
showMultipleModels: true,
};
export const MultipleModelingsValidationFailedNeutral = Template.bind({});
MultipleModelingsValidationFailedNeutral.args = {
export const ValidationFailedNeutral = Template.bind({});
ValidationFailedNeutral.args = {
language,
method,
modeledMethods: [
@@ -91,11 +80,10 @@ MultipleModelingsValidationFailedNeutral.args = {
createNeutralModeledMethod(method),
],
modelingStatus: "unsaved",
showMultipleModels: true,
};
export const MultipleModelingsValidationFailedDuplicate = Template.bind({});
MultipleModelingsValidationFailedDuplicate.args = {
export const ValidationFailedDuplicate = Template.bind({});
ValidationFailedDuplicate.args = {
language,
method,
modeledMethods: [
@@ -108,5 +96,4 @@ MultipleModelingsValidationFailedDuplicate.args = {
createSinkModeledMethod(method),
],
modelingStatus: "unsaved",
showMultipleModels: true,
};

View File

@@ -216,7 +216,6 @@ LibraryRow.args = {
viewState: createMockModelEditorViewState({
showGenerateButton: true,
showLlmButton: true,
showMultipleModels: true,
}),
hideModeledMethods: false,
};

View File

@@ -6,10 +6,7 @@ import { Meta, StoryFn } from "@storybook/react";
import { MethodRow as MethodRowComponent } from "../../view/model-editor/MethodRow";
import { CallClassification, Method } from "../../model-editor/method";
import { ModeledMethod } from "../../model-editor/modeled-method";
import {
MULTIPLE_MODELS_GRID_TEMPLATE_COLUMNS,
SINGLE_MODEL_GRID_TEMPLATE_COLUMNS,
} from "../../view/model-editor/ModeledMethodDataGrid";
import { MULTIPLE_MODELS_GRID_TEMPLATE_COLUMNS } from "../../view/model-editor/ModeledMethodDataGrid";
import { DataGrid } from "../../view/common/DataGrid";
import { createMockModelEditorViewState } from "../../../test/factories/model-editor/view-state";
@@ -35,12 +32,8 @@ const Template: StoryFn<typeof MethodRowComponent> = (args) => {
[args],
);
const gridTemplateColumns = args.viewState?.showMultipleModels
? MULTIPLE_MODELS_GRID_TEMPLATE_COLUMNS
: SINGLE_MODEL_GRID_TEMPLATE_COLUMNS;
return (
<DataGrid gridTemplateColumns={gridTemplateColumns}>
<DataGrid gridTemplateColumns={MULTIPLE_MODELS_GRID_TEMPLATE_COLUMNS}>
<MethodRowComponent
{...args}
modeledMethods={modeledMethods}
@@ -63,6 +56,7 @@ const method: Method = {
{
label: "open(...)",
url: {
type: "lineColumnLocation",
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
startLine: 14,
startColumn: 24,
@@ -74,6 +68,7 @@ const method: Method = {
{
label: "open(...)",
url: {
type: "lineColumnLocation",
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
startLine: 25,
startColumn: 24,
@@ -100,7 +95,6 @@ const modeledMethod: ModeledMethod = {
const viewState = createMockModelEditorViewState({
showGenerateButton: true,
showLlmButton: true,
showMultipleModels: true,
});
export const Unmodeled = Template.bind({});

View File

@@ -30,7 +30,6 @@ ModelEditor.args = {
},
showGenerateButton: true,
showLlmButton: true,
showMultipleModels: true,
}),
initialMethods: [
{
@@ -112,6 +111,7 @@ ModelEditor.args = {
{
label: "println(...)",
url: {
type: "lineColumnLocation",
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
startLine: 29,
startColumn: 9,
@@ -123,6 +123,7 @@ ModelEditor.args = {
{
label: "println(...)",
url: {
type: "lineColumnLocation",
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/test/java/org/example/HelloControllerTest.java",
startLine: 29,
startColumn: 9,

View File

@@ -28,16 +28,6 @@ ResultTablesHeader.args = {
resultSetNames: ["#select", "alerts"],
resultSet: {
t: "InterpretedResultSet",
schema: {
name: "#select",
rows: 15,
columns: [
{
name: "x",
kind: "s",
},
],
},
name: "#select",
interpretation: {
sourceLocationPrefix: "/home/bulk-builder/bulk-builder",

View File

@@ -18,6 +18,7 @@ const Template: StoryFn<typeof ClickableLocationComponent> = (args) => (
export const ClickableLocation = Template.bind({});
ClickableLocation.args = {
loc: {
type: "lineColumnLocation",
uri: "file:/home/runner/work/sql2o-example/sql2o-example/src/main/java/org/example/HelloController.java",
startLine: 22,
startColumn: 27,

View File

@@ -14,6 +14,7 @@ import {
} from "../../variant-analysis/shared/variant-analysis";
import { createMockVariantAnalysis } from "../../../test/factories/variant-analysis/shared/variant-analysis";
import { createMockRepositoryWithMetadata } from "../../../test/factories/variant-analysis/shared/repository";
import { ColumnKind } from "../../common/raw-result-types";
export default {
title: "Variant Analysis/Variant Analysis",
@@ -207,26 +208,22 @@ const repoResults: VariantAnalysisScannedRepositoryResult[] = [
variantAnalysisId: 1,
repositoryId: 1,
rawResults: {
schema: {
resultSet: {
name: "#select",
rows: 1,
totalRowCount: 1,
columns: [
{
kind: "i",
kind: ColumnKind.Integer,
},
],
},
resultSet: {
schema: {
name: "#select",
rows: 1,
columns: [
rows: [
[
{
kind: "i",
type: "number",
value: 60688,
},
],
},
rows: [[60688]],
],
},
fileLinkPrefix:
"https://github.com/octodemo/hello-world-1/blob/59a2a6c7d9dde7a6ecb77c2f7e8197d6925c143b",

View File

@@ -1,9 +1,9 @@
import { CodeQLCliServer } from "../codeql-cli/cli";
import { Logger } from "../common/logging";
import { transformBqrsResultSet } from "../common/bqrs-cli-types";
import { AnalysisRawResults } from "./shared/analysis-result";
import { MAX_RAW_RESULTS } from "./shared/result-limits";
import { SELECT_TABLE_NAME } from "../common/interface-types";
import { bqrsToResultSet } from "../common/bqrs-raw-results-mapper";
export async function extractRawResults(
cliServer: CodeQLCliServer,
@@ -34,9 +34,9 @@ export async function extractRawResults(
pageSize: MAX_RAW_RESULTS,
});
const resultSet = transformBqrsResultSet(schema, chunk);
const resultSet = bqrsToResultSet(schema, chunk);
const capped = !!chunk.next;
return { schema, resultSet, fileLinkPrefix, sourceLocationPrefix, capped };
return { resultSet, fileLinkPrefix, sourceLocationPrefix, capped };
}

View File

@@ -45,9 +45,8 @@ export async function exportVariantAnalysisResults(
): Promise<void> {
await withProgress(
async (progress: ProgressCallback, token: CancellationToken) => {
const variantAnalysis = await variantAnalysisManager.getVariantAnalysis(
variantAnalysisId,
);
const variantAnalysis =
await variantAnalysisManager.getVariantAnalysis(variantAnalysisId);
if (!variantAnalysis) {
void extLogger.log(
`Could not find variant analysis with id ${variantAnalysisId}`,
@@ -61,9 +60,8 @@ export async function exportVariantAnalysisResults(
throw new UserCancellationException("Cancelled");
}
const repoStates = await variantAnalysisManager.getRepoStates(
variantAnalysisId,
);
const repoStates =
await variantAnalysisManager.getRepoStates(variantAnalysisId);
void extLogger.log(
`Exporting variant analysis results for variant analysis with id ${variantAnalysis.id}`,

View File

@@ -1,4 +1,3 @@
import { CellValue } from "../common/bqrs-cli-types";
import { tryGetRemoteLocation } from "../common/bqrs-utils";
import { createRemoteFileRef } from "../common/location-link-utils";
import {
@@ -19,6 +18,7 @@ import type {
VariantAnalysisScannedRepositoryResult,
} from "./shared/variant-analysis";
import type { RepositoryWithMetadata } from "./shared/repository";
import { CellValue } from "../common/raw-result-types";
type MarkdownLinkType = "local" | "gist";
@@ -298,9 +298,9 @@ function generateMarkdownForRawResults(
analysisRawResults: AnalysisRawResults,
): string[] {
const tableRows: string[] = [];
const columnCount = analysisRawResults.schema.columns.length;
const columnCount = analysisRawResults.resultSet.columns.length;
// Table headers are the column names if they exist, and empty otherwise
const headers = analysisRawResults.schema.columns.map(
const headers = analysisRawResults.resultSet.columns.map(
(column) => column.name || "",
);
const tableHeader = `| ${headers.join(" | ")} |`;
@@ -327,23 +327,25 @@ function generateMarkdownForRawTableCell(
sourceLocationPrefix: string,
) {
let cellValue: string;
switch (typeof value) {
switch (value.type) {
case "string":
case "number":
case "boolean":
cellValue = `\`${convertNonPrintableChars(value.toString())}\``;
cellValue = `\`${convertNonPrintableChars(value.value.toString())}\``;
break;
case "object":
case "entity":
{
const url = tryGetRemoteLocation(
value.url,
value.value.url,
fileLinkPrefix,
sourceLocationPrefix,
);
if (url) {
cellValue = `[\`${convertNonPrintableChars(value.label)}\`](${url})`;
cellValue = `[\`${convertNonPrintableChars(
value.value.label,
)}\`](${url})`;
} else {
cellValue = `\`${convertNonPrintableChars(value.label)}\``;
cellValue = `\`${convertNonPrintableChars(value.value.label)}\``;
}
}
break;

View File

@@ -18,10 +18,6 @@ export async function getRepositorySelection(
const selectedDbItem = dbManager.getSelectedDbItem();
if (selectedDbItem) {
switch (selectedDbItem.kind) {
case DbItemKind.LocalDatabase || DbItemKind.LocalList:
throw new UserCancellationException(
"Local databases and lists are not supported yet.",
);
case DbItemKind.RemoteSystemDefinedList:
return { repositoryLists: [selectedDbItem.listName] };
case DbItemKind.RemoteUserDefinedList:

View File

@@ -183,9 +183,8 @@ async function copyExistingQueryPack(
if (
await cliServer.cliConstraints.supportsGenerateExtensiblePredicateMetadata()
) {
const metadata = await cliServer.generateExtensiblePredicateMetadata(
originalPackRoot,
);
const metadata =
await cliServer.generateExtensiblePredicateMetadata(originalPackRoot);
metadata.extensible_predicates.forEach((predicate) => {
if (predicate.path.endsWith(".ql")) {
toCopy.push(join(originalPackRoot, predicate.path));

View File

@@ -1,7 +1,6 @@
import { RawResultSet, ResultSetSchema } from "../../common/bqrs-cli-types";
import { RawResultSet } from "../../common/raw-result-types";
export interface AnalysisRawResults {
schema: ResultSetSchema;
resultSet: RawResultSet;
fileLinkPrefix: string;
sourceLocationPrefix: string;

View File

@@ -21,9 +21,8 @@ export const createVariantAnalysisContentProvider = (
}
const variantAnalysisId = parseInt(variantAnalysisIdString);
const variantAnalysis = await variantAnalysisManager.getVariantAnalysis(
variantAnalysisId,
);
const variantAnalysis =
await variantAnalysisManager.getVariantAnalysis(variantAnalysisId);
if (!variantAnalysis) {
void showAndLogWarningMessage(
extLogger,

View File

@@ -180,9 +180,8 @@ export class VariantAnalysisResultsManager extends DisposableObject {
repositoryFullName,
);
const repoTask: VariantAnalysisRepositoryTask = await readRepoTask(
storageDirectory,
);
const repoTask: VariantAnalysisRepositoryTask =
await readRepoTask(storageDirectory);
if (!repoTask.databaseCommitSha || !repoTask.sourceLocationPrefix) {
throw new Error("Missing database commit SHA");

View File

@@ -1,6 +1,6 @@
import * as React from "react";
import { render as reactRender, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { userEvent } from "@testing-library/user-event";
import { CodePaths, CodePathsProps } from "../CodePaths";
import { createMockCodeFlows } from "../../../../../test/factories/variant-analysis/shared/CodeFlow";

View File

@@ -39,7 +39,7 @@ export const DeterminateProgressRing = ({ percent }: Props) => (
aria-valuemax={100}
aria-valuenow={percent}
>
<svg className="progress" viewBox="0 0 16 16">
<svg className="progress" viewBox="0 0 16 16" role="presentation">
<Background cx="8px" cy="8px" r="7px" />
<Determinate
style={{

View File

@@ -1,6 +1,6 @@
import * as React from "react";
import { styled } from "styled-components";
import classNames from "classnames";
import * as classNames from "classnames";
type Props = {
name: string;

View File

@@ -1,6 +1,6 @@
import * as React from "react";
import { Codicon } from "./Codicon";
import classNames from "classnames";
import * as classNames from "classnames";
type Props = {
label?: string;

View File

@@ -1,13 +1,13 @@
import * as React from "react";
import { ResultRow } from "../../common/bqrs-cli-types";
import { sendTelemetry } from "../common/telemetry";
import { Column, Row } from "../../common/raw-result-types";
import RawTableHeader from "../results/RawTableHeader";
import RawTableRow from "../results/RawTableRow";
interface Props {
columns: ReadonlyArray<{ name?: string }>;
columns: readonly Column[];
schemaName: string;
rows: ResultRow[];
rows: Row[];
databaseUri: string;
className?: string;

View File

@@ -7,7 +7,7 @@ import { MethodName } from "../model-editor/MethodName";
import { ModeledMethod } from "../../model-editor/modeled-method";
import { VSCodeTag } from "@vscode/webview-ui-toolkit/react";
import { ReviewInEditorButton } from "./ReviewInEditorButton";
import { ModeledMethodsPanel } from "./ModeledMethodsPanel";
import { MultipleModeledMethodsPanel } from "./MultipleModeledMethodsPanel";
import { QueryLanguage } from "../../common/query-language";
const Container = styled.div`
@@ -55,7 +55,6 @@ export type MethodModelingProps = {
method: Method;
modeledMethods: ModeledMethod[];
isModelingInProgress: boolean;
showMultipleModels?: boolean;
onChange: (methodSignature: string, modeledMethods: ModeledMethod[]) => void;
};
@@ -65,7 +64,6 @@ export const MethodModeling = ({
modeledMethods,
method,
isModelingInProgress,
showMultipleModels = false,
onChange,
}: MethodModelingProps): JSX.Element => {
return (
@@ -79,11 +77,10 @@ export const MethodModeling = ({
<ModelingStatusIndicator status={modelingStatus} />
<MethodName {...method} />
</DependencyContainer>
<ModeledMethodsPanel
<MultipleModeledMethodsPanel
language={language}
method={method}
modeledMethods={modeledMethods}
showMultipleModels={showMultipleModels}
isModelingInProgress={isModelingInProgress}
modelingStatus={modelingStatus}
onChange={onChange}

View File

@@ -110,7 +110,6 @@ export function MethodModelingView({ initialViewState }: Props): JSX.Element {
method={method}
modeledMethods={modeledMethods}
isModelingInProgress={isModelingInProgress}
showMultipleModels={viewState?.showMultipleModels}
onChange={onChange}
/>
);

View File

@@ -1,65 +0,0 @@
import * as React from "react";
import { useCallback } from "react";
import { ModeledMethod } from "../../model-editor/modeled-method";
import { MethodModelingInputs } from "./MethodModelingInputs";
import { Method } from "../../model-editor/method";
import { styled } from "styled-components";
import { MultipleModeledMethodsPanel } from "./MultipleModeledMethodsPanel";
import { convertToLegacyModeledMethod } from "../../model-editor/shared/modeled-methods-legacy";
import { QueryLanguage } from "../../common/query-language";
import { ModelingStatus } from "../../model-editor/shared/modeling-status";
export type ModeledMethodsPanelProps = {
language: QueryLanguage;
method: Method;
modeledMethods: ModeledMethod[];
modelingStatus: ModelingStatus;
isModelingInProgress: boolean;
showMultipleModels: boolean;
onChange: (methodSignature: string, modeledMethods: ModeledMethod[]) => void;
};
const SingleMethodModelingInputs = styled(MethodModelingInputs)`
padding-bottom: 0.5rem;
`;
export const ModeledMethodsPanel = ({
language,
method,
modeledMethods,
modelingStatus,
isModelingInProgress,
showMultipleModels,
onChange,
}: ModeledMethodsPanelProps) => {
const handleSingleChange = useCallback(
(modeledMethod: ModeledMethod) => {
onChange(modeledMethod.signature, [modeledMethod]);
},
[onChange],
);
if (!showMultipleModels) {
return (
<SingleMethodModelingInputs
language={language}
method={method}
modeledMethod={convertToLegacyModeledMethod(modeledMethods)}
modelingStatus={modelingStatus}
isModelingInProgress={isModelingInProgress}
onChange={handleSingleChange}
/>
);
}
return (
<MultipleModeledMethodsPanel
language={language}
method={method}
modeledMethods={modeledMethods}
modelingStatus={modelingStatus}
isModelingInProgress={isModelingInProgress}
onChange={onChange}
/>
);
};

View File

@@ -1,6 +1,6 @@
import * as React from "react";
import { render as reactRender, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { userEvent } from "@testing-library/user-event";
import {
MethodModelingInputs,
MethodModelingInputsProps,

View File

@@ -1,90 +0,0 @@
import * as React from "react";
import { render as reactRender, screen } from "@testing-library/react";
import { createMethod } from "../../../../test/factories/model-editor/method-factories";
import { createSinkModeledMethod } from "../../../../test/factories/model-editor/modeled-method-factories";
import {
ModeledMethodsPanel,
ModeledMethodsPanelProps,
} from "../ModeledMethodsPanel";
import { QueryLanguage } from "../../../common/query-language";
describe(ModeledMethodsPanel.name, () => {
const render = (props: ModeledMethodsPanelProps) =>
reactRender(<ModeledMethodsPanel {...props} />);
const language = QueryLanguage.Java;
const method = createMethod();
const modeledMethods = [createSinkModeledMethod(), createSinkModeledMethod()];
const modelingStatus = "unmodeled";
const isModelingInProgress = false;
const onChange = jest.fn();
describe("when show multiple models is disabled", () => {
const showMultipleModels = false;
it("renders the method modeling inputs", () => {
render({
language,
method,
modeledMethods,
isModelingInProgress,
modelingStatus,
onChange,
showMultipleModels,
});
expect(screen.getAllByRole("combobox")).toHaveLength(4);
});
it("does not render the pagination", () => {
render({
language,
method,
modeledMethods,
isModelingInProgress,
modelingStatus,
onChange,
showMultipleModels,
});
expect(
screen.queryByLabelText("Previous modeling"),
).not.toBeInTheDocument();
expect(screen.queryByLabelText("Next modeling")).not.toBeInTheDocument();
});
});
describe("when show multiple models is enabled", () => {
const showMultipleModels = true;
it("renders the method modeling inputs once", () => {
render({
language,
method,
modeledMethods,
isModelingInProgress,
modelingStatus,
onChange,
showMultipleModels,
});
expect(screen.getAllByRole("combobox")).toHaveLength(4);
});
it("renders the pagination", () => {
render({
language,
method,
modeledMethods,
isModelingInProgress,
modelingStatus,
onChange,
showMultipleModels,
});
expect(screen.getByLabelText("Previous modeling")).toBeInTheDocument();
expect(screen.getByLabelText("Next modeling")).toBeInTheDocument();
expect(screen.getByText("1/2")).toBeInTheDocument();
});
});
});

View File

@@ -10,7 +10,7 @@ import {
MultipleModeledMethodsPanel,
MultipleModeledMethodsPanelProps,
} from "../MultipleModeledMethodsPanel";
import userEvent from "@testing-library/user-event";
import { userEvent } from "@testing-library/user-event";
import { ModeledMethod } from "../../../model-editor/modeled-method";
import { QueryLanguage } from "../../../common/query-language";

View File

@@ -2,7 +2,6 @@ import * as React from "react";
import { styled } from "styled-components";
import { pluralize } from "../../common/word";
import { DataGridCell, DataGridRow } from "../common/DataGrid";
import { ModelEditorViewState } from "../../model-editor/shared/view-state";
const HiddenMethodsCell = styled(DataGridCell)`
text-align: center;
@@ -11,23 +10,19 @@ const HiddenMethodsCell = styled(DataGridCell)`
interface Props {
numHiddenMethods: number;
someMethodsAreVisible: boolean;
viewState: ModelEditorViewState;
}
export function HiddenMethodsRow({
numHiddenMethods,
someMethodsAreVisible,
viewState,
}: Props) {
if (numHiddenMethods === 0) {
return null;
}
const gridColumn = viewState.showMultipleModels ? "span 6" : "span 5";
return (
<DataGridRow>
<HiddenMethodsCell gridColumn={gridColumn}>
<HiddenMethodsCell gridColumn="span 6">
{someMethodsAreVisible && "And "}
{pluralize(numHiddenMethods, "method", "methods")} modeled in other
CodeQL packs

View File

@@ -7,11 +7,27 @@ const Name = styled.span`
word-break: break-all;
`;
const TypeMethodName = (method: Method) => {
if (!method.typeName) {
return <>{method.methodName}</>;
}
if (!method.methodName) {
return <>{method.typeName}</>;
}
return (
<>
{method.typeName}.{method.methodName}
</>
);
};
export const MethodName = (method: Method): JSX.Element => {
return (
<Name>
{method.packageName && <>{method.packageName}.</>}
{method.typeName}.{method.methodName}
<TypeMethodName {...method} />
{method.methodParameters}
</Name>
);

View File

@@ -136,8 +136,8 @@ const ModelableMethodRow = forwardRef<HTMLElement | undefined, MethodRowProps>(
}, [focusedIndex]);
const modeledMethods = useMemo(
() => modeledMethodsToDisplay(modeledMethodsProp, method, viewState),
[modeledMethodsProp, method, viewState],
() => modeledMethodsToDisplay(modeledMethodsProp, method),
[modeledMethodsProp, method],
);
const validationErrors = useMemo(
@@ -219,13 +219,11 @@ const ModelableMethodRow = forwardRef<HTMLElement | undefined, MethodRowProps>(
<DataGridCell>
<InProgressDropdown />
</DataGridCell>
{viewState.showMultipleModels && (
<DataGridCell>
<CodiconRow appearance="icon" disabled={true}>
<Codicon name="add" label="Add new model" />
</CodiconRow>
</DataGridCell>
)}
<DataGridCell>
<CodiconRow appearance="icon" disabled={true}>
<Codicon name="add" label="Add new model" />
</CodiconRow>
</DataGridCell>
</>
)}
{!props.modelingInProgress && (
@@ -267,28 +265,26 @@ const ModelableMethodRow = forwardRef<HTMLElement | undefined, MethodRowProps>(
onChange={modeledMethodChangedHandlers[index]}
/>
</DataGridCell>
{viewState.showMultipleModels && (
<DataGridCell>
{index === modeledMethods.length - 1 ? (
<CodiconRow
appearance="icon"
aria-label="Add new model"
onClick={handleAddModelClick}
disabled={addModelButtonDisabled}
>
<Codicon name="add" />
</CodiconRow>
) : (
<CodiconRow
appearance="icon"
aria-label="Remove model"
onClick={removeModelClickedHandlers[index]}
>
<Codicon name="trash" />
</CodiconRow>
)}
</DataGridCell>
)}
<DataGridCell>
{index === 0 ? (
<CodiconRow
appearance="icon"
aria-label="Add new model"
onClick={handleAddModelClick}
disabled={addModelButtonDisabled}
>
<Codicon name="add" />
</CodiconRow>
) : (
<CodiconRow
appearance="icon"
aria-label="Remove model"
onClick={removeModelClickedHandlers[index]}
>
<Codicon name="trash" />
</CodiconRow>
)}
</DataGridCell>
</DataGridRow>
))}
{validationErrors.map((error, index) => (
@@ -336,9 +332,7 @@ const UnmodelableMethodRow = forwardRef<
<ViewLink onClick={jumpToMethod}>View</ViewLink>
</ApiOrMethodRow>
</DataGridCell>
<DataGridCell gridColumn={`span ${viewState.showMultipleModels ? 5 : 4}`}>
Method already modeled
</DataGridCell>
<DataGridCell gridColumn="span 5">Method already modeled</DataGridCell>
</DataGridRow>
);
});
@@ -354,15 +348,10 @@ function sendJumpToMethodMessage(method: Method) {
function modeledMethodsToDisplay(
modeledMethods: ModeledMethod[],
method: Method,
viewState: ModelEditorViewState,
): ModeledMethod[] {
if (modeledMethods.length === 0) {
return [createEmptyModeledMethod("none", method)];
}
if (viewState.showMultipleModels) {
return modeledMethods;
} else {
return modeledMethods.slice(0, 1);
}
return modeledMethods;
}

View File

@@ -27,14 +27,18 @@ const LoadingContainer = styled.div`
font-weight: 600;
`;
const ModelEditorContainer = styled.div`
margin-top: 1rem;
`;
const ModelEditorContainer = styled.div``;
const HeaderContainer = styled.div`
display: flex;
flex-direction: row;
align-items: end;
background-color: var(--vscode-editor-background);
position: sticky;
z-index: 1;
top: 0;
padding-top: 1rem;
padding-bottom: 1rem;
`;
const HeaderColumn = styled.div`
@@ -67,7 +71,7 @@ const EditorContainer = styled.div`
const ButtonsContainer = styled.div`
display: flex;
gap: 0.4em;
margin-bottom: 1rem;
margin-top: 1rem;
`;
type Props = {
@@ -300,6 +304,25 @@ export function ModelEditor({
</LinkIconButton>
)}
</HeaderRow>
<HeaderRow>
<ButtonsContainer>
<VSCodeButton
onClick={onSaveAllClick}
disabled={modifiedSignatures.size === 0}
>
Save all
</VSCodeButton>
<VSCodeButton appearance="secondary" onClick={onRefreshClick}>
Refresh
</VSCodeButton>
{viewState.showGenerateButton &&
viewState.mode === Mode.Framework && (
<VSCodeButton onClick={onGenerateFromSourceClick}>
Generate
</VSCodeButton>
)}
</ButtonsContainer>
</HeaderRow>
</HeaderColumn>
<HeaderSpacer />
<HeaderColumn>
@@ -313,23 +336,6 @@ export function ModelEditor({
</HeaderContainer>
<EditorContainer>
<ButtonsContainer>
<VSCodeButton
onClick={onSaveAllClick}
disabled={modifiedSignatures.size === 0}
>
Save all
</VSCodeButton>
<VSCodeButton appearance="secondary" onClick={onRefreshClick}>
Refresh
</VSCodeButton>
{viewState.showGenerateButton &&
viewState.mode === Mode.Framework && (
<VSCodeButton onClick={onGenerateFromSourceClick}>
Generate
</VSCodeButton>
)}
</ButtonsContainer>
<ModeledMethodsList
methods={methods}
modeledMethodsMap={modeledMethods}

View File

@@ -9,8 +9,6 @@ import { ModelEditorViewState } from "../../model-editor/shared/view-state";
import { ScreenReaderOnly } from "../common/ScreenReaderOnly";
import { DataGrid, DataGridCell } from "../common/DataGrid";
export const SINGLE_MODEL_GRID_TEMPLATE_COLUMNS =
"0.5fr 0.125fr 0.125fr 0.125fr 0.125fr";
export const MULTIPLE_MODELS_GRID_TEMPLATE_COLUMNS =
"0.5fr 0.125fr 0.125fr 0.125fr 0.125fr max-content";
@@ -61,12 +59,8 @@ export const ModeledMethodDataGrid = ({
const someMethodsAreVisible = methodsWithModelability.length > 0;
const gridTemplateColumns = viewState.showMultipleModels
? MULTIPLE_MODELS_GRID_TEMPLATE_COLUMNS
: SINGLE_MODEL_GRID_TEMPLATE_COLUMNS;
return (
<DataGrid gridTemplateColumns={gridTemplateColumns}>
<DataGrid gridTemplateColumns={MULTIPLE_MODELS_GRID_TEMPLATE_COLUMNS}>
{someMethodsAreVisible && (
<>
<DataGridCell rowType="header">API or method</DataGridCell>
@@ -74,11 +68,9 @@ export const ModeledMethodDataGrid = ({
<DataGridCell rowType="header">Input</DataGridCell>
<DataGridCell rowType="header">Output</DataGridCell>
<DataGridCell rowType="header">Kind</DataGridCell>
{viewState.showMultipleModels && (
<DataGridCell rowType="header">
<ScreenReaderOnly>Add or remove models</ScreenReaderOnly>
</DataGridCell>
)}
<DataGridCell rowType="header">
<ScreenReaderOnly>Add or remove models</ScreenReaderOnly>
</DataGridCell>
{methodsWithModelability.map(({ method, methodCanBeModeled }) => {
const modeledMethods = modeledMethodsMap[method.signature] ?? [];
return (
@@ -100,7 +92,6 @@ export const ModeledMethodDataGrid = ({
<HiddenMethodsRow
numHiddenMethods={numHiddenMethods}
someMethodsAreVisible={someMethodsAreVisible}
viewState={viewState}
/>
</DataGrid>
);

View File

@@ -1,18 +1,11 @@
import * as React from "react";
import { render, screen } from "@testing-library/react";
import { HiddenMethodsRow } from "../HiddenMethodsRow";
import { createMockModelEditorViewState } from "../../../../test/factories/model-editor/view-state";
describe(HiddenMethodsRow.name, () => {
const viewState = createMockModelEditorViewState();
it("does not render with 0 hidden methods", () => {
const { container } = render(
<HiddenMethodsRow
numHiddenMethods={0}
someMethodsAreVisible={true}
viewState={viewState}
/>,
<HiddenMethodsRow numHiddenMethods={0} someMethodsAreVisible={true} />,
);
expect(container).toBeEmptyDOMElement();
@@ -20,11 +13,7 @@ describe(HiddenMethodsRow.name, () => {
it("renders with 1 hidden methods and no visible methods", () => {
render(
<HiddenMethodsRow
numHiddenMethods={1}
someMethodsAreVisible={false}
viewState={viewState}
/>,
<HiddenMethodsRow numHiddenMethods={1} someMethodsAreVisible={false} />,
);
expect(
@@ -34,11 +23,7 @@ describe(HiddenMethodsRow.name, () => {
it("renders with 1 hidden methods and visible methods", () => {
render(
<HiddenMethodsRow
numHiddenMethods={1}
someMethodsAreVisible={true}
viewState={viewState}
/>,
<HiddenMethodsRow numHiddenMethods={1} someMethodsAreVisible={true} />,
);
expect(
@@ -48,11 +33,7 @@ describe(HiddenMethodsRow.name, () => {
it("renders with 3 hidden methods and no visible methods", () => {
render(
<HiddenMethodsRow
numHiddenMethods={3}
someMethodsAreVisible={false}
viewState={viewState}
/>,
<HiddenMethodsRow numHiddenMethods={3} someMethodsAreVisible={false} />,
);
expect(
@@ -62,11 +43,7 @@ describe(HiddenMethodsRow.name, () => {
it("renders with 3 hidden methods and visible methods", () => {
render(
<HiddenMethodsRow
numHiddenMethods={3}
someMethodsAreVisible={true}
viewState={viewState}
/>,
<HiddenMethodsRow numHiddenMethods={3} someMethodsAreVisible={true} />,
);
expect(

View File

@@ -2,7 +2,7 @@ import * as React from "react";
import { render as reactRender, screen } from "@testing-library/react";
import { createMethod } from "../../../../test/factories/model-editor/method-factories";
import { LibraryRow, LibraryRowProps } from "../LibraryRow";
import userEvent from "@testing-library/user-event";
import { userEvent } from "@testing-library/user-event";
import { createMockModelEditorViewState } from "../../../../test/factories/model-editor/view-state";
describe(LibraryRow.name, () => {

View File

@@ -24,4 +24,48 @@ describe(MethodName.name, () => {
const name = `${method.typeName}.${method.methodName}${method.methodParameters}`;
expect(screen.getByText(name)).toBeInTheDocument();
});
it("renders method name without method name but with parameters", () => {
const method = createMethod({
packageName: "",
methodName: "",
});
render(method);
const name = `${method.typeName}${method.methodParameters}`;
expect(screen.getByText(name)).toBeInTheDocument();
});
it("renders method name without method name and parameters", () => {
const method = createMethod({
packageName: "",
methodName: "",
methodParameters: "",
});
render(method);
const name = `${method.typeName}`;
expect(screen.getByText(name)).toBeInTheDocument();
});
it("renders method name without package and type name", () => {
const method = createMethod({
packageName: "",
typeName: "",
});
render(method);
const name = `${method.methodName}${method.methodParameters}`;
expect(screen.getByText(name)).toBeInTheDocument();
});
it("renders method name without type name", () => {
const method = createMethod({
typeName: "",
});
render(method);
const name = `${method.packageName}.${method.methodName}${method.methodParameters}`;
expect(screen.getByText(name)).toBeInTheDocument();
});
});

View File

@@ -7,7 +7,7 @@ import {
import { createMethod } from "../../../../test/factories/model-editor/method-factories";
import { MethodRow, MethodRowProps } from "../MethodRow";
import { ModeledMethod } from "../../../model-editor/modeled-method";
import userEvent from "@testing-library/user-event";
import { userEvent } from "@testing-library/user-event";
import { createMockModelEditorViewState } from "../../../../test/factories/model-editor/view-state";
describe(MethodRow.name, () => {
@@ -195,10 +195,6 @@ describe(MethodRow.name, () => {
{ ...modeledMethod, type: "sink" },
{ ...modeledMethod, type: "summary" },
],
viewState: {
...viewState,
showMultipleModels: true,
},
});
const kindInputs = screen.getAllByRole("combobox", { name: "Model type" });
@@ -208,24 +204,6 @@ describe(MethodRow.name, () => {
expect(kindInputs[2]).toHaveValue("summary");
});
it("renders only first model when showMultipleModels feature flag is disabled", () => {
render({
modeledMethods: [
{ ...modeledMethod, type: "source" },
{ ...modeledMethod, type: "sink" },
{ ...modeledMethod, type: "summary" },
],
viewState: {
...viewState,
showMultipleModels: false,
},
});
const kindInputs = screen.getAllByRole("combobox", { name: "Model type" });
expect(kindInputs.length).toBe(1);
expect(kindInputs[0]).toHaveValue("source");
});
it("can update fields when there are multiple models", async () => {
render({
modeledMethods: [
@@ -233,10 +211,6 @@ describe(MethodRow.name, () => {
{ ...modeledMethod, type: "sink", kind: "code-injection" },
{ ...modeledMethod, type: "summary" },
],
viewState: {
...viewState,
showMultipleModels: true,
},
});
onChange.mockReset();
@@ -268,26 +242,9 @@ describe(MethodRow.name, () => {
expect(screen.getByText("Method already modeled")).toBeInTheDocument();
});
it("doesn't show add/remove buttons when multiple methods feature flag is disabled", async () => {
render({
modeledMethods: [modeledMethod],
viewState: {
...viewState,
showMultipleModels: false,
},
});
expect(screen.queryByLabelText("Add new model")).not.toBeInTheDocument();
expect(screen.queryByLabelText("Remove model")).not.toBeInTheDocument();
});
it("shows disabled button add new model when there are no modeled methods", async () => {
render({
modeledMethods: [],
viewState: {
...viewState,
showMultipleModels: true,
},
});
const addButton = screen.queryByLabelText("Add new model");
@@ -300,10 +257,6 @@ describe(MethodRow.name, () => {
it("disabled button to add new model when there is one unmodeled method", async () => {
render({
modeledMethods: [{ ...modeledMethod, type: "none" }],
viewState: {
...viewState,
showMultipleModels: true,
},
});
const addButton = screen.queryByLabelText("Add new model");
@@ -316,10 +269,6 @@ describe(MethodRow.name, () => {
it("enabled button to add new model when there is one modeled method", async () => {
render({
modeledMethods: [modeledMethod],
viewState: {
...viewState,
showMultipleModels: true,
},
});
const addButton = screen.queryByLabelText("Add new model");
@@ -335,10 +284,6 @@ describe(MethodRow.name, () => {
{ ...modeledMethod, type: "source" },
{ ...modeledMethod, type: "none" },
],
viewState: {
...viewState,
showMultipleModels: true,
},
});
const addButton = screen.queryByLabelText("Add new model");
@@ -350,7 +295,7 @@ describe(MethodRow.name, () => {
expect(removeButton?.getElementsByTagName("input")[0]).toBeEnabled();
});
it("shows add model button on last row and remove model button on all other rows", async () => {
it("shows add model button on first row and remove model button on all other rows", async () => {
render({
modeledMethods: [
{ ...modeledMethod, type: "source" },
@@ -358,10 +303,6 @@ describe(MethodRow.name, () => {
{ ...modeledMethod, type: "summary" },
{ ...modeledMethod, type: "none" },
],
viewState: {
...viewState,
showMultipleModels: true,
},
});
const addButtons = screen.queryAllByLabelText("Add new model");
@@ -378,10 +319,6 @@ describe(MethodRow.name, () => {
it("can add a new model", async () => {
render({
modeledMethods: [modeledMethod],
viewState: {
...viewState,
showMultipleModels: true,
},
});
onChange.mockReset();
@@ -401,7 +338,7 @@ describe(MethodRow.name, () => {
]);
});
it("can delete the first modeled method", async () => {
it("cannot delete the first modeled method (but delete second instead)", async () => {
render({
modeledMethods: [
{ ...modeledMethod, type: "source" },
@@ -409,10 +346,6 @@ describe(MethodRow.name, () => {
{ ...modeledMethod, type: "none" },
{ ...modeledMethod, type: "summary" },
],
viewState: {
...viewState,
showMultipleModels: true,
},
});
onChange.mockReset();
@@ -420,7 +353,7 @@ describe(MethodRow.name, () => {
expect(onChange).toHaveBeenCalledTimes(1);
expect(onChange).toHaveBeenCalledWith(method.signature, [
{ ...modeledMethod, type: "sink" },
{ ...modeledMethod, type: "source" },
{ ...modeledMethod, type: "none" },
{ ...modeledMethod, type: "summary" },
]);
@@ -434,14 +367,10 @@ describe(MethodRow.name, () => {
{ ...modeledMethod, type: "none" },
{ ...modeledMethod, type: "summary" },
],
viewState: {
...viewState,
showMultipleModels: true,
},
});
onChange.mockReset();
await userEvent.click(screen.getAllByLabelText("Remove model")[2]);
await userEvent.click(screen.getAllByLabelText("Remove model")[1]);
expect(onChange).toHaveBeenCalledTimes(1);
expect(onChange).toHaveBeenCalledWith(method.signature, [
@@ -457,10 +386,6 @@ describe(MethodRow.name, () => {
{ ...modeledMethod, type: "source" },
{ ...modeledMethod, type: "sink" },
],
viewState: {
...viewState,
showMultipleModels: true,
},
});
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
@@ -472,10 +397,6 @@ describe(MethodRow.name, () => {
{ ...modeledMethod, type: "source" },
{ ...modeledMethod, type: "source" },
],
viewState: {
...viewState,
showMultipleModels: true,
},
});
expect(screen.getByRole("alert")).toBeInTheDocument();
@@ -494,10 +415,6 @@ describe(MethodRow.name, () => {
{ ...modeledMethod, type: "source" },
{ ...modeledMethod, type: "neutral", kind: "source" },
],
viewState: {
...viewState,
showMultipleModels: true,
},
});
expect(screen.getAllByRole("alert").length).toBe(2);

View File

@@ -1,7 +1,7 @@
import * as React from "react";
import { render, screen } from "@testing-library/react";
import { ModelKindDropdown } from "../ModelKindDropdown";
import userEvent from "@testing-library/user-event";
import { userEvent } from "@testing-library/user-event";
import {
createNoneModeledMethod,
createSinkModeledMethod,

View File

@@ -2,8 +2,9 @@ import * as React from "react";
import { select } from "d3";
import { jumpToLocation } from "./result-table-utils";
import { graphviz, GraphvizOptions } from "d3-graphviz";
import { tryGetLocationFromString } from "../../common/bqrs-utils";
import { useCallback, useEffect } from "react";
import { mapUrlValue } from "../../common/bqrs-raw-results-mapper";
import { isUrlValueResolvable } from "../../common/raw-result-types";
type GraphProps = {
graphData: string;
@@ -42,8 +43,8 @@ export function Graph({ graphData, databaseUri }: GraphProps) {
.attributer(function (d) {
if (d.tag === "a") {
const url = d.attributes["xlink:href"] || d.attributes["href"];
const loc = tryGetLocationFromString(url);
if (loc !== undefined) {
const loc = mapUrlValue(url);
if (loc !== undefined && isUrlValueResolvable(loc)) {
d.attributes["xlink:href"] = "#";
d.attributes["href"] = "#";
loc.uri = `file://${loc.uri}`;

View File

@@ -6,21 +6,22 @@ import {
RawResultsSortState,
NavigateMsg,
NavigationDirection,
RawTableResultSet,
} from "../../common/interface-types";
import RawTableHeader from "./RawTableHeader";
import RawTableRow from "./RawTableRow";
import { ResultRow } from "../../common/bqrs-cli-types";
import { onNavigation } from "./ResultsApp";
import { tryGetResolvableLocation } from "../../common/bqrs-utils";
import { sendTelemetry } from "../common/telemetry";
import { assertNever } from "../../common/helpers-pure";
import { EmptyQueryResultsMessage } from "./EmptyQueryResultsMessage";
import { useScrollIntoView } from "./useScrollIntoView";
import {
isUrlValueResolvable,
RawResultSet,
} from "../../common/raw-result-types";
type RawTableProps = {
databaseUri: string;
resultSet: RawTableResultSet;
resultSet: RawResultSet;
sortState?: RawResultsSortState;
offset: number;
};
@@ -67,11 +68,12 @@ export function RawTable({
return prevSelectedItem;
}
const cellData = rowData[nextColumn];
if (cellData != null && typeof cellData === "object") {
const location = tryGetResolvableLocation(cellData.url);
if (location !== undefined) {
jumpToLocation(location, databaseUri);
}
if (
cellData?.type === "entity" &&
cellData.value.url &&
isUrlValueResolvable(cellData.value.url)
) {
jumpToLocation(cellData.value.url, databaseUri);
}
return { row: nextRow, column: nextColumn };
});
@@ -126,7 +128,7 @@ export function RawTable({
return <EmptyQueryResultsMessage />;
}
const tableRows = dataRows.map((row: ResultRow, rowIndex: number) => (
const tableRows = dataRows.map((row, rowIndex) => (
<RawTableRow
key={rowIndex}
rowIndex={rowIndex + offset}
@@ -159,8 +161,8 @@ export function RawTable({
return (
<table className={className}>
<RawTableHeader
columns={resultSet.schema.columns}
schemaName={resultSet.schema.name}
columns={resultSet.columns}
schemaName={resultSet.name}
sortState={sortState}
/>
<tbody>{tableRows}</tbody>

View File

@@ -6,11 +6,10 @@ import {
SortDirection,
} from "../../common/interface-types";
import { nextSortDirection } from "./result-table-utils";
import { Column } from "../../common/raw-result-types";
interface Props {
readonly columns: ReadonlyArray<{
name?: string;
}>;
readonly columns: readonly Column[];
readonly schemaName: string;
readonly sortState?: RawResultsSortState;
readonly preventSort?: boolean;

View File

@@ -1,11 +1,11 @@
import * as React from "react";
import { ResultRow } from "../../common/bqrs-cli-types";
import { selectedRowClassName, zebraStripe } from "./result-table-utils";
import RawTableValue from "./RawTableValue";
import { Row } from "../../common/raw-result-types";
interface Props {
rowIndex: number;
row: ResultRow;
row: Row;
databaseUri: string;
className?: string;
selectedColumn?: number;

View File

@@ -1,8 +1,9 @@
import * as React from "react";
import { Location } from "./locations/Location";
import { CellValue } from "../../common/bqrs-cli-types";
import { RawNumberValue } from "../common/RawNumberValue";
import { CellValue } from "../../common/raw-result-types";
import { assertNever } from "../../common/helpers-pure";
interface Props {
value: CellValue;
@@ -15,21 +16,23 @@ export default function RawTableValue({
databaseUri,
onSelected,
}: Props): JSX.Element {
switch (typeof value) {
switch (value.type) {
case "boolean":
return <span>{value.toString()}</span>;
return <span>{value.value.toString()}</span>;
case "number":
return <RawNumberValue value={value} />;
return <RawNumberValue value={value.value} />;
case "string":
return <Location label={value.toString()} />;
default:
return <Location label={value.value} />;
case "entity":
return (
<Location
loc={value.url}
label={value.label}
loc={value.value.url}
label={value.value.label}
databaseUri={databaseUri}
onClick={onSelected}
/>
);
default:
assertNever(value);
}
}

View File

@@ -9,7 +9,7 @@ interface Props {
function getResultCount(resultSet: ResultSet): number {
switch (resultSet.t) {
case "RawResultSet":
return resultSet.schema.rows;
return resultSet.resultSet.totalRowCount;
case "InterpretedResultSet":
return resultSet.interpretation.numTotalResults;
}

View File

@@ -10,7 +10,7 @@ export function ResultTable(props: ResultTableProps) {
const { resultSet } = props;
switch (resultSet.t) {
case "RawResultSet":
return <RawTable {...props} resultSet={resultSet} />;
return <RawTable {...props} resultSet={resultSet.resultSet} />;
case "InterpretedResultSet": {
const data = resultSet.interpretation.data;
switch (data.t) {

Some files were not shown because too many files have changed in this diff Show More