Skip to content

Commit 8396655

Browse files
committed
Avoid clobbering quick-query file when re-opened
Only recreate the qlpack.yml file. Also, add an integration test for quick-query creation.
1 parent ab31d86 commit 8396655

4 files changed

Lines changed: 123 additions & 47 deletions

File tree

extensions/ql-vscode/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- Add better error messages when AST Viewer is unable to create an AST. [#753](https://github.com/github/vscode-codeql/pull/753)
77
- 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)
88
- Ensure CodeQL version in status bar updates correctly when version changes. [#754](https://github.com/github/vscode-codeql/pull/754)
9+
- Avoid deleting the quick query file when it is re-opened. [#747](https://github.com/github/vscode-codeql/pull/747)
910

1011
## 1.4.2 - 2 February 2021
1112

Lines changed: 60 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
import * as fs from 'fs-extra';
22
import * as yaml from 'js-yaml';
33
import * as path from 'path';
4-
import { CancellationToken, ExtensionContext, window as Window, workspace, Uri } from 'vscode';
4+
import {
5+
CancellationToken,
6+
ExtensionContext,
7+
window as Window,
8+
workspace,
9+
Uri
10+
} from 'vscode';
511
import { ErrorCodes, ResponseError } from 'vscode-languageclient';
612
import { CodeQLCliServer } from './cli';
713
import { DatabaseUI } from './databases-ui';
8-
import { logger } from './logging';
914
import {
1015
getInitialQueryContents,
1116
getPrimaryDbscheme,
1217
getQlPackForDbscheme,
13-
showAndLogErrorMessage,
1418
showBinaryChoiceDialog,
1519
} from './helpers';
1620
import {
@@ -21,23 +25,35 @@ import {
2125
const QUICK_QUERIES_DIR_NAME = 'quick-queries';
2226
const QUICK_QUERY_QUERY_NAME = 'quick-query.ql';
2327
const QUICK_QUERY_WORKSPACE_FOLDER_NAME = 'Quick Queries';
28+
const QLPACK_FILE_HEADER = '# This is an automatically generated file.\n\n';
2429

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

29-
function getQuickQueriesDir(ctx: ExtensionContext): string {
34+
async function getQuickQueriesDir(ctx: ExtensionContext): Promise<string> {
3035
const storagePath = ctx.storagePath;
3136
if (storagePath === undefined) {
3237
throw new Error('Workspace storage path is undefined');
3338
}
3439
const queriesPath = path.join(storagePath, QUICK_QUERIES_DIR_NAME);
35-
fs.ensureDir(queriesPath, { mode: 0o700 });
40+
await fs.ensureDir(queriesPath, { mode: 0o700 });
3641
return queriesPath;
3742
}
3843

44+
function updateQuickQueryDir(queriesDir: string, index: number, len: number) {
45+
workspace.updateWorkspaceFolders(
46+
index,
47+
len,
48+
{ uri: Uri.file(queriesDir), name: QUICK_QUERY_WORKSPACE_FOLDER_NAME }
49+
);
50+
}
3951

40-
52+
function findExistingQuickQueryEditor() {
53+
return Window.visibleTextEditors.find(editor =>
54+
path.basename(editor.document.uri.fsPath) === QUICK_QUERY_QUERY_NAME
55+
);
56+
}
4157

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

53-
function updateQuickQueryDir(queriesDir: string, index: number, len: number) {
54-
workspace.updateWorkspaceFolders(
55-
index,
56-
len,
57-
{ uri: Uri.file(queriesDir), name: QUICK_QUERY_WORKSPACE_FOLDER_NAME }
58-
);
59-
}
60-
6169
try {
62-
const workspaceFolders = workspace.workspaceFolders || [];
63-
const queriesDir = await getQuickQueriesDir(ctx);
64-
6570
// If there is already a quick query open, don't clobber it, just
6671
// show it.
67-
const existing = workspace.textDocuments.find(doc => path.basename(doc.uri.fsPath) === QUICK_QUERY_QUERY_NAME);
68-
if (existing !== undefined) {
69-
Window.showTextDocument(existing);
72+
const existing = findExistingQuickQueryEditor();
73+
if (existing) {
74+
await Window.showTextDocument(existing.document);
7075
return;
7176
}
7277

78+
const workspaceFolders = workspace.workspaceFolders || [];
79+
const queriesDir = await getQuickQueriesDir(ctx);
80+
7381
// We need to have a multi-root workspace to make quick query work
7482
// at all. Changing the workspace from single-root to multi-root
7583
// causes a restart of the whole extension host environment, so we
@@ -88,10 +96,11 @@ export async function displayQuickQuery(
8896
}
8997

9098
const index = workspaceFolders.findIndex(folder => folder.name === QUICK_QUERY_WORKSPACE_FOLDER_NAME);
91-
if (index === -1)
99+
if (index === -1) {
92100
updateQuickQueryDir(queriesDir, workspaceFolders.length, 0);
93-
else
101+
} else {
94102
updateQuickQueryDir(queriesDir, index, 1);
103+
}
95104

96105
// We're going to infer which qlpack to use from the current database
97106
const dbItem = await databaseUI.getDatabaseItem(progress, token);
@@ -102,31 +111,38 @@ export async function displayQuickQuery(
102111
const datasetFolder = await dbItem.getDatasetFolder(cliServer);
103112
const dbscheme = await getPrimaryDbscheme(datasetFolder);
104113
const qlpack = await getQlPackForDbscheme(cliServer, dbscheme);
105-
106-
const quickQueryQlpackYaml: any = {
107-
name: 'quick-query',
108-
version: '1.0.0',
109-
libraryPathDependencies: [qlpack]
110-
};
111-
112-
const qlFile = path.join(queriesDir, QUICK_QUERY_QUERY_NAME);
113114
const qlPackFile = path.join(queriesDir, 'qlpack.yml');
114-
await fs.writeFile(qlFile, getInitialQueryContents(dbItem.language, dbscheme), 'utf8');
115-
await fs.writeFile(qlPackFile, yaml.safeDump(quickQueryQlpackYaml), 'utf8');
116-
Window.showTextDocument(await workspace.openTextDocument(qlFile));
117-
}
118-
119-
// TODO: clean up error handling for top-level commands like this
120-
catch (e) {
121-
if (e instanceof UserCancellationException) {
122-
logger.log(e.message);
115+
const qlFile = path.join(queriesDir, QUICK_QUERY_QUERY_NAME);
116+
const shouldRewrite = await checkShouldRewrite(qlPackFile, qlpack);
117+
118+
// Only rewrite the qlpack file if the database has changed
119+
if (shouldRewrite) {
120+
const quickQueryQlpackYaml: any = {
121+
name: 'quick-query',
122+
version: '1.0.0',
123+
libraryPathDependencies: [qlpack]
124+
};
125+
await fs.writeFile(qlPackFile, QLPACK_FILE_HEADER + yaml.safeDump(quickQueryQlpackYaml), 'utf8');
123126
}
124-
else if (e instanceof ResponseError && e.code == ErrorCodes.RequestCancelled) {
125-
logger.log(e.message);
127+
128+
if (shouldRewrite || !(await fs.pathExists(qlFile))) {
129+
await fs.writeFile(qlFile, getInitialQueryContents(dbItem.language, dbscheme), 'utf8');
126130
}
127-
else if (e instanceof Error)
128-
showAndLogErrorMessage(e.message);
129-
else
131+
132+
await Window.showTextDocument(await workspace.openTextDocument(qlFile));
133+
} catch (e) {
134+
if (e instanceof ResponseError && e.code == ErrorCodes.RequestCancelled) {
135+
throw new UserCancellationException(e.message);
136+
} else {
130137
throw e;
138+
}
139+
}
140+
}
141+
142+
async function checkShouldRewrite(qlPackFile: string, newDependency: string) {
143+
if (!(await fs.pathExists(qlPackFile))) {
144+
return true;
131145
}
146+
const qlPackContents: any = yaml.safeLoad(await fs.readFile(qlPackFile, 'utf8'));
147+
return qlPackContents.libraryPathDependencies?.[0] !== newDependency;
132148
}

extensions/ql-vscode/src/vscode-tests/cli-integration/queries.test.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { fail } from 'assert';
2-
import { CancellationToken, commands, extensions, Uri } from 'vscode';
2+
import { CancellationToken, commands, ExtensionContext, extensions, Uri } from 'vscode';
33
import * as sinon from 'sinon';
44
import * as path from 'path';
55
import * as fs from 'fs-extra';
66
import 'mocha';
77
import { expect } from 'chai';
8+
import * as yaml from 'js-yaml';
89

910
import { DatabaseItem, DatabaseManager } from '../../databases';
1011
import { CodeQLExtensionInterface } from '../../extension';
@@ -34,6 +35,11 @@ describe('Queries', function() {
3435
let sandbox: sinon.SinonSandbox;
3536
let progress: sinon.SinonSpy;
3637
let token: CancellationToken;
38+
let ctx: ExtensionContext;
39+
40+
let qlpackFile: string;
41+
let qlFile: string;
42+
3743

3844
beforeEach(async () => {
3945
sandbox = sinon.createSandbox();
@@ -45,6 +51,9 @@ describe('Queries', function() {
4551
cli = extension.cliServer;
4652
qs = extension.qs;
4753
cli.quiet = true;
54+
ctx = extension.ctx;
55+
qlpackFile = `${ctx.storagePath}/quick-queries/qlpack.yml`;
56+
qlFile = `${ctx.storagePath}/quick-queries/quick-query.ql`;
4857
} else {
4958
throw new Error('Extension not initialized. Make sure cli is downloaded and installed properly.');
5059
}
@@ -126,4 +135,42 @@ describe('Queries', function() {
126135
fail(e);
127136
}
128137
});
138+
139+
it('should create a quick query', async () => {
140+
safeDel(qlFile);
141+
safeDel(qlpackFile);
142+
143+
await commands.executeCommand('codeQL.quickQuery');
144+
145+
// should have created the quick query file and query pack file
146+
expect(fs.pathExistsSync(qlFile)).to.be.true;
147+
expect(fs.pathExistsSync(qlpackFile)).to.be.true;
148+
149+
const qlpackContents: any = await yaml.safeLoad(
150+
fs.readFileSync(qlpackFile, 'utf8')
151+
);
152+
// Should have chosen the js libraries
153+
expect(qlpackContents.libraryPathDependencies[0]).to.eq('codeql-javascript');
154+
});
155+
156+
it('should avoid creating a quick query', async () => {
157+
fs.writeFileSync(qlpackFile, yaml.safeDump({
158+
name: 'quick-query',
159+
version: '1.0.0',
160+
libraryPathDependencies: ['codeql-javascript']
161+
}));
162+
fs.writeFileSync(qlFile, 'xxx');
163+
await commands.executeCommand('codeQL.quickQuery');
164+
165+
// should not have created the quick query file because database schema hasn't changed
166+
expect(fs.readFileSync(qlFile, 'utf8')).to.eq('xxx');
167+
});
168+
169+
function safeDel(file: string) {
170+
try {
171+
fs.unlinkSync(file);
172+
} catch (e) {
173+
// ignore
174+
}
175+
}
129176
});

extensions/ql-vscode/src/vscode-tests/minimal-workspace/config.test.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ describe('config listeners', () => {
2626
});
2727

2828
interface TestConfig<T> {
29-
clazz: new() => {};
29+
clazz: new () => {};
3030
settings: {
3131
name: string;
3232
property: string;
@@ -84,19 +84,31 @@ describe('config listeners', () => {
8484
beforeEach(async () => {
8585
origValue = workspace.getConfiguration().get(setting.name);
8686
await workspace.getConfiguration().update(setting.name, setting.values[0]);
87+
await wait();
8788
spy.resetHistory();
8889
});
8990

9091
afterEach(async () => {
9192
await workspace.getConfiguration().update(setting.name, origValue);
93+
await wait();
9294
});
9395

9496
it(`should listen for changes to '${setting.name}'`, async () => {
9597
await workspace.getConfiguration().update(setting.name, setting.values[1]);
96-
expect(spy.calledOnce).to.be.true;
98+
await wait();
9799
expect(listener[setting.property]).to.eq(setting.values[1]);
100+
expect(spy).to.have.been.calledOnce;
98101
});
99102
});
100103
});
101104
});
105+
106+
// Need to wait some time since the onDidChangeConfiguration listeners fire
107+
// asynchronously and we sometimes need to wait for them to complete in
108+
// order to have as successful test.
109+
async function wait(ms = 50) {
110+
return new Promise(resolve =>
111+
setTimeout(resolve, ms)
112+
);
113+
}
102114
});

0 commit comments

Comments
 (0)