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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions extensions/ql-vscode/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- Add better error messages when AST Viewer is unable to create an AST. [#753](https://github.com/github/vscode-codeql/pull/753)
- Cache AST viewing operations so that subsequent calls to view the AST of a single file will be extremely fast. [#753](https://github.com/github/vscode-codeql/pull/753)
- Ensure CodeQL version in status bar updates correctly when version changes. [#754](https://github.com/github/vscode-codeql/pull/754)
- Avoid deleting the quick query file when it is re-opened. [#747](https://github.com/github/vscode-codeql/pull/747)

## 1.4.2 - 2 February 2021

Expand Down
104 changes: 60 additions & 44 deletions extensions/ql-vscode/src/quick-query.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import * as fs from 'fs-extra';
import * as yaml from 'js-yaml';
import * as path from 'path';
import { CancellationToken, ExtensionContext, window as Window, workspace, Uri } from 'vscode';
import {
CancellationToken,
ExtensionContext,
window as Window,
workspace,
Uri
} from 'vscode';
import { ErrorCodes, ResponseError } from 'vscode-languageclient';
import { CodeQLCliServer } from './cli';
import { DatabaseUI } from './databases-ui';
import { logger } from './logging';
import {
getInitialQueryContents,
getPrimaryDbscheme,
getQlPackForDbscheme,
showAndLogErrorMessage,
showBinaryChoiceDialog,
} from './helpers';
import {
Expand All @@ -21,23 +25,35 @@ import {
const QUICK_QUERIES_DIR_NAME = 'quick-queries';
const QUICK_QUERY_QUERY_NAME = 'quick-query.ql';
const QUICK_QUERY_WORKSPACE_FOLDER_NAME = 'Quick Queries';
const QLPACK_FILE_HEADER = '# This is an automatically generated file.\n\n';

export function isQuickQueryPath(queryPath: string): boolean {
return path.basename(queryPath) === QUICK_QUERY_QUERY_NAME;
}

function getQuickQueriesDir(ctx: ExtensionContext): string {
async function getQuickQueriesDir(ctx: ExtensionContext): Promise<string> {
const storagePath = ctx.storagePath;
if (storagePath === undefined) {
throw new Error('Workspace storage path is undefined');
}
const queriesPath = path.join(storagePath, QUICK_QUERIES_DIR_NAME);
fs.ensureDir(queriesPath, { mode: 0o700 });
await fs.ensureDir(queriesPath, { mode: 0o700 });
return queriesPath;
}

function updateQuickQueryDir(queriesDir: string, index: number, len: number) {
workspace.updateWorkspaceFolders(
index,
len,
{ uri: Uri.file(queriesDir), name: QUICK_QUERY_WORKSPACE_FOLDER_NAME }
);
}


function findExistingQuickQueryEditor() {
return Window.visibleTextEditors.find(editor =>
path.basename(editor.document.uri.fsPath) === QUICK_QUERY_QUERY_NAME
);
}

/**
* Show a buffer the user can enter a simple query into.
Expand All @@ -50,26 +66,18 @@ export async function displayQuickQuery(
token: CancellationToken
) {

function updateQuickQueryDir(queriesDir: string, index: number, len: number) {
workspace.updateWorkspaceFolders(
index,
len,
{ uri: Uri.file(queriesDir), name: QUICK_QUERY_WORKSPACE_FOLDER_NAME }
);
}

try {
const workspaceFolders = workspace.workspaceFolders || [];
const queriesDir = await getQuickQueriesDir(ctx);

// If there is already a quick query open, don't clobber it, just
// show it.
const existing = workspace.textDocuments.find(doc => path.basename(doc.uri.fsPath) === QUICK_QUERY_QUERY_NAME);
if (existing !== undefined) {
Window.showTextDocument(existing);
const existing = findExistingQuickQueryEditor();
if (existing) {
await Window.showTextDocument(existing.document);
return;
}

const workspaceFolders = workspace.workspaceFolders || [];
const queriesDir = await getQuickQueriesDir(ctx);

// We need to have a multi-root workspace to make quick query work
// at all. Changing the workspace from single-root to multi-root
// causes a restart of the whole extension host environment, so we
Expand All @@ -88,10 +96,11 @@ export async function displayQuickQuery(
}

const index = workspaceFolders.findIndex(folder => folder.name === QUICK_QUERY_WORKSPACE_FOLDER_NAME);
if (index === -1)
if (index === -1) {
updateQuickQueryDir(queriesDir, workspaceFolders.length, 0);
else
} else {
updateQuickQueryDir(queriesDir, index, 1);
}

// We're going to infer which qlpack to use from the current database
const dbItem = await databaseUI.getDatabaseItem(progress, token);
Expand All @@ -102,31 +111,38 @@ export async function displayQuickQuery(
const datasetFolder = await dbItem.getDatasetFolder(cliServer);
const dbscheme = await getPrimaryDbscheme(datasetFolder);
const qlpack = await getQlPackForDbscheme(cliServer, dbscheme);

const quickQueryQlpackYaml: any = {
name: 'quick-query',
version: '1.0.0',
libraryPathDependencies: [qlpack]
};

const qlFile = path.join(queriesDir, QUICK_QUERY_QUERY_NAME);
const qlPackFile = path.join(queriesDir, 'qlpack.yml');
await fs.writeFile(qlFile, getInitialQueryContents(dbItem.language, dbscheme), 'utf8');
await fs.writeFile(qlPackFile, yaml.safeDump(quickQueryQlpackYaml), 'utf8');
Window.showTextDocument(await workspace.openTextDocument(qlFile));
}

// TODO: clean up error handling for top-level commands like this
catch (e) {
if (e instanceof UserCancellationException) {
logger.log(e.message);
const qlFile = path.join(queriesDir, QUICK_QUERY_QUERY_NAME);
const shouldRewrite = await checkShouldRewrite(qlPackFile, qlpack);

// Only rewrite the qlpack file if the database has changed
if (shouldRewrite) {
const quickQueryQlpackYaml: any = {
name: 'quick-query',
version: '1.0.0',
libraryPathDependencies: [qlpack]
};
await fs.writeFile(qlPackFile, QLPACK_FILE_HEADER + yaml.safeDump(quickQueryQlpackYaml), 'utf8');
}
else if (e instanceof ResponseError && e.code == ErrorCodes.RequestCancelled) {
logger.log(e.message);

if (shouldRewrite || !(await fs.pathExists(qlFile))) {
await fs.writeFile(qlFile, getInitialQueryContents(dbItem.language, dbscheme), 'utf8');
}
else if (e instanceof Error)
showAndLogErrorMessage(e.message);
else

await Window.showTextDocument(await workspace.openTextDocument(qlFile));
} catch (e) {
if (e instanceof ResponseError && e.code == ErrorCodes.RequestCancelled) {
throw new UserCancellationException(e.message);
} else {
throw e;
}
}
}

async function checkShouldRewrite(qlPackFile: string, newDependency: string) {
if (!(await fs.pathExists(qlPackFile))) {
return true;
}
const qlPackContents: any = yaml.safeLoad(await fs.readFile(qlPackFile, 'utf8'));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth having a simple type here instead of any?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it adds much value here. If we are going to start using qlpack files elsewhere in the code, then yes.

return qlPackContents.libraryPathDependencies?.[0] !== newDependency;
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { fail } from 'assert';
import { CancellationToken, commands, extensions, Uri } from 'vscode';
import { CancellationToken, commands, ExtensionContext, extensions, Uri } from 'vscode';
import * as sinon from 'sinon';
import * as path from 'path';
import * as fs from 'fs-extra';
import 'mocha';
import { expect } from 'chai';
import * as yaml from 'js-yaml';

import { DatabaseItem, DatabaseManager } from '../../databases';
import { CodeQLExtensionInterface } from '../../extension';
Expand Down Expand Up @@ -34,6 +35,11 @@ describe('Queries', function() {
let sandbox: sinon.SinonSandbox;
let progress: sinon.SinonSpy;
let token: CancellationToken;
let ctx: ExtensionContext;

let qlpackFile: string;
let qlFile: string;


beforeEach(async () => {
sandbox = sinon.createSandbox();
Expand All @@ -45,6 +51,9 @@ describe('Queries', function() {
cli = extension.cliServer;
qs = extension.qs;
cli.quiet = true;
ctx = extension.ctx;
qlpackFile = `${ctx.storagePath}/quick-queries/qlpack.yml`;
qlFile = `${ctx.storagePath}/quick-queries/quick-query.ql`;
} else {
throw new Error('Extension not initialized. Make sure cli is downloaded and installed properly.');
}
Expand Down Expand Up @@ -126,4 +135,42 @@ describe('Queries', function() {
fail(e);
}
});

it('should create a quick query', async () => {
safeDel(qlFile);
safeDel(qlpackFile);

await commands.executeCommand('codeQL.quickQuery');

// should have created the quick query file and query pack file
expect(fs.pathExistsSync(qlFile)).to.be.true;
expect(fs.pathExistsSync(qlpackFile)).to.be.true;

const qlpackContents: any = await yaml.safeLoad(
fs.readFileSync(qlpackFile, 'utf8')
);
// Should have chosen the js libraries
expect(qlpackContents.libraryPathDependencies[0]).to.eq('codeql-javascript');
});

it('should avoid creating a quick query', async () => {
fs.writeFileSync(qlpackFile, yaml.safeDump({
name: 'quick-query',
version: '1.0.0',
libraryPathDependencies: ['codeql-javascript']
}));
fs.writeFileSync(qlFile, 'xxx');
await commands.executeCommand('codeQL.quickQuery');

// should not have created the quick query file because database schema hasn't changed
expect(fs.readFileSync(qlFile, 'utf8')).to.eq('xxx');
});

function safeDel(file: string) {
try {
fs.unlinkSync(file);
} catch (e) {
// ignore
}
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ describe('config listeners', () => {
});

interface TestConfig<T> {
clazz: new() => {};
clazz: new () => {};
settings: {
name: string;
property: string;
Expand Down Expand Up @@ -84,19 +84,31 @@ describe('config listeners', () => {
beforeEach(async () => {
origValue = workspace.getConfiguration().get(setting.name);
await workspace.getConfiguration().update(setting.name, setting.values[0]);
await wait();
spy.resetHistory();
});

afterEach(async () => {
await workspace.getConfiguration().update(setting.name, origValue);
await wait();
});

it(`should listen for changes to '${setting.name}'`, async () => {
await workspace.getConfiguration().update(setting.name, setting.values[1]);
expect(spy.calledOnce).to.be.true;
await wait();
expect(listener[setting.property]).to.eq(setting.values[1]);
expect(spy).to.have.been.calledOnce;
});
});
});
});

// Need to wait some time since the onDidChangeConfiguration listeners fire
// asynchronously and we sometimes need to wait for them to complete in
// order to have as successful test.
async function wait(ms = 50) {
return new Promise(resolve =>
setTimeout(resolve, ms)
);
}
});