Skip to content

Commit 742bca1

Browse files
committed
Remote queries: Handle nested queries
This change allows remote queries to run a query from a directory that is not in the root of the qlpack. The change is the following: 1. walk up the directory hierarchy to check for a non-local qlpack.yml 2. Copy over the files as before, but keep track of the relative location of the query compared to the location of the qlpack.yml. 3. Change the defaultSuite of the qlpack.yml so that _only_ this query is run as part of the default query. Also, this adds a new integration test to ensure the nested query is packaged appropriately.
1 parent 3743895 commit 742bca1

7 files changed

Lines changed: 150 additions & 24 deletions

File tree

extensions/ql-vscode/src/run-remote-query.ts

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,19 @@ import { getRemoteControllerRepo, getRemoteRepositoryLists, setRemoteControllerR
1111
import { tmpDir } from './run-queries';
1212
import { ProgressCallback, UserCancellationException } from './commandRunner';
1313
import { OctokitResponse } from '@octokit/types/dist-types';
14-
1514
interface Config {
1615
repositories: string[];
1716
ref?: string;
1817
language?: string;
1918
}
2019

20+
interface QlPack {
21+
name: string;
22+
version: string;
23+
dependencies: { [key: string]: string };
24+
defaultSuite?: Record<string, unknown>;
25+
defaultSuiteFile?: Record<string, unknown>;
26+
}
2127
interface RepoListQuickPickItem extends QuickPickItem {
2228
repoList: string[];
2329
}
@@ -89,13 +95,9 @@ async function generateQueryPack(cliServer: cli.CodeQLCliServer, queryFile: stri
8995
base64Pack: string,
9096
language: string
9197
}> {
92-
const originalPackRoot = path.dirname(queryFile);
93-
// TODO this assumes that the qlpack.yml is in the same directory as the query file, but in reality,
94-
// the file could be in a parent directory.
95-
const targetQueryFileName = path.join(queryPackDir, path.basename(queryFile));
96-
97-
// the server is expecting the query file to be named `query.ql`. Rename it here.
98-
const renamedQueryFile = path.join(queryPackDir, 'query.ql');
98+
const originalPackRoot = await findPackRoot(queryFile);
99+
const packRelativePath = path.relative(originalPackRoot, queryFile);
100+
const targetQueryFileName = path.join(queryPackDir, packRelativePath);
99101

100102
let language: string | undefined;
101103
if (await fs.pathExists(path.join(originalPackRoot, 'qlpack.yml'))) {
@@ -138,8 +140,6 @@ async function generateQueryPack(cliServer: cli.CodeQLCliServer, queryFile: stri
138140

139141
// copy only the query file to the query pack directory
140142
// and generate a synthetic query pack
141-
// TODO this has a limitation that query packs inside of a workspace will not resolve its peer dependencies.
142-
// Something to work on later. For now, we will only support query packs that are not in a workspace.
143143
void logger.log(`Copying ${queryFile} to ${queryPackDir}`);
144144
await fs.copy(queryFile, targetQueryFileName);
145145
void logger.log('Generating synthetic query pack');
@@ -156,7 +156,8 @@ async function generateQueryPack(cliServer: cli.CodeQLCliServer, queryFile: stri
156156
throw new UserCancellationException('Could not determine language.');
157157
}
158158

159-
await fs.rename(targetQueryFileName, renamedQueryFile);
159+
// fix the default suite of the query pack dir
160+
await fixDefaultSuite(queryPackDir, packRelativePath);
160161

161162
const bundlePath = await getPackedBundlePath(queryPackDir);
162163
void logger.log(`Compiling and bundling query pack from ${queryPackDir} to ${bundlePath}. (This may take a while.)`);
@@ -189,6 +190,21 @@ async function ensureQueryPackName(queryPackDir: string) {
189190
}
190191
}
191192

193+
async function findPackRoot(queryFile: string): Promise<string> {
194+
// recursively find the directory containing qlpack.yml
195+
let dir = path.dirname(queryFile);
196+
while (!(await fs.pathExists(path.join(dir, 'qlpack.yml')))) {
197+
dir = path.dirname(dir);
198+
if (dir === '/') {
199+
// there is no qlpack.yml in this direcory or any parent directory.
200+
// just use the query file's directory as the pack root.
201+
return path.dirname(queryFile);
202+
}
203+
}
204+
205+
return dir;
206+
}
207+
192208
async function createRemoteQueriesTempDirectory() {
193209
const remoteQueryDir = await tmp.dir({ dir: tmpDir.name, unsafeCleanup: true });
194210
const queryPackDir = path.join(remoteQueryDir.path, 'query-pack');
@@ -413,3 +429,23 @@ export async function attemptRerun(
413429
void showAndLogErrorMessage(error);
414430
}
415431
}
432+
433+
/**
434+
* Updates the default suite of the query pack. This is used to ensure
435+
* only the specified query is run.
436+
*
437+
* @param queryPackDir The directory containing the query pack
438+
* @param packRelativePath The relative path to the query pack from the root of the query pack
439+
*/
440+
async function fixDefaultSuite(queryPackDir: string, packRelativePath: string): Promise<void> {
441+
const packPath = path.join(queryPackDir, 'qlpack.yml');
442+
const qlpack = (await yaml.safeLoad(await fs.readFile(packPath, 'utf8'))) as QlPack;
443+
delete qlpack.defaultSuite;
444+
delete qlpack.defaultSuiteFile;
445+
446+
qlpack.defaultSuite = {
447+
description: 'Query suite for remote query',
448+
query: packRelativePath
449+
};
450+
await fs.writeFile(packPath, yaml.safeDump(qlpack));
451+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// This file should not be included the remote query pack.
2+
select 1
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
int number() {
2+
result = 1
3+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
name: github/remote-query-pack
2+
version: 0.0.0
3+
dependencies:
4+
codeql/javascript-all: '*'
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import javascript
2+
import otherfolder.lib
3+
4+
select number()
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
name: github/remote-query-pack
22
version: 0.0.0
3-
extractor: javascript
43
dependencies:
54
codeql/javascript-all: '*'

extensions/ql-vscode/src/vscode-tests/cli-integration/run-remote-query.test.ts

Lines changed: 90 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { CodeQLExtensionInterface } from '../../extension';
1313
import { setRemoteControllerRepo, setRemoteRepositoryLists } from '../../config';
1414
import { UserCancellationException } from '../../commandRunner';
1515

16-
describe('Remote queries', function() {
16+
describe.only('Remote queries', function() {
1717
const baseDir = path.join(__dirname, '../../../src/vscode-tests/cli-integration');
1818

1919
let sandbox: sinon.SinonSandbox;
@@ -80,8 +80,7 @@ describe('Remote queries', function() {
8080
const queryPackDir = path.join(queryPackRootDir, 'query-pack');
8181
printDirectoryContents(queryPackDir);
8282

83-
// in-pack.ql renamed to query.ql
84-
expect(fs.existsSync(path.join(queryPackDir, 'query.ql'))).to.be.true;
83+
expect(fs.existsSync(path.join(queryPackDir, 'in-pack.ql'))).to.be.true;
8584
expect(fs.existsSync(path.join(queryPackDir, 'lib.qll'))).to.be.true;
8685
expect(fs.existsSync(path.join(queryPackDir, 'qlpack.yml'))).to.be.true;
8786

@@ -96,7 +95,7 @@ describe('Remote queries', function() {
9695
const compiledPackDir = path.join(queryPackDir, '.codeql/pack/github/remote-query-pack/0.0.0/');
9796
printDirectoryContents(compiledPackDir);
9897

99-
expect(fs.existsSync(path.join(compiledPackDir, 'query.ql'))).to.be.true;
98+
expect(fs.existsSync(path.join(compiledPackDir, 'in-pack.ql'))).to.be.true;
10099
expect(fs.existsSync(path.join(compiledPackDir, 'lib.qll'))).to.be.true;
101100
expect(fs.existsSync(path.join(compiledPackDir, 'qlpack.yml'))).to.be.true;
102101
// depending on the cli version, we should have one of these files
@@ -105,6 +104,7 @@ describe('Remote queries', function() {
105104
fs.existsSync(path.join(compiledPackDir, 'codeql-pack.lock.yml'))
106105
).to.be.true;
107106
expect(fs.existsSync(path.join(compiledPackDir, 'not-in-pack.ql'))).to.be.false;
107+
verifyQlPack(path.join(compiledPackDir, 'qlpack.yml'), 'in-pack.ql', 'github/remote-query-pack', '0.0.0');
108108

109109
// dependencies
110110
const libraryDir = path.join(compiledPackDir, '.codeql/libraries/codeql');
@@ -129,8 +129,8 @@ describe('Remote queries', function() {
129129

130130
const queryPackDir = path.join(queryPackRootDir, 'query-pack');
131131
printDirectoryContents(queryPackDir);
132-
// in-pack.ql renamed to query.ql
133-
expect(fs.existsSync(path.join(queryPackDir, 'query.ql'))).to.be.true;
132+
133+
expect(fs.existsSync(path.join(queryPackDir, 'in-pack.ql'))).to.be.true;
134134
expect(fs.existsSync(path.join(queryPackDir, 'qlpack.yml'))).to.be.true;
135135
// depending on the cli version, we should have one of these files
136136
expect(
@@ -143,8 +143,10 @@ describe('Remote queries', function() {
143143
// the compiled pack
144144
const compiledPackDir = path.join(queryPackDir, '.codeql/pack/codeql-remote/query/0.0.0/');
145145
printDirectoryContents(compiledPackDir);
146-
expect(fs.existsSync(path.join(compiledPackDir, 'query.ql'))).to.be.true;
146+
expect(fs.existsSync(path.join(compiledPackDir, 'in-pack.ql'))).to.be.true;
147147
expect(fs.existsSync(path.join(compiledPackDir, 'qlpack.yml'))).to.be.true;
148+
verifyQlPack(path.join(compiledPackDir, 'qlpack.yml'), 'in-pack.ql', 'codeql-remote/query', '0.0.0');
149+
148150
// depending on the cli version, we should have one of these files
149151
expect(
150152
fs.existsSync(path.join(compiledPackDir, 'qlpack.lock.yml')) ||
@@ -165,6 +167,60 @@ describe('Remote queries', function() {
165167
expect(packNames).to.deep.equal(['javascript-all', 'javascript-upgrades']);
166168
});
167169

170+
it('should run a remote query that is nested inside a qlpack', async () => {
171+
const fileUri = getFile('data-remote-qlpack-nested/subfolder/in-pack.ql');
172+
173+
const queryPackRootDir = (await runRemoteQuery(cli, credentials, fileUri, true, progress, token))!;
174+
175+
// to retrieve the list of repositories
176+
expect(showQuickPickSpy).to.have.been.calledOnce;
177+
178+
// check a few files that we know should exist and others that we know should not
179+
180+
// the tarball to deliver to the server
181+
printDirectoryContents(queryPackRootDir);
182+
expect(fs.readdirSync(queryPackRootDir).find(f => f.startsWith('qlpack-') && f.endsWith('-generated.tgz'))).not.to.be.undefined;
183+
184+
const queryPackDir = path.join(queryPackRootDir, 'query-pack');
185+
printDirectoryContents(queryPackDir);
186+
187+
expect(fs.existsSync(path.join(queryPackDir, 'subfolder/in-pack.ql'))).to.be.true;
188+
expect(fs.existsSync(path.join(queryPackDir, 'qlpack.yml'))).to.be.true;
189+
// depending on the cli version, we should have one of these files
190+
expect(
191+
fs.existsSync(path.join(queryPackDir, 'qlpack.lock.yml')) ||
192+
fs.existsSync(path.join(queryPackDir, 'codeql-pack.lock.yml'))
193+
).to.be.true;
194+
expect(fs.existsSync(path.join(queryPackDir, 'otherfolder/lib.qll'))).to.be.true;
195+
expect(fs.existsSync(path.join(queryPackDir, 'not-in-pack.ql'))).to.be.false;
196+
197+
// the compiled pack
198+
const compiledPackDir = path.join(queryPackDir, '.codeql/pack/github/remote-query-pack/0.0.0/');
199+
printDirectoryContents(compiledPackDir);
200+
expect(fs.existsSync(path.join(compiledPackDir, 'otherfolder/lib.qll'))).to.be.true;
201+
expect(fs.existsSync(path.join(compiledPackDir, 'subfolder/in-pack.ql'))).to.be.true;
202+
expect(fs.existsSync(path.join(compiledPackDir, 'qlpack.yml'))).to.be.true;
203+
verifyQlPack(path.join(compiledPackDir, 'qlpack.yml'), 'subfolder/in-pack.ql', 'github/remote-query-pack', '0.0.0');
204+
205+
// depending on the cli version, we should have one of these files
206+
expect(
207+
fs.existsSync(path.join(compiledPackDir, 'qlpack.lock.yml')) ||
208+
fs.existsSync(path.join(compiledPackDir, 'codeql-pack.lock.yml'))
209+
).to.be.true;
210+
expect(fs.existsSync(path.join(compiledPackDir, 'not-in-pack.ql'))).to.be.false;
211+
// should have generated a correct qlpack file
212+
const qlpackContents: any = yaml.safeLoad(fs.readFileSync(path.join(compiledPackDir, 'qlpack.yml'), 'utf8'));
213+
expect(qlpackContents.name).to.equal('github/remote-query-pack');
214+
expect(qlpackContents.version).to.equal('0.0.0');
215+
expect(qlpackContents.dependencies?.['codeql/javascript-all']).to.equal('*');
216+
217+
// dependencies
218+
const libraryDir = path.join(compiledPackDir, '.codeql/libraries/codeql');
219+
printDirectoryContents(libraryDir);
220+
const packNames = fs.readdirSync(libraryDir).sort();
221+
expect(packNames).to.deep.equal(['javascript-all', 'javascript-upgrades']);
222+
});
223+
168224
it('should cancel a run before uploading', async () => {
169225
const fileUri = getFile('data-remote-no-qlpack/in-pack.ql');
170226

@@ -180,15 +236,37 @@ describe('Remote queries', function() {
180236
}
181237
});
182238

239+
function verifyQlPack(qlpackPath: string, queryPath: string, packName: string, packVersion: string) {
240+
const qlPack = yaml.safeLoad(fs.readFileSync(qlpackPath, 'utf8'));
241+
242+
// don't check the build metadata since it is variable
243+
delete (qlPack as any).buildMetadata;
244+
245+
expect(qlPack).to.deep.equal({
246+
name: packName,
247+
version: packVersion,
248+
dependencies: {
249+
'codeql/javascript-all': '*',
250+
},
251+
library: false,
252+
defaultSuite: [{
253+
description: 'Query suite for remote query',
254+
query: queryPath,
255+
}]
256+
});
257+
}
258+
183259
function getFile(file: string): Uri {
184260
return Uri.file(path.join(baseDir, file));
185261
}
186262

187263
function printDirectoryContents(dir: string) {
188-
console.log(`DIR ${dir}`);
189-
if (!fs.existsSync(dir)) {
190-
console.log(`DIR ${dir} does not exist`);
191-
}
192-
fs.readdirSync(dir).sort().forEach(f => console.log(` ${f}`));
264+
dir;
265+
// uncomment to debug
266+
// console.log(`DIR ${dir}`);
267+
// if (!fs.existsSync(dir)) {
268+
// console.log(`DIR ${dir} does not exist`);
269+
// }
270+
// fs.readdirSync(dir).sort().forEach(f => console.log(` ${f}`));
193271
}
194272
});

0 commit comments

Comments
 (0)