关于 ast-grep-practice
ast-grep-practice 是一项技能,指导开发者在通用 Linter 无法表达规则时,用 ast-grep 编写可复现的结构化代码规则,并涵盖测试、修复与 CI 集成。
主要功能
- 01判断何时使用 ast-grep
- 02配置结构化规则
- 03测试驱动开发规则
- 04谨慎添加自动修复
- 05集成 CI 扫描与测试
适用场景
- 01检查项目专属 AST 模式
- 02检测并迁移弃用 API
- 03在 CI 前测试自定义规则
- 04将结构检查加入 CI
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ mizchi/ skills --skill ast-grep-practice安装后在 Claude Code 中输入/ast-grep-practice确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
README.mdSKILL-ja.mdreferences/cli.mdkind-catalog.mdrule-yaml.mdtesting.md详细介绍
ast-grep 实践
使用 ast-grep 补充通用 lint 工具(ESLint、oxlint、Biome、clippy 等),处理这些工具无法表达的模式。始终优先采用可复现的静态规则,而不是自然语言提示。
安装
# npm (project-local recommended)
npm install -D @ast-grep/cli
npx ast-grep --help
# or cargo
cargo install ast-grep --locked
# or brew
brew install ast-grep
包管理器选择:如果项目的 package.json 设置了 packageManager,请遵循其设置(pnpm / yarn / 等)。否则,请使用 npm 在本地安装。在 CI 中使用相同的工具(混用会导致锁文件和二进制文件解析路径分散)。全局安装仅用于开发机器;CI 和仓库内脚本必须始终使用本地引用。
快速入门
用于验证是否正常运行的最小配置:
mkdir -p rules rule-tests
cat > sgconfig.yml << 'EOF'
ruleDirs:
- rules
testConfigs:
- testDir: rule-tests
EOF
cat > rules/no-console-log.yml << 'EOF'
id: no-console-log
language: TypeScript
severity: warning
rule:
pattern: console.log($$$ARGS)
message: Do not leave console.log behind.
fix: ''
EOF
cat > rule-tests/no-console-log-test.yml << 'EOF'
id: no-console-log
valid:
- logger.info('ok')
invalid:
- console.log('debug')
- "console.log('a', 'b')"
EOF
ast-grep test --skip-snapshot-tests # run tests
ast-grep scan src/ # scan project
原则
- 首先检查现有 lint 工具是否能覆盖该规则
- 需要“结构化模式”时使用 ast-grep
- 通过 TDD 开发规则:先编写测试 → 实现规则 → 集成到 CI
- 能写
fix时就写。优先选择带自动修复的规则,而不是仅检测规则
如何在 ast-grep 和现有 lint 工具之间做选择
| 情况 | 工具 |
|---|---|
| 未使用的导入、no-console、命名约定 | ESLint / oxlint / Biome |
| 类型错误、无法到达的代码 | TypeScript 编译器 / clippy |
| 格式化 | Prettier / Biome / rustfmt |
| 禁止特定的函数调用模式 | ast-grep |
| 检测并重写已弃用的 API | ast-grep(fix) |
| 在特定上下文中禁止某种模式 | ast-grep(inside/has) |
| 项目特定的结构约束 | ast-grep |
以下情况表明 ast-grep 是合适的选择:
- 无法通过配置现有规则来表达该规则
- 规则依赖 AST 中的父级 / 子级 / 兄弟节点关系
- 需要自动重写(迁移)
项目设置
sgconfig.yml
ruleDirs: # required: directories holding rule files
- rules
testConfigs: # optional: test configuration
- testDir: rule-tests
utilDirs: # optional: shared utility rules
- rule-utils
languageGlobs: # optional: mapping for non-standard extensions (unnecessary for TS/JS/Python etc.)
html: ['*.vue', '*.svelte', '*.astro']
目录结构
project/
sgconfig.yml
rules/
no-direct-env-access.yml
prefer-result-type.yml
rule-tests/
no-direct-env-access-test.yml
prefer-result-type-test.yml
__snapshots__/
rule-utils/
is-async-function.yml
ast-grep scan 会从包含 sgconfig.yml 的目录开始,运行 ruleDirs 下的所有规则。
规则文件结构
id: no-direct-env-access
language: TypeScript
severity: warning
rule:
pattern: process.env.$KEY
not:
inside:
kind: function_declaration
has:
pattern: getEnv
stopBy: end
message: Do not reference process.env directly. Go through getEnv().
note: Ensures type-safe access to environment variables.
fix: getEnv('$KEY')
files:
- "src/**"
ignores:
- "src/config.ts"
字段
| 字段 | 必填 | 描述 |
|---|---|---|
id | 是 | 规则标识符 |
language | 是 | 目标语言 |
rule | 是 | 匹配条件 |
severity | 否 | hint、warning、error |
message | 否 | 单行描述 |
note | 否 | 详细描述 / 迁移指南 |
fix | 否 | 自动修复模板 |
constraints | 否 | 对元变量的附加约束 |
transform | 否 | 对元变量进行文本转换 |
files | 否 | 目标 glob |
ignores | 否 | 排除的 glob |
url | 否 | 文档 URL |
抑制注释
// ast-grep-ignore
someCode()
// ast-grep-ignore: no-direct-env-access
process.env.NODE_ENV
元变量的注意事项
模式匹配须知:
$OBJ.$PROP只匹配点号访问,不匹配obj['key'](方括号访问)$VAR只匹配一个 AST 节点$$$VARS匹配零个或多个 AST 节点(可变参数、多个语句等)$_是通配符(不捕获内容)。相同名称可以匹配不同内容- 元变量必须占据完整节点:
obj.on$EVENT和"hello $NAME"均不起作用
fix(自动修复)
决定是否附加修复
fix 很方便,但由于它会自动应用,可能会改变语义。出现以下情况时,不要附加修复(仅检测):
- 重写会改变类型安全性(例如,
as any→as unknown会改变类型推断结果) - 可能会改变副作用或求值顺序(短路行为、异常发生时机)
- 由于依赖上下文,正确的替换方案并非唯一(例如,调整参数顺序的 API 迁移;必须进行审核)
- 删除操作会与同一语句中的其他表达式产生牵连
如果拿不准,就跳过 fix,并在 note 中记录手动迁移步骤。只有确信“在所有位置替换都是安全的”时才附加 fix。
基础
rule:
pattern: console.log($ARG)
fix: logger.info($ARG)
在 fix 模板中可以直接使用元变量。未匹配的元变量会变成空字符串。
删除
rule:
pattern: console.log($$$ARGS)
fix: ''
fix: '' 会删除匹配的节点。请注意,可能会留下空行。如果还想删除语句末尾的 ; 或尾随逗号,务必与 expandEnd 配合使用(见下文“范围扩展”)。如果可以接受留下空行,则不需要 expandEnd。经验法则:在后续会运行格式化工具(如 Prettier 等)的项目中,空行会被自动整理,因此不需要 expandEnd;如果不会运行格式化工具,则建议使用 expandEnd。
使用 any: 合并多个模式时的修复
如果 any: 下的每个分支都可以使用相同的修复模板,就可以合并为一条规则:
rule:
any:
- pattern: $ARR.filter($P).length === 0
- pattern: $ARR.filter($P).length == 0
fix: '!$ARR.some($P)' # metavariables shared across both branches + identical template
如果各分支的修复不同,务必拆分为单独的规则(不能在 any: 中为每个分支编写单独的修复)。例如,=== 0 → !some() 和 !== 0 → some() 应拆分。即使意图相同,拆分也可以接受(将 id 对齐为 *-empty / *-nonempty 可提高可读性)。
多行
rule:
pattern: |
def foo($X):
$$$S
fix: |-
def bar($X):
$$$S
缩进相对于原代码中的位置保持不变。
范围扩展(FixConfig)
如果想在删除时包含尾随逗号等内容:
fix:
template: ''
expandEnd:
regex: ','
使用 CLI 快速重写
ast-grep run --pattern 'oldFunc($$$ARGS)' --rewrite 'newFunc($$$ARGS)' --lang typescript .
# --update-all applies everywhere without confirmation
constraints
为元变量添加额外条件。仅支持 $ARG(不支持 $$$ARGS)。规则匹配后,会根据这些条件筛选匹配项。
在 constraints 和结构约束(has/inside/not)之间进行选择:
- 想要约束元变量的内容 → 使用
constraints(例如,$METHOD是get/set/delete之一) - 想要约束模式外部或内部的结构 → 使用
has/inside/not/precedes/follows(例如,位于特定父节点内,或包含特定子节点) - 如果可以直接在模式中写出具体字面值,这是最简单的方式(
pattern: new Set($X)可确保存在 Set)
在 rule 下直接同时编写 pattern 和 has / not 时,会按 AND 进行求值(匹配模式 AND has 为 true)。使用这种结构可以添加 pattern 单独无法表达的结构约束。
rule:
pattern: $OBJ.$METHOD($$$ARGS)
constraints:
METHOD:
regex: '^(get|set|delete)$'
OBJ:
kind: identifier
可用字段:kind、regex、pattern
注意:not 中受约束的元变量可能无法按预期运行。
transform
在 fix 中使用匹配到的元变量之前,对其进行文本转换。
replace(正则替换)
transform:
NEW_NAME:
replace:
source: $NAME
replace: 'get(\w+)'
by: 'fetch$1'
fix: $NEW_NAME($$$ARGS)
substring
transform:
INNER:
substring:
source: $STR
startChar: 1
endChar: -1
负索引从末尾开始计数。语义与 Python 切片相同。
convert(大小写转换)
transform:
SNAKE:
convert:
source: $NAME
toCase: snakeCase
separatedBy: [caseChange]
支持的格式:camelCase、snakeCase、kebabCase、pascalCase、upperCase、lowerCase、capitalize
rewrite(实验性)
使用重写规则递归重写元变量中的节点。
transform:
REWRITTEN:
rewrite:
source: $$$BODY
rewriters: [migrate-api-call]
joinBy: "\n"
utils(工具规则)
使用 matches 引用 utilDirs 下定义的共享规则。
# rule-utils/is-async-function.yml
id: is-async-function
language: TypeScript
rule:
any:
- kind: function_declaration
has:
field: async
regex: async
- kind: arrow_function
has:
field: async
regex: async
# rules/async-no-try-catch.yml
id: async-no-try-catch
language: TypeScript
rule:
all:
- matches: is-async-function
- has:
pattern: await $EXPR
stopBy: end
- not:
has:
kind: try_statement
stopBy: end
message: async function lacks try-catch.
severity: warning
测试
测试分为两种,不要混为一谈:
- 分类测试(
test --skip-snapshot-tests):仅验证valid/invalid下列出的代码是否被正确分类。这是在 CI 中运行的测试。 - 快照测试(
test/test -U):将无效代码中的匹配位置和修复结果保存为快照,以检测回归。首次使用-U生成快照,之后由人工审核。提交 CI 前至少运行一次。
测试文件格式
测试文件中的 id 必须与规则文件的 id 相同。文件名可自行决定(惯例:{rule-id}-test.yml)。
# rule-tests/no-direct-env-access-test.yml
id: no-direct-env-access
valid:
- getEnv('NODE_ENV')
- "function setup() { return getEnv('PORT') }"
invalid:
- process.env.NODE_ENV
- process.env.PORT
运行测试
# classification test (is valid/invalid correct?)
ast-grep test --skip-snapshot-tests
# generate / update snapshots
ast-grep test -U
# interactive snapshot review
ast-grep test --interactive
测试结果标记:
.:通过N:有噪声(误报——匹配到有效代码)M:缺失(漏报——未能匹配到无效代码)
工作流
- 在
rule-tests/下编写测试文件(Red) - 在
rules/下编写规则(Green) - 使用
ast-grep test --skip-snapshot-tests验证 - 使用
ast-grep test -U生成快照 - 审核快照并提交
CI 集成
justfile
ast-grep-test:
ast-grep test
ast-grep-lint:
ast-grep scan
check: format-check typecheck ast-grep-lint test
GitHub Actions
使工具与开发环境保持一致(如果项目使用 pnpm,则在 CI 中使用 pnpm;如果使用 npm,则使用 npm):
- uses: actions/setup-node@v7
with: { node-version: 24, cache: npm } # for pnpm projects: pnpm/action-setup@v6 + cache: pnpm
- run: npm ci # for pnpm: pnpm install --frozen-lockfile
- name: ast-grep rule tests
run: npx ast-grep test --skip-snapshot-tests
- name: ast-grep scan
run: npx ast-grep scan --error
严重性和退出代码:
- 默认情况下,如果至少有一项发现的严重性为
error,ast-grep scan会以非零状态退出 - 传入
--error后,warning/hint也会导致非零状态退出(当 CI 也应在出现警告时失败时使用) - 可以指定严重性,例如
--error=error,逐步收紧规则 --format json可生成结构化输出(用于与其他工具集成)
查找 kind 名称
kind 名称取决于语言的 Tree-sitter 语法。
# AST dump (named nodes only, use these when writing rules)
ast-grep run --pattern 'YOUR_CODE' --lang typescript --debug-query=ast
# CST dump (all nodes, including anonymous tokens)
ast-grep run --pattern 'YOUR_CODE' --lang typescript --debug-query=cst
请参阅 references/kind-catalog.md,了解各语言常见 kind 的目录(涵盖 TypeScript / Rust / Go / Python)。
实用规则示例
TypeScript:禁止 as any 类型转换(仅检测,不修复)
id: no-as-any
language: TypeScript
severity: error
rule:
pattern: $EXPR as any
message: as any disables the type system. Go through as unknown or a type guard.
note: |
Why no auto-fix: mechanically replacing `as any` → `as unknown` changes type
inference results and introduces new compile errors at call sites. Detection-only,
migrate manually.
匹配 as any 这样的类型断言时,$EXPR as any 可作用于 as_expression 节点。$EXPR 会匹配整个左侧部分,因此既能匹配 JSON.parse(raw) as any,也能匹配 (value as any)。
TypeScript:替换已弃用的 API
id: migrate-old-api
language: TypeScript
severity: error
rule:
pattern: oldClient.fetch($URL, $OPTS)
fix: newClient.request($URL, $OPTS)
message: oldClient.fetch is deprecated. Migrate to newClient.request.
禁止特定导入
id: no-lodash-import
language: TypeScript
severity: warning
rule:
pattern: import $_ from 'lodash'
message: Do not import lodash wholesale. Use lodash/xxx.
fix: import $_ from 'lodash/xxx' // TODO: fix the correct path
TypeScript:禁止在 React 组件中直接调用 fetch
id: no-fetch-in-component
language: TypeScript
severity: warning
rule:
pattern: fetch($$$ARGS)
inside:
any:
- kind: function_declaration
has:
field: return_type
pattern: JSX.Element
- kind: arrow_function
inside:
kind: variable_declarator
regex: '^[A-Z]'
stopBy: end
message: Do not fetch directly inside a component. Use hooks or a server action.
Rust:禁止使用 unwrap()
id: no-unwrap
language: Rust
severity: warning
rule:
pattern: $EXPR.unwrap()
not:
inside:
kind: function_item
regex: '#\[test\]'
stopBy: end
message: Do not use unwrap() outside tests. Use ? or expect().
note: unwrap() panics, so avoid it in production code.
Rust:标记 unsafe 代码块
id: flag-unsafe-block
language: Rust
severity: warning
rule:
kind: unsafe_block
message: unsafe block. Explain the safety rationale in a comment.
Rust:将 println! 迁移到日志宏
id: no-println-in-lib
language: Rust
severity: warning
rule:
pattern: println!($$$ARGS)
not:
inside:
kind: function_item
regex: 'fn main'
stopBy: end
message: Do not use println! in library code. Use log::info! etc.
fix: log::info!($$$ARGS)
files:
- "src/lib.rs"
- "src/**/mod.rs"
- "src/**/*.rs"
ignores:
- "src/main.rs"
- "src/bin/**"
Go:检测被忽略的错误
id: no-ignored-error
language: Go
severity: error
rule:
kind: short_var_declaration
has:
kind: identifier
regex: '^_$'
field: left
has:
kind: call_expression
field: right
stopBy: end
message: Do not ignore errors with _. Handle them appropriately.
Go:防止遗漏 defer Close()
id: defer-close-after-open
language: Go
severity: warning
rule:
kind: short_var_declaration
has:
pattern: os.Open($PATH)
field: right
stopBy: end
not:
precedes:
pattern: defer $_.Close()
stopBy:
kind: return_statement
message: Add defer Close() immediately after os.Open.
Python:禁止裸 except
id: no-bare-except
language: Python
severity: warning
rule:
kind: except_clause
not:
has:
kind: identifier
stopBy: neighbor
message: Do not use bare except. Specify the exception type.
Python:将 print() 迁移到日志记录器
id: no-print-in-src
language: Python
severity: warning
rule:
pattern: print($$$ARGS)
not:
inside:
kind: function_definition
regex: 'def main'
stopBy: end
message: Use logger instead of print().
fix: logger.info($$$ARGS)
files:
- "src/**"
参考资料
技能内详情
- references/rule-yaml.md — 所有规则 YAML 字段、求值顺序、元变量绑定范围、
any:+ 修复合并/拆分、$$$ARGS空匹配等 - references/testing.md — 分类测试与快照测试、多行代码表示法、快照操作
- references/cli.md — 常用子命令和标志、
--error/ 退出代码 /--format json - references/kind-catalog.md — 各语言 kind 目录(TypeScript / Rust / Go / Python)