20260920173439
This commit is contained in:
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)。
|
||||
- 自定义函数注册接口(当前白名单为编译期常量)。
|
||||
Reference in new issue
Block a user