Skip to content

Commit d7ba941

Browse files
authored
Merge pull request #1009 from github/aeisenberg/remote-nested-queries
Remote queries: Handle nested queries
2 parents 81e6028 + e58201e commit d7ba941

File tree

7 files changed

+170
-38
lines changed

7 files changed

+170
-38
lines changed

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

+57-30
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+
export interface QlPack {
21+
name: string;
22+
version: string;
23+
dependencies: { [key: string]: string };
24+
defaultSuite?: Record<string, unknown>[];
25+
defaultSuiteFile?: string;
26+
}
2127
interface RepoListQuickPickItem extends QuickPickItem {
2228
repoList: string[];
2329
}
@@ -33,6 +39,11 @@ interface QueriesResponse {
3339
*/
3440
const REPO_REGEX = /^(?:[a-zA-Z0-9]+-)*[a-zA-Z0-9]+\/[a-zA-Z0-9-_]+$/;
3541

42+
/**
43+
* Well-known names for the query pack used by the server.
44+
*/
45+
const QUERY_PACK_NAME = 'codeql-remote/query';
46+
3647
/**
3748
* Gets the repositories to run the query against.
3849
*/
@@ -89,13 +100,9 @@ async function generateQueryPack(cliServer: cli.CodeQLCliServer, queryFile: stri
89100
base64Pack: string,
90101
language: string
91102
}> {
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');
103+
const originalPackRoot = await findPackRoot(queryFile);
104+
const packRelativePath = path.relative(originalPackRoot, queryFile);
105+
const targetQueryFileName = path.join(queryPackDir, packRelativePath);
99106

100107
let language: string | undefined;
101108
if (await fs.pathExists(path.join(originalPackRoot, 'qlpack.yml'))) {
@@ -125,9 +132,6 @@ async function generateQueryPack(cliServer: cli.CodeQLCliServer, queryFile: stri
125132
})
126133
});
127134

128-
// ensure the qlpack.yml has a valid name
129-
await ensureQueryPackName(queryPackDir);
130-
131135
void logger.log(`Copied ${copiedCount} files to ${queryPackDir}`);
132136

