拓冰建站拓冰建站
首页 / 资讯中心 / 正文

深入解析 parsy:Semgrep 中基于 Python 解析器组合子的锁文件解析实践

SAST应用安全静态分析开发工具代码质量【免费下载链接】semgrepLightweight static analysis for many languages. Find bug variants with patterns that look like source code.项目地址https://gitcode.com/GitHub_Trending/se/semgrep点击查看免费下载导读parsy 是一个优雅的 Python 文本解析库它通过将小解析器组合成复杂的大解析器以声明式方式完成文本解析任务本质上是面向 LL(∞) 文法的单子式解析器组合子monadic parser combinator库。在 Semgrep 项目中parsy 被 vendor 到cli/src/semdep/external/parsy并经过深度改造——在解析流中增量跟踪行号与列号直接服务于cli/src/semdep下一系列 lockfile 解析器如 requirements.txt、yarn.lock、go.mod 等。读完本文你将掌握 parsy 的解析器组合原理、Semgrep 对它的定制改造Position 三元组索引以及这些能力如何被真实 lockfile 解析器复用。一、parsy 是什么解析器组合子的核心思想parsy 的核心主张是“通过组合小解析器来解析文本”先定义能匹配单个 token 的最小解析器如匹配一个字符、一个字符串或一个正则表达式再借助、|、.many()、.sep_by()等组合子把它们拼装成能匹配完整语法结构的复杂解析器。这一点在 parsy/README.rst 中有明确表述——它是一个 Python 解析器组合子库受 Haskell 的 Parsec、Parsnip、Parsimmon 等经典库启发与它们同属解析器组合子范式。该库要求Python 3.7 及以上。在 Semgrep 仓库中 vendored 的版本为 2.0见 version.py。解析器组合子最吸引人的地方在于解析器本身就是普通 Python 对象可以被当作一等公民组合、传参、递归引用。这让语法定义几乎不需要单独的 DSL 或代码生成步骤直接用 Python 表达即可。二、Semgrep 为什么 vendor parsy行/列增量跟踪改造2.1 Vendoring 的动机parsy 原版在解析流中只记录一个整数索引消费了多少个元素而 Semgrep 的锁文件解析需要精确的行号与列号来定位依赖项在 lockfile 中的位置这是 SCA 结果能准确回指源码行的基础。因此 Semgrep 将 parsy vendor 进仓库并做了定制修改这一点在 VENDOR_README.md 中有完整说明把单个整数索引替换为offset、line、column 三元组offset即原来的索引语义——流中已消费的元素个数当解析输入是字符串时每消费一个字符都会增量更新 line 与 column当解析输入不是字符串如 token 列表时line 与 column 均置为 -1 并忽略该改动因向后不兼容而暂时无法合并回上游所以 Semgrep 以 vendoring 方式维护直到上游合并且发版。2.2 Position 三元组在源码中的落地在 parsy/init.py 中这个三元组被实现为不可变 dataclassPositiondataclass(frozenTrue) class Position: offset: int line: int column: int而增量更新逻辑集中在make_index_update函数中它统计被消费片段中的换行数\n若含换行则把column重置为最后一行内的偏移否则直接累加列号def make_index_update(consumed: str) - Callable[[Position], Position]: slen len(consumed) line_count consumed.count(\n) last_nl consumed.rfind(\n) return lambda index: Position( offsetindex.offset slen, lineindex.line line_count, columnslen - (last_nl 1) if last_nl 0 else index.column slen, )所有消费型解析器string、regex、test_item等都通过它推进Position。对于非字符串流如 token 列表则统一产出Position(match.end(), -1, -1)之类的占位值表示行/列信息不可用。2.3 错误报告中的行号利用改造后的位置信息被ParseError直接消费。ParseError持有expected期望集合、stream与index失败的 Positionclass ParseError(RuntimeError): def line_info(self): if isinstance(self.stream, str): return f{self.index.line}:{self.index.column} else: return str(self.index.offset)错误消息格式为expected xxx at line:column这对定位 lockfile 中的语法错误至关重要。三、核心 API 全景Parser 类与组合子3.1 Parser 的语义与入口Parser是一个包装了流 起始 Position → Result函数的对象见 parsy/init.py。它的两个入口方法parse(stream)必须解析整个输入内部等价于(self eof).parse_partial(stream)即强制要求 EOFparse_partial(stream)解析尽可能长的前缀返回(结果, 剩余部分)元组失败则抛ParseError。初始化索引规则字符串流从Position(0, 0, 0)开始非字符串流从Position(0, -1, -1)开始。Result是解析结果的数据类包含status、index成功时的新位置、value、furthest最远失败位置和expected期望集合。其中aggregate方法负责合并多个分支的失败信息保留最远失败点若两个失败点相同则合并期望消息集合这是组合子能给出高质量错误报告的基础。3.2 原语解析器原语行为string(s, transformnoop)精确匹配字符串s可选transform对期望串与输入串同时做归一化如大小写不敏感regex(exp, flags0, group0)用re.match从当前 offset 匹配group可提取指定捕获组test_item(func, desc)用谓词func测试流中的单个元素成功则消费它test_char(func, desc)test_item的字符语义别名match_item(item)测试下一个元素是否等于给定值char_from(s)匹配字符串s中的任意一个字符string_from(*strings)按长度降序排列后依次尝试匹配正确处理重叠字符串如与success(value)/fail(msg)恒成功不消费/ 恒失败any_char、whitespace、letter、digit、decimal_digit常用便捷原语eof仅当流已耗尽时成功from_enum(EnumClass)将枚举成员的值解析为对应枚举项3.3 组合子与修饰符顺序组合p1 p2取p2的值、p1 p2取p1的值、p1 p2拼接两个值要求可seq(*ps, **kw_ps)顺序执行并按位置/关键字收集结果。选择组合p1 | p2与alt(*parsers)按声明顺序尝试失败则尝试下一个。重复组合.times(min, max)限定次数、.many()0 次或多次、.at_least(n)、.at_most(n)、.sep_by(sep, min, max)以sep分隔重复、.until(other, min, max, consume_other)重复直到other成功默认不消费other。值变换.map(f)、.combine(f)把结果列表展开为f(*args)、.combine_dict(f)把 dict 结果展开为f(**kwargs)自动剔除None键和_前缀键、.concat()把结果列表.join成字符串、.result(v)恒返回v、.tag(name)包装为(name, value)二元组。可选与描述.optional(defaultNone)、.desc(description)覆盖失败时的期望消息。位置捕获index返回当前 offset、line_info返回(line, column)、.mark()把结果包装为((start_row, start_col), value, (end_row, end_col))三元组。lookaheadpeek(p)在不消费的情况下预览.should_fail(desc)是负向先行断言p成功则整体失败、p失败则整体成功。递归定义forward_declaration是一个空壳解析器需在真正使用前调用.become(parser)注入行为用于相互递归的语法如 JSON。生成器语法generate装饰器允许用 yield 语法编写命令式风格的解析器yield parser会驱动解析并把结果送回生成器若生成器return一个 Parser 对象则以该 Parser 继续解析。3.4 运算符速查运算符等价语义p1 p2p1.then(p2)取 p2 结果p1 p2p1.skip(p2)取 p1 结果p1 p2顺序解析并拼接operator.addp1 | p2alt(p1, p2)选择p * np.times(n)恰好 n 次p * range(a, b)p.times(a, b-1)四、Semgrep 中的真实用法从单子链到整文件语法4.1 语义化组合工具util.pycli/src/semdep/parsers/util.py 是全部 lockfile 解析器共享的组合子库在 parsy 之上构建了更高层的语义工具not_any(*chars)匹配任意不在给定字符集内的字符序列内部用regex(f[^{escape(...)}])实现line_number line_info.map(lambda t: t[0] 1)parsy 的行号是 0 起始但编辑器是 1 起始这里统一加 1mark_line(p)记录解析前所在行号返回(行号, 结果)二元组pair/triple用bind链把两个/三个解析器结果组装成元组quoted(p)解析被双引号包裹的p引号不进结果string() p string()upto(*s, include_other, consume_other, allow_newline)解析到某个分隔符为止可把分隔符附加进结果或丢弃默认不允许换行被消费integer、any_str、word、line、consume_line常用便捷解析器delay(f)延迟解析器构造解决相互递归函数定义导致的无限求值问题yarn.py 中用到become(p1, p2)forward_declaration.become的类型化版本用于先声明后定义的相互递归。此外 util.py 还包含一个带行号注释的 JSON 解析器json_value、json_object、array、json_doc其注释明确说明它改编自 parsy 官方仓库的 examples/json.py增加了类型标注、行号跟踪并用become实现自引用——这是前向声明 递归的标准范式。4.2 用组合子为 requirements.txt 编写完整语法requirements.pycli/src/semdep/parsers/requirements.py 是 parsy 组合风格的最佳示范。整个文件没有手写字符扫描循环而是分层声明whitespace regex(r[ \t]) | string(\\\n) # 水平空白或行续接 package upto(, , , , [, \n) # 包名 遇到分隔符为止 extras_specifier string([) upto(], consume_otherTrue) version_specifier string_from(, , , , , , ~, !).bind(...) dep package.bind(...) # 包名 可选 extras 版本约束 flag_line (string(--) | string(-)) consume_line requirements mark_line(flag_line | dep | consume_line | comment_line) \ .sep_by(string(\n).at_least(1)) \ .map(lambda xs: [(l, x) for (l, x) in xs if x])值得注意的工程细节package采用用分隔符反向定义而非完整正则upto(, , , , [, \n)直接表达包名就是遇到这些字符之前的文本代码注释明确说明这是有意为之version_specifier用string_from(, , ...)按长度降序处理与的重叠问题并只在且版本不含*时才保留该约束注释在预处理阶段由preprocessors.CommentRemover()剥离COMMENT_REGEX r(^|\s)#.*$解析器本身只处理干净内容每个依赖项通过mark_line携带行号后续在parse_requirements中产出带line_number的FoundDependency。4.3 其他解析器的复用模式cli/src/semdep/parsers/下几乎所有解析器都从 parsy 引入原语组合模式高度一致go_mod.py用regexstringalt解析require ( ... )块与单行 requirepoetry.py用any_char、eof、regex、string、success解析pyproject.toml的依赖片段pipfile.py / gem.py / mix.py / swiftpm.py / pom_tree.py / gradle.py分别基于string、regex、any_char、peek、success等原语构建各自生态的 lockfile 语法。4.4 错误处理闭环util.py 的parse_dependency_file是 parsy 解析器的统一调用入口读文件 → 预处理 → 空文件检查 →parser.parse(text)。捕获到ParseError时会利用改造后的行/列信息生成带行号、列号和出错行的DependencyParserError同样注意0 起始行号 → 1 起始的换算parse_error_to_str则重写了ParseError.__str__的格式便于转义特殊字符。整个流程把 parsy 的低层异常转换为 SCA 可消费的结构化错误对象。五、深入源码的补充细节Result 聚合机制parsy/init.pyaggregate在alt、seq、times等所有多分支场景中持续跟踪furthest最远失败点并合并expected集合——这让alt的最终错误信息能列出所有可能的期望。times的 min/max 语义times(n)恰好 n 次times(min, max)至少 min、至多 maxmany()等价于times(0, inf)optional()等价于times(0,1) 默认值映射。类型桩type stubscli/src/semdep/external/parsy/__init__.pyi提供泛型化的类型注解如Parser[int]让 Mypy 能静态检查解析器的组合类型。util.py 开头有一段重要说明运行时Parser不接受类型参数因此所有类型注解必须写成字符串形式模块声明from __future__ import annotations使其求值为字符串否则Parser[int]这类表达式会在运行时抛错——这是类型安全与运行时行为之间需要牢记的边界。条件限制vendored parsy 面向 Python 3.7行/列跟踪仅在字符串输入时有效字节流或 token 列表输入的行列信息为 -1不可用。若需为这些解析器调试递归深度SEMGREP_PYTHON_RECURSION_LIMIT_INCREASE环境变量可在解析超深嵌套结构时提高 Python 递归上限见 util.py 中RecursionError的处理提示。六、小结parsy 在 Semgrep 中的定位parsy 在 Semgrep 中不是又一个第三方依赖而是 lockfile 解析基础设施的语法引擎它提供最小原语与组合子、Position行/列跟踪、统一的ParseError报错而cli/src/semdep在其上实现了可读、可维护、类型可检查的声明式解析器。若要深入阅读 vendored 库本体parsy/init.py 与 vendor 说明 VENDOR_README.md阅读组合子封装层cli/src/semdep/parsers/util.py阅读端到端范例cli/src/semdep/parsers/requirements.py以及 go_mod.py、poetry.py、yarn.py 等同目录解析器。掌握这套原语 组合子 行号跟踪的思维后你不仅能用 parsy 写出声明式解析器也能直接理解 Semgrep 解析任意 lockfile 的内部机制。赞分享SAST应用安全静态分析开发工具代码质量【免费下载链接】semgrepLightweight static analysis for many languages. Find bug variants with patterns that look like source code.项目地址https://gitcode.com/GitHub_Trending/se/semgrep点击查看免费下载相关推荐深入解析GraphQL-Tools中的解析器组合功能深入解析GraphQL Tools中的解析器组合功能 引言为什么需要解析器组合 在GraphQL服务器开发中我们经常需要在解析器Resolver中实现后端API设计visx/group 深入解析基于 SVG g 的 visx 分组容器组件visx/group 深入解析基于 SVG g 的 visx 分组容器组件 visx/group 是 visx 可视化组件库中最基础、使用最广泛的原子组数据可视化前端图表库doomgeneric社区贡献指南如何参与开源项目开发与维护doomgeneric社区贡献指南如何参与开源项目开发与维护 doomgeneric是一个致力于简化 Doom 移植过程的开源项目通过实现少量核心函数即可将上一篇APScheduler 版本迁移指南从 v1/v2/v3 全面升级到 v4.0 的架构变化与实践要点下一篇G-Helper终极指南轻量级华硕笔记本控制解决方案深度解析创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门