@variojs/core 0.1.0
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/__tests__/README.md +101 -0
- package/__tests__/errors.test.ts +139 -0
- package/__tests__/expression/cache.test.ts +118 -0
- package/__tests__/expression/compiler.test.ts +111 -0
- package/__tests__/expression/evaluate.test.ts +95 -0
- package/__tests__/expression/parser.test.ts +57 -0
- package/__tests__/expression/whitelist.test.ts +59 -0
- package/__tests__/performance.test.ts +379 -0
- package/__tests__/runtime/create-context.test.ts +78 -0
- package/__tests__/runtime/loop-context-pool.test.ts +74 -0
- package/__tests__/runtime/path.test.ts +128 -0
- package/__tests__/vm/executor-timeout.test.ts +117 -0
- package/__tests__/vm/executor.test.ts +173 -0
- package/__tests__/vm/handlers/array.test.ts +113 -0
- package/__tests__/vm/handlers/call.test.ts +93 -0
- package/dist/errors.d.ts +100 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +132 -0
- package/dist/errors.js.map +1 -0
- package/dist/expression/cache.d.ts +53 -0
- package/dist/expression/cache.d.ts.map +1 -0
- package/dist/expression/cache.js +158 -0
- package/dist/expression/cache.js.map +1 -0
- package/dist/expression/compiler.d.ts +34 -0
- package/dist/expression/compiler.d.ts.map +1 -0
- package/dist/expression/compiler.js +123 -0
- package/dist/expression/compiler.js.map +1 -0
- package/dist/expression/dependencies.d.ts +17 -0
- package/dist/expression/dependencies.d.ts.map +1 -0
- package/dist/expression/dependencies.js +106 -0
- package/dist/expression/dependencies.js.map +1 -0
- package/dist/expression/evaluate.d.ts +22 -0
- package/dist/expression/evaluate.d.ts.map +1 -0
- package/dist/expression/evaluate.js +75 -0
- package/dist/expression/evaluate.js.map +1 -0
- package/dist/expression/evaluator.d.ts +22 -0
- package/dist/expression/evaluator.d.ts.map +1 -0
- package/dist/expression/evaluator.js +506 -0
- package/dist/expression/evaluator.js.map +1 -0
- package/dist/expression/index.d.ts +12 -0
- package/dist/expression/index.d.ts.map +1 -0
- package/dist/expression/index.js +12 -0
- package/dist/expression/index.js.map +1 -0
- package/dist/expression/parser.d.ts +15 -0
- package/dist/expression/parser.d.ts.map +1 -0
- package/dist/expression/parser.js +42 -0
- package/dist/expression/parser.js.map +1 -0
- package/dist/expression/utils.d.ts +46 -0
- package/dist/expression/utils.d.ts.map +1 -0
- package/dist/expression/utils.js +78 -0
- package/dist/expression/utils.js.map +1 -0
- package/dist/expression/whitelist.d.ts +24 -0
- package/dist/expression/whitelist.d.ts.map +1 -0
- package/dist/expression/whitelist.js +198 -0
- package/dist/expression/whitelist.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/runtime/context.d.ts +8 -0
- package/dist/runtime/context.d.ts.map +1 -0
- package/dist/runtime/context.js +7 -0
- package/dist/runtime/context.js.map +1 -0
- package/dist/runtime/create-context.d.ts +50 -0
- package/dist/runtime/create-context.d.ts.map +1 -0
- package/dist/runtime/create-context.js +73 -0
- package/dist/runtime/create-context.js.map +1 -0
- package/dist/runtime/index.d.ts +10 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +10 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/loop-context-pool.d.ts +58 -0
- package/dist/runtime/loop-context-pool.d.ts.map +1 -0
- package/dist/runtime/loop-context-pool.js +114 -0
- package/dist/runtime/loop-context-pool.js.map +1 -0
- package/dist/runtime/path.d.ts +114 -0
- package/dist/runtime/path.d.ts.map +1 -0
- package/dist/runtime/path.js +302 -0
- package/dist/runtime/path.js.map +1 -0
- package/dist/runtime/proxy.d.ts +18 -0
- package/dist/runtime/proxy.d.ts.map +1 -0
- package/dist/runtime/proxy.js +53 -0
- package/dist/runtime/proxy.js.map +1 -0
- package/dist/runtime/sandbox.d.ts +20 -0
- package/dist/runtime/sandbox.d.ts.map +1 -0
- package/dist/runtime/sandbox.js +32 -0
- package/dist/runtime/sandbox.js.map +1 -0
- package/dist/types.d.ts +175 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +22 -0
- package/dist/types.js.map +1 -0
- package/dist/vm/action.d.ts +11 -0
- package/dist/vm/action.d.ts.map +1 -0
- package/dist/vm/action.js +10 -0
- package/dist/vm/action.js.map +1 -0
- package/dist/vm/errors.d.ts +5 -0
- package/dist/vm/errors.d.ts.map +1 -0
- package/dist/vm/errors.js +5 -0
- package/dist/vm/errors.js.map +1 -0
- package/dist/vm/executor.d.ts +35 -0
- package/dist/vm/executor.d.ts.map +1 -0
- package/dist/vm/executor.js +137 -0
- package/dist/vm/executor.js.map +1 -0
- package/dist/vm/handlers/array/pop.d.ts +12 -0
- package/dist/vm/handlers/array/pop.d.ts.map +1 -0
- package/dist/vm/handlers/array/pop.js +28 -0
- package/dist/vm/handlers/array/pop.js.map +1 -0
- package/dist/vm/handlers/array/push.d.ts +13 -0
- package/dist/vm/handlers/array/push.d.ts.map +1 -0
- package/dist/vm/handlers/array/push.js +42 -0
- package/dist/vm/handlers/array/push.js.map +1 -0
- package/dist/vm/handlers/array/shift.d.ts +12 -0
- package/dist/vm/handlers/array/shift.d.ts.map +1 -0
- package/dist/vm/handlers/array/shift.js +28 -0
- package/dist/vm/handlers/array/shift.js.map +1 -0
- package/dist/vm/handlers/array/splice.d.ts +12 -0
- package/dist/vm/handlers/array/splice.d.ts.map +1 -0
- package/dist/vm/handlers/array/splice.js +59 -0
- package/dist/vm/handlers/array/splice.js.map +1 -0
- package/dist/vm/handlers/array/unshift.d.ts +13 -0
- package/dist/vm/handlers/array/unshift.d.ts.map +1 -0
- package/dist/vm/handlers/array/unshift.js +42 -0
- package/dist/vm/handlers/array/unshift.js.map +1 -0
- package/dist/vm/handlers/array/utils.d.ts +10 -0
- package/dist/vm/handlers/array/utils.d.ts.map +1 -0
- package/dist/vm/handlers/array/utils.js +33 -0
- package/dist/vm/handlers/array/utils.js.map +1 -0
- package/dist/vm/handlers/batch.d.ts +12 -0
- package/dist/vm/handlers/batch.d.ts.map +1 -0
- package/dist/vm/handlers/batch.js +40 -0
- package/dist/vm/handlers/batch.js.map +1 -0
- package/dist/vm/handlers/call.d.ts +14 -0
- package/dist/vm/handlers/call.d.ts.map +1 -0
- package/dist/vm/handlers/call.js +65 -0
- package/dist/vm/handlers/call.js.map +1 -0
- package/dist/vm/handlers/emit.d.ts +12 -0
- package/dist/vm/handlers/emit.d.ts.map +1 -0
- package/dist/vm/handlers/emit.js +26 -0
- package/dist/vm/handlers/emit.js.map +1 -0
- package/dist/vm/handlers/if.d.ts +13 -0
- package/dist/vm/handlers/if.d.ts.map +1 -0
- package/dist/vm/handlers/if.js +35 -0
- package/dist/vm/handlers/if.js.map +1 -0
- package/dist/vm/handlers/index.d.ts +11 -0
- package/dist/vm/handlers/index.d.ts.map +1 -0
- package/dist/vm/handlers/index.js +46 -0
- package/dist/vm/handlers/index.js.map +1 -0
- package/dist/vm/handlers/log.d.ts +12 -0
- package/dist/vm/handlers/log.d.ts.map +1 -0
- package/dist/vm/handlers/log.js +41 -0
- package/dist/vm/handlers/log.js.map +1 -0
- package/dist/vm/handlers/loop.d.ts +12 -0
- package/dist/vm/handlers/loop.d.ts.map +1 -0
- package/dist/vm/handlers/loop.js +71 -0
- package/dist/vm/handlers/loop.js.map +1 -0
- package/dist/vm/handlers/navigate.d.ts +12 -0
- package/dist/vm/handlers/navigate.d.ts.map +1 -0
- package/dist/vm/handlers/navigate.js +43 -0
- package/dist/vm/handlers/navigate.js.map +1 -0
- package/dist/vm/handlers/set.d.ts +15 -0
- package/dist/vm/handlers/set.d.ts.map +1 -0
- package/dist/vm/handlers/set.js +30 -0
- package/dist/vm/handlers/set.js.map +1 -0
- package/dist/vm/index.d.ts +8 -0
- package/dist/vm/index.d.ts.map +1 -0
- package/dist/vm/index.js +7 -0
- package/dist/vm/index.js.map +1 -0
- package/package.json +34 -0
- package/src/errors.ts +194 -0
- package/src/expression/README.md +192 -0
- package/src/expression/cache.ts +199 -0
- package/src/expression/compiler.ts +144 -0
- package/src/expression/dependencies.ts +116 -0
- package/src/expression/evaluate.ts +95 -0
- package/src/expression/evaluator.ts +640 -0
- package/src/expression/index.ts +27 -0
- package/src/expression/parser.ts +54 -0
- package/src/expression/utils.ts +89 -0
- package/src/expression/whitelist.ts +224 -0
- package/src/globals.d.ts +10 -0
- package/src/index.ts +72 -0
- package/src/runtime/context.ts +8 -0
- package/src/runtime/create-context.ts +133 -0
- package/src/runtime/index.ts +28 -0
- package/src/runtime/loop-context-pool.ts +134 -0
- package/src/runtime/path.ts +372 -0
- package/src/runtime/proxy.ts +66 -0
- package/src/runtime/sandbox.ts +43 -0
- package/src/types.ts +177 -0
- package/src/vm/errors.ts +10 -0
- package/src/vm/executor.ts +210 -0
- package/src/vm/handlers/array/pop.ts +47 -0
- package/src/vm/handlers/array/push.ts +68 -0
- package/src/vm/handlers/array/shift.ts +47 -0
- package/src/vm/handlers/array/splice.ts +78 -0
- package/src/vm/handlers/array/unshift.ts +68 -0
- package/src/vm/handlers/array/utils.ts +41 -0
- package/src/vm/handlers/batch.ts +57 -0
- package/src/vm/handlers/call.ts +92 -0
- package/src/vm/handlers/emit.ts +39 -0
- package/src/vm/handlers/if.ts +48 -0
- package/src/vm/handlers/index.ts +52 -0
- package/src/vm/handlers/log.ts +54 -0
- package/src/vm/handlers/loop.ts +102 -0
- package/src/vm/handlers/navigate.ts +64 -0
- package/src/vm/handlers/set.ts +43 -0
- package/src/vm/index.ts +8 -0
- package/tsconfig.json +17 -0
- package/vitest.config.ts +30 -0
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 表达式求值器
|
|
3
|
+
*
|
|
4
|
+
* 功能:
|
|
5
|
+
* - 安全求值 AST
|
|
6
|
+
* - 支持白名单函数调用
|
|
7
|
+
* - 执行步数/时间限制
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type * as ESTree from '@babel/types'
|
|
11
|
+
import type { RuntimeContext, ExpressionOptions } from '../types.js'
|
|
12
|
+
import { ExpressionError, ErrorCodes } from '../errors.js'
|
|
13
|
+
import { isSafePropertyAccess } from '../runtime/sandbox.js'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 白名单全局函数
|
|
17
|
+
*/
|
|
18
|
+
const WHITELISTED_GLOBALS = new Set([
|
|
19
|
+
'String', 'Number', 'Boolean', 'BigInt', 'Symbol',
|
|
20
|
+
'Array', 'Object', 'Math', 'Date',
|
|
21
|
+
])
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 白名单函数(带命名空间)
|
|
25
|
+
*/
|
|
26
|
+
const WHITELISTED_FUNCTIONS = new Set([
|
|
27
|
+
'Array.isArray',
|
|
28
|
+
'Object.is',
|
|
29
|
+
'Number.isFinite',
|
|
30
|
+
'Number.isInteger',
|
|
31
|
+
'Number.isNaN',
|
|
32
|
+
'Number.isSafeInteger',
|
|
33
|
+
'Math.abs',
|
|
34
|
+
'Math.round',
|
|
35
|
+
'Math.floor',
|
|
36
|
+
'Math.ceil',
|
|
37
|
+
'Math.random',
|
|
38
|
+
'Math.max',
|
|
39
|
+
'Math.min',
|
|
40
|
+
'Date.now',
|
|
41
|
+
])
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 安全的数组方法(只读或返回新数组,不修改原状态)
|
|
45
|
+
*/
|
|
46
|
+
const SAFE_ARRAY_METHODS = new Set([
|
|
47
|
+
// 返回新数组
|
|
48
|
+
'slice',
|
|
49
|
+
'concat',
|
|
50
|
+
'filter',
|
|
51
|
+
'map',
|
|
52
|
+
'flat',
|
|
53
|
+
'flatMap',
|
|
54
|
+
'toReversed', // ES2023 不修改原数组的 reverse
|
|
55
|
+
'toSorted', // ES2023 不修改原数组的 sort
|
|
56
|
+
'toSpliced', // ES2023 不修改原数组的 splice
|
|
57
|
+
'with', // ES2023 不修改原数组的索引赋值
|
|
58
|
+
// 只读方法
|
|
59
|
+
'indexOf',
|
|
60
|
+
'lastIndexOf',
|
|
61
|
+
'includes',
|
|
62
|
+
'find',
|
|
63
|
+
'findIndex',
|
|
64
|
+
'findLast',
|
|
65
|
+
'findLastIndex',
|
|
66
|
+
'every',
|
|
67
|
+
'some',
|
|
68
|
+
'at',
|
|
69
|
+
// 返回字符串
|
|
70
|
+
'join',
|
|
71
|
+
'toString',
|
|
72
|
+
'toLocaleString',
|
|
73
|
+
// 在链式调用中安全使用(如 slice().reverse())
|
|
74
|
+
'reverse',
|
|
75
|
+
'sort',
|
|
76
|
+
])
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 运行时辅助函数
|
|
80
|
+
*/
|
|
81
|
+
const RUNTIME_HELPERS: Record<string, (...args: unknown[]) => unknown> = {
|
|
82
|
+
'$truncate': (str: unknown, length: unknown): unknown => {
|
|
83
|
+
if (typeof str !== 'string') return str
|
|
84
|
+
const len = typeof length === 'number' ? length : 0
|
|
85
|
+
return str.length > len ? str.slice(0, len) + '...' : str
|
|
86
|
+
},
|
|
87
|
+
'$format': (date: unknown, _format?: unknown): unknown => {
|
|
88
|
+
const d = typeof date === 'number' ? new Date(date) : (date as Date)
|
|
89
|
+
// 简单格式化,可根据需要扩展
|
|
90
|
+
// format 参数暂未实现,保留接口以便未来扩展
|
|
91
|
+
return d.toISOString()
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 安全求值 AST
|
|
97
|
+
*
|
|
98
|
+
* @param ast AST 节点
|
|
99
|
+
* @param ctx 运行时上下文
|
|
100
|
+
* @param options 求值选项
|
|
101
|
+
* @returns 求值结果(类型无法静态推导)
|
|
102
|
+
*
|
|
103
|
+
* 注意:表达式求值结果类型无法在编译时确定,返回 unknown
|
|
104
|
+
*/
|
|
105
|
+
export function evaluateExpression(
|
|
106
|
+
ast: ESTree.Node,
|
|
107
|
+
ctx: RuntimeContext,
|
|
108
|
+
options: ExpressionOptions = {}
|
|
109
|
+
): unknown {
|
|
110
|
+
const mergedOptions = {
|
|
111
|
+
...ctx.$exprOptions,
|
|
112
|
+
...options
|
|
113
|
+
}
|
|
114
|
+
const allowGlobals = mergedOptions.allowGlobals === true
|
|
115
|
+
const maxSteps = mergedOptions.maxSteps ?? 1000
|
|
116
|
+
const timeout = mergedOptions.timeout ?? 100
|
|
117
|
+
let stepCount = 0
|
|
118
|
+
const startTime = Date.now()
|
|
119
|
+
|
|
120
|
+
function getGlobalValueByName(name: string): unknown {
|
|
121
|
+
if (!allowGlobals) return undefined
|
|
122
|
+
// 只有在 allowGlobals 为 true 时才返回全局对象
|
|
123
|
+
if (name === 'globalThis') return globalThis
|
|
124
|
+
if (name === 'window' && typeof window !== 'undefined') return window
|
|
125
|
+
if (name === 'document' && typeof document !== 'undefined') return document
|
|
126
|
+
if (name === 'global' && typeof global !== 'undefined') return global
|
|
127
|
+
if (name === 'self' && typeof self !== 'undefined') return self
|
|
128
|
+
// 只有在 allowGlobals 为 true 时才访问 globalThis 的属性
|
|
129
|
+
return (globalThis as Record<string, unknown>)[name]
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function isGlobalObjectName(name: string): boolean {
|
|
133
|
+
return ['window', 'document', 'global', 'globalThis', 'self'].includes(name)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function checkLimits(): void {
|
|
137
|
+
stepCount++
|
|
138
|
+
if (stepCount > maxSteps) {
|
|
139
|
+
throw new ExpressionError(
|
|
140
|
+
JSON.stringify(ast),
|
|
141
|
+
`Expression evaluation exceeded max steps (${maxSteps})`,
|
|
142
|
+
ErrorCodes.EXPRESSION_MAX_STEPS_EXCEEDED,
|
|
143
|
+
{
|
|
144
|
+
metadata: {
|
|
145
|
+
maxSteps,
|
|
146
|
+
currentSteps: stepCount
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (Date.now() - startTime > timeout) {
|
|
153
|
+
throw new ExpressionError(
|
|
154
|
+
JSON.stringify(ast),
|
|
155
|
+
`Expression evaluation exceeded timeout (${timeout}ms)`,
|
|
156
|
+
ErrorCodes.EXPRESSION_TIMEOUT,
|
|
157
|
+
{
|
|
158
|
+
metadata: {
|
|
159
|
+
timeout,
|
|
160
|
+
elapsedTime: Date.now() - startTime
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function evaluate(node: ESTree.Node): unknown {
|
|
168
|
+
checkLimits()
|
|
169
|
+
|
|
170
|
+
// 使用类型断言处理 node.type,因为 @babel/types 的类型定义可能不完整
|
|
171
|
+
// TypeScript 的严格类型检查可能不识别某些节点类型,使用类型断言绕过
|
|
172
|
+
const nodeType = (node as { type: string }).type as string
|
|
173
|
+
switch (nodeType) {
|
|
174
|
+
case 'NumericLiteral':
|
|
175
|
+
case 'StringLiteral':
|
|
176
|
+
case 'BooleanLiteral':
|
|
177
|
+
case 'BigIntLiteral':
|
|
178
|
+
case 'DecimalLiteral':
|
|
179
|
+
case 'RegExpLiteral': {
|
|
180
|
+
// 这些字面量类型都有 value 属性
|
|
181
|
+
return (node as any).value;
|
|
182
|
+
}
|
|
183
|
+
case 'NullLiteral': {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
case 'Literal': {
|
|
188
|
+
const literal = node as ESTree.Literal
|
|
189
|
+
// 处理不同类型的字面量
|
|
190
|
+
// @babel/types 的 Literal 类型包含多种字面量类型
|
|
191
|
+
// 使用类型守卫安全访问 value 属性
|
|
192
|
+
if ('value' in literal && literal.value !== undefined) {
|
|
193
|
+
return literal.value
|
|
194
|
+
}
|
|
195
|
+
// NullLiteral 或 BigIntLiteral 可能没有 value 属性,或 value 为 null
|
|
196
|
+
// 检查类型名称
|
|
197
|
+
const literalType = (literal as { type: string }).type
|
|
198
|
+
if (literalType === 'NullLiteral') {
|
|
199
|
+
return null
|
|
200
|
+
}
|
|
201
|
+
// 其他情况:尝试访问 value,如果不存在则返回 undefined
|
|
202
|
+
return (literal as { value?: unknown }).value ?? undefined
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
case 'Identifier': {
|
|
206
|
+
const name = (node as ESTree.Identifier).name
|
|
207
|
+
|
|
208
|
+
// 检查是否为全局对象名称(无论是否存在,都应该被禁止,除非 allowGlobals)
|
|
209
|
+
// 必须在检查 ctx 之前,防止 ctx 中有同名属性绕过检查
|
|
210
|
+
if (isGlobalObjectName(name) && !allowGlobals) {
|
|
211
|
+
throw new ExpressionError(
|
|
212
|
+
name,
|
|
213
|
+
`Access to global "${name}" is not allowed in expressions`,
|
|
214
|
+
ErrorCodes.EXPRESSION_UNSAFE_ACCESS
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// 从上下文获取值(优先从 ctx,但全局对象名称已经被上面的检查拦截)
|
|
219
|
+
// 注意:即使 ctx 中有 window/document 等属性,也不应该访问(安全考虑)
|
|
220
|
+
if (name in ctx && !isGlobalObjectName(name)) {
|
|
221
|
+
return ctx[name]
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// 允许直接访问全局对象(由 allowGlobals 控制)
|
|
225
|
+
if (allowGlobals) {
|
|
226
|
+
const globalValue = getGlobalValueByName(name)
|
|
227
|
+
if (globalValue !== undefined) {
|
|
228
|
+
return globalValue
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// 如果是白名单全局函数,允许访问(但仅用于函数调用,这里返回 undefined)
|
|
233
|
+
if (WHITELISTED_GLOBALS.has(name)) {
|
|
234
|
+
return (globalThis as Record<string, unknown>)[name]
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// 未定义的标识符返回 undefined(而不是抛出错误,允许可选链等场景)
|
|
238
|
+
return undefined
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
case 'MemberExpression': {
|
|
242
|
+
const member = node as ESTree.MemberExpression
|
|
243
|
+
const object = evaluate(member.object)
|
|
244
|
+
|
|
245
|
+
// null/undefined 检查
|
|
246
|
+
if (object == null) {
|
|
247
|
+
return undefined
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// 禁止访问全局对象(检查对象是否为全局对象)
|
|
251
|
+
if (!allowGlobals) {
|
|
252
|
+
// 检查对象是否为全局对象
|
|
253
|
+
const isGlobal = object === globalThis ||
|
|
254
|
+
(typeof window !== 'undefined' && object === window) ||
|
|
255
|
+
(typeof global !== 'undefined' && object === global) ||
|
|
256
|
+
(typeof self !== 'undefined' && object === self)
|
|
257
|
+
if (isGlobal) {
|
|
258
|
+
throw new ExpressionError(
|
|
259
|
+
JSON.stringify(node),
|
|
260
|
+
'Access to global object is not allowed in expressions',
|
|
261
|
+
ErrorCodes.EXPRESSION_UNSAFE_ACCESS
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (member.computed) {
|
|
267
|
+
// 计算属性:obj[key]
|
|
268
|
+
const key = evaluate(member.property)
|
|
269
|
+
return (object as Record<string | number, unknown>)[key as string | number]
|
|
270
|
+
} else {
|
|
271
|
+
// 静态属性:obj.prop
|
|
272
|
+
const prop = (member.property as ESTree.Identifier).name
|
|
273
|
+
return (object as Record<string, unknown>)[prop]
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
case 'OptionalMemberExpression': {
|
|
278
|
+
const member = node as ESTree.OptionalMemberExpression
|
|
279
|
+
const object = evaluate(member.object)
|
|
280
|
+
|
|
281
|
+
// null/undefined 检查(可选链特性)
|
|
282
|
+
if (object == null) {
|
|
283
|
+
return undefined
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// 禁止访问全局对象(检查对象是否为全局对象)
|
|
287
|
+
if (!allowGlobals) {
|
|
288
|
+
// 检查对象是否为全局对象
|
|
289
|
+
const isGlobal = object === globalThis ||
|
|
290
|
+
(typeof window !== 'undefined' && object === window) ||
|
|
291
|
+
(typeof global !== 'undefined' && object === global) ||
|
|
292
|
+
(typeof self !== 'undefined' && object === self)
|
|
293
|
+
if (isGlobal) {
|
|
294
|
+
throw new ExpressionError(
|
|
295
|
+
JSON.stringify(node),
|
|
296
|
+
'Access to global object is not allowed in expressions',
|
|
297
|
+
ErrorCodes.EXPRESSION_UNSAFE_ACCESS
|
|
298
|
+
)
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (member.computed) {
|
|
303
|
+
// 计算属性:obj?.[key]
|
|
304
|
+
const key = evaluate(member.property)
|
|
305
|
+
return (object as Record<string | number, unknown>)?.[key as string | number]
|
|
306
|
+
} else {
|
|
307
|
+
// 静态属性:obj?.prop
|
|
308
|
+
const prop = (member.property as ESTree.Identifier).name
|
|
309
|
+
return (object as Record<string, unknown>)?.[prop]
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
case 'BinaryExpression': {
|
|
314
|
+
const binary = node as ESTree.BinaryExpression
|
|
315
|
+
const left = evaluate(binary.left)
|
|
316
|
+
const right = evaluate(binary.right)
|
|
317
|
+
const operator = binary.operator
|
|
318
|
+
|
|
319
|
+
// 类型安全的二元运算
|
|
320
|
+
switch (operator) {
|
|
321
|
+
case '+': return (left as number) + (right as number)
|
|
322
|
+
case '-': return (left as number) - (right as number)
|
|
323
|
+
case '*': return (left as number) * (right as number)
|
|
324
|
+
case '/': return (left as number) / (right as number)
|
|
325
|
+
case '%': return (left as number) % (right as number)
|
|
326
|
+
case '**': return (left as number) ** (right as number)
|
|
327
|
+
case '==': return left == right
|
|
328
|
+
case '!=': return left != right
|
|
329
|
+
case '===': return left === right
|
|
330
|
+
case '!==': return left !== right
|
|
331
|
+
case '<': return (left as number) < (right as number)
|
|
332
|
+
case '<=': return (left as number) <= (right as number)
|
|
333
|
+
case '>': return (left as number) > (right as number)
|
|
334
|
+
case '>=': return (left as number) >= (right as number)
|
|
335
|
+
case '<<': return (left as number) << (right as number)
|
|
336
|
+
case '>>': return (left as number) >> (right as number)
|
|
337
|
+
case '>>>': return (left as number) >>> (right as number)
|
|
338
|
+
case '&': return (left as number) & (right as number)
|
|
339
|
+
case '|': return (left as number) | (right as number)
|
|
340
|
+
case '^': return (left as number) ^ (right as number)
|
|
341
|
+
case 'in': return (left as string | number) in (right as object)
|
|
342
|
+
case 'instanceof': {
|
|
343
|
+
// instanceof 需要检查 right 是否为构造函数
|
|
344
|
+
if (typeof right !== 'function' && typeof right !== 'object') {
|
|
345
|
+
throw new ExpressionError(
|
|
346
|
+
JSON.stringify(node),
|
|
347
|
+
'Right-hand side of instanceof must be a constructor',
|
|
348
|
+
ErrorCodes.EXPRESSION_EVALUATION_ERROR
|
|
349
|
+
)
|
|
350
|
+
}
|
|
351
|
+
return left instanceof (right as new (...args: unknown[]) => unknown)
|
|
352
|
+
}
|
|
353
|
+
default:
|
|
354
|
+
throw new ExpressionError(
|
|
355
|
+
JSON.stringify(node),
|
|
356
|
+
`Unsupported binary operator: ${operator}`,
|
|
357
|
+
ErrorCodes.EXPRESSION_EVALUATION_ERROR
|
|
358
|
+
)
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
case 'LogicalExpression': {
|
|
363
|
+
const logical = node as ESTree.LogicalExpression
|
|
364
|
+
const left = evaluate(logical.left)
|
|
365
|
+
const operator = logical.operator
|
|
366
|
+
|
|
367
|
+
if (operator === '&&') {
|
|
368
|
+
return left && evaluate(logical.right)
|
|
369
|
+
} else if (operator === '||') {
|
|
370
|
+
return left || evaluate(logical.right)
|
|
371
|
+
} else if (operator === '??') {
|
|
372
|
+
return left ?? evaluate(logical.right)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
throw new ExpressionError(
|
|
376
|
+
JSON.stringify(node),
|
|
377
|
+
`Unsupported logical operator: ${operator}`,
|
|
378
|
+
ErrorCodes.EXPRESSION_EVALUATION_ERROR
|
|
379
|
+
)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
case 'UnaryExpression': {
|
|
383
|
+
const unary = node as ESTree.UnaryExpression
|
|
384
|
+
const argument = evaluate(unary.argument)
|
|
385
|
+
const operator = unary.operator
|
|
386
|
+
|
|
387
|
+
switch (operator) {
|
|
388
|
+
case '+': return +(argument as number)
|
|
389
|
+
case '-': return -(argument as number)
|
|
390
|
+
case '!': return !argument
|
|
391
|
+
case '~': return ~(argument as number)
|
|
392
|
+
case 'typeof': return typeof argument
|
|
393
|
+
case 'void': return void argument
|
|
394
|
+
case 'delete': throw new ExpressionError(
|
|
395
|
+
JSON.stringify(node),
|
|
396
|
+
'delete operator is not allowed',
|
|
397
|
+
ErrorCodes.EXPRESSION_EVALUATION_ERROR
|
|
398
|
+
)
|
|
399
|
+
default:
|
|
400
|
+
throw new ExpressionError(
|
|
401
|
+
JSON.stringify(node),
|
|
402
|
+
`Unsupported unary operator: ${operator}`,
|
|
403
|
+
ErrorCodes.EXPRESSION_EVALUATION_ERROR
|
|
404
|
+
)
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
case 'ConditionalExpression': {
|
|
409
|
+
const conditional = node as ESTree.ConditionalExpression
|
|
410
|
+
const test = evaluate(conditional.test)
|
|
411
|
+
return test ? evaluate(conditional.consequent) : evaluate(conditional.alternate)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// NullishCoalescingExpression 在 @babel/types 中可能不存在
|
|
415
|
+
// 使用 LogicalExpression 处理 ?? 运算符(operator === '??')
|
|
416
|
+
// 如果类型系统支持,可以添加此 case
|
|
417
|
+
|
|
418
|
+
case 'CallExpression': {
|
|
419
|
+
const call = node as ESTree.CallExpression
|
|
420
|
+
const callee = call.callee
|
|
421
|
+
|
|
422
|
+
// 获取函数名和对象
|
|
423
|
+
let funcName: string
|
|
424
|
+
let funcObj: unknown
|
|
425
|
+
let callContext: unknown = null // 函数调用的上下文(this)
|
|
426
|
+
|
|
427
|
+
if (callee.type === 'Identifier') {
|
|
428
|
+
// 直接函数调用:String(), Math.max()
|
|
429
|
+
funcName = callee.name
|
|
430
|
+
|
|
431
|
+
// 检查是否为全局对象(禁止访问)
|
|
432
|
+
if (!isSafePropertyAccess(funcName, ctx, { allowGlobals })) {
|
|
433
|
+
throw new ExpressionError(
|
|
434
|
+
funcName,
|
|
435
|
+
`Access to global "${funcName}" is not allowed in expressions`,
|
|
436
|
+
'UNSAFE_ACCESS'
|
|
437
|
+
)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// 从上下文或全局获取函数
|
|
441
|
+
funcObj = ctx[funcName] ?? (globalThis as Record<string, unknown>)[funcName]
|
|
442
|
+
callContext = null // 全局函数调用,this 为 null
|
|
443
|
+
} else if (callee.type === 'MemberExpression') {
|
|
444
|
+
// 成员函数调用:Math.max(), Array.isArray(), array.slice()
|
|
445
|
+
const member = callee as ESTree.MemberExpression
|
|
446
|
+
const obj = evaluate(member.object)
|
|
447
|
+
|
|
448
|
+
// null/undefined 检查
|
|
449
|
+
if (obj == null) {
|
|
450
|
+
throw new ExpressionError(
|
|
451
|
+
JSON.stringify(node),
|
|
452
|
+
`Cannot call method on ${obj === null ? 'null' : 'undefined'}`,
|
|
453
|
+
ErrorCodes.EXPRESSION_EVALUATION_ERROR
|
|
454
|
+
)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// 检查对象是否为全局对象(禁止访问)
|
|
458
|
+
if (!allowGlobals) {
|
|
459
|
+
// 检查对象是否为全局对象
|
|
460
|
+
const isGlobal = obj === globalThis ||
|
|
461
|
+
(typeof window !== 'undefined' && obj === window) ||
|
|
462
|
+
(typeof global !== 'undefined' && obj === global) ||
|
|
463
|
+
(typeof self !== 'undefined' && obj === self)
|
|
464
|
+
if (isGlobal) {
|
|
465
|
+
throw new ExpressionError(
|
|
466
|
+
JSON.stringify(node),
|
|
467
|
+
'Access to global object is not allowed in expressions',
|
|
468
|
+
ErrorCodes.EXPRESSION_UNSAFE_ACCESS
|
|
469
|
+
)
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// 检查是否是数组实例方法调用(安全的只读方法)
|
|
474
|
+
if (Array.isArray(obj) && !member.computed && member.property.type === 'Identifier') {
|
|
475
|
+
const methodName = (member.property as ESTree.Identifier).name
|
|
476
|
+
if (SAFE_ARRAY_METHODS.has(methodName)) {
|
|
477
|
+
// 这是安全的数组方法,直接使用
|
|
478
|
+
funcObj = (obj as any)[methodName]
|
|
479
|
+
funcName = `Array.${methodName}` // 用于标识和错误信息
|
|
480
|
+
callContext = obj // 数组方法调用,this 为数组本身
|
|
481
|
+
} else {
|
|
482
|
+
// 不是安全的数组方法,需要检查白名单
|
|
483
|
+
const objName = member.object.type === 'Identifier'
|
|
484
|
+
? (member.object as ESTree.Identifier).name
|
|
485
|
+
: String(obj)
|
|
486
|
+
funcName = `${objName}.${methodName}`
|
|
487
|
+
funcObj = (obj as unknown as Record<string, unknown>)?.[methodName]
|
|
488
|
+
callContext = obj
|
|
489
|
+
}
|
|
490
|
+
} else {
|
|
491
|
+
// 非数组对象,正常处理
|
|
492
|
+
// 获取对象标识符名称(如果是 Identifier)
|
|
493
|
+
let objName = '';
|
|
494
|
+
if (member.object.type === 'Identifier') {
|
|
495
|
+
objName = (member.object as ESTree.Identifier).name;
|
|
496
|
+
} else {
|
|
497
|
+
// 对于其他情况,使用字符串表示,但可能不在白名单内
|
|
498
|
+
objName = String(obj);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (member.computed) {
|
|
502
|
+
const prop = evaluate(member.property)
|
|
503
|
+
funcName = `${objName}[${String(prop)}]`
|
|
504
|
+
funcObj = (obj as Record<string, unknown>)?.[prop as string]
|
|
505
|
+
callContext = obj
|
|
506
|
+
} else {
|
|
507
|
+
const prop = (member.property as ESTree.Identifier).name
|
|
508
|
+
funcName = `${objName}.${prop}`
|
|
509
|
+
funcObj = (obj as Record<string, unknown>)?.[prop]
|
|
510
|
+
callContext = obj
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
} else {
|
|
514
|
+
throw new ExpressionError(
|
|
515
|
+
JSON.stringify(node),
|
|
516
|
+
'Invalid function call: only Identifier and MemberExpression are allowed',
|
|
517
|
+
ErrorCodes.EXPRESSION_EVALUATION_ERROR
|
|
518
|
+
)
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// 检查白名单
|
|
522
|
+
// 如果是数组方法(funcName 以 Array. 开头且方法在安全列表中),则允许
|
|
523
|
+
const isArrayMethod = funcName.startsWith('Array.') && SAFE_ARRAY_METHODS.has(funcName.split('.')[1])
|
|
524
|
+
const isWhitelisted = isArrayMethod ||
|
|
525
|
+
WHITELISTED_FUNCTIONS.has(funcName) ||
|
|
526
|
+
WHITELISTED_GLOBALS.has(funcName.split('.')[0]) ||
|
|
527
|
+
RUNTIME_HELPERS[funcName] !== undefined
|
|
528
|
+
|
|
529
|
+
if (!allowGlobals && !isWhitelisted) {
|
|
530
|
+
throw new ExpressionError(
|
|
531
|
+
funcName,
|
|
532
|
+
`Function "${funcName}" is not in whitelist`,
|
|
533
|
+
ErrorCodes.EXPRESSION_FUNCTION_NOT_WHITELISTED
|
|
534
|
+
)
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// 执行函数调用
|
|
538
|
+
const args = call.arguments.map(arg => {
|
|
539
|
+
if (arg.type === 'SpreadElement') {
|
|
540
|
+
throw new ExpressionError(
|
|
541
|
+
JSON.stringify(node),
|
|
542
|
+
'Spread operator is not allowed',
|
|
543
|
+
ErrorCodes.EXPRESSION_EVALUATION_ERROR
|
|
544
|
+
)
|
|
545
|
+
}
|
|
546
|
+
return evaluate(arg)
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
// 调用运行时辅助函数
|
|
550
|
+
if (RUNTIME_HELPERS[funcName]) {
|
|
551
|
+
return RUNTIME_HELPERS[funcName](...args)
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// 调用白名单全局函数或数组方法
|
|
555
|
+
if (typeof funcObj === 'function') {
|
|
556
|
+
try {
|
|
557
|
+
// 如果有上下文(如数组方法),使用 apply;否则直接调用
|
|
558
|
+
return callContext !== null
|
|
559
|
+
? funcObj.apply(callContext, args)
|
|
560
|
+
: funcObj(...args)
|
|
561
|
+
} catch (error: unknown) {
|
|
562
|
+
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
563
|
+
throw new ExpressionError(
|
|
564
|
+
funcName,
|
|
565
|
+
`Error calling "${funcName}": ${errorMessage}`,
|
|
566
|
+
ErrorCodes.EXPRESSION_EVALUATION_ERROR
|
|
567
|
+
)
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
throw new ExpressionError(
|
|
572
|
+
funcName,
|
|
573
|
+
`"${funcName}" is not a function${funcObj === undefined ? ' (undefined)' : funcObj === null ? ' (null)' : ''}`,
|
|
574
|
+
ErrorCodes.EXPRESSION_EVALUATION_ERROR
|
|
575
|
+
)
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
case 'ArrayExpression': {
|
|
579
|
+
const array = node as ESTree.ArrayExpression
|
|
580
|
+
return array.elements.map(el => {
|
|
581
|
+
if (el == null) return null
|
|
582
|
+
if (el.type === 'SpreadElement') {
|
|
583
|
+
throw new ExpressionError(
|
|
584
|
+
JSON.stringify(node),
|
|
585
|
+
'Spread operator is not allowed',
|
|
586
|
+
ErrorCodes.EXPRESSION_EVALUATION_ERROR
|
|
587
|
+
)
|
|
588
|
+
}
|
|
589
|
+
return evaluate(el)
|
|
590
|
+
})
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
case 'ObjectExpression': {
|
|
594
|
+
const object = node as ESTree.ObjectExpression
|
|
595
|
+
const result: Record<string, any> = {}
|
|
596
|
+
|
|
597
|
+
for (const prop of object.properties) {
|
|
598
|
+
if (prop.type === 'ObjectMethod' || prop.type === 'SpreadElement') {
|
|
599
|
+
throw new ExpressionError(
|
|
600
|
+
JSON.stringify(node),
|
|
601
|
+
'Object methods and spread are not allowed',
|
|
602
|
+
ErrorCodes.EXPRESSION_EVALUATION_ERROR
|
|
603
|
+
)
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const key = prop.key.type === 'Identifier'
|
|
607
|
+
? prop.key.name
|
|
608
|
+
: String(evaluate(prop.key))
|
|
609
|
+
|
|
610
|
+
result[key] = evaluate(prop.value)
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
return result
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
case 'TemplateLiteral': {
|
|
617
|
+
const template = node as ESTree.TemplateLiteral
|
|
618
|
+
let result = ''
|
|
619
|
+
|
|
620
|
+
for (let i = 0; i < template.quasis.length; i++) {
|
|
621
|
+
result += template.quasis[i].value.cooked
|
|
622
|
+
if (i < template.expressions.length) {
|
|
623
|
+
result += String(evaluate(template.expressions[i]))
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
return result
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
default:
|
|
631
|
+
throw new ExpressionError(
|
|
632
|
+
JSON.stringify(node),
|
|
633
|
+
`Unsupported node type: ${node.type}`,
|
|
634
|
+
ErrorCodes.EXPRESSION_EVALUATION_ERROR
|
|
635
|
+
)
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
return evaluate(ast)
|
|
640
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expression 模块导出
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { parseExpression } from './parser.js'
|
|
6
|
+
export { validateAST } from './whitelist.js'
|
|
7
|
+
export { evaluateExpression } from './evaluator.js'
|
|
8
|
+
export { evaluate } from './evaluate.js'
|
|
9
|
+
export { extractDependencies } from './dependencies.js'
|
|
10
|
+
export {
|
|
11
|
+
getCachedExpression,
|
|
12
|
+
setCachedExpression,
|
|
13
|
+
invalidateCache,
|
|
14
|
+
clearCache
|
|
15
|
+
} from './cache.js'
|
|
16
|
+
export {
|
|
17
|
+
extractExpression,
|
|
18
|
+
normalizeExpression,
|
|
19
|
+
isExpressionFormat,
|
|
20
|
+
extractExpressionsRecursively
|
|
21
|
+
} from './utils.js'
|
|
22
|
+
export {
|
|
23
|
+
compileSimpleExpression,
|
|
24
|
+
getCompiledExpression,
|
|
25
|
+
clearCompiledCache,
|
|
26
|
+
type CompiledExpression
|
|
27
|
+
} from './compiler.js'
|