133137
language = await findLanguage(cliServer, Uri.file(targetQueryFileName));
@@ -138,13 +142,11 @@ async function generateQueryPack(cliServer: cli.CodeQLCliServer, queryFile: stri
138142

139143
// copy only the query file to the query pack directory
140144
// 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.
143145
void logger.log(`Copying ${queryFile} to ${queryPackDir}`);
144146
await fs.copy(queryFile, targetQueryFileName);
145147
void logger.log('Generating synthetic query pack');
146148
const syntheticQueryPack = {
147-
name: 'codeql-remote/query',
149+
name: QUERY_PACK_NAME,
148150
version: '0.0.0',
149151
dependencies: {
150152
[`codeql/${language}-all`]: '*',
@@ -156,7 +158,7 @@ async function generateQueryPack(cliServer: cli.CodeQLCliServer, queryFile: stri
156158
throw new UserCancellationException('Could not determine language.');
157159
}
158160

159-
await fs.rename(targetQueryFileName, renamedQueryFile);
161+
await ensureNameAndSuite(queryPackDir, packRelativePath);
160162

161163
const bundlePath = await getPackedBundlePath(queryPackDir);
162164
void logger.log(`Compiling and bundling query pack from ${queryPackDir} to ${bundlePath}. (This may take a while.)`);
@@ -170,23 +172,24 @@ async function generateQueryPack(cliServer: cli.CodeQLCliServer, queryFile: stri
170172
};
171173
}
172174

173-
/**
174-
* Ensure that the qlpack.yml has a valid name. For local purposes,
175-
* Anonymous packs and names that are not prefixed by a scope (ie `<foo>/`)
176-
* are sufficient. But in order to create a pack, the name must be prefixed.
177-
*
178-
* @param queryPackDir the directory containing the query pack.
179-
*/
180-
async function ensureQueryPackName(queryPackDir: string) {
181-
const pack = yaml.safeLoad(await fs.readFile(path.join(queryPackDir, 'qlpack.yml'), 'utf8')) as { name: string; };
182-
if (!pack.name || !pack.name.includes('/')) {
183-
if (!pack.name) {
184-
pack.name = 'codeql-remote/query';
185-
} else if (!pack.name.includes('/')) {
186-
pack.name = `codeql-remote/${pack.name}`;
175+
async function findPackRoot(queryFile: string): Promise<string> {
176+
// recursively find the directory containing qlpack.yml
177+
let dir = path.dirname(queryFile);
178+
while (!(await fs.pathExists(path.join(dir, 'qlpack.yml')))) {
179+
dir = path.dirname(dir);
180+
if (isFileSystemRoot(dir)) {
181+
// there is no qlpack.yml in this direcory or any parent directory.
182+
// just use the query file's directory as the pack root.
183+
return path.dirname(queryFile);
187184
}
188-
await fs.writeFile(path.join(queryPackDir, 'qlpack.yml'), yaml.safeDump(pack));
189185
}
186+
187+
return dir;
188+
}
189+
190+
function isFileSystemRoot(dir: string): boolean {
191+
const pathObj = path.parse(dir);
192+
return pathObj.root === dir && pathObj.base === '';
190193
}
191194

192195
async function createRemoteQueriesTempDirectory() {
@@ -413,3 +416,27 @@ export async function attemptRerun(
413416
void showAndLogErrorMessage(error);
414417
}
415418
}
419+
420+
/**
421+
* Updates the default suite of the query pack. This is used to ensure
422+
* only the specified query is run.
423+
*
424+
* Also, ensure the query pack name is set to the name expected by the server.
425+
*
426+
* @param queryPackDir The directory containing the query pack
427+
* @param packRelativePath The relative path to the query pack from the root of the query pack
428+
*/
429+
async function ensureNameAndSuite(queryPackDir: string, packRelativePath: string): Promise<void> {
430+
const packPath = path.join(queryPackDir, 'qlpack.yml');
431+
const qlpack = yaml.safeLoad(await fs.readFile(packPath, 'utf8')) as QlPack;
432+
delete qlpack.defaultSuiteFile;
433+
434+
qlpack.name = QUERY_PACK_NAME;
435+
436+
qlpack.defaultSuite = [{
437+
description: 'Query suite for remote query'
438+
}, {
439+
query: packRelativePath.replace(/\\/g, '/')
440+
}];
441+
await fs.writeFile(packPath, yaml.safeDump(qlpack));
442+
}
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
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
int number() {
2+
result = 1
3+
}
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: '*'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import javascript
2+
import otherfolder.lib
3+
4+
select number()
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

+100-7
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@ import * as sinon from 'sinon';
44
import { CancellationToken, extensions, QuickPickItem, Uri, window } from 'vscode';
55
import 'mocha';
66
import * as fs from 'fs-extra';
7+
import * as os from 'os';
78
import * as yaml from 'js-yaml';
89

9-
import { runRemoteQuery } from '../../run-remote-query';
10+
import { QlPack, runRemoteQuery } from '../../run-remote-query';
1011
import { Credentials } from '../../authentication';
1112
import { CliVersionConstraint, CodeQLCliServer } from '../../cli';
1213
import { CodeQLExtensionInterface } from '../../extension';
1314
import { setRemoteControllerRepo, setRemoteRepositoryLists } from '../../config';
1415
import { UserCancellationException } from '../../commandRunner';
16+
import { lte } from 'semver';
1517

1618
describe('Remote queries', function() {
1719
const baseDir = path.join(__dirname, '../../../src/vscode-tests/cli-integration');
@@ -80,8 +82,7 @@ describe('Remote queries', function() {
8082
const queryPackDir = path.join(queryPackRootDir, 'query-pack');
8183
printDirectoryContents(queryPackDir);
8284

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

@@ -96,7 +97,7 @@ describe('Remote queries', function() {
9697
const compiledPackDir = path.join(queryPackDir, '.codeql/pack/github/remote-query-pack/0.0.0/');
9798
printDirectoryContents(compiledPackDir);
9899

99-
expect(fs.existsSync(path.join(compiledPackDir, 'query.ql'))).to.be.true;
100+
expect(fs.existsSync(path.join(compiledPackDir, 'in-pack.ql'))).to.be.true;
100101
expect(fs.existsSync(path.join(compiledPackDir, 'lib.qll'))).to.be.true;
101102
expect(fs.existsSync(path.join(compiledPackDir, 'qlpack.yml'))).to.be.true;
102103
// depending on the cli version, we should have one of these files
@@ -105,6 +106,7 @@ describe('Remote queries', function() {
105106
fs.existsSync(path.join(compiledPackDir, 'codeql-pack.lock.yml'))
106107
).to.be.true;
107108
expect(fs.existsSync(path.join(compiledPackDir, 'not-in-pack.ql'))).to.be.false;
109+
verifyQlPack(path.join(compiledPackDir, 'qlpack.yml'), 'in-pack.ql', '0.0.0', await pathSerializationBroken());
108110

109111
// dependencies
110112
const libraryDir = path.join(compiledPackDir, '.codeql/libraries/codeql');
@@ -129,8 +131,8 @@ describe('Remote queries', function() {
129131

130132
const queryPackDir = path.join(queryPackRootDir, 'query-pack');
131133
printDirectoryContents(queryPackDir);
132-
// in-pack.ql renamed to query.ql
133-
expect(fs.existsSync(path.join(queryPackDir, 'query.ql'))).to.be.true;
134+
135+
expect(fs.existsSync(path.join(queryPackDir, 'in-pack.ql'))).to.be.true;
134136
expect(fs.existsSync(path.join(queryPackDir, 'qlpack.yml'))).to.be.true;
135137
// depending on the cli version, we should have one of these files
136138
expect(
@@ -143,8 +145,10 @@ describe('Remote queries', function() {
143145
// the compiled pack
144146
const compiledPackDir = path.join(queryPackDir, '.codeql/pack/codeql-remote/query/0.0.0/');
145147
printDirectoryContents(compiledPackDir);
146-
expect(fs.existsSync(path.join(compiledPackDir, 'query.ql'))).to.be.true;
148+
expect(fs.existsSync(path.join(compiledPackDir, 'in-pack.ql'))).to.be.true;
147149
expect(fs.existsSync(path.join(compiledPackDir, 'qlpack.yml'))).to.be.true;
150+
verifyQlPack(path.join(compiledPackDir, 'qlpack.yml'), 'in-pack.ql', '0.0.0', await pathSerializationBroken());
151+
148152
// depending on the cli version, we should have one of these files
149153
expect(
150154
fs.existsSync(path.join(compiledPackDir, 'qlpack.lock.yml')) ||
@@ -165,6 +169,60 @@ describe('Remote queries', function() {
165169
expect(packNames).to.deep.equal(['javascript-all', 'javascript-upgrades']);
166170
});
167171

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

@@ -180,6 +238,41 @@ describe('Remote queries', function() {
180238
}
181239
});
182240

241+
function verifyQlPack(qlpackPath: string, queryPath: string, packVersion: string, pathSerializationBroken: boolean) {
242+
const qlPack = yaml.safeLoad(fs.readFileSync(qlpackPath, 'utf8')) as QlPack;
243+
244+
if (pathSerializationBroken) {
245+
// the path serialization is broken, so we force it to be the path in the pack to be same as the query path
246+
qlPack.defaultSuite![1].query = queryPath;
247+
}
248+
249+
// don't check the build metadata since it is variable
250+
delete (qlPack as any).buildMetadata;
251+
252+
expect(qlPack).to.deep.equal({
253+
name: 'codeql-remote/query',
254+
version: packVersion,
255+
dependencies: {
256+
'codeql/javascript-all': '*',
257+
},
258+
library: false,
259+
defaultSuite: [{
260+
description: 'Query suite for remote query'
261+
}, {
262+
query: queryPath
263+
}]
264+
});
265+
}
266+
267+
/**
268+
* In version 2.7.2 and earlier, relative paths were not serialized correctly inside the qlpack.yml file.
269+
* So, ignore part of the test for these versions.
270+
*
271+
* @returns true if path serialization is broken in this run
272+
*/
273+
async function pathSerializationBroken() {
274+
return lte((await cli.getVersion()), '2.7.2') && os.platform() === 'win32';
275+
}
183276
function getFile(file: string): Uri {
184277
return Uri.file(path.join(baseDir, file));
185278
}

0 commit comments

Comments
 (0)