Add validation function for modeled methods
This commit is contained in:
141
extensions/ql-vscode/src/model-editor/shared/validation.ts
Normal file
141
extensions/ql-vscode/src/model-editor/shared/validation.ts
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import { ModeledMethod } from "../modeled-method";
|
||||||
|
import { MethodSignature } from "../method";
|
||||||
|
import { assertNever } from "../../common/helpers-pure";
|
||||||
|
|
||||||
|
export type ModeledMethodValidationError = {
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
actionText: string;
|
||||||
|
index: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method will reset any properties which are not used for the specific type of modeled method.
|
||||||
|
*
|
||||||
|
* It will also set the `provenance` to `manual` since multiple modelings of the same method with a
|
||||||
|
* different provenance are not actually different.
|
||||||
|
*
|
||||||
|
* The returned canonical modeled method should only be used for comparisons. It should not be used
|
||||||
|
* for display purposes, saving the model, or any other purpose which requires the original modeled
|
||||||
|
* method to be preserved.
|
||||||
|
*
|
||||||
|
* @param modeledMethod The modeled method to canonicalize
|
||||||
|
*/
|
||||||
|
function canonicalizeModeledMethod(
|
||||||
|
modeledMethod: ModeledMethod,
|
||||||
|
): ModeledMethod {
|
||||||
|
const methodSignature: MethodSignature = {
|
||||||
|
signature: modeledMethod.signature,
|
||||||
|
packageName: modeledMethod.packageName,
|
||||||
|
typeName: modeledMethod.typeName,
|
||||||
|
methodName: modeledMethod.methodName,
|
||||||
|
methodParameters: modeledMethod.methodParameters,
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (modeledMethod.type) {
|
||||||
|
case "none":
|
||||||
|
return {
|
||||||
|
...methodSignature,
|
||||||
|
type: "none",
|
||||||
|
input: "",
|
||||||
|
output: "",
|
||||||
|
kind: "",
|
||||||
|
provenance: "manual",
|
||||||
|
};
|
||||||
|
case "source":
|
||||||
|
return {
|
||||||
|
...methodSignature,
|
||||||
|
type: "source",
|
||||||
|
input: "",
|
||||||
|
output: modeledMethod.output,
|
||||||
|
kind: modeledMethod.kind,
|
||||||
|
provenance: "manual",
|
||||||
|
};
|
||||||
|
case "sink":
|
||||||
|
return {
|
||||||
|
...methodSignature,
|
||||||
|
type: "sink",
|
||||||
|
input: modeledMethod.input,
|
||||||
|
output: "",
|
||||||
|
kind: modeledMethod.kind,
|
||||||
|
provenance: "manual",
|
||||||
|
};
|
||||||
|
case "summary":
|
||||||
|
return {
|
||||||
|
...methodSignature,
|
||||||
|
type: "summary",
|
||||||
|
input: modeledMethod.input,
|
||||||
|
output: modeledMethod.output,
|
||||||
|
kind: modeledMethod.kind,
|
||||||
|
provenance: "manual",
|
||||||
|
};
|
||||||
|
case "neutral":
|
||||||
|
return {
|
||||||
|
...methodSignature,
|
||||||
|
type: "neutral",
|
||||||
|
input: "",
|
||||||
|
output: "",
|
||||||
|
kind: "",
|
||||||
|
provenance: "manual",
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
assertNever(modeledMethod.type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateModeledMethods(
|
||||||
|
modeledMethods: ModeledMethod[],
|
||||||
|
): ModeledMethodValidationError[] {
|
||||||
|
// Anything that is not modeled will not be saved, so we don't need to validate it
|
||||||
|
const consideredModeledMethods = modeledMethods.filter(
|
||||||
|
(modeledMethod) => modeledMethod.type !== "none",
|
||||||
|
);
|
||||||
|
|
||||||
|
const result: ModeledMethodValidationError[] = [];
|
||||||
|
|
||||||
|
// If the same model is present multiple times, only the first one makes sense, so we should give
|
||||||
|
// an error for any duplicates.
|
||||||
|
const seenModeledMethods = new Set<string>();
|
||||||
|
for (const modeledMethod of consideredModeledMethods) {
|
||||||
|
const canonicalModeledMethod = canonicalizeModeledMethod(modeledMethod);
|
||||||
|
const key = JSON.stringify(canonicalModeledMethod);
|
||||||
|
|
||||||
|
if (seenModeledMethods.has(key)) {
|
||||||
|
result.push({
|
||||||
|
title: "Duplicated classification",
|
||||||
|
message:
|
||||||
|
"This method has two identical or conflicting classifications.",
|
||||||
|
actionText: "Modify or remove the duplicated classification.",
|
||||||
|
index: modeledMethods.indexOf(modeledMethod),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
seenModeledMethods.add(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const neutralModeledMethod = consideredModeledMethods.find(
|
||||||
|
(modeledMethod) => modeledMethod.type === "neutral",
|
||||||
|
);
|
||||||
|
const hasNonNeutralModeledMethod = consideredModeledMethods.some(
|
||||||
|
(modeledMethod) => modeledMethod.type !== "neutral",
|
||||||
|
);
|
||||||
|
|
||||||
|
// If there is a neutral model and any other model, that is an error
|
||||||
|
if (neutralModeledMethod && hasNonNeutralModeledMethod) {
|
||||||
|
// Another validation will validate that only one neutral method is present, so we only need
|
||||||
|
// to return an error for the first one
|
||||||
|
|
||||||
|
result.push({
|
||||||
|
title: "Conflicting classification",
|
||||||
|
message:
|
||||||
|
"This method has a neutral classification, which conflicts with other classifications.",
|
||||||
|
actionText: "Modify or remove the neutral classification.",
|
||||||
|
index: modeledMethods.indexOf(neutralModeledMethod),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by index so that the errors are always in the same order
|
||||||
|
result.sort((a, b) => a.index - b.index);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -146,7 +146,7 @@ const config: Config = {
|
|||||||
// testLocationInResults: false,
|
// testLocationInResults: false,
|
||||||
|
|
||||||
// The glob patterns Jest uses to detect test files
|
// The glob patterns Jest uses to detect test files
|
||||||
testMatch: ["**/*.test.[jt]s"],
|
testMatch: ["**/*.{test,spec}.[jt]s"],
|
||||||
|
|
||||||
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
||||||
// testPathIgnorePatterns: [
|
// testPathIgnorePatterns: [
|
||||||
|
|||||||
@@ -0,0 +1,328 @@
|
|||||||
|
import { validateModeledMethods } from "../../../../src/model-editor/shared/validation";
|
||||||
|
import { createModeledMethod } from "../../../factories/model-editor/modeled-method-factories";
|
||||||
|
|
||||||
|
describe(validateModeledMethods.name, () => {
|
||||||
|
it("should not give an error with valid modeled methods", () => {
|
||||||
|
const modeledMethods = [
|
||||||
|
createModeledMethod({
|
||||||
|
type: "source",
|
||||||
|
output: "ReturnValue",
|
||||||
|
}),
|
||||||
|
createModeledMethod({
|
||||||
|
type: "sink",
|
||||||
|
input: "Argument[this]",
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const errors = validateModeledMethods(modeledMethods);
|
||||||
|
|
||||||
|
expect(errors).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not give an error with valid modeled methods and an unmodeled method", () => {
|
||||||
|
const modeledMethods = [
|
||||||
|
createModeledMethod({
|
||||||
|
type: "source",
|
||||||
|
output: "ReturnValue",
|
||||||
|
}),
|
||||||
|
createModeledMethod({
|
||||||
|
type: "sink",
|
||||||
|
input: "Argument[this]",
|
||||||
|
}),
|
||||||
|
createModeledMethod({
|
||||||
|
type: "none",
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const errors = validateModeledMethods(modeledMethods);
|
||||||
|
|
||||||
|
expect(errors).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not give an error with valid modeled methods and multiple unmodeled methods", () => {
|
||||||
|
const modeledMethods = [
|
||||||
|
createModeledMethod({
|
||||||
|
type: "none",
|
||||||
|
}),
|
||||||
|
createModeledMethod({
|
||||||
|
type: "source",
|
||||||
|
output: "ReturnValue",
|
||||||
|
}),
|
||||||
|
createModeledMethod({
|
||||||
|
type: "sink",
|
||||||
|
input: "Argument[this]",
|
||||||
|
}),
|
||||||
|
createModeledMethod({
|
||||||
|
type: "none",
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const errors = validateModeledMethods(modeledMethods);
|
||||||
|
|
||||||
|
expect(errors).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not give an error with a single neutral model", () => {
|
||||||
|
const modeledMethods = [
|
||||||
|
createModeledMethod({
|
||||||
|
type: "neutral",
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const errors = validateModeledMethods(modeledMethods);
|
||||||
|
|
||||||
|
expect(errors).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not give an error with a neutral model and an unmodeled method", () => {
|
||||||
|
const modeledMethods = [
|
||||||
|
createModeledMethod({
|
||||||
|
type: "neutral",
|
||||||
|
}),
|
||||||
|
createModeledMethod({
|
||||||
|
type: "none",
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const errors = validateModeledMethods(modeledMethods);
|
||||||
|
|
||||||
|
expect(errors).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should give an error with exact duplicate modeled methods", () => {
|
||||||
|
const modeledMethods = [createModeledMethod(), createModeledMethod()];
|
||||||
|
|
||||||
|
const errors = validateModeledMethods(modeledMethods);
|
||||||
|
|
||||||
|
expect(errors).toEqual([
|
||||||
|
{
|
||||||
|
index: 1,
|
||||||
|
title: expect.stringMatching(/duplicate/i),
|
||||||
|
message: expect.stringMatching(/identical/i),
|
||||||
|
actionText: expect.stringMatching(/remove/i),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should give an error with duplicate modeled methods with different provenance", () => {
|
||||||
|
const modeledMethods = [
|
||||||
|
createModeledMethod({
|
||||||
|
provenance: "df-generated",
|
||||||
|
}),
|
||||||
|
createModeledMethod({
|
||||||
|
provenance: "manual",
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const errors = validateModeledMethods(modeledMethods);
|
||||||
|
|
||||||
|
expect(errors).toEqual([
|
||||||
|
{
|
||||||
|
index: 1,
|
||||||
|
title: expect.stringMatching(/duplicate/i),
|
||||||
|
message: expect.stringMatching(/identical/i),
|
||||||
|
actionText: expect.stringMatching(/remove/i),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should give an error with duplicate modeled methods with different source unused fields", () => {
|
||||||
|
const modeledMethods = [
|
||||||
|
createModeledMethod({
|
||||||
|
type: "source",
|
||||||
|
input: "Argument[this]",
|
||||||
|
output: "ReturnValue",
|
||||||
|
}),
|
||||||
|
createModeledMethod({
|
||||||
|
type: "source",
|
||||||
|
input: "Argument[1]",
|
||||||
|
output: "ReturnValue",
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const errors = validateModeledMethods(modeledMethods);
|
||||||
|
|
||||||
|
expect(errors).toEqual([
|
||||||
|
{
|
||||||
|
index: 1,
|
||||||
|
title: expect.stringMatching(/duplicate/i),
|
||||||
|
message: expect.stringMatching(/identical/i),
|
||||||
|
actionText: expect.stringMatching(/remove/i),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should give an error with duplicate modeled methods with different sink unused fields", () => {
|
||||||
|
const modeledMethods = [
|
||||||
|
createModeledMethod({
|
||||||
|
type: "sink",
|
||||||
|
input: "Argument[this]",
|
||||||
|
output: "ReturnValue",
|
||||||
|
}),
|
||||||
|
createModeledMethod({
|
||||||
|
type: "sink",
|
||||||
|
input: "Argument[this]",
|
||||||
|
output: "Argument[this]",
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const errors = validateModeledMethods(modeledMethods);
|
||||||
|
|
||||||
|
expect(errors).toEqual([
|
||||||
|
{
|
||||||
|
index: 1,
|
||||||
|
title: expect.stringMatching(/duplicate/i),
|
||||||
|
message: expect.stringMatching(/identical/i),
|
||||||
|
actionText: expect.stringMatching(/remove/i),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should give an error with duplicate modeled methods with different summary unused fields", () => {
|
||||||
|
const supportedTrue = {
|
||||||
|
supported: true,
|
||||||
|
};
|
||||||
|
const supportedFalse = {
|
||||||
|
supported: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const modeledMethods = [
|
||||||
|
createModeledMethod({
|
||||||
|
type: "sink",
|
||||||
|
input: "Argument[this]",
|
||||||
|
output: "ReturnValue",
|
||||||
|
...supportedTrue,
|
||||||
|
}),
|
||||||
|
createModeledMethod({
|
||||||
|
type: "sink",
|
||||||
|
input: "Argument[this]",
|
||||||
|
output: "Argument[this]",
|
||||||
|
...supportedFalse,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const errors = validateModeledMethods(modeledMethods);
|
||||||
|
|
||||||
|
expect(errors).toEqual([
|
||||||
|
{
|
||||||
|
index: 1,
|
||||||
|
title: expect.stringMatching(/duplicate/i),
|
||||||
|
message: expect.stringMatching(/identical/i),
|
||||||
|
actionText: expect.stringMatching(/remove/i),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should give an error with duplicate modeled methods with different neutral unused fields", () => {
|
||||||
|
const modeledMethods = [
|
||||||
|
createModeledMethod({
|
||||||
|
type: "neutral",
|
||||||
|
input: "Argument[this]",
|
||||||
|
output: "ReturnValue",
|
||||||
|
}),
|
||||||
|
createModeledMethod({
|
||||||
|
type: "neutral",
|
||||||
|
input: "Argument[1]",
|
||||||
|
output: "Argument[this]",
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const errors = validateModeledMethods(modeledMethods);
|
||||||
|
|
||||||
|
expect(errors).toEqual([
|
||||||
|
{
|
||||||
|
index: 1,
|
||||||
|
title: expect.stringMatching(/duplicate/i),
|
||||||
|
message: expect.stringMatching(/identical/i),
|
||||||
|
actionText: expect.stringMatching(/remove/i),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should give an error with neutral combined with other models", () => {
|
||||||
|
const modeledMethods = [
|
||||||
|
createModeledMethod({
|
||||||
|
type: "sink",
|
||||||
|
}),
|
||||||
|
createModeledMethod({
|
||||||
|
type: "neutral",
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const errors = validateModeledMethods(modeledMethods);
|
||||||
|
|
||||||
|
expect(errors).toEqual([
|
||||||
|
{
|
||||||
|
index: 1,
|
||||||
|
title: expect.stringMatching(/conflicting/i),
|
||||||
|
message: expect.stringMatching(/neutral/i),
|
||||||
|
actionText: expect.stringMatching(/remove/i),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should give an error with duplicate neutral combined with other models", () => {
|
||||||
|
const modeledMethods = [
|
||||||
|
createModeledMethod({
|
||||||
|
type: "neutral",
|
||||||
|
}),
|
||||||
|
createModeledMethod({
|
||||||
|
type: "sink",
|
||||||
|
}),
|
||||||
|
createModeledMethod({
|
||||||
|
type: "neutral",
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const errors = validateModeledMethods(modeledMethods);
|
||||||
|
|
||||||
|
expect(errors).toEqual([
|
||||||
|
{
|
||||||
|
index: 0,
|
||||||
|
title: expect.stringMatching(/conflicting/i),
|
||||||
|
message: expect.stringMatching(/neutral/i),
|
||||||
|
actionText: expect.stringMatching(/remove/i),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
index: 2,
|
||||||
|
title: expect.stringMatching(/duplicate/i),
|
||||||
|
message: expect.stringMatching(/identical/i),
|
||||||
|
actionText: expect.stringMatching(/remove/i),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should include unmodeled methods in the index", () => {
|
||||||
|
const modeledMethods = [
|
||||||
|
createModeledMethod({
|
||||||
|
type: "none",
|
||||||
|
}),
|
||||||
|
createModeledMethod({
|
||||||
|
type: "neutral",
|
||||||
|
}),
|
||||||
|
createModeledMethod({
|
||||||
|
type: "sink",
|
||||||
|
}),
|
||||||
|
createModeledMethod({
|
||||||
|
type: "neutral",
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const errors = validateModeledMethods(modeledMethods);
|
||||||
|
|
||||||
|
expect(errors).toEqual([
|
||||||
|
{
|
||||||
|
index: 1,
|
||||||
|
title: expect.stringMatching(/conflicting/i),
|
||||||
|
message: expect.stringMatching(/neutral/i),
|
||||||
|
actionText: expect.stringMatching(/remove/i),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
index: 3,
|
||||||
|
title: expect.stringMatching(/duplicate/i),
|
||||||
|
message: expect.stringMatching(/identical/i),
|
||||||
|
actionText: expect.stringMatching(/remove/i),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user