多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

vega-expression 解析器与代码生成器全解析:Vega 表达式语言的安全求值引擎

vega-expression 解析器与代码生成器全解析:Vega 表达式语言的安全求值引擎 数据可视化【免费下载链接】vegaA visualization grammar.项目地址https://gitcode.com/gh_mirrors/ve/vega点击查看免费下载导读vega-expression是 Vega 可视化语法体系中负责表达式环节的核心基础包它把一个受限制的 JavaScript 表达式子集解析为抽象语法树AST再通过可配置的代码生成器输出可被eval执行的 JavaScript 代码。在 Vega 中编码属性encode、信号signal、变换transform等场景大量依赖表达式来描述动态计算逻辑。读完本文你将掌握parse、codegen、constants、functions、ASTNode五个核心 API 的完整用法理解其安全设计边界禁用赋值、new、控制流语句并能在自己的 Vega 扩展或工具链中正确配置字段跟踪与全局变量白名单。一、设计目标简单、富有表现力、且安全Vega 的表达式语言是一种JavaScript 表达式的受限子集。根据 packages/vega-expression/README.md 的定位该包由两部分组成解析器parser将表达式字符串解析为 ESTree 格式的抽象语法树代码生成器code generator接收 AST输出可eval执行的代码字符串。解析器识别基本 JavaScript 表达式但明确禁用三类语法赋值运算符、等new表达式控制流语句for、while、switch等。代码生成器则在此基础上进一步限制允许调用的函数集合与可引用的变量名集合。整套设计的目的是提供简单、富有表现力、且具有安全意识的表达式求值。这一安全策略在 parser.js 中有直接体现。例如parsePostfixExpression遇到/--直接抛出DISABLED错误parseUnaryExpression对delete、void、typeof同样拒绝parseExpression遇到逗号序列表达式a, b也会抛出DISABLED。此外解析器对关键字表parser.js中出现的if、for、new、var、function等词法单元会做拦截其中if虽被列为关键字但通过legalKeywords特例放行——它被解析为标识符并在函数层由if(a, b, c)这种三元函数形式替代。二、parse(expression)基于 Esprima 的精简解析器parse(expression)接收一个 JavaScript 表达式字符串返回 ESTree 格式的抽象语法树。其实现位于 parser.js文件头部保留了完整的 Esprima 版权与许可声明源码注释也明确说明该表达式解析器基于 Esprima (http://esprima.org/)。2.1 词法分析解析器采用手写的词法扫描器按 Esprima 的规范实现分词标识符支持$、_、ASCII 字母数字以及通过RegexNonAsciiIdentifierStart/RegexNonAsciiIdentifierPart两个大型 Unicode 正则支持的非 ASCII 标识符字符parser.js数字字面量十进制、十六进制0x、科学计数法3e5、3e5均支持但八进制字面量被禁用012会抛错测试见 parser-test.js字符串字面量支持单双引号、转义序列、Unicode 码点转义\u{...}正则字面量支持/pattern/flags形式并在 parser.js 中通过testRegExp预先校验正则的合法性。2.2 语法分析语法层实现了完整的表达式优先级链从 parser.js 的主表达式parsePrimaryExpression字面量、数组、对象、括号、正则逐级上升到 parseExpression主表达式 → 成员访问/函数调用. [ ] ( )→ 一元运算 → 二元运算按优先级→ 条件表达式 ?:其中二元运算符的优先级表在 parser.js 中定义||(1) (2) 位或(3) 异或(4) 与(5) 相等(6) 关系(7) 移位(8) 加减(9) 乘除模(11)与标准 JavaScript 一致。对象字面量会拒绝重复属性名数组字面量允许[1, , 3]形式的稀疏元素elements中以null占位。2.3 错误行为错误消息与 V8 保持一致如Unexpected token %0、Unexpected end of input错误对象会携带index与description属性便于定位。测试用例 parser-test.js 覆盖了大量合法/非法输入字面量布尔、数字、字符串、正则、数组、对象、运算符优先级、非法输入0x、012、未闭合字符串、非法正则等。三、ASTNode(type) 与 AST 遍历ASTNode(type)是表达式 AST 节点的构造函数实现在 ast.js。节点的type属性取自一组常量RawCode、Literal、Property、Identifier、ArrayExpression、BinaryExpression、CallExpression、ConditionalExpression、LogicalExpression、MemberExpression、ObjectExpression、UnaryExpression。每个节点都实现了visit(visitor)方法以深度优先方式遍历子树若visitor(node)返回真值则提前终止遍历ast.js。子节点的枚举规则定义在children(node)中例如CallExpression的子节点是[callee].concat(arguments)MemberExpression是[object, property]而Identifier、Literal、RawCode是叶子节点。这套遍历机制是代码生成器实现函数白名单注入的基础——functions选项正是给定一个 AST visitor 实例来工作的。包的入口 index.js 将上述类型常量与parseExpression、codegenExpression、functions、constants全部导出TypeScript 声明见 index.d.ts。四、codegen(options)可配置的代码生成器codegen(options)创建一个按 options 配置的代码生成器返回的生成函数接收 AST输出可eval的 JavaScript 代码。输出是一个对象包含三个属性类型声明见 index.d.ts属性含义code生成的代码字符串fields在fieldvar作用域内被引用的所有属性的哈希即表达式依赖的数据字段名集合globals在allowed白名单之外被引用的所有变量的哈希核心实现位于 codegen.js它维护一个Generators表为每种 AST 节点类型提供代码生成函数例如Literal直接输出原始字面量n.rawBinaryExpression输出(left op right)ConditionalExpression输出(test?consequent:alternate)。4.1 options 详解codegen支持的配置项如下以下为原文 API 的完整继承并结合源码补充说明constants允许的顶层常量哈希从常量名映射到常量值值以字符串形式原样注入生成的代码。默认使用包的 constants 对象。在 codegen.js 中当标识符命中constants时直接输出对应的注入代码例如表达式中的PI会被替换为Math.PI。functions一个函数接收 AST visitor 实例作为输入返回允许调用的函数哈希。值可以是字符串原样注入后追加参数也可以是函数接收参数 AST 节点数组返回要注入的代码。默认使用包的functions方法。在 codegen.js 中CallExpression要求 callee 必须是标识符Illegal callee type函数名必须命中functions白名单否则报Unrecognized function。**forbidden**禁止在表达式作用域内引用的变量名数组通常用于封禁不安全的全局变量。在 [codegen.js](https://link.gitcode.com/i/999707be0732d4ea77b65f247e31bcc9#L44-L45) 中命中即报Illegal identifier。allowed允许在表达式作用域内引用的变量名数组通常对应表达式的函数参数名。不在白名单内的变量会被收集为全局变量见globalvar。注意allowed与constants的判定顺序在 codegen.js 的Identifier生成器中判定顺序是forbidden→constants→allowed→ 全局变量。fieldvar生成表达式函数内主要数据输入参数的名称。例如函数function(d) { return d.x * d.y; }中d是字段变量x、y是其访问的属性。在 codegen.js 的MemberExpression生成器中若对象是fieldvar则会把访问的属性名会先通过stripQuotes去除引号以净化字段名注释引用 issue #1653记录到fields中——这正是 Vega 数据流做字段依赖追踪的基础。globalvar必填查找全局变量所用的变量名。该名字会被注入到生成代码中作为全局变量引用的作用域前缀。它也可以是一个函数接收源输入中的变量名返回要写入输出的代码。默认实现是id ${globalvar}[${id}]见 codegen.js即生成globalvar[id]形式的访问。4.2 一个完整的生成示例假设有如下表达式与配置import { parseExpression, codegenExpression } from vega-expression; const ast parseExpression(d.x * d.y PI); const codegen codegenExpression({ constants: { PI: Math.PI }, // 允许 PI 常量 fieldvar: d, // 数据字段变量 allowed: [d], // 允许的参数名 globalvar: GLOBALS // 全局变量作用域前缀 }); const result codegen(ast); // result.code ≈ ((d.x * d.y) Math.PI) // result.fields ≈ [x, y] // result.globals ≈ []若表达式改为d.x * width且width不在allowed中则width会被收集进globals生成代码中写作GLOBALS[width]从而实现全局变量必须经由显式作用域对象访问的安全约束。此外 codegen.js 的ObjectExpression生成器还会校验对象字面量的键名不得覆盖Object.prototype上的危险方法通过vega-util的DisallowedObjectProperties检查进一步收紧安全性。五、安全机制小结三层防线综合源码vega-expression的安全设计可以归纳为三层防线词法/语法层parser.js拒绝赋值、new、/--、delete/void/typeof、逗号序列表达式、八进制字面量、控制流关键字调用层codegen.js只允许调用白名单内的函数callee 必须是简单标识符禁止obj.method()这种任意方法调用标识符层codegen.js通过forbidden/allowed/constants/globalvar精确控制哪些名字可访问、哪些名字必须经由指定作用域对象间接访问。六、constants 与 functions6.1 constants默认常量constants对象定义 Vega 表达式语言的默认常量值从常量标识符映射到定义该常量的 JavaScript 代码。实现在 constants.js完整映射如下常量注入代码NaNNaNEMath.ELN2Math.LN2LN10Math.LN10LOG2EMath.LOG2ELOG10EMath.LOG10EPIMath.PISQRT1_2Math.SQRT1_2SQRT2Math.SQRT2MIN_VALUENumber.MIN_VALUEMAX_VALUENumber.MAX_VALUE可见大多数常量被映射到Math.*或Number.*等内置对象上避免引入全局作用域污染。6.2 functions默认函数白名单functions(codegen)接收一个 codegen 实例返回所有合法函数名到函数值的映射。值可以是字符串原样注入并追加参数也可以是函数接收参数 AST 数组、返回注入代码。实现在 functions.js其中大部分数学函数直接映射为内置方法字符串例如abs: Math.abs、isNaN: Number.isNaN、random: Math.random。日期/时间类函数通过fncall辅助函数生成构造器 方法调用代码例如date生成(new Date(x)).getDate()DATE new Date与fn(getDate, DATE, 0)的组合utc映射为Date.UTC。值得注意的是datetime直接映射为new Date。两个特殊函数展示了函数值形态的灵活用法functions.js 与 #L100-L105clamp(value, min, max)校验参数个数必须恰为 3 个生成Math.max(min, Math.min(max, value))if(test, consequent, alternate)以函数形式替代被禁用的if语句生成三元表达式(test?consequent:alternate)。七、包内置的常量与函数清单vega-expression自带上述常量以及下列函数所有其他 Vega 表达式函数由 packages/vega-functions 包提供两者合起来构成完整的 Vega 表达式函数库。Math 函数isNaN、isFinite、abs、acos、asin、atan、atan2、ceil、cos、exp、floor、hypot、log、max、min、pow、random、round、sin、sqrt、tan、clampDate/Time 函数now、utc、datetime、date、day、year、month、hours、minutes、seconds、milliseconds、time、timezoneoffset、utcdate、utcday、utcyear、utcmonth、utchours、utcminutes、utcseconds、utcmillisecondsSequence数组或字符串函数length、join、indexof、lastindexof、reverse、slice、sortString 函数parseFloat、parseInt、upper、lower、replace、split、substring、trim、atob、btoa、encodeURIComponentRegExp 函数regexp、test以上清单均可在 functions.js 的返回对象中逐一核对。八、在 Vega 项目中的实际位置与用法vega-expression是 monorepolerna.json中的一个独立包package.jsonpackages/vega-expression/package.json声明其版本为 6.2.1依赖vega-util与types/estree提供 ESM 构建产物。它被上游的 packages/vega-parser将 Vega 规范解析为数据流和 packages/vega-view 等包使用用于把规范中的表达式字符串编译为可在数据流中执行的回调函数。典型使用模式为import { parseExpression, codegenExpression, constants, functions } from vega-expression; // 1. 解析表达式字符串 const ast parseExpression(datum.value * 2); // 2. 创建代码生成器使用默认常量与函数白名单 const codegen codegenExpression({ fieldvar: datum, allowed: [datum], globalvar: global }); // 3. 生成可执行代码 依赖字段追踪 const { code, fields, globals } codegen(ast);关于该表达式语言支持的完整 JavaScript 表达式范围可进一步阅读仓库内的官方文档 docs/docs/expressions.md。若你需要自定义函数白名单例如限制只能调用部分数学函数可以通过codegen的functions选项实现该选项接收一个 AST visitor让你在访问器内部决定哪些函数调用被允许、如何生成对应代码。九、测试与验证包内测试覆盖了解析与代码生成两个层面可直接运行验证npm test会先构建再执行 tape 测试见 packages/vega-expression/package.jsonpackages/vega-expression/test/parser-test.js545 行的解析器测试覆盖字面量、运算符、数组/对象初始化器、成员访问、非法输入抛错等场景packages/vega-expression/test/codegen-test.js代码生成器测试验证生成代码字符串、字段追踪与全局变量收集行为。例如解析器测试断言parse(true)得到{type: Literal, value: true, raw: true}parser-test.js并断言012、09等非法数字字面量抛错与本文 2.1 节的描述相互印证。结语vega-expression用不到两个核心文件就实现了受限 JS 子集解析 安全代码生成的完整能力parse基于 Esprima 提供符合 ESTree 的语法树codegen通过constants、functions、forbidden、allowed、fieldvar、globalvar六个配置项精确控制生成代码的作用域与安全边界fields/globals输出则为 Vega 数据流的依赖追踪与全局变量治理提供了直接支撑。理解这个包也就理解了 Vega 中所有表达式求值的底层运行机制无论是排查表达式报错、编写自定义变换还是评估在自身系统中复用这套安全表达式引擎的可行性都能做到有的放矢。赞分享数据可视化【免费下载链接】vegaA visualization grammar.项目地址https://gitcode.com/gh_mirrors/ve/vega点击查看免费下载相关推荐Vega 表达式解释器 vega-interpreter 完全指南CSP 兼容的 AST 求值方案Vega 表达式解释器 vega interpreter 完全指南CSP 兼容的 AST 求值方案 vega interpreter 是 Vega 可视化语法数据可视化用 ce-explain 在改动前理解某子系统的实现方式与设计原因用 ce explain 在改动前理解某子系统的实现方式与设计原因 准备修改一个自己没写过的子系统时最大的风险不是改动本身而是不知道它现在的行为边界和当时的数据可视化vega-functions 深度解析Vega 表达式语言的自定义函数库实现与使用指南vega functions 深度解析Vega 表达式语言的自定义函数库实现与使用指南 说明本文以 packages/vega functions/READ数据可视化上一篇如何快速上手ByteDance-Seed/TaskMemQwen3VLMoe模型本地部署与推理完整指南下一篇PHPBrew自定义构建终极指南如何添加私有扩展和应用补丁创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表