diff --git a/extensions/ql-vscode/scripts/generate-schemas.ts b/extensions/ql-vscode/scripts/generate-schemas.ts index 26e48bd8f..e767593c6 100644 --- a/extensions/ql-vscode/scripts/generate-schemas.ts +++ b/extensions/ql-vscode/scripts/generate-schemas.ts @@ -20,6 +20,21 @@ const schemas = [ "extension-pack-metadata.schema.json", ), }, + { + path: join( + extensionDirectory, + "src", + "model-editor", + "model-extension-file.ts", + ), + type: "ModelExtensionFile", + schemaPath: join( + extensionDirectory, + "src", + "model-editor", + "model-extension-file.schema.json", + ), + }, ]; async function generateSchemas() { diff --git a/extensions/ql-vscode/src/model-editor/data-schema.json b/extensions/ql-vscode/src/model-editor/data-schema.json deleted file mode 100644 index fab11d91a..000000000 --- a/extensions/ql-vscode/src/model-editor/data-schema.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "type": "object", - "properties": { - "extensions": { - "type": "array", - "items": { - "type": "object", - "required": ["addsTo", "data"], - "properties": { - "addsTo": { - "type": "object", - "required": ["pack", "extensible"], - "properties": { - "pack": { - "type": "string" - }, - "extensible": { - "type": "string" - } - } - }, - "data": { - "type": "array", - "items": { - "type": "array", - "items": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "boolean" - }, - { - "type": "number" - } - ] - } - } - } - } - } - } - } -} diff --git a/extensions/ql-vscode/src/model-editor/model-extension-file.schema.json b/extensions/ql-vscode/src/model-editor/model-extension-file.schema.json new file mode 100644 index 000000000..da1a62fff --- /dev/null +++ b/extensions/ql-vscode/src/model-editor/model-extension-file.schema.json @@ -0,0 +1,54 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/ModelExtensionFile", + "definitions": { + "ModelExtensionFile": { + "type": "object", + "properties": { + "extensions": { + "type": "array", + "items": { + "$ref": "#/definitions/ModelExtension" + } + } + }, + "required": ["extensions"] + }, + "ModelExtension": { + "type": "object", + "properties": { + "addsTo": { + "$ref": "#/definitions/ExtensibleReference" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/DataRow" + } + } + }, + "required": ["addsTo", "data"] + }, + "ExtensibleReference": { + "type": "object", + "properties": { + "pack": { + "type": "string" + }, + "extensible": { + "type": "string" + } + }, + "required": ["pack", "extensible"] + }, + "DataRow": { + "type": "array", + "items": { + "$ref": "#/definitions/DataTuple" + } + }, + "DataTuple": { + "type": ["boolean", "number", "string"] + } + } +} diff --git a/extensions/ql-vscode/src/model-editor/model-extension-file.ts b/extensions/ql-vscode/src/model-editor/model-extension-file.ts new file mode 100644 index 000000000..78a4676c5 --- /dev/null +++ b/extensions/ql-vscode/src/model-editor/model-extension-file.ts @@ -0,0 +1,17 @@ +export type ExtensibleReference = { + pack: string; + extensible: string; +}; + +export type DataTuple = boolean | number | string; + +export type DataRow = DataTuple[]; + +export type ModelExtension = { + addsTo: ExtensibleReference; + data: DataRow[]; +}; + +export type ModelExtensionFile = { + extensions: ModelExtension[]; +}; diff --git a/extensions/ql-vscode/src/model-editor/predicates.ts b/extensions/ql-vscode/src/model-editor/predicates.ts index de4aa0a90..8793f7e97 100644 --- a/extensions/ql-vscode/src/model-editor/predicates.ts +++ b/extensions/ql-vscode/src/model-editor/predicates.ts @@ -1,16 +1,15 @@ import { ModeledMethod, ModeledMethodType, Provenance } from "./modeled-method"; +import { DataTuple } from "./model-extension-file"; export type ExtensiblePredicateDefinition = { extensiblePredicate: string; - generateMethodDefinition: (method: ModeledMethod) => Tuple[]; - readModeledMethod: (row: Tuple[]) => ModeledMethod; + generateMethodDefinition: (method: ModeledMethod) => DataTuple[]; + readModeledMethod: (row: DataTuple[]) => ModeledMethod; supportedKinds?: string[]; }; -type Tuple = boolean | number | string; - -function readRowToMethod(row: Tuple[]): string { +function readRowToMethod(row: DataTuple[]): string { return `${row[0]}.${row[1]}#${row[3]}${row[4]}`; } diff --git a/extensions/ql-vscode/src/model-editor/yaml.ts b/extensions/ql-vscode/src/model-editor/yaml.ts index e02e6a022..c1c704481 100644 --- a/extensions/ql-vscode/src/model-editor/yaml.ts +++ b/extensions/ql-vscode/src/model-editor/yaml.ts @@ -7,12 +7,13 @@ import { extensiblePredicateDefinitions, } from "./predicates"; -import * as dataSchemaJson from "./data-schema.json"; +import * as modelExtensionFileSchema from "./model-extension-file.schema.json"; import { Mode } from "./shared/mode"; import { assertNever } from "../common/helpers-pure"; +import { ModelExtensionFile } from "./model-extension-file"; -const ajv = new Ajv({ allErrors: true }); -const dataSchemaValidate = ajv.compile(dataSchemaJson); +const ajv = new Ajv({ allErrors: true, allowUnionTypes: true }); +const modelExtensionFileSchemaValidate = ajv.compile(modelExtensionFileSchema); function createDataProperty( methods: ModeledMethod[], @@ -211,24 +212,29 @@ export function createFilenameForPackage( return `${prefix}${packageName}${suffix}.yml`; } -export function loadDataExtensionYaml( - data: any, -): Record | undefined { - dataSchemaValidate(data); +function validateModelExtensionFile(data: unknown): data is ModelExtensionFile { + modelExtensionFileSchemaValidate(data); - if (dataSchemaValidate.errors) { + if (modelExtensionFileSchemaValidate.errors) { throw new Error( - `Invalid data extension YAML: ${dataSchemaValidate.errors + `Invalid data extension YAML: ${modelExtensionFileSchemaValidate.errors .map((error) => `${error.instancePath} ${error.message}`) .join(", ")}`, ); } - const extensions = data.extensions; - if (!Array.isArray(extensions)) { + return true; +} + +export function loadDataExtensionYaml( + data: unknown, +): Record | undefined { + if (!validateModelExtensionFile(data)) { return undefined; } + const extensions = data.extensions; + const modeledMethods: Record = {}; for (const extension of extensions) { diff --git a/extensions/ql-vscode/test/vscode-tests/no-workspace/model-editor/auto-model-codeml-queries.test.ts b/extensions/ql-vscode/test/vscode-tests/no-workspace/model-editor/auto-model-codeml-queries.test.ts index 465db8ebd..161c88210 100644 --- a/extensions/ql-vscode/test/vscode-tests/no-workspace/model-editor/auto-model-codeml-queries.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/no-workspace/model-editor/auto-model-codeml-queries.test.ts @@ -20,6 +20,7 @@ import { pathExists, readFile } from "fs-extra"; import { load as loadYaml } from "js-yaml"; import { CancellationTokenSource } from "vscode-jsonrpc"; import { QueryOutputDir } from "../../../../src/run-queries-shared"; +import { ModelExtensionFile } from "../../../../src/model-editor/model-extension-file"; describe("runAutoModelQueries", () => { let resolveQueriesSpy: jest.SpiedFunction< @@ -186,7 +187,9 @@ describe("generateCandidateFilterPack", () => { const filterFile = join(packDir, "filter.yml"); expect(await pathExists(filterFile)).toBe(true); // Read the contents of filterFile and parse as yaml - const yaml = await loadYaml(await readFile(filterFile, "utf8")); + const yaml = (await loadYaml( + await readFile(filterFile, "utf8"), + )) as ModelExtensionFile; const extensions = yaml.extensions; expect(extensions).toBeInstanceOf(Array); expect(extensions).toHaveLength(1);