
从 sqlglot、ast-grep 和 pyrefly 出发,分别练习把 SQL 解析成表达式树再跨方言写回、用语法树搜索和改写代码,以及在不运行代码的前提下推断 Python 类型,并给出今晚能跑通的最小路径。
ARRAY_INSERT 位置的换算。12 ast-grep 的稳定版停在 8 月 31 日的 0.45.3,主分支 9 月 28 日合进了「自定义语言加载策略」。34 pyrefly 在 9 月 29 日凌晨连发 1.3.2 稳定补丁与 1.4.0-dev.2,主分支到今天上午还有提交。567--tokenize 和 --parse 正好把前两段单独暴露出来,不写一行 Python 就能看到中间产物。8Select、Column、Alias、Add、Table 各是一个节点类,节点带 find_all() 这类遍历方法,也带 .sql() 把自己写回文本。官方另写了一篇 AST primer 专门讲这棵树怎么走。810Dialect 之后,改 Tokenizer 里的引号、标识符定界符和关键字表,改 Generator 里的 TRANSFORMS(节点怎么打印)和 TYPE_MAPPING(类型怎么翻译),一门方言就立起来了。仓库 sqlglot/dialects/ 下每个方言一个文件,可以挑一个短的照抄。8sqlglot/optimizer/ 里有 qualify、annotate_types、pushdown_projections、simplify、eliminate_subqueries 等十几条,optimize() 负责按顺序串起来。想改哪条规则,改动面就是那一个文件。8pip install sqlglot
# 把 DuckDB 的 EPOCH_MS 翻成 Hive 的写法
python -m sqlglot "SELECT EPOCH_MS(1618088028295)" --read duckdb --write hive
# SELECT
# FROM_UNIXTIME(1618088028295 / POW(10, 3))
# 只看分词结果
python -m sqlglot "SELECT 1" --tokenize
# <Token token_type: TokenType.SELECT, text: SELECT, line: 1, col: 6, ...>
# 只看表达式树
python -m sqlglot "SELECT a, b + 1 AS c FROM d" --parseSelect(expressions=[Column(...), Alias(this=Add(this=Column(...), expression=Literal(...)))], from_=From(this=Table(...)))。命令行的三个开关来自仓库里的 sqlglot/__main__.py,--parse 打印的就是节点的 repr。8--read 和 --write 翻一次,再 --parse 看树;同一句在 --tokenize 和 --parse 两个阶段的差别,正好是「文本」和「结构」的差别。8parse_one("SELECT * FROM x JOIN y JOIN z"),用 find_all(exp.Table) 把表名打出来,再用 .sql() 把树写回去。8posts/ast_primer.md 和 posts/python_sql_engine.md 两篇官方长文,它们把一个 SQL 引擎从零拆出来讲,配着这次改动的 sqlglot/optimizer/pushdown_projections.py 一起看。10sqlglot[c] 编译版出于性能考虑禁掉了运行时子类化,写自定义方言要退回纯 Python 版。8 第二,转译是尽力而为:遇到目标方言不支持的表达式,默认只打一行警告并给出替代写法,想让它直接抛异常要把 unsupported_level 设成 RAISE;类型敏感的改写比如 qualify 默认不开,需要自己传 schema。8 第三,这一版有六条破坏性变更,升级前把 CHANGELOG 顶部那段读完。1print($A) 能跨行跨空格匹配到任意单参数调用,而 print(a, b) 不会命中——一个元变量只吃一个节点。12$META 匹配一个具名节点,$$$ARGS 匹配零个到多个节点(函数参数、语句都行),$$VAR 连匿名节点也吃,名字以 _ 开头的元变量只匹配不捕获。同一个名字出现两次就变成等价约束:$A == $A 只命中两边完全相同的比较。12strictness 从 cst、smart、ast、relaxed、signature 到 template 共六档,默认 smart:模式里的节点必须全部命中,目标代码里的匿名节点(比如 async 这个关键字)可以跳过。这正是 function $A() {} 能同时命中普通函数和 async function 的原因。1314inside、has、follows、precedes 四个操作符让规则可以写成「在 for 循环里的 await 表达式」,stopBy 控制向上或向下搜索到哪一层停。15id、language、rule、message、severity,sgconfig.yml 用 ruleDirs 指向规则目录,ast-grep scan 一次跑完,ast-grep test 还能给规则写快照用例。16schemas/ 下有 29 个文件,Python、TypeScript、Rust、Go、C 各一份,写 YAML 规则时编辑器能直接补全字段名。17# pip 上的包名是 ast-grep-cli,也可以换成 npm install -g @ast-grep/cli 或 brew install ast-grep
pip install ast-grep-cli
# 找一个文件,写一句模式
ast-grep run -p 'print($A)' -l python t2.py
# t2.py:1:print("start")
# t2.py:4:print(value)
# 看模式在 tree-sitter 里被解析成什么
ast-grep run --debug-query=cst -p 'print("start")' -l python t2.py
# 改写默认只预览 diff,加 --update-all 才落盘
ast-grep run -p 'print($A)' -r 'log($A)' -l python t2.pyprint($A) 命中 print("start") 和 print(value),跳过 print(a, b);--debug-query=cst 打出的树是 module → expression_statement → call → argument_list → string,连括号这种匿名节点都在。1213# rules/no-print.yml
id: no-print
language: Python
severity: warning
message: 用日志代替 print
rule:
pattern: print($A)$A == $A 在自己的项目里找重复比较,再用 $_FUNC($_FUNC) 找单参数调用;第二条里两个同名元变量因为带下划线不被捕获,各自可以匹配不同内容。12pattern: await $PROMISE 加上 inside: {kind: for_in_statement, stopBy: end},它会报出循环里的 await。15--strictness 六档对同一段代码的命中差异。18$FIELD = $INIT 想选类字段会被当成赋值表达式,这时要用 pattern 对象的 context 和 selector 把上下文补全。12Any 之后,类型推断会出错,比如 x: Any | None 在 if x is None: x = get_any() 之后应当收窄成 Any,之前会留成 Any | None,导致后面访问属性时误报。5 同一时刻还切了 1.4.0-dev.2,官方说明这一版打包了 493 个提交、20 位贡献者,并提醒 dev 版不要钉在生产项目上。6 主分支到今天上午还有提交,最近几条围绕 except handler 的窄化和不可达代码的回归。7import * 传递地解开),再把每个模块单独转成一组绑定,最后求解绑定——跨模块的部分就去别的模块取值。20define x@1、use x@2、anon @2,@ 后面是标识符在文件里的字节偏移,用来消歧;值是别的键或字面量,所以整张图是一张有向图。20Type::Var;控制流汇合处用 phi 合并两个值,phi(int, str) 就是 int | str。对自引用,官方给的做法是取可达上界:x = x 这种循环最后收敛成一个具体类型。20pyrefly_types 定义类型和类型上的运算,pyrefly_graph 负责索引与依赖缓存,pyrefly_config 读配置(顺带迁移 mypy 和 pyright 的配置),pyrefly_bundled 装 typeshed 存根,pyrefly_wasm 是浏览器沙箱。20pip install pyrefly 之后 pyrefly init 生成配置、pyrefly check 开始检查。21 没有配置文件时它走 basic 预设,只报高置信度的错误。读源码的门槛在体量:仓库含网站和一致性测试语料,clone 一次要拉下将近 2 GB,建议按 crate 目录分批看。20pip install pyrefly
pyrefly init # 生成 pyrefly.toml,并尝试迁移已有的 mypy/pyright 配置
pyrefly check # 检查整个项目
pyrefly check --summarize-errorsERROR Argument `Literal['2']` is not assignable to parameter `y` with type `int` in function `add` [bad-argument-type]
ERROR Object of class `int` has no attribute `missing_attribute` [missing-attribute]
INFO 2 errors.gitignore 覆盖),pyrefly check 会把文件全部跳过,只留一句 No Python files matched patterns,换到仓库外的目录就正常。str | None,在 if x is None: 分支里直接返回,再调 .upper(),看它是否放行;把判断改成 if x is not None: 再看一次诊断。这一条对应上面的 flow types 设计。20pyrefly infer,比较它补出来的签名和你心里想的那份差在哪。19pyrefly_types 里的 Type::Var 怎么被填上。20pyrefly suppress 把已有错误标成忽略,再逐条去掉。1921 另外,增量只做到模块级,超大仓库首次全量检查的时间要按这个前提估计;想先感受界面再决定要不要装,官方有一个浏览器沙箱。20| 你想练的主线 | 仓库与本期信号 | 最小条件 | 第一条可观察路径 | 主要限制 |
|---|---|---|---|---|
| 解析器与表达式树:把文本变成结构 | sqlglot · 30.21.0 北京时间 10 月 1 日凌晨发布 | 一条 pip install,零依赖 | python -m sqlglot "SELECT a, b + 1 AS c FROM d" --parse 打出表达式树 | 编译版不能写自定义方言;转译对不支持的表达式只发警告。(README) |
| 语法树搜索与批量改写 | ast-grep · 0.45.3 8 月 31 日发布,主分支 9 月 28 日仍有提交 | npm / pip / brew 任一 | ast-grep run -p 'print($A)' -l python . 直接列出命中行 | 每门语言要对应 tree-sitter 语法;模式必须解析成合法代码。(README) |
| 类型推断与语言服务器 | pyrefly · 1.3.2 北京时间 9 月 29 日凌晨发布,主分支今天仍有提交 | 一条 pip install | pyrefly check 在自己的项目上打出带错误类别的诊断 | 只在模块级增量;仓库体量大,按 crate 分批读。(README) |
github.com
pypi.org
github.com
github.com
github.com
github.com
github.com
github.com
github.com
github.com
github.com
ast-grep.github.io
ast-grep.github.io
ast-grep.github.io
ast-grep.github.io
ast-grep.github.io
github.com
ast-grep.github.io
github.com
github.com
pyrefly.org
pypi.org
This story was produced automatically by a channel. One sentence is all it takes for Neodrop to keep producing for you.