Merge pull request #91 from github/cschleiden/improve-expression-validation
Validate expressions even when calling `fromJson`
This commit is contained in:
@@ -50,3 +50,5 @@ function errorDescription(typ: ErrorType): string {
|
||||
return "Unknown error";
|
||||
}
|
||||
}
|
||||
|
||||
export class ExpressionEvaluationError extends Error {}
|
||||
|
||||
@@ -1,27 +1,42 @@
|
||||
import * as data from "./data";
|
||||
import {ExpressionEvaluationError} from "./errors";
|
||||
import {Evaluator} from "./evaluator";
|
||||
import {Lexer} from "./lexer";
|
||||
import {Parser} from "./parser";
|
||||
|
||||
test("evaluator", () => {
|
||||
const input = "foo['']";
|
||||
describe("evaluator", () => {
|
||||
const lexAndParse = (input: string) => {
|
||||
const lexer = new Lexer(input);
|
||||
const result = lexer.lex();
|
||||
|
||||
const lexer = new Lexer(input);
|
||||
const result = lexer.lex();
|
||||
// Parse
|
||||
const parser = new Parser(result.tokens, ["foo"], []);
|
||||
const expr = parser.parse();
|
||||
return expr;
|
||||
};
|
||||
|
||||
// Parse
|
||||
const parser = new Parser(result.tokens, ["foo"], []);
|
||||
const expr = parser.parse();
|
||||
it("basic evaluation", () => {
|
||||
const expr = lexAndParse("foo['']");
|
||||
|
||||
// Evaluate expression
|
||||
const evaluator = new Evaluator(
|
||||
expr,
|
||||
new data.Dictionary({
|
||||
key: "foo",
|
||||
value: new data.Dictionary({key: "bar", value: new data.NumberData(42)})
|
||||
})
|
||||
);
|
||||
const eresult = evaluator.evaluate();
|
||||
// Evaluate expression
|
||||
const evaluator = new Evaluator(
|
||||
expr,
|
||||
new data.Dictionary({
|
||||
key: "foo",
|
||||
value: new data.Dictionary({key: "bar", value: new data.NumberData(42)})
|
||||
})
|
||||
);
|
||||
const eresult = evaluator.evaluate();
|
||||
|
||||
expect(eresult.kind).toBe(data.Kind.Null);
|
||||
expect(eresult.kind).toBe(data.Kind.Null);
|
||||
});
|
||||
|
||||
it("handle runtime errors", () => {
|
||||
const expr = lexAndParse("fromJson('test') == 123");
|
||||
const evaluator = new Evaluator(expr, new data.Dictionary());
|
||||
|
||||
expect(() => evaluator.evaluate()).toThrowError(
|
||||
new ExpressionEvaluationError("Error parsing JSON when evaluating fromJson")
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,14 +14,19 @@ import {
|
||||
import * as data from "./data";
|
||||
import {FilteredArray} from "./filtered_array";
|
||||
import {wellKnownFunctions} from "./funcs";
|
||||
import {FunctionDefinition} from "./funcs/info";
|
||||
import {idxHelper} from "./idxHelper";
|
||||
import {TokenType} from "./lexer";
|
||||
import {equals, falsy, greaterThan, lessThan, truthy} from "./result";
|
||||
|
||||
export class EvaluationError extends Error {}
|
||||
|
||||
export class Evaluator implements ExprVisitor<data.ExpressionData> {
|
||||
constructor(private n: Expr, private context: data.Dictionary) {}
|
||||
/**
|
||||
* Creates a new evaluator
|
||||
* @param n Parsed expression to evaluate
|
||||
* @param context Context data to use
|
||||
* @param functions Optional map of function implementations. If given, these will be preferred over the built-in functions.
|
||||
*/
|
||||
constructor(private n: Expr, private context: data.Dictionary, private functions?: Map<string, FunctionDefinition>) {}
|
||||
|
||||
public evaluate(): data.ExpressionData {
|
||||
return this.eval(this.n);
|
||||
@@ -109,7 +114,7 @@ export class Evaluator implements ExprVisitor<data.ExpressionData> {
|
||||
try {
|
||||
idxResult = this.eval(ia.index);
|
||||
} catch (e) {
|
||||
throw new Error(`could not evaluate index for index access: ${e}`);
|
||||
throw new Error(`could not evaluate index for index access: ${e}`, {cause: e});
|
||||
}
|
||||
idx = new idxHelper(false, idxResult);
|
||||
}
|
||||
@@ -150,16 +155,14 @@ export class Evaluator implements ExprVisitor<data.ExpressionData> {
|
||||
// Evaluate arguments
|
||||
const args = functionCall.args.map(arg => this.eval(arg));
|
||||
|
||||
return fcall(functionCall, args);
|
||||
// Get function definitions
|
||||
const functionName = functionCall.functionName.lexeme.toLowerCase();
|
||||
const f = this.functions?.get(functionName) || wellKnownFunctions[functionName];
|
||||
|
||||
return f.call(...args);
|
||||
}
|
||||
}
|
||||
|
||||
function fcall(fc: FunctionCall, args: data.ExpressionData[]): data.ExpressionData {
|
||||
const f = wellKnownFunctions[fc.functionName.lexeme.toLowerCase()];
|
||||
|
||||
return f.call(...args);
|
||||
}
|
||||
|
||||
function filteredArrayAccess(fa: FilteredArray, idx: idxHelper): data.ExpressionData {
|
||||
const result = new FilteredArray();
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {ExpressionData} from "../data";
|
||||
import {reviver} from "../data/reviver";
|
||||
import {ExpressionEvaluationError} from "../errors";
|
||||
import {FunctionDefinition} from "./info";
|
||||
|
||||
export const fromjson: FunctionDefinition = {
|
||||
@@ -16,7 +17,10 @@ export const fromjson: FunctionDefinition = {
|
||||
throw new Error("empty input");
|
||||
}
|
||||
|
||||
const d = JSON.parse(is, reviver);
|
||||
return d;
|
||||
try {
|
||||
return JSON.parse(is, reviver);
|
||||
} catch (e) {
|
||||
throw new ExpressionEvaluationError("Error parsing JSON when evaluating fromJson", {cause: e});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,7 +2,8 @@ export {Expr} from "./ast";
|
||||
export {complete, CompletionItem} from "./completion";
|
||||
export {DescriptionDictionary, DescriptionPair, isDescriptionDictionary} from "./completion/descriptionDictionary";
|
||||
export * as data from "./data";
|
||||
export {ExpressionError} from "./errors";
|
||||
export {ExpressionError, ExpressionEvaluationError} from "./errors";
|
||||
export {Evaluator} from "./evaluator";
|
||||
export {wellKnownFunctions} from "./funcs";
|
||||
export {Lexer, Result} from "./lexer";
|
||||
export {Parser} from "./parser";
|
||||
|
||||
Reference in New Issue
Block a user