一键导入
ast-grep-practice
Use for ast-grep lint rules when general-purpose linters cannot express patterns. Covers config, rules, testing, CI integration.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Use for ast-grep lint rules when general-purpose linters cannot express patterns. Covers config, rules, testing, CI integration.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Use when adding/editing scripts under scripts/. Covers layout conventions, shell rules, output contracts, validation.
Use for TypeScript/ECMAScript compatibility changes. Defines decision order for semantic compatibility, WASI lowering, Node fallback.
Use when adding/editing docs under docs/. Enforces state separation between final-state docs, current-state, issues, and historical state.
Use when adding/editing/moving/renaming fixtures under fixtures/. Covers naming rules, content rules, inventory, reference updates.
Use when acting as gatekeeper/reviewer for PRs or agent outputs. Covers sources of truth, required commands, reject conditions, domain checklists.
Use when resolving git merge conflicts, especially after git pull --rebase or git merge origin/master
| name | ast-grep-practice |
| description | Use for ast-grep lint rules when general-purpose linters cannot express patterns. Covers config, rules, testing, CI integration. |
Complement general-purpose lint tools (ESLint, oxlint, Biome, clippy, etc.) with ast-grep for patterns they cannot express. Always prefer reproducible static rules over natural-language prompts.
Execute the commands below before claiming rules are integrated; on failure, fix or document blockers. Without mise, use mise with the same name. First time: mise trust (docs).
mise run check scripts (ensures all scripts/*.sh and mise still parse)mise run fmt and mise run nextest (you touched the repo, including rule tests under tests or rule-tests/)mise run check# 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 manager selection: If the project's package.json has packageManager set, follow it (pnpm / yarn / etc.). Otherwise install locally with npm. Use the same tool in CI (mixing them splits lockfile and binary resolution paths). Keep global installs on dev machines only; CI and in-repo scripts must always use local references.
Minimum configuration to verify operation:
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
fix when you can. Prefer rules with auto-fix over detection-only rules| Case | Tool |
|---|---|
| unused import, no-console, naming convention | ESLint / oxlint / Biome |
| type error, unreachable code | TypeScript compiler / clippy |
| formatting | Prettier / Biome / rustfmt |
| Forbidding a specific function-call pattern | ast-grep |
| Detecting and rewriting deprecated APIs | ast-grep (fix) |
| Forbidden pattern inside a specific context | ast-grep (inside/has) |
| Project-specific structural constraints | ast-grep |
Signs that ast-grep is the right choice:
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 runs every rule under ruleDirs starting from the directory that contains sgconfig.yml.
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"
| Field | Required | Description |
|---|---|---|
id | Yes | rule identifier |
language | Yes | target language |
rule | Yes | match condition |
severity | No | hint, warning, error |
message | No | one-line description |
note | No | detailed description / migration guide |
fix | No | auto-fix template |
constraints | No | additional constraints on metavariables |
transform | No | text transformation of metavariables |
files | No | target glob |
ignores | No | excluded glob |
url | No | documentation URL |
// ast-grep-ignore
someCode()
// ast-grep-ignore: no-direct-env-access
process.env.NODE_ENV
Pattern-matching caveats:
$OBJ.$PROP matches dot access only. It does not match obj['key'] (bracket access)$VAR matches exactly one AST node$$$VARS matches zero or more AST nodes (variadic arguments, multiple statements, etc.)$_ is a wildcard (does not capture). The same name can match different contentsobj.on$EVENT and "hello $NAME" do not workfix is convenient, but since it is applied automatically it can change semantics. Do not attach one (detection-only) when:
as any → as unknown changes type-inference results)When in doubt, skip fix and document the manual migration steps in note. Only attach fix when you are confident that "replacing everywhere is safe".
rule:
pattern: console.log($ARG)
fix: logger.info($ARG)
Metavariables are usable as-is inside the fix template. Unmatched metavariables become empty strings.
rule:
pattern: console.log($$$ARGS)
fix: ''
fix: '' deletes the matched node. Note that a blank line may remain. If you want to also remove trailing ; on statements or trailing commas, always combine with expandEnd (see "Range expansion" below). If leaving a blank line is acceptable, expandEnd is not needed. Rule of thumb: in projects where a formatter (Prettier etc.) runs after, blank lines get tidied automatically, so expandEnd is unnecessary; if no formatter runs, expandEnd is recommended.
any:If each branch under any: can use the same fix template, it is fine to consolidate into one rule:
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
If the fix differs per branch, always split into separate rules (you cannot write a per-branch fix inside any:). Example: === 0 → !some() and !== 0 → some() should be split. Splitting is acceptable even when the intent is the same (aligning the ids as *-empty / *-nonempty keeps things readable).
rule:
pattern: |
def foo($X):
$$$S
fix: |-
def bar($X):
$$$S
Indentation is preserved relative to the original code's position.
When you want to include the trailing comma etc. in the deletion:
fix:
template: ''
expandEnd:
regex: ','
ast-grep run --pattern 'oldFunc($$$ARGS)' --rewrite 'newFunc($$$ARGS)' --lang typescript .
# --update-all applies everywhere without confirmation
Add extra conditions to metavariables. Only $ARG is supported ($$$ARGS is not). It filters matches after the rule matches.
Choosing between constraints and structural constraints (has/inside/not):
constraints (e.g. $METHOD is one of get / set / delete)has / inside / not / precedes / follows (e.g. inside a specific parent, or having a specific child)pattern: new Set($X) guarantees Set is present)Writing pattern together with has / not directly under rule is evaluated as AND (matches pattern AND has is true). Use this shape to tack on structural constraints that pattern alone cannot express.
rule:
pattern: $OBJ.$METHOD($$$ARGS)
constraints:
METHOD:
regex: '^(get|set|delete)$'
OBJ:
kind: identifier
Usable fields: kind, regex, pattern
Note: constrained metavariables inside not may not behave as expected.
Textually transform matched metavariables before using them in fix.
transform:
NEW_NAME:
replace:
source: $NAME
replace: 'get(\w+)'
by: 'fetch$1'
fix: $NEW_NAME($$$ARGS)
transform:
INNER:
substring:
source: $STR
startChar: 1
endChar: -1
Negative indices count from the end. Same semantics as Python slicing.
transform:
SNAKE:
convert:
source: $NAME
toCase: snakeCase
separatedBy: [caseChange]
Supported cases: camelCase, snakeCase, kebabCase, pascalCase, upperCase, lowerCase, capitalize
Recursively rewrite nodes inside a metavariable using rewriter rules.
transform:
REWRITTEN:
rewrite:
source: $$$BODY
rewriters: [migrate-api-call]
joinBy: "\n"
Reference shared rules defined under utilDirs with matches.
# 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
There are two kinds of tests. Do not conflate them:
test --skip-snapshot-tests): only verifies that the code listed under valid / invalid is classified correctly. This is the one to run in CI.test / test -U): pins the match positions and fix results on invalid code as snapshots and detects regressions. Generate for the first time with -U, then have humans review afterwards. Run at least once before CI.The id inside the test file must match the id of the rule file. The filename is free (convention: {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
Test result markers:
. : passN : noisy (false positive — matches valid code)M : missing (false negative — fails to match invalid code)rule-tests/ (Red)rules/ (Green)ast-grep test --skip-snapshot-testsast-grep test -Uast-grep-test:
ast-grep test
ast-grep-lint:
ast-grep scan
check: format-check typecheck ast-grep-lint test
Align tools with the dev environment (use pnpm in CI if the project uses pnpm, npm if it uses npm):
- uses: actions/setup-node@v4
with: { node-version: 24, cache: npm } # for pnpm projects: pnpm/action-setup@v4 + 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
severity and exit codes:
ast-grep scan exits non-zero by default if at least one finding has error severity--error makes warning / hint also cause non-zero exit (use when CI should fail on warnings too)--error=error to tighten gradually--format json for structured output (for integrating with other tools)Kind names depend on the language's Tree-sitter grammar.
# 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
See references/kind-catalog.md for a per-language catalog of common kinds (covers TypeScript / Rust / Go / Python).
as any casts (detection only, no fix)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.
When matching a type assertion like as any, $EXPR as any works on the as_expression node. $EXPR matches the whole left-hand side, so it matches both JSON.parse(raw) as any and (value as any).
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
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.
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.
id: flag-unsafe-block
language: Rust
severity: warning
rule:
kind: unsafe_block
message: unsafe block. Explain the safety rationale in a comment.
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/**"
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.
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.
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.
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/**"
any: + fix consolidation/splitting, $$$ARGS empty match, etc.--error / exit codes / --format json