Merge pull request #2244 from github/dbartol/debug-context

Refactor local query evaluation to prepare for debug adapter
This commit is contained in:
Dave Bartolomeo
2023-03-30 10:23:11 -04:00
committed by GitHub
23 changed files with 1279 additions and 1091 deletions

View File

@@ -5,31 +5,18 @@ import {
TemplatePrintAstProvider,
TemplatePrintCfgProvider,
} from "./contextual/templateProvider";
import { compileAndRunQuery } from "./local-queries";
import { QueryRunner } from "./queryRunner";
import { QueryHistoryManager } from "./query-history/query-history-manager";
import { DatabaseUI } from "./local-databases-ui";
import { ResultsView } from "./interface";
import { AstCfgCommands } from "./common/commands";
import { LocalQueries } from "./local-queries";
type AstCfgOptions = {
queryRunner: QueryRunner;
queryHistoryManager: QueryHistoryManager;
databaseUI: DatabaseUI;
localQueryResultsView: ResultsView;
queryStorageDir: string;
localQueries: LocalQueries;
astViewer: AstViewer;
astTemplateProvider: TemplatePrintAstProvider;
cfgTemplateProvider: TemplatePrintCfgProvider;
};
export function getAstCfgCommands({
queryRunner,
queryHistoryManager,
databaseUI,
localQueryResultsView,
queryStorageDir,
localQueries,
astViewer,
astTemplateProvider,
cfgTemplateProvider,
@@ -59,12 +46,7 @@ export function getAstCfgCommands({
window.activeTextEditor?.document,
);
if (res) {
await compileAndRunQuery(
queryRunner,
queryHistoryManager,
databaseUI,
localQueryResultsView,
queryStorageDir,
await localQueries.compileAndRunQuery(
false,
res[0],
progress,

View File

@@ -3,7 +3,8 @@ export interface LogOptions {
trailingNewline?: boolean;
}
export interface Logger {
/** Minimal logger interface. */
export interface BaseLogger {
/**
* Writes the given log message, optionally followed by a newline.
* This function is asynchronous and will only resolve once the message is written
@@ -15,7 +16,10 @@ export interface Logger {
* @param options Optional settings.
*/
log(message: string, options?: LogOptions): Promise<void>;
}
/** Full logger interface, including a function to show the log in the UI. */
export interface Logger extends BaseLogger {
/**
* Reveal the logger channel in the UI.
*

View File

@@ -4,7 +4,7 @@ import { DatabaseItem } from "../local-databases";
import { ChildAstItem, AstItem } from "../astViewer";
import fileRangeFromURI from "./fileRangeFromURI";
import { Uri } from "vscode";
import { QueryWithResults } from "../run-queries-shared";
import { QueryOutputDir } from "../run-queries-shared";
/**
* A class that wraps a tree of QL results from a query that
@@ -14,12 +14,12 @@ export default class AstBuilder {
private roots: AstItem[] | undefined;
private bqrsPath: string;
constructor(
queryResults: QueryWithResults,
outputDir: QueryOutputDir,
private cli: CodeQLCliServer,
public db: DatabaseItem,
public fileName: Uri,
) {
this.bqrsPath = queryResults.query.resultsPaths.resultsPath;
this.bqrsPath = outputDir.bqrsPath;
}
async getRoots(): Promise<AstItem[]> {

View File

@@ -19,8 +19,9 @@ import {
runContextualQuery,
} from "./queryResolver";
import { CancellationToken, LocationLink, Uri } from "vscode";
import { QueryWithResults } from "../run-queries-shared";
import { QueryOutputDir } from "../run-queries-shared";
import { QueryRunner } from "../queryRunner";
import { QueryResultType } from "../pure/new-messages";
export const SELECT_QUERY_NAME = "#select";
export const TEMPLATE_NAME = "selectedSourceFile";
@@ -78,21 +79,23 @@ export async function getLocationsForUriString(
token,
templates,
);
if (results.successful) {
links.push(...(await getLinksFromResults(results, cli, db, filter)));
if (results.resultType === QueryResultType.SUCCESS) {
links.push(
...(await getLinksFromResults(results.outputDir, cli, db, filter)),
);
}
}
return links;
}
async function getLinksFromResults(
results: QueryWithResults,
outputDir: QueryOutputDir,
cli: CodeQLCliServer,
db: DatabaseItem,
filter: (srcFile: string, destFile: string) => boolean,
): Promise<FullLocationLink[]> {
const localLinks: FullLocationLink[] = [];
const bqrsPath = results.query.resultsPaths.resultsPath;
const bqrsPath = outputDir.bqrsPath;
const info = await cli.bqrsInfo(bqrsPath);
const selectInfo = getResultSetSchema(SELECT_QUERY_NAME, info);
if (isValidSelect(selectInfo)) {

View File

@@ -13,11 +13,10 @@ import {
import { KeyType, kindOfKeyType, nameOfKeyType, tagOfKeyType } from "./keyType";
import { CodeQLCliServer } from "../cli";
import { DatabaseItem } from "../local-databases";
import { extLogger } from "../common";
import { createInitialQueryInfo } from "../run-queries-shared";
import { CancellationToken, Uri } from "vscode";
import { extLogger, TeeLogger } from "../common";
import { CancellationToken } from "vscode";
import { ProgressCallback } from "../progress";
import { QueryRunner } from "../queryRunner";
import { CoreCompletedQuery, QueryRunner } from "../queryRunner";
import { redactableError } from "../pure/errors";
import { QLPACK_FILENAMES } from "../pure/ql";
@@ -169,32 +168,30 @@ export async function runContextualQuery(
progress: ProgressCallback,
token: CancellationToken,
templates: Record<string, string>,
) {
): Promise<CoreCompletedQuery> {
const { packPath, createdTempLockFile } = await resolveContextualQuery(
cli,
query,
);
const initialInfo = await createInitialQueryInfo(
Uri.file(query),
{
name: db.name,
databaseUri: db.databaseUri.toString(),
},
const queryRun = qs.createQueryRun(
db.databaseUri.fsPath,
{ queryPath: query, quickEvalPosition: undefined },
false,
getOnDiskWorkspaceFolders(),
queryStorageDir,
undefined,
templates,
);
void extLogger.log(
`Running contextual query ${query}; results will be stored in ${queryStorageDir}`,
`Running contextual query ${query}; results will be stored in ${queryRun.outputDir.querySaveDir}`,
);
const queryResult = await qs.compileAndRunQueryAgainstDatabase(
db,
initialInfo,
queryStorageDir,
const results = await queryRun.evaluate(
progress,
token,
templates,
new TeeLogger(qs.logger, queryRun.outputDir.logPath),
);
if (createdTempLockFile) {
await removeTemporaryLockFile(packPath);
}
return queryResult;
return results;
}

View File

@@ -32,8 +32,7 @@ import {
runContextualQuery,
} from "./queryResolver";
import { isCanary, NO_CACHE_AST_VIEWER } from "../config";
import { QueryWithResults } from "../run-queries-shared";
import { QueryRunner } from "../queryRunner";
import { CoreCompletedQuery, QueryRunner } from "../queryRunner";
/**
* Runs templated CodeQL queries to find definitions in
@@ -155,17 +154,12 @@ export class TemplateQueryReferenceProvider implements ReferenceProvider {
}
}
type QueryWithDb = {
query: QueryWithResults;
dbUri: Uri;
};
/**
* Run templated CodeQL queries to produce AST information for
* source-language files.
*/
export class TemplatePrintAstProvider {
private cache: CachedOperation<QueryWithDb>;
private cache: CachedOperation<CoreCompletedQuery>;
constructor(
private cli: CodeQLCliServer,
@@ -173,7 +167,9 @@ export class TemplatePrintAstProvider {
private dbm: DatabaseManager,
private queryStorageDir: string,
) {
this.cache = new CachedOperation<QueryWithDb>(this.getAst.bind(this));
this.cache = new CachedOperation<CoreCompletedQuery>(
this.getAst.bind(this),
);
}
async provideAst(
@@ -186,14 +182,14 @@ export class TemplatePrintAstProvider {
"Cannot view the AST. Please select a valid source file inside a CodeQL database.",
);
}
const { query, dbUri } = this.shouldCache()
const completedQuery = this.shouldCache()
? await this.cache.get(fileUri.toString(), progress, token)
: await this.getAst(fileUri.toString(), progress, token);
return new AstBuilder(
query,
completedQuery.outputDir,
this.cli,
this.dbm.findDatabaseItem(dbUri)!,
this.dbm.findDatabaseItem(Uri.file(completedQuery.dbPath))!,
fileUri,
);
}
@@ -206,7 +202,7 @@ export class TemplatePrintAstProvider {
uriString: string,
progress: ProgressCallback,
token: CancellationToken,
): Promise<QueryWithDb> {
): Promise<CoreCompletedQuery> {
const uri = Uri.parse(uriString, true);
if (uri.scheme !== zipArchiveScheme) {
throw new Error(
@@ -242,7 +238,7 @@ export class TemplatePrintAstProvider {
[TEMPLATE_NAME]: zippedArchive.pathWithinSourceArchive,
};
const queryResult = await runContextualQuery(
const results = await runContextualQuery(
query,
db,
this.queryStorageDir,
@@ -252,10 +248,7 @@ export class TemplatePrintAstProvider {
token,
templates,
);
return {
query: queryResult,
dbUri: db.databaseUri,
};
return results;
}
}

View File

@@ -115,10 +115,7 @@ import {
QueryServerCommands,
TestUICommands,
} from "./common/commands";
import {
getLocalQueryCommands,
showResultsForCompletedQuery,
} from "./local-queries";
import { LocalQueries } from "./local-queries";
import { getAstCfgCommands } from "./ast-cfg-commands";
import { getQueryEditorCommands } from "./query-editor";
import { App } from "./common/app";
@@ -260,6 +257,7 @@ export interface CodeQLExtensionInterface {
readonly distributionManager: DistributionManager;
readonly databaseManager: DatabaseManager;
readonly databaseUI: DatabaseUI;
readonly localQueries: LocalQueries;
readonly variantAnalysisManager: VariantAnalysisManager;
readonly dispose: () => void;
}
@@ -716,12 +714,6 @@ async function activateWithInstalledDistribution(
void extLogger.log("Initializing query history manager.");
const queryHistoryConfigurationListener = new QueryHistoryConfigListener();
ctx.subscriptions.push(queryHistoryConfigurationListener);
const showResults = async (item: CompletedLocalQueryInfo) =>
showResultsForCompletedQuery(
localQueryResultsView,
item,
WebviewReveal.Forced,
);
const queryStorageDir = join(ctx.globalStorageUri.fsPath, "queries");
await ensureDir(queryStorageDir);
@@ -795,8 +787,10 @@ async function activateWithInstalledDistribution(
ctx,
queryHistoryConfigurationListener,
labelProvider,
async (from: CompletedLocalQueryInfo, to: CompletedLocalQueryInfo) =>
showResultsForComparison(compareView, from, to),
async (
from: CompletedLocalQueryInfo,
to: CompletedLocalQueryInfo,
): Promise<void> => showResultsForComparison(compareView, from, to),
);
ctx.subscriptions.push(qhm);
@@ -817,7 +811,8 @@ async function activateWithInstalledDistribution(
cliServer,
queryServerLogger,
labelProvider,
showResults,
async (item: CompletedLocalQueryInfo) =>
localQueries.showResultsForCompletedQuery(item, WebviewReveal.Forced),
);
ctx.subscriptions.push(compareView);
@@ -853,6 +848,18 @@ async function activateWithInstalledDistribution(
true,
);
const localQueries = new LocalQueries(
app,
qs,
qhm,
dbm,
cliServer,
databaseUI,
localQueryResultsView,
queryStorageDir,
);
ctx.subscriptions.push(localQueries);
void extLogger.log("Initializing QLTest interface.");
const testExplorerExtension = extensions.getExtension<TestHub>(
testExplorerExtensionId,
@@ -906,11 +913,7 @@ async function activateWithInstalledDistribution(
...databaseUI.getCommands(),
...dbModule.getCommands(),
...getAstCfgCommands({
queryRunner: qs,
queryHistoryManager: qhm,
databaseUI,
localQueryResultsView,
queryStorageDir,
localQueries,
astViewer,
astTemplateProvider,
cfgTemplateProvider,
@@ -930,16 +933,7 @@ async function activateWithInstalledDistribution(
}
const queryServerCommands: QueryServerCommands = {
...getLocalQueryCommands({
app,
queryRunner: qs,
queryHistoryManager: qhm,
databaseManager: dbm,
cliServer,
databaseUI,
localQueryResultsView,
queryStorageDir,
}),
...localQueries.getCommands(),
};
for (const [commandName, command] of Object.entries(queryServerCommands)) {
@@ -989,6 +983,7 @@ async function activateWithInstalledDistribution(
return {
ctx,
cliServer,
localQueries,
qs,
distributionManager,
databaseManager: dbm,

View File

@@ -754,7 +754,7 @@ export async function tryGetQueryMetadata(
* Creates a file in the query directory that indicates when this query was created.
* This is important for keeping track of when queries should be removed.
*
* @param queryPath The directory that will containt all files relevant to a query result.
* @param queryPath The directory that will contain all files relevant to a query result.
* It does not need to exist.
*/
export async function createTimestampFile(storagePath: string) {

View File

@@ -1,18 +1,19 @@
import { CancellationToken } from "vscode";
import { CodeQLCliServer } from "../cli";
import { ProgressCallback } from "../progress";
import { Logger } from "../common";
import { DatabaseItem } from "../local-databases";
import {
Dataset,
deregisterDatabases,
registerDatabases,
} from "../pure/legacy-messages";
import { InitialQueryInfo, LocalQueryInfo } from "../query-results";
import { QueryRunner } from "../queryRunner";
import { QueryWithResults } from "../run-queries-shared";
import { CoreQueryResults, CoreQueryTarget, QueryRunner } from "../queryRunner";
import { QueryOutputDir } from "../run-queries-shared";
import { QueryServerClient } from "./queryserver-client";
import {
clearCacheInDatabase,
compileAndRunQueryAgainstDatabase,
compileAndRunQueryAgainstDatabaseCore,
} from "./run-queries";
import { upgradeDatabaseExplicit } from "./upgrades";
@@ -21,10 +22,18 @@ export class LegacyQueryRunner extends QueryRunner {
super();
}
get cliServer() {
get cliServer(): CodeQLCliServer {
return this.qs.cliServer;
}
get customLogDirectory(): string | undefined {
return undefined;
}
get logger(): Logger {
return this.qs.logger;
}
async restartQueryServer(
progress: ProgressCallback,
token: CancellationToken,
@@ -47,25 +56,29 @@ export class LegacyQueryRunner extends QueryRunner {
): Promise<void> {
await clearCacheInDatabase(this.qs, dbItem, progress, token);
}
async compileAndRunQueryAgainstDatabase(
dbItem: DatabaseItem,
initialInfo: InitialQueryInfo,
queryStorageDir: string,
public async compileAndRunQueryAgainstDatabaseCore(
dbPath: string,
query: CoreQueryTarget,
additionalPacks: string[],
generateEvalLog: boolean,
outputDir: QueryOutputDir,
progress: ProgressCallback,
token: CancellationToken,
templates?: Record<string, string>,
queryInfo?: LocalQueryInfo,
): Promise<QueryWithResults> {
return await compileAndRunQueryAgainstDatabase(
this.qs.cliServer,
templates: Record<string, string> | undefined,
logger: Logger,
): Promise<CoreQueryResults> {
return await compileAndRunQueryAgainstDatabaseCore(
this.qs,
dbItem,
initialInfo,
queryStorageDir,
dbPath,
query,
generateEvalLog,
additionalPacks,
outputDir,
progress,
token,
templates,
queryInfo,
logger,
);
}

View File

@@ -1,28 +1,164 @@
import * as tmp from "tmp-promise";
import { basename, join } from "path";
import { basename } from "path";
import { CancellationToken, Uri } from "vscode";
import { LSPErrorCodes, ResponseError } from "vscode-languageclient";
import * as cli from "../cli";
import { DatabaseItem } from "../local-databases";
import {
getOnDiskWorkspaceFolders,
showAndLogErrorMessage,
showAndLogExceptionWithTelemetry,
showAndLogWarningMessage,
tryGetQueryMetadata,
upgradesTmpDir,
} from "../helpers";
DatabaseContentsWithDbScheme,
DatabaseItem,
DatabaseResolver,
} from "../local-databases";
import { showAndLogExceptionWithTelemetry, upgradesTmpDir } from "../helpers";
import { ProgressCallback } from "../progress";
import { QueryMetadata } from "../pure/interface-types";
import { extLogger, Logger, TeeLogger } from "../common";
import { extLogger, Logger } from "../common";
import * as messages from "../pure/legacy-messages";
import { InitialQueryInfo, LocalQueryInfo } from "../query-results";
import * as newMessages from "../pure/new-messages";
import * as qsClient from "./queryserver-client";
import { asError, getErrorMessage } from "../pure/helpers-pure";
import { compileDatabaseUpgradeSequence } from "./upgrades";
import { QueryEvaluationInfo, QueryWithResults } from "../run-queries-shared";
import { QueryEvaluationInfo, QueryOutputDir } from "../run-queries-shared";
import { redactableError } from "../pure/errors";
import { CoreQueryResults, CoreQueryTarget } from "../queryRunner";
import { Position } from "../pure/messages-shared";
export async function compileQuery(
qs: qsClient.QueryServerClient,
program: messages.QlProgram,
quickEvalPosition: Position | undefined,
outputDir: QueryOutputDir,
progress: ProgressCallback,
token: CancellationToken,
logger: Logger,
): Promise<messages.CompilationMessage[]> {
let compiled: messages.CheckQueryResult | undefined;
try {
const target: messages.CompilationTarget = quickEvalPosition
? {
quickEval: { quickEvalPos: quickEvalPosition },
}
: { query: {} };
const params: messages.CompileQueryParams = {
compilationOptions: {
computeNoLocationUrls: true,
failOnWarnings: false,
fastCompilation: false,
includeDilInQlo: true,
localChecking: false,
noComputeGetUrl: false,
noComputeToString: false,
computeDefaultStrings: true,
emitDebugInfo: true,
},
extraOptions: {
timeoutSecs: qs.config.timeoutSecs,
},
queryToCheck: program,
resultPath: outputDir.compileQueryPath,
target,
};
// Update the active query logger every time there is a new request to compile.
// This isn't ideal because in situations where there are queries running
// in parallel, each query's log messages are interleaved. Fixing this
// properly will require a change in the query server.
qs.activeQueryLogger = logger;
compiled = await qs.sendRequest(
messages.compileQuery,
params,
token,
progress,
);
} finally {
void logger.log(" - - - COMPILATION DONE - - - ");
}
return (compiled?.messages || []).filter(
(msg) => msg.severity === messages.Severity.ERROR,
);
}
async function runQuery(
qs: qsClient.QueryServerClient,
upgradeQlo: string | undefined,
availableMlModels: cli.MlModelInfo[],
dbContents: DatabaseContentsWithDbScheme,
templates: Record<string, string> | undefined,
generateEvalLog: boolean,
outputDir: QueryOutputDir,
progress: ProgressCallback,
token: CancellationToken,
): Promise<messages.EvaluationResult> {
let result: messages.EvaluationResult | null = null;
const logPath = outputDir.logPath;
const callbackId = qs.registerCallback((res) => {
result = {
...res,
logFileLocation: logPath,
};
});
const availableMlModelUris: messages.MlModel[] = availableMlModels.map(
(model) => ({ uri: Uri.file(model.path).toString(true) }),
);
const queryToRun: messages.QueryToRun = {
resultsPath: outputDir.bqrsPath,
qlo: Uri.file(outputDir.compileQueryPath).toString(),
compiledUpgrade: upgradeQlo && Uri.file(upgradeQlo).toString(),
allowUnknownTemplates: true,
templateValues: createSimpleTemplates(templates),
availableMlModels: availableMlModelUris,
id: callbackId,
timeoutSecs: qs.config.timeoutSecs,
};
const dataset: messages.Dataset = {
dbDir: dbContents.datasetUri.fsPath,
workingSet: "default",
};
if (
generateEvalLog &&
(await qs.cliServer.cliConstraints.supportsPerQueryEvalLog())
) {
await qs.sendRequest(messages.startLog, {
db: dataset,
logPath: outputDir.evalLogPath,
});
}
const params: messages.EvaluateQueriesParams = {
db: dataset,
evaluateId: callbackId,
queries: [queryToRun],
stopOnError: false,
useSequenceHint: false,
};
try {
await qs.sendRequest(messages.runQueries, params, token, progress);
} finally {
qs.unRegisterCallback(callbackId);
if (
generateEvalLog &&
(await qs.cliServer.cliConstraints.supportsPerQueryEvalLog())
) {
await qs.sendRequest(messages.endLog, {
db: dataset,
logPath: outputDir.evalLogPath,
});
}
}
return (
result || {
evaluationTime: 0,
message: "No result from server",
queryId: -1,
runId: callbackId,
resultType: messages.QueryResultType.OTHER_ERROR,
}
);
}
/**
* A collection of evaluation-time information about a query,
@@ -40,7 +176,7 @@ export class QueryInProgress {
readonly querySaveDir: string,
readonly dbItemPath: string,
databaseHasMetadataFile: boolean,
readonly queryDbscheme: string, // the dbscheme file the query expects, based on library path resolution
readonly queryDbscheme: string, // the dbscheme file the query expects, ba`sed on library path resolution
readonly quickEvalPosition?: messages.Position,
readonly metadata?: QueryMetadata,
readonly templates?: Record<string, string>,
@@ -54,162 +190,6 @@ export class QueryInProgress {
);
/**/
}
get compiledQueryPath() {
return this.queryEvalInfo.compileQueryPath;
}
async run(
qs: qsClient.QueryServerClient,
upgradeQlo: string | undefined,
availableMlModels: cli.MlModelInfo[],
dbItem: DatabaseItem,
progress: ProgressCallback,
token: CancellationToken,
logger: Logger,
queryInfo: LocalQueryInfo | undefined,
): Promise<messages.EvaluationResult> {
if (!dbItem.contents || dbItem.error) {
throw new Error("Can't run query on invalid database.");
}
let result: messages.EvaluationResult | null = null;
const callbackId = qs.registerCallback((res) => {
result = {
...res,
logFileLocation: this.queryEvalInfo.logPath,
};
});
const availableMlModelUris: messages.MlModel[] = availableMlModels.map(
(model) => ({ uri: Uri.file(model.path).toString(true) }),
);
const queryToRun: messages.QueryToRun = {
resultsPath: this.queryEvalInfo.resultsPaths.resultsPath,
qlo: Uri.file(this.compiledQueryPath).toString(),
compiledUpgrade: upgradeQlo && Uri.file(upgradeQlo).toString(),
allowUnknownTemplates: true,
templateValues: createSimpleTemplates(this.templates),
availableMlModels: availableMlModelUris,
id: callbackId,
timeoutSecs: qs.config.timeoutSecs,
};
const dataset: messages.Dataset = {
dbDir: dbItem.contents.datasetUri.fsPath,
workingSet: "default",
};
if (
queryInfo &&
(await qs.cliServer.cliConstraints.supportsPerQueryEvalLog())
) {
await qs.sendRequest(messages.startLog, {
db: dataset,
logPath: this.queryEvalInfo.evalLogPath,
});
}
const params: messages.EvaluateQueriesParams = {
db: dataset,
evaluateId: callbackId,
queries: [queryToRun],
stopOnError: false,
useSequenceHint: false,
};
try {
await qs.sendRequest(messages.runQueries, params, token, progress);
if (qs.config.customLogDirectory) {
void showAndLogWarningMessage(
`Custom log directories are no longer supported. The "codeQL.runningQueries.customLogDirectory" setting is deprecated. Unset the setting to stop seeing this message. Query logs saved to ${this.queryEvalInfo.logPath}.`,
);
}
} finally {
qs.unRegisterCallback(callbackId);
if (
queryInfo &&
(await qs.cliServer.cliConstraints.supportsPerQueryEvalLog())
) {
await qs.sendRequest(messages.endLog, {
db: dataset,
logPath: this.queryEvalInfo.evalLogPath,
});
if (await this.queryEvalInfo.hasEvalLog()) {
await this.queryEvalInfo.addQueryLogs(
queryInfo,
qs.cliServer,
logger,
);
} else {
void showAndLogWarningMessage(
`Failed to write structured evaluator log to ${this.queryEvalInfo.evalLogPath}.`,
);
}
}
}
return (
result || {
evaluationTime: 0,
message: "No result from server",
queryId: -1,
runId: callbackId,
resultType: messages.QueryResultType.OTHER_ERROR,
}
);
}
async compile(
qs: qsClient.QueryServerClient,
program: messages.QlProgram,
progress: ProgressCallback,
token: CancellationToken,
logger: Logger,
): Promise<messages.CompilationMessage[]> {
let compiled: messages.CheckQueryResult | undefined;
try {
const target = this.quickEvalPosition
? {
quickEval: { quickEvalPos: this.quickEvalPosition },
}
: { query: {} };
const params: messages.CompileQueryParams = {
compilationOptions: {
computeNoLocationUrls: true,
failOnWarnings: false,
fastCompilation: false,
includeDilInQlo: true,
localChecking: false,
noComputeGetUrl: false,
noComputeToString: false,
computeDefaultStrings: true,
emitDebugInfo: true,
},
extraOptions: {
timeoutSecs: qs.config.timeoutSecs,
},
queryToCheck: program,
resultPath: this.compiledQueryPath,
target,
};
// Update the active query logger every time there is a new request to compile.
// This isn't ideal because in situations where there are queries running
// in parallel, each query's log messages are interleaved. Fixing this
// properly will require a change in the query server.
qs.activeQueryLogger = logger;
compiled = await qs.sendRequest(
messages.compileQuery,
params,
token,
progress,
);
} finally {
void logger.log(" - - - COMPILATION DONE - - - ");
}
return (compiled?.messages || []).filter(
(msg) => msg.severity === messages.Severity.ERROR,
);
}
}
export async function clearCacheInDatabase(
@@ -237,10 +217,10 @@ export async function clearCacheInDatabase(
function reportNoUpgradePath(
qlProgram: messages.QlProgram,
query: QueryInProgress,
queryDbscheme: string,
): void {
throw new Error(
`Query ${qlProgram.queryPath} expects database scheme ${query.queryDbscheme}, but the current database has a different scheme, and no database upgrades are available. The current database scheme may be newer than the CodeQL query libraries in your workspace.\n\nPlease try using a newer version of the query libraries.`,
`Query ${qlProgram.queryPath} expects database scheme ${queryDbscheme}, but the current database has a different scheme, and no database upgrades are available. The current database scheme may be newer than the CodeQL query libraries in your workspace.\n\nPlease try using a newer version of the query libraries.`,
);
}
@@ -250,32 +230,27 @@ function reportNoUpgradePath(
async function compileNonDestructiveUpgrade(
qs: qsClient.QueryServerClient,
upgradeTemp: tmp.DirectoryResult,
query: QueryInProgress,
queryDbscheme: string,
qlProgram: messages.QlProgram,
dbItem: DatabaseItem,
dbContents: DatabaseContentsWithDbScheme,
progress: ProgressCallback,
token: CancellationToken,
): Promise<string> {
if (!dbItem?.contents?.dbSchemeUri) {
throw new Error("Database is invalid, and cannot be upgraded.");
}
// Dependencies may exist outside of the workspace and they are always on the resolved search path.
const upgradesPath = qlProgram.libraryPath;
const { scripts, matchesTarget } = await qs.cliServer.resolveUpgrades(
dbItem.contents.dbSchemeUri.fsPath,
dbContents.dbSchemeUri.fsPath,
upgradesPath,
true,
query.queryDbscheme,
queryDbscheme,
);
if (!matchesTarget) {
reportNoUpgradePath(qlProgram, query);
reportNoUpgradePath(qlProgram, queryDbscheme);
}
const result = await compileDatabaseUpgradeSequence(
qs,
dbItem,
scripts,
upgradeTemp,
progress,
@@ -286,34 +261,67 @@ async function compileNonDestructiveUpgrade(
throw new Error(error);
}
// We can upgrade to the actual target
qlProgram.dbschemePath = query.queryDbscheme;
qlProgram.dbschemePath = queryDbscheme;
// We are new enough that we will always support single file upgrades.
return result.compiledUpgrade;
}
export async function compileAndRunQueryAgainstDatabase(
cliServer: cli.CodeQLCliServer,
qs: qsClient.QueryServerClient,
dbItem: DatabaseItem,
initialInfo: InitialQueryInfo,
queryStorageDir: string,
progress: ProgressCallback,
token: CancellationToken,
templates?: Record<string, string>,
queryInfo?: LocalQueryInfo, // May be omitted for queries not initiated by the user. If omitted we won't create a structured log for the query.
): Promise<QueryWithResults> {
if (!dbItem.contents || !dbItem.contents.dbSchemeUri) {
throw new Error(
`Database ${dbItem.databaseUri} does not have a CodeQL database scheme.`,
);
function translateLegacyResult(
legacyResult: messages.EvaluationResult,
): Omit<CoreQueryResults, "dispose"> {
let newResultType: newMessages.QueryResultType;
let newMessage = legacyResult.message;
switch (legacyResult.resultType) {
case messages.QueryResultType.SUCCESS:
newResultType = newMessages.QueryResultType.SUCCESS;
break;
case messages.QueryResultType.CANCELLATION:
newResultType = newMessages.QueryResultType.CANCELLATION;
break;
case messages.QueryResultType.OOM:
newResultType = newMessages.QueryResultType.OOM;
break;
case messages.QueryResultType.TIMEOUT:
// This is the only legacy result type that doesn't exist for the new query server. Format the
// message here, and let the later code treat it as `OTHER_ERROR`.
newResultType = newMessages.QueryResultType.OTHER_ERROR;
newMessage = `timed out after ${Math.round(
legacyResult.evaluationTime / 1000,
)} seconds`;
break;
case messages.QueryResultType.OTHER_ERROR:
default:
newResultType = newMessages.QueryResultType.OTHER_ERROR;
break;
}
// Get the workspace folder paths.
const diskWorkspaceFolders = getOnDiskWorkspaceFolders();
return {
resultType: newResultType,
message: newMessage,
evaluationTime: legacyResult.evaluationTime,
};
}
export async function compileAndRunQueryAgainstDatabaseCore(
qs: qsClient.QueryServerClient,
dbPath: string,
query: CoreQueryTarget,
generateEvalLog: boolean,
additionalPacks: string[],
outputDir: QueryOutputDir,
progress: ProgressCallback,
token: CancellationToken,
templates: Record<string, string> | undefined,
logger: Logger,
): Promise<CoreQueryResults> {
const dbContents = await DatabaseResolver.resolveDatabaseContents(
Uri.file(dbPath),
);
// Figure out the library path for the query.
const packConfig = await cliServer.resolveLibraryPath(
diskWorkspaceFolders,
initialInfo.queryPath,
const packConfig = await qs.cliServer.resolveLibraryPath(
additionalPacks,
query.queryPath,
);
if (!packConfig.dbscheme) {
@@ -327,17 +335,15 @@ export async function compileAndRunQueryAgainstDatabase(
// won't trigger this check)
// This test will produce confusing results if we ever change the name of the database schema files.
const querySchemaName = basename(packConfig.dbscheme);
const dbSchemaName = basename(dbItem.contents.dbSchemeUri.fsPath);
const dbSchemaName = basename(dbContents.dbSchemeUri?.fsPath);
if (querySchemaName !== dbSchemaName) {
void extLogger.log(
`Query schema was ${querySchemaName}, but database schema was ${dbSchemaName}.`,
);
throw new Error(
`The query ${basename(
initialInfo.queryPath,
)} cannot be run against the selected database (${
dbItem.name
}): their target languages are different. Please select a different database and try again.`,
query.queryPath,
)} cannot be run against the selected database: their target languages are different. Please select a different database and try again.`,
);
}
@@ -349,20 +355,14 @@ export async function compileAndRunQueryAgainstDatabase(
// Since we are compiling and running a query against a database,
// we use the database's DB scheme here instead of the DB scheme
// from the current document's project.
dbschemePath: dbItem.contents.dbSchemeUri.fsPath,
queryPath: initialInfo.queryPath,
dbschemePath: dbContents.dbSchemeUri.fsPath,
queryPath: query.queryPath,
};
// Read the query metadata if possible, to use in the UI.
const metadata = await tryGetQueryMetadata(cliServer, qlProgram.queryPath);
let availableMlModels: cli.MlModelInfo[] = [];
try {
availableMlModels = (
await cliServer.resolveMlModels(
diskWorkspaceFolders,
initialInfo.queryPath,
)
await qs.cliServer.resolveMlModels(additionalPacks, query.queryPath)
).models;
if (availableMlModels.length) {
void extLogger.log(
@@ -381,57 +381,53 @@ export async function compileAndRunQueryAgainstDatabase(
);
}
const hasMetadataFile = await dbItem.hasMetadataFile();
const query = new QueryInProgress(
join(queryStorageDir, initialInfo.id),
dbItem.databaseUri.fsPath,
hasMetadataFile,
packConfig.dbscheme,
initialInfo.quickEvalPosition,
metadata,
templates,
);
const logger = new TeeLogger(qs.logger, query.queryEvalInfo.logPath);
await query.queryEvalInfo.createTimestampFile();
let upgradeDir: tmp.DirectoryResult | undefined;
try {
upgradeDir = await tmp.dir({ dir: upgradesTmpDir, unsafeCleanup: true });
const upgradeQlo = await compileNonDestructiveUpgrade(
qs,
upgradeDir,
query,
packConfig.dbscheme,
qlProgram,
dbItem,
dbContents,
progress,
token,
);
let errors;
try {
errors = await query.compile(qs, qlProgram, progress, token, logger);
errors = await compileQuery(
qs,
qlProgram,
query.quickEvalPosition,
outputDir,
progress,
token,
logger,
);
} catch (e) {
if (
e instanceof ResponseError &&
e.code === LSPErrorCodes.RequestCancelled
) {
return createSyntheticResult(query, "Query cancelled");
return createSyntheticResult("Query cancelled");
} else {
throw e;
}
}
if (errors.length === 0) {
const result = await query.run(
const result = await runQuery(
qs,
upgradeQlo,
availableMlModels,
dbItem,
dbContents,
templates,
generateEvalLog,
outputDir,
progress,
token,
logger,
queryInfo,
);
if (result.resultType !== messages.QueryResultType.SUCCESS) {
const error = result.message
? redactableError`${result.message}`
@@ -439,22 +435,15 @@ export async function compileAndRunQueryAgainstDatabase(
void extLogger.log(error.fullMessage);
void showAndLogExceptionWithTelemetry(error);
}
const message = formatLegacyMessage(result);
return {
query: query.queryEvalInfo,
message,
result,
successful: result.resultType === messages.QueryResultType.SUCCESS,
logFileLocation: result.logFileLocation,
};
return translateLegacyResult(result);
} else {
// Error dialogs are limited in size and scrollability,
// so we include a general description of the problem,
// and direct the user to the output window for the detailed compilation messages.
// However we don't show quick eval errors there so we need to display them anyway.
void logger.log(
`Failed to compile query ${initialInfo.queryPath} against database scheme ${qlProgram.dbschemePath}:`,
`Failed to compile query ${query.queryPath} against database scheme ${qlProgram.dbschemePath}:`,
);
const formattedMessages: string[] = [];
@@ -465,22 +454,12 @@ export async function compileAndRunQueryAgainstDatabase(
formattedMessages.push(formatted);
void logger.log(formatted);
}
if (initialInfo.isQuickEval && formattedMessages.length <= 2) {
// If there are more than 2 error messages, they will not be displayed well in a popup
// and will be trimmed by the function displaying the error popup. Accordingly, we only
// try to show the errors if there are 2 or less, otherwise we direct the user to the log.
void showAndLogErrorMessage(
`Quick evaluation compilation failed: ${formattedMessages.join(
"\n",
)}`,
);
} else {
void showAndLogErrorMessage(
(initialInfo.isQuickEval ? "Quick evaluation" : "Query") +
compilationFailedErrorTail,
);
}
return createSyntheticResult(query, "Query had compilation errors");
return {
evaluationTime: 0,
resultType: newMessages.QueryResultType.COMPILATION_ERROR,
message: formattedMessages[0],
};
}
} finally {
try {
@@ -493,11 +472,6 @@ export async function compileAndRunQueryAgainstDatabase(
}
}
const compilationFailedErrorTail =
" compilation failed. Please make sure there are no errors in the query, the database is up to date," +
" and the query and database use the same target language. For more details on the error, go to View > Output," +
" and choose CodeQL Query Server from the dropdown.";
export function formatLegacyMessage(result: messages.EvaluationResult) {
switch (result.resultType) {
case messages.QueryResultType.CANCELLATION:
@@ -521,21 +495,11 @@ export function formatLegacyMessage(result: messages.EvaluationResult) {
/**
* Create a synthetic result for a query that failed to compile.
*/
function createSyntheticResult(
query: QueryInProgress,
message: string,
): QueryWithResults {
function createSyntheticResult(message: string): CoreQueryResults {
return {
query: query.queryEvalInfo,
evaluationTime: 0,
resultType: newMessages.QueryResultType.OTHER_ERROR,
message,
result: {
evaluationTime: 0,
queryId: 0,
resultType: messages.QueryResultType.OTHER_ERROR,
message,
runId: 0,
},
successful: false,
};
}

View File

@@ -27,18 +27,11 @@ const MAX_UPGRADE_MESSAGE_LINES = 10;
*/
export async function compileDatabaseUpgradeSequence(
qs: qsClient.QueryServerClient,
dbItem: DatabaseItem,
resolvedSequence: string[],
currentUpgradeTmp: tmp.DirectoryResult,
progress: ProgressCallback,
token: vscode.CancellationToken,
): Promise<messages.CompileUpgradeSequenceResult> {
if (
dbItem.contents === undefined ||
dbItem.contents.dbSchemeUri === undefined
) {
throw new Error("Database is invalid, and cannot be upgraded.");
}
// If possible just compile the upgrade sequence
return await qs.sendRequest(
messages.compileUpgradeSequence,

View File

@@ -95,6 +95,10 @@ export interface DatabaseContents {
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.
@@ -165,7 +169,7 @@ async function getDbSchemeFiles(dbDirectory: string): Promise<string[]> {
export class DatabaseResolver {
public static async resolveDatabaseContents(
uri: vscode.Uri,
): Promise<DatabaseContents> {
): Promise<DatabaseContentsWithDbScheme> {
if (uri.scheme !== "file") {
throw new Error(
`Database URI scheme '${uri.scheme}' not supported; only 'file' URIs are supported.`,
@@ -199,9 +203,12 @@ export class DatabaseResolver {
`Database '${databasePath}' contains multiple CodeQL dbschemes under '${dbPath}'.`,
);
} else {
contents.dbSchemeUri = vscode.Uri.file(resolve(dbPath, dbSchemeFiles[0]));
const dbSchemeUri = vscode.Uri.file(resolve(dbPath, dbSchemeFiles[0]));
return {
...contents,
dbSchemeUri,
};
}
return contents;
}
public static async resolveDatabase(

View File

@@ -7,384 +7,575 @@ import {
Uri,
window,
} from "vscode";
import { extLogger } from "./common";
import { BaseLogger, extLogger, Logger, TeeLogger } from "./common";
import { MAX_QUERIES } from "./config";
import { gatherQlFiles } from "./pure/files";
import { basename } from "path";
import {
createTimestampFile,
findLanguage,
getOnDiskWorkspaceFolders,
showAndLogErrorMessage,
showAndLogExceptionWithTelemetry,
showAndLogWarningMessage,
showBinaryChoiceDialog,
tryGetQueryMetadata,
} from "./helpers";
import { displayQuickQuery } from "./quick-query";
import { QueryRunner } from "./queryRunner";
import {
CoreCompletedQuery,
CoreQueryResults,
QueryRunner,
} from "./queryRunner";
import { QueryHistoryManager } from "./query-history/query-history-manager";
import { DatabaseUI } from "./local-databases-ui";
import { ResultsView } from "./interface";
import { DatabaseItem, DatabaseManager } from "./local-databases";
import { createInitialQueryInfo } from "./run-queries-shared";
import {
createInitialQueryInfo,
determineSelectedQuery,
EvaluatorLogPaths,
generateEvalLogSummaries,
logEndSummary,
QueryEvaluationInfo,
QueryOutputDir,
QueryWithResults,
SelectedQuery,
} from "./run-queries-shared";
import { CompletedLocalQueryInfo, LocalQueryInfo } from "./query-results";
import { WebviewReveal } from "./interface-utils";
import { asError, getErrorMessage } from "./pure/helpers-pure";
import { CodeQLCliServer } from "./cli";
import { LocalQueryCommands } from "./common/commands";
import { App } from "./common/app";
type LocalQueryOptions = {
app: App;
queryRunner: QueryRunner;
queryHistoryManager: QueryHistoryManager;
databaseManager: DatabaseManager;
cliServer: CodeQLCliServer;
databaseUI: DatabaseUI;
localQueryResultsView: ResultsView;
queryStorageDir: string;
};
export function getLocalQueryCommands({
app,
queryRunner,
queryHistoryManager,
databaseManager,
cliServer,
databaseUI,
localQueryResultsView,
queryStorageDir,
}: LocalQueryOptions): LocalQueryCommands {
const runQuery = async (uri: Uri | undefined) =>
withProgress(
async (progress, token) => {
await compileAndRunQuery(
queryRunner,
queryHistoryManager,
databaseUI,
localQueryResultsView,
queryStorageDir,
false,
uri,
progress,
token,
undefined,
);
},
{
title: "Running query",
cancellable: true,
},
);
const runQueryOnMultipleDatabases = async (uri: Uri | undefined) =>
withProgress(
async (progress, token) =>
await compileAndRunQueryOnMultipleDatabases(
cliServer,
queryRunner,
queryHistoryManager,
databaseManager,
databaseUI,
localQueryResultsView,
queryStorageDir,
progress,
token,
uri,
),
{
title: "Running query on selected databases",
cancellable: true,
},
);
const runQueries = async (_: Uri | undefined, multi: Uri[]) =>
withProgress(
async (progress, token) => {
const maxQueryCount = MAX_QUERIES.getValue() as number;
const [files, dirFound] = await gatherQlFiles(
multi.map((uri) => uri.fsPath),
);
if (files.length > maxQueryCount) {
throw new Error(
`You tried to run ${files.length} queries, but the maximum is ${maxQueryCount}. Try selecting fewer queries or changing the 'codeQL.runningQueries.maxQueries' setting.`,
);
}
// warn user and display selected files when a directory is selected because some ql
// files may be hidden from the user.
if (dirFound) {
const fileString = files.map((file) => basename(file)).join(", ");
const res = await showBinaryChoiceDialog(
`You are about to run ${files.length} queries: ${fileString} Do you want to continue?`,
);
if (!res) {
return;
}
}
const queryUris = files.map((path) => Uri.parse(`file:${path}`, true));
// Use a wrapped progress so that messages appear with the queries remaining in it.
let queriesRemaining = queryUris.length;
function wrappedProgress(update: ProgressUpdate) {
const message =
queriesRemaining > 1
? `${queriesRemaining} remaining. ${update.message}`
: update.message;
progress({
...update,
message,
});
}
wrappedProgress({
maxStep: queryUris.length,
step: queryUris.length - queriesRemaining,
message: "",
});
await Promise.all(
queryUris.map(async (uri) =>
compileAndRunQuery(
queryRunner,
queryHistoryManager,
databaseUI,
localQueryResultsView,
queryStorageDir,
false,
uri,
wrappedProgress,
token,
undefined,
).then(() => queriesRemaining--),
),
);
},
{
title: "Running queries",
cancellable: true,
},
);
const quickEval = async (uri: Uri) =>
withProgress(
async (progress, token) => {
await compileAndRunQuery(
queryRunner,
queryHistoryManager,
databaseUI,
localQueryResultsView,
queryStorageDir,
true,
uri,
progress,
token,
undefined,
);
},
{
title: "Running query",
cancellable: true,
},
);
const codeLensQuickEval = async (uri: Uri, range: Range) =>
withProgress(
async (progress, token) =>
await compileAndRunQuery(
queryRunner,
queryHistoryManager,
databaseUI,
localQueryResultsView,
queryStorageDir,
true,
uri,
progress,
token,
undefined,
range,
),
{
title: "Running query",
cancellable: true,
},
);
const quickQuery = async () =>
withProgress(
async (progress, token) =>
displayQuickQuery(app, cliServer, databaseUI, progress, token),
{
title: "Run Quick Query",
},
);
return {
"codeQL.runQuery": runQuery,
"codeQL.runQueryContextEditor": runQuery,
"codeQL.runQueryOnMultipleDatabases": runQueryOnMultipleDatabases,
"codeQL.runQueryOnMultipleDatabasesContextEditor":
runQueryOnMultipleDatabases,
"codeQL.runQueries": runQueries,
"codeQL.quickEval": quickEval,
"codeQL.quickEvalContextEditor": quickEval,
"codeQL.codeLensQuickEval": codeLensQuickEval,
"codeQL.quickQuery": quickQuery,
};
}
export async function compileAndRunQuery(
qs: QueryRunner,
qhm: QueryHistoryManager,
databaseUI: DatabaseUI,
localQueryResultsView: ResultsView,
queryStorageDir: string,
quickEval: boolean,
selectedQuery: Uri | undefined,
progress: ProgressCallback,
token: CancellationToken,
databaseItem: DatabaseItem | undefined,
range?: Range,
): Promise<void> {
if (qs !== undefined) {
// If no databaseItem is specified, use the database currently selected in the Databases UI
databaseItem =
databaseItem || (await databaseUI.getDatabaseItem(progress, token));
if (databaseItem === undefined) {
throw new Error("Can't run query without a selected database");
}
const databaseInfo = {
name: databaseItem.name,
databaseUri: databaseItem.databaseUri.toString(),
};
// handle cancellation from the history view.
const source = new CancellationTokenSource();
token.onCancellationRequested(() => source.cancel());
const initialInfo = await createInitialQueryInfo(
selectedQuery,
databaseInfo,
quickEval,
range,
);
const item = new LocalQueryInfo(initialInfo, source);
qhm.addQuery(item);
try {
const completedQueryInfo = await qs.compileAndRunQueryAgainstDatabase(
databaseItem,
initialInfo,
queryStorageDir,
progress,
source.token,
undefined,
item,
);
qhm.completeQuery(item, completedQueryInfo);
await showResultsForCompletedQuery(
localQueryResultsView,
item as CompletedLocalQueryInfo,
WebviewReveal.Forced,
);
// Note we must update the query history view after showing results as the
// display and sorting might depend on the number of results
} catch (e) {
const err = asError(e);
err.message = `Error running query: ${err.message}`;
item.failureReason = err.message;
throw e;
} finally {
await qhm.refreshTreeView();
source.dispose();
}
}
}
import { DisposableObject } from "./pure/disposable-object";
import { QueryResultType } from "./pure/new-messages";
import { redactableError } from "./pure/errors";
interface DatabaseQuickPickItem extends QuickPickItem {
databaseItem: DatabaseItem;
}
async function compileAndRunQueryOnMultipleDatabases(
cliServer: CodeQLCliServer,
qs: QueryRunner,
qhm: QueryHistoryManager,
dbm: DatabaseManager,
databaseUI: DatabaseUI,
localQueryResultsView: ResultsView,
queryStorageDir: string,
progress: ProgressCallback,
token: CancellationToken,
uri: Uri | undefined,
): Promise<void> {
let filteredDBs = dbm.databaseItems;
if (filteredDBs.length === 0) {
void showAndLogErrorMessage(
"No databases found. Please add a suitable database to your workspace.",
);
return;
}
// If possible, only show databases with the right language (otherwise show all databases).
const queryLanguage = await findLanguage(cliServer, uri);
if (queryLanguage) {
filteredDBs = dbm.databaseItems.filter(
(db) => db.language === queryLanguage,
);
if (filteredDBs.length === 0) {
void showAndLogErrorMessage(
`No databases found for language ${queryLanguage}. Please add a suitable database to your workspace.`,
);
return;
}
}
const quickPickItems = filteredDBs.map<DatabaseQuickPickItem>((dbItem) => ({
databaseItem: dbItem,
label: dbItem.name,
description: dbItem.language,
}));
/**
* Databases that were selected in the quick pick menu.
*/
const quickpick = await window.showQuickPick<DatabaseQuickPickItem>(
quickPickItems,
{ canPickMany: true, ignoreFocusOut: true },
);
if (quickpick !== undefined) {
// Collect all skipped databases and display them at the end (instead of popping up individual errors)
const skippedDatabases = [];
const errors = [];
for (const item of quickpick) {
try {
await compileAndRunQuery(
qs,
qhm,
databaseUI,
localQueryResultsView,
queryStorageDir,
false,
uri,
progress,
token,
item.databaseItem,
);
} catch (e) {
skippedDatabases.push(item.label);
errors.push(getErrorMessage(e));
}
}
if (skippedDatabases.length > 0) {
void extLogger.log(`Errors:\n${errors.join("\n")}`);
void showAndLogWarningMessage(
`The following databases were skipped:\n${skippedDatabases.join(
"\n",
)}.\nFor details about the errors, see the logs.`,
);
}
} else {
void showAndLogErrorMessage("No databases selected.");
function formatResultMessage(result: CoreQueryResults): string {
switch (result.resultType) {
case QueryResultType.CANCELLATION:
return `cancelled after ${Math.round(
result.evaluationTime / 1000,
)} seconds`;
case QueryResultType.OOM:
return "out of memory";
case QueryResultType.SUCCESS:
return `finished in ${Math.round(result.evaluationTime / 1000)} seconds`;
case QueryResultType.COMPILATION_ERROR:
return `compilation failed: ${result.message}`;
case QueryResultType.OTHER_ERROR:
default:
return result.message ? `failed: ${result.message}` : "failed";
}
}
export async function showResultsForCompletedQuery(
localQueryResultsView: ResultsView,
query: CompletedLocalQueryInfo,
forceReveal: WebviewReveal,
): Promise<void> {
await localQueryResultsView.showResults(query, forceReveal, false);
/**
* Tracks the evaluation of a local query, including its interactions with the UI.
*
* The client creates an instance of `LocalQueryRun` when the evaluation starts, and then invokes
* the `complete()` function once the query has completed (successfully or otherwise).
*
* Having the client tell the `LocalQueryRun` when the evaluation is complete, rather than having
* the `LocalQueryRun` manage the evaluation itself, may seem a bit clunky. It's done this way
* because once we move query evaluation into a Debug Adapter, the debugging UI drives the
* evaluation, and we can only respond to events from the debug adapter.
*/
export class LocalQueryRun {
public constructor(
private readonly outputDir: QueryOutputDir,
private readonly localQueries: LocalQueries,
private readonly queryInfo: LocalQueryInfo,
private readonly dbItem: DatabaseItem,
public readonly logger: Logger, // Public so that other clients, like the debug adapter, know where to send log output
private readonly queryHistoryManager: QueryHistoryManager,
private readonly cliServer: CodeQLCliServer,
) {}
/**
* Updates the UI based on the results of the query evaluation. This creates the evaluator log
* summaries, updates the query history item for the evaluation with the results and evaluation
* time, and displays the results view.
*
* This function must be called when the evaluation completes, whether the evaluation was
* successful or not.
* */
public async complete(results: CoreQueryResults): Promise<void> {
const evalLogPaths = await this.summarizeEvalLog(
results.resultType,
this.outputDir,
this.logger,
);
if (evalLogPaths !== undefined) {
this.queryInfo.setEvaluatorLogPaths(evalLogPaths);
}
const queryWithResults = await this.getCompletedQueryInfo(results);
this.queryHistoryManager.completeQuery(this.queryInfo, queryWithResults);
await this.localQueries.showResultsForCompletedQuery(
this.queryInfo as CompletedLocalQueryInfo,
WebviewReveal.Forced,
);
// Note we must update the query history view after showing results as the
// display and sorting might depend on the number of results
await this.queryHistoryManager.refreshTreeView();
}
/**
* Updates the UI in the case where query evaluation throws an exception.
*/
public async fail(err: Error): Promise<void> {
err.message = `Error running query: ${err.message}`;
this.queryInfo.failureReason = err.message;
await this.queryHistoryManager.refreshTreeView();
}
/**
* Generate summaries of the structured evaluator log.
*/
private async summarizeEvalLog(
resultType: QueryResultType,
outputDir: QueryOutputDir,
logger: BaseLogger,
): Promise<EvaluatorLogPaths | undefined> {
const evalLogPaths = await generateEvalLogSummaries(
this.cliServer,
outputDir,
);
if (evalLogPaths !== undefined) {
if (evalLogPaths.endSummary !== undefined) {
void logEndSummary(evalLogPaths.endSummary, logger); // Logged asynchrnously
}
} else {
// Raw evaluator log was not found. Notify the user, unless we know why it wasn't found.
if (resultType === QueryResultType.SUCCESS) {
void showAndLogWarningMessage(
`Failed to write structured evaluator log to ${outputDir.evalLogPath}.`,
);
} else {
// Don't bother notifying the user if there's no log. For some errors, like compilation
// errors, we don't expect a log. For cancellations and OOM errors, whether or not we have
// a log depends on how far execution got before termination.
}
}
return evalLogPaths;
}
/**
* Gets a `QueryWithResults` containing information about the evaluation of the query and its
* result, in the form expected by the query history UI.
*/
private async getCompletedQueryInfo(
results: CoreQueryResults,
): Promise<QueryWithResults> {
// Read the query metadata if possible, to use in the UI.
const metadata = await tryGetQueryMetadata(
this.cliServer,
this.queryInfo.initialInfo.queryPath,
);
const query = new QueryEvaluationInfo(
this.outputDir.querySaveDir,
this.dbItem.databaseUri.fsPath,
await this.dbItem.hasMetadataFile(),
this.queryInfo.initialInfo.quickEvalPosition,
metadata,
);
if (results.resultType !== QueryResultType.SUCCESS) {
const message = results.message
? redactableError`Failed to run query: ${results.message}`
: redactableError`Failed to run query`;
void showAndLogExceptionWithTelemetry(message);
}
const message = formatResultMessage(results);
const successful = results.resultType === QueryResultType.SUCCESS;
return {
query,
result: {
evaluationTime: results.evaluationTime,
queryId: 0,
resultType: successful
? QueryResultType.SUCCESS
: QueryResultType.OTHER_ERROR,
runId: 0,
message,
},
message,
successful,
};
}
}
export class LocalQueries extends DisposableObject {
public constructor(
private readonly app: App,
private readonly queryRunner: QueryRunner,
private readonly queryHistoryManager: QueryHistoryManager,
private readonly databaseManager: DatabaseManager,
private readonly cliServer: CodeQLCliServer,
private readonly databaseUI: DatabaseUI,
private readonly localQueryResultsView: ResultsView,
private readonly queryStorageDir: string,
) {
super();
}
public getCommands(): LocalQueryCommands {
const runQuery = async (uri: Uri | undefined) =>
withProgress(
async (progress, token) => {
await this.compileAndRunQuery(false, uri, progress, token, undefined);
},
{
title: "Running query",
cancellable: true,
},
);
const runQueryOnMultipleDatabases = async (uri: Uri | undefined) =>
withProgress(
async (progress, token) =>
await this.compileAndRunQueryOnMultipleDatabases(
progress,
token,
uri,
),
{
title: "Running query on selected databases",
cancellable: true,
},
);
const runQueries = async (_: Uri | undefined, multi: Uri[]) =>
withProgress(
async (progress, token) => {
const maxQueryCount = MAX_QUERIES.getValue() as number;
const [files, dirFound] = await gatherQlFiles(
multi.map((uri) => uri.fsPath),
);
if (files.length > maxQueryCount) {
throw new Error(
`You tried to run ${files.length} queries, but the maximum is ${maxQueryCount}. Try selecting fewer queries or changing the 'codeQL.runningQueries.maxQueries' setting.`,
);
}
// warn user and display selected files when a directory is selected because some ql
// files may be hidden from the user.
if (dirFound) {
const fileString = files.map((file) => basename(file)).join(", ");
const res = await showBinaryChoiceDialog(
`You are about to run ${files.length} queries: ${fileString} Do you want to continue?`,
);
if (!res) {
return;
}
}
const queryUris = files.map((path) =>
Uri.parse(`file:${path}`, true),
);
// Use a wrapped progress so that messages appear with the queries remaining in it.
let queriesRemaining = queryUris.length;
function wrappedProgress(update: ProgressUpdate) {
const message =
queriesRemaining > 1
? `${queriesRemaining} remaining. ${update.message}`
: update.message;
progress({
...update,
message,
});
}
wrappedProgress({
maxStep: queryUris.length,
step: queryUris.length - queriesRemaining,
message: "",
});
await Promise.all(
queryUris.map(async (uri) =>
this.compileAndRunQuery(
false,
uri,
wrappedProgress,
token,
undefined,
).then(() => queriesRemaining--),
),
);
},
{
title: "Running queries",
cancellable: true,
},
);
const quickEval = async (uri: Uri) =>
withProgress(
async (progress, token) => {
await this.compileAndRunQuery(true, uri, progress, token, undefined);
},
{
title: "Running query",
cancellable: true,
},
);
const codeLensQuickEval = async (uri: Uri, range: Range) =>
withProgress(
async (progress, token) =>
await this.compileAndRunQuery(
true,
uri,
progress,
token,
undefined,
range,
),
{
title: "Running query",
cancellable: true,
},
);
const quickQuery = async () =>
withProgress(
async (progress, token) =>
displayQuickQuery(
this.app,
this.cliServer,
this.databaseUI,
progress,
token,
),
{
title: "Run Quick Query",
},
);
return {
"codeQL.runQuery": runQuery,
"codeQL.runQueryContextEditor": runQuery,
"codeQL.runQueryOnMultipleDatabases": runQueryOnMultipleDatabases,
"codeQL.runQueryOnMultipleDatabasesContextEditor":
runQueryOnMultipleDatabases,
"codeQL.runQueries": runQueries,
"codeQL.quickEval": quickEval,
"codeQL.quickEvalContextEditor": quickEval,
"codeQL.codeLensQuickEval": codeLensQuickEval,
"codeQL.quickQuery": quickQuery,
};
}
/**
* Creates a new `LocalQueryRun` object to track a query evaluation. This creates a timestamp
* file in the query's output directory, creates a `LocalQueryInfo` object, and registers that
* object with the query history manager.
*
* Once the evaluation is complete, the client must call `complete()` on the `LocalQueryRun`
* object to update the UI based on the results of the query.
*/
public async createLocalQueryRun(
selectedQuery: SelectedQuery,
dbItem: DatabaseItem,
outputDir: QueryOutputDir,
tokenSource: CancellationTokenSource,
): Promise<LocalQueryRun> {
await createTimestampFile(outputDir.querySaveDir);
if (this.queryRunner.customLogDirectory) {
void showAndLogWarningMessage(
`Custom log directories are no longer supported. The "codeQL.runningQueries.customLogDirectory" setting is deprecated. Unset the setting to stop seeing this message. Query logs saved to ${outputDir.logPath}`,
);
}
const initialInfo = await createInitialQueryInfo(selectedQuery, {
databaseUri: dbItem.databaseUri.toString(),
name: dbItem.name,
});
// When cancellation is requested from the query history view, we just stop the debug session.
const queryInfo = new LocalQueryInfo(initialInfo, tokenSource);
this.queryHistoryManager.addQuery(queryInfo);
const logger = new TeeLogger(this.queryRunner.logger, outputDir.logPath);
return new LocalQueryRun(
outputDir,
this,
queryInfo,
dbItem,
logger,
this.queryHistoryManager,
this.cliServer,
);
}
public async compileAndRunQuery(
quickEval: boolean,
queryUri: Uri | undefined,
progress: ProgressCallback,
token: CancellationToken,
databaseItem: DatabaseItem | undefined,
range?: Range,
): Promise<void> {
await this.compileAndRunQueryInternal(
quickEval,
queryUri,
progress,
token,
databaseItem,
range,
);
}
/** Used by tests */
public async compileAndRunQueryInternal(
quickEval: boolean,
queryUri: Uri | undefined,
progress: ProgressCallback,
token: CancellationToken,
databaseItem: DatabaseItem | undefined,
range?: Range,
): Promise<CoreCompletedQuery> {
const selectedQuery = await determineSelectedQuery(
queryUri,
quickEval,
range,
);
// If no databaseItem is specified, use the database currently selected in the Databases UI
databaseItem =
databaseItem || (await this.databaseUI.getDatabaseItem(progress, token));
if (databaseItem === undefined) {
throw new Error("Can't run query without a selected database");
}
const coreQueryRun = this.queryRunner.createQueryRun(
databaseItem.databaseUri.fsPath,
{
queryPath: selectedQuery.queryPath,
quickEvalPosition: selectedQuery.quickEvalPosition,
},
true,
getOnDiskWorkspaceFolders(),
this.queryStorageDir,
undefined,
undefined,
);
// handle cancellation from the history view.
const source = new CancellationTokenSource();
try {
token.onCancellationRequested(() => source.cancel());
const localQueryRun = await this.createLocalQueryRun(
selectedQuery,
databaseItem,
coreQueryRun.outputDir,
source,
);
try {
const results = await coreQueryRun.evaluate(
progress,
source.token,
localQueryRun.logger,
);
await localQueryRun.complete(results);
return results;
} catch (e) {
// It's odd that we have two different ways for a query evaluation to fail: by throwing an
// exception, and by returning a result with a failure code. This is how the code worked
// before the refactoring, so it's been preserved, but we should probably figure out how
// to unify both error handling paths.
const err = asError(e);
await localQueryRun.fail(err);
throw e;
}
} finally {
source.dispose();
}
}
private async compileAndRunQueryOnMultipleDatabases(
progress: ProgressCallback,
token: CancellationToken,
uri: Uri | undefined,
): Promise<void> {
let filteredDBs = this.databaseManager.databaseItems;
if (filteredDBs.length === 0) {
void showAndLogErrorMessage(
"No databases found. Please add a suitable database to your workspace.",
);
return;
}
// If possible, only show databases with the right language (otherwise show all databases).
const queryLanguage = await findLanguage(this.cliServer, uri);
if (queryLanguage) {
filteredDBs = this.databaseManager.databaseItems.filter(
(db) => db.language === queryLanguage,
);
if (filteredDBs.length === 0) {
void showAndLogErrorMessage(
`No databases found for language ${queryLanguage}. Please add a suitable database to your workspace.`,
);
return;
}
}
const quickPickItems = filteredDBs.map<DatabaseQuickPickItem>((dbItem) => ({
databaseItem: dbItem,
label: dbItem.name,
description: dbItem.language,
}));
/**
* Databases that were selected in the quick pick menu.
*/
const quickpick = await window.showQuickPick<DatabaseQuickPickItem>(
quickPickItems,
{ canPickMany: true, ignoreFocusOut: true },
);
if (quickpick !== undefined) {
// Collect all skipped databases and display them at the end (instead of popping up individual errors)
const skippedDatabases = [];
const errors = [];
for (const item of quickpick) {
try {
await this.compileAndRunQuery(
false,
uri,
progress,
token,
item.databaseItem,
);
} catch (e) {
skippedDatabases.push(item.label);
errors.push(getErrorMessage(e));
}
}
if (skippedDatabases.length > 0) {
void extLogger.log(`Errors:\n${errors.join("\n")}`);
void showAndLogWarningMessage(
`The following databases were skipped:\n${skippedDatabases.join(
"\n",
)}.\nFor details about the errors, see the logs.`,
);
}
} else {
void showAndLogErrorMessage("No databases selected.");
}
}
public async showResultsForCompletedQuery(
query: CompletedLocalQueryInfo,
forceReveal: WebviewReveal,
): Promise<void> {
await this.localQueryResultsView.showResults(query, forceReveal, false);
}
}

View File

@@ -16,7 +16,11 @@ import {
DatabaseInfo,
} from "./pure/interface-types";
import { QueryStatus } from "./query-status";
import { QueryEvaluationInfo, QueryWithResults } from "./run-queries-shared";
import {
EvaluatorLogPaths,
QueryEvaluationInfo,
QueryWithResults,
} from "./run-queries-shared";
import { formatLegacyMessage } from "./legacy-query-server/run-queries";
import { sarifParser } from "./sarif-parser";
@@ -253,6 +257,14 @@ export class LocalQueryInfo {
this.initialInfo.userSpecifiedLabel = label;
}
/** Sets the paths to the various structured evaluator logs. */
public setEvaluatorLogPaths(logPaths: EvaluatorLogPaths): void {
this.evalLogLocation = logPaths.log;
this.evalLogSummaryLocation = logPaths.humanReadableSummary;
this.jsonEvalLogSummaryLocation = logPaths.jsonSummary;
this.evalLogSummarySymbolsLocation = logPaths.summarySymbols;
}
/**
* The query's file name, unless it is a quick eval.
* Queries run through quick evaluation are not usually the entire query file.

View File

@@ -9,22 +9,32 @@ import {
registerDatabases,
upgradeDatabase,
} from "../pure/new-messages";
import { InitialQueryInfo, LocalQueryInfo } from "../query-results";
import { QueryRunner } from "../queryRunner";
import { QueryWithResults } from "../run-queries-shared";
import { CoreQueryResults, CoreQueryTarget, QueryRunner } from "../queryRunner";
import { QueryServerClient } from "./queryserver-client";
import { compileAndRunQueryAgainstDatabase } from "./run-queries";
import { compileAndRunQueryAgainstDatabaseCore } from "./run-queries";
import * as vscode from "vscode";
import { getOnDiskWorkspaceFolders } from "../helpers";
import { CodeQLCliServer } from "../cli";
import { Logger } from "../common";
import { QueryOutputDir } from "../run-queries-shared";
export class NewQueryRunner extends QueryRunner {
constructor(public readonly qs: QueryServerClient) {
super();
}
get cliServer() {
get cliServer(): CodeQLCliServer {
return this.qs.cliServer;
}
get customLogDirectory(): string | undefined {
return this.qs.config.customLogDirectory;
}
get logger(): Logger {
return this.qs.logger;
}
async restartQueryServer(
progress: ProgressCallback,
token: CancellationToken,
@@ -57,25 +67,29 @@ export class NewQueryRunner extends QueryRunner {
};
await this.qs.sendRequest(clearCache, params, token, progress);
}
async compileAndRunQueryAgainstDatabase(
dbItem: DatabaseItem,
initialInfo: InitialQueryInfo,
queryStorageDir: string,
public async compileAndRunQueryAgainstDatabaseCore(
dbPath: string,
query: CoreQueryTarget,
additionalPacks: string[],
generateEvalLog: boolean,
outputDir: QueryOutputDir,
progress: ProgressCallback,
token: CancellationToken,
templates?: Record<string, string>,
queryInfo?: LocalQueryInfo,
): Promise<QueryWithResults> {
return await compileAndRunQueryAgainstDatabase(
this.qs.cliServer,
templates: Record<string, string> | undefined,
logger: Logger,
): Promise<CoreQueryResults> {
return await compileAndRunQueryAgainstDatabaseCore(
this.qs,
dbItem,
initialInfo,
queryStorageDir,
dbPath,
query,
generateEvalLog,
additionalPacks,
outputDir,
progress,
token,
templates,
queryInfo,
logger,
);
}

View File

@@ -1,21 +1,10 @@
import { join } from "path";
import { CancellationToken } from "vscode";
import * as cli from "../cli";
import { ProgressCallback } from "../progress";
import { DatabaseItem } from "../local-databases";
import {
getOnDiskWorkspaceFolders,
showAndLogExceptionWithTelemetry,
showAndLogWarningMessage,
tryGetQueryMetadata,
} from "../helpers";
import { extLogger, TeeLogger } from "../common";
import * as messages from "../pure/new-messages";
import { QueryResultType } from "../pure/legacy-messages";
import { InitialQueryInfo, LocalQueryInfo } from "../query-results";
import { QueryEvaluationInfo, QueryWithResults } from "../run-queries-shared";
import { QueryOutputDir } from "../run-queries-shared";
import * as qsClient from "./queryserver-client";
import { redactableError } from "../pure/errors";
import { CoreQueryResults, CoreQueryTarget } from "../queryRunner";
import { Logger } from "../common";
/**
* run-queries.ts
@@ -31,140 +20,58 @@ import { redactableError } from "../pure/errors";
* output and results.
*/
export async function compileAndRunQueryAgainstDatabase(
cliServer: cli.CodeQLCliServer,
export async function compileAndRunQueryAgainstDatabaseCore(
qs: qsClient.QueryServerClient,
dbItem: DatabaseItem,
initialInfo: InitialQueryInfo,
queryStorageDir: string,
dbPath: string,
query: CoreQueryTarget,
generateEvalLog: boolean,
additionalPacks: string[],
outputDir: QueryOutputDir,
progress: ProgressCallback,
token: CancellationToken,
templates?: Record<string, string>,
queryInfo?: LocalQueryInfo, // May be omitted for queries not initiated by the user. If omitted we won't create a structured log for the query.
): Promise<QueryWithResults> {
if (!dbItem.contents || !dbItem.contents.dbSchemeUri) {
throw new Error(
`Database ${dbItem.databaseUri} does not have a CodeQL database scheme.`,
);
}
templates: Record<string, string> | undefined,
logger: Logger,
): Promise<CoreQueryResults> {
const target =
query.quickEvalPosition !== undefined
? {
quickEval: { quickEvalPos: query.quickEvalPosition },
}
: { query: {} };
// Read the query metadata if possible, to use in the UI.
const metadata = await tryGetQueryMetadata(cliServer, initialInfo.queryPath);
const hasMetadataFile = await dbItem.hasMetadataFile();
const query = new QueryEvaluationInfo(
join(queryStorageDir, initialInfo.id),
dbItem.databaseUri.fsPath,
hasMetadataFile,
initialInfo.quickEvalPosition,
metadata,
);
if (!dbItem.contents || dbItem.error) {
throw new Error("Can't run query on invalid database.");
}
const target = query.quickEvalPosition
? {
quickEval: { quickEvalPos: query.quickEvalPosition },
}
: { query: {} };
const diskWorkspaceFolders = getOnDiskWorkspaceFolders();
const extensionPacks = (await qs.cliServer.useExtensionPacks())
? Object.keys(await qs.cliServer.resolveQlpacks(diskWorkspaceFolders, true))
? Object.keys(await qs.cliServer.resolveQlpacks(additionalPacks, true))
: undefined;
const db = dbItem.databaseUri.fsPath;
const logPath = queryInfo ? query.evalLogPath : undefined;
const evalLogPath = generateEvalLog ? outputDir.evalLogPath : undefined;
const queryToRun: messages.RunQueryParams = {
db,
additionalPacks: diskWorkspaceFolders,
db: dbPath,
additionalPacks,
externalInputs: {},
singletonExternalInputs: templates || {},
outputPath: query.resultsPaths.resultsPath,
queryPath: initialInfo.queryPath,
dilPath: query.dilPath,
logPath,
outputPath: outputDir.bqrsPath,
queryPath: query.queryPath,
dilPath: outputDir.dilPath,
logPath: evalLogPath,
target,
extensionPacks,
};
const logger = new TeeLogger(qs.logger, query.logPath);
await query.createTimestampFile();
let result: messages.RunQueryResult | undefined;
try {
// Update the active query logger every time there is a new request to compile.
// This isn't ideal because in situations where there are queries running
// in parallel, each query's log messages are interleaved. Fixing this
// properly will require a change in the query server.
qs.activeQueryLogger = logger;
result = await qs.sendRequest(
messages.runQuery,
queryToRun,
token,
progress,
);
if (qs.config.customLogDirectory) {
void showAndLogWarningMessage(
`Custom log directories are no longer supported. The "codeQL.runningQueries.customLogDirectory" setting is deprecated. Unset the setting to stop seeing this message. Query logs saved to ${query.logPath}.`,
);
}
} finally {
if (queryInfo) {
if (await query.hasEvalLog()) {
await query.addQueryLogs(queryInfo, qs.cliServer, logger);
} else {
void showAndLogWarningMessage(
`Failed to write structured evaluator log to ${query.evalLogPath}.`,
);
}
}
}
if (result.resultType !== messages.QueryResultType.SUCCESS) {
const message = result?.message
? redactableError`${result.message}`
: redactableError`Failed to run query`;
void extLogger.log(message.fullMessage);
void showAndLogExceptionWithTelemetry(
redactableError`Failed to run query: ${message}`,
);
}
let message;
switch (result.resultType) {
case messages.QueryResultType.CANCELLATION:
message = `cancelled after ${Math.round(
result.evaluationTime / 1000,
)} seconds`;
break;
case messages.QueryResultType.OOM:
message = "out of memory";
break;
case messages.QueryResultType.SUCCESS:
message = `finished in ${Math.round(
result.evaluationTime / 1000,
)} seconds`;
break;
case messages.QueryResultType.COMPILATION_ERROR:
message = `compilation failed: ${result.message}`;
break;
case messages.QueryResultType.OTHER_ERROR:
default:
message = result.message ? `failed: ${result.message}` : "failed";
break;
}
const successful = result.resultType === messages.QueryResultType.SUCCESS;
// Update the active query logger every time there is a new request to compile.
// This isn't ideal because in situations where there are queries running
// in parallel, each query's log messages are interleaved. Fixing this
// properly will require a change in the query server.
qs.activeQueryLogger = logger;
const result = await qs.sendRequest(
messages.runQuery,
queryToRun,
token,
progress,
);
return {
query,
result: {
evaluationTime: result.evaluationTime,
queryId: 0,
resultType: successful
? QueryResultType.SUCCESS
: QueryResultType.OTHER_ERROR,
runId: 0,
message,
},
message,
successful,
resultType: result.resultType,
message: result.message,
evaluationTime: result.evaluationTime,
};
}

View File

@@ -2,8 +2,44 @@ import { CancellationToken } from "vscode";
import { CodeQLCliServer } from "./cli";
import { ProgressCallback } from "./progress";
import { DatabaseItem } from "./local-databases";
import { InitialQueryInfo, LocalQueryInfo } from "./query-results";
import { QueryWithResults } from "./run-queries-shared";
import { QueryOutputDir } from "./run-queries-shared";
import { Position, QueryResultType } from "./pure/new-messages";
import { BaseLogger, Logger } from "./common";
import { basename, join } from "path";
import { nanoid } from "nanoid";
export interface CoreQueryTarget {
/** The full path to the query. */
queryPath: string;
/**
* Optional position of text to be used as QuickEval target. This need not be in the same file as
* `query`.
*/
quickEvalPosition?: Position;
}
export interface CoreQueryResults {
readonly resultType: QueryResultType;
readonly message: string | undefined;
readonly evaluationTime: number;
}
export interface CoreQueryRun {
readonly queryTarget: CoreQueryTarget;
readonly dbPath: string;
readonly id: string;
readonly outputDir: QueryOutputDir;
evaluate(
progress: ProgressCallback,
token: CancellationToken,
logger: BaseLogger,
): Promise<CoreCompletedQuery>;
}
/** Includes both the results of the query and the initial information from `CoreQueryRun`. */
export type CoreCompletedQuery = CoreQueryResults &
Omit<CoreQueryRun, "evaluate">;
export abstract class QueryRunner {
abstract restartQueryServer(
@@ -12,6 +48,8 @@ export abstract class QueryRunner {
): Promise<void>;
abstract cliServer: CodeQLCliServer;
abstract customLogDirectory: string | undefined;
abstract logger: Logger;
abstract onStart(
arg0: (
@@ -25,15 +63,20 @@ export abstract class QueryRunner {
token: CancellationToken,
): Promise<void>;
abstract compileAndRunQueryAgainstDatabase(
dbItem: DatabaseItem,
initialInfo: InitialQueryInfo,
queryStorageDir: string,
/**
* Overridden in subclasses to evaluate the query via the query server and return the results.
*/
public abstract compileAndRunQueryAgainstDatabaseCore(
dbPath: string,
query: CoreQueryTarget,
additionalPacks: string[],
generateEvalLog: boolean,
outputDir: QueryOutputDir,
progress: ProgressCallback,
token: CancellationToken,
templates?: Record<string, string>,
queryInfo?: LocalQueryInfo, // May be omitted for queries not initiated by the user. If omitted we won't create a structured log for the query.
): Promise<QueryWithResults>;
templates: Record<string, string> | undefined,
logger: BaseLogger,
): Promise<CoreQueryResults>;
abstract deregisterDatabase(
progress: ProgressCallback,
@@ -54,4 +97,52 @@ export abstract class QueryRunner {
): Promise<void>;
abstract clearPackCache(): Promise<void>;
/**
* Create a `CoreQueryRun` object. This creates an object whose `evaluate()` function can be
* called to actually evaluate the query. The returned object also contains information about the
* query evaluation that is known even before evaluation starts, including the unique ID of the
* evaluation and the path to its output directory.
*/
public createQueryRun(
dbPath: string,
query: CoreQueryTarget,
generateEvalLog: boolean,
additionalPacks: string[],
queryStorageDir: string,
id = `${basename(query.queryPath)}-${nanoid()}`,
templates: Record<string, string> | undefined,
): CoreQueryRun {
const outputDir = new QueryOutputDir(join(queryStorageDir, id));
return {
queryTarget: query,
dbPath,
id,
outputDir,
evaluate: async (
progress: ProgressCallback,
token: CancellationToken,
logger: BaseLogger,
): Promise<CoreCompletedQuery> => {
return {
id,
outputDir,
dbPath,
queryTarget: query,
...(await this.compileAndRunQueryAgainstDatabaseCore(
dbPath,
query,
additionalPacks,
generateEvalLog,
outputDir,
progress,
token,
templates,
logger,
)),
};
},
};
}
}

View File

@@ -20,18 +20,14 @@ import {
remove,
readdir,
} from "fs-extra";
import {
ensureMetadataIsComplete,
InitialQueryInfo,
LocalQueryInfo,
} from "./query-results";
import { ensureMetadataIsComplete, InitialQueryInfo } from "./query-results";
import { isQuickQueryPath } from "./quick-query";
import { nanoid } from "nanoid";
import { CodeQLCliServer } from "./cli";
import { SELECT_QUERY_NAME } from "./contextual/locationFinder";
import { DatabaseManager } from "./local-databases";
import { DecodedBqrsChunk, EntityValue } from "./pure/bqrs-cli-types";
import { extLogger, Logger } from "./common";
import { BaseLogger, extLogger } from "./common";
import { generateSummarySymbolsFile } from "./log-insights/summary-parser";
import { getErrorMessage } from "./pure/helpers-pure";
@@ -42,7 +38,18 @@ import { getErrorMessage } from "./pure/helpers-pure";
* Compiling and running QL queries.
*/
export function findQueryLogFile(resultPath: string): string {
/**
* Holds the paths to the various structured log summary files generated for a query evaluation.
*/
export interface EvaluatorLogPaths {
log: string;
humanReadableSummary: string | undefined;
endSummary: string | undefined;
jsonSummary: string | undefined;
summarySymbols: string | undefined;
}
function findQueryLogFile(resultPath: string): string {
return join(resultPath, "query.log");
}
@@ -66,20 +73,11 @@ function findQueryEvalLogEndSummaryFile(resultPath: string): string {
return join(resultPath, "evaluator-log-end.summary");
}
export class QueryEvaluationInfo {
/**
* Note that in the {@link readQueryHistoryFromFile} method, we create a QueryEvaluationInfo instance
* by explicitly setting the prototype in order to avoid calling this constructor.
*/
constructor(
public readonly querySaveDir: string,
public readonly dbItemPath: string,
private readonly databaseHasMetadataFile: boolean,
public readonly quickEvalPosition?: messages.Position,
public readonly metadata?: QueryMetadata,
) {
/**/
}
/**
* Provides paths to the files that can be generated in the output directory for a query evaluation.
*/
export class QueryOutputDir {
constructor(public readonly querySaveDir: string) {}
get dilPath() {
return join(this.querySaveDir, "results.dil");
@@ -120,9 +118,35 @@ export class QueryEvaluationInfo {
return findQueryEvalLogEndSummaryFile(this.querySaveDir);
}
get bqrsPath() {
return join(this.querySaveDir, "results.bqrs");
}
}
export class QueryEvaluationInfo extends QueryOutputDir {
// We extend `QueryOutputDir`, rather than having it as a property, because we need
// `QueryOutputDir`'s `querySaveDir` property to be a property of `QueryEvaluationInfo`. This is
// because `QueryEvaluationInfo` is serialized directly as JSON, and before we hoisted
// `QueryOutputDir` out into a base class, `querySaveDir` was a property on `QueryEvaluationInfo`
// itself.
/**
* Note that in the {@link readQueryHistoryFromFile} method, we create a QueryEvaluationInfo instance
* by explicitly setting the prototype in order to avoid calling this constructor.
*/
constructor(
querySaveDir: string,
public readonly dbItemPath: string,
private readonly databaseHasMetadataFile: boolean,
public readonly quickEvalPosition?: messages.Position,
public readonly metadata?: QueryMetadata,
) {
super(querySaveDir);
}
get resultsPaths() {
return {
resultsPath: join(this.querySaveDir, "results.bqrs"),
resultsPath: this.bqrsPath,
interpretedResultsPath: join(
this.querySaveDir,
this.metadata?.kind === "graph"
@@ -228,85 +252,6 @@ export class QueryEvaluationInfo {
return pathExists(this.evalLogPath);
}
/**
* Add the structured evaluator log to the query evaluation info.
*/
async addQueryLogs(
queryInfo: LocalQueryInfo,
cliServer: CodeQLCliServer,
logger: Logger,
) {
queryInfo.evalLogLocation = this.evalLogPath;
queryInfo.evalLogSummaryLocation =
await this.generateHumanReadableLogSummary(cliServer);
void this.logEndSummary(queryInfo.evalLogSummaryLocation, logger); // Logged asynchrnously
if (isCanary()) {
// Generate JSON summary for viewer.
await cliServer.generateJsonLogSummary(
this.evalLogPath,
this.jsonEvalLogSummaryPath,
);
queryInfo.jsonEvalLogSummaryLocation = this.jsonEvalLogSummaryPath;
await generateSummarySymbolsFile(
this.evalLogSummaryPath,
this.evalLogSummarySymbolsPath,
);
queryInfo.evalLogSummarySymbolsLocation = this.evalLogSummarySymbolsPath;
}
}
/**
* Calls the appropriate CLI command to generate a human-readable log summary.
* @param qs The query server client.
* @returns The path to the log summary, or `undefined` if the summary could not be generated. */
private async generateHumanReadableLogSummary(
cliServer: CodeQLCliServer,
): Promise<string | undefined> {
try {
await cliServer.generateLogSummary(
this.evalLogPath,
this.evalLogSummaryPath,
this.evalLogEndSummaryPath,
);
return this.evalLogSummaryPath;
} catch (e) {
void showAndLogWarningMessage(
`Failed to generate human-readable structured evaluator log summary. Reason: ${getErrorMessage(
e,
)}`,
);
return undefined;
}
}
/**
* Logs the end summary to the Output window and log file.
* @param logSummaryPath Path to the human-readable log summary
* @param qs The query server client.
*/
private async logEndSummary(
logSummaryPath: string | undefined,
logger: Logger,
): Promise<void> {
if (logSummaryPath === undefined) {
// Failed to generate the log, so we don't expect an end summary either.
return;
}
try {
const endSummaryContent = await readFile(
this.evalLogEndSummaryPath,
"utf-8",
);
void logger.log(" --- Evaluator Log Summary --- ");
void logger.log(endSummaryContent);
} catch (e) {
void showAndLogWarningMessage(
`Could not read structured evaluator log end of summary file at ${this.evalLogEndSummaryPath}.`,
);
}
}
/**
* Creates the CSV file containing the results of this query. This will only be called if the query
* does not have interpreted results and the CSV file does not already exist.
@@ -447,7 +392,7 @@ export interface QueryWithResults {
* Information about which query will be to be run. `quickEvalPosition` and `quickEvalText`
* is only filled in if the query is a quick query.
*/
interface SelectedQuery {
export interface SelectedQuery {
queryPath: string;
quickEvalPosition?: messages.Position;
quickEvalText?: string;
@@ -642,36 +587,113 @@ async function convertToQlPath(filePath: string): Promise<string> {
* Determines the initial information for a query. This is everything of interest
* we know about this query that is available before it is run.
*
* @param selectedQueryUri The Uri of the document containing the query to be run.
* @param selectedQuery The query to run, including any quickeval info.
* @param databaseInfo The database to run the query against.
* @param isQuickEval true if this is a quick evaluation.
* @param range the selection range of the query to be run. Only used if isQuickEval is true.
* @returns The initial information for the query to be run.
*/
export async function createInitialQueryInfo(
selectedQueryUri: Uri | undefined,
selectedQuery: SelectedQuery,
databaseInfo: DatabaseInfo,
isQuickEval: boolean,
range?: Range,
): Promise<InitialQueryInfo> {
// Determine which query to run, based on the selection and the active editor.
const { queryPath, quickEvalPosition, quickEvalText } =
await determineSelectedQuery(selectedQueryUri, isQuickEval, range);
const isQuickEval = selectedQuery.quickEvalPosition !== undefined;
return {
queryPath,
queryPath: selectedQuery.queryPath,
isQuickEval,
isQuickQuery: isQuickQueryPath(queryPath),
isQuickQuery: isQuickQueryPath(selectedQuery.queryPath),
databaseInfo,
id: `${basename(queryPath)}-${nanoid()}`,
id: `${basename(selectedQuery.queryPath)}-${nanoid()}`,
start: new Date(),
...(isQuickEval
? {
queryText: quickEvalText!, // if this query is quick eval, it must have quick eval text
quickEvalPosition,
queryText: selectedQuery.quickEvalText!, // if this query is quick eval, it must have quick eval text
quickEvalPosition: selectedQuery.quickEvalPosition,
}
: {
queryText: await readFile(queryPath, "utf8"),
queryText: await readFile(selectedQuery.queryPath, "utf8"),
}),
};
}
export async function generateEvalLogSummaries(
cliServer: CodeQLCliServer,
outputDir: QueryOutputDir,
): Promise<EvaluatorLogPaths | undefined> {
const log = outputDir.evalLogPath;
if (!(await pathExists(log))) {
// No raw JSON log, so we can't generate any summaries.
return undefined;
}
let humanReadableSummary: string | undefined = undefined;
let endSummary: string | undefined = undefined;
if (await generateHumanReadableLogSummary(cliServer, outputDir)) {
humanReadableSummary = outputDir.evalLogSummaryPath;
endSummary = outputDir.evalLogEndSummaryPath;
}
let jsonSummary: string | undefined = undefined;
let summarySymbols: string | undefined = undefined;
if (isCanary()) {
// Generate JSON summary for viewer.
jsonSummary = outputDir.jsonEvalLogSummaryPath;
await cliServer.generateJsonLogSummary(log, jsonSummary);
if (humanReadableSummary !== undefined) {
summarySymbols = outputDir.evalLogSummarySymbolsPath;
await generateSummarySymbolsFile(humanReadableSummary, summarySymbols);
}
}
return {
log,
humanReadableSummary,
endSummary,
jsonSummary,
summarySymbols,
};
}
/**
* Calls the appropriate CLI command to generate a human-readable log summary.
* @param cliServer The cli server client.
* @param outputDir The query's output directory, where all of the logs are located.
* @returns True if the summary and end summary were generated, or false if not.
*/
async function generateHumanReadableLogSummary(
cliServer: CodeQLCliServer,
outputDir: QueryOutputDir,
): Promise<boolean> {
try {
await cliServer.generateLogSummary(
outputDir.evalLogPath,
outputDir.evalLogSummaryPath,
outputDir.evalLogEndSummaryPath,
);
return true;
} catch (e) {
void showAndLogWarningMessage(
`Failed to generate human-readable structured evaluator log summary. Reason: ${getErrorMessage(
e,
)}`,
);
return false;
}
}
/**
* Logs the end summary to the Output window and log file.
* @param logSummaryPath Path to the human-readable log summary
* @param qs The query server client.
*/
export async function logEndSummary(
endSummary: string,
logger: BaseLogger,
): Promise<void> {
try {
const endSummaryContent = await readFile(endSummary, "utf-8");
void logger.log(" --- Evaluator Log Summary --- ");
void logger.log(endSummaryContent);
} catch (e) {
void showAndLogWarningMessage(
`Could not read structured evaluator log end of summary file at ${endSummary}.`,
);
}
}

View File

@@ -1,7 +1,12 @@
import { readdirSync, readFileSync } from "fs-extra";
import { join } from "path";
import * as tmp from "tmp";
import { Logger, OutputChannelLogger, TeeLogger } from "../../../src/common";
import {
BaseLogger,
Logger,
OutputChannelLogger,
TeeLogger,
} from "../../../src/common";
jest.setTimeout(999999);
@@ -88,7 +93,7 @@ describe("OutputChannelLogger tests", function () {
function createSideLogger(
logger: Logger,
additionalLogLocation: string,
): Logger {
): BaseLogger {
return new TeeLogger(
logger,
join(tempFolders.storagePath.name, additionalLogLocation),

View File

@@ -19,12 +19,11 @@ import {
import { importArchiveDatabase } from "../../../src/databaseFetcher";
import { CliVersionConstraint, CodeQLCliServer } from "../../../src/cli";
import { describeWithCodeQL } from "../cli";
import { tmpDir } from "../../../src/helpers";
import { createInitialQueryInfo } from "../../../src/run-queries-shared";
import { QueryRunner } from "../../../src/queryRunner";
import { CompletedQueryInfo } from "../../../src/query-results";
import { SELECT_QUERY_NAME } from "../../../src/contextual/locationFinder";
import { createMockCommandManager } from "../../__mocks__/commandsMock";
import { LocalQueries } from "../../../src/local-queries";
import { QueryResultType } from "../../../src/pure/new-messages";
import { createVSCodeCommandManager } from "../../../src/common/vscode/commands";
import { AllCommands, QueryServerCommands } from "../../../src/common/commands";
@@ -38,6 +37,7 @@ describeWithCodeQL()("Queries", () => {
let databaseManager: DatabaseManager;
let cli: CodeQLCliServer;
let qs: QueryRunner;
let localQueries: LocalQueries;
const progress = jest.fn();
let token: CancellationToken;
let ctx: ExtensionContext;
@@ -55,6 +55,7 @@ describeWithCodeQL()("Queries", () => {
databaseManager = extension.databaseManager;
cli = extension.cliServer;
qs = extension.qs;
localQueries = extension.localQueries;
cli.quiet = true;
ctx = extension.ctx;
qlpackFile = `${ctx.storageUri?.fsPath}/quick-queries/qlpack.yml`;
@@ -66,7 +67,11 @@ describeWithCodeQL()("Queries", () => {
safeDel(qlFile);
safeDel(qlpackFile);
token = {} as CancellationToken;
token = {
onCancellationRequested: (_) => {
void _;
},
} as CancellationToken;
// Add a database, but make sure the database manager is empty first
await cleanDatabases(databaseManager);
@@ -136,22 +141,21 @@ describeWithCodeQL()("Queries", () => {
}
async function runQueryWithExtensions() {
const result = new CompletedQueryInfo(
await qs.compileAndRunQueryAgainstDatabase(
dbItem,
await mockInitialQueryInfo(queryUsingExtensionPath),
join(tmpDir.name, "mock-storage-path"),
progress,
token,
),
const result = await localQueries.compileAndRunQueryInternal(
false,
Uri.file(queryUsingExtensionPath),
progress,
token,
dbItem,
undefined,
);
// Check that query was successful
expect(result.successful).toBe(true);
expect(result.resultType).toBe(QueryResultType.SUCCESS);
// Load query results
const chunk = await qs.cliServer.bqrsDecode(
result.getResultsPath(SELECT_QUERY_NAME, true),
result.outputDir.bqrsPath,
SELECT_QUERY_NAME,
{
// there should only be one result
@@ -167,31 +171,33 @@ describeWithCodeQL()("Queries", () => {
it("should run a query", async () => {
const queryPath = join(__dirname, "data", "simple-query.ql");
const result = qs.compileAndRunQueryAgainstDatabase(
dbItem,
await mockInitialQueryInfo(queryPath),
join(tmpDir.name, "mock-storage-path"),
const result = await localQueries.compileAndRunQueryInternal(
false,
Uri.file(queryPath),
progress,
token,
dbItem,
undefined,
);
// just check that the query was successful
expect((await result).successful).toBe(true);
expect(result.resultType).toBe(QueryResultType.SUCCESS);
});
// Asserts a fix for bug https://github.com/github/vscode-codeql/issues/733
it("should restart the database and run a query", async () => {
await appCommandManager.execute("codeQL.restartQueryServer");
const queryPath = join(__dirname, "data", "simple-query.ql");
const result = await qs.compileAndRunQueryAgainstDatabase(
dbItem,
await mockInitialQueryInfo(queryPath),
join(tmpDir.name, "mock-storage-path"),
const result = await localQueries.compileAndRunQueryInternal(
false,
Uri.file(queryPath),
progress,
token,
dbItem,
undefined,
);
expect(result.successful).toBe(true);
expect(result.resultType).toBe(QueryResultType.SUCCESS);
});
it("should create a quick query", async () => {
@@ -241,15 +247,4 @@ describeWithCodeQL()("Queries", () => {
// ignore
}
}
async function mockInitialQueryInfo(queryPath: string) {
return await createInitialQueryInfo(
Uri.file(queryPath),
{
name: dbItem.name,
databaseUri: dbItem.databaseUri.toString(),
},
false,
);
}
});

View File

@@ -5,6 +5,7 @@ import { CancellationToken, ExtensionContext, Uri, workspace } from "vscode";
import {
DatabaseContents,
DatabaseContentsWithDbScheme,
DatabaseEventKind,
DatabaseItemImpl,
DatabaseManager,
@@ -687,7 +688,7 @@ describe("local databases", () => {
resolveDatabaseContentsSpy = jest
.spyOn(DatabaseResolver, "resolveDatabaseContents")
.mockResolvedValue({} as DatabaseContents);
.mockResolvedValue({} as DatabaseContentsWithDbScheme);
addDatabaseSourceArchiveFolderSpy = jest.spyOn(
databaseManager,

View File

@@ -3,8 +3,9 @@ import { readFileSync } from "fs-extra";
import AstBuilder from "../../../../src/contextual/astBuilder";
import { CodeQLCliServer } from "../../../../src/cli";
import { Uri } from "vscode";
import { QueryWithResults } from "../../../../src/run-queries-shared";
import { QueryOutputDir } from "../../../../src/run-queries-shared";
import { mockDatabaseItem, mockedObject } from "../../utils/mocking.helpers";
import path from "path";
/**
*
@@ -52,11 +53,12 @@ describe("AstBuilder", () => {
const astBuilder = createAstBuilder();
const roots = await astBuilder.getRoots();
const bqrsPath = path.normalize("/a/b/c/results.bqrs");
const options = { entities: ["id", "url", "string"] };
expect(mockCli.bqrsDecode).toBeCalledWith("/a/b/c", "nodes", options);
expect(mockCli.bqrsDecode).toBeCalledWith("/a/b/c", "edges", options);
expect(mockCli.bqrsDecode).toBeCalledWith(bqrsPath, "nodes", options);
expect(mockCli.bqrsDecode).toBeCalledWith(bqrsPath, "edges", options);
expect(mockCli.bqrsDecode).toBeCalledWith(
"/a/b/c",
bqrsPath,
"graphProperties",
options,
);
@@ -137,13 +139,7 @@ describe("AstBuilder", () => {
function createAstBuilder() {
return new AstBuilder(
{
query: {
resultsPaths: {
resultsPath: "/a/b/c",
},
},
} as QueryWithResults,
new QueryOutputDir("/a/b/c"),
mockCli,
mockDatabaseItem({
resolveSourceFile: undefined,

View File

@@ -13,7 +13,10 @@ import { tmpDir } from "../../../src/helpers";
import { QueryServerClient } from "../../../src/legacy-query-server/queryserver-client";
import { CodeQLCliServer } from "../../../src/cli";
import { SELECT_QUERY_NAME } from "../../../src/contextual/locationFinder";
import { QueryInProgress } from "../../../src/legacy-query-server/run-queries";
import {
QueryInProgress,
compileQuery as compileQueryLegacy,
} from "../../../src/legacy-query-server/run-queries";
import { LegacyQueryRunner } from "../../../src/legacy-query-server/legacyRunner";
import { DatabaseItem } from "../../../src/local-databases";
import { DeepPartial, mockedObject } from "../utils/mocking.helpers";
@@ -30,7 +33,6 @@ describe("run-queries", () => {
const saveDir = "query-save-dir";
const info = createMockQueryInfo(true, saveDir);
expect(info.compiledQueryPath).toBe(join(saveDir, "compiledQuery.qlo"));
expect(info.queryEvalInfo.dilPath).toBe(join(saveDir, "results.dil"));
expect(info.queryEvalInfo.resultsPaths.resultsPath).toBe(
join(saveDir, "results.bqrs"),
@@ -185,14 +187,15 @@ describe("run-queries", () => {
queryPath: "",
};
const results = await info.compile(
const results = await compileQueryLegacy(
qs as any,
mockQlProgram,
undefined,
info.queryEvalInfo,
mockProgress as any,
mockCancel as any,
qs.logger,
);
expect(results).toEqual([{ message: "err", severity: Severity.ERROR }]);
expect(qs.sendRequest).toHaveBeenCalledTimes(1);
@@ -214,7 +217,7 @@ describe("run-queries", () => {
timeoutSecs: 5,
},
queryToCheck: mockQlProgram,
resultPath: info.compiledQueryPath,
resultPath: info.queryEvalInfo.compileQueryPath,
target: { query: {} },
},
mockCancel,