# @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)。 - 自定义函数注册接口(当前白名单为编译期常量)。