Files
workspace/code/one-designer/packages/report-expression
2026-09-20 17:34:40 +08:00
..
2026-09-20 17:34:40 +08:00
2026-09-20 17:34:40 +08:00
2026-09-20 17:34:40 +08:00
2026-09-20 17:34:40 +08:00
2026-09-20 17:34:40 +08:00
2026-09-20 17:34:40 +08:00
2026-09-20 17:34:40 +08:00
2026-09-20 17:34:40 +08:00

@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 } } }) // "有"

安全边界

这是本包最重要的设计约束。安全通过五层保证:

  1. AST 即全部 — 求值器只处理 ast.ts 中定义的 9 种节点,没有其它可执行形式。
  2. 作用域白名单 — 根标识符只认识 row / params / group / page / index / rownumber / dataset。 window、globalThis、process、document 一律解析为 undefined。
  3. 原型链防护 — 属性访问拒绝 __proto__、constructor、prototype, 因此 row.constructor.constructor 拿不到 Function。
  4. 函数白名单 — 解析期即拒绝未登记的函数调用,非法表达式在任何求值之前就失败。
  5. 异常兜底 — 求值过程中的任何异常都转成诊断,不向上抛(§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)。
  • 自定义函数注册接口(当前白名单为编译期常量)。