npm run format -ws
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { ExpressionData } from "./data";
|
||||
import { Token } from "./lexer";
|
||||
import {ExpressionData} from "./data";
|
||||
import {Token} from "./lexer";
|
||||
|
||||
export interface ExprVisitor<R> {
|
||||
visitLiteral(literal: Literal): R;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { complete, CompletionItem, trimTokenVector } from "./completion";
|
||||
import { BooleanData } from "./data/boolean";
|
||||
import { Dictionary } from "./data/dictionary";
|
||||
import { StringData } from "./data/string";
|
||||
import { FunctionInfo } from "./funcs/info";
|
||||
import { Lexer, TokenType } from "./lexer";
|
||||
import {complete, CompletionItem, trimTokenVector} from "./completion";
|
||||
import {BooleanData} from "./data/boolean";
|
||||
import {Dictionary} from "./data/dictionary";
|
||||
import {StringData} from "./data/string";
|
||||
import {FunctionInfo} from "./funcs/info";
|
||||
import {Lexer, TokenType} from "./lexer";
|
||||
|
||||
const testContext = new Dictionary(
|
||||
{
|
||||
@@ -11,24 +11,24 @@ const testContext = new Dictionary(
|
||||
value: new Dictionary(
|
||||
{
|
||||
key: "FOO",
|
||||
value: new StringData(""),
|
||||
value: new StringData("")
|
||||
},
|
||||
{
|
||||
key: "BAR_TEST",
|
||||
value: new StringData(""),
|
||||
value: new StringData("")
|
||||
}
|
||||
),
|
||||
)
|
||||
},
|
||||
{
|
||||
key: "secrets",
|
||||
value: new Dictionary({
|
||||
key: "AWS_TOKEN",
|
||||
value: new BooleanData(true),
|
||||
}),
|
||||
value: new BooleanData(true)
|
||||
})
|
||||
},
|
||||
{
|
||||
key: "hashFiles(1,255)",
|
||||
value: new Dictionary(),
|
||||
value: new Dictionary()
|
||||
}
|
||||
);
|
||||
|
||||
@@ -38,17 +38,13 @@ const testComplete = (input: string): CompletionItem[] => {
|
||||
const pos = input.indexOf("|");
|
||||
input = input.replace("|", "");
|
||||
|
||||
const results = complete(
|
||||
input.slice(0, pos >= 0 ? pos : input.length),
|
||||
testContext,
|
||||
testFunctions
|
||||
);
|
||||
const results = complete(input.slice(0, pos >= 0 ? pos : input.length), testContext, testFunctions);
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
function completionItems(...labels: string[]): CompletionItem[] {
|
||||
return labels.map((label) => ({ label, function: false }));
|
||||
return labels.map(label => ({label, function: false}));
|
||||
}
|
||||
|
||||
describe("auto-complete", () => {
|
||||
@@ -58,7 +54,7 @@ describe("auto-complete", () => {
|
||||
label: "toJson",
|
||||
description:
|
||||
"`toJSON(value)`\n\nReturns a pretty-print JSON representation of `value`. You can use this function to debug the information provided in contexts.",
|
||||
function: true,
|
||||
function: true
|
||||
};
|
||||
expect(testComplete("to")).toContainEqual(expected);
|
||||
expect(testComplete("toJs")).toContainEqual(expected);
|
||||
@@ -69,7 +65,7 @@ describe("auto-complete", () => {
|
||||
it("removes parentheses from passed in function context", () => {
|
||||
expect(testComplete("|")).toContainEqual({
|
||||
label: "hashFiles",
|
||||
function: true,
|
||||
function: true
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -92,9 +88,7 @@ describe("auto-complete", () => {
|
||||
});
|
||||
|
||||
it("provides suggestions for contexts in function call", () => {
|
||||
expect(testComplete("toJSON(env.|)")).toEqual(
|
||||
completionItems("BAR_TEST", "FOO")
|
||||
);
|
||||
expect(testComplete("toJSON(env.|)")).toEqual(completionItems("BAR_TEST", "FOO"));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -106,34 +100,19 @@ describe("trimTokenVector", () => {
|
||||
}>([
|
||||
{
|
||||
input: "env.",
|
||||
expected: [TokenType.IDENTIFIER, TokenType.DOT, TokenType.EOF],
|
||||
expected: [TokenType.IDENTIFIER, TokenType.DOT, TokenType.EOF]
|
||||
},
|
||||
{
|
||||
input: "github.act",
|
||||
expected: [
|
||||
TokenType.IDENTIFIER,
|
||||
TokenType.DOT,
|
||||
TokenType.IDENTIFIER,
|
||||
TokenType.EOF,
|
||||
],
|
||||
expected: [TokenType.IDENTIFIER, TokenType.DOT, TokenType.IDENTIFIER, TokenType.EOF]
|
||||
},
|
||||
{
|
||||
input: "1 == github.act",
|
||||
expected: [
|
||||
TokenType.IDENTIFIER,
|
||||
TokenType.DOT,
|
||||
TokenType.IDENTIFIER,
|
||||
TokenType.EOF,
|
||||
],
|
||||
expected: [TokenType.IDENTIFIER, TokenType.DOT, TokenType.IDENTIFIER, TokenType.EOF]
|
||||
},
|
||||
{
|
||||
input: "github.mona == github.act",
|
||||
expected: [
|
||||
TokenType.IDENTIFIER,
|
||||
TokenType.DOT,
|
||||
TokenType.IDENTIFIER,
|
||||
TokenType.EOF,
|
||||
],
|
||||
expected: [TokenType.IDENTIFIER, TokenType.DOT, TokenType.IDENTIFIER, TokenType.EOF]
|
||||
},
|
||||
{
|
||||
input: "github['test'].",
|
||||
@@ -143,14 +122,14 @@ describe("trimTokenVector", () => {
|
||||
TokenType.STRING,
|
||||
TokenType.RIGHT_BRACKET,
|
||||
TokenType.DOT,
|
||||
TokenType.EOF,
|
||||
],
|
||||
},
|
||||
])("$input", ({ input, expected }) => {
|
||||
TokenType.EOF
|
||||
]
|
||||
}
|
||||
])("$input", ({input, expected}) => {
|
||||
const l = new Lexer(input);
|
||||
const lr = l.lex();
|
||||
|
||||
const tv = trimTokenVector(lr.tokens);
|
||||
expect(tv.map((x) => x.type)).toEqual(expected);
|
||||
expect(tv.map(x => x.type)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Dictionary, isDictionary } from "./data/dictionary";
|
||||
import { ExpressionData } from "./data/expressiondata";
|
||||
import { Evaluator } from "./evaluator";
|
||||
import { wellKnownFunctions } from "./funcs";
|
||||
import { FunctionInfo } from "./funcs/info";
|
||||
import { Lexer, Token, TokenType } from "./lexer";
|
||||
import { Parser } from "./parser";
|
||||
import {Dictionary, isDictionary} from "./data/dictionary";
|
||||
import {ExpressionData} from "./data/expressiondata";
|
||||
import {Evaluator} from "./evaluator";
|
||||
import {wellKnownFunctions} from "./funcs";
|
||||
import {FunctionInfo} from "./funcs/info";
|
||||
import {Lexer, Token, TokenType} from "./lexer";
|
||||
import {Parser} from "./parser";
|
||||
|
||||
export type CompletionItem = {
|
||||
label: string;
|
||||
@@ -20,11 +20,7 @@ export type CompletionItem = {
|
||||
// - context.path.| or context.path['| -- auto-complete context access
|
||||
// - toJS| -- auto-complete function call or top-level
|
||||
// - | -- auto-complete function call or top-level context access
|
||||
export function complete(
|
||||
input: string,
|
||||
context: Dictionary,
|
||||
extensionFunctions: FunctionInfo[]
|
||||
): CompletionItem[] {
|
||||
export function complete(input: string, context: Dictionary, extensionFunctions: FunctionInfo[]): CompletionItem[] {
|
||||
// Lex
|
||||
const lexer = new Lexer(input);
|
||||
const lexResult = lexer.lex();
|
||||
@@ -68,7 +64,7 @@ export function complete(
|
||||
|
||||
const p = new Parser(
|
||||
pathTokenVector,
|
||||
context.pairs().map((x) => x.key),
|
||||
context.pairs().map(x => x.key),
|
||||
extensionFunctions
|
||||
);
|
||||
const expr = p.parse();
|
||||
@@ -82,14 +78,11 @@ export function complete(
|
||||
function functionItems(extensionFunctions: FunctionInfo[]): CompletionItem[] {
|
||||
const result: CompletionItem[] = [];
|
||||
|
||||
for (const fdef of [
|
||||
...Object.values(wellKnownFunctions),
|
||||
...extensionFunctions,
|
||||
]) {
|
||||
for (const fdef of [...Object.values(wellKnownFunctions), ...extensionFunctions]) {
|
||||
result.push({
|
||||
label: fdef.name,
|
||||
description: fdef.description,
|
||||
function: true,
|
||||
function: true
|
||||
});
|
||||
}
|
||||
|
||||
@@ -104,7 +97,7 @@ function contextKeys(context: ExpressionData): CompletionItem[] {
|
||||
return (
|
||||
context
|
||||
.pairs()
|
||||
.map((x) => completionItemFromContext(x.key))
|
||||
.map(x => completionItemFromContext(x.key))
|
||||
// Sort contexts
|
||||
.sort((a, b) => a.label.localeCompare(b.label))
|
||||
);
|
||||
@@ -119,7 +112,7 @@ function completionItemFromContext(context: string): CompletionItem {
|
||||
|
||||
return {
|
||||
label: isFunc ? context.substring(0, parenIndex) : context,
|
||||
function: isFunc,
|
||||
function: isFunc
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
ExpressionData,
|
||||
ExpressionDataInterface,
|
||||
Kind,
|
||||
kindStr,
|
||||
} from "./expressiondata";
|
||||
import {ExpressionData, ExpressionDataInterface, Kind, kindStr} from "./expressiondata";
|
||||
|
||||
export class Array implements ExpressionDataInterface {
|
||||
private v: ExpressionData[] = [];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ExpressionDataInterface, Kind } from "./expressiondata";
|
||||
import {ExpressionDataInterface, Kind} from "./expressiondata";
|
||||
|
||||
export class BooleanData implements ExpressionDataInterface {
|
||||
constructor(public readonly value: boolean) {}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Dictionary } from "./dictionary";
|
||||
import { StringData } from "./string";
|
||||
import {Dictionary} from "./dictionary";
|
||||
import {StringData} from "./string";
|
||||
|
||||
describe("dictionary", () => {
|
||||
it("pairs contains all values", () => {
|
||||
const d = new Dictionary();
|
||||
d.add("ABC", new StringData("val"));
|
||||
|
||||
expect(d.pairs()).toEqual([{ key: "ABC", value: new StringData("val") }]);
|
||||
expect(d.pairs()).toEqual([{key: "ABC", value: new StringData("val")}]);
|
||||
});
|
||||
|
||||
it("does not add duplicate entries", () => {
|
||||
@@ -14,6 +14,6 @@ describe("dictionary", () => {
|
||||
d.add("ABC", new StringData("val1"));
|
||||
d.add("abc", new StringData("val2"));
|
||||
|
||||
expect(d.pairs()).toEqual([{ key: "ABC", value: new StringData("val1") }]);
|
||||
expect(d.pairs()).toEqual([{key: "ABC", value: new StringData("val1")}]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
import {
|
||||
ExpressionData,
|
||||
ExpressionDataInterface,
|
||||
Kind,
|
||||
kindStr,
|
||||
Pair,
|
||||
} from "./expressiondata";
|
||||
import {ExpressionData, ExpressionDataInterface, Kind, kindStr, Pair} from "./expressiondata";
|
||||
|
||||
export class Dictionary implements ExpressionDataInterface {
|
||||
private keys: string[] = [];
|
||||
private v: ExpressionData[] = [];
|
||||
private indexMap: { [key: string]: number } = {};
|
||||
private indexMap: {[key: string]: number} = {};
|
||||
|
||||
constructor(...pairs: Pair[]) {
|
||||
for (const p of pairs) {
|
||||
@@ -56,7 +50,7 @@ export class Dictionary implements ExpressionDataInterface {
|
||||
const result: Pair[] = [];
|
||||
|
||||
for (const key of this.keys) {
|
||||
result.push({ key, value: this.v[this.indexMap[key.toLowerCase()]] });
|
||||
result.push({key, value: this.v[this.indexMap[key.toLowerCase()]]});
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Dictionary } from "./dictionary";
|
||||
import { Null } from "./null";
|
||||
import { Array } from "./array";
|
||||
import { StringData } from "./string";
|
||||
import { NumberData } from "./number";
|
||||
import { BooleanData } from "./boolean";
|
||||
import {Dictionary} from "./dictionary";
|
||||
import {Null} from "./null";
|
||||
import {Array} from "./array";
|
||||
import {StringData} from "./string";
|
||||
import {NumberData} from "./number";
|
||||
import {BooleanData} from "./boolean";
|
||||
|
||||
export enum Kind {
|
||||
String = 0,
|
||||
@@ -12,7 +12,7 @@ export enum Kind {
|
||||
Boolean,
|
||||
Number,
|
||||
CaseSensitiveDictionary,
|
||||
Null,
|
||||
Null
|
||||
}
|
||||
|
||||
export function kindStr(k: Kind): string {
|
||||
@@ -43,13 +43,7 @@ export interface ExpressionDataInterface {
|
||||
number(): number;
|
||||
}
|
||||
|
||||
export type ExpressionData =
|
||||
| Array
|
||||
| Dictionary
|
||||
| StringData
|
||||
| BooleanData
|
||||
| NumberData
|
||||
| Null;
|
||||
export type ExpressionData = Array | Dictionary | StringData | BooleanData | NumberData | Null;
|
||||
|
||||
export type Pair = {
|
||||
key: string;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
export { Array } from "./array";
|
||||
export { BooleanData } from "./boolean";
|
||||
export { Dictionary } from "./dictionary";
|
||||
export { ExpressionData, Kind } from "./expressiondata";
|
||||
export { Null } from "./null";
|
||||
export { NumberData } from "./number";
|
||||
export { replacer } from "./replacer";
|
||||
export { reviver } from "./reviver";
|
||||
export { StringData } from "./string";
|
||||
export {Array} from "./array";
|
||||
export {BooleanData} from "./boolean";
|
||||
export {Dictionary} from "./dictionary";
|
||||
export {ExpressionData, Kind} from "./expressiondata";
|
||||
export {Null} from "./null";
|
||||
export {NumberData} from "./number";
|
||||
export {replacer} from "./replacer";
|
||||
export {reviver} from "./reviver";
|
||||
export {StringData} from "./string";
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
ExpressionData,
|
||||
ExpressionDataInterface,
|
||||
Kind,
|
||||
} from "./expressiondata";
|
||||
import {ExpressionData, ExpressionDataInterface, Kind} from "./expressiondata";
|
||||
|
||||
export class Null implements ExpressionDataInterface {
|
||||
constructor() {}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ExpressionDataInterface, Kind } from "./expressiondata";
|
||||
import {ExpressionDataInterface, Kind} from "./expressiondata";
|
||||
|
||||
export class NumberData implements ExpressionDataInterface {
|
||||
constructor(public readonly value: number) {}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Array } from "./array";
|
||||
import { Dictionary } from "./dictionary";
|
||||
import { Null } from "./null";
|
||||
import { NumberData } from "./number";
|
||||
import { replacer } from "./replacer";
|
||||
import { StringData } from "./string";
|
||||
import {Array} from "./array";
|
||||
import {Dictionary} from "./dictionary";
|
||||
import {Null} from "./null";
|
||||
import {NumberData} from "./number";
|
||||
import {replacer} from "./replacer";
|
||||
import {StringData} from "./string";
|
||||
|
||||
describe("replacer", () => {
|
||||
it("null", () => {
|
||||
@@ -11,22 +11,14 @@ describe("replacer", () => {
|
||||
});
|
||||
|
||||
it("array", () => {
|
||||
expect(
|
||||
JSON.stringify(
|
||||
new Array(new StringData("a"), new StringData("b")),
|
||||
replacer,
|
||||
" "
|
||||
)
|
||||
).toEqual('[\n "a",\n "b"\n]');
|
||||
expect(JSON.stringify(new Array(new StringData("a"), new StringData("b")), replacer, " ")).toEqual(
|
||||
'[\n "a",\n "b"\n]'
|
||||
);
|
||||
});
|
||||
|
||||
it("dictionary", () => {
|
||||
expect(
|
||||
JSON.stringify(
|
||||
new Dictionary({ key: "a", value: new NumberData(42) }),
|
||||
replacer,
|
||||
" "
|
||||
)
|
||||
).toEqual('{\n "a": 42\n}');
|
||||
expect(JSON.stringify(new Dictionary({key: "a", value: new NumberData(42)}), replacer, " ")).toEqual(
|
||||
'{\n "a": 42\n}'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Array } from "./array";
|
||||
import { BooleanData } from "./boolean";
|
||||
import { Dictionary } from "./dictionary";
|
||||
import { Null } from "./null";
|
||||
import { NumberData } from "./number";
|
||||
import { StringData } from "./string";
|
||||
import {Array} from "./array";
|
||||
import {BooleanData} from "./boolean";
|
||||
import {Dictionary} from "./dictionary";
|
||||
import {Null} from "./null";
|
||||
import {NumberData} from "./number";
|
||||
import {StringData} from "./string";
|
||||
|
||||
/**
|
||||
* Replacer can be passed to JSON.stringify to convert an ExpressionData object into plain JSON
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Array } from "./array";
|
||||
import { BooleanData } from "./boolean";
|
||||
import { Dictionary } from "./dictionary";
|
||||
import { ExpressionData } from "./expressiondata";
|
||||
import { Null } from "./null";
|
||||
import { NumberData } from "./number";
|
||||
import { reviver } from "./reviver";
|
||||
import { StringData } from "./string";
|
||||
import {Array} from "./array";
|
||||
import {BooleanData} from "./boolean";
|
||||
import {Dictionary} from "./dictionary";
|
||||
import {ExpressionData} from "./expressiondata";
|
||||
import {Null} from "./null";
|
||||
import {NumberData} from "./number";
|
||||
import {reviver} from "./reviver";
|
||||
import {StringData} from "./string";
|
||||
|
||||
describe("reviver", () => {
|
||||
const tests: {
|
||||
@@ -16,32 +16,32 @@ describe("reviver", () => {
|
||||
{
|
||||
name: "null",
|
||||
data: "null",
|
||||
want: new Null(),
|
||||
want: new Null()
|
||||
},
|
||||
{
|
||||
name: "number",
|
||||
data: "1",
|
||||
want: new NumberData(1),
|
||||
want: new NumberData(1)
|
||||
},
|
||||
{
|
||||
name: "string",
|
||||
data: `"a"`,
|
||||
want: new StringData("a"),
|
||||
want: new StringData("a")
|
||||
},
|
||||
{
|
||||
name: "true boolean",
|
||||
data: "true",
|
||||
want: new BooleanData(true),
|
||||
want: new BooleanData(true)
|
||||
},
|
||||
{
|
||||
name: "false boolean",
|
||||
data: "false",
|
||||
want: new BooleanData(false),
|
||||
want: new BooleanData(false)
|
||||
},
|
||||
{
|
||||
name: "array",
|
||||
data: `[1,2,3]`,
|
||||
want: new Array(new NumberData(1), new NumberData(2), new NumberData(3)),
|
||||
want: new Array(new NumberData(1), new NumberData(2), new NumberData(3))
|
||||
},
|
||||
{
|
||||
name: "nested array",
|
||||
@@ -51,7 +51,7 @@ describe("reviver", () => {
|
||||
new NumberData(2),
|
||||
new Array(new NumberData(3), new NumberData(4)),
|
||||
new NumberData(5)
|
||||
),
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "complex array",
|
||||
@@ -59,26 +59,23 @@ describe("reviver", () => {
|
||||
want: new Array(
|
||||
new Dictionary({
|
||||
key: "a",
|
||||
value: new Array(new BooleanData(true), new NumberData(2)),
|
||||
value: new Array(new BooleanData(true), new NumberData(2))
|
||||
}),
|
||||
new Dictionary({ key: "b", value: new StringData("three") }),
|
||||
new Dictionary({ key: "c", value: new Null() })
|
||||
),
|
||||
new Dictionary({key: "b", value: new StringData("three")}),
|
||||
new Dictionary({key: "c", value: new Null()})
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "dictionary",
|
||||
data: `{ "object1": {} }`,
|
||||
want: new Dictionary({
|
||||
key: "object1",
|
||||
value: new Dictionary(),
|
||||
}),
|
||||
},
|
||||
value: new Dictionary()
|
||||
})
|
||||
}
|
||||
];
|
||||
|
||||
test.each(tests)(
|
||||
"$name",
|
||||
({ data, want }: { data: string; want: ExpressionData }) => {
|
||||
expect(JSON.parse(data, reviver)).toEqual(want);
|
||||
}
|
||||
);
|
||||
test.each(tests)("$name", ({data, want}: {data: string; want: ExpressionData}) => {
|
||||
expect(JSON.parse(data, reviver)).toEqual(want);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Array as dArray } from "./array";
|
||||
import { BooleanData } from "./boolean";
|
||||
import { Dictionary } from "./dictionary";
|
||||
import { ExpressionData, Pair } from "./expressiondata";
|
||||
import { Null } from "./null";
|
||||
import { NumberData } from "./number";
|
||||
import { StringData } from "./string";
|
||||
import {Array as dArray} from "./array";
|
||||
import {BooleanData} from "./boolean";
|
||||
import {Dictionary} from "./dictionary";
|
||||
import {ExpressionData, Pair} from "./expressiondata";
|
||||
import {Null} from "./null";
|
||||
import {NumberData} from "./number";
|
||||
import {StringData} from "./string";
|
||||
|
||||
/**
|
||||
* Reviver can be passed to `JSON.parse` to convert plain JSON into an `ExpressionData` object.
|
||||
@@ -35,10 +35,10 @@ export function reviver(key: string, val: any): ExpressionData {
|
||||
if (typeof val === "object") {
|
||||
return new Dictionary(
|
||||
...Object.keys(val).map(
|
||||
(k) =>
|
||||
k =>
|
||||
({
|
||||
key: k,
|
||||
value: val[k],
|
||||
value: val[k]
|
||||
} as Pair)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ExpressionDataInterface, Kind } from "./expressiondata";
|
||||
import {ExpressionDataInterface, Kind} from "./expressiondata";
|
||||
|
||||
export class StringData implements ExpressionDataInterface {
|
||||
constructor(public readonly value: string) {}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Pos, Token, tokenString } from "./lexer";
|
||||
import {Pos, Token, tokenString} from "./lexer";
|
||||
|
||||
export const MAX_PARSER_DEPTH = 50;
|
||||
export const MAX_EXPRESSION_LENGTH = 21000;
|
||||
@@ -13,7 +13,7 @@ export enum ErrorType {
|
||||
ErrorTooFewParameters,
|
||||
ErrorTooManyParameters,
|
||||
ErrorUnrecognizedContext,
|
||||
ErrorUnrecognizedFunction,
|
||||
ErrorUnrecognizedFunction
|
||||
}
|
||||
|
||||
export class ExpressionError extends Error {
|
||||
@@ -42,8 +42,8 @@ function errorDescription(typ: ErrorType): string {
|
||||
return "Too few parameters supplied";
|
||||
case ErrorType.ErrorTooManyParameters:
|
||||
return "Too many parameters supplied";
|
||||
case ErrorType.ErrorUnrecognizedContext:
|
||||
return "Unrecognized named-value";
|
||||
case ErrorType.ErrorUnrecognizedContext:
|
||||
return "Unrecognized named-value";
|
||||
case ErrorType.ErrorUnrecognizedFunction:
|
||||
return "Unrecognized function";
|
||||
default: // Should never reach here.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as data from "./data";
|
||||
import { Evaluator } from "./evaluator";
|
||||
import { Lexer } from "./lexer";
|
||||
import { Parser } from "./parser";
|
||||
import {Evaluator} from "./evaluator";
|
||||
import {Lexer} from "./lexer";
|
||||
import {Parser} from "./parser";
|
||||
|
||||
test("evaluator", () => {
|
||||
const input = "foo['']";
|
||||
@@ -18,7 +18,7 @@ test("evaluator", () => {
|
||||
expr,
|
||||
new data.Dictionary({
|
||||
key: "foo",
|
||||
value: new data.Dictionary({ key: "bar", value: new data.NumberData(42) }),
|
||||
value: new data.Dictionary({key: "bar", value: new data.NumberData(42)})
|
||||
})
|
||||
);
|
||||
const eresult = evaluator.evaluate();
|
||||
|
||||
@@ -9,14 +9,14 @@ import {
|
||||
Literal,
|
||||
Logical,
|
||||
Star,
|
||||
Unary,
|
||||
Unary
|
||||
} from "./ast";
|
||||
import * as data from "./data";
|
||||
import { FilteredArray } from "./filtered_array";
|
||||
import { wellKnownFunctions } from "./funcs";
|
||||
import { idxHelper } from "./idxHelper";
|
||||
import { TokenType } from "./lexer";
|
||||
import { equals, falsy, greaterThan, lessThan, truthy } from "./result";
|
||||
import {FilteredArray} from "./filtered_array";
|
||||
import {wellKnownFunctions} from "./funcs";
|
||||
import {idxHelper} from "./idxHelper";
|
||||
import {TokenType} from "./lexer";
|
||||
import {equals, falsy, greaterThan, lessThan, truthy} from "./result";
|
||||
|
||||
export class EvaluationError extends Error {}
|
||||
|
||||
@@ -60,9 +60,7 @@ export class Evaluator implements ExprVisitor<data.ExpressionData> {
|
||||
return new data.BooleanData(greaterThan(left, right));
|
||||
|
||||
case TokenType.GREATER_EQUAL:
|
||||
return new data.BooleanData(
|
||||
equals(left, right) || greaterThan(left, right)
|
||||
);
|
||||
return new data.BooleanData(equals(left, right) || greaterThan(left, right));
|
||||
|
||||
case TokenType.LESS:
|
||||
return new data.BooleanData(lessThan(left, right));
|
||||
@@ -150,25 +148,19 @@ export class Evaluator implements ExprVisitor<data.ExpressionData> {
|
||||
|
||||
visitFunctionCall(functionCall: FunctionCall): data.ExpressionData {
|
||||
// Evaluate arguments
|
||||
const args = functionCall.args.map((arg) => this.eval(arg));
|
||||
const args = functionCall.args.map(arg => this.eval(arg));
|
||||
|
||||
return fcall(functionCall, args);
|
||||
}
|
||||
}
|
||||
|
||||
function fcall(
|
||||
fc: FunctionCall,
|
||||
args: data.ExpressionData[]
|
||||
): data.ExpressionData {
|
||||
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 {
|
||||
function filteredArrayAccess(fa: FilteredArray, idx: idxHelper): data.ExpressionData {
|
||||
const result = new FilteredArray();
|
||||
|
||||
for (const item of fa.values()) {
|
||||
@@ -225,10 +217,7 @@ function arrayAccess(a: data.Array, idx: idxHelper): data.ExpressionData {
|
||||
return new data.Null();
|
||||
}
|
||||
|
||||
function objectAccess(
|
||||
obj: data.Dictionary,
|
||||
idx: idxHelper
|
||||
): data.ExpressionData {
|
||||
function objectAccess(obj: data.Dictionary, idx: idxHelper): data.ExpressionData {
|
||||
if (idx.star) {
|
||||
const fa = new FilteredArray(...obj.values());
|
||||
return fa;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { ErrorType, ExpressionError } from "./errors";
|
||||
import { contains } from "./funcs/contains";
|
||||
import { endswith } from "./funcs/endswith";
|
||||
import { format } from "./funcs/format";
|
||||
import { fromjson } from "./funcs/fromjson";
|
||||
import { FunctionDefinition, FunctionInfo } from "./funcs/info";
|
||||
import { join } from "./funcs/join";
|
||||
import { startswith } from "./funcs/startswith";
|
||||
import { tojson } from "./funcs/tojson";
|
||||
import { Token } from "./lexer";
|
||||
import {ErrorType, ExpressionError} from "./errors";
|
||||
import {contains} from "./funcs/contains";
|
||||
import {endswith} from "./funcs/endswith";
|
||||
import {format} from "./funcs/format";
|
||||
import {fromjson} from "./funcs/fromjson";
|
||||
import {FunctionDefinition, FunctionInfo} from "./funcs/info";
|
||||
import {join} from "./funcs/join";
|
||||
import {startswith} from "./funcs/startswith";
|
||||
import {tojson} from "./funcs/tojson";
|
||||
import {Token} from "./lexer";
|
||||
|
||||
export type ParseContext = {
|
||||
allowUnknownKeywords: boolean;
|
||||
@@ -15,24 +15,20 @@ export type ParseContext = {
|
||||
extensionFunctions: Map<string, FunctionInfo>;
|
||||
};
|
||||
|
||||
export const wellKnownFunctions: { [name: string]: FunctionDefinition } = {
|
||||
export const wellKnownFunctions: {[name: string]: FunctionDefinition} = {
|
||||
contains: contains,
|
||||
endswith: endswith,
|
||||
format: format,
|
||||
fromjson: fromjson,
|
||||
join: join,
|
||||
startswith: startswith,
|
||||
tojson: tojson,
|
||||
tojson: tojson
|
||||
};
|
||||
|
||||
// validateFunction returns the function definition for the given function name.
|
||||
// If the function does not exist or an incorrect number of arguments is provided,
|
||||
// an error is returned.
|
||||
export function validateFunction(
|
||||
context: ParseContext,
|
||||
identifier: Token,
|
||||
argCount: number
|
||||
) {
|
||||
export function validateFunction(context: ParseContext, identifier: Token, argCount: number) {
|
||||
// Expression function names are case-insensitive.
|
||||
const name = identifier.lexeme.toLowerCase();
|
||||
|
||||
@@ -42,10 +38,7 @@ export function validateFunction(
|
||||
f = context.extensionFunctions.get(name);
|
||||
if (!f) {
|
||||
if (!context.allowUnknownKeywords) {
|
||||
throw new ExpressionError(
|
||||
ErrorType.ErrorUnrecognizedFunction,
|
||||
identifier
|
||||
);
|
||||
throw new ExpressionError(ErrorType.ErrorUnrecognizedFunction, identifier);
|
||||
}
|
||||
|
||||
// Skip argument validation for unknown functions
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Array, BooleanData, ExpressionData, Kind } from "../data";
|
||||
import { equals } from "../result";
|
||||
import { FunctionDefinition } from "./info";
|
||||
import {Array, BooleanData, ExpressionData, Kind} from "../data";
|
||||
import {equals} from "../result";
|
||||
import {FunctionDefinition} from "./info";
|
||||
|
||||
export const contains: FunctionDefinition = {
|
||||
name: "contains",
|
||||
@@ -32,5 +32,5 @@ export const contains: FunctionDefinition = {
|
||||
}
|
||||
|
||||
return new BooleanData(false);
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BooleanData, ExpressionData } from "../data";
|
||||
import { toUpperSpecial } from "../result";
|
||||
import { FunctionDefinition } from "./info";
|
||||
import {BooleanData, ExpressionData} from "../data";
|
||||
import {toUpperSpecial} from "../result";
|
||||
import {FunctionDefinition} from "./info";
|
||||
|
||||
export const endswith: FunctionDefinition = {
|
||||
name: "endsWith",
|
||||
@@ -23,5 +23,5 @@ export const endswith: FunctionDefinition = {
|
||||
const rs = toUpperSpecial(right.coerceString());
|
||||
|
||||
return new BooleanData(ls.endsWith(rs));
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { Null, NumberData, StringData } from "../data";
|
||||
import { format } from "./format";
|
||||
import {Null, NumberData, StringData} from "../data";
|
||||
import {format} from "./format";
|
||||
|
||||
describe("format", () => {
|
||||
it("null", () => {
|
||||
expect(format.call(new StringData("{0}"), new Null())).toEqual(new StringData(""));
|
||||
});
|
||||
it("number", () => {
|
||||
expect(format.call(new StringData("{0}"), new NumberData(42))).toEqual(
|
||||
new StringData("42")
|
||||
);
|
||||
expect(format.call(new StringData("{0}"), new NumberData(42))).toEqual(new StringData("42"));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ExpressionData, StringData } from "../data";
|
||||
import { FunctionDefinition } from "./info";
|
||||
import {ExpressionData, StringData} from "../data";
|
||||
import {FunctionDefinition} from "./info";
|
||||
|
||||
export const format: FunctionDefinition = {
|
||||
name: "format",
|
||||
@@ -32,9 +32,7 @@ export const format: FunctionDefinition = {
|
||||
if (argIndex.success) {
|
||||
// Check parameter count
|
||||
if (1 + argIndex.result > args.length - 1) {
|
||||
throw new Error(
|
||||
`The following format string references more arguments than were supplied: ${fs}`
|
||||
);
|
||||
throw new Error(`The following format string references more arguments than were supplied: ${fs}`);
|
||||
}
|
||||
|
||||
// Append the portion before the left brace
|
||||
@@ -69,7 +67,7 @@ export const format: FunctionDefinition = {
|
||||
}
|
||||
|
||||
return new StringData(result.join(""));
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
function safeCharAt(string: string, index: number): string {
|
||||
@@ -95,7 +93,7 @@ function readArgIndex(string: string, startIndex: number): ArgIndex {
|
||||
// Validate at least one digit
|
||||
if (length < 1) {
|
||||
return <ArgIndex>{
|
||||
success: false,
|
||||
success: false
|
||||
};
|
||||
}
|
||||
|
||||
@@ -105,7 +103,7 @@ function readArgIndex(string: string, startIndex: number): ArgIndex {
|
||||
return <ArgIndex>{
|
||||
success: !isNaN(result),
|
||||
result: result,
|
||||
endIndex: endIndex,
|
||||
endIndex: endIndex
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ExpressionData } from "../data";
|
||||
import { reviver } from "../data/reviver";
|
||||
import { FunctionDefinition } from "./info";
|
||||
import {ExpressionData} from "../data";
|
||||
import {reviver} from "../data/reviver";
|
||||
import {FunctionDefinition} from "./info";
|
||||
|
||||
export const fromjson: FunctionDefinition = {
|
||||
name: "fromJson",
|
||||
@@ -18,5 +18,5 @@ export const fromjson: FunctionDefinition = {
|
||||
|
||||
const d = JSON.parse(is, reviver);
|
||||
return d;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ExpressionData } from "../data";
|
||||
import {ExpressionData} from "../data";
|
||||
|
||||
export interface FunctionInfo {
|
||||
name: string;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Array, ExpressionData, Kind, StringData } from "../data";
|
||||
import { FunctionDefinition } from "./info";
|
||||
import {Array, ExpressionData, Kind, StringData} from "../data";
|
||||
import {FunctionDefinition} from "./info";
|
||||
|
||||
export const join: FunctionDefinition = {
|
||||
name: "join",
|
||||
@@ -25,11 +25,11 @@ export const join: FunctionDefinition = {
|
||||
return new StringData(
|
||||
(args[0] as Array)
|
||||
.values()
|
||||
.map((item) => item.coerceString())
|
||||
.map(item => item.coerceString())
|
||||
.join(separator)
|
||||
);
|
||||
}
|
||||
|
||||
return new StringData("");
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BooleanData, ExpressionData } from "../data";
|
||||
import { toUpperSpecial } from "../result";
|
||||
import { FunctionDefinition } from "./info";
|
||||
import {BooleanData, ExpressionData} from "../data";
|
||||
import {toUpperSpecial} from "../result";
|
||||
import {FunctionDefinition} from "./info";
|
||||
|
||||
export const startswith: FunctionDefinition = {
|
||||
name: "startsWith",
|
||||
@@ -23,5 +23,5 @@ export const startswith: FunctionDefinition = {
|
||||
const rs = toUpperSpecial(right.coerceString());
|
||||
|
||||
return new BooleanData(ls.startsWith(rs));
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ExpressionData, StringData } from "../data";
|
||||
import { replacer } from "../data/replacer";
|
||||
import { FunctionDefinition } from "./info";
|
||||
import {ExpressionData, StringData} from "../data";
|
||||
import {replacer} from "../data/replacer";
|
||||
import {FunctionDefinition} from "./info";
|
||||
|
||||
export const tojson: FunctionDefinition = {
|
||||
name: "toJson",
|
||||
@@ -10,5 +10,5 @@ export const tojson: FunctionDefinition = {
|
||||
maxArgs: 1,
|
||||
call: (...args: ExpressionData[]): ExpressionData => {
|
||||
return new StringData(JSON.stringify(args[0], replacer, " "));
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ExpressionData } from "./data";
|
||||
import {ExpressionData} from "./data";
|
||||
|
||||
export class idxHelper {
|
||||
public readonly str: string | undefined;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export { complete } from "./completion";
|
||||
export {complete} from "./completion";
|
||||
export * as data from "./data";
|
||||
export { ExpressionError } from "./errors";
|
||||
export { Evaluator } from "./evaluator";
|
||||
export { Lexer } from "./lexer";
|
||||
export { Parser } from "./parser";
|
||||
export {ExpressionError} from "./errors";
|
||||
export {Evaluator} from "./evaluator";
|
||||
export {Lexer} from "./lexer";
|
||||
export {Parser} from "./parser";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Lexer, Token, TokenType } from "./lexer";
|
||||
import {Lexer, Token, TokenType} from "./lexer";
|
||||
|
||||
describe("lexer", () => {
|
||||
const tests: {
|
||||
@@ -6,26 +6,26 @@ describe("lexer", () => {
|
||||
tokenType: TokenType[];
|
||||
token?: Token;
|
||||
}[] = [
|
||||
{ input: "<", tokenType: [TokenType.LESS] },
|
||||
{ input: ">", tokenType: [TokenType.GREATER] },
|
||||
{input: "<", tokenType: [TokenType.LESS]},
|
||||
{input: ">", tokenType: [TokenType.GREATER]},
|
||||
|
||||
{ input: "!=", tokenType: [TokenType.BANG_EQUAL] },
|
||||
{ input: "==", tokenType: [TokenType.EQUAL_EQUAL] },
|
||||
{ input: "<=", tokenType: [TokenType.LESS_EQUAL] },
|
||||
{ input: ">=", tokenType: [TokenType.GREATER_EQUAL] },
|
||||
{input: "!=", tokenType: [TokenType.BANG_EQUAL]},
|
||||
{input: "==", tokenType: [TokenType.EQUAL_EQUAL]},
|
||||
{input: "<=", tokenType: [TokenType.LESS_EQUAL]},
|
||||
{input: ">=", tokenType: [TokenType.GREATER_EQUAL]},
|
||||
|
||||
{ input: "&&", tokenType: [TokenType.AND] },
|
||||
{ input: "||", tokenType: [TokenType.OR] },
|
||||
{input: "&&", tokenType: [TokenType.AND]},
|
||||
{input: "||", tokenType: [TokenType.OR]},
|
||||
|
||||
// Numbers
|
||||
{ input: "12", tokenType: [TokenType.NUMBER] },
|
||||
{ input: "12.0", tokenType: [TokenType.NUMBER] },
|
||||
{ input: "0", tokenType: [TokenType.NUMBER] },
|
||||
{ input: "-0", tokenType: [TokenType.NUMBER] },
|
||||
{ input: "-12.0", tokenType: [TokenType.NUMBER] },
|
||||
{input: "12", tokenType: [TokenType.NUMBER]},
|
||||
{input: "12.0", tokenType: [TokenType.NUMBER]},
|
||||
{input: "0", tokenType: [TokenType.NUMBER]},
|
||||
{input: "-0", tokenType: [TokenType.NUMBER]},
|
||||
{input: "-12.0", tokenType: [TokenType.NUMBER]},
|
||||
|
||||
// Strings
|
||||
{ input: "'It''s okay'", tokenType: [TokenType.STRING] },
|
||||
{input: "'It''s okay'", tokenType: [TokenType.STRING]},
|
||||
{
|
||||
input: "format('{0} == ''queued''', needs)",
|
||||
tokenType: [
|
||||
@@ -34,34 +34,28 @@ describe("lexer", () => {
|
||||
TokenType.STRING,
|
||||
TokenType.COMMA,
|
||||
TokenType.IDENTIFIER,
|
||||
TokenType.RIGHT_PAREN,
|
||||
],
|
||||
TokenType.RIGHT_PAREN
|
||||
]
|
||||
},
|
||||
|
||||
// Arrays
|
||||
{
|
||||
input: "[1,2]",
|
||||
tokenType: [
|
||||
TokenType.LEFT_BRACKET,
|
||||
TokenType.NUMBER,
|
||||
TokenType.COMMA,
|
||||
TokenType.NUMBER,
|
||||
TokenType.RIGHT_BRACKET,
|
||||
],
|
||||
tokenType: [TokenType.LEFT_BRACKET, TokenType.NUMBER, TokenType.COMMA, TokenType.NUMBER, TokenType.RIGHT_BRACKET]
|
||||
},
|
||||
|
||||
// Simple expressions
|
||||
{
|
||||
input: "1 == 2",
|
||||
tokenType: [TokenType.NUMBER, TokenType.EQUAL_EQUAL, TokenType.NUMBER],
|
||||
tokenType: [TokenType.NUMBER, TokenType.EQUAL_EQUAL, TokenType.NUMBER]
|
||||
},
|
||||
{
|
||||
input: "1== 1",
|
||||
tokenType: [TokenType.NUMBER, TokenType.EQUAL_EQUAL, TokenType.NUMBER],
|
||||
tokenType: [TokenType.NUMBER, TokenType.EQUAL_EQUAL, TokenType.NUMBER]
|
||||
},
|
||||
{
|
||||
input: "1< 1",
|
||||
tokenType: [TokenType.NUMBER, TokenType.LESS, TokenType.NUMBER],
|
||||
tokenType: [TokenType.NUMBER, TokenType.LESS, TokenType.NUMBER]
|
||||
},
|
||||
|
||||
// Identifiers
|
||||
@@ -73,9 +67,9 @@ describe("lexer", () => {
|
||||
lexeme: "github",
|
||||
pos: {
|
||||
line: 0,
|
||||
column: 0,
|
||||
},
|
||||
},
|
||||
column: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Keywords
|
||||
@@ -87,9 +81,9 @@ describe("lexer", () => {
|
||||
lexeme: "true",
|
||||
pos: {
|
||||
line: 0,
|
||||
column: 0,
|
||||
},
|
||||
},
|
||||
column: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
@@ -100,9 +94,9 @@ describe("lexer", () => {
|
||||
lexeme: "false",
|
||||
pos: {
|
||||
line: 0,
|
||||
column: 0,
|
||||
},
|
||||
},
|
||||
column: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
@@ -113,32 +107,21 @@ describe("lexer", () => {
|
||||
lexeme: "null",
|
||||
pos: {
|
||||
line: 0,
|
||||
column: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
column: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
test.each(tests)(
|
||||
"$input",
|
||||
({
|
||||
input,
|
||||
tokenType,
|
||||
token,
|
||||
}: {
|
||||
input: string;
|
||||
tokenType: TokenType[];
|
||||
token?: Token;
|
||||
}) => {
|
||||
const l = new Lexer(input);
|
||||
test.each(tests)("$input", ({input, tokenType, token}: {input: string; tokenType: TokenType[]; token?: Token}) => {
|
||||
const l = new Lexer(input);
|
||||
|
||||
const r = l.lex();
|
||||
const r = l.lex();
|
||||
|
||||
const want = r.tokens.map((t) => t.type);
|
||||
const want = r.tokens.map(t => t.type);
|
||||
|
||||
tokenType.push(TokenType.EOF);
|
||||
tokenType.push(TokenType.EOF);
|
||||
|
||||
expect(want).toEqual(tokenType);
|
||||
}
|
||||
);
|
||||
expect(want).toEqual(tokenType);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { StringData } from "./data";
|
||||
import { MAX_EXPRESSION_LENGTH } from "./errors";
|
||||
import {StringData} from "./data";
|
||||
import {MAX_EXPRESSION_LENGTH} from "./errors";
|
||||
|
||||
export enum TokenType {
|
||||
UNKNOWN,
|
||||
@@ -28,7 +28,7 @@ export enum TokenType {
|
||||
FALSE,
|
||||
NULL,
|
||||
|
||||
EOF,
|
||||
EOF
|
||||
}
|
||||
|
||||
export type Pos = {
|
||||
@@ -104,7 +104,6 @@ export class Lexer {
|
||||
this.previous() != TokenType.RIGHT_BRACKET &&
|
||||
this.previous() != TokenType.RIGHT_PAREN &&
|
||||
this.previous() != TokenType.STAR
|
||||
|
||||
) {
|
||||
this.consumeNumber();
|
||||
} else {
|
||||
@@ -118,9 +117,7 @@ export class Lexer {
|
||||
break;
|
||||
|
||||
case "!":
|
||||
this.addToken(
|
||||
this.match("=") ? TokenType.BANG_EQUAL : TokenType.BANG
|
||||
);
|
||||
this.addToken(this.match("=") ? TokenType.BANG_EQUAL : TokenType.BANG);
|
||||
break;
|
||||
|
||||
case "=":
|
||||
@@ -134,15 +131,11 @@ export class Lexer {
|
||||
break;
|
||||
|
||||
case "<":
|
||||
this.addToken(
|
||||
this.match("=") ? TokenType.LESS_EQUAL : TokenType.LESS
|
||||
);
|
||||
this.addToken(this.match("=") ? TokenType.LESS_EQUAL : TokenType.LESS);
|
||||
break;
|
||||
|
||||
case ">":
|
||||
this.addToken(
|
||||
this.match("=") ? TokenType.GREATER_EQUAL : TokenType.GREATER
|
||||
);
|
||||
this.addToken(this.match("=") ? TokenType.GREATER_EQUAL : TokenType.GREATER);
|
||||
break;
|
||||
|
||||
case "&":
|
||||
@@ -199,18 +192,18 @@ export class Lexer {
|
||||
this.tokens.push({
|
||||
type: TokenType.EOF,
|
||||
lexeme: "",
|
||||
pos: this.pos(),
|
||||
pos: this.pos()
|
||||
});
|
||||
|
||||
return {
|
||||
tokens: this.tokens,
|
||||
tokens: this.tokens
|
||||
};
|
||||
}
|
||||
|
||||
private pos(): Pos {
|
||||
return {
|
||||
line: this.line,
|
||||
column: this.start - this.lastLineOffset,
|
||||
column: this.start - this.lastLineOffset
|
||||
};
|
||||
}
|
||||
|
||||
@@ -268,7 +261,7 @@ export class Lexer {
|
||||
type,
|
||||
lexeme: this.input.substring(this.start, this.offset),
|
||||
pos: this.pos(),
|
||||
value,
|
||||
value
|
||||
});
|
||||
}
|
||||
|
||||
@@ -282,9 +275,7 @@ export class Lexer {
|
||||
|
||||
if (isNaN(value)) {
|
||||
throw new Error(
|
||||
`Unexpected symbol: '${lexeme}'. Located at position ${
|
||||
this.start + 1
|
||||
} within expression: ${this.input}`
|
||||
`Unexpected symbol: '${lexeme}'. Located at position ${this.start + 1} within expression: ${this.input}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -304,11 +295,9 @@ export class Lexer {
|
||||
if (this.atEnd()) {
|
||||
// Unterminated string
|
||||
throw new Error(
|
||||
`Unexpected symbol: '${this.input.substring(
|
||||
this.start
|
||||
)}'. Located at position ${this.start + 1} within expression: ${
|
||||
this.input
|
||||
}`
|
||||
`Unexpected symbol: '${this.input.substring(this.start)}'. Located at position ${
|
||||
this.start + 1
|
||||
} within expression: ${this.input}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -360,9 +349,7 @@ export class Lexer {
|
||||
|
||||
if (!isLegalIdentifier(lexeme)) {
|
||||
throw new Error(
|
||||
`Unexpected symbol: '${lexeme}'. Located at position ${
|
||||
this.start + 1
|
||||
} within expression: ${this.input}`
|
||||
`Unexpected symbol: '${lexeme}'. Located at position ${this.start + 1} within expression: ${this.input}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -400,19 +387,9 @@ function isLegalIdentifier(str: string): boolean {
|
||||
}
|
||||
|
||||
const first = str[0];
|
||||
if (
|
||||
(first >= "a" && first <= "z") ||
|
||||
(first >= "A" && first <= "Z") ||
|
||||
first == "_"
|
||||
) {
|
||||
if ((first >= "a" && first <= "z") || (first >= "A" && first <= "Z") || first == "_") {
|
||||
for (const c of str.substring(1).split("")) {
|
||||
if (
|
||||
(c >= "a" && c <= "z") ||
|
||||
(c >= "A" && c <= "Z") ||
|
||||
(c >= "0" && c <= "9") ||
|
||||
c == "_" ||
|
||||
c == "-"
|
||||
) {
|
||||
if ((c >= "a" && c <= "z") || (c >= "A" && c <= "Z") || (c >= "0" && c <= "9") || c == "_" || c == "-") {
|
||||
// OK
|
||||
} else {
|
||||
return false;
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
import {
|
||||
Binary,
|
||||
ContextAccess,
|
||||
Expr,
|
||||
FunctionCall,
|
||||
Grouping,
|
||||
IndexAccess,
|
||||
Literal,
|
||||
Logical,
|
||||
Star,
|
||||
Unary,
|
||||
} from "./ast";
|
||||
import {Binary, ContextAccess, Expr, FunctionCall, Grouping, IndexAccess, Literal, Logical, Star, Unary} from "./ast";
|
||||
import * as data from "./data";
|
||||
import { ErrorType, ExpressionError, MAX_PARSER_DEPTH } from "./errors";
|
||||
import { ParseContext, validateFunction } from "./funcs";
|
||||
import { FunctionInfo } from "./funcs/info";
|
||||
import { Token, TokenType } from "./lexer";
|
||||
import {ErrorType, ExpressionError, MAX_PARSER_DEPTH} from "./errors";
|
||||
import {ParseContext, validateFunction} from "./funcs";
|
||||
import {FunctionInfo} from "./funcs/info";
|
||||
import {Token, TokenType} from "./lexer";
|
||||
|
||||
export class Parser {
|
||||
private extContexts: Map<string, boolean>;
|
||||
@@ -32,11 +21,7 @@ export class Parser {
|
||||
* @param extensionContexts Available context names
|
||||
* @param extensionFunctions Available functions (beyond the built-in ones)
|
||||
*/
|
||||
constructor(
|
||||
private tokens: Token[],
|
||||
extensionContexts: string[],
|
||||
extensionFunctions: FunctionInfo[]
|
||||
) {
|
||||
constructor(private tokens: Token[], extensionContexts: string[], extensionFunctions: FunctionInfo[]) {
|
||||
this.extContexts = new Map<string, boolean>();
|
||||
this.extFuncs = new Map();
|
||||
|
||||
@@ -44,9 +29,9 @@ export class Parser {
|
||||
this.extContexts.set(contextName.toLowerCase(), true);
|
||||
}
|
||||
|
||||
for (const { name, func } of extensionFunctions.map((x) => ({
|
||||
for (const {name, func} of extensionFunctions.map(x => ({
|
||||
name: x.name,
|
||||
func: x,
|
||||
func: x
|
||||
}))) {
|
||||
this.extFuncs.set(name.toLowerCase(), func);
|
||||
}
|
||||
@@ -54,7 +39,7 @@ export class Parser {
|
||||
this.context = {
|
||||
allowUnknownKeywords: false,
|
||||
extensionContexts: this.extContexts,
|
||||
extensionFunctions: this.extFuncs,
|
||||
extensionFunctions: this.extFuncs
|
||||
};
|
||||
}
|
||||
|
||||
@@ -65,7 +50,7 @@ export class Parser {
|
||||
if (this.atEnd()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
result = this.expression();
|
||||
|
||||
if (!this.atEnd()) {
|
||||
@@ -151,14 +136,7 @@ export class Parser {
|
||||
// ! is higher precedence than >, >=, <, <=
|
||||
let expr = this.unary();
|
||||
|
||||
while (
|
||||
this.match(
|
||||
TokenType.GREATER,
|
||||
TokenType.GREATER_EQUAL,
|
||||
TokenType.LESS,
|
||||
TokenType.LESS_EQUAL
|
||||
)
|
||||
) {
|
||||
while (this.match(TokenType.GREATER, TokenType.GREATER_EQUAL, TokenType.LESS, TokenType.LESS_EQUAL)) {
|
||||
const operator = this.previous();
|
||||
const right = this.unary();
|
||||
|
||||
@@ -191,11 +169,7 @@ export class Parser {
|
||||
|
||||
let depthIncreased = 0;
|
||||
|
||||
if (
|
||||
expr instanceof Grouping ||
|
||||
expr instanceof FunctionCall ||
|
||||
expr instanceof ContextAccess
|
||||
) {
|
||||
if (expr instanceof Grouping || expr instanceof FunctionCall || expr instanceof ContextAccess) {
|
||||
let cont = true;
|
||||
while (cont) {
|
||||
switch (true) {
|
||||
@@ -207,10 +181,7 @@ export class Parser {
|
||||
indexExpr = this.expression();
|
||||
}
|
||||
|
||||
this.consume(
|
||||
TokenType.RIGHT_BRACKET,
|
||||
ErrorType.ErrorUnexpectedSymbol
|
||||
);
|
||||
this.consume(TokenType.RIGHT_BRACKET, ErrorType.ErrorUnexpectedSymbol);
|
||||
|
||||
// Track depth
|
||||
this.incrDepth();
|
||||
@@ -225,17 +196,11 @@ export class Parser {
|
||||
|
||||
if (this.match(TokenType.IDENTIFIER)) {
|
||||
let property = this.previous();
|
||||
expr = new IndexAccess(
|
||||
expr,
|
||||
new Literal(new data.StringData(property.lexeme))
|
||||
);
|
||||
expr = new IndexAccess(expr, new Literal(new data.StringData(property.lexeme)));
|
||||
} else if (this.match(TokenType.STAR)) {
|
||||
expr = new IndexAccess(expr, new Star());
|
||||
} else {
|
||||
throw this.buildError(
|
||||
ErrorType.ErrorUnexpectedSymbol,
|
||||
this.peek()
|
||||
);
|
||||
throw this.buildError(ErrorType.ErrorUnexpectedSymbol, this.peek());
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -307,20 +272,14 @@ export class Parser {
|
||||
const expr = this.expression();
|
||||
|
||||
if (this.atEnd()) {
|
||||
throw this.buildError(
|
||||
ErrorType.ErrorUnexpectedEndOfExpression,
|
||||
this.previous()
|
||||
); // Back up to get the last token before the EOF
|
||||
throw this.buildError(ErrorType.ErrorUnexpectedEndOfExpression, this.previous()); // Back up to get the last token before the EOF
|
||||
}
|
||||
|
||||
this.consume(TokenType.RIGHT_PAREN, ErrorType.ErrorUnexpectedSymbol);
|
||||
return new Grouping(expr);
|
||||
|
||||
case this.atEnd():
|
||||
throw this.buildError(
|
||||
ErrorType.ErrorUnexpectedEndOfExpression,
|
||||
this.previous()
|
||||
); // Back up to get the last token before the EOF
|
||||
throw this.buildError(ErrorType.ErrorUnexpectedEndOfExpression, this.previous()); // Back up to get the last token before the EOF
|
||||
}
|
||||
|
||||
throw this.buildError(ErrorType.ErrorUnexpectedSymbol, this.peek());
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BooleanData, ExpressionData, NumberData, StringData } from "./data";
|
||||
import { coerceTypes, toUpperSpecial } from "./result";
|
||||
import {BooleanData, ExpressionData, NumberData, StringData} from "./data";
|
||||
import {coerceTypes, toUpperSpecial} from "./result";
|
||||
|
||||
describe("coerceTypes", () => {
|
||||
const tests: {
|
||||
@@ -13,52 +13,52 @@ describe("coerceTypes", () => {
|
||||
}[] = [
|
||||
{
|
||||
name: "number-bool",
|
||||
data: { l: new NumberData(1), r: new BooleanData(true) },
|
||||
data: {l: new NumberData(1), r: new BooleanData(true)},
|
||||
wantL: new NumberData(1),
|
||||
wantR: new NumberData(1),
|
||||
wantR: new NumberData(1)
|
||||
},
|
||||
{
|
||||
name: "number-bool-false",
|
||||
data: { l: new NumberData(1), r: new BooleanData(false) },
|
||||
data: {l: new NumberData(1), r: new BooleanData(false)},
|
||||
wantL: new NumberData(1),
|
||||
wantR: new NumberData(0),
|
||||
wantR: new NumberData(0)
|
||||
},
|
||||
{
|
||||
name: "bool-number-false",
|
||||
data: { l: new BooleanData(false), r: new NumberData(1) },
|
||||
data: {l: new BooleanData(false), r: new NumberData(1)},
|
||||
wantL: new NumberData(0),
|
||||
wantR: new NumberData(1),
|
||||
wantR: new NumberData(1)
|
||||
},
|
||||
{
|
||||
name: "number-number",
|
||||
data: { l: new NumberData(1), r: new NumberData(2) },
|
||||
data: {l: new NumberData(1), r: new NumberData(2)},
|
||||
wantL: new NumberData(1),
|
||||
wantR: new NumberData(2),
|
||||
wantR: new NumberData(2)
|
||||
},
|
||||
{
|
||||
name: "string-string",
|
||||
data: { l: new StringData("a"), r: new StringData("b") },
|
||||
data: {l: new StringData("a"), r: new StringData("b")},
|
||||
wantL: new StringData("a"),
|
||||
wantR: new StringData("b"),
|
||||
wantR: new StringData("b")
|
||||
},
|
||||
{
|
||||
name: "string-number",
|
||||
data: { l: new StringData("a"), r: new NumberData(1) },
|
||||
data: {l: new StringData("a"), r: new NumberData(1)},
|
||||
wantL: new NumberData(NaN),
|
||||
wantR: new NumberData(1),
|
||||
wantR: new NumberData(1)
|
||||
},
|
||||
{
|
||||
name: "number-string",
|
||||
data: { l: new NumberData(1), r: new StringData("a") },
|
||||
data: {l: new NumberData(1), r: new StringData("a")},
|
||||
wantL: new NumberData(1),
|
||||
wantR: new NumberData(NaN),
|
||||
wantR: new NumberData(NaN)
|
||||
},
|
||||
{
|
||||
name: "bool-bool",
|
||||
data: { l: new BooleanData(false), r: new BooleanData(true) },
|
||||
data: {l: new BooleanData(false), r: new BooleanData(true)},
|
||||
wantL: new BooleanData(false),
|
||||
wantR: new BooleanData(true),
|
||||
},
|
||||
wantR: new BooleanData(true)
|
||||
}
|
||||
];
|
||||
|
||||
test.each(tests)(
|
||||
@@ -66,9 +66,9 @@ describe("coerceTypes", () => {
|
||||
({
|
||||
data,
|
||||
wantL,
|
||||
wantR,
|
||||
wantR
|
||||
}: {
|
||||
data: { l: ExpressionData; r: ExpressionData };
|
||||
data: {l: ExpressionData; r: ExpressionData};
|
||||
wantL: ExpressionData;
|
||||
wantR: ExpressionData;
|
||||
}) => {
|
||||
@@ -80,22 +80,19 @@ describe("coerceTypes", () => {
|
||||
});
|
||||
|
||||
describe("toUpperSpecial", () => {
|
||||
const tests: { input: string; want: string }[] = [
|
||||
{ input: "", want: "" },
|
||||
{ input: "abc", want: "ABC" },
|
||||
{ input: "ıabc", want: "ıABC" },
|
||||
{ input: "ııabc", want: "ııABC" },
|
||||
{ input: "abcı", want: "ABCı" },
|
||||
{ input: "abcıı", want: "ABCıı" },
|
||||
{ input: "abcıdef", want: "ABCıDEF" },
|
||||
{ input: "abcııdef", want: "ABCııDEF" },
|
||||
{ input: "abcıdefıghi", want: "ABCıDEFıGHI" },
|
||||
const tests: {input: string; want: string}[] = [
|
||||
{input: "", want: ""},
|
||||
{input: "abc", want: "ABC"},
|
||||
{input: "ıabc", want: "ıABC"},
|
||||
{input: "ııabc", want: "ııABC"},
|
||||
{input: "abcı", want: "ABCı"},
|
||||
{input: "abcıı", want: "ABCıı"},
|
||||
{input: "abcıdef", want: "ABCıDEF"},
|
||||
{input: "abcııdef", want: "ABCııDEF"},
|
||||
{input: "abcıdefıghi", want: "ABCıDEFıGHI"}
|
||||
];
|
||||
|
||||
test.each(tests)(
|
||||
"$input",
|
||||
({ input, want }: { input: string; want: string }) => {
|
||||
expect(toUpperSpecial(input)).toEqual(want);
|
||||
}
|
||||
);
|
||||
test.each(tests)("$input", ({input, want}: {input: string; want: string}) => {
|
||||
expect(toUpperSpecial(input)).toEqual(want);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -76,10 +76,7 @@ export function coerceTypes(
|
||||
|
||||
// Similar to the Javascript abstract equality comparison algorithm http://www.ecma-international.org/ecma-262/5.1/#sec-11.9.3.
|
||||
// Except string comparison is OrdinalIgnoreCase, and objects are not coerced to primitives.
|
||||
export function equals(
|
||||
lhs: data.ExpressionData,
|
||||
rhs: data.ExpressionData
|
||||
): boolean {
|
||||
export function equals(lhs: data.ExpressionData, rhs: data.ExpressionData): boolean {
|
||||
let [lv, rv] = coerceTypes(lhs, rhs);
|
||||
|
||||
if (lv.kind != rv.kind) {
|
||||
@@ -125,10 +122,7 @@ export function equals(
|
||||
|
||||
// Similar to the Javascript abstract equality comparison algorithm http://www.ecma-international.org/ecma-262/5.1/#sec-11.9.3.
|
||||
// Except string comparison is OrdinalIgnoreCase, and objects are not coerced to primitives.
|
||||
export function greaterThan(
|
||||
lhs: data.ExpressionData,
|
||||
rhs: data.ExpressionData
|
||||
): boolean {
|
||||
export function greaterThan(lhs: data.ExpressionData, rhs: data.ExpressionData): boolean {
|
||||
const [lv, rv] = coerceTypes(lhs, rhs);
|
||||
|
||||
if (lv.kind != rv.kind) {
|
||||
@@ -166,10 +160,7 @@ export function greaterThan(
|
||||
|
||||
// Similar to the Javascript abstract equality comparison algorithm http://www.ecma-international.org/ecma-262/5.1/#sec-11.9.3.
|
||||
// Except string comparison is OrdinalIgnoreCase, and objects are not coerced to primitives.
|
||||
export function lessThan(
|
||||
lhs: data.ExpressionData,
|
||||
rhs: data.ExpressionData
|
||||
): boolean {
|
||||
export function lessThan(lhs: data.ExpressionData, rhs: data.ExpressionData): boolean {
|
||||
const [lv, rv] = coerceTypes(lhs, rhs);
|
||||
|
||||
if (lv.kind != rv.kind) {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { Expr } from "./ast";
|
||||
import {Expr} from "./ast";
|
||||
import * as data from "./data";
|
||||
import { kindStr } from "./data/expressiondata";
|
||||
import { replacer } from "./data/replacer";
|
||||
import { reviver } from "./data/reviver";
|
||||
import { ExpressionError } from "./errors";
|
||||
import { Evaluator } from "./evaluator";
|
||||
import { Lexer, Result } from "./lexer";
|
||||
import { Parser } from "./parser";
|
||||
import {kindStr} from "./data/expressiondata";
|
||||
import {replacer} from "./data/replacer";
|
||||
import {reviver} from "./data/reviver";
|
||||
import {ExpressionError} from "./errors";
|
||||
import {Evaluator} from "./evaluator";
|
||||
import {Lexer, Result} from "./lexer";
|
||||
import {Parser} from "./parser";
|
||||
|
||||
interface TestResult {
|
||||
value: data.ExpressionData;
|
||||
@@ -18,11 +18,11 @@ interface TestResult {
|
||||
enum TestErrorKind {
|
||||
Lexing = "lexing",
|
||||
Parsing = "parsing",
|
||||
Evaluation = "evaluation",
|
||||
Evaluation = "evaluation"
|
||||
}
|
||||
|
||||
enum SkipOptionKind {
|
||||
Typescript = "typescript",
|
||||
Typescript = "typescript"
|
||||
}
|
||||
|
||||
interface TestOptions {
|
||||
@@ -83,9 +83,7 @@ describe("x-lang tests", () => {
|
||||
let tests = testCases[testCaseName];
|
||||
|
||||
// Filter out tests that are not supported by typescript
|
||||
tests = tests.filter(
|
||||
(t) => !t.options?.skip?.includes(SkipOptionKind.Typescript)
|
||||
);
|
||||
tests = tests.filter(t => !t.options?.skip?.includes(SkipOptionKind.Typescript));
|
||||
|
||||
const testName = path.basename(file, ".json");
|
||||
|
||||
@@ -94,7 +92,7 @@ describe("x-lang tests", () => {
|
||||
}
|
||||
|
||||
describe(testName, () => {
|
||||
test.each(tests)(`${testName}: ${testCaseName} ($expr)`, (testCase) => {
|
||||
test.each(tests)(`${testName}: ${testCaseName} ($expr)`, testCase => {
|
||||
if (!testCase.contexts) {
|
||||
testCase.contexts = new data.Dictionary();
|
||||
}
|
||||
@@ -114,37 +112,29 @@ describe("x-lang tests", () => {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`unexpected error lexing expression: ${e.message} ${e.stack}`
|
||||
);
|
||||
throw new Error(`unexpected error lexing expression: ${e.message} ${e.stack}`);
|
||||
}
|
||||
|
||||
// Parse
|
||||
const contextNames = testCase.contexts.pairs().map((x) => x.key);
|
||||
const contextNames = testCase.contexts.pairs().map(x => x.key);
|
||||
const parser = new Parser(result.tokens, contextNames, []);
|
||||
let expr: Expr;
|
||||
try {
|
||||
expr = parser.parse();
|
||||
|
||||
if (testCase.err?.kind === TestErrorKind.Parsing) {
|
||||
throw new Error(
|
||||
"expected error parsing expression, but got none"
|
||||
);
|
||||
throw new Error("expected error parsing expression, but got none");
|
||||
}
|
||||
} catch (e: any) {
|
||||
// Did test expect parsing error?
|
||||
if (testCase.err?.kind === TestErrorKind.Parsing) {
|
||||
// Test expects parsing error
|
||||
const pe = e as ExpressionError;
|
||||
expect(errorWithExpression(pe, testCase.expr)).toContain(
|
||||
testCase.err.value
|
||||
);
|
||||
expect(errorWithExpression(pe, testCase.expr)).toContain(testCase.err.value);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`unexpected error parsing expression: ${e.message} ${e.stack}`
|
||||
);
|
||||
throw new Error(`unexpected error parsing expression: ${e.message} ${e.stack}`);
|
||||
}
|
||||
|
||||
// Evaluate expression
|
||||
@@ -158,28 +148,20 @@ describe("x-lang tests", () => {
|
||||
}
|
||||
|
||||
if (testCase.err?.kind === TestErrorKind.Evaluation) {
|
||||
throw new Error(
|
||||
"expected error evaluating expression, but got none"
|
||||
);
|
||||
throw new Error("expected error evaluating expression, but got none");
|
||||
}
|
||||
|
||||
expect(kindStr(result.kind)).toBe(testCase.result.kind);
|
||||
|
||||
expect(JSON.stringify(result, replacer)).toEqual(
|
||||
JSON.stringify(testCase.result.value, replacer)
|
||||
);
|
||||
expect(JSON.stringify(result, replacer)).toEqual(JSON.stringify(testCase.result.value, replacer));
|
||||
} catch (e: any) {
|
||||
if (testCase.err?.kind === TestErrorKind.Evaluation) {
|
||||
const pe = e as ExpressionError;
|
||||
expect(errorWithExpression(pe, testCase.expr)).toContain(
|
||||
testCase.err.value
|
||||
);
|
||||
expect(errorWithExpression(pe, testCase.expr)).toContain(testCase.err.value);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`unexpected error evaluating expression: ${e.message} ${e.stack}`
|
||||
);
|
||||
throw new Error(`unexpected error evaluating expression: ${e.message} ${e.stack}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -189,9 +171,7 @@ describe("x-lang tests", () => {
|
||||
|
||||
function errorWithExpression(e: ExpressionError, expr: string): string {
|
||||
if (e.pos !== undefined) {
|
||||
return `${e.message}. Located at position ${
|
||||
e.pos.column + 1
|
||||
} within expression: ${expr}`;
|
||||
return `${e.message}. Located at position ${e.pos.column + 1} within expression: ${expr}`;
|
||||
}
|
||||
|
||||
return `${e.message}. Located within expression: ${expr}`;
|
||||
|
||||
Reference in New Issue
Block a user