diff --git a/extensions/ql-vscode/src/bqrs-cli-types.ts b/extensions/ql-vscode/src/bqrs-cli-types.ts index 397937ca0d8..9d45d133d5d 100644 --- a/extensions/ql-vscode/src/bqrs-cli-types.ts +++ b/extensions/ql-vscode/src/bqrs-cli-types.ts @@ -1,14 +1,34 @@ export const PAGE_SIZE = 1000; -export type ColumnKind = "f" | "i" | "s" | "b" | "d" | "e"; +/** + * The single-character codes used in the bqrs format for the the kind + * of a result column. This namespace is intentionally not an enum, see + * the "for the sake of extensibility" comment in messages.ts. + */ +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace ColumnKindCode { + export const FLOAT = "f"; + export const INTEGER = "i"; + export const STRING = "s"; + export const BOOLEAN = "b"; + export const DATE = "d"; + export const ENTITY = "e"; +} + +export type ColumnKind = + | typeof ColumnKindCode.FLOAT + | typeof ColumnKindCode.INTEGER + | typeof ColumnKindCode.STRING + | typeof ColumnKindCode.BOOLEAN + | typeof ColumnKindCode.DATE + | typeof ColumnKindCode.ENTITY; export interface Column { name?: string; kind: ColumnKind; } - export interface ResultSetSchema { name: string; rows: number; diff --git a/extensions/ql-vscode/src/cli.ts b/extensions/ql-vscode/src/cli.ts index f5d0bd4ded9..11a6126c5fd 100644 --- a/extensions/ql-vscode/src/cli.ts +++ b/extensions/ql-vscode/src/cli.ts @@ -10,10 +10,10 @@ import * as tk from 'tree-kill'; import * as util from 'util'; import { CancellationToken, Disposable } from 'vscode'; import { BQRSInfo, DecodedBqrsChunk } from "./bqrs-cli-types"; -import { DistributionProvider } from './distribution'; -import { assertNever } from './helpers-pure'; -import { QueryMetadata, SortDirection } from './interface-types'; -import { Logger, ProgressReporter } from './logging'; +import { DistributionProvider } from "./distribution"; +import { assertNever } from "./helpers-pure"; +import { QueryMetadata, SortDirection } from "./interface-types"; +import { Logger, ProgressReporter } from "./logging"; /** * The version of the SARIF format that we are using. @@ -614,6 +614,27 @@ export class CodeQLCliServer implements Disposable { "Resolving qlpack information", ); } + + /** + * Gets information about queries in a query suite. + * @param suite The suite to resolve. + * @param additionalPacks A list of directories to search for qlpacks before searching in `searchPath`. + * @param searchPath A list of directories to search for packs not found in `additionalPacks`. If undefined, + * the default CLI search path is used. + * @returns A list of query files found. + */ + resolveQueriesInSuite(suite: string, additionalPacks: string[], searchPath?: string[]): Promise { + const args = ['--additional-packs', additionalPacks.join(path.delimiter)]; + if (searchPath !== undefined) { + args.push('--search-path', path.join(...searchPath)); + } + args.push(suite); + return this.runJsonCodeQlCliCommand( + ['resolve', 'queries'], + args, + "Resolving queries", + ); + } } /** diff --git a/extensions/ql-vscode/src/config.ts b/extensions/ql-vscode/src/config.ts index a1f5bb84d36..9d93e198ee7 100644 --- a/extensions/ql-vscode/src/config.ts +++ b/extensions/ql-vscode/src/config.ts @@ -39,6 +39,17 @@ class Setting { const ROOT_SETTING = new Setting('codeQL'); +// Enable experimental features + +/** + * This setting is deliberately not in package.json so that it does + * not appear in the settings ui in vscode itself. If users want to + * enable experimental features, they can add + * "codeQl.experimentalFeatures" directly in their vscode settings + * json file. + */ +export const EXPERIMENTAL_FEATURES_SETTING = new Setting('experimentalFeatures', ROOT_SETTING); + // Distribution configuration const DISTRIBUTION_SETTING = new Setting('cli', ROOT_SETTING); diff --git a/extensions/ql-vscode/src/databases.ts b/extensions/ql-vscode/src/databases.ts index 2b3328a0108..e77716d63ac 100644 --- a/extensions/ql-vscode/src/databases.ts +++ b/extensions/ql-vscode/src/databases.ts @@ -631,6 +631,11 @@ export class DatabaseManager extends DisposableObject { return this._databaseItems.find(item => item.databaseUri.toString(true) === uriString); } + public findDatabaseItemBySourceArchive(uri: vscode.Uri): DatabaseItem | undefined { + const uriString = uri.toString(true); + return this._databaseItems.find(item => item.sourceArchive && item.sourceArchive.toString(true) === uriString); + } + private async addDatabaseItem(item: DatabaseItemImpl) { this._databaseItems.push(item); this.updatePersistedDatabaseList(); diff --git a/extensions/ql-vscode/src/definitions.ts b/extensions/ql-vscode/src/definitions.ts new file mode 100644 index 00000000000..26221efd9bd --- /dev/null +++ b/extensions/ql-vscode/src/definitions.ts @@ -0,0 +1,203 @@ +import * as fs from 'fs-extra'; +import * as yaml from 'js-yaml'; +import * as tmp from 'tmp'; +import * as vscode from "vscode"; +import { decodeSourceArchiveUri, zipArchiveScheme } from "./archive-filesystem-provider"; +import { ColumnKindCode, EntityValue, getResultSetSchema, LineColumnLocation, UrlValue } from "./bqrs-cli-types"; +import { CodeQLCliServer } from "./cli"; +import { DatabaseItem, DatabaseManager } from "./databases"; +import * as helpers from './helpers'; +import { CachedOperation } from './helpers'; +import * as messages from "./messages"; +import { QueryServerClient } from "./queryserver-client"; +import { compileAndRunQueryAgainstDatabase, QueryWithResults } from "./run-queries"; + +/** + * Run templated CodeQL queries to find definitions and references in + * source-language files. We may eventually want to find a way to + * generalize this to other custom queries, e.g. showing dataflow to + * or from a selected identifier. + */ + +const TEMPLATE_NAME = "selectedSourceFile"; +const SELECT_QUERY_NAME = "#select"; + +enum KeyType { + DefinitionQuery = 'DefinitionQuery', + ReferenceQuery = 'ReferenceQuery', +} + +function tagOfKeyType(keyType: KeyType): string { + switch (keyType) { + case KeyType.DefinitionQuery: return "ide-contextual-queries/local-definitions"; + case KeyType.ReferenceQuery: return "ide-contextual-queries/local-references"; + } +} + +async function resolveQueries(cli: CodeQLCliServer, qlpack: string, keyType: KeyType): Promise { + const suiteFile = tmp.fileSync({ postfix: '.qls' }).name; + const suiteYaml = { qlpack, include: { kind: 'definitions', 'tags contain': tagOfKeyType(keyType) } }; + await fs.writeFile(suiteFile, yaml.safeDump(suiteYaml), 'utf8'); + + const queries = await cli.resolveQueriesInSuite(suiteFile, helpers.getOnDiskWorkspaceFolders()); + if (queries.length === 0) { + throw new Error("Couldn't find any queries for qlpack"); + } + return queries; +} + +async function qlpackOfDatabase(cli: CodeQLCliServer, db: DatabaseItem): Promise { + if (db.contents === undefined) + return undefined; + const datasetPath = db.contents.datasetUri.fsPath; + const { qlpack } = await helpers.resolveDatasetFolder(cli, datasetPath); + return qlpack; +} + +interface FullLocationLink extends vscode.LocationLink { + originUri: vscode.Uri; +} + +export class TemplateQueryDefinitionProvider implements vscode.DefinitionProvider { + private cache: CachedOperation; + + constructor( + private cli: CodeQLCliServer, + private qs: QueryServerClient, + private dbm: DatabaseManager, + ) { + this.cache = new CachedOperation(this.getDefinitions.bind(this)); + } + + async getDefinitions(uriString: string): Promise { + return getLinksForUriString(this.cli, this.qs, this.dbm, uriString, (src, _dest) => src === uriString); + } + + async provideDefinition(document: vscode.TextDocument, position: vscode.Position, _token: vscode.CancellationToken): Promise { + const fileLinks = await this.cache.get(document.uri.toString()); + const locLinks: vscode.LocationLink[] = []; + for (const link of fileLinks) { + if (link.originSelectionRange!.contains(position)) { + locLinks.push(link); + } + } + return locLinks; + } +} + +export class TemplateQueryReferenceProvider implements vscode.ReferenceProvider { + private cache: CachedOperation; + + constructor( + private cli: CodeQLCliServer, + private qs: QueryServerClient, + private dbm: DatabaseManager, + ) { + this.cache = new CachedOperation(this.getReferences.bind(this)); + } + + async getReferences(uriString: string): Promise { + return getLinksForUriString(this.cli, this.qs, this.dbm, uriString, (_src, dest) => dest === uriString); + } + + async provideReferences(document: vscode.TextDocument, position: vscode.Position, _context: vscode.ReferenceContext, _token: vscode.CancellationToken): Promise { + const fileLinks = await this.cache.get(document.uri.toString()); + const locLinks: vscode.Location[] = []; + for (const link of fileLinks) { + if (link.targetRange!.contains(position)) { + locLinks.push({ range: link.originSelectionRange!, uri: link.originUri }); + } + } + return locLinks; + } +} + +interface FileRange { + file: vscode.Uri; + range: vscode.Range; +} + +async function getLinksFromResults(results: QueryWithResults, cli: CodeQLCliServer, db: DatabaseItem, filter: (srcFile: string, destFile: string) => boolean): Promise { + const localLinks: FullLocationLink[] = []; + const bqrsPath = results.query.resultsPaths.resultsPath; + const info = await cli.bqrsInfo(bqrsPath); + const selectInfo = getResultSetSchema(SELECT_QUERY_NAME, info); + if (selectInfo && selectInfo.columns.length == 3 + && selectInfo.columns[0].kind == ColumnKindCode.ENTITY + && selectInfo.columns[1].kind == ColumnKindCode.ENTITY + && selectInfo.columns[2].kind == ColumnKindCode.STRING) { + // TODO: Page this + const allTuples = await cli.bqrsDecode(bqrsPath, SELECT_QUERY_NAME); + for (const tuple of allTuples.tuples) { + const src = tuple[0] as EntityValue; + const dest = tuple[1] as EntityValue; + const srcFile = src.url && fileRangeFromURI(src.url, db); + const destFile = dest.url && fileRangeFromURI(dest.url, db); + if (srcFile && destFile && filter(srcFile.file.toString(), destFile.file.toString())) { + localLinks.push({ targetRange: destFile.range, targetUri: destFile.file, originSelectionRange: srcFile.range, originUri: srcFile.file }); + } + } + } + return localLinks; +} + +async function getLinksForUriString( + cli: CodeQLCliServer, + qs: QueryServerClient, + dbm: DatabaseManager, + uriString: string, + filter: (src: string, dest: string) => boolean +) { + const uri = decodeSourceArchiveUri(vscode.Uri.parse(uriString)); + const sourceArchiveUri = vscode.Uri.file(uri.sourceArchiveZipPath).with({ scheme: zipArchiveScheme }); + + const db = dbm.findDatabaseItemBySourceArchive(sourceArchiveUri); + if (db) { + const qlpack = await qlpackOfDatabase(cli, db); + if (qlpack === undefined) { + throw new Error("Can't infer qlpack from database source archive"); + } + const links: FullLocationLink[] = [] + for (const query of await resolveQueries(cli, qlpack, KeyType.ReferenceQuery)) { + const templates: messages.TemplateDefinitions = { + [TEMPLATE_NAME]: { + values: { + tuples: [[{ + stringValue: uri.pathWithinSourceArchive + }]] + } + } + }; + const results = await compileAndRunQueryAgainstDatabase(cli, qs, db, false, vscode.Uri.file(query), templates); + if (results.result.resultType == messages.QueryResultType.SUCCESS) { + links.push(...await getLinksFromResults(results, cli, db, filter)); + } + } + return links; + } else { + return []; + } +} + +function fileRangeFromURI(uri: UrlValue, db: DatabaseItem): FileRange | undefined { + if (typeof uri === "string") { + return undefined; + } else if ('startOffset' in uri) { + return undefined; + } else { + const loc = uri as LineColumnLocation; + const range = new vscode.Range(Math.max(0, loc.startLine - 1), + Math.max(0, loc.startColumn - 1), + Math.max(0, loc.endLine - 1), + Math.max(0, loc.endColumn)); + try { + const parsed = vscode.Uri.parse(uri.uri, true); + if (parsed.scheme === "file") { + return { file: db.resolveSourceFile(parsed.fsPath), range }; + } + return undefined; + } catch (e) { + return undefined; + } + } +} diff --git a/extensions/ql-vscode/src/discovery.ts b/extensions/ql-vscode/src/discovery.ts index 40fb1f46066..3f4dff5953a 100644 --- a/extensions/ql-vscode/src/discovery.ts +++ b/extensions/ql-vscode/src/discovery.ts @@ -84,4 +84,4 @@ export abstract class Discovery extends DisposableObject { * @param results The discovery results returned by the `discover` function. */ protected abstract update(results: T): void; -} \ No newline at end of file +} diff --git a/extensions/ql-vscode/src/extension.ts b/extensions/ql-vscode/src/extension.ts index 156fbd5b857..7fcdf5ef3ae 100644 --- a/extensions/ql-vscode/src/extension.ts +++ b/extensions/ql-vscode/src/extension.ts @@ -1,25 +1,23 @@ -import { commands, Disposable, ExtensionContext, extensions, ProgressLocation, ProgressOptions, window as Window, Uri } from 'vscode'; +import { commands, Disposable, ExtensionContext, extensions, languages, ProgressLocation, ProgressOptions, Uri, window as Window } from 'vscode'; import { LanguageClient } from 'vscode-languageclient'; +import { testExplorerExtensionId, TestHub } from 'vscode-test-adapter-api'; import * as archiveFilesystemProvider from './archive-filesystem-provider'; -import { DistributionConfigListener, QueryServerConfigListener, QueryHistoryConfigListener } from './config'; +import { CodeQLCliServer } from './cli'; +import { DistributionConfigListener, QueryHistoryConfigListener, QueryServerConfigListener, EXPERIMENTAL_FEATURES_SETTING } from './config'; import { DatabaseManager } from './databases'; import { DatabaseUI } from './databases-ui'; -import { - DistributionUpdateCheckResultKind, DistributionManager, FindDistributionResult, FindDistributionResultKind, GithubApiError, - DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT, GithubRateLimitedError -} from './distribution'; +import { TemplateQueryDefinitionProvider, TemplateQueryReferenceProvider } from './definitions'; +import { DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT, DistributionManager, DistributionUpdateCheckResultKind, FindDistributionResult, FindDistributionResultKind, GithubApiError, GithubRateLimitedError } from './distribution'; import * as helpers from './helpers'; +import { assertNever } from './helpers-pure'; import { spawnIdeServer } from './ide-server'; import { InterfaceManager, WebviewReveal } from './interface'; import { ideServerLogger, logger, queryServerLogger } from './logging'; -import { compileAndRunQueryAgainstDatabase, tmpDirDisposal, UserCancellationException } from './run-queries'; -import { CompletedQuery } from './query-results'; import { QueryHistoryManager } from './query-history'; +import { CompletedQuery } from './query-results'; import * as qsClient from './queryserver-client'; -import { CodeQLCliServer } from './cli'; -import { assertNever } from './helpers-pure'; import { displayQuickQuery } from './quick-query'; -import { TestHub, testExplorerExtensionId } from 'vscode-test-adapter-api'; +import { compileAndRunQueryAgainstDatabase, tmpDirDisposal, UserCancellationException } from './run-queries'; import { QLTestAdapterFactory } from './test-adapter'; import { TestUIService } from './test-ui'; @@ -337,6 +335,17 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu })); ctx.subscriptions.push(client.start()); + + if (EXPERIMENTAL_FEATURES_SETTING.getValue()) { + languages.registerDefinitionProvider( + { scheme: archiveFilesystemProvider.zipArchiveScheme }, + new TemplateQueryDefinitionProvider(cliServer, qs, dbm) + ); + languages.registerReferenceProvider( + { scheme: archiveFilesystemProvider.zipArchiveScheme }, + new TemplateQueryReferenceProvider(cliServer, qs, dbm) + ); + } } function initializeLogging(ctx: ExtensionContext): void { diff --git a/extensions/ql-vscode/src/helpers.ts b/extensions/ql-vscode/src/helpers.ts index c5bcaa8e917..96716904cb1 100644 --- a/extensions/ql-vscode/src/helpers.ts +++ b/extensions/ql-vscode/src/helpers.ts @@ -1,5 +1,9 @@ +import * as fs from 'fs-extra'; +import * as glob from 'glob-promise'; +import * as yaml from 'js-yaml'; import * as path from 'path'; import { CancellationToken, ExtensionContext, ProgressOptions, window as Window, workspace } from 'vscode'; +import { CodeQLCliServer } from './cli'; import { logger } from './logging'; import { QueryInfo } from './run-queries'; @@ -244,3 +248,110 @@ function createRateLimitedResult(): RateLimitedResult { kind: InvocationRateLimiterResultKind.RateLimited }; } + + +export type DatasetFolderInfo = { + dbscheme: string; + qlpack: string; +} + +export async function getQlPackForDbscheme(cliServer: CodeQLCliServer, dbschemePath: string): Promise { + const qlpacks = await cliServer.resolveQlpacks(getOnDiskWorkspaceFolders()); + const packs: { packDir: string | undefined; packName: string }[] = + Object.entries(qlpacks).map(([packName, dirs]) => { + if (dirs.length < 1) { + logger.log(`In getQlPackFor ${dbschemePath}, qlpack ${packName} has no directories`); + return { packName, packDir: undefined }; + } + if (dirs.length > 1) { + logger.log(`In getQlPackFor ${dbschemePath}, qlpack ${packName} has more than one directory; arbitrarily choosing the first`); + } + return { + packName, + packDir: dirs[0] + } + }); + for (const { packDir, packName } of packs) { + if (packDir !== undefined) { + const qlpack = yaml.safeLoad(await fs.readFile(path.join(packDir, 'qlpack.yml'), 'utf8')); + if (qlpack.dbscheme !== undefined && path.basename(qlpack.dbscheme) === path.basename(dbschemePath)) { + return packName; + } + } + } + throw new Error(`Could not find qlpack file for dbscheme ${dbschemePath}`); +} + +export async function resolveDatasetFolder(cliServer: CodeQLCliServer, datasetFolder: string): Promise { + const dbschemes = await glob(path.join(datasetFolder, '*.dbscheme')) + + if (dbschemes.length < 1) { + throw new Error(`Can't find dbscheme for current database in ${datasetFolder}`); + } + + dbschemes.sort(); + const dbscheme = dbschemes[0]; + if (dbschemes.length > 1) { + Window.showErrorMessage(`Found multiple dbschemes in ${datasetFolder} during quick query; arbitrarily choosing the first, ${dbscheme}, to decide what library to use.`); + } + + const qlpack = await getQlPackForDbscheme(cliServer, dbscheme); + return { dbscheme, qlpack }; +} + +/** + * A cached mapping from strings to value of type U. + */ +export class CachedOperation { + private readonly operation: (t: string) => Promise; + private readonly cached: Map; + private readonly lru: string[]; + private readonly inProgressCallbacks: Map void, (reason?: any) => void][]>; + + constructor(operation: (t: string) => Promise, private cacheSize = 100) { + this.operation = operation; + this.lru = []; + this.inProgressCallbacks = new Map void, (reason?: any) => void][]>(); + this.cached = new Map(); + } + + async get(t: string): Promise { + // Try and retrieve from the cache + const fromCache = this.cached.get(t); + if (fromCache !== undefined) { + // Move to end of lru list + this.lru.push(this.lru.splice(this.lru.findIndex(v => v === t), 1)[0]) + return fromCache; + } + // Otherwise check if in progress + const inProgressCallback = this.inProgressCallbacks.get(t); + if (inProgressCallback !== undefined) { + // If so wait for it to resolve + return await new Promise((resolve, reject) => { + inProgressCallback.push([resolve, reject]); + }); + } + + // Otherwise compute the new value, but leave a callback to allow sharing work + const callbacks: [(u: U) => void, (reason?: any) => void][] = []; + this.inProgressCallbacks.set(t, callbacks); + try { + const result = await this.operation(t); + callbacks.forEach(f => f[0](result)); + this.inProgressCallbacks.delete(t); + if (this.lru.length > this.cacheSize) { + const toRemove = this.lru.shift()!; + this.cached.delete(toRemove); + } + this.lru.push(t); + this.cached.set(t, result); + return result; + } catch (e) { + // Rethrow error on all callbacks + callbacks.forEach(f => f[1](e)); + throw e; + } finally { + this.inProgressCallbacks.delete(t); + } + } +} diff --git a/extensions/ql-vscode/src/logging.ts b/extensions/ql-vscode/src/logging.ts index 9b925bb4571..d8eeb422338 100644 --- a/extensions/ql-vscode/src/logging.ts +++ b/extensions/ql-vscode/src/logging.ts @@ -60,7 +60,7 @@ export class OutputChannelLogger extends DisposableObject implements Logger { * function if you don't need to guarantee that the log writing is complete before * continuing. */ - async log(message: string, options = { } as LogOptions): Promise { + async log(message: string, options = {} as LogOptions): Promise { if (options.trailingNewline === undefined) { options.trailingNewline = true; } @@ -116,7 +116,7 @@ class AdditionalLogLocation extends Disposable { super(() => { /**/ }); } - async log(message: string, options = { } as LogOptions): Promise { + async log(message: string, options = {} as LogOptions): Promise { if (options.trailingNewline === undefined) { options.trailingNewline = true; } diff --git a/extensions/ql-vscode/src/quick-query.ts b/extensions/ql-vscode/src/quick-query.ts index aa0b15313c9..204d19c4bb7 100644 --- a/extensions/ql-vscode/src/quick-query.ts +++ b/extensions/ql-vscode/src/quick-query.ts @@ -1,5 +1,4 @@ import * as fs from 'fs-extra'; -import * as glob from 'glob-promise'; import * as yaml from 'js-yaml'; import * as path from 'path'; import { ExtensionContext, window as Window, workspace, Uri } from 'vscode'; @@ -18,33 +17,6 @@ export function isQuickQueryPath(queryPath: string): boolean { return path.basename(queryPath) === QUICK_QUERY_QUERY_NAME; } -async function getQlPackFor(cliServer: CodeQLCliServer, dbschemePath: string): Promise { - const qlpacks = await cliServer.resolveQlpacks(helpers.getOnDiskWorkspaceFolders()); - const packs: { packDir: string | undefined; packName: string }[] = - Object.entries(qlpacks).map(([packName, dirs]) => { - if (dirs.length < 1) { - logger.log(`In getQlPackFor ${dbschemePath}, qlpack ${packName} has no directories`); - return { packName, packDir: undefined }; - } - if (dirs.length > 1) { - logger.log(`In getQlPackFor ${dbschemePath}, qlpack ${packName} has more than one directory; arbitrarily choosing the first`); - } - return { - packName, - packDir: dirs[0] - } - }); - for (const { packDir, packName } of packs) { - if (packDir !== undefined) { - const qlpack = yaml.safeLoad(await fs.readFile(path.join(packDir, 'qlpack.yml'), 'utf8')); - if (qlpack.dbscheme !== undefined && path.basename(qlpack.dbscheme) === path.basename(dbschemePath)) { - return packName; - } - } - } - throw new Error(`Could not find qlpack file for dbscheme ${dbschemePath}`); -} - /** * `getBaseText` heuristically returns an appropriate import statement * prelude based on the filename of the dbscheme file given. TODO: add @@ -128,19 +100,7 @@ export async function displayQuickQuery(ctx: ExtensionContext, cliServer: CodeQL } const datasetFolder = await dbItem.getDatasetFolder(cliServer); - const dbschemes = await glob(path.join(datasetFolder, '*.dbscheme')) - - if (dbschemes.length < 1) { - throw new Error(`Can't find dbscheme for current database in ${datasetFolder}`); - } - - dbschemes.sort(); - const dbscheme = dbschemes[0]; - if (dbschemes.length > 1) { - Window.showErrorMessage(`Found multiple dbschemes in ${datasetFolder} during quick query; arbitrarily choosing the first, ${dbscheme}, to decide what library to use.`); - } - - const qlpack = await getQlPackFor(cliServer, dbscheme); + const { qlpack, dbscheme } = await helpers.resolveDatasetFolder(cliServer, datasetFolder); const quickQueryQlpackYaml: any = { name: "quick-query", version: "1.0.0",