Merge pull request #2445 from github/koesie10/move-local-databases
Split up `local-databases.ts` file into multiple files
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
import vscode from "vscode";
|
||||
|
||||
/**
|
||||
* The layout of the database.
|
||||
*/
|
||||
export enum DatabaseKind {
|
||||
/** A CodeQL database */
|
||||
Database,
|
||||
/** A raw QL dataset */
|
||||
RawDataset,
|
||||
}
|
||||
|
||||
export interface DatabaseContents {
|
||||
/** The layout of the database */
|
||||
kind: DatabaseKind;
|
||||
/**
|
||||
* The name of the database.
|
||||
*/
|
||||
name: string;
|
||||
/** The URI of the QL dataset within the database. */
|
||||
datasetUri: vscode.Uri;
|
||||
/** The URI of the source archive within the database, if one exists. */
|
||||
sourceArchiveUri?: vscode.Uri;
|
||||
/** The URI of the CodeQL database scheme within the database, if exactly one exists. */
|
||||
dbSchemeUri?: vscode.Uri;
|
||||
}
|
||||
|
||||
export interface DatabaseContentsWithDbScheme extends DatabaseContents {
|
||||
dbSchemeUri: vscode.Uri; // Always present
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { DatabaseItem } from "./database-item";
|
||||
|
||||
export enum DatabaseEventKind {
|
||||
Add = "Add",
|
||||
Remove = "Remove",
|
||||
|
||||
// Fired when databases are refreshed from persisted state
|
||||
Refresh = "Refresh",
|
||||
|
||||
// Fired when the current database changes
|
||||
Change = "Change",
|
||||
|
||||
Rename = "Rename",
|
||||
}
|
||||
|
||||
export interface DatabaseChangedEvent {
|
||||
kind: DatabaseEventKind;
|
||||
item: DatabaseItem | undefined;
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
// Exported for testing
|
||||
import * as cli from "../../codeql-cli/cli";
|
||||
import vscode from "vscode";
|
||||
import { FullDatabaseOptions } from "./database-options";
|
||||
import { basename, dirname, join, relative } from "path";
|
||||
import { asError } from "../../pure/helpers-pure";
|
||||
import {
|
||||
decodeSourceArchiveUri,
|
||||
encodeArchiveBasePath,
|
||||
encodeSourceArchiveUri,
|
||||
zipArchiveScheme,
|
||||
} from "../../common/vscode/archive-filesystem-provider";
|
||||
import { DatabaseItem, PersistedDatabaseItem } from "./database-item";
|
||||
import { isLikelyDatabaseRoot } from "../../helpers";
|
||||
import { stat } from "fs-extra";
|
||||
import { pathsEqual } from "../../pure/files";
|
||||
import { DatabaseContents } from "./database-contents";
|
||||
import { DatabaseResolver } from "./database-resolver";
|
||||
import { DatabaseChangedEvent, DatabaseEventKind } from "./database-events";
|
||||
|
||||
export class DatabaseItemImpl implements DatabaseItem {
|
||||
private _error: Error | undefined = undefined;
|
||||
private _contents: DatabaseContents | undefined;
|
||||
/** A cache of database info */
|
||||
private _dbinfo: cli.DbInfo | undefined;
|
||||
|
||||
public constructor(
|
||||
public readonly databaseUri: vscode.Uri,
|
||||
contents: DatabaseContents | undefined,
|
||||
private options: FullDatabaseOptions,
|
||||
private readonly onChanged: (event: DatabaseChangedEvent) => void,
|
||||
) {
|
||||
this._contents = contents;
|
||||
}
|
||||
|
||||
public get name(): string {
|
||||
if (this.options.displayName) {
|
||||
return this.options.displayName;
|
||||
} else if (this._contents) {
|
||||
return this._contents.name;
|
||||
} else {
|
||||
return basename(this.databaseUri.fsPath);
|
||||
}
|
||||
}
|
||||
|
||||
public set name(newName: string) {
|
||||
this.options.displayName = newName;
|
||||
}
|
||||
|
||||
public get sourceArchive(): vscode.Uri | undefined {
|
||||
if (this.options.ignoreSourceArchive || this._contents === undefined) {
|
||||
return undefined;
|
||||
} else {
|
||||
return this._contents.sourceArchiveUri;
|
||||
}
|
||||
}
|
||||
|
||||
public get contents(): DatabaseContents | undefined {
|
||||
return this._contents;
|
||||
}
|
||||
|
||||
public get dateAdded(): number | undefined {
|
||||
return this.options.dateAdded;
|
||||
}
|
||||
|
||||
public get error(): Error | undefined {
|
||||
return this._error;
|
||||
}
|
||||
|
||||
public async refresh(): Promise<void> {
|
||||
try {
|
||||
try {
|
||||
this._contents = await DatabaseResolver.resolveDatabaseContents(
|
||||
this.databaseUri,
|
||||
);
|
||||
this._error = undefined;
|
||||
} catch (e) {
|
||||
this._contents = undefined;
|
||||
this._error = asError(e);
|
||||
throw e;
|
||||
}
|
||||
} finally {
|
||||
this.onChanged({
|
||||
kind: DatabaseEventKind.Refresh,
|
||||
item: this,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public resolveSourceFile(uriStr: string | undefined): vscode.Uri {
|
||||
const sourceArchive = this.sourceArchive;
|
||||
const uri = uriStr ? vscode.Uri.parse(uriStr, true) : undefined;
|
||||
if (uri && uri.scheme !== "file") {
|
||||
throw new Error(
|
||||
`Invalid uri scheme in ${uriStr}. Only 'file' is allowed.`,
|
||||
);
|
||||
}
|
||||
if (!sourceArchive) {
|
||||
if (uri) {
|
||||
return uri;
|
||||
} else {
|
||||
return this.databaseUri;
|
||||
}
|
||||
}
|
||||
|
||||
if (uri) {
|
||||
const relativeFilePath = decodeURI(uri.path)
|
||||
.replace(":", "_")
|
||||
.replace(/^\/*/, "");
|
||||
if (sourceArchive.scheme === zipArchiveScheme) {
|
||||
const zipRef = decodeSourceArchiveUri(sourceArchive);
|
||||
const pathWithinSourceArchive =
|
||||
zipRef.pathWithinSourceArchive === "/"
|
||||
? relativeFilePath
|
||||
: `${zipRef.pathWithinSourceArchive}/${relativeFilePath}`;
|
||||
return encodeSourceArchiveUri({
|
||||
pathWithinSourceArchive,
|
||||
sourceArchiveZipPath: zipRef.sourceArchiveZipPath,
|
||||
});
|
||||
} else {
|
||||
let newPath = sourceArchive.path;
|
||||
if (!newPath.endsWith("/")) {
|
||||
// Ensure a trailing slash.
|
||||
newPath += "/";
|
||||
}
|
||||
newPath += relativeFilePath;
|
||||
|
||||
return sourceArchive.with({ path: newPath });
|
||||
}
|
||||
} else {
|
||||
return sourceArchive;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the state of this database, to be persisted in the workspace state.
|
||||
*/
|
||||
public getPersistedState(): PersistedDatabaseItem {
|
||||
return {
|
||||
uri: this.databaseUri.toString(true),
|
||||
options: this.options,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if the database item refers to an exported snapshot
|
||||
*/
|
||||
public async hasMetadataFile(): Promise<boolean> {
|
||||
return await isLikelyDatabaseRoot(this.databaseUri.fsPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns information about a database.
|
||||
*/
|
||||
private async getDbInfo(server: cli.CodeQLCliServer): Promise<cli.DbInfo> {
|
||||
if (this._dbinfo === undefined) {
|
||||
this._dbinfo = await server.resolveDatabase(this.databaseUri.fsPath);
|
||||
}
|
||||
return this._dbinfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `sourceLocationPrefix` of database. Requires that the database
|
||||
* has a `.dbinfo` file, which is the source of the prefix.
|
||||
*/
|
||||
public async getSourceLocationPrefix(
|
||||
server: cli.CodeQLCliServer,
|
||||
): Promise<string> {
|
||||
const dbInfo = await this.getDbInfo(server);
|
||||
return dbInfo.sourceLocationPrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns path to dataset folder of database.
|
||||
*/
|
||||
public async getDatasetFolder(server: cli.CodeQLCliServer): Promise<string> {
|
||||
const dbInfo = await this.getDbInfo(server);
|
||||
return dbInfo.datasetFolder;
|
||||
}
|
||||
|
||||
public get language() {
|
||||
return this.options.language || "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the root uri of the virtual filesystem for this database's source archive.
|
||||
*/
|
||||
public getSourceArchiveExplorerUri(): vscode.Uri {
|
||||
const sourceArchive = this.sourceArchive;
|
||||
if (sourceArchive === undefined || !sourceArchive.fsPath.endsWith(".zip")) {
|
||||
throw new Error(this.verifyZippedSources());
|
||||
}
|
||||
return encodeArchiveBasePath(sourceArchive.fsPath);
|
||||
}
|
||||
|
||||
public verifyZippedSources(): string | undefined {
|
||||
const sourceArchive = this.sourceArchive;
|
||||
if (sourceArchive === undefined) {
|
||||
return `${this.name} has no source archive.`;
|
||||
}
|
||||
|
||||
if (!sourceArchive.fsPath.endsWith(".zip")) {
|
||||
return `${this.name} has a source folder that is unzipped.`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `uri` belongs to this database's source archive.
|
||||
*/
|
||||
public belongsToSourceArchiveExplorerUri(uri: vscode.Uri): boolean {
|
||||
if (this.sourceArchive === undefined) return false;
|
||||
return (
|
||||
uri.scheme === zipArchiveScheme &&
|
||||
decodeSourceArchiveUri(uri).sourceArchiveZipPath ===
|
||||
this.sourceArchive.fsPath
|
||||
);
|
||||
}
|
||||
|
||||
public async isAffectedByTest(testPath: string): Promise<boolean> {
|
||||
const databasePath = this.databaseUri.fsPath;
|
||||
if (!databasePath.endsWith(".testproj")) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const stats = await stat(testPath);
|
||||
if (stats.isDirectory()) {
|
||||
return !relative(testPath, databasePath).startsWith("..");
|
||||
} else {
|
||||
// database for /one/two/three/test.ql is at /one/two/three/three.testproj
|
||||
const testdir = dirname(testPath);
|
||||
const testdirbase = basename(testdir);
|
||||
return pathsEqual(
|
||||
databasePath,
|
||||
join(testdir, `${testdirbase}.testproj`),
|
||||
process.platform,
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// No information available for test path - assume database is unaffected.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import vscode from "vscode";
|
||||
import * as cli from "../../codeql-cli/cli";
|
||||
import { DatabaseContents } from "./database-contents";
|
||||
import { DatabaseOptions } from "./database-options";
|
||||
|
||||
/** An item in the list of available databases */
|
||||
export interface DatabaseItem {
|
||||
/** The URI of the database */
|
||||
readonly databaseUri: vscode.Uri;
|
||||
/** The name of the database to be displayed in the UI */
|
||||
name: string;
|
||||
|
||||
/** The primary language of the database or empty string if unknown */
|
||||
readonly language: string;
|
||||
/** The URI of the database's source archive, or `undefined` if no source archive is to be used. */
|
||||
readonly sourceArchive: vscode.Uri | undefined;
|
||||
/**
|
||||
* The contents of the database.
|
||||
* Will be `undefined` if the database is invalid. Can be updated by calling `refresh()`.
|
||||
*/
|
||||
readonly contents: DatabaseContents | undefined;
|
||||
|
||||
/**
|
||||
* The date this database was added as a unix timestamp. Or undefined if we don't know.
|
||||
*/
|
||||
readonly dateAdded: number | undefined;
|
||||
|
||||
/** If the database is invalid, describes why. */
|
||||
readonly error: Error | undefined;
|
||||
/**
|
||||
* Resolves the contents of the database.
|
||||
*
|
||||
* @remarks
|
||||
* The contents include the database directory, source archive, and metadata about the database.
|
||||
* If the database is invalid, `this.error` is updated with the error object that describes why
|
||||
* the database is invalid. This error is also thrown.
|
||||
*/
|
||||
refresh(): Promise<void>;
|
||||
/**
|
||||
* Resolves a filename to its URI in the source archive.
|
||||
*
|
||||
* @param file Filename within the source archive. May be `undefined` to return a dummy file path.
|
||||
*/
|
||||
resolveSourceFile(file: string | undefined): vscode.Uri;
|
||||
|
||||
/**
|
||||
* Holds if the database item has a `.dbinfo` or `codeql-database.yml` file.
|
||||
*/
|
||||
hasMetadataFile(): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Returns `sourceLocationPrefix` of exported database.
|
||||
*/
|
||||
getSourceLocationPrefix(server: cli.CodeQLCliServer): Promise<string>;
|
||||
|
||||
/**
|
||||
* Returns dataset folder of exported database.
|
||||
*/
|
||||
getDatasetFolder(server: cli.CodeQLCliServer): Promise<string>;
|
||||
|
||||
/**
|
||||
* Returns the root uri of the virtual filesystem for this database's source archive,
|
||||
* as displayed in the filesystem explorer.
|
||||
*/
|
||||
getSourceArchiveExplorerUri(): vscode.Uri;
|
||||
|
||||
/**
|
||||
* Holds if `uri` belongs to this database's source archive.
|
||||
*/
|
||||
belongsToSourceArchiveExplorerUri(uri: vscode.Uri): boolean;
|
||||
|
||||
/**
|
||||
* Whether the database may be affected by test execution for the given path.
|
||||
*/
|
||||
isAffectedByTest(testPath: string): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Gets the state of this database, to be persisted in the workspace state.
|
||||
*/
|
||||
getPersistedState(): PersistedDatabaseItem;
|
||||
|
||||
/**
|
||||
* Verifies that this database item has a zipped source folder. Returns an error message if it does not.
|
||||
*/
|
||||
verifyZippedSources(): string | undefined;
|
||||
}
|
||||
|
||||
export interface PersistedDatabaseItem {
|
||||
uri: string;
|
||||
options?: DatabaseOptions;
|
||||
}
|
||||
@@ -1,50 +1,34 @@
|
||||
import { pathExists, stat, remove } from "fs-extra";
|
||||
import { glob } from "glob";
|
||||
import { join, basename, resolve, relative, dirname, extname } from "path";
|
||||
import * as vscode from "vscode";
|
||||
import * as cli from "../codeql-cli/cli";
|
||||
import { ExtensionContext } from "vscode";
|
||||
import {
|
||||
showAndLogWarningMessage,
|
||||
showAndLogInformationMessage,
|
||||
isLikelyDatabaseRoot,
|
||||
showAndLogExceptionWithTelemetry,
|
||||
isFolderAlreadyInWorkspace,
|
||||
getFirstWorkspaceFolder,
|
||||
showNeverAskAgainDialog,
|
||||
} from "../helpers";
|
||||
import { ProgressCallback, withProgress } from "../common/vscode/progress";
|
||||
import {
|
||||
zipArchiveScheme,
|
||||
encodeArchiveBasePath,
|
||||
decodeSourceArchiveUri,
|
||||
encodeSourceArchiveUri,
|
||||
} from "../common/vscode/archive-filesystem-provider";
|
||||
import { DisposableObject } from "../pure/disposable-object";
|
||||
import { Logger, extLogger } from "../common";
|
||||
import { asError, getErrorMessage } from "../pure/helpers-pure";
|
||||
import { QueryRunner } from "../query-server";
|
||||
import { containsPath, pathsEqual } from "../pure/files";
|
||||
import { redactableError } from "../pure/errors";
|
||||
import vscode, { ExtensionContext } from "vscode";
|
||||
import { extLogger, Logger } from "../../common";
|
||||
import { DisposableObject } from "../../pure/disposable-object";
|
||||
import { App } from "../../common/app";
|
||||
import { QueryRunner } from "../../query-server";
|
||||
import * as cli from "../../codeql-cli/cli";
|
||||
import { ProgressCallback, withProgress } from "../../common/vscode/progress";
|
||||
import {
|
||||
getAutogenerateQlPacks,
|
||||
isCodespacesTemplate,
|
||||
setAutogenerateQlPacks,
|
||||
} from "../config";
|
||||
import { QlPackGenerator } from "../qlpack-generator";
|
||||
import { QueryLanguage } from "../common/query-language";
|
||||
import { App } from "../common/app";
|
||||
} from "../../config";
|
||||
import { extname, join } from "path";
|
||||
import { FullDatabaseOptions } from "./database-options";
|
||||
import { DatabaseItemImpl } from "./database-item-impl";
|
||||
import {
|
||||
getFirstWorkspaceFolder,
|
||||
isFolderAlreadyInWorkspace,
|
||||
showAndLogExceptionWithTelemetry,
|
||||
showNeverAskAgainDialog,
|
||||
} from "../../helpers";
|
||||
import { existsSync } from "fs";
|
||||
|
||||
/**
|
||||
* databases.ts
|
||||
* ------------
|
||||
* Managing state of what the current database is, and what other
|
||||
* databases have been recently selected.
|
||||
*
|
||||
* The source of truth of the current state resides inside the
|
||||
* `DatabaseManager` class below.
|
||||
*/
|
||||
import { QlPackGenerator } from "../../qlpack-generator";
|
||||
import { QueryLanguage } from "../../common/query-language";
|
||||
import { asError, getErrorMessage } from "../../pure/helpers-pure";
|
||||
import { DatabaseItem, PersistedDatabaseItem } from "./database-item";
|
||||
import { redactableError } from "../../pure/errors";
|
||||
import { remove } from "fs-extra";
|
||||
import { containsPath } from "../../pure/files";
|
||||
import { DatabaseChangedEvent, DatabaseEventKind } from "./database-events";
|
||||
import { DatabaseResolver } from "./database-resolver";
|
||||
|
||||
/**
|
||||
* The name of the key in the workspaceState dictionary in which we
|
||||
@@ -58,509 +42,6 @@ const CURRENT_DB = "currentDatabase";
|
||||
*/
|
||||
const DB_LIST = "databaseList";
|
||||
|
||||
export interface DatabaseOptions {
|
||||
displayName?: string;
|
||||
ignoreSourceArchive?: boolean;
|
||||
dateAdded?: number | undefined;
|
||||
language?: string;
|
||||
}
|
||||
|
||||
export interface FullDatabaseOptions extends DatabaseOptions {
|
||||
ignoreSourceArchive: boolean;
|
||||
dateAdded: number | undefined;
|
||||
language: string | undefined;
|
||||
}
|
||||
|
||||
interface PersistedDatabaseItem {
|
||||
uri: string;
|
||||
options?: DatabaseOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* The layout of the database.
|
||||
*/
|
||||
export enum DatabaseKind {
|
||||
/** A CodeQL database */
|
||||
Database,
|
||||
/** A raw QL dataset */
|
||||
RawDataset,
|
||||
}
|
||||
|
||||
export interface DatabaseContents {
|
||||
/** The layout of the database */
|
||||
kind: DatabaseKind;
|
||||
/**
|
||||
* The name of the database.
|
||||
*/
|
||||
name: string;
|
||||
/** The URI of the QL dataset within the database. */
|
||||
datasetUri: vscode.Uri;
|
||||
/** The URI of the source archive within the database, if one exists. */
|
||||
sourceArchiveUri?: vscode.Uri;
|
||||
/** The URI of the CodeQL database scheme within the database, if exactly one exists. */
|
||||
dbSchemeUri?: vscode.Uri;
|
||||
}
|
||||
|
||||
export interface DatabaseContentsWithDbScheme extends DatabaseContents {
|
||||
dbSchemeUri: vscode.Uri; // Always present
|
||||
}
|
||||
|
||||
/**
|
||||
* An error thrown when we cannot find a valid database in a putative
|
||||
* database directory.
|
||||
*/
|
||||
class InvalidDatabaseError extends Error {}
|
||||
|
||||
async function findDataset(parentDirectory: string): Promise<vscode.Uri> {
|
||||
/*
|
||||
* Look directly in the root
|
||||
*/
|
||||
let dbRelativePaths = await glob("db-*/", {
|
||||
cwd: parentDirectory,
|
||||
});
|
||||
|
||||
if (dbRelativePaths.length === 0) {
|
||||
/*
|
||||
* Check If they are in the old location
|
||||
*/
|
||||
dbRelativePaths = await glob("working/db-*/", {
|
||||
cwd: parentDirectory,
|
||||
});
|
||||
}
|
||||
if (dbRelativePaths.length === 0) {
|
||||
throw new InvalidDatabaseError(
|
||||
`'${parentDirectory}' does not contain a dataset directory.`,
|
||||
);
|
||||
}
|
||||
|
||||
const dbAbsolutePath = join(parentDirectory, dbRelativePaths[0]);
|
||||
if (dbRelativePaths.length > 1) {
|
||||
void showAndLogWarningMessage(
|
||||
`Found multiple dataset directories in database, using '${dbAbsolutePath}'.`,
|
||||
);
|
||||
}
|
||||
|
||||
return vscode.Uri.file(dbAbsolutePath);
|
||||
}
|
||||
|
||||
// exported for testing
|
||||
export async function findSourceArchive(
|
||||
databasePath: string,
|
||||
): Promise<vscode.Uri | undefined> {
|
||||
const relativePaths = ["src", "output/src_archive"];
|
||||
|
||||
for (const relativePath of relativePaths) {
|
||||
const basePath = join(databasePath, relativePath);
|
||||
const zipPath = `${basePath}.zip`;
|
||||
|
||||
// Prefer using a zip archive over a directory.
|
||||
if (await pathExists(zipPath)) {
|
||||
return encodeArchiveBasePath(zipPath);
|
||||
} else if (await pathExists(basePath)) {
|
||||
return vscode.Uri.file(basePath);
|
||||
}
|
||||
}
|
||||
|
||||
void showAndLogInformationMessage(
|
||||
`Could not find source archive for database '${databasePath}'. Assuming paths are absolute.`,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Gets the relative paths of all `.dbscheme` files in the given directory. */
|
||||
async function getDbSchemeFiles(dbDirectory: string): Promise<string[]> {
|
||||
return await glob("*.dbscheme", { cwd: dbDirectory });
|
||||
}
|
||||
|
||||
export class DatabaseResolver {
|
||||
public static async resolveDatabaseContents(
|
||||
uri: vscode.Uri,
|
||||
): Promise<DatabaseContentsWithDbScheme> {
|
||||
if (uri.scheme !== "file") {
|
||||
throw new Error(
|
||||
`Database URI scheme '${uri.scheme}' not supported; only 'file' URIs are supported.`,
|
||||
);
|
||||
}
|
||||
const databasePath = uri.fsPath;
|
||||
if (!(await pathExists(databasePath))) {
|
||||
throw new InvalidDatabaseError(
|
||||
`Database '${databasePath}' does not exist.`,
|
||||
);
|
||||
}
|
||||
|
||||
const contents = await this.resolveDatabase(databasePath);
|
||||
|
||||
if (contents === undefined) {
|
||||
throw new InvalidDatabaseError(
|
||||
`'${databasePath}' is not a valid database.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Look for a single dbscheme file within the database.
|
||||
// This should be found in the dataset directory, regardless of the form of database.
|
||||
const dbPath = contents.datasetUri.fsPath;
|
||||
const dbSchemeFiles = await getDbSchemeFiles(dbPath);
|
||||
if (dbSchemeFiles.length === 0) {
|
||||
throw new InvalidDatabaseError(
|
||||
`Database '${databasePath}' does not contain a CodeQL dbscheme under '${dbPath}'.`,
|
||||
);
|
||||
} else if (dbSchemeFiles.length > 1) {
|
||||
throw new InvalidDatabaseError(
|
||||
`Database '${databasePath}' contains multiple CodeQL dbschemes under '${dbPath}'.`,
|
||||
);
|
||||
} else {
|
||||
const dbSchemeUri = vscode.Uri.file(resolve(dbPath, dbSchemeFiles[0]));
|
||||
return {
|
||||
...contents,
|
||||
dbSchemeUri,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static async resolveDatabase(
|
||||
databasePath: string,
|
||||
): Promise<DatabaseContents> {
|
||||
const name = basename(databasePath);
|
||||
|
||||
// Look for dataset and source archive.
|
||||
const datasetUri = await findDataset(databasePath);
|
||||
const sourceArchiveUri = await findSourceArchive(databasePath);
|
||||
|
||||
return {
|
||||
kind: DatabaseKind.Database,
|
||||
name,
|
||||
datasetUri,
|
||||
sourceArchiveUri,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** An item in the list of available databases */
|
||||
export interface DatabaseItem {
|
||||
/** The URI of the database */
|
||||
readonly databaseUri: vscode.Uri;
|
||||
/** The name of the database to be displayed in the UI */
|
||||
name: string;
|
||||
|
||||
/** The primary language of the database or empty string if unknown */
|
||||
readonly language: string;
|
||||
/** The URI of the database's source archive, or `undefined` if no source archive is to be used. */
|
||||
readonly sourceArchive: vscode.Uri | undefined;
|
||||
/**
|
||||
* The contents of the database.
|
||||
* Will be `undefined` if the database is invalid. Can be updated by calling `refresh()`.
|
||||
*/
|
||||
readonly contents: DatabaseContents | undefined;
|
||||
|
||||
/**
|
||||
* The date this database was added as a unix timestamp. Or undefined if we don't know.
|
||||
*/
|
||||
readonly dateAdded: number | undefined;
|
||||
|
||||
/** If the database is invalid, describes why. */
|
||||
readonly error: Error | undefined;
|
||||
/**
|
||||
* Resolves the contents of the database.
|
||||
*
|
||||
* @remarks
|
||||
* The contents include the database directory, source archive, and metadata about the database.
|
||||
* If the database is invalid, `this.error` is updated with the error object that describes why
|
||||
* the database is invalid. This error is also thrown.
|
||||
*/
|
||||
refresh(): Promise<void>;
|
||||
/**
|
||||
* Resolves a filename to its URI in the source archive.
|
||||
*
|
||||
* @param file Filename within the source archive. May be `undefined` to return a dummy file path.
|
||||
*/
|
||||
resolveSourceFile(file: string | undefined): vscode.Uri;
|
||||
|
||||
/**
|
||||
* Holds if the database item has a `.dbinfo` or `codeql-database.yml` file.
|
||||
*/
|
||||
hasMetadataFile(): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Returns `sourceLocationPrefix` of exported database.
|
||||
*/
|
||||
getSourceLocationPrefix(server: cli.CodeQLCliServer): Promise<string>;
|
||||
|
||||
/**
|
||||
* Returns dataset folder of exported database.
|
||||
*/
|
||||
getDatasetFolder(server: cli.CodeQLCliServer): Promise<string>;
|
||||
|
||||
/**
|
||||
* Returns the root uri of the virtual filesystem for this database's source archive,
|
||||
* as displayed in the filesystem explorer.
|
||||
*/
|
||||
getSourceArchiveExplorerUri(): vscode.Uri;
|
||||
|
||||
/**
|
||||
* Holds if `uri` belongs to this database's source archive.
|
||||
*/
|
||||
belongsToSourceArchiveExplorerUri(uri: vscode.Uri): boolean;
|
||||
|
||||
/**
|
||||
* Whether the database may be affected by test execution for the given path.
|
||||
*/
|
||||
isAffectedByTest(testPath: string): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Gets the state of this database, to be persisted in the workspace state.
|
||||
*/
|
||||
getPersistedState(): PersistedDatabaseItem;
|
||||
|
||||
/**
|
||||
* Verifies that this database item has a zipped source folder. Returns an error message if it does not.
|
||||
*/
|
||||
verifyZippedSources(): string | undefined;
|
||||
}
|
||||
|
||||
export enum DatabaseEventKind {
|
||||
Add = "Add",
|
||||
Remove = "Remove",
|
||||
|
||||
// Fired when databases are refreshed from persisted state
|
||||
Refresh = "Refresh",
|
||||
|
||||
// Fired when the current database changes
|
||||
Change = "Change",
|
||||
|
||||
Rename = "Rename",
|
||||
}
|
||||
|
||||
export interface DatabaseChangedEvent {
|
||||
kind: DatabaseEventKind;
|
||||
item: DatabaseItem | undefined;
|
||||
}
|
||||
|
||||
// Exported for testing
|
||||
export class DatabaseItemImpl implements DatabaseItem {
|
||||
private _error: Error | undefined = undefined;
|
||||
private _contents: DatabaseContents | undefined;
|
||||
/** A cache of database info */
|
||||
private _dbinfo: cli.DbInfo | undefined;
|
||||
|
||||
public constructor(
|
||||
public readonly databaseUri: vscode.Uri,
|
||||
contents: DatabaseContents | undefined,
|
||||
private options: FullDatabaseOptions,
|
||||
private readonly onChanged: (event: DatabaseChangedEvent) => void,
|
||||
) {
|
||||
this._contents = contents;
|
||||
}
|
||||
|
||||
public get name(): string {
|
||||
if (this.options.displayName) {
|
||||
return this.options.displayName;
|
||||
} else if (this._contents) {
|
||||
return this._contents.name;
|
||||
} else {
|
||||
return basename(this.databaseUri.fsPath);
|
||||
}
|
||||
}
|
||||
|
||||
public set name(newName: string) {
|
||||
this.options.displayName = newName;
|
||||
}
|
||||
|
||||
public get sourceArchive(): vscode.Uri | undefined {
|
||||
if (this.options.ignoreSourceArchive || this._contents === undefined) {
|
||||
return undefined;
|
||||
} else {
|
||||
return this._contents.sourceArchiveUri;
|
||||
}
|
||||
}
|
||||
|
||||
public get contents(): DatabaseContents | undefined {
|
||||
return this._contents;
|
||||
}
|
||||
|
||||
public get dateAdded(): number | undefined {
|
||||
return this.options.dateAdded;
|
||||
}
|
||||
|
||||
public get error(): Error | undefined {
|
||||
return this._error;
|
||||
}
|
||||
|
||||
public async refresh(): Promise<void> {
|
||||
try {
|
||||
try {
|
||||
this._contents = await DatabaseResolver.resolveDatabaseContents(
|
||||
this.databaseUri,
|
||||
);
|
||||
this._error = undefined;
|
||||
} catch (e) {
|
||||
this._contents = undefined;
|
||||
this._error = asError(e);
|
||||
throw e;
|
||||
}
|
||||
} finally {
|
||||
this.onChanged({
|
||||
kind: DatabaseEventKind.Refresh,
|
||||
item: this,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public resolveSourceFile(uriStr: string | undefined): vscode.Uri {
|
||||
const sourceArchive = this.sourceArchive;
|
||||
const uri = uriStr ? vscode.Uri.parse(uriStr, true) : undefined;
|
||||
if (uri && uri.scheme !== "file") {
|
||||
throw new Error(
|
||||
`Invalid uri scheme in ${uriStr}. Only 'file' is allowed.`,
|
||||
);
|
||||
}
|
||||
if (!sourceArchive) {
|
||||
if (uri) {
|
||||
return uri;
|
||||
} else {
|
||||
return this.databaseUri;
|
||||
}
|
||||
}
|
||||
|
||||
if (uri) {
|
||||
const relativeFilePath = decodeURI(uri.path)
|
||||
.replace(":", "_")
|
||||
.replace(/^\/*/, "");
|
||||
if (sourceArchive.scheme === zipArchiveScheme) {
|
||||
const zipRef = decodeSourceArchiveUri(sourceArchive);
|
||||
const pathWithinSourceArchive =
|
||||
zipRef.pathWithinSourceArchive === "/"
|
||||
? relativeFilePath
|
||||
: `${zipRef.pathWithinSourceArchive}/${relativeFilePath}`;
|
||||
return encodeSourceArchiveUri({
|
||||
pathWithinSourceArchive,
|
||||
sourceArchiveZipPath: zipRef.sourceArchiveZipPath,
|
||||
});
|
||||
} else {
|
||||
let newPath = sourceArchive.path;
|
||||
if (!newPath.endsWith("/")) {
|
||||
// Ensure a trailing slash.
|
||||
newPath += "/";
|
||||
}
|
||||
newPath += relativeFilePath;
|
||||
|
||||
return sourceArchive.with({ path: newPath });
|
||||
}
|
||||
} else {
|
||||
return sourceArchive;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the state of this database, to be persisted in the workspace state.
|
||||
*/
|
||||
public getPersistedState(): PersistedDatabaseItem {
|
||||
return {
|
||||
uri: this.databaseUri.toString(true),
|
||||
options: this.options,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if the database item refers to an exported snapshot
|
||||
*/
|
||||
public async hasMetadataFile(): Promise<boolean> {
|
||||
return await isLikelyDatabaseRoot(this.databaseUri.fsPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns information about a database.
|
||||
*/
|
||||
private async getDbInfo(server: cli.CodeQLCliServer): Promise<cli.DbInfo> {
|
||||
if (this._dbinfo === undefined) {
|
||||
this._dbinfo = await server.resolveDatabase(this.databaseUri.fsPath);
|
||||
}
|
||||
return this._dbinfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `sourceLocationPrefix` of database. Requires that the database
|
||||
* has a `.dbinfo` file, which is the source of the prefix.
|
||||
*/
|
||||
public async getSourceLocationPrefix(
|
||||
server: cli.CodeQLCliServer,
|
||||
): Promise<string> {
|
||||
const dbInfo = await this.getDbInfo(server);
|
||||
return dbInfo.sourceLocationPrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns path to dataset folder of database.
|
||||
*/
|
||||
public async getDatasetFolder(server: cli.CodeQLCliServer): Promise<string> {
|
||||
const dbInfo = await this.getDbInfo(server);
|
||||
return dbInfo.datasetFolder;
|
||||
}
|
||||
|
||||
public get language() {
|
||||
return this.options.language || "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the root uri of the virtual filesystem for this database's source archive.
|
||||
*/
|
||||
public getSourceArchiveExplorerUri(): vscode.Uri {
|
||||
const sourceArchive = this.sourceArchive;
|
||||
if (sourceArchive === undefined || !sourceArchive.fsPath.endsWith(".zip")) {
|
||||
throw new Error(this.verifyZippedSources());
|
||||
}
|
||||
return encodeArchiveBasePath(sourceArchive.fsPath);
|
||||
}
|
||||
|
||||
public verifyZippedSources(): string | undefined {
|
||||
const sourceArchive = this.sourceArchive;
|
||||
if (sourceArchive === undefined) {
|
||||
return `${this.name} has no source archive.`;
|
||||
}
|
||||
|
||||
if (!sourceArchive.fsPath.endsWith(".zip")) {
|
||||
return `${this.name} has a source folder that is unzipped.`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `uri` belongs to this database's source archive.
|
||||
*/
|
||||
public belongsToSourceArchiveExplorerUri(uri: vscode.Uri): boolean {
|
||||
if (this.sourceArchive === undefined) return false;
|
||||
return (
|
||||
uri.scheme === zipArchiveScheme &&
|
||||
decodeSourceArchiveUri(uri).sourceArchiveZipPath ===
|
||||
this.sourceArchive.fsPath
|
||||
);
|
||||
}
|
||||
|
||||
public async isAffectedByTest(testPath: string): Promise<boolean> {
|
||||
const databasePath = this.databaseUri.fsPath;
|
||||
if (!databasePath.endsWith(".testproj")) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const stats = await stat(testPath);
|
||||
if (stats.isDirectory()) {
|
||||
return !relative(testPath, databasePath).startsWith("..");
|
||||
} else {
|
||||
// database for /one/two/three/test.ql is at /one/two/three/three.testproj
|
||||
const testdir = dirname(testPath);
|
||||
const testdirbase = basename(testdir);
|
||||
return pathsEqual(
|
||||
databasePath,
|
||||
join(testdir, `${testdirbase}.testproj`),
|
||||
process.platform,
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// No information available for test path - assume database is unaffected.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A promise that resolves to an event's result value when the event
|
||||
* `event` fires. If waiting for the event takes too long (by default
|
||||
@@ -1164,15 +645,3 @@ export class DatabaseManager extends DisposableObject {
|
||||
return dbInfo.languages?.[0] || "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the set of directories containing upgrades, given a list of
|
||||
* scripts returned by the cli's upgrade resolution.
|
||||
*/
|
||||
export function getUpgradesDirectories(scripts: string[]): vscode.Uri[] {
|
||||
const parentDirs = scripts.map((dir) => dirname(dir));
|
||||
const uniqueParentDirs = new Set(parentDirs);
|
||||
return Array.from(uniqueParentDirs).map((filePath) =>
|
||||
vscode.Uri.file(filePath),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export interface DatabaseOptions {
|
||||
displayName?: string;
|
||||
ignoreSourceArchive?: boolean;
|
||||
dateAdded?: number | undefined;
|
||||
language?: string;
|
||||
}
|
||||
|
||||
export interface FullDatabaseOptions extends DatabaseOptions {
|
||||
ignoreSourceArchive: boolean;
|
||||
dateAdded: number | undefined;
|
||||
language: string | undefined;
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import vscode from "vscode";
|
||||
import { pathExists } from "fs-extra";
|
||||
import { basename, join, resolve } from "path";
|
||||
import {
|
||||
DatabaseContents,
|
||||
DatabaseContentsWithDbScheme,
|
||||
DatabaseKind,
|
||||
} from "./database-contents";
|
||||
import { glob } from "glob";
|
||||
import {
|
||||
showAndLogInformationMessage,
|
||||
showAndLogWarningMessage,
|
||||
} from "../../helpers";
|
||||
import { encodeArchiveBasePath } from "../../common/vscode/archive-filesystem-provider";
|
||||
|
||||
export class DatabaseResolver {
|
||||
public static async resolveDatabaseContents(
|
||||
uri: vscode.Uri,
|
||||
): Promise<DatabaseContentsWithDbScheme> {
|
||||
if (uri.scheme !== "file") {
|
||||
throw new Error(
|
||||
`Database URI scheme '${uri.scheme}' not supported; only 'file' URIs are supported.`,
|
||||
);
|
||||
}
|
||||
const databasePath = uri.fsPath;
|
||||
if (!(await pathExists(databasePath))) {
|
||||
throw new InvalidDatabaseError(
|
||||
`Database '${databasePath}' does not exist.`,
|
||||
);
|
||||
}
|
||||
|
||||
const contents = await this.resolveDatabase(databasePath);
|
||||
|
||||
if (contents === undefined) {
|
||||
throw new InvalidDatabaseError(
|
||||
`'${databasePath}' is not a valid database.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Look for a single dbscheme file within the database.
|
||||
// This should be found in the dataset directory, regardless of the form of database.
|
||||
const dbPath = contents.datasetUri.fsPath;
|
||||
const dbSchemeFiles = await getDbSchemeFiles(dbPath);
|
||||
if (dbSchemeFiles.length === 0) {
|
||||
throw new InvalidDatabaseError(
|
||||
`Database '${databasePath}' does not contain a CodeQL dbscheme under '${dbPath}'.`,
|
||||
);
|
||||
} else if (dbSchemeFiles.length > 1) {
|
||||
throw new InvalidDatabaseError(
|
||||
`Database '${databasePath}' contains multiple CodeQL dbschemes under '${dbPath}'.`,
|
||||
);
|
||||
} else {
|
||||
const dbSchemeUri = vscode.Uri.file(resolve(dbPath, dbSchemeFiles[0]));
|
||||
return {
|
||||
...contents,
|
||||
dbSchemeUri,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static async resolveDatabase(
|
||||
databasePath: string,
|
||||
): Promise<DatabaseContents> {
|
||||
const name = basename(databasePath);
|
||||
|
||||
// Look for dataset and source archive.
|
||||
const datasetUri = await findDataset(databasePath);
|
||||
const sourceArchiveUri = await findSourceArchive(databasePath);
|
||||
|
||||
return {
|
||||
kind: DatabaseKind.Database,
|
||||
name,
|
||||
datasetUri,
|
||||
sourceArchiveUri,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An error thrown when we cannot find a valid database in a putative
|
||||
* database directory.
|
||||
*/
|
||||
class InvalidDatabaseError extends Error {}
|
||||
|
||||
async function findDataset(parentDirectory: string): Promise<vscode.Uri> {
|
||||
/*
|
||||
* Look directly in the root
|
||||
*/
|
||||
let dbRelativePaths = await glob("db-*/", {
|
||||
cwd: parentDirectory,
|
||||
});
|
||||
|
||||
if (dbRelativePaths.length === 0) {
|
||||
/*
|
||||
* Check If they are in the old location
|
||||
*/
|
||||
dbRelativePaths = await glob("working/db-*/", {
|
||||
cwd: parentDirectory,
|
||||
});
|
||||
}
|
||||
if (dbRelativePaths.length === 0) {
|
||||
throw new InvalidDatabaseError(
|
||||
`'${parentDirectory}' does not contain a dataset directory.`,
|
||||
);
|
||||
}
|
||||
|
||||
const dbAbsolutePath = join(parentDirectory, dbRelativePaths[0]);
|
||||
if (dbRelativePaths.length > 1) {
|
||||
void showAndLogWarningMessage(
|
||||
`Found multiple dataset directories in database, using '${dbAbsolutePath}'.`,
|
||||
);
|
||||
}
|
||||
|
||||
return vscode.Uri.file(dbAbsolutePath);
|
||||
}
|
||||
|
||||
/** Gets the relative paths of all `.dbscheme` files in the given directory. */
|
||||
async function getDbSchemeFiles(dbDirectory: string): Promise<string[]> {
|
||||
return await glob("*.dbscheme", { cwd: dbDirectory });
|
||||
}
|
||||
|
||||
// exported for testing
|
||||
export async function findSourceArchive(
|
||||
databasePath: string,
|
||||
): Promise<vscode.Uri | undefined> {
|
||||
const relativePaths = ["src", "output/src_archive"];
|
||||
|
||||
for (const relativePath of relativePaths) {
|
||||
const basePath = join(databasePath, relativePath);
|
||||
const zipPath = `${basePath}.zip`;
|
||||
|
||||
// Prefer using a zip archive over a directory.
|
||||
if (await pathExists(zipPath)) {
|
||||
return encodeArchiveBasePath(zipPath);
|
||||
} else if (await pathExists(basePath)) {
|
||||
return vscode.Uri.file(basePath);
|
||||
}
|
||||
}
|
||||
|
||||
void showAndLogInformationMessage(
|
||||
`Could not find source archive for database '${databasePath}'. Assuming paths are absolute.`,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
11
extensions/ql-vscode/src/databases/local-databases/index.ts
Normal file
11
extensions/ql-vscode/src/databases/local-databases/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export {
|
||||
DatabaseContents,
|
||||
DatabaseContentsWithDbScheme,
|
||||
DatabaseKind,
|
||||
} from "./database-contents";
|
||||
export { DatabaseChangedEvent, DatabaseEventKind } from "./database-events";
|
||||
export { DatabaseItem } from "./database-item";
|
||||
export { DatabaseItemImpl } from "./database-item-impl";
|
||||
export { DatabaseManager } from "./database-manager";
|
||||
export { DatabaseResolver } from "./database-resolver";
|
||||
export { DatabaseOptions, FullDatabaseOptions } from "./database-options";
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
DatabaseItemImpl,
|
||||
DatabaseManager,
|
||||
DatabaseResolver,
|
||||
findSourceArchive,
|
||||
FullDatabaseOptions,
|
||||
} from "../../../src/databases/local-databases";
|
||||
import { Logger } from "../../../src/common";
|
||||
@@ -32,6 +31,7 @@ import {
|
||||
mockDbOptions,
|
||||
sourceLocationUri,
|
||||
} from "../../factories/databases/databases";
|
||||
import { findSourceArchive } from "../../../src/databases/local-databases/database-resolver";
|
||||
|
||||
describe("local databases", () => {
|
||||
let databaseManager: DatabaseManager;
|
||||
|
||||
Reference in New Issue
Block a user