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.
This commit is contained in:
Koen Vlaswinkel
2023-09-25 15:12:54 +02:00
parent e43adb6424
commit db55e9cd42
7 changed files with 195 additions and 0 deletions

View File

@@ -99,6 +99,42 @@ jobs:
run: |
npm run find-deadcode
generated:
name: Check generated code
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 1
- uses: actions/setup-node@v3
with:
node-version: '16.17.1'
cache: 'npm'
cache-dependency-path: extensions/ql-vscode/package-lock.json
- name: Install dependencies
working-directory: extensions/ql-vscode
run: |
npm ci
shell: bash
- name: Check that repo is clean
run: |
git diff --exit-code
git diff --exit-code --cached
- name: Generate code
working-directory: extensions/ql-vscode
run: |
npm run generate
- name: Check for changes
run: |
git diff --exit-code
git diff --exit-code --cached
unit-test:
name: Unit Test
runs-on: ${{ matrix.os }}

View File

@@ -1842,6 +1842,8 @@
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"lint:scenarios": "ts-node scripts/lint-scenarios.ts",
"generate": "npm-run-all -p generate:*",
"generate:schemas": "ts-node scripts/generate-schemas.ts",
"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\"",
"postinstall": "patch-package",
"prepare": "cd ../.. && husky install"

View File

@@ -0,0 +1,45 @@
import { createGenerator } from "ts-json-schema-generator";
import { join, resolve } from "path";
import { outputJSON } from "fs-extra";
const extensionDirectory = resolve(__dirname, "..");
const schemas = [
{
path: join(
extensionDirectory,
"src",
"model-editor",
"extension-pack-metadata.ts",
),
type: "ExtensionPackMetadata",
schemaPath: join(
extensionDirectory,
"src",
"model-editor",
"extension-pack-metadata.schema.json",
),
},
];
async function generateSchemas() {
for (const schemaDefinition of schemas) {
const schema = createGenerator({
path: schemaDefinition.path,
tsconfig: resolve(extensionDirectory, "tsconfig.json"),
type: schemaDefinition.type,
skipTypeCheck: true,
topRef: true,
additionalProperties: true,
}).createSchema(schemaDefinition.type);
await outputJSON(schemaDefinition.schemaPath, schema, {
spaces: 2,
});
}
}
generateSchemas().catch((e: unknown) => {
console.error(e);
process.exit(2);
});

View File

@@ -0,0 +1,42 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$ref": "#/definitions/ExtensionPackMetadata",
"definitions": {
"ExtensionPackMetadata": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"version": {
"type": "string"
},
"dataExtensions": {
"anyOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
},
"extensionTargets": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"required": [
"name",
"version",
"dataExtensions",
"extensionTargets"
]
}
}
}

View File

@@ -0,0 +1,6 @@
export type ExtensionPackMetadata = {
name: string;
version: string;
dataExtensions: string | string[];
extensionTargets: Record<string, string>;
};

View File

@@ -2,6 +2,7 @@ import { join } from "path";
import { outputFile, pathExists, readFile } from "fs-extra";
import { dump as dumpYaml, load as loadYaml } from "js-yaml";
import { Uri } from "vscode";
import Ajv from "ajv";
import { CodeQLCliServer } from "../codeql-cli/cli";
import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
import { ProgressCallback } from "../common/vscode/progress";
@@ -18,6 +19,12 @@ import {
} from "./extension-pack-name";
import { autoPickExtensionsDirectory } from "./extensions-workspace-folder";
import { ExtensionPackMetadata } from "./extension-pack-metadata";
import * as extensionPackMetadataSchemaJson from "./extension-pack-metadata.schema.json";
const ajv = new Ajv({ allErrors: true });
const extensionPackValidate = ajv.compile(extensionPackMetadataSchemaJson);
export async function pickExtensionPack(
cliServer: Pick<CodeQLCliServer, "resolveQlpacks">,
databaseItem: Pick<DatabaseItem, "name" | "language">,
@@ -170,6 +177,22 @@ async function writeExtensionPack(
return extensionPack;
}
function validateExtensionPack(
extensionPack: unknown,
): extensionPack is ExtensionPackMetadata {
extensionPackValidate(extensionPack);
if (extensionPackValidate.errors) {
throw new Error(
`Invalid extension pack YAML: ${extensionPackValidate.errors
.map((error) => `${error.instancePath} ${error.message}`)
.join(", ")}`,
);
}
return true;
}
async function readExtensionPack(
path: string,
language: string,
@@ -188,6 +211,10 @@ async function readExtensionPack(
throw new Error(`Could not parse ${qlpackPath}`);
}
if (!validateExtensionPack(qlpack)) {
throw new Error(`Could not validate ${qlpackPath}`);
}
const dataExtensionValue = qlpack.dataExtensions;
if (
!(

View File

@@ -363,6 +363,43 @@ describe("pickExtensionPack", () => {
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
});
it("shows an error when the pack YAML does not contain name", async () => {
const tmpDir = await dir({
unsafeCleanup: true,
});
const cliServer = mockCliServer({
"github/vscode-codeql-java": [tmpDir.path],
});
await outputFile(
join(tmpDir.path, "codeql-pack.yml"),
dumpYaml({
version: "0.0.0",
library: true,
extensionTargets: {
"codeql/java-all": "*",
},
dataExtensions: ["models/**/*.yml"],
}),
);
expect(
await pickExtensionPack(
cliServer,
databaseItem,
logger,
progress,
maxStep,
),
).toEqual(undefined);
expect(logger.showErrorMessage).toHaveBeenCalledTimes(1);
expect(logger.showErrorMessage).toHaveBeenCalledWith(
"Could not read extension pack github/vscode-codeql-java",
);
expect(cliServer.resolveQlpacks).toHaveBeenCalled();
});
it("shows an error when the pack YAML does not contain dataExtensions", async () => {
const tmpDir = await dir({
unsafeCleanup: true,