Merge parser and expressions into repository

This commit is contained in:
Christopher Schleiden
2023-01-06 15:54:31 -08:00
parent 4f961a6247
commit 70e33f999d
328 changed files with 35337 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
@github:registry=https://npm.pkg.github.com
+16
View File
@@ -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"],
};
+51
View File
@@ -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"
}
}
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
npm run build
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
npm test
+103
View File
@@ -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.");
}
}
+156
View File
@@ -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);
});
});
+147
View File
@@ -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);
}
+40
View File
@@ -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;
}
}
+25
View File
@@ -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;
};
+9
View File
@@ -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";
+21
View File
@@ -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;
}
}
+23
View File
@@ -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}');
});
});
+46
View File
@@ -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);
}
);
});
+49
View File
@@ -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;
}
+17
View File
@@ -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);
}
}
+52
View File
@@ -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";
}
}
+27
View File
@@ -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);
});
+245
View File
@@ -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 {}
+63
View File
@@ -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);
}
}
+36
View File
@@ -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);
},
};
+27
View File
@@ -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")
);
});
});
+122
View File
@@ -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;
}
+22
View File
@@ -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;
},
};
+14
View File
@@ -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;
}
+35
View File
@@ -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));
},
};
+14
View File
@@ -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, " "));
},
};
+20
View File
@@ -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;
}
}
}
}
+6
View File
@@ -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";
+144
View File
@@ -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);
}
);
});
+425
View File
@@ -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;
}
+397
View File
@@ -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);
}
}
+101
View File
@@ -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);
}
);
});
+229
View File
@@ -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("");
}
+198
View File
@@ -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
View File
@@ -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"
}
}
]
}
+4
View File
@@ -0,0 +1,4 @@
{
"refer ./operator_not.json": [
]
}
+124
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 }
}
]
}
+171
View File
@@ -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
View File
@@ -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
View File
@@ -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": "monalisa@users.noreply.github.com",
"name": "mona",
"username": "monalisa"
},
"committer": {
"email": "noreply@github.com",
"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": "monalisa@users.noreply.github.com",
"name": "mona",
"username": "monalisa"
},
"committer": {
"email": "noreply@github.com",
"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": "monalisa@users.noreply.github.com",
"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": "monalisa@users.noreply.github.com",
"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": "git@github.com: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
View File
@@ -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}"
}
}
]
}
+10
View File
@@ -0,0 +1,10 @@
{
"exclude": ["./src/**/*.test.ts"],
"extends": "./tsconfig.json",
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"noEmit": false,
"outDir": "./dist"
}
}
+18
View File
@@ -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"]
}
}
+21
View File
@@ -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"
}
}
+2
View File
@@ -0,0 +1,2 @@
/dist
/node_modules
+2
View File
@@ -0,0 +1,2 @@
@github:registry=https://npm.pkg.github.com
+3
View File
@@ -0,0 +1,3 @@
/dist
/node_modules
/src/workflows/workflow-v1.0.json
+3
View File
@@ -0,0 +1,3 @@
{
"semi": false
}
+20
View File
@@ -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"
}
}
]
}
+15
View File
@@ -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,
},
],
},
}
+63
View File
@@ -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")
}
})
})
+121
View File
@@ -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
}
}
}
})
})
+5
View File
@@ -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