Add command to view the DIL of a query

This commit is contained in:
Andrew Eisenberg 2020-10-02 16:12:03 -07:00
Родитель 8e817ee01a
Коммит 9300c07d42
7 изменённых файлов: 172 добавлений и 2 удалений

Просмотреть файл

@ -12,6 +12,7 @@
- Fix proper escaping of backslashes in SARIF message strings.
- Allow setting `codeQL.runningQueries.numberOfThreads` and `codeQL.runningTests.numberOfThreads` to 0, (which is interpreted as 'use one thread per core on the machine').
- Clear the problems view of all CodeQL query results when a database is removed.
- Add a `View DIL` command on query history items. This opens a text editor containing the Datalog Intermediary Language representation of the compiled query.
## 1.3.3 - 16 September 2020

Просмотреть файл

@ -329,6 +329,10 @@
"command": "codeQLQueryHistory.viewSarif",
"title": "View SARIF"
},
{
"command": "codeQLQueryHistory.viewDil",
"title": "View DIL"
},
{
"command": "codeQLQueryHistory.setLabel",
"title": "Set Label"
@ -484,6 +488,11 @@
"group": "9_qlCommands",
"when": "view == codeQLQueryHistory && viewItem == interpretedResultsItem"
},
{
"command": "codeQLQueryHistory.viewDil",
"group": "9_qlCommands",
"when": "view == codeQLQueryHistory"
},
{
"command": "codeQLTests.showOutputDifferences",
"group": "qltest@1",
@ -600,6 +609,10 @@
"command": "codeQLQueryHistory.viewSarif",
"when": "false"
},
{
"command": "codeQLQueryHistory.viewDil",
"when": "false"
},
{
"command": "codeQLQueryHistory.setLabel",
"when": "false"

Просмотреть файл

@ -650,6 +650,14 @@ export class CodeQLCliServer implements Disposable {
'Resolving queries',
);
}
async generateDil(qloFile: string, outFile: string): Promise<void> {
await this.runCodeQlCliCommand(
['query', 'decompile'],
['--kind', 'dil', '-o', outFile, qloFile],
'Generating DIL',
);
}
}
/**

Просмотреть файл

@ -353,6 +353,7 @@ async function activateWithInstalledDistribution(
const qhm = new QueryHistoryManager(
ctx,
qs,
queryHistoryConfigurationListener,
showResults,
async (from: CompletedQuery, to: CompletedQuery) =>

Просмотреть файл

@ -7,6 +7,7 @@ import { QueryWithResults } from './run-queries';
import * as helpers from './helpers';
import { logger } from './logging';
import { URLSearchParams } from 'url';
import { QueryServerClient } from './queryserver-client';
/**
* query-history.ts
@ -180,6 +181,7 @@ export class QueryHistoryManager {
constructor(
ctx: ExtensionContext,
private qs: QueryServerClient,
private queryHistoryConfigListener: QueryHistoryConfig,
private selectedCallback: (item: CompletedQuery) => Promise<void>,
private doCompareCallback: (
@ -250,6 +252,12 @@ export class QueryHistoryManager {
this.handleViewSarif.bind(this)
)
);
ctx.subscriptions.push(
helpers.commandRunner(
'codeQLQueryHistory.viewDil',
this.handleViewDil.bind(this)
)
);
ctx.subscriptions.push(
helpers.commandRunner(
'codeQLQueryHistory.itemClicked',
@ -459,6 +467,19 @@ export class QueryHistoryManager {
}
}
async handleViewDil(
singleItem: CompletedQuery,
multiSelect: CompletedQuery[],
) {
if (!this.assertSingleQuery(multiSelect)) {
return;
}
await this.tryOpenExternalFile(
await singleItem.query.ensureDilPath(this.qs)
);
}
async getQueryText(queryHistoryItem: CompletedQuery): Promise<string> {
if (queryHistoryItem.options.queryText) {
return queryHistoryItem.options.queryText;
@ -511,7 +532,9 @@ export class QueryHistoryManager {
private async tryOpenExternalFile(fileLocation: string) {
const uri = vscode.Uri.file(fileLocation);
try {
await vscode.window.showTextDocument(uri);
await vscode.window.showTextDocument(uri, {
preview: false
});
} catch (e) {
if (
e.message.includes(

Просмотреть файл

@ -51,6 +51,7 @@ export class QueryInfo {
private static nextQueryId = 0;
readonly compiledQueryPath: string;
readonly dilPath: string;
readonly resultsPaths: ResultsPaths;
readonly dataset: Uri; // guarantee the existence of a well-defined dataset dir at this point
readonly queryID: number;
@ -65,9 +66,10 @@ export class QueryInfo {
) {
this.queryID = QueryInfo.nextQueryId++;
this.compiledQueryPath = path.join(tmpDir.name, `compiledQuery${this.queryID}.qlo`);
this.dilPath = path.join(tmpDir.name, `results${this.queryID}.dil`);
this.resultsPaths = {
resultsPath: path.join(tmpDir.name, `results${this.queryID}.bqrs`),
interpretedResultsPath: path.join(tmpDir.name, `interpretedResults${this.queryID}.sarif`),
interpretedResultsPath: path.join(tmpDir.name, `interpretedResults${this.queryID}.sarif`)
};
if (dbItem.contents === undefined) {
throw new Error('Can\'t run query on invalid database.');
@ -169,6 +171,29 @@ export class QueryInfo {
async hasInterpretedResults(): Promise<boolean> {
return fs.pathExists(this.resultsPaths.interpretedResultsPath);
}
/**
* Holds if this query already has DIL produced
*/
async hasDil(): Promise<boolean> {
return fs.pathExists(this.dilPath);
}
async ensureDilPath(qs: qsClient.QueryServerClient): Promise<string> {
if (await this.hasDil()) {
return this.dilPath;
}
if (!(await fs.pathExists(this.compiledQueryPath))) {
throw new Error(
`Cannot create DIL because compiled query is missing. ${this.compiledQueryPath}`
);
}
await qs.cliServer.generateDil(this.compiledQueryPath, this.dilPath);
return this.dilPath;
}
}
export interface QueryWithResults {

Просмотреть файл

@ -0,0 +1,99 @@
import * as chai from 'chai';
import * as path from 'path';
import 'mocha';
import 'sinon-chai';
import * as sinon from 'sinon';
import * as chaiAsPromised from 'chai-as-promised';
import { QueryInfo } from '../../run-queries';
import { QlProgram, Severity, compileQuery } from '../../messages';
import { DatabaseItem } from '../../databases';
chai.use(chaiAsPromised);
const expect = chai.expect;
describe('run-queries', () => {
it('should create a QueryInfo', () => {
const info = createMockQueryInfo();
const queryID = info.queryID;
expect(path.basename(info.compiledQueryPath)).to.eq(`compiledQuery${queryID}.qlo`);
expect(path.basename(info.dilPath)).to.eq(`results${queryID}.dil`);
expect(path.basename(info.resultsPaths.resultsPath)).to.eq(`results${queryID}.bqrs`);
expect(path.basename(info.resultsPaths.interpretedResultsPath)).to.eq(`interpretedResults${queryID}.sarif`);
expect(info.dataset).to.eq('file:///abc');
});
describe('compile', () => {
it('should compile', async () => {
const info = createMockQueryInfo();
const qs = createMockQueryServerClient();
const mockProgress = 'progress-monitor';
const mockCancel = 'cancel-token';
const results = await info.compile(
qs as any,
mockProgress as any,
mockCancel as any
);
expect(results).to.deep.eq([
{ message: 'err', severity: Severity.ERROR }
]);
expect(qs.sendRequest).to.have.been.calledOnceWith(
compileQuery,
{
compilationOptions: {
computeNoLocationUrls: true,
failOnWarnings: false,
fastCompilation: false,
includeDilInQlo: true,
localChecking: false,
noComputeGetUrl: false,
noComputeToString: false,
},
extraOptions: {
timeoutSecs: 5
},
queryToCheck: 'my-program',
resultPath: info.compiledQueryPath,
target: { query: {} }
},
mockCancel,
mockProgress
);
});
});
function createMockQueryInfo() {
return new QueryInfo(
'my-program' as unknown as QlProgram,
{
contents: {
datasetUri: 'file:///abc'
}
} as unknown as DatabaseItem,
'my-scheme' // queryDbscheme
);
}
function createMockQueryServerClient() {
return {
config: {
timeoutSecs: 5
},
sendRequest: sinon.stub().returns(new Promise(resolve => {
resolve({
messages: [
{ message: 'err', severity: Severity.ERROR },
{ message: 'warn', severity: Severity.WARNING },
]
});
})),
logger: {
log: sinon.spy()
}
};
}
});