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 an option _View Results (CSV)_ to view the results of a non-alert query. The existing options for alert queries have been renamed to _View Alerts_ to avoid confusion. [#929](https://github.com/github/vscode-codeql/pull/929)
- Allow users to specify the number of paths to display for each alert. [#931](https://github.com/github/vscode-codeql/pull/931)
- Adjust pagination controls in _CodeQL Query Results_ to always be visible [#936](https://github.com/github/vscode-codeql/pull/936)
- Fix bug where _View AST_ fails due to recent refactoring in the standard library and query packs. [#939](https://github.com/github/vscode-codeql/pull/939)

## 1.5.3 - 18 August 2021

Expand Down
16 changes: 15 additions & 1 deletion extensions/ql-vscode/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -775,11 +775,15 @@ export class CodeQLCliServer implements Disposable {
* the default CLI search path is used.
* @returns A list of query files found.
*/
resolveQueriesInSuite(suite: string, additionalPacks: string[], searchPath?: string[]): Promise<string[]> {
async resolveQueriesInSuite(suite: string, additionalPacks: string[], searchPath?: string[]): Promise<string[]> {
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.

Why wasn't this async before?

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.

Previously, its only return statement was returning an appropriately-typed promise from a function call, so there was no need to use await inside the implementation.

const args = ['--additional-packs', additionalPacks.join(path.delimiter)];
if (searchPath !== undefined) {
args.push('--search-path', path.join(...searchPath));
}
if (await this.cliConstraints.supportsAllowLibraryPacksInResolveQueries()) {
// All of our usage of `codeql resolve queries` needs to handle library packs.
args.push('--allow-library-packs');
}
args.push(suite);
return this.runJsonCodeQlCliCommand<string[]>(
['resolve', 'queries'],
Expand Down Expand Up @@ -1064,6 +1068,12 @@ export class CliVersionConstraint {
*/
public static CLI_VERSION_WITH_DB_REGISTRATION = new SemVer('2.4.1');

/**
* CLI version where the `--allow-library-packs` option to `codeql resolve queries` was
* introduced.
*/
public static CLI_VERSION_WITH_ALLOW_LIBRARY_PACKS_IN_RESOLVE_QUERIES = new SemVer('2.6.1');

constructor(private readonly cli: CodeQLCliServer) {
/**/
}
Expand All @@ -1088,6 +1098,10 @@ export class CliVersionConstraint {
return this.isVersionAtLeast(CliVersionConstraint.CLI_VERSION_WITH_RESOLVE_QLREF);
}

public async supportsAllowLibraryPacksInResolveQueries() {
return this.isVersionAtLeast(CliVersionConstraint.CLI_VERSION_WITH_ALLOW_LIBRARY_PACKS_IN_RESOLVE_QUERIES);
}

async supportsDatabaseRegistration() {
return this.isVersionAtLeast(CliVersionConstraint.CLI_VERSION_WITH_DB_REGISTRATION);
}
Expand Down
104 changes: 82 additions & 22 deletions extensions/ql-vscode/src/contextual/queryResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ import {
} from './keyType';
import { CodeQLCliServer } from '../cli';
import { DatabaseItem } from '../databases';
import { QlPacksForLanguage } from '../helpers';

export async function qlpackOfDatabase(cli: CodeQLCliServer, db: DatabaseItem): Promise<string> {
export async function qlpackOfDatabase(cli: CodeQLCliServer, db: DatabaseItem): Promise<QlPacksForLanguage> {
if (db.contents === undefined) {
throw new Error('Database is invalid and cannot infer QLPack.');
}
Expand All @@ -21,28 +22,87 @@ export async function qlpackOfDatabase(cli: CodeQLCliServer, db: DatabaseItem):
return await helpers.getQlPackForDbscheme(cli, dbscheme);
}

/**
* Finds the contextual queries with the specified key in a list of CodeQL packs.
*
* @param cli The CLI instance to use.
* @param qlpacks The list of packs to search.
* @param keyType The contextual query key of the query to search for.
* @returns The found queries from the first pack in which any matching queries were found.
*/
async function resolveQueriesFromPacks(cli: CodeQLCliServer, qlpacks: string[], keyType: KeyType): Promise<string[]> {
for (const qlpack of qlpacks) {
const suiteFile = (await tmp.file({
postfix: '.qls'
})).path;
const suiteYaml = {
qlpack,
include: {
kind: kindOfKeyType(keyType),
'tags contain': tagOfKeyType(keyType)
}
};
await fs.writeFile(suiteFile, yaml.safeDump(suiteYaml), 'utf8');

export async function resolveQueries(cli: CodeQLCliServer, qlpack: string, keyType: KeyType): Promise<string[]> {
const suiteFile = (await tmp.file({
postfix: '.qls'
})).path;
const suiteYaml = {
qlpack,
include: {
kind: kindOfKeyType(keyType),
'tags contain': tagOfKeyType(keyType)
const queries = await cli.resolveQueriesInSuite(suiteFile, helpers.getOnDiskWorkspaceFolders());
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.

I'm concerned about calling the CLI in a loop. Each call is a few seconds since it needs to restart the vm. If it's only going to be called 2 or 3 times for each AST Viewer call, maybe that's fine.

Alternatively, could you combine all the qlpacks into one suite? It would look like this:

const allPacks = [
  {
    qlpack: 'codeql/cpp-queries',
    include: { ... }
  },
  {
    qlpack: 'codeql/cpp-all',
    include: { ... }
  }
];

And then you can just send this suite directly to the CLI. I think it would work.

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.

We could use the CodeQL CLI server (codeql execute cli-server) . Probably not in this pull request, but it could be something to consider in a future refactoring.

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.

Actually, did a little bit of digging here and we are using the cli server for all calls to the cli. The queries are handled by the query server and other commands by the cli server.

This means that it is less important to combine these pack references into a single suite.

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.

Once the packs are all properly refactored, we'll always find the queries in the dbscheme library pack, so we'll never search the query pack. The only situation where we'll search both packs is with a >=2.6.1 CLI and packs that were released at the same time as 2.6.1. By the next dist upgrade, the contextual queries will already be in the library pack.

if (queries.length > 0) {
return queries;
}
};
await fs.writeFile(suiteFile, yaml.safeDump(suiteYaml), 'utf8');

const queries = await cli.resolveQueriesInSuite(suiteFile, helpers.getOnDiskWorkspaceFolders());
if (queries.length === 0) {
void helpers.showAndLogErrorMessage(
`No ${nameOfKeyType(keyType)} queries (tagged "${tagOfKeyType(keyType)}") could be found in the current library path. \
Try upgrading the CodeQL libraries. If that doesn't work, then ${nameOfKeyType(keyType)} queries are not yet available \
for this language.`
);
throw new Error(`Couldn't find any queries tagged ${tagOfKeyType(keyType)} for qlpack ${qlpack}`);
}
return queries;

return [];
}

export async function resolveQueries(cli: CodeQLCliServer, qlpacks: QlPacksForLanguage, keyType: KeyType): Promise<string[]> {
const cliCanHandleLibraryPack = await cli.cliConstraints.supportsAllowLibraryPacksInResolveQueries();
const packsToSearch: string[] = [];
let blameCli: boolean;

if (cliCanHandleLibraryPack) {
// The CLI can handle both library packs and query packs, so search both packs in order.
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.

Ah...so it's only ever 2 calls to the CLI for resolve queries. Still might make sense to combine them into a single suite.

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.

See above. Even the two-call case is only for a specific combination of CLI/pack versions.

packsToSearch.push(qlpacks.dbschemePack);
if (qlpacks.queryPack !== undefined) {
packsToSearch.push(qlpacks.queryPack);
}
// If we don't find the query, it's because it's not there, not because the CLI was unable to
// search the pack.
blameCli = false;
} else {
// Older CLIs can't handle `codeql resolve queries` with a suite that references a library pack.
if (qlpacks.dbschemePackIsLibraryPack) {
if (qlpacks.queryPack !== undefined) {
// Just search the query pack, because some older library/query releases still had the
// contextual queries in the query pack.
packsToSearch.push(qlpacks.queryPack);
}
// If we don't find it, it's because the CLI was unable to search the library pack that
// actually contains the query. Blame any failure on the CLI, not the packs.
blameCli = true;
} else {
// We have an old CLI, but the dbscheme pack is old enough that it's still a unified pack with
// both libraries and queries. Just search that pack.
packsToSearch.push(qlpacks.dbschemePack);
// Any CLI should be able to search the single query pack, so if we don't find it, it's
// because the language doesn't support it.
blameCli = false;
}
}

const queries = await resolveQueriesFromPacks(cli, packsToSearch, keyType);
if (queries.length > 0) {
return queries;
}

// No queries found. Determine the correct error message for the various scenarios.
const errorMessage = blameCli ?
`Your current version of the CodeQL CLI, '${(await cli.getVersion()).version}', \
is unable to use contextual queries from recent versions of the standard CodeQL libraries. \
Please upgrade to the latest version of the CodeQL CLI.`
:
`No ${nameOfKeyType(keyType)} queries (tagged "${tagOfKeyType(keyType)}") could be found in the current library path. \
Try upgrading the CodeQL libraries. If that doesn't work, then ${nameOfKeyType(keyType)} queries are not yet available \
for this language.`;

void helpers.showAndLogErrorMessage(errorMessage);
throw new Error(`Couldn't find any queries tagged ${tagOfKeyType(keyType)} in any of the following packs: ${packsToSearch.join(', ')}.`);
}
4 changes: 2 additions & 2 deletions extensions/ql-vscode/src/contextual/templateProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,8 @@ export class TemplatePrintAstProvider {
throw new Error('Can\'t infer database from the provided source.');
}

const qlpack = await qlpackOfDatabase(this.cli, db);
const queries = await resolveQueries(this.cli, qlpack, KeyType.PrintAstQuery);
const qlpacks = await qlpackOfDatabase(this.cli, db);
const queries = await resolveQueries(this.cli, qlpacks, KeyType.PrintAstQuery);
if (queries.length > 1) {
throw new Error('Found multiple Print AST queries. Can\'t continue');
}
Expand Down
68 changes: 56 additions & 12 deletions extensions/ql-vscode/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
workspace,
env
} from 'vscode';
import { CodeQLCliServer } from './cli';
import { CodeQLCliServer, QlpacksInfo } from './cli';
import { logger } from './logging';

/**
Expand Down Expand Up @@ -254,9 +254,55 @@ function createRateLimitedResult(): RateLimitedResult {
};
}

export async function getQlPackForDbscheme(cliServer: CodeQLCliServer, dbschemePath: string): Promise<string> {
export interface QlPacksForLanguage {
/** The name of the pack containing the dbscheme. */
dbschemePack: string;
/** `true` if `dbschemePack` is a library pack. */
dbschemePackIsLibraryPack: boolean;
/**
* The name of the corresponding standard query pack.
* Only defined if `dbschemePack` is a library pack.
*/
queryPack?: string;
}

interface QlPackWithPath {
packName: string;
packDir: string | undefined;
}

async function findDbschemePack(packs: QlPackWithPath[], dbschemePath: string): Promise<{ name: string; isLibraryPack: boolean; }> {
for (const { packDir, packName } of packs) {
if (packDir !== undefined) {
const qlpack = yaml.safeLoad(await fs.readFile(path.join(packDir, 'qlpack.yml'), 'utf8')) as { dbscheme?: string; library?: boolean; };
if (qlpack.dbscheme !== undefined && path.basename(qlpack.dbscheme) === path.basename(dbschemePath)) {
return {
name: packName,
isLibraryPack: qlpack.library === true
};
}
}
}
throw new Error(`Could not find qlpack file for dbscheme ${dbschemePath}`);
}

function findStandardQueryPack(qlpacks: QlpacksInfo, dbschemePackName: string): string | undefined {
const matches = dbschemePackName.match(/^codeql\/(?<language>[a-z]+)-all$/);
if (matches) {
const queryPackName = `codeql/${matches.groups!.language}-queries`;
if (qlpacks[queryPackName] !== undefined) {
return queryPackName;
}
}

// Either the dbscheme pack didn't look like one where the queries might be in the query pack, or
// no query pack was found in the search path. Either is OK.
return undefined;
}

export async function getQlPackForDbscheme(cliServer: CodeQLCliServer, dbschemePath: string): Promise<QlPacksForLanguage> {
const qlpacks = await cliServer.resolveQlpacks(getOnDiskWorkspaceFolders());
const packs: { packDir: string | undefined; packName: string }[] =
const packs: QlPackWithPath[] =
Object.entries(qlpacks).map(([packName, dirs]) => {
if (dirs.length < 1) {
void logger.log(`In getQlPackFor ${dbschemePath}, qlpack ${packName} has no directories`);
Expand All @@ -270,15 +316,13 @@ export async function getQlPackForDbscheme(cliServer: CodeQLCliServer, dbschemeP
packDir: dirs[0]
};
});
for (const { packDir, packName } of packs) {
if (packDir !== undefined) {
const qlpack = yaml.safeLoad(await fs.readFile(path.join(packDir, 'qlpack.yml'), 'utf8')) as { dbscheme: string };
if (qlpack.dbscheme !== undefined && path.basename(qlpack.dbscheme) === path.basename(dbschemePath)) {
return packName;
}
}
}
throw new Error(`Could not find qlpack file for dbscheme ${dbschemePath}`);
const dbschemePack = await findDbschemePack(packs, dbschemePath);
const queryPack = dbschemePack.isLibraryPack ? findStandardQueryPack(qlpacks, dbschemePack.name) : undefined;
return {
dbschemePack: dbschemePack.name,
dbschemePackIsLibraryPack: dbschemePack.isLibraryPack,
queryPack
};
}

export async function getPrimaryDbscheme(datasetFolder: string): Promise<string> {
Expand Down
2 changes: 1 addition & 1 deletion extensions/ql-vscode/src/quick-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export async function displayQuickQuery(

const datasetFolder = await dbItem.getDatasetFolder(cliServer);
const dbscheme = await getPrimaryDbscheme(datasetFolder);
const qlpack = await getQlPackForDbscheme(cliServer, dbscheme);
const qlpack = (await getQlPackForDbscheme(cliServer, dbscheme)).dbschemePack;
const qlPackFile = path.join(queriesDir, 'qlpack.yml');
const qlFile = path.join(queriesDir, QUICK_QUERY_QUERY_NAME);
const shouldRewrite = await checkShouldRewrite(qlPackFile, qlpack);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@ describe('queryResolver', () => {
let writeFileSpy: sinon.SinonSpy;
let getQlPackForDbschemeSpy: sinon.SinonStub;
let getPrimaryDbschemeSpy: sinon.SinonStub;
let mockCli: Record<string, sinon.SinonStub>;
let mockCli: Record<string, sinon.SinonStub | Record<string, sinon.SinonStub>>;
beforeEach(() => {
mockCli = {
resolveQueriesInSuite: sinon.stub()
resolveQueriesInSuite: sinon.stub(),
cliConstraints: {
supportsAllowLibraryPacksInResolveQueries: sinon.stub().returns(true),
}
};
module = createModule();
});
Expand All @@ -30,7 +33,7 @@ describe('queryResolver', () => {

it('should resolve a query', async () => {
mockCli.resolveQueriesInSuite.returns(['a', 'b']);
const result = await module.resolveQueries(mockCli, 'my-qlpack', KeyType.DefinitionQuery);
const result = await module.resolveQueries(mockCli, { dbschemePack: 'my-qlpack' }, KeyType.DefinitionQuery);
expect(result).to.deep.equal(['a', 'b']);
expect(writeFileSpy.getCall(0).args[0]).to.match(/.qls$/);
expect(yaml.safeLoad(writeFileSpy.getCall(0).args[1])).to.deep.equal({
Expand All @@ -42,18 +45,34 @@ describe('queryResolver', () => {
});
});

it('should resolve a query from the queries pack if this is an old CLI', async () => {
// pretend this is an older CLI
(mockCli.cliConstraints as any).supportsAllowLibraryPacksInResolveQueries.returns(false);
mockCli.resolveQueriesInSuite.returns(['a', 'b']);
const result = await module.resolveQueries(mockCli, { dbschemePackIsLibraryPack: true, dbschemePack: 'my-qlpack', queryPack: 'my-qlpack2' }, KeyType.DefinitionQuery);
expect(result).to.deep.equal(['a', 'b']);
expect(writeFileSpy.getCall(0).args[0]).to.match(/.qls$/);
expect(yaml.safeLoad(writeFileSpy.getCall(0).args[1])).to.deep.equal({
qlpack: 'my-qlpack2',
include: {
kind: 'definitions',
'tags contain': 'ide-contextual-queries/local-definitions'
}
});
});

it('should throw an error when there are no queries found', async () => {
mockCli.resolveQueriesInSuite.returns([]);

// TODO: Figure out why chai-as-promised isn't failing the test on an
// unhandled rejection.
try {
await module.resolveQueries(mockCli, 'my-qlpack', KeyType.DefinitionQuery);
await module.resolveQueries(mockCli, { dbschemePack: 'my-qlpack' }, KeyType.DefinitionQuery);
// should reject
expect(true).to.be.false;
} catch (e) {
expect(e.message).to.eq(
'Couldn\'t find any queries tagged ide-contextual-queries/local-definitions for qlpack my-qlpack'
'Couldn\'t find any queries tagged ide-contextual-queries/local-definitions in any of the following packs: my-qlpack.'
);
}
});
Expand Down