@report/expression
受限表达式的词法、语法、AST 与求值器。不执行任意 JavaScript。
对应设计文档 §4.2、§10。
定位
表达式系统使用受限语法和 AST,禁止通过 eval 或 new Function 执行任意 JavaScript(§4.2)。
本包只依赖 @report/core 的类型,不依赖 DOM、不依赖任何求值框架。
安装与使用
import { evaluateExpression, compile, evaluateCompiled, ExpressionCache } from "@report/expression"
// 一次性求值
const { value, diagnostics } = evaluateExpression("row.price * row.quantity", {
scope: { row: { price: 3, quantity: 4 } },
})
// value === 12
// 预编译(明细表每行求值一次时使用,避免重复解析)
const compiled = compile("if(row.amount > 0, '有', '无')")
evaluateCompiled(compiled, { scope: { row: { amount: 5 } } }) // "有"
安全边界
这是本包最重要的设计约束。安全通过五层保证:
- AST 即全部 — 求值器只处理
ast.ts中定义的 9 种节点,没有其它可执行形式。 - 作用域白名单 — 根标识符只认识
row/params/group/page/index/rownumber/dataset。window、globalThis、process、document一律解析为undefined。 - 原型链防护 — 属性访问拒绝
__proto__、constructor、prototype, 因此row.constructor.constructor拿不到Function。 - 函数白名单 — 解析期即拒绝未登记的函数调用,非法表达式在任何求值之前就失败。
- 异常兜底 — 求值过程中的任何异常都转成诊断,不向上抛(§13)。
parse("eval('1+1')") // null,报"不允许调用函数"
parse("row.constructor") // 求值为 undefined
parse("row.__proto__") // 求值为 undefined
parse("`${row.amount}`") // null,模板字符串不受支持
parse("(x) => x") // null,箭头函数不受支持
parse("row.amount = 1") // null,赋值不受支持
parse("row.name.toUpperCase()") // null,成员方法调用不受支持
字符串与数组不提供任何方法,只能读 length 与下标。
语法
conditional ?:
logical-or ||
logical-and &&
nullish ??
equality == != === !==
relational > >= < <=
additive + -
multiplicative * / %
unary - + !
postfix . [] ()
primary 字面量 / 标识符 / ( expr )
字段访问是表达式求值的核心用法:
row.amount 字段
params.customerName 参数
row.price * row.quantity 运算
if(row.amount > 0, "有", "无") 条件
format(row.date, "YYYY-MM-DD") 格式化
内置函数
| 分类 | 函数 |
|---|---|
| 逻辑 | if and or not isnull coalesce |
| 字符串 | concat len upper lower trim substr replace |
| 数学 | round floor ceil abs min max |
| 转换 | number string date |
| 格式化 | format |
if 是惰性求值:只求值命中的分支,未命中分支的错误不会影响结果。
format 同时支持日期与数字模式:
format(row.date, "YYYY-MM-DD") // 2024-03-05
format(1234567.891, "#,##0.00") // 1,234,567.89
format(0.1234, "0.0%") // 12.3%
replace 只做字面量替换,不接受正则,避免 ReDoS 与转义歧义:
replace("a.b.c", ".", "-") // "a-b-c"
replace("abc", ".", "X") // "abc"('.' 只匹配字面点)
date 返回 ISO 字符串而非 Date 对象,保证表达式结果始终是可序列化的纯数据(§3.5)。
空值语义
报表场景下空单元格很常见,因此约定:
null/undefined参与算术视为 0(避免整列求和变空)。- 与字符串相加按拼接处理,
null视为空串。 - 除数为 0 返回
null并产生诊断,不产生Infinity。 - 字段缺失返回
undefined并产生data.dataset-missing诊断。
错误处理
单个字段错误不应导致整个文档无结果(§10)。所有 API 都返回结果对象而非抛异常:
const { value, diagnostics } = evaluateExpression("1 +", {
scope,
elementId: "el-1",
path: "/sections/1/children/0/binding/expression",
})
// value === null
// diagnostics[0].code === "expression.parse-error"
// diagnostics[0].elementId === "el-1"
值绑定
binding.ts 把文档模型里的 ValueBinding { field, expression, format } 解析成最终值。
field 是 expression 的语法糖(fieldToExpression("amount") → "row.amount"),
两条路径行为完全一致。
import { resolveBinding, ExpressionCache } from "@report/expression"
const cache = new ExpressionCache()
resolveBinding(
{ field: "amount", format: "#,##0.00" },
{ scope: { row: { amount: 1234.5 } } },
cache,
)
// { value: 1234.5, text: "1,234.50", diagnostics: [] }
目录结构
src/
├── tokenizer.ts 词法分析(最长匹配运算符、中文标识符)
├── ast.ts AST 节点定义与遍历工具
├── parser.ts 递归下降解析器 + 优先级爬升
├── scope.ts 求值作用域与安全属性访问
├── builtins.ts 内置函数(纯函数)
├── evaluator.ts 受限求值器
├── binding.ts ValueBinding 求值与格式化
└── index.ts 公共出口
开发
pnpm build # tsup 打包
pnpm test # vitest
pnpm typecheck # tsc --noEmit
当前测试覆盖
tests/expression.test.ts 共 83 个用例,其中 20 个专门验证安全边界:
- 禁止任意代码执行:
eval、Function、require、import()、宿主 API - 原型链逃逸:
constructor、__proto__、prototype及其组合 - 不支持的语法:模板字符串、箭头函数、赋值、
new、this、注释 - 作用域白名单与
extra自定义根 - 运算语义、空值处理、内置函数、格式化
- 错误诊断与编译缓存
尚未实现
- 聚合函数(
sum/avg/count),需要配合分组的上下文聚合(阶段 3)。 - 自定义函数注册接口(当前白名单为编译期常量)。