@cloudpss/expression 0.6.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/analyze.d.ts +7 -0
- package/dist/analyze.d.ts.map +1 -0
- package/dist/analyze.js +38 -0
- package/dist/analyze.js.map +1 -0
- package/dist/context.d.ts +41 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +18 -0
- package/dist/context.js.map +1 -0
- package/dist/definitions/argument.d.ts +33 -0
- package/dist/definitions/argument.d.ts.map +1 -0
- package/dist/definitions/argument.js +2 -0
- package/dist/definitions/argument.js.map +1 -0
- package/dist/definitions/constraint.d.ts +32 -0
- package/dist/definitions/constraint.d.ts.map +1 -0
- package/dist/definitions/constraint.js +20 -0
- package/dist/definitions/constraint.js.map +1 -0
- package/dist/definitions/parameter-decoration.d.ts +27 -0
- package/dist/definitions/parameter-decoration.d.ts.map +1 -0
- package/dist/definitions/parameter-decoration.js +17 -0
- package/dist/definitions/parameter-decoration.js.map +1 -0
- package/dist/definitions/parameter-group.d.ts +38 -0
- package/dist/definitions/parameter-group.d.ts.map +1 -0
- package/dist/definitions/parameter-group.js +31 -0
- package/dist/definitions/parameter-group.js.map +1 -0
- package/dist/definitions/parameter.d.ts +214 -0
- package/dist/definitions/parameter.d.ts.map +1 -0
- package/dist/definitions/parameter.js +44 -0
- package/dist/definitions/parameter.js.map +1 -0
- package/dist/definitions/variable.d.ts +14 -0
- package/dist/definitions/variable.d.ts.map +1 -0
- package/dist/definitions/variable.js +14 -0
- package/dist/definitions/variable.js.map +1 -0
- package/dist/definitions.d.ts +7 -0
- package/dist/definitions.d.ts.map +1 -0
- package/dist/definitions.js +7 -0
- package/dist/definitions.js.map +1 -0
- package/dist/eval.d.ts +15 -0
- package/dist/eval.d.ts.map +1 -0
- package/dist/eval.js +54 -0
- package/dist/eval.js.map +1 -0
- package/dist/expression.d.ts +37 -0
- package/dist/expression.d.ts.map +1 -0
- package/dist/expression.js +18 -0
- package/dist/expression.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/main.d.ts +49 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +238 -0
- package/dist/main.js.map +1 -0
- package/dist/migrate.d.ts +5 -0
- package/dist/migrate.d.ts.map +1 -0
- package/dist/migrate.js +23 -0
- package/dist/migrate.js.map +1 -0
- package/dist/migrator/access.d.ts +6 -0
- package/dist/migrator/access.d.ts.map +1 -0
- package/dist/migrator/access.js +176 -0
- package/dist/migrator/access.js.map +1 -0
- package/dist/migrator/call.d.ts +6 -0
- package/dist/migrator/call.d.ts.map +1 -0
- package/dist/migrator/call.js +405 -0
- package/dist/migrator/call.js.map +1 -0
- package/dist/migrator/concat.d.ts +6 -0
- package/dist/migrator/concat.d.ts.map +1 -0
- package/dist/migrator/concat.js +32 -0
- package/dist/migrator/concat.js.map +1 -0
- package/dist/migrator/condition.d.ts +6 -0
- package/dist/migrator/condition.d.ts.map +1 -0
- package/dist/migrator/condition.js +110 -0
- package/dist/migrator/condition.js.map +1 -0
- package/dist/migrator/interface.d.ts +24 -0
- package/dist/migrator/interface.d.ts.map +1 -0
- package/dist/migrator/interface.js +8 -0
- package/dist/migrator/interface.js.map +1 -0
- package/dist/migrator/node.d.ts +12 -0
- package/dist/migrator/node.d.ts.map +1 -0
- package/dist/migrator/node.js +119 -0
- package/dist/migrator/node.js.map +1 -0
- package/dist/migrator/operator.d.ts +6 -0
- package/dist/migrator/operator.d.ts.map +1 -0
- package/dist/migrator/operator.js +299 -0
- package/dist/migrator/operator.js.map +1 -0
- package/dist/migrator/parser.d.ts +3 -0
- package/dist/migrator/parser.d.ts.map +1 -0
- package/dist/migrator/parser.js +76 -0
- package/dist/migrator/parser.js.map +1 -0
- package/dist/migrator/special.d.ts +4 -0
- package/dist/migrator/special.d.ts.map +1 -0
- package/dist/migrator/special.js +26 -0
- package/dist/migrator/special.js.map +1 -0
- package/dist/migrator/state.d.ts +64 -0
- package/dist/migrator/state.d.ts.map +1 -0
- package/dist/migrator/state.js +251 -0
- package/dist/migrator/state.js.map +1 -0
- package/dist/migrator/symbol.d.ts +6 -0
- package/dist/migrator/symbol.d.ts.map +1 -0
- package/dist/migrator/symbol.js +50 -0
- package/dist/migrator/symbol.js.map +1 -0
- package/dist/migrator/to-type.d.ts +10 -0
- package/dist/migrator/to-type.d.ts.map +1 -0
- package/dist/migrator/to-type.js +89 -0
- package/dist/migrator/to-type.js.map +1 -0
- package/dist/migrator/utils.d.ts +14 -0
- package/dist/migrator/utils.d.ts.map +1 -0
- package/dist/migrator/utils.js +64 -0
- package/dist/migrator/utils.js.map +1 -0
- package/dist/parser.d.ts +41 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +36 -0
- package/dist/parser.js.map +1 -0
- package/dist/scope.d.ts +43 -0
- package/dist/scope.d.ts.map +1 -0
- package/dist/scope.js +204 -0
- package/dist/scope.js.map +1 -0
- package/dist/type.d.ts +21 -0
- package/dist/type.d.ts.map +1 -0
- package/dist/type.js +50 -0
- package/dist/type.js.map +1 -0
- package/jest.config.js +3 -0
- package/package.json +26 -0
- package/src/analyze.ts +44 -0
- package/src/context.ts +54 -0
- package/src/definitions/argument.ts +40 -0
- package/src/definitions/constraint.ts +49 -0
- package/src/definitions/parameter-decoration.ts +42 -0
- package/src/definitions/parameter-group.ts +62 -0
- package/src/definitions/parameter.ts +276 -0
- package/src/definitions/variable.ts +23 -0
- package/src/definitions.ts +6 -0
- package/src/eval.ts +64 -0
- package/src/expression.ts +62 -0
- package/src/index.ts +5 -0
- package/src/main.ts +295 -0
- package/src/migrate.ts +25 -0
- package/src/migrator/access.ts +201 -0
- package/src/migrator/call.ts +397 -0
- package/src/migrator/concat.ts +28 -0
- package/src/migrator/condition.ts +120 -0
- package/src/migrator/interface.ts +30 -0
- package/src/migrator/node.ts +131 -0
- package/src/migrator/operator.ts +339 -0
- package/src/migrator/parser.ts +106 -0
- package/src/migrator/special.ts +30 -0
- package/src/migrator/state.ts +262 -0
- package/src/migrator/symbol.ts +53 -0
- package/src/migrator/to-type.ts +93 -0
- package/src/migrator/utils.ts +67 -0
- package/src/parser.ts +69 -0
- package/src/scope.ts +208 -0
- package/src/type.ts +66 -0
- package/tests/analyze.ts +15 -0
- package/tests/condition.ts +49 -0
- package/tests/definition.ts +42 -0
- package/tests/eval-complex.ts +150 -0
- package/tests/eval.ts +42 -0
- package/tests/functions.ts +22 -0
- package/tests/import.ts +31 -0
- package/tests/scope.ts +33 -0
- package/tests/setup.ts +41 -0
- package/tests/tsconfig.json +3 -0
- package/tests/unwrap.ts +46 -0
- package/tsconfig.json +3 -0
package/src/scope.ts
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { createVmContext, isVmExtern, type VmAny, type VmContext, type VmValue } from '@mirascript/mirascript';
|
|
2
|
+
import type { Context } from './context.js';
|
|
3
|
+
import {
|
|
4
|
+
type CompiledExpression,
|
|
5
|
+
type Expression,
|
|
6
|
+
type ExpressionOrValue,
|
|
7
|
+
ExpressionTag,
|
|
8
|
+
isExpression,
|
|
9
|
+
} from './expression.js';
|
|
10
|
+
import { DEFAULTS, evaluateEval } from './eval.js';
|
|
11
|
+
import { parse } from './parser.js';
|
|
12
|
+
import { TypeInfo } from './type.js';
|
|
13
|
+
const { hasOwn } = Object;
|
|
14
|
+
|
|
15
|
+
const RAW = Symbol.for('@private/expression:raw');
|
|
16
|
+
|
|
17
|
+
/** 是否需要创建代理对象 */
|
|
18
|
+
function needsProxy(value: unknown): value is Expression | object {
|
|
19
|
+
return isExpression(value) || (value != null && typeof value == 'object');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** 创建代理对象 */
|
|
23
|
+
function createProxy(value: Expression | object, scope: Scope, path: readonly PropertyKey[]): unknown {
|
|
24
|
+
// 先检查 isExpression,因为 'function' 也有可能是 Expression
|
|
25
|
+
if (isExpression(value)) {
|
|
26
|
+
return scope.eval(value, path);
|
|
27
|
+
}
|
|
28
|
+
if (typeof value != 'object' || value == null) {
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
if (
|
|
32
|
+
value instanceof Date ||
|
|
33
|
+
value instanceof Promise ||
|
|
34
|
+
value instanceof RegExp ||
|
|
35
|
+
ArrayBuffer.isView(value) ||
|
|
36
|
+
value instanceof ArrayBuffer ||
|
|
37
|
+
(typeof SharedArrayBuffer != 'undefined' && value instanceof SharedArrayBuffer)
|
|
38
|
+
) {
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
return new Proxy(value, new ScopeProxyHandler(scope, path));
|
|
42
|
+
}
|
|
43
|
+
/** 函数的执行环境 */
|
|
44
|
+
class ScopeProxyHandler implements ProxyHandler<object> {
|
|
45
|
+
constructor(
|
|
46
|
+
readonly scope: Scope,
|
|
47
|
+
readonly path: readonly PropertyKey[],
|
|
48
|
+
) {}
|
|
49
|
+
/**
|
|
50
|
+
* @inheritdoc
|
|
51
|
+
*/
|
|
52
|
+
get(target: object, p: string | symbol): unknown {
|
|
53
|
+
if (p === RAW) {
|
|
54
|
+
return target;
|
|
55
|
+
}
|
|
56
|
+
const value = Reflect.get(target, p) as unknown;
|
|
57
|
+
if (!needsProxy(value)) {
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
if (isVmExtern(target)) {
|
|
61
|
+
return createProxy(value, this.scope, this.path);
|
|
62
|
+
}
|
|
63
|
+
return createProxy(value, this.scope, [...this.path, p]);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 函数的执行环境 */
|
|
68
|
+
export class Scope {
|
|
69
|
+
/** 最大求值计数 */
|
|
70
|
+
static readonly MAX_RECURSION = 500;
|
|
71
|
+
constructor(
|
|
72
|
+
/** 生成环境的工厂函数 */
|
|
73
|
+
readonly scope?:
|
|
74
|
+
| ((key: string) => ExpressionOrValue | undefined)
|
|
75
|
+
| Record<string, ExpressionOrValue | undefined>,
|
|
76
|
+
/** 求值失败是否抛异常 */
|
|
77
|
+
readonly throws = true,
|
|
78
|
+
/** 执行环境名称 */
|
|
79
|
+
readonly name = '<anonymous>',
|
|
80
|
+
) {
|
|
81
|
+
if (scope == null) {
|
|
82
|
+
this.getter = (key) => this.context?.imported.get(key);
|
|
83
|
+
} else if (typeof scope == 'function') {
|
|
84
|
+
this.getter = (key) => {
|
|
85
|
+
const v = scope(key);
|
|
86
|
+
if (v === undefined) return this.context?.imported.get(key);
|
|
87
|
+
if (!needsProxy(v)) return v;
|
|
88
|
+
return createProxy(v, this, [key]) as VmValue;
|
|
89
|
+
};
|
|
90
|
+
} else {
|
|
91
|
+
this.getter = (key) => {
|
|
92
|
+
if (!hasOwn(scope, key)) return this.context?.imported.get(key);
|
|
93
|
+
const v = scope[key];
|
|
94
|
+
if (!needsProxy(v)) return v;
|
|
95
|
+
return createProxy(v, this, [key]) as VmValue;
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
protected readonly getter: (key: string) => VmAny;
|
|
101
|
+
/** 上下文 */
|
|
102
|
+
protected context: Context | null = null;
|
|
103
|
+
/** 求值计数 */
|
|
104
|
+
protected evalCounter = 0;
|
|
105
|
+
/** 重置执行环境 */
|
|
106
|
+
reset(context: Context | null): () => void {
|
|
107
|
+
const oldContext = this.context;
|
|
108
|
+
const oldCounter = this.evalCounter;
|
|
109
|
+
this.context = context;
|
|
110
|
+
this.evalCounter = 0;
|
|
111
|
+
return () => {
|
|
112
|
+
this.context = oldContext;
|
|
113
|
+
this.evalCounter = oldCounter;
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/** 函数的执行环境,惰性求值 */
|
|
117
|
+
protected _proxy?: VmContext;
|
|
118
|
+
/** 函数的执行环境,惰性求值 */
|
|
119
|
+
get proxy(): VmContext {
|
|
120
|
+
this._proxy ??= createVmContext(this.getter);
|
|
121
|
+
return this._proxy;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** 检查环境中是否有值 */
|
|
125
|
+
has(key: string): boolean {
|
|
126
|
+
const { scope } = this;
|
|
127
|
+
if (scope == null) return false;
|
|
128
|
+
let v;
|
|
129
|
+
if (typeof scope == 'function') {
|
|
130
|
+
v = scope(key);
|
|
131
|
+
} else if (hasOwn(scope, key)) {
|
|
132
|
+
v = scope[key];
|
|
133
|
+
}
|
|
134
|
+
return v !== undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** 获取环境中的值 */
|
|
138
|
+
get(key: string): VmAny {
|
|
139
|
+
const { scope } = this;
|
|
140
|
+
if (scope == null) return undefined;
|
|
141
|
+
let v;
|
|
142
|
+
if (typeof scope == 'function') {
|
|
143
|
+
v = scope(key);
|
|
144
|
+
} else if (hasOwn(scope, key)) {
|
|
145
|
+
v = scope[key];
|
|
146
|
+
}
|
|
147
|
+
if (!isExpression(v)) return v;
|
|
148
|
+
return this.eval(v, [key]);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 表达式递归求值
|
|
153
|
+
*/
|
|
154
|
+
eval(expression: Expression | CompiledExpression, path: readonly PropertyKey[]): VmValue {
|
|
155
|
+
if (this.evalCounter >= Scope.MAX_RECURSION) {
|
|
156
|
+
throw new Error(`Execution recursion exceeds limit`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
this.evalCounter++;
|
|
160
|
+
try {
|
|
161
|
+
let result;
|
|
162
|
+
if (typeof expression == 'function') {
|
|
163
|
+
result = expression(this) as VmValue;
|
|
164
|
+
} else {
|
|
165
|
+
const { context } = this;
|
|
166
|
+
if (!context) {
|
|
167
|
+
throw new Error(`Undefined context`);
|
|
168
|
+
}
|
|
169
|
+
const exp = parse(context, expression.source, this.throws);
|
|
170
|
+
if (exp.func != null) {
|
|
171
|
+
result = evaluateEval(context, exp, this);
|
|
172
|
+
} else if (exp.error != null) {
|
|
173
|
+
throw exp.error;
|
|
174
|
+
} else {
|
|
175
|
+
result = DEFAULTS;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (result === DEFAULTS) return null;
|
|
179
|
+
const tag = expression[ExpressionTag];
|
|
180
|
+
if (!tag) return result;
|
|
181
|
+
return TypeInfo.to(result, TypeInfo.parse(tag));
|
|
182
|
+
} catch (ex) {
|
|
183
|
+
throw new Error(`${(ex as Error).message || String(ex)}\nIn ${this.name}: @ ${path.join('/')}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** 移除 PROXY */
|
|
189
|
+
function unwrapCore<T>(value: T): T | undefined {
|
|
190
|
+
if (value == null || (typeof value != 'object' && typeof value != 'function')) {
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
const raw = (value as T & { [RAW]?: T })[RAW];
|
|
194
|
+
const obj = raw ?? value;
|
|
195
|
+
for (const key in obj) {
|
|
196
|
+
if (!hasOwn(obj, key)) continue;
|
|
197
|
+
const unwrapped = unwrapCore(obj[key]);
|
|
198
|
+
if (unwrapped !== undefined) {
|
|
199
|
+
obj[key] = unwrapped;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return raw;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** 移除 PROXY */
|
|
206
|
+
export function unwrap<T>(value: T): T {
|
|
207
|
+
return unwrapCore(value) ?? value;
|
|
208
|
+
}
|
package/src/type.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { TypeName, VmValue, VmValueMap } from '@mirascript/mirascript';
|
|
2
|
+
import { lib } from '@mirascript/mirascript/subtle';
|
|
3
|
+
|
|
4
|
+
/** 类型信息 */
|
|
5
|
+
export type TypeInfo = 'string' | 'number' | 'boolean';
|
|
6
|
+
|
|
7
|
+
const x: TypeInfo = 'boolean';
|
|
8
|
+
x satisfies TypeName;
|
|
9
|
+
|
|
10
|
+
/** 类型映射 */
|
|
11
|
+
type TypeMap = VmValueMap & {
|
|
12
|
+
s: string;
|
|
13
|
+
f: number;
|
|
14
|
+
b: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** Ts 类型 */
|
|
18
|
+
export type TsTypeOf<T extends TypeInfo | LegacyType> = TypeMap[T];
|
|
19
|
+
|
|
20
|
+
/** 兼容的旧类型 */
|
|
21
|
+
export type LegacyType = 's' | 'f' | 'b';
|
|
22
|
+
|
|
23
|
+
/** 类型信息 */
|
|
24
|
+
export const TypeInfo = {
|
|
25
|
+
parse: (type: TypeInfo | LegacyType): TypeInfo => {
|
|
26
|
+
if (type === 's' || type === 'string') return 'string';
|
|
27
|
+
if (type === 'f' || type === 'number') return 'number';
|
|
28
|
+
if (type === 'b' || type === 'boolean') return 'boolean';
|
|
29
|
+
throw new TypeError(`Invalid type '${String(type)}'`);
|
|
30
|
+
},
|
|
31
|
+
is: <T extends TypeInfo | LegacyType>(value: VmValue | undefined, type: T): value is TsTypeOf<T> => {
|
|
32
|
+
switch (type) {
|
|
33
|
+
case 'b':
|
|
34
|
+
case 'boolean':
|
|
35
|
+
return typeof value === 'boolean';
|
|
36
|
+
case 'f':
|
|
37
|
+
case 'number':
|
|
38
|
+
return typeof value === 'number';
|
|
39
|
+
case 's':
|
|
40
|
+
case 'string':
|
|
41
|
+
return typeof value === 'string';
|
|
42
|
+
default:
|
|
43
|
+
(type) satisfies never;
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
to: <T extends TypeInfo | LegacyType>(value: VmValue | undefined, type: T): TsTypeOf<T> => {
|
|
48
|
+
/** 返回类型 */
|
|
49
|
+
type R = TsTypeOf<T>;
|
|
50
|
+
if (!type) return value as R;
|
|
51
|
+
switch (type) {
|
|
52
|
+
case 'b':
|
|
53
|
+
case 'boolean':
|
|
54
|
+
return lib.to_boolean(value) as R;
|
|
55
|
+
case 'f':
|
|
56
|
+
case 'number':
|
|
57
|
+
return lib.to_number(value) as R;
|
|
58
|
+
case 's':
|
|
59
|
+
case 'string':
|
|
60
|
+
return lib.to_string(value) as R;
|
|
61
|
+
default:
|
|
62
|
+
(type) satisfies never;
|
|
63
|
+
return value as R;
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
};
|
package/tests/analyze.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Evaluator, Expression, Scope } from '../dist/index.js';
|
|
2
|
+
|
|
3
|
+
const e = new Evaluator();
|
|
4
|
+
const s = new Scope({}, false);
|
|
5
|
+
|
|
6
|
+
describe.skip('Analyze', () => {
|
|
7
|
+
it('simple', () => {
|
|
8
|
+
expect(e.analyze(Expression('a+b'), s)).toEqual([['a'], ['b']]);
|
|
9
|
+
});
|
|
10
|
+
it('access', () => {
|
|
11
|
+
expect(e.analyze(Expression('a.b[1]+b'), s)).toEqual([['a', 'b'], ['b']]);
|
|
12
|
+
expect(e.analyze(Expression('a.b["x"]+b'), s)).toEqual([['a', 'b', 'x'], ['b']]);
|
|
13
|
+
expect(e.analyze(Expression('a.b[x]+b'), s)).toEqual([['a', 'b'], ['x'], ['b']]);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Evaluator, type ExpressionSource, Scope } from '../dist/index.js';
|
|
2
|
+
|
|
3
|
+
const e = new Evaluator();
|
|
4
|
+
const s = new Scope(undefined, false);
|
|
5
|
+
|
|
6
|
+
describe('Evaluate conditions', () => {
|
|
7
|
+
it('can evaluate boolean literals', () => {
|
|
8
|
+
expect(e.evaluateCondition(true, s)).toBe(true);
|
|
9
|
+
expect(e.evaluateCondition(false, s)).toBe(false);
|
|
10
|
+
});
|
|
11
|
+
it('can evaluate empty literals', () => {
|
|
12
|
+
// @ts-expect-error null
|
|
13
|
+
expect(e.evaluateCondition(null, s)).toBe(true);
|
|
14
|
+
expect(e.evaluateCondition(undefined, s)).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
it('can evaluate well known strings', () => {
|
|
17
|
+
expect(e.evaluateCondition('' as ExpressionSource<boolean>, s)).toBe(true);
|
|
18
|
+
expect(e.evaluateCondition('true' as ExpressionSource<boolean>, s)).toBe(true);
|
|
19
|
+
expect(e.evaluateCondition('false' as ExpressionSource<boolean>, s)).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
it('can convert to boolean', () => {
|
|
22
|
+
expect(e.evaluateCondition('""' as ExpressionSource<boolean>, s)).toBe(true);
|
|
23
|
+
expect(e.evaluateCondition('nil' as ExpressionSource<boolean>, s)).toBe(true);
|
|
24
|
+
expect(e.evaluateCondition('undefined' as ExpressionSource<boolean>, s)).toBe(true);
|
|
25
|
+
expect(e.evaluateCondition('0' as ExpressionSource<boolean>, s)).toBe(true);
|
|
26
|
+
expect(e.evaluateCondition('nan' as ExpressionSource<boolean>, s)).toBe(true);
|
|
27
|
+
expect(e.evaluateCondition('(false)' as ExpressionSource<boolean>, s)).toBe(false);
|
|
28
|
+
|
|
29
|
+
expect(e.evaluateCondition('""' as ExpressionSource<boolean>, s, false)).toBe(true);
|
|
30
|
+
expect(e.evaluateCondition('nil' as ExpressionSource<boolean>, s, false)).toBe(false);
|
|
31
|
+
expect(e.evaluateCondition('undefined' as ExpressionSource<boolean>, s, false)).toBe(false);
|
|
32
|
+
expect(e.evaluateCondition('0' as ExpressionSource<boolean>, s, false)).toBe(true);
|
|
33
|
+
expect(e.evaluateCondition('nan' as ExpressionSource<boolean>, s, false)).toBe(true);
|
|
34
|
+
expect(e.evaluateCondition('(false)' as ExpressionSource<boolean>, s, false)).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
it('can evaluate complex', () => {
|
|
37
|
+
expect(e.evaluateCondition('12>3' as ExpressionSource<boolean>, s)).toBe(true);
|
|
38
|
+
expect(e.evaluateCondition('12<3' as ExpressionSource<boolean>, s)).toBe(false);
|
|
39
|
+
expect(e.evaluateCondition('12==3' as ExpressionSource<boolean>, s)).toBe(false);
|
|
40
|
+
expect(e.evaluateCondition('12!=3' as ExpressionSource<boolean>, s)).toBe(true);
|
|
41
|
+
expect(e.evaluateCondition('12>=3' as ExpressionSource<boolean>, s)).toBe(true);
|
|
42
|
+
expect(e.evaluateCondition('12<=3' as ExpressionSource<boolean>, s)).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
it('can return default on error', () => {
|
|
45
|
+
expect(e.evaluateCondition('12>>3' as ExpressionSource<boolean>, undefined, true)).toBe(true);
|
|
46
|
+
// @ts-expect-error null
|
|
47
|
+
expect(e.evaluateCondition('12>>3' as ExpressionSource<boolean>, null, false)).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as def from '../dist/definitions.js';
|
|
2
|
+
|
|
3
|
+
describe('definitions', () => {
|
|
4
|
+
it('should handle parameter', () => {
|
|
5
|
+
expect(def.Parameter()).toBeDefined();
|
|
6
|
+
expect(def.isParameter(def.Parameter())).toBe(true);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('should handle parameter group', () => {
|
|
10
|
+
expect(def.ParameterGroup()).toBeDefined();
|
|
11
|
+
expect(def.isParameterGroup(def.ParameterGroup())).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('should handle parameter decoration', () => {
|
|
15
|
+
expect(def.ParameterDecoration()).toBeDefined();
|
|
16
|
+
expect(def.isParameterDecoration(def.ParameterDecoration())).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should handle parameter decoration group', () => {
|
|
20
|
+
expect(def.ParameterDecorationGroup()).toBeDefined();
|
|
21
|
+
expect(def.isParameterDecorationGroup(def.ParameterDecorationGroup())).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should handle variable', () => {
|
|
25
|
+
expect(def.Variable()).toBeDefined();
|
|
26
|
+
expect(def.isVariable(def.Variable())).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should handle choice', () => {
|
|
30
|
+
expect(def.Choice()).toBeDefined();
|
|
31
|
+
expect(def.isChoice(def.Choice())).toBe(true);
|
|
32
|
+
|
|
33
|
+
expect(def.Choice('x').key).toBe('x');
|
|
34
|
+
expect(def.isChoice(def.Choice('x'))).toBe(true);
|
|
35
|
+
|
|
36
|
+
expect(def.Choice(0).key).toBe(0);
|
|
37
|
+
expect(def.isChoice(def.Choice(0))).toBe(true);
|
|
38
|
+
|
|
39
|
+
expect(def.Choice(false).key).toBe(false);
|
|
40
|
+
expect(def.isChoice(def.Choice(false))).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { VmExtern } from '@mirascript/mirascript';
|
|
2
|
+
import { Evaluator, Expression, Scope } from '../dist/index.js';
|
|
3
|
+
|
|
4
|
+
const e = new Evaluator();
|
|
5
|
+
|
|
6
|
+
describe('Evaluator should work correctly', () => {
|
|
7
|
+
it('should eval deep', () => {
|
|
8
|
+
const s = new Scope(
|
|
9
|
+
(k) =>
|
|
10
|
+
({
|
|
11
|
+
a: Expression('b+d'),
|
|
12
|
+
b: Expression('c+d'),
|
|
13
|
+
c: 12,
|
|
14
|
+
d: Expression('c'),
|
|
15
|
+
})[k],
|
|
16
|
+
false,
|
|
17
|
+
);
|
|
18
|
+
const result = e.evaluate(Expression('a'), s);
|
|
19
|
+
expect(result).toBe(36);
|
|
20
|
+
});
|
|
21
|
+
it('should eval deep with object', () => {
|
|
22
|
+
const s = new Scope(
|
|
23
|
+
{
|
|
24
|
+
a: { a: Expression('b.b + d.d') },
|
|
25
|
+
b: { b: Expression('c.c + d.d') },
|
|
26
|
+
c: { c: 12 },
|
|
27
|
+
d: { d: Expression('c.c') },
|
|
28
|
+
},
|
|
29
|
+
false,
|
|
30
|
+
);
|
|
31
|
+
const result = e.evaluate(Expression('a.a'), s);
|
|
32
|
+
expect(result).toBe(36);
|
|
33
|
+
});
|
|
34
|
+
it('should eval deep with array', () => {
|
|
35
|
+
const s = new Scope(
|
|
36
|
+
{
|
|
37
|
+
a: [Expression('b.0 + d.0')],
|
|
38
|
+
b: [Expression('c.0 + d.0')],
|
|
39
|
+
c: [12],
|
|
40
|
+
d: [Expression('c.0')],
|
|
41
|
+
},
|
|
42
|
+
false,
|
|
43
|
+
);
|
|
44
|
+
const result = e.evaluate(Expression('a.0'), s);
|
|
45
|
+
expect(result).toBe(36);
|
|
46
|
+
});
|
|
47
|
+
it('should eval deep with extern', () => {
|
|
48
|
+
const s = new Scope(
|
|
49
|
+
{
|
|
50
|
+
a: new VmExtern([Expression('b.0 + d.0')]),
|
|
51
|
+
b: new VmExtern([Expression('c.0 + d.0')]),
|
|
52
|
+
c: new VmExtern([12]),
|
|
53
|
+
d: new VmExtern([Expression('c.0')]),
|
|
54
|
+
},
|
|
55
|
+
false,
|
|
56
|
+
);
|
|
57
|
+
const result = e.evaluate(Expression('a.0'), s);
|
|
58
|
+
expect(result).toBe(36);
|
|
59
|
+
});
|
|
60
|
+
it('should eval deep with compiled expression', () => {
|
|
61
|
+
const s = new Scope(
|
|
62
|
+
{
|
|
63
|
+
a: Expression('b+d'),
|
|
64
|
+
b: e.compile(Expression('c+d')),
|
|
65
|
+
c: 12,
|
|
66
|
+
d: Expression('c'),
|
|
67
|
+
},
|
|
68
|
+
false,
|
|
69
|
+
);
|
|
70
|
+
const result = e.evaluate(Expression('a'), s);
|
|
71
|
+
expect(result).toBe(36);
|
|
72
|
+
});
|
|
73
|
+
it('should eval complex', () => {
|
|
74
|
+
const s = new Scope(
|
|
75
|
+
{
|
|
76
|
+
a: Expression('b'),
|
|
77
|
+
b: Expression('c'),
|
|
78
|
+
c: Expression('d'),
|
|
79
|
+
d: Expression('e'),
|
|
80
|
+
e: Expression('f'),
|
|
81
|
+
f: Expression('g'),
|
|
82
|
+
g: Expression('h'),
|
|
83
|
+
h: Expression('i'),
|
|
84
|
+
i: Expression('j'),
|
|
85
|
+
j: Expression('k'),
|
|
86
|
+
k: Expression('l'),
|
|
87
|
+
l: Expression('m'),
|
|
88
|
+
m: Expression('n'),
|
|
89
|
+
n: Expression('o'),
|
|
90
|
+
o: Expression('p'),
|
|
91
|
+
p: Expression('q'),
|
|
92
|
+
q: Expression('r'),
|
|
93
|
+
r: Expression('s'),
|
|
94
|
+
s: Expression('t'),
|
|
95
|
+
t: Expression('u'),
|
|
96
|
+
u: Expression('v'),
|
|
97
|
+
v: Expression('w'),
|
|
98
|
+
w: Expression('x'),
|
|
99
|
+
x: Expression('y'),
|
|
100
|
+
y: Expression('z'),
|
|
101
|
+
z: 1,
|
|
102
|
+
},
|
|
103
|
+
false,
|
|
104
|
+
);
|
|
105
|
+
const result = e.evaluate(Expression('a + a + a + a + a + a + a + a + a + a'), s);
|
|
106
|
+
expect(result).toBe(10);
|
|
107
|
+
});
|
|
108
|
+
it('should report loop', () => {
|
|
109
|
+
const s = new Scope(
|
|
110
|
+
{
|
|
111
|
+
a: Expression('b'),
|
|
112
|
+
b: Expression('c'),
|
|
113
|
+
c: Expression('a'),
|
|
114
|
+
},
|
|
115
|
+
true,
|
|
116
|
+
);
|
|
117
|
+
expect(() => e.evaluate(Expression('a'), s)).toThrow(/Maximum call depth exceeded/);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should not pass some special as proxy', () => {
|
|
121
|
+
expect.assertions(9);
|
|
122
|
+
const o = new VmExtern({
|
|
123
|
+
x: { p: 2 },
|
|
124
|
+
y: 1,
|
|
125
|
+
z: Expression('o.y+o.x.p'),
|
|
126
|
+
d: new Date(),
|
|
127
|
+
r: /r/,
|
|
128
|
+
f() {
|
|
129
|
+
return this.z;
|
|
130
|
+
},
|
|
131
|
+
p: Promise.resolve(1),
|
|
132
|
+
a: new ArrayBuffer(0),
|
|
133
|
+
v: new DataView(new ArrayBuffer(0)),
|
|
134
|
+
test() {
|
|
135
|
+
expect(this).toBeProxy();
|
|
136
|
+
expect(this.d).not.toBeProxy();
|
|
137
|
+
expect(this.r).not.toBeProxy();
|
|
138
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
139
|
+
expect(this.f).not.toBeProxy();
|
|
140
|
+
expect(this.p).not.toBeProxy();
|
|
141
|
+
expect(this.a).not.toBeProxy();
|
|
142
|
+
expect(this.v).not.toBeProxy();
|
|
143
|
+
return this.f();
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
const s = new Scope({ o }, true);
|
|
147
|
+
expect(e.evaluate(Expression('o.test()'), s)).toBe(3);
|
|
148
|
+
expect(e.evaluate(Expression('o.z'), s)).toBe(3);
|
|
149
|
+
});
|
|
150
|
+
});
|
package/tests/eval.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Evaluator, Expression, Scope } from '../dist/index.js';
|
|
2
|
+
|
|
3
|
+
const e = new Evaluator();
|
|
4
|
+
const s = new Scope({}, false);
|
|
5
|
+
|
|
6
|
+
describe('Evaluator should work correctly', () => {
|
|
7
|
+
it('should eval const', () => {
|
|
8
|
+
const result = e.evaluate(Expression('12'), s);
|
|
9
|
+
expect(result).toBe(12);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should eval exp', () => {
|
|
13
|
+
const result = e.evaluate(Expression('exp(2)'), s);
|
|
14
|
+
expect(result).toBe(Math.exp(2));
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should eval error', () => {
|
|
18
|
+
const result = e.evaluate(Expression('exp+++'), s);
|
|
19
|
+
expect(result).toBe(undefined);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('Evaluator should convert result', () => {
|
|
24
|
+
it('should eval const', () => {
|
|
25
|
+
const result = e.evaluate(Expression('12', 'string'), s);
|
|
26
|
+
expect(result).toBe('12');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should eval exp', () => {
|
|
30
|
+
const result = e.evaluate(Expression('exp(2)', 's'), s);
|
|
31
|
+
expect(result).toBe(Math.exp(2).toString());
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should not convert when error', () => {
|
|
35
|
+
// syntax error
|
|
36
|
+
const result = e.evaluate(Expression('exp+++', 's'), s, 1);
|
|
37
|
+
expect(result).toBe(1);
|
|
38
|
+
// runtime error
|
|
39
|
+
const result2 = e.evaluate(Expression('x() + 1', 's'), s, 1);
|
|
40
|
+
expect(result2).toBe(1);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { VmRecord } from '@mirascript/mirascript';
|
|
2
|
+
import { Evaluator, Expression, Scope } from '../dist/index.js';
|
|
3
|
+
|
|
4
|
+
const e = new Evaluator();
|
|
5
|
+
const s = new Scope((k) => (k === 'v' ? ({ a: 2, b: 1, c: undefined } as unknown as VmRecord) : undefined));
|
|
6
|
+
|
|
7
|
+
describe('Evaluator should provided functions', () => {
|
|
8
|
+
it('should eval keys/values/entries', () => {
|
|
9
|
+
expect(e.evaluate(Expression('keys(v)'), s)).toEqual(['a', 'b', 'c']);
|
|
10
|
+
expect(e.evaluate(Expression('values(v)'), s)).toEqual([2, 1, undefined]);
|
|
11
|
+
expect(e.evaluate(Expression('entries(v)'), s)).toEqual([
|
|
12
|
+
{ 0: 'a', 1: 2 },
|
|
13
|
+
{ 0: 'b', 1: 1 },
|
|
14
|
+
{ 0: 'c', 1: null },
|
|
15
|
+
]);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should eval type', () => {
|
|
19
|
+
expect(e.evaluate(Expression('type(exp) == "function"'), s)).toBe(true);
|
|
20
|
+
expect(e.evaluate(Expression('type(exp) == "function"'), s)).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
});
|
package/tests/import.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { VmFunction } from '@mirascript/mirascript';
|
|
2
|
+
import { Evaluator, Expression, Scope } from '../dist/index.js';
|
|
3
|
+
import { lib } from '@mirascript/mirascript/subtle';
|
|
4
|
+
|
|
5
|
+
const e = new Evaluator();
|
|
6
|
+
const e2 = new Evaluator();
|
|
7
|
+
const s = new Scope(undefined, false);
|
|
8
|
+
|
|
9
|
+
describe('Import should work correctly', () => {
|
|
10
|
+
it('can import modules', () => {
|
|
11
|
+
e.import({ add: (x: number, y: number) => x + y });
|
|
12
|
+
expect(e.evaluate(Expression('add(1,2)'), s)).toBe(3);
|
|
13
|
+
expect(e.evaluate(Expression('add::type()'), s)).toBe('extern');
|
|
14
|
+
expect(e2.evaluate(Expression('add(1,2)'), s)).toBe(undefined);
|
|
15
|
+
expect(e2.evaluate(Expression('add::type()'), s)).toBe('nil');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('can remove modules', () => {
|
|
19
|
+
e.import({ add: (x: number, y: number) => x + y });
|
|
20
|
+
expect(e.evaluate(Expression('add(1,2)'), s)).toBe(3);
|
|
21
|
+
e.import({ add: undefined });
|
|
22
|
+
expect(e.evaluate(Expression('add(1,2)'), s)).toBe(undefined);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('can replace modules', () => {
|
|
26
|
+
e.import({ add: (x: number, y: number) => x + y });
|
|
27
|
+
expect(e.evaluate(Expression('add::type()'), s)).toBe('extern');
|
|
28
|
+
e.import({ add: VmFunction((x, y) => lib.to_number(x) + lib.to_number(y)) });
|
|
29
|
+
expect(e.evaluate(Expression('add::type()'), s)).toBe('function');
|
|
30
|
+
});
|
|
31
|
+
});
|
package/tests/scope.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Evaluator, Expression, Scope } from '../dist/index.js';
|
|
2
|
+
|
|
3
|
+
const e = new Evaluator();
|
|
4
|
+
|
|
5
|
+
describe('Scope should work correctly', () => {
|
|
6
|
+
it('can create from undefined', () => {
|
|
7
|
+
const s = new Scope(undefined, false);
|
|
8
|
+
expect(e.evaluate(Expression('12'), s)).toBe(12);
|
|
9
|
+
});
|
|
10
|
+
it('can create from function', () => {
|
|
11
|
+
const s = new Scope((k) => (k === 'a' ? 12 : undefined), false);
|
|
12
|
+
expect(e.evaluate(Expression('a'), s)).toBe(12);
|
|
13
|
+
expect(e.evaluate(Expression('b'), s)).toBe(undefined);
|
|
14
|
+
});
|
|
15
|
+
it('can create from object', () => {
|
|
16
|
+
const s = new Scope({ a: 12, b: Expression('a + 1') }, false);
|
|
17
|
+
expect(e.evaluate(Expression('a'), s)).toBe(12);
|
|
18
|
+
expect(e.evaluate(Expression('b'), s)).toBe(13);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('Scope should not leak dangerous objects', () => {
|
|
23
|
+
it('should not access prototype fields', () => {
|
|
24
|
+
const s = new Scope({}, false);
|
|
25
|
+
expect(e.evaluate(Expression('__proto__'), s)).toBe(undefined);
|
|
26
|
+
expect(e.evaluate(Expression('toString'), s)).toBe(undefined);
|
|
27
|
+
});
|
|
28
|
+
it('should access same name props', () => {
|
|
29
|
+
const s = new Scope({ ['__proto__']: 12, toString: 'x' }, false);
|
|
30
|
+
expect(e.evaluate(Expression('__proto__'), s)).toBe(12);
|
|
31
|
+
expect(e.evaluate(Expression('toString'), s)).toBe('x');
|
|
32
|
+
});
|
|
33
|
+
});
|