Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions extensions/ql-vscode/src/codeql-cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { promisify } from "util";
import { CancellationToken, Disposable, Uri } from "vscode";

import {
BQRSInfo,
BqrsInfo,
DecodedBqrs,
DecodedBqrsChunk,
} from "../common/bqrs-cli-types";
Expand Down Expand Up @@ -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",
Expand Down
86 changes: 28 additions & 58 deletions extensions/ql-vscode/src/common/bqrs-cli-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -13,111 +13,81 @@ 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;
endLine: number;
endColumn: number;
}

export interface WholeFileLocation {
export interface BqrsWholeFileLocation {
uri: string;
startLine: never;
startColumn: never;
endLine: never;
endColumn: never;
}

export type ResolvableLocationValue = WholeFileLocation | LineColumnLocation;

export type UrlValue = ResolvableLocationValue | string;

export type CellValue = EntityValue | number | string | boolean;
export type BqrsUrlValue =
| BqrsWholeFileLocation
| BqrsLineColumnLocation
| string;

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[];
}
Expand Down
216 changes: 216 additions & 0 deletions extensions/ql-vscode/src/common/bqrs-raw-results-mapper.ts
Original file line number Diff line number Diff line change
@@ -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"
);
}
Loading