Merge parser and expressions into repository
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
@github:registry=https://npm.pkg.github.com
|
||||||
|
|
||||||
Executable
+16
@@ -0,0 +1,16 @@
|
|||||||
|
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||||
|
export default {
|
||||||
|
preset: "ts-jest/presets/default-esm",
|
||||||
|
moduleNameMapper: {
|
||||||
|
"^(\\.{1,2}/.*)\\.js$": "$1",
|
||||||
|
},
|
||||||
|
transform: {
|
||||||
|
"^.+\\.tsx?$": [
|
||||||
|
"ts-jest",
|
||||||
|
{
|
||||||
|
useESM: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
moduleFileExtensions: ["ts", "js"],
|
||||||
|
};
|
||||||
Executable
+51
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"name": "@github/actions-expressions",
|
||||||
|
"version": "0.0.9",
|
||||||
|
"license": "MIT",
|
||||||
|
"type": "module",
|
||||||
|
"source": "./src/index.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"./*": {
|
||||||
|
"import": "./dist/*.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"typesVersions": {
|
||||||
|
"*": {
|
||||||
|
".": [
|
||||||
|
"dist/index.d.ts"
|
||||||
|
],
|
||||||
|
"*": [
|
||||||
|
"dist/*.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/github/actions-expressions/"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc --build tsconfig.build.json",
|
||||||
|
"clean": "rimraf dist",
|
||||||
|
"prepublishOnly": "npm run build && npm run test",
|
||||||
|
"test": "NODE_OPTIONS=\"--experimental-vm-modules\" jest",
|
||||||
|
"test-watch": "NODE_OPTIONS=\"--experimental-vm-modules\" jest --watch",
|
||||||
|
"watch": "tsc --build tsconfig.build.json --watch"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist/**/*"
|
||||||
|
],
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/jest": "^29.0.3",
|
||||||
|
"jest": "^29.0.3",
|
||||||
|
"prettier": "^2.7.1",
|
||||||
|
"rimraf": "^3.0.2",
|
||||||
|
"ts-jest": "^29.0.3",
|
||||||
|
"typescript": "^4.7.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+3
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
npm run build
|
||||||
Executable
+3
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
npm test
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import { ExpressionData } from "./data";
|
||||||
|
import { Token } from "./lexer";
|
||||||
|
|
||||||
|
export interface ExprVisitor<R> {
|
||||||
|
visitLiteral(literal: Literal): R;
|
||||||
|
visitUnary(unary: Unary): R;
|
||||||
|
visitBinary(binary: Binary): R;
|
||||||
|
visitLogical(binary: Logical): R;
|
||||||
|
visitGrouping(grouping: Grouping): R;
|
||||||
|
visitContextAccess(contextAccess: ContextAccess): R;
|
||||||
|
visitIndexAccess(indexAccess: IndexAccess): R;
|
||||||
|
visitFunctionCall(functionCall: FunctionCall): R;
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class Expr {
|
||||||
|
abstract accept<R>(v: ExprVisitor<R>): R;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Literal extends Expr {
|
||||||
|
constructor(public literal: ExpressionData) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
accept<R>(v: ExprVisitor<R>): R {
|
||||||
|
return v.visitLiteral(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Unary extends Expr {
|
||||||
|
constructor(public operator: Token, public expr: Expr) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
accept<R>(v: ExprVisitor<R>): R {
|
||||||
|
return v.visitUnary(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FunctionCall extends Expr {
|
||||||
|
constructor(public functionName: Token, public args: Expr[]) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
accept<R>(v: ExprVisitor<R>): R {
|
||||||
|
return v.visitFunctionCall(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Binary extends Expr {
|
||||||
|
constructor(public left: Expr, public operator: Token, public right: Expr) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
accept<R>(v: ExprVisitor<R>): R {
|
||||||
|
return v.visitBinary(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Logical extends Expr {
|
||||||
|
constructor(public operator: Token, public args: Expr[]) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
accept<R>(v: ExprVisitor<R>): R {
|
||||||
|
return v.visitLogical(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Grouping extends Expr {
|
||||||
|
constructor(public group: Expr) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
accept<R>(v: ExprVisitor<R>): R {
|
||||||
|
return v.visitGrouping(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ContextAccess extends Expr {
|
||||||
|
constructor(public name: Token) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
accept<R>(v: ExprVisitor<R>): R {
|
||||||
|
return v.visitContextAccess(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class IndexAccess extends Expr {
|
||||||
|
constructor(public expr: Expr, public index: Expr) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
accept<R>(v: ExprVisitor<R>): R {
|
||||||
|
return v.visitIndexAccess(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Star extends Expr {
|
||||||
|
accept<R>(v: ExprVisitor<R>): R {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
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(
|
||||||
|
{
|
||||||
|
key: "env",
|
||||||
|
value: new Dictionary(
|
||||||
|
{
|
||||||
|
key: "FOO",
|
||||||
|
value: new StringData(""),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "BAR_TEST",
|
||||||
|
value: new StringData(""),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "secrets",
|
||||||
|
value: new Dictionary({
|
||||||
|
key: "AWS_TOKEN",
|
||||||
|
value: new BooleanData(true),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "hashFiles(1,255)",
|
||||||
|
value: new Dictionary(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const testFunctions: FunctionInfo[] = [];
|
||||||
|
|
||||||
|
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
|
||||||
|
);
|
||||||
|
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
|
||||||
|
function completionItems(...labels: string[]): CompletionItem[] {
|
||||||
|
return labels.map((label) => ({ label, function: false }));
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("auto-complete", () => {
|
||||||
|
describe("top-level", () => {
|
||||||
|
it("includes built-in functions", () => {
|
||||||
|
const expected: CompletionItem = {
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
expect(testComplete("to")).toContainEqual(expected);
|
||||||
|
expect(testComplete("toJs")).toContainEqual(expected);
|
||||||
|
expect(testComplete("1 == toJS")).toContainEqual(expected);
|
||||||
|
expect(testComplete("toJS| == 1")).toContainEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("removes parentheses from passed in function context", () => {
|
||||||
|
expect(testComplete("|")).toContainEqual({
|
||||||
|
label: "hashFiles",
|
||||||
|
function: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("for contexts", () => {
|
||||||
|
it("provides suggestions for env", () => {
|
||||||
|
const expected = completionItems("BAR_TEST", "FOO");
|
||||||
|
expect(testComplete("env.X")).toEqual(expected);
|
||||||
|
expect(testComplete("1 == env.F")).toEqual(expected);
|
||||||
|
expect(testComplete("env.")).toEqual(expected);
|
||||||
|
expect(testComplete("env.FOO")).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("provides suggestions for secrets", () => {
|
||||||
|
const expected = completionItems("AWS_TOKEN");
|
||||||
|
|
||||||
|
expect(testComplete("secrets.A")).toEqual(expected);
|
||||||
|
expect(testComplete("1 == secrets.F")).toEqual(expected);
|
||||||
|
expect(testComplete("toJSON(secrets.")).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("provides suggestions for contexts in function call", () => {
|
||||||
|
expect(testComplete("toJSON(env.|)")).toEqual(
|
||||||
|
completionItems("BAR_TEST", "FOO")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("trimTokenVector", () => {
|
||||||
|
test.each<{
|
||||||
|
input: string;
|
||||||
|
expected: TokenType[];
|
||||||
|
}>([
|
||||||
|
{
|
||||||
|
input: "env.",
|
||||||
|
expected: [TokenType.IDENTIFIER, TokenType.DOT, TokenType.EOF],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "github.act",
|
||||||
|
expected: [
|
||||||
|
TokenType.IDENTIFIER,
|
||||||
|
TokenType.DOT,
|
||||||
|
TokenType.IDENTIFIER,
|
||||||
|
TokenType.EOF,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "1 == github.act",
|
||||||
|
expected: [
|
||||||
|
TokenType.IDENTIFIER,
|
||||||
|
TokenType.DOT,
|
||||||
|
TokenType.IDENTIFIER,
|
||||||
|
TokenType.EOF,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "github.mona == github.act",
|
||||||
|
expected: [
|
||||||
|
TokenType.IDENTIFIER,
|
||||||
|
TokenType.DOT,
|
||||||
|
TokenType.IDENTIFIER,
|
||||||
|
TokenType.EOF,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "github['test'].",
|
||||||
|
expected: [
|
||||||
|
TokenType.IDENTIFIER,
|
||||||
|
TokenType.LEFT_BRACKET,
|
||||||
|
TokenType.STRING,
|
||||||
|
TokenType.RIGHT_BRACKET,
|
||||||
|
TokenType.DOT,
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
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;
|
||||||
|
description?: string;
|
||||||
|
function: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Complete returns a list of completion items for the given expression.
|
||||||
|
//
|
||||||
|
// The main functionality is auto-completing functions and context access:
|
||||||
|
// We can only provide assistance if the input is in one of the following forms (with | denoting the cursor position):
|
||||||
|
// - context.path.inp| or context.path['inp| -- auto-complete context access
|
||||||
|
// - 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[] {
|
||||||
|
// Lex
|
||||||
|
const lexer = new Lexer(input);
|
||||||
|
const lexResult = lexer.lex();
|
||||||
|
|
||||||
|
// Find interesting part of the tokenVector. For example, for an expression like `github.actor == env.actor.log|`, we are
|
||||||
|
// only interested in the `env.actor.log` part for auto-completion
|
||||||
|
const tokenInputVector = trimTokenVector(lexResult.tokens);
|
||||||
|
|
||||||
|
// Start by skipping the EOF token
|
||||||
|
let tokenIdx = tokenInputVector.length - 2;
|
||||||
|
|
||||||
|
if (tokenIdx >= 0) {
|
||||||
|
switch (tokenInputVector[tokenIdx].type) {
|
||||||
|
// If there is a (partial) identifier under the cursor, ignore that
|
||||||
|
case TokenType.IDENTIFIER:
|
||||||
|
tokenIdx--;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TokenType.STRING:
|
||||||
|
// TODO: Support string for `context.name['test|`
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tokenIdx < 0) {
|
||||||
|
// Vector only contains the EOF token. Suggest functions and root context access
|
||||||
|
const result = contextKeys(context);
|
||||||
|
|
||||||
|
// Merge with functions
|
||||||
|
result.push(...functionItems(extensionFunctions));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine path that led to the last token
|
||||||
|
// Use parser & evaluator to determine context to complete.
|
||||||
|
const pathTokenVector = tokenInputVector.slice(0, tokenIdx);
|
||||||
|
|
||||||
|
// Include the original EOF token to make the parser happy
|
||||||
|
pathTokenVector.push(tokenInputVector[tokenInputVector.length - 1]);
|
||||||
|
|
||||||
|
const p = new Parser(
|
||||||
|
pathTokenVector,
|
||||||
|
context.pairs().map((x) => x.key),
|
||||||
|
extensionFunctions
|
||||||
|
);
|
||||||
|
const expr = p.parse();
|
||||||
|
|
||||||
|
const ev = new Evaluator(expr, context);
|
||||||
|
const result = ev.evaluate();
|
||||||
|
|
||||||
|
return contextKeys(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
function functionItems(extensionFunctions: FunctionInfo[]): CompletionItem[] {
|
||||||
|
const result: CompletionItem[] = [];
|
||||||
|
|
||||||
|
for (const fdef of [
|
||||||
|
...Object.values(wellKnownFunctions),
|
||||||
|
...extensionFunctions,
|
||||||
|
]) {
|
||||||
|
result.push({
|
||||||
|
label: fdef.name,
|
||||||
|
description: fdef.description,
|
||||||
|
function: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort functions
|
||||||
|
result.sort((a, b) => a.label.localeCompare(b.label));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function contextKeys(context: ExpressionData): CompletionItem[] {
|
||||||
|
if (isDictionary(context)) {
|
||||||
|
return (
|
||||||
|
context
|
||||||
|
.pairs()
|
||||||
|
.map((x) => completionItemFromContext(x.key))
|
||||||
|
// Sort contexts
|
||||||
|
.sort((a, b) => a.label.localeCompare(b.label))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function completionItemFromContext(context: string): CompletionItem {
|
||||||
|
const parenIndex = context.indexOf("(");
|
||||||
|
const isFunc = parenIndex >= 0 && context.indexOf(")") >= 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: isFunc ? context.substring(0, parenIndex) : context,
|
||||||
|
function: isFunc,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function trimTokenVector(tokenVector: Token[]): Token[] {
|
||||||
|
let tokenIdx = tokenVector.length;
|
||||||
|
|
||||||
|
while (tokenIdx > 0) {
|
||||||
|
const token = tokenVector[tokenIdx - 1];
|
||||||
|
switch (token.type) {
|
||||||
|
case TokenType.IDENTIFIER:
|
||||||
|
case TokenType.DOT:
|
||||||
|
case TokenType.EOF:
|
||||||
|
case TokenType.LEFT_BRACKET:
|
||||||
|
case TokenType.RIGHT_BRACKET:
|
||||||
|
case TokenType.STRING:
|
||||||
|
tokenIdx--;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only keep the part of the token vector we're interested in
|
||||||
|
return tokenVector.slice(tokenIdx);
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import {
|
||||||
|
ExpressionData,
|
||||||
|
ExpressionDataInterface,
|
||||||
|
Kind,
|
||||||
|
kindStr,
|
||||||
|
} from "./expressiondata";
|
||||||
|
|
||||||
|
export class Array implements ExpressionDataInterface {
|
||||||
|
private v: ExpressionData[] = [];
|
||||||
|
|
||||||
|
constructor(...data: ExpressionData[]) {
|
||||||
|
for (const d of data) {
|
||||||
|
this.add(d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly kind = Kind.Array;
|
||||||
|
|
||||||
|
public primitive = false;
|
||||||
|
|
||||||
|
coerceString(): string {
|
||||||
|
return kindStr(this.kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
number(): number {
|
||||||
|
return NaN;
|
||||||
|
}
|
||||||
|
|
||||||
|
add(value: ExpressionData) {
|
||||||
|
this.v.push(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
get(index: number): ExpressionData {
|
||||||
|
return this.v[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
values(): ExpressionData[] {
|
||||||
|
return this.v;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { ExpressionDataInterface, Kind } from "./expressiondata";
|
||||||
|
|
||||||
|
export class BooleanData implements ExpressionDataInterface {
|
||||||
|
constructor(public readonly value: boolean) {}
|
||||||
|
|
||||||
|
public readonly kind = Kind.Boolean;
|
||||||
|
|
||||||
|
public primitive = true;
|
||||||
|
|
||||||
|
coerceString(): string {
|
||||||
|
if (this.value) {
|
||||||
|
return "true";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "false";
|
||||||
|
}
|
||||||
|
|
||||||
|
number(): number {
|
||||||
|
if (this.value) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
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") }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import {
|
||||||
|
ExpressionData,
|
||||||
|
ExpressionDataInterface,
|
||||||
|
Kind,
|
||||||
|
kindStr,
|
||||||
|
Pair,
|
||||||
|
} from "./expressiondata";
|
||||||
|
|
||||||
|
export class Dictionary implements ExpressionDataInterface {
|
||||||
|
private keys: string[] = [];
|
||||||
|
private v: ExpressionData[] = [];
|
||||||
|
private indexMap: { [key: string]: number } = {};
|
||||||
|
|
||||||
|
constructor(...pairs: Pair[]) {
|
||||||
|
for (const p of pairs) {
|
||||||
|
this.add(p.key, p.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly kind = Kind.Dictionary;
|
||||||
|
|
||||||
|
public primitive = false;
|
||||||
|
|
||||||
|
coerceString(): string {
|
||||||
|
return kindStr(this.kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
number(): number {
|
||||||
|
return NaN;
|
||||||
|
}
|
||||||
|
|
||||||
|
add(key: string, value: ExpressionData) {
|
||||||
|
if (this.indexMap[key.toLowerCase()]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.keys.push(key);
|
||||||
|
this.v.push(value);
|
||||||
|
this.indexMap[key.toLowerCase()] = this.v.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
get(key: string): ExpressionData | undefined {
|
||||||
|
const index = this.indexMap[key.toLowerCase()];
|
||||||
|
if (index === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.v[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
values(): ExpressionData[] {
|
||||||
|
return this.v;
|
||||||
|
}
|
||||||
|
|
||||||
|
pairs(): Pair[] {
|
||||||
|
const result: Pair[] = [];
|
||||||
|
|
||||||
|
for (const key of this.keys) {
|
||||||
|
result.push({ key, value: this.v[this.indexMap[key.toLowerCase()]] });
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isDictionary(x: ExpressionData): x is Dictionary {
|
||||||
|
return x.kind === Kind.Dictionary;
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
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,
|
||||||
|
Array,
|
||||||
|
Dictionary,
|
||||||
|
Boolean,
|
||||||
|
Number,
|
||||||
|
CaseSensitiveDictionary,
|
||||||
|
Null,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function kindStr(k: Kind): string {
|
||||||
|
switch (k) {
|
||||||
|
case Kind.Array:
|
||||||
|
return "Array";
|
||||||
|
case Kind.Boolean:
|
||||||
|
return "Boolean";
|
||||||
|
case Kind.Null:
|
||||||
|
return "Null";
|
||||||
|
case Kind.Number:
|
||||||
|
return "Number";
|
||||||
|
case Kind.Dictionary:
|
||||||
|
return "Object";
|
||||||
|
case Kind.String:
|
||||||
|
return "String";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExpressionDataInterface {
|
||||||
|
kind: Kind;
|
||||||
|
primitive: boolean;
|
||||||
|
|
||||||
|
coerceString(): string;
|
||||||
|
|
||||||
|
number(): number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ExpressionData =
|
||||||
|
| Array
|
||||||
|
| Dictionary
|
||||||
|
| StringData
|
||||||
|
| BooleanData
|
||||||
|
| NumberData
|
||||||
|
| Null;
|
||||||
|
|
||||||
|
export type Pair = {
|
||||||
|
key: string;
|
||||||
|
value: ExpressionData;
|
||||||
|
};
|
||||||
@@ -0,0 +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";
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import {
|
||||||
|
ExpressionData,
|
||||||
|
ExpressionDataInterface,
|
||||||
|
Kind,
|
||||||
|
} from "./expressiondata";
|
||||||
|
|
||||||
|
export class Null implements ExpressionDataInterface {
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
public readonly kind = Kind.Null;
|
||||||
|
|
||||||
|
public primitive = true;
|
||||||
|
|
||||||
|
coerceString(): string {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
number(): number {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ExpressionDataInterface, Kind } from "./expressiondata";
|
||||||
|
|
||||||
|
export class NumberData implements ExpressionDataInterface {
|
||||||
|
constructor(public readonly value: number) {}
|
||||||
|
|
||||||
|
public readonly kind = Kind.Number;
|
||||||
|
|
||||||
|
public primitive = true;
|
||||||
|
|
||||||
|
coerceString(): string {
|
||||||
|
if (this.value === -0) {
|
||||||
|
return "0";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Workaround to limit the precision to at most 15 digits. Format the number to a string, then parse
|
||||||
|
// it back to a number to remove trailing zeroes to prevent numbers to be converted to 1.200000000...
|
||||||
|
return (+this.value.toFixed(15)).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
number(): number {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
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", () => {
|
||||||
|
expect(JSON.stringify(new Null(), replacer, " ")).toEqual("null");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("array", () => {
|
||||||
|
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}');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
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
|
||||||
|
*
|
||||||
|
* See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#replacer
|
||||||
|
*/
|
||||||
|
export function replacer(key: string, value: any): any {
|
||||||
|
if (value instanceof Null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value instanceof BooleanData) {
|
||||||
|
return value.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value instanceof NumberData) {
|
||||||
|
return value.number();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value instanceof StringData) {
|
||||||
|
return value.coerceString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value instanceof Array) {
|
||||||
|
return value.values();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value instanceof Dictionary) {
|
||||||
|
const pairs = value.pairs();
|
||||||
|
|
||||||
|
const r: any = {};
|
||||||
|
for (const p of pairs) {
|
||||||
|
r[p.key] = p.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
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: {
|
||||||
|
name: string;
|
||||||
|
data: string;
|
||||||
|
want: ExpressionData;
|
||||||
|
}[] = [
|
||||||
|
{
|
||||||
|
name: "null",
|
||||||
|
data: "null",
|
||||||
|
want: new Null(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "number",
|
||||||
|
data: "1",
|
||||||
|
want: new NumberData(1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "string",
|
||||||
|
data: `"a"`,
|
||||||
|
want: new StringData("a"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "true boolean",
|
||||||
|
data: "true",
|
||||||
|
want: new BooleanData(true),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "false boolean",
|
||||||
|
data: "false",
|
||||||
|
want: new BooleanData(false),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "array",
|
||||||
|
data: `[1,2,3]`,
|
||||||
|
want: new Array(new NumberData(1), new NumberData(2), new NumberData(3)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nested array",
|
||||||
|
data: `[1,2,[3,4],5]`,
|
||||||
|
want: new Array(
|
||||||
|
new NumberData(1),
|
||||||
|
new NumberData(2),
|
||||||
|
new Array(new NumberData(3), new NumberData(4)),
|
||||||
|
new NumberData(5)
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "complex array",
|
||||||
|
data: `[{"a":[true,2]},{"b":"three"},{"c":null}]`,
|
||||||
|
want: new Array(
|
||||||
|
new Dictionary({
|
||||||
|
key: "a",
|
||||||
|
value: new Array(new BooleanData(true), new NumberData(2)),
|
||||||
|
}),
|
||||||
|
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(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
test.each(tests)(
|
||||||
|
"$name",
|
||||||
|
({ data, want }: { data: string; want: ExpressionData }) => {
|
||||||
|
expect(JSON.parse(data, reviver)).toEqual(want);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
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.
|
||||||
|
*
|
||||||
|
* See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#reviver
|
||||||
|
*/
|
||||||
|
export function reviver(key: string, val: any): ExpressionData {
|
||||||
|
if (val === null) {
|
||||||
|
return new Null();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof val === "string") {
|
||||||
|
return new StringData(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof val === "number") {
|
||||||
|
return new NumberData(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof val === "boolean") {
|
||||||
|
return new BooleanData(val as boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(val)) {
|
||||||
|
return new dArray(...val);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof val === "object") {
|
||||||
|
return new Dictionary(
|
||||||
|
...Object.keys(val).map(
|
||||||
|
(k) =>
|
||||||
|
({
|
||||||
|
key: k,
|
||||||
|
value: val[k],
|
||||||
|
} as Pair)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass through value
|
||||||
|
return val;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { ExpressionDataInterface, Kind } from "./expressiondata";
|
||||||
|
|
||||||
|
export class StringData implements ExpressionDataInterface {
|
||||||
|
constructor(public readonly value: string) {}
|
||||||
|
|
||||||
|
public readonly kind = Kind.String;
|
||||||
|
|
||||||
|
public primitive = true;
|
||||||
|
|
||||||
|
coerceString(): string {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
number(): number {
|
||||||
|
return Number(this.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { Pos, Token, tokenString } from "./lexer";
|
||||||
|
|
||||||
|
export const MAX_PARSER_DEPTH = 50;
|
||||||
|
export const MAX_EXPRESSION_LENGTH = 21000;
|
||||||
|
|
||||||
|
export enum ErrorType {
|
||||||
|
ErrorUnexpectedSymbol,
|
||||||
|
ErrorUnrecognizedNamedValue,
|
||||||
|
ErrorUnexpectedEndOfExpression,
|
||||||
|
|
||||||
|
ErrorExceededMaxDepth,
|
||||||
|
ErrorExceededMaxLength,
|
||||||
|
ErrorTooFewParameters,
|
||||||
|
ErrorTooManyParameters,
|
||||||
|
ErrorUnrecognizedContext,
|
||||||
|
ErrorUnrecognizedFunction,
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ExpressionError extends Error {
|
||||||
|
constructor(private typ: ErrorType, private tok: Token) {
|
||||||
|
super(`${errorDescription(typ)}: '${tokenString(tok)}'`);
|
||||||
|
|
||||||
|
this.pos = this.tok.pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
public pos: Pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
function errorDescription(typ: ErrorType): string {
|
||||||
|
switch (typ) {
|
||||||
|
case ErrorType.ErrorUnexpectedEndOfExpression:
|
||||||
|
return "Unexpected end of expression";
|
||||||
|
case ErrorType.ErrorUnexpectedSymbol:
|
||||||
|
return "Unexpected symbol";
|
||||||
|
case ErrorType.ErrorUnrecognizedNamedValue:
|
||||||
|
return "Unrecognized named-value";
|
||||||
|
case ErrorType.ErrorExceededMaxDepth:
|
||||||
|
return `Exceeded max expression depth ${MAX_PARSER_DEPTH}`;
|
||||||
|
case ErrorType.ErrorExceededMaxLength:
|
||||||
|
return `Exceeded max expression length ${MAX_EXPRESSION_LENGTH}`;
|
||||||
|
case ErrorType.ErrorTooFewParameters:
|
||||||
|
return "Too few parameters supplied";
|
||||||
|
case ErrorType.ErrorTooManyParameters:
|
||||||
|
return "Too many parameters supplied";
|
||||||
|
case ErrorType.ErrorUnrecognizedContext:
|
||||||
|
return "Unrecognized named-value";
|
||||||
|
case ErrorType.ErrorUnrecognizedFunction:
|
||||||
|
return "Unrecognized function";
|
||||||
|
default: // Should never reach here.
|
||||||
|
return "Unknown error";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import * as data from "./data";
|
||||||
|
import { Evaluator } from "./evaluator";
|
||||||
|
import { Lexer } from "./lexer";
|
||||||
|
import { Parser } from "./parser";
|
||||||
|
|
||||||
|
test("evaluator", () => {
|
||||||
|
const input = "foo['']";
|
||||||
|
|
||||||
|
const lexer = new Lexer(input);
|
||||||
|
const result = lexer.lex();
|
||||||
|
|
||||||
|
// Parse
|
||||||
|
const parser = new Parser(result.tokens, ["foo"], []);
|
||||||
|
const expr = parser.parse();
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
});
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
import {
|
||||||
|
Binary,
|
||||||
|
ContextAccess,
|
||||||
|
Expr,
|
||||||
|
ExprVisitor,
|
||||||
|
FunctionCall,
|
||||||
|
Grouping,
|
||||||
|
IndexAccess,
|
||||||
|
Literal,
|
||||||
|
Logical,
|
||||||
|
Star,
|
||||||
|
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";
|
||||||
|
|
||||||
|
export class EvaluationError extends Error {}
|
||||||
|
|
||||||
|
export class Evaluator implements ExprVisitor<data.ExpressionData> {
|
||||||
|
constructor(private n: Expr, private context: data.Dictionary) {}
|
||||||
|
|
||||||
|
public evaluate(): data.ExpressionData {
|
||||||
|
return this.eval(this.n);
|
||||||
|
}
|
||||||
|
|
||||||
|
private eval(n: Expr): data.ExpressionData {
|
||||||
|
return n.accept(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
visitLiteral(literal: Literal): data.ExpressionData {
|
||||||
|
return literal.literal;
|
||||||
|
}
|
||||||
|
|
||||||
|
visitUnary(unary: Unary): data.ExpressionData {
|
||||||
|
const r = this.eval(unary.expr);
|
||||||
|
|
||||||
|
if (unary.operator.type === TokenType.BANG) {
|
||||||
|
return new data.BooleanData(falsy(r));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`unknown unary operator: ${unary.operator.lexeme}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
visitBinary(binary: Binary): data.ExpressionData {
|
||||||
|
const left = this.eval(binary.left);
|
||||||
|
const right = this.eval(binary.right);
|
||||||
|
|
||||||
|
switch (binary.operator.type) {
|
||||||
|
case TokenType.EQUAL_EQUAL:
|
||||||
|
return new data.BooleanData(equals(left, right));
|
||||||
|
|
||||||
|
case TokenType.BANG_EQUAL:
|
||||||
|
return new data.BooleanData(!equals(left, right));
|
||||||
|
|
||||||
|
case TokenType.GREATER:
|
||||||
|
return new data.BooleanData(greaterThan(left, right));
|
||||||
|
|
||||||
|
case TokenType.GREATER_EQUAL:
|
||||||
|
return new data.BooleanData(
|
||||||
|
equals(left, right) || greaterThan(left, right)
|
||||||
|
);
|
||||||
|
|
||||||
|
case TokenType.LESS:
|
||||||
|
return new data.BooleanData(lessThan(left, right));
|
||||||
|
|
||||||
|
case TokenType.LESS_EQUAL:
|
||||||
|
return new data.BooleanData(equals(left, right) || lessThan(left, right));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`unknown binary operator: ${binary.operator.lexeme}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
visitLogical(logical: Logical): data.ExpressionData {
|
||||||
|
let result: data.ExpressionData;
|
||||||
|
|
||||||
|
for (const arg of logical.args) {
|
||||||
|
result = this.eval(arg);
|
||||||
|
|
||||||
|
// Break?
|
||||||
|
if (
|
||||||
|
(logical.operator.type === TokenType.AND && falsy(result)) ||
|
||||||
|
(logical.operator.type === TokenType.OR && truthy(result))
|
||||||
|
) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// result is always assigned before we return here
|
||||||
|
return result!;
|
||||||
|
}
|
||||||
|
|
||||||
|
visitGrouping(grouping: Grouping): data.ExpressionData {
|
||||||
|
return this.eval(grouping.group);
|
||||||
|
}
|
||||||
|
|
||||||
|
visitContextAccess(contextAccess: ContextAccess): data.ExpressionData {
|
||||||
|
const r = this.context.get(contextAccess.name.lexeme)!;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
visitIndexAccess(ia: IndexAccess): data.ExpressionData {
|
||||||
|
let idx: idxHelper;
|
||||||
|
if (ia.index instanceof Star) {
|
||||||
|
idx = new idxHelper(true, undefined);
|
||||||
|
} else {
|
||||||
|
let idxResult: data.ExpressionData;
|
||||||
|
try {
|
||||||
|
idxResult = this.eval(ia.index);
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`could not evaluate index for index access: ${e}`);
|
||||||
|
}
|
||||||
|
idx = new idxHelper(false, idxResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
const objResult = this.eval(ia.expr);
|
||||||
|
|
||||||
|
let result: data.ExpressionData;
|
||||||
|
switch (objResult.kind) {
|
||||||
|
case data.Kind.Array: {
|
||||||
|
const tobjResult = objResult as data.Array;
|
||||||
|
if (tobjResult instanceof FilteredArray) {
|
||||||
|
result = filteredArrayAccess(tobjResult as FilteredArray, idx);
|
||||||
|
} else {
|
||||||
|
result = arrayAccess(tobjResult, idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case data.Kind.Dictionary: {
|
||||||
|
const tobjResult = objResult as data.Dictionary;
|
||||||
|
result = objectAccess(tobjResult, idx);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
if (idx.star) {
|
||||||
|
result = new FilteredArray();
|
||||||
|
} else {
|
||||||
|
result = new data.Null();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
visitFunctionCall(functionCall: FunctionCall): data.ExpressionData {
|
||||||
|
// Evaluate arguments
|
||||||
|
const args = functionCall.args.map((arg) => this.eval(arg));
|
||||||
|
|
||||||
|
return fcall(functionCall, 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();
|
||||||
|
|
||||||
|
for (const item of fa.values()) {
|
||||||
|
// Check the type of the nested item
|
||||||
|
switch (item.kind) {
|
||||||
|
case data.Kind.Dictionary: {
|
||||||
|
const ti = item as data.Dictionary;
|
||||||
|
if (idx.star) {
|
||||||
|
for (const v of ti.values()) {
|
||||||
|
result.add(v);
|
||||||
|
}
|
||||||
|
} else if (idx.str !== undefined) {
|
||||||
|
const v = ti.get(idx.str);
|
||||||
|
if (v !== undefined) {
|
||||||
|
result.add(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case data.Kind.Array: {
|
||||||
|
const ti = item as data.Array;
|
||||||
|
if (idx.star) {
|
||||||
|
for (const v of ti.values()) {
|
||||||
|
result.add(v);
|
||||||
|
}
|
||||||
|
} else if (idx.int !== undefined && idx.int < ti.values().length) {
|
||||||
|
result.add(ti.get(idx.int));
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function arrayAccess(a: data.Array, idx: idxHelper): data.ExpressionData {
|
||||||
|
if (idx.star) {
|
||||||
|
const fa = new FilteredArray();
|
||||||
|
for (const item of a.values()) {
|
||||||
|
fa.add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (idx.int !== undefined && idx.int < a.values().length) {
|
||||||
|
return a.get(idx.int);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new data.Null();
|
||||||
|
}
|
||||||
|
|
||||||
|
function objectAccess(
|
||||||
|
obj: data.Dictionary,
|
||||||
|
idx: idxHelper
|
||||||
|
): data.ExpressionData {
|
||||||
|
if (idx.star) {
|
||||||
|
const fa = new FilteredArray(...obj.values());
|
||||||
|
return fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (idx.str !== undefined) {
|
||||||
|
const r = obj.get(idx.str);
|
||||||
|
if (r !== undefined) {
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new data.Null();
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import * as data from "./data";
|
||||||
|
|
||||||
|
export class FilteredArray extends data.Array {}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
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;
|
||||||
|
extensionContexts: Map<string, boolean>;
|
||||||
|
extensionFunctions: Map<string, FunctionInfo>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const wellKnownFunctions: { [name: string]: FunctionDefinition } = {
|
||||||
|
contains: contains,
|
||||||
|
endswith: endswith,
|
||||||
|
format: format,
|
||||||
|
fromjson: fromjson,
|
||||||
|
join: join,
|
||||||
|
startswith: startswith,
|
||||||
|
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
|
||||||
|
) {
|
||||||
|
// Expression function names are case-insensitive.
|
||||||
|
const name = identifier.lexeme.toLowerCase();
|
||||||
|
|
||||||
|
let f: FunctionInfo | undefined;
|
||||||
|
f = wellKnownFunctions[name];
|
||||||
|
if (!f) {
|
||||||
|
f = context.extensionFunctions.get(name);
|
||||||
|
if (!f) {
|
||||||
|
if (!context.allowUnknownKeywords) {
|
||||||
|
throw new ExpressionError(
|
||||||
|
ErrorType.ErrorUnrecognizedFunction,
|
||||||
|
identifier
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip argument validation for unknown functions
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (argCount < f.minArgs) {
|
||||||
|
throw new ExpressionError(ErrorType.ErrorTooFewParameters, identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (argCount > f.maxArgs) {
|
||||||
|
throw new ExpressionError(ErrorType.ErrorTooManyParameters, identifier);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { Array, BooleanData, ExpressionData, Kind } from "../data";
|
||||||
|
import { equals } from "../result";
|
||||||
|
import { FunctionDefinition } from "./info";
|
||||||
|
|
||||||
|
export const contains: FunctionDefinition = {
|
||||||
|
name: "contains",
|
||||||
|
description:
|
||||||
|
"`contains( search, item )`\n\nReturns `true` if `search` contains `item`. If `search` is an array, this function returns `true` if the `item` is an element in the array. If `search` is a string, this function returns `true` if the `item` is a substring of `search`. This function is not case sensitive. Casts values to a string.",
|
||||||
|
minArgs: 2,
|
||||||
|
maxArgs: 2,
|
||||||
|
call: (...args: ExpressionData[]): ExpressionData => {
|
||||||
|
const left = args[0];
|
||||||
|
const right = args[1];
|
||||||
|
|
||||||
|
if (left.primitive) {
|
||||||
|
const ls = left.coerceString();
|
||||||
|
if (right.primitive) {
|
||||||
|
const rs = right.coerceString();
|
||||||
|
return new BooleanData(ls.toLowerCase().includes(rs.toLowerCase()));
|
||||||
|
}
|
||||||
|
} else if (left.kind === Kind.Array) {
|
||||||
|
const la = left as Array;
|
||||||
|
if (la.values().length === 0) {
|
||||||
|
return new BooleanData(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const v of la.values()) {
|
||||||
|
if (equals(right, v)) {
|
||||||
|
return new BooleanData(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new BooleanData(false);
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { BooleanData, ExpressionData } from "../data";
|
||||||
|
import { toUpperSpecial } from "../result";
|
||||||
|
import { FunctionDefinition } from "./info";
|
||||||
|
|
||||||
|
export const endswith: FunctionDefinition = {
|
||||||
|
name: "endsWith",
|
||||||
|
description:
|
||||||
|
"`endsWith( searchString, searchValue )`\n\nReturns `true` if `searchString` ends with `searchValue`. This function is not case sensitive. Casts values to a string.",
|
||||||
|
minArgs: 2,
|
||||||
|
maxArgs: 2,
|
||||||
|
call: (...args: ExpressionData[]): ExpressionData => {
|
||||||
|
const left = args[0];
|
||||||
|
if (!left.primitive) {
|
||||||
|
return new BooleanData(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const right = args[1];
|
||||||
|
if (!right.primitive) {
|
||||||
|
return new BooleanData(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ls = toUpperSpecial(left.coerceString());
|
||||||
|
const rs = toUpperSpecial(right.coerceString());
|
||||||
|
|
||||||
|
return new BooleanData(ls.endsWith(rs));
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
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")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import { ExpressionData, StringData } from "../data";
|
||||||
|
import { FunctionDefinition } from "./info";
|
||||||
|
|
||||||
|
export const format: FunctionDefinition = {
|
||||||
|
name: "format",
|
||||||
|
description:
|
||||||
|
"`format( string, replaceValue0, replaceValue1, ..., replaceValueN)`\n\nReplaces values in the `string`, with the variable `replaceValueN`. Variables in the `string` are specified using the `{N}` syntax, where `N` is an integer. You must specify at least one `replaceValue` and `string`. There is no maximum for the number of variables (`replaceValueN`) you can use. Escape curly braces using double braces.",
|
||||||
|
minArgs: 1,
|
||||||
|
maxArgs: 255 /*MAX_ARGUMENTS*/,
|
||||||
|
call: (...args: ExpressionData[]): ExpressionData => {
|
||||||
|
const fs = args[0].coerceString();
|
||||||
|
|
||||||
|
const result: string[] = [];
|
||||||
|
let index = 0;
|
||||||
|
|
||||||
|
while (index < fs.length) {
|
||||||
|
const lbrace = fs.indexOf("{", index);
|
||||||
|
let rbrace = fs.indexOf("}", index);
|
||||||
|
|
||||||
|
// Left brace
|
||||||
|
if (lbrace >= 0 && (rbrace < 0 || rbrace > lbrace)) {
|
||||||
|
// Escaped left brace
|
||||||
|
if (safeCharAt(fs, lbrace + 1) === "{") {
|
||||||
|
result.push(fs.substr(index, lbrace - index + 1));
|
||||||
|
index = lbrace + 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Left brace, number, optional format specifiers, right brace
|
||||||
|
if (rbrace > lbrace + 1) {
|
||||||
|
const argIndex = readArgIndex(fs, lbrace + 1);
|
||||||
|
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}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append the portion before the left brace
|
||||||
|
if (lbrace > index) {
|
||||||
|
result.push(fs.substr(index, lbrace - index));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append the arg
|
||||||
|
result.push(`${args[1 + argIndex.result].coerceString()}`);
|
||||||
|
index = rbrace + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`The following format string is invalid: ${fs}`);
|
||||||
|
}
|
||||||
|
// Right brace
|
||||||
|
else if (rbrace >= 0) {
|
||||||
|
// Escaped right brace
|
||||||
|
if (safeCharAt(fs, rbrace + 1) === "}") {
|
||||||
|
result.push(fs.substr(index, rbrace - index + 1));
|
||||||
|
index = rbrace + 2;
|
||||||
|
} else {
|
||||||
|
throw new Error(`The following format string is invalid: ${fs}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Last segment
|
||||||
|
else {
|
||||||
|
result.push(fs.substr(index));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new StringData(result.join(""));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function safeCharAt(string: string, index: number): string {
|
||||||
|
if (string.length > index) {
|
||||||
|
return string[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
return "\0";
|
||||||
|
}
|
||||||
|
|
||||||
|
function readArgIndex(string: string, startIndex: number): ArgIndex {
|
||||||
|
// Count the number of digits
|
||||||
|
let length = 0;
|
||||||
|
while (true) {
|
||||||
|
const nextChar = safeCharAt(string, startIndex + length);
|
||||||
|
if (nextChar >= "0" && nextChar <= "9") {
|
||||||
|
length++;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate at least one digit
|
||||||
|
if (length < 1) {
|
||||||
|
return <ArgIndex>{
|
||||||
|
success: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the number
|
||||||
|
const endIndex = startIndex + length - 1;
|
||||||
|
const result = parseInt(string.substr(startIndex, length));
|
||||||
|
return <ArgIndex>{
|
||||||
|
success: !isNaN(result),
|
||||||
|
result: result,
|
||||||
|
endIndex: endIndex,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArgIndex {
|
||||||
|
success: boolean;
|
||||||
|
result: number;
|
||||||
|
endIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FormatSpecifiers {
|
||||||
|
success: boolean;
|
||||||
|
result: string;
|
||||||
|
rbrace: number;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { ExpressionData } from "../data";
|
||||||
|
import { reviver } from "../data/reviver";
|
||||||
|
import { FunctionDefinition } from "./info";
|
||||||
|
|
||||||
|
export const fromjson: FunctionDefinition = {
|
||||||
|
name: "fromJson",
|
||||||
|
description:
|
||||||
|
"`fromJSON(value)`\n\nReturns a JSON object or JSON data type for `value`. You can use this function to provide a JSON object as an evaluated expression or to convert environment variables from a string.",
|
||||||
|
minArgs: 1,
|
||||||
|
maxArgs: 1,
|
||||||
|
call: (...args: ExpressionData[]): ExpressionData => {
|
||||||
|
const input = args[0];
|
||||||
|
const is = input.coerceString();
|
||||||
|
|
||||||
|
if (is.trim() === "") {
|
||||||
|
throw new Error("empty input");
|
||||||
|
}
|
||||||
|
|
||||||
|
const d = JSON.parse(is, reviver);
|
||||||
|
return d;
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { ExpressionData } from "../data";
|
||||||
|
|
||||||
|
export interface FunctionInfo {
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
minArgs: number;
|
||||||
|
maxArgs: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FunctionDefinition extends FunctionInfo {
|
||||||
|
call: (...args: ExpressionData[]) => ExpressionData;
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { Array, ExpressionData, Kind, StringData } from "../data";
|
||||||
|
import { FunctionDefinition } from "./info";
|
||||||
|
|
||||||
|
export const join: FunctionDefinition = {
|
||||||
|
name: "join",
|
||||||
|
description:
|
||||||
|
"`join( array, optionalSeparator )`\n\nThe value for `array` can be an array or a string. All values in `array` are concatenated into a string. If you provide `optionalSeparator`, it is inserted between the concatenated values. Otherwise, the default separator `,` is used. Casts values to a string.",
|
||||||
|
minArgs: 1,
|
||||||
|
maxArgs: 2,
|
||||||
|
call: (...args: ExpressionData[]): ExpressionData => {
|
||||||
|
// Primitive
|
||||||
|
if (args[0].primitive) {
|
||||||
|
return new StringData(args[0].coerceString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Array
|
||||||
|
if (args[0].kind === Kind.Array) {
|
||||||
|
// Separator
|
||||||
|
let separator = ",";
|
||||||
|
if (args.length > 1 && args[1].primitive) {
|
||||||
|
separator = args[1].coerceString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert items to strings
|
||||||
|
return new StringData(
|
||||||
|
(args[0] as Array)
|
||||||
|
.values()
|
||||||
|
.map((item) => item.coerceString())
|
||||||
|
.join(separator)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new StringData("");
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { BooleanData, ExpressionData } from "../data";
|
||||||
|
import { toUpperSpecial } from "../result";
|
||||||
|
import { FunctionDefinition } from "./info";
|
||||||
|
|
||||||
|
export const startswith: FunctionDefinition = {
|
||||||
|
name: "startsWith",
|
||||||
|
description:
|
||||||
|
"`startsWith( searchString, searchValue )`\n\nReturns `true` when `searchString` starts with `searchValue`. This function is not case sensitive. Casts values to a string.",
|
||||||
|
minArgs: 2,
|
||||||
|
maxArgs: 2,
|
||||||
|
call: (...args: ExpressionData[]): ExpressionData => {
|
||||||
|
const left = args[0];
|
||||||
|
if (!left.primitive) {
|
||||||
|
return new BooleanData(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const right = args[1];
|
||||||
|
if (!right.primitive) {
|
||||||
|
return new BooleanData(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ls = toUpperSpecial(left.coerceString());
|
||||||
|
const rs = toUpperSpecial(right.coerceString());
|
||||||
|
|
||||||
|
return new BooleanData(ls.startsWith(rs));
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { ExpressionData, StringData } from "../data";
|
||||||
|
import { replacer } from "../data/replacer";
|
||||||
|
import { FunctionDefinition } from "./info";
|
||||||
|
|
||||||
|
export const tojson: FunctionDefinition = {
|
||||||
|
name: "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.",
|
||||||
|
minArgs: 1,
|
||||||
|
maxArgs: 1,
|
||||||
|
call: (...args: ExpressionData[]): ExpressionData => {
|
||||||
|
return new StringData(JSON.stringify(args[0], replacer, " "));
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { ExpressionData } from "./data";
|
||||||
|
|
||||||
|
export class idxHelper {
|
||||||
|
public readonly str: string | undefined;
|
||||||
|
public readonly int: number | undefined;
|
||||||
|
|
||||||
|
constructor(public readonly star: boolean, idx: ExpressionData | undefined) {
|
||||||
|
if (!star) {
|
||||||
|
if (idx!.primitive) {
|
||||||
|
this.str = idx!.coerceString();
|
||||||
|
}
|
||||||
|
|
||||||
|
let f = idx!.number();
|
||||||
|
if (!isNaN(f) && isFinite(f) && f >= 0) {
|
||||||
|
f = Math.floor(f);
|
||||||
|
this.int = f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
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";
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
import { Lexer, Token, TokenType } from "./lexer";
|
||||||
|
|
||||||
|
describe("lexer", () => {
|
||||||
|
const tests: {
|
||||||
|
input: string;
|
||||||
|
tokenType: TokenType[];
|
||||||
|
token?: Token;
|
||||||
|
}[] = [
|
||||||
|
{ 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.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] },
|
||||||
|
|
||||||
|
// Strings
|
||||||
|
{ input: "'It''s okay'", tokenType: [TokenType.STRING] },
|
||||||
|
{
|
||||||
|
input: "format('{0} == ''queued''', needs)",
|
||||||
|
tokenType: [
|
||||||
|
TokenType.IDENTIFIER,
|
||||||
|
TokenType.LEFT_PAREN,
|
||||||
|
TokenType.STRING,
|
||||||
|
TokenType.COMMA,
|
||||||
|
TokenType.IDENTIFIER,
|
||||||
|
TokenType.RIGHT_PAREN,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Arrays
|
||||||
|
{
|
||||||
|
input: "[1,2]",
|
||||||
|
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],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "1== 1",
|
||||||
|
tokenType: [TokenType.NUMBER, TokenType.EQUAL_EQUAL, TokenType.NUMBER],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "1< 1",
|
||||||
|
tokenType: [TokenType.NUMBER, TokenType.LESS, TokenType.NUMBER],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Identifiers
|
||||||
|
{
|
||||||
|
input: "github",
|
||||||
|
tokenType: [TokenType.IDENTIFIER],
|
||||||
|
token: {
|
||||||
|
type: TokenType.IDENTIFIER,
|
||||||
|
lexeme: "github",
|
||||||
|
pos: {
|
||||||
|
line: 0,
|
||||||
|
column: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Keywords
|
||||||
|
{
|
||||||
|
input: "true",
|
||||||
|
tokenType: [TokenType.TRUE],
|
||||||
|
token: {
|
||||||
|
type: TokenType.TRUE,
|
||||||
|
lexeme: "true",
|
||||||
|
pos: {
|
||||||
|
line: 0,
|
||||||
|
column: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
input: "false",
|
||||||
|
tokenType: [TokenType.FALSE],
|
||||||
|
token: {
|
||||||
|
type: TokenType.FALSE,
|
||||||
|
lexeme: "false",
|
||||||
|
pos: {
|
||||||
|
line: 0,
|
||||||
|
column: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
input: "null",
|
||||||
|
tokenType: [TokenType.NULL],
|
||||||
|
token: {
|
||||||
|
type: TokenType.NULL,
|
||||||
|
lexeme: "null",
|
||||||
|
pos: {
|
||||||
|
line: 0,
|
||||||
|
column: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
test.each(tests)(
|
||||||
|
"$input",
|
||||||
|
({
|
||||||
|
input,
|
||||||
|
tokenType,
|
||||||
|
token,
|
||||||
|
}: {
|
||||||
|
input: string;
|
||||||
|
tokenType: TokenType[];
|
||||||
|
token?: Token;
|
||||||
|
}) => {
|
||||||
|
const l = new Lexer(input);
|
||||||
|
|
||||||
|
const r = l.lex();
|
||||||
|
|
||||||
|
const want = r.tokens.map((t) => t.type);
|
||||||
|
|
||||||
|
tokenType.push(TokenType.EOF);
|
||||||
|
|
||||||
|
expect(want).toEqual(tokenType);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,425 @@
|
|||||||
|
import { StringData } from "./data";
|
||||||
|
import { MAX_EXPRESSION_LENGTH } from "./errors";
|
||||||
|
|
||||||
|
export enum TokenType {
|
||||||
|
UNKNOWN,
|
||||||
|
LEFT_PAREN,
|
||||||
|
RIGHT_PAREN,
|
||||||
|
LEFT_BRACKET,
|
||||||
|
RIGHT_BRACKET,
|
||||||
|
COMMA,
|
||||||
|
DOT,
|
||||||
|
|
||||||
|
BANG,
|
||||||
|
BANG_EQUAL,
|
||||||
|
EQUAL_EQUAL,
|
||||||
|
GREATER,
|
||||||
|
GREATER_EQUAL,
|
||||||
|
LESS,
|
||||||
|
LESS_EQUAL,
|
||||||
|
AND,
|
||||||
|
OR,
|
||||||
|
|
||||||
|
STAR,
|
||||||
|
NUMBER,
|
||||||
|
STRING,
|
||||||
|
IDENTIFIER,
|
||||||
|
TRUE,
|
||||||
|
FALSE,
|
||||||
|
NULL,
|
||||||
|
|
||||||
|
EOF,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Pos = {
|
||||||
|
line: number;
|
||||||
|
column: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Token = {
|
||||||
|
type: TokenType;
|
||||||
|
|
||||||
|
lexeme: string;
|
||||||
|
value?: string | number | boolean;
|
||||||
|
|
||||||
|
pos: Pos;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function tokenString(tok: Token): string {
|
||||||
|
switch (tok.type) {
|
||||||
|
case TokenType.EOF:
|
||||||
|
return "EOF";
|
||||||
|
case TokenType.NUMBER:
|
||||||
|
return tok.lexeme;
|
||||||
|
case TokenType.STRING:
|
||||||
|
return tok.value!.toString();
|
||||||
|
default:
|
||||||
|
return tok.lexeme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Result = {
|
||||||
|
tokens: Token[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Lexer {
|
||||||
|
private start = 0;
|
||||||
|
private offset = 0;
|
||||||
|
|
||||||
|
private line = 0;
|
||||||
|
private lastLineOffset = 0;
|
||||||
|
|
||||||
|
private tokens: Token[] = [];
|
||||||
|
|
||||||
|
constructor(private input: string) {}
|
||||||
|
|
||||||
|
lex(): Result {
|
||||||
|
if (this.input.length > MAX_EXPRESSION_LENGTH) {
|
||||||
|
throw new Error("ErrorExceededMaxLength");
|
||||||
|
}
|
||||||
|
|
||||||
|
while (!this.atEnd()) {
|
||||||
|
this.start = this.offset;
|
||||||
|
const c = this.next();
|
||||||
|
|
||||||
|
switch (c) {
|
||||||
|
case "(":
|
||||||
|
this.addToken(TokenType.LEFT_PAREN);
|
||||||
|
break;
|
||||||
|
case ")":
|
||||||
|
this.addToken(TokenType.RIGHT_PAREN);
|
||||||
|
break;
|
||||||
|
case "[":
|
||||||
|
this.addToken(TokenType.LEFT_BRACKET);
|
||||||
|
break;
|
||||||
|
case "]":
|
||||||
|
this.addToken(TokenType.RIGHT_BRACKET);
|
||||||
|
break;
|
||||||
|
case ",":
|
||||||
|
this.addToken(TokenType.COMMA);
|
||||||
|
break;
|
||||||
|
case ".":
|
||||||
|
if (
|
||||||
|
this.previous() != TokenType.IDENTIFIER &&
|
||||||
|
this.previous() != TokenType.RIGHT_BRACKET &&
|
||||||
|
this.previous() != TokenType.RIGHT_PAREN &&
|
||||||
|
this.previous() != TokenType.STAR
|
||||||
|
|
||||||
|
) {
|
||||||
|
this.consumeNumber();
|
||||||
|
} else {
|
||||||
|
this.addToken(TokenType.DOT);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "-":
|
||||||
|
case "+":
|
||||||
|
this.consumeNumber();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "!":
|
||||||
|
this.addToken(
|
||||||
|
this.match("=") ? TokenType.BANG_EQUAL : TokenType.BANG
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "=":
|
||||||
|
if (!this.match("=")) {
|
||||||
|
// Illegal; continue reading until we hit a boundary character and return an error
|
||||||
|
this.consumeIdentifier();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.addToken(TokenType.EQUAL_EQUAL);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "<":
|
||||||
|
this.addToken(
|
||||||
|
this.match("=") ? TokenType.LESS_EQUAL : TokenType.LESS
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ">":
|
||||||
|
this.addToken(
|
||||||
|
this.match("=") ? TokenType.GREATER_EQUAL : TokenType.GREATER
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "&":
|
||||||
|
if (!this.match("&")) {
|
||||||
|
// Illegal; continue reading until we hit a boundary character and return an error
|
||||||
|
this.consumeIdentifier();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.addToken(TokenType.AND);
|
||||||
|
break;
|
||||||
|
case "|":
|
||||||
|
if (!this.match("|")) {
|
||||||
|
// Illegal; continue reading until we hit a boundary character and return an error
|
||||||
|
this.consumeIdentifier();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.addToken(TokenType.OR);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "*":
|
||||||
|
this.addToken(TokenType.STAR);
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Ignore whitespace.
|
||||||
|
case " ":
|
||||||
|
case "\r":
|
||||||
|
case "\t":
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "\n":
|
||||||
|
++this.line;
|
||||||
|
this.lastLineOffset = this.offset;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "'":
|
||||||
|
this.consumeString();
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
switch (true) {
|
||||||
|
case isDigit(c):
|
||||||
|
this.consumeNumber();
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
this.consumeIdentifier();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tokens.push({
|
||||||
|
type: TokenType.EOF,
|
||||||
|
lexeme: "",
|
||||||
|
pos: this.pos(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
tokens: this.tokens,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private pos(): Pos {
|
||||||
|
return {
|
||||||
|
line: this.line,
|
||||||
|
column: this.start - this.lastLineOffset,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private atEnd(): boolean {
|
||||||
|
return this.offset >= this.input.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
private peek(): string {
|
||||||
|
if (this.atEnd()) {
|
||||||
|
return "\0";
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.input[this.offset];
|
||||||
|
}
|
||||||
|
|
||||||
|
private peekNext(): string {
|
||||||
|
if (this.offset + 1 >= this.input.length) {
|
||||||
|
return "\0";
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.input[this.offset + 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
private previous(): TokenType {
|
||||||
|
const l = this.tokens.length;
|
||||||
|
if (l == 0) {
|
||||||
|
return TokenType.EOF;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.tokens[l - 1].type;
|
||||||
|
}
|
||||||
|
|
||||||
|
private next(): string {
|
||||||
|
return this.input[this.offset++];
|
||||||
|
}
|
||||||
|
|
||||||
|
private reverse(): string {
|
||||||
|
return this.input[--this.offset];
|
||||||
|
}
|
||||||
|
|
||||||
|
private match(expected: string): boolean {
|
||||||
|
if (this.atEnd()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this.input[this.offset] !== expected) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.offset++;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private addToken(type: TokenType, value?: string | number | boolean) {
|
||||||
|
this.tokens.push({
|
||||||
|
type,
|
||||||
|
lexeme: this.input.substring(this.start, this.offset),
|
||||||
|
pos: this.pos(),
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private consumeNumber() {
|
||||||
|
while (!this.atEnd() && (!isBoundary(this.peek()) || this.peek() == ".")) {
|
||||||
|
this.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const lexeme = this.input.substring(this.start, this.offset);
|
||||||
|
const value = new StringData(lexeme).number();
|
||||||
|
|
||||||
|
if (isNaN(value)) {
|
||||||
|
throw new Error(
|
||||||
|
`Unexpected symbol: '${lexeme}'. Located at position ${
|
||||||
|
this.start + 1
|
||||||
|
} within expression: ${this.input}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.addToken(TokenType.NUMBER, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private consumeString() {
|
||||||
|
while ((this.peek() !== "'" || this.peekNext() === "'") && !this.atEnd()) {
|
||||||
|
if (this.peek() === "\n") this.line++;
|
||||||
|
if (this.peek() === "'" && this.peekNext() === "'") {
|
||||||
|
// Escaped "'", consume
|
||||||
|
this.next();
|
||||||
|
}
|
||||||
|
this.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Closing '
|
||||||
|
this.next();
|
||||||
|
|
||||||
|
// Trim the surrounding quotes.
|
||||||
|
let value = this.input.substring(this.start + 1, this.offset - 1);
|
||||||
|
value = value.replace("''", "'");
|
||||||
|
|
||||||
|
this.addToken(TokenType.STRING, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private consumeIdentifier() {
|
||||||
|
while (!this.atEnd() && !isBoundary(this.peek())) {
|
||||||
|
this.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
let tokenType = TokenType.IDENTIFIER;
|
||||||
|
let tokenValue = undefined;
|
||||||
|
|
||||||
|
const lexeme = this.input.substring(this.start, this.offset);
|
||||||
|
|
||||||
|
if (this.previous() != TokenType.DOT) {
|
||||||
|
switch (lexeme) {
|
||||||
|
case "true":
|
||||||
|
tokenType = TokenType.TRUE;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "false":
|
||||||
|
tokenType = TokenType.FALSE;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "null":
|
||||||
|
tokenType = TokenType.NULL;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "NaN":
|
||||||
|
tokenType = TokenType.NUMBER;
|
||||||
|
tokenValue = NaN;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "Infinity":
|
||||||
|
tokenType = TokenType.NUMBER;
|
||||||
|
tokenValue = Infinity;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLegalIdentifier(lexeme)) {
|
||||||
|
throw new Error(
|
||||||
|
`Unexpected symbol: '${lexeme}'. Located at position ${
|
||||||
|
this.start + 1
|
||||||
|
} within expression: ${this.input}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.addToken(tokenType, tokenValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDigit(c: string) {
|
||||||
|
return c >= "0" && c <= "9";
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBoundary(c: string): boolean {
|
||||||
|
switch (c) {
|
||||||
|
case "(":
|
||||||
|
case "[":
|
||||||
|
case ")":
|
||||||
|
case "]":
|
||||||
|
case ",":
|
||||||
|
case ".":
|
||||||
|
case "!":
|
||||||
|
case ">":
|
||||||
|
case "<":
|
||||||
|
case "=":
|
||||||
|
case "&":
|
||||||
|
case "|":
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return /\s/.test(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLegalIdentifier(str: string): boolean {
|
||||||
|
if (str == "") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const first = str[0];
|
||||||
|
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 == "-"
|
||||||
|
) {
|
||||||
|
// OK
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,397 @@
|
|||||||
|
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";
|
||||||
|
|
||||||
|
export class Parser {
|
||||||
|
private extContexts: Map<string, boolean>;
|
||||||
|
private extFuncs: Map<string, FunctionInfo>;
|
||||||
|
|
||||||
|
private offset: number = 0;
|
||||||
|
private depth: number = 0;
|
||||||
|
|
||||||
|
private context: ParseContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new parser for the given tokens
|
||||||
|
*
|
||||||
|
* @param tokens Tokens to build a parse tree from
|
||||||
|
* @param extensionContexts Available context names
|
||||||
|
* @param extensionFunctions Available functions (beyond the built-in ones)
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
private tokens: Token[],
|
||||||
|
extensionContexts: string[],
|
||||||
|
extensionFunctions: FunctionInfo[]
|
||||||
|
) {
|
||||||
|
this.extContexts = new Map<string, boolean>();
|
||||||
|
this.extFuncs = new Map();
|
||||||
|
|
||||||
|
for (const contextName of extensionContexts) {
|
||||||
|
this.extContexts.set(contextName.toLowerCase(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const { name, func } of extensionFunctions.map((x) => ({
|
||||||
|
name: x.name,
|
||||||
|
func: x,
|
||||||
|
}))) {
|
||||||
|
this.extFuncs.set(name.toLowerCase(), func);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.context = {
|
||||||
|
allowUnknownKeywords: false,
|
||||||
|
extensionContexts: this.extContexts,
|
||||||
|
extensionFunctions: this.extFuncs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public parse(): Expr {
|
||||||
|
let result!: Expr;
|
||||||
|
|
||||||
|
// No tokens
|
||||||
|
if (this.atEnd()) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = this.expression();
|
||||||
|
|
||||||
|
if (!this.atEnd()) {
|
||||||
|
throw this.buildError(ErrorType.ErrorUnexpectedSymbol, this.peek());
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private expression(): Expr {
|
||||||
|
this.incrDepth();
|
||||||
|
|
||||||
|
try {
|
||||||
|
return this.logicalOr();
|
||||||
|
} finally {
|
||||||
|
this.decrDepth();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private logicalOr(): Expr {
|
||||||
|
// && is higher precedence than ||
|
||||||
|
let expr = this.logicalAnd();
|
||||||
|
|
||||||
|
if (this.check(TokenType.OR)) {
|
||||||
|
// Track depth
|
||||||
|
this.incrDepth();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const logical = new Logical(this.peek(), [expr]);
|
||||||
|
expr = logical;
|
||||||
|
|
||||||
|
while (this.match(TokenType.OR)) {
|
||||||
|
const right = this.logicalAnd();
|
||||||
|
logical.args.push(right);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.decrDepth();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
private logicalAnd(): Expr {
|
||||||
|
// == and != are higher precedence than &&
|
||||||
|
let expr = this.equality();
|
||||||
|
|
||||||
|
if (this.check(TokenType.AND)) {
|
||||||
|
// Track depth
|
||||||
|
this.incrDepth();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const logical = new Logical(this.peek(), [expr]);
|
||||||
|
expr = logical;
|
||||||
|
|
||||||
|
while (this.match(TokenType.AND)) {
|
||||||
|
const right = this.equality();
|
||||||
|
logical.args.push(right);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.decrDepth();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
private equality(): Expr {
|
||||||
|
// >, >=, <, <= are higher precedence than == and !=
|
||||||
|
let expr = this.comparison();
|
||||||
|
|
||||||
|
while (this.match(TokenType.BANG_EQUAL, TokenType.EQUAL_EQUAL)) {
|
||||||
|
const operator = this.previous();
|
||||||
|
const right = this.comparison();
|
||||||
|
|
||||||
|
expr = new Binary(expr, operator, right);
|
||||||
|
}
|
||||||
|
|
||||||
|
return expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
private comparison(): Expr {
|
||||||
|
// ! is higher precedence than >, >=, <, <=
|
||||||
|
let expr = this.unary();
|
||||||
|
|
||||||
|
while (
|
||||||
|
this.match(
|
||||||
|
TokenType.GREATER,
|
||||||
|
TokenType.GREATER_EQUAL,
|
||||||
|
TokenType.LESS,
|
||||||
|
TokenType.LESS_EQUAL
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
const operator = this.previous();
|
||||||
|
const right = this.unary();
|
||||||
|
|
||||||
|
expr = new Binary(expr, operator, right);
|
||||||
|
}
|
||||||
|
|
||||||
|
return expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unary(): Expr {
|
||||||
|
if (this.match(TokenType.BANG)) {
|
||||||
|
// Track depth
|
||||||
|
this.incrDepth();
|
||||||
|
|
||||||
|
const operator = this.previous();
|
||||||
|
const unary = this.unary();
|
||||||
|
|
||||||
|
try {
|
||||||
|
return new Unary(operator, unary);
|
||||||
|
} finally {
|
||||||
|
this.decrDepth();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.index();
|
||||||
|
}
|
||||||
|
|
||||||
|
private index(): Expr {
|
||||||
|
let expr = this.call();
|
||||||
|
|
||||||
|
let depthIncreased = 0;
|
||||||
|
|
||||||
|
if (
|
||||||
|
expr instanceof Grouping ||
|
||||||
|
expr instanceof FunctionCall ||
|
||||||
|
expr instanceof ContextAccess
|
||||||
|
) {
|
||||||
|
let cont = true;
|
||||||
|
while (cont) {
|
||||||
|
switch (true) {
|
||||||
|
case this.match(TokenType.LEFT_BRACKET):
|
||||||
|
let indexExpr: Expr;
|
||||||
|
if (this.match(TokenType.STAR)) {
|
||||||
|
indexExpr = new Star();
|
||||||
|
} else {
|
||||||
|
indexExpr = this.expression();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.consume(
|
||||||
|
TokenType.RIGHT_BRACKET,
|
||||||
|
ErrorType.ErrorUnexpectedSymbol
|
||||||
|
);
|
||||||
|
|
||||||
|
// Track depth
|
||||||
|
this.incrDepth();
|
||||||
|
depthIncreased++;
|
||||||
|
expr = new IndexAccess(expr, indexExpr);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case this.match(TokenType.DOT):
|
||||||
|
// Track depth
|
||||||
|
this.incrDepth();
|
||||||
|
depthIncreased++;
|
||||||
|
|
||||||
|
if (this.match(TokenType.IDENTIFIER)) {
|
||||||
|
let property = this.previous();
|
||||||
|
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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
cont = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < depthIncreased; i++) {
|
||||||
|
this.decrDepth();
|
||||||
|
}
|
||||||
|
|
||||||
|
return expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
private call(): Expr {
|
||||||
|
if (!this.check(TokenType.IDENTIFIER)) {
|
||||||
|
return this.primary();
|
||||||
|
}
|
||||||
|
|
||||||
|
const identifier = this.next();
|
||||||
|
|
||||||
|
if (!this.match(TokenType.LEFT_PAREN)) {
|
||||||
|
if (!this.extContexts.has(identifier.lexeme.toLowerCase())) {
|
||||||
|
throw this.buildError(ErrorType.ErrorUnrecognizedContext, identifier);
|
||||||
|
}
|
||||||
|
return new ContextAccess(identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function call
|
||||||
|
const args: Expr[] = [];
|
||||||
|
|
||||||
|
// Arguments
|
||||||
|
while (!this.match(TokenType.RIGHT_PAREN)) {
|
||||||
|
const aexp = this.expression();
|
||||||
|
|
||||||
|
args.push(aexp);
|
||||||
|
|
||||||
|
if (!this.check(TokenType.RIGHT_PAREN)) {
|
||||||
|
this.consume(TokenType.COMMA, ErrorType.ErrorUnexpectedSymbol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validateFunction(this.context, identifier, args.length);
|
||||||
|
|
||||||
|
return new FunctionCall(identifier, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
private primary(): Expr {
|
||||||
|
switch (true) {
|
||||||
|
case this.match(TokenType.FALSE):
|
||||||
|
return new Literal(new data.BooleanData(false));
|
||||||
|
|
||||||
|
case this.match(TokenType.TRUE):
|
||||||
|
return new Literal(new data.BooleanData(true));
|
||||||
|
|
||||||
|
case this.match(TokenType.NULL):
|
||||||
|
return new Literal(new data.Null());
|
||||||
|
|
||||||
|
case this.match(TokenType.NUMBER):
|
||||||
|
return new Literal(new data.NumberData(this.previous().value as number));
|
||||||
|
|
||||||
|
case this.match(TokenType.STRING):
|
||||||
|
return new Literal(new data.StringData(this.previous().value as string));
|
||||||
|
|
||||||
|
case this.match(TokenType.LEFT_PAREN):
|
||||||
|
const expr = this.expression();
|
||||||
|
|
||||||
|
if (this.atEnd()) {
|
||||||
|
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.ErrorUnexpectedSymbol, this.peek());
|
||||||
|
}
|
||||||
|
|
||||||
|
// match consumes the next token if it matches any of the given types
|
||||||
|
private match(...tokenTypes: TokenType[]): boolean {
|
||||||
|
for (const tokenType of tokenTypes) {
|
||||||
|
if (this.check(tokenType)) {
|
||||||
|
this.next();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check peeks whether the next token is of the given type
|
||||||
|
private check(tokenType: TokenType): boolean {
|
||||||
|
if (this.atEnd()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.peek().type == tokenType;
|
||||||
|
}
|
||||||
|
|
||||||
|
// atEnd peeks whether the next token is EOF
|
||||||
|
private atEnd(): boolean {
|
||||||
|
return this.peek().type == TokenType.EOF;
|
||||||
|
}
|
||||||
|
|
||||||
|
private next(): Token {
|
||||||
|
if (!this.atEnd()) {
|
||||||
|
this.offset++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.previous();
|
||||||
|
}
|
||||||
|
|
||||||
|
private peek(): Token {
|
||||||
|
return this.tokens[this.offset];
|
||||||
|
}
|
||||||
|
|
||||||
|
// previous returns the previous token
|
||||||
|
private previous(): Token {
|
||||||
|
return this.tokens[this.offset - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// consume attempts to consume the next token if it matches the given type. It returns an error of
|
||||||
|
// the given ParseErrorKind otherwise.
|
||||||
|
private consume(tokenType: TokenType, errorType: ErrorType) {
|
||||||
|
if (this.check(tokenType)) {
|
||||||
|
this.next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw this.buildError(errorType, this.peek());
|
||||||
|
}
|
||||||
|
|
||||||
|
private incrDepth() {
|
||||||
|
this.depth++;
|
||||||
|
if (this.depth > MAX_PARSER_DEPTH) {
|
||||||
|
throw this.buildError(ErrorType.ErrorExceededMaxDepth, this.peek());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private decrDepth() {
|
||||||
|
this.depth--;
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildError(errType: ErrorType, token: Token): Error {
|
||||||
|
return new ExpressionError(errType, token);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { BooleanData, ExpressionData, NumberData, StringData } from "./data";
|
||||||
|
import { coerceTypes, toUpperSpecial } from "./result";
|
||||||
|
|
||||||
|
describe("coerceTypes", () => {
|
||||||
|
const tests: {
|
||||||
|
name: string;
|
||||||
|
data: {
|
||||||
|
l: ExpressionData;
|
||||||
|
r: ExpressionData;
|
||||||
|
};
|
||||||
|
wantL: ExpressionData;
|
||||||
|
wantR: ExpressionData;
|
||||||
|
}[] = [
|
||||||
|
{
|
||||||
|
name: "number-bool",
|
||||||
|
data: { l: new NumberData(1), r: new BooleanData(true) },
|
||||||
|
wantL: new NumberData(1),
|
||||||
|
wantR: new NumberData(1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "number-bool-false",
|
||||||
|
data: { l: new NumberData(1), r: new BooleanData(false) },
|
||||||
|
wantL: new NumberData(1),
|
||||||
|
wantR: new NumberData(0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bool-number-false",
|
||||||
|
data: { l: new BooleanData(false), r: new NumberData(1) },
|
||||||
|
wantL: new NumberData(0),
|
||||||
|
wantR: new NumberData(1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "number-number",
|
||||||
|
data: { l: new NumberData(1), r: new NumberData(2) },
|
||||||
|
wantL: new NumberData(1),
|
||||||
|
wantR: new NumberData(2),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "string-string",
|
||||||
|
data: { l: new StringData("a"), r: new StringData("b") },
|
||||||
|
wantL: new StringData("a"),
|
||||||
|
wantR: new StringData("b"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "string-number",
|
||||||
|
data: { l: new StringData("a"), r: new NumberData(1) },
|
||||||
|
wantL: new NumberData(NaN),
|
||||||
|
wantR: new NumberData(1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "number-string",
|
||||||
|
data: { l: new NumberData(1), r: new StringData("a") },
|
||||||
|
wantL: new NumberData(1),
|
||||||
|
wantR: new NumberData(NaN),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bool-bool",
|
||||||
|
data: { l: new BooleanData(false), r: new BooleanData(true) },
|
||||||
|
wantL: new BooleanData(false),
|
||||||
|
wantR: new BooleanData(true),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
test.each(tests)(
|
||||||
|
"$name",
|
||||||
|
({
|
||||||
|
data,
|
||||||
|
wantL,
|
||||||
|
wantR,
|
||||||
|
}: {
|
||||||
|
data: { l: ExpressionData; r: ExpressionData };
|
||||||
|
wantL: ExpressionData;
|
||||||
|
wantR: ExpressionData;
|
||||||
|
}) => {
|
||||||
|
const [gotL, gotR] = coerceTypes(data.l, data.r);
|
||||||
|
expect(gotL).toEqual(wantL);
|
||||||
|
expect(gotR).toEqual(wantR);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
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" },
|
||||||
|
];
|
||||||
|
|
||||||
|
test.each(tests)(
|
||||||
|
"$input",
|
||||||
|
({ input, want }: { input: string; want: string }) => {
|
||||||
|
expect(toUpperSpecial(input)).toEqual(want);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
import * as data from "./data";
|
||||||
|
|
||||||
|
export function falsy(d: data.ExpressionData): boolean {
|
||||||
|
switch (d.kind) {
|
||||||
|
case data.Kind.Null:
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case data.Kind.Boolean:
|
||||||
|
return !d.value;
|
||||||
|
|
||||||
|
case data.Kind.Number:
|
||||||
|
const dv = d.value;
|
||||||
|
return dv === 0 || isNaN(dv);
|
||||||
|
|
||||||
|
case data.Kind.String:
|
||||||
|
return d.value.length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function truthy(d: data.ExpressionData): boolean {
|
||||||
|
return !falsy(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Similar to the Javascript abstract equality comparison algorithm http://www.ecma-international.org/ecma-262/5.1/#sec-11.9.3.
|
||||||
|
// Except objects are not coerced to primitives.
|
||||||
|
export function coerceTypes(
|
||||||
|
li: data.ExpressionData,
|
||||||
|
ri: data.ExpressionData
|
||||||
|
): [data.ExpressionData, data.ExpressionData] {
|
||||||
|
let lv = li;
|
||||||
|
let rv = ri;
|
||||||
|
|
||||||
|
// Do nothing, same kind
|
||||||
|
if (li.kind === ri.kind) {
|
||||||
|
return [lv, rv];
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (li.kind) {
|
||||||
|
// Number, String
|
||||||
|
case data.Kind.Number:
|
||||||
|
if (ri.kind === data.Kind.String) {
|
||||||
|
rv = new data.NumberData(ri.number());
|
||||||
|
return [lv, rv];
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
// String, Number
|
||||||
|
case data.Kind.String:
|
||||||
|
if (ri.kind === data.Kind.Number) {
|
||||||
|
lv = new data.NumberData(li.number());
|
||||||
|
return [lv, rv];
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Boolean|Null, Any
|
||||||
|
case data.Kind.Null:
|
||||||
|
case data.Kind.Boolean:
|
||||||
|
lv = new data.NumberData(li.number());
|
||||||
|
return coerceTypes(lv, rv);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any, Boolean|Null
|
||||||
|
switch (ri.kind) {
|
||||||
|
case data.Kind.Null:
|
||||||
|
case data.Kind.Boolean:
|
||||||
|
rv = new data.NumberData(ri.number());
|
||||||
|
return coerceTypes(lv, rv);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [lv, rv];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
let [lv, rv] = coerceTypes(lhs, rhs);
|
||||||
|
|
||||||
|
if (lv.kind != rv.kind) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (lv.kind) {
|
||||||
|
// Null, Null
|
||||||
|
case data.Kind.Null:
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// Number, Number
|
||||||
|
case data.Kind.Number:
|
||||||
|
const ld = lv.value;
|
||||||
|
const rd = (rv as data.NumberData).value;
|
||||||
|
if (isNaN(ld) || isNaN(rd)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ld == rd;
|
||||||
|
|
||||||
|
// String, String
|
||||||
|
case data.Kind.String:
|
||||||
|
const ls = lv.value;
|
||||||
|
const rs = (rv as data.StringData).value;
|
||||||
|
return toUpperSpecial(ls) === toUpperSpecial(rs);
|
||||||
|
|
||||||
|
// Boolean, Boolean
|
||||||
|
case data.Kind.Boolean:
|
||||||
|
const lb = lv.value;
|
||||||
|
const rb = (rv as data.BooleanData).value;
|
||||||
|
return lb == rb;
|
||||||
|
|
||||||
|
// Object, Object
|
||||||
|
case data.Kind.Dictionary:
|
||||||
|
case data.Kind.Array:
|
||||||
|
// Check reference equality
|
||||||
|
return lv === rv;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
const [lv, rv] = coerceTypes(lhs, rhs);
|
||||||
|
|
||||||
|
if (lv.kind != rv.kind) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (lv.kind) {
|
||||||
|
// Number, Number
|
||||||
|
case data.Kind.Number:
|
||||||
|
const lf = lv.value;
|
||||||
|
const rf = (rv as data.NumberData).value;
|
||||||
|
if (isNaN(lf) || isNaN(rf)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return lf > rf;
|
||||||
|
|
||||||
|
// String, String
|
||||||
|
case data.Kind.String:
|
||||||
|
let ls = lv.value;
|
||||||
|
let rs = (rv as data.StringData).value;
|
||||||
|
ls = toUpperSpecial(ls);
|
||||||
|
rs = toUpperSpecial(rs);
|
||||||
|
return ls > rs;
|
||||||
|
|
||||||
|
// Boolean, Boolean
|
||||||
|
case data.Kind.Boolean:
|
||||||
|
const lb = (lv as data.BooleanData).value;
|
||||||
|
const rb = (rv as data.BooleanData).value;
|
||||||
|
return lb && !rb;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
const [lv, rv] = coerceTypes(lhs, rhs);
|
||||||
|
|
||||||
|
if (lv.kind != rv.kind) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (lv.kind) {
|
||||||
|
// Number, Number
|
||||||
|
case data.Kind.Number:
|
||||||
|
const lf = lv.value;
|
||||||
|
const rf = (rv as data.NumberData).value;
|
||||||
|
if (isNaN(lf) || isNaN(rf)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return lf < rf;
|
||||||
|
|
||||||
|
// String, String
|
||||||
|
case data.Kind.String:
|
||||||
|
let ls = lv.value;
|
||||||
|
let rs = (rv as data.StringData).value;
|
||||||
|
ls = toUpperSpecial(ls);
|
||||||
|
rs = toUpperSpecial(rs);
|
||||||
|
return ls < rs;
|
||||||
|
|
||||||
|
// Boolean, Boolean
|
||||||
|
case data.Kind.Boolean:
|
||||||
|
const lb = lv.value;
|
||||||
|
const rb = (rv as data.BooleanData).value;
|
||||||
|
return !lb && rb;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not toUpper the small-dotless-ı
|
||||||
|
export function toUpperSpecial(s: string): string {
|
||||||
|
const sb: string[] = [];
|
||||||
|
let i = 0;
|
||||||
|
let len = s.length;
|
||||||
|
let found = s.indexOf("ı");
|
||||||
|
while (i < len) {
|
||||||
|
if (i < found) {
|
||||||
|
sb.push(s.substring(i, found).toUpperCase()); // Append upper segment
|
||||||
|
i = found;
|
||||||
|
} else if (i == found) {
|
||||||
|
sb.push(s.substring(i, i + 1));
|
||||||
|
i += 1;
|
||||||
|
found = s.indexOf("ı", i);
|
||||||
|
} else {
|
||||||
|
sb.push(s.substring(i).toUpperCase()); // Append upper remaining
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.join("");
|
||||||
|
}
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
import * as fs from "fs";
|
||||||
|
import * as path from "path";
|
||||||
|
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";
|
||||||
|
|
||||||
|
interface TestResult {
|
||||||
|
value: data.ExpressionData;
|
||||||
|
kind: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TestErrorKind {
|
||||||
|
Lexing = "lexing",
|
||||||
|
Parsing = "parsing",
|
||||||
|
Evaluation = "evaluation",
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SkipOptionKind {
|
||||||
|
Typescript = "typescript",
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TestOptions {
|
||||||
|
skip: SkipOptionKind[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TestError {
|
||||||
|
value: string;
|
||||||
|
kind: TestErrorKind;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TestCase {
|
||||||
|
expr: string;
|
||||||
|
result: TestResult;
|
||||||
|
err: TestError | undefined;
|
||||||
|
contexts: data.Dictionary;
|
||||||
|
options: TestOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
function testCaseReviver(key: string, val: any): any {
|
||||||
|
if (key === "contexts") {
|
||||||
|
const tmp = JSON.stringify(val);
|
||||||
|
return JSON.parse(tmp, reviver);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "result") {
|
||||||
|
const t = val as TestResult;
|
||||||
|
t.value = reviver("value", t.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
const testFiles = "./testData";
|
||||||
|
|
||||||
|
describe("x-lang tests", () => {
|
||||||
|
const files = fs.readdirSync(testFiles);
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const fileName = path.join(testFiles, file);
|
||||||
|
|
||||||
|
const fileStat = fs.statSync(fileName);
|
||||||
|
if (fileStat.isDirectory()) {
|
||||||
|
throw new Error("sub-directories are not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path.extname(fileName) !== ".json") {
|
||||||
|
throw new Error("only json files are supported " + file);
|
||||||
|
}
|
||||||
|
|
||||||
|
const testFile = fs.readFileSync(fileName, "utf8");
|
||||||
|
|
||||||
|
const testCases = JSON.parse(testFile, testCaseReviver) as {
|
||||||
|
[name: string]: TestCase[];
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const testCaseName of Object.keys(testCases)) {
|
||||||
|
let tests = testCases[testCaseName];
|
||||||
|
|
||||||
|
// Filter out tests that are not supported by typescript
|
||||||
|
tests = tests.filter(
|
||||||
|
(t) => !t.options?.skip?.includes(SkipOptionKind.Typescript)
|
||||||
|
);
|
||||||
|
|
||||||
|
const testName = path.basename(file, ".json");
|
||||||
|
|
||||||
|
if (tests.length === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe(testName, () => {
|
||||||
|
test.each(tests)(`${testName}: ${testCaseName} ($expr)`, (testCase) => {
|
||||||
|
if (!testCase.contexts) {
|
||||||
|
testCase.contexts = new data.Dictionary();
|
||||||
|
}
|
||||||
|
|
||||||
|
const lexer = new Lexer(testCase.expr);
|
||||||
|
let result: Result;
|
||||||
|
try {
|
||||||
|
result = lexer.lex();
|
||||||
|
|
||||||
|
if (testCase.err && testCase.err.kind === TestErrorKind.Lexing) {
|
||||||
|
throw new Error("expected error lexing expression, but got none");
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
// Did test expect lexing error? If so, compare error message.
|
||||||
|
if (testCase.err && testCase.err.kind === TestErrorKind.Lexing) {
|
||||||
|
expect(e.message).toContain(testCase.err.value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`unexpected error lexing expression: ${e.message} ${e.stack}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse
|
||||||
|
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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} 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
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`unexpected error parsing expression: ${e.message} ${e.stack}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evaluate expression
|
||||||
|
try {
|
||||||
|
let result: data.ExpressionData;
|
||||||
|
if (expr !== undefined) {
|
||||||
|
const evaluator = new Evaluator(expr, testCase.contexts);
|
||||||
|
result = evaluator.evaluate();
|
||||||
|
} else {
|
||||||
|
result = new data.Null();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (testCase.err?.kind === TestErrorKind.Evaluation) {
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
} catch (e: any) {
|
||||||
|
if (testCase.err?.kind === TestErrorKind.Evaluation) {
|
||||||
|
const pe = e as ExpressionError;
|
||||||
|
expect(errorWithExpression(pe, testCase.expr)).toContain(
|
||||||
|
testCase.err.value
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`unexpected error evaluating expression: ${e.message} ${e.stack}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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 within expression: ${expr}`;
|
||||||
|
}
|
||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
{
|
||||||
|
"empty_expression": [{
|
||||||
|
"expr": "",
|
||||||
|
"result": {
|
||||||
|
"kind": "Null",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
"equal_simple": [{
|
||||||
|
"expr": "1 == 2",
|
||||||
|
"result": {
|
||||||
|
"kind": "Boolean",
|
||||||
|
"value": false
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
"context_simple_access": [{
|
||||||
|
"expr": "simple",
|
||||||
|
"result": {
|
||||||
|
"kind": "String",
|
||||||
|
"value": "foo"
|
||||||
|
},
|
||||||
|
"contexts": {
|
||||||
|
"simple": "foo"
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
"context_case-insensitive": [{
|
||||||
|
"expr": "SIMple.TEst",
|
||||||
|
"result": {
|
||||||
|
"kind": "Number",
|
||||||
|
"value": 123.0
|
||||||
|
},
|
||||||
|
"contexts": {
|
||||||
|
"simPLE": {
|
||||||
|
"teST": 123
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
"context access with wildcard": [{
|
||||||
|
"expr": "toJson(input.*.foo)",
|
||||||
|
"result": {
|
||||||
|
"kind": "String",
|
||||||
|
"value": "[\n 32,\n 42,\n -10,\n 0,\n 2,\n 17\n]"
|
||||||
|
},
|
||||||
|
"contexts": {
|
||||||
|
"input": {
|
||||||
|
"test": { "foo": 32 },
|
||||||
|
"test2": { "foo": 42 },
|
||||||
|
"test3": { "foo": -10 },
|
||||||
|
"test4": { "foo": 0 },
|
||||||
|
"test5": { "foo": 2 },
|
||||||
|
"test6": { "foo": 17 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
"unknown context": [
|
||||||
|
{
|
||||||
|
"expr": "nosuchcontext.foo",
|
||||||
|
"err": {
|
||||||
|
"kind": "parsing",
|
||||||
|
"value": "Unrecognized named-value: 'nosuchcontext'. Located at position 1 within expression: nosuchcontext.foo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"refer ./operator_not.json": [
|
||||||
|
]
|
||||||
|
}
|
||||||
+124
@@ -0,0 +1,124 @@
|
|||||||
|
{
|
||||||
|
"null": [
|
||||||
|
{
|
||||||
|
"expr": "null == 0",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"boolean": [
|
||||||
|
{
|
||||||
|
"expr": "true == 1",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "false == 0",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"string": [
|
||||||
|
{
|
||||||
|
"expr": "'' == 0",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' 123456.789 ' == 123456.789",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' +123456.789 ' == 123456.789",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' -123456.789 ' == -123456.789",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' 0xff ' == 255",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' 0xFF ' == 255",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' 0o10 ' == 8",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' Infinity ' == Infinity",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' -Infinity ' == -Infinity",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' NaN ' != NaN",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' NaN ' == NaN",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' NaN ' > NaN",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' NaN ' < NaN",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' abc ' != NaN",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' abc ' == NaN",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' abc ' > NaN",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' abc ' < NaN",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"array": [
|
||||||
|
{
|
||||||
|
"expr": "fromjson('[]') != NaN",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "fromjson('[]') == NaN",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "fromjson('[]') > NaN",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "fromjson('[]') < NaN",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"object": [
|
||||||
|
{
|
||||||
|
"expr": "fromjson('[]') != NaN",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "fromjson('[]') == NaN",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "fromjson('[]') > NaN",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "fromjson('[]') < NaN",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+178
@@ -0,0 +1,178 @@
|
|||||||
|
{
|
||||||
|
"null": [
|
||||||
|
{
|
||||||
|
"expr": "format('{0}', null)",
|
||||||
|
"result": { "kind": "String", "value": "" }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"boolean": [
|
||||||
|
{
|
||||||
|
"expr": "format('{0}', true)",
|
||||||
|
"result": { "kind": "String", "value": "true" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{0}', false)",
|
||||||
|
"result": { "kind": "String", "value": "false" }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"number": [
|
||||||
|
{
|
||||||
|
"expr": "format('{0}', 1)",
|
||||||
|
"result": { "kind": "String", "value": "1" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{0}', .5)",
|
||||||
|
"result": { "kind": "String", "value": "0.5" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{0}', 0.5)",
|
||||||
|
"result": { "kind": "String", "value": "0.5" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{0}', 2)",
|
||||||
|
"result": { "kind": "String", "value": "2" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{0}', -1)",
|
||||||
|
"result": { "kind": "String", "value": "-1" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{0}', -.5)",
|
||||||
|
"result": { "kind": "String", "value": "-0.5" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{0}', -0.5)",
|
||||||
|
"result": { "kind": "String", "value": "-0.5" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{0}', -2.0)",
|
||||||
|
"result": { "kind": "String", "value": "-2" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{0}', 0)",
|
||||||
|
"result": { "kind": "String", "value": "0" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{0}', 0.0)",
|
||||||
|
"result": { "kind": "String", "value": "0" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{0}', -0)",
|
||||||
|
"result": { "kind": "String", "value": "0" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{0}', -0.0)",
|
||||||
|
"result": { "kind": "String", "value": "0" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{0}', 123456.789)",
|
||||||
|
"result": { "kind": "String", "value": "123456.789" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{0}', +123456.789)",
|
||||||
|
"result": { "kind": "String", "value": "123456.789" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{0}', -123456.789)",
|
||||||
|
"result": { "kind": "String", "value": "-123456.789" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{0}', 0.84551240822557006)",
|
||||||
|
"result": { "kind": "String", "value": "0.84551240822557" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{0}', 1.9)",
|
||||||
|
"result": { "kind": "String", "value": "1.9" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{0}', 1234.56)",
|
||||||
|
"result": { "kind": "String", "value": "1234.56" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{0}', 0xff)",
|
||||||
|
"result": { "kind": "String", "value": "255" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{0}', 0xFF)",
|
||||||
|
"result": { "kind": "String", "value": "255" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{0}', 0o10)",
|
||||||
|
"result": { "kind": "String", "value": "8" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{0}', 1.2e2)",
|
||||||
|
"result": { "kind": "String", "value": "120" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{0}', 1.2E2)",
|
||||||
|
"result": { "kind": "String", "value": "120" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{0}', 1.2e+2)",
|
||||||
|
"result": { "kind": "String", "value": "120" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{0}', 1.2E+2)",
|
||||||
|
"result": { "kind": "String", "value": "120" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{0}', 1.2e-2)",
|
||||||
|
"result": { "kind": "String", "value": "0.012" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{0}', 1.2E-2)",
|
||||||
|
"result": { "kind": "String", "value": "0.012" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{0}', Infinity)",
|
||||||
|
"result": { "kind": "String", "value": "Infinity" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{0}', -Infinity)",
|
||||||
|
"result": { "kind": "String", "value": "-Infinity" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{0}', NaN)",
|
||||||
|
"result": { "kind": "String", "value": "NaN" }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"string": [
|
||||||
|
{
|
||||||
|
"expr": "format('{0}', 'string value')",
|
||||||
|
"result": { "kind": "String", "value": "string value" }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"array": [
|
||||||
|
{
|
||||||
|
"expr": "format('{0}', context)",
|
||||||
|
"contexts": {
|
||||||
|
"context": []
|
||||||
|
},
|
||||||
|
"result": { "kind": "String", "value": "Array" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{0}', context)",
|
||||||
|
"contexts": {
|
||||||
|
"context": [1, 2, 3]
|
||||||
|
},
|
||||||
|
"result": { "kind": "String", "value": "Array" }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"object": [
|
||||||
|
{
|
||||||
|
"expr": "format('{0}', context)",
|
||||||
|
"contexts": {
|
||||||
|
"context": {}
|
||||||
|
},
|
||||||
|
"result": { "kind": "String", "value": "Object" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{0}', context)",
|
||||||
|
"contexts": {
|
||||||
|
"context": { "a": 1, "b": 2, "c": 3 }
|
||||||
|
},
|
||||||
|
"result": { "kind": "String", "value": "Object" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+122
@@ -0,0 +1,122 @@
|
|||||||
|
{
|
||||||
|
"string": [
|
||||||
|
{
|
||||||
|
"expr": "contains('abcdef', 'cde')",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "contains('abCdEf', 'cDe')",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "contains('abcdef', '')",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "contains('1234', 23)",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "contains('true', true)",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "contains('false', false)",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "contains('asdf', null)",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "contains('true', false)",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"array": [
|
||||||
|
{
|
||||||
|
"expr": "contains(test, 'hello')",
|
||||||
|
"result": { "kind": "Boolean", "value": true },
|
||||||
|
"contexts": { "test": ["hello"] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "contains(test, 'world')",
|
||||||
|
"result": { "kind": "Boolean", "value": true },
|
||||||
|
"contexts": { "test": ["hello", "world"] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "contains(test, 'asdf')",
|
||||||
|
"result": { "kind": "Boolean", "value": false },
|
||||||
|
"contexts": { "test": ["hello"] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "contains(test, 123)",
|
||||||
|
"result": { "kind": "Boolean", "value": true },
|
||||||
|
"contexts": { "test": [123.0] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "contains(test, 123)",
|
||||||
|
"result": { "kind": "Boolean", "value": true },
|
||||||
|
"contexts": { "test": [123.0] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "contains(test, null)",
|
||||||
|
"result": { "kind": "Boolean", "value": true },
|
||||||
|
"contexts": { "test": [""] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "contains(test, false)",
|
||||||
|
"result": { "kind": "Boolean", "value": true },
|
||||||
|
"contexts": { "test": [""] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "contains(test, false)",
|
||||||
|
"result": { "kind": "Boolean", "value": true },
|
||||||
|
"contexts": { "test": [0] }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"filtered_array": [
|
||||||
|
{
|
||||||
|
"expr": "contains(test.*, 'hello')",
|
||||||
|
"result": { "kind": "Boolean", "value": true },
|
||||||
|
"contexts": { "test": ["hello"] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "contains(test.*, 'world')",
|
||||||
|
"result": { "kind": "Boolean", "value": true },
|
||||||
|
"contexts": { "test": ["hello", "world"] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "contains(test.*, 'asdf')",
|
||||||
|
"result": { "kind": "Boolean", "value": false },
|
||||||
|
"contexts": { "test": ["hello"] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "contains(test.*, 123)",
|
||||||
|
"result": { "kind": "Boolean", "value": true },
|
||||||
|
"contexts": { "test": [123.0] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "contains(test.*, 123)",
|
||||||
|
"result": { "kind": "Boolean", "value": true },
|
||||||
|
"contexts": { "test": [123.0] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "contains(test.*, null)",
|
||||||
|
"result": { "kind": "Boolean", "value": true },
|
||||||
|
"contexts": { "test": [""] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "contains(test.*, false)",
|
||||||
|
"result": { "kind": "Boolean", "value": true },
|
||||||
|
"contexts": { "test": [""] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "contains(test.*, false)",
|
||||||
|
"result": { "kind": "Boolean", "value": true },
|
||||||
|
"contexts": { "test": [0] }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+129
@@ -0,0 +1,129 @@
|
|||||||
|
{
|
||||||
|
"basics": [
|
||||||
|
{
|
||||||
|
"expr": "endsWith('abcdef', 'def')",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "endsWith('abcdef', 'de')",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "endsWith('abcdef', '')",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "endsWith('1234', 34)",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "endsWith('true', true)",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "endsWith('false', false)",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "endsWith('asdf', null)",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "endsWith('1234', 23)",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "endsWith('true', false)",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"case-insensitive-a-thru-z": [
|
||||||
|
{
|
||||||
|
"expr": "endsWith('asdf_ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "endsWith('asdf_abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"case-insensitive-ς-(final-lowercase-sigma)-Σ-(capital-sigma)-σ-(non-final-sigma)": [
|
||||||
|
{
|
||||||
|
"expr": "endsWith('asdf_ς', 'Σ')",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "endsWith('asdf_ς', 'σ')",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "endsWith('asdf_Σ', 'ς')",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "endsWith('asdf_Σ', 'σ')",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "endsWith('asdf_σ', 'ς')",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "endsWith('asdf_σ', 'Σ')",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"case-insensitive-ü-Ü": [
|
||||||
|
{
|
||||||
|
"expr": "endsWith('asdf_ü', 'Ü')",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "endsWith('asdf_Ü', 'ü')",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"case-insensitive-ç-Ç": [
|
||||||
|
{
|
||||||
|
"expr": "endsWith('asdf_ç', 'Ç')",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "endsWith('asdf_Ç', 'ç')",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"case-sensitive-i-İ": [
|
||||||
|
{
|
||||||
|
"expr": "endsWith('asdf_i', 'İ')",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "endsWith('asdf_İ', 'i')",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"case-sensitive-ı-I": [
|
||||||
|
{
|
||||||
|
"expr": "endsWith('asdf_ı', 'I')",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "endsWith('asdf_I', 'ı')",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"cyrillic-letters": [
|
||||||
|
{
|
||||||
|
"expr": "endsWith('asdf_АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЬЭЮЯ', 'абвгдежзийклмнопрстуфхцчшщьэюя')",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+207
@@ -0,0 +1,207 @@
|
|||||||
|
{
|
||||||
|
"format": [
|
||||||
|
{
|
||||||
|
"expr": "format(null)",
|
||||||
|
"result": { "kind": "String", "value": "" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format(null, 'some arg')",
|
||||||
|
"result": { "kind": "String", "value": "" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('')",
|
||||||
|
"result": { "kind": "String", "value": "" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('', 'some arg')",
|
||||||
|
"result": { "kind": "String", "value": "" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('123{0}456', 'abc')",
|
||||||
|
"result": { "kind": "String", "value": "123abc456" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('123{0}456{0}789', 'abc')",
|
||||||
|
"result": { "kind": "String", "value": "123abc456abc789" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('123{0}456{1}789', 'abc', 'def')",
|
||||||
|
"result": { "kind": "String", "value": "123abc456def789" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{0}123', 'abc')",
|
||||||
|
"result": { "kind": "String", "value": "abc123" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('123{0}', 'abc')",
|
||||||
|
"result": { "kind": "String", "value": "123abc" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('123{0}{1}456', 'abc', 'def')",
|
||||||
|
"result": { "kind": "String", "value": "123abcdef456" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{{0}}', 'abc')",
|
||||||
|
"result": { "kind": "String", "value": "{0}" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{{{{0}}}}', 'abc')",
|
||||||
|
"result": { "kind": "String", "value": "{{0}}" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('}}', 'abc')",
|
||||||
|
"result": { "kind": "String", "value": "}" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{{', 'abc')",
|
||||||
|
"result": { "kind": "String", "value": "{" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('}}{{', 'abc')",
|
||||||
|
"result": { "kind": "String", "value": "}{" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('}}{{}}', 'abc')",
|
||||||
|
"result": { "kind": "String", "value": "}{}" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{0}', Infinity)",
|
||||||
|
"result": { "kind": "String", "value": "Infinity" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{0}', -Infinity)",
|
||||||
|
"result": { "kind": "String", "value": "-Infinity" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{0}', NaN)",
|
||||||
|
"result": { "kind": "String", "value": "NaN" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{0}', -0)",
|
||||||
|
"result": { "kind": "String", "value": "0" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{0}', -0.0)",
|
||||||
|
"result": { "kind": "String", "value": "0" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{0')",
|
||||||
|
"err": {
|
||||||
|
"kind": "evaluation",
|
||||||
|
"value": "The following format string is invalid: {0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{0', '')",
|
||||||
|
"err": {
|
||||||
|
"kind": "evaluation",
|
||||||
|
"value": "The following format string is invalid: {0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{0}}', '')",
|
||||||
|
"err": {
|
||||||
|
"kind": "evaluation",
|
||||||
|
"value": "The following format string is invalid: {0}}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{0}}}}', '')",
|
||||||
|
"err": {
|
||||||
|
"kind": "evaluation",
|
||||||
|
"value": "The following format string is invalid: {0}}}}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('0}')",
|
||||||
|
"err": {
|
||||||
|
"kind": "evaluation",
|
||||||
|
"value": "The following format string is invalid: 0}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('0}', '')",
|
||||||
|
"err": {
|
||||||
|
"kind": "evaluation",
|
||||||
|
"value": "The following format string is invalid: 0}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{{0}')",
|
||||||
|
"err": {
|
||||||
|
"kind": "evaluation",
|
||||||
|
"value": "The following format string is invalid: {{0}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{{0}', '')",
|
||||||
|
"err": {
|
||||||
|
"kind": "evaluation",
|
||||||
|
"value": "The following format string is invalid: {{0}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{{{{0}')",
|
||||||
|
"err": {
|
||||||
|
"kind": "evaluation",
|
||||||
|
"value": "The following format string is invalid: {{{{0}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{{{{0}', '')",
|
||||||
|
"err": {
|
||||||
|
"kind": "evaluation",
|
||||||
|
"value": "The following format string is invalid: {{{{0}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('}0{')",
|
||||||
|
"err": {
|
||||||
|
"kind": "evaluation",
|
||||||
|
"value": "The following format string is invalid: }0{"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('}0{', '')",
|
||||||
|
"err": {
|
||||||
|
"kind": "evaluation",
|
||||||
|
"value": "The following format string is invalid: }0{"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('}{0}')",
|
||||||
|
"err": {
|
||||||
|
"kind": "evaluation",
|
||||||
|
"value": "The following format string is invalid: }{0}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('}{0}', '')",
|
||||||
|
"err": {
|
||||||
|
"kind": "evaluation",
|
||||||
|
"value": "The following format string is invalid: }{0}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{0}{', '')",
|
||||||
|
"err": {
|
||||||
|
"kind": "evaluation",
|
||||||
|
"value": "The following format string is invalid: {0}{"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{0}')",
|
||||||
|
"err": {
|
||||||
|
"kind": "evaluation",
|
||||||
|
"value": "The following format string references more arguments than were supplied: {0}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('{0}{1}', 'abc')",
|
||||||
|
"err": {
|
||||||
|
"kind": "evaluation",
|
||||||
|
"value": "The following format string references more arguments than were supplied: {0}{1}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+67
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
"basics": [
|
||||||
|
{ "expr": "fromJSON('null')", "result": { "kind": "Null", "value": null } },
|
||||||
|
{ "expr": "fromJSON('true')", "result": { "kind": "Boolean", "value": true } },
|
||||||
|
{ "expr": "fromJSON('false')", "result": { "kind": "Boolean", "value": false } },
|
||||||
|
{ "expr": "fromJSON('0')", "result": { "kind": "Number", "value": 0 } },
|
||||||
|
{ "expr": "fromJSON('-0')", "result": { "kind": "Number", "value": -0 } },
|
||||||
|
{ "expr": "fromJSON('123456789')", "result": { "kind": "Number", "value": 123456789 } },
|
||||||
|
{ "expr": "fromJSON('-123456789')", "result": { "kind": "Number", "value": -123456789 } },
|
||||||
|
{ "expr": "fromJSON('1234.5')", "result": { "kind": "Number", "value": 1234.5 } },
|
||||||
|
{ "expr": "fromJSON('-1234.5')", "result": { "kind": "Number", "value": -1234.5 } },
|
||||||
|
{ "expr": "fromJSON('\"\"')", "result": { "kind": "String", "value": "" } },
|
||||||
|
{ "expr": "fromJSON('\"abc\"')", "result": { "kind": "String", "value": "abc" } },
|
||||||
|
{ "expr": "fromJSON('\"abc''def\"')", "result": { "kind": "String", "value": "abc'def" } },
|
||||||
|
{ "expr": "fromJSON('\"abc\\\\\\\"def\"')", "result": { "kind": "String", "value": "abc\\\"def" } }
|
||||||
|
],
|
||||||
|
"array": [
|
||||||
|
{ "expr": "fromJSON('[]')", "result": { "kind": "Array", "value": [] } },
|
||||||
|
{ "expr": "fromJSON('[1, 2, 3]')", "result": { "kind": "Array", "value": [1, 2, 3] } },
|
||||||
|
{
|
||||||
|
"expr": "fromJSON('[[1, 2, 3], [\"abc\",\"def\",\"ghi\"], [true, false, null, [], {}]]')",
|
||||||
|
"result": {
|
||||||
|
"kind": "Array",
|
||||||
|
"value": [[1, 2, 3], ["abc", "def", "ghi"], [true, false, null, [], {}]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"object": [
|
||||||
|
{ "expr": "fromJSON('{}')", "result": { "kind": "Object", "value": {} } },
|
||||||
|
{
|
||||||
|
"expr": "fromJSON('{\"one\": \"value one\", \"two\": \"value two\", \"three\": \"value three\"}')",
|
||||||
|
"result": {
|
||||||
|
"kind": "Object",
|
||||||
|
"value": {
|
||||||
|
"one": "value one",
|
||||||
|
"two": "value two",
|
||||||
|
"three": "value three"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "fromJSON('{\"nested-one\": {\"one\": 1,\"two\": 2,\"three\": 3},\"nested-two\": {\"string one\": \"value one\",\"string two\": \"value two\",\"string three\": \"value three\"},\"nested-three\": {\"true\": true,\"false\": false,\"null\": null,\"array\": [],\"object\": {}}\n}')",
|
||||||
|
"result": {
|
||||||
|
"kind": "Object",
|
||||||
|
"value": {
|
||||||
|
"nested-one": {
|
||||||
|
"one": 1,
|
||||||
|
"two": 2,
|
||||||
|
"three": 3
|
||||||
|
},
|
||||||
|
"nested-two": {
|
||||||
|
"string one": "value one",
|
||||||
|
"string two": "value two",
|
||||||
|
"string three": "value three"
|
||||||
|
},
|
||||||
|
"nested-three": {
|
||||||
|
"true": true,
|
||||||
|
"false": false,
|
||||||
|
"null": null,
|
||||||
|
"array": [],
|
||||||
|
"object": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+113
@@ -0,0 +1,113 @@
|
|||||||
|
{
|
||||||
|
"join": [
|
||||||
|
{
|
||||||
|
"expr": "join(github.event.issue.labels.*.name, ', ')",
|
||||||
|
"contexts": {
|
||||||
|
"github": {
|
||||||
|
"event": {
|
||||||
|
"issue": {
|
||||||
|
"labels": [
|
||||||
|
{
|
||||||
|
"name": "bug"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "enhancement"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "help wanted"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": { "kind": "String", "value": "bug, enhancement, help wanted" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "join(null)",
|
||||||
|
"result": { "kind": "String", "value": "" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "join(true)",
|
||||||
|
"result": { "kind": "String", "value": "true" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "join(123.456)",
|
||||||
|
"result": { "kind": "String", "value": "123.456" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "join('abc')",
|
||||||
|
"result": { "kind": "String", "value": "abc" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "join(myArray)",
|
||||||
|
"contexts": { "myArray": [] },
|
||||||
|
"result": { "kind": "String", "value": "" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "join(myObject)",
|
||||||
|
"contexts": { "myObject": {} },
|
||||||
|
"result": { "kind": "String", "value": "" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "join(myObject)",
|
||||||
|
"contexts": { "myObject": { "key1": "value1" } },
|
||||||
|
"result": { "kind": "String", "value": "" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "join(myArray)",
|
||||||
|
"contexts": { "myArray": [ null, true, 123.456, "abc", [], ["def"], {}, { "key1": "value1" } ] },
|
||||||
|
"result": { "kind": "String", "value": ",true,123.456,abc,Array,Array,Object,Object" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "join(myArray, null)",
|
||||||
|
"contexts": { "myArray": [ "_ITEM-1_", "_ITEM-2_", "_ITEM-3_" ] },
|
||||||
|
"result": { "kind": "String", "value": "_ITEM-1__ITEM-2__ITEM-3_" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "join(myArray, true)",
|
||||||
|
"contexts": { "myArray": [ "_ITEM-1_", "_ITEM-2_", "_ITEM-3_" ] },
|
||||||
|
"result": { "kind": "String", "value": "_ITEM-1_true_ITEM-2_true_ITEM-3_" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "join(myArray, 123.456)",
|
||||||
|
"contexts": { "myArray": [ "_ITEM-1_", "_ITEM-2_", "_ITEM-3_" ] },
|
||||||
|
"result": { "kind": "String", "value": "_ITEM-1_123.456_ITEM-2_123.456_ITEM-3_" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "join(myArray, ' | ')",
|
||||||
|
"contexts": { "myArray": [ "_ITEM-1_", "_ITEM-2_", "_ITEM-3_" ] },
|
||||||
|
"result": { "kind": "String", "value": "_ITEM-1_ | _ITEM-2_ | _ITEM-3_" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "join(myArray, mySeparator)",
|
||||||
|
"contexts": {
|
||||||
|
"myArray": [ "_ITEM-1_", "_ITEM-2_", "_ITEM-3_" ],
|
||||||
|
"mySeparator": []
|
||||||
|
},
|
||||||
|
"result": { "kind": "String", "value": "_ITEM-1_,_ITEM-2_,_ITEM-3_" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "join(myArray, mySeparator)",
|
||||||
|
"contexts": {
|
||||||
|
"myArray": [ "_ITEM-1_", "_ITEM-2_", "_ITEM-3_" ],
|
||||||
|
"mySeparator": {}
|
||||||
|
},
|
||||||
|
"result": { "kind": "String", "value": "_ITEM-1_,_ITEM-2_,_ITEM-3_" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "join()",
|
||||||
|
"err": {
|
||||||
|
"kind": "parsing",
|
||||||
|
"value": "Too few parameters supplied: "
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "join(1, 2, 3)",
|
||||||
|
"err": {
|
||||||
|
"kind": "parsing",
|
||||||
|
"value": "Too many parameters supplied: "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"number": [
|
||||||
|
{ "expr": "1", "result": { "kind": "Number", "value": 1.0 } },
|
||||||
|
{ "expr": ".5", "result": { "kind": "Number", "value": 0.5 } },
|
||||||
|
{ "expr": "0.5", "result": { "kind": "Number", "value": 0.5 } },
|
||||||
|
{ "expr": "2", "result": { "kind": "Number", "value": 2.0 } },
|
||||||
|
{ "expr": "-1", "result": { "kind": "Number", "value": -1.0 } },
|
||||||
|
{ "expr": "+1", "result": { "kind": "Number", "value": 1.0 } },
|
||||||
|
{ "expr": "-.5", "result": { "kind": "Number", "value": -0.5 } },
|
||||||
|
{ "expr": "-2", "result": { "kind": "Number", "value": -2.0 } },
|
||||||
|
{ "expr": "format('{0}', -Infinity)", "result": { "kind": "String", "value": "-Infinity" } },
|
||||||
|
{ "expr": "format('{0}', Infinity)", "result": { "kind": "String", "value": "Infinity" } },
|
||||||
|
{ "expr": "format('{0}', +Infinity)", "result": { "kind": "String", "value": "Infinity" } },
|
||||||
|
{ "expr": "format('{0}', NaN)", "result": { "kind": "String", "value": "NaN" } },
|
||||||
|
{ "expr": "0", "result": { "kind": "Number", "value": 0.0 } },
|
||||||
|
{ "expr": "0.0", "result": { "kind": "Number", "value": 0.0 } },
|
||||||
|
{ "expr": "-0", "result": { "kind": "Number", "value": -0.0 } },
|
||||||
|
{ "expr": "-0.0", "result": { "kind": "Number", "value": -0.0 } },
|
||||||
|
{ "expr": "0x0", "result": { "kind": "Number", "value": 0.0 } },
|
||||||
|
{ "expr": "0x00", "result": { "kind": "Number", "value": 0.0 } },
|
||||||
|
{ "expr": "0xf", "result": { "kind": "Number", "value": 15.0 } },
|
||||||
|
{ "expr": "0xfF", "result": { "kind": "Number", "value": 255.0 } },
|
||||||
|
{ "expr": "0xfFf", "result": { "kind": "Number", "value": 4095.0 } },
|
||||||
|
{ "expr": "0o0", "result": { "kind": "Number", "value": 0.0 } },
|
||||||
|
{ "expr": "0o7", "result": { "kind": "Number", "value": 7.0 } },
|
||||||
|
{ "expr": "0o77", "result": { "kind": "Number", "value": 63.0 } },
|
||||||
|
{ "expr": "0o777", "result": { "kind": "Number", "value": 511.0 } },
|
||||||
|
{ "expr": "1e1", "result": { "kind": "Number", "value": 10.0 } },
|
||||||
|
{ "expr": "1e2", "result": { "kind": "Number", "value": 100.0 } },
|
||||||
|
{ "expr": "1E1", "result": { "kind": "Number", "value": 10.0 } },
|
||||||
|
{ "expr": "1E+1", "result": { "kind": "Number", "value": 10.0 } },
|
||||||
|
{ "expr": "1e-1", "result": { "kind": "Number", "value": 0.1 } },
|
||||||
|
{ "expr": "1E-1", "result": { "kind": "Number", "value": 0.1 } },
|
||||||
|
{
|
||||||
|
"expr": "0x01p2",
|
||||||
|
"err": { "kind": "lexing", "value": "Unexpected symbol: '0x01p2'. Located at position 1 within expression: 0x01p2" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "-Inf",
|
||||||
|
"err": { "kind": "lexing", "value": "Unexpected symbol: '-Inf'. Located at position 1 within expression: -Inf" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "-0xFF",
|
||||||
|
"err": { "kind": "lexing", "value": "Unexpected symbol: '-0xFF'. Located at position 1 within expression: -0xFF" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "0xFZ",
|
||||||
|
"err": { "kind": "lexing", "value": "Unexpected symbol: '0xFZ'. Located at position 1 within expression: 0xFZ" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"operator_and": [
|
||||||
|
{
|
||||||
|
"expr": "true && true && true",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{ "expr": "true && true", "result": { "kind": "Boolean", "value": true } },
|
||||||
|
{
|
||||||
|
"expr": "true && true && false",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "true && false && true",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "false && true",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "false && false",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{ "expr": "true && 1 && 2", "result": { "kind": "Number", "value": 2.0 } },
|
||||||
|
{ "expr": "true && 0 && 2", "result": { "kind": "Number", "value": 0.0 } },
|
||||||
|
{
|
||||||
|
"expr": "true && 'a' && 'b'",
|
||||||
|
"result": { "kind": "String", "value": "b" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "true && '' && 'asdf'",
|
||||||
|
"result": { "kind": "String", "value": "" }
|
||||||
|
},
|
||||||
|
{ "expr": "null && true", "result": { "kind": "Null", "value": null } },
|
||||||
|
{
|
||||||
|
"expr": "test && 123",
|
||||||
|
"contexts": { "test": {} },
|
||||||
|
"result": { "kind": "Number", "value": 123.0 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "test && 456",
|
||||||
|
"contexts": { "test": {} },
|
||||||
|
"result": { "kind": "Number", "value": 456.0 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "test && 789",
|
||||||
|
"contexts": { "test": [] },
|
||||||
|
"result": { "kind": "Number", "value": 789.0 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+202
@@ -0,0 +1,202 @@
|
|||||||
|
{
|
||||||
|
"property-basics": [
|
||||||
|
{
|
||||||
|
"expr": "foo.bar",
|
||||||
|
"contexts": {
|
||||||
|
"foo": {
|
||||||
|
"bar": "baz"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": { "kind": "String", "value": "baz" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo.Bar",
|
||||||
|
"contexts": {
|
||||||
|
"foo": {
|
||||||
|
"Bar": "baz"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": { "kind": "String", "value": "baz" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo.b",
|
||||||
|
"contexts": {
|
||||||
|
"foo": {
|
||||||
|
"b": "baz"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": { "kind": "String", "value": "baz" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo._",
|
||||||
|
"contexts": {
|
||||||
|
"foo": {
|
||||||
|
"_": "baz"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": { "kind": "String", "value": "baz" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo._bar",
|
||||||
|
"contexts": {
|
||||||
|
"foo": {
|
||||||
|
"_bar": "baz"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": { "kind": "String", "value": "baz" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo.b_ar",
|
||||||
|
"contexts": {
|
||||||
|
"foo": {
|
||||||
|
"b_ar": "baz"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": { "kind": "String", "value": "baz" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo.b-ar",
|
||||||
|
"contexts": {
|
||||||
|
"foo": {
|
||||||
|
"b-ar": "baz"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": { "kind": "String", "value": "baz" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "fromJson('{\"one\": \"one val\"}').one",
|
||||||
|
"result": { "kind": "String", "value": "one val" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "(fromJson('{\"one\": \"one val\"}')).one",
|
||||||
|
"result": { "kind": "String", "value": "one val" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[*]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": {
|
||||||
|
"one": "one val",
|
||||||
|
"two": "two val"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": { "kind": "Array", "value": ["one val", "two val"] }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"property-case-insensitive": [
|
||||||
|
{
|
||||||
|
"expr": "foo.bar",
|
||||||
|
"contexts": {
|
||||||
|
"foo": {
|
||||||
|
"BAR": "baz"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": { "kind": "String", "value": "baz" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo.BAR",
|
||||||
|
"contexts": {
|
||||||
|
"foo": {
|
||||||
|
"bar": "baz"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": { "kind": "String", "value": "baz" }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"property-matches-const": [
|
||||||
|
{
|
||||||
|
"expr": "foo.true",
|
||||||
|
"contexts": {
|
||||||
|
"foo": {
|
||||||
|
"true": "it's true"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": { "kind": "String", "value": "it's true" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo.false",
|
||||||
|
"contexts": {
|
||||||
|
"foo": {
|
||||||
|
"false": "it's false"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": { "kind": "String", "value": "it's false" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo.Infinity",
|
||||||
|
"contexts": {
|
||||||
|
"foo": {
|
||||||
|
"Infinity": "it's Infinity"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": { "kind": "String", "value": "it's Infinity" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo.NaN",
|
||||||
|
"contexts": {
|
||||||
|
"foo": {
|
||||||
|
"NaN": "it's NaN"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": { "kind": "String", "value": "it's NaN" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo.null",
|
||||||
|
"contexts": {
|
||||||
|
"foo": {
|
||||||
|
"null": "it's null"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": { "kind": "String", "value": "it's null" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo.format",
|
||||||
|
"contexts": {
|
||||||
|
"foo": {
|
||||||
|
"format": "it's format"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": { "kind": "String", "value": "it's format" }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"property-errors": [
|
||||||
|
{
|
||||||
|
"expr": "foo.b@r",
|
||||||
|
"contexts": {
|
||||||
|
"foo": {
|
||||||
|
"b@r": "baz"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"err": {
|
||||||
|
"kind": "lexing",
|
||||||
|
"value": "Unexpected symbol: 'b@r'. Located at position 5 within expression: foo.b@r"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo.1",
|
||||||
|
"contexts": {
|
||||||
|
"foo": {}
|
||||||
|
},
|
||||||
|
"err": {
|
||||||
|
"kind": "parsing",
|
||||||
|
"value": "Unexpected symbol: '1'. Located at position 5 within expression: foo.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "fromjson('').1",
|
||||||
|
"err": {
|
||||||
|
"kind": "parsing",
|
||||||
|
"value": "Unexpected symbol: '1'. Located at position 14 within expression: fromjson('').1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[1].2",
|
||||||
|
"contexts": {
|
||||||
|
"foo": {}
|
||||||
|
},
|
||||||
|
"err": {
|
||||||
|
"kind": "parsing",
|
||||||
|
"value": "Unexpected symbol: '2'. Located at position 8 within expression: foo[1].2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+430
@@ -0,0 +1,430 @@
|
|||||||
|
{
|
||||||
|
"boolean": [
|
||||||
|
{ "expr": "true == true", "result": { "kind": "Boolean", "value": true } },
|
||||||
|
{
|
||||||
|
"expr": "false == false",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "false == true",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"number": [
|
||||||
|
{ "expr": "0 == -0", "result": { "kind": "Boolean", "value": true } },
|
||||||
|
{ "expr": "2 == 2", "result": { "kind": "Boolean", "value": true } },
|
||||||
|
{ "expr": "1.001 == 1.002", "result": { "kind": "Boolean", "value": false } },
|
||||||
|
{ "expr": "120 == 1.2e2", "result": { "kind": "Boolean", "value": true } },
|
||||||
|
{ "expr": "1 == 2", "result": { "kind": "Boolean", "value": false } },
|
||||||
|
{ "expr": "NaN == NaN", "result": { "kind": "Boolean", "value": false } },
|
||||||
|
{
|
||||||
|
"expr": "Infinity == Infinity",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "-Infinity == Infinity",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "0 == Infinity",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "1 == Infinity",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"string": [
|
||||||
|
{ "expr": "'a' == 'a'", "result": { "kind": "Boolean", "value": true } },
|
||||||
|
{ "expr": "'a' == 'b'", "result": { "kind": "Boolean", "value": false } }
|
||||||
|
],
|
||||||
|
|
||||||
|
"null": [
|
||||||
|
{ "expr": "null == null", "result": { "kind": "Boolean", "value": true } }
|
||||||
|
],
|
||||||
|
|
||||||
|
"object": [
|
||||||
|
{
|
||||||
|
"expr": "object1 == object1",
|
||||||
|
"contexts": { "object1": {} },
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "object1 == object2",
|
||||||
|
"contexts": { "object1": {}, "object2": {} },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"array": [
|
||||||
|
{
|
||||||
|
"expr": "array1 == array1",
|
||||||
|
"contexts": { "array1": [] },
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "array1 == array2",
|
||||||
|
"contexts": { "array1": [], "array2": [] },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_bool_number": [
|
||||||
|
{ "expr": "false == 0", "result": { "kind": "Boolean", "value": true } },
|
||||||
|
{ "expr": "true == 1", "result": { "kind": "Boolean", "value": true } },
|
||||||
|
{ "expr": "false == NaN", "result": { "kind": "Boolean", "value": false } },
|
||||||
|
{ "expr": "true == 2", "result": { "kind": "Boolean", "value": false } }
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_bool_string": [
|
||||||
|
{ "expr": "false == ''", "result": { "kind": "Boolean", "value": true } },
|
||||||
|
{
|
||||||
|
"expr": "false == ' '",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "false == ' 0.0 '",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "true == ' 1.0 '",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "true == ' 1 '",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "false == '-1'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{ "expr": "true == '2'", "result": { "kind": "Boolean", "value": false } }
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_bool_null": [
|
||||||
|
{
|
||||||
|
"expr": "false == null",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "true == null",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_bool_object-array": [
|
||||||
|
{
|
||||||
|
"expr": "false == test",
|
||||||
|
"contexts": { "test": {} },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "false == test",
|
||||||
|
"contexts": { "test": [] },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "true == test",
|
||||||
|
"contexts": { "test": {} },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "true == test",
|
||||||
|
"contexts": { "test": [] },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_number_bool": [
|
||||||
|
{ "expr": "0 == false", "result": { "kind": "Boolean", "value": true } },
|
||||||
|
{ "expr": "1 == true", "result": { "kind": "Boolean", "value": true } },
|
||||||
|
{
|
||||||
|
"expr": "NaN == false",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{ "expr": "2 == true", "result": { "kind": "Boolean", "value": false } }
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_number_string": [
|
||||||
|
{
|
||||||
|
"expr": "0 == ' -0.0 '",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{ "expr": "0 == ''", "result": { "kind": "Boolean", "value": true } },
|
||||||
|
{ "expr": "0 == ' '", "result": { "kind": "Boolean", "value": true } },
|
||||||
|
{
|
||||||
|
"expr": "120 == ' +1.2e2 '",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "120.0 == ' 1.2E2 '",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "120 == ' 1.2e+2 '",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "-120 == ' -1.2E+2 '",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "0.012 == ' 1.2e-2 '",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "0.012 == ' 1.2E-2 '",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "255.0 == ' 0xff '",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "8.0 == ' 0o10 '",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "Infinity == ' Infinity '",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "-Infinity == ' -Infinity '",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "NaN == 'NaN'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{ "expr": "1 == '0'", "result": { "kind": "Boolean", "value": false } },
|
||||||
|
{ "expr": "1 == 'a'", "result": { "kind": "Boolean", "value": false } }
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_number_null": [
|
||||||
|
{ "expr": "0 == null", "result": { "kind": "Boolean", "value": true } }
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_number_object-array": [
|
||||||
|
{
|
||||||
|
"expr": "0 == test",
|
||||||
|
"contexts": { "test": {} },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "1 == test",
|
||||||
|
"contexts": { "test": {} },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "0 == test",
|
||||||
|
"contexts": { "test": [] },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "1 == test",
|
||||||
|
"contexts": { "test": [] },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_string_bool": [
|
||||||
|
{ "expr": "'' == false", "result": { "kind": "Boolean", "value": true } },
|
||||||
|
{
|
||||||
|
"expr": "' ' == false",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' 0.0 ' == false",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' 1.0 ' == true",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' 1 ' == true",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'-1' == false",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{ "expr": "'2' == true", "result": { "kind": "Boolean", "value": false } }
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_string_number": [
|
||||||
|
{
|
||||||
|
"expr": "' -0.0 ' == 0",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{ "expr": "'' == 0", "result": { "kind": "Boolean", "value": true } },
|
||||||
|
{ "expr": "' ' == 0", "result": { "kind": "Boolean", "value": true } },
|
||||||
|
{
|
||||||
|
"expr": "' +1.2e2 ' == 120",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' 1.2E2 ' == 120.0",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' 1.2e+2 ' == 120",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' -1.2E+2 ' == -120",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' 1.2e-2 ' == 0.012",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' 1.2E-2 ' == 0.012",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' 0xff ' == 255.0",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' 0o10 ' == 8.0",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' Infinity ' == Infinity",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' -Infinity ' == -Infinity",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'NaN' == NaN",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{ "expr": "'0' == 1", "result": { "kind": "Boolean", "value": false } },
|
||||||
|
{ "expr": "'a' == 1", "result": { "kind": "Boolean", "value": false } }
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_string_null": [
|
||||||
|
{ "expr": "'' == null", "result": { "kind": "Boolean", "value": true } },
|
||||||
|
{
|
||||||
|
"expr": "' ' == null",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{ "expr": "'0' == null", "result": { "kind": "Boolean", "value": true } },
|
||||||
|
{
|
||||||
|
"expr": "'false' == null",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'null' == null",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_string_object": [
|
||||||
|
{
|
||||||
|
"expr": "'' == test",
|
||||||
|
"contexts": { "test": {} },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'false' == test",
|
||||||
|
"contexts": { "test": {} },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'0' == test",
|
||||||
|
"contexts": { "test": {} },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'null' == test",
|
||||||
|
"contexts": { "test": {} },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'Object' == test",
|
||||||
|
"contexts": { "test": {} },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_string_array": [
|
||||||
|
{
|
||||||
|
"expr": "'' == test",
|
||||||
|
"contexts": { "test": [] },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'false' == test",
|
||||||
|
"contexts": { "test": [] },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'0' == test",
|
||||||
|
"contexts": { "test": [] },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'null' == test",
|
||||||
|
"contexts": { "test": [] },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'Array' == test",
|
||||||
|
"contexts": { "test": [] },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_null_bool": [
|
||||||
|
{
|
||||||
|
"expr": "null == false",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "null == true",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_null_number": [
|
||||||
|
{ "expr": "null == 0", "result": { "kind": "Boolean", "value": true } },
|
||||||
|
{ "expr": "false == 1", "result": { "kind": "Boolean", "value": false } }
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_null_string": [
|
||||||
|
{ "expr": "null == ''", "result": { "kind": "Boolean", "value": true } },
|
||||||
|
{
|
||||||
|
"expr": "null == ' '",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{ "expr": "null == '0'", "result": { "kind": "Boolean", "value": true } },
|
||||||
|
{
|
||||||
|
"expr": "null == 'false'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "null == 'null'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_null_object-array": [
|
||||||
|
{
|
||||||
|
"expr": "null == test",
|
||||||
|
"contexts": { "test": {} },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "null == test",
|
||||||
|
"contexts": { "test": [] },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+376
@@ -0,0 +1,376 @@
|
|||||||
|
{
|
||||||
|
"bool": [
|
||||||
|
{
|
||||||
|
"expr": "false > true",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "false > false",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "true > false",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "true > true",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"number": [
|
||||||
|
{
|
||||||
|
"expr": "1 > 2",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "1 > 1",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "2 > 1",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "1.002 > 1.001",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"string": [
|
||||||
|
{
|
||||||
|
"expr": "'def' > 'abc'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'a' > 'b'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"array": [
|
||||||
|
{
|
||||||
|
"expr": "test > test",
|
||||||
|
"contexts": { "test": [] },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"object": [
|
||||||
|
{
|
||||||
|
"expr": "test > test",
|
||||||
|
"contexts": { "test": {} },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_bool_number": [
|
||||||
|
{
|
||||||
|
"expr": "false > 0",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "false > -11",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "true > 1",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "true > 0",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "false > NaN",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "true > NaN",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "false > Infinity",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "true > Infinity",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "false > -Infinity",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "true > -Infinity",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_bool_string": [
|
||||||
|
{
|
||||||
|
"expr": "false > '0'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "false > '-1'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "true > '1'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "true > '0'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_bool_null": [
|
||||||
|
{
|
||||||
|
"expr": "false > null",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "true > null",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_bool_object": [
|
||||||
|
{
|
||||||
|
"expr": "false > test",
|
||||||
|
"contexts": { "test": {} },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "true > test",
|
||||||
|
"contexts": { "test": {} },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_bool_array": [
|
||||||
|
{
|
||||||
|
"expr": "false > test",
|
||||||
|
"contexts": { "test": [] },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "true > test",
|
||||||
|
"contexts": { "test": [] },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_number_bool": [
|
||||||
|
{
|
||||||
|
"expr": "1 > false",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "0 > false",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "2 > true",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "1 > true",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "NaN > false",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "NaN > true",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "Infinity > false",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "Infinity > true",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "-Infinity > false",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "-Infinity > true",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_number_string": [
|
||||||
|
{
|
||||||
|
"expr": "0 > ' 0 '",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "0 > ' -1 '",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "1 > ' 1 '",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "1 > ' 0 '",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_number_null": [
|
||||||
|
{
|
||||||
|
"expr": "1 > null",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "0 > null",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_number_object": [
|
||||||
|
{
|
||||||
|
"expr": "0 > test",
|
||||||
|
"contexts": { "test": {} },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "1 > test",
|
||||||
|
"contexts": { "test": {} },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_number_array": [
|
||||||
|
{
|
||||||
|
"expr": "0 > test",
|
||||||
|
"contexts": { "test": [] },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "1 > test",
|
||||||
|
"contexts": { "test": [] },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_string_bool": [
|
||||||
|
{
|
||||||
|
"expr": "'0' > false",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'1' > false",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'1' > true",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'2' > true",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_string_number": [
|
||||||
|
{
|
||||||
|
"expr": "' 0 ' > -1",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' 0 ' > 0",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' 1 ' > 0",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' 1 ' > 1",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_string_null": [
|
||||||
|
{
|
||||||
|
"expr": "'' > null",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'1' > null",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_string_object": [
|
||||||
|
{
|
||||||
|
"expr": "'' > test",
|
||||||
|
"contexts": { "test": {} },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_string_array": [
|
||||||
|
{
|
||||||
|
"expr": "'' > test",
|
||||||
|
"contexts": { "test": [] },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_null_bool": [
|
||||||
|
{
|
||||||
|
"expr": "null > false",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "null > true",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_null_number": [
|
||||||
|
{
|
||||||
|
"expr": "null > 0",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "null > -1",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_null_string": [
|
||||||
|
{
|
||||||
|
"expr": "null > ' 0 '",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "null > ' -1 '",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_null_object": [
|
||||||
|
{
|
||||||
|
"expr": "null > test",
|
||||||
|
"contexts": { "test": {} },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_null_array": [
|
||||||
|
{
|
||||||
|
"expr": "null > test",
|
||||||
|
"contexts": { "test": [] },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"bool": [
|
||||||
|
{
|
||||||
|
"expr": "false >= true",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "false >= false",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "true >= false",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "true >= true",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+354
@@ -0,0 +1,354 @@
|
|||||||
|
{
|
||||||
|
"array-index-coercion": [
|
||||||
|
{
|
||||||
|
"expr": "foo[null]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": [
|
||||||
|
"zero",
|
||||||
|
"one",
|
||||||
|
"two"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": { "kind": "String", "value": "zero" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[false]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": [
|
||||||
|
"zero",
|
||||||
|
"one",
|
||||||
|
"two"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": { "kind": "String", "value": "zero" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[true]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": [
|
||||||
|
"zero",
|
||||||
|
"one",
|
||||||
|
"two"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": { "kind": "String", "value": "one" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[' 0.0 ']",
|
||||||
|
"contexts": {
|
||||||
|
"foo": [
|
||||||
|
"zero",
|
||||||
|
"one",
|
||||||
|
"two"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": { "kind": "String", "value": "zero" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[' 0x02 ']",
|
||||||
|
"contexts": {
|
||||||
|
"foo": [
|
||||||
|
"zero",
|
||||||
|
"one",
|
||||||
|
"two"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": { "kind": "String", "value": "two" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[' asdf ']",
|
||||||
|
"contexts": {
|
||||||
|
"foo": [
|
||||||
|
"zero",
|
||||||
|
"one",
|
||||||
|
"two"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": { "kind": "Null", "value": null }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[fromjson('[]')]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": [
|
||||||
|
"zero",
|
||||||
|
"one",
|
||||||
|
"two"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": { "kind": "Null", "value": null }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[fromjson('{}')]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": [
|
||||||
|
"zero",
|
||||||
|
"one",
|
||||||
|
"two"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": { "kind": "Null", "value": null }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"array-index-out-of-range": [
|
||||||
|
{
|
||||||
|
"expr": "foo[-1]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": [
|
||||||
|
"zero",
|
||||||
|
"one",
|
||||||
|
"two"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": { "kind": "Null", "value": null }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[3]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": [
|
||||||
|
"zero",
|
||||||
|
"one",
|
||||||
|
"two"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": { "kind": "Null", "value": null }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[-Infinity]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": [
|
||||||
|
"zero",
|
||||||
|
"one",
|
||||||
|
"two"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": { "kind": "Null", "value": null }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[Infinity]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": [
|
||||||
|
"zero",
|
||||||
|
"one",
|
||||||
|
"two"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": { "kind": "Null", "value": null }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[NaN]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": [
|
||||||
|
"zero",
|
||||||
|
"one",
|
||||||
|
"two"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": { "kind": "Null", "value": null }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"object-case-insensitive": [
|
||||||
|
{
|
||||||
|
"expr": "foo['bAr']",
|
||||||
|
"contexts": {
|
||||||
|
"foo": {
|
||||||
|
"BaR": "baz"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": { "kind": "String", "value": "baz" }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"object-index-coercion": [
|
||||||
|
{
|
||||||
|
"expr": "foo[5]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": {
|
||||||
|
"5": "it's 5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": { "kind": "String", "value": "it's 5" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[.5]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": {
|
||||||
|
"0.5": "it's 0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": { "kind": "String", "value": "it's 0.5" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[-.5]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": {
|
||||||
|
"-0.5": "it's -0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": { "kind": "String", "value": "it's -0.5" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[0x0f]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": {
|
||||||
|
"15": "it's 15"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": { "kind": "String", "value": "it's 15" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[NaN]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": {
|
||||||
|
"NaN": "it's NaN"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": { "kind": "String", "value": "it's NaN" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[Infinity]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": {
|
||||||
|
"Infinity": "it's Infinity"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": { "kind": "String", "value": "it's Infinity" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[-Infinity]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": {
|
||||||
|
"-Infinity": "it's -Infinity"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": { "kind": "String", "value": "it's -Infinity" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[null]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": {
|
||||||
|
"": "it's null"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": { "kind": "String", "value": "it's null" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[true]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": {
|
||||||
|
"true": "it's true"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": { "kind": "String", "value": "it's true" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[false]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": {
|
||||||
|
"false": "it's false"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": { "kind": "String", "value": "it's false" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[fromJson('{}')]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": {
|
||||||
|
"Object": "It's Object"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": { "kind": "Null", "value": null }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[format('{0}', fromJson('{}'))]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": {
|
||||||
|
"Object": "It's Object"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": { "kind": "String", "value": "It's Object" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[fromJson('[]')]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": {
|
||||||
|
"Array": "It's Array"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": { "kind": "Null", "value": null }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[format('{0}', fromJson('[]'))]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": {
|
||||||
|
"Array": "It's Array"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": { "kind": "String", "value": "It's Array" }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"object-index-out-of-range": [
|
||||||
|
{
|
||||||
|
"expr": "foo['']",
|
||||||
|
"contexts": {
|
||||||
|
"foo": {
|
||||||
|
"bar": "baz"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": { "kind": "Null", "value": null }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo['asdf']",
|
||||||
|
"contexts": {
|
||||||
|
"foo": {
|
||||||
|
"bar": "baz"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": { "kind": "Null", "value": null }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index-following-function": [
|
||||||
|
{
|
||||||
|
"expr": "fromJson('[\"one\", \"two\"]')[1]",
|
||||||
|
"result": { "kind": "String", "value": "two" }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index-following-group": [
|
||||||
|
{
|
||||||
|
"expr": "(fromJson('[\"one\", \"two\"]'))[1]",
|
||||||
|
"result": { "kind": "String", "value": "two" }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index-star": [
|
||||||
|
{
|
||||||
|
"expr": "foo[*]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": {
|
||||||
|
"one": "one val",
|
||||||
|
"two": "two val",
|
||||||
|
"*": "star val"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": { "kind": "Array", "value": ["one val", "two val", "star val"] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo['*']",
|
||||||
|
"contexts": {
|
||||||
|
"foo": {
|
||||||
|
"one": "one val",
|
||||||
|
"two": "two val",
|
||||||
|
"*": "star val"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": { "kind": "String", "value": "star val" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[*]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": [
|
||||||
|
"one",
|
||||||
|
"two"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": { "kind": "Array", "value": ["one", "two"] }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+556
@@ -0,0 +1,556 @@
|
|||||||
|
{
|
||||||
|
"filtered-array-from-literal": [
|
||||||
|
{
|
||||||
|
"expr": "foo.*",
|
||||||
|
"contexts": {
|
||||||
|
"foo": null
|
||||||
|
},
|
||||||
|
"result": { "kind": "Array", "value": [] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo.*",
|
||||||
|
"contexts": {
|
||||||
|
"foo": false
|
||||||
|
},
|
||||||
|
"result": { "kind": "Array", "value": [] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo.*",
|
||||||
|
"contexts": {
|
||||||
|
"foo": true
|
||||||
|
},
|
||||||
|
"result": { "kind": "Array", "value": [] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo.*",
|
||||||
|
"contexts": {
|
||||||
|
"foo": 123
|
||||||
|
},
|
||||||
|
"result": { "kind": "Array", "value": [] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo.*",
|
||||||
|
"contexts": {
|
||||||
|
"foo": "abc"
|
||||||
|
},
|
||||||
|
"result": { "kind": "Array", "value": [] }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filtered-array-from-array": [
|
||||||
|
{
|
||||||
|
"expr": "foo.*",
|
||||||
|
"contexts": {
|
||||||
|
"foo": [
|
||||||
|
"zero",
|
||||||
|
"one"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": { "kind": "Array", "value": ["zero", "one"] }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filtered-array-from-object": [
|
||||||
|
{
|
||||||
|
"expr": "foo.*",
|
||||||
|
"contexts": {
|
||||||
|
"foo": {
|
||||||
|
"one": "one val",
|
||||||
|
"two": "two val"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": { "kind": "Array", "value": ["one val", "two val"] }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nested-object": [
|
||||||
|
{
|
||||||
|
"expr": "foo.*.one",
|
||||||
|
"contexts": {
|
||||||
|
"foo": [
|
||||||
|
{
|
||||||
|
"one": "obj_1_prop_1",
|
||||||
|
"two": "obj_1_prop_2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"one": "obj_2_prop_1",
|
||||||
|
"two": "obj_2_prop_2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": { "kind": "Array", "value": ["obj_1_prop_1", "obj_2_prop_1"] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[*]['']",
|
||||||
|
"contexts": {
|
||||||
|
"foo": [
|
||||||
|
{
|
||||||
|
"": "empty string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": { "kind": "Array", "value": ["empty string"] }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nested-object-property-case-insensitive": [
|
||||||
|
{
|
||||||
|
"expr": "foo.*.thePROPERTY",
|
||||||
|
"contexts": {
|
||||||
|
"foo": [
|
||||||
|
{
|
||||||
|
"THEproperty": "the property"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": { "kind": "Array", "value": ["the property"] }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nested-object-property-not-found": [
|
||||||
|
{
|
||||||
|
"expr": "foo.*.one",
|
||||||
|
"contexts": {
|
||||||
|
"foo": [
|
||||||
|
{
|
||||||
|
"one": "obj_1_prop_1",
|
||||||
|
"two": "obj_1_prop_2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"zero": "obj_2_prop_0",
|
||||||
|
"three": "obj_2_prop_3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"one": "obj_3_prop_1",
|
||||||
|
"two": "obj_3_prop_2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": { "kind": "Array", "value": ["obj_1_prop_1", "obj_3_prop_1"] }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nested-object-property-coercion": [
|
||||||
|
{
|
||||||
|
"expr": "foo[*][null]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": [
|
||||||
|
{
|
||||||
|
"": "empty string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": { "kind": "Array", "value": ["empty string"] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[*][false]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": [
|
||||||
|
{
|
||||||
|
"false": "It's false"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": { "kind": "Array", "value": ["It's false"] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[*][true]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": [
|
||||||
|
{
|
||||||
|
"true": "It's true"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": { "kind": "Array", "value": ["It's true"] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[*][0x0f]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": [
|
||||||
|
{
|
||||||
|
"15": "fifteen"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": { "kind": "Array", "value": ["fifteen"] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[*][NaN]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": [
|
||||||
|
{
|
||||||
|
"NaN": "It's NaN"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": { "kind": "Array", "value": ["It's NaN"] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[*][Infinity]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": [
|
||||||
|
{
|
||||||
|
"Infinity": "It's Infinity"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": { "kind": "Array", "value": ["It's Infinity"] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[*][-Infinity]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": [
|
||||||
|
{
|
||||||
|
"-Infinity": "It's -Infinity"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": { "kind": "Array", "value": ["It's -Infinity"] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[*][fromjson('[]')]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": [
|
||||||
|
{
|
||||||
|
"": "empty string",
|
||||||
|
"Array": "It's array"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": { "kind": "Array", "value": [] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[*][fromjson('{}')]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": [
|
||||||
|
{
|
||||||
|
"": "empty string",
|
||||||
|
"Object": "It's object"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": { "kind": "Array", "value": [] }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nested-array": [
|
||||||
|
{
|
||||||
|
"expr": "foo[*][1]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": [
|
||||||
|
[
|
||||||
|
"ary_0_idx_0",
|
||||||
|
"ary_0_idx_1",
|
||||||
|
"ary_0_idx_2"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"ary_1_idx_0",
|
||||||
|
"ary_1_idx_1",
|
||||||
|
"ary_1_idx_2"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": { "kind": "Array", "value": ["ary_0_idx_1", "ary_1_idx_1"] }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nested-array-index-not-found": [
|
||||||
|
{
|
||||||
|
"expr": "foo[*][1]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": [
|
||||||
|
[
|
||||||
|
"ary_0_idx_0",
|
||||||
|
"ary_0_idx_1",
|
||||||
|
"ary_0_idx_2"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"ary_1_idx_0"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"ary_2_idx_0",
|
||||||
|
"ary_2_idx_1",
|
||||||
|
"ary_2_idx_2"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": { "kind": "Array", "value": ["ary_0_idx_1", "ary_2_idx_1"] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[*][-1]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": [
|
||||||
|
[
|
||||||
|
"zero",
|
||||||
|
"one",
|
||||||
|
"two"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": { "kind": "Array", "value": [] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[*][NaN]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": [
|
||||||
|
[
|
||||||
|
"zero",
|
||||||
|
"one",
|
||||||
|
"two"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": { "kind": "Array", "value": [] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[*][Infinity]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": [
|
||||||
|
[
|
||||||
|
"zero",
|
||||||
|
"one",
|
||||||
|
"two"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": { "kind": "Array", "value": [] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[*][Infinity]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": [
|
||||||
|
[
|
||||||
|
"zero",
|
||||||
|
"one",
|
||||||
|
"two"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": { "kind": "Array", "value": [] }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nested-array-index-coercion": [
|
||||||
|
{
|
||||||
|
"expr": "foo[*][null]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": [
|
||||||
|
[
|
||||||
|
"zero",
|
||||||
|
"one",
|
||||||
|
"two"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": { "kind": "Array", "value": ["zero"] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[*][false]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": [
|
||||||
|
[
|
||||||
|
"zero",
|
||||||
|
"one",
|
||||||
|
"two"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": { "kind": "Array", "value": ["zero"] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[*][true]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": [
|
||||||
|
[
|
||||||
|
"zero",
|
||||||
|
"one",
|
||||||
|
"two"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": { "kind": "Array", "value": ["one"] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[*]['']",
|
||||||
|
"contexts": {
|
||||||
|
"foo": [
|
||||||
|
[
|
||||||
|
"zero",
|
||||||
|
"one",
|
||||||
|
"two"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": { "kind": "Array", "value": ["zero"] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[*]['abc']",
|
||||||
|
"contexts": {
|
||||||
|
"foo": [
|
||||||
|
[
|
||||||
|
"zero",
|
||||||
|
"one",
|
||||||
|
"two"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": { "kind": "Array", "value": [] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[*][' 0x01 ']",
|
||||||
|
"contexts": {
|
||||||
|
"foo": [
|
||||||
|
[
|
||||||
|
"zero",
|
||||||
|
"one",
|
||||||
|
"two"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": { "kind": "Array", "value": ["one"] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[*][fromjson('[]')]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": [
|
||||||
|
[
|
||||||
|
"zero",
|
||||||
|
"one",
|
||||||
|
"two"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": { "kind": "Array", "value": [] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[*][fromjson('{}')]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": [
|
||||||
|
[
|
||||||
|
"zero",
|
||||||
|
"one",
|
||||||
|
"two"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": { "kind": "Array", "value": [] }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"double-star": [
|
||||||
|
{
|
||||||
|
"expr": "foo.*.*",
|
||||||
|
"contexts": {
|
||||||
|
"foo": [
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
123,
|
||||||
|
"hello",
|
||||||
|
[
|
||||||
|
"zero",
|
||||||
|
"one",
|
||||||
|
"two"
|
||||||
|
],
|
||||||
|
{
|
||||||
|
"p1": "v1",
|
||||||
|
"p2": "v2",
|
||||||
|
"p3": "v3"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": { "kind": "Array", "value": ["zero", "one", "two", "v1", "v2", "v3"] }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"double-property": [
|
||||||
|
{
|
||||||
|
"expr": "foo.*.p.nestedp",
|
||||||
|
"contexts": {
|
||||||
|
"foo": [
|
||||||
|
{
|
||||||
|
"p": {
|
||||||
|
"nestedp": "val 1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"p": {
|
||||||
|
"nestedp": "val 2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": { "kind": "Array", "value": ["val 1", "val 2"] }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"double-index": [
|
||||||
|
{
|
||||||
|
"expr": "foo[*][1][2]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": [
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"0_0_0",
|
||||||
|
"0_0_1",
|
||||||
|
"0_0_2",
|
||||||
|
"0_0_3"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"0_1_0",
|
||||||
|
"0_1_1",
|
||||||
|
"0_1_2",
|
||||||
|
"0_1_3"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"0_2_0",
|
||||||
|
"0_2_1",
|
||||||
|
"0_2_2",
|
||||||
|
"0_2_3"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"1_0_0",
|
||||||
|
"1_0_1",
|
||||||
|
"1_0_2",
|
||||||
|
"1_0_3"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"1_1_0",
|
||||||
|
"1_1_1",
|
||||||
|
"1_1_2",
|
||||||
|
"1_1_3"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"1_2_0",
|
||||||
|
"1_2_1",
|
||||||
|
"1_2_2",
|
||||||
|
"1_2_3"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": { "kind": "Array", "value": ["0_1_2", "1_1_2"] }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"not-found-always-returns-empty-array": [
|
||||||
|
{
|
||||||
|
"expr": "foo[*][1]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": []
|
||||||
|
},
|
||||||
|
"result": { "kind": "Array", "value": [] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[*][1][2]",
|
||||||
|
"contexts": {
|
||||||
|
"foo": []
|
||||||
|
},
|
||||||
|
"result": { "kind": "Array", "value": [] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo.*.bar",
|
||||||
|
"contexts": {
|
||||||
|
"foo": {}
|
||||||
|
},
|
||||||
|
"result": { "kind": "Array", "value": [] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo.*.bar.baz",
|
||||||
|
"contexts": {
|
||||||
|
"foo": []
|
||||||
|
},
|
||||||
|
"result": { "kind": "Array", "value": [] }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+378
@@ -0,0 +1,378 @@
|
|||||||
|
{
|
||||||
|
"bool": [
|
||||||
|
{
|
||||||
|
"expr": "false < true",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "false < false",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "true < false",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "true < true",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"number": [
|
||||||
|
{
|
||||||
|
"expr": "1 < 2",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "1 < 1",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "2 < 1",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "1.001 < 1.002",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"string": [
|
||||||
|
{
|
||||||
|
"expr": "'abc' < 'def'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'b' < 'a'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"array": [
|
||||||
|
{
|
||||||
|
"expr": "test < test",
|
||||||
|
"contexts": { "test": [] },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"object": [
|
||||||
|
{
|
||||||
|
"expr": "test < test",
|
||||||
|
"contexts": { "test": {} },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_bool_number": [
|
||||||
|
{
|
||||||
|
"expr": "false < 0",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "false < 1",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "true < 1",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "true < 2",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "false < NaN",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "true < NaN",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "false < Infinity",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "true < Infinity",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "false < -Infinity",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "true < -Infinity",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_bool_string": [
|
||||||
|
{
|
||||||
|
"expr": "false < '0'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "false < '1'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "true < '1'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "true < '2'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_bool_null": [
|
||||||
|
{
|
||||||
|
"expr": "false < null",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "true < null",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_bool_object": [
|
||||||
|
{
|
||||||
|
"expr": "false < test",
|
||||||
|
"contexts": { "test": {} },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "true < test",
|
||||||
|
"contexts": { "test": {} },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_bool_array": [
|
||||||
|
{
|
||||||
|
"expr": "false < test",
|
||||||
|
"contexts": { "test": [] },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "true < test",
|
||||||
|
"contexts": { "test": [] },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_number_bool": [
|
||||||
|
{
|
||||||
|
"expr": "-1 < false",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "0 < false",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "0 < true",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "1 < true",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "NaN < false",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "NaN < true",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "Infinity < false",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "Infinity < true",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "-Infinity < false",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "-Infinity < true",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_number_string": [
|
||||||
|
{
|
||||||
|
"expr": "0 < ' 0 '",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "0 < ' 1 '",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "1 < ' 1 '",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "1 < ' 2 '",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_number_null": [
|
||||||
|
{
|
||||||
|
"expr": "-1 < null",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "0 < null",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_number_object": [
|
||||||
|
{
|
||||||
|
"expr": "0 < test",
|
||||||
|
"contexts": { "test": {} },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "-1 < test",
|
||||||
|
"contexts": { "test": {} },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_number_array": [
|
||||||
|
{
|
||||||
|
"expr": "0 < test",
|
||||||
|
"contexts": { "test": [] },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "-1 < test",
|
||||||
|
"contexts": { "test": [] },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_string_bool": [
|
||||||
|
{
|
||||||
|
"expr": "'0' < false",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'-1' < false",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'1' < true",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'0' < true",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_string_number": [
|
||||||
|
{
|
||||||
|
"expr": "' 0 ' < 1",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' 0 ' < 0",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' 1 ' < 2",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' 1 ' < 1",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_string_null": [
|
||||||
|
{
|
||||||
|
"expr": "'' < null",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'-1' < null",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_string_object": [
|
||||||
|
{
|
||||||
|
"expr": "'' < test",
|
||||||
|
"contexts": { "test": {} },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_string_array": [
|
||||||
|
{
|
||||||
|
"expr": "'' < test",
|
||||||
|
"contexts": { "test": [] },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_null_bool": [
|
||||||
|
{
|
||||||
|
"expr": "null < false",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "null < true",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_null_number": [
|
||||||
|
{
|
||||||
|
"expr": "null < 0",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "null < 1",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_null_string": [
|
||||||
|
{
|
||||||
|
"expr": "null < ' 0 '",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "null < ' 1 '",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_null_object": [
|
||||||
|
{
|
||||||
|
"expr": "null < test",
|
||||||
|
"contexts": { "test": {} },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_null_array": [
|
||||||
|
{
|
||||||
|
"expr": "null < test",
|
||||||
|
"contexts": { "test": [] },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"bool": [
|
||||||
|
{
|
||||||
|
"expr": "false <= true",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "false <= false",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "true <= false",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "true <= true",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+597
@@ -0,0 +1,597 @@
|
|||||||
|
{
|
||||||
|
"boolean": [
|
||||||
|
{
|
||||||
|
"expr": "true != true",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "false != false",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "true != true",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"number": [
|
||||||
|
{
|
||||||
|
"expr": "0 != -0",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "2 != 2",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "120 != 1.2e2",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "1 != 2",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "1.001 != 1.002",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "NaN != NaN",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "0 != NaN",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "1 != NaN",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "Infinity != Infinity",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "-Infinity != Infinity",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "0 != Infinity",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "1 != Infinity",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"string": [
|
||||||
|
{
|
||||||
|
"expr": "'a' != 'a'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'a' != 'b'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"null": [
|
||||||
|
{ "expr": "null != null", "result": { "kind": "Boolean", "value": false } }
|
||||||
|
],
|
||||||
|
|
||||||
|
"object": [
|
||||||
|
{
|
||||||
|
"expr": "object1 != object1",
|
||||||
|
"contexts": { "object1": {} },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "object1 != object2",
|
||||||
|
"contexts": { "object1": {}, "object2": {} },
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"array": [
|
||||||
|
{
|
||||||
|
"expr": "array1 != array1",
|
||||||
|
"contexts": { "array1": [] },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "array1 != array2",
|
||||||
|
"contexts": { "array1": [], "array2": [] },
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_bool_number": [
|
||||||
|
{
|
||||||
|
"expr": "false != 0",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "false != 1",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "true != 1",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "false != NaN",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "true != 2",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_bool_string": [
|
||||||
|
{
|
||||||
|
"expr": "false != ''",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "false != ' '",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "false != ' 0.0 '",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "false != ' 1.0 '",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "true != ' 1.0 '",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "true != ' 1 '",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "false != '-1'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "true != '2'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_bool_null": [
|
||||||
|
{
|
||||||
|
"expr": "false != null",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "true != null",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_bool_object-array": [
|
||||||
|
{
|
||||||
|
"expr": "false != test",
|
||||||
|
"contexts": { "test": {} },
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "false != test",
|
||||||
|
"contexts": { "test": [] },
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "true != test",
|
||||||
|
"contexts": { "test": {} },
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "true != test",
|
||||||
|
"contexts": { "test": [] },
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_number_bool": [
|
||||||
|
{
|
||||||
|
"expr": "0 != false",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "1 != false",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "1 != true",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "2 != true",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "NaN != false",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_number_string": [
|
||||||
|
{
|
||||||
|
"expr": "0 != ' -0.0 '",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "0 != ''",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "0 != ' '",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "120 != ' +1.2e2 '",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "120.0 != ' 1.2E2 '",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "120 != ' 1.2e+2 '",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "-120 != ' -1.2E+2 '",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "0.012 != ' 1.2e-2 '",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "0.012 != ' 1.2E-2 '",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "255.0 != ' 0xff '",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "8.0 != ' 0o10 '",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "Infinity != ' Infinity '",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "-Infinity != ' -Infinity '",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "NaN != 'NaN'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "1 != '0'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "1 != '1'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "1 != 'a'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "0 != 'a'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_number_null": [
|
||||||
|
{
|
||||||
|
"expr": "0 != null",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "1 != null",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_number_object-array": [
|
||||||
|
{
|
||||||
|
"expr": "0 != test",
|
||||||
|
"contexts": { "test": {} },
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "1 != test",
|
||||||
|
"contexts": { "test": {} },
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "0 != test",
|
||||||
|
"contexts": { "test": [] },
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "1 != test",
|
||||||
|
"contexts": { "test": [] },
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_string_bool": [
|
||||||
|
{
|
||||||
|
"expr": "'' != false",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' ' != false",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' 0.0 ' != false",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' 1.0 ' != false",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' 1.0 ' != true",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' 1 ' != true",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'-1' != true",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'2' != true",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_string_number": [
|
||||||
|
{
|
||||||
|
"expr": "' -0.0 ' != 0",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'' != 0",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' ' != 0",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' +1.2e2 ' != 120",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' 1.2E2 ' != 120.0",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' 1.2e+2 ' != 120",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' -1.2E+2 ' != -120",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' 1.2e-2 ' != 0.012",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' 1.2E-2 ' != 0.012",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' 0xff ' != 255.0",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' 0o10 ' != 8.0",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' Infinity ' != Infinity",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' -Infinity ' != -Infinity",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'NaN' != NaN",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'0' != 1",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'1' != 1",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'a' != 1",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'a' != 0",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_string_null": [
|
||||||
|
{
|
||||||
|
"expr": "'' != null",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "' ' != null",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'0' != null",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'1' != null",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'false' != null",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'null' != null",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_string_object": [
|
||||||
|
{
|
||||||
|
"expr": "'' != test",
|
||||||
|
"contexts": { "test": {} },
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'false' != test",
|
||||||
|
"contexts": { "test": {} },
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'0' != test",
|
||||||
|
"contexts": { "test": {} },
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'1' != test",
|
||||||
|
"contexts": { "test": {} },
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'null' != test",
|
||||||
|
"contexts": { "test": {} },
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'Object' != test",
|
||||||
|
"contexts": { "test": {} },
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_string_array": [
|
||||||
|
{
|
||||||
|
"expr": "'' != test",
|
||||||
|
"contexts": { "test": [] },
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'false' != test",
|
||||||
|
"contexts": { "test": [] },
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'0' != test",
|
||||||
|
"contexts": { "test": [] },
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'0' != test",
|
||||||
|
"contexts": { "test": [] },
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'null' != test",
|
||||||
|
"contexts": { "test": [] },
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'Array' != test",
|
||||||
|
"contexts": { "test": [] },
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_null_bool": [
|
||||||
|
{
|
||||||
|
"expr": "null != false",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "null != true",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_null_number": [
|
||||||
|
{
|
||||||
|
"expr": "null != 0",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "null != 1",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_null_string": [
|
||||||
|
{
|
||||||
|
"expr": "null != ''",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "null != ' '",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "null != '0'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "null != '1'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "null != 'false'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "null != 'null'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"coerce_null_object-array": [
|
||||||
|
{
|
||||||
|
"expr": "null != test",
|
||||||
|
"contexts": { "test": {} },
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "null != test",
|
||||||
|
"contexts": { "test": [] },
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"not": [
|
||||||
|
{ "expr": "!!true", "result": { "kind": "Boolean", "value": true } },
|
||||||
|
{ "expr": "!!false", "result": { "kind": "Boolean", "value": false } },
|
||||||
|
{ "expr": "!!1", "result": { "kind": "Boolean", "value": true } },
|
||||||
|
{ "expr": "!!.5", "result": { "kind": "Boolean", "value": true } },
|
||||||
|
{ "expr": "!!0.5", "result": { "kind": "Boolean", "value": true } },
|
||||||
|
{ "expr": "!!2", "result": { "kind": "Boolean", "value": true } },
|
||||||
|
{ "expr": "!!-1", "result": { "kind": "Boolean", "value": true } },
|
||||||
|
{ "expr": "!!-.5", "result": { "kind": "Boolean", "value": true } },
|
||||||
|
{ "expr": "!!-2", "result": { "kind": "Boolean", "value": true } },
|
||||||
|
{ "expr": "!!-Infinity", "result": { "kind": "Boolean", "value": true } },
|
||||||
|
{ "expr": "!!Infinity", "result": { "kind": "Boolean", "value": true } },
|
||||||
|
{ "expr": "!!NaN", "result": { "kind": "Boolean", "value": false } },
|
||||||
|
{ "expr": "!!0", "result": { "kind": "Boolean", "value": false } },
|
||||||
|
{ "expr": "!!0.0", "result": { "kind": "Boolean", "value": false } },
|
||||||
|
{ "expr": "!!-0", "result": { "kind": "Boolean", "value": false } },
|
||||||
|
{ "expr": "!!-0.0", "result": { "kind": "Boolean", "value": false } },
|
||||||
|
{ "expr": "!!'a'", "result": { "kind": "Boolean", "value": true } },
|
||||||
|
{ "expr": "!!'false'", "result": { "kind": "Boolean", "value": true } },
|
||||||
|
{ "expr": "!!'0'", "result": { "kind": "Boolean", "value": true } },
|
||||||
|
{ "expr": "!!' '", "result": { "kind": "Boolean", "value": true } },
|
||||||
|
{ "expr": "!!''", "result": { "kind": "Boolean", "value": false } },
|
||||||
|
{ "expr": "!!fromJson('[]')", "result": { "kind": "Boolean", "value": true } },
|
||||||
|
{ "expr": "!!fromJson('{}')", "result": { "kind": "Boolean", "value": true } },
|
||||||
|
{ "expr": "!!null", "result": { "kind": "Boolean", "value": false } }
|
||||||
|
]
|
||||||
|
}
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"or": [
|
||||||
|
{
|
||||||
|
"expr": "true || true || true",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{ "expr": "true || true", "result": { "kind": "Boolean", "value": true } },
|
||||||
|
{
|
||||||
|
"expr": "true || true || false",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{ "expr": "true || false", "result": { "kind": "Boolean", "value": true } },
|
||||||
|
{ "expr": "false || true", "result": { "kind": "Boolean", "value": true } },
|
||||||
|
{
|
||||||
|
"expr": "false || false",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "0 || 0 || 2 || 3",
|
||||||
|
"result": { "kind": "Number", "value": 2.0 }
|
||||||
|
},
|
||||||
|
{ "expr": "false || 0", "result": { "kind": "Number", "value": 0.0 } },
|
||||||
|
{
|
||||||
|
"expr": "false || '' || 'a' || 'b'",
|
||||||
|
"result": { "kind": "String", "value": "a" }
|
||||||
|
},
|
||||||
|
{ "expr": "false || ''", "result": { "kind": "String", "value": "" } },
|
||||||
|
{ "expr": "null || true", "result": { "kind": "Boolean", "value": true } },
|
||||||
|
{ "expr": "false || null", "result": { "kind": "Null", "value": null } }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,376 @@
|
|||||||
|
{
|
||||||
|
"lower-vs-upper": [
|
||||||
|
{
|
||||||
|
"expr": "'abcdefghijklmnopqrstuvwxyz' == 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'abcdefghijklmnopqrstuvwxyz' != 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'abcdefghijklmnopqrstuvwxyz' < 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'abcdefghijklmnopqrstuvwxyz' <= 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'abcdefghijklmnopqrstuvwxyz' > 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'abcdefghijklmnopqrstuvwxyz' >= 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"upper-vs-lower": [
|
||||||
|
{
|
||||||
|
"expr": "'ABCDEFGHIJKLMNOPQRSTUVWXYZ' == 'abcdefghijklmnopqrstuvwxyz'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'ABCDEFGHIJKLMNOPQRSTUVWXYZ' != 'abcdefghijklmnopqrstuvwxyz'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'ABCDEFGHIJKLMNOPQRSTUVWXYZ' < 'abcdefghijklmnopqrstuvwxyz'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'ABCDEFGHIJKLMNOPQRSTUVWXYZ' <= 'abcdefghijklmnopqrstuvwxyz'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'ABCDEFGHIJKLMNOPQRSTUVWXYZ' > 'abcdefghijklmnopqrstuvwxyz'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'ABCDEFGHIJKLMNOPQRSTUVWXYZ' >= 'abcdefghijklmnopqrstuvwxyz'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"a-z-equivalent-to-A-Z-wrt-chars-between-Z-and-a": [
|
||||||
|
{
|
||||||
|
"expr": "'A' < '['",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'A' <= '['",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'A' > '['",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'A' >= '['",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'a' < '['",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'a' <= '['",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'a' > '['",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'a' >= '['",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"case-insensitive-ς-(final-lowercase-sigma)-Σ-(capital-sigma)-σ-(non-final-sigma)": [
|
||||||
|
{
|
||||||
|
"expr": "'ς' == 'Σ'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'ς' != 'Σ'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'ς' < 'Σ'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'ς' <= 'Σ'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'ς' > 'Σ'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'ς' >= 'Σ'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'ς' == 'σ'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'ς' != 'σ'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'ς' < 'σ'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'ς' <= 'σ'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'ς' > 'σ'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'ς' >= 'σ'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'Σ' == 'σ'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'Σ' != 'σ'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'Σ' < 'σ'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'Σ' <= 'σ'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'Σ' > 'σ'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'Σ' >= 'σ'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"case-insensitive-ü-Ü": [
|
||||||
|
{
|
||||||
|
"expr": "'ü' == 'Ü'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'ü' != 'Ü'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'ü' < 'Ü'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'ü' <= 'Ü'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'ü' > 'Ü'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'ü' >= 'Ü'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'Ü' == 'ü'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'Ü' != 'ü'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'Ü' < 'ü'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'Ü' <= 'ü'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'Ü' > 'ü'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'Ü' >= 'ü'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"case-insensitive-ç-Ç": [
|
||||||
|
{
|
||||||
|
"expr": "'ç' == 'Ç'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'ç' != 'Ç'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'ç' < 'Ç'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'ç' <= 'Ç'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'ç' > 'Ç'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'ç' >= 'Ç'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'Ç' == 'ç'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'Ç' != 'ç'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'Ç' < 'ç'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'Ç' <= 'ç'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'Ç' > 'ç'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'Ç' >= 'ç'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"case-sensitive-i-İ": [
|
||||||
|
{
|
||||||
|
"expr": "'i' == 'İ'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'i' != 'İ'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'i' < 'İ'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'i' <= 'İ'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'i' > 'İ'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'i' >= 'İ'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'İ' == 'i'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'İ' != 'i'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'İ' < 'i'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'İ' <= 'i'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'İ' > 'i'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'İ' >= 'i'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"case-sensitive-ı-I": [
|
||||||
|
{
|
||||||
|
"expr": "'ı' == 'I'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'ı' != 'I'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'ı' < 'I'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'ı' <= 'I'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'ı' > 'I'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'ı' >= 'I'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'I' == 'ı'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'I' != 'ı'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'I' < 'ı'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'I' <= 'ı'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'I' > 'ı'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'I' >= 'ı'",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"cyrillic-letters": [
|
||||||
|
{
|
||||||
|
"expr": "'АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЬЭЮЯ' == 'абвгдежзийклмнопрстуфхцчшщьэюя'",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
{
|
||||||
|
"operator_precedence_._and_._evaluate_left_to_right": [
|
||||||
|
{
|
||||||
|
"expr": "foo.bar.baz",
|
||||||
|
"contexts": { "foo": { "bar": { "baz": 2 } } },
|
||||||
|
"result": { "kind": "Number", "value": 2.0 }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"operator_precedence_._is_higher_than_!": [
|
||||||
|
{
|
||||||
|
"expr": "!foo.bar",
|
||||||
|
"contexts": { "foo": { "bar": true } },
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "(!foo).bar",
|
||||||
|
"contexts": { "foo": { "bar": true } },
|
||||||
|
"result": { "kind": "Null", "value": null }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"operator_precedence_._is_higher_than_>": [
|
||||||
|
{
|
||||||
|
"expr": "3 > foo.bar",
|
||||||
|
"contexts": { "foo": { "bar": 2 } },
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo.bar > 2",
|
||||||
|
"contexts": { "foo": { "bar": 3 } },
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"operator_precedence_[]_is_higher_than_>": [
|
||||||
|
{
|
||||||
|
"expr": "3 > foo['bar']",
|
||||||
|
"contexts": { "foo": { "bar": 2 } },
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo['bar'] > 2",
|
||||||
|
"contexts": { "foo": { "bar": 3 } },
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"operator_precedence_!_is_higher_than_<": [
|
||||||
|
{
|
||||||
|
"expr": "!2 < 3",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "!(2 < 3)",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"operator_precedence_<_and_<_evaluate_left_to_right": [
|
||||||
|
{
|
||||||
|
"expr": "3 < 2 < 1",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "3 < (2 < 1)",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"operator_precedence_<_and_<=_evaluate_left_to_right": [
|
||||||
|
{
|
||||||
|
"expr": "3 < 2 <= 1",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "3 < (2 <= 1)",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"operator_precedence_<=_and_<_evaluate_left_to_right": [
|
||||||
|
{
|
||||||
|
"expr": "3 <= 2 < 1",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "3 <= (2 < 1)",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"operator_precedence_>_and_<_evaluate_left_to_right": [
|
||||||
|
{
|
||||||
|
"expr": "0 > 0 < 1",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "0 > (0 < 1)",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"operator_precedence_>_and_>_evaluate_left_to_right": [
|
||||||
|
{
|
||||||
|
"expr": "2 > 2 > 0",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "2 > (2 > 0)",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"operator_precedence_<=_is_higher_than_==": [
|
||||||
|
{
|
||||||
|
"expr": "2 <= 3 == true",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "2 <= (3 == true)",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"operator_precedence_==_and_==_evaluate_left_to_right": [
|
||||||
|
{
|
||||||
|
"expr": "2 == 2 == 1",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "2 == (2 == 1)",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"operator_precedence_==_and_!=_evaluate_left_to_right": [
|
||||||
|
{
|
||||||
|
"expr": "2 == 2 != 0",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "2 == (2 != 0)",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"operator_precedence_==_is_higher_than_&&": [
|
||||||
|
{
|
||||||
|
"expr": "1 == 1 && 2",
|
||||||
|
"result": { "kind": "Number", "value": 2.0 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "1 == (1 && 2)",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'a' == 'a' && 'b'",
|
||||||
|
"result": { "kind": "String", "value": "b" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'a' == ('a' && 'b')",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"operator_precedence_&&_is_higher_than_||": [
|
||||||
|
{
|
||||||
|
"expr": "false && 0 || null",
|
||||||
|
"result": { "kind": "Null", "value": null }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "false && (0 || null)",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "1 || 2 && 3",
|
||||||
|
"result": { "kind": "Number", "value": 1.0 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "(1 || 2) && 3",
|
||||||
|
"result": { "kind": "Number", "value": 3.0 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+143
@@ -0,0 +1,143 @@
|
|||||||
|
{
|
||||||
|
"basics": [
|
||||||
|
{
|
||||||
|
"expr": "startsWith('abcdef', 'abc')",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "startsWith('abcdef', 'bc')",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "startsWith('abcdef', '')",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "startsWith('1234', 12)",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "startsWith('true', true)",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "startsWith('false', false)",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "startsWith('asdf', null)",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "startsWith('1234', 23)",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "startsWith('true', false)",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "startsWith('true', false, true)",
|
||||||
|
"err": {
|
||||||
|
"kind": "parsing",
|
||||||
|
"value": "Too many parameters supplied: "
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "startsWith('true')",
|
||||||
|
"err": {
|
||||||
|
"kind": "parsing",
|
||||||
|
"value": "Too few parameters supplied: "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"case-insensitive-a-thru-z": [
|
||||||
|
{
|
||||||
|
"expr": "startsWith('ABCDEFGHIJKLMNOPQRSTUVWXYZ_asdf', 'abcdefghijklmnopqrstuvwxyz')",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "startsWith('abcdefghijklmnopqrstuvwxyz_asdf', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"case-insensitive-ς-(final-lowercase-sigma)-Σ-(capital-sigma)-σ-(non-final-sigma)": [
|
||||||
|
{
|
||||||
|
"expr": "startsWith('ς_asdf', 'Σ')",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "startsWith('ς_asdf', 'σ')",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "startsWith('Σ_asdf', 'ς')",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "startsWith('Σ_asdf', 'σ')",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "startsWith('σ_asdf', 'ς')",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "startsWith('σ_asdf', 'Σ')",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"case-insensitive-ü-Ü": [
|
||||||
|
{
|
||||||
|
"expr": "startsWith('ü_asdf', 'Ü')",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "startsWith('Ü_asdf', 'ü')",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"case-insensitive-ç-Ç": [
|
||||||
|
{
|
||||||
|
"expr": "startsWith('ç_asdf', 'Ç')",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "startsWith('Ç_asdf', 'ç')",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"case-sensitive-i-İ": [
|
||||||
|
{
|
||||||
|
"expr": "startsWith('i_asdf', 'İ')",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "startsWith('İ_asdf', 'i')",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"case-sensitive-ı-I": [
|
||||||
|
{
|
||||||
|
"expr": "startsWith('ı_asdf', 'I')",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "startsWith('I_asdf', 'ı')",
|
||||||
|
"result": { "kind": "Boolean", "value": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"cyrillic-letters": [
|
||||||
|
{
|
||||||
|
"expr": "startsWith('АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЬЭЮЯ_asdf', 'абвгдежзийклмнопрстуфхцчшщьэюя')",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+605
@@ -0,0 +1,605 @@
|
|||||||
|
{
|
||||||
|
"parsing-errors": [
|
||||||
|
{
|
||||||
|
"expr": "1 <",
|
||||||
|
"err": {
|
||||||
|
"kind": "parsing",
|
||||||
|
"value": "Unexpected end of expression: '<'. Located at position 3 within expression: 1 <"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "1 < )",
|
||||||
|
"err": {
|
||||||
|
"kind": "parsing",
|
||||||
|
"value": "Unexpected symbol: ')'. Located at position 5 within expression: 1 < )"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "> 1",
|
||||||
|
"err": {
|
||||||
|
"kind": "parsing",
|
||||||
|
"value": "Unexpected symbol: '>'. Located at position 1 within expression: > 1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "github.()",
|
||||||
|
"contexts": { "github": { "sha": "abc123" } },
|
||||||
|
"err": {
|
||||||
|
"kind": "parsing",
|
||||||
|
"value": "Unexpected symbol: '('. Located at position 8 within expression: github.()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "endswith(1, 2 a)",
|
||||||
|
"err": {
|
||||||
|
"kind": "parsing",
|
||||||
|
"value": "Unexpected symbol: 'a'. Located at position 15 within expression: endswith(1, 2 a)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "(1,",
|
||||||
|
"err": {
|
||||||
|
"kind": "parsing",
|
||||||
|
"value": "Unexpected symbol: ','. Located at position 3 within expression: (1,"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "1 2",
|
||||||
|
"err": {
|
||||||
|
"kind": "parsing",
|
||||||
|
"value": "Unexpected symbol: '2'. Located at position 3 within expression: 1 2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "(1)2",
|
||||||
|
"err": {
|
||||||
|
"kind": "parsing",
|
||||||
|
"value": "Unexpected symbol: '2'. Located at position 4 within expression: (1)2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "tojson(1)2",
|
||||||
|
"err": {
|
||||||
|
"kind": "parsing",
|
||||||
|
"value": "Unexpected symbol: '2'. Located at position 10 within expression: tojson(1)2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo 2",
|
||||||
|
"contexts": { "foo": "bar" },
|
||||||
|
"err": {
|
||||||
|
"kind": "parsing",
|
||||||
|
"value": "Unexpected symbol: '2'. Located at position 5 within expression: foo 2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo.bar 2",
|
||||||
|
"contexts": { "foo": "bar" },
|
||||||
|
"err": {
|
||||||
|
"kind": "parsing",
|
||||||
|
"value": "Unexpected symbol: '2'. Located at position 9 within expression: foo.bar 2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo['bar']2",
|
||||||
|
"contexts": { "foo": "bar" },
|
||||||
|
"err": {
|
||||||
|
"kind": "parsing",
|
||||||
|
"value": "Unexpected symbol: '2'. Located at position 11 within expression: foo['bar']2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "nonExistentFunction()",
|
||||||
|
"err": {
|
||||||
|
"kind": "parsing",
|
||||||
|
"value": "Unrecognized function: 'nonExistentFunction'. Located at position 1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "false && nonExistentFunction()",
|
||||||
|
"err": {
|
||||||
|
"kind": "parsing",
|
||||||
|
"value": "Unrecognized function: 'nonExistentFunction'. Located at position 1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "1 = < 2",
|
||||||
|
"err": {
|
||||||
|
"kind": "lexing",
|
||||||
|
"value": "Unexpected symbol: '='. Located at position 3 within expression: 1 = < 2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "1 & < 2",
|
||||||
|
"err": {
|
||||||
|
"kind": "lexing",
|
||||||
|
"value": "Unexpected symbol: '&'. Located at position 3 within expression: 1 & < 2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "1 | < 2",
|
||||||
|
"err": {
|
||||||
|
"kind": "lexing",
|
||||||
|
"value": "Unexpected symbol: '|'. Located at position 3 within expression: 1 | < 2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "1 =2 =3",
|
||||||
|
"err": {
|
||||||
|
"kind": "lexing",
|
||||||
|
"value": "Unexpected symbol: '=2'. Located at position 3 within expression: 1 =2 =3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "1 &2 &3",
|
||||||
|
"err": {
|
||||||
|
"kind": "lexing",
|
||||||
|
"value": "Unexpected symbol: '&2'. Located at position 3 within expression: 1 &2 &3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "1 |2 |3",
|
||||||
|
"err": {
|
||||||
|
"kind": "lexing",
|
||||||
|
"value": "Unexpected symbol: '|2'. Located at position 3 within expression: 1 |2 |3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "1 @abc 2",
|
||||||
|
"err": {
|
||||||
|
"kind": "lexing",
|
||||||
|
"value": "Unexpected symbol: '@abc'. Located at position 3 within expression: 1 @abc 2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "'foo",
|
||||||
|
"err": {
|
||||||
|
"kind": "lexing",
|
||||||
|
"value": "Unexpected symbol: ''foo'. Located at position 1 within expression: 'foo"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "fromjson(a,b ",
|
||||||
|
"err": {
|
||||||
|
"kind": "parsing",
|
||||||
|
"value": "Unrecognized named-value: 'a'. Located at position 10 within expression: fromjson(a,b "
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": " true ||( false && false",
|
||||||
|
"err": {
|
||||||
|
"kind": "parsing",
|
||||||
|
"value": "Unexpected end of expression: 'false'. Located at position 20 within expression: true ||( false && false"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "😊 Emoji!! (unicode)",
|
||||||
|
"err": {
|
||||||
|
"kind": "lexing",
|
||||||
|
"value": "Unexpected symbol: '😊'. Located at position 1 within expression: 😊 Emoji!! (unicode)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "github.event_name == 'push'\n && !contains(env.COMMIT_MESSAGES)",
|
||||||
|
"contexts": { "github": { "event_name": "push" }},
|
||||||
|
"err": {
|
||||||
|
"kind": "parsing",
|
||||||
|
"value": "Unrecognized named-value: 'env'. Located at position 43 within expression: github.event_name == 'push'\n && !contains(env.COMMIT_MESSAGES)"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"skip": ["typescript"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "github.event_name == 'push' \n&& true\n&& !contains(env.COMMIT_MESSAGES)",
|
||||||
|
"contexts": { "github": { "event_name": "push" }},
|
||||||
|
"err": {
|
||||||
|
"kind": "parsing",
|
||||||
|
"value": "Unrecognized named-value: 'env'. Located at position 51 within expression: github.event_name == 'push' \n&& true\n&& !contains(env.COMMIT_MESSAGES)"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"skip": ["typescript"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "github.event_name == 'push' \n&& true\n&& true\n&& !contains(env.COMMIT_MESSAGES)",
|
||||||
|
"contexts": { "github": { "event_name": "push" }},
|
||||||
|
"err": {
|
||||||
|
"kind": "parsing",
|
||||||
|
"value": "Unrecognized named-value: 'env'. Located at position 59 within expression: github.event_name == 'push' \n&& true\n&& true\n&& !contains(env.COMMIT_MESSAGES)"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"skip": ["typescript"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "github.event_name == 'push' \n&& true\n&& true\n&& true\n&& !contains(env.COMMIT_MESSAGES)",
|
||||||
|
"contexts": { "github": { "event_name": "push" }},
|
||||||
|
"err": {
|
||||||
|
"kind": "parsing",
|
||||||
|
"value": "Unrecognized named-value: 'env'. Located at position 67 within expression: github.event_name == 'push' \n&& true\n&& true\n&& true\n&& !contains(env.COMMIT_MESSAGES)"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"skip": ["typescript"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "1 && ..github/workflow.md",
|
||||||
|
"err": {
|
||||||
|
"kind": "lexing",
|
||||||
|
"value": "Unexpected symbol: '..github/workflow.md'. Located at position 6 within expression: 1 && ..github/workflow.md"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "..github/workflow.md",
|
||||||
|
"err": {
|
||||||
|
"kind": "lexing",
|
||||||
|
"value": "Unexpected symbol: '..github/workflow.md'. Located at position 1 within expression: ..github/workflow.md"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": ".github/workflow.md",
|
||||||
|
"err": {
|
||||||
|
"kind": "lexing",
|
||||||
|
"value": "Unexpected symbol: '.github/workflow.md'. Located at position 1 within expression: .github/workflow.md"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": ".github/workflow",
|
||||||
|
"err": {
|
||||||
|
"kind": "lexing",
|
||||||
|
"value": "Unexpected symbol: '.github/workflow'. Located at position 1 within expression: .github/workflow"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "github/workflow.md",
|
||||||
|
"err": {
|
||||||
|
"kind": "lexing",
|
||||||
|
"value": "Unexpected symbol: 'github/workflow'. Located at position 1 within expression: github/workflow.md"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "..github/workflow.md && 1",
|
||||||
|
"err": {
|
||||||
|
"kind": "lexing",
|
||||||
|
"value": "Unexpected symbol: '..github/workflow.md'. Located at position 1 within expression: ..github/workflow.md && 1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"depth-errors": [
|
||||||
|
{
|
||||||
|
"expr": "!!!!! !!!!! !!!!! !!!!! !!!!! !!!!! !!!!! !!!!! !!!!! !!!!! false",
|
||||||
|
"err": {
|
||||||
|
"kind": "parsing",
|
||||||
|
"value": "Exceeded max expression depth 50"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "!!!!! !!!!! !!!!! !!!!! !!!!! !!!!! !!!!! !!!!! !!!!! !!!! false",
|
||||||
|
"result": { "kind": "Boolean", "value": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo._1._2._3._4._5._6._7._8._9._10._11._12._13._14._15._16._17._18._19._20._21._22._23._24._25._26._27._28._29._30._31._32._33._34._35._36._37._38._39._40._41._42._43._44._45._46._47._48._49._50",
|
||||||
|
"contexts": { "foo": null },
|
||||||
|
"err": {
|
||||||
|
"kind": "parsing",
|
||||||
|
"value": "Exceeded max expression depth 50"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo._1._2._3._4._5._6._7._8._9._10._11._12._13._14._15._16._17._18._19._20._21._22._23._24._25._26._27._28._29._30._31._32._33._34._35._36._37._38._39._40._41._42._43._44._45._46._47._48._49",
|
||||||
|
"contexts": { "foo": null },
|
||||||
|
"result": { "kind": "Null", "value": null }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo .*.*.*.*.* .*.*.*.*.* .*.*.*.*.* .*.*.*.*.* .*.*.*.*.* .*.*.*.*.* .*.*.*.*.* .*.*.*.*.* .*.*.*.*.* .*.*.*.*.*",
|
||||||
|
"contexts": { "foo": null },
|
||||||
|
"err": {
|
||||||
|
"kind": "parsing",
|
||||||
|
"value": "Exceeded max expression depth 50"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo .*.*.*.*.* .*.*.*.*.* .*.*.*.*.* .*.*.*.*.* .*.*.*.*.* .*.*.*.*.* .*.*.*.*.* .*.*.*.*.* .*.*.*.*.* .*.*.*.*",
|
||||||
|
"contexts": { "foo": null },
|
||||||
|
"result": { "kind": "Array", "value": [] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20][21][22][23][24][25][26][27][28][29][30][31][32][33][34][35][36][37][38][39][40][41][42][43][44][45][46][47][48][49][50]",
|
||||||
|
"contexts": { "foo": null },
|
||||||
|
"err": {
|
||||||
|
"kind": "parsing",
|
||||||
|
"value": "Exceeded max expression depth 50"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20][21][22][23][24][25][26][27][28][29][30][31][32][33][34][35][36][37][38][39][40][41][42][43][44][45][46][47][48][49]",
|
||||||
|
"contexts": { "foo": null },
|
||||||
|
"result": { "kind": "Null", "value": null }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo [*][*][*][*][*] [*][*][*][*][*] [*][*][*][*][*] [*][*][*][*][*] [*][*][*][*][*] [*][*][*][*][*] [*][*][*][*][*] [*][*][*][*][*] [*][*][*][*][*] [*][*][*][*][*]",
|
||||||
|
"contexts": { "foo": null },
|
||||||
|
"err": {
|
||||||
|
"kind": "parsing",
|
||||||
|
"value": "Exceeded max expression depth 50"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "foo [*][*][*][*][*] [*][*][*][*][*] [*][*][*][*][*] [*][*][*][*][*] [*][*][*][*][*] [*][*][*][*][*] [*][*][*][*][*] [*][*][*][*][*] [*][*][*][*][*] [*][*][*][*]",
|
||||||
|
"contexts": { "foo": null },
|
||||||
|
"result": { "kind": "Array", "value": [] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "fromjson(tojson(fromjson(tojson(fromjson(tojson(fromjson(tojson(fromjson(tojson( fromjson(tojson(fromjson(tojson(fromjson(tojson(fromjson(tojson(fromjson(tojson( fromjson(tojson(fromjson(tojson(fromjson(tojson(fromjson(tojson(fromjson(tojson( fromjson(tojson(fromjson(tojson(fromjson(tojson(fromjson(tojson(fromjson(tojson( fromjson(tojson(fromjson(tojson(fromjson(tojson(fromjson(tojson(fromjson(tojson( 1 )))))))))) )))))))))) )))))))))) )))))))))) ))))))))))",
|
||||||
|
"err": {
|
||||||
|
"kind": "parsing",
|
||||||
|
"value": "Exceeded max expression depth 50"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "fromjson(tojson(fromjson(tojson(fromjson(tojson(fromjson(tojson(fromjson(tojson( fromjson(tojson(fromjson(tojson(fromjson(tojson(fromjson(tojson(fromjson(tojson( fromjson(tojson(fromjson(tojson(fromjson(tojson(fromjson(tojson(fromjson(tojson( fromjson(tojson(fromjson(tojson(fromjson(tojson(fromjson(tojson(fromjson(tojson( fromjson(tojson(fromjson(tojson(fromjson(tojson(fromjson(tojson(fromjson( '1' ))))))))) )))))))))) )))))))))) )))))))))) ))))))))))",
|
||||||
|
"result": { "kind": "Number", "value": 1 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('depth3'||format('depth5'||format('depth7'||format('depth9'||format('depth11'||format('depth13'||format('depth15'||format('depth17'||format('depth19'||format('depth21'||format('depth23'||format('depth25'||format('depth27'||format('depth29'||format('depth31'||format('depth33'||format('depth35'||format('depth37'||format('depth39'||format('depth41'||format('depth43'||format('depth45'||format('depth47'||format('depth49'||format('depth51'||'depth51')))))))))) )))))))))) )))))",
|
||||||
|
"err": {
|
||||||
|
"kind": "parsing",
|
||||||
|
"value": "Exceeded max expression depth 50"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('depth3'||format('depth5'||format('depth7'||format('depth9'||format('depth11'||format('depth13'||format('depth15'||format('depth17'||format('depth19'||format('depth21'||format('depth23'||format('depth25'||format('depth27'||format('depth29'||format('depth31'||format('depth33'||format('depth35'||format('depth37'||format('depth39'||format('depth41'||format('depth43'||format('depth45'||format('depth47'||format('depth49'||format('depth50')))))))))) )))))))))) )))))",
|
||||||
|
"result": { "kind": "String", "value": "depth3" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('depth3'&&format('depth5'&&format('depth7'&&format('depth9'&&format('depth11'&&format('depth13'&&format('depth15'&&format('depth17'&&format('depth19'&&format('depth21'&&format('depth23'&&format('depth25'&&format('depth27'&&format('depth29'&&format('depth31'&&format('depth33'&&format('depth35'&&format('depth37'&&format('depth39'&&format('depth41'&&format('depth43'&&format('depth45'&&format('depth47'&&format('depth49'&&format('depth51'&&'depth51')))))))))) )))))))))) )))))",
|
||||||
|
"err": {
|
||||||
|
"kind": "parsing",
|
||||||
|
"value": "Exceeded max expression depth 50"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "format('depth3'&&format('depth5'&&format('depth7'&&format('depth9'&&format('depth11'&&format('depth13'&&format('depth15'&&format('depth17'&&format('depth19'&&format('depth21'&&format('depth23'&&format('depth25'&&format('depth27'&&format('depth29'&&format('depth31'&&format('depth33'&&format('depth35'&&format('depth37'&&format('depth39'&&format('depth41'&&format('depth43'&&format('depth45'&&format('depth47'&&format('depth49'&&format('depth50')))))))))) )))))))))) )))))",
|
||||||
|
"result": { "kind": "String", "value": "depth50" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "1 || 2 || 3 || 4 || 5 || 6 || 7 || 8 || 9 || 10 || 11 || 12 || 13 || 14 || 15 || 16 || 17 || 18 || 19 || 20 || 21 || 22 || 23 || 24 || 25 || 26 || 27 || 28 || 29 || 30 || 31 || 32 || 33 || 34 || 35 || 36 || 37 || 38 || 39 || 40 || 41 || 42 || 43 || 44 || 45 || 46 || 47 || 48 || 49 || 50 || 51 || 52 || 53 || 54 || 55 || 56 || 57 || 58 || 59 || 60",
|
||||||
|
"result": { "kind": "Number", "value": 1 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "1 && 2 && 3 && 4 && 5 && 6 && 7 && 8 && 9 && 10 && 11 && 12 && 13 && 14 && 15 && 16 && 17 && 18 && 19 && 20 && 21 && 22 && 23 && 24 && 25 && 26 && 27 && 28 && 29 && 30 && 31 && 32 && 33 && 34 && 35 && 36 && 37 && 38 && 39 && 40 && 41 && 42 && 43 && 44 && 45 && 46 && 47 && 48 && 49 && 50 && 51 && 52 && 53 && 54 && 55 && 56 && 57 && 58 && 59 && 60",
|
||||||
|
"result": { "kind": "Number", "value": 60 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "1 && 2 || 3 && 4 || 5 && 6 || 7 && 8 || 9 && 10 || 11 && 12 || 13 && 14 || 15 && 16 || 17 && 18 || 19 && 20 || 21 && 22 || 23 && 24 || 25 && 26 || 27 && 28 || 29 && 30 || 31 && 32 || 33 && 34 || 35 && 36 || 37 && 38 || 39 && 40 || 41 && 42 || 43 && 44 || 45 && 46 || 47 && 48 || 49 && 50 || 51 && 52 || 53 && 54 || 55 && 56 || 57 && 58 || 59 && 60",
|
||||||
|
"result": { "kind": "Number", "value": 2 }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"memory-errors": [
|
||||||
|
{
|
||||||
|
"options": {
|
||||||
|
"skip": ["typescript"]
|
||||||
|
},
|
||||||
|
"expr": "startswith(format('{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}', tojson(github)), format('{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}', tojson(github)))",
|
||||||
|
"contexts": {
|
||||||
|
"github": {
|
||||||
|
"ref": "refs/heads/main",
|
||||||
|
"sha": "2ce6b095f4cceb616efed52266e3e3c0367ba795",
|
||||||
|
"repository": "monalisa/testing",
|
||||||
|
"repository_owner": "monalisa",
|
||||||
|
"repository_owner_id": "12102068",
|
||||||
|
"repositoryUrl": "git://github.com/monalisa/testing.git",
|
||||||
|
"run_id": "2827197126",
|
||||||
|
"run_number": "845",
|
||||||
|
"retention_days": "90",
|
||||||
|
"run_attempt": "1",
|
||||||
|
"artifact_cache_size_limit": "10",
|
||||||
|
"repository_visibility": "public",
|
||||||
|
"repository_id": "1",
|
||||||
|
"actor_id": "1",
|
||||||
|
"actor": "monalisa",
|
||||||
|
"triggering_actor": "monalisa",
|
||||||
|
"workflow": "CI",
|
||||||
|
"head_ref": "",
|
||||||
|
"base_ref": "",
|
||||||
|
"event_name": "push",
|
||||||
|
"event": {
|
||||||
|
"after": "2ce6b095f4cceb616efed52266e3e3c0367ba795",
|
||||||
|
"base_ref": null,
|
||||||
|
"before": "bbe29778fdad33232cd67bd84205d334b94412d7",
|
||||||
|
"commits": [
|
||||||
|
{
|
||||||
|
"author": {
|
||||||
|
"email": "[email protected]",
|
||||||
|
"name": "mona",
|
||||||
|
"username": "monalisa"
|
||||||
|
},
|
||||||
|
"committer": {
|
||||||
|
"email": "[email protected]",
|
||||||
|
"name": "GitHub",
|
||||||
|
"username": "web-flow"
|
||||||
|
},
|
||||||
|
"distinct": true,
|
||||||
|
"id": "2ce6b095f4cceb616efed52266e3e3c0367ba795",
|
||||||
|
"message": "Update main.yml",
|
||||||
|
"timestamp": "2022-08-09T12:40:12-05:00",
|
||||||
|
"tree_id": "0dfd49aedd0cc103865d00587df3d23aa6f571f7",
|
||||||
|
"url": "https://github.com/monalisa/testing/commit/2ce6b095f4cceb616efed52266e3e3c0367ba795"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"compare": "https://github.com/monalisa/testing/compare/bbe29778fdad...2ce6b095f4cc",
|
||||||
|
"created": false,
|
||||||
|
"deleted": false,
|
||||||
|
"forced": false,
|
||||||
|
"head_commit": {
|
||||||
|
"author": {
|
||||||
|
"email": "[email protected]",
|
||||||
|
"name": "mona",
|
||||||
|
"username": "monalisa"
|
||||||
|
},
|
||||||
|
"committer": {
|
||||||
|
"email": "[email protected]",
|
||||||
|
"name21": "GitHub",
|
||||||
|
"username": "web-flow"
|
||||||
|
},
|
||||||
|
"distinct": true,
|
||||||
|
"id": "2ce6b095f4cceb616efed52266e3e3c0367ba795",
|
||||||
|
"message": "Update main.yml",
|
||||||
|
"timestamp": "2022-08-09T12:40:12-05:00",
|
||||||
|
"tree_id": "0dfd49aedd0cc103865d00587df3d23aa6f571f7",
|
||||||
|
"url": "https://github.com/monalisa/testing/commit/2ce6b095f4cceb616efed52266e3e3c0367ba795"
|
||||||
|
},
|
||||||
|
"pusher": {
|
||||||
|
"email": "[email protected]",
|
||||||
|
"name": "monalisa"
|
||||||
|
},
|
||||||
|
"ref": "refs/heads/main",
|
||||||
|
"repository": {
|
||||||
|
"allow_forking": true,
|
||||||
|
"archive_url": "https://api.github.com/repos/monalisa/testing/{archive_format}{/ref}",
|
||||||
|
"archived": false,
|
||||||
|
"assignees_url": "https://api.github.com/repos/monalisa/testing/assignees{/user}",
|
||||||
|
"blobs_url": "https://api.github.com/repos/monalisa/testing/git/blobs{/sha}",
|
||||||
|
"branches_url": "https://api.github.com/repos/monalisa/testing/branches{/branch}",
|
||||||
|
"clone_url": "https://github.com/monalisa/testing.git",
|
||||||
|
"collaborators_url": "https://api.github.com/repos/monalisa/testing/collaborators{/collaborator}",
|
||||||
|
"comments_url": "https://api.github.com/repos/monalisa/testing/comments{/number}",
|
||||||
|
"commits_url": "https://api.github.com/repos/monalisa/testing/commits{/sha}",
|
||||||
|
"compare_url": "https://api.github.com/repos/monalisa/testing/compare/{base}...{head}",
|
||||||
|
"contents_url": "https://api.github.com/repos/monalisa/testing/contents/{+path}",
|
||||||
|
"contributors_url": "https://api.github.com/repos/monalisa/testing/contributors",
|
||||||
|
"created_at": 1516230842,
|
||||||
|
"default_branch": "main",
|
||||||
|
"deployments_url": "https://api.github.com/repos/monalisa/testing/deployments",
|
||||||
|
"description": null,
|
||||||
|
"disabled": false,
|
||||||
|
"downloads_url": "https://api.github.com/repos/monalisa/testing/downloads",
|
||||||
|
"events_url": "https://api.github.com/repos/monalisa/testing/events",
|
||||||
|
"fork": false,
|
||||||
|
"forks": 1,
|
||||||
|
"forks_count": 1,
|
||||||
|
"forks_url": "https://api.github.com/repos/monalisa/testing/forks",
|
||||||
|
"full_name": "monalisa/testing",
|
||||||
|
"git_commits_url": "https://api.github.com/repos/monalisa/testing/git/commits{/sha}",
|
||||||
|
"git_refs_url": "https://api.github.com/repos/monalisa/testing/git/refs{/sha}",
|
||||||
|
"git_tags_url": "https://api.github.com/repos/monalisa/testing/git/tags{/sha}",
|
||||||
|
"git_url": "git://github.com/monalisa/testing.git",
|
||||||
|
"has_downloads": true,
|
||||||
|
"has_issues": true,
|
||||||
|
"has_pages": false,
|
||||||
|
"has_projects": true,
|
||||||
|
"has_wiki": true,
|
||||||
|
"homepage": null,
|
||||||
|
"hooks_url": "https://api.github.com/repos/monalisa/testing/hooks",
|
||||||
|
"html_url": "https://github.com/monalisa/testing",
|
||||||
|
"id": 117904191,
|
||||||
|
"is_template": false,
|
||||||
|
"issue_comment_url": "https://api.github.com/repos/monalisa/testing/issues/comments{/number}",
|
||||||
|
"issue_events_url": "https://api.github.com/repos/monalisa/testing/issues/events{/number}",
|
||||||
|
"issues_url": "https://api.github.com/repos/monalisa/testing/issues{/number}",
|
||||||
|
"keys_url": "https://api.github.com/repos/monalisa/testing/keys{/key_id}",
|
||||||
|
"labels_url": "https://api.github.com/repos/monalisa/testing/labels{/name}",
|
||||||
|
"language": "Shell",
|
||||||
|
"languages_url": "https://api.github.com/repos/monalisa/testing/languages",
|
||||||
|
"license": null,
|
||||||
|
"master_branch": "main",
|
||||||
|
"merges_url": "https://api.github.com/repos/monalisa/testing/merges",
|
||||||
|
"milestones_url": "https://api.github.com/repos/monalisa/testing/milestones{/number}",
|
||||||
|
"mirror_url": null,
|
||||||
|
"name": "testing",
|
||||||
|
"node_id": "MDEwOlJlcG9zaXRvcnkxMTc5MDQxOTE=",
|
||||||
|
"notifications_url": "https://api.github.com/repos/monalisa/testing/notifications{?since,all,participating}",
|
||||||
|
"open_issues": 5,
|
||||||
|
"open_issues_count": 5,
|
||||||
|
"owner": {
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/12102068?v=4",
|
||||||
|
"email": "[email protected]",
|
||||||
|
"events_url": "https://api.github.com/users/monalisa/events{/privacy}",
|
||||||
|
"followers_url": "https://api.github.com/users/monalisa/followers",
|
||||||
|
"following_url": "https://api.github.com/users/monalisa/following{/other_user}",
|
||||||
|
"gists_url": "https://api.github.com/users/monalisa/gists{/gist_id}",
|
||||||
|
"gravatar_id": "",
|
||||||
|
"html_url": "https://github.com/monalisa",
|
||||||
|
"id": 12102068,
|
||||||
|
"login": "monalisa",
|
||||||
|
"name": "monalisa",
|
||||||
|
"node_id": "MDQ6VXNlcjEyMTAyMDY4",
|
||||||
|
"organizations_url": "https://api.github.com/users/monalisa/orgs",
|
||||||
|
"received_events_url": "https://api.github.com/users/monalisa/received_events",
|
||||||
|
"repos_url": "https://api.github.com/users/monalisa/repos",
|
||||||
|
"site_admin": true,
|
||||||
|
"starred_url": "https://api.github.com/users/monalisa/starred{/owner}{/repo}",
|
||||||
|
"subscriptions_url": "https://api.github.com/users/monalisa/subscriptions",
|
||||||
|
"type": "User",
|
||||||
|
"url": "https://api.github.com/users/monalisa"
|
||||||
|
},
|
||||||
|
"private": false,
|
||||||
|
"pulls_url": "https://api.github.com/repos/monalisa/testing/pulls{/number}",
|
||||||
|
"pushed_at": 1660066812,
|
||||||
|
"releases_url": "https://api.github.com/repos/monalisa/testing/releases{/id}",
|
||||||
|
"size": 611,
|
||||||
|
"ssh_url": "[email protected]:monalisa/testing.git",
|
||||||
|
"stargazers": 1,
|
||||||
|
"stargazers_count": 1,
|
||||||
|
"stargazers_url": "https://api.github.com/repos/monalisa/testing/stargazers",
|
||||||
|
"statuses_url": "https://api.github.com/repos/monalisa/testing/statuses/{sha}",
|
||||||
|
"subscribers_url": "https://api.github.com/repos/monalisa/testing/subscribers",
|
||||||
|
"subscription_url": "https://api.github.com/repos/monalisa/testing/subscription",
|
||||||
|
"svn_url": "https://github.com/monalisa/testing",
|
||||||
|
"tags_url": "https://api.github.com/repos/monalisa/testing/tags",
|
||||||
|
"teams_url": "https://api.github.com/repos/monalisa/testing/teams",
|
||||||
|
"topics": [],
|
||||||
|
"trees_url": "https://api.github.com/repos/monalisa/testing/git/trees{/sha}",
|
||||||
|
"updated_at": "2022-01-05T02:14:32Z",
|
||||||
|
"url": "https://github.com/monalisa/testing",
|
||||||
|
"visibility": "public",
|
||||||
|
"watchers": 1,
|
||||||
|
"watchers_count": 1,
|
||||||
|
"web_commit_signoff_required": false
|
||||||
|
},
|
||||||
|
"sender": {
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/12102068?v=4",
|
||||||
|
"events_url": "https://api.github.com/users/monalisa/events{/privacy}",
|
||||||
|
"followers_url": "https://api.github.com/users/monalisa/followers",
|
||||||
|
"following_url": "https://api.github.com/users/monalisa/following{/other_user}",
|
||||||
|
"gists_url": "https://api.github.com/users/monalisa/gists{/gist_id}",
|
||||||
|
"gravatar_id": "",
|
||||||
|
"html_url": "https://github.com/monalisa",
|
||||||
|
"id": 12102068,
|
||||||
|
"login": "monalisa",
|
||||||
|
"node_id": "MDQ6VXNlcjEyMTAyMDY4",
|
||||||
|
"organizations_url": "https://api.github.com/users/monalisa/orgs",
|
||||||
|
"received_events_url": "https://api.github.com/users/monalisa/received_events",
|
||||||
|
"repos_url": "https://api.github.com/users/monalisa/repos",
|
||||||
|
"site_admin": true,
|
||||||
|
"starred_url": "https://api.github.com/users/monalisa/starred{/owner}{/repo}",
|
||||||
|
"subscriptions_url": "https://api.github.com/users/monalisa/subscriptions",
|
||||||
|
"type": "User",
|
||||||
|
"url": "https://api.github.com/users/monalisa"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"server_url": "https://github.com",
|
||||||
|
"api_url": "https://api.github.com",
|
||||||
|
"graphql_url": "https://api.github.com/graphql",
|
||||||
|
"ref_name": "main",
|
||||||
|
"ref_protected": false,
|
||||||
|
"ref_type": "branch",
|
||||||
|
"secret_source": "Actions",
|
||||||
|
"workspace": "/home/runner/work/testing/testing",
|
||||||
|
"action": "__run_2",
|
||||||
|
"event_path": "/home/runner/work/_temp/_github_workflow/event.json",
|
||||||
|
"action_repository": "",
|
||||||
|
"action_ref": "",
|
||||||
|
"path": "/home/runner/work/_temp/_runner_file_commands/add_path_04146e5b-5a26-490d-b886-886641aa5651",
|
||||||
|
"env": "/home/runner/work/_temp/_runner_file_commands/set_env_04146e5b-5a26-490d-b886-886641aa5651",
|
||||||
|
"step_summary": "/home/runner/work/_temp/_runner_file_commands/step_summary_04146e5b-5a26-490d-b886-886641aa5651"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"err": {
|
||||||
|
"kind": "evaluation",
|
||||||
|
"value": "The maximum allowed memory size was exceeded"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+223
@@ -0,0 +1,223 @@
|
|||||||
|
{
|
||||||
|
"basics": [
|
||||||
|
{
|
||||||
|
"expr": "tojson(null)",
|
||||||
|
"result": {
|
||||||
|
"kind": "String",
|
||||||
|
"value": "null"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "tojson(true)",
|
||||||
|
"result": {
|
||||||
|
"kind": "String",
|
||||||
|
"value": "true"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "tojson(false)",
|
||||||
|
"result": {
|
||||||
|
"kind": "String",
|
||||||
|
"value": "false"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "tojson(0)",
|
||||||
|
"result": {
|
||||||
|
"kind": "String",
|
||||||
|
"value": "0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "tojson(-0)",
|
||||||
|
"result": {
|
||||||
|
"kind": "String",
|
||||||
|
"value": "0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "tojson(123456789)",
|
||||||
|
"result": {
|
||||||
|
"kind": "String",
|
||||||
|
"value": "123456789"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "tojson(-123456789)",
|
||||||
|
"result": {
|
||||||
|
"kind": "String",
|
||||||
|
"value": "-123456789"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "tojson(1234.5)",
|
||||||
|
"result": {
|
||||||
|
"kind": "String",
|
||||||
|
"value": "1234.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "tojson(-1234.5)",
|
||||||
|
"result": {
|
||||||
|
"kind": "String",
|
||||||
|
"value": "-1234.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "tojson('')",
|
||||||
|
"result": {
|
||||||
|
"kind": "String",
|
||||||
|
"value": "\"\""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "tojson('abc')",
|
||||||
|
"result": {
|
||||||
|
"kind": "String",
|
||||||
|
"value": "\"abc\""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "tojson('abc''def')",
|
||||||
|
"result": {
|
||||||
|
"kind": "String",
|
||||||
|
"value": "\"abc'def\""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "tojson('abc\\\"def')",
|
||||||
|
"result": {
|
||||||
|
"kind": "String",
|
||||||
|
"value": "\"abc\\\\\\\"def\""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "tojson(emptyArray)",
|
||||||
|
"contexts": {
|
||||||
|
"emptyArray": []
|
||||||
|
},
|
||||||
|
"result": {
|
||||||
|
"kind": "String",
|
||||||
|
"value": "[]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "tojson(emptyObject)",
|
||||||
|
"contexts": {
|
||||||
|
"emptyObject": {}
|
||||||
|
},
|
||||||
|
"result": {
|
||||||
|
"kind": "String",
|
||||||
|
"value": "{}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"arrays": [
|
||||||
|
{
|
||||||
|
"expr": "tojson(myArray)",
|
||||||
|
"contexts": {
|
||||||
|
"myArray": []
|
||||||
|
},
|
||||||
|
"result": {
|
||||||
|
"kind": "String",
|
||||||
|
"value": "[]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "tojson(myArray)",
|
||||||
|
"contexts": {
|
||||||
|
"myArray": [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": {
|
||||||
|
"kind": "String",
|
||||||
|
"value": "[\n 1,\n 2,\n 3\n]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "tojson(myArray)",
|
||||||
|
"contexts": {
|
||||||
|
"myArray": [
|
||||||
|
[
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"abc",
|
||||||
|
"def",
|
||||||
|
"ghi"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
[],
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": {
|
||||||
|
"kind": "String",
|
||||||
|
"value": "[\n [\n 1,\n 2,\n 3\n ],\n [\n \"abc\",\n \"def\",\n \"ghi\"\n ],\n [\n true,\n false,\n null,\n [],\n {}\n ]\n]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"object": [
|
||||||
|
{
|
||||||
|
"expr": "tojson(myObject)",
|
||||||
|
"contexts": {
|
||||||
|
"myObject": {}
|
||||||
|
},
|
||||||
|
"result": {
|
||||||
|
"kind": "String",
|
||||||
|
"value": "{}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "tojson(myObject)",
|
||||||
|
"contexts": {
|
||||||
|
"myObject": {
|
||||||
|
"one": "value one",
|
||||||
|
"two" : "value two",
|
||||||
|
"three": "value three"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": {
|
||||||
|
"kind": "String",
|
||||||
|
"value": "{\n \"one\": \"value one\",\n \"two\": \"value two\",\n \"three\": \"value three\"\n}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "tojson(myObject)",
|
||||||
|
"contexts": {
|
||||||
|
"myObject": {
|
||||||
|
"nested-one": {
|
||||||
|
"one": 1,
|
||||||
|
"two": 2,
|
||||||
|
"three": 3
|
||||||
|
},
|
||||||
|
"nested-two": {
|
||||||
|
"string one": "value one",
|
||||||
|
"string two": "value two",
|
||||||
|
"string three": "value three"
|
||||||
|
},
|
||||||
|
"nested-three": {
|
||||||
|
"true": true,
|
||||||
|
"false": false,
|
||||||
|
"null": null,
|
||||||
|
"array": [],
|
||||||
|
"object": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": {
|
||||||
|
"kind": "String",
|
||||||
|
"value": "{\n \"nested-one\": {\n \"one\": 1,\n \"two\": 2,\n \"three\": 3\n },\n \"nested-two\": {\n \"string one\": \"value one\",\n \"string two\": \"value two\",\n \"string three\": \"value three\"\n },\n \"nested-three\": {\n \"true\": true,\n \"false\": false,\n \"null\": null,\n \"array\": [],\n \"object\": {}\n }\n}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"exclude": ["./src/**/*.test.ts"],
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"noEmit": false,
|
||||||
|
"outDir": "./dist"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"include": ["src"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "esnext",
|
||||||
|
"target": "ES2020",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true,
|
||||||
|
"moduleResolution": "node"
|
||||||
|
},
|
||||||
|
"watchOptions": {
|
||||||
|
"watchFile": "useFsEvents",
|
||||||
|
"watchDirectory": "useFsEvents",
|
||||||
|
"synchronousWatchDirectory": true,
|
||||||
|
"excludeDirectories": ["**/node_modules", "dist"]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"es2021": true
|
||||||
|
},
|
||||||
|
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 12,
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"plugins": ["@typescript-eslint"],
|
||||||
|
"root": true,
|
||||||
|
"rules": {
|
||||||
|
"@typescript-eslint/no-unused-vars": "off",
|
||||||
|
"@typescript-eslint/no-empty-function": "off",
|
||||||
|
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
"@typescript-eslint/no-non-null-assertion": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
/dist
|
||||||
|
/node_modules
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
@github:registry=https://npm.pkg.github.com
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
/dist
|
||||||
|
/node_modules
|
||||||
|
/src/workflows/workflow-v1.0.json
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"semi": false
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Debug Jest Tests",
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"runtimeArgs": [
|
||||||
|
"--inspect-brk",
|
||||||
|
"${workspaceRoot}/node_modules/jest/bin/jest.js",
|
||||||
|
"--runInBand"
|
||||||
|
],
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"internalConsoleOptions": "neverOpen",
|
||||||
|
"env": {
|
||||||
|
"NODE_OPTIONS": "--experimental-vm-modules"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||||
|
export default {
|
||||||
|
preset: "ts-jest/presets/default-esm",
|
||||||
|
moduleNameMapper: {
|
||||||
|
"^(\\.{1,2}/.*)\\.js$": "$1",
|
||||||
|
},
|
||||||
|
transform: {
|
||||||
|
"^.+\\.tsx?$": [
|
||||||
|
"ts-jest",
|
||||||
|
{
|
||||||
|
useESM: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
{
|
||||||
|
"name": "@github/actions-workflow-parser",
|
||||||
|
"version": "0.0.42",
|
||||||
|
"license": "MIT",
|
||||||
|
"type": "module",
|
||||||
|
"source": "./src/index.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"./*": {
|
||||||
|
"import": "./dist/*.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"typesVersions": {
|
||||||
|
"*": {
|
||||||
|
".": [
|
||||||
|
"dist/index.d.ts"
|
||||||
|
],
|
||||||
|
"*": [
|
||||||
|
"dist/*.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/github/actions-workflow-parser/"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc --build tsconfig.build.json",
|
||||||
|
"clean": "rimraf dist",
|
||||||
|
"format": "prettier --write '**/*.ts'",
|
||||||
|
"format-check": "prettier --check '**/*.ts'",
|
||||||
|
"lint": "eslint **/*.ts",
|
||||||
|
"lint-fix": "eslint --fix **/*.ts",
|
||||||
|
"prepublishOnly": "npm run build && npm run test",
|
||||||
|
"test": "NODE_OPTIONS=\"--experimental-vm-modules\" jest",
|
||||||
|
"test-xlang": "NODE_OPTIONS=\"--experimental-vm-modules\" jest --testPathPattern xlang",
|
||||||
|
"test-watch": "NODE_OPTIONS=\"--experimental-vm-modules\" jest --watch",
|
||||||
|
"watch": "tsc --build tsconfig.build.json --watch"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@github/actions-expressions": "*",
|
||||||
|
"yaml": "^2.0.0-8"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist/**/*"
|
||||||
|
],
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/jest": "^29.0.3",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.40.0",
|
||||||
|
"@typescript-eslint/parser": "^5.40.0",
|
||||||
|
"eslint": "7.32.0",
|
||||||
|
"jest": "^29.0.3",
|
||||||
|
"prettier": "^2.7.1",
|
||||||
|
"rimraf": "^3.0.2",
|
||||||
|
"ts-jest": "^29.0.3",
|
||||||
|
"typescript": "^4.8.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
import { StringToken } from "./templates/tokens"
|
||||||
|
import { isBasicExpression, isString } from "./templates/tokens/type-guards"
|
||||||
|
import { nullTrace } from "./test-utils/null-trace"
|
||||||
|
import { parseWorkflow } from "./workflows/workflow-parser"
|
||||||
|
|
||||||
|
describe("Workflow Expression Parsing", () => {
|
||||||
|
it("preserves original expressions when building format", () => {
|
||||||
|
const result = parseWorkflow(
|
||||||
|
"test.yaml",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: "test.yaml",
|
||||||
|
content: `on: push
|
||||||
|
run-name: Test \${{ github.event_name }} \${{ github.ref }}
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- run: echo 'hello'`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
nullTrace
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result.context.errors.getErrors()).toHaveLength(0)
|
||||||
|
|
||||||
|
const run = result.value!.assertMapping("run")!
|
||||||
|
const runNameMapping = run.get(1)!
|
||||||
|
expect(runNameMapping?.key?.assertString("run-name key").value).toBe(
|
||||||
|
"run-name"
|
||||||
|
)
|
||||||
|
|
||||||
|
const v = runNameMapping.value!
|
||||||
|
expect(v).not.toBeUndefined()
|
||||||
|
|
||||||
|
if (!isBasicExpression(v)) {
|
||||||
|
throw new Error("expected run-name to be a basic expression")
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(v.originalExpressions).toHaveLength(2)
|
||||||
|
expect(v.originalExpressions!.map((x) => x.toDisplayString())).toEqual([
|
||||||
|
"${{ github.event_name }}",
|
||||||
|
"${{ github.ref }}",
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("preserves original expressions when building format for multi-line strings", () => {
|
||||||
|
const result = parseWorkflow(
|
||||||
|
"test.yaml",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: "test.yaml",
|
||||||
|
content: `on: push
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- run: |
|
||||||
|
echo \${{ github.event_name }}
|
||||||
|
echo 'hello' \${{ github.ref }}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
nullTrace
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result.context.errors.getErrors()).toHaveLength(0)
|
||||||
|
|
||||||
|
const run = result.value!.assertMapping("run")!
|
||||||
|
const jobsMapping = run.get(1)!
|
||||||
|
expect(jobsMapping?.key?.assertString("jobs").value).toBe("jobs")
|
||||||
|
|
||||||
|
const job = jobsMapping
|
||||||
|
.value!.assertMapping("jobs")!
|
||||||
|
.get(0)!
|
||||||
|
.value!.assertMapping("job")
|
||||||
|
const stepRun = job
|
||||||
|
.get(1)
|
||||||
|
.value!.assertSequence("steps")
|
||||||
|
.get(0)
|
||||||
|
.assertMapping("step")
|
||||||
|
.get(0)
|
||||||
|
.value!.assertScalar("step-run")
|
||||||
|
|
||||||
|
if (!isBasicExpression(stepRun)) {
|
||||||
|
throw new Error("expected run-name to be a basic expression")
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(stepRun.originalExpressions).toHaveLength(2)
|
||||||
|
expect(
|
||||||
|
stepRun.originalExpressions!.map((x) => [x.toDisplayString(), x.range])
|
||||||
|
).toEqual([
|
||||||
|
[
|
||||||
|
"${{ github.event_name }}",
|
||||||
|
{
|
||||||
|
start: [7, 16],
|
||||||
|
end: [7, 40],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"${{ github.ref }}",
|
||||||
|
{
|
||||||
|
start: [8, 24],
|
||||||
|
end: [8, 41],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("return errors and string token with preserved expressions for (multiple) expression errors", () => {
|
||||||
|
const result = parseWorkflow(
|
||||||
|
"test.yaml",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: "test.yaml",
|
||||||
|
content: `on: push
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- run: |
|
||||||
|
echo \${{ abc }}
|
||||||
|
echo 'hello' \${{ gith }}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
nullTrace
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result.context.errors.getErrors()).toHaveLength(2)
|
||||||
|
|
||||||
|
const run = result.value!.assertMapping("run")!
|
||||||
|
const jobsMapping = run.get(1)!
|
||||||
|
expect(jobsMapping?.key?.assertString("jobs").value).toBe("jobs")
|
||||||
|
|
||||||
|
const job = jobsMapping
|
||||||
|
.value!.assertMapping("jobs")!
|
||||||
|
.get(0)!
|
||||||
|
.value!.assertMapping("job")
|
||||||
|
const stepRun = job
|
||||||
|
.get(1)
|
||||||
|
.value!.assertSequence("steps")
|
||||||
|
.get(0)
|
||||||
|
.assertMapping("step")
|
||||||
|
.get(0)
|
||||||
|
.value!.assertScalar("step-run")
|
||||||
|
|
||||||
|
expect(isString(stepRun)).toBe(true)
|
||||||
|
expect((stepRun as StringToken).value).toContain("${{")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("reports all errors for multi-line expressions at the correct locations", () => {
|
||||||
|
const result = parseWorkflow(
|
||||||
|
"test.yaml",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: "test.yaml",
|
||||||
|
content: `on: push
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- run: |
|
||||||
|
echo \${{ fromJSON2('test') }}
|
||||||
|
echo 'hello' \${{ toJSON2(inputs.test) }}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
nullTrace
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result.context.errors.getErrors()).toEqual([
|
||||||
|
{
|
||||||
|
prefix: "test.yaml (Line: 6, Col: 14)",
|
||||||
|
range: {
|
||||||
|
start: [7, 16],
|
||||||
|
end: [7, 40],
|
||||||
|
},
|
||||||
|
rawMessage: "Unrecognized function: 'fromJSON2'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prefix: "test.yaml (Line: 6, Col: 14)",
|
||||||
|
range: {
|
||||||
|
start: [8, 24],
|
||||||
|
end: [8, 51],
|
||||||
|
},
|
||||||
|
rawMessage: "Unrecognized function: 'toJSON2'",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("parses isExpression strings into expression tokens", () => {
|
||||||
|
const result = parseWorkflow(
|
||||||
|
"test.yaml",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: "test.yaml",
|
||||||
|
content: `on: push
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
steps:
|
||||||
|
- run: echo 'hello'`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
nullTrace
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result.context.errors.getErrors()).toHaveLength(0)
|
||||||
|
|
||||||
|
const workflowRoot = result.value!.assertMapping("root")!
|
||||||
|
const jobs = workflowRoot.get(1).value.assertMapping("jobs")
|
||||||
|
const build = jobs.get(0).value.assertMapping("job")
|
||||||
|
const ifToken = build.get(1).value
|
||||||
|
expect(ifToken.toString()).toEqual("${{ github.event_name == 'push' }}")
|
||||||
|
|
||||||
|
if (!isBasicExpression(ifToken)) {
|
||||||
|
throw new Error("expected if to be a basic expression")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import { TemplateValidationError } from "./templates/template-validation-error"
|
||||||
|
import { nullTrace } from "./test-utils/null-trace"
|
||||||
|
import { parseWorkflow } from "./workflows/workflow-parser"
|
||||||
|
|
||||||
|
describe("parseWorkflow", () => {
|
||||||
|
it("parses valid workflow", () => {
|
||||||
|
const result = parseWorkflow(
|
||||||
|
"test.yaml",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: "test.yaml",
|
||||||
|
content:
|
||||||
|
"on: push\njobs:\n build:\n runs-on: ubuntu-latest\n steps:\n - run: echo 'hello'",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
nullTrace
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result.context.errors.getErrors()).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("contains range for error", () => {
|
||||||
|
const result = parseWorkflow(
|
||||||
|
"test.yaml",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: "test.yaml",
|
||||||
|
content:
|
||||||
|
"on: push\njobs:\n build:\n steps:\n - run: echo 'hello'",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
nullTrace
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result.context.errors.getErrors()).toEqual([
|
||||||
|
new TemplateValidationError(
|
||||||
|
"Required property is missing: runs-on",
|
||||||
|
"test.yaml (Line: 4, Col: 5)",
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
start: [4, 5],
|
||||||
|
end: [5, 24],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("error range for expression is constrained to scalar node", () => {
|
||||||
|
const result = parseWorkflow(
|
||||||
|
"test.yaml",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: "test.yaml",
|
||||||
|
content: `on: push
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: \${{ github.event = 12 }}
|
||||||
|
run: echo 'hello'`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
nullTrace
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result.context.errors.getErrors()).toEqual([
|
||||||
|
new TemplateValidationError(
|
||||||
|
"Unexpected symbol: '='. Located at position 14 within expression: github.event = 12",
|
||||||
|
"test.yaml (Line: 6, Col: 13)",
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
start: [6, 13],
|
||||||
|
end: [6, 37],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("tokens contain descriptions", () => {
|
||||||
|
const result = parseWorkflow(
|
||||||
|
"test.yaml",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: "test.yaml",
|
||||||
|
content:
|
||||||
|
"on: push\nname: hello\njobs:\n build:\n runs-on: ubuntu-latest\n steps:\n - run: echo 'hello'",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
nullTrace
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result.context.errors.getErrors()).toHaveLength(0)
|
||||||
|
expect(result.value).not.toBeUndefined()
|
||||||
|
const root = result.value!.assertMapping("root")
|
||||||
|
expect(root.description).toBe("Workflow file with strict validation")
|
||||||
|
for (const pair of root) {
|
||||||
|
const key = pair.key.assertString("key").value
|
||||||
|
switch (key) {
|
||||||
|
case "name": {
|
||||||
|
const nameKey = pair.key.assertString("name")
|
||||||
|
expect(nameKey.definition).not.toBeUndefined()
|
||||||
|
expect(nameKey.description).toBe("The name of the workflow.")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case "on": {
|
||||||
|
const onKey = pair.key.assertString("on")
|
||||||
|
const onValue = pair.value.assertString("push")
|
||||||
|
expect(onKey.definition).not.toBeUndefined()
|
||||||
|
expect(onKey.description).toBe(
|
||||||
|
"The name of the GitHub event that triggers the workflow. You can provide a single event string, array of events, array of event types, or an event configuration map that schedules a workflow or restricts the execution of a workflow to specific files, tags, or branch changes. For a list of available events, see https://help.github.com/en/github/automating-your-workflow-with-github-actions/events-that-trigger-workflows."
|
||||||
|
)
|
||||||
|
expect(onValue.definition).not.toBeUndefined()
|
||||||
|
expect(onValue.description).toBe(
|
||||||
|
"Runs your workflow when you push a commit or tag."
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export { convertWorkflowTemplate } from "./model/convert"
|
||||||
|
export { WorkflowTemplate } from "./model/workflow-template"
|
||||||
|
export * from "./templates/tokens/type-guards"
|
||||||
|
export { NoOperationTraceWriter, TraceWriter } from "./templates/trace-writer"
|
||||||
|
export { parseWorkflow, ParseWorkflowResult } from "./workflows/workflow-parser"
|
||||||
@@ -0,0 +1,348 @@
|
|||||||
|
import { nullTrace } from "../test-utils/null-trace"
|
||||||
|
import { parseWorkflow } from "../workflows/workflow-parser"
|
||||||
|
import { convertWorkflowTemplate, ErrorPolicy } from "./convert"
|
||||||
|
|
||||||
|
function serializeTemplate(template: unknown): unknown {
|
||||||
|
return JSON.parse(JSON.stringify(template))
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("convertWorkflowTemplate", () => {
|
||||||
|
it("converts workflow with one job", () => {
|
||||||
|
const result = parseWorkflow(
|
||||||
|
"wf.yaml",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: "wf.yaml",
|
||||||
|
content: `on: push
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
nullTrace
|
||||||
|
)
|
||||||
|
|
||||||
|
const template = convertWorkflowTemplate(
|
||||||
|
result.context,
|
||||||
|
result.value!,
|
||||||
|
ErrorPolicy.TryConversion
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(serializeTemplate(template)).toEqual({
|
||||||
|
events: {
|
||||||
|
push: {},
|
||||||
|
},
|
||||||
|
jobs: [
|
||||||
|
{
|
||||||
|
id: "build",
|
||||||
|
if: {
|
||||||
|
expr: "success()",
|
||||||
|
type: 3,
|
||||||
|
},
|
||||||
|
name: "build",
|
||||||
|
needs: undefined,
|
||||||
|
outputs: undefined,
|
||||||
|
"runs-on": "ubuntu-latest",
|
||||||
|
steps: [],
|
||||||
|
type: "job",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("converts workflow if expressions", () => {
|
||||||
|
const result = parseWorkflow(
|
||||||
|
"wf.yaml",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: "wf.yaml",
|
||||||
|
content: `on: push
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
if: \${{ true }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
deploy:
|
||||||
|
if: true
|
||||||
|
runs-on: ubuntu-latest`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
nullTrace
|
||||||
|
)
|
||||||
|
|
||||||
|
const template = convertWorkflowTemplate(
|
||||||
|
result.context,
|
||||||
|
result.value!,
|
||||||
|
ErrorPolicy.TryConversion
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(serializeTemplate(template)).toEqual({
|
||||||
|
events: {
|
||||||
|
push: {},
|
||||||
|
},
|
||||||
|
jobs: [
|
||||||
|
{
|
||||||
|
id: "build",
|
||||||
|
if: {
|
||||||
|
expr: "success()",
|
||||||
|
type: 3,
|
||||||
|
},
|
||||||
|
name: "build",
|
||||||
|
"runs-on": "ubuntu-latest",
|
||||||
|
steps: [],
|
||||||
|
type: "job",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "deploy",
|
||||||
|
if: {
|
||||||
|
expr: "success()",
|
||||||
|
type: 3,
|
||||||
|
},
|
||||||
|
name: "deploy",
|
||||||
|
"runs-on": "ubuntu-latest",
|
||||||
|
steps: [],
|
||||||
|
type: "job",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("converts workflow with empty needs", () => {
|
||||||
|
const result = parseWorkflow(
|
||||||
|
"wf.yaml",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: "wf.yaml",
|
||||||
|
content: `on: push
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
needs: # comment to preserve whitespace in test
|
||||||
|
runs-on: ubuntu-latest`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
nullTrace
|
||||||
|
)
|
||||||
|
|
||||||
|
const template = convertWorkflowTemplate(
|
||||||
|
result.context,
|
||||||
|
result.value!,
|
||||||
|
ErrorPolicy.TryConversion
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(serializeTemplate(template)).toEqual({
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
Message: "wf.yaml (Line: 4, Col: 12): Unexpected value ''",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
events: {
|
||||||
|
push: {},
|
||||||
|
},
|
||||||
|
jobs: [
|
||||||
|
{
|
||||||
|
id: "build",
|
||||||
|
if: {
|
||||||
|
expr: "success()",
|
||||||
|
type: 3,
|
||||||
|
},
|
||||||
|
name: "build",
|
||||||
|
needs: [],
|
||||||
|
outputs: undefined,
|
||||||
|
"runs-on": "ubuntu-latest",
|
||||||
|
steps: [],
|
||||||
|
type: "job",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("converts workflow with needs errors", () => {
|
||||||
|
const result = parseWorkflow(
|
||||||
|
"wf.yaml",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: "wf.yaml",
|
||||||
|
content: `on: push
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
needs: [unknown-job, job3]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
job2:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
job3:
|
||||||
|
needs: job1
|
||||||
|
runs-on: ubuntu-latest`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
nullTrace
|
||||||
|
)
|
||||||
|
|
||||||
|
const template = convertWorkflowTemplate(
|
||||||
|
result.context,
|
||||||
|
result.value!,
|
||||||
|
ErrorPolicy.TryConversion
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(serializeTemplate(template)).toEqual({
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
Message:
|
||||||
|
"wf.yaml (Line: 4, Col: 13): Job 'job1' depends on unknown job 'unknown-job'.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Message:
|
||||||
|
"wf.yaml (Line: 4, Col: 26): Job 'job1' depends on job 'job3' which creates a cycle in the dependency graph.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Message:
|
||||||
|
"wf.yaml (Line: 9, Col: 12): Job 'job3' depends on job 'job1' which creates a cycle in the dependency graph.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
events: {
|
||||||
|
push: {},
|
||||||
|
},
|
||||||
|
jobs: [
|
||||||
|
{
|
||||||
|
id: "job1",
|
||||||
|
if: {
|
||||||
|
expr: "success()",
|
||||||
|
type: 3,
|
||||||
|
},
|
||||||
|
name: "job1",
|
||||||
|
needs: ["unknown-job", "job3"],
|
||||||
|
"runs-on": "ubuntu-latest",
|
||||||
|
steps: [],
|
||||||
|
type: "job",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "job2",
|
||||||
|
if: {
|
||||||
|
expr: "success()",
|
||||||
|
type: 3,
|
||||||
|
},
|
||||||
|
name: "job2",
|
||||||
|
"runs-on": "ubuntu-latest",
|
||||||
|
steps: [],
|
||||||
|
type: "job",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "job3",
|
||||||
|
if: {
|
||||||
|
expr: "success()",
|
||||||
|
type: 3,
|
||||||
|
},
|
||||||
|
name: "job3",
|
||||||
|
needs: ["job1"],
|
||||||
|
"runs-on": "ubuntu-latest",
|
||||||
|
steps: [],
|
||||||
|
type: "job",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("converts workflow with invalid on", () => {
|
||||||
|
const result = parseWorkflow(
|
||||||
|
"wf.yaml",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: "wf.yaml",
|
||||||
|
content: `on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
test:
|
||||||
|
options: 123
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- run: echo hello`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
nullTrace
|
||||||
|
)
|
||||||
|
|
||||||
|
const template = convertWorkflowTemplate(
|
||||||
|
result.context,
|
||||||
|
result.value!,
|
||||||
|
ErrorPolicy.TryConversion
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(template.jobs).not.toBeUndefined()
|
||||||
|
expect(template.jobs).toHaveLength(1)
|
||||||
|
expect(serializeTemplate(template)).toEqual({
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
Message: "wf.yaml (Line: 5, Col: 18): Unexpected value '123'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Message:
|
||||||
|
"wf.yaml (Line: 5, Col: 18): Unexpected type 'NumberToken' encountered while reading 'input options'. The type 'SequenceToken' was expected.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
events: {},
|
||||||
|
jobs: [
|
||||||
|
{
|
||||||
|
id: "build",
|
||||||
|
if: {
|
||||||
|
expr: "success()",
|
||||||
|
type: 3,
|
||||||
|
},
|
||||||
|
name: "build",
|
||||||
|
needs: undefined,
|
||||||
|
outputs: undefined,
|
||||||
|
"runs-on": "ubuntu-latest",
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
id: "__run",
|
||||||
|
if: {
|
||||||
|
expr: "success()",
|
||||||
|
type: 3,
|
||||||
|
},
|
||||||
|
run: "echo hello",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
type: "job",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("converts workflow with invalid jobs", () => {
|
||||||
|
const result = parseWorkflow(
|
||||||
|
"wf.yaml",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: "wf.yaml",
|
||||||
|
content: `on: push
|
||||||
|
jobs:
|
||||||
|
build:`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
nullTrace
|
||||||
|
)
|
||||||
|
|
||||||
|
const template = convertWorkflowTemplate(
|
||||||
|
result.context,
|
||||||
|
result.value!,
|
||||||
|
ErrorPolicy.TryConversion
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(template.jobs).not.toBeUndefined()
|
||||||
|
expect(template.jobs).toHaveLength(0)
|
||||||
|
expect(serializeTemplate(template)).toEqual({
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
Message: "wf.yaml (Line: 3, Col: 9): Unexpected value ''",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Message:
|
||||||
|
"wf.yaml (Line: 3, Col: 9): Unexpected type 'NullToken' encountered while reading 'job build'. The type 'MappingToken' was expected.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
events: {
|
||||||
|
push: {},
|
||||||
|
},
|
||||||
|
jobs: [],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { TemplateContext } from "../templates/template-context"
|
||||||
|
import {
|
||||||
|
TemplateToken,
|
||||||
|
TemplateTokenError,
|
||||||
|
} from "../templates/tokens/template-token"
|
||||||
|
import { convertConcurrency } from "./converter/concurrency"
|
||||||
|
import { convertOn } from "./converter/events"
|
||||||
|
import { handleTemplateTokenErrors } from "./converter/handle-errors"
|
||||||
|
import { convertJobs } from "./converter/jobs"
|
||||||
|
import { WorkflowTemplate } from "./workflow-template"
|
||||||
|
|
||||||
|
export enum ErrorPolicy {
|
||||||
|
ReturnErrorsOnly,
|
||||||
|
TryConversion,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertWorkflowTemplate(
|
||||||
|
context: TemplateContext,
|
||||||
|
root: TemplateToken,
|
||||||
|
errorPolicy: ErrorPolicy = ErrorPolicy.ReturnErrorsOnly
|
||||||
|
): WorkflowTemplate {
|
||||||
|
const result = {} as WorkflowTemplate
|
||||||
|
|
||||||
|
if (
|
||||||
|
context.errors.getErrors().length > 0 &&
|
||||||
|
errorPolicy === ErrorPolicy.ReturnErrorsOnly
|
||||||
|
) {
|
||||||
|
result.errors = context.errors.getErrors().map((x) => ({
|
||||||
|
Message: x.message,
|
||||||
|
}))
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const rootMapping = root.assertMapping("root")
|
||||||
|
|
||||||
|
for (const item of rootMapping) {
|
||||||
|
const key = item.key.assertString("root key")
|
||||||
|
|
||||||
|
switch (key.value) {
|
||||||
|
case "on":
|
||||||
|
result.events = handleTemplateTokenErrors(root, context, {}, () =>
|
||||||
|
convertOn(context, item.value)
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
|
case "jobs":
|
||||||
|
result.jobs = handleTemplateTokenErrors(root, context, [], () =>
|
||||||
|
convertJobs(context, item.value)
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
|
case "concurrency":
|
||||||
|
handleTemplateTokenErrors(root, context, {}, () =>
|
||||||
|
convertConcurrency(context, item.value)
|
||||||
|
)
|
||||||
|
result.concurrency = item.value
|
||||||
|
break
|
||||||
|
case "env":
|
||||||
|
result.env = item.value
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof TemplateTokenError) {
|
||||||
|
context.error(err.token, err)
|
||||||
|
} else {
|
||||||
|
// Report error for the root node
|
||||||
|
context.error(root, err)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (context.errors.getErrors().length > 0) {
|
||||||
|
result.errors = context.errors.getErrors().map((x) => ({
|
||||||
|
Message: x.message,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { TemplateContext } from "../../templates/template-context"
|
||||||
|
import { TemplateToken } from "../../templates/tokens/template-token"
|
||||||
|
import { isString } from "../../templates/tokens/type-guards"
|
||||||
|
import { ConcurrencySetting } from "../workflow-template"
|
||||||
|
|
||||||
|
export function convertConcurrency(
|
||||||
|
context: TemplateContext,
|
||||||
|
token: TemplateToken
|
||||||
|
): ConcurrencySetting {
|
||||||
|
const result: ConcurrencySetting = {}
|
||||||
|
|
||||||
|
if (token.isExpression) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
if (isString(token)) {
|
||||||
|
result.group = token
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
const concurrencyProperty = token.assertMapping("concurrency group")
|
||||||
|
for (const property of concurrencyProperty) {
|
||||||
|
const propertyName = property.key.assertString("concurrency group key")
|
||||||
|
if (property.key.isExpression || property.value.isExpression) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch (propertyName.value) {
|
||||||
|
case "group":
|
||||||
|
result.group = property.value.assertString("concurrency group")
|
||||||
|
break
|
||||||
|
case "cancel-in-progress":
|
||||||
|
result.cancelInProgress =
|
||||||
|
property.value.assertBoolean("cancel-in-progress").value
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
context.error(
|
||||||
|
propertyName,
|
||||||
|
`Invalid property name: ${propertyName.value}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import { TemplateContext } from "../../templates/template-context"
|
||||||
|
import {
|
||||||
|
MappingToken,
|
||||||
|
SequenceToken,
|
||||||
|
StringToken,
|
||||||
|
TemplateToken,
|
||||||
|
} from "../../templates/tokens"
|
||||||
|
import { isString } from "../../templates/tokens/type-guards"
|
||||||
|
import { Container, Credential } from "../workflow-template"
|
||||||
|
|
||||||
|
export function convertToJobContainer(
|
||||||
|
context: TemplateContext,
|
||||||
|
container: TemplateToken
|
||||||
|
): Container | undefined {
|
||||||
|
let image: StringToken | undefined
|
||||||
|
let env: MappingToken | undefined
|
||||||
|
let ports: SequenceToken | undefined
|
||||||
|
let volumes: SequenceToken | undefined
|
||||||
|
let options: StringToken | undefined
|
||||||
|
|
||||||
|
// Skip validation for expressions for now to match
|
||||||
|
// behavior of the other parsers
|
||||||
|
for (const [_, token, __] of TemplateToken.traverse(container)) {
|
||||||
|
if (token.isExpression) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isString(container)) {
|
||||||
|
// Workflow uses shorthand syntax `container: image-name`
|
||||||
|
image = container.assertString("container item")
|
||||||
|
return { image: image }
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapping = container.assertMapping("container item")
|
||||||
|
if (mapping)
|
||||||
|
for (const item of mapping) {
|
||||||
|
const key = item.key.assertString("container item key")
|
||||||
|
const value = item.value
|
||||||
|
|
||||||
|
switch (key.value) {
|
||||||
|
case "image":
|
||||||
|
image = value.assertString("container image")
|
||||||
|
break
|
||||||
|
case "credentials":
|
||||||
|
convertToJobCredentials(context, value)
|
||||||
|
break
|
||||||
|
case "env":
|
||||||
|
env = value.assertMapping("container env")
|
||||||
|
for (const envItem of env) {
|
||||||
|
envItem.key.assertString("container env value")
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case "ports":
|
||||||
|
ports = value.assertSequence("container ports")
|
||||||
|
for (const port of ports) {
|
||||||
|
port.assertString("container port")
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case "volumes":
|
||||||
|
volumes = value.assertSequence("container volumes")
|
||||||
|
for (const volume of volumes) {
|
||||||
|
volume.assertString("container volume")
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case "options":
|
||||||
|
options = value.assertString("container options")
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
context.error(key, `Unexpected container item key: ${key.value}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!image) {
|
||||||
|
context.error(container, "Container image cannot be empty")
|
||||||
|
} else {
|
||||||
|
return { image, env, ports, volumes, options }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertToJobServices(
|
||||||
|
context: TemplateContext,
|
||||||
|
services: TemplateToken
|
||||||
|
): Container[] | undefined {
|
||||||
|
const serviceList: Container[] = []
|
||||||
|
|
||||||
|
const mapping = services.assertMapping("services")
|
||||||
|
for (const service of mapping) {
|
||||||
|
service.key.assertString("service key")
|
||||||
|
const container = convertToJobContainer(context, service.value)
|
||||||
|
if (container) {
|
||||||
|
serviceList.push(container)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return serviceList
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertToJobCredentials(
|
||||||
|
context: TemplateContext,
|
||||||
|
value: TemplateToken
|
||||||
|
): Credential | undefined {
|
||||||
|
const mapping = value.assertMapping("credentials")
|
||||||
|
|
||||||
|
let username: StringToken | undefined
|
||||||
|
let password: StringToken | undefined
|
||||||
|
|
||||||
|
for (const item of mapping) {
|
||||||
|
const key = item.key.assertString("credentials item")
|
||||||
|
const value = item.value
|
||||||
|
|
||||||
|
switch (key.value) {
|
||||||
|
case "username":
|
||||||
|
username = value.assertString("credentials username")
|
||||||
|
break
|
||||||
|
case "password":
|
||||||
|
password = value.assertString("credentials password")
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
context.error(key, `credentials key ${key.value}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { username, password }
|
||||||
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
import { TemplateContext } from "../../templates/template-context"
|
||||||
|
import { MappingToken } from "../../templates/tokens/mapping-token"
|
||||||
|
import { SequenceToken } from "../../templates/tokens/sequence-token"
|
||||||
|
import { TemplateToken } from "../../templates/tokens/template-token"
|
||||||
|
import {
|
||||||
|
isLiteral,
|
||||||
|
isMapping,
|
||||||
|
isSequence,
|
||||||
|
isString,
|
||||||
|
} from "../../templates/tokens/type-guards"
|
||||||
|
import { TokenType } from "../../templates/tokens/types"
|
||||||
|
import {
|
||||||
|
BranchFilterConfig,
|
||||||
|
EventsConfig,
|
||||||
|
PathFilterConfig,
|
||||||
|
ScheduleConfig,
|
||||||
|
TagFilterConfig,
|
||||||
|
TypesFilterConfig,
|
||||||
|
WorkflowFilterConfig,
|
||||||
|
} from "../workflow-template"
|
||||||
|
import { convertStringList } from "./string-list"
|
||||||
|
import { convertEventWorkflowDispatchInputs } from "./workflow-dispatch"
|
||||||
|
|
||||||
|
export function convertOn(
|
||||||
|
context: TemplateContext,
|
||||||
|
token: TemplateToken
|
||||||
|
): EventsConfig {
|
||||||
|
if (isLiteral(token)) {
|
||||||
|
const event = token.assertString("on")
|
||||||
|
|
||||||
|
return {
|
||||||
|
[event.value]: {},
|
||||||
|
} as EventsConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSequence(token)) {
|
||||||
|
const result = {} as EventsConfig
|
||||||
|
|
||||||
|
for (const item of token) {
|
||||||
|
const event = item.assertString("on")
|
||||||
|
result[event.value] = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMapping(token)) {
|
||||||
|
const result = {} as EventsConfig
|
||||||
|
|
||||||
|
for (const item of token) {
|
||||||
|
const eventKey = item.key.assertString("event name")
|
||||||
|
const eventName = eventKey.value
|
||||||
|
|
||||||
|
if (item.value.templateTokenType === TokenType.Null) {
|
||||||
|
result[eventName] = {}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schedule is the only event that can be a sequence, handle that separately
|
||||||
|
if (eventName === "schedule") {
|
||||||
|
const scheduleToken = item.value.assertSequence(`event ${eventName}`)
|
||||||
|
result.schedule = convertSchedule(context, scheduleToken)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// All other events are defined as mappings. During schema validation we already ensure that events
|
||||||
|
// receive only known keys, so here we can focus on the values and whether they are valid.
|
||||||
|
const eventToken = item.value.assertMapping(`event ${eventName}`)
|
||||||
|
|
||||||
|
result[eventName] = {
|
||||||
|
...convertPatternFilter("branches", eventToken),
|
||||||
|
...convertPatternFilter("tags", eventToken),
|
||||||
|
...convertPatternFilter("paths", eventToken),
|
||||||
|
...convertFilter("types", eventToken),
|
||||||
|
...convertFilter("workflows", eventToken),
|
||||||
|
// TODO - share input parsing for now, but workflow_call also needs outputs and secrets
|
||||||
|
...convertEventWorkflowDispatchInputs(context, eventToken),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
context.error(token, "Invalid format for 'on'")
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertPatternFilter<
|
||||||
|
T extends BranchFilterConfig & TagFilterConfig & PathFilterConfig
|
||||||
|
>(name: "branches" | "tags" | "paths", token: MappingToken): T {
|
||||||
|
const result = {} as T
|
||||||
|
|
||||||
|
for (const item of token) {
|
||||||
|
const key = item.key.assertString(`${name} filter key`)
|
||||||
|
|
||||||
|
switch (key.value) {
|
||||||
|
case name:
|
||||||
|
if (isString(item.value)) {
|
||||||
|
result[name] = [item.value.value]
|
||||||
|
} else {
|
||||||
|
result[name] = convertStringList(
|
||||||
|
name,
|
||||||
|
item.value.assertSequence(`${name} list`)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case `${name}-ignore`:
|
||||||
|
if (isString(item.value)) {
|
||||||
|
result[`${name}-ignore`] = [item.value.value]
|
||||||
|
} else {
|
||||||
|
result[`${name}-ignore`] = convertStringList(
|
||||||
|
`${name}-ignore`,
|
||||||
|
item.value.assertSequence(`${name}-ignore list`)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertFilter<T extends TypesFilterConfig & WorkflowFilterConfig>(
|
||||||
|
name: "types" | "workflows",
|
||||||
|
token: MappingToken
|
||||||
|
): T {
|
||||||
|
const result = {} as T
|
||||||
|
|
||||||
|
for (const item of token) {
|
||||||
|
const key = item.key.assertString(`${name} filter key`)
|
||||||
|
|
||||||
|
switch (key.value) {
|
||||||
|
case name:
|
||||||
|
if (isString(item.value)) {
|
||||||
|
result[name] = [item.value.value]
|
||||||
|
} else {
|
||||||
|
result[name] = convertStringList(
|
||||||
|
name,
|
||||||
|
item.value.assertSequence(`${name} list`)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertSchedule(
|
||||||
|
context: TemplateContext,
|
||||||
|
token: SequenceToken
|
||||||
|
): ScheduleConfig[] | undefined {
|
||||||
|
const result = [] as ScheduleConfig[]
|
||||||
|
for (const item of token) {
|
||||||
|
const mappingToken = item.assertMapping(`event schedule`)
|
||||||
|
if (mappingToken.count == 1) {
|
||||||
|
const schedule = mappingToken.get(0)
|
||||||
|
const scheduleKey = schedule.key.assertString(`schedule key`)
|
||||||
|
if (scheduleKey.value == "cron") {
|
||||||
|
const cron = schedule.value.assertString(`schedule cron`)
|
||||||
|
// Validate the cron string
|
||||||
|
if (
|
||||||
|
!cron.value.match(/((((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*) ?){5,7})/)
|
||||||
|
) {
|
||||||
|
context.error(cron, "Invalid cron string")
|
||||||
|
}
|
||||||
|
result.push({ cron: cron.value })
|
||||||
|
} else {
|
||||||
|
context.error(scheduleKey, `Invalid schedule key`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
context.error(mappingToken, "Invalid format for 'schedule'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { TemplateContext } from "../../templates/template-context"
|
||||||
|
import {
|
||||||
|
TemplateToken,
|
||||||
|
TemplateTokenError,
|
||||||
|
} from "../../templates/tokens/template-token"
|
||||||
|
|
||||||
|
export function handleTemplateTokenErrors<TResult>(
|
||||||
|
root: TemplateToken,
|
||||||
|
context: TemplateContext,
|
||||||
|
defaultValue: TResult,
|
||||||
|
f: () => TResult
|
||||||
|
): TResult {
|
||||||
|
let r: TResult = defaultValue
|
||||||
|
|
||||||
|
try {
|
||||||
|
r = f()
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof TemplateTokenError) {
|
||||||
|
context.error(err.token, err)
|
||||||
|
} else {
|
||||||
|
// Report error for the root node
|
||||||
|
context.error(root, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
import { IdBuilder } from "./id-builder"
|
||||||
|
|
||||||
|
function build(...segments: string[]): string {
|
||||||
|
const builder = new IdBuilder()
|
||||||
|
for (const segment of segments) {
|
||||||
|
builder.appendSegment(segment)
|
||||||
|
}
|
||||||
|
return builder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("ID Builder", () => {
|
||||||
|
it("builds IDs", () => {
|
||||||
|
expect(build("one")).toEqual("one")
|
||||||
|
|
||||||
|
expect(build("one", "two")).toEqual("one_two")
|
||||||
|
|
||||||
|
expect(build("one", "two", "three")).toEqual("one_two_three")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("empty builder", () => {
|
||||||
|
const builder = new IdBuilder()
|
||||||
|
expect(builder.build()).toEqual("job")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("ignores empty segments", () => {
|
||||||
|
expect(build("", "one")).toEqual("one")
|
||||||
|
|
||||||
|
expect(build("one", "", "two", "")).toEqual("one_two")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("handles illegal characters", () => {
|
||||||
|
const builder = new IdBuilder()
|
||||||
|
builder.appendSegment("hello world!")
|
||||||
|
expect(builder.build()).toEqual("hello_world_")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("handles illegal leading characters", () => {
|
||||||
|
expect(build("!hello")).toEqual("_hello")
|
||||||
|
|
||||||
|
expect(build("!hello", "!world")).toEqual("_hello__world")
|
||||||
|
|
||||||
|
expect(build("!@world", "!@world")).toEqual("__world___world")
|
||||||
|
|
||||||
|
expect(build("123")).toEqual("_123")
|
||||||
|
|
||||||
|
expect(build("123", "456")).toEqual("_123_456")
|
||||||
|
|
||||||
|
expect(build("-abc")).toEqual("_-abc")
|
||||||
|
|
||||||
|
expect(build("-abc", "-def")).toEqual("_-abc_-def")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("allows legal characters", () => {
|
||||||
|
expect(build("abyzABYZ0189_-")).toEqual("abyzABYZ0189_-")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("allows legal leading characters", () => {
|
||||||
|
expect(build("abc")).toEqual("abc")
|
||||||
|
|
||||||
|
expect(build("bcd")).toEqual("bcd")
|
||||||
|
|
||||||
|
expect(build("zyx")).toEqual("zyx")
|
||||||
|
|
||||||
|
expect(build("yxw")).toEqual("yxw")
|
||||||
|
|
||||||
|
expect(build("ABCD")).toEqual("ABCD")
|
||||||
|
|
||||||
|
expect(build("BCDE")).toEqual("BCDE")
|
||||||
|
|
||||||
|
expect(build("ZYXW")).toEqual("ZYXW")
|
||||||
|
|
||||||
|
expect(build("YXWV")).toEqual("YXWV")
|
||||||
|
|
||||||
|
expect(build("_abc")).toEqual("_abc")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("errors for max collisions", () => {
|
||||||
|
const builder = new IdBuilder()
|
||||||
|
builder.appendSegment("abc")
|
||||||
|
builder.appendSegment("def")
|
||||||
|
expect(builder.build()).toEqual("abc_def")
|
||||||
|
|
||||||
|
for (let i = 2; i < 1000; i++) {
|
||||||
|
builder.appendSegment("abc")
|
||||||
|
builder.appendSegment("def")
|
||||||
|
expect(builder.build()).toEqual(`abc_def_${i}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.appendSegment("abc")
|
||||||
|
builder.appendSegment("def")
|
||||||
|
expect(() => builder.build()).toThrowError("Unable to create a unique name")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("takes suffix into account for max length", () => {
|
||||||
|
const builder = new IdBuilder()
|
||||||
|
|
||||||
|
const name =
|
||||||
|
"_234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"
|
||||||
|
builder.appendSegment(name)
|
||||||
|
expect(builder.build()).toEqual(name)
|
||||||
|
|
||||||
|
builder.appendSegment(name)
|
||||||
|
expect(builder.build()).toEqual(
|
||||||
|
"_2345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678_2"
|
||||||
|
)
|
||||||
|
|
||||||
|
builder.appendSegment(name)
|
||||||
|
expect(builder.build()).toEqual(
|
||||||
|
"_2345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678_3"
|
||||||
|
)
|
||||||
|
|
||||||
|
builder.appendSegment(name)
|
||||||
|
expect(builder.build()).toEqual(
|
||||||
|
"_2345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678_4"
|
||||||
|
)
|
||||||
|
|
||||||
|
builder.appendSegment(name)
|
||||||
|
expect(builder.build()).toEqual(
|
||||||
|
"_2345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678_5"
|
||||||
|
)
|
||||||
|
|
||||||
|
builder.appendSegment(name)
|
||||||
|
expect(builder.build()).toEqual(
|
||||||
|
"_2345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678_6"
|
||||||
|
)
|
||||||
|
|
||||||
|
builder.appendSegment(name)
|
||||||
|
expect(builder.build()).toEqual(
|
||||||
|
"_2345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678_7"
|
||||||
|
)
|
||||||
|
|
||||||
|
builder.appendSegment(name)
|
||||||
|
expect(builder.build()).toEqual(
|
||||||
|
"_2345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678_8"
|
||||||
|
)
|
||||||
|
|
||||||
|
builder.appendSegment(name)
|
||||||
|
expect(builder.build()).toEqual(
|
||||||
|
"_2345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678_9"
|
||||||
|
)
|
||||||
|
|
||||||
|
builder.appendSegment(name)
|
||||||
|
expect(builder.build()).toEqual(
|
||||||
|
"_234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567_10"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
const SEPARATOR = "_"
|
||||||
|
const MAX_ATTEMPTS = 1000
|
||||||
|
const MAX_LENGTH = 100
|
||||||
|
|
||||||
|
export class IdBuilder {
|
||||||
|
private name: string[] = []
|
||||||
|
private readonly distinctNames: Set<string> = new Set()
|
||||||
|
|
||||||
|
public appendSegment(value: string) {
|
||||||
|
if (value.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.name.length == 0) {
|
||||||
|
const first = value[0]
|
||||||
|
if (this.isAlpha(first) || first == "_") {
|
||||||
|
// Legal first char
|
||||||
|
} else if (this.isNumeric(first) || first == "-") {
|
||||||
|
// Illegal first char, but legal char.
|
||||||
|
// Prepend "_".
|
||||||
|
this.name.push("_")
|
||||||
|
} else {
|
||||||
|
// Illegal char
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Separator
|
||||||
|
this.name.push(SEPARATOR)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const c of value) {
|
||||||
|
{
|
||||||
|
if (this.isAlphaNumeric(c) || c == "_" || c == "-") {
|
||||||
|
// Legal
|
||||||
|
this.name.push(c)
|
||||||
|
} else {
|
||||||
|
// Illegal
|
||||||
|
this.name.push(SEPARATOR)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public build(): string {
|
||||||
|
const original = this.name.length > 0 ? this.name.join("") : "job"
|
||||||
|
let suffix = ""
|
||||||
|
for (let attempt = 1; attempt < MAX_ATTEMPTS; attempt++) {
|
||||||
|
if (attempt === 1) {
|
||||||
|
suffix = ""
|
||||||
|
} else {
|
||||||
|
suffix = "_" + attempt
|
||||||
|
}
|
||||||
|
|
||||||
|
const candidate =
|
||||||
|
original.substring(
|
||||||
|
0,
|
||||||
|
Math.min(original.length, MAX_LENGTH - suffix.length)
|
||||||
|
) + suffix
|
||||||
|
|
||||||
|
if (!this.distinctNames.has(candidate)) {
|
||||||
|
this.distinctNames.add(candidate)
|
||||||
|
this.name = []
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Unable to create a unique name")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a known identifier to the set of distinct ids.
|
||||||
|
* @param value The value to add
|
||||||
|
* @returns An error if the value is invalid, otherwise undefined
|
||||||
|
*/
|
||||||
|
public tryAddKnownId(value: string): string | undefined {
|
||||||
|
if (!value || !this.isValid(value) || value.length >= MAX_LENGTH) {
|
||||||
|
return `The identifier '${value}' is invalid. IDs may only contain alphanumeric characters, '_', and '-'. IDs must start with a letter or '_' and and must be less than ${MAX_LENGTH} characters.`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.startsWith(SEPARATOR + SEPARATOR)) {
|
||||||
|
return `The identifier '${value}' is invalid. IDs starting with '__' are reserved.`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.distinctNames.has(value)) {
|
||||||
|
return `The identifier '${value}' may not be used more than once within the same scope.`
|
||||||
|
}
|
||||||
|
|
||||||
|
this.distinctNames.add(value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A name is valid if it starts with a letter or underscore, and contains only
|
||||||
|
* letters, numbers, underscores, and hyphens.
|
||||||
|
* @param name The string name to validate
|
||||||
|
* @returns Whether the name is valid
|
||||||
|
*/
|
||||||
|
private isValid(name: string): boolean {
|
||||||
|
let first = true
|
||||||
|
for (const c of name) {
|
||||||
|
if (first) {
|
||||||
|
first = false
|
||||||
|
if (!this.isAlpha(c) && c != "_") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (!this.isAlphaNumeric(c) && c != "_" && c != "-") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private isAlphaNumeric(c: string): boolean {
|
||||||
|
return this.isAlpha(c) || this.isNumeric(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
private isNumeric(c: string): boolean {
|
||||||
|
return c >= "0" && c <= "9"
|
||||||
|
}
|
||||||
|
|
||||||
|
private isAlpha(c: string): boolean {
|
||||||
|
return (c >= "a" && c <= "z") || (c >= "A" && c <= "Z")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { TemplateContext } from "../../../templates/template-context"
|
||||||
|
import { TemplateToken } from "../../../templates/tokens/template-token"
|
||||||
|
import { isScalar } from "../../../templates/tokens/type-guards"
|
||||||
|
import { ActionsEnvironmentReference } from "../../workflow-template"
|
||||||
|
|
||||||
|
export function convertToActionsEnvironmentRef(
|
||||||
|
context: TemplateContext,
|
||||||
|
token: TemplateToken
|
||||||
|
): ActionsEnvironmentReference {
|
||||||
|
const result: ActionsEnvironmentReference = {}
|
||||||
|
|
||||||
|
if (token.isExpression) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isScalar(token)) {
|
||||||
|
result.name = token
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const environmentMapping = token.assertMapping("job environment")
|
||||||
|
|
||||||
|
for (const property of environmentMapping) {
|
||||||
|
const propertyName = property.key.assertString("job environment key")
|
||||||
|
if (property.key.isExpression || property.value.isExpression) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (propertyName.value) {
|
||||||
|
case "name":
|
||||||
|
result.name = property.value.assertScalar("job environment name key")
|
||||||
|
break
|
||||||
|
|
||||||
|
case "url":
|
||||||
|
result.url = property.value
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,345 @@
|
|||||||
|
import { TemplateContext } from "../../templates/template-context"
|
||||||
|
import {
|
||||||
|
BasicExpressionToken,
|
||||||
|
MappingToken,
|
||||||
|
StringToken,
|
||||||
|
} from "../../templates/tokens"
|
||||||
|
import { TemplateToken } from "../../templates/tokens/template-token"
|
||||||
|
import {
|
||||||
|
isMapping,
|
||||||
|
isSequence,
|
||||||
|
isString,
|
||||||
|
} from "../../templates/tokens/type-guards"
|
||||||
|
import { Job } from "../workflow-template"
|
||||||
|
import { convertConcurrency } from "./concurrency"
|
||||||
|
import { convertToJobContainer, convertToJobServices } from "./container"
|
||||||
|
import { handleTemplateTokenErrors } from "./handle-errors"
|
||||||
|
import { IdBuilder } from "./id-builder"
|
||||||
|
import { convertToActionsEnvironmentRef } from "./job/environment"
|
||||||
|
import { convertSteps } from "./steps"
|
||||||
|
|
||||||
|
type nodeInfo = {
|
||||||
|
name: string
|
||||||
|
needs: StringToken[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertJobs(
|
||||||
|
context: TemplateContext,
|
||||||
|
token: TemplateToken
|
||||||
|
): Job[] {
|
||||||
|
if (isMapping(token)) {
|
||||||
|
const result: Job[] = []
|
||||||
|
const jobsWithSatisfiedNeeds: nodeInfo[] = []
|
||||||
|
const alljobsWithUnsatisfiedNeeds: nodeInfo[] = []
|
||||||
|
|
||||||
|
for (const item of token) {
|
||||||
|
const jobKey = item.key.assertString("job name")
|
||||||
|
const jobDef = item.value.assertMapping(`job ${jobKey.value}`)
|
||||||
|
|
||||||
|
const job = handleTemplateTokenErrors(token, context, undefined, () =>
|
||||||
|
convertJob(context, jobKey, jobDef)
|
||||||
|
)
|
||||||
|
if (job) {
|
||||||
|
result.push(job)
|
||||||
|
const node = {
|
||||||
|
name: job.id.value,
|
||||||
|
needs: Object.assign([], job.needs),
|
||||||
|
}
|
||||||
|
if (node.needs.length > 0) {
|
||||||
|
alljobsWithUnsatisfiedNeeds.push(node)
|
||||||
|
} else {
|
||||||
|
jobsWithSatisfiedNeeds.push(node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//validate job needs
|
||||||
|
validateNeeds(
|
||||||
|
token,
|
||||||
|
context,
|
||||||
|
result,
|
||||||
|
jobsWithSatisfiedNeeds,
|
||||||
|
alljobsWithUnsatisfiedNeeds
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
context.error(token, "Invalid format for jobs")
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateNeeds(
|
||||||
|
token: TemplateToken,
|
||||||
|
context: TemplateContext,
|
||||||
|
result: Job[],
|
||||||
|
jobsWithSatisfiedNeeds: nodeInfo[],
|
||||||
|
alljobsWithUnsatisfiedNeeds: nodeInfo[]
|
||||||
|
) {
|
||||||
|
if (jobsWithSatisfiedNeeds.length == 0) {
|
||||||
|
context.error(
|
||||||
|
token,
|
||||||
|
"The workflow must contain at least one job with no dependencies."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Figure out which nodes would start after current completes
|
||||||
|
while (jobsWithSatisfiedNeeds.length > 0) {
|
||||||
|
const currentJob = jobsWithSatisfiedNeeds.shift()
|
||||||
|
if (currentJob == undefined) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
for (let i = alljobsWithUnsatisfiedNeeds.length - 1; i >= 0; i--) {
|
||||||
|
const unsatisfiedJob = alljobsWithUnsatisfiedNeeds[i]
|
||||||
|
for (let j = unsatisfiedJob.needs.length - 1; j >= 0; j--) {
|
||||||
|
const need = unsatisfiedJob.needs[j]
|
||||||
|
if (need.value == currentJob.name) {
|
||||||
|
unsatisfiedJob.needs.splice(j, 1)
|
||||||
|
if (unsatisfiedJob.needs.length == 0) {
|
||||||
|
jobsWithSatisfiedNeeds.push(unsatisfiedJob)
|
||||||
|
alljobsWithUnsatisfiedNeeds.splice(i, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check whether some jobs will never execute
|
||||||
|
if (alljobsWithUnsatisfiedNeeds.length > 0) {
|
||||||
|
const jobNames = result.map((x) => x.id.value)
|
||||||
|
for (const unsatisfiedJob of alljobsWithUnsatisfiedNeeds) {
|
||||||
|
for (const need of unsatisfiedJob.needs) {
|
||||||
|
if (jobNames.includes(need.value)) {
|
||||||
|
context.error(
|
||||||
|
need,
|
||||||
|
`Job '${unsatisfiedJob.name}' depends on job '${need.value}' which creates a cycle in the dependency graph.`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
context.error(
|
||||||
|
need,
|
||||||
|
`Job '${unsatisfiedJob.name}' depends on unknown job '${need.value}'.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertJob(
|
||||||
|
context: TemplateContext,
|
||||||
|
jobKey: StringToken,
|
||||||
|
token: MappingToken
|
||||||
|
): Job {
|
||||||
|
const error = new IdBuilder().tryAddKnownId(jobKey.value)
|
||||||
|
if (error) {
|
||||||
|
context.error(jobKey, error)
|
||||||
|
}
|
||||||
|
const result: Job = {
|
||||||
|
type: "job",
|
||||||
|
id: jobKey,
|
||||||
|
name: undefined,
|
||||||
|
needs: undefined,
|
||||||
|
if: new BasicExpressionToken(
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
"success()",
|
||||||
|
undefined,
|
||||||
|
undefined
|
||||||
|
),
|
||||||
|
env: undefined,
|
||||||
|
concurrency: undefined,
|
||||||
|
environment: undefined,
|
||||||
|
strategy: undefined,
|
||||||
|
"runs-on": undefined,
|
||||||
|
container: undefined,
|
||||||
|
services: undefined,
|
||||||
|
outputs: undefined,
|
||||||
|
steps: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of token) {
|
||||||
|
const propertyName = item.key.assertString("job property name")
|
||||||
|
switch (propertyName.value) {
|
||||||
|
case "concurrency":
|
||||||
|
handleTemplateTokenErrors(item.value, context, undefined, () =>
|
||||||
|
convertConcurrency(context, item.value)
|
||||||
|
)
|
||||||
|
result.concurrency = item.value
|
||||||
|
break
|
||||||
|
|
||||||
|
case "container":
|
||||||
|
// Do early validation, but don't convert
|
||||||
|
convertToJobContainer(context, item.value)
|
||||||
|
result.container = item.value
|
||||||
|
break
|
||||||
|
|
||||||
|
case "env":
|
||||||
|
result.env = item.value.assertMapping("job env")
|
||||||
|
break
|
||||||
|
|
||||||
|
case "environment":
|
||||||
|
handleTemplateTokenErrors(item.value, context, undefined, () =>
|
||||||
|
convertToActionsEnvironmentRef(context, item.value)
|
||||||
|
)
|
||||||
|
result.environment = item.value
|
||||||
|
break
|
||||||
|
|
||||||
|
case "name":
|
||||||
|
result.name = item.value.assertScalar("job name")
|
||||||
|
break
|
||||||
|
|
||||||
|
case "needs":
|
||||||
|
result.needs = []
|
||||||
|
if (isString(item.value)) {
|
||||||
|
const jobNeeds = item.value.assertString("job needs id")
|
||||||
|
result.needs.push(jobNeeds)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSequence(item.value)) {
|
||||||
|
for (const seqItem of item.value) {
|
||||||
|
const jobNeeds = seqItem.assertString("job needs id")
|
||||||
|
result.needs.push(jobNeeds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case "outputs":
|
||||||
|
result.outputs = item.value.assertMapping("job outputs")
|
||||||
|
break
|
||||||
|
|
||||||
|
case "runs-on":
|
||||||
|
handleTemplateTokenErrors(item.value, context, undefined, () =>
|
||||||
|
convertRunsOn(context, item.value)
|
||||||
|
)
|
||||||
|
result["runs-on"] = item.value
|
||||||
|
break
|
||||||
|
|
||||||
|
case "services":
|
||||||
|
// Do early validation, but don't convert
|
||||||
|
convertToJobServices(context, item.value)
|
||||||
|
result.services = item.value
|
||||||
|
break
|
||||||
|
|
||||||
|
case "steps":
|
||||||
|
result.steps = convertSteps(context, item.value)
|
||||||
|
break
|
||||||
|
|
||||||
|
case "strategy":
|
||||||
|
result.strategy = item.value
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.name) {
|
||||||
|
result.name = result.id
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
type RunsOn = {
|
||||||
|
labels: Set<string>
|
||||||
|
group: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertRunsOn(context: TemplateContext, token: TemplateToken): RunsOn {
|
||||||
|
const labels = convertRunsOnLabels(token)
|
||||||
|
|
||||||
|
if (!isMapping(token)) {
|
||||||
|
return {
|
||||||
|
labels,
|
||||||
|
group: "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let group = ""
|
||||||
|
|
||||||
|
for (const item of token) {
|
||||||
|
const key = item.key.assertString("job runs-on property name")
|
||||||
|
switch (key.value) {
|
||||||
|
case "group": {
|
||||||
|
if (item.value.isExpression) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupName = item.value.assertString(
|
||||||
|
"job runs-on group name"
|
||||||
|
).value
|
||||||
|
const names = groupName.split("/")
|
||||||
|
switch (names.length) {
|
||||||
|
case 1: {
|
||||||
|
group = groupName
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
|
if (
|
||||||
|
!["org", "organization", "ent", "enterprise"].includes(names[0])
|
||||||
|
) {
|
||||||
|
context.error(
|
||||||
|
item.value,
|
||||||
|
`Invalid runs-on group name '${groupName}. Please use 'organization/' or 'enterprise/' prefix to target a single runner group.'`
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (!names[1]) {
|
||||||
|
context.error(
|
||||||
|
item.value,
|
||||||
|
`Invalid runs-on group name '${groupName}'.`
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
group = groupName
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
context.error(
|
||||||
|
item.value,
|
||||||
|
`Invalid runs-on group name '${groupName}. Please use 'organization/' or 'enterprise/' prefix to target a single runner group.'`
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case "labels": {
|
||||||
|
const mapLabels = convertRunsOnLabels(item.value)
|
||||||
|
for (const label of mapLabels) {
|
||||||
|
labels.add(label)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
labels,
|
||||||
|
group,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertRunsOnLabels(token: TemplateToken): Set<string> {
|
||||||
|
const labels = new Set<string>()
|
||||||
|
if (token.isExpression) {
|
||||||
|
return labels
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isString(token)) {
|
||||||
|
labels.add(token.value)
|
||||||
|
return labels
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSequence(token)) {
|
||||||
|
for (const item of token) {
|
||||||
|
if (item.isExpression) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = item.assertString("job runs-on label sequence item")
|
||||||
|
labels.add(label.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return labels
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
import { TemplateContext } from "../../templates/template-context"
|
||||||
|
import {
|
||||||
|
BasicExpressionToken,
|
||||||
|
MappingToken,
|
||||||
|
ScalarToken,
|
||||||
|
StringToken,
|
||||||
|
TemplateToken,
|
||||||
|
} from "../../templates/tokens"
|
||||||
|
import { isSequence } from "../../templates/tokens/type-guards"
|
||||||
|
import { isActionStep } from "../type-guards"
|
||||||
|
import { ActionStep, Step } from "../workflow-template"
|
||||||
|
import { handleTemplateTokenErrors } from "./handle-errors"
|
||||||
|
import { IdBuilder } from "./id-builder"
|
||||||
|
|
||||||
|
export function convertSteps(
|
||||||
|
context: TemplateContext,
|
||||||
|
steps: TemplateToken
|
||||||
|
): Step[] {
|
||||||
|
if (!isSequence(steps)) {
|
||||||
|
context.error(steps, "Invalid format for steps")
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const idBuilder = new IdBuilder()
|
||||||
|
|
||||||
|
const result: Step[] = []
|
||||||
|
for (const item of steps) {
|
||||||
|
const step = handleTemplateTokenErrors(steps, context, undefined, () =>
|
||||||
|
convertStep(context, idBuilder, item)
|
||||||
|
)
|
||||||
|
if (step) {
|
||||||
|
result.push(step)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const step of result) {
|
||||||
|
if (step.id) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = ""
|
||||||
|
if (isActionStep(step)) {
|
||||||
|
id = createActionStepId(step)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
id = "run"
|
||||||
|
}
|
||||||
|
|
||||||
|
idBuilder.appendSegment(`__${id}`)
|
||||||
|
step.id = idBuilder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertStep(
|
||||||
|
context: TemplateContext,
|
||||||
|
idBuilder: IdBuilder,
|
||||||
|
step: TemplateToken
|
||||||
|
): Step | undefined {
|
||||||
|
const mapping = step.assertMapping("steps item")
|
||||||
|
|
||||||
|
let run: ScalarToken | undefined
|
||||||
|
let id: StringToken | undefined
|
||||||
|
let name: ScalarToken | undefined
|
||||||
|
let uses: StringToken | undefined
|
||||||
|
let continueOnError: boolean | undefined
|
||||||
|
let env: MappingToken | undefined
|
||||||
|
const ifCondition = new BasicExpressionToken(
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
"success()",
|
||||||
|
undefined,
|
||||||
|
undefined
|
||||||
|
)
|
||||||
|
for (const item of mapping) {
|
||||||
|
const key = item.key.assertString("steps item key")
|
||||||
|
switch (key.value) {
|
||||||
|
case "id":
|
||||||
|
id = item.value.assertString("steps item id")
|
||||||
|
if (id) {
|
||||||
|
const error = idBuilder.tryAddKnownId(id.value)
|
||||||
|
if (error) {
|
||||||
|
context.error(id, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case "name":
|
||||||
|
name = item.value.assertScalar("steps item name")
|
||||||
|
break
|
||||||
|
case "run":
|
||||||
|
run = item.value.assertScalar("steps item run")
|
||||||
|
break
|
||||||
|
case "uses":
|
||||||
|
uses = item.value.assertString("steps item uses")
|
||||||
|
break
|
||||||
|
case "env":
|
||||||
|
env = item.value.assertMapping("step env")
|
||||||
|
break
|
||||||
|
case "continue-on-error":
|
||||||
|
continueOnError = item.value.assertBoolean(
|
||||||
|
"steps item continue-on-error"
|
||||||
|
).value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (run) {
|
||||||
|
return {
|
||||||
|
id: id?.value || "",
|
||||||
|
name,
|
||||||
|
if: ifCondition,
|
||||||
|
"continue-on-error": continueOnError,
|
||||||
|
env,
|
||||||
|
run,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uses) {
|
||||||
|
return {
|
||||||
|
id: id?.value || "",
|
||||||
|
name,
|
||||||
|
if: ifCondition,
|
||||||
|
"continue-on-error": continueOnError,
|
||||||
|
env,
|
||||||
|
uses,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
context.error(step, "Expected uses or run to be defined")
|
||||||
|
}
|
||||||
|
|
||||||
|
function createActionStepId(step: ActionStep): string {
|
||||||
|
const uses = step.uses.value
|
||||||
|
if (uses.startsWith("docker://")) {
|
||||||
|
return uses.substring("docker://".length)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uses.startsWith("./") || uses.startsWith(".\\")) {
|
||||||
|
return "self"
|
||||||
|
}
|
||||||
|
|
||||||
|
const segments = uses.split("@")
|
||||||
|
if (segments.length != 2) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathSegments = segments[0].split(/[\\/]/).filter((s) => s.length > 0)
|
||||||
|
const gitRef = segments[1]
|
||||||
|
|
||||||
|
if (
|
||||||
|
pathSegments.length >= 2 &&
|
||||||
|
pathSegments[0] &&
|
||||||
|
pathSegments[1] &&
|
||||||
|
gitRef
|
||||||
|
) {
|
||||||
|
return `${pathSegments[0]}/${pathSegments[1]}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { SequenceToken } from "../../templates/tokens/sequence-token"
|
||||||
|
|
||||||
|
export function convertStringList(
|
||||||
|
name: string,
|
||||||
|
token: SequenceToken
|
||||||
|
): string[] {
|
||||||
|
const result = [] as string[]
|
||||||
|
|
||||||
|
for (const item of token) {
|
||||||
|
result.push(item.assertString(`${name} item`).value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import { TemplateContext } from "../../templates/template-context"
|
||||||
|
import { MappingToken } from "../../templates/tokens/mapping-token"
|
||||||
|
import { ScalarToken } from "../../templates/tokens/scalar-token"
|
||||||
|
import {
|
||||||
|
InputConfig,
|
||||||
|
InputType,
|
||||||
|
WorkflowDispatchConfig,
|
||||||
|
} from "../workflow-template"
|
||||||
|
import { convertStringList } from "./string-list"
|
||||||
|
|
||||||
|
export function convertEventWorkflowDispatchInputs(
|
||||||
|
context: TemplateContext,
|
||||||
|
token: MappingToken
|
||||||
|
): WorkflowDispatchConfig {
|
||||||
|
const result: WorkflowDispatchConfig = {}
|
||||||
|
|
||||||
|
for (const item of token) {
|
||||||
|
const key = item.key.assertString("workflow dispatch input key")
|
||||||
|
|
||||||
|
switch (key.value) {
|
||||||
|
case "inputs":
|
||||||
|
result.inputs = convertWorkflowDispatchInputs(
|
||||||
|
context,
|
||||||
|
item.value.assertMapping("workflow dispatch inputs")
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertWorkflowDispatchInputs(
|
||||||
|
context: TemplateContext,
|
||||||
|
token: MappingToken
|
||||||
|
): {
|
||||||
|
[inputName: string]: InputConfig
|
||||||
|
} {
|
||||||
|
const result: { [inputName: string]: InputConfig } = {}
|
||||||
|
|
||||||
|
for (const item of token) {
|
||||||
|
const inputName = item.key.assertString("input name")
|
||||||
|
const inputMapping = item.value.assertMapping("input configuration")
|
||||||
|
|
||||||
|
result[inputName.value] = convertWorkflowDispatchInput(
|
||||||
|
context,
|
||||||
|
inputMapping
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertWorkflowDispatchInput(
|
||||||
|
context: TemplateContext,
|
||||||
|
token: MappingToken
|
||||||
|
): InputConfig {
|
||||||
|
const result: InputConfig = {
|
||||||
|
type: InputType.string, // Default to string
|
||||||
|
}
|
||||||
|
|
||||||
|
let defaultValue: undefined | ScalarToken
|
||||||
|
|
||||||
|
for (const item of token) {
|
||||||
|
const key = item.key.assertString("workflow dispatch input key")
|
||||||
|
|
||||||
|
switch (key.value) {
|
||||||
|
case "description":
|
||||||
|
result.description = item.value.assertString("input description").value
|
||||||
|
break
|
||||||
|
|
||||||
|
case "required":
|
||||||
|
result.required = item.value.assertBoolean("input required").value
|
||||||
|
break
|
||||||
|
|
||||||
|
case "default":
|
||||||
|
defaultValue = item.value.assertScalar("input default")
|
||||||
|
break
|
||||||
|
|
||||||
|
case "type":
|
||||||
|
result.type =
|
||||||
|
InputType[
|
||||||
|
item.value.assertString("input type")
|
||||||
|
.value as keyof typeof InputType
|
||||||
|
]
|
||||||
|
break
|
||||||
|
|
||||||
|
case "options":
|
||||||
|
result.options = convertStringList(
|
||||||
|
"input options",
|
||||||
|
item.value.assertSequence("input options")
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
context.error(item.key, `Invalid key '${key.value}'`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate default value
|
||||||
|
if (defaultValue !== undefined) {
|
||||||
|
try {
|
||||||
|
switch (result.type) {
|
||||||
|
case InputType.boolean:
|
||||||
|
result.default = defaultValue.assertBoolean("input default").value
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
|
case InputType.string:
|
||||||
|
case InputType.choice:
|
||||||
|
case InputType.environment:
|
||||||
|
result.default = defaultValue.assertString("input default").value
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
context.error(defaultValue, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate `options` for `choice` type
|
||||||
|
if (result.type === InputType.choice) {
|
||||||
|
if (result.options === undefined || result.options.length === 0) {
|
||||||
|
context.error(token, "Missing 'options' for choice input")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (result.options !== undefined) {
|
||||||
|
context.error(
|
||||||
|
token,
|
||||||
|
"Input type is not 'choice', but 'options' is defined"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { ActionStep, RunStep, Step } from "./workflow-template"
|
||||||
|
|
||||||
|
export function isRunStep(step: Step): step is RunStep {
|
||||||
|
return (step as RunStep).run !== undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isActionStep(step: Step): step is ActionStep {
|
||||||
|
return (step as ActionStep).uses !== undefined
|
||||||
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
import {
|
||||||
|
BasicExpressionToken,
|
||||||
|
MappingToken,
|
||||||
|
ScalarToken,
|
||||||
|
SequenceToken,
|
||||||
|
StringToken,
|
||||||
|
TemplateToken,
|
||||||
|
} from "../templates/tokens"
|
||||||
|
|
||||||
|
export type WorkflowTemplate = {
|
||||||
|
events: EventsConfig
|
||||||
|
jobs: Job[]
|
||||||
|
concurrency: TemplateToken
|
||||||
|
env: TemplateToken
|
||||||
|
|
||||||
|
errors?: {
|
||||||
|
Message: string
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ConcurrencySetting = {
|
||||||
|
group?: StringToken
|
||||||
|
cancelInProgress?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ActionsEnvironmentReference = {
|
||||||
|
name?: TemplateToken
|
||||||
|
url?: TemplateToken
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Job = {
|
||||||
|
type: string
|
||||||
|
id: StringToken
|
||||||
|
name?: ScalarToken
|
||||||
|
needs?: StringToken[]
|
||||||
|
if: BasicExpressionToken
|
||||||
|
env?: MappingToken
|
||||||
|
concurrency?: TemplateToken
|
||||||
|
environment?: TemplateToken
|
||||||
|
strategy?: TemplateToken
|
||||||
|
"runs-on"?: TemplateToken
|
||||||
|
container?: TemplateToken
|
||||||
|
services?: TemplateToken
|
||||||
|
outputs?: MappingToken
|
||||||
|
steps: Step[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Container = {
|
||||||
|
image: StringToken
|
||||||
|
credentials?: Credential
|
||||||
|
env?: MappingToken
|
||||||
|
ports?: SequenceToken
|
||||||
|
volumes?: SequenceToken
|
||||||
|
options?: StringToken
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Credential = {
|
||||||
|
username: StringToken | undefined
|
||||||
|
password: StringToken | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Step = ActionStep | RunStep
|
||||||
|
|
||||||
|
type BaseStep = {
|
||||||
|
id: string
|
||||||
|
name?: ScalarToken
|
||||||
|
if: BasicExpressionToken
|
||||||
|
"continue-on-error"?: boolean
|
||||||
|
env?: MappingToken
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RunStep = BaseStep & {
|
||||||
|
run: ScalarToken
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ActionStep = BaseStep & {
|
||||||
|
uses: StringToken
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EventsConfig = {
|
||||||
|
schedule?: ScheduleConfig[]
|
||||||
|
workflow_dispatch?: WorkflowDispatchConfig
|
||||||
|
workflow_call?: WorkflowCallConfig
|
||||||
|
|
||||||
|
// Events that support filters
|
||||||
|
pull_request?: BranchFilterConfig & PathFilterConfig & TypesFilterConfig
|
||||||
|
pull_request_target?: BranchFilterConfig &
|
||||||
|
PathFilterConfig &
|
||||||
|
TypesFilterConfig
|
||||||
|
push?: BranchFilterConfig & TagFilterConfig & PathFilterConfig
|
||||||
|
workflow_run?: WorkflowFilterConfig & BranchFilterConfig & TypesFilterConfig
|
||||||
|
|
||||||
|
// Events that only support activity types
|
||||||
|
branch_protection_rule?: TypesFilterConfig
|
||||||
|
check_run?: TypesFilterConfig
|
||||||
|
check_suite?: TypesFilterConfig
|
||||||
|
disccusion?: TypesFilterConfig
|
||||||
|
disccusion_comment?: TypesFilterConfig
|
||||||
|
issue_comment?: TypesFilterConfig
|
||||||
|
issues?: TypesFilterConfig
|
||||||
|
label?: TypesFilterConfig
|
||||||
|
merge_group?: TypesFilterConfig
|
||||||
|
milestone?: TypesFilterConfig
|
||||||
|
project?: TypesFilterConfig
|
||||||
|
project_card?: TypesFilterConfig
|
||||||
|
project_column?: TypesFilterConfig
|
||||||
|
pull_request_review?: TypesFilterConfig
|
||||||
|
pull_request_review_comment?: TypesFilterConfig
|
||||||
|
registry_package?: TypesFilterConfig
|
||||||
|
repository_dispatch?: TypesFilterConfig
|
||||||
|
release?: TypesFilterConfig
|
||||||
|
watch?: TypesFilterConfig
|
||||||
|
|
||||||
|
// Index signature to allow easier lookup
|
||||||
|
[eventName: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TypesFilterConfig = {
|
||||||
|
types?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BranchFilterConfig = {
|
||||||
|
branches?: string[]
|
||||||
|
"branches-ignore"?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TagFilterConfig = {
|
||||||
|
tags?: string[]
|
||||||
|
"tags-ignore"?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PathFilterConfig = {
|
||||||
|
paths?: string[]
|
||||||
|
"paths-ignore"?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WorkflowDispatchConfig = {
|
||||||
|
inputs?: { [inputName: string]: InputConfig }
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WorkflowCallConfig = {
|
||||||
|
inputs: { [inputName: string]: InputConfig }
|
||||||
|
// TODO - these are supported in C# and Go but not in TS yet
|
||||||
|
// outputs: { [outputName: string]: OutputConfig }
|
||||||
|
// secrets: { [secretName: string]: SecretConfig }
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum InputType {
|
||||||
|
string = "string",
|
||||||
|
choice = "choice",
|
||||||
|
boolean = "boolean",
|
||||||
|
environment = "environment",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InputConfig = {
|
||||||
|
type: InputType
|
||||||
|
description?: string
|
||||||
|
required?: boolean
|
||||||
|
default?: string | boolean | number
|
||||||
|
options?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ScheduleConfig = {
|
||||||
|
cron: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WorkflowFilterConfig = {
|
||||||
|
workflows?: string[]
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { FunctionInfo } from "@github/actions-expressions/funcs/info"
|
||||||
|
import { MAX_CONSTANT } from "./template-constants"
|
||||||
|
|
||||||
|
export function splitAllowedContext(allowedContext: string[]): {
|
||||||
|
namedContexts: string[]
|
||||||
|
functions: FunctionInfo[]
|
||||||
|
} {
|
||||||
|
const FUNCTION_REGEXP = /^([a-zA-Z0-9_]+)\(([0-9]+),([0-9]+|MAX)\)$/
|
||||||
|
|
||||||
|
const namedContexts: string[] = []
|
||||||
|
const functions: FunctionInfo[] = []
|
||||||
|
if (allowedContext.length > 0) {
|
||||||
|
for (const contextItem of allowedContext) {
|
||||||
|
const match = contextItem.match(FUNCTION_REGEXP)
|
||||||
|
if (match) {
|
||||||
|
const functionName = match[1]
|
||||||
|
const minParameters = Number.parseInt(match[2])
|
||||||
|
const maxParametersRaw = match[3]
|
||||||
|
const maxParameters =
|
||||||
|
maxParametersRaw === MAX_CONSTANT
|
||||||
|
? Number.MAX_SAFE_INTEGER
|
||||||
|
: Number.parseInt(maxParametersRaw)
|
||||||
|
functions.push(<FunctionInfo>{
|
||||||
|
name: functionName,
|
||||||
|
minArgs: minParameters,
|
||||||
|
maxArgs: maxParameters,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
namedContexts.push(contextItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
namedContexts: namedContexts,
|
||||||
|
functions: functions,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
import { Evaluator, Lexer, Parser } from "@github/actions-expressions"
|
||||||
|
import { Expr } from "@github/actions-expressions/ast"
|
||||||
|
import {
|
||||||
|
Dictionary,
|
||||||
|
ExpressionData,
|
||||||
|
Kind,
|
||||||
|
} from "@github/actions-expressions/data/index"
|
||||||
|
import { FunctionInfo } from "@github/actions-expressions/funcs/info"
|
||||||
|
import { TemplateContext } from "./template-context"
|
||||||
|
import {
|
||||||
|
BasicExpressionToken,
|
||||||
|
BooleanToken,
|
||||||
|
LiteralToken,
|
||||||
|
MappingToken,
|
||||||
|
NullToken,
|
||||||
|
NumberToken,
|
||||||
|
SequenceToken,
|
||||||
|
StringToken,
|
||||||
|
TemplateToken,
|
||||||
|
} from "./tokens"
|
||||||
|
import { TokenType } from "./tokens/types"
|
||||||
|
|
||||||
|
export function evaluateStringToken(
|
||||||
|
token: BasicExpressionToken,
|
||||||
|
context: TemplateContext
|
||||||
|
): TemplateToken {
|
||||||
|
const expr = parseExpression(
|
||||||
|
token.expression,
|
||||||
|
context.expressionNamedContexts,
|
||||||
|
context.expressionFunctions
|
||||||
|
)
|
||||||
|
if (!expr) {
|
||||||
|
throw new Error("Unexpected empty expression")
|
||||||
|
}
|
||||||
|
// TODO: Pass in context
|
||||||
|
const evaluator = new Evaluator(expr, new Dictionary())
|
||||||
|
const result = evaluator.evaluate()
|
||||||
|
if (!result.primitive) {
|
||||||
|
context.error(token, "Expected a string")
|
||||||
|
return new StringToken(
|
||||||
|
token.file,
|
||||||
|
token.range,
|
||||||
|
token.expression,
|
||||||
|
token.definitionInfo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return new StringToken(
|
||||||
|
token.file,
|
||||||
|
token.range,
|
||||||
|
result.coerceString(),
|
||||||
|
token.definitionInfo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function evaluateSequenceToken(
|
||||||
|
token: BasicExpressionToken,
|
||||||
|
context: TemplateContext
|
||||||
|
): TemplateToken {
|
||||||
|
const expr = parseExpression(
|
||||||
|
token.expression,
|
||||||
|
context.expressionNamedContexts,
|
||||||
|
context.expressionFunctions
|
||||||
|
)
|
||||||
|
if (!expr) {
|
||||||
|
throw new Error("Unexpected empty expression")
|
||||||
|
}
|
||||||
|
// TODO: Pass in context
|
||||||
|
const evaluator = new Evaluator(expr, new Dictionary())
|
||||||
|
const result = evaluator.evaluate()
|
||||||
|
const value = convertToTemplateToken(token, result)
|
||||||
|
if (value.templateTokenType !== TokenType.Sequence) {
|
||||||
|
context.error(token, "Expected a sequence")
|
||||||
|
return new SequenceToken(token.file, token.range, token.definitionInfo)
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
export function evaluateMappingToken(
|
||||||
|
token: BasicExpressionToken,
|
||||||
|
context: TemplateContext
|
||||||
|
): MappingToken {
|
||||||
|
const expr = parseExpression(
|
||||||
|
token.expression,
|
||||||
|
context.expressionNamedContexts,
|
||||||
|
context.expressionFunctions
|
||||||
|
)
|
||||||
|
if (!expr) {
|
||||||
|
throw new Error("Unexpected empty expression")
|
||||||
|
}
|
||||||
|
// TODO: Pass in context
|
||||||
|
const evaluator = new Evaluator(expr, new Dictionary())
|
||||||
|
const result = evaluator.evaluate()
|
||||||
|
const value = convertToTemplateToken(token, result)
|
||||||
|
if (value.templateTokenType !== TokenType.Mapping) {
|
||||||
|
context.error(token, "Expected a mapping")
|
||||||
|
return new MappingToken(token.file, token.range, token.definitionInfo)
|
||||||
|
}
|
||||||
|
return value as MappingToken
|
||||||
|
}
|
||||||
|
|
||||||
|
export function evaluateTemplateToken(
|
||||||
|
token: BasicExpressionToken,
|
||||||
|
context: TemplateContext
|
||||||
|
): TemplateToken {
|
||||||
|
const expr = parseExpression(
|
||||||
|
token.expression,
|
||||||
|
context.expressionNamedContexts,
|
||||||
|
context.expressionFunctions
|
||||||
|
)
|
||||||
|
if (!expr) {
|
||||||
|
throw new Error("Unexpected empty expression")
|
||||||
|
}
|
||||||
|
// TODO: Pass in context
|
||||||
|
const evaluator = new Evaluator(expr, new Dictionary())
|
||||||
|
const result = evaluator.evaluate()
|
||||||
|
return convertToTemplateToken(token, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertToTemplateToken(
|
||||||
|
token: BasicExpressionToken,
|
||||||
|
result: ExpressionData
|
||||||
|
): TemplateToken {
|
||||||
|
// Literal
|
||||||
|
const literal = convertToLiteralToken(token, result)
|
||||||
|
if (literal) {
|
||||||
|
return literal
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Support expressions that return a sequence or mapping token
|
||||||
|
|
||||||
|
// Leverage the expression SDK to traverse the object
|
||||||
|
switch (result.kind) {
|
||||||
|
case Kind.Dictionary: {
|
||||||
|
const mapping = new MappingToken(
|
||||||
|
token.file,
|
||||||
|
token.range,
|
||||||
|
token.definitionInfo
|
||||||
|
)
|
||||||
|
for (const { key, value } of result.pairs()) {
|
||||||
|
const keyToken = new StringToken(
|
||||||
|
token.file,
|
||||||
|
token.range,
|
||||||
|
key,
|
||||||
|
token.definitionInfo
|
||||||
|
)
|
||||||
|
const valueToken = convertToTemplateToken(token, value)
|
||||||
|
mapping.add(keyToken, valueToken)
|
||||||
|
}
|
||||||
|
return mapping
|
||||||
|
}
|
||||||
|
case Kind.Array: {
|
||||||
|
const sequence = new SequenceToken(
|
||||||
|
token.file,
|
||||||
|
token.range,
|
||||||
|
token.definitionInfo
|
||||||
|
)
|
||||||
|
for (const value of result.values()) {
|
||||||
|
const itemToken = convertToTemplateToken(token, value)
|
||||||
|
sequence.add(itemToken)
|
||||||
|
}
|
||||||
|
return sequence
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error("Unable to convert the object to a template token")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertToLiteralToken(
|
||||||
|
token: BasicExpressionToken,
|
||||||
|
result: ExpressionData
|
||||||
|
): LiteralToken | undefined {
|
||||||
|
switch (result.kind) {
|
||||||
|
case Kind.Null:
|
||||||
|
return new NullToken(token.file, token.range, token.definitionInfo)
|
||||||
|
case Kind.Boolean:
|
||||||
|
return new BooleanToken(
|
||||||
|
token.file,
|
||||||
|
token.range,
|
||||||
|
result.value,
|
||||||
|
token.definitionInfo
|
||||||
|
)
|
||||||
|
case Kind.Number:
|
||||||
|
return new NumberToken(
|
||||||
|
token.file,
|
||||||
|
token.range,
|
||||||
|
result.value,
|
||||||
|
token.definitionInfo
|
||||||
|
)
|
||||||
|
case Kind.String:
|
||||||
|
return new StringToken(
|
||||||
|
token.file,
|
||||||
|
token.range,
|
||||||
|
result.value,
|
||||||
|
token.definitionInfo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseExpression(
|
||||||
|
expression: string,
|
||||||
|
contexts: string[],
|
||||||
|
functions: FunctionInfo[]
|
||||||
|
): Expr {
|
||||||
|
const lexer = new Lexer(expression)
|
||||||
|
const result = lexer.lex()
|
||||||
|
const p = new Parser(result.tokens, contexts, functions)
|
||||||
|
return p.parse()
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user