Skip to content

Commit 95f46b3

Browse files
Merge pull request #2055 from github/elena/download-ql-packs
Generate QL pack for CodeTour
2 parents a962656 + ab29fb7 commit 95f46b3

File tree

5 files changed

+264
-18
lines changed

5 files changed

+264
-18
lines changed

extensions/ql-vscode/src/cli.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { CompilationMessage } from "./pure/legacy-messages";
2828
import { sarifParser } from "./sarif-parser";
2929
import { dbSchemeToLanguage, walkDirectory } from "./helpers";
3030
import { App } from "./common/app";
31+
import { QueryLanguage } from "./qlpack-generator";
3132

3233
/**
3334
* The version of the SARIF format that we are using.
@@ -1216,6 +1217,23 @@ export class CodeQLCliServer implements Disposable {
12161217
);
12171218
}
12181219

1220+
/**
1221+
* Adds a core language QL library pack for the given query language as a dependency
1222+
* of the current package, and then installs them. This command modifies the qlpack.yml
1223+
* file of the current package. Formatting and comments will be removed.
1224+
* @param dir The directory where QL pack exists.
1225+
* @param language The language of the QL pack.
1226+
*/
1227+
async packAdd(dir: string, queryLanguage: QueryLanguage) {
1228+
const args = ["--dir", dir];
1229+
args.push(`codeql/${queryLanguage}-all`);
1230+
return this.runJsonCodeQlCliCommandWithAuthentication(
1231+
["pack", "add"],
1232+
args,
1233+
`Adding and installing ${queryLanguage} pack dependency.`,
1234+
);
1235+
}
1236+
12191237
/**
12201238
* Downloads a specified pack.
12211239
* @param packs The `<package-scope/name[@version]>` of the packs to download.

extensions/ql-vscode/src/databases.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { QueryRunner } from "./queryRunner";
2626
import { pathsEqual } from "./pure/files";
2727
import { redactableError } from "./pure/errors";
2828
import { isCodespacesTemplate } from "./config";
29+
import { QlPackGenerator, QueryLanguage } from "./qlpack-generator";
2930

3031
/**
3132
* databases.ts
@@ -655,9 +656,27 @@ export class DatabaseManager extends DisposableObject {
655656
return;
656657
}
657658

658-
await showBinaryChoiceDialog(
659-
`We've noticed you don't have a QL pack downloaded to analyze this database. Can we set up a ${databaseItem.language} query pack for you`,
659+
const answer = await showBinaryChoiceDialog(
660+
`We've noticed you don't have a CodeQL pack available to analyze this database. Can we set up a query pack for you?`,
660661
);
662+
663+
if (!answer) {
664+
return;
665+
}
666+
667+
try {
668+
const qlPackGenerator = new QlPackGenerator(
669+
folderName,
670+
databaseItem.language as QueryLanguage,
671+
this.cli,
672+
this.ctx.storageUri?.fsPath,
673+
);
674+
await qlPackGenerator.generate();
675+
} catch (e: unknown) {
676+
void this.logger.log(
677+
`Could not create skeleton QL pack: ${getErrorMessage(e)}`,
678+
);
679+
}
661680
}
662681

663682
private async reregisterDatabases(
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { writeFile } from "fs-extra";
2+
import { dump } from "js-yaml";
3+
import { join } from "path";
4+
import { Uri, workspace } from "vscode";
5+
import { CodeQLCliServer } from "./cli";
6+
7+
export type QueryLanguage =
8+
| "csharp"
9+
| "cpp"
10+
| "go"
11+
| "java"
12+
| "javascript"
13+
| "python"
14+
| "ruby"
15+
| "swift";
16+
17+
export class QlPackGenerator {
18+
private readonly qlpackName: string;
19+
private readonly qlpackVersion: string;
20+
private readonly header: string;
21+
private readonly qlpackFileName: string;
22+
private readonly folderUri: Uri;
23+
24+
constructor(
25+
private readonly folderName: string,
26+
private readonly queryLanguage: QueryLanguage,
27+
private readonly cliServer: CodeQLCliServer,
28+
private readonly storagePath: string | undefined,
29+
) {
30+
if (this.storagePath === undefined) {
31+
throw new Error("Workspace storage path is undefined");
32+
}
33+
this.qlpackName = `getting-started/codeql-extra-queries-${this.queryLanguage}`;
34+
this.qlpackVersion = "1.0.0";
35+
this.header = "# This is an automatically generated file.\n\n";
36+
37+
this.qlpackFileName = "qlpack.yml";
38+
this.folderUri = Uri.file(join(this.storagePath, this.folderName));
39+
}
40+
41+
public async generate() {
42+
// create QL pack folder and add to workspace
43+
await this.createWorkspaceFolder();
44+
45+
// create qlpack.yml
46+
await this.createQlPackYaml();
47+
48+
// create example.ql
49+
await this.createExampleQlFile();
50+
51+
// create codeql-pack.lock.yml
52+
await this.createCodeqlPackLockYaml();
53+
}
54+
55+
private async createWorkspaceFolder() {
56+
await workspace.fs.createDirectory(this.folderUri);
57+
58+
const end = (workspace.workspaceFolders || []).length;
59+
60+
await workspace.updateWorkspaceFolders(end, 0, {
61+
name: this.folderName,
62+
uri: this.folderUri,
63+
});
64+
}
65+
66+
private async createQlPackYaml() {
67+
const qlPackFilePath = join(this.folderUri.fsPath, this.qlpackFileName);
68+
69+
const qlPackYml = {
70+
name: this.qlpackName,
71+
version: this.qlpackVersion,
72+
dependencies: {},
73+
};
74+
75+
await writeFile(qlPackFilePath, this.header + dump(qlPackYml), "utf8");
76+
}
77+
78+
private async createExampleQlFile() {
79+
const exampleQlFilePath = join(this.folderUri.fsPath, "example.ql");
80+
81+
const exampleQl = `
82+
/**
83+
* This is an automatically generated file
84+
* @name Empty block
85+
* @kind problem
86+
* @problem.severity warning
87+
* @id ${this.queryLanguage}/example/empty-block
88+
*/
89+
90+
import ${this.queryLanguage}
91+
92+
select "Hello, world!"
93+
`.trim();
94+
95+
await writeFile(exampleQlFilePath, exampleQl, "utf8");
96+
}
97+
98+
private async createCodeqlPackLockYaml() {
99+
await this.cliServer.packAdd(this.folderUri.fsPath, this.queryLanguage);
100+
}
101+
}

extensions/ql-vscode/test/vscode-tests/minimal-workspace/databases.test.ts

Lines changed: 50 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { testDisposeHandler } from "../test-dispose-handler";
2323
import { QueryRunner } from "../../../src/queryRunner";
2424
import * as helpers from "../../../src/helpers";
2525
import { Setting } from "../../../src/config";
26+
import { QlPackGenerator } from "../../../src/qlpack-generator";
2627

2728
describe("databases", () => {
2829
const MOCK_DB_OPTIONS: FullDatabaseOptions = {
@@ -32,11 +33,13 @@ describe("databases", () => {
3233
};
3334

3435
let databaseManager: DatabaseManager;
36+
let extensionContext: ExtensionContext;
3537

3638
let updateSpy: jest.Mock<Promise<void>, []>;
3739
let registerSpy: jest.Mock<Promise<void>, []>;
3840
let deregisterSpy: jest.Mock<Promise<void>, []>;
3941
let resolveDatabaseSpy: jest.Mock<Promise<DbInfo>, []>;
42+
let packAddSpy: jest.Mock<any, []>;
4043
let logSpy: jest.Mock<any, []>;
4144

4245
let showBinaryChoiceDialogSpy: jest.SpiedFunction<
@@ -52,6 +55,7 @@ describe("databases", () => {
5255
registerSpy = jest.fn(() => Promise.resolve(undefined));
5356
deregisterSpy = jest.fn(() => Promise.resolve(undefined));
5457
resolveDatabaseSpy = jest.fn(() => Promise.resolve({} as DbInfo));
58+
packAddSpy = jest.fn();
5559
logSpy = jest.fn(() => {
5660
/* */
5761
});
@@ -60,16 +64,19 @@ describe("databases", () => {
6064
.spyOn(helpers, "showBinaryChoiceDialog")
6165
.mockResolvedValue(true);
6266

67+
extensionContext = {
68+
workspaceState: {
69+
update: updateSpy,
70+
get: () => [],
71+
},
72+
// pretend like databases added in the temp dir are controlled by the extension
73+
// so that they are deleted upon removal
74+
storagePath: dir.name,
75+
storageUri: Uri.parse(dir.name),
76+
} as unknown as ExtensionContext;
77+
6378
databaseManager = new DatabaseManager(
64-
{
65-
workspaceState: {
66-
update: updateSpy,
67-
get: () => [],
68-
},
69-
// pretend like databases added in the temp dir are controlled by the extension
70-
// so that they are deleted upon removal
71-
storagePath: dir.name,
72-
} as unknown as ExtensionContext,
79+
extensionContext,
7380
{
7481
registerDatabase: registerSpy,
7582
deregisterDatabase: deregisterSpy,
@@ -79,6 +86,7 @@ describe("databases", () => {
7986
} as unknown as QueryRunner,
8087
{
8188
resolveDatabase: resolveDatabaseSpy,
89+
packAdd: packAddSpy,
8290
} as unknown as CodeQLCliServer,
8391
{
8492
log: logSpy,
@@ -589,20 +597,46 @@ describe("databases", () => {
589597

590598
describe("createSkeletonPacks", () => {
591599
let mockDbItem: DatabaseItemImpl;
600+
let language: string;
601+
let generateSpy: jest.SpyInstance;
602+
603+
beforeEach(() => {
604+
language = "ruby";
605+
606+
const options: FullDatabaseOptions = {
607+
dateAdded: 123,
608+
ignoreSourceArchive: false,
609+
language,
610+
};
611+
mockDbItem = createMockDB(options);
612+
613+
generateSpy = jest
614+
.spyOn(QlPackGenerator.prototype, "generate")
615+
.mockImplementation(() => Promise.resolve());
616+
});
592617

593618
describe("when the language is set", () => {
594619
it("should offer the user to set up a skeleton QL pack", async () => {
595-
const options: FullDatabaseOptions = {
596-
dateAdded: 123,
597-
ignoreSourceArchive: false,
598-
language: "ruby",
599-
};
600-
mockDbItem = createMockDB(options);
601-
602620
await (databaseManager as any).createSkeletonPacks(mockDbItem);
603621

604622
expect(showBinaryChoiceDialogSpy).toBeCalledTimes(1);
605623
});
624+
625+
it("should return early if the user refuses help", async () => {
626+
showBinaryChoiceDialogSpy = jest
627+
.spyOn(helpers, "showBinaryChoiceDialog")
628+
.mockResolvedValue(false);
629+
630+
await (databaseManager as any).createSkeletonPacks(mockDbItem);
631+
632+
expect(generateSpy).not.toBeCalled();
633+
});
634+
635+
it("should create the skeleton QL pack for the user", async () => {
636+
await (databaseManager as any).createSkeletonPacks(mockDbItem);
637+
638+
expect(generateSpy).toBeCalled();
639+
});
606640
});
607641

608642
describe("when the language is not set", () => {
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { join } from "path";
2+
import { existsSync } from "fs";
3+
import { QlPackGenerator, QueryLanguage } from "../../../src/qlpack-generator";
4+
import { CodeQLCliServer } from "../../../src/cli";
5+
import { Uri, workspace } from "vscode";
6+
import { getErrorMessage } from "../../../src/pure/helpers-pure";
7+
import * as tmp from "tmp";
8+
9+
describe("QlPackGenerator", () => {
10+
let packFolderName: string;
11+
let packFolderPath: string;
12+
let qlPackYamlFilePath: string;
13+
let exampleQlFilePath: string;
14+
let language: string;
15+
let generator: QlPackGenerator;
16+
let packAddSpy: jest.SpyInstance;
17+
let dir: tmp.DirResult;
18+
19+
beforeEach(async () => {
20+
dir = tmp.dirSync();
21+
22+
language = "ruby";
23+
packFolderName = `test-ql-pack-${language}`;
24+
packFolderPath = Uri.file(join(dir.name, packFolderName)).fsPath;
25+
26+
qlPackYamlFilePath = join(packFolderPath, "qlpack.yml");
27+
exampleQlFilePath = join(packFolderPath, "example.ql");
28+
29+
packAddSpy = jest.fn();
30+
const mockCli = {
31+
packAdd: packAddSpy,
32+
} as unknown as CodeQLCliServer;
33+
34+
generator = new QlPackGenerator(
35+
packFolderName,
36+
language as QueryLanguage,
37+
mockCli,
38+
dir.name,
39+
);
40+
});
41+
42+
afterEach(async () => {
43+
try {
44+
dir.removeCallback();
45+
46+
const workspaceFolders = workspace.workspaceFolders || [];
47+
const folderIndex = workspaceFolders.findIndex(
48+
(workspaceFolder) => workspaceFolder.name === dir.name,
49+
);
50+
51+
if (folderIndex !== undefined) {
52+
workspace.updateWorkspaceFolders(folderIndex, 1);
53+
}
54+
} catch (e) {
55+
console.log(
56+
`Could not remove folder from workspace: ${getErrorMessage(e)}`,
57+
);
58+
}
59+
});
60+
61+
it("should generate a QL pack", async () => {
62+
expect(existsSync(packFolderPath)).toBe(false);
63+
expect(existsSync(qlPackYamlFilePath)).toBe(false);
64+
expect(existsSync(exampleQlFilePath)).toBe(false);
65+
66+
await generator.generate();
67+
68+
expect(existsSync(packFolderPath)).toBe(true);
69+
expect(existsSync(qlPackYamlFilePath)).toBe(true);
70+
expect(existsSync(exampleQlFilePath)).toBe(true);
71+
72+
expect(packAddSpy).toHaveBeenCalledWith(packFolderPath, language);
73+
});
74+
});

0 commit comments

Comments
 (0)