Skip to content

Commit db55e9c

Browse files
committed
Generate schema for extension pack metadata
After the upgrade to the correct types for js-yaml, the return type of `load` is correctly typed as `unknown`. This means that we can't use the return value directly, but need to validate it first. This adds such validation by generating a JSON schema for a newly created typed. The JSON schema generation is very similar to how we do it in https://github.com/github/codeql-variant-analysis-action.
1 parent e43adb6 commit db55e9c

7 files changed

Lines changed: 195 additions & 0 deletions

File tree

.github/workflows/main.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,42 @@ jobs:
9999
run: |
100100
npm run find-deadcode
101101
102+
generated:
103+
name: Check generated code
104+
runs-on: ubuntu-latest
105+
steps:
106+
- name: Checkout
107+
uses: actions/checkout@v4
108+
with:
109+
fetch-depth: 1
110+
111+
- uses: actions/setup-node@v3
112+
with:
113+
node-version: '16.17.1'
114+
cache: 'npm'
115+
cache-dependency-path: extensions/ql-vscode/package-lock.json
116+
117+
- name: Install dependencies
118+
working-directory: extensions/ql-vscode
119+
run: |
120+
npm ci
121+
shell: bash
122+
123+
- name: Check that repo is clean
124+
run: |
125+
git diff --exit-code
126+
git diff --exit-code --cached
127+
128+
- name: Generate code
129+
working-directory: extensions/ql-vscode
130+
run: |
131+
npm run generate
132+
133+
- name: Check for changes
134+
run: |
135+
git diff --exit-code
136+
git diff --exit-code --cached
137+
102138
unit-test:
103139
name: Unit Test
104140
runs-on: ${{ matrix.os }}

extensions/ql-vscode/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1842,6 +1842,8 @@
18421842
"storybook": "storybook dev -p 6006",
18431843
"build-storybook": "storybook build",
18441844
"lint:scenarios": "ts-node scripts/lint-scenarios.ts",
1845+
"generate": "npm-run-all -p generate:*",
1846+
"generate:schemas": "ts-node scripts/generate-schemas.ts",
18451847
"check-types": "find . -type f -name \"tsconfig.json\" -not -path \"./node_modules/*\" | sed -r 's|/[^/]+$||' | sort | uniq | xargs -I {} sh -c \"echo Checking types in {} && cd {} && npx tsc --noEmit\"",
18461848
"postinstall": "patch-package",
18471849
"prepare": "cd ../.. && husky install"
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { createGenerator } from "ts-json-schema-generator";
2+
import { join, resolve } from "path";
3+
import { outputJSON } from "fs-extra";
4+
5+
const extensionDirectory = resolve(__dirname, "..");
6+
7+
const schemas = [
8+
{
9+
path: join(
10+
extensionDirectory,
11+
"src",
12+
"model-editor",
13+
"extension-pack-metadata.ts",
14+
),
15+
type: "ExtensionPackMetadata",
16+
schemaPath: join(
17+
extensionDirectory,
18+
"src",
19+
"model-editor",
20+
"extension-pack-metadata.schema.json",
21+
),
22+
},
23+
];
24+
25+
async function generateSchemas() {
26+
for (const schemaDefinition of schemas) {
27+
const schema = createGenerator({
28+
path: schemaDefinition.path,
29+
tsconfig: resolve(extensionDirectory, "tsconfig.json"),
30+
type: schemaDefinition.type,
31+
skipTypeCheck: true,
32+
topRef: true,
33+
additionalProperties: true,
34+
}).createSchema(schemaDefinition.type);
35+
36+
await outputJSON(schemaDefinition.schemaPath, schema, {
37+
spaces: 2,
38+
});
39+
}
40+
}
41+
42+
generateSchemas().catch((e: unknown) => {
43+
console.error(e);
44+
process.exit(2);
45+
});
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"$ref": "#/definitions/ExtensionPackMetadata",
4+
"definitions": {
5+
"ExtensionPackMetadata": {
6+
"type": "object",
7+
"properties": {
8+
"name": {
9+
"type": "string"
10+
},
11+
"version": {
12+
"type": "string"
13+
},
14+
"dataExtensions": {
15+
"anyOf": [
16+
{
17+
"type": "string"
18+
},
19+
{
20+
"type": "array",
21+
"items": {
22+
"type": "string"
23+
}
24+
}
25+
]
26+
},
27+
"extensionTargets": {
28+
"type": "object",
29+
"additionalProperties": {
30+
"type": "string"
31+
}
32+
}
33+
},
34+
"required": [
35+
"name",
36+
"version",
37+
"dataExtensions",
38+
"extensionTargets"
39+
]
40+
}
41+
}
42+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export type ExtensionPackMetadata = {
2+
name: string;
3+
version: string;
4+
dataExtensions: string | string[];
5+
extensionTargets: Record<string, string>;
6+
};

