@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.
Files changed (209) hide show
  1. package/__tests__/README.md +101 -0
  2. package/__tests__/errors.test.ts +139 -0
  3. package/__tests__/expression/cache.test.ts +118 -0
  4. package/__tests__/expression/compiler.test.ts +111 -0
  5. package/__tests__/expression/evaluate.test.ts +95 -0
  6. package/__tests__/expression/parser.test.ts +57 -0
  7. package/__tests__/expression/whitelist.test.ts +59 -0
  8. package/__tests__/performance.test.ts +379 -0
  9. package/__tests__/runtime/create-context.test.ts +78 -0
  10. package/__tests__/runtime/loop-context-pool.test.ts +74 -0
  11. package/__tests__/runtime/path.test.ts +128 -0
  12. package/__tests__/vm/executor-timeout.test.ts +117 -0
  13. package/__tests__/vm/executor.test.ts +173 -0
  14. package/__tests__/vm/handlers/array.test.ts +113 -0
  15. package/__tests__/vm/handlers/call.test.ts +93 -0
  16. package/dist/errors.d.ts +100 -0
  17. package/dist/errors.d.ts.map +1 -0
  18. package/dist/errors.js +132 -0
  19. package/dist/errors.js.map +1 -0
  20. package/dist/expression/cache.d.ts +53 -0
  21. package/dist/expression/cache.d.ts.map +1 -0
  22. package/dist/expression/cache.js +158 -0
  23. package/dist/expression/cache.js.map +1 -0
  24. package/dist/expression/compiler.d.ts +34 -0
  25. package/dist/expression/compiler.d.ts.map +1 -0
  26. package/dist/expression/compiler.js +123 -0
  27. package/dist/expression/compiler.js.map +1 -0
  28. package/dist/expression/dependencies.d.ts +17 -0
  29. package/dist/expression/dependencies.d.ts.map +1 -0
  30. package/dist/expression/dependencies.js +106 -0
  31. package/dist/expression/dependencies.js.map +1 -0
  32. package/dist/expression/evaluate.d.ts +22 -0
  33. package/dist/expression/evaluate.d.ts.map +1 -0
  34. package/dist/expression/evaluate.js +75 -0
  35. package/dist/expression/evaluate.js.map +1 -0
  36. package/dist/expression/evaluator.d.ts +22 -0
  37. package/dist/expression/evaluator.d.ts.map +1 -0
  38. package/dist/expression/evaluator.js +506 -0
  39. package/dist/expression/evaluator.js.map +1 -0
  40. package/dist/expression/index.d.ts +12 -0
  41. package/dist/expression/index.d.ts.map +1 -0
  42. package/dist/expression/index.js +12 -0
  43. package/dist/expression/index.js.map +1 -0
  44. package/dist/expression/parser.d.ts +15 -0
  45. package/dist/expression/parser.d.ts.map +1 -0
  46. package/dist/expression/parser.js +42 -0
  47. package/dist/expression/parser.js.map +1 -0
  48. package/dist/expression/utils.d.ts +46 -0
  49. package/dist/expression/utils.d.ts.map +1 -0
  50. package/dist/expression/utils.js +78 -0
  51. package/dist/expression/utils.js.map +1 -0
  52. package/dist/expression/whitelist.d.ts +24 -0
  53. package/dist/expression/whitelist.d.ts.map +1 -0
  54. package/dist/expression/whitelist.js +198 -0
  55. package/dist/expression/whitelist.js.map +1 -0
  56. package/dist/index.d.ts +19 -0
  57. package/dist/index.d.ts.map +1 -0
  58. package/dist/index.js +21 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/runtime/context.d.ts +8 -0
  61. package/dist/runtime/context.d.ts.map +1 -0
  62. package/dist/runtime/context.js +7 -0
  63. package/dist/runtime/context.js.map +1 -0
  64. package/dist/runtime/create-context.d.ts +50 -0
  65. package/dist/runtime/create-context.d.ts.map +1 -0
  66. package/dist/runtime/create-context.js +73 -0
  67. package/dist/runtime/create-context.js.map +1 -0
  68. package/dist/runtime/index.d.ts +10 -0
  69. package/dist/runtime/index.d.ts.map +1 -0
  70. package/dist/runtime/index.js +10 -0
  71. package/dist/runtime/index.js.map +1 -0
  72. package/dist/runtime/loop-context-pool.d.ts +58 -0
  73. package/dist/runtime/loop-context-pool.d.ts.map +1 -0
  74. package/dist/runtime/loop-context-pool.js +114 -0
  75. package/dist/runtime/loop-context-pool.js.map +1 -0
  76. package/dist/runtime/path.d.ts +114 -0
  77. package/dist/runtime/path.d.ts.map +1 -0
  78. package/dist/runtime/path.js +302 -0
  79. package/dist/runtime/path.js.map +1 -0
  80. package/dist/runtime/proxy.d.ts +18 -0
  81. package/dist/runtime/proxy.d.ts.map +1 -0
  82. package/dist/runtime/proxy.js +53 -0
  83. package/dist/runtime/proxy.js.map +1 -0
  84. package/dist/runtime/sandbox.d.ts +20 -0
  85. package/dist/runtime/sandbox.d.ts.map +1 -0
  86. package/dist/runtime/sandbox.js +32 -0
  87. package/dist/runtime/sandbox.js.map +1 -0
  88. package/dist/types.d.ts +175 -0
  89. package/dist/types.d.ts.map +1 -0
  90. package/dist/types.js +22 -0
  91. package/dist/types.js.map +1 -0
  92. package/dist/vm/action.d.ts +11 -0
  93. package/dist/vm/action.d.ts.map +1 -0
  94. package/dist/vm/action.js +10 -0
  95. package/dist/vm/action.js.map +1 -0
  96. package/dist/vm/errors.d.ts +5 -0
  97. package/dist/vm/errors.d.ts.map +1 -0
  98. package/dist/vm/errors.js +5 -0
  99. package/dist/vm/errors.js.map +1 -0
  100. package/dist/vm/executor.d.ts +35 -0
  101. package/dist/vm/executor.d.ts.map +1 -0
  102. package/dist/vm/executor.js +137 -0
  103. package/dist/vm/executor.js.map +1 -0
  104. package/dist/vm/handlers/array/pop.d.ts +12 -0
  105. package/dist/vm/handlers/array/pop.d.ts.map +1 -0
  106. package/dist/vm/handlers/array/pop.js +28 -0
  107. package/dist/vm/handlers/array/pop.js.map +1 -0
  108. package/dist/vm/handlers/array/push.d.ts +13 -0
  109. package/dist/vm/handlers/array/push.d.ts.map +1 -0
  110. package/dist/vm/handlers/array/push.js +42 -0
  111. package/dist/vm/handlers/array/push.js.map +1 -0
  112. package/dist/vm/handlers/array/shift.d.ts +12 -0
  113. package/dist/vm/handlers/array/shift.d.ts.map +1 -0
  114. package/dist/vm/handlers/array/shift.js +28 -0
  115. package/dist/vm/handlers/array/shift.js.map +1 -0
  116. package/dist/vm/handlers/array/splice.d.ts +12 -0
  117. package/dist/vm/handlers/array/splice.d.ts.map +1 -0
  118. package/dist/vm/handlers/array/splice.js +59 -0
  119. package/dist/vm/handlers/array/splice.js.map +1 -0
  120. package/dist/vm/handlers/array/unshift.d.ts +13 -0
  121. package/dist/vm/handlers/array/unshift.d.ts.map +1 -0
  122. package/dist/vm/handlers/array/unshift.js +42 -0
  123. package/dist/vm/handlers/array/unshift.js.map +1 -0
  124. package/dist/vm/handlers/array/utils.d.ts +10 -0
  125. package/dist/vm/handlers/array/utils.d.ts.map +1 -0
  126. package/dist/vm/handlers/array/utils.js +33 -0
  127. package/dist/vm/handlers/array/utils.js.map +1 -0
  128. package/dist/vm/handlers/batch.d.ts +12 -0
  129. package/dist/vm/handlers/batch.d.ts.map +1 -0
  130. package/dist/vm/handlers/batch.js +40 -0
  131. package/dist/vm/handlers/batch.js.map +1 -0
  132. package/dist/vm/handlers/call.d.ts +14 -0
  133. package/dist/vm/handlers/call.d.ts.map +1 -0
  134. package/dist/vm/handlers/call.js +65 -0
  135. package/dist/vm/handlers/call.js.map +1 -0
  136. package/dist/vm/handlers/emit.d.ts +12 -0
  137. package/dist/vm/handlers/emit.d.ts.map +1 -0
  138. package/dist/vm/handlers/emit.js +26 -0
  139. package/dist/vm/handlers/emit.js.map +1 -0
  140. package/dist/vm/handlers/if.d.ts +13 -0
  141. package/dist/vm/handlers/if.d.ts.map +1 -0
  142. package/dist/vm/handlers/if.js +35 -0
  143. package/dist/vm/handlers/if.js.map +1 -0
  144. package/dist/vm/handlers/index.d.ts +11 -0
  145. package/dist/vm/handlers/index.d.ts.map +1 -0
  146. package/dist/vm/handlers/index.js +46 -0
  147. package/dist/vm/handlers/index.js.map +1 -0
  148. package/dist/vm/handlers/log.d.ts +12 -0
  149. package/dist/vm/handlers/log.d.ts.map +1 -0
  150. package/dist/vm/handlers/log.js +41 -0
  151. package/dist/vm/handlers/log.js.map +1 -0
  152. package/dist/vm/handlers/loop.d.ts +12 -0
  153. package/dist/vm/handlers/loop.d.ts.map +1 -0
  154. package/dist/vm/handlers/loop.js +71 -0
  155. package/dist/vm/handlers/loop.js.map +1 -0
  156. package/dist/vm/handlers/navigate.d.ts +12 -0
  157. package/dist/vm/handlers/navigate.d.ts.map +1 -0
  158. package/dist/vm/handlers/navigate.js +43 -0
  159. package/dist/vm/handlers/navigate.js.map +1 -0
  160. package/dist/vm/handlers/set.d.ts +15 -0
  161. package/dist/vm/handlers/set.d.ts.map +1 -0
  162. package/dist/vm/handlers/set.js +30 -0
  163. package/dist/vm/handlers/set.js.map +1 -0
  164. package/dist/vm/index.d.ts +8 -0
  165. package/dist/vm/index.d.ts.map +1 -0
  166. package/dist/vm/index.js +7 -0
  167. package/dist/vm/index.js.map +1 -0
  168. package/package.json +34 -0
  169. package/src/errors.ts +194 -0
  170. package/src/expression/README.md +192 -0
  171. package/src/expression/cache.ts +199 -0
  172. package/src/expression/compiler.ts +144 -0
  173. package/src/expression/dependencies.ts +116 -0
  174. package/src/expression/evaluate.ts +95 -0
  175. package/src/expression/evaluator.ts +640 -0
  176. package/src/expression/index.ts +27 -0
  177. package/src/expression/parser.ts +54 -0
  178. package/src/expression/utils.ts +89 -0
  179. package/src/expression/whitelist.ts +224 -0
  180. package/src/globals.d.ts +10 -0
  181. package/src/index.ts +72 -0
  182. package/src/runtime/context.ts +8 -0
  183. package/src/runtime/create-context.ts +133 -0
  184. package/src/runtime/index.ts +28 -0
  185. package/src/runtime/loop-context-pool.ts +134 -0
  186. package/src/runtime/path.ts +372 -0
  187. package/src/runtime/proxy.ts +66 -0
  188. package/src/runtime/sandbox.ts +43 -0
  189. package/src/types.ts +177 -0
  190. package/src/vm/errors.ts +10 -0
  191. package/src/vm/executor.ts +210 -0
  192. package/src/vm/handlers/array/pop.ts +47 -0
  193. package/src/vm/handlers/array/push.ts +68 -0
  194. package/src/vm/handlers/array/shift.ts +47 -0
  195. package/src/vm/handlers/array/splice.ts +78 -0
  196. package/src/vm/handlers/array/unshift.ts +68 -0
  197. package/src/vm/handlers/array/utils.ts +41 -0
  198. package/src/vm/handlers/batch.ts +57 -0
  199. package/src/vm/handlers/call.ts +92 -0
  200. package/src/vm/handlers/emit.ts +39 -0
  201. package/src/vm/handlers/if.ts +48 -0
  202. package/src/vm/handlers/index.ts +52 -0
  203. package/src/vm/handlers/log.ts +54 -0
  204. package/src/vm/handlers/loop.ts +102 -0
  205. package/src/vm/handlers/navigate.ts +64 -0
  206. package/src/vm/handlers/set.ts +43 -0
  207. package/src/vm/index.ts +8 -0
  208. package/tsconfig.json +17 -0
  209. 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'