问题
“这个函数在哪被调用了?”
我敲下 rg "getUserInfo" 然后回车。屏幕刷出几十行结果——注释里的、字符串里的、真正的调用、还有一堆看起来像但其实不是的。
每次都这样,我花了比实际搜索更多的时间在过滤噪音上。跨行的函数调用?正则写到怀疑人生。想区分函数定义和函数调用?
然后我发现了 ast-grep。
它到底是什么
ast-grep 用 Tree-sitter 解析代码,生成语法树,然后让你用代码片段作为搜索模式。听起来很学术?其实很直观。
想找所有 console.log 调用:
1ast-grep -p 'console.log($ARG)'
$ARG 是元变量,匹配任意参数。不是正则,是语法结构匹配。
这意味着:
- 注释里的
console.log不会被匹配 - 字符串里的也不会
- 跨行的调用?自动处理
第一次用的时候,我盯着干净的搜索结果愣了几秒。
代码解析:它看到的世界
要理解 ast-grep 为什么有效,得先看看它眼中的代码长什么样。
这段 JavaScript:
1function greet(name) {
2 console.log("Hello, " + name);
3}
在 Tree-sitter 解析后变成:
program
└── function_declaration
├── name: identifier "greet"
├── parameters: formal_parameters
│ └── identifier "name"
└── body: statement_block
└── expression_statement
└── call_expression
├── function: member_expression
│ ├── object: identifier "console"
│ └── property: property_identifier "log"
└── arguments
└── binary_expression
├── left: string "Hello, "
├── operator: "+"
└── right: identifier "name"
每个节点都有类型:function_declaration、call_expression、identifier。
当你写 console.log($ARG) 作为模式时,ast-grep 在树里找 call_expression,其中 function 是 console.log,arguments 可以是任意内容。
这就是为什么它能区分真正的调用和注释里的文本——注释在语法树里是 comment 节点,根本不会被 call_expression 模式匹配到。
和其他工具的对比
ripgrep:快,真的快。但它看到的是文本行,不是代码结构。ast-grep 看到的是语法树,它知道什么是函数、什么是类、什么是参数。代价是速度慢一点,但在大多数场景下仍然是亚秒级。
我的使用策略:先用 rg 快速扫一眼,需要精确匹配时切 ast-grep。
Semgrep:更偏安全审计,有污点分析、数据流追踪这些重型功能。ast-grep 更轻量,启动快,适合日常开发:找代码、写 lint 规则、批量重构。
如果你需要检测 SQL 注入的数据流,用 Semgrep。如果你想把所有 var 换成 const,ast-grep 更顺手。
真实案例:Bevy 0.14 迁移
Bevy 是 Rust 游戏引擎,0.14 版本有大量破坏性更新。社区用 ast-grep 写了迁移脚本。
一个典型的变更:Timer::from_seconds(duration, false) 改成 Timer::from_seconds(duration, TimerMode::Once)。
规则:
1id: timer-mode-migration
2language: rust
3rule:
4 pattern: Timer::from_seconds($DURATION, false)
5fix: Timer::from_seconds($DURATION, TimerMode::Once)
另一个:
1id: timer-mode-repeating
2language: rust
3rule:
4 pattern: Timer::from_seconds($DURATION, true)
5fix: Timer::from_seconds($DURATION, TimerMode::Repeating)
两条规则,几百个调用点,几分钟搞定。手动改?一天起步,还容易漏。
真实案例:React 19 forwardRef 迁移
React 19 改变了 ref 的传递方式。之前需要 forwardRef 包裹,现在直接作为 props 传递。
旧代码:
1const Input = forwardRef((props, ref) => {
2 return <input ref={ref} {...props} />;
3});
新代码:
1const Input = ({ ref, ...props }) => {
2 return <input ref={ref} {...props} />;
3};
迁移规则:
1id: remove-forwardref
2language: tsx
3rule:
4 pattern: forwardRef(($PROPS, $REF) => { $$$BODY })
5fix: |
6 ({ $REF, ...$PROPS }) => { $$$BODY }
$$$BODY 匹配函数体内的所有语句。一条规则覆盖整个代码库的 forwardRef 调用。
结构化文档的应用
ast-grep 不只能处理代码。Tree-sitter 支持 JSON、YAML、HTML,这打开了新的可能。
Kubernetes 配置审计 - 找所有没有指定镜像 tag 的 deployment:
1id: require-image-tag
2language: yaml
3rule:
4 kind: block_mapping_pair
5 has:
6 kind: flow_node
7 pattern: 'image: $IMG'
8 not:
9 has:
10 pattern: 'image: $IMG:$TAG'
11message: "镜像必须指定 tag,不要用 latest"
这比正则靠谱多了。正则会被 # image: nginx 这样的注释骗到,ast-grep 不会。
HTML 无障碍检查 - 找所有没有 alt 属性的 <img> 标签:
1id: img-alt-required
2language: html
3rule:
4 kind: element
5 has:
6 kind: tag_name
7 regex: "^img$"
8 not:
9 has:
10 kind: attribute
11 has:
12 kind: attribute_name
13 regex: "^alt$"
14message: "img 标签必须有 alt 属性"
Markdown 文档规范 - 找所有一级标题(确保文档只有一个 h1):
1id: single-h1
2language: markdown
3rule:
4 kind: atx_heading
5 has:
6 kind: atx_h1_marker
把这些规则放进 CI,文档规范自动落地。
局限性与思考
ast-grep 理解语法,但不理解语义。它不知道变量的类型,不追踪数据流,不做跨文件分析。需要这些能力?还是得用 TypeScript 的类型检查、Semgrep 的污点分析、或者专门的静态分析工具。
仔细思考后会自然想到:ast-grep 是否能很好地结合其他结构化分析工具?
我的工作流
- 日常搜索:
rg快速定位 - 需要精确匹配:
ast-grep -p 'pattern' - 批量修改:写 YAML 规则,
ast-grep scan --interactive逐个确认 - CI 检查:把规则提交到仓库,
ast-grep scan自动 lint
总结
ast-grep 不是要替代 grep 或者 Semgrep。它填补的是中间地带:比文本搜索更精确,比完整静态分析更轻量。在"需要结构化精度、又要保持 CLI 速度"的场景下,它是个好工具,想象空间也很大。
试试看:brew install ast-grep
参考资料