20260920173439

This commit is contained in:
oneao committed 2026-09-20 17:34:40 +08:00
1 parent f4bfc1ddfc
commit 6f03d92425
174 files changed
+23490 -157

No files matched your search

@@ -0,0 +1,186 @@
# @report/expression
受限表达式的词法、语法、AST 与求值器。**不执行任意 JavaScript。**
> 对应设计文档 §4.2、§10。
## 定位
表达式系统使用**受限语法和 AST**,禁止通过 `eval` 或 `new Function` 执行任意 JavaScript(§4.2)。
本包只依赖 `@report/core` 的类型,不依赖 DOM、不依赖任何求值框架。
## 安装与使用
```ts
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)。
```ts
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` 同时支持日期与数字模式:
```ts
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 与转义歧义:
```ts
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 都返回结果对象而非抛异常:
```ts
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"`),
两条路径行为完全一致。
```ts
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 公共出口
```
## 开发
```bash
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)。
- 自定义函数注册接口(当前白名单为编译期常量)。