extensions/ql-vscode/src/model-editor/extension-pack-picker.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { join } from "path";
22
import { outputFile, pathExists, readFile } from "fs-extra";
33
import { dump as dumpYaml, load as loadYaml } from "js-yaml";
44
import { Uri } from "vscode";
5+
import Ajv from "ajv";
56
import { CodeQLCliServer } from "../codeql-cli/cli";
67
import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
78
import { ProgressCallback } from "../common/vscode/progress";
@@ -18,6 +19,12 @@ import {
1819
} from "./extension-pack-name";
1920
import { autoPickExtensionsDirectory } from "./extensions-workspace-folder";
2021

22+
import { ExtensionPackMetadata } from "./extension-pack-metadata";
23+
import * as extensionPackMetadataSchemaJson from "./extension-pack-metadata.schema.json";
24+
25+
const ajv = new Ajv({ allErrors: true });
26+
const extensionPackValidate = ajv.compile(extensionPackMetadataSchemaJson);
27+
2128
export async function pickExtensionPack(
2229
cliServer: Pick<CodeQLCliServer, "resolveQlpacks">,
2330
databaseItem: Pick<DatabaseItem, "name" | "language">,
@@ -170,6 +177,22 @@ async function writeExtensionPack(
170177
return extensionPack;
171178
}
172179

180+
function validateExtensionPack(
181+
extensionPack: unknown,
182+
): extensionPack is ExtensionPackMetadata {
183+
extensionPackValidate(extensionPack);
184+
185+
if (extensionPackValidate.errors) {
186+
throw new Error(
187+
`Invalid extension pack YAML: ${extensionPackValidate.errors
188+
.map((error) => `${error.instancePath} ${error.message}`)
189+
.join(", ")}`,
190+
);
191+
}
192+
193+
return true;
194+
}
195+
173196
async function readExtensionPack(
174197
path: string,
175198
language: string,
@@ -188,6 +211,10 @@ async function readExtensionPack(
188211
throw new Error(`Could not parse ${qlpackPath}`);
189212
}
190213

214+
if (!validateExtensionPack(qlpack)) {
215+
throw new Error(`Could not validate ${qlpackPath}`);
216+
}
217+
191218
const dataExtensionValue = qlpack.dataExtensions;
192219
if (
193220
!(

extensions/ql-vscode/test/vscode-tests/no-workspace/model-editor/extension-pack-picker.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,43 @@ describe("pickExtensionPack", () => {
363363
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
364364
});
365365

366+
it("shows an error when the pack YAML does not contain name", async () => {
367+
const tmpDir = await dir({
368+
unsafeCleanup: true,
369+
});
370+
371+
const cliServer = mockCliServer({
372+
"github/vscode-codeql-java": [tmpDir.path],
373+
});
374+
375+
await outputFile(
376+
join(tmpDir.path, "codeql-pack.yml"),
377+
dumpYaml({
378+
version: "0.0.0",
379+
library: true,
380+
extensionTargets: {
381+
"codeql/java-all": "*",
382+
},
383+
dataExtensions: ["models/**/*.yml"],
384+
}),
385+
);
386+
387+
expect(
388+
await pickExtensionPack(
389+
cliServer,
390+
databaseItem,
391+
logger,
392+
progress,
393+
maxStep,
394+
),
395+
).toEqual(undefined);
396+
expect(logger.showErrorMessage).toHaveBeenCalledTimes(1);
397+
expect(logger.showErrorMessage).toHaveBeenCalledWith(
398+
"Could not read extension pack github/vscode-codeql-java",
399+
);
400+
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
401+
});
402+
366403
it("shows an error when the pack YAML does not contain dataExtensions", async () => {
367404
const tmpDir = await dir({
368405
unsafeCleanup: true,

0 commit comments

Comments
 (